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:
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
? Is there a working, different and/or better method to do that?
Generic MethodsAll generic method declarations have a type parameter section delimited by angle brackets (< and >) that precedes the method's return type ( < E > in the next example). Each type parameter section contains one or more type parameters separated by commas.
We can subtype a generic class or interface by extending or implementing it.
Yes, it is mandatory to implement all the methods in a class that implements an interface until and unless that class is declared as an abstract class. Implement every method defined by the interface.
In order to call an interface method from a java program, the program must instantiate the interface implementation program. A method can then be called using the implementation object.
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).
Instead, assuming you are in Java 8, I would do:
@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.
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).
Polluting the DTO with JDBC may be problematic because the client/caller will probably not have a valid ResultSet
to pass to the 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).
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.
If you are stuck with Java 6, you should use the following idiom:
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. Type erasure means that generic type information is removed--or erased--from Java bytecode after compilation. This process is performed in order to maintain backward compatibility written with code prior to Java 5 (in which generics were introduced). Originally, many of the generic types we use in Java 5 and higher were simple classes. For example, a List
was just a normal class that held Object
instances and required explicit casting to ensure type-safety:
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. For example, a List
now became List<T>
, where T
is the type of the elements in the list. 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. 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. 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). Therefore, compiled generic Java code must still be equivalent to non-generic code. 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. 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:
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:
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
. 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. 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
. In other words, that the Object
we received from the List
is actually a Foo
at runtime.
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. This also restricts us from creating objects of type T
(i.e. new T();
) as well.
This conundrum is so common that it is actually found in the Java libraries themselves. For example, every Collection
object has two methods to convert a Collection
into an array: Object[] toArray()
and <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
. For example, if we look at the JDK 9 source code for AbstractCollection
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
. 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?"). 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):
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. 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
). 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 (i.e. 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. For example, the signature for your getParsableDTOs
method would become:
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
. This is the most generalized manner, since any Function
that accepts ResultSet
objects and produces T
objects can be used. 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:
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:
Parser parser = new Parser();
parser.getParsableDTOs("FOO_TABLE", rs -> { return new Foo(); });
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With