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.
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.
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.
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.
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.
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.
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!
É 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.
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:
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.
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.
Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.
Emitimos certificados para atestar que você finalizou nossos cursos e formações.
Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.
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.
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.
Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.
Emitimos certificados para atestar que você finalizou nossos cursos e formações.
Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.
Acesso completo
durante 1 ano
Estude 24h/dia
onde e quando quiser
Novos cursos
todas as semanas