Tải bản đầy đủ (.pdf) (20 trang)

Động lực học lập trình Java, Phần 2: Giới thiệu sự phản chiếu Sử dụng thông tin lớp trong thời gian chạy để khởi động việc lập trình của bạn pps

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (259.94 KB, 20 trang )

Động lực học lập trình Java, Phần 2: Giới thiệu sự phản chiếu
Sử dụng thông tin lớp trong thời gian chạy để khởi động việc lập trình của bạn
Dennis Sosnoski, Nhà tư vấn, Sosnoski Software Solutions, Inc.
Tóm tắt: Sự phản chiếu cho phép truy cập mã của bạn tới thông tin bên trong đối
với các lớp được nạp vào JVM và cho phép bạn viết mã để làm việc với các lớp
được lựa chọn trong quá trình thực hiện, không phải trong mã nguồn. Điều này tạo
cho sự phản chiếu một công cụ quan trọng để xây dựng các ứng dụng linh hoạt.
Nhưng xem ra nếu được sử dụng không thích hợp, sự phản chiếu có thể tốn
kém. Trong Phần 2 của loạt bài của mình về bản chất của nền tảng Java, nhà tư
vấn phần mềm Dennis Sosnoski đưa ra một sự giới thiệu về cách sử dụng sự phản
chiếu, cũng như xem xét một số các chi phí liên quan. Bạn cũng sẽ tìm hiểu cách
Java Reflection API (API phản chiếu Java) cho phép bạn kết nối vào các đối tượng
trong thời gian chạy.
Trong "Động lực học lập trình Java, Phần 1," tôi đã cung cấp cho bạn một sự giới
thiệu về các lớp lập trình Java và nạp lớp. Bài viết đó mô tả một số tư liệu thông
tin rộng lớn theo định dạng lớp nhị phân Java. Trong bài viết tháng này, tôi sẽ
trình bày những điều cơ bản về việc sử dụng Java Reflection API để truy cập và sử
dụng một số thông tin như vậy trong thời gian chạy. Để giúp duy trì những điều
này thú vị với các nhà phát triển, những người đã biết những điều cơ bản của sự
phản chiếu, tôi sẽ trình bày một cái nhìn về cách so sánh hiệu năng phản chiếu với
truy cập trực tiếp.
Đừng bỏ lỡ phần còn lại của loạt bài này
Phần 1, "Các lớp Java và nạp lớp" (04.2003)

Phần 3, "Ứng dụng sự phản chiếu" (07.2003)

Phần 4, "Chuyển đổi lớp bằng Javassist" (09.2003)

Phần 5, "Việc chuyển các lớp đang hoạt động" (02.2004)

Phần 6, "Các thay đổi hướng-khía cạnh với Javassist" (03.2004)



Phần 7, "Kỹ thuật bytecode với BCEL" (04.2004)

Phần 8, "Thay thế sự phản chiếu bằng việc tạo mã" (06.2004)
Sử dụng sự phản chiếu khác với lập trình Java tiêu chuẩn ở chỗ nó làm việc với
siêu dữ liệu dữ liệu mô tả dữ liệu khác. Kiểu siêu dữ liệu cụ thể được truy cập
bởi sự phản chiếu của ngôn ngữ Java là sự mô tả về các lớp và các đối tượng bên
trong JVM. Sự phản chiếu cho phép bạn truy cập trong thời gian chạy đến một loạt
các thông tin lớp. Thậm chí nó còn cho phép bạn đọc và viết các trường và các
phương thức gọi của một lớp được chọn trong thời gian chạy.
Sự phản chiếu là một công cụ mạnh. Nó cho phép bạn xây dựng mã linh hoạt, mã
này có thể được lắp ráp trong thời gian chạy mà không đòi hỏi các liên kết mã
nguồn giữa các thành phần. Nhưng một số khía cạnh của sự phản chiếu có thể khó
hiểu. Trong bài này, tôi sẽ đi vào những lý do tại sao bạn có thể không muốn sử
dụng sự phản chiếu trong các chương trình của bạn, cũng như những lý do tại sao
bạn muốn. Sau khi bạn biết các sự thỏa hiệp, bạn có thể quyết định cho chính mình
khi những lợi ích có giá trị hơn những hạn chế.
Lớp của những người mới bắt đầu
Điểm khởi đầu để sử dụng sự phản chiếu luôn luôn là một cá thể java.lang.Class.
Nếu bạn muốn làm việc với một lớp định sẵn, thì ngôn ngữ Java cung cấp một
phím tắt dễ dàng để có được cá thể Class trực tiếp:
Class clas = MyClass.class;

Khi bạn sử dụng kỹ thuật này, tất cả các công việc liên quan đến việc nạp các lớp
diễn ra ở hậu trường. Tuy nhiên, nếu bạn cần phải đọc tên lớp trong thời gian chạy
từ một số nguồn bên ngoài, thì cách tiếp cận này không phải là sắp thực hiện. Thay
vào đó, bạn cần phải sử dụng một trình nạp lớp để tìm thông tin lớp. Dưới đây là
một cách để thực hiện điều đó:
// "name" is the class name to load
Class clas = null;

try {
clas = Class.forName(name);
} catch (ClassNotFoundException ex) {
// handle exception case
}
// use the loaded class

Nếu lớp đã được nạp, bạn sẽ tìm lại các thông tin Class hiện có. Nếu lớp chưa
được nạp, trình nạp lớp sẽ nạp nó bây giờ và trả về cá thể lớp vừa mới được xây
dựng.


Sự phản chiếu trên một lớp
Đối tượng Class mang đến cho bạn tất cả các kết nối cơ bản để truy cập phản
chiếu đến siêu dữ liệu lớp. Siêu dữ liệu này bao gồm các thông tin về chính lớp đó,
chẳng hạn như gói và siêu lớp của lớp đó, cũng như các giao diện được lớp đó
triển khai thực hiện. Nó cũng bao gồm các chi tiết về các hàm tạo, các trường và
các phương thức được lớp đó xác định. Các mục sau cùng này là những thứ hầu
như thường được sử dụng trong lập trình, vì vậy tôi sẽ đưa ra một số ví dụ về làm
việc với chúng sau trong phần này.
Hỏi chuyên gia: Dennis Sosnoski về các vấn đề JVM và bytecode
Đối với các ý kiến hay các câu hỏi về tài liệu được trình bày trong loạt bài này,
cũng như bất cứ điều gì khác có liên quan đến Java bytecode, định dạng lớp nhị
phân Java hoặc các vấn đề JVM chung, hãy truy cập vào diễn đàn thảo luận JVM
và Bytecode, do Dennis Sosnoski kiểm soát.
Đối với mỗi một trong ba kiểu này của các thành phần lớp các hàm tạo
(constructor), các trường và các phương thức java.lang.Class cung cấp bốn cuộc
gọi thể hiện sự phản chiếu riêng biệt để truy cập thông tin theo nhiều cách khác
nhau. Tất cả các cuộc gọi đi theo sau một dạng chuẩn. Đây là một tập được sử
dụng để tìm các hàm tạo:

 Constructor getConstructor(Class[] params) Tìm ra hàm tạo công khai
bằng cách sử dụng các kiểu tham số cụ thể.
 Constructor[] getConstructors() Tìm ra tất cả các hàm tạo công khai cho
lớp đó.
 Constructor getDeclaredConstructor(Class[] params) Tìm ra hàm tạo (bất
kể mức truy cập) bằng cách sử dụng các kiểu tham số cụ thể.
 Constructor[] getDeclaredConstructors() Tìm ra tất cả các hàm tạo (bất
kể mức truy cập) cho lớp đó.
Mỗi một trong các cuộc gọi này trả về một hoặc nhiều cá thể
java.lang.reflect.Constructor. Lớp Constructor này định nghĩa một phương thức
newInstance lấy một mảng các đối tượng làm đối số duy nhất của nó, sau đó trả về
một cá thể vừa được xây dựng của lớp gốc. Mảng các đối tượng là các giá trị tham
số sử dụng cho cuộc gọi hàm tạo. Như là một ví dụ về cách làm việc này, giả sử
bạn có một lớp TwoString với một hàm tạo lấy một cặp String, như thể hiện trong
Liệt kê 1:

Liệt kê 1. Lớp được xây dựng từ cặp strings
public class TwoString {
private String m_s1, m_s2;
public TwoString(String s1, String s2) {
m_s1 = s1;
m_s2 = s2;
}
}

Mã được hiển thị trong Liệt kê 2 tìm ra hàm tạo và sử dụng nó để tạo một cá thể
của lớp TwoString khi sử dụng Strings "a" và "b":

Liệt kê 2. Cuộc gọi sự phản chiếu cho hàm tạo
Class[] types = new Class[] { String.class, String.class };

Constructor cons = TwoString.class.getConstructor(types);
Object[] args = new Object[] { "a", "b" };
TwoString ts = (TwoString)cons.newInstance(args);

Mã trong Liệt kê 2 bỏ qua một số kiểu có thể của các ngoại lệ đã kiểm tra được
các phương thức phản chiếu khác nhau đưa ra. Các ngoại lệ này được trình bày chi
tiết trong các mô tả Javadoc API, vậy để cho ngắn gọn, tôi để chúng ở ngoài các ví
dụ này.
Trong khi tôi đang nói chủ đề về các hàm tạo, ngôn ngữ lập trình Java cũng định
nghĩa một phương thức phím tắt đặc biệt mà bạn có thể sử dụng để tạo một cá thể
của một lớp bằng một hàm tạo no-argument (hoặc mặc định). Phím tắt này được
nhúng vào trong định nghĩa Class riêng của nó như sau:
Object newInstance() Xây dựng cá thể mới khi sử dụng hàm tạo mặc định
Mặc dù cách tiếp cận này chỉ cho phép bạn sử dụng một hàm tạo cụ thể, nó tạo
một phím tắt rất tiện lợi nếu đó là một thứ bạn muốn. Kỹ thuật này đặc biệt có ích
khi làm việc với JavaBeans, JavaBeans được dùng để xác định một hàm tạo công
khai, không có đối số (no-argument).
Các trường của sự phản chiếu
Các cuộc gọi phản chiếu Class (lớp) nhằm truy cập thông tin về trường là tương tự
như các cuộc gọi được dùng để truy cập các hàm tạo, với tên trường được sử dụng
thay cho một mảng của các kiểu tham số:
 Field getField(String name) Tìm ra trường công khai có tên.
 Field[] getFields() Tìm ra tất cả các trường công khai của lớp đó.
 Field getDeclaredField(String name) Tìm ra trường có tên được lớp đó
khai báo.
 Field[] getDeclaredFields() Tìm ra tất cả các trường được lớp đó khai
báo.
Mặc dù có sự tương đồng với các cuộc gọi hàm tạo, cũng có một sự khác biệt quan
trọng khi nói đến các trường: hai trường đầu tiên trả về các thông tin cho các
trường công khai để có thể truy cập chúng thông qua lớp đó ngay cả những lớp

được thừa kế từ một lớp ông bà. Hai trường cuối trả về các thông tin cho các
trường được lớp đó khai báo trực tiếp không phân biệt các kiểu truy cập của
trường.
Các cá thể java.lang.reflect.Field được các cuộc gọi trả về định nghĩa các phương
thức getXXX và setXXX cho tất cả các kiểu nguyên thủy, cũng như các phương
thức get và set chung làm việc với các tham chiếu đối tượng. Nó cho bạn quyết
định sử dụng một phương thức thích hợp dựa trên kiểu trường thực tế, mặc dù các
phương thức getXXX sẽ xử lý tự động các biến đổi mở rộng (như khi sử dụng
phương thức getInt để lấy ra một giá trị byte).
Liệt kê 3 cho thấy một ví dụ về việc sử dụng các phương thức phản chiếu trường,
dưới dạng một phương thức để tăng một trường int của một đối tượng theo tên:

Liệt kê 3. Tăng một trường bằng sự phản chiếu
public int incrementField(String name, Object obj) throws {
Field field = obj.getClass().getDeclaredField(name);
int value = field.getInt(obj) + 1;
field.setInt(obj, value);
return value;
}

Phương thức này bắt đầu hiển thị một số tính linh hoạt có thể với sự phản chiếu.
Thay vì làm việc với một lớp cụ thể, incrementField sử dụng phương thức
getClass của đối tượng được chuyển qua để tìm thông tin lớp, sau đó trực tiếp tìm
trường có tên trong lớp đó.
Các phương thức của sự phản chiếu
Sự phản chiếu Class gọi truy cập thông tin của phương thức rất giống với những
sự phản chiếu được sử dụng cho các hàm tạo và các trường:
 Method getMethod(String name, Class[] params) Tìm ra phương thức
công khai có tên bằng cách sử dụng các kiểu tham số cụ thể.
 Method[] getMethods() Tìm ra tất cả các phương thức công khai của lớp.

 Method getDeclaredMethod(String name, Class[] params) Tìm ra
phương thức công khai có tên được lớp đó khai báo bằng cách sử dụng các
kiểu tham số cụ thể.
 Method[] getDeclaredMethods() Tìm ra tất cả các phương thức được lớp
đó khai báo.
Như với các cuộc gọi trường, hai phương thức đầu tiên trả về thông tin cho các
phương thức công khai có thể được truy cập thông qua lớp đó, ngay cả các lớp đó
được thừa kế từ một lớp ông bà. Hai phương thức cuối trả về thông tin cho các
phương thức được lớp đó khai báo trực tiếp, mà không liên quan đến kiểu truy cập
của phương thức này.
Các cá thể java.lang.reflect.Method được các cuộc gọi trả về định nghĩa một
phương thức invoke (gọi) mà bạn có thể sử dụng để gọi phương thức đó trên một
cá thể của lớp định nghĩa. Phương thức invoke này lấy hai đối số cung cấp cá thể
lớp và một mảng các giá trị tham số cho cuộc gọi này.
Liệt kê 4 đưa ví dụ về trường tiến thêm một bước, khi hiển thị một ví dụ về sự
phản chiếu của phương thức đang hành động. Phương thức này làm tăng một
thuộc tính int JavaBean được xác định bằng phương thức get và set. Ví dụ, nếu đối
tượng đã xác định phương thức getCount và setCount cho một giá trị count (đếm)
số nguyên, thì bạn có thể vượt qua "count" như tham số name trong một cuộc gọi
đến phương thức này để tăng giá trị đó.

Liệt kê 4. Làm tăng một thuộc tính JavaBean bằng sự phản chiếu
public int incrementProperty(String name, Object obj) {
String prop = Character.toUpperCase(name.charAt(0)) +
name.substring(1);
String mname = "get" + prop;
Class[] types = new Class[] {};
Method method = obj.getClass().getMethod(mname, types);
Object result = method.invoke(obj, new Object[0]);
int value = ((Integer)result).intValue() + 1;

mname = "set" + prop;
types = new Class[] { int.class };
method = obj.getClass().getMethod(mname, types);
method.invoke(obj, new Object[] { new Integer(value) });
return value;
}

Để thực hiện theo các quy ước JavaBeans, tôi biến đổi chữ cái đầu của thuộc tính
tên thành chữ hoa, sau đó dựa vào get để xây dựng tên phương thức đọc và set để
xây dựng tên phương thức viết. Các phương thức đọc JavaBeans chỉ trả về giá trị
và viết các phương thức lấy giá trị làm tham số duy nhất, vì vậy tôi chỉ định các
kiểu tham số cho các phương thức cho phù hợp. Cuối cùng, quy ước đòi hỏi các
phương thức là công khai, vậy tôi sử dụng dạng tra cứu thông tin để tìm các
phương thức công khai có khả năng gọi được trên lớp đó.
Ví dụ này là một ví dụ đầu tiên mà tôi đã chuyển qua các giá trị nguyên thủy khi
sử dụng sự phản chiếu, vậy chúng ta hãy xem xét cách làm này. Nguyên tắc cơ
bản rất đơn giản: bất cứ khi nào bạn cần phải chuyển qua một giá trị nguyên thủy,
chỉ cần thay thế một cá thể của lớp trình bao (wrapper) tương ứng (được định
nghĩa trong gói java.lang cho kiểu nguyên thủy đó. Điều này áp dụng cho cả các
cuộc gọi và cả các trả về. Vì vậy, khi tôi gọi phương thức get trong ví dụ của tôi,
tôi chờ đợi kết quả là một trình bao java.lang.Integer cho giá trị thuộc tính int thực
sự.
Phản chiếu các mảng
Các mảng là các đối tượng trong ngôn ngữ lập trình Java. Giống như tất cả các đối
tượng, chúng có các lớp. Nếu bạn có một mảng, bạn có thể nhận được lớp của
mảng đó khi sử dụng phương thức getClass chuẩn, cũng giống như với bất kỳ đối
tượng khác. Tuy nhiên, việc nhận được lớp đó mà không có một cá thể hiện có làm
việc khác với các kiểu đối tượng khác. Ngay cả sau khi bạn có một lớp mảng
không có nhiều lớp bạn có thể làm việc trực tiếp với nó, việc truy cập hàm tạo
được sự phản chiếu cho các lớp thông thường đưa ra không làm việc với các mảng

và các mảng không có bất kỳ các trường nào dễ truy cập. Các phương thức
java.lang.Object cơ bản chỉ được định nghĩa cho đối tượng mảng.
Việc xử lý đặc biệt của các mảng sử dụng một tập hợp các phương thức tĩnh được
lớp java.lang.reflect.Array cung cấp. Các phương thức trong lớp này cho phép bạn
tạo các mảng mới, nhận được chiều dài của một đối tượng mảng và đọc và viết các
giá trị có chỉ mục của một đối tượng mảng.
Liệt kê 5 cho thấy một phương thức hữu ích để thay đổi kích thước một mảng hiện
có một cách hiệu quả. Nó sử dụng sự phản chiếu để tạo một mảng mới cùng kiểu,
sau đó sao chép tất cả các dữ liệu suốt từ mảng cũ trước khi trả về mảng mới.

Liệt kê 5. Phát triển một mảng bằng sự phản chiếu
public Object growArray(Object array, int size) {
Class type = array.getClass().getComponentType();
Object grown = Array.newInstance(type, size);
System.arraycopy(array, 0, grown, 0,
Math.min(Array.getLength(array), size));
return grown;
}



An ninh và sự phản chiếu
An ninh có thể là một vấn đề phức tạp khi đối phó với sự phản chiếu. Mã kiểu-
khung công tác thường sử dụng sự phản chiếu và với điều này bạn có thể muốn
khung công tác có truy cập đầy đủ tới mã của bạn mà không cần quan tâm về các
hạn chế truy cập thông thường. Tuy vậy việc truy cập không kiểm soát được có thể
tạo những nguy cơ an ninh chính trong các trường hợp khác, chẳng hạn như khi
mã được thi hành trong một môi trường được chia sẻ bởi mã không đáng tin cậy.
Do các nhu cầu xung đột nhau, nên ngôn ngữ lập trình Java định nghĩa một cách
tiếp cận đa cấp để xử lý an ninh phản chiếu. Các chế độ cơ bản là bắt tuân theo các

hạn chế như nhau trên sự phản chiếu như đã áp dụng cho việc truy cập mã nguồn:
 Truy cập từ bất cứ ở đâu tới các thành phần công khai của lớp.
 Không truy cập bên ngoài lớp riêng của nó tới các thành phần riêng.
 Truy cập có giới hạn tới các thành phần được bảo vệ và các thành phần gói
(truy cập mặc định).
Tuy nhiên có một cách đơn giản xung quanh các hạn chế này thỉnh thoảng có.
Tất cả các lớp Constructor, Field, và Method mà tôi đã sử dụng trong các ví dụ
trước đó mở rộng một lớp cơ sở chung lớp java.lang.reflect.AccessibleObject.
Lớp này định nghĩa một phương thức setAccessible cho phép bạn bật hoặc tắt
kiểm tra truy cập cho một cá thể của một trong những lớp này. Việc bắt giữ duy
nhất (catch) là nếu có một trình quản lý an ninh, nó sẽ kiểm tra xem mã tắt kiểm
tra truy cập có cho phép làm như vậy không. Nếu không cho phép, trình quản lý an
ninh đưa ra một lỗi ngoại lệ.
Liệt kê 6 giải thích một chương trình có sử dụng sự phản chiếu trên một cá thể của
lớp TwoString của Liệt kê 1 để hiển thị điều này đang hoạt động:

Liệt kê 6. An ninh phản chiếu đang hoạt động
public class ReflectSecurity {
public static void main(String[] args) {
try {
TwoString ts = new TwoString("a", "b");
Field field = clas.getDeclaredField("m_s1");
// field.setAccessible(true);
System.out.println("Retrieved value is " +
field.get(inst));
} catch (Exception ex) {
ex.printStackTrace(System.out);
}
}
}


Nếu bạn biên dịch mã này và chạy nó trực tiếp từ dòng lệnh mà không có bất kỳ
tham số đặc biệt nào, nó sẽ đưa ra một IllegalAccessException trên cuộc gọi
field.get(inst). Nếu bạn không ghi chú dòng field.setAccessible(true) ), sau đó biên
dịch lại và chạy mã đó, nó sẽ thành công. Cuối cùng, nếu bạn thêm tham số JVM -
Djava.security.manager trên dòng lệnh để kích hoạt một trình quản lý an ninh, một
lần nữa nó sẽ không thành công, trừ khi bạn xác định các quyền truy cập cho lớp
ReflectSecurity.


Hiệu năng phản chiếu
Sự phản chiếu là một công cụ mạnh, nhưng bị một vài hạn chế. Một trong những
nhược điểm chính là ảnh hưởng về hiệu năng. Khi sử dụng sự phản chiếu về cơ
bản là một hoạt động được giải thích, ở đó bạn ra lệnh cho JVM những gì bạn
muốn làm và nó thực hiện nó cho bạn. Kiểu hoạt động này sẽ luôn chậm hơn so
với việc thực hiện trực tiếp cùng một hoạt động. Để giải thích các chi phí hiệu
năng về việc sử dụng phản chiếu, tôi đã chuẩn bị một tập các chương trình chuẩn
cho bài này (xem Tài nguyên để có một đường liên kết đến mã đầy đủ).
Liệt kê 7 cho thấy một đoạn trích từ việc thử nghiệm hiệu năng truy cập trường,
bao gồm các phương pháp thử nghiệm cơ bản. Mỗi phương thức thử nghiệm một
dạng truy cập vào các trường accessSame làm việc với các trường thành viên
của cùng một đối tượng, accessOther sử dụng các trường của đối tượng khác được
truy cập trực tiếp và accessReflection sử dụng các trường của đối tượng khác được
sự phản chiếu truy cập. Trong mỗi trường hợp, các phương pháp thực hiện các tính
toán như nhau một chuỗi cộng/nhân đơn giản trong một vòng lặp.

Liệt kê 7. Mã thử nghiệm hiệu năng truy cập trường
public int accessSame(int loops) {
m_value = 0;
for (int index = 0; index < loops; index++) {

m_value = (m_value + ADDITIVE_VALUE) *
MULTIPLIER_VALUE;
}
return m_value;
}

public int accessReference(int loops) {
TimingClass timing = new TimingClass();
for (int index = 0; index < loops; index++) {
timing.m_value = (timing.m_value + ADDITIVE_VALUE) *
MULTIPLIER_VALUE;
}
return timing.m_value;
}

public int accessReflection(int loops) throws Exception {
TimingClass timing = new TimingClass();
try {
Field field = TimingClass.class.
getDeclaredField("m_value");
for (int index = 0; index < loops; index++) {
int value = (field.getInt(timing) +
ADDITIVE_VALUE) * MULTIPLIER_VALUE;
field.setInt(timing, value);
}
return timing.m_value;
} catch (Exception ex) {
System.out.println("Error using reflection");
throw ex;
}

}

Chương trình thử nghiệm gọi mỗi phương thức lặp lại nhiều lần với số đếm vòng
lặp lớn, lấy trung bình các phép đo thời gian trên một số các cuộc gọi. Thời gian
cho cuộc gọi đầu tiên tới mỗi phương thức không được bao gồm trong giá trị trung
bình, để cho thời gian khởi chạy không phải là một tham số trong các kết quả.
Trong các hoạt động thử nghiệm cho bài viết này, tôi đã sử dụng một số đếm vòng
lặp là 10 triệu cho mỗi cuộc gọi, chạy trên một hệ thống PIIIm 1GHz. Các kết quả
tạo thời gian của tôi với ba JVM Linux khác nhau được thể hiện trong Hình 1. Tất
cả các thử nghiệm đã sử dụng các giá trị cài đặt mặc định cho từng JVM.

Hình 1. Các thời gian truy cập trường

Thang đo logarit của biểu đồ trên hiển thị toàn bộ dải thời gian, nhưng làm giảm
tác động trực quan của các sự khác biệt. Trong trường hợp của hai tập hợp số liệu
đầu tiên (các Sun JVM), thời gian thực hiện khi sử dụng sự phản chiếu là lớn hơn
1000 lần so với khi sử dụng truy cập trực tiếp. IBM JVM theo so sánh tốt hơn một
chút, nhưng phương thức phản chiếu vẫn còn mất dài hơn 700 lần so với các
phương thức khác. Không có các sự khác biệt đáng kể nào theo thời gian giữa hai
phương thức khác trên bất kỳ JVM nào, mặc dù IBM JVM đã chạy nhanh gần như
gấp đôi so với các Sun JVM. Có khả năng, sự khác biệt này phản chiếu sự tối ưu
hóa chuyên dụng được các Sun Hot Spot JVM sử dụng, chúng có xu hướng thực
hiện không tốt theo tiêu chuẩn đơn giản.
Bên cạnh các thử nghiệm thời gian truy cập trường, tôi đã thực hiện cùng một loại
thử nghiệm tính giờ cho các cuộc gọi phương thức. Với các cuộc gọi phương thức,
tôi đã thử ba sự thay đổi truy cập giống như với việc truy cập trường, với biến
thêm vào cho việc sử dụng các phương thức không đối số so với cho qua và trả về
một giá trị theo các cuộc gọi phương thức. Liệt kê 8 cho thấy mã với ba phương
thức được sử dụng để thử nghiệm dạng giá trị được cho qua và trả về của các cuộc
gọi.


Liệt kê 8. Mã thử nghiệm hiệu năng truy cập phương thức
public int callDirectArgs(int loops) {
int value = 0;
for (int index = 0; index < loops; index++) {
value = step(value);
}
return value;
}

public int callReferenceArgs(int loops) {
TimingClass timing = new TimingClass();
int value = 0;
for (int index = 0; index < loops; index++) {
value = timing.step(value);
}
return value;
}

public int callReflectArgs(int loops) throws Exception {
TimingClass timing = new TimingClass();
try {
Method method = TimingClass.class.getMethod
("step", new Class [] { int.class });
Object[] args = new Object[1];
Object value = new Integer(0);
for (int index = 0; index < loops; index++) {
args[0] = value;
value = method.invoke(timing, args);
}

return ((Integer)value).intValue();
} catch (Exception ex) {
System.out.println("Error using reflection");
throw ex;
}
}

Hình 2 cho thấy kết quả tính thời gian của tôi cho các cuộc gọi phương thức. Ở
đây một lần nữa, sự phản chiếu chậm hơn nhiều so với khả năng trực tiếp. Các
khác biệt này không khá lớn như với trường hợp truy cập trường, mặc dù có phạm
vi từ chậm hơn vài trăm lần trên Sun 1.3.1 JVM đến ít hơn 30 lần chậm hơn trên
IBM JVM cho trường hợp không đối số. Hiệu năng thử nghiệm cho các cuộc gọi
phương thức phản chiếu với các đối số là chậm hơn đáng kể so với các cuộc gọi
không đối số trên tất cả các JVM. Điều này một phần có lẽ là do trình bao
java.lang.Integer cần thiết cho giá trị int được chuyển qua và được trả về. Do các
Integer là không thay đổi, một cái mới cần được tạo cho mỗi lần trả về phương
thức, bổ sung thêm chi phí hoạt động đáng kể.

Hình 2. Các thời gian gọi phương thức

Hiệu năng phản chiếu đã là một vùng trọng tâm cho Sun khi phát triển các JVM
1.4, nó cho thấy trong các kết quả gọi phương thức phản chiếu. Sun JVM 1.4.1
cho thấy hiệu năng được cải thiện đáng kể so với phiên bản 1.3.1 cho kiểu hoạt
động này, chạy nhanh hơn khoảng bảy lần trong các lần thử nghiệm của tôi. IBM
1.4.0 JVM lại đưa ra hiệu năng tốt hơn cho lần thử nghiệm đơn giản này, tuy vậy,
chạy nhanh hơn 2 đến 3 lần so với Sun 1.4.1 JVM.
Tôi cũng đã viết một chương trình thử nghiệm tính giờ tương tự để tạo các đối
tượng sử dụng phản chiếu. Tuy nhiên, các sự khác nhau với trường hợp này gần
như không đáng kể như với trường hợp gọi trường và trường hợp gọi phương thức.
Việc xây dựng một cá thể java.lang.Object đơn giản với một cuộc gọi

newInstance() tốn thời gian nhiều hơn khoảng 12 lần so với việc sử dụng new
Object() trên Sun 1.3.1 JVM, nhiều hơn khoảng bốn lần trên IBM 1.4.0 JVM và
chỉ nhiều hơn khoảng hai lần trên Sun 1.4.1 JVM. Việc xây dựng một mảng khi sử
dụng Array.newInstance(type, size) phải mất tối đa là khoảng hai lần dài hơn khi
sử dụng new type[size] với bất kỳ JVM thử nghiệm nào, với sự khác biệt giảm
xuống khi kích thước mảng tăng lên.


Tóm tắt phản chiếu
Sự phản chiếu của ngôn ngữ Java đưa ra một cách rất linh hoạt về các thành phần
của chương trình liên kết động. Nó cho phép chương trình của bạn tạo và vận hành
các đối tượng của bất kỳ các lớp (dễ bị các hạn chế về an ninh) mà không cần phải
mã cố định (hardcode) các lớp đích trước thời hạn. Các tính năng này làm cho sự
phản chiếu đặc biệt có ích để tạo các thư viện làm việc với các đối tượng theo
những cách rất chung chung. Ví dụ sự phản chiếu thường được sử dụng trong
khung công tác vẫn tồn tại đối tượng cho các cơ sở dữ liệu, XML, hoặc các định
dạng bên ngoài khác.
Sự phản chiếu cũng có hai hạn chế. Một là vấn đề hiệu năng. Sự phản chiếu chậm
hơn nhiều so với mã trực tiếp khi sử dụng để truy cập trường và phương thức. Các
vấn đề này ở mức độ nào đó phụ thuộc vào cách sự phản chiếu được sử dụng trong
một chương trình. Nếu nó được sử dụng như một phần tương đối ít xảy ra về hoạt
động của chương trình, thì hiệu năng chậm sẽ không phải là một mối quan tâm.
Ngay cả các số liệu thời gian trong trường hợp xấu nhất trong thời gian thử
nghiệm của tôi đã cho thấy các hoạt động phản chiếu chỉ mất có một vài micro
giây. Các vấn đề về hiệu năng chỉ trở thành một mối quan tâm thật sự nếu sự phản
chiếu được sử dụng trong logic cốt lõi của các ứng dụng hiệu năng-tới hạn.
Một hạn chế đáng sợ hơn cho nhiều ứng dụng là việc sử dụng sự phản chiếu có thể
che khuất những gì thực sự sẽ xảy ra bên trong mã của bạn. Các lập trình viên
mong đợi nhìn thấy logic của một chương trình trong mã nguồn và các kỹ thuật
như là sự phản chiếu mà nó bỏ qua các mã nguồn có thể tạo những vấn đề về bảo

trì. Mã phản chiếu cũng phức tạp hơn so với các mã trực tiếp tương ứng, như có
thể được thấy trong các ví dụ mã từ các so sánh hiệu năng. Các cách tốt nhất để
đối phó với những vấn đề này là sử dụng phản chiếu ít đi chỉ ở những nơi mà nó
thực sự bổ sung thêm tính linh hoạt có ích và dẫn chứng tài liệu sử dụng của nó
bên trong các lớp đích.
Trong lần cài đặt tiếp theo, tôi sẽ cho ví dụ chi tiết hơn về việc sử dụng sự phản
chiếu như thế nào. Ví dụ này cung cấp một API để xử lý các tham số trên dòng
lệnh của một ứng dụng Java, một công cụ bạn có thể thấy có ích cho ứng dụng của
riêng bạn. Nó cũng được xây dựng trên những thế mạnh của sự phản chiếu trong
khi tránh được các điểm yếu. Sự phản chiếu có làm đơn giản hóa việc xử lý dòng
lệnh của bạn? Tìm thấy trong phần 3 của loạt bài Động lực học lập trình Java .


Mục lục

 Lớp của những người mới bắt đầu
 Sự phản chiếu trên một lớp
 An ninh và sự phản chiếu
 Hiệu năng phản chiếu
 Tóm tắt phản chiếu

×