Java Class

June 28, 20266 min readUpdated 8/20/2026

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 equals and hashCode together, always. Two objects that are equal must have the same hash code, or the object will go into a HashMap and 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.
  • equals takes Object, not your type. Writing equals(Point p) is an overload, not an override — which is exactly what @Override catches.

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.