Java中反射的相关知识及应用

概述

关于Java中的反射概念,我摘取了维基百科上的解释。
反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。简
单来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

这里要特别注意术语“反射”和“内省”(type introspection)的关系。内省(或称“自省”)机制仅指程序在运行时对自身信息(称为元数据)的检测;反射机制不仅包括要能在运行时对程序自身信息进行检测,还要求程序能进一步根据这些信息改变程序状态或结构。
概念是概念,我们还是通过学习反射相关的一些类和方法加深理解。
下面是反射中主要涉及到的操作。
获取字节码的三种方式
通过字节码文件创建类的对象
通过字节码文件获取类中的字段
通过字节码文件获取类中的方法
应用:利用反射越过泛型检测

获取字节码的三种方式

我将三种方式都放到了一个类中,在这个Java文件中,我们另外建了一个工具类(Person)用来验证我们获取字节码的三种方式是否正确。

package com.codepig.blog.反射;

public class GetByreCodeFile {

    public static void main(String[] args) throws Exception {
        
        //方式一:通过Class类中的forName方法获取
        Class clazz1 = Class.forName("com.codepig.blog.反射.Person");
        
        //方式二:通过对象名.getClass的方式获取(getClass方法是所有类的父类Object类中的方法)
        //先新建一个Person对象
        Person p = new Person(22,"spy");
        Class clazz2 = p.getClass();
        
        //方式三:直接通过类名.class的方式创建
        Class clazz3 = Person.class;
        
        //检测通过这三种方式获得的字节码是否一致
        System.out.println(clazz1 == clazz2);
        System.out.println(clazz2 == clazz3);
    }

}

class Person{
    int age;
    String name;
    
    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }
}

输出结果:

true
true

从输出结果我们也可以看到三种方式获取的字节码都是一样的。

通过我们前面所说的三种方式可以获取类的字节码文件,通过字节码文件我们我们可以创建类的对象,有了对象,也才可以聊关于获取字段和方法的内容。

package com.codepig.blog.反射;

import java.lang.reflect.Constructor;

public class GetInstance {

    public static void main(String[] args) throws Exception {
        
        //老规矩:一切反射的操作前提是先获取类字节码文件
        Class clazz = Class.forName("com.codepig.blog.反射.Coder");
        
        //创建对象的第一种方式:直接调用Class类中的newInstance方法获取
        //clazz.newInstance();//直接调用此方法返回的是Object类(可以通过加.的方法验证)需要强制类型转换
        Coder c1 = (Coder)clazz.newInstance(); //注意:注意这里调用的无参构造方法,因此类中一定要有无参构造方法
        
        //创建对象的第二种方式:先调用Class类中的getConstructor方法获取类的构造方法,再利用构造方法创建对象
        Constructor c = clazz.getConstructor(Integer.class, String.class);
        Coder c2 = (Coder)c.newInstance(20,"zz");
        System.out.println(c2.name);
    }

}

class Coder{
    Integer age;
    String name;
    
    public Coder() {
        
    }
    public Coder(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
}

两种创建对象的方式,第一种通过字节码直接调用newInstance的方法,调用的是类的无参构造器,如果需要对类的字段赋值,需要额外的操作,这个newInstance方法没有传入参数。
第二种先通过字节码获取有参构造器,然后再通过构造器直接创建有参对象的方法相对于第一种方式的好处是省去对字段赋值的操作。
需要注意的一点是:

//第一种newInstance方法是在Class类中
public T newInstance()
        throws InstantiationException, IllegalAccessException

//第二种newInstance方法是在Constructor类中
public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException

通过反射获取类中的字段

我们知道无论是类中的字段还是方法都有私有和公有之分,对私有字段及方法的获取和对公有字段方法及属性的获取是不一致的。

获取非私有字段

package com.codepig.blog.反射;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class GetPubField {

    public static void main(String[] args) throws Exception {
        //获取公共方法的前提肯定是先获取类的字节码文件,三种方式任意一种都可以
        Class clazz = Class.forName("com.codepig.blog.反射.Student");
        
        //通过Class类中的getField方法获取字节码文件对应类中的字段。
        Field f = clazz.getField("age");
        
        //创建一个Student对象
        Constructor c = clazz.getConstructor(Integer.class,String.class);
        Student myGirl = (Student)c.newInstance(20,"lht");
        
        //通过调用Field中的set方法设置指定对象的字段值 
        f.set(myGirl, 18);
        System.out.println(myGirl.age);
    }

}

package com.codepig.blog.反射;

class Student{
    public Integer age;
    String name;
    
    public Student() {}
    public Student(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
}

这里需要注意的一点是,如果我们获取的字段不是public修饰符修饰的,会报异常(nosuchfieldexception),也就是找不到我们要找的属性,即便是default、protected等修饰符。

获取私有字段

私有字段的获取和非私有字段的获取主要有一点不同就是:非私有字段获取的过程中需要去除其私有属性。

package com.codepig.blog.反射;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class GetPriField {

    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("com.codepig.blog.反射.Student");
        Constructor c = clazz.getConstructor(Integer.class ,String.class);
        Student stu = (Student)c.newInstance(20,"spy");
        
        //获取私有属性name
        //通过字节码文件获取私有属性
        Field f = clazz.getDeclaredField("name");
        //去除私有属性
        f.setAccessible(true);
        //设置属性值
        f.set(stu, "ssss");
        System.out.println(stu.getName());
    }

}

输出结果

ssss

这里也有一点需要注意的是,即便我们去除了私有性,也不能通过对象名.属性的方式获取私有属性,还是通过get方法获取。

通过字节码文件获取类中的方法

同属性一样,方法也有私有公有之分,因此他们获取的途径类似

package com.codepig.blog.反射;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class GetPubMethod {

    public static void main(String[] args) throws Exception {
        //老规矩,一切的反射活动都需要经历几步:获取字节码文件、构造对象
        Class clazz = Class.forName("com.codepig.blog.反射.Student");
        Constructor c = clazz.getConstructor(Integer.class,String.class);
        Student stu = (Student)c.newInstance(33,"spy");
        
        //获取公有方法
        Method m = clazz.getMethod("show");
        //通过invoke方法让指定对象调用此方法
        m.invoke(stu);
        
        System.out.println("--------------");
        
        //获取私有方法
        //通过字节码获取私有方法
        Method m1 = clazz.getDeclaredMethod("hasGf",String.class);
        //去除私有属性
        m1.setAccessible(true);
        m1.invoke(stu, "有");
    }
}

输出结果:

我是:spy
我今年33岁了
我有女朋友

可以看出获取方法和获取字段是相似的。

应用:利用反射越过泛型检测

我们知道集合当中添加泛型检测是为了保证传入的数据是程序员指定的数据类型,但是泛型限制会在编译成class文件后就被擦除了(这一点我们可以从反编译class文件成的Java文件看出),它并不会对程序的运行效率等有任何的影响,只是对数据传入加了一个限制。
那我们是不是就不可以添加泛型限制之外的数据类型了呢,面试中经常问到这样一道题(我听别人说的面试中经常遇到(手动狗头)):如何利用反射越过泛型检测?

主体思路就是:我们不使用我们定义的添加了泛型的集合去直接操作元素,我们直接获取对应集合的字节码文件(其中所谓的泛型已经被擦除),然后通过该字节码文件映射得到其中的方法(说的很高端,其实就是getMethod(手动滑稽)),再使用此方法对我们的集合添加元素。

import java.lang.reflect.Method;
import java.util.ArrayList;

public class ReflectApplication1 {

    public static void main(String[] args) throws Exception, SecurityException {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(10);
        //list.add("sdd");             //error 字符串无法添加到泛型为包装类的集合当中
        
        Class clazz = Class.forName("java.util.ArrayList");
        Method m = clazz.getMethod("add", Object.class);
        m.invoke(list, "sdd");        //true  通过反射获取add方法,编译时已经擦除了泛型检测
        System.out.println(list);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值