Độ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

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.

Dennis Sosnoski, Nhà tư vấn, Sosnoski Software Solutions, Inc.

Dennis Sosnoski là một nhà tư vấn và nhà trợ giúp đào tạo chuyên về các dịch vụ Web và SOA dựa trên-Java. Kinh nghiệm phát triển phần mềm chuyên nghiệp của ông trải suốt hơn 30 năm qua, với một thập kỉ cuối tập trung vào các công nghệ XML và Java phía máy chủ. Dennis là nhà phát triển hàng đầu về dụng cụ liên kết dữ liệu XML JiBX mã nguồn mở, cũng là một người có duyên nợ với khung công tác của các dịch vụ Web Apache Axis2. Ông cũng là một trong những thành viên của nhóm chuyên gia đặc tả kỹ thuật của Jax-WS 2.0 và JAXB 2.0. Xem trang web của ông để có thông tin về các dịch vụ đào tạo và tư vấn của ông.



04 12 2009

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""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 getXXXsetXXX cho tất cả các kiểu nguyên thủy, cũng như các phương thức getset 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 getset. Ví dụ, nếu đối tượng đã xác định phương thức getCountsetCount 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
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
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 .


Tải về

Mô tảTênKích thước
Mã ví dụj-dyn0603.zip12KB

Tài nguyên

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Công nghệ Java
ArticleID=452540
ArticleTitle=Động lực học lập trình Java, Phần 2: Giới thiệu sự phản chiếu
publish-date=12042009