13

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

在软件工程中,一对单应方法是指名称相同但需求不同的两种方法。让我们看一个人为的例子,以使问题尽可能清楚:

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

我该如何实施I12?C#有办法做到这一点,但 Java 没有。所以唯一的办法就是破解。如何最可靠地使用反射/字节码技巧/等来完成它(即它不必是一个完美的解决方案,我只想要一个效果最好的解决方案)?


请注意,我无法合法进行逆向工程的一些现有的封闭源大量遗留代码需要一个类型参数,并将两者I12委托给具有作为参数的代码和具有作为参数的代码。所以基本上我需要创建一个知道它何时应该作为以及何时应该作为的实例,我相信这可以通过在直接调用者的运行时查看字节码来完成。我们可以假设调用者没有使用反射,因为这是简单的代码。问题是作者没想到Java会从两个接口合并,所以现在我必须想出解决这个问题的最佳方法。没有什么叫I12I1I2I12I1I2I12fI12.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();
}

Theater进行Artist表演,这可能导致Artist被补充。剧院的观众可以在表演之间评判艺术家。表演者的自尊心越高,观众越可能喜欢Artist,但如果自尊心超过某个点,观众就会对艺术家产生负面评价:

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.complement是 的同形异义词Set.complement

4

7 回答 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
        }
    }
}
于 2013-05-11T06:37:05.813 回答
3

如何解决您的具体情况

ArtistSet 只是一个 Artist 和一个 Set:

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

从 OO 的角度来看,这不是一个有用的声明。艺术家是一种名词,一种具有定义属性和动作(方法)的“事物”。Set 是事物的集合——独特元素的集合。相反,请尝试:

ArtistSet 只是一组艺术家。

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

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

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

如何解决更一般的情况

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

Java 不允许您实现您请求的行为 - 您必须有替代解决方法。 Java 不允许一个类为来自多个不同接口的相同方法签名提供多个实现(使用某种形式的限定/别名/注释来区分多次实现相同的方法)。请参阅Java 覆盖两个接口、方法名称冲突Java - 接口实现中的方法名称冲突

例如,如果您有以下情况

 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 };
}
于 2013-05-15T06:06:19.953 回答
3

尽管 Gray Kemmey 做出了勇敢的尝试,但我会说这个问题,正如你所说的那样,它是无法解决的。作为一般规则,ArtistSet您无法知道调用它的代码是期待 aArtist还是 a Set

此外,即使您可以,根据您对其他各种答案的评论,您实际上也需要将 an 传递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对象,以便以后可以轻松地将其分开。

于 2013-05-18T05:57:02.340 回答
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()had in mind的作者C.f("Hello")应该返回 5 或 72,则无法通过查看代码来判断。

很好,所以我一般不能使用字节码技巧为单应函数提供不同的行为,但我确实有一个像我的示例类这样的类TheaterManager。我应该怎么做才能实施ArtistSet.complement()

您提出的实际问题实际答案是创建您自己的替代实现,不需要. 您不需要更改库的实现,您需要自己编写。TheaterManagerArtistSet

您引用的另一个示例问题实际答案基本上是“委托给”,因为没有接收对象的函数继续将该对象传递给期望对象的函数。I12.f()I2.f()I12I1

Stack Overflow 仅用于一般感兴趣的问题和答案

在这里拒绝一个问题的陈述的原因之一是“它只与一个非常狭窄的情况有关,这种情况通常不适用于互联网的全球观众。” 因为我们希望有所帮助,所以处理此类狭隘问题的首选方法是将问题修改为更广泛适用。对于这个问题,我采取了回答问题的广泛适用版本的方法,而不是实际编辑问题以删除使其对您的情况独特的原因。

在商业编程的现实世界中,任何具有损坏接口的 Java 库I12都不会积累甚至几十个商业客户端,除非它可以通过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>a和 a之间没有类或方法签名的区别,参数TalentAgent<Set>的正式类型people只是List. 调用者的类接口或方法签名中没有任何内容可以通过查看堆栈来告诉您该做什么。

因此,您需要实现多种策略,其中之一是反编译调用方法的代码,以寻找调用者期待一个或另一个类的线索。涵盖所有可能发生这种情况的方式必须非常复杂,因为除其他外,您无法提前知道它实际期望的类,只能知道它期望实现一个接口的类。

有成熟且极其复杂的开源字节码实用程序,包括在运行时自动为给定类生成代理的工具(早在 Java 语言支持该类之前就编写好了),因此没有开源的事实处理这种情况的效用充分说明了采用这种方法的努力与效用的比率。

于 2013-05-20T00:10:35.267 回答
1

好的,经过大量研究,我有另一个想法来完全适应这种情况。由于您不能直接修改他们的代码......您可以自己强制修改。

免责声明:下面的示例代码非常简化。我的目的是展示如何做到这一点的一般方法,而不是产生功能性的源代码来做到这一点(因为这本身就是一个项目)。

问题是这些方法是单应的。所以要解决它,我们可以重命名方法。很简单,对吧?我们可以使用Instrument 包来实现这一点。正如您将在链接文档中看到的那样,它允许您创建一个“代理”,它可以在加载类时直接修改它们,或者即使它们已经加载,也可以重新修改它们。

本质上,这需要您创建两个类:

  • 预处理和重新加载类的代理类;和,
  • ClassFileTransformer指定您要进行的更改的实现。

代理类必须定义 apremain()agentmain()方法,具体取决于您是希望它在 JVM 启动时开始处理,还是在它已经运行之后开始处理。这方面的例子在上面的包文档中。这些方法使您可以访问一个Instrumenation实例,这将允许您注册您的ClassFileTransformer. 所以它可能看起来像这样:

接口FixAgent.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;
    }

当然,这是简化的。它将在任何类中用“compliment”替换单词“complement” extendsimplements Artist因此您很可能需要进一步对其进行条件化(例如,如果Artist.class.isAssignableFrom(classBeingRedefined) && Set.class.isAssignableFrom(classBeingRedefined),您显然不想将“complement”的每个实例替换为“恭维”,因为“补充”Set是完全合法的)。

所以,现在我们已经更正了Artist接口及其实现。错字不见了,方法有两个不同的名称,所以没有单应性。这允许我们现在在我们的CommunityTheatre类中有两个不同的实现,每个都将正确实现/覆盖ArtistSet.

不幸的是,我们现在创建了另一个(可能更大)问题。我们刚刚打破了对complement()实现类的所有以前合法的引用Artist。为了解决这个问题,我们需要创建另一个ClassFileTransformer用我们的新方法名称替换这些调用。

这有点困难,但并非不可能。本质上,新ClassFileTransformer的(假设我们称之为OldComplementTransformer)将必须执行以下步骤:

  1. 找到与以前相同的字节串(代表旧方法名称的字节,“补码”);
  2. 获取此之前的字节,这些字节表示调用该方法的对象引用;
  3. 将这些字节转换为Object;
  4. 检查是否ObjectArtist; 和,
  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 和测试来说简直就是地狱。但它肯定是强大的,它解决了这个问题。(从技术上讲,我想它可以通过删除它来避免这个问题,但是......我会尽我所能。)

我们可能已经解决了这个问题的其他方法:

这两种方法都允许您直接操作内存中的字节。当然可以围绕这些设计一个解决方案,但我相信这会更加困难并且更不安全。所以我选择了上面的路线。

我认为这个解决方案甚至可以变得更加通用,成为一个非常有用的库,用于集成代码库。在变量、命令行参数或配置文件中指定需要重构的接口和方法,然后让她放松。在运行时协调 Java 中冲突接口的库。(当然,我认为如果他们只是修复 Java 8 中的错误,对每个人来说仍然会更好。)

于 2013-07-18T19:01:44.707 回答
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它需要一个对象用于这两个功能,并且它不知道asSetandasArtist方法。问题不在于创建SetandArtist的供应商,而是将它们组合在一起的供应商,而不是使用访问者模式或仅指定一个接口来反映我在上面制作的asSetasArtist方法。如果你能说服你的一个供应商“C”来修复那个界面,你的世界会快乐很多。

祝你好运!

于 2013-05-19T02:02:15.930 回答
-1

狗,我有一种强烈的感觉,你遗漏了一些对解决方案至关重要的细节。这经常发生在 SO 因为

  • 人们需要省略很多细节才能使问题达到合理的规模和范围,
  • 人们不完全理解问题和解决方案(这就是他们寻求帮助的原因),因此他们无法确定哪些细节重要,哪些不重要,并且
  • 该人无法自行解决问题的原因是因为他们不了解此细节的重要性,这与他们忽略它的原因相同。

我在另一个答案中说过我会对 ArtistSet 做些什么。但请记住上述内容,我将为您提供另一种解决方案,以解决稍微不同的问题。假设我有来自不良供应商的代码:

package com.bad;

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

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

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

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

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

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

但是,这种情况仍然比 ArtistSet 好得多,因为这里记录了预期的行为。对于ISybil.compareTo()应该做什么没有混淆。所以我会按如下方式创建类。将 compareTo() 实现为 com.worse 的 Sybil 类期望并委托其他所有内容:

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

}

和一个与 com.bad 完全一样的 Alpha 类说它应该:

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。

于 2013-05-18T19:06:33.090 回答