jario_
Back to overview

Chapter 7

Classes and objects

So far you had single variables and arrays. But a bank account consists of an owner and a balance, and it can take money. A class bundles such data with the matching methods. It's the blueprint; with new you build as many objects from it as you like.

In the tasks, every class is in its own file named after it, e.g. Account.java. In the examples here, everything is in one file so you can see it at a glance. Then only Main may be public.

Fields, constructor, methods

  • Fields are the data of each object. They are private: only the class itself can access them. From outside, that goes through methods like getBalance(), the getters. This is called encapsulation.
  • The constructor has the name of the class, no return type, and runs on new Account("Ada"). It fills the fields.
  • this.owner is the object's field, owner alone is the parameter. If both have the same name, you need this.
  • Methods without static work with the fields of the object you call them on: ada.deposit(50) only changes Ada's account.
  • toString() defines what an object looks like as text. println(ada) calls it automatically.

final in front of a field means: it's set once in the constructor and never changed afterwards.

class Account {
    private final String owner;
    private double balance;

    public Account(String owner) {
        this.owner = owner;
        this.balance = 0;
    }

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    public double getBalance() {
        return balance;
    }

    public String toString() {
        return owner + ": " + balance + " €";
    }
}

public class Main {
    public static void main(String[] args) {
        Account ada = new Account("Ada");
        Account bob = new Account("Bob");
        ada.deposit(50);
        ada.deposit(-10);
        bob.deposit(20);
        System.out.println(ada);
        System.out.println(bob);
        System.out.println("Ada has " + ada.getBalance());
    }
}

static and constants

A field without static exists once in every object. A field with static exists only once for the whole class; all objects share it. You can use it, for example, to count how many objects there are. You call a static method through the class name: Ticket.issued(). It has no this and so can't read normal fields. That's why everything was static so far: main runs without any object.

static final makes a constant, written in capitals: public static final int MAX = 7;.

A class can have several constructors. With this(...) in the first line, one calls the other, so the logic is in one place only:

public Duration(int minutes, int seconds) {
    this(minutes * 60 + seconds);
}
class Ticket {
    public static final String OFFICE = "Town hall";
    private static int counter = 0;
    private final int number;

    public Ticket() {
        counter++;
        this.number = counter;
    }

    public int getNumber() {
        return number;
    }

    public static int issued() {
        return counter;
    }
}

public class Main {
    public static void main(String[] args) {
        Ticket a = new Ticket();
        Ticket b = new Ticket();
        Ticket c = new Ticket();
        System.out.println(Ticket.OFFICE);
        System.out.println("a: " + a.getNumber());
        System.out.println("c: " + c.getNumber());
        System.out.println("Issued: " + Ticket.issued());
    }
}

Records

For classes that only hold data together, there's a short form: record Point(int x, int y) { }. This line creates private final fields, a constructor, the getters x() and y() (without get), a toString() like Point[x=3, y=4] and an equals that compares the values.

A record can't be changed. If something should "change", a method returns a new record, like moved in the example. Math.abs(n) returns the absolute value of a number, i.e. without its sign.

record Point(int x, int y) {
    public int distance(Point p) {
        return Math.abs(x - p.x()) + Math.abs(y - p.y());
    }

    public Point moved(int dx, int dy) {
        return new Point(x + dx, y + dy);
    }
}

public class Main {
    public static void main(String[] args) {
        Point a = new Point(3, 4);
        Point b = a.moved(1, -2);
        System.out.println(a);
        System.out.println(b);
        System.out.println("x of b: " + b.x());
        System.out.println("Distance: " + a.distance(b));
        System.out.println(a.equals(new Point(3, 4)));
        System.out.println(a == new Point(3, 4));
    }
}

Common mistakes

  • Field shadowed

    If a parameter or local variable has the same name as a field, it shadows it. Both lines look right and don't change the field:

    public Player(String name) {
        name = name;           // parameter assigned to itself
    }
    
    public void addPoints(int points) {
        int score = this.score + points; // new local variable
    }

    Correct is this.name = name; and score = score + points; without int.

  • Comparing objects with ==

    == checks whether it's the same object. Two points with equal values are still two objects, as the last line of the record example shows. For equal values there's equals, automatically for records.

  • Forgetting toString

    Without your own toString(), println(book) prints something like Book@1b6d3586. The method must be called exactly public String toString().

  • Reading a field from a static method

    A static method has no object and therefore no this. If you access a normal field there, the compiler reports non-static variable cannot be referenced from a static context.

  • Empty slots in an object array

    new Measurement[7] creates seven empty slots, all null. If you call a method on one, the program stops with NullPointerException. Only loop over the slots you've already filled.