Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create DEEP immutable object in runtime

I need to create immutable copy of an object in runtime with Java. I made use of org.springframework.cglib.beans.ImmutableBean, which can create immutable copy of an object using CGLIB.

But the problem is that it provides "first-level" immutability: it disallows change of an input object's properties, but it allows to change inner objects (e.g. get collection and add an element to it or get inner object and modify it's parameters etc.)

So the question is: what's the correct way of creating deep (recursive) immutable copy of an object so that one can't change inner objects also (at any level of nesting)?

like image 924
Dmytro Titov Avatar asked Mar 11 '16 15:03

Dmytro Titov


People also ask

How can we make an object immutable in JavaScript?

You can freeze (make immutable) an object using the function Object. freeze(obj) . The object passed to the freeze method will become immutable. The freeze() method also returns the same object.

How do you make an object immutable in TypeScript?

Immutability using the readonly keyword. The const keyword is a JavaScript keyword which means you can make a variable immutable natively. However, TypeScript provides a readonly keyword that can be used as a compile-time check to avoid mutation of object properties, class properties, array, etc.

Can we create custom immutable object in Java?

In Java, when we create an object of an immutable class, we cannot change its value. For example, String is an immutable class. Hence, we cannot change the content of a string once created. Besides, we can also create our own custom immutable classes.


1 Answers

You may traverse the object tree and use CGLIB to make each object immutable by using interceptor which skips required methods. The tough part though is to determine all methods which modify the object's state - for each object in the tree.

package ut.test;

import static org.junit.Assert.assertEquals;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

import com.google.common.collect.Lists;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class MyTest {

    public static class Inner {
        private String data = "hello";

        public Inner() {}

        public String getData() {
            return data;
        }

        public void setData(String data) {
            this.data = data;
        }

        @Override
        public String toString() {
            return data;
        }

    }

    public static class Outer {
        private List<Inner> list = Lists.newArrayList(new Inner());

        public Outer() {}

        public List<Inner> getList() {
            return list;
        }

        public void setList(List<Inner> list) {
            this.list = list;
        }
    }

    public static class GetOnlyDelegatingMethodInterceptor implements MethodInterceptor {
        private Object delegate;

        public GetOnlyDelegatingMethodInterceptor(Object delegate) {
            this.delegate = delegate;
        }

        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            if (method.getName().startsWith("get")) {
                return makeImmutable(proxy.invoke(delegate, args));
            }

            if (method.getName().equals("toString")) {
                return proxy.invoke(delegate, args);
            }

            if (method.getDeclaringClass().equals(Object.class)) {
                return proxy.invoke(delegate, args);
            }

            // you may check for other methods here

            // skip all others
            return null;
        }
    }

    private static Object makeImmutable(Object obj) {
        if (obj == null) {
            return obj;
        }

        Enhancer e = new Enhancer();
        e.setSuperclass(obj.getClass());
        e.setCallback(new GetOnlyDelegatingMethodInterceptor(obj));
        return e.create();
    }

    @Test
    public void testImmutable() {
        Outer outerImmutable = (Outer) makeImmutable(new Outer());

        // this is initial state
        assertEquals(outerImmutable.getList().toString(), "[hello]");

        // trying to set empty list
        outerImmutable.setList(new ArrayList<>());
        // but it's still the same
        assertEquals(outerImmutable.getList().toString(), "[hello]");

        // going deeper
        outerImmutable.getList().get(0).setData("bye!");
        // but still no changes
        assertEquals(outerImmutable.getList().toString(), "[hello]");
    }
}
like image 63
Alex Avatar answered Sep 22 '22 17:09

Alex