深入解析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对于代码维护有非常好的作用。

(0)

相关推荐

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

    在Java中声明属性.方法和类时,可使用关键字final来修饰.final变量即为常量,只能赋值一次:final方法不能被子类重写:final类不能被继承. 1.final成员 声明 final 字段有助于优化器作出更好的优化决定,因为如果编译器知道字段的值不会更改,那么它能安全地在寄存器中高速缓存该值.final 字段还通过让编译器强制该字段为只读来提供额外的安全级别.   1.1关于final成员赋值 1)在java中,普通变量可默认初始化.但是final类型的变量必须显式地初始化.   2

  • Java中final关键字详解

    谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来了解final这个关键字的用法. 主要介绍:一.final关键字的基本用法.二.深入理解final关键字 一.final关键字的基本用法 在Java中,final关键字可以用来修饰类.方法和变量(包括成员变量和局部变量).下面就从这三个方面来了解一下final关键字的基本用法. 1.修饰类 当用final修饰一个类时,表明这个类不能

  • Java中的final关键字详细介绍

    •final变量如果在变量前加final关键字,则这个变量一旦被初始化,便不可再改变. 如果一个final变量是类成员变量,则必须被初始化,且只能被初始化一次. 方法中的参数也可以是final变量.这在我们需要传递引用型的变量时非常有用,因为有时候我们并不希望调用函数修改该变量而影响到原函数中对象的值.因此将引用型变量设为final类型可以有效方式变量被调用参数修改.此时在调用方法中只可以使用该变量,但不能对其做任何修改. 复制代码 代码如下: void test(final int a){ 

  • java中final关键字使用示例详解

    final经常和static一起使用来声明常量,你也会看到final是如何改善应用性能的.final关键字的含义?final在Java中是一个保留的关键字,可以声明成员变量.方法.类以及本地变量.一旦你将引用声明作final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误.什么是final变量?凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为final的都叫作final变量.final变量经常和static关键字一起使用,

  • java final 和instanceof 关键字的区别

    final 可以适用的范围:修饰类:使用这种修饰符的类无法被继承 修饰函数:被修饰的不能被重写 修饰属性:1.final修饰的成员变量是常量,值不能被修改      而java的命名规则:常量都要大写 当形参变量使用final修饰基本类型变量,在函数中该变量不能被修改   引用类型变量:不能改变地址 复制代码 代码如下: /* final class A{    public final void eat(){        System.out.println("测试");    }

  • Java中final关键字的用法总结

    1.final修饰类 被final修饰的类不能被继承,因此final类的成员方法也不能被覆写,被final关键字修饰的类没有子类,因此类的实现细节也无法改变,无法被扩展.final类中的所有成员方法都会被隐式地指定为final方法,final类中的成员变量可以根据需要设为final. 2.final修饰方法 一个类中的方法如果被final关键字修饰,则其子类无法覆写该方法,只能被子类继承.如果父类中的某个方法不想被其子类所覆写,可将该方法定义为final类型,另外,父类中的私有方法(即被priv

  • 详解Java编程中static关键字和final关键字的使用

    Java static关键字以及Java静态变量和静态方法 static 修饰符能够与变量.方法一起使用,表示是"静态"的. 静态变量和静态方法能够通过类名来访问,不需要创建一个类的对象来访问该类的静态成员,所以static修饰的成员又称作类变量和类方法.静态变量与实例变量不同,实例变量总是通过对象来访问,因为它们的值在对象和对象之间有所不同. 请看下面的例子: public class Demo { static int i = 10; int j; Demo() { this.j

  • 详解Java中的final关键字的使用

    final含义 final是Java中的一个保留关键字,可以声明成员变量.方法和类.一旦你将引用声明为final类型,你将不能再改变这个引用了.编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误. final变量 凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为final的都叫做final变量.下面是final修饰变量的例子: final int constValue = 1; // constValue = 2; The final local v

  • java关键字final使用方法详解

    它所表示的是"这部分是无法修改的".不想被改变的原因有两个:效率.设计.使用到final的有三种情况:数据.方法.类. 一. final数据 有时候数据的恒定不变是很有用的,它能够减轻系统运行时的负担.对于这些恒定不变的数据我可以叫做"常量"."常量"主要应用与以下两个地方:1.编译期常量,永远不可改变.2.运行期初始化时,我们希望它不会被改变.对于编译期常量,它在类加载的过程就已经完成了初始化,所以当类加载完成后是不可更改的,编译期可以将它代入

  • java 中的static关键字和final关键字的不同之处

    static 1.在类中,用static修饰的属性,称为静态属性.为这个类的所有对象所共有,存放在静态存储区,所有该类的对象都可以访问且访问的都是同一变量.可以用作计数器,来统计总共创建了多少个各类的对象. 2.在类中,用static 修饰的方法为静态方法,在静态方法中不可以访问非静态的属性和方法,但在非静态方法中可以访问静态方法和属性:且static方法多态失效,不能使用this. 3.由于静态属性和方法是属于该类的所有对象的,所以可以用类名.静态属性/方法名---来访问. 4.static

  • Java中的final关键字详解及实例

    Java中的final关键字 1.修饰类的成员变量 这是final的主要用途之一,和C/C++的const,即该成员被修饰为常量,意味着不可修改. 上面的代码对age进行初始化后就不可再次赋值,否则编译时会报类似上图的错误. 如果修饰的是引用类型的变量,那么初始化后就不能让他指向另一个对象,如下图所示 2.修饰方法 用final关键字修饰的方法是不能被该类的子类override(重写),因此,如果在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的. 注:类的private方法会

  • JAVA中的final关键字用法实例详解

    本文实例讲述了JAVA中的final关键字用法.分享给大家供大家参考,具体如下: 根据上下文环境,java的关键字final也存在着细微的区别,但通常指的是"这是无法改变的."不想改变的理由有两种:一种是效率,另一种是设计.由于两个原因相差很远,所以关键子final可能被误用. 接下来介绍一下使用到final的三中情况:数据,方法,类 final数据 许多编程语言都有某种方法,来向编译器告知一块数据是恒定不变的.有时数据的恒定不变是很有用的,例如: 1. 一个编译时恒定不变的常量 2.

随机推荐