Hi! Today we'll consider a very important topic that concerns our objects. Without exaggeration, we can say you'll use this topic in real life every day!
We're talking about constructors.
This may be the first time you're hearing this term, but you've actually already used constructors. You just didn't realize it :) We convince ourselves of this later.
What in the world are constructors and why are they needed?
Let's consider two examples.
public class Car {
String model;
int maxSpeed;
public static void main(String[] args) {
Car bugatti = new Car();
bugatti.model = "Bugatti Veyron";
bugatti.maxSpeed = 378;
}
}
We created our car, and set its model and maximum speed.
But the Car
object would obviously not have 2 fields in a real project. For example, it might have 16 fields!
public class Car {
String model;// model
int maxSpeed;// maximum speed
int wheels;// wheel width
double engineVolume;// engine volume
String color;// color
int productionYear;// production year
String ownerFirstName;// first name of owner
String ownerLastName;// last name of owner
long price;// price
boolean isNew;// flag indicating whether car is new
int seatsInTheCar;// number of seats in the car
String cabinMaterial;// interior material
boolean insurance;// flag indicating whether car is insured
String manufacturerCountry;// manufacturer country
int trunkVolume;// size of the trunk
int accelerationTo100km;// how long it takes to accelerate to 100 km/h (in seconds)
public static void main(String[] args) {
Car bugatti = new Car();
bugatti.color = "blue";
bugatti.accelerationTo100km = 3;
bugatti.engineVolume = 6.3;
bugatti.manufacturerCountry = "Italy";
bugatti.ownerFirstName = "Amigo";
bugatti.productionYear = 2016;
bugatti.insurance = true;
bugatti.price = 2000000;
bugatti.isNew = false;
bugatti.seatsInTheCar = 2;
bugatti.maxSpeed = 378;
bugatti.model = "Bugatti Veyron";
}
}
We've created a new Car
object.
There's one problem: we have 16 fields, but we only initialized 12! Look at the code now and try to find the fields we forgot! Not so easy, huh?
In this situation, a programmer can easily make a mistake and fail to initialize some field. As a result, the program will behave incorrectly:
public class Car {
String model;// model
int maxSpeed;// maximum speed
int wheels;// wheel width
double engineVolume;// engine volume
String color;// color
int productionYear;// production year
String ownerFirstName;// first name of owner
String ownerLastName;// last name of owner
long price;// price
boolean isNew;// flag indicating whether car is new
int seatsInTheCar;// number of seats in the car
String cabinMaterial;// interior material
boolean insurance;// flag indicating whether car is insured
String manufacturerCountry;// manufacturer country
int trunkVolume;// size of the trunk
int accelerationTo100km;// how long it takes to accelerate to 100 km/h (in seconds)
public static void main(String[] args) {
Car bugatti = new Car();
bugatti.color = "blue";
bugatti.accelerationTo100km = 3;
bugatti.engineVolume = 6.3;
bugatti.manufacturerCountry = "Italy";
bugatti.ownerFirstName = "Amigo";
bugatti.productionYear = 2016;
bugatti.insurance = true;
bugatti.price = 2000000;
bugatti.isNew = false;
bugatti.seatsInTheCar = 2;
bugatti.maxSpeed = 378;
bugatti.model = "Bugatti Veyron";
System.out.println("Model: Bugatti Veyron. Engine volume: " + bugatti.engineVolume + ". Trunk volume: " + bugatti.trunkVolume + ". Cabin material: " + bugatti.cabinMaterial +
". Wheel width: " + bugatti.wheels + ". Purchased in 2018 by Mr. " + bugatti.ownerLastName);
}
}
Console output:
Model: Bugatti Veyron. Engine volume: 6.3. Trunk volume: 0. Cabin material: null. Wheel width: 0. Purchased in 2018 by Mr. null
Your buyer, who gave up $2 million for the car, obviously won't like being called "Mr. null"!
But seriously, the bottom line is that our program created an object incorrectly: a car with a wheel width of 0 (i.e. no wheels at all), a missing trunk, a cabin made of an unknown material, and above all, an undefined owner.
You can only imagine how such a mistake can "go off" when the program is running!
We need to avoid such situations somehow. We need to restrict our program: when creating a new Car
object, we want the fields, such as the model and maximum speed, to always be specified. Otherwise, we want to prevent the creation of the object.
Constructors handle this task with ease.
They got their name for a reason. The constructor creates a kind of class "skeleton" that each new object must match. For convenience, let's go back to the simpler version of the Car
class with two fields.
Considering our requirements, the Car
class's constructor will look like this:
public Car(String model, int maxSpeed) {
this.model = model;
this.maxSpeed = maxSpeed;
}
// And creating an object now looks like this:
public static void main(String[] args) {
Car bugatti = new Car("Bugatti Veyron", 378);
}
Note how a constructor is declared.
It's similar to a regular method, but it doesn't have a return type. Moreover, the constructor specifies the class name (Car
) starting with an uppercase letter.
Additionally, the constructor is used with a keyword that is new for you: this
.
The keyword this
is for indicating a particular object.
The code in the constructor
public Car(String model, int maxSpeed) {
this.model = model;
this.maxSpeed = maxSpeed;
}
can be interpreted almost verbatim:
"The model
for this car (the one we're creating now) is the model
argument passed to the constructor. The maxSpeed
for this car (the one we're creating) is the maxSpeed
argument passed to the constructor."
And that's just what happens:
public class Car {
String model;
int maxSpeed;
public Car(String model, int maxSpeed) {
this.model = model;
this.maxSpeed = maxSpeed;
}
public static void main(String[] args) {
Car bugatti = new Car("Bugatti Veyron", 378);
System.out.println(bugatti.model);
System.out.println(bugatti.maxSpeed);
}
}
Console output:
Bugatti Veyron
378
The constructor correctly assigned the required values.
You may have noticed that a constructor is very similar to an ordinary method! So it is. A constructor is really a method, but with specific features :)
Just like with methods, we passed arguments to our constructor. And just like calling a method, calling a constructor won't work unless you specify them:
public class Car {
String model;
int maxSpeed;
public Car(String model, int maxSpeed) {
this.model = model;
this.maxSpeed = maxSpeed;
}
public static void main(String[] args) {
Car bugatti = new Car(); // Error!
}
}
You can see that the constructor accomplishes what we were trying to achieve. Now you can't create a car without a speed or model!
The similarity between constructors and methods doesn't end here. Just like methods, constructors can be overloaded.
Imagine you have 2 pet cats at home. You got one of them as a kitten. But the second one you took in from the street when it was already grown, and you don't know exactly how old it is.
In this case, we want our program to be able to create two kinds of cats: those with a name and age (for the first cat), and those with only a name (for the second cat).
For this, we will overload the constructor:
public class Cat {
String name;
int age;
// For the first cat
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
// For the second cat
public Cat(String name) {
this.name = name;
}
public static void main(String[] args) {
Cat smudge = new Cat("Smudge", 5);
Cat streetCatNamedBob = new Cat("Bob");
}
}
In addition to the original constructor with "name" and "age" parameters, we added one more with only a name parameter. In precisely the same way that we overloaded methods in previous lessons. Now we can create both kinds of cats :)
Remember that at the beginning of the lesson we said that you've already used constructors without realizing it?
We meant what we said. The fact is that every class in Java has what is called a default constructor.
It doesn't take any arguments, but it is invoked every time you create any object of any class.
public class Cat {
public static void main(String[] args) {
Cat smudge = new Cat(); // The default constructor is invoked here
}
}
At first glance, it's invisible. We created an object, so what? Where is the constructor doing anything here?
To see it, let's explicitly write an empty constructor for the Cat
class. We'll display some phrase inside it. If the phrase is displayed, then the constructor was invoked.
public class Cat {
public Cat() {
System.out.println("A cat has been created!");
}
public static void main(String[] args) {
Cat smudge = new Cat(); // The default constructor is invoked here
}
}
Console output:
A cat has been created!
There's the confirmation! The default constructor is always invisibly present in your classes.
But you need to know one more thing about it.
The default constructor is eliminated from a class once you create a constructor with arguments.
In fact, we've already seen proof of this above. It was in this code:
public class Cat {
String name;
int age;
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
Cat smudge = new Cat(); //Error!
}
}
We couldn't create a Cat
without a name and age, because we declared a Cat
constructor with string
and int
parameters. This caused the default constructor to immediately vanish from the class.
So be sure to remember that if you need several constructors in your class, including a no-argument constructor, you'll have to declare it separately.
For example, suppose we are creating a program for a veterinary clinic. Our clinic wants to do good deeds and help homeless kittens whose names and ages are unknown.
Then our code should look like this:
public class Cat {
String name;
int age;
// For cats with owners
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
// For street cats
public Cat() {
}
public static void main(String[] args) {
Cat smudge = new Cat("Smudge", 5);
Cat streetCat = new Cat();
}
}
Now that we have written an explicit default constructor, we can create both types of cats :)
As with any method, the order of arguments passed to a constructor is very important.
Let's swap the name
and age
arguments in our constructor.
public class Cat {
String name;
int age;
public Cat(int age, String name) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
Cat smudge = new Cat("Smudge", 10); // Error!
}
}
An error! The constructor clearly stipulates that when a Cat
object is created, it must be passed a number and a string, in this order. So, our code doesn't work.
Be sure to remember this and keep it in mind when declaring your own classes:
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
public Cat(int age, String name) {
this.age = age;
this.name = name;
}
These are two totally different constructors!
If we were to express in a single sentence the answer to the question "Why do I need a constructor?", we might say, "To ensure that objects always have a valid state". When you use constructors, all your variables will be correctly initialized. Your programs won't have any cars with a speed of 0 or any other "invalid" objects.
Their main benefit is for the programmer.
If you initialize fields manually (after creating an object), there's a great risk that you'll miss something and introduce a bug. But this won't happen with a constructor: if you fail to pass all the required arguments or you pass the wrong types of arguments, the compiler will immediately register an error.
We must also separately say that you should not put your program's logic inside a constructor. This is what methods are for. Methods are where you should define all the required functionality.
Let's see why adding logic to a constructor is a bad idea:
public class CarFactory {
String name;
int age;
int carsCount;
public CarFactory(String name, int age, int carsCount) {
this.name = name;
this.age = age;
this.carsCount = carsCount;
System.out.println("Our car factory is called " + this.name);
System.out.println("It was founded " + this.age + " years ago" );
System.out.println("Since that time, it has produced " + this.carsCount + " cars");
System.out.println("On average, it produces " + (this.carsCount/this.age) + " cars per year");
}
public static void main(String[] args) {
CarFactory ford = new CarFactory("Ford", 115 , 50000000);
}
}
We have a CarFactory
class that describes the car factory. Inside the constructor, we initialize all the fields and include some logic: we display some information about the factory.
It seems like there's nothing bad about this. The program works fine.
Console output:
Our car factory is called Ford
It was founded 115 years ago
Since that time, it has produced 50000000 cars
On average, it produces 434782 cars per year
But we've actually laid a time-delayed mine. And this sort of code can very easily lead to errors.
Suppose that now we are talking not about Ford, but about a new factory called "Amigo Motors", which has existed for less than a year and has produced 1000 cars:
public class CarFactory {
String name;
int age;
int carsCount;
public CarFactory(String name, int age, int carsCount) {
this.name = name;
this.age = age;
this.carsCount = carsCount;
System.out.println("Our car factor is called " + this.name);
System.out.println("It was founded " + this.age + " years ago" );
System.out.println("Since that time, it has produced " + this.carsCount + " cars");
System.out.println("On average, it produces " + (this.carsCount/this.age) + " cars per year");
}
public static void main(String[] args) {
CarFactory ford = new CarFactory("Amigo Motors", 0 , 1000);
}
}
Console output:
Our car factory is called Amigo Motors
Exception in thread "main" java.lang.ArithmeticException: / by zero
It was founded 0 years ago
Since that time, it has produced 1000 cars
at CarFactory.(CarFactory.java:15)
at CarFactory.main(CarFactory.java:23)
Process finished with exit code 1
Boom! The program ends with some kind of incomprehensible error. Can you try to guess the cause?
The problem is in the logic we put into the constructor. More specifically, this line:
System.out.println("On average, it produces " + (this.carsCount/this.age) + " cars per year");
Here you're performing a calculation and dividing the number of cars produced by the factory's age. And since our factory is new (i.e. it's 0 years old), we divide by 0, which we can't do in math.
Consequently, the program terminates with an error.
What should we have done?
Put all the logic in a separate method. Let's call it printFactoryInfo()
. You can pass a CarFactory
object to it as an argument. You can put all the logic there, and simultaneously handle potential errors (like ours involving zero years).
To each his own. Constructors are needed to set valid object state. We have methods for business logic. Don't mix one with the other.
Was published on CodeGym blog .