web-dev-qa-db-fra.com

Comment créer une variable qui ne peut être définie qu'une seule fois mais qui n'est pas définitive dans Java

Je veux une classe dont je peux créer des instances avec une variable non définie (le id), puis initialiser cette variable plus tard, et l'avoir immuable après l'initialisation. En effet, je voudrais une variable final que je puisse initialiser en dehors du constructeur.

Actuellement, j'improvise cela avec un setter qui lance un Exception comme suit:

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

Est-ce une bonne façon de faire ce que j'essaie de faire? Je pense que je devrais pouvoir définir quelque chose comme immuable après son initialisation, ou qu'il existe un modèle que je peux utiliser pour le rendre plus élégant.

35
Richard Russell

Permettez-moi de vous suggérer une décision un peu plus élégante. Première variante (sans lever d'exception):

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

}

Deuxième variante (avec levée d'une exception):

     public void setId(long id)  {
         this.id = this.id == null ? id : throw_();
     }

     public int throw_() {
         throw new RuntimeException("id is already set");
     }
27
Andremoniy

L'exigence "ne définir qu'une seule fois" semble un peu arbitraire. Je suis assez certain que ce que vous recherchez est une classe qui passe en permanence de l'état non initialisé à l'état initialisé. Après tout, il peut être pratique de définir l'ID d'un objet plus d'une fois (via la réutilisation de code ou autre), tant que l'ID n'est pas autorisé à changer une fois que l'objet est "construit".

Un modèle assez raisonnable consiste à suivre cet état "construit" dans un champ distinct:

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

Avec ce modèle, vous construisez l'objet, définissez ses valeurs (autant de fois que cela vous convient), puis appelez build() pour "l'immutifier".

Ce modèle présente plusieurs avantages par rapport à votre approche initiale:

  1. Aucune valeur magique n'est utilisée pour représenter les champs non initialisés. Par exemple, 0 Est tout aussi valide un identifiant que toute autre valeur long.
  2. Les setters ont un comportement cohérent. Avant d'appeler build(), ils fonctionnent. Après l'appel de build(), ils lancent, quelles que soient les valeurs que vous transmettez. (Notez l'utilisation d'exceptions non vérifiées pour plus de commodité).
  3. La classe est marquée final, sinon un développeur pourrait étendre votre classe et remplacer les setters.

Mais cette approche a un inconvénient assez important: les développeurs utilisant cette classe ne peuvent pas savoir, au moment de la compilation , si un objet particulier a été initialisé ou non. Bien sûr, vous pouvez ajouter une méthode isBuilt() pour que les développeurs puissent vérifier, au moment de l'exécution , si l'objet est initialisé, mais ce serait tellement plus pratique de connaître ces informations au moment de la compilation. Pour cela, vous pouvez utiliser le modèle de générateur:

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

C'est bien mieux pour plusieurs raisons:

  1. Nous utilisons les champs final, donc le compilateur et les développeurs savent que ces valeurs ne peuvent pas être modifiées.
  2. La distinction entre les formes initialisées et non initialisées de l'objet est décrite via le système de type de Java. Il n'y a tout simplement pas de poseur pour appeler l'objet une fois qu'il a été construit.
  3. Les instances de la classe intégrée sont garanties thread-safe.

Oui, c'est un peu plus compliqué à entretenir, mais à mon humble avis, les avantages l'emportent sur le coût.

10
cambecc

Bibliothèque Guava (que je recommande très fortement) de Google est livré avec une classe qui résout très bien ce problème: SettableFuture . Cela fournit la sémantique à jeu unique que vous demandez, mais aussi beaucoup plus:

  1. La possibilité de communiquer une exception à la place (la méthode setException);
  2. La possibilité d'annuler l'événement de manière explicite;
  3. La possibilité d'enregistrer des écouteurs qui seront avertis lorsque la valeur est définie, une exception est notifiée ou le futur est annulé (l'interface ListenableFuture ).
  4. La famille de types Future en général utilisée pour la synchronisation entre les threads dans les programmes multithread, donc SettableFuture joue très bien avec ceux-ci.

Java 8 a également sa propre version: CompletableFuture .

2
Luis Casillas

Vous pouvez simplement ajouter un drapeau booléen, et dans votre setId (), définir/vérifier le booléen. Si j'ai bien compris la question, nous n'avons pas besoin de structure/motif complexe ici. Que dis-tu de ça:

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

}

de cette façon, si vous setId(0l); il pense que l'ID est également défini. Vous pouvez changer si cela ne convient pas à votre exigence de logique métier.

pas édité dans un IDE, désolé pour le problème typo/format, s'il y en avait ...

2
Kent

J'ai cette classe, similaire à AtomicReference de JDK, et je l'utilise principalement pour le code hérité:

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

J'ai une seule responsabilité et vérifie les appels get et set, donc il échoue tôt lorsque le code client l'abuse.

1
palacsint

Voici deux façons; le premier est fondamentalement le même que certains autres mentionnés dans d'autres réponses, mais il est ici pour comparer avec les secondes. Donc, la première façon, Once est d'avoir une valeur qui ne peut être définie qu'une seule fois en l'imposant dans le setter. Mon implémentation nécessite des valeurs non nulles, mais si vous souhaitez pouvoir définir la valeur null, vous devrez alors implémenter un indicateur booléen 'isSet' comme suggéré dans d'autres réponses.

La deuxième façon, Lazy, consiste à fournir une fonction qui fournit la valeur paresseusement une fois que le getter est appelé pour la première fois.

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

Vous pouvez donc les utiliser comme suit;

//
// 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
1
Ezward

Voici la solution que j'ai trouvée basée sur le mélange de certaines des réponses et des commentaires ci-dessus, en particulier celle de @KatjaChristiansen sur l'utilisation de l'assertion.

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

}

À la fin de la journée, je soupçonne que mon besoin de cela est une indication de mauvaises décisions de conception ailleurs, et je préfère trouver un moyen de créer l'objet uniquement lorsque je connais l'identifiant et de définir l'identifiant sur final. De cette façon, plus d'erreurs peuvent être détectées au moment de la compilation.

1
Richard Russell

essayez d'avoir un vérificateur d'int

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

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

    if (checker==1){
        id = 123456;
    } 
}
0
Frisco Freeze

// vous pouvez essayer ceci:

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