Is this solution watertight?
No. (Though it might be adequate, depending on how and where the code is used.)
In Item 77: For instance control, prefer enum types to readResolve (Effective Java, 2nd ed), Bloch demonstrates how an attacker could have a class like yours return any value. The attack relies on hand-crafted byte input and being able to run code on the target (so your code might be a security risk if used in sand-boxed environments, some RMI cases, etc.). I do not know if that is the only attack - it is the only one mentioned. The solution is to declare all fields transient, but then you have the issue of how to store the value.
You may be able to get round these problems using the serialization proxy pattern (Item 78 in the book - there's a reason every Java programmer who's read it recommends it).
public final class Thing implements Serializable {
private static final long serialVersionUID = 1L;
private static final Thing[] INSTANCES = new Thing[2];
private static int NEXT_ORDINAL = 0;
public static final Thing INSTANCE0 = new Thing(
"whatever0");
public static final Thing INSTANCE1 = new Thing(
"whatever1");
private transient final String someState;
public String someMethod() {
return someState;
}
private final int ordinal;
private Thing(String someState) {
this.someState = someState;
ordinal = NEXT_ORDINAL++;
INSTANCES[ordinal] = this;
}
private Object writeReplace() {
return new ThingProxy(this);
}
private void readObject(ObjectInputStream stream)
throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
private static class ThingProxy implements Serializable {
private static final long serialVersionUID = 1L;
private final int ordinal;
private ThingProxy(Thing t) {
ordinal = t.ordinal;
}
private Object readResolve()
throws ObjectStreamException {
return INSTANCES[ordinal];
}
}
}
Though, as with copying anything security related from the internet, caveat emptor. I am by no means an expert.