理解Java中的final和static关键字

时间:2021-10-11 00:39:05

回顾这两个关键字前,先考虑一个问题:

Static变量存储在JVM中的位置,或者说static变量是如何被加载的?

JVM会把类的静态方法和静态变量在类加载的过程中读入方法区(Method Area),相当于常驻内存,
如果一个方法或者变量声明为static,可以节约内存,不必要为每个对象实例化的时候分配内存。

1.final关键字

根据程序上下文环境,Java关键字final有“这是无法改变的”或者“终态的”含义,
它可以修饰非抽象类、非抽象类成员方法和变量。
final类不能被继承,没有子类,final类中的方法默认是final的;
final方法不能被子类的方法覆盖,但可以被继承
final成员变量表示常量,只能被赋值一次,赋值后值不再改变;
注意,final不能用于修饰构造方法;
父类的private方法是不能被子类方法访问和覆盖的,因此private类型的方法默认是final类型的,也就是说编译器对final方法和private方法做的优化是一样的。

(1)final类
final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final的。在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会载被扩展,那么就设计为final类。
典型的如JDK中的String,StringBuffer和StringBuilder。

(2)final方法
如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法。
使用final方法的原因有二:
把方法锁定,防止任何继承类修改它的意义和实现;
高效。编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。
注意和private方法的区分,private方法不可以在子类实例中访问,final可以在子类实例中直接调用,但是不能覆盖修改。

(3)final变量(常量)
用final修饰的成员变量表示常量,值一旦给定就无法改变!
final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。

(4)final参数
当函数参数为final类型时,你可以读取使用该参数,但是无法改变该参数的值。

12345678910 public class FinalParam {    public static void main(String[] args){        FinalParam test=new FinalParam();        test.change(10);    }    public void change(final int i){//      i++; 编译报错        System.out.print(i);    }}

  

2.static关键字

static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块。

(1)JVM对static关键字的处理

被static修饰的成员变量和成员方法独立于该类的任何对象。它不依赖类特定的实例,被类的所有实例共享。
只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象。

(2)static变量

按照是否静态的对类成员变量进行分类可分两种:一种是被static修饰的变量,叫静态变量或类变量;另一种是没有被static修饰的变量,叫实例变量。两者的区别是:
对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。
对于实例变量,没创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响(灵活)。

(3)static静态方法

静态方法可以直接通过类名调用,任何的实例也都可以调用,因此静态方法中不能用this和super关键字,不能直接访问所属类的实例变量和实例方法(就是不带static的成员变量和成员成员方法),只能访问所属类的静态成员变量和成员方法。因为实例成员与特定的对象关联。
因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。

(4)static代码块

static代码块也叫静态代码块,是在类中独立于类成员的static语句块,可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块。
如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。

12345678910111213141516171819202122232425262728293031 public class StaticArea {     private static int a;    private int b;     static {        StaticArea.a = 1;        System.out.println(a);        StaticArea temp = new StaticArea();        temp.f();        temp.b = 1000;        System.out.println(temp.b);    }     static {        StaticArea.a = 2;        System.out.println(a);    }     public static void main(String[] args) {    }     static {        StaticArea.a = 3;        System.out.println(a);    }     public void f() {        System.out.println("执行实例中方法");    }}

输出:

1       
执行实例中方法        
1000      
2      
3      

3.同时使用static和final关键字

static final用来修饰成员变量和成员方法,可简单理解为“全局常量”,
对于变量,表示一旦给值就不可修改,并且通过类名可以访问。
对于方法,表示不可覆盖,并且可以通过类名直接访问。

4.JVM对final常量的优化

摘自知乎问题 JVM对于声明为final的局部变量(local var)做了哪些性能优化?

以下代码:
static int foo() {
int a = someValueA();
int b = someValueB();
return a + b; // 这里访问局部变量
}
与带final的版本,
static int foo() {
final int a = someValueA();
final int b = someValueB();
return a + b; // 这里访问局部变量
}
效果一模一样,由javac编译得到的字节码会是这样:
invokestatic someValueA:()I
istore_0 // 设置a的值
invokestatic someValueB:()I
istore_1 // 设置b的值
iload_0 // 读取a的值
iload_1 // 读取b的值
iadd
ireturn

字节码里没有任何东西能体现出局部变量的final与否,Class文件里除字节码(Code属性)外的辅助数据结构也没有记录任何体现final的信息。既然带不带final的局部变量在编译到Class文件后都一样了,其访问效率必然一样高,JVM不可能有办法知道什么局部变量原本是用final修饰来声明的。

但有一个例外,那就是声明的“局部变量”并不是一个变量,而是编译时常量的情况:
static int foo2() {
final int a = 2; // 声明常量a
final int b = 3; // 声明常量b
return a + b; // 常量表达式
}
这样的话实际上a和b都不是变量,而是编译时常量,在Java语言规范里称为constant variable。
Chapter 4. Types, Values, and Variables
其访问会按照Java语言对常量表达式的规定而做常量折叠。
Chapter 15. Expressions
实际效果跟这样的代码一样:
static int foo3() {
return 5;
}
由javac编译得到对应的字节码会是:
iconst_5 // 常量折叠了,没有“访问局部变量”
ireturn

而这种情况如果去掉final修饰,那么a和b就会被看作普通的局部变量而不是常量表达式,在字节码层面上的效果会不一样
static int foo4() {
int a = 2;
int b = 3;
return a + b;
}
就会编译为:
iconst_2
istore_0 // 设置a的值
iconst_3
istore_1 // 设置b的值
iload_0 // 读取a的值
iload_1 // 读取b的值
iadd
ireturn

但其实这种层面上的差异只对比较简易的JVM影响较大,因为这样的VM对解释器的依赖较大,原本Class文件里的字节码是怎样的它就怎么执行;对高性能的JVM(例如HotSpot、J9等)则没啥影响。这种程度的差异在经过好的JIT编译器处理后又会被消除掉,上例中无论是 foo3() 还是 foo4() 经过JIT编译都一样能被折叠为常量5。

 

参考:
Final关键字对JVM类加载器的影响