Use generic to store common supertype in Java
Asked Answered
J

2

13

Suppose I have a method "mix" that takes two Lists of possibly different types T and S and returns a single List containing the elements of both. For type-safety, I'd like to specify that the returned List is of a type R, where R is a supertype common to both T and S. For example:

List<Number> foo = mix(
    Arrays.asList<Integer>(1, 2, 3),
    Arrays.asList<Double>(1.0, 2.0, 3.0)
);

To specify this, I could declare the method as

static <R, T extends R, S extends R> List<R> mix(List<T> ts, List<S> ss)

But what if I want to make mix an instance method instead of static, on the class List2<T>?

<R, T extends R, S extends R> List<R> mix ...

shadows the <T> on the instance of List2, so that's no good.

<R, T extends S&T, S extends R> List<R> mix ...

solves the shadowing problem, but isn't accepted by the compiler

<R super T, S extends R> List<R> mix ...

is rejected by the compiler because lower-bounded wildcards can't be stored in a named variable (only used in ? super X expressions)

I could move the arguments to the class itself, like List2<R, T extends R, S extends R>, but the type information really has no business being on the instance level, because it's only used for one method call, and you would have to re-cast the object every time you wanted to invoke the method on different arguments.

As far as I can tell, there's no way to do this with generics. The best I can do would be to return a raw List2 and cast it at the callsite, like before generics were introduced. Does anybody have a better solution?

Jadejaded answered 18/8, 2013 at 21:12 Comment(10)
Why do you have S extends T instead of S extends R?Meza
Also, it would be better if you can show the body of your method - mix. What exactly are you doing in it? Do you really need those those type parameters? It looks like you can go with bounded wildcards instead.Meza
In fact, I don't understand the need of R type parameter. Since you just intend to return a List<Number>, then just have that as return type. Frankly speaking, I don't see any use of generic method here.Meza
whoops. It should be S extends R, that was a typo that got copy-pasted.Jadejaded
the List<Number> part is just an example. I want it to be able to mix any two types. A trivial implementation of mix could be just ArrayList<R> result = new ArrayList<>(ts.size() + ss.size()); result.addAll(ts); result.addAll(ss); return result;Jadejaded
R is intended to be some superclass common to T and S. For a Double and an Integer, it could be Number. For a StringBuilder and a String, it could be CharSequence. For a Scanner and a Comparator, it could be Object.Jadejaded
<R super T, S extends R> List<R> is really the signature that would make sense. Related post: Bounding generics with 'super' keyword. You may have to settle for keeping it a static method.Backandforth
+1 This is a good one. I don't think there is a way to do this. Unless you declare the relationship of T against R in the declartion of the type List2. Which would simply be moving the type parameters in the method declaration to the class declartion: class List2<R, T extends R, S extends R>, which I assume is not what you wanted.Disentwine
@PaulBellora. R super T is not supported.Meza
@RohitJain Yes, I know that. Please read my entire comment.Backandforth
B
8

As noted in the question and in the comments, the following signature would be ideal:

<R super T, S extends R> List<R> mix(List<S> otherList)

But of course, R super T is not allowed by the language (note that polygenelubricants's answer on the linked post is wrong - there are use cases for this syntax, as your question demonstrates).

There's no way to win here - you only have one of several workarounds to choose from:

  • Resort to using a signature with raw types. Don't do this.
  • Keep mix a static method. This is actually a decent option, unless it needs to be part of your class's interface for polymorphism-related reasons, or you plan for mix to be such a commonly used method that you think keeping it static is unnacceptable.
  • Settle with the signature of mix being overly restrictive, and document that certain unchecked casts will be necessary on the part of the caller. This is similar to what Guava's Optional.or had to do. From that method's documentation:

Note about generics: The signature public T or(T defaultValue) is overly restrictive. However, the ideal signature, public <S super T> S or(S), is not legal Java. As a result, some sensible operations involving subtypes are compile errors:

Optional<Integer> optionalInt = getSomeOptionalInt();
Number value = optionalInt.or(0.5); // error

As a workaround, it is always safe to cast an Optional<? extends T> to Optional<T>. Casting [the above Optional instance] to Optional<Number> (where Number is the desired output type) solves the problem:

Optional<Number> optionalInt = (Optional) getSomeOptionalInt();
Number value = optionalInt.or(0.5); // fine

Unfortunately for you, it's not always safe to cast List2<? extends T> to List2<T>. For example, casting a List2<Integer> to a List2<Number> could permit a Double to be added to something that was only supposed to hold Integers and lead to unexpected runtime errors. The exception would be if List2 was immutable (like Optional), but this seems unlikely.

Still, you could get away with such casts if you were careful and documented type-unsafe code with explanations. Assuming mix had the following signature (and implementation, for fun):

List<T> mix(final List<? extends T> otherList) {

    final int totalElements = (size() + otherList.size());
    final List<T> result = new ArrayList<>(totalElements);

    Iterator<? extends T> itr1 = iterator();
    Iterator<? extends T> itr2 = otherList.iterator();
    while (result.size() < totalElements) {
        final T next = (itr1.hasNext() ? itr1 : itr2).next();
        result.add(next);
        final Iterator<? extends T> temp = itr1;
        itr1 = itr2;
        itr2 = temp;
    }

    return result;
}

Then you might have the following call site:

final List2<Integer> ints = new List2<>(Arrays.asList(1, 2, 3));
final List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);

final List<Number> mixed;
// type-unsafe code within this scope
{
    @SuppressWarnings("unchecked") // okay because intsAsNumbers isn't written to
    final List2<Number> intsAsNumbers = (List2<Number>)(List2<?>)ints;
    mixed = intsAsNumbers.mix(doubles);
}

System.out.println(mixed); // [1, 1.5, 2, 2.5, 3, 3.5]

Again, a settling for a static mix is going to be cleaner and have no risk to type-safety. I would make sure to have very good reasons not to keep it that way.

Backandforth answered 20/8, 2013 at 3:55 Comment(0)
I
0

The only thing I'm not sure in your question is whether you already know of which supertype these subclasses extends, or you want a completely generic method where you'd pass two subtypes of any given superclass.

In the first case, I did something similar recently, with an Abstract Class and several subtypes:

public <V extends Superclass> List<Superclass> mix(List<V> list1, List<V> list2) {
  List<Superclass> mixedList;

  mixedList.addAll(list1);
  mixedList.addAll(list2);
}

The latter case is much more complicated. I'd suggest you rethink your design, since it makes much more sense for the mix method to be in the Superclass or in a class which knows the superclass and its subtypes, since you're returning a List of the Superclass.

If you really want to do this, you would have to refactor List2 to List2 and do the following:

public <R, V extends R> List<R> mix(List<V> list1, List<V> list2) {
    List<R> mixedList;

    mixedList.addAll(list1);
    mixedList.addAll(list2);

    return mixedList;
}
Immortalize answered 22/1, 2014 at 22:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.