Converting List of childs to List of parents in one line

Converting List of Banana to List of Fruit ...

public class Fruit { }

public class Banana extends Fruit { }

public List<Banana> allBananas() {
    return new ArrayList<>(Arrays.asList(new Banana(), new Banana()));
}

... before returning I have to do following two-steps-casting:

public List<Fruit> getFruits() {
    List<? extends Fruit> fruits = (List<? extends Fruit>) allBananas();
    return (List<Fruit>) fruits;
}

To avoid the intermediate step it can be done as:

public List<Fruit> getFruits() {
    return allBananas().stream().collect(Collectors.toList());
}

But in this case I have to create new List object which is not so good in terms of performance.

I think I don't understand something and maybe doing it wrong. I want to know is my code fully correct and safe? If so - is there a better way to cast? If not - what is wrong and can be improved?

Jon Skeet
people
quotationmark

It really depends on what you want to achieve.

If you're happy to create a copy of the list, then I'd just use something like:

public List<Fruit> getFruits() {
    return new ArrayList<>(allBananas());
}

That's now independent of the original list, so the caller can then do what they want with it. (If they modify any of the existing bananas, those modifications will be seen of course, but that's not a change to the list itself.)

However, it sounds like you don't want to copy the list, which means you effectively want some kind of view on the existing list. The tricky bit here is doing that safely. Your casting code is not safe:

// Don't do this!
public List<Fruit> getFruits() {
    List<? extends Fruit> fruits = (List<? extends Fruit>) allBananas();
    return (List<Fruit>) fruits;
}

Here's why it's broken:

List<Banana> bananas = allBananas();
List<Fruit> fruit = getFruits();
System.out.println(bananas == fruits); // Same list
fruit.add(new Apple()); // This is fine, right?
Banana banana = bananas.get(0); // This should be fine, right?

You'll actually end up with an invalid cast exception in the last line, because you've basically violated type safety in your getFruits() method. A List<Banana> is not a List<Fruit>.

Your safe options are:

  • Change the method return type to List<? extends Fruit> at which point you don't need any casting:

    public List<? extends Fruit> getFruits() {
        return allBananas();
    }
    

    Now the caller can't add to the list (except null), but can remove items from the list.

  • Use an implementation of List<Fruit> which permits additions, but checks at execution time that each item is a Banana, e.g.

    return (List<Fruit>) Collections.checkedList(allBananas(), Banana.class);
    
  • Return an unmodifiable view of the list, e.g.

    public List<Fruit> getFruits() {
        return Collections.unmodifiableList(allBananas());
    }
    

Which option is appropriate (including copying) is very context-dependent.

people

See more on this question at Stackoverflow