How to create a variable that can be set only once

2019-02-05 18:43发布

I want a class that I can create instances of with one variable unset (the id), then initialise this variable later, and have it immutable after initialisation. Effectively, I'd like a final variable that I can initialise outside of the constructor.

Currently, I'm improvising this with a setter that throws an Exception as follows:

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

Is this a good way of going about what I'm trying to do? I feel like I should be able to set something as immutable after it's initialised, or that there is a pattern I can use to make this more elegant.

11条回答
在下西门庆
2楼-- · 2019-02-05 18:50

try have an int checker like

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

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

    if (checker==1){
        id = 123456;
    } 
}
查看更多
做自己的国王
3楼-- · 2019-02-05 18:59

//u can try this:

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

查看更多
smile是对你的礼貌
4楼-- · 2019-02-05 19:03

Google's Guava library (which I recommend very highly) comes with a class that solves this problem very well: SettableFuture. This provides the set-once semantics that you ask about, but also a lot more:

  1. The ability to communicate an exception instead (the setException method);
  2. The ability to cancel the event explicitly;
  3. The ability to register listeners that will be notified when the value is set, an exception is notified or the future is canceled (the ListenableFuture interface).
  4. The Future family of types in general used for synchronization between threads in multithreaded programs, so SettableFuture plays very nicely with these.

Java 8 also has its own version of this: CompletableFuture.

查看更多
SAY GOODBYE
5楼-- · 2019-02-05 19:04

The "set only once" requirement feels a bit arbitrary. I'm fairly certain what you're looking for is a class that transitions permanently from uninitialized to initialized state. After all, it may be convenient to set an object's id more than once (via code reuse or whatever), as long as the id is not allowed to change after the object is "built".

One fairly reasonable pattern is to keep track of this "built" state in a separate field:

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

Usage:

Example e = new Example();

// do lots of stuff

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

// at this point, e is immutable

With this pattern, you construct the object, set its values (as many times as is convenient), and then call build() to "immutify" it.

There are several advantages to this pattern over your initial approach:

  1. There are no magic values used to represent uninitialized fields. For example, 0 is just as valid an id as any other long value.
  2. Setters have a consistent behavior. Before build() is called, they work. After build() is called, they throw, regardless of what values you pass. (Note the use of unchecked exceptions for convenience).
  3. The class is marked final, otherwise a developer could extend your class and override the setters.

But this approach has a fairly big drawback: developers using this class can't know, at compile time, if a particular object has been initialized or not. Sure, you could add an isBuilt() method so developers can check, at runtime, if the object is initialized, but it would be so much more convenient to know this information at compile time. For that, you could use the builder pattern:

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

Usage:

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

This is much better for several reasons:

  1. We're using final fields, so both the compiler and developers know these values cannot be changed.
  2. The distinction between initialized and uninitialized forms of the object is described via Java's type system. There is simply no setter to call on the object once it has been built.
  3. Instances of the built class are guaranteed thread safe.

Yes, it's a bit more complicated to maintain, but IMHO the benefits outweigh the cost.

查看更多
混吃等死
6楼-- · 2019-02-05 19:08

Here's the solution I came up with based on mixing some of the answers and comments above, particularly one from @KatjaChristiansen on using 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;
    }

}

At the end of the day, I suspect that my need for this is an indication of poor design decisions elsewhere, and I should rather find a way of creating the object only when I know the Id, and setting the id to final. This way, more errors can be detected at compile time.

查看更多
smile是对你的礼貌
7楼-- · 2019-02-05 19:11

You can simply add a boolean flag, and in your setId(), set/check the boolean. If I understood the question right, we don't need any complex structure/pattern here. How about this:

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

}

in this way, if you setId(0l); it thinks that the ID is set too. You can change if it is not right for your business logic requirement.

not edited it in an IDE, sorry for the typo/format problem, if there was...

查看更多
登录 后发表回答