首页 > 其他分享 >深入理解Class对象

深入理解Class对象

时间:2023-10-06 13:58:15浏览次数:32  
标签:初始化 对象 class 理解 深入 static Class 加载

深入理解Class对象

Class类的概念

想要理解RTTI在Java中的工作原理,首先得知道类型信息在运行时是如何表示的。Java用Class类来表示运行时的类型信息,首先必须明确,Class类跟Java API中定义的String、Integer等类以及我们自己定义的类是一样的,是一个实实在在的类,只不过名字特殊点,在JDK的java.lang包中。

那么Class类到底有什么作用呢?是什么的抽象,其实例又表示什么呢?

对于我们自己定义的类,我们用类来抽象现实中的某些事物,比如我们定义名为Dog的类来抽象现实中的狗,然后可以实例化这个类,用这些实例来表示一条黑狗、一条黄狗、我家的狗、你家的狗等等。我们还用Cat类来抽象现实中的猫,用Brid类来抽象现实中的鸟。那么,Dog、Cat、Brid这三个类之间有没有共同特征了,可不可以对这三个类进行抽象呢?

当然可以,我们都知道所有的class都是Object的子类,都有类名,有hashcode,可以判断类型属于class、interface、enum还是annotation。另外可以定义一些方法,比如获取某个方法、获取类型名等等。这样就封装了一个表示类型的类 — Class,用来提取这些类的一些共同特征,表示对这些类(或接口)的抽象。而Dog、Cat、Brid这三个类就分别是Class类的对象。也就是说,每个类都有一个Class对象,即每当我们编写并且编译一个新类,就会产生一个对应的Class对象,被保存在一个同名.class文件(编译后的字节码文件)里。

下面我们来分析一下Class类的源码:

//前一个Class表示这是一个类的声明,第二个Class是类的名称,
  <T>表示这是一个泛型类,并实现了四种接口。
public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type, AnnotatedElement {
  
    //定义了三个静态变量
    private static final int ANNOTATION= 0x00002000;
    private static final int ENUM      = 0x00004000;
    private static final int SYNTHETIC = 0x00001000;

    //定义了一个名为registerNatives()的本地方法,并在静态块中调用:
    private static native void registerNatives();
    static {
        registerNatives();
    }

    // 私有构造函数,只能由JVM调用,创建该类实例
    private Class(ClassLoader loader) {
        classLoader = loader;
    }
    
    /*如果Class对象是一个Java类,返回class full_classname,即class 包名.类名;
      比如上面例子的List,返回的就是class java.util.List;
      如果是接口,将class改成interface;
      如果是void类型,则返回void;
      如果是基本类型,返回基本类型。*/
	public String toString() {
        return (isInterface() ?"interface " : (isPrimitive() ? "" : "class")) + getName();
    }

注意:Class类的构造器是private的,这意味着我们无法用new关键字得到一个Class对象。为了生成一个类的Class对象,必须通过运行Java虚拟机(JVM)中的类加载器子系统。

从上我们可以总结出:

  • Class类的作用是运行时提供或获得某个对象的类型信息;
  • Class类也是类的一种,只是名字和class关键字高度相似;
  • Class类的对象表示你创建的类的类型信息,比如你创建一个Dog类,那么,Java编译后就会创建一个包含Dog类型信息的Class对象;
  • Class类只有私有构造函数,因此对应的Class对象不能像普通类一样,以 new 操作符的方式创建,只能通过JVM加载。
  • 一个class类有且只有一个相对应的Class对象(无论创建多少个实例对象,在JVM中都只有一个Class对象),如下图所示:
    在这里插入图片描述

Class对象的加载

那么JVM是如何加载这个类的?

当程序创建第一个对类的静态成员的引用时,JVM中的类加载器子系统会将类对应的Class对象加载到JVM中。这个证明构造器也是类的静态方法,尽管构造器前并没有用static关键字修饰。因此,当我们使用new操作符创建一个类的实例对象时,也会被当作对类的静态成员的引用。

可以看出,Java一门动态加载的语言,Java中的类在需要时才会被加载。也就是说,我们编写出的Java程序,在它们开始运行之前并非被完全加载到内存的,其各个部分是在需要时才加载。因此,在我们需要用到某个类时,类加载器首先检查这个类的Class对象是否已被加载,如果没有加载,默认的类加载器就会先根据类名查找.class文件。在这个类的字节码文件被加载时,它们要接受验证,以确保其没有被破坏、并且不包含不良Java代码(这是java众多安全检测机制中的一个),检测通过后Class对象就被载入内存了,可以被用来创建这个类的所有实例对象。下图表示了一个类加载的过程:
在这里插入图片描述

  • 第一阶段(加载):类加载器根据类名找到此类的.Class文件,并把这个文件包含的字节码加载到内存中,生成Class对象。

  • 第二阶段(链接):又分为三个步骤,分别是:

    (1) 验证阶段:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

    (2) 准备阶段:正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。

    (3)解析阶段:虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 第三阶段(初始化):类中静态属性和初始化赋值,以及静态块的执行。

Class对象的获取方式

Java主要提供了三种方式来获取一个实例对象对应的Class对象:

  1. Class.forName():

这个方法是Class类的一个static成员方法。Class对象和其他对象一样,我们可以获取并操作它的引用,forName()就是取得Class对象的引用的一种方法,该方法允许我们无需通过持有该类的实例对象引用而去获取Class对象。

	try {
	  		//"com.yang"是包名
	 		Class c1 = Class.forName("com.yang.Dog");
		} 	catch (ClassNotFoundException e) {
		         e.printStackTrace();
 	   }

注意:如果Class.forName()没有找到你要加载的类,会抛出ClassNotFoundException异常。因此,在调用forName()方法时,需要向上面一样,给出一个ClassNotFoundException异常捕获。

  1. getClass():

通过new一个对象,用这个对象调用getClass()方法来获取Class的引用。这个方法属于根类Object的一部分,将返回表示该对象类型的Class引用。

	Dog dog = new Dog();
	Class c2 = dog.getClass();
  1. 类字面常量:
	//字面常量的方式获取Class对象
	Class c3 = Dog.class;

用类字面常量的方式来生成Class对象的引用,在编译时会受到检查(因此不需要置于try语句块中来捕获异常),相对前面两种方式显得更简单、更安全。

采用字面常量的方式不仅可以应用于普通的类,也可以应用在接口、数组以及基本数据类型,这点在反射技术应用传递参数时很有帮助,关于反射技术稍后会分析。另外,因为基本数据类型有着对应的基本包装类型,其包装类型有一个标准字段TYPE,而这个TYPE就是一个引用,指向基本数据类型的Class对象,其等价转换如下:

Class对象TYPE字段
boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

一般建议使用.class的形式,这样可以保持与普通类一致。

上面我们分析了类加载的三个步骤,初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常熟静态域进行首次引用时才执行,而使用“.class”来创建Class对象时,触发的是加载阶段,并不会触发最后阶段类的初始化,下面引用《Java编程思想》中的例子来说明这点:

import java.util.*;

class Initable {
  //静态成员常量,编译期就确定值
  static final int staticFinal = 47;
  //静态成员常量,运行期才确定值
  static final int staticFinal2 =
    ClassInitialization.rand.nextInt(1000);
  //静态初始化块
  static {
    System.out.println("Initializing Initable");
  }
}

class Initable2 {
  //静态成员变量
  static int staticNonFinal = 147;
   //静态初始化块
  static {
    System.out.println("Initializing Initable2");
  }
}

class Initable3 {
  //静态成员变量
  static int staticNonFinal = 74;
  //静态初始化块
  static {
    System.out.println("Initializing Initable3");
  }
}

public class ClassInitialization {
  public static Random rand = new Random(47);
  public static void main(String[] args) throws Exception {
    //字面常量方法获取Class对象
    Class initable = Initable.class;
    System.out.println("After creating Initable ref");
    //不触发类初始化
    System.out.println(Initable.staticFinal);
    //会触发类初始化
    System.out.println(Initable.staticFinal2);
    //会触发类初始化
    System.out.println(Initable2.staticNonFinal);
    //forName()方法获取Class对象
    Class initable3 = Class.forName("Initable3");
    System.out.println("After creating Initable3 ref");
    System.out.println(Initable3.staticNonFinal);
  }
}

Output:

After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74

根据运行结果可以看出:

  1. 初始化有效地实现了尽可能的“惰性”,通过.Class语法来获取Initable类的Class对象时没有触发初始化,通过Class.forName()方式来获取Initable3类的Class对象时就进行了初始化。
  2. 调用Initable.staticFinal变量时,只输出了“47”,并没有打印“Initializing Initable”,说明也没有触发初始化,这是因为staticFinal值是“编译期静态常量”,在编译时其值“47”存储到了NotInitialization常量池中,对常量Initable.staticFinal的引用实际都被转化为NotInitialization类对自身常量池的引用了。如果将一个域只设置为static或final,如Initable2.staticNonFinal,还是会触发初始化。

标签:初始化,对象,class,理解,深入,static,Class,加载
From: https://www.cnblogs.com/xhengge/p/17744496.html

相关文章

  • 理解Map
    1.MapMap用于保存具有映射关系的数据,因此Map集合中保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value,key和value都可以是任何引用类型的数据。Map的key不允许重复,即同一个Map对象的任何两个key通过equals()方法比较总是返回false。如果把Map里的所有key放在......
  • 深入探讨Java Stream流:数据处理的新思维
    文章目录1.流式思想1.1输入流与输出流1.2Stream流2.使用Stream流的步骤3.获取Stream流3.1容器3.2数组4.Stream流中间操作方法4.1`filter(Predicate<?superT>predicate)`4.2`limit(longmaxSize)`4.3`skip(longn)`4.4`distinct()`4.5`sorted()`和`sorted(Compar......
  • 深入探究数据结构与算法:构建强大编程基础
    文章目录1.为什么学习数据结构与算法?1.1提高编程技能1.2解决复杂问题1.3面试准备1.4提高代码效率2.学习资源2.1经典教材2.2在线学习平台2.3学习编程社区3.数据结构与算法的实际应用3.1排序算法3.2图算法3.3字符串匹配算法4.结论......
  • styled-components & CSS pseudo classes All In One
    styled-components&CSSpseudoclassesAllInOne::after&::beforeCSS伪元素constListItem=styled.li`font-size:70px;font-weight:bold;cursor:pointer;color:transparent;-webkit-text-stroke:1pxwhite;position:relative;&......
  • 9张图深入剖析ConcurrentHashMap
    前言在日常的开发中,我们经常使用key-value键值对的HashMap,其使用哈希表实现,用空间换取时间,提升查询性能但在多线程的并发场景中,HashMap并不是线程安全的如果想使用线程安全的,可以使用ConcurrentHashMap、HashTable、Collections.synchronizedMap等但由于后面二者使用synchroniz......
  • 9张图深入剖析ConcurrentHashMap
    前言在日常的开发中,我们经常使用key-value键值对的HashMap,其使用哈希表实现,用空间换取时间,提升查询性能但在多线程的并发场景中,HashMap并不是线程安全的如果想使用线程安全的,可以使用ConcurrentHashMap、HashTable、Collections.synchronizedMap等但由于后面二者使用synchroni......
  • 感性理解梯度下降 GD、随机梯度下降 SGD 和 SVRG
    MLTheory太魔怔了!!!!!从微积分课上我们学到对一个\(\mathscrC^2\)函数,其二阶泰勒展开的皮亚诺余项形式\[f(\bmw')=f(\bmw)+\langle\nablaf(\bmw),\bmw'-\bmw\rangle+o(\|\bmw'-\bmw\|)\]这说明只要\(\bmw'\)和\(\bmw\)挨得足够接近,我们就可......
  • 管理的本质是控制还是理解?
    管理的本质既不是协调,也不是决策,更不是控制,而是服务,管理通过激活与释放要素对象的能量和潜力,为目标、结果服务。 德鲁克所说“管理是实践,管理是激发善意和潜能”的观点是最贴近实质的,也是最讨巧、智慧的概括,其它的表述要么是部分职能、功能,要是某些手段、方法。为了把管理这......
  • 透彻理解 pandas 切片中 df.loc ,df.iloc
    在处理pandas数据框时,选择数据是非常常见的操作。为了满足这种需求,pandas提供了多种选择数据的方法,其中最常用的是df.loc和df.iloc。尽管它们在许多情况下都可以互换使用,但它们之间确实存在一些关键区别,初学者必须明确这些区别以避免潜在的错误。1.基本定义df.loc:基于标......
  • 题解 P9701【[GDCPC2023] Classic Problem】
    题如其名,确实挺经典的。我们称边权在输入中给定的边为特殊边,其它边为平凡边。称特殊边涉及到的点为特殊点,其它点为平凡点。显然,对于连续的若干平凡点\([l,r]\),他们内部的最优连边方式就是连成一条链,花费\(r-l\)的代价。我们先把这样的代价加到答案中,然后将极长连续平凡点缩成......