48

我想要一个类,我可以用一个未设置的变量(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");
        }
    }
}

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

4

12 回答 12

37

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

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");
     }
于 2013-01-03T23:14:49.230 回答
12

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

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

  1. 没有用于表示未初始化字段的魔法值。例如,与任何其他值0一样有效的 id 。long
  2. 设置者具有一致的行为。在build()被调用之前,它们工作。调用后build(),无论您传递什么值,它们都会抛出。(为方便起见,请注意使用未经检查的异常)。
  3. 该类被标记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 的类型系统来描述的。一旦对象被构建,就没有设置器来调用它。
  3. 已构建类的实例保证线程安全。

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

于 2013-01-03T22:52:54.500 回答
3

Google 的Guava 库(我非常推荐)自带一个类,可以很好地解决这个问题:SettableFuture. 这提供了您询问的 set-once 语义,但还提供了更多:

  1. 传达异常的能力(setException方法);
  2. 显式取消事件的能力;
  3. 注册侦听器的能力,这些侦听器将在设置值时通知,通知异常或取消未来(ListenableFuture接口)。
  4. Future通常用于多线程程序中线程之间同步的类型系列,因此可以SettableFuture很好地使用这些类型。

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

于 2016-04-20T22:04:59.063 回答
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 中编辑它,抱歉,错字/格式问题,如果有...

于 2013-01-03T23:05:03.267 回答
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 时创建对象的方法。这样,可以在编译时检测到更多错误。

于 2013-01-04T11:42:16.120 回答
1

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

我有单一的责任并检查两者getset调用,因此当客户端代码滥用它时它会提前失败。

于 2014-08-01T15:02:52.830 回答
1

这里有两种方法;第一个与其他答案中提到的其他一些基本相同,但在这里与秒进行对比。所以第一种方法,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
于 2015-05-07T01:35:27.520 回答
1

我最近在编写一些代码来构造一个边引用其节点的不可变循环图时遇到了这个问题。我还注意到,这个问题的现有答案都不是线程安全的(实际上允许多次设置该字段),所以我想我会贡献我的答案。基本上,我刚刚创建了一个名为的包装类FinalReference,它包装AtomicReference并利用了AtomicReference'scompareAndSet()方法。通过调用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();
  }
}
于 2020-05-01T05:57:43.397 回答
0

尝试有一个像这样的 int 检查器

private long id = 0;
static int checker = 0;

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

    if (checker==1){
        id = 123456;
    } 
}
于 2016-02-05T13:28:09.633 回答
0

//你可以试试这个:

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

于 2016-05-08T15:07:13.010 回答
-2

将字段标记为私有而不公开 asetter就足够了:

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(改变对象状态的东西)之后验证对象的状态。

于 2013-01-03T19:56:07.107 回答
-6

我认为单例模式可能是你应该研究的东西。谷歌一下,看看这个模式是否符合你的设计目标。

下面是一些关于如何使用枚举在 Java 中制作单例的 sudo 代码。我认为这是基于 Joshua Bloch 在 Effective Java 中概述的设计,不管怎样,如果你还没有这本书,这本书值得一读。

public enum JavaObject {
    INSTANCE;

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

用法:

JavaObject.INSTANCE.doSomething();
于 2013-01-03T20:31:17.977 回答