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

时间:2013-01-03 19:54:13

标签: java reference final

我想要一个可以用一个变量unset(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");
        }
    }
}

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

12 个答案:

答案 0 :(得分:27)

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

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

答案 1 :(得分: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;
    }
}

用法:

Example e = new Example();

// do lots of stuff

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

// at this point, e is immutable

使用此模式,您可以构造对象,设置其值(尽可能多次),然后调用build()以“免除”它。

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

  1. 没有用于表示未初始化字段的魔术值。例如,0与任何其他long值一样有效。
  2. Setters具有一致的行为。在调用build()之前,它们可以正常工作。调用build()后,无论您传递的是什么值,它们都会抛出。 (为方便起见,请注意使用未经检查的例外情况。)
  3. 该类标记为final,否则开发人员可以扩展您的类并覆盖设置者。
  4. 但是这种方法有一个相当大的缺点:使用这个类的开发人员无法知道在编译时,如果某个特定对象已被初始化。当然,您可以添加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. 构建类的实例保证线程安全。
    4. 是的,维护起来有点复杂,但恕我直言,其好处超过了成本。

答案 2 :(得分:2)

你可以简单地添加一个布尔标志,并在你的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中编辑它,抱歉打字错误/格式问题,如果有......

答案 3 :(得分:2)

Google的Guava library(我非常推荐)附带了一个可以很好地解决这个问题的课程:SettableFuture。这提供了您询问的set-once语义,但也提供了更多:

  1. 代替传递异常的能力(setException方法);
  2. 明确取消活动的能力;
  3. 能够注册在设置值时通知的侦听器,通知异常或取消将来(ListenableFuture interface)。
  4. Future类型系列通常用于多线程程序中线程之间的同步,因此SettableFuture可以很好地处理这些类型。
  5. Java 8也有自己的版本:CompletableFuture

答案 4 :(得分:1)

这是我提出的解决方案,基于混合上面的一些答案和评论,特别是@KatjaChristiansen使用断言的答案和评论。

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时。这样,可以在编译时检测到更多错误。

答案 5 :(得分:1)

我有这个类,类似于JDK's 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;
    }
}

我有single responsibilty并检查getset两个电话,因此当客户端代码误用时,它会提前失败。

答案 6 :(得分:1)

这有两种方式;第一个与其他答案中提到的其他一些基本相同,但它在这里与秒对比。所以第一种方式,一次是通过在setter中强制执行一次只能设置一次的值。我的实现需要非空值,但是如果你想能够设置为null,那么你需要实现一个'isSet'布尔标志,如其他答案所示。

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

0000000: 2a00 0000 0000 0000 0000 0000 0000 0000  *...............

所以你可以按如下方式使用这些;

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

答案 7 :(得分:0)

尝试使用像

这样的int检查器
private long id = 0;
static int checker = 0;

public void methodThatWillSetValueOfId(stuff){
    checker = checker + 1

    if (checker==1){
        id = 123456;
    } 
}

答案 8 :(得分:0)

//你可以试试这个:

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

答案 9 :(得分:0)

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

FinalReference

答案 10 :(得分:-2)

将字段标记为私有而不公开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(改变对象状态的东西)之后验证对象的状态。

答案 11 :(得分:-6)

我认为单身人士模式可能是你应该考虑的事情。谷歌稍微检查一下这种模式是否符合你的设计目标。

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

public enum JavaObject {
    INSTANCE;

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

用法:

JavaObject.INSTANCE.doSomething();