如何在Java中实现与单应性方法的接口?

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

在软件工程中,一对同形方法是两种具有相同名称但要求不同的方法。 让我们看一个人为的例子,让问题尽可能清晰:

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

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


请注意,我不能合法地逆向工程的一些现有的闭源大量遗留代码需要I12类型的参数,并将I12委托给具有I1作为参数的代码,以及将I2作为参数的代码。 所以基本上我需要创建一个I12的实例,知道它何时应该充当I1 ,何时它应该充当I2 ,我相信可以通过在直接调用者的运行时查看字节码来完成。 我们可以假设调用者没有使用reflection,因为这是简单的代码。 问题是I12的作者没想到Java会从两个接口合并f ,所以现在我必须想出最好的解决问题的方法。 没有什么能调用I12.f (显然如果作者写了一些实际上叫做I12.f代码,他会在出售之前注意到这个问题)。

请注意,我实际上是在寻找这个问题的答案,而不是如何重构我无法改变的代码。 我正在寻找最好的启发式方法,或者如果存在的话,可以找到精确的解决方案。 请参阅Gray的答案以获得有效示例(我确信有更强大的解决方案)。


这是一个具体的例子,说明两个接口内的单应方法问题是如何发生的。 这是另一个具体的例子:

我有以下6个简单的类/接口。 它类似于剧院周围的商业和在其中表演的艺术家。 为了简单起见,我们假设它们都是由不同的人创建的。

Set代表一个集合,如集合论:

 interface Set { /** Complements this set, ie: 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只是一个ArtistSet

 /** A set of associated artists, eg: 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 ,问题就变得清晰了:两个超级ArtistSet都指定complement应该做其他事情,所以你必须以同样的方式在同一个类中实现具有相同签名的两个complement方法。 Artist.complementArtist.complement的同形异义词。

新想法,有点凌乱……

 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 } } } 

尽管Gray Kemmey的勇敢尝试,我会说问题,因为你已经说过它是不可解决的。 作为给定ArtistSet的一般规则,您无法知道调用它的代码是期望Artist还是Set

此外,即使你可以根据你对各种其他答案的评论,你实际上还需要将ArtistSet传递给供应商提供的函数,这意味着函数没有给编译器或人类任何关于它期望的线索。 。 对于任何技术上正确的答案,你完全没有运气。

作为完成工作的实际编程问题,我将执行以下操作(按此顺序):

  1. 向创建需要ArtistSet的界面的任何人以及自己生成ArtistSet界面的人提交错误报告。
  2. 向提供需要ArtistSet的函数的供应商ArtistSet支持请求,并询问他们对complement()的行为的期望。
  3. 实现complement()函数以抛出exception。
 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(); } } 

通过抛出exception,您有机会获得堆栈跟踪,从而为您提供关于调用者期望的两种行为中的哪一种的线索。 幸运的是,没有人调用该function,这就是为什么供应商得到了解决此问题的代码。 虽然运气较少但仍有一些,他们处理exception。 如果不是这样,那么,至少现在你将有一个堆栈跟踪,你可以查看以确定调用者真正期待的内容并可能实现它(虽然我不情愿地想到这样一个错误,我已经解释了我是怎么回事会在另一个答案中这样做)。

顺便说一句,对于其余的实现,我会将所有内容委托给通过构造函数传入的实际ArtistSet对象,以便以后可以轻松拆分。

如何解决您的具体案例

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

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

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

ArtistSet只是一组艺术家。

  /** A set of associated artists, eg: a band. */ interface ArtistSet extends Set { }; 

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

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

如何解决更一般的案例

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

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

  • 避免使用Inheritence(extends或implements)而是使用Object Composition(请参阅http://en.wikipedia.org/wiki/Composition_over_inheritance )

例如,如果您有以下内容

  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中,具有两个具有单应方法的超接口的类被认为仅具有该方法的一个实现。 (请参阅Java语言规范部分8.4.8 )。 这允许类方便地从多个接口inheritance,这些接口都实现相同的其他接口,并且只实现一次该function。 这也简化了语言,因为这样就不需要语法和方法调度支持来区分基于它们来自哪个接口的单应方法。

因此,实现具有两个具有单应方法的超接口的类的正确方法是提供满足两个超接口的约定的单个方法。

C#有办法做到这一点。 如何在Java中完成? 这个没有构造吗?

C#定义的接口与Java不同,因此具有Java不具备的function。

在Java中,语言构造被定义为意味着所有接口都获得相同方法的相同单个实现。 没有Java语言构造用于基于对象的编译时类创建多重inheritance的接口函数的替代行为。 这是Java语言设计者的有意识选择。

如果没有,如何最可靠地使用reflection/字节码技巧/等?

“它”不能用reflection/字节码技巧来完成,因为决定选择哪个接口版本的单应方法所需的信息不一定存在于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 if(s); } // f1( i, "Hi") == 72 public static int f2(I2 i, String s) { return if(s); } // f2( i, "Hi") == 2 public static int f12(I12 i, String s) { return if(s);} // f12(i, "Hi") == ??? } 

根据Java语言规范,实现I12的类必须以这样的方式执行: C.f1()C.f2()C.f12()在使用相同的参数调用时返回完全相同的结果。 如果C.f12(i, "Hello")有时返回72并且有时根据C.f12()的调用方式返回5,那将是程序中的严重错误并且违反了语言规范。

此外,如果C类的作者期望f12()中存在某种一致的行为,则C类中没有字节码或其他信息表明它是否应该是I1.f(s)I2.f(s)的行为I2.f(s) 。 如果C.f12()的作者考虑到Cf("Hello")应该返回5或72,那么从查看代码就无法判断。

很好,所以我通常不能使用字节码技巧为同形函数提供不同的行为,但我真的有一个像我的示例类TheaterManager类。 我该怎么做才能实现ArtistSet.complement()

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

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

Stack Overflow仅用于普遍感兴趣的问题和答案

在这里拒绝一个问题的一个明确理由是“它只与一个非常狭窄的情况有关,这种情况通常不适用于全球互联网用户。” 因为我们希望提供帮助,处理这些狭隘问题的首选方法是修改问题以便更广泛地适用。 对于这个问题,我采取的方法是回答广泛适用的问题版本,而不是实际编辑问题,以删除使其独特的问题。

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

  • 委托给I1.f()
  • 委托给I2.f()
  • 没做什么
  • 抛出exception
  • 根据I12对象的某些成员的值,在每次调用的基础上选择上述策略之一

如果成千上万甚至只有少数几家公司在Java中使用这个库的这一部分,那么您可以放心,他们已经使用了其中一种解决方案。 如果即使是少数公司也没有使用该库,那么Stack Overflow的问题就太窄了。

好吧, TheaterManager过于简单了。 在实际情况中,我很难替换那个类,我不喜欢你概述的任何实际解决方案。 我不能用花哨的JVM技巧解决这个问题吗?

这取决于你想要修复的内容。 如果要通过将所有调用映射到I12.f()然后解析堆栈以确定调用者并根据该调用来选择行为来修复特定库。 您可以通过Thread.currentThread().getStackTrace()访问堆栈。

如果您遇到呼叫者,则无法识别您可能很难确定他们想要的版本。 例如,您可以从generics调用(就像您给出的其他特定示例中的实际情况一样),例如:

 public class TalentAgent { public static void butterUp(List people) { for (T a: people) { a.complement() } } } 

在Java中, generics被实现为擦除 ,这意味着在编译时丢弃所有类型信息。 TalentAgentTalentAgent之间没有类或方法签名差异, people参数的正式类型只是List 。 调用者的类接口或方法签名中没有任何内容可以通过查看堆栈来告诉您该怎么做。

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

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

好的,经过大量的研究,我有另一个想法来完全适应这种情况。 由于您无法直接修改其代码……您可以自行强制进行修改。

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

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

从本质上讲,这需要您制作两个类:

  • 预处理和重新加载类的代理类; 和,
  • 一个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; } 

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

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

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

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

  1. 找到与以前相同的字节串(表示旧方法名称的字符串,“补码”);
  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中的错误,对每个人来说仍然会更好。)

Here’s what I’d do to remove the ambiguity:

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

If I were passing this object to the HR department, I’d actually give it the value returned from asSet() to hire/fire the entire group.

If I were passing this object to the Theater for a performance, I’d actually give it the value returned from asArtist() to be treated as talent.

This works as long as YOU are in control of talking to the different components directly…

But I realize that your problem is a single third-party vendor has created a component, TheaterManager , that expects one object for both of these functions and it won’t know about the asSet and asArtist methods. The problem is not with the vendors that created Set and Artist , it is the vendor that combined them instead of using a Visitor pattern or just specifying an interface that would mirror the asSet and asArtist methods I made above. If you can convince your one vendor “C” to fix that interface, your world will be a lot happier.

祝你好运!

Dog, I have a strong feeling you are leaving out some details that are crucial to the solution. This often happens on SO because

  • people need to leave out a lot of details to get the question to a reasonable size and scope,
  • people do not fully understand the problem and the solution (which is why they are asking for help) so they cannot be sure which details are important and which are not, and
  • the reason the person cannot solve the problem on their own is because they do not understand the importance of this detail, which is the same reason they left it out.

I’ve said in another answer what I would do about ArtistSet. But keeping the above in mind I will give you another solution to a slightly different problem. Lets say I had code from a bad vendor:

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

This is bad because you should declare a function returning a Comparator to implement the sorting strategy, but whatever. Now I get code from a worse company:

 package com.worse; import com.bad.IAlpha; // an Alpha ordered by name length public interface ISybil extends IAlpha, Comparable {} 

This is worse, because it is totally wrong, in that it overrides behavior incompatibly. An ISybil orders itself by name length, but an IAlpha orders itself alphabetically, except an ISybil is an IAlpha . They were mislead by the anti-pattern of IAlpha when they could and should have done something like:

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

However , this situation is still much better than ArtistSet because here the expected behavior is documented. There is no confusion about what ISybil.compareTo() should do. So I would create classes as follows. A Sybil class that implements compareTo() as com.worse expects and delegates everything else:

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

and an Alpha class that works exactly like com.bad said it should:

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

Note that I included type conversion methods: Alpha.getSybil() and Sybil.getAlpha(). This is so I could create my own wrappers around any com.worse vendor’s methods that take or return Sybils so I can avoid polluting my code or any other vendor’s code with com.worse’s breakage. So if com.worse had:

 public ISybil breakage(ISybil broken); 

I could write a function

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

and be done with it, except I would still complain vociferously to com.worse and politely to com.bad.