无法在泛型类型
T
上调用静态方法是
类型擦除的副作用。类型擦除意味着在 Java 字节码编译后会删除或擦除泛型类型信息。这个过程是为了保持向后兼容性,以便与在 Java 5 之前编写的代码一起使用(其中引入了泛型)。最初,我们在 Java 5 及更高版本中使用的许多泛型类型都是简单的类。例如,
List
只是一个普通的类,它保存
Object
实例并需要显式转换以确保类型安全:
List myList = new List();
myList.add(new Foo());
Foo foo = (Foo) myList.get(0);
自从Java 5引入泛型后,许多这些类都升级为泛型类。例如,
List
现在变成了
List<T>
,其中
T
是列表中元素的类型。这使得编译器可以执行静态(编译时)类型检查,并消除了执行显式强制转换的需要。例如,使用泛型,上面的代码片段可以简化为以下形式:
List<Foo> myList = new List<Foo>();
myList.add(new Foo());
Foo foo = myList.get(0);
这种通用方法有两个主要优点:(1) 可以避免繁琐且难以控制的类型转换,(2) 编译器可以在编译时确保我们不会混淆类型或执行不安全的操作。例如,以下代码将是非法的,并且会在编译时出错:
List<Foo> myList = new List<Foo>();
myList.add(new Bar()); // Illegal: cannot use Bar where Foo is expected
虽然泛型在类型安全方面有很大帮助,但将它们引入Java可能会破坏现有的代码。例如,仍然应该可以创建一个
List
对象而不需要任何泛型类型信息(这称为使用它作为原始类型)。因此,编译后的泛型Java代码仍必须等同于非泛型代码。换句话说,引入泛型不应影响编译器生成的字节码,因为这将破坏现有的非泛型代码。
因此,决定只在编译时处理泛型。这意味着编译器使用泛型类型信息来确保类型安全,但一旦Java源代码被编译,此泛型类型信息将被删除。如果我们查看问题中方法的生成字节码,就可以验证这一点。例如,假设我们将该方法放在名为
Parser
的类中,并将该方法简化为以下内容:
public class Parser {
public <T extends ParsableDTO<T>> List<T> getParsableDTOs(String table, Class<T> clazz) {
T dto = null;
List<T> list = new ArrayList<>();
list.add(dto);
return list;
}
}
如果我们编译这个类并使用“javap -c Parser.class”检查它的字节码,我们会看到以下内容:
Compiled from "Parser.java"
public class var.Parser {
public var.Parser();
Code:
0: aload_0
1: invokespecial #8
4: return
public <T extends var.ParsableDTO<T>> java.util.List<T> getParsableDTOs(java.lang.String, java.lang.Class<T>);
Code:
0: aconst_null
1: astore_3
2: new #18
5: dup
6: invokespecial #20
9: astore 4
11: aload 4
13: aload_3
14: invokeinterface #21, 2
19: pop
20: aload 4
22: areturn
}
代码行 14: invokeinterface #21, 2
表示我们在使用 T
类型的参数调用了 List
上的 add
方法,尽管实际上我们的源代码中的参数类型是 Object
。由于泛型不能影响编译器生成的字节码,编译器将泛型类型替换为 Object
(这使得泛型类型 T
不可具体化),然后根据需要执行对象的预期类型转换。例如,如果我们编译以下代码:
public class Parser {
public void doSomething() {
List<Foo> foos = new ArrayList<>();
foos.add(new Foo());
Foo myFoo = foos.get(0);
}
}
我们得到以下字节码:
public class var.Parser {
public var.Parser();
Code:
0: aload_0
1: invokespecial
4: return
public void doSomething();
Code:
0: new
3: dup
4: invokespecial
7: astore_1
8: aload_1
9: new
12: dup
13: invokespecial
16: invokeinterface
21: pop
22: aload_1
23: iconst_0
24: invokeinterface
29: checkcast
32: astore_2
33: return
}
第29行的checkcast #18
指示编译器添加了一条指令来检查我们从List
(使用get(0)
)收到的Object
是否可以转换为Foo
。换句话说,我们从List
收到的Object
在运行时实际上是一个Foo
。
那么这与您的问题有何关系呢?在Java中进行类似T.parse(rs)
的调用是无效的,因为编译器无法知道在运行时要调用哪个类的静态方法parse
,因为泛型类型信息在运行时丢失。这也限制了我们无法创建T
类型的对象(即new T();
)。
这个难题非常普遍,以至于它实际上可以在Java库中找到。例如,每个
Collection
对象都有两种方法将
Collection
转换为数组:
Object[] toArray()
和
<T> T[] toArray(T[] a)
。后者允许客户端提供预期类型的数组。这为
Collection
提供了足够的类型信息,在运行时创建并返回预期(相同)类型
T
的数组。例如,如果我们查看JDK 9源代码中的
AbstractCollection
。
public <T> T[] toArray(T[] a) {
T[] r = a.length >= size ? a :
(T[])java.lang.reflect.Array
.newInstance(a.getClass().getComponentType(), size);
}
我们可以看到,该方法能够利用反射创建一个新的类型为
T
的数组,但这需要使用对象
a
。实质上,
a
是被提供的,以便该方法可以在运行时确定
T
的实际类型(对象
a
被问到,“你是什么类型?”)。如果我们不能提供
T[]
参数,则必须使用
Object[] toArray()
方法,该方法只能创建一个
Object[]
(同样来自
AbstractCollection
源代码):
public Object[] toArray() {
Object[] r = new Object[size()];
}
toArray(T[])
使用的解决方案对于您的情况是可行的,但有一些非常重要的区别使其成为一个糟糕的解决方案。在
toArray(T[])
中使用反射是可以接受的,因为在Java中创建数组是一个标准化的过程(由于数组不是用户定义的类,而是标准化的类,就像
String
一样)。因此,构建过程(例如提供哪些参数)是已知的,并且是标准化的。在调用类型上的静态方法的情况下,我们不知道该静态方法是否实际存在于提供的类型中(即没有等效于实现接口来确保静态方法存在的方法)。
相反,最常见的约定是提供一个函数,该函数可用于将请求的参数(在此情况下为
ResultSet
)映射到
T
对象。例如,您的
getParsableDTOs
方法的签名将变为:
public <T extends ParsableDTO<T>> List<T> getParsableDTOs(String table, Function<ResultSet, T> mapper) {
}
mapper
参数仅是一个Function<ResultSet, T>
,这意味着它消耗一个ResultSet
并产生一个T
。这是最通用的方式,因为任何接受ResultSet
对象并生成T
对象的Function
都可以使用。我们也可以创建一个特定的接口来实现此目的:
@FunctionalInterface
public interface RowMapper<T> {
public T mapRow(ResultSet rs);
}
将方法签名更改为以下内容:
public <T extends ParsableDTO<T>> List<T> getParsableDTOs(String table, RowMapper<T> mapper) {
}
因此,将您的代码中的非法调用(对
T
的静态调用)替换为映射函数,我们最终得到:
public <T extends ParsableDTO<T>> List<T> getParsableDTOs(String table, RowMapper<T> mapper) {
List<T> rtn_lst = new ArrayList<T>();
ResultSet rs = doQueryWithReturn(StringQueryComposer
.createLikeSelectQuery(table, null, null, null, true));
try {
while(rs.next()) {
rtn_lst.add(mapper.mapRow(rs));
}
rs.close();
} catch (SQLException e) {
System.err.println("Can't parse DTO from "
+ table + " at " + dateformat.format(new Date()));
System.err.println("\nError on " + e.getClass().getName()
+ ": " + e.getMessage());
e.printStackTrace();
}
return rtn_lst;
}
此外,由于我们将
@FunctionalInterface
用作
getParsableDTOs
的参数,因此可以使用lambda函数将
ResultSet
映射到
T
,如下所示:
Parser parser = new Parser();
parser.getParsableDTOs("FOO_TABLE", rs -> { return new Foo(); });
parse()
方法中的<T>
。它隐藏了接口中声明的T
。 - shmosel