Java成长记 反射

反射是Java中的一个非常重要的概念,在之前的写代码过程中,也不止一次的听说过反射和使用反射来实现某一目的。但是还是觉得有些云里雾里,不够清晰。今天就更系统的学习了反射的相关知识,因为这是Java高级开发必须了解的,对个人掌握Java开发有很好的提高作用。
在Java中,有两样东西并不是面向对象的,一是基本数据类型,如int a = 5,但他存在包装类来弥补,二是static静态的东西,他是属于类而不是对象的。这是两个特例,实际我们在面向对象开发学习的过程中,一直存有一个观念 在面向对象的世界里,万事万物皆对象。那么问题来了,我们所写的每一个类,比如String类,User类,他们是不是对象呢?又是谁的对象?

1 类是对象

类是对象,类是java.lang.Class类的实例对象。这个对象我们称之为该类的类类型(class type)。

1.1 类类型的三种表示方法(eg:A a = new A())
  1. 已知类名A
    Class c1 = A.class;
    实际上在告诉我们任何一个类都有一个隐含的静态变量class
  2. 已知类A的实例对象a
    CLass c2 = a.getClass();
  3. 已知类A的全称,比如com.gai.A
    Class c3 = Class.forName(“com.gai.A”);
1.2 一个类只可能是Class类的一个实例对象

比如代码上输出c1==c2和c1==c3,结果都为true,可知c1,c2,c3其实是同一实例对象。

1.3 通过类类型创建该类的对象实例

意思就是通过c1或c2或c3创建A类的实例对象

A a = (A) c1.newInstance();//获取A的实例

需要注意的是,实例化成功的先决条件是类A 必须有 无参构造函数

2 动态加载类

上面所说的Class.forName(“类的全称”);不仅表示了类的类类型,还代表了动态加载类。那么是按照什么区分动态加载类和静态加载类呢?我们必须要区分编译、运行。

编译时刻加载类是静态加载类,运行时刻加载类是动态加载类

2.1 静态加载类

因为我们现在基本都使用了IDE进行开发,编译和运行工作都由IDE代劳,所以这次我们用最原始的记事本来写个简单的示例来进行区分:

示例代码:

class Office
{
    public static void main(String[] args) 
    {
        //new 创建对象 是静态加载类,在编译时刻就需要加载所有可能使用到的类
        if("Word".equals(args[0]))
        {
            Word w = new Word();
            w.start();
        }
        if("Excel".equals(args[0]))
        {
            Excel e = new Excel();
            e.start();
        }
    }
}
class Word {
    public void start() {
        system.out.println("Word...start()");
    }
}

运行结果:
静态加载类
我们在cmd黑窗口中使用javac命令进行编译,会发现程序编译报错,我们都知道原因是因为不存在Excel类,所以编译失败。可是我们也不一定会用到Excel类啊,比如我只想使用Word,那为什么还会报错呢?原因就是这个时候程序进行的是类的静态加载。new创建对象是静态加载类,在编译时刻就需要加载所有可能使用到的类。比如此时我想使用Word类,但是因为Excle类不存在,所以程序报错,Word类也无法使用。

2.2 动态加载类

现实生活中,我们希望只要Word类存在我们就可以使用Word类,当我们使用Excel类的时候再报错告诉我们Excel类不存在。这就是静态加载存在的问题,那么该如何解决这个问题,实现类用则加载,不用就不加载的目的呢?答案就是采用动态加载类。

示例代码:

class OfficeBetter
{
    public static void main(String[] args) 
    {
        try
        {
            //动态加载类,在运行时刻加载
            Class c = Class.forName(args[0]);
            //通过类类型,创建该类的对象
            Word word = (Word) c.newInstance();
            word.start();
            //Excel excel= (Excel) c.newInstance();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }
}
class Word {
    public void start() {
        system.out.println("Word...start()");
    }
}

运行结果:
cmd运行结果
从cmd中就可以发现只有输入Excel时程序才会报错,提示Excel类不存在。

2.3 进一步优化(使用接口)

因为我们可以通过类类型的newInstance()方法创建该类对象,但是我们必须要往具体的类做强制类型转换。这里又存在一个问题,如果我们往Word做转换,加载的类是Excel怎么办?如果以后再加一个PPT类怎么办?我们都知道软件开发中有一个原则,那就是对拓展开放,对修改关闭。所以我们可以给他们统一标准,只要他们都是这个标准就可以了。
代码示例:

interface OfficeAble {
    public void start();
}
class OfficeBetter
{
    public static void main(String[] args) 
    {
        ...
        OfficeAble word = (OfficeAble) c.newInstance();
        word.start();
        ...
    }
}
class Word implement OfficeAble {
    ...
}
class Excel implement OfficeAble {
    ...
}

以后要修改的时候,不需要再重新编译之前的代码,只需要添加新有的功能和代码。比如有一个PPT类,那么我们只需要新建一个PPT类实现OfficeAble接口即可。

2.4 基本数据类型的类类型

除了类存在类类型,我们还需要知道,基本数据类型也存在类类型,即存在class隐含成员变量。void关键字也存在类类型。

示例代码:

public class ClassDemo2 {
    public static void main(String[] args) {
        Class c1 = int.class;//int 的类类型
        Class c2 = String.class;//String的类类型
        Class c3 = double.class;//double数据类型的,类类型
        Class c4 = Double.class;//Doouble类的类类型,注意区分
        Class c5 = void.class;

        System.out.println(c1.getName());//int
        System.out.println(c2.getName());//打印类的全称 java.lang.String
        System.out.println(c2.getSimpleName());//不包含包名的类的全称 String
        System.out.println(c5.getName());//void
    }
}

3 获取方法信息

3.1 Method类

因为万物皆对象,所以方法也是对象,是Method类的对象。这里主要是通过两个方法来获取当前类的方法对象。主要的区别在于能不能加载到自己private和protected的方法和加载到继承自父类的方法。
- getMethods() 获取所有的public的函数,包括父类继承而来的
- getDeclaredMethods() 获取的是所有该类自己声明的方法,不问访问权限

示例代码:

    /**
     * 打印类的成员函数信息
     * @param obj 该对象所属类的信息
     */
    public static void printMethodMessage(Object obj) {
        //要获取类的信息,首先要获取类的类类型
        Class c = obj.getClass();//传递的是哪个子类的对象 c就是该子类的类类型
        //获取类的名称
        System.out.println("类的名称是:"+c.getName());
        /*
         * Method类 方法对象
         * 一个成员方法就是一个Method对象
         * getMethods() 获取所有的public的函数,包括父类继承而来的
         * getDeclaredMethods() 获取的是所有该类自己声明的方法,不问访问权限
         */
        Method[] ms = c.getMethods();//c.getDeclaredMethods()
        for (int i = 0; i < ms.length; i++) {
            //得到方法的返回值类型的类类型 如返回值为String,则值为String.class
            Class returnType = ms[i].getReturnType();
            System.out.print(returnType.getName()+" ");
            //得到方法的名称
            System.out.print(ms[i].getName()+" (");
            //获得参数类型--->得到的是参数列表的类型的类类型
            Class[] paramTypes = ms[i].getParameterTypes();
            for (Class class1 : paramTypes) {
                System.out.print(class1.getName()+",");
            }
            System.out.println(")");
        }
    }

运行结果:略

3.2 getClass()源码解析

或许obj.getClass()获得当前类的类类型会感觉到迷惑,因为传进来的是一个Object对象,那么他的类类型不该是Object.class么?我们查看Object的源码

public final native Class<?> getClass();

可以发现这个方法由native进行修饰,这是一个本地方法,这里涉及到Java的JNI机制,有兴趣的可以去google一下,简单的说就是native本地方法 java来声明,c语言来实现,java来调用。所以传递的是哪个子类的对象 c就是该子类的类类型。

4 获取成员变量信息

4.1 Fieled类

成员变量也是对象,java.lang.reflect包中的Fieled类封装了关于成员变量的操作。与获取成员函数的方法类似,也有自己的方法获取成员变量:
- getFields() 方法获取的是所有的public的成员变量的信息
- getDeclaredFields() 获取的是该类自己声明的成员变量的信息

示例代码:

    /**
     * 打印类的成员变量信息
     * @param obj 该对象所属类的信息
     */
    public static void printFieldMessage(Object obj) {
        Class c = obj.getClass();
        /*
         * 成员变量也是对象
         * java.lang.reflect.Filed
         * Fieled类封装了关于成员变量的操作
         * getFields()方法获取的是所有的public的成员变量的信息
         * getDeclaredFields()获取的是该类自己声明的成员变量的信息
         */
        Field[] fs = c.getDeclaredFields();
        for (Field field : fs) {
            //获取成员变量的修饰符
            String fieldModify = Modifier.toString(field.getModifiers());
            //得到成员变量的类型的类类型 
            Class fieldType = field.getType();
            //得到成员变量类型的名字
            String typeName = fieldType.getName();
            //得到成员变量的名称
            String fieldName = field.getName();
            System.out.println(fieldModify+" "+typeName+" "+fieldName);
        }
    }

运行结果:略

4.2 获取修饰符

有时候我们希望能够知道成员变量带有什么修饰符,或者是否存在某个修饰符,那么我们该如何做呢?具体代码实现的上面示例中已给出,现在简要谈谈原因,(感兴趣可以去看看叉叉哥的文章:Modifier解析

  • Member接口
    Member表示一个类中的成员,包括成员变量、方法、构造方法三种实现,上面用到的Field就是Member的一种。Member接口有个方法:
    int getModifiers() 返回由此 Member所表示的成员或构造方法的修饰符的int值。
  • Class类
    Class类中也存在一个方法:
    int getModifiers() 返回此类或接口以整数编码的修饰符的int值。
  • Modifier类
    我们需要调用需要用到java.lang.reflect.Modifier这个类,Modifier提供了很多静态方法。如public static String toString(int mod)就可以输出该整数对应的所有的修饰符。public static boolean isPublic(int mod)就可以判断该整数对应的是不是包含public修饰符。

5 获取构造函数信息

5.1 Constructor类

类的构造函数也是成员变量,java.lang.reflect.Constructor中封装了构造函数的信息。同上,存在两种获取构造函数的方法:
- getConstructors() 方法获取的是所有的public的构造函数的信息
- getDeclaredConstructors() 获取的是该类自己声明的构造函数的信息

示例代码:

    /**
     * 打印类的构造函数信息
     * @param obj 该对象所属类的信息
     */
    public static void printConstructMessage(Object obj) {
        Class c = obj.getClass();
        /*
         * 构造函数也是对象
         * java.lang.reflect.Constructor中封装了构造函数的信息
         * getConstructors()方法获取的是所有的public的构造函数的信息
         * getDeclaredConstructors()获取的是该类自己声明的构造函数的信息
         */
        Constructor[] constructors = c.getDeclaredConstructors();
        for (Constructor constructor : constructors) {
            System.out.print(constructor.getName()+" (");
            //获取构造函数的参数列表--->得到的是参数列表的类类型
            Class[] paramTypes = constructor.getParameterTypes();
            for (Class class1 : paramTypes) {
                System.out.print(class1.getName()+",");
            }
            System.out.println(")");
        }
    }

运行结果:略

5.2 工具类的使用

可以把上面的三个方法放到一个ClassUtil工具类中,保存到自己的代码工具箱中。可进行重用,话说程序员都是很懒的生物,编码过程中有个原则就是“不写重复的代码,不做重复的工作”,还得努力扩充自己的工具库啊。

6 方法反射的基本操作

  • 如何获取某个方法
    方法的名称和方法的参数才能唯一决定某个方法
  • 方法反射的操作
    method.invoke(对象,参数列表)

示例代码:

public class MethodDemo1 {

    public static void main(String[] args) {
        try {
            //获取print(int,int)方法
            //1.要获取一个方法就是获取该类的信息,获取类的信息首先要获取类的类类型
            A a = new A();
            Class c = a.getClass();
            /*
             * 2.获取方法 名称和参数的类类型列表来决定
             * getMethod获取的是public的方法
             * getDeclaredMethod自己声明的方法
             */
//          Method m = c.getMethod("print", new Class[]{int.class,int.class});
            Method m = c.getMethod("print", int.class,int.class);//...称之为可变参数

            //方法的反射操作 用m对象来进行方法调用,和a.print调用的效果完全相同
            //方法如果没有返回值返回null,有返回值返回具体的返回值
//          Object o = m.invoke(a, new Object[]{10,20});
            Object o = m.invoke(a, 10,20);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

class A {
    public void print(int a, int b) {
        System.out.println(a+b);
    }
}

运行结果:30
有兴趣深入了解的可以去看看寂静沙滩的JAVA深入研究——Method的Invoke方法

7 通过反射了解集合泛型的本质

我们在编程过程中,经常使用到的一个概念就是泛型,最常用的就是在集合的使用中,我们通过指定泛型类型来限制集合所能添加的值的类型。比如

        List<String> list = new ArrayList<String>();
        list.add(100);//报错

提示参数类型不匹配,不能添加。那么,到底什么是泛型呢?他是为什么能够起到数据类型的限制的呢?现在我们通过Class,Method来认识泛型的本质。
示例代码:

        List<String> list = new ArrayList<String>();
        list.add("hello");

        List list1 = new ArrayList();

        Class c1 = list.getClass();
        Class c2 = list.getClass();
        System.out.println(c1 == c2);//true

运行结果:true

我们都知道一个类只有一个类类型实例,上面代码中c1==c2结果返回true,说明编译之后集合的泛型是去泛型化的,也就是Java中集合的泛型,是防止错误输入的,只在编译阶段有效,绕过编译就无效了,而类类型就类似于字节码文件,因为.class就是字节码,运行时进行判断的。所以我们必须要区分编译与运行。
那么我们可不可以在list中添加非String类型的数据呢?根据上面运行结果我们可以想到,因为泛型是在编译阶段起作用,只要我们绕过了编译就饶过了泛型,就可以添加了。这里就可以用反射来进行验证。

示例代码:

try {
            Method m = c1.getMethod("add", Object.class);
            m.invoke(list, 100);
            System.out.println(list.size());//绕过了编译就饶过了泛型
            System.out.println(list);
//          for (String string : list) {
//              System.out.println(string);
//          }//现在不能这样编译,因为会进行类型判断,报错
        } catch (Exception e) {
            e.printStackTrace();
        }

运行结果:
2
[hello, 100]

从运行结果中我们知道我们成功的绕过了泛型,但是此时我们是不能用foreach进行遍历的,因为他会进行类型验证,会报错ClassCastException。所以,我们必须要牢记一点:

反射的操作都是绕过编译,运行时执行

有兴趣的可以去看看Cedar老师的视频反射——Java高级开发必须懂的。谢谢老师!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值