Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 975
Matthew S. Avatar asked Sep 27 '19 01:09

Matthew S.


People also ask

Why do we need bounded type parameters in Java?

There may be times when you want to restrict the types that can be used as type arguments in a parameterized type. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for.

What does upper bound mean in Java?

Upper-bound is when you specify (? extends Field) means argument can be any Field or subclass of Field. Lower-bound is when you specify (? super Field) means argument can be any Field or superclass of Field.

What is the difference between upper bounded wildcards and bounded type parameters?

What is the difference between a wildcard bound and a type parameter bound? A wildcard can have only one bound, while a type parameter can have several bounds. A wildcard can have a lower or an upper bound, while there is no such thing as a lower bound for a type parameter.

What is the purpose of bounded type in generic programming?

Whenever you want to restrict the type parameter to subtypes of a particular class you can use the bounded type parameter. If you just specify a type (class) as bounded parameter, only sub types of that particular class are accepted by the current generic class. These are known as bounded-types in generics in Java.


2 Answers

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.

like image 142
ggf31416 Avatar answered Sep 25 '22 22:09

ggf31416


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

I checked:

  • §8.4.4 - Generic Methods

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

  • §8.1.2 - Generic Classes and Type Parameters
  • §8.8.4 - Generic Constructors
  • §9.1.2 - Generic Interfaces and Type Parameters

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());
like image 24
Vince Avatar answered Sep 25 '22 22:09

Vince