final

来源:互联网 发布:宏晶单片机烧录软件 编辑:程序博客网 时间:2024/06/07 06:46


final关键字可用于修饰类,变量和方法,final表示它修饰的类,方法和变量不可改变。

如何理解final这个Java关键字的意思?就是说这个东西不能改变。

为什么要禁止改变?考虑两方面的因素,设计和效率

这篇博客我会详细的整理下Java中final关键字的使用。


final修饰变量

final修饰变量时,表示该变量一旦获得初始值就不可被改变,final既可以修饰成员变量,包括类变量和实例变量,也可以修饰局部变量,形参。

严格的说,final修饰的变量不可被改变,一旦获得初始值,该final变量的值就不能被重新赋值。由于final变量获得初始值之后不能被重新赋值,因此final修饰成员变量和修饰局部变

量时有一定的不同。在这里我先整理一下final修饰变量的几种情况。


final成员变量

成员变量是随类初始化或对象初始化而初始化的。当类初始化时,系统会为该类的类变量分配内存,并分配默认值;当创建对象时,系统会为该对象的实例变量分配内存,并分配

默认值。当执行静态初始化块时可以对类变量赋初始值,当执行普通初始化块,构造器时可对实例变量赋初始值。因此,成员变量的初始值可以在定义该变量时指定默认值,也可

以在初始化块,构造器中指定初始值。对于final修饰的成员变量而已,一旦有了初始值,就不能被重新赋值。

java语法规定,final修饰的成员变量必须由我们显式指定初始值。与普通成员变量不同的是,final成员变量包括类变量和实例变量,必须由程序员显式初始化,系统不会对final成员

变量进行隐式初始化。归纳一下,final修饰的类变量,实例变量能指定初始值的地方如下:

1),类变量:必须在静态初始化块中指定初始值或者声明该类变量时指定初始值,而且只能在这两个地方的其中之一指定

2),实例变量:必须在非静态初始化中或者声明该实例变量的时候或者构造器中指定初始值,而且只能在这三个地方的其中之一指定

/** * final修饰属性 *  * @author LinkinPark */public class LinkinPark{// 1,定义成员变量,声明变量时赋初始值final int a = 6;// 2,定义成员变量,在初始化块中赋初始化值final String name;{name = "LinkinPark";}// 3,定义成员变量,在构造器中赋初始化值final int age;public LinkinPark(){age = 25;}// 4,定义静态成员变量,声明变量时赋初始值final static int b = 8;// 5,定义静态成员变量,静态初始化中赋初始值final static String nick;static{nick = "郭奉孝";}}

值得注意的是,这里有两个编程陷阱,如下两种情况代码报错,编译不过:

1),final修饰属性,一定要对变量显式赋初始值,且只能赋值一次

public class LinkinPark{// 定义一个成员变量,在初始化块中赋初始化值final String name = "NightWish";{// 下行代码name属性重复赋值,编译不过// The final field LinkinPark.name cannot be assignedname = "LinkinPark";}}
2),final修饰属性,如果在初始化块中或者在构造器中对变量赋初始值,那么就不能再初始化之前访问变量。

public class LinkinPark{// 定义一个成员变量,在初始化块中赋初始化值final String name;{// name属性尚未初始化,这里访问报错,编译不过// The final field LinkinPark.name cannot be assignedSystem.out.println(name);name = "LinkinPark";}}


final局部变量

系统不会对局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。

1),如果final修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该final变量赋初始值,但只能一次,不能重复赋值

2),如果final修饰的局部变量在定义时已经指定默认值,则后面的代码不能再对该变量赋值

/** * final修饰局部变量 *  * @author LinkinPark */public class LinkinPark{public void sayHi(){// 1,定义一个局部变量,申明变量时赋初始值final String hi = "你用你优雅的姿势say Hi";// 2,定义一个局部变量,后面的代码中为该变量赋初始值final String name;name = "LinkinPark";// 3,定义一个局部变量,申明变量和后面代码中重复赋值。代码报错,编译不过。// The final local variable age cannot be assigned. It must be blank and not using a compound assignmentfinal int age = 18;age = 25;}}


final修饰形参

形参在调用该方法时,由系统根据传入的参数来完成初始化,使用final修饰的形参不能被赋值

/** * final修饰形参 *  * @author LinkinPark */public class LinkinPark{public void sayHi(final String hi){// final修饰的形参不能赋值,下面代码报错,编译不过// The final local variable hi cannot be assigned. It must be blank and not using a compound assignmenthi = "你用你优雅的姿势say Hi";}}


final修饰基本类型变量和引用类型变量的区别

当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。

对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变

如下代码演示final修饰引用类型变量的情形。

/** * final修饰引用类型的变量 *  * @author LinkinPark */public class LinkinPark{public void testLinkin(){// linkin是一个引用变量,使用final修饰,只是引用地址不变,引用的对象可以改变final Linkin linkin = new Linkin();linkin.setName("LinkinPark");linkin.setName("NightWish");// 下行代码报错,编译不过,因为修改了linkin的引用地址linkin = null;}public void testArrays(){// 数组也是一种引用类型,使用final修饰效果和final修饰普通对象一样final int[] arr = { 1, 2, 3 };arr[0] = 0;// 代码报错,编译不过,arr变量的引用地址不能改变arr = null;}}/** * 定义一个Linkin类 *  * @author LinkinPark */class Linkin{private String name;public String getName(){return name;}public void setName(String name){this.name = name;}}


可执行“宏替换”的final变量

这里先来科普下宏变量,宏变量就是一个变量的初始值在编译时就确定下来了,编译器会把程序中用到该变量的地方直接替换成该变量的值

比如说一个具有实际逻辑意义的数字,如果我们在编码的时候直接写这个数字而没有专门为这个变量声明一个变量,代码运行没有任何问题。但是考虑代码的维护性,在以后不必

修改多个调用到该数字的地方,我们就可以将这个数字定义一个宏变量,如果以后要修改,只修改一个地方就OK。

这里我举一个例子,我写一段代码,定义一个宏变量,然后编译,将生成的class文件反编译出来,看下结果。

/** * final修饰变量,执行宏替换 *  * @author LinkinPark */public class LinkinPark{public static void main(String[] args){// 定义一个宏变量final String name = "LinkinPark";System.out.println(name);}}
源码如上,然后javac -d . LinkinPark.java,将编译后的LinkinPark.class文件用反编译工具反编译一下,代码如下:

import java.io.PrintStream;public class Linkin{  public static void main(String[] paramArrayOfString)  {    System.out.println("LinkinPark");  }}
OK,源码中name的宏变量不在了,源码中使用到name这个宏变量的地方都被换成了源码中name属性的值了。


宏替换的本质就是说编译器会把程序中所有用到该变量的地方直接替换成了这个变量的值了。

对于一个final变量,不管它是类变量,实例变量,还是局部变量,只要该变量满足如下三个条件,这个final变量就不是一个变量,而是相当于一个直接量。

1),使用final修饰符修饰

2),在定义该final变量时指定了初始值

3),该初始值可以在编译时就被确定下来。

final修饰符的一个重要用途就是定义宏变量,当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个final变量本质上就是一个宏变量,编

译器会把程序中所有用到该变量的地方直接替换成该变量的值。

除了声明一个final变量时直接赋初始值的情况外,如果被赋的表达式只是基本的算术表达式或字符串连接运算,没有访问普通变量,也没有调用方法,Java编译器同样会将这种

final变量当成宏变量处理

/** * final修饰变量,执行宏替换 *  * @author LinkinPark */public class LinkinPark{public static void main(String[] args){// 以下5个定义都是宏变量,申明变量时,指定初始值或者进行简单的运算或者字符串连接final String name = "LinkinPark";final int a = 3 + 2;final double b = 1.0 / 2;final String nick = "Linkin" + "Park";final String huhu1 = "LinkinPark" + 25;// 下面变量调用方法,所以不是宏变量final String huhu2 = "LininPark" + String.valueOf(25);// 下行代码输出trueSystem.out.println(huhu1 == "LinkinPark25");// 下行代码输出falseSystem.out.println(huhu2 == "LinkinPark25");}}

对于实例变量而言,既可以在定义该变量时赋初始值,也可以在非静态初始化块,构造器对他赋初始值,在这三个地方指定初始值的效果基本一样,但对于final修饰变量而言,只

有在定义该变量时指定初始值才会有宏变量的效果。


java中会用常量池来管理曾经通过的字符串直接量,关于String类的整理我后面会详细的整理到的,这里先暂时了解一下字符串的常量池。

比如,执行Sting a = "java"语句之后,常量池就会缓存一个字符串"java",如果程序再次执行String b = "java",系统将会让b直接指向常量池中的"java"字符串,所以这个时候a==b 

将会返回true。但是这里有一个小细节就是说,如果想使用字符串的常量池,声明的几个变量必须在编译时候就确定了取值。不管是直接赋值,还是使用final定义成了宏变量,总

之就是说几个变量在定义的时候就确定值了,这个时候就会用到缓存,"=="判断返回true。如果值没有确定,那么编译的时候编译器也不会执行宏替换,也就无法将变量指向字符

串池中缓存的数据了,"=="判断返回false。

/** * final修饰变量使用字符串的常量池 *  * <pre> * 要想使用字符串的常量池,变量的值必须是编译时候就确定下来了。 * 1,申明变量直接赋值,确定了变量具体的值 * 2,使用宏替换,编译的时候将变量转换成具体的值 * 这里时候”==“判断返回true。 * </pre> *  * @author LinkinPark */public class LinkinPark{public static void main(String[] args){// 没有进行变量的引用计算,所以一直会用字符串的缓存池String name1 = "LinkinPark";String name2 = "LinkinPark";String name3 = "Linkin" + "Park";System.out.println(name1 == name2);System.out.println(name1 == name3);// 进行了变量的引用计算,所以不会用字符串的缓存池String firstName = "Linkin";String secondName = "Park";String name4 = firstName + secondName;System.out.println(name1 == name4);// 使用final修饰变量,执行了宏替换,所以会用字符串的缓存池final String firstName1 = "Linkin";final String secondName2 = "Park";String name5 = firstName1 + secondName2;System.out.println(name1 == name5);// true// true// false// true}}


final方法

之所以要使用final方法,可能是出于对两方面理由的考虑。

1),为方法上锁,防止任何继承类改变它的本来含义

设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。比如。Java提供的Object类里就有一个final方法,getClass(),因为

Java不希望任何类重写这个方法,所以使用final把这个方法密封起来,但是对于该类提供的toString()和equals()方法,都允许子类重写,因此没有使用final修饰他们。

public class SubClass extends SuperClass{// 下面代码报错,编译不过。// Cannot override the final method from SuperClass@Overridepublic void test(){System.out.println("父类中该方法使用final修饰,子类不能重写");}}class SuperClass{public final void test(){System.out.println("这个方法使用final修饰,子类不能重写");}}

2),采用final 方法的第二个理由是程序执行的效率

将一个方法设成 final 后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个final 方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而

采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替

换方法调用。这样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能感受不到嵌入代码所带来的任何性能提升。因为任何提升都被花在方法

内部的时间抵消了。Java 编译器能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final 方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。


通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。类内所有private方法都自动成为final。由于我们不能访问一个 private方

法,所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。当然也可为一个 private方法添加final 指示符,但却不能为那个方法提供任何额外的含义。值得注

意的是,final修饰的方法仅仅是不能被重写,而不是不能被重载


final类

final修饰的类不可以有子类,比如java.lang.Math类就是一个final类,它不可以有子类。

// 类声明报错,编译不错// The type SubClass cannot subclass the final class SuperClasspublic class SubClass extends SuperClass{}// 一个类用final修饰,不允许有子类继承final class SuperClass{}

如果说整个类都是final,就表明自己不希望从这个类继承,或者不允许其他任何人采取这种操作。出于这样或那样的原因,我们的类肯定不需要进行任何改变;或者出于安全方面

的理由,我们不希望进行子类化;或许还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效。

注意数据成员既可以是final,也可以不是,取决于我们具体选择。应用于final 的规则同样适用于数据成员,无论类是否被定义成final。将类定义成 final后,结果只是禁止进行继

承,没有更多的限制。然而,由于它禁止了继承,所以一个final类中的所有方法都默认为 final,此时再也无法覆盖它们。所以与我们将一个方法明确声明为final一样,编译器此时

有相同的效率选择。可为final类内的一个方法添加final 指示符,但这样做没有任何意义。

关于使用final修饰类,在实际编码中有一个比较重要的应用,就是设计不可变类,设计一个缓存实例的不可变类,这两块东西我会在面向对象下面整理一篇博客,大家可以去那边

找相关内容,这篇博客重点整理final关键字的用法,关于不可变类这里就不做赘述啦。



final总结

设计一个类时,往往需要考虑是否将一个方法设为 final。可能会觉得使用自己的类时执行效率非常重要,没有人想覆盖自己的方法,这种想法在某些时候是正确的。但要慎重作出

自己的假定。通常,我们很难预测一个类以后会以什么样的形式再生或重复利用,常规用途的类尤其如此。

若将一个方法定义成 final,就可能杜绝了在其他程序员的项目中对自己的类进行继承的途径,因为我们根本没有想到它会象那样使用。标准Java 库是阐述这一观点的最好例子。

1),Vector类:其中特别常用的一个类是 Vector。如果我们考虑代码的执行效率,就会发现只有不把任何方法设为final,才能使其发挥更大的作用。我们很容易就会想到自己应继

承和覆盖如此有用的一个类,但它的设计者却否定了我们的想法。但我们至少可以用两个理由来反驳他们。首先,Stack(堆栈)是从Vector 继承来的,Stack是一个 Vector,这种

说法是不确切的。其次,对于Vector 许多重要的方法,如addElement()以及 elementAt()等,它们都变成了 synchronized(同步的),这会造成显著的性能开销,可能会把final提

供的性能改善抵销得一干二净。因此,程序员不得不猜测到底应该在哪里进行优化。在标准库里居然采用了如此笨拙的设计,真不敢想象会在程序员里引发什么样的情绪。

上面的这段话我不知道摘自那里,一直在我自己的笔记中存着,刚才我自己翻了下JDK8中的Vector类,发现里面的addElement()等几个方法并没有使用final修饰,当然Vector类也

没有使用final修饰,从而被继承了好多的子类。

2),另一个值得注意的是Hashtable(散列表),它是另一个重要的标准类,该类没有采用任何final 方法。明显的一些类的设计人员与其他设计人员有着全然不同的素质(注意比

较Hashtable 极短的方法名与Vecor 的方法名)。对类库的用户来说,这显然是不应该如此轻易就能看出的。一个产品的设计变得不一致后,会加大用户的工作量。这也从另一个

侧面强调了代码设计与检查时需要很强的责任心


关于上面说的JDK中的几个设计好与不好我不发表个人观点,这里我想说下final在实际编码中的使用。只要是我们不想别人乱动我们的代码,这里说的乱动就是连续2次的赋值操

作,或者通过继承我们的类重写我们原有代码中的方法,那么我们就要加上final来修饰变量防止该变量被2次赋值,加上final来修饰方法防止该方法被重写,加上final修饰类防止该

类被继承。在实际的编码过程中,我们往往不注意,也很少加final,其实这个习惯不好的。比如说我们在用持久层框架做CRUD操作的时候,方法入参传入的主键ID就不应该被反

复修改,所以这个时候就应该添加final修饰形参。比如说我们自己写的一些工具类,我们自己在代码中大量使用,万一别人继承后者重写了我们的方法的时候,可能会引发一些问

题,所以说我们自己写的工具类也都要添加final。


1 0