Course outline · 0% complete

0/29 lessons0%

Course overview →

Encapsulation: private, getters, toString

lesson 5-3 · ~6 min · 15/29

Guarding your fields

In lesson 5-1, anyone could write rex.age = -50. In a single-file program that is survivable. In a codebase where forty other classes touch your BankAccount, one careless write from anywhere corrupts data that every other feature trusts.

Encapsulation closes that door. Mark fields private so only code inside the class can touch them, then expose controlled access through methods:

class BankAccount {
  private int balance = 0;

  void deposit(int amount) {
    if (amount > 0) {
      balance += amount;
    } else {
      System.out.println("deposit rejected");
    }
  }

  int getBalance() {
    return balance;
  }
}

Now every change to balance passes through deposit, which enforces the rules in one place. A method that reads a field is a getter, such as getBalance, and one that writes it with validation is a setter, such as setName.

Outside code writing account.balance = 999 no longer compiles, so the invalid state cannot even be expressed.

A class that defends its own balance

One valid deposit, one invalid deposit, and a readable printed form.

public class Main {
  public static void main(String[] args) {
    BankAccount account = new BankAccount();
    account.deposit(100);
    account.deposit(-50);
    System.out.println("balance: " + account.getBalance());
    System.out.println(account);
  }
}

class BankAccount {
  private int balance = 0;

  void deposit(int amount) {
    if (amount > 0) {
      balance += amount;
    } else {
      System.out.println("deposit rejected");
    }
  }

  int getBalance() {
    return balance;
  }

  @Override
  public String toString() {
    return "BankAccount[balance=" + balance + "]";
  }
}

Output

deposit rejected
balance: 100
BankAccount[balance=100]

The negative deposit was refused by the class itself, so the balance stayed correct without main checking anything. The rule lives with the data it protects.

The last line passes the object straight to println, and Java calls the object's toString to decide what to print. That method is the subject of the next section.

toString and access levels

toString() is Java's __str__. Override it and printing an object shows something readable instead of BankAccount@1b6d3586, which is the class name and a memory-derived number.

The @Override annotation asks the compiler to verify that you really are replacing an inherited method, which catches typos such as tostring that would otherwise compile as a new unrelated method.

One more term is needed before the access table makes sense. A package is Java's way of grouping related classes under a shared name, the way folders group files. Each class declares its package at the top of the file, as in package com.shop.billing;, and classes in the same package sit in the same folder of the project.

The standard library ships packages such as java.util, where the collections live. That is what a line like import java.util.ArrayList; refers to, and unit 7 puts it to work.

The access keywords you now know:

KeywordWho can access
publicany code anywhere
privateonly inside the same class
none writtenany code in the same package

House style is fields private and methods public unless they are internal helpers. That keeps every object in charge of its own valid state, which is the core promise of OOP.

What private actually blocks

When a field is declared private int balance, code in another class that writes account.balance = 999 does not compile.

private means only code inside BankAccount itself can read or write the field. The compiler rejects the access from outside, so the invalid state cannot be expressed at all, and this is encapsulation enforced at compile time rather than checked at runtime.

The restriction is per class, not per object. A method inside BankAccount may freely read other.balance on a different BankAccount, which is how a transferTo method can be written.

Note also what private does not do. It is an organizing rule for source code, not a security boundary, so it protects your invariants from accidents rather than protecting secrets from an attacker.

A Player with validated scoring

Private fields, a constructor, a guarded mutator, getters, and a printed form.

public class Main {
  public static void main(String[] args) {
    Player p = new Player("Ada");
    p.addPoints(10);
    p.addPoints(-3);
    p.addPoints(5);
    System.out.println(p);
  }
}

class Player {
  private String name;
  private int score = 0;

  Player(String name) {
    this.name = name;
  }

  void addPoints(int p) {
    if (p > 0) {
      score += p;
    }
  }

  String getName() {
    return name;
  }

  int getScore() {
    return score;
  }

  @Override
  public String toString() {
    return name + ": " + score;
  }
}

Output

Ada: 15

addPoints guards with if (p > 0) before touching score, so the 10 and the 5 land while the -3 is ignored, leaving 15.

toString has to be public String toString() exactly, since it is replacing an inherited method, and the @Override above it is what makes the compiler check that claim.