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



我想要一个类,我可以创建一个变量未设置(id)的实例,然后稍后初始化该变量,并在初始化后使其不可变。实际上,我想要一个可以在构造函数之外初始化的final变量。

目前,我正在用一个setter来临时解决这个问题,它抛出一个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");
        }
    }
}

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

让我建议你做一个更优雅的决定。第一个变量(不抛出异常):

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

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

使用这个模式,您构建对象,设置它的值(尽可能多的次数),然后调用build()来"免疫"它。

与最初的方法相比,这种模式有以下几个优点:
    没有魔法值用来表示未初始化的字段。例如,0和其他long值一样有效。
  1. 设置具有一致的行为。在调用build()之前,它们工作。在调用build()之后,不管你传递了什么值,它们都会抛出。(注意为了方便使用未检查的异常)。
  2. 类被标记为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);
        }
    }
}

用法:

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

这样做要好得多,原因如下:

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

是的,维护起来有点复杂,但我认为收益大于成本。

我最近在编写一些代码来构建一个不可变的循环图时遇到了这个问题,其中边引用了它们的节点。我还注意到,这个问题的现有答案都不是线程安全的(这实际上允许多次设置该字段),所以我认为我应该提供我的答案。基本上,我只是创建了一个名为FinalReference的包装器类,它包装了AtomicReference并利用了AtomicReferencecompareAndSet()方法。通过调用compareAndSet(null, newValue),您可以确保多个并发修改线程最多只设置一次新值。该调用是原子性的,只有当现有值为空时才会成功。请参阅下面的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();
  }
}

Google的Guava库(我非常推荐)提供了一个很好地解决这个问题的类:SettableFuture。这提供了您所要求的set-once语义,但还有更多:

  1. 通信异常的能力(setException方法);
  2. 显式取消事件的能力;
  3. 注册侦听器的能力,当值被设置时将被通知,异常被通知或取消(ListenableFuture接口)。
  4. Future类型家族通常用于多线程程序中线程之间的同步,因此SettableFuture非常适合这些。

Java 8也有自己的版本:CompletableFuture .

您可以简单地添加一个布尔标志,并在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;
    }
}

I有单一的责任,检查getset调用,所以当客户端代码误用它时,它会早期失败。

有两种方法;第一个与其他答案中提到的其他一些基本相同,但这里是为了与第二个进行对比。第一种方法,Once是通过在setter中强制设置一个只能设置一次的值。我的实现需要非空值,但如果您希望能够设置为空,那么您需要实现'isSet'布尔标志,如其他答案所建议的。

第二种方法,Lazy,是提供一个函数,在第一次调用getter时惰性地提供值。

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

尝试使用像

这样的int检查器
private long id = 0;
static int checker = 0;
public void methodThatWillSetValueOfId(stuff){
    checker = checker + 1
    if (checker==1){
        id = 123456;
    } 
}

//你可以试试这个:

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

将字段标记为private并且不暴露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");   
        }  
    }  
}  

这种方法类似于契约式设计,在调用mutator(改变对象状态的东西)之后验证对象的状态。

我认为您应该研究一下单例模式。用谷歌搜索一下,看看这个模式是否符合你的设计目标。

下面是一些关于如何在Java中使用enum创建单例的sudo代码。我认为这是基于Joshua Bloch在Effective Java中概述的设计,无论如何,如果你还没有这本书,这本书值得一读。

public enum JavaObject {
    INSTANCE;
    public void doSomething(){
        System.out.println("Hello World!");
    }
}

用法:

JavaObject.INSTANCE.doSomething();

最新更新