Class Methods
Methods defined inside a class give its objects behavior. There are two kinds: instance methods, which operate on a particular object and its fields, and static methods, which belong to the class as a whole. Choosing between them, and designing methods that protect an object's state, is central to writing object-oriented Java.
Instance Methods
An instance method is declared without static. It is called on an object, and inside it the fields refer to that object's values:
public class Thermostat {
private double current;
private double target;
public Thermostat(double current, double target) {
this.current = current;
this.target = target;
}
public void raiseTarget(double degrees) {
target += degrees;
}
public boolean isHeating() {
return current < target;
}
public String status() {
return String.format("current %.1f°, target %.1f°, %s",
current, target, isHeating() ? "heating" : "idle");
}
}
Thermostat living = new Thermostat(19.5, 21);
Thermostat bedroom = new Thermostat(20, 18);
living.raiseTarget(1);
System.out.println(living.status()); // current 19.5°, target 22.0°, heating
System.out.println(bedroom.status()); // current 20.0°, target 18.0°, idle
When living.raiseTarget(1) runs, target means living's target. The same method called on bedroom would change bedroom's target. Inside status, isHeating() with no object in front calls the method on the same object status was called on.
The implicit this
Every instance method receives a hidden reference to the object it was called on, available as this. Writing target inside the method is shorthand for this.target:
public void raiseTarget(double degrees) {
this.target += degrees; // identical to: target += degrees;
}
this becomes necessary when a parameter has the same name as a field, and when an object needs to pass itself to another method. The this keyword has its own lesson.
Static Methods
A static method is declared with static. It belongs to the class, is called on the class name, and has no this:
public class Temperature {
public static double celsiusToFahrenheit(double c) {
return c * 9 / 5 + 32;
}
public static double fahrenheitToCelsius(double f) {
return (f - 32) * 5 / 9;
}
}
double f = Temperature.celsiusToFahrenheit(37); // 98.6
The conversion depends only on its argument, not on any object's state, so there is no reason to create a Temperature object to perform it. Math.max, Integer.parseInt, List.of, and Arrays.sort are static methods for the same reason.
What static methods cannot do
Because a static method has no object, it cannot use instance fields or call instance methods directly:
public class Thermostat {
private double target;
public static void reset() {
target = 20; // compile error: non-static variable target cannot be
// referenced from a static context
}
}
There is no single target to reset: every Thermostat object has its own. A static method can work with instances only if it is given one, for example as a parameter.
Instance Versus Static: How to Choose
| Question | If yes |
|---|---|
| Does the method read or change an object's fields? | Instance method |
| Does the result differ between objects of the class? | Instance method |
| Does it depend only on its parameters? | Static method |
| Is it a factory that creates instances? | Static method |
| Is it a general utility (math, parsing, validation)? | Static method, often in a utility class |
public class Rectangle {
private final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double area() { // instance: uses this object's fields
return width * height;
}
public boolean fitsInside(Rectangle other) { // instance: compares this with another
return width <= other.width && height <= other.height;
}
public static Rectangle square(double side) { // static factory
return new Rectangle(side, side);
}
public static Rectangle parse(String text) { // static factory from text like "4x3"
String[] parts = text.toLowerCase().split("x");
return new Rectangle(Double.parseDouble(parts[0].strip()),
Double.parseDouble(parts[1].strip()));
}
}
Rectangle box = Rectangle.parse("40 x 30");
Rectangle tile = Rectangle.square(25);
System.out.println(box.area()); // 1200.0
System.out.println(tile.fitsInside(box)); // true
Static factory methods
Methods like square and parse are static factory methods: alternatives to constructors. They have advantages:
- They have descriptive names (
square(25)says more thannew Rectangle(25, 25)). - They can return a cached instance instead of always creating a new one (
Integer.valueOf,List.of()). - They can return a subtype, hiding which implementation is used.
The JDK uses them widely: LocalDate.of(...), Path.of(...), Optional.empty(), String.valueOf(...).
Kinds of Instance Methods
Accessors (getters)
Return information about the object without changing it:
public double getTarget() { return target; }
public boolean isHeating() { return current < target; }
By convention, accessors start with get, or is for booleans. Some accessors return stored values; others, like isHeating, compute an answer from the state.
Mutators
Change the object's state. Good mutators validate their input so that the object can never enter an invalid state:
public void setTarget(double target) {
if (target < 5 || target > 35) {
throw new IllegalArgumentException("target must be between 5 and 35");
}
this.target = target;
}
Named, intention-revealing mutators are usually better than generic setters:
public void raiseTarget(double degrees) { setTarget(target + degrees); }
public void lowerTarget(double degrees) { setTarget(target - degrees); }
Here, the range check lives in exactly one place, and every route that changes target passes through it.
Query methods and command methods
A useful design rule is to separate queries (return information, change nothing) from commands (change state, return nothing or a status). A method named getBalance that also deducts a fee surprises every caller. Keeping the two separate makes code easier to reason about and safer to call repeatedly.
Returning this for Method Chaining
A method that modifies the object can return this, allowing several calls in a single expression:
public class EmailBuilder {
private String to, subject, body = "";
public EmailBuilder to(String to) { this.to = to; return this; }
public EmailBuilder subject(String subject) { this.subject = subject; return this; }
public EmailBuilder line(String text) { this.body += text + "\n"; return this; }
public String build() {
return "To: " + to + "\nSubject: " + subject + "\n\n" + body;
}
}
String email = new EmailBuilder()
.to("ada@example.com")
.subject("Course enrollment")
.line("Hi Ada,")
.line("You are enrolled in Java Basics.")
.build();
Each call returns the same builder, so the next call can follow directly. StringBuilder.append, HttpRequest.newBuilder(), and many configuration APIs use this pattern.
Methods Calling Other Methods
Methods within a class call each other by name. Breaking a complex operation into smaller private methods keeps each piece understandable:
public class Payroll {
private static final double TAX_RATE = 0.10;
private static final double OVERTIME_MULTIPLIER = 1.5;
public double netPay(double hourlyRate, double hours) {
double gross = grossPay(hourlyRate, hours);
return gross - tax(gross);
}
private double grossPay(double rate, double hours) {
double regular = Math.min(hours, 40);
double overtime = Math.max(hours - 40, 0);
return regular * rate + overtime * rate * OVERTIME_MULTIPLIER;
}
private double tax(double amount) {
return amount * TAX_RATE;
}
}
netPay is the public operation; grossPay and tax are private helpers, implementation details hidden from other classes. They can be rewritten freely without affecting any code outside Payroll.
Overloaded Methods
A class can have several methods with the same name and different parameter lists:
public void raiseTarget() { raiseTarget(0.5); } // default step
public void raiseTarget(double degrees) { setTarget(target + degrees); }
The compiler chooses the version matching the call's arguments. Overloading has its own lesson.
Practical Example: Bank Account
import java.util.ArrayList;
import java.util.List;
public class BankAccount {
private static int nextNumber = 1001; // shared counter
private final int number;
private final String owner;
private double balance;
private final List<String> history = new ArrayList<>();
private BankAccount(String owner) {
this.number = nextNumber++;
this.owner = owner;
}
public static BankAccount open(String owner, double initialDeposit) {
BankAccount acc = new BankAccount(owner);
acc.deposit(initialDeposit);
return acc;
}
public void deposit(double amount) {
requirePositive(amount);
balance += amount;
record("DEP", amount);
}
public void withdraw(double amount) {
requirePositive(amount);
if (amount > balance) {
throw new IllegalStateException("Insufficient funds in " + number);
}
balance -= amount;
record("WDR", amount);
}
public void transferTo(BankAccount target, double amount) {
this.withdraw(amount);
target.deposit(amount);
}
public double getBalance() { return balance; }
public List<String> getHistory() { return List.copyOf(history); }
private void record(String type, double amount) {
history.add(type + " " + amount + " -> " + balance);
}
private static void requirePositive(double amount) {
if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
}
@Override
public String toString() {
return "#" + number + " " + owner + ": " + balance;
}
public static void main(String[] args) {
BankAccount ada = BankAccount.open("Ada", 1000);
BankAccount linus = BankAccount.open("Linus", 200);
ada.transferTo(linus, 300);
linus.withdraw(100);
System.out.println(ada);
System.out.println(linus);
System.out.println(linus.getHistory());
}
}
Output:
#1001 Ada: 700.0
#1002 Linus: 400.0
[DEP 200.0 -> 200.0, DEP 300.0 -> 500.0, WDR 100.0 -> 400.0]
The constructor is private, so accounts can only be created through the static factory open, which guarantees every account starts with a validated deposit. nextNumber is static because it is shared across all accounts; number, owner, balance, and history are instance fields because each account has its own. transferTo is an instance method that operates on two objects: this (the source) and target. record is a private instance helper that needs the object's balance; requirePositive is a private static helper because it checks only its argument. getHistory returns an unmodifiable copy so callers cannot alter the account's records.