Module 7: Abstract Classes and Interfaces
Introduction
“Abstraction is the elimination of the irrelevant and the amplification of the essential.”
Robert C. Martin
One of the biggest advantages of object-oriented programming (OOP) is the ability to leverage inheritance and polymorphism. Inheritance allows more specific types of objects with each new subclass. Conversely, going back up the class hierarchy then each class becomes more generalized.
Learning Objectives
- Abstract Classes
- Interfaces
- Multiple Inheritance
- Interface Testing
- Plugins
- Alternatives to Inheritance
- Advanced Topics
- Summary
Abstract Classes
There are cases where there will be a common, shared behavior but there is no way to have a common, shared implementation. Some examples include a Shape class calculating the area of the shape or the perimeter of the shape. Both a Circle and a Square class would need to have methods to do this, but the actual calculation of the area or perimeter would be different between a Circle and a Square. Another example would be a card game application. All card games need to score the player’s hand, but the scoring is done differently in each game.
In these cases, when a common behavior is needed but there is no common implementation possible, the method in question and the class must be declared as abstract. The abstract class and method are represented in UML Class Diagrams either with the class name and abstract method in italics, or like this:
classDiagram
Shape <|-- Circle
Shape <|-- Square
class Shape {
<<abstract>>
+area() double
+perimeter() double
}
class Circle {
-radius : double
+area() double
+perimeter() double
}
class Square {
-sideLength : double
+area() double
+perimeter() double
}
This diagram represents an abstract class, Shape, with two abstract methods. Through inheritance the Circle and Square class extend Shape and implement their own area() and perimeter() methods.
An abstract method declares a method signature but with no method body. If a class has even one abstract method, the entire class must also be abstract. If a method is abstract and therefore missing an implementation, it’s not possible to create an instance of that class. If that method was called, Java wouldn’t know what to do! This is why any class with an abstract method makes the entire class an abstract class. A subclass that is not also abstract must override this method and provide an implementation before an object instance can be created. This means that abstract methods cannot be private.
/**
* Represent a geometric shape
* @version 20240730.01
*/
public abstract class Shape
{
...
public Shape()
{
}
...
/**
* Calculate the area of the shape
* @return the area of the shape
*/
public abstract double area();
/**
* Calculate the perimeter of the shape
* @return the area of the shape
*/
public abstract double perimeter();
}
Abstract Class Usage
Since there are parts of the class implementation declared but missing, abstract classes can only be used as a superclass, no instances of an abstract class can be created. Abstract classes can have everything a regular class would have, including instance variables, constructors, and methods that are implemented. Since an abstract class can have some implementation, abstract classes are still limited to single class inheritance.
Abstract classes are used when you want to provide a common base implementation for related classes but don’t know enough to create a common shared implementation. They are only used in an inheritance hierarchy as the superclass of more specific classes that can provide the implementation. The subclasses do this by overriding the abstract method to provide the implementation.
public class Circle extends Shape
{
private double radius;
private double area;
public Circle(double radius)
{
super();
}
...
@Override
public double area()
{
// PI r^2
return Math.PI * radius * radius;
}
@Override
public double perimeter()
{
// 2PIr
return 2 * Math.PI * radius;
}
...
}
These abstract classes can still be used as a static data type, so all the benefits of polymorphism are still possible. In this example, even though Shape is an abstract class it can still be used as the instance’s data type.
Shape shape = new Circle(16.0);
The abstract method in Shape is essentially a contract with the Java compiler. It says Shape doesn’t know what the abstract method’s implementation will be, but it guarantees to the compiler that they will be implemented in the subclass.
This gives the best of all worlds! The DRY principle is leveraged with the ability to write commonly implemented code once, while guaranteeing a common functionality (calculating the area of a shape in the example) and still using polymorphism. Abstract classes can do all of this by simply allowing unknown details to be implemented in a common way later in a specific subclass.
Interfaces
Imagine taking the concept of an abstract class to the extreme, where every single method was abstract. Is this possible? Yes! It is a concept called an interface and is a surprisingly powerful tool in object-oriented programming.
Abstract Method Redux
An abstract method in Java is a method that is declared without an implementation. This means that the method does not have a body and is meant to be overridden in subclasses. This is used to define a shared behavior among the subclasses even when there is no way to know how that might be implemented. Abstract methods enable a clear and enforceable contract for subclasses that the Java compiler recognizes.
Interfaces
An interface is treated like a special type of class in Java. In a lot of ways an interface is like an abstract class, because it defines behavior for common classes, but doesn’t provide any implementation. Interfaces provide a powerful way to achieve abstraction in Java. They allow developers to define a contract that classes can implement, specifying what methods a class must have without dictating how these methods should be implemented. There are many examples of this from comparing two objects for sorting to doing database operations.
Variables can be declared in an interface, but they must be public, static, and constant. Since every method in an interface is abstract, the abstract keyword is omitted. Likewise, every method in an interface must be public, so the public keyword is also omitted.
public interface InterfaceName
{
ReturnType methodName(parameters);
...
}
An interface can be used in Java much like an abstract class. The interface essentially provides a contract to the Java compiler guaranteeing that at least these methods will be there when the application runs. An interface can be used as a static data type for a variable. Like abstract classes, an instance of an interface cannot be created with the new keyword.
A very good example of the power of interfaces can be seen in the Java Comparable interface. The Comparable interface in Java is built into the SDK and is used to define a natural ordering for objects of a class. This allows objects of a class to be compared and sorted in a collection. It defines a single method, compareTo(), that determines the order of objects.
Think about comparing String objects to each other versus comparing Integer objects to each other. The String would probably be compared alphabetically, but the Integer class would use the actual numeric value for comparison. There is no common implementation for these, but the behavior of comparing them for sorting is quite easy to understand. This is a great use of an interface!
Because an interface does not dictate any shared implementation, they promote loose coupling and design flexibility. They allow developers to change the implementation (the dynamic type of an object) without affecting the code that depends on the interface. This makes the code more modular, which is easier to maintain and extend. This is known as the Dependency Inversion Principle.
The Comparable interface shown earlier is a great example of this, with the static Collections.sort() method using the Comparable interface to sort any collection of objects, whether they are part of the Java SDK or custom objects created as part of a single project. In short, because the Comparable.sort() method can guarantee the objects have a compareTo() method, it will work for absolutely anything that has this implemented without needing to be rewritten.
Using Interfaces
An interface is very similar to an abstract class, but the way it is declared in a class is slightly different. Since there are normally no concrete methods available, an interface uses the keyword implements instead of extends. Here’s an example of a class implementing the Comparable interface:
public class Shape implements Comparable<Shape>
{
...
public abstract area();
...
@Override
public int compareTo(Shape o)
{
// Comparing the area of the two shapes
return this.area() - o.area();
}
}
Another good example from the Java SDK of an interface with similar functionality is the Comparator interface. The Comparator interface allows custom sorting of objects that don’t have a natural ordering, or when you want to sort objects in an order different from their natural ordering. Consider a playing card for a card game. It will have a rank, points, and a suit. It can be useful to sort on any one of these fields. The Comparator interface makes this possible.
Implementing the Comparator interface is done in a different class than the one being compared. That class must override the compare(T o1, T o2) method for comparing two objects of the same type. The compare() method returns an integer just like the Comparable interface’s compareTo() method.
public SuitComparator implements Comparator<Card>
{
public Suitcomparator()
{
}
@Override
public int compare(Card o1, Card o2)
{
// Assuming Suit is an enum, use the Comparable is using a Generic, which says this must use a class of the type defined inside the <> brackets.
// In this case, Comparable is being implemented only with Shape objects.
// ordinal integer value for comparison
return o1.getSuit().ordinal() - o2.getSuit().ordinal();
}
}
Other implementations can be created for rank and points in this case, and these can all be passed to Collections.sort() or Arrays.sort() for sorting. It would be used like this, assuming there was a collection of Card objects called cards:
Collections.sort(cards, new SuitComparator());
While Comparator uses another helper class to do its work, it makes sense to implement it in the class itself. This can be done using a static method that will return a Comparator object.
public Card implements Comparable<Card>, Comparator<Card>
{
...
@Override
public int compareTo(Card o)
{
return this.getPoints() - o.getPoints();
}
public static Comparator<Card> suitComparator = new Comparator<Card>
{
@Override
public int compare(Card o1, Card o2)
{
// Assuming Suit is an enum, use the ordinal integer value for comparison
return o1.getSuit().ordinal() - o2.getSuit().ordinal();
}
}
}
Here the static method suitComparator() returns an anonymous implementation of the Comparator interface. Other implementations can be created for rank and points in this case, and these can all be passed to Collections.sort() or Arrays.sort() for sorting. This keeps all these utility classes directly associated with the class they work on. It would be used like this, assuming there was a collection of Card objects called cards:
Collections.sort(cards, Card.suitComparator);
Sorting with a Secondary Key
It is also possible to do a sort on a secondary key if the primary sort field is equal. People’s names are often sorted this way, where if two different people have the same last name, they will then be sorted on Anonymous classes are created and instantiated in a single expression. They are anonymous because they don’t have a class name associated with them. their first name. The person’s first name is the secondary sort key. The Card object could also do the same thing, where if the main field was equal a secondary field could help refine the comparison.
import java.util.Comparator;
public Card implements Comparable<Card>
{
...
@Override
public int compareTo(Card o)
{
int result = this.getPoints() - o.getPoints();
// If points are equal, compare the suit
if (result == 0)
{
// Assuming suit is an enum
result = this.getSuit().ordinal() - o.getSuit().ordinal();
}
return result;
}
public static Comparator<Card> suitComparator = new Comparator<Card>
{
@Override
public int compare(Card o1, Card o2)
{
// Assuming Suit is an enum, use the ordinal integer value for comparison
int result = o1.getSuit().ordinal() - o2.getSuit().ordinal();
if (result == 0)
{
result = o1.getPoints - o2.getPoints();
}
return result;
}
}
}
Now, if two cards have the same points, the two will be sorted against each other using the suit when using Comparable. Similarly, if two cards have the same suit, they will be sorted against each other using the points when using the Comparator.
Comparable vs Comparator
- The Comparable interface can be used to provide a single (natural) way of sorting, whereas the Comparator interface is used to provide different ways of sorting collections of the same object
- For using Comparable, classes need to implement it in the class, but when using Comparator a separate class needs to be created
- The Comparable interface is in java.lang package and doesn’t require an import
- The Comparator interface is in java.util package and does require an import
- Arrays.sort() and Collection.sort() methods automatically uses the Comparable interface’s compareTo() method of the class
Interface Documentation
Classes that implement an interface and have Javadoc can often skip writing the Javadoc for the methods the class overrides. Any class that implements this interface doesn’t have to write this documentation again, the documentation on the interface applies everywhere it is implemented. However, the documentation may be written to elaborate on specific implementation details.
Multiple Inheritance
Class inheritance is tightly controlled in Java, with each class only being allowed to directly inherit from one other class. This is done to prevent any contradiction between two parent classes. Imagine if two different classes implemented the exact same method and a subclass inherited from each directly. When that superclass method was called, which one would Java use? There would be no way to know which should have priority.
classDiagram
class SubClass
SuperClass1 <|-- SubClass
SuperClass2 <|-- SubClass
class SuperClass1 {
+method1()
}
class SuperClass2 {
+method1()
}
Here, if such a class hierarchy were possible and an instance of SubClass tried to call method1() there are two possible choices. Would Java run the implementation in SuperClass1 or SuperClass2? There would be no way to prioritize one over the other. To avoid this, Java eliminates the problem completely by preventing multiple inheritance from classes. This is done with Java syntax by only allowing a class to directly extend one other class.
Is this a problem with interfaces though? Since interfaces have no implementation, the implementing class must provide its own override implementation.
classDiagram
class Interface1 {
<<interface>>
+method1() void
}
class Interface2 {
<<interface>>
+method1() void
}
class MyClass {
+method1() void
}
Interface1 <|.. MyClass
Interface2 <|.. MyClass
This means if two different interfaces happened to declare the same method interface, the subclass override would still provide a clear implementation for Java to call. This means there would be no confusion about which implementation to use. Because of this, it is possible to implement multiple interfaces in Java, giving a form of multiple inheritance.
public class MyClass implements Comparable, Cloneable
{
...
}
The comma separated list of interfaces tells the Java Compiler that all methods defined in both interfaces will be implemented. If two different interfaces declared the same method, the implementing class would satisfy both interfaces when that method was implemented.
Interface Methods
Default methods are a feature added in Java version 8 for Java interfaces and provide a way to add method implementations to interfaces. The main purpose of default methods is to enable adding new methods to existing interfaces without forcing all implementing classes to provide an implementation. This allowed adding new methods to existing interfaces in the JDK without breaking backwards compatibility. They are declared using the default keyword. This blurs the line significantly between abstract classes and interfaces.
interface InterfaceWithDefaultMethod
{
void abstractMethod();
default void defaultMethod()
{
System.out.println("This is a default method.");
}
}
Default methods also introduce potential problems with the ability of interfaces to provide multiple inheritance. The workaround is if two different interfaces provide a default method implementation for the same method signature, the implementing class MUST override that and provide its own implementation. If a class wants to use one of the interface’s methods, it needs to override the method and then call the specific interface implementation.
interface Interface1
{
default void show()
{
System.out.println("Default show method in Interface1");
}
}
interface Interface2
{
default void show()
{
System.out.println("Default show method in Interface2");
}
}
class MyClass implements Interface1, Interface2
{
// Providing an implementation of the show method from both interfaces
public void show()
{
// Provide our own implementation
System.out.println("Method in MyClass");
// Or, use super keyword to call the show method of one of the Interfaces
Interface1.super.show();
}
}
Since abstract classes already provided a way to implement some but not all methods, why was this introduced? As mentioned earlier, this enabled adding new methods to existing interfaces without breaking backwards compatibility, since the default method provides an implementation for older code written before the interface had new methods added. Default methods also helped facilitate the introduction of lambda expressions and functional interfaces in Java 8. If a project doesn’t fit into one of these two situations, it is recommended to stick with abstract classes.
In addition to default methods, interfaces can also have static methods, just like a class. These methods are called the same way as well.
Interface Fields
While an interface can have a field, such a field is implicitly public, static, and final, meaning it is effectively constant. As such, naming conventions for any constant apply (ALL_CAPS using snake case). Since they are public, they can be accessed directly without the need of an accessor method. Interface fields are of limited usefulness but is a feature to be aware of.
Interface Testing
Since interfaces define a shared set of behavior, it is possible to write one set of unit tests that will work for any implementation. This makes testing significantly easier and more scalable.
Since any class that implements an interface must implement the same methods as declared in the interface, it makes sense that the same tests should work for all implementations of the interface. Instead of re-writing the same tests, with a little bit of thought it is possible to design one set of tests that can be reused. Using reflection, it is possible to create an instance of a class using the Class object. The Object class includes an instance of the Class class as a field, accessible with ClassType.class or the method instance.getClass().
Here is a very basic example testing the size() method of the List interface, using the ArrayList and LinkedList implementations. Reflection is used to create instances of each in a JUnit 5 parameterized test:
@ParameterizedTest
@ValueSource(classes = { ArrayList.class, LinkedList.class })
void parameterizedInterfaceTest(Class<?> c)
{
// Arrange - Declaring the fixture using the Interface type
List<Integer> l = null;
try
{
// Act - this may throw an exception
// Reflection is used to get an instance of the class from a constructor
// This instance is then cast as a List<Integer>
l = (List<Integer>) c.getDeclaredConstructor().newInstance();
// Assert
Assertions.assertEquals(0, l.size());
}
catch (Exception e)
{
// If getDeclaredConstructor threw an exception, the test failed
Assertions.fail();
}
}
If there were a third implementation of the interface, the same test could be used by simply adding it to the ValueSource collection. Classes that extend an abstract class may be able to use this approach to test the abstract methods.
Plugins
A software plugin is a component loaded from outside the application. Plugins add specific enhancements or functionality to an existing computer program or application without modifying its core code. Plugins enable 3rd party users to add new features, improve performance, or integrate third-party services without altering the main program.
When a user interacts with the plugin’s elements, the plugin code is executed by the main application, performing its intended function. This could involve adding new tools, processing data, or calling external services. IntelliJ is one example of a software package that supports using plugins to add, improve, or change its functionality. On the welcome page of IntelliJ, where a project can be opened or created, there is also an option to manage Plugins.
Search for the following three plugins and read about what each one of them does:
- Indent Rainbow by Dmitry Murzin
- Rainbow Brackets by Zhihao Zhang
- SonarLint by SonarSource
None of these three plugins were written by JetBrains or a member of the IntelliJ team. Each adds a nice enhancement to IntelliJ that isn’t natively available. How is it possible for a person or company to write code that will directly integrate with IntelliJ if they aren’t on the IntelliJ development team? The answer is simple, Interfaces!
Any application that supports plugins provides an interface to 3rd party developers outside of the organization. The method or methods in that interface provide the hook that allows the main application to understand how to interact with the plugin code.
The main application (IntelliJ in this case) loads each plugin from a JAR file in a particular folder on the computer’s hard drive. It then declares an instance of the class(es) inside the JAR file using the interface as the static object type. The program then calls the method(s) on that interface and the plugin then executes, adding the new functionality.
Writing an application that supports plugins is beyond the scope of this book. For a basic working code example and accompanying article, look at the following GitHub repository on Java plugin quickstart
Alternatives to Inheritance
Like many things in life, there are many ways to do the same thing in programming. Often it comes down to measuring the tradeoffs between different approaches when picking the best solution. Inheritance offers many advantages in software design, especially with what is possible around polymorphism. There are often situations where things will not fit into an object hierarchy though, and if inheritance is the only tool available when those things happen either a very convoluted and illogical hierarchy is used or the same code is written out in multiple places. Neither of those lead to a high-quality codebase. Fortunately, there are several other ways to solve this problem. Two of the most common are composition and mixin-style behavior.
Composition
Classes should ideally follow the Single Responsibility Principle (SRP), meaning a class should have one primary responsibility and therefore only one reason to change. This sounds great in theory, but how can complex objects be built that have interwoven responsibilities? The answer is simple, break them up into several smaller classes that are responsible for one thing, then use them together to create the more complex behavior. This is known as composition, because a class is composed of other classes.
Composition is an object-oriented design approach where a class contains instances of other classes as instance variables, and delegates work to them using the “has-a” relationship instead of using inheritance and the “is-a” relationship. In plain English, composition means an object contains other objects and relies on them to perform specific tasks. This lets objects in different hierarchies share one implementation of something they may have in common. The containing class is said to be composed of the contained classes, and it uses their behavior through their exposed public API (public methods). In Java, composition is achieved by creating an instance of one class within another class.
In this example, the Car class is composed partially of the Motor class:
class Motor
{
...
void start()
{
System.out.println("Motor started.");
}
}
class Car
{
private final Motor motor;
Car()
{
motor = new Motor();
}
void startCar()
{
motor.start();
}
}
With this structure, the Motor class can easily be re-used in any other class, such as a Motorcycle, Boat, Airplane, Bandsaw, Drill Press, etc. without being forced into a class hierarchy. It would be an amazing stretch of logic to put all of those classes into one object hierarchy just so they could share an implementation of a motor. Composition solves this problem by moving the definition of a motor into its own class and using the “has-a” relationship instead of the “is-a” relationship to give these other classes the shared behavior.
While both composition and inheritance promote code reuse, they have different implications and tradeoffs:
- Flexibility: Composition provides more flexibility than inheritance. With composition, you can change the behavior of a class by replacing the composed objects at runtime. With inheritance, the behavior is fixed at compile-time
- Code Reuse: Inheritance promotes code reuse through the inheritance hierarchy, while composition allows code reuse by creating instances of existing classes
- Coupling: Inheritance creates a tight coupling between the subclass and superclass, as changes in the superclass can affect the subclass. Composition promotes loose coupling, as the containing class and contained classes are independent of each other
- Extensibility: Composition is generally preferred for extensibility, as it allows you to create new classes by combining existing ones, without modifying the existing code
- Polymorphism: Both inheritance and composition can participate in polymorphism when interfaces are used
- Interfaces often provide more flexible polymorphic designs than inheritance alone, in large part because they focus on what a piece of code needs to do and not on how it is done
Mixins
Another approach to sharing behaviors outside of an inheritance hierarchy is known as a mixin. Mixins provide a way to add a reusable bundle of behavior to a class without relying on object inheritance and are especially relevant when working with interfaces. Because Java classes can implement multiple interfaces, default methods allow small pieces of reusable behavior to be shared across otherwise unrelated classes. This is useful when behaviors are not specific to an object type, and might be useful across more than one object in more than one inheritance hierarchy. Some examples would be adding logging or JSON serialization support to a class. With a mixin, behavior is injected into a class, often so the class gains methods as if they were its own.
Java does not support true mixins directly. However, interface default methods, which were introduced in Java version 8, can be used to achieve many mixin-style behaviors.
The term mixin comes from the phrase “mix in,” meaning to combine something into something else. In software, it refers to code meant to be “mixed into” a class or object to add behavior without using a full inheritance relationship.
In comparison, composition is a broader design approach where an object has other objects as fields and delegates work to them using the “has-a” relationship, which keeps boundaries more explicit. Mixins instead add behaviors to classes in Java by leveraging an interface’s default method capabilities. Since a class can implement many interfaces, it is possible to add multiple mixins wherever the mixin behavior might be needed.
public interface Printable
{
default void printBanner(String message)
{
System.out.println("=== " + message + " ===");
}
}
This mixin provides a reusable method for any class to print a message that is automatically wrapped with === before the message and === after the message, making it clear in the output that this is a banner message and likely labeling a section of the output.
Using an interface with mixin-style behavior
Any class that implements this interface will now have the printBanner method available in the class. An example of using this in a StudentService class would look like this:
public class StudentService implements Printable
{
public void processStudent()
{
printBanner("Processing Student");
}
}
Now the StudentService class can print several banner messages without having to code the leading and trailing markers each time. Any other class, no matter what its inheritance hierarchy is, could add the exact same capabilities as well by simply adding the implements Printable statement to the end of their class declaration.
Mixins vs. Composition
| Aspect | Mixin | Composition |
|---|---|---|
| Relationship | “Adds behavior to this class” | “This object uses other objects” |
| Visibility | Less explicit; methods may appear magically | More explicit; delegation is visible |
| Reuse style | Good for shared, orthogonal behavior | Good for modeling parts and collaborators |
| Coupling | Can become tangled through hidden method availability | Usually clearer, looser coupling |
| Debugging | Harder to trace where methods came from | Easier to trace and test dependencies |
Mixins work well for cross-cutting features that you want to stamp onto many classes, such as logging helpers, timestamp behavior, or small reusable traits. They are most attractive when the shared code is cohesive and the class API should stay convenient.
Composition is usually better when the behavior is a real collaborator with its own state or lifecycle, such as a PaymentProcessor used by an OrderService. It scales better for complex systems because each piece can be understood, tested, and replaced independently.
Rule of Thumb Use a mixin when you want to add capability; use composition when you want to model a part-whole relationship or keep dependencies explicit. In many codebases, composition is the safer default, while mixins are a targeted tool for small reusable behavior.
Inheritance is one of the most powerful features of object-oriented programming, but it is not the only way to share behavior between classes. Professional developers often prefer interfaces and composition because they create designs that are easier to extend, test, and maintain. As you continue learning software design, remember that inheritance is a tool, not a goal. The best solution is the one that models the problem clearly while keeping the code flexible and understandable.
Advanced Topics
Marker Interfaces
A Java interface that does not define any methods or fields is commonly referred to as a marker interface. Marker interfaces provide the ability to do type tagging and provide metadata about classes. Marker interfaces are used to tag or mark a class as having a particular property or capability. This tagging can then be used by other parts of the code, such as frameworks or libraries, to apply specific behavior to marked classes. Cloneable is a good example of a marker interface used for tagging.
The other common use of a marker interface is to provide metadata about a class. This metadata can then be checked at runtime using reflection to determine if a class should be treated differently based on the presence of the marker interface.
Composition over Inheritance
Inheritance is a key feature of object-oriented programming with many advantages, but it is not the only way to achieve clean code reuse. Two other approaches exist, composition and mixins.
Inheritance still makes sense when there is a true “is-a” relationship and the base class is specifically designed for extension. In practice, though, composition is the safer default for most reusable object-oriented design.
Since a design using composition makes classes more flexible, less tightly coupled, and easier to change without breaking existing code, many experienced developers and software engineers favor composition over inheritance. It also tends to improve testability and keeps responsibilities separated into smaller, focused components.
Summary
Abstract Class Summary
- An abstract method can only be declared in an abstract class. If a subclass of an abstract class doesn’t override and implement all abstract methods from the superclass, then it also must be abstract
- Abstract classes and methods are non-static, although an abstract class can implement a static method
- An abstract class cannot be instantiated using the new operator, but constructors can still be defined. They are used in subclasses with a call to super
- A class with an abstract method must be an abstract class. It is possible though for a class to be abstract with no abstract methods. In this case, the abstract class still cannot have instances created due to being abstract
- A subclass can be abstract even if the superclass is not. For instance, the Object class is not abstract, but other Java classes can be
- A subclass can override a method from the superclass and define it as abstract. This is extremely rare but might be useful if the implementation in the superclass is invalid for some reason in the subclass
- An instance of an abstract class cannot be created with the new operator, but an abstract class can be used as a data type. This means all the capability of Polymorphism applies to abstract classes
Interfaces Summary
- Cannot be instantiated
- Can only contain abstract methods (prior to Java 8)
- Can have default and static methods (Java 8+)
- Cannot have instance variables (only static final constants)
- Support multiple inheritance (a class can implement multiple interfaces)
- All members are implicitly public
- Used to define a contract that classes must adhere to
Comparing Interfaces and Abstract Classes
- Abstract classes can have state (instance variables), while interfaces cannot
- A class can extend only one abstract class but implement multiple interfaces
- Abstract classes can have constructors, interfaces cannot
- Abstract classes can have any access modifier for members, interfaces are implicitly public
- Both abstract classes and interfaces are used for abstraction, but abstract classes are typically used when you want to provide a common base implementation, while interfaces are used to define a contract for unrelated classes with a common behavior
- A class name is a noun, whereas interface names may be adjectives or nouns
Alternatives to Inheritance
- Composition uses other classes as fields and calls methods on those fields instead of inheriting methods from a parent class.
- This makes it easy to share behavior outside of an inheritance hierarchy
- Mixins implement share behaviors using default methods in Java.
- Shared behaviors such as logging or serialization can be added to clases without relying on an inheritance hierarchy