IntelliJ IDEA
IntelliJ IDEA – the Leading Java and Kotlin IDE, by JetBrains
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 fromEmployee
, 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:
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: | Class | Package | Subclass of same package | Subclass of another package | Anywhere |
public | ✔ | ✔ | ✔ | ✔ | ✔ |
protected | ✔ | ✔ | ✔ | ✔ | |
package-private (no modifier) | ✔ | ✔ | ✔ | ||
private | ✔ |
Going back to our example, you will see that the nam
e 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!