我有这个界面:
public interface ParsableDTO<T> {
public <T> T parse(ResultSet rs) throws SQLException;
}
在某种dto类中实现,并在另一个类中实现此方法:
public <T extends ParsableDTO<T>> List<T> getParsableDTOs(String table,
Class<T> dto_class) {
List<T> rtn_lst = new ArrayList<T>();
ResultSet rs = doQueryWithReturn(StringQueryComposer
.createLikeSelectQuery(table, null, null, null, true));
try {
while(rs.next()) {
rtn_lst.add(T.parse(rs)); //WRONG, CAN'T ACCESS TO parse(...) OF ParsableDTO<T>
}
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;
}
如何访问可解析特定parse(ResultSet rs)
的界面的T
方法?是否有一种可行的,不同的和/或更好的方法来做到这一点?
答案 0 :(得分:9)
您正在尝试在泛型上调用非静态方法,该方法在编译时会被擦除。即使该方法是静态的,编译器也无法允许(因为在这种情况下T是ParseableDTO
,而从不具体实现)。
相反,假设您使用的是Java 8,我会这样做:
@FunctionalInterface
public interface RowMapper<T> {
T mapRow(ResultSet rs) throws SQLException;
}
然后:
public <T> List<T> getParsableDTOs(String table, RowMapper<T> mapper) {
try (ResultSet rs = doQueryWithReturn(StringQueryComposer
.createLikeSelectQuery(table, null, null, null, true))) {
List<T> rtn_lst = new ArrayList<T>();
while(rs.next()) {
rtn_lst.add(mapper.mapRow(rs));
}
return rtn_lst;
} catch (SQLException e) {
// ...
}
return rtn_lst;
}
接口RowMapper
派生自现有框架,例如JDBC Template。
这个想法是分开关注点:DTO不受JDBC相关方法的污染(例如:映射或解析,但我建议你避免使用parse
名称,因为你并没有真正解析SQL {{1你甚至可以在DAO中保留映射(lambda使它更容易实现)。
使用JDBC对DTO进行污染可能会有问题,因为客户端/调用方可能没有有效的ResultSet
传递给ResultSet
。更糟糕的是:在较新的JDK(9 ++)中,parse
接口位于ResultSet
模块中,可能不可用(如果考虑Web服务,客户端根本不需要JDBC)。
另外,从Java 7开始,您可以使用 try-with-resource 和java.sql
以更安全的方式自动关闭它:在您的实现中,您是如果没有错误,只关闭ResultSet
。
如果你遇到Java 6,你应该使用以下习语:
ResultSet
答案 1 :(得分:1)
无法在泛型T
上调用静态方法是type erasure的副作用。类型擦除意味着在编译后从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 // Method java/lang/Object."<init>":()V
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 // class java/util/ArrayList
5: dup
6: invokespecial #20 // Method java/util/ArrayList."<init>":()V
9: astore 4
11: aload 4
13: aload_3
14: invokeinterface #21, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload 4
22: areturn
}
行14: invokeinterface #21, 2
表示我们使用add
参数在List
上调用Object
,即使源代码中实际的参数类型为{{1} }}。由于泛型不能影响编译器生成的字节码,编译器会将通用类型替换为T
(这使得泛型类型Object
non-reifiable)然后,如果需要,执行强制转换为预期的对象类型。例如,如果我们编译以下内容:
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 #8 // Method java/lang/Object."<init>":()V
4: return
public void doSomething();
Code:
0: new #15 // class java/util/ArrayList
3: dup
4: invokespecial #17 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: new #18 // class var/Foo
12: dup
13: invokespecial #20 // Method Foo."<init>":()V
16: invokeinterface #21, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
21: pop
22: aload_1
23: iconst_0
24: invokeinterface #27, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
29: checkcast #18 // class Foo
32: astore_2
33: return
}
显示编译器添加了一条指令,用于检查我们从29: checkcast #18
(使用Object
)收到的List
是否可以转换为{{ 1}}。换句话说,我们从get(0)
收到的Foo
在运行时实际上是Object
。
那么这个因素如何影响你的问题呢?在Java中进行诸如List
之类的调用是无效的,因为编译器无法在运行时知道调用静态方法Foo
的类,因为泛型类型信息在运行时丢失。这也限制了我们创建T.parse(rs)
类型的对象(即parse
)。
这个难题非常普遍,实际上它存在于Java库本身中。例如,每个T
对象都有两种方法可将new T();
转换为数组:Collection
和Collection
。后者允许客户端提供预期类型的数组。这为Object[] toArray()
在运行时提供了足够的类型信息,以创建和返回预期(相同)类型<T> T[] toArray(T[] a)
的数组。例如,如果我们查看Collection
T
我们看到该方法能够使用反射创建类型为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
,&#34;您是什么类型的?&#34 )。如果我们无法提供T
参数,则必须使用a
方法,该方法只能创建T[]
(再次来自Object[] toArray()
源代码):
Object[]
AbstractCollection
使用的解决方案对于您的情况来说是合理的,但有一些非常重要的差异使其成为一个糟糕的解决方案。在public Object[] toArray() {
Object[] r = new Object[size()];
// ...
}
情况下使用反射是可以接受的,因为数组的创建是Java中的标准化过程(因为数组不是用户定义的类,而是标准化的类,很像toArray(T[])
)。因此,构建过程(例如供应的参数)是已知的先验和标准化的。在对类型调用静态方法的情况下,我们不知道静态方法实际上将存在于所提供的类型中(即,没有相当于实现接口以确保静态方法存在方法)。
相反,最常见的约定是提供一个函数,该函数可用于将请求的参数(在本例中为toArray(T[])
)映射到String
对象。例如,ResultSet
方法的签名将变为:
T
getParsableDTOs
参数只是public <T extends ParsableDTO<T>> List<T> getParsableDTOs(String table, Function<ResultSet, T> mapper) {
/* ... */
}
,这意味着它会消耗mapper
并生成Function<ResultSet, T>
。这是最通用的方式,因为可以使用任何接受ResultSet
个对象并生成T
个对象的Function
。我们也可以为此目的创建一个特定的接口:
ResultSet
并将方法签名更改为以下内容:
T
因此,使用mapper函数获取代码并将非法调用(静态调用@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)); // <--- Map value using our mapper function
}
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;
}
的参数,我们可以使用lambda函数将@FunctionalInterface
映射到getParsableDTOs
,如: / p>
ResultSet
答案 2 :(得分:0)
从<T>
方法中删除parse()
。
它隐藏了接口声明的T
。
答案 3 :(得分:0)
目前,parse()
是ParsableDTO
上的一个实例方法,因此您需要一个T
类型的实例(例如dto_class
的实例)来访问该方法。例如:
T t = dto_class.newInstance();
rtn_lst.add(t.parse(rs));
我认为它也是一个实例方法 - 如果它们是静态的,你就不能在ParsableDTO
的子类上调用该方法的不同版本。
另外,可能另外,这看起来很奇怪:<T extends ParsableDTO<T>>
。
这表明parse()
将返回扩展ParsableDTO
的实例。如果这不是故意的,那么最好在那里有两种通用类型:
public <T, P extends ParsableDTO<T>> List<T> getParsableDTOs(String table,
Class<P> dto_class) {
...
P p = dto_class.newInstance();
rtn_lst.add(p.parse(rs));
并且同意早先关于接口及其方法有两个<T>
声明的评论。它编译得很好,但暗示parse()
返回的类型可能与T
中声明的ParsableDTO<T>
不同。
答案 4 :(得分:0)
您只需要将 getParsableDTOs 的方法签名更改为使用ParsableDTO<T>
而不是Class<T>
。在你的while循环中做
rtn_lst.add(dto_class.parse(RS));