Quelle est la différence entre un constructeur par défaut et l'initialisation directe des champs d'un objet?
Quelles sont les raisons de préférer l'un des exemples suivants à l'autre?
public class Foo
{
private int x = 5;
private String[] y = new String[10];
}
public class Foo
{
private int x;
private String[] y;
public Foo()
{
x = 5;
y = new String[10];
}
}
Les initiateurs sont exécutés devant les corps des constructeurs. (Ce qui a des implications si vous avez à la fois des initialiseurs et des constructeurs, le code du constructeur s'exécute en second et remplace une valeur initialisée)
Les initiateurs sont bons lorsque vous avez toujours besoin de la même valeur initiale (comme dans votre exemple, un tableau de taille donnée ou un entier de valeur spécifique), mais cela peut fonctionner en votre faveur ou contre vous:
Si vous avez de nombreux constructeurs qui initialisent des variables différemment (c'est-à-dire avec des valeurs différentes), les initialiseurs sont inutiles car les modifications seront remplacées et inutiles.
D'un autre côté, si vous avez de nombreux constructeurs qui s'initialisent avec la même valeur, vous pouvez enregistrer des lignes de code (et rendre votre code légèrement plus facile à maintenir) en gardant l'initialisation en un seul endroit.
Comme Michael l'a dit, il y a aussi une question de goût - vous voudrez peut-être garder le code en un seul endroit. Bien que si vous avez plusieurs constructeurs, votre code ne soit en aucun cas au même endroit, donc je privilégierais les initialiseurs.
La raison de préférer l'exemple 1 est qu'il s'agit de la même fonctionnalité pour moins de code (ce qui est toujours bon).
A part ça, aucune différence.
Cependant, si vous avez des constructeurs explicites, je préférerais mettre tout le code d'initialisation dans ceux-ci (et les enchaîner) plutôt que de le diviser entre constructeurs et initialiseurs de champ.
Faut-il privilégier l'initialiseur de champ ou le constructeur pour donner une valeur par défaut à un champ?
Je ne prendrai pas en compte les exceptions qui peuvent survenir lors de l'instanciation de champ et l'instanciation paresseuse/désirée de champ qui touchent à d'autres problèmes que les problèmes de lisibilité et de maintenabilité.
Pour deux codes qui exécutent la même logique et produisent le même résultat, la voie avec la meilleure lisibilité et maintenabilité doit être privilégiée.
choisir la première ou la deuxième option est avant tout une question d'organisation du code , lisibilité et maintenabilité .
garder une cohérence dans la façon de choisir (cela rend le code d'application global plus clair)
n'hésitez pas à utiliser les initialiseurs de champs pour instancier Collection
champs pour éviter NullPointerException
n'utilisez pas d'initialiseurs de champs pour les champs qui peuvent être écrasés par les constructeurs
dans les classes avec un seul constructeur, la méthode d'initialisation du champ est généralement plus lisible et moins verbeuse
dans les classes avec plusieurs constructeurs où les constructeurs n'ont pas ou très peu de couplage entre eux , la méthode d'initialisation du champ est généralement plus lisible et moins verbeuse
dans les classes avec plusieurs constructeurs où les constructeurs ont un couplage entre eux , aucune des deux façons n'est vraiment meilleure mais quelle que soit la manière choisie, en la combinant avec le constructeur de chaînage est le chemin (voir cas d'utilisation 1).
Avec un code très simple, l'affectation lors de la déclaration de champ semble meilleure et elle l'est.
C'est moins verbeux et plus direct:
public class Foo {
private int x = 5;
private String[] y = new String[10];
}
que la façon constructeur:
public class Foo{
private int x;
private String[] y;
public Foo(){
x = 5;
y = new String[10];
}
}
Dans les classes réelles avec des spécificités si réelles, les choses sont différentes.
En effet, selon les spécificités rencontrées, une voie, l'autre ou l'une d'entre elles doit être privilégiée.
Cas d'étude 1
Je vais partir d'une simple classe Car
que je mettrai à jour pour illustrer ces points.Car
déclare 4 champs et certains constructeurs qui ont une relation entre eux.
1.Il n'est pas souhaitable de donner une valeur par défaut dans les initialiseurs de champ pour tous les champs
public class Car {
private String name = "Super car";
private String Origin = "Mars";
private int nbSeat = 5;
private Color color = Color.black;
...
...
// Other fields
...
public Car() {
}
public Car(int nbSeat) {
this.nbSeat = nbSeat;
}
public Car(int nbSeat, Color color) {
this.nbSeat = nbSeat;
this.color = color;
}
}
Les valeurs par défaut spécifiées dans la déclaration des champs ne sont pas toutes fiables. Seuls les champs name
et Origin
ont vraiment des valeurs par défaut.
Les champs nbSeat
et color
sont d'abord valorisés dans leur déclaration, puis ils peuvent être écrasés dans les constructeurs avec des arguments.
Elle est sujette aux erreurs et en plus de cette façon d'évaluer les champs, la classe diminue son niveau de fiabilité. Comment pourrait-on s'appuyer sur une valeur par défaut attribuée lors de la déclaration des champs alors qu'elle s'est avérée non fiable pour deux champs?
2.Utiliser un constructeur pour évaluer tous les champs et s'appuyer sur le chaînage des constructeurs est très bien
public class Car {
private String name;
private String Origin;
private int nbSeat;
private Color color;
...
...
// Other fields
...
public Car() {
this(5, Color.black);
}
public Car(int nbSeat) {
this(nbSeat, Color.black);
}
public Car(int nbSeat, Color color) {
this.name = "Super car";
this.Origin = "Mars";
this.nbSeat = nbSeat;
this.color = color;
}
}
Cette solution est très bien car elle ne crée pas de duplication, elle rassemble toute la logique à un endroit: le constructeur avec le nombre maximum de paramètres.
Il a un seul inconvénient: l'exigence de chaîner l'appel à un autre constructeur.
Mais est-ce un inconvénient?
.Donner une valeur par défaut dans les initialiseurs de champs pour les champs auxquels les constructeurs ne leur attribuent pas de nouvelle valeur est mieux mais a toujours des problèmes de duplication
En ne valorisant pas les champs nbSeat
et color
dans leur déclaration, nous distinguons clairement les champs avec des valeurs par défaut et les champs sans.
public class Car {
private String name = "Super car";
private String Origin = "Mars";
private int nbSeat;
private Color color;
...
...
// Other fields
...
public Car() {
nbSeat = 5;
color = Color.black;
}
public Car(int nbSeat) {
this.nbSeat = nbSeat;
color = Color.black;
}
public Car(int nbSeat, Color color) {
this.nbSeat = nbSeat;
this.color = color;
}
}
Cette solution est plutôt fine mais répète la logique d'instanciation dans chaque constructeur Car
contrairement à la solution précédente avec chaînage constructeur.
Dans cet exemple simple, nous pourrions commencer à comprendre le problème de duplication mais cela semble seulement un peu ennuyeux.
Dans des cas réels, la duplication peut être très importante car le constructeur peut effectuer le calcul et la validation.
Avoir un seul constructeur exécutant la logique d'instanciation devient donc très utile.
Donc finalement l'affectation dans la déclaration des champs n'épargnera pas toujours le constructeur à déléguer à un autre constructeur.
Voici une version améliorée.
4.Donner une valeur par défaut dans les initialiseurs de champs pour les champs auxquels les constructeurs ne leur attribuent pas de nouvelle valeur et s'appuyer sur le chaînage des constructeurs est correct
public class Car {
private String name = "Super car";
private String Origin = "Mars";
private int nbSeat;
private Color color;
...
...
// Other fields
...
public Car() {
this(5, Color.black);
}
public Car(int nbSeat) {
this(nbSeat, Color.black);
}
public Car(int nbSeat, Color color) {
// assignment at a single place
this.nbSeat = nbSeat;
this.color = color;
// validation rules at a single place
...
}
}
Cas d'étude 2
Nous allons modifier la classe Car
d'origine.
Maintenant, Car
déclare 5 champs et 3 constructeurs qui n'ont aucune relation entre eux.
1.Utiliser un constructeur pour évaluer des champs avec des valeurs par défaut n'est pas souhaitable
public class Car {
private String name;
private String Origin;
private int nbSeat;
private Color color;
private Car replacingCar;
...
...
// Other fields
...
public Car() {
initDefaultValues();
}
public Car(int nbSeat, Color color) {
initDefaultValues();
this.nbSeat = nbSeat;
this.color = color;
}
public Car(Car replacingCar) {
initDefaultValues();
this.replacingCar = replacingCar;
// specific validation rules
}
private void initDefaultValues() {
name = "Super car";
Origin = "Mars";
}
}
Comme nous n'évaluons pas les champs name
et Origin
dans leur déclaration et que nous n'avons pas de constructeur commun naturellement invoqué par d'autres constructeurs, nous sommes obligés d'introduire une méthode initDefaultValues()
et l'invoquer dans chaque constructeur. Il ne faut donc pas oublier d'appeler cette méthode.
Notez que nous pourrions incorporer le corps de initDefaultValues()
dans le constructeur no arg mais invoquer this()
sans arg de l'autre constructeur n'est pas nécessairement naturel et peut être facilement oublié:
public class Car {
private String name;
private String Origin;
private int nbSeat;
private Color color;
private Car replacingCar;
...
...
// Other fields
...
public Car() {
name = "Super car";
Origin = "Mars";
}
public Car(int nbSeat, Color color) {
this();
this.nbSeat = nbSeat;
this.color = color;
}
public Car(Car replacingCar) {
this();
this.replacingCar = replacingCar;
// specific validation rules
}
}
2.Donner une valeur par défaut dans les initialiseurs de champs pour les champs auxquels les constructeurs ne leur attribuent pas de nouvelle valeur est correct
public class Car {
private String name = "Super car";
private String Origin = "Mars";
private int nbSeat;
private Color color;
private Car replacingCar;
...
...
// Other fields
...
public Car() {
}
public Car(int nbSeat, Color color) {
this.nbSeat = nbSeat;
this.color = color;
}
public Car(Car replacingCar) {
this.replacingCar = replacingCar;
// specific validation rules
}
}
Ici, nous n'avons pas besoin d'avoir une méthode initDefaultValues()
ou un constructeur no arg pour appeler. Les initialiseurs de champ sont parfaits.
Conclusion
Dans tous les cas) La valorisation des champs dans les initialiseurs de champs ne doit pas être effectuée pour tous les champs mais uniquement pour ceux qui ne peuvent pas être écrasés par un constructeur.
Cas d'utilisation 1) Dans le cas de plusieurs constructeurs avec un traitement commun entre eux, il est principalement basé sur l'opinion.
Solution 2 (tiliser le constructeur pour évaluer tous les champs et s'appuyer sur le chaînage des constructeurs) et solution 4 (Donner une valeur par défaut dans les initialiseurs de champs pour les champs que les constructeurs n'affectent pas pour eux une nouvelle valeur et s'appuyer sur les constructeurs enchaînant) apparaissent comme les solutions les plus lisibles, maintenables et robustes.
Cas d'utilisation 2) Dans le cas de plusieurs constructeurs sans traitement/relation communs entre eux comme dans le cas d'un constructeur unique, solution 2 (Donner une valeur par défaut dans les initialiseurs de champ pour les champs que les constructeurs ne donnent pas) ne leur attribuez pas de nouvelle valeur) est plus joli.
Je préfère les initialiseurs de champ et recourir à un constructeur par défaut lorsqu'il y a une logique d'initialisation complexe à effectuer (par exemple, remplir une carte, un ivar dépend d'un autre via une série d'étapes heuristiques à exécuter, etc.).
@Michael B a déclaré:
... Je préfère mettre tout le code d'initialisation dans ceux-ci (et les enchaîner) plutôt que de le diviser entre les constructeurs et les initialiseurs de champ.
MichaelB (je m'incline devant le représentant 71+ K) est parfaitement logique, mais ma tendance est de conserver les initialisations simples dans les initialiseurs finaux en ligne et de faire la partie complexe des initialisations dans le constructeur.
La seule différence à laquelle je peux penser est que si vous deviez ajouter un autre constructeur
public Foo(int inX){
x = inX;
}
alors dans le premier exemple, vous n'auriez plus de constructeur par défaut alors que dans le 2ème exemple, vous auriez toujours le constructeur par défaut (et pourriez même l'appeler depuis l'intérieur de notre nouveau constructeur si nous le voulions)