How does casting this object to a generic type work?
up vote
10
down vote
favorite
My understanding is generic types are invariant, so if we have B
as a subtype of A
, then List<B>
has no relationship with List<A>
. So casting won't work on List<A>
and List<B>
.
From Effective Java Third Edition we have this snippet of code:
// Generic singleton factory pattern
private static UnaryOperator<Object> IDENTIFY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identifyFunction() {
return (UnaryOperator<T>) IDENTIFY_FN; //OK But how, why?
}
public static void main(String args) {
String strings = {"a", "b", "c"};
UnaryOperator<String> sameString = identifyFunction();
for (String s : strings) {
System.out.println(sameString.apply(s));
}
}
Here I am confused. We have cast IDENTIFY_FN
, whose type is UnaryOperator<Object>
, to UnaryOperator<T>
, which has another type parameter.
When type erasure happens String is a subtype of Object, but so far as I know UnaryOperator<String>
is not a subtype of UnaryOperator<Object>
.
Do Object and T relate somehow? And how does casting succeed in this case?
java generics casting effective-java
add a comment |
up vote
10
down vote
favorite
My understanding is generic types are invariant, so if we have B
as a subtype of A
, then List<B>
has no relationship with List<A>
. So casting won't work on List<A>
and List<B>
.
From Effective Java Third Edition we have this snippet of code:
// Generic singleton factory pattern
private static UnaryOperator<Object> IDENTIFY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identifyFunction() {
return (UnaryOperator<T>) IDENTIFY_FN; //OK But how, why?
}
public static void main(String args) {
String strings = {"a", "b", "c"};
UnaryOperator<String> sameString = identifyFunction();
for (String s : strings) {
System.out.println(sameString.apply(s));
}
}
Here I am confused. We have cast IDENTIFY_FN
, whose type is UnaryOperator<Object>
, to UnaryOperator<T>
, which has another type parameter.
When type erasure happens String is a subtype of Object, but so far as I know UnaryOperator<String>
is not a subtype of UnaryOperator<Object>
.
Do Object and T relate somehow? And how does casting succeed in this case?
java generics casting effective-java
add a comment |
up vote
10
down vote
favorite
up vote
10
down vote
favorite
My understanding is generic types are invariant, so if we have B
as a subtype of A
, then List<B>
has no relationship with List<A>
. So casting won't work on List<A>
and List<B>
.
From Effective Java Third Edition we have this snippet of code:
// Generic singleton factory pattern
private static UnaryOperator<Object> IDENTIFY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identifyFunction() {
return (UnaryOperator<T>) IDENTIFY_FN; //OK But how, why?
}
public static void main(String args) {
String strings = {"a", "b", "c"};
UnaryOperator<String> sameString = identifyFunction();
for (String s : strings) {
System.out.println(sameString.apply(s));
}
}
Here I am confused. We have cast IDENTIFY_FN
, whose type is UnaryOperator<Object>
, to UnaryOperator<T>
, which has another type parameter.
When type erasure happens String is a subtype of Object, but so far as I know UnaryOperator<String>
is not a subtype of UnaryOperator<Object>
.
Do Object and T relate somehow? And how does casting succeed in this case?
java generics casting effective-java
My understanding is generic types are invariant, so if we have B
as a subtype of A
, then List<B>
has no relationship with List<A>
. So casting won't work on List<A>
and List<B>
.
From Effective Java Third Edition we have this snippet of code:
// Generic singleton factory pattern
private static UnaryOperator<Object> IDENTIFY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identifyFunction() {
return (UnaryOperator<T>) IDENTIFY_FN; //OK But how, why?
}
public static void main(String args) {
String strings = {"a", "b", "c"};
UnaryOperator<String> sameString = identifyFunction();
for (String s : strings) {
System.out.println(sameString.apply(s));
}
}
Here I am confused. We have cast IDENTIFY_FN
, whose type is UnaryOperator<Object>
, to UnaryOperator<T>
, which has another type parameter.
When type erasure happens String is a subtype of Object, but so far as I know UnaryOperator<String>
is not a subtype of UnaryOperator<Object>
.
Do Object and T relate somehow? And how does casting succeed in this case?
java generics casting effective-java
java generics casting effective-java
edited Nov 17 at 3:17
Boann
36.5k1286119
36.5k1286119
asked Nov 16 at 12:00
hamza belmellouki
869
869
add a comment |
add a comment |
4 Answers
4
active
oldest
votes
up vote
4
down vote
accepted
Generics don't exist at runtime. At runtime, every UnaryOperator<T>
is a UnaryOperator<Object>
. The cast is necessary to placate the compiler at compile-time. At runtime it's meaningless.
2
The question was why the cast is legal at compile-time. A direct cast like(UnityOperator<String>) IDENTITY_FN
would not be legal.
– ohlec
Nov 16 at 12:19
1
@ohlec He already answered that. Generics are invariant.UnityOperator<String> != UnityOperator<Object>
. As I said,UnaryOperator<T>
isUnaryOperator<Object>
.
– Michael
Nov 16 at 12:21
1
@ohlec What's your point? Type bounds are compile-time checks. Casts are runtime operations. A compiler will stop you doing a cast that it knows cannot succeed (useful) but otherwise will not step in.
– Michael
Nov 16 at 12:28
I think this is exactly what the asker was unsure about. The compiler complains when it knows a cast will fail, but not when it knows a cast might fail, like in this case.
– ohlec
Nov 16 at 12:34
Even though I upvoted your answer, I have problems explaining why the compiler does not complains about this:private static List<Object> IDENTIFY_FN = new ArrayList<Object>(); @SuppressWarnings("unchecked") public static <T> UnaryOperator<T> identifyFunction() { return (UnaryOperator<T>) IDENTIFY_FN; }
– Eugen Covaci
Nov 16 at 12:40
|
show 4 more comments
up vote
3
down vote
The JLS allows such cast:
A cast from a type S to a parameterized type T is unchecked
unless at least one of the following conditions holds:
S <: T
All of the type arguments of T are unbounded wildcards.
[ ... ]
As a result, an unchecked cast causes a compile-time unchecked warning, unless suppressed by the SuppressWarnings
annotation.
Furthermore, during the type erasure process, identifyFunction
and IDENTIFY_FN
compiles into:
private static UnaryOperator IDENTIFY_FN;
public static UnaryOperator identifyFunction() {
return IDENTIFY_FN; // cast is removed
}
and the checkcast
is added to the call site:
System.out.println(sameString.apply(s));
^
INVOKEINTERFACE java/util/function/UnaryOperator.apply (Ljava/lang/Object)Ljava/lang/Object
CHECKCAST java/lang/String
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
checkcast
succeeds, because the identity function returns its argument unmodified.
add a comment |
up vote
2
down vote
The cast
return (UnaryOperator<T>) IDENTIFY_FN;
basically amounts to a cast to the raw type UnaryOperator
, because T
is erased at runtime and ignored for the purpose of casts at compile-time. You can cast a generic type to its raw type (for backwards compatibility reasons), but you should get an "unchecked" warning.
This would also work, for example:
UnaryOperator<String> foo = (UnaryOperator) IDENTITY_FN;
add a comment |
up vote
2
down vote
This cast compiles, because it's a special case of a narrowing conversion. (According to §5.5, narrowing conversions are one of the types of conversions allowed by a cast, so most of this answer is going to focus on the rules for narrowing conversions.)
Note that while UnaryOperator<T>
is not a subtype of UnaryOperator<Object>
(so the cast isn't a "downcast"), it's still considered a narrowing conversion. From §5.6.1:
A narrowing reference conversion treats expressions of a reference type
S
as expressions of a different reference typeT
, whereS
is not a subtype ofT
. [...] Unlike widening reference conversion, the types need not be directly related. However, there are restrictions that prohibit conversion between certain pairs of types when it can be statically proven that no value can be of both types.
Some of these "sideways" casts fail due to special rules, for example the following will fail:
List<String> a = ...;
List<Double> b = (List<String>) a;
Specifically, this is given by a rule in §5.1.6.1 which states that:
If there exists a parameterized type
X
that is a supertype ofT
, and a parameterized typeY
that is a supertype ofS
, such that the erasures ofX
andY
are the same, thenX
andY
are not provably distinct (§4.5).
Using types from the
java.util
package as an example, no narrowing reference conversion exists fromArrayList<String>
toArrayList<Object>
, or vice versa, because the type argumentsString
andObject
are provably distinct. For the same reason, no narrowing reference conversion exists fromArrayList<String>
toList<Object>
, or vice versa. The rejection of provably distinct types is a simple static gate to prevent "stupid" narrowing reference conversions.
In other words, if a
and b
have a common supertype with the same erasure (in this case, for example, List
), then they must be what the JLS is calling "provably distinct", given by §4.5:
Two parameterized types are provably distinct if either of the following is true:
They are parameterizations of distinct generic type declarations.
Any of their type arguments are provably distinct.
And §4.5.1:
Two type arguments are provably distinct if one of the following is true:
Neither argument is a type variable or wildcard, and the two arguments are not the same type.
One type argument is a type variable or wildcard, with an upper bound (from capture conversion, if necessary) of
S
; and the other type argumentT
is not a type variable or wildcard; and neither|S| <: |T|
nor|T| <: |S|
.
Each type argument is a type variable or wildcard, with upper bounds (from capture conversion, if necessary) of
S
andT
; and neither|S| <: |T|
nor|T| <: |S|
.
So, given the above rules, List<String>
and List<Double>
are provably distinct (via the 1st rule from 4.5.1), because String
and Double
are different type arguments.
However, UnaryOperator<T>
and UnaryOperator<Object>
are not provably distinct (via the 2nd rule from 4.5.1), because:
One type argument is a type variable (
T
, with a bound ofObject
.)The bound of that type variable is the same as the type argument to the other type (
Object
).
Since UnaryOperator<T>
and UnaryOperator<Object>
are not provably distinct, the narrowing conversion is allowed, hence the cast compiles.
One way to think about why the compiler permits some of these casts but not others is: in the case of the type variable, it can't prove that T
definitely isn't Object
. For example, we could have a situation like this:
UnaryOperator<String> aStringThing = Somewhere::doStringThing;
UnaryOperator<Double> aDoubleThing = Somewhere::doDoubleThing;
<T> UnaryOperator<T> getThing(Class<T> t) {
if (t == String.class)
return (UnaryOperator<T>) aStringThing;
if (t == Double.class)
return (UnaryOperator<T>) aDoubleThing;
return null;
}
In those cases, we actually know the cast is correct as long as nobody else is doing something funny (like unchecked casting the Class<T>
argument).
So in the general case of casting to UnaryOperator<T>
, we might actually be doing something legitimate. In comparison, with the case of casting List<String>
to List<Double>
, we can say pretty authoritatively that it's always wrong.
add a comment |
4 Answers
4
active
oldest
votes
4 Answers
4
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
4
down vote
accepted
Generics don't exist at runtime. At runtime, every UnaryOperator<T>
is a UnaryOperator<Object>
. The cast is necessary to placate the compiler at compile-time. At runtime it's meaningless.
2
The question was why the cast is legal at compile-time. A direct cast like(UnityOperator<String>) IDENTITY_FN
would not be legal.
– ohlec
Nov 16 at 12:19
1
@ohlec He already answered that. Generics are invariant.UnityOperator<String> != UnityOperator<Object>
. As I said,UnaryOperator<T>
isUnaryOperator<Object>
.
– Michael
Nov 16 at 12:21
1
@ohlec What's your point? Type bounds are compile-time checks. Casts are runtime operations. A compiler will stop you doing a cast that it knows cannot succeed (useful) but otherwise will not step in.
– Michael
Nov 16 at 12:28
I think this is exactly what the asker was unsure about. The compiler complains when it knows a cast will fail, but not when it knows a cast might fail, like in this case.
– ohlec
Nov 16 at 12:34
Even though I upvoted your answer, I have problems explaining why the compiler does not complains about this:private static List<Object> IDENTIFY_FN = new ArrayList<Object>(); @SuppressWarnings("unchecked") public static <T> UnaryOperator<T> identifyFunction() { return (UnaryOperator<T>) IDENTIFY_FN; }
– Eugen Covaci
Nov 16 at 12:40
|
show 4 more comments
up vote
4
down vote
accepted
Generics don't exist at runtime. At runtime, every UnaryOperator<T>
is a UnaryOperator<Object>
. The cast is necessary to placate the compiler at compile-time. At runtime it's meaningless.
2
The question was why the cast is legal at compile-time. A direct cast like(UnityOperator<String>) IDENTITY_FN
would not be legal.
– ohlec
Nov 16 at 12:19
1
@ohlec He already answered that. Generics are invariant.UnityOperator<String> != UnityOperator<Object>
. As I said,UnaryOperator<T>
isUnaryOperator<Object>
.
– Michael
Nov 16 at 12:21
1
@ohlec What's your point? Type bounds are compile-time checks. Casts are runtime operations. A compiler will stop you doing a cast that it knows cannot succeed (useful) but otherwise will not step in.
– Michael
Nov 16 at 12:28
I think this is exactly what the asker was unsure about. The compiler complains when it knows a cast will fail, but not when it knows a cast might fail, like in this case.
– ohlec
Nov 16 at 12:34
Even though I upvoted your answer, I have problems explaining why the compiler does not complains about this:private static List<Object> IDENTIFY_FN = new ArrayList<Object>(); @SuppressWarnings("unchecked") public static <T> UnaryOperator<T> identifyFunction() { return (UnaryOperator<T>) IDENTIFY_FN; }
– Eugen Covaci
Nov 16 at 12:40
|
show 4 more comments
up vote
4
down vote
accepted
up vote
4
down vote
accepted
Generics don't exist at runtime. At runtime, every UnaryOperator<T>
is a UnaryOperator<Object>
. The cast is necessary to placate the compiler at compile-time. At runtime it's meaningless.
Generics don't exist at runtime. At runtime, every UnaryOperator<T>
is a UnaryOperator<Object>
. The cast is necessary to placate the compiler at compile-time. At runtime it's meaningless.
answered Nov 16 at 12:13
Michael
17.9k73167
17.9k73167
2
The question was why the cast is legal at compile-time. A direct cast like(UnityOperator<String>) IDENTITY_FN
would not be legal.
– ohlec
Nov 16 at 12:19
1
@ohlec He already answered that. Generics are invariant.UnityOperator<String> != UnityOperator<Object>
. As I said,UnaryOperator<T>
isUnaryOperator<Object>
.
– Michael
Nov 16 at 12:21
1
@ohlec What's your point? Type bounds are compile-time checks. Casts are runtime operations. A compiler will stop you doing a cast that it knows cannot succeed (useful) but otherwise will not step in.
– Michael
Nov 16 at 12:28
I think this is exactly what the asker was unsure about. The compiler complains when it knows a cast will fail, but not when it knows a cast might fail, like in this case.
– ohlec
Nov 16 at 12:34
Even though I upvoted your answer, I have problems explaining why the compiler does not complains about this:private static List<Object> IDENTIFY_FN = new ArrayList<Object>(); @SuppressWarnings("unchecked") public static <T> UnaryOperator<T> identifyFunction() { return (UnaryOperator<T>) IDENTIFY_FN; }
– Eugen Covaci
Nov 16 at 12:40
|
show 4 more comments
2
The question was why the cast is legal at compile-time. A direct cast like(UnityOperator<String>) IDENTITY_FN
would not be legal.
– ohlec
Nov 16 at 12:19
1
@ohlec He already answered that. Generics are invariant.UnityOperator<String> != UnityOperator<Object>
. As I said,UnaryOperator<T>
isUnaryOperator<Object>
.
– Michael
Nov 16 at 12:21
1
@ohlec What's your point? Type bounds are compile-time checks. Casts are runtime operations. A compiler will stop you doing a cast that it knows cannot succeed (useful) but otherwise will not step in.
– Michael
Nov 16 at 12:28
I think this is exactly what the asker was unsure about. The compiler complains when it knows a cast will fail, but not when it knows a cast might fail, like in this case.
– ohlec
Nov 16 at 12:34
Even though I upvoted your answer, I have problems explaining why the compiler does not complains about this:private static List<Object> IDENTIFY_FN = new ArrayList<Object>(); @SuppressWarnings("unchecked") public static <T> UnaryOperator<T> identifyFunction() { return (UnaryOperator<T>) IDENTIFY_FN; }
– Eugen Covaci
Nov 16 at 12:40
2
2
The question was why the cast is legal at compile-time. A direct cast like
(UnityOperator<String>) IDENTITY_FN
would not be legal.– ohlec
Nov 16 at 12:19
The question was why the cast is legal at compile-time. A direct cast like
(UnityOperator<String>) IDENTITY_FN
would not be legal.– ohlec
Nov 16 at 12:19
1
1
@ohlec He already answered that. Generics are invariant.
UnityOperator<String> != UnityOperator<Object>
. As I said, UnaryOperator<T>
is UnaryOperator<Object>
.– Michael
Nov 16 at 12:21
@ohlec He already answered that. Generics are invariant.
UnityOperator<String> != UnityOperator<Object>
. As I said, UnaryOperator<T>
is UnaryOperator<Object>
.– Michael
Nov 16 at 12:21
1
1
@ohlec What's your point? Type bounds are compile-time checks. Casts are runtime operations. A compiler will stop you doing a cast that it knows cannot succeed (useful) but otherwise will not step in.
– Michael
Nov 16 at 12:28
@ohlec What's your point? Type bounds are compile-time checks. Casts are runtime operations. A compiler will stop you doing a cast that it knows cannot succeed (useful) but otherwise will not step in.
– Michael
Nov 16 at 12:28
I think this is exactly what the asker was unsure about. The compiler complains when it knows a cast will fail, but not when it knows a cast might fail, like in this case.
– ohlec
Nov 16 at 12:34
I think this is exactly what the asker was unsure about. The compiler complains when it knows a cast will fail, but not when it knows a cast might fail, like in this case.
– ohlec
Nov 16 at 12:34
Even though I upvoted your answer, I have problems explaining why the compiler does not complains about this:
private static List<Object> IDENTIFY_FN = new ArrayList<Object>(); @SuppressWarnings("unchecked") public static <T> UnaryOperator<T> identifyFunction() { return (UnaryOperator<T>) IDENTIFY_FN; }
– Eugen Covaci
Nov 16 at 12:40
Even though I upvoted your answer, I have problems explaining why the compiler does not complains about this:
private static List<Object> IDENTIFY_FN = new ArrayList<Object>(); @SuppressWarnings("unchecked") public static <T> UnaryOperator<T> identifyFunction() { return (UnaryOperator<T>) IDENTIFY_FN; }
– Eugen Covaci
Nov 16 at 12:40
|
show 4 more comments
up vote
3
down vote
The JLS allows such cast:
A cast from a type S to a parameterized type T is unchecked
unless at least one of the following conditions holds:
S <: T
All of the type arguments of T are unbounded wildcards.
[ ... ]
As a result, an unchecked cast causes a compile-time unchecked warning, unless suppressed by the SuppressWarnings
annotation.
Furthermore, during the type erasure process, identifyFunction
and IDENTIFY_FN
compiles into:
private static UnaryOperator IDENTIFY_FN;
public static UnaryOperator identifyFunction() {
return IDENTIFY_FN; // cast is removed
}
and the checkcast
is added to the call site:
System.out.println(sameString.apply(s));
^
INVOKEINTERFACE java/util/function/UnaryOperator.apply (Ljava/lang/Object)Ljava/lang/Object
CHECKCAST java/lang/String
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
checkcast
succeeds, because the identity function returns its argument unmodified.
add a comment |
up vote
3
down vote
The JLS allows such cast:
A cast from a type S to a parameterized type T is unchecked
unless at least one of the following conditions holds:
S <: T
All of the type arguments of T are unbounded wildcards.
[ ... ]
As a result, an unchecked cast causes a compile-time unchecked warning, unless suppressed by the SuppressWarnings
annotation.
Furthermore, during the type erasure process, identifyFunction
and IDENTIFY_FN
compiles into:
private static UnaryOperator IDENTIFY_FN;
public static UnaryOperator identifyFunction() {
return IDENTIFY_FN; // cast is removed
}
and the checkcast
is added to the call site:
System.out.println(sameString.apply(s));
^
INVOKEINTERFACE java/util/function/UnaryOperator.apply (Ljava/lang/Object)Ljava/lang/Object
CHECKCAST java/lang/String
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
checkcast
succeeds, because the identity function returns its argument unmodified.
add a comment |
up vote
3
down vote
up vote
3
down vote
The JLS allows such cast:
A cast from a type S to a parameterized type T is unchecked
unless at least one of the following conditions holds:
S <: T
All of the type arguments of T are unbounded wildcards.
[ ... ]
As a result, an unchecked cast causes a compile-time unchecked warning, unless suppressed by the SuppressWarnings
annotation.
Furthermore, during the type erasure process, identifyFunction
and IDENTIFY_FN
compiles into:
private static UnaryOperator IDENTIFY_FN;
public static UnaryOperator identifyFunction() {
return IDENTIFY_FN; // cast is removed
}
and the checkcast
is added to the call site:
System.out.println(sameString.apply(s));
^
INVOKEINTERFACE java/util/function/UnaryOperator.apply (Ljava/lang/Object)Ljava/lang/Object
CHECKCAST java/lang/String
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
checkcast
succeeds, because the identity function returns its argument unmodified.
The JLS allows such cast:
A cast from a type S to a parameterized type T is unchecked
unless at least one of the following conditions holds:
S <: T
All of the type arguments of T are unbounded wildcards.
[ ... ]
As a result, an unchecked cast causes a compile-time unchecked warning, unless suppressed by the SuppressWarnings
annotation.
Furthermore, during the type erasure process, identifyFunction
and IDENTIFY_FN
compiles into:
private static UnaryOperator IDENTIFY_FN;
public static UnaryOperator identifyFunction() {
return IDENTIFY_FN; // cast is removed
}
and the checkcast
is added to the call site:
System.out.println(sameString.apply(s));
^
INVOKEINTERFACE java/util/function/UnaryOperator.apply (Ljava/lang/Object)Ljava/lang/Object
CHECKCAST java/lang/String
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
checkcast
succeeds, because the identity function returns its argument unmodified.
edited Nov 16 at 23:56
answered Nov 16 at 12:50
Oleksandr
7,77543467
7,77543467
add a comment |
add a comment |
up vote
2
down vote
The cast
return (UnaryOperator<T>) IDENTIFY_FN;
basically amounts to a cast to the raw type UnaryOperator
, because T
is erased at runtime and ignored for the purpose of casts at compile-time. You can cast a generic type to its raw type (for backwards compatibility reasons), but you should get an "unchecked" warning.
This would also work, for example:
UnaryOperator<String> foo = (UnaryOperator) IDENTITY_FN;
add a comment |
up vote
2
down vote
The cast
return (UnaryOperator<T>) IDENTIFY_FN;
basically amounts to a cast to the raw type UnaryOperator
, because T
is erased at runtime and ignored for the purpose of casts at compile-time. You can cast a generic type to its raw type (for backwards compatibility reasons), but you should get an "unchecked" warning.
This would also work, for example:
UnaryOperator<String> foo = (UnaryOperator) IDENTITY_FN;
add a comment |
up vote
2
down vote
up vote
2
down vote
The cast
return (UnaryOperator<T>) IDENTIFY_FN;
basically amounts to a cast to the raw type UnaryOperator
, because T
is erased at runtime and ignored for the purpose of casts at compile-time. You can cast a generic type to its raw type (for backwards compatibility reasons), but you should get an "unchecked" warning.
This would also work, for example:
UnaryOperator<String> foo = (UnaryOperator) IDENTITY_FN;
The cast
return (UnaryOperator<T>) IDENTIFY_FN;
basically amounts to a cast to the raw type UnaryOperator
, because T
is erased at runtime and ignored for the purpose of casts at compile-time. You can cast a generic type to its raw type (for backwards compatibility reasons), but you should get an "unchecked" warning.
This would also work, for example:
UnaryOperator<String> foo = (UnaryOperator) IDENTITY_FN;
edited Nov 16 at 12:20
answered Nov 16 at 12:10
ohlec
1,636717
1,636717
add a comment |
add a comment |
up vote
2
down vote
This cast compiles, because it's a special case of a narrowing conversion. (According to §5.5, narrowing conversions are one of the types of conversions allowed by a cast, so most of this answer is going to focus on the rules for narrowing conversions.)
Note that while UnaryOperator<T>
is not a subtype of UnaryOperator<Object>
(so the cast isn't a "downcast"), it's still considered a narrowing conversion. From §5.6.1:
A narrowing reference conversion treats expressions of a reference type
S
as expressions of a different reference typeT
, whereS
is not a subtype ofT
. [...] Unlike widening reference conversion, the types need not be directly related. However, there are restrictions that prohibit conversion between certain pairs of types when it can be statically proven that no value can be of both types.
Some of these "sideways" casts fail due to special rules, for example the following will fail:
List<String> a = ...;
List<Double> b = (List<String>) a;
Specifically, this is given by a rule in §5.1.6.1 which states that:
If there exists a parameterized type
X
that is a supertype ofT
, and a parameterized typeY
that is a supertype ofS
, such that the erasures ofX
andY
are the same, thenX
andY
are not provably distinct (§4.5).
Using types from the
java.util
package as an example, no narrowing reference conversion exists fromArrayList<String>
toArrayList<Object>
, or vice versa, because the type argumentsString
andObject
are provably distinct. For the same reason, no narrowing reference conversion exists fromArrayList<String>
toList<Object>
, or vice versa. The rejection of provably distinct types is a simple static gate to prevent "stupid" narrowing reference conversions.
In other words, if a
and b
have a common supertype with the same erasure (in this case, for example, List
), then they must be what the JLS is calling "provably distinct", given by §4.5:
Two parameterized types are provably distinct if either of the following is true:
They are parameterizations of distinct generic type declarations.
Any of their type arguments are provably distinct.
And §4.5.1:
Two type arguments are provably distinct if one of the following is true:
Neither argument is a type variable or wildcard, and the two arguments are not the same type.
One type argument is a type variable or wildcard, with an upper bound (from capture conversion, if necessary) of
S
; and the other type argumentT
is not a type variable or wildcard; and neither|S| <: |T|
nor|T| <: |S|
.
Each type argument is a type variable or wildcard, with upper bounds (from capture conversion, if necessary) of
S
andT
; and neither|S| <: |T|
nor|T| <: |S|
.
So, given the above rules, List<String>
and List<Double>
are provably distinct (via the 1st rule from 4.5.1), because String
and Double
are different type arguments.
However, UnaryOperator<T>
and UnaryOperator<Object>
are not provably distinct (via the 2nd rule from 4.5.1), because:
One type argument is a type variable (
T
, with a bound ofObject
.)The bound of that type variable is the same as the type argument to the other type (
Object
).
Since UnaryOperator<T>
and UnaryOperator<Object>
are not provably distinct, the narrowing conversion is allowed, hence the cast compiles.
One way to think about why the compiler permits some of these casts but not others is: in the case of the type variable, it can't prove that T
definitely isn't Object
. For example, we could have a situation like this:
UnaryOperator<String> aStringThing = Somewhere::doStringThing;
UnaryOperator<Double> aDoubleThing = Somewhere::doDoubleThing;
<T> UnaryOperator<T> getThing(Class<T> t) {
if (t == String.class)
return (UnaryOperator<T>) aStringThing;
if (t == Double.class)
return (UnaryOperator<T>) aDoubleThing;
return null;
}
In those cases, we actually know the cast is correct as long as nobody else is doing something funny (like unchecked casting the Class<T>
argument).
So in the general case of casting to UnaryOperator<T>
, we might actually be doing something legitimate. In comparison, with the case of casting List<String>
to List<Double>
, we can say pretty authoritatively that it's always wrong.
add a comment |
up vote
2
down vote
This cast compiles, because it's a special case of a narrowing conversion. (According to §5.5, narrowing conversions are one of the types of conversions allowed by a cast, so most of this answer is going to focus on the rules for narrowing conversions.)
Note that while UnaryOperator<T>
is not a subtype of UnaryOperator<Object>
(so the cast isn't a "downcast"), it's still considered a narrowing conversion. From §5.6.1:
A narrowing reference conversion treats expressions of a reference type
S
as expressions of a different reference typeT
, whereS
is not a subtype ofT
. [...] Unlike widening reference conversion, the types need not be directly related. However, there are restrictions that prohibit conversion between certain pairs of types when it can be statically proven that no value can be of both types.
Some of these "sideways" casts fail due to special rules, for example the following will fail:
List<String> a = ...;
List<Double> b = (List<String>) a;
Specifically, this is given by a rule in §5.1.6.1 which states that:
If there exists a parameterized type
X
that is a supertype ofT
, and a parameterized typeY
that is a supertype ofS
, such that the erasures ofX
andY
are the same, thenX
andY
are not provably distinct (§4.5).
Using types from the
java.util
package as an example, no narrowing reference conversion exists fromArrayList<String>
toArrayList<Object>
, or vice versa, because the type argumentsString
andObject
are provably distinct. For the same reason, no narrowing reference conversion exists fromArrayList<String>
toList<Object>
, or vice versa. The rejection of provably distinct types is a simple static gate to prevent "stupid" narrowing reference conversions.
In other words, if a
and b
have a common supertype with the same erasure (in this case, for example, List
), then they must be what the JLS is calling "provably distinct", given by §4.5:
Two parameterized types are provably distinct if either of the following is true:
They are parameterizations of distinct generic type declarations.
Any of their type arguments are provably distinct.
And §4.5.1:
Two type arguments are provably distinct if one of the following is true:
Neither argument is a type variable or wildcard, and the two arguments are not the same type.
One type argument is a type variable or wildcard, with an upper bound (from capture conversion, if necessary) of
S
; and the other type argumentT
is not a type variable or wildcard; and neither|S| <: |T|
nor|T| <: |S|
.
Each type argument is a type variable or wildcard, with upper bounds (from capture conversion, if necessary) of
S
andT
; and neither|S| <: |T|
nor|T| <: |S|
.
So, given the above rules, List<String>
and List<Double>
are provably distinct (via the 1st rule from 4.5.1), because String
and Double
are different type arguments.
However, UnaryOperator<T>
and UnaryOperator<Object>
are not provably distinct (via the 2nd rule from 4.5.1), because:
One type argument is a type variable (
T
, with a bound ofObject
.)The bound of that type variable is the same as the type argument to the other type (
Object
).
Since UnaryOperator<T>
and UnaryOperator<Object>
are not provably distinct, the narrowing conversion is allowed, hence the cast compiles.
One way to think about why the compiler permits some of these casts but not others is: in the case of the type variable, it can't prove that T
definitely isn't Object
. For example, we could have a situation like this:
UnaryOperator<String> aStringThing = Somewhere::doStringThing;
UnaryOperator<Double> aDoubleThing = Somewhere::doDoubleThing;
<T> UnaryOperator<T> getThing(Class<T> t) {
if (t == String.class)
return (UnaryOperator<T>) aStringThing;
if (t == Double.class)
return (UnaryOperator<T>) aDoubleThing;
return null;
}
In those cases, we actually know the cast is correct as long as nobody else is doing something funny (like unchecked casting the Class<T>
argument).
So in the general case of casting to UnaryOperator<T>
, we might actually be doing something legitimate. In comparison, with the case of casting List<String>
to List<Double>
, we can say pretty authoritatively that it's always wrong.
add a comment |
up vote
2
down vote
up vote
2
down vote
This cast compiles, because it's a special case of a narrowing conversion. (According to §5.5, narrowing conversions are one of the types of conversions allowed by a cast, so most of this answer is going to focus on the rules for narrowing conversions.)
Note that while UnaryOperator<T>
is not a subtype of UnaryOperator<Object>
(so the cast isn't a "downcast"), it's still considered a narrowing conversion. From §5.6.1:
A narrowing reference conversion treats expressions of a reference type
S
as expressions of a different reference typeT
, whereS
is not a subtype ofT
. [...] Unlike widening reference conversion, the types need not be directly related. However, there are restrictions that prohibit conversion between certain pairs of types when it can be statically proven that no value can be of both types.
Some of these "sideways" casts fail due to special rules, for example the following will fail:
List<String> a = ...;
List<Double> b = (List<String>) a;
Specifically, this is given by a rule in §5.1.6.1 which states that:
If there exists a parameterized type
X
that is a supertype ofT
, and a parameterized typeY
that is a supertype ofS
, such that the erasures ofX
andY
are the same, thenX
andY
are not provably distinct (§4.5).
Using types from the
java.util
package as an example, no narrowing reference conversion exists fromArrayList<String>
toArrayList<Object>
, or vice versa, because the type argumentsString
andObject
are provably distinct. For the same reason, no narrowing reference conversion exists fromArrayList<String>
toList<Object>
, or vice versa. The rejection of provably distinct types is a simple static gate to prevent "stupid" narrowing reference conversions.
In other words, if a
and b
have a common supertype with the same erasure (in this case, for example, List
), then they must be what the JLS is calling "provably distinct", given by §4.5:
Two parameterized types are provably distinct if either of the following is true:
They are parameterizations of distinct generic type declarations.
Any of their type arguments are provably distinct.
And §4.5.1:
Two type arguments are provably distinct if one of the following is true:
Neither argument is a type variable or wildcard, and the two arguments are not the same type.
One type argument is a type variable or wildcard, with an upper bound (from capture conversion, if necessary) of
S
; and the other type argumentT
is not a type variable or wildcard; and neither|S| <: |T|
nor|T| <: |S|
.
Each type argument is a type variable or wildcard, with upper bounds (from capture conversion, if necessary) of
S
andT
; and neither|S| <: |T|
nor|T| <: |S|
.
So, given the above rules, List<String>
and List<Double>
are provably distinct (via the 1st rule from 4.5.1), because String
and Double
are different type arguments.
However, UnaryOperator<T>
and UnaryOperator<Object>
are not provably distinct (via the 2nd rule from 4.5.1), because:
One type argument is a type variable (
T
, with a bound ofObject
.)The bound of that type variable is the same as the type argument to the other type (
Object
).
Since UnaryOperator<T>
and UnaryOperator<Object>
are not provably distinct, the narrowing conversion is allowed, hence the cast compiles.
One way to think about why the compiler permits some of these casts but not others is: in the case of the type variable, it can't prove that T
definitely isn't Object
. For example, we could have a situation like this:
UnaryOperator<String> aStringThing = Somewhere::doStringThing;
UnaryOperator<Double> aDoubleThing = Somewhere::doDoubleThing;
<T> UnaryOperator<T> getThing(Class<T> t) {
if (t == String.class)
return (UnaryOperator<T>) aStringThing;
if (t == Double.class)
return (UnaryOperator<T>) aDoubleThing;
return null;
}
In those cases, we actually know the cast is correct as long as nobody else is doing something funny (like unchecked casting the Class<T>
argument).
So in the general case of casting to UnaryOperator<T>
, we might actually be doing something legitimate. In comparison, with the case of casting List<String>
to List<Double>
, we can say pretty authoritatively that it's always wrong.
This cast compiles, because it's a special case of a narrowing conversion. (According to §5.5, narrowing conversions are one of the types of conversions allowed by a cast, so most of this answer is going to focus on the rules for narrowing conversions.)
Note that while UnaryOperator<T>
is not a subtype of UnaryOperator<Object>
(so the cast isn't a "downcast"), it's still considered a narrowing conversion. From §5.6.1:
A narrowing reference conversion treats expressions of a reference type
S
as expressions of a different reference typeT
, whereS
is not a subtype ofT
. [...] Unlike widening reference conversion, the types need not be directly related. However, there are restrictions that prohibit conversion between certain pairs of types when it can be statically proven that no value can be of both types.
Some of these "sideways" casts fail due to special rules, for example the following will fail:
List<String> a = ...;
List<Double> b = (List<String>) a;
Specifically, this is given by a rule in §5.1.6.1 which states that:
If there exists a parameterized type
X
that is a supertype ofT
, and a parameterized typeY
that is a supertype ofS
, such that the erasures ofX
andY
are the same, thenX
andY
are not provably distinct (§4.5).
Using types from the
java.util
package as an example, no narrowing reference conversion exists fromArrayList<String>
toArrayList<Object>
, or vice versa, because the type argumentsString
andObject
are provably distinct. For the same reason, no narrowing reference conversion exists fromArrayList<String>
toList<Object>
, or vice versa. The rejection of provably distinct types is a simple static gate to prevent "stupid" narrowing reference conversions.
In other words, if a
and b
have a common supertype with the same erasure (in this case, for example, List
), then they must be what the JLS is calling "provably distinct", given by §4.5:
Two parameterized types are provably distinct if either of the following is true:
They are parameterizations of distinct generic type declarations.
Any of their type arguments are provably distinct.
And §4.5.1:
Two type arguments are provably distinct if one of the following is true:
Neither argument is a type variable or wildcard, and the two arguments are not the same type.
One type argument is a type variable or wildcard, with an upper bound (from capture conversion, if necessary) of
S
; and the other type argumentT
is not a type variable or wildcard; and neither|S| <: |T|
nor|T| <: |S|
.
Each type argument is a type variable or wildcard, with upper bounds (from capture conversion, if necessary) of
S
andT
; and neither|S| <: |T|
nor|T| <: |S|
.
So, given the above rules, List<String>
and List<Double>
are provably distinct (via the 1st rule from 4.5.1), because String
and Double
are different type arguments.
However, UnaryOperator<T>
and UnaryOperator<Object>
are not provably distinct (via the 2nd rule from 4.5.1), because:
One type argument is a type variable (
T
, with a bound ofObject
.)The bound of that type variable is the same as the type argument to the other type (
Object
).
Since UnaryOperator<T>
and UnaryOperator<Object>
are not provably distinct, the narrowing conversion is allowed, hence the cast compiles.
One way to think about why the compiler permits some of these casts but not others is: in the case of the type variable, it can't prove that T
definitely isn't Object
. For example, we could have a situation like this:
UnaryOperator<String> aStringThing = Somewhere::doStringThing;
UnaryOperator<Double> aDoubleThing = Somewhere::doDoubleThing;
<T> UnaryOperator<T> getThing(Class<T> t) {
if (t == String.class)
return (UnaryOperator<T>) aStringThing;
if (t == Double.class)
return (UnaryOperator<T>) aDoubleThing;
return null;
}
In those cases, we actually know the cast is correct as long as nobody else is doing something funny (like unchecked casting the Class<T>
argument).
So in the general case of casting to UnaryOperator<T>
, we might actually be doing something legitimate. In comparison, with the case of casting List<String>
to List<Double>
, we can say pretty authoritatively that it's always wrong.
edited Nov 16 at 17:17
answered Nov 16 at 16:38
Radiodef
31.3k126594
31.3k126594
add a comment |
add a comment |
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53337494%2fhow-does-casting-this-object-to-a-generic-type-work%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown