Lambdas em Java transformam um pequeno comportamento num valor, e streams encadeiam esses valores em pipelines preguiçosos. Veja como um pipeline roda de verdade, quais collectors usar e quando um laço simples fica mais claro.
Uma lambda é um trecho curto de código que você passa adiante como se fosse um valor. Um stream é um pipeline que leva uma sequência de valores por etapas feitas dessas lambdas. Juntos, eles substituem muitos dos laços que enchiam o código Java mais antigo.
Este post cobre lambdas e as interfaces funcionais por trás delas, referências de método, o que uma lambda pode capturar, pipelines de stream, avaliação preguiçosa, collectors, streams primitivos, reduce, gatherers e os casos em que um laço ainda é a ferramenta certa. Todo programa abaixo rodou no Java 25, e a saída foi colada da execução. Para rodar um você mesmo, salve como Main.java e rode java Main.java.
De classe anônima a lambda
Uma lambda é um jeito mais curto de escrever um objeto que implementa uma interface com um único método abstrato. Ordenar palavras pelo tamanho mostra o caminho. Aqui está o mesmo Comparator escrito de três jeitos:
List<String> sortedBy(List<String> words, Comparator<String> order) {
var copy = new ArrayList<>(words);
copy.sort(order);
return copy;
}
void main() {
var words = List.of("banana", "fig", "cherry", "kiwi");
var anonymous = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
Comparator<String> lambda = (a, b) -> Integer.compare(a.length(), b.length());
Comparator<String> byKey = Comparator.comparing(String::length);
IO.println(sortedBy(words, anonymous));
IO.println(sortedBy(words, lambda));
IO.println(sortedBy(words, byKey));
}
Ele imprime:
[fig, kiwi, banana, cherry]
[fig, kiwi, banana, cherry]
[fig, kiwi, banana, cherry]
A classe anônima escreve por extenso a interface, o nome do método e os tipos dos parâmetros. A lambda fica só com os parâmetros e o corpo, porque o compilador já sabe que o alvo é um Comparator<String>, e um Comparator tem um método para preencher. A terceira linha nem escreve uma comparação: Comparator.comparing monta uma a partir de uma chave, o tamanho.
banana fica na frente de cherry em todas as linhas, embora as duas tenham seis letras. List.sort é estável, então elementos iguais mantêm a ordem original.
Interfaces funcionais
Uma interface funcional é uma interface com exatamente um método abstrato, e uma lambda pode fazer o papel de qualquer uma delas. O JDK traz um conjunto de interfaces genéricas em java.util.function, então raramente você precisa criar a sua:
@FunctionalInterface
interface PriceRule {
int apply(int cents);
}
void main() {
Function<String, Integer> length = s -> s.length();
Predicate<String> isLong = s -> s.length() > 4;
Supplier<List<String>> fresh = () -> new ArrayList<>();
Consumer<String> shout = s -> IO.println(s.toUpperCase() + "!");
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
UnaryOperator<String> tidy = s -> s.strip();
PriceRule discount = cents -> cents * 90 / 100;
IO.println(length.apply("coffee"));
IO.println(isLong.test("tea"));
var list = fresh.get();
list.add("new");
IO.println(list);
shout.accept("hello");
IO.println(add.apply(2, 3));
IO.println("[" + tidy.apply(" milk ") + "]");
IO.println(discount.apply(500));
}
Ele imprime:
6
false
[new]
HELLO!
5
[milk]
450
Cada uma tem o nome do seu formato:
Function<T, R>recebe umTe retorna umR. Você chama comapply.Predicate<T>recebe umTe retorna umboolean, comtest.Supplier<T>não recebe nada e retorna umT, comget.Consumer<T>recebe umTe não retorna nada, comaccept.BiFunction<T, U, R>recebe dois argumentos e retorna umR.UnaryOperator<T>é umaFunction<T, T>: o mesmo tipo na entrada e na saída.
PriceRule é nossa. @FunctionalInterface é opcional, mas faz o compilador conferir a regra do “exatamente um”. Adicionamos um segundo método abstrato, int undo(int cents), e o build falhou com Unexpected @FunctionalInterface annotation, seguido de multiple non-overriding abstract methods found in interface PriceRule.
Referências de método, quatro tipos
Uma referência de método, escrita com ::, é uma lambda cujo corpo inteiro é uma chamada de método. Existem quatro tipos, e a diferença está em de onde vem o objeto:
record Point(int x, int y) {}
void main() {
// static method: s -> Integer.parseInt(s)
Function<String, Integer> parse = Integer::parseInt;
// method on one object you already have: s -> prefix.concat(s)
String prefix = "id-";
Function<String, String> tag = prefix::concat;
// method on whatever object arrives: s -> s.toUpperCase()
Function<String, String> upper = String::toUpperCase;
// constructor: (x, y) -> new Point(x, y)
BiFunction<Integer, Integer, Point> make = Point::new;
IO.println(parse.apply("42") + 1);
IO.println(tag.apply("7"));
IO.println(upper.apply("tea"));
IO.println(make.apply(3, 4));
}
Ele imprime:
43
id-7
TEA
Point[x=3, y=4]
O comentário acima de cada linha é a lambda que ela substitui. A que mais confunde é String::toUpperCase. Parece uma chamada estática, mas toUpperCase é um método de instância, então o primeiro argumento vira o objeto em que o método roda.
O que uma lambda pode capturar
Uma lambda pode ler variáveis locais do método em volta dela, mas só se elas forem final ou efetivamente final, ou seja, nada atribui um novo valor a elas depois de definidas. Contar com uma lambda quebra essa regra:
void main() {
int count = 0;
List.of("a", "b", "c").forEach(s -> count++);
IO.println(count);
}
O build falha com:
Main.java:3: error: local variables referenced from a lambda expression must be final or effectively final
List.of("a", "b", "c").forEach(s -> count++);
^
Uma lambda pode viver mais que o método que a criou, por exemplo quando fica guardada num campo ou roda em outra thread. Por isso o Java copia o valor para dentro da lambda, em vez de compartilhar a variável. Se a variável pudesse mudar depois, a cópia e o original iam discordar, e o Java proíbe isso em tempo de compilação. Campos não têm essa limitação, porque a lambda chega a eles através de um objeto.
Mais uma diferença em relação às classes anônimas: dentro de uma lambda, this significa o mesmo que no código em volta.
void main() {
Runnable lambda = () -> IO.println("lambda this: " + this.getClass().getName());
Runnable anonymous = new Runnable() {
@Override
public void run() {
IO.println("anonymous this: " + this.getClass().getName());
}
};
lambda.run();
anonymous.run();
}
Ele imprime:
lambda this: Main
anonymous this: Main$1
O this da lambda é o objeto Main em que main roda. A classe anônima é uma classe nova, Main$1, então o this dela é ela mesma.
Um pipeline de stream: fonte, etapas, resultado
Um pipeline de stream tem três partes: uma fonte, qualquer número de operações intermediárias e uma operação terminal que produz um resultado. Aqui está uma lista de pedidos limpa numa passada só:
void main() {
var orders = List.of("tea", "coffee", "tea", "juice", "water", "coffee", "milk");
var result = orders.stream()
.filter(o -> !o.equals("water"))
.distinct()
.map(String::toUpperCase)
.sorted()
.limit(3)
.toList();
IO.println(result);
IO.println(orders);
}
Ele imprime:
[COFFEE, JUICE, MILK]
[tea, coffee, tea, juice, water, coffee, milk]
- Fonte:
orders.stream(). Coleções, arrays,Stream.of(...)e arquivos podem ser fontes. - Operações intermediárias:
filtermantém os elementos que passam numPredicate,distinctdescarta repetidos,maptransforma cada elemento em outra coisa,sortedordena elimitfica com os primeiros. Cada uma retorna um stream novo. - Operação terminal:
toList()coleta o que sobrou.
A última linha mostra que a lista de origem continua intacta. Um stream lê a fonte. Ele não a altera.
toList() ou collect(Collectors.toList())
Stream.toList() chegou no Java 16. Conferimos: com javac --release 15, a chamada falha com cannot find symbol. Antes disso, você escrevia collect(Collectors.toList()), e muito código ainda escreve. Os dois não são exatamente iguais:
void main() {
var viaCollect = Stream.of("b", "a").collect(Collectors.toList());
viaCollect.add("c");
IO.println(viaCollect);
var viaToList = Stream.of("b", "a").toList();
IO.println(viaToList);
viaToList.add("c");
}
Ele imprime e para:
[b, a, c]
[b, a]
Exception in thread "main" java.lang.UnsupportedOperationException
toList() retorna uma lista não modificável, então add lança exceção. Collectors.toList() não promete nada nem num sentido nem no outro, e hoje ele acaba devolvendo um ArrayList. Use toList(), a menos que você realmente precise mudar a lista depois. Se precisar, deixe isso explícito com Collectors.toCollection(ArrayList::new).
Avaliação preguiçosa: nada roda até você pedir
Operações intermediárias não fazem trabalho nenhum quando você as chama. Elas só descrevem o trabalho. O trabalho começa quando uma operação terminal pede um resultado:
void main() {
var pipeline = Stream.of(1, 2, 3).map(n -> {
IO.println("map saw " + n);
return n * 10;
});
IO.println("pipeline built");
var result = pipeline.toList();
IO.println(result);
}
Ele imprime:
pipeline built
map saw 1
map saw 2
map saw 3
[10, 20, 30]
pipeline built vem primeiro. Quando a linha do map rodou, ela não chamou a lambda nem uma vez. Ela retornou um stream que lembra “multiplique por dez”. A lambda só rodou quando toList() puxou os elementos pelo pipeline.
Um elemento de cada vez
Um stream não roda filter na lista inteira e depois map no resultado. Cada elemento percorre o pipeline todo antes de o próximo começar:
void main() {
var result = Stream.of("apple", "fig", "cherry", "kiwi")
.filter(w -> {
IO.println("filter " + w);
return w.length() > 3;
})
.map(w -> {
IO.println("map " + w);
return w.toUpperCase();
})
.toList();
IO.println(result);
}
Ele imprime:
filter apple
map apple
filter fig
filter cherry
map cherry
filter kiwi
map kiwi
[APPLE, CHERRY, KIWI]
Leia de cima para baixo. apple passa no filtro e vai direto para o map. fig não passa no filtro, então o map nunca o vê. Depois cherry vai até o fim, e então kiwi. Um laço com um if dentro faz exatamente a mesma coisa, e é essa a ideia: um stream é um laço que você descreve em vez de escrever.
sorted é a exceção. Ele não pode passar nada adiante até ter visto todos os elementos, então primeiro junta todos:
void main() {
var result = Stream.of("kiwi", "apple", "fig")
.peek(w -> IO.println("before sorted " + w))
.sorted()
.peek(w -> IO.println("after sorted " + w))
.toList();
IO.println(result);
}
Ele imprime:
before sorted kiwi
before sorted apple
before sorted fig
after sorted apple
after sorted fig
after sorted kiwi
[apple, fig, kiwi]
peek roda um Consumer em cada elemento que passa, o que ajuda a enxergar o fluxo. sorted e distinct são chamadas de operações stateful (com estado), porque precisam guardar elementos. filter e map são stateless.
Curto-circuito: parar cedo, até num stream infinito
Algumas operações param o pipeline assim que têm a resposta. findFirst para no primeiro elemento que chega até ele, e limit(n) para depois de n. É isso que torna segura uma fonte infinita:
void main() {
var firstBig = Stream.iterate(1, n -> n * 2)
.peek(n -> IO.println("looked at " + n))
.filter(n -> n > 20)
.findFirst();
IO.println(firstBig);
var firstFive = Stream.iterate(1, n -> n * 2).limit(5).toList();
IO.println(firstFive);
}
Ele imprime:
looked at 1
looked at 2
looked at 4
looked at 8
looked at 16
looked at 32
Optional[32]
[1, 2, 4, 8, 16]
Stream.iterate(1, n -> n * 2) descreve 1, 2, 4, 8 e assim por diante, para sempre. O pipeline olhou seis valores, achou 32 e parou. findFirst retorna um Optional, porque um stream pode não ter primeiro elemento. A parte sobre Optional trata disso. Troque findFirst() por toList() e o programa nunca termina.
A avaliação preguiçosa pode pular suas lambdas
A avaliação preguiçosa trouxe uma surpresa que a gente não esperava. Uma operação terminal pode decidir que nem precisa rodar o pipeline:
void main() {
long quick = Stream.of(1, 2, 3)
.peek(n -> IO.println("peek " + n))
.count();
IO.println("count " + quick);
long slow = Stream.of(1, 2, 3)
.filter(n -> n > 1)
.peek(n -> IO.println("peek " + n))
.count();
IO.println("count " + slow);
}
Ele imprime:
count 3
peek 2
peek 3
count 2
O primeiro peek nunca rodou. Stream.of(1, 2, 3) sabe que tem três elementos, peek não pode mudar isso, então count responde 3 sem puxar nada pelo pipeline. Quando há um filter no caminho, o tamanho não é conhecido, e todo elemento precisa passar. O JDK faz isso desde o Java 9, e a documentação de Stream permite. Então não coloque dentro de peek ou map um trabalho do qual você depende. Uma lambda num stream deve calcular um valor, não causar um efeito.
Explicado como se você tivesse dez anos
Um stream é uma linha de montagem numa fábrica de brinquedos. Ao longo da linha ficam os trabalhadores: um joga fora os brinquedos quebrados, um pinta os bons, um coloca na caixa. Bem no final fica um cliente.
Montar a linha não faz nenhum brinquedo. Nada se move até o cliente no final dizer “quero um brinquedo pronto”.
Aí o primeiro brinquedo percorre a linha inteira: conferido, pintado, embalado, entregue. Só então o segundo brinquedo começa. Se o cliente só queria um brinquedo, a linha para, e os outros brinquedos nunca saem da pilha.
A versão precisa
Um pipeline de stream é uma cadeia de objetos de estágio. Cada operação intermediária, como filter ou map, acrescenta um estágio e retorna na hora. A operação terminal dá a partida na avaliação. Ela pede elementos à fonte um por um, e cada elemento é empurrado pela lambda de cada estágio, em ordem, antes de a fonte receber o pedido do próximo.
Estágios stateless como filter e map tratam um elemento e o passam adiante na hora. Estágios stateful como sorted seguram os elementos até a fonte esvaziar. Operações de curto-circuito, findFirst, anyMatch, limit e companhia, avisam que terminaram, e a fonte para de produzir. Como a operação terminal vê o pipeline inteiro antes de começar, ela também pode pular estágios cujo resultado não precisa, como o count fez.
Onde a analogia falha: uma linha de montagem de verdade com um pintor lento teria brinquedos esperando na fila entre os trabalhadores. Um stream sequencial nunca tem. Não existe fila nem segundo brinquedo a caminho, exceto quando um estágio stateful como sorted para a linha para juntar tudo. E com parallel(), várias linhas rodam ao mesmo tempo e os elementos não chegam em ordem.
Vendo findFirst parar um stream
A animação acompanha Stream.of(1, 2, 3, 4, 5).filter(n -> n % 2 == 1).map(n -> n * 10).findFirst(), que retorna Optional[10]:
Um stream de cinco elementos puxado por filter, map e findFirst. Nada se move até findFirst pedir. O elemento 1 passa no filtro, vira 10 e chega a findFirst, que retorna Optional[10]. O stream para ali, e 2 a 5 nunca são lidos.
Aqui estão esses passos em palavras, caso a animação não rode para você:
Stream.of,filteremapretornam na hora. Eles montaram um pipeline, e nenhum elemento se moveu.findFirst()é a operação terminal. Ele pede um elemento ao pipeline, e o pedido chega à fonte.- A fonte entrega o
1. O filtro testa1 % 2 == 1, que é verdadeiro, então o1passa. - O
1entra nomape sai como10. - O
10chega afindFirst(). Era tudo de que ele precisava, então ele embrulha o valor comoOptional[10]. findFirst()avisa o pipeline que terminou. A fonte nunca entrega 2, 3, 4 ou 5, e nenhuma lambda roda para eles.
Um stream só pode ser usado uma vez
Um stream é uma viagem só de ida pela fonte, então uma segunda operação terminal no mesmo objeto stream falha:
void main() {
var words = Stream.of("tea", "coffee", "milk");
IO.println(words.count());
IO.println(words.count());
}
Ele imprime e para:
3
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
A solução é voltar à fonte, list.stream(), toda vez que você precisar de um pipeline novo. Para deixar outro código criar streams novos, passe a coleção, não um stream.
Collectors: agrupar, particionar, juntar
collect é a operação terminal que monta algo a partir dos elementos, e Collectors guarda os montadores prontos. Aqui estão vendas agrupadas, divididas e juntadas:
record Sale(String city, String product, int amount) {}
void main() {
var sales = List.of(
new Sale("Lisbon", "tea", 12),
new Sale("Porto", "coffee", 30),
new Sale("Lisbon", "coffee", 25),
new Sale("Braga", "tea", 8),
new Sale("Porto", "tea", 15));
Map<String, Long> perCity = sales.stream()
.collect(Collectors.groupingBy(Sale::city, TreeMap::new, Collectors.counting()));
IO.println(perCity);
Map<Boolean, List<Integer>> bigOrSmall = sales.stream()
.map(Sale::amount)
.collect(Collectors.partitioningBy(a -> a >= 20));
IO.println(bigOrSmall);
String cities = sales.stream()
.map(Sale::city)
.distinct()
.sorted()
.collect(Collectors.joining(", ", "<", ">"));
IO.println(cities);
}
Ele imprime:
{Braga=1, Lisbon=2, Porto=2}
{false=[12, 8, 15], true=[30, 25]}
<Braga, Lisbon, Porto>
groupingBy(key, mapFactory, downstream)coloca os elementos em grupos pela chave e depois roda um segundo collector em cada grupo.counting()transforma cada grupo no seu tamanho. Sem oTreeMap::new, você recebe umHashMap, que não tem uma ordem útil na hora de imprimir. OTreeMapmantém as cidades ordenadas.partitioningBy(predicate)sempre cria exatamente dois grupos,falseetrue, mesmo quando um deles fica vazio.joining(separator, prefix, suffix)cola strings. Ele só funciona num stream de strings, e é por isso que omapvem antes.
toMap e chaves duplicadas
Collectors.toMap monta um map a partir de uma função de chave e uma função de valor. Quando dois elementos produzem a mesma chave, você precisa dizer o que acontece, ou o stream lança exceção:
record Sale(String city, int amount) {}
void main() {
var sales = List.of(new Sale("Lisbon", 12), new Sale("Porto", 30), new Sale("Lisbon", 25));
Map<String, Integer> totals = sales.stream()
.collect(Collectors.toMap(Sale::city, Sale::amount, Integer::sum, TreeMap::new));
IO.println(totals);
Map<String, Integer> broken = sales.stream()
.collect(Collectors.toMap(Sale::city, Sale::amount));
IO.println(broken);
}
Ele imprime e para:
{Lisbon=37, Porto=30}
Exception in thread "main" java.lang.IllegalStateException: Duplicate key Lisbon (attempted merging values 12 and 25)
O terceiro argumento, Integer::sum, é a função de merge. Ela recebe o valor antigo e o novo e retorna o que manter, então as vendas de Lisbon somaram 37. Sem ela, o segundo Lisbon lança IllegalStateException. A mensagem mostra a chave e os dois valores. Para um map de chave para contagem ou soma, groupingBy com counting() ou summingInt diz a mesma coisa de forma mais direta.
Streams primitivos
IntStream, LongStream e DoubleStream guardam valores int, long e double puros, e têm os métodos numéricos que faltam em Stream<Integer>:
void main() {
IO.println(IntStream.range(0, 5).sum());
IO.println(IntStream.rangeClosed(1, 5).boxed().toList());
var words = List.of("tea", "coffee", "milk");
int letters = words.stream().mapToInt(String::length).sum();
OptionalDouble average = words.stream().mapToInt(String::length).average();
IO.println(letters);
IO.println(average);
IO.println(average.orElse(0));
IO.println(IntStream.empty().average());
}
Ele imprime:
10
[1, 2, 3, 4, 5]
13
OptionalDouble[4.333333333333333]
4.333333333333333
OptionalDouble.empty
range(0, 5) vai de 0 a 4, e rangeClosed(1, 5) inclui o 5. boxed() volta para um Stream<Integer> quando você precisa de uma List.
average() retorna um OptionalDouble, não um double, porque um stream vazio não tem média. A última linha mostra esse caso.
mapToInt é o mais importante. words.stream().map(String::length) daria um Stream<Integer>: cada tamanho embrulhado num objeto Integer, e nenhum método sum() para chamar. mapToInt dá um IntStream de ints puros. Nenhum objeto wrapper é criado, e sum, average, min e max vêm junto.
reduce, em poucas palavras
reduce combina todos os elementos num único valor, aplicando uma função par a par:
void main() {
int total = Stream.of(3, 4, 5).reduce(0, Integer::sum);
Optional<Integer> product = Stream.of(3, 4, 5).reduce((a, b) -> a * b);
Optional<Integer> nothing = Stream.<Integer>empty().reduce((a, b) -> a * b);
IO.println(total);
IO.println(product);
IO.println(nothing);
IO.println(IntStream.of(3, 4, 5).sum());
}
Ele imprime:
12
Optional[60]
Optional.empty
12
Com um valor inicial, 0 aqui, você recebe um resultado simples. Sem ele, você recebe um Optional, já que pode não haver nada para combinar. reduce é genérico, então quem lê precisa descobrir o que ele calcula. Para somas, contagens, mínimos, máximos e junções, a versão com nome (sum(), count(), max(...), Collectors.joining) diz isso diretamente, como faz a última linha.
Gatherers: etapas intermediárias sob medida
Um gatherer é uma operação intermediária sob medida, usada com Stream.gather. Os gatherers viraram definitivos no Java 24. Conferimos: com javac --release 23, o build falha com Gatherers is a preview API and is disabled by default, e no Java 25 nenhuma flag é necessária. Gatherers traz alguns prontos, como janelas fixas e deslizantes:
void main() {
var readings = List.of(3, 5, 4, 8, 9, 2, 7);
IO.println(readings.stream().gather(Gatherers.windowFixed(3)).toList());
IO.println(readings.stream().gather(Gatherers.windowSliding(3)).toList());
var movingAverage = readings.stream()
.gather(Gatherers.windowSliding(3))
.map(w -> w.stream().mapToInt(Integer::intValue).average().orElseThrow())
.toList();
IO.println(movingAverage);
}
Ele imprime:
[[3, 5, 4], [8, 9, 2], [7]]
[[3, 5, 4], [5, 4, 8], [4, 8, 9], [8, 9, 2], [9, 2, 7]]
[4.0, 5.666666666666667, 7.0, 6.333333333333333, 6.0]
windowFixed(3) corta o stream em grupos de três, e o último grupo fica com o que sobrar. windowSliding(3) anda um elemento por vez, que é o que uma média móvel precisa. Antes dos gatherers, agrupar elementos vizinhos exigia um laço com índices. Você também pode escrever o seu com Gatherer.of.
Quando um laço é mais claro
Streams são bons em “pegue estes, fique com alguns, transforme, colete”. Fora desse formato, quatro casos aparecem o tempo todo.
Exceções checadas. Nenhuma das interfaces funcionais de java.util.function declara uma exceção checada, então uma lambda não pode lançar uma:
int parsePort(String s) throws IOException {
if (s.isBlank()) {
throw new IOException("blank port");
}
return Integer.parseInt(s);
}
void main() {
var ports = Stream.of("80", "443").map(s -> parsePort(s)).toList();
IO.println(ports);
}
O build falha com:
Main.java:9: error: unreported exception IOException; must be caught or declared to be thrown
var ports = Stream.of("80", "443").map(s -> parsePort(s)).toList();
^
As saídas são capturar a exceção dentro da lambda e embrulhá-la numa não checada, ou escrever um helper que faça isso por você. As duas escondem a exceção da assinatura do método. Um laço for pode simplesmente deixá-la propagar.
Mudar estado local, e sair cedo. Aqui está “compre os itens em ordem até o próximo estourar o orçamento”, uma vez como stream e outra como laço:
void main() {
var prices = List.of(40, 25, 30, 50, 10);
int budget = 100;
int[] spentBox = {0};
long boughtByStream = prices.stream()
.takeWhile(p -> {
if (spentBox[0] + p > budget) {
return false;
}
spentBox[0] += p;
return true;
})
.count();
IO.println("stream: bought " + boughtByStream + ", spent " + spentBox[0]);
int spent = 0;
int bought = 0;
for (int price : prices) {
if (spent + price > budget) {
break;
}
spent += price;
bought++;
}
IO.println("loop: bought " + bought + ", spent " + spent);
}
Ele imprime:
stream: bought 3, spent 95
loop: bought 3, spent 95
Os dois dão a mesma resposta. A versão com stream precisa de um array de um elemento, porque a lambda não pode atribuir valor a uma variável local, então ela muda o conteúdo de um array. O predicado do takeWhile tem um efeito colateral, exatamente o que a surpresa do count lá atrás desaconselhou. O laço diz o que acontece na ordem em que acontece, e o break é a saída antecipada, sem truque.
Depuração. Num laço, você põe um breakpoint numa linha e olha todas as variáveis. Num pipeline, o código que você escreveu roda dentro da biblioteca de streams. Os stack traces se enchem de frames de java.util.stream e de nomes gerados como lambda$main$0, e um breakpoint cai dentro de uma lambda sem nenhuma variável de laço por perto. Se você precisa de várias chamadas a peek para entender um pipeline, ele provavelmente quer ser um laço.
Uma regra curta: use um stream quando o pipeline se lê como uma frase, filtre isto, mapeie aquilo, colete. Use um laço quando você precisa de break, de uma exceção checada, de várias variáveis que mudam juntas ou de um depurador.
O que lembrar
- Uma lambda implementa uma interface com um único método abstrato.
Function,Predicate,Supplier,Consumer,BiFunctioneUnaryOperatorcobrem a maioria das necessidades. - Uma referência de método, como
String::lengthouPoint::new, é uma lambda que só chama um método. - Uma lambda só pode capturar variáveis locais efetivamente final.
- Um stream não roda nada até uma operação terminal pedir, e então leva um elemento por todas as etapas antes do próximo.
findFirstelimitparam o stream cedo, até numa fonte infinita. - Não dependa de efeitos colaterais em
peekoumap. A operação terminal pode pulá-los, como ocountfez. - Um stream só pode ser usado uma vez.
toList()é não modificável. Dê umTreeMapaogroupingByquando a ordem importar, e dê uma função de merge aotoMapquando as chaves puderem se repetir. - Use
mapToIntpara números, e um laço simples para exceções checadas, saídas antecipadas e estado que muda.
Um stream descreve o trabalho, e a operação terminal decide quanto dele realmente roda.