Just added: Algorithms you can run and practice
Updated Jun 22, 2026 Edit on GitHub

Classes & Objects - Java's Whole Worldview

Up to now you've written methods, loops, and if statements inside something called class Main that you mostly ignored. This phase is where class stops being scenery and becomes the point. In Java, the class isn't a feature - it's the feature. Almost everything you build is a class, and almost every value you touch is an object made from one.

That worldview runs deeper than in most languages. Python lets you write a loose function in a file; Go has standalone functions everywhere. Java does not - no code lives outside a class. This phase shows you how to think in classes: bundle data with its behavior, hand out copies of that bundle, protect it from the rest of your program.

The mental model: blueprint and instance

What it actually is. A class is a blueprint - a description that says "things of this kind hold this data and can do these things." An object (also called an instance) is one concrete thing built from it with the new keyword. The class is the architect's drawing; objects are the actual houses built from it - one drawing, as many houses as you like, each with its own address and furniture.

📝 Class - the template/blueprint, written once. Object / instance - one real thing made from it, created with new. The class Account is the idea of a bank account; new Account(...) is your account, separate from everyone else's.

Why this is the whole worldview. In many languages, classes are one tool among several. In Java, every program is a set of classes, every value with behavior is an object, and even main sits inside one. Stop asking "where do I put this loose code?" and start asking "what kind of thing is this?"

💡 Key point. Everything below is one sentence repeated in different clothes: the data and the behavior that belongs with it live together inside an object. When a detail feels arbitrary, return to that line.

Fields, constructors, and this

Let's build a real blueprint. An Account holds data (an owner's name, a balance) and offers behavior (deposit, check balance). Data lives in fields; setup happens in a constructor; this is how a method points at its own object.

What it actually is. A field is a variable belonging to each object - its own slice of data. A constructor is a special method, named exactly like the class, that runs once when you write new, to fill in the object's starting fields. this refers to "the object this method is running on" - reach for it when a parameter name collides with a field name.

public class Account {
    private String owner;     // a field - each Account gets its own
    private double balance;   // another field

    public Account(String owner, double balance) {  // constructor: same name as the class
        this.owner = owner;       // this.owner = the field; owner = the parameter
        this.balance = balance;
    }

    public void deposit(double amount) {
        this.balance += amount;   // 'this.' is optional here - no name clash
    }

    public double getBalance() {
        return balance;           // reading the field directly
    }
}
public class Main {
    public static void main(String[] args) {
        Account ada = new Account("Ada", 100.0);  // build one instance
        Account bob = new Account("Bob", 50.0);    // build another, totally separate

        ada.deposit(25.0);

        System.out.println(ada.getBalance());
        System.out.println(bob.getBalance());
    }
}
$ java Main.java
125.0
50.0

What just happened: new Account("Ada", 100.0) allocated a fresh object and ran the constructor, copying the parameters into that object's own owner and balance fields. ada and bob are independent objects: depositing into ada changed ada's balance and left bob's untouched, since each carries its own copy of the data - that separateness is the entire reason objects exist.

📝 this - a reference to the current object. Inside the constructor, owner (parameter) and this.owner (field) share a name; this.owner = owner means "store the parameter into my field." Without this., you'd assign the parameter to itself and the field stays empty. ⚠️ A classic silent bug - write owner = owner and your account starts up with a blank name and no error to explain why.

Instance vs static: the object vs the class itself

Some things belong to each object; some belong to the class as a whole. Java draws that line with the keyword static, explaining a mystery from Phase 1: why main is static.

What it actually is. An instance member (no static) belongs to each object - every Account has its own balance. A static member (static) belongs to the class itself - exactly one copy, shared by everyone, existing even if you never create a single object.

public class Account {
    private static int accountCount = 0;  // ONE counter, shared by the whole class
    private String owner;                  // each object's own field

    public Account(String owner) {
        this.owner = owner;
        accountCount++;                    // bump the shared counter on every new Account
    }

    public static int getAccountCount() {  // a static method - call it on the class
        return accountCount;
    }
}
public class Main {
    public static void main(String[] args) {
        new Account("Ada");
        new Account("Bob");
        new Account("Cy");

        // Called on the CLASS, not on an object:
        System.out.println(Account.getAccountCount());
    }
}
$ java Main.java
3

What just happened: accountCount is static, so it isn't stored inside any one Account - it lives on the class, and all three constructors incremented the same counter. We read it with Account.getAccountCount() (on the class), not ada.getAccountCount() (on an object), since it was never about any single account. Instance data answers "what's true of this object?"; static data answers "what's true of all of them at once?"

💡 Why main is static. When a program runs, no objects exist yet - the JVM hasn't created anything, so the entry point can't be an instance method. static means "this belongs to the class and can run with zero objects in existence" - exactly what a starting point needs.

Encapsulation: expose behavior, not raw data

Every field above was marked private. That's not decoration - it's the single most important habit in object-oriented Java.

📝 Encapsulation - keeping an object's data (private fields) hidden from the outside world, letting other code interact with it only through the object's methods. Nobody reaches in and changes a field directly.

Why hiding state prevents whole classes of bugs. If balance were public, any code could write ada.balance = -9999 and your account would silently go invalid - and finding that negative balance later, you'd have no idea which of a hundred lines did it. Make the field private and force all changes through one method, and that method becomes the one checkpoint every change must pass.

Here's a setter that refuses to let the balance go negative:

public class Account {
    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }

    public void withdraw(double amount) {
        if (amount > balance) {              // the guard lives in ONE place
            System.out.println("Denied: insufficient funds");
            return;                          // reject the bad change, leave balance untouched
        }
        balance -= amount;
    }

    public double getBalance() {
        return balance;                      // a getter: read access, no write access
    }
}
public class Main {
    public static void main(String[] args) {
        Account ada = new Account(100.0);

        ada.withdraw(30.0);   // fine
        ada.withdraw(500.0);  // rejected by the guard

        System.out.println(ada.getBalance());
    }
}
$ java Main.java
Denied: insufficient funds
70.0

What just happened: balance is private, so the only way to change it from outside is withdraw, which checks the amount before touching the field. The bad withdrawal was rejected and the balance held at 70.0. With withdraw the only door that subtracts from balance, you get a guarantee: no withdrawal can ever push the balance negative.

💡 Expose behavior, not raw data. Don't reflexively generate a getter and setter for every field - that just makes the field public with extra steps. Ask what the object should let callers do: an account should let you deposit and withdraw; whether it stores that as one balance field or a transaction list is the object's private business.

The trio: toString, equals, and hashCode

Two final pieces complete your mental model of a Java object - one is the single most common trap that catches beginners. Both come down to: Java objects don't behave the way you'd hope until you tell them how.

Objects print as gibberish until you override toString. Print an object you made and you'll get something like Account@1b6d3586 - class name and memory hash, useless to a human. Java calls toString() whenever it needs a text version, and the default is that gibberish. Override it and printing makes sense:

public class Account {
    private String owner;
    private double balance;

    public Account(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }

    @Override
    public String toString() {
        return owner + " ($" + balance + ")";
    }
}
public class Main {
    public static void main(String[] args) {
        Account ada = new Account("Ada", 100.0);
        System.out.println(ada);   // println calls toString() for you
    }
}
$ java Main.java
Ada ($100.0)

What just happened: System.out.println(ada) needed text, so it called ada.toString(). We overrode that method to return a readable description, so instead of Account@1b6d3586 we got Ada ($100.0). @Override tells the compiler "I mean to replace an inherited method" - misspell the name and it catches the mistake instead of silently creating a new, uncalled method.

⚠️ The #1 Java beginner trap: == vs .equals(). This bites everyone. For objects, == does not compare contents - it asks "are these the same object in memory?" To compare value, use .equals(). The default .equals() you inherit also just checks identity - so until overridden, two accounts with identical data count as unequal.

public class Account {
    private String owner;
    private double balance;

    public Account(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Account)) return false;
        Account other = (Account) o;
        return balance == other.balance && owner.equals(other.owner);
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(owner, balance);  // override TOGETHER with equals
    }
}
public class Main {
    public static void main(String[] args) {
        Account a = new Account("Ada", 100.0);
        Account b = new Account("Ada", 100.0);  // same data, different object

        System.out.println(a == b);        // same object in memory?
        System.out.println(a.equals(b));   // same value?
    }
}
$ java Main.java
false
true

What just happened: a and b hold identical data but are two separate objects, so a == b is false - == compares identity, not contents. Our overridden .equals() compares the actual fields, so a.equals(b) is true. Rule to burn in: use .equals() for value comparison, reserve == for "is it literally the same object." (Strings are the most common place this bites - Phase 9 has the full gotcha.)

⚠️ Override equals and hashCode together, always. Not optional politeness - it's a contract Java relies on. Hash-based collections like HashMap/HashSet use hashCode() to find objects fast: equal objects must have equal hash codes. Override equals but not hashCode, and two "equal" accounts can land in different buckets - a HashSet stores both as distinct, a HashMap lookup fails to find a key that's really there.

Recap

  1. A class is a blueprint; an object / instance is one thing built from it with new. In Java, this is the whole worldview - almost everything you build is a class.
  2. Fields hold each object's own data; a constructor (same name as the class) fills them in when you call new; this points at the current object and disambiguates a field from a same-named parameter.
  3. Instance members belong to each object; static members belong to the class itself (one shared copy) - which is exactly why main is static: it runs before any object exists.
  4. Encapsulation means private fields plus methods as the only doors in. Putting the rules in one guarded method (reject a negative balance) prevents whole classes of bugs. Expose behavior, not raw data.
  5. Override toString so objects print readably instead of as Account@1b6d3586.
  6. ⚠️ == compares object identity; .equals() compares value - and you must override equals and hashCode together or hash-based collections break.

You can now design a Java object: bundle the data, guard it, give it behavior, make it print and compare sensibly. Next: connecting objects to each other - how one class builds on another, and how interfaces let unrelated classes promise the same behavior.

Quick check

Test yourself on the ideas most likely to bite you in real code:

[
  {
    "q": "Inside a constructor, why write `this.owner = owner` instead of just `owner = owner`?",
    "choices": [
      "`this.owner` is the object's field while `owner` is the parameter - without `this.`, you'd just assign the parameter to itself and the field would stay empty",
      "`this.` makes the assignment run faster",
      "It's purely stylistic; both lines do exactly the same thing",
      "`this.` is required in every assignment inside any method"
    ],
    "answer": 0,
    "explain": "When a parameter shares a name with a field, the unqualified name refers to the parameter. `this.owner` explicitly means the field. Writing `owner = owner` assigns the parameter to itself, leaving the field at its default - a silent bug with no error."
  },
  {
    "q": "You create two `Account` objects with identical owner and balance. What do `a == b` and `a.equals(b)` return, assuming `equals` is properly overridden?",
    "choices": [
      "`a == b` is false (different objects in memory); `a.equals(b)` is true (same values)",
      "Both are true - identical data means identical objects",
      "Both are false - Java never considers separate objects equal",
      "`a == b` is true and `a.equals(b)` is false"
    ],
    "answer": 0,
    "explain": "`==` compares object identity: two separate objects are never `==` even with identical data. A properly overridden `.equals()` compares the actual field values, so it returns true. This `==` vs `.equals()` split is the #1 Java beginner trap."
  },
  {
    "q": "Why must you override `hashCode` whenever you override `equals`?",
    "choices": [
      "Hash-based collections (HashMap, HashSet) require equal objects to have equal hash codes - override only `equals` and lookups silently break",
      "`hashCode` is what makes objects print readably",
      "The compiler refuses to compile a class that overrides only one of them",
      "`hashCode` controls how the constructor initializes fields"
    ],
    "answer": 0,
    "explain": "It's a contract: equal objects must return equal hash codes. HashMap/HashSet use hashCode to place objects in buckets, so if two 'equal' objects hash differently, a set stores both as distinct and a map lookup fails to find a key that's really there. Always change the pair together."
  }
]

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. Inside a constructor, why write `this.owner = owner` instead of just `owner = owner`?

2. You create two `Account` objects with identical owner and balance. What do `a == b` and `a.equals(b)` return, assuming `equals` is properly overridden?

3. Why must you override `hashCode` whenever you override `equals`?