Class Inheritance in Java
What is Inheritance?
Inheritance allows you to inherit another class's variables, methods, constructors, etc. Inheritance allows for code reuse and for creating a class hierarchy that can be used with polymorphism. Inheritance represents an "is-a" relationship.
Java doesn't support multiple inheritance. A class can only inherit one class. A class can inherit another class by using the extends keyword.
public class Message {
}
public class Comment extends Message {
}
In this example, Comment inherits Message. Comment is-a Message. Message isn't a Comment. The Message class is referred to as the superclass or also called a parent class. The Comment class is referred to as the subclass or child class.
Class Inheritance
In the previous example, Comment will have all variables, methods, constructors, etc. defined in Message. This will be shown by expanding on this example.
public class Message {
private final String text;
public Message(final String text) {
this.text = text;
}
public String text() {
return text;
}
}
public class Comment extends Message {
public Comment(final String text) {
super(text);
}
}
Anything defined in Message that is marked as private is not accessible to Comment but a Comment will still have everything defined in Message.
final var comment = new Comment("Hello!");
System.out.println(comment.text());
This would print the text "Hello" to the console. You can see in this example that it is using what is defined in the Message class as if it were defined in the Comment class.
Using super
Similar to how using this allows you to specify an instance variable or instance method, you can use super to specify instance variables, instance methods, and constructors in a superclass. Using super is usually only done with constructors and methods but can be used with fields as well. In the previous example, you'll notice the Comment constructor calls the constructor in Message class using super().
Constructors
When inheriting a class, the subclass must call a constructor in the superclass. This is shown in the previous example with the Comment class. To invoke a constructor in the superclass, you use super(). If the superclass has a no-argument constructor, it will be implicitly called, and you don't have to call it.
public class Message {
// No-argument constructor
public Message() {
// ...
}
}
public class Comment extends Message {
// No-argument constructor
public Comment() {
// ...
}
public Comment(final UUID id) {
// ...
}
}
The Message class defines a no-argument constructor. The Comment class has two constructors; both of these implicitly call super() even though it isn't there. You can provide the super() call if you want for clarity, but it isn't required.
If you are wanting to call a different constructor other than a no-argument constructor, you can do so by calling super() and passing the arguments to that constructor.
public class Message {
private final UUID id;
private final String text;
public Message(final UUID id, final String text) {
this.id = id;
this.text = text;
}
}
public class Comment extends Message {
public Comment(final UUID id, final String text) {
super(id, text);
}
}
Overriding Methods
When a class inherits another class, sometimes you will want to override one of the methods in the superclass and give it a different implementation. One use case is that a method doesn't apply to the subclass, so you'd want to throw an exception. Another use case may be that it doesn't apply to that class, such as a flag. This is shown in the following example:
public class Message {
public boolean isExpired() {
// ...
}
}
public class Comment extends Message {
@Override
public boolean isExpired() {
return true;
}
}
In this example, comments don't expire, so it overrides the isExpired() method and always returns true.
Another use case is if you need to enhance the method. You can do this by reusing the same behavior in the superclass and then providing whatever additional code you need.
public class Message {
private String text;
public String text() {
return text;
}
}
public class Comment extends Message {
@Override
public String text() {
return "Comment: " + super.text();
}
}
In this example, a call to the superclasses text() method is invoked. This invokes the text() method in the superclass. It then adds a prefix to the text that is returned from the superclasses text() method.
You'll notice that when overriding a method it is marked with the @Override annotation. When overriding a method, the @Override annotation should always be used because the compiler will make sure that you are actually overriding a method.
protected Access Modifier
The protected access modifier is used in inheritance. It restricts a variable, method, or constructor to be accessible in the same package or to any classes that inherit it even if they are outside of the package. The protected access modifier is useful when you need something that is only accessible internally to the classes that inherit it. This is usually only used with constructors and methods.
Preventing Inheritance
You can prevent a class from being inherited by marking the class as final.
public final class FinalClass {
// ...
}
If another class tries to extend this class, it will result in a compiler error.
Java 17 introduced the ability to have sealed classes. A sealed class allows you to specify what classes a class can inherit in a permits clause. There is a lot you can do with sealed classes. For more on sealed classes, click here.
Hiding Instance Variables
Hiding is something that occurs when you have the same instance variable name in both a superclass and a subclass and is accessible to the subclass. This is shown with the text variable in the following example:
public class Message {
protected String text;
}
public class Comment extends Message {
protected String text;
}
When you are in the Comment class, it will use the text variable in the Comment class and hide the text variable in the Message class. This can lead to bugs. To fix this, you could use super to reference the text variable in Message. The bigger problem is that an instance variable should never be marked as anything but private. Instance variables should only be accessible in the class they are defined in. If you need to access them, they should be done through getter and setter methods depending on if you need to access or modify the variable. Using proper encapsulation, hiding will never be a problem.
Hiding also applies to static variables and static methods.
Conclusion
Inheritance is used for both defining a class hierarchy and code reuse. If you need a class hierarchy, inheritance should be used. Don't use inheritance if you are only using it for code reuse. If you need code reused, composition is better than inheritance.