JavaSE基础知识(十七)--Java复用代码之关键字final的详细描述

Java SE 是什么,包括哪些内容(十七)?

本文内容参考自Java8标准
再次感谢Java编程思想对本文的启发!
从某个方面来讲,关键字final的作用与关键字protected的作用是相反的。
根据上下文环境,Java的关键字final的含义和作用存在着细微的差别。但通常它之的是:这个内容是无法改变的
不想被改变可能处于两个原因:设计和效率。由于这两个方面相差的比较远,所以关键字final可能会被误用。
下面从三个发面来描述关键字final的具体作用。

1、数据

许多的编程语言都会通过某种方式告知编译器某块数据区域是恒定不变的。有时,数据的恒定不变是很有用的,比如:
⑴、一个永不改变的编译时常量(也就是在编译器就确定了值)。
⑵、一个在运行期被初始化的值,而你希望它的值再也不能被改变(虽然值在运行期的时候才被确定,但是不想再被改变了)。
对于编译器常量这种情况,编译器可以将该常量值带入任何可能用到它的计算式中,也就是说,可以在编译期执行计算式(实际上就是这个计算式的结果有可能在编译期就已经得出了),这减轻了运行时的负担–在Java中,这类常量必须是基本数据类型,并且以关键字final修饰,同时,必须在声明这个常量的时候就赋值,否则它的值将永远是默认值
如果一个变量同时被static和final修饰(static必须在final前面!),那么它将会占据一段不能改变的存储空间(static表示与对象实例无关,那么就与类相关,类只有唯一的一个,它的存储空间就是唯一的,所以这个域的存储空间也是唯一的,final仅表示不变。)。
当你把对象引用而不是基本类型用final修饰的时候,其含义会有一些迷惑,因为对于基本类型,表示它的值不能被修改,而对于引用,它表示使引用恒定不变,也就是一个引用一旦被初始化指向了一个对象,它无法再被修改指向另一个对象(但是这个对象本身的内容是可以改变的,比如你可以给它的类域重新赋值等等,你需要记住这一点)。
实际上,Java并未提供任何使**对象(不是类!)**本身恒定不变的方法(但是你可以通过编写类来使对象保持恒定不变),这一限制同样适用于数组引用,因为它也是对象(final无法限制数组本身存储的内容保持不变,你仍然可以通过数组下标给数组重新赋值)。
下面通过一个示例来说明final修饰类域的情况:
PS:根据惯例,同时被static和final修饰的变量是一个常量(编译期常量),将用大写表示,如果有多个单词,单词之间用下划线分隔。
代码示例:

// final修饰变量
   //类Value
   class Value{
      //int类型的类变量i
      int i;
      //构造方法,带一个int类型的形式参数i
      public Value(int i){
         //初始化类变量i
         this.i = i;
      }
   }
   //类FinalData
   public class FinalData{
      //Java中的随机类Random,能生成随机数,创建对象的时候
      //需要一个int类型的数作为随机因子
      private static final Random rand = new Random(47);
      //String类型的类变量id
      private String id;
      //构造方法,带一个String类型的形式参数id
      private FinalData(String id){
         //初始化类变量id
         this.id = id;
      }
      //final修饰的类变量valueOne,初始化值为9,那它的值就恒定不变为9,不能再被改变。
      //虽然它没有被static修饰,但是无论创建多少个对象,它的值都是9,不能改变。
      //因为它的值在编译期就确定了。
      private final int valueOne = 9;
      //final修饰的类变量VALUE_TWO,初始化值为99,那它的值就恒定不变为99,不能再被改变。
      //这里使用了关键字static,其实有没有它没什么很大的区别,有了static只是更加明确了
      //它的值与创建对象的多少都没关系,只与类相关。
      private static final int VALUE_TWO = 99;
      //同上
      public static final int VALUE_THREE = 39;
      //final修饰的类变量i4,但是在编译期不能得出i4的值,因为代码"rand.nextInt(20);"不会在
      //编译期运行,只有在运行期才会运行得出结果。所以这个变量的值是在运行期的时候被初始化的
      //所以它的值在每一个对象中都可能不同的,与对象相关,它在每个对象里面一旦被初始化就
      //不能再被改变了
      private final int i4 = rand.nextInt(20);
      //INT_5也是在运行期被初始化的,它与i4有重大区别,INT_5在初次被初始化之后,就不能再被
      //改变了,因为这里的static强调的是一份,所以INT_5与创建多少个对象无关,只与类相关。
      static final int INT_5 = rand.nextInt(20);
      //正常的组合,初始化了类Value的引用v1
      private Value v1 = new Value(11);
      //正常的组合,初始化了类Value的引用v2,只是因为有final的修饰,它不能再指向
      //另一个新对象了,但是在每一个FinalData对象里面都能被初始化一次!
      private final Value v2 = new Value(11);
      //正常的组合,初始化了类Value的引用VAL_3,只是因为有final的修饰,它不能再指向
      //另一个新对象了,同时又有static的修饰,它与类相关(与对象无关),只能初始化这一次!
      private static final Value VAL_3 = new Value(33);  
      //a也是对象的引用,同上
      private final int[] a = {1,2,3,4,5,6};
      //toString()方法
      public String toString(){
         //返回字符串"id"+":"+"i4="+i4+",INT_5="+INT_5;
         return "id"+":"+"i4="+i4+",INT_5="+INT_5;
      }
      //程序执行入口main方法
      public static void main(String[] args) {
         //创建FinalData类的对象,引用为fd1,初始化参数为"fd1"
         FinalData fd1 = new FinalData("fd1");
         //fd1.valueOne++;这句代码执行会报错,因为变量valueOne是final修饰的,在
         //定义的时候已经复制了,不能再改变。
         //下面这句代码是可以执行的,虽然v2是final修饰的,但是不妨碍修饰v2对象的内容。
         //仅仅只是v2不能再指向其它的对象了。
         fd1.v2.i++;
         //下面这句代码可以执行的,v1没有被final修饰,完全可以重新new一个对象赋值给它。
         fd1.v1 = new Value(9);
         //for循环
         for(int i = 0;i<fd1.a.length;i++){
            //fd1.a[i]代表的是数组的内容,虽然数组引用a被final修饰,但是
            //不妨碍给数组的内容重新赋值。
            fd1.a[i]++;
         }
         //下面这句代码不能执行,因为引用v2被final修饰了,不能重新赋值一个新对象给它。
         //fd1.v2 = new Value(0);
         //下面这句代码不能执行,因为引用VAL_3被final修饰了,不能重新赋值一个新对象给它。
         //fd1.VAL_3 = new Value(0);
         //下面这句代码不能执行,因为引用a被final修饰了,不能重新赋值一个新的数组对象给它。
         //fd1.a = new int[3];
         //打印引用fd1,实际上调用的是类FinalData的toString()方法。
         System.out.println(fd1);
         //打印字符串"Creating new FinalData"
         System.out.println("Creating new FinalData");
         //创建类FinalData的新对象fd2
         FinalData fd2 = new FinalData("fd2");
         //打印引用fd1,实际上调用的是类FinalData的toString()方法。
         System.out.println(fd1);
         //打印引用fd2,实际上调用的是类FinalData的toString()方法。
         System.out.println(fd2);
      }
   }

valueOneVALUE_TWO都是final修饰的基本数据类型数值,它们二者均为编译器常量,没有本质的区别。VALUE_THREE是一种更加典型的对常量的定义方式:因为:
1、定义为public,可以被用于包之外。
2、定义为static,则强调只有一份。
3、定义为final,则说明它是常量。
PS:带有恒定初始值的static final修饰的基本类型的命名,全部都用大写字母!如果有多个单词,单词之间用"_"分隔。
不能因为变量是final修饰的就认为在编译时可以知道它的值,因为,我们还可以在运行时对变量进行初始化操作(变量i4和INT_5就是在运行时通过随机对象Random进行了初始化),这种变量在编译期是无法确定它的值的。
同时i4INT_5两个变量的初始化还说明了将final数值定义为静态和非静态的区别(有static修饰就是静态的,没有就是非静态的),此区别只有在运行时初始化数值的时候才能显现(被static修饰的数值不论初始化多少次都不会变,比如INT_5),这是因为编译器对编译时数值一视同仁(但这种差异也有可能通过优化而消失),当程序运行的时候,你就注意到了这个区别。
PS:在对象fd1和fd2中,i4的值都是唯一的,但是INT_5的值是不能通过创建第二个FinalData对象而改变的,因为它是static的,在类加载的时候就已经被初始化了,而不是每次创建对象的时候被初始化。
static final修饰与final修饰的区别!
对象引用v1VAL_3说明了final修饰对象引用的意义,正如上面示例代码中看到的,虽然v2是被final修饰的,但是v2指向的那个对象的内容还是可以被改变的,由于v2是一个对象引用,final修饰它的意义在于不能让它重新再指向另一个对象,这对数组也是同样的意义。
暂时还没有什么办法能让数组对象本身成为final的!
从实际效果上看,用final修饰基本类型比用final修饰引用的作用要更大,效果也更直接和明显。
空白final
Java允许生成"空白final",所谓"空白final"是指被final修饰,但是未在声明处就初始化的变量,无论在什么情况下,编译器都会确保空白final在使用前被初始化!
空白final在关键字final的使用上提供了更大的灵活性,所以,一个类中的final域就可以做到根据对象的不同而有所不同,却又能保持其恒定不变的特性。
代码示例:

// 空白final
   //类Poppet
   class Poppet{
      //int类型的类变量i
      private int i;
      //构造方法,带一个int类型的形式参数ii
      Poppet(int ii){
         //为类变量i赋值。
         i = ii;
      }
   }
   //类BlankFinal
   public class BlankFinal{
      //final修饰的类变量i,初始化为0
      private final int i = 0;
      //这就是空白final类变量j,用final修饰它,但是不初始化。
      private final int j;
      //这就是空白final类变量p,用final修饰它,但是不初始化。
      private final Poppet p;
      //构造方法一
      public BlankFinal(){
         //空白final变量一定要在构造方法里面进行初始化!
         //如果你不在这里初始化,那么Java的编译器就会通过默认初始化给它赋值了!
         //一般情况下这是没有什么意义的。
         i = 1;
         //同上。但是引用更繁琐一些,因为在这里不给对象的引用进行初始化,
         //它将被Java编译器默认初始化成null值,更没有意义。
         p = new Poppet(1);
      }
      //构造方法二,带一个int类型的形式参数
      public BlankFinal(int x){
         //空白final变量一定要在构造方法里面进行初始化!
         i = x;
         p = new Poppet(x);
      }
      //拥有空白final,就可以在多个构造方法里面进行初始化,同时使用它恒定不变的特性。
      //程序执行入口main方法
      public static void main(String[] args) {
         new BlankFinal();
         new BlankFinal(43);
      }
   }

如何保证final修饰的域能在使用前一定被初始化,因为你必须在声明final域的地方或者类的构造方法中堆它进行初始化!
final参数
Java允许在形式参数列表中将参数声明为final的!这意味着你无法在方法体内更改参数的值或者它引用的对象。
代码示例:

// final参数
   //类Gizmo
   class Gizmo{
      //方法spin()
      public void spin(){}
   }
   //类FinalArguments
   public class FinalArguments{
      //方法with(final Gizmo g),带一个final修饰的类Gizmo的形式参数g
      void with(final Gizmo g){
         //下面的代码不能执行,因为g是final修饰的,在方法体内不能r让
         //它再指向其他的Gizmo类对象
         //g = new Gizmo();
      }
      //方法with(final Gizmo g),带一个类Gizmo的形式参数g
      void with(Gizmo g){
         //下面的代码可以执行,因为g不是final的。
         g = new Gizmo();
         //调用方法spin()
         g.spin();
      }
      //方法f(final int i),带一个final修饰的int类型的形式参数i
      void f(final int i){
         //下面的代码不能执行,因为i是final的,它本身不能重新被赋值
         //但是可以参与某些运算.
         //i++;
      }
      //方法int(int i),带一个int类型的形式参数i
      int f(int i){
         //返回参数i+1的值。
         return i+1;
      }
   }

以上代码示例分别展示了当基本类型的参数被指明为final的结果,你可以读取参数,但是却无法修改参数,这里暂时作为一种了解,这种方式多用于向匿名内部类传递参数。

2、方法

将方法定义为final的意义有两个:
⑴、将方法锁定,让继承的子类不能再修改这个方法的实现。也就是说,如果你写了一个类是专门提供给客户端程序员使用的,但是你认为你的类中有那么几个方法的实现好的不能再好了,那么你就用final修饰这些方法,那么客户端程序员如果继承了你的类,他将不能修改这些方法的实现,只能直接用。
PS:多半是出于设计的考虑:想确保方法在继承关系中不被覆盖!
⑵、效率因素,在早期的Java实现中,如果将一个方法用final修饰,就是通知编译器将这个方法的调用改为内嵌调用。
当编译器发现一个final方法时,它会根据谨慎的判断,跳过插入程序代码这种正常方式,而执行方法调用机制(将参数压入栈,跳至方法代码处并执行,然后跳回并清理栈中的参数,处理返回值),并且以方法体中的实际代码副本来替代方法调用。以上的过程将消除方法调用的开销。
如果一个方法很大,程序代码必将会膨胀,因而可能看不到任何内嵌调用带来的性能提高,因为所带来的性能提高会因为花费于方法内的时间量而被缩减。
PS:第二点存在于Java SE版本5-6左右,现在都在使用Java SE10了,所以,只需要明确清楚第一点就行!
在最近的Java版本中,虚拟机(特别是hotspot技术)可以探测到这些情况,并优化去掉这些效率反而降低的额外的内嵌调用,因此不再需要通过final修饰方法来进行优化了,实际上,现在这样的做法应该受到劝阻。在使用Java的时候,应该让编译器和JVM去处理效率的问题。
还是那句话,现在使用final去修饰方法只有一个目的:明确方法禁止被覆盖!

final和private关键字
类中的所有private方法都隐式的指定为是final的,由于private方法在子类中根本就是不可见的,所以根本无法覆盖它,等同于是final的了。如果你在private方法的基础上增加关键字final,这并没有任何的实际意义。
但是在现实中,如果你试图去**"覆盖"一个private**方法(等同于是final的),似乎是奏效的,编译器也不会给出任何错误的提示信息。
代码示例:

// 覆盖一个private方法
   //类WithFinals
   class WithFinals{
      //private final方法f()
      private final void f(){
         //打印字符串"WithFinals.f()"
         System.out.println("WithFinals.f()");
      }
      //private方法g()
      private void g(){
         //打印字符串"WithFinals.g()"
         System.out.println("WithFinals.g()");
      }
   }
   //类OverriddingPrivate继承类WithFinals
   class OverriddingPrivate extends WithFinals{
      //private final方法f()
      private final void f(){
         //打印字符串"OverriddingPrivate.f()"
         //你需要知道的是这里并不是覆盖(重新实现)了父类的方法f(),
         //它是另外一个方法f(),与父类中的方法f()无关。
         System.out.println("OverriddingPrivate.f()");
      }
      private void g(){
         //打印字符串"OverriddingPrivate.g()"
         //你需要知道的是这里并不是覆盖(重新实现)了父类的方法g(),
         //它是另外一个方法f(),与父类中的方法g()无关。
         System.out.println("OverriddingPrivate.g()");
      }
   }
   //类OverriddingPrivate2继承类OverriddingPrivate
   class OverriddingPrivate2 extends OverriddingPrivate{
      //public final方法f()
      public final void f(){
         //打印字符串"OverriddingPrivate2.f()"
         //你需要知道的是这里并不是覆盖(重新实现)了父类的方法f(),
         //它是另外一个方法f(),与父类中的方法f()无关。
         System.out.println("OverriddingPrivate2.f()");
      }
      //方法g()
      public void g(){
         //打印字符串"OverriddingPrivate2.g()"
         //你需要知道的是这里并不是覆盖(重新实现)了父类的方法g(),
         //它是另外一个方法f(),与父类中的方法g()无关。
         System.out.println("OverriddingPrivate2.g()");
      }
   }
   //类FinalOverriddingIllusion
   public class FinalOverriddingIllusion{
      //程序执行入口main方法
      public static void main(String[] args) {
         //创建类OverriddingPrivate2对象实例
         OverriddingPrivate2 op2 = new OverriddingPrivate2();
         //调用方法f();
         op2.f();
         //调用方法g();
         op2.g();
         //因为类OverriddingPrivate是类OverriddingPrivate2的父类,所以可以通过
         //引用的传递将op2指向的对象传递给类OverriddingPrivate的引用op
         OverriddingPrivate op = op2;
         //下面的代码不能执行,因为在类OverriddingPrivate中,方法f()是private的
         //op.f();
         //下面的代码不能执行,因为在类OverriddingPrivate中,方法g()是private的
         //op.g();
         //因为类WithFinals是类OverriddingPrivate2的父类,所以可以通过
         //引用的传递将op2指向的对象传递给类WithFinals的引用w
         WithFinals w = op2;
         //下面的代码不能执行,因为在类WithFinals中,方法f()是private的
         //w.f();
         //下面的代码不能执行,因为在类WithFinals中,方法g()是private的
         //w.g();
      }
   }

PS:上面的代码形式好像是子类覆盖了父类的方法,但是实际上并不是那么回事!仅仅是代码形式像而已!
继承关系中,方法的覆盖只有在某方法是类的接口的一部分时才会发生,即,必须能将一个对象向上转型为它的父类型,并调用相同名称的方法(上面的示例代码中,向上转型后,并不能调用同名的方法),如果某方法为private,它根本就不是类的接口的一部分,它仅仅只是隐藏在类中的一些代码而已(碰巧方法的名称相同而已),此时你并没有覆盖父类中的方法,而是产生了一个新方法。
由于private方法无法触及而且能有效隐藏,除了把它看成是类的一部分以外,其他内容都与它无关,不需要考虑!
如果类中的方法是public、protected、或者是包访问权限,那么它在子类中就能被覆盖!参考代码示例中类OverriddingPrivate2的public方法。

3、类

当你用final去修饰整个类的时候(将关键字final置于关键字class之前),就表明你不打算让这个类被继承。
PS:可能是处于某种考虑,你认为这个类的设计永远不需要做出任何改动,或者出于安全的需要,你认为这个类不应该有子类!
代码示例:

// final修饰类
   //类SmallBrain
   class SmallBrain{}
   //final修饰的类Dinosaur
   final class Dinosaur{
      //int类型的类变量i,初始化值为7
      int i = 7;
      //int类型的类变量j,初始化值为1
      int j = 1;
      //创建类SmallBrain的对象
      SmallBrain x = new SmallBrain();
      //方法f()
      void f(){}
   }
   //下面的代码不能执行,因为final修饰的类Dinosaur不能被继承
   //class Further extends Dinosaur{}
   public class Jurassic{
      //程序执行入口main方法
      public static void main(String[] args) {
         //创建类Dinosaur的对象实例
         Dinosaur n = new Dinosaur();
         //调用方法f()
         n.f();
         //虽然类是final的,但是类变量的值还是可以改变的.
         n.i = 40;
         //虽然类是final的,但是类变量的值还是可以改变的.
         n.j++;
      }
   }

实际上final类的类变量你可以继续根据需要来设置它是否是final,无论类是否是final的,这都不影响类变量成为final的。
final类是不能被继承的,这也就等同于这个类的所有方法都不能被覆盖了。所以,如果你想给final类的方法添加final关键字,是没有任何实际意义的!

4、有关关键字final的忠告

在设计类的时候,将方法指定为final是明智的,你可能会觉得没人会想去覆盖你的方法,这可能是对的。
但是在实际中,要遇见类是如何被复用的是很困难的,特别是对于一个通用的类来说更是如此!如果将一个方法指定为final的,可能会妨碍其他程序员在项目中通过继承复用你的类。
Java的标准程序库就是一个很好的例子,特别是Java1.0/1.1中的Vector类被广泛运用,如果从效率的角度考虑,如果将所有的方法都被指定为final,它可能会更加有用,很容易想到,人们可能会想要继承并覆盖如此基础而有用的类,但是设计者可能会觉得如此做不太合适,有两个原因:
⑴、类Stack继承自Vector,就说Stack是一个Vector,这从逻辑的观点看是不正确的,尽管如此,Java的设计者们仍旧继承了Vector,在以这种方式创建Stack时,他们应该意识到final方法过于严苛了。
上面这句话可能会有点费解,可能是翻译的时候的逻辑问题,我来通顺的解释下:
类Stack是继承自类Vector,但是从逻辑上来讲,不能说类Stack就是一个Vector,因为这两个类的特点以及设计的出发点都不同,就目前为止看来,通过继承类Vector,类Stack曾今一度很好用,如果类Vector的方法全都是final的,也就没有类Stack了。所以将类Vector的所有方法都设置为final就过于严苛了,虽然它是一个基础好用的类,虽然你能遇见到它将被很多其他类继承,但是这样还是值得的。
类Stack继承自类Vector!
、Vector的许多重要方法–如addElement()和elementAt()是同步的(这里的同步指的是多线程控制的问题,开销特别大,有兴趣,待我写完了有关多线程的博文后,你们可以自行了解),这将导致很大的执行开销,基本就抵消了final带来的好处,这种情况增强了人们猜测程序员无法正确猜测优化应该发生在何处的观点。如此蹩脚的设计,却出现在了我们人人都要使用的标准程序库中,这是很糟糕的。
幸运的是,现代Java的容器库使用类ArraList代替了类Vector,ArrayList的行为要合理的多。(实际上也就是代码的实现逻辑更合理了!但是遗憾的是仍然存在使用旧容器库代码编写新程序代码的情况。)
还有另一个类:HashTable,这个例子同样有趣,它也是一个重要的Java1.0/1.1标准类库,它不含任何的final方法,实际上你去研究它的源码你会发现,整个类可能是由互不相关的人一起设计的(HashTable中的方法相对于Vector中的方法要简洁的多),这个类本不该如此轻率,这种情况只能让使用类的程序员需要付出更多的努力,简直就是粗糙的代码,粗糙的设计。
因为HashTable类设计以及代码都存在瞎搞的嫌疑,所以现代Java容器库用HashMap代替了HashTable(不要以为大神级别的人物就不会瞎搞)。
PS:时间有限,有关Java SE的内容会持续更新!今天就先写这么多,如果有疑问或者有兴趣,可以加QQ:2649160693,并注明CSDN,我会就博文中有疑义的问题做出解答。同时希望博文中不正确的地方各位加以指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值