final是Java中的一个重要的关键字
第一次对这个final关键字有好奇是在一次调用回调方法时,使用匿名类作为参数时,匿名类中的方法参数列表中的参数由final修饰。
在Java中final可以用来修饰类,方法和字段上。
一.修饰类
我们看String类的源码可以发现,String就是一个由final修饰的类。
当一个类被final修饰时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。
二.修饰方法
被final修饰的方法不能被重写。
所有的private方法都默认为final。
三.修饰变量
当final修饰基本类型时,其数值一旦在初始化之后便不能更改。
当final修饰普通类型时,一旦该引用指向了一个对象,便该引用不能再指向另一对象,也就是说该引用指向的堆内存空间地址是一个固定值,不能改变。
1.final变量和普通变量
我们通过下面一个例子看看final变量和普通变量的区别。
@Test
public void test() {
String s1 = "kobe24";
final String s2 = "kobe";
String s3 = s2 + 24;
String s4 = "kobe";
String s5 = s4 + 24;
System.out.println(s1 == s3);
System.out.println(s1 == s5);
}
输出结果
true
false
我们发现final变量和普通变量输出的结果不同。
当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。这种和C语言中的宏替换有点像。因此在上面的一段代码中,由于变量s2被final修饰,因此会被当做编译器常量,所以在使用到s2的地方会直接将变量s2 替换为它指向的值。而对于变量d的访问却需要在运行时通过链接来进行。不过要注意,只有在编译期间能确切知道final变量值的情况下,编译器才会进行这样的优化。
2.final和static修饰变量的区别
class MyClass {
public static double staticValue = Math.random();
public final double finalValue = Math.random();
}
@Test
public void testdifferent() {
MyClass myClass1 = new MyClass();
MyClass myClass2 = new MyClass();
System.out.println(myClass1.staticValue);
System.out.println(myClass1.finalValue);
System.out.println(myClass2.staticValue);
System.out.println(myClass2.finalValue);
}
0.4193498037768091
0.9681006311453694
0.4193498037768091
0.6924483252540155
我们发现两次的staticValue没变,而两次的finalValue改变了。
staticValue不变是因为静态变量位于内存(位于方法区)中,始终只有一份这个引用的副本,所有线程共享。在上述代码中,执行 MyClass myClass1 = new MyClass();这句代码时,staticValue完成初始化,使用Math.random()获取值,并将该值放入方法区中,staticValue指向该值存放的地址,所以第一次调用的staticValue的时候是直接去访问调用方法区的数值,第二次也是。
而访问finalValue时,虽然说final指向的对象的地址不能变,但是并不意味着final修饰的变量指向的对象的内容不能变,所以两次的结果会不一样。
2)再来看看由final static共同修饰的变量
首先,我们知道在接口中可以存在“变量”,但是这个“变量“必须由final static 修饰。为什么呢?
笼统的讲,因为由final static 修饰的变量实际上就是编译期常量,是在编译的时候就被赋值确定了的,而接口又不能初始化,而不由final static修饰的变量是需要在类的初始化阶段正式赋值的,所以接口中不允许定义这样的变量。
我们了解了final static修饰的变量的初始化时期,那让我们看看下面的代码。
class Super {
public final static int Value = 24;
static {
System.out.println("super 静态语句块");
}
public Super() {
System.out.println("Super Constructor");
}
}
class Father extends Super {
public final static int Value = 23;
static {
System.out.println("Father 静态语句块");
}
public Father() {
System.out.println("Father Constructor");
}
}
class Son extends Father {
static {
System.out.println("Son 静态语句块");
}
public Son() {
System.out.println("Son Constructor");
}
}
public class FinalStatic {
public static void main(String[] args) {
System.out.println(Father.Value);
}
}
23
只输出了Fahter.Value的值,并没有完成类的初始化工作。
因为常量在编译阶段会存入调用它的类的常量池中,本质上没有直接引用到定义该常量的类,因此不会触发定义常量的类的初始化
虽然程序中引用了Father类的常量Value,但是在编译阶段将此常量的值23存储到了调用它的类的常量池中,对常量Fahter.Value的引用实际上转化为了FinalTest类对自身常量池的引用。也就是说,实际上FinalStatic 的Class文件之中并没有FinalStatic 类的符号引用入口,这两个类在编译成Class文件后就不存在任何联系了。
现在我们将final修饰符去掉,只留下static,将Father.Value,改为Son.Value
class Super {
public static int Value = 24;
static {
System.out.println("super 静态语句块");
}
public Super() {
System.out.println("Super Constructor");
}
}
class Father extends Super {
public static int Value = 23;
static {
System.out.println("Father 静态语句块");
}
public Father() {
System.out.println("Father Constructor");
}
}
public class FinalStatic {
public static void main(String[] args) {
System.out.println(Son.Value);
}
}
super 静态语句块
Father 静态语句块
23
我们发现进行了初始化操作,但是我们注意到并没有对Son这个类进行初始化。为什么呢?
static变量的解析发生在静态解析阶段,也即是初始化之前,此时已经将字段的符号引用转化为了内存引用,也便将它与对应的类关联在了一起,由于在子类中没有查找到与m相匹配的字段,那么m便不会与子类关联在一起,因此并不会触发子类的初始化。
另外注意初始化阶段是执行类构造器()方法的过程。
这里简单说明下()方法的执行规则:
1、()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
2、()方法与实例构造器()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。因此,在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object。
3、()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成()方法。
4、接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成()方法。但是接口鱼类不同的是:执行接口的()方法不需要先执行父接口的()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。
5、虚拟机会保证一个类的()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
三.为什么String是不可变的
我们知道String类是不可变的。
String k="kobe";
k="bryant";
给一个已有字符串”kobe”第二次赋值成”bryant”,不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。
首先String类用final修饰,拒绝任何类继承该类。而且当我们打开String的源码的时候我们发现,String的操作主要是对一个char类型数组的操作。而且我们发现这个数组也被final修饰了,这个就意味这个数组所指向的对象的地址不能改变了,但是仅仅如此还不行,对象里面的内容仍然是可以被改变的。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
即Array变量只是stack上的一个引用,数组的本体结构在heap堆。String类里的value用final修饰,只是说stack里的这个叫value的引用地址不可变。没有说堆里array本身数据不可变。
之所以String是不可变,关键是因为SUN公司的工程师,在后面所有String的方法里很小心的没有去动Array里的元素,没有暴露内部成员字段。private final char value[]这一句里,private的私有访问权限的作用都比final大。而且设计师还很小心地把整个String设成final禁止继承,避免被其他人继承后破坏。所以String是不可变的关键都在底层的实现,而不是一个final。考验的是工程师构造数据类型,封装数据的功力。