`

有关有效使用 final 关键字的准则

    博客分类:
  • Java
阅读更多

       final 关键字常常被误用 - 声明类和方法时使用过度,而声明实例字段时却使用不足。本文探究了一些有关有效使用 final 的准则。


如同它的“表亲”- C 中的 const 关键字一样,根据上下文, final 表示不同的东西。 final 关键字可应用于类、方法或字段。应用于类时,意味着该类不能再生成子类。应用于方法时,意味着该方法不能被子类覆盖。应用于字段时,意味着该字段的值在每个构造器内必须 只能 赋值一次而且此后该值永远不变。

 

大多数 Java 文本都适当地描述了使用 final 关键字的用法和后果,但是很少以准则的方式提供有关何时使用 final 及使用频率的内容。根据我的经验, final 非常过度地用于类和方法(通常是因为开发人员错误地相信这会提高性能),而在其用武之地 - 声明类实例变量 - 却使用不足。

 

为什么这个类是 final?

 

对于开发人员来说,将类声明为 final ,却不给出为何作出这一决定的说明,这样的做法很普遍,在开放源码项目中尤其如此。一段时间之后,特别是如果原来的开发人员不再参与代码的维护,其它开发人员将总是发问“为何类 X 被声明成 final ?”。通常没人知道,当有人确实知道或喜欢猜测时,答案几乎总是“因为这能使它运行得更快”。普遍的理解是:将类或方法声明成 final 会使编译器更容易地内联方法调用,但是这种理解是不正确的(或者至少说是大大地言过其实了)。

 

final 类和方法在编程时可能是非常大的麻烦 - 它们限制您选择重用已有的代码和扩展已有类的功能。有时有很好的理由将类声明成 final (如强制不变性),此时使用 final 的益处将大于其不便之处。性能提高几乎总是成为破坏良好的面向对象设计原则的坏理由,而当性能提高很小或者根本没有提高时,则它真正是个很差的权衡方法。

 

过早优化

 

出于性能的考虑,在项目的早期阶段将方法或类声明成 final 是个坏主意,这有多个原因。首先,早期阶段设计不是考虑循环计算性能优化的时候,尤其当此类决定可能约束您使用 final 进行设计。其次,通过将方法或类声明成 final 而获得的性能优势通常为零。而且,将复杂的有状态的类声明成 final 不利于面向对象的设计,并导致体积庞大且面面俱到的类,因为它们不能轻松地重构成更小更紧凑的类。

 

和许多有关 Java 性能的神话一样,将类或方法声明成 final 会带来更佳的性能,这一错误观念被广泛接受但极少进行检验。其论点是:将方法或类声明成 final 意味着编译器可以更加积极地内联方法调用,因为它知道在运行时这正是要调用的方法的版本。但这显然是不正确的。仅仅因为类 X 编译成 final 类 Y,并不意味着同样版本的类 Y 将在运行时被装入。因此编译器不能安全地内联这样的跨类方法调用,不管是不是 final 。只有当方法是 private 时,编译器才能自由地内联它,在这种情况下, final 关键字是多余的。

 

另一方面,运行时环境和 JIT 编译器拥有更多有关真正装入什么类的信息,可以比编译者作出好得多的优化决定。如果运行时环境知道没有装入继承 Y 的类,那么它可以安全地内联对 Y 方法的调用,不管 Y 是不是 final (只要它能在随后装入 Y 子类时使这种 JIT 编译的代码无效)。因此事实是,尽管 final 对于不执行任何全局相关性分析的“哑”运行时优化器可能是有用的,但它的使用实际上不支持太多的编译时优化,而且智能的 JIT 执行运行时优化时不需要它。






似曾相识 - 重新回忆 register 关键字

 

final 用于优化决定时和 C 中不赞成使用的 register 关键字非常相似。让程序员帮助优化器这一愿望促成了 register 关键字,但事实上,发现这并不是很有用。正如我们在其它方面愿意相信的那样,在作出代码优化决定方面编译器通常比人做得出色,在现在的 RISC 处理器上更是如此。事实上,大多数 C 编译器完全忽略了 register 关键字。早先的 C 编译器忽略它是因为这些编译器根本就不起优化作用;现今的编译器忽略它是因为编译器不用它就能作更好的优化决定。任何一种情况下, register 关键字都没有添加什么性能优势,和应用于 Java 类或方法的 final 关键字很相似。如果您想优化您的代码,请坚持使用那些可以大幅度提高性能的优化,比如使用有效的算法且不执行冗余的计算 - 将循环计算优化留给编译器和 JVM 去做。






使用 final 保持不变性

 

虽然性能并不是将类或方法声明为 final 的好理由,然而有时仍有充足的理由编写 final 类。最常见的是 final 保证那些旨在不发生变化的类 保持 不变。不变类对于简化面向对象程序的设计非常有用 - 不变的对象只需要较少的防御性编码,并且不要求严格的同步。您不会在您的代码中构建这一设想:类是不变的,然后让某些人用使其可变的方式来继承它。将不变的类声明成 final 保证了这类错误不会偷偷溜进您的程序中。

 

final 用于类或方法的另一个原因是为了防止方法间的链接被破坏。例如,假定类 X 的某个方法的实现假设了方法 M 将以某种方式工作。将 X 或 M 声明成 final 将防止派生类以这种方式重新定义 M,从而导致 X 的工作不正常。尽管不用这些内部相关性来实现 X 可能会更好,但这不总是可行的,而且使用 final 可以防止今后这类不兼容的更改。

如果您必须使用 final 类或方法,请记录下为什么这么做

 

无论何种情况,当您确实选择了将方法或类声明成 final 时,请记录下为什么这样做的原因。否则,今后的维护人员将可能疑惑这样做是否有好的原因(因为经常没有);而且会被您的决定所约束,但同时还不知道您这样做的动机是为了得到什么好处。在许多情况下,将类或方法声明成 final 的决定一直推迟到开发过程后期是有意义的,那时您已经拥有关于类是如何交互及可能如何被继承的更好信息了。您可能发现您根本不需要将类声明为 final ,或者您可以重构类以便将 final 应用于更小更简单的类。






final 字段

 

final 字段和 final 类或方法有很大的不同,以至于我觉得让它们共享相同的关键字是不公平的。 final 字段是只读字段,要保证它的值在构建时(或者,对于 static final 字段,是在类初始化时)只设置一次。正如较早讨论的那样,对于 final 类和方法,您将总是问自己是否真的 需要 使用 final 。对于 final 字段,您将问自己相反的问题 - 这个字段真的 需要 是可变的吗?您可能会很惊讶,这个答案为何常常是“不需要”。

 

文档说明价值

 

final 字段有几个好处。对于那些想使用或继承您的类的开发人员来说,将字段声明成 final 有重要的文档说明好处 - 这不仅帮助解释了该类是如何工作的,还获得了编译器在加强您的设计决定方面的帮助。和 final 方法不同,声明 final 字段有助于优化器作出更好的优化决定,因为如果编译器知道字段的值不会更改,那么它能安全地在寄存器中高速缓存该值。 final 字段还通过让编译器强制该字段为只读来提供额外的安全级别。

 

极端情况下,一个类,其字段都是 final 原语或对不变对象的 final 引用,那么该类本身就变成是不变的 - 事实上这是一个非常方便的情况。即使该类不是完全不变的,使其某部分状态不变可以大大简化开发 - 您不必为了保证您正在查看 final 字段的当前值或者确保没有其他人在更改对象状态的这部分而保持同步。

 

那么为什么 final 字段使用得如此不足呢?一个原因是因为要正确使用它们有点麻烦,对于其构造器能抛出异常的对象引用来说尤其如此。因为 final 字段在每个构造器中必须只初始化一次,如果 final 对象引用的构造器可能抛出异常,编译器可能会报错,说该字段没有被初始化。编译器一般比较智能化,足以发现在两个互斥代码分支(比如, if...else 块)的每个分支中的初始化恰好只进行了一次,但是它对 try...catch 块通常不会如此“宽容”。例如,大多数 Java 编译器不会接受清单 1 中的代码:


清单 1. final 引用字段的无效初始化

public class Foo { 
  private final Thingie thingie;
  public Foo() {
    try { 
      thingie = new Thingie();
    }
    catch (ThingieConstructionException e) {
      thingie = Thingie.getDefaultThingie();
    }
  }
}


但是它们会接受清单 2 中的代码,它相当于:


清单 2. final 引用字段的有效初始化

public class Foo { 
  private final Thingie thingie;
  public Foo() {
    Thingie tempThingie;
    try { 
      tempThingie = new Thingie();
    }
    catch (ThingieConstructionException e) {
      tempThingie = Thingie.getDefaultThingie();
    }
    thingie = tempThingie;
  }
}







final 字段的局限性

 

final 字段仍然有一些严重的限制。尽管数组引用能被声明成 final ,但是该数组的元素却不能。这意味着暴露 public final 数组字段的或者通过它们的方法将引用返回给这些字段的类(例如,清单 3 中所示的 DangerousStates 类)都不是不可改变的。同样,尽管对象引用可以被声明成 final 字段,而它所引用的对象仍可能是可变的。如果您想要使用 final List ,例如清单 3 中所示的 SafeStates 类: 字段创建不变的对象,您必须防止对数组或可变对象的引用“逃离”您的类。要不用重复克隆该数组做到这一点,一个简单的方法是将数组转变成


清单 3. 暴露数组引用使类成为可变的

// Not immutable -- the states array could be modified by a malicious 
caller
public class DangerousStates {
  private final String[] states = new String[] { "Alabama", "Alaska", ... };
  public String[] getStates() { 
    return states;
  }
}
// Immutable -- returns an unmodifiable List instead
public class SafeStates {
  private final String[] states = new String[] { "Alabama", "Alaska", ... };
  private final List statesAsList 
    = new AbstractList() {
        public Object get(int n) { 
          return states[n];
        }
        public int size() {
          return states.length;
        }
      };
        
  public List getStates() {
    return statesAsList;
  }
}


为什么不继承 final 以应用于数组和引用的对象,类似于 C 和 C++ 中 const 的使用那样呢?C++ 中 const 的语义和使用相当混淆,根据它在表达式中所出现的位置表示不同的东西。Java 架构设计师设法把我们从这种混淆中“解救”出来,但遗憾的是他们在这个过程中产生出了一些新的混淆。






结束语

 

要对类、方法和字段有效使用 final ,有一些基本的准则可以遵循。特别要注意的是,不要尝试将 final 用作性能管理工具;要提高您的程序的性能,有更好且约束更少的方法。在反映您程序的基本语义处使用 final :用来指示这些类将是不可改变的或那些字段将是只读的。如果您选择创建 final 类或方法,请确保您清楚地记录您为何这么做 - 您的同事会感激您的。

 

 

 

转自http://www.ibm.com/developerworks/cn/java/j-jtp1029/

 

分享到:
评论

相关推荐

    有效使用 final 的准则1

    【有效使用 final 的准则】 在Java编程中,`final`关键字是一个重要的概念,它具有多重含义,但在实际应用中往往被误解或误用。本文主要探讨如何有效地利用`final`关键字,以避免过度使用和不必要的限制。 首先,`...

    Java学习总结(2023/03/19)

    13. 避免过度使用final关键字。 14. 使用枚举替代switch语句。 15. 避免使用原始类型进行参数校验。 16. 在equals()方法中比较对象时,要处理null值。 17. 使用私有静态工厂方法代替构造函数。 18. 优先考虑使用私有...

    Java编程规范-动力节点共9页.pdf.zip

    7. **常量与变量**:合理使用final关键字定义常量,避免全局变量的滥用,尽可能减少副作用。 8. **方法设计**:方法应尽可能短小,每个方法只做一件事。避免过长的方法,这样可以提高代码的可测试性和可维护性。 9...

    Java编程规范(第三版)

    合理使用final关键字,它可以增强代码的可预测性和安全性。 9. **错误处理**:避免在finally块中抛出新的异常,这会覆盖原有的异常信息。使用`System.exit()`应谨慎,因为它会立即终止程序,可能丢失异常信息。 10...

    Java编程开发的编码规范

    - 常量定义使用final关键字,并遵循大写命名规则。 - 避免使用匈牙利命名法,变量名应反映其用途,如`String userName`而非`sName`。 6. **异常处理**: - 避免空的`catch`块,至少打印错误信息或记录日志。 - ...

    2021-2022计算机二级等级考试试题及答案No.9408.docx

    18. 最终属性定义:在Java中,final常量的正确定义应包括类型、final关键字和初始值,如`static final int i=100`。 19. 图片框坐标系统:在某些编程环境中,如VB或VB.NET,可以使用Scale方法来重新定义图片框的...

    2021-2022计算机二级等级考试试题及答案No.15539.docx

    4. Java中的final关键字修饰的变量一旦初始化后就不能再改变。 5. 构造函数没有返回值。 6. ASCII码包含128个不同的字符。 7. ROM是只读存储器,用于存储固定不变的数据。 8. Excel2003中,输入"("和")"可以表示正...

    2021-2022计算机二级等级考试试题及答案No.4180.docx

    - 使用DISTINCT关键字可以检索去掉重复记录的所有元组。 13. 数据完整性: - 数据完整性确保存入数据库的数据是正确且一致的。 14. 字符数组:字符数组的一个元素通常用于存储单个字符。 15. 字节表示数值:一...

    Alibaba Java Coding Guidelines-1.0.4

    - 推荐使用`final`关键字修饰类、变量和方法,增加代码的确定性。 6. **并发编程** - 避免使用`wait()`, `notify()`, `notifyAll()`,推荐使用`java.util.concurrent`包中的高级并发工具。 - 对于线程安全的类,...

    2021-2022计算机二级等级考试试题及答案No.19759.docx

    7. Java类的继承性:使用final关键字修饰的类不能被其他类继承。 8. DOS命令:在DOS环境下,AUTOEXEC.BAT文件通常用来存放启动时执行的命令。根据描述,命令D.CDUSER会改变当前目录到USER,符合题目描述。 9. Word...

    JAVA研发规范[定义].pdf

    - **声明**:变量和常量声明应简洁明了,类型和变量名之间用空格隔开,常量声明通常用final关键字。 - **类声明**:类名首字母大写,每个单词首字母大写,类的公共成员应有适当的访问修饰符。 5. **命名规范** -...

    Java面向对象编程深入解析与实战案例

    此外还介绍了面向对象的设计原则、对象的生命周期管理以及高级话题诸如最终关键字final的使用等重要内容。 适合人群:具有一定Java基础的知识背景的研发人员,特别是希望提升面向对象设计能力的人群。 使用场景及...

    java编程规范

    - 考虑使用`final`关键字,有助于垃圾回收。 7. **设计模式**: - 学习并应用常见的设计模式,如工厂模式、单例模式、观察者模式等。 - 使用模式来解决常见问题,提高代码的可读性和复用性。 8. **单元测试**:...

    编程思想.docx

    在继承机制中,final关键字用于防止类被继承,防止方法被覆盖,或确保属性值不可变。子类可以通过extend关键字继承父类,并可以通过super关键字访问父类的非私有成员和非final成员。构造器的调用需要注意,子类构造...

    2021-2022计算机二级等级考试试题及答案No.16628.docx

    18. final 关键字可以修饰类、属性和方法;abstract 可修饰类和方法;抽象方法没有方法体;final 与 abstract 不能同时使用,因为它们的性质相冲突。 19. 强连通图的特征是任意两个顶点之间都有路径相连,对于 n 个...

    改善Java程序的151个建议

    **背景介绍**:在Java中,通常使用`final`关键字来定义常量,确保它们的值一旦初始化后就不能被修改。然而,有一些特殊情况可能会导致“常量”变得不再“常量”。 **案例解析**:书中的示例展示了一个接口中的常量`...

    java_编程常用英语单词_解释

    一旦使用 `final` 关键字定义了变量或方法,就不能再对其进行修改。 #### Fragments (段落; 代码块) - **发音**:['frægmәnt] - **定义**:Fragments 在编程中通常指的是代码的一部分,这部分代码可以单独运行或...

    阿里 Java 开发手册多版本.7z

    - 合理使用`final`关键字,有助于JVM优化。 9. **异常日志** - 异常信息应包含足够的上下文信息,便于定位问题。 - 使用统一的日志框架,如`Log4j`、`SLF4J`,并设置合适的日志级别。 10. **单元测试** - 每个...

    阿里巴巴java代码规范标准

    1. 使用`synchronized`关键字时,注意锁的对象和粒度,避免死锁和竞态条件。 2. 使用volatile确保变量在多线程环境中的可见性,但不保证原子性。 3. 避免无限制地创建线程,合理利用线程池。 八、设计模式与重构 1....

    2021-2022计算机二级等级考试试题及答案No.12288.docx

    13. Java或C++中,用final关键字修饰的类表示不能被继承。 14. 类构造函数不应声明为void,因为构造函数是用来初始化对象的,必须有返回值,即对象本身。所以选项B是错误的。 15. 计算机中,所有信息以二进制形式...

Global site tag (gtag.js) - Google Analytics