Safely call setter after getter chain eg foo.getX().getY().setZ(...);

Viewed 681

How do I safely call setter after getter chain eg foo.getX().getY().setZ(...);? For example, suppose I have a nested POJO, and I want to be able to set a field of a nested object.

Foo foo = ...
foo.getX().getY().setZ(...);

I want the behavior to be such that if X and Y do not exist then they are created automatically; otherwise it reuses the existing object.

In other words, I want it to be behave equivalent to

Foo foo = ...
X x = foo.getX();
if (x == null) { 
  x = new X();
  foo.setX(x);
}

Y y = x.getY();
if (y == null) {
  y = newY();
  x.setY(y);
}

y.setZ(...);

I'm wondering if there is a trick out there using reflection/functional that comes close to this.

I also have the following constraints:

  • I cannot modify any of the classes
  • The solution must know about only the public getters and setters, not the private instance variables
  • I want the getter to modify the internal state only when specifically requested; I don't want x = foo.getX() to modify foo.
4 Answers

Use functional programming. Create a method that accepts a getter, a setter and a supplier for the default value, that returns a getter encapsulating the logic you need:

public static <T, U> Function<T, U> getOrSetDefault(
        Function<T, U> getter,
        BiConsumer<T, U> setter,
        Supplier<U> defaultValue) {

    return t -> {
        U u = getter.apply(t);
        if (u == null) {
            u = defaultValue.get();
            setter.accept(t, u);
        }
        return u;
    };
}

Then create these decorated getters:

Function<Foo, X> getX = getOrSetDefault(Foo::getX, Foo::setX, X::new);
Function<X, Y> getY = getOrSetDefault(X::getY, X::setY, Y::new);

Finally, chain them and apply the resulting function passing in your foo instance as an argument:

Foo foo = ...
getX.andThen(getY).apply(foo).setZ(...);

EDIT: This assumes that both X and Y have a no-args constructor that is referenced by X::new and Y::new, respectively. But you could use anything as the Supplier, i.e. an already created instance, or the return value of a method, etc.

TL;DR: Don't try to force functional Java where there clearly is no place for it.


The only way you can do this functionally in Java 8 without modifying any of the classes is using Optionals and their .orElse() method. It gets really long really quick, but it's the only way that actually makes sense using functional if you want to do it in one line only.

Optional.ofNullable(foo.getX()).orElseGet(() -> { foo.setX(new X()); return foo.getX(); }).setY(...);

If foo.setX() also returns the setted value it can be simplified as:

Optional.ofNullable(foo.getX()).orElseGet(() -> foo.setX(new X())).setY(...);

This is the only generic and functional way of doing it that I can think of. Stated the above, you can clearly see that this becomes huge and ugly even for just a chain of two getters, so I wouldn't advise it. I would definitely suggest you to use the classic multi-statement approach if you have to chain more than one call.

Another option, even thought not really that functional, is to use the tristate operator, still only if the setter returns the setted value:

(foo.getX() == null ? foo.setX(new X()) : foo.getX()).setY(...);

This has the probably unwanted side effect of calling the getter twice if the element is found, which you may not like, but could be possibly ignored if the getter caches the value somehow.

To start off I just want to mention that this probably isn't the best solution and I'm sure there are ways to optimize this. That said, I wanted to try my hand at CGLIB and ObjenesisHelper again.

Using CGLIB and ObjenesisHelper we can wrap the data object in a proxy which will intercept the get methods. Using this interceptor we can add the logic you described in your post. Lets start off by assume these are our data types (using lombok for brevity).

@Data class W { private X x; }
@Data class X { private Y y; }
@Data class Y { private Z z; }
@Data class Z { private int alpha; }

Our final solution can be used like the following:

public static void main(String[] args) {
    final W w = ProxyUtil.withLazyDefaults(new W());
    System.out.println(w.getX().getY().getZ().getAlpha());
}

Implementation

Currently, if we try to invoke new W().getX().getY().getZ().getAlpha() we will get a NullPointerException when invoking getY() because getX() returned null. Even if we manage to produce a default X value, we will still need a default Y value to not get a null pointer on getZ() and getAlpha() and so forth. The proxy we create needs to be generic and be able to wrap its sub components recursively.

Okay, so lets start. The first thing we need to do is create a MethodInterceptor. Whenever any call hits our proxy instance it will perform the logic of our MethodInterceptor. We need to first determine if the method called is a getter. If not we will ignore it. During this getter call, if the value is not present in our data we will create it and update the object. If the value contained by the getter is an original unwrapped class, we will replace it with a wraped version. Finally we will return the wrapped instance. Edit I updated this to not inject wrapped instances into the real Data objects. This will be less performant if the object is accessed mutliple times this way

public class ProxyUtil {
    public static <T> T withLazyDefaults(final T data) {
        final MethodInterceptor interceptor = (object, method, args, proxy) -> {
            if (method.getName().startsWith("get")) {
                final Class<?> returnType = method.getReturnType();
                Object response = method.invoke(data, args);
                if (response == null) {
                    response = returnType.newInstance();
                    data.getClass()
                        .getDeclaredMethod(
                            method.getName().replaceFirst("get", "set"),
                            returnType)
                        .invoke(data, response);
                }
                if (!returnType.isPrimitive()) {
                    response = withLazyDefaults(response);
                }
                return response;
            }
            return method.invoke(data, args);
        };
        ...

The rest of this method involves using CGLIB and Objenisis Helper to construct the wrapper instance. CGLib will allow you to proxy both classes and interfaces and ObjenesisHelper will allow you to construct an instance of a class without having to invoke a constructor. See here for a CGLib example and here for a ObjenesisHelper example.

        ...
        final Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(data.getClass());
        final Set<Class<?>> interfaces = new LinkedHashSet<>();
        if (data.getClass().isInterface()) {
            interfaces.add(data.getClass());
        }
        interfaces.addAll(Arrays.asList(data.getClass().getInterfaces()));
        enhancer.setInterfaces(interfaces.toArray(new Class[interfaces.size()]));
        enhancer.setCallbackType(interceptor.getClass());

        final Class<?> proxyClass = enhancer.createClass();
        Enhancer.registerStaticCallbacks(proxyClass, new Callback[]{interceptor});
        return (T) ObjenesisHelper.newInstance(proxyClass);
    }
}

Caveats

  • This is not a thread safe operation.
  • Reflection will slow down your code.
  • Better error handling needs to added for the reflection calls.
  • If a class does not have a no-arg constructor this will not work.
  • Does not account for inheritance of data classes
  • This could be best effort by checking for a no-arg ctor/setter first.

I ended up using a combination of functional and reflection and tried to make the interface similar to Java's Optional. Here is an example of how I would write foo.getX().getY().setZ(val);

MutableGetter.of(foo).map(Foo::getX).map(x::getY).get().setZ(val);

This is the code (It's still WIP). I used reflection to avoid having to pass the setter and constructor

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import lombok.Getter;
import lombok.NonNull;

public class MutableGetter<T>
{
    private T object;

    private MutableGetter(T object)
    {
        this.object = object;
    }

    public static <T> MutableGetter<T> of(@NonNull T object)
    {
        return new MutableGetter<>(object);
    }

    public <U> MutableGetter<U> map(Function<T, U> getter)
    {
        Method getterMethod = getGetterMethod(object.getClass(), getter);
        BiConsumer<T, U> setter = getSetter(getterMethod);
        Supplier<U> defaultValue = getDefaultValue(getterMethod);

        U nextObject = getter.apply(object);
        if (nextObject == null) {
            nextObject = defaultValue.get();
            setter.accept(object, nextObject);
        }

        return new MutableGetter<>(nextObject);
    }

    public T get()
    {
        return object;
    }

    private static <U> Supplier<U> getDefaultValue(Method getterMethod)
    {
        return () -> {
            try {
                Constructor<?> constructor = getterMethod.getReturnType().getConstructor();
                constructor.setAccessible(true);
                return (U) constructor.newInstance();
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        };
    }

    private static <T, U> BiConsumer<T,U> getSetter(Method getterMethod)
    {
        return (obj, arg) -> {
            Method setterMethod = getSetterFromGetter(getterMethod);
            setterMethod.setAccessible(true);

            try {
                setterMethod.invoke(obj, arg);
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        };
    }

    private static Method getSetterFromGetter(Method getter)
    {
        if (!getter.getName().startsWith("get")) {
            throw new IllegalStateException("The getter method must start with 'get'");
        }

        String setterName = getter.getName().replaceFirst("get", "set");

        Method[] methods = getter.getDeclaringClass().getMethods();

        for (Method method: methods) {
            if (method.getName().equals(setterName)) {
                return method;
            }
        }

        throw new IllegalStateException(String.format("Couldn't find setter in class %s with name %s", getter.getDeclaringClass(), setterName));
    }

    private static <T, U> Method getGetterMethod(Class<?> clazz, Function<T, U> getter)
    {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setSuperclass(clazz);

        MethodRecorder methodRecorder = new MethodRecorder();

        T proxy;
        try {
            proxy = (T) proxyFactory.create(new Class<?>[0], new Object[0], methodRecorder);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

        getter.apply(proxy);

        return methodRecorder.getLastInvokedMethod();
    }

    private static class MethodRecorder implements MethodHandler
    {
        @Getter
        private Method lastInvokedMethod;

        @Override
        public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args)
        {
            this.lastInvokedMethod = thisMethod;
            return null; // the result is ignored
        }
    }

}

Let me know if you have any suggestions

Related