Conteúdo


Java.next

Java 8 como Java.next

Avaliando Java 8 como uma substituição de Java adequada

Comments

Conteúdos da série:

Esse conteúdo é a parte # de # na série: Java.next

Fique ligado em conteúdos adicionais dessa série.

Esse conteúdo é parte da série:Java.next

Fique ligado em conteúdos adicionais dessa série.

A orientação original desta série era comparar e contrastar três linguagens da JVM mais novas para ajudar a avaliar qual é a provável sucessora da linguagem Java. Mas, nesse meio tempo, a linguagem Java passou por sua mudança mais significativa desde a adição de genéricos. Agora, Java em si apresenta muitas das características desejáveis de Groovy, Scala e Clojure. Nesta parte do artigo, considero Java 8 como uma linguagem Java.next e incluo exemplos que mostram a eficiência com que os paradigmas da programação da linguagem foram ampliados.

Por fim, funções de ordem mais alta

Funções de ordem mais alta pegam outras funções como argumentos ou retornam outras funções como resultado. Java— talvez o último baluarte entre as linguagens populares— finalmente tem funções de ordem superior, na forma de blocos lambda. Em vez de apenas incluir funções de ordem superior à linguagem, os engenheiros de Java 8 tiveram decisões inteligentes e habilitaram interfaces mais antigas para aproveitar os recursos funcionais.

Na parte do artigo "Estilos de codificação funcional", demonstrei implementações nas linguagens Java.next para um problema que tradicionalmente teria sido resolvido de modo imperativo. O problema coloca uma lista de nomes de entrada com o requisito de remover entradas de caractere único e retornar uma lista delimitada por vírgula com cada nome com a primeira letra maiúscula. A solução Java imperativa aparece na Listagem 1.

Lista 1. Transformação de nome imperativa
public String cleanNames(List<String> listOfNames) {
    StringBuilder result = new StringBuilder();
    for(int i = 0; i < listOfNames.size(); i++) {
        if (listOfNames.get(i).length() > 1) {
            result.append(capitalizeString(listOfNames.get(i))).append(",");
        }
    }
    return result.substring(0, result.length() - 1).toString();
}

public String capitalizeString(String s) {
    return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
}

Iteração é a norma em versões anteriores de Java, mas, com o Java 8, a tarefa é realizada de maneira mais elegante com fluxos— uma abstração que atua como um cruzamento entre uma coleção e um canal UNIX® . A Listagem 2 usa fluxos.

Lista 2. Transformação de nome em Java 8
public String cleanNames(List<String> names) {
    return names
            .stream()
            .filter(name -> name.length() > 1)
            .map(name -> capitalize(name))
            .collect(Collectors.joining(","));
}

private String capitalize(String e) {
    return e.substring(0, 1).toUpperCase() + e.substring(1, e.length());
}

A versão iterativa na Listagem 1 deve combinar tarefas de filtragem, transformação e concatenação dentro de um único loop for ,— pois é ineficiente fazer um loop pela coleção para cada tarefa. Com fluxos em Java 8, é possível encadear e compor funções juntas até chamar uma função que gere uma saída (chamada de operação terminal), como collect() ou forEach().

O método filter() na Listagem 2 é o mesmo método de filtro comum em linguagens funcionais (consulte "Java.next: Overcome synonym suffering"). O método filter() aceita uma função de ordem superior que retorna um valor booleano, usado como o critério de filtragem: true indica inclusão na coleção filtrada, e false, indica ausência na coleção.

O método filter() aceita um tipo de Predicate<T>, que é um método que retorna um valor booleano. É possível criar instâncias de predicado explicitamente, se desejar, como mostra a Listagem 3.

Lista 3. Criando um predicado manualmente
Predicate<String> p = (name) -> name.startsWith("Mr");
List<String> l = List.of("Mr Rogers", "Ms Robinson", "Mr Ed");
l.stream().filter(p).forEach(i -> System.out.println(i));

Na Listagem 3, eu crio um predicado atribuindo o bloco lambda de filtragem a ele. Quando chamo o método filter() na terceira linha, passo o predicado como o parâmetro esperado.

Na Listagem 2, o método map() funciona como o esperado, aplicando o método capitalize() a cada elemento na coleção. Por fim, chamo o método collect() , que é uma operação terminal — um método que gera valores a partir do fluxo. O método collect() realiza a operação reduce familiar: combinar elementos para produzir um resultado (tipicamente) menor, às vezes um único valor (por exemplo, uma operação de sum ). Java 8 tem um método reduce() , mas collect() é preferido neste caso, pois funciona com eficiência com contêineres mutáveis, como StringBuilder.

Adicionando o reconhecimento de construções funcionais, como map e reduce , para classes e coleções existentes, Java enfrenta um problema de atualizações de coleção eficientes. Por exemplo, uma operação reduce é muito menos útil se não for possível utilizá-la em coleções Java típicas, como ArrayList. Muitas das bibliotecas de coleção em Scala e Clojure são imutáveis, o que permite ao tempo de execução gerar operações eficientes. Java 8 não pode forçar os desenvolvedores a alterar coleções, e muitas das classes de coleção existentes em Java são mutáveis. Portanto, Java 8 inclui métodos que realizam mutable reduction para coleções como ArrayList e StringBuilder que atualizam os elementos existentes, em vez de substituir o resultado a cada vez. Embora reduce() funcione na Listagem 2, , collect() funciona com mais eficiência para a coleção retornada nessa instância.

Uma das vantagens das linguagens funcionais abordadas na parte do artigo "Contrasting concurrency" é a facilidade com que se pode paralelizar coleções, adicionando um único modificador com frequência. Java 8 oferece a mesma vantagem, como mostra a Listagem 4.

Lista 4. Processamento paralelo em Java 8
public String cleanNamesP(List<String> names) {
    return names 
            .parallelStream() 
            .filter(n -> n.length() > 1) 
            .map(e -> capitalize(e)) 
            .collect(Collectors.joining(","));
}

Como em Scala, posso fazer operações de fluxo funcionarem em paralelo na Listagem 4 adicionando um modificador parallelStream() . A programação funcional adia os detalhes da implementação para o tempo de execução, permitindo o trabalho com um nível de abstração maior. A facilidade com que o encadeamento pode ser aplicado a coleções exemplifica essa vantagem.

As diferenças entre tipos de redutor em Java 8 ilustram a dificuldade de adicionar um paradigma profundo, como programação funcional, às construções da linguagem existente. A equipe de Java 8 fez um ótimo trabalho em adicionar construções funcionais de uma maneira em grande parte contínua. Um bom exemplo dessa integração é a adição de interfaces funcionais.

Interfaces funcionais

Um idioma comum de Java é uma interface com um método único — chamado de interface SAM (single abstract method)— , tais como Runnable ou Callable. Em muitos casos, SAMs são usados principalmente como um mecanismo de transporte para código portátil. Código portátil é mais bem implementado em Java 8 com um bloco lambda. Um mecanismo inteligente chamado interfaces funcionais permite que lambdas e SAMs interajam de maneiras úteis. Uma interface funcional inclui um único método abstrato (e pode incluir vários métodos padrão). Interfaces funcionais aumentam as SAM existentes para habilitar a substituição de classes internas anônimas tradicionais com um bloco lambda. Por exemplo, a interface Runnable agora pode ser sinalizada com a anotação @FunctionalInterface . Essa anotação opcional diz ao compilador para verificar se Runnable é uma interface (e não uma classe ou enum) e se o tipo anotado atende aos requisitos de uma interface funcional.

Como um exemplo da capacidade de substituição de blocos lambda, é possível criar um novo encadeamento em Java 8 enviando um bloco lambda no lugar de uma classe interna anônima Runnable :

new Thread(() -> System.out.println("Inside thread")).start();

Interfaces funcionais podem se integrar continuamente com blocos lambda em vários locais úteis. Interfaces funcionais são uma inovação notável, pois funcionam bem com idiomas Java estabelecidos.

Métodos padrão

Com Java 8, também é possível declarar métodos padrão em interfaces. Um método padrão é um método público não abstrato e não estático (com um corpo) declarado em um tipo de interface e marcado com a palavra-chave default . Cada método padrão é adicionado automaticamente a classes que implementam a interface — uma maneira conveniente de decorar classes com funcionalidade padrão. Por exemplo, a interface Comparator agora inclui mais de uma dezena de métodos padrão. Se eu criar um comparador usando um bloco lambda, posso criar trivialmente o comparador reverso, como mostra a Listagem 5.

Lista 5. Métodos padrão do Comparator
List<Integer> n = List.of(1, 4, 45, 12, 5, 6, 9, 101);
Comparator<Integer> c1 = (x, y) -> x - y;
Comparator<Integer> c2 = c1.reversed();
System.out.println("Smallest = " + n.stream().min(c1).get());
System.out.println("Largest = " + n.stream().min(c2).get());

Na Listagem 5, eu crio uma instância Comparator envolta em um bloco lambda. Então, posso criar um comparador reverso chamando o método padrão reversed() . A habilidade de anexar métodos padrão a interfaces imita o uso comum de mixins (consulte a parte do artigo "Mixins and traits") e é uma boa adição à linguagem Java.

Optional

Observe a chamada final para get() nas chamadas terminais na Listagem 5. Chamadas para métodos integrados, como min() , retornam um Optional em vez de um valor. Esse comportamento imita o recurso option de Java.next, mostrado em Scala em "Common ground in Groovy, Scala, and Clojure, Part 3." Optional impede que retornos de método incluam null como um erro, com null como um valor legítimo. Por exemplo, operações terminais em Java 8 podem usar o método ifPresent() para executar um bloco de código apenas se o resultado legítimo existir. Por exemplo, esse código imprime o resultado apenas se houver um valor presente:

n.stream()
    .min((x, y) -> x - y)
    .ifPresent(z -> System.out.println("smallest is " + z));

Também existe um orElse() que pode ser usado para realizar ação adicional. a navegação na interface do Comparator em Java 8 oferece uma visualização esclarecedora da eficiência que os métodos padrão adicionam.

Mais sobre fluxos

A interface de fluxo e os recursos relacionados em Java 8 são um conjunto bem pensado de extensões que darão nova vida à linguagem Java.

A abstração de fluxo em Java 8 torna recursos funcionais avançados possíveis. Os fluxos atuam, em muitos sentidos, como coleções, mas com diferenças importantes:

  • Os fluxos não armazenam valores. Em vez disso, atuam como um pipeline para uma origem de entrada a um destino com uso de uma operação terminal.
  • Os fluxos são projetados para serem funcionais, e não stateful. Por exemplo, a operação filter() retorna um fluxo de valores filtrados sem modificar a coleção subjacente.
  • Operações de fluxo tentam ter a menor lentidão possível (consulte "Java.next: Memoization and functional synergy" e "Functional thinking: Laziness, Part 1"). Uma coleção lenta realiza trabalhos apenas se for necessário recuperar valores.
  • Os fluxos podem ser ilimitados (ou infinitos). Por exemplo, é possível construir um fluxo que retorne todos os números e usar métodos como limit() e findFirst() para reunir subconjuntos.
  • Como Iterator, os fluxos são consumidos ao uso e devem ser gerados novamente antes de uma próxima reutilização.

As operações de fluxo são intermediárias ou terminais . Operações intermediárias retornam um novo fluxo e sempre são lentas. Por exemplo, usar uma operação filter() em um fluxo não o filtra de fato, mas cria um fluxo que retorna os valores filtrados apenas quando cruzados por uma operação terminal . As operações terminais cruzam o fluxo, produzindo valores ou efeitos colaterais (se você escrever funções que produzam efeitos colaterais, o que não encorajamos).

Os fluxos já incluem muitas operações terminais úteis. Por exemplo, pegue o exemplo do classificador de número da série Functional thinking (que também reaparece em duas partes anteriores de Java.next ). A Listagem 6 implementa o classificador em Java 8.

Lista 6. Classificador de número em Java 8
public class NumberClassifier {

    public static IntStream factorsOf(int number) {
        return range(1, number + 1)
                .filter(potential -> number % potential == 0);
    }

    public static boolean isPerfect(int number) {
        return factorsOf(number).sum() == number * 2;
    }

    public static boolean isAbundant(int number) {
        return factorsOf(number).sum() > number * 2;
    }

    public static boolean isDeficient(int number) {
        return factorsOf(number).sum() < number * 2;
    }

}

Se estiver familiarizado com versões de classificador de número em outras linguagens (consulte "Functional thinking: Thinking functionally"). Verá que falta uma declaração de método sum() na Listagem 6. Em todas as outras implementações desse código em linguagens alternativas, eu mesmo tive que escrever o método sum() . Java 8 inclui sum() como uma operação terminal, liberando-me de escrevê-la. Ocultando partes móveis, a programação funcional reduz a possibilidade de erro do desenvolvedor. Se não houver necessidade de implementar sum(), descarta-se o erro na implementação. A interface de fluxo e os recursos relacionados em Java 8 são um conjunto bem pensado de extensões que darão nova vida à linguagem Java.

Em outras versões do classificador de número, mostrei uma versão otimizada do método factors() , que cruza fatores em potencial apenas até a raiz quadrada e gera os fatores simétricos. A versão otimizada do método Java 8 factors() aparece na Listagem 7.

Lista 7. Classificador otimizado em Java 8
    public static List fastFactorsOf(int number) {
        List<Integer> factors = range(1, (int) (sqrt(number) + 1))
                .filter(potential -> number % potential == 0)
                .boxed()
                .collect(Collectors.toList());
        List factorsAboveSqrt = factors
                .stream()
                .map(e -> number / e).collect(toList());
        factors.addAll(factorsAboveSqrt);
        return factors.stream().distinct().collect(toList());
    }

O método factorsOf() na Listagem 7 não pode apenas combinar dois fluxos em um único resultado, embora os fluxos possam ser concatenados. Porém, quando um fluxo tiver sido cruzado, ele é exaurido (como um Iterator) e deve ser gerado novamente antes da reutilização. Na Listagem 7, crio duas coleções usando fluxos e concateno os resultados, adicionando uma chamada para distinct() , para manipular a extremidade do caso de duplicatas devido a raízes quadradas de número inteiro. Fluxos em Java 8 são extremamente eficientes, incluindo a capacidade de compor fluxos.

Conclusão

Nesta parte do artigo, investiguei Java 8 como uma linguagem Java.next, e ela obtém uma boa pontuação. As bibliotecas de fluxo bem projetadas e os mecanismos de extensão inteligentes, como métodos padrão, permitem que grandes quantidades de código Java existente se beneficiem de novos recursos com pouco esforço.

Na próxima parte do artigo, concluo a série com algumas reflexões sobre a escolha das linguagens.


Recursos para download


Temas relacionados

  • Projeto JDK 8: faça o download de Java 8.
  • Expressões lambda: veja esse tutorial para saber mais sobre alguns novos recursos de Java 8.
  • "Java 8 language changes" (Dennis Sosnoski, developerWorks, 2014): obtenha uma visão crítica de lambdas e mudanças de interface na linguagem Java.
  • Functional Programming in Java (Venkat Subramaniam, Pragmatic Bookshelf, 2014): consulte esse excelente recurso.
  • Functional Thinking (Neal Ford, O'Reilly Media, 2014): saiba mais sobre vários tópicos de Java 8 nesse livro do autor da série.
  • Série de artigos Functional thinking : explore a programação funcional na série da coluna de Neal Ford em developerWorks.
  • Language designer's notebook: nessa série do developerWorks, o arquiteto de linguagem Java Brian Goetz explora alguns problemas de design da linguagem que representaram desafios para a evolução da linguagem Java no Java SE 7, Java SE 8 e posteriores.
  • "Introdução à multitenancy do Java" (Graeme Johnson e Michael Dawson, developerWorks, setembro de 2013): conheça um novo recurso para sistemas em nuvem no IBM Java 8 beta.
  • Scala: o Scala é uma linguagem moderna e funcional na JVM.
  • Clojure: o Clojure é um Lisp moderno e funcional executado na JVM.
  • IBM SDK, Java Technology Edition Versão 8: participe do IBM SDK para o programa Java 8.0 Beta.

Comentários

Acesse ou registre-se para adicionar e acompanhar os comentários.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=980848
ArticleTitle=Java.next: Java 8 como Java.next
publish-date=08152014