Updated Jun 22, 2026

Classes & Objects — Java's Whole Worldview

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

That's the worldview to absorb here, and it runs deeper than in most languages. Python lets you write a loose function in a file. Go has standalone functions everywhere. Java does not: there is no such thing as code that lives outside a class. So instead of fighting that, this phase shows you how to think in classes — how to bundle data with the behavior that belongs to it, hand out copies of that bundle, and 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 that blueprint with the new keyword. The class is the architect's drawing; the objects are the actual houses built from it. One drawing, as many houses as you like, each with its own address and its own furniture.

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

Why this is the whole worldview. In many languages, classes are one tool among several. In Java they are the organizing principle of the entire language: every program is a set of classes, every value with behavior is an object, and even main — the very first thing that runs — sits inside a class. Once you stop asking "where do I put this loose code?" and start asking "what kind of thing is this, and what can it do?", Java's design stops feeling bureaucratic and starts feeling consistent.

💡 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 some data (an owner's name, a balance) and offers some behavior (deposit, check the balance). The data lives in fields; the setup happens in a constructor; and this is how a method points at its own object.

What it actually is. A field is a variable that belongs 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 — its job is to fill in the new object's starting fields. this refers to "the particular object this method is running on," and you 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, which copied the two parameters into that object's own owner and balance fields. ada and bob are two independent objects: depositing into ada changed ada's balance and left bob's untouched, because each one 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 (the parameter) and this.owner (the field) are two different things that happen to share a name; this.owner = owner means "store the parameter into my field." Without this., you'd be assigning the parameter to itself and the field would stay empty. ⚠️ This is a classic silent bug — write owner = owner and your account starts up with a blank name and no error to tell you why.

Instance vs static: the object vs the class itself

Some things belong to each object. Some things belong to the class as a whole. Java draws that line with the keyword static, and understanding it explains a mystery you've been staring at since 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 — there is exactly one copy, shared by everyone, and it exists 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) rather than ada.getAccountCount() (on an object), because 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 you run a program, no objects exist yet — the JVM hasn't created anything. So the entry point can't be an instance method (there's no instance to call it on). Marking main as static means "this belongs to the class and can run with zero objects in existence," which is exactly what a starting point needs. That public static void main you've been copying isn't a magic spell; every word now means something.

Encapsulation: expose behavior, not raw data

You may have noticed every field above was marked private. That's not decoration — it's the single most important habit in object-oriented Java, and it has a name.

📝 Encapsulation — keeping an object's data (private fields) hidden from the outside world, and letting other code interact with it only through the object's methods. The object guards its own state; nobody reaches in and changes a field directly.

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

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. Because there's no other door into balance, you have a guarantee: no account can ever go negative, enforced in a single method instead of trusted to every caller everywhere.

💡 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 list of transactions is the object's private business. Methods describe capabilities; fields are implementation.

The trio: toString, equals, and hashCode

Two final pieces complete your mental model of a Java object — and one of them 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 — the class name and a memory hash, useless to a human. Java calls a method named toString() whenever it needs a text version of your object, and the default is that gibberish. Override it and printing suddenly 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). The @Override annotation tells the compiler "I mean to replace an inherited method" — if you misspell the name, it'll catch the mistake instead of silently creating a brand-new method that never gets called.

⚠️ The #1 Java beginner trap: == vs .equals(). This one bites everyone. For objects, == does not compare contents — it asks "are these the same object in memory?" To compare whether two objects are equal in value, you use .equals(). And the default .equals() (the one you inherit) also just checks memory identity — so until you override it, 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. The rule to burn in now: for objects, use .equals() for value comparison and reserve == for "is it literally the same object." (Strings are the most common place this bites — Phase 9 dissects the full gotcha.)

⚠️ Override equals and hashCode together, always. Notice we overrode both. That's not optional politeness — it's a contract Java relies on. Hash-based collections like HashMap and HashSet use hashCode() to find objects fast, and the rule is: equal objects must have equal hash codes. Override equals but not hashCode, and two "equal" accounts can land in different buckets — so a HashSet will happily store both as if they were different, and a HashMap lookup will fail to find a key you know is there. The fix is the habit: change one, change the other. We'll go deeper in Phase 9; for now, just never split the pair.

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, and make it print and compare sensibly. Next we connect objects to each other — how one class can build 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."
  }
]

← Phase 4: Control Flow & Methods · Guide overview · Phase 6: Inheritance & Interfaces →

Check your understanding

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`?

Was this page helpful?