Skip to main content
Object Oriented Design, Analysis and Programming

Java Object Model, Type System, Cloning and Generics

Published: 2026-08-19
Level: postgraduate
Audience: Postgraduate students in Object Oriented Design, Analysis and Programming

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

  • Classes — The Blueprint Idea — covered in Lecture 16: Introduction to Java and Object-Oriented Programming Foundations
  • Objects and Instances — Concrete Values From a Blueprint — covered in Lecture 16: Introduction to Java and Object-Oriented Programming Foundations
  • Inheritance — One Class Acquiring Properties of Another — covered in Lecture 16: Introduction to Java and Object-Oriented Programming Foundations
  • Prototype — Cloning Objects from a Model — covered in Lecture 13: Design Patterns — Gang of Four Solutions
  • Generics — Parameterized Types — covered in Lecture 22: Java Collections Framework and Generics
  • Generic Interfaces — covered in Lecture 22: Java Collections Framework and Generics

# Java Object Model, Type System, Cloning and Generics

24.1 The Object Class — Root of the Java Inheritance Hierarchy

24.1.1 Definition and Position in the Hierarchy

Why can you call toString() on any object you create, even though you never wrote that method? What guarantees that equals, hashCode, and clone exist for every class in your program? The answer lies in a single root class that every Java class inherits from.

Every class in Java, whether it is a predefined library class or a class you write yourself, sits under one common ancestor. That ancestor is the Object class — the topmost class in the hierarchy, the root from which every other class descends directly or indirectly. When you write class Person { } without an explicit extends clause, the compiler treats it as class Person extends Object. If you later write class Employee extends Person, the chain is Employee -> Person -> Object. There is no Java class that escapes this chain. The class lives in java.lang, which is the default package. You never write import java.lang.Object.

Think of Object as the trunk of a family tree. Person is a first branch off the trunk, Employee is a twig off Person, but the trunk still supplies sap to every twig. The inheritance you use with extends is the grafting mechanism; the root is Object itself. Where the analogy breaks: a real tree trunk is wood, while Object is code — its "sap" is methods that the compiler copies as inherited members, not nutrients.

Object as universal supertype. Object is defined in java.lang. It is the direct superclass of any class that declares no extends, and the indirect superclass of every other class. Because of rule 6 of the subtype system (S is a subtype of Object whenever S is not primitive), a variable of type Object can hold a reference to any object value, and rules for assigning subtype values to supertype variables without a cast apply first to Object. The compiler enforces that every class, abstract class, interface, and array type ultimately has Object as an ancestor, and every enum implicitly has Enum then Object in its chain.

Because Object sits at the very top, every method declared in Object is automatically available in every Java class without any import or explicit inheritance. The practical payoff is simple: a framework method that accepts Object x can call x.toString() or x.hashCode() and it will compile for any argument, because the保证 exists at the language level. You can keep the inherited version as it is, or you can provide your own version that overrides the inherited one.

A useful mental check is the class header expansion:

class Person { }                          // compiler reads: class Person extends Object
class Employee extends Person { }         // chain is Employee -> Person -> Object
Object o = new Person();                  // legal: Person is a subtype of Object
Object arr = new int[10];                 // legal: array types are subtypes of Object

Visual intuition: picture a vertical hierarchy diagram. At the top is a box labeled java.lang.Object. Directly below it are two columns: on the left, your classes Person and Employee stacked; on the right, library classes like String and ArrayList. Horizontal arrows for extends point upward to the parent, and the single converging point at the apex is always Object. The takeaway is that there is exactly one apex, so common behavior can live once and be shared everywhere.

24.1.2 Methods of Object Available to All Classes

The Object toolkit. The session listed the most used Object methods and what each one does. All are instance methods, so they work through a reference obj:

  • String toString() — returns a text description of the object. Used automatically when you concatenate: "r=" + r becomes "r=" + r.toString().
  • boolean equals(Object obj) — compares two objects for content equality, one field at a time, rather than comparing reference addresses. The default Object.equals is this == obj, so classes that want logical equality must override it.
  • int hashCode() — returns a hash code derived from the object's storage information. It is not the raw address, but a hash of that address information. Equal objects must have equal hash codes; two references that alias the same storage show the same hash.
  • Object clone() — declared protected in Object. Creates a shallow field-by-field copy and returns it as Object. A class that wants public cloning must widen access and implement Cloneable.
  • Class<?> getClass() — returns the runtime type descriptor (Class object) of the object.
  • protected void finalize() throws Throwable — called once before the garbage collector reclaims the object (now deprecated in modern JDKs, but still exam-relevant for this lecture's context).
  • void wait(), void notify(), void notifyAll() — threading coordination, mentioned only by role.

For array types, getClass().isArray() and getComponentType() add array-specific type inquiry.

A point that avoids later confusion: hashCode() does not give you a street address you can dereference. It gives a compact integer summary of where the object lives, so two objects that live at different places will normally show two different hash codes, while two references that alias the same place show the same hash code. The contract is directional: if x.equals(y) is true, then x.hashCode() == y.hashCode() must be true; the reverse need not hold, because collisions are allowed.

Scope: The methods above are instance methods, so you need an object to call them. They work for any object type, but not for primitive values themselves — int, double, and boolean are not objects. Their wrappers Integer, Double, Boolean are objects and do inherit from Object, so they carry these methods. void is not a type at all; it marks "no value returned." Trying to call Object methods on a null reference throws NullPointerException; null itself is the sole value of the null type, which is a subtype of every non-primitive type.

Pitfalls

  • Confusing == with equals. == on objects checks identity (same box?), equals checks content (same field values?). Using == to compare strings or employees will miss logically equal but distinct objects.
  • Forgetting hashCode when overriding equals. If you define equals to compare names and salaries, you must also define hashCode to combine the same fields (for example 11 * name.hashCode() + 13 * new Double(salary).hashCode() as shown in the textbook). Otherwise HashSet and HashMap will fail to locate equal objects.
  • Calling clone without Cloneable. Even if you write a correct public clone that calls super.clone(), the call throws CloneNotSupportedException at runtime unless the class implements Cloneable.
  • Expecting Object to be instantiated directly for business logic. You can write new Object(), but that instance has no fields of interest. Object's value is as a root and a polymorphic holder, not as a domain entity.

Real-world connection: the universal root is why logging frameworks can declare void log(Object msg) and collections can declare HashSet<Object>. The framework calls msg.toString() or msg.hashCode() without knowing the concrete class. In IDE debuggers, the "Inspect" view calls toString() on whatever selection you have, because the IDE knows Object guarantees that method exists.

24.1.3 Quick Review — Class as Blueprint, Object as Instance

To set up the cloning discussion, the session revisited the first building block: a class is a blueprint or structure, and an object is an instance that fills that structure with values.

Blueprint versus house. class Person is the architect's drawing: it says there will be a name field (itself with firstName and lastName) and an address field (with street and city). No family lives in the drawing. When you write:

Person p = new Person();
p.name.firstName = "Smith";
p.name.lastName = "Jones";
p.address.street = "10 Park Ave";
p.address.city = "Pune";

you have built one house from the drawing and moved a family in. A second statement Person q = new Person() builds a second house from the same drawing with different values. Employee E in the lecture is the same pattern: the class says name and salary exist; the object E stores name = "Smith" and salary = 35000. A different employee object would share the shape but carry name = "Asha" and salary = 42000. Both objects have type Person or Employee, and both have Object methods available.

  • Trace: class Person defines fields but allocates no storage for "Smith".
  • new Person() allocates storage and returns a reference.
  • Assigning field values fills that storage.
  • p == q would be false (two boxes); p.getClass() == q.getClass() would be true (same blueprint).

Recap + bridge. Object is the apex of every inheritance chain in java.lang, so toString, equals, hashCode, clone, getClass, and finalize are available everywhere. A class is the shape; an object is one filled-in example. That distinction matters next, because copying a reference (giving another person the same house key) is not the same as cloning an object (building a second house with the same furniture).

24.2 Object Identity, Reference Assignment and Why Copying Is Not Cloning

24.2.1 What Assignment Actually Copies

If ABC A2 = A1 copied the object, changing A2 would leave A1 untouched. Try that mental test now — does Java work that way, or do both names see the same change?

A frequent early mistake is to treat = as an object copier. It is not. It copies the reference variable, not the object.

Think of an object as a box in a warehouse and a reference variable as a sticky label with an arrow pointing to the box. The assignment A2 = A1 copies the label's arrow, not the box. After the copy you have two labels pointing to one box. Painting inside the box through one label shows through the other label, because there is only one box. The analogy breaks when boxes nest (an Employee contains a reference to a Date box) — then labels can point to outer and inner boxes independently, which is why shallow copying later needs care.

Reference versus object. A reference is the arrow (the variable that holds an address). An object is the box in memory that holds field values. Declaring ABC A1 = new ABC() does two things: new ABC() allocates a fresh box and returns its address; ABC A1 creates a label; = stores the returned address in the label. No other box is created by a later A2 = A1.

The three-label diagram — assignment copying reference A1 at 1000 to A2 and A3.

ABC A1 = new ABC();   // allocate box at 1000, A1 -> 1000
ABC A2 = A1;          // copy arrow: A2 -> 1000
ABC A3 = A1;          // copy arrow: A3 -> 1000
// also shown as myCar1 and myCar2 copying a car object — same diagram
  • Step 1: memory has one box ABC at address 1000 with its fields.
  • Step 2: after A2 = A1, variable A2 holds the value 1000 (the address), not a new ABC.
  • Step 3: after A3 = A1, variable A3 also holds 1000.
  • Count: three variables, one object. The picture is three arrows converging on one box.

Proof of aliasing: suppose the ABC has a field int value set to 10. Run:

A1.value = 99;
System.out.println(A2.value); // prints 99
System.out.println(A3.value); // prints 99

All three reads see 99, because they reach the same storage. If = had copied the object, A2.value would still be 10. The fact that it is 99 shows no new object was created.

Variations that show the same idea: myCar1 and myCar2 are two Car labels. Car myCar2 = myCar1; does not build a second car in the garage.

Sense-check: if you want two independent cars that start with the same mileage but can be driven separately, aliasing fails — one mileage update would move the other car's odometer, which is wrong.

Pitfalls

  • Expecting = to clone. New learners write Employee e2 = e1; e2.salary = 50000; and expect e1.salary to stay 35000. It does not — both labels see the single box, so e1.salary is now 50000 as well.
  • Counting variables as objects. A1, A2, A3 are not three objects. They are three names for one object. The heap count did not change.
  • Using == to test content. A1 == A2 is true here because they share the address, but A1 == A2 being true does not mean two objects have equal fields — it means there is only one object.

Visual intuition: draw a rectangle for the heap, place one small square inside labeled ABC @1000. Draw three dots outside labeled A1, A2, A3. Draw three arrows from each dot to the single square. The takeaway is one target, three incoming edges. Adding a fourth label A4 = A1 just adds a fourth converging arrow; the square count stays at one.

Real-world connection: aliasing shows up in business code when two service layers hold references to the same Order object. A discount applied through one reference appears in the other layer's view. Frameworks sometimes use this on purpose (a cache gives many callers the same live object), and sometimes it is a bug when independent snapshots were intended.

24.2.2 The Problem Assignment Leaves Unsolved

If your goal is two independent objects that start with the same content but can then evolve independently, assignment fails. The session stated it in direct terms: assignment creates a copy of the reference variable, not a copy of the object. To get two boxes at two addresses — say A1 at 1000 and A2 at 2000 — with the same initial field values but distinct identities, you need a different mechanism. That mechanism is cloning.

Scope: Assignment is the right tool when you want sharing — for example, passing a large object to a helper method without copying cost. It is the wrong tool when you need isolation for later mutation, undo/redo snapshots, or prototype creation. In those cases you must allocate a second box and initialize its fields to match the first, which is exactly what a correctly implemented clone does.

Recap + bridge. = copies the arrow at A1 (address 1000) to A2 and A3, so there is one box and three names. Mutations are shared, which proves no new object exists. When you need two boxes at 1000 and 2000 with equal starting content but separate futures, aliasing cannot deliver — the next section builds the cloning mechanism that can.

24.3 Cloning — Creating an Exact Independent Copy

24.3.1 Definition and the Three Conditions Clone Must Meet

How do you get two boxes that start with identical furniture but live at different addresses, so rearranging one living room does not move the other's sofa? Assignment gave you one box with three keys. Cloning builds the second box.

Cloning means creating a new instance of the class of the current object and initializing all its fields with exactly the same contents as the original, but at a different identity. It is the mechanism that turns aliasing (A2 = A1 with one box at 1000) into duplication (two boxes at 1000 and 2000).

The three clone conditions. Written with X for the original and X.clone() for the copy, a correct clone must satisfy:

  1. Reference inequality: X.clone() != X must be true. The two references point to different addresses. With == you test addresses, so a correct clone gives false for ==.
  1. Content equality: X.clone().equals(X) must be true. Field-by-field content compares equal. This assumes the class has a proper equals that compares fields; the default Object.equals would be false for distinct boxes, which is why cloned classes often override equals.
  1. Class identity: X.clone().getClass() == X.getClass() must be true. The new object belongs to exactly the same runtime class as the original. A Rectangle clone is a Rectangle, not a generic Shape.

The session tested condition 1 with == and showed false for a correct clone, and tested condition 2 with equals and showed true. Condition 3 was checked via getClass() comparison.

Concrete check with Employee E. Original E has name = "Smith", salary = 35000; its clone E2 = E.clone() also has name = "Smith", salary = 35000. Verify:

Employee E = new Employee("Smith", 35000);
Employee E2 = E.clone();
System.out.println(E == E2);                    // false — distinct boxes (condition 1)
System.out.println(E.equals(E2));               // true  — same field values (condition 2)
System.out.println(E.getClass() == E2.getClass()); // true — same class (condition 3)
System.out.println(E.hashCode() == E2.hashCode()); // usually false — hash reflects storage
  • E == E2 is false because E is at, say, 1000 and E2 at 2000.
  • E.equals(E2) is true only if Employee.equals compares name and salary; without that override it would be false even though fields match.
  • getClass equality confirms the clone was not sliced to a supertype.

Sense-check: if any of the three is violated, the operation is not cloning — it is either aliasing (violates 1), a different object (violates 2), or a type error (violates 3).

Visual intuition: draw two separate rectangles side by side, left labeled X @1000, right labeled X.clone() @2000. Inside each, write the same field list: name="Smith", salary=35000. Draw a red != between the outer box addresses and a green equals==true between the inner field lists. The takeaway is same contents, different containers.

Scope: Cloning as taught here is for reference types that have chosen to support it. Primitive fields are copied by value automatically. For reference fields, Object.clone gives shallow copying (arrows copied, not inner boxes) — full independence for mutable inner objects needs extra work, which the next section handles. Not every class should be Cloneable; if a class holds an open stream or unique resource, duplicating the reference without thought creates shared-resource bugs.

24.3.2 Requirements — Public Clone and the Cloneable Interface

Two requirements must be satisfied by any class that wants to be cloneable:

  1. The class must declare a clone method that is public. The version in Object is protected, so you must widen access. Otherwise code outside the package cannot call it.
  2. The class must implement the Cloneable interface. The correct Java spelling is Cloneable (you may see informal spellings like clonable). It is a marker interface — it declares no methods of its own — but its presence is the signal that Object.clone() is allowed to copy this class. Without it, a call to clone throws CloneNotSupportedException.

Why super.clone() is the center. You do not build the copy field by field by hand in the common case. You delegate to the ancestor implementation with super.clone(). Because Object is the root ancestor, super in this context means the Object class, and super.clone() invokes the native Object.clone that creates the shallow field-by-field copy at the platform level. Your override then adjusts the result (cast, deep copy of mutable fields) and returns it. Trying to use new CloneA() plus manual field assignments would work for simple cases but misses the native allocation path and is error-prone when subclasses add fields.

Because cloning can fail — for example, trying to clone an object through a reference of an unrelated class — the call to super.clone() must sit inside a try/catch for CloneNotSupportedException, or the enclosing method must declare throws CloneNotSupportedException. Both styles appeared in the lecture.

Minimal Account template — two handling styles for CloneNotSupportedException.

// Style A: handle inside (catch)
class Account implements Cloneable {
    public Object clone() {
        try {
            return super.clone(); // calls Object.clone, creates field-by-field copy
        } catch (CloneNotSupportedException e) {
            return null; // only reached if Cloneable was missing
        }
    }
}

// Style B: forward to caller (throws) — same call, different contract
class CloneA implements Cloneable {
    public CloneA clone() throws CloneNotSupportedException {
        Object o1 = super.clone();
        return (CloneA) o1; // cast because Object.clone returns Object
    }
}
  • Both call super.clone() — "the clone method of Object, the universal root."
  • Account keeps return type Object and avoids a cast on the caller side at the cost of a catch block.
  • CloneA narrows return type to CloneA and moves exception duty to the caller with throws.
  • Forgetting implements Cloneable makes super.clone() throw at runtime even though the code compiles.

Trade-off: narrow return types give cleaner call sites (CloneA c = a.clone() without cast) but require covariance allowed since Java 5; general return types avoid casting inside the method. The lecture showed both and stated both are valid — pick one and be consistent within a project.

Pitfalls

  • Misspelling Cloneable. Clonable, Cloneble, and clonable do not compile or do not match the marker. The interface name is exactly Cloneable.
  • Keeping clone protected. If you override as protected Object clone(), code in another package cannot call a.clone(). Widen to public.
  • Swallowing the exception silently. catch (CloneNotSupportedException e) { return null; } hides a programming error if you forgot implements Cloneable. A better catch for debugging is to throw an AssertionError since the exception should never happen when the marker is present.

24.3.3 Worked Examples — Clone Class A and Clone Class B

The session built two complete programs to show how the pattern is used from the caller side and how inheritance interacts with Cloneable.

Example 1 — Class CloneA with super.clone and CloneNotSupportedException handling.

class CloneA implements Cloneable {
    public CloneA clone() throws CloneNotSupportedException {
        Object o1 = super.clone();       // Object.clone creates the copy at native level
        return (CloneA) o1;              // cast from Object to CloneA
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        CloneA a = new CloneA();
        CloneA cloneA = a.clone();       // cloneA is the cloned copy of a
        System.out.println(a == cloneA);          // false — different addresses
        System.out.println(a.equals(cloneA));     // true if equals is overridden, else false
        System.out.println(a.getClass() == cloneA.getClass()); // true
    }
}

Walkthrough:

  1. implements Cloneable satisfies the marker requirement, so Object.clone is permitted.
  2. public CloneA clone() throws CloneNotSupportedException widens visibility and forwards the checked exception instead of catching locally.
  3. super.clone() allocates a new CloneA box with the same field values as a, but at a new address (say a at 1000, o1 at 2000). The static type is Object.
  4. (CloneA) o1 narrows the static type so the method can return CloneA and callers avoid an extra cast.
  5. CloneA cloneA = a.clone() stores the second box's address in a new label. Now a and cloneA have equal content, different identities.

Return-type note: returning Object would also work and would avoid the inner cast, but then main would need CloneA cloneA = (CloneA) a.clone();. The object created is the same; only the compile-time type differs.

Example 2 — Class CloneB extends CloneA showing hashCode difference H1 not equal H2.

class CloneB extends CloneA {
    public CloneB clone() throws CloneNotSupportedException {
        Object o2 = super.clone();       // still reaches Object.clone through the chain
        return (CloneB) o2;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        CloneB b = new CloneB();
        CloneB clonedB = b.clone();
        System.out.println(b.hashCode());       // e.g. 366712145
        System.out.println(clonedB.hashCode()); // e.g. 1829164700 — different number
        System.out.println(b == clonedB);       // false
        System.out.println(b.hashCode() != clonedB.hashCode()); // true — H1 != H2
    }
}

Why CloneB does not need implements Cloneable again: interfaces are inherited, so CloneA's marker flows to CloneB. super.clone() starts in CloneB, goes to CloneA.clone(), which ultimately calls Object.clone — the chain still reaches the root.

The two hashCode prints are the identity check. hashCode() hashes the storage information, not the exact address, but two objects at different addresses normally yield different integers. The session reported the two numbers as completely different, confirming b at, say, 1000 and clonedB at 2000 with H1 != H2. If hashCode had been equal, it would suggest aliasing, not cloning.

Comparison within the example: CloneA vs CloneB — the code shape is identical (call super.clone, cast, return). The difference is that CloneB must cast to CloneB and inherits the marker, while CloneA declared it. Both use the throws style; the Account example used the try/catch style. The lecture kept total behavior the same across styles to stress that exception handling is the only variance.

Q: What happens if I try to clone an object of class ABC by holding it in a variable of an unrelated class XYZ and calling clone?

A: That path throws CloneNotSupportedException. The exception signals that the runtime does not have permission to copy this type — most often because the class did not implement Cloneable or because the reference does not point to a cloneable instance. The design is intentional: Object.clone checks instanceof Cloneable at runtime and throws if the check fails, so a class must opt in. To handle it, either wrap super.clone() in try/catch (CloneNotSupportedException e) and handle or rethrow as an unchecked exception, or declare throws CloneNotSupportedException on your clone method and let the caller deal with it. Both keep the program from terminating abruptly and make the failure explicit.

Source label: *[24.3.qna.1 — related class XYZ throws CloneNotSupportedException]*

Q: Why is the return type sometimes Object and sometimes the class itself, like CloneA?

A: Both are correct and create the same object. Object.clone() is declared to return Object, the most general type, so keeping public Object clone() avoids a cast inside your method but forces a cast on every call site. Narrowing to public CloneA clone() with return (CloneA) super.clone(); inside does the cast once, in the source class, so callers can write CloneA x = a.clone(); without a cast. Since Java 5, covariant return types allow this narrowing when overriding. Choose one style per codebase: keep Object when you want the minimal override, narrow to the class type when you want call-site convenience.

Source label: *[24.3.qna.2 — return type Object versus CloneA]*

Pitfalls for this concept

  • Calling new instead of super.clone inside clone. return new CloneA(); creates a default object, not a field-by-field copy. Fields added later by subclasses will be missed.
  • Forgetting to cast after super.clone when narrowing. return super.clone(); does not compile when the method declares CloneA return; you must write return (CloneA) super.clone();.
  • Expecting CloneNotSupportedException to be unchecked. It is checked, so main or the caller must either catch or throws. The lecture showed both fixes.

Exam note: Be ready to state the three clone conditions with !=, equals, and getClass, to draw the 1000 vs 2000 address picture with == false and equals true, and to implement public clone with Cloneable and super.clone handling CloneNotSupportedException via either throws or try/catch on demand.

Real-world: cloning is the basis of the Prototype pattern — a graphics editor clones a Shape prototype to create new shapes with preset style, or a web request handler clones a template Employee record before applying a tentative raise, so the original stays untouched if validation fails.

Recap + bridge. Cloning creates a second box with equal fields but a different address, satisfying !=, equals, and getClass equality. It requires public clone, implements Cloneable, and super.clone() with checked-exception handling. CloneB inherits the marker and proves independence via H1 != H2. The default copy is shallow, however — the next section shows why a Date field needs explicit deep copying while a String field does not.

24.4 Shallow Copy versus Deep Copy

24.4.1 Shallow Copy — What Object.clone Gives You by Default

When Object.clone copies an Employee, does it duplicate the Date object on the calendar or just the bookmark that points to the calendar?

The Object.clone() path performs a shallow copy. It copies the top-level object and copies each field value as it is. For a primitive field like int salary, the value 35000 is duplicated by value, so the two employee boxes hold independent 35000s. For a reference field like String name or Date hireDate, the reference itself — the arrow — is duplicated, not the object it points to.

Shallow means share the inner boxes. After Employee cloned = (Employee) E.clone() via shallow super.clone(), the heap has two outer employee boxes (E at, say, 1000 and cloned at 2000) whose fields were copied arrow-by-arrow. int salary is independent because primitives live inside the box. name and hireDate are arrows that now both point to the same inner boxes. The verification condition E != cloned holds, and E.getClass() == cloned.getClass() holds, but the inner sharing decides whether aliasing bugs appear.

Employee shallow copy with String at 1064 and Date at 2044 shared references.

class Employee implements Cloneable {
    String name;    // reference to String box
    int salary;     // primitive
    Date hireDate;  // reference to mutable Date box
    public Employee clone() throws CloneNotSupportedException {
        return (Employee) super.clone(); // shallow only
    }
}

Employee E = new Employee();
E.name = "Smith";          // String at 1064
E.salary = 35000;
E.hireDate = new Date();   // Date at 2044, e.g. 15 Jan 2020
Employee cloned = E.clone();

Heap after shallow copy:

  • E at 1000: salary=35000, name -> 1064 ("Smith"), hireDate -> 2044.
  • cloned at 2000: salary=35000, name -> 1064 (same arrow), hireDate -> 2044 (same arrow).

Diagram: two outer squares with three slots each; salary slots each contain 35000 directly; name slots each have an arrow converging on one String circle at 1064; hireDate slots each have an arrow converging on one Date circle at 2044.

Why the two reference types behave differently despite the same sharing:

  • String is immutable — once the object at 1064 holds "Smith", no method can change that character sequence. You can make E.name point to a different String, but you cannot change the characters inside 1064. Sharing is harmless because neither owner can mutate the shared box.
  • Date is mutableE.hireDate.setTime(nextWeek) changes the field values inside the Date box at 2044. Because cloned.hireDate points to that same box, the clone sees the changed date without any assignment to cloned.

Trace that proves coupling:

System.out.println(cloned.hireDate); // e.g. 15 Jan 2020
E.hireDate.setMonth(5);              // mutate through E
System.out.println(cloned.hireDate); // now June — changed via E's arrow
System.out.println(E.name == cloned.name);       // true — shared String
System.out.println(E.hireDate == cloned.hireDate); // true — shared Date

Sense-check: if shallow copy had duplicated inner objects, mutating E.hireDate would leave cloned.hireDate unchanged. The fact that it does change confirms arrows were copied, not boxes.

Visual intuition: picture a photocopier that copies a folder cover and staples copies of the sticky notes that say "see document at 1064." The documents themselves are not photocopied. For a String document laminated in plastic, sharing the original is fine; for a pencil-written Date note, two people sharing one note will overwrite each other's edits.

Scope: Shallow copy is what Object.clone guarantees. It is sufficient when all reference fields are immutable (String, Integer, LocalDate) or when you intentionally want shared inner objects (for example, many Employee objects sharing a single Department object). It is insufficient when a mutable field must be independent. The decision per field depends on mutability, not on type alone.

Pitfalls for shallow copy

  • Assuming primitives and references are treated the same. They are not — primitives copy values, references copy arrows.
  • Thinking == on field String means independent copy. == on references checks sharing; true means one box, two arrows. With strings, true is safe but still sharing.

24.4.2 Deep Copy — Copying the Referenced Objects as Well

A deep copy is a fully independent copy that duplicates the entire reachable object graph, including the objects that reference fields point to. In the textbook terms, it is a "sufficiently deep" copy — deep enough for correctness.

Deep means copy the inner mutable boxes too. You start from the shallow result and then explicitly clone each mutable field you need to isolate. After cloned.hireDate = (Date) hireDate.clone(), the two employees have distinct Date storage, so mutation through one no longer leaks through the other. Immutable fields can safely stay shared, which saves work and memory.

Employee deep copy explicit cloned hireDate at 3024 independent Date objects.

class Employee implements Cloneable {
    String name;
    int salary;
    Date hireDate;

    public Employee clone() throws CloneNotSupportedException {
        Employee cloned = (Employee) super.clone(); // shallow step: 1000 -> 2000
        cloned.hireDate = (Date) hireDate.clone();  // explicit deep step for mutable field
        return cloned;
    }
}

Employee E = new Employee("Smith", 35000, new Date()); // name@1064, hireDate@2044
Employee cloned = E.clone();

State after deep copy:

  • Shallow step: cloned at 2000 with cloned.name -> 1064, cloned.hireDate -> 2044 (shared).
  • Deep step: hireDate.clone() allocates a fresh Date at 3024 whose fields equal the original's fields, then cloned.hireDate -> 3024 replaces the shared arrow. cloned.name remains -> 1064.

Addresses: outer boxes at 1000 and 2000, one shared String at 1064, two distinct Date boxes at 2044 (owned by E) and 3024 (owned by cloned).

Independence proof:

System.out.println(E.hireDate == cloned.hireDate); // false — now two Date boxes (3024 vs 2044)
System.out.println(E.name == cloned.name);          // true — still one String (safe)
E.hireDate.setYear(2025);
System.out.println(cloned.hireDate.getYear()); // still 2020 — no leak

The lecture framed this as three distinct addresses now existing where previously two were shared (1000 outer original, 2000 outer clone, plus 1064 string shared and 2044/3024 date separated).

Extra case — chain of mutability: if Date itself held a mutable field that you also need isolated, you would deep-copy that field inside Date.clone() as well. The rule recurses: clone every mutable piece you need independent; leave immutable sharing in place.

Q: If shallow copy already duplicates the top object, why do we still see a problem?

A: Because duplication stops at the first layer. The arrows inside the top object are copied, but not the boxes those arrows point to. When you duplicate an Employee, the salary slot gets its own 35000, but hireDate gets a copy of the arrow to the Date at 2044, so both employees hold tickets to the same Date room. When that room is mutable, two owners of the same ticket can surprise each other by repainting the room — E.hireDate.setTime(...) changes what cloned.hireDate sees. Deep copy replaces sharing with an additional copy of the box (hireDate.clone() at 3024), so each employee owns a private room.

Source label: *[24.4.qna.1 — shallow copy Date mutable coupling]*

Q: Do I need to deep copy every field?

A: No. Copy every mutable reachable object you want to be independent. Immutable objects like String, Integer, Boolean, and modern java.time.LocalDate can stay shared — the diagram keeps two arrows to one String box at 1064 without risk, because neither owner can change the string's characters. If a field is final and immutable, sharing is not a bug — it is efficient. For Employee, that means keep name shared and clone hireDate. In general, walk the reference graph: for each outgoing arrow, ask "could someone mutate this target and surprise a clone?" If yes, add cloned.field = (Type) field.clone(); for that arrow.

Source label: *[24.4.qna.2 — String immutable Date mutable deep copy]*

Scope — when to pick which: Choose shallow when outer independence is enough, or when inner objects are immutable or intentionally shared (e.g., shared Department where a name change should be visible to all employees). Choose deep when inner mutation must be isolated (date, list, mutable address object). The cost of deep copy is extra allocations at 3024-like addresses and the need for each inner type to be Cloneable with its own correct clone.

Pitfalls

  • Forgetting to clone a mutable field. A single super.clone() without the extra hireDate.clone() line leaves hidden coupling that shows up only after a later mutation.
  • Deep-copying immutable fields unnecessarily. Creating new String(name) wastes memory; leave String shared.
  • Missing subclass mutable fields. Manager extends Employee with a Date promotionDate must override clone and also clone promotionDate; inheriting Employee.clone alone does not isolate the subclass state.

Recap + bridge. Shallow copy via super.clone() duplicates the outer box at 2000 but shares inner arrows to 1064 (String) and 2044 (Date). Sharing a String at 1064 is safe because it cannot be mutated; sharing a Date at 2044 is unsafe until you add cloned.hireDate = (Date) hireDate.clone() to create a private copy at 3024. The next section moves from copying logic to lifetime: once objects are no longer needed, how the runtime decides they are eligible for collection and when it actually reclaims them.

Exam note: Expect to label a given clone as shallow or deep, to state what happens to hireDate after E.hireDate.setMonth(...) in each case, and to write the deep-copy line cloned.hireDate = (Date) hireDate.clone(); with the String versus Date mutability justification.

24.5 Garbage Collection and Object Lifecycle

24.5.1 What Garbage Collection Does

If every new needed a matching free written by hand, what happens when a method forgets one free on an error path, or when two labels share one box and one caller frees it too early?

Garbage collection is the automatic reclamation of memory occupied by objects that are no longer reachable. When an object has no live reference pointing to it — no arrow from any variable, array slot, or field still reaches it — it is unreferenced and becomes eligible for collection. The runtime can then free the memory so it can be reused, without the programmer calling a free operation.

Eligibility versus collection. Eligible means "no incoming reference, so the collector is allowed to reclaim." Collected means "the collector has actually run and reclaimed." The session framed this as a continual efficiency task: as a program runs, some objects go out of use; instead of requiring the programmer to free each one by hand, a collector runs — at a time the Java Virtual Machine (JVM) chooses — and sweeps up the unreachable ones. The gap between the two states is intentional: the JVM batches work, runs when memory pressure or pause policy suggests it, and may delay collection even though objects are already eligible.

Think of the heap as a whiteboard room. Allocating new Test() writes a note on the board and pins it up with a string (the reference). Cutting the string (nulling, reassigning, or never tying one) leaves the note hanging by nothing — it is still ink on the board but no one can read it. Eligible means the note has no strings; collected means the janitor has erased it to make space. The janitor chooses when to erase based on overall board fullness, not instantly when one string is cut. The analogy breaks: real GC traces reachability from roots (stack, statics), not just by counting strings, so cycles of references with no outside root are still collectible.

Visual intuition: a directed graph with nodes for objects and edges for references. Roots are stack variables and static fields at the top; reachable nodes are those with any path from a root. White nodes with zero incoming paths are eligible. The collector's job is to erase white nodes. The takeaway is that reachability, not reference count alone, decides eligibility.

24.5.2 Three Ways an Object Becomes Unreferenced

The lecture listed three concrete patterns that make an object eligible, each framed as "available for garbage collection," not "destroyed immediately."

Pattern 1 — Nulling the reference.

ABC a = new ABC();  // a -> object at, say, 1000
a = null;           // arrow severed, box at 1000 now has no incoming edge

State change: before null, reference set includes a; after, no variable points to 1000. The object is unreachable, so it moves from "live" to "eligible." If another variable b = a had existed before the null, b would still keep it live — eligibility needs zero inbound edges.

Pattern 2 — Reassigning a reference to another object.

Test t1 = new Test(); // t1 -> box A (alive)
Test t2 = new Test(); // t2 -> box B (alive)
t2 = t1;              // t2 now -> A; B loses its sole arrow

Trace:

  • Heap has two boxes A and B, each with one incoming arrow.
  • t2 = t1 copies the arrow value (as in 24.2) so both labels point to A.
  • Box B now has no incoming arrow, so B becomes eligible even though t2 still holds an object (it is A, not B). The same = that earlier created aliasing now orphans a box.

Pattern 3 — Anonymous object.

new Test(); // allocate box C, return its address, but store it nowhere

No variable, no field, no array slot holds the address, so C is unreachable from the start. The memory it occupies is available for collection as soon as the collector runs. Common look-alike: new String("hi"); with no assignment creates an anonymous string object that is immediately eligible (though string interning nuances apply in practice, the exam model treats it as eligible).

Each bullet uses Test or ABC as generic class names on purpose — the mechanism is independent of class.

Sense-check: printing t2 after t2 = t1 shows the value from t1, not the old t2 value, confirming the old box was abandoned.

24.5.3 Finalize and Explicit GC Signaling

Two additional mechanisms were covered that sit beside eligibility:

finalize() — declared as protected void finalize() throws Throwable in Object. It is called once, just before the collector reclaims the object, when the object has no more references. You can override it to dispose system resources, perform cleanup, and reduce memory leaks. Important properties:

  • Runs at most once per object. After it completes, the object is on its way to being reclaimed; calling it a second time has no meaning.
  • The call originates from the collector thread, at a time the JVM chooses, not at the moment the last reference disappears.
  • In modern Java the method is deprecated because cleanup should use try-with-resources or Cleaner, but the lecture retains it for exam and lifecycle understanding.

Example override:

class Resource implements AutoCloseable {
    protected void finalize() throws Throwable {
        try { cleanupNativeHandle(); }
        finally { super.finalize(); }
    }
    public void close() { cleanupNativeHandle(); }
}

System.gc() / Runtime.gc() — declared as public static void gc() in System and as public void gc() in Runtime. Calling it is a request to the JVM to perform cleanup now. The lecture was careful to say the JVM decides whether this is the best moment based on available resources and timing; the call is a hint, not a command that forces immediate collection. If you have already nulled or orphaned objects, System.gc() hints that reclaiming now would be useful.

ABC a = new ABC();
a = null;
System.gc();      // hint: "an eligible object exists"
Runtime.getRuntime().gc(); // same hint via singleton

Scope: System.gc() is not a guarantee. The JVM may ignore it, delay it, or run a small nursery collection instead of a full one. Never write correctness logic that assumes "after System.gc(), my finalize has run." For deterministic cleanup, call close() explicitly.

Pitfalls

  • Thinking finalize always runs. If the JVM exits without memory pressure, objects may never be finalized. Do not rely on it to flush persistent state.
  • Confusing finalize with destructor timing. In C++ a destructor runs when the stack frame ends; in Java finalize runs when the heap collector reaches the object — the gap can be seconds or longer.
  • Assuming System.gc() frees memory instantly. Free memory numbers from Runtime.freeMemory() may not change right after the call, because the collector may schedule later.

Recap + bridge. Objects move from live to eligible by nulling, reassigning so an old box loses its sole arrow, or creating an anonymous object with no arrow at all. finalize() runs once, at most, just before reclaim, and System.gc() / Runtime.gc() are hints the JVM may defer. Eligibility is immediate; actual erasure is the collector's schedule. Next, the session shows how to observe that schedule's effect on heap size through the Runtime singleton.

Real-world: long-running services avoid slow memory growth by nulling or removing cached objects when entries expire, and by avoiding patterns that create many short-lived anonymous objects inside tight loops. For leak hunting, engineering teams snapshot Runtime.freeMemory() and totalMemory() before and after a workload, request System.gc(), and watch whether free space returns to baseline.

24.6 Java Runtime Classes and Memory Introspection

24.6.1 The Runtime Environment and Why a Runtime Class Exists

If Java is platform independent, where does platform-dependent information like free heap live, and how does a portable program ask for it?

Java is described as platform independent because it separates the language from the machine through the Java Virtual Machine and the Java Runtime Environment. The source compiles to bytecode, the bytecode runs on the JVM, and the JVM talks to the operating system. To let a portable program ask environment questions — how much memory is free, how much is total, when to run the collector, how to launch a process — the standard library provides java.lang.Runtime.

The Runtime singleton. Only one Runtime instance exists per Java application, because there is only one JVM instance per process. You do not write new Runtime() — its constructor is private. You obtain the single instance with Runtime.getRuntime() and then call instance methods on it. The pattern is Singleton: one object server for the whole process.

Runtime r = Runtime.getRuntime(); // the only Runtime object
long free = r.freeMemory();
r.gc();           // hint at GC via the same singleton
Process p = r.exec("notepad"); // launch external process (when permitted)

Runtime.getRuntime() returns the same reference each time: Runtime.getRuntime() == Runtime.getRuntime() is true.

Scope: Runtime reflects the heap as seen by the JVM, not the entire machine's RAM. totalMemory() is what the OS has given the JVM so far, not the physical RAM total. freeMemory() is the free portion of that granted heap. On a constrained container, total may grow over time up to -Xmx; on a quiet run it may stay small. Never interpret these as system-wide diagnostics without also checking OS metrics.

Pitfalls

  • Trying new Runtime(). The constructor is private — the line does not compile. Always go through getRuntime().
  • Assuming one Runtime per thread. It is per JVM, not per thread. All threads share the same memory counters.

Visual intuition: picture a computer chassis. Inside, a large rectangle is the OS memory. A shaded sub-rectangle inside it is the JVM heap. Runtime is the small status LCD on the heap's frame that reports "total allocated" and "free inside." The LCD does not show outside-the-heap RAM; it shows what the JVM currently owns.

24.6.2 Memory Queries and Worked Calculations

Three methods formed a small example chain that the session tied directly to garbage-collection intuition:

  • public long freeMemory() — amount of free memory in the JVM heap, in bytes (long because heaps can exceed Integer.MAX_VALUE).
  • public long totalMemory() — total memory allocated to the JVM by the OS so far, in bytes.
  • Derived used memory — computed as used = total - free. Also derived max approximations via freeMemory + used.

The memory identity. At any instant: total heap owned = live + free, where live is what survives GC and free is reclaimable. So usedBytes = totalMemory() - freeMemory() estimates live + unreclaimed garbage. After a GC hint, re-measurement gives a better live estimate because garbage has been swept into free.

Runtime freeMemory 266421656 bytes and totalMemory used 1 MB calculation.

The session walked through live numbers printed on screen. Recreating the trace:

class ABC {
    public static void main(String[] args) {
        Runtime r = Runtime.getRuntime();
        long free = r.freeMemory();  // e.g. 266421656
        long total = r.totalMemory(); // e.g. total bytes reported next
        System.out.println("Free: " + free);
        System.out.println("Total: " + total);
        System.out.println("Used bytes: " + (total - free));
        System.out.println("Used MB: " + (total - free) / (1024 * 1024));
    }
}

Given numbers reported: freeMemory = 266421656 bytes in one run. A later total read is larger. Combining them:

long free = 266421656L;                // bytes free
long total = 267470232L; // example total that makes used ≈ 1 MB (session reported 1 MB used)
long usedBytes = total - free;        // 1048576 bytes
long usedKB = usedBytes / 1024;       // 1024 KB
long usedMB = usedKB / 1024;          // 1 MB

Steps shown on board:

  1. Read freeMemory = F bytes (e.g. 266421656).
  2. Read totalMemory = T bytes.
  3. Compute U = T - F bytes.
  4. Compute U / 1024 = kilobytes, then / 1024 = megabytes.
  5. Print the megabyte value — the session showed 1 MB used.

Converting bytes to megabytes always uses two divides because 1 KB = 1024 B and 1 MB = 1024 KB = 1024*1024 B. So MB = bytes / (1024 * 1024). With the textbook MemoryDemo style, initial free after gc was about 841424 bytes larger than the used estimate, illustrating that roughly 17 KB was consumed by the integer array allocation between measurements.

Extra drill for practice (swap numbers and verify): if free = 751392 and total = 1048568 (from the MemoryDemo trace in Exploring java.lang), used = 297176 bytes; 297176 / 1024 / 1024 ≈ 0.28 MB. After GC the free rose to 841424, used dropped, matching reclaimed garbage from discarded Integer boxes.

Sense-check: usedMB must be smaller than totalMemory()/ (1024*1024). If you compute a negative used, you swapped the subtraction.

Other Runtime capabilities mentioned in passing include executing a process with exec(String) and invoking the garbage collector with gc(), both accessed through the same singleton. The session treated these as the same gateway idea: the JVM environment is a single place you ask about resources.

Pitfalls for the calculation

  • Measuring before GC and calling it "live." Free-before includes garbage not yet swept, so total - free overstates live. Call r.gc() and re-measure to approximate live after sweep.
  • Integer overflow. freeMemory returns long; storing in int truncates heaps above 2 GB.
  • Assuming stable numbers. Each run's freeMemory differs due to JIT, class loading, and GC timing. The exam expects you to apply the formula, not to memorize 266421656 as a constant.

Recap + bridge. Runtime is a singleton reached via getRuntime() that reports freeMemory() and totalMemory() in bytes; used = total - free and usedMB = used / (1024 * 1024) gave 1 MB used with free ≈ 266421656 in the demo. Those numbers let you size caches and trace leaks, and they directly tie to the GC eligibility discussion before and the type-system theory that follows — all part of the Java object model the JVM manages.

Real-world: production services log Runtime.getRuntime().freeMemory() and totalMemory() periodically to track heap pressure, to decide cache eviction, and to alert when free falls below a threshold. Operators size heap with -Xms/-Xmx and watch these counters after deployments to verify GC tuning.

24.7 Java Type System

24.7.1 What a Type Is

When you say int x = 5; and later x + y, what rule decides that + is allowed, and what rule would stop x.equals(y) from compiling?

A type is a set of values together with the set of operations that can be applied to those values. For a class type Account, the values are the possible states of its data members (every distinct combination of field values that could exist), and the operations are the methods you can call on an account — deposit, getBalance, toString. The same pairing applies to primitives, where the values of int are the bit patterns and the operations include +, -, *, /, ==.

Why types matter. Java is strongly typed: the compiler checks that each operation is legal for the variable's type, and the virtual machine checks casts at runtime. That lets the compiler catch Employee e = new Employee(); e.clear(); if clear is not defined for Employee at compile time, rather than later at deployment with an unforeseen value. A variable Employee e can hold only references to Employee or its subclasses, so every operation on e must be valid for that family.

An analogy: a type is like a board game box. The values are the board positions reachable in that game; the operations are the moves the rulebook allows. A chess box allows knight moves; a checkers box does not. Swapping boxes changes both positions and legal moves.

Scope: "Type" in this lecture means the Java language type system, not UML types or database types. The session uses it to explain variable assignments, method applicability, and subtype rules. It does not cover generic type bounds here (those come later with bounded types).

Pitfalls

  • Confusing type with class. Every class is a type, but types also include interfaces, arrays, primitives, and null type. A variable's type tells you the contract; the object's class tells you the implementation.

24.7.2 The Six Kinds of Type in Java

The session enumerated the complete non-generic type inventory from the language spec:

  • Primitive typesint, short, long, byte, char, float, double, boolean. These are the built-in value types you already know. No heap object, no Object methods, no null.
  • Class types — every class is a type. If you write class ABC or class XYZ, the name ABC and the name XYZ each denote a type whose values are the possible objects of that class and whose operations are its methods.
  • Interface types — an interface is also a type, even though you cannot instantiate it directly with new. Its values are references to objects of classes that implement it.
  • Array types — an array such as int[] or String[] is its own type with indexing arr[i], length field, and inherited Object methods. Component type matters for subtyping.
  • Null type — the type of the null value. The spec defines it as the type with the single value null, so every value, including null, belongs to exactly one type. null is a subtype of every non-primitive type, which is why you can write String s = null; but not int x = null;.
  • Void is not a typevoid is a return-position marker meaning "no value is returned," not a set of values with operations. Do not list void as a sixth type alongside primitives.

Recognizing each kind in code.

int n = 13;                   // primitive type value
Rectangle r = new Rectangle(); // class type value
Shape s = r;                  // interface type variable holding Rectangle
String[] words = {"hi"};      // array type
Object o = null;              // null type value stored in Object variable
void m() { return; }          // void marks no value, not a type of value
  • n holds a 32-bit integer value, operation n + 1 is legal.
  • r holds a reference to a heap object, operation r.getClass() is legal.
  • s is typed as Shape but at runtime holds a Rectangle because Rectangle is a subtype of Shape.
  • words.length and words[0] are the array-type operations.

Visual intuition: draw five columns labeled Primitive, Class, Interface, Array, Null. Under each, place example tokens: 42, new ABC(), Shape variable, int[], null. Cross out void placed outside the table with a "not a type" note. The takeaway is that every expression you write lands in one column, and null has its own column.

24.7.3 Supertypes and Subtypes

When B extends A, or when B implements an interface A, the inheritance relation becomes a type relation with rules for substitution:

  • A is a supertype of B — it is the parent in the hierarchy, with members that the child can use when they are public or protected.
  • B is a subtype of A — it inherits members and can directly call methods of the supertype.

The lecture phrased this as "A is supertype of B, B is subtype of A" and noted it is the same hierarchy you already know from inheritance, now read as a statement about types rather than just about classes.

Complete subtype rules for non-generic types. S is a subtype of T if any of these holds:

  1. S and T are the same type.
  2. Both are class types and S is a direct or indirect subclass of T.
  3. Both are interface types and S is a subinterface of T.
  4. S is a class type, T is an interface, and S or a superclass of S implements T.
  5. Both are array types and the component type of S is a subtype of the component type of T.
  6. S is not primitive and T is Object.
  7. S is an array type and T is Cloneable or Serializable.
  8. S is the null type and T is not primitive.

From these, JButton is a subtype of Component by rule 2 through AbstractButton, JComponent, Container; int[] is a subtype of Object by rule 6; JButton[] is a subtype of Component[] by rule 5. Note int is not a subtype of long, and int[] is not a subtype of Object[] — primitives and their arrays form separate hierarchies.

Illustration with the textbook figure: Object at the top, Component -> Container -> JComponent -> AbstractButton -> JButton vertically. LayoutManager interface with LayoutManager2 subtype, and FlowLayout implementing LayoutManager. The point is that subtype edges follow both extends and implements.

Scope — what substitution guarantees: You can assign a value of a subtype whenever a supertype value is expected without a cast: Component c = new JButton(); is legal, and c can call Component methods. The reverse needs a cast and a runtime check: JButton b = (JButton) c; succeeds only if c actually holds a JButton.

Pitfalls

  • Treating arrays as covariant without the runtime check. Rectangle[] r = new Rectangle[10]; Shape[] s = r; compiles by rule 5, but s[0] = new Polygon(...) compiles yet throws ArrayStoreException because every array remembers its component type.

24.7.4 What Can Appear on the Right-Hand Side of an Instantiation

Given a type X and an object x1, what expression can you legally place on the right of X x1 = ...? The answer depends on what X is, and the session returned to this table several times to link declared type versus actual runtime type:

  • If X is an interface — you cannot write new X() because interfaces cannot be instantiated directly (they have no constructor body to allocate). The right-hand side must be an instance of any class that implements X. Writing X x1 = new ClassThatImplementsX() is the correct shape.
  • If X is an abstract class — you again cannot write new X() directly, because abstract classes leave methods unimplemented. The right-hand side must be an instance of any concrete (non-abstract) subclass that extends X. Only the concrete child can be created.
  • If X is a concrete class — the right-hand side can be new X() itself, or new SubclassOfX() for any subclass of X.

Applying the table.

interface Shape { void draw(); }
abstract class AbstractShape implements Shape { }
class Rectangle extends AbstractShape { void draw() {} }
class Circle extends AbstractShape { void draw() {} }

Shape s1 = new Rectangle();        // legal — Rectangle implements Shape
Shape s2 = new Circle();           // legal — Circle implements Shape
// Shape s3 = new Shape();         // illegal — interface cannot be new'd
// AbstractShape a1 = new AbstractShape(); // illegal — abstract cannot be new'd
AbstractShape a2 = new Rectangle(); // legal — concrete subtype
Rectangle r1 = new Rectangle();     // legal — same class
Rectangle r2 = new Rectangle();     // Rectangle r = new Circle(); // illegal — sibling, not subtype
Shape s4 = new AbstractShape() { void draw() {} }; // legal via anonymous concrete subclass

Check: Shape s = new Rectangle() compiles because Rectangle is a subtype of Shape by rules 2+4, so the new value's type is acceptable for Shape variable. The actual object type at runtime, found later with getClass(), remains Rectangle.

Visual intuition: a decision tree with root "What is X?" branching to Interface (must pick implementing class), Abstract (must pick concrete child), Concrete (may pick self or child). Each leaf shows new with a class name that satisfies the edge label. The takeaway is that new always names a concrete class; the left-hand type decides which concretes are allowed.

Recap + bridge. A type pairs values with allowed operations; the six Java kinds are primitive, class, interface, array, null, and note that void is not a type. Subtype is defined by eight inclusive rules, and instantiation follows the Interface / Abstract / Concrete table for X x1 = new .... Because a supertype variable can hold any subtype object, declared type and runtime type diverge — exactly what the next section's instanceof, getClass, and .class tools let you distinguish.

Real-world: subtype rules explain why a method void paint(Component c) can accept a JButton, why a List<Component> cannot hold int without boxing, and why frameworks declare handler parameters as Object or interface types to accept many implementations while still needing the inquiry tools of the next section to recover the concrete type when behavior must branch.

24.8 Type Inquiry — Finding the Actual Type of an Object or Class

24.8.1 instanceof — Is This Value a Subtype of That Type?

You have Object x that at runtime holds either a Rectangle (which is a Shape) or a Box (which is not). Before you cast x to Shape, how do you ask "is this a kind of Shape?" without knowing its exact class?

The instanceof operator tests whether the type of an object is a subtype of a given type. It answers "does this object belong to this family?" rather than "what is its exact class?" The result is boolean, and null instanceof T is false (no exception).

Given a class Shape and an object e:

e instanceof Shape

This expression returns true if e is of type Shape itself or of any subclass of Shape (including indirect subclasses), and false otherwise. Because the test is inclusive of subclasses, a true result does not tell you the concrete class — a rectangle object and a circle object would both return true for instanceof Shape.

instanceof Shape test with Rectangle true and Box false guarding cast.

class Shape { void draw() {} }
class Rectangle extends Shape { }
class Box { } // does not extend Shape

Object x = new Rectangle(); // Rectangle extends Shape
if (x instanceof Shape) {
    Shape s = (Shape) x; // safe because the test already passed
    // s.draw() now valid
}
System.out.println(new Rectangle() instanceof Shape); // true — Rectangle isSubtype Shape
System.out.println(new Box() instanceof Shape);       // false — Box unrelated

// For the lecture's direct contrast:
Object y = new Box();
if (y instanceof Shape) {
    Shape s2 = (Shape) y; // not reached, because y instanceof Shape is false
} else {
    System.out.println("y is Box, not a Shape — cast would fail, so we skip");
}

Trace for x = new Rectangle():

  1. Runtime, x points to a Rectangle box.
  2. x instanceof Shape asks: is Rectangle a subtype of Shape? By rule 2, yes (Rectangle extends Shape), so result is true.
  3. Entering the branch, (Shape) x is verified by the VM and succeeds; s now aliases the same box via a supertype label.

Trace for y = new Box():

  1. Runtime, y points to a Box.
  2. y instanceof Shape checks subtype: Box is not a subtype of Shape, so false.
  3. Branch skipped, avoiding ClassCastException that a blind (Shape) y would throw.

Sense-check: null instanceof Shape is false, not an exception, so a missing object safely fails the guard.

The pattern "check before cast" is the main value of instanceof in program flow, especially when iterating heterogeneous collections like ArrayList<Object> where elements have different concrete types.

Visual intuition: draw a Venn-like subtype tree. Largest oval is Object, inner oval is Shape, inner-most oval is Rectangle; a separate disconnected oval is Box. Drop a dot for x inside Rectangle (hence also inside Shape and Object) and for y inside Box. instanceof Shape asks "is the dot inside the Shape oval?" The takeaway is inside = true, outside = false, and inside innermost already means inside all ancestors.

Scope: instanceof answers family membership, not exact identity. Use it when you need to know "can I safely treat this as a Shape?" and are willing to accept any subtype. For exact class (is it precisely Rectangle and not a subclass Square), use getClass or .class comparison instead.

Pitfalls

  • Using instanceof to replace polymorphism. Code that chains if (x instanceof A) ... else if (x instanceof B) ... often should be a polymorphic method x.draw() with overrides. Reserve instanceof for when the subtype really needs a different code path, such as a safe cast before a Shape-only method.
  • Forgetting null handling. Some learners add an extra x != null test before instanceof; it is not needed because null instanceof T is already false.

24.8.2 getClass and getName — The Exact Runtime Type

When you need the precise class, not just family membership, use getClass():

Class c = e.getClass(); // e is any non-null object reference

The left-hand side type is Class, written as Class<?> or Class in the lecture as a type descriptor. A Class object carries information about the type — its name, its superclass, array status, component type, and other reflective details. From it you can ask for the name:

String name = e.getClass().getName(); // e.g. "Shape" or "Rectangle"
System.out.println(e.getClass().getName());

If e holds a Rectangle, getName() returns "Rectangle" (in package java.awt it returns "java.awt.Rectangle"), not just "Shape". This is the difference from instanceof: getClass gives the concrete class that the object actually belongs to right now, while instanceof gives a family check.

Class as descriptor. Think of an Employee object as a person and its Class object as the personnel folder. The person carries values (name = "Jane Doe", salary = 50000); the folder carries metadata (name = "Employee", superclass = java.lang.Object). The folder exists once per type in the VM, no matter how many person objects exist. Methods on Class let you inspect the folder: getName() reads the folder label, isArray() checks if the folder is for an array type, getComponentType() shows double for double[].

getClass getName exact type versus instanceof family.

Shape e = new Rectangle();
System.out.println(e.getClass().getName()); // prints "Rectangle" — exact type
System.out.println(e instanceof Shape);     // true — family
System.out.println(e instanceof Rectangle); // true — family includes self
System.out.println(e.getClass() == Rectangle.class); // true — exact
System.out.println(e.getClass() == Shape.class);     // false — exact is Rectangle, not Shape

// Null nuance:
Object n = null;
// n.getClass() would throw NullPointerException — guard with null check or instanceof
System.out.println(n instanceof Shape); // false, safe on null

Contrast: e instanceof Shape true for Rectangle and for Circle; e.getClass().getName() distinguishes them. Use the first when adding a shape to a List<Shape> and the second when debugging which concrete class caused a failure.

Note on arrays by getClass: double[] a = new double[10]; Class c = a.getClass(); c.isArray() is true, c.getComponentType() is double, and c.getName() looks historically odd ("[D" for double[], "[Ljava.lang.String;" for String[][]) because array names follow internal encoding.

Scope: getClass() is final, so no override can change it — it always reflects the true VM type. It works on any reference, including arrays, but not on primitives directly (int.class exists via literal, 42.getClass() is illegal because 42 is not an object).

Pitfalls

  • Calling getClass() on null. ((Object) null).getClass() throws NullPointerException; null instanceof X does not.
  • Confusing getName() with toString(). getName() gives the type name; toString() on the object gives instance content. Printing e vs e.getClass().getName() answers different questions.

24.8.3 The .class Suffix and Class-Object Comparison

A third inquiry path uses the .class suffix. Writing .class after a type name yields the Class object that describes that type without needing an instance:

Class c1 = Rectangle.class; // descriptor for the Rectangle type
Class c2 = e.getClass();     // descriptor for the actual object e
// Also: int.class describes the primitive int type; String[].class describes array type
Class c3 = Class.forName("java.awt.Rectangle"); // same descriptor via name lookup

For every type loaded into the virtual machine there is exactly one Class object. That uniqueness makes reference comparison meaningful:

if (e.getClass() == Rectangle.class) {
    // e's exact type is Rectangle
}
if (e.getClass() == Shape.class) {
    // would mean e was exactly a Shape instantiation, not a subtype
}

The == here compares descriptor references, not content. Because the VM holds a single canonical Class object per type, two descriptors for the same type are == to each other, and descriptors for different types are not. This lets you test "is the exact type Rectangle?" in addition to the broader "is it a kind of Shape?" test that instanceof provides.

Rectangle.class descriptor comparison with ==.

Shape a = new Rectangle();
Shape b = new Circle(); // assume Circle extends Shape
System.out.println(a.getClass() == Rectangle.class); // true — exact match
System.out.println(b.getClass() == Rectangle.class); // false — Circle descriptor != Rectangle descriptor
System.out.println(a instanceof Rectangle);            // true — family again
System.out.println(a.getClass() == Shape.class);      // false — exact is Rectangle

// Choosing between the two tools:
void handle(Shape s) {
    if (s instanceof Shape) { /* any shape accepted */ }
    if (s.getClass() == Rectangle.class) { /* only Rectangle, not Circle nor Square extends Rectangle */ }
}

Decision guide: use instanceof Shape when you want to accept Rectangle, Circle, and any future Triangle subclass; use getClass() == Rectangle.class when dispatch must be exact and a subclass should not match.

Q: Why not always use getClass().getName() and skip instanceof?

A: They answer different questions, so the right tool depends on the question. Use instanceof when you want to know "can I safely treat this object as a Shape?" — it returns true for Rectangle, Circle, or any current and future subtype, so your method stays open to extension. Use getClass()/getName() or == on .class when you want "what is the exact class right now?" — for logging the concrete name, for exact-type routing in serializers, or for equals symmetry checks where getClass() != other.getClass() must fail for different classes. A program that only checks exact class would miss valid subtype opportunities (Rectangle through a Shape parameter); a program that only uses instanceof would never learn the concrete name for diagnostics or framework branching. The lecture stresses this inclusive-family versus exact-type contrast with Rectangle (true for both) versus Box (false for instanceof Shape, and Box.class != Shape.class).

Source label: *[24.8.qna.1 — instanceof family versus getClass exact type]*

Pitfalls for the whole inquiry suite

  • Using == on Class names instead of Class objects. Compare descriptors with ==, not getName().equals(...), when you want identity; string comparison works but misses canonical guarantee and is slower.
  • Expecting Class.forName and .class to differ. Both yield the same canonical object when the name is qualified (java.awt.Rectangle); Rectangle.class is just a compiler-friendly literal that avoids a string.
  • Type inquiry as polymorphism substitute. The tip from the textbook applies: if you find if (e.getClass() == Employee.class) doA(); else if (e.getClass() == Manager.class) doB();, consider replacing with e.action() and overrides. Type inquiry is for boundaries (deserialization, casting guards, debugging), polymorphism is for behavior variation.

Exam note: Type inquiry appears both as concept and as code pattern. Be ready to explain why x instanceof Shape is false for a Box and true for a Rectangle, to convert between e.getClass(), e.getClass().getName(), and Rectangle.class comparisons, and to choose instanceof for family guards versus getClass for exact-type fixes.

Real-world: application code guards casts in heterogeneous collections with if (x instanceof Shape) { Shape s = (Shape) x; } to avoid ClassCastException. Frameworks and JSON serializers use obj.getClass() == Rectangle.class or getClass().getName() for exact-type routing, logging, and audit. Debuggers print e.getClass().getName() to show which concrete class is actually live when a Shape variable surprises you.

Recap + bridge. instanceof tests inclusive subtype membership and guards casts (Rectangle true, Box false for Shape). getClass/getName return the exact runtime descriptor and string name, and Rectangle.class is the canonical literal you can compare with ==. Choosing family versus exact lets you either accept all Shape subtypes or isolate one concrete class, and choosing polymorphism over inquiry keeps behavioral variation maintainable. The finite-enum idea next uses the exact-type guarantee differently — its values are a closed catalogue of one Class with fixed instances.

24.9 Enum Types — A Finite Set of Values

24.9.1 Why Enum Is a Special Type

If int size = 1 means SMALL and 99 accidentally reaches your method, how does the compiler stop that invalid value from ever existing?

An enum is a type that holds a finite, fixed set of named values and no others. If you define an enum Size with values SMALL, MEDIUM, LARGE, then every value of type Size is one of those three. You cannot create a fourth size value at runtime; size++ that would produce 4 on an int-backed fake enum is impossible with a real enum because the type itself is the catalogue. That boundedness is the entire point: it restricts a program to a known catalogue and lets the compiler check type errors.

Think of an enum as a closed catalogue like a paper paint swatch with three tabs SMALL, MEDIUM, LARGE. You can point to any tab, but you cannot invent a new tab by mixing paint — the manufacturer printed only three. The lecture used Size with SMALL/MEDIUM/LARGE and Season with WINTER/SPRING/SUMMER/FALL as two swatches. The analogy maps to bounded type: catalogues restrict choice. It breaks with enums that carry per-constant data — those tabs have attached specs (like WINTER(5)), which a paper swatch does not push into behavior.

Enum as bounded type. An enum is the language's built-in "only these values" type. Compared to public static final int SMALL=1, MEDIUM=2, the enum gives compile-time membership checks, safe == comparison, readable switch cases, and no invalid integer slipping through. It is also Serializable and Comparable via java.lang.Enum.

24.9.2 Syntax and What the Enum Keyword Really Creates

Syntax:

enum Size {
    SMALL, MEDIUM, LARGE
}

What this declaration creates under the surface is a class with a private constructor and a fixed number of instances. Conceptually:

// expansion of enum Size { SMALL, MEDIUM, LARGE } — not what you write, but what it means
class Size {
    private Size() { } // private — objects cannot be created outside the class
    public static final Size SMALL  = new Size();
    public static final Size MEDIUM = new Size();
    public static final Size LARGE  = new Size();
}

Because the constructor is private, outside code cannot call new Size(). The only instances that ever exist are the public static final ones named in the enum body. They are accessed statically as Size.SMALL, Size.MEDIUM, Size.LARGE. At class-load time, the VM creates exactly those instances, in the order written, and no more.

The session also used a Season example with four constants:

enum Season {
    WINTER, SPRING, SUMMER, FALL
}
// conceptual:
public static final Season WINTER = new Season();
public static final Season SPRING = new Season();
// ...

Every element (WINTER etc.) is a value of the enum type, and the enum type itself functions as a class whose instances are exactly those listed values.

Placement is flexible: an enum can be declared outside a class at the top level, or inside a class as a member. Both styles appeared:

// outside — top level enum Season
enum Season { WINTER, SPRING, SUMMER, FALL }
class ABC { void show(Season s) { System.out.println(s); } }

// inside — member enum Season
class ABC {
    enum Season { WINTER, SPRING, SUMMER, FALL }
    void show(Season s) { System.out.println(s); }
}

The difference is namespace and access (ABC.Season when nested), not capability.

Scope: An enum implicitly extends java.lang.Enum, so it cannot also extend another class, and it cannot be extended (it is effectively final). It can implement interfaces, declare fields, methods, and constructors, but constructors must be private or package-private in effect and are only invoked for the listed constants.

Pitfalls

  • Trying new Size(). Does not compile — private constructor and enum instantiation ban.
  • Expecting Enum subtyping to allow extra instances via reflection. Attempts to new an enum reflectively are blocked by the runtime.

24.9.3 Working with Enum Values — values, valueOf and ordinal

The Enum API. Every enum E gets these members automatically from java.lang.Enum:

  • String toString() — returns the constant's name ("WINTER" for WINTER).
  • static E[] values() — returns all constants in declaration order (a fresh array each call).
  • static E valueOf(String) — returns the constant whose name equals the string; throws IllegalArgumentException if the name is unknown. Note: the textbook call valueOf here returns the constant itself (WINTER), but the parameter-taking constructor version can attach data.
  • int ordinal() — index of the constant starting at 0.
  • int compareTo(E other) — compares by ordinal order.
  • Class<?> getClass() and getName() work as usual, because enums are classes.

values() and ordinal() are not defined in java.lang.Enum as normal inherited methods — the compiler generates values for each enum type.

Season enum WINTER SPRING SUMMER FALL values loop printing.

class ABC {
    enum Season { WINTER, SPRING, SUMMER, FALL }

    public static void main(String[] args) {
        for (Season s : Season.values()) {
            System.out.println(s); // prints WINTER, then SPRING, SUMMER, FALL
        }
    }
}

Trace:

  • Season.values() builds and returns Season[] with elements at indices 0..3 holding the singletons at 1064-like addresses per constant (one box per constant, shared globally).
  • First loop iteration s is the object at the WINTER address, toString yields "WINTER" and prints.
  • Second iteration s is SPRING, prints "SPRING", and so on for four passes.

Output lines: WINTER newline SPRING newline SUMMER newline FALL.

Access and storage:

Season s = Season.WINTER;
System.out.println(s); // WINTER
s = Season.SUMMER;
System.out.println(s); // SUMMER
// s can also be null: Season t = null; // allowed, but null is not a Season value

Enum ordinal and valueOf with WINTER index 0 and attached values 5 10 15 20 — identity versus index.

System.out.println(Season.WINTER.ordinal()); // 0
System.out.println(Season.SPRING.ordinal()); // 1
System.out.println(Season.SUMMER.ordinal()); // 2
System.out.println(Season.FALL.ordinal());   // 3

// valueOf identity case (no extra field):
System.out.println(Season.valueOf("WINTER")); // WINTER — the value of WINTER is WINTER
System.out.println(Season.valueOf("WINTER") == Season.WINTER); // true — same singleton

The lecture paired the two prints for WINTER to contrast them: valueOf conceptually gives the constant WINTER itself, while ordinal() gives the integer 0. With the value-carrying version of Season (next subsection), WINTER also carries 5, so s is WINTER but s.value is 5. The phrase "value of a constant" in that simple enum means the constant itself.

Ordinal demo loop:

for (Season s : Season.values()) {
    System.out.println(s + " ordinal " + s.ordinal());
}
// WINTER ordinal 0
// SPRING ordinal 1
// SUMMER ordinal 2
// FALL ordinal 3

Additional nuance: compareTo follows ordinal, so WINTER.compareTo(SPRING) < 0 because 0 < 1; equals is == for enums, and == is the idiomatic comparison for enum constants.

Visual intuition: place four labeled tokens in order on a rail: position 0 WINTER, 1 SPRING, 2 SUMMER, 3 FALL. ordinal() reads the position number; valueOf("WINTER") picks the token at position 0; values() returns the whole rail as an array. The takeaway is name lives on the token, index is separate.

24.9.4 Giving Enum Constants Their Own Data — Initial Values and a Private Constructor

You can attach data to each constant by giving it a constructor argument. This is how the lecture modelled WINTER(5) etc. — each season carries a number that travels with the constant:

Season with attached values 5 10 15 20 and private constructor.

enum Season {
    WINTER(5), SPRING(10), SUMMER(15), FALL(20);

    private int value; // the attached data for this constant

    private Season(int value) {
        this.value = value; // this.value = value — field gets parameter
    }
    int getValue() { return value; }
}

public static void main(String[] args) {
    for (Season s : Season.values()) {
        System.out.println(s + " " + s.value);
        // prints: WINTER 5, SPRING 10, SUMMER 15, FALL 20
    }
}

Allocation trace:

  1. Class load creates WINTER: new Season(5) calls private constructor with value=5, stores 5 into WINTER.value.
  2. Similarly SPRING(10) stores 10, SUMMER(15) stores 15, FALL(20) stores 20. Each constructor runs once per constant, in order written.
  3. Loop: s is WINTER, s.value is 5, prints WINTER 5; next SPRING 10, and so on.

The lecture wrote the assignment as this.value = value to stress which value is the field (this.value) and which is the parameter (value). Overloaded constructors are also allowed — some constants can take arguments while others use a default no-arg constructor that sets value = -1 to mean "no data available" (as in Apple with RedDel).

Scope: Per-constant data makes the enum more than a label — it is a type-safe lookup table. Each constant has its own copy of value (like a column in the catalogue). The data is immutable after construction if the field is final, which is good practice.

Pitfalls

  • Exposing mutable field directly. Declaring public int value lets callers change it; make it private final with a getter.
  • Forgetting the semicolon. When an enum declares fields/methods after constants, the constant list must end with ; before the field declarations.

24.9.5 Feature Summary for Enum

The session closed the enum topic with a short checklist that again ties back to the expansion above:

  • Every enum implicitly extends java.lang.Enum (so it cannot extend another class and cannot be subclassed).
  • toString() returns the enum constant's name ("WINTER" for WINTER).
  • values() returns all constants present in the enum — use with for-each to traverse the catalogue.
  • ordinal() returns the declaration index (WINTER 0, FALL 3).
  • compareTo and compareTo order follow ordinal; equals is == for enums.
  • An enum can declare a constructor; it must be private or package-private in effect, and it is executed separately for each constant at class-loading time, in the order written.
  • Enum objects cannot be created explicitly with new, and the enum constructor cannot be invoked directly from outside — that is why the set remains bounded.
  • An enum can contain concrete methods but not abstract methods in the lecture's presented form (a fully detailed enum with per-constant class bodies can have abstract methods, but that variant was not in this session).
  • Bonus: because Enum implements Comparable, you can store enums in TreeSet/TreeMap with natural ordinal order, and because String.valueOf(s) delegates to toString(), printing is the name.

Q: Can I write new Season() to get an extra season?

A: No. The enum constructor is private and the language forbids explicit enum instantiation. The only Season objects that ever exist are the four listed in the enum body: the public static final Season WINTER, SPRING, SUMMER, FALL created at class-load time. Attempting new Season() outside the enum body does not compile, and reflective attempts to break the rule are rejected at runtime. That compile-time plus runtime enforcement is why enums are trusted as closed catalogues — callers cannot smuggle an invalid season past a method void schedule(Season s).

Source label: *[24.9.qna.1 — private constructor forbids explicit enum instantiation]*

Pitfalls

  • Using ordinal() for persistent storage. If you reorder constants later, stored ordinals mean different seasons. Persist the name (toString()) or a stable attached value instead.
  • Switch without covering all constants. If you add MONSOON later, an existing switch(Season) that handles only the four old cases silently falls through to default; prefer exhaustive handling or forget-default style with compiler warning.

Exam note: Expect a code-output question on values() traversal, on ordinal() values (WINTER 0 versus WINTER 5), on private constructor and no new, and on the WINTER(5)/SPRING(10) attached-value loop output. Remember the semicolon before fields and the this.value = value line.

Real-world: enums are used wherever a catalogue must be closed — Size { SMALL, MEDIUM, LARGE } for a product line, Season { WINTER, SPRING, SUMMER, FALL } for calendar logic, OrderStatus or DayOfWeek in services — so callers cannot pass an arbitrary string like "winter" with a typo, and the compiler, not runtime validation, guarantees only valid values flow through the system.

Recap + bridge. An enum is a finite class-like catalogue whose constants are public static final singletons built once with a private constructor (Size and Season examples). Standard tools are values() for iteration, valueOf for name lookup, ordinal for index (0..3), and per-constant fields like WINTER(5) via this.value = value. Because only listed constants exist, new Season() is impossible. Next, the generics story reuses that same "restrict to a known set" idea but applies it to type parameters — bounded types let you say which types may fill a slot T.

24.10 Bounded Types — Restricting a Generic Parameter

24.10.1 Recap — Unbounded Generics

If one template class Identity<T> can hold a Long in one line and a String in the next, what decides which types are allowed, and how does the compiler still check method calls through T?

To make the restriction clear, the session first reviewed the plain generic class. This recap anchors the bounded form that follows:

Unbounded type parameter T. T is a type parameter, a slot that the programmer fills when creating an object. With unbounded T, any reference type is allowed.

class Identity<T> {
    T obj;
    Identity(T obj) { this.obj = obj; }
    T getObj() { return obj; }
}

Here every T inside the body behaves as the chosen argument: if you form Identity<Long>, the field is a Long and getObj() returns a Long; if you form Identity<String>, the field is a String and getObj() returns a String. The class is type-parameterized: the actual type is chosen at use time, and the compiler checks that use site and body agree.

Identity<Long> number = new Identity<>(123L);
System.out.println(number.getObj()); // T is Long, getObj returns Long, prints 123
System.out.println(number.getObj().getClass().getName()); // java.lang.Long

Identity<String> name = new Identity<>("unk");
System.out.println(name.getObj());   // T is String, getObj returns String, prints unk
System.out.println(name.getObj().getClass().getName()); // java.lang.String

The test class Test that held main in the slides created the two identities exactly this way, printed each with getObj() and with obj.getClass().getName(), and showed Long and String flowing through the same template. The phrase "type parameterized type" in the lecture captured this choose-at-use-site behavior. T could be Long, String, Integer, Float, or your own ABC/XYZ with no restriction.

Two instantiations of the same template.

class Test {
    public static void main(String[] args) {
        Identity<Long> x = new Identity<>(123L);
        Identity<String> y = new Identity<>("hello");
        System.out.println(x.getObj()); // 123 — T was Long
        System.out.println(y.getObj()); // hello — T was String
        // Identity<int> z = new Identity<>(5); // illegal — primitives need wrapper Integer
    }
}
  • Line 1: T becomes Long; constructor expects Long; getObj returns Long.
  • Line 2: T becomes String; constructor expects String; getObj returns String.
  • Line 3: int rejected as type argument; Integer required because generics work over reference types only.

Sense-check: both objects share the single class definition Identity, but the compiler enforces that x.getObj() + 1 is legal (Long numeric) while y.getObj() + 1 is not (String).

Visual intuition: a rubber stamp cut as T obj. Press it once with Long ink — the imprint reads Long obj; ink it with String — the imprint reads String obj. The stamp shape does not change, but the ink color (type argument) does. The takeaway is one definition, many inkings.

24.10.2 What Bounding Means

A bounded type restricts which types may be used as arguments for a parameterized type. Instead of allowing any T, you say "T must be A or a subtype of A." That is, T must stand in an inheritance relationship with a stated upper bound. The compiler then knows every allowed T has the members of A, so calling A members through T is safe.

The syntax uses extends — the same word that declares subclassing — even when the bound is an interface (there, extends still means "subtype of"):

class Identity<T extends A> { ... }
// class Bound<T extends A> { ... } // same pattern named Bound in the variant example

Three parts of a bounded declaration. In T extends A, the pieces are in strict order:

  1. Name — the type parameter (T, or T1 in multi-parameter cases).
  2. Keywordextends.
  3. Upper bound — the restricting type (A; more generally Number, Comparable<T>, MyClass & MyInterface with & for multiple bounds).

So T extends A reads as "T is a bounded parameter whose upper bound is A." Concrete T is only allowed to be A itself or any class that extends A. Anything unrelated to A is rejected at compile time — the program "starts complaining" with a bound mismatch error. The bound is inclusive: A itself qualifies, not just strict children.

The general form with multiple bounds: class Gen<T extends MyClass & MyInterface> means T must be a subtype of MyClass and implement MyInterface.

How is this different from the enum catalogue? An enum lists allowed values; a bounded type lists allowed types. Both close an otherwise open set. With unbounded T, the set is all reference types; with T extends A, the set narrows to A's family.

Scope: Upper bounds are compile-time constraints that also supply compile-time knowledge for method calls. Inside Bound<T extends A>, the statement objRef.displayClass() is legal only because A declares displayClass(). Without the bound, T could be String with no such method, so the call would not compile. Lower bounds (? super T) exist for wildcards but are not the focus of this bounded-type section; the exam emphasis here is upper bounds with extends.

Pitfalls

  • Using implements for bounds. Bound<T implements A> is wrong — bounds always use extends even for interfaces.
  • Assuming T extends A allows A's siblings. B extends A and C extends A are allowed, but String or Integer (unrelated to A) are rejected.
  • Forgetting primitives need wrappers. Bound<int> is illegal; Bound<Integer> uses the wrapper Integer which is not a subtype of A here anyway.

Comparison table — unbounded vs bounded:

Aspect class Identity<T> class Bound<T extends A>
Allowed arguments Any reference type Only A and its subtypes
Can call obj.displayClass() inside class? No — T might lack it Yes — A guarantees it
Rejects Bound<String>? String accepted as T Rejected if String not subtype of A
Typical use Generic containers Methods that need A members (e.g., doubleValue() when T extends Number)

When to pick which: use unbounded when the container truly works for any type; use bounded when the body needs operations promised by the bound.

24.10.3 Worked Example — A, B and C

Bound T extends A with classes A B C and output Inside class C B and super A — the canonical bounded-type demo.

class A {
    void displayClass() { System.out.println("Inside super class A"); }
}
class B extends A {
    void displayClass() { System.out.println("Inside class B"); }
}
class C extends A {
    void displayClass() { System.out.println("Inside class C"); }
}

class Bound<T extends A> {
    private T objRef;
    Bound(T obj) { this.objRef = obj; }
    void doRunTest() {
        objRef.displayClass(); // valid because upper bound A guarantees displayClass exists
    }
}

class BoundedClass {
    public static void main(String[] args) {
        Bound<C> bCc = new Bound<>(new C());
        bCc.doRunTest(); // prints "Inside class C"

        Bound<B> bCb = new Bound<>(new B());
        bCb.doRunTest(); // prints "Inside class B"

        Bound<A> bCa = new Bound<>(new A());
        bCa.doRunTest(); // prints "Inside super class A"
    }
}

Execution trace, step by step in the order of main:

  1. new C() allocates a C box whose dynamic type is C and which inherits A members. Wrapping it in Bound<C> checks the bound: is C a subtype of A? Yes via extends, so compile succeeds.
  2. bCc.doRunTest() dispatches objRef.displayClass() with runtime receiver C, so C.displayClass() runs and prints Inside class C.
  3. new B() similarly passes the T extends A check.
  4. bCb.doRunTest() finds B receiver, prints Inside class B.
  5. new A() is itself the bound. Bound<A> is allowed because the bound is inclusive.
  6. bCa.doRunTest() finds A receiver (no override beyond itself), prints Inside super class A.

The output order reported on screen was therefore Inside class C, then Inside class B, then Inside super class A — matching the order of construction in main, not alphabetical.

Type argument check:

  • Bound<C> allowed, Bound<B> allowed, Bound<A> allowed — all subtypes or equal to A.
  • Bound<String> would not compile when T extends A: the compiler emits a bound mismatch because String is not A or its child. Similarly Bound<Integer> rejected.
  • The guarantee direction: inside Bound, the compiler can safely allow objRef.displayClass() only because the bound promises A.displayClass exists for all T.

Extension: Stats<T extends Number> from T6 Chapter 14 is the same idea for numbers — average() could call nums[i].doubleValue() because Number declares doubleValue, and Stats<String> is then rejected because String is not a subtype of Number.

Sense-check: swapping the declarations to bCc first, bCb second, bCa third yields output C B A as printed; changing the first line to Bound<String> x = new Bound<>("hi") produces a compile error, not a runtime exception — bounding protects at compile time.

Q: How is T extends A different from plain T?

A: With just T, tagged as unbounded, any type argument is accepted — Long, String, Integer, Float, your own ABC or XYZ, any reference type — and the body cannot call A-specific members through T because no A is guaranteed. With T extends A, the compiler restricts concrete arguments to exactly A and its subtypes (A, B, C in the lecture family) and rejects everything else at compile time; because A is now guaranteed, the body can safely call displayClass() on a T reference. The bound both narrows the catalogue of allowed types and lifts the body: bounded T lets generic code call members of A knowing they exist, while unbounded T keeps generic code to Object-level members only.

Source label: *[24.10.qna.1 — bounded vs unbounded generic]*

Pitfalls for bounded types

  • Forgetting the extends position. class Bound<A extends T> reverses name and bound; the correct order is class Bound<T extends A>.
  • Mixing wildcard bounds with type-parameter bounds. Bound<? extends A> is a wildcard use site; class Bound<T extends A> is a declaration-site bound. They compose but are distinct — do not replace one with the other on the wrong side.
  • Expecting runtime rejection. Bounded-type errors are compile-time. If the IDE shows red on Bound<String> with T extends A, that is the bound working — no heap object was created.

Exam note: Be ready to state the three parts name-extends-upper-bound, to label which of Bound<C>, Bound<B>, Bound<A>, Bound<String> are allowed, and to write the output sequence Inside class C, Inside class B, Inside super class A in construction order. A 4-mark bounded-type question was listed as likely that tests exactly this output and the name-extends-bound recall.

Real-world: bounded generics keep container Stats<T extends Number> safe for average() that calls doubleValue(), and let a cache Cache<T extends Persistent> guarantee save() exists. In the strategy pattern arenas of T1 and T4, similar constraints appear as Repository<T extends Entity> — the bound ensures persistence operations exist without tying the repository to one concrete Employee or Order class.

Recap + bridge. Unbounded Identity<T> accepts any T (Long, String), while bounded Bound<T extends A> with parts , extends, A\) accepts only A, B, C and lets objRef.displayClass() compile because A promises it; construction order gave outputs C, B, A, and unrelated String/Integer` are rejected. Bounded types thus narrow the type catalogue just as enums narrowed the value catalogue. The final topic carries this parameterization further — it lets individual methods and whole interfaces, not just single classes, carry type parameters.

24.11 Generic Methods and Generic Interfaces

24.11.1 Generic Methods — Parameterized at the Method Level

Do you need to make the whole toolbox generic just because one screwdriver should fit many screw sizes?

A generic method carries its own type parameter, independent of whether its enclosing class is generic. That lets a single operation be type-flexible while the rest of the class stays non-generic. The lecture contrasts this with a generic class where every member shares the same T.

Generic method signature. A generic method declares its type parameter before the return type:

<T> T getObject(T obj) { return obj; }

Here <T> before T declares the parameter; the second T as return type uses it; T obj as the formal parameter receives it. The caller's argument choice determines the return type flowing back. The parallel example printObject in the lecture shows a bounded-free method that prints the runtime name:

class Identity<T> {
    void printObject(T obj) {
        System.out.println(obj.getClass().getName());
        System.out.println(obj);
    }
}

Because T here is unbounded, printObject accepts a Long, a String, an Integer, or any reference type. Contrast with a method whose header is <T> void printObject(T obj) inside a non-generic class — there the method declares the T, not the class.

Generic method printObject and generic return type with Identity Long String.

class Identity<T> {
    void printObject(T obj) {
        System.out.println(obj.getClass().getName());
        System.out.println(obj);
    }
    <U> U getObject(U obj) { return obj; } // generic method — declares U
}
class Demo {
    public static void main(String[] args) {
        Identity<?> helper = new Identity<>();
        helper.printObject(123);      // T inferred as Integer — prints java.lang.Integer and 123
        helper.printObject("hello");  // T inferred as String  — prints java.lang.String  and hello

        String s = helper.<String>getObject("hi"); // explicit type arg — returns String
        Long n = helper.<Long>getObject(123L);     // returns Long
    }
}

Trace for helper.printObject(123):

  1. Literal 123 is an int; autobox to Integer because T expects an object.
  2. T inferred as Integer; obj at runtime is that Integer.
  3. obj.getClass().getName() yields java.lang.Integer.
  4. System.out.println(obj) prints 123.

Trace for helper.printObject("hello"): T is String; getClass().getName() is java.lang.String; second print is hello.

For getObject, the caller writes helper.<String>getObject("hi") or relies on inference from argument type; the compiler then knows the call returns String and assigns it to String s without a cast. Generic return types let a helper echo any type transparently, similar to GenMethDemo.isIn in T6 Chapter 14 that was static <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y).

Visual intuition: a generic class is a tinted warehouse where every aisle shares the same color; a generic method is one aisle that can change color at will while the rest of the warehouse stays neutral. The takeaway is class-level T fixes the whole class; method-level <T> fixes just that method call.

Scope: Generic methods can be static or instance methods; the type parameter is always fresh per call. The method's T shadows any class T if both exist, so prefer distinct names (U) when nesting to avoid confusion.

Pitfalls

  • Forgetting the leading <T>. T getObject(T obj) without <T> tries to name a real type T, not a parameter — does not compile unless the class declares T.
  • Expecting inference to always work. In chains like String s = getObject("hi").substring(1), inference succeeds; in assignment-free getObject("hi"); it may not — add an explicit <String> or target variable.

24.11.2 Generic Interfaces — Declaring and Implementing with Parameters

An interface can also be parameterized. Instead of a single concrete contract like interface Converter { Integer convert(String); } tied to two types, a generic interface describes a shape that implementors fill with chosen types.

Generic interface Declared and realized. The session's demo interface uses two type parameters and two swapped methods:

interface DemoInterface<T1, T2> {
    T2 doSomeOperation(T1 t);
    T1 doReverseOperation(T2 t);
}

Here T1 and T2 are placeholders at the interface level. The first method takes a T1 and returns a T2; the second takes a T2 and returns a T1. The pairing is intentional — it forces implementors to respect the directional swap and to commit to a bidirectional contract. More generally, interface MinMax<T extends Comparable<T>> { T min(); T max(); } shows a single-parameter bounded interface, and class Gen<T extends MyClass & MyInterface>-like bounds also apply to interfaces.

Generic interface DemoInterface T1 T2 with DemoClass String Integer implementation.

interface DemoInterface<T1, T2> {
    T2 doSomeOperation(T1 t);
    T1 doReverseOperation(T2 t);
}

class DemoClass implements DemoInterface<String, Integer> {
    public Integer doSomeOperation(String s) {
        return s.length(); // consumes String, produces Integer
    }
    public String doReverseOperation(Integer n) {
        return String.valueOf(n); // consumes Integer, produces String
    }
}

// Use site:
DemoInterface<String, Integer> d = new DemoClass();
System.out.println(d.doSomeOperation("hello"));   // 5 — String -> Integer
System.out.println(d.doReverseOperation(5));      // "5" — Integer -> String

Mapping walkthrough written on the board:

  • implements DemoInterface<String, Integer> means T1 -> String, T2 -> Integer throughout the contract.
  • Therefore doSomeOperation must be Integer doSomeOperation(String) and doReverseOperation must be String doReverseOperation(Integer). If you swap names or types (e.g., String doSomeOperation(Integer)), the compiler reports non-overriding method.
  • Inside DemoClass, T1 and T2 no longer exist as variables — they are replaced by the chosen concretes for this class.

Additional instantiations the lecture noted but did not code exhaustively:

class ABC<T1, T2> implements DemoInterface<T1, T2> { // pass-through generics
    public T2 doSomeOperation(T1 t) { /* generic body */ return null; }
    public T1 doReverseOperation(T2 t) { return null; }
}

There ABC declares its own T1, T2 and forwards them, so new ABC<String, Integer>() and new ABC<Long, String>() give different concrete pairs from one class definition.

Sense-check: DemoInterface<String, Integer> and DemoInterface<Integer, String> are different types — assigning one to the other does not compile, even though both are DemoInterface in name. Generic types differ when type arguments differ.

The session closed the entire module by connecting this back to bounded types — a generic interface can also be bounded, for example interface Converter<T extends Number>, though the worked code kept the bound on the class side — and by advising hands-on practice: copy each small program, run it, swap Long for String or C for B, and watch how the compiler accepts or rejects the change. Swaps that violate bounds produce compile-time red, not runtime surprises.

Q: When to use generic method versus generic class?

A: When only one operation needs to be type-flexible and the rest of the class does not. A generic class like Identity<T> makes every member see the same T — field obj, constructor parameter, getObj() return — so every use of the class is tinted by one T. A generic method like <T> void printObject(T obj) or <T> T getObject(T obj) makes just that method flexible, so the class can stay non-generic (or stay with its own T) while still offering a parameterized operation; call sites that need flexibility use the method, others ignore it. As a rule, if flexibility is pervasive across members, prefer a generic class; if it is localized to one helper or factory, prefer a generic method.

Source label: *[24.11.qna.1 — generic method versus generic class]*

Scope — when to use generic class vs method vs interface:

  • Generic class — choose when the whole abstraction always works over the same type (container Box<T> that stores T).
  • Generic method — choose when one method among many should be flexible independent of the enclosing class (utility printObject, factory make).
  • Generic interface — choose when the contract itself varies over types and many implementors will supply different concretes (Converter<T1,T2>, MinMax<T>).

Pitfalls

  • Raw DemoInterface without arguments. DemoClass implements DemoInterface (raw) is legal with warnings but loses type safety — casts reappear and ClassCastException returns; prefer the parameterized form.
  • Forgetting to implement both methods after changing type arguments. If you change implements DemoInterface<String, Integer> to DemoInterface<Integer, String>, both method headers must swap; one mismatch leaves the class abstract.

Real-world: generic interfaces are common in library design — Converter<T1, T2> with T2 convert(T1) and T1 reverse(T2), Repository<T> or Comparator<T>, and Guava-like Function<T1,T2> — where the same contract must work for String/Integer, Long/String, or other pairs without duplicating the interface text. A single DemoInterface<String, Integer> can drive a form mapper, while DemoInterface<Long, String> with the same source text drives an ID stringifier.

Recap + bridge. Generic methods declare <T> before the return (<T> T getObject(T obj)) so one operation can echo any type, while the rest of the class need not be generic; generic interfaces like DemoInterface<T1, T2> declare the slot pair once and implementors bind them (DemoClass implements DemoInterface<String, Integer> maps T1->String, T2->Integer). Both extend the generics arc that began with unbounded Identity<T> and tightened with bounded Bound<T extends A>. The module's closing advice applies to all — copy, run, swap type arguments, and watch compile-time acceptance turn red or green exactly where theory predicts.

Exam Guidance Summary

The session is the final content-covering contact session; the next two sessions are review and revision of all prior topics. The items below pull together every explicit exam hint given for Lecture 24, organized by concept so you can drill directly.

Exam note: Object fundamentals — Object as root in java.lang default package, universal methods toString, equals, hashCode, clone, getClass, finalize — are foundational and expected in concept questions. Know that every class implicitly extends Object and that java.lang needs no import.

Exam note: Cloning versus assignment — assignment copies the reference, cloning copies the object. Be able to state the three clone conditions (X.clone() != X on reference, X.clone().equals(X) on content, X.clone().getClass() == X.getClass() on class) and draw the 1000 versus 2000 address picture with == false and equals true.

Exam note: Requirements for cloning — public clone plus implements Cloneable plus super.clone() with CloneNotSupportedException handling — are a likely implementation question. Know both handling styles: try/catch inside the method and throws CloneNotSupportedException forwarded to the caller.

Exam note: The hashCode difference between an original and its clone (H1 != H2) is the check that identities differ; recall that hashCode reflects storage information, not the exact address, but differing hashes prove different storage.

Exam note: Shallow versus deep copy with the Employee (String immutable safe to share at 1064, Date mutable must be cloned at 2044/3024, int salary) is the central applied example. Expect to label a given clone as shallow or deep, explain why String sharing is safe and Date sharing is not, and write the extra line cloned.hireDate = (Date) hireDate.clone().

Exam note: Garbage collection — three eligibility patterns (nulling a = null, reassigning t2 = t1 orphaning the old box, anonymous new Test()), plus finalize() executed once protected void finalize() throws Throwable and System.gc()/Runtime.gc() as a hint not a forced collection — is examinable as a short descriptive question.

Exam note: Runtime memory — singleton Runtime.getRuntime(), freeMemory() and totalMemory() in bytes, derived used as used = total - free, singleton nature, and conversion bytes / 1024 / 1024 = MB — appeared with concrete numbers (266421656 bytes free, 1 MB used). Practice the subtraction and the two divides; remember free rises after GC.

Exam note: Type system — definition (values plus operations), six kinds (primitive, class, interface, array, null, void not a type), supertype/subtype as inheritance via the eight rules, and right-hand-side instantiation rules for interface / abstract class / concrete class type X in X x1 = new ... — is a theory question with possible code choices. Know when next X() is illegal.

Exam note: Type inquiry — instanceof (family test inclusive of subtypes, null instanceof T false), getClass()/getName() (exact runtime type), .class suffix (canonical descriptor) and == between Class objects — is highlighted with the Shape/Rectangle/Box contrast. Know when to guard a cast with instanceof and when to compare descriptors for exact type.

Exam note: Enum — finite catalogue, private constructor, public static final instances, placement inside or outside a class, values() traversal with for-each, ordinal() versus attached value, initial values WINTER(5) etc. with this.value = value, and the feature list (extends Enum, toString, values, ordinal, one constructor run per constant at class loading, no explicit new, concrete methods only) — is a probable code-output question including the WINTER 5 / SPRING 10 loop.

Exam note: Bounded types — recap of Identity<T> unbounded, then Bound<T extends A> with name-extends-upper-bound three-part pattern, example with A, B extends A, C extends A, and output order Inside class C, Inside class B, Inside super class A — is explicitly noted as a candidate for a 4-mark question. Know why Bound<String> is rejected when T extends A.

Exam note: Generic methods (<T> T method(T arg) with leading type parameter, e.g., <T> T getObject(T obj), printObject) and generic interfaces (DemoInterface<T1, T2> with DemoClass implements DemoInterface<String, Integer>) close the generics arc; expect to map T1/T2 to concrete choices and write matching method headers Integer doSomeOperation(String) / String doReverseOperation(Integer).

Practical preparation advised in the session: copy each small program (Account, CloneA/CloneB, Employee shallow/deep, Bound, Identity, DemoClass), run it locally, swap type arguments (Long versus String, B versus C, String/Integer mapping) and observe compiler acceptance versus rejection and runtime hashCode/getName outputs. That running practice also prepares for viva-style "what if I change this line?" questions.

Key Industry Applications

Why this lecture matters outside the exam. The Java Object Model underpins framework design, collection correctness, memory operations, type-safe APIs, and domain modeling in production systems.

  • Universal Object root — Any framework can accept Object and call toString/equals/hashCode for logging, collections, and debugging without knowing the concrete class. For example, a logging aspect log(Object x) prints x.toString() for any domain entity because Object guarantees it.
  • Content equality with equals versus identity with == — The basis of correct behavior in hash-based collections like HashMap and HashSet, where hashCode and equals must agree. A correctly overridden Employee.equals and hashCode lets a HashSet<Employee> detect duplicate hires; identity == would treat equal-content employees as distinct.
  • Cloning for safe snapshots — Duplicating a business entity such as an Employee or Account before a tentative update, so the original remains untouched if the change is rolled back. Implemented with super.clone() plus Cloneable, as in CloneA/CloneB and Account, this is the coded form of the Prototype pattern used in editors and transaction staging.
  • Shallow versus deep copy discipline — Choosing to share immutable String fields at 1064 while explicitly deep-copying mutable Date or collection fields at 2044/3024. That decision appears in data-transfer objects, message cloning in queues, and prototype copies where shared mutable state would leak edits between copies.
  • Garbage collection awareness — Writing allocation patterns that let the JVM reclaim quickly (nulling caches, avoiding long-lived anonymous objects, understanding eligibility via nulling/reassigning/anonymous new Test()) in long-running services so heap does not grow without bound and System.gc() remains an advisory hint rather than a fix.
  • Runtime memory introspection — Monitoring Runtime.getRuntime().freeMemory() and totalMemory() to size caches, tune heap with -Xmx, or log memory pressure in production diagnostics. Computing used = total - free and usedMB = used / (1024*1024) (the 266421656 to 1 MB drill) drives capacity alerts and cache eviction logic.
  • Type system and inquiry — Using instanceof guards before casts in heterogeneous collections (if (x instanceof Shape) { Shape s = (Shape) x; }) to avoid ClassCastException, and using getClass()/getName() or Rectangle.class == e.getClass() for exact-type routing in serializers, ORMs, and message dispatchers where Box versus Rectangle must branch differently.
  • Enums as closed catalogues — Modeling fixed domains like Size { SMALL, MEDIUM, LARGE } or Season { WINTER, SPRING, SUMMER, FALL } or OrderStatus so callers cannot pass arbitrary strings. Attached data such as WINTER(5) turns the enum into a type-safe lookup table where invalid values are caught at compile time and iteration via values() drives batch handling.
  • Bounded generics — Constraining a generic container or operation to T extends Number or T extends A so type-safe operations like displayClass() or average() with doubleValue() are guaranteed to exist. Bound<T extends A> is the teaching example; production uses include Cache<T extends Entity> and Stats<T extends Number>.
  • Generic interfaces — Defining reusable contracts such as DemoInterface<T1, T2> or Converter<T1, T2> or Repository<T> that work across String/Integer, Long/String, or other pairs without duplicating interface text. DemoClass implements DemoInterface<String, Integer> maps T1->String, T2->Integer once, giving Integer doSomeOperation(String) and String doReverseOperation(Integer) with full compile-time safety.

Together these patterns appear in frameworks from Spring (generic repositories) to Android (type inquiry and enums for state machines) and in every service that batches Employee snapshots, monitors heap, or designs a closed catalogue type.

OODAP Lecture 24 notes · Java Object Model, Type System, Cloning and Generics

Object Oriented Design, Analysis and Programming Architecture· postgraduate· 2026-08-19

Sections Breakdown

124.1 The Object Class — Root of the Java Inheritance Hierarchy

Object in java.lang is the universal supertype; every class inherits its methods like toString, equals, hashCode, clone, getClass.

224.2 Object Identity, Reference Assignment and Why Copying Is Not Cloning

Assignment copies the reference address (A1 at 1000 to A2/A3) not the object, creating aliasing where mutations are shared.

324.3 Cloning — Creating an Exact Independent Copy

Cloning creates a second box at 2000 with equal fields via super.clone, Cloneable and public clone, satisfying !=, equals and class identity.

424.4 Shallow Copy versus Deep Copy

Object.clone gives shallow copy sharing String at 1064 safely and Date at 2044 unsafely; deep copy clones the mutable Date to 3024.

524.5 Garbage Collection and Object Lifecycle

GC reclaims unreachable objects; eligibility via nulling, reassigning orphaning a box, or anonymous allocation; finalize once and System.gc as hint.

624.6 Java Runtime Classes and Memory Introspection

Runtime is a singleton via getRuntime; freeMemory and totalMemory in bytes give used = total - free and MB via /1024/1024, e.g. 266421656 to 1 MB.

724.7 Java Type System

A type is values plus operations; Java has primitive, class, interface, array, null (void not a type) and eight subtype rules.

824.8 Type Inquiry — Finding the Actual Type of an Object or Class

instanceof tests inclusive family (Rectangle true, Box false for Shape) while getClass/getName and Rectangle.class with == give exact runtime type.

924.9 Enum Types — A Finite Set of Values

Enum is a closed catalogue extending Enum with private constructor and static final constants; values/ordinal/valueOf and per-constant data WINTER(5).

1024.10 Bounded Types — Restricting a Generic Parameter

Bounded T extends A restricts generics to A family, guaranteeing displayClass; Bound<C,B,A> produce C,B,A output while String rejected.

1124.11 Generic Methods and Generic Interfaces

Generic method declares <T> before return for per-method flexibility; generic interface DemoInterface<T1,T2> bound by DemoClass implements DemoInterface<String,Integer>.

12Exam Guidance Summary

Aggregated exam must-knows across cloning, shallow/deep, GC, Runtime, types, enums, bounds and generics.

13Key Industry Applications

Production uses from Object root logging to Prototype cloning, deep copy, GC, Runtime heap, type guards, enums and bounded generics.

Postgraduate students in Object Oriented Design, Analysis and Programming

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.

The Object Class — Root of the Java Inheritance Hierarchy

Must-know: Object is the top of every inheritance chain in java.lang and its methods are available everywhere.

⚠️ Top pitfall: Using == for content equality instead of equals, and forgetting hashCode when overriding equals.

Self-check: Why does new Person() compile without extends yet have toString?

Connects to: 24.2, 24.7

Object Identity, Reference Assignment and Why Copying Is Not Cloning

Must-know: Assignment aliasing copies reference arrows to one box at 1000; mutation is visible through all aliases.

⚠️ Top pitfall: Expecting A2 = A1 to clone; it does not.

Self-check: After A2 = A1, will modifying through A1 show via A2?

Connects to: 24.3

Cloning — Creating an Exact Independent Copy

Must-know: Clone must satisfy X.clone()!=X, equals true, getClass equal; requires public clone, Cloneable, super.clone with exception handling.

⚠️ Top pitfall: Misspelling Cloneable or keeping clone protected.

Self-check: What three conditions must X.clone() meet and what does H1!=H2 prove?

Connects to: 24.4

Shallow Copy versus Deep Copy

Must-know: Shallow shares mutable Date at 2044; deep adds hireDate.clone() to 3024; String at 1064 can stay shared.

⚠️ Top pitfall: Leaving Date shared after shallow copy.

Self-check: After shallow Employee clone, does mutating E.hireDate affect cloned.hireDate?

Connects to: 24.3, 24.5

Garbage Collection and Object Lifecycle

Must-know: Three eligibility patterns plus finalize runs once and System.gc is a hint not a command.

⚠️ Top pitfall: Assuming System.gc forces immediate collection.

Self-check: List three ways an object becomes eligible.

Connects to: 24.6

Java Runtime Classes and Memory Introspection

Must-know: Runtime is singleton; used = total - free; MB = bytes/(1024*1024); 266421656 bytes example to 1 MB used.

[ used = totalMemory - freeMemory ]

⚠️ Top pitfall: Using new Runtime() or reading int instead of long.

Self-check: Compute used MB if total=267470232 and free=266421656.

Connects to: 24.5

Java Type System

Must-know: Type = values + ops; six kinds; eight subtype rules; RHS instantiation rules for interface/abstract/concrete X.

⚠️ Top pitfall: Listing void as a type.

Self-check: Can new X() appear when X is an interface?

Connects to: 24.8

Type Inquiry — Finding the Actual Type of an Object or Class

Must-know: instanceof family inclusive vs getClass exact descriptor; Box false, Rectangle true for Shape.

⚠️ Top pitfall: Using exact check when family guard was needed.

Self-check: Why is x instanceof Shape false for Box but true for Rectangle?

Connects to: 24.7, 24.9

Enum Types — A Finite Set of Values

Must-know: Enum = finite class with private ctor; values traversal, ordinal index, WINTER(5) via this.value=value, no new.

⚠️ Top pitfall: Using ordinal for persistence.

Self-check: What does Season.values() return and why is new Season() illegal?

Connects to: 24.10

Bounded Types — Restricting a Generic Parameter

Must-know: T extends A has parts name-extends-upper bound; Bound<C,B,A> allowed with C,B,A output, String rejected.

⚠️ Top pitfall: Using implements for bounds.

Self-check: What are three parts of T extends A?

Connects to: 24.11

Generic Methods and Generic Interfaces

Must-know: Generic method <T> before return vs generic class; DemoInterface<T1,T2> maps to String/Integer methods.

⚠️ Top pitfall: Forgetting leading <T> on generic method.

Self-check: When to use generic method vs generic class?

Connects to: 24.10

Was this lecture useful?

Loading comments…
🤖

BitsNotes AI Assistant

Subject Notes Assistant

Configure AI Chat

Choose how to access the chatbot
Have your own API key?

Switch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.

🔑 Enter API key above to fetch live models from provider, or enter model name manually.
OpenAI-Compatible API Support

Choose any provider preset (Gemini, DeepSeek, Kimi, GLM, MiniMax, Qwen, OpenAI, Groq, Ollama, etc.) or enter a custom endpoint URL.

Security & Privacy First

Your API key is sent directly from your browser to your specified provider. BitsNotes servers never store or see your key.