7

Fact:

javac is programmed to detect if a variable is final or if it can be treated as effectively final.

Proof:

This code illustrates this.

public static void finalCheck() {
        String str1 = "hello";
        Runnable r = () -> {
             str1 = "hello";
        };
}

This fails to compile because compiler is able to detect String reference str1 is being re-assigned in function.

Now

Situation 1:

Javac does great optimization for final String instances by avoiding to create StringBuilder and related operations.

Proof

This java method

  public static void finalCheck() {
    final String str1 = "hello";
    final String str2 = "world";
    String str3 = str1 + " " + str2;
    System.out.println(str3);
  }

Compiles to

  public static void finalCheck();
    Code:
       0: ldc           #3                  // String hello world
       2: astore_2
       3: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: aload_2
       7: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      10: return

Question:

But now when we have them as effectively final

public static void finalCheck() {
    String str1 = "hello";
    String str2 = "world";
    String str3 = str1 + " " + str2;
    System.out.println(str3);
}

It doesn't optimize the similar way and ends up compiling into

  public static void finalCheck();
    Code:
       0: ldc           #3                  // String hello
       2: astore_0
       3: ldc           #4                  // String world
       5: astore_1
       6: aload_0
       7: aload_1
       8: invokedynamic #5,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
      13: astore_2
      14: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      17: aload_2
      18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      21: return

JVM

$java -version
java version "10" 2018-03-20
Java(TM) SE Runtime Environment 18.3 (build 10+46)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10+46, mixed mode)

Compiler

$javac -version
javac 10

question: why doesn't it optimize for effective final ?

jmj
  • 237,923
  • 42
  • 401
  • 438
  • 5
    What's your question? – shmosel Apr 17 '18 at 02:27
  • @shmosel explicitly specified question, thanks for pointing it out – jmj Apr 17 '18 at 04:36
  • 2
    It isn't required to do this optimization, or for the first `final` case either as a matter of fact. Any answer as to why it does one and not the other is primarily opinion-based, unless you can get the compiler author here. – user207421 Apr 17 '18 at 04:44
  • `javac` does not because it is specified this way. Yet I think that the jit compiler is capable of handling this, such that both examples are equal in efficiency. However, to proof it we will need a microbenchmark. – CoronA Apr 17 '18 at 05:24
  • 1
    @CoronA It isn't 'specified this way'. It isn't specified either way. If you believe otherwise please provide citation from JLS. – user207421 Apr 17 '18 at 05:46
  • 1
    @EJP surprisingly, the specification is explicit enough to mandate the behavior shown by `javac` regarding both cases. I added answer... – Holger May 05 '18 at 22:06

1 Answers1

6

The introduction of the effectively final concept did not affect the rules regarding constant expressions and string concatenation.

Refer to the Java® Language Specification, §15.18.1. String Concatenation Operator +

The String object is newly created (§12.5) unless the expression is a constant expression (§15.28).

The referenced section, §12.5. Creation of New Class Instances, removes any doubt:

Execution of a string concatenation operator + (§15.18.1) that is not part of a constant expression (§15.28) always creates a new String object to represent the result.

So while certain constructs may have a predictable string result, even when not being a constant expression, replacing them with a constant result would violate the specification. Only constant expressions may (event must) get replaced by their constant value at compile time. Regarding referenced variables, §15.28 states that they must be constant variables according to §4.12.4 to be constant expressions:

A constant variable is a final variable of primitive type or type String that is initialized with a constant expression (§15.28).

Note the requirement of being final for a constant variable.

There is also the concept of implicitly final variables, which is different to effectively final:

Three kinds of variable are implicitly declared final: a field of an interface (§9.3), a local variable declared as a resource of a try-with-resources statement (§14.20.3), and an exception parameter of a multi-catch clause (§14.20). An exception parameter of a uni-catch clause is never implicitly declared final, but may be effectively final.

So, not much surprising, interface fields are implicitly final (they are also implicitly static), as they always were, and the other two cases of implicitly final variables can never be strings nor of a primitive type, hence are never constants.

Effectively final variables are treated specially (like final variables) only in certain use cases

  • Rethrowing caught exceptions with more freedom (improved type checking) (since Java 7)
  • They may be referenced (captured) by lambda expressions and inner classes (since Java 8)
  • Refer to them with the try-with-resource (try(existingVariable) { … } (since Java 9)

but otherwise, they are not treated like final variables.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • 1
    Right, but there is an opportunity of optimizing, I was looking for a technical reason of why compiler skips it. Is there anything technically breaking if it did. ? – jmj May 08 '18 at 00:47
  • 2
    You mean, violating the specification is not reason enough not to do it? – Holger May 08 '18 at 06:06
  • 2
    By the way, it irritates me that you stated that you used javac 10 but the bytecode looks like pre-Java 9. Did you use the `-target` or `--release` option? – Holger May 08 '18 at 06:38
  • 1
    @Holger well yes, violating it in the *current form*, but why not change it? I mean there is obvious room for improvement here, the byte-code is considerably smaller in this case. They introduced effectively final, why not support it further? I don't get it. I understand the reasoning via the specification, but otherwise I am a bit confused. good answer though – Eugene May 08 '18 at 12:26
  • 3
    @Eugene well, as you can see in my answer, “effectively final” was introduced in Java 7 for a very limited feature and gradually got new use cases with every version. So don’t be surprised if it gets more in future versions, i.e. [JEP303](http://openjdk.java.net/jeps/303) suggests that the new “constant expressions” could include effectively final variables if they are initialized with constant expressions. Since it relates to JEP 309 which is targeted for Java 11, it might not last that long. But the answer to “why don’t we have it today?” is “because it didn’t change the last two decades”… – Holger May 08 '18 at 13:04
  • 1
    @Holger - You are right about bytecode version, it was IDE mis configuration. I added the raw bytecode using external JDK tools. – jmj May 08 '18 at 17:27