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
49public 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
21public 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
21public 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
6public 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
15public 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
17public 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
7public class PrivateFinalTest{
private final void test(){}
}
class Sub extends PrivateFinalTest{
//该方法不是重写,而是一个新的子类中的方法,只是一样
public void test(){}
}final方法
仅仅
是不能被重写
,可以被重载
。1
2
3
4
5
6public 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
21public 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
12public 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
74class 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
19public 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对象,因此两次通过