Java streams have no if-else operation, so we write the condition inside a lambda, where filter() keeps only the elements of the if branch, map() with a ternary or a switch picks a value per branch, and Collectors.partitioningBy() splits the elements into the if group and the else group in one pass. Which one we use depends on whether we need one branch, a value per element or both groups.
We need if-else logic in a stream whenever elements are handled differently, for example credits and debits on a bank statement, valid and invalid rows in a CSV import, or orders that ship free and orders that pay shipping. The Stream API gives each of these cases a clean form without a loop.
The following example applies the three techniques to the same list of payment amounts in cents.
List<Integer> amounts = List.of(120, -40, 75, -15);
List<Integer> credits = amounts.stream().filter(a -> a > 0).toList(); // [120, 75]
List<String> labels = amounts.stream().map(a -> a > 0 ? "credit" : "debit").toList(); // [credit, debit, credit, debit]
Map<Boolean, List<Integer>> split = amounts.stream().collect(Collectors.partitioningBy(a -> a > 0)); // {false=[-40, -15], true=[120, 75]}
Notice that none of the three lines changes a variable outside the stream. We start with the classic if-else inside forEach() and move on to filter(), map() with switch, partitioningBy(), groupingBy() for more than two branches and Optional for the else case of a single result, and finish with a bank statement example.
1. Choosing an if-else Technique for a Stream
A stream sends every element through the same chain of operations, so there is no branch in the chain itself. The branching happens per element, inside the lambda of an operation, and the operation we pick decides what the branches produce.

| What we need | Operation | Result |
|---|---|---|
| Run an action per element, different per branch | forEach() with if-else | Side effects only |
| Keep only the if branch | filter() | A stream of matching elements |
| A value for every element | map() with a ternary or switch | A stream of the same size |
| Both groups at once | Collectors.partitioningBy() | Map<Boolean, List<T>> |
| Three or more groups | Collectors.groupingBy() | Map<K, List<T>> |
| One result or a fallback | findFirst() plus Optional | A value or a default |
2. The if-else Condition Inside forEach()
The most direct form puts a normal if-else block in the Consumer that we pass to forEach(). It reads like a loop, and we can have as many else if branches as we need.
List<Integer> numbers = List.of(-1, 1, -2, 3, 0);
List<String> log = new ArrayList<>();
numbers.forEach(n -> {
if (n == 0) {
log.add("zero");
} else if (n > 0) {
log.add("positive");
} else {
log.add("negative");
}
});
List<String> result = log; // [negative, positive, negative, positive, zero]
The approach fits when each branch has a side effect, such as sending a message or writing a log line. It is a poor fit for building collections, because the lambda changes a list outside the stream. That code breaks under parallel() and hides the result, whereas map() or a collector returns it.
A named Consumer keeps the call short when the same branching is used in several places.
List<String> alerts = new ArrayList<>();
Consumer<Integer> notifyByAmount = cents -> {
if (cents >= 1000) {
alerts.add("call: " + cents);
} else {
alerts.add("email: " + cents);
}
};
List.of(250, 4000).forEach(notifyByAmount);
List<String> sent = alerts; // [email: 250, call: 4000]
3. Only the if Branch With filter()
When the else branch does nothing, the condition becomes a Predicate for filter(). The elements that fail the test are dropped, and the rest of the pipeline sees only the if branch.
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
Predicate<Integer> isEven = n -> n % 2 == 0;
List<Integer> evens = numbers.stream().filter(isEven).toList(); // [2, 4, 6]
List<Integer> odds = numbers.stream().filter(isEven.negate()).toList(); // [1, 3, 5]
Two filters with opposite conditions give both branches, but they walk the source twice. For a list in memory that is cheap, while for a stream from a file or a database it means reading the data twice or failing because a stream can be used only once. In that case we use partitioningBy() from section 5.
4. A Value per Branch With map()
When every element produces a result and only the result differs by branch, map() is the right operation. A ternary operator covers two branches, and a switch expression covers more without nested ternaries.
List<Integer> temperatures = List.of(-5, 12, 31);
List<String> advice = temperatures.stream().map(t -> t < 0 ? "coat" : "shirt").toList(); // [coat, shirt, shirt]
Since Java 21, a switch can test patterns with when guards, so a chain such as if-else if-else becomes a list of cases. We put it in a method, which the lambda calls, so the stream stays one line.
static String describe(Integer celsius) {
return switch (celsius) {
case Integer t when t < 0 -> "freezing";
case Integer t when t < 25 -> "mild";
default -> "hot";
};
}
List<Integer> temperatures = List.of(-5, 12, 31);
List<String> words = temperatures.stream().map(t -> describe(t)).toList(); // [freezing, mild, hot]
The cases are checked from top to bottom, like an if-else if chain, so the order of the guards matters. The default case plays the role of the final else.
5. Both Branches at Once With partitioningBy()
The collector Collectors.partitioningBy() sends each element to the true or false list in a single pass, and the result always contains both keys, even when one list is empty. We read the if branch with get(true) and the else branch with get(false).
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
Map<Boolean, List<Integer>> byParity = numbers.stream().collect(Collectors.partitioningBy(n -> n % 2 == 0));
List<Integer> evens = byParity.get(true); // [2, 4, 6]
List<Integer> odds = byParity.get(false); // [1, 3, 5]
Map<Boolean, Long> counts = numbers.stream().collect(Collectors.partitioningBy(n -> n > 4, Collectors.counting())); // {false=4, true=2}
Map<Boolean, List<Integer>> none = numbers.stream().collect(Collectors.partitioningBy(n -> n > 100)); // {false=[1, 2, 3, 4, 5, 6], true=[]}
The second argument is a downstream collector that summarizes each branch, such as counting(), summingInt() or mapping(). When we need two different results instead of two lists, such as the sum of the if branch and the count of the else branch, Collectors.teeing() combines two collectors in one pass.
6. More Than Two Branches With groupingBy()
A condition with three or more outcomes maps naturally to Collectors.groupingBy(). The classifier function returns the branch for each element, and the collector builds one list per branch.
enum Size { SMALL, MEDIUM, LARGE }
static Size sizeOf(int grams) {
if (grams < 100) {
return Size.SMALL;
} else if (grams < 1000) {
return Size.MEDIUM;
}
return Size.LARGE;
}
List<Integer> parcels = List.of(50, 400, 2500, 80, 900);
Map<Size, List<Integer>> bySize = parcels.stream().collect(Collectors.groupingBy(g -> sizeOf(g), () -> new EnumMap<>(Size.class), Collectors.toList())); // {SMALL=[50, 80], MEDIUM=[400, 900], LARGE=[2500]}
Unlike partitioningBy(), the map from groupingBy() contains only the branches that have elements. The EnumMap supplier keeps the keys in the order of the enum constants, whereas the default HashMap has no defined order.
7. The else Case for a Single Result
Sometimes the stream produces at most one element, for example the first order over a limit, and the else branch is what happens when nothing matches. The terminal operation findFirst() returns an Optional, and its methods express the if and the else without a null check.
List<Integer> orders = List.of(300, 800, 1500);
String firstLarge = orders.stream().filter(o -> o > 1000).findFirst().map(o -> "review " + o).orElse("nothing to review"); // "review 1500"
String noneLarge = orders.stream().filter(o -> o > 5000).findFirst().map(o -> "review " + o).orElse("nothing to review"); // "nothing to review"
int mustExist = orders.stream().filter(o -> o > 5000).findFirst().orElseThrow(); // NoSuchElementException
The method ifPresentOrElse() runs one of two actions, and orElseThrow() turns the else branch into an exception when a missing element is an error.
8. Splitting a Bank Statement Into Credits and Debits
A banking app shows a monthly statement with two totals, money in and money out, and marks every transaction above 1,000 euros for review. The transactions arrive as one list, and each rule is an if-else on the amount.
record Transaction(String description, int cents) {}
List<Transaction> statement = List.of(new Transaction("salary", 320000), new Transaction("rent", -95000), new Transaction("groceries", -8400), new Transaction("refund", 1200), new Transaction("laptop", -129900));
Map<Boolean, Integer> totals = statement.stream().collect(Collectors.partitioningBy(t -> t.cents() > 0, Collectors.summingInt(t -> t.cents())));
int moneyIn = totals.get(true); // 321200
int moneyOut = totals.get(false); // -233300
List<String> review = statement.stream().filter(t -> Math.abs(t.cents()) > 100000).map(t -> t.description()).toList(); // [salary, laptop]
List<String> lines = statement.stream().map(t -> (t.cents() > 0 ? "+ " : "- ") + t.description()).toList(); // [+ salary, - rent, - groceries, + refund, - laptop]
The totals come from one pass with partitioningBy() and summingInt(), the review list is a plain filter(), and the statement lines use map() with a ternary. Each rule stays one line, and none of them writes to a variable outside the stream.
9. Stream if-else FAQs
Moving branching code from loops into streams raises a few recurring questions.
9.1. Can We Use if-else Inside Stream map()?
Yes. A lambda with a block body can contain any if-else chain, as long as every branch returns a value of the same type. For two branches a ternary is shorter, and for longer chains a method with a switch keeps the stream readable.
9.2. Is Using filter() Twice Slower Than partitioningBy()?
Yes, in iteration work, because two filters walk the source twice. For a small list in memory the difference is too small to matter, but for a large source, an I/O-backed stream or a stream that can be consumed only once, partitioningBy() is the correct choice.
9.3. How Do We Apply Several Conditions in One filter()?
We combine them with && and || in one lambda, or chain named predicates with and() and or(). Chaining several filter() calls has the same effect as &&, as shown in multiple filters in streams.
9.4. How Do We Throw an Exception in the else Branch?
Inside map(), the else branch can throw an unchecked exception such as IllegalArgumentException, which stops the stream at that element. For a single result, orElseThrow() on the Optional throws NoSuchElementException, or the exception from the supplier we pass to it.
10. Conclusion
A stream branches inside its lambdas, never between operations. We use filter() when only the if branch continues, map() with a ternary or a switch when every element gets a value, and partitioningBy() or groupingBy() when we need the groups themselves.
The if-else block inside forEach() is still fine for actions such as logging or sending messages, but not for filling lists. For a single result, Optional handles the else branch with orElse() or orElseThrow().
11. References
- Collectors.partitioningBy() Javadoc (Java 25)
- Collectors.groupingBy() Javadoc
- Stream.filter() Javadoc
- JEP 441, Pattern Matching for switch
- Optional Javadoc
Happy Learning !!
int a; list.forEach(i-> { if(i%2==0) a=i; })how to handle this ? variable in lambda should be final or effectively final
Multiple ways to this:
1. use AtomicInteger instead of int.
AtomicInteger a = new AtomicInteger(0);list.forEach(i-> {
if(i%2==0)
a.set(i);
});
int answer = a.get();
2. Refactor: you are looking for last element divisible by 2.
int a = list.stream().filter(i -> i%2 == 0).reduce((first, second) -> second).orElse(null);Why do you suggested to use AtomicInteger instead of int?
The only application of AtomicReference<T> I know is to “explicitly” implement pass by reference — in other words, exchange multiple values to and pro a java method
C allows this by passing &var (in place of var). C++ too
Java allows a single return value. So use AtomicReference<T>
We can as well put all variables we wanna exchange in a single object, pass the object to the java method — but we might feel lazy and/or unnecessary to create a new class just for this purpose
May I know in which other situations are AtomicReference<T>, AtomicInteger, etc useful?