Interfaces and Abstract Classes - softwareconstruction240/softwareconstruction GitHub Wiki
🖥️ Slides
📖 Required Reading: Core Java for the Impatient
- Chapter 3:
- Section 1 - Interfaces
- Section 2 - Static, Default, and Private Methods
- Section 3 - Examples of Interfaces
- Chapter 4:
- Section 1 - Extending a Class
- Section 2 - Object: The Cosmic Superclass
- Section 3 - Enumerations
Polymorphism is the blanket term used in computer science to represent the idea of morphing an object to fit into many (i.e. poly) different contexts. In Java, the use of inheritance and abstract classes are the primary ways to provide polymorphism. You use the extends
keyword to inherit another class's functionality, and you use the implements
keyword to inherit an interface definition. Polymorphism allows you to decouple, or abstract, a class's internals, from how it is used. That makes it so you can significantly alter the class without having to change how the class is used.
Interfaces allow you to define what something does, without specifying how it does it. It also allows you to create and supply alternative implementations for the interface. For example, you can have an interface that defines what a List
can do, and then create classes that provide different implementations of the List
. Perhaps one implementation uses less memory, and a different one is faster. You can then write code that uses the List
interface and not have to think about if it is using the fast version or the memory efficient version.
public interface List<E> extends SequencedCollection<E> {
void add(int index, E element);
E remove(int index);
int size();
void clear();
ListIterator<E> listIterator();
}
The following example shows two implementations of a List
. One that uses an array, and one that uses a linked list. The two lists can be passed to a function, addAndPrint
in this case, that doesn't know anything about the implementation of the list, it just knows that it can call the interface's add
method.
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
public class ListExample {
public void listExample() {
// Represent two different implementations of the List interface.
List<String> list1 = new ArrayList<>();
List<String> list2 = new LinkedList<>();
addAndPrint(list1, "vanilla");
addAndPrint(list2, "taco");
}
// This function takes any implementation of the List interface.
private void addAndPrint(List<String> list, String value) {
// The add method is defined on the list interface.
list.add(value);
System.out.println(value);
}
}
Note that, because addAndPrint()
doesn't know anything about list
's type except that it implements the List
interface, it can only call methods on list
that are declared in the interface. If LinkedList
had an additional method that was not declared in the interface, and addAndPrint()
tried to call that method on list
, it would not compile, even if it was passed a LinkedList
.
In addition to using a class that implements one of the JDK standard interfaces, you can write your own interface implementations, and even define completely new interfaces. Creating an interface is similar to creating a class, except you use the interface
keyword and only define the signature of the methods that the interface represents.
For example, if you wanted to create a specialized iterator that returned each character in a string you could write the following:
public interface CharIterator {
/** Returns true if there is another character to iterate. */
boolean hasNext();
/** Returns the next character. */
char next();
}
You can then implement the CharIterator
interface by specifying the implements
keyword after the class name declaration, and writing each of the methods defined by the interface. The @Override
annotation is not technically necessary, but it makes it clear that the method is part of an interface.
public class AlphabetIterator implements CharIterator {
int current = 0;
String charString = "abcdefg";
@Override
public boolean hasNext() {
return current < charString.length();
}
@Override
public char next() {
return charString.charAt(current++);
}
}
In the discussion for classes and objects we showed how you can inherit code from another class and treat it as if the code was included in a derived class. By default, all classes in Java derive from the Object class. That means they inherit
the Object classes fields and methods. You can also explicitly state what class you derive from by using the extends
keyword. In the following example, the SubClass
extends the SuperClass
and uses the SuperClass's toString method to implement its toString method. SubClass can do this because everything in the derived class literally becomes part of the subclass just as if the code had been written in the subclass.
public static class SuperClass extends Object {
String name = "super";
public String toString() {
return name;
}
}
public static class SubClass extends SuperClass {
public String toString() {
return "Sub-class of " + super.toString();
}
}
Abstract classes provide another type of polymorphism. However, unlike interfaces, where the subclass implements all of the functionality of the interface, a base class that is an abstract class provides some of the implementation and leaves other methods to be implemented by the subclass.
The JDK's Iterator
interface allows you to walk through, or iterate, through a collection of objects.
public interface Iterator<E> {
boolean hasNext();
E next();
We can create a class that implements the iterator interface by returning the characters of a string, but also defines a new abstract method for iterating that allows for a string to be prefixed to each iteration result. This is done by specifying the abstract
keyword on the nextWithPrefix
method, without providing an implementation of the method.
You can think of abstract classes as a class/interface hybrid.
/**
* The abstract keyword signifies that this class contains methods
* that must be implemented by a subclass
*/
public static abstract class AlphabetIterator implements Iterator {
int current = 0;
String charString = "abcdefg";
public boolean hasNext() {
return current < charString.length();
}
public Object next() {
return charString.substring(current, ++current);
}
/**
* This method is not implemented by the abstract class and
* so it must be implemented by the subclass.
*/
public abstract String nextWithPrefix(String prefix);
}
A subclass extends
the abstract class by providing an implementation of the abstract nextWithPrefix
method. Additionally, for instructive purposes, we also override the next
method to include a default prefix that represents the numerical order of the iterated items.
Note the use of the super
keyword in the nextWithPrefix
function. super
allows you to access methods in the abstract base class when you have methods with the same name as the subclass. In this example, it is used to access the abstract class's next
function instead of the next
function implemented by PrefixAlphabetIterator
. Without the use of super
, the call to next
would have created an infinite loop.
public static class PrefixAlphabetIterator extends AlphabetIterator {
public String next() {
return nextWithPrefix(String.format("%d.", current + 1));
}
public String nextWithPrefix(String prefix) {
return String.format("%s. %s", prefix, super.next());
}
}
Like interfaces, the name of the abstract class can be used to represent the subclass when passed to functions that expect the abstract class. In the following code, we create an object of the type PrefixAlphabetIterator
and then pass it to a function that expects the abstract class AlphabetIterator
.
public static void main(String[] args) {
var iter = new PrefixAlphabetIterator();
print(new PrefixAlphabetIterator());
}
public static void print(AlphabetIterator iter) {
while (iter.hasNext()) {
System.out.println(iter.nextWithPrefix("+ "));
}
}
Polymorphism is great because it makes it so code only needs to know about the provided interface. For example, if you want to have a list that can contain any type of object that extends the Object
class, it can safely ignore the fact that those objects are also things like String, Integer, Person, or Map classes. However, sometimes you need to know if an object is of a specific type so that you can use it in different ways. This is where the instanceof
operator comes in handy. instanceof
will return true, if the provided variable is of the given type (including if it extends or implements it). A simple example of its use is demonstrated with a test to see if a string literal is an instance of type String
.
if ("I am a string" instanceof String) {
// Always true
}
In the following example, we create a list that contains objects of different primitive types. We then iterate over this list and use the instanceof
operator to execute different code based on the type of the list item.
public static void main(String[] args) {
List<Object> list = List.of('a', "b", 3);
for (var item : list) {
if (item instanceof String) {
System.out.println("String");
} else if (item instanceof Integer) {
System.out.println("Integer");
} else if (item instanceof Character) {
System.out.println("Character");
}
}
}
Starting in Java version 17 the pattern matching version of instanceof that automatically casts the object if it is of the specified type. This makes it very convenient to both test and cast in the one statement.
public class PatternMatchInstanceOfExample {
public static void main(String[] args) {
List<Object> list = List.of('a', "b", 3);
for (var item : list) {
if (item instanceof String stringItem) {
System.out.println(stringItem.toUpperCase());
} else if (item instanceof Integer intItem) {
System.out.println(intItem + 100);
} else if (item instanceof Character charItem) {
System.out.println(charItem.compareTo('a'));
}
}
}
}
If you don't want a subclass to override a method in a base class then you can prefix the method with the keyword final
. You can also use the final keyword to make a class's field value immutable. (You can still call methods on final fields, however, so it's not the same as the C++ const
.)
public class FinalExample {
/** This variable cannot be changed */
final double PI = 3.14;
/** This method cannot be overridden */
final public double getPI() {
return PI;
}
}
WIth what you have learned here, consider the Chess program. How should chess pieces be abstracted? Should the piece type be represented as a data field?
classDiagram
class ChessPiece {
TeamColor
PieceType
pieceMoves()
}
Or would it be more appropriate to represent it using abstract class inheritance?
classDiagram
class ChessPiece {
TeamColor
pieceMoves()
}
ChessPiece <|-- King
ChessPiece <|-- Queen
ChessPiece <|-- Pawn
class King {
pieceMoves()
}
class Queen {
pieceMoves()
}
class Pawn {
pieceMoves()
}
Or should the rules be abstracted out of the chess so that the chess piece only represents the properties of the piece and not the rules of a game?
classDiagram
class ChessPiece {
TeamColor
pieceMoves()
}
class Rule {
<<interface>>
pieceMoves()
}
KingRule --|> Rule
QueenRule --|> Rule
PawnRule --|> Rule
class KingRule {
pieceMoves()
}
class QueenRule {
pieceMoves()
}
class PawnRule {
pieceMoves()
}
Can I also use abstract classes, interfaces, and inheritances to build something that turns the dial even more? Or is this taking things too far? All of these questions are part of learning how to design software.
Here is a possible design to incorporates all of the abstraction concepts into an architecture for representing rules.
classDiagram
class Rules {
getRule()
}
Rules --> MovementRule
class MovementRule {
<<interface>>
pieceMoves()
}
class BaseMovementRule {
<<abstract>>
private calculateMoves()
pieceMoves()
}
BaseMovementRule --|> MovementRule
KingRule --|> BaseMovementRule
QueenRule --|> BaseMovementRule
PawnRule --|> BaseMovementRule
class KingRule {
pieceMoves()
}
class QueenRule {
pieceMoves()
}
class PawnRule {
pieceMoves()
}
- What polymorphism is
- What abstract classes are and when to use them
- What interfaces are and when to use them
- How interfaces and abstract classes are both similar and different
- How to implement an interface in a class
- How to create an interface
- How to do inheritance in Java