如何在Java中使用同形方法实现接口?

13

在英语中,同形异义词指的是拼写相同但意思不同的两个单词。

在软件工程中,同形异义方法指的是具有相同名称但要求不同的两种方法。接下来通过一个人为例子来更加清晰地阐述这一问题:

interface I1 { 
    /** return 1 */ 
    int f()
}
interface I2 {
    /** return 2*/
    int f()
}
interface I12 extends I1, I2 {}

如何实现 I12 接口?C# 中有一种方式可以实现,但是 Java 没有。所以唯一的方法是通过一个 hack 来解决。如何通过反射/字节码技巧等方式 最可靠地 实现它(即它不必是完美的解决方案,我只需要最好的方案)?


请注意,某些现有的闭源大型遗留代码需要一个类型为 I12 的参数,并将 I12 委托给具有 I1 参数和 I2 参数的代码。因此,基本上我需要创建一个知道何时应该作为 I1 和何时应该作为 I2 行事的 I12 实例,这可以通过在运行时查看立即调用者的字节码来完成。我们可以假设调用者不使用反射,因为这是直接的代码。问题在于 I12 的作者没有预料到 Java 会合并两个接口中的 f,所以现在我必须想出一个最好的 hack 来解决问题。没有任何方法调用 I12.f(显然,如果作者编写了实际调用 I12.f 的代码,他在销售之前就会注意到这个问题)。

请注意,我实际上正在寻找答案,而不是如何重构我无法更改的代码。我正在寻找可能的最佳启发式或确切的解决方案(如果存在)。有关有效示例,请参见 Gray 的回答(我确信还有更健壮的解决方案)。


这里是两个接口中同名方法可能导致的问题的具体示例。这里是另一个具体示例:

我有以下 6 个简单的类/接口。它类似于一个围绕戏剧及其演员的业务。为简单起见并具体说明,让我们假设它们都是由不同的人创建的。

Set 表示集合,就像集合论中一样:

interface Set {
    /** Complements this set,
        i.e: all elements in the set are removed,
        and all other elements in the universe are added. */
    public void complement();
    /** Remove an arbitrary element from the set */
    public void remove();
    public boolean empty();
}

HRDepartment 使用 Set 来表示员工。它使用复杂的过程来解码哪些员工应该被录用或解雇:

import java.util.Random;
class HRDepartment {
    private Random random = new Random();
    private Set employees;

    public HRDepartment(Set employees) {
        this.employees = employees;
    }

    public void doHiringAndLayingoffProcess() {
        if (random.nextBoolean())
            employees.complement();
        else
            employees.remove();
        if (employees.empty())
            employees.complement();
    }
}

Set中的员工集合可能是已经向雇主申请的员工。因此,当在该集合上调用complement时,所有现有的员工都将被解雇,所有之前申请过的其他人将被雇佣。

Artist代表艺术家,例如音乐家或演员。艺术家有自我意识。当他得到他人的赞美时,这种自我意识会增强:

interface Artist {
    /** Complements the artist. Increases ego. */
    public void complement();
    public int getEgo();
}

剧场让一个艺术家表演,这可能会导致艺术家受到赞扬。观众可以在演出之间评判艺术家。表演者的自我意识越高,观众就越喜欢艺术家,但如果自我意识超过一定程度,观众就会对艺术家产生负面看法:

import java.util.Random;
public class Theater {
    private Artist artist;
    private Random random = new Random();

    public Theater(Artist artist) {
        this.artist = artist;
    }
    public void perform() {
        if (random.nextBoolean())
            artist.complement();
    }
    public boolean judge() {
        int ego = artist.getEgo();
        if (ego > 10)
            return false;
        return (ego - random.nextInt(15) > 0);
    }
}

ArtistSet 就是一个 Artist 和一个 Set

/** A set of associated artists, e.g: a band. */
interface ArtistSet extends Set, Artist {
}

TheaterManager运营演出。如果剧院的观众对艺术家的评价是负面的,剧院将与人力资源部交涉,随后将解雇艺术家,招聘新人等等:

class TheaterManager {
    private Theater theater;
    private HRDepartment hr;

    public TheaterManager(ArtistSet artists) {
        this.theater = new Theater(artists);
        this.hr = new HRDepartment(artists);
    }

    public void runShow() {
        theater.perform();
        if (!theater.judge()) {
            hr.doHiringAndLayingoffProcess();
        }
    }
}

一旦你尝试实现一个ArtistSet,问题就变得清晰了:两个超接口都指定了complement应该做其他事情,因此你必须在同一个类中以某种方式实现具有相同签名的两个complement方法。 Artist.complementSet.complement的同形异义词。


1
可能是 https://dev59.com/1XE85IYBdhLWcg3wYicB 的重复问题。 - Harshal Pandya
1
@Dog 应该实现 ArtistSet,ArtistSet 听起来只是一个艺术家的集合,它本身不是一个艺术家。 - Dev Blanked
3
很遗憾,Artist接口的方法使用了错误的单词,应该是拼写错误了。Artist的方法应该是compliment(),其中有一个字母"i"。实际上,这两个complement方法的含义对应于英语中的_同音异义词_,即Set中的"complement"和Artist中的"compliment"。 - rgettman
1
所描述的问题无法解决。要么问题太假设,要么描述有误。 - Old Pro
3
在Java语言定义中找到错误,询问如何解决。然而,线程被粉丝们垃圾信息轰炸,声称这个错误并不重要。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
显示剩余26条评论
7个回答

4
新的想法,有点乱...
public class MyArtistSet implements ArtistSet {

    public void complement() {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();

        // the last element in stackTraceElements is the least recent method invocation
        // so we want the one near the top, probably index 1, but you might have to play
        // with it to figure it out: could do something like this

        boolean callCameFromHR = false;
        boolean callCameFromTheatre = false;

        for(int i = 0; i < 3; i++) {
           if(stackTraceElements[i].getClassName().contains("Theatre")) {
               callCameFromTheatre = true;
           }
           if(stackTraceElements[i].getClassName().contains("HRDepartment")) {
               callCameFromHR = true;
           }
        }

        if(callCameFromHR && callCameFromTheatre) {
            // problem
        }
        else if(callCameFromHR) {
            // respond one way
        }
        else if(callCameFromTheatre) {
            // respond another way
        }
        else {
            // it didn't come from either
        }
    }
}

1
@SirKnigget:到目前为止,这是唯一真正回答问题的答案。供应商把我置于这种境地,现在我必须以最健壮的方式解决它。不过,我认为可能有比这更可靠的方法。 - Dog
Glen Best的回答似乎涉及到问题的核心——设计本身和对面向对象编程的理解。我并没有说你的回答在技术上不可行——我是说它不适用于任何可发布和可维护的产品。 - SirKnigget
1
我知道有一个复合对象,但是如果它来自于一个Theatre和另一个HRDepartment,那么它不允许以一种方式响应complement方法,这似乎是OP所要求的。 - Gray Kemmey
一个勇敢的尝试!但是你需要调用类中包含字符串"Theatre"或"HDRDepartment"吗?那就超出了"有点混乱"。那是一颗定时炸弹。你不能认真地将其编写成代码并交付给客户。 - Glen Best
5
幸运的是,他说供应商代码(即确保调用类具有这些字符串的代码)无法更改。看,我已经承认过不止一次了,它很混乱,但OP要求绕过Java的某个功能。所以,是的,我给他一个hack/混乱/危险的解决方案,但至少这是一个真正的解决方案。所以让我们停止对我的答案为什么不正确而做道德讲演。每个人,包括我在内,都知道缺点。 - Gray Kemmey
显示剩余7条评论

3

如何解决您特定的情况

ArtistSet只是一个艺术家和一组:

 /** A set of associated artists, e.g: a band. */
 interface ArtistSet extends Set, Artist { }

从面向对象的角度来看,这不是一个有用的声明。艺术家是名词的一种,是一个具有定义属性和操作(方法)的“事物”。

集合是物品的聚合体 - 独特元素的集合。相反,可以尝试使用:

ArtistSet 只是由艺术家组成的集合。

 /** A set of associated artists, e.g: a band. */
 interface ArtistSet extends Set<Artist> { };

然后,针对你的特定情况,同音方法位于从未组合在一个类型中的接口上,因此你没有冲突并且可以编写程序......

此外,您不需要声明 ArtistSet,因为您实际上没有使用任何新声明来扩展 Set。您只是实例化了一个类型参数,因此可以将所有用法替换为 Set<Artist>

如何解决更一般的情况

对于这种冲突,方法名称甚至不需要在英语语言意义上是同音的——它们可以是相同的单词,具有相同的英语含义,在java中用于不同的上下文。如果您希望将两个接口应用于类型,但它们包含具有冲突语义/处理定义的相同声明(例如方法签名),则会发生冲突。

Java不允许您实现所请求的行为,您必须使用替代解决方案。 Java不允许类从多个不同的接口提供相同方法签名的多个实现(使用某种形式的限定符/别名/注释来区分)。请参见Java overriding two interfaces, clash of method names,Java - Method name collision in interface implementation

例如,如果您有以下内容

 interface TV {
     void switchOn();
     void switchOff();
     void changeChannel(int ChannelNumber);
 }

 interface Video {
     void switchOn();
     void switchOff();
     void eject();
     void play();
     void stop();
 }

如果您有一个同时拥有这两个特性的对象,您可以将它们结合在一个新接口(可选)或类型中:

interface TVVideo {
     TV getTv();
     Video getVideo();
}


class TVVideoImpl implements TVVideo {
     TV tv;
     Video video;

     public TVVideoImpl() {
         tv = new SomeTVImpl(....);
         video = new SomeVideoImpl(....);
     }

     TV getTv() { return tv };
     Video getVideo() { return video };
}

你是在说Java中不应该使用多个接口吗? - Dog
不应该让事情变得更加清晰——如果一个类具有多个可以被其他类独立使用的行为,则使用多个接口是一件好事。通常,类组合是将其他类的多个行为聚合到一起的有用工具。在这里,接口组合对于避免Q中的方法签名冲突问题非常有用。 - Glen Best
问题在于编写ArtistSet的供应商没有注意到两个超级接口的冲突,因此我需要找到最可靠的解决方法。你是说供应商之所以做错了Java,是因为他没有每次创建接口/类时都检查所有超级接口/超级类是否存在冲突? - Dog
"ArtistSet" 实现 Artist 和 Set 没有意义 - 请参见 A 部分。如果来自供应商的两个接口是独立的,例如电视和视频,则可以创建自己的组合接口和组合类,并协调对单独供应商类的调用。如果供应商接口和类是相互依赖的,则必须遵循供应商预集成的设计。干杯 :) - Glen Best
你说存在多个接口的有效用例。那么很明显,作者有可能意外地引入了具有同音方法的两个超级接口。问题是如何解决代码中出现这种错误的情况。 - Dog
显示剩余3条评论

3
尽管Gray Kemmey做出了勇敢的尝试,但我认为根据您陈述的问题,该问题无法解决。通常情况下,给定一个ArtistSet,你无法知道调用它的代码是期望一个Artist还是一个Set
此外,即使您可以这样做,根据您对其他答案的评论,您实际上需要将ArtistSet传递给供应商提供的函数,这意味着该函数没有给编译器或人类任何提示,即它究竟要求什么。你完全没有任何技术上正确的答案。
作为完成工作的实际编程问题,我会按照以下顺序执行:
1. 向创建需要ArtistSet接口和生成ArtistSet界面本身的人员报告错误。 2. 给提供需要ArtistSet的函数的供应商提交支持请求,并问他们期望complement()的行为是什么。 3. 实现complement()函数以抛出异常。
public class Sybil implements ArtistSet {
  public void complement() { 
    throw new UnsupportedOperationException('What am I supposed to do'); 
  }
  ...
}

因为严肃地说,你不知道该怎么办。 当被这样叫唤时,正确的做法是什么(你如何确定)?

class TalentAgent {
    public void pr(ArtistSet artistsSet) {
      artistSet.complement();
    }
}

通过抛出异常,您有机会获得堆栈跟踪,从而了解调用者期望的是这两种行为中的哪一种。幸运的是,没有人调用该函数,这就是为什么供应商在存在此问题的情况下推出代码。运气不太好但还是有些的话,他们会处理异常。如果连那个都不行,那么至少现在您将拥有一个堆栈跟踪,可以回顾一下以确定调用者真正期望的是什么,可能会实现它(虽然我很担心通过这种方式来延续错误,但我已经在这篇其他答案中解释了我如何做到这一点)。

另外,对于其余的实现,我会通过构造函数将所有内容委托给实际的ArtistSet对象,以便稍后轻松地分离它们。


问题是如何解决这个问题。到目前为止,格雷的答案是最好的,但我相信还有更好的方法。1:供应商不会改变他们的代码。2:每个补充方法应该做什么已经很清楚了,问题是供应商不知道Java没有一种简单的方法来为两个接口实现它。3将使代码无法工作。 - Dog
@Dog 1:你怎么知道3会使代码无法工作?异常在什么情况下抛出?2:ArtistSet.complement()应该做什么非常不清楚。3:如果你神奇地为ArtistSet创建了完美的解决方案,但发现供应商的函数仍然有漏洞,你会怎么做?我会在实现不支持的操作异常后做完全相同的事情。如果有人抱怨我应该修复它,我会像你一样解释,这是供应商的代码,我无法更改。 - Old Pro
数字3会导致代码无法工作,因为当它尝试调用“complement”时,程序会崩溃。ArtistSet.complement应该会崩溃(但是通过“Artist”或“Set”引用调用时不会)。 - Dog

1
好的,经过大量研究,我有另一个想法来完全适应这种情况。由于您无法直接修改他们的代码...您可以强制自己进行修改。
免责声明:下面的示例代码非常简化。我的意图是展示如何完成此操作的一般方法,而不是生成可执行源代码(因为这本身就是一个项目)。
问题在于这些方法是同形异义词。因此,为了解决这个问题,我们只需重命名这些方法即可。很简单,对吧?我们可以使用仪器包 来实现这一点。正如您在链接的文档中看到的那样,它允许您创建一个“代理”,该代理可以直接修改类加载时或重新修改它们甚至已经被加载的类。
基本上,这需要您制作两个类:
- 预处理并重新加载类的代理类; 以及 - ClassFileTransformer实现,指定要进行的更改。
代理类必须定义premain()agentmain()方法之一,具体取决于您是希望它在JVM启动时开始处理还是在其已经运行后开始处理。此包文档中提供了示例。这些方法使您可以访问Instrumenation实例,从而允许您注册ClassFileTransformer。因此,它可能看起来像这样:

InterfaceFixAgent.java

public class InterfaceFixAgent {

    public static void premain(String agentArgs, Instrumentation inst) {

        //Register an ArtistTransformer
        inst.addTransformer(new ArtistTransformer());

        //In case the Artist interface or its subclasses 
        //have already been loaded by the JVM
        try {
            for(Class<?> clazz : inst.getAllLoadedClasses()) {
                if(Artist.class.isAssignableFrom(clazz)) {
                    inst.retransformClasses(clazz);
                }
            }
        }
        catch(UnmodifiableClassException e) {
            //TODO logging
            e.printStackTrace();
        }
    }
}

ArtistTransformer.java

public class ArtistTransformer implements ClassFileTransformer {

    private static final byte[] BYTES_TO_REPLACE = "complement".getBytes();
    private static final byte[] BYTES_TO_INSERT = "compliment".getBytes();

    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {

        if(Artist.class.isAssignableFrom(classBeingRedefined)) {
            //Loop through the classfileBuffer, find sequences of bytes
            //which match BYTES_TO_REPLACE, replace with BYTES_TO_INSERT
        }
        else return classfileBuffer;
    }

当然,这只是简化的。它将替换任何继承或实现Artist的类中的“complement”一词为“compliment”,因此您很可能需要进一步进行条件化(例如,如果Artist.class.isAssignableFrom(classBeingRedefined) && Set.class.isAssignableFrom(classBeingRedefined),则显然不希望将每个“complement”实例都替换为“compliment”,因为Set的“complement”是完全合法的)。
所以,现在我们已经更正了Artist接口及其实现。错别字消失了,方法有两个不同的名称,所以没有同音异义词。这使我们现在可以在CommunityTheatre类中拥有两个不同的实现,每个实现都将正确地实现/覆盖ArtistSet中的方法。
不幸的是,我们现在创建了另一个(可能更大的)问题。我们刚刚破坏了所有先前合法的实现Artist类的complement()引用。为了解决这个问题,我们需要创建另一个ClassFileTransformer,将这些调用替换为我们的新方法名称。
这有点困难,但并非不可能。新的ClassFileTransformer(我们称之为OldComplementTransformer)需要执行以下步骤:
  1. 找到与之前相同的字节串(表示旧方法名“complement”的字节串);
  2. 获取调用该方法的对象引用之前的字节;
  3. 将这些字节转换为一个Object
  4. 检查该Object是否是一个Artist;并且
  5. 如果是,则用新方法名替换这些字节。
一旦你完成了第二个转换器,就可以修改InterfaceFixAgent来适应它。(我还简化了retransformClasses()调用,因为在上面的示例中,我们在转换器本身内执行了所需的检查。) InterfaceFixAgent.java(已修改)
public class InterfaceFixAgent {

    public static void premain(String agentArgs, Instrumentation inst) {

        //Register our transformers
        inst.addTransformer(new ArtistTransformer());
        inst.addTransformer(new OldComplementTransformer());

        //Retransform the classes that have already been loaded
        try {
            inst.retransformClasses(inst.getAllLoadedClasses());
        }
        catch(UnmodifiableClassException e) {
            //TODO logging
            e.printStackTrace();
        }
    }
}

现在...我们的程序可以运行了。编写这个程序肯定不容易,并且进行QA和测试也将是非常困难的。但它肯定是强大的,而且解决了问题。(从技术上讲,我想它是通过删除问题来避免它,但是......我会接受我所能得到的。)
其他解决问题的方法:
- 不安全的API - 用C编写的native方法 这两种方法都允许您直接操作内存中的字节。当然可以设计一种解决方案,但我认为这将更加困难,也更不安全。所以我选择了上面的路线。
我认为这个解决方案甚至可以变得更加通用,成为一个非常有用的库,用于集成代码库。在变量、命令行参数或配置文件中指定需要重构的接口和方法,然后让它自由运行。这是一个在 Java 运行时协调冲突接口的库。(当然,如果每个人都在 Java 8 中修复了这个 bug,那么对大家来说会更好。)

除非存在您自己没有制作的“ArtistSet”实现,否则这应该适用于所有情况。但是可以合理地假设不存在现有的实现,因为除非他们像这样绕过了它,否则不可能实现。我唯一不确定的是,当某些字节码调用补集时,它是否捕获对象的类型?我猜是的。我会在有时间的时候尝试实现这个并看看我能走多远。此外,我想知道在运行时修改别人的代码是否是一个法律问题。到目前为止,这是最好的答案。 - Dog
1
@Dog 关于合法性,你提出了一个非常好的观点。你可能需要让你的供应商签署同意书。如果你在一家大公司工作,我相信法律部门不会感到高兴。 - asteri

1
如何实现具有同名方法的两个超级接口的类?
在Java中,一个类具有两个具有同名方法的超级接口被认为只有一个该方法的实现。(参见Java语言规范8.4.8节)。这使得类可以方便地从多个实现相同其他接口的接口继承,并且只需实现一次该函数。这也简化了语言,因为它消除了基于接口的同名方法来自哪个接口区分的语法和方法调度支持的需要。
因此,实现具有同名方法的两个超级接口的类的正确方法是提供一个满足两个超级接口合同的单个方法。
C#有一种方法可以做到这一点。在Java中如何实现?没有这样的构造吗?
C#以不同于Java的方式定义接口,因此具有Java没有的功能。
在Java中,该语言结构定义为意味着所有接口都获得同名方法的相同单个实现。 Java没有创建基于对象的编译时类的多重继承接口函数的替代行为的语言结构。这是Java语言设计者做出的有意选择。

如果不是这样,如何使用反射/字节码技巧等最可靠地完成?

使用反射/字节码技巧无法完成"它",因为决定选择哪个接口版本的同形方法所需的信息并不一定存在于Java源代码中。给定:

interface I1 { 
    // return ASCII character code of first character of String s 
    int f(String s); // f("Hello") returns 72
}
interface I2 {
    // return number of characters in String s 
    int f(String s);  // f("Hello") returns 5
}

interface I12 extends I1, I2 {}

public class C {
  public static int f1(I1 i, String s) { return i.f(s); }  // f1( i, "Hi") == 72
  public static int f2(I2 i, String s) { return i.f(s); }  // f2( i, "Hi") == 2
  public static int f12(I12 i, String s) { return i.f(s);} // f12(i, "Hi") == ???
}

根据Java语言规范,实现I12的类必须以这样的方式进行,即在使用相同参数调用C.f1()、C.f2()和C.f12()时返回完全相同的结果。如果C.f12(i,“Hello”)有时基于如何调用C.f12()返回72,有时返回5,那么这将是程序中的严重错误,并违反了语言规范。
此外,如果类C的作者希望从f12()获得某种一致的行为,则类C中没有字节码或其他信息表明它应该是I1.f(s)或I2.f(s)的行为。如果C.f12()的作者认为C.f("Hello")应该返回5或72,则无法从代码中看出。
好的,所以我通常不能使用字节码技巧为同音函数提供不同的行为,但我确实有一个像示例类TheaterManager的类。我该怎么做来实现ArtistSet.complement()?
你所问的 实际问题实际答案 是创建自己的替代实现 TheaterManager,不需要 ArtistSet。你不需要更改库的实现,而是需要编写自己的实现。
你所引用的实际答案 对于其他示例问题基本上是“将I12.f()委托给I2.f()”,因为没有一个函数接收I12对象然后将该对象传递给期望I1对象的函数。
Stack Overflow仅用于一般兴趣的问题和答案。

拒绝在此处提出问题的一个声明原因是,“它只与一个非常狭窄的情况相关,该情况不适用于互联网的全球受众。” 因为我们希望有所帮助,处理这种狭窄的问题的首选方法是修改问题,使其更具广泛适用性。对于这个问题,我采取了回答更广泛适用版本的方法,而不是实际编辑问题以删除使其独特的部分。

在商业编程的现实世界中,任何具有像I12这样破损接口的Java库都不会积累甚至数十个商业客户,除非可以通过以下一种方式之一实现I12.f():

  • 委托给I1.f()
  • 委托给I2.f()
  • 什么也不做
  • 抛出异常
  • 基于I12对象的某些成员变量的值,在每次调用时选择上述策略之一
如果有成千上万甚至只有一小部分公司在使用Java库中的这个部分,那么您可以确信他们已经使用了其中的某些解决方案。如果没有任何公司使用该库,则该问题对于Stack Overflow来说太狭窄了。
好的,“TheaterManager”是一个过度简化的例子。在实际情况下,我很难替换那个类,也不喜欢你提出的任何实用的解决方案。我能不能用高级JVM技巧来解决这个问题?
这取决于你想要解决什么问题。如果你想通过映射所有对"I12.f()"的调用并解析堆栈以确定调用者并选择基于此的行为来修复特定库,你可以通过"Thread.currentThread().getStackTrace()"访问堆栈。
如果你遇到一个你不认识的调用者,你可能会很难弄清楚他们想要哪个版本。例如,你可能会被从通用的地方调用(就像你给出的其他具体例子中的实际情况一样)。
public class TalentAgent<T extends Artist> {
  public static void butterUp(List<T> people) {
    for (T a: people) {
      a.complement()
    }
  }
}
 

在Java中,泛型被实现为擦除, 这意味着所有类型信息在编译时都被丢弃了。 TalentAgent<Artist>TalentAgent<Set>之间没有类或方法签名的区别,而people参数的形式类型只是List。 在调用者的类接口或方法签名中没有任何东西告诉您通过查看堆栈要做什么。

因此,您需要实现多种策略之一,其中之一是反编译调用方法的代码,寻找暗示调用者期望一个类还是另一个类。 它必须非常复杂,以涵盖所有这可能发生的方式,因为除其他事项外,您无法预先知道它实际上期望的是哪个类,只知道它期望实现其中一个接口的类。

有成熟和极其复杂的开源字节码实用程序,包括一个在运行时自动生成给定类的代理的实用程序(在Java语言支持该功能之前就已编写),因此,没有处理此情况的开源实用程序说明了追求此方法的努力与有用性的比率。


1
简而言之,OP问如何在没有hack的情况下最好地近似C#的功能,显然这不是一个可行的答案。"C#定义接口的方式与Java不同,因此具有Java没有的功能。"这是可笑的。显然,Java设计师犯了一个错误。将其称为设计的一部分最多是愚蠢的。这是一个经典的设计错误,导致了一种无法组合的语言。要避免同形方法问题的唯一方法是如果有一个全局中央接口存储库,并在其周围加锁(就像Unicode贡献者一样,但用于接口)。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
2
哦,等等,我错了。你不需要一个中央接口协会,你只需要确保每当你扩展多个接口时,它们是不相交的。但由于所有开发人员都像你一样认为每次扩展多个接口时检查不相交性是不切实际的,所以问题就会发生。"务实"并不可组合。你可以继续说这个问题不太可能发生,但它已经发生在OP身上了,所以滥用这些废话并没有真正帮助任何人。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
@Longpoke,OP问如何在Java中实现C#构造,而Java中没有此构造。事实上,Python、Ruby、Objective C、PHP或JavaScript也没有这个构造,它们与Java、C++和C#一起是当前用于生产商业软件的主要面向对象语言。换句话说,专业共识是C++和C#设计师犯了一个错误,允许使用这个构造,因为它会导致像OP这样的问题。无论如何,抱怨语言没有你想要的功能是浪费大家时间。OP应该使用C#。 - Old Pro
你只是重申了问题,要么回答它,要么离开。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
你说得没错,Java确实是C++的委婉说法,而C++则是C语言的更糟糕版本,只有那些把能够处理复杂事物与智力混为一谈的人才会使用它。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
显示剩余4条评论

0

这是我为消除歧义所做的事情:

interface Artist {
    void complement(); // [SIC] from OP, really "compliment"
    int getEgo();
}

interface Set {
    void complement(); // as in Set Theory
    void remove();
    boolean empty(); // [SIC] from OP, I prefer: isEmpty()
}

/**
 * This class is to represent a Set of Artists (as a group) -OR-
 * act like a single Artist (with some aggregate behavior).  I
 * choose to implement NEITHER interface so that a caller is
 * forced to designate, for any given operation, which type's
 * behavior is desired.
 */
class GroupOfArtists { // does NOT implement either

    private final Set setBehavior = new Set() {
        @Override public void remove() { /*...*/ }
        @Override public boolean empty() { return true; /* TODO */ }            
        @Override public void complement() {
            // implement Set-specific behavior
        }
    };

    private final Artist artistBehavior = new Artist() {
        @Override public int getEgo() { return Integer.MAX_VALUE; /* TODO */ }            
        @Override public void complement() {
            // implement Artist-specific behavior
        }
    };

    Set asSet() {
        return setBehavior;
    }

    Artist asArtist() {
        return artistBehavior;
    }
}

如果我将这个对象传递给人力资源部门,我实际上会给它从asSet()返回的值来雇用/解雇整个团队。

如果我将这个对象传递给剧院进行表演,我实际上会给它从asArtist()返回的值来被视为才华。

只要您直接控制与不同组件的交互,这就可以正常工作...

但我意识到您的问题是单个第三方供应商创建了一个组件TheaterManager,它期望一个对象执行这两个功能,并且它不知道asSetasArtist方法。问题不在于创建SetArtist的供应商,而是将它们合并而不是使用访问者模式或指定与我上面制作的asSetasArtist方法相似的接口的供应商。如果您能说服您的一个供应商“C”修复该接口,那么您的世界将更加幸福。

祝你好运!


好的评论,但问题在于编写ArtistSet的人不知道Java会合并两个超级接口的方法,所以他们不会想以这种方式编写类。你为什么要这样写呢?只是为了避免名称冲突,还是多个超级接口总体上都不好? - Dog
1
多个超级接口本身没有问题,只要它们不重叠功能或者在重叠时它们是语义上兼容的。但这里并非如此,这真的是ArtistSet作者的问题——无论他们最初是否理解冲突,都不能改变他们的Java实现存在设计缺陷并应该被修复的事实。我对面临这个问题的OP感到同情。 - William Price
由于两个接口在“补充”方法上存在互斥的重叠,我根本无法同时实现两个接口:其中一个将是错误的。我可以做的是实现一个接口(我认为最常用的那个),然后像上面为另一个接口使用“封装行为”方法。至少这样我不会将“隐藏”的歧义/错误进一步传播到自己的代码中,但我承认这并不是解决OP与第三方库集成需求的方法。这是供厂商修复的示例。 - William Price

-1
狗狗,我有一种强烈的感觉,你漏掉了一些对解决方案至关重要的细节。这在SO上经常发生,因为:
  • 人们需要省略很多细节,以使问题的大小和范围合理;
  • 人们不完全理解问题和解决方案(这就是为什么他们寻求帮助的原因),所以他们无法确定哪些细节是重要的,哪些不是;
  • 人们不能自己解决问题的原因是他们不理解这个细节的重要性,这也是他们忽略它的原因。

我已经在另一个答案中说过我会如何处理ArtistSet。但是请记住以上内容,我将为您提供另一种解决稍微不同问题的方法。假设我有来自糟糕供应商的代码:

package com.bad;

public interface IAlpha {
    public String getName();
    // Sort Alphabetically by Name
    public int compareTo(IAlpha other);
}

这很糟糕,因为你应该声明一个返回Comparator<IAlpha>的函数来实现排序策略,但无论如何。现在我从一个更糟糕的公司得到了代码:

package com.worse;
import com.bad.IAlpha;

// an Alpha ordered by name length
public interface ISybil extends IAlpha, Comparable<IAlpha> {}

这是更糟糕的,因为它完全错误,覆盖了不兼容的行为。一个ISybil按名称长度排序,但一个IAlpha按字母顺序排序,除了一个ISybil 是一个 IAlpha。当他们本可以并且应该做类似以下的事情时,他们被IAlpha的反模式误导了:

public interface ISybil extends IAlpha {
  public Comparator<IAlpha> getLengthComparator();
}

然而,这种情况比ArtistSet要好得多,因为这里的预期行为是有文档记录的。对于ISybil.compareTo()应该做什么没有任何困惑。所以我会创建以下类。一个Sybil类,它将compareTo()实现为com.worse所期望的,并委托其他所有内容:

package com.hack;

import com.bad.IAlpha;
import com.worse.ISybil;

public class Sybil implements ISybil {

    private final Alpha delegate;

    public Sybil(Alpha delegate) { this.delegate = delegate; }
    public Alpha getAlpha() {   return delegate; }
    public String getName() { return delegate.getName(); }
    public int compareTo(IAlpha other) {
        return delegate.getName().length() - other.getName().length();
    }

}

还有一个 Alpha 类,它的工作方式与 com.bad 所说的完全一样:

package com.hack;
import com.bad.IAlpha;

public class Alpha implements IAlpha {
    private String name;
    private final Sybil sybil;
    public Alpha(String name) { 
        this.name = name;
        this.sybil = new Sybil(this);
    }

    // Sort Alphabetically
    public int compareTo(IAlpha other) {
        return name.compareTo(other.getName());
    }

    public String getName() { return name; }
    public Sybil getSybil() { return sybil; }
}

请注意,我包含了类型转换方法:Alpha.getSybil()和Sybil.getAlpha()。这样我就可以创建自己的包装器,围绕任何需要或返回Sybils的com.worse供应商的方法。这样我就可以避免在我的代码或其他供应商的代码中污染com.worse的问题。所以如果com.worse有:
public ISybil breakage(ISybil broken);

我可以编写一个函数

public Alpha safeDelegateBreakage(Alpha alpha) {
  return breakage(alpha.getSybil).getAlpha();
}

做完就行了,除非我仍然会向com.worse大声抱怨,向com.bad客气地抱怨。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接