反射


反射

Java的反射是指程序在运行期可以拿到一个对象的所有信息。

Class类

除了int等基础类型,Java的其他类型都是class(包括interface)。而class是在JVM中动态加载的,当JVM第一次读取到某个数据类型时,会在内存中创建一个与之关联的Class类的实例。

  • Class类:

    public final class Class {
        private Class() {}
    }

    构造方法是private,这表明只有JVM才能调用此构造方法,java程序是无法创建Class实例的。

  • JVM内部加载String类:

    Class cls = new Class(String);

    JVM持有的每个Class实例都指向一个数据类型(class或interface),并且每个实例都保存了该class(即数据类型)的所有信息,包括类名、包名、父类、方法、域等等。

获取一个class的Class实例

  • 通过一个class的静态变量获取:

    Class cls = String.class;
  • 通过一个class方法的getClass方法获取:

    String s = "Hello";
    Class cls = s.getClass();
  • 在forName方法中,指定完整类名获取:

    Class cls = Class.forName("java.lang.String");

注意:这三种方式获取的Class实例都是同一个实例,因为JVM对每个加载的Class只创建一个Class实例来表示它的类型。

比较两个class实例

用instanceof不但匹配指定类型,还匹配指定类型的子类。而用==判断class实例可以精确地判断数据类型,但不能作子类型比较。

通过Class实例来创建对应数据类型的实例

// 获取String的Class实例:
Class cls = String.class;
// 创建一个String实例:
String s = (String) cls.newInstance();

动态加载

// Main.java
public class Main {
    public static void main(String[] args) {
        if (args.length > 0) {
            create(args[0]);
        }
    }

    static void create(String name) {
        Person p = new Person(name);
    }
}

当执行Main.java时,由于用到了Main,因此,JVM首先会把Main.class加载到内存。然而,并不会加载Person.class,除非程序执行到create()方法,JVM发现需要加载Person类时,才会首次加载Person.class。如果没有执行create()方法,那么Person.class根本就不会被加载。

利用JVM动态加载class的特性,我们才能在运行期根据条件加载不同的实现类。例如,Commons Logging总是优先使用Log4j,只有当Log4j不存在时,才使用JDK的logging。

访问字段

  • Class类提供了以下几个方法来获取字段:

    • Field getField(name):根据字段名获取某个public的field(包括父类
    • Field getDeclaredField(name):根据字段名获取当前类的某个field(包括private的field,但不包括父类
    • Field[] getFields():获取所有public的field(包括父类
    • Field[] getDeclaredFields():获取当前类的所有field(包括private的field,但不包括父类
  • 一个Field对象包含了一个字段的所有信息:

    • getName():返回字段名称,例如,”name”;
    • getType():返回字段类型,也是一个Class实例,例如,String.class;
    • getModifiers():返回字段的修饰符,它是一个int值,不同的值表示不同的含义。
public class Hello {

	private final String gesture = "hi";

	public static void main(String[] args) {
		Class cl = Hello.class;
		Field f;
		try {
			f = cl.getDeclaredField("gesture");
			System.out.println(f.getName());
            //wil print gesture
			System.out.println(f.getType());
            //will print class java.lang.String
			int n = f.getModifiers();
			System.out.println(n);
            //will print 18
			System.out.println(Modifier.isFinal(n));
            //will print true
			System.out.println(Modifier.isStatic(n));
            //will print false
			System.out.println(Modifier.isProtected(n));
            //will print false
			System.out.println(Modifier.isPublic(n));
            //will print false
			System.out.println(Modifier.isPrivate(n));
            //will print true
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 

	}

}

获取字段值

  • 用Field.get(Object)获取指定实例的指定字段的值。

  • 当需要获取的字段为private时,可以写一句:

    f.setAccessible(true);

    这样就可以访问了。

public class Hello {

	public static void main(String[] args) {
		Class cl = Person.class;
		try {
			Field f = cl.getDeclaredField("name");
			Person aperson = new Person("秦锋");
			f.setAccessible(true);
			System.out.println(f.get(aperson));
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}

class Person {
	private String name;

	public Person(String name) {
		this.name = name;
	}
}

问题:如果使用反射可以获取private字段的值,那么封装还有意义吗?

答案是正常情况下,我们总是通过p.name来访问Person的name字段,编译器会根据private等修饰符决定是否允许访问字段,这样就达到了数据封装的目的。

而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。

而且setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对java和javax开头的package的类调用setAccessible(true),这样可以保证JVM核心库的安全。(廖老师yyds)

设置字段值

设置字段值是通过Field.set(Object, Object)实现的,其中第一个Object参数是指定的实例,第二个Object参数是待修改的值。

public class Hello {

	public static void main(String[] args) {
		Class cl = Person.class;
		try {
			Field f = cl.getDeclaredField("name");
			Person aperson1 = new Person("秦锋");
			f.setAccessible(true);
			f.set(aperson1, "周玉豪");
			System.out.println(f.get(aperson1));
            //will print 周玉豪
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
        }

	}

}

class Person {
	private String name;

	public Person(String name) {
		this.name = name;
	}
}

调用方法

  • Class类提供了以下几个方法来获取Method:
    • Method getMethod(name, Class…):获取某个public的Method(包括父类
    • Method getDeclaredMethod(name, Class…):获取当前类的某个Method(包括private,但不包括父类
    • Method[] getMethods():获取所有public的Method(包括父类
    • Method[] getDeclaredMethods():获取当前类的所有Method(包括private,但不包括父类
  • 一个Method对象包含一个方法的所有信息:
    • getName():返回方法名称,例如:”getScore”;
    • getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class;
    • getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class};
    • getModifiers():返回方法的修饰符,它是一个int值,不同的值表示不同的含义。
  • 对Method实例调用invoke就相当于调用该方法,invoke的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
  • 使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。
public class Hello {

	public static void main(String[] args) {
		Class cl = Person.class;
		try {
			Method m = cl.getMethod("print", null);
            //没有参数,写null
			Person aperson = new Person("秦锋");
			m.invoke(aperson, null);
            //will print 秦锋
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

}

class Person {
	private String name;

	public Person(String name) {
		this.name = name;
	}

	public void print() {
		System.out.println(this.name);
	}
}

调用静态方法

invoke方法传入的第一个参数为null。

public class Hello {

	public static void main(String[] args) {
		Class cl = Math.class;
		try {
			Method m = cl.getMethod("sqrt", double.class);
			System.out.println(m.invoke(null, 3));
			// will print 1.7320508075688772
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}

调用非public方法

通过Method.setAccessible(true)允许其调用。

public class Hello {

	public static void main(String[] args) {
		Class cl = Person.class;
		Person aperson = new Person("秦锋");
		try {
			Method m = cl.getDeclaredMethod("print", null);
			m.setAccessible(true);
			m.invoke(aperson, null);
            //will print 秦锋
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}

class Person {
	private String name;

	public Person(String name) {
		this.name = name;
	}

	private void print() {
		System.out.println(this.name);
	}
}

调用构造方法

  • 通过调用Class.newInstance()方法来创建实例:

    public class Hello {
    	private String s = "hello";
    
    	public static void main(String[] args) {
    		try {
    			Hello p = Hello.class.newInstance();
    			System.out.println(p.s);
                //will print hello
    		} catch (Exception e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    
    }

    局限:它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。

  • 通过获取Constructor对象来创建实例

    通过Class实例获取Constructor的方法如下:

    • getConstructor(Class…):获取某个public的Constructor;
    • getDeclaredConstructor(Class…):获取某个Constructor;
    • getConstructors():获取所有public的Constructor;
    • getDeclaredConstructors():获取所有Constructor。

    注意:以上方法均和父类无关。

    public class Hello {
    
    	public static void main(String[] args) {
    		Class cl = Person.class;
    		try {
    			Constructor cons = cl.getDeclaredConstructor(String.class);
    			cons.setAccessible(true);
    			Person p = (Person) cons.newInstance("秦锋");
    			p.print();
    		} catch (Exception e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    
    	}
    
    }
    
    class Person {
    	private String name;
    
    	private Person(String name) {
    		this.name = name;
    	}
    
    	public void print() {
    		System.out.println(this.name);
    	}
    }

    **注意:**和Field、Method一样,当要访问非public的构造方法时,需要设置cons.setAccessible(true)。

获取继承关系

获取父类的Class

有了Class实例,我们还可以获取它的父类的Class:

public class Hello {

	public static void main(String[] args) {
		Class cl = Integer.class;
		while (cl.getSuperclass() != null) {
			System.out.println(cl.getSuperclass());
			cl = cl.getSuperclass();
		}
        //will print
        //class java.lang.Number
        //class java.lang.Object

	}

}

获取interface

通过Class我们就可以查询到实现的接口:

public class Hello {

	public static void main(String[] args) {
		Class cl = Integer.class;
		Class[] cls = cl.getInterfaces();
		for (Class c : cls) {
			System.out.println(c);
		}
        //will print 
        //interface java.lang.Comparable
        //interface java.lang.constant.Constable
        //interface java.lang.constant.ConstantDesc

	}

}

判断向上转型是否成立

通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。

public class Hello {

	public static void main(String[] args) {
		System.out.println(Integer.class.isAssignableFrom(Integer.class));
		// will print true
		System.out.println(Number.class.isAssignableFrom(Integer.class));
		//will print true
		System.out.println(Integer.class.isAssignableFrom(Number.class));
		//will print false
		System.out.println(Object.class.isAssignableFrom(String.class));
		//will print true

	}

}

动态代理

Java标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface的实例。

  • 在运行期动态创建一个interface实例的方法如下:
  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;

  2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:

    • 使用的ClassLoader,通常就是接口或者被代理的对象的ClassLoader;

    • 需要实现的接口数组,至少需要传入一个接口进去;

    • 用来处理接口方法调用的InvocationHandler实例。

  3. 将返回的Object强制转型为接口。

  • 当使用已创建的代理实例去调用方法时,会转而去调用InvocatonHandler中的invoke方法。
public class Hello {

	public static void main(String[] args) {
		Person st = new Student();
		InvocationHandler ih = new InvocationHandler() {
			private Object target = st;

			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				// TODO Auto-generated method stub
				method.invoke(st, args);
                //调用st中的method
                //will print 已调用speak方法
				return null;
			}
		};
        //创建代理接口的实例
		Person pr = (Person) Proxy.newProxyInstance(st.getClass().getClassLoader(), st.getClass().getInterfaces(), ih);
		pr.speak();

	}

}

interface Person {
	void speak();
}

class Student implements Person {
	public void speak() {
		System.out.println("已调用speak方法");
	}
}

文章作者: 淡夜
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 淡夜 !
评论
  目录