我最近看到可以声明一个也受接口限制的返回类型。考虑以下类和接口:
public class Foo {
public String getFoo() { ... }
}
public interface Bar {
public void setBar(String bar);
}
我可以声明一个这样的返回类型:
public class FooBar {
public static <T extends Foo & Bar> T getFooBar() {
//some implementation that returns a Foo object,
//which is forced to implement Bar
}
}
如果我从某个地方调用该方法,我的IDE告诉我返回类型的方法是String getFoo()
以及setBar(String)
,但仅限于如果我在函数后面指出一个点,如下所示:
FooBar.getFooBar(). // here the IDE is showing the available methods.
有没有办法获得对这样一个对象的引用?我的意思是,如果我做这样的事情:
//bar only has the method setBar(String)
Bar bar = FooBar.getFooBar();
//foo only has the getFoo():String method
Foo foo = FooBar.getFooBar();
我想有这样的参考(伪代码):
<T extents Foo & Bar> fooBar = FooBar.getFooBar();
//or maybe
$1Bar bar = FooBar.getFooBar();
//or else maybe
Foo&Bar bar = FooBar.getFooBar();
这在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();
}
}
Java真的发明了这么好的功能,但是忘记了想要引用它吗?
虽然泛型方法的类型参数可以通过边界限制,例如extends Foo & Bar
,但它们最终由调用者决定。当您致电getFooBar()
时,呼叫站点已经知道T
正在解决的问题。通常,这些类型参数将由编译器推断,这就是您通常不需要指定它们的原因,如下所示:
FooBar.<FooAndBar>getFooBar();
但即使T
被推断为FooAndBar
,这真的发生在幕后。
那么,要回答你的问题,这样的语法如下:
Foo&Bar bothFooAndBar = FooBar.getFooBar();
在实践中永远不会有用。原因是调用者必须已经知道T
是什么。要么T
是一些具体的类型:
FooAndBar bothFooAndBar = FooBar.<FooAndBar>getFooBar(); // T is FooAndBar
或者,T
是一个未解析的类型参数,我们在其范围内:
<U extends Foo & Bar> void someGenericMethod() {
U bothFooAndBar = FooBar.<U>getFooBar(); // T is U
}
另一个例子:
class SomeGenericClass<V extends Foo & Bar> {
void someMethod() {
V bothFooAndBar = FooBar.<V>getFooBar(); // T is V
}
}
从技术上讲,这就包含了答案。但我还想指出你的示例方法getFooBar
本质上是不安全的。请记住,调用者决定T
是什么,而不是方法。由于getFooBar
没有采取任何与T
相关的参数,并且由于type erasure,它唯一的选择是返回null
或通过制作一个未经检查的演员来“谎言”,冒险heap pollution。一个典型的解决方法是getFooBar
采取Class<T>
论证,或者例如FooFactory<T>
。
当我断言getFooBar
的调用者必须总是知道T
是什么时,我发现我错了。正如@MiserableVariable指出的那样,在某些情况下,泛型方法的type参数被推断为通配符捕获,而不是具体的类型或类型变量。有关his answer实现的一个很好的例子,请参阅getFooBar
,该实现使用代理来推动他的观点,即T
未知。
正如我们在评论中所讨论的那样,an example using getFooBar
造成了混乱,因为从中推断T
没有任何论据。某些编译器throw an error对getFooBar()
的无环境调用,而其他人are fine with it。我认为不一致的编译错误 - 以及调用FooBar.<?>getFooBar()
非法的事实 - 验证了我的观点,但这些结果证明是红色的鲱鱼。
根据@ MiserableVariable的答案,我将使用泛型方法和参数的an new example放在一起,以消除混淆。假设我们有接口Foo
和Bar
以及一个实现FooBarImpl
:
interface Foo { }
interface Bar { }
static class FooBarImpl implements Foo, Bar { }
我们还有一个简单的容器类,它包含了一个实现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();
}
}
现在让我们说我们有一个通配符参数化类型的FooBarContainer
:
FooBarContainer<?> unknownFooBarContainer = ...;
我们被允许将unknownFooBarContainer
传递给unwrap
。这表明我之前的断言是错误的,因为调用网站不知道T
是什么 - 只是它是在界限extends Foo & Bar
内的某种类型。
FooBarContainer.unwrap(unknownFooBarContainer); // T is a wildcard capture, ?
正如我所说,使用通配符调用unwrap
是非法的:
FooBarContainer.<?>unwrap(unknownFooBarContainer); // compiler error
我只能猜测这是因为通配符捕获永远不能相互匹配 - 在调用站点提供的?
参数是模糊的,没有办法说它应该特别匹配unknownFooBarContainer
类型的通配符。
所以,这是OP询问的语法的用例。在unwrap
上调用unknownFooBarContainer
会返回? extends Foo & Bar
类型的引用。我们可以将该引用分配给Foo
或Bar
,但不能同时分配两者:
Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = FooBarContainer.unwrap(unknownFooBarContainer);
如果由于某种原因unwrap
价格昂贵而我们只想打电话一次,我们将被迫施展:
Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = (Bar)foo;
所以这就是假设语法会派上用场的地方:
Foo&Bar fooBar = FooBarContainer.unwrap(unknownFooBarContainer);
这只是一个相当模糊的用例。对于允许这样的语法,包括好的和坏的,都会有非常广泛的影响。它会在不需要的地方打开滥用的空间,并且完全可以理解为什么语言设计者没有实现这样的事情。但我仍然认为考虑这个问题很有意思。
(Mostly for @MiserableVariable)以下是getFooBar
等不安全方法如何导致堆污染及其影响的演练。给出以下接口和实现:
interface Foo { }
static class Foo1 implements Foo {
public void foo1Method() { }
}
static class Foo2 implements Foo { }
让我们实现一个不安全的方法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
}
在这里,当新的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
}
。
你可能在想,“好吧,它在通话网站上爆炸而不是方法,大不了。”但是当涉及更多仿制药时,它会变得更加复杂。例如:
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());
}
}
并将其返回到另一种方法。你得到了我希望的想法。
在某些情况下,调用者可以在不知道具体类型的情况下使用返回值的被调用方法。甚至可能根本不存在这样的类型,它只是一个代理:
FooBar1
FooBar2
和Foo
都实施Bar
和main
。在getFooBar1
中,对getFooBar2
和getFooBar
的调用可以分配给一个变量,尽管没有强有力的理由让它知道恕我直言。
但show
是一个有趣的案例,它使用代理。实际上,它可能是实现两个接口的对象的唯一实例。一种不同的方法(这里是FooBarWrapper
)可以与类型更安全的方式使用临时,但如果没有问题中描述的class Wrapper<T extends U & V>
hack,则无法将其分配给变量。甚至不可能创建通用包装器,不允许使用public interface CoordinateView extends View
{
Coordinate getCoordinate();
//Maybe more stuff
}
。
唯一的麻烦似乎是定义语法,其他类型检查机制似乎已到位,至少在Oracle javac 1.7.0中。
就像@Paul Bellora在他的回答中提到的那样,类型由调用者解决,因为它现在基本上它将调用它。我想在一个用例中添加他的答案,我认为语法的使用可能是有益的。
总有一些替代方法可以避免使用这种语法。我想不出一个完全必要的例子。但是,我可以想到一个特定情况的用例,这种语法可以方便地使用,虽然我自己甚至没有使用它。我知道它不是最好的例子,但它可以达到目的。
最近我一直在开发用户界面。在这个应用程序中,我使用库来管理我的GUI元素。除了库的功能之外,我还创建了一个自定义界面,在我的应用程序中定义了一个View,它具有特定类型数据的输入,比如输入坐标。该界面看起来像:
public MyWindow extends Window implements CoordinateView, OtherInterface
{
private Button submitButton;
public MyWindow()
{
super();
//Create all the elements
submitButton.addClickHandler(
new ClickHandler()
{
@Override
onCLick(ClickEvent e)
{
getModel().add(getCoordinate());
destroy();
}
});
}
}
我的应用程序中有几个实现此接口的窗口。现在让我们说,由于某种原因,我想在模型中存储在窗口中提交的最后一个坐标,然后关闭窗口。为此,我可以将一个处理程序附加到提交表单的窗口按钮,当用户关闭Window时,处理程序将被触发。我可以通过在每个窗口中匿名添加处理程序来实现这一点,例如:
public class MyController <T extends Window & CoordinateView> implements ClickHandler
{
private T windowWithCoordinates;
public MyController (T window)
{
windowWithCoordinates = window;
}
@Override
onClick(ClickEvent e)
{
getModel().add(windowWithCoordinates.getCoordinate());
windowWithCoordinate.destroy();
}
}
但是,这种设计对我来说并不理想,它不够模块化。考虑到我有相当数量的窗口具有这种行为,改变它可能会变得相当乏味。所以我宁愿在类中提取匿名方法,以便更容易更改和维护。但问题是destroy()方法没有在任何接口中定义,只是窗口的一部分,而getCoordinate()方法是在我定义的接口中定义的。
在这种情况下,我可以使用多个边界,如下所示:
public MyWindow extends Window implements CoordinateView, OtherInterface
{
private Button submitButton;
public MyWindow()
{
super();
//Create all the elements
submitButton.addClickHandler(new MyController<MyWindow>(this));
}
}
那么windows中的代码现在将是:
CoordinateView
请注意,行为将保持不变,代码只是一种粘性。它只是更模块化,但它不需要创建一个额外的接口,以便能够正确提取它。
或者,我可以定义一个扩展public interface CoordinateWindow extends CoordinateView
{
void destroy();
}
的附加接口,并定义一个关闭窗口的方法。
public class MyController implements ClickHandler
{
private CoordinateWindow windowWithCoordinates;
public MyController (CoordinateWindow window)
{
windowWithCoordinates = window;
}
@Override
onClick(ClickEvent e)
{
getModel().add(windowWithCoordinates.getCoordinate());
windowWithCoordinate.destroy();
}
}
public MyWindow extends Window implements CoordinateWindow
{
private Button submitButton;
public MyWindow()
{
super();
//Create all the elements
submitButton.addClickHandler(new MyController(this));
}
@Override
void destroy()
{
this.destroy();
}
}
让窗口实现这个更具体的接口,而不是在提取的控制器中不必要地使用泛型参数:
public class Foo
{
public String getFoo() { return ""; } // must have a body
}
public interface Bar // no ()
{
public void setBar(String bar);
}
public class FooBar<T>
{
public static <T extends Foo & Bar> T getFooBar()
{
return null;
}
}
public class FB
{
private FooBar<Object> fb = new FooBar<Object>();
public static void main(String args[])
{
new FB();
}
public FB()
{
System.out.println(fb.getFooBar());
}
}
FB.java:12: type parameters of <T>T cannot be determined; no unique maximal instance exists for type variable T with upper bounds java.lang.Object,Foo,Bar
System.out.println(fb.getFooBar());
^
1 error
对于某些人来说,这种方法可以被视为比以前更清晰,甚至更可重复使用,因为现在可以将其添加到指定层次结构之外的其他“窗口”。就个人而言,我也更喜欢这种方法。然而,它可能导致更多的编码,因为必须仅为了获得对期望方法的访问而定义新接口。
总而言之,虽然我个人不推荐它,但我认为使用具有多个边界的泛型类型可以帮助耦合定义,同时减少代码量。
不确定Eclipse为你做了什么,但上面的大部分代码都没有接近编译....
我做了相应的更改以尽可能地编译它,这是我得到的:
qazxswpoi