7.2
Abstraction, Polymorphism, Abstract Classes, and Interfaces
Program to a shared contract while each object keeps its own implementation.
Previously on
Section 7.1 made AirUnit a subtype of Unit. That gave the child shared state and behaviour.
This lesson uses the type relationship itself. One variable will hold several kinds of unit, and each object will answer the same request differently.
The problem
A report must count deployable units. Each concrete type uses a different rule.
static boolean isDeployable(Object value) {
if (value instanceof GroundUnit) {
GroundUnit unit = (GroundUnit) value;
return unit.getReadiness() >= 60;
}
if (value instanceof AirUnit) {
AirUnit unit = (AirUnit) value;
return unit.getReadiness() >= 60 && unit.isWeatherClear();
}
if (value instanceof NavalUnit) {
NavalUnit unit = (NavalUnit) value;
return unit.getReadiness() >= 50;
}
return false;
}Every new unit type requires another branch. Every report must know every concrete class.
The decision belongs with the object that owns the required state. The report needs one question: isDeployable().
The idea
Put the common operation on the parent type
abstract class Unit {
private final String name;
private final int readiness;
Unit(String name, int readiness) {
this.name = name;
this.readiness = readiness;
}
String getName() {
return name;
}
int getReadiness() {
return readiness;
}
abstract boolean isDeployable();
}Unit defines shared state and the required operation. It does not invent one deployability rule that fits every subtype.
The subclasses complete the missing method:
class GroundUnit extends Unit {
GroundUnit(String name, int readiness) {
super(name, readiness);
}
@Override
boolean isDeployable() {
return getReadiness() >= 60;
}
}
class AirUnit extends Unit {
private final boolean weatherClear;
AirUnit(String name, int readiness, boolean weatherClear) {
super(name, readiness);
this.weatherClear = weatherClear;
}
@Override
boolean isDeployable() {
return getReadiness() >= 60 && weatherClear;
}
boolean isWeatherClear() {
return weatherClear;
}
}
class NavalUnit extends Unit {
NavalUnit(String name, int readiness) {
super(name, readiness);
}
@Override
boolean isDeployable() {
return getReadiness() >= 50;
}
}Now the report depends only on Unit:
static int countDeployable(Unit[] units) {
int count = 0;
for (Unit unit : units) {
if (unit.isDeployable()) {
count++;
}
}
return count;
}The array may contain all three concrete classes:
Unit[] units = {
new GroundUnit("Atlas", 70),
new AirUnit("Kite", 80, false),
new NavalUnit("Tide", 55)
};
System.out.println(countDeployable(units)); // 2Each element variable has declared type Unit. Each referenced object retains its real class.
The expression unit.isDeployable() has one source form. Different method bodies run for different objects. That is polymorphism.
Abstraction separates the request from its implementation
The report knows what it can ask: whether a unit is deployable. It does not know how each type decides.
That separation is abstraction. A useful abstraction exposes the operation a client needs and hides details the client should not coordinate.
Encapsulation and abstraction are related but not identical:
| Encapsulation | Abstraction | |
|---|---|---|
| Main question | Who may access or change this state? | What operation should the client see? |
| Typical tools | private fields and controlled methods | parent types, abstract methods, and interfaces |
| Benefit | preserves invariants | lets callers ignore implementation choice |
Under the hood
Going deeperReference type and object type do different jobs
Unit unit = new AirUnit("Kite", 80, true);Unit is the variable’s declared type. AirUnit is the object’s run-time class.
The compiler uses the declared type to check which members can be called:
unit.getName(); // allowed: Unit declares it
unit.isDeployable(); // allowed: Unit declares it
// unit.isWeatherClear(); // not allowed if Unit has no such methodThe JVM uses the receiver object’s class when dispatching an overridden instance method. Therefore unit.isDeployable() runs AirUnit.isDeployable().
This division protects type safety. The compiler guarantees that every possible Unit supports the call. Run-time dispatch preserves specialised behaviour.
Upcasting is safe; downcasting needs proof
Assigning a child reference to a parent variable is upcasting:
AirUnit air = new AirUnit("Kite", 80, true);
Unit unit = air;No explicit cast is required. Every AirUnit satisfies the Unit contract.
The opposite direction is a downcast:
Unit unit = new GroundUnit("Atlas", 70);
AirUnit air = (AirUnit) unit; // ClassCastException at run timeThe declared type alone cannot prove that the object is an AirUnit.
Use instanceof when a concrete operation is genuinely required:
if (unit instanceof AirUnit air) {
System.out.println(air.isWeatherClear());
}Pattern matching both checks the run-time type and creates the narrowed variable.
If a method contains many such branches, first ask whether the shared abstraction needs another operation.
Overriding has exact rules
An override replaces an inherited instance-method implementation for the same signature.
Use @Override. It asks the compiler to verify your intention and catches misspellings or wrong parameter types.
A valid override follows these rules:
- The name and parameter types match the inherited method.
- The return type is the same or a permitted covariant subtype.
- Access is not made more restrictive.
- A new checked exception is not broader than the inherited declaration allows.
- The inherited method is not
final.
Private methods are not visible to subclasses, so a same-named child method does not override them.
Static methods belong to classes and are hidden, not overridden. Their selection uses the declared reference or class name.
class Parent {
static void kind() { System.out.println("parent static"); }
void speak() { System.out.println("parent instance"); }
}
class Child extends Parent {
static void kind() { System.out.println("child static"); }
@Override
void speak() { System.out.println("child instance"); }
}
Parent value = new Child();
value.kind(); // parent static
value.speak(); // child instancePrefer calling static methods with a class name, such as Parent.kind(). It makes the non-polymorphic choice visible.
Overloading and overriding can occur together
class Parent {
void send(Object value) { System.out.println("parent object"); }
}
class Child extends Parent {
@Override
void send(Object value) { System.out.println("child object"); }
void send(String value) { System.out.println("child string"); }
}
Parent receiver = new Child();
receiver.send("hello");The output is child object.
First, the compiler performs overload selection using declared type Parent. It sees send(Object), not the child-only overload.
Then the JVM dispatches that selected signature using the real receiver. It finds Child.send(Object).
Use this order for any mixed question:
- Compiler chooses the signature.
- JVM chooses the override for that signature.
What abstract means
An abstract class cannot be instantiated directly:
// new Unit("unknown", 0); // Unit is abstractIt may contain constructors, fields, concrete methods, and abstract methods. Its constructor still runs as part of building a concrete subclass.
An abstract method declares a required signature without a body:
abstract boolean isDeployable();A concrete subclass must implement every inherited abstract method. Otherwise, that subclass must also be declared abstract.
An abstract class does not need to contain an abstract method. Declaring it abstract can mean the type is useful as a base but incomplete as a direct object.
Interfaces describe roles across class families
Suppose units and employees can both produce a report. They do not belong in one implementation hierarchy.
interface Reportable {
String report();
}Classes from unrelated hierarchies can implement the same interface:
class AirUnit extends Unit implements Reportable {
// Unit methods omitted
@Override
public String report() {
return "air unit report";
}
}
class Employee implements Reportable {
@Override
public String report() {
return "employee report";
}
}The interface becomes a variable, parameter, or return type:
static void printReport(Reportable value) {
System.out.println(value.report());
}printReport needs the capability, not the concrete class or its inheritance tree.
An ordinary interface method is implicitly public abstract. Implementations must therefore be public.
Interface fields are constants. They are implicitly public static final, so interfaces cannot hold per-object instance state.
Modern interfaces may also declare default methods with bodies, static methods, and private helper methods. Default methods let an interface evolve shared behaviour without becoming a stateful superclass.
Abstract class or interface
| Need | Better starting point |
|---|---|
| Shared per-object fields and constructor rules | abstract class |
| One substantial implementation for closely related classes | abstract class |
| A role shared by unrelated classes | interface |
| Several capabilities on one class | interfaces |
| A stable dependency type for callers | usually interface |
A class extends one class and implements several interfaces:
class AirUnit extends Unit implements Reportable, Comparable<AirUnit> {
}Interfaces can inherit from several interfaces. They avoid multiple inheritance of instance state.
Default methods can still conflict. If two unrelated interfaces provide the same default signature, the implementing class must resolve the conflict explicitly.
The bytecode keeps the decision open
Run javap -c on a small polymorphic example. A normal virtual call uses an instruction such as invokevirtual. An interface call commonly uses invokeinterface.
The class file contains a symbolic method reference. At run time, method resolution and dispatch find the implementation for the receiver’s class.
JVM implementations optimise this heavily. They may use dispatch tables, inline caches, or speculative inlining. Those are implementation techniques, not source-language guarantees.
The source-level rule remains enough for reasoning: the compiler validates the call against the declared type, then run-time dispatch selects the instance override.
What it costs
Polymorphism removes concrete-type branches when each object can own the operation. A vague parent type with unrelated methods creates a different form of coupling.
Abstract classes share implementation but commit descendants to one class hierarchy. Interfaces are more flexible but may require repeated implementation or separate collaborators.
Choose the smallest contract the caller needs. A narrow abstraction is easier to implement, test, and change.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
A variable has declared type Unit and refers to an AirUnit. Which type controls available method calls, and which type selects an override?
Show the answer
The declared type
Unitcontrols what the compiler allows at the call site. If the method is overridable, the run-time classAirUnitselects the implementation when the call executes.What is the central difference between overloading and overriding?
Show the answer
Overloading selects among different parameter lists at compile time. Overriding selects an implementation of the same instance-method signature at run time. Overload selection uses compile-time argument types. Override dispatch uses the receiver object's run-time class.
When should a design use an interface rather than an abstract class?
Show the answer
Use an interface for a capability or role that unrelated class families can implement. Use an abstract class when closely related classes need shared per-object state, construction logic, or a substantial common implementation. A class can implement several interfaces but extend only one class.
Why is repeated downcasting often a design warning?
Show the answer
The caller is recovering concrete types that the abstraction was meant to hide. If every branch asks what the object is, the shared contract may be missing an operation that each implementation should provide polymorphically.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises100 pointsabout 110 minutes
Delete Every instanceof
ex-7-2-aRewrite a method so that adding a new type never touches it again.
The starting version works. It also has to be edited every time somebody adds a unit type, and so does every other method shaped like it in the program. Miss one and it silently ignores the new type.
Move each rule into the class it belongs to and watch the loop collapse to one line.
Then prove it worked. Add a fourth unit type with a completely different rule, and check whether countDeployable appears in your diff. If it does not, you have understood polymorphism. If it does, the rule is still sitting in the loop.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class NoInstanceof {
Unit(String name, double readiness)
Ground(String name, double readiness, int armour)
Air(String name, double readiness, boolean weatherClear)
Naval(String name, double readiness, int displacement)
Space(String name, double readiness, int shielding)
Unit: String getName(), double getReadiness(), boolean isDeployable()
NoInstanceof: static int countDeployable(Unit[] units)
}Thresholds: ground 60, air 60 and clear weather, naval 50, space 80. The tests define a sixth type of their own and hand it to countDeployable. The instanceof version in the starter gets the five known types right and gets that one wrong, which is the difference the exercise is about.
What your program must do
- Move each rule into the class it belongs to
- Rewrite countDeployable so it contains no instanceof and no casts
- Give Space its rule without editing countDeployable at all
- Confirm countDeployable never appears in your diff after the first rewrite
class Unit {
private final String name;
private final double readiness;
Unit(String name, double readiness) {
this.name = name;
this.readiness = readiness;
}
String getName() { return name; }
double getReadiness() { return readiness; }
// TODO: the rule most units follow
boolean isDeployable() { return false; }
}
class Ground extends Unit {
private final int armour;
Ground(String name, double readiness, int armour) { super(name, readiness); this.armour = armour; }
int getArmour() { return armour; }
// TODO: does ground differ from Unit, or is there nothing to write here?
}
class Air extends Unit {
private final boolean weatherClear;
Air(String name, double readiness, boolean weatherClear) { super(name, readiness); this.weatherClear = weatherClear; }
boolean isWeatherClear() { return weatherClear; }
// TODO: air needs the readiness AND the weather
}
class Naval extends Unit {
private final int displacement;
Naval(String name, double readiness, int displacement) { super(name, readiness); this.displacement = displacement; }
int getDisplacement() { return displacement; }
// TODO: naval sails at 50
}
// The fourth type. It arrived after countDeployable was written, which is the point.
class Space extends Unit {
private final int shielding;
Space(String name, double readiness, int shielding) { super(name, readiness); this.shielding = shielding; }
int getShielding() { return shielding; }
// TODO: space needs 80
}
public class NoInstanceof {
// The version to delete. It is correct today and it has to be edited
// every time somebody adds a type. Note that it does not know about Space.
static int countDeployable(Unit[] units) {
int ready = 0;
for (Unit u : units) {
if (u instanceof Ground) {
if (u.getReadiness() >= 60) ready++;
} else if (u instanceof Air) {
Air a = (Air) u;
if (u.getReadiness() >= 60 && a.isWeatherClear()) ready++;
} else if (u instanceof Naval) {
if (u.getReadiness() >= 50) ready++;
}
}
return ready;
}
public static void main(String[] args) {
Unit[] units = {
new Ground("Anvil", 74, 3),
new Air("Kestrel", 88, false),
new Naval("Trident", 55, 9000),
new Space("Orbiter", 91, 12),
};
System.out.println(countDeployable(units) + " of " + units.length + " deployable");
}
}
Hint 1
Unit an isDeployable() with the most common rule, then override it only in the classes that differ. Ground matches the common rule, so ground overrides nothing.Hint 2
for (Unit u : units) if (u.isDeployable()) ready++;. Everything else moved into the classes, which is where it was always describing something about.Hint 3almost the answer
countDeployable shows up in your diff again, a rule is still sitting in the loop.See the Two invokes
ex-7-2-bBack in Section 1.2 you ran javap on a Hello World and were told to keep the output, because it would matter twice. This is the first time.
Compile this and run javap -c on it. Look only at main.
You will see two different instructions. One names a method exactly, including its parameter types. The other names only a method name and a class that is not even the class of the object involved.
That difference is the entire answer to “what is the difference between overloading and overriding”, and you can now point at it in a file rather than reciting it.
Write your explanation in your own words. Two sentences is enough, and it should mention who decided and when.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Invokes {
static String overriddenCallInstruction()
static String classRecordedByThatInstruction()
static String overloadedCallInstruction()
static String intCallDescriptor()
static String doubleCallDescriptor()
static boolean overloadChosenByCompiler()
static boolean overrideChosenByCompiler()
static String predictedForChar()
static String predictedForLong()
static String predictedForInteger()
static String predictedForNull()
}The first seven are what you read in javap. Descriptors go in exactly the form javap prints them, such as (I)V. Whitespace and case are ignored, nothing else is. The four predictions are which pick overload the compiler chooses, answered as int, double or Object. Do not change over or pick, because the questions are about those exact methods.
What your program must do
- Compile the file and run javap -c on the class, reading only main
- Record the instruction and the class name on the overridden call
- Record the instruction and both descriptors on the overloaded calls
- Predict which pick overload is chosen for char, long, Integer and null, then run it and check
class Unit { boolean deployable() { return true; } }
class AirUnit extends Unit { @Override boolean deployable() { return false; } }
public class Invokes {
// The two the bytecode question is about. Leave them alone.
static void over(int x) { }
static void over(double x) { }
// The three the prediction questions are about. Leave these alone too.
static String pick(int x) { return "int"; }
static String pick(double x) { return "double"; }
static String pick(Object x) { return "Object"; }
// ---- what javap showed ----
// Compile this file, then run: javap -c Invokes
// Read only the main method. Fill in what you see.
// TODO: the instruction on the u.deployable() line
static String overriddenCallInstruction() { return "?"; }
// TODO: the class name written beside it. Read it carefully before you answer.
static String classRecordedByThatInstruction() { return "?"; }
// TODO: the instruction on both over(...) lines
static String overloadedCallInstruction() { return "?"; }
// TODO: the descriptor beside over(5), in the form javap prints it
static String intCallDescriptor() { return "?"; }
// TODO: the descriptor beside over(5.0)
static String doubleCallDescriptor() { return "?"; }
// TODO: had the compiler already picked which over() runs?
static boolean overloadChosenByCompiler() { return false; }
// TODO: had it already picked which deployable() runs?
static boolean overrideChosenByCompiler() { return true; }
// ---- predictions, from the same rule ----
// Which pick() does the compiler choose for each of these?
// Answer first, then run main and check.
// TODO
static String predictedForChar() { return "?"; }
// TODO
static String predictedForLong() { return "?"; }
// TODO
static String predictedForInteger() { return "?"; }
// TODO
static String predictedForNull() { return "?"; }
public static void main(String[] args) {
Unit u = new AirUnit();
System.out.println(u.deployable());
over(5);
over(5.0);
char c = 'x';
long millis = 5L;
Integer boxed = 7;
System.out.println("char -> " + pick(c));
System.out.println("long -> " + pick(millis));
System.out.println("Integer -> " + pick(boxed));
System.out.println("null -> " + pick(null));
}
}
Hint 1
javap -c Invokes in the same folder as the class file. Read the lines between public static void main and the closing brace, and nothing else.Hint 2
AirUnit and the bytecode names Unit, the declared type of the variable. The compiler wrote down a name and left the choice for later.Hint 3almost the answer
char widens to int. long cannot narrow to int, so it widens to double. An Integer already is an Object, so nothing has to be converted at all.Abstract Class or Interface
ex-7-2-cFour things to model. Two of them are interfaces, one is an abstract class, and one could arguably be either.
Decide each one and write the reason down before you write any code. The deciding question is almost always the same: does this need to hold data that differs from object to object?
The Crate is the part that proves your design. It is not a vehicle in any sense, and it still needs to be tracked. If your design cannot handle that without duplication, you have used inheritance where you needed an interface.
Finish by writing a method that takes a Trackable and passing it both a Plane and a Crate. Two completely unrelated classes flowing through one method is the thing interfaces are for.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Choose {
Vehicle(String name, double fuelLevel): String getName(), double getFuelLevel(), void refuel(double litres)
Flyable: void takeOff(), void land(), boolean isAirborne()
Trackable: String position()
Repairable: void takeDamage(), void repair(), boolean isDamaged()
Plane(String name, double fuelLevel)
Crate(String id, String position): String getId()
Choose: static String[] positionsOf(Trackable[] items)
Choose: static boolean vehicleCanBeAnInterface(), interfaceFieldsAreShared(), interfaceCanHoldPerObjectState(), crateIsAVehicle()
}Which of the four are interfaces and which is a class is the exercise, so that is what the tests check first. A new Plane starts on the ground and undamaged. refuel rejects a negative amount with IllegalArgumentException. A plane's position must differ between airborne and grounded, and what the two strings say is up to you. One test implements Trackable itself and hands it to positionsOf.
What your program must do
- Decide abstract class or interface for all four, with a written reason for each
- Implement Plane and Crate, with Crate not a Vehicle in any sense
- Write positionsOf to take anything Trackable, and pass it both a Plane and a Crate
- Answer the four reason methods, and be able to say why Vehicle cannot be an interface
// You are modelling a fleet. Decide abstract class or interface for each,
// and implement all four.
//
// Vehicle - every vehicle has a name and a fuel level, and can refuel
// Flyable - can take off and land. Applies to planes AND to some drones.
// Trackable - reports a position. Applies to vehicles AND to cargo crates.
// Repairable - can be damaged and repaired. Applies to almost everything.
//
// Then build: Plane (a Vehicle that is Flyable, Trackable, Repairable)
// Crate (not a Vehicle at all, and still Trackable)
//
// Decide each one and write the reason down before you write any code.
// TODO: class or interface?
abstract class Vehicle {
// TODO: name, fuel level, a constructor, getName, getFuelLevel, refuel
}
// TODO: class or interface?
interface Flyable {
// TODO: takeOff, land, isAirborne
}
// TODO: class or interface?
interface Trackable {
// TODO: position
}
// TODO: class or interface?
interface Repairable {
// TODO: takeDamage, repair, isDamaged
}
// TODO
class Plane {
}
// TODO: what must Crate NOT extend?
class Crate {
}
public class Choose {
// TODO: accept anything trackable and return each position, in order
static String[] positionsOf(Trackable[] items) {
return new String[0];
}
// ---- the reasons, written down ----
// TODO: could Vehicle have been an interface?
static boolean vehicleCanBeAnInterface() { return true; }
// TODO: is a field declared in an interface one value shared by everybody?
static boolean interfaceFieldsAreShared() { return false; }
// TODO: can an interface hold data that differs from object to object?
static boolean interfaceCanHoldPerObjectState() { return true; }
// TODO: in your design, is a Crate a Vehicle?
static boolean crateIsAVehicle() { return true; }
public static void main(String[] args) {
// TODO: build a Plane and a Crate, and pass both through positionsOf
}
}
Hint 1
Hint 2
public static final whether you write those words or not, meaning one shared value that never changes. That rules it out for Vehicle.Hint 3almost the answer
Trackable accepts both, which is exactly the thing a single-parent hierarchy cannot give you.The Override That Was Not
ex-7-2-dThree methods in a subclass, and none of them do what they appear to do.
Predict all three printed lines before running. Two of the results are surprising, and the third is surprising in a different way.
Then add @Override to both instance methods and watch two silent bugs turn into two compile errors with line numbers. That transformation is the whole argument for writing the annotation every time, and it costs nothing.
The static method is the interesting leftover. Fix the other two and it still prints the parent’s version. Work out why, and you will understand something most Java developers get wrong when asked.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class NotAnOverride {
Unit: boolean isDeployable(double minReadiness), String describe(), static String kind()
AirUnit extends Unit: the same three, with the two instance ones genuinely overriding
NotAnOverride: the prediction and explanation methods
static boolean predictedDeployableLine()
static String predictedDescribeLine()
static String predictedKindLine()
static int overrideAnnotationErrors()
static String kindIsChosenBy()
static boolean staticMethodsCanBePolymorphic()
static String hidingOrOverriding()
}The three predictions are about the broken version as given, so answer them before you change anything. Then fix both instance methods so they really override, and leave the static one exactly as it is. AirUnit.describe returns "an air unit". kindIsChosenBy is "variable" or "object", hidingOrOverriding is "hiding" or "overriding", and both ignore case and surrounding spaces.
What your program must do
- Predict all three printed lines before running anything
- Add @Override to both instance methods and record how many errors appear
- Fix both so they genuinely override, removing the versions that never did
- Explain why the static one prints what it does, even after the other two are fixed
class Unit {
boolean isDeployable(double minReadiness) { return true; }
String describe() { return "a unit"; }
static String kind() { return "unit"; }
}
class AirUnit extends Unit {
boolean isDeployable(int minReadiness) { return false; } // note the type
String describ() { return "an air unit"; } // note the spelling
static String kind() { return "air"; }
}
public class NotAnOverride {
// ---- predictions about the BROKEN version above, before you change anything ----
// TODO: what does System.out.println(u.isDeployable(60.0)) print?
static boolean predictedDeployableLine() { return false; }
// TODO: what does System.out.println(u.describe()) print?
static String predictedDescribeLine() { return "?"; }
// TODO: what does System.out.println(Unit.kind()) print, with u declared as a Unit?
static String predictedKindLine() { return "?"; }
// ---- now add @Override to both instance methods and count what the compiler says ----
// TODO: how many errors appeared?
static int overrideAnnotationErrors() { return 0; }
// ---- then fix both, and look at the static one again ----
// TODO: "variable" or "object"? Which one decides that Unit.kind runs?
static String kindIsChosenBy() { return "?"; }
// TODO
static boolean staticMethodsCanBePolymorphic() { return true; }
// TODO: "hiding" or "overriding"? What is AirUnit.kind doing to Unit.kind?
static String hidingOrOverriding() { return "?"; }
public static void main(String[] args) {
Unit u = new AirUnit();
System.out.println(u.isDeployable(60.0));
System.out.println(u.describe());
System.out.println(Unit.kind());
System.out.println(AirUnit.kind());
}
}
Hint 1
Hint 2
@Override turns both silent bugs into compile errors with line numbers. That is the entire reason to write it every time.Hint 3almost the answer
u is declared as Unit, so Unit.kind() runs. This is called hiding, and it does not become polymorphic no matter what you do.After the credits
Trace every polymorphic call in two stages:
compile time: is this call valid, and which signature is selected?
run time: which override implements that signature for this object?That model covers parent variables, interfaces, abstract classes, arrays of mixed subtypes, and later collection APIs.
Threads you opened in this section
- PolymorphismList list = new ArrayList<>(). You will type this thousands of times.Phase X. The Collections Framework
- Abstract classOnce interfaces got default methods, this choice got harder.8.5 - Interfaces Deep Dive
- Abstract classAbstractList does the boring work so ArrayList does not have to.Phase X. The Collections Framework
- InterfaceDefault, static, functional and marker interfaces. The full picture.8.5 - Interfaces Deep Dive
- InterfaceThe entire Collections Framework is interfaces first, classes second.Phase X. The Collections Framework
- InterfaceEvery lambda you write is secretly an interface being implemented.11.1 - Lambdas and Functional Interfaces
Polymorphism will return in 8.3 - The Object Class: `equals`, `hashCode`, `toString`