【问题标题】:How to implements a method of a generic interface?如何实现泛型接口的方法?
【发布时间】:2018-09-07 14:00:07
【问题描述】:

我有这个界面:

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

【问题讨论】:

  • 你需要有一个ParsableDTO 的实例才能做到这一点
  • parse() 方法中删除&lt;T&gt;。它隐藏了接口声明的T

标签: java generics methods interface implements


【解决方案1】:

您试图在泛型上调用非静态方法,该方法在编译时会被删除。即使方法是静态的,编译器也不会允许这样做(因为在这种情况下 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 ResultSet 此处) ,您甚至可以将映射保留在 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();}
   }

【讨论】:

    【解决方案2】:

    无法在泛型类型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&lt;T&gt;,其中T 是列表中元素的类型。这允许编译器执行静态(编译时)类型检查,并消除了执行显式转换的需要。例如,上面的 sn-p 使用泛型简化为以下内容:

    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 行表示我们使用Object 参数在List 上调用了add,即使我们的源代码中参数的实际类型是T。由于泛型无法影响编译器生成的字节码,因此编译器将泛型类型替换为Object(这使得泛型类型为Tnon-reifiable),然后,如果需要,执行转换回对象的预期类型.例如,如果我们编译如下:

    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()&lt;T&gt; T[] toArray(T[] a)。后者允许客户端提供预期类型的​​数组。这在运行时为Collection 提供了足够的类型信息,以创建并返回预期(相同)类型T 的数组。例如,如果我们查看 AbstractCollection 的 JDK 9 源代码

    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&lt;ResultSet, T&gt;,这意味着它使用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(); });
    

    【讨论】:

      【解决方案3】:

      parse() 方法中删除&lt;T&gt;。 它隐藏了接口声明的T

      【讨论】:

      • 不解决问题,由于某种原因,IDE 告诉我暂时无法到达 T 中 ParsableDTO 中的方法。因为要使用该方法,T必须实例化,但我不能实例化对象,我希望接口中的方法创建一个相同类型的对象......如果我使用静态方法,我不能在泛型类型中调用“ParsableDTO”的方法,我必须从“ParsableDTO”调用它,又错了,因为不是 T 的特定方法,而是接口中什么都不做的方法(PS: 在这里是因为我尝试以静态方式使用该方法)
      • 不需要为此声明一个新的功能接口。你可以在java.util.function.*中使用Function&lt;ResultSet, T&gt;
      • @user4789408 你想在这里做什么......获取在接口中声明的工厂/构造函数方法在 Java 中根本不起作用或者不是 Java 方式。也许它在其他语言中是这样,但不是在这个语言中。此回复中提出的解决方案是关于解决这种情况的最佳或 java 最佳实践。
      【解决方案4】:

      就目前而言,parse()ParsableDTO 上的一个实例方法,因此您需要一个T 类型的实例(例如dto_class)来访问该方法。例如:

      T t = dto_class.newInstance();
      rtn_lst.add(t.parse(rs));
      

      我认为它作为实例方法也是正确的 - 如果它们是静态的,您将无法在 ParsableDTO 的子类上调用不同版本的方法。


      另外,顺便说一句,这看起来很奇怪:&lt;T extends ParsableDTO&lt;T&gt;&gt;

      这表明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));
      

      并且同意早期的 cmets 关于接口及其方法上有两个 &lt;T&gt; 声明。它编译得很好,但暗示parse() 返回的类型可能与ParsableDTO&lt;T&gt; 中声明的T 不同。

      【讨论】:

      • 一个人应该总是从反思中清醒过来,除非真的没有其他选择。
      【解决方案5】:

      您只需将getParsableDTOs 的方法签名更改为使用ParsableDTO&lt;T&gt; 而不是Class&lt;T&gt;。在你的while循环里面做

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-06-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多