Sitemap
MrDebugg3r

MrDebugg3r

Java 21 Sequenced Collections

5 min readSep 3, 2024

--

Desde o anúncio da Oracle de lançar o Java semestralmente, após a versão 9, tivemos três versões LTS (Long term support, suporte a longo prazo) lançadas. Atualmente estamos na versão 21, lançada em 19 de setembro de 2023.

Java 21 veio com novos recursos (features). Gosto das Sequenced Collections, porque elas fazem a manipulação das coleções Java mais fáceis do que antes.

As “Coleções Sequenciadas” fazem parte da JEP 431 (JDK Enhancement Proposal, proposta de melhoria da JDK). Em resumo, novas interfaces foram adicionadas para representar coleções com um ordinal de encontro. Cada coleção tem o primeiro, o segundo, até o último elemento bem definidos. Há uma API uniforme para processar os primeiros e últimos elementos, bem como na ordem inversa.

O que “sequenciado” significa? Os elementos estão arranjados numa ordem particular. A coleção tem os primeiros e últimos elementos, e os elementos entre eles são predecessores ou sucessores.

Qual problema essa JEP resolve?

As coleções, a maioria, não tinham uma forma de encontrar os elementos com uma encounter order*, — primeiro, segundo, etc — e também não era uma API uniforme.

*encounter order, “ordem de encontro”, não faz muito sentido em português, doravante vou usar o termo “ordinal de posição”.

Press enter or click to view image in full size

Veja que antes de Java 21, List e Deque definem métodos para um ordinal de posição, métodos get(), getFirst() e getLast(), mas o supertipo, Collection, não. E ainda, nem a interface Set, nem o subtipo HashSet tem ordinal de posição. O ordinal de posição é um meio de identificar elementos baseados na sua posição numa ordem específica — primeiro, segundo, …, último.

Esta hieraquia é confusa e é potencialmente mais desafiante de depurar (debug). Um problema surge quando visões de coleção (view collections*) são rebaixadas (downgrade) à semântica mais fraca. E.g., encapsular (wrapping) uma LinkedHashSet com Collections::unmodifiableSet resulta num Set que descarta informação sobre o ordinal de posição.

*view collections, no Collections Framework é um objeto leve que implementa as interfaces Collection ou Map, mas não é uma coleção real no sentido tradicional.

LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
Set<String> unmodifiableSet = Collections.unmodifiableSet(linkedHashSet);

Em resumo, enquanto muitas implementações permitem recuperar o primeiro ou último elemento, cada coleção define seu próprio método, e alguns não são visíveis de imediato ou faltam. E.g., não podemos recuperar diretamente o último elemento de uma LinkedHashSet sem percorrê-la por inteiro. Iterar do primeiro ao último elemento é direto e consistente, mas não em sentido contrário. Veja:

// lista usada em todos os exemplos seguintes
List<String> frutas = Arrays.asList("banana", "cereja", "maçã");

// usando Iterator
Iterator<String> iterator = frutas.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}

// ... usado for
for (int i = 0; i < frutas.size(); i++) {
String element = frutas.get(i);
System.out.println(element);
}

// .. com Stream
frutas.stream().forEach(element -> System.out.println(element));

// ... convertendo a lista para array e iterando o array
String[] array = frutas.toArray(new String[frutas.size()]);
for (String element : array) {
System.out.println(element);
}

Iterar inversamente é diferente em cada caso. NavigableSet provê a visão descendingSet() para iteração inversa:

for (var e : navSet.descendingSet())
process(e);

// Deque também faz com descendingIterator:
for (var it = deque.descendingIterator(); it.hasNext();) {
var e = it.next();
process(e);
}

// List também o faz, mas com ListIterator:
for (var it = list.listIterator(list.size()); it.hasPrevious();) {
var e = it.previous();
process(e);
}

Finalmente, LinkedHashSet não suporta a iteração inversa. A única forma prática de fazê-lo é copiar seus elementos noutra coleção.

É poderoso e eficaz processar os elementos duma coleção usando streams, mas difícil na ordem inversa. Somente NavigableSet o faz convenientemente.

navigableSet.descendingSet().stream();

As outras coleções, faz-se, ou copiando os elementos para outra coleção ou criando uma stream de um Spliterator customizado que inverte a iteração.

Com o Java 21…

Sabendo-se os problemas, vamos ver como as interfaces funcionarão do Java 21 em diante.

Get MrDebugg3r’s stories in your inbox

Join Medium for free to get updates from this writer.

Novas interfaces para coleções, sets et maps sequenciados foram incluídas, bem como a sua adaptação na hierarquia existente. Todos os novos métodos declarados nestas interfaces têm implementações default.

Vejamos a interface SequencedCollection.

Uma coleção sequenciada suporta operações comuns dos dois lados (início e fim) e facilita o processamento de elementos nas ordens direta e inversa.

Os métodos addFirst(), addLast(), getFirst(), getLast(), removeFirst() e removeLast() vieram da interface Deque.

Ademais, o método reversed() foi adicionado. Ele provê uma visão (view) inversa da coleção original. Quaisquer modificações na coleção original serão visíveis na visão.

Um pouco de prática

Vamos ver como essas mudanças podem ser aplicadas!

List<String> lista = new ArrayList<>();
lista.add("Letra B");
lista.addLast("Letra Z"); // j21, adiciona no fim da lista
lista.addFirst("Letra A"); // j21, adiciona no início da lista
// saída: lista [Letra A, Letra B, Letra Z]

System.out.println( lista.getFirst() ); // saída: Letra A
System.out.println( lista.getLast() ); // saída: Letra Z

System.out.println( lista.removeFirst() ); // remove e retorna Letra A
System.out.println( lista.removeLast() ); // remove e retorna Letra Z
System.out.println( lista ); // [Letra B]

Como eu mencionei acima, é possível iterar de várias formas em ordem inversa:

for ( String str : lista.reversed() );  // com enhanced for

lista.reversed().stream().forEach( System.out::println ); // stream

lista.reversed().parallelStream().forEach( System.out::println ); // parallelStream

for ( Object str : lista.reversed().toArray() ); // array

Do exemplo acima, ainda poderíamos alterar apenas uma linha de código para usar uma LinkedList.

// List<String> lista = new ArrayList<>();
List<String> lista = new LinkedList<>();
// ... e o código a seguir não precisaria ser alterado

Caso a lista esteja vazia, uma exceção será lançada.

List<String> lista = new LinkedList<>();  // lista vazia
System.out.println( lista.getLast() ); // lança uma java.util.NoSuchElementException

A NoSuchElementException indicará que nenhum elemento foi encontrado:

Exception in thread "main" java.util.NoSuchElementException
at java.base/java.util.LinkedList.getLast(LinkedList.java:268)
at com.mrdebugger.jep431.TestNoSuchElementException.main(TestNoSuchElementException.java:10)

Ainda falta falar dos Sequenced Sets e Sequenced Maps, que deixarei para outro artigo. Finalmente, deixo um diagrama de classes mais completo e você, deixe o seu “aplauso” e comentário, basta se inscrever com o Google, rápido e simples!

Referências

MARKS, Stuart. JEP 431: Sequenced Collections. 23 out. 2023. Disponível em: https://openjdk.org/jeps/431. Acesso em: 2 setembro 2024.

EVANS, Ben. Java to Move to 6-Monthly Release Cadence. 6 set. 2017. Disponível em: https://www.infoq.com/news/2017/09/Java6Month/. Acesso em: 2 setembro 2024.

PIASECKI, Robert. Views in Java Collections Framework. 19 mar. 2014. Disponível em: https://softwarecave.org/2014/03/19/views-in-java-collections-framework/. Acesso em: 2 setembro 2024.

--

--