文章目录
前言
李刚老师《JAVA疯狂讲义》第5版,第5章学习笔记。
JAVA中的final修饰符可以用来修饰变量、方法和类,用于表示被修饰的变量、方法和类不可变。
1.final修饰变量
final变量获得初始值后,就无法被重新赋值,当其修饰成员变量和实例变量时,有所不同。修饰基本类型变量和引用类型变量时也有所不同
1.1 final修饰成员变量
当类或类的对象初始化时,成员变量就会初始化,成员变量初始化时,系统就会为该变量指定默认初始值。
因此,若final修饰的成员变量,不指定初始值的话,这个final成员变量的值就只会是0,’\u0000’,false或null。因此JAVA中规定:final修饰的成员变量,必须由程序员显式地指定初始值。
类变量:必须在声明该变量,或在静态初始化块中为其指定初始值
实例变量:必须在声明该变量,或在非静态初始化块,或在构造器中为其指定初始值。
例如:
public class Demo01 {
//声明类变量时,指定初始值
final int a = 5;
//下方变量将在构造器或初始化块中指定初始值
final String str;
final int b;
final static double c;
//初始化块,指定非静态final变量初值
{
str = "Hello";
//下方语句非法,因为a被final修饰,并且已经被指定过初值
//a = 6;
}
//静态初始化块,指定静态final变量初值
static {
c = 6.0;
}
//构造器,指定非静态final变量初值
public Demo01() {
b = 7;
//因为变量str已经在初始化块中赋值了,因此不能再次赋值
//下方语句非法
//str="JAVA";
}
public void testFinal() {
//普通方法中,不能为fianl成员变量赋值,下方语句报错
//a = 7;
//c = 3.2;
}
public static void main(String[] args) {
Demo01 d = new Demo01();
System.out.println(d.a);
System.out.println(d.b);
System.out.println(d.c);
System.out.println(d.str);
}
}
上方代码输出结果为:
5
7
6.0
Hello
注意:
如果打算在构造器或者初始化块中对fianl成员变量进行初始化,则不要在初始化之前就访问该成员变量的值。
1.2 final修饰局部变量
与成员变量不同,系统不会自动为局部变量分配空间与初始化,因此final局部变量可以在后面使用的时候再赋值,但是也只能赋值一次。
注意:
当final修饰方法中的形参时,该参数不能在方法体中被赋值,因为系统会在调用该方法时,根据传入的参数来初始化,例如:
public class Demo01 {
public void test1(final int a) {
//下方语句会报错
a = 10;
}
}
1.3 final修饰基本类型和引用类型变量
final修饰基本类型变量时,不能重新赋值;final修饰引用类型变量时,也不能重新赋值。但是,引用类型变量存储的是地址,是指向对象的地址,地址不能改变,并不意味着对象本身不能改变,例如:
import java.util.Arrays;
class NBAplayer{
private String team;
//无参构造器
public NBAplayer() {
}
//有参构造器
public NBAplayer(String team) {
this.team = team;
}
//team的get方法
public String getTeam() {
return this.team;
}
//team的set方法
public void setTeam(String team) {
this.team = team;
}
}
public class Demo01 {
public static void main(String[] args) {
//final修饰数组变量,数组是引用变量
final int[] Score = {20,30,40,50};
//对数组进行排序
Arrays.sort(Score);
//数组值可以更改
Score[1] = 4;
//指向的对象不可更改,因此下方语句非法
//Score = null;
//final修饰NABplayer类变量,是引用变量
final NBAplayer N = new NBAplayer("Lakers");
//改变该引用变量指向对象的实例变量
N.setTeam("Nuggets");
//对N重新赋值,非法
//N = new NBAplayer("Heat");
}
}
上述代码中,Score为final修饰的数组变量,N为final修饰的类的对象的引用变量,二者本身不能修改,但是可以利用二者修改所引用的对象本身。
1.3 利用final定义直接量
final修饰的变量,满足以下2个条件,就可以直接当作直接量使用:
- 定义时指定了初始值
- 初始值可以在编译时就被确定下来
什么样的初始值可以在编译时就被确定下来呢?
就是给变量初始化的表达式只是进行了基本的算术表达式或字符串连接运算,没有访问普通变量,没有调用方法,就可以在编译时就被确定下来。
例如:
public class Demo01 {
public static void main(String[] args) {
//以下均可以被当作直接量
final int a = 5;
final double b = 1.2 + 3;
final String c = "湖人" + "总冠军!";
final String d = "乔丹" + 3 + "连冠";
//以下变量不会被当作直接量
final String e = "乔丹" + String.valueOf(3) + "连冠";
}
}
因为字符串类型变量e调用了String类的valueOf方法,因此不会在编译时就确定下来,因此不能被当作直接量。
能否被当作直接量有什么区别呢?
例如:
public class Demo01 {
public static void main(String[] args) {
final String a = "湖人总冠军";
String b = "湖人" + "总冠军";
String Str1 = "湖人";
String Str2 = "总冠军";
final String c = Str1 + Str2;
//将输出true
System.out.println(a == b);
//将输false
System.out.println(a == c);
}
}
a、b为直接量,c由于使用了Str1和Str2两个变量,不是直接量。
所以a == b输出true,也就是,a,b指向同一块内存区域;
a ==c 输出false,也就是,a,c指向不同内存区域。
也就是说,JAVA会将直接量保存在一个常量池中,对该值进行一个缓存,再次定义一个值相同的变量(不需要是直接量)时,会直接指向该值,而不会在内存中另外开辟一块区域。
2.final修饰方法
final修饰的方法不可被重写,如果父类中的某些方法,不希望被子类重写,可以利用final修饰符修饰,例如:
class test{
final void test() {
}
}
class test1 extends test{
//下方语句会报错
void test() {
}
}
同时使用final、private修饰符会出现比较有意思的结果:
class test{
private final void test() {
}
}
class test1 extends test{
//下方语句不会报错
void test() {
}
}
为什么不会报错呢?
private修饰的方法只可以在自己类中被方法,它的作用域只是自己的类。因此,实际上,子类中定义的,并不是对父类中方法的重写,而是完完全全的一个新方法,二者没有任何关系。
注意:
final方法是不能被子类重写,但是可以被重载,例如:
class test{
private final void test() {
}
void test(String name) {
}
}
3.final修饰类
当子类继承父类时,可以访问父类的内部数据,重写父类的方法,这可能导致一些不安全因素。因此,当需要一个类无法被继承时,可以使用final修饰这个类。
4.利用final实现不可变类
JAVA中,不可变类的实例变量不可变。JAVA提供的8个基础数据类型的包装类,java.lang.String类都是不可变类。
注意:
不可变类指的是实例本身不可变,而不是指向实例的引用变量不可变。
例如:
public class Demo01 {
public static void main(String[] args) {
String str = new String("湖人队");
String str1 = str;
//下方代码输出true,说明str1和str指向同一块内存
System.out.println(str1 == str);
str = "掘金队";
//下方代码输出true,说明str1和str指向不同内存
System.out.println(str1 == str);
//下方代码输出掘金队,说明str指向的内存发生变化
System.out.println(str);
//下方代码输出湖人队,说明str1指向的内存未发生变化
//实例变量也未发生变化
System.out.println(str1);
}
}
不可变类的实例,可以方便的被多个对象共享,如果程序经常要使用到相同的不可变对象,为避免重复创建,减少系统开销,应对这些不可变对象进行缓存,以下借助数组实现缓存:
class NBAteam{
//NBA球队一共30个
private static int MAX_TEAM = 30;
//创建数组来缓存已有实例
private static NBAteam[] cacheTeam = new NBAteam[MAX_TEAM];
//记录缓存实例在缓存中的位置
private static int pos = 0;
private final String teamName;
//带参构造器
private NBAteam(String teamName) {
this.teamName = teamName;
}
//teamName的get方法
public String getName() {
return teamName;
}
//构建缓存数组
public static NBAteam valueOf(String name) {
for(int i = 0; i < MAX_TEAM;i++) {
//如果缓存数组中,第i个元素不是空,并且这个元素的teamName和方法参数中的name相同
//那么就直接取出来这个元素
if(cacheTeam[i] != null && cacheTeam[i].getName().equals(name)) {
return cacheTeam[i];
}
}
//如果内存池满了,就把缓存池的第一个元素覆盖,然后重置pos
if(pos == MAX_TEAM) {
cacheTeam[0] = new NBAteam(name);
pos = 1;
}else
{
//如果是一个新元素,就放到cacheTeam这个数组缓存池中
cacheTeam[pos++] = new NBAteam(name);
}
//返回缓存池中的对象
return cacheTeam[pos-1];
}
}
public class Demo01{
public static void main(String[] args) {
NBAteam team1 = NBAteam.valueOf("湖人队");
NBAteam team2 = NBAteam.valueOf("湖人队");
//以下代码将会输出true
System.out.println(team1 == team2);
}
}
NBAteam类利用一个长度为30的数组来缓存对象,创建新对象时,若该对象在该数组中存在,则不会创建新对象,而会直接返回缓存池中已存在的对象。若无该对象,则会创建新对象,放到数组中。因此,上述代码输出为true。
若创建的对象数量超过了30,则按照先进先出的原则,从一个元素开始覆盖,依次类推。
注意:
缓存不可随便使用,因为缓存的对象需要占用内存空间,所以,如果对象的使用频率较小,不应该缓存该对象。
对于JAVA本身的包装类,例如:java.lang.Interge类,就采用了类似于NBAteam类的缓存策略,例如: