当前位置: 首页 > 面试题库 >

如何引用具有多个界限的通用返回类型

郜玉石
2023-03-14
问题内容

我最近看到,可以声明一个返回类型,该返回类型也受接口限制。考虑以下类和接口:

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),但是仅当我在
Function 后面指向一个点时才这样:

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,并且由于类型擦除,因此唯一的选择是null通过进行未经检查的强制转换来返回或“说谎”,从而冒堆污染的风险。一个典型的解决方法是getFooBar采用一个Class<T>参数,或者FooFactory<T>例如。

更新资料

当我断言调用方getFooBar必须始终知道是什么时,事实证明我错了T。正如@MiserableVariable指出的那样,在某些情况下,泛型方法的类型参数被推断为
通配符捕获 ,而不是具体的类型或类型变量。

正如我们在评论中所讨论的,一个使用getFooBar混乱的示例,因为它不需要任何参数来推断T。某些编译器在无上下文调用时抛出错误,getFooBar()而其他编译器则对此表示满意。我
认为 不一致的编译错误以及调用FooBar.<?>getFooBar()是非法的事实证实了我的观点,但事实证明这些都是红色鲱鱼。

基于@MiserableVariable的答案,我整理了一个新示例,该示例使用带有参数的泛型方法来消除混乱。假设我们有接口FooBar和实施FooBarImpl

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

我们还有一个简单的容器类,用于包装实现Foo和的某种类型的实例Bar。它声明一个愚蠢的静态方法unwrap,采用aFooBarContainer并返回其引用:

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-只是它是边界内的某种类型extendsFoo & Bar

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

如前所述,unwrap使用通配符进行调用是非法的:

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

我只能猜测这是因为通配符捕获永远无法匹配-
?调用站点提供的参数是模棱两可的,没有办法说它应该与类型的通配符特别匹配unknownFooBarContainer

因此,这是OP询问的语法的用例。调用unwrapunknownFooBarContainer返回类型的引用? extends Foo & Bar。我们可以将该引用分配给FooBar,但不能两者都使用:

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);

这只是一个相当晦涩的用例。允许使用这种语法,无论好坏,都会有非常广泛的含义。这将在不需要的地方打开滥用的空间,而且为什么语言设计人员没有实现这种目的也是完全可以理解的。但是我仍然认为思考很有趣。

关于堆污染的注意事项

主要针对@MiserableVariable以下是不安全方法如何getFooBar导致堆污染及其含义的演练。给定以下接口和实现:

interface Foo { }

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

static class Foo2 implements Foo { }

让我们实现一个unsafe方法getFoogetFooBar该示例类似于但已简化:

@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,它是“未登记”,因为类型擦除意味着运行时不知道它应该会失败,即使它应该在这种情况下,由于TFoo1。相反,堆是“污染的”,这意味着引用指向不应该允许的对象。

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
}

现在,它不会在呼叫站点爆炸。当foo1Listget
的内容被使用时,它会炸毁。这就是堆污染变得更难以调试的方式,因为异常stacktrace不会将您引向实际的问题。

当调用者本身在通用范围内时,它变得更加复杂。想象一下,而不是List<Foo1>得到a List<T>,而是将其放入a Map<K,List<T>>并将其返回另一种方法。你明白我的想法。



 类似资料:
  • 我试图用Java中的一个通用返回类型来实现< code >命令模式。 在SO上查看了这个答案后,我创建了一个类(接口),如下所示: 然而,Eclipse一直抱怨: SearchResultsPage 类型的方法 execute(ArrayList, T) 必须重写或实现超类型方法 但当我点击 在超类型命令中创建 execute() Eclipse自动生成方法<code>T execute(Arra

  • 问题内容: 假设我有一个超类,它定义了以下抽象方法 现在,如果我想在某些子类中覆盖它 我收到有关类型安全和未经检查的转换的警告: 类型安全:返回类型为从类型需要选中转换,以符合从类型 没有下摔倒,如果?有什么办法可以适当消除警告吗? 问题答案: 重写方法的返回类型必须是重写方法的返回类型的子类型。 不是where 的子类型。T在这里未知。 是每个子类型化规则的的子类型。 有关通配符的一些子类型化规

  • 这个问题与这个问题很接近,但有一个主要区别。 可能的要求: (1) 我想生成一个带有通用返回值的Java函数。 (2)输入参数列表总是相同的。( (3) 函数应知道预期的返回参数类型。 我的尝试: 因为没有生成的实例,所以它不起作用。尝试使用

  • 问题内容: 按引用返回的切片为空: 如何通过引用从函数返回切片? 问题答案: 通过分配给,您可以更改指向的位置,而不是指向的值。要做后者,而不是write 。

  • 为了理解TypeScript的精神,我在我的组件和服务中编写了全类型签名,这扩展到了angular2表单的自定义验证函数。 我知道我可以重载函数签名,但这需要每个返回类型的参数不同,因为将每个签名编译为单独的函数: 我还知道我可以返回单个类型(如Promise),它本身可以是多个子类型: 但是,在angular2自定义表单验证器的上下文中,单个签名(一个类型为的参数)可以返回两种不同的类型:带有表

  • 问题内容: 我在很多地方都读过,包括在方法返回类型中使用有界通配符是一个坏主意。但是,我在课堂上找不到避免这种情况的方法。我想念什么吗? 情况看起来像这样: 总而言之,这是一个我希望能够 使用 任何与英语有所不同的出版物的类。该类需要允许从外部访问发布,但理想情况下,的调用者不希望将结果用作有界通配符。他们会很高兴的。 有办法解决吗? 问题答案: 有界通配符具有传染性,这就是您链接到的页面似乎在哀