5.4 final修饰符

简介

final关键字可以用于修饰类、方法、变量,用于表示它修饰的类、变量、方法不可以改变。
final修饰变量时,表示该变量一旦获得初始值就不可以被改变,final既可以修饰成员变量(包括类变量和实例变量),也可以修饰局部变量、形参。
由于final变量获取初始值后不能被重新赋值,因此final修饰成员变量和局部变量有一定不同。

一、final成员变量(类变量、实例变量)

  成员变量时随着类初始化或对象初始化而初始化的。当类初始化时,系统会为之分配内存空间,并分配初始值;当创建对象时,系统会为该实例变量分配内存,并分配默认值。因此当执行类初始化块时,可以对类变量赋值;当执行普通初始化块、构造器时可对是变量赋初始值。因此成员变量可在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值。
final修饰的成员变量必须由程序员显示地指定初始值
★类变量:必须在静态初始化块中指定初始化值或声明该类变量时指定初始值,而且只能在这两个地方的其中1之一。
★实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值,而且只能是三个地方其中一个。

class FinalVariableTest 
{
	//定义成员变量时指定默认初始值,合法
	final int a=6;
	//下面变量将在构造器中或初始化块分配内存
	final String str;
	final int c;
	final static double d;
	//下面定义ch实例变量不合法,因为没有在初始化块、构造器中指定初始化值
	//final char ch;

	//初始化块,可对没有指定默认值的实例变量指定初始值
	{
		str="Hello";
		//下面语句不合法,因为成员变量a已经指定了初始值,不能为a重新赋值
		//a=9;
	}

	//静态初始化块,可对没有指定初始值的的类变量指定初始值
	static{
		d=6;//合法
	}

	//构造器中指定初始化值
	public FinalVariableTest()
	{
		c=5;
	}

	//普通方法不能为final修饰的成员变量赋值
	public void changeFinal()
	{
		//ch='a';
	}
	public static void main(String[] args)
	{
		var ft=new FinalVariableTest();
		System.out.println(ft.a);//输出6
		System.out.println(ft.c);//输出5
		System.out.println(ft.d);//输出6.0

	}
}

注意:如果打算在构造器、初始化块中对final成员变量进行初始化,则不要在初始化之前访问final成员变量;否则,由于Java允许通过方法来访问final成员变量,此时系统将final成员变量默认初始化为0('/u0000'、false、nulll)的情况。
示例:

class FinalErrorTest 
{
	//系统不会对final成员变量进行默认初始化
	final int age;
	final char ch;
	final String str;
	{
		//age变量没有初始化,所以此处的代码将引起错误
		//System.out.println(age);//FinalErrorTest.java:7: 错误: 可能尚未初始化变量age
		printVar();//这行代码时合法的将输出0
		age=6;
		ch='a';
		str="疯狂Java";

		System.out.println(age);
		System.out.println(ch);
		System.out.println(str);
	}

	public void printVar(){
		System.out.println(age);
		System.out.println(ch);
		System.out.println(str);
	}

	public static void main(String[] args) 
	{
		var p=new FinalErrorTest();
	}
}

输出结果:

从上面的程序可以看出,直接打印成员变量将引起错误,通过方法来访问final修饰的成员变量,此时是允许的将输出age=0,ch= ' ',str=null。这显然违背了final成员设计的初衷:对final成员变量,程序当然希望总是能访问到它固定的、显示初始化值。
final成员变量在显示初始化之前不可以直接访问,但可以通过方法来访问,这是Java设计的一个缺陷。因此建议避免在final成员变量显示初始化之前访问它。

二、final局部变量

  系统不会对局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰的局部变量时,既可以在定义时指定默认值,也可以不指定默认值。
如果final修饰的局部变量在定义时没有默认值,则可以在后面代码中对final变量赋初始值,当只能依次一次。

class FinalLocalVarTest 
{
	public void test(final int a)
	{
		//不能对final修饰的形参赋值,下面语句非法
		//a=5;//FinalLocalVarTest.java:6: 错误: 不能分配最终参数a
	}
	public static void main(String[] args) 
	{
		final var str="hello";
		final double d;
		d=5.0;
	}
}

因为形参在调用方法时,由系统根据传入的参数来完成初始化,因此使用final修饰的形参不能被赋值。

三、final修饰基本类型变量和引用类型变量的区别

  当使用final修饰基本类型变量时,不能对基本类型的变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它仅仅只是保存一个引用,final只保证这个引用变量所引用的地址不会改变,即一致引用同一个对象,但这个对象的内容完全可以改变。

import java.util.Arrays;
class Person 
{
	private int age;
	public Person(){};
	public Person(int age)
	{
		this.age=age;
	}
	public void setAge(int age)
	{
		this.age=age;
	}
	public String toString()
	{
		return this.getClass().getName()+"[age:"+this.age+"]";
	}
}

public class FinalReferenceTest
{
	public static void main(String[] args)
	{
		//final修饰的数组变量,iArr是一个引用变量
		final int[] iArr={5,12,8,6};
		System.out.println(iArr.toString());//[I@27716f4

		//对数组元素进行排序,合法
		Arrays.sort(iArr);
		for(int ele:iArr)
		{
			System.out.print("  "+ele);
		}//  5  6  8  12
		System.out.println();

		System.out.println(iArr.toString());//[I@27716f4

		final var p=new Person(22);
		System.out.println(p.toString());
		//p是一个引用变量,可以修改Person对象的age实例变量
		p.setAge(18);
		System.out.println(p.toString());
	}
}
---------- 运行Java捕获输出窗 ----------
[I@27716f4
  5  6  8  12
[I@27716f4
Person[age:22]
Person[age:18]

输出完成 (耗时 0 秒) - 正常终止

四、可执行“宏替换”的final变量

  对于一个final变量而言,不管它是类变量、实例变量,还是局部变量,只要该变量满足两个条件,这个final修饰的变量就不在是一个变量,而是一个直接量。编译器会将程序中所有用到该变量的地方直接替换成变量的值。
1、使用final修饰符修饰。
2、在定义final变量时指定了初始值或该初始值在编译时就可以被确定下来。
这里再回顾以下前面内容:Java常量池专门用于管理在编译时被确定的并保存在已编译的.class文件中一些数据。它包括类、方法、接口中的常量,还有字符串常量。

class FinalTest 
{
	public static void main(String[] args) 
	{
		//定义四个final“宏变量”
		final int MAX=20;//直接给定初始值直接量
		final var a=1+9;//编译时期可以确定下来
		final String str="疯狂"+"Java";
		final String book="疯狂Java讲义:"+99.0;

		//下面books变量值在调用了方法,所以无法在编译时确定下来
		final var books="疯狂Java讲义:"+String.valueOf(99.0);

		//判断是否相等、
		System.out.println(book=="疯狂Java讲义:99.0");//true
		System.out.println(books=="疯狂Java讲义:99.0");//false

		//String类已经重写了equals()方法,只要字符串内容相同,就输出true
		System.out.println(book.equals(books));//true

	}
}

注意:对于实例变量而言,既可以在定义实例变量的时候赋初值,也可以在非静态初始化块,构造器中对它赋初值,在这三个地方指定初始值的效果基本一样。但对于final实例变量而言,只有在定义该变量时指定初始值才会有“宏变量”的效果。

五、final方法

  final修饰方法不可以被重写。Java提供的Object类里就有一个final方法:getClass(),因为Java不允许任何类重写该方法,所以把final这个方法密封起来。但对于提供的toString()和equals()方法,都允许子类重写,因此没有final修饰。

class FinalMethodTest 
{
	public final void test()
	{
		System.out.println("这是一个test()方法");
	}
}
public class Sub extends FinalMethodTest
{
	@Override
	public final void test()
	{
		System.out.println("子类重写父类的方法");
	}
}
---------- 编译Java ----------
Sub.java:11: 错误: Sub中的test()无法覆盖FinalMethodTest中的test()
	public final void test()
	                  ^
  被覆盖的方法为final
1 个错误

输出完成 (耗时 1 秒) - 正常终止

对于一个private方法,因为它仅仅在当前类可见,其子类无法访问该方法,所以子类无法重写该方法——如果子类中定义了一个与父类private方法有相同的方法名、形参列表、相同返回值类型,也不是方法重写,只是重新定义了一个新方法。

class PrivateFinalMed 
{
	private final void test()
	{
		System.out.println("这是test方法");
	}
}

class SubTest extends PrivateFinalMed
{
	@Override
	public void test()
	{
		System.out.println("这是重写的test()方法");
	}//SubTest.java:11: 错误: 方法不会覆盖或实现超类型的方法
}

六、final类

final修饰的类不可以有子类,例如java.lang.Math就是一个final类,它不可以有子类。

final class FinalClass 
{
}
class SubFinalClass extends FinalClass
{
}
//SubFinalClass.java:4: 错误: 无法从最终FinalClass进行继承

七、不可变(immutable)类

  不可变类的意思是创建该类的实例后,该实例的实例变量是不可以改变的。java.lang.String类是不可变类,当创建他们的实例后,其实力变量不可以改变。

class ImmutableClass 
{
	public static void main(String[] args) 
	{
		//String类是一个不可变类,它的实例的实例变量不可改变
		String str="abc";
		System.out.println(str);
		//String str="123";//ImmutableClass.java:7: 错误: 已在方法 main(String[])中定义了变量 str	
	}
}

自定义不可变类,规则如下:
1、使用private和final修饰符来修饰成员变量。
2、提供带参数的构造器(或返回该实例的类方法),用于根据传入参数来初始化类里的成员变量。
3、仅为该类的成员变量提供getter方法,不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量。
4、如有必要重写Object类的hashcode()和equals()方法。equals()方法根据关键成员变量作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()判断相等的对象的hashCode()也相等。
java.lang.String就是根据String对象里的字符序列作为相等的标准,其hashCode()也是根据字符序列计算得到。
程序示例:

class ImmutableStringTest 
{
	public static void main(String[] args) 
	{
		//str1和str2在编译时确定字符串值,因此缓存在常量池中
		String str1="good";
		String str2="good";
		System.out.println(str1==str2);//输出true
		//下面输出的hashCode()值也是相同的
		System.out.println(str1.hashCode());
		System.out.println(str2.hashCode());

		//String变量并不能在编译阶段获得确定值,因此不在常量池
		var str3=new String("good");
		var str4=new String("good");
		System.out.println(str3==str4);//输出false
		//String类重写了equals()方法和hashCode()方法
		System.out.println(str3.equals(str4));//输出true
		//下面输出的hashCode()值也是相同的
		System.out.println(str3.hashCode());
		System.out.println(str4.hashCode());
	}
}
---------- 运行Java捕获输出窗 ----------
true
3178685
3178685
false
true
3178685
3178685

输出完成 (耗时 0 秒) - 正常终止

下面自定义了一个不可变类,程序将Address类的detail和postCode成员变量都使用private隐藏起来,并使用final修饰,不允许其他方法修改这两个成员变量的值。

class  Address
{
	//final修饰的实例变量,可以在定义时、构造器、初始化块中赋初值。但只能赋第一次初值
	private final String detail;
	private final String postCode;

	//在构造器中赋初值
	public Address(String detail,String postCode)
	{
		this.detail=detail;
		this.postCode=postCode;
	}

	//仅为这两个方法提供getter()方法
	public String getDetail()
	{
		return this.detail;
	}
	public String getPostCode()
	{
		return this.postCode;
	}

	//重写equals()方法,判断两个对象是否相等
	public boolean equals(Object obj)
	{
		if(this==obj)
			return true;
		else if(obj!=null&&obj.getClass()==Address.class)
		{
			var p=(Address)obj;
			if(p.getDetail()==this.getDetail()&&p.getPostCode()==this.getPostCode())
				return true;
			else 
				return false;
		}
		else
			return false;
	}

	//重写hashCode()方法,只要对象的关键成员变量形同,就返回相同的值
	public int hashCode()
	{
		return detail.hashCode()+postCode.hashCode()*31;
	}

	public static void main(String[] args) 
	{
		Address a1=new Address("北京","456789");
		Address a2=new Address("北京","456789");
		//不能修改该类的对象的实例变量,但是可以访问实例变量
		System.out.println(a1.getDetail());
		System.out.println(a1.getPostCode());

		System.out.println(a1.equals(a2));
		System.out.println(a1.hashCode());
		System.out.println(a2.hashCode());

	}
}
---------- 运行Java捕获输出窗 ----------
北京
456789
true
475139922
475139922

输出完成 (耗时 0 秒) - 正常终止

用final修饰引用类型变量时,仅表示这个引用变量不可以被重新赋值,但这个变量所指向的对象依然可以改变。这就会有一个问题:当创建不可变类时,如果它包含的成员变量类型是可变的,那么其对象值依然是可以改变的——这个不可变类是失败的。
下面定义一个Person类,但因为Person类包含一个引用变量的成员变量,且这个引用类是可变类,所以导致Person类也变成可变类。

class Name 
{
	private String firstName;
	private String lastName;
	//构造器
	public Name(){}
	public Name(String firstName,String lastName)
	{
		this.firstName=firstName;
		this.lastName=lastName;
	}

	//getter()方法
	public String getFirstName()
	{
		return this.firstName;
	}
	public String getLastName()
	{
		return this.firstName;
	}
	//setter()方法
	public void setFirstName(String firstName)
	{
		this.firstName=firstName;
	}
	public void setLastName(String lastName)
	{
		this.lastName=lastName;
	}
}

public class Person
{
	private final Name name;
	private Person(Name name)
	{
		this.name=name;
	}
	public Name getName()
	{
		return name;
	}
	public static void main(String[] args)
	{
		var n=new Name("悟空","孙");
		var p=new Person(n);
		//Person对象的name的firstName值为“悟空”
		System.out.println(p.getName().getFirstName());
		**n.setFirstName("八戒");**
		////Person对象的name的firstName值为“八戒”
		System.out.println(p.getName().getFirstName());

	}
}
---------- 运行Java捕获输出窗 ----------
悟空
八戒

输出完成 (耗时 0 秒) - 正常终止

上面程序中粗体代码修改了Name对象(可变的实例)的firstName的值,但由于Person类的name实例引用该Name对象,这就会导致Person对象的firstName会被改变,这就破坏了Person类是一个不可变类的初衷。

八、缓存实例的不可变类

不可变类的实例状态不可以改变,可以很方便地被多个对象共享。如果程序需要经常使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。如果可能应该将已经创建的不可变类的实例进行缓存。
介绍一个使用数组来作为缓存池,从而实现缓存实例的不可变类。

class  CacheImmutable
{
	private static int MAX_SIZE=10;
	//使用数组来缓存已有的实例
	private static CacheImmutable[] cache=new CacheImmutable[MAX_SIZE];
	//记录缓存实例在缓存中的位置,cache[pos-1]是最新的缓存实例
	private static int pos=0;
	private final String name;

	//构造器
	private CacheImmutable(String name)
	{
		this.name=name;
	}
	public String getName()
	{
		return name;
	}

	
	public static CacheImmutable valueOf(String name)
	{
		//遍历已缓存的对象
		for(var i=0;i<MAX_SIZE;i++)
		{
			//如果已有相同的实例,则返回该实例的缓存的实例
			if(cache[i]!=null&&cache[i].getName()==name)
			{
				return cache[i];
			}

		}
		//如果缓存已满
		if(pos==MAX_SIZE)
		{
			//把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池最开始的地方
			cache[0]=new CacheImmutable(name);
			//把pos设为1
			pos=1;
		}
		else
		{
			//把新创建的对象缓存起来,pos加1
			cache[pos++]=new CacheImmutable(name);
		}
		return cache[pos-1];
	}

	//重写hashCode()方法
	public int hashCode()
	{
		return name.hashCode();
	}
	public static void main(String[] args) 
	{
		var c1=CacheImmutable.valueOf("hello");
		var c2=CacheImmutable.valueOf("hello");
		System.out.println(c1==c2);//输出true
	}
}

上面的CacheImmutable类使用了一个数组来缓存该类的对象,这个数组的长度为MAX_SIZE,即该类共可以缓存MAX_SIZE个CacheImmutable对象。当缓存池已满时,缓存池采用“先入先出(FIFO)”规则来决定哪个对象将被移除缓存池。下图示范了缓存实例不可变类实例图:

注:如果某个对象的使用率不高,缓存该实例就弊大于利;反之,如果某个对象需要频繁地重复使用,混村该实例就利大于弊。
例如Java提供的Integer类,就采用了CacheInnutable类相同的处理策略,如果采用new构造器来创建Integer对象,则每次返回全新的Integer对象;如果采用valueOf()方法创建对象,则会缓存该方法创建的实例。因此通过new构造器创建Integer对象不会启用缓存,因此性能比较差,Java 9已经将该构造器标定为过时。

public class IntegerCacheTest 
{
	public static void main(String[] args) 
	{
		var int1=new Integer(6);//注: IntegerCacheTest.java使用或覆盖了已过时的 API。
		//生成新的Integer对象,并缓存该对象
		var int2=Integer.valueOf(6);
		//直接从缓存中取出Integer对象
		var int3=Integer.valueOf(6);

		System.out.println(int1==int2);//输出false
		System.out.println(int2==int3);//输出true
		//Integer只缓存-128-127之间的Integer对象。
		//因此200对应的Integer对象没有缓存
		Integer int4=200;
		Integer int5=200;
		System.out.println(int5.equals(int4));//输出true  包装类重写了equals()方法
		System.out.println(int4==int5);//输出false
	}
}

5.4 final修饰符

全文结束