Raf*_*l T 38 java generics interface multiple-inheritance
我最近看到可以声明一个也受接口限制的返回类型.考虑以下类和接口:
public class Foo {
public String getFoo() { ... }
}
public interface Bar {
public void setBar(String bar);
}
Run Code Online (Sandbox Code Playgroud)
我可以声明一个这样的返回类型:
public class FooBar {
public static <T extends Foo & Bar> T getFooBar() {
//some implementation that returns a Foo object,
//which is forced to implement Bar
}
}
Run Code Online (Sandbox Code Playgroud)
如果我调用该方法从什么地方,我的IDE告诉我,在返回类型的方法String getFoo(),以及setBar(String),但只有当我点了后面的点功能如下:
FooBar.getFooBar(). // here the IDE is showing the available methods.
Run Code Online (Sandbox Code Playgroud)
有没有办法获得对这样一个对象的引用?我的意思是,如果我做这样的事情:
//bar only has the method setBar(String)
Bar bar = FooBar.getFooBar();
//foo only has the getFoo():String method
Foo foo = FooBar.getFooBar();
Run Code Online (Sandbox Code Playgroud)
我想有这样的参考(伪代码):
<T extents Foo & Bar> fooBar = FooBar.getFooBar();
//or maybe
$1Bar bar = FooBar.getFooBar();
//or else maybe
Foo&Bar bar = FooBar.getFooBar();
Run Code Online (Sandbox Code Playgroud)
这在Java中是否有可能,或者我只能声明这样的返回类型?我认为Java也必须以某种方式输入它.我宁愿不诉诸这样的包装,因为它感觉像是作弊:
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();
}
}
Run Code Online (Sandbox Code Playgroud)
Java真的发明了这么好的功能,但是忘记了想要引用它吗?
Pau*_*ora 30
虽然泛型方法的类型参数可以受到边界的限制,例如extends Foo & Bar,它们最终由调用者决定.当您致电时getFooBar(),呼叫站点已经知道T要解决的问题.通常,这些类型参数将由编译器推断,这就是您通常不需要指定它们的原因,如下所示:
FooBar.<FooAndBar>getFooBar();
Run Code Online (Sandbox Code Playgroud)
但即使T被推断出来FooAndBar,那真的是幕后发生的事情.
那么,要回答你的问题,这样的语法如下:
Foo&Bar bothFooAndBar = FooBar.getFooBar();
Run Code Online (Sandbox Code Playgroud)
在实践中永远不会有用.原因是调用者必须已经知道是什么T.要么T是一些具体的类型:
FooAndBar bothFooAndBar = FooBar.<FooAndBar>getFooBar(); // T is FooAndBar
Run Code Online (Sandbox Code Playgroud)
或者,T是一个未解析的类型参数,我们在其范围内:
<U extends Foo & Bar> void someGenericMethod() {
U bothFooAndBar = FooBar.<U>getFooBar(); // T is U
}
Run Code Online (Sandbox Code Playgroud)
另一个例子:
class SomeGenericClass<V extends Foo & Bar> {
void someMethod() {
V bothFooAndBar = FooBar.<V>getFooBar(); // T is V
}
}
Run Code Online (Sandbox Code Playgroud)
从技术上讲,这就包含了答案.但我还想指出你的示例方法getFooBar本质上是不安全的.请记住,调用者决定了什么T,而不是方法.由于getFooBar没有采取任何与之相关的参数T,并且由于类型擦除,它唯一的选择是null通过进行未经检查的演员来返回或"撒谎",从而冒着堆污染的风险.一个典型的解决办法将是getFooBar采取Class<T>说法,要不然FooFactory<T>例如.
当我断言调用者getFooBar必须总是知道是什么时,事实证明我错了T.正如@MiserableVariable指出的那样,在某些情况下,泛型方法的type参数被推断为通配符捕获,而不是具体的类型或类型变量.请参阅他的答案,了解一个getFooBar使用代理实现其T未知点的实现的一个很好的例子.
正如我们在评论中讨论的那样,一个使用getFooBar创建混淆的例子,因为它不需要参数来推断T.某些编译器会在无环境调用时抛出错误,getFooBar()而其他编译器则可以正常运行.我认为不一致的编译错误 - 以及调用FooBar.<?>getFooBar()是非法的事实- 验证了我的观点,但这些结果证明是红色的鲱鱼.
基于@MiserableVariable的答案,我将一个使用泛型方法和参数的新示例放在一起,以消除混淆.假设我们有接口Foo和Bar实现FooBarImpl:
interface Foo { }
interface Bar { }
static class FooBarImpl implements Foo, Bar { }
Run Code Online (Sandbox Code Playgroud)
我们也有一些包装类型实现的一个实例的简单容器类Foo和Bar.它声明了一个愚蠢的静态方法unwrap,它接受FooBarContainer并返回它的引用:
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();
}
}
Run Code Online (Sandbox Code Playgroud)
现在假设我们有一个通配符参数化类型FooBarContainer:
FooBarContainer<?> unknownFooBarContainer = ...;
Run Code Online (Sandbox Code Playgroud)
我们被允许unknownFooBarContainer进入unwrap.这表明我之前的断言是错误的,因为调用站点不知道是什么T- 只是它在边界内是某种类型extends Foo & Bar.
FooBarContainer.unwrap(unknownFooBarContainer); // T is a wildcard capture, ?
Run Code Online (Sandbox Code Playgroud)
正如我所说,unwrap使用通配符调用是非法的:
FooBarContainer.<?>unwrap(unknownFooBarContainer); // compiler error
Run Code Online (Sandbox Code Playgroud)
我只能猜测这是因为通配符捕获永远不能相互匹配 - ?在调用站点提供的参数是模糊的,没有办法说它应该特别匹配类型中的通配符unknownFooBarContainer.
所以,这是OP询问的语法的用例.调用unwrap的unknownFooBarContainer返回类型的引用? extends Foo & Bar.我们可以将该引用分配给Fooor Bar,但不是两者:
Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = FooBarContainer.unwrap(unknownFooBarContainer);
Run Code Online (Sandbox Code Playgroud)
如果由于某种原因unwrap是昂贵的,我们只想打电话一次,我们将被迫施放:
Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = (Bar)foo;
Run Code Online (Sandbox Code Playgroud)
所以这就是假设语法会派上用场的地方:
Foo&Bar fooBar = FooBarContainer.unwrap(unknownFooBarContainer);
Run Code Online (Sandbox Code Playgroud)
这只是一个相当模糊的用例.对于允许这样的语法,包括好的和坏的,都会有非常广泛的影响.它会在不需要的地方打开滥用的空间,并且完全可以理解为什么语言设计者没有实现这样的事情.但我仍然认为考虑这个问题很有意思.
(主要针对@MiserableVariable)以下是一个不安全的方法如何getFooBar导致堆污染及其含义的演练.给出以下接口和实现:
interface Foo { }
static class Foo1 implements Foo {
public void foo1Method() { }
}
static class Foo2 implements Foo { }
Run Code Online (Sandbox Code Playgroud)
让我们实现一个不安全的方法getFoo,类似于getFooBar这个例子但是简化了:
@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
}
Run Code Online (Sandbox Code Playgroud)
这里,当new Foo2被T强制转换时,它被"取消选中",这意味着由于类型擦除,运行时不知道它应该失败,即使它应该在这种情况下T也是如此Foo1.相反,堆被"污染",这意味着引用指向它们不应该被允许的对象.
失败发生在方法返回后,当Foo2实例尝试分配给foo1具有可重新类型的引用时Foo1.
你可能在想,"好吧,它在通话网站上爆炸而不是方法,大不了." 但是当涉及更多仿制药时,它会变得更加复杂.例如:
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
}
Run Code Online (Sandbox Code Playgroud)
现在它不会在通话现场爆炸.当使用内容时,它会在一段时间后爆炸foo1List.这就是堆污染变得难以调试的原因,因为异常堆栈跟踪并不能指向实际问题.
当调用者处于泛型范围内时,它会变得更加复杂.想象一下,而不是得到一个List<Foo1>我们得到一个List<T>,把它放入一个Map<K, List<T>>并将其返回到另一个方法.你得到了我希望的想法.
在某些情况下,调用者可以在不知道具体类型的情况下使用返回值的被调用方法.甚至可能根本不存在这样的类型,它只是一个代理:
import java.lang.reflect.*;
interface Foo {}
interface Bar {}
class FooBar1 implements Foo, Bar {public String toString() { return "FooBar1"; }}
class FooBar2 implements Foo, Bar {public String toString() { return "FooBar2"; }}
class FooBar {
static <T extends Foo & Bar> T getFooBar1() { return (T) new FooBar1(); }
static <T extends Foo & Bar> T getFooBar2() { return (T) new FooBar2(); }
static <T extends Foo & Bar> T getFooBar() {
return (T)
Proxy.newProxyInstance(
Foo.class.getClassLoader(),
new Class[] { Foo.class, Bar.class },
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) {
return "PROXY!!!";}});
}
static <U extends Foo & Bar> void show(U u) { System.out.println(u); }
public static void main(String[] args) {
show(getFooBar1());
show(getFooBar2());
show(getFooBar());
}
}
Run Code Online (Sandbox Code Playgroud)
两者FooBar1并FooBar2实现Foo和Bar.在main,调用getFooBar1和getFooBar2可以分配给变量,虽然没有强有力的理由让它知道恕我直言.
但是getFooBar有趣的情况是使用代理.实际上,它可能是实现两个接口的对象的唯一实例.不同的方法(show此处)可以与类型更安全的方式使用临时,但如果没有FooBarWrapper问题中描述的黑客,则无法将其分配给变量.甚至不可能创建通用包装器,class Wrapper<T extends U & V>是不允许的.
唯一的麻烦似乎是定义语法,其他类型检查机制似乎已到位,至少在Oracle javac 1.7.0中.