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).
In simple terms, objects or primitive values are effectively final if we do not change their values after initialization. In the case of objects, if we do not change the reference of an object, then it is effectively final — even if a change occurs in the state of the referenced object.
The local variables that a lambda expression may use are referred to as “effectively final”. An effectively final variable is one whose value doesn't change after it's first assigned. There is no need to explicitly declare such a variable as final, although doing so would not be an error.
A variable is considered an effective final if it is not modified after initialization in the local block. This means you can now use the local variable without the final keyword inside an anonymous class or lambda expression, provided they must be effectively final.
Forcing the variable to be final avoids giving the impression that incrementing start inside the lambda could actually modify the start method parameter.
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.
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.
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.
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 isint
, and other expressions that are not of typeint
undergo widening primitive conversion toint
.
So everything is promoted to int
as a
is an int
already. That explains the output of 97
.
The version with the final
variable is matched by this rule:
If one of the operands is of type
T
whereT
isbyte
,short
, orchar
, and the other operand is a constant expression (§15.29) of typeint
whose value is representable in typeT
, then the type of the conditional expression isT
.
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
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With