final修饰符
- final 可用于修饰类、变量和方法,用于表示它修饰的类、方法和变量不可改变。
- final修饰的大都应用于基本类型域,或不可变类。- 不可变类:如果类中的每个方法都不会改变其对象,这种类是不可变的类。例如String类。
 
- 不可变类:如果
- 对于可变类,使用final修饰只是表示存储在对象变量中的对象引用不会再指向其他对象,不过这个对象中的属性可以更改。
final修饰特点
- 修饰类时,类不能被继承。
- 修饰变量时,变量就变成了常量,表示该变量一旦获取了初始值就不可被改变,就不能被重新赋值。包括类变量(静态)和实例变量(非静态)。一般与public static配合使用。
- 修饰方法时,方法不能被重写。
- final修饰局部变量时:- 方法内部或者方法声明上
- 基本类型:值不能被改变
- 引用类型:- 地址引用不能被改变,不会再指向其他对象,但是对象中的属性可以改变。会报 “无法为最终变量x分配值” 的错误
 
final变量的初始化时机
final成员变量
- final修饰的成员变量- 必须显式的进行初始化赋值来指定初始值,否则默认值的是个- 无效值,会报“- 可能尚未初始化变量xxx”的错误。- 1 
 2
 3
 4
 5
 6
 7- //方式1:直接进行显式得初始化赋值,不进行赋值,会报错 
 Class Demo{
 final int num ; //没有显式赋值,报错
 final int num = 10; //进行显式得初始化赋值
 public Demo(){
 }
 }
- 也可以在 - 构造方法执行完毕前对其进行- 初始化赋值- 1 
 2
 3
 4
 5
 6
 7- //方式2:在构造方法执行完毕之前进行赋值初始化 
 Class Demo{
 final int num;
 public Demo(){
 num = 10; //在构造方法执行完毕之前进行赋值初始化
 }
 }
- 当 - 类初始化时,系统会为类的- 类变量分配内存并分配- 默认值;当- 创建对象时,系统会为该对象的- 实例变量分配内存,并分配- 默认值。也就是说,当执行- 静态初始化块是可以对- 类变量赋- 初始化值,当执行- 普通初始化块、构造器时可对- 实例变量赋- 初始化值。即- 成员变量的初始化值可以在定义该变量时指定默认值,可以在初始化块、构造器中初始化值。
- 为什么final修饰的成员变量必须显式的进行初始化呢? - 对于final修饰的成员变量而言,一旦有了初始化值,就不能被重新赋值。如果既没有在定义成员变量时指定初始值,也没有在初始化、构造器中为成员变量指定初始值,那么这些成员变量的值就会一直是系统默认值,即系统默认分配的0、’\u0000’、false或null,那么这么成员变量就无任何意义。所以final修饰的成员变量必须由程序员显式的指定初始化值。
 
- 对于
- 归纳一下final修饰的类变量、实例变量能指定初始值的地方: - 类变量:必须在- 静态初始化块中指定初始值或在- 声明定义该变量时指定初始值,并且只能在这- 两处的其中之一指定。
- 实例变量:必须在- 非静态初始化块中、- 声明该实例变量时或- 构造器中指定初始值,并且只能在- 三个地方之一指定。
- final实例变量不能在静态代码块中指定初始值- 因为静态代码块是静态成员,不能访问非静态成员。
 
- 因为静态代码块是静态成员,
- final类变量(静态变量)- 不能放在- 普通初始化块中指定初始值- 因为类变量在类初始化时已经被初始化了,普通初始化块不能再对其重新赋值了。
 
- 因为
- final成员变量都不能在普通方法中初始化值。
- 以下代码示例为final成员变量初始化总结:
 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49- public 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 = 5.6;
 }
 // 构造器,可对既没有指定默认值、有没有在初始化块中
 // 指定初始值的实例变量指定初始值
 public FinalVariableTest()
 {
 // 如果在初始化块中已经对str指定了初始化值,
 // 构造器中不能对final变量重新赋值,下面赋值语句非法
 // str = "java";
 c = 5;
 }
 public void changeFinal()
 {
 // 普通方法不能为final修饰的成员变量赋值
 // d = 1.2;
 // 不能在普通方法中为final成员变量指定初始值
 // ch = 'a';
 }
 public static void main(String[] args)
 {
 FinalVariableTest ft = new FinalVariableTest();
 System.out.println(ft.a);
 System.out.println(ft.c);
 System.out.println(ft.d);
 }
 }
- final成员变量在 - 显式初始化之前- 不能直接访问,但- 可以通过方法来间接访问,但建议开发者- 尽量避免在final变量显式初始化之前访问它,此时访问,其值是系统默认值。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21- public class FinalErrorTest 
 {
 // 定义一个final修饰的实例变量
 // 系统不会对final成员变量进行默认初始化
 final int age;
 {
 System.out.println("初始化块执行");
 // age没有初始化,所以此处代码将引起错误。
 // System.out.println(age);
 printAge(); //合法,会访问到age变量,值为默认值0
 age = 6;
 System.out.println(age);
 }
 public void printAge(){
 System.out.println(age);
 }
 public static void main(String[] args)
 {
 new FinalErrorTest();
 }
 }
final局部变量
- 系统不会对局部变量就行初始化, - 局部变量必须由程序员显式初始化。
- 如果final局部变量在定义时没有指定初始值,则可以在后面代码中对其赋初值,但 - 只能一次,不能重复赋值。
- 如果在 - 定义时已经指定初始值,那么在后面代码中- 不能再对该变量赋值。
- final修饰形参,形参在调用该方法时, - 由系统根据传入的参数来完成初始化,因此使用final修饰的形参不能再被赋值。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21- public class FinalLocalVariableTest 
 {
 public void test(final int a)
 {
 // 不能对final修饰的形参赋值,下面语句非法
 // a = 5;
 }
 public static void main(String[] args)
 {
 // 定义final局部变量时指定默认值,则str变量无法重新赋值
 final String str = "hello";
 // 下面赋值语句非法
 // str = "Java";
 // 定义final局部变量时没有指定默认值,则d变量可被赋值一次
 final double d;
 // 第一次赋初始值,成功
 d = 5.6;
 // 对final变量重复赋值,下面语句非法
 // d = 3.4;
 }
 }
可执行“宏替换”的final变量
- 对于一个final变量,只要满足三个条件,这个final变量就不再是一个变量,而是相当于一个 - 直接量。- 使用final修饰
- 在定义该final变量时指定了初始值
- 该初始值可以在编译时就被确定下来
 - 举个示例: - 1 
 2
 3
 4
 5
 6- public class FinalTest{ 
 public static void main(String[] args){
 final int a = 5;
 System.out.println(a);
 }
 }- 上面示例中,final变量在定义时就指定了初始化值为5。在程序执行过程中,变量a其实根本不存在,当程序执行到 - System.out.println(a);时,实际转换为执行- System.out.println(5);。此时这个现象就被称为- 宏替换。
- 当定义final变量时就为该变量指定了初始值,而且 - 该初始值可以在编译时就确定下来,那么这个final变量本质上就是一个宏变量,- 编译器会把程序中- 所有用到该变量的地方直接替换成该变量的值。
- 如果被赋的表达式只是 - 基本的算术表达式或- 字符串连接运算,- 没有访问普通变量,调用方法,Java编译器同样会将这种final变量- 当成“宏变量”处理。示例如下:- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15- public class FinalReplaceTest 
 {
 public static void main(String[] args)
 {
 // 下面定义了4个final“宏变量”
 final int a = 5 + 2;
 final double b = 1.2 / 3;
 final String str = "胡" + "啊呦";
 final String book = "Java核心技术:" + 99.0;
 // 下面的book2变量的值因为调用了方法,所以无法在编译时被确定下来
 final String book2 = "Java核心技术:" + String.valueOf(99.0); //①
 System.out.println(book == "Java核心技术:99.0"); //true
 System.out.println(book2 == "Java核心技术:99.0"); //false
 }
 }- 示例中,即使字符串连接运算中包含隐式类型(将数值转换成字符串)转换,编译器依然可以 - 在编译时就确定a,b,str,book这4个变量的值,因此它们都是“宏变量”。- 定义book2变量时显式使用方法将数值99.0转换为字符串,但由于该变量的值 - 需要调用String类的方法,因此编译器- 无法编译时确定book2的值,book2- 不会被当成宏变量处理。- book是一个- 宏变量,他将被- 直接替换成"Java核心技术:99.0",所以第一个判断为- true,相等。book2则- 不相等。
- Java中会 - 使用常量池来管理曾经用过的字符串直接量,例如执行- String a = "java";语句之后,常量池中就会- 缓存一个字符串"java";如果程序- 再次执行String b = "java";系统将- 会让b直接指向常量池中的"java"字符串。因此a==b将返回- true。
- 分析以下代码: - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17- public class StringJoinTest 
 {
 public static void main(String[] args)
 {
 String s1 = "疯狂Java";
 // s2变量引用的字符串可以编译时就确定出来,
 // 因此s2直接引用常量池中已有的"疯狂Java"字符串
 String s2 = "疯狂" + "Java";
 System.out.println(s1 == s2); // 输出true
 // 定义2个字符串直接量
 String str1 = "疯狂"; //①
 String str2 = "Java"; //②
 // 将str1和str2进行连接运算
 String s3 = str1 + str2;
 System.out.println(s1 == s3); // 输出false
 }
 }- s2在 - 编译时期就能确定值,系统会让s2- 直接指向常量池中已经缓存的“疯狂Java”字符串。- str1、str2只是 - 两个普通变量,编译器- 不会执行宏替换,所以在- 编译时无法确定s3的值,就无法让其指向字符串池中的已有值,所以- s1==s3为false;只有它们用- final修饰后,- 才能够宏替换,这样编译器即可- 在编译阶段就确定s3的值,就让让- s3指向常量池中的值,那么就会变成- true。
final方法
- 不希望子类重写父类的某个方法,则可以- 使用final修饰该方法,如果子类试图重写该方法,就会引发- 编译错误。- Object类中就有个一个final方法:getClass(),因为Java不希望任何类重写这个方法,所以使用final把这个方法密封起来。
 
- 即使使用final修饰一个 - private访问权限的方法,依然- 可以在其子类中定义与该方法具有相同方法名、相同形参列表、相同返回值类型的方法。该方法- 不是重写,只是子类中的方法定义与其恰巧相同。- 1 
 2
 3
 4
 5
 6
 7- public class PrivateFinalTest{ 
 private final void test(){}
 }
 class Sub extends PrivateFinalTest{
 //该方法不是重写,而是一个新的子类中的方法,只是一样
 public void test(){}
 }
- final方法 - 仅仅是- 不能被重写,- 可以被重载。- 1 
 2
 3
 4
 5
 6- public class FinalOverload{ 
 //final修饰的方法,可以被重载,不能被重写
 public final void test(){}
 public final void test(String str){}
 
 }
final类
- final修饰的类不可以有子类,不能被继承。final类试图被其他类继承,那么会发生编译错误。
- 为了安全因素,保证某个类不可被继承,则可以使用final修饰这个类。
不可变类
- 不可变类的意思是创建该类实例后,该实例的 - 实例变量是不可改变的。- Java中的8个包装类和java.lang.String类都是不可变类,当创建它们的实例后,其实例变量不可改变。
 
- Java中的
- 如果需要创建自定义的不可变类,遵循以下规则: - 使用 - private和- final修饰符来修饰该类的- 成员变量
- 提供 - 带参数构造器,用于- 根据传入参数来初始化类中的成员变量
- 仅为该类的成员变量 - 提供getter方法,- 不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量
- 如果有必要,重写hashCode()和equals()。 - equals()方法根据关键成员变量来作为两个对象是否相等的标准
- 应保证两个用equals()方法判断为相等的对象的hashCode()也相等
 - 例如String类对象里的 - 字符序列作为相等的标准,其- hashCode()方法也是根据- 字符序列计算得到的。- String类中的 - equals()方法源码- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21- public boolean equals(Object anObject) { 
 if (this == anObject) {
 return true;
 }
 if (anObject instanceof String) {
 String anotherString = (String)anObject;
 int n = value.length;
 if (n == anotherString.value.length) {
 char v1[] = value;
 char v2[] = anotherString.value;
 int i = 0;
 while (n-- != 0) {
 if (v1[i] != v2[i])
 return false;
 i++;
 }
 return true;
 }
 }
 return false;
 }- String类中的hashCode()方法的源码: - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12- public int hashCode() { 
 int h = hash;
 if (h == 0 && value.length > 0) {
 char val[] = value;
 
 for (int i = 0; i < value.length; i++) {
 h = 31 * h + val[i];
 }
 hash = h;
 }
 return h;
 }
 
- 可变类的含义是该类的- 实例变量是- 可变的。大部分创建的类都是可变类,特别是JavaBean,因为总是为其实例变量提供了setter和getter方法。
- 与可变类相比, - 不可变类的实例在整个生命周期中永远处于初始化状态,它的实例变量不可改变。因此对不可变类的实例的控制将更加简单。
- 如果需要设计一个不可变类,尤其要注意其 - 引用类型的成员变量,如果- 引用类型的成员变量的类是可变的,就必须采取必要的措施来- 保护该成员变量所引用的对象不会被修改,这样才能创建真正的不可变类。
缓存不可变类
- 不可变类的实例状态不可改变,可以很 - 方便的被多个对象共享。如果程序经常使用相同的不可变实例,就应该考虑- 缓存这种- 不可变类的实例。毕竟- 重复创建相同的对象没有意义,而且会加大系统开销。
- 用 - 数组创建缓存池,用于缓存实例:- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74- 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中 - Integer类就采取了上述CacheImmutale类相同的处理策略,如果采用- new构造器来创建Integetr对象,则每次返回- 全新的Integer对象;如果采用- valueOf()方法来创建Integer对象,则会- 缓存该方法创建的对象。
- 由于new构造器方式创建Integer对象不会启用缓存,因此性能较差,所以 - Java9中已经将该构造器标记为过时,全面采用valueOf()方法创建。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19- 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
 // 由于Integer只缓存-128~127之间的值,
 // 因此200对应的Integer对象没有被缓存。
 Integer in4 = Integer.valueOf(200);
 Integer in5 = Integer.valueOf(200);
 System.out.println(in4 == in5); //输出false
 }
 }- 由于Integer只缓存-128~127之间的Integer对象,因此两次通过Integer.valueOf(200)方法生成的Integer对象不是同一个。
 
- 由于Integer只缓存-128~127之间的Integer对象,因此两次通过