Vavr Collections API Guide

VAVR (anteriormente conocido como Javaslang) es una biblioteca funcional sin fines de lucro para Java 8+. Le permite escribir código funcional similar a Scala en Java y sirve para reducir la cantidad de código y mejorar su calidad. Sitio de la biblioteca .

Debajo del corte hay una traducción de un artículo que sistematiza información sobre la API de Vavr Collections .

Traducido por @middle_java

Artículo original modificado por última vez: 15 de agosto de 2019

1. Descripción general


La biblioteca Vavr, anteriormente conocida como Javaslang, es una biblioteca funcional para Java. En este artículo, exploramos su poderosa colección API.

Consulte este artículo para obtener más información sobre esta biblioteca.

2. Colecciones persistentes


Una colección persistente, cuando se modifica, crea una nueva versión de la colección sin cambiar la versión actual.

La compatibilidad con múltiples versiones de la misma colección puede conducir a un uso ineficiente de la CPU y la memoria. Sin embargo, la biblioteca de la colección Vavr supera esto al compartir la estructura de datos entre diferentes versiones de la colección.

Esto es fundamentalmente diferente de unmodifiableCollection() de las Collections clase de utilidad de Java, que simplemente proporciona un contenedor para la colección base.

Intentar modificar dicha colección UnsupportedOperationException lugar de crear una nueva versión. Además, la colección base sigue siendo mutable a través de un enlace directo a ella.

3. Transitable


Traversable es el tipo base de todas las colecciones Vavr. Esta interfaz define métodos comunes a todas las estructuras de datos.

Proporciona algunos métodos predeterminados útiles, como size() , get() , filter() , isEmpty() y otros que son heredados por las isEmpty() .

Exploramos más la colección de la biblioteca.

4. Seq


Comencemos con las secuencias.

La interfaz Seq es una estructura de datos secuencial. Esta es la interfaz principal para List , Stream , Queue , Array , Vector y CharSeq . Todas estas estructuras de datos tienen sus propias propiedades únicas, que discutiremos a continuación.

4.1. Lista


List es una operación calculada energéticamente (evaluada con entusiasmo, se realiza tan pronto como se conocen los valores de sus operandos) una secuencia de elementos que extiende la interfaz LinearSeq .

Las List persistentes List construyen recursivamente usando la cabeza y la cola :

  • La cabeza es el primer elemento.
  • Cola : una lista que contiene los elementos restantes (esta lista también se forma a partir de la cabeza y la cola)

La API de List contiene métodos de fábrica estáticos que puede usar para crear una List . Puede usar el método static of() para crear una instancia de List partir de uno o más objetos.

También puede usar el método estático empty() para crear una List vacía y el método ofAll() para crear una List de tipo Iterable :

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

Veamos algunos ejemplos de manipulación de listas.

Podemos usar el método drop() y sus variantes para eliminar los primeros 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) elimina n elementos de la lista, comenzando por el primer elemento, mientras que dropRight() hace lo mismo, comenzando por el último elemento de la lista.

dropUntil() elimina los elementos de la lista hasta que el predicado sea true , mientras que dropWhile() elimina los elementos hasta que el predicado sea true .

También hay dropRightWhile() y dropRightUntil() que eliminan elementos que comienzan desde la derecha.

A continuación, take(int n) usa para recuperar elementos de la lista. Toma n elementos de la lista y luego se detiene. También hay takeRight(int n) , que toma elementos del final de la 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); 

Finalmente, takeUntil() toma elementos de la lista hasta que el predicado se vuelva true . Hay una variante de takeWhile() que también toma un argumento predicado.

Además, la API tiene otros métodos útiles, por ejemplo, distinct() , que devuelve una lista de elementos con duplicados eliminados, y distinctBy() , que acepta Comparator para determinar la igualdad.

Es muy interesante que también haya intersperse() , que inserta un elemento entre cada elemento de la lista. Esto puede ser muy conveniente para operaciones con 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"); 

¿Quieres dividir la lista en categorías? Y para esto hay una 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); 

El método group(int n) divide List en grupos de n elementos cada uno. El método groupdBy() acepta una Function que contiene la lógica de división de la lista y devuelve un Map con dos elementos: true y false .

La clave true se asigna a la List elementos que satisfacen la condición especificada en Function . La clave false asigna a la List elementos que no satisfacen esta condición.

Como se esperaba, al cambiar la List , la List original no cambia realmente. En cambio, la nueva versión de List siempre se devuelve.

También podemos interactuar con List usando la semántica de la pila, extrayendo elementos de acuerdo con el principio de "último en entrar, primero en salir" (LIFO). En este sentido, existen métodos API como peek() , pop() y push() para manipular la pila:

 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)); 

La función pushAll() se usa para insertar un rango de enteros en la pila, y la función peek() se usa para recuperar el elemento head de la pila. También hay un método peekOption() que puede ajustar el resultado en un objeto Option .

Hay otros métodos interesantes y realmente útiles en la interfaz de la List que están documentados a fondo en los documentos de Java .

4.2. Cola


La Queue inmutable almacena elementos, lo que le permite recuperarlos de acuerdo con el principio FIFO (primero en entrar, primero en salir).

Queue interior consta de dos listas vinculadas: la List frontal y la List posterior. La List frontal contiene elementos que se eliminan de la cola y la List posterior contiene los elementos en cola.

Esto le permite colocar las operaciones de cola y eliminación de la cola en la complejidad O (1) . Cuando la List finaliza en la List frontal cuando se elimina de la cola, la List posterior List invierte y se convierte en la nueva List frontal.

Vamos a crear una cola:

 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))); 

La función dequeue() elimina el elemento head de Queue y devuelve Tuple2<T, Q> . El primer elemento de la tupla es el elemento principal eliminado de la cola, el segundo elemento de la tupla son los elementos restantes de la Queue .

Podemos usar la combination(n) para obtener todas las N combinaciones posibles de elementos en la Queue :

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

Una vez más, la Queue original no cambia al agregar / eliminar elementos de la cola.

4.3. Stream


Stream es una implementación de una lista perezosamente vinculada que es significativamente diferente de java.util.stream . A diferencia de java.util.stream , Stream Vavr almacena datos y calcula perezosamente elementos posteriores.
Digamos que tenemos enteros Stream :

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

Al imprimir el resultado de s.toString() en la consola, solo se mostrará Stream (2 ,?) . Esto significa que solo se calculó el elemento principal de Stream , mientras que los elementos de cola no.

Llamar a s.get(3) y luego mostrar el resultado de s.tail() devuelve Stream (1, 3, 4 ,?) . Por el contrario, si no llama s.get(3) a s.get(3) , lo que hará que Stream calcule el último elemento, solo Stream (1 ,?) será el resultado de s.tail() ) . Esto significa que solo se calculó el primer elemento de cola.

Este comportamiento puede mejorar el rendimiento y permite que Stream se use para representar secuencias que son (teóricamente) infinitamente largas.
Stream en Vavr es inmutable y puede estar Empty o en Cons . Cons consisten en el elemento principal y la cola calculada perezosamente de Stream . A diferencia de List , Stream almacena el elemento head en la memoria. Los elementos de cola se calculan según sea necesario.

Creemos un Stream de 10 enteros positivos y calculemos la suma de 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); 

A diferencia de la API Stream de Java 8, Stream in Vavr es una estructura de datos para almacenar una secuencia de elementos.

Por lo tanto, tiene métodos como get() , append() , insert() y otros para manipular sus elementos. drop() , distinct() y algunos otros métodos discutidos anteriormente también están disponibles.

Finalmente, demostremos rápidamente tabulate() en Stream . Este método devuelve un Stream longitud n contiene elementos que son el resultado de aplicar la función:

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

También podemos usar zip() para crear un Stream desde Tuple2<Integer, Integer> , que contiene elementos formados al combinar dos 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 es una secuencia indexada inmutable que proporciona un acceso aleatorio eficiente. Se basa en una matriz de objetos Java. En esencia, este es un contenedor Traversable para una matriz de objetos de tipo T

Puede crear una instancia de Array utilizando el método estático of() . Además, puede crear un rango de elementos utilizando los métodos estáticos range() y rangeBy() . El método rangeBy() tiene un tercer parámetro, que le permite determinar el paso.

Los métodos range() y rangeBy() crearán elementos, comenzando solo desde el valor inicial hasta el valor final menos uno. Si necesitamos incluir el valor final, podemos usar rangeClosed() o 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); 

Trabajemos con los elementos usando el í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. Vector


Vector es un cruce entre Array y List , que proporciona otra secuencia indexada de elementos, que permite el acceso aleatorio y la modificación en tiempo 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 es un objeto de colección para representar una secuencia de caracteres primitivos. En esencia, este es un contenedor para String con la adición de operaciones de recopilación.

Para crear CharSeq debes hacer lo siguiente.

 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. Establecer


Esta sección discute las diversas implementaciones de Set en la biblioteca de colección. Una característica única de la estructura de datos Set es que no permite valores duplicados.

Hay varias implementaciones de Set . El principal es HashSet . TreeSet no permite elementos duplicados y se puede ordenar. LinkedHashSet conserva el orden de inserción de los elementos.

Echemos un vistazo más de cerca a estas implementaciones una tras otra.

5.1. Hashset


HashSet tiene métodos de fábrica estáticos para crear nuevas instancias. Algunos de los cuales estudiamos anteriormente en este artículo, por ejemplo of() , ofAll() y variaciones en los métodos range() .

La diferencia entre los dos conjuntos se puede obtener utilizando el método diff() . Además, los métodos union() e intersect() devuelven la unión e intersección de dos 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)); 

También podemos realizar operaciones básicas, como agregar y eliminar 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")); 

La implementación de HashSet se basa en el trie mapeado de matriz de Hash (HAMT) , que cuenta con un rendimiento superior en comparación con el HashTable habitual y su estructura lo hace adecuado para soportar colecciones persistentes.

5.2. Conjunto de árboles


TreeSet es una implementación de la interfaz SortedSet . Almacena un conjunto de elementos ordenados y se implementa utilizando árboles de búsqueda binarios. Todas sus operaciones se realizan durante el tiempo O (log n) .

Por defecto, los elementos TreeSet se ordenan en su orden natural.
SortedSet un SortedSet usando un orden de clasificación 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 los elementos de forma personalizada, pase una instancia de Comparator al crear TreeSet . También puede crear una cadena a partir de un 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


Las colecciones Vavr también tienen una implementación inmutable de BitSet . La interfaz BitSet extiende la interfaz SortedSet . BitSet se puede crear utilizando métodos estáticos en BitSet.Builder .
Al igual que con otras implementaciones de la estructura de datos Set , BitSet no le permite agregar registros duplicados a un conjunto.

Hereda métodos de manipulación de la interfaz Traversable . Tenga en cuenta que es diferente de java.util.BitSet de la biblioteca estándar de Java. BitSet datos de BitSet no pueden contener valores de String .

Considere crear una instancia de BitSet utilizando el método de fábrica of() :

 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 seleccionar los primeros cuatro elementos de BitSet utilizamos el takeUntil() . La operación devolvió una nueva instancia. Tenga en cuenta que el método takeUntil() se define en la interfaz Traversable , que es la interfaz principal para BitSet .

Otros métodos y operaciones descritos anteriormente definidos en la interfaz Traversable también se aplican a BitSet .

6. Mapa


Map es una estructura de datos clave-valor. Map in Vavr es inmutable y tiene implementaciones para HashMap , TreeMap y LinkedHashMap .

Típicamente, los contratos de mapeo no permiten claves duplicadas, mientras que los valores duplicados mapeados a claves diferentes pueden serlo.

6.1. Hashmap


HashMap es una implementación de la interfaz de Map inmutable. Almacena pares clave-valor utilizando un hash de claves.

Map en Vavr usa Tuple2 para representar pares clave-valor en lugar del 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()); 

Al igual que HashSet , la implementación de HashMap basa en el trie mapeado de matriz de Hash (HAMT) , lo que conduce a un tiempo constante para casi todas las operaciones.
Los elementos del mapa se pueden filtrar por clave utilizando el método filterKeys() o por valor utilizando el método filterValues() . Ambos métodos toman 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")); 

También puede transformar elementos del map() utilizando el método map() . Por ejemplo, vamos a convertir map1 a 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. Mapa de árbol


TreeMap es una implementación de la interfaz SortedMap . Al igual que con TreeSet , se TreeSet una instancia personalizada de Comparator para personalizar la clasificación de los elementos de TreeMap .
SortedMap la creación de 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 defecto, TreeMap entradas de TreeMap se ordenan en orden de clave natural. Sin embargo, puede especificar el Comparator que se utilizará para ordenar:

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

Como en el caso de TreeSet , la implementación de TreeMap también se crea utilizando el árbol, por lo tanto, sus operaciones tienen tiempo O (log n) . El map.get(key) devuelve Option , que contiene el valor de la clave de mapa especificada.

7. compatibilidad Java


La API de Vavr Collection es totalmente compatible con Java Collection Framework. Veamos cómo se hace esto en la práctica.

7.1. Convertir de Java a Vavr


Cada implementación de colección en Vavr tiene una fábrica estática del método ofAll() que acepta java.util.Iterable . Esto le permite crear una colección Vavr a partir de una colección Java . Del mismo modo, otro método de fábrica de ofAll() acepta directamente Java Stream .

Para convertir una List Java en una List inmutable:

 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); 

Otra función útil es collector() , que se puede usar junto con Stream.collect() para obtener la colección 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. Convertir de Vavr a Java


La interfaz Value tiene muchos métodos para convertir de un tipo Vavr a un tipo Java . Estos métodos tienen el formato toJavaXXX() .

Considere un par de ejemplos:

 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()); 

También podemos usar Java 8 Collectors para recopilar elementos de las colecciones 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. Vistas de colecciones de Java


Además, la biblioteca proporciona las llamadas vistas de colección que funcionan mejor cuando se convierten en colecciones Java. Los métodos de transformación en la sección anterior iteran sobre (iteran) todos los elementos para crear una colección Java.

Las vistas, por otro lado, implementan interfaces Java estándar y delegan llamadas de método a la colección base de Vavr.

Al momento de escribir esto, solo se admite la vista de List . Cada colección secuencial tiene dos métodos: uno para crear una representación inmutable, el otro para mutable.

Llamar a métodos para cambiar en una vista inmutable UnsupportedOperationException .

Veamos un ejemplo:

 @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 crear una vista inmutable:

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

8. Conclusiones


En este tutorial, aprendimos sobre las diversas estructuras de datos funcionales proporcionadas por la API de Vavr Collections. También hay métodos API útiles y productivos que se pueden encontrar en el documento de Java y en la Guía del usuario de Vavr Collections.

Finalmente, es importante tener en cuenta que la biblioteca también define Try , Option , Either y Future , que amplían la interfaz Value y, como resultado, implementan la interfaz Java Iterable . Esto significa que en algunas situaciones pueden comportarse como colecciones.

El código fuente completo para todos los ejemplos en este artículo se puede encontrar en Github .

Materiales adicionales:
habr.com/en/post/421839
www.baeldung.com/vavr

Traducido por @middle_java

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


All Articles