文章目录
很多人应该很清楚,在 Java 中,是不能直接创建泛型对象和泛型数组的。原因是 Java 有类型擦除,任何泛型类型在擦除之后就变成了 Object 类型,因此创建泛型对象就相当于创建了一个 Object 类型的对象。创建 Object 类型的对象通常没有任何意义,所以直接创建泛型对象的行为被编译器禁止。泛型数组也是一样。
不能创建泛型对象,意味着下面的代码是非法的。
// 设 T 为已被“赋值”的泛型变量
T targetObject = new T(...); // 因为类型擦除,所以 new T(...) 会退化为 new Object(...)
从某种意义上说,Java 实际上并没有真正实现泛型。Java 中的泛型只是基于面向对象语言共有的允许“向上引用”的语法而已。因为 Java 中所有的对象都直接或间接继承自 Object 类,所以基于这种技术的泛型变得可行。也因为这样,在 Java 中的很多情况下,可以使用 Object 类来代替使用泛型。
真正的泛型并不是无法实现的,例如 C++
就已经实现了真正的泛型,它将其称为 模板
。为什么 Java 不实现真正的泛型技术呢?第一,Java 与 C 家族的语言相比,Java 相对而言不是一个更注重算法的语言,因此使用泛型的场景不是太多。例如,Java 的语法默认支持的是浅拷贝。与动不动就调用 复制构造函数
、=运算符重载函数
的 C++
相比,Java 更喜欢直接复制对象的引用(指针)值,来重用之前的对象,因此,真正需要创建对象的情况不多。第二,真正实现泛型之后,之前涉及泛型的代码将不能用。对于市场占有率岌岌可危的 Java 的来说,将会极大地损失用户,带来灾难性的后果。
不过,由于 Java 还有其它丰富语法,因此这并不是没有办法。
创建泛型对象
Java 8 之前:使用反射创建
在 Java 8 之前,使用反射就可以创建泛型对象。操作模板如下:
设 T 的某个构造器的参数分别为类 A、类 B 的对象。类 A、类 B 均有公有的无参数构造器,且 A、B 均不是泛型(变量):
// classT 为已有 Class 对象,函数 getClassT 代表一个返回 Class 对象的函数
Class<T> classT = getClassT();
// aObject 为已有对象,函数 getFirstParameter 代表一个返回 A 对象的函数
A aObject = getFirstParameter();
// bObject 为已有对象,函数 getSecondParameter 代表一个返回 B 对象的函数
B bObject = getSecondParameter();
// “通过类来获得类对象”:使用 T 的有参数构造器来创建对象
Object targetObj = classT
.getConstructor(A.class, B.class)
.newInstance(aObject, bObject);
这种办法为什么可以创建泛型对象
为什么这样做可行呢?原因是,擦除的类在虚拟机中仍然保留有原先泛型的微弱记忆,因此如果使用反射技术就可以找到原先的类,并调用其构造函数来创建该类对象。这就相当于说,多年失散的父子可能第一眼不能相认,但仍然可以通过亲子鉴定来确定他们的关系。
在 Java 8 之后:使用 IoC 技术创建
在 Java 8 之后,Java 终于有了久违的 lambda 表达式。使用 lambda 表达式及其变体也可以用于间接创建泛型对象。
有些读者可能对此不太理解,这里从一个简单的示例开始,最后来讲它的原理。
首先先定义一个示例类:
package org.wangpai.genericcreator.model;
public class DemoObject {
public DemoObject() {
this.msg = "本对象是使用无参构造器创建的";
}
public DemoObject(String para1, String para2) {
this.msg = para1 + para2;
}
private String msg = "这是 DemoObject";
public void show() {
System.out.println(this.msg);
}
}
注意:本 DemoObject 类有一个无参构造器。Java 8 提供了一个名为 Supplier<T>
的泛型接口,它同时也是一个 函数式接口
。它有一个无参方法 get
可用于返回一个泛型对象,条件是需要实现这个接口。这很简单,由于支持 lamdba 的语法,可以提供一种 构造器引用
。就像这样。
public static <T> T createGenericObject(Supplier<T> genericObjectCreator) {
return genericObjectCreator.get();
}
DemoObject demoObject1 = GenericCreator.createGenericObject(DemoObject::new);
前面有言,DemoObject 支持一个无参构造器,因此 Supplier<T>
接口将使用这个构造器来创建 DemoObject 对象。
请注意,Supplier<T>
接口本身没有任何魅力,它只是一个没有任何实现的接口,它也不是一个 native 方法。是我们提供了创建 DemoObject 对象的方法实现。因此,并一定非要使用 Supplier<T>
接口,我们可以自己定义一个接口。
Supplier<T>
接口的方法是无参的,所以它一般说来只能调用无参构造器,我们可以设计一个有参接口。
接口
package org.wangpai.genericcreator.model;
@FunctionalInterface
public interface DemoObjectConstructor<T> {
T constructor(String firstPara, String secondPara);
}
然后再提供它的 接口调用方法
,以及 接口实现方法
。
接口调用方法
public static <T> T createGenericObject(DemoObjectConstructor<T> genericObjectCreator,
String firstPara, String secondPara) {
return genericObjectCreator.constructor(firstPara, secondPara);
}
接口实现方法
DemoObject demoObject2 = GenericCreator.createGenericObject(
(firstPara, secondPara) -> new DemoObject(firstPara, secondPara),
"本对象是使用两个参数的构造器创建的。",
"编号 002");
当然,也可以直接提供 构造器引用
。
DemoObject demoObject3 = GenericCreator.createGenericObject(DemoObject::new,
"本对象是使用两个参数的构造器创建的。", "编号 003");
综合代码如下:
package org.wangpai.genericcreator.test;
import org.wangpai.genericcreator.model.DemoObject;
import org.wangpai.genericcreator.model.GenericCreator;
public class GenericObjectTest {
public static void main(String[] args) {
DemoObject demoObject1 = GenericCreator.createGenericObject(DemoObject::new);
demoObject1.show();
DemoObject demoObject2 = GenericCreator.createGenericObject(
(firstPara, secondPara) -> new DemoObject(firstPara, secondPara),
"本对象是使用两个参数的构造器创建的。",
"编号 002");
demoObject2.show();
DemoObject demoObject3 = GenericCreator.createGenericObject(DemoObject::new,
"本对象是使用两个参数的构造器创建的。", "编号 003");
demoObject3.show();
}
}
运行结果如下:
本对象是使用无参构造器创建的
本对象是使用两个参数的构造器创建的。编号 002
本对象是使用两个参数的构造器创建的。编号 003
这种办法为什么可以创建泛型对象
不是有类型擦除,为什么还可以创建泛型对象呢?注意,这并不与“不能直接创建泛型对象”所矛盾,这实际上是基于一种 IoC 技术。上面的泛型方法实际上自身并没有直接创建泛型对象,它只是调用了一个方法,而这个方法是我们在使用这个泛型方法时才实现,且这个方法也没有创建泛型对象,它创建的是具体类型的对象,所以这不矛盾。
IoC 需要函数式编程的支持。这种办法的原理其实就是构造这样的一个方法 A,这个方法 A 假定一个方法 B 会返回一个泛型对象,因此它可以直接使用方法 B 的返回值,也就是那个泛型对象,而不需要创建它。创建这个泛型对象是由方法 B 完成的,而方法 B 的实现是在要调用方法 A 时临时完成的。因此,这个办法实际上就是在调用方法 A 的这时,提供方法 B 的实现,而这个方法 B 会创建一个具体的非泛型对象并返回,从而表现得好像方法 A 可以创建泛型对象一样。
关于函数式编程,可见笔者的另一篇博客:
Java 函数式编程入门:
https://blog.csdn.net/wangpaiblog/article/details/122762637
能不能事先提供方法 B 的实现呢?不能。还是因为不能直接创建泛型对象,而又因为是泛型,所以无法知道需要创建的具体是哪个具体类的对象,所以只能在使用方法 A 时提供。因为在那时,方法 A 的使用者就知道了自己具体需要使用的类型是什么,因此就可以为其提供构造方法,这叫做泛型的实例化。
创建泛型数组
有了前面的铺垫,现在就变得很简单。因为数组本质上也是一种特殊的对象类型,所以从本质上来讲,此处的办法与前面的原理是一样的,只是使用的 API 有所不同。
Java 8 之前:使用反射创建
操作模板如下:
// classArrayT 为已有 Class 对象,函数 getArrayClassT 代表一个返回目标数组所属的 Class 对象的函数
Class<T> classArrayT = getArrayClassT();
// arrayLength 为已有 int 类型变量,函数 getArrayLength 代表一个返回所需数组长度的函数
int arrayLength = getArrayLength();
// 通过数组类型来创建空数组
T[] targetArray = (T[]) Array.newInstance(classArrayT.getComponentType(), arrayLength);
示例如下:
public static <T> T[] createGenericArray(Class<T[]> classArrayT, int arrayLength) {
return (T[]) Array.newInstance(classArrayT.getComponentType(), arrayLength);
}
int length = 100;
// demoArray 为空数组,不能直接使用,还需初始化其内各元素
Demo[] demoArray = GenericCreator.createGenericArray(Demo[].class, length);
注意:这样创建之后,得到的是一个空有长度无内容的空数组,因此,后续使用该数组,还需要依次初始化该数组的各个元素。
在 Java 8 之后:使用 IoC 技术创建
因为创建空数组不需要参数,所以创建泛型数组将会比前面创建泛型变量简单。此处不再需要自定义接口了,因为 JDK 已经提供了一个 IntFunction<T[]>
接口,这个接口正好有一个 int 类型的参数。因此只需要提供它的 接口调用方法
,然后直接传入一个数组的构造器即可。
接口调用方法
public static <T> T[] createGenericArray(IntFunction<T[]> genericArrayCreator, int arrayLength) {
return genericArrayCreator.apply(arrayLength);
}
接口实现方法
int length = 10;
DemoObject[] demoArray1 = GenericCreator.createGenericArray(DemoObject[]::new, length);
但是要注意,这样创建之后,得到的是一个空有长度无内容的空数组,因此,后续使用该数组,还需要依次初始化该数组的各个元素。
不是不能实现让数组在创建时初始化。为此,需要自行编写一个返回数组的 lambda 表达式。
int length = 10;
DemoObject[] demoArray2 = GenericCreator.createGenericArray(
arrayLength -> {
DemoObject[] demoArray = new DemoObject[arrayLength];
for (int order = 0; order < demoArray.length; ++order) {
demoArray[order] = new DemoObject("本对象是使用两个参数的构造器创建的。", "编号 " + order);
}
return demoArray;
}, length);
综合代码如下:
package org.wangpai.genericcreator.test;
import org.wangpai.genericcreator.model.DemoObject;
import org.wangpai.genericcreator.model.GenericCreator;
public class GenericArrayTest {
public static void main(String[] args) {
int length = 10;
// demoArray1 为空数组,不能直接使用,还需初始化其内各元素
DemoObject[] demoArray1 = GenericCreator.createGenericArray(DemoObject[]::new, length);
DemoObject[] demoArray2 = GenericCreator.createGenericArray(
arrayLength -> {
DemoObject[] demoArray = new DemoObject[arrayLength];
for (int order = 0; order < demoArray.length; ++order) {
demoArray[order] = new DemoObject("本对象是使用两个参数的构造器创建的。", "编号 " + order);
}
return demoArray;
}, length);
for (var demoObject : demoArray2) {
demoObject.show();
}
}
}
运行结果如下:
本对象是使用两个参数的构造器创建的。编号 0
本对象是使用两个参数的构造器创建的。编号 1
本对象是使用两个参数的构造器创建的。编号 2
本对象是使用两个参数的构造器创建的。编号 3
本对象是使用两个参数的构造器创建的。编号 4
本对象是使用两个参数的构造器创建的。编号 5
本对象是使用两个参数的构造器创建的。编号 6
本对象是使用两个参数的构造器创建的。编号 7
本对象是使用两个参数的构造器创建的。编号 8
本对象是使用两个参数的构造器创建的。编号 9
测评
那么,究竟是使用反射的方案,程序运行速度快,还是使用 IoC 方案呢?很多人可能认为反射会拖慢速度,所以是使用反射更慢。不过实践是检验真理的唯一标准。
这里以创建数组为例进行了测试。由于运行结果会因机器性能、运行环境等而异,运行结果仅供参考。
@Test
void test() {
long maxTime = 10000000000L;
int length = 10000;
{
final long START_TIME = System.currentTimeMillis();
for (long time = 0; time < maxTime; ++time) {
GenericCreator.createGenericArray(Demo[]::new, length);
}
final long interval = System.currentTimeMillis() - START_TIME;
System.out.println(String.format("测试 1 运行用时:%dms", interval));
}
{
final long START_TIME = System.currentTimeMillis();
for (long time = 0; time < maxTime; ++time) {
GenericCreator.createGenericArray(Demo[].class, length);
}
final long interval = System.currentTimeMillis() - START_TIME;
System.out.println(String.format("测试 2 运行用时:%dms", interval));
}
// ----------- 以上是预测试,结果不计最终测试结果。实验结果表明,方法在第一次运行时,时间会偏大很多,所以先进行一次预测试 ----------
{
final long START_TIME = System.currentTimeMillis();
for (long time = 0; time < maxTime; ++time) {
GenericCreator.createGenericArray(Demo[]::new, length);
}
final long interval = System.currentTimeMillis() - START_TIME;
System.out.println(String.format("测试 1 运行用时:%dms", interval));
}
{
final long START_TIME = System.currentTimeMillis();
for (long time = 0; time < maxTime; ++time) {
GenericCreator.createGenericArray(Demo[].class, length);
}
final long interval = System.currentTimeMillis() - START_TIME;
System.out.println(String.format("测试 2 运行用时:%dms", interval));
}
}
测试结果表明,在数组长度为 1 万、连续测试 100 亿次时,这两种方案的运行结果均大致为 3.4s,因此就运行效率而言,无论使用哪种方案都是可以的。
完整源代码
已上传至 GitHub 中,可免费下载:https://github.com/wangpaiblog/20220916_create-generic-object
总结
使用 IoC 技术间接实现创建泛型对象,总结起来,就是需要完成以下三个部分:
-
接口
-
接口调用方法
-
接口实现方法