十九、类型信息(2)

2023-10-28 17:04
文章标签 十九 类型信息

本文主要是介绍十九、类型信息(2),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本章概要

  • Class 对象
    • 类字面常量
    • 泛化的 Class 引用
    • cast() 方法

Class 对象

要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为 **Class**对象 的特殊对象完成的,它包含了与类有关的信息。实际上,Class 对象就是用来创建该类所有"常规"对象的。Java 使用 Class 对象来实现 RTTI,即便是类型转换这样的操作都是用 Class 对象实现的。不仅如此,Class 类还提供了很多使用 RTTI 的其它方式。

类是程序的一部分,每个类都有一个 Class 对象。换言之,每当我们编写并且编译了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class 文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用"类加载器"子系统把这个类加载到内存中。

类加载器子系统可能包含一条类加载器链,但有且只有一个原生类加载器,它是 JVM 实现的一部分。原生类加载器加载的是”可信类”(包括 Java API 类)。它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持 Web 服务器应用,或者通过网络下载类),也可以挂载额外的类加载器。

所有的类都是第一次使用时动态加载到 JVM 中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。

其实构造器也是类的静态方法,虽然构造器前面并没有 static 关键字。所以,使用 new 操作符创建类的新对象,这个操作也算作对类的静态成员引用。

因此,Java 程序在它开始运行之前并没有被完全加载,很多部分是在需要时才会加载。这一点与许多传统编程语言不同,动态加载使得 Java 具有一些静态加载语言(如 C++)很难或者根本不可能实现的特性。

类加载器首先会检查这个类的 Class 对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找 .class 文件(如果有附加的类加载器,这时候可能就会在数据库中或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM 会对其进行验证,确保它没有损坏,并且不包含不良的 Java 代码(这是 Java 安全防范的一种措施)。

一旦某个类的 Class 对象被载入内存,它就可以用来创建这个类的所有对象。下面的示范程序可以证明这点:

// typeinfo/SweetShop.java
// 检查类加载器工作方式
class Cookie {static {System.out.println("Loading Cookie");}
}class Gum {static {System.out.println("Loading Gum");}
}class Candy {static {System.out.println("Loading Candy");}
}public class SweetShop {public static void main(String[] args) {System.out.println("inside main");new Candy();System.out.println("After creating Candy");try {Class.forName("Gum");} catch (ClassNotFoundException e) {System.out.println("Couldn't find Gum");}System.out.println("After Class.forName(\"Gum\")");new Cookie();System.out.println("After creating Cookie");}
}

输出结果:

在这里插入图片描述

上面的代码中,CandyGumCookie 这几个类都有一个 static{...} 静态初始化块,这些静态初始化块在类第一次被加载的时候就会执行。也就是说,静态初始化块会打印出相应的信息,告诉我们这些类分别是什么时候被加载了。而在主方法里边,创建对象的代码都放在了 print() 语句之间,以帮助我们判断类加载的时间点。

从输出中可以看到,Class 对象仅在需要的时候才会被加载,static 初始化是在类加载时进行的。

代码里面还有特别有趣的一行:

Class.forName("Gum");

所有 Class 对象都属于 Class 类,而且它跟其他普通对象一样,我们可以获取和操控它的引用(这也是类加载器的工作)。forName()Class 类的一个静态方法,我们可以使用 forName() 根据目标类的类名(String)得到该类的 Class 对象。上面的代码忽略了 forName() 的返回值,因为那个调用是为了得到它产生的“副作用”。从结果可以看出,forName() 执行的副作用是如果 Gum 类没有被加载就加载它,而在加载的过程中,Gumstatic 初始化块被执行了。

还需要注意的是,如果 Class.forName() 找不到要加载的类,它就会抛出异常 ClassNotFoundException。上面的例子中我们只是简单地报告了问题,但在更严密的程序里,就要考虑在异常处理程序中把问题解决掉。

无论何时,只要你想在运行时使用类型信息,就必须先得到那个 Class 对象的引用。Class.forName() 就是实现这个功能的一个便捷途径,因为使用该方法你不需要先持有这个类型 的对象。但是,如果你已经拥有了目标类的对象,那就可以通过调用 getClass() 方法来获取 Class 引用了,这个方法来自根类 Object,它将返回表示该对象实际类型的 Class 对象的引用。Class 包含很多有用的方法,下面代码展示了其中的一部分:

interface HasBatteries {
}interface Waterproof {
}interface Shoots {
}class Toy {// 注释下面的无参数构造器会引起 NoSuchMethodError 错误Toy() {}Toy(int i) {}
}class FancyToy extends Toyimplements HasBatteries, Waterproof, Shoots {FancyToy() {super(1);}
}public class ToyTest {static void printInfo(Class cc) {System.out.println("Class name: " + cc.getName() +" is interface? [" + cc.isInterface() + "]");System.out.println("Simple name: " + cc.getSimpleName());System.out.println("Canonical name : " + cc.getCanonicalName());}public static void main(String[] args) {Class c = null;try {c = Class.forName("com.example.test.ToyTest");} catch (ClassNotFoundException e) {System.out.println("Can't find FancyToy");System.exit(1);}printInfo(c);for (Class face : c.getInterfaces()) {printInfo(face);}Class up = c.getSuperclass();Object obj = null;try {// Requires no-arg constructor:obj = up.newInstance();} catch (InstantiationException e) {System.out.println("Cannot instantiate");System.exit(1);} catch (IllegalAccessException e) {System.out.println("Cannot access");System.exit(1);}printInfo(obj.getClass());}
}

输出结果:

在这里插入图片描述

FancyToy 继承自 Toy 并实现了 HasBatteriesWaterproofShoots 接口。在 main 方法中,我们创建了一个 Class 引用,然后在 try 语句里边用 forName() 方法创建了一个 FancyToy 的类对象并赋值给该引用。需要注意的是,传递给 forName() 的字符串必须使用类的全限定名(包含包名)。

printInfo() 函数使用 getName() 来产生完整类名,使用 getSimpleName() 产生不带包名的类名,getCanonicalName() 也是产生完整类名(除内部类和数组外,对大部分类产生的结果与 getName() 相同)。isInterface() 用于判断某个 Class 对象代表的是否为一个接口。因此,通过 Class 对象,你可以得到关于该类型的所有信息。

在主方法中调用的 Class.getInterfaces() 方法返回的是存放 Class 对象的数组,里面的 Class 对象表示的是那个类实现的接口。

另外,你还可以调用 getSuperclass() 方法来得到父类的 Class 对象,再用父类的 Class 对象调用该方法,重复多次,你就可以得到一个对象完整的类继承结构。

Class 对象的 newInstance() 方法是实现“虚拟构造器”的一种途径,虚拟构造器可以让你在不知道一个类的确切类型的时候,创建这个类的对象。在前面的例子中,up 只是一个 Class 对象的引用,在编译期并不知道这个引用会指向哪个类的 Class 对象。当你创建新实例时,会得到一个 Object 引用,但是这个引用指向的是 Toy 对象。当然,由于得到的是 Object 引用,目前你只能给它发送 Object 对象能够接受的调用。而如果你想请求具体对象才有的调用,你就得先获取该对象更多的类型信息,并执行某种转型。另外,使用 newInstance() 来创建的类,必须带有无参数的构造器。在本章稍后部分,你将会看到如何通过 Java 的反射 API,用任意的构造器来动态地创建类的对象。

类字面常量

Java 还提供了另一种方法来生成类对象的引用:类字面常量。对上述程序来说,就像这样:FancyToy.class;。这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不必放在 try 语句块中)。并且它根除了对 forName() 方法的调用,所以效率更高。

类字面常量不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。另外,对于基本数据类型的包装类,还有一个标准字段 TYPETYPE 字段是一个引用,指向对应的基本数据类型的 Class 对象,如下所示:

…等价于…
boolean.classBoolean.TYPE
char.classCharacter.TYPE
byte.classByte.TYPE
short.classShort.TYPE
int.classInteger.TYPE
long.classLong.TYPE
float.classFloat.TYPE
double.classDouble.TYPE
void.classVoid.TYPE

我的建议是使用 .class 的形式,以保持与普通类的一致性。

注意,有一点很有趣:当使用 .class 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象。为了使用类而做的准备工作实际包含三个步骤:

  1. 加载,这是由类加载器执行的。该步骤将查找字节码(通常在 classpath 所指定的路径中查找,但这并非是必须的),并从这些字节码中创建一个 Class 对象。
  2. 链接。在链接阶段将验证类中的字节码,为 static 字段分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。
  3. 初始化。如果该类具有超类,则先初始化超类,执行 static 初始化器和 static 初始化块。

直到第一次引用一个 static 方法(构造器隐式地是 static)或者非常量的 static 字段,才会进行类初始化。

import java.util.*;class Initable {static final int STATIC_FINAL = 47;static final int STATIC_FINAL2 =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 voidmain(String[] args) throws Exception {Class initable = Initable.class;System.out.println("After creating Initable ref");// Does not trigger initialization:System.out.println(Initable.STATIC_FINAL);// Does trigger initialization:System.out.println(Initable.STATIC_FINAL2);// Does trigger initialization:System.out.println(Initable2.staticNonFinal);Class initable3 = Class.forName("com.example.test.Initable3");System.out.println("After creating Initable3 ref");System.out.println(Initable3.staticNonFinal);}
}

输出结果:

在这里插入图片描述

初始化有效地实现了尽可能的“惰性”,从对 initable 引用的创建中可以看到,仅使用 .class 语法来获得对类对象的引用不会引发初始化。但与此相反,使用 Class.forName() 来产生 Class 引用会立即就进行初始化,如 initable3

如果一个 static final 值是“编译期常量”(如 Initable.staticFinal),那么这个值不需要对 Initable 类进行初始化就可以被读取。但是,如果只是将一个字段设置成为 staticfinal,还不足以确保这种行为。例如,对 Initable.staticFinal2 的访问将强制进行类的初始化,因为它不是一个编译期常量。

如果一个 static 字段不是 final 的,那么在对它访问时,总是要求在它被读取之前,要先进行链接(为这个字段分配存储空间)和初始化(初始化该存储空间),就像在对 Initable2.staticNonFinal 的访问中所看到的那样。

泛化的 Class 引用

Class 引用总是指向某个 Class 对象,而 Class 对象可以用于产生类的实例,并且包含可作用于这些实例的所有方法代码。它还包含该类的 static 成员,因此 Class 引用表明了它所指向对象的确切类型,而该对象便是 Class 类的一个对象。
但是,Java 设计者看准机会,将它的类型变得更具体了一些。Java 引入泛型语法之后,我们可以使用泛型对 Class 引用所指向的 Class 对象的类型进行限定。在下面的实例中,两种语法都是正确的:

// typeinfo/GenericClassReferences.javapublic class GenericClassReferences {public static void main(String[] args) {Class intClass = int.class;Class<Integer> genericIntClass = int.class;genericIntClass = Integer.class; // 同一个东西intClass = double.class;// genericIntClass = double.class; // 非法}
}

普通的类引用不会产生警告信息。你可以看到,普通的类引用可以重新赋值指向任何其他的 Class 对象,但是使用泛型限定的类引用只能指向其声明的类型。通过使用泛型语法,我们可以让编译器强制执行额外的类型检查。

那如果我们希望稍微放松一些限制,应该怎么办呢?乍一看,下面的操作好像是可以的:

Class<Number> geenericNumberClass = int.class;

这看起来似乎是起作用的,因为 Integer 继承自 Number。但事实却是不行,因为 IntegerClass 对象并不是 NumberClass 对象的子类。

为了在使用 Class 引用时放松限制,我们使用了通配符,它是 Java 泛型中的一部分。通配符就是 ?,表示“任何事物”。因此,我们可以在上例的普通 Class 引用中添加通配符,并产生相同的结果:

// typeinfo/WildcardClassReferences.javapublic class WildcardClassReferences {public static void main(String[] args) {Class<?> intClass = int.class;intClass = double.class;}
}

使用 Class<?> 比单纯使用 Class 要好,虽然它们是等价的,并且单纯使用 Class 不会产生编译器警告信息。使用 Class<?> 的好处是它表示你并非是碰巧或者由于疏忽才使用了一个非具体的类引用,而是特意为之。

为了创建一个限定指向某种类型或其子类的 Class 引用,我们需要将通配符与 extends 关键字配合使用,创建一个范围限定。这与仅仅声明 Class<Number> 不同,现在做如下声明:

// typeinfo/BoundedClassReferences.javapublic class BoundedClassReferences {public static void main(String[] args) {Class<? extends Number> bounded = int.class;bounded = double.class;bounded = Number.class;// Or anything else derived from Number.}
}

Class 引用添加泛型语法的原因只是为了提供编译期类型检查,因此如果你操作有误,稍后就会发现这点。使用普通的 Class 引用你要确保自己不会犯错,因为一旦你犯了错误,就要等到运行时才能发现它,很不方便。

下面的示例使用了泛型语法,它保存了一个类引用,稍后又用 newInstance() 方法产生类的对象:

import java.util.function.*;
import java.util.stream.*;class CountedInteger {private static long counter;private final long id = counter++;@Overridepublic String toString() {return Long.toString(id);}
}public class DynamicSupplier<T> implements Supplier<T> {private Class<T> type;public DynamicSupplier(Class<T> type) {this.type = type;}@Overridepublic T get() {try {return type.newInstance();} catch (InstantiationException | IllegalAccessException e) {throw new RuntimeException(e);}}public static void main(String[] args) {Stream.generate(new DynamicSupplier<>(CountedInteger.class)).skip(10).limit(5).forEach(System.out::println);}
}

输出结果:

在这里插入图片描述

注意,这个类必须假设与它一起工作的任何类型都有一个无参构造器,否则运行时会抛出异常。编译期对该程序不会产生任何警告信息。

当你将泛型语法用于 Class 对象时,newInstance() 将返回该对象的确切类型,而不仅仅只是在 ToyTest.java 中看到的基类 Object。然而,这在某种程度上有些受限:

public class GenericToyTest {public static voidmain(String[] args) throws Exception {Class<FancyToy> ftClass = FancyToy.class;// Produces exact type:FancyToy fancyToy = ftClass.newInstance();Class<? super FancyToy> up = ftClass.getSuperclass();// This won't compile:// Class<Toy> up2 = ftClass.getSuperclass();// Only produces Object:Object obj = up.newInstance();}
}

如果你手头的是超类,那编译器将只允许你声明超类引用为“某个类,它是 FancyToy 的超类”,就像在表达式 Class<? super FancyToy> 中所看到的那样。而不会接收 Class<Toy> 这样的声明。这看上去显得有些怪,因为 getSuperClass() 方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了(在本例中就是 Toy.class),而不仅仅只是"某个类"。不管怎样,正是由于这种含糊性,up.newInstance 的返回值不是精确类型,而只是 Object

cast() 方法

Java 中还有用于 Class 引用的转型语法,即 cast() 方法:

// typeinfo/ClassCasts.javaclass Building {}
class House extends Building {}public class ClassCasts {public static void main(String[] args) {Building b = new House();Class<House> houseType = House.class;House h = houseType.cast(b);h = (House)b; // ... 或者这样做.}
}

cast() 方法接受参数对象,并将其类型转换为 Class 引用的类型。但是,如果观察上面的代码,你就会发现,与实现了相同功能的 main 方法中最后一行相比,这种转型好像做了很多额外的工作。

cast() 在无法使用普通类型转换的情况下会显得非常有用,在你编写泛型代码时,如果你保存了 Class 引用,并希望以后通过这个引用来执行转型,你就需要用到 cast()。但事实却是这种情况非常少见,我发现整个 Java 类库中,只有一处使用了 cast()(在 com.sun.mirror.util.DeclarationFilter 中)。

Java 类库中另一个没有任何用处的特性就是 Class.asSubclass(),该方法允许你将一个 Class 对象转型为更加具体的类型。

这篇关于十九、类型信息(2)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/294667

相关文章

【重学 MySQL】十九、位运算符的使用

【重学 MySQL】十九、位运算符的使用 示例检查权限添加权限移除权限 在 MySQL 中,位运算符允许你直接在整数类型的列或表达式上进行位级操作。这些操作对于处理那些需要在二进制表示上进行直接修改或比较的场景特别有用,比如权限管理、状态标记等。 &(位与) 对两个数的二进制表示进行位与操作。只有两个相应的二进制位都为 1 时,结果的该位才为 1,否则为 0。 |(位

类型信息:反射-Class

在说反射前提一个概念:RTTI(在运行时,识别一个对象的类型) public class Shapes {public static void main(String[] args) {List<Shape> shapes = Arrays.asList(new Circle(), new Square(), new Triangle());for (Shape shape : shapes

Flink实例(十九):Flink 异步IO (四)实例 (二) MySQL

业务如下: 接收kafka数据,转为user对象,调用async,使用user.id 查询对应的phone,放回user对象,输出  主类: import com.alibaba.fastjson.JSON;import com.venn.common.Common;import org.apache.flink.formats.json.JsonNodeDeserializatio

【硬刚ES】ES基础(十九) Query Filtering 与多字符串多字段查询

本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的ES部分补充。

OCI编程高级篇(十九) 创建和使用OCI连接池

上一节介绍了连接池的概念和使用连接池的步骤,这一节看看具体的操作是怎样的,先看一下用到的函数原型和参数。 创建连接池函数OCIConnectionPoolCreate(),原型和参数如下。 sword OCIConnectionPoolCreate ( OCIEnv *envhp,     OCIError          *errhp,     OCICPool        *poolh

【Unity 3D】学习笔记十九:实例:游戏人物移动

结合学习笔记十八,来学习游戏中人物的基本移动(真的感觉好基础啊)。不多说,直接上代码。 例: //动画数组private var animUp: Object[] ;private var animDown: Object[] ;private var animLeft: Object[] ;private var animRight: Object[] ;//地图贴图priv

斗破C++编程入门系列之十九:C++程序设计必知:多文件结构和编译预处理命令(九星斗者)

斗破C++目录: 斗破C++编程入门系列之前言(斗之气三段) 斗破C++编程入门系列之二:Qt的使用介绍(斗之气三段) 斗破C++编程入门系列之三:数据结构(斗之气三段) 斗破C++编程入门系列之四:运算符和表达式(斗之气五段) 斗破C++编程入门系列之五:算法的基本控制结构之选择结构(斗之气八段) 斗破C++编程入门系列之六:算法的基本控制结构之循环结构(斗之气八段) 斗破C++编程入门系列之

Django REST Framework(十九)权限

Django REST framework (DRF) 的权限认证涉及以下几个方面:全局权限配置、局部权限配置、自定义权限类、以及自定义认证类。以下是关于这些方面的详细说明: 1. 全局权限配置 在 Django 项目的配置文件 settings.py 中,可以全局配置 DRF 的权限管理类。这种设置适用于整个项目中的所有视图。默认情况下,如果不做任何配置,DRF 会允许所有用户访问视图(Al

【从问题中去学习k8s】k8s中的常见面试题(夯实理论基础)(十九)

本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》:python零基础入门学习 《python运维脚本》: python运维脚本实践 《shell》:shell学习 《terraform》持续更新中:terraform_Aws学习零基础入门到最佳实战 《k8》从问题中去学习k8s 《docker学习》暂未更新 《ceph学习》ceph日常问题解决分享 《日志收集》ELK

代谢组数据分析(十九):随机森林构建代谢组预后模型

介绍 建立胃癌(GC)预后模型时,从队列3中的181名患者中,使用右删失结果数据进行了随机分层抽样,分为训练数据集(n = 121)和测试数据集(n = 60)。训练了一个包含1000棵树的随机生存森林(RSF)模型,根据它们基于排列的特征重要性来选择突出的特征。通过再次训练随机生存模型,并选取28种代谢物,建立了显示出色预测能力(AUROC = 0.832,95% CI:0.697-0.9