Why can't a Generic Type Parameter have a lower bound in Java?
Asked Answered
Z

7

57

I gather that you cannot bind a Java generics type parameter to a lower bound (i.e. using the super keyword). I was reading what the Angelika Langer Generics FAQ had to say on the subject. They say it basically comes down to a lower bound being useless ("not making any sense").

I'm not convinced. I can imagine a use for them to help you be more flexible to callers of a library method that produces a typed result. Imagine a method that created an array list of a user-specified size and filled it with the empty string. A simple declaration would be

public static ArrayList<String> createArrayListFullOfEmptyStrings(int i);

But that's unnecessarily restrictive to your clients. Why can't they invoke your method like this:

//should compile
List<Object> l1 = createArrayListFullOfEmptyStrings(5); 
List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);
List<String> l3 = createArrayListFullOfEmptyStrings(5);

//shouldn't compile
List<Integer> l4 = createArrayListFullOfEmptyStrings(5);

At this point I would be tempted to try the following definition:

public static <T super String> List<T> createArrayListFullOfEmptyStrings(int size) {
  List<T> list = new ArrayList<T>(size);
  for(int i = 0; i < size; i++) {
     list.add("");
  }
  return list;
}

But it will not compile; the super keyword is illegal in this context.

Is my example above a bad example (ignoring what I say below)? Why isn't a lower bound useful here? And if it would be useful, what's the real reason that it is not permitted in Java?

P.S.

I know that a better organization might be something like this:

public static void populateListWithEmptyStrings(List<? super String> list, int size);

List<CharSequence> list = new ArrayList<CharSequence>();
populateListWithEmptyStrings(list, 5);

Can we for the purpose of this question pretend that due to a requirement, we need to do both operations in one method call?

Edit

@Tom G (justifiably) asks what benefit having a List<CharSequence> would have over a List<String>. For one, nobody said the returned list is immutable, so here's one advantage:

List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);
l2.add(new StringBuilder("foo").append("bar"));
Zsolway answered 4/2, 2011 at 20:44 Comment(6)
Another legitimate use of a lower bound would be with Collection.toArray(T[]). By changing the type variable from <T> to <T super E>, you could prevent ArrayStoreExceptions from happening with that method.Bonne
@Jeffrey: That's a great use case. I never realized that toArray isn't bound in any way to the element type of the collection.Zsolway
Another use-case that I stumbled across: Guava's Optional.or(T). "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..."Zsolway
Here's a method I wanted to write that would use an upper- and lower-bounded type parameter (though I don't think that's legal even with wildcards): <T, C extends Collection<T> super List<T>> C internIfEmpty(C collection) { return collection.isEmpty() ? Collections.emptyList() : collection; }Unweighed
@shmosel: That's a good one. Since it'd all need to be statically known anyway, this is probably a good case for just overloading the methods...one for <T> Collection<T> internIfEmpty(Collection<T>) and another <T> List<T> internIfEmpty(List<T>). The compiler will call the more specific one for you, and you can just have the first return the result of the second.Zsolway
@MarkPeters, that's exactly what I'm doing. But it would be nice if there were a way to declare that some type is within a bounded range.Unweighed
K
16

Basically, it's not useful enough.

I think your example points out the only advantage of a lower bound, a feature the FAQ calls Restricted Instantiation:

The bottom line is: all that a " super " bound would buy you is the restriction that only supertypes of Number can be used as type arguments. ....

But as the other posts point out, the usefulness of even this feature can be limited.

Due to the nature of polymorphism and specialization, upper bounds are far more useful than lower bounds as described by the FAQ (Access To Non-Static Members and Type Erasure). I suspect the complexity introduced by lower bounds aren't worth its limited value.


OP: I want to add I think you did show it is useful, just not useful enough. Come up with the irrefutable killer use cases and I'll back the JSR. :-)

Koh answered 4/2, 2011 at 21:20 Comment(8)
Hah, I don't care that much. I'm fine with this being a cost-benefit decision. I was just frustrated at the FAQ's insistence that they "make no sense". Unfortunately they only examine the scenario of class type parameters in that section, while completely ignoring method type parameters.Zsolway
@Mark - yeah, the wording in the FAQ is a little strong, given the statements later conceding a useful feature that gets downplayed (for right or wrong). Thanks for the interesting question though - I like the ones that make me think and learn something new, or at least better than I knew it before.Koh
"Come up with the irrefutable killer use cases and I'll back the JSR." How about an interface that's intrinsically Covariant. Because Java has no concept of covariance, the best I could do (assuming I could do TSuper super TCovariant) is expose a simple method that casts the object (ie, a method on Iterable<T> like "castUp<TSuper super T() {return (Iterable<TSuper>) this;}). Such a method, combined with Java's type-inference, would be really handy, no?Ambsace
@Bert F: I think you are misreading the Generics FAQs here. The passage you cite actually talks about super (i.e. lower) bounds for type parameter on classes. The FAQs has this to say about lower bounds for methods: "On the other hand, generic methods would occasionally profit from a type parameter with a lower bound". One such case appears in Mark Peters' example.Chichi
@Lil You are right, but I didn't misread the FAQ - the FAQ changed since I posted my answer years ago (web.archive.org/web/20110707143151/http://…). The FAQ changed to acknowledge that the OPs corner case is occasionally useful, which I also acknowledge in my last sentence. I may update the answer in the future reflect the updated FAQ and your comment.Koh
What about a database manager class which is downward compatible up to a certain point? E.g. you want to be able to include super-classes of your type but create elements of your new type. You might want something like: class DBManager<T extends foo, S super T>{...} where T represents the current version of your data-type and S the older versions you want to include in your manager. Assuming you build new versions by extension of the older versions.Pampa
@Ambsace To give your example some oomph, I'd mention that covariance includes every read-only interface/value-type, including immutable collections, suppliers, streams, optionals, and generic value wrappers. Given the prevalence of prefer-immutable-data and functional syntax with Java 8, this seems much more relevant now than it was 5 years ago.Bod
There is a popular use case in a type hierarchy you surely use daily: Collection<E>.toArray(T[]). The type parameter T of the method toArray is supposed to denote a super type of the Collection’s type parameter E, but it is not possible to specify the relationship, therefore T can be anything, unrelated to E, and the compiler won’t tell you. This affects all collections.Ribbonfish
H
10

the spec does talk about lower bounds of type parameters, for example

4.10.2

a type variable is a direct supertype of its lower bound.

5.1.10

a fresh type variable ... whose lower bound

It appears that a type variable only has a (non-null) lower bound if it's a synthetic one as result of wildcard capture. What if the language allow lower bounds on all type parameters? Probably it doesn't cause a lot of trouble, and it's excluded only to keep generics simpler (well ...) Update it is said that theoretical investigation of lower bounded type parameters is not thoroughly conducted.

Update: a paper claiming lower bounds are ok: "Java Type Infererence Is Broken: Can We Fix It" by Daniel Smith

RETRACT: the following argument is wrong. OP's example is legitimate.

Your particular example is not very convincing. First it's not type safe. The returned list is indeed a List<String>, it's unsafe to view it as another type. Suppose your code compiles:

    List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);

then we can add non-String to it, which is wrong

    CharSequence chars = new StringBuilder();
    l2.add(chars); 

Well a List<String> is not, but somewhat like a list of CharSequence. Your need can be solved by using wildcard:

public static  List<String> createArrayListFullOfEmptyStrings(int size)  

// a list of some specific subtype of CharSequence 
List<? extends CharSequence> l2 = createArrayListFullOfEmptyStrings(5);

// legal. can retrieve elements as CharSequence
CharSequence chars = l2.get(0);

// illegal, won't compile. cannot insert elements as CharSequence
l2.add(new StringBuilder());
Hyden answered 4/2, 2011 at 23:49 Comment(4)
Thanks for the JLS refs. Now two points. 1) It's not type-unsafe to assign the result of createArray... to a List<CharSequence> (if lower bounds were supported). It never was a List<String>; it will have been a List<CharSequence> "from creation" (though with erasure that's a weird thing to say). It will have never been viewed as a List<String>. Going on to add a StringBuilder instance to it would in no way be unsafe. 2) Assigning the result to a List<? extends CharSequence> gives absolutely no benefit over just using a List<String>; it's actually more restrictive.Zsolway
You are right, your example is valid. Lower bound is not included probably because it's not studied well. It's not a simple matter, there must be theoretical support. For example, see JLS3 15.12.2.7, and notice how complicated it is. It is so complicated, java compilers have a long list of bugs associated with it. Now imagine we add lower bound. Not only the algorithm will inflate many times, but we probably don't even know whether it's a sound algorithm.Hyden
Thanks, upvoted for the JLS references. It definitely is complicated stuff; if there's one place that the JLS loses me it's when dealing with the formalities of Generics. You say "it is said that theoretical investigation of lower bounded type parameters is not thoroughly conducted". For my curiousity, do you have a link/reference to where that's said?Zsolway
The paper mentioned by the JLS, which Java Generics is based upon: sato.kuis.kyoto-u.ac.jp/~igarashi/papers/variance.htmlHyden
R
1

More than an answer, this is another (possibly killer?) use case. I have a ModelDecorator helper. I want it to have the following public API

class ModelDecorator<T>{
    public static <T> ModelDecorator<T> create(Class<T> clazz);
    public <SUPER> T from(SUPER fromInstance);
}

So, given classes A, B extends A, it can be used like this:

A a = new A();
B b = ModelDecorator.create(B.class).from(a);

But I want to have bounds on T and SUPER, so I make sure that only subclases can be instantiated using the API. At this moment, I can do:

C c = new C();
B b = ModelDecorator.create(B.class).from(c);

Where B DOES not inherit from C.

Obviously, if I could do:

    public <SUPER super T> T from(SUPER fromInstance);

That would solve my problem.

Rapallo answered 7/2, 2017 at 7:46 Comment(1)
Interesting use case. I wonder if this wouldn't really work even with super, because you could always satisfy SUPER with Object, meaning you could still pass anything into from(), because everything is assignable to Object. It wouldn't actually enforce that c's concrete type is a supertype to B, just that one of c's supertypes is also a supertype of B (which we already know to be true with Object).Zsolway
E
0

What advantage does typing the List give you at that point? When you iterate over the returned collection, you should still be able to do the following:

for(String s : returnedList) {
CharSequence cs = s;
//do something with your CharSequence
}
Epistemic answered 4/2, 2011 at 20:47 Comment(2)
Thanks Tom, I'll add an edit as a reply. In short, it would allow you to add something other than a String to the list.Zsolway
Sounds good. Understand that I don't think it should be illegal -- I just think it doesn't scratch an itch I've had.Epistemic
C
0

Edit: I bring good news. There is a way to get most of what you want.

public static <R extends List<? super String>> R createListFullOfEmptyString(IntFunction<R> creator, int size)
{
  R list = creator.apply(size);
  for (int i = 0; i < size; i++)
  {
    list.add("");
  }
  return list;
}

// compiles
List<Object> l1 = createListFullOfEmptyString(ArrayList::new, 5);
List<CharSequence> l2 = createListFullOfEmptyString(ArrayList::new, 5);
List<String> l3 = createListFullOfEmptyString(ArrayList::new, 5);
// doesn't compile
List<Integer> l4 = createListFullOfEmptyString(ArrayList::new, 5);

The downside is clients do need to provide either an instance of R to mutate, or some means to construct an R. There is no other way to safely construct it.

I'll retain my original answer below for informational purposes.


In summary:

There is not a good reason, it just has not been done.

And until such time as it is, it will be impossible to write exact types with correct variance for methods that do all of:

A) Accept or create parametrized data structure

B) Write computed (not-passed-in) value(s) to that data structure

C) Return that data structure

Writing/accepting values is exactly the case where contravariance applies, which means the type parameter on the data structure must be lower-bounded by the type of the value being written to the data structure. The only way to express that in Java currently is using a lower-bounded wildcard on the data structure, e.g. List<? super T>.


If we are designing an API such as the OP's, which might naturally (but not legally) be expressed as:

// T is the type of the value(s) being computed and written to the data structure

// Method creates the data structure
<S super T> Container<S> create()

// Method writes to the data structure
<S super T> Container<S> write(Container<S> container)

Then the options available to us are:

A) Use a lower-bounded wildcard, and force callers to cast the output:

// This one is actually useless - there is no type the caller can cast to that is both read- and write-safe.
Container<? super T> create()

// Caller must cast result to the same type they passed in.
Container<? super T> write(Container<? super T> container)

B) Overly restrict the type parameter on the data structure to match the type of the value being written, and force callers to cast the input and output:

// Caller must accept as-is; cannot write values of type S (S super T) into the result.
Container<T> create()

// Caller must cast Container<S> (S super T) to Container<T> before calling, then cast the result back to Container<S>.
Container<T> write(Container<T> container)

C) Use a new type parameter and do our own unsafe casting internally:

// Caller must ensure S is a supertype of T - we cast T to S internally!
<S> Container<S> create()

// Caller must ensure S is a supertype of T - we cast T to S internally!
<S> Container<S> write(Container<S> container)

Pick your poison.

Chameleon answered 14/7, 2019 at 4:43 Comment(0)
W
-1

This gets you almost there.

static public void main(String...args){
    List<Object> l1 = createArrayListFullOf("", 5);
    List<CharSequence> l2 = createArrayListFullOf("", 5);
    List<String> l3 = createArrayListFullOf("", 5);

    // vvv compile time error vvv
    List<Integer> l4 = createArrayListFullOf("", 5);
}


public static <T, E extends T> List<T> createArrayListFullOf(E value, int size) {
    List<T> list = new ArrayList<T>(size);
    for (int i = 0; i < size; i++) {
        list.add(value);
    }
    return list;
}

I think this maybe closer to what you are looking for. Basically T becomes a super of E by making E extends T. The problem is you need some reference for type for T - namely E that needs to be bound. I this case a value-to-fill parameter is added that binds to E.

Whippoorwill answered 15/3 at 17:11 Comment(2)
How is "almost an answer" an answer?Gerianne
It gets you a similar effect of a bounded super. However it is not bounded to a literal type but an another generic variable will is bind to a type at invocation time. So if the Op is looking for a lower bounded generic this can be used. It also provides the invocation behavior the OP is looking for (Works with Object, CharSequence, String, fails on Integer).Whippoorwill
E
-3

Hmm, ok - let's work with this. You define a method:

public static <T super String> List<T> createArrayListFullOfEmptyStrings(int size) {

What does that mean? It means that if I call your method, then I get back a list of some superclass of String. Maybe it returns a list of String. Maybe it returns a list of Object. I don't know.

Cool.

List<Object> l1 = createArrayListFullOfEmptyStrings(5);

According to you, that should compile. But that's not right! I can put an Integer into a list of Object - l1.add(3) . But if you are returning a list of String, then doing that should be illegal.

List<String> l3 = createArrayListFullOfEmptyStrings(5);

According to you, that should compile. But that's not right! l3.get(1) should always return a String ... but that method might have returned a list of Object, meaning that l3.get(1) could conceivably be an Integer.

The only thing that works is

List<? super String> l5 = createArrayListFullOfEmptyStrings(5);

All I know is that I can safely call l4.put("foo"), and I can safely get Object o = l4.get(2) .

Escarpment answered 16/2, 2011 at 8:0 Comment(2)
Of course you can put an Integer into it (if you use List<Object>). **I'm not returning a List of Strings, I'm returning a List of Objects that happens to only contain Strings at that point. There's nothing unsafe about it.Zsolway
As for the List<String> being able to contain an Integer, that's not how lower bounds work. You can't add anything but a String to a List<? super String>.Zsolway

© 2022 - 2024 — McMap. All rights reserved.