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),accis 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
= valuewritten 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.
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.
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).
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.
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.)
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();).
Try it yourself
- 1
Add a rule
In the bank account example, make
depositrefuse amounts of 0 or less: change it to returnboolean, returnfalsefor bad amounts, and print the result ofa.deposit(-50). Predict the transaction count before running. - 2
Swap the field order
In the
Gameexample, moveint later = 7;aboveint sneaky = readLater();. Predict the new value ofsneaky, then run. - 3
Call an instance method from main
Add
void hello() { System.out.println("hi"); }toMainand callhello();frommain. Read the error, then fix it withnew Main().hello();.
Code & diagrams
Expected output
withdraw 300: true
withdraw 900: false
transfer 200: true
Asha has 500 after 3 transactions
Ben has 200 after 1 transactionsExpected output
area: 12
perimeter: 14
square? false
after scale(2): 8 x 6
area: 48
area again 48`sneaky` is 0, not 7: `later` hadn't been initialised yet when `readLater()` ran.
Expected output
lives = 3
bonus = 6
total = 9
sneaky = 0Break 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;.
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).
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 toaload_1; ldc2_w 500; invokevirtual BankAccount.deposit:(J)V. Inside the method,balance += amountisaload_0; dup; getfield balance; lload_1; ladd; putfield balance. Slot 0 isthis, and thelongparameter 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
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
An instance method is a method without
static. You call it on an object (account.deposit(50)), and inside it, a bare field name likebalancemeans "thebalanceof 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
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
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
Putting the rule next to the data is the core idea of object-oriented programming. Instead of code everywhere doing
acc.balance -= amountand forgetting to check for overdraft, one methodwithdrawowns that rule. Topic 4.5 then makes the fieldprivateso nobody can bypass the method. - 6
A
staticmethod likemainhas no object. That's whymaincan'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 explainsstaticfully.
Explain it without notes
What is the difference between an instance field and a local variable, in terms of where it lives and how long it lives?
How can one copy of a method's bytecode work correctly for thousands of different objects?
Why does main get a compile error when it calls an instance method of Main directly?
In what order do field initialisers run, and what's the danger of a field initialiser calling a method?
Practice
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().
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.
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 (
withdrawchecks 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 isprivatefields (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
thisparameter and the static-context error.Done when you can predict the order of field initialisers, including the forward-reference trap.