Why does java require a cast for the instantiation of a bounded type parameter to its upper bound class?
Asked Answered
M

2

8

Java requires the instantiation of a bounded type parameter to its upper bound class to have a cast, for example:

<T extends Integer> void passVal (T t) {
    Integer number = 5;
    t = (T) number; // Without cast a compile error is issued
}

T is already restricted to Integer or one of its sub classes (I know, there aren't any) upon declaration, and it seems to me that any bounded type parameter may only be instantiated to its upper bound class or one of its sub classes, so why is the cast needed?

Also, I know that this isn't the case if I instantiate the parameter through invocation outside of this method, so don't offer that as a answer; the question is specific to bounded type parameters being instantiated within their declaring class/methods.

Minor answered 27/9, 2019 at 1:15 Comment(0)
K
9

T doesn't mean Integer, it must be valid for Integer or any class that extends from it. Let's say that StrangeInteger extends from Integer and replace T with StrangeInteger:

void passVal (StrangeInteger t) {
    Integer number = 5;
    t = (StrangeInteger) number;
}

It attempts to assign a Integer variable to a StrangeInteger variable, you can't do that unless number was a StrangeInteger or a derived class in the first place. In fact your code should (conceptually) throw an Exception at runtime unless t is an Integer, but it won't actually do that in this case due to type erasure (see Edit 2).

The situation is similar to:

Object obj = "Hello"
String t = (String)obj; // this will not fail, but requires a cast

Object obj2 = getDBConnection();
String t2 = (String)obj2; // this will fail at runtime

Edit: Integer is indeed final, so T can only be Integer, but it's possible that the compiler is not checking if the upper bound is final, after all it makes little sense for an upper bound to be final, so allowing that special case adds complexity for very little real gain.

Edit 2: TL; DR: You are confusing upper and lower bounds, but there are caveats with type erasure. This will break as soon as you do anything that is worth doing using generics rather than just using the base type.

English is not my first language so I may be not totally clear.

I think you are struggling with the difference between using a generic type with an upper bound and just using the upper bound as type. The idea of generics (from C++ and other languages) is that the code must be valid if you replace T by any type T allowed by the bounds, so you can't call any method that is not defined in the upper bound.

A being an upper bound on T also means that you can always assign a T object to an A variable. You can't assign safely an A object to a T variable (unless A == T), you could only do that if A was a lower bound on T, not an upper bound. Also see Understanding upper and lower bounds on ? in Java Generics.

Java uses type erasure to implement generics, there are a few advantages, but that causes some limitations that are not always apparent. Because of type erasure in this case the cast itself won't fail, after type checking T is replaced by the upper bound in the type erasure step, i.e. (T)number is replaced by (Integer)number. An exception will still occur if you do anything that causes a cast to the subclass, for example if you return the altered t and assign the result to a variable of the subclass, because the compiler adds an implicit cast.

This will also fail if you call a method that depends on an subclass of T, which is a common pattern, for example:

List<Person> persons = ...
Comparator<Person> nameComparator = (p1,p2) -> p1.getName().compareTo(p2.getName())
java.util.Collections.sort(persons,nameComparator);

The following code sample display the behavior in several cases. I used System.err on everything to avoid order issues in the output.

import java.util.function.Consumer;
import java.util.function.Function;

class A {
    @Override public String toString(){ return "A";}
    public String foo(){ return "foo";}
}

class B extends A {
    @Override public String toString(){ return "B";}
    public String bar(){ return "bar";}
}

class C extends B { }

public class Main {

    public static void main(String[] args) {
        Function<A,String> funA = a -> a.foo();
        Function<B,String> funB = b -> b.bar();
        Function<C,String> funC = c -> c.bar();
        Consumer<B> ignoreArgument = b -> {
            System.err.println("  Consumer called");
        };

        B b = new B();
        System.err.println("* voidTest *");
        voidTest(b);
        System.err.println("------------");
        System.err.println("* returnTest *"); 
        returnTest(b);
        System.err.println("returnTest without using result did not throw");
        System.err.println("------------");
        try {
            System.err.println("Returned " + returnTest(b).toString());
            System.err.println("returnTest: invoking method on result did not throw");
        }
        catch(Exception ex) {
            System.err.println("returnTest: invoking method on result threw");
            ex.printStackTrace();
        }
        System.err.println("------------");
        B b2 = null;
        try {
            b2 = returnTest(b);
            System.err.println("returnTest: assigning result to a B variable did not throw");
        }
        catch(Exception ex) {
            System.err.println("returnTest: assigning result to a B variable threw");
            ex.printStackTrace();
        }
        System.err.println("------------");
        System.err.println("* functionTest funA *");
        functionTest(b, funA);
        System.err.println("------------");
        System.err.println("* functionTest funB * ");
        functionTest(b, funB);
        System.err.println("------------");
        System.err.println("* consumerTest *");
        consumerTest(b, ignoreArgument);
        // The following won't work because C is not B or a superclass of B
        // Compiler error functionTest(T, Function<? super T,String>) is not applicable for the arguments (B, Function<C,String>)
        // functionTest(b, funC); 
    }

    private static <T extends A> void voidTest(T t){
        System.err.println("  Before: " + t.toString());
        t = (T)new A(); // warning Type safety: Unchecked cast from A to T
        System.err.println("  After: " + t.toString());
    }

    private static <T extends A> T returnTest(T t){
        System.err.println("  Before: " + t.toString());
        t = (T)new A();
        System.err.println("  After: " + t.toString());
        return t;
    }

    private static <T extends A> void functionTest(T t, Function<? super T,String> fun) {
        System.err.println("  fun Before: " + fun.apply(t));
        t = (T)new A();
        try {
            System.err.println("  fun After: " + fun.apply(t));
        }
        catch(Exception ex) {
            System.err.println("  fun After: threw");
            ex.printStackTrace();
        }
    }

    private static <T extends A> void consumerTest(T t, Consumer<? super T> c) {
        System.err.print("  Before: ");
        c.accept(t);
        t = (T)new A();
        try {
            System.err.println("  After: ");
            c.accept(t);
            System.err.println("    c.accept(t) After: worked");
        }
        catch(Exception ex) {
            System.err.println("    c.accept(t) After: threw");
            ex.printStackTrace();
        }
    }
}

The output under OpenJDK 11 is:

* voidTest *
  Before: B
  After: A
------------
* returnTest *
  Before: B
  After: A
returnTest without using result did not throw
------------
  Before: B
  After: A
returnTest: invoking method on result threw
java.lang.ClassCastException: class A cannot be cast to class B (A and B are in unnamed module of loader 'app')
    at Main.main(Main.java:35)
------------
  Before: B
  After: A
returnTest: assigning result to a B variable threw
java.lang.ClassCastException: class A cannot be cast to class B (A and B are in unnamed module of loader 'app')
    at Main.main(Main.java:45)
------------
* functionTest funA *
  fun Before: foo
  fun After: foo
------------
* functionTest funB * 
  fun Before: bar
  fun After: threw
java.lang.ClassCastException: class A cannot be cast to class B (A and B are in unnamed module of loader 'app')
    at Main.functionTest(Main.java:83)
    at Main.main(Main.java:57)
------------
* consumerTest *
  Before:   Consumer called
  After: 
    c.accept(t) After: threw
java.lang.ClassCastException: class A cannot be cast to class B (A and B are in unnamed module of loader 'app')
    at Main.consumerTest(Main.java:97)
    at Main.main(Main.java:60)

I'm not entirely sure resultTest why didn't caused an exception if the result is completely ignored, maybe a cast is not required by the language in that case, or the compiler removed it. Calling a method defined in the upper bound on the result still caused an exception. Finally an observation from consumerTest is that it didn't need to call bar() to cause a ClassCastException, it just needed to pass t to the consumer that expects a B argument.

Kirstinkirstyn answered 27/9, 2019 at 1:45 Comment(5)
I thought this answered my question, but after experimentation I'm still a little confused. It seems like what you're are saying is that the cast is required to avoid cast errors at runtime. If we were to create two classes A and B, B extends A. A has a method and B declares it's own as well, and then we create a method with a type parameter of upper bound A, and use the reference of that parameter to access methods of the type parameters class will are only allowed to access methods defined in A, not B. So you are allowed to pass sub classes but it will be treated as the upper bound classMinor
It seems like if you pass that parameter an instance of B when calling the method, behind the scenes java will instantly convert by saying: A a = new B(); because the type parameter's upper bound is A. If that's the case it's hard to imagine a scenario where a cast error can occur, making the cast redundant. What am I getting wrong?Minor
If you return T and pass a B argument the return type will be B, not A. Also you can pass an instance of a generic type that is specific to the type argument, an example is a custom Comparator for Collections.sort(list,comparator).Kirstinkirstyn
The funny thing is that your code as it is will actually work. Because of type erasure, a quirk of Java generics, the cast itself won't fail. It will throw a ClassCastException later if you return t and use the result or call anything that needs T instead of A. I will edit my answer later with more details.Kirstinkirstyn
Thank you for going out of your way to provide such an elaborate answer! So here's what I think I've been missing: 1. You should not assign a instance of a upper bound to the bounded type within the declaring class/method, because it is risky and may cause run time errors when the method/class is invoked/instantiated in a meaningful way. 2. I now realize that the cast is required because java does not know at compile time if you are down casting.Minor
T
2

The language specification does not currently include explicit checks for finalized types in any sense.

I checked:

And, even though they don't apply to your case, I took the liberty of checking:

Even though Integer is final, if it wasn't, your code could break.


What you have, assuming the compiler doesn't check for finalized types, would be similar to:

class Super
class Sub extends Super

<T extends Super> void passVal (T t) {
    Super super = new Super();
    return (T) super;
}

Which would break if we called:

passVal(new Sub());
Trinidadtrinitarian answered 27/9, 2019 at 1:57 Comment(1)
Are you sure it would break? I'm not getting an error, it seems as though your return is literally saying return (Super) super; I just tried it with two classes A and B, B extends A; created a method with bounded parameter of A, then passed it an instance of B, and got no run time error. The method created an object of A (super) and then returned (T) super.Minor

© 2022 - 2024 — McMap. All rights reserved.