I've stumbled upon a piece of code that has me wondering why it compiles successfully:
public class Main { public static void main(String[] args) { String s = newList(); // why does this line compile? System.out.println(s); } private static <T extends List<Integer>> T newList() { return (T) new ArrayList<Integer>(); } }
What is interesting is that if I modify the signature of method newList
with <T extends ArrayList<Integer>>
it doesn't work anymore.
Update after comments & responses: If I move the generic type from the method to the class the code doesn't compile anymore:
public class SomeClass<T extends List<Integer>> { public void main(String[] args) { String s = newList(); // this doesn't compile anymore System.out.println(s); } private T newList() { return (T) new ArrayList<Integer>(); } }
Code that uses generics has many benefits over non-generic code: Stronger type checks at compile time. A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find.
Generics enable the use of stronger type-checking, the elimination of casts, and the ability to develop generic algorithms. Without generics, many of the features that we use in Java today would not be possible.
To overcome the above problems of collections(type-safety, type casting) generics introduced in java 1.5v . Main objectives of generics are: 1) To provide type safety to the collections. 2) To resolve type casting problems. To hold only string type of objects we can create a generic version of ArrayList as follows.
Definition: “A generic type is a generic class or interface that is parameterized over types.” Essentially, generic types allow you to write a general, generic class (or method) that works with different types, allowing for code re-use.
If you declare a type parameter at a method, you are allowing the caller to pick an actual type for it, as long as that actual type will fulfill the constraints. That type doesn’t have to be an actual concrete type, it might be an abstract type, a type variable or an intersection type, in other, more colloquial words, a hypothetical type. So, as said by Mureinik, there could be a type extending String
and implementing List
. We can’t manually specify an intersection type for the invocation, but we can use a type variable to demonstrate the logic:
public class Main { public static <X extends String&List<Integer>> void main(String[] args) { String s = Main.<X>newList(); System.out.println(s); } private static <T extends List<Integer>> T newList() { return (T) new ArrayList<Integer>(); } }
Of course, newList()
can’t fulfill the expectation of returning such a type, but that’s the problem of the definition (or implementation) of this method. You should get an “unchecked” warning when casting ArrayList
to T
. The only possible correct implementation would be returning null
here, which renders the method quite useless.
The point, to repeat the initial statement, is that the caller of a generic method chooses the actual types for the type parameters. In contrast, when you declare a generic class like with
public class SomeClass<T extends List<Integer>> { public void main(String[] args) { String s = newList(); // this doesn't compile anymore System.out.println(s); } private T newList() { return (T) new ArrayList<Integer>(); } }
the type parameter is part of the contract of the class, so whoever creates an instance will pick the actual types for that instance. The instance method main
is part of that class and has to obey that contract. You can’t pick the T
you want; the actual type for T
has been set and in Java, you usually can’t even find out what T
is.
The key point of generic programming is to write code that works independently of what actual types have been chosen for the type parameters.
But note that you can create another, independent instance with whatever type you like and invoke the method, e.g.
public class SomeClass<T extends List<Integer>> { public <X extends String&List<Integer>> void main(String[] args) { String s = new SomeClass<X>().newList(); System.out.println(s); } private T newList() { return (T) new ArrayList<Integer>(); } }
Here, the creator of the new instance picks the actual types for that instance. As said, that actual type doesn’t need to be a concrete type.
I'm guessing this is because List
is an interface. If we ignore the fact that String
is final
for a second, you could, in theory, have a class that extends String
(meaning you could assign it to s
) but implements List<Integer>
(meaning it could be returned from newList()
). Once you change the return type from an interface (T extends List
) to a concrete class (T extends ArrayList
) the compiler can deduce they aren't assignable from each other, and produces an error.
This, of course, breaks down since String
is, in fact, final
, and we could expect the compiler to take this into account. IMHO, it's a bug, although I must admit I'm no compiler-expert and there might be a good reason to ignore the final
modifier at this point.
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