Imperative, object-oriented, functional
Three ways of organising the same computation, what each optimises for, and why Java has all three.
A paradigm is a way of organising a program — what you treat as the main thing. The same computation can be arranged several ways, and the arrangement changes what is easy to change later. That last clause is the whole point: paradigms are not about what a program can compute, they are about what it costs to modify.
Java has three of them in it, which is why Java code written in 2005 and 2025 can look like different languages.
Imperative: a list of steps
The oldest arrangement, and the one that matches how the machine actually works. You describe how: do this, then that, changing state as you go.
int total = 0;
for (int i = 0; i < prices.length; i++) {
total = total + prices[i];
}There is a variable that changes, a loop that drives it, and an order you could narrate. Every program is imperative at the bottom, because the CPU has no other mode.
What it is good at: being obvious. You can read it top to bottom and know what happened.
What it costs: the state is exposed. Anything could change total between those lines, and as a method grows, keeping track of what has changed and when becomes the difficulty.
Object-oriented: data and behaviour together
Instead of a program that acts on data, you build things that hold their own data and know how to act on it. The unit is the object, and the discipline is that an object's insides are its own business.
class Cart {
private final List<Item> items; // nobody outside can touch this
Money total() { … } // they ask the cart instead
}The change is not syntax. It is who is allowed to know. A Cart that keeps its items private can change how it stores them without breaking anybody, because nobody was depending on how. That is the trade being made: a little ceremony now for the freedom to change later.
What it is good at: drawing lines around things that change together, and hiding what shifts behind something that does not.
What it costs: ceremony, and the temptation to make everything an object when a function would do. A codebase of OrderProcessorFactoryManager is object-orientation applied without judgement.
Functional: values in, values out
Organise around functions that take values and return values, and do not change anything. No variable is reassigned; nothing is modified in place. A new value is produced instead.
int total = Arrays.stream(prices).sum();Notice what is missing: no counter, no accumulator you can see, no loop you could get wrong by one. You said what you wanted rather than how to get it.
What it is good at: being safe to reason about, and to run in parallel. If nothing changes, nothing can change underneath you — which is most of the difficulty in concurrent programs.
What it costs: it can hide the cost. stream() looks free and is not, and a chain of five operations can be harder to debug than the loop it replaced, because there is no line to put a breakpoint on.
A loop in a costume
That last cost is worth being concrete about, because "functional" is the paradigm people most often adopt in appearance only. Here is a stream that looks functional and is not:
import java.util.stream.IntStream;
public class Costume {
public static void main(String[] args) {
int[] prices = IntStream.rangeClosed(1, 50_000).toArray();
int[] total = { 0 };
IntStream.of(prices).parallel().forEach(p -> total[0] += p);
System.out.println("mutating a variable: " + total[0]);
System.out.println("returning a value: " + IntStream.of(prices).parallel().sum());
}
}The sum of 1 to 50,000 is 1,250,025,000 — arithmetic, not opinion. Three runs of that program:
mutating a variable: 1241644883
returning a value: 1250025000
--
mutating a variable: 1194425176
returning a value: 1250025000
--
mutating a variable: 867830509
returning a value: 1250025000The second line is right every time. The first is wrong every time, and wrong differently every time — several threads read total[0], each adds its own price to the value it read, and the last one to write wins. The additions the others made simply vanish.
Look at what separates the two lines. Both use parallel(). Both are one expression. The difference is that one of them changes something outside itself and the other returns a value. That is the actual content of "functional", and it is why the word is not a synonym for "uses streams".
They are not a ranking
The common mistake is to read this as a progression — imperative is old, functional is modern, use the newest. That is wrong, and it produces code that is clever and unreadable.
They are tools with different grains:
| Situation | The grain that fits |
|---|---|
| A tight loop over an array, performance matters | imperative |
| A concept with rules about itself — an order, an account | object-oriented |
| A transformation of data with no side effects | functional |
Real Java uses all three in one file, and that is not sloppiness. A service class is object-oriented; the method inside it transforms a list functionally; the innermost loop is imperative because that is what is fastest and clearest there.
Misconceptions
- "Object-oriented means using classes." You can write entirely imperative code inside classes, and most beginners do. The paradigm is in whether the data is hidden and the behaviour lives with it, not in the keyword.
- "Functional means using streams." A stream that mutates a variable outside it is not functional, it is a loop in a costume — and a broken one, once threads are involved.
- "You should pick one and be consistent." Consistency within a method, yes. A whole codebase in one paradigm is a codebase fighting its language.