Lombok: Eliminando código “clichê” no Java

Em um código fonte Java típico são necessárias várias classes para troca de informações, seja para o mapeamento objeto/relacional, para retorno de um serviço REST/web-service ou até mesmo para troca de informações em uma API interna.

Geralmente são utilizadas classes “POJO” ou “Poor Old Java Object”, traduzido como “Simples e velho objeto java”. Se parece como a classe abaixo:

class Montanha {
    private String nome;
    private double latitude;
    private double longitude;
    private String pais;
}

O problema é que para uma classe ter o encapsulamento correto, é necessário criar o código para acessar as propriedade, os getters/setters. A classe fica assim:

import java.util.Objects;

class Montanha {
    private String nome;
    private double latitude;
    private double longitude;
    private String pais;

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public double getLatitude() {
        return latitude;
    }

    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }

    public String getPais() {
        return pais;
    }

    public void setPais(String pais) {
        this.pais = pais;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Montanha montanha = (Montanha) o;
        return Double.compare(montanha.latitude, latitude) == 0 &&
                Double.compare(montanha.longitude, longitude) == 0 &&
                Objects.equals(nome, montanha.nome) &&
                Objects.equals(pais, montanha.pais);
    }

    @Override
    public int hashCode() {

        return Objects.hash(nome, latitude, longitude, pais);
    }

    @Override
    public String toString() {
        return "Montanha{" +
                "nome='" + nome + '\'' +
                ", latitude=" + latitude +
                ", longitude=" + longitude +
                ", pais='" + pais + '\'' +
                '}';
    }
}

Um tanto grande para  uma simples classe, não é mesmo?

Muitas IDEs possuem a funcionalidade para inserir este código automaticamente. Este foi gerado no Intellij Idea, e tudo que precisei fazer foi pressionar ALT+INS, escolher a opção para gerar os getters/setters e aceitar. Depois fiz a mesma coisa para gerar o equals/hashCode e o toString.

Apresentando o Projeto Lombok

Tudo pode ficar mais simples e elegante com o Project Lombok.

O @Data

import lombok.Data;

@Data
class Montanha {
    private String nome;
    private double latitude;
    private double longitude;
    private String pais;
}

Para testar:

public class Main {

    public static void main(String... args) {
        Montanha montanha = new Montanha();
        montanha.setNome("Monte Everest");
        montanha.setLatitude(27.986065);
        montanha.setLongitude(86.922623);
        montanha.setPais("Nepal");

        Montanha montanha2 = new Montanha();
        montanha2.setNome("Monte Everest");
        montanha2.setLatitude(27.986065);
        montanha2.setLongitude(86.922623);
        montanha2.setPais("Nepal");


        System.out.println(montanha);
        System.out.println(montanha.equals(montanha2));
    }
}

Saída:

Montanha(nome=Monte Everest, latitude=27.986065, longitude=86.922623, pais=Nepal)
true

Para que isto funcione:

  1. Adicione o lombok ao seu projeto (exemplo utilizando o maven):
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.20</version>
        </dependency>
    </dependencies>
  2. Instale o plugin no seu editor favorito (para que possa completar automaticamente o código).

O lombok irá atuar em tempo de compilação e gerar o código automaticamente.

OBS: Apenas o lombok 1.6.21 (ainda em desenvolvimento na data deste texto) suporta o Java 9+.

O @Value

O @Value cria uma classe imutável, ou seja, todos os parâmetros são necessários no construtor e a classe não possui os setters.

import lombok.Value;

@Value
class Montanha {
    private String nome;
    private double latitude;
    private double longitude;
    private String pais;
}

public class Main {
        public static void main(String... args) {
        Montanha montanha = new Montanha("Monte Everest", 27.986065,86.922623, "Nepal");
        System.out.println(montanha);
    }
}

Veja na página do lombok todos os outros recursos que ele oferece!

API Stream do Java 8

Similar ao Linq do .net / C#, o Java possui a stream API, que a grosso modo, permite que coleções de dados sejam manipuladas utilizando uma API fluente com expressões lambdas.

Como funcionam os streams?

As operações “stream” são intermediárias ou finais, onde as intermediárias retornam o tipo genérico Stream<> e as finais retornam void ou algum tipo diferente de Stream<>, como no caso abaixo o forEach.

package com.company;

import java.util.Arrays;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        List<String> atores = Arrays.asList("Brad Pitt",
                                            "Edward Norton",
                                            "Christian Bale",
                                            "Chris Pratt",
                                            "Buttercup Cumbersnatch");

        atores.stream()
                .filter(a -> a.startsWith("B"))
                .map(String::toUpperCase)
                .sorted()
                .forEach(System.out::println);
    }
}
BRAD PITT
BUTTERCUP CUMBERSNATCH

No exemplo acima acontece o seguinte:

  • o .stream() transforma a lista de atores no tipo Stream<String>, permitindo o uso da API;
  • .filter() irá realizar um filtro na coleção de dados, onde neste caso estamos buscando atores que começam com “B”;
  • o .map() irá mapear o resultado – é similar ao SELECT do SQL, onde podemos transformar o objeto na saída desejada;
  • .sorted() ordena os itens;
  • .forEach() sai da API stream e enumera os itens.

Geralmente o stream será utilizado a partir do .stream() de uma coleção de dados, mas pode-se criar um stream diretamente, como neste exemplo:

package com.company;

import java.util.stream.Stream;

public class Main {

    public static void main(String[] args) {
        Stream.of("Brad Pitt",
                "Edward Norton",
                "Christian Bale",
                "Chris Pratt",
                "Buttercup Cumbersnatch")
                .filter(a -> a.startsWith("B"))
                .map(String::toUpperCase)
                .sorted()
                .forEach(System.out::println);
    }
}

O resultado é o mesmo do exemplo anterior.

A API stream provê algumas facilidades, como no exemplo abaixo:

String entrada = "1234";
System.out.println(entrada.chars().allMatch(Character::isDigit));

Este trecho irá verificar se a String de entrada possui apenas números. Pode ser facilmente adaptada para verificar se é letra com o Character::isLetter, ou se é minúsculo com Character::isLowercase, etc.

O método String.chars() retorna um IntStream, onde cada caractere é representado por um inteiro.

Existem outros tipos especializados de streams, como o LongStream e o DoubleStream.

Operações avançadas

Os exemplos utilizados serão baseados no seguinte trecho de código:

class Pessoa {
    String nome;
    int idade;

    Pessoa(String nome, int idade) {
        this.nome = nome;
        this.idade = idade;
    }

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public int getIdade() {
        return idade;
    }

    public void setIdade(int idade) {
        this.idade = idade;
    }

    @Override
    public String toString() {
        return nome;
    }
}

        List<Pessoa> pessoas =
                Arrays.asList(
                        new Pessoa("João", 15),
                        new Pessoa("Pedro", 30),
                        new Pessoa("Oscar", 30),
                        new Pessoa("Lucas", 10));

Collect

O collect é uma função terminal que converte o stream em um resultado, podendo ser uma Lista, um Set ou um Map.

List<String> collect = pessoas.stream()
        .map(Pessoa::getNome)
        .collect(Collectors.toList());

collect.forEach(System.out::println);
João
Pedro
Oscar
Lucas
Agrupando:
Map<Integer, List<Pessoa>> collect = pessoas.stream()
                .collect(Collectors.groupingBy(Pessoa::getIdade));

Este exemplo usa um Collector para agrupar as pessoas por idade.

Média
Double collect = pessoas.stream()
        .collect(Collectors.averagingInt(Pessoa::getIdade));

Este irá retornar a média de idade das pessoas.

Existem outros métodos na classe estática Collectors que podem ser úteis.

Outras operações

Encontrar o primeiro item

O finalizador findFirst() irá retornar o primeiro item do stream, que pode ser combinado com um filtro (ou qualquer outra função intermediária).

Optional<Pessoa> first = pessoas.stream()
        .filter(p -> p.getIdade() == 30).findFirst();

Um Optional<> é retornado, pois pode não existir o item.

Encontrar o maior/menor item

Optional<Pessoa> first = pessoas.stream()
                .max(Comparator.comparing(Pessoa::getIdade));

Para encontrar a pessoa de menor idade, basta substituir o max por min.

Distinct / Eliminar duplicados

List<Integer> collect = pessoas.stream()
        .map(Pessoa::getIdade)
        .distinct()
        .collect(Collectors.toList());

O exemplo acima retorna todas as idades da coleção, sem repeti-las.

Verificar se existe algum item

if(pessoas.stream().anyMatch(pessoa -> pessoa.getIdade() == 30)) 
    System.out.println("Existe uma pessoa com 30 anos");

O anyMatch() irá verificar se existe ao menos um item que satisfaça a(s) condição(ções).

Filtrar por vários itens

List<String> nomesProcurados = Arrays.asList("João", "Pedro");

pessoas.stream()
        .filter(p -> nomesProcurados.contains(p.getNome()))
        .map(Pessoa::getNome)
        .forEach(System.out::println);
João
Pedro

Este exemplo irá procurar os nomes João e Pedro entre as pessoas.

Reduzir

O método reduce() irá reduzir a stream a apenas um item final. Veja o exemplo:

List<BigDecimal> nums = Arrays.asList(new BigDecimal("10"),
                                      new BigDecimal("20"),
                                      new BigDecimal("30"));

Optional<BigDecimal> reduce = nums.stream().reduce(BigDecimal::add);

System.out.println(reduce.get());

A saída será a soma dos itens (neste caso 60), pois foi passado como argumento BigDecimal::add.

Achatar

O método flatMap() achata uma coleção dentro de uma coleção.

List<Pessoa> pessoas =
        Arrays.asList(
                new Pessoa("João", 15, Arrays.asList("Gabriel", "Vinícius")),
                new Pessoa("Pedro", 30, Arrays.asList("Joaquim", "Túlio")),
                new Pessoa("Oscar", 30, Arrays.asList("Lorenzo", "Guilherme")),
                new Pessoa("Lucas", 10, Arrays.asList("Marcos", "Fernando")));


pessoas.stream()
        .flatMap(f -> f.getPrimos().stream())
        .forEach(System.out::println);
Gabriel
Vinícius
Joaquim
Túlio
Lorenzo
Guilherme
Marcos
Fernando

A saída deste exemplo será uma lista com o nome de todos os primos.

Complicando um pouco mais, podemos combinar um novo objeto juntando o “pai” com os “filhos”:

pessoas.stream()
         .flatMap(f -> f.getPrimos().stream().map(m -> String.join(" -> ",f.getNome(), m)))
         .forEach(System.out::println);
João -> Gabriel
João -> Vinícius
Pedro -> Joaquim
Pedro -> Túlio
Oscar -> Lorenzo
Oscar -> Guilherme
Lucas -> Marcos
Lucas -> Fernando

Ordenar

A função sorted() recebe um comparador para ordenar os itens.

pessoas.stream()
        .sorted(Comparator.comparingInt(Pessoa::getIdade))
        .forEach(System.out::println);

Para ordenar decrescente:

pessoas.stream()
        .sorted(Comparator.comparingInt(Pessoa::getIdade).reversed())
        .forEach(System.out::println);

Esta outra forma pode ser utilizado para casos mais complicados, onde o desenvolvedor deseja customizar a ordenação:

pessoas.stream()
        .sorted((o1, o2) -> {
            if(o1.getIdade() == o2.getIdade())
                return 0;
            else if(o1.getIdade() > o2.getIdade())
                return 1;
            else
                return -1;
        })
        .forEach(System.out::println);

Reutilizando Streams

O Java não permite que streams sejam reutilizados.

Stream<Pessoa> pessoaStream= pessoas.stream().filter(p -> p.getIdade() == 30);

List<Pessoa> collect1 = pessoaStream.collect(Collectors.toList());
Optional<Pessoa> oscar = pessoaStream.filter(p -> p.getNome().equals("Oscar")).findFirst();

Este código gera a seguinte exceção:

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:203)
    at java.util.stream.ReferencePipeline.<init>(ReferencePipeline.java:94)
    at java.util.stream.ReferencePipeline$StatelessOp.<init>(ReferencePipeline.java:618)
    at java.util.stream.ReferencePipeline$2.<init>(ReferencePipeline.java:163)
    at java.util.stream.ReferencePipeline.filter(ReferencePipeline.java:162)
    at com.company.Main.main(Main.java:53)

Pode ser resolvido utilizando o Supplier<>:

Supplier<Stream<Pessoa>> pessoaStreamSupplier = () -> pessoas.stream().filter(p -> p.getIdade() == 30);

List<Pessoa> collect1 = pessoaStreamSupplier.get().collect(Collectors.toList());
Optional<Pessoa> oscar = pessoaStreamSupplier.get().filter(p -> p.getNome().equals("Oscar")).findFirst();

Java: diferença entre variavel++ e ++variavel

Qual a diferença entre ++variavel e variavel++ ?

Veja o código abaixo:

package com.company;

public class Main {
    public static void main(String... args) {

        int var1 = 1;
        int var2 = 1;

        ++var1;
        var2++;

        System.out.println(String.format("var1: %d, var2: %d", var1, var2));
    }
}

A saída será:

var1: 2, var2: 2

Então é a mesma coisa?

Não!

Veja agora:

package com.company;

public class Main {
    public static void main(String... args) {

        int var1 = 1;
        int var2 = 1;

        System.out.println(String.format("var1: %d, var2: %d", var1++, ++var2));
    }
}

Com a mudança do lugar (foi para a mesma linha da saída), o resultado agora é diferente:

var1: 1, var2: 2

A diferença é que var++ retorna o valor atual e depois incrementa enquanto ++var incrementa e depois retorna o valor. Portanto no primeiro exemplo, como o println ocorria após a operação de incremento de ambas as variáveis, o resultado ficou igual.

No “for”

Adivinhe a saída destas linhas de código:

package com.company;

public class Main {
    public static void main(String... args) {

        for(int i=0; i<10; i++)
            System.out.println(i);

        for(int i=0; i<10; ++i)
            System.out.println(i);
    }
}

Se você respondeu que são iguais, acertou! Ambas irão imprimir de 0 a 9. Isto porque como o incremento ocorre separadamente, não tem diferença.

No “do… while”

Adivinhe novamente. Tem diferença?

package com.company;

public class Main {
    public static void main(String... args) {

        int i = 0;

        do {
            System.out.println(i);
        } while (i++ < 10);

        i = 0;

        do {
            System.out.println(i);
        } while (++i < 10);
    }
}

Neste caso tem! No primeiro “do…while” na comparaçã de while (i++ < 10) o valor primeiro é comparado e depois é incrementado, então o valor vai de 0 a 10.

No segundo “do…while” o valor é incrementado e depois comparado então só vai até 9, pois é o último valor menor que 10.

Java: varargs / variadic functions / funções variádicas

Funções variádicas (ou função com quantidade variável de argumentos) são funções que aceitam número de parâmetros variáveis, ou seja, a quantidade de parâmetros não é fixada. A abreviação “varargs” é comumente utilizada para referir-se a estes tipos de funções.

No Java o argumento de funções variádicas é representado por “…” (três pontos). Veja o exemplo abaixo:

package com.company;

public class Main {

    public static void main(String... args) {
        Main m = new Main();

        m.imprimir();
        m.imprimir("Banana", "Maça", "Abacate");
        m.imprimirIntStrings(10, "Laranja", "Tomate");
    }

    public void imprimir(String... strs) {
        for(String s : strs) {
            System.out.println(s);
        }
    }

    public void imprimirIntStrings(int inteiro, String... strs) {
        System.out.println(String.format("%d", inteiro));
        imprimir(strs);
    }
}

A saída será:

Banana
Maça
Abacate
10
Laranja
Tomate

Onde:

public void imprimir(String... strs) { }

É equivalente a:

public void imprimir(String[] strs) {}

Ou seja, na realidade “String…” é um array de Strings com ZERO ou mais elementos.

A própria implementação do String.format utiliza este “artifício”:

public static String format(String format, Object... args) {
    return new Formatter().format(format, args).toString();
}

Uma função “varargs” pode ter mais de um parâmetro, mas apenas um parâmetro vararg que deverá ser sempre o último, como ocorre nesta parte do exemplo:

public void imprimirIntStrings(int inteiro, String... strs) {
    System.out.println(String.format("%d", inteiro));
    imprimir(strs);
}

Na realidade é apenas uma abreviação, ou uma forma mais “limpa” de escrever. Sem os “varargs” teríamos que fazer:

public static void main(String... args) {
    Main m = new Main();

    m.imprimir();
    m.imprimir(new String[] {"Banana", "Maça", "Abacate"});
    m.imprimirIntStrings(10, new String[] {"Laranja", "Tomate"});
}

No entanto, observe que isto não é válido:

package com.company;

public class Main {

    public static void main(String... args) {
        Main m = new Main();

        m.imprimir();
        m.imprimir("Banana", "Maça", "Abacate");
    }

    public void imprimir(String[] strs) {
        for(String s : strs) {
            System.out.println(s);
        }
    }
}

O compilador irá gerar erro nas duas linhas:

m.imprimir();
m.imprimir("Banana", "Maça", "Abacate");

Sendo, respectivamente:

Error:(8, 10) java: method imprimir in class com.company.Main cannot be applied to given types;
   required: java.lang.String[]
   found: no arguments
   reason: actual and formal argument lists differ in length
Error:(9, 10) java: method imprimir in class com.company.Main cannot be applied to given types;
  required: java.lang.String[]
  found: java.lang.String,java.lang.String,java.lang.String
  reason: actual and formal argument lists differ in length

Então apesar de “String… strs” ser equivalente a “String[] strs”, existem diferenças:

  • “String… strs” aceita ZERO parâmetros, enquanto “String[] strs” aceitaria apenas um array vazio;
  • “String… strs” aceita ser chamada com vários parâmetros do tipo String, enquanto “String[] strs” aceita apenas um parâmetro que deve ser um array de String.

Java: para comparar, utilizar == ou .equals() ?

Em linhas gerais no Java o “==” compara se o objeto é a mesma referência e o .equals() faz a comparação dos valores. Mas existem algumas formas de complicar..

O .equals() é uma função da classe base Object e é implementado de forma diferente em algumas sub classes.

Veja, por exemplo, a implementação do .equals do objeto String:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

A implementação primeiro compara se é a mesma referência, pois se for, certamente são iguais. Depois verifica se o parâmetro é uma instância de String, pois se não for, não é igual. Sem seguida compara o tamanho, se for diferente, não é igual. E por último compara caractere por caractere, e retorna false se algum for diferente.

Veja as variações de comparações no exemplo abaixo:

package com.company;

public class Main {

    public static void main(String[] args) {
        int int1 = 1;
        int int2 = 1;

        Integer integer1 = 1;
        Integer integer2 = 1;
        Integer integer3 = new Integer(1);

        Integer integergrande1 = 123456;
        Integer integergrande2 = 123456;

        String str1 = "Olá";
        String str2 = "Olá";
        String str3 = new String("Olá");

        //ambos true - tipos primitivos
        System.out.println(String.format("int1 == int2: %s", int1 == int2));
        System.out.println(String.format("int1 == 1: %s", int1 == 1));

        //true - caching
        System.out.println(String.format("integer1 == integer2: %s", integer1 == integer2));
        //true - autoboxing
        System.out.println(String.format("integer1 == 1: %s", integer1 == 1));

        //false
        System.out.println(String.format("integer1 == integer3: %s", integer1 == integer3));

        //false
        System.out.println(String.format("integergrande1 == integergrande2: %s", integergrande1 == integergrande2));

        //true - pool de strings
        System.out.println(String.format("str1 == str2: %s", str1 == str2));
        //false - não apontam para mesma referência
        System.out.println(String.format("str1 == str3: %s", str1 == str3));
        //true - são iguais
        System.out.println(String.format("str1.equals(str2): %s", str1.equals(str2)));
        //true - são iguais
        System.out.println(String.format("str1.equals(str3): %s", str1.equals(str3)));
    }
}

 

1) Tipos primitivos (int, double, long, etc..) sempre comparam o valor, portanto a parte abaixo retorna true:

//ambos true - tipos primitivos
System.out.println(String.format("int1 == int2: %s", int1 == int2));
System.out.println(String.format("int1 == 1: %s", int1 == 1));

2) Neste trecho (onde integer1 e integer2 são objetos Integer) retorna true, pois o Java faz cache da faixa -127 até 128 (um tanto estranho, não é?):

System.out.println(String.format("integer1 == integer2: %s", integer1 == integer2));

3) Neste trecho (comparando objeto Integer com constante) retorna true, pois ocorre o autoboxing:

System.out.println(String.format("integer1 == 1: %s", integer1 == 1));

O autoboxing surgiu no Java 5.0 com a introdução dos tipos de Objeto equivalentes aos tipos primitivos, por exemplo int -> Integer justamente para facilitar o uso entre os tipos equivalentes.

Então o integer1 será transformado em int antes da comparação, retornando true, pois são iguais.

4) Retorna false, pois apesar de terem valores iguais, ambos são Integer, portanto não ocorre o autoboxing e a comparação é feita por referência:

System.out.println(String.format("integer1 == integer3: %s", integer1 == integer3));

5) Retorna false, pois não cai no intervalo de cache de -127 a 128:

System.out.println(String.format("integergrande1 == integergrande2: %s", integergrande1 == integergrande2));

6) Retorna true por causa do pool de Strings. Este método é chamado de “string interning” e consiste em apontar para a mesma referência strings iguais, como ocorre neste caso:

System.out.println(String.format("str1 == str2: %s", str1 == str2));

7) Porém, nem todas as situações fazem uso do string pool. No caso abaixo retorna false, pois str3, apesar de ser igual a str1, foi criada de forma diferente:

System.out.println(String.format("str1 == str3: %s", str1 == str3));

8) O método .equals() sempre compara o valor (a não ser que exista uma implementação que faça diferente, já que este método pode ser sobrescrito):

System.out.println(String.format("str1.equals(str2): %s", str1.equals(str2)));

Na real, o .equals de Object compara a referência:

public boolean equals(Object obj) {
    return (this == obj);
}

9) Também retorna true, já que o .equals() de string compara os valores:

System.out.println(String.format("str1.equals(str3): %s", str1.equals(str3)));

Se tiver dúvida, utilize o .equals() para comparar valores e o == para comparar referências, a não ser para os tipos primitivos onde pode ser utilizado == para comparar valores.