之前对关键字 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 修饰的方法称为静态方法类方法,随着类的加载而加载,可通过使用类名.静态方法名()的方式进行调用。在静态方法中,只能调用静态的方法或变量;而在非静态方法中,既可以调用非静态的方法或变量,也可以调用静态的方法和变量。因此,在静态方法内,不能使用thissuper关键字。

 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