簡體   English   中英

Class.getDeclaredMethods() 返回繼承的默認方法

[英]Class.getDeclaredMethods() returns inherited default methods

當在類對象上調用方法getDeclaredMethods時,應該返回一個 Method 對象數組,表示直接聲明為該類一部分的方法。 它不應該返回任何繼承的方法。

當我直接通過 Android Studio 安裝我的應用程序時,無論活動構建變體如何,這都可以正常工作。 切換到發布版本不足以觸發問題。

編譯 APK 或 App Bundle (.aab) 並以這種方式安裝應用程序時會出現問題。 (直接將 APK 復制到設備上,或者在 Google Play 商店推出捆綁包並從那里安裝應用程序。)

這是我的測試場景,在一個新的 Android Studio 項目中,使用 SDK 33、 minSdk 21 (Android 5.0)、 minifyEnabled false並刪除了默認的proguardFiles語句,以確保這不是由 R8 / ProGuard 引起的。

界面:

// TestInterface.java

package com.example.testapp;

public interface TestInterface {
    default String methodWithDefault() {
        return "default";
    }

    String methodWithoutDefault();
}

實現類:

// TestClass.java

package com.example.testapp;

public class TestClass implements TestInterface {
    @Override
    public String methodWithoutDefault() {
        return "non-default";
    }
}

測試用例:

// MainActivity.java

package com.example.testapp;

import android.os.Bundle;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TestClass test = new TestClass();
        StringBuilder sb = new StringBuilder("Methods:\n");
        for (Method m : TestClass.class.getDeclaredMethods()) {
            sb.append('\n').append(m.toString()).append('\n');
            try {
                String s = (String) m.invoke(test);
                sb.append("Result: ").append(s).append('\n');
            } catch (InvocationTargetException e) {
                sb.append("Target exception: ").append(e.getTargetException()).append('\n');
            } catch (IllegalAccessException e) {
                sb.append("Illegal access.\n");
            }
        }

        System.out.println(sb);

        TextView textView = findViewById(R.id.textView);
        textView.setText(sb.toString());
    }
}

app/build.gradle的內容:

plugins {
    id 'com.android.application'
}

android {
    namespace 'com.example.testapp'
    compileSdk 33

    defaultConfig {
        applicationId "com.example.testapp"
        minSdk 21
        targetSdk 33
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
        }
    }
    compileOptions {
        sourceCompatibility 11
        targetCompatibility 11
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.5.1'
    implementation 'com.google.android.material:material:1.7.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}

直接從 Android Studio 運行時的輸出:

Methods:

public java.lang.String com.example.testapp.TestClass.methodWithoutDefault()
Result: non-default

構建 APK 並將其安裝到設備時的輸出:

Methods:

public java.lang.String com.example.testapp.TestClass.methodWithDefault()
Result: default

public java.lang.String com.example.testapp.TestClass.methodWithoutDefault()
Result: non-default

問題:

  1. 為什么會這樣?
  2. 解決它的最佳方法是什么?

在典型的橡皮鴨調試方式中,我發現了一些重要的細節以及如何在改進測試用例的同時解決這個問題,然后再將其發布到 StackOverflow 上......

首先,讓我們也打印一些方法的屬性。 我們可以修改MainActivity如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TestClass test = new TestClass();
        StringBuilder sb = new StringBuilder("Methods:\n");
        for (Method m : TestClass.class.getDeclaredMethods()) {
            sb.append('\n').append(m.toString()).append('\n');
            sb.append("Synthetic: ").append(m.isSynthetic()).append('\n');
            sb.append("Bridge: ").append(m.isBridge()).append('\n');
            sb.append("Default: ").append(m.isDefault()).append('\n');
            try {
                String s = (String) m.invoke(test);
                sb.append("Result: ").append(s).append('\n');
            } catch (InvocationTargetException e) {
                sb.append("Target exception: ").append(e.getTargetException()).append('\n');
            } catch (IllegalAccessException e) {
                sb.append("Illegal access.\n");
            }
        }

        System.out.println(sb);

        TextView textView = findViewById(R.id.textView);
        textView.setText(sb.toString());
    }
}

這讓 Android Studio 抱怨,因為isDefault()僅從 SDK 24 開始可用,但我們的 minSdk 是 21。

讓我們將 minSdk 增加到 24並檢查輸出:

Methods:

public java.lang.String com.example.testapp.TestClass.methodWithoutDefault()
Synthetic: false
Bridge: false
Default: false
Result: non-default

哦,繼承的方法沒有了,如果你玩玩 minSdk。 你會發現問題出現在任何 <= 23 的值上,所以:我們做出第一個重要的認識:

1.只有當minSdk小於24時才會出現這個問題。

(請注意,您安裝 APK 的 Android 設備的實際 SDK 版本似乎並不重要;我正在 SDK 25 / Android 7.1.1 設備上測試這一切。)

讓我們切換回 minSdk 21,並以 SDK 版本檢查為條件調用isDefault ,如下所示:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TestClass test = new TestClass();
        StringBuilder sb = new StringBuilder("Methods:\n");
        for (Method m : TestClass.class.getDeclaredMethods()) {
            sb.append('\n').append(m.toString()).append('\n');
            sb.append("Synthetic: ").append(m.isSynthetic()).append('\n');
            sb.append("Bridge: ").append(m.isBridge()).append('\n');
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                sb.append("Default: ").append(m.isDefault()).append('\n');
            }
            try {
                String s = (String) m.invoke(test);
                sb.append("Result: ").append(s).append('\n');
            } catch (InvocationTargetException e) {
                sb.append("Target exception: ").append(e.getTargetException()).append('\n');
            } catch (IllegalAccessException e) {
                sb.append("Illegal access.\n");
            }
        }

        System.out.println(sb);

        TextView textView = findViewById(R.id.textView);
        textView.setText(sb.toString());
    }
}

在使用 SDK 版本 24 或更高版本的設備上安裝新 APK 時,這會產生以下輸出:

Methods:

public java.lang.String com.example.testapp.TestClass.methodWithDefault()
Synthetic: true
Bridge: false
Default: false
Result: default

public java.lang.String com.example.testapp.TestClass.methodWithoutDefault()
Synthetic: false
Bridge: false
Default: false
Result: non-default

令人困惑的是,該方法標記為默認方法。 但它被標記為合成的,所以:

2. 我們可以通過Method.isSynthetic()過濾掉繼承的方法。

所以,回答我們的問題:

  1. 為什么會這樣?

我想合成方法是在 minSdk 小於 24 時生成的,因為 APK可以安裝在舊的 Android 設備上,它的 JVM 中沒有對默認接口方法的“直接”支持。

當直接通過 Android Studio 安裝應用程序時,我猜 minSdk 值被忽略了,因為 Android Studio 可以檢查設備的實際版本是什么。

如果有人有更准確的信息,請分享。

  1. 解決它的最佳方法是什么?

可以通過對它們調用isSynthetic()來過濾掉繼承的默認方法,這將返回true

如果你想保留一些其他的合成方法,只過濾掉這些方法,我不知道如何實現,但這種情況應該是非常罕見的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM