Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does casting this object to a generic type work?

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?

like image 584
Hamza Belmellouki Avatar asked Nov 16 '18 12:11

Hamza Belmellouki


People also ask

Why there's need to cast generic sobjects to specific sobject types?

Why there's need to Cast Generic sObjects to Specific sObject Types? sObject refers to any object that can be stored in the Force.com platform database.The sObject data type can be used in code that processes different types of sObjects. But You can't assign Generic type Object to Particular (Account) Object For e.g

What is an object type casting in Java?

Casting means picking an Object of one specific type and making it into another Object type. This process is called casting a variable. Actually this case is not particular to Java, as many other programming languages helps casting of their variable types. But, in Java it is not possible to cast any variable to any random type.

What happens when you cast a reference to an object?

A cast operation between reference types does not change the run-time type of the underlying object; it only changes the type of the value that is being used as a reference to that object. For more information, see Polymorphism. In some reference type conversions, the compiler cannot determine whether a cast will be valid.

What does it mean to cast a variable?

Well, all casting really means is taking an Object of one particular type and “turning it into” another Object type. This process is called casting a variable. This topic is not specific to Java, as many other programming languages support casting of their variable types.


2 Answers

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 type T, where S is not a subtype of T. [...] 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 of T, and a parameterized type Y that is a supertype of S, such that the erasures of X and Y are the same, then X and Y are not provably distinct (§4.5).

    Using types from the java.util package as an example, no narrowing reference conversion exists from ArrayList<String> to ArrayList<Object>, or vice versa, because the type arguments String and Object are provably distinct. For the same reason, no narrowing reference conversion exists from ArrayList<String> to List<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 argument T 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 and T; 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:

  1. One type argument is a type variable (T, with an upper bound of Object.)

  2. 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.

like image 70
Radiodef Avatar answered Sep 30 '22 18:09

Radiodef


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.

like image 43
Oleksandr Pyrohov Avatar answered Sep 30 '22 17:09

Oleksandr Pyrohov