之前对关键字 final 和 static 的具体使用场景很是模糊,什么时候使用 final,什么时候使用 static,当使用它们的时候会产生什么效果。本文将对 final 和 static 进行详细介绍,并给出一些需要注意的细节。
final
final 意为最终的、不可更改的,它可以用来修饰类、方法、属性、局部变量等。
final 修饰类
被 final 修改的类不能被其它类所继承,如 Integer、String 类,都是被 final 修饰的。因此被 final 修饰的类中,其成员方法是没有机会被覆盖的。但需要注意的是:final 类中所有的成员方法都会被隐式的指定为 final 方法。
其好处在于:对于不可变类(被 final 修饰的类)来说,它们的对象是只读的,在多线程环境下会安全的共享,不用外的开销。如图所示:
final 修饰方法
如果一个类不允许其子类重写某个方法,则可以将这个方法声明为 final 方法。而类中的所有 private 方法都被隐式的指定为 final。
重写(Override)和重载(Overload)的区别:
- 重写是指子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。即外壳不变,内容重写。
- 重载是指在同一个类里面,方法名字相同,而参数不同,返回值类型可以相同也可以不同。
使用 final 方法的原因有两个:
- 为了将方法锁定,以防止任何继承类修改它的含义;
- 为了效率,将一个方法声明为 final,就是同意编译器将针对该方法的所有调用都转化为
内嵌调用
,就相当于在编译的时候已经静态绑定了,不需要在运行时再动态绑定。但是,如果方法过于庞大,可能看不到内嵌调用
带来的性能提升。因此,在最近的 Java 版本中,不需要使用 final 方法进行这些优化了。
如下图所示,子类的 m2() 方法不能对父类中被 final 修饰的 m2() 方法进行重写:
因此,如果想禁止某方法在子类中被覆盖,则需要将该方法声明为 final。
如下示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class FinalAndStaticTest {
public void m1() {
System.out.println("父类中普通的 m1 方法");
}
public final void m2() {
System.out.println("父类中被 final 修饰的 m2 方法");
}
}
class FinalTest extends FinalAndStaticTest {
public void m1() {
System.out.println("子类中重写了父类中普通的 m1 方法");
}
public static void main(String[] args) {
FinalTest ft = new FinalTest();
ft.m1(); // 子类重写了父类中普通的 m1() 方法
ft.m2(); // 调用了从父类继承过来的 final m2() 方法
}
}
|
输出结果如下:
1
2
|
子类中重写了父类中普通的 m1 方法
父类中被 final 修饰的 m2 方法
|
final 修饰变量
修饰变量是 final 使用最多的地方,程序中有时需要将某些数据规定为固定不变的,这种类型的变量只能被赋值一次,一旦赋值之后就不能再改变了。例如:
- 一个永不可变的编译时常量;
- 一个在运行时被初始化的值,而在程序处理的过程中不希望它被改变。
需要注意的是,对于一个被 final 修饰的变量:
- 如果该变量是基本数据类型,则其数值一旦初始化之后就不能被更改;
- 如果该变量是引用类型的变量,则对其初始化之后该变量的引用地址不可变,但地址中的内容是可以改变的。
- 也就是说,当 final 用于对象时,final 会使该对象的引用恒定不变,一旦引用指向了一个对象,就无法再把它改为指向另一个对象。
如下示例:
可以看出,对于变量 A 来说,不能再进行f.A = 1;
赋值操作。对于变量 B 来说,必须在初始化的时候就对其进行赋值。
注意,被static final
修饰的变量 A 可以通过类名
直接访问;而对于被static final
修饰的方法来说,也可以使用类名
直接访问到。
final 修饰方法的形参
假如一个方法中的形参被 final 所修饰,则当我们调用此方法时,给定其一个实参,一旦赋值以后就只能在方法体内使用此形参,但不能进行重新赋值。
1
2
3
4
|
public void show(final int NUM) {
// NUM = 10; 不能再次修改被 final 修饰的形参
Syetem.out.println(NUM); // 可以进行读操作
}
|
final 小结
- JVM 和 Java 应用程序会缓存 final 变量,从而提高性能;
- final 变量可以安全的在多线程环境下进行共享,而不需要额外的开销;
- 在匿名类中所有变量都必须是 final 变量;
- final 方法不能被重写;
- final 类不能被继承;
- final 成员变量必须在声明的时候进行初始化或在构造器中进行初始化,否则编译报错;
- 接口中声明的所有变量本身是 final 的;
- final 方法在编译阶段称为静态绑定;
- 对于集合对象声明为 final 的话,指的是其引用不能更改,但还是可以向集合中添加或删除元素的。
static
当我们在创建一个类的时候,其实就是在描述其对象的属性和行为,并没有产生实质上的对象,只有通过 new 关键字才会产生出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。
有时候希望在无论产生了多少对象的情况下,我希望某些特定的数据在内存空间里只有一份,即我只想为某一特定区域分配单一存储空间,而不用去考虑它创建了多少对象。此外,我希望某个方法不与包含它的类的任何对象联系在一起,即使没有创建对象,也能够调用这个方法,此时可以使用 static 关键字。
static 可以用来修饰类的成员变量、类的成员方法以及代码块,其主要的目的就是让多个对象共享一些存储空间。因此,被 static 修饰的方法或变量不需要依赖对象来进行访问,只要类被加载了,就可以通过使用类名
的方式去访问。例如类名.变量名
或类名.方法名()
。
static 修饰变量
按照声明的位置不同,变量的分类可分为两种:
- 成员变量:位于类的内部以及方法体之外。按照是否是静态的,可以将成员变量分为两种:
- 被 static 修饰的变量称为
静态变量
或类变量
;
- 不被 static 修饰的变量称为
实例变量
。
- 局部变量:位于方法体内部。其中,局部变量可以分为三种:
- 在方法中、构造器中定义的变量称为
形参
;
- 在方法内部定义的变量称为
方法局部变量
;
- 在代码块内定义的变量称为
代码块局部变量
。
两者在初始化值方面的异同:
- 相同点:都有生命周期;
- 不同点:局部变量除了形参外,都需要显式初始化。
对于静态变量
,其在内存中只有一份拷贝,JVM 只为静态变量分配一次内存,在加载类的过程中完成静态变量的内存分配。而对于实例变量
来说,每创建一个实例,就会为实例变量分配一次内存,实例变量在内存中可以有多份拷贝,互不影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class StaticTest {
public static int i;
public int j;
StaticTest(int j) {
this.j = j;
}
public static void main(String[] args) {
System.out.println(StaticTest.i); // 输出 0
}
}
|
此外,静态变量的初始化顺序是按照所定义的顺序进行初始化的。
static 修饰方法
被 static 修饰的方法称为静态方法
或类方法
,随着类的加载而加载,可通过使用类名.静态方法名()
的方式进行调用。在静态方法中,只能调用静态的方法或变量;而在非静态方法中,既可以调用非静态的方法或变量,也可以调用静态的方法和变量。因此,在静态方法内,不能使用this
、super
关键字。
1
2
3
4
5
6
7
8
9
10
11
|
public class StaticMethodTest {
static int sum (int x, int y) {
return x + y;
}
public static void main(String[] args) {
int sum = StaticMethodTest.sum(1, 2);
System.out.println(sum); // 输出 3
}
}
|
static 修饰代码块
被 static 修饰的代码块称为静态代码块
,JVM 在加载类时会执行静态代码块,如果有多个静态代码块,则按照顺序执行,每个代码块只会被执行一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class StaticBlockTest {
public static int i;
static {
i = 1;
System.out.println("静态代码块中的 i: " + i);
}
public static void main(String[] args) {
StaticBlockTest test = new StaticBlockTest();
System.out.println("main 方法中的 i: " + test.i);
StaticBlockTest test1 = new StaticBlockTest();
}
}
|
输出结果如下:
1
2
|
静态代码块中的 i: 1
main 方法中的 i: 1
|
可以看出,由于静态代码块是在类的初始化阶段完成的,因此在创建类的第二个对象test1
的时候,该类的静态代码块就不会执行了。
static 修饰内部类
非静态内部类依赖于外部类的实例,也就是说:需要先创建外部类的实例,才能用这个实例去创建非静态内部类。而静态内部类不需要,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class OuterClass {
class InnerClass {
}
static class StaticInnerClass {
}
public static void main(String[] args) {
// 下面这行被注释的代码是错误的,因为非静态内部类依赖于外部类的实例
// InnerClass innerClass = new InnerClass();
// 首先需要创建外部类的实例,然后用这个实例去创建非静态内部类
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
// 而静态内部类就厉害了,人家自己就可以创建对象,而不用管外部类
StaticInnerClass staticInnerClass = new StaticInnerClass();
}
}
|
此外,静态内部类不能访问外部类的非静态变量和方法。
static 小结
- 一个类的静态方法只能访问其静态变量;
- 一个类的静态方法不能直接调用非静态方法;
- 静态方法中不存在当前对象,所以不能使用 this 和 super;
- 静态方法不能被非静态方法覆盖;
- 构造方法不能被 static 修饰;
- 局部变量不能被 static 修饰。
代码块的访问顺序
不使用 static 修饰的非静态代码块相当于对构造器的补充,用于创建对象时给对象进行初始化,在构造器之前执行。而使用 static 修饰的静态代码块负责对类进行初始化,因此静态代码块是在类初始化阶段就执行。因为静态代码块是在类的初始化阶段完成的,因此在创建某个类的第二个对象时,该类的静态代码块就不会执行了。下面通过一个例子,观察一下非静态代码块
、静态代码块
、构造器
之间执行的顺序:
对于单个类的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
public class CodeBlockTest {
CodeBlockTest() {
System.out.println("CodeBlockTest 的构造器.");
}
{
System.out.println("CodeBlockTest 的非静态代码块 1");
}
{
System.out.println("CodeBlockTest 的非静态代码块 2");
}
static {
System.out.println("CodeBlockTest 的静态代码块 1");
}
static {
System.out.println("CodeBlockTest 的静态代码块 2");
}
}
class Demo {
public static void main(String[] args) {
CodeBlockTest test1 = new CodeBlockTest();
System.out.println("====================");
CodeBlockTest test2 = new CodeBlockTest();
}
}
|
输出结果如下,由于静态代码块是随着类的加载而加载的,在创建第二个对象的test2
时候,静态代码块就不会再创建了。
1
2
3
4
5
6
7
8
9
|
CodeBlockTest 的静态代码块 1
CodeBlockTest 的静态代码块 2
CodeBlockTest 的非静态代码块 1
CodeBlockTest 的非静态代码块 2
CodeBlockTest 的构造器.
====================
CodeBlockTest 的非静态代码块 1
CodeBlockTest 的非静态代码块 2
CodeBlockTest 的构造器.
|
对于多个类(继承)的情况:
对于继承的情况,总体上也是按照静态代码块
->非静态代码块
->构造器
的顺序进行输出的,但由于涉及到了父类
和子类
的情况,详细的输出流程应该是:
- 先后执行父类 A 的静态代码块、父类 B 的静态代码块、子类的静态代码块;
- 然后再执行父类 A 的非静态代码块和构造器,父类 B 的非静态代码块和构造器,最后执行子类的非静态代码块和构造器。
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
class A {
A() {
System.out.println("A 的构造器.");
}
{
System.out.println("A 的非静态代码块 1.");
}
{
System.out.println("A 的非静态代码块 2.");
}
static {
System.out.println("A 的静态代码块 1.");
}
static {
System.out.println("A 的静态代码块 2.");
}
}
class B extends A {
B() {
System.out.println("B 的构造器.");
}
{
System.out.println("B 的非静态代码块 1.");
}
{
System.out.println("B 的非静态代码块 2.");
}
static {
System.out.println("B 的静态代码块 1.");
}
static {
System.out.println("B 的静态代码块 2.");
}
}
class C extends B {
C() {
System.out.println("C 的构造器.");
}
{
System.out.println("C 的非静态代码块 1.");
}
{
System.out.println("C 的非静态代码块 2.");
}
static {
System.out.println("C 的静态代码块 1.");
}
static {
System.out.println("C 的静态代码块 2.");
}
}
public class Test {
public static void main(String[] args) {
C c1 = new C();
System.out.println("============");
C c2 = new C();
}
}
|
输出结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
A 的静态代码块 1.
A 的静态代码块 2.
B 的静态代码块 1.
B 的静态代码块 2.
C 的静态代码块 1.
C 的静态代码块 2.
A 的非静态代码块 1.
A 的非静态代码块 2.
A 的构造器.
B 的非静态代码块 1.
B 的非静态代码块 2.
B 的构造器.
C 的非静态代码块 1.
C 的非静态代码块 2.
C 的构造器.
============
A 的非静态代码块 1.
A 的非静态代码块 2.
A 的构造器.
B 的非静态代码块 1.
B 的非静态代码块 2.
B 的构造器.
C 的非静态代码块 1.
C 的非静态代码块 2.
C 的构造器.
|
参考
https://www.cnblogs.com/dolphin0520/p/3736238.html
https://matt33.com/2015/10/17/The-keyword-of-java/
https://www.cnblogs.com/dolphin0520/p/3799052.html