就像它的表妹, const
用C关键字, final
的手段根据上下文几个不同的事情。 final
关键字可以应用于类,方法或字段。 当应用于类时,这意味着该类不能被子类化。 当应用于方法时,这意味着该方法不能被子类覆盖。 当应用到现场,就意味着该字段的值必须在每个构造正好分配一次,之后永远无法改变。
大多数Java文本描述正确的使用和使用的后果, 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执行运行时优化。
Deja vu-再次使用register关键字
利用final
的优化决策是非常相似的弃用的register
C中的关键字register
关键字是由愿望所驱使让程序员帮助优化,但在现实中,这竟然不是非常有帮助。 就像我们想相信的那样,编译器在做出代码优化决策方面通常比人类要好,尤其是在现代RISC处理器上。 实际上,大多数C编译器会完全忽略register
关键字。 早期的C编译器忽略了它,因为他们根本没有优化。 目前的编译器会忽略它,因为如果没有它,他们可以做出更好的优化决策。 在这两种情况下, register
关键字几乎都没有增加性能优势,就像将final
关键字应用于Java类或方法时一样。 如果要优化代码,则坚持使用将产生重大变化的优化,例如使用高效的算法而不执行冗余计算,而将循环计数优化留给编译器和JVM。
使用final保留不变性
虽然性能不是将类或方法声明为final
充分理由,但有时编写final
类仍然有充分的理由。 最常见的是, final
保证要成为不变的类保持不变。 不变类对于简化面向对象程序的设计非常有用-不变对象需要较少的防御性编码并提供宽松的同步要求。 您不想在代码中建立类是不可变的假设,然后让某人以使其可变的方式对其进行扩展。 将不可变的类声明为final
确保此类错误不会蔓延到您的程序中。
对类或方法使用final
另一个原因是防止方法之间的链接被破坏。 例如,假设类X的某些方法的实现假定方法M将以某种方式运行。 将X或M声明为final
将阻止派生类以导致X行为不正确的方式重新定义M。 虽然在没有这些内部依赖性的情况下实现X可能会更好,但它并不总是实用的,并且使用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
块中)中的每个初始化if...else
恰好构成了一个初始化,但是对try...catch
块通常不太宽容。 例如,大多数Java编译器不会接受清单1中的代码:
public class Foo {
private final Thingie thingie;
public Foo() {
try {
thingie = new Thingie();
}
catch (ThingieConstructionException e) {
thingie = Thingie.getDefaultThingie();
}
}
}
但是他们会接受清单2中的代码,该代码是等效的:
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
类。
// 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
语言反映程序基本语义的地方使用final
:表明类是不可变的,而字段是只读的。 如果您选择创建final
类或方法,请确保您清楚地记录了这样做的原因-您的同事将感谢您。
翻译自: https://www.ibm.com/developerworks/java/library/j-jtp1029/index.html