javaarraylistcollectionssuperclassabstraction

Efficient way to abstract multidimensional ArrayList to Collection


I have a class that returns an ArrayList of ArrayLists. I would like to return a Collection of Collections to abstract the output datatype.

With a standard ArrayList, its easy - just define the return type as Collection. Unfortunately, with a 2D ArrayList, that doesn't work:

//This does not compile
public Collection<Collection<Object> myFunction() {
  return new ArrayList<ArrayList<Object>>();
}

//This does compile (but isn't what I want)
public Collection<ArrayList<Object> myFunction() {
  return new ArrayList<ArrayList<Object>>();
}

Obviously one can create a Collection, then add the ArrayLists individually, converting them to Collections, but this is more complicated than I would expect. What is the proper way to abstract a multidimensional return type?


Solution

  • Obviously one can create a Collection, then add the ArrayLists individually, converting them to Collections, but this is more complicated than I would expect. What is the proper way to abstract a multidimensional return type?

    That's not how it works. ArrayLists are a collection. That's what class Dog extends Animal means: All dogs already are animals. There is no 'converting'. You don't convert a dog to an animal, it's already one.

    Hence, add the ArrayLists individually, converting them to Collections is nonsense.

    // paraphrased
    ArrayList<ArrayList<Object>> a = new ArrayList<ArrayList<Object>>();
    Collection<Collection<Object>> b = a;
    

    no, this does not compile, because that is a nonsensical statement. The thing is, collections have an add method. You can add things to them. And java is a reference-based system, so in the code above there is only a single list-of-lists (count the new - I only count one, so there must be only one list here, there cannot possibly be 2), just.. you have 2 variables pointing at the same thing. Like an address book with 2 pages, both with the same address on it.

    Via variable b I could do: b.add(new HashSet<Object>()). That should be obvious: All HashSets are collections, the .add() method of variable b requires a Collection<Object>, and.. new HashSet<Object>() is a Collection<Object>, therefore, that is allowed. And indeed that is valid java here.

    Except.. it's the same list. a also points at this list. Which.. now has a HashSet in it. A variable of type ArrayList<ArrayList<Object>> has.. a HashSet in it.

    Oh dear.

    That's all wrong.

    Which is why java does not allow Collection<Collection<Object>> b = a; here - because that is nonsense.

    You can opt into covariance by writing:

    Collection<? extends Collection<?>> b = a;
    

    This is not too useful - the point of ? here is to stop you from calling 'add', and then all is well. This assignment works, and indeed b.add(new HashSet<>()) no longer works now, but, b.get(0).add(someObject) also no longer works.

    The correct move is to go to where-ever you make that initial arraylist and make it correct right from the get-go: Make that a new ArrayList<Collection<Object>>. Because all ArrayLists are collections, you can call .add(new ArrayList<Object>()) on a variable of type ArrayList<Collection<Object>>, no problem. And you can assign an ArrayList<Collection<Object>> to a variable of type Collection<Collection<Object>>.

    Generics are invariant - if you need type FourFootedAnimal, only that type will do. You can't provide a subtype or a supertype. But outside of generics, java is covariant - FourFootedAnimals will do, but so will a Dog or a Cat. If you want covariance, use ? extends (and ? on its own is short for ? extends Object), but note that this disables all attempts to add.