Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
279 views
in Technique[技术] by (71.8m points)

java - How to reference a generic return type with multiple bounds

I have recently seen that one can declare a return type that is also bounded by an interface. Consider the following class and interface:

public class Foo {
    public String getFoo() { ... }
}

public interface Bar {
    public void setBar(String bar);
}

I can declare a return type like this:

public class FooBar {
    public static <T extends Foo & Bar> T getFooBar() {
        //some implementation that returns a Foo object,
        //which is forced to implement Bar
    }
}

If I call that method from somewhere, my IDE is telling me that the return type has the method String getFoo() as well as setBar(String), but only If I point a dot behind the Function like this:

FooBar.getFooBar(). // here the IDE is showing the available methods.

Is there a way to get a reference to such an Object? I mean, if I would do something like this:

//bar only has the method setBar(String)
Bar bar = FooBar.getFooBar();
//foo only has the getFoo():String method
Foo foo = FooBar.getFooBar();

I would like to have a reference like this (pseudo code):

<T extents Foo & Bar> fooBar = FooBar.getFooBar();
//or maybe
$1Bar bar = FooBar.getFooBar();
//or else maybe
Foo&Bar bar = FooBar.getFooBar();

Is this possible somehow in Java, or am I only able to declare return types like this? I think Java has to type it also, somehow. I'd prefer not to resort to a wrapper like this, as it feels like cheating:

public class FooBarWrapper<T extends Foo&Bar> extends Foo implements Bar {
    public T foobar;

    public TestClass(T val){
        foobar = val;
    }


    @Override
    public void setBar(String bar) {
        foobar.setBar(bar);
    }

    @Override
    public String getFoo() {
        return foobar.getFoo();
    }
}

Did Java really invent such a nice feature, but forget that one would like to have a reference to it?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

While the type parameters of a generic method can be restricted by bounds, such as extends Foo & Bar, they are ultimately decided by the caller. When you call getFooBar(), the call site already knows what T is being resolved to. Often, these type parameters will be inferred by the compiler, which is why you don't usually need to specify them, like this:

FooBar.<FooAndBar>getFooBar();

But even when T is inferred to be FooAndBar, that's really whats happening behind the scenes.

So, to answer your question, such a syntax like this:

Foo&Bar bothFooAndBar = FooBar.getFooBar();

Would never be useful in practice. The reason is that the caller must already know what T is. Either T is some concrete type:

FooAndBar bothFooAndBar = FooBar.<FooAndBar>getFooBar(); // T is FooAndBar

Or, T is an unresolved type parameter, and we're in its scope:

<U extends Foo & Bar> void someGenericMethod() {
    U bothFooAndBar = FooBar.<U>getFooBar(); // T is U
}

Another example of that:

class SomeGenericClass<V extends Foo & Bar> {
    void someMethod() {
        V bothFooAndBar = FooBar.<V>getFooBar(); // T is V
    }
}

Technically, that wraps up the answer. But I'd also like to point out that your example method getFooBar is inherently unsafe. Remember that the caller decides what T gets to be, not the method. Since getFooBar doesn't take any parameters related to T, and because of type erasure, its only options would be to return null or to "lie" by making an unchecked cast, risking heap pollution. A typical workaround would be for getFooBar to take a Class<T> argument, or else a FooFactory<T> for example.

Update

It turns out I was wrong when I asserted that the caller of getFooBar must always know what T is. As @MiserableVariable points out, there are some situations where the type argument of a generic method is inferred to be a wildcard capture, rather than a concrete type or type variable. See his answer for a great example of a getFooBar implementation that uses a proxy to drive home his point that T is unknown.

As we discussed in the comments, an example using getFooBar created confusion because it takes no arguments to infer T from. Certain compilers throw an error on a contextless call to getFooBar() while others are fine with it. I thought that the inconsistent compile errors - along with the fact that calling FooBar.<?>getFooBar() is illegal - validated my point, but these turned out to be red herrings.

Based on @MiserableVariable's answer, I put together an new example that uses a generic method with an argument, to remove the confusion. Assume we have interfaces Foo and Bar and an implementation FooBarImpl:

interface Foo { }
interface Bar { }
static class FooBarImpl implements Foo, Bar { }

We also have a simple container class that wraps an instance of some type implementing Foo and Bar. It declares a silly static method unwrap that takes a FooBarContainer and returns its referent:

static class FooBarContainer<T extends Foo & Bar> {

    private final T fooBar;

    public FooBarContainer(T fooBar) {
        this.fooBar = fooBar;
    }

    public T get() {
        return fooBar;
    }

    static <T extends Foo & Bar> T unwrap(FooBarContainer<T> fooBarContainer) {
        return fooBarContainer.get();
    }
}

Now let's say we have a wildcard parameterized type of FooBarContainer:

FooBarContainer<?> unknownFooBarContainer = ...;

We're allowed to pass unknownFooBarContainer into unwrap. This shows my earlier assertion was wrong, because the call site doesn't know what T is - only that it is some type within the bounds extends Foo & Bar.

FooBarContainer.unwrap(unknownFooBarContainer); // T is a wildcard capture, ?

As I noted, calling unwrap with a wildcard is illegal:

FooBarContainer.<?>unwrap(unknownFooBarContainer); // compiler error

I can only guess that this is because wildcard captures can never match each other - the ? argument provided at the call site is ambiguous, with no way of saying that it should specifically match the wildcard in the type of unknownFooBarContainer.

So, here's the use case for the syntax the OP is asking about. Calling unwrap on unknownFooBarContainer returns a reference of type ? extends Foo & Bar. We can assign that reference to Foo or Bar, but not both:

Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = FooBarContainer.unwrap(unknownFooBarContainer);

If for some reason unwrap were expensive and we only wanted to call it once, we would be forced to cast:

Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = (Bar)foo;

So this is where the hypothetical syntax would come in handy:

Foo&Bar fooBar = FooBarContainer.unwrap(unknownFooBarContainer);

This is just one fairly obscure use case. There would be pretty far-ranging implications for allowing such a syntax, both good and bad. It would open up room for abuse where it wasn't needed, and it's completely understandable why the language designers didn't implement such a thing. But I still think it's interesting to think about.


A note about heap pollution

(Mostly for @MiserableVariable) Here's a walkthrough of how an unsafe method like getFooBar causes heap pollution, and its implications. Given the following interface and implementations:

interface Foo { }

static class Foo1 implements Foo {
    public void foo1Method() { }
}

static class Foo2 implements Foo { }

Let's implement an unsafe method getFoo, similar to getFooBar but simplified for this example:

@SuppressWarnings("unchecked")
static <T extends Foo> T getFoo() {
    //unchecked cast - ClassCastException is not thrown here if T is wrong
    return (T)new Foo2();
}

public static void main(String[] args) {
    Foo1 foo1 = getFoo(); //ClassCastException is thrown here
}

Here, when the new Foo2 is cast to T, it is "unchecked", meaning because of type erasure the runtime doesn't know it should fail, even though it should in this case since T was Foo1. Instead, the heap is "polluted", meaning references are pointing to objects they shouldn't have been allowed to.

The failure happens after the method returns, when the Foo2 instance tries to get assigned to the foo1 reference, which has the reifiable type Foo1.

You're probably thinking, "Okay so it blew up at the call site instead of the method, big deal." But it can easily get more complicated when more generics are involved. For example:

static <T extends Foo> List<T> getFooList(int size) {
    List<T> fooList = new ArrayList<T>(size);
    for (int i = 0; i < size; i++) {
        T foo = getFoo();
        fooList.add(foo);
    }
    return fooList;
}

public static void main(String[] args) {

    List<Foo1> foo1List = getFooList(5);

    // a bunch of things happen

    //sometime later maybe, depending on state
    foo1List.get(0).foo1Method(); //ClassCastException is thrown here
}

Now it doesn't blow up at the call site. It blows up sometime later when the contents of foo1List get used. This is how heap pollution gets harder to debug, because the exception stacktrace doesn't point you to the actual problem.

It gets even more complicated when the caller is in generic scope itself. Imagine instead of getting a List<Foo1> we're getting a List<T>, putting it in a Map<K, List<T>> and returning it to yet another method. You get the idea I hope.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...