重拾Java(8)-反射

Java 在需要使用到某个类时会载入 .class 文档,在 JVM 产生 Java.lang.Class 实例代表该文档,从 Class 实例开始,就可以获得类的许多类型。 .class 文档反映了类基本信息,因而从 Class 等API取得类信息的方法就称为反射

一、Class与.class文档

Java 在真正需要某个类时才会加载对应的 .class 文档,而非在程序启动时就加载所有类,因为大部分时候我们只需要用到应用程序部分资源,有选择地加载可以节省系统资源 java.lang.Class 的实例代表 Java 应用程序运行时加载的 .class 文档,类、接口、Enum等编译过后,都会生成 .class 文档,所以 Class可以用来包含类、接口、Enum等信息 Class 类没有公开的构造函数,实例是由 JVM 自动产生,每个 .class 文档加载时, JVM 会自动生成对应的 Class 对象 可以通过 Object 的 getClass() 方法或者通过 .class 常量取得每个对象对应的 Class 对象。如果是基本类型,可以使用对象的包装类加载 .TYPE 取得 Class 对象 例如,使用 Integer.TYPE 可取得代表 int 基本类型的 Class,通过 Integer.class 取得代表 Integer.class 文档的 Class

在取得 Class 对象后,就可以操作 Class 对象的公开方法取得类基本信息 例如

package com.czy.demo;

public class Student {
    
    public static void main(String[] args) {
        Class cl=Student.class;
        System.out.println("类名称:"+cl.getName());
        System.out.println("简单类名称:"+cl.getSimpleName());
        System.out.println("包名:"+cl.getPackage());
        System.out.println("是否为接口:"+cl.isInterface());
        System.out.println("是否为基本类型:"+cl.isPrimitive());
        System.out.println("是否为数组对象:"+cl.isArray());
        System.out.println("父类名称:"+cl.getSuperclass().getName());
    }

}

输出结果为

类名称:com.czy.demo.Student
简单类名称:Student
包名:package com.czy.demo
是否为接口:false
是否为基本类型:false
是否为数组对象:false
父类名称:java.lang.Object

Java 在真正需要类时才会加载 .class 文档,即在生成对象时才会加载 .class 文档。如果只是使用类声明了一个变量,此时并不会加载.class文档,而只是让编译程序检查对应的 .class 文档是否存在

例如,在 Stduent 类中定义了 static 静态区域块,在首次加载 .class 文档时会被执行(这是默认情况下,也可以指定不执行)

public class Student {
    
    static {
        System.out.println("载入了 Student.class 文档");
    }
    
}

再来测试加载顺序

package com.czy.demo;

public class Main {
    
    public static void main(String[] args) {
        Student student;
        System.out.println("声明了 Student 变量");
        student=new Student();
        System.out.println("生成了 Student 实例");
    }
    
}

输出结果为

声明了 Student 变量
载入了 Student.class 文档
生成了 Student 实例

二、使用Class.forName()

在某些情况下,会存在事先不知道类名称,需要事后指定类名称来动态加载类的情况 可以使用 Class.forName() 方法实现动态加载类,用字符串指定类名称来获得类相关信息

package com.czy.demo;

public class Main {
    
    public static void main(String[] args) {
        try{
            Class cl=Class.forName("java.lang.String");
            System.out.println("类名称:"+cl.getName());
            System.out.println("简单类名称:"+cl.getSimpleName());
            System.out.println("包名:"+cl.getPackage());
            System.out.println("是否为接口:"+cl.isInterface());
            System.out.println("是否为基本类型:"+cl.isPrimitive());
            System.out.println("是否为数组对象:"+cl.isArray());
            System.out.println("父类名称:"+cl.getSuperclass().getName());
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
    }
    
}

输出结果

类名称:java.lang.String
简单类名称:String
包名:package java.lang, Java Platform API Specification, version 1.8
是否为接口:false
是否为基本类型:false
是否为数组对象:false
父类名称:java.lang.Object

Class.forName() 另一重载版本可以指定类名称、加载类时是否执行静态区域块、类加载器

    public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

例如 还是使用 Student 类

package com.czy.demo;

public class Student {
    
    static {
        System.out.println("执行静态区域块");
    }
    
}

测试调用顺序

public class Main {
    
    public static void main(String[] args) {
        try{
            Class cl=Class.forName("com.czy.demo.Student",false,Main.class.getClassLoader());
            System.out.println("已载入class文档");
            Student student;
            System.out.println("声明了Student变量");
            student=new Student();
            System.out.println("生成Student实例");
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
    }
    
}

输出结果

已载入class文档
声明了Student变量
执行静态区域块
生成Student实例

由于指定了加载类时不执行静态区域块,所以在建立了 Stduent 实例后,静态区域块才被执行。类加载器则是使用 Main.class 文档的类加载器

因此,只有一个参数的 Class.forName(String name) 方法,等同于

   Class.forName(className,  true,  currentLoader)

即默认加载静态区域块,使用当前类的类加载器来载入类

三、从Class获得信息

Class对象代表加载的.class文档,取得Class对象后,就可以取得.class文档中记载的信息,如包、构造函数、数据成员、方法成员等 每一种信息都对有对应的类型,如包对应的类型是 java.lang.Package,构造函数对应的类型是 java.lang.reflect.Constructor

例如,先来为Student类增添多种类型的不同信息

package com.czy.demo;

public final class Student {
    
    enum Gender{
        male,female
    }
    
    private String name;
    
    public int age;
    
    protected Gender gender;
    
    public Student(String name,int age){
        
    }
    
    public Student(String name,int age,Gender gender){
        
    }
    
    private Student(){
        
    }
    
    public String getName() {
        return name;
    }
    
    private int getAge(){
        return age;
    }
    
    protected Gender getGender(){
        return gender;
    }
    
}

再来获取各种信息

public class Main {

    public static void main(String[] args) {
        try {
            Class cl = Class.forName("com.czy.demo.Student");
            
            // 取得包对象
            Package p = cl.getPackage();
            System.out.println("包名:" + p.getName());
            // 访问修饰符
            int modifier = cl.getModifiers();
            System.out.println("类访问修饰符:" + Modifier.toString(modifier));

            System.out.println();
            
            //取得构造函数信息
            Constructor[] constructors=cl.getConstructors();
            for(Constructor constructor:constructors){
                System.out.print("访问修饰符:" + Modifier.toString(constructor.getModifiers()));
                System.out.print("   构造函数名:"+constructor.getName());
                System.out.println();
            }
            
            System.out.println();
            
            //取得声明的数据成员
            Field[] fields = cl.getDeclaredFields();
            for (Field field : fields) {
                System.out.print("访问修饰符:" + Modifier.toString(field.getModifiers()));
                System.out.print("   类型:"+field.getType().getName());
                System.out.print("   成员名:"+field.getName());
                System.out.println();
            }
            
            System.out.println();
            
            //取得成员方法息
            Method[] methods=cl.getDeclaredMethods();
            for(Method method:methods){
                System.out.print("访问修饰符:" + Modifier.toString(method.getModifiers()));
                System.out.print("   返回值类型:"+method.getReturnType().getName());
                System.out.print("   方法名:"+method.getName());
                System.out.println();
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}

运行结果

包名:com.czy.demo
类访问修饰符:public final

访问修饰符:public   构造函数名:com.czy.demo.Student
访问修饰符:public   构造函数名:com.czy.demo.Student

访问修饰符:private   类型:java.lang.String   成员名:name
访问修饰符:public   类型:int   成员名:age
访问修饰符:protected   类型:com.czy.demo.Student$Gender   成员名:gender

访问修饰符:public   返回值类型:java.lang.String   方法名:getName
访问修饰符:private   返回值类型:int   方法名:getAge
访问修饰符:protected   返回值类型:com.czy.demo.Student$Gender   方法名:getGender

四、利用Class建立对象

如果已有确切的类,那么就可以使用new关键字建立实例。如果不知道类名称,那么可以利用Class.forName() 动态加载.class文档,取得Class对象之后,利用其newInstance()方法建立实例

        Class cl=Class.forName("ClassName");
        Object object=cl.newInstance();

这种事先不知道类名称,又需要建立类实例的需求,一般情况下都是由于开发者需要得到某个类对象并对其行为进行操纵,可是该类又是由他人开发且还未完工,因此就需要来动态加载.class文档

例如,你需要来控制学生、老师或者家长的唱歌行为,可是学生、老师和家长这些类又是由其他人来设计的,你只是对开始与暂停操作进行控制 那么,你可以规定学生类必须实现Sing接口

public interface Sing {
    
    void start();
    
}

那么,就可以来进行自己的开发了,将动态加载的对象强转为Sing

public class Main {

    public static void main(String[] args) {
        try {
            Sing palyer = (Sing) Class.forName("className").newInstance();
            palyer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    }

}

然后规定他人设计的学生类必须实现Sing接口

package com.czy.demo;

public class Student implements Sing {

    @Override
    public void start() {
        System.out.println("学生唱歌");
    }

}

这样,等到得到确切的类名称后,修改main方法的className即可

public static void main(String[] args) {
        try {
            Sing palyer = (Sing) Class.forName("com.czy.demo.Student").newInstance();
            palyer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    }

五、操作成员方法

java.lang.reflect.Method 实例是方法的代表对象,可以使用 invoke() 方法来动态调用指定的方法

例如,修改Student类,将get方法都指定为公有的,将set方法指定为私有的

package com.czy.demo;

public class Student {

    private String name;

    private int age;

    public Student() {

    }

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

    public String getName() {
        System.out.println("调用了getName方法,Name:" + name);
        return name;
    }

    public int getAge() {
        System.out.println("调用了getAge方法,Age:" + age);
        return age;
    }

    private void setName(String name) {
        this.name = name;
        System.out.println("调用了setName方法,name:" + name);
    }

    private void setAge(int age) {
        this.age = age;
        System.out.println("调用了setAge方法,age:" + age);
    }

}

一般情况下,类的私有方法只有在其内部才可以被调用,通过反射我们可以来突破这一限制

首先来调用公有方法

public class Main {

    public static void main(String[] args) throws Exception {
        Class cl = Class.forName("com.czy.demo.Student");
        // 指定构造函数
        Constructor constructor = cl.getConstructor(String.class, Integer.TYPE);
        // 根据指定的构造函数来获取对象
        Object object = constructor.newInstance("叶", 22);

        // 指定方法名称来获取对应的公开的Method实例
        Method getName = cl.getMethod("getName");
        // 调用对象object的方法
        getName.invoke(object);

        // 指定方法名称来获取对应的公开的Method实例
        Method getAge = cl.getMethod("getAge");
        // 调用对象object的方法
        getAge.invoke(object);

    }

}

输出结果如下所示,可以知道Student对象的两个get方法成功被调用了

调用了getName方法,Name:叶
调用了getAge方法,Age:22

受保护或私有方法的调用步骤略有不同

public class Main {

    public static void main(String[] args) throws Exception {
        Class cl = Class.forName("com.czy.demo.Student");
        // 指定构造函数
        Constructor constructor = cl.getConstructor(String.class, Integer.TYPE);
        // 根据指定的构造函数来获取对象
        Object object = constructor.newInstance("叶", 22);

        // 指定方法名称来获取对应的私有的Method实例
        Method setName = cl.getDeclaredMethod("setName", String.class);
        setName.setAccessible(true);
        setName.invoke(object, "新的名字");
        
        // 指定方法名称来获取对应的私有的Method实例
        Method setAge = cl.getDeclaredMethod("setAge", Integer.TYPE);
        setAge.setAccessible(true);
        setAge.invoke(object, 23);
    }

}

输出结果如下所示,可以看到私有方法一样在外部被调用了

调用了setName方法,name:新的名字
调用了setAge方法,age:23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IT可乐

Java的深拷贝和浅拷贝

  关于Java的深拷贝和浅拷贝,简单来说就是创建一个和已知对象一模一样的对象。可能日常编码过程中用的不多,但是这是一个面试经常会问的问题,而且了解深拷贝和浅拷...

3666
来自专栏专注 Java 基础分享

关于类的对象创建与初始化

今天,我们就来解决一个问题,一个类实例究竟要经过多少个步骤才能被创建出来,也就是下面这行代码的背后,JVM 做了哪些事情? Object obj = new ...

3405
来自专栏从流域到海域

Python iterator迭代器

迭代器iterator是面向对象的程序设计语言都提供的遍历序列对象的一种方法,在Python中封装程度更高,其把迭代协议在语言的层面就已经实现了,所以使用...

2449
来自专栏企鹅号快讯

每周四更面试题:True+True=?

面试题:True + Ture == ? Python 的 “+” 号会根据操作对象数据类型的不同而进行重载,操作对象为数字类型时,它是算术运算符;操作对象为序...

1997
来自专栏平凡文摘

深入理解Java类型信息(Class对象)与反射机制

1473
来自专栏xingoo, 一个梦想做发明家的程序员

复制控制---复制构造函数

复制构造函数 只有单个形参,而且该参数是对本类类型对象的引用。 主要用于: 1 根据另一个同类型的对象显示或隐式的初始化一个对象 string a = "abc...

1985
来自专栏HappenLee的技术杂谈

C++雾中风景3:const用法的小结

const关键字,翻译成中文是常量,常数的意思。所以在绝大多数场合之中,const是来定义常量的,定义常量也是好的编程习惯。在C类语言之中,定义常量通常会使用宏...

1163
来自专栏我和我大前端的故事

深入了解原型

说原型之前先说说对象,好像在工作中,对象用的挺多的,原型基本上没有用。既然没有用那我还要不要学习呢?思考了很久,还是学一学,万一以后的工作用的着呢?领导常说,上...

923
来自专栏编程坑太多

js数组的拷贝赋值复制-你真的懂?

3623
来自专栏互联网杂技

深入理解javascript原型和闭包(1)——一切都是对象

“一切都是对象”这句话的重点在于如何去理解“对象”这个概念。 ——当然,也不是所有的都是对象,值类型就不是对象。 首先咱们还是先看看javascript中一个常...

37216

扫码关注云+社区

领取腾讯云代金券