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