学习反射看这一篇就够了

关注公众号:小李不秃

1. 反射

Java 分编译期和运行期

编译方式说明:

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

我们带着以下几个问题去学习今天的知识

  • 反射是什么?
  • 反射的作用是什么?
  • 反射的优点是什么?缺点是什么?

1.1 什么是反射

反射(Reflection)是 Java 的特性之一,它可以让运行中的 Java 程序获取自身的信息,并且可以操作类或者对象的内部属性。

Oracle 官方对反射的解释是:

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.

The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

通过反射,我们可以在程序运行时获得程序集中每一个类型的成员和成员信息。我们平时所用的 new 去创建的对象的类型,是在编译期就确定下来了。而 Java 反射可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制创建对象,即使这个对象的类型在编译期是未知的。

反射的核心是 JVM 在运行时才会动态加载类、调用方法和访问属性,它不需要在编译期知道运行的对象是谁。

Java 反射主要提供以下的功能:

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法

我们可以在运行时取到「任意」你想要的类、对象、变量、方法等。

注:反射是在运行时操作的,而不是编译时

1.2 反射的主要用途

  • 实现工厂模式和代理模式等设计模式。
  • JDBC 的数据库连接。
  • Spring、Struts 等框架,使用反射在运行时动态加载需要加载的对象。
  • IDE 开发工具的提示,比如当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法。

1.3 反射的优缺点

优点:

  • 可以在运行期对类型进行判断,动态类加载等操作。
  • 提高代码的灵活度。例如:JDBC 可以动态连接数据库。

缺点:

  • 性能问题

    因为反射包括了一些动态类型,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些直接调用慢的多。所以尽量避免在经常被执行的代码或者对性能要求很高的程序中使用反射。

    (反射大概比直接调用慢 50 ~ 100 倍,但是需要在执行 100 万遍的时候才会有所感觉)

  • 安全限制

    使用反射技术要求程序必须在一个没有安全限制的环境中运行。

  • 内部暴漏

    反射允许代码执行一些在正常情况下不被允许的操作(访问私有的属性或方法),所以使用反射可能会导致意料之外的副作用 —— 代码有功能上的错误,降低可移植性。反射破坏了代码的抽象性,因此当平台发生改变的时候,代码的行为就可能也随着变化。

提问:Java 反射可以访问和修改私有成员变量,那封装成 private 还有意义么?

既然小偷可以访问和搬走私有成员家具,那封装成防盗门还有意义么?这是一样的道理,并且 Java 从应用层给我们提供了安全管理机制——安全管理器,每个 Java 应用都可以拥有自己的安全管理器,它会在运行阶段检查需要保护的资源的访问权限及其它规定的操作权限,保护系统免受恶意操作攻击,以达到系统的安全策略。

所以其实反射在使用时,内部有安全控制,如果安全设置禁止了这些,那么反射机制就无法访问私有成员。

1.4 具体使用

1.4.1 Class

Class 存放着对应类型的运行时信息。在 Java 程序运行时,Java 虚拟机为所有类型维护一个 java.lang.Class 对象。该 Class 对象存放着所有关于该对象的运行时信息。

1.4.1.1 获取 Class

那我们如何获取想要的 Class,look this Code

    public static void main(String[] args) {
        // 方式1:Object.getClass()
        // Object 类的 getClass() 方法返回一个 Class 类实例
        String name = "不是秃头的小李程序员";
        Class<?> classType = name.getClass();
        System.out.println("Object.getClass() classType: " + classType);

        // 方式2:T.Class
        // T 是任意 Java 类型
        Class<?> classType2 = String.class;
        System.out.println("T.Class classType: " + classType2);

        // 方式3:Class.forName
        try {
            Class<?> classType3 = Class.forName("java.lang.String");
            System.out.println("Class.forName classType: " + classType2);
            // 根据 className 没有找到类,会抛出 ClassNotFoundException 异常
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

获取 Class 的三种方法

  • getClass()
  • T.class
  • Class.forName

这三种用法需要根据具体的场景灵活去使用,比如 JDBC 获取连接的数据库类型,就通过 Class.forName(“类路径”)。

1.4.1.2 获取父类 Class

通过 getSuperclass() 方法获取父类的 Class,示例如下:

Class<?> superclass = Integer.class.getSuperclass();
System.out.println(superclass);
System.out.println(superclass.getSuperclass());
System.out.println(superclass.getSuperclass().getSuperclass());
System.out.println(superclass.getSuperclass().getSuperclass().getSuperclass());

运行结果

class java.lang.Number
class java.lang.Object
null
Exception in thread "main" java.lang.NullPointerException

可以看到 Integer 的父类是 Number,Number 的父类是 Object,Object 就没有父类了,所以在 null 之后会抛出空指针的异常。

1.4.1.3 小结

获取到想要的 Class 了,就可以获取到它的一切信息。

在获取想要的信息前,不妨先了解个知识点。

方法中带与不带「Declared」的区别

  1. 不带「Declared」的方法支持取出包括继承、公有(public)的 Field、Method 和 Constructor。
  2. 带「Declared」的方法支持取出包括当前类所有的构造函数(包括公有和私有的,不包括继承)的 Field、Method 和 Constructor。

1.4.2 Field

1.4.2.1 获取 Field

如何通过 Class 实例获取字段信息。Class 类提供了以下几个方法来获取字段:

  • Field getField(String name):根据字段名获取某个 public 的 field
  • Field[] getFields():获取所有 public 的 field
  • Field getDeclaredField(String name):根据字段名获取当前类的某个 field
  • Field[] getDeclaredFields():获取当前类的所有 field

快快快,show me code

public class FiledTest1 {
    public static void main(String[] args) throws NoSuchFieldException {
        Class stdClass = Student.class;
        // 获取 public 字段 "score"
        System.out.println(stdClass.getField("score"));
        // 获取继续的 public 字段 "name"
        System.out.println(stdClass.getField("name"));
        // 获取 private 字段 "grade"
        System.out.println(stdClass.getDeclaredField("grade"));
    }
}

class Student extends Person{
    public int score;
    private int grade;
}

class Person{
    public String name;
}

运行结果:

public int com.javastudy.reflection.Fields.Student.score
public java.lang.String com.javastudy.reflection.Fields.Person.name
private int com.javastudy.reflection.Fields.Student.grade

1.4.2.2 获取 Field 的信息

一个 Filed 对象包含了一个字段的所有信息:

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

The java.lang.reflect.Method.getModifiers() method returns the Java language modifiers for the method represented by this Method object, as an integer. The Modifier class should be used to decode the modifiers.

getmodifiers()方法以整数的形式返回该方法对象所表示的方法的 Java 语言修饰符。应该使用修饰词类来解码修饰词。

public class FieldTest2 {

    private final String name = "不是秃头的小李程序员";

    public static void main(String[] args) throws NoSuchFieldException {
        Class c = FieldTest2.class;
        Field field = c.getDeclaredField("name");
        int mod = field.getModifiers();
        System.out.println("name: " + field.getName());
        System.out.println("type: " + field.getType());
        System.out.println("final: " + Modifier.isFinal(mod));
        System.out.println("public: " + Modifier.isPublic(mod));
        System.out.println("protected: " + Modifier.isProtected(mod));
        System.out.println("private: " + Modifier.isPrivate(mod));
        System.out.println("static: " + Modifier.isStatic(mod));
    }
}

运行结果:

name: name
type: class java.lang.String
finaltrue
publicfalse
protectedfalse
privatetrue
staticfalse

1.4.2.3 获取字段值

我们拿到了 Field,该通过 Field 获取该字段对应的值。我们还是用上面的例子来获取 name 值。

public class FieldTest3 {

    private final String name = "不是秃头的小李程序员";

    public static void main(String[] args) throws Exception {
        Object object = new FieldTest3();
        Class c = FieldTest3.class;
        Field field = c.getDeclaredField("name");
        Object value = field.get(object);
        System.out.println(value);
    }
}

运行结果:

不是秃头的小李程序员

我们通过 get() 来获取 Field 的值,那么咱们在看一个下面的例子:

public class FieldTest4 {

    public static void main(String[] args) throws Exception 
        Object animal = new Animal("不是秃头的小李程序员 Animal111");
        Class c = Animal.class;
        Field field = c.getDeclaredField("name");
        Object value = field.get(animal);
        System.out.println(value);
//        Animal animal = new Animal();
//        animal.testFiled();
    }
}

class Animal {
    private String name;

    public Animal() {
    }

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

    public void testFiled() throws Exception {
        Object animal = new Animal("不是秃头的小李程序员 Animal222");
        Class c = Animal.class;
        Field field = c.getDeclaredField("name");
        Object value = field.get(animal);
        System.out.println(value);
    }
}

运行结果:

Exception in thread "main" java.lang.IllegalAccessException: Class com.javastudy.reflection.Fields.FieldTest4 can not access a member of class com.javastudy.reflection.Fields.Animal with modifiers "private"

WTF?竟然出现异常了,小李你是在玩我么,第一次就可以,第二次又提示没有权限,到底能不能获取我想要的值。当然能,不能我就秃给你看。

我们只需要在 filed.get() 的上一步加上下面的代码就可以了。管你是 public 还是 private.

field.setAccessible(true);

那我们想一想为什么第一次不加上面的代码也能访问呢?

因为是在自己的类里访问的,就拿你自己想一想,你有一个鼻子和两个耳朵,它们就是你私有的(private),你可以随便摸他们和抠他们(呕),但是别人想碰的时候,必须要经过你的同意才行(setAccessible(true))。这么理解下刚才的代码,你就明白了。如果还不明白可以打开上面的两段注释:

Animal animal = new Animal();
animal.testFiled();

运行结果:

不是秃头的小李程序员222

1.4.2.4 设置字段值

通过 Field 实例既能获取指定实例的字段值,也可以设置字段的值。

设置字段值通过 Fieldset 方法实现。

// 第一个参数是指定的实例
// 第二个参数是待修改的值
void set(Object obj, Object value)

示例代码如下:

public class FieldTest5 {
    public static void main(String[] args) throws Exception {
        Teacher teacher = new Teacher("不是秃头的小李程序员");
        Class c = teacher.getClass();
        Field field = c.getDeclaredField("name");
        field.setAccessible(true);
        field.set(teacher,"小李不秃头");
        System.out.println(field.get(teacher));
    }
}
class Teacher{
    private String name;

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

    public String getName() {
        return name;
    }
}

打印结果:

小李不秃头

为了不秃头,我容易么(此处嘤嘤嘤)。嘤完就该提问题了

Field 在 get 和 set 的 obj 参数有什么作用?

回答:我们可以通过查看 api 的注释,了解到这两个方法在获取静态实例的时候,obj 可以传 null,如果要获取对象的实例 obj 参数就不能为空,否则会返回 NullException。

1.4.2.5 小结

Java 的反射 API 提供的 Field 类封装了字段的所有信息:

  • 通过 Class 实例获取 Field 实例的方法:getField(String name),getFields(),getDeclaredField(),getDeclaredFields()
  • 通过 Field 实例获取字段信息的方法:getName(),getType(),getModifiers()
  • 通过 Field 实例可以读取或者设置某个对象的字段,如果存在访问限制,首先要调用 setAccessible(true),再访问非 public 字段。

1.4.3 Method

1.4.3.1 获取 Method

通过 Class 实例获取所有 Method 的信息,Class 类提供了以下几个方法来获取方法:

  • Method getMethod(String name, Class… parameterTypes):根据方法名和参数类型获取某个 public 的 Method

  • Method[] getMethods():获取所有 public 的 Method

  • Method getDeclaredMethod(String name, Class… parameterTypes):根据方法名和参数获取当前类的某个 Method

  • Method[] getDeclaredMethods():获取当前类的所有 Method

看一下示例代码:

public class MethodTest1 {
    public static void main(String[] args) throws Exception{
        Class c = Student.class;
        // 获取public方法getScore,参数为String;
        System.out.println(c.getMethod("getScore",String.class));
        // 获取继承的public方法getName,无参数;
        System.out.println(c.getMethod("getName"));
        // 获取private方法getGrade,参数为int;
        System.out.println(c.getDeclaredMethod("getGrade",int.class));
    }
}

运行结果:

public int com.javastudy.reflection.Methods.Student.getScore(java.lang.String)
public java.lang.String com.javastudy.reflection.Methods.Person.getName()
private int com.javastudy.reflection.Methods.Student.getGrade(int)

1.4.3.2 获取 Method 的信息

一个 Method 对象包含了一个方法的所有信息:

  • getName():返回方法名称,例如:「getScore」
  • getReturnType():返回方法的返回值类型,是一个 Class 实例,例如:「String.class」
  • getParameterTypes():返回方法的参数类型,是一个 Class 数组,例如:{String.class, int.class}
  • getModifiers():返回方法的修饰符,和 Field 的 getModifiers() 大同小异

示例如下:

public class MethodTest2 {
    public static void main(String[] args) throws Exception{
        Class c = Student.class;
        Method method= c.getDeclaredMethod("getGrade",int.class);

        System.out.println("name : " + method.getName());
        System.out.println("returnType : " + method.getReturnType());
        Class<?>[] parameterTypes = method.getParameterTypes();
        System.out.println("paramaterTypes的长度 : " + parameterTypes.length);
        for (Class parameterType : parameterTypes){
            System.out.println("paramaterTypes : " + parameterType);
        }
    }
}

运行结果:

name : getGrade
returnType : int
paramaterTypes的长度 : 1
paramaterTypes : int

1.4.3.3 调用方法

调用普通方法

先看示例:

public class MethodTest3 {
    public static void main(String[] args) throws Exception {
        String s = "不是秃头的小李程序员";
        Method method = String.class.getMethod("substring"int.class);
        Method method2 = String.class.getMethod("substring"int.class, int.class);
        String result = (String) method.invoke(s,7);
        String result2 = (String) method2.invoke(s,1,9);
        System.out.println(result);
        System.out.println(result2);
    }
}

运行结果:
程序员
是秃头的小李程序

分析下程序员小李是如何秃头的:

  1. 通过 Class 实例的 getMethod 方法获取Method,getMethod 的 name 和参数不同,获取的 Method 也是不同的。
  2. 使用 Method 的 invoke 方法就相当于调用该方法。invoke 的第一个参数是对象实例,后面的可变参数与方法参数一致,否则将报错。
调用静态方法

调用静态方法,无需指定实例对象,invoke 方法传入的第一个参数永远是 null 或者空值 ”“,我们看下面的例子:

public class MethodTest4 {
    public static void main(String[] args) throws Exception{
        // 获取Integer.parseInt(Stirng)方法,参数是String
        Method method = Integer.class.getMethod("parseInt", String.class);
        // 调用静态方法获取结果
        // Integer result = (Integer)method.invoke("", "12345");
        Integer result = (Integer)method.invoke(null"12345");
        System.out.println(result);
    }
}

运行结果:12345
调用非public方法

对于非public方法,我们可以通过 Class.getDeclaredMethod() 获取,但是调用的时候会抛出一个IllegalAccessException。为了调用非public方法,通过Method.setAccessible(true)允许其调用:

public class MethodTest5 {
    public static void main(String[] args) throws Exception{
        Person p = new Person();
        Method method = p.getClass().getDeclaredMethod("setName", String.class);
        method.setAccessible(true);
        method.invoke(p,"不是秃头的小李程序员");
        System.out.println(p.name);
    }
}

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

1.4.3.4 多态

如果一个 Person 定义了 hello() 方法,并且它的子类Student也重写了该方法,那么我们从 Person.class 获取的 Method 作用于 Student 实例时,调用的方法是哪个?

public class MethodTest6 {
    public static void main(String[] args) throws Exception{
        // 获取Person的hello方法
        Method method = Person.class.getMethod("hello");
        // 对Student实例调用hello方法
        method.invoke(new Student());
    }
}

public class Person {
    public void hello(){
        System.out.println("Person:hello");
    }
}

public class Student extends Person {
    public void hello(){
        System.out.println("Student:hello");
    }
}

1.4.3 .5运行结果

Student:hello

发现打印出的是Student:hello,所以使用反射调用方法时,仍遵循多态原则:即总是调用实际类型的覆盖方法。

上述的反射代码:

Method m = Person.class.getMethod("hello");
m.invoke(new Student());

相当于:

Person p = new Student();
p.hello();

1.4.3.5 小结

Java 的反射 API 提供的 Method 对象封装了方法的所有信息:

  • 通过 Class 实例获取 Method 实例的方法:getMethod(),getMethods(),getDeclaredMethod(),getDeclaredMethods()
  • 通过 Method 实例获取字段信息的方法:getName(),getReturnType(),getParameterTypes(),getModifiers()
  • 通过 Method 实例可以调用某个对象的方法:Object invoke(Object instance, Object… parameters)
  • 通过设置 setAccessible(true) 来访问非 public 方法
  • 通过反射调用方法时,仍能遵循多态原则

1.4.4 Constructor

1.4.4.1 获取 Constructor

通过 Class 实例获取所有 Constructor 的信息,Class 类提供了以下几个方法来获取方法:

  • Constructor getConstructor(Class… parameterTypes):根据参数获取 public 的 Contructor

  • Constructor[] getConstructors():获取所有 public 的 Contructor

  • Constructor getDeclaredConstructor(Class… parameterTypes):根据参数获取当前类的 Contructor

  • Constructor[] getDeclaredConstructors():获取所有当前类的 Contructor

Constructor 总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。

示例如下:

public class ContructorTest1 {
    public static void main(String[] args) throws Exception{
        Class c = Person.class;
        Person p = (Person) c.newInstance();

        Constructor cons1 = c.getConstructor(int.class);
        Person p1 = (Person)cons1.newInstance(30);

        Constructor cons2 = c.getDeclaredConstructor(String.class);
        cons2.setAccessible(true);
        Person p2 = (Person)cons2.newInstance("不是秃头的小李程序员");

        Constructor cons3 = c.getConstructor(String.class, int.class);
        Person p3 = (Person)cons3.newInstance("不是秃头的小李程序员-35",35);
    }
}
Person.class

public class Person {
    private String name;
    private int age;

    public Person() {
        System.out.println("Person");
    }

    public Person(int age) {
        this.age = age;
        System.out.println("Person age:" + age);
    }

    private Person(String name) {
        this.name = name;
        System.out.println("Person name:" + name);
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("Person toString:" + toString());
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

运行结果:

Person
Person age:30
Person name:不是秃头的小李程序员
Person toString:Person{name='不是秃头的小李程序员-35', age=35}

1.4.4.2 小结

通过上面的结果,得出以下结论:

  1. 通过 Class 实例的 newInstance() 获取到的是无参构造函数
  2. 获取有参构造函数需要通过 Class 实例获取 Constructor 实例,获取方法:getConstructor(),getConstructors(),getDeclaredConstructor(Class… parameterTypes),getDeclaredConstructors()
  3. 通过 Constructor 的 newInstance(Object… parameters) 创建实例对象
  4. 调用非 public 的 Contructor,需要通过设置 setAccessible(true) 设置允许访问,但可能会失败。

1.4.5 Interface

我们可以通过 Class 的 getInterfaces() 获取当前类实现的所有接口,示例如下:

public class ReflectionInterfaceTest {
    public static void main(String[] args) {
        Class s = Integer.class;
        Class[] interfaces = s.getInterfaces();
        for (Class c:interfaces){
            System.out.println(c);
        }
    }
}

运行结果:

interface java.lang.Comparable

2. 总结

通过本文你大概了解了反射,让我们再复习一下:

  • 反射是 Java 的特性之一,可以通过反射动态的获取对象。
  • 反射的用途:代理模式等设计模式;通过 JDBC 连接数据库;Spring 框架动态加载对象
  • 反射的优点:动态加载、提高代码的灵活度
  • 反射的缺点:性能问题、安全限制、内部暴漏
  • 反射的使用:通过获取到 Class 实例,就能获取到我们想要的所有信息,包括获取成员变量、方法和构造函数,分别对应的是 Field,Method 和 Constructor。可以通过这些类的内部方法获取信息,例如:通过getName() 获取名称。
  • 如果我们修改或者访问带有 private 的变量或方法,需要先设置 method.setAccessible(true),才可以进行后续的操作。

下一节我带你们看看反射的原理,让我们更进一步的了解它。

3. 参考

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

https://zhidao.baidu.com/question/1639472919001036380.html

https://www.liaoxuefeng.com/wiki/1252599548343744/1264803033837024

posted @ 2019-12-12 20:39  小李不秃  阅读(654)  评论(3编辑  收藏  举报