1
\$\begingroup\$

Commenting omitted to give you the idea. I've been toying with this recently as a variant of the Builder pattern. I've recently fallen in love with immutable objects for the benefits they give in larger concurrent systems.

Questions:

Is there a way to do this without having to instantiate two Objects? (ie. Something.Mutable extends Something but adds setters, while preserving final on the underlying fields once the object gets finalized?

If I have to instantiate two Objects, can I keep final on their somehow and not repeat myself in the contained Builder Object?

public class Something {
    private final int someInteger;
    private final String someString;
    public Something(int someInteger, String someString) {
        this.someInteger = someInteger;
        this.someString = someString;
    }
    public int getSomeInteger() { return someInteger; }
    public String getSomeString() { return someString; }

    public static class Mutable {
        private int someInteger;
        private String someString;
        public Something.Mutable() {}
        public int getSomeInteger() { return someInteger; }
        public String getSomeString() { return someString; }

        public Something.Mutable setSomeInteger(int someInteger) { 
            this.someInteger = someInteger;
            return this;
        }
        public Something.Mutable setSomeString(String someString) { 
            this.someString = someString;
            return this;
        }

        public Something build() {
            return new Something(someInteger, someString);
        }
    }
}

Here is an example of me using it:

Something something = new Something.Mutable()
        .setString("hi")
        .setInteger(42)
        .build();
\$\endgroup\$
  • 1
    \$\begingroup\$ Your code doesn't compile: The inner class must be just Mutable, and you need to rename the finalize method (which is already defined in Object). \$\endgroup\$ – Landei May 6 '12 at 20:56
  • \$\begingroup\$ Thanks for the comment, I clearly typed this up in the editor; I'll update! \$\endgroup\$ – corykendall May 6 '12 at 23:31
1
\$\begingroup\$

If you really want to avoid object creation, you have no other choice as dropping final, e.g.:

public interface Something {

    public int getSomeInteger();
    public String getSomeString();

    public static class Mutable implements Something {
        private int someInteger;
        private String someString;
        private boolean open = true;

        public int getSomeInteger() {
            return someInteger;
        }

        public String getSomeString() {
            return someString;
        }

        public Something.Mutable setSomeInteger(int someInteger) {
            assert open;
            this.someInteger = someInteger;
            return this;
        }

        public Something.Mutable setSomeString(String someString) {
            assert open;
            this.someString = someString;
            return this;
        }

        public Something make() {
            open = false;
            return this;
        }
    }
}

But this pattern is brittle, and the Java runtime is pretty good in dealing with object creation these days, so if you don't write a graphic or math lib or so, you shouldn't care about.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the answer, it's unfortunate java doesn't seem to support "freeze". \$\endgroup\$ – corykendall May 7 '12 at 18:09
1
\$\begingroup\$

Effective Java 2nd Edition has a complete chapter on this topic: Item 2: Consider a builder when faced with many constructor parameters.

You can put a freezed flag into the class then set it in the build method and check it in every setter.

public class Something {
    private int someInteger;
    private String someString;

    private boolean freezed;

    public Something() {
    }

    public int getSomeInteger() {
        return someInteger;
    }

    public Something setSomeInteger(final int someInteger) {
        checkFreezed();
        this.someInteger = someInteger;
        return this;
    }

    public String getSomeString() {
        return someString;
    }

    public Something setSomeString(final String someString) {
        checkFreezed();
        this.someString = someString;
        return this;
    }

    private void checkFreezed() {
        if (freezed) {
            throw new IllegalStateException();
        }
    }

    public Something build() {
        freezed = true;
        return this;
    }
}

Of course it has its own drawbacks:

[...] it can cause errors at runtime, as the compiler cannot ensure that the programmer calls the freeze method on an object before using it.

(From Effective Java 2nd Edition, Item 2: Consider a builder when faced with many constructor parameters)

You can avoid some repetition with an IDE plugin which generates the builder for you (for example, with fluent-builders-generator-eclipse-plugin).

Note that the term finalize is used for a completely different mechanism in Java. I'd use the regular build word.


Edit: Anyway, it's possible with only one object creation via reflection but it's really ugly. I don't think that it's worth it, object creation is cheap nowadays.

Something:

public interface Something {

    int getSomeInteger();

}

SomethingReflected:

import java.lang.reflect.Field;

public class SomethingReflected implements Something {

    private final int someInteger;

    public SomethingReflected() {
        this.someInteger = 0;
    }

    public SomethingReflected(final int someInteger) {
        super();
        this.someInteger = someInteger;
    }

    public SomethingReflected setSomeInteger(final int someInteger) {
        updateField("someInteger", someInteger);
        return this;
    }

    private void updateField(final String fieldName, final int value) {
        try {
            final Field field = this.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(this, value);
        } catch (final NoSuchFieldException e) {
            throw new AssertionError(e);
        } catch (final IllegalAccessException e) {
            throw new AssertionError(e);
        }
    }

    @Override
    public int getSomeInteger() {
        return someInteger;
    }

    public Something build() {
        return this;
    }
}

Another drawback is that clients could misuse it easily:

final SomethingReflected somethingReflected = new SomethingReflected();
somethingReflected.setSomeInteger(5);

final Something something = somethingReflected.build();

assertEquals(5, something.getSomeInteger());

somethingReflected.setSomeInteger(6);
final Something something2 = somethingReflected.build();

assertEquals(5, something.getSomeInteger()); // fails, returns 6
\$\endgroup\$
  • \$\begingroup\$ Thanks for this useful pattern; unfortunately it means I lose the final keyword and the speedups that offers. I'll wait around a few days to see if any other answers come in, and otherwise probably accept this one. \$\endgroup\$ – corykendall May 7 '12 at 18:10
  • 1
    \$\begingroup\$ @corykendall: The "final speedup" is nowadays often neglectable, if there is any. Generally you seem to be too concerned with speed - but then you stay with mutable objects anyway. There is a little price to pay for more secure immutable objects, but I'd say in 95% of the cases that doesn't matter. \$\endgroup\$ – Landei May 8 '12 at 15:22
  • \$\begingroup\$ If immutable objects are guaranteed to really are immutable, then code can safely replace references to distinct but equal objects with references to a common object. For such replacement to be safe, however, there should be no way by which even erroneously-threaded code could create an "immutable" object that mutates. It's difficult to provide such assurance when using popsicle immunity. It's much safer to say that no immutable object should encapsulate any object which has ever been exposed to mutation without cloning it first. \$\endgroup\$ – supercat Jan 24 '14 at 17:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.