Software Engineer

Explorando as Novidades do Java: Um Guia das Funcionalidades do Java 9 ao 21

Este é um compilado das funcionalidades que considerei importantes para quem utiliza Java no dia a dia. Analisei a documentação de lançamento do OpenJDK, começando da versão 9 até a versão mais recente de longo prazo (LTS), que atualmente é a versão 21.
Meu objetivo é criar uma base de conhecimento de acesso prático, para simplificar e gerar maior produtividade para nós, desenvolvedores. Assim, poderemos compreender e aproveitar rapidamente as melhorias na linguagem, aplicando-as tanto na escrita quanto na leitura do código Java.

Collection factory methods

Release 9 JEP 269
Foi introduzido factory methods que podem ser acionados diretamente da interface para a criação de coleções imutáveis, ou seja, que não permitem adição ou remoção de elementos.
Um dos benefícios desses métodos é a otimização, pois eliminam algumas camadas. Quando retornamos uma lista com argumentos específicos, uma coleção com os campos será criada diretamente, sem a necessidade de usar um array por trás, como no método Arrays.asList().

Stream API

toList()

Como sabemos podemos coletar resultados de um stream usando a Api de Coletores, um coletor muito usado é o Collectors.toList(). Por este motivo, a adição do método toList é conveniente.

dropWhile()

Durante um stream podemos remover elementos enquanto a condição for verdadeira usando dropWhile.

takeWhile()

Mantem elementos enquanto a condição for verdadeira.

mapMulti()

O operador mapMulti permite substituir elementos de um stream com zero ou mais elementos. Pode ser usado para substituir um flatMap de maneira imperativa, especialmente para pequenos ou zero elementos de stream, evitando a instanciação de um novo stream para cada grupo requerido pelo flatMap.
Exemplo com flatMap, onde precisamos de um stream para albuns e outro stream intermediário para artistas.
Exemplo com mapMulti, tememos melhor desempenho e reaproveitamos o mesmo fluxo de stream, sem necessidade de um stream intermediário para cada elemento.

Local-Variable type inference

Release 10, JEP 286
Permite omitir a declaração do tipo em variáveis locais. Em vez de usar o tipo podemos usar a palavra var e o Java vai fazer a inferência do tipo, motivado para reduzir o boilerplate de código Java.

Text Blocks

Release 15 JEP 378
Nos permite escrever Strings com múltiplas linhas sem a necessidade de concatenação de String. Basta englobar o texto entre aspas triplas """ e quebrar a linha, tornando desnecessário escapar as aspas.

Records

Release 16 JEP 395
É uma forma simples e concisa de definir classes para transportadores de dados imutáveis. A declaração dos campos e tipos no corpo da classe não são necessários pois são configurados no construtor.
Os records implementam automaticamente métodos baseados em dados como acessadores, equals, hashCode, e toString. Visto que muitos desenvolvedores tendem a querer economizar código, omitem a implementação de métodos que podem levar a um comportamento indesejado ou reduzir a capacidade de depuração.
A seguir temos um exemplo de uma classe transportadora de dados (DTO), geralmente seguem este modelo de implementação:
A implementação anterior usando classes pode ser facilmente substituída por records. Na declaração de um record, os campos são identificados diretamente no cabeçalho e são final por padrão. Veja o exemplo:
É uma boa prática declarar apenas atributos classificados como imutáveis, primitivos, String, LocalDate ou outros records. Ao adicionarmos objetos mutáveis, como uma coleção, podemos obter resultados diferentes em equals e hashCode. Isso pode levar a problemas de segurança ao usar esses objetos como chaves em um Map ou como membros de um Set. Portanto, para esses casos, é recomendável fazer uma cópia da coleção mutável no construtor canônico.
Em comparação com uma classe normal, Records possuem algumas limitações:
  • Não podemos herdar um Record de outra classe e sua classe super é sempre java.lang.Record como acontece com classes Enum.
  • Uma classe Record é implicitamente final, não pode ser abstract.
  • Para suportar a imutabilidade, seus campos são final.
  • A declaração dos campos não é suportada no corpo da classe, somente no header.
  • Não é permitido a declaração de métodos nativos, isso faria a classe depender de um estado externo e não explícito da classe. (Métodos Java com implementação escrita em outra linguagem como C/C++).

Sealed Interfaces e Classes

Release 17 JEP 409
Classes e interfaces “seladas” restringem quais outras classes ou interfaces podem extendê-las ou implementá-las. Em termos práticos, ela conhece seus descendentes em tempo de compilação.
Há outras maneiras de restringir a extensão de uma classe. O modificador final proíbe completamente quem tem acesso à classe base de estendê-la. Tornar a classe base package-private impossibilita a herança a partir de outros pacotes, porém não permite que a classe base seja referenciada ou usada como tipo de uma variável local.
Para fechar essa lacuna, agora a verificação de instanceof para tipos selados com subclasses passa a ser bem-sucedida. Por exemplo, ao verificar uma instância de uma classe selada A, com subclasses B e C, saberemos se a instância é A, B ou C.
Ao criar uma superclasse, devemos ser capazes de expressar um conjunto específico de subclasses, para que o compilador e o leitor reconheçam as classes descendentes.
Para declarar uma classe ou interface, basta usarmos a palavra-chave sealed como modificador.
Para declarar quais classes são permitidas, usamos a cláusula permits .

Pattern Matching - instanceof

Release 16 JEP 394
Pattern Matching é um conceito amplamente utilizado em linguagens funcionais. A ideia é encontrar a sequência ou construtor correspondente. Se o padrão for atendido, partes individuais desse padrão podem ser vinculados a variáveis.

Pattern Matching - switch

Release 21 JEP 441
Uma extensão similar à aplicada em instanceof, permite que padrões apareçam nos rótulos de cada case. Isso significa que, em vez de apenas comparar uma variável com um valor específico em cada caso, podemos usar padrões mais complexos para realizar correspondências.
Essas mudanças devem tornar o switch mais expressivo e também melhorar a legibilidade do código, especialmente quando há uma variedade de casos tratados com lógica condicional.
Podemos usar patter matching com switch para:
Correspondência de tipos: Verificar se um objeto é de um determinado tipo.
Desestruturação de registros: Extrair valores de campos de um registro.
Correspondência de padrões complexos: Usar padrões mais sofisticados, como combinações de padrões, para lidar com situações complexas de forma concisa.

Lidando com null

Tradicionalmente, um switch gera NullPointerException se a expressão for avaliada como nula. No entanto, agora há uma inspeção no case que permite o tratamento adequado dessa situação.
Blocos sem tratamento do case null mantêm o comportamento padrão, lançando NullPointerException. Veja os exemplos a seguir nos quais ambos são equivalentes e lançam exceção.

Pattern matching - records

Release 21 JEP 440
O objetivo é ampliar a correspondência de padrões para desestruturar registros e permitir consultas de dados mais avançadas, além de adicionar padrões aninhados para tornar as consultas de dados mais compostas.

Sequenced Collections

Release 21 JEP 431
Java oferece coleções ordenadas, como LinkedHashMap e List, no entanto, não possui uma interface consistente para essas coleções. Cada implementação possui suas particularidades e não há padronização das operações que lidam com os elementos das coleções.
As novas interfaces foram projetadas para garantir que todos os elementos sejam organizados em uma ordem definida e ofereçam suporte a operações comuns para o processamento desses elementos, tanto do primeiro ao último quanto do último ao primeiro.

AutoCloseable HTTP Client

Release 21 JEP 321
A API do cliente HTTP existe desde o Java 11, e agora possui a capacidade de fechamento automático. Ao implementar a interface AutoCloseable, o JDK automaticamente chama o método close() após a conclusão da instrução try-with-resources, assegurando uma liberação eficiente de recursos.

Virtual Threads

Release 21 JEP 444
O Java 21 adicionou um novo tipo de thread chamado thread virtual. Diferentemente das threads tradicionais, que são gerenciadas pelo sistema operacional, as threads virtuais são implementadas diretamente pelo JDK..
As threads virtuais podem melhorar significativamente o rendimento do aplicativo quando há um elevado número de tarefas simultâneas (acima de milhares). E também quando a carga de trabalho não está vinculada à CPU, pois ter muito mais thread do que núcleos de processador não pode melhorar o rendimento nesse caso.
Podemos melhorar o rendimento de aplicações de servidor, porque consistem em um grande número de tarefas simultâneas que passam grande parte do tempo esperando.
Estamos acostumados a usar pool de thread para controlar o limite de concorrência e evitar exceder os recursos disponíveis. No entanto, com threads virtuais, essa preocupação não é mais necessária, pois elas não são custosas e podem ser executadas por tarefa sem a necessidade de agrupamento.

Conclusão

Neste post, com tantas atualizações, percebemos o esforço e o contínuo comprometimento da comunidade em inovar a linguagem e manter a plataforma robusta e moderna para nós, desenvolvedores.
Analisamos as funcionalidades lançadas entre as versões 9 e 21 do Java, consideradas relevantes especialmente para os desenvolvedores que escrevem código de negócios e utilizam Java no dia a dia.
Muitas outras funcionalidades foram lançadas desde a versão 9. Obviamente, mostrar todas elas não foi minha intenção. No entanto, você pode estar interessado em mudanças na JVM, bibliotecas ou ferramentas e, quem sabe, até mesmo conhecer todas as implementações. Para isso, recomendo que avalie a documentação oficial do Projeto OpenJDK.

Fontes