Ensure different types of generics

294 Views Asked by At

I am trying to create a generic converter interface, which will convert T objects to U objects by using the same method name, i.e. convert:

public interface GenericConverter<T, U> {
  T convert(U fromObject);
  U convert(T fromObject);
}

Of course, generics erasure tranforms both methods into the following during compilation:

convert(object fromObject);

So both methods have the same erasure, which results in an error during compilation.

In my example it is logical that I will always use different object types for T and U. Is there a way to keep the same method name (convert), be able to encapsulate the fact that T and U are different types and ensure that the proper method will be called in each case?

3

There are 3 best solutions below

0
On BEST ANSWER

Unless the two types T and U are based in two separate type hierarchies (i.e. each one will always have some distinct superclass), there's no way of having the two methods with same name. It doesn't even make sense semantically in that case - what should be the semantic difference between the two methods if you cannot distinguish the two types in any reasonable matter?

Apart of the suggested renaming of the methods, consider also only having one such method in the interface and instead using a GenericConverter<T, U> and GenericConverter<U, T> wherever you need to transform both ways.

0
On

Problem

When you say,

it is logical that I will always use different object types for T and U

Compiler does not know. Types can be forced to be same, but not to be different (without constraints).


Approach 1

class ConversionSource {}

class ConversionTarget {}

interface GenericConverter<T extends ConversionSource, U extends ConversionTarget> {
    T convert(U obj);
    U convert(T obj);
}

Now, erasures are different. You get the behavior you want with source you want, but usage is severely restricted because of constraints.


Approach 2

interface InvertibleConverter<T, U> {
    U convert(T obj);
    InvertibleConverter<U, T> inverse();
}

class Tokenizer implements InvertibleConverter<String, Stream<String>> {

    @Override
    Stream<String> convert(String obj) {
        return Arrays.stream(obj.split(" "));
    }

    @Override
    InvertibleConverter<Stream<String>, String> inverse() {
        return new InvertibleConverter<Stream<String>, String>() {
            @Override
            public String convert(Stream<String> obj) {
                return obj.collect(Collectors.joining(" "));
            }

            @Override
            public InvertibleConverter<String, Stream<String>> inverse() {
                return Tokenizer.this;
            }
        };
    }
}

Usage can be as follows

InvertibleConverter<String, Stream<String>> splitter = new Tokenizer();
String sentence = "This is a sentence";
Stream<String> words = splitter.convert(sentence);
String sameSentence = splitter.inverse().convert(words);

This approach works even when T and U are identical.

Hope this helps.
Good luck

0
On

It's not directly possible due to type erasure. Several options have already been listed in the other answers. One of them implicitly aimed at separating the conversions. So instead of having a single converter, you could have two distinct ones:

public interface GenericConverter<T, U> {
  U convert(T fromObject);
}

GenericConverter<Integer, String> forward = Converters.integerString();
GenericConverter<String, Integer> backward = Converters.stringInteger();

But note that the GenericConverter interface in this cases is structurally equal to the Function interface - so there is probably no reason to create a new one.

Instead, if you want to have this "forward and backward converter" as some sort of a named entity (with both conversion functions inseparably linked together), you could define an interface for that:

public interface GenericConverter<T, U> {
  Function<T, U> forward();
  Function<U, T> backward();
}

This could be used as follows:

GenericConverter<Integer, String> converter = Converters.integerString();

String string = converter.forward().apply(someInteger);
Integer integer = converter.backward().apply(someString);

Whether or not this is the "best" solution here depends on the intended usage patterns. One advantage could be that, with a generic (!) utility function like this...

private static GenericConverter<T, U> create(
    Function<T, U> forward, Function<U, T> backward) {
    return new GenericConverter() {
        @Override
        public Function<T, U> forward() {
            return forward;
        }
        @Override
        public Function<U, T> backward() {
            return backward;
        }
    }
}

creating a new converter would be easy as pie:

public static GenericConverter<Integer, String> integerString() {
    return create(
        integer -> String.valueOf(integer), 
        string -> Integer.parseInt(string)
    );
}