final关键字可用于修饰类,变量和方法。当final修饰变量时,表示该变量一旦获得初始值就不能重新被赋值。
1. final成员变量
对于final修饰的成员变量而言,一旦有了初始值,就不能被重赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块,构造器中为成员变量指定初始值,那么这些成员变量的值一直是系统默认分配的0,“\u0000”,false,null,这些变量就完全失去了意义,因此Java语法规定:final修饰的成员变量必须由程序员显式地指定初始值
归纳起来,final修饰的类变量,实例变量能指定初始值的地方如下:
类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。
实例变量:必须在非静态初始化块,声明该实例变量或构造器中指定初始值,而且只能在三个地方之一指定。
注意:对于final修饰的类变量不能在普通初始化块中指定初始值,因为类变量在类初始化阶段就已经被初始化了,普通初始化块不能对其重新赋值。
public void changeFinal()
{
//d为final修饰的成员变量,且已经赋过初值
// 普通方法不能Ϊfinal修饰的成员变量赋值
// d = 1.2;
//ch为final修饰的成员变量,未赋过初值
// 普通方法不能Ϊfinal修饰的成员变量指定初始值
// ch = 'a';
}
public class FinalErrorTest
{
// 定义一个final修饰的实例变量
//系统不会为final成员变量进行默认初始化
final int age;
{
// age没有进行初始化,下面代码将错误
//System.out.println(age);
printAge();//此处代码时允许的,将输出0
age = 6;
System.out.println(age);//此处将输出6
}
public void printAge(){
System.out.println(age);
}
public static void main(String[] args)
{
new FinalErrorTest();
}
}
从上面的代码可知,final修饰的成员变量在未进行初始化前是不能被直接访问的。但可以通过方法来访问,基本上可以断定这是Java设计上的一个缺陷。
2. final修饰基本数据类型与引用类型变量的区别
final修饰的基本数据类型变量可以在声明时赋值,或者在后面代码中赋值,但只能对其赋值一次,因此final修饰的基本变量不会改变,但对于引用类型变量,它保存的仅仅是一个地址,final只保证这个引用类型指向的地址不变,即一直引用同一个对象,但引用的对象完全可以改变。
class Person
{
private int age;
public Person(){}
// 有参数的构造器
public Person(int age)
{
this.age = age;
}
// 省略age的setter,getter方法
public class FinalReferenceTest
{
public static void main(String[] args)
{
// final修饰数组变量,iArr是引用变量
final int[] iArr = {5, 6, 12, 9};
System.out.println(Arrays.toString(iArr));
// 对数组元素进行排序,合法
Arrays.sort(iArr);
System.out.println(Arrays.toString(iArr));
// 对数组元素进行赋值,合法
iArr[2] = -8;
System.out.println(Arrays.toString(iArr));
// 下面语句对iArr重新赋值,非法
// iArr = null;
// final修饰Person变量,p是引用变量
final Person p = new Person(45);
//改变Person对象的age实例变量,合法
p.setAge(23);
System.out.println(p.getAge());
// 下面语句对p重新赋值,非法
// p = null;
}
}
3. 可执行“宏替换”的final变量
对一个final变量来说,不管它是类变量,实例变量,还是局部变量,只要变量满足下面三个条件这个final变量就不再是一个变量,而是相当于一个直接量。
- 使用final修饰
- 在定义该变量时指定了初始值
- 该初始值可以在编译时就被确定下来(赋值表达式只是基本的算术运算,字符串连接,没有访问变量,调用方法时,在编译时都能确定下来)
提示: 1 final修饰符的一个重要用途就是定义“宏变量”,程序运行时,用到宏变量的地方直接被换成该变量的值。
注意: 对于实例变量而言,既可以在定义该变量时指定初始值,也可以在非静态初始化块,构造器中赋初始值,在这三个地方指定初始债的效果基本一样,但对于final修饰的实例变量而言,只有在定义该变量时指定初始值才会有“宏变量”的效果。类变量大体上也一样
4. final方法
final修饰的方法不可被重写,若不希望子类重写父类的某个方法,则可以使用final修饰该方法。
java提供的Object类就有一个final方法:getclass(),因为不希望任何类重写该方法,所以使用final将这个方法密封起来。但对于该类提供的toString()和equals()方法,都允许子类重写,因此没有使用final修饰它。
注意: 1 final修饰的方法是针对于重写方法方面,所以考虑重写方面需要注意的地方,比如;若父类的peivate修饰的方法子类无法访问,也就无法进行重写,所以当final修饰一个private的方法时,子类中就算写了一个函数名,参数列表,返回值一样的方法也不算重写。
2 final修饰的方法只是不能被重写,并不是不能被重载。
5. 不可变类
不可变(immutable)类的意思是创建该类实例后,该实例的实例变量是不可改变的。 Java提供的8个包装类和java.lang.String类都是不可变类,当创建他们的实例后,其实例的实例变量不可改变
Double d = new Double(6.5);
String str = new String("Hello");
上面程序创建了一个Double对象和一个String对象,并为这两个对象传入了6.5和"Hello"作为参数,那么Double类和String类肯定需要提供实例变量来保存这两个参数,但程序无法修改这两个实例变量的值。
自定义不可变类需要满足下面的规则:
- 使用private和final修饰该类的成员变量
- 提供带参构造器,用于根据传入参数来初始化类里的成员变量
- 仅为该类成员变量提供getter方法,不要为该类提供setter方法,因为普通方法无法修改final修饰的成员变量。
- 如果有必要,重写Object类的hashCode和equals方法。equals方法根据关键成员变量来作为两个对象是否的标准,除此之外,还应该保证两个用equals方法判断为相等的对象的hashCode值也相等。
前面介绍final关键字是提到,当使用final修饰引用变量时,仅表示该引用变量不可重新赋值,但引用类型所指向的对象依然可以改变。这就产生了一个问题:当创建不可变类时,如果它包含的成员得类型是可变的,那么其对象的成员变量的值依然是可以改变的----这个不可变类就是失败的。
class Name
{
private String firstName;
private String lastName;
public Name(){}
public Name(String firstName , String lastName)
{
this.firstName = firstName;
this.lastName = lastName;
}
// 省略firstName,lastName的setter,getter方法
}
public class Person
{
private final Name name;
public Person(Name name)
{
this.name = name;
}
public Name getName()
{
return name;
}
public static void main(String[] args)
{
Name n = new Name("悟空", "孙");
Person p = new Person(n);
// Person对象的name的firstNameֵ值为"悟空"
System.out.println(p.getName().getFirstName());
// 改变Person对象的name的firstNameֵ值
n.setFirstName("八戒");
// Person对象的name的firstNameֵ值被改为"八戒"
System.out.println(p.getName().getFirstName());
}
}
为了保持Person对象的不可变性,必须保护好Person对象的引用类型成员变量,为此对Person类做出如下修改。
public class Person
{
private final Name name;
public Person(Name name)
{
//设置name实例变量为临时创建的Name对象,
//该对象的firstname和lastname与传入的name参数的firstname和lastname相同
this.name = new Name(name.getFirstName(),name.getLastName());
}
public Name getName()
{
//返回一个匿名对象,该对象的firstname和lastname
//与该对象里的name的firstname和lastname相同
return new Name(name.getFirstName(),name.getLastName());
}
public
Person类改写了设置name实例变量的方法,也改写了name的getter方法。当程序向Person构造器里传入一个Name对象时,该构造器创建Person对象并不是直接利用已有的Name对象(利用已有的对象有风险,因为这个已有的对象是可变的,如果程序改变了这个Name对象,将会导致Person对象也发生变化),而是重新创建一个Name对象来赋给Person对象的name实例变量。
当Person对象返回name变量时,它并没有直接把name实例变量返回,直接返回name实例变量的值也可能导致它所引用的Name对象被改变。
==如果需要设计一个不可变类,尤其要注意其引用类型的成员变量,如果引用类型的成员变量的类是可变的,就必须采取必要的措施来保护该成员变量所引用的对象不会被修改,==这样才是真正的不可变类。
6. 缓存实例的不可变类
不可变类的实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。毕竟重复创建相同的对象没有太大意义,而且加大系统开销,如果可能,应该将已经创建的不可变类的实例进行缓存。
缓存是设计中一个非常有用的模式,缓存实现的方法有很多种,不同的实现方法可能存在较大的性能差别。
class CacheImmutale
{
private static int MAX_SIZE = 10;
//使用数组来缓存已有的实例
private static CacheImmutale[] cache
= new CacheImmutale[MAX_SIZE];
// 记录缓存实例在数组中的位置,cache[pos-1]是最新缓存实例
private static int pos = 0;
private final String name;
//隐藏构造器
private CacheImmutale(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
//通过下面的方法来创建对象
public static CacheImmutale valueOf(String name)
{
// 遍历已经缓存的实例
for (int i = 0 ; i < MAX_SIZE; i++)
{
// 如果已经有相同的实例,则直接返回该缓存的实例
if (cache[i] != null
&& cache[i].getName().equals(name))
{
return cache[i];
}
}
// 如果缓存池已满
if (pos == MAX_SIZE)
{
//把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池最开始的位置
cache[0] = new CacheImmutale(name);
// 把pos设为1
pos = 1;
}
else
{
// 把新创建的对象缓存起来,pos加1
cache[pos++] = new CacheImmutale(name);
}
return cache[pos - 1];
}
public boolean equals(Object obj)
{
if(this == obj)
{
return true;
}
if (obj != null && obj.getClass() == CacheImmutale.class)
{
CacheImmutale ci = (CacheImmutale)obj;
return name.equals(ci.getName());
}
return false;
}
public int hashCode()
{
return name.hashCode();
}
}
public class CacheImmutaleTest
{
public static void main(String[] args)
{
CacheImmutale c1 = CacheImmutale.valueOf("hello");
CacheImmutale c2 = CacheImmutale.valueOf("hello");
// 下面代码将输出true
System.out.println(c1 == c2);
}
}
提示: 是否需要隐藏构造器完全取决于系统要求,盲目乱用缓存也可能导致系统性能下降,如某对象只用一次,重复使用概率不大,缓存该实例就弊大于利。
例如Java提供的java.lang.Integer类,它就采用了上面相同的处理策略,如果采用new构造器来创建对象,则每次返回全新的Integer对象;如果采用ValueOf()方法来创建Integer对象,则会缓存该方法创建的对象(由于Integer构造器不会启动缓存,性能较差,Java9已将将该构造器标记为过时)
public class IntegerCacheTest
{
public static void main(String[] args)
{
// 生成新的Integer对象
Integer in1 = new Integer(6);
// 生成新的Integer对象 ,并缓存该对象
Integer in2 = Integer.valueOf(6);
//直接从缓存中取出Ineger对象
Integer in3 = Integer.valueOf(6);
System.out.println(in1 == in2); // 输出false
System.out.println(in2 == in3); //输出true
// 由于Intege只缓存-128~127֮的值
// 因此200对应的Integer对象没有被缓存
Integer in4 = Integer.valueOf(200);
Integer in5 = Integer.valueOf(200);
System.out.println(in4 == in5); //输出false
}
}