Capítulo 3 Strings, Comparadores e Filtros
Alguns métodos introduzidos pelo JDK são muito úteis para escrever código de estilo funcional. Já estamos muito familiarizados com algumas classes e interfaces da biblioteca JDK, como String. Para nos livrarmos do estilo antigo a que estamos acostumados, precisamos procurar ativamente oportunidades para usar esses novos métodos. Da mesma forma, quando precisamos usar uma classe interna anônima com apenas um método, podemos agora substituí-la por uma expressão lambda, sem ter que escrevê-la tão complicada quanto antes.
Neste capítulo, usaremos expressões lambda e referências de método para percorrer strings, implementar a interface Comparator, visualizar arquivos no diretório e monitorar alterações em arquivos e diretórios. Alguns dos métodos apresentados no capítulo anterior continuarão a aparecer aqui para nos ajudar a completar melhor essas tarefas. As novas técnicas que você aprender ajudarão a transformar códigos longos e tediosos em algo conciso, rápido de implementar e fácil de manter.
Iterar sobre string
O método chars() é um novo método na classe String, que faz parte da interface CharSequence. É uma ferramenta muito útil se você deseja percorrer rapidamente a sequência de caracteres de String. Com este iterador interno, podemos operar convenientemente em cada caractere da string. Tente usá-lo para processar uma string primeiro. Aqui estão algumas maneiras de usar referências de método.
Copie o código do código da seguinte forma:
string final str = "w00t";
str.chars()
.forEach(ch -> System.out.println(ch));
O método chars() retorna um objeto Stream, que podemos usar seu iterador interno forEach() para percorrer. No iterador, podemos acessar diretamente os caracteres da string. Abaixo está a saída do loop pela string e da impressão de cada caractere.
Copie o código do código da seguinte forma:
119
48
48
116
Este não é o resultado que queremos. Esperamos ver letras, mas a saída são números. Isso ocorre porque o método chars() retorna um Stream inteiro em vez de um tipo de caractere. Vamos primeiro entender esta API e depois otimizar os resultados de saída.
No código anterior, criamos uma expressão lambda como parâmetro de entrada do método forEach. Ele simplesmente passa os parâmetros para um método println(). Como esta operação é muito comum, podemos usar o compilador Java para simplificar este código. Assim como no uso de referências de método na página 25, substitua-a por uma referência de método e deixe o compilador fazer o roteamento dos parâmetros para nós.
Vimos como criar uma referência de método para um método de instância. Por exemplo, o método name.toUpperCase(), a referência do método é String::toUpperCase. No exemplo a seguir, estamos chamando um método de instância que faz referência estaticamente a System.out. O lado esquerdo dos dois dois pontos referenciados pelo método pode ser um nome de classe ou uma expressão. Com essa flexibilidade, podemos facilmente criar uma referência ao método println(), como abaixo.
Copie o código do código da seguinte forma:
str.chars()
.forEach(System.out::println);
Como você pode ver, o compilador Java pode concluir o roteamento de parâmetros de maneira muito inteligente. Lembre-se de que expressões lambda e referências de método só podem aparecer onde uma interface funcional é recebida, e o compilador Java irá gerar um método correspondente lá (Anotação: O compilador irá gerar uma implementação da interface funcional, que possui apenas um método.). O método que usamos antes refere-se a String::toUpperCase, e os parâmetros passados para o método gerado eventualmente se tornarão o objeto alvo da chamada do método, como este: parâmetro.toUpperCase(). Isso ocorre porque a referência do método é baseada no nome da classe (String). A referência do método no exemplo acima é baseada em uma expressão, que é uma instância de PrintStream e é referenciada por meio de System.out. Como o objeto da chamada do método já existe, o compilador Java decide usar os parâmetros do método gerado como parâmetros deste método println: System.out.println(name).
(Anotação: Na verdade, existem principalmente dois cenários. Uma referência de método também é passada. Um é o objeto percorrido, é claro, o objeto alvo da chamada de método, como name.toUpperCase, e o outro é usado como parâmetro de a chamada do método, como System. out.println(name).)
O código fica muito mais simples depois de usar referências de método, mas precisamos ter um entendimento mais profundo de como ele funciona. Depois que nos familiarizarmos com as referências de métodos, poderemos descobrir o roteamento de parâmetros por conta própria.
Embora o código neste exemplo seja bastante conciso, o resultado ainda é insatisfatório. Esperávamos ver letras, mas em vez disso apareceram números. Para resolver este problema, vamos escrever um método para gerar int como letras.
Copie o código do código da seguinte forma:
private static void printChar(int aChar) {
System.out.println((char)(aChar));
}
O uso de referências de método pode otimizar facilmente os resultados de saída.
Copie o código do código da seguinte forma:
str.chars()
.forEach(IterateString::printChar);
Agora, embora o resultado retornado por chars() seja int, não importa. Quando precisarmos imprimir, iremos convertê-lo em caracteres. Desta vez, a saída é finalmente letras.
Copie o código do código da seguinte forma:
c
0
0
t
Se quisermos processar caracteres em vez de inteiros desde o início, podemos converter diretamente os inteiros em caracteres após chamar chars:
Copie o código do código da seguinte forma:
str.chars()
.mapToObj(ch -> Character.valueOf((char)ch))
.forEach(System.out::println);
Aqui usamos um iterador interno do Stream retornado por chars(). Claro, mais do que este método pode ser usado. Depois de obter o objeto Stream, podemos usar seus métodos, como map(), filter(), reduzir(), etc. Podemos usar o método filter() para filtrar caracteres que são números:
Copie o código do código da seguinte forma:
str.chars()
.filter(ch -> Character.isDigit(ch))
.forEach(ch -> printChar(ch));
Ao produzir desta forma, só podemos ver números:
Copie o código do código da seguinte forma:
0
0
Da mesma forma, além de passar expressões lambda para os métodos filter() e forEach(), também podemos usar referências de método.
Copie o código do código da seguinte forma:
str.chars()
.filter(Caracter::isDigit)
.forEach(IterateString::printChar);
A referência do método aqui elimina o roteamento redundante de parâmetros. Neste exemplo, também vemos um uso diferente dos dois métodos anteriores. Na primeira vez que referenciamos um método de instância, na segunda vez foi um método de referência estática (System.out). Desta vez é uma referência a um método estático - as referências de método têm pago silenciosamente.
Todas as referências a métodos de instância e métodos estáticos têm a mesma aparência: por exemplo, String::toUpperCase e Character::isDigit. O compilador determina se o método é um método de instância ou um método estático para determinar como rotear parâmetros. Se for um método de instância, ele usará os parâmetros de entrada do método gerado como o objeto alvo da chamada do método, como o parâmetro toUpperCase(); foi especificado, como System::out.println ()). Além disso, se for um método estático, os parâmetros de entrada do método gerado serão utilizados como parâmetros do método referenciado, como Character.isDigit(parâmetro). O Apêndice 2 na página 152 contém instruções detalhadas sobre como usar referências de métodos e sua sintaxe.
Embora as referências de métodos sejam convenientes de usar, ainda há um problema - ambiguidade causada por conflitos de nomenclatura de métodos. Se o método correspondente for um método de instância e um método estático, o compilador reportará um erro devido à ambiguidade do método. Por exemplo, se escrevermos Double::toString assim, na verdade queremos converter um tipo double em uma string, mas o compilador não sabe se deve chamar o método de instância de public String toString() ou chamar public static String toString. (double), porque ambos os métodos são da classe Double. Se você se deparar com tal situação, não desanime, basta usar expressões lambda para completá-la.
Quando estivermos confortáveis com a programação funcional, poderemos alternar entre expressões lambda e referências de método à vontade.
Nesta seção, usamos um novo método em Java 8 para iterar strings. Vamos dar uma olhada nas melhorias na interface do Comparador.