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.
$ 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.
$ 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:
$ 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:
$ 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.
$ 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
- 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. - Fields hold each object's own data; a constructor (same name as the class) fills them in when
you call
new;thispoints at the current object and disambiguates a field from a same-named parameter. - Instance members belong to each object;
staticmembers belong to the class itself (one shared copy) - which is exactly whymainisstatic: it runs before any object exists. - Encapsulation means
privatefields 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. - Override
toStringso objects print readably instead of asAccount@1b6d3586. - ⚠️
==compares object identity;.equals()compares value - and you must overrideequalsandhashCodetogether 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`?