3分鐘快速搞懂Java的橋接方法

萬貓學社發表於2020-12-16

什麼是橋接方法?

Java中的橋接方法(Bridge Method)是一種為了實現某些Java語言特性而由編譯器自動生成的方法。

我們可以通過Method類的isBridge方法來判斷一個方法是否是橋接方法。

在位元組碼檔案中,橋接方法會被標記為ACC_BRIDGEACC_SYNTHETIC,其中ACC_BRIDGE用於表示該方法是由編譯器產生的橋接方法,ACC_SYNTHETIC用於表示該方法是由編譯器自動生成。

文章持續更新,微信搜尋「萬貓學社第一時間閱讀,關注後回覆「電子書」,免費獲取12本Java必讀技術書籍。

什麼時候生成橋接方法?

為了實現哪些Java語言特性會生成橋接方法?最常見的兩種情況就是協變返回值型別型別擦除,因為它們導致了父類方法的引數和實際呼叫的方法引數型別不一致。下面我們通過兩個例子更好地理解一下。

文章持續更新,微信搜尋「萬貓學社第一時間閱讀,關注後回覆「電子書」,免費獲取12本Java必讀技術書籍。

協變返回型別

協變返回型別是指子類方法的返回值型別不必嚴格等同於父類中被重寫的方法的返回值型別,而可以是更 "具體" 的型別。

在Java 1.5新增了對協變返回型別的支援,即子類重寫父類方法時,返回的型別可以是子類方法返回型別的子類。下面看一個例子:

public class Parent {
    Number get() {
        return 1;
    }
}
public class Child extends Parent {

    @Override
    Integer get() {
        return 1;
    }
}

Child類重寫其父類Parent的get方法,Parent的get方法返回型別為Number,而Child類中get方法返回型別為Integer。

將這段程式碼進行編譯,再反編譯:

javac Child.java
javap -v -c Child.class

文章持續更新,微信搜尋「萬貓學社第一時間閱讀,關注後回覆「電子書」,免費獲取12本Java必讀技術書籍。

結果如下:

public class Child extends Parent
......省略部分結果......
  java.lang.Integer get();
    descriptor: ()Ljava/lang/Integer;
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_1
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 5: 0

  java.lang.Number get();
    descriptor: ()Ljava/lang/Number;
    flags: ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #3                  // Method get:()Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 1: 0

從上面的結果可以看到,有一個方法java.lang.Number get(), 在原始碼中是沒有出現過的,是由編譯器自動生成的,該方法被標記為ACC_BRIDGEACC_SYNTHETIC,就是我們前面所說的橋接方法。

這個方法就起了一個橋接的作用,它所做的就是把對自身的呼叫通過invokevirtual指令再呼叫方法java.lang.Integer get()

編譯器這麼做的原因是什麼呢?因為在JVM方法中,返回型別也是方法簽名的一部分,而橋接方法的簽名和其父類的方法簽名一致,以此就實現了協變返回值型別。

文章持續更新,微信搜尋「萬貓學社第一時間閱讀,關注後回覆「電子書」,免費獲取12本Java必讀技術書籍。

型別擦除

泛型是Java 1.5才引進的概念,在這之前是沒有泛型的概念的,但泛型程式碼能夠很好地和之前版本的程式碼很好地相容,這是為什麼呢?

這是因為,在編譯期間Java編譯器會將型別引數替換為其上界(型別引數中extends子句的型別),如果上界沒有定義,則預設為Object,這就叫做型別擦除。

當一個子類在繼承(或實現)一個父類(或介面)的泛型方法時,在子類中明確指定了泛型型別,那麼在編譯時編譯器會自動生成橋接方法,例如:

public class Parent<T> {

    void set(T t) {
    }
}
public class Child extends Parent<String> {

    @Override
    void set(String str) {
    }
}

文章持續更新,微信搜尋「萬貓學社第一時間閱讀,關注後回覆「電子書」,免費獲取12本Java必讀技術書籍。

Child類在繼承其父類Parent的泛型方法時,明確指定了泛型型別為String,將這段程式碼進行編譯,再反編譯:

public class Child extends Parent<java.lang.String>
......省略部分結果......
  void set(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags:
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 5: 0

  void set(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #2                  // class java/lang/String
         5: invokevirtual #3                  // Method set:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 1: 0

從上面的結果可以看到,有一個方法void set(java.lang.Object), 在原始碼中是沒有出現過的,是由編譯器自動生成的,該方法被標記為ACC_BRIDGEACC_SYNTHETIC,就是我們前面所說的橋接方法。

這個方法就起了一個橋接的作用,它所做的就是把對自身的呼叫通過invokevirtual指令再呼叫方法void set(java.lang.String)

編譯器這麼做的原因是什麼呢?因為Parent類在型別擦除之後,變成這樣:

public class Parent<Object> {

    void set(Object t) {
    }
}

編譯器為了讓子類有一個與父類的方法簽名一致的方法,就在子類自動生成一個與父類的方法簽名一致的橋接方法。

文章持續更新,微信搜尋「萬貓學社第一時間閱讀,關注後回覆「電子書」,免費獲取12本Java必讀技術書籍。

如何查詢橋接方法的實際方法

在Spring Framework中已經實現了查詢橋接方法的實際方法的功能,就在spring-core模組中的BridgeMethodResolver類中,像這樣直接使用就行了:

method = BridgeMethodResolver.findBridgedMethod(method);

findBridgedMethod方法是怎麼實現的呢?我們來分析一下原始碼(spring-core的版本為5.2.8.RELEASE):

public static Method findBridgedMethod(Method bridgeMethod) {
    // 如果不是橋連方法,就直接返回原方法。
	if (!bridgeMethod.isBridge()) {
		return bridgeMethod;
	}
    // 先從本地快取讀取,快取中有則直接返回。
	Method bridgedMethod = cache.get(bridgeMethod);
	if (bridgedMethod == null) {
		List<Method> candidateMethods = new ArrayList<>();
        // 以方法名稱和入參個數相等為篩選條件。
		MethodFilter filter = candidateMethod ->
				isBridgedCandidateFor(candidateMethod, bridgeMethod);
        // 遞迴該類及其所有父類上的所有方法,符合篩選條件就新增進來。
		ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass()
            , candidateMethods::add, filter);
		if (!candidateMethods.isEmpty()) {
            // 如果符合篩選條件的方法個數為1,則直接採用;
            // 否則,呼叫searchCandidates方法再次篩選。
			bridgedMethod = candidateMethods.size() == 1 ?
					candidateMethods.get(0) :
					searchCandidates(candidateMethods, bridgeMethod);
		}
        // 如果找不到實際方法,則返回原來的橋連方法。
		if (bridgedMethod == null) {
			// A bridge method was passed in but we couldn't find the bridged method.
			// Let's proceed with the passed-in method and hope for the best...
			bridgedMethod = bridgeMethod;
		}
        // 把查詢的結果放入記憶體快取。
		cache.put(bridgeMethod, bridgedMethod);
	}
	return bridgedMethod;
}

文章持續更新,微信搜尋「萬貓學社第一時間閱讀,關注後回覆「電子書」,免費獲取12本Java必讀技術書籍。

我們再看一下再次篩選的searchCandidates方法是如何實現的:

private static Method searchCandidates(List<Method> candidateMethods, Method bridgeMethod) {
	if (candidateMethods.isEmpty()) {
		return null;
	}
	Method previousMethod = null;
	boolean sameSig = true;
    // 遍歷候選方法的列表
	for (Method candidateMethod : candidateMethods) {
        // 對比橋接方法的泛型型別引數和候選方法是否匹配,如果匹配則直接返回該候選方法。
		if (isBridgeMethodFor(bridgeMethod, candidateMethod, bridgeMethod.getDeclaringClass())) {
			return candidateMethod;
		}
		else if (previousMethod != null) {
            // 如果不匹配,則判斷所有候選方法的引數列表是否相等。
			sameSig = sameSig && Arrays.equals(candidateMethod.getGenericParameterTypes()
                , previousMethod.getGenericParameterTypes());
		}
		previousMethod = candidateMethod;
	}
    // 如果所有候選方法的引數列表全相等,則返回第一個候選方法。
	return (sameSig ? candidateMethods.get(0) : null);
}

總結以上原始碼就是,通過判斷方法名、引數的個數以及泛型型別引數來獲取橋接方法的實際方法。

微信公眾號:萬貓學社

微信掃描二維碼

關注後回覆「電子書」

獲取12本Java必讀技術書籍

3分鐘快速搞懂Java的橋接方法

相關文章