Java中Class对象详解

Wesley13
• 阅读 930

https://blog.csdn.net/mcryeasy/article/details/52344729

待优化整理 总结

Class类简介

  在java世界里,一切皆对象。从某种意义上来说,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对象是在类加载的时候由Java虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。一个类被加载到内存并供我们使用需要经历如下三个阶段:

  1. 加载,这是由类加载器(ClassLoader)执行的。通过一个类的全限定名来获取其定义的二进制字节流(Class字节码),将这个字节流所代表的静态存储结构转化为方法去的运行时数据接口,根据字节码在java堆中生成一个代表这个类的java.lang.Class对象。

  2. 链接。在链接阶段将验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求,为静态域分配存储空间并设置类变量的初始值(默认的零值),并且如果必需的话,将常量池中的符号引用转化为直接引用。

  3. 初始化。到了此阶段,才真正开始执行类中定义的java程序代码。用于执行该类的静态初始器和静态初始块,如果该类有父类的话,则优先对其父类进行初始化。

  
  所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。这一点与许多传统语言都不同。动态加载使能的行为,在诸如C++这样的静态加载语言中是很难或者根本不可能复制的。

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


如何获得Class对象

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

  1. Class.forName(“类的全限定名”)
  2. 实例对象.getClass()
  3. 类名.class (类字面常量)

Class.forName 和getClass()

我们先看看如下的例子:

  1. package com.cry;
  2. class Dog {
  3. static {
  4. System.out.println("Loading Dog");
  5. }
  6. }
  7. class Cat {
  8. static {
  9. System.out.println("Loading Cat");
  10. }
  11. }
  12. public class Test {
  13. public static void main(String\[\] args){
  14. System.out.println("inside main");
  15. new Dog();
  16. System.out.println("after creating Dog");
  17. try {
  18. Class cat=Class.forName("com.cry.Cat");
  19. } catch (ClassNotFoundException e) {
  20. System.out.println("Couldn't find Cat");
  21. }
  22. System.out.println("finish main");
  23. }
  24. }
  25. /\* Output:
  26. inside main
  27. Loading Dog
  28. after creating Dog
  29. Loading Cat
  30. finish main
  31. \*/

  上面的Dog、Cat类中都有一个静态语句块,该语句块在类第一次被加载时候被执行。这时会有相应的信息打印出来,告诉我们这个类什么时候被加载了。从输出中可以看到,Class对象仅在需要的时候才被加载,static初始化是在类加载时进行的。

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

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

  1. package com.cry;
  2. class Dog {
  3. static {
  4. System.out.println("Loading Dog");
  5. }
  6. }
  7. public class Test {
  8. public static void main(String\[\] args) {
  9. System.out.println("inside main");
  10. Dog d = new Dog();
  11. System.out.println("after creating Dog");
  12. Class c = d.getClass();
  13. System.out.println("finish main");
  14. }
  15. }
  16. /\* Output:
  17. inside main
  18. Loading Dog
  19. after creating Dog
  20. finish main
  21. \*/

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

类字面常量

  java还提供了另一种方法来生成对Class对象的引用。即使用类字面常量,就像这样:Cat.class,这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。并且根除了对forName()方法的调用,所有也更高效。类字面量不仅可以应用于普通的类,也可以应用于接口、数组及基本数据类型。

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

  1. Class c1 = Integer.class;
  2. Class c2 = int.class;
  3. System.out.println(c1);
  4. System.out.println(c2);
  5. System.out.println(c1 == c2);
  6. /\* Output
  7. class java.lang.Integer
  8. int
  9. false
  10. \*/

但是在包装类中有个一个字段TYPE,TYPE字段是一个引用,指向对应的基本数据类型的Class对象,如下所示,左右两边相互等价: 
Java中Class对象详解  
  
  用.class来创建对Class对象的引用时,不会自动地初始化该Class对象(这点和Class.forName方法不同)。类对象的初始化阶段被延迟到了对静态方法或者非常数静态域首次引用时才执行:

  1. package com.cry;
  2. class Dog {
  3. static final String s1 = "Dog\_s1";
  4. static String s2 = "Dog\_s2";
  5. static {
  6. System.out.println("Loading Dog");
  7. }
  8. }
  9. class Cat {
  10. static String s1 = "Cat\_s1";
  11. static {
  12. System.out.println("Loading Cat");
  13. }
  14. }
  15. public class Test {
  16. public static void main(String\[\] args) throws ClassNotFoundException {
  17. System.out.println("----Star Dog----");
  18. Class dog = Dog.class;
  19. System.out.println("------");
  20. System.out.println(Dog.s1);
  21. System.out.println("------");
  22. System.out.println(Dog.s2);
  23. System.out.println("---start Cat---");
  24. Class cat = Class.forName("com.cry.Cat");
  25. System.out.println("-------");
  26. System.out.println(Cat.s1);
  27. System.out.println("finish main");
  28. }
  29. }
  30. /\* Output:
  31. ----Star Dog----
  32. ------
  33. Dog\_s1
  34. ------
  35. Loading Dog
  36. Dog\_s2
  37. ---start Cat---
  38. Loading Cat
  39. -------
  40. Cat\_s1
  41. finish main
  42. \*/

  从上面我们可以看到,如果仅使用.class语法来获得对类的Class引用是不会引发初始化的。但是如果使用Class.forName来产生引用,就会立即进行了初始化,就像Cat所看到的。

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

通过javap -c -v对Dog的字节码进行反汇编:

  1. {
  2. static final java.lang.String s1;
  3. flags: ACC\_STATIC, ACC\_FINAL
  4. ConstantValue: String Dog\_s1
  5. static java.lang.String s2;
  6. flags: ACC\_STATIC
  7. com.cry.Dog();
  8. flags:
  9. Code:
  10. stack=1, locals=1, args\_size=1
  11. 0: aload\_0
  12. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  13. 4: return
  14. LineNumberTable:
  15. line 3: 0
  16. LocalVariableTable:
  17. Start Length Slot Name Signature
  18. 0 5 0 this Lcom/cry/Dog;
  19. static {};
  20. flags: ACC\_STATIC
  21. Code:
  22. stack=2, locals=0, args\_size=0
  23. 0: ldc #2 // String Dog\_s2
  24. 2: putstatic #3 // Field s2:Ljava/lang/String;
  25. 5: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
  26. 8: ldc #5 // String Loading Dog
  27. 10: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  28. 13: return
  29. LineNumberTable:
  30. line 6: 0
  31. line 9: 5
  32. line 10: 13
  33. }

  从上面可以看出s1在编译后被ConstantValue属性修饰 ConstantValue: String Dog\_s1,表示即同时被final和static修饰。而s2并没有被ConstantValue修饰,因为它不是一个编译时常量。在static{}中表示类的初始化操作,在操作中我们看到只有s2字段进行了赋值,而却没有s1的踪影,因此调用s1字段是不会触发类的初始化的。

小结

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

  1. package com.cry;
  2. class Cat {
  3. static {
  4. System.out.println("Loading Cat");
  5. }
  6. }
  7. public class Test {
  8. public static void main(String\[\] args) throws ClassNotFoundException {
  9. System.out.println("inside main");
  10. Class c1 = Cat.class;
  11. Class c2= Class.forName("com.cry.Cat");
  12. Class c3=new Cat().getClass();
  13. Class c4 =new Cat().getClass();
  14. System.out.println(c1==c2);
  15. System.out.println(c2==c3);
  16. System.out.println("finish main");
  17. }
  18. }
  19. /\* Output:
  20. inside main
  21. -------
  22. Loading Cat
  23. true
  24. true
  25. finish main
  26. \*/

从上面我们可以看出执行不同获取Class引用的方法,返回的其实都是同一个Class对象。

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


泛型Class引用

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

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

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

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

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

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

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

  1. Class<?> c1 = int.class;
  2. c1= double.class;

Class

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

通配符?不仅可以与extend结合,而且还可以与super关键字相结合,表示被限定为某种类型,或该类型的任何父类型:

  1. Class<? super Integer> c1 = Integer.class;
  2. c1 = Number.class;
  3. c1 = Object.class;
  4. c1=Integer.class.getSuperclass();

向Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查。


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。

  1. package com.cry;
  2. import java.lang.reflect.Field;
  3. interface I1 {
  4. }
  5. interface I2 {
  6. }
  7. class Cell{
  8. public int mCellPublic;
  9. }
  10. class Animal extends Cell{
  11. private int mAnimalPrivate;
  12. protected int mAnimalProtected;
  13. int mAnimalDefault;
  14. public int mAnimalPublic;
  15. private static int sAnimalPrivate;
  16. protected static int sAnimalProtected;
  17. static int sAnimalDefault;
  18. public static int sAnimalPublic;
  19. }
  20. class Dog extends Animal implements I1, I2 {
  21. private int mDogPrivate;
  22. public int mDogPublic;
  23. protected int mDogProtected;
  24. private int mDogDefault;
  25. private static int sDogPrivate;
  26. protected static int sDogProtected;
  27. static int sDogDefault;
  28. public static int sDogPublic;
  29. }
  30. public class Test {
  31. public static void main(String\[\] args) throws IllegalAccessException, InstantiationException {
  32. Class<Dog> dog = Dog.class;
  33. //类名打印
  34. System.out.println(dog.getName()); //com.cry.Dog
  35. System.out.println(dog.getSimpleName()); //Dog
  36. System.out.println(dog.getCanonicalName());//com.cry.Dog
  37. //接口
  38. System.out.println(dog.isInterface()); //false
  39. for (Class iI : dog.getInterfaces()) {
  40. System.out.println(iI);
  41. }
  42. /\*
  43. interface com.cry.I1
  44. interface com.cry.I2
  45. \*/
  46. //父类
  47. System.out.println(dog.getSuperclass());//class com.cry.Animal
  48. //创建对象
  49. Dog d = dog.newInstance();
  50. //字段
  51. for (Field f : dog.getFields()) {
  52. System.out.println(f.getName());
  53. }
  54. /\*
  55. mDogPublic
  56. sDogPublic
  57. mAnimalPublic
  58. sAnimalPublic
  59. mCellPublic //父类的父类的公共字段也打印出来了
  60. \*/
  61. System.out.println("---------");
  62. for (Field f : dog.getDeclaredFields()) {
  63. System.out.println(f.getName());
  64. }
  65. /\*\* 只有自己类声明的字段
  66. mDogPrivate
  67. mDogPublic
  68. mDogProtected
  69. mDogDefault
  70. sDogPrivate
  71. sDogProtected
  72. sDogDefault
  73. sDogPublic
  74. \*/
  75. }
  76. }

getName、getCanonicalName与getSimpleName的区别:

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

  1. package com.cry;
  2. public class Test {
  3. private class inner{
  4. }
  5. public static void main(String\[\] args) throws ClassNotFoundException {
  6. //普通类
  7. System.out.println(Test.class.getSimpleName()); //Test
  8. System.out.println(Test.class.getName()); //com.cry.Test
  9. System.out.println(Test.class.getCanonicalName()); //com.cry.Test
  10. //内部类
  11. System.out.println(inner.class.getSimpleName()); //inner
  12. System.out.println(inner.class.getName()); //com.cry.Test$inner
  13. System.out.println(inner.class.getCanonicalName()); //com.cry.Test.inner
  14. //数组
  15. System.out.println(args.getClass().getSimpleName()); //String\[\]
  16. System.out.println(args.getClass().getName()); //\[Ljava.lang.String;
  17. System.out.println(args.getClass().getCanonicalName()); //java.lang.String\[\]
  18. //我们不能用getCanonicalName去加载类对象,必须用getName
  19. //Class.forName(inner.class.getCanonicalName()); 报错
  20. Class.forName(inner.class.getName());
  21. }
  22. }

参考: 
java中的Class对象和new关键字解析 
Thinking in Java


原文地址:https://blog.csdn.net/dufufd/article/details/80537638
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
AndroidStudio封装SDK的那些事
<divclass"markdown\_views"<!flowchart箭头图标勿删<svgxmlns"http://www.w3.org/2000/svg"style"display:none;"<pathstrokelinecap"round"d"M5,00,2.55,5z"id"raphael
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这