Default constructor vs. inline field initializatio

2019-01-01 15:05发布

问题:

What\'s the difference between a default constructor and just initializing an object\'s fields directly?

What reasons are there to prefer one of the following examples over the other?

Example 1

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

Example 2

public class Foo
{
    private int x;
    private String[] y;

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

回答1:

Initialisers are executed before constructor bodies. (Which has implications if you have both initialisers and constructors, the constructor code executes second and overrides an initialised value)

Initialisers are good when you always need the same initial value (like in your example, an array of given size, or integer of specific value), but it can work in your favour or against you:

If you have many constructors that initialise variables differently (i.e. with different values), then initialisers are useless because the changes will be overridden, and wasteful.

On the other hand, if you have many constructors that initialise with the same value then you can save lines of code (and make your code slightly more maintainable) by keeping initialisation in one place.

Like Michael said, there\'s a matter of taste involved as well - you might like to keep code in one place. Although if you have many constructors your code isn\'t in one place in any case, so I would favour initialisers.



回答2:

The reason to prefer Example one is that it\'s the same functionality for less code (which is always good).

Apart from that, no difference.

However, if you do have explicit constructors, I\'d prefer to put all initialization code into those (and chain them) rather than splitting it up between constructors and field initializers.



回答3:

Should we favor field initializer or constructor to give a default value to a field?

I will not consider exceptions that may rise during field instantiation and field lazy/eager instantiation that touch other concerns than readability and maintainability concerns.
For two codes that perform the same logic and produce the same result, the way with the best readability and maintainability should be favored.

TL;DR

  • choosing the first or the second option is before all a question of code organization, readability and maintainability.

  • keep a consistency in the way of choosing (it makes overall application code clearer)

  • don\'t hesitate to use field initializers to instantiate Collection fields to prevent NullPointerException

  • don\'t use field initializers for fields that may be overwritten by constructors

  • in classes with a single constructor, the field initializer way is generally more readable and less verbose

  • in classes with multiple constructors where constructors have no or very few coupling between them, the field initializer way is generally more readable and less verbose

  • in classes with multiple constructors where constructors have coupling between them, none of the two ways is really better but whatever the chosen way, combining it with the chaining constructor is the way (see use case 1).


OP Question

With a very simple code, the assignment during the field declaration seems better and it is.

This is less verbose and more straight :

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

than the constructor way :

public class Foo{
    private int x;
    private String[] y;

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

In real classes with so real specificities, things are different.
In fact, according to specificities encountered, a way, the other one or anyone of them should be favored.


More elaborated examples to illustrate

Study case 1

I will start from a simple Car class that I will update to illustrate these points.
Car declare 4 fields and some constructors that have relation between them.

1.Giving a default value in field intializers for all fields is undesirable

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

}

Default values specified in the fields declaration are not all reliable. Only name and origin fields have really default values.

nbSeat and color fields are first valued in their declaration, then these may be overwritten in the constructors with arguments.
It is error-prone and besides with this way of valuing fields, the class decreases its reliability level. How could rely on any default value assigned during the fields declaration while it has proven to be not reliable for two fields ?

2.Using constructor to value all fields and relying on constructors chaining is fine

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

}

This solution is really fine as it doesn\'t create duplication, it gathers all the logic at a place : the constructor with the maximum number of parameters.
It has a single drawback : the requirement to chain the call to another constructor.
But is it a drawback ?

3.Giving a default value in field intializers for fields which constructors don\'t assign to them a new value is better but has still duplication issues

By not valuing nbSeat and color fields in their declaration, we distinguish clearly fields with default values and fields without.

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

}

This solution is rather fine but it repeats the instantiation logic in each Car constructor contrary to the previous solution with constructor chaining.

In this simple example, we could start to understand the duplication issue but it seems only a little annoying.
In real cases, the duplication may be much important as the constructor may perform computation and validation.
Having a single constructor performing the instantiation logic becomes so very helpful.

So finally the assignment in the fields declaration will not always spare the constructor to delegate to another constructor.

Here is an improved version.

4.Giving a default value in field intializers for fields which constructors don\'t assign to them a new value and relying on constructors chaining is fine

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

}

Study case 2

We will modify the original Car class.
Now,Car declare 5 fields and 3 constructors that have no relation between them.

1.Using constructor to value fields with default values is undesirable

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

}

As we don\'t value name and origin fields in their declaration and we have not a common constructor naturally invoked by other constructors, we are forced to introduce a initDefaultValues() method and invoke it in each constructor. So we have not to forget to call this method.
Note that we could inline initDefaultValues() body in the no arg constructor but invoking this() with no arg from the other constructor is not necessary natural and may be easily forgotten :

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.Giving a default value in field initializers for fields which constructors don\'t assign to them a new value is fine

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          
  }

}

Here we don\'t need to have a initDefaultValues() method or a no arg constructor to call. Field initializers is perfect.


Conclusion

In any cases) Valuing fields in field initializers should not be performed for all fields but only for those that cannot be overwritten by a constructor.

Use case 1) In case of multiple constructors with common processing between them, it is primarily opinion-based.
Solution 2 (Using constructor to value all fields and relying on constructors chaining) and solution 4 (Giving a default value in field intializers for fields which constructors don\'t assign to them a new value and relying on constructors chaining) appear as the most readable, maintainable and robust solutions.

Use case 2) In case of multiple constructors with no common processing/relation between them as in the single constructor case, solution 2 (Giving a default value in field intializers for fields which constructors don\'t assign to them a new value) looks better.



回答4:

I prefer field initializers and resort to a default constructor when there is complex initialization logic to perform (e.g. populate a map, one ivar depends on another through a series of heuristic steps to execute, etc).

@Michael B said:

... I\'d prefer to put all initialization code into those (and chain them) rather than splitting it up between constructors and field initializers.

MichaelB (I bow to the 71+ K rep) makes perfect sense, but my tendency is to keep the simple initializations in the inline final initializers and do the complex part of the initializations in the constructor.



回答5:

The only difference that I can think of is that if you were to add another constructor

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

then in the first example, you would no longer have a default constructor whereas in the 2nd example, you would still have the default constructor (and could even make a call to it from inside our new constructor if we wanted to)