Java 反射是不好的做法嗎?
1. 概述
隨著時間的推移,反射 API 的使用在 Java 社群內引發了廣泛的爭論,有時被視為一種不好的做法。雖然它被流行的 Java 框架和庫廣泛使用,但它的潛在缺點阻礙了它在常規伺服器端應用程式中的頻繁使用。
在本教程中,我們將深入研究反射可能為我們的程式碼庫帶來的好處和缺點。此外,我們將探討何時適合或不適合使用反射,最終幫助我們確定它是否屬於不良實踐。
2. 理解Java反射
在電腦科學中,反思性程式設計或反射是一個過程檢查、內省和修改其結構和行為的能力。當程式語言完全支援反射時,它允許在運行時檢查和修改程式碼庫中的類別和物件的結構和行為,從而允許原始程式碼重寫其自身的各個方面。
根據這個定義,Java提供了對反射的全面支援。除了 Java 之外,其他支援反射程式設計的常見程式語言還有 C#、Python 和 JavaScript。
許多流行的 Java 框架(例如 Spring 和 Hibernate ,
都依賴它來提供高級功能,例如依賴注入、面向方面的程式設計和資料庫映射。除了透過框架或函式庫間接使用反射之外,我們還可以藉助java.lang.reflect
套件或 Reflections 函式庫直接使用它。
3.Java反射的優點
如果謹慎使用,Java 反射可以成為一個強大且通用的功能。在本節中,我們將探討反射的一些主要優點以及如何在某些場景中有效地使用它。
3.1.動態配置
反射API支援動態編程,增強應用程式的靈活性和適應性。當我們遇到所需的類別或模組直到運行時才知道的場景時,這一點被證明是有價值的。
此外,透過利用反射的動態功能,開發人員可以建立可以輕鬆即時重新配置的系統,而無需進行大量程式碼變更。
例如,Spring 框架使用反射來建立和配置 bean。它掃描類別路徑組件並根據註解和 XML 配置動態實例化和配置 Bean,允許開發人員在不更改原始程式碼的情況下新增或修改 Bean。
3.2.可擴展性
使用反射的另一個顯著優點是可擴展性。這使我們能夠在運行時合併新的功能或模組,而無需更改應用程式的核心程式碼。
為了說明這一點,假設我們正在使用一個第三方庫,該庫定義了一個基類並合併了多個子類型以進行多態反序列化。我們希望透過引入擴展相同基類的自訂子類型來擴展功能。 Reflection API
對於這個特定的用例非常有用,因為我們可以利用它在運行時動態註冊這些自訂子類型,並輕鬆地將它們與第三方程式庫整合。因此,我們可以使庫適應我們的特定要求,而無需更改其程式碼庫。
3.3.程式碼分析
反射的另一個用例是程式碼分析,它允許我們動態檢查程式碼。這特別有用,因為它可以提高軟體開發的品質。
例如,ArchUnit(一個用於架構單元測試的 Java 函式庫)利用反射和字節碼分析。庫無法透過Reflection API取得的資訊是在字節碼層級取得的。透過這種方式,該程式庫動態分析程式碼,我們能夠強制執行架構規則和約束,確保我們軟體專案的完整性和高品質。
4. Java 反射的缺點
正如我們在上一節中看到的,反射對於各種應用程式來說都是一個強大的功能。然而,它有一系列缺點,在我們決定在專案中使用它之前我們需要考慮這些缺點。在本節中,我們將深入研究此功能的一些主要缺點。
4.1.效能開銷
Java 反射動態解析類型並可能限制某些 JVM 最佳化。因此,反射操作的性能比非反射操作慢。所以,在處理效能敏感的應用程式時,我們應該考慮避免在頻繁呼叫的程式碼部分中進行反射。
為了示範這一點,我們將建立一個非常簡單的Person
類別並對其執行一些反射和非反射操作:
public class Person {
private String firstName;
private String lastName;
private Integer age;
public Person(String firstName, String lastName, Integer age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
// standard getters and setters
}
現在,我們可以建立一個基準來查看呼叫我們類別的getters
的時間差異:
public class MethodInvocationBenchmark {
@Benchmark
@Fork(value = 1, warmups = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void directCall() {
directCall(new Person("John", "Doe", 50));
}
@Benchmark
@Fork(value = 1, warmups = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void reflectiveCall()
throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
reflectiveCall(new Person("John", "Doe", 50));
}
private void directCall(Person person) {
String firstName = person.getFirstName();
String lastName = person.getLastName();
Integer age = person.getAge();
}
private void reflectiveCall(Person person)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method getFirstNameMethod = Person.class.getMethod("getFirstName");
String firstName = (String) getFirstNameMethod.invoke(person);
Method getLastNameMethod = Person.class.getMethod("getLastName");
String lastName = (String) getLastNameMethod.invoke(person);
Method getAgeMethod = Person.class.getMethod("getAge");
Integer age = (Integer) getAgeMethod.invoke(person);
}
}
讓我們檢查運行方法呼叫基準測試的結果:
Benchmark Mode Cnt Score Error Units
MethodInvocationBenchmark.directCall avgt 5 0.411 ± 0.086 ns/op
MethodInvocationBenchmark.reflectiveCall avgt 5 90.965 ± 0.316 ns/op
現在,讓我們建立另一個基準來測試反射初始化與直接呼叫建構函數相比的效能:
public class InitializationBenchmark {
@Benchmark
@Fork(value = 1, warmups = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void directInit() {
Person person = new Person("John", "Doe", 50);
}
@Benchmark
@Fork(value = 1, warmups = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void reflectiveInit()
throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<Person> constructor = Person.class.getDeclaredConstructor(
String.class, String.class, Integer.class);
Person person = constructor.newInstance("John", "Doe", 50);
}
}
讓我們檢查一下構造函數呼叫的結果:
Benchmark Mode Cnt Score Error Units
InitializationBenchmark.directInit avgt 5 0.402 ± 0.007 ns/op
InitializationBenchmark.reflectiveInit avgt 5 24.594 ± 0.619 ns/op
在查看了兩個基準測試的結果後,我們可以合理地推斷,對於呼叫方法或初始化物件等用例,在 Java 中使用反射可能會相當緩慢。
我們的文章《使用 Java 進行微基準測試》提供了更多關於我們用於比較執行時間的資訊。
4.2.內部結構暴露
反射允許在非反射程式碼中可能受到限制的操作。一個很好的例子是存取和操作類別的私有欄位和方法的能力。這樣做就違反了封裝,這是物件導向程式設計的基本原則。
作為一個例子,讓我們創建一個只有一個私有字段的虛擬類,而不創建任何getters
或setters
:
public class MyClass {
private String veryPrivateField;
public MyClass() {
this.veryPrivateField = "Secret Information";
}
}
現在,讓我們嘗試在單元測試中存取這個私有欄位:
@Test
public void givenPrivateField_whenUsingReflection_thenIsAccessible()
throws IllegalAccessException, NoSuchFieldException {
MyClass myClassInstance = new MyClass();
Field privateField = MyClass.class.getDeclaredField("veryPrivateField");
privateField.setAccessible(true);
String accessedField = privateField.get(myClassInstance).toString();
assertEquals(accessedField, "Secret Information");
}
4.3.失去編譯時安全性
反射的另一個缺點是編譯時安全性的損失。在典型的 Java 開發中,編譯器執行嚴格的類型檢查並確保我們正確使用類別、方法和欄位。然而,反射繞過了這些檢查,因此,一些錯誤直到運行時才被發現。因此,這可能會導致難以檢測的錯誤,並可能損害我們程式碼庫的可靠性。
4.4.程式碼的可維護性降低
使用反射會顯著降低程式碼的可維護性。嚴重依賴反射的程式碼往往比非反射程式碼的可讀性較差。可讀性降低可能會導致維護困難,因為開發人員更難理解程式碼的意圖和功能。
另一個挑戰是有限的工具支援。並非所有開發工具和 IDE 都完全支援反射。因此,這可能會減慢開發速度並使其更容易出錯,因為開發人員必須依靠手動檢查來發現問題。
4.5.安全問題
Java 反射涉及存取和操作程序的內部元素,這可能會導致安全性問題。在限制性環境中,允許反射存取可能存在風險,因為惡意程式碼可能會嘗試利用反射來獲得對敏感資源的未經授權的存取或執行違反安全性原則的操作。
5. Java 9 對反射的影響
Java 9 中模組的引入為模組封裝程式碼的方式帶來了重大變化。在 Java 9 之前,使用反射很容易破壞封裝。
預設情況下,模組不再公開其內部結構。然而,Java 9 提供了一些機制來選擇性地授予模組之間反射存取的權限。這允許我們在必要時打開特定的套件,確保與遺留程式碼或第三方程式庫的兼容性。
6.什麼時候該使用Java反射?
在探索了反射的優點和缺點之後,我們可以確定何時適合或不使用這個強大功能的一些用例。
在動態行為至關重要的情況下,使用 Reflection API 被證明是有價值的。正如我們已經看到的,許多著名的框架和函式庫,例如 Spring 和 Hibernate,都依賴它的關鍵功能。在這些情況下,反射使這些框架能夠為開發人員提供靈活性和客製化。此外,當我們自己創建庫或框架時,反射可以使其他開發人員能夠擴展和自訂他們與我們程式碼的交互,使其成為合適的選擇。
此外,反射可以作為擴展我們無法修改的程式碼的選項。因此,當我們使用第三方程式庫或遺留程式碼並需要整合新功能或調整現有功能而不改變原始程式碼庫時,它可以成為一個強大的工具。它允許我們存取和操作原本無法存取的元素,使其成為此類場景的實用選擇。
但是,在考慮使用反射時要小心謹慎,這一點很重要。在具有強烈安全要求的應用程式中,應謹慎使用反射代碼。反射允許存取程式的內部元素,這可能會被惡意程式碼利用。此外,在處理效能關鍵型應用程式時,特別是在頻繁呼叫的程式碼片段中,反射的效能開銷可能會成為一個問題。此外,如果編譯時類型檢查對我們的專案至關重要,我們應該考慮避免使用反射程式碼,因為它缺乏編譯時安全性。
七、結論
正如我們在整篇文章中所了解的那樣,Java 中的反射應該被視為需要謹慎使用的強大工具,而不是被標記為不良實踐。與任何功能類似,過度使用反射確實可以被認為是一種不好的做法。然而,如果認真地運用並且只有在真正必要的時候,反思就可以成為一筆寶貴的財富。
與往常一樣,原始碼可以 在 GitHub 上取得。