88 votes

Valeur par défaut à lombok. Comment initier la valeur par défaut avec le constructeur et le builder ?

J'ai un objet

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
    private int id;
    private String nick;
    private boolean isEmailConfirmed = true;
}

Et je l'initialise de deux façons

UserInfo ui = new UserInfo();
UserInfo ui2 = UserInfo.builder().build();

System.out.println("ui: " + ui.isEmailConfirmed());
System.out.println("ui2: " + ui2.isEmailConfirmed());

Voici le résultat

ui: true
ui2: false

Il semble que le constructeur n'obtienne pas de valeur par défaut. J'ajoute @Builder.Default à ma propriété et mon objet ressemble maintenant à ceci

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo { 
    private int id;
    private String nick;
    @Builder.Default
    private boolean isEmailConfirmed = true;
}

Voici la sortie de la console

ui: false
ui2: true

Comment puis-je faire en sorte que les deux soient true ?

1 votes

Il semble qu'il s'agisse d'un problème connu (cf. github.com/rzwitserloot/lombok/issues/1347 ).

69voto

A mon avis, ce n'est pas possible (sans avoir décomposé le code). Mais pourquoi n'implémentez-vous pas simplement le constructeur dont vous avez besoin ? Lombok est censé vous faciliter la vie, et si quelque chose ne fonctionne pas avec Lombok, faites-le simplement à l'ancienne.

@Data
@Builder
@AllArgsConstructor
public class UserInfo { 
    private int id;
    private String nick;
    @Builder.Default
    private boolean isEmailConfirmed = true;

    public UserInfo(){
        isEmailConfirmed = true;
    }
}

Sortie de console :

ui: true
ui2: true

Mise à jour
A partir de 01/2021, ce bug semble être corrigé à Lombok au moins pour les constructeurs générés. Notez qu'il y a toujours une question similaire quand vous mélangez Builder.Default et des constructeurs explicites.

4 votes

Attention ! Actuellement, ce @Builder.Default est cassé depuis la v1.16.16 = Il initialisera vos champs à null sans tenir compte de la valeur initialisée par défaut définie au niveau du champ... Voir Numéro ici

0 votes

La solution n'est pas parfaite car vous devez initialiser le champ deux fois. Une fois lors de la déclaration du champ, et la seconde fois dans un constructeur. Cela rend le code sujet aux erreurs. J'avais l'habitude d'utiliser celle-ci stackoverflow.com/a/50392165/2443502

1 votes

Pour rendre le code moins sujet aux erreurs, j'ai ajouté un test JUnit qui vérifie que le constructeur no-arg est bien écrit : assertThat(new UserInfo().equals(UserInfo.builder().build()).describedAs("s‌​ome properties marked with @Builder.Default are not properly set in the no-arg constructor").isTrue();

47voto

Marcin Kłopotek Points 1190

Depuis le @Builder.Default l'annotation est brisée je ne l'utiliserais pas du tout. Vous pouvez toutefois utiliser l'approche suivante en déplaçant l'élément @Builder du niveau de la classe au constructeur personnalisé :

@Data
@NoArgsConstructor
public class UserInfo {

    private int id;
    private String nick;
    private boolean isEmailConfirmed = true;

    @Builder
    @SuppressWarnings("unused")
    private UserInfo(int id, String nick, Boolean isEmailConfirmed) {
        this.id = id;
        this.nick = nick;
        this.isEmailConfirmed = Optional.ofNullable(isEmailConfirmed).orElse(this.isEmailConfirmed);
    }
}

De cette façon, vous vous assurez :

  • le champ isEmailConfirmed est initialisé à un seul endroit, ce qui rend le code moins sujet aux erreurs et plus facile à maintenir par la suite.
  • le site UserInfo sera initialisée de la même manière, que vous utilisiez un constructeur ou un constructeur sans argument.

En d'autres termes, la condition est la suivante true :

new UserInfo().equals(UserInfo.builder().build())

Dans ce cas, la création de l'objet est cohérente, quelle que soit la façon dont vous le créez. C'est particulièrement important lorsque votre classe est utilisée par un framework de mapping ou par un fournisseur JPA et que vous ne l'instanciez pas manuellement par un builder mais qu'un constructeur no-args est invoqué derrière votre dos pour créer l'instance.

L'approche décrit ci-dessus est très similaire mais présente un inconvénient majeur. Vous devez initialiser le champ à deux endroits, ce qui rend le code sujet aux erreurs car vous êtes tenu de conserver des valeurs cohérentes.

1 votes

Étant donné que Lombok a de nombreuses particularités qui, si vous n'en êtes pas conscient, peuvent vous apporter beaucoup plus de maux de tête que d'avantages, vous pouvez également essayer soit Immutables ( immutables.github.io ) ou Autovalue ( github.com/google/auto/blob/master/value/userguide/builders.md ).

3 votes

Tous les autres outils présentent un seul gros inconvénient : Ils génèrent une nouvelle classe. Ils ont sûrement moins de problèmes car ils ne sont que des processeurs d'annotations utilisant une API bien connue (et moche). Lombok doit fonctionner différemment et présente effectivement quelques bogues. Cependant, je l'utilise depuis ses débuts et j'en suis plutôt satisfait.

0 votes

L'annotation n'est pas totalement cassée, mais elle a des problèmes avec les objets non primitifs. Les valeurs par défaut fonctionnent pour moi avec Boolean à partir de la version 1.16.22

8voto

mav3n Points 166

Une autre façon est de définir votre propre getter méthode Remplacement de el lombok getter :

@Data
@Builder
@AllArgsConstructor
public class UserInfo { 
    private int id;
    private String nick;
    private Boolean isEmailConfirmed;

    public Boolean getIsEmailConfirmed(){
      return Objects.isNull(isEmailConfirmed) ? true : isEmailConfirmed;
    }
}

9 votes

Il est déconseillé d'avoir une logique dans les getters basés sur les propriétés. Imaginez la situation où vous voulez ajouter une méthode supplémentaire dans la classe, la méthode dépend du champ (isEmailConfirmed). Il peut être confus pour l'implémenteur d'utiliser un champ directement ou d'appeler le getter à la place. Les valeurs peuvent être différentes, ce qui peut entraîner des bogues.

0 votes

@MarcinKopotek Votre point est bon, mais je pense que cela peut être géré en utilisant l'annotation Deprecated sur ce champ privé pour restreindre son utilisation directe. De plus, si je suis l'implémenteur de cette classe et que je sais que j'ai besoin d'une valeur par défaut et que j'ai écrit un code pour cela, je devrais évidemment savoir que je ne dois pas utiliser ce champ directement pour l'utiliser sur une autre méthode logique dans cette classe.

2 votes

@SahilChhabra L'ambiguïté est mauvaise. Déprécié a un but particulier, sauf si vous voulez rendre les utilisateurs de votre code encore plus confus. Enfin, en supposant que que c'est "votre classe" et "vous devriez savoir" que ce champ ne doit pas être utilisé etc. etc. a un autre problème sérieux tenez vous en à la cohérence et votre code montrera du soin à la fois aux futurs développeurs et à ses utilisateurs finaux !

3voto

laurent Points 463

Voici mon approche :

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
public class UserInfo { 
    private int id;
    private String nick;
    private boolean isEmailConfirmed = true;
}

Et puis

UserInfo ui = new UserInfo().toBuilder().build();

0 votes

J'utilise la même approche mais au lieu d'utiliser : UserInfo ui = new UserInfo().toBuilder().build(); Je préfère utiliser : UserInfo ui = new UserInfo(); même chose avec moins de code.

-1voto

gavenkoa Points 6974

Constructeurs personnalisés et @Builder.Default ne travailleront probablement jamais ensemble.

Les auteurs du cadre veulent éviter les initialisations doubles pour @Builder .

Je réutilise .builder() por public static CLAZZ of(...) méthodes :

@Builder
public class Connection {
    private String user;
    private String pass;

    @Builder.Default
    private long timeout = 10_000;

    @Builder.Default
    private String port = "8080";

    public static Connection of(String user, String pass) {
        return Connection.builder()
            .user(user)
            .pass(pass)
            .build();
    }

    public static Connection of(String user, String pass, String port) {
        return Connection.builder()
            .user(user)
            .pass(pass)
            .port(port)
            .build();
    }

    public static Connection of(String user, String pass, String port, long timeout) {
        return Connection.builder()
            .user(user)
            .pass(pass)
            .port(port)
            .timeout(timeout)
            .build();
    }
}

Vérifiez la discussion correspondante : https://github.com/rzwitserloot/lombok/issues/1347

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X