在Java中,我们看到许多可以使用final
关键字的地方,但它的使用并不常见。
例如:
String str = "abc";
System.out.println(str);
在上述情况下,str
可以是final
,但通常不会这样做。
当一个方法永远不会被覆盖时,我们可以使用final关键字。同样,如果一个类不会被继承。
在任何或所有这些案例中使用final关键字是否真的能提高性能?如果是这样,那怎么样?请解释。如果正确使用final
对性能有用,那么Java程序员应该养成哪些习惯来充分利用关键字?
答案 0 :(得分:255)
通常不是。对于虚方法,HotSpot会跟踪该方法是否已实际被覆盖,并且能够执行优化,例如内联假设方法尚未被覆盖 - 直到它加载一个覆盖该方法的类,此时它可以撤消(或部分撤销)这些优化。
(当然,假设你正在使用HotSpot - 但它是迄今为止最常见的JVM,所以...)
在我看来,您应该基于清晰的设计和可读性而不是出于性能原因而使用final
。如果您出于性能原因想要更改任何内容,则应在将最清晰的代码变形之前执行适当的测量 - 这样您就可以决定是否所获得的额外性能值得更差的可读性/设计。 (根据我的经验,它几乎不值得; YMMV。)
final
在我的经验中最常见的用法,虽然作为Josh Bloch的“继承设计或禁止它的设计”的经验支持者,我应该更频繁地使用final
来进行课程学习。 ..
答案 1 :(得分:71)
简短回答:别担心!
答案很长:
在谈论最终局部变量时请记住,使用关键字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
会对性能产生任何可测量的影响。
答案 2 :(得分:56)
是的,可以。这是一个最终可以提升绩效的实例:
条件编译是一种技术,其中代码行不会根据特定条件编译到类文件中。这可用于删除生产版本中的大量调试代码。
考虑以下事项:
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属性转换为final属性,您告诉编译器每当它看到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
。
这证明了最终变量对性能的影响,至少对于这个简单的情况而言。
答案 3 :(得分:36)
根据IBM的说法 - 它不适用于类或方法。
http://www.ibm.com/developerworks/java/library/j-jtp04223.html
答案 4 :(得分:12)
你真的在问两个(至少)不同的情况:
final
用于本地变量final
for methods / classes Jon Skeet已经回答了2)。关于1):
我不认为这会有所作为;对于局部变量,编译器可以推断出变量是否为final(只需检查它是否被赋值多次)。因此,如果编译器想要优化仅分配一次的变量,无论变量是否实际声明为final
,它都可以这样做。
final
可能对受保护/公共类字段产生影响;在那里,编译器很难找出该字段是否被多次设置,因为它可能发生在不同的类中(甚至可能没有加载)。但即使这样,JVM也可以使用Jon描述的技术(乐观地优化,如果加载了一个确实会改变字段的类,则还原)。
总之,我认为没有任何理由可以帮助提高性能。 所以这种微优化不太可能有所帮助。您可以尝试对其进行基准测试以确保,但我怀疑它会有所作为。
编辑:
实际上,根据TimoWestkämper的回答,在某些情况下,final
可以提高类字段的性能。我有所纠正。
答案 5 :(得分:10)
令我感到惊讶的是,没有人真正发布过一些经过反编译的真实代码,以证明至少存在一些细微差别。
作为参考,我们针对javac
版本8
,9
和10
对此进行了测试。
假设这个方法:
public static int test() {
/* final */ Object left = new Object();
Object right = new Object();
return left.hashCode() + right.hashCode();
}
按原样编译此代码,生成完全相同的字节代码,与final
存在时{(1}})相同。
但是这个:
final Object left = new Object();
产地:
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
代码几乎是不言自明的,如果有编译时常量,它将被直接加载到操作数堆栈上(它不会像前面的例子一样通过{{1}存储到局部变量数组中}) - 这是有道理的,因为没有人可以改变它。
另一方面,为什么在第二种情况下编译器不生成 0: bipush 12
2: istore_1
3: bipush 11
5: iload_1
6: iadd
7: ireturn
超出我的意思,它不像是以任何方式使用插槽bipush 12; istore_0; iload_0
(它可能会缩小变量数组这种方式,但可能是我缺少一些内部细节,无法确定)
我很惊讶地看到了这样的优化,考虑到istore_0 ... iload_0
的小事。至于我们应该总是使用0
吗?我甚至不打算写javac
测试(我最初想要),我确信差异是final
的顺序(如果可能的话,可以被捕获) 。这可能是一个问题的唯一地方是,当一个方法由于它的大小而无法内联时(并且声明JMH
会将该大小缩小几个字节)。
还有两个ns
需要解决。首先是方法是final
(从final
角度来看),这样的方法是单态 - 这些是final
JIT
}。
然后有JVM
个实例变量(必须在每个构造函数中设置);这些非常重要,因为它们可以保证正确发布的参考文献the most beloved,并且也完全由final
指定。
答案 6 :(得分:5)
注意:不是java专家
如果我正确记住了我的java,那么使用final关键字提高性能的方法很少。 我一直都知道它存在于“好代码” - 设计和可读性。
答案 7 :(得分:2)
最终(至少对于成员变量和参数而言)对人来说要比对机器要多。
优良作法是尽可能使变量为final。我希望Java默认情况下将“变量”定为final,并使用“ Mutable”关键字来允许更改。不可变的类可以产生更好的线程代码,只要浏览每个成员前面都带有“ final”的类,就可以很快证明它是不可变的。
另一种情况-我一直在转换很多代码以使用@ NonNull / @ Nullable批注(您可以说方法参数不能为null,然后IDE会在传递变量的任何地方警告您, t标记为@NonNull-整个事件蔓延到一个荒谬的程度)。在成员变量或参数被标记为final时,证明成员变量或参数不能为空要容易得多,因为您知道它不会在其他任何地方重新分配。
我的建议是养成默认情况下为成员和参数应用final的习惯,它只是几个字符,但如果没有其他问题,将带您改进编码风格。
方法或类的最终定义是另一个概念,因为它不允许非常有效的重用形式,并且并没有真正告诉读者很多信息。最好的用法可能是他们使String和其他内部类型最终化的方式,因此您可以在任何地方都依赖一致的行为-这样可以避免很多错误(尽管有时我喜欢扩展字符串....哦,可能性)
答案 8 :(得分:2)
正如其他地方所提到的,局部变量的“ final”(而成员变量的程度要小一些)则更多是样式问题。
'final'表示您希望变量保持不变(即变量不会改变!)。然后,编译器可以通过抱怨您是否违反自己的约束来帮助您。
我的观点是,如果默认情况下标识符是标识符(对不起,我不能将不变的东西称为“变量”),Java将是一种更好的语言,并要求您明确声明它们是变量。但是,话虽如此,我通常不对已初始化且从未分配过的局部变量使用“ final”。似乎太吵了。
(我在成员变量上使用final)
答案 9 :(得分:1)
我不是专家,但我认为如果不会覆盖{@ 1}}关键字,则应将final
关键字添加到该类或方法中,并保留变量。如果有任何方法可以优化这些东西,编译器会为你做这件事。
答案 10 :(得分:1)
实际上,在测试一些与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();
}
}
在第3次迭代之后,随着VM的预热,我一直得到这些数字没有最后的关键词:
Simple array copy took : 02.64
System.arrayCopy took : 03.20
ShaderInput took : 00.77
Unsafe float array took : 05.47
使用最终关键字:
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,此处未显示)执行得非常糟糕。
答案 11 :(得分:0)
以final声明的成员将在整个计划中可用,因为 与非最终成员不同,如果这些成员尚未在程序中使用,则垃圾收集器仍不会照顾他们。 由于内存管理不善可能会导致性能问题。
答案 12 :(得分:-1)
肯定是的,如果变量转换为常量,
因为我们知道java编译器将这样的最终变量转换为可能的常量
作为常量java编译器的概念,在编译时直接用它的引用替换值
如果是没有任何运行时进程的字符串或基本类型,则java变量中的变为常量
否则它只是最终(不可更改)变量,
&安培; constatnt的使用总是比参考更快。
因此,如果可能,请使用任何编程语言中的常量以获得更好的性能
答案 13 :(得分:-3)
final
关键字可以在Java中以五种方式使用。
一个类是final:一个类是final意味着我们不能被扩展或继承意味着继承是不可能的。
同样 - 一个对象是最终的:有时我们没有修改对象的内部状态,所以在这种情况下我们可以指定对象是最终对象。对象最终意味着不是变量也是最终对象。
一旦参考变量成为最终变量,就无法将其重新分配给其他对象。但只要字段不是最终的
,就可以更改对象的内容