Java – Tìm hiểu về Reflection (Runtime Type Information)

Reflection là kĩ thuật rất cần thiết để lấy các thông tin của một kiểu dữ liệu. Dựa vào đó ta có thể kích hoạt (như các phương thức) hoặc tạo thể hiện của kiểu dữ liệu đó. Một ứng dụng quan trọng của reflection mà bạn có thể biết là Java Bean. Nhờ đó, các IDE (như NetBeans) có thể lấy được các thông tin và thiết lập giá trị cho các đối tượng.

Kiến trúc của Java Reflection API

Các lớp được dùng trong reflection nằm trong hai package là java.langjava.lang.reflect. Package java.lang.reflect bao gồm ba lớp chính mà bạn cần biết là Constructor, Field và Method:

–      Class<T>: lớp này đại diện cho các lớp, interface và chứa các phương thức dùng để lấy các đối tượng kiểu Constructor, Field, Method,…

–      AccessibleObject: các kiểm tra về phạm vi truy xuất (public, private, protected) của field, method, constructor sẽ được bỏ qua. Nhờ đó bạn có thể dùng reflection để thay đổi, thực thi các thành phần này mà không cần quan tâm đến phạm vi truy xuất của nó.

–      Constructor: chứa các thông tin về một constructor của lớp.

–      Field: chứa các thông tin về một field của lớp, interface.

–      Method: chứa các thông tin về một phương thức của lớp, interface.

Java Reflection API

Tạo đối tượng Class<>

Đối tượng kiểu này được tạo ra bằng cách sử dụng phương thức static  Class.forName():

try {

	Class c =  Class.forName("yinyang.Foo");
	// ...
}
catch (ClassNotFoundException e) {
	System.err.println(e);
}

Trong trường hợp không tìm thấy lớp tương ứng, phương thức trên sẽ ném ra ngoại lệ ClassNotFoundException. Điều này có thể bất tiện vì bạn phải sử dụng try catch hoặc ném ngoại lệ này khỏi phương thức.

Vậy bạn có thể dùng cách khác với từ class (class literal), cách này đảm bảo rằng lớp được sử dụng luôn luôn tồn tại và không có ngoại lệ nào xảy ra:

Class c1 = Foo.class;
Class c2 = int.class; // equivalent to Integer.class;

Đối với các kiểu dữ liệu nguyên thủy như void, int, boolean, char,… bạn có thể dùng field TYPE để lấy được đối tượng Class tương ứng, tuy nhiên bạn cần viết đúng tên lớp. Như vậy các dòng lệnh sau cho ra kết quả tương tự nhau:

Class c1 = int.class;
Class c2 = Integer.class;
Class c3 = Integer.TYPE;

Sau đây là một ví dụ đơn giản dùng reflection để in ra các thông tin của lớp:

package yinyang;

import java.lang.reflect.Method;

public class Foo {
	static void Test() throws ClassNotFoundException
	{
		Class c = Class.forName("yinyang.Foo");

		System.out.println("Name: "+c.getName());
		System.out.println("Simple Name: "+c.getSimpleName());
		System.out.println("Declared Methods:");
		for(Method m:c.getDeclaredMethods())
			System.out.println("-"+m);
	}
	public static void main(String[] args)
	{
		try {
			Test();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

Output:

Name: yinyang.Foo

Simple Name: Foo

Declared Methods:

-public static void yinyang.Foo.main(java.lang.String[])

-static void yinyang.Foo.Test() throws java.lang.ClassNotFoundException

Lấy và gán giá trị cho field

Để truy xuất đến một thành viên của lớp nói chung (field, method,…), bạn chỉ cần lấy được đối tượng đại diện tương ứng thông qua các phương thức của Class. Sau đó sử dụng các phương thức của đối tượng đại diện để thao tác.

Trong ví dụ này, ta sẽ sử dụng hai phương thức:

– Class.getField(String name): trả về một đối tượng Field.

– Field.set(Object obj, Object value): gán value cho field tương ứng của đối tượng obj. Bạn có thể thay thế phương thức này bằng các phương thức setXXX() cho mỗi kiểu dữ liệu cụ thể (setInt, setBoolean, setChar,…).

import java.lang.reflect.Field;

public class Foo {

	public int myField=0;

	public static void main(String args[])
	{
		try {
			Foo foo=new Foo();
			System.out.println("Default value: "+foo.myField);
			Class c = Class.forName("Foo");

			Field f=c.getField("myField");
			f.set(foo, 100);
			System.out.println("After changing: " +foo.myField);
		}
		catch (Throwable e) {
			e.printStackTrace();
		}
	}
}

Output:

Default value: 0

After changing: 100

Thực thi một phương thức

Để kích hoạt một phương thức cụ thể, bạn cần sử dụng hai phương thức instance sau:

–      Class.getMethod(String name, Class[] parameterTypes): trả về đối tượng Method đại diện cho một phương thức của lớp. Phương thức này được xác định qua tên và các kiểu tham số.

–      Method.invoke(Object obj, Object[] args) thực thi phương thức tương ứng của đối tượng obj với các tham số args.

Ví dụ sau thực thi phương thức add(int a,int b) của lớp Foo bằng cách tạo một đối tượng Foo và truyền vào làm tham số đầu tiên trong phương thức Method.invoke(). Nếu phương thức add() là static, bạn chỉ cần truyền null vào làm tham số đầu tiên của phương thức Method.invoke().

import java.lang.reflect.Method;

public class Foo {
	 public int add(int a,int b)
     {
		return a+b;
     }

     public static void main(String args[])
     {
        try {
          Class c = Class.forName("Foo");
           Method method = c.getMethod("add",int.class,int.class);

           Object ret= method.invoke(new Foo(), 3,5);
           System.out.println("3+5="+ret);
        }
        catch (Throwable e) {
           e.printStackTrace();
        }
     }
}

Output:

3+5=8

Tạo thể hiện của lớp

Có hai phương thức để tạo một thể hiện của lớp:

–      Class.newInstance(): tạo một đối tượng với constructor không có tham số.

–      Constructor. newInstance(Object[] initargs): tạo đối tượng với constructor có tham số.

Ví dụ:

import java.lang.reflect.Constructor;

public class Foo {

	public Foo(){
		System.out.println("inside Foo()");
	}

	public Foo(String text){
		System.out.println("inside Foo(String): "+text);
	}

     public static void main(String args[])
     {
        try {

           Class c = Class.forName("Foo");

           // parameterless constructor
           Object foo1=c.newInstance();

           // parametrized constructor
           Constructor con=c.getConstructor(String.class);
           Object foo2=con.newInstance("Hello");
        }
        catch (Throwable e) {
           e.printStackTrace();
        }
     }
}

Output:

inside Foo()

inside Foo(String): Hello

Lưu ý

–      Bởi vì Class là một lớp generic, bạn có thể dùng kí tự wildcard để xác định kiểu dữ liệu mà nó đại diện:

Class<?> c1=int.class;

Class<? extends Number> c2 = int.class;

–      Đối với các thành viên không public, bạn có thể lấy được các đối tượng đại diện của chúng (constructor, field, method) bằng các phương thức getDeclaredXXX().