前言
全文介绍:final关键字修饰的变量,方法,以及类,最后从JVM的角度来讲了对其浅浅的了解。
它最大的特点就是不可变。
优点:
代码本身:不希望final描述的对象所表现的含义改变,尽管可以说破坏了多态
安全:final对象具有只读属性,是线程安全的
效率:无法修改final对象本身,对其引用的操作更为高效
1. final修饰方法:
不能重写 。 --- Object类有一个方法getClass()也是final方法。
但如果加了private修饰方法,那么就可以“重写了”?
原因:private仅在当前类可见的,其子类也无法看到,所以如果子类写一个一模一样的,其实是一个新方法。
2. final 修饰类:
不可以有子类。 --- Java.lang.Math类就是一个final类
3. final 修饰变量:
3.1 final修饰成员变量
final修饰成员变量必须显示指定初始值,而修饰局部变量也要(局部变量不用final也要指定初始值,而成员变量不加final会自动赋值默认值)
如图所示,final修饰类变量有2个地方可以初始化。修饰实例变量有3个地方初始化。
记住:类变量和类初始化有关,而实例和对象初始化有关,那么初始化的话类变量只能在静态块中了,而实例的话,由于new对象才会有,所有实例变量只能在构造块(普通初始化块)或者构造器中初始化,再加上它们各自的声明就初始化一种。
final指定了就不能重新赋值,实例变量不能在静态块中初始值,因为前者要实例,后者类加载就有了。所以静态成员不可访问实例变量,反过来, 类变量不能在构造块中/构造器中初始值,因为类变量在类初始化就已经初始化了,构造块不能再对它进行第二次赋值了。
public class Test {
final int a = 1;
final String b;
final int c;
final static double d;
//构造块
{
b = "Hello";
//a = 9; 由于已经赋值过了
}
//静态块
static {
d = 5.6;
}
//构造方法
public Test(){
c = 5;
//b = "java"; 已经赋值过了
}
//普通方法
public void fun(){
//d = 1.2; 不能再普通方法中为final成员变量赋值
//当然加static也不行
}
Main方法调用即可...
}
3.2 fianl修饰局部变量
直接声明初始化或者之后赋值就可以了(但只能赋值一次)
在匿名内部类中用局部变量要用final声明,但是JDK1.8之后可以不加final。
3.3 final修饰基本类型是值,而修饰引用型变量是地址。
public class Test {
public static void main(String[] args) {
// 数组是在堆上存储的,栈上arr上存储的是数组的一个地址,它不管内容
final int[] arr = {1,2,3,4};
System.out.println(arr);
// 修改数组的值
arr[0] = 2;
System.out.println(arr);
//打印的是数组的地址,没有变
//arr = null; 这样就会报错了
}
}
比如再看一道:
public class Test {
public final int a;
//定义final实例变量,在构造器中初始值
public Test(int b){
a = b;
}
public static void main(String[] args) {
//实例变量,有了实例才会有,下面两个t不是一个对象。当然如果加static就会报错
Test t = new Test(2);
System.out.println(t.a);
t = new Test(3);
System.out.println(t.a);
// 尽管这么写不规范,但是这两个t不是同一个对象,地址是不一样
}
}
4. JVM角度来看final:
4.1 final对变量的存储来说是没有关系的,有关系的是static。final只是表示赋值就不修改了。
JVM规范中,已被加载的类信息,常量,静态变量(因为static变量已经被加载了)是存储在方法区中的,方法区和堆是一个逻辑部分。实例变量存储在堆上,局部变量是在栈上的。
4.2 final修饰的局部变量放哪里?
还是那句话,final对变量的存储来说是没有关系的,局部变量放在栈中,一般在函数中声明一个局部变量,Jvm在栈中为它开辟空间,结束就销毁。
final关键字来修饰变量表示该变量一旦赋值就不能更改了。同时编译器必须保证该变量在使用前被初始化赋值。
//直接声明常量,并赋值
public static final int a = 1;
//直接声明常量,并赋值
public final int b = 1;
那么得知,a是一个静态变量,类加载完其也加载完了,那么a是在方法区中,1就是放在常量池中。而b是这个类的实例变量,所以无论有没有final都是在堆区。
4.3 宏替换
在编译时就被确定下来了,就直接替换它,如被赋值表达式为基本算术表达式或字符串连接,没有访问普通变量,调用方法,编译器都会当场宏变量。编译时确定了就放在常量池中。对于final实力变量,只有在定义时出示话才会有“宏变量”的效果。
public class Test {
public static void main(String[] args) {
final int a = 2 + 3;
final String b = "jae" + "1";
//编译器无法编译就能确定b1的值
final String b1 = "jae" + String.valueOf(1);
System.out.println(b == "jae1"); //true
System.out.println(b == b1); //false
System.out.println(b1 == "jae1"); //false
String s1 = "jae" + "wang"; //此时常量池已经有了
String s2 = "jaewang";
System.out.println(s1 == s2); //true
String str1 = "jae";
String str2 = "wang";
String str3 = str1 + str2;
System.out.println(s1 == str3); //false
// 把str1,str2改成final,则返回true
}
}
4.4 小聊String
不可变模式,对象一旦创建属性就不会改变,不可变模式可以实现对象的共享(可以用一个对象实例赋值给多个对象变量)
池的思想,把需要共享的数据放在池中 (节省空间,共享数据),String类可以用“”中的字面值创建对象。会去池中去找,有就返回地址给该变量,没有就在池中创建一个返回。如果是new在堆空间中创建String类的对象,则不会有上述的过程。String类中的intern()方法会将在堆空间中创建的String类对象中的字符串和串池中的比对,如果有相同的串就返回这个串的串池中的地址。( 当然后面版本改了,详情看)
缺点:不变模式在对于对象进行修改,添加操作是使相当麻烦的,会产生很多的中间垃圾对象。创建和销毁的资源的开销是相当大的。String类在字符串连接时会先的效率很低,因为它所产生的对象是不能够修改的,当连接字符串时也就是只能创建新的对象。
改进:对于很多的字符串连接,应当使用StringBuffer类,在使用这个类的对象来进行字符串连接时就不会有多余的中间对象生成,从而优化了效率。