【问题标题】:Java Generics: Use generic class as type parameter of another generic classJava 泛型:使用泛型类作为另一个泛型类的类型参数
【发布时间】:2020-08-07 11:49:40
【问题描述】:

在 Java 中有什么方法可以使用一个泛型类作为另一个泛型类的类型参数?

例如,我有MessageMessageBus这样的:

public interface Message<V> {};

public interface MessageBus {
    <V> V execute(Message<V> message);
}

现在效果很好。

然后,我得到 CommandCommandBus 像这样:

public interface Command<V> {};

public interface CommandBus {
    <V> V execute(Command<V> command);
}

它也很好用。

但是,CommandBus 的实现与MessageBus 几乎相同。 我想将MessageBus 定义为这样的泛型类:

public interface Message<V> {};
public interface MessageBus<T extends Message> {};
public interface Command<V> extends Message<V> {};
public interface CommandBus extends MessageBus<Command> {};

看起来还不错,但是当我想定义方法execute时出现问题:

public interface MessageBus<T extends Message> {
    <V> V execute(T<V> message);       // compiles error, T<V> is invalid
    <V> V execute(T message);          // T means Message<Object>, not Message<V>
    <V> V execute(Message<V> message); // CommandBus cannot override Message<V> to Command<V> in subclass
}

请注意,V 在每次调用时都是不同的类型,所以我不能像这样定义MessageBus

public interface MessageBus<V, T extends Message<V>> {
    /**
     * In this way, each bus can have only one fixed return type.
     * But different return type is expected on different message,
     * just like the first interface above.
     */
    V execute(T message);
}

我想这样使用它们:

public class Command1 implements Command<Integer> {};
public class Command2 implements Command<String> {};
public class Command3 implements Command<String> {};
public class Message1 implements Message<String> {};

CommandBus commandBus;
Integer v1 = commandBus.execute(new Command1());  // fine
String v2 = commandBus.execute(new Command2());   // fine
String v3 = commandBus.execute(new Command3());   // fine
String v4 = commandBus.execute(new Command1());   // should compile error, return type mismatch
String v5 = commandBus.execute(new Message1());   // should compile error, only support Command

我会有各种MessageBus,比如CommandBus extends MessageBus&lt;Command&gt;QueryBus extends MessageBus&lt;Query&gt;EventBus extends MessageBus&lt;Event&gt;等等。

我将如何实现这种行为?

谢谢。

【问题讨论】:

  • Message&lt;T&gt; 中的T 突然不见了?这就是前面代码中返回的内容。
  • @daniu 这就是问题所在:返回类型V 应该是方法execute 上的泛型类型,而不是类MessageBus 上的泛型类型。当Message 转到MessageBus 的类型参数时,V 不能去那里。 (我编辑了问题并将Message&lt;T&gt;重命名为Message&lt;V&gt;,因为它与MessageBus&lt;T extends Message&gt;中的T不同)

标签: java generics


【解决方案1】:

您从Message 中丢失了T,这是原始返回类型。

public interface Message<T> {};
public interface MessageBus<T> {};
public interface Command<T> extends Message<T> {};
public interface CommandBus<T> extends MessageBus<T> {};

现在你的课程会是这样的

class MessageBus<String> {
    String execute(Message<String> m) { ... }
}
class CommandBus<Integer> {
    String execute(Message<Integer> m) { ... }
    Integer execute(Command<Integer> c) { ... }
}

【讨论】:

  • 注意每个Message都有自己的返回类型,MessageBus.execute返回类型T应该随着Message而改变,所以不能作为MessageBus的类型参数使用。
  • “MessageBus.execute 返回类型 T 应该随 Message 改变”是什么意思?
  • 大牛?您将 LiuSXwww 原来的 interfaces 中的 MessageBusCommandBus 更改为具体的 classes 的理由是什么我>?
  • ...你是什么意思“MessageBus.execute 返回类型 T 应该随 Message 改变”?...“——我不是 OP。但我猜,大牛,他指的是你的String execute(Message&lt;Integer&gt; m) { ... }。给定方法的 m 参数的 Message&lt;Integer&gt; 参数化类型,则声明为 CommandBus.execute(Message&lt;Integer&gt;) 的方法应返回 CommandBus.execute(Message&lt;Integer&gt;) i>Integer。也就是说:execute()方法的返回类型应该和Message&lt;T&gt;被实例化的类型参数的类型相同。在您回答的那部分的实例中,该类型是 Integer
  • @daniu 对于同一实例CommandBuscommandBuscommandBus.execute(Command&lt;Integer&gt;) 应该返回一个IntegercommandBus.execute(Command&lt;String&gt;) 应该返回一个StringcommandBus.execute(Command&lt;V&gt;) 应该返回一个@ 987654344@.
【解决方案2】:

...我将如何实现此行为?...

Here's how I'd implement it

public interface MessageBus< S extends Foo< ? > > {

    < T extends Foo< U >, U > U execute( T message );
}

我介绍了您在此处看到的 Foo 界面,因为为了能够做您说您想做的事情 - 并在 类型安全 方式MessageCommand 需要相同类型 .

this representative example 的上下文中,Foo 承担了必要的共性。您可以将 Foo 重命名为在您的实际业务领域中更有意义的名称。

...我想像这样使用它们:“

   ...
   public class Command1 implements Command<Integer> {};
   public class Command2 implements Command<String> {};
   public class Command3 implements Command<String> {};
   public class Message1 implements Message<String> {};
   ...

my demo 我已经确认该解决方案可以像这样使用......

    CommandBus commandBus = new StandardCmdBus() ;
    
    CharSequence v0 = commandBus.execute(new CharSeqCommand());   
    
    Integer v1 = commandBus.execute(new IntegerCommand());    // fine
    
    String v2 = commandBus.execute(new StringCommand());      // fine    
    
    String v3 = commandBus.execute(new StringMessage());      // fine
    
    /*String v4 = commandBus.execute(new CharSeqCommand());*/ // error: incompatible types: inference variable U has incompatible bounds

我还介绍了具体的 MessagesCommands,这会打印到 stdout...

 Hail Bopp!
        666
Heal Setan!
Hell Santa!

【讨论】:

  • 快到了,但String v3 = commandBus.execute(new StringMessage()); 应该会编译错误。我找不到您的 CommandBusMessageBus 之间的任何区别。
  • 帮我一个忙?更详细地澄清为什么它“应该编译错误”?请?我的意思是特定情况下“应该编译错误”的条件是什么?从你原来的描述看不是很清楚。 TIA。
  • CommandBusMessageBus 之间的唯一区别是 S,但从未使用过。所以它们是一样的。
  • 我的描述中提到过:String v5 = commandBus.execute(new Message1()); // should compile error。命令总线应该只发送命令,不能发送其他消息。这就是我想使用泛型的原因。
  • 谢谢。在找到我在这里介绍的那个之前,我已经玩过两种或三种不同的排列。我的目标是得到与您编写的原始代码尽可能相似的东西。几乎可以肯定有一个不同的实现会在你想要的条件下“编译错误”。但我有理由确定该实现不会像我当前的实现那样与您的原始代码相似。如果我很快又有空闲时间,我可能会再试一次。再次感谢。顺便说一句,对于日常编码 kata 来说是个好问题 :)
【解决方案3】:

您已经知道,从您最初遇到的各种编译错误中,您的原始实现无法执行您现在已明确是您真正想要做的事情。在他/她的回答中,@daniu 暗示了一个原因。

您的原始代码想做something the language forbids。即:使用与超级签名完全不同的签名覆盖方法

因此,您不希望按照原始问题中的代码所示方式构建应用程序。要做到你所说的实际上想要做的事情,你的原始代码需要一些重要的重构。我的第一个答案中的代码也是如此。因此,这个单独的答案......

public interface MessageBus< V > { 
    
    V evaluate( Message<V> msg );
}

public interface CommandBus< V > { 
    
    V evaluate( Command<V> msg );
}

public interface EventBus< V > { 
    
    V evaluate( Event<V> msg );
}
...
public interface Message < V >{ 
    
    V evaluate( );
}
public interface Command< V > extends Message< V >{ 
    
    V evaluate( );
}
...
public class StandardCmdBus { 
    
    public static <S, T extends Command<S>> S evaluate( T cmd ){ return cmd.evaluate();  }
}

I demonstrate how that could be actually used 像这样……

public static void main(String args[]) {
  
    Stream< Command< ? > > shellCmds = of( () -> 3.14, () -> "javac", () -> "gcc", () -> 42.424242D, () -> "grep", () -> 666);
    
    Stream< Command< ? > > robotCmds = of( () -> "Assemble 666,666,666,666 Cars!", () -> "Exterminate!", () -> "Disallow: /questions/63242515/", () -> 777.9311, () -> "Rescue Will Robinson!", () -> "Kill all humans!", () -> 666);
    
    Stream< Message< ? > > msgs = of( () -> "What hath God wrought?", () -> "Sending out an S.O.S...", () -> "I like apples...", () -> 666, () -> "Friends of space, how are you all? Have you eaten yet? Come visit us if you have time.", () -> "?.?.?", () -> "The answer is...", () -> 42);

    Stream< Event< ? > > evts = of( () -> "The Big Bang", () -> "First Contact", () -> 867.5309, () -> "The Moon Landing", () -> "onLoad()", () -> 666, () -> "The Rapture" );
    
    Stream< Query< ? > > queries = of( () -> "The Ultimate Query...", () -> 42 );
    
    CommandBus< ? > cmdBus = ( cmd ) -> cmd.evaluate( );
    
    MessageBus< ? > msgBus = ( msg ) -> msg.evaluate( );
    
    EventBus< ? > evtBus = ( evt ) -> evt.evaluate( );
    
    QueryBus< ? > qBus = ( q ) -> q.evaluate( );
    
    /* Totally type safe; no unchecked warnings (i.e. no casts invovled) */
    robotCmds.map( StandardCmdBus::evaluate ).forEach( out::println );
    
    /* Less type safe; But totally fine; „uses unchecked or unsafe opertions“ (i.e. casts invovled) */
    use( shellCmds, cmdBus::evaluate ); 
    
    use( msgs, msgBus::evaluate ); 
    
    use( evts, evtBus::evaluate );
    
    use( queries, qBus::evaluate );
    
    Message< String > wtf = ( ) -> "// should compile error";
    
    /* cmdBus.evaluate( wtf );*/ /* error: incompatible types: Message<String> cannot be converted to Command<CAP#1> */ 
    
}

…打印出来…

Assemble 666,666,666,666 Cars!
Exterminate!
Disallow: /questions/63242515/
777.9311
Rescue Will Robinson!
Kill all humans!
666
...
-----------

What hath God wrought?
Sending out an S.O.S...
I like apples...
666
Friends of space, how are you all? Have you eaten yet? Come visit us if you have time.
?.?.?
The answer is...
42        
...
-----------

The Ultimate Query...
42

...

您最初的实现有点过分热心地使用泛型。在某些时候,我们都为此感到内疚。它们是一种很酷的多态形式。但如果使用不当,它们会导致比解决的问题更多的问题。

【讨论】:

  • 您的界面与我描述的第一部分相似。我知道它运作良好。但是所有的公共汽车都是复杂且相似的。我不想一次又一次地重复自己。此外,CommandBus 是一个特殊的MessageBus。所以我想要继承和泛型,就像C++ 中的模板,或C 中的宏。但这在 Java 中似乎是不允许的。
  • 如果不需要返回值,可以使用public interface MessageBus&lt;T extends Message&gt; { void execute(T message);}public interface CommandBus&lt;T extends Command&gt; extends MessageBus&lt;T&gt; {}。这是合理的,并且在 Java 中是允许的。现在我希望总线返回一个值并保持类型安全。这显然是合理的,但在 Java 中是不允许的。
  • 我描述的重点是:我可以定义单独的类型安全总线,一次又一次地重复自己。我还可以让所有总线都继承MessageBus 或其他一些基类,不需要重复自己,但它不是类型安全的。现在我想现在如何定义类型安全总线,不再重复。
  • CommandBus 是一个特别的MessageBus“——你在 2 天前说过:„CommandBus几乎MessageBus“相同。 “IS ALMOST”关系不是“IS A”关系。你试过 MB{&lt;V&gt; V execute(Message&lt;V&gt; message);} &lt;- CB{&lt;V&gt; V execute(Command&lt;V&gt; command);}。但是that's illegal。这些方法有different signatures。要覆盖,方法参数必须完全相同。不是亚型。但是一模一样
  • 就像 C++ 中的模板“——Java 的泛型和 C++ 的模板之间的相似之处只是表面的。它们是根本不同的。 — “但它似乎在 Java 中是不允许的” — 你可能想要的是“Double Dispatch”。所以你是对的。 Java 不这样做 OOTB。您是否考虑过overloading 作为一个选项? @daniu 在您的 Q 的 cmets 中暗示了这一点。 Visitor Design Pattern 也可能适用。
【解决方案4】:

...CommandBus 是一个特殊的MessageBus...

考虑以下类结构。将 MessageBus 视为 AnimalCommandBus 视为 Cat@ 987654327@ 类似于 FoodCandy 类似于 Command

 +----------------+
 |     Animal     |
 +----------------+                +----------------+
 |eat(Food)       |                |      Food      |
 |                                 |                |
 |                |                +--------^-------+
 +-------^--------+                         |
         |                                  |
         |                      >-----------------------<
 +----------------+             |                       |
 |      Cat       |    +----------------+      +----------------+
 +----------------+    |      Candy     |      |      Grass     |
 |eat(Food)       |    |                |      |                |
 |eat(Candy)      |    +----------------+      +----------------+
 |eat(Grass)      |
 +----------------+



任何“Animal”绝对必须eat(Food)“。如果它不吃东西,那么它就是 NOT 一个 Animal。这就是继承的意义所在。

类似地:任何“ISMessageBus”绝对必须@987654336 @“。如果它不执行消息,那么它是 NOT 一个 MessageBus

要说:“命令总线应该只发送命令,没有其他消息”就像说:“Cat 应该只吃Candy,没有其他@987654340 @“。如果它不吃Food,那么它就是NOT一个Animal

当然,一个Cat 可以CandyGrass专业情况下。这不是 Cat's 默认行为。

„...我想要继承和泛型...

两者都可以。但我认为您对组合它们的期望不可能的。

如果您真的想要继承,那么您必须接受 Cat 必须 eat(Food)。如果您想要一个不 execute(Message)CommandBus,那么您想要继承。

当然,可以将 CommandBus 实现为 execute(Command)。但这必须在添加execute(Message)因为继承execute(Message)CommandBus 的默认行为。

【讨论】:

  • Animal&lt;T extends Food&gt; 表示Animal 必须 eat 某种Food,而不是所有类型的FoodCat extends Animal&lt;Candy&gt; 表示Cat 必须 eat(Candy),但NOT 其他Food。这在 Java 中是允许的,和现实世界一样:动物吃食物,但是有很多动物只吃肉,很多动物从不吃肉。
  • 关键点是:MessageBus&lt;T extends Message&gt;表示MessageBus 必须 execute(T),这个TMessage的一种特殊,不是所有的Message。所以CommandBus extends MessageBus&lt;Command&gt; 表示CommandBus 必须 execute(Command)NOT execute(Message)
  • 你可以试试:public class Message {}; public class Command extends Message {}; public class MessageBus&lt;T extends Message&gt; { public void execute(T message) {} }; public class CommandBus extends MessageBus&lt;Command&gt; {};。此CommandBus 可以 执行Command不能 其他Message。很明显,这个CommandBus还是一个特殊的MessageBus
  • 太棒了@LiuSXwww!你已经解决了你的问题!恭喜!如果您分享一个工作演示的链接,我将不胜感激。当我从错误中吸取教训时,我学到的东西最多。此外,您应该发布您的解决方案的解释。请?为了社区的利益,您可以发布自己问题的答案,以寻求类似的解决方案。我会完全赞成! TIA。
  • 抱歉,我的问题还没有解决。此解决方案不适用于Message&lt;V&gt;。我发布这个问题是为了找到使CommandBus extends MessageBus&lt;Command&gt;&lt;V&gt; V execute(Command&lt;V&gt;) 类型安全的解决方案。我可以轻松实现这两个目标中的任何一个,但不能同时实现这两个目标。
猜你喜欢
  • 1970-01-01
  • 2014-08-02
  • 2021-10-31
  • 1970-01-01
  • 2021-12-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多