Review and Revision of Modules 8 to 14
Prerequisite Knowledge
This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.
Previously Covered in This Subject
- Inheritance — covered in Lecture 1: Object-Oriented Analysis and Design
- The Life Cycle: Inception to Retirement — covered in Lecture 3: Object-Oriented Analysis and Design: Objects, Models, and the Software Process
- Worked Example: Exception Handling in a Cash Payment System — covered in Lecture 5: Requirements Engineering and Use Case Modeling
- Functional Versus Object Decomposition — covered in Lecture 8: Object-Oriented Analysis and the Domain Model
- Interfaces Versus Abstract Classes — Hierarchy Freedom — covered in Lecture 11: GRASP Patterns and Design Principles — Polymorphism, Indirection, Fabrication and Protected Variations
- Class Diagram Notation — Classes, Attributes, Methods, and Interfaces — covered in Lecture 12: Object Oriented Design Principles and UML Modeling
- Prototype — Cloning Objects from a Model — covered in Lecture 13: Design Patterns — Gang of Four Solutions
- MOOD Suite — Method and Attribute Inheritance Factors and Coupling Factor — covered in Lecture 15: Measuring Object-Oriented Design
- Method Overloading as a Form of Polymorphism — covered in Lecture 16: Introduction to Java and Object-Oriented Programming Foundations
- Static Methods, Overloading and Inheritance Interaction — covered in Lecture 17: Constructors, Static Members, this, final and Software Development Life Cycle
- PrintWriter Class — Writing to Text Files with Exception Handling — covered in Lecture 19: Packages, Input-Output Streams and File Handling in Java
- The Driver — How Dispatch Chooses the Right Method — covered in Lecture 20: Inheritance, Type Conversion and Abstract Classes in Java
# Review and Revision of Modules 8 to 14
This lecture is a consolidation revision that revisits the core building blocks introduced across Modules 8–14 — inheritance and polymorphism, abstract types and interfaces, exception handling, generics and collections, concurrency, object copying, enumerations, and type bounds. Rather than introducing one new algorithm, it threads a single design habit through every topic: declare a clear contract at the top, implement it concretely below, and protect shared resources when many participants act at once. Use this preamble as a roadmap: skim the 16 concept blocks in order, note how each remedial diagram re-uses the previous one, and keep the Exam Guidance Summary as a checklist while reading.
26.1 Inheritance and Method Categories
26.1.1 What Inheritance Means and Why It Matters
Hook: Why write the same fields and methods twice when every checking account is already a bank account? Inheritance answers that question — it lets a new class start with everything an existing class already does, then add only what is new.
An inheritance — a mechanism where a child class receives properties of a parent class — is one of the core ideas in object oriented programming. A parent class — also called a base class or super class — defines data members and member functions. A child class — also called a derived class or sub class — can have one or more siblings, each inherits from the same parent, yet each adds its own detail.
Think of a family recipe book. The parent book contains basic dough, sauce, and baking steps shared by all pizzas. Each child book copies all those base recipes automatically, then adds its own toppings page — pepperoni for one child, paneer for another. You never re-copy the dough recipe. That is exactly how class inheritance reuses code.
Intuition and analogy — family tree: A parent class is the trunk, child classes are branches. Every branch inherits the trunk's structure (fields) and abilities (methods), extends outward with its own leaves, but remains connected to the same trunk. Where the analogy breaks: unlike a biological family, a Java child has exactly one class parent — Java does not allow a class to extend two classes at once. Multiple parents come through interfaces, not through class inheritance.
A child can reach the data members of the parent when they are marked public or protected. It can also reach the member functions of the parent. The same idea appears everywhere: interfaces build on inheritance, generic collections rely on class relationships, the Java object model is a hierarchy whose root is Object, and even multi-threading uses inheritance when a class extends Thread.
Formalize — how inheritance is declared and what is inherited: An inheritance hierarchy is built with the keyword extends, as in class Child extends Parent. The child inherits all non-private members of the parent: public members are reachable everywhere, protected members are reachable inside the child and inside the same package, and package-private members are reachable only inside the same package. A private member remains private to the declaring class — the child inherits the storage but cannot access it directly; it must use a public or protected getter or setter. A subclass can itself become a superclass for another subclass, forming a multi-level chain such as Child extends Parent extends Grandparent. In every chain the subclass constructor must eventually invoke a superclass constructor, and the only specified superclass for any subclass is one. The Object class sits at the top of every hierarchy, so every class ultimately inherits methods like toString() and equals() even if no explicit extends is written.
From the view of the derived class, the base exposes three categories of methods:
- Constructors — special methods that build an object. A derived constructor can call a parent constructor by using the keyword
super. If the parent has several constructors, the derived constructor selects one by matching the number and type of arguments. In code,super(arguments)appears as the first line of the derived constructor. The call with the right argument list chooses which overloaded parent constructor runs; without an explicitsuper(...)the compiler insertssuper()to call the no-argument parent constructor.
- Overridden functions — methods whose name, return type, and argument list already exist in the parent and are re-declared in the child. From a function of the derived class you can reach the parent version by prefixing with
super.
- Normal functions — any other methods defined in the base that are not constructors and not overridden. You call them directly by name from the derived class, no prefix needed.
In a diagram, draw a vertical chain with Grandparent at the top, Parent in the middle, and Child at the bottom, each boxed. Arrows point upward labeled extends. Inside each box list the declared methods. Mark constructors with a special symbol, overridden methods with a shared color across levels, and normal methods without marking. A second small diagram shows BankAccount at the top with balance and accountNumber, and CheckingAccount below with an extra overdraftLimit — illustrating how only the new field must be declared.
Scope and assumptions: Inheritance models an is-a relationship — a CheckingAccount is-a BankAccount. Use it when the child truly specializes the parent. Do not use inheritance to share code between unrelated concepts; prefer composition (a field) when the relationship is has-a. Fields marked private in the parent are not directly visible; direct access requires protected or accessor methods. The super(...) constructor call must be the first statement in the child constructor — no assignments before it. Finally, inheritance reuses implementation but also tightly couples the child to the parent's design; changing the parent can break children.
Pitfalls: Do not declare parent fields as private then try this.balance = 10 inside the child — it will not compile. Do not forget that super(...) and this(...) cannot both appear as the first line; choose one. Do not confuse overriding with overloading — changing the parameter list creates a new overloaded method rather than an override, even if the name matches.
Real-world context: extending a framework class to reuse common behaviour is routine. In banking software a base BankAccount holds balance, accountNumber, deposit() and withdraw(). A CheckingAccount extends it, reuses those fields and methods, and adds overdraftLimit and processCheck(). In application frameworks a base Thread or Exception class is extended the same way — the child reuses threading or error-handling machinery while adding domain logic. In the Java collections framework the same inheritance principle lets ArrayList and LinkedList share the Collection contract while storing data differently.
Recap and bridge: Inheritance lets a child automatically receive the parent's accessible fields and methods, adding only what is new. Three method views matter: constructors reached via super(...) as the first line, overridden methods reached via super.method() when needed, and normal methods called directly. Next we zoom into the most tested of the three — how an overridden method is chosen at call time.
26.1.2 Method Overriding in Detail
Hook: If both parent and child define getData() with the same shape, which one does x.getData() run when x is a child object? The answer is always the child's — unless you explicitly ask for the parent's.
An overridden method — a method where child and parent share the same name and the same type signature — is central to inheritance. The type signature in this setting includes the method name, the return type, the number of arguments, and the type of each argument. When the signature matches, we say the child overrides the parent method. For overriding the name, the return type, the count of parameters and each parameter type must all line up; a single difference turns the pair into overloaded methods, not overridden ones.
Formalize — exact rule and how super reaches the parent version: A method M in XYZ overrides M in ABC exactly when class XYZ extends ABC and both declare M with identical name, identical return type, and identical ordered parameter types. When that holds, any call x.M(...) where x has runtime type XYZ selects XYZ.M(...); the parent's M is hidden but not lost — inside XYZ.M(...) you can write super.M(...) to invoke the parent's implementation, for example to reuse its work before adding extra steps. Overloading by contrast changes the parameter list (different count or types) while keeping the name, which creates a separate method rather than hiding the parent's. The compiler enforces the override contract; adding @Override lets it catch a typo where you intended to override but wrote a slightly different signature.
Consider a parent class ABC with a method getData(). A child class XYZ extends ABC and also declares getData() with the same return type and the same argument count and types. That is an overridden method. If you create an object x of type XYZ and write x.getData(), the call resolves to getData() as defined in the derived class XYZ, not the base version. Inside the derived version, if you need the base behaviour, you write super.getData().
Worked mini-example — overriding getData(): Define class ABC { void getData(){ System.out.println("ABC data"); } } and class XYZ extends ABC { void getData(){ System.out.println("XYZ data"); super.getData(); } }. Create XYZ x = new XYZ(); then call x.getData();. Step 1: the JVM looks at the actual object x, sees its class is XYZ, finds XYZ.getData() overrides ABC.getData(), so it runs the child's version — prints XYZ data. Step 2: inside that child method super.getData() explicitly calls ABC.getData() — prints ABC data. Final output is two lines: XYZ data then ABC data. If XYZ had declared getData(int a) instead, the signatures differ and it would be overloading; x.getData() with no arguments would still route to ABC.getData().
Scope and assumptions: The same-signature rule used here applies to instance methods that are public or protected and not final, static, or private. A final method cannot be overridden, a static method is hidden not overridden, and a private method in the parent is not visible to be overridden at all. Return-type covariance is allowed in newer Java (a child may return a subtype), but for this revision treat identical return types as the required pattern. Also, super.getData() is reachable only from inside the child instance method, not from main via x.super.getData() — outside code chooses which class to instantiate instead.
A second visual: draw ABC above XYZ with a box for each. List getData(): void inside both boxes in the same color to signal overriding. Draw a solid arrow from x.getData() at the call site down to XYZ.getData(), and a dashed super arrow from inside XYZ.getData() back up to ABC.getData(). Label the signature components — name, return void, parameter list () — along the arrow to reinforce the check.
Pitfalls: Do not think a different return type still counts as overriding — it does not (you get a compile error or a separate overload). Do not think super can skip two levels in one hop — Child.super.print() jumps only to Parent; Parent must itself call super.print() to reach Grandparent. Do not forget that calling the overridden method without super inside the child causes recursion (getData() calling getData()), not a parent call.
Recap and bridge: Overriding needs the same name, same return type, same parameter count and types; then the child's version silently wins and super is the key to reach the parent's. Exam note: expect questions that test the exact definition — same name plus same return type plus same number and types of arguments — and the use of super to reach the parent version from within the overriding method. A frequent slip is to think that a different return type still counts as overriding; it does not. This dispatch choice becomes dynamic when a parent reference points to a child object, which the next topic explores.
26.1.3 Worked Example — Multi-Level Inheritance with print()
Setup: three levels — Grandparent at the top, Parent in the middle, Child at the bottom. Each declares the same method:
class Grandparent {
public void print() { System.out.println("grandparent"); }
}
class Parent extends Grandparent {
public void print() { System.out.println("parent"); }
}
class Child extends Parent {
public void print() { System.out.println("child"); }
}
Each print() is an overridden method because name, return type void, and argument list () are identical at all three levels. The hierarchy is Child extends Parent extends Grandparent, arrows pointing upward to the parent.
Worked trace — three levels and super walking one level at a time: Start with Child c = new Child(); — construction runs Grandparent constructor, then Parent, then Child (super-to-sub order). Call c.print(); — the runtime type is Child, so the JVM selects Child.print() and prints child (bolded answer for this call: child). Now trace super chains. If Child.print() is coded as { super.print(); System.out.println("child"); } then calling c.print() first runs Parent.print() — which itself may call super.print() — then finishes the child line. Concrete step-by-step with the chain super included: (1) c.print() enters Child.print(). (2) super.print() jumps to Parent.print(). (3) Parent.print() executes super.print() which jumps to Grandparent.print() and prints grandparent. (4) Control returns to Parent.print() which prints parent. (5) Control returns to Child.print() which prints child. Combined output in that chaining version is grandparent, then parent, then child on successive lines. Without any super calls, only the most derived line child appears — the parent versions are hidden, not executed. Sense-check: the method that matches the actual object always wins; super is the only way to walk one step upward.
A second walk-through in the same style uses getData() in ABC and XYZ described above, with XYZ x = new XYZ(); x.getData(); routing to the child version unless super.getData() is used inside XYZ. The diagram for this duplicate example mirrors the print() chain: XYZ box below ABC, arrow down from x.getData() to the child block, super arrow up inside the child.
Pitfalls and scope: The three-level chain makes clear that overriding is not limited to one parent-child pair — the same rule applies at each level independently. The word super never means grandparent in one step; it always means the immediate parent. Also, you cannot write c.super.print() from outside the class to bypass the child; super is a keyword for use inside the child class body only.
Recap: Multi-level overriding repeats the same signature at each inheritance step; a call through a Child variable executes the Child version, and each super.print() moves exactly one level upward. This sets up the next idea — what happens when the variable's declared type is Parent but the object inside is a Child.
26.2 Dynamic Method Dispatch, Runtime Polymorphism and Upcasting
26.2.1 Runtime Polymorphism Explained
Hook: A variable declared as Parent can hold a Child object — and when you call show(), it is the child's show() that runs. How does Java know which version to pick when the declaration says one type and the object says another?
A dynamic method dispatch — the rule by which a call to an overridden method is resolved at runtime rather than at compile time — is the mechanism behind runtime polymorphism — polymorphism where the actual method executed depends on the object, not the reference type, during execution. A related term is upcasting — when a reference variable of the parent type points to an object of the child type. Together they give the "one interface, multiple behaviours" power of object-oriented design.
Intuition — label on the box versus content inside the box: Think of a shipping box labeled Parent, but inside the box you actually placed a Child object. The label tells the compiler what operations are allowed to be requested (you can only ask for methods declared in Parent), but the content decides how each request is carried out. When you say "please show() yourself," the content — the real Child object — answers, not the label. This is the professor's central image preserved verbatim: the label says Parent, the content is Child, and the content decides what happens. Where the analogy breaks: unlike a real shipping box, the Java label also restricts what you can ask — you cannot call a method that exists only in Child through a Parent reference without a downcast, even though the content would know how to answer.
In Java, you can write Parent obj; on the left side and obj = new Child(); on the right side. The left side type is Parent, the right side object is Child. This is allowed and is called upcasting. The variable obj is a parent reference, the object it points to is a child object. The assignment compiles because every Child is-a Parent, so a parent-typed variable can safely refer to any child object. The opposite — Child c = new Parent(); — does not compile without a cast, because not every parent is a child.
When you later call obj.show(), which show() runs? The answer is determined by the object, not by the variable type. If Child overrides show(), the child's show() runs even though the variable is declared as Parent. That choice is made at runtime, after the program starts, when the actual object type is known. The compiler only checks that Parent declares a show(); the JVM at runtime follows the object's virtual table to the overriding version.
Think of it like this: the label on the box says Parent, but the content inside the box is a Child. When you ask to play or show, the content decides what happens. This separation is why inheritance supports flexible code where a parent reference can work with many child types.
Formalize — the three rules that make dispatch work: (1) Upcasting assignment rule: If Child extends Parent, then Parent p = new Child(); is legal and is called upcasting; p can refer to a Child (or grandchild). (2) Compile-time check: The compiler allows p.show() only if Parent declares show() (or inherits it) with a compatible signature. (3) Runtime selection: At the call moment the JVM inspects the actual object that p points to, looks up that class's method table, and if the object's class overrides show() it invokes the overriding version; otherwise it walks up the chain to the first superclass that provides show(). Only virtual instance methods (non-private, non-static, non-final) dispatch this way; static, private, and final methods are bound at compile time and do not participate in dynamic dispatch.
A visual makes the mechanism clear: draw two columns. Left column shows the reference variable obj : Parent as a small labeled box with an arrow pointing to an object bubble on the right labeled Child object. Inside the Child bubble write the overridden method show() -> "child show" in bold, and inside the Parent shape behind it write show() -> "parent show" in light gray. The call arrow obj.show() goes from the variable to the bubble's table, then a selection arrow highlights the child's version. Annotate the arrow "runtime lookup — object type wins."
Scope and assumptions — when dispatch applies and when it does not: Dynamic dispatch applies only to overridden instance methods that are public or protected (or package-private in the same package) and that use the identical signature. It does not apply to constructors, static methods, private methods, or final methods — those are resolved from the reference type at compile time. The object, not the reference, matters only when overriding is present; if Child does not override show(), the lookup walks to Parent.show() and runs it. Upcasting is always safe without a cast; the dangerous direction is downcasting (Child c = (Child) p;), which needs a runtime check and can throw ClassCastException if p does not actually point to a Child.
Pitfalls: Do not say "the variable's type decides which method runs" — that is compile-time thinking; at runtime the object's type decides. Do not try Parent p = new Child(); p.childOnlyMethod(); — even though the object has that method, the Parent label does not advertise it, so the compiler rejects it. Do not confuse overloading with overriding — an overloaded method with a different parameter list in Child does not replace the parent's version; dispatch picks by exact signature, then by object type. Do not expect static methods to be polymorphic — Parent.showStatic() called via a child reference still runs Parent's version.
Real-world and domain connection: Runtime polymorphism is the reason a single method like void render(Shape s){ s.draw(); } can draw circles, rectangles, and triangles without if branches — each Shape child overrides draw() differently, and s may point to any of them. In the Java collections framework a single Collection<BankAccount> accounts list can store SavingsAccount and CheckingAccount objects; iterating and calling acc.calculateInterest() dispatches to the right child each time. Server code, GUI event handling, and payment processing all lean on a parent reference handling many child types through the same call site.
Recap and bridge: Upcasting lets a parent-typed variable hold a child object; dynamic dispatch then guarantees that an overridden show() call executes the child's version, chosen at runtime by inspecting the real object. The label controls what you may ask, the content controls how it answers. Next we see how a class can leave that answer deliberately unspecified for children to fill in.
26.2.2 Worked Example — Parent Reference to Child Object
Code:
class Parent {
void show() { System.out.println("parent show"); }
}
class Child extends Parent {
void show() { System.out.println("child show"); } // overridden
}
public class Test {
public static void main(String[] args) {
Parent obj = new Child(); // upcasting: parent reference, child object
obj.show(); // calls Child.show()
}
}
Worked trace — every step and what the JVM does: Step 1 — declaration Parent obj creates a reference variable obj whose static (compile-time) type is Parent. The compiler records that obj may only call members declared in Parent. Step 2 — new Child() allocates a new object whose dynamic (runtime) type is Child. The object's method table points to Child.show() because Child overrides show(). Step 3 — assignment obj = new Child() is upcasting; the bit-pattern for the Child object address is stored in the parent-typed slot obj. No conversion of the object occurs — only the reference's view changes. Step 4 — call obj.show(). The compiler checks: does Parent have show()? Yes — void show(). Call is allowed. At runtime the JVM follows the reference to the actual object, sees its dynamic type is Child, looks up show() in Child's virtual table, finds the overriding Child.show(), and invokes it. Output is child show (bolded). Step 5 — if Child had not overridden show(), the lookup would walk one level up to Parent.show() and that light-gray fallback would run instead, printing parent show. The red highlight in the displayed code marks the overridden show() in Child to stress this point. The same pattern applies to any hierarchy depth — grandparent reference pointing to parent or child object, selection still follows the real object. Sense-check: because the real object is a Child, not a bare Parent, the more specific behaviour correctly wins; if the output were parent show it would mean dispatch used the label, not the content, which Java does not do.
Visual reinforcement: Draw the timeline of the call. At compile time, a checkpoint labeled "compiler: Parent has show()? YES — compile succeeds." At runtime, a second checkpoint labeled "JVM: object is Child — use Child.show()." Connect the two with a dashed line showing that runtime corrects any label-only guess.
Scope and nuance: The variable obj can be reassigned: obj = new Parent(); obj.show(); would then print parent show because the object changed. After upcasting, you can only call methods visible through Parent; a downcast ((Child)obj).childOnlyMethod() is needed to reach child-only members, and it fails if obj does not actually point to a Child. In a three-level hierarchy Grandparent g = new Child(); g.show(); still prefers Child.show() if it overrides all the way — the most derived override always wins.
Recap and exam bridge: Exam note: questions often ask "which show() will be called" given Parent obj = new Child(); obj.show();. The answer is the child's version, explained with the terms upcasting (parent reference holding a child object) and dynamic method dispatch (runtime choice based on the actual object, not the reference type). Be ready to name both terms, draw the label-versus-content picture, and state why a direct call like new Parent().show() would give parent show while the upcast gives child show.
26.3 Abstract Classes
26.3.1 Definition and Purpose
Hook: What if you want to force every bank account type to define getData() and showData(), but you have no meaningful way to implement those methods for the generic idea of "an account"? An abstract class lets you declare the promise without giving the answer.
An abstract class — a restricted class that cannot be used to create direct objects — works as a template. It can declare methods without giving them a body and can also define concrete methods. An abstract method — a method declared with the keyword abstract and no implementation — states what a sub class must do, not how. It is a responsibility handed downward: the parent decides the name and signature, the child decides the body.
Intuition — stencil versus finished painting: Think of an abstract class as a stencil with two cut-out shapes labeled getData and showData. You cannot hang the stencil on the wall as art (you cannot instantiate it), but you can lay it over a canvas and paint inside the cut-outs — each concrete child fills the same outlines with its own colors. Where the analogy breaks: unlike a single-use stencil, one abstract parent can stencil many different children, and the parent stencil may already have some fully painted sections (concrete methods) that children inherit without repainting.
You mark a class with the keyword abstract, for example abstract class ABC. You mark a method similarly, for example abstract void getData(); and abstract void showData();. Inside an abstract class you cannot create objects directly with new ABC(); instead you create a concrete sub class that extends the abstract class and provides bodies for all abstract methods. Only then can you instantiate the sub class.
Formalize — syntax, contract, and completeness rule: Declare an abstract class as abstract class ABC { abstract void getData(); abstract void showData(); void concreteHelper(){ ... } }. Each abstract method ends with a semicolon and has no braces. Any class containing at least one abstract method must itself be marked abstract. The rule for concrete children: a subclass DerivedA extends ABC must override every abstract method from ABC with a concrete body; if it misses even one, the subclass itself must be declared abstract and remains non-instantiable. You may still declare fields, constructors, concrete instance methods, and static methods inside an abstract class — it is not purely promises. You can also declare a reference variable of abstract type (ABC ref;) and point it at a concrete child object (ref = new DerivedA(10);), which is how runtime polymorphism continues to work; what you cannot do is new ABC() on its own. Constructors exist in abstract classes to initialize shared fields when a child calls super(...).
An abstract class enforces a hierarchy: the declaration lives at the top, the definitions live below. It ensures that related classes share symmetry and promise the same set of operations. That symmetry is valuable in large codebases — every account type, every shape type, every handler type promises the same entry points, so calling code can rely on them uniformly.
Visual: draw a pyramid. At the apex put the abstract box ABC with two entries marked "abstract — no body": getData() and showData(). Below the apex draw two concrete boxes DerivedA and DerivedB, each with solid versions of getData() and showData() filled in. Draw arrows upward labeled extends. Cross out an illustration of new ABC() with a red X and label "compiler error — cannot instantiate abstract," while showing new DerivedA(10) with a green check.
Scope and assumptions: Use an abstract class when related children share structure and some behaviour, and you want to capture common fields or partially shared methods alongside abstract promises. An abstract class can have private or protected fields, multiple constructors, and fully implemented methods — it is not forbidden from doing work. It cannot be instantiated, but it can be referenced via a parent-typed variable pointing to a concrete child. If a concrete child fails to implement an inherited abstract method, compilation fails unless the child is itself marked abstract. An abstract class may declare zero abstract methods and still be non-instantiable if you want to force subclassing.
Pitfalls: Do not write new ABC() and expect it to compile — the compiler blocks any direct instantiation of an abstract type. Do not leave an abstract method without a body in a class you claim is concrete — the compiler will insist the class be marked abstract. Do not assume an abstract class cannot have a constructor — it can, and a child constructor that calls super(args) runs it to initialize inherited fields. Do not confuse an abstract class with an interface — an abstract class can hold state (fields) and constructors; an interface's instance fields are implicitly public static final.
Real-world and domain connection: The Java library uses abstract classes such as java.awt.Component and java.util.AbstractList as shared skeletons that concrete widgets and lists flesh out. In banking code an abstract class BankAccount { abstract void calculateInterest(); void deposit(double amt){ balance+=amt; } } forces every account type to supply its interest math while reusing common deposit logic — the exact symmetry the lecture enforces with getData() and showData().
Recap and bridge: An abstract class is a non-instantiable template that may contain both abstract promises and concrete helpers; every concrete child must fulfil all abstract promises or remain abstract itself. This idea generalizes one step further with interfaces, which capture pure promises without the class's state.
26.3.2 Worked Example — ABC with Two Abstract Methods
abstract class ABC {
abstract void getData();
abstract void showData();
}
class DerivedA extends ABC {
DerivedA() { } // zero-argument constructor, does blank initialization
DerivedA(int a) { this.a = a; } // one-argument constructor, stores value in field a
void getData() { /* body that reads or sets data */ }
void showData() { /* body that prints data */ }
int a;
}
Worked trace — declaring, fulfilling, and instantiating: Step 1 — declaration: abstract class ABC with abstract void getData(); and abstract void showData();. At this point ABC has a contract but no behaviour for those two names; the compiler marks ABC non-instantiable. Step 2 — subclass header: class DerivedA extends ABC. The child inherits the promise to implement both methods. Step 3 — constructors: DerivedA() does blank initialization (field a stays 0, the default for int); DerivedA(int a){ this.a = a; } receives parameter a and stores it in the field this.a, so new DerivedA(10) holds a == 10 while new DerivedA() holds 0. Both constructors, if written with super() implicitly, chain to Object via ABC's implicit no-arg constructor. Step 4 — fulfilling the contract: void getData(){ ... } and void showData(){ ... } provide concrete bodies that override the abstract declarations; without both overrides DerivedA would have to be marked abstract and could not be instantiated — the compiler would emit "DerivedA is not abstract and does not override abstract method ...". Step 5 — usage: DerivedA d = new DerivedA(10); d.getData(); compiles and runs because d is a concrete instance; d.showData() prints the stored a as implemented. In contrast, ABC x = new ABC(); fails to compile — "ABC is abstract; cannot be instantiated." Step 6 — polymorphic reference still works: ABC ref = new DerivedA(10); ref.showData(); compiles because ABC declares showData(), and at runtime dispatches to DerivedA.showData() — the call to the abstract declaration correctly routes to the child's concrete body. Sense-check: the abstract parent defines what must exist; the child defines how it behaves — separation of specification from implementation.
A second check: draw memory slots. For new DerivedA(10), show a heap object with field a = 10 and two method-pointer slots getData -> DerivedA.getData and showData -> DerivedA.showData. For the hypothetical new ABC(), draw a crossed-out box explaining that those two slots have no target — hence the instantiation ban.
Scope and nuance: If a subclass needs only one of the two abstract methods, it cannot still be concrete — it must implement both or stay abstract for someone else to complete. Adding a new abstract method to ABC later will break every concrete child until each adds the new override — this is why Java 8 later added default methods to interfaces as a softer evolution tool. Also, an abstract class can declare final concrete methods that children cannot override, mixing fixed behaviour with required overrides in one hierarchy.
Common confusion: students sometimes try to instantiate the abstract parent directly. That fails. You can only hold a parent reference to a child object, similar to upcasting, but the object itself must be of a concrete child. Another trap is forgetting that DerivedA's two constructors are not inherited — they must be written explicitly if you want both the zero-argument and the int form.
Recap: abstract class ABC declares getData() and showData() without bodies; DerivedA must supply both plus any constructors it needs; direct new ABC() is illegal while new DerivedA(10) and ABC ref = new DerivedA(10) are legal and polymorphic.
26.4 Interfaces and Multiple Inheritance
26.4.1 Interface Basics
Hook: A class can extend only one parent, yet real objects need many promises at once — a checking account must be a BankAccount and be Printable and be Serializable. How does Java give one class several parents without the diamond confusion that plagues multiple class inheritance?
An interface — a contract that lists method declarations without bodies (except for default and static methods treated later) — defines what implementing classes must do. A concrete class that implements an interface must provide bodies for all declared methods. Think of the interface as a checklist of method signatures; implementing it is signing a contract to tick every box.
Intuition — electrical socket and adapters: A class is like an appliance, an interface is like a wall socket standard (say "must have two flat pins and one round pin"). Any appliance that wants to plug in must expose the matching pins — the interface does not care how the appliance generates power internally, only that the plug shape is correct. Where the analogy breaks: unlike a physical socket, a Java class can plug into many different socket standards at once (multiple interfaces), and the socket itself can extend another socket's checklist.
Key syntax contrast: a class extends another class using extends, but a class implements an interface using implements. An interface can extend other interfaces using extends. Java does not allow a class to extend more than one class, so multiple inheritance of classes is blocked. Multiple inheritance through interfaces is allowed: a single class can implement several interfaces, combining their contracts.
Formalize — exact syntax and rules: Declare an interface as interface Bank { void deposit(double amt); void withdraw(double amt); }. Every method without a modifier is implicitly public abstract; fields if any are implicitly public static final. A class promises the contract with class CheckingAccount extends BankAccount implements Bank, Printable — extends must appear before implements, and multiple interfaces are comma-separated. If interface Printable extends Bank is written, that is allowed because interfaces extend interfaces; a class that implements the sub-interface must satisfy the union of all inherited abstract methods. Shape note from slides: draw two interface boxes Bank and Printable at the top, a class box BankAccount just below the left one, and a combined box CheckingAccount at the bottom with one extends arrow to BankAccount and two implements dashed arrows to Bank and Printable. The implementing class box must list bodies for every void deposit(...), void withdraw(...), and void print() or compilation fails.
Hierarchy practice: think of BankAccount as a class, Bank as an interface. CheckingAccount can be written as class CheckingAccount extends BankAccount implements BankInterface. The diagram with two interfaces on top and a single class below that implements both illustrates this.
The related idea of association between classes describes how objects relate, and the chart shown links multiple interfaces to a single implementing class to highlight that a class can promise several sets of behaviour at once.
A visual detail preserved from the slides: interfaces are often drawn with the <<interface>> stereotype and dashed implements arrows (open triangle, dashed line) versus solid extends arrows (open triangle, solid line). The distinction makes clear at a glance which relationship is class inheritance and which is contract implementation.
Scope and assumptions: Use interfaces when unrelated classes need a shared ability (flyable birds and flyable drones can both implement Flyable), and use class inheritance when there is a genuine is-a hierarchy with shared state or shared implementation. An implementing class must supply every abstract interface method unless the class is itself abstract; missing one method is a compile error. Prior to Java 8, every interface method was abstract by default — field state lived only in the class. Multiple implements is preferred over multiple extends because it avoids ambiguity where two parent classes define the same method with different bodies.
Pitfalls: Do not write class A extends I1, I2 — interfaces use implements, not a second extends. Do not write implements BankAccount when BankAccount is a class — classes are extends, interfaces are implements. Do not assume implements Printable automatically brings in a nested Showable — that requires implements Printable.Showable explicitly (next section). Do not forget that interface methods are public — the implementing method must be declared public, writing just void print(){...} without public will not compile.
Real-world and domain connection: The Java standard library recovers multiple inheritance through interfaces in practice. class ArrayList implements List, RandomAccess, Cloneable, and Serializable at once, each interface adding a separate promise. In domain code a CheckingAccount extends BankAccount implements Bank, Printable promises both money operations and the ability to print a statement; a PatientRecord extends Person implements Serializable, Comparable combines persistence and ordering. Choosing interfaces over multiple class inheritance keeps the type system clean.
Recap and bridge: A class extends at most one class but may implements many interfaces, each contributing abstract methods that the class must fill. The contract-vs-implementation split makes checked multiple inheritance safe. Next we examine how Java 8 softened the "every method is abstract" rule so published contracts could evolve without breaking every implementer.
26.4.2 Worked Example — Multiple Inheritance via Interfaces
interface Bank { void deposit(double amt); void withdraw(double amt); }
interface Printable { void print(); }
class BankAccount { double balance; }
class CheckingAccount extends BankAccount implements Bank, Printable {
public void deposit(double amt) { balance += amt; }
public void withdraw(double amt) { balance -= amt; }
public void print() { System.out.println(balance); }
}
Worked trace — why this compiles and what each clause contributes: Step 1 — interface Bank declares two promises: deposit(double) and withdraw(double), both implicitly public abstract. Step 2 — interface Printable declares print(). Neither interface contains fields of its own. Step 3 — class BankAccount { double balance; } supplies storage — the balance field that all accounts share. Step 4 — class CheckingAccount extends BankAccount implements Bank, Printable does three things at once: (a) via extends BankAccount it inherits balance directly, no redeclaration needed, so balance += amt refers to the inherited field; (b) via implements Bank it must supply public void deposit(double amt){ balance+=amt; } and public void withdraw(double amt){ balance-=amt; } — each adds the amount math directly on the inherited field; (c) via implements Printable it must supply public void print(){ System.out.println(balance); }. Step 5 — instantiation CheckingAccount c = new CheckingAccount(); c.deposit(100); c.withdraw(30); c.print(); proceeds as: deposit sets balance to 100, withdraw reduces it to 70, print emits 70. Final printed value is 70 (bolded). If any of the three bodies were missing, javac would report "CheckingAccount is not abstract and does not override abstract method ..." and compilation stops — the contract is enforced. Sense-check: the single extends gives the field, the two implements give the required operations, and the one class body satisfies all three sources at once — exactly how Java simulates multiple inheritance without multiple class parents.
A second illustration from the slides links several standard interfaces (Serializable, Comparable, Cloneable) to one class box; the lesson repeats: one class can promise several sets of behaviour, each interface contributes its own set, and the implementing class covers the union.
Scope and nuance: If two interfaces declare the same method void print();, the implementing class supplies a single public void print() that satisfies both — there is no ambiguity because interfaces did not supply bodies (pre-Java-8). If one interface had declared int print() while the other declared void print(), the class could not satisfy both — the compiler rejects incompatible signatures. Also, default and static methods (next section) change this clean picture, since interfaces can then carry bodies.
In industry, the CheckingAccount pattern appears as class SavingsAccount extends Account implements InterestBearing, Insurable, or class DataNode extends Node implements Serializable, Cloneable. Each implements adds a cross-cutting capability while the single extends preserves the primary is-a lineage.
Recap: CheckingAccount extends BankAccount reuses storage; implements Bank, Printable forces the three method bodies; together they achieve multiple inheritance of types safely, with one concrete balance field shared by all three operations.
26.5 Default and Static Methods in Interfaces
26.5.1 Default Methods as Fallback Implementations
Hook: Ten thousand classes already implement Printable — and you now need every one of them to support a new show() operation. Requiring each class to add a method would break the universe. How can an interface grow without breaking its implementers?
Before default methods, any method in an interface had no body, and every implementing class had to define it. A default method — a method in an interface that carries the keyword default and includes a body — provides an implementation directly inside the interface without breaking existing classes that implement the interface. It allows adding new methods to interfaces without forcing every old implementing class to change. The body is a fallback: if a class does not override it, the interface's version runs; if the class does override, the class's version is preferred.
Example layout as shown: interface Printable declares void print(); as a normal abstract declaration with no body. Right next to it, highlighted in red, is default void show() { System.out.println("default show"); }. The keyword default appears before the return type void, the method name is show, and the body prints.
Formalize — default method syntax and resolution rule: Write interface Printable { void print(); default void show(){ System.out.println("default show"); } }. The default keyword is a modifier before the return type, and the method must include braces with a body. An implementing class Trial implements Printable is now forced to implement only void print(); show() may be omitted and the class still compiles. The call-resolution rule: t.show() where t has static type Trial first looks for show() in the class Trial and its superclasses; if found, that version wins; only if no class version exists does the JVM fall back to Printable's default show(). If Trial itself later writes public void show(){ System.out.println("trial show"); }, the fallback is silently shadowed and t.show() emits trial show instead of default show. If a class implements two interfaces that both supply a conflicting default show(), the class must explicitly override show() to resolve the diamond or compilation fails.
Usage with a class:
interface Printable {
void print();
default void show() { System.out.println("default show"); }
}
class Trial implements Printable {
public void print() { System.out.println("trial print"); }
}
public class Test {
public static void main(String[] args) {
Trial t = new Trial();
t.print(); // calls Trial.print()
t.show(); // no show() in Trial, so falls back to Printable's default show()
}
}
Worked trace — fallback in action and the override upgrade: Compile-time phase: interface Printable contains one abstract print() and one default show() with a body. class Trial implements Printable implements print() as public void print(){...}; it does not implement show() — the compiler uses the default as the satisfied implementation and marks Trial concrete without error. Run-time phase: Trial t = new Trial(); t.print(); looks up print() on the Trial object, finds Trial.print(), and prints trial print. Next, t.show(); looks up show() on the same Trial object, checks the class Trial for a method show, finds none, then inspects its interfaces, finds Printable.default show(), and invokes it — prints default show (bolded fallback answer). Now consider the upgrade: add public void show(){ System.out.println("trial show"); } to Trial and recompile. The new class version now hides the default; t.show() prints trial show instead. Deleting the show() body from Trial again would revert to default show with no recompilation needed elsewhere. Sense-check: default methods do not require a class change; they behave like a safety net that vanishes once the class writes its own version.
Walk-through summary: Trial implements Printable and must define print() because it is abstract. It does not define show(), yet t.show() compiles and runs because Printable supplies a default show(). If later Trial adds its own show(), calls to t.show() will prefer the class version over the default. If you delete the show() method from Trial (when it exists), you must also remove any direct call expectation, but the program still compiles due to the fallback. A common question discussed is "what happens when show() is removed from Trial?" Nothing breaks for the outer interface implementation, because the default remains.
A visual: draw Printable as a dashed box with two compartments — upper says print() — abstract (no body), lower says show() — default with body. Draw Trial as a solid box with one compartment print() — concrete and a dashed outline where show() would be, connected with an arrow labeled "fallback -> interface default" that only fires when the class compartment is empty.
Scope and assumptions: Default methods were introduced in Java 8 to let published interfaces evolve. They may call private helper methods inside the same interface and may not access instance fields of the implementing class directly — they behave like interface-owned bodies, not class fields. An abstract class can still define state (fields), a default method cannot (interfaces have only public static final constants). When a class inherits the same default method from two interfaces, it must disambiguate by overriding and optionally delegating with InterfaceName.super.show().
Q&A — the examined confusion: Q: What happens when the implementation of show() is removed from Trial? Does the program break? A: No, the program still works. show() is a default method in the interface Printable. If Trial does not provide its own show(), the call t.show() simply uses the interface's default implementation. Default methods exist exactly to allow this fallback without forcing every implementer to define the method. Think of it as evolution insurance: ten million old classes can keep running while a new show() capability is rolled out through the interface alone.
Pitfalls: Do not omit default and still give a body — void show(){...} inside an interface without default or static fails to compile (the compiler expects abstract only). Do not expect t.super.show() to reach the default from outside Trial — the Interface.super.method() syntax only works from inside the implementing class's own override. Do not assume the default will be chosen over a class method — class methods always beat default methods in resolution.
Recap: default void show(){...} inside Printable is a concrete fallback that lets Trial omit show() and still compile; t.show() runs Trial.show() when present and Printable's default show() otherwise. This teaching moment on default method fallback and interface evolution is essential to retain.
26.5.2 Static Methods in Interfaces
Hook: A helper that needs no object state — like a factory or a utility — should be reachable as InterfaceName.helper() without ever creating an implementing object. Can an interface own such a method?
A static method in an interface — a method marked static inside an interface, accessed via the interface name — behaves like a static method in a class. A static variable is a class variable; a static method is reached through the class or interface name, not through an object. Unlike default methods, a static interface method is never inherited by the implementing class and never participates in polymorphism.
Formalize — syntax and access rule: Write interface Printable { static void show(){ System.out.println("static show"); } }. The static keyword precedes the return type, the body is required, and the method is implicitly public. You call it only qualified: Printable.show(); — there is no need for new Printable() (illegal anyway) and no t.show() instance form. If ABC is a class with static void getData(), you similarly call ABC.getData(); the same way. A static interface method is not abstract, is not overridden, is not inherited, and cannot be chosen by dynamic dispatch; it is bound at compile time to the interface type where it is declared. A default method is instance-bound and can be overridden; a static method is type-bound and cannot.
If ABC is a class with static void getData(), you call it as ABC.getData(). Similarly, if Printable is an interface with static void show(), you call it as Printable.show();. No object of Printable is needed.
Example:
interface Printable {
static void show() { System.out.println("static show"); }
}
// call site
Printable.show();
Worked contrast — default versus static at the call site: Keep interface Printable { default void show(){ System.out.println("default show"); } static void helper(){ System.out.println("static helper"); } } and class Trial implements Printable { public void print(){ } }. Calls: Trial t = new Trial(); t.show(); compiles — show() is an instance fallback reachable through the object, prints default show. In contrast, t.helper(); does not compile — helper() is not an instance member of Trial. The correct call is Printable.helper(); which resolves at compile time to the interface's static body and prints static helper. Adding helper() to Trial would be a different static method on the class, not an override of the interface's static — the two hide rather than override. Rule bolded: call instance default methods through the object, call static interface methods through the interface name.
Normal instance methods in an interface remain abstract; default and static methods are the two forms that can carry a body inside an interface. Visual contrast: draw three interface compartments — print() abstract, show() default (instance, inherited), helper() static (type, not inherited). Draw an instance call arrow from a Trial object to the default compartment and a static call arrow directly on the interface box for helper(), showing that the implementing class box never receives a dashed inheritance arrow for the static member.
Scope and assumptions: A static interface method cannot be marked default — the two modifiers are mutually exclusive. A static interface method cannot use super or this to reach instance state because there is no this. It may freely call other static interface methods or utility logic. Prior to Java 8 interfaces could not own static bodies at all; post-8 they can, making interfaces richer mix-in providers.
Pitfalls: Do not call t.show() when show() was declared static in the interface — the compiler rejects it as "non-static method cannot be referenced from a static context" in the other direction; correct is Printable.show(). Do not expect @Override on a static method to override the interface's static — it will error because no overriding occurs. Do not try new Printable() to reach a static interface method — instantiation of an interface is illegal.
Recap: static void show() inside an interface is called as Printable.show() via the type name, is never inherited, and never dispatched polymorphically, completing the pair with default methods that are inherited instance fallbacks.
26.5.3 Industry Note
Real-world: Java 8 introduced default methods to let APIs evolve. Collections interfaces added new default methods without breaking the many classes that already implemented them. Classic examples are Collection.forEach, List.sort, Map.getOrDefault, and Iterable enhancements — each was added as a default so the thousands of existing List and Map implementations kept compiling without modification. Static interface methods similarly provide namespaced helpers like List.of() or Comparator.comparing(). Together they let large ecosystems extend contracts over time while preserving backward compatibility, the exact fallback lesson examined above.
Recap of 26.5: Default instance methods give a fallback body reachable through an object when the class omits an override; static interface methods give a helper reachable only through the interface name. The former preserves t.show() compatibility, the latter enforces Printable.show() discipline — one preserves polymorphism, the other escapes it.
26.6 Nesting of Classes and Interfaces
26.6.1 Forms of Nesting
Hook: When two types only make sense together — a Printable contract and the Showable ability that only a printable thing would expose — should they live in separate files or be grouped as outer and inner?
A nested class — a class defined inside another class — and a nested interface — an interface defined inside another class or interface — allow logical grouping in one place for readability and maintainability. Grouping says these types are coupled in purpose and will not be reused far apart; it also controls visibility and namespace so Printable.Showable is directly a sub-contract of Printable.
Intuition — folder and sub-folder: Think of Printable as a folder on a shelf and Showable as a sub-folder inside it. The outer folder holds its own document (print()), the sub-folder holds a second document (show()). Grabbing the outer folder does not automatically grab the sub-folder's document. Where the analogy breaks: unlike a filesystem, Java lets an inner type access private members of the outer, and the compiler tracks nested types as qualified names rather than separate top-level names.
Four combinations are possible:
- A class inside a class:
class ABC { class XYZ { } }—XYZis an inner class insideABC. - An interface inside an interface.
- A class inside an interface.
- An interface inside a class.
Nested members can access all members of the outer class, including private data members and methods, because the inner member is considered part of the outer object. This privileged reach is the practical gain beyond mere grouping — an inner class can manipulate the outer's private state without exposing it.
Formalize — qualified names and required implements: Nesting is written by textual inclusion: interface Printable { void print(); interface Showable { void show(); } }. The inner interface's qualified name is Printable.Showable, and the inner scheme is not automatic inheritance — Printable and Printable.Showable are separate contracts. class Trial implements Printable satisfies only print(); class Trial2 implements Printable.Showable satisfies only show(); a class that needs both writes class Combined implements Printable, Printable.Showable (or implements Printable.Showable if the outer's print() is also needed, list both). A class inside a class is written class Outer { class Inner { void use(){ System.out.println(privateField); } } } — the inner Inner can read privateField of Outer directly because it is treated as part of the outer's implementation.
Example with two interfaces, one nested:
interface Printable { // outer interface
void print(); // belongs to outer
interface Showable { // inner interface
void show(); // belongs to inner
}
}
class Trial implements Printable {
public void print() { System.out.println("print"); }
// implementing Printable does NOT force implementing Showable's show()
}
// To require show(), you would write:
class Trial2 implements Printable.Showable {
public void print() { System.out.println("print"); } // if also implementing outer?
public void show() { System.out.println("show"); }
}
Worked trace — what implements actually promises: Start with the nested declaration above where Printable holds void print() and Printable.Showable holds void show(). Case A — class Trial implements Printable: the compiler creates a to-do list [print] — only print() must be supplied. Trial defines public void print(){...} and compiles; show() is not on the list, so omitting it is correct. Calling new Trial().print() prints print; calling new Trial().show() does not compile — no such method is promised. Case B — class Trial2 implements Printable.Showable: the to-do list is [show] only; Trial2 must supply public void show(){...}. If Trial2 wants both promises it must write class Both implements Printable, Printable.Showable giving to-do list [print, show] and supply both bodies — then b.print() and b.show() both compile. Deleting show() from Trial in Case A never breaks anything because show() was never required — that is why the lecture notes "removing show() from Trial causes no problem." Sense-check: the qualified name Printable.Showable is a separate address; implementing the outer does not deliver the inner envelope.
Clarification as discussed: outer interface is Printable with void print(), inner interface is Showable with void show(). A class Trial that writes implements Printable must define print() but need not define show(), because show() lives in the inner interface. Removing show() from Trial causes no problem in this setup because it was never required. Conversely, a class that writes implements Printable.Showable would be forced to define show() from the inner interface, and if it also implements the outer, it would need both.
Visual: draw a large outer rectangle Printable with print() inside. Draw a smaller rectangle Showable nestled in the upper-right corner of the outer, containing show(). Draw class Trial below with a dashed arrow to the outer rectangle only, and class Trial2 with a dashed arrow to the inner rectangle. Label the dashed arrows implements Printable and implements Printable.Showable. This spatial nesting reinforces that namespacing is enclosure, not inheritance.
Scope and assumptions: A nested interface inside an interface is implicitly public static even without the keywords — no outer instance is needed to refer to Printable.Showable. A nested class inside another class as a member inner class needs an outer instance (new Outer().new Inner()), while a static nested class does not. All nested types can be public, protected, package-private, or private, controlling who outside may name them. Inner types can reach private outer members, but the outer does not automatically reach into private members of the inner.
Q&A — the crucial distinction: Q: If Trial implements the outer interface Printable, does it also have to implement Showable.show()? A: No. Showable is an inner interface. Implementing the outer Printable only requires print(). The inner show() is a separate contract and becomes required only when a class explicitly implements Printable.Showable (the inner) instead of just Printable. Outer interface has no automatic access to inner interface methods. The qualifier Printable. matters — it selects a different checklist, not the same checklist plus extras.
Pitfalls: Do not write class Trial implements Printable and add @Override public void show(){...} expecting it to count — the class is not required to define show() and adding it does not satisfy a missing print(). Do not confuse implements Printable.Showable with extends Printable — the inner is reached with the dot qualifier and is still an implements relationship when a class adopts it. Do not assume an inner type is inherited textual nesting — a member class inside an interface is still separate unless imported via implements.
Recap: Nesting groups a type inside another for organization and private access, but produces a separate qualified name Outer.Inner. Implementing the outer does not implement the inner; each implements clause must name the exact contract it promises.
26.6.2 Three Kinds of Inner Classes
Hook: Beyond grouping, inner classes differ by where they are allowed to live — as a member beside a field, nameless at the new site, or hidden inside a single method. Why three forms, and when would you choose each?
Nested classes appear in three practical forms:
- Member inner class — a non-static class defined directly inside an outer class as a member, with a name and full access to the outer's instance state. It behaves like a field that is itself a class. You create it as
Outer outer = new Outer(); Outer.Inner in = outer.new Inner();and it holds an implicit reference to the enclosingouterobject.
- Anonymous inner class — a class defined without a name, typically right where it is instantiated, useful for one-off implementations. It is written as
Printable p = new Printable(){ public void print(){...}};— noclassname, no reuse elsewhere, created and used at the single expression site. Common for short listener orRunnableoverrides.
- Local inner class — a class defined inside a method or block, visible only within that scope. It cannot have a public modifier and its instances capture only
finalor effectively final locals. Example:void process(){ class Helper { void run(){...}} Helper h = new Helper(); }—Helpervanishes outsideprocess().
Formalize — lifetime and access distinctions: A member inner class lives as long as its outer instance and carries that outer reference implicitly, so it can call Outer.this.privateMethod() directly. An anonymous inner class has no constructor name and is a single-use subclass/implementor created at the new expression; it too can access the outer's private state but exists only at that call site. A local inner class is lexically scoped to a method — outside the method its name is meaningless — and like an anonymous class it may access only final/effectively final locals plus the outer's fields. All three forms can reach private outer data because the compiler treats them as part of the outer's implementation, but only the member form has a reusable name that appears in multiple statements.
These forms appear in detail in the supplemental slides with full code, intentionally skipped in the review due to time constraints but recommended for self-study. The key property remains: an inner class is part of the outer class, so it can reach private fields of the outer. For revision, sketch three panels: panel 1 shows a class box with a smaller class box beside a field labeled "member — has a name, needs outer.new Inner()"; panel 2 shows a new expression with a curly block and the label "anonymous — no name, at the new site"; panel 3 shows a method box with a tiny class inside labeled "local — lives only inside the method."
Visual plus self-study cue: compare the instantiation forms side-by-side in prose. Member: Outer out = new Outer(); Inner in = out.new Inner(); in.use(); — two steps, needs an outer instance. Anonymous: Printable p = new Printable(){ public void print(){...}}; p.print(); — one expression, no name. Local: inside void m(){ class Local{ ... } Local l = new Local(); } — the class name Local is unknown to any other method. This table makes the "where it lives" distinction concrete.
Scope and assumptions: Member inner classes cannot declare static members except static final constants. Anonymous classes cannot declare explicit constructors (no name), so initialization goes in an instance block. Local inner classes cannot be public or static, and they capture locals only if those locals are effectively final. All three are desugared by the compiler to separate class files with names like OuterInner.class or Outer1.class (the compiler uses a dollar sign in the actual file name), but logically remain nested.
Pitfalls: Do not try new Inner() without an outer instance for a non-static member inner class — the compiler will require outer.new Inner(). Do not try to give an anonymous class a constructor name — write initialization in an initializer. Do not modify a local variable after a local or anonymous inner class captures it — the variable must stay effectively final, otherwise compilation fails. Do not assume local inner classes are visible elsewhere — moving a local class outside its method is not refactoring, it is a structural change.
Real-world and domain connection: Member inner classes power builders such as Map.Entry as an inner view of Map, or LinkedList's internal Node class. Anonymous inner classes historically implemented event listeners — button.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ ... } }) — before lambdas. Local inner classes collect short helpers inside a single complex method without polluting the public API. Recognizing which nesting form is in use lets readers predict where the name is visible and whether an outer instance is implicitly carried.
Recap and bridge: Member inner classes have a name and need an outer instance, anonymous classes have no name and live at the new site, local classes live inside a single method — all three can reach private outer state but differ in lifespan and naming. With grouping and lifetimes covered, the lecture turns to robustness when the runtime goes wrong.
26.7 Exception Handling — Try, Catch, Finally and Hierarchy
26.7.1 What an Exception Is
Hook: The code compiled without error — yet at runtime it stops dead on a single line and the next println never executes. What happened between compilation and execution, and what name does Java give that abrupt event?
An exception — an event where a program that compiled without problems nevertheless stops in an abnormal way during execution — must be handled to keep the program alive. Compilation checks syntax and types; exceptions report runtime conditions that syntax alone could not prevent, such as a zero divisor arriving from user input, a missing file, or an out-of-range index.
A textbook trigger uses three variables:
int a = 10;
int b = 0; // supplied by the user
int c = a / b; // statement that may generate an exception
The spoken description is "" where and . Mathematically this is with . Division by zero gives infinity in theory, but in Java integer division by zero throws an ArithmeticException and terminates execution at that line abnormally. Whatever statements follow this line never execute. That abnormal termination point is the exception.
Formalize — the exact failing expression and the ArithmeticException contract: The risky assignment is with and . In exact mathematics is undefined (not a finite integer); on real numbers a hand-waving limit would tend toward infinity, but Java integers have no representation for infinity, so the language defines the operation as erroneous at runtime. The JVM detects b == 0 at the division bytecode and, instead of producing a value for c, creates an object of type ArithmeticException and throws it, abandoning the current call frame. If that throw is not caught, the thread's run terminates and the JVM prints the stack trace; all lines after int c = a / b; in the same method are unreachable in the exception path. The Java integer division contract is: for int or long, is valid only when ; when the expression does not evaluate to a number — it raises ArithmeticException. Floating-point division by 0.0 is different (it yields Infinity per IEEE 754) and does not throw — a common point of confusion to keep distinct from integer division.
Real-world: unexpected input, file not found, network drop, or wrong array index all generate exceptions. A good program anticipates suspect statements and prepares handling paths, because unhandled exceptions freeze user sessions, leave files open, or roll back a half-written transaction.
Visual: draw a vertical sequence of boxes representing program lines. Box a=10, Box b=0, Box c = a/b highlighted in red with a branch labeled exception thrown, and Box println(c) grayed out with an X labeled "skipped." An arrow from the red box jumps to a catch handler box to show the alternative handled path.
Scope and assumptions: The zero-divisor example assumes int division. If a and b were double, 10.0 / 0.0 would yield Double.POSITIVE_INFINITY without an exception — different type, different contract. The variable b is described as supplied by the user precisely because compile-time checking cannot predict runtime user input; hence exception handling is a runtime concept, not a compile-time syntax fix. An exception does not mean the compiler missed a syntax error; it means the runtime state violates a precondition the code assumed.
Pitfalls: Do not assume a program that compiles is free of exceptions — compilation guarantees syntax, not runtime validity. Do not confuse integer division by zero (ArithmeticException) with general arithmetic — overflow in int arithmetic wraps silently without an exception. Do not promise that "division by zero always throws" — qualify it as integer division; floating-point division by zero is governed by IEEE 754 and produces Infinity or NaN.
Recap: An exception is a runtime abnormal termination at a line like when for integer types; the JVM throws ArithmeticException at that line and abandons subsequent lines in the path. The remedy is to anticipate such suspect lines and guard them, which the next subsection makes concrete.
26.7.2 Try, Catch, Finally and Hierarchy
Intuition — hospital analogy: Think of try as the operating theatre where risky surgery happens, catch blocks as the specialists on standby each trained for a different emergency (one for ArithmeticException, another for any Exception), and finally as the cleanup crew that sterilizes the room no matter whether surgery succeeded or a crisis occurred. Where the analogy breaks: unlike a hospital, Java matches only the first compatible catch; later catches are never consulted once one fires.
Java provides three constructs for handling:
try— encloses the statement you suspect may generate an exception. Writing that statement inside atryblock prepares the program to deal with trouble rather than crash.catch— follows atryblock and handles the exception thrown from inside thetry. Whatever exceptions thetrythrows are caught in correspondingcatchblocks.finally— differs from both. Acatchruns only if an exception occurs. Afinallyalways runs, whether an exception occurred or not. It is the guaranteed cleanup phase for closing files, releasing locks, or flushing buffers.
Formalize — syntax, ordering rule, and hierarchy position: The template is try { riskyStatement(); } catch (SpecificException e){ handleSpecific(); } catch (Exception e){ handleGeneric(); } finally { alwaysRun(); }. The catch ordering constraint is strict: specific exception types such as ArithmeticException must appear before their supertypes such as Exception, otherwise the supertype would shadow the specific handler and the compiler rejects the unreachable code. Hierarchy-wise, Throwable is the ultimate parent. Its two main branches are Error (virtual-machine failures, not normally caught) and Exception (program conditions). Under Exception sit checked branches (for example IOException) and the unchecked branch rooted at RuntimeException which contains ArithmeticException and ArrayIndexOutOfBoundsException. The risky integer operation in this section is and its integer precondition is When the precondition fails the JVM constructs new ArithmeticException("/ by zero") and throws it; the first catch whose declared type is assignable from that throw handles it.
Example structure:
try {
int c = a / b; // risky statement c = a/b with b==0
} catch (ArithmeticException e) {
System.out.println("divide by zero");
} catch (Exception e) {
System.out.println("other exception");
} finally {
System.out.println("cleanup");
}
Worked comparison — no handling versus handling with exact output trace: Setup values: int a = 10; int b = 0;. Scenario A — no handling: lines execute as int c = a / b; System.out.println("after");. At c = a/b the JVM detects , throws ArithmeticException, abandons the assignment to c, skips println("after") entirely, and the program terminates at that line with a stack trace containing java.lang.ArithmeticException: / by zero. No later line in the method runs. Scenario B — with handling: try { int c = a / b; System.out.println("inside try after"); } catch (ArithmeticException e){ System.out.println("divide by zero"); } catch (Exception e){ System.out.println("other"); } finally { System.out.println("cleanup"); } System.out.println("after try-catch");. Step trace: enter try, attempt int c = 10 / 0 -> throw ArithmeticException. Jump to first catch (ArithmeticException e) — type matches, so print divide by zero (bolded: which catch fires). Skip the second catch (Exception e) because a catch already handled the throw. Enter finally — regardless of success or failure, print cleanup. Exit the whole construct and print after try-catch — the program lives past the failure. If b had been 5 instead, then c = 10 / 5 would succeed, c becomes 2, println("inside try after") would run, catch blocks would be skipped, but finally would still print cleanup before after try-catch. The finally guarantee is unconditional. Sense-check: the specific ArithmeticException handler must appear before the generic Exception handler; reversing them makes the specific handler unreachable and the compiler refuses the file — the ordering rule is syntactic, not stylistic.
Hierarchy visual: draw an inverted tree. Top node Throwable. Two children Error and Exception. Under Exception branch to IOException (checked branch) and RuntimeException (unchecked branch). Under RuntimeException draw ArithmeticException and ArrayIndexOutOfBoundsException. Annotate checked versus unchecked, and add a note that ArithmeticException is the descendant that catches for integer division, while IOException is never triggered by division.
Worked comparison — no handling versus handling:
- Class with no handling: the division line throws, the following
System.out.printlnnever runs; the program ends at the exception line. - Class with handling: the risky division is placed inside
try, twocatchblocks are supplied — one forArithmeticException(the specific divide-by-zero case) and one forException(a broader catch). Whichever matches handles the problem, and the program continues. Thefinallyblock runs in both success and failure paths.
Hierarchy: an exception hierarchy — a tree of exception types — sits under Throwable. At the top, Throwable is the ultimate parent. Below it, Exception is one of the root classes for all checked and runtime exceptions that programmers interact with. Specific exceptions such as ArithmeticException and ArrayIndexOutOfBoundsException extend deeper. Using Exception as a catch type catches many subtypes, which is why ordering matters: specific catches first, generic Exception later.
Mathematical view: for integers, the operation with is not a valid integer result, hence the exception. The division law in integer arithmetic requires the divisor to be non-zero for the quotient to be defined; the hardware signals a fault when that precondition is broken, and Java maps the fault to ArithmeticException rather than returning a bogus integer. The verbal description "a divided by b where b is zero" maps directly to the failing Java line.
Scope and assumptions: The two-catch example assumes the integer path where throws ArithmeticException; if the code used double, no throw would occur and neither catch would execute. The hierarchy described assumes the standard Java SE Throwable tree — library-specific exceptions add further leaves but never alter the parent relationships shown. finally is assumed to be present for cleanup; if present, it runs even when a return or loop break appears inside try or catch, unless the JVM exits or the thread is killed.
Pitfalls: Do not place catch (Exception e) before catch (ArithmeticException e) — the first handler would swallow all exceptions including ArithmeticException, making the second unreachable; the compiler errors with "exception has already been caught." Do not assume catch runs when no exception occurs — it never does; only finally is guaranteed. Do not leave code after c = a/b outside any try expecting it to still run after a throw — without handling, the program terminates at the throw site. Do not write a bare catch { } that silently swallows Throwable including OutOfMemoryError — catch the most specific type you can handle and let truly fatal errors propagate.
Recap and exam bridge: Exam note: be ready to write a try with multiple catch blocks and a finally, to identify which catch handles a given throw (specific ArithmeticException before generic Exception), and to explain why code after an unhandled c = a/b with is unreachable. Also know the hierarchy Throwable -> Exception -> RuntimeException -> ArithmeticException and that with int types is valid only for . Exam note: finally always executes — whether triggered the throw or let the try succeed, whether catch handled it, whether try contained a return, the finally print still appears.
26.8 Throw versus Throws, Checked versus Unchecked, User-Defined Exceptions
26.8.1 Checked versus Unchecked
Hook: The compiler forces you to handle IOException right now, yet it lets ArithmeticException slip by quietly until the program crashes. What makes one check happen at compile time and the other only at runtime?
A checked exception — a condition checked during compile time — must be declared or handled at compile time; examples include IOException. An unchecked exception — a condition that appears only at runtime — falls under RuntimeException; examples include ArithmeticException and ArrayIndexOutOfBoundsException. The distinction is not about severity but about where the Java language insists you acknowledge the risk.
Formalize — where each lives in the hierarchy and what the compiler enforces: Draw again Throwable at the top with branches Error and Exception. Under Exception, the checked branch contains direct subclasses such as IOException, SQLException, ClassNotFoundException — these are not descendants of RuntimeException. The unchecked branch is exactly RuntimeException and all its descendants, including ArithmeticException, ArrayIndexOutOfBoundsException, NullPointerException, IllegalArgumentException, and custom exceptions such as InvalidBoxDimensionException extends RuntimeException. Enforcement rule: any method that can throw a checked exception must satisfy one of two compile-time options — either catch it locally with try/catch, or declare it in its own signature with throws CheckedException; failure to do either is a compilation error. Any method that might throw an unchecked RuntimeException descendant is not required by the compiler to declare or catch it; the throw may still occur at runtime and will propagate if left unhandled, but the compiler will not stop compilation. Hence checked exceptions represent anticipated external failures the caller should plan for, while unchecked exceptions represent logic bugs or precondition violations the caller typically should fix rather than declare.
In the hierarchy, Exception splits into checked branches (direct subclasses of Exception other than RuntimeException) and the unchecked branch rooted at RuntimeException. IOException is a classic checked example: if a method can throw it, the compiler forces handling. RuntimeException branches contain errors that the compiler does not force you to handle upfront but that can still crash a running program.
A visual table lets the difference pop: columns Type, Root, Compile-time handling required?, Examples. Row 1: Checked, under Exception but not RuntimeException, Yes — catch or throws`, IOException. Row 2: Unchecked, under RuntimeException, No — compiler allows silence, runtime may still throw, ArithmeticException, ArrayIndexOutOfBoundsException, InvalidBoxDimensionException. The tree diagram highlights the fork point immediately below Exception`.
Scope and assumptions: Checked versus unchecked is defined by inheritance, not by annotation. Any subclass of RuntimeException is automatically unchecked even if it models a domain check like an invalid box dimension by choice; had InvalidBoxDimensionException extended Exception directly it would have been checked instead. Errors (Error branch) are also unchecked but are not meant to be caught. Whether an exception is checked also determines whether its throws declaration is mandatory in methods that propagate it — this matters in the next subsection contrasting throw (one site, generates) with throws (signature, forwards).
Pitfalls: Do not assume "unchecked" means "unimportant" — an unhandled ArithmeticException terminates the thread just as abruptly as a checked failure. Do not write catch (RuntimeException e) expecting to satisfy a checked IOException requirement — catching an unchecked parent does not count as handling the checked child. Do not declare throws RuntimeException to "make it checked" — the compiler still does not enforce callers to handle it; only the hierarchy decides checkedness. Do not swallow a checked IOException with an empty catch — check its message or log it, otherwise the compiler is satisfied but the bug is still hidden.
Recap: Checked exceptions (IOException) live on the Exception branch outside RuntimeException and the compiler forces a catch or throws; unchecked exceptions (ArithmeticException, ArrayIndexOutOfBoundsException, InvalidBoxDimensionException extends RuntimeException) live under RuntimeException and the compiler permits but does not require declaration. This declared-versus-voluntary split explains why the next pair throw/throws matters most for checked paths.
26.8.2 Throw versus Throws
Hook: Two nearly identical words differ by a single letter — throw versus throws — yet one creates a failure right now and the other only warns the caller that a failure may arrive later. Which one actually makes the program jump?
A throw clause — throw used to generate an exception — creates an exception object at a precise line. It is used for exception classes only. If you as a user decide that "whenever a value is less than zero, that should count as an exception" — even though the computer sees no error — you can enforce it with throw. A throws clause — throws used to forward an exception — states that a method may forward an exception to its caller when it cannot handle it itself. It is a forwarding mechanism, while throw is a generating mechanism. Specifically, if a method throws a checked exception, its signature must declare throws CheckedException, otherwise compilation fails.
Formalize — exact placement, number, and checkedness interaction: throw is a statement inside a method body that takes exactly one expression: throw new IllegalArgumentException("a < 0"); — it constructs the exception object with new and immediately transfers control to the nearest matching catch. throws is a clause in the method header after the parameter list and before the body: void readFile() throws IOException { ... } — it lists one or more exception types (comma-separated) the method may propagate without catching internally. Pairing rules: throw can throw any Throwable descendant, checked or unchecked, at any throw site. If the thrown type (or any throws type) is a checked exception, the enclosing method must either catch it or declare it with throws; if the thrown type is unchecked (extends RuntimeException), declaration is optional. The forwarding chain can be multi-level: Box constructor throws InvalidBoxDimensionException, caller method may itself throws again, up to main where a final catch may handle it.
Example to contrast:
// throw generates
if (a < 0) throw new IllegalArgumentException("a < 0"); // generation
// throws forwards
void readFile() throws IOException { // forwarding to caller
// code that may throw IOException internally
}
Worked contrast — one method generates, the next forwards: Method A — generating with throw: void checkAge(int age){ if(age < 0) throw new IllegalArgumentException("negative age"); System.out.println(age); }. Call checkAge(-3) -> condition true -> throw new IllegalArgumentException creates the object and abandons println(age) at once; the caller sees the throw. Call checkAge(5) -> condition false -> no throw, prints 5. Method B — forwarding with throws: void readFile() throws IOException { FileReader fr = new FileReader("data.txt"); }. Here new FileReader may throw IOException (checked). readFile() chooses not to catch it, so its header must contain throws IOException; the throw is created inside FileReader, forwarded through readFile()'s signature to whoever called readFile(). If a caller writes try { readFile(); } catch (IOException e){ ... } the chain ends there. If instead InvalidBoxDimensionException extends RuntimeException is thrown with throw new InvalidBoxDimensionException(-2) from a constructor that also declares throws InvalidBoxDimensionException, the throws is technically optional because the exception is unchecked, but writing it documents the intent and matches the lecture code. Bolded rule: throw creates the jump right at the line; throws writes the warning label on the method's door that the jump may pass through.
A visual clarifies the roles: draw a vertical call stack with main at the top calling readFile() which calls new FileReader. Place a red throw new IOException lightning bolt inside FileReader. Draw a dashed throws IOException arrow on the readFile() frame edge pointing upward, labeled "forwarding — no handling here." At the main frame draw a catch (IOException) box that absorbs the bolt. Label the two spots: inside the body (throw) versus on the signature line (throws). This picture preserves the lecture's line-level distinction.
Scope and assumptions: throw requires a new Exception(...) object (or an existing variable); writing throw IOException; without new is illegal. throws lists types, not objects, as in throws IOException, SQLException. A single method may contain multiple throw statements throwing different types, and its header may then list multiple throws entries covering them. For unchecked exceptions like InvalidBoxDimensionException extends RuntimeException, declaring throws is optional even when throw is used inside; consistency with the lecture example still writes the declaration for clarity.
Pitfalls: Do not write throws new IllegalArgumentException(...) — throws takes only type names, never new. Do not write throw IOException; without new — throw takes only an object reference. Do not omit throws IOException on a method that calls a checked-throwing API without a local catch — the compiler will reject the file. Do not think throws handles the exception — it only advertises it; only try/catch handles.
Recap: throw is the verb that creates and fires an exception object at a specific statement; throws is the warning in the method header that this method may let that fired exception fly to its caller. Checked exceptions must be covered by one of the two; unchecked exceptions tolerate silence on throws even though throw still works.
26.8.3 Worked Example — User-Defined Exception InvalidBoxDimensionException
Goal: treat an invalid box size as an exception the user defines, not the language's built-in one — negative or zero dimensions are not a Java error, but the application decides they should abruptly stop construction just as a language exception would.
Step 1 — define the exception. It extends RuntimeException, making it unchecked (so no mandatory throws declaration everywhere, as noted that it specifically extends the runtime branch):
class InvalidBoxDimensionException extends RuntimeException {
InvalidBoxDimensionException(double value) {
System.out.println("Box instance with invalid dimension: " + value);
}
}
Constructor receives a double value — the invalid dimension — and prints Box instance with invalid dimension plus the value. Real production code would also call super("Box instance with invalid dimension: " + value) to carry the message into the throwable's message field and optionally store the value as a field, but the lecture's defined form prints directly from the constructor for brevity.
Step 2 — define Box that uses it:
class Box {
double length, width, height;
Box(double length, double width, double height) throws InvalidBoxDimensionException {
if (length <= 0 || width <= 0 || height <= 0) {
throw new InvalidBoxDimensionException(length <=0 ? length : width <=0 ? width : height);
}
this.length = length;
this.width = width;
this.height = height;
}
double area() { return 2*(length*width + width*height + height*length); } // example helper
}
Worked trace — validating every dimension path: Keep the InvalidBoxDimensionException constructor that prints the bad value, and the Box constructor that checks if (length <= 0 || width <= 0 || height <= 0) then throw new InvalidBoxDimensionException(badValue) else assigns fields. Case 1 — invalid length only: new Box(-2, 5, 3). Evaluate condition: length <= 0 is -2 <= 0 true, so || short-circuits — throw new InvalidBoxDimensionException(-2) fires, constructor prints Box instance with invalid dimension: -2.0 (bolded trace for this path), field assignments this.length = length and friends are skipped, the partially constructed object is discarded, and the throw propagates to the caller. Case 2 — invalid width only: new Box(4, 0, 3). First test -2 <=0 analog becomes 4 <= 0 false, continue to width <= 0 is 0 <= 0 true, so the thrown value selects width as 0.0 and prints Box instance with invalid dimension: 0.0. Case 3 — middle and last invalid together: new Box(4, -1, -5) still selects the first failing value in the ternary order and prints -1.0; the point is that any dimension <= 0 triggers a single InvalidBoxDimensionException. Case 4 — all valid: new Box(4, 5, 6). Condition evaluates 4 <= 0 false, 5 <= 0 false, 6 <= 0 false, combined false, so throw is skipped, assignments run this.length=4, this.width=5, this.height=6, and area() would compute . Sense-check: the lecture chooses RuntimeException as the parent so the check fires without forcing every caller to declare throws — had it extended Exception directly, every method constructing a Box would need a mandatory throws InvalidBoxDimensionException declaration or its own try/catch, making the unchecked choice a deliberate API-style decision.
Walk-through recap: Box has three double fields length, width, height. Its constructor declares throws InvalidBoxDimensionException — the forwarding clause. Inside, it checks if (length <= 0 || width <= 0 || height <= 0). If any dimension is less than or equal to zero, it executes throw new InvalidBoxDimensionException(...), passing the offending value to the constructor which prints the message. This is the generation point using throw. If all dimensions are valid, the constructor assigns them and optionally computes area that can be printed via area().
Step 3 — handling at creation site:
try {
Box b = new Box(-2, 5, 3); // invalid length triggers throw
} catch (InvalidBoxDimensionException e) {
System.out.println("handled invalid box");
}
Worked call-site trace — recovery without termination: Wrap creation as try { Box b = new Box(-2, 5, 3); System.out.println("created " + b.length); } catch (InvalidBoxDimensionException e){ System.out.println("handled invalid box"); } System.out.println("continue");. Sequence: enter try, call new Box(-2,5,3) — inside the constructor the throw fires and the constructor prints Box instance with invalid dimension: -2.0 before the exception propagates. The System.out.println("created ...") inside try after the construction is skipped because control left at the throw. The nearest matching catch (InvalidBoxDimensionException e) handles it and prints handled invalid box. Then System.out.println("continue") after the whole try/catch runs — the program did not terminate, it resumed past the handled failure (bolded recovery point: execution continues at continue). Without the surrounding try/catch, the same throw would terminate that thread's path through main with a stack trace.
Real-world: user-defined exceptions model domain errors — negative box, negative balance, duplicate enrollment, expired ticket — where the language sees no failure but the application does. Choosing extends RuntimeException keeps the API lightweight (no forced declaration at every call site) while still letting callers wrap construction in try/catch where they want to recover; choosing extends Exception makes the checked contract louder and forces callers to decide at compile time.
Scope and assumptions for user-defined exceptions: An unchecked user-defined exception typically stores the offending value or a message in a field by calling super(message) in its constructor; the lecture's constructor prints immediately instead. For a checked counterpart, the Box constructor's throws InvalidBoxDimensionException would become mandatory rather than documentary, and every method creating a Box without its own catch would need to repeat throws. The area() helper is not invoked during construction — it is available only on successfully constructed boxes with all dimensions > 0.
Pitfalls: Do not extend Exception and forget to add throws to the Box constructor — the file will not compile because a checked type is thrown without declaration. Do not extend RuntimeException then assume you cannot catch it — any unchecked exception can still be caught; "unchecked" only means the compiler does not force you to. Do not write throw new InvalidBoxDimensionException without new — throw needs an object, just like any other statement that constructs a class. Do not validate only one dimension — test length, width, and height each against <= 0 as the lecture code does.
Recap and exam bridge: Exam note: be ready to write a domain exception class InvalidBoxDimensionException extends RuntimeException with a constructor that reports the bad dimension, a Box constructor that throws InvalidBoxDimensionException and contains if (length<=0||width<=0||height<=0) throw new InvalidBoxDimensionException(badValue); with the three double fields length, width, height, and a try/catch creation site try{ Box b=new Box(-2,5,3);} catch(InvalidBoxDimensionException e){...}. State that throw generates the throw inside the constructor while throws in the header forwards it when the constructor does not locally handle it, and that choosing RuntimeException as the parent makes the exception unchecked so throws is documentary rather than compiler-mandated.
26.9 Generics — Parameterized Types
26.9.1 The Idea of Generics
Hook: You want one Identity class that can hold a String today and a Long tomorrow — without copying the file twice, without casting, and without letting the compiler silently mix the wrong types. How can a single definition safely wear many type hats?
A generic — also called a parameterized type — lets a class or method work with a type supplied in brackets, written as ClassName<T>. The type parameter T is a type variable that is replaced throughout the class execution by whatever concrete type you pass. The syntax was shown as class Identity<T> { T obj; ... } and the type variable T must be followed consistently inside the class.
Intuition — stamp and color analogy: Think of T as a blank stamp shape and the angle-bracket argument as the ink color you press at each usage. The stamp body (class Identity) never changes, only the color (String or Long) you dip it in changes what prints. Once you stamp Identity<String>, every blank inside — field, constructor argument, return value — prints in String ink; stamp again as Identity<Long> and every blank now prints in Long ink. Where the analogy breaks: unlike ink, the type argument is checked at compile time — you cannot stamp String ink then later press a Long object through the same Identity<String> slot; the compiler blocks the mismatched color.
If you set T to String, then every T inside the class behaves like String: the field obj is a String, the constructor argument is a String, and the return type is a String. If on the next execution you set T to Long, then every T becomes Long: the field is a Long and so on. The class body does not change, only the substituted type changes.
Formalize — type-parameter mechanics and type safety promise: Declare a generic as class Identity<T> { T obj; Identity(T obj){ this.obj = obj; } T getObj(){ return obj; } }. T is a type variable ranging over reference types (String, Integer, Long, BankAccount, custom classes); it cannot be a primitive (int) without boxing to Integer. At each use site the argument replaces T uniformly: Identity<String> id2 = new Identity<String>("hello"); fixes T == String, so id2.obj has static type String, constructor expects String, and getObj() returns String. Identity<Long> id1 = new Identity<Long>(123L); fixes T == Long. After substitution, the compiler inserts casts internally but rejects mismatched assignments such as id1.obj = "hello" at compile time — no cast exception at runtime is needed to catch the mix-up. The angle-bracket list can contain multiple parameters (class Pair<T,U>) with independent substitution per parameter. Generics are erased at runtime (type erasure) but produce full compile-time checking; this is why arrays and generics interact with care — avoid creating new T[10] inside generic code.
A visual contrast helps: draw Identity<T> as a template box with three blanks T obj, Identity(T obj), T getObj(). Duplicate the box twice, coloring one blue labeled T=String with every T rewritten as String, and one orange labeled T=Long with every T rewritten as Long. An arrow from the template to each colored duplicate says "substitution at new time." Mark a red X on id1 = new Identity<Long>("hello") with the label "compile error — Long slot cannot accept String."
Scope and assumptions: The type variable name T is conventional for "type," K/V for key/value in maps, E for element in collections — naming follows domain, not language rule. Type inference lets Identity<String> id2 = new Identity<>("hello"); with <> (diamond) on the right, but the argument on the left still governs T. A raw usage Identity idRaw = new Identity("hello"); without <> compiles with a warning and disables generic checking, allowing heterogeneous misuse — avoid raw types in new code. Generics do not create new runtime types; Identity<String>.class and Identity<Long>.class are the same Class<Identity> at runtime due to erasure.
Pitfalls: Do not pass a primitive as a type argument — Identity<int> is illegal; use Identity<Integer>. Do not create new T() or new T[10] inside the generic body without reflection — T is erased, so new cannot infer the concrete array type. Do not mix type arguments after construction — Identity<String> s = new Identity<>("hi"); s.obj = 123; fails because s.obj is statically String. Do not rely on runtime instanceof T checks — erasure makes if (obj instanceof T) uncheckable; test if (obj instanceof String) after casting instead.
Real-world and domain connection: The Java collections framework depends on this very mechanism — ArrayList<String> versus ArrayList<Integer>, HashMap<K,V> versus Pair<T,U> — one library definition serves every type safely. In application code the same principle appears as Response<T>, Cache<K,V>, or Result<T,Error> where the surrounding logic (caching, retry) is reusable while the payload type changes per call.
Recap and bridge: A generic ClassName<T> declares a type type variable that each new ClassName<Concrete> replaces uniformly across fields and methods, turning one definition into many type-safe copies without duplication. The template idea then scales to one parameter (Identity<T>) and to two parameters where order also matters.
26.9.2 Single Type Parameter — Identity<T> Walk-Through
class Identity<T> {
T obj;
Identity(T obj) { this.obj = obj; }
T getObj() { return obj; }
}
Formalize — exact members and substitution per instantiation: Identity<T> has one field T obj, one constructor Identity(T obj) that stores the argument via this.obj = obj, and one accessor T getObj(){ return obj; }. For each distinct type argument, the compiler conceptually stamps a concrete copy: when T is Long, the stamped copy is Long obj; Identity(Long obj); Long getObj();; when T is String, the stamped copy is String obj; Identity(String obj); String getObj();. No bytecode duplication actually occurs due to erasure, but the static-type view is exactly that stamped copy, and the compiler enforces that only Long values flow through the Long stamped version and only String through the String version.
Usage:
Identity<Long> id1 = new Identity<Long>(123L); // T is Long, value 123L
System.out.println(id1.getObj()); // prints 123
Identity<String> id2 = new Identity<String>("hello"); // T is String
System.out.println(id2.getObj()); // prints hello
Worked trace — two type arguments through the same class body: Keep the Identity definition above. Case Long: write Identity<Long> id1 = new Identity<Long>(123L);. Substitution step — replace every T with Long: constructor becomes Identity(Long obj), field becomes Long obj, accessor becomes Long getObj(). Call site supplies 123L which has type Long — compatible, so construction stores id1.obj = 123L (boxed Long with primitive value 123). Call id1.getObj() — return type is now Long, the call returns Long 123L, and System.out.println prints 123 (bolded Long path answer). Attempt id1.obj = "hi" would be rejected — String is not Long. Case String: write Identity<String> id2 = new Identity<String>("hello");. Substitution replaces every T with String: field String obj, constructor Identity(String obj), accessor String getObj(). Call site supplies "hello" of type String — compatible. Store id2.obj = "hello". Call id2.getObj() returns String "hello", println emits hello (bolded String path answer). Shared pattern confirmed: the class body never changed — only the <> argument changed — and each getObj() call returned the exact type the variable promised, without a cast in user code. Sense-check: one definition handled a numeric family and a text family correctly because the type parameter acts like a compile-time macro that clones the class's type face.
Steps summary: for id1, the generic argument is Long, so T everywhere becomes Long, the stored object is a Long with value 123. Printing via getObj() shows the long value. For id2, T is String, the object is a String "hello", and printing shows the string. The same class handles both type families without duplication.
Visual: draw a table with rows Declaration, Field type, Constructor, Stored value, getObj() return, Prints. Fill row for id1: Identity<Long>, Long, Identity(Long), 123L, Long 123L, 123. Row for id2: Identity<String>, String, Identity(String), "hello", String "hello", hello. The alignment makes clear that the column shapes match across the two rows — only the concrete name differs.
Scope and nuance: Long versus long matters — the type argument must be the boxed Long, but the literal 123L auto-boxes neatly. Identity<T> cannot be instantiated as new T() inside its own body; only concrete instantiations like new Identity<String>(...) are valid. If inference is used, Identity<Long> id1 = new Identity<>(123L); infers <> == <Long> from the left side; writing new Identity(123) without any generic hint creates a raw type and loses checking.
Recap: Identity<T> is one blueprint whose single parameter T becomes Long for id1 and String for id2; every T inside the blueprint is replaced by the chosen concrete type, so field, constructor, and accessor all agree without user casts.
26.9.3 Multiple Type Parameters — T and U
A generic with multiple parameters — e.g., class Container<T, U> — takes two placeholders. In the example, one parameter is T treated as String and the other is U treated as Integer. Objects can then be created in either order.
class Pair<T, U> {
T first; U second;
Pair(T first, U second) { this.first = first; this.second = second; }
void print() { System.out.println(first + " " + second); }
}
Pair<String, Integer> i1 = new Pair<String, Integer>("hello", 10);
Pair<Integer, String> i2 = new Pair<Integer, String>(10, "hello");
i1.print(); // hello 10 — String then Integer for i1
i2.print(); // 10 hello — Integer then String for i2 (swapped)
Formalize — order-sensitive substitution across two independent blanks: Pair<T,U> declares T first; U second; Pair(T first, U second); void print(){ println(first + " " + second); }. Each instantiation fixes both parameters positionally: angle-bracket position 1 fixes T, position 2 fixes U, and the constructor call's argument positions must align. For Pair<String,Integer> i1, substitution is T==String, U==Integer, so the stamped copy becomes String first; Integer second; Pair(String first, Integer second);. Supplying ("hello", 10) matches String then Integer — legal. For Pair<Integer,String> i2, substitution is T==Integer, U==String, so stamped copy becomes Integer first; String second; Pair(Integer first, String second); and supplying (10, "hello") matches the swapped order. Swapping the type arguments without swapping the construction arguments is a compile error — new Pair<String,Integer>(10, "hello") would try to store Integer 10 into first declared String.
Explanation: for i1, T maps to String and U maps to Integer, so creation prints string first then integer. For i2, the arguments are reversed: T is Integer and U is String, so the printed order is integer then string. Both use the same generic definition but behave differently based on the order of type arguments passed.
Worked trace — printing order follows the type-argument order: Case i1: Pair<String,Integer> i1 = new Pair<String,Integer>("hello", 10);. After T==String, U==Integer, fields are first: "hello" (String) and second: 10 boxed to Integer. Call i1.print() executes System.out.println(first + " " + second) -> "hello" + " " + 10 -> hello 10 (bolded answer for i1). Case i2: Pair<Integer,String> i2 = new Pair<Integer,String>(10, "hello");. Substitution is T==Integer, U==String, fields are first: 10 (Integer) and second: "hello" (String). Call i2.print() concatenates 10 + " " + "hello" -> 10 hello (bolded swapped answer for i2). Both calls used the identical print() body from Pair<T,U> — only the concrete types swapped — yet the outputs are visibly ordered by how the type arguments were laid out. Sense-check: the two objects prove that order matters — Pair<A,B> and Pair<B,A> are distinct types and the compiler will not assign one to the other without a cast.
A second view reinforces the pattern: draw Pair<T,U> as a generic railcar with two compartments labeled T: first and U: second. Show railcar i1 with String:hello in the T compartment and Integer:10 in the U compartment. Show railcar i2 with Integer:10 in the T compartment and String:hello in the U compartment. The railcar shape is identical; only the color label on each compartment swapped, yet the order of compartments (first then second) stayed fixed — compartment position determines which logical argument sits where.
Scope and assumptions: Multiple type parameters can share bounds independently, e.g., class Cache<K extends Comparable<K>, V> — not needed here but common in real maps. The type variable names T and U follow convention for unrelated types; K/V would conventionally label pair-as-map-entry. Diamond inference applies: Pair<String,Integer> p = new Pair<>("hi", 10); infers <String,Integer> from the left. Two-parameter generics erase each T and U to Object unless bounded, so instanceof T remains illegal and array creation new T[5] remains illegal for each parameter individually.
Pitfalls: Do not swap type arguments but keep the same argument order — Pair<String,Integer> bad = new Pair<>("hello", 10) is fine, but Pair<Integer,String> also = new Pair<>("hello", 10) fails because first expects Integer. Do not assume Pair<String,Integer> is assignable to Pair<Integer,String> — the two are incompatible even though they store the same values in different positions. Do not use primitives in angle brackets — Pair<int,String> is illegal; use Pair<Integer,String>.
Real-world: collections such as ArrayList<T> and HashMap<K,V> are generics. HashMap<String,Integer> mirrors Pair<String,Integer> for the key-value sense, while HashMap<Integer,String> mirrors the swapped form. Parameterized methods and parameterized types throughout the library work on the same substitution principle, and the two-parameter Pair pattern recurs as Map.Entry<K,V>, BiFunction<T,U,R>, or Result<Value,Error>.
Recap and exam bridge: Pair<T,U> handles one-parameter identity's idea extended to two placeholders where type-argument order defines which compartment is String and which is Integer; Pair<String,Integer>("hello",10) prints hello 10 while Pair<Integer,String>(10,"hello") prints 10 hello — same body, swapped stamps, swapped output. The type substitution habit learned here carries directly to the collections framework's Collection<T> family next.
26.10 Collections Framework
26.10.1 Array versus List and the Need for a Uniform Interface
Hook: An array stores ten numbers in one solid block; a linked list scatters the same ten numbers across the heap and connects them with pointers. The goal is identical — hold a group — yet the code to add a new element to an array looks nothing like the code to splice a node into a list. Must every data structure force you to learn a separate language for the same choice?
Different data structures — ways of organizing a group of data — store the same values differently. An array stores elements in consecutive blocks of memory for the same data type, giving fast index access but fixed size. A list (linked variant) stores elements at scattered memory locations connected through links, growing flexibly but with different cost to reach an element. Other structures include map, tree, set, but arrays and lists illustrate the split.
Intuition — shelf versus scattered treasure hunt: Think of an array as a numbered shelf with ten adjacent slots — you jump to slot 7 instantly by counting, but the shelf cannot magically sprout an eleventh slot. A linked list is like ten treasure chests hidden around a field where chest 1 contains a map to chest 2, chest 2 maps to chest 3, and so on — you cannot reach chest 7 without opening the chain from chest 1, but you can bury a new chest anywhere by rewriting one map. Where the analogy breaks: unlike chests, linked nodes are objects with typed next pointers, not parchment maps; the array shelf also stores references to objects, not raw values, when the element type is an object.
The objective of both array and list is the same — hold a group of data — but the storage mechanism differs. Without a common interface, code that adds to an array would look different from code that adds to a linked list. The collections framework provides a single, uniform way: call an add method and pass the element, no matter whether the underlying store is an array or a linked list. That convenience avoids handling implementation details of each structure.
Formalize — storage contract versus interface contract: Array contract: elements occupy a single allocation base + i * elementSize so random access by index is and append beyond capacity requires reallocating a larger block. List (linked) contract: each node stores (element, nextRef) at an arbitrary address, traversal from head follows head.next.next... so reaching position costs while splicing at a known node costs after locating the predecessor. The collections framework lifts the shared intent "work with a group of elements" into interfaces: Collection<T> promises add(T e), List<T> adds index operations, Set<T> promises uniqueness, Map<K,V> maps keys to values. An ArrayList implements List using a dynamic array under the hood; a LinkedList implements List using a doubly linked chain under the hood — the same add(T e) call compiles for either because both satisfy Collection.add/List.add, and the implementation-specific array shifting or pointer rewiring happens invisibly beneath the interface.
Visual: draw two memory diagrams side by side. Left: a horizontal row of five adjacent boxes labeled al[0]..al[4] with an arrow add(5) needing to grow the row if full. Right: five scattered boxes Node1..Node5 each with a field next arrow hopping to the next box, with add("C") splicing by rewiring the tail pointer. Below both, draw a thin horizontal bar labeled Collection<T> — add(T e) that spans across the two diagrams, emphasizing the single call that covers both representations.
Scope and assumptions: The consecutive-versus-scattered distinction applies to the backing storage, not the element identity — both an ArrayList<String> and a LinkedList<String> store String references as elements. The add uniform interface assumes the collection's capacity rules: a bounded collection (rare) could refuse add, while ArrayList resizes automatically. The cost discussion assumes the usual implementations: resizable ArrayList with geometric growth amortizes append to , while LinkedList traversal remains linear even though node insertion itself is cheap.
Pitfalls: Do not assume arrays and lists have the same growth behaviour — int[] a = new int[5]; a.add(10); does not compile because plain arrays have fixed length and no add method; only ArrayList grows. Do not equate List with ArrayList — List is the interface, ArrayList and LinkedList are its two concrete strategies. Do not judge by storage alone when choosing — also compare whether you need frequent random access (ArrayList) versus frequent insertion or removal in the middle (LinkedList).
Real-world and domain connection: The same uniformity carries beyond arrays and lists to Set, Map, Queue, and Deque. Calling add, remove, contains, or isEmpty on a HashSet, TreeSet, or PriorityQueue uses the same vocabulary even though a hash table, a red-black tree, and a heap back them respectively. That is the framework's promise: learn one vocabulary, apply it to every aggregate, and swap underlying storage without rewriting calling code.
Recap and bridge: Arrays keep elements consecutively, lists scatter them with links, but the collections framework hides both strategies behind one add-style vocabulary so callers program to the uniform Collection interface rather than to array versus linked details. The next subsection names those uniform operations.
26.10.2 Collection Interface and Core Methods
Hook: If every aggregate promised exactly the same handful of verbs — add, contains, clear, isEmpty, size — the caller would never need to ask what structure hides underneath. What are those verbs, and which generic interface encloses them?
A collection — declared as the generic interface Collection<T> — specifies the type of objects the collection will hold, analogous to the Identity<T> generics seen before. It declares core methods that every collection supports. General methods include boolean add(T e), boolean addAll(Collection<? extends T> c), void clear(), boolean contains(Object o), boolean isEmpty(), and int size(), plus many more. These are the shared methods available whether the actual store is an array, list, map, tree, or list iterator.
Formalize — Collection<T> as a parameterized contract and its core method signatures: interface Collection<T> { boolean add(T e); boolean addAll(Collection<? extends T> c); void clear(); boolean contains(Object o); boolean isEmpty(); int size(); Iterator<T> iterator(); boolean remove(Object o); Object[] toArray(); } — the type parameter T is the element type, matching the generics idea that Collection<String> holds strings while Collection<Integer> holds integers. boolean add(T e) returns true when the collection changed (relevant for sets that reject duplicates). boolean addAll(Collection<? extends T> c) bulk-adds another collection. void clear() empties the collection; boolean contains(Object o) tests presence via equals; boolean isEmpty() is shorthand for size()==0; int size() counts elements. List<T> extends Collection<T> adding index operations T get(int), add(int,E), remove(int); Set<T> extends Collection<T> adding uniqueness; Map<K,V> is a related but separate parameterized interface with its own put, get, containsKey. A class ArrayList<T> implements List<T> and LinkedList<T> implements List<T> both inherit Collection.add through List, which is why the identical call compiles on either.
Related views are the Collection interface itself that enables working with a group of objects, and the List, Set, and Map specializations that refine the contract. Draw the interface diamond: Collection<T> at the top, List<T> and Set<T> beneath it, Map<K,V> beside them as a cousin, with ArrayList and LinkedList beneath List and HashSet beneath Set. Label the central bar add(T e), clear(), contains(Object), isEmpty(), size() to show the shared core, then annotate the outer branches with their refinements (get(index) for List, containsKey for Map).
Scope and assumptions: The Collection API assumes reference-typed elements; primitive int must be boxed to Integer for Collection<Integer>. Wildcard Collection<? extends T> in addAll allows a Collection<Integer> to be added to a Collection<Number> — use extends for producers, super for consumers. Map<K,V> is not a Collection — it does not extends Collection — but it is part of the same framework and exposes a Collection<V> view via values(). Iterators (Iterator<T> iterator()) are the framework's traversal abstraction and work uniformly across array-backed and linked-backed collections.
Pitfalls: Do not box yourself with raw types — Collection c = new ArrayList(); without <T> silences generic checking and allows accidental mixing like adding an Integer and a String into the same logical group; prefer Collection<String>. Do not confuse contains(Object) with contains by identity — contains uses equals, so object equality must be correctly overridden. Do not call add(T e) expecting it to insert at a specific position on a Collection — ordered insertion at index is List.add(int, T), while Set.add has no index concept.
Real-world and domain connection: The uniform core is what lets library code accept Collection<BankAccount> accounts and process any concrete shape — list, set, or queue — without knowing which backing structure the caller chose. Utility code such as Collections.sort(List<T>) or Collections.binarySearch operates through the interface; switching from ArrayList to LinkedList happens at construction, and the rest of the calls stay identical. The next two examples make that sameness concrete with real add calls.
Recap: Collection<T> is a generic parameterized interface whose type argument T fixes the element type and whose core methods (add, addAll, clear, contains, isEmpty, size) are inherited by List, Set, and indirectly by Map's view, giving one vocabulary for every aggregate.
26.10.3 Worked Example — ArrayList (Unparameterized)
import java.util.ArrayList;
class ArrayListTest {
public static void main(String[] args) {
ArrayList al = new ArrayList(); // raw, no type parameter
al.add(10); // Integer 10
al.add("hello"); // String
al.add(3.14f); // Float
al.add(true); // Boolean
System.out.println(al);
}
}
Worked trace — raw ArrayList accepts heterogeneous elements (demonstration-only anti-pattern): Line ArrayList al = new ArrayList(); declares a raw type with no <T> — compile-time checking for homogeneous types is disabled and an unchecked warning is emitted, yet the class still functions. Call al.add(10) — 10 boxes to Integer 10; backing array stores a reference to that Integer at index 0, size becomes 1. Call al.add("hello") — a String object at index 1, size 2. Call al.add(3.14f) — a Float 3.14f at index 2, size 3. Call al.add(true) — a Boolean true at index 3, size 4. At no step does the compiler enforce that all adds share a type — that is the cost of omitting <T>. Call System.out.println(al) — ArrayList.toString() iterates indices 0..3 and appends each element's toString(), yielding printed form [10, hello, 3.14, true] (bolded). The overloaded al.add(index, element) can insert at a specific position, e.g., al.add(1, "mid") would shift "hello", 3.14f, true one slot right and place "mid" at index 1. Important note the lecture repeats: you never manually manage the backing array growth or shift; you simply call add and the framework handles capacity (geometric resizing) and element movement internally. Sense-check: the lecture retains the raw form ArrayList without <> to demonstrate that the same add call works across variegated payloads, but real code should parameterize as ArrayList<String> or ArrayList<Integer> to prevent such mixing — the framework permits heterogeneous add only when you discarding its type parameter.
Steps summary: create al as a raw ArrayList from java.util with no type parameter. Without a parameter, the list accepts heterogeneous elements: integer 10, string "hello", float 3.14f, boolean true together in the same list, even though such mixing is rarely desired in production. Printing the list shows all four. To add at a specific position, the overloaded add(index, element) can be used. You do not adjust array internals; you simply call add.
Visual: draw the backing Object[] inside ArrayList as four adjacent slots at scattered conceptual addresses but stored contiguously in the backing array, each slot holding a reference arrow to a different object bubble — Integer, String, Float, Boolean. Label the vertical size counter size=4. Below, draw a LinkedList node chain that would look scattered and uses next arrows instead, reinforcing that al.add needs no caller-side array management in either implementation.
Scope and nuance: Raw ArrayList is kept for historical compatibility; new code should write ArrayList<Integer> or ArrayList<String>. The import java.util.ArrayList; is required; without it ArrayList is unresolved. The element 10 auto-boxes to Integer, 3.14f to Float, true to Boolean — the backing store is Object[], so each slot holds a reference, not a primitive. The add(E) for ArrayList is amortized while add(index, E) shifts suffix elements right and costs — the uniform call hides differing costs.
Recap: Unparameterized ArrayList al = new ArrayList() lets repeated al.add(...) store mixed types Integer/String/Float/Boolean as [10, hello, 3.14, true] without reallocation logic in calling code — at the price of losing compile-time type safety, which the parameterized counterpart restores.
26.10.4 Worked Example — LinkedList<String> (Parameterized)
import java.util.LinkedList;
class LinkedListTest {
public static void main(String[] args) {
LinkedList<String> l1 = new LinkedList<String>();
l1.add("F");
l1.add("B");
l1.add("C"); // additional adds
System.out.println(l1); // prints [F, B, C] order of insertion
}
}
Worked trace — parameterized LinkedList<String> restricts to one type and still uses the same add: Line LinkedList<String> l1 = new LinkedList<String>(); fixes T == String for this list, enabling compile-time rejection of l1.add(10) (type mismatch). Call l1.add("F"): LinkedList appends a node containing String "F" at the tail by null-initial head/tail handling and rewiring tail.next; size becomes 1. Call l1.add("B"): appends node String "B" after F by pointer update; size 2. Call l1.add("C"): appends node String "C"; size 3. The linked structure on the heap is Node(F) -> Node(B) -> Node(C) at scattered addresses connected via next references, not a contiguous array. Call System.out.println(l1) iterates via a list iterator hopping next pointers in insertion order, collects toString() of each element, and prints [F, B, C] order of insertion (bolded). Removing the <String> would revert to raw heterogeneous behaviour where l1.add(10) would compile by erasure; adding <String> restricts to strings only and the same add("F") call that previously needed no type awareness now carries a compile-time guarantee. The crucial point preserved from the lecture holds: the background handling of array storage versus linked storage is hidden; the caller uses the same add method in both examples. Overloaded add(int index, E e) on LinkedList walks from the nearer end to locate predecessor, then splices the new node with pointer rewiring rather than shifting a contiguous block. Sense-check: iterating either ArrayList or LinkedList with the same loop and calling add with the same verb produced the same observable insertion-order sequence — the framework delivered the uniform vocabulary promise while scattering the heap layout differently.
Steps summary: create l1 as LinkedList<String> — now the parameterized type is constrained to String. Calls to add("F") then add("B") insert at the end by following links rather than shifting a contiguous block. The linked version stores elements at scattered locations linked together (elements 1, 2, 3 connected via links). Printing l1 shows [F, B, C]. The crucial point: the background handling of array storage versus linked storage is hidden; the caller uses the same add method in both examples. Removing the type parameter reverts to raw heterogeneous behaviour; adding <String> restricts to strings only.
Visual comparison: keep the left panel's ArrayList with its contiguous backing array, and the right panel's LinkedList as scattered nodes with next arrows. Put a single horizontal banner labeled Collection.add(T e) across both panels, with identical l1.add("F") text above each panel, showing verbatim call text on divergent internals and identical printed result order [F, B, C] beneath.
Scope and nuance: LinkedList implements both List<String> and Queue<String>/Deque<String>, so beyond add(T) it also offers addFirst, addLast, offer, poll for queue semantics; the uniform Collection.add is one of its many aliases. Traversing LinkedList by get(index) inside a loop costs because each get re-walks; prefer an iterator or for-each to keep per-element cost . Raw versus parameterized is a compile-time distinction — at runtime both lists hold Object references due to erasure, but only the parameterized form delivers early compilation errors for accidental mixing.
In industry, the same uniform methods extend to adding elements to a tree, set, or map. Collections provide the standard interface so developers do not rewrite insertion logic per structure. In practice a producer method declared as void fill(Collection<String> c) can be called with either new ArrayList<String>() or new LinkedList<String>() without change — the receiving side only calls c.add(...).
Recap and bridge: LinkedList<String> l1 with l1.add("F"), l1.add("B"), l1.add("C") produced [F, B, C] via scattered nodes and next pointers, yet the caller typed the same add used for the array-backed list — the single interface covered both backing strategies. With uniform storage handled, the lecture shifts from static structures to concurrent execution and the threading mechanisms that share these collections.
26.11 Multithreading — Thread Creation and Life Cycle
26.11.1 What a Thread Is
Hook: A single running program P1 needs to download a file, play music, and respond to your click — all at once, on shared data. Must you start three separate programs, or can one program host three cooperating paths inside itself?
A process — a running program such as P1 — can contain several threads — lightweight paths of execution, for example T1, T2, T3 belonging to P1 — that work on shared data. Multithreading — having multiple threads within a single process — is the technique where those threads cooperate on the same data held by P1. In the first two demonstrations, only a single thread existed; now multiple threads share common data. The process provides the shared address space (heap, statics); each thread provides its own stack, program counter, and scheduling state.
A thread in Java is built in two ways: (1) by extending the Thread class, (2) by implementing the Runnable interface. Thread is a predefined class. Runnable is an interface. Both approaches funnel through the same entry point that the threading system actually invokes.
A central method appears in both approaches: public void run(). Whatever line of action you want a thread to execute must be written inside run(). That method keeps the thread active; when the code inside run() is entered, the thread has work to do. Never call run() directly expecting a new thread — call start() instead, as explained below.
Formalize — two construction forms that converge on run(): Form 1 — extend Thread: class ABC extends Thread { public void run(){ System.out.println("thread is running"); } } then ABC obj = new ABC(); obj.start(); — start() is inherited from Thread and triggers the JVM to spawn a new call stack that enters ABC.run(). Form 2 — implement Runnable: class ABC implements Runnable { public void run(){ System.out.println("thread is running"); } } then ABC a = new ABC(); Thread t = new Thread(a); t.start(); — the Runnable object a is wrapped in a real Thread t; t.start() again enters ABC.run() on a new stack. The signature public void run() is mandated by Runnable.run() and by Thread's override; a method named run with a different signature (e.g., int run()) would not be recognized as the thread entry. The call t.start() vs t.run() distinction is structural: start() asks the OS to create a second stream of execution, run() runs in the caller thread without creating concurrency.
Visual: draw P1 as a large rounded box containing three smaller threads T1, T2, T3 each with its own tiny stack icon, plus a shared heap region labeled "shared data." Draw two construction branches to the left: branch A shows class ABC extends Thread with an arrow obj.start() -> ABC.run(); branch B shows class ABC implements Runnable plus new Thread(a) wrapping, then t.start() -> ABC.run(). Both branches meet at a single public void run() box to stress convergence.
Scope and assumptions: Threads inside one process share heap objects, static fields, and open files, but have private run() stacks — local variables inside run() are not shared while this-fields pointing into the shared heap are. Extending Thread ties the class to being a thread exclusively; implementing Runnable keeps the class free to extend another base class (important because Java allows only one extends). Thread itself implements Runnable, which is why new Thread(runnable) accepts the object. The Runnable interface has exactly one abstract method, making it compatible with lambdas: Thread t = new Thread(() -> System.out.println("run")); is a modern shorthand.
Pitfalls: Do not confuse Thread the class with Runnable the interface — class ABC extends Thread uses extends, class ABC implements Runnable uses implements. Do not write obj.run() expecting a new thread — run() executes synchronously in the calling thread without parallelism; only start() creates a new thread. Do not give run() a non-public or non-void signature — protected void run() will not correctly override the interface method and the compiler may not flag the silent mismatch without @Override. Do not assume a finished thread can be restarted — start() on a terminated thread throws IllegalThreadStateException.
Real-world and domain connection: Extending Thread appears in simple demos, while implementing Runnable (or submitting to an ExecutorService) dominates production code because it separates the task definition from the threading machinery. Web servers use one Runnable per request sharing the same Cache object, media pipelines use parallel threads sharing a frame buffer, and batch jobs use thread pools sharing a queue — all are the P1 containing T1,T2,T3 working on shared data the lecture names.
Recap and bridge: A process P1 hosts threads T1,T2,T3; a Java thread is made either by extending Thread or implementing Runnable, and both routes hand control to the single method public void run() after start() is invoked. With construction established, the next question is how those threads live, pause, and end.
26.11.2 Thread States
Intuition — daily schedule analogy: Imagine a worker who is hired (New), given a desk when the boss has space (Running), steps away to nap or wait for a delivery (Waiting), and clocks out when the day's task list is done (Terminated). A Java thread's four states mirror that daily schedule exactly. Where the analogy breaks: unlike a human day, a terminated Java thread can never be rehired — its object remains but its execution never resumes.
Threads move through four states:
- New state — a thread object has been created but not yet started.
- Running state — after
start()is invoked, if the processor is free the thread is allocated CPU and executes its task; while running, it performs the code inrun(). - Waiting state — the thread pauses for a reason, for example purposely sleeping via
sleep(milliseconds)or waiting for input/output. When the sleep timer expires or the I/O completes, the thread returns to running. - Terminated state — also called dead. When the task finishes and the closing curly bracket of
public void run()is reached, after all tasks insiderun()have executed, the thread moves to terminated. A terminated thread cannot be restarted.
Transitions: new Thread() creates new state. thread.start() attempts to move to running if CPU is free. Thread.sleep(1000) moves to waiting for 1000 milliseconds. Completion of run() moves to terminated.
Formalize — state names, triggers, and the underlying Java states: The lecture's four-state model is a pedagogical simplification of the seven values of Thread.State. Mapping: New corresponds to Thread.State.NEW (thread object created, start() not yet called). Running conflates RUNNABLE (eligible to run, start() called, may be on CPU or ready queue) and RUNNING as execution of run(). Waiting covers BLOCKED, WAITING, TIMED_WAITING — entered by Thread.sleep(millis), Object.wait(), join(), or I/O wait — and recovers upon timeout, notification, or I/O completion. Terminated maps to TERMINATED (also called dead) — entered when run() falls off its final } or when the thread throws an uncaught throwable outward. Transitions are: NEW --start()--> RUNNABLE/RUNNING --sleep()/io--> WAITING --timeout/completion--> RUNNABLE ... --run() exits--> TERMINATED. Calling start() on a TERMINATED or RUNNABLE thread again throws IllegalThreadStateException — a terminated thread is dead, not paused.
Visual: draw a state diamond. Top node New with arrow new Thread() arriving. Down-right arrow labeled thread.start() (if CPU free) to node Running with small run(){...} inset and a self-loop labeled "executing." Down-left arrow from Running to Waiting labeled sleep(millis) / wait() / I/O, with return arrow back labeled sleep expires / notify() / I/O done. Final downward arrow from Running to Terminated (Dead) labeled } end of run(). Cross out a dashed arrow attempting to return from Terminated to New with label "illegal — cannot restart."
Scope and assumptions: The "if the processor is free" caveat acknowledges the scheduler — start() does not guarantee immediate CPU; it makes the thread eligible, and the OS chooses the interleaving. sleep always pauses the currently running thread, is a static method called as Thread.sleep(1000), and throws InterruptedException as a checked signal that another thread interrupt()ed the sleeper. The lecture's Waiting lumps TIMED_WAITING (sleep with timeout) and indefinite WAITING (no timeout) — both are pausing but differ on how they wake. Terminated is irreversible; reachability of the Thread object as a regular Java object remains, but its execution context cannot be resurrected.
Pitfalls: Do not call start() twice on the same Thread object — the second call fails with IllegalThreadStateException and does not resume from Terminated. Do not confuse Thread.sleep(1000) as instance sleep on a named variable t.sleep() — it always sleeps the calling thread, not the receiver. Do not assume a thread in Running cannot be preempted — the scheduler may time-slice without entering Waiting. Do not test thread state by polling with tight loops — use join(), latches, or executor completion instead of busy waiting.
Real-world and domain connection: Health checks and monitoring panels report Thread.State such as TIMED_WAITING while a pool thread sleeps between jobs; thread-pool frameworks recycle Runnable tasks across a fixed set of long-lived threads rather than restarting dead threads. The promised flow "control goes back" after run() is the observable dispatch loop: a completed run() signals completion, returns the thread to the pool or to TERMINATED, and lets dependent consumers proceed.
Recap: new Thread() -> New, thread.start() -> Running (executing run()), sleep(millis) or I/O -> Waiting -> back to Running when the reason ends, final } of run() -> Terminated with no restart allowed.
26.11.3 Worked Example — Extending Thread
class ABC extends Thread {
public void run() {
System.out.println("thread is running");
}
}
public class Test {
public static void main(String[] args) {
ABC obj = new ABC(); // ABC is-a Thread, thread has no separate name beyond object reference
obj.start(); // calls Thread.start(), which in turn causes run() to execute
}
}
Worked trace — creation, start, and run on the new call stack: Step 1 — definition: class ABC extends Thread inherits the field set and methods of Thread, including native start(), and must override public void run() — here printing thread is running. Without the Thread parent, obj.start() would not exist. Step 2 — construction: ABC obj = new ABC(); allocates a Thread-derived object in heap, state is New (Thread.State.NEW); no new call stack exists yet, only the main thread is running. Step 3 — start: obj.start(); is a native invocation inherited from Thread. The JVM verifies the thread is unused (NEW), marks it RUNNABLE, asks the OS to schedule a second stack, and returns immediately without waiting for the new thread to finish. The printing may therefore interleave with prints that follow start() in main. Step 4 — run entry: the new stack eventually reaches its entry trampoline which invokes ABC.run() on the obj receiver; line System.out.println("thread is running") executes on the second stack and prints bolded output thread is running. Step 5 — termination: run() falls off its closing brace, the thread's state becomes Terminated, and it cannot be start()ed again — a second obj.start() would throw IllegalThreadStateException. The thread is referenced only via the ABC object; no separate string name was assigned, so debugging will show default name like Thread-0. Sense-check: calling obj.run() directly would print the same line but on the main thread without concurrency — start() is the only path that purchases parallelism; run() alone is just a normal method call.
Visual supplement: draw two vertical stacks side by side. Left stack is main thread calling ABC obj = new ABC() then obj.start(). Right stack appears at the start() call-out as a new column labeled ABC thread entering run(). Shade the moment start() returns in main while run() continues independently in the new column — illustrating that start() does not block.
Scope and nuance: Extending Thread locks the inheritance chain — ABC can no longer extend another application class. Overriding run() should keep the public void run() signature exactly; adding @Override annotation lets the compiler flag typos. Passing no name to the Thread means getName() returns an auto-generated Thread-N — use new Thread(null, runnable, "myName") or extends Thread(String name) to name threads after their role. The Thread subclass object itself lives at a distinct heap address, separate from the call-stack that runs it.
Recap: ABC extends Thread must define public void run(); new ABC() yields a New thread object; obj.start() (inherited) spawns a second stack that enters run() and prints thread is running, after which the thread dies and cannot be restarted.
26.11.4 Worked Example — Implementing Runnable
class ABC implements Runnable {
public void run() {
System.out.println("thread is running");
}
}
public class Test {
public static void main(String[] args) {
ABC a = new ABC();
Thread t = new Thread(a); // wrap runnable in a Thread
t.start(); // start() from Thread, control goes to ABC.run()
}
}
Worked trace — wrapping a task and delegating to it: Step 1 — definition: class ABC implements Runnable promises exactly one method public void run(); being Runnable does not by itself create any thread or any start() method — ABC alone cannot be start()ed. Step 2 — task instantiation: ABC a = new ABC(); allocates a plain object whose run() prints thread is running; state is not yet tracked as a Thread. Step 3 — wrapping: Thread t = new Thread(a); constructs a real Thread object backed by the Runnable target a. Inside Thread, the field target stores the Runnable reference; t is now in New state. Step 4 — start: t.start(); is the actual thread-creation operation owned by Thread, identical to the previous form. The new stack is scheduled, t becomes RUNNABLE, and the new trampoline invokes t's internal run() which delegates as if (target != null) target.run(); — thereby executing ABC.run() on the new stack. Output is again bolded thread is running. Step 5 — post-run: the wrapped thread t terminates just like the subclass case; the Runnable object a remains as a reusable task object, and could be wrapped in multiple threads (new Thread(a) again) because the task is separate from the thread that runs it. Sense-check: if you mistakenly call a.run() directly, the print occurs on the main thread without parallelism; if you mistakenly call t.run() directly, that too runs on main. Only t.start() on the Thread wrapper creates the second stream that eventually calls ABC.run().
Steps summary: here ABC implements Runnable via implements, so it must define public void run(). To start a thread, you wrap the runnable object a inside a Thread object t = new Thread(a). Calling t.start() again delegates to run() in ABC. The printed line is the same. Both approaches — extending Thread versus implementing Runnable — eventually funnel through public void run() as the active entry point.
A comparative table cements the dual forms: column 1 header Extend Thread, column 2 header Implement Runnable. Row Declaration: class ABC extends Thread versus class ABC implements Runnable. Row Has start()?: yes, inherited versus no — wrap in new Thread(a). Row Create: ABC obj = new ABC() versus ABC a = new ABC(); Thread t = new Thread(a). Row Launch: obj.start() -> ABC.run() versus t.start() -> ABC.run(). Row Can also extend another class?: no versus yes. Both rows end with the same printed line thread is running.
Scope and assumptions: The new Thread(Runnable) constructor stores the target without starting it — construction and scheduling are distinct, as the lecture separates new Thread(this) from t.start(). Lambda shorthand Thread t = new Thread(() -> System.out.println("...")); is exactly this pattern with an inline Runnable. The phrase "taking this runnable via this operator" in the slides refers to the pattern t = new Thread(this) inside a constructor when the enclosing class itself implements Runnable — this is the receiver-as-Runnable.
Recap and exam bridge: Exam note: know both creation idioms — extend Thread versus implement Runnable plus wrapping new Thread(runnable) — know why start() is called instead of directly calling run() (only start() creates a new thread; direct run() runs in the caller thread without concurrency), and know how wrapping a Runnable with new Thread(this) and start() leads to run() on the new stack, printing thread is running as the shared outcome.
26.12 Thread Priorities and First Multi-Thread Example
26.12.1 Priority Values
Hook: When two threads compete for the same CPU — one painting the screen you see and another syncing files in the background — which one should the scheduler favor, and how do you whisper that preference in code?
Every thread has a priority — an integer between 1 and 10 that hints to the scheduler which thread should run first. The default priority is 5. Moving toward 10 means higher priority, first chance to execute compared with a low priority near 1. A greater number represents a higher priority in the Java multithreading environment.
Formalize — range, constants, default, and hint semantics: Java defines the range as Thread.MIN_PRIORITY = 1, Thread.NORM_PRIORITY = 5, Thread.MAX_PRIORITY = 10. A newly created Thread inherits the priority of the thread that created it, which by default is NORM_PRIORITY 5 for the main thread and therefore for most children unless explicitly changed. Access is via instance methods int getPriority() and void setPriority(int newPriority). Supplying a value below 1 or above 10 throws IllegalArgumentException. Crucially, priority is a scheduling hint, not a guarantee — the OS scheduler may honor it strictly, may time-slice regardless, or may ignore it (some platforms map the ten Java levels to fewer OS levels). So priority ordering influences probability of being scheduled sooner, not a contract about exact execution order.
Methods getPriority() and setPriority(int newPriority) read and adjust the value. Example adjustment as shown: for a thread someT, reduce priority by 2 from default 5 to 3 using code similar to someT.setPriority(someT.getPriority() - 2);. For another thread factT, increase by 2 from 5 to 7 using factT.setPriority(factT.getPriority() + 2);. After adjustment, factT with priority 7 is favoured over someT with priority 3.
Worked numeric trace — the two arithmetic adjustments: Assume both threads initially inherit default 5 from the creating thread. For someT: evaluate someT.getPriority() gives 5; compute 5 - 2 = 3; execute someT.setPriority(3) — bolded result someT priority = 3 (below normal, background-leaning). For factT: evaluate factT.getPriority() gives 5; compute 5 + 2 = 7; execute factT.setPriority(7) — bolded result factT priority = 7 (above normal, foreground-leaning). Comparison: 7 > 3, so the scheduler's hint says factT should be preferred over someT when both are eligible. The codes also show method pairing: getPriority() reads the hint, arithmetic computes the new level, setPriority installs it. If either call used setPriority(11) the JVM would throw IllegalArgumentException: priority out of range. Sense-check: a higher integer always means higher priority, but the absolute ordering (10 first, 1 last) is a policy hint, not a latch that locks the lower thread out — starvation is still platform-dependent.
Visual: draw a horizontal bar from 1 to 10. Mark ticks at 1 MIN, 5 NORM (default), 10 MAX. Place someT dot at 3 with an arrow rightward labeled -2 from 5, and factT dot at 7 with arrow rightward labeled +2 from 5. Shade the region between 3 and 7 showing 7 > 3 favored. Add a caution icon with caption "hint, not lock — scheduler may reorder."
Scope and assumptions: Priority values are integers, not enumerated names; use the constants for clarity. Adjusting priority after start() is visible to the scheduler promptly, but the adjustment is not retroactive to work already completed. Priority cannot rescue a logically blocking join — a thread at priority 10 still waits on join() or sleep exit. Lower-priority threads are not starved by contract — a conforming JVM may still schedule priority 3 alongside priority 7 albeit less frequently.
Pitfalls: Do not write someT.priority = 3 as direct field access — the field is encapsulated; only setPriority(int) may change it with bounds checking. Do not expect setPriority(10) to make a thread run instantly regardless of CPU count — priority influences eligibility, not possession; concurrent hardware and scheduler quantum still decide. Do not assume priority persists across new Thread(runnable) without inheritance logic — explicitly set it on each new thread if you need a non-default level.
Real-world and domain connection: Priority hints are used in servers and interactive systems where responsiveness matters more than background work — the UI/event-dispatch thread may hover near 7-8 while file-indexing or sync threads linger at 3-4. Media servers elevate audio threads to reduce dropouts, while game loops demote asset-loading workers. The win is latency for the high-priority path, not throughput; excessive priority gaming can starve low-priority housekeeping and is mitigated by executors with fair queues rather than raw priority tweaks.
Recap: Thread priorities are integers 1..10 with 5 default, read by getPriority() and written by setPriority(int); 5-2=3 makes someT low, 5+2=7 makes factT high, and larger number hints the scheduler to favor execution, without a hard ordering guarantee.
26.12.2 Worked Example — Sum and Factorial Threads
class SumThread implements Runnable {
public void run() {
int sum = 0;
for (int i = 1; i <= 5; i++) sum += i;
System.out.println("sum = " + sum); // printed one by one during execution
// control returns to caller after loop finishes
}
}
class FactThread implements Runnable {
public void run() {
int fact = 1;
for (int i = 1; i <= 5; i++) fact *= i;
System.out.println("fact = " + fact);
}
}
public class Test {
public static void main(String[] args) {
Thread someT = new Thread(new SumThread());
Thread factT = new Thread(new FactThread());
someT.setPriority(someT.getPriority() - 2); // 5 -> 3
factT.setPriority(factT.getPriority() + 2); // 5 -> 7
someT.start();
factT.start();
}
}
Worked trace — both runnables compute, then priority decides hint and interleaving remains OS-chosen: Step 1 — define SumThread: run() loops i=1..5, accumulating sum. Iteration trace: i=1 sum=1, i=2 sum=3, i=3 sum=6, i=4 sum=10, i=5 sum=15 — prints bolded sum = 15. Step 2 — define FactThread: loop i=1..5 factorial fact. Trace: i=1 fact=1, i=2 fact=2, i=3 fact=6, i=4 fact=24, i=5 fact=120 — prints bolded fact = 120. The phrase "control returns to caller after loop finishes" means each run() falls off its block and the thread moves to Terminated without an explicit return; intermediate prints if any were inside the loop would emit one by one, but here the final println emits once after the loop. Step 3 — wiring: Thread someT = new Thread(new SumThread()); Thread factT = new Thread(new FactThread()); creates two New threads. Step 4 — priority tuning: someT.setPriority(5-2) installs 3; factT.setPriority(5+2) installs 7. Step 5 — scheduling: someT.start(); factT.start(); marks both eligible. The scheduler sees factT at 7 and someT at 3 and is more likely to schedule factT first, yet exact interleaving is OS-determined — possible orders include fact=120 before sum=15, the reverse, or even interleaved interior prints if loops printed per iteration. Neither start() blocks; main does not join, so it may exit before either worker prints unless the workers are non-daemon joins or a sleep keeps main alive in demos. Sense-check: priority did not change arithmetic results — 15 and 120 are deterministic — only hinted which thread the CPU visits first.
Walk-through reiterated: two runnable classes SumThread (computes sum 1+2+...+n) and FactThread (computes factorial). Threads someT and factT wrap them. Priorities are set as above. Starting both lets the scheduler decide order, but factT at 7 is more likely to run before someT at 3, although the exact interleaving remains up to the operating system. The run() bodies show the pattern "what is written in public void run, like sum should be printed one by one, and when execution is done, control goes back" — the loop prints stepwise, then the thread terminates after reaching the end of run().
A timeline diagram makes interleaving visual: horizontal time axis, two swim lanes someT (priority 3) and factT (priority 7). Mark launch points start() with vertical dashed lines, then shade CPU usage bars where each thread occupies the CPU, with factT's bar slightly ahead but with a footnote "order influenced by priority, decided by OS — bars may swap." Annotate the compute phase loop accumulates feeding into a final println(sum=15) or println(fact=120) box, and a terminal dot labeled Terminated.
Scope and nuance: The loops 1..5 are illustrative; real tasks would use n from input, while the thread-wrapping and priority pattern stay identical. sum and fact are local to each run() and therefore not shared — no synchronization is needed here, unlike the next section where a shared CallMe object forces coordination. Daemon status (not modified here) would also affect whether the JVM waits for these printers; by default they are user threads, so the JVM lingers until both finish.
Recap and bridge: SumThread.run() yields sum=15 and FactThread.run() yields fact=120; someT is set to priority 3 while factT is set to 7, so the higher-priority factT is hinted to run first though the scheduler ultimately chooses the interleaving. When those two threads instead share one mutable CallMe object rather than private locals, the same start pattern requires additional coordination — the next section's monitor and synchronized.
26.13 Thread Synchronization — Monitors, Critical Sections and Synchronized Access
26.13.1 Critical Section and Monitor
Hook: Two threads posting to the same log line at the same instant should not splatter [hello [java [programming ] ] ] across the console. How do you tell the JVM "let only one thread at a time touch this shared method"?
When two or more threads work on a shared resource, uncontrolled access leads to wrong results. A critical section — also called a mutually exclusive block or shared-resource section — is code that only one thread should execute at a time. If thread T1 already executes the section, thread T2 should not enter until T1 leaves. Thread synchronization is the coordination that enforces this, achieved with the concept of a monitor.
A monitor — an object used as a mutually exclusive lock — works as follows: T1 enters, it owns the monitor and acquires a lock. Any other thread like T2 attempting to enter the locked monitor is suspended until the first thread exits the monitor. When T1 exits, the monitor is freed and T2 can acquire it, enter under lock, execute, then free the monitor again. The critical section stays protected by holding and freeing the monitor.
Formalize — monitor association and entry/exit protocol: In Java every object carries an intrinsic monitor (often called an intrinsic lock or monitor lock) that threads compete to hold. The linguistic coupling decides which monitor is used: a synchronized instance method uses the monitor of this (here the shared CallMe target object); a synchronized(target){ ... } block uses exactly the monitor of the named object target; a static synchronized method would use the Class object's monitor. The protocol is: thread T1 attempting to enter a synchronized region first attempts monitor.enter() — if the monitor is free it acquires it, sets the owner to T1, and executes the region. Thread T2 arriving while the monitor is held is placed in the entry set (not queued for CPU) and is suspended. Critically, calls to Thread.sleep(1000) while holding the monitor do not release the monitor — the sleeper still owns the lock and the second thread remains suspended even though the CPU is idle. When the holder exits the synchronized method or block (normally or via exception), it calls monitor.exit() and the JVM wakes one waiting entrant to acquire and proceed.
Visual: draw a door labeled monitor of shared CallMe target. T1 is inside the room executing call(msg), holding a key labeled lock. T2 and T3 wait outside the door with a sign "suspended." When T1 sleeps inside (eyes closed, but still gripping the key), the others remain outside because the key is not released. Only when T1 steps out and hands back the key does T2 receive it and enter.
Scope and assumptions: The monitor protects a code region associated with an object, not a variable scope — two different objects' synchronized methods do not block each other even if the method code is identical, because the monitors are distinct. The critical section should be as small as possible to reduce contention; wrapping unrelated code in the same monitor unnecessarily serializes computation that could otherwise run in parallel. The lecture's monitor is the single shared CallMe target reference that all three Caller objects share — highlighting why using synchronized(target) on that exact reference is needed for mutual exclusion.
Pitfalls: Do not assume sleep releases the lock — Thread.sleep(1000) inside a synchronized method keeps the monitor held, forcing others to wait even while the holder idles; only Object.wait() releases the lock (it purposefully belongs to coordination, not sleep). Do not mix synchronized regions on different objects expecting exclusion — synchronized(objA){} and synchronized(objB){} never block each other because the monitors differ even if the inner statement is target.call(...) in both. Do not forget that every call path must synchronize on the same monitor — one unsynchronized path into the shared resource nullifies the guarantee.
Real-world and domain connection: Monitors and critical sections prevent corrupted transfers in banking (synchronized on the shared Account during withdraw), duplicate seat bookings in airline reservations, and garbled structured logs when ten request threads share one file writer. In producer-consumer pipelines the shared queue's put/take methods are the canonical critical sections, each synchronized on the queue's monitor even though the payload differs.
Recap: A critical section is mutual-exclusion code touched through one shared monitor; the first thread acquires the monitor and owns it through sleep until it exits, while any other thread attempting the same monitor is suspended until the holder frees it. The next subsection displays that suspension failure when the monitor is omitted.
26.13.2 Worked Example WITHOUT Synchronization — CallMe, Caller, Sync
class CallMe {
void call(String msg) {
System.out.print("[" + msg); // e.g., [hello
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("]"); // ]
}
}
class Caller implements Runnable {
CallMe target; String msg; Thread t;
Caller(CallMe target, String msg) {
this.target = target; this.msg = msg;
t = new Thread(this); // create thread taking this runnable via this operator
t.start(); // start immediately; will enter public void run
}
public void run() {
target.call(msg); // call target.call with message
}
}
class Sync {
public static void main(String[] args) {
CallMe target = new CallMe(); // one shared object, name target repeated
Caller ob1 = new Caller(target, "hello");
Caller ob2 = new Caller(target, "java");
Caller ob3 = new Caller(target, "programming");
}
}
Worked trace — why interleaving garbles the brackets: Setup: one heap object CallMe target is shared; three Caller objects ob1, ob2, ob3 each store the same target reference (this.target = target) and the message (hello, java, programming) then create t = new Thread(this) and call t.start(). Each thread's path is run() { target.call(msg); } which expands to System.out.print("[" + msg); sleep(1000); println("]");. Expected sequential ideal (if one call finished before the next started): [hello] on its own line, then [java], then [programming]. What actually happens without synchronized: schedule ob1 first — CallMe.call("hello") prints [hello (no newline yet) and sleeps 1000 ms. The processor should not stay idle, so the scheduler gives the CPU to ob2 even though ob1 is still conceptually inside call. ob2 prints [java and sleeps. Then ob3 prints [programming and sleeps. While all three sleep their elapsed timers expire in some order, and each executes the deferred println("]"). Because the opening brackets were printed before any closing bracket, output observed is garbled, for example bolded garbled form [hello [java [programming ] ] ] rather than the intended three bracketed lines [hello], [java], [programming]. The repeated phrase "The name of the object is target" stressed by the lecture is precisely this point — all three callers alias the same target monitor-capable object yet synchronization is absent, so the aliased sharing is visible only as a race. Sequence of execution is decided by sleep timing and scheduler quantum, not by creation order. Sense-check: fixing by reordering sleep removal would mask but not cure the race — two callers calling call without delay could still interleave at the print boundary; only a monitor removes the statistical window.
Steps without synchronized restated: CallMe.call(String msg) prints "[" + msg, then sleeps 1000 ms, then prints ]. Caller implements Runnable; its constructor initializes target, msg, creates new Thread(this) and calls start(). When started, control reaches public void run() which does target.call(msg).
In Sync.main, one shared CallMe target is created. Three Caller objects ob1, ob2, ob3 are created with messages "hello", "java", "programming". Ideal sequential expectation is [hello], then [java], then [programming] each on its own line.
What actually happens without synchronization: ob1 prints [hello and sleeps 1000 ms. The processor should not stay idle, so the scheduler gives the CPU to ob2, which prints [java and sleeps; then ob3 prints [programming and sleeps. When the sleep timers expire, the closing ] characters appear interleaved, yielding a garbled order observed on the system. The text repeats The name of the object is target many times, stressing that all three callers share the same target reference, making the race obvious. The sequence of execution is decided by sleep timing and scheduler, not by creation order.
A timeline makes the interleaving visible: horizontal time lane per caller with events T: open [msg, box sleep 1000, event close ]. Without synchronization the three open events cluster early, the three sleep boxes overlap, and the three close events cluster late, interleaving as open1 open2 open3 close? close? close?. Add a dotted monitor absence label across the top explaining why the sleep window invites entry.
Scope and assumptions: This example uses System.out.print and println as the observable shared resource; mixing print without newline deliberately exposes the interleaving by holding the bracket half-open. The 1000 ms sleep is a deterministic contention amplifier — even without sleep the race exists at the print instruction boundary, but the one-second hold guarantees the window is hit on a slow scheduler. All three callers share the same target; distinct CallMe objects would hide the bug because each thread would hold a different monitor (or none).
Recap: Without a monitor, three threads sharing one CallMe target each print msg after sleep(1000) produces [hello [java [programming ] ] ] style garbling rather than [hello], [java], [programming] — proving that sharing the reference alias alone does not protect the bracketed critical section.
26.13.3 Fix 1 — Synchronized Method
To restrict the race, prefix the method definition with synchronized:
class CallMe {
synchronized void call(String msg) {
System.out.print("[" + msg);
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("]");
}
}
Everything else remains the same. By marking call as synchronized, the monitor associated with the CallMe object is held for the entire execution of the method, even if the current thread sleeps and the processor could be given to another. The current thread finishes printing the closing bracket before another thread can enter call. The scheduler will wait even if waiting means keeping the CPU idle for 1000 ms. Output becomes ordered: [hello] then [java] then [programming], each message bracketed correctly.
Worked trace — monitor held through sleep restores order: Keep CallMe with synchronized void call(String msg) on the shared target object. Thread ob1 arrives first and attempts to enter call; monitor of target is free, so it acquires it, prints [hello, then sleeps 1000 ms still holding the monitor (bolded holding point: sleep does not release lock). ob2 arrives while ob1 sleeps and attempts to enter call on the same target; the JVM checks target's monitor — it is owned by ob1 — so ob2 is suspended and not dispatched, even though the CPU is idle. Same for ob3. When ob1's sleep expires it prints ] — completing [hello] — exits the synchronized method, performs monitor.exit() on target, and wakes one waiter. The JVM schedules ob2, which acquires target's monitor, prints [java, sleeps holding the same monitor, prints ], releases; then ob3 acquires, prints [programming], sleeps held, prints ]. Observable output is now bolded ordered form [hello] then [java] then [programming] each on its own line, with no cross bracket insertion. Timing note: total wall time is roughly three seconds because the holder keeps the CPU idle during each sleep — mutual exclusion trades throughput for correctness.
A visual contrast helps: keep the same three lane timeline but shade the synchronized monitor as a single roped corridor. ob1 enters and hangs a "occupied" sign that stays during sleep, ob2 and ob3 wait at the door instead of walking in; after ob1 exits the door, ob2 passes in with the same sign, then ob3. The idle-CPU-while-holding habit is annotated: "sleep keeps the lock."
Scope and nuance: synchronized on an instance method is shorthand for synchronized(this){ ... } covering the whole body — the monitor chosen is this, here the shared target. If each caller had been given a distinct CallMe object, the method would synchronize on three different monitors and still interleave; correctness depends on sharing the monitor object, which the lecture achieves by reusing one target variable. For static data, static synchronized would instead lock on CallMe.class.
Recap: synchronized void call makes the shared target's monitor the gate; the holder keeps the gate locked even while sleep(1000) idles the CPU, so [hello], [java], [programming] appear bracket-complete rather than interleaved.
26.13.4 Fix 2 — Synchronized Statement (Block)
Instead of marking the method, synchronize at the call site:
class Caller implements Runnable {
// ...
public void run() {
synchronized(target) { // synchronized keyword with object
target.call(msg);
}
}
}
Here the synchronized(target) block tracks synchronization. One by one, whichever thread reaches the block first owns the monitor of target; other threads wait until that thread finishes and frees the monitor. The rest of the logic stays identical, and the printed sequence becomes correct with proper brackets.
Worked trace — block form protects only the call, leaving the rest unsynchronized: Remove synchronized from CallMe.call and keep it unsynchronized, but change Caller.run() to synchronized(target){ target.call(msg); }. Now CallMe.call itself does not own any monitor; the monitor is acquired and released in Caller.run() around the call. Arrival trace: ob1 reaches synchronized(target) when target's monitor is free, acquires it, invokes target.call("hello") which prints [hello, sleeps 1000 ms while ob1 still holds the target monitor (holder is still ob1 in the calling frame), prints ], unwinds back to run(), exits the synchronized(target) block and releases target. Only then can ob2's synchronized(target) succeed, repeat for [java], release, then ob3 for [programming]. The visible order is again bolded ordered [hello] [java] [programming], identical to Fix 1 for this pattern because the critical section contains exactly the same call. The difference is bookkeeping: Fix 1 locks inside the callee for every caller automatically; Fix 2 locks in each caller, allowing choose-per-callsite granularity — code in the same class but outside synchronized(target) still runs freely without the lock, so you can protect a smaller sub-region. Sense-check: swapping synchronized(target) for synchronized(this) here would fail, because each Caller has a different this object; the three threads would each hold their own monitor and still interleave at target.call — target identity matters.
Real-world: synchronization prevents corrupted transfers, duplicate bookings, or garbled logs when many threads share counters, files, or sockets. The two forms — synchronized method and synchronized(object) { ... } block — are the standard tools.
Q&A — the examined distinction: Q: Why does the unsynchronized version interleave brackets while the synchronized version does not? A: Without synchronization, each thread sleeps after printing msg and yields the CPU, so other threads enter call before the first finishes and interleave as [hello [java [programming ] ] ]. With synchronized, the first thread holds the monitor for the whole method or block, so other threads attempting to enter are suspended until it exits, even if it sleeps and leaves the CPU idle during that sleep. The monitor transforms "idle CPU means run next thread" into "idle CPU but still holding the lock means wait."
A second comparative figure: show two code panels. Left panel highlights synchronized before void call in CallMe. Right panel highlights synchronized(target){ around target.call(msg); in Caller.run(). Draw a shared monitor circle labeled target with arrows from both panels pointing to the same circle, annotating "Fix 1 locks the callee, Fix 2 locks the call site — same monitor object, same correctness."
Scope and assumptions: Fix 1 is simpler and protects every call to CallMe.call regardless of which caller forgets to wrap; Fix 2 is more flexible and lets one thread protect a batch synchronized(target){ call("hello"); call("world"); } as one atomic batch. If the synchronized region is widened to cover unrelated computation, contention worsens without benefit — minimize the protected span. Also, synchronized(target) assumes target is non-null and immutable across the block; reassigning target = new CallMe() mid-execution changes which monitor is held on the next acquisition.
Pitfalls: Do not synchronize on a boxed primitive or string literal that may alias unexpectedly — use a dedicated private final Object lock = new Object(); in real code rather than exposing this. Do not leave one access path unsynchronized — a single target.call(msg) outside any monitor invalidates the protection of the other guarded calls. Do not expect fairness — the JVM wakes one waiter, but which of ob2 or ob3 wakes first is not guaranteed; the order observed as [hello] [java] [programming] matches start order in demos but can reorder at scale.
Recap and exam bridge: Exam note: be ready to predict interleaving for CallMe without synchronization (garbled [hello [java [programming ] ] ]) versus with synchronized method or synchronized(target) block (ordered [hello] [java] [programming]), to name which monitor is used (target's intrinsic lock), to state why sleep(1000) still holds the lock, and to explain that both fixes — method-level synchronized void call and call-site synchronized(target){ ... } — protect the same critical section when the same target object is shared.
26.14 Java Object Model — Object Cloning, Shallow versus Deep Copy
26.14.1 The Root Object and the Need for Cloning
Hook: You want a new employee record A3 with the same name = "Mike" and salary = 35000 as E1 — but A3 = E1 does not clone the record; it only copies the arrow pointing to it. How do you get a genuinely separate object whose content happens to be equal?
In the Java object model, Object is the universal parent class. Every class implicitly or explicitly descends from Object, so every object inherits toString(), equals(), hashCode(), and clone(). Cloning concerns copying an object so that the copy has a distinct identity but equal content — the clone's fields hold the same values as the original, yet the two objects live at different addresses. That distinct address means later mutation of one does not disturb the other (deep for mutable referenced parts) — unlike alias assignment which leaves a single address behind two names.
Example hierarchy: class ABC declares two data members, String name and double salary (later also Date hireDate for shallow/deep contrast). Objects created: E1 with name = "Mike" and salary = 35000, E2 with name = "Smith" and salary = 40000, sharing the same class.
Formalize — identity versus equality and what A3 = E1 actually does: A reference variable holds an address, not the object store. Draw address 1000 as a heap cell containing fields { name -> "Mike" object, salary = 35000 }. Variable E1 holds 1000. The statement A3 = E1; copies address 1000 into A3, so now E1 == 1000 and A3 == 1000 — identity equality A3 == E1 holds, A3.equals(E1) also holds because content is shared, but there is only one cell. Mutating E1.name = "Smith" would be visible through A3 because both references alias the same cell. The goal of cloning is E.clone() creating a second cell such as 1001 with field-wise equal values but a different address, so clone == E is false while field equality holds. String duplication is a corner: because String is immutable, sharing its reference is safe; Date is mutable, so shallow sharing leaves a hidden alias.
If a new object A3 should hold the same values as E1 ("Mike", 35000), writing A3 = E1; does not create a separate entity. In memory, suppose address 1000 stores the fields "Mike" and 35000, and E1 points to 1000. The assignment makes A3 also point to 1000; both references share the same address, no separate copy exists. Changing one would affect the other.
To get a genuine separate copy, we need object cloning. The Java mechanism for this is the marker Cloneable interface cooperating with Object.clone().
Visual: draw two heap boxes. Box at 1000 with "Mike" and 35000. Two arrows enter it labeled E1 and A3 after A3 = E1. Next panel labeled "cloning" shows two boxes: box 1000 with E arrow, and new box 1001 with cloned arrow, each containing "Mike" and 35000 but at distinct addresses, illustrating distinct identity with equal content.
Scope and assumptions: The lecture's ABC mixes immutable String, primitive double salary, and mutable Date hireDate precisely to later show why some fields need extra work. The assignment alias demonstration assumes heap objects, not primitives — int b = a; for primitives does copy value; reference assignment copies address. Object is the root even when the source writes class ABC with no explicit extends; the extends-Object is implicit and is what supplies clone() for the hierarchy.
Pitfalls: Do not confuse A3 = E1 with cloning — assignment aliases, cloning duplicates. Do not assume E1.equals(E2) means E1==E2; equality is field-wise, identity is address-wise. Do not attempt ABC a3 = (ABC) E1.clone() without the class cooperating (see next subsection) — it throws or refuses to compile unless Cloneable is implemented and clone() is made accessible.
Teaching moment preserved — assignment versus cloning: Copying a reference A3 = E1 shares address 1000 and leaves one heap cell behind two names; only cloning creates a second heap cell at a new address with equal field values and therefore distinct identity. When the copy must later evolve independently (change name, adjust salary, shift hire date), alias assignment silently corrupts the original while cloning isolates it — the reason A3 = E1 is insufficient and cloning is needed for distinct identity with equal content.
Recap: Object crowns the hierarchy; A3 = E1 copies the address to 1000 leaving one object with two names; cloning aims to create a second address 1001 with the same Mike/35000 content but independent identity, which requires the Cloneable plus clone() machinery.
26.14.2 Worked Example — Cloneable and clone()
class ABC implements Cloneable {
String name;
double salary;
Date hireDate; // from Date class, mutable
public Object clone() throws CloneNotSupportedException {
return super.clone(); // call Object.clone()
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
ABC E = new ABC(); E.name = "Mike"; E.salary = 35000;
ABC clone_A = (ABC) E.clone(); // creates distinct object with same content
// Now E and clone_A have same name/salary but different identities
}
}
Formalize — contractual bits Cloneable, clone() signature, and what super.clone() does: The marker interface Cloneable carries no methods — it is a permission flag telling Object.clone() that field-by-field copying is allowed; without it, Object.clone() throws CloneNotSupportedException. The method to expose is exactly public Object clone() throws CloneNotSupportedException { return super.clone(); } — visibility must be at least public (the Object original is protected), return type Object (before covariance) requiring a cast at the call site, and the throws to propagate the permission check failure. super.clone() on a class that implements Cloneable performs a native shallow field copy: it allocates a fresh heap cell of the same runtime class (ABC), bit-copies each field value, and for reference fields copies the pointer (not the pointee). After ABC clone_A = (ABC) E.clone(); the condition E != clone_A holds (distinct addresses) while E.name.equals(clone_A.name) and E.salary == clone_A.salary hold (equal content). The cast (ABC) is required because Object clone() returns Object; with Object clone() modern code sometimes narrows the return type to ABC as public ABC clone()..., but the lecture keeps the classic Object form and explicit cast.
Walk-through: ABC implements Cloneable. The method public Object clone() throws CloneNotSupportedException is required; it calls super.clone(), which asks Object to create a field-by-field copy. That call returns an object that is cast and assigned to clone_A. After cloning, E and clone_A contain the same content ("Mike", 35000) but their addresses differ, their references differ, their identities differ. This is the duplicated object versus the original object.
Worked trace — constructing E and forking a distinct clone_A: Step 1 — ABC E = new ABC(); E.name = "Mike"; E.salary = 35000; allocates heap cell say 1000 with fields name -> String "Mike" (immutable reference), salary = 35000, hireDate -> null initially. Step 2 — method contract: ABC declares implements Cloneable (permission granted) and overrides public Object clone() throws CloneNotSupportedException { return super.clone(); } forwarding to native Object.clone(). Step 3 — call ABC clone_A = (ABC) E.clone();. Inside E.clone() -> super.clone() -> native code allocates new cell say 1001 as a raw byte copy of the field slots at 1000. So slots: name at 1001 gets name pointer copied (points to same "Mike" string object — safe because String is immutable), salary at 1001 gets 35000.0 as value copy, hireDate at 1001 gets the same pointer as at 1000 (still aliased — unsafe for mutable Date, fixed in deep step). Return value is a reference to 1001; cast (ABC) succeeds because the new cell's runtime class is ABC; assignment clone_A = stores 1001. Verification: E == clone_A evaluates false (different addresses 1000 != 1001), E.name.equals(clone_A.name) true, E.salary == clone_A.salary true. In main the declaration throws CloneNotSupportedException covers the checked branch where Cloneable was omitted — had Cloneable been omitted, super.clone() would throw and the method would abort rather than allocate 1001. Sense-check: if you wrote ABC clone_A = E; instead of cloning, you would have one cell 1000 with two arrows and E == clone_A true — the opposite of distinct identity, showing why Cloneable plus super.clone() is needed rather than plain assignment.
The main method shows Clone A calling A.clone() to obtain clone_A. The assignment separates identity while keeping equality of field values. Cost visual: heap panel after cloning shows E -> 1000: { name->"Mike", salary 35000, hireDate->D0 } and clone_A -> 1001: { name->"Mike" (shared pointer), salary 35000 (own copy), hireDate->D0 (shared pointer pending deep fix) }. Label primitive salary as value-copied, name as shared-immutable, hireDate as shared-mutable (bug pending).
Scope and assumptions: Object.clone() is a native field-by-field copy without invoking any constructor — field initializers and constructors do not rerun for the clone, so any constructor-time validation must be handled separately if needed. Cloneable is a marker, not a typed clone() declaration; the compiler does not enforce the clone() override, but the runtime throws unless the flag is present. Exceptions: CloneNotSupportedException is checked, so callers must either catch or propagate it with throws as main does here. Modern code often overrides with covariant public ABC clone() to avoid the cast, but the checked throws remains.
Pitfalls: Do not implements Cloneable without overriding clone() to public — Object.clone() is protected, so callers outside ABC could not call it directly without widening. Do not swallow CloneNotSupportedException with an empty catch — the permission failure indicates a design error where a supposedly cloneable class omitted the marker. Do not cast E.clone() to ABC without verifying the runtime type — with super.clone() the cast is safe, but cloning through a parent reference requires attention to which class actually carries the new cell. Do not assume the clone is deep — super.clone() copies pointers, so mutating shared Date through one handle mutates the other until the deep fix is applied.
Recap: ABC implements Cloneable exposes public Object clone() throws CloneNotSupportedException { return super.clone(); }; calling ABC clone_A = (ABC) E.clone(); allocates a new heap cell 1001 with field-wise equal Mike/35000 but a different address, giving distinct identity that assignment A3 = E does not.
26.14.3 Shallow Copy versus Deep Copy
The clone produced by super.clone() alone is a shallow copy — the top object's fields are copied, but referenced objects are not duplicated; only the reference pointers are copied. Thus class references remain shared. With shallow copy, E and cloned have same primitive salary value but stored separately (changing one does not affect the other). However name and hireDate in both objects still point to the same String object and the same Date object in memory. Their object references are not changed; only a shallow copy is created. For String this sharing is safe because String is immutable and cannot be directly changed. For Date it is not safe, because mutating the date via one object would be visible through the other.
Formalize — which fields shallow is safe for and the deep remedy: After ABC cloned = (ABC) super.clone(); the partition is: primitive double salary — value-copied, independent; immutable reference String name — pointer-copied but safe because String has no mutator (name = new String(...) reassigns the field rather than mutating the shared object); mutable reference Date hireDate — pointer-copied and unsafe because Date exposes setTime(...) that mutates the shared Date instance. Deep copy for the mutable part is therefore cloned.hireDate = (Date) this.hireDate.clone(); or cloned.hireDate = new Date(this.hireDate.getTime()); executed after the shallow copy, allocating a fresh Date cell D1 with the same epoch value as original D0 and rewiring cloned.hireDate to D1. The hireDate copy is made from E.hireDate to clone.hireDate, isolating mutation. With this step the shared-pointer class converts to E.hireDate -> D0, cloned.hireDate -> D1 where D0.equals(D1) but D0 != D1, while name still safely shares the immutable "Mike" and salary is already separate.
To create a deep copy — a fully independent duplicate including referenced mutable objects — we copy the mutable field separately after the shallow clone:
public Object clone() throws CloneNotSupportedException {
ABC cloned = (ABC) super.clone(); // shallow as before
// String needs no extra work (immutable)
// Date must be copied explicitly for deep copy
cloned.hireDate = (Date) this.hireDate.clone(); // or new Date(this.hireDate.getTime())
// salary already independent as primitive
return cloned;
}
// Usage: cloned.hireDate now independent from E.hireDate
// Statement form shown as: hireDate copied from E.hireDate to clone.hireDate
Walk-through anchored to the memory diagram: start from shallow clone E:1000 and cloned:1001 with shared D0. Leave String name as shared (immutable, safe). For Date, create a separate Date object and assign to cloned.hireDate, copying value from E.hireDate to clone.hireDate. Now clone.hireDate points to a different address than E.hireDate. The salary primitive is already separate. The resulting clone is deep for the mutable part while still efficient for immutable strings.
Worked mutation check — shallow bug versus deep isolation: Start shallow: ABC E = new ABC(); E.hireDate = new Date(2024-01-01); E.name = "Mike"; ABC shallow = (ABC) E.clone(); — heap sharing is shallow.hireDate == E.hireDate (same D0), shallow.name == E.name (same "Mike"). Mutate through E.hireDate.setTime(new Date(2025-06-01).getTime());. Because both pointers read D0, observe shallow.hireDate.getTime() now also returns June 2025 — bolded shallow leak: changing E leaked into the clone via Date. In contrast, mutating E.name = "Smith" does not affect shallow.name, because assignment replaces E.name's pointer rather than mutating the immutable "Mike" object — shallow.name still points to "Mike". Now apply the deep fix: ABC deep = (ABC) E.clone(); deep.hireDate = (Date) E.hireDate.clone(); — now deep.hireDate == D1, a fresh copy. Mutate deep.hireDate.setTime(new Date(2026-01-01).getTime());. Check E.hireDate.getTime() — it still returns June 2025, unchanged, while deep.hireDate is January 2026. Bolded deep isolation: cloned.hireDate and E.hireDate are now independent heap objects D1 != D0 with distinct mutations. For salary, try E.salary = 40000 after cloning — deep.salary remains 35000 in both shallow and deep because primitives were value-copied at allocation. Sense-check confirms the field taxonomy: String shared is harmless, double separate is automatic, Date shared is dangerous and requires the extra Date.clone() step to break the alias.
If you mutate cloned.hireDate after deep cloning, E.hireDate stays unchanged, confirming independence. With shallow cloning alone, the mutation would leak. Heap diagrams sequence this story: panel A shows shallow alias cloned.hireDate -> D0 <- E.hireDate; panel B shows deep isolation cloned.hireDate -> D1, E.hireDate -> D0 with D0 and D1 holding equal epoch but different arrows; a caption marks "mutate Date via clone — no leak after deep" versus "leak before deep."
Scope and assumptions: Deep copying is applied selectively to mutable references — not all references require it. Common mutable candidates needing deep copy are Date, ArrayList, StringBuilder, arrays, and any custom mutable object graph; immutable candidates (String, Integer, LocalDate) can stay shallow. Deep copy after super.clone() must null-check hireDate if the source may be null, otherwise this.hireDate.clone() throws NullPointerException. For complex object graphs, cloning the entire graph may require recursive clone() on each mutable node or a copy constructor pattern.
Pitfalls: Do not add deep comment but forget the assignment — leaving Date shared reintroduces the leak even though the code mentions "deep copy." Do not call this.hireDate.clone() without casting back to Date if the overridden clone() still returns Object. Do not deep-copy immutable fields — new String(this.name) wastes memory with no safety gain. Do not assume super.clone() allocates new Date objects — it never does; the extra Date allocation is always the caller's explicit step after the native copy.
Real-world: cloning supports safe duplication of user profiles, configuration snapshots, or prototype patterns where a new object should start with the same state but evolve independently. In prototype frameworks each concrete product clones a registered prototype; in persistence layers a detached entity is deep-cloned so changes can be merged without corrupting the cached original. The principle proven here — hireDate copied from E.hireDate to clone.hireDate via an explicit Date.clone() or new Date(getTime()), salary automatically independent, String safely shared — guides exactly which fields to deepen in each domain copy.
Recap and bridge: super.clone() shallow-copies every field pointer, which safely shares immutable String and correctly separates primitive double salary, but dangerously aliases mutable Date hireDate; adding cloned.hireDate = (Date) this.hireDate.clone() after the shallow copy breaks the alias, yielding cloned.hireDate != E.hireDate as independent Date objects while cloned.name == E.name remains intentionally shared. With reliable duplication mechanics in place, the lecture turns to fixing the allowed value set rather than aliasing.
26.15 Enumerated Types (enum)
26.15.1 What an Enum Is
Hook: A variable should only ever be SMALL, MEDIUM, or LARGE — never "small", "EXTRA LARGE", or 42. How does the compiler stop anyone from inventing a fourth size that the production line cannot handle?
An enum — an enumerated type with a finite set of values — restricts a variable to only those values listed in the set. It is defined with the keyword enum and is internally equivalent to a class with a private constructor and a fixed number of instances. Once the set is declared, no new element can be added at runtime; the closed set is the type's completeness.
Consider Size with values SMALL, MEDIUM, LARGE:
enum Size { SMALL, MEDIUM, LARGE }
This declaration defines a class whose instances are exactly SMALL, MEDIUM, LARGE. No other values can be used. Access is via Size.SMALL, Size.MEDIUM, Size.LARGE.
Formalize — syntactic shape and class equivalence: Write enum Size { SMALL, MEDIUM, LARGE }. Each identifier before a comma is a public constant of type Size, ordered by declaration (useful for iteration and comparison). The compiler desugars the enum into a final class roughly as shown below, making each constant a public static final Size field initialized once via a private constructor, and making the constructor private so outside code cannot call new Size() to mint new elements. Enums optionally carry fields, constructors with arguments, and methods, and they implicitly extend java.lang.Enum (which explains why they cannot extend any other class). Size.values() returns an array of the declared constants in declaration order; Size.valueOf("SMALL") looks up by name; size.ordinal() returns the zero-based declaration index.
The equivalence shown makes this explicit:
class Size {
private Size() {} // private constructor: no outside creation
public static final Size SMALL = new Size();
public static final Size MEDIUM = new Size();
public static final Size LARGE = new Size();
}
Worked comparison — enum syntax versus class pattern: Enum form: enum Size { SMALL, MEDIUM, LARGE } — three constants declared in braces, no new visible. Class pattern that the enum expands to: class Size { private Size(){} public static final Size SMALL = new Size(); public static final Size MEDIUM = new Size(); public static final Size LARGE = new Size(); }. Points: constructor is private — new Size() outside Size fails to compile ("constructor is not visible"). The three public constants are static final so their values never change and they are reachable via the class name: Size.SMALL, Size.MEDIUM, Size.LARGE. Assignment Size s = Size.MEDIUM; is legal; Size s = new Size(); is illegal; Size s = "SMALL"; is type-mismatched — only the three declared constants are admissible. Loop iteration for(Size s: Size.values()) visits SMALL -> MEDIUM -> LARGE in that declaration order (bolded order guarantee). Adding a fourth constant after deployment requires editing the enum source; no runtime statement can inject one. Sense-check: when you assign an enum variable only one of the three constant objects may be held, which is why comparing with == is correct and preferred over .equals() for enums (they are effectively singletons).
Steps: a private constructor prevents creating objects outside the class. Instances are created inside the class with static final so their values never change and they are reachable via the class name (Size.SMALL). The enum keyword builds the same structure automatically.
Visual: draw two panels. Panel A shows the terse enum box enum Size { SMALL, MEDIUM, LARGE } with a lock icon on the constructor. Panel B expands to the class box with three rows SMALL: static final, MEDIUM: static final, LARGE: static final, each pointing to a heap singleton, and annotate new Size() outside with a red X labeled private. The lock and the static final together caption "compiler-enforced closed set, no runtime extension."
Scope and assumptions: The bare enum Size { SMALL, MEDIUM, LARGE } assumes no per-constant state; when state is needed the enum supports payload fields: enum Size { SMALL(10), MEDIUM(20), LARGE(30); private final int width; Size(int w){ this.width=w; } }. An enum may implement interfaces but may not extend a class (it already extends Enum). Enum constants may optionally add constant-specific behavior by overriding methods with a class body per constant. Enums are serializable by guarantee, with readResolve enforced to preserve singleton identity across serialization.
Pitfalls: Do not rely on ordinal() as a stable persistence key — inserting a constant mid-list shifts later ordinals and corrupts stored indices; persist the name instead. Do not instantiate with new Size() — the constructor is implicitly private, so outside construction is blocked by design; creation happens only via the declared constants. Do not compare enum values with size1.equals(size2) when == is simpler and null-safe in the right direction — both work, but == also makes clear that enum instances are singletons and never duplicated.
Real-world and domain connection: Enums appear wherever only fixed choices exist — days of week Day { MONDAY, ... }, order status OrderStatus { PENDING, SHIPPED, DELIVERED, RETURNED }, compass directions Direction { NORTH, EAST, SOUTH, WEST }, or UI theme Theme { LIGHT, DARK }. Replacing error-prone raw String or int codes with enums moves the "must be one of this set" check from a runtime if to a compile-time type error, which is exactly the inventory-safe Size.SMALL discipline illustrated.
Recap: enum Size { SMALL, MEDIUM, LARGE } declares a class whose only instances are those three public static final constants reachable as Size.SMALL and so on, backed by a private constructor that forbids outside creation — a closed type-safe set that the Season walk-through now exercises in a loop.
26.15.2 Worked Example — Season with For-Each Loop
class ABC {
enum Season { WINTER, SPRING, SUMMER, FALL }
public static void main(String[] args) {
for (Season s : Season.values()) {
System.out.println(s); // prints each constant
}
}
}
Worked trace — declaration, iteration, and ordered printing: Step 1 — declare enum Season { WINTER, SPRING, SUMMER, FALL } inside class ABC. The four constants are created as public static final singletons of type Season, in that syntactic order: WINTER ordinal 0, SPRING 1, SUMMER 2, FALL 3. Step 2 — inside main execute for (Season s : Season.values()). Season.values() returns a new array Season[4] containing {WINTER, SPRING, SUMMER, FALL} in declaration order. Step 3 — iterate. First pass: s = WINTER, body System.out.println(s) calls WINTER.toString() which by default returns the name WINTER — prints bolded WINTER. Second pass: s = SPRING prints SPRING. Third: s = SUMMER prints SUMMER. Fourth: s = FALL prints FALL. Output sequence is therefore WINTER, SPRING, SUMMER, FALL each on its own line, in the exact order the values appear in the enum declaration (bolded order: WINTER -> SPRING -> SUMMER -> FALL). The lecture note mentioning a variant run that once printed WINTER, SUMMER, SPRING, FALL does not match declaration order; only the declaration order WINTER, SPRING, SUMMER, FALL is contractually guaranteed by values(). Step 4 — immutability check: attempting Season s = new Season() or adding a fifth season at runtime would not compile — the closed set WINTER..FALL is exhaustive after declaration, and only those four values may ever be assigned to a Season variable. Sense-check: after the loop you may switch on seasons switch(s){ case WINTER: ... break; } without default because the compiler knows the case set is exhaustive.
Walk-through: enum Season holds four values WINTER, SPRING, SUMMER, FALL. ABC contains this enum and a main method with a for-each loop. Loop control variable s takes values present in Season one by one. System.out.println(s) prints each constant as the loop iterates. The order printed follows declaration order: first WINTER, then SPRING or SUMMER (discussion notes variation: first WINTER, then SUMMER, then SPRING, then FALL on one run, but declaration order governs). Once values are defined in the set, they cannot be changed; only the listed values are allowed. You cannot add a new season at runtime.
Visual: draw Season as a vertical column of four labeled islands WINTER, SPRING, SUMMER, FALL in stack order top to bottom matching declaration. Draw the for-each loop as a hopper that visits each island in top-to-bottom order, printing its label upon arrival. Label the hopper for (Season s : Season.values()) and mark the printed stream to the right as WINTER ... FALL in vertical order aligning with the islands.
Scope and assumptions: Inside class ABC, Season is a nested enum and is implicitly static — no ABC instance is needed to refer to Season.WINTER. The qualified name inside ABC.main may be shortened to Season.WINTER; outside ABC it would be ABC.Season.WINTER unless imported. Season.values() allocates a fresh array each call — avoid calling it in tight loops by caching the array locally if needed. for (Season s : Season.values()) is the idiomatic iteration; for (int i=0; i< Season.values().length; i++) compiles but is unidiomatic for enums.
Pitfalls: Do not read a Season variable as a String and compare with s.equals("WINTER") — s has type Season, so compare with s == Season.WINTER directly to keep type safety. Do not depend on ordinal() surviving file formats — if later a MIDWINTER is inserted between WINTER and SPRING, ordinals of SPRING and beyond shift; stable code compares by name or explicit field instead. Do not nest enum iteration with modification of the iterated array — values() returns a fresh copy, but mutating that copy does not affect the enum constants themselves.
Real-world: enums appear wherever only fixed choices exist — days of week, order status (PENDING, SHIPPED, DELIVERED), compass directions — giving compile-time safety over raw strings. A workflow that processes Season s can exhaustively handle the four branches without if on String typos like "Wintr"; the compiler rejects an assignment of "WINTER" the String to Season s at once, enforcing the closed set guarantee where a String Constant would only fail at runtime.
Recap and bridge: enum Season { WINTER, SPRING, SUMMER, FALL } declares exactly four constants iterated by for(Season s: Season.values()) printing WINTER through FALL in declaration order; the set is closed, ordered, and accessed as Season.WINTER without runtime extension. With fixed value sets handled, the lecture turns to fixing which types a generic may range over.
26.16 Bounded Types
26.16.1 Bounding a Generic Type
Hook: A generic Bound<T> that accepts any T cannot even call displayClass() on its field obj — what if T turns out to be String and there is no displayClass to call? How does a bound restore the lost capability while keeping the class reusable?
An unbounded generic — e.g., class Bound<T> where T can be String, Long, Integer, or anything — places no limits on the type argument. A bounded type — written as T extends A — restricts T to type A itself or any sub class of A. The phrase T extends A is the bound; A is the upper limit. With the bound in place, obj.displayClass() becomes safe to call because the compiler knows every allowed T is at least an A.
Formalize — bound syntax, upper limit meaning, and the capability guarantee it restores: Write class Bound<T extends A> { private T obj; Bound(T obj){ this.obj = obj; } void doRunTest(){ obj.displayClass(); } }. The header T extends A is read as "T ranges over A and all subclasses that inherit from A." No other type is admissible — not String, not Long, not Integer unless that type itself descends from A. The bound is called an upper bound because A is the topmost allowed type; descendants B extends A and C extends A sit below it and are included. The capability guarantee: because every admissible T is known to have the members of A, the body of Bound may invoke any A method on obj; without extends A the call obj.displayClass() would not compile because the erased type would be Object which lacks displayClass. Multiple bounds are written T extends A & Serializable & Comparable<T> when needed, combining several upper requirements, but the lecture fixes the single upper bound T extends A.
Why bound? Without a bound you cannot call A's methods on a T value safely, because T might be unrelated. With T extends A, the compiler knows every allowed T is at least an A and supports A's operations.
Visual: draw A at the top with a method displayClass() inside. Draw B extends A and C extends A as two children below A, each with an overriding displayClass() body. Draw an unrelated island String far away. Shade the region covering A, B, C as the bounded range labeled T extends A — admissible, and mark String outside the shade labeled "outside — rejected." Label the Bound<T> box holding T obj with an arrow to obj.displayClass() annotated "safe because bound guarantees method exists."
Scope and assumptions: The bound keyword is extends even when A is an interface; for an interface A one still writes T extends A, not implements. T extends A is the default capability set for T; if nothing about T but Object operations is needed, the unbounded class Bound<T> suffices. Bounded generics participate in erasure: after compilation T erases to A (the bound) rather than to Object, which affects reflection-visible signatures and bridge methods but not user logic.
Teaching moment preserved — the restricting role of T extends A: The phrase T extends A restricts type arguments that can be used as parameterized types to those related to A — either A itself or its children B and C, nothing beyond. It is a filter on admissible types and an enabler inside the generic: it filters at the new Bound<Concrete> site and enables inside the body to call A's methods on the field typed T. The example "restricting types that can be used as type arguments in a parameterized type" is exactly this filtering role, illustrated by the admissible set {A, B, C} and the rejected outsiders {String, Long, Integer}.
Pitfalls: Do not write T super A expecting to restrict to superclasses — the lower bound super is for wildcards (? super A) rather than type-parameter bounds, which use only extends. Do not omit extends A then argue obj.displayClass() should compile — the generic body is checked against the bound, and without the bound obj has only Object members. Do not assume the bound forbids further specialization — Bound<B> and Bound<C> are both legal uses even though the generic guarantees only A's interface; the extra behavior comes from overriding.
Real-world and domain connection: Bounded generics appear as class SortedList<T extends Comparable<T>> where the bound ensures compareTo exists, method <T extends Number> void process(T value) where the bound guarantees numeric operations, or Spring's Repository<T extends Entity> where persistence operations assume getId(). Each bound says "this generic expects at least these promises from its type argument, or it will not compile."
Recap: Unbounded T ranges over anything and cannot call A's method; T extends A filters to A and its subclasses and simultaneously guarantees that obj.displayClass() inside the generic body is legal because every admissible T is known to be an A.
26.16.2 Worked Example — Bound<T extends A> with A, B, C Hierarchy
Hierarchy setup:
class A {
void displayClass() { System.out.println("inside class A"); }
}
class B extends A {
void displayClass() { System.out.println("inside subclass B"); } // overrides
}
class C extends A {
void displayClass() { System.out.println("inside subclass C"); } // overrides
}
class Bound<T extends A> {
private T obj; // field of generic type T bounded by A
Bound(T obj) { this.obj = obj; }
void doRunTest() { obj.displayClass(); } // safe because T extends A guarantees displayClass
}
Worked trace — three admissible type arguments and one rejected outsider, with dispatch through the bound: Step 1 — class A { void displayClass(){ println("inside class A"); } } defines the top type and the required method. Step 2 — class B extends A { void displayClass(){ println("inside subclass B"); } } and class C extends A { void displayClass(){ println("inside subclass C"); } } each inherit from A and override the same signature — B and C are admissible children. Step 3 — generic Bound<T extends A> stores private T obj — after erasure this field's erasure bound is A, but user-visible type is T. Construction Bound(T obj){ this.obj = obj; } keeps the exact passed runtime type, and void doRunTest(){ obj.displayClass(); } compiles because the bound guarantees A contributes displayClass(). Step 4 — use with A: Bound<A> ba = new Bound<A>(new A()); ba.doRunTest(); calls A.displayClass() on the contained A object and prints bolded inside class A. Step 5 — use with B: Bound<B> bb = new Bound<B>(new B()); bb.doRunTest(); stores a B, doRunTest looks up displayClass() on that B instance, dynamic dispatch finds B's override, prints bolded inside subclass B. Step 6 — use with C: Bound<C> bc = new Bound<C>(new C()); bc.doRunTest(); similarly prints bolded inside subclass C. The line "this type can directly be A or any of the subclasses of A" is exactly these three stamps Bound<A>, Bound<B>, Bound<C> each created from the same definition but carrying a different actual T. Step 7 — rejection case: Bound<String> bad = new Bound<String>(new String("x")); fails compilation with error "type argument String is not within bounds of type-variable T" because String does not extend A; String, Long, Integer are all outside T extends A and are rejected before any doRunTest could be called. The hierarchy discussion repeats Bound definition and emphasizes the bound line T extends A as the limiting keyword — that single phrase is what filters admissible T and legitimizes obj.displayClass() in one move. Sense-check: the same doRunTest() body dispatched differently for A, B, C because the stored obj retained its concrete runtime type while sharing the static bound guarantee — bounded generics preserve both type safety and runtime polymorphism.
Steps reiterated: A defines void displayClass(). B extends A and overrides the same method, printing "inside subclass B". C extends A and overrides similarly, printing "inside subclass C". Each is a sub type or child of A. Bound declares T extends A, holds a private T obj, receives a T via constructor, and offers doRunTest() that calls obj.displayClass().
What T can be? Directly A, or any sub class — so B or C. The statement "this type can directly be A or any of the subclasses of A" captures the rule. Creating new Bound<A>(new A()) is allowed and stores an A. Creating new Bound<B>(new B()) is allowed and stores a B; calling doRunTest() dispatches to B's version and prints "inside subclass B". Similarly new Bound<C>(new C()) prints "inside subclass C". The hierarchy discussion repeats Bound definition and emphasizes the bound line T extends A as the limiting keyword.
What is forbidden? Passing String, Long, Integer, or any type unrelated to A. The attempt new Bound<String>(new String("x")) fails to compile because String does not extend A. The bound restricts type arguments that can be used as parameterized types to those related to A — either A itself or its children B and C, nothing beyond.
A variant phrase used is "restricting types that can be used as type arguments in a parameterized type" — exactly what bounded types achieve. This lets a generic class safely use methods of the bound (here displayClass) on the generic field.
A table sharpens the admissibility rule: columns Instantiation, Bound check, doRunTest prints. Row 1: Bound<A>(new A()), A extends A — allowed, inside class A. Row 2: Bound<B>(new B()), B extends A — allowed, inside subclass B. Row 3: Bound<C>(new C()), C extends A — allowed, inside subclass C. Row 4: Bound<String>(new String("x")), String NOT extends A — compile error, no execution — rejected before call.
Real-world: bounded generics appear as class SortedList<T extends Comparable<T>> or method <T extends Number> void process(T value) where the bound ensures numeric or comparable behaviour is present.
Q&A — the property test: Q: Can T in Bound<T extends A> be a String? A: No. T extends A limits T to A or any class that extends A, such as B or C. String does not extend A, so it is rejected at compile time. Only A, B, or C can be used to create a Bound object. The same rejection applies to Long, Integer, or any unrelated type — only the A family passes the bound check, and obj.displayClass() inside Bound stays safe precisely because those outsiders never pass.
Scope and nuance: T extends A is an upper bound; ? extends A is the analogous wildcard upper bound on a usage site like List<? extends A> allowing List<B> to be read as List<? extends A>. The raw use Bound without <> would compile with a warning but discard the bound checking; prefer parameterized uses. Side note: class Bound<T extends A> can itself be referenced as a type argument, e.g., List<Bound<B>>, where the inner B already satisfied T extends A and the outer list imposes no further bound.
Pitfalls: Do not interpret T extends A as "strictly children of A" — it includes A itself (Bound<A> is allowed). Do not attempt new Bound<String> hoping the error appears only at doRunTest() — the error fires at the instantiation new Bound<String>(...) before any method is entered. Do not write class Bound<T> { void doRunTest(){ ((A)obj).displayClass(); }} without a bound as a workaround — it compiles via cast but defers the check to a ClassCastException at runtime; the bounded form fails earlier and faster at compile time without runtime danger.
Recap: Bound<T extends A> admits exactly A, B, C (each T extends A holds) and rejects String/Long/Integer; Bound<A> prints inside class A, Bound<B> prints inside subclass B, Bound<C> prints inside subclass C through obj.displayClass() in doRunTest(), demonstrating both filtering and safe method access in one construct.
Exam Guidance Summary
The review covers modules 8 to 14: inheritance and overriding, dynamic dispatch and upcasting, abstract classes, interfaces with default and static methods, nesting, exception handling with try / catch / finally, throw versus throws plus checked versus unchecked and a user-defined InvalidBoxDimensionException, generics with single and multiple parameters, the collections framework, multithreading life-cycle and priorities, synchronization via monitors (synchronized method and synchronized block), object cloning with shallow versus deep copy, enumerated types, and bounded types (T extends A).
Exam note — how questions are likely formed: Plan for conceptual questions ("define overriding — same name, same return type, same argument count and types — and explain super to reach the parent version", "explain dynamic method dispatch and upcasting — parent reference, child object, runtime choice Parent obj = new Child(); obj.show(); prints child show", "distinguish abstract class — abstract class ABC cannot be instantiated, may hold state and concrete helpers — from interface — interface Printable with implements and multiple inheritance"). Expect code-tracing questions: given Parent obj = new Child(); obj.show(); which show() runs; given CallMe with three callers hello, java, programming predict interleaving with and without synchronized — without gives [hello [java [programming ] ] ], with synchronized void call or synchronized(target){ target.call(msg); } gives [hello] [java] [programming]; given try { int c = a/b; } catch (ArithmeticException e){ } catch (Exception e){ } finally{ cleanup; } with and which catch fires (ArithmeticException first) and that finally always executes whether or . Expect short write-code asks: write a user-defined exception class InvalidBoxDimensionException extends RuntimeException { InvalidBoxDimensionException(double v){ System.out.println("Box instance with invalid dimension: "+v); } } that triggers when any dimension length <= 0 || width <= 0 || height <= 0 via throw new InvalidBoxDimensionException(bad) inside Box's constructor which declares throws InvalidBoxDimensionException and stores double length, width, height; write a generic identity class class Identity<T> { T obj; Identity(T obj){this.obj=obj;} T getObj(){return obj;}} and instantiate Identity<Long>(123L) printing 123 and Identity<String>("hello") printing hello, plus Pair<T,U> with Pair<String,Integer> printing hello 10 and swapped Pair<Integer,String> printing 10 hello; write Bound<T extends A> with A { void displayClass(){...}}, B extends A, C extends A, field T obj, method doRunTest(){ obj.displayClass(); } and note that T extends A admits A,B,C and rejects String. When a question involves try/catch/finally, place the risky c = a/b with inside try, handle specific ArithmeticException before generic Exception, and remember that finally always executes. For cloning, state implements Cloneable, override public Object clone() throws CloneNotSupportedException { return super.clone(); } and handle CloneNotSupportedException, then explain shallow copy shares Date pointers (leak on mutation) while deep copy fixes with cloned.hireDate = (Date) hireDate.clone() and leaves immutable String shared and primitive salary separate. For enums, know that enum Season { WINTER, SPRING, SUMMER, FALL } is accessed as Season.WINTER and iterated with for (Season s : Season.values()) printing WINTER SPRING SUMMER FALL in declaration order. For generics and bounded types, be explicit about the substitution rule T replaced by concrete argument at new time and the exact bound T extends A limits admissible type arguments to A or subclasses B, C.
For the assessment, plan for conceptual questions ("define overriding", "explain dynamic method dispatch", "distinguish abstract class from interface"), code-tracing questions (given Parent obj = new Child(); obj.show(); which method runs; given CallMe with three callers predict interleaving with and without synchronized), and short write-code asks (write a user-defined exception that triggers when a dimension is <= 0; write a generic identity class Identity<T>; write Bound<T extends A>). When a question involves try / catch / finally, place the risky statement inside try, handle specific exceptions first, and remember that finally always executes. For cloning, state implements Cloneable, override clone() with super.clone() and handle CloneNotSupportedException, then explain shallow versus deep handling of mutable fields like Date versus immutable String. For enums, know that enum Season { WINTER, SPRING, SUMMER, FALL } is accessed as Season.WINTER and iterated with for (Season s : Season.values()). For generics and bounded types, be explicit about the substitution rule and the exact bound T extends A limits.
Time constraints prevented covering each slide in full depth; referenced supplemental slides contain additional worked programs for inner classes (member, anonymous, local) and fuller collection demonstrations — reviewing them with the code shown adds clarity. Write assumptions in simple form and present any table or step sequence as shown, since that style aids evaluation.
How to present answers: Write assumptions in simple form and present any table or step sequence exactly as shown (state loops, hierarchy diagrams, call tables) — the evaluation style rewards a visible, ordered trace of each worked example plus a precise earlier rule invocation (for example "overriding requires same signature" or "dynamic dispatch chooses the object type"). When citing a specific exam pattern, name the code shape, the first matching handler, and the expected bolded output line.
Key Industry Applications
Real-world: inheriting from base account or framework classes to reuse fields and behaviour; using parent references to handle many child types through the same call site via runtime polymorphism; modelling contracts with abstract classes and interfaces, and recovering multiple inheritance by implementing several interfaces (e.g., a checking account that extends a bank account and implements a bank contract); evolving published interfaces with default methods so old implementers keep working without re-coding, and exposing utility operations as InterfaceName.staticMethod(); grouping related types with nested classes and interfaces for maintainability, with member, anonymous, and local inner classes as common patterns; handling runtime failures with try / catch / finally, generating domain errors with throw and forwarding checked errors with throws, modelling domain exceptions such as invalid dimensions; building type-safe reusable containers with generics (Identity<T>, Pair<T,U>, ArrayList<T>, LinkedList<T>, Map<K,V>) and restricting them with bounded types (T extends A, T extends Comparable, T extends Number); accessing every data structure uniformly through the collections framework core methods add, clear, contains, isEmpty, size; creating concurrent programs with Thread or Runnable plus public void run(), managing life-cycle states new → running → waiting (via sleep) → terminated, tuning with priorities 1–10 (default 5) and coordinating shared resources with monitors, synchronized methods, and synchronized blocks to avoid interleaving; duplicating state safely with Cloneable and clone() / super.clone(), applying deep copying for mutable fields like Date while leaving immutable String shared; restricting values to a finite safe set with enum such as Size.SMALL or Season.
Industry map — which topic shows up where: Banking and enterprise: BankAccount extends Account hierarchies and CheckingAccount implements Bank, Printable collections of contracts route money operations through polymorphic Account references while reusing balance storage. Platform and API: abstract AbstractList skeletons and interface Collection<T> uniform operations let one fill(Collection<String> c){ c.add(...); } serve ArrayList and LinkedList. Publishing a new default void show() to Printable ships a fallback without rebuilding ten thousand implementers; adding static Comparator.comparing() helpers ships utility without an object. Reliability: try/catch around c = a / b with guarding ArithmeticException, throw new InvalidBoxDimensionException for domain rejection, and throws IOException forwarding for checked I/O keep failures from silently corrupting state. Data and type safety: Identity<T> and Pair<T,U> pattern scales to ArrayList<String>, LinkedList<String>, HashMap<K,V> and is tightened with T extends A or T extends Comparable<T> so SortedList<T> can safely call compareTo. Concurrency: Thread/Runnable with public void run() and states NEW -> RUNNABLE -> WAITING (sleep) -> TERMINATED, priority hint 1..10 (default 5 tuned by getPriority()/setPriority()), plus synchronized monitors on the shared CallMe target that hold through sleep(1000) protect the bracketed critical section. State safety: Cloneable plus super.clone() shallow copy plus selective hireDate deep copy via Date.clone() snapshots configuration while leaving String name economically shared. Exhaustiveness: enum Size { SMALL, MEDIUM, LARGE } and enum Season iterate via Season.values() and for(Season s: ...) grant compile-time closed sets.
In Java practice, multithreading, collections, generics, exception handling, cloning, and enums appear together in production services — a concurrent LinkedList of BankAccount tasks pulled by Thread pool workers that handle IOException via throws, duplicate snapshots via clone() for audit, and iterate Season or Size enums for reporting. The habit threading every section is the same one the preamble named: declare the contract (abstract/interface/Collection<T>/enum/T extends A), implement it exactly once per concrete shape, and guard the shared path (synchronized/try/finally) when many actors converge.
Recap: The 16 concepts form a toolbox — inheritance reuses, polymorphism dispatches, abstract and interface specify, default/static evolve, nesting groups, exceptions guard, generics and bounded types parameterize, collections unify verbs, threads parallelize under monitor protection, cloning duplicates with controlled depth, and enums close the value set. Master each verbally (definitions, signatures) and operationally (which method runs, which monitor is held, which catch fires, what prints) to move from code reading to system building.
OODAP Lecture 26 notes · Review and Revision of Modules 8 to 14
Sections Breakdown
Child inherits parent accessible fields and methods; three views constructors via super, overridden via super.method, normal direct.
Upcasting parent reference holds child object; overridden show resolved at runtime by object type, label vs content.
Abstract class template with abstract methods cannot be instantiated; concrete child must implement all abstracts.
Interface contract with implements; class extends one class but implements many interfaces.
Default method fallback inside interface; static interface method via InterfaceName.method().
Nested class/interface grouped inside another; outer Printable and inner Showable separate contracts.
Exception at c=a/b with b=0 throws ArithmeticException; try catch finally and Throwable hierarchy.
throw generates, throws forwards; checked vs unchecked; InvalidBoxDimensionException extends RuntimeException.
Generic Identity T substitutes at new time; Pair T,U order matters.
Uniform Collection T interface; ArrayList contiguous vs LinkedList scattered via same add.
Process P1 hosts threads T1,T2,T3; created via Thread or Runnable to run().
Priority 1..10 default 5 via getPriority/setPriority; Sum 15 vs Fact 120.
Critical section via monitor lock; synchronized method vs block protect shared CallMe.
Object root; Cloneable super.clone creates distinct cell; shallow shares Date, deep clones.
Enum closed set Size and Season via private constructor static final constants iterated by values().
Bounded T extends A restricts to A or B C; allows obj.displayClass safely, rejects String.
Assessment checklist for overriding, dispatch, abstract/interface, try-catch-finally, throw, generics, bounded, cloning, enum, synchronization.
Domain map banking, API evolution, reliability, data, concurrency, snapshotting, enums.
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
Inheritance and Method Categories
Must-know: Child inherits parent accessible fields and methods; three views constructors via super, overridden via super.method, normal direct.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Dynamic Method Dispatch, Runtime Polymorphism and Upcasting
Must-know: Upcasting parent reference holds child object; overridden show resolved at runtime by object type, label vs content.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Abstract Classes
Must-know: Abstract class template with abstract methods cannot be instantiated; concrete child must implement all abstracts.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Interfaces and Multiple Inheritance
Must-know: Interface contract with implements; class extends one class but implements many interfaces.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Default and Static Methods in Interfaces
Must-know: Default method fallback inside interface; static interface method via InterfaceName.method().
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Nesting of Classes and Interfaces
Must-know: Nested class/interface grouped inside another; outer Printable and inner Showable separate contracts.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Exception Handling
Must-know: Exception at c=a/b with b=0 throws ArithmeticException; try catch finally and Throwable hierarchy.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Throw versus Throws
Must-know: throw generates, throws forwards; checked vs unchecked; InvalidBoxDimensionException extends RuntimeException.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Generics
Must-know: Generic Identity T substitutes at new time; Pair T,U order matters.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Collections Framework
Must-know: Uniform Collection T interface; ArrayList contiguous vs LinkedList scattered via same add.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Multithreading
Must-know: Process P1 hosts threads T1,T2,T3; created via Thread or Runnable to run().
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Thread Priorities
Must-know: Priority 1..10 default 5 via getPriority/setPriority; Sum 15 vs Fact 120.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Thread Synchronization
Must-know: Critical section via monitor lock; synchronized method vs block protect shared CallMe.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Object Cloning
Must-know: Object root; Cloneable super.clone creates distinct cell; shallow shares Date, deep clones.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Enumerated Types
Must-know: Enum closed set Size and Season via private constructor static final constants iterated by values().
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Bounded Types
Must-know: Bounded T extends A restricts to A or B C; allows obj.displayClass safely, rejects String.
⚠️ Top pitfall: See pitfalls in section
Self-check: Explain core concept in own words
Connects to: Related concepts in this lecture
Was this lecture useful?
BitsNotes AI Assistant
Subject Notes AssistantConfigure AI Chat
Choose how to access the chatbotSigned in as
Powered by BitsNotes — 20 messages per day. No API key needed. Want unlimited access? Use "Bring Your Own Key" mode.
Sign in to use AI Chat
Get 20 free AI messages per day to ask questions about your lecture notes. Sign in with Google or GitHub — it takes 5 seconds.
Sign In to BitsNotesSwitch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.