Composition and inheritance are two important approaches for achieving code reuse and designing relationships between classes in Java. Although inheritance is useful when a genuine subtype relationship exists, composition is often preferred because it provides greater flexibility, reduces coupling, and many more.
- Inheritance promotes code reuse through superclass-subclass relationships.
- Composition promotes code reuse through object composition and delegation.
- Composition generally provides more flexibility than inheritance.
Inheritance
Inheritance is a Java mechanism in which a subclass acquires accessible fields and methods from a superclass. It is used to represent an "is-a" relationship and is implemented using the extends keyword.
- Supports method overriding and runtime polymorphism.
- Allows common behavior to be defined in a superclass and specialized in subclasses.
- A Java class can directly extend only one class.
class Person {
// Private fields for encapsulation
private String name;
private int age;
// Constructor to initialize fields
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter for name
public String getName() {
return name;
}
// Setter for name
public void setName(String name) {
this.name = name;
}
// Getter for age
public int getAge() {
return age;
}
// Setter for age
public void setAge(int age) {
this.age = age;
}
}
class Employee extends Person {
// Private field for encapsulation
private int salary;
// Constructor to initialize fields
public Employee(String name, int age, int salary) {
super(name, age); // Call superclass constructor
this.salary = salary;
}
// Getter for salary
public int getSalary() {
return salary;
}
// Setter for salary
public void setSalary(int salary) {
this.salary = salary;
}
// Method to display employee information
public void displayInfo() {
System.out.println("Name: " + getName());
System.out.println("Age: " + getAge());
System.out.println("Salary: " + salary);
}
}
public class Main {
public static void main(String[] args) {
// Create an Employee object
Employee emp = new Employee("Geek1", 30, 50000);
// Display employee information
emp.displayInfo();
}
}
Output
Name: Geek1 Age: 30 Salary: 50000
Explanation: In this example, the Employee class inherits from Person and adds a new property, salary.
Composition
Composition is a design approach in which a class contains references to objects of other classes and uses them to provide functionality. It represents a "has-a relationship".
- Components can be replaced without changing the containing class.
- Promotes loose coupling when the class depends on interfaces or abstractions.
- Allows different implementations to be combined at runtime.
For example: A
Carhas anEngine. Instead of inheriting engine behavior, theCarclass can contain anEngineobject and delegate engine-related operations to it.
class Address {
// Private fields for encapsulation
private String street;
private String city;
private String zipCode;
// Constructor to initialize fields
public Address(String street, String city, String zipCode) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
}
// Getter for street
public String getStreet() {
return street;
}
// Setter for street
public void setStreet(String street) {
this.street = street;
}
// Getter for city
public String getCity() {
return city;
}
// Setter for city
public void setCity(String city) {
this.city = city;
}
// Getter for zipCode
public String getZipCode() {
return zipCode;
}
// Setter for zipCode
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
// Method to display address (delegation)
public void displayAddress() {
System.out.println(street + ", " + city + ", " + zipCode);
}
}
class Person {
// Private fields for encapsulation
private String name;
private Address address;
// Constructor to initialize fields
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
// Getter for name
public String getName() {
return name;
}
// Setter for name
public void setName(String name) {
this.name = name;
}
// Getter for address
public Address getAddress() {
return address;
}
// Setter for address
public void setAddress(Address address) {
this.address = address;
}
// Method to display person information
public void displayInfo() {
System.out.println("Name: " + name);
System.out.print("Address: ");
address.displayAddress();
}
}
public class Main {
public static void main(String[] args) {
// Create an Address object
Address addr = new Address("123 Main St", "Springfield", "12345");
// Create a Person object
Person person = new Person("Geek1", addr);
// Display person information
person.displayInfo();
}
}
Output
Name: Geek1 Address: 123 Main St, Springfield, 12345
Explanation: In this example, the Person class uses composition to include an Address object. This creates a "has-a" relationship between Person and Address.
Why Favor Composition Over Inheritance?
Composition is often favored over inheritance because it provides greater flexibility when behavior needs to change independently.
- Loose Coupling: A class can depend on an interface or component instead of being tightly coupled to a superclass.
- Greater Flexibility: Components can be replaced or changed without creating new subclasses.
- Runtime Behavior Changes: A class can work with different implementations during its lifetime.
- Better Reusability: The same component can be used by multiple classes.
- Better Testability: Components can be replaced with test implementations or mocks.
- Avoids Large Inheritance Hierarchies: Multiple independently varying behaviors do not require a separate subclass for every combination.
- Encapsulation: Implementation details can remain inside the composed component.
- Easier Maintenance: Changes to a component generally do not require changes to the inheritance hierarchy.
Composition vs Inheritance
| Feature | Inheritance | Composition |
|---|---|---|
| Relationship | Is-a | Has-a |
| Main mechanism | extends | Object reference |
| Coupling | Generally tighter | Generally looser |
| Behavior reuse | Through inheritance | Through delegation |
| Flexibility | Less flexible when behavior varies | More flexible |
| Runtime replacement | Not normally possible for the inherited implementation | Component can be replaced |
| Multiple independent behaviors | Can lead to many subclasses | Behaviors can be combined |
| Testing | Can be more difficult with tightly coupled hierarchies | Components can be substituted easily |
| Example | Dog extends Animal | Car has Engine |