如何创建一个只能设置一次但在Java中不是最终的变量

Ric*_*ell 37 java final reference

我想要一个类,我可以使用一个变量unset(the id)创建实例,然后稍后初始化此变量,并在初始化后使其不可变.实际上,我想要一个final我可以在构造函数之外初始化的变量.

目前,我正在通过一个Exception如下所示的setter来即兴创作:

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");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是一个很好的方式来实现我想要做的事情吗?我觉得我应该能够在初始化后设置一些不可变的东西,或者我可以使用一种模式来使它更优雅.

And*_*niy 28

让我建议你一点点优雅的决定.第一个变种(没有抛出异常):

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;
    }

}
Run Code Online (Sandbox Code Playgroud)

第二个变种(抛出异常):

     public void setId(long id)  {
         this.id = this.id == null ? id : throw_();
     }

     public int throw_() {
         throw new RuntimeException("id is already set");
     }
Run Code Online (Sandbox Code Playgroud)

  • 我建议不要这样或者至少强烈建议将方法名称更改为'setIdUnlessAlreadySet',因为'setID'在这里很容易引起误解,而不是这个方法正在做什么.它设置值,除非已存在值.想象一下,你调用setID(长id),但它根本不会这样做.那是调试的噩梦. (3认同)

cam*_*ecc 10

"只设置一次"的要求感觉有点武断.我很确定你要找的是一个从未初始化状态永久转换到初始化状态的类.毕竟,只要在"构建"对象之后不允许更改id,就可以方便地设置对象的id多次(通过代码重用或其他).

一个相当合理的模式是在一个单独的字段中跟踪这个"建立"状态:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

Example e = new Example();

// do lots of stuff

e.setId(12345L);
e.build();

// at this point, e is immutable
Run Code Online (Sandbox Code Playgroud)

使用此模式,您可以构造对象,设置其值(方便的次数),然后调用build()"immutify"它.

与初始方法相比,此模式有几个优点:

  1. 没有用于表示未初始化字段的魔术值.例如,0与任何其他long值一样有效.
  2. Setters具有一致的行为.在build()被叫之前,他们工作.在build()被叫之后,无论你传递什么值,他们都会抛出.(为方便起见,请注意使用未经检查的异常).
  3. 该类被标记final,否则开发人员可以扩展您的类并覆盖setter.

但是这种方法有一个相当大的缺点:使用这个类的开发人员在编译时无法知道特定对象是否已经初始化.当然,您可以添加一个isBuilt()方法,以便开发人员可以在运行时检查对象是否已初始化,但在编译时知道此信息会更方便.为此,您可以使用构建器模式:

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

Example.Builder builder = new Example.Builder();
builder.setId(12345L);
Example e = builder.build();
Run Code Online (Sandbox Code Playgroud)

由于以下几个原因,这要好得多:

  1. 我们正在使用final字段,因此编译器和开发人员都知道这些值无法更改.
  2. 通过Java的类型系统描述了对象的初始化和未初始化形式之间的区别.一旦构建了对象,就没有可以调用它的setter.
  3. 构建类的实例保证线程安全.

是的,维护起来有点复杂,但恕我直言,其好处超过了成本.

  • 至于"构建者"模式,你是正确的,对于这个简单的例子,它之间并没有太大的区别,只是简单地说'示例e =新例子(12345L)`.区别来自类型告诉你的内容.如果您有一个`Example.Builder`实例,您知道所有字段都可以随时设置(包括id,最终设置时).但是,如果你有一个`Example`的实例,你知道该对象已经完全初始化,并且将来不会(不能)改变.这是一个非常有力的保证. (5认同)

ent*_*erd 6

我最近在编写一些代码来构造一个不可变的循环图(其中边引用其节点)时遇到了这个问题。我还注意到这个问题的现有答案都不是线程安全的(这实际上允许多次设置该字段),所以我想我会贡献我的答案。基本上,我只是创建了一个名为 的包装器类FinalReference,它包装了AtomicReference并利用 的AtomicReference方法compareAndSet()。通过调用compareAndSet(null, newValue),您可以确保多个并发修改线程最多设置一次新值。该调用是原子的,并且仅当现有值为 null 时才会成功。请参阅下面的示例源代码FinalReference和示例测试代码的 Github 链接来演示正确性。

public final class FinalReference<T> {
  private final AtomicReference<T> reference = new AtomicReference<T>();

  public FinalReference() {
  }

  public void set(T value) {
    this.reference.compareAndSet(null, value);
  }

  public T get() {
    return this.reference.get();
  }
}
Run Code Online (Sandbox Code Playgroud)