深入解析Java编程中final关键字的作用

2022-10-18,

final关键字正如其字面意思一样,意味着最后,比如被final修饰后类不能集成、变量不能被再赋值等,以下我们就来深入解析Java编程中final关键字的作用:

final class
当一个类被定义成final class,表示该类的不能被其他类继承,即不能用在extends之后。否则在编译期间就会得到错误。

package com.iderzheng.finalkeyword;
 
public final class FinalClass {
}
 
// Error: cannot inherit from final
class PackageClass extends FinalClass {
}

Java支持把class定义成final,似乎违背了面向对象编程的基本原则,但在另一方面,封闭的类也保证了该类的所有方法都是固定不变的,不会有子类的覆盖方法需要去动态加载。这给编译器做优化时提供了更多的可能,最好的例子是String,它就是final类,Java编译器就可以把字符串常量(那些包含在双引号中的内容)直接变成String对象,同时对运算符+的操作直接优化成新的常量,因为final修饰保证了不会有子类对拼接操作返回不同的值。
对于所有不同的类定义—顶层类(全局或包可见)、嵌套类(内部类或静态嵌套类)都可以用final来修饰。但是一般来说final多用来修饰在被定义成全局(public)的类上,因为对于非全局类,访问修饰符已经将他们限制了它们的也可见性,想要继承这些类已经很困难,就不用再加一层final限制。

另外要提到的是匿名类(Anonymous Class)虽然说同样不能被继承,但它们并没有被编译器限制成final。

import java.lang.reflect.Modifier;
 
public class Main {
 
  public static void main(String[] args) {
    Runnable anonymous = new Runnable() {
      @Override
      public void run() {
      }
    };
 
    System.out.println(Modifier.isFinal(anonymous.getClass().getModifiers()));
  }
}

 输出:

 false

final Method
跟继承观念密切相关是多态(Polymorphism),其中牵扯到了覆盖(Overriding)和隐藏(Hiding)的概念区别(为方便起见,以下对这两个概念统一称为“重写”)。但不同于C++中方法定义是否有加virtual关键字会影响子类相同方法签名的方法是覆盖还是隐藏,在Java里子类用相同方法签名重写父类方法,对于类方法(静态方法)会形成隐藏,而对象方法(非静态方法)只发生覆盖。由于Java允许通过对象直接访问类方法,也使得Java不允许在同一个类中类方法和对象方法有相同的签名。

final类限定了整个类不能被继承,进而也表示该类里的所有方法都不能被子类所覆盖和隐藏。当类不被final修饰时,依然可以对部分方法使用final进行修饰来防止这些方法被子类重写。

同样的,这样的设计破坏了面向对象的多态性,但是final方法可以保证其执行的确定性,从而确保了方法调用的稳定性。在一些框架设计中就会经常见到抽象类的一些已实现方法的方法被限制成final,因为在框架中一些驱动代码会依赖这些方法的实现了完成既定的目标,所以不希望有子类对它进行覆盖。

下边的例子展示了final修饰在不同类型的方法中起到的作用:

package com.iderzheng.other;
 
public class FinalMethods {
  public static void publicStaticMethod() {
 
  }
 
  public final void publicFinalMethod() {
  }
 
  public static final void publicStaticFinalMethod() {
  }
 
  protected final void protectedFinalMethod() {
  }
 
  protected static final void protectedStaticFinalMethod() {
  }
 
  final void finalMethod() {
  }
 
  static final void staticFinalMethod() {
  }
 
  private static final void privateStaticFinalMethod() {
  }
 
  private final void privateFinalMethod() {
  }
}
package com.iderzheng.finalkeyword;
 
import com.iderzheng.other.FinalMethods;
 
public class Methods extends FinalMethods {
 
  public static void publicStaticMethod() {
  }
 
  // Error: cannot override
  public final void publicFinalMethod() {
  }
 
  // Error: cannot override
  public static final void publicStaticFinalMethod() {
  }
 
  // Error: cannot override
  protected final void protectedFinalMethod() {
  }
 
  // Error: cannot override
  protected static final void protectedStaticFinalMethod() {
  }
 
  final void finalMethod() {
  }
 
  static final void staticFinalMethod() {
  }
 
  private static final void privateStaticFinalMethod() {
  }
 
  private final void privateFinalMethod() {
  }
}

首先注意上边的例子里,FinalMethods和Methods是定义在不同的包(package)下。对于第一个publicStaticMethod,子类成功重写了父类的静态方法,但因为是静态方法所以发生的其实是“隐藏”。具体表现为调用Methods.publicStaticMethod()会执行Methods类中的实现,调用FinalMethods.publicStaticMethod()时执行并不会发生多态加载子类的实现,而是直接使用FinalMethods的实现。所以在用子类去访问方法时,会隐藏了父类相同方法签名的方法的可见性。
对于全局方法publicFinalMethod就像final修饰方法描述的那样禁止子类定义相同的方法去覆盖它,在编译时就会抛出异常。不过在子类定义方法名字一样但是带有个参数,比如:publicFinalMethod(String x)是可以的,因为这是同步的方法签名。

在Intellij里,IDE对publicStaticFinalMethod显示了一个警告:'static' method declared 'final'。在它看来这是多余的,但从实例中可以看出final同样禁止了子类定义相同的静态方法去隐藏它。在实际开发中,子类和父类定义相同的静态方法的行为是极为不推荐的,因为隐藏方法需要开发者注意使用不同类名限定会有不同的效果,就很容易带来错误。而且在类的内部是可以不使用类名限定直接调用静态方法,开发者再度做继承时可能没有注意到隐藏的存在默认在使用父类的方法时就会发现不是预期的结果。所以对静态方法应该默认已经是final而不该去隐藏他们,也因此IDE觉得是多余的修饰。

父类中protected修饰和public修饰的方法对于子类都是可见的,所以final修饰protected方法的情况和public方法是一样的。想提到的是在实际开发中一般很少定义protected静态方法,因为这样的方法实用性太低。

对于父类package方法,处在不同的package下的子类是不可见的,private方法已经定制了只有父类自己可访问。所以编译器允许子类去定义相同的方法。但这不形成覆盖或隐藏,因为父类已经通过修饰符来隐藏了这些方法,而非子类的重写造成的。当然如果子类和父类在同一package下,那么情况也和之前的public、protected一样了。

final方法为何会高效:

final方法会在编译的过程中利用内嵌机制进行inline优化。inline优化是指:在编译的时候直接调用函数代码替换,而不是在运行时调用函数。inline需要在编译的时候就知道最后要用哪个函数, 显然,非final是不行的。非final方法可能在子类中被重写,由于可能出现多态的情况,编译器在编译阶段并不能确定将来调用方法的对象的真正类型,也就无法确定到底调用哪个方法。

final Variable
简单说,Java里的final变量只能且必须被初始化一次,之后该变量就与该值绑定。但该次赋值不一定要在变量被定义时被立刻初始化,Java也支持通过条件语句给final变量不同的结果,只是无论如何该变量都只能变赋值一次。

不过Java的final变量并非绝对的常量,因为Java的对象变量只是引用值,所以final只是表示该引用不能改变,而对象的内容依然可以修改。对比C/C++的指针,它更像是type * const variable而非type const * variable。

Java的变量可以分为两类:局部变量(Local Variable)和类成员变量(Class Field)。下边还是用代码来分别介绍它们的初始化情况。

Local Variable

局部变量主要指定义在方法中的变量,出了方法它们就会消失不可访问。其中有可分出一种特殊情况:函数参数。对于这种情况,其初始化与函数被调用时传入的参数绑定。

对于其他的局部变量,它们被定义在方法中,其值就可以被有条件的初始化:

public String method(final boolean finalParam) {
  // Error: final parameter finalParam may not be assigned
  // finalParam = true;
 
  final Object finalLocal = finalParam ? new Object() : null;
 
  final int finalVar;
  if (finalLocal != null) {
    finalVar = 21;
  } else {
    finalVar = 7;
  }
 
  // Error: variable finalVar might already have been assigned
  // finalVar = 80;
 
  final String finalRet;
  switch (finalVar) {
    case 21:
      finalRet = "me";
      break;
    case 7:
      finalRet = "she";
      break;
    default:
      finalRet = null;
  }
 
  return finalRet;
}

从上述例子中可以看出被final修饰的函数参数无法被赋予新的值,但是其他final的局部变量则可以在条件语句中被赋值。这样也给final提供了一定的灵活性。
当然条件语句中的所有条件里都应该包含对final局部变量的赋值,否则就会得到变量可能未被初始化的错误

public String method(final Object finalParam) {
  final int finalVar;
  if (finalParam != null) {
    finalVar = 21;
  }
 
  final String finalRet;
 
  // Error: variable finalVar might not have been initialized
  switch (finalVar) {
    case 21:
      finalRet = "me";
      break;
    case 7:
      finalRet = "she";
      break;
  }
 
  // Error: variable finalRet might not have been initialized
  return finalRet;
}

理论上局部变量没有被定义成final的必要,合理设计的方法应该可以很好的维护局部变量。只是在Java方法中使用匿名函数做闭包时,Java要求被引用的局部变量必须被定义为final:

public Runnable method(String string) {
  int integer = 12;
  return new Runnable() {
    @Override
    public void run() {
      // ERROR: needs to be declared final
      System.out.println(string);
      // ERROR: needs to be declared final
      System.out.println(integer);
    }
  };
}

Class Field

类成员变量其实也能分成两种:静态和非静态。对于静态类成员变量,因为它们与类相关,所以除了在定义时直接初始化,还可以放在static block中,而使用后者可以执行更多复杂的语句:

package com.iderzheng.finalkeyword;
 
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
 
public class StaticFinalFields {
  static final int STATIC_FINAL_INIT_INLINE = 7;
  static final Set<Integer> STATIC_FINAL_INIT_STATIC_BLOCK;
 
  /** Static Block **/
  static {
    if (System.currentTimeMillis() % 2 == 0) {
      STATIC_FINAL_INIT_STATIC_BLOCK = new HashSet<>();
    } else {
      STATIC_FINAL_INIT_STATIC_BLOCK = new LinkedHashSet<>();
    }
    STATIC_FINAL_INIT_STATIC_BLOCK.add(7);
    STATIC_FINAL_INIT_STATIC_BLOCK.add(21);
  }
}

 Java中也有非静态的block可以对非静态的成员变量进行初始化,但是对于这些变量,更多的时候还是放在构造函数(constructor)里进行初始化。当然必须保证每个final变量在构造函数里都有被初始化一次,如果通过this()调用了其他的构造函数,则这些final变量不能再在该构造函数里被赋值了。

package com.iderzheng.finalkeyword;
 
public class FinalFields {
 
  final long FINAL_INIT_INLINE = System.currentTimeMillis();
  final long FINAL_INIT_BLOCK;
  final long FINAL_INIT_CONSTRUCTOR;
 
  /** Initial Block **/
  {
    FINAL_INIT_BLOCK = System.nanoTime();
  }
 
  FinalFields() {
    this(217);
  }
 
  FinalFields(boolean bool) {
    FINAL_INIT_CONSTRUCTOR = 721;
  }
 
  FinalFields(long init) {
    FINAL_INIT_CONSTRUCTOR = init;
  }
}

当final用来修饰类(Class) 和方法(Method)时,它主要影响面向对象的继承性,没有了继承性就没有了子类对父类的代码依赖,所以在维护时修改代码就不用考虑会不会破坏子类的实现,就显得更加方便。而当它用在变量(Variable)上时,Java保证了变量值不会修改,更进一步设计保证类的成员也不能修改的话,那么整个变量就可以变成常量使用,对于多线程编程是非常有利的。所以final对于代码维护有非常好的作用。

您可能感兴趣的文章:

  • Java中final作用于变量、参数、方法及类该如何处理
  • 简单理解Java的垃圾回收机制与finalize方法的作用
  • Java垃圾回收finalize()作用详解
  • Java常见面试题之final在java中的作用是什么

《深入解析Java编程中final关键字的作用.doc》

下载本文的Word格式文档,以方便收藏与打印。