Module 2: Polymorphism and Interfaces
Module Overview
Explore polymorphism, interfaces, and how to use them effectively in Java.
Learning Objectives
- Understand polymorphism and its benefits in Java
- Learn how to create and implement interfaces
- Apply polymorphic design patterns to make code more flexible
- Recognize when to use interfaces versus abstract classes
Key Concepts
Polymorphism
Polymorphism is the ability to process objects differently based on their data type. It allows a single interface to represent different underlying types, enabling more flexible and reusable code.
Benefits of Polymorphism:
- Code reusability and maintainability
- Simplified method calls across different types
- Runtime determination of which implementation to execute
- Ability to extend functionality without modifying existing code
Example:
// Interface defining a contract
public interface Vehicle {
void start();
void stop();
void accelerate();
}
// Implementation for a Car
public class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car engine starts with ignition key");
}
@Override
public void stop() {
System.out.println("Car stops by applying brakes");
}
@Override
public void accelerate() {
System.out.println("Car accelerates by pressing gas pedal");
}
}
// Implementation for an Electric Bike
public class ElectricBike implements Vehicle {
@Override
public void start() {
System.out.println("Electric bike starts with power button");
}
@Override
public void stop() {
System.out.println("Bike stops by applying brakes");
}
@Override
public void accelerate() {
System.out.println("Bike accelerates by twisting throttle");
}
}
// Using polymorphism
public class VehicleTest {
public static void main(String[] args) {
// Vehicle reference can hold different implementations
Vehicle myCar = new Car();
Vehicle myBike = new ElectricBike();
// Same method calls, different behaviors
myCar.start(); // "Car engine starts with ignition key"
myBike.start(); // "Electric bike starts with power button"
}
}
Motivation
Imagine you were writing some code to travel from one place to another, but you didn't know (or care) what type of vehicle would be used. You're probably more concerned with navigating the directions and steering the vehicle than how many wheels the vehicle had, whether it had a steering wheel or handlebars, or what type of fuel is used. Let's oversimplify even more and say that you are only concerned with steering the vehicle. As long as you can do that with a vehicle, you should be able to use that vehicle in your application.
To be more specific, let's say that you just need to turn left or right. Instead of learning a series of methods to call on a car, a truck, a tractor, or bicycle to achieve this, wouldn't it be great if you could just count on a turnLeft() and a turnRight() method, and let the particular vehicle in question figure out how to make that happen? Letting your code worry about navigation and use the same method calls to steer the vehicle no matter the vehicle type is an example of polymorphism, a key characteristic of object-oriented programming.
Polymorphism
Polymorphism is the property of a language like Java that lets you define a set of callable methods on a variable in your code regardless of the actual class that the variable points to. You may not know or care what the actual class of the object will be when the code is running. That simplifies things by giving you a "layer of abstraction". This means that you can ignore the implementation details of how the various classes actually implement those methods. You only care that those methods exist and are callable in your code. In the example above, you don't care that the vehicle is a Car or Tractor or Bicycle, you just want to be able to steer it. So instead of declaring your vehicle as a specific vehicle type, we'd like to declare it as a general type of object that "can be steered", or maybe Steerable? Let's compare the two worlds:
NavigationApp needs to be aware of each vehicle type to be able to steer it:
public class NavigationApp {
private Car car; // in case the vehicle is a car
private Tractor tractor; // in case the vehicle is a tractor
private Bicycle bicycle; // in case the vehicle is a bicycle
...
public TripInfo navigate(Location startLocation, Location endLocation) {
ArrayList<TripStep> tripSteps = findPath(startLocation, endLocation);
for (TripStep step : tripSteps) {
// turn left, with instructions specific to each
// possible vehicle type
if (step.equals(LEFT_TURN)) {
if (car != null) {
car.turnOnLeftBlinker();
car.turnSteeringWheelLeft();
...
}
if (tractor != null) {
// tractor-specific instructions
}
if (bicycle != null) {
// bicycle-specific instructions
}
}
// ... turn right etc.
}
}
}
NavigationApp just wants to be able to steer the thing and just worry about navigating:
public class NavigationApp {
// Polymorphism: you can have one variable here, even though
// you don't know what kind of vehicle it will be
private Steerable vehicle;
public TripInfo navigate(Location startLocation, Location endLocation) {
ArrayList<TripStep> tripSteps = findPath(startLocation, endLocation);
for (TripStep step : tripSteps) {
// turn left
if (step.equals(LEFT_TURN)) {
// Polymorphism: You can call turnLeft() on the
// vehicle object, even though you don't know
// (or care) if it will be a car/tractor/bicycle...
vehicle.turnLeft();
}
// ... turn right etc.
}
}
}
Here the "abstraction" your NavigatorApp is dealing with is a Steerable, which is going to make your logic a lot simpler than needing to deal with a lower-level (more detailed, less abstract) abstraction like Car or Tractor. No matter what the actual vehicle type is, we know it's Steerable, so we can turn left and right the same way with any of the vehicles.
One more thing: when you declare your variables, you can use interfaces as the variable's type, just as we did above with the Steerable variable, vehicle. However, when you're actually instantiating an object, you can't actually instantiate an interface. Why is that? The interface defines what the object can do, not how it does it. The interface doesn't have the method implementations to run. So you instantiate a class that has the appropriate method implementations. This means we might have lines of code that look something like this:
Steerable myVehicle = new Car();
At first, this may look a little weird because we're used to the variable type and the class we instantiate being the same. But this is also part of the key of polymorphism, the type of our variable and the type of the actual object that the variable points to can be different. The variable type will be more generic than the class that gets instantiated. Several different classes might be instantiated and assigned to the same interface type variable. This makes sense: we need to have method definitions in the actual object we are passing around. We often refer to the actual object type as the "concrete class" that myVehicle happens to be. In this example, Car is the concrete class that satisfies the Steerable interface.
"is-a" vs "has-a"
The relationship between the concrete class and the interface that it implements is often referred to as an "is-a" relationship. A Car is-a Steerable, because if you have a Car instance, you can treat it as a Steerable. This is another way to describe what polymorphism does for us. The two types are different, but one is a specific sub-type of the other.
In object-oriented programming, we often refer to a "has-a" relationship when one object contains another. Our class diagrams show examples of one class containing another or one class "has-a" another class. For example, in the Unit 1 project, we have An Order class that has-a OrderItem. (In fact, the Order has many OrderItem objects.) The Order class has a variable that points to a list of OrderItem objects.
Notice that in both cases, the relationship has a direction. A Car is-a Steerable, but a Steerable isn't necessarily always a Car (e.g., tractor). Similarly Order has-a (or has-many) OrderItem, but OrderItem does not contain an Order.
These two types of relationships are the key relationships to think about in object-oriented programming and what to think about when creating your class diagrams. As you may have seen by now in your project diagramming, these relationships in the diagram are directional as well. The concrete class points to the interface that it implements. The class that contains the other has the diamond attached to it.
Anatomy of an interface
An interface definition is similar to a class definition in Java, except its methods are declared without any implementation. We also use the keyword, interface, instead of class at the top of the file. For our example above, the Steerable interface might look something like this:
public interface Steerable {
void turnLeft();
void turnRight();
}
Notice that the method signature is defined in the interface, but there is no method implementation. Instead of curly braces and lines of code following the method name, we just have a semicolon. This is not calling turnLeft() and turnRight(). The interface is defining the method signatures. These are the methods that a class that wants to be considered a Steerable must implement. If they implement them, they can be treated as a Steerable in our code.
We usually leave off the public keyword for the interface methods, as they are going to be treated as public by Java regardless. You need to be able to call these methods for the class to satisfy the required interface methods.
A class implements an interface
Now, what does the class that wants to be treated as a Steerable look like? For example, our Car class might look something like this:
public class Car implements Steerable {
@Override
public void turnLeft() {
turnOnLeftBlinker();
turnSteeringWheelLeft();
...
}
@Override
public void turnRight() {
turnOnRightBlinker();
turnSteeringWheelRight();
...
}
}
Notice two things:
- The class must declare that it implements Steerable in the class declaration
- The "@Override" before method. Just do this in your code as well. We'll cover what this is in just a bit
- The class must provide implementations of the methods defined in the interface. If either of them is missing, the Car class will not compile
That second point is the most important to remember. The interface is setting the rules about what can be considered a Steerable. It's often referred to as a "contract" that the interface defines: "As long as you implement these methods, I'll let you into the Steerable club. If you don't implement these specific methods, you're in breach of contract!"
@Override
Okay, back to that @Override business. This is called an "annotation" in Java that lets us tell the Java compiler there is something special about this method. In this case, we're telling the compiler that we are "overriding" a method defined in the interface that this class implements.
It's not strictly necessary to include the annotation, but it can help you discover a bug where you think you're implementing the interface method, but you actually aren't (e.g., misspelling, a different set of parameters). We recommend always including the @Override annotation on your interface-implementing methods/
What else can an interface contain?
A Java interface can also contain public static final variables (essentially, constants). We tend to discourage doing this. You can use a class to define these if necessary.
Java has also added the ability to provide "default" implementations of methods in the interface. Yes, we just got through telling you that interfaces declare methods but don't implement them! It turns out you can put method implementations in your interface, and these methods can call methods defined by the interface. Please don't do this, though. This 'feature' was deemed necessary to introduce some new features to existing Java classes/interfaces without breaking decades' worth of code. At BloomTech, you should not need to create default implementations.
Anything else?
We probably won't run into this directly, but a class can implement more than one interface. Take a look at the ArrayList javadoc, for example. ArrayList in Java8 implements six interfaces at the same time:
- Serializable,
- Cloneable,
- Iterable,
- Collection,
- List,
- RandomAccess.
Most of these interfaces include specific methods that the ArrayList class must (and does) implement. This also means that if you have an ArrayList you could store it in any of these variable types, it does not have to be an ArrayList! In fact, very soon, we'll start storing our ArrayList objects in List variables. This would allow us to more easily replace the ArrayList with another class that also implements List if we want to down the line. Most of the time, our code doesn't care if it's actually an ArrayList. We usually just call the methods defined in List (add(), remove(), get() etc.).
Some interfaces don't define any methods at all, often referred to as "marker interfaces". These signify a property that a class has even without implementing a specific method. One example is Java's Serializable interface, which contains no methods. It's used to signal to the compiler that a class can be written to a String or a file and recreated later as an object. We'll cover serialization in a later lesson; we just wanted to give you a heads up that you might run into an empty interface.
Another polymorphism analogy
As another example, let's assume we're in the office of a newspaper firm. The president of the newspaper has brought in doughnuts, and people are gathered around a table eating them. At some point, the president says, "Everyone, get back to work."
- Sue, the Editor, goes back to reviewing the latest editorial.
- Jim, the Designer, starts coming up with new layouts for next month's edition.
- Stephanie, the Journalist, picks up her phone and starts calling her sources, looking for the next scoop.
- Jessica, the Software Developer, starts looking at urgent bugs that internal users have pointed out to her.
However, the president said, "Everyone, get back to work" once. The president doesn't have to know what Editors do or Designers do. The president is not a detailed person. They just say, "get back to work." The message "get back to work" is a polymorphic message.
If this were Java code, it could look like this:
public interface CanDoWork {
void work();
}
public class Editor implements CanDoWork {
@Override
public void work() {
System.out.println("I'm reviewing an editorial...");
}
}
public class Designer implements CanDoWork {
@Override
public void work() {
System.out.println("I'm coming up with new layouts...");
}
}
public class Journalist implements CanDoWork {
@Override
public void work() {
System.out.println("I'm calling my sources...");
}
}
public class SoftwareEngineer implements CanDoWork {
@Override
public void work() {
System.out.println("I'm fixing bugs...");
}
}
There are many kinds of roles in a company, yet they can all do work. By implementing CanDoWork, the president can just tell them all to get back to work!
public class President {
// Everyone is treated as if they are a CanDoWork instance. The president does not need to know what they
// specifically work on.
CanDoWork editor = new Editor();
CanDoWork designer = new Designer();
CanDoWork journalist = new Journalist();
CanDoWork softwareEngineer = new SoftwareEngineer();
public void breakTimeIsOver() {
editor.work();
designer.work();
journalist.work();
softwareEngineer.work();
}
}
Interfaces
An interface is a contract that defines what a class can do without specifying how it does it. Interfaces contain method signatures without implementations and can include constant values.
Characteristics of Interfaces:
- Define methods without implementation (abstract by default)
- Cannot be instantiated directly
- Classes can implement multiple interfaces
- All fields in interfaces are implicitly public, static, and final
Example:
// Interface definition
public interface Sortable {
// Constants (implicitly public, static, final)
int ASCENDING = 1;
int DESCENDING = -1;
// Abstract methods (no implementation)
void sort(int direction);
boolean isSorted();
}
// Class implementing the interface
public class SortableArray implements Sortable {
private int[] data;
public SortableArray(int[] data) {
this.data = data;
}
@Override
public void sort(int direction) {
// Implementation of sorting algorithm
if (direction == ASCENDING) {
// Sort in ascending order
} else {
// Sort in descending order
}
}
@Override
public boolean isSorted() {
// Implementation to check if array is sorted
return true; // Placeholder
}
}
"Is-a" vs "Has-a" Relationships
In object-oriented programming, understanding the relationships between classes is crucial:
"Is-a" Relationship (Inheritance/Implementation):
- Established through class inheritance or interface implementation
- Example: A Car "is-a" Vehicle, a Circle "is-a" Shape
- Represented by extends or implements keywords
"Has-a" Relationship (Composition):
- Established when one class contains an instance of another class
- Example: A Car "has-a" Engine, a Library "has-a" collection of Books
- Implemented by declaring member variables
Example:
// "Is-a" relationship example
public interface Shape {
double area();
double perimeter();
}
public class Circle implements Shape { // Circle "is-a" Shape
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}
// "Has-a" relationship example
public class Car {
private Engine engine; // Car "has-a" Engine
private Wheel[] wheels;
public Car() {
engine = new Engine();
wheels = new Wheel[4];
for (int i = 0; i < 4; i++) {
wheels[i] = new Wheel();
}
}
public void start() {
engine.turnOn();
}
}