Java's type erasure generics are generally limited compared to real generics/templates because you don't have access to the actual class object(unless you pass it as runtime parameter) so you can't create new instances of it, create arrays of it, or inspect it. But recently I've encountered a case where type erasure is actually helpful.
The problem
Lets say we have this interface:
public interface Foo {
public Object createValue();
public void handleValue(Object value);
public void printValue(Object value);
}
Implemented by these classes:
import java.util.LinkedList;
import java.util.List;
public class Bar implements Foo {
@Override
public Object createValue() {
List<String> list = new LinkedList<>();
list.add("one");
list.add("two");
list.add("three");
return list;
}
@Override
@SuppressWarnings("unchecked")
public void handleValue(Object value) {
List<String> list = (List<String>) value;
for (int i = 0; i < list.size(); ++i) {
list.set(i, "(" + list.get(i) + ")");
}
}
@Override
@SuppressWarnings("unchecked")
public void printValue(Object value) {
List<String> list = (List<String>) value;
for (String entry : list) {
System.out.printf("- %s\n", entry);
}
}
}
public class Baz implements Foo {
@Override
public Object createValue() {
return new int[]{ 1, 2, 3 };
}
@Override
public void handleValue(Object value) {
int[] array = (int[]) value;
for (int i = 0; i < array.length; ++i) {
array[i] = array[i] * 10;
}
}
@Override
public void printValue(Object value) {
int[] array = (int[]) value;
for (int entry : array) {
System.out.printf("- %s\n", entry);
}
}
}
Foo
is an API for using a value - it can create it, do something with it, and print it. Bar
implements Foo
with a list of strings that can be surrounded by parenthesizes, while Baz
implements it with an array of integers that are multiplied by 10. There is a hidden assumption here that handleValue
and printValue
accept objects of the same type returned by createValue
of the same Foo
object:
public class App {
public static void main(String[] args) {
Foo[] foos = new Foo[]{ new Bar(), new Baz() };
for (Foo foo : foos) {
Object value = foo.createValue();
foo.handleValue(value);
foo.printValue(value);
}
}
}
So - that assumption is easy to fulfill in actual code, but nothing forces it.
Proper OOP practices dictate that handleValue
and printValue
should be methods of the value:
public interface Value {
public void handle();
public void print();
}
public interface Foo {
public Value createValue();
}
Sadly, this is not always possible. Sometimes we don't control the types returned from createValue
. Sometimes we do control them, but they belong in a different module that should not know about these particular operations.
Generics to the rescue?
This is what generic are for, aren't they? Just make Foo
use a generic parameter instead of Object
:
public interface Foo<T> {
public T createValue();
public void handleValue(T value);
public void printValue(T value);
}
import java.util.LinkedList;
import java.util.List;
public class Bar implements Foo<List<String>> {
@Override
public List<String> createValue() {
List<String> list = new LinkedList<>();
list.add("one");
list.add("two");
list.add("three");
return list;
}
@Override
public void handleValue(List<String> value) {
for (int i = 0; i < value.size(); ++i) {
value.set(i, "(" + value.get(i) + ")");
}
}
@Override
public void printValue(List<String> value) {
for (String entry : value) {
System.out.printf("- %s\n", entry);
}
}
}
public class Baz implements Foo<int[]> {
@Override
public int[] createValue() {
return new int[]{ 1, 2, 3 };
}
@Override
public void handleValue(int[] value) {
for (int i = 0; i < value.length; ++i) {
value[i] = value[i] * 10;
}
}
@Override
public void printValue(int[] value) {
for (int entry : value) {
System.out.printf("- %s\n", entry);
}
}
}
So, we solved the problem of having to downcast in handleValue
and printValue
. So now we are typesafe, right?
WRONG!
We did not resolve the type issue - we have simply moved it from the declaration to the usage. When we loop over our array of Foo
s we are still keeping the value from createValue
as Object
, and still passing it as Object
to handleValue
and printValue
.
Java is kind enough to warn us about it:
$ javac App.java -Xlint:unchecked
App.java:6: warning: [unchecked] unchecked call to handleValue(T) as a member of the raw type Foo
foo.handleValue(value);
^
where T is a type-variable:
T extends Object declared in interface Foo
App.java:7: warning: [unchecked] unchecked call to printValue(T) as a member of the raw type Foo
foo.printValue(value);
^
where T is a type-variable:
T extends Object declared in interface Foo
2 warnings
But... what can we do about it?
The solution
public class App {
public static void main(String[] args) {
Foo[] foos = new Foo[]{ new Bar(), new Baz() };
for (Foo<?> foo : foos) {
createHandlePrint(foo);
}
}
private static <T> void createHandlePrint(Foo<T> foo) {
T value = foo.createValue();
foo.handleValue(value);
foo.printValue(value);
}
}
I created an helper method - createHandlePrint
. This method has a generic parameter, but it's caller doesn't pass it - neither explicitly nor implicitly. We only need it inside, to be be able to declare that value
is the same type returned by the createValue
of the foo
passed to us, so it is safe to pass it back to that foo
.
This is only possible thanks to type erasure. With real generics createHandlePrint
would have been a different method for each parametrization of T
, and I wouldn't be able to call them both from the same line of main
. Unless I used reflection - which kind of defeats the purpose of having the compiler verify the type... (and also doesn't work with all implementations of generics)
Top comments (0)