In the previous section, we established that types act as "contracts" enforced by the Java compiler, ensuring safety by checking your code before it runs. Now, let’s connect this idea to the class relationships you’ve already learned about: inheritance (extends) and interfaces (implements), concepts you’ve learned about in previous chapters. You’ve likely used these features, perhaps creating a SavingsAccount extends BankAccount or similar structures. But maybe you’ve also been frustrated when a method you knew existed in your specific subclass wasn’t directly usable through a variable of the superclass type. Why does that happen? It’s because these mechanisms specifically define how type contracts relate to each other, and the compiler strictly follows those relationships.
Subsection10.3.1Inheritance (extends) as Contract Extension
When you declare a class using extends, like public class Car extends Vehicle, you’re telling the compiler more than just that a Car reuses code from Vehicle. You are declaring that a Car fulfills the entire public contract of a Vehicle (and its protected contract available to subclasses). It promises to provide all the non-private methods and fields defined by Vehicle, potentially adding its own specific features or overriding some behaviors. This establishes the "is-a" relationship which directly influences how the compiler checks your code. Defining these relationships clearly aligns with Step 1 (Data Definitions) and Step 2 (Method Signatures) of our Design Recipe, helping ensure correctness and clarity at compile time.
Vehicle (Superclass Contract)
↑
│ extends (Inherits and Extends Contract)
Car (Subclass Contract)
The compiler uses this "is-a" understanding to allow for safe substitution. Because a Car guarantees it can do everything a Vehicle can (according to the Vehicle contract), you can assign a Car object to a Vehicle variable:
// Assuming Car extends Vehicle
Vehicle myVehicle = new Car(/* constructor args */); // OK! A Car fulfills the Vehicle contract.
// We can call methods defined in the Vehicle contract
myVehicle.accelerate(10.0); // Assumed defined in Vehicle
// double speed = myVehicle.getSpeed(); // Also OK if getSpeed() is in Vehicle
This works because the compiler knows, based on the extends relationship, that any object assigned to myVehicle is guaranteed to have the methods defined in the Vehicle class contract.
Crucially, the declared type of the variable (here, Vehicle) determines the contract enforced by the compiler for direct method calls on that variable, not the actual type of the object it happens to hold at runtime (here, a Car). Therefore, you cannot directly call methods specific only to the Car class using the Vehicle variable:
// Assume Car has a specific method like openTrunk() not present in Vehicle
// Vehicle myVehicle = new Car(...);
// myVehicle.openTrunk(); // Compile Error! Method openTrunk() undefined for type Vehicle.
This results in a compile error because the method openTrunk() is not part of the Vehicle contract that the myVehicle variable promises to uphold. The compiler only guarantees what’s defined by the variable’s declared type.
Casting in Java is the process of explicitly telling the compiler to treat an object reference as if it were of a different type (usually a more specific subtype). For example, (Car) myVehicle; attempts to cast the myVehicle reference to the Car type. To access Car-specific methods through a Vehicle variable, you would need to perform such a cast, typically after ensuring it’s safe using a runtime check with instanceof:
Vehicle myVehicle = new Car(...);
if (myVehicle instanceof Car) {
Car myCar = (Car) myVehicle; // Cast the Vehicle reference to a Car reference
myCar.openTrunk(); // OK! Now the compiler knows it has the Car contract.
}
While casting works, it moves the essential type verification from the compiler (compile time) to the JVM (runtime). This is inherently riskier because if the object isn’t actually a Car when the cast executes (perhaps due to a logic error elsewhere), a ClassCastException occurs, crashing the program. Compile-time checks prevent such crashes entirely. If you find yourself using frequent casting, it might indicate your design could benefit from more precise type contracts or potentially features like Generics, which we’ll explore soon. Furthermore, if the class contract changes later (e.g., the openTrunk method is renamed or removed from Car), the compiler immediately catches errors in code directly using a Car reference, but the runtime cast might only fail later during execution if not properly updated, leading to unexpected crashes.
Subsection10.3.2Interfaces (implements) as Added Contracts
Interfaces work similarly but define contracts based on capabilities ("can-do" or "capability-based" relationships) rather than a hierarchical "is-a" relationship. When a class declares that it implements an interface, like public class Circle implements Drawable, it’s making a promise to the compiler: "I guarantee that I provide implementations for all the methods defined in the Drawable interface contract." Again, this definition of capabilities fits within Step 1 and Step 2 of the Design Recipe.
// Simple interface contract
public interface Drawable {
void draw(); // Contract: Implementing classes must provide a draw() method
}
// Class fulfilling the contract
public class Circle implements Drawable {
private double radius;
// Constructor...
@Override
public void draw() {
System.out.println("Drawing a circle with radius " + radius);
}
public double getRadius() { // Circle-specific method, not part of Drawable contract
return radius;
}
}
Because the Circle class fulfills the Drawable contract, the compiler allows safe substitution:
Drawable shape = new Circle(/* constructor args */); // OK! A Circle fulfills the Drawable contract.
// We can call methods defined in the Drawable contract
shape.draw();
The compiler permits this because it knows any object assigned to shape is guaranteed to have the draw() method defined in the Drawable contract.
As with inheritance, the variable shape’s declared type (Drawable) governs the compiler’s checks. You cannot directly call methods specific to the Circle class using the Drawable variable:
// Drawable shape = new Circle(...);
// double r = shape.getRadius(); // Compile Error! Method getRadius() undefined for type Drawable.
This gives a compile error because getRadius() is part of the Circle’s specific contract, not the general Drawable contract held by the shape variable. Accessing it would require a cast, similar to the inheritance example.
Subsection10.3.3Understanding Relationships for Flexibility
In summary, extends and implements are powerful tools that define relationships between type contracts. Inheritance (extends) signifies an extension and specialization of a contract ("is-a"), while interfaces (implements) signify the addition of capability contracts ("can-do"). The compiler leverages these declared relationships to allow for safe substitution – treating more specific objects (like a Car or Circle) through the lens of their more general contract (Vehicle or Drawable). This simplifies code and ensures flexibility, enabling related types to be treated uniformly as long as they adhere to the same shared contract. This ability to treat related types uniformly based on their shared contracts is the essence of polymorphism, which we will explore in action in the next section. As we’ll see shortly, Java provides an elegant solution called Generics to address some of the rigidity we encounter when using inheritance and interfaces alone, especially concerning casting and type safety in collections.
The Dog contract completely replaces the Animal contract.
Incorrect. Inheritance means Dog inherits and extends the Animal contract; it doesn’t replace it.
A Dog object is guaranteed to fulfill the public contract defined by the Animal class.
Correct. The extends keyword establishes an "is-a" relationship, meaning the subclass contract includes the public (and protected) parts of the superclass contract.
An Animal object can always be treated as a Dog.
Incorrect. While a Dog can be treated as an Animal, the reverse is not true without an explicit (and potentially unsafe) cast.
The Dog class can only access private members of the Animal class.
Incorrect. Subclasses cannot access private members of the superclass directly. They inherit public and protected members.
Why does the code Vehicle myVehicle = new Car(); myVehicle.openTrunk(); cause a compile error if openTrunk() is defined only in Car and not in Vehicle?
Because myVehicle might actually hold a Bicycle object at runtime.
Incorrect. While true that myVehicle could hold other subtypes, the compile error occurs because the *declared type* Vehicle dictates the contract, not the runtime object type.
Because calling subclass-specific methods is never allowed in Java.
Incorrect. It is allowed, but requires casting the reference to the specific subclass type first.
Because the compiler only checks method calls against the contract of the variable’s declared type (Vehicle), which doesn’t include openTrunk().
Correct. The compiler enforces the contract based on the variable’s type (Vehicle), ignoring the fact it currently holds a Car unless explicitly cast.
Because Car must override all methods from Vehicle.
Incorrect. Subclasses only need to override methods if required (e.g., abstract methods) or desired; adding new methods is common.
It causes compile-time errors if the subclass method doesn’t exist.
Incorrect. The primary risk is at runtime. If the cast itself is invalid, a runtime error occurs before any method call.
It makes the code significantly slower due to the casting operation.
Incorrect. While there might be a minuscule performance cost, the main risk is related to type safety, not speed.
It prevents the use of polymorphism.
Incorrect. Casting is often used *after* polymorphism has allowed different object types to be stored or passed using a common supertype reference.
It moves type checking from compile time to runtime, potentially causing a ClassCastException if the object isn’t of the expected type.
Correct. Casting bypasses the compiler’s guarantee for that specific operation, deferring the check to runtime where it can fail with a ClassCastException if the object’s actual type is incompatible with the cast type.
Incorrect. Implementing an interface establishes a "can-do" relationship (fulfills a contract), not an "is-a" subtype relationship in the sense of class inheritance.
That Button guarantees to provide implementations for all methods defined in the Clickable interface.
Correct. The implements keyword signifies a promise to fulfill the contract specified by the interface by implementing its methods.
That Clickable can access all public methods of Button.
Incorrect. References of the interface type (Clickable) can only access methods defined within the Clickable interface itself.
That Button inherits fields from Clickable.
Incorrect. Interfaces (prior to default methods providing some implementation) primarily define method signatures (contracts) and constants, not inheritable instance fields.