Guia da API de coleções do Vavr

O VAVR (anteriormente conhecido como Javaslang) é uma biblioteca funcional sem fins lucrativos para Java 8+. Ele permite que você escreva códigos funcionais do tipo Scala em Java e serve para reduzir a quantidade de códigos e melhorar sua qualidade. Site da biblioteca .

Abaixo do corte, há uma tradução de um artigo que sistematiza informações na API do Vavr Collections .

Traduzido por @middle_java

Artigo original da última modificação: 15 de agosto de 2019

1. Visão geral


A biblioteca Vavr, anteriormente conhecida como Javaslang, é uma biblioteca funcional para Java. Neste artigo, exploramos sua poderosa API de coleta.

Consulte este artigo para obter mais informações sobre esta biblioteca.

2. Coleções persistentes


Uma coleção persistente, quando modificada, cria uma nova versão da coleção sem alterar a versão atual.

O suporte para várias versões da mesma coleção pode levar ao uso ineficiente da CPU e da memória. No entanto, a biblioteca de coleções do Vavr supera isso compartilhando a estrutura de dados entre diferentes versões da coleção.

Isso é fundamentalmente diferente de unmodifiableCollection() da classe de utilitário Java Collections , que simplesmente fornece um wrapper para a coleção base.

Tentar modificar essa coleção UnsupportedOperationException vez de criar uma nova versão. Além disso, a coleção base ainda é mutável através de um link direto para ela.

3. Tranversável


Traversable é o tipo base de todas as coleções do Vavr. Essa interface define métodos comuns a todas as estruturas de dados.

Ele fornece alguns métodos padrão úteis, como size() , get() , filter() , isEmpty() e outros que são herdados pelas sub-interfaces.

Exploramos ainda mais a biblioteca de coleções.

4. Seq


Vamos começar com as sequências.

A interface Seq é uma estrutura de dados seqüencial. Essa é a interface pai para List , Stream , Queue , Array , Vector e CharSeq . Todas essas estruturas de dados têm suas próprias propriedades únicas, as quais discutiremos abaixo.

4.1 Lista


List é uma operação calculada energeticamente (avaliada com avidez, é executada assim que os valores de seus operandos se tornam conhecidos) uma sequência de elementos que estendem a interface LinearSeq .

As List persistentes List construídas recursivamente usando a cabeça e a cauda :

  • A cabeça é o primeiro elemento
  • Cauda - uma lista contendo os elementos restantes (esta lista também é formada a partir da cabeça e da cauda)

A API da List contém métodos estáticos de fábrica que você pode usar para criar uma List . Você pode usar o método static of() para criar uma instância da List partir de um ou mais objetos.

Você também pode usar o método static empty() para criar uma List vazia e o método ofAll() para criar uma List do tipo Iterable :

 List < String > list = List.of( "Java", "PHP", "Jquery", "JavaScript", "JShell", "JAVA"); 

Vejamos alguns exemplos de manipulação de lista.

Podemos usar o método drop() e suas variantes para remover os primeiros N elementos:

 List list1 = list.drop(2); assertFalse(list1.contains("Java") && list1.contains("PHP")); List list2 = list.dropRight(2); assertFalse(list2.contains("JAVA") && list2.contains("JShell")); List list3 = list.dropUntil(s - > s.contains("Shell")); assertEquals(list3.size(), 2); List list4 = list.dropWhile(s - > s.length() > 0); assertTrue(list4.isEmpty()); 

drop(int n) remove n itens da lista, iniciando no primeiro item, enquanto dropRight() faz o mesmo, iniciando no último item da lista.

dropUntil() remove os itens da lista até que o predicado seja true , enquanto dropWhile() remove os itens até que o predicado seja true .

Existem também os dropRightWhile() e dropRightUntil() que removem itens começando pela direita.

Em seguida, take(int n) usado para recuperar itens da lista. Ele pega n itens da lista e depois para. Há também takeRight(int n) , que recebe elementos do final da lista:

 List list5 = list.take(1); assertEquals(list5.single(), "Java"); List list6 = list.takeRight(1); assertEquals(list5.single(), "Java"); List list7 = list.takeUntil(s - > s.length() > 6); assertEquals(list3.size(), 3); 

Por fim, takeUntil() pega elementos da lista até que o predicado se torne true . Há uma variante de takeWhile() que também aceita um argumento predicado.

Além disso, a API possui outros métodos úteis, por exemplo, mesmo distinct() , que retorna uma lista de elementos com duplicatas excluídas, além de distinctBy() , que aceita o Comparator para determinar a igualdade.

É muito interessante que também exista intersperse() , que insere um elemento entre cada elemento da lista. Isso pode ser muito conveniente para operações com String :

 List list8 = list .distinctBy((s1, s2) - > s1.startsWith(s2.charAt(0) + "") ? 0 : 1); assertEquals(list3.size(), 2); String words = List.of("Boys", "Girls") .intersperse("and") .reduce((s1, s2) - > s1.concat(" " + s2)) .trim(); assertEquals(words, "Boys and Girls"); 

Deseja dividir a lista em categorias? E para isso existe uma API:

 Iterator < List < String >> iterator = list.grouped(2); assertEquals(iterator.head().size(), 2); Map < Boolean, List < String >> map = list.groupBy(e - > e.startsWith("J")); assertEquals(map.size(), 2); assertEquals(map.get(false).get().size(), 1); assertEquals(map.get(true).get().size(), 5); 

O método group(int n) divide List em grupos de n elementos cada. O método groupdBy() pega uma Function que contém a lógica para dividir a lista e retorna um Map com dois elementos: true e false .

A chave true é mapeada para a List elementos que satisfazem a condição especificada em Function . A chave false mapeada para a List elementos que não atendem a essa condição.

Como esperado, ao alterar a List , a List original não é alterada. Em vez disso, a nova versão da List sempre retornada.

Também podemos interagir com o List usando a semântica da pilha - extraindo elementos de acordo com o princípio “último a entrar, primeiro a sair” (LIFO). Nesse sentido, existem métodos de API como peek() , pop() e push() para manipular a pilha:

 List < Integer > intList = List.empty(); List < Integer > intList1 = intList.pushAll(List.rangeClosed(5, 10)); assertEquals(intList1.peek(), Integer.valueOf(10)); List intList2 = intList1.pop(); assertEquals(intList2.size(), (intList1.size() - 1)); 

A função pushAll() é usada para inserir um intervalo de números inteiros na pilha, e a função peek() é usada para recuperar o elemento principal da pilha. Há também um método peekOption() que pode peekOption() o resultado em um objeto Option .

Existem outros métodos interessantes e realmente úteis na interface da List que são completamente documentados nos documentos Java .

4.2 Fila


A Queue imutável armazena elementos, permitindo recuperá-los de acordo com o princípio FIFO (primeiro a entrar, primeiro a sair).

Queue interna consiste em duas listas vinculadas: a List frontal e a List traseira. A List frontal contém itens removidos da fila e a List traseira contém os itens na fila.

Isso permite que você coloque as operações de enfileiramento e remoção da fila na complexidade O (1) . Quando a List termina na List frontal quando é removida da fila, a List posterior List revertida e se torna a nova List frontal.

Vamos criar uma fila:

 Queue < Integer > queue = Queue.of(1, 2); Queue < Integer > secondQueue = queue.enqueueAll(List.of(4, 5)); assertEquals(3, queue.size()); assertEquals(5, secondQueue.size()); Tuple2 < Integer, Queue < Integer >> result = secondQueue.dequeue(); assertEquals(Integer.valueOf(1), result._1); Queue < Integer > tailQueue = result._2; assertFalse(tailQueue.contains(secondQueue.get(0))); 

A função dequeue() remove o elemento head da Queue e retorna Tuple2<T, Q> . O primeiro elemento da tupla é o elemento principal removido da fila, o segundo elemento da tupla são os demais elementos da Queue .

Podemos usar a combination(n) para obter todas as N combinações possíveis de elementos na Queue :

 Queue < Queue < Integer >> queue1 = queue.combinations(2); assertEquals(queue1.get(2).toCharSeq(), CharSeq.of("23")); 

Mais uma vez, a Queue original não muda ao adicionar / remover itens da fila.

4.3 Stream


Stream é uma implementação de uma lista vinculada lenta que é significativamente diferente de java.util.stream . Ao contrário do java.util.stream , o Stream Vavr armazena dados e calcula preguiçosamente os elementos subseqüentes.
Digamos que temos números inteiros de Stream :

 Stream < Integer > s = Stream.of(2, 1, 3, 4); 

Ao imprimir o resultado de s.toString() no console, apenas Stream (2 ,?) será exibido. Isso significa que apenas o elemento principal Stream foi calculado, enquanto os elementos finais não foram.

Chamar s.get(3) e exibir o resultado de s.tail() retornará Stream (1, 3, 4 ,?) . Pelo contrário, se você não chamar s.get(3) - o que fará o Stream calcular o último elemento - somente o Stream (1 ,?) será o resultado de s.tail() ) . Isso significa que apenas o primeiro elemento da cauda foi calculado.

Esse comportamento pode melhorar o desempenho e permite que o Stream seja usado para representar seqüências que são (teoricamente) infinitamente longas.
Stream no Vavr é imutável e pode ser Empty ou Cons . Cons consiste no elemento principal e na cauda calculada preguiçosamente do Stream . Diferentemente da List , o Stream armazena Stream o elemento head na memória. Os elementos de cauda são calculados conforme necessário.

Vamos criar um Stream de 10 números inteiros positivos e calcular a soma dos números pares:

 Stream < Integer > intStream = Stream.iterate(0, i - > i + 1) .take(10); assertEquals(10, intStream.size()); long evenSum = intStream.filter(i - > i % 2 == 0) .sum() .longValue(); assertEquals(20, evenSum); 

Diferente da API de Stream do Java 8, o Stream in Vavr é uma estrutura de dados para armazenar uma sequência de elementos.

Portanto, ele possui métodos como get() , append() , insert() e outros para manipular seus elementos. drop() , distinct() e alguns outros métodos discutidos anteriormente também estão disponíveis.

Por fim, vamos demonstrar rapidamente tabulate() no Stream . Este método retorna um Stream comprimento n contendo elementos resultantes da aplicação da função:

 Stream < Integer > s1 = Stream.tabulate(5, (i) - > i + 1); assertEquals(s1.get(2).intValue(), 3); 

Também podemos usar zip() para criar um Stream partir de Tuple2<Integer, Integer> , que contém elementos formados pela combinação de dois Stream :

 Stream < Integer > s = Stream.of(2, 1, 3, 4); Stream < Tuple2 < Integer, Integer >> s2 = s.zip(List.of(7, 8, 9)); Tuple2 < Integer, Integer > t1 = s2.get(0); assertEquals(t1._1().intValue(), 2); assertEquals(t1._2().intValue(), 7); 

4.4 Matriz


Array é uma sequência indexada imutável que fornece acesso aleatório eficiente. É baseado em uma matriz de objetos Java. Em essência, este é um wrapper Traversable para uma matriz de objetos do tipo T

Você pode criar uma instância de Array usando o método estático of() . Além disso, você pode criar um intervalo de elementos usando os métodos estáticos range() e rangeBy() . O método rangeBy() possui um terceiro parâmetro, que permite determinar a etapa.

Os métodos range() e rangeBy() criarão elementos, começando apenas do valor inicial para o valor final menos um. Se precisarmos incluir o valor final, podemos usar rangeClosed() ou rangeClosedBy() :

 Array < Integer > rArray = Array.range(1, 5); assertFalse(rArray.contains(5)); Array < Integer > rArray2 = Array.rangeClosed(1, 5); assertTrue(rArray2.contains(5)); Array < Integer > rArray3 = Array.rangeClosedBy(1, 6, 2); assertEquals(list3.size(), 3); 

Vamos trabalhar com os elementos usando o índice:

 Array < Integer > intArray = Array.of(1, 2, 3); Array < Integer > newArray = intArray.removeAt(1); assertEquals(3, intArray.size()); assertEquals(2, newArray.size()); assertEquals(3, newArray.get(1).intValue()); Array < Integer > array2 = intArray.replace(1, 5); assertEquals(s1.get(0).intValue(), 5); 

4.5 Vetor


Vector é um cruzamento entre Array e List , fornecendo outra sequência indexada de elementos, permitindo acesso aleatório e modificação em tempo constante:

 Vector < Integer > intVector = Vector.range(1, 5); Vector < Integer > newVector = intVector.replace(2, 6); assertEquals(4, intVector.size()); assertEquals(4, newVector.size()); assertEquals(2, intVector.get(1).intValue()); assertEquals(6, newVector.get(1).intValue()); 

4.6 Charseq


CharSeq é um objeto de coleção para representar uma sequência de caracteres primitivos. Em essência, este é um wrapper para String com a adição de operações de coleção.

Para criar o CharSeq você deve fazer o seguinte.

 CharSeq chars = CharSeq.of("vavr"); CharSeq newChars = chars.replace('v', 'V'); assertEquals(4, chars.size()); assertEquals(4, newChars.size()); assertEquals('v', chars.charAt(0)); assertEquals('V', newChars.charAt(0)); assertEquals("Vavr", newChars.mkString()); 

5. Defina


Esta seção discute as várias implementações do Set na biblioteca de coleções. Um recurso exclusivo da estrutura de dados do Set é que ele não permite valores duplicados.

Existem várias implementações do Set . O principal é o HashSet . TreeSet não permite elementos duplicados e pode ser classificado. LinkedHashSet preserva a ordem de inserção dos elementos.

Vamos dar uma olhada mais de perto nessas implementações, uma após a outra.

5.1 Hashset


HashSet possui métodos estáticos de fábrica para criar novas instâncias. Alguns dos quais estudamos anteriormente neste artigo, por exemplo, of() , ofAll() e variações nos métodos range() .

A diferença entre os dois conjuntos pode ser obtida usando o método diff() . Além disso, os métodos union() e intersect() retornam a união e a interseção de dois conjuntos :

 HashSet < Integer > set0 = HashSet.rangeClosed(1, 5); HashSet < Integer > set0 = HashSet.rangeClosed(1, 5); assertEquals(set0.union(set1), HashSet.rangeClosed(1, 6)); assertEquals(set0.diff(set1), HashSet.rangeClosed(1, 2)); assertEquals(set0.intersect(set1), HashSet.rangeClosed(3, 5)); 

Também podemos executar operações básicas, como adicionar e remover elementos:

 HashSet < String > set = HashSet.of("Red", "Green", "Blue"); HashSet < String > newSet = set.add("Yellow"); assertEquals(3, set.size()); assertEquals(4, newSet.size()); assertTrue(newSet.contains("Yellow")); 

A implementação do HashSet é baseada no HAMT (Hash Array Arrie) , que apresenta desempenho superior ao HashTable usual e sua estrutura o torna adequado para suportar coleções persistentes.

5.2 Treeset


TreeSet é uma implementação da interface SortedSet . Ele armazena um conjunto de elementos classificados e é implementado usando árvores de pesquisa binária. Todas as suas operações são executadas durante o tempo O (log n) .

Por padrão, os elementos TreeSet são classificados em sua ordem natural.
Vamos criar um SortedSet usando uma ordem de classificação natural:

 SortedSet < String > set = TreeSet.of("Red", "Green", "Blue"); assertEquals("Blue", set.head()); SortedSet < Integer > intSet = TreeSet.of(1, 2, 3); assertEquals(2, intSet.average().get().intValue()); 

Para organizar os itens de maneira personalizada, passe uma instância do Comparator ao criar o TreeSet . Você também pode criar uma sequência a partir de um conjunto de elementos:

 SortedSet < String > reversedSet = TreeSet.of(Comparator.reverseOrder(), "Green", "Red", "Blue"); assertEquals("Red", reversedSet.head()); String str = reversedSet.mkString(" and "); assertEquals("Red and Green and Blue", str); 

5.3 Bitset


As coleções do Vavr também possuem uma implementação imutável do BitSet . A interface BitSet estende a interface SortedSet . BitSet pode ser criado usando métodos estáticos no BitSet.Builder .
Como em outras implementações da estrutura de dados do Set , o BitSet não permite adicionar registros duplicados a um conjunto.

Ele herda métodos de manipulação da interface Traversable . Observe que é diferente do java.util.BitSet da biblioteca Java padrão. BitSet dados do BitSet não podem conter valores de String .

Considere criar uma instância do BitSet usando o método of() factory:

 BitSet < Integer > bitSet = BitSet.of(1, 2, 3, 4, 5, 6, 7, 8); BitSet < Integer > bitSet1 = bitSet.takeUntil(i - > i > 4); assertEquals(list3.size(), 4); 

Para selecionar os quatro primeiros elementos BitSet usamos o comando takeUntil() . A operação retornou uma nova instância. Observe que o método takeUntil() é definido na interface Traversable , que é a interface pai do BitSet .

Outros métodos e operações descritos acima definidos na interface Traversable também se aplicam ao BitSet .

6. Mapa


Map é uma estrutura de dados de valor-chave. Map no Vavr é imutável e possui implementações para HashMap , LinkedHashMap e LinkedHashMap .

Normalmente, contratos de mapas não permitem chaves duplicadas, enquanto valores duplicados mapeados para chaves diferentes podem ser.

6.1 Hashmap


HashMap é uma implementação da interface imutável do Map . Ele armazena pares de valores-chave usando um hash de chaves.

Map no Vavr usa Tuple2 para representar pares de valores-chave em vez do tipo de Entry tradicional:

 Map < Integer, List < Integer >> map = List.rangeClosed(0, 10) .groupBy(i - > i % 2); assertEquals(2, map.size()); assertEquals(6, map.get(0).get().size()); assertEquals(5, map.get(1).get().size()); 

Como o HashSet , a implementação do HashMap baseada no HAMT (Hash Array Array) , que leva a um tempo constante para quase todas as operações.
Os elementos do mapa podem ser filtrados por chave usando o método filterKeys() ou por valor usando o método filterValues() . Ambos os métodos consideram o Predicate como argumento:

 Map < String, String > map1 = HashMap.of("key1", "val1", "key2", "val2", "key3", "val3"); Map < String, String > fMap = map1.filterKeys(k - > k.contains("1") || k.contains("2")); assertFalse(fMap.containsKey("key3")); Map < String, String > map1 = map1.filterValues(v - > v.contains("3")); assertEquals(list3.size(), 1); assertTrue(fMap2.containsValue("val3")); 

Você também pode transformar elementos do map() usando o método map() . Por exemplo, vamos converter map1 em Map<String, Integer> :

 Map < String, Integer > map2 = map1.map( (k, v) - > Tuple.of(k, Integer.valueOf(v.charAt(v.length() - 1) + ""))); assertEquals(map2.get("key1").get().intValue(), 1); 

6.2 Treemap


O SortedMap é uma implementação da interface SortedMap . Como no TreeSet , uma instância personalizada do Comparator usada para personalizar a classificação dos elementos do TreeMap .
SortedMap demonstrar a criação do SortedMap :

 SortedMap < Integer, String > map = TreeMap.of(3, "Three", 2, "Two", 4, "Four", 1, "One"); assertEquals(1, map.keySet().toJavaArray()[0]); assertEquals("Four", map.get(4).get()); 

Por padrão, as entradas do TreeMap são classificadas em ordem de chave natural. No entanto, você pode especificar o Comparator a ser usado para classificação:

 TreeMap < Integer, String > treeMap2 = TreeMap.of(Comparator.reverseOrder(), 3, "three", 6, "six", 1, "one"); assertEquals(treeMap2.keySet().mkString(), "631"); 

Como no caso do TreeSet , a implementação do TreeSet também é criada usando a árvore, portanto, suas operações têm tempo O (log n) . O método map.get(key) retorna Option , que contém o valor da chave de mapa especificada.

7. compatibilidade com Java


A API Vavr Collection é totalmente compatível com o Java Collection Framework. Vamos ver como isso é feito na prática.

7.1 Converter de Java para Vavr


Cada implementação de coleção no Vavr possui um método ofAll() fábrica estática que aceita java.util.Iterable . Isso permite criar uma coleção Vavr a partir de uma coleção Java . Da mesma forma, outro método de fábrica ofAll() aceita diretamente o Java Stream .

Para converter uma List Java em uma List imutável:

 java.util.List < Integer > javaList = java.util.Arrays.asList(1, 2, 3, 4); List < Integer > vavrList = List.ofAll(javaList); java.util.stream.Stream < Integer > javaStream = javaList.stream(); Set < Integer > vavrSet = HashSet.ofAll(javaStream); 

Outra função útil é collector() , que pode ser usada em conjunto com Stream.collect() para obter a coleção Vavr:

 List < Integer > vavrList = IntStream.range(1, 10) .boxed() .filter(i - > i % 2 == 0) .collect(List.collector()); assertEquals(4, vavrList.size()); assertEquals(2, vavrList.head().intValue()); 

7.2 Converter de Vavr para Java


A interface Value possui muitos métodos para converter de um tipo Vavr para um tipo Java . Esses métodos têm o formato toJavaXXX() .

Considere alguns exemplos:

 Integer[] array = List.of(1, 2, 3) .toJavaArray(Integer.class); assertEquals(3, array.length); java.util.Map < String, Integer > map = List.of("1", "2", "3") .toJavaMap(i - > Tuple.of(i, Integer.valueOf(i))); assertEquals(2, map.get("2").intValue()); 

Também podemos usar o Java 8 Collectors para coletar itens das coleções do Vavr:

 java.util.Set < Integer > javaSet = List.of(1, 2, 3) .collect(Collectors.toSet()); assertEquals(3, javaSet.size()); assertEquals(1, javaSet.toArray()[0]); 

7.3 Exibições de coleções Java


Além disso, a biblioteca fornece as chamadas visualizações de coleção que funcionam melhor quando convertidas em coleções Java. Os métodos de transformação na seção anterior repetem (repetem) todos os elementos para criar uma coleção Java.

As visualizações, por outro lado, implementam interfaces Java padrão e delegam chamadas de método à coleção base do Vavr.

No momento da redação deste artigo, apenas a visualização em List é suportada. Cada coleção seqüencial possui dois métodos: um para criar uma representação imutável e outro para mutável.

Chamar métodos para alterar em uma exibição imutável UnsupportedOperationException .

Vejamos um exemplo:

 @Test(expected = UnsupportedOperationException.class) public void givenVavrList_whenViewConverted_thenException() { java.util.List < Integer > javaList = List.of(1, 2, 3) .asJava(); assertEquals(3, javaList.get(2).intValue()); javaList.add(4); } 

Para criar uma visão imutável:

 java.util.List < Integer > javaList = List.of(1, 2, 3) .asJavaMutable(); javaList.add(4); assertEquals(4, javaList.get(3).intValue()); 

8. Conclusões


Neste tutorial, aprendemos sobre as várias estruturas de dados funcionais fornecidas pela API Vavr Collections. Também existem métodos de API úteis e produtivos que podem ser encontrados no documento Java e no Guia do Usuário do Vavr Collections.

Finalmente, é importante observar que a biblioteca também define Try , Option , Either e Future , que estendem a interface Value e, como resultado, implementam a interface Java Iterable . Isso significa que, em algumas situações, eles podem se comportar como coleções.

O código fonte completo de todos os exemplos deste artigo pode ser encontrado no Github .

Materiais adicionais:
habr.com/en/post/421839
www.baeldung.com/vavr

Traduzido por @middle_java

Source: https://habr.com/ru/post/pt474402/


All Articles