IntelliJ IDEA Java

Easy Hacks: How To Create Inheritance In Java

Inheritance is one of the fundamental attributes of object-oriented programming, in Java and other programming languages. It lets you create classes that are derived from another class (base class or superclass) and reuse, extend, or modify the behavior of the superclass. This principle allows you to build class hierarchies and reuse existing code.

Java itself uses inheritance everywhere: you will see many of the JDK classes inherit other classes, and every class in Java implicitly extends java.lang.Object. We won’t focus on that too much in this post, and instead look at some examples of how you can use inheritance in your own code.

Imagine you want to create Employee and Customer classes in your application. With inheritance, you could write both classes so they inherit the name and address properties from a parent Person class. There are several benefits to this in terms of code reusability and modularity, as you’ll see throughout this post.

What is inheritance in Java?

Let’s continue with the example from the introduction: a hierarchy with a Person, Employee and Customer class. The Person class comes with a name property, a getter for that property, and a constructor:

public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

If you want to use the Person class in code, you can instantiate a new object from it and, for example, print its name to the console:

var person = new Person("Maarten");
System.out.println(person.getName());

// Prints: Maarten

The Employee class will need a name and an employeeId property. Since an Employee shares a lot of characteristics with a person (at least until the robots rise up and steal all our jobs), you can reuse the Person class as a base.

In Java, the base class is usually referred to as a superclass, while the class that inherits from it is called the subclass. You may also come across terminology like parent class to refer to the superclass, and child class to refer to the subclass.

Here’s an example of the Employee class, inheriting from the Person class:

public class Employee extends Person {
    private final int employeeId;

    public Employee(String name, int employeeId) {
        super(name);
        this.employeeId = employeeId;
    }

    public int getEmployeeId() {
        return employeeId;
    }
}

There are a couple of things to unpack here. First of all, the extends keyword is used to tell the Java compiler that Employee extends Person. Next, in the Employee constructor, you’ll see the super(name) syntax that passes the name parameter into the superclass constructor, so that the underlying Person can store this property value.

What’s interesting about this is that, because we’re using inheritance, Employee does not have to implement name itself. Yet, the Employee class now exposes two properties: name and employeeId:

var employee = new Employee("Maarten", 123);
System.out.printf("%s (%s)", employee.getName(), employee.getEmployeeId());

// Prints: Maarten (123)

In a similar way, you can create a Customer class that also extends Person, and adds a customerId property of type String:

public class Customer extends Person {
    private final String customerId;

    public Customer(String name, String customerId) {
        super(name);
        this.customerId = customerId;
    }

    public String getCustomerId() {
        return customerId;
    }
}

When using the Customer class, you’ll get the name property and its getter from the Person superclass:

var customer = new Customer("Maarten", "ABC");
System.out.printf("%s (%s)", customer.getName(), customer.getCustomerId());

// Prints: Maarten (ABC)

As you can see from this example, inheritance lets you reuse properties and methods from the superclass in any subclass.

If you’re using IntelliJ IDEA Ultimate, you can create a UML diagram from these classes and visualize the hierarchy we have just created. Use the Ctrl+Alt+Shift+D shortcut on Windows/Linux, or ⌘Сmd+⌥Opt+⇧Shift+U on macOS, or the context menu on a package or class in the Project tool window:

Tip: Use the icons in the toolbar to toggle whether you want to display fields, constructors, methods, or properties.

In some cases, you may want to be able to have a common superclass that cannot be instantiated, but with subclasses that can be. This is helpful when you have common properties or methods but it also makes no sense to use the superclass directly without the extras that subclasses add to it. In Java, you can use the abstract modifier to tell the compiler that the type name can be used, and the class can be inherited from, but you don’t want new objects created directly:

public abstract class Person { } // can be inherited from, but not instantiated
new Person(); // does not work
public class Employee extends Person { } // can be inherited from & instantiated
new Employee(); // works
public class Customer extends Person { } // can be inherited from & instantiated
new Customer(); // works

Next to abstract classes, you can also define and inherit from interfaces. While an abstract class is typically expected to contain base implementations for some of its members, interfaces are more lightweight. An interface defines the members that are available on its subclasses, but doesn’t add an implementation. They are much like a contract.

If you rewrite Person to be an interface, you can communicate to consumers of your code that a Person implementation will have a getter for the name property. There is, however, no implementation for that getter, nor is there a backing field to contain the name. Instead, the implementation of the interface will have to provide all that:

public interface Person {
  String getName(); // consumers can expect this getter to exist, and subclasses have to provide an implementation
}

class Employee implements Person {

  @Override
  public String getName() {
      return ""; // may need a backing field, constructor, ...
  }
}

Note that here, instead of the extends keyword, interfaces are inherited from with the implements keyword.

Now let’s spice things up a little bit!

Overriding methods from a Java class

In a class hierarchy, subclasses can override the behavior of the superclass. Let’s extend our example a little bit, and add a title property and its getter to the Employee class.

Tip: when you add String title as an argument in the constructor for Employee, you can use Alt+Enter (Windows/Linux) or ⌥Opt+Enter (macOS) to let IntelliJ IDEA create a field and update existing usages:

Here’s the updated Employee:

public class Employee extends Person {
    private final String title;
    private final int employeeId;

    public Employee(String name, String title, int employeeId) {
        super(name);
        this.title = title;
        this.employeeId = employeeId;
    }

    public int getEmployeeId() {
        return employeeId;
    }

    public String getTitle() {
        return title;
    }
}

The Employee class can now represent various employees, with their title and ID:

var employee1 = new Employee("Maarten", "Developer Advocate", 123);
var employee2 = new Employee("Hadi", "Developer Advocate", 456);

As far as hierarchies go, Employee is a great example. But here’s the problem: Hadi is actually a team lead, not just an Employee. How can we represent this? Time for more inheritance! Let’s introduce a TeamLead subclass that inherits from Employee, and always suffixes the value returned in the getter for title with “(team lead)”:

public class TeamLead extends Employee {

    public TeamLead(String name, String title, int employeeId) {
        super(name, title, employeeId);
    }

    @Override
    public String getTitle() {
        return super.getTitle() + " (team lead)";
    }
}

The implementation of the TeamLead class is quite straightforward: it reuses all code from the Employee superclass. However, it overrides the getTitle() getter implementation:

  • The @Override annotation informs the Java compiler that you are overriding a method in the superclass. This helps the compiler warn you when the method does not exist, or doesn’t have the same signature as the base.
  • Using super.getTitle() calls into the getter from Employee, and the overridden version then appends a suffix before returning the value.

You can use the TeamLead class and see the suffix being printed to the console:

var employee = new TeamLead("Hadi", "Developer Advocate", 456);

System.out.printf("%s (%s) - %s", employee.getName(), employee.getEmployeeId(), employee.getTitle());

// Prints: Hadi (456) - Developer Advocate (team lead)

Preventing inheritance with the final modifier

As they say, “with great power comes great responsibility”, and that’s true for inheritance as well. One superclass can have infinite subclasses – but this may not always be desirable. In our example, you may want to prevent TeamLead from having further subclasses or prevent inheritance from a certain class entirely..

In Java, you can use the final modifier to set limitations on extensibility. You may have noticed final is being used for the name property in Person, effectively making it “read-only” and preventing it from being reassigned.

The final modifier can also be used with inheritance. When final is applied to a class, you’ll see inheriting from it is no longer possible. For example, when we add final to the Employee class, the TeamLead class we created in the previous section will no longer compile, and IntelliJ IDEA will also display an error message telling you that classes cannot inherit from Employee:

You can also add the final modifier to methods in your class hierarchy. For example, if you want to prevent the getTitle() getter from being overridden, you can make it final:

If you want to prevent a class from being extended and limit inheritance to a couple of known subclasses, you can also use the sealed modifier and a permits clause:

public sealed class Person permits Employee { } // no other classes than Employee can extend Person

If you want to use sealed classes in your code, IntelliJ IDEA will help you generate the permits clause based on the existing class hierarchy and will update all classes in the hierarchy accordingly:

Seal class with IntelliJ IDEA

Inheritance and access level modifiers

Java has several access modifiers you can use to control the scope of classes, constructors, properties, methods, and other members.

For classes, there are two access level modifiers: public, making the class available everywhere; and package-private, making the class available only in the package it’s defined in.

Looking at inheritance and class hierarchies, a public class can be inherited everywhere – in all packages and modules of your own code base, and in referencing code. Package-private classes can be inherited only within the same package, and are not available outside of this package. That is, unless they have the final modifier – then inheritance is altogether not allowed.

public class A { } // available everywhere, can be inherited from in any package
class B { } // available in the current package, can be inherited from in the current package

At the member level, the same principle applies: you can use the public modifier to make a member available everywhere, or you can use private to make it available only within the same class. You can also use no modifier at all, which would make the member available in the package where the member is located.

In addition, you can use the protected modifier to allow subclasses to access a specific member.

public class C { // available everywhere, can be inherited from in any package
  private int a; // available only within class C
  public void b() { } // available everywhere
  protected void c() // available only within class C and its subclasses in any package
}

Here’s a quick summary of access levels and where any given class member will be available:

Available in:ClassPackageSubclass of same packageSubclass of another packageAnywhere
public
protected
package-private (no modifier)
private

Going back to our example, you will see that the name field is private, which means that any subclass has to use the getName() getter to access its value. If you were to make it protected, the name field would be available in all subclasses of Person.

While all access level modifiers are useful to define the scope where classes and their members are available, the protected modifier is quite useful when creating class hierarchies through inheritance. Your superclass can define protected fields or methods that can be accessed by subclasses but not by consumers of those classes.

Drawbacks of inheritance

Inheritance has several benefits, such as code reusability and the ability to extend existing functionality. Common properties and behaviors can be defined in a superclass, and subclasses can inherit and build upon them and add or override functionality.

There are some drawbacks, though, such as tight coupling between classes. Any changes to the superclass can affect all the subclasses. Having large class hierarchies may lead to complexity, especially in large projects where multiple levels of inheritance are involved.

Summary

In this post, we’ve looked at inheritance as a fundamental concept in Java programming. Inheritance enables the creation of new classes, known as child classes, by extending properties and behaviors from existing classes, known as superclasses, using the extends keyword. We’ve also covered abstract classes and interfaces.

We’ve also seen the final modifier, which is used to prevent inheritance of a class or to prevent overriding a specific class member. By combining that with access level modifiers, you can define where classes can be inherited, and where class members are available.

Give it a try in IntelliJ IDEA!

image description