Suppose you tracked three bank accounts in a flat script. You would keep one list of owners, a parallel list of balances, and a third of account numbers — and every time you added an account you would have to append to all three, in lockstep. Forget one and the lists slide out of alignment: the second customer's balance gets charged to the first. That whole family of bug — related data drifting apart — is exactly what object-oriented programming exists to prevent.
A class bundles the data and the actions that belong to a thing into a single unit, so they can never be separated. An object is one real account produced from that blueprint.
flowchart LR CL["class BankAccount (blueprint)"] --> O1["new BankAccount(Mara, 100)"] CL --> O2["new BankAccount(Jules, 250)"] style CL fill:#c2410c,color:#fff
Fields, constructors, and new
Inside a class you declare fields — the data each object carries. A constructor is the special block that sets up a brand-new object the moment it is created; it shares the class's name and has no return type. You bring an object into existence with the new keyword: new BankAccount("Mara", 100) builds one account, with its own owner and balance, separate from every other account.
Two words get used loosely but mean different things. The class is the blueprint, written once. An object — also called an instance — is a concrete value of that type. One class can spawn thousands of instances.
A class is also a type
Defining BankAccount does more than hand you a constructor — it creates a brand-new type you can use everywhere a type is expected. You can declare a variable BankAccount acc;, accept one as a method parameter like void transfer(BankAccount target, int amount), or gather many into a List<BankAccount>. Before you write any classes, the only types you have are the built-in ones (int, String, boolean), and every class you add enlarges the vocabulary your program can speak.
This is what statically typed really means: the compiler knows the type of every expression, so a call like acc.deposit(...) is checked against BankAccount's actual methods at build time.
public class Main {
static class BankAccount {
private String owner;
private int balance;
BankAccount(String owner, int openingBalance) {
this.owner = owner;
this.balance = openingBalance;
}
String getOwner() {
return owner;
}
int getBalance() {
return balance;
}
void deposit(int amount) {
balance = balance + amount;
}
}
public static void main(String[] args) {
BankAccount acc = new BankAccount("Mara", 100);
acc.deposit(50);
System.out.println("Owner: " + acc.getOwner());
System.out.println("Balance: " + acc.getBalance());
}
}
Objects carry behaviour, not just data
A class is more than a bag of fields. The methods you write inside it are the actions an object can perform, and each one runs on the object you called it on. Write acc.deposit(50) and the method updates acc's own balance — which is why two accounts stay independent even though they share the same code.
Notice deposit has no static keyword. That marks it as an instance method: it belongs to an object, not to the class as a whole. Inside an instance method, a bare balance quietly means this.balance — the field of whichever object you invoked the method on.
this: the field, not the parameter
Constructors usually name their parameters exactly like the fields they fill — owner for owner. That reads cleanly, but it creates a puzzle: inside the constructor, does the bare word owner mean the incoming parameter or the field?
Java's rule is nearest wins. An unqualified name refers to the closest declaration, which here is the parameter. So owner = owner; would copy a parameter into itself and leave the field untouched — a silent no-op. Writing this.owner = owner; breaks the tie: this means the object currently being built, so the left side is unambiguously the field.
flowchart LR IN["new BankAccount(owner, 100)"] --> C["constructor runs"] C --> P["bare owner = parameter"] C --> F["this.owner = field"] P -.-> N["without this:<br/>parameter copied into itself"] F --> OK["field set correctly"] style F fill:#3776ab,color:#fff style N fill:#b45309,color:#fff
Encapsulation: the object owns its data
Mark the fields private and the outside world cannot touch them directly — it must go through the methods you choose to expose. A getter hands back a field's value; getBalance() lets callers read the balance without being able to write it. That split between reading and writing is encapsulation: the object keeps control of how its state changes, even as the rest of the program reads the values freely.
deposit is the other half of the bargain. Funneling every change through one method leaves a single place to add rules later — reject negatives, log the transaction, cap the amount.
The trap: objects are references
Here is the surprise that bites every newcomer. Assigning an object to a new variable does not copy it. Box b = a; makes b point at the same object as a — there is still only one box. Mutate it through b and the change is visible through a, because they are two names for one thing.
This is why a == b prints true: == on objects compares references, not the values inside. Two separately-built boxes holding the same number are not ==. To compare contents you call .equals() — the subject of a later lesson. Primitives like int copy on assignment; objects never do.
flowchart LR
A["Box a"] --> OBJ[("one Box<br/>value 99")]
B["Box b = a"] -.same object.-> OBJ
C["Box c = new Box(99)"] --> OBJ2[("a different Box<br/>value 99")]
style OBJ fill:#b45309,color:#fff
style OBJ2 fill:#3776ab,color:#fff
public class Main {
static class Box {
int value;
Box(int value) {
this.value = value;
}
}
public static void main(String[] args) {
Box a = new Box(10);
Box b = a; // b and a are the SAME object
b.value = 99; // so this changes a's value too
System.out.println("a.value: " + a.value);
System.out.println("same object: " + (a == b));
Box c = new Box(99); // a separate object
System.out.println("a == c: " + (a == c));
}
}
What does this program print? Read it carefully and type the exact output (two lines).
public class Main {
static class BankAccount {
private String owner;
private int balance;
BankAccount(String owner, int openingBalance) {
this.owner = owner;
this.balance = openingBalance;
}
String getOwner() {
return owner;
}
int getBalance() {
return balance;
}
void deposit(int amount) {
balance = balance + amount;
}
}
public static void main(String[] args) {
BankAccount acc = new BankAccount("Mara", 100);
acc.deposit(50);
System.out.println("Owner: " + acc.getOwner());
System.out.println("Balance: " + acc.getBalance());
}
}
The constructor stored the opening balance of
100.deposit(50)adds 50 to the balance, making it150.getOwner()returns theownerfield, which is"Mara".
What is a constructor's job?
A constructor runs once, automatically, when new creates an object — its job is to initialise that object's fields so it starts in a valid state.
Inside BankAccount(String owner, ...), what does the bare word owner refer to?
Java resolves an unqualified name to the closest declaration — here, the parameter. To reach the field you must write this.owner; otherwise owner = owner would copy the parameter into itself and leave the field alone.
References check. y is assigned from x, then mutated. Predict all three lines before you check.
public class Main {
static class Counter {
int count;
Counter(int start) {
this.count = start;
}
}
public static void main(String[] args) {
Counter x = new Counter(0);
Counter y = x;
y.count = 5;
System.out.println(x.count);
System.out.println(y.count);
System.out.println(x == y);
}
}
Counter y = xdoes not copy the object —yis another name for the very same object asx.So mutating
y.countalso changes whatx.countreads.==compares references; sincexandypoint at one object, it istrue.
A class has private int balance and exposes no method that lowers it. From outside the class, what can a caller do with balance?
private means only code inside the class can see or touch the field. With no getter and no setter, outside callers cannot read or change it at all — that is encapsulation doing its job. Add a getter or a method only if you decide outside code should have that access.
Recap
- A class bundles fields and methods;
newbuilds an instance with its own values, and the class becomes a type you can use everywhere. - A constructor runs once at creation to put an object into a valid starting state.
this.fieldmeans the field; a bare name means the nearest local — usually the parameter.- Mark fields
privateand expose getters/methods so the object controls its own state. - Assigning an object makes an alias, not a copy;
==compares references, so two equal-valued objects are not==, and anullreference throws if you dereference it.
Next you will meet Java's collections — the resizable containers that hold many objects like these.