JAVA反射的基础--Class对象、类加载器

Class对象

反射如何形成,首先我们需要看一下所有类的顶尖继承类—Object类,其内部有一个public final Class getClass()方法,这个方法在最顶尖的类中,因此所有的类都会包含这个方法,它的返回值是一个Class对象,我们可以通过对象反射求出类的名称。

反射就像是照镜子,对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个 Class 对象包含特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息。
Class类到底有什么作用呢?

  1. Class 本身也是一个类
  2. Class 对象只能由系统建立对象
  3. 一个加载的类在 JVM 中只会有一个Class实例
  4. 一个Class对象对应的是一个加载到JVM中的一个.class文件
  5. 每个类的实例都会记得自己是由哪个 Class 实例所生成
  6. 通过Class可以完整地得到一个类中的所有被加载的结构
  7. Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象
方法名功能说明
static ClassforName(String name)返回指定类名name的Class对象
Object newInstance()调用缺省构造函数,返回Class对象的一个实例
getName()返回此Class对象所表示的实体(类,接口,数组类或void)的名称。
Class getSuperClass()返回当前Class对象的父类的Class对象
Class[] getinterfaces()获取当前Class对象的接口
ClassLoader getClassLoader()返回该类的类加载器
Constructor[] getConstructors()返回一个包含某些Constructor对象的数组
Method getMothed(String name,Class… T)返回一个Method对象,此对象的形参类型为paramType
Field[] getDeclaredFields()返回Field对象的一个数组

获取Class类的实例

  1. 若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高。
    假设现有一个Person实体类。(类的内部有name,id,age属性、无参有参构造,set,get方法,重写了toString方法)
Class clazz = Person.class;
  1. 已知某个类的实例,调用该实例的getClass()方法获取Class对象。
Person person=new Person();
Class clazz = person.getClass();
  1. 已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException异常
//demo01包下有Person实体类
Class clazz = Class.forName("demo01.Person");
  1. 内置基本数据类型可以直接用类名.Type
  2. 利用ClassLoader
    哪些数据类型可以有Class对象呢?
标识符解释
class外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
interface接口
[]数组
enum枚举
annotation注解@interface
primitive type基本数据类型
void
public static void main(String[] args) {
        Class c1 = Object.class; //类
        Class c2 = Comparable.class;  //接口
        Class c3 = String[].class;  //数组
        Class c4 = int[][].class; //二维数组
        Class c5 = ElementType.class; //枚举
        Class c6 = Override.class; //注解
        Class c7 = Integer.class; //基本数据类型
        Class c8 = Void.class;// void
        Class c9 = Class.class;//Class
		Class c20 = String[][].class; //二维数组
        int[] a = new int[10];
        int[] b = new int[100];


        Class c10 =a.getClass();
        Class c11 =b.getClass();

        //数组类型一样的情况下 , 同个维度 , 只有一个class对象
        System.out.println(c10==c11);
        System.out.println(c20==c4);
       
        System.out.println("类:"+c1);
        System.out.println("接口"+c2);
        System.out.println("数组"+c3);
        System.out.println("二维数组"+c4);
        System.out.println("枚举"+c5);
        System.out.println("注解"+c6);
        System.out.println("基本数据类型"+c7);
        System.out.println("void"+c8);
        System.out.println("Class"+c9);
        System.out.println("c10"+c10);
        System.out.println("c11"+c11);
    }

给出结果作为对比学习吧

true //解释:c10和c11都是int类型的数组,因此他们的class是相同的,可以看C10和c11的class输出来确定他们是相同的class对象
false //解释:虽说c20和c4都是二维数组,但一个是String类型,一个是int类型,因为class是不相同的:class java.lang.Object
接口:interface java.lang.Comparable
数组:class [Ljava.lang.String;
二维数组:class [[I
枚举:class java.lang.annotation.ElementType
注解:interface java.lang.Override
基本数据类型:class java.lang.Integer
void:class java.lang.Void
Class:class java.lang.Class
c10:class [I
c11:class [I

类的加载

要了解类的加载,需要先了解Java内存分析

Java内存分析图

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化

步骤解释
1.类的加载(Load)将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成
2.类的链接(Link)将类的二进制数据合并到JRE中
3.类的初始化(Initialize)JVM负责对类进行初始化

类的加载过程与ClassLoader的理解

加载: 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象。
链接: 将Java类的二进制代码合并到JVM的运行状态之中的过程。

  1. 验证:确保加载的类信息符合JVM规范,没有安全方面的问题
  2. 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
  3. 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

初始化:
4. 执行类构造器 < clinit >()方法的过程。类构造器 < clinit >()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
5. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
6. 虚拟机会保证一个类的 < clinit >()方法在多线程环境中被正确加锁和同步。

类初始化

类的主动引用

(一定会发生类的初始化)

  1. 当虚拟机启动,先初始化main方法所在的类
  2. new一个类的对象
  3. 调用类的静态成员(除了final常量)和静态方法
  4. 使用java.lang.reflect包的方法对类进行反射调用
  5. 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
类的被动引用

(不会发生类的初始化)

  1. 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化
  2. 通过数组定义类引用,不会触发此类的初始化
  3. 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
//主函数:
public static void main(String[] args) {
        //1.主动引用
        Son son =  new Son();
        System.out.println(son.m);

        //2.反射也会产生主动引用

        try {
            Class.forName("com.Father");//这是我的Father类所在的地址,如果复制代码需要改这一块的url
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        //3.被动加载 , 通过子类引用父类的静态变量 , 不会导致子类初始化
        System.out.println(Son.b);

        //4.被动加载
        Son[] array = new Son[5];

        //5.被动加载
        System.out.println(Son.M);

    }
    

class Father{
    /*
    <clinit>(){
        int b = 2;
        System.out.println("父类被加载");
    }
     */
    static int b = 2;
    static {
        System.out.println("父类被加载");
    }
}

class Son extends Father{
    static {
        System.out.println("子类被加载");
        m = 300;
    }
    static int m = 100;

    static final int M = 1;

}

类加载器的运行步骤

  • 类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时
    数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问
    入口。
  • 类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维
    持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象

** Java程序运行步骤如下:**
1. 源程序(*.java文件)
2. Java编译器
3. 字节码(*.class文件)
4. 类装载器
5. 字节码校验器
6. 解释器
7. 操作系统平台

类加载器作用是用来把类(class)装载进内存的。JVM 规范定义了如下类型的类的加载器。
加载器
提到了类加载器,就会说到双亲委派机制

public static void main(String[] args) throws ClassNotFoundException {

        //1.获得系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);
        //sun.misc.Launcher$AppClassLoader@18b4aac2    系统类加载器

        //2.获得系统类加载器的父类加载器--->扩展类加载器
        ClassLoader parent = systemClassLoader.getParent();
        System.out.println(parent);
        //sun.misc.Launcher$ExtClassLoader@4554617c  扩展类加载器
        
        //3.扩展类加载器的父类--->根加载器(C++写的)
        ClassLoader parent1 = parent.getParent();
        System.out.println(parent1); 
        //null  因为是C++写的根加载器,无法直接获取,因此返回null值

        //4.测试当前类是哪个加载器加载的
        ClassLoader classLoader = Class.forName("com.Test03").getClassLoader();
        System.out.println(classLoader);
        //sun.misc.Launcher$AppClassLoader@18b4aac2  字节写的类属于系统类加载器

        //5.测试Object类是哪个加载器加载的
        classLoader = Class.forName("java.lang.Object").getClassLoader();
        System.out.println(classLoader);
        //null   Object类属于引导加载器中的核心代码库,因此返回null值


        //系统类加载器(ClassLoader)可以加载的类的路径
        //都保存在系统变量  :  System.getProperty("java.class.path") ;
        System.out.println(System.getProperty("java.class.path"));
        //这个可以看到所有使用到的加载器,包括很多系统类加载器,扩展类加载器,还有你所使用的IDE所用到的加载器,也会给出当前代码所在的路径
    }

看到这里就会有一个大胆的想法,对于Object类它属于核心类加载器,因此返回null值,那如果我自己写一个Object类,会不会也就变更了JVM呢?
这个我确实试了,结果是不行,查了资料发现,这是因为双亲委派机制的原因导致的,因此我们自己写的和JVM中的类加载器命名相同的类,会被JVM忽略掉。
双亲委派机制:

  • 如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

class的各种方法

class对象得到之后,如何得到类的属性、方法、构造器可以查看以下代码:
很重要,很重要!!!一定要看,要不然下一个博客讲反射中的一些代码会不知道为什么用了*()方法而不用**方法
链接为:JAVA的反射及应用.

public static void main(String[] args) throws
            ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {

        Class c1 = Class.forName("java.lang.String");

        //获得名字
        System.out.println("*****************************************************");
        System.out.println(c1.getName()); //获得类的名字 : 包名 + 类名
        System.out.println(c1.getSimpleName()); // 类名


        //获得属性
        System.out.println("*****************************************************");
        Field[] fields = c1.getFields();  //只能获得类的public属性
        for (Field field : fields) {
            System.out.println(field);
        }

        Field[] declaredFields = c1.getDeclaredFields();  //获得类的全部属性

        for (Field field : declaredFields) {
            System.out.println(field);
        }

/*
        Field name = c1.getDeclaredField("name");  //获得指定属性的值
        System.out.println(name);
*/


        //获得方法
        System.out.println("*****************************************************");
        Method[] methods = c1.getMethods();  //获得本类及其父类的所有public方法

        for (Method method : methods) {
            System.out.println("默认:"+method);
        }


        methods = c1.getDeclaredMethods();  //获得本类的全部方法
        for (Method method : methods) {
            System.out.println("Declared:"+method);
        }

        //重载 , 如果只知道方法的名字 , 是找不到具体的方法的
/*        Method getName = c1.getDeclaredMethod("getName",null);
        Method setName = c1.getDeclaredMethod("setName",String.class);
        System.out.println(getName);
        System.out.println(setName);*/

        //获得构造器
        System.out.println("*****************************************************");
        Constructor[] constructors = c1.getConstructors();
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
        }

/*        //获得指定的构造器
        Constructor constructor = c1.getConstructor(null);  //无参构造
        System.out.println("指定:"+constructor);
        constructor = c1.getConstructor(String.class,int.class,int.class);  //获得有参构造
        System.out.println("指定:"+constructor);*/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值