简体   繁体   中英

Why does Java have lower bounds in generics?

I'm trying to design my own programming language, and am thinking about generics. I've been doing Java for quite a while now and know about the extends and super generic bounds.

I'm reading this post and trying to understand the need for the lower bounds.

In my language, I am planning to do generics the same way as a regular field, if you say List<MyObject> , you can store either a MyObject , or any subtype of MyObject . Makes sense right?

On the post, they have the following class hierarchy:

class Person implements Comparable<Person> {
  ...
}

class Student extends Person {
  ...
}

They then have a sort method:

public static <T extends Comparable<T>> void sort(List<T> list) {
  ...
}

What I think, is that you should be able to send a List<Student> to this method. As a Student extends Person , the compare method would be handled by it's superclass, Person .

The reason for the error message is that the compiler infers the type parameter of the sort method as T:=Student and that class Student is not Comparable<Student> . It is Comparable<Person> , but that does not meet the requirements imposed by the bound of the type parameter of method sort. It is required that T (ie Student ) is Comparable<T> (ie Comparable<Student> ), which in fact it is not.

The above doesn't make any sense to me...you should be able to do student.compare(person) , so why doesn't this work?

Maybe it's saying that Student should implement it's own comparable method so that Student has a say in the comparison? You don't need to do anything special, just override Person 's method. You won't be able to guarantee you are comparing to another Student , but that can be checked with instanceof .

Is there something I'm missing here?

And after all this thinking, I'm now wondering what the purpose of extends is. From my understanding, in a List<MyType> , you can only put a MyType in, not any of it's subclasses. As mentioned above, this doesn't make any sense to me and you should be able to put any subclass in the list like a field.

I should probably make this clear, it's not "why doesn't it work in Java", but "why doesn't it work in generics theory". I just tagged java because that is where I'm making my comparisons.

First: The method declaration

public static <T extends Comparable<T>> void sort(List<T> list)

does not make much sense for me. I thing it should be

public static <T extends Comparable<? super T>> void sort(List<T> list)

Then it would be possible to write sort(listOfStudents) . Now I will explain the advantage of upper and lower bounded wildcards :


Polymorphism of type parameters is not transferred to it's generic type

This mean a list of students ( List<Student> ) is not a list of persons ( List<Person> ). A instruction like

List<Person> list = new List<Student>();

would fail in Java. There is a simple reason: list.add(new Person()); would be illegal for a list of students but not for a list of persons.

Upper Bounded Wildcards

But maybe you have a function which doesn't care whether the objects are subclasses or not. For example: You could have a method like this:

void printAll(List<Person> list)

They just print some data about all persons to stdout. If you have a list of students ( List<Student> listOfStudents ) you could write:

List<Person> listOfPersons = new ArrayList<>();
for (final Student student : listOfStudents) {
    listOfPersons.add(student);
}
printAll(listOfPersons);

But you may see that it isn't a very nice solution. Another solution would be to use upper bounded wildcards for printAll :

void printAll(List<? extends Person> list)

You can write something like Person person = list.get(0) in printAll . But you cannot write print.add(new Person()) because list could be a list of students or something else.

Lower Bounded Wildcards

Now the same in the other direction: Lets say you have a function which generates some students and put them in a list. Something like this:

void generateStudents(List<Student> list) {
    for (int i = 0; i < 10; ++i) {
        list.add(new Student());
    }
}

Now you have a list of persons ( List<Person> listOfPersons ) and want to generate students in this list. You could write

List<Student> listOfStudents = new ArrayList<>();
generateStudents(listOfStudents);
for (Student student : listOfStudents) {
    listOfPersons.add(student);
}

You may see again, that it is not a very nice solution. You could also change the declaration of generateStudents to

void generateStudents(List<? super Student> list)

Now, you can just write generateStudents(listOfPersons); .

I think your confusion may be coming from the fact that while elements of List<Student> can be compared to each other by virtue of the fact that class Student subclasses Person which implements Comparable<Person> (class Student therefore inherits compareTo(Person o) , which can be called with an instance of Student ), you still cannot call the sort method with a List<Student> ...

The problem is that when the Java compiler encounters the statement:

sort(studentList); 

Where studentList is an instance of parameterized type List<Student> , it uses type inference to infer that the type argument to the sort method T is Student , and Student does not satisfy the upper bound: Student extends Comparable<Student> . Therefore, the compiler will throw an error in this case, telling you that the inferred type does not conform to the constraints.

The article that you linked to shows you that the solution to this is to re-write the sort method as:

public static <T extends Comparable <? super T > > void sort(List<T> list)

This method signature loosens the constraint on the type parameter so that you can call the method with a List<Student> .

I'm not too clear on the last part of your post:

in a List<MyType> , you can only put a MyType in, not any of it's subclasses.

If you're referring to the elements of List<MyType> , yes, you can put any element that is a subtype of MyType in this list, say, MySubType . If you're referring to a variable with List<MyType> as its reference type, then no, you cannot put a reference to List<MySubType> in a variable of type List<MyType> . The reason for this is easy to see when you consider the following:

List<MyType> a;
List<MySubType> b = new ArrayList<>();
a = b; // compile-time-error, but assume OK for now
a.add(new MyType()); // Based on the type of a, this should be OK, but it's not because a is actually a reference to List<MySubType>.

I also think you should refer to Java Generics by Wadler and Naftalin, an excellent introduction to Java (5+) Type System.

When you ask "what is the purpose of extends keyword?" based on your observations about collections of objects, the first thing you should remember is generic collections are tricky. I quote from the Wadler/Naftalin book (emphasis mine):

In Java, one type is a subtype of another if they are related by an extends or implements clause: Integer is a subtype of Number. Subtyping is transitive.

If A is a subtype of B, B is the supertype of A.

Liskov's Substitution Principle tells us that wherever a value of one type is expected, one may provide a value of any subtype of that type: a variable of a given type may be assigned a value of any subtype of that type, and a method with a parameter of a given type may be invoked with an argument of any subtype of that type.

It's because of violation of Liskov's Substitution Principle (that would arise very rapidly in practice) that a List<Integer> is not a subtype of List<Number> although Integer is a subtype of Number . The other way round also does not work because List<Number> is NOT a subtype of List<Integer>.

This paragraph should help us understand why the keyword extends is essential to support inheritance and polymorphism, and yet it (sort of) comes in the way of generic collections.

I think your question would be stated better as: "What are valid use cases for using lower bound generics in JAVA"? I had the same question and it makes sense to use the upper bounds when you have a list and you want to use different types that are all subtypes or the supertype of a hierarchy of classes.

For example, you want methods to populate a list of numbers with int, double, and long. Using extends you can do this as they are all subtypes of Number.

On the other hand, if you want to restrict methods to only use Integer, then you want the more narrowly defined class so that only int is allowed, not float, double, etc.

From the java docs :

The abstract class Number is the superclass of platform classes representing numeric values that are convertible to the primitive types byte, double, float, int, long, and short.

The Integer class wraps a value of the primitive type int in an object. An object of type Integer contains a single field whose type is int.

A better example might be work you are doing at the JVM level, so you want to restrict your methods to use the VirtualMachineError to get specific information written to the logs, rather than using the Error class, from which it inherits, to limit the number of errors you write to the logs for something very refined, possibly relating to some debugging task.

These are obviously contrived examples, but the theme is the lower bounds can be used to restrict method type parameters.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM