Por que você precisa atualizar para o Java 8 já

Publicados: 2022-03-11

A versão mais recente da plataforma Java, Java 8, foi lançada há mais de um ano. Muitas empresas e desenvolvedores ainda estão trabalhando com versões anteriores, o que é compreensível, pois há muitos problemas com a migração de uma versão da plataforma para outra. Mesmo assim, muitos desenvolvedores ainda estão iniciando novos aplicativos com versões antigas do Java. Existem poucas boas razões para fazer isso, porque o Java 8 trouxe algumas melhorias importantes para a linguagem.

Existem muitos recursos novos no Java 8. Mostrarei alguns dos mais úteis e interessantes:

  • Expressões lambda
  • API de fluxo para trabalhar com coleções
  • Encadeamento de tarefas assíncronas com CompletableFuture
  • Nova API de tempo

Expressões lambda

Um lambda é um bloco de código que pode ser referenciado e passado para outro pedaço de código para execução futura uma ou mais vezes. Por exemplo, funções anônimas em outras linguagens são lambdas. Assim como as funções, lambdas podem receber argumentos no momento de sua execução, modificando seus resultados. O Java 8 introduziu expressões lambda , que oferecem uma sintaxe simples para criar e usar lambdas.

Vamos ver um exemplo de como isso pode melhorar nosso código. Aqui temos um comparador simples que compara dois valores Integer pelo seu módulo 2:

 class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }

Uma instância desta classe pode ser chamada, futuramente, em código onde este comparador for necessário, assim:

 ... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...

A nova sintaxe lambda nos permite fazer isso de forma mais simples. Aqui está uma expressão lambda simples que faz a mesma coisa que o método compare de BinaryComparator :

 (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

A estrutura tem muitas semelhanças com uma função. Entre parênteses, configuramos uma lista de argumentos. A sintaxe -> mostra que este é um lambda. E na parte direita desta expressão, configuramos o comportamento do nosso lambda.

EXPRESSÃO JAVA 8 LAMBDA

Agora podemos melhorar nosso exemplo anterior:

 ... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...

Podemos definir uma variável com este objeto. Vamos ver como fica:

 Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

Agora podemos reutilizar essa funcionalidade, assim:

 ... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...

Observe que nesses exemplos, o lambda está sendo passado para o método sort() da mesma forma que a instância de BinaryComparator é passada no exemplo anterior. Como a JVM sabe interpretar o lambda corretamente?

Para permitir que funções recebam lambdas como argumentos, o Java 8 introduz um novo conceito: interface funcional . Uma interface funcional é uma interface que possui apenas um método abstrato. Na verdade, o Java 8 trata as expressões lambda como uma implementação especial de uma interface funcional. Isso significa que, para receber um lambda como argumento de método, o tipo declarado desse argumento precisa apenas ser uma interface funcional.

Quando declaramos uma interface funcional, podemos adicionar a notação @FunctionalInterface para mostrar aos desenvolvedores o que é:

 @FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }

Agora, podemos chamar o método sendDTO , passando diferentes lambdas para obter um comportamento diferente, assim:

 sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));

Referências de métodos

Os argumentos lambda nos permitem modificar o comportamento de uma função ou método. Como podemos ver no último exemplo, às vezes o lambda serve apenas para chamar outro método ( sendToAndroid ou sendToIos ). Para este caso especial, o Java 8 apresenta uma abreviação conveniente: referências de método . Essa sintaxe abreviada representa um lambda que chama um método e tem o formato objectName::methodName . Isso nos permite tornar o exemplo anterior ainda mais conciso e legível:

 sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);

Nesse caso, os métodos sendToAndroid e sendToIos são implementados this classe. Também podemos referenciar os métodos de outro objeto ou classe.

API de transmissão

O Java 8 traz novas habilidades para trabalhar com Collections , na forma de uma nova Stream API. Essa nova funcionalidade é fornecida pelo pacote java.util.stream e visa permitir uma abordagem mais funcional à programação com coleções. Como veremos, isso é possível em grande parte graças à nova sintaxe lambda que acabamos de discutir.

A API Stream oferece fácil filtragem, contagem e mapeamento de coleções, bem como diferentes maneiras de obter fatias e subconjuntos de informações delas. Graças à sintaxe de estilo funcional, a API Stream permite um código mais curto e elegante para trabalhar com coleções.

Vamos começar com um pequeno exemplo. Usaremos este modelo de dados em todos os exemplos:

 class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }

Vamos imaginar que precisamos imprimir todos os autores de uma coleção de books que escreveram um livro depois de 2005. Como faríamos isso em Java 7?

 for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }

E como faríamos isso no Java 8?

 books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element

É apenas uma expressão! Chamar o método stream() em qualquer Collection retorna um objeto Stream encapsulando todos os elementos dessa coleção. Isso pode ser manipulado com diferentes modificadores da API Stream, como filter() e map() . Cada modificador retorna um novo objeto Stream com os resultados da modificação, que pode ser manipulado posteriormente. O método .forEach() nos permite realizar alguma ação para cada instância do fluxo resultante.

Este exemplo também demonstra a estreita relação entre programação funcional e expressões lambda. Observe que o argumento passado para cada método no fluxo é um lambda personalizado ou uma referência de método. Tecnicamente, cada modificador pode receber qualquer interface funcional, conforme descrito na seção anterior.

A API Stream ajuda os desenvolvedores a ver as coleções Java de um novo ângulo. Imagine agora que precisamos obter um Map dos idiomas disponíveis em cada país. Como isso seria implementado no Java 7?

 Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }

No Java 8, as coisas são um pouco mais organizadas:

 import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));

O método collect() nos permite coletar os resultados de um stream de diferentes maneiras. Aqui, podemos ver que primeiro agrupa por país e depois mapeia cada grupo por idioma. ( groupingBy() e toSet() são métodos estáticos da classe Collectors .)

API JAVA 8 STREAM

Existem muitas outras habilidades da API Stream. A documentação completa pode ser encontrada aqui. Eu recomendo ler mais para obter uma compreensão mais profunda de todas as ferramentas poderosas que este pacote tem a oferecer.

Encadeamento de tarefas assíncronas com CompletableFuture

No pacote java.util.concurrent do Java 7, existe uma interface Future<T> , que nos permite obter o status ou resultado de alguma tarefa assíncrona no futuro. Para utilizar esta funcionalidade, devemos:

  1. Crie um ExecutorService , que gerencia a execução de tarefas assíncronas e pode gerar objetos Future para acompanhar seu progresso.
  2. Crie uma tarefa Runnable de forma assíncrona.
  3. Execute a tarefa no ExecutorService , que fornecerá um Future dando acesso ao status ou resultados.

Para fazer uso dos resultados de uma tarefa assíncrona, é necessário monitorar seu progresso de fora, usando os métodos da interface Future , e quando estiver pronto, recuperar explicitamente os resultados e realizar outras ações com eles. Isso pode ser bastante complexo para implementar sem erros, especialmente em aplicativos com um grande número de tarefas simultâneas.

No Java 8, no entanto, o conceito Future é levado mais longe, com a interface CompletableFuture<T> , que permite a criação e execução de cadeias de tarefas assíncronas. É um mecanismo poderoso para criar aplicativos assíncronos em Java 8, pois permite processar automaticamente os resultados de cada tarefa após a conclusão.

Vejamos um exemplo:

 import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);

O método CompletableFuture.supplyAsync cria uma nova tarefa assíncrona no Executor padrão (normalmente ForkJoinPool ). Quando a tarefa for finalizada, seus resultados serão automaticamente fornecidos como argumentos para a função this::getLinks , que também é executada em uma nova tarefa assíncrona. Finalmente, os resultados desta segunda etapa são automaticamente impressos em System.out . thenApply() e thenAccept() são apenas dois dos vários métodos úteis disponíveis para ajudá-lo a criar tarefas simultâneas sem usar manualmente Executors .

O CompletableFuture facilita o gerenciamento do sequenciamento de operações assíncronas complexas. Digamos que precisamos criar uma operação matemática de várias etapas com três tarefas. A tarefa 1 e a tarefa 2 usam algoritmos diferentes para encontrar um resultado para a primeira etapa, e sabemos que apenas um deles funcionará enquanto o outro falhará. No entanto, qual deles funciona depende dos dados de entrada, que não sabemos com antecedência. O resultado dessas tarefas deve ser somado com o resultado da tarefa 3 . Assim, precisamos encontrar o resultado da tarefa 1 ou da tarefa 2 e o resultado da tarefa 3 . Para conseguir isso, podemos escrever algo assim:

 import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution

Se examinarmos como o Java 8 lida com isso, veremos que todas as três tarefas serão executadas ao mesmo tempo, de forma assíncrona. Apesar da tarefa 2 falhar com uma exceção, o resultado final será computado e impresso com sucesso.

PROGRAMAÇÃO ASSÍNCRONA JAVA 8 COM CompletableFuture

CompletableFuture facilita muito a criação de tarefas assíncronas com vários estágios e nos oferece uma interface fácil para definir exatamente quais ações devem ser executadas na conclusão de cada estágio.

API de data e hora Java

Conforme declarado pela própria admissão de Java:

Antes do lançamento do Java SE 8, o mecanismo de data e hora Java era fornecido pelas classes java.util.Date , java.util.Calendar e java.util.TimeZone , bem como suas subclasses, como java.util.GregorianCalendar . Essas classes tinham várias desvantagens, incluindo

  • A classe Calendar não era segura para tipos.
  • Como as classes eram mutáveis, elas não podiam ser usadas em aplicativos multithread.
  • Bugs no código do aplicativo eram comuns devido à numeração incomum de meses e à falta de segurança de tipo.”

O Java 8 finalmente resolve esses problemas de longa data, com o novo pacote java.time , que contém classes para trabalhar com data e hora. Todos eles são imutáveis ​​e possuem APIs semelhantes ao popular framework Joda-Time, que quase todos os desenvolvedores Java usam em seus aplicativos em vez dos nativos Date , Calendar e TimeZone .

Aqui estão algumas das classes úteis neste pacote:

  • Clock - Um relógio para informar a hora atual, incluindo o instante atual, data e hora com fuso horário.
  • Duration e Period - Uma quantidade de tempo. Duration usa valores baseados em tempo, como “76,8 segundos, e Period , baseado em data, como “4 anos, 6 meses e 12 dias”.
  • Instant - Um ponto instantâneo no tempo, em vários formatos.
  • LocalDate , LocalDateTime , LocalTime , Year , YearMonth - Uma data, hora, ano, mês ou alguma combinação deles, sem fuso horário no sistema de calendário ISO-8601.
  • OffsetDateTime , OffsetTime - Uma data e hora com um deslocamento de UTC/Greenwich no sistema de calendário ISO-8601, como "2015-08-29T14:15:30+01:00".
  • ZonedDateTime - Uma data e hora com um fuso horário associado no sistema de calendário ISO-8601, como "1986-08-29T10:15:30+01:00 Europe/Paris".

API JAVA 8 TIME

Às vezes, precisamos encontrar alguma data relativa, como “primeira terça-feira do mês”. Para esses casos, java.time fornece uma classe especial TemporalAdjuster . A classe TemporalAdjuster contém um conjunto padrão de ajustadores, disponíveis como métodos estáticos. Estes permitem-nos:

  • Encontre o primeiro ou último dia do mês.
  • Encontre o primeiro ou último dia do mês seguinte ou anterior.
  • Encontre o primeiro ou último dia do ano.
  • Encontre o primeiro ou último dia do ano seguinte ou anterior.
  • Encontre o primeiro ou último dia da semana dentro de um mês, como “primeira quarta-feira de junho”.
  • Encontre o dia da semana seguinte ou anterior, como "próxima quinta-feira".

Aqui está um pequeno exemplo de como obter a primeira terça-feira do mês:

 LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
Ainda usando Java 7? Venha com o programa! #Java8
Tweet

Java 8 em resumo

Como podemos ver, o Java 8 é uma versão de época da plataforma Java. Há muitas mudanças de linguagem, principalmente com a introdução de lambdas, que representa um movimento para trazer habilidades de programação mais funcionais para Java. A API Stream é um bom exemplo de como lambdas podem mudar a maneira como trabalhamos com ferramentas Java padrão com as quais já estamos acostumados.

Além disso, o Java 8 traz alguns novos recursos para trabalhar com programação assíncrona e uma revisão muito necessária de suas ferramentas de data e hora.

Juntas, essas mudanças representam um grande avanço para a linguagem Java, tornando o desenvolvimento Java mais interessante e eficiente.