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

10

我有这个接口:

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;
}

如何访问可以解析特定 T 的接口中的方法 parse(ResultSet rs)?有没有其他可行、不同或更好的方法?


4
为了做到这一点,您需要拥有一个“可解析的DTO”的实例。 - Thiyagu
8
请移除 parse() 方法中的 <T>。它隐藏了接口中声明的 T - shmosel
5个回答

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”这个名称,因为你在这里并没有真正解析SQLResultSet,而且你甚至可以将映射留在DAO中(lambda使其更容易实现)。
用JDBC污染DTO可能会有问题,因为客户/调用者可能没有有效的ResultSet可传递给parse。更糟糕的是,在较新的JDK(9++)中,ResultSet接口在java.sql模块中可能不可用(如果您考虑一个Web服务,客户端根本不需要JDBC) 。
另外一方面,从Java 7开始,您可以使用try-with-resourceResultSet自动关闭它以更安全的方式:在您的实现中,只有在没有错误时才会关闭ResultSet
如果你被困在Java 6中,你应该使用以下习惯用语:
   ResultSet rs = null;
   try {
     rs = ...; // obtain rs
     // do whatever
   } finally {
     if (null != rs) {rs.close();}
   }

1
无法在泛型类型 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                  // 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 表示我们在使用 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 #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指示编译器添加了一条指令来检查我们从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)); // <--- 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;
}

此外,由于我们将@FunctionalInterface用作getParsableDTOs的参数,因此可以使用lambda函数将ResultSet映射到T,如下所示:
Parser parser = new Parser();
parser.getParsableDTOs("FOO_TABLE", rs -> { return new Foo(); });

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()返回的类型可能与ParsableDTO<T>中声明的T不同。


除非真的别无选择,否则应该始终避免使用反射。 - Valentin Ruano

0

parse()方法中删除<T>。它隐藏了接口声明的T


1
不要解决问题,由于某些原因,IDE告诉我无法在while中访问ParsableDTO<T>中的方法T。因为要使用该方法,必须实例化T,但我无法实例化对象,我希望接口中的方法创建一个相同类型的对象...如果我将该方法设置为静态,则无法从泛型类型“ParsableDTO”调用“ParsableDTO<T>”的方法,我必须从“ParsableDTO”调用它,这是错误的,因为该方法不是T的特定方法,而是什么都不做的接口方法。(PS:这里的<T>是因为我尝试以静态方式使用该方法) - user4789408
不需要为此声明新的函数式接口。您可以在java.util.function.*中使用Function<ResultSet, T> - Valentin Ruano
@user4789408,你在这里尝试做什么...在Java中获取在接口中声明的工厂/构造方法根本行不通,或者说这不是Java的方式。也许在其他语言中可以,但在这种情况下不行。此回复中提出的解决方案是解决此问题的最佳或Java最佳实践。 - Valentin Ruano

0

你只需要更改 getParsableDTOs 方法签名,使用 ParsableDTO<T> 而不是 Class<T>。在 while 循环内部执行以下操作:

rtn_lst.add(dto_class.parse(rs));


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接