深入理解Java Class对象

一、深入理解Java类型信息(Class对象)

RTTI(Run-Time Type Identification)运行时类型识别,对于这个词一直是 C++ 中的概念,至于Java中出现RRTI的说法则是源于《Thinking in Java》一书,其作用是在运行时识别一个对象的类型和类的信息。这里分两种:

编译期的:也叫传统的”RRTI”,它假定我们在编译期已知道了所有类型(在没有反射机制创建和使用类对象时,一般都是编译期已确定其类型,如new对象时该类必须已定义好),
另外一种是运行期的:利用反射机制它允许我们在运行时发现和使用类型的信息。
在Java中用来表示运行时类型信息的对应类就是Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中。

其实对于任意一个Class对象,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个Class对象来源于同一个Class文件,只要加载它们的类加载器不同,那这两个Class对象就必定不相等

这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。

所以在java虚拟机中使用双亲委派模型来组织类加载器之间的关系,来保证Class对象的唯一性。

二、Class对象

Class类被创建后的对象就是Class对象,Class对象表示的是自己手动编写类的类型信息,比如创建一个Shapes类,那么,JVM就会创建一个Shapes对应Class类的Class对象,
该Class对象保存了Shapes类相关的类型信息。实际上在Java中每个类都有一个Class对象,
每当我们编写并且编译一个新创建的类就会产生一个对应Class对象并且这个Class对象会被保存在同名.class文件里(编译后的字节码文件保存的就是Class对象),那为什么需要这样一个Class对象呢?
是这样的,当我们new一个新对象或者引用静态成员变量时,Java虚拟机(JVM)中的类加载器子系统会将对应Class对象加载到JVM中,
然后JVM再根据这个类型信息相关的Class对象创建我们需要实例对象或者提供静态变量的引用值。
注意,我们定义的一个类,无论创建多少个实例对象,在JVM中都只有一个Class对象与其对应,即:在内存中每个类有且只有一个相对应的Class对象。

三、Class对象的含义与作用

①手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且这个Class对象保存在同名.class的文件中(字节码文件),
比如创建一个Shapes类,编译Shapes类后就会创建其包含Shapes类相关类型信息的Class对象,并保存在Shapes.class字节码文件中;
.java文件编译成.class文件
.class文件要加载到虚拟机里面才能执行,Class就是这个字节码文件
如果虚拟机拿到这个文件就可以动态的,操作这个实例
所以说,这个玩意就是用来和虚拟机打交道的;

②每个通过关键字class标识的类,无论创建多少个实例对象,在内存中有且只有一个与之对应的Class对象来描述其类型信息;
③Class类只有私有构造函数,因此对应Class对象只能有JVM创建和加载;
Class类的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要;

向Class引用添加泛型约束仅仅是为了提供编译期类型的检查从而避免将错误延续到运行时期。

注:Class对象是后面反射的基础,反射包中的很多类都需要和Class对象一起配合使用。

四、获取一个类的Class对象基本的三种方式

1)上面例子我们已经知道Class.forName()方法的调用将会返回一个对应类的Class对象,因此如果我们想获取一个类的运行时类型信息并加以使用时,
可以调用Class.forName()方法获取Class对象的引用,这样做的好处是无需通过持有该类的实例对象引用而去获取Class对象;

2)调用一个实例对象的getClass()方法(getClass方法从顶级类Object继承而来的)获取其类的Class对象(表示该对象的实际类型的Class对象引用);

3)通过Class字面常量方法生成类的Class对象引用;这种方式相对前面两种方法更加简单、安全、高效。因为它在编译器就会受到编译器的检查,
同时通过字面量的方法获取Class对象的引用不会自动初始化该类(forName()、getClass()方法都需要加载类)。
比如某个对象(属性)不是Object的直接或间接子类(不提供getClass()方法),如interface,基本数据类型。可以通过“.class”属性获取Class<T>对象。

五、理解泛化的Class对象引用

由于Class的引用总数指向某个类的Class对象,利用Class对象可以创建实例类,这也就足以说明Class对象的引用指向的对象确切的类型。在Java SE5引入泛型后,我们可以利用泛型来表示Class对象更具体的类型,即使在运行期间会被擦除,但编译期足以确保我们使用正确的对象类型。如下:

public class ClazzDemo {
 
    public static void main(String[] args){
        //没有泛型
        Class intClass = int.class;
 
        //带泛型的Class对象
        Class<Integer> integerClass = int.class;
 
        integerClass = Integer.class;
 
        //没有泛型的约束,可以随意赋值
        intClass= double.class;
 
        //编译期错误,无法编译通过
        //integerClass = double.class
    }
}
从代码可以看出,声明普通的Class对象,在编译器并不会检查Class对象的确切类型是否符合要求,如果存在错误只有在运行时才得以暴露出来。但是通过泛型声明指明类型的Class对象,编译器在编译期将对带泛型的类进行额外的类型检查,确保在编译期就能保证类型的正确性,实际上Integer.class就是一个Class<Integer>类的对象。

面对下述语句,确实可能令人困惑,但该语句确实是无法编译通过的:

//编译无法通过
Class<Number> numberClass=Integer.class;
我们或许会想Integer不就是Number的子类吗?然而事实并非这般简单,毕竟Integer的Class对象并非Number的Class对象的子类,前面提到过,所有的Class对象都只来源于Class类,看来事实确实如此。当然我们可以利用通配符“?”来解决问题:

Class<?> intClass = int.class;
intClass = double.class;
这样的语句并没有什么问题,毕竟通配符指明所有类型都适用,那么为什么不直接使用Class还要使用Class<?>呢?这样做的好处是告诉编译器,我们是确实是采用任意类型的泛型,而非忘记使用泛型约束,至少前者在编译器检查时不会产生警告信息。当然我们还可以使用extends关键字告诉编译器接收某个类型的子类,如解决前面Number与Integer的问题:

//编译通过!
Class<? extends Number> clazz = Integer.class;
//赋予其他类型
clazz = double.class;
clazz = Number.class;
实际上,应该时刻记住向Class引用添加泛型约束仅仅是为了提供编译期类型的检查从而避免将错误延续到运行时期。

  

六、拓展 Java创建对象的几种方式

①使用new关键字(最简单和最常用的方式)

User user = new User();

  

②使用反射机制

1 使用Class类的newInstance方法
可以使用Class类的newInstance方法创建对象。这个newInstance方法调用无参的构造函数创建对象。

//创建方法1
User user = (User)Class.forName("根路径.User").newInstance(); 
//创建方法2(用这个最好)
User user = User.class.newInstance();
//创建方法1
User user = (User)Class.forName("根路径.User").newInstance(); 
//创建方法2(用这个最好)
User user = User.class.newInstance();

2 使用Constructor类的newInstance方法
和Class类的newInstance方法很像, java.lang.reflect.Constructor类里也有一个newInstance方法可以创建对象。我们可以通过这个newInstance方法调用有参数的和私有的构造函数。

Constructor<User> constructor = User.class.getConstructor();
User user = constructor.newInstance();
这两种newInstance方法就是大家所说的反射。事实上Class的newInstance方法内部调用Constructor的newInstance方法。

 

③使用clone方法

无论何时我们调用一个对象的clone方法,jvm就会创建一个新的对象,将前面对象的内容全部拷贝进去。用clone方法创建对象并不会调用任何构造函数。
要使用clone方法,我们需要先实现Cloneable接口并实现其定义的clone方法。

 

七、java之构造方法私有化 及单例设计 待续。。。

参考:

深入理解Java类型信息(Class对象)

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

Java中Class对象详解

java 7 Reflection详解(一)

Java中创建对象的5种方式

Java 创建对象的几种方式

原文地址:https://www.cnblogs.com/-courage/p/14690932.html