[Pesquisar este blog]

quinta-feira, 22 de dezembro de 2016

Coleções e Streams outra vez::Parte IV

A nova API Stream foi adicionada ao Java 8 permitir a realização de operações de filtragem, mapeamento e redução sobre coleções. Grande parte de seus métodos são encadeáveis a outros, como na programação funcional, permitindo a criação de pipelines de operações. Assim é possível tratar os elementos de uma coleção sem a necessidade de construção explícita de laços de repetição e, com isso, especificando a realização de operações em massa sobre as coleções.

Segundo a Oracle:
Classes do novo pacote java.util.stream compõem a API Stream para suportar operações no estilo funcional em streams de elementos. A API Stream á integrada a API de Coleções, o que possibilita operações em massa sobre as coleções, tal como transformações de mapeamento/redução sequenciais ou paralelas. (Oracle, 2015, http://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html )


Neste último artigo desta série sobre coleções e streams serão comentados outros exemplos para mostrar outras aplicações desta API. Esta série foi dividida em  quatro artigos:

Nos fragmentos de código que seguem, o comentário inicial indica o nome arquivo-fonte Java onde estão contidos, permitindo que sejam testados com maior facilidade. Todos exemplos estão disponíveis no GitHub (projeto pjandl/stream_again).

Mind Map

Na figura abaixo temos um mapa mental para auxiliar no uso da API Stream. Do lado esquerdo é possível visualizar Stream<T>, que representa um stream de qualquer tipo T. A partir de um stream deste tipo, são possíveis operações terminais, operações terminais de mapeamento e outras operações intermediárias.

Mapa mental para operações terminais e intermediárias de Stream<T>.
As operações terminais estão agrupadas na parte superior da figura e sua execução resulta em tipos primitivos (boolean e long) ou tipos objeto (T, Optional<T>, Object e arrays). Muitas destas operações são de redução, pois processam um stream produzindo um resultado final de tipo específico.

As operações terminais de mapeamento resultam em streams de tipo R ou de outros tipos diferentes de Stream<T>. Com estes streams é possível encadear novas operações, criando pipelines do resultado do mapeamento.

Na parte inferior da figura estão ilustradas operações intermediárias, que a partir do stream de entrada Stream<T> resultam em outro stream do mesmo tipo Stream<T>, permitindo o encadeamento de operações e a construção de pipelines de operações.

Uma aplicação

A Enviromental Protection Agency (EPA) é uma agência norte-americana que divulga anualmente informações sobre os automóveis à venda nos EUA sob o nome de Fuel Eficiency Guide. Para cada modelo temos dados: do fabricante, do motor, da transmissão, do combustível usado e do consumo.

Com a planilha de dados de 2015, foi gerado um arquivo CSV (Comma Separated Values), ilustrado abaixo.

Fragmento de arquivo CSV originado do Fuel Eficiency Guide 2015/EPA.
O programa que segue, ProcessCSVtoList lê o arquivo CSV denominado "2015 FEGuide.xlsx", criando um objeto Car com os dados de cada modelo encontrado no arquivo. A lista criada, na verdade uma instância de ArrayList, é serializada, permitindo que os dados dos modelos sejam persistidos e usados por outras aplicações sem a necessidade de bancos de dados ou da repetição deste processamento.

// ProcessCSVtoList.java
package jandl.streamAgain.app;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

public class ProcessCSVtoList {

public static void main(String[] args) throws Exception {
List<Car> list = new ArrayList<>();
BufferedReader br = new BufferedReader(
  new FileReader("2015 FEGuide.csv"));
String line = br.readLine();
while ((line = br.readLine()) != null) {
StringTokenizer st = new StringTokenizer(line, ";");
if (st.countTokens() == 22) {
String[] data = new String[22];
int i = 0;
while (st.hasMoreTokens()) {
data[i] = st.nextToken();
i++;
}
list.add(dataToCar(data));
}
}
System.out.println("cars: " + list.size());
br.close();
serializeList(list, "2015carlist.ser");
}

public static Car dataToCar(String[] data)
  throws ParseException {
Car car = new Car();
car.modelYear = Integer.parseInt(data[0]);
car.manufacturerName = data[1];
car.manufacturerDivision = data[2];
car.carline = data[3];
NumberFormat nf = NumberFormat.getNumberInstance();
car.engineVolume = nf.parse((data[4])).doubleValue();
car.numberOfCylinders = Integer.parseInt(data[5]);
car.transmission = data[6];
car.fuelEficiencyCity = Integer.parseInt(data[7]);
car.fuelEficiencyHighway = Integer.parseInt(data[8]);
car.fuelEficiencyCombined = Integer.parseInt(data[9]);
car.airAspirationMethod = data[10];
car.transmissionDescription = data[11];
car.numberOfGears = Integer.parseInt(data[12]);
car.driveDescription = data[13];
car.maximumEthanolPercentage = Integer.parseInt(data[14]);
car.maximumBiodieselPercentage = Integer.parseInt(data[15]);
car.fuelUsage = data[16];
car.numberOfIntakeValvesPerCyl = Integer.parseInt(data[17]);
car.numberOfExhaustValvesPerCyl = Integer.parseInt(data[18]);
car.varValveTimingDescription = data[19];
car.fuelMeteringSysDescription = data[20];
car.oilViscosity = data[21];
return car;
}

public static void serializeList(List<Car> list, String fileName)
     throws Exception {

ObjectOutputStream oos = new ObjectOutputStream(
  new FileOutputStream(fileName));
oos.writeObject(list);
oos.close();
}

public static List<Car> deserializeList(String fileName)
  throws Exception {
ObjectInputStream ois = new ObjectInputStream(
  new FileInputStream(fileName));
@SuppressWarnings("unchecked")
List<Car> list = (List<Car>) ois.readObject();
ois.close();
return list;
}
}

A classe  ProcessCSVtoList contém quatro métodos estáticos: main(String[]), dataToCar(String[]), serializeList(List<Car>, String) e serializeList(String). O método main(String[], que constitui o início da aplicação, lê o arquivo CSV e, com o auxílio de um StringTokenizer, separa os dados de cada campo dos automóveis, armazenando-os em um array de String. Este array é transformados em um objeto Car por meio do  uso do método dataToCar(String[]), cuja implementação é pouco elegante, mas adequada.

O método serializeList(List<Car>, String) realiza a serialização do objeto List<Car> recebido, salvando-o num arquivo cujo nome é dado pelo segundo parâmetro. Já o método deserializeList(String) efetua a operação inversa, ou seja, recupera um ArrayList de objetos Car do arquivo denominado pelo parâmetro recebido.

A classe Car é muito simples, constituindo-se apenas de um conjunto de campos para armazenar os dados de cada modelo. O único método disponível é toString(), implementado para facilitar a exibição de tais dados.

// Car.java
package jandl.streamAgain.app;

import java.io.Serializable;

public class Car implements Serializable {
private static final long serialVersionUID = 1L;

int modelYear;
String manufacturerName;
String manufacturerDivision;
String carline;
double engineVolume;
int numberOfCylinders;
String transmission;
int fuelEficiencyCity;
int fuelEficiencyHighway;
int fuelEficiencyCombined;
String airAspirationMethod;
String transmissionDescription;
int numberOfGears;
String driveDescription;
int maximumEthanolPercentage;
int maximumBiodieselPercentage;
String fuelUsage;
int numberOfIntakeValvesPerCyl;
int numberOfExhaustValvesPerCyl;
String varValveTimingDescription;
String fuelMeteringSysDescription;
String oilViscosity;

public String toString() {
return manufacturerName + " "+ carline + " " + engineVolume + " " + transmission;
}
}

Considerando que existem mais de 1000 modelos diferentes na lista que dispomos, para encontrarmos aqueles cujos motores possuem, por exemplo, 10 cilindros, seria necessário código como:

List<Car> lista = ProcessCSVtoList.deserializeList("2015carlist.ser");
int iCylinders = 10;
// código convencional
for(Car c: lista) {
if (c.numberOfCylinders==iCylinders) {
System.out.println(c);
}
}

Com o uso da API Stream e de expressões lambda, tal construção se reduz para:

// StreamFrag401.java
lista.stream()
     .filter(car -> car.numberOfCylinders==cylinders)
     .forEach(c-> System.out.println(c));

Observe que a variável cylinder, definida fora da expressão lambda, pode ser utilizada sem qualquer problema, desde que para leitura. Isto é chamado de captura de variáveis.

Caso seja necessário a contagem dos elementos filtrados pelo pipeline, o uso de variáveis simples não é adequado, pois não podem ser alteradas (as expressões lambdas se comportam como closures e não produzem efeitos colaterais) requerendo um pequeno expediente: o uso de um objeto para realizar a contagem.

O código convencional que segue filtra e conta veículos que podem utilizar um percentual mínimo de etanol indicado pela variável ethanol.

List<Car> lista = ProcessCSVtoList.deserializeList("2015carlist.ser");
int ethanol = 85;

// código convencional
int count = 0;
for(Car c: lista) {
if (c.maximumEthanolPercentage>=ethanol) {
System.out.println(c);
count++
}
}
System.out.println("count = " + count);

Como antes, o uso da API Stream e expressões lambda simplifica bastante o código necessário. Observe também o uso de uma instância da classe Counter, dada a seguir, que implementa um conveniente contador.

Counter counter = new Counter();
lista.stream()
     .filter(car -> car.maximumEthanolPercentage>=ethanol)
     .forEach(c-> { System.out.println(c); counter.inc(); });
System.out.println("count = " + counter.count);

A classe Counter é simples, direta e útil!

package jandl.streamAgain.app;

public class Counter {
public int count;
public Counter() { this(0); }
public Counter(int count) { count = 0; }
public void inc() { count++; }
public void preset(int count) { this.count = count; }
public void reset() { count = 0; }
}

Composição de Predicados

A filtragem de elementos de um stream utiliza-se de predicados, isto é, de objetos que implementam a interface funcional Predicate<T>, convenientemente substituíveis por expressões lambda. Quando o critério de filtragem é simples, tal como mostrado nos fragmentos anteriores, a expressão lambda obtida é igualmente simples.

Mas existem situações onde o critério de filtragem é composto e, portanto, mais complexo. Como por exemplo, filtrar modelos que aceitem mais de 50% etanol, que tenham motores de 6 cilindros e transmissão com 7 ou mais velocidades. Para efetuar tal filtragem é possível escrever:

lista.stream()
.filter(car -> car.maximumEthanolPercentage >= ethanol &&
car.numberOfCylinders==iCylinders &&
car.numberOfGears>=gears)
.forEach(c ->System.out.println(c));

É fácil perceber que predicados compostos tem legibilidade comprometida. Para contornar isso, mantendo o código legível e mais fácil de manter, podemos fazer como no fragmento que segue:

// StreamFrag403.java
List<Car> lista =
    ProcessCSVtoList.deserializeList("2015carlist.ser");


int ethanol = 50;
int iCylinders = 6;
int gears = 7;

Predicate<Car> ethanolPredicate =
    car -> car.maximumEthanolPercentage >= ethanol;

Predicate<Car> cylinderPredicate =
    car -> car.numberOfCylinders==iCylinders;

Predicate<Car> gearPredicate =
    car -> car.numberOfGears>=gears;


lista.stream()     .filter(ethanolPredicate
             .and(cylinderPredicate)
             .and(gearPredicate))
     .forEach(c -> System.out.println(c));

Assim, cada predicado é definido separadamente e composto por meio de operações disponíveis na interface Predicate<T>, melhorando a legibilidade, manutenabilidade e reusabilidade do código.

Execução sequencial ou paralela

A API Stream possibilita a execução sequencial ou paralela de suas operações, o que é determinado pela maneira com que o stream é criado a partir de sua fonte.

Enquanto o método default stream() retorna um stream sequencial de uma coleção, seu correspondente parallelStream() retorna um stream cuja execução acontecerá em paralelo. Num stream sequencial as operações acontecerão na ordem em que os elementos do stream são encontrados, ou seja, sequencialmente, sendo executadas em uma thread (e portanto ocupando apenas um processador ou núcleo do sistema). Já num stream paralelo, várias operações serão executadas concorrentemente em múltiplas threads, utilizando os elementos do stream em ordem diversa da sequencial, sendo possivelmente executados por vários processadores (ou núcleos) do sistema. Isto permite explorar melhor os recursos do sistema.

No fragmento que segue, temos uma grande lista com 2 milhões de elementos inteiros gerados aleatoriamente.

// StreamFrag404.java
int MAXIMO = 2000000;
List<String> valores = new ArrayList<>(MAXIMO);
for (int i = 0; i < MAXIMO; i++) {
UUID uuid = UUID.randomUUID();
valores.add(uuid.toString());
}

Os elementos desta lista são ordenados com uso de uma stream sequencial e de uma stream paralela, exibindo diferentes tempos de processamento. Observe que os trechos de código são idênticos, exceto pela forma de obtenção do stream.

// StreamFrag404.java
long tIni, tFim, num, tDecorrido;

// ordenação sequencial
tIni = System.nanoTime();
num = valores.stream().sorted().count();
System.out.println(num);
tFim = System.nanoTime();
tDecorrido = TimeUnit.NANOSECONDS.toMillis(tFim - tIni);
System.out.printf("Ordenacao sequencial: %d ms\n", tDecorrido);
// ordenação paralela
tIni = System.nanoTime();
num = valores.parallelStream().sorted().count();
System.out.println(num);
tFim = System.nanoTime();
tDecorrido = TimeUnit.NANOSECONDS.toMillis(tFim - tIni);
System.out.printf("Ordenacao paralela  : %d ms\n", tDecorrido);

Como esperado, o tempo de processamento com uso de um stream paralelo é menor do que com o uso do stream sequencial.

No próximo fragmento, a lista de modelos de veículos é filtrada para exibição daqueles com tranmissão com 9 velocidades.

// StreamFrag405.java
List<Car> lista = ProcessCSVtoList.deserializeList("2015carlist.ser");
int gears = 9;

System.out.println("==========");
Counter counter = new Counter();
lista.stream()
.filter(car -> car.numberOfGears==gears)
.forEach(s-> { System.out.println(s); counter.inc(); });
System.out.println("count = " + counter.count);
System.out.println("==========");
counter.reset();
lista.parallelStream()
.filter(car -> car.numberOfGears==gears)
.forEach(s-> { System.out.println(s); counter.inc(); });
System.out.println("count = " + counter.count);
System.out.println("==========");

Enquanto que com uso do stream sequencial os modelos são exibidos na mesma ordem em figuram na lista completa, a utilização do stream paralelo produz um resultado com ordenação diversa, pois threads diferentes processaram porções distintas da lista. Mas o mesmo número de registros é exibido pelas duas estratégias.

Conclusões

A API Stream adiciona um conjunto extremamente flexível e poderoso de características às Coleções, facilitando muito a produção de código mais simples, de melhor legibilidade, de manutenção mais fácil e, possivelmente de reuso mais fácil.
No entanto, é bastante provável que não existam ganhos de desempenho por parte da aplicação, pois tais faciilidades oneram os tempos de execução. Ainda assim não devem ser desprezadas, pois em geral, a perda de perfomance não será perceptível para aplicações comuns, tornando substancial o ganho de produtividade do programador. Já aplicações massivas e de alto desempenho terão que utilizar mais cautelosamente esta API, balanceando os ganhos possíveis decorrentes do desenvolvimento simplificado com os requisitos de performance exigidos pela aplicação.
Conhecer e explorar esta API é, desta maneira, mandatório, para que possa ser empregada com sucesso conforme as demandas de cada projeto!

Este artigo faz parte de uma pequena série Coleções e Streams outra vez:

Para saber mais


domingo, 11 de dezembro de 2016

Aprender sempre::educação continuada e pós-graduação

É unânime! Qualquer gestor de recursos humanos; executivo de empresa de qualquer porte ou segmento; profissional de coaching ou mentoring; especialista em desenvolvimento de carreira; e professor vai afirmar categoricamente: precisamos aprender sempre e ao longo de toda vida.


Por que?

Objetivamente, o trabalho faz parte da vida da grande maioria das pessoas, que, por muitas e diversas razões trabalham e buscam o trabalho como maneira de prover o sustento próprio e de suas famílias; mas também como um caminho para que alcancem seus sonhos, nem sempre materiais.

O exercício de qualquer profissão requer dedicação, esforço e capricho, podendo se apropriar de novas tecnologias, materiais e processos para que aconteça de maneira mais eficiente, lucrativa, benéfica e, por que não, mais proveitosa e prazerosa para todos os envolvidos.

Assim, independente da profissão e do ramo de atuação, a educação e o aperfeiçoamento profissional tem que ser vistos como necessidades constantes, daí o termo educação continuada. Segundo Angélica Alvim, diretora do Mackenzie, "É importante que os profissionais façam especialização e aperfeiçoamento para que se mantenham no mercado". Esta afirmação coincide com o depoimento de muitos profissionais de RH que colocam que, para as empresas de hoje, resolver problemas conhecidos já não suficiente, mas a capacidade e a iniciativa de identificar novos problemas, propor e desenvolver soluções para enfrentá-los.

A grande integração econômica proporcionada pela globalização trouxe enormes facilidades para as empresas em termos de acesso a novos mercados, tecnologias e fornecedores. Mas veio acompanhada de desafios. O principal é a manutenção da competitividade numa situação de multiplicidade de concorrentes, consumidores mais exigentes, transformações sociais constantes e crises, muitas crises.

Segundo Leonardo Trevisan, professor da PUC, o vocabulário empresarial inclui expressões como realidade virtual, internet das coisas, big data, entre outras coisas. Ou seja, as novas tecnologias estão transformando a indústria e as corporações, tornando cada vez necessários profissionais qualificados e especializados.

Este cenário complexo cria uma situação bem simples de ser entendida: sem conhecimento, a vida no trabalho será cada vez mais difícil, até mesmo porque as oportunidades, que sempre existirão, serão encontradas por aqueles que estiverem preparados para enfrentá-las.

Então escolher e concluir um bom curso de graduação é um investimento necessário para se colocar favoravelmente em relação as demandas das empresas. E para que sua carreira possa seguir num crescente, a pós-graduação, na forma de cursos de especialização, mestrado e doutorado, é essencial para galgar posições de maiores responsabilidade e, claro, maiores ganhos.

Ao mesmo tempo, exatamente pelo fato do trabalho ocupar uma parte substancial de nossas vidas, é muito importante aprender com nossas experiências, sejam acertos ou erros, para nos fortalecermos para continuar buscando novas metas.

Aprendizado não é a palavra-chave em tudo isso? Finalizo com a frase de Ylana Muller, professora do IBMEC:
 "Trabalhe para viver. A energia do trabalho deve ser para transformar e realizar".


Para Saber Mais

domingo, 27 de novembro de 2016

Carreira em TI

Estudo recente realizado pela IDC, empresa de inteligência de mercado, estima que o déficit de profissionais de TI no Brasil será menor em 2019 do que em 2015, quando sobraram quase 200 mil vagas na área, que não puderam ser preenchidas devido a falta de profissionais qualificados.

Esta notícia pode ser interpretada de maneiras diferentes, mas complementares:
  • a demanda por mão-de-obra com formação tecnológica é, hoje, bastante alta, e permanecerá elevada nos próximos anos;
  • os esforços do setor privado (substanciais) e do governo (em queda), não conseguiram suprir a demanda das empresas de tecnologia, que vivem, conforme muitos gerentes de RH, uma guerra por talentos;
  • a lacuna de profissionais preparados faz com que os salários oferecidos pelo mercado sejam maiores.
Existe hoje uma necessidade enorme de novos aplicativos móveis e no projeto de sistemas de alta usabilidade, além de sistemas de apoio server-side, com uso intensivo de arquiteturas orientadas à serviço, distribuídas e em nuvem. Alguns números:
  • US$59.9 bilhões de investimento na área de TI em 2015 no Brasil (líder disparado na América Latina).
  • R$19.6 bilhões movimentados pelo e-Commerce no Brasil, apenas no 1o trimestre de 2016; 
  • 170% de crescimento de e-Commerce via smartphones no Brasil;
  • 50% das empresas brasileiras pretendem manter ou aumentar seu projetos em TI;
  • 63% do mercado brasileiro de TI se concentra na região sudeste;
  • Previsão de aumento nos gastos com nuvem em 2017.
Daí serem muito procurados os analistas de sistemas, os cientistas da computação, os engenheiros de computação e outros profissionais com competências para o desenvolvimento de software. A demanda é tão grande que surgiram muitas iniciativas para promover o ensino de programação entre crianças e adolescentes, como forma de incentivar e direcionar um número maior de pessoas para área.

Além dos conhecimentos tradicionais de técnicas de programação, engenharia de software e linguagens como Java e C#, existem oportunidades para aqueles que dominem o Python e o PHP. Já tendeências mais recentes, como aplicativos para cloud computing, big data e web analytics são diferenciais enormes, pois existe um pequeno número de pessoas que dominam estes assuntos. Mas não podem ser esquecidos os conteúdos de outras áreas, bem como (um mínimo) de proficiência em inglês e, quem sabe, outro idioma. 

E não é apenas no segmento de Tecnologia da Informação. A área de  Telecomunicações passa pelas mesmas dificuldades, principalmente no nível técnico, envolvendo a integração de sistemas, incluindo sua segurança. Há quem diga que serão 400 mil vagas em aberto nas áreas de TI & TELECOM até 2020.
Embora o número de cursos ofertados nas instituições de ensino superior tenha aumentado muito nos últimos 10 anos, a procura tem, surpreendentemente, diminuído. As razões para isso são muitas: os problemas enfrentados pelo ensino médio no Brasil; as exigências dos currículos que parecem desarticuladas com as necessidades do mercado; e o desinteresse pelas carreiras em TI, apesar das oportunidades existentes. Isto acaba mantendo ainda mais aquecido este mercado.

E quem já é graduado, ou está prestes a concluir sua graduação, não pode nem pensar em parar de estudar. O caminho do sucesso requer fazer uma pós-graduação ou, no mínimo, buscar certificações profissionais.

Em resumo, quem estiver bem preparado, com conhecimento técnico substancial, e também possuir perfil proativo, vontade de trabalhar e de crescer, não terá dificuldades em encontrar seu lugar!


domingo, 20 de novembro de 2016

Coleções e Streams outra vez::Parte III

Neste terceiro artigo da série sobre Coleções e Streams, o assunto são as operações terminais. A API Streams, permite a manipulação do conteúdo das coleções de maneira muito flexível e elegante, possibilitando a execução de operações em massa sobre seus elementos, aproveitando a simplicidade das construções funcionais.

Revisão Rápida

A ideia de stream é representar uma sequência de elementos sobre os quais uma ou mais operações podem ser executadas. Um stream associado à uma coleção fornece uma sequência de seus elementos, onde cada uma figura uma única vez. Mas, diferentemente das coleções, cujo conteúdo é estável e pode ser reutilizado; ou dos iterators, que podem permitir a navegação entre os elementos das coleçõesos streams são consumíveis, ou seja, só podem ser utilizados uma única vez por uma operação intermediária ou terminal. Sua reutilização provoca a exceção IllegalStateException.


A proposta da API de Streams é organizar um stream pipeline, como o ilustrado na figura anterior, iniciado  com uma fonte (ou origem) dos dados; uma sequência de zero ou mais operações intermediárias; e uma operação terminal (ou final).


O fragmento de código que segue mostra um stream pipeline com cinco estágios que, resumidamente, filtra os nomes da lista convertidos para maiúsculas com quatro caracteres que são terminados por "RO", contando a quantidade de ocorrências, mas mantendo a coleção lista inalterada:

// StreamFragm301.java
long c = lista.stream()
.map(s -> s.toUpperCase())
.filter(s -> s.length()==4)
.filter(s -> s.endsWith("RO"))
.count();

A coleção coleção de objetos String lista é acionada pelo método stream(), operação fonte que constitui o primeiro estágio onde é criado um stream que inicia o pipeline. No segundo estágio cada elemento é mapeado com map(Function<T,R>) para sua versão em maiúsculas. O dois estágios seguintes usam método filter(Predicate), uma operação intermediária (que toma um stream como entrada, resultando em outro), cuja expressões lambda fornecidas determinam o predicado da filtragem, isto é, o critério de aceitação dos elementos do stream de entrada que serão enviados para o stream de saída. No último estágio temos a operação terminal count() (que toma um stream como entrada e produz um tipo não encadeável) cujo resultado long não permite a continuação do encadeamento do stream pipeline.
Nos fragmentos de código que seguem, o comentário inicial indica o nome arquivo-fonte Java onde estão contidos, permitindo que sejam testados com maior facilidade. Todos exemplos estão disponíveis no GitHub (projeto pjandl/stream_again).

Neste artigo serão comentadas as operações de tipo terminal, lembrando que este material foi dividido em quatro artigos:


Operações Terminais

As operações terminais processam o conteúdo de um stream produzindo um objeto/valor único ou aplicando uma operação sobre todos os seus elementos. Em ambos os casos, o resultado não é um novo stream, o que impede a continuação do stream pipeline, encerrando-o (daí a terminologia de operação terminal).

As operações que produzem um resultado único, sob alguns aspectos são operações de redução dos elementos do stream no sentido de obter um valor especial que representa um aspecto específico do conjunto de elementos, como sua contagem, seu máximo, seu mínimo, uma ocorrência específica ou o resultado de sua redução.

Um outro tipo de operação terminal é aquela que aplica uma mesma operação a cada um dos elementos do stream, ou seja, efetuando uma operação em massa (ou bulk operation).

Também existem operações terminais que convertem um stream em um array, finalizando o pipeline.

Redução

A redução é uma operação terminal que processa, avalia ou agrega os elementos do stream resultando num valor ou objeto final que representa um aspecto específico do conjunto de elementos contidos no stream.

Redução
booleanallMatch (Predicate<T> p)
Retorna um resultado lógico que indica se todos os elementos do stream atendem o predicado p dado.
booleananyMatch (Predicate<T> p)
Retorna um resultado lógico que indica se ao menos um dos elementos do stream atendem o predicado p dado.
booleannoneMatch (Predicate<T> p)
Retorna um resultado lógico que indica se nenhum os elementos do stream atendem o predicado p dado.
longcount ( )
Conta o número de elementos contidos neste stream.
Optional<T>findAny ()
Retorna um objeto Optional<T> contendo um elemento qualquer do stream; ou um objeto Optional<T> vazio quando o stream não contém elementos.
Optional<T>findFirst ()
Retorna um objeto Optional<T> contendo o primeiro elemento do stream; ou um objeto Optional<T> vazio quando o stream não contém elementos.
Optional<T>max (Comparator<T> c)
Retorna um objeto Optional<T> contendo o maior elemento do stream segundo o comparador c fornecido; ou um Optional<T> vazio se o stream não contém elementos.
Optional<T>min (Comparator<T> c)
Retorna um objeto Optional<T> contendo o menor elemento do stream segundo o comparador c fornecido; ou um Optional<T> vazio se o stream não contém elementos.
Optional<T>reduce (BinaryOperator<T> acum)
Realiza a redução do stream por meio da função acumuladora-associativa acum, retornando um objeto Optional<T> com o resultado.
Treduce (T iden, BinaryOperator<T> acum)
Realiza a redução do stream, tomando iden como valor-identidade e acum como uma função acumuladora-associativa, retornando um objeto Optional<T> com o resultado.


No fragmento que segue, uma coleção de String, definida na variável lista, tem seu stream associado obtido e depois avaliado com os métodos allMatch(Predicate<T>)anyMatch(Predicate<T>)noneMatch(Predicate<T>), os quais respectivamente verificarão para um dado predicado se todos os elementos o atendem, se algum elemento o atende e se nenhum elemento o atende.
// StreamFrag301.java
List<String> lista = Arrays.asList("zEro", "Um", "DoIs", "TRes", "QuaTRo", "CiNcO", "SEIS", "setE", "oITo", "noVe", "DeZ");

// verifica se todas as strings são iniciadas com 'Q'
System.out.println("lista[ todos|inicial 'Q']? " +
lista.stream().
allMatch(s -> s.toUpperCase().startsWith("Q")));

// verifica se alguma string tem comprimento 3
System.out.println("lista[ algum|compr.==3]? " +
lista.stream().
anyMatch(s -> s.length()==3));

// verifica se nenhuma string é iniciada com 'x'
System.out.println("lista[nenhum|final 'x']? " +
lista.stream().
noneMatch(s -> s.toLowerCase().endsWith("x")));

O método findAny() retorna um elemento do stream (tipicamente o primeiro, mas sem garantia), enquanto findFirst() retorna o primeiro elemento do stream. Considerando as situações em que o stream está vazio, ao invés de retornar um valor null, que potencialmente poderia provocar o lançamento de NullPointerException, estes métodos sempre retornam um objeto de tipo Optional<T>. Um Optional<T> encapsula um outro objeto de qualquer tipo, ou mesmo um resultado null, simplificando o tratamento do resultado de operações que podem ou não retornar um valor.

O trecho que código abaixo mostra o uso de uma lista de nomes construída na classe Colecoes, onde se obtém um valor qualquer com findAny() e o primeiro valor com findFirst(). Observe as diferentes formas de uso do objeto Optional<String> retornado.
// StreamFragm302.java
List<String> lista = Colecoes.outrosNomes;

Optional<?> opt1 = null;
opt1 = lista.stream()
.findAny(); // obtém qualquer elemento
System.out.println(opt1);
lista.stream()
.findFirst() // obtém primeiro elemento
.ifPresent(opt2 -> System.out.println(opt2));

lista.stream()
.filter(s -> s.equals("Peter Jandl Junior"))
.findFirst() // obtém primeiro elemento
.ifPresent(opt2 -> System.out.println(opt2));

Os métodos max(Comparator<T>) e min(Comparator<T>) permitem obter o maior ou o menor elemento de um stream, retornando o resultado como um objeto Optional<T>, pois o stream pode estar vazio. O comparador fornecido deve atender a interface Comparator<T>. Segue um outro exemplo:
// StreamFrag303.java
List<String> lista = Colecoes.nomes;
Optional<?> opt = null;
opt = lista.stream()
.map(s -> s) // adição de operação intermediária dummy
.min(Comparator.comparing(s -> s.length()));
System.out.println(opt);

Colecoes.cores.stream()
.map(c -> c.getRed()) // mapeia cor RGB em seu valor Red
.max(Comparator.comparing(s -> s)) // comparador elementar
.ifPresent(v -> System.out.println(v));

Finalmente um exemplo do método de redução utilizado para obter a soma de uma série de valores (o componente azul) obtido a partir de uma coleção de objetos (uma lista de cores):
// StreamFrag304.java
long somaBlue = Colecoes.cores.stream()
.map(c -> c.getBlue()) // mapeia cor RGB em seu valor Blue
.reduce(0, (x,y) -> x + y); // redução (soma dos elementos)
System.out.println(somaBlue);

O uso de arrays ou listas de valores é ainda mais simples, pois não requer o mapeamento intermediário:
// StreamFrag304.java
Integer array[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int soma = 0; 

soma = Stream.of(array) // stream obtida de array
.reduce(0, (x,y) -> x + y); // redução (soma dos elementos)
System.out.println(soma);

List<Integer> lista = Arrays.asList(array); // lista obtida de array
soma = lista.stream() // stream obtida de lista
.reduce(0, Integer::sum); // redução (soma dos elementos)
System.out.println(soma);

Operação em Massa

São operações terminais onde uma operação específica é aplicada individualmente a todos os elementos presentes no stream, sem retorno de valor.

Operação em Massa
voidforEach (Consumer<T> c)
Aplica a função consumidora a cada elemento presente no stream, sem observar a ordem em que se encontram seus elementos (não-determinístico).
voidforEachOrdered (Consumer<T> c)
Aplica a função consumidora a cada elemento presente no stream, observando a ordem em que se encontram seus elementos (determinístico).

O método forEach() fará a aplicação a função consumidora dada em observar a ordem em que os elementos se encontram no stream. Como consequência não se pode prever como os elementos serão processados em conjunto (situação não-determinística), embora seja garantidos que a função Consumer<T> dada será aplicada a todos. O método forEachOrdered() observará a sequência de elementos presente no stream para aplicação da função, proporcionando um efeito determinístico, mas que não aproveitará da possível paralelização. Segue um fragmento ilustrando o :
// StreamFrag305.java
List<String> lista = Colecoes.nomes;
lista.stream().parallel() // stream paralela
.forEach(s->System.out.println(s));

System.out.println();

Colecoes.nomes.stream() // stream sequencial
.forEachOrdered(System.out::println);

Conversão

Permite transformar um stream em um array de elementos.

Conversão
Object[]toArray ()
Retorna um array contendo os elementos deste stream.

Um exemplo de uso da operação terminal de conversão é:
Object[] o = Colecoes.nomes.stream()
.filter(s -> s.startsWith("K"))
.toArray();

As operações terminais são um importante recursos no tratamento dos streams, pois permitem a realização de operações de redução, em massa ou sua conversão. No próximo post veremos aplicações interessantes da API Stream!


Este artigo faz parte de uma pequena série Coleções e Streams outra vez:

Para saber mais