☕ Java

Object Class

java.lang.Object is the root of the entire Java class hierarchy. Every class in Java implicitly extends Object if it does not explicitly extend another class. This means every Java object — from a simple String to a complex data structure — inherits a set of fundamental methods from Object: equals(), hashCode(), toString(), getClass(), clone(), wait(), notify(), notifyAll(), and finalize(). Understanding these methods, when to override them, and how they interact is essential for writing correct Java code.

Object as the Universal Superclass

The decision to have a single root class — Object — from which every class descends was a deliberate design choice in Java. It provides a universal type that can hold any object, enabling data structures like List<Object> and Map<String, Object> to store any kind of object. It defines a set of operations that every object must support — equality comparison, a string representation, hash code computation, and class identity — because these are operations so fundamental that every object needs them. Every class you write in Java automatically extends Object unless it explicitly extends another class. Even if it explicitly extends another class, that chain of inheritance eventually reaches Object. String extends Object. Integer extends Number which extends Object. ArrayList extends AbstractList which extends AbstractCollection which extends Object. There is no Java class that does not have Object somewhere in its inheritance chain. This universality has profound practical consequences. Any variable of type Object can hold any object. Methods that accept Object can accept anything. The equals() and hashCode() methods defined in Object establish a contract that all objects must honour, enabling collections like HashSet and HashMap to work generically. The toString() method means you can always print any object, even if the result is the default class@hashcode form. Understanding what Object provides and when to override its methods is foundational knowledge for every Java developer.
Java
// ── Every class implicitly extends Object: ────────────────────────────
public class Point {
    private double x;
    private double y;
    // Implicitly: extends Object
}

// ── Equivalent to: ────────────────────────────────────────────────────
public class Point extends Object {
    private double x;
    private double y;
}

// ── Object methods available on every object: ─────────────────────────
Point p = new Point(3.0, 4.0);

String  s1 = p.toString();       // "Point@1b6d3586" (default)
int     h  = p.hashCode();       // memory-address-based int
Class   c  = p.getClass();       // java.lang.Class for Point
boolean eq = p.equals(p);        // true (same reference)

// ── Object reference can hold any object: ─────────────────────────────
Object obj1 = new Point(1, 2);
Object obj2 = "Hello";
Object obj3 = 42;                // autoboxed to Integer
Object obj4 = new ArrayList<>();

// ── Inheritance chain for String: ────────────────────────────────────
// String
//   └─ extends Object

// ── Inheritance chain for ArrayList: ─────────────────────────────────
// ArrayList
//   └─ extends AbstractList
//        └─ extends AbstractCollection
//             └─ extends Object

// ── Object's methods: ─────────────────────────────────────────────────
//
// equals(Object obj)    — are these two objects logically equal?
// hashCode()            — integer hash for use in hash-based collections
// toString()            — human-readable string representation
// getClass()            — the Class object representing this object's type
// clone()               — protected — creates a field-by-field copy
// finalize()            — deprecated — called before GC (avoid)
// wait()                — thread synchronisation — waits on monitor
// wait(long timeout)    — wait with timeout
// notify()              — wakes one waiting thread on this object's monitor
// notifyAll()           — wakes all waiting threads

Overriding equals() — Logical Equality

Object's default equals() method compares references — two variables are equal only if they point to the exact same object in memory. This is identity comparison, not logical equality. For value objects — objects that represent values rather than entities — reference equality is almost never what you want. Consider two separate Point(3.0, 4.0) objects. They represent the same mathematical point, but Object's default equals() returns false because they are different objects in memory. Similarly, two Customer objects with the same ID and name should be considered equal for business logic purposes, but the default equals() returns false. The equals() method should be overridden whenever two objects that have the same logical value should be considered equal, regardless of whether they are the same physical object. The overriding implementation must satisfy a formal contract defined in the Java documentation: reflexivity (an object equals itself), symmetry (if a equals b then b equals a), transitivity (if a equals b and b equals c then a equals c), consistency (repeated calls return the same result if neither object changes), and null-safety (x.equals(null) must return false, never throw). Violating the equals() contract produces subtle, maddening bugs in collections. A HashSet that stores two objects that are logically equal but use default equals() will store both, creating duplicates. A HashMap keyed on objects with broken equals() may not find values even when the key should match. The correctness of all hash-based collection operations depends on equals() being implemented correctly.
Java
// ── Default equals() — reference equality (rarely what you want): ────
public class Point {
    double x, y;
    Point(double x, double y) { this.x = x; this.y = y; }
}

Point p1 = new Point(3.0, 4.0);
Point p2 = new Point(3.0, 4.0);

System.out.println(p1.equals(p2));  // false — different objects!
System.out.println(p1 == p2);       // false — different references

// ── Correct equals() — logical equality based on field values: ────────
public final class Point {
    private final double x;
    private final double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object obj) {
        // 1. Reflexivity: same reference is always equal
        if (this == obj) return true;

        // 2. Null safety: never equal to null
        if (obj == null) return false;

        // 3. Type check: must be same class (or use instanceof for subclass support)
        if (getClass() != obj.getClass()) return false;

        // 4. Cast and compare significant fields:
        Point other = (Point) obj;
        return Double.compare(this.x, other.x) == 0
            && Double.compare(this.y, other.y) == 0;
        // Use Double.compare for doubles — avoid == with floating point
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);   // must override hashCode when overriding equals
    }
}

Point p1 = new Point(3.0, 4.0);
Point p2 = new Point(3.0, 4.0);
Point p3 = new Point(1.0, 2.0);

System.out.println(p1.equals(p2));  // true  — same values
System.out.println(p1.equals(p3));  // false — different values
System.out.println(p1.equals(null));// falsenull-safe
System.out.println(p1.equals("hi"));// false — different type

// ── equals() enables correct Set and Map behaviour: ───────────────────
Set<Point> points = new HashSet<>();
points.add(new Point(3.0, 4.0));
points.add(new Point(3.0, 4.0));   // duplicate — same logical value
System.out.println(points.size()); // 1 — deduplication works

Overriding hashCode() — The equals-hashCode Contract

The equals-hashCode contract is one of the most important rules in Java: if two objects are equal according to equals(), they must have the same hashCode(). This rule is the foundation of the correctness of all hash-based data structures — HashMap, HashSet, LinkedHashMap, and Hashtable all rely on this contract. When you add an object to a HashSet or use it as a HashMap key, the collection computes the object's hashCode and uses it to determine which bucket to store the object in. When you later search for the object, the collection computes the hashCode again and looks only in the corresponding bucket. If two logically equal objects have different hashCodes, the second add to a HashSet would compute a different bucket and find an apparently empty bucket, concluding the object is not present — and storing a duplicate. The collection is logically broken. The converse is not required: two objects can have the same hashCode without being equal. This is called a hash collision and is handled by the collection's bucket being a list or tree of objects that all have the same hash, falling back to equals() to distinguish them. All objects having the same hashCode (return 1) is technically valid but results in all collections degenerating to O(n) performance because everything ends up in one bucket. A good hashCode implementation distributes objects evenly across hash values, is consistent with equals(), and is fast to compute. The standard idiom is Objects.hash(field1, field2, ...) which combines field hash codes in a way that minimises collisions. For performance-critical code, a manually tuned combination of prime number multiplication is sometimes used.
Java
// ── The contract: equals objects must have equal hashCodes: ──────────
public class Student {
    private final String  id;
    private final String  name;
    private final int     year;

    public Student(String id, String name, int year) {
        this.id   = Objects.requireNonNull(id);
        this.name = Objects.requireNonNull(name);
        this.year = year;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Student other = (Student) obj;
        // Two students are equal if they have the same ID:
        return Objects.equals(id, other.id);
    }

    @Override
    public int hashCode() {
        // hashCode must be based on the SAME fields as equals:
        return Objects.hash(id);    // only id — same as equals
    }

    @Override
    public String toString() {
        return "Student{id='" + id + "', name='" + name +
               "', year=" + year + "}";
    }
}

// ── WRONG: equals without matching hashCode breaks collections: ───────
public class BrokenStudent {
    private String id;

    public BrokenStudent(String id) { this.id = id; }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof BrokenStudent other)
            return Objects.equals(id, other.id);
        return false;
    }
    // NO hashCode override — uses Object's identity-based hashCode!
    // Two BrokenStudents with same id will have DIFFERENT hashCodes.
}

// Demonstrating the breakage:
BrokenStudent bs1 = new BrokenStudent("S001");
BrokenStudent bs2 = new BrokenStudent("S001");

System.out.println(bs1.equals(bs2));    // true  — same id
System.out.println(bs1.hashCode() == bs2.hashCode()); // false — broken contract!

Set<BrokenStudent> set = new HashSet<>();
set.add(bs1);
set.add(bs2);
System.out.println(set.size());         // 2 — duplicate stored! Bug!

// ── CORRECT: Student with matching equals and hashCode: ───────────────
Student s1 = new Student("S001", "Alice", 2);
Student s2 = new Student("S001", "Alice", 2);

System.out.println(s1.equals(s2));    // true
System.out.println(s1.hashCode() == s2.hashCode());  // true — contract satisfied

Set<Student> students = new HashSet<>();
students.add(s1);
students.add(s2);
System.out.println(students.size());  // 1 — deduplication correct

Overriding toString()

Object's default toString() returns the class name followed by @ and the object's hashCode in hexadecimal: com.example.Point@7852e922. This is rarely useful for debugging or logging. Overriding toString() to return a meaningful, human-readable description of the object's state is one of the most valuable quality-of-life improvements you can make to a class. toString() is called implicitly in many situations: when you use string concatenation with +, when you pass an object to System.out.println(), when an object is logged, and when it appears in a debugger or test failure message. A well-implemented toString() makes debugging dramatically easier — seeing Customer{id=42, name='Alice', email='alice@example.com'} instead of com.example.Customer@4e25154f tells you immediately what the object is. The toString() format should include the class name and all significant fields. It should be concise but complete — enough to identify the object and its key state. For sensitive classes, fields like passwords, credit card numbers, and authentication tokens should be excluded or masked in toString() to prevent accidental logging of sensitive data.
Java
// ── Default toString() — barely useful: ──────────────────────────────
public class Address {
    private String street;
    private String city;
    private String postcode;
    // No toString() override
}

Address addr = new Address("123 Main St", "London", "EC1A 1BB");
System.out.println(addr);   // Address@7852e922 — not useful!

// ── Overridden toString() — immediately informative: ──────────────────
public class Address {
    private final String street;
    private final String city;
    private final String postcode;
    private final String country;

    public Address(String street, String city, String postcode, String country) {
        this.street   = street;
        this.city     = city;
        this.postcode = postcode;
        this.country  = country;
    }

    @Override
    public String toString() {
        return "Address{street='" + street + "', city='" + city +
               "', postcode='" + postcode + "', country='" + country + "'}";
    }
}

Address addr = new Address("123 Main St", "London", "EC1A 1BB", "UK");
System.out.println(addr);
// Address{street='123 Main St', city='London', postcode='EC1A 1BB', country='UK'}

// ── toString() in string concatenation — implicit call: ───────────────
Customer customer = new Customer("Alice", "alice@example.com");
System.out.println("Customer: " + customer);    // calls customer.toString()

// ── Sensitive data — exclude from toString(): ─────────────────────────
public class UserCredentials {
    private final String username;
    private final String password;   // sensitive — never log!
    private final String apiKey;     // sensitive — never log!

    public UserCredentials(String username, String password, String apiKey) {
        this.username = username;
        this.password = password;
        this.apiKey   = apiKey;
    }

    @Override
    public String toString() {
        // Only include non-sensitive fields:
        return "UserCredentials{username='" + username +
               "', password='[PROTECTED]', apiKey='[PROTECTED]'}";
    }
}

// ── Java record auto-generates useful toString(): ─────────────────────
public record Point(double x, double y) { }

Point p = new Point(3.0, 4.0);
System.out.println(p);   // Point[x=3.0, y=4.0]  — automatic!

Related Topics in Object-Oriented Programming

OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.