terça-feira, 27 de agosto de 2019

Ordenação de listas no Java 8

Nesse artigo iremos abordar as possíveis formas de ordenar listas no Java 8.

Ordenando lista de objetos que não implementam Comparable

Para começarmos a trabalhar com ordenação, primeiramente vamos criar uma lista de produtos:

Product product1 = new Product(4, "Notebook Samsung", new BigDecimal("1200.00"));
Product product2 = new Product(3, "Notebook Dell", new BigDecimal("1200.00"));
Product product3 = new Product(2, "TV", new BigDecimal("3100.00"));
Product product4 = new Product(1, "Sofá", new BigDecimal("2500.00"));

List<Product> products = Arrays.asList(product1, product2, product3, product4);

Acima temos uma lista de produtos, e a classe Product não implementa Comparable, portanto precisamos criar uma implementação de Comparator para fazer a ordenação. Antes do Java 8 podíamos passar para o método Collections.sort a lista a ser ordenada juntamente com a implementação de Comparator:

Comparator<Product> comparator = new Comparator<>() {
 @Override
 public int compare(Product p1, Product p2) {
  return p1.getName().compareTo(p2.getName());
 }
};

Collections.sort(products, comparator);

Uma das variações do código acima, mas agora fazendo uso do Java 8, é utilizar uma expressão lambda no lugar de classe anônima para gerar uma implementação de Comparator, já que ela é uma interface funcional:

Comparator<Product> comparator = (p1, p2) -> p1.getName().compareTo(p2.getName());
Collections.sort(products, comparator);

Simplificando em uma única linha temos:

Collections.sort(products, (p1, p2) -> p1.getName().compareTo(p2.getName()));

Na versão 8 do Java também foi incluído um método default chamado sort na interface List. Podemos utilizá-lo no lugar do Collections.sort, basta invocá-lo diretamente a partir da nossa lista de produtos:

products.sort((p1, p2) -> p1.getName().compareTo(p2.getName()));

As novidades nessa versão não param por ai, ainda foram adicionados alguns métodos estáticos na interface Comparator que funcionam como uma factory de Comparators. Por exemplo podemos utilizar o Comparator.comparing que recebe uma Function e devolve um Comparator:

Function<Product, String> extractName = p -> p.getName();
products.sort(Comparator.comparing(extractName));

O código acima foi quebrado em duas linhas para fins didáticos e de clareza de código, mas podemos simplificá-lo em uma única linha da seguinte forma:

products.sort(Comparator.comparing(p -> p.getName()));

Visando um código mais fácil de entender que favoreça a manutenibilidade, podemos passar para o comparing um method reference(outro recurso introduzido na versão 8 da linguagem) no lugar de uma expressão lambda:

products.sort(Comparator.comparing(Product::getName));

A respeito do method reference, pode ser usado no lugar de uma expressão lambda quando a mesma não faz nada além de chamar um único método existente, como nesse caso que chama apenas p.getName(). Nesses casos geralmente é sempre mais claro chamar o método existente pelo nome através de um method reference. Podemos dizer que os method references são um tipo especial de expressão lambda que são mais fáceis de ler.

Ordenando listas de objetos que implementam Comparable

Abaixo temos uma lista de Strings. Como String implementa Comparable podemos ordenar uma lista de Strings facilmente usando Collections.sort, o que não é nenhuma novidade pois era a forma usada até a versão anterior do Java:

List<String> series = Arrays.asList("Friends", "Prison Break", "Breaking Bad", "Black Mirror");
Collections.sort(series);

O código acima produz a seguinte saída:

[Black Mirror, Breaking Bad, Friends, Prison Break]

Podemos reescrever o código acima utilizando o método default chamado sort que se encontra presente em List na versão 8 do Java, porém é necessário que passemos um Comparator. Um Comparator para ordenar em ordem natural pode ser conseguido através de Comparator.naturalOrder():

series.sort(Comparator.naturalOrder());

O código acima produz a seguinte saída:

[Black Mirror, Breaking Bad, Friends, Prison Break]

Para ordenar na ordem inversa usamos Comparator.reverseOrder():

series.sort(Comparator.reverseOrder());

O código acima produz a seguinte saída:

[Prison Break, Friends, Breaking Bad, Black Mirror]

Evitando autoboxing desnecessário no sort

No código abaixo a ordenação é feita pelo id do produto que é do tipo primitivo int. Dividimos o código em múltiplas linhas com fins didáticos para melhor entendimento. Tivemos que criar uma Function que contém um método apply que irá receber um Product e retornar um Integer ao invés de int. Por conta disso irá ocorrer autoboxing toda vez que esse método for invocado, e ele pode ser invocado muitas vezes durante o sort. Veja:

Function<Product, Integer> extractId = p -> p.getId();
Comparator<Product> comparator = Comparator.comparing(extractId);
products.sort(comparator);

Para evitar autoboxing desnecessário podemos utilizar ToIntFunction ao invés de Function e também fazer uso de Comparator.comparingInt no lugar de Comparator.comparing:

ToIntFunction<Product> extractId = p -> p.getId();
Comparator<Product> comparator = Comparator.comparingInt(extractId);
products.sort(comparator);

O objetivo do artigo era dar um panorama geral das possibilidades mais comuns de ordenação de listas usando Java 8, mais do que ser uma referência extensiva. Espero que tenham gostado.

Para quem quiser executar os códigos do artigo, estão disponíveis no GitHub.

Pô-pô-por hoje é só pe-pe-pessoal! (os mais velhos pegaram a referência a Looney Tunes hahaha)

quarta-feira, 21 de agosto de 2019

Java 8 - Expressões Lambda e Interfaces funcionais

A linguagem Java surgiu em 1995, portanto já completou 24 anos de existência. Levando em conta sua história, podemos destacar o lançamento do Java 5 em 2004 que foi um marco para a linguagem pois foram incluídas mudanças significativas, das quais podemos ressaltar enums, anotações e generics. Em 2014, com a chegada do Java 8 pudemos observar uma nova onda de mudanças.

Apesar do Java 8 ter sido lançado a alguns anos, tenho observado no mercado que muitas empresas ainda não adotaram essa versão ainda ou estão em processo de migração. Por conta disso escreverei uma série de artigos sobre os recursos incluídos no Java 8 para agregar valor a comunidade e ajudar os desenvolvedores que ainda não se atualizaram ou que precisam de referências para consultar caso estejam começando a utilizar essa versão profissionalmente.

Esse primeiro artigo dessa série irá abordar as expressões lambda e as interfaces funcionais - talvez essas tenham sido as novidades mais comentadas na época do lançamento. Caso você queira executar os códigos desse post, tudo está disponível nesse repositório do GitHub.

Antes do Java 8(usando Anonymous Inner Classes)

Uma forma didática de explicar esses novos conceitos é comparando-os com os códigos equivalentes da versão anterior. Vamos então ver como fazíamos antes para iniciar uma Thread que usasse uma implementação de Runnable. Nesse caso a primeira coisa seria criar uma implementação de Runnable dessa forma:

public class RunnableImplementationExample implements Runnable {

 @Override
 public void run() {
  System.out.println("Running usual implementation...");
 }

}

Agora que criamos uma classe chamada RunnableImplementationExample, que implementa Runnable, podemos instanciá-la e passá-la como argumento para o construtor de Thread. Depois basta iniciar a Thread com o método start(). Veja:


Runnable runnable = new RunnableImplementationExample();
Thread thread = new Thread(runnable);
thread.start();

Outra forma muito utilizada para fazer a mesma coisa é fazer uso de uma classe anônima conforme o código a seguir:

Runnable runnable = new Runnable() {
 public void run() {
  System.out.println("Running anonymous 1...");
 }
};

Thread thread = new Thread(runnable);
thread.start();

Apesar dessa sintaxe ser um pouco estranha pois usa new em conjunto com o nome da interface(que nesse caso é Runnable), não estamos criando uma instância da interface, mas sim uma instância de uma classe anônima(sem nome) que implementa a interface.

Geralmente quando utilizamos uma classe anônima, é usada de forma pontual uma única vez no código, portanto não é necessário nessas ocasiões fazer a atribuição a uma variável. Podemos passar a instância da classe anônima direto no construtor da Thread desse jeito:

Thread thread = new Thread(new Runnable() {
 public void run() {
  System.out.println("Running anonymous 2...");
 }
});
thread.start();

Note que essa maneira é muito verborrágica, ou seja, temos que escrever uma quantidade excessiva de código para tarefas triviais. No entanto não havia o que fazer para contornar isso até o Java 7, mas ai que as expressões lambda do Java 8 entram em cena.

Expressões Lambda

Uma expressão lambda pode ser definida como uma maneira mais simples e menos verbosa de implementar uma interface que tem um único método abstrato(uma interface do tipo SAM que significa "Single Abastract Method" ou simplesmente uma interface funcional). Em outras palavras podemos dizer que através de uma expressão lambda podemos criar um código equivalente a uma classe anônima. Vejamos o mesmo código reescrito com Java 8 fazendo uso de uma expressão lambda:


Runnable runnable = () -> {System.out.println("Running lambda 1...");};
Thread thread = new Thread(runnable);
thread.start();

Usando uma expressão lambda conseguimos diminuir consideravelmente o montante de código final. Como estamos atribuindo o resultado da expressão lambda para uma variável do tipo Runnable, e essa interface tem apenas um método, tudo funciona perfeitamente como em um passe de mágica e nem precisamos deixar explícito qual método da interface está sendo sobrescrito(o nome do método não aparece, apenas um par de parênteses sem nada dentro, seguido do símbolo -> e do corpo da expressão enclausurado entre chaves).

Podemos simplificar ainda mais esse código:

Thread thread = new Thread(() -> System.out.println("Running lambda 2..."));
thread.start();

Passamos a expressão lambda direto ao construtor de Thread, além de removermos as chaves { } do bloco e o ponto e vírgula - o que só é possível devido ao bloco ter apenas uma instrução.

Os códigos apresentados até essa parte do artigo se encontram no pacote br.com.bruno.lambdaexamples.example1 no projeto que se encontra no GitHub.

Interfaces funcionais

Se abrirmos o código fonte da interface Runnable perceberemos a anotação @FunctionalInterface:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface Runnable is used
     * to create a thread, starting the thread causes the object's
     * run method to be called in that separately executing
     * thread.
     * * The general contract of the method run is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

Essa annotation indica que essa interface é uma interface funcional, ou seja, ela deve ter apenas um único método abstrato - nesse caso possui apenas o método run(). Uma interface funcional pode até ter outros métodos desde que eles sejam default(outra novidade do Java 8 que veremos brevemente mais adiante) ou estáticos.

Como você já deve ter percebido as expressões lambda trabalham em conjunto com as interfaces funcionais e só funcionam com esse tipo de interface. A anotação @FunctionalInterface é opcional, mas é uma boa prática para assegurar que sua interface sempre terá apenas um método abstrato e que ninguém irá adicionar outro método abstrato por engano. Caso alguém tente adicionar mais um método abstrato em uma interface que possui essa annotation gerará erro de compilação:

NomeDaInterface.java:3: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  NomeDaInterface is not a functional interface
    multiple non-overriding abstract methods found in interface NomeDaInterface
1 error

Apresentado o conceito de interface funcional, vamos a um exemplo prático onde criaremos uma interface com a anotação @FunctionalInterface e faremos uso dela em conjunto com uma expressão lambda. Segue definição da nossa interface:

@FunctionalInterface
public interface ValidatorInterfaceExample {
 boolean validate(T t);
}

Nesse exemplo criamos uma interface com o objetivo de fazer validações. Agora vamos criar uma implementação dessa interface, por meio de uma classe anônima, para validar CEPs:


ValidatorInterfaceExample<String> zipCodeValidator = new ValidatorInterfaceExample<>() {

 @Override
 public boolean validate(String zipCode) {
  return zipCode.matches("[0-9]{5}-[0-9]{3}");
 }
 
};

System.out.println(zipCodeValidator.validate("06455-906"));

Esse código imprime true.

Reescrevendo esse código com expressão lambda ficaria assim:

ValidatorInterfaceExample<String> zipCodeValidator = 
  (String zipCode) -> {return zipCode.matches("[0-9]{5}-[0-9]{3}");};
System.out.println(zipCodeValidator.validate("06455-906"));

Podemos simplificar ainda mais esse código retirando o tipo do argumento, pois o compilador consegue inferir o tipo automaticamente. Como o bloco é composto de apenas uma linha também é possível se livrar das chaves que o envolvem, além de retirar a palavra return e o ponto e vírgula:


ValidatorInterfaceExample<String> zipCodeValidator = 
  zipCode -> zipCode.matches("[0-9]{5}-[0-9]{3}");
System.out.println(zipCodeValidator.validate("06455-906"));

Os códigos dessa parte do artigo se encontram no pacote br.com.bruno.lambdaexamples.example2 no projeto que está no GitHub.

Interfaces funcionais predefinidas

Além de algumas interfaces já conhecidas de versões anteriores como o Runnable, Readable, Iterable, Comparable, entre outras, se enquadrarem como interfaces funcionais e serem reconhecidas como tais pela JVM, e além de podermos criar nossas próprias interfaces funcionais, no pacote java.util.function introduzido no Java 8 encontramos uma variedade de interfaces funcionais predefinidas(Function, BiFunction, UnaryOperator, BiOperator, Predicate, Supplier, Consumer, dentre outras). Segue uma relação de algumas das principais interfaces funcionais com suas respectivas descrições:

Interface Funcional Descrição
Function Define uma função que recebe um parâmetro e retorna um resultado. O tipo do resultado pode ser diferente do tipo do parâmetro.
BiFunction Define uma função que recebe dois parâmetros e retorna um resultado. O tipo do resultado pode ser diferente do tipo de qualquer parâmetro.
UnaryOperator Representa uma operação em um único operando que retorna um resultado cujo tipo é o mesmo do operando. A interface UnaryOperator pode ser entendida como se fosse uma Function que retorna um valor que é do mesmo tipo do parâmetro. De fato, UnaryOperator é uma subinterface de Function(UnaryOperator extends Function).
BiOperator Representa uma operação com dois operandos. O resultado e os operandos devem ser do mesmo tipo.
Predicate Uma Function que recebe um parâmetro e retorna true ou false baseado no valor do parâmetro.
Supplier Representa um fornecedor de resultados.
Consumer Uma operação recebe um parâmetro porém não retorna nenhum resultado.

Para tornar o artigo mais prático, vamos a um exemplo utilizando a interface funcional Function, que modela uma função que recebe um parâmetro e retorna um resultado. O tipo do resultado pode ser diferente do tipo do parâmetro. O código desse exemplo irá converter milhas em quilômetro:

package br.com.bruno.lambdaexamples.example3;

import java.util.function.Function;

public class FunctionExample {

 public static void main(String[] args) {
  int miles = 3;
  double kilometers = convertMilesToKilometers(miles);
  System.out.printf("%d miles = %3.2f kilometers\n", miles, kilometers);
 }

 private static double convertMilesToKilometers(int miles) {
  /*
   *  Utilizando a interface Function
   *  que é usada para criar uma função que recebe um argumento e retorna um valor.
   *  Nesse exemplo recebe um Integer e retorna um Double.
   *  Unboxing e Autoboxing ocorrem sem problemas nesse código.
   */
  Function<Integer, Double> milesToKilometers = (m) -> 1.6 * m;
  return milesToKilometers.apply(miles);
 }

}

Esse exemplo usando Function está no GitHub no pacote br.com.bruno.lambdaexamples.example3.

Conforme dito anteriormente uma interface funcional podem conter apenas um método abstrato, entretanto não existe problema algum se ela também contiver outros métodos desde que eles sejam default ou static. Esse cenário se aplica a interface funcional Function que possui um único método abstrato chamado apply, um método static chamado identity e outros dois métodos default chamados compose e andThen. Vejamos um exemplo utilizando todos os métodos dessa interface. A primeira parte do exemplo faz uso dos métodos compose e andThen:

Function<Integer, Integer> multiply = (value) -> value * 2;
Function<Integer, Integer> add = (value) -> value + 3;

Function<Integer, Integer> addThenMultiply = multiply.compose(add);

System.out.println(addThenMultiply.apply(3)); //Imprime 12

Function<Integer, Integer> multiplyThenAdd = multiply.andThen(add);

System.out.println(multiplyThenAdd.apply(3)); //Imprime 9

Basicamente tanto um método como o outro retornam uma função composta. O que varia é a ordem de execução das funções que originaram a função composta quando é chamado o apply. Quando a composição é feita dessa forma multiply.compose(add) a função add executa primeiro e o resultado é passado para a função multiply que é executada na sequência. Já se estruturarmos a composição desse jeito multiply.andThen(add) é executada primeiro a função multiply e o resultado passado para a função add que executa em seguida.

Mas ainda resta a pergunta: onde esses dois métodos estão implementados? Dentro da interface? Isso mesmo, a implementação deles está dentro da interface Function. A partir do Java 8 podemos ter métodos com implementação dentro das interfaces, eles são os métodos default(na documentação do beta também podem ser referenciados como defender methods ou extension methods). Vamos entender um pouco melhor a motivação para criação desse recurso. Imagine se na interface List a Oracle colocasse mais um método abstrato, o que aconteceria? Todas as implementações de List teriam que implementar esse método. Isso geraria um trabalho enorme, mas esse não é o principal problema. Ao atualizar para o Java 8, as bibliotecas que tem sua própria implementação de List iriam quebrar, como é o caso do Hibernate. Por isso a decisão final foi optar pelos métodos default que possibilitam que coloquemos uma implementação de um método na interface e as classes que implementam tal interface passam a ter esse método também, de certa forma as implementações "herdam" esse método.

Por último vamos a segunda parte desse exemplo final que mostra como utilizar o método identity():

Function<Integer,Integer> theSame = Function.identity();
System.out.println(theSame.apply(5)); //Imprime 5, pois essa Function retorna o próprio argumento que recebeu

Basicamente esse método serve para retornar uma função que sempre retorna o mesmo argumento que recebeu.

Está disponível no GitHub esse exemplo mostrando como usar cada método da interface Function no pacote br.com.bruno.lambdaexamples.example4 dentro do projeto.

Com isso chegamos ao final desse texto. Em artigos posteriores serão abordados outros assuntos relativos as outras novidades e recursos interessantes do Java 8. Caso você tenha gostado desse texto ou tenha sugestões para próximos artigos comente no post.

Até a próxima javeiros!!!