简体   繁体   English

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

[英]How to implements a method of a generic interface?

I have this interface: 我有这个界面:

public interface ParsableDTO<T> {
    public <T> T parse(ResultSet rs) throws SQLException;
}

Implemented in some kind of dto classes and this method in another class: 在某种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;
}

How can I access the method parse(ResultSet rs) of the interface that can parse a specific T ? 如何访问可以解析特定T的接口的方法parse(ResultSet rs) Is there a working, different and/or better method to do that? 有没有一种工作,不同和/或更好的方法来做到这一点?

You are trying to call a non static method on a generic, which is erased when compiled. 您正在尝试在泛型上调用非静态方法,该方法在编译时会被擦除。 Even if the method was static, there is no way the compiler would allow that (because T is ParseableDTO in that case, and never the concrete implementation). 即使该方法是静态的,编译器也无法允许(因为在这种情况下T是ParseableDTO ,而从不具体实现)。

Instead, assuming you are in Java 8, I would do: 相反,假设您使用的是Java 8,我会这样做:

@FunctionalInterface
public interface RowMapper<T> {
    T mapRow(ResultSet rs) throws SQLException;
}

And then: 然后:

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

The interface RowMapper is derived from existing framework, such as JDBC Template . RowMapper接口派生自现有框架,例如JDBC Template

The idea is to separate concerns: the DTO is not polluted by JDBC related method (eg: mapping or parsing, but I suggest you to avoid the parse name as you are not really parsing a SQL ResultSet here), and you may even leave the mapping in the DAO (lambda make it easier to implements). 想法是分开关注点:DTO不受JDBC相关方法的污染(例如:映射或解析,但我建议你避免使用parse名称,因为你实际上并没有在这里解析SQL ResultSet ),你甚至可能会离开在DAO中映射(lambda使其更容易实现)。

Polluting the DTO with JDBC may be problematic because the client/caller will probably not have a valid ResultSet to pass to the parse . 使用JDBC对DTO进行污染可能会有问题,因为客户端/调用者可能没有有效的ResultSet传递给parse Worse: in newer JDK (9++), the ResultSet interface is in the java.sql module which may be unavailable (if you think about a web service, the client does not need JDBC at all). 更糟糕的是:在较新的JDK(9 ++)中, ResultSet接口位于java.sql模块中,该模块可能不可用(如果考虑Web服务,客户端根本不需要JDBC)。

On a side note, from Java 7 onward, you may use try-with-resource with the ResultSet to automatically close it in a safer way: in your implementation, you are only closing the ResultSet if there was no errors. 另外,从Java 7开始,您可以使用带有ResultSet try-with-resource以更安全的方式自动关闭它:在您的实现中,如果没有错误,您只关闭ResultSet

If you are stuck with Java 6, you should use the following idiom: 如果你遇到Java 6,你应该使用以下习语:

   ResultSet rs = null;
   try {
     rs = ...; // obtain rs
     // do whatever
   } finally {
     if (null != rs) {rs.close();}
   }

The inability to call a static method on the generic type T is a side-effect of type erasure . 无法在泛型类型T上调用静态方法是类型擦除的副作用。 Type erasure means that generic type information is removed--or erased--from Java bytecode after compilation. 类型擦除意味着在编译后从Java字节码中删除或擦除泛型类型信息。 This process is performed in order to maintain backward compatibility written with code prior to Java 5 (in which generics were introduced). 执行此过程是为了保持在Java 5之前用代码编写的向后兼容性(其中引入了泛型)。 Originally, many of the generic types we use in Java 5 and higher were simple classes. 最初,我们在Java 5及更高版本中使用的许多泛型类型都是简单类。 For example, a List was just a normal class that held Object instances and required explicit casting to ensure type-safety: 例如, List只是一个普通类,它包含Object实例并需要显式转换以确保类型安全:

List myList = new List();
myList.add(new Foo());
Foo foo = (Foo) myList.get(0);

Once generics were introduced in Java 5, many of these classes were upgraded to generic classes. 一旦在Java 5中引入了泛型,许多这些类就升级为泛型类。 For example, a List now became List<T> , where T is the type of the elements in the list. 例如, List现在变为List<T> ,其中T是列表中元素的类型。 This allowed the compiler to perform static (compile-time) type checking and removed the need to perform explicit casting. 这允许编译器执行静态(编译时)类型检查,并且不需要执行显式转换。 For example, the above snippet is reduced to the following using generics: 例如,使用泛型将上面的代码段简化为以下内容:

List<Foo> myList = new List<Foo>();
myList.add(new Foo());
Foo foo = myList.get(0);

There are two major benefits to this generic approach: (1) tedious and unruly casting is removed and (2) the compiler can ensure at compile-time that we do not mix types or perform unsafe operations. 这种通用方法有两个主要的好处:(1)删除了繁琐和不守规矩的转换;(2)编译器可以在编译时确保我们不混合类型或执行不安全的操作。 For example, the following would be illegal and would cause an error during compilation: 例如,以下内容是非法的,并且在编译期间会导致错误:

List<Foo> myList = new List<Foo>();
myList.add(new Bar());  // Illegal: cannot use Bar where Foo is expected

Although generics help a great deal with type safety, their inclusion into Java risked breaking existing code. 尽管泛型有助于提高类型安全性,但将它们包含在Java中可能会破坏现有代码。 For example, it should still be valid to create a List object without any generic type information (this is called using it as a raw type). 例如,创建一个没有任何泛型类型信息的List对象仍然有效(这称为使用它作为原始类型)。 Therefore, compiled generic Java code must still be equivalent to non-generic code. 因此,编译的通用Java代码仍必须等同于非通用代码。 Stated another way, the introduction of generics should not affect the bytecode generated by the compiler since this would break existing, non-generic code. 换句话说,泛型的引入不应该影响编译器生成的字节码,因为这会破坏现有的非泛型代码。

Thus, the decision was made to only deal with generics at and before compile time. 因此,决定只在编译时和编译时处理泛型。 This means that the compiler uses generic type information to ensure type safety, but once the Java source code is compiled, this generic type information is removed. 这意味着编译器使用泛型类型信息来确保类型安全,但是一旦编译了Java源代码,就会删除此泛型类型信息。 This can be verified if we look at the generated bytecode of the method in your question. 如果我们在您的问题中查看方法的生成字节码,则可以验证这一点。 For example, suppose we put that method in a class called Parser and simplify that method to the following: 例如,假设我们将该方法放在一个名为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;
    }
}

If we compile this class and inspect its bytecode using javap -c Parser.class , we see the following: 如果我们使用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
}

The line 14: invokeinterface #21, 2 denotes that we have called add on List using an Object argument even though the actual type of the argument in our source code is T . 14: invokeinterface #21, 214: invokeinterface #21, 2表示我们使用Object参数调用了add on List即使源代码中实际的参数类型是T Since generics cannot affect the bytecode generated by the compiler, the compiler replaces generic types with Object (this makes the generic type T non-reifiable ) and then, if needed, performs a cast back to the expected type of the object. 由于泛型不能影响编译器生成的字节码,因此编译器用Object替换泛型类型(这使泛型类型T 不可重新生成 ),然后,如果需要,执行强制转换回对象的预期类型。 For example, if we compile the following: 例如,如果我们编译以下内容:

public class Parser {

    public void doSomething() {
        List<Foo> foos = new ArrayList<>();
        foos.add(new Foo());
        Foo myFoo = foos.get(0);
    }
}

we get the following bytecode: 我们得到以下字节码:

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
}

The line 29: checkcast #18 shows that the compiler added an instruction to check that the Object that we received from the List (using get(0) ) can be cast to Foo . 29: checkcast #1829: checkcast #18显示编译器添加了一条指令,用于检查我们从List接收的Object (使用get(0) )是否可以转换为Foo In other words, that the Object we received from the List is actually a Foo at runtime. 换句话说,我们从List收到的Object实际上是运行时的Foo

So how does this factor into your question? 那么这个因素如何影响你的问题呢? Making a call such as T.parse(rs) is invalid in Java because the compiler has no way of knowing at runtime on what class to call the static method parse since the generic type information is lost at runtime. 调用诸如T.parse(rs)类的调用在Java中是无效的,因为编译器无法在运行时知道调用静态方法parse类,因为泛型类型信息在运行时丢失。 This also restricts us from creating objects of type T (ie new T(); ) as well. 这也限制了我们创建类型为T对象(即new T(); )。

This conundrum is so common that it is actually found in the Java libraries themselves. 这个难题非常普遍,实际上它存在于Java库本身中。 For example, every Collection object has two methods to convert a Collection into an array: Object[] toArray() and <T> T[] toArray(T[] a) . 例如,每个Collection对象都有两种方法将Collection转换为数组: Object[] toArray()<T> T[] toArray(T[] a) The latter allows the client to supply an array of the expected type. 后者允许客户端提供预期类型的​​数组。 This provides the Collection with enough type information at runtime to create and return an array of the expected (same) type T . 这为Collection在运行时提供了足够的类型信息,以创建和返回预期(相同)类型T的数组。 For example, if we look at the JDK 9 source code for AbstractCollection 例如,如果我们查看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);
    // ...
}

we see that the method is able to create a new array of type T using reflection, but this requires using the object a . 我们看到该方法能够使用反射创建一个T类型的新数组,但这需要使用对象a In essence, a is supplied so that the method can ascertain the actual type of T at runtime (the object a is asked, "What type are you?"). 从本质上讲, a提供如此,该方法能确定的实际类型的T在运行时(对象a是问,“什么类型是你吗?”)。 If we cannot provide a T[] argument, the Object[] toArray() method must be used, which is only able to create an Object[] (again from the AbstractCollection source code): 如果我们不能提供T[]参数,则必须使用Object[] toArray()方法,该方法只能创建一个Object[] (同样来自AbstractCollection源代码):

public Object[] toArray() {
    Object[] r = new Object[size()];
    // ...
}

The solution used by toArray(T[]) is a plausible one for your situation, but there are some very important differences that make it a poor solution. toArray(T[])使用的解决方案对于您的情况来说是合理的,但是有一些非常重要的差异使得它成为一个糟糕的解决方案。 Using reflection is acceptable in the toArray(T[]) case because the creation of an array is a standardized process in Java (since arrays are not user-defined classes, but rather, standardized classes, much like String ). toArray(T[])情况下使用反射是可以接受的,因为数组的创建是Java中的标准化过程(因为数组不是用户定义的类,而是标准化类,非常类似于String )。 Therefore, the construction process (such as which arguments to supply) is known a priori and standardized. 因此,构造过程(例如供应的参数)是先验和标准化的。 In the case of calling a static method on a type, we do not know that the static method will, in fact, be present for the supplied type (ie there is no equivalent of an implementing an interface to ensure a method is present for static methods). 在对类型调用静态方法的情况下,我们不知道静态方法实际上将存在于所提供的类型中(即,没有相当于实现接口以确保静态方法存在方法)。

Instead, the most common convention is to supply a function that can be used to map the requested argument ( ResultSet in this case) to a T object. 相反,最常见的约定是提供一个函数,该函数可用于将请求的参数(在本例中为ResultSet )映射到T对象。 For example, the signature for your getParsableDTOs method would become: 例如, getParsableDTOs方法的签名将变为:

public <T extends ParsableDTO<T>> List<T> getParsableDTOs(String table, Function<ResultSet, T> mapper) {
    /* ... */
}

The mapper argument is simply a Function<ResultSet, T> , which means that it consumes a ResultSet and produces a T . mapper参数只是一个Function<ResultSet, T> ,这意味着它使用ResultSet并生成一个T This is the most generalized manner, since any Function that accepts ResultSet objects and produces T objects can be used. 这是最通用的方式,因为可以使用任何接受ResultSet对象并生成T对象的Function We could also create a specific interface for this purpose as well: 我们还可以为此目的创建一个特定的接口:

@FunctionalInterface
public interface RowMapper<T> {
    public T mapRow(ResultSet rs);
}

and change the method signature to the following: 并将方法签名更改为以下内容:

public <T extends ParsableDTO<T>> List<T> getParsableDTOs(String table, RowMapper<T> mapper) {
    /* ... */
}

Thus, taking your code and replacing the illegal call (the static call to T ) with the mapper function, we end up with: 因此,使用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;
}

Additionally, because we used a @FunctionalInterface as a parameter to getParsableDTOs , we can use a lambda function to map the ResultSet to a T , as in: 另外,因为我们使用@FunctionalInterface作为getParsableDTOs的参数,我们可以使用lambda函数将ResultSet映射到T ,如下所示:

Parser parser = new Parser();
parser.getParsableDTOs("FOO_TABLE", rs -> { return new Foo(); });

Remove <T> from the parse() method. parse()方法中删除<T> It's hiding the T declared by the interface. 它隐藏了接口声明的T

As it stands, parse() is an instance method on the ParsableDTO so you will need an instance of type T (eg of the dto_class ) to access the method. 就目前而言, parse()ParsableDTO上的一个实例方法,因此您需要一个类型为T的实例(例如dto_class )才能访问该方法。 For example: 例如:

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

I think it is right as an instance method too - you wouldn't be able to call different versions of the method on the subclasses of ParsableDTO if they were static. 我认为它也是一个实例方法 - 如果它们是静态的,你将无法在ParsableDTO的子类上调用该方法的不同版本。


Also, possibly as an aside, this looks curious: <T extends ParsableDTO<T>> . 另外,可能另外,这看起来好奇: <T extends ParsableDTO<T>>

This is suggesting that parse() will be returning instances that extend ParsableDTO . 这表明parse()将返回扩展ParsableDTO实例。 If that's not intentional, it may be best to have two generic types there: 如果这不是故意的,那么最好有两种通用类型:

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

And agree with the earlier comments about there being two <T> declarations on the interface and its method. 并且同意之前关于在接口及其方法上有两个<T>声明的注释。 It compiles fine, but hints that the type returned by parse() could be different from the T declared in ParsableDTO<T> . 它编译得很好,但暗示parse()返回的类型可能与ParsableDTO<T>声明的T不同。

You just need to change the method signature of getParsableDTOs to use ParsableDTO<T> instead of Class<T> . 您只需要更改getParsableDTOs的方法签名以使用ParsableDTO<T>而不是Class<T> Inside your while loop do 在你的while循环里面做

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM