A class is a blueprint. It says what data an object holds and what it can do, and then you create objects from it. Every line of Java you have written so far lived inside one.
Fields, constructor, methods
public class Account {
private String owner; // fields — the data each object holds
private double balance;
public Account(String owner, double balance) { // constructor — how one is created
this.owner = owner;
this.balance = balance;
}
public void deposit(double amount) { // behaviour
if (amount <= 0) {
throw new IllegalArgumentException("deposit must be positive");
}
balance += amount;
}
public double getBalance() {
return balance;
}
}
Note that the class holds the data and the rules about that data together. The check in
deposit is the point: because balance is private, there is no
way to change it that bypasses that check. That is encapsulation, and it is the reason to write a
class rather than a bag of variables.
new, and what it does
class Account {
private String owner;
private double balance;
Account(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
double getBalance() { return balance; }
}
class Demo {
void run() {
Account a = new Account("Folau", 100);
Account b = new Account("Lisa", 250);
System.out.println(a.getBalance()); // 100
System.out.println(b.getBalance()); // 250 — separate objects, separate data
}
}
new Account(...) does three things: allocates memory for a new object, runs the
constructor to initialise it, and returns a reference to it. The variable a holds that
reference, not the object itself — which is why two variables can point at one object, as
Data Types showed.
this
this refers to the object the method was called on. Its most common use is
disambiguating a field from a parameter with the same name:
class Account {
private String owner;
Account(String owner) {
this.owner = owner; // this.owner is the field; owner is the parameter
}
}
Naming the parameter after the field is deliberate and conventional — the alternative
(ownerParam) is worse. Where there is no clash, this is optional and usually
omitted.
Constructors
If you write no constructor, Java gives you a no-argument one for free. Write any constructor and that free one disappears — which surprises people:
class Point {
int x, y;
Point(int x, int y) { // now `new Point()` no longer compiles
this.x = x;
this.y = y;
}
}
Several constructors can coexist, and one can delegate to another with this(...). Put
the real work in one of them so validation is not duplicated:
class Account {
private final String owner;
private double balance;
Account(String owner) {
this(owner, 0); // delegate — must be the FIRST statement
}
Account(String owner, double balance) {
if (owner == null || owner.isBlank()) {
throw new IllegalArgumentException("owner required");
}
this.owner = owner;
this.balance = balance;
}
}
A constructor's job is to leave the object in a valid state. Validating here — rather than hoping
callers behave — means an Account that exists is an Account that makes
sense.
Getters, setters, and not writing them reflexively
class Person {
private String name;
private int age;
public String getName() { return name; }
public void setAge(int age) {
if (age < 0) throw new IllegalArgumentException("age cannot be negative");
this.age = age;
}
}
The reason to prefer a getter over a public field is not ceremony — it is that a method is a place you can later add validation, logging, or a computed value without changing every caller. A public field offers nowhere to put any of that.
But do not generate a setter for every field out of habit. A field with a public setter is a field
anyone can change at any time, which is exactly the thing encapsulation was for. Ask whether the
value should be changeable at all; often the answer is no, and final plus a constructor
argument is better.
When a class exists only to carry data, a record replaces all of this with one line.
The two methods you inherit and will have to override
Every class silently extends Object, which supplies toString(),
equals() and hashCode(). The defaults are rarely what you want.
class Point {
private final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public String toString() {
return "Point(" + x + ", " + y + ")"; // default prints Point@1b6d3586
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x && y == other.y; // default compares identity
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
Three rules that matter:
- Override
equalsandhashCodetogether, always. Two objects that areequalmust have the same hash code, or the object will go into aHashMapand never come back out. - Use
@Override. It costs nothing and turns a misspelled method name into a compile error instead of a method nobody calls. equalstakesObject, not your type. Writingequals(Point p)is an overload, not an override — which is exactly what@Overridecatches.
A real one
From the console bank app this site uses for examples. Note which fields are final
and which is not — that split is the design decision, not an accident:
public class Account {
private final long id;
private final long userId;
private final AccountType type;
private final String number;
private BigDecimal balance;
public Account(long id, long userId, AccountType type, String number, BigDecimal balance) {
this.id = id;
this.userId = userId;
this.type = type;
this.number = number;
this.balance = Money.round(balance);
}
public BigDecimal balance() {
return balance;
}
}
An account's id, owner, type and number never change, so they are final and the
compiler enforces it. The balance does change, so it is not. That is why this is a class rather than
a record — a record's components are all final, and a bank account
whose balance can never change is not much use.
Notice Money.round(balance) in the constructor. The balance is normalised on the way
in, so no Account anywhere in the system can hold a value with three decimal places.
That is the constructor doing its job: leaving the object in a state the rest of the code can
trust.
Where to put the class
One public class per file, and the file must be named after it — Account.java holds
public class Account. That is a compiler rule, not a convention.
A small helper class used by only one other class can be nested inside it rather than given its
own file. Mark it static unless it genuinely needs access to the outer object's fields;
a non-static nested class holds a hidden reference to its enclosing instance, which is a real source
of surprise memory retention.
class Order {
private final List<Line> lines = new ArrayList<>();
static class Line { // static: no hidden link back to the Order
final String sku;
final int quantity;
Line(String sku, int quantity) {
this.sku = sku;
this.quantity = quantity;
}
}
void add(String sku, int quantity) {
lines.add(new Line(sku, quantity));
}
}
Next
Classes on their own are containers. Object-oriented programming is
next — inheritance, polymorphism, and why "reuse this code" usually has a better answer than
extends.