1.修饰类
当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。
在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。
修饰方法
用final关键字修饰方法,它表示该方法不能被覆盖。(但可以继承之). 关于private和final关键字还有一点联系,这就是类中所有的private方法都隐式地指定为是final的,由于无法在类外使用private方法,所以也就无法覆盖它。
3.修饰变量
对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
在java中,用final关键字修饰的变量,只能进行一次赋值操作,并且在生存期内不可以改变它的值。更重要的是,final会告诉编译器,这个数据是不会修改的,那么编译器就可能会在编译时期就对该数据进行替换甚至执行计算,这样可以对我们的程序起到一点优化。不过在针对基本类型和引用类型时,final关键字的效果存在细微差别。我们来看下面的例子:
1
class
Value {
2
int
v;
3
public
Value(
int
v) {
4
this
.v = v;
5
}
6
}
7
8
public
class
FinalTest {
9
10
final
int
f1 = 1;
11
final
int
f2;
12
public
FinalTest() {
13
f2 = 2;
14
}
15
16
public
static
void
main(String[] args) {
17
final
int
value1 = 1;
18
// value1 = 4;
19
final
double
value2;
20
value2 = 2.0;
21
final
Value value3 =
new
Value(1);
22
value3.v = 4;
23
}
24
}
上面的例子中,我们先来看一下main方法中的几个final修饰的数据,
在给value1赋初始值之后,我们无法再对value1的值进行修改,final关键字起到了常量的作用。从value2我们可以看到,final修饰的变量可以不在声明时赋值,即可以先声明,后赋值。value3时一个引用变量,这里我们可以看到final修饰引用变量时,只是限定了引用变量的引用不可改变,即不能将value3再次引用另一个Value对象,但是引用的对象的值是可以改变的
,从内存模型中我们看的更加清晰:
上图中,final修饰的值用粗线条的边框表示它的值是不可改变的,我们知道引用变量的值实际上是它所引用的对象的地址,也就是说该地址的值是不可改变的,从而说明了为什么引用变量不可以改变引用对象。而实际引用的对象实际上是不受final关键字的影响的,所以它的值是可以改变的。
另一方面,我们看到了用final修饰成员变量时的细微差别,因为final修饰的数据的值是不可改变的,所以我们必须确保在使用前就已经对成员变量赋值了。因此对于final修饰的成员变量,我们有且只有两个地方可以给它赋值,一个是声明该成员时赋值,另一个是在构造方法中赋值,在这两个地方我们必须给它们赋初始值。
最后我们需要注意的一点是,同时使用static和final修饰的成员在内存中只占据一段不能改变的存储空间。
2.修饰方法参数
前面我们可以看到,如果变量是我们自己创建的,那么使用final修饰表示我们只会给它赋值一次且不会改变变量的值。那么如果变量是作为参数传入的,我们怎么保证它的值不会改变呢?这就用到了final的第二种用法,即在我们编写方法时,可以在参数前面添加final关键字,它表示在整个方法中,我们不会(实际上是不能)改变参数的值:
public
class
FinalTest {
/* ... */
public
void
finalFunc(
final
int
i,
final
Value value) {
// i = 5; 不能改变i的值 // v = new Value(); 不能改变v的值
value.v = 5;
// 可以改变引用对象的值
}}
Q)何为未被初始化的 final 变量?
Ans)在声明final修饰的变量的时候没有初始化的变量,也称为 blank final 变量。
考虑这样一个场景,一个对象的某个字段,只有在该对象被实例化的时候被赋值一次,以后该字段的值不会被改变。这个时候blank final 变量就派上用场了。
1
class
Student{
2
int
id;
3
String name;
4
final
String PAN_CARD_NUMBER;
5
...
6
}
Student中的PAN_CARD_NUMBER 字段只有在学生实例被创建的时候赋值一次,以后不会改变。
Q)何为final参数?
Ans)被final修饰的参数,该参数的值不能改变。栗子:
1
class
Bike11{
2
int
cube(
final
int
n){
3
n=n+2;
//can't be changed as n is final
4
n*n*n;
5
}
6
public
static
void
main(String args[]){
7
Bike11 b=
new
Bike11();
8
b.cube(5);
9
}
10
}
上述代码中,cube() 方法的参数n被final修饰,其内部对n试图修改将产生编译错误。
Q)构造函数能声明为final吗?
Ans)不能,因为构造函数从来不会被继承。
二.深入理解final关键字
在了解了final关键字的基本用法之后,这一节我们来看一下final关键字容易混淆的地方。
1.类的final变量和普通变量有什么区别?
当用final作用于类的成员变量时,成员变量(注意是类的成员变量,局部变量只需要保证在使用之前被初始化赋值即可)必须在定义时或者构造器中进行初始化赋值,而且final变量一旦被初始化赋值之后,就不能再被赋值了。
那么final变量和普通变量到底有何区别呢?下面请看一个例子:
public
class
Test {
public
static
void
main(String[] args) {
String a =
"hello2"
;
final
String b =
"hello"
;
String d =
"hello"
;
String c = b +
2
;
String e = d +
2
;
System.out.println((a == c));
System.out.println((a == e));
}
}
|
true
false
大家可以先想一下这道题的输出结果。为什么第一个比较结果为true,而第二个比较结果为fasle。这里面就是final变量和普通变量的区别了,
当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。
这种和C语言中的宏替换有点像。因此在上面的一段代码中,由于变量b被final修饰,因此会被当做编译器常量,所以在使用到b的地方会直接将变量b 替换为它的 值。而对于变量d的访问却需要在运行时通过链接来进行。想必其中的区别大家应该明白了,不过要注意,
只有在编译期间能确切知道final变量值的情况下,编译器才会进行这样的优化
,比如下面的这段代码就不会进行优化:
public
class
Test {
public
static
void
main(String[] args) {
String a =
"hello2"
;
final String b = getHello();
String c = b +
2
;
System.out.println((a == c));
}
public
static
String getHello() {
return
"hello"
;
}
}
|
这段代码的输出结果为false。
2.被final修饰的引用变量指向的对象内容可变吗?
在上面提到被final修饰的引用变量一旦初始化赋值之后就不能再指向其他的对象,那么该引用变量指向的对象的内容可变吗?看下面这个例子:
public
class
Test {
public
static
void
main(String[] args) {
final
MyClass myClass =
new
MyClass();
System.out.println(++myClass.i);
}
}
class
MyClass {
public
int
i =
0
;
}
|
这段代码可以顺利编译通过并且有输出结果,输出结果为1。这说明
引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的。
3.final和static
很多时候会容易把static和final关键字混淆,
static作用于成员变量用来表示只保存一份副本,而final的作用是用来保证变量不可变
。看下面这个例子:
public
class
Test {
public
static
void
main(String[] args) {
MyClass myClass1 =
new
MyClass();
MyClass myClass2 =
new
MyClass();
System.out.println(myClass1.i);
System.out.println(myClass1.j);
System.out.println(myClass2.i);
System.out.println(myClass2.j);
}
}
class
MyClass {
public
final
double
i = Math.random();
public
static
double
j = Math.random();
}
|
运行这段代码就会发现,
每次打印的两个j值都是一样的,而i的值却是不同的
。从这里就可以知道final和static变量的区别了。
4.匿名内部类中使用的外部局部变量为什么只能是final变量?
5.关于final参数的问题
关于网上流传的”当你在方法中不需要改变作为参数的对象变量时,明确使用final进行声明,会防止你无意的修改而影响到调用方法外的变量“这句话,我个人理解这样说是不恰当的。
因为无论参数是基本数据类型的变量还是引用类型的变量,使用final声明都不会达到上面所说的效果。
看这个例子就清楚了:
上面这段代码好像让人觉得用final修饰之后,就不能在方法中更改变量i的值了。殊不知,方法changeValue和main方法中的变量i根本就不是一个变量,
因为java参数传递采用的是值传递,对于基本类型的变量,相当于直接将变量进行了拷贝。所以即使没有final修饰的情况下,在方法内部改变了变量i的值也不会影响方法外的i。
再看下面这段代码:
public
class
Test {
public
static
void
main(String[] args) {
MyClass myClass =
new
MyClass();
StringBuffer buffer =
new
StringBuffer(
"hello"
);
myClass.changeValue(buffer);
System.out.println(buffer.toString());
}
}
class
MyClass {
void
changeValue(
final
StringBuffer buffer) {
buffer.append(
"world"
);
}
}
|
运行这段代码就会发现输出结果为 helloworld。很显然,
用final进行修饰并没有阻止在changeValue中改变buffer指向的对象的内容
。有人说假如把final去掉了,万一在changeValue中让buffer指向了其他对象怎么办。有这种想法的朋友可以自己动手写代码试一下这样的结果是什么,如果把final去掉了,然后在changeValue中让buffer指向了其他对象,也不会影响到main方法中的buffer,原因在于java采用的是值传递,对于引用变量,传递的是引用的值,也就是说让实参和形参同时指向了同一个对象,因此让形参重新指向另一个对象对实参并没有任何影响。
所以关于网上流传的final参数的说法,我个人不是很赞同。
参考资料:
《Java编程思想》