Classes and objects in Java — practice with a bank, a library and a lab inventory (and the account that belonged to nobody)

My first BankAccount class compiled on the first try. That alone should have made me suspicious. I created Sergio’s account, deposited some money, printed it, and got this:

Account of null: 150.0 €

The money was there, but the holder wasn’t. In the constructor I’d written holder = holder; instead of this.holder = holder;. Java doesn’t complain: it assigns the parameter to itself and leaves the attribute as null. Since that day I write this. in every constructor, even when it isn’t needed, out of pure habit.

In the theory article you saw the pieces: attributes, constructors, this, encapsulation, static, toString and equals. Here we put them together in three complete programs:

  1. A bank account that protects its balance and generates unique account numbers.
  2. A library with an array of books that can be lent and given back.
  3. A lab inventory, with automatic codes and equals.

Program 1: a bank account that defends itself

The balance is the perfect example of why private and methods exist: nobody outside should be able to write account.balance = 1000000.

public class BankAccount {

    private static int nextNumber = 1;        // shared: generates unique numbers

    private final int number;                 // set once, never changes
    private final String holder;
    private double balance;                   // only touched from inside

    public BankAccount(String holder, double initialBalance) {
        this.number = nextNumber++;           // uses the value and gets the next one ready
        this.holder = holder;
        this.balance = Math.max(0, initialBalance);
    }

    public BankAccount(String holder) {
        this(holder, 0);
    }

    public boolean deposit(double amount) {
        if (amount <= 0) return false;
        balance += amount;
        return true;
    }

    public boolean withdraw(double amount) {
        if (amount <= 0 || amount > balance) return false;
        balance -= amount;
        return true;
    }

    public boolean transfer(BankAccount target, double amount) {
        if (target == null || target == this) return false;
        if (!withdraw(amount)) return false;      // not enough money: stop here
        target.deposit(amount);                   // touches ANOTHER object of the same class
        return true;
    }

    public double getBalance() { return balance; }
    public String getHolder() { return holder; }

    @Override
    public String toString() {
        return String.format("Account %03d · %s · %.2f €", number, holder, balance);
    }

    public static void main(String[] args) {
        BankAccount sergio = new BankAccount("Sergio", 150);
        BankAccount laura = new BankAccount("Laura");

        System.out.println(sergio.deposit(50));       // true
        System.out.println(sergio.withdraw(500));     // false: not enough
        System.out.println(sergio.transfer(laura, 80));
        System.out.println(laura.deposit(-20));       // false: negative amount

        System.out.println(sergio);
        System.out.println(laura);
    }
}

Output:

true
false
true
false
Account 001 · Sergio · 120.00 €
Account 002 · Laura · 80.00 €

Why do the methods return boolean?

Because an operation might not happen (withdrawing more than there is, depositing a negative amount) and the caller needs to find out. Returning true/false is the simplest way to report it. When you get to exceptions you’ll have another tool for this; for now, a boolean is clear and enough.

How are the numbers 001 and 002 generated?

With a static attribute: nextNumber is a single one for the whole class. Each constructor does nextNumber++: it stores the current value in number and leaves the counter ready for the next account. If nextNumber weren’t static, every account would have its own counter starting at 1, and they’d all be account 001.

number is final because an account number never changes. Marking it that way means that if anyone tries to change it by mistake, the code won’t compile.

How can transfer touch another account’s balance?

Inside transfer we use target.deposit(amount), a public method. But we could also have written target.balance += amount, and it would compile: private means “private to the class“, not “private to the object”. A BankAccount object can see the private attributes of another BankAccount. Even so, we use deposit so the validation lives in one place.

Forgetting to check target == this. Transferring from an account to itself looks harmless, but in real systems it creates nonsense transactions. If an operation makes no sense, reject it.

A formatting detail

%03d pads with zeros to 3 digits (1 → 001) and %.2f shows 2 decimal places.

Program 2: a library with an array of objects

Now we have two classes working together: Book, which knows whether it’s been lent, and Library, which stores many books.

public class Book {
    private final String isbn;
    private final String title;
    private final String author;
    private boolean lent;              // false by default

    public Book(String isbn, String title, String author) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
    }

    public String getIsbn()  { return isbn; }
    public boolean isLent()  { return lent; }      // booleans use "is"

    public boolean lend() {
        if (lent) return false;
        lent = true;
        return true;
    }

    public boolean giveBack() {
        if (!lent) return false;
        lent = false;
        return true;
    }

    @Override
    public String toString() {
        return title + " (" + author + ")" + (lent ? " [lent]" : "");
    }
}
public class Library {
    private final Book[] books;
    private int numBooks = 0;

    public Library(int capacity) {
        books = new Book[capacity];
    }

    public boolean add(Book book) {
        if (numBooks == books.length || find(book.getIsbn()) != null) return false;
        books[numBooks++] = book;
        return true;
    }

    // Returns the book, or null if it isn't there
    public Book find(String isbn) {
        for (int i = 0; i < numBooks; i++) {
            if (books[i].getIsbn().equals(isbn)) return books[i];
        }
        return null;
    }

    public boolean lend(String isbn) {
        Book b = find(isbn);
        return b != null && b.lend();      // if it's null, lend() is never called
    }

    public boolean giveBack(String isbn) {
        Book b = find(isbn);
        return b != null && b.giveBack();
    }

    public void listAvailable() {
        for (int i = 0; i < numBooks; i++) {
            if (!books[i].isLent()) System.out.println("  " + books[i]);
        }
    }

    public static void main(String[] args) {
        Library lib = new Library(10);
        lib.add(new Book("978-84-01", "Java for Everyone", "L. Pérez"));
        lib.add(new Book("978-84-02", "Data Structures", "M. Ruiz"));
        lib.add(new Book("978-84-03", "Python in Depth", "A. Gil"));
        System.out.println(lib.add(new Book("978-84-01", "Copy", "X")));  // repeated ISBN

        System.out.println(lib.lend("978-84-02"));   // true
        System.out.println(lib.lend("978-84-02"));   // false: already lent
        System.out.println(lib.lend("999"));         // false: doesn't exist
        System.out.println("Available:");
        lib.listAvailable();
        System.out.println(lib.giveBack("978-84-02"));
        System.out.println(lib.find("978-84-02"));
    }
}

Output:

false
true
false
false
Available:
  Java for Everyone (L. Pérez)
  Python in Depth (A. Gil)
true
Data Structures (M. Ruiz)

Who decides whether a book can be lent?

The book itself. Library.lend only finds it and asks it to be lent; the rule (“you can’t lend it twice”) lives in Book.lend. Each class handles its own business. If the rule changes tomorrow (say, some books can’t be lent at all), you only touch Book.

Why doesn’t b != null && b.lend() blow up?

Because && short-circuits: if b != null is false, Java already knows the result is false and doesn’t evaluate the second part. So lend() is never called on null. If you wrote b.lend() && b != null, the order would be reversed and an unknown ISBN would give you a NullPointerException.

Returning null from a method (like find) and using the result without checking. Whenever a method can return null, the caller has to ask if (x != null) first.

Why isLent and not getLent?

It’s a Java convention: getters for boolean start with is. That way if (!book.isLent()) reads almost like a sentence.

Program 3: a lab inventory with automatic codes

The last program combines static, constants and equals. Every piece of lab equipment gets a unique code (LAB-001, LAB-002…) the moment it’s created.

public class Equipment {
    public static final String PREFIX = "LAB";
    private static int counter = 0;

    private final String code;
    private final String type;
    private boolean broken;

    public Equipment(String type) {
        counter++;
        this.code = PREFIX + "-" + String.format("%03d", counter);
        this.type = type;
    }

    public static int getCounter() { return counter; }

    public String getCode()     { return code; }
    public String getType()     { return type; }
    public boolean isBroken()   { return broken; }
    public void markBroken()    { broken = true; }
    public void repair()        { broken = false; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Equipment)) return false;
        return code.equals(((Equipment) o).code);
    }

    @Override
    public int hashCode() { return code.hashCode(); }

    @Override
    public String toString() {
        return code + " " + type + (broken ? " (broken)" : "");
    }
}
public class LabInventory {
    public static void main(String[] args) {
        Equipment[] lab = {
            new Equipment("Laptop"), new Equipment("Monitor"),
            new Equipment("Laptop"), new Equipment("Router")
        };
        lab[1].markBroken();
        lab[3].markBroken();

        int broken = 0;
        for (Equipment e : lab) {
            System.out.println(e);
            if (e.isBroken()) broken++;
        }
        System.out.println("Items created: " + Equipment.getCounter() + " · broken: " + broken);

        Equipment same = lab[0];
        System.out.println(lab[0].equals(same) + " " + lab[0].equals(lab[2]));
        lab[1].repair();
        System.out.println(lab[1]);
    }
}

Output:

LAB-001 Laptop
LAB-002 Monitor (broken)
LAB-003 Laptop
LAB-004 Router (broken)
Items created: 4 · broken: 2
true false
LAB-002 Monitor

Why aren’t the two laptops equal?

Because our equals compares the code, not the type. lab[0] and lab[2] are both laptops, but they’re two different machines (LAB-001 and LAB-003). Deciding what “equal” means is a design decision: for an inventory, two items are the same only if they have the same code.

What’s with the array in curly braces?

Equipment[] lab = { new Equipment(...), ... }; creates the array and the objects at the same time. Unlike new Equipment[4], there are no null slots here: every position already has its object.

PREFIX is public static final: a constant any class can read (Equipment.PREFIX) and nobody can change. If the prefix becomes ROOM tomorrow, you change a single line.

The counter is private even though it’s static. Belonging to the class doesn’t mean it should be public: we only read it through getCounter(), so nobody can reset it to 0 from outside.

Try it yourself

  • In the bank account, add an overdraft limit: let the balance go down to −100 €. Which method do you need to change? And what if the limit were different for each account?
  • In the library, add a counter of how many times each book has been lent, and a method that returns the most-lent book.
  • In the inventory, make repair() work only if the item was broken and return a boolean, like Book.giveBack().

Watch it step by step in Python Tutor

Python Tutor runs your code line by line and draws the objects and method calls. Choose Java, paste this program and step through it with Next:

public class Tutor {
    static class Account {
        double balance;
        Account(double balance) { this.balance = balance; }

        boolean transfer(Account target, double amount) {
            if (amount > balance) return false;
            balance -= amount;            // this.balance
            target.balance += amount;     // the OTHER object's balance
            return true;
        }
    }

    public static void main(String[] args) {
        Account a = new Account(100);
        Account b = new Account(20);
        boolean ok = a.transfer(b, 30);
        System.out.println(ok + " " + a.balance + " " + b.balance);
    }
}

Output:

true 70.0 50.0

What to watch:

  1. After the two news, there are **two Account objects** on the heap, one with 100 and one with 20.
  2. When transfer starts, a new frame appears with three entries: this (pointing to a), target (pointing to b) and amount (30).
  3. With balance -= amount, the object this points to changes: 100 → 70. With target.balance += amount, the other one changes: 20 → 50.
  4. When the method returns, its frame disappears, but the objects are still there with their new balances. The changes aren’t lost because they were made on the objects, not on copies.

Summary

ProgramKey ideaWhat to take away
Bank accountprivate + validating methods + static numberingThe object protects its data and reports with a boolean when it can’t do something
LibraryTwo classes working together + an array of objectsEach class owns its rules; always check for null
Inventorystatic final, a counter and equals by codeYou define what “equal” means; constants go in UPPERCASE

In the exercises you’ll do it on your own: a Product class, an immutable Fraction and a complete Shop, plus three tricky snippets.

Next step: exercisesClasses and objects in Java — exercises with products, fractions and a shop (and the static that emptied the whole warehouse)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *