Как создать переменную, которая может быть установлена только один раз, но не является окончательной в Java
Я хочу класс, который я могу создать экземпляры с одной переменной unset (id
), а затем инициализировать эту переменную позже, и она неизменяемым после инициализации. Фактически, я бы хотел final
переменная, которую я могу инициализировать вне конструктора.
В настоящее время я импровизирую это с сеттером, который бросает Exception
следующим образом:
public class Example {
private long id = 0;
// Constructors and other variables and methods deleted for clarity
public long getId() {
return id;
}
public void setId(long id) throws Exception {
if ( this.id == 0 ) {
this.id = id;
} else {
throw new Exception("Can't change id once set");
}
}
}
это хороший способ сделать то, что я пытаюсь сделать? Я чувствую, что я должен быть в состоянии чтобы установить что-то как неизменное после инициализации, или что есть шаблон, который я могу использовать, чтобы сделать это более элегантным.
11 ответов
позвольте мне предложить вам немного более элегантное решение. Первый вариант (без исключения):
public class Example {
private Long id;
// Constructors and other variables and methods deleted for clarity
public long getId() {
return id;
}
public void setId(long id) {
this.id = this.id == null ? id : this.id;
}
}
второй вариант (с исключением):
public void setId(long id) {
this.id = this.id == null ? id : throw_();
}
public int throw_() {
throw new RuntimeException("id is already set");
}
требование "установить только один раз" кажется немного произвольным. Я уверен, что вы ищете класс, который постоянно переходит из неинициализированного состояния в инициализированное. В конце концов, может быть удобно установить идентификатор объекта более одного раза (через повторное использование кода или что-то еще), если идентификатор не разрешается изменять после того, как объект "построен".
один довольно разумный шаблон-отслеживать это "построенное" состояние в отдельном поле:
public final class Example {
private long id;
private boolean isBuilt;
public long getId() {
return id;
}
public void setId(long id) {
if (isBuilt) throw new IllegalArgumentException("already built");
this.id = id;
}
public void build() {
isBuilt = true;
}
}
использование:
Example e = new Example();
// do lots of stuff
e.setId(12345L);
e.build();
// at this point, e is immutable
С помощью этого шаблона вы строите объект, устанавливаете его значения (столько раз, сколько удобно), а затем вызываете build()
в "immutify" его.
есть несколько преимуществ этого шаблона по сравнению с вашим первоначальным подходом:
- для представления неинициализированных полей не используются магические значения. Например,
0
имеет такой же идентификатор, как и любой другойlong
значение. - сеттеры имеют последовательное поведение. До
build()
называется, они работают. Послеbuild()
называется, они бросают, независимо от того, какие ценности вы передаете. (Обратите внимание на использование непроверенных исключений для удобства). - класс помечен
final
, в противном случае разработчик может расширить свой класс и переопределить сеттеры.
но этот подход имеет довольно большой недостаток: разработчики, использующие этот класс, не могут знать,во время компиляции, если определенный объект инициализирован или нет. Конечно, вы можете добавить isBuilt()
способ, чтобы разработчики могли проверить, во время, если объект инициализируется, но было бы намного удобнее знать эту информацию во время компиляции. Для этого вы можете использовать шаблон builder:
public final class Example {
private final long id;
public Example(long id) {
this.id = id;
}
public long getId() {
return id;
}
public static class Builder {
private long id;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Example build() {
return new Example(id);
}
}
}
использование:
Example.Builder builder = new Example.Builder();
builder.setId(12345L);
Example e = builder.build();
это гораздо лучше по нескольким причинам:
- мы используем
final
поля, поэтому компилятор и разработчики знают, что эти значения не могут быть изменен. - различие между инициализированными и неинициализированными формы объекта описывается через систему типов Java. Просто нет сеттера для вызова объекта после его создания.
- экземпляры построенного класса гарантированы потокобезопасными.
Да, это немного сложнее поддерживать, но ИМХО преимущества перевешивают стоимость.
вы можете просто добавить логический флаг и в setId () установить/проверить логическое значение. Если я правильно понял вопрос, нам не нужна сложная структура/шаблон здесь. Как насчет этого:
public class Example {
private long id = 0;
private boolean touched = false;
// Constructors and other variables and methods deleted for clarity
public long getId() {
return id;
}
public void setId(long id) throws Exception {
if ( !touchted ) {
this.id = id;
touched = true;
} else {
throw new Exception("Can't change id once set");
}
}
}
таким образом, если вы setId(0l);
он думает, что ID-это тоже набор. Вы можете изменить, если это не подходит для вашего требования бизнес-логики.
не редактировал его в IDE, извините за проблему опечатки / формата, если она была...
вот решение, которое я придумал, основанное на смешивании некоторых ответов и комментариев выше, особенно одного из @KatjaChristiansen при использовании assert.
public class Example {
private long id = 0L;
private boolean idSet = false;
public long getId() {
return id;
}
public void setId(long id) {
// setId should not be changed after being set for the first time.
assert ( !idSet ) : "Can't change id from " + this.id + " to " + id;
this.id = id;
idSet = true;
}
public boolean isIdSet() {
return idSet;
}
}
в конце концов, я подозреваю, что моя потребность в этом является признаком плохих дизайнерских решений в другом месте, и я должен скорее найти способ создания объекта только тогда, когда я знаю Id, и установить id в final. Таким образом, во время компиляции можно обнаружить больше ошибок.
у меня есть этот класс, подобно пакета JDK AtomicReference
, и я использую его в основном для устаревшего кода:
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
public class PermanentReference<T> {
private T reference;
public PermanentReference() {
}
public void set(final @Nonnull T reference) {
checkState(this.reference == null,
"reference cannot be set more than once");
this.reference = checkNotNull(reference);
}
public @Nonnull T get() {
checkState(reference != null, "reference must be set before get");
return reference;
}
}
Я один ответственности и как get
и set
вызовы, поэтому он терпит неудачу рано, когда клиентский код злоупотребляет им.
вот два способа; первый в основном такой же, как и некоторые другие, упомянутые в других ответах, но он здесь, чтобы интерпретировать секунды. Таким образом, Первый способ-иметь значение, которое может быть установлено только один раз, применяя его в сеттере. Моя реализация требует ненулевых значений, но если вы хотите иметь возможность установить значение null, то вам нужно будет реализовать логический флаг "isSet", как предложено в других ответах.
второй путь, ленивый, обеспечить функцию которая лениво поставляет значение после первого вызова геттера.
import javax.annotation.Nonnull;
public final class Once<T>
{
private T value;
public set(final @Nonnull T value)
{
if(null != this.value) throw new IllegalStateException("Illegal attempt to set a Once value after it's value has already been set.");
if(null == value) throw new IllegalArgumentException("Illegal attempt to pass null value to Once setter.");
this.value = value;
}
public @Nonnull T get()
{
if(null == this.value) throw new IllegalStateException("Illegal attempt to access unitialized Once value.");
return this.value;
}
}
public final class Lazy<T>
{
private Supplier<T> supplier;
private T value;
/**
* Construct a value that will be lazily intialized the
* first time the getter is called.
*
* @param the function that supplies the value or null if the value
* will always be null. If it is not null, it will be called
* at most one time.
*/
public Lazy(final Supplier<T> supplier)
{
this.supplier = supplier;
}
/**
* Get the value. The first time this is called, if the
* supplier is not null, it will be called to supply the
* value.
*
* @returns the value (which may be null)
*/
public T get()
{
if(null != this.supplier)
{
this.value = this.supplier.get();
this.supplier = null; // clear the supplier so it is not called again
// and can be garbage collected.
}
return this.value;
}
}
таким образом, вы можете использовать их следующим образом;
//
// using Java 8 syntax, but this is not a hard requirement
//
final Once<Integer> i = Once<>();
i.set(100);
i.get(); // returns 100
// i.set(200) would throw an IllegalStateException
final Lazy<Integer> j = Lazy<>(() -> i);
j.get(); // returns 100
Google гуава библиотека (что я рекомендую очень высоко) поставляется с классом, который решает эту проблему очень хорошо:SettableFuture
. Это обеспечивает семантику set-once, о которой вы спрашиваете, но и многое другое:
- возможность сообщить исключение вместо этого (
setException
способ); - возможность явного отмены события;
- возможность регистрации слушателей, которые будут уведомлены, когда значение установите, исключение уведомляется или будущее отменяется (
ListenableFuture
интерфейс). - на
Future
семейство типов вообще используется для синхронизации между потоками в многопоточных программах, так чтоSettableFuture
очень хорошо играет с ними.
в Java 8 также имеет свою собственную версию этого: CompletableFuture
.
попробуйте иметь int checker, как
private long id = 0;
static int checker = 0;
public void methodThatWillSetValueOfId(stuff){
checker = checker + 1
if (checker==1){
id = 123456;
}
}
/ / u может попробовать следующее:
class Star
{
private int i;
private int j;
static boolean a=true;
Star(){i=0;j=0;}
public void setI(int i,int j) {
this.i =i;
this.j =j;
something();
a=false;
}
public void printVal()
{
System.out.println(i+" "+j);
}
public static void something(){
if(!a)throw new ArithmeticException("can't assign value");
}
}
public class aClass
{
public static void main(String[] args) {
System.out.println("");
Star ob = new Star();
ob.setI(5,6);
ob.printVal();
ob.setI(6,7);
ob.printVal();
}
}
маркировка поля частная и не подвергая setter
должно быть достаточно:
public class Example{
private long id=0;
public Example(long id)
{
this.id=id;
}
public long getId()
{
return this.id;
}
если этого недостаточно, и вы хотите, чтобы кто-то мог изменить его X раз, вы можете сделать это:
public class Example
{
...
private final int MAX_CHANGES = 1;
private int changes = 0;
public void setId(long id) throws Exception {
validateExample();
changes++;
if ( this.id == 0 ) {
this.id = id;
} else {
throw new Exception("Can't change id once set");
}
}
private validateExample
{
if(MAX_CHANGES==change)
{
throw new IllegalStateException("Can no longer update this id");
}
}
}
этот подход сродни проектированию по контракту, в котором вы проверяете состояние объекта после вызова мутатора (что-то, что изменяет состояние объекта).
Я думаю, что шаблон singleton может быть чем-то, что вы должны изучить. Google вокруг немного, чтобы проверить, соответствует ли этот шаблон вашим целям дизайна.
Ниже приведен некоторый код sudo о том, как сделать синглтон в Java с помощью перечисления. Я думаю, что это основано на дизайне Джошуа Блоха, изложенном в эффективной Java, в любом случае это книга, которую стоит взять, если у вас ее еще нет.
public enum JavaObject {
INSTANCE;
public void doSomething(){
System.out.println("Hello World!");
}
}
использование:
JavaObject.INSTANCE.doSomething();