[英]How to implement interfaces with homographic methods in Java?
在英語中,同形異義詞對是兩個具有相同拼寫但含義不同的單詞。
在軟件工程中,一對同形方法是兩種具有相同名稱但要求不同的方法。 讓我們看一個人為的例子,讓問題盡可能清晰:
interface I1 {
/** return 1 */
int f()
}
interface I2 {
/** return 2*/
int f()
}
interface I12 extends I1, I2 {}
我該如何實施I12
? C# 有辦法做到這一點,但Java沒有。 所以唯一的方法是破解。 怎樣才能最可靠地使用反射/字節碼技巧/等(也就是說它不一定是一個完美的解決方案,我只想要一個效果最好的解決方案)?
需要注意的是遺留代碼,我不能合法逆向工程現有的一些封閉源代碼的龐大的一塊需要類型的參數I12
和代表I12
既具有代碼I1
作為參數,和代碼具有I2
作為參數。 所以基本上我需要創建一個I12
的實例,它知道它何時應該作為I1
,何時它應該作為I2
,我相信可以通過在直接調用者的運行時查看字節碼來完成。 我們可以假設調用者沒有使用反射,因為這是簡單的代碼。 問題是I12
的作者沒想到Java會從兩個接口合並f
,所以現在我必須想出最好的解決問題的方法。 沒有任何東西可以調用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();
}
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
,問題就變得清晰了:兩個超級ArtistSet
都指定complement
應該做其他事情,所以你必須以同樣的方式在同一個類中實現具有相同簽名的兩個complement
方法。 Artist.complement
是單應Set.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
}
}
}
如何解決您的具體案例
ArtistSet只是一個藝術家和一套:
/** A set of associated artists, e.g: a band. */
interface ArtistSet extends Set, Artist { }
從OO的角度來看,這不是一個有用的聲明。 藝術家是一種名詞,一種定義了屬性和動作(方法)的“東西”。 集合是事物的集合 - 獨特元素的集合。 相反,嘗試:
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 };
}
盡管Gray Kemmey的勇敢嘗試,我會說問題,因為你已經說過它是不可解決的。 作為給定ArtistSet
的一般規則,您無法知道調用它的代碼是否期望Artist
或Set
。
此外,即使你可以根據你對各種其他答案的評論,你實際上還需要將ArtistSet
傳遞給供應商提供的函數,這意味着函數沒有給編譯器或人類任何關於它期望的線索。 。 對於任何技術上正確的答案,你完全沒有運氣。
作為完成工作的實際編程問題,我將執行以下操作(按此順序):
ArtistSet
的界面的任何人以及自己生成ArtistSet
界面的人提交錯誤報告。 ArtistSet
的函數的供應商ArtistSet
支持請求,並詢問他們對complement()
的行為的期望。 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();
}
}
通過拋出異常,您有機會獲得堆棧跟蹤,從而為您提供關於調用者期望的兩種行為中的哪一種的線索。 幸運的是,沒有人會調用該功能,這就是為什么供應商在運輸代碼方面遇到了這個問題。 雖然運氣較少但仍有一些,他們處理異常。 如果不是這樣,那么,至少現在你將有一個堆棧跟蹤,你可以查看以確定調用者真正期待的內容並可能實現它(雖然我不情願地想到這種方式永久存在錯誤,但我已經解釋了我是怎么回事會在另一個答案中這樣做)。
順便說一句,對於其余的實現,我會將所有內容委托給通過構造函數傳入的實際Artist
和Set
對象,以便以后可以輕松拆分。
在Java中,具有兩個具有單應方法的超接口的類被認為僅具有該方法的一個實現。 (請參閱Java語言規范部分8.4.8 )。 這允許類方便地從多個接口繼承,這些接口都實現相同的其他接口,並且只實現一次該功能。 這也簡化了語言,因為這樣就不需要語法和方法調度支持來區分基於它們來自哪個接口的單應方法。
因此,實現具有兩個具有單應方法的超接口的類的正確方法是提供滿足兩個超接口的契約的單個方法。
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")
有時返回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
對象的函數。
這里拒絕一個問題的原因之一是“它只與一個非常狹窄的情況有關,而這種情況通常不適用於全球互聯網用戶。” 因為我們希望提供幫助,處理這些狹隘問題的首選方法是修改問題,以便更廣泛地適用。 對於這個問題,我采取的方法是回答廣泛適用的問題版本,而不是實際編輯問題,以刪除使其獨特的問題。
在商業編程的現實世界中,任何像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語言支持之前很久就已編寫),因此沒有開源的事實處理這種情況的實用程序說明了在采用這種方法時努力與有用的比率。
好的,經過大量的研究,我有另一個想法來完全適應這種情況。 由於您無法直接修改其代碼......您可以自行強制進行修改。
免責聲明:以下示例代碼非常簡化。 我的目的是展示如何做到這一點的一般方法,而不是生成有效的源代碼(因為這本身就是一個項目)。
問題是這些方法是單應性的。 所以要解決它,我們可以重命名方法。 簡單吧? 我們可以使用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;
}
當然,這是簡化的。 它將在extends
或implements Artist
任何類中用“compliment”替換“補充”一詞,因此您很可能需要進一步對其進行條件化(例如,如果Artist.class.isAssignableFrom(classBeingRedefined) && Set.class.isAssignableFrom(classBeingRedefined)
,你顯然不希望用“compliment”替換“補充”的每個實例,因為Set
的“補充”是完全合法的)。
所以,現在我們已經糾正了Artist
接口及其實現。 錯字消失了,方法有兩個不同的名字,所以沒有單應性。 這允許我們現在在CommunityTheatre
類中有兩個不同的實現,每個實現都將正確實現/覆蓋ArtistSet
的方法。
不幸的是,我們現在已經創建了另一個(可能更大)的問題。 我們剛剛從實現Artist
類中刪除了所有以前合法的對complement()
引用。 要解決這個問題,我們需要創建另一個ClassFileTransformer
,用我們新的方法名替換這些調用。
這有點困難,但並非不可能。 基本上,新的ClassFileTransformer
(假設我們稱之為OldComplementTransformer
)必須執行以下步驟:
Object
; Object
是否是Artist
; 和, 完成第二個變換器后,可以修改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中的錯誤,對每個人來說仍然會更好。)
這是我要做的消除歧義的方法:
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
,它需要這兩個函數的一個對象,它不會知道asSet
和asArtist
方法。 問題不在於創建Set
和Artist
的供應商,而是供應商將它們組合在一起而不是使用訪問者模式,或僅指定一個將鏡像我上面提到的asSet
和asArtist
方法的接口。 如果你可以說服你的一個供應商“C”來修復這個界面,你的世界將會更加快樂。
祝好運!
狗,我有一種強烈的感覺,你遺漏了一些對解決方案至關重要的細節。 這通常發生在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()
應該做什么沒有混淆。 所以我會按如下方式創建類。 實現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()。 這樣我就可以圍繞任何com.worse供應商的方法創建自己的包裝器,這些方法可以使用或返回Sybils,因此我可以避免使用com.worse的破壞來污染我的代碼或任何其他供應商的代碼。 所以如果com.worse有:
public ISybil breakage(ISybil broken);
我可以寫一個函數
public Alpha safeDelegateBreakage(Alpha alpha) {
return breakage(alpha.getSybil).getAlpha();
}
並且完成它,除了我仍然會大聲抱怨com.wad和禮貌地com.bad。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.