Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to combine two types to create another TypeLiteral?

Given this method signature, is it possible to implement it? If so, how?

TypeLiteral<MyClass<T, R>> combineTypes(Class<T> typeOne, Class<R> typeTwo) {
  // Awesome implementation here
}

Background

I have an interface, ApiHandler<TReq, TRes> which I am trying to create in a factory, with Guice, given type TReq. I can also pass in the type TRes if necessary. Unfortunately, the TReq and TRes have no meaningful parent classes or interfaces for reasons beyond my control (they are generated from Apache Thrift).

like image 582
MrPiao Avatar asked Sep 13 '25 01:09

MrPiao


2 Answers

If you're computing it based on runtime arguments, it's no longer a literal: It's just a Type.

Though you could use Guava's equivalent framework of TypeToken and its utilities, there's a Type utility class built into Guice: com.google.inject.util.Types. Use newParameterizedType(Type rawType, Type... typeArguments) to create the Type you want, noting that ParameterizedType and Class both implement Type.

static ParameterizedType combineTypes(Class<?> typeOne, Class<?> typeTwo) {
  return Types.newParameterizedType(MyClass.class, typeOne, typeTwo);
}

Unfortunately, AbstractModule.bind and LinkedBindingBuilder.to don't offer overloads for Type; just Class, TypeLiteral, and Key. Luckily, you can generate a Key reflectively using a Type, using Key.get(Type):

bind(Key.get(combineTypes(Foo.class, Bar.class))).to(MyClassFooBar.class);

Note, in this, that ParameterizedType is not itself a parameterized type. This defeats some of the clever generics-based protection that Guice's bind EDSL offers. To get the above to work, you may need to @SuppressWarnings, return the raw type Key, or consider having combineTypes return a Key<MyClass<T, R>> (which would require a cast from Key.get(Type)'s return value Key<?>). If you really must use a TypeLiteral, you can produce a TypeLiteral through Key.getTypeLiteral, but that would also require a cast from TypeLiteral<?>—and would not be a "type literal" by any meaningful definition.

like image 140
Jeff Bowman Avatar answered Sep 15 '25 14:09

Jeff Bowman


TypeLiteral has two constructors; the normal one when used as intended TypeLiteral<MyClass<Foo, Bar>>() {}, and the unsafe one which is not generic and is created directly: new TypeLiteral(type). Looking at the code for Guice's TypeLiteral here, we see the regular constructor uses parameterized.getActualTypeArguments()[0] for this parameter, where parameterized is a ParameterizedType. To make something that matches MyClass<R, T>, this would be another ParameterizedType, but with two parameters, not one, and each parameter would be a plain class. You can now create an implementation of the ParameterizedType interface that fulfills the contract needed, construct a TypeLiteral, and cast it to the correct return value (it won't have the correct generic type, you'll have to do an unsafe cast).

It looks like this:

TypeLiteral<MyClass<T, R>> combineTypes(Class<T> typeOne, Class<R> typeTwo) {
    return new TypeLiteral(new ParameterizedType() {
        @RecentlyNonNull
        Type[] getActualTypeArguments() {
            return new Type[] {typeOne, typeTwo};
        }

        @RecentlyNonNull
        Type getRawType() {
            return MyClass.class;
        }

        Type getOwnerType() {
            // this is only needed for nested classes, eg. if this was Foo.Myclass this would return Foo.class.
            return null;
        }

    });
}
like image 45
Logan Pickup Avatar answered Sep 15 '25 15:09

Logan Pickup