DEV Community

Idan Arye
Idan Arye

Posted on

Abusing Type Erasure: Passing Back the Same Subclass the Object Yields

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 Foos 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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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)