public interface Freezable<T> extends Cloneable
There are often times when you need objects to be objects 'safe', so that they can't be modified. Examples are when objects need to be thread-safe, or in writing robust code, or in caches. If you are only creating your own objects, you can guarantee this, of course -- but only if you don't make a mistake. If you have objects handed into you, or are creating objects using others handed into you, it is a different story. It all comes down to whether you want to take the Blanche Dubois approach ("depend on the kindness of strangers") or the Andy Grove approach ("Only the Paranoid Survive").
For example, suppose we have a simple class:
public class A { protected Collection b; protected Collection c; public Collection get_b() { return b; } public Collection get_c() { return c; } public A(Collection new_b, Collection new_c) { b = new_b; c = new_c; } }
Since the class doesn't have any setters, someone might think that it is immutable. You know where this is leading, of course; this class is unsafe in a number of ways. The following illustrates that.
public test1(SupposedlyImmutableClass x, SafeStorage y) { // unsafe getter A a = x.getA(); Collection col = a.get_b(); col.add(something); // a has now been changed, and x too // unsafe constructor a = new A(col, col); y.store(a); col.add(something); // a has now been changed, and y too }
There are a few different techniques for having safe classes.
There are advantages and disadvantages of each of these.
The Freezable
model supplements these choices by giving you
the ability to build up an object by calling various methods, then when it is
in a final state, you can make it immutable. Once immutable, an
object cannot ever be modified, and is completely thread-safe: that
is, multiple threads can have references to it without any synchronization.
If someone needs a mutable version of an object, they can use
cloneAsThawed()
, and modify the copy. This provides a simple,
effective mechanism for safe classes in circumstances where the alternatives
are insufficient or clumsy. (If an object is shared before it is immutable,
then it is the responsibility of each thread to mutex its usage (as with
other objects).)
Here is what needs to be done to implement this interface, depending on the type of the object.
These are the easiest. You just use the interface to reflect that, by adding the following:
public class A implements Freezable<A> { ... public final boolean isFrozen() {return true;} public final A freeze() {return this;} public final A cloneAsThawed() { return this; } }
These can be final methods because subclasses of immutable objects must
themselves be immutable. (Note: freeze
is returning
this
for chaining.)
Add a protected 'flagging' field:
protected volatile boolean frozen; // WARNING: must be volatile
Add the following methods:
public final boolean isFrozen() { return frozen; }; public A freeze() { frozen = true; // WARNING: must be final statement before return return this; }
Add a cloneAsThawed()
method following the normal pattern for
clone()
, except that frozen=false
in the new
clone.
Then take the setters (that is, any method that can change the internal state of the object), and add the following as the first statement:
if (isFrozen()) { throw new UnsupportedOperationException("Attempt to modify frozen object"); }
Any subclass of a Freezable
will just use its superclass's
flagging field. It must override freeze()
and
cloneAsThawed()
to call the superclass, but normally does not
override isFrozen()
. It must then just pay attention to its
own getters, setters and fields.
Internal caches are cases where the object is logically unmodified, but internal state of the object changes. For example, there are const C++ functions that cast away the const on the "this" pointer in order to modify an object cache. These cases are handled by mutexing the internal cache to ensure thread-safety. For example, suppose that UnicodeSet had an internal marker to the last code point accessed. In this case, the field is not externally visible, so the only thing you need to do is to synchronize the field for thread safety.
Internal fields are called safe if they are either
frozen
or immutable (such as String or primitives). If you've
never allowed internal access to these, then you are all done. For example,
converting UnicodeSet to be Freezable
is just accomplished
with the above steps. But remember that you have allowed
access to unsafe internals if you have any code like the following, in a
getter, setter, or constructor:
Collection getStuff() { return stuff; } // caller could keep reference & modify void setStuff(Collection x) { stuff = x; } // caller could keep reference & modify MyClass(Collection x) { stuff = x; } // caller could keep reference & modify
These also illustrated in the code sample in Background above.
To deal with unsafe internals, the simplest course of action is to do the
work in the freeze()
function. Just make all of your internal
fields frozen, and set the frozen flag. Any subsequent getter/setter will
work properly. Here is an example:
Warning! The 'frozen' boolean MUST be volatile, and must be set as the last statement in the method.
public A freeze() { if (!frozen) { foo.freeze(); frozen = true; } return this; }
If the field is a Collection
or Map
, then to
make it frozen you have two choices. If you have never allowed access to the
collection from outside your object, then just wrap it to prevent future
modification.
zone_to_country = Collections.unmodifiableMap(zone_to_country);
If you have ever allowed access, then do a clone()
before wrapping it.
zone_to_country = Collections.unmodifiableMap(zone_to_country.clone());
If a collection (or any other container of objects) itself can contain mutable objects, then for a safe clone you need to recurse through it to make the entire collection immutable. The recursing code should pick the most specific collection available, to avoid the necessity of later downcasing.
Note: An annoying flaw in Java is that the generic collections, like
Map
orSet
, don't have aclone()
operation. When you don't know the type of the collection, the simplest course is to just create a new collection:zone_to_country = Collections.unmodifiableMap(new HashMap(zone_to_country));
Modifier and Type | Method and Description |
---|---|
T |
cloneAsThawed()
Provides for the clone operation.
|
T |
freeze()
Freezes the object.
|
boolean |
isFrozen()
Determines whether the object has been frozen or not.
|
boolean isFrozen()
T freeze()
T cloneAsThawed()
Copyright © 2016 Unicode, Inc. and others.