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_javaArtí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
TPuede 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/421839www.baeldung.com/vavrTraducido por
@middle_java