美文网首页
Java Class对象简单的解释

Java Class对象简单的解释

作者: Heezier | 来源:发表于2020-11-13 01:04 被阅读0次

    一、一个Java文件到运行经过的步骤,Class对象的来源

    Java程序从源文件创建到程序运行要经过两大步骤:

    1、javac编译器将源文件编译为class字节码;

    2、java命令启动jvm运行解释class字节码。

    我们来走一下这两个过程:

    编写一个java文件,分别定义了一个内部接口和内部类

    public class TestJava {
    
        static int i = 8;
        static {
            i++;
            System.out.println("init i" + i);
        }
    
        public static void main(String[] args) {
    
        }
        interface ITest{
    
        }
    
        class InnerTest{
    
        }
    }
    

    第一步:编译一个java文件

    javac -encoding utf-8  TestJava.java
    

    查看一下编译结果:

    image-20201112233941760

    可以看到不管是内部接口还是内部类,都会生成class文件。

    第二步:运行

    image-20201112234212191

    这个过程其实还包括:

    image-20201112235509263
    1.从源码到字节码

    jvm运行的是class字节码文件,只要是这种格式的文件就行,所以,实际上jvm并不像我之前想象地那样与java语言紧紧地捆绑在一起。其他语言编写的源码编译成字节码文件,交给jvm去运行,只要是合法的字节码文件,jvm都会正确地跑起来。

    2.Java虚拟机的基本结构及其内存分区

    Java虚拟机要运行字节码指令,就要先加载字节码文件,谁来加载,怎么加载,加载到哪里……谁来运行,怎么运行

    image-20201113000336002

    上图为JVM内存模型:

    方法区:又被称为永久代,用来存储类的信息,例如:方法,方法名,返回值,常量。当它无法满足内存分配需求时,方法区会抛出OutOfMemoryError。

    字符串常量、静态变量数据存放区域

    java6中 所有常量池数据是存放在永久代中,但到java7后 Hostpot 把永久代中的字符串常量、静态变量数据迁移到了堆中,后面的java 8并没有对这部分内容进行迁移,在java8 中字符串常量、静态变量数据还是放到堆中,所以常量池只是在JVM规范定义上属于方法区,但Hotspot在实现的时候部分常量池的内容实际上是保存在堆中了。

    :存放new出来的对象信息, 全局变量。

    虚拟机栈

    栈这部分区域主要是用于线程运行方法的区域,此区域属于线程私有的空间,每一个线程创建后都会申请一个自己单独的栈空间,每一个方法的调用都会对应着一个栈帧。

    本地方法栈

    由于java需要与一些底层系统如操作系统或某些硬件交换信息时的情况,这个时候就需要通过调用native本地方法来实现,本地方法栈和虚拟机栈功差不多,区别在于本地方法栈是虚拟机调用native方法时使用的。

    程序计数器

    程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器,程序计数器记录着某个线程当前执行指令的位置,此区域属于线程隔离区。

    JVM内存有VM的功能模块来进项管理和分配. JVM的功能模块主要包括类加载器、执行引擎和垃圾回收系统

    3.类加载器加载TestJava.class到内存:

    ​ 1)类加载器会在指定的classpath中找到TestJava.class这个文件,然后读取字节流中的数据,将其存储在方法区中。

    ​ 2)会根据TestJava.class的信息建立一个Class对象,这个对象比较特殊,一般也存放在方法区中,用于作为运行时访问TestJava类的各种数据的接口。

    ​ 3)必要的验证工作,格式、语义等。验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求。

    ​ 4)为TestJava中的静态字段分配内存空间,也是在方法区中,并进行零初始化,即数字类型初始化为0,boolean初始化为false,引用类型初始化为null等。

    TestJava 中的静态变量 i

    static int i = 8;
    

    此时,并不会执行赋值为8的操作,而是将其初始化为0;

    此时也可称为内存准备完毕。

    5)初始化操作:由于编译阶段,编译器收集所有的静态字段的赋值语句及静态代码块,并按语句出现的顺序拼接出一个类初始化方法<clinit>()。此时,执行引擎会调用这个方法对静态字段进行代码中编写的初始化操作。

    6)执行引擎找到main()这个入口方法,执行其中的字节码指令

    那么在上述的流程中,class字节码运行大概可分为三个流程:

    1. 加载,这是由类加载器(ClassLoader)执行的。通过一个类的全限定名来获取其定义的二进制字节流(Class字节码),将这个字节流所代表的静态存储结构转化为方法去的运行时数据接口,根据字节码在java堆中生成一个代表这个类的java.lang.Class对象。
    2. 链接。在链接阶段将验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求,为静态域分配存储空间并设置类变量的初始值(默认的零值),并且如果必需的话,将常量池中的符号引用转化为直接引用。
    3. 初始化。到了此阶段,才真正开始执行类中定义的java程序代码。用于执行该类的静态初始器和静态初始块,如果该类有父类的话,则优先对其父类进行初始化。

    所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。

    在类加载阶段,类加载器首先检查这个类的Class对象是否已经被加载。如果尚未加载,默认的类加载器就会根据类的全限定名查找.class文件。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良java代码。一旦某个类的Class对象被载入内存,我们就可以它来创建这个类的所有对象。

    所以每一个java类在被调用实例化的过程中,会优先生产该类的Class对象。这就是Class对象的来源。

    二、Class对象和实例对象

    java有两种对象:实例对象和Class对象。每个类的运行时的类型信息就是用Class对象表示的。它包含了与类有关的信息。其实我们的实例对象就通过Class对象来创建的。Java使用Class对象执行其RTTI(运行时类型识别,Run-Time Type Identification),多态是基于RTTI实现的。

    每一个类都有一个Class对象,每当编译一个新类就产生一个Class对象,基本类型 (boolean, byte, char, short, int, long, float, and double)有Class对象,数组有Class对象,就连关键字void也有Class对象(void.class)。Class对象对应着java.lang.Class类,如果说类是对象抽象和集合的话,那么Class类就是对类的抽象和集合。

    三、Class对象获取

    有三种获得Class对象的方式:

    1. Class.forName(“类的全限定名”)
    2. 实例对象.getClass()
    3. 类名.class (类字面常量)
    public class ClassUnderstand {
        public final static int finalcount = 0;
        public static int count = 0;
        static{
            count ++;
            System.out.println("ClassUnderstand class加载 count变化:" + count);
        }
        public ClassUnderstand(){
            System.out.println("构造方法");
        }
    }
    
    
    
    public class ClassUnderstand2 {
        static int count = 0;
        static{
            count ++;
            System.out.println("ClassUnderstand2 class加载 次数:" + count);
        }
        public ClassUnderstand2(){
            System.out.println("无参数构造方法");
        }
        public ClassUnderstand2(String str, int i){
            System.out.println("构造方法 参数:" + str + "  int:" + i);
        }
        public ClassUnderstand2(String str){
            System.out.println("构造方法 参数:" + str);
        }
    }
    
    
    package com.java.basis.classunderstand;
    
    import com.java.basis.classunderstand.ClassUnderstand2;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    public class Test {
    
        public static void main(String[] args) {
            Class aClass = null;
            Class aClassFromname = null;
            Class aClassObject1 = null;
            Class aClassObject2 = null;
            Class aClassFromname2 = null;
    
            aClass = ClassUnderstand.class;
            System.out.println("aClass = " + aClass);
            System.out.println("aClass = " + ClassUnderstand.finalcount);
            System.out.println("aClass = " + ClassUnderstand.count);
            System.out.println("========================");
            try {
    
                aClassFromname = Class.forName("com.java.basis.classunderstand.ClassUnderstand");
                System.out.println("aClassFromname = " + aClassFromname);
                System.out.println("========================");
    
                aClassObject1 = new ClassUnderstand().getClass();
                System.out.println("aClassObject1 = " + aClassObject1.getClass());
                System.out.println("========================");
    
                aClassObject2 = new ClassUnderstand().getClass();
                System.out.println("aClassObject2 = " + aClassObject2.getClass());
                System.out.println("========================");
    
                aClassFromname2 = Class.forName("com.java.basis.classunderstand.ClassUnderstand");
                System.out.println("aClassFromname2 = " + aClassFromname2);
                System.out.println("========================");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
    
    
            if(aClass == aClassFromname &&  aClassFromname == aClassObject1 && aClassObject2 == aClassObject1){
                System.out.println("所有class对象相等");
            }
    
    
            try {
                Constructor constructor =  ClassUnderstand2.class.getConstructor( String.class, int.class);
                constructor.newInstance("你好", 100);
    
                Constructor constructor2 =  ClassUnderstand2.class.getConstructor(String.class);
                constructor2.newInstance("第二个构造函数");
    
                ClassUnderstand2 classUnderstand2 =  ClassUnderstand2.class.newInstance();
    
                ClassUnderstand2 classUnderstand21 = new ClassUnderstand2("news实例化");
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
    
        }
    }
    
    

    运行结果:

    aClass = class com.java.basis.classunderstand.ClassUnderstand
    aClass = 0
    ClassUnderstand class加载 count变化:1
    aClass = 1
    ========================
    aClassFromname = class com.java.basis.classunderstand.ClassUnderstand
    ========================
    构造方法
    aClassObject1 = class java.lang.Class
    ========================
    构造方法
    aClassObject2 = class java.lang.Class
    ========================
    aClassFromname2 = class com.java.basis.classunderstand.ClassUnderstand
    ========================
    所有class对象相等
    ClassUnderstand2 class加载 次数:1
    构造方法 参数:你好  int:100
    构造方法 参数:第二个构造函数
    无参数构造方法
    构造方法 参数:news实例化
    
    Process finished with exit code 0
    
    
    

    1.Class.forName方法是Class类的一个静态成员。forName在执行的过程中发现如果类还没有被加载,那么JVM就会调用类加载器去加载类,并返回加载后的Class对象。Class对象和其他对象一样,我们可以获取并操作它的引用。在类加载的过程中,类的静态语句块会被执行。如果Class .forName找不到你要加载的类,它会抛出ClassNotFoundException异常。

    2.Class.forName的好处就在于,不需要为了获得Class引用而持有该类型的对象,只要通过全限定名就可以返回该类型的一个Class引用。如果你已经有了该类型的对象,那么我们就可以通过调用getClass()方法来获取Class引用了,这个方法属于根类Object的一部分,它返回的是表示该对象的实际类型的Class引用

    3.利用new操作符创建对象后,类已经装载到内存中了,所以执行getClass()方法的时候,就不会再去执行类加载的操作了,而是直接从java堆中返回该类型的Class引用。

    4.用.class来创建对Class对象的引用时,不会自动地初始化该Class对象(这点和Class.forName方法不同)。类对象的初始化阶段被延迟到了对静态方法或者非常数静态域首次引用时才执行。但是如果使用Class.forName来产生引用,就会立即进行了初始化。

    5.如果一个字段被static final修饰,我们称为”编译时常量“,那么在调用这个字段的时候是不会对类进行初始化的。因为被static和final修饰的字段,在编译期就把结果放入了常量池中了。但是,如果只是将一个域设置为static 或final的,还不足以确保这种行为,就如调用ClassUnderstand.count字段后,会强制ClassUnderstand进行类的初始化,因为ClassUnderstand.count字段不是一个编译时常量。

    三、基本数据类型的Class对象

    注意:基本数据类型的Class对象和包装类的Class对象是不一样的:

            Class c1 = Integer.class;
            Class c2 = int.class;
            System.out.println(c1);
            System.out.println(c2);
            System.out.println(c1 == c2);
    /* Output
    class java.lang.Integer
    int
    false
     */
    

    但是在包装类中有个一个字段TYPE,TYPE字段是一个引用,指向对应的基本数据类型的Class对象,如下所示,左右两边相互等价:


    这里写图片描述

    四、结论

    1.一旦类被加载了到了内存中,那么不论通过哪种方式获得该类的Class对象,它们返回的都是指向同一个java堆地址上的Class引用。jvm不会创建两个相同类型的Class对象;

    2.用.class来创建对Class对象的引用时,不会自动地初始化该Class对象,但是如果使用Class.forName来产生引用,就会立即进行了初始化;

    3.引用static final修饰的变量并不会初始化该对象;

    其他:

    其实对于任意一个Class对象,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个Class对象来源于同一个Class文件,只要加载它们的类加载器不同,那这两个Class对象就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。所以在java虚拟机中使用双亲委派模型来组织类加载器之间的关系,来保证Class对象的唯一性。

    五、泛型Class引用

    Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一个对象。在JavaSE5中,允许你对Class引用所指向的Class对象的类型进行限定,也就是说你可以对Class对象使用泛型语法。通过泛型语法,可以让编译器强制指向额外的类型检查:

    public final class Class<T> implements java.io.Serializable,
                                  GenericDeclaration,
                                  Type,
                                  AnnotatedElement {
    
    Class<Integer> c1 = int.class;
       c1=Integer.class;
       //c1=Double.class; 编译报错
    

    虽然int.class和Integer.class指向的不是同一个Class对象引用,但是它们基本类型和包装类的关系,int可以自动包装为Integer,所以编译器可以编译通过。

    泛型中的类型可以持有其子类的引用吗?不行

    Class<Number> c1 = Integer.class;  //编译报错
    

    虽然Integer继承自Number,但是编译器无法编译通过。

    为了使用泛化的Class引用放松限制,我们还可以使用通配符,它是Java泛型的一部分。通配符的符合是”?“,表示“任何事物“:

    Class<?> c1 = int.class;
      c1= double.class;
    
      Class<? extends Number> c1 = Integer.class;
            c1 = Number.class;
            c1 = Double.class;
            // c1=String.class; 报错,不属于Number类和其子类
    

    六、Class类的方法

    方法名 说明
    forName() (1)获取Class对象的一个引用,但引用的类还没有加载(该类的第一个对象没有生成)就加载了这个类。 (2)为了产生Class引用,forName()立即就进行了初始化。
    Object-getClass() 获取Class对象的一个引用,返回表示该对象的实际类型的Class引用。
    getName() 取全限定的类名(包括包名),即类的完整名字。
    getSimpleName() 获取类名(不包括包名)
    getCanonicalName() 获取全限定的类名(包括包名)
    isInterface() 判断Class对象是否是表示一个接口
    getInterfaces() 返回Class对象数组,表示Class对象所引用的类所实现的所有接口。
    getSupercalss() 返回Class对象,表示Class对象所引用的类所继承的直接基类。应用该方法可在运行时发现一个对象完整的继承结构。
    newInstance() 返回一个Oject对象,是实现“虚拟构造器”的一种途径。使用该方法创建的类,必须带有无参的构造器
    getFields() 获得某个类的所有的公共(public)的字段,包括继承自父类的所有公共字段。 类似的还有getMethods和getConstructors。
    getDeclaredFields 获得某个类的自己声明的字段,即包括public、private和proteced,默认但是不包括父类声明的任何字段。类似的还有getDeclaredMethods和getDeclaredConstructors。

    getName、getCanonicalName与getSimpleName的区别:

    getSimpleName:只获取类名
    getName:类的全限定名,jvm中Class的表示,可以用于动态加载Class对象,例如Class.forName。
    getCanonicalName:返回更容易理解的表示,主要用于输出(toString)或log打印,大多数情况下和getName一样,但是在内部类、数组等类型的表示形式就不同了。

    
    public class TestGetName {
        private  class inner{
        }
        public static void main(String[] args) throws ClassNotFoundException {
            //普通类
            System.out.println(Test.class.getSimpleName()); //Test
            System.out.println(Test.class.getName()); //com.cry.Test
            System.out.println(Test.class.getCanonicalName()); //com.cry.Test
            //内部类
            System.out.println(inner.class.getSimpleName()); //inner
            System.out.println(inner.class.getName()); //com.cry.Test$inner
            System.out.println(inner.class.getCanonicalName()); //com.cry.Test.inner
            //数组
            System.out.println(args.getClass().getSimpleName()); //String[]
            System.out.println(args.getClass().getName()); //[Ljava.lang.String;
            System.out.println(args.getClass().getCanonicalName()); //java.lang.String[]
            //我们不能用getCanonicalName去加载类对象,必须用getName
            //Class.forName(inner.class.getCanonicalName()); 报错
            Class.forName(inner.class.getName());
        }
    }
    
    Test
    com.java.basis.classunderstand.Test
    com.java.basis.classunderstand.Test
    inner
    com.java.basis.classunderstand.TestGetName$inner
    com.java.basis.classunderstand.TestGetName.inner
    String[]
    [Ljava.lang.String;
    java.lang.String[]
    
    Process finished with exit code 0
    

    参考文档:https://blog.csdn.net/dufufd/article/details/80537638

    https://www.cnblogs.com/dqrcsc/p/4671879.html?utm_source=tuicool&utm_medium=referral

    相关文章

      网友评论

          本文标题:Java Class对象简单的解释

          本文链接:https://www.haomeiwen.com/subject/jpgmbktx.html