Java 反射

前言

反射,作为 Java 中的一个重要模块,需要我们熟练掌握。它可以赋予jvm动态编译的能力,在各个框架的源码中也经常出现,如果不能掌握此模块,在学习开源框架的底层设计时,难免会感觉到有一些吃力,今天来简单梳理一下这个模块的核心内容。

一、什么是反射

反射即反向探知,有点类似于考古学家根据发掘的物品来探知以前的事情。

在 Java 程序中,指在程序运行状态中:

1. 对于给定的一个类(Class)对象,可以获得这个类(Class)对象的所有属性和方法;

2. 对于给定的一个对象(new xxxClassName<? extends Object>),都能调用它的任意一个属性和方法

这种动态获取类的内容以及动态调用对象的方法和获取属性的机制,就叫做 Java 的反射机制。

首先写个简单的例子来体验一下反射,创建一个类:

public class Person {

	public void jump() {
		System.out.println("jump...");
	}
}

通过反射来获取 Person 类的属性:

public class MainTest {

	public static void main(String[] args) throws Exception {
		//获取一个类对象
		Class<Person> personClass = Person.class;
		// 在 java.lang.reflect 包下面的都是反射相关的,可以参考在线 API 操作 https://www.matools.com/api/java8
        // 通过反射中的API 操作,获取类对象的属性和方法
		String simpleName = personClass.getSimpleName();
		String className = personClass.getName();
		ClassLoader classLoader = personClass.getClassLoader();
		Field[] fields = personClass.getFields();
		Method[] methods = personClass.getMethods();

		System.out.println(simpleName);
		System.out.println(className);
		System.out.println(classLoader);
		System.out.println(fields.length);
		System.out.println(methods.length);

		Person person = personClass.newInstance();
		person.jump();
		Method jump = personClass.getDeclaredMethod("jump", null);
		// 通过反射执行方法
        jump.invoke(personClass.newInstance());
	}
}

 通过上面的例子,可以看到,通过反射可以拿到 Person 对象中相关的属性和方法,也可以执行 jump 方法,这里与我们平时的 new Person().jump() 的形式不同。这种利用反射操作对象的形式就是反向探知。

二、反射的优缺点

优点:

        增加程序的灵活性,避免固有逻辑写死到程序中;

        代码相对简洁,可以提高代码的复用性。

缺点:

        相比直接调用,反射有比较大的性能消耗;

        存在内部暴露和安全隐患

下面,我们通过一个案例来体会一下它的优点

创建一个接口:

public interface Ball {

	void playBall();
}

接着创建两个对应的实现类:

public class BasketBall implements Ball {
	
	@Override
	public void playBall() {
		System.out.println("打篮球...");	
	}
}


public class FootBall implements Ball{

	@Override
	public void playBall() {
		System.out.println("踢足球...");
	}
}

好了,现在根据业务需求,需要根据不同的运动,获取对应的对象来完成一些操作,我们可以像下面这样写:

public class BallMainTest {

	public static void main(String[] args) {
		Ball football = getInstance("football");
		football.playBall();
	}

	/**
	 * 根据传入不同的key 获取对应的实例对象
	 * @param key
	 * @return
	 */
	public static Ball getInstance(String key) {
		if ("football".equals(key)) {
			return new FootBall();
		}
		if ("basketball".equals(key)) {
			return new BasketBall();
		}
		return null;
	}
}

 那么问题来了,当我们的业务范围扩大,需要增加排球、乒乓球等,getInstance方法是不是需要继续增加 if 分支呢?这样看来,我们写到程序中的那些 if 判断,就相当于是将程序写死了,不够灵活,我们可以利用反射,进行改造:

	/**
	 * 利用反射来动态获取
	 *
	 * @param key
	 * @return
	 */
	public static Ball getInstanceReflectByKey(String key) {
		String basePackage = "com.custom.fs";
		Ball ball = null;
		try {
			Class<?> clazz = Class.forName(basePackage + "." + key);
			ball = (Ball) clazz.newInstance();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return ball;
	}

改造后,我们发现,只需要动态传入你需要的类型名字即可,程序变的更加灵活,即使后续新加了其他的球类运动,只要符合我们的package 路径规则即可。

优点体会了,再体会一下它的缺点,为什么说它存在较大的性能消耗呢?同样,通过案例来分析。

我们分别通过两种方式来创建 100w 个实例对象,看看他们的耗时如何。

  1. 利用普通的 new 实例来创建
    	public static void main(String[] args) {
    		// 通过正常调用来创建 100w 个实例
    		long start = System.currentTimeMillis();
    		for (int i = 0; i <1000000; i++) {
    			getInstance("football");
    		}
    		long end = System.currentTimeMillis();
    		System.out.println("总计耗时:" + (end - start));
    	}

  2. 利用发射方式创建
    	public static void main(String[] args) {
    		// 通过正常调用来创建 100w 个实例
    		long start = System.currentTimeMillis();
    		for (int i = 0; i <1000000; i++) {
    			getInstanceReflectByKey("FootBall");
    		}
    		long end = System.currentTimeMillis();
    		System.out.println("总计耗时:" + (end - start));
    	}

     可以看到,利用反射方式的耗时是普通方式耗时的将近 100 倍。既然性能这么低,我们为什么还要学习,这就好比考古学家去研究一个已经灭绝的物种,难度肯定要比研究现有的(一只猫,一只狗)物种要难,所以,虽然它性能低,我们还是有学习的必要的。

反射为什么这么慢?通过源码来分析一下,首先看一下Class.forName()方法干了啥:

@CallerSensitive
public static Class<?> forName(String className)
                throws ClassNotFoundException {
      Class<?> caller = Reflection.getCallerClass();
      return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}


 /** Called after security check for system loader access checks have been made. */
private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

发现,是一个 native 本地方法,看不到具体的实现,那么继续查看class.newInstance()方法:

@CallerSensitive
public T newInstance()
    throws InstantiationException, IllegalAccessException
{
    if (System.getSecurityManager() != null) {
        // 进行安全检查
        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false);
    }

    // NOTE: the following code may not be strictly correct under
    // the current Java memory model.

    // Constructor lookup
    if (cachedConstructor == null) {
        if (this == Class.class) {
            throw new IllegalAccessException(
                "Can not call newInstance() on the Class for java.lang.Class"
            );
        }
        try {
            Class<?>[] empty = {};
            final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
            // Disable accessibility checks on the constructor
            // since we have to do the security check here anyway
            // (the stack depth is wrong for the Constructor's
            // security check to work)
            java.security.AccessController.doPrivileged(
                new java.security.PrivilegedAction<Void>() {
                    public Void run() {
                            c.setAccessible(true);
                            return null;
                        }
                    });
            cachedConstructor = c;
        } catch (NoSuchMethodException e) {
            throw (InstantiationException)
                new InstantiationException(getName()).initCause(e);
        }
    }
    Constructor<T> tmpConstructor = cachedConstructor;
    // Security check (same as in java.lang.reflect.Constructor)
    int modifiers = tmpConstructor.getModifiers();
    if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
        Class<?> caller = Reflection.getCallerClass();
        if (newInstanceCallerCache != caller) {
            // 进行安全检查
            Reflection.ensureMemberAccess(caller, this, null, modifiers);
            newInstanceCallerCache = caller;
        }
    }
    // Run constructor
    try {
        return tmpConstructor.newInstance((Object[])null);
    } catch (InvocationTargetException e) {
        Unsafe.getUnsafe().throwException(e.getTargetException());
        // Not reached
        return null;
    }
}

可以看到,每一次反射,都进行了2-3 次的安全检验,所以我们总结一下它慢的原因:

1. 调用了 native 方法

2. 每次反射都进行了安全检验

三、反射的基础操作

           先来看一张图,了解 Class内部都有什么东西

 1. 获取类对象的四种方式

Class<Person> clazz1 = Person.class;
Class<? extends Person> clazz2 = new Person().getClass();
Class<?> clazz3 = Class.forName("com.custom.fs.Person");
Class<?> clazz4 = MainTest.class.getClassLoader().loadClass("com.custom.fs.Person");

2. 基本操作


/**
 *  获取类的修饰符 public\private...
 *  返回 int 类型,具体值的代表含义,参考 API,有详细说明
	 */
int modifiers = clazz1.getModifiers();
// 获取类对象的包名
Package classPackage = clazz1.getPackage();
String name = clazz1.getName();
String simpleName1 = clazz1.getSimpleName();
ClassLoader classLoader1 = clazz1.getClassLoader();
AnnotatedType[] annotatedInterfaces = clazz1.getAnnotatedInterfaces();

 3. 字段操作

       先定义一个父类Person,注意,它里面有 public 修饰的成员变量和 private 修饰的成员变量:

public class Person {

	private String phone;
	public static String name;

	public void jump() {
		System.out.println("jump...");
	}
}

         再创建一个子类,继承父类,注意子类中成员变量的修饰符:

public class Student extends Person{

	public String idCard;
	private String lastName;
	public static int age;

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
}

         让我们来测试一下,通过 class 类对象来获取字段及操作字段:

public class FieldTest {

	public static void main(String[] args) throws Exception {
		Class<Student> clazz = Student.class;

		//获取类型中定义的字段 公有的以及父类公有的
		Field[] fields = clazz.getFields();
		for (Field field : fields) {
			System.out.println(field.getModifiers() + " " + field.getName());
		}

		System.out.println("___________________");
		// 只能获取到对象自己定义的字段,可以获取到私有的
		Field[] declaredFields = clazz.getDeclaredFields();
		for (Field field : declaredFields) {
			System.out.println(field.getModifiers() + " " + field.getName());
		}
		Student student = clazz.newInstance();
		//获取对象中的指定字段并赋值
		Field lastName = clazz.getDeclaredField("lastName");
		//通过反射修改私有变量,需要先进行授权,强吻,不让吻也得吻
		lastName.setAccessible(true);
		//意思是,修改 student 对象中的 lastName 字段,赋值为 John
		lastName.set(student,"John");
		System.out.println(student.getLastName());
	}
}

 4. 类中方法的操作

       同样的,我们在 Student 类和 Person 类中添加公有和私有的方法,通过反射来获取一下,看看结果:

public class Person {

	private String phone;
	public static String name;

	public void jump() {
		System.out.println("jump...");
	}

	private String getPhone() {
		return phone;
	}

	private void setPhone(String phone) {
		this.phone = phone;
	}
}


public class Student extends Person{

	public String idCard;
	private String lastName;
	public static int age;

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public void say(String msg) {
		System.out.println("hello:" + msg);
	}

	private void workHome(String type,int longTime) {
		System.out.println("写" + type + "作业...." + longTime + "小时...");
	}
}

测试一下:

public class MethodTest {

	public static void main(String[] args) throws Exception {

		Student student = new Student();
		Class<? extends Student> studentClass = student.getClass();

		//通过反射获取 student 中的方法,与 Field 字段的操作类似,可以获取到父类的公有方法
		Method[] methods = studentClass.getMethods();
		for (Method method : methods) {
			System.out.println(method.getModifiers() + " " + method.getName());
		}
		System.out.println("=====================");

		// 只能获取到当前类自己的方法,包括私有的
		Method[] declaredMethods = studentClass.getDeclaredMethods();
		for (Method declaredMethod : declaredMethods) {
			System.out.println(declaredMethod.getModifiers() + " " + declaredMethod.getName());
		}

		// 调用方法
		Method say = studentClass.getDeclaredMethod("workHome", String.class, int.class);
		say.setAccessible(true);
		say.invoke(student,"Java",2);
	}
}

5. 构造方法操作

      其实构造方法的操作,同 Field、Method 一样,了解了上面的内容,构造器的操作,这里不进行过多演示了。

四、反射破坏单例

          先来写一个简单的单例代码,如下:

public class UserSingle {
	
	private static UserSingle instance;
	
	private UserSingle () {
		
	}
	
	public static UserSingle getInstance() {
		if (instance == null) {
			instance = new UserSingle();
		}
		return instance;
	}
}

测试一下正常调用,和利用反射破坏单例:

public class SingleTest {

	public static void main(String[] args) throws Exception {
		UserSingle instance1 = UserSingle.getInstance();
		UserSingle instance2 = UserSingle.getInstance();
		UserSingle instance3 = UserSingle.getInstance();
		System.out.println(instance1);
		System.out.println(instance2);
		System.out.println(instance3);

		System.out.println("================");

		// 利用反射破坏单例
		Class<UserSingle> userSingleClass = UserSingle.class;
		Constructor<UserSingle> constructor = userSingleClass.getDeclaredConstructor();
		constructor.setAccessible(true);
		UserSingle userSingle = constructor.newInstance(null);
		System.out.println(userSingle);
	}
}

可以看到,正常使用单例可以满足,当我们使用反射来进行构造的时候,破坏了单例模式,那么该如何处理呢?将单例代码,稍加改造:

public class UserSingle {

	private static UserSingle instance;

	private UserSingle () {
		// 防止反射破坏单例
		if (instance != null) {
			throw new RuntimeException("实例已初始化,不允许重复操作");
		}
	}

	public static UserSingle getInstance() {
		if (instance == null) {
			instance = new UserSingle();
		}
		return instance;
	}
}

如上,我们在单例类的私有构造方法中,加入判断,来限制通过反射破坏单例。

这里的单例写的比较简单,感兴趣的童鞋,可以查看我的另外一篇关于单例模式的详细讲解。设计模式二(单例模式)_搬运Gong的博客-CSDN博客icon-default.png?t=M276https://blog.csdn.net/qq_20315217/article/details/114130214

五、反射的应用场景

          反射在各大框架中,使用的也是比较多。

JDBC 的封装

Spring IOC

MyBaits

...

 这里不对其他框架源码使用反射进行分析,主在理解反射,后面会单独出一篇关于 Spring 源码分析的内容,到时再进行详细描述。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值