在 Java 中,我们看到很多可以使用 final
关键字的地方,但它的使用并不常见。
例如:
String str = "abc";
System.out.println(str);
在上述情况下,str
可以是 final
,但这通常被省略。
当一个方法永远不会被覆盖时,我们可以使用 final 关键字。同样,对于不会被继承的类。
在任何或所有这些情况下使用 final 关键字真的可以提高性能吗?如果是这样,那怎么办?请解释。如果正确使用 final
确实对性能很重要,那么 Java 程序员应该养成哪些习惯来充分利用该关键字?
通常不会。对于虚拟方法,HotSpot 会跟踪该方法是否实际上已被覆盖,并且能够在方法尚未被覆盖的情况下执行优化,例如内联 - 直到它加载一个覆盖该方法的类,此时它可以撤消(或部分撤消)这些优化。
(当然,这是假设您使用的是 HotSpot - 但它是迄今为止最常见的 JVM,所以......)
在我看来,您应该基于清晰的设计和可读性而不是出于性能原因使用 final
。如果您出于性能原因想要更改任何内容,则应在将最清晰的代码变形之前执行适当的测量 - 这样您就可以确定所获得的任何额外性能是否值得较差的可读性/设计。 (根据我的经验,这几乎不值得;YMMV。)
编辑:正如最后提到的那样,值得提出的是,无论如何,就清晰的设计而言,它们通常是一个好主意。它们还改变了跨线程可见性方面的保证行为:在构造函数完成后,保证任何最终字段立即在其他线程中可见。根据我的经验,这可能是 final
最常见的用法,尽管作为 Josh Bloch 的“设计继承或禁止它”经验法则的支持者,我可能应该更频繁地将 final
用于类......
简短的回答:别担心!
长答案:
在谈论最终局部变量时,请记住使用关键字 final
将有助于编译器静态地优化代码,这最终可能会产生更快的代码。例如,下面示例中的最终字符串 a + b
是静态连接的(在编译时)。
public class FinalTest {
public static final int N_ITERATIONS = 1000000;
public static String testFinal() {
final String a = "a";
final String b = "b";
return a + b;
}
public static String testNonFinal() {
String a = "a";
String b = "b";
return a + b;
}
public static void main(String[] args) {
long tStart, tElapsed;
tStart = System.currentTimeMillis();
for (int i = 0; i < N_ITERATIONS; i++)
testFinal();
tElapsed = System.currentTimeMillis() - tStart;
System.out.println("Method with finals took " + tElapsed + " ms");
tStart = System.currentTimeMillis();
for (int i = 0; i < N_ITERATIONS; i++)
testNonFinal();
tElapsed = System.currentTimeMillis() - tStart;
System.out.println("Method without finals took " + tElapsed + " ms");
}
}
结果?
Method with finals took 5 ms
Method without finals took 273 ms
在 Java Hotspot VM 1.7.0_45-b18 上测试。
那么实际的性能提升有多少呢?我不敢说。在大多数情况下可能是微不足道的(在此综合测试中约为 270 纳秒,因为完全避免了字符串连接 - 一种罕见的情况),但在高度优化的实用程序代码中它可能是一个因素。无论如何,原始问题的答案是肯定的,它可能会提高性能,但充其量只是微不足道。
除了编译时的好处,我找不到任何证据表明使用关键字 final
对性能有任何可衡量的影响。
String
连接比添加 Integers
的操作成本更高。静态执行(如果可能)更有效,这就是测试显示的内容。
是的,它可以。这是一个 final 可以提高性能的实例:
条件编译是一种不根据特定条件将代码行编译到类文件中的技术。这可用于删除生产构建中的大量调试代码。
考虑以下:
public class ConditionalCompile {
private final static boolean doSomething= false;
if (doSomething) {
// do first part.
}
if (doSomething) {
// do second part.
}
if (doSomething) {
// do third part.
}
if (doSomething) {
// do finalization part.
}
}
通过将 doSomething 属性转换为最终属性,您已经告诉编译器无论何时看到 doSomething,它都应该根据编译时替换规则将其替换为 false。编译器的第一遍将代码更改为如下所示:
public class ConditionalCompile {
private final static boolean doSomething= false;
if (false){
// do first part.
}
if (false){
// do second part.
}
if (false){
// do third part.
}
if (false){
// do finalization part.
}
}
完成此操作后,编译器会再次查看它,并发现代码中有无法访问的语句。由于您使用的是顶级编译器,因此它不喜欢所有那些无法访问的字节码。所以它会删除它们,你最终会得到这个:
public class ConditionalCompile {
private final static boolean doSomething= false;
public static void someMethodBetter( ) {
// do first part.
// do second part.
// do third part.
// do finalization part.
}
}
从而减少任何过多的代码,或任何不必要的条件检查。
编辑:作为一个例子,让我们看下面的代码:
public class Test {
public static final void main(String[] args) {
boolean x = false;
if (x) {
System.out.println("x");
}
final boolean y = false;
if (y) {
System.out.println("y");
}
if (false) {
System.out.println("z");
}
}
}
使用 Java 8 编译此代码并使用 javap -c Test.class
反编译时,我们得到:
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static final void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: ifeq 14
6: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #22 // String x
11: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: iconst_0
15: istore_2
16: return
}
我们可以注意到编译后的代码仅包含非最终变量 x
。这证明了最终变量对性能有影响,至少对于这个简单的情况。
根据 IBM - 它不适用于类或方法。
http://www.ibm.com/developerworks/java/library/j-jtp04223.html
令我惊讶的是,实际上没有人发布一些经过反编译的真实代码来证明至少存在一些细微差别。
作为参考,这已针对 javac
版本 8
、9
和 10
进行了测试。
假设这个方法:
public static int test() {
/* final */ Object left = new Object();
Object right = new Object();
return left.hashCode() + right.hashCode();
}
按原样编译此代码会生成与存在 final
时 (final Object left = new Object();
) 时相同的 exact 字节码。
但是这个:
public static int test() {
/* final */ int left = 11;
int right = 12;
return left + right;
}
产生:
0: bipush 11
2: istore_0
3: bipush 12
5: istore_1
6: iload_0
7: iload_1
8: iadd
9: ireturn
留下 final
会产生:
0: bipush 12
2: istore_1
3: bipush 11
5: iload_1
6: iadd
7: ireturn
代码几乎是不言自明的,如果有编译时间常数,它将直接加载到操作数堆栈中(它不会像前面的示例那样通过 bipush 12; istore_0; iload_0
存储到局部变量数组中) - 排序of 是有道理的,因为没有人可以改变它。
另一方面,为什么在第二种情况下编译器不产生 istore_0 ... iload_0
超出了我的理解,它不像插槽 0
以任何方式使用(它可以以这种方式缩小变量数组,但可能我错过了一些内部细节,不能确定)
我很惊讶看到这样的优化,考虑到小 javac
所做的事情。至于我们应该总是使用final
吗?我什至不打算编写 JMH
测试(我最初想这样做),我确信差异是 ns
的顺序(如果可能被捕获的话)。这可能是一个问题的唯一地方,是当一个方法因为它的大小而不能被内联时(并且声明 final
会将该大小缩小几个字节)。
还有两个final
需要解决。首先是当一个方法是 final
(从 JIT
的角度来看)时,这种方法是 单态 - 这些是 JVM
的 the most beloved。
然后有 final
个实例变量(必须在每个构造函数中设置);这些很重要,因为它们将保证正确发布的参考,as touched a bit here 并且也由 JLS
准确指定。
话虽这么说:这里的每个答案还有一件事是看不见的:garbage collection
。这将花费大量时间来解释,但是当您读取一个变量时,GC
有一个所谓的 barrier
用于该读取。每个 aload
和 getField
都通过这样的屏障 a lot more details here “保护”。理论上,final
字段不需要这样的“保护”(它们可以完全跳过障碍)。因此,如果 GC 这样做 - final
将提高性能。
javac -g FinalTest.java
) 的 Java 8 (JDK 1.8.0_162) 编译了代码,使用 javap -c FinalTest.class
反编译了代码,但没有获得相同的结果(使用 final int left=12
,我得到了 bipush 11; istore_0; bipush 12; istore_1; bipush 11; iload_1; iadd; ireturn
)。所以是的,生成的字节码取决于很多因素,很难说是否有 final
会对性能产生影响。但由于字节码不同,可能存在一些性能差异。
您实际上是在询问两种(至少)不同的情况:
final 用于局部变量 final 用于方法/类
Jon Skeet 已经回答了 2)。关于1):
我不认为这有什么不同;对于局部变量,编译器可以推断该变量是否为最终变量(只需检查它是否被多次赋值)。因此,如果编译器想要优化只分配一次的变量,无论变量是否实际声明为 final
,它都可以这样做。
final
可能对受保护/公共类字段产生影响;编译器很难确定该字段是否被多次设置,因为它可能来自不同的类(甚至可能尚未加载)。但即便如此,JVM 也可以使用 Jon 描述的技术(乐观地优化,如果加载了确实改变了字段的类,则恢复)。
总而言之,我看不出它应该有助于提高性能的任何理由。所以这种微优化不太可能有帮助。您可以尝试对其进行基准测试以确保,但我怀疑它会有所作为。
编辑:
实际上,根据 Timo Westkämper 的回答,final
在某些情况下可以提高类字段的性能。我站得更正了。
final
,编译器已经检查了这些情况,以确保您不会将其分配两次。据我所知,可以(并且可能)使用相同的检查逻辑来检查变量是否可以是final
。
javac
更好地优化。但这只是猜测。
final
变量是原始类型或 String
类型,并且像问题示例中那样立即分配有编译时常量,则编译器 必须 内联它,因为该变量是每个规范的编译时常量。但是对于大多数用例,代码可能看起来不同,但无论是内联常量还是从性能方面从局部变量读取,这仍然没有区别。
注意:不是java专家
如果我没记错我的 java,那么使用 final 关键字提高性能的方法很少。我一直都知道它的存在是为了“好代码”——设计和可读性。
Final(至少对于成员变量和参数)对于人类而言比对机器而言更多。
尽可能使变量成为最终变量是一种很好的做法。我希望 Java 在默认情况下将“变量”设为 final,并有一个“可变”关键字来允许更改。不可变的类会带来更好的线程代码,只要看一眼每个成员前面都有“final”的类就会很快表明它是不可变的。
另一种情况——我一直在转换大量代码以使用 @NonNull/@Nullable 注释(您可以说方法参数不能为空,然后 IDE 会在您传递未标记 @ 的变量的每个地方警告您NonNull——整个事情蔓延到一个荒谬的程度)。证明成员变量或参数在标记为 final 时不能为空要容易得多,因为您知道它不会在其他任何地方重新分配。
我的建议是养成在默认情况下为成员和参数应用 final 的习惯,它只是几个字符,但如果没有别的,它会推动你改进你的编码风格。
方法或类的 final 是另一个概念,因为它不允许一种非常有效的重用形式,并且并没有真正告诉读者太多。最好的用途可能是他们将 String 和其他内在类型设为 final 的方式,这样你就可以在任何地方依赖一致的行为——这可以防止很多错误(尽管有时我会喜欢扩展字符串......哦可能性)
final
会使代码变得更大……但它真的会变得更好吗?
final
不会像 SHOUTING 那样添加那么多! ;)
正如在别处提到的,局部变量的“final”,以及在较小程度上的成员变量,更多的是风格问题。
'final' 是您希望变量不改变的声明(即,变量不会改变!)。如果您违反了自己的约束,编译器可以通过抱怨来帮助您。
我同意如果标识符(对不起,我不能将不变的东西称为“变量”)默认情况下是最终的,并且要求您明确地说它们是变量,Java 会成为一种更好的语言。但是话虽如此,我通常不会对已初始化且从未分配过的局部变量使用“final”;它似乎太吵了。
(我确实在成员变量上使用 final )
我不是专家,但我想你应该在类或方法中添加 final
关键字,如果它不会被覆盖并且不理会变量。如果有任何方法可以优化这些事情,编译器会为你做这件事。
实际上,在测试一些与 OpenGL 相关的代码时,我发现在私有字段上使用 final 修饰符会降低性能。这是我测试的课程的开始:
public class ShaderInput {
private /* final */ float[] input;
private /* final */ int[] strides;
public ShaderInput()
{
this.input = new float[10];
this.strides = new int[] { 0, 4, 8 };
}
public ShaderInput x(int stride, float val)
{
input[strides[stride] + 0] = val;
return this;
}
// more stuff ...
这是我用来测试各种替代方案性能的方法,其中 ShaderInput 类:
public static void test4()
{
int arraySize = 10;
float[] fb = new float[arraySize];
for (int i = 0; i < arraySize; i++) {
fb[i] = random.nextFloat();
}
int times = 1000000000;
for (int i = 0; i < 10; ++i) {
floatVectorTest(times, fb);
arrayCopyTest(times, fb);
shaderInputTest(times, fb);
directFloatArrayTest(times, fb);
System.out.println();
System.gc();
}
}
在第三次迭代之后,随着虚拟机的预热,我一直得到这些数字,但没有最后的关键词:
Simple array copy took : 02.64
System.arrayCopy took : 03.20
ShaderInput took : 00.77
Unsafe float array took : 05.47
使用 final 关键字:
Simple array copy took : 02.66
System.arrayCopy took : 03.20
ShaderInput took : 02.59
Unsafe float array took : 06.24
请注意 ShaderInput 测试的数字。
我将这些字段设为公开还是私有都没有关系。
顺便说一句,还有一些更令人费解的事情。 ShaderInput 类优于所有其他变体,即使使用 final 关键字也是如此。这是了不起的 b/c 它基本上是一个包装浮点数组的类,而其他测试直接操作数组。必须弄清楚这一点。可能与 ShaderInput 的流畅界面有关。
此外 System.arrayCopy 实际上对于小型数组显然比在 for 循环中简单地将元素从一个数组复制到另一个数组要慢一些。并且使用 sun.misc.Unsafe(以及直接的 java.nio.FloatBuffer,此处未显示)执行非常小。
public ShaderInput x(final int stride, final float val) { input[strides[stride] + 0] = val;返回这个; }
以我的经验,做任何变量或字段 final 确实可以提高性能。
final int arraySize = 10;最终浮点[] fb = 新浮点[arraySize]; for (int i = 0; i < arraySize; i++) { fb[i] = random.nextFloat(); } 最终整数倍 = 1000000000; for (int i = 0; i < 10; ++i) { floatVectorTest(times, fb); arrayCopyTest(次, fb); shaderInputTest(次,FB); directFloatArrayTest(次, fb); System.out.println(); System.gc(); }
我不能说这是不常见的,因为据我所知,这是在 java 中声明常量的唯一方法。作为一名 javascript 开发人员,我知道关键字常量的重要性。如果您在生产级别工作,并且您的值永远不会被其他编码人员意外更改,例如 SSN 编号甚至名称之类的值。然后你必须使用 final 关键字来声明变量。如果某些类可以被继承,有时会很麻烦。因为如果很多人在一个团队中工作,那么有人可以继承一个类,扩展它,甚至更改父类的变量和方法。这可以用关键字 final 停止,因为除非使用 final 关键字,否则即使是静态变量也可以更改。就您的问题而言,我认为 final 关键字不会影响代码的性能,但它绝对可以通过确保其他团队成员不会意外修改任何需要保持不变的内容来防止人为错误。
final
关键字可以在 Java 中以五种方式使用。
类是最终的 引用变量是最终的 局部变量是最终的 方法是最终的
一个类是最终的:一个类是最终的意味着我们不能被扩展或继承意味着继承是不可能的。
类似地 - 对象是最终的:有时我们不会修改对象的内部状态,因此在这种情况下我们可以指定对象是最终对象。对象最终意味着不是变量也是最终的。
一旦引用变量成为最终变量,就不能将其重新分配给其他对象。但是可以改变对象的内容,只要它的字段不是最终的
final
是什么意思,而是使用 final
是否会影响性能。
不定期副业成功案例分享
final
,因为它使代码更易于理解,并有助于发现错误(因为它使程序员的意图明确)。由于这些样式问题,PMD 可能建议使用final
,而不是出于性能原因。Immutable classes are easier to design, implement, and use than mutable classes. They are less prone to error and are more secure.
。此外,An immutable object can be in exactly one state, the state in which it was created.
与Mutable objects, on the other hand, can have arbitrarily complex state spaces.
。根据我的个人经验,使用关键字final
应该突出开发人员倾向于不变性而不是“优化”代码的意图。我鼓励你阅读这一章,引人入胜!final
关键字可以减少字节码的数量,这可能会对性能产生影响。