103

So far I thought that effectively final and final are more or less equivalent and that the JLS would treat them similar if not identical in the actual behavior. Then I found this contrived scenario:

final int a = 97;
System.out.println(true ? a : 'c'); // outputs a

// versus

int a = 97;
System.out.println(true ? a : 'c'); // outputs 97

Apparently, the JLS makes an important difference between the two here and I am not sure why.

I read other threads like

but they do not go into such detail. After all, on a broader level they appear to be pretty much equivalent. But digging deeper, they apparently differ.

What is causing this behavior, can anyone provide some JLS definitions that explain this?


Edit: I found another related scenario:

final String a = "a";
System.out.println(a + "b" == "ab"); // outputs true

// versus

String a = "a";
System.out.println(a + "b" == "ab"); // outputs false

So the string interning also behaves differently here (I dont want to use this snippet in real code, just curious about the different behavior).

Holger
  • 285,553
  • 42
  • 434
  • 765
Zabuzard
  • 25,064
  • 8
  • 58
  • 82
  • 2
    Very interesting question! I would expect Java to behave the same for both cases, but I am now enlightened. I am asking myself if this was always the behaviour or if it differs in previous versions – Lino Sep 04 '20 at 09:58
  • 8
    @Lino The wording for the last quote in the great answer below is the same all the way back to [Java 6](https://docs.oracle.com/javase/specs/jls/se6/html/expressions.html#15.25): "If one of the operands is of type *T* where *T* is `byte`, `short`, or `char`, and the other operand is a constant expression of type `int` whose value is representable in type *T*, then the type of the conditional expression is *T*." --- Even found a Java 1.0 doc at Berkeley. [Same text](http://titanium.cs.berkeley.edu/doc/java-langspec-1.0/15.doc.html#5257). --- Yes, it has always been that way. – Andreas Sep 04 '20 at 10:07
  • 1
    The way you "find" things is interesting :P You're welcome :) – phant0m Sep 04 '20 at 20:52

2 Answers2

65

First of all, we are talking about local variables only. Effectively final does not apply to fields. This is important, since the semantics for final fields are very distinct and are subject to heavy compiler optimizations and memory model promises, see $17.5.1 on the semantics of final fields.

On a surface level final and effectively final for local variables are indeed identical. However, the JLS makes a clear distinction between the two which actually has a wide range of effects in special situations like this.


Premise

From JLS§4.12.4 about final variables:

A constant variable is a final variable of primitive type or type String that is initialized with a constant expression (§15.29). Whether a variable is a constant variable or not may have implications with respect to class initialization (§12.4.1), binary compatibility (§13.1), reachability (§14.22), and definite assignment (§16.1.1).

Since int is primitive, the variable a is such a constant variable.

Further, from the same chapter about effectively final:

Certain variables that are not declared final are instead considered effectively final: ...

So from the way this is worded, it is clear that in the other example, a is not considered a constant variable, as it is not final, but only effectively final.


Behavior

Now that we have the distinction, lets lookup what is going on and why the output is different.

You are using the conditional operator ? : here, so we have to check its definition. From JLS§15.25:

There are three kinds of conditional expressions, classified according to the second and third operand expressions: boolean conditional expressions, numeric conditional expressions, and reference conditional expressions.

In this case, we are talking about a numeric conditional expressions, from JLS§15.25.2:

The type of a numeric conditional expression is determined as follows:

And that is the part where the two cases get classified differently.

effectively final

The version that is effectively final is matched by this rule:

Otherwise, general numeric promotion (§5.6) is applied to the second and third operands, and the type of the conditional expression is the promoted type of the second and third operands.

Which is the same behavior as if you would do 5 + 'd', i.e. int + char, which results in int. See JLS§5.6

Numeric promotion determines the promoted type of all the expressions in a numeric context. The promoted type is chosen such that each expression can be converted to the promoted type, and, in the case of an arithmetic operation, the operation is defined for values of the promoted type. The order of expressions in a numeric context is not significant for numeric promotion. The rules are as follows:

[...]

Next, widening primitive conversion (§5.1.2) and narrowing primitive conversion (§5.1.3) are applied to some expressions, according to the following rules:

In a numeric choice context, the following rules apply:

If any expression is of type int and is not a constant expression (§15.29), then the promoted type is int, and other expressions that are not of type int undergo widening primitive conversion to int.

So everything is promoted to int as a is an int already. That explains the output of 97.

final

The version with the final variable is matched by this rule:

If one of the operands is of type T where T is byte, short, or char, and the other operand is a constant expression (§15.29) of type int whose value is representable in type T, then the type of the conditional expression is T.

The final variable a is of type int and a constant expression (because it is final). It is representable as char, hence the outcome is of type char. That concludes the output a.


String example

The example with the string equality is based on the same core difference, final variables are treated as constant expression/variable, and effectively final is not.

In Java, string interning is based on constant expressions, hence

"a" + "b" + "c" == "abc"

is true as well (dont use this construct in real code).

See JLS§3.10.5:

Moreover, a string literal always refers to the same instance of class String. This is because string literals - or, more generally, strings that are the values of constant expressions (§15.29) - are "interned" so as to share unique instances, using the method String.intern (§12.5).

Easy to overlook as it is primarily talking about literals, but it actually applies to constant expressions as well.

Zabuzard
  • 25,064
  • 8
  • 58
  • 82
  • 8
    The problem is that you would expect `... ? a : 'c'` to behave the same whether `a` is a *variable* or a *constant*. There is nothing apparently wrong with the expression. --- By contrast, `a + "b" == "ab"` is a **bad expression**, because strings needs to be compared using `equals()` ([How do I compare strings in Java?](https://stackoverflow.com/q/513832/5221149)). The fact that it "accidentally" works when `a` is a *constant*, is just a quirk of the interning of string literals. – Andreas Sep 04 '20 at 10:19
  • 5
    @Andreas Yes, but note that **string interning** is a clearly defined feature of Java. It is not a coincidence which might change tomorrow or in a different JVM. `"a" + "b" + "c" == "abc"` must be `true` in any valid Java implementation. – Zabuzard Sep 04 '20 at 10:30
  • 10
    True, it's a well-defined quirk, but `a + "b" == "ab"` is still a **wrong expression**. Even if you **know** that `a` is a *constant*, it is too error-prone to not call `equals()`. Or maybe *fragile* is a better word, i.e. too likely to fall apart when the code is maintained in the future. – Andreas Sep 04 '20 at 10:33
  • 2
    Note that even in the primary domain of effectively final variables, i.e. their use in lambda expressions, the difference may alter the runtime behavior, i.e. it can make the difference between a capturing and a non-capturing lambda expression, the latter evaluating to a singleton, but the former producing a new object. In other words, `(final) String str = "a"; Stream.of(null, null). map( x -> () -> System.out.println(str)) .reduce((a,b) -> () -> System.out.println(a == b)) .ifPresent(Runnable::run);` changes its outcome when `str` is (not) `final`. – Holger Sep 25 '20 at 16:13
7

Another aspect is that if the variable is declared final in the body of the method it has a different behaviour from a final variable passed as parameter.

public void testFinalParameters(final String a, final String b) {
  System.out.println(a + b == "ab");
}

...
testFinalParameters("a", "b"); // Prints false

while

public void testFinalVariable() {
   final String a = "a";
   final String b = "b";
   System.out.println(a + b == "ab");  // Prints true
}

...
testFinalVariable();

it happens because the compiler knows that using final String a = "a" the a variable will always have the "a" value so that a and "a" can be interchanged without problems. Differently, if a is not defined final or it is defined final but its value is assigned at runtime (as in the example above where final is the a parameter) the compiler doesn't know anything before its use. So the concatenation happens at runtime and a new string is generated, not using the intern pool.


Basically the behaviour is: if the compiler knows that a variable is a constant can use it the same as using the constant.

If the variable is not defined final (or it is final but its value is defined at runtime) there is no reason for the compiler to handle it as a constant also if its value is equal to a constant and its value is never changed.

Davide Lorenzo MARINO
  • 26,420
  • 4
  • 39
  • 56