Inheritance and composition are two programming techniques developers use to establish relationships between classes and objects. Whereas inheritance derives one class from another, composition defines a class as the sum of its parts.
Classes and objects created through inheritance are tightly coupled because changing the parent or superclass in an inheritance relationship risks breaking your code. Classes and objects created through composition are loosely coupled, meaning that you can more easily change the component parts without breaking your code.
Because loosely coupled code offers more flexibility, many developers have learned that composition is a better technique than inheritance, but the truth is more complex. Choosing a programming tool is similar to choosing the correct kitchen tool: You wouldn't use a butter knife to cut vegetables, and in the same way you shouldn't choose composition for every programming scenario.
In this Java Challenger you'll learn the difference between inheritance and composition and how to decide which is correct for your program. Next, I'll introduce you to several important but challenging aspects of Java inheritance: method overriding, the super
keyword, and type casting. Finally, you'll test what you've learned by working through an inheritance example line by line to determine what the output should be.
When to use inheritance in Java
In object-oriented programming, we can use inheritance when we know there is an "is a" relationship between a child and its parent class. Some examples would be:
- A person is a human.
- A cat is an animal.
- A car is a vehicle.
In each case, the child or subclass is a specialized version of the parent or superclass. Inheriting from the superclass is an example of code reuse. To better understand this relationship, take a moment to study the Car
class, which inherits from Vehicle
:
class Vehicle {
String brand;
String color;
double weight;
double speed;
void move() {
System.out.println("The vehicle is moving");
}
}
public class Car extends Vehicle {
String licensePlateNumber;
String owner;
String bodyStyle;
public static void main(String... inheritanceExample) {
System.out.println(new Vehicle().brand);
System.out.println(new Car().brand);
new Car().move();
}
}
When you are considering using inheritance, ask yourself whether the subclass really is a more specialized version of the superclass. In this case, a car is a type of vehicle, so the inheritance relationship makes sense.
When to use composition in Java
In object-oriented programming, we can use composition in cases where one object "has" (or is part of) another object. Some examples would be:
- A car has a battery (a battery is part of a car).
- A person has a heart (a heart is part of a person).
- A house has a living room (a living room is part of a house).
To better understand this type of relationship, consider the composition of a House
:
public class CompositionExample {
public static void main(String... houseComposition) {
new House(new Bedroom(), new LivingRoom());
// The house now is composed with a Bedroom and a LivingRoom
}
static class House {
Bedroom bedroom;
LivingRoom livingRoom;
House(Bedroom bedroom, LivingRoom livingRoom) {
this.bedroom = bedroom;
this.livingRoom = livingRoom;
}
}
static class Bedroom { }
static class LivingRoom { }
}
In this case, we know that a house has a living room and a bedroom, so we can use the Bedroom
and LivingRoom
objects in the composition of a House
.
Inheritance vs composition: Two examples
Consider the following code. Is this a good example of inheritance?
import java.util.HashSet;
public class CharacterBadExampleInheritance extends HashSet<Object> {
public static void main(String... badExampleOfInheritance) {
BadExampleInheritance badExampleInheritance = new BadExampleInheritance();
badExampleInheritance.add("Homer");
badExampleInheritance.forEach(System.out::println);
}
In this case, the answer is no. The child class inherits many methods that it will never use, resulting in tightly coupled code that is both confusing and difficult to maintain. If you look closely, it is also clear that this code does not pass the "is a" test.
Now let's try the same example using composition:
import java.util.HashSet;
import java.util.Set;
public class CharacterCompositionExample {
static Set<String> set = new HashSet<>();
public static void main(String... goodExampleOfComposition) {
set.add("Homer");
set.forEach(System.out::println);
}
Using composition for this scenario allows the CharacterCompositionExample
class to use just two of HashSet
's methods, without inheriting all of them. This results in simpler, less coupled code that will be easier to understand and maintain.
Method overriding with Java inheritance
Inheritance allows us to reuse the methods and other attributes of one class in a new class, which is very convenient. But for inheritance to really work, we also need to be able to change some of the inherited behavior within our new subclass. For instance, we might want to specialize the sound a Cat
makes:
class Animal {
void emitSound() {
System.out.println("The animal emitted a sound");
}
}
class Cat extends Animal {
@Override
void emitSound() {
System.out.println("Meow");
}
}
class Dog extends Animal {
}
public class Main {
public static void main(String... doYourBest) {
Animal cat = new Cat(); // Meow
Animal dog = new Dog(); // The animal emitted a sound
Animal animal = new Animal(); // The animal emitted a sound
cat.emitSound();
dog.emitSound();
animal.emitSound();
}
}
This is an example of Java inheritance with method overriding. First, we extend the Animal
class to create a new Cat
class. Next, we override the Animal
class's emitSound()
method to get the specific sound the Cat
makes. Even though we've declared the class type as Animal
, when we instantiate it as Cat
we will get the cat's meow.
Does Java have multiple inheritance?
Unlike some languages, such as C++, Java does not allow multiple inheritance with classes. You can use multiple inheritance with interfaces, however. The difference between a class and an interface, in this case, is that interfaces don't keep state.
If you attempt multiple inheritance like I have below, the code won't compile:
class Animal {}
class Mammal {}
class Dog extends Animal, Mammal {}
A solution using classes would be to inherit one-by-one:
class Animal {}
class Mammal extends Animal {}
class Dog extends Mammal {}
Another solution is to replace the classes with interfaces:
interface Animal {}
interface Mammal {}
class Dog implements Animal, Mammal {}
Using ‘super' to access parent classes methods
When two classes are related through inheritance, the child class must be able to access every accessible field, method, or constructor of its parent class. In Java, we use the reserved word super
to ensure the child class can still access its parent's overridden method:
public class SuperWordExample {
class Character {
Character() {
System.out.println("A Character has been created");
}
void move() {
System.out.println("Character walking...");
}
}
class Moe extends Character {
Moe() {
super();
}
void giveBeer() {
super.move();
System.out.println("Give beer");
}
}
}
In this example, Character
is the parent class for Moe. Using super
, we are able to access Character
's move()
method in order to give Moe a beer.
Using constructors with inheritance
When one class inherits from another, the superclass's constructor always will be loaded first, before loading its subclass. In most cases, the reserved word super
will be added automatically to the constructor. However, if the superclass has a parameter in its constructor, we will have to deliberately invoke the super
constructor, as shown below:
public class ConstructorSuper {
class Character {
Character() {
System.out.println("The super constructor was invoked");
}
}
class Barney extends Character {
// No need to declare the constructor or to invoke the super constructor
// The JVM will to that
}
}
If the parent class has a constructor with at least one parameter, then we must declare the constructor in the subclass and use super
to explicitly invoke the parent constructor. The super
reserved word won't be added automatically and the code won't compile without it. For example:
public class CustomizedConstructorSuper {
class Character {
Character(String name) {
System.out.println(name + "was invoked");
}
}
class Barney extends Character {
// We will have compilation error if we don't invoke the constructor explicitly
// We need to add it
Barney() {
super("Barney Gumble");
}
}
}
Type casting and the ClassCastException
Casting is a way of explicitly communicating to the compiler that you really do intend to convert a given type. It's like saying, "Hey, JVM, I know what I'm doing so please cast this class with this type." If a class you've cast isn't compatible with the class type you declared, you will get a ClassCastException
.
In inheritance, we can assign the child class to the parent class without casting but we can't assign a parent class to the child class without using casting.
Consider the following example:
public class CastingExample {
public static void main(String... castingExample) {
Animal animal = new Animal();
Dog dogAnimal = (Dog) animal; // We will get ClassCastException
Dog dog = new Dog();
Animal dogWithAnimalType = new Dog();
Dog specificDog = (Dog) dogWithAnimalType;
specificDog.bark();
Animal anotherDog = dog; // It's fine here, no need for casting
System.out.println(((Dog)anotherDog)); // This is another way to cast the object
}
}
class Animal { }
class Dog extends Animal { void bark() { System.out.println("Au au"); } }
When we try to cast an Animal
instance to a Dog
we get an exception. This is because the Animal
doesn't know anything about its child. It could be a cat, a bird, a lizard, etc. There is no information about the specific animal.
The problem in this case is that we've instantiated Animal
like this:
Animal animal = new Animal();
Then tried to cast it like this:
Dog dogAnimal = (Dog) animal;
Because we don't have a Dog
instance, it's impossible to assign an Animal
to the Dog
. If we try, we will get a ClassCastException
.
In order to avoid the exception, we should instantiate the Dog
like this:
Dog dog = new Dog();
then assign it to Animal
:
Animal anotherDog = dog;
In this case, because we've extended the Animal
class, the Dog
instance doesn't even need to be cast; the Animal
parent class type simply accepts the assignment.
Casting with supertypes
It's possible to declare a Dog
with the supertype Animal
, but if we want to invoke a specific method from Dog
, we will need to cast it. As an example, what if we wanted to invoke the bark()
method? The Animal
supertype has no way to know exactly what animal instance we're invoking, so we have to cast Dog
manually before we can invoke the bark()
method:
Animal dogWithAnimalType = new Dog();
Dog specificDog = (Dog) dogWithAnimalType;
specificDog.bark();
You can also use casting without assigning the object to a class type. This approach is handy when you don't want to declare another variable:
System.out.println(((Dog)anotherDog)); // This is another way to cast the object
Take the Java inheritance challenge!
You've learned some important concepts of inheritance, so now it's time to try out an inheritance challenge. To start, study the following code: