目录
final修饰符可用于修饰类、变量和方法。
如果final修饰变量,就表示变量一旦获得了初始值,就无法改变。
如果final修饰方法,就表示该方法不能被子类重写。
如果final修饰类,就表示该类不能被子类继承。
final修饰变量
final可以修饰成员变量(类变量和实例变量),也可以修饰局部变量、形参。
final成员变量
本来看起来是很简单的事情,但问题就出在,成员变量初始化的过程比较复杂。
成员变量的初始化要分成类变量和实例变量分开考虑:
类变量可以在以下2个地方显式初始化:
1.定义类变量时指定默认值
2.在静态初始化块中
实例变量可以在以下3个地方显式初始化:
1.定义实例变量时指定默认值
2.在初始化块中
3.在构造器中
final修饰的成员变量(根据是类变量还是实例变量选初始化的地方)必须在这些地方之一显式初始化,而且只能初始化一次,否则会引发编译错误。
这其实很好理解,因为在对象创建结束后,如果没有显式初始化,成员变量会被自动分配零值,而成员变量又是final修饰的,只能一直保持零值,这是毫无意义的。所以java要求final修饰的成员变量必须在对象初始化阶段显式初始化,不能等到在方法内再初始化成员变量,已经晚了。
注意:假如想通过构造器初始化final实例变量,需要在每个构造器里面都提供初始化操作。
错误案例:
public class FinalTest {
// 由于有一个构造器没有为num赋值,这行代码会报错
private final int num;
public FinalTest() {
num = 10;
}
public FinalTest(int num) {
// 这里没有为num赋值,假如调用这个构造方法,就会出现问题,所以编译会报错
}
}
final修饰成员变量的设计缺陷
final成员变量不能在显式初始化之前直接通过变量名访问,但是可以通过调用方法,在方法里和未被显式初始化的成员变量交互访问。
这样做会访问值为零的final成员变量。这是毫无意义的,应该是java的设计缺陷。
错误案例:
public class FinalTest {
private final int age;
{
// 居然能通过方法访问还未显式初始化的final成员变量,输出零值,这是一种设计缺陷
printAge();
age = 20;
}
public void printAge() {
System.out.println(age);
}
public static void main(String[] args) {
FinalTest ft = new FinalTest();
}
}
final局部变量
因为系统不会给局部变量设置默认的零值,必须程序员显式初始化。
所以final修饰局部变量的规则其实很好理解:只能初始化一次。
你可以在声明变量的同时显式初始化,也可以在之后的执行语句显式初始化,但只能初始化一次。
对于形参,由于第一次初始化是调用者传递实参完成的,所以不能对形参进行重新赋值。
final与引用类型变量
引用变量存储的只不过是对象的索引,final修饰的引用变量表示该引用变量只能一直指向同一个对象。但还是可以改变对象内部的成员数据。
final与宏替换
满足这些条件的final变量就是直接量:
1.有final修饰符
2.定义变量的同时指定了初始值
3.在编译阶段可以确定初始值
前两条很好理解,主要是第三条:
不满足宏替换的初始化:final int age = this.getAge(); final int age1 = age;
这两个初始值赋值是通过方法调用和访问普通变量得到的,这两个行为都需要在运行阶段才能完成,所以编译阶段没办法直接确定初始值。
所以,上面两种初始化不满足第三个条件,age和age1就不是直接量。
满足宏替换的初始化:final String s = “hello”; final String s1 = “wor” + “ld" + 123; final var a = 5 + 3;
上面这几种初始化方式,要么直接赋值直接量,要么只是进行了简单的字符串连接运算和算术运算。这些操作都是编译阶段完成的,所以满足第三条。
这些同时满足3条的final变量,就是一个“宏变量”,编译器会把所有出现该变量的地方直接用对应的值代替。
只要满足这三个条件,不管是类变量、实例变量还是局部变量,系统都会把它当成直接量。
直接量的命名规则
直接量的所有字母都需要大写,用下划线分割。
举例:
public static final double PI = 3.14159265358979323846;
private static final double DEGREES_TO_RADIANS = 0.017453292519943295;
private static final double RADIANS_TO_DEGREES = 57.29577951308232;
final修饰方法
final修饰的方法不能被子类重写。
但是,如果父类的方法用private修饰,那子类本身就无法访问该方法,自然就无法重写。就没有必要用final修饰了。即使用final修饰了private的父类方法,还是没任何意义。可以认为,private修饰的方法,隐式的添加了final关键字。
public class FinalTest {
// 父类方法已经用private修饰了,子类哪怕定义了同名,同参数列表,同返回值的方法,也不算重写,final修饰毫无意义
private final void test() {
}
}
class sub extends FinalTest {
public void test() {
}
}
final修饰的方法是不能被重写,但可以被重载。
public class FinalTest {
public final void test() {
}
}
class sub extends FinalTest {
// 都是test方法,但是参数列表不同,重载是允许的
// 相当于子类继承了父类的test()方法,自己重载了test(int i)的版本
public void test(int i) {
}
}
final修饰类
final类表示不可被子类继承,也就是位于继承树的最底端。
这种final类不可能有子类,所以可以认为final类里的所有方法都不能被重写,相当于这些方法都隐式的加了final。
final与不可变类
不可变类:指创建该类的对象后,对象的实例变量不可改变。
比如8个包装类和String类都是不可变类。
不可变类需要几部分:
1.用private和final修饰符修饰该类的成员变量。 private的目的是封装,不让直接改变成员变量的值,final的目的是保证成员变量一旦被初始化就无法改变。
2.不能提供成员变量的setter方法。目的是不让外界改变实例变量的值。
3.通过构造器或者返回实例的类方法设置实例变量的初始值,通过传递参数的方式初始化所有实例变量。 目的是设置所有实例变量的初始值,这是唯一一次初始化的机会。
4.如果有必要,重写Object类的hashCode()和equals()方法。
举例:
public class Address {
private final String detail;
private final String postcode;
// 通过构造器初始化实例成员变量,之后再就不可改变
public Address(String detail, String postcode) {
this.detail = detail;
this.postcode = postcode;
}
public String getDetail() {
return detail;
}
public String getPostcode() {
return postcode;
}
// 重写equals方法
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj != null && obj.getClass() == Address.class) {
Address adr = (Address)obj;
return adr.detail.equals(this.detail) && adr.postcode.equals(this.postcode);
}
return false;
}
// 简单重写hashCode方法
public int hashCode() {
return detail.hashCode() + postcode.hashCode() * 31;
}
}
对于实例变量全是基本数据类型的类来说,上面4条就足够了。
但是,如果实例变量是可变类的引用,虽然上面4条可以保证该引用一直指向同一个对象,但是外界可以改变该对象内部的成员。
举例如下:
public class Person {
// Name是一个可变类,虽然name永远指向一个Name的对象,但是Name对象内部的成员可以被外界改变
private final Name name;
public 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);
System.out.println(p.getName().getFirstName());
// p指向的Person对象的实例变量name和n指向同一个name对象
// 所以可以通过引用n来改变name对象内部成员的值
n.setFirstName("八戒");
// 最后会输出八戒,这不是我们想要的结果
System.out.println(p.getName().getFirstName());
}
}
class Name {
private String firstName;
private String lastName;
public Name() {
}
public Name(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
要解决上述问题,就不能让外界可以访问到可变类类型的实例变量。
要注意2点:
1.初始化可变类类型的实例变量时,只能在类的内部新创建对象初始化,不能让外界得到这个对象的引用。
2.返回一个和可变类实例变量指向对象的内部成员完全相同的一个新的对象,外界只能修改新的对象的内部成员,无法影响到原本的对象。
修改后如下:
public class Person {
private final Name name;
public Person(Name name) {
// 这里name的初始化是通过内部创建的Name对象得到的引用,外界无法得到这个引用
this.name = new Name(name.getFirstName(), name.getLastName());
}
public Name getName() {
// 也不能把name指向的对象返回给外界,而是重新创建一个新对象
// 这个新对象的成员变量的值和name指向的对象的成员变量的值相同
// 这样外界随便改变这个对象的成员变量也无法影响到name指向的对象成员的值
return new Name(name.getFirstName(), name.getLastName());
}
public static void main(String[] args) {
var n = new Name("悟空", "孙");
var p = new Person(n);
System.out.println(p.getName().getFirstName());
n.setFirstName("八戒");
System.out.println(p.getName().getFirstName());
}
}
class Name {
private String firstName;
private String lastName;
public Name() {
}
public Name(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
final与缓存实例的不可变类
由于不可变类的实例状态不能改变,就没必要多次创建相同的不可变类实例,而是应该缓存实例。
是否需要使用缓存要看某个对象使用的次数。如果重复使用概率不大,就没必要缓存;如果需要多次使用,缓存就很有价值。
举例:用数组来缓存已经创建的实例,这只是一个简单的实现
public class CacheImmutable {
// 用一个内部类来记录缓存相关的状态和数据
static class InnerCache {
private static final int MAX_SIZE = 10; // 定义数组的最大缓存容量
private static int pos = 0; // 记录下一个缓存对象引用的下标
private static final CacheImmutable[] cache = new CacheImmutable[MAX_SIZE]; // 用数组缓存不可变类
}
private final String name;
private CacheImmutable(String name) {
this.name = name;
}
public String getName() {
return name;
}
// 通过静态方法创建CacheImmutable对象,如果已经缓存了相同实例变量的对象,就直接返回缓存的对象
public static CacheImmutable valueOf(String name) {
// 如果name为空,就抛出异常
if (name == null) {
throw new NullPointerException("null");
}
// 依次检查所有缓存的对象的name实例变量是否和参数的name相同
for (CacheImmutable cacheImmutable : InnerCache.cache) {
if (cacheImmutable != null && cacheImmutable.name.equals(name)) {
return cacheImmutable;
// 如果已经访问的数组元素已经为null,说明后面都不是有效缓存,就可以结束循环
} else if (cacheImmutable == null) {
break;
}
}
// 程序设定为缓存如果已满,就清空缓存,从第零个元素开始重新缓存
// pos的值为与MAX_SIZE求余的结果,这样如果pos=MAX_SIZE,就会从第零个元素开始缓存,并且pos被重新设置为0
InnerCache.cache[InnerCache.pos %= InnerCache.MAX_SIZE] = new CacheImmutable(name);
// 返回当前被缓存的对象,并且让pos为下一个被缓存的对象下标
return InnerCache.cache[InnerCache.pos++];
}
// 重写equals方法
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj != null && obj.getClass() == CacheImmutable.class) {
var ci = (CacheImmutable)obj;
return ci.name.equals(name);
}
return false;
}
public int hashCode() {
return name.hashCode();
}
}
class CacheImmutableTest {
public static void main(String[] args) {
var c1 = CacheImmutable.valueOf("hello");
var c2 = CacheImmutable.valueOf("hello");
System.out.println(c1 == c2);
}
}
Integer对象就有类似的实现,Integer通过valueOf方法创建实例,缓存了-127~128之间的Integer对象。