问题
Is there a standard practice in Java, while using the builder pattern, to ensure that a member variable is set at most once. I need to make sure that the setter is called 0 or 1 times but never more. I would like to throw a RuntimeException
of some type but am worried about synchronization issues as well as best-practices in this area.
回答1:
There's nothing wrong with raising an exception if a user calls a method in an illegal way like you describe, but it's not terribly elegant. The idea behind the builder pattern is to let users write fluent, readable object definitions, and compile-time safety is a big part of that. If users can't be confident the builder will succeed even if it compiles, you're introducing additional complexity users now need to understand and account for.
There are a couple of ways to accomplish what you're describing, lets explore them:
Just let users do what they want
One nice thing about builders is they can let you construct multiple different objects from the same builder:
List<Person> jonesFamily = new ArrayList<>(); Person.Builder builder = new Person.Builder().setLastName("Jones"); for(String firstName : jonesFamilyFirstNames) { family.add(builder.setFirstName(firstName).build()); }
I assume you have a good reason for forbidding this sort of behavior, but I'd be remiss if I didn't call out this useful trick. Maybe you don't need to restrict this in the first place.
Raise an Exception
You suggest raising an exception, and that will certainly work. Like I said, I don't think it's the most elegant solution, but here's one implementation (using Guava's Preconditions, for extra readability):
public class Builder { private Object optionalObj = null; // ... public Builder setObject(Object setOnce) { checkState(optionalObj == null, "Don't call setObject() more than once"); optionalObj = setOnce; } // ... }
This raises an IllegalStateException, so you can just call
throw new IllegalStateException()
if you aren't using Guava (you should be... :) ). Assuming you're not passing builder objects around between threads, you should have no synchronization issues. If you are, you should put some further thought into why you need the same builder in different threads - that's almost surely an anti-pattern.Don't provide the method at all
This is the cleanest, clearest way to prevent the user from calling a method you don't want them to - don't provide it in the first place. Instead, override either the builder's constructor or
build()
method so they can optionally pass the value at that time, but at no other time. This way you clearly guarantee the value can be set at most once per object constructed.public class Builder { // ... public Obj build() { ... } public Obj build(Object onceOnly) { ... } }
Use different types to expose certain methods
I haven't actually done this, and it might be more confusing than it's worth (in particular, you'll likely need to use a self-bounding generic for the methods in
Builder
), but it came to mind while I was writing and could be very explicit for certain use cases. Have your restricted method in a subclass of the builder, and that method returns the parent type, therefore preventing the caller from re-calling the method. An example might help, if that didn't make sense:public class Builder { // contains regular builder methods } public class UnsetBuilder extends Builder { public Builder setValue(Object obj) { ... } } // the builder constructor actually returns an UnsetBuilder public static UnsetBuilder builder() { ... }
Then we can call something like:
builder().setValue("A").build();
But we'd get a compile-time error if we tried to call:
builder().setValue("A").setValue("B").build();
because
setValue()
returnsBuilder
, which lacks asetValue()
method, therefore preventing the second case. This would be tricky to get exactly right (what if the user casts theBuilder
back to aUnsetBuilder
?) but with some effort would do what you're looking for.
回答2:
Object objectToSet;
boolean isObjectSet = false;
void setObject(Object object) throws RuntimeException {
if(!isObjectSet) {
objectToSet=object;
isObjectSet=true;
} else {
throw new RuntimeException();
}
}
This is the standard practice.
回答3:
You can use the Builder pattern outlined in Effective Java. This doesn't quite meets your specifications, but I think it should meet your needs. Let me know.
class Foo {
private final Object objectToSet;
private Foo(Foo.Builder builder) {
objectToSet = builder.getObjectToSet();
}
public static class Builder {
private Object objectToSet;
public Builder() { }
private Object getObjectToSet() {
return objectToSet;
}
public Builder objectToSet(Object objectToSet) {
this.objectToSet = objectToSet;
return this;
}
public Foo build() {
return new Foo(this);
}
}
}
Then later:
Object someObject = 10;
Foo foo = new Foo.Builder()
.objectToSet(someObject)
.build();
// A Foo's `objectToSet` can only be set once
This allows you to define a class (this case Foo
) that has properties that can only be set once. As a bonus, you can make the the Foo
truly immutable.
回答4:
You can define your variable as final
.
final MyClass object;
EDIT: If it sets more than once you get a compile-time error.
Or as mentioned by other, you can check your variable state in the setter and for thread-safety, declare the setter method as synchronized
.
public synchronized void setMyObject(Object object)
{
if (this.myObject == null)
{
this.myObject = object;
} else {
throw new RuntimeException();
}
}
Use the setter method even in constructor or anywhere else.
Note: Using synchronized
methods may lead to performance instability in huge processing.
来源:https://stackoverflow.com/questions/25497728/set-a-value-at-most-once-with-the-builder-pattern