If you read my article – How to create an immutable class – I have mentioned there, to use final keyword to make your class immutable.
But.. Why?
- An immutable class has only one version on all the system. Like String class in Java. You can not create another version of it. The Behaviour of String class is consistent on all the machines.
- You can not modify the current behaviour of a class. And make it behave in an unexpected way, or make it do things that it was not supposed to do.
- It’s easy to maintain.
- It make perfect candidate – to be used as a key in any hashbased datastructure like Hashmap. or HashSet.
We will try to understand it through by writing Java code example, on why we need to make an immutable class final.
So, Here we have a food class, which returns different types of food, at different times of day.
public class Food{ private String breakfast = "Poori-Sabji"; private String lunch = "Chawal-Daal"; private String dinner = "Roti-Sabji"; public Food(String breakfast, String lunch, String dinner) { this.breakfast = breakfast; this.lunch = lunch; this.dinner = dinner; } public String getBreakfast() { return breakfast; } public String getLunch () { return lunch; } public String getDinner() { return dinner; } }
This Food class is immutable, if we ignore the fact, for now, that it is not declared final.
This class has String type private-member variable, and has no setter method. All values are passed through constructor alone.
String is an immutable class, and without any setter-method, this class is effectively immutable.
And we have a Buffet class, which uses Food-class to serve food.
public class Buffet { private void serve(Food food){ System.out.println("---- " + food.getClass().getName() + " --------------"); System.out.println("Serving " + food.getBreakfast()); System.out.println("Serving " + food.getLunch()); System.out.println("Serving " + food.getDinner()); } public static void main(String... args) { Buffet buffet = new Buffet(); buffet.serve(new Food("Poori-Sabji", "Chawal-Daal", "Roti-Sabji")); } }
Output of above code –
---- Food -------------- Serving Poori-Sabji Serving Chawal-Daal Serving Roti-Sabji
Now, we are going to make an evil change. This will have a lethal imapct on Buffet class.
We define a class Poison.
public class Poison extends Food { public Poison(String breakfast, String lunch, String dinner){ super(breakfast, lunch, dinner); } @Override public String getLunch() { return "POISON"; } }
In the Poison class, we extend Food-class and @Override
the getLunch method. Now, the getLunch method returns "POISON"
.
Let us modify the main method of Buffet class.
public static void main(String... args) { Buffet buffet = new Buffet(); buffet.serve(new Poison("Poori-Sabji", "Chawal-Daal", "Roti-Sabji")); }
Now, we are passing an instance of Poison class.
Output :-
---- Poison -------------- Serving Poori-Sabji Serving POISON Serving Roti-Sabji
So, next time when a person is eating lunch, we are sure, he is not going to make home alive.
Because, during lunch he is served Poison.
I was able to play, easily, with an unsuspecting function and pass it an instance which acted contrary to the way it was supposed to act.
Can we handle it? yes !!!
Method 1
We can handle it by adding food instanceof Food
as an if-statement in eat
method of Buffet-class. But, that’s not the right way.
Method 2
We can declare all the getter methods as final in Food-class. Everytime, we add a new member variable, we have to declare it’s getter as final.
public final String getBreakfast() { return breakfast; } public final String getLunch () { return lunch; } public final String getDinner() { return dinner; }
Method 3
We can declare Food class as final. This way we won’t have to make individual getter methods as final. If class is declared final, then all of it’s method becomes effective final, immediately.
Final Food class code is here.
public final class Food{ private String breakfast = "Poori-Sabji"; private String lunch = "Chawal-Daal"; private String dinner = "Roti-Sabji"; public Food(String breakfast, String lunch, String dinner) { this.breakfast = breakfast; this.lunch = lunch; this.dinner = dinner; } public String getBreakfast() { return breakfast; } public String getLunch () { return lunch; } public String getDinner() { return dinner; } }
Remember, now the Poison class will give compile time exception, because Food can not be extended anymore.