LambdaMetaFactory boxing / unboxing parameters and return types

308 Views Asked by At

I've got the following two methods:

  public static <T, R> IGetter<T, R> createGetterViaMethodname( final Class<T> clazz, final String methodName,
                                                                final Class<R> fieldType )
      throws Throwable
  {
    final MethodHandles.Lookup caller = MethodHandles.lookup();
    final MethodType methodType = MethodType.methodType( fieldType );
    final MethodHandle target = caller.findVirtual( clazz, methodName, methodType );
    final MethodType type = target.type();
    final CallSite site = LambdaMetafactory.metafactory(
        caller,
        "get",
        MethodType.methodType( IGetter.class ),
        type.erase(),
        target,
        type );

    final MethodHandle factory = site.getTarget();
    return (IGetter<T, R>) factory.invoke();
  }

  public static <T, I> ISetter<T, I> createSetterViaMethodname( final Class<T> clazz, final String methodName,
                                                                final Class<I> fieldType )
      throws Throwable
  {

    final MethodHandles.Lookup caller = MethodHandles.lookup();
    final MethodType methodType = MethodType.methodType( void.class, fieldType );
    final MethodHandle target = caller.findVirtual( clazz, methodName, methodType );
    final MethodType type = target.type();
    final CallSite site = LambdaMetafactory.metafactory(
        caller,
        "set",
        MethodType.methodType( ISetter.class ),
        type.erase(),
        target,
        type );

    final MethodHandle factory = site.getTarget();
    return (ISetter<T, I>) factory.invoke();
  }

including the following two interfaces:

@FunctionalInterface
public interface IGetter<T, R>
{
  @Nullable
  R get( T object );
}

@FunctionalInterface
public interface ISetter<T, I>
{
  void set( T object, @Nullable I value );
}

This works great for all Class-Types, including the Number-Wrappers for primitive types such as Integer for int. However, if I have a setter that takes an int or a getter that returns an ìnt, it tries passing / returning the Number-Wrapper, resulting in an exception.

What's the correct way to box / unbox this without having to make another method. The reason here is to keep the API clean and simple to use. I am willing to take a small performance hit for the boxing / unboxing here.

2

There are 2 best solutions below

2
On BEST ANSWER

There is no built-in "pretty" way to convert a primitive class to a wrapper class, so you gotta use a map like this:

private final static Map<Class<?>, Class<?>> map = new HashMap<>();
static {
    map.put(boolean.class, Boolean.class);
    map.put(byte.class, Byte.class);
    map.put(short.class, Short.class);
    map.put(char.class, Character.class);
    map.put(int.class, Integer.class);
    map.put(long.class, Long.class);
    map.put(float.class, Float.class);
    map.put(double.class, Double.class);
}

Or use one of the other ways here.

Once you do that, you can just check if fieldType is a primitive. If it is, change the return type/parameter type of the method type by looking up the wrapper type in the map.

For the getter:

MethodType type = target.type();
if (fieldType.isPrimitive()) {
  type = type.changeReturnType(map.get(fieldType));
}

For the setter:

MethodType type = target.type();
if (fieldType.isPrimitive()) {
  type = type.changeParameterType(1, map.get(fieldType));
}

Just in case it wasn't clear, the caller would pass the primitive class for primitive getters and setters:

createSetterViaMethodname(Main.class, "setFoo", int.class)

// for a setter declared like this:

public void setFoo(int i) {
    ...
}

Full code:

public class Main {

  public static void main(String[] args) throws Throwable {
    // this prints 1234567
    createSetterViaMethodname(Main.class, "setFoo", int.class).set(new Main(), 1234567);
  }

  public void setFoo(int i) {
    System.out.println(i);
  }

  public final static Map<Class<?>, Class<?>> map = new HashMap<>();
  static {
    map.put(boolean.class, Boolean.class);
    map.put(byte.class, Byte.class);
    map.put(short.class, Short.class);
    map.put(char.class, Character.class);
    map.put(int.class, Integer.class);
    map.put(long.class, Long.class);
    map.put(float.class, Float.class);
    map.put(double.class, Double.class);
  }

  public static <T, R> IGetter<T, R> createGetterViaMethodname( final Class<T> clazz, final String methodName,
      final Class<R> fieldType )
      throws Throwable
  {
    final MethodHandles.Lookup caller = MethodHandles.lookup();
    final MethodType methodType = MethodType.methodType( fieldType );
    final MethodHandle target = caller.findVirtual( clazz, methodName, methodType );
    MethodType type = target.type();
    if (fieldType.isPrimitive()) {
      type = type.changeReturnType(map.get(fieldType));
    }
    final CallSite site = LambdaMetafactory.metafactory(
        caller,
        "get",
        MethodType.methodType( IGetter.class ),
        type.erase(),
        target,
        type);

    final MethodHandle factory = site.getTarget();
    return (IGetter<T, R>) factory.invoke();
  }

  public static <T, I> ISetter<T, I> createSetterViaMethodname( final Class<T> clazz, final String methodName,
      final Class<I> fieldType )
      throws Throwable
  {

    final MethodHandles.Lookup caller = MethodHandles.lookup();
    final MethodType methodType = MethodType.methodType( void.class, fieldType );
    final MethodHandle target = caller.findVirtual( clazz, methodName, methodType );
    MethodType type = target.type();
    if (fieldType.isPrimitive()) {
      type = type.changeParameterType(1, map.get(fieldType));
    }
    final CallSite site = LambdaMetafactory.metafactory(
        caller,
        "set",
        MethodType.methodType( ISetter.class ),
        type.erase(),
        target,
        type );

    final MethodHandle factory = site.getTarget();
    return (ISetter<T, I>) factory.invoke();
  }
}

@FunctionalInterface
interface IGetter<T, R>
{
  @Nullable
  R get( T object );
}

@FunctionalInterface
interface ISetter<T, I>
{
  void set( T object, @Nullable I value );
}
0
On

An alternative is to use an external library instead of the LambdaMetafactory. The Cojen/Maker library provides direct control over code generation and it performs boxing/unboxing conversions automatically.

This example works just fine even when the field type is an int. The caller must provide the lookup class directly in order to access any non-public methods, but this would also be necessary in the original example. This means that the <T> param isn't used, but this example should suffice.

import org.cojen.maker.ClassMaker;
import org.cojen.maker.MethodMaker;

public class SetterGetterMaker {
    public static <T, R> IGetter<T, R> createGetterViaMethodname(MethodHandles.Lookup lookup,
                                                                 String methodName)
        throws Throwable
    {
        ClassMaker cm = ClassMaker.begin(null, lookup).implement(IGetter.class);
        cm.addConstructor();
        MethodMaker mm = cm.addMethod(Object.class, "get", Object.class).public_();
        mm.return_(mm.param(0).cast(lookup.lookupClass()).invoke(methodName));
        var newLookup = cm.finishHidden();
        var newClass = newLookup.lookupClass();
        var ctorHandle = newLookup.findConstructor(newClass, MethodType.methodType(void.class));
        return (IGetter<T, R>) ctorHandle.invoke();
    }

    public static <T, I> ISetter<T, I> createSetterViaMethodname(MethodHandles.Lookup lookup,
                                                                 String methodName,
                                                                 Class<I> fieldType )
        throws Throwable
    {
        ClassMaker cm = ClassMaker.begin(null, lookup).implement(ISetter.class);
        cm.addConstructor();
        MethodMaker mm = cm.addMethod(void.class, "set", Object.class, Object.class).public_();
        var fieldVar = mm.param(1).cast(fieldType);
        mm.param(0).cast(lookup.lookupClass()).invoke(void.class, methodName, null, fieldVar);
        var newLookup = cm.finishHidden();
        var newClass = newLookup.lookupClass();
        var ctorHandle = newLookup.findConstructor(newClass, MethodType.methodType(void.class));
        return (ISetter<T, I>) ctorHandle.invoke();
    }
}