如何实现通用接口的方法?

时间:2018-03-28 18:48:38

标签: java generics methods interface implements

我有这个界面:

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方法?是否有一种可行的,不同的和/或更好的方法来做到这一点?

5 个答案:

答案 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();转换为数组:CollectionCollection。后者允许客户端提供预期类型的​​数组。这为Object[] toArray()在运行时提供了足够的类型信息,以创建和返回预期(相同)类型<T> T[] toArray(T[] a)的数组。例如,如果我们查看Collection

的JDK 9源代码
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));