Java反射机制学习笔记

版权声明:原创内容,如需转载请联系作者。 https://blog.csdn.net/CrankZ/article/details/81570900

前置知识

Java类型

Java中每个类型要么是引用类型,要么是基本类型。

基本数据类型:byte、short、int、long、float、double、char、boolean
除掉这四类八种基本类型,其它的都是对象,也就是引用类型,包括数组。

对于每个对象类型,JVM都会为其初始化一个java.lang.Class的实例,可以检查包括属性和方法在内的对象运行时的属性。Class同样也可以创建一个新的类和对象。最重要的是,他是所有反射API的入口。

什么是反射?

简介

  • 定义:Java语言中 一种 动态(运行时)访问、检测 & 修改它本身的能力
  • 作用:动态(运行时)获取类的完整结构信息 & 调用对象的方法
  1. 类的结构信息包括:变量、方法等
  2. 正常情况下,Java类在编译前,就已经被加载到JVM中;而反射机制使得程序运行时还可以动态地去操作类的变量、方法等信息

反射就是动态加载对象,并对对象进行剖析。在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能成为Java反射机制。

应用场景

  • 动态(运行时)获取 类文件结构信息(如变量、方法等) & 调用对象的方法
  • 动态代理
  • 工厂模式优化
  • Java JDBC数据库
  • 等等

使用步骤

在使用Java反射机制时,主要步骤包括: 

  1. 获取 目标类型的Class对象 
  2. 通过获取的Class对象,完成对这个类属性、方法、构造函数的信息的获取与后续操作

如何获得Class对象

获取Class主要三种方法对比:

方式

使用范围

getClass()

需要对象实例,仅能用于引用类型

.class

无需对象实例,既可以是引用类型也可以是基本类型

forName()

只需要类的全限定名

TYPE

基本类型的包装类

.getClass()

//getClass()是Object类的方法。所有引用类型都有这个方法。
String str = “”;
Class c1 = str.getClass();

Boolean bool = false;
Class c2 = bool.getClass();

.class

基本类型和引用类型都有.class

// 注:Class对象表示的是一个类型,而这个类型未必一定是类
// 如,int不是类,但int.class是一个Class类型的对象
Class c1 = int.class;
Class c2 = Integer.class;

.forName()

注意

如果类名保存字符串中,并可在运行时该变,即可使用这种方法。forName() 方法会爆ClassNotFoundException 异常,所以需要进行异常处理。

Class.forName() 内部通过反射API根据目标类名将类手动加载到内存中,称为类加载器加载方法。加载过程中会把目标类的static方法,变量,代码块加载到JVM,注意此时尚未创建对象实例

使用这个方法需要知道类的全限定名。只能应用于引用类型。数组类型的全限定名可以由Class.getName()获取。

// 加载自定义类
Class c1 = Class.forName("com.baidu.lala.haha.xxx");
// 加载Java官方类
Class c2 = Class.forName("java.lang.Boolean");

TYPE

只能用于基本类型的包装类和java.lang.Void

// Double.TYPE 就相当于double.class
Class c1 = Double.TYPE;
Class c2 = double.class;

// Void.TYPE相当于void.class
Class c3 = Void.TYPE;
Class c4 = void.class;

操作

获得Class对象之后要进行操作。操作主要有三个类

  1. Constructor类
  2. Field类
  3. Method类

Field

主要方法

  • Field[] getFields():获得类中所有public变量
  • Field[] getDeclaredFields():获得类中所有访问权限变量
  • Field getField(String name):根据变量名得到指定的public变量
  • Field getDeclaredField(String name):根据变量名获得指定的变量,访问权限不限

我们新建一个Student类

// 为了方便演示,已删除无关代码
public class Student {
    private String name;
}

获得Field(属性)

clz.getFields()只能获取public的属性,包括父类的。

想要获得所有权限的属性得用clz.getDeclaredFields()

Field是属性类,可以获取属性相关的信息,比如:

  • 属性类型:field.getType().getName()
  • 属性名称:filed.getName();
  • 属性修饰符:Modifier.toString(field.getModifiers())
  • 等等.......
public static void main(String[] args) {
    Class c = Student.class;
    printFields(c);
}
// 打印属性信息
private static void printFields(Class c) {
    // 获得所有属性
    Field[] fields = c.getDeclaredFields();
    for (Field field : fields) {
        // 类型
        String type = field.getType().getName();
        // 名称
        String name = field.getName();
        // 修饰符
        String modifiers = Modifier.toString(field.getModifiers());
        if (modifiers.length() > 0) {
            System.out.print("修饰符:" + modifiers + " ");
        }
        System.out.println("类型:" + type + " 名称:" + name);
    }
}


结果:

操作Field(属性)

// 操作属性
private static void operateFields(Class c) {
    try {
        Student stu = (Student) c.newInstance();
        Field field = c.getDeclaredField("name");
        // 属性为private,默认禁止修改
        // 所以要加上这一行,不然报错
        field.setAccessible(true);
        System.out.println("修改前:" + field.get(stu));
        field.set(stu, "张三");
        System.out.println("修改后:" + field.get(stu));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

结果:

Method

获得方法--Method

  • Method[] getMethods():获得所有public方法;
  • Method[] getDeclaredMethods():获得所有访问权限的方法;
  • Method getMethod(String name, Class[] params):根据方法签名获取类自身对应public方法,或者从基类继承和接口实现的对应public方法
  • Method getDeclaredMethod(String name, Class[] params):根据方法签名获得对应的类自身声明方法访问权限不限

Student类

// 为了方便演示,已删除无关代码
public class Student {
    // 有参函数
    public void m1() {
        System.out.println("我是无参函数");
    }
    // 无参函数
    public void m2(String value) {
        System.out.println("我是有参函数 " + value);
    }
}

获得Method(方法)

Method是方法类,可以获取方法相关的信息比如:

  • 方法返回类型:method.getReturnType().getName()
  • 方法修饰符:Modifier.toString(method.getModifiers())
  • 方法参数信息: method.getParameters()
  • 方法上的注解: method.getAnnotations()
  • 等等.......
// 打印类中所有方法
private static void printMethods(Class c) {
    Method[] methods = c.getDeclaredMethods();
    for (Method method : methods) {
        String retType = method.getReturnType().getName();
        String name = method.getName();

        String modifiers = Modifier.toString(method.getModifiers());
        System.out.print("方法修饰符:" + modifiers);
        System.out.println(";方法类型:" + retType + ";方法名称:" + name);
        printParameters(method.getParameters());
        System.out.println("");
    }
}

// 打印参数名称和类型
private static void printParameters(Parameter[] parameters) {
    for (Parameter parameter : parameters) {
        String paramName = parameter.getName();
        String paramType = parameter.getType().getName();
        System.out.println("参数名称:" + paramName + ";参数类型:" + paramType);
    }
}

结果:

调用Method(方法)

// 通过反射调用类的方法
private static void invokeMethod(Class c) {
    try {
        Student stu = (Student) c.newInstance();
        // 调用无参函数
        Method method1 = c.getMethod("m1");
        method1.invoke(stu);
        // 调用有参函数
        Method method2 = c.getMethod("m2", String.class);
        method2.invoke(stu, "啦啦啦");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

结果:

Constructor

获取构造器--Constructor

  • Constructor[] getConstructors():获得所有public构造器;
  • Constructor[] getDeclaredConstructors():获得所有访问权限的构造器
  • Constructor getConstructor(Class[] params):根据指定参数获得对应构造器;
  • Constructor getDeclaredConstructor(Class[] params):根据指定参数获得对应构造器;

Student类

// 为了方便演示,已删除无关代码
public class Student {
    public Student() {
        System.out.println("我是无参构造函数");
    }
    public Student(String value) {
        System.out.println("我是有参构造函数 " + value);
    }
}

打印所有构造函数

// 查看所有构造函数
private static void printConstructors(Class c) {
    Constructor[] constructors = c.getDeclaredConstructors();
    for (Constructor constructor : constructors) {
        String name = c.getName();
        String modifiers = Modifier.toString(c.getModifiers());
        System.out.print("构造函数修饰符:" + modifiers);
        System.out.println(";构造函数名称" + name);
        printParameters(constructor.getParameters());
        System.out.println("");
    }
}

// 打印参数名称和类型(和上面的一样)
private static void printParameters(Parameter[] parameters) {
    for (Parameter parameter : parameters) {
        String paramName = parameter.getName();
        String paramType = parameter.getType().getName();
        System.out.println("参数名称:" + paramName + ";参数类型:" + paramType);
    }
}

结果

调用构造函数

// 通过反射调用构造函数
private static void invokeConstructors(Class c) {
    try {
        Student stu1 = (Student) c.getConstructor().newInstance();
        Student stu2 = (Student) c.getConstructor(String.class).newInstance("呵呵呵");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

结果:

反射优缺点

优点

灵活性高。因为反射属于动态编译,即只有到运行时才动态创建 &获取对象实例。

Tips:编译方式说明: 
1. 静态编译:在编译时确定类型 & 绑定对象。如常见的使用new关键字创建对象 
2. 动态编译:运行时确定类型 & 绑定对象。动态编译体现了Java的灵活性、多态特性 & 降低类之间的藕合性

缺点

  • 执行效率低 
    因为反射的操作 主要通过JVM执行,所以时间成本会 高于 直接执行相同操作
    反射是一种解释操作,用于字段和方法接入时要远慢于直接代码
    1. 因为接口的通用性,Java的invoke方法是传object和object[]数组的。基本类型参数需要装箱和拆箱,产生大量额外的对象和内存开销,频繁促发GC。
    2. 编译器难以对动态调用的代码提前做优化,比如方法内联。
    3. 反射需要按名检索类和方法,有一定的时间开销。
  • 容易破坏类结构 
    因为反射操作饶过了源码,容易干扰类原有的内部逻辑

效率低举例

Student类

// 为了方便演示,已删除无关代码
public class Student {
    private String name;
    public void setName(String name) {
        this.name = name;
    }
}

直接创建对象,调用方法设置值,然后获取值,时间在300ms左右

long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
    Student stu = new Student();
    System.out.println(stu.getName());

}
long end = System.currentTimeMillis();
System.out.println(end - start);

利用反射来实现上面的功能,时间在500ms左右

long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
    Class<?> clz = Class.forName("fs.Student");
    Student stu = (Student) clz.newInstance();
    Method method = clz.getMethod("setName", String.class);
    method.invoke(stu, "张三");
    System.out.println(stu.getName());
}
long end = System.currentTimeMillis();
System.out.println(end - start);

反射与泛型

使用反射越过泛型检查

    首先,这个知识点的背景是这样的,如果你创建了一个集合,限制传入Integer类型,但是后面使用时,你想传入String类型,怎么办呢?先看几行代码的例子:

public static void main(String[] args) throws Exception {
    //创建一个List只能接收Integer类型
    ArrayList<Integer> list = new ArrayList<>();
    list.add(100);
    // 以下代码会报错
    //list.add("hello");
    //下面使用反射来越过泛型检查
    Class clazz = list.getClass();
    //获得List的add方法,在反射内部接收的都是Object对像
    Method add = clazz.getDeclaredMethod("add", Object.class);
    //执行add操作
    add.invoke(list, "hello");
    add.invoke(list, "world");
    add.invoke(list, "java se");
    System.out.println(list);
}

     对于泛型,在编写程序时都是给编译器看的,可以查看class文件的反编译结果,发现是没有泛型的,所以从理论上来说,对于上面的list对像,可以存储任意类型,在反射内部处理中,都是处理的Object类型,所以就有了以上代码。

使用反射来获取泛型信息

    java采用泛型擦除的机制来引入泛型,也就是说,泛型仅仅是给编译器javac看的,来确保数据的安全性和免去数据类型转换,但是,一旦编译完成,所有和泛型相关的东西都被擦除,这一点也可以从类编译的class文件反编译看到。

    在实际应用中,为了获得和泛型有关的信息,Java就新增了几种类型来代表不能被归一到Class类中的类型,但又和基本数据类型齐名的类型,通常使用的是如下两个:

  • GenericType:表示一种元素类型是参数化的类型或者类型变量的数组类型。
  • ParameterizedType:表示一种参数化的类型。

    为什么要引入这两种呢,实际上,在通过反射获得成员变量时,Field类有一个方法是getType,可以获得该字段的属性,但是这种属性如果是泛型就获取不到了,所以才引入了上面两种类型。

看下面一个例子:

Student类

// 为了方便演示,已删除无关代码
public class Student {
    private Map<String,Integer> score ;
}

处理泛型的函数

// 处理泛型
private static void getGenericType(Class c) {
    Field field = null;
    try {
        field = c.getDeclaredField("score");
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    }
    System.out.println("score的类型是:" + field.getType());
    Type genericType = field.getGenericType();
    if (genericType instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) genericType;
        Type rawType = parameterizedType.getRawType();
        System.out.println("原始类型:" + rawType);
        // 获得泛型类型的泛型参数
        Type[] typeArgs = parameterizedType.getActualTypeArguments();
        // 打印泛型参数
        for (int i = 0; i < typeArgs.length; i++) {
            System.out.println("第" + i + "个泛型类型是:" + typeArgs[i]);
        }
    } else {
        System.out.println("获取泛型信息失败");
    }
}

结果

工厂模式优化

将反射用于工厂模式

先来看看,如果不用反射的时候,的工厂模式吧:

interface fruit{
    public abstract void eat();
}

class Apple implements fruit{
    public void eat(){
        System.out.println("Apple");
    }
}

class Orange implements fruit{
    public void eat(){
        System.out.println("Orange");
    }
}
// 不用反射的构造工厂类
class Factory{
    public static fruit getInstance(String fruitName){
        fruit f=null;
        if("Apple".equals(fruitName)){
            f=new Apple();
        }
        if("Orange".equals(fruitName)){
            f=new Orange();
        }
        return f;
    }
}
class hello{
    public static void main(String[] a){
        fruit f=Factory.getInstance("Orange");
        f.eat();
    }
}

这样,当我们在添加一个子类的时候,就需要修改工厂类了。如果我们添加太多的子类的时候,改的就会很多。

现在我们看看利用反射机制:

class Factory {
    // className为完整的全限定名
    public static fruit getInstance(String className) {
        fruit f = null;
        try {
            f = (fruit) Class.forName(className).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return f;
    }
}

class Main{
    public static void main(String[] a) {
        // 传入的为完整的全限定名,比如"com.baidu.xx.xx.xx"
        // 这里我都放在统一目录下,所以直接写名字了
        fruit f = Factory.getInstance("Apple");
        if(f != null) {
            f.eat()
        }
    }
}

好处:不论添加多少个水果,工厂类都不需要修改。

动态代理

看我另一篇博文

 

参考:

https://blog.csdn.net/carson_ho/article/details/80921333

https://www.codemore.top/p/c66895f0-176f-3e00-94cb-994110faf023

https://juejin.im/post/5afaeb456fb9a07aa43c5e3b

https://blog.csdn.net/gdutxiaoxu/article/details/68947735

https://juejin.im/post/5b6aca85e51d45194b194a5

https://blog.csdn.net/fanwenjieok/article/details/46861433

 

猜你喜欢

转载自blog.csdn.net/CrankZ/article/details/81570900