Command Palette

Search for a command to run...

PHASE 4Beginner ~28 min· topic 2 of 12

Topic 4.2

Fields and Instance Methods

In one line

Fields are an object's memory: the data it carries between method calls. Instance methods are the object's behaviour: code that reads and changes that object's own fields, so the data and the rules for it live together.

Think of it like this

A piggy bank. The coins inside are its state (how much money it holds right now). The slot and the hammer are its behaviour (put a coin in, break it open). You don't reach inside and count coins by hand; you use the actions the piggy bank offers. Each piggy bank on the shelf has its own coins. In Java, the coins are fields and the actions are instance methods.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

State
The data an object holds right now, stored in its fields. A bank account's state is its balance.
Behaviour
What an object can do: its instance methods.
Instance field
A non-static field. Every object gets its own copy.
Instance method
A non-static method. It runs on one particular object and can use that object's fields.
Receiver
The object a method is called on. In acc.deposit(50), acc is the receiver.
Query method
A method that answers a question and returns a value without changing the object.
Command method
A method that changes the object's state, for example adding money to a balance.
Field initialiser
The = value written next to a field declaration. It runs for each new object.

Step by step

01State lives in fields

A BankAccount needs to remember two things between calls: who owns it and how much is in it. Those become fields. They live inside the object on the heap, so they survive after any method returns.

Compare that with a local variable inside deposit: it exists only while deposit runs. If you stored the balance in a local, it would vanish at the closing brace.

Main.javawhole filejava
class BankAccount {
    String owner;          // instance field
    long balance;          // instance field, in paise/cents
    int transactions = 0;  // field with an initialiser
}

02Behaviour lives in instance methods

deposit and withdraw are commands: they change the balance. getBalance is a query: it only reports.

Inside deposit, the name balance means *this object's* balance. Call a.deposit(500) and a's balance changes; b's doesn't.

withdraw returns a boolean to say whether it worked. The overdraft rule lives here, in one place, instead of being repeated wherever money leaves an account.

Main.javawhole filejava
void deposit(long amount) {
    balance += amount;
    transactions++;
}

boolean withdraw(long amount) {
    if (amount > balance) {
        return false;        // rule: no overdraft
    }
    balance -= amount;
    transactions++;
    return true;
}

long getBalance() {
    return balance;          // query: no change
}

03How does one method know which object it's working on?

There is only one copy of the deposit bytecode, yet it updates the right account every time. The trick: the compiler turns a.deposit(500) into "push a, push 500, call deposit". Inside the method, the object arrives as local slot 0, named this. Every bare field access balance is really this.balance.

So an instance method is a static-looking function with a hidden first parameter. The bytecode instruction is invokevirtual, which also handles overriding (Phase 5).

How does one method know which object it's working on?diagram
Rendering diagram…

04Methods can call other methods on the same object

Inside an instance method you can call another instance method without a receiver: transfer can call withdraw(amount), which means this.withdraw(amount).

A method can also take another object of the same class as a parameter and call methods on it: transfer(BankAccount to, long amount) withdraws from this and deposits into to.

Main.javawhole filejava
boolean transfer(BankAccount to, long amount) {
    if (!withdraw(amount)) {     // this.withdraw(amount)
        return false;
    }
    to.deposit(amount);          // a different receiver
    return true;
}

05Field initialisers run in order

When an object is created, field initialisers run top to bottom, before the constructor body (Topic 4.3 shows the full order). So int max = 10; int half = max / 2; gives half = 5.

Reverse the two lines and the compiler stops you with illegal forward reference: half would read max before it's been set. (Through a method call you *can* sneak past the check and you'd read the default 0, which is exactly the kind of bug the rule exists to catch.)

terminal
$ javac Main.java
── expected output ──
Main.java:2: error: illegal forward reference
int b = a * 2;
^
1 error

06Why main can't use instance members directly

main is static: the JVM calls it without creating any Main object. So inside main there is no this. Writing greet() or count = 1 there asks "which object's?" and there's no answer.

The fix is always the same: create an object, then call through it: new Main().greet() or, more usually, create the domain object you need (BankAccount a = new BankAccount();).

terminal
$ javac Main.java
── expected output ──
Main.java:5: error: non-static method greet() cannot be referenced from a static context
greet();
^
1 error

Try it yourself

  1. 1

    Add a rule

    In the bank account example, make deposit refuse amounts of 0 or less: change it to return boolean, return false for bad amounts, and print the result of a.deposit(-50). Predict the transaction count before running.

  2. 2

    Swap the field order

    In the Game example, move int later = 7; above int sneaky = readLater();. Predict the new value of sneaky, then run.

  3. 3

    Call an instance method from main

    Add void hello() { System.out.println("hi"); } to Main and call hello(); from main. Read the error, then fix it with new Main().hello();.

Code & diagrams

A bank account with its own rules New tab
Sign in to run this example in your browser.

Expected output

withdraw 300: true
withdraw 900: false
transfer 200: true
Asha has 500 after 3 transactions
Ben has 200 after 1 transactions
Queries vs commands on a rectangle New tab
Sign in to run this example in your browser.

Expected output

area:      12
perimeter: 14
square?    false
after scale(2): 8 x 6
area:      48
area again 48
Field initialisers run in declaration order New tab

`sneaky` is 0, not 7: `later` hadn't been initialised yet when `readLater()` ran.

Sign in to run this example in your browser.

Expected output

lives = 3
bonus = 6
total = 9
sneaky = 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use instance members from a static method

Inside Main, declare int count; and void greet() {}, then in main write greet(); and count = 1;.

terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: non-static method greet() cannot be referenced from a static context
greet();
^
Main.java:6: error: non-static variable count cannot be referenced from a static context
count = 1;
^
2 errors

Break #2

Store state in a local instead of a field

In deposit, write long balance = 0; balance += amount; (declaring a local with the same name).

terminal
$ java Main
── what you'll see ──
withdraw 300: false
withdraw 900: false
transfer 200: false
Asha has 0 after 1 transactions
Ben has 0 after 0 transactions

Myth vs fact

Myth

Each object stores its own copy of the methods' code.

Fact

Objects store only fields (plus a header). Method bytecode is stored once per class, and the receiver is passed in as the hidden this parameter.

Myth

Field initialisers run once, when the class loads.

Fact

Instance field initialisers run for every new object, as part of construction. Only static field initialisers run once, at class initialisation (Topic 4.6).

Myth

A getter method is always slower than reading the field.

Fact

The JIT compiler inlines small methods like getters, so after warm-up getBalance() costs the same as reading balance directly.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Bytecode view: a.deposit(500) compiles to aload_1; ldc2_w 500; invokevirtual BankAccount.deposit:(J)V. Inside the method, balance += amount is aload_0; dup; getfield balance; lload_1; ladd; putfield balance. Slot 0 is this, and the long parameter takes two slots (1 and 2).

  • ▸

    Field access is resolved at compile time by the static type, not the runtime class: fields are never polymorphic. Only methods are dispatched dynamically. This matters in Phase 5, where a subclass field with the same name hides rather than overrides.

  • ▸

    HotSpot lays out fields to minimise padding (bigger fields first, then smaller ones), not in source order, and may fill gaps in the header. Never rely on field declaration order for memory layout; it only controls initialisation order.

  • ▸

    Command-query separation (Bertrand Meyer) is a design rule, not a language rule. Methods like List.remove(int) both change state and return a value; that's fine when the result is information about the change. What you avoid is a "getter" with hidden side effects.

Remember this

  1. 1

    An instance field is declared inside the class body, outside every method. Each object gets its own copy, created when the object is created and kept as long as the object lives. A local variable is declared inside a method; it's created when the method starts and disappears when it returns (Topic 3.6).

  2. 2

    An instance method is a method without static. You call it on an object (account.deposit(50)), and inside it, a bare field name like balance means "the balance of the object this method was called on". The same method code works for every object because the JVM passes the object in as a hidden first parameter, this (Topic 4.4).

  3. 3

    Methods come in two flavours. Queries return information without changing anything (getBalance(), area()). Commands change the object's state (deposit(), birthday()). Keeping them separate makes code easier to reason about: calling a query twice never changes the answer.

  4. 4

    A field initialiser (int lives = 3;) runs for every new object, in the order the fields are written, before the constructor body. A field can't read a field declared below it in its initialiser (illegal forward reference), because that field hasn't been initialised yet.

  5. 5

    Putting the rule next to the data is the core idea of object-oriented programming. Instead of code everywhere doing acc.balance -= amount and forgetting to check for overdraft, one method withdraw owns that rule. Topic 4.5 then makes the field private so nobody can bypass the method.

  6. 6

    A static method like main has no object. That's why main can't call an instance method or read an instance field directly: there's no "this account" to use. You first create an object and call the method on it. Topic 4.6 explains static fully.

Explain it without notes

01

What is the difference between an instance field and a local variable, in terms of where it lives and how long it lives?

02

How can one copy of a method's bytecode work correctly for thousands of different objects?

03

Why does main get a compile error when it calls an instance method of Main directly?

04

In what order do field initialisers run, and what's the danger of a field initialiser calling a method?

Practice

01

Write a class Thermostat with a double target field and methods up() (adds 0.5), down() (subtracts 0.5) and describe() (returns "Target: <target>"). Start at 20.0, call up() three times and down() once, and print describe().

02

Write a class Player with String name and int score, a command addPoints(int p) that ignores negative values, and a query isWinning(Player other). Show it with two players.

03

Write a class Stopwatch with an int laps field and a method lap() that returns the new lap number. Call it three times and print each return value.

Trade-offs

  • ↔

    Methods that own their rules (withdraw checks the balance) remove duplicated checks across the codebase, but they only protect you if callers can't change the field directly. That's why the next step is private fields (Topic 4.5).

  • ↔

    Many small methods read well and the JIT inlines them, but very deep chains of tiny methods can make stack traces and debugging noisier. Name methods after what they do, and prefer a few well-named methods over many trivial wrappers.

Done when you can

  • Done when you can explain where instance fields and local variables live and how long each lasts.

  • Done when you can write classes whose methods read and change their own fields.

  • Done when you can tell a query method from a command method.

  • Done when you can explain the hidden this parameter and the static-context error.

  • Done when you can predict the order of field initialisers, including the forward-reference trap.