Summary: Abstraction, Interfaces, Generics & Nested Classes¶
Combined Knowledge from: Tim Buchalka's Course (Sections 11–13) + Effective Java
Mastery Level:
Topic Overview¶
This topic is the heart of professional Java design. It covers the full abstraction toolbox — from abstract classes and interfaces to generic types, bounded wildcards, and nested classes. Together, these features let you write code that is simultaneously type-safe, reusable, flexible, and encapsulated.
mindmap
root((Topic 4))
**Abstraction**
Abstract Classes
Generalization
Method Modifiers
**Interfaces**
Contracts
Default Methods
Multiple Inheritance of Type
Coding to Interface
**Generics**
Generic Classes
Generic Methods
Bounded Types
Wildcards & PECS
Type Erasure
**Nested Classes**
Static Nested
Inner Classes
Local Classes
Anonymous Classes
Core Concepts¶
1. Abstract Classes¶
Definition: A class declared abstract that cannot be instantiated. It acts as a strict base class that forces subclasses to provide implementations for all abstract methods.
classDiagram
class Animal {
<<abstract>>
#String type
-String size
-double weight
+Animal(type, size, weight)
+move(speed)* void
+makeNoise()* void
+getExplicitType() String$
}
class Mammal {
<<abstract>>
+move(speed) void
+shedHair()* void
}
class Dog {
+makeNoise() void
+shedHair() void
}
class Fish {
+move(speed) void
+makeNoise() void
}
class Horse {
+makeNoise() void
+shedHair() void
}
Animal <|-- Mammal
Animal <|-- Fish
Mammal <|-- Dog
Mammal <|-- Horse
note for Mammal "Implements move()\nAdds shedHair()\nLeaves makeNoise() abstract"
note for Animal "Cannot instantiate.\nBut CAN use as type in\nvariables, params, collections."
Key Rules Quick Reference¶
| Rule | Detail |
|---|---|
| Cannot instantiate | new Animal() → compile error |
| Can have constructors | Subclasses MUST call super(...) |
| Mix abstract + concrete | Some methods forced, some inherited, some locked (final) |
abstract + private |
❌ Illegal — contradictory modifiers |
abstract + final |
❌ Illegal — contradictory modifiers |
| Abstract class extends abstract | OK — can implement some, none, or all parent abstract methods, and add new ones |
When to Use Abstract Classes¶
flowchart TD
Q1["Do subclasses share common behavior?"] -->|Yes| Q2["Is some behavior the same across all?"]
Q2 -->|Yes| Q3["Should creation be prevented for the base?"]
Q3 -->|Yes| AC["Use Abstract Class\n✓ Shared code via concrete methods\n✓ Forced contracts via abstract methods\n✓ No direct instantiation"]
Q3 -->|No| CC["Use Concrete Parent"]
Q2 -->|No| IF["Consider Interface Only"]
Q1 -->|No| NONE["No inheritance needed"]
style AC fill:#1b5e20,color:#fff
2. Interfaces¶
Definition: A contract type defining behavioral obligations. Any class implementing an interface promises to provide all its abstract methods. Unlike abstract classes, a class can implement multiple interfaces.
Interface Member Types¶
flowchart LR
subgraph members["Interface Members"]
M1["Abstract methods\n(implicit: public abstract)\nMust be implemented"]
M2["Default methods (JDK 8)\n(explicit: default keyword)\nInherited, can be overridden"]
M3["Static methods (JDK 8)\n(called via InterfaceName.method())\nNot inherited"]
M4["Private methods (JDK 9)\n(code reuse inside interface)\nNot accessible outside"]
M5["Constants\n(implicit: public static final)\nAlways accessible"]
end
Abstract Class vs Interface — The Decision Matrix¶
flowchart TD
Q1["Do the types share an IS-A family relationship?"] -->|Yes, same family| Q2["Do they share common implementation?"]
Q2 -->|Yes| AC["Abstract Class"]
Q1 -->|No, unrelated types sharing behavior| IF["Interface"]
Q2 -->|No| BOTH["Consider Interface with\nSkeletal Implementation\n(Abstract + Interface)"]
style AC fill:#1565c0,color:#fff
style IF fill:#6a1b9a,color:#fff
style BOTH fill:#4527a0,color:#fff
| Feature | Abstract Class | Interface |
|---|---|---|
| Multiple inheritance | ❌ Single extends | ✅ Multiple implements |
| Constructor | ✅ Yes | ❌ No |
| Instance state (fields) | ✅ Yes | ❌ No (constants only) |
| Common implementation | ✅ Concrete methods | ✅ Default methods (JDK 8+) |
| Records/Enums can use | ❌ Cannot extend | ✅ Can implement |
| Skeletal implementation | — | ✅ AbstractXxx pattern |
Coding to an Interface¶
// ❌ Tight coupling — breaks when implementation changes
private static void triggerFliers(ArrayList<FlightEnabled> fliers) { ... }
// ✅ Coded to interface — any List implementation works
private static void triggerFliers(List<FlightEnabled> fliers) { ... }
Apply this to parameters, return types, local variables, and fields. The implementation becomes a one-line swap.
3. Generics¶
Definition: A language mechanism that lets you write classes and methods parameterized by type, providing compile-time type safety without sacrificing reusability.
The Three-Stage Evolution¶
flowchart LR
subgraph s1["Stage 1: Specific"]
B1["BaseballTeam\nList<BaseballPlayer>\n❌ Only baseball"]
end
subgraph s2["Stage 2: Polymorphic"]
B2["SportsTeam\nList<Player>\n⚠️ Any sport, NO type safety"]
end
subgraph s3["Stage 3: Generic"]
B3["Team<T extends Player>\nList<T>\n✅ Any sport WITH compile-time safety"]
end
s1 -->|"add interface"| s2
s2 -->|"add type param"| s3
style s1 fill:#8b0000,color:#fff
style s2 fill:#8b6914,color:#fff
style s3 fill:#006400,color:#fff
Type Parameter Naming Conventions¶
| Letter | Meaning | Usage |
|---|---|---|
T |
Type | class Box<T> — general-purpose |
E |
Element | interface List<E> — JCF collections |
K / V |
Key / Value | interface Map<K, V> |
N |
Number | Numeric types |
S, U |
2nd, 3rd params | class Team<T, S> |
Upper Bounds¶
// Without bound: T can only use Object methods
class Team<T> { ... }
// With upper bound: T can use Player's methods too
class Team<T extends Player> { ... }
// OR multiple bounds (class must come first!):
class QueryList<T extends Student & QueryItem> { ... }
Comparable and Comparator¶
flowchart TD
subgraph natural["Natural Ordering — Comparable<T>"]
C1["class Student implements Comparable<Student>"]
M1["compareTo(Student o): compare THIS vs o"]
C1 --> M1 --> U1["Arrays.sort(array) — no extra arg"]
end
flowchart TD
subgraph custom["Custom Ordering — Comparator<T>"]
C2["class GpaComparator implements Comparator<Student>"]
M2["compare(Student o1, Student o2): compare two externals"]
C2 --> M2 --> U2["Arrays.sort(array, comparator)\n.reversed() for descending"]
end
| Aspect | Comparable<T> |
Comparator<T> |
|---|---|---|
| Package | java.lang |
java.util |
| Method | compareTo(T o) — 1 arg |
compare(T o1, T o2) — 2 args |
| Number per class | One (natural order) | Unlimited |
| Who implements | The class itself | A separate class |
4. Wildcards & PECS¶
The Core Problem: Generics are invariant — List<LPAStudent> is NOT a List<Student>, even though LPAStudent extends Student. Wildcards add controlled flexibility.
flowchart LR
subgraph types["Type Hierarchy (IS-A applies)"]
S["Student"] --> L["LPAStudent"]
end
subgraph containers["Container Types (IS-A does NOT apply)"]
LS["List<Student>"]
LL["List<LPAStudent>"]
LS -.-x LL
end
The PECS Principle¶
**P**roducer **E**xtends, **C**onsumer **S**uper
flowchart TD
subgraph pecs["PECS Decision"]
Q["What does the parameter do?"]
Q --> PE["Produces values\n(you READ from it)"]
Q --> CS["Consumes values\n(you WRITE to it)"]
Q --> BOTH2["Both reads and writes"]
PE --> EXT["Use ? extends T"]
CS --> SUP["Use ? super T"]
BOTH2 --> TP["Use type parameter T\n(no wildcard)"]
end
style EXT fill:#1565c0,color:#fff
style SUP fill:#4527a0,color:#fff
style TP fill:#2e7d32,color:#fff
| Wildcard | Reads as | Can Write | Use Case |
|---|---|---|---|
<?> |
Object |
❌ | Any type, logic uses instanceof |
<? extends T> |
T |
❌ | Producer — reading elements (pushAll) |
<? super T> |
Object |
✅ T + subtypes | Consumer — adding elements (popAll) |
5. Nested Classes¶
Definition: A class declared inside another class or interface. Used when two classes are tightly coupled and the inner type has no meaningful existence independent of the outer.
flowchart TD
NC["Nested Class"] --> Q1["Does it need access to\nouter INSTANCE members?"]
Q1 -->|No| SN["Static Nested Class\n(static class Inner)\nInstantiate: new Outer.Inner()"]
Q1 -->|Yes| Q2["Where is it used?"]
Q2 -->|"Across the whole class"| IC["Inner Class\n(class Inner)\nInstantiate: outerInst.new Inner()"]
Q2 -->|"Only inside a method"| Q3["Used once or multiple times?"]
Q3 -->|"Multiple times"| LC["Local Class\n(class inside method block)"]
Q3 -->|"Only once"| AC["Anonymous Class\n(new Interface(){...})\nPrefer lambda for functional interfaces"]
style SN fill:#1565c0,color:#fff
style IC fill:#6a1b9a,color:#fff
style LC fill:#4527a0,color:#fff
style AC fill:#006064,color:#fff
| Type | static? |
Outer Ref? | Scope | Instantiation |
|---|---|---|---|---|
| Static Nested | ✅ | ❌ | Class-level | new Outer.Nested() |
| Inner | ❌ | ✅ | Class-level | outerInst.new Inner() |
| Local | ❌ | ✅ | Method block | Inside method only |
| Anonymous | ❌ | ✅ | Expression | new Interface() { ... } |
Key Internals to Understand¶
1. Arrays (Covariant) vs Generics (Invariant)¶
This is one of Java's most important design trade-offs. Arrays and generic types have opposing variance rules:
flowchart LR
subgraph arr["Arrays — COVARIANT"]
A1["String[] IS-A Object[]\n(allowed by compiler)"]
A2["objectArray[0] = new Integer(1)"]
A3["💥 ArrayStoreException\nat RUNTIME"]
A1 --> A2 --> A3
end
subgraph gen["Generics — INVARIANT"]
G1["List<String> is NOT a List<Object>\n(rejected by compiler)"]
G2["✅ Compile error immediately\nBug caught before shipping"]
end
style A3 fill:#c62828,color:#fff
style G2 fill:#2e7d32,color:#fff
// ❌ ARRAY COVARIANCE — compiles but fails at runtime:
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit here!"; // ArrayStoreException at runtime!
// ✅ GENERIC INVARIANCE — fails at compile time (safer):
List<Object> objects = new ArrayList<Long>(); // COMPILE ERROR
Why arrays are covariant: Legacy design from Java 1.0, before generics existed, to allow methods like Arrays.sort(Object[]) to sort any array.
Why generics are invariant: To prevent the type-safety hole shown above. Wildcards (? extends T) provide controlled covariance when needed.
2. Type Erasure Mechanism¶
The Java compiler erases all generic type information before producing bytecode. This makes generics backward-compatible with pre-Java 5 code, but has important implications.
flowchart LR
subgraph source["Source Code (.java)"]
S1["List<String> names"]
S2["Team<T extends Player>"]
S3["<T extends Comparable<T>> T max(List<T>)"]
end
subgraph bytecode["Bytecode (.class) — After Erasure"]
B1["List names (Object elements)"]
B2["Team (T → Player)"]
B3["Comparable max(List list)"]
end
S1 -->|"erase → Object"| B1
S2 -->|"erase → first bound"| B2
S3 -->|"erase → Comparable"| B3
Erasure Rules¶
| Source | Erased To |
|---|---|
T (no bound) |
Object |
T extends Foo |
Foo |
T extends Foo & Bar |
Foo (first bound) |
List<String> |
List |
List<Integer> |
List |
The Three Consequences of Type Erasure¶
// 1. Cannot create generic arrays
new T[10] // ❌ "generic array creation" — type not known at runtime
// 2. Cannot overload on type arguments — same erasure
void process(List<String> list) { } // ❌ Both erase to process(List)
void process(List<Integer> list) { } // ❌ "have the same erasure"
// 3. Cannot use instanceof with parameterized types
if (obj instanceof List<String>) { } // ❌ Type erased at runtime
if (obj instanceof List<?>) { } // ✅ Allowed — wildcard is safe
Why Erasure Exists
Type erasure preserves binary compatibility: compiled ArrayList.class from Java 5+ works with all Java code regardless of the type parameter. The JVM sees only the raw type, and the compiler inserts casts wherever needed (these are guaranteed safe because it verified the types before erasing them).
The Bridge Method¶
When a class implements a parameterized interface, the compiler inserts a synthetic bridge method to handle type erasure correctly:
// Source:
class StudentList implements Comparable<Student> {
public int compareTo(Student o) { ... } // Parameterized method
}
// After erasure (what bytecode contains):
class StudentList implements Comparable {
public int compareTo(Student o) { ... } // Original
public int compareTo(Object o) { // Synthetic bridge method!
return compareTo((Student) o); // Delegates + casts
}
}
3. Raw Types vs Parameterized Types¶
Raw types are the dangerous "no type parameter" form of a generic class. They exist only for backward compatibility.
flowchart LR
subgraph raw["Raw Type — stamps (no param)"]
R1["Can add anything to it\nNo compile-time check"]
R2["Cast required on retrieval"]
R3["ClassCastException at runtime"]
end
subgraph param["Parameterized Type — stamps<Stamp>"]
P1["Only Stamp can be added\nCompile-time enforced"]
P2["No cast on retrieval — Stamp returned"]
P3["Type error caught at compile time"]
end
style R3 fill:#c62828,color:#fff
style P3 fill:#2e7d32,color:#fff
The key distinction:
| Form | Example | Safety |
|---|---|---|
| Raw type | List stamps |
❌ No type checking |
| Unbounded wildcard | List<?> stamps |
✅ Read-only, safe |
| Parameterized | List<Stamp> stamps |
✅ Full type safety |
Use
List<?>when you genuinely don't care about the element type (e.g., counting elements in common). Use a parameterized type for everything else. Never use raw types in new code.
4. PECS Principle (Producer Extends, Consumer Super)¶
PECS is the mental model for choosing the correct wildcard when writing flexible generic APIs.
flowchart TD
subgraph insight["Core Insight"]
I1["? extends T ← T-values flow OUT\n(we read them as T)"]
I2["? super T ← T-values flow IN\n(we add them as T)"]
end
subgraph examples["Real Examples"]
E1["pushAll(Iterable<? extends E> src)\nsrc PRODUCES E values → extends"]
E2["popAll(Collection<? super E> dst)\ndst CONSUMES E values → super"]
E3["Comparable<? super T> — compareTo CONSUMES\nT values to compare → super"]
end
insight --> examples
// Stack<E> with PECS-correct methods:
// src is a PRODUCER of E values — use ? extends E
public void pushAll(Iterable<? extends E> src) {
for (E e : src) push(e);
}
// dst is a CONSUMER of E values — use ? super E
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) dst.add(pop());
}
The payoff: Stack<Number> can now push from Iterable<Integer> and pop into Collection<Object> — the API is maximally flexible without sacrificing type safety.
Corollary:
Comparator<T>andComparable<T>are consumers (they consume T values to produce a result). For maximum flexibility, declare them with? super T:<T extends Comparable<? super T>>.
5. Abstract Class vs Interface — The Deep Decision¶
flowchart TD
Q1["What relationship do the types have?"]
Q1 -->|"IS-A family\n(Dog IS-A Animal)"| Q2["Do they share implementation?"]
Q1 -->|"BEHAVES-AS role\n(Bird BEHAVES-AS FlightEnabled)"| IF["Interface"]
Q1 -->|"Both family AND role"| BOTH["extends AbstractClass\n+ implements Interface"]
Q2 -->|"Yes — common code"| AC["Abstract Class"]
Q2 -->|"No — only contracts"| PURE["Pure Interface\n(all abstract methods)"]
style AC fill:#1565c0,color:#fff
style IF fill:#6a1b9a,color:#fff
style BOTH fill:#4527a0,color:#fff
style PURE fill:#006064,color:#fff
The critical rule from Effective Java (Item 23): Tagged classes (with discriminator fields + switch statements) are always inferior to class hierarchies. If you have a switch on a type tag, that's a sign you need an abstract class.
// ❌ TAGGED CLASS — the anti-pattern
class Shape {
enum Type { CIRCLE, RECTANGLE }
Type shapeType;
double radius; double length; double width;
double area() {
return switch (shapeType) { case CIRCLE -> ...; case RECTANGLE -> ...; }
}
}
// ✅ CLASS HIERARCHY — the abstraction pattern
abstract class Shape { abstract double area(); }
class Circle extends Shape { double area() { return PI * r * r; } }
class Rectangle extends Shape { double area() { return l * w; } }
6. Nested Class Scope and Access Rules¶
The Hidden Outer Reference¶
flowchart TD
subgraph nonstatic["Nonstatic Inner Class — DANGER"]
OBJ["Outer Object (can't be GC'd!)"]
INNER["Inner Instance"]
INNER -- "hidden enclosing\nreference" --> OBJ
CLIENT["Long-lived client"] --> INNER
GC["GC"] -. "OBJ is blocked from collection" .-> OBJ
end
subgraph static2["Static Nested Class — SAFE"]
SOBJ["Outer Object (can be GC'd)"]
SNESTED["Static Nested Instance\n(no back-link)"]
SCLIENT["Long-lived client"] --> SNESTED
SGC["GC"] --> SOBJ
end
style GC fill:#c62828,color:#fff
style SGC fill:#2e7d32,color:#fff
Effectively Final in Local Classes¶
Local and anonymous classes can only capture effectively final local variables:
void method(String prefix) { // prefix is effectively final
class Printer {
void print(String s) {
System.out.println(prefix + s); // ✅ captured from enclosing method
}
}
prefix = "new"; // ❌ If you add this line, Printer won't compile!
// "Local variable 'prefix' defined in enclosing scope must be final or effectively final"
}
Why? The local class instance may outlive the method stack frame. To avoid a dangling reference, Java copies the local variable's value at the time the class is instantiated — but only if the value never changes (effectively final).
The .new Syntax for Inner Classes¶
// Static nested: no outer instance needed
Employee.EmployeeComparator<Employee> comp = new Employee.EmployeeComparator<>("name");
// Inner class: outer instance REQUIRED
StoreEmployee outer = new StoreEmployee();
StoreEmployee.StoreComparator<StoreEmployee> comp = outer.new StoreComparator<>();
// Or chained in one expression:
var comp = new StoreEmployee().new StoreComparator<>();
Resolving Shadowed Fields: OuterClass.this¶
public class Meal {
private double price = 5.0; // Outer field
private class Item {
private double price; // Inner field shadows outer!
public Item() {
this.price = Meal.this.price; // Outer.this.field
// ^^^^^^^^^^^^ explicit outer reference
}
}
}
Design Patterns & Best Practices¶
The Java Abstraction Toolkit¶
flowchart LR
subgraph problem["The Design Problem"]
P1["Need reusable, type-safe,\nextensible code"]
end
subgraph tools["The Toolkit"]
T1["Abstract Class\n→ Shared implementation\n→ Forced contracts"]
T2["Interface\n→ Multiple roles\n→ Unrelated types\n→ Records + Enums"]
T3["Generics\n→ Type-safe containers\n→ Reusable algorithms"]
T4["Wildcards\n→ API flexibility\n→ PECS-guided"]
T5["Nested Classes\n→ Tight encapsulation\n→ Helper types"]
end
problem --> tools
Effective Java Best Practices Applied¶
| Practice | Item | Why It Matters |
|---|---|---|
| Interfaces over abstract classes (when possible) | Item 20 | Records and enums can't extend classes, but can implement interfaces |
| Default methods: design carefully | Item 21 | Adding defaults to existing interfaces can silently break invariants |
| Interfaces only to define types | Item 22 | Constant interfaces pollute namespace and leak impl details |
| Class hierarchies over tagged classes | Item 23 | Tagged classes need O(variants) switch updates; hierarchy is O(1) |
| Static member classes over nonstatic | Item 24 | Inner classes hold hidden outer ref → memory leak risk |
| Don't use raw types | Item 26 | Raw types defer type errors to runtime |
| Eliminate unchecked warnings | Item 27 | Each warning = potential ClassCastException |
| Favor generic types | Item 29 | Generic containers remove client-side casts |
| Favor generic methods | Item 30 | Recursive type bound <T extends Comparable<T>> |
| Use bounded wildcards (PECS) | Item 31 | Maximum API flexibility without unsafe operations |
Common Pitfalls¶
1. Trying to Instantiate an Abstract Class¶
Animal a = new Animal("Dog", "big", 100); // ❌ "Cannot instantiate abstract class"
// ✅ Fix: Animal a = new Dog("Pug", "small", 20);
2. abstract + private and abstract + final¶
private abstract void move(String speed); // ❌ Contradictory: must override, but can't see
abstract final void move(String speed); // ❌ Contradictory: must override, but can't override
3. Generic Array Creation¶
// ❌ Cannot create generic arrays directly
E[] elements = new E[10]; // "generic array creation"
// ✅ Cast with @SuppressWarnings + comment
@SuppressWarnings("unchecked")
E[] elements = (E[]) new Object[10]; // Safe: array never exposed externally
4. Raw Comparable — The ClassCastException Trap¶
class Student implements Comparable { // ❌ Raw Comparable
public int compareTo(Object o) {
Student other = (Student) o; // ClassCastException at runtime if misused!
}
}
class Student implements Comparable<Student> { // ✅ Parameterized
public int compareTo(Student o) { ... } // Type checked at compile time
}
5. Overloading on Type Arguments (Same Erasure)¶
// ❌ Both erase to process(List) — compiler rejects
void process(List<String> list) { ... }
void process(List<Integer> list) { ... }
// ✅ One method with wildcard + instanceof
void process(List<?> list) {
for (var e : list) {
if (e instanceof String s) { ... }
if (e instanceof Integer i) { ... }
}
}
6. Calling Inner Class Methods Without an Outer Instance¶
// ❌ Nonstatic inner class requires outer instance
var comp = new StoreEmployee.StoreComparator<>(); // Error!
// ✅ Must use outer instance to call .new
var comp = new StoreEmployee().new StoreComparator<>();
7. Adding Elements to ? extends T¶
// ❌ Cannot add to ? extends — compiler can't guarantee type safety
void readFrom(List<? extends Animal> list) {
list.add(new Dog()); // COMPILE ERROR — might actually be List<Fish>
}
// ✅ Use ? extends only for reading
void readFrom(List<? extends Animal> list) {
for (Animal a : list) { ... } // Reading is always safe
}
8. Calling Interface Default Methods with Wrong Super¶
// ❌ Plain super refers to the parent CLASS (Object), not the interface
return super.transition(stage); // Compile error!
// ✅ Must qualify with the interface name
return FlightEnabled.super.transition(stage); // Correct
Best Practices Checklist¶
Abstract Classes:
- Make base classes
abstractwhen direct instantiation makes no sense - Use
protectedfor fields subclasses need to read directly - Use
finalon methods that must NOT be overridden by subclasses - Always provide a
super(...)call chain in every subclass constructor - Prefer abstract classes over tagged classes with discriminator + switch
Interfaces:
- Use interface types in parameters, return types, local vars, and fields (coding to interface)
- Add
defaultmethods only when backwards compatibility requires it; document carefully - Never use interfaces as constant repositories — use utility classes or enums
- Use
InterfaceName.super.method()to call default methods from overriding implementations
Generics:
- Never use raw types in new code — always specify type parameters
- Eliminate all unchecked warnings; if suppressing, use narrowest scope + comment
- Use upper bounds (
T extends X) when a type parameter must call X's methods - Implement
Comparable<T>(not rawComparable) to prevent ClassCastException - Static methods in generic classes need their own type parameter declaration
Wildcards:
- Producer (reads) →
? extends T; Consumer (writes) →? super T - Never use wildcards in return types — they leak into client code
- Use
Comparable<? super T>for the most flexible natural-ordering constraints
Nested Classes:
- Default to
staticfor nested classes — only removestaticif outer-instance access is genuinely needed - Prefer
staticnested class for Comparators, Builders, Entries, and helper types - In inner classes, use
Outer.this.fieldto reference shadowed outer fields - Prefer lambdas over anonymous classes for functional interfaces (Topics 5+)
Learning Resources¶
Abstract Classes & Interfaces¶
Generics¶
- Oracle — Generics Tutorial
- Baeldung — Java Generics Deep Dive
- Angelika Langer — Java Generics FAQ ⭐ The definitive reference
Type Erasure¶
Wildcards & PECS¶
- Oracle — Wildcards
- Baeldung — Wildcards in Java Generics
- Stack Overflow — What is PECS? ⭐ Classic answer
Nested Classes¶
Effective Java¶
Related Topics¶
- OOP & Class Design Internals
- Arrays, Lists & Autoboxing
- Lambdas & Streams (anonymous classes → lambdas)
References¶
- Course: Tim Buchalka — Java Programming Masterclass (Sections 11, 12 & 13)
- Book: Effective Java — Joshua Bloch (Items 20–24, 26–27, 29–31)
- API: java.lang.Comparable
- API: java.util.Comparator
- API: java.util.List
- Spec: JLS §8.1.4 — Superclasses and Subclasses
- Spec: JLS §4.10 — Subtyping
Completed: 2026-02-25 | Confidence: 9/10