Primeiras aulas do curso Java moderno: tire proveito dos novos recursos do Java 8

Java moderno: tire proveito dos novos recursos do Java 8

Default Methods - Default Methods

O Java 8

As tarefas mais comuns de um programador podem ser desafiadoras no Java. O motivo? A sintaxe com mais de 20 anos, tornando-a uma linguagem burocrática. Felizmente isso mudou significativamente com o Java 8. Um exemplo? Ordenação de objetos.

Vamos usar o Eclipse nos nossos exemplos. Você pode utilizar qualquer outra IDE, ou até mesmo a linha de comando. O antigo Eclipse Kepler 4.3 possui suporte ao Java 8 via download, a partir do Eclipse Luna 4.4, esse suporte já vem ativado. Lembre-se de verificar se você tem o Java 8 instalado, indo ao console/terminal e digitando java -version. Deve sair 1.8.0. Caso contrário, atualize a versão do seu java.

No eclipse, crie um novo projeto chamado Java8 e, através das propriedades do projeto, escolha a opção Java Compiler e ative a versão 8. Se ela não está disponível e você tem certeza que instalou o Java 8, basta adicionar esse JDK em Windows, Preferences, Java, Installed JREs.

Em uma nova classe OrdenaStrings, crie o método main e vamos fazer uma lista de strings e trabalhar com ele sem nenhum dos novos recursos da linguagem:

List<String> palavras = new ArrayList<>();
palavras.add("alura online");
palavras.add("casa do código");
palavras.add("caelum");

Vale lembrar que podemos criar uma lista de objetos diretamente via Arrays.asList, fazendo List<String> palavras = Arrays.asList("", "", ...). A diferença é que não se pode mudar a quantidade de elementos de uma lista devolvida por esse método.

Como fazemos para ordenar essa lista? Podemos fazer isso sem usar nenhuma novidade: com o Collections.sort:

Collections.sort(palavras);
System.out.println(palavras);

E se quisermos ordenar essas palavras em uma ordem diferente? Por exemplo, pela ordem do tamanho das palavras. Nesse caso, utilizaremos um Comparator. Podemos criá-la como uma outra classe, por enquanto apenas o esqueleto:


class ComparadorDeStringPorTamanho implements Comparator<String> {

    public int compare(String s1, String s2) {
        return 0;
    }

}

O que preencher aí dentro? Se você lembrar, o contrato da interface Comparator diz que devemos devolver um número negativo se o primeiro objeto for menor que o segundo, um número positivo caso contrário e zero se forem equivalentes. Esse "maior", "menor" e "equivalente" é algo que nós decidimos. No nosso caso, vamos dizer que uma String é "menor" que outra se ela tiver menos caracteres. Então podemos fazer:

class ComparadorDeStringPorTamanho implements Comparator<String> {
    public int compare(String s1, String s2) {
        if(s1.length() < s2.length()) 
            return -1;
        if(s1.length() > s2.length()) 
            return 1;
        return 0;
    }
}

E, para ordenar com esse novo critério de comparação, podemos fazer:

Comparator<String> comparador = new ComparadorDeStringPorTamanho();
Collections.sort(palavras, comparador);

Até aqui, nenhuma novidade. No decorrer do curso, você verá como esse código ficará muito, muito menor, mais sucinto e expressivo com cada recurso que formos estudar do Java 8. Vamos ao primeiro deles. Em vez de usar o Collections.sort, podemos invocar essa operação na própria List! Veja:

Comparator<String> comparador = new ComparadorDeStringPorTamanho();
palavras.sort(comparador);

Parece pouco, mas há muita coisa por trás. Em primeiro lugar, esse método sort não existia antes na interface List, nem em suas mães (Collection e Iterable).

Será então que simplesmente adicionaram um novo método? Se tivessem feito assim, haveria um grande problema: todas as classes que implementam List parariam de compilar, pois não teriam o método sort. E há muitas, muitas classes que implementam essas interfaces básicas do Java. Há implementações no Hibernate, no Google Collections e muito mais.

Default Methods

Para evitar essa quebra, o Java 8 optou por criar um novo recurso que possibilitasse adicionar métodos em interfaces e implementá-los ali mesmo! Se você abrir o código fonte da interface List, verá esse método:

    default void sort(Comparator<? super E> c) {
        Collections.sort(this, c);
    }

É um default method! Um método de interface que você não precisa implementar na sua classe se não quiser, pois você terá já essa implementação default. Repare que ele simplesmente delega a invocação para o bom e velho Collections.sort, mas veremos que outros métodos fazem muito mais.

Default methods foi uma forma que o Java encontrou para evoluir interfaces antigas, sem gerar incompatibilidades. Não é uma novidade da linguagem: Scala, C# e outras possuem recursos similares e até mais poderosos. E repare que é diferente de uma classe abstrata: em uma interface você não pode ter atributos de instância, apenas esses métodos que delegam chamadas ou trabalham com os próprios métodos da interface.

foreach, Consumer e interfaces no java.util.functions

Vamos a um outro método default adicionado as coleções do Java: o forEach na interface Iterable. Como Iterable é mãe de Collection, temos acesso a esse método na nossa lista.

Se você abrir o JavaDoc ou utilizar o auto complete do Eclipse, verá que List.forEach recebe um Consumer, que é uma das muitas interfaces do novo pacote java.util.functions. Então vamos criar um consumidor de String:

class ConsumidorDeString implements Consumer<String> {
    public void accept(String s) {
        System.out.println(s);
    }
}

E podemos passar uma instância dessa para o forEach:

Consumer<String> consumidor = new ConsumidorDeString();
palavras.forEach(consumidor);

Interessante? Ainda não muito. Talvez fosse mais direto e simples escrever um for(String s : lista).

Default methods é o primeiro recurso que conhecemos. Sim, é bastante simples e parece não trazer grandes melhorias. O segredo é utilizá-los junto com lambdas, que você verá a seguir, e trará um impacto significativo para o seu código.

Que venham os lambdas! - Que venham os lambdas!

Melhorando com classes anônimas

Vamos retomar o nosso forEach, ele precisa da classe que implementa Consumer:

class ConsumidorDeString implements Consumer<String> {
    public void accept(String s) {
        System.out.println(s);
    }
}

E a invocação:

Consumer<String> consumidor = new ConsumidorDeString();
palavras.forEach(consumidor);

Se você já está acostumado com Java há mais tempo, sabe que nesses casos não criamos uma classe isolada. Fazemos tudo ao mesmo tempo, criando a classe e instanciando-a:

Consumer<String> consumidor = new Consumer<String>() {
    public void accept(String s) {
        System.out.println(s);
    }
};
palavras.forEach(consumidor);

São as chamadas classes anônimas, que usamos com frequência para implementar listeners e callbacks que não terão reaproveitamento.

Poderíamos até mesmo evitar a criação da variável consumidor, passando a classe anônima diretamente para o forEach:

palavras.forEach(new Consumer<String>() {
    public void accept(String s) {
        System.out.println(s);
    }
});

Quando começamos a aprender Java, essa sintaxe pode intimidar. Ela aparece com frequência, em especial nesses casos onde a implementação é curta e simples.

Lambda para simplificar

Tendo essas dificuldade e verbosidade da sintaxe das classes anônimas em vista, o Java 8 traz uma nova forma de implementar essas interfaces ainda mais sucinta. É a sintaxe do lambda. Em vez de escrever a classe anônima, deixamos de escrever alguns itens que podem ser inferidos.

Como essa interface só tem um método, não precisamos escrever o nome do método. Também não daremos new. Apenas declararemos os argumentos e o bloco a ser executado, separados por ->:

palavras.forEach((String s) -> {
    System.out.println(s);
});

É uma forma bem mais sucinta de escrever! Essa sintaxe funciona para qualquer interface que tenha apenas um método abstrato, e é por esse motivo que nem precisamos falar que estamos implementando o método accept, já que não há outra possibilidade. Podemos ir além e remover a declaração do tipo do parâmetro, que o compilador também infere:

palavras.forEach((s) -> {
    System.out.println(s);
});

Quando há apenas um parâmetro, nem mesmo os parenteses são necessários:

palavras.forEach(s -> {
    System.out.println(s);
});

Dá pra melhorar? Sim. podemos remover as chaves de declaração do bloco, assim como o ponto e vírgula, pois só existe uma única instrução:

palavras.forEach(s -> System.out.println(s));

Pronto. Em vez de usarmos classes anônimas, utilizamos o lambda para escrever códigos simples e sucintos nesses casos. Uma interface que possui apenas um método abstrato é agora conhecida como interface funcional e pode ser utilizada dessa forma.

Outro exemplo é o próprio Comparator que já vimos. Se utilizarmos a forma de classe anônima, teremos essa situação:

palavras.sort(new Comparator<String>() {
    public int compare(String s1, String s2) {
        if (s1.length() < s2.length())
            return -1;
        if (s1.length() > s2.length())
            return 1;
        return 0;
    }
});

Como aplicar a mesma lógica para transformar isso em um lambda? Basta removermos quase tudo da assinatura do método, assim como o new Comparator e adicionar o -> entre os parâmetros e o bloco. Além disso, podemos tirar o tipo dos parâmetros:

palavras.sort((s1, s2) -> {
    if (s1.length() < s2.length())
        return -1;
    if (s1.length() > s2.length())
        return 1;
    return 0;
});

Melhor? Parece que sim. Mas ainda não muito interessante. O lambda se encaixa melhor quando a expressão dentro do bloco é mais curta. Normalmente com apenas um statement. Conhecendo a API do Java, podemos ver que há um método que compara dois inteiros e retorna negativo/positivo/zero dependendo se o primeiro for menor/maior/igual ao segundo. É o Integer.compare. Com ele, reduzimos o lambda para o seguinte:

palavras.sort((s1, s2) -> {
    return Integer.compare(s1.length(), s2.length());
});

Dá para fazer melhor. Como há apenas um único statement, podemos remover as chaves. Além disso, o return pode ser eliminado que o compilador vai inferir que deve ser retornado o valor que o próprio compare devolver:

palavras.sort((s1, s2) -> Integer.compare(s1.length(), s2.length()));

Compare com a nossa primeira versão. Muito melhor! Claro que poderíamos ter utilizado o Integer.compare desde o capítulo anterior, mas a combinação com o lambda deixa tudo mais legível e simples.

Vale lembrar que não é porque digitamos menos linhas que o código é necessariamente mais simples. Às vezes, pouco código pode tornar difícil de entender uma ideia, um algoritmo. Não é o nosso caso.

Código mais sucinto com Method references - Código mais sucinto com Method references

Com os lambdas e métodos default, conseguimos escrever a ordenação das Strings de uma forma bem mais sucinta:

palavras.sort((s1, s2) -> Integer.compare(s1.length(), s2.length()));

Podemos ir além.

Métodos default em Comparator

Há vários métodos auxiliares no Java 8. Até em interfaces como o Comparator. E você pode ter um método default que é estático. Esse é o caso do Comparator.comparing, que é uma fábrica, uma factory, de Comparator. Passamos o lambda para dizer qual será o critério de comparação desse Comparator, repare:

palavras.sort(Comparator.comparing(s -> s.length()));

Veja a expressividade da linha, está escrito algo como "palavras ordene comparando s.length". Podemos quebrar em duas linhas para ver o que esse novo método faz exatamente:

Comparator<String> comparador = Comparator.comparing(s -> s.length());
palavras.sort(comparador);

Dizemos que Comparator.comparing recebe um lambda, mas essa é uma expressão do dia a dia. Na verdade, ela recebe uma instância de uma interface funcional. No caso é a interface Function que tem apenas um método, o apply. Para utilizarmos o Comparator.comparing, nem precisamos ficar decorando os tipos e assinatura do método dessas interfaces funcionais. Essa é uma vantagem dos lambdas. Você também vai acabar programando dessa forma. É claro que, com o tempo, você vai conhecer melhor as funções do pacote java.util.functions. Vamos quebrar o código mais um pouco. Não se esqueça de dar os devidos imports.

Function<String, Integer> funcao = s -> s.length();
Comparator<String> comparador = Comparator.comparing(funcao);
palavras.sort(comparador);

A interface Function vai nos ajudar a passar um objeto para o Comparator.comparing que diz qual será a informação que queremos usar como critério de comparação. Ela recebe dois tipos genéricos. No nosso caso, recebe uma String, que é o tipo que queremos comparar, e um Integer, que é o que queremos extrair dessa string para usar como critério. Poderia até mesmo criar uma classe anônima para implementar essa Function e seu método apply, sem utilizar nenhum lambda. O código ficaria grande e tedioso.

Quisemos quebrar em três linhas para que você pudesse enxergar o que ocorre por trás exatamente. Sem dúvida o palavras.sort(Comparator.comparing(s -> s.length())) é mais fácil de ler. Dá para melhorar ainda mais? Sim!

Method reference

É muito comum escrevermos lambdas curtos, que simplesmente invocam um único método. É o exemplo de s -> s.length(). Dada uma String, invoque e retorne o método length. Por esse motivo, há uma forma de escrever esse tipo de lambda de uma forma ainda mais reduzida. Em vez de fazer:

palavras.sort(Comparator.comparing(s -> s.length()));

Fazemos uma referência ao método (method reference):

palavras.sort(Comparator.comparing(String::length));

São equivalentes nesse caso! Sim, é estranho ver String::length e dizer que é equivalente a um lambda, pois não há nem a -> e nem os parênteses de invocação ao método. Por isso é chamado de method reference. Ela pode ficar ainda mais curta com o import static:

import static java.util.Comparator.*;
palavras.sort(comparing(String::length));

Vamos ver melhor a semelhança entre um lambda e seu method reference equivalente. Veja as duas declarações a seguir:

Function<String, Integer> funcao1 = s -> s.length();
Function<String, Integer> funcao2 = String::length;

Elas ambas geram a mesma função: dada um String, invoca o método length e devolve este Integer. As duas serão avaliadas/resolvidas (evaluated) para Functions equivalentes.

Quer um outro exemplo? Vejamos o nosso forEach, que recebe um Consumer:

palavras.forEach(s -> System.out.println(s));

Dada uma String, invoque o System.out.println passando-a como argumento. É possível usar method reference aqui também! Queremos invocar o println de System.out:

palavras.forEach(System.out::println);

Novamente pode parecer estranho. Não há os parênteses, não há a flechinha (->), nem os argumentos que o Consumer recebe. Fica tudo implícito. Dessa vez, o argumento recebido (isso é, cada palavra dentro da lista palavras), não será a variável onde o método será invocado. O Java 8 consegue perceber que tem um println que recebe objetos, e invocará esse método, passando a String da vez.

Quando usar lambda e quando usar method reference? Algumas vezes não é possível usar method references. Se você tiver, por exemplo, um lambda que dada uma String, pega os 5 primeiros caracteres, faríamos s -> s.substring(0, 5). Esse lambda não pode ser escrito como method reference! Pois não é uma simples invocação de métodos onde os parâmetros são os mesmos que os do lambda.

Sobre o curso Java moderno: tire proveito dos novos recursos do Java 8

O curso Java moderno: tire proveito dos novos recursos do Java 8 possui 92 minutos de vídeos, em um total de 38 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

Plus

  • Acesso a TODOS os cursos da plataforma

    Mais de 1200 cursos completamente atualizados, com novos lançamentos todas as semanas, em Programação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

12X
R$85
à vista R$1.020
Matricule-se

Pro

  • Acesso a TODOS os cursos da plataforma

    Mais de 1200 cursos completamente atualizados, com novos lançamentos todas as semanas, em Programação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

12X
R$120
à vista R$1.440
Matricule-se
Conheça os Planos para Empresas

Acesso completo
durante 1 ano

Estude 24h/dia
onde e quando quiser

Novos cursos
todas as semanas