Constructor predeterminado vs. inicialización de campo en línea

¿Cuál es la diferencia entre un constructor predeterminado y simplemente inicializar los campos de un objeto directamente?

¿Qué razones hay para preferir uno de los siguientes ejemplos sobre el otro?

Ejemplo 1

public class Foo { private int x = 5; private String[] y = new String[10]; } 

Ejemplo 2

 public class Foo { private int x; private String[] y; public Foo() { x = 5; y = new String[10]; } } 

Los inicializadores se ejecutan antes que los cuerpos constructores. (Lo cual tiene implicaciones si tiene inicializadores y constructores, el código de constructor se ejecuta en segundo lugar y anula un valor inicializado)

Los inicializadores son buenos cuando siempre se necesita el mismo valor inicial (como en su ejemplo, una matriz de tamaño determinado o un número entero de valor específico), pero puede funcionar a su favor o en su contra:

Si tiene muchos constructores que inicializan las variables de manera diferente (es decir, con valores diferentes), los inicializadores son inútiles porque los cambios serán anulados y desperdiciados.

Por otro lado, si tiene muchos constructores que inician con el mismo valor, puede guardar líneas de código (y hacer que su código sea ligeramente más fácil de mantener) manteniendo la inicialización en un solo lugar.

Como dijo Michael, también hay una cuestión de gustos: puede que le guste mantener el código en un solo lugar. Aunque si tiene muchos constructores, su código no está en un lugar en ningún caso, por lo que preferiría los inicializadores.

La razón para preferir el Ejemplo uno es que es la misma funcionalidad para menos código (que siempre es bueno).

Aparte de eso, no hay diferencia.

Sin embargo, si tiene constructores explícitos, preferiría poner todos los códigos de inicialización (y encadenarlos) en lugar de dividirlos entre constructores e inicializadores de campo.

Prefiero los inicializadores de campo y recurro a un constructor predeterminado cuando hay una lógica de inicialización compleja para realizar (por ejemplo, poblar un mapa, un ivar depende de otro a través de una serie de pasos heurísticos para ejecutar, etc.).

@Michael B dijo:

… Prefiero poner todo el código de inicialización en esos (y encadenarlos) en lugar de dividirlo entre constructores e inicializadores de campo.

MichaelB (me inclino ante el representante de 71+ K) tiene mucho sentido, pero mi tendencia es mantener las inicializaciones simples en los inicializadores finales en línea y hacer la parte compleja de las inicializaciones en el constructor.

¿Deberíamos favorecer que el inicializador de campo o el constructor den un valor predeterminado a un campo?

No consideraré las excepciones que puedan surgir durante la creación de instancias de campo y la creación de instancias vagas / impacientes en el campo que toquen otras inquietudes además de las inquietudes de legibilidad y mantenimiento.
Para dos códigos que realizan la misma lógica y producen el mismo resultado, se debe favorecer el camino con la mejor legibilidad y capacidad de mantenimiento.

TL; DR

  • elegir la primera o la segunda opción es, antes que nada, una cuestión de organización del código, legibilidad y facilidad de mantenimiento .

  • mantener una coherencia en la forma de elegir

  • no dude en utilizar los inicializadores de campo para crear instancias de los campos de NullPointerException para evitar NullPointerException

  • no use los inicializadores de campo para los campos que pueden ser sobrescritos por los constructores

  • en clases con un único constructor, el modo de inicialización de campo es generalmente más legible y menos detallado

  • en clases con múltiples constructores donde los constructores no tienen o tienen muy pocos acoplamientos entre ellos , el modo de inicialización de campo es generalmente más legible y menos detallado

  • en clases con múltiples constructores donde los constructores tienen un acoplamiento entre ellos , ninguna de las dos maneras es realmente mejor

  • en clases con múltiples constructores donde las clases declaran muchos campos , ya que los campos con valores predeterminados pueden estar dispersos, valorarlos en un constructor central puede sonar interesante


Pregunta OP

Con un código muy simple, la asignación durante la statement de campo parece mejor y lo es.

Esto es menos detallado y más directo:

 public class Foo { private int x = 5; private String[] y = new String[10]; } 

que la forma del constructor:

 public class Foo{ private int x; private String[] y; public Foo(){ x = 5; y = new String[10]; } } 

En clases reales con especificidades tan reales, las cosas son diferentes.
De hecho, de acuerdo con las especificidades encontradas, de una manera, el otro o cualquiera de ellos debería ser favorecido.


Ejemplos más elaborados para ilustrar

Caso de estudio 1

Comenzaré a partir de una clase simple de Car que actualizaré para ilustrar estos puntos.
Car declara 4 campos y algunos constructores que tienen relación entre ellos.

1. Dar un valor predeterminado en intializadores de campo para todos los campos es indeseable

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

Los valores predeterminados especificados en la statement de campos no son todos confiables. Solo los campos de name y origin tienen valores predeterminados.

nbSeat campos de nbSeat y color se valoran primero en su statement, y luego pueden sobreescribirse en los constructores con argumentos.
Es propenso a errores y, además de esta manera de valorar campos, la clase disminuye su nivel de fiabilidad. ¿Cómo se puede confiar en cualquier valor predeterminado asignado durante la statement de campos mientras que ha demostrado no ser confiable para dos campos?

2. Usar al constructor para valorar todos los campos y confiar en el encadenamiento de constructores está 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; } } 

Esta solución es realmente buena ya que no crea duplicación, reúne toda la lógica en un lugar: el constructor con el número máximo de parámetros.
Tiene un único inconveniente: el requisito de encadenar la llamada a otro constructor.
Pero, ¿es un inconveniente?

3. Dar un valor predeterminado en los intializadores de campo para los campos que los constructores no les asignan un nuevo valor es mejor, pero aún tiene problemas de duplicación

Al no valorar los campos nbSeat y color en su statement, distinguimos claramente los campos con los valores predeterminados y los campos sin.

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

Esta solución es bastante buena, pero repite la lógica de creación de instancias en cada constructor de Car contrariamente a la solución anterior con el encadenamiento de constructores.

En este simple ejemplo, podríamos comenzar a entender el problema de la duplicación, pero parece un poco molesto.
En casos reales, la duplicación puede ser muy importante ya que el constructor puede realizar el cálculo y la validación.
Tener un único constructor que realice la lógica de creación de instancias resulta muy útil.

Así que, finalmente, la asignación en la statement de los campos no siempre le ahorrará al constructor delegar en otro constructor.

Aquí hay una versión mejorada.

4. Dar un valor predeterminado en intializadores de campo para campos que los constructores no les asignan un nuevo valor y confiar en el encadenamiento de constructores está bien

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

Caso de estudio 2

Modificaremos la clase de Car original.
Ahora, Car declara 5 campos y 3 constructores que no tienen relación entre ellos.

1. Usar el constructor para valorar campos con valores predeterminados es indeseable

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

Como no valoramos los campos de name y origin en su statement y no tenemos un constructor común invocado de forma natural por otros constructores, estamos obligados a introducir un método initDefaultValues() e invocarlo en cada constructor. Entonces no debemos olvidarnos de llamar a este método.
Tenga en cuenta que podríamos initDefaultValues() cuerpo de initDefaultValues() en el constructor no arg pero invocar this() sin arg del otro constructor no es necesario y se puede olvidar fácilmente:

 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. Dar un valor predeterminado en los inicializadores de campo para los campos que los constructores no les asignan un nuevo valor está bien

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

Aquí no necesitamos tener un método initDefaultValues() o un constructor no arg para llamar. Inicializadores de campo es perfecto.


Conclusión

En cualquier caso) Los campos de valorización en los inicializadores de campo no deberían realizarse para todos los campos, sino solo para aquellos que no pueden ser sobrescritos por un constructor.

Caso de uso 1) En el caso de constructores múltiples con procesamiento común entre ellos, se basa principalmente en la opinión.
La solución 2 ( Usar constructor para valorar todos los campos y confiar en el encadenamiento de constructores ) y la solución 4 ( Dar un valor predeterminado en intializadores de campo para campos que los constructores no les asignan un nuevo valor y confiar en el encadenamiento de constructores ) aparecen como los más legibles , soluciones sostenibles y robustas.

Caso de uso 2) En el caso de constructores múltiples sin procesamiento / relación común entre ellos como en el caso del constructor único, la solución 2 ( dando un valor predeterminado en intializadores de campo para los campos que los constructores no les asignan un nuevo valor ) se ve mejor .

La única diferencia que puedo pensar es que si agregas otro constructor

 public Foo(int inX){ x = inX; } 

luego, en el primer ejemplo, ya no tendrías un constructor predeterminado, mientras que en el segundo ejemplo, todavía tendrías el constructor predeterminado (e incluso podrías hacer una llamada desde nuestro nuevo constructor si quisiéramos)