java基础|一文搞懂java中的高大上技术“反射”

反射的概念 ? JAVA反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;
java中应用的几个场景:

  1. Spring 框架中bean的初始化用到了反射
  2. 破坏单例模式是我们也用到了反射
  3. 获取标注的注解的类时也用到了反射
  4. JDBC连接数据
反射的基本使用 Java 反射的主要组成部分有4个:
  • Class:任何运行在内存中的所有类都是该 Class 类的实例对象,每个 Class 类对象内部都包含了本来的所有信息。记着一句话,通过反射干任何事,先找 Class 准没错!
  • Field:描述一个类的属性,内部包含了该属性的所有信息,例如数据类型,属性名,访问修饰符······
  • Constructor:描述一个类的构造方法,内部包含了构造方法的所有信息,例如参数类型,参数名字,访问修饰符······
  • Method:描述一个类的所有方法(包括抽象方法),内部包含了该方法的所有信息,与Constructor类似,不同之处是 Method 拥有返回值类型信息,因为构造方法是没有返回值的。
参考其他博主的脑图:
java基础|一文搞懂java中的高大上技术“反射”
文章图片

反射中的用法有非常非常多,常见的功能有以下这几个:
  • 运行时获取一个类的 Class 对象
  • 运行时构造一个类的实例化对象
  • 运行时获取一个类的所有信息:变量、方法、构造器、注解
下面写几个使用的实例,类的话就以下边这个为基础:
public class SmallPineapple { public String name; public int age; private double weight; // 体重只有自己知道public SmallPineapple() {}public SmallPineapple(String name, int age) { this.name = name; this.age = age; } public void getInfo() { System.out.print("["+ name + " 的年龄是:" + age + "]"); } }

1、获取类的 Class 对象
获取 Class 对象的方法有3种:
  • 类名.class:这种获取方式只有在编译前已经声明了该类的类型才能获取到 Class 对象
Class clazz = SmallPineapple.class;

  • 实例.getClass():通过实例化对象获取该实例的 Class 对象
SmallPineapple sp = new SmallPineapple(); Class clazz = sp.getClass();

  • Class.forName(className):通过类的全限定名获取该类的 Class 对象
Class clazz = Class.forName("com.bean.smallpineapple");

拿到 Class对象就可以对它为所欲为了:剥开它的皮(获取类信息)、指挥它做事(调用它的方法),看透它的一切(获取属性),总之它就没有隐私了。
【java基础|一文搞懂java中的高大上技术“反射”】不过在程序中,每个类的 Class 对象只有一个,也就是说你只有这一个奴隶。我们用上面三种方式测试,通过三种方式打印各个 Class 对象都是相同的。
Class clazz1 = Class.forName("com.bean.SmallPineapple"); Class clazz2 = SmallPineapple.class; SmallPineapple instance = new SmallPineapple(); Class clazz3 = instance.getClass(); System.out.println("Class.forName() == SmallPineapple.class:" + (clazz1 == clazz2)); System.out.println("Class.forName() == instance.getClass():" + (clazz1 == clazz3)); System.out.println("instance.getClass() == SmallPineapple.class:" + (clazz2 == clazz3));

java基础|一文搞懂java中的高大上技术“反射”
文章图片

2、构造类的实例化对象
通过反射构造一个类的实例方式有2种:
  • Class 对象调用newInstance()方法
Class clazz = Class.forName("com.bean.SmallPineapple"); SmallPineapple smallPineapple = (SmallPineapple) clazz.newInstance(); smallPineapple.getInfo(); // [null 的年龄是:0]

即使 SmallPineapple 已经显式定义了构造方法,通过 newInstance() 创建的实例中,所有属性值都是对应类型的初始值,因为 newInstance() 构造实例会调用默认无参构造器。
  • Constructor 构造器调用newInstance()方法
Class clazz = Class.forName("com.bean.SmallPineapple"); Constructor constructor = clazz.getConstructor(String.class, int.class); constructor.setAccessible(true); SmallPineapple smallPineapple2 = (SmallPineapple) constructor.newInstance("小菠萝", 21); smallPineapple2.getInfo(); // [小菠萝 的年龄是:21]

通过 getConstructor(Object… paramTypes) 方法指定获取指定参数类型的 Constructor, Constructor 调用 newInstance(Object… paramValues) 时传入构造方法参数的值,同样可以构造一个实例,且内部属性已经被赋值。
通过Class对象调用 newInstance() 会走默认无参构造方法,如果想通过显式构造方法构造实例,需要提前从Class中调用getConstructor()方法获取对应的构造器,通过构造器去实例化对象。
3、获取一个类的所有信息
Class 对象中包含了该类的所有信息,在编译期我们能看到的信息就是该类的变量、方法、构造器,在运行时最常被获取的也是这些信息,下边列举几个常用的API:
java基础|一文搞懂java中的高大上技术“反射”
文章图片

获取类中的变量(Field)
  • Field[] getFields():获取类中所有被public修饰的所有变量
  • Field getField(String name):根据变量名获取类中的一个变量,该变量必须被public修饰
  • Field[] getDeclaredFields():获取类中所有的变量,但无法获取继承下来的变量
  • Field getDeclaredField(String name):根据姓名获取类中的某个变量,无法获取继承下来的变量
获取类中的方法(Method)
  • Method[] getMethods():获取类中被public修饰的所有方法
  • Method getMethod(String name, Class… paramTypes):根据名字和参数类型获取对应方法,该方法必须被public修饰
  • Method[] getDeclaredMethods():获取所有方法,但无法获取继承下来的方法
  • Method getDeclaredMethod(String name, Class… paramTypes):根据名字和参数类型获取对应方法,无法获取继承下来的方法
获取类的构造器(Constructor)
  • Constuctor[] getConstructors():获取类中所有被public修饰的构造器
  • Constructor getConstructor(Class… paramTypes):根据参数类型获取类中某个构造器,该构造器必须被public修饰
  • Constructor[] getDeclaredConstructors():获取类中所有构造器
  • Constructor getDeclaredConstructor(class… paramTypes):根据参数类型获取对应的构造器
每种功能内部以 Declared 细分为2类:
Declared修饰的方法:可以获取该类内部包含的所有变量、方法和构造器,但是无法获取继承下来的信息
Declared修饰的方法:可以获取该类中public修饰的变量、方法和构造器,可获取继承下来的信息
如果想获取类中**所有的(包括继承)**变量、方法和构造器,则需要同时调用getXXXs()getDeclaredXXXs()两个方法,用Set集合存储它们获得的变量、构造器和方法,以防两个方法获取到相同的东西。
例如:要获取SmallPineapple获取类中所有的变量,代码应该是下面这样写。
Class clazz = Class.forName("com.bean.SmallPineapple"); // 获取 public 属性,包括继承Field[] fields1 = clazz.getFields(); // 获取所有属性,不包括继承 Field[] fields2 = clazz.getDeclaredFields(); // 将所有属性汇总到 set Set allFields = new HashSet<>(); allFields.addAll(Arrays.asList(fields1)); allFields.addAll(Arrays.asList(fields2));


不知道你有没有发现一件有趣的事情,如果父类的属性用protected修饰,利用反射是无法获取到的。
protected 修饰符的作用范围:只允许同一个包下或者子类访问,可以继承到子类。
getFields() 只能获取到本类的public属性的变量值;
getDeclaredFields() 只能获取到本类的所有属性,不包括继承的;无论如何都获取不到父类的 protected 属性修饰的变量,但是它的的确确存在于子类中。
获取注解 它并不是专属于 Class 对象的一种信息,每个变量,方法和构造器都可以被注解修饰,所以在反射中,Field,Constructor 和 Method 类对象都可以调用下面这些方法获取标注在它们之上的注解。
  • Annotation[] getAnnotations():获取该对象上的所有注解
  • Annotation getAnnotation(Class annotaionClass):传入注解类型,获取该对象上的特定一个注解
  • Annotation[] getDeclaredAnnotations():获取该对象上的显式标注的所有注解,无法获取继承下来的注解
  • Annotation getDeclaredAnnotation(Class annotationClass):根据注解类型,获取该对象上的特定一个注解,无法获取继承下来的注解
只有注解的@Retension标注为RUNTIME时,才能够通过反射获取到该注解,@Retension 有3种保存策略:
  • SOURCE:只在**源文件(.java)**中保存,即该注解只会保留在源文件中,编译时编译器会忽略该注解,例如 @Override 注解
  • CLASS:保存在字节码文件(.class)**中,注解会随着编译跟随字节码文件中,但是**运行时不会对该注解进行解析
  • RUNTIME:一直保存到运行时,用得最多的一种保存策略,在运行时可以获取到该注解的所有信息
像下面这个例子,SmallPineapple 类继承了抽象类PineapplegetInfo()方法上标识有 @Override 注解,且在子类中标注了@Transient注解,在运行时获取子类重写方法上的所有注解,只能获取到@Transient的信息。
public abstract class Pineapple { public abstract void getInfo(); } public class SmallPineapple extends Pineapple { @Transient @Override public void getInfo() { System.out.print("小菠萝的身高和年龄是:" + height + "cm ; " + age + "岁"); }

启动类Bootstrap获取 SmallPineapple 类中的 getInfo() 方法上的注解信息:
public class Bootstrap { /** * 根据运行时传入的全类名路径判断具体的类对象 * @param path 类的全类名路径 */ public static void execute(String path) throws Exception { Class obj = Class.forName(path); Method method = obj.getMethod("getInfo"); Annotation[] annotations = method.getAnnotations(); for (Annotation annotation : annotations) { System.out.println(annotation.toString()); } } public static void main(String[] args) throws Exception { execute("com.pineapple.SmallPineapple"); } }

通过反射调用方法 通过反射获取到某个 Method 类对象后,可以通过调用invoke方法执行。
  • invoke(Oject obj, Object... args):参数1指定调用该方法的对象,参数2是方法的参数列表值。
如果调用的方法是静态方法,参数1只需要传入null,因为静态方法不与某个对象有关,只与某个类有关。
可以像下面这种做法,通过反射实例化一个对象,然后获取Method方法对象,调用invoke()指定SmallPineapplegetInfo()方法。
Class clazz = Class.forName("com.bean.SmallPineapple"); Constructor constructor = clazz.getConstructor(String.class, int.class); constructor.setAccessible(true); SmallPineapple sp = (SmallPineapple) constructor.newInstance("小菠萝", 21); //也可直接 sp.getInfo()调用 Method method = clazz.getMethod("getInfo"); if (method != null) { method.invoke(sp, null); }

反射的优势及缺陷 反射的优点:
  • 增加程序的灵活性:面对需求变更时,可以灵活地实例化不同对象
但是,有得必有失,一项技术不可能只有优点没有缺点,反射也有两个比较隐晦的缺点:
  • 破坏类的封装性:可以强制访问 private 修饰的信息
  • 性能损耗:反射相比直接实例化对象、调用方法、访问变量,中间需要非常多的检查步骤和解析步骤,JVM无法对它们优化。
    java基础|一文搞懂java中的高大上技术“反射”
    文章图片

    此篇文章我是在公众号看到的感觉不错,原文讲的比较多,此处我只贴出代码实例以供参考:https://mp.weixin.qq.com/s/0CgfpF9BoqGfCTM3_aBTDw (作者小菠萝所以类名为SmallPineapple哈哈)

    推荐阅读