Guia da API de coleções no Vavr
1. Visão geral
A biblioteca Vavr, anteriormente conhecida como Javaslang, é uma biblioteca funcional para Java. Neste artigo, exploramos sua poderosa API de coleções.
Para obter mais informações sobre esta biblioteca, leiathis article.
2. Coleções Persistentes
Uma coleção persistente quando modificada produz uma nova versão da coleção, preservando a versão atual.
Manter várias versões da mesma coleção pode levar a CPU ineficiente e uso de memória. No entanto, a biblioteca de coleções do Vavr supera isso compartilhando a estrutura de dados entre diferentes versões de uma coleção.
Isso é fundamentalmente diferente dounmodifiableCollection() do Java da classe de utilitárioCollections, que apenas fornece um invólucro em torno de uma coleção subjacente.
Tentar modificar tal coleção resulta emUnsupportedOperationException em vez de uma nova versão sendo criada. Além disso, a coleção subjacente ainda é mutável por meio de sua referência direta.
3. Traversable
Traversable é o tipo base de todas as coleções Vavr - esta interface define métodos que são compartilhados entre todas as estruturas de dados.
Ele fornece alguns métodos padrão úteis, comosize(),get(),filter(),isEmpty()e outros que são herdados por subinterfaces.
Vamos explorar mais a biblioteca de coleções.
4. Seq
Vamos começar com sequências.
A interfaceSeq representa estruturas de dados sequenciais. É a interface pai paraList,Stream,Queue,Array,Vector eCharSeq. Todas essas estruturas de dados têm suas próprias propriedades exclusivas que exploraremos a seguir.
4.1. List
AList é uma sequência avidamente avaliada de elementos que estendem a interfaceLinearSeq.
Lists persistentes são formados recursivamente a partir de uma cabeça e uma cauda:
-
Cabeça - o primeiro elemento
-
Tail - uma lista contendo os elementos restantes (essa lista também é formada a partir de uma cabeça e uma cauda)
Existem métodos de fábrica estáticos na APIList que podem ser usados para criar umList. Podemos usar o métodoof() estático para criar uma instância deList a partir de um ou mais objetos.
Também podemos usar oempty() estático para criar umList vazio eofAll() para criar umList a partir de um tipoIterable:
List list = List.of(
"Java", "PHP", "Jquery", "JavaScript", "JShell", "JAVA");
Vamos dar uma olhada em alguns exemplos de como manipular listas.
Podemos usardrop() e suas variantes para remover os primeiros elementosN:
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) removen número de elementos da lista começando do primeiro elemento, enquantodropRight() faz o mesmo começando do último elemento na lista.
dropUntil() continua removendo elementos da lista até que o predicado seja avaliado como verdadeiro, enquantodropWhile() continua removendo elementos enquanto o predicado é verdadeiro.
Também hádropRightWhile()edropRightUntil() que começa a remover elementos da direita.
Em seguida,take(int n) é usado para obter elementos de uma lista. Leva o númeron de elementos da lista e então para. Também há umtakeRight(int n) que começa a pegar elementos do final da lista:
List list5 = list.take(1);
assertEquals(list5.single(), "Java");
List list6 = list.takeRight(1);
assertEquals(list6.single(), "JAVA");
List list7 = list.takeUntil(s -> s.length() > 6);
assertEquals(list7.size(), 3);
Finalmente,takeUntil() continua pegando elementos da lista até que o predicado seja verdadeiro. Há uma variantetakeWhile() que também aceita um argumento de predicado.
Além disso, existem outros métodos úteis na API, por exemplo, na verdade odistinct() que retorna uma lista de elementos não duplicados, bem como odistinctBy() que aceitaComparator para determinar a igualdade.
Curiosamente, há também ointersperse() que insere um elemento entre cada elemento de uma lista. Pode ser muito útil para operaçõesString:
List list8 = list
.distinctBy((s1, s2) -> s1.startsWith(s2.charAt(0) + "") ? 0 : 1);
assertEquals(list8.size(), 2);
String words = List.of("Boys", "Girls")
.intersperse("and")
.reduce((s1, s2) -> s1.concat( " " + s2 ))
.trim();
assertEquals(words, "Boys and Girls");
Deseja dividir uma lista em categorias? Bem, há uma API para isso também:
Iterator> iterator = list.grouped(2);
assertEquals(iterator.head().size(), 2);
Map> 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);
Ogroup(int n) divide aList em grupos den elementos cada. OgroupdBy() aceita umFunction que contém a lógica para dividir a lista e retorna umMap com duas entradas -trueefalse.
A chavetrue mapeia paraList de elementos que satisfazem a condição especificada emFunction; a chavefalse mapeia paraList de elementos que não satisfazem.
Como esperado, ao alterar aList, oList original não é realmente modificado. Em vez disso, uma nova versão deList sempre é retornada.
Também podemos interagir com aList usando a semântica de pilha - recuperação de elementos do último a entrar, primeiro a sair (LIFO). Até este ponto, existem métodos API para manipular uma pilha, comopeek(),pop()epush():
List intList = List.empty();
List 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çãopushAll() é usada para inserir um intervalo de inteiros na pilha, enquantopeek() é usada para obter o topo da pilha. Também existe opeekOption() que pode envolver o resultado em um objetoOption.
Existem outros métodos interessantes e realmente úteis na interfaceList que estão perfeitamente documentados emJava docs.
4.2. Queue
UmQueue imutável armazena elementos que permitem uma recuperação first-in-first-out (FIFO).
UmQueue internamente consiste em duas listas vinculadas, umaList frontal e umaList traseira. OList dianteiro contém os elementos que foram retirados da fila e oList traseiro contém os elementos que foram retirados da fila.
Isso permite que as operaçõesenqueueedequeue sejam realizadas em O (1). Quando oList dianteiro fica sem elementos,List’s dianteiro e traseiro são trocados e oList traseiro é invertido.
Vamos criar uma fila:
Queue queue = Queue.of(1, 2);
Queue secondQueue = queue.enqueueAll(List.of(4,5));
assertEquals(3, queue.size());
assertEquals(5, secondQueue.size());
Tuple2> result = secondQueue.dequeue();
assertEquals(Integer.valueOf(1), result._1);
Queue tailQueue = result._2;
assertFalse(tailQueue.contains(secondQueue.get(0)));
A funçãodequeue remove o elemento principal deQueuee retorna umTuple2<T, Q>. A tupla contém o elemento principal que foi removido como a primeira entrada e os elementos restantes deQueue como a segunda entrada.
Podemos usarcombination(n) para obter todas as combinações possíveis deN de elementos emQueue:
Queue> queue1 = queue.combinations(2);
assertEquals(queue1.get(2).toCharSeq(), CharSeq.of("23"));
Novamente, podemos ver que oQueue original não é modificado durante o enfileiramento / desenfileiramento dos elementos.
4.3. Stream
UmStream é uma implementação de uma lista vinculada preguiçosa e é bem diferente dejava.util.stream. Ao contrário dejava.util.stream, o VavrStream armazena dados e avalia preguiçosamente os próximos elementos.
Digamos que temos umStream de inteiros:
Stream s = Stream.of(2, 1, 3, 4);
Imprimir o resultado des.toString() no console mostrará apenasStream(2, ?). Isso significa que apenas a cabeça deStream foi avaliada, enquanto a cauda não foi avaliada.
Invocars.get(3) e subsequentemente exibir o resultado des.tail() retornaStream(1, 3, 4, ?). Ao contrário, sem invocars.get(3) primeiro–, o que faz com queStream avalie o último elemento - o resultado des.tail() será apenasStream(1, ?). Isso significa que apenas o primeiro elemento da cauda foi avaliado.
Esse comportamento pode melhorar o desempenho e possibilita usarStream para representar sequências que são (teoricamente) infinitamente longas.
VavrStream é imutável e pode serEmpty ouCons. UmCons consiste em um elemento principal e uma cauda preguiçosa computadaStream. Ao contrário de aList, para aStream, apenas o elemento principal é mantido na memória. Os elementos da cauda são calculados sob demanda.
Vamos criar umStream de 10 inteiros positivos e calcular a soma dos números pares:
Stream 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);
Ao contrário da API Java 8Stream, aStream de Vavr é uma estrutura de dados para armazenar uma sequência de elementos.
Assim, possui métodos comoget(),append(),insert()e outros para manipulação de seus elementos. Odrop(),distinct() e alguns outros métodos considerados anteriormente também estão disponíveis.
Finalmente, vamos demonstrar rapidamente otabulate() em aStream. Este método retorna umStream de comprimenton, que contém elementos que são o resultado da aplicação de uma função:
Stream s1 = Stream.tabulate(5, (i)-> i + 1);
assertEquals(s1.get(2).intValue(), 3);
Também podemos usarzip() para gerar umStream deTuple2<Integer, Integer>, que contém elementos que são formados pela combinação de doisStreams:
Stream s = Stream.of(2,1,3,4);
Stream> s2 = s.zip(List.of(7,8,9));
Tuple2 t1 = s2.get(0);
assertEquals(t1._1().intValue(), 2);
assertEquals(t1._2().intValue(), 7);
4.4. Array
UmArray é uma sequência imutável e indexada que permite acesso aleatório eficiente. É apoiado por Javaarray de objetos. Essencialmente, é um wrapperTraversable para uma matriz de objetos do tipoT.
Podemos instanciar umArray usando o método estáticoof(). Também podemos gerar elementos de intervalo usando os métodos estáticosrange()erangeBy(). OrangeBy() possui um terceiro parâmetro que nos permite definir a etapa.
Os métodosrange()erangeBy() gerarão apenas elementos começando do valor inicial ao valor final menos um. Se precisarmos incluir o valor final, podemos usarrangeClosed() ourangeClosedBy():
Array rArray = Array.range(1, 5);
assertFalse(rArray.contains(5));
Array rArray2 = Array.rangeClosed(1, 5);
assertTrue(rArray2.contains(5));
Array rArray3 = Array.rangeClosedBy(1,6,2);
assertEquals(rArray3.size(), 3);
Vamos manipular os elementos por índice:
Array intArray = Array.of(1, 2, 3);
Array newArray = intArray.removeAt(1);
assertEquals(3, intArray.size());
assertEquals(2, newArray.size());
assertEquals(3, newArray.get(1).intValue());
Array array2 = intArray.replace(1, 5);
assertEquals(array2.get(0).intValue(), 5);
4.5. Vector
UmVector é um tipo deArray eList que fornece outra sequência indexada de elementos que permite acesso aleatório e modificação em tempo constante:
Vector intVector = Vector.range(1, 5);
Vector 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 expressar uma sequência de caracteres primitivos. É essencialmente um wrapperString com a adição de operações de coleta.
Para criar umCharSeq:
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. Set
Nesta seção, elaboramos várias implementações deSet na biblioteca de coleções. A característica única da estrutura de dadosSet é que ela não permite valores duplicados.
Existem, no entanto, diferentes implementações deSet - oHashSet sendo o básico. OTreeSet não permite elementos duplicados e pode ser classificado. OLinkedHashSet mantém a ordem de inserção de seus elementos.
Vamos dar uma olhada nessas implementações, uma por uma.
5.1. HashSet
HashSet tem métodos de fábrica estáticos para criar novas instâncias - alguns dos quais exploramos anteriormente neste artigo - comoof(),ofAll()e variações dos métodosrange().
Podemos obter a diferença entre dois conjuntos usando o métododiff(). Além disso, os métodosunion()eintersect() retornam o conjunto de união e o conjunto de interseção dos dois conjuntos:
HashSet set0 = HashSet.rangeClosed(1,5);
HashSet set1 = HashSet.rangeClosed(3, 6);
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 set = HashSet.of("Red", "Green", "Blue");
HashSet newSet = set.add("Yellow");
assertEquals(3, set.size());
assertEquals(4, newSet.size());
assertTrue(newSet.contains("Yellow"));
A implementação deHashSet é apoiada por umHash array mapped trie (HAMT), que possui um desempenho superior quando comparado a umHashTable comum e sua estrutura o torna adequado para apoiar uma coleção persistente.
5.2. TreeSet
UmTreeSet imutável é uma implementação da interfaceSortedSet. Ele armazena umSet de elementos classificados e é implementado usando árvores de pesquisa binárias. Todas as suas operações são executadas no tempo O (log n).
Por padrão, os elementos de aTreeSet são classificados em sua ordem natural.
Vamos criar umSortedSet usando a ordem de classificação natural:
SortedSet set = TreeSet.of("Red", "Green", "Blue");
assertEquals("Blue", set.head());
SortedSet intSet = TreeSet.of(1,2,3);
assertEquals(2, intSet.average().get().intValue());
Para ordenar os elementos de maneira personalizada, passe uma instânciaComparator enquanto cria umTreeSet.. Também podemos gerar uma string a partir dos elementos definidos:
SortedSet 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 Vavr também contêm uma implementaçãoBitSet imutável. A interfaceBitSet estende a interfaceSortedSet. BitSet pode ser instanciado usando métodos estáticos emBitSet.Builder.
Como outras implementações da estrutura de dadosSet,BitSet não permite que entradas duplicadas sejam adicionadas ao conjunto.
Ele herda métodos de manipulação da interfaceTraversable. Observe que é diferente dejava.util.BitSet na biblioteca Java padrão. Os dados deBitSet não podem conter os valores deString.
Vamos ver como criar uma instânciaBitSet usando o método de fábricaof():
BitSet bitSet = BitSet.of(1,2,3,4,5,6,7,8);
BitSet bitSet1 = bitSet.takeUntil(i -> i > 4);
assertEquals(bitSet1.size(), 4);
UsamostakeUntil() para selecionar os primeiros quatro elementos deBitSet.. A operação retornou uma nova instância. Observe quetakeUntil() é definido na interfaceTraversable, que é uma interface pai deBitSet.
Outros métodos e operações demonstrados acima, que são definidos na interfaceTraversable, também são aplicáveis aBitSet.
6. Map
Um mapa é uma estrutura de dados de valor-chave. OMap de Vavr é imutável e tem implementações paraHashMap,TreeMap eLinkedHashMap.
Geralmente, os contratos de mapa não permitem chaves duplicadas - embora possa haver valores duplicados mapeados para chaves diferentes.
6.1. HashMap
UmHashMap é uma implementação de uma interfaceMap imutável. Ele armazena pares de valores-chave usando o código de hash das chaves.
OMap de Vavr usaTuple2 para representar pares de valores-chave em vez de um tipoEntry tradicional:
Map> 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());
Semelhante aHashSet, uma implementaçãoHashMap é apoiada por um hash array mapped trie (HAMT) resultando em tempo constante para quase todas as operações.
Podemos filtrar as entradas do mapa por chaves, usando o métodofilterKeys() ou por valores, usando o métodofilterValues(). Ambos os métodos aceitamPredicate como argumento:
Map map1
= HashMap.of("key1", "val1", "key2", "val2", "key3", "val3");
Map fMap
= map1.filterKeys(k -> k.contains("1") || k.contains("2"));
assertFalse(fMap.containsKey("key3"));
Map fMap2
= map1.filterValues(v -> v.contains("3"));
assertEquals(fMap2.size(), 1);
assertTrue(fMap2.containsValue("val3"));
Também podemos transformar as entradas do mapa usando o métodomap(). Vamos, por exemplo, transformarmap1 em aMap<String, Integer>:
Map 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
UmTreeMap imutável é uma implementação da interfaceSortedMap. Semelhante aTreeSet, uma instânciaComparator é usada para personalizar elementos de classificação deTreeMap.
Vamos demonstrar a criação de umSortedMap:
SortedMap 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 deTreeMap são classificadas na ordem natural das chaves. Podemos, no entanto, especificar umComparator que será usado para classificação:
TreeMap treeMap2 =
TreeMap.of(Comparator.reverseOrder(), 3,"three", 6, "six", 1, "one");
assertEquals(treeMap2.keySet().mkString(), "631");
Como acontece comTreeSet, uma implementaçãoTreeMap também é modelada usando uma árvore, portanto, suas operações são de tempo O (log n). Omap.get(key) retorna umOption que envolve um valor na chave especificada no mapa.
7. Interoperabilidade com Java
A API de coleta é totalmente interoperável com a estrutura de coleta do Java. Vamos ver como isso é feito na prática.
7.1. Conversão de Java para Vavr
Cada implementação de coleção no Vavr tem um método de fábrica estáticoofAll() que levajava.util.Iterable. Isso nos permite criar uma coleção Vavr a partir de uma coleção Java. Da mesma forma, outro método de fábricaofAll() pega um JavaStream diretamente.
Para converter umList Java em umList imutável:
java.util.List javaList = java.util.Arrays.asList(1, 2, 3, 4);
List vavrList = List.ofAll(javaList);
java.util.stream.Stream javaStream = javaList.stream();
Set vavrSet = HashSet.ofAll(javaStream);
Outra função útil é ocollector() que pode ser usado em conjunto comStream.collect() para obter uma coleção Vavr:
List 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. Conversão de Vavr para Java
A interfaceValue possui muitos métodos para converter um tipo Vavr em um tipo Java. Esses métodos são do formatotoJavaXXX().
Vejamos alguns exemplos:
Integer[] array = List.of(1, 2, 3)
.toJavaArray(Integer.class);
assertEquals(3, array.length);
java.util.Map map = List.of("1", "2", "3")
.toJavaMap(i -> Tuple.of(i, Integer.valueOf(i)));
assertEquals(2, map.get("2").intValue());
Também podemos usar Java 8Collectors para coletar elementos de coleções Vavr:
java.util.Set javaSet = List.of(1, 2, 3)
.collect(Collectors.toSet());
assertEquals(3, javaSet.size());
assertEquals(1, javaSet.toArray()[0]);
7.3. Exibições de coleção Java
Como alternativa, a biblioteca fornece as chamadas visualizações de coleção com melhor desempenho ao converter para coleções Java. Os métodos de conversão da seção anterior percorrem todos os elementos para construir uma coleção Java.
As visualizações, por outro lado, implementam interfaces Java padrão e delegam chamadas de método à coleção Vavr subjacente.
No momento em que este livro foi escrito, apenas a exibiçãoList é suportada. Cada coleção seqüencial possui dois métodos, um para criar uma visão imutável e outro para uma visão mutável.
Chamar métodos modificadores em visualizações imutáveis resulta emUnsupportedOperationException.
Vejamos um exemplo:
@Test(expected = UnsupportedOperationException.class)
public void givenVavrList_whenViewConverted_thenException() {
java.util.List 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 javaList = List.of(1, 2, 3)
.asJavaMutable();
javaList.add(4);
assertEquals(4, javaList.get(3).intValue());
8. Conclusão
Neste tutorial, aprendemos sobre várias estruturas de dados funcionais fornecidas pela API de coleta do Vavr. Existem métodos de API mais úteis e produtivos que podem ser encontrados nas coleçõesJavaDoceuser guide do Vavr.
Finalmente, é importante notar que a biblioteca também defineTry,Option,Either eFuture que estendem a interfaceValue e, como consequência, implementam Java's Interface deIterable. Isso implica que eles podem se comportar como uma coleção em algumas situações.
O código-fonte completo para todos os exemplos neste artigo pode ser encontradoover on Github.