Is there a benefit for a List<? extends MyObject>?
Asked Answered
P

4

5

Given a simple class …

public class Parent {

    private final String value;

    public Parent(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return value;
    }

}

… and a subclass of it, …

public class Child extends Parent {
    public Child(String value) {
        super(value);
    }
}

… the following program compiles and executes without errors or warnings:

public class Main {

    public static void main(String[] args) {
        List<? extends Parent> items = getItems();
        for (Parent p : items) {
            System.out.println(p.toString());
        }
    }

    private static List<? extends Parent> getItems() {
        List<Parent> il = new ArrayList<Parent>();
        il.add(new Parent("Hello"));
        il.add(new Child("World"));
        return il;
    }
}

But what about the return type of getItems()? It seems semantically wrong to me, since the result of the function does contain a Parent object which is not a subclass of Parent what I would expect from that method signature. Given this behaviour, I would have expected a simple List<Parent> as return type. However, since the compiler doesn’t complain, do I miss something? Is there any benefit from this implementation, aside from an (unnecessary) “write protection” for the result list? Or is this java style to write-protect result lists (not in this example, but, for example, to expose an object’s field list without needing to create a copy)? Or can this simply be considered “wrong” (in terms of teaching)? If so, why does neither the compiler nor even Eclipse complain about it, not even showing a hint?

Predicant answered 2/9, 2014 at 6:41 Comment(3)
I think you've listed the benefit - write protection. Whether it is required in your particular code example is another debate, but certainly neither the compiler nor Eclipse should warn you about anything.Morn
I would see the benefit the other way around: read-guarantee. You know you can read to a Parent-typed variable. You don't know if the List is able to store Parent or only Child why it could be a bad idea to write to it.Didi
In addition to the answers (which already cover the main point: Flexibility!) : 1. Parent IS a subclass of Parent and 2. The list is not "write protected": You can still add null (or remove elements)Delphinus
S
4

List<A> is invariant. List<? extends A> is covariant, and List<? super A> is contravariant.

The covariant type can't be modified without a cast (which makes sense, because mutable collections are inherently not covariant).

If the collection doesn't need to be modified, then there is no reason not to use List<? extends A> to gain the appropriate subtype relationships (e.g. List<? extends Integer> is a subtype of List<? extends Number>).

I wouldn't suggest looking at this as "write protection" since the cast to circumvent it is trivial. Look at it as simply using the most general type possible. If you're writing a method that accepts a List<A> argument, and that method never needs to modify the list, then List<? extends A> is the more general and thus more appropriate type.

As a return type, using covariance is also useful. Suppose you're implementing a method whose return type is List<Number>, and you have a List<Integer> you want to return. You're kinda found as a stranger in the Alps: Either copy it or do an ugly cast. But if your types were correct, and the interface called for a List<? extends Number>, then you could just return the thing you have.

Seism answered 2/9, 2014 at 7:3 Comment(5)
((List<Whatever>) list).add(whatever)Seism
If Integer is a subtype of Number that doesn't mean that List<? extends Integer> is a subtype of List<? extends Number>.Cristiecristin
Yes - That's exactly what I said?Seism
@ChrisMartin Ok, I thought that was what you meant. I'm not sure that's really trivial - its unchecked and brittle and requires knowledge of the actual type used.Morn
Using the crappy standard collections library is already brittle and requires knowledge of the concrete type, since List is used for both mutable and immutable lists.Seism
C
3

You already mentioned the "write protection" as a benefit, which, depending on the usage scenario, may be useful or not.

Another benefit is some flexibility, in the sense that you can change the implementation of the method and write something along the lines of

    List<Child> il = new ArrayList<Child>();
    il.add(new Child("Hello"));
    il.add(new GrandChild("World")); // assume there is such a thing
    List<? extends Parent> result = il;
    return result;

which does change the type of the contents of your list, but it keeps the interface (read:public method signature) intact. This is crucial if you write a framework, for example.

Cristiecristin answered 2/9, 2014 at 6:55 Comment(2)
It has to be List<Child> (otherwise you can't even add new elements), and then you can't add GrandChild instances. I upvoted this (accidentally, before I noticed this error), because it's IMHO the main reason: You can return a List<Child> which still counts as List<? extends Parent>.Delphinus
My bad, started from the original code. Should be alright now.Cristiecristin
D
2

The ? extends X and ? super Y syntax comes in handy when you start working with specialized Lists in sub-classes and you need to ensure that no invalid object is mixed in. With regular ArrayLists you can always risk an unchecked cast and circumvent the restriction, but maybe this example illustrates the way you can use the parametrizations:

import java.util.AbstractList;
import java.util.List;

/**
 * https://mcmap.net/q/1986681/-is-there-a-benefit-for-a-list-lt-extends-myobject-gt/1266906
 */
public class ListTest {

    public static void main(String[] args) {
        ParentList parents = new ParentList(new Parent("one"), new Child("two"));
        List<? extends Parent> parentsA = parents; // Read-Only, items assignable to Parent
        List<? super Parent> parentsB = parents; // Write-Only, input of type Parent
        List<Parent> parentC = parents; // Valid as A and B were valid
        List<? super Child> parentD = parents; // Valid as you can store a Child
        // List<? extends Child> parentE = parents; // Invalid as not all contained values are Child

        ChildList children = new ChildList(new Child("one"), new Child("two"));
        List<? extends Parent> childrenA = children; // Read-Only, items assignable to Parent
        // List<? super Parent> childrenB = children; // Invalid as ChildList can not store a Parent
        List<? super Child> childrenC = children; // Write-Only, input of type Child
        // List<Parent> childrenD = children; // Invalid as B was invalid
        List<Child> childrenE = children;
    }


    public static class ChildList extends AbstractList<Child> {

        private Child[] values;

        public ChildList(Child... values) {
            this.values = values;
        }

        @Override
        public Child get(int index) {
            return values[index];
        }

        @Override
        public Child set(int index, Child element) {
            Child oldValue = values[index];
            values[index] = element;
            return oldValue;
        }

        @Override
        public int size() {
            return values.length;
        }
    }

    public static class ParentList extends AbstractList<Parent> {

        private Parent[] values;

        public ParentList(Parent... values) {
            this.values = values;
        }

        @Override
        public Parent get(int index) {
            return values[index];
        }

        @Override
        public Parent set(int index, Parent element) {
            Parent oldValue = values[index];
            values[index] = element;
            return oldValue;
        }

        @Override
        public int size() {
            return values.length;
        }
    }

    public static class Parent {

        private final String value;

        public Parent(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return value;
        }

    }

    public static class Child extends Parent {
        public Child(String value) {
            super(value);
        }
    }
}

A last note on the naming:

  • read ? extends X either as 'X or any type extending X' or 'something assignable to a variable of type X'
  • read ? super X either as 'X or any type X extends' or 'something I can assign a variable of type X to'.
Didi answered 2/9, 2014 at 8:7 Comment(4)
It's fine that you try to explain the workings of ? extends and ? super, but the lists will not be "read only". The lists can still be modified (they only can't be populated with wrong types)Delphinus
@Delphinus without further casting you will not be able to call set. So practically it becomes read-only while technically it is not. There is Collections.unmodifieableList() to make it 'read-only' if you do not use reflection, ... or else: it becomes harder to write why I choose to call it read-only.Didi
list.set(0, null) still works. But sure, in general, lists could/should be exposes as Collections.unmodifiableList - and in this case something like List<Parent> p = Collections.unmodifiableList(childList) works as well.Delphinus
I would usally combine both like List<? extends Parent> p = Collections.unmodifiableList(childList) as then your compiler and your runtime will both say "read-only".Didi
L
-1

when you say List<? extends Parent> it means any object of Parent Type or any of its children. I agree that the it is sort of misleading, but that's the way it works.

One potential benefit I can see from this pattern is when using reflection API in the main function.

Suppose if you have the function return List<Parent>, and in your main(), if you do

private static List<Parent> getItems() { ... }

public static void main(String[] args) {
    List<? extends Parent> items = getItems();
    for (Parent p : items) {
        System.out.println(p.getClass().getSimpleName());
    }
}

would yield you

Parent
Parent

where as if you had

private static List<? extends Parent> getItems() { ... }

The main would print

Parent
Child
Lasko answered 2/9, 2014 at 6:52 Comment(2)
-1 This is not correct. It seemed odd to me, so I tested the code and it prints Parent / Child in both cases.Morn
The instant you realize that generic typing is basically erased after compilation, you know that Java could not even do this if it wanted to...Didi

© 2022 - 2024 — McMap. All rights reserved.