Abstract Classes
Table of Contents
Introduction
Classes act as the blueprints for objects. Objects in turn act as the in-memory representations of things in the real world, or at the very least, things we wish to perform computations on. If we pair that with our understanding of inheritance and polymorphism we'll quickly run into situations where behaviors become a bit too difficult to generalize regardless of the fact that they may be present in many sorts of the same category of object. For example, we know all animals make sounds, but those sounds will differ between each animal. We might want to enforce the contract of the ability to make sounds without establishing a generic, shared sound. Java provides the ability to declare abstract classes for exactly this scenario.
What is an Abstract Class?
According to the Java Language Specfication (§ 8.1.1.1):
An abstract class is a class that is incomplete, or to be considered incomplete.
This does not sound desireable at all! However, it's the semantic behavior of abstract classes that provides its usefulness. Because an abstract class is considered "incomplete" by Java that means they cannot be instantiated. We can define an abstract class Animal like so:
1: abstract class Animal{ 2: String name; 3: Animal(String n){ 4: name = new String(n); 5: } 6: void eat(){ 7: System.out.printf("%s eats!%n", name); 8: } 9: abstract void makeSound(); 10: }
We can see the use of a new keyword, abstract, above. abstract is used as a class modifier and as a method modifier. All classes with abstract methods must themselves be abstract classes. Classes which inherit from abstract classes naively, must also necessarily be abstract:
1: class Mammal extends Animal{ 2: Mammal(String n){ 3: super(n); 4: } 5: void breathe(){ 6: System.out.printf("%s breathes!%n", name); 7: } 8: }
If I try to compile the above I get:
josephraskind@stargazer:/tmp/abstract_classes$ javac Mammal.java
Mammal.java:1: error: Mammal is not abstract and does not override abstract method makeSound() in Animal
class Mammal extends Animal{
^
1 error
error: compilation failed
What does the above error mean? First, all classes are abstract which have at least one abstract method. Since Mammal is a subclass of Animal it has one abstract method makeSound(). Second, if a class is abstract it must be modified with the abstract keyword.
What's the usefulness of an abstract class if I can never instantiate it and all subclasses are abstract? Let's take a look at the error message in Listing 3: "Mammal is not abstract and does not override abstract method makeSound() in Animal". Ah ha! We must override an abstract method to dissolve its abstract​ness. Mammal is still a bit abstract to deserve an implemenation of makeSound(), so we can tag is as abstract and move on to Dog:
1: abstract class Mammal extends Animal{ 2: Mammal(String n){ 3: super(n); 4: } 5: void breathe(){ 6: System.out.printf("%s breathes!%n", name); 7: } 8: } 9: 10: final class Dog extends Mammal{ 11: Dog(String n){ 12: super(n); 13: } 14: void eat(){ 15: System.out.printf("%s eats meat!%n", name); 16: } 17: void makeSound(){ 18: System.out.printf("%s barks!%n", name); 19: } 20: }
The above compiles nicely! This is because Dog finally provides an implementation for makeSound() which decrements the number of fully abstract methods to 0. That turns Dog from a potentially abstract class to a fully concretized class.
As seen on Lines 6-8 in Listing 1, abstract classes may have concrete implementations of methods when shared behavior is desired.
Abstract Class Members and Instantiation
As seen above in Listing 1, abstract classes are allowed to have instance fields and instance methods. However, that does not mean that an abstract class can be instantiated in its own right:
1: public class Farm{ 2: public static void main(String[] args){ 3: Animal a = new Animal("Lassie"); 4: } 5: }
If we try to compile this we get:
josephraskind@stargazer:/tmp/abstract_classes$ javac Farm.java
Farm.java:51: error: Animal is abstract; cannot be instantiated
Animal a = new Animal("Lassie");
^
1 error
This should be somewhat intuitive. Animal is still missing an implementation of makeSound() so if it were invoked the Java runtime would have no idea what to do—it would be undefined behavior, something which the language specification avoids as much as possible. However, that does not mean we cannot ever have variables of type Animal:
1: public class Farm{ 2: public static void main(String[] args){ 3: Animal a = new Dog("Lassie"); 4: a.makeSound(); 5: } 6: }
When we compile and run this we get:
1: josephraskind@stargazer:/tmp/abstract_classes$ java Farm.java 2: Lassie barks!
Why does this work? Refer back to the chapter on polymorphism.
Interfaces
Java allows for the declaration of an even-more-abstract class in the form of interfaces. An interface is a totally abstract class where there is absolutely no concrete implemenation at all. Every method within an interface is by definition abstract. We can alter Animal to transform it into an interface:
1: interface Animal{ 2: void eat(); 3: void makeSound(); 4: }
Notice what's happened: we have removed the instance field name and we have gotten rid of the eat() implementation. These are both necessary steps in turning Animal into an interface as they may not have any sort of concrete implementation1. An interface may not have an instance field as an interface is meant to purely as a set of guide rails for further class definitions. Let us do the name to Mammal:
1: interface Mammal extends Animal{ 2: void breathe(); 3: }
We can see that extends works just the same when defining an interface. Here Mammal is defined as a subinterface of Animal which means that it inherits all of the abstract methods associated with Animal. I can now redefine Dog which will implement Mammal:
1: final class Dog implements Mammal{ 2: String name; 3: Dog(String n){ 4: name = new String(n); 5: } 6: public void breathe(){ 7: System.out.printf("%s breathes air!%n", name); 8: } 9: public void eat(){ 10: System.out.printf("%s eats meat!%n", name); 11: } 12: public void makeSound(){ 13: System.out.printf("%s barks!%n", name); 14: } 15: }
We can then write some code to test out this new Dog class:
1: public class Farm{ 2: public static void main(String[] args){ 3: Dog d = new Dog("Lassie"); 4: d.breathe(); 5: d.eat(); 6: d.makeSound(); 7: } 8: }
Compiling and running the above gives us:
josephraskind@stargazer:/tmp/abstract_classes$ java Farm.java Lassie breathes air! Lassie eats meat! Lassie barks!
Because Dog implements Mammal and Dog is not tagged with abstract, Dog must provide a concrete implementation for all abstract methods associated with the Mammal interface.
Why Abstract Classes/Interfaces?
As shown in Listing 7 we can still leverage polymorphism with abstract classes. This is also true with interfaces. For example, if I redefined the Cow and Pig classes from the previous chapter to implement Animal:
1: final class Cow implements Animal{ 2: String name; 3: Cow(String n){ 4: name = new String(n); 5: } 6: public void eat(){ 7: System.out.printf("%s eats grass!%n", name); 8: } 9: public void makeSound(){ 10: System.out.printf("%s moos!%n", name); 11: } 12: } 13: final class Pig implements Animal{ 14: String name; 15: Pig(String n){ 16: name = new String(n); 17: } 18: public void eat(){ 19: System.out.printf("%s eats everything!%n", name); 20: } 21: public void makeSound(){ 22: System.out.printf("%s oinks!%n", name); 23: } 24: }
We can use the old polymorphic example below:
1: public class Farm{ 2: public static void main(String[] args){ 3: Animal[] arr = {new Dog("Lassie"), new Cow("Bessie"), new Pig("Porky")}; 4: for(Animal a : arr) 5: a.makeSound(); 6: } 7: }
Which gives us the same output as before:
josephraskind@stargazer:/tmp/abstract_classes$ java Farm.java Lassie barks! Bessie moos! Porky oinks!
Our use of interfaces solves the problem of upstream classes ruining downstream subclasses. Each implementation is guaranteed to have their own makeSound() method, but each class has its own unique implementation. Of course, what we gain in control over implemenation, we lose in code reuse.
Exercises
The chapter shows that
Animalcannot be instantiated directly once made abstract. Attempt to compile and run the following and record the error:Animal a = new Animal("Generic");
Then assign a
Doginstance to anAnimalreference and verify it compiles. In your own words explain why instantiating an abstract class is prohibited while assigning a subclass instance to an abstract type reference is permitted.The chapter states that a class containing any abstract method must itself be declared abstract. Attempt to compile the following and record the error:
class Shape { abstract double area(); }
Add the
abstractkeyword toShapeand recompile. Then create a concrete subclassCirclewith adouble radiusfield that implementsarea. What happens if you declareCircleabstract as well?- The chapter introduces abstract classes as a way to share concrete method implementations while forcing subclasses to define certain behaviors. Add a concrete method
describetoAnimalthat prints"I am [name] and I say: "followed by the result ofmakeSound. Instantiate aDogand calldescribe. How is it possible forAnimal's concretedescribemethod to callmakeSoundwhenAnimalitself provides no implementation ofmakeSound? - The chapter shows that
Mammalremains abstract because it does not implementmakeSound. Add a concretebreathemethod toMammaland verify thatDoginherits it. Then attempt to instantiateMammaldirectly and record the error. What does this confirm about the relationship between having concrete methods and being instantiable? - Abstract classes can have constructors even though they cannot be instantiated. Add a constructor to
Animalthat prints"An animal is being created!"and verify it is called when aDogis instantiated. Explain in your own words why an abstract class needs a constructor if it can never be directly instantiated. - The chapter motivates abstract classes as a tool for enforcing a contract across a hierarchy. Design your own three-level abstract hierarchy on a topic of your choosing. The top level should be fully abstract, the middle level should implement some but not all abstract methods and add at least one new concrete method, and the bottom level should be fully concrete. Instantiate the bottom level class and verify it has access to all methods defined at every level.