Java
Java OOP
›Short versionOpen if you only have a minute.
- A class describes a kind of thing. An object is one live instance, with its own field values.
- An invariant is a rule that stays true between public method calls, such as a balance that only changes through deposit and withdraw.
- Encapsulation keeps fields private so the class methods are the only way to change that state.
- Inheritance fits when every instance of the subclass is an instance of the superclass for its whole life.
- Composition fits when one object contains another as a part. A statement has an account. A statement is not an account.
- Polymorphism lets a caller use a superclass or interface type while the runtime object supplies the overriding method body.
A transfer screen shows a balance of 80. Somewhere else in the program, a line writes balance = -40 because a refund was applied twice. The number on the screen is now a lie, and every later line that trusted the account has to discover the lie for itself.
Object-oriented programming in Java is the habit of putting that number, and the rule about it, in the same . An is one account. Its methods are the only operations that may change the balance, and they leave an standing when they return.
Own the account rule
Describe
A class names fields and the methods that may touch them.
Hide
Private fields force callers through your methods.
Enforce
Each public method restores the invariant before it returns.
Relate
Extend when is-a holds; compose when has-a is honest.
Encapsulation first
- Private balance means no distant line can assign -40 directly.
- deposit and withdraw are the public surface; they reject illegal amounts at the call.
Inheritance vs composition
- SavingsAccount extends Account when every savings account is an account for its whole life.
- Statement holds an Account field when the statement has an account but is not one.
Pick the relationship that matches the sentence
| Sentence | Tool | Why |
|---|---|---|
| Every savings account is an account | extends Account | Subclass can stand in for Account |
| Every statement has an account | private Account field | Outer type is not an account |
| Caller knows Account, object is SavingsAccount | Polymorphism | Runtime runs the override |
| balance only changes in deposit/withdraw | Invariant + encapsulation | Callers rely on the rule |
Read the diagram definition
sequenceDiagram
participant Caller
participant Account
participant Balance as private balance
Caller->>Account: withdraw(20)
Account->>Account: amount legal?
alt amount too large
Account-->>Caller: IllegalArgumentException
else ok
Account->>Balance: subtract amount
Account-->>Caller: return
end
Note over Account,Balance: invariant holds until next callThe account owns the rule
The class below is small on purpose. balance is private. Callers can deposit, withdraw, or read the current amount. They cannot assign the field. That is : the data and the operations travel together, and the operations are the public surface.
public class Account {
private int balance;
public Account(int opening) {
if (opening < 0) {
throw new IllegalArgumentException("opening balance");
}
balance = opening;
}
public void deposit(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("amount");
}
balance += amount;
}
public void withdraw(int amount) {
if (amount <= 0 || amount > balance) {
throw new IllegalArgumentException("amount");
}
balance -= amount;
}
public int balance() {
return balance;
}
}After new Account(80), the invariant holds: the balance is non-negative, and it changes only inside these methods. withdraw refuses an amount larger than the balance, so the caller hears about the mistake at the call, not three screens later.
$ javac Account.java$ java Account
A kind of account, or a thing that has one
is the right tool when the new type is a more specific account. A savings account is an account. It can live in an Account variable, and a caller can still withdraw. If savings accounts add interest, that method can be overridden. The rest of the program keeps talking to Account. That substitution is .
class SavingsAccount extends Account {
public SavingsAccount(int opening) {
super(opening);
}
public void addInterest(int rateBasisPoints) {
deposit(balance() * rateBasisPoints / 10_000);
}
}A monthly statement is a different relationship. The statement lists an account, a period, and a closing number. It is not an account. Handing a statement to a method that expects an Account would be a type lie. Give the statement a field. That is .
class Statement {
private final Account account;
private final String period;
public Statement(Account account, String period) {
this.account = account;
this.period = period;
}
public int closingBalance() {
return account.balance();
}
}The practical test is a sentence you can say without flinching. "Every savings account is an account" survives. "Every statement is an account" does not. When the sentence fails, the field is the honest model, and the outer class calls the part.
What the caller is allowed to assume
Abstraction, in this setting, means the caller depends on a small promise. "I can ask an account for its balance and I can withdraw a legal amount." The caller does not depend on whether the balance is an int, a long, or a later type that tracks currency. The methods are the promise. The fields are an implementation detail.
Overloading and overriding both reuse a method name, and they are chosen at different times. Overloading is static dispatch: the compiler picks deposit(int) or deposit(String) from the argument types it can see. Overriding is dynamic dispatch: account.withdraw(20) runs the method body of the object account points at, which may be a subclass.
A Car class needs an engine. Should Car extend Engine?
Car contains an engine. A car is not an engine, so a method written for Engine should not receive a Car. Declare private final Engine engine and construct the engine inside the car, or accept one that the car owns. That is composition. Extend a class only when the subclass can stand in for the superclass without breaking its rules.
Simple rule of thumb
Put state and the rule about that state in one class. Callers talk to methods, not fields. Extend only when the subtype can honor every superclass promise for the object's entire lifetime; otherwise hold the part in a field.
Good default: private fields, a constructor that rejects bad input, public methods that restore the invariant, and composition until you can say every instance of the subclass is an instance of the superclass without flinching.