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

2022-08-31 22:11:22

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

目前,我正在用一个二传手即兴创作,该二传手会抛出如下内容: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");
        }
    }
}

这是我想要做的事情的好方法吗?我觉得我应该能够在初始化后将某些内容设置为不可变,或者有一种模式可以用来使它更优雅。


答案 1

让我建议你一个更优雅的决定。第一个变体(不引发异常):

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

答案 2

“只设置一次”的要求感觉有点武断。我很确定您正在寻找的是一个从未初始化状态永久转换为初始化状态的类。毕竟,多次设置对象的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;
    }
}

用法:

Example e = new Example();

// do lots of stuff

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

// at this point, e is immutable

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

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

  1. 没有用于表示未初始化字段的幻数值。例如,是与任何其他值一样有效的 ID。0long
  2. 二传手具有一致的行为。在调用之前,它们工作。在 调用 之后,它们会抛出,而不管您传递什么值。(为方便起见,请注意使用未经检查的异常)。build()build()
  3. 该类被标记为 ,否则开发人员可以扩展您的类并重写 setter。final

但是这种方法有一个相当大的缺点:使用这个类的开发人员在编译时无法知道特定对象是否已初始化。当然,您可以添加一个方法,以便开发人员可以在运行时检查对象是否已初始化,但是在编译时了解此信息会更加方便。为此,您可以使用生成器模式: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);
        }
    }
}

用法:

Example.Builder builder = new Example.Builder();
builder.setId(12345L);
Example e = builder.build();

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

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

是的,维护起来有点复杂,但恕我直言,好处大于成本。