简体   繁体   中英

How can I create a Functional interface implementation for Fields?

Consider a field weight in class Animal . I want to be able to create a getter and setter functional interface objects for manipulating this field.

class Animal {
  int weight;
}

My current approach is similar to one used for methods:

public static Supplier getter(Object obj, Class<?> cls, Field f) throws Exception {
  boolean isstatic = Modifier.isStatic(f.getModifiers());
  MethodType sSig = MethodType.methodType(f.getType());
  Class<?> dCls = Supplier.class;
  MethodType dSig = MethodType.methodType(Object.class);
  String dMthd = "get";
  MethodType dType = isstatic? MethodType.methodType(dCls) : MethodType.methodType(dCls, cls);
  MethodHandles.Lookup lookup = MethodHandles.lookup();
  MethodHandle fctry = LambdaMetafactory.metafactory(lookup, dMthd, dType, dSig, lookup.unreflectGetter(f), sSig).getTarget();
  fctry = !isstatic && obj!=null? fctry.bindTo(obj) : fctry;
  return (Supplier)fctry.invoke();
}

But this gives the following error:

java.lang.invoke.LambdaConversionException: Unsupported MethodHandle kind: getField x.Animal.weight:()int

UPDATE

I am trying to create a class ObjectMap implementing interface Map , which basically tries to represent an object as a Map , where the object can be of any type. Was currently using Field.get() and Field.set() for manipulating fields in get() and put() methods, and using above mentioned approach to create Supplier and Consumer objects for invoking getter and setter methods. I was wondering if i could merge the two separate methods into one.

Example class which could be used as a Map through ObjectMap :

public class ThisCanBeAnything {
  /* fields */
  public String normalField;
  private int hiddenFiled;
  private String hiddenReadonlyField;

  /* getters and setters */
  public int hiddenField() {
    return hiddenField;
  }
  public void hiddenField(int v) {
    System.out.println("set: hiddenField="+v);
    hiddenField = v;
  }

  public String hiddenReadonlyField() {
    return hiddenReadonlyField;
  }
}

And here is the expected usage:

Object o = new ThisCanBeAnything();
Map m = new ObjectMap(o);
m.put("normalField", "Normal");
System.out.println(m.get("normalField")); // Normal
m.put("hiddenField", 1); // set: hiddenField=1
System.out.println(m.get("hiddenField")); // 1
m.put("hiddenReadonlyField", 1); // does not do anything
System.out.println(m.get("hiddenReadonlyField")); // null

You are making it too difficult that it needs to be. When you have a Field , you can directly invoke unreflectGetter on the lookup factory to retrieve a MethodHandle :

Produces a method handle giving read access to a reflected field. The type of the method handle will have a return type of the field's value type. If the field is static, the method handle will take no arguments. Otherwise, its single argument will be the instance containing the field.

public static Supplier<Object> getter(Object obj, Class<?> cls, Field f) {
    f.setAccessible(true);
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    return () -> {
        try {
            MethodHandle handle = lookup.unreflectGetter(f);
            return Modifier.isStatic(f.getModifiers()) ? handle.invoke() : handle.invoke(obj);
        } catch (Throwable t) {
            throw new IllegalArgumentException(t);
        }
    };
}

This returns a supplier of the value of the field. Depending on the accessibility of the field, you might need to invoke setAccessible(true) .

Note that method handles and the reflection API also differs in terms of performance and might be faster.

You can directly write the lambda, you don't need the LambdaMetafactory at all:

public static Supplier getter(Object obj, Field f) {
    return () -> {
        try {
            return f.get(obj);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    };
}

Or a runtime-typesafe version:

public static <T> Supplier<T> getter(Object obj, Class<T> fieldClass, Field f) {

    if (!fieldClass.isAssignableFrom(f.getType()))
        throw new RuntimeException("Field is not of expected type");

    return () -> {
        try {
            return (T) f.get(obj);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    };
}

eg:

private class X {
    public int a;
}

@Test
public void supplier_getter_test() throws NoSuchFieldException {
    X a = new X();
    a.a = 5;

    Supplier<Integer> sup = getter(a, int.class, X.class.getField("a"));

    assertEquals(5, sup.get().intValue());
}

Functional style lets you think about such things in new ways. Instead of a reflection-based approach like

Supplier getter(Object obj, Class<?> cls, Field f){...}

try something like

static <O,F> Supplier<F> getter(O obj, Function<O,F> extractor) {
    return () -> extractor.apply(obj);
}

which you would invoke like

Supplier<Integer> getWeight = getter(animal, a -> a.weight);
Integer weight = getWeight.get();

Is a -> a.weight any harder than coming up with a Field via reflection?

One advantage is that you could use fields or methods as needed, eg, if you added a getter for weight,

Supplier<Integer> getWeight = getter(animal, Animal::getWeight);

A similar setter factory might be

static <O,F> Consumer<F> setter(O obj, BiConsumer<O,F> modifier) {
    return field -> modifier.accept(obj,field);
}

Invoked like this

Consumer<Integer> setWeight = setter(animal, (a, w) -> a.weight = w);
setWeight.accept(90);

You can't bind a MethodHandle bearing a direct access to a field to a function interface instance, but you can bind the accessor method of the Field instance:

public static Supplier getter(Object obj, Class<?> cls, Field f) throws Throwable {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle get=lookup.findVirtual(Field.class,"get",MethodType.genericMethodType(1));
    MethodHandle fctry = LambdaMetafactory.metafactory(lookup, "get",
        get.type().changeReturnType(Supplier.class), MethodType.genericMethodType(0),
        get, MethodType.genericMethodType(0)).getTarget();
    return (Supplier)fctry.invoke(f, Modifier.isStatic(f.getModifiers())? null: obj);
}

Though in this specific example you may consider generating an IntSupplier instead:

public static IntSupplier getter(Object obj, Class<?> cls, Field f) throws Throwable {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle get=lookup.findVirtual(Field.class, "getInt",
            MethodType.methodType(int.class, Object.class));
    MethodHandle fctry = LambdaMetafactory.metafactory(lookup, "getAsInt",
        get.type().changeReturnType(IntSupplier.class), MethodType.methodType(int.class),
        get, MethodType.methodType(int.class)).getTarget();
    return (IntSupplier)fctry.invoke(f, Modifier.isStatic(f.getModifiers())? null: obj);
}

final Animal animal = new Animal();
IntSupplier s=getter(animal, Animal.class, Animal.class.getDeclaredField("weight"));
animal.weight=42;
System.out.println(s.getAsInt());

I know it's a late answer, but I have developed a library that you can use to turn any MethodHandle into a lambda function. The performance is the same as if you would manually implement the function with direct access.

The impl is based around the fact that static final MethodHandle s are being inlined to point of being as fast as direct access. More info on this can be found here: How can I improve performance of Field.set (perhap using MethodHandles)?

The library can be found here: https://github.com/LanternPowered/Lmbda . For now you will have to use Jitpack to access it (small library so it won't take long to compile): https://jitpack.io/#LanternPowered/Lmbda

An example for setting a field on a object:

import org.lanternpowered.lmbda.LmbdaFactory;
import org.lanternpowered.lmbda.LmbdaType;
import org.lanternpowered.lmbda.MethodHandlesX;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.function.ObjIntConsumer;

public class LambdaSetterTest {

    public static void main(String... args) throws Exception {
        final MethodHandles.Lookup lookup = MethodHandlesX.privateLookupIn(TestObject.class, MethodHandles.lookup());
        final MethodHandle methodHandle = lookup.findSetter(TestObject.class, "data", int.class);

        final ObjIntConsumer<TestObject> setter = LmbdaFactory.create(new LmbdaType<ObjIntConsumer<TestObject>>() {}, methodHandle);

        final TestObject object = new TestObject();
        System.out.println(100 == object.getData());
        setter.accept(object, 10000);
        System.out.println(10000 == object.getData());
    }

    public static class TestObject {

        private int data = 100;

        int getData() {
            return this.data;
        }
    }
}

And getting a field from a object:

import org.lanternpowered.lmbda.LmbdaFactory;
import org.lanternpowered.lmbda.LmbdaType;
import org.lanternpowered.lmbda.MethodHandlesX;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.function.ToIntFunction;

public class LambdaSetterTest {

    public static void main(String... args) throws Exception {
        final MethodHandles.Lookup lookup = MethodHandlesX.privateLookupIn(TestObject.class, MethodHandles.lookup());
        final MethodHandle methodHandle = lookup.findGetter(TestObject.class, "data", int.class);

        final ToIntFunction<TestObject> getter = LmbdaFactory.create(new LmbdaType<ToIntFunction<TestObject>>() {}, methodHandle);

        final TestObject object = new TestObject();
        System.out.println(100 == getter.applyAsInt(object));
        object.setData(10000);
        System.out.println(10000 == getter.applyAsInt(object));
    }

    public static class TestObject {

        private int data = 100;

        void setData(int value) {
            this.data = value;
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM