3 Operaciones terminales¶
Reducción pura¶
Métodos estándar de reducción¶
-
long count()
: retorna el número de elementos de un stream. Ejemplo: -
Las clases
IntStream
,LongStream
yDoubleStream
, correspondientes a streams de elementos de tipos primitivos numéricos, disponen del métodosum()
para calcular la suma de los elementos del stream:public class Sum{ public void show(){ int sumEvenNumbers; sumEvenNumbers = IntStream.of(30, 23, 24, 57, 8, 15) .filter(n -> n % 2 == 0) .sum(); System.out.println(sumEvenNumbers); // 62 } public static void main(String[] args){ new Sum().show(); } }
Si disponemos de un stream genérico de elementos y no de un stream y no de un stream de alguna de las clases mencionadas anteriormente, podemos usar alguno de los métodos
mapToInt()
,mapToLong()
,mapToDouble()
,flatMapToInt()
, etc., para obtener un stream de un tipo específico. Ejemplo: -
Estas clases de streams de elementos de tipo primitivo numérico también disponen de métodos para obtener el valor máxima,
max()
, el valor mínimo,min()
y la media aritmética,average()
, de los elementos numéricos del stream. Dado que el stream sobre el que se apliquen puede estar vacío, estos métodos retornan un Optional.public class MaxMinAverage{ public void show(){ OptionalInt minEvenNumbers, maxEvenNumbers; OptionalDouble averageEvenNumbers; minEvenNumbers = IntStream.of(30, 23, 24, 57, 8, 15) .filter(n -> n % 2 == 0) .min(); minEvenNumbers.ifPresent(System.out::println); // 8 maxEvenNumbers = IntStream.of(30, 23, 24, 57, 8, 15) .filter(n -> n % 2 == 0) .max(); maxEvenNumbers.ifPresent(System.out::println); // 30 averageEvenNumbers = IntStream.of(30, 23, 24, 57, 8, 15) .filter(n -> n % 2 == 0) .average(); averageEvenNumbers.ifPresent(n -> System.out.printf("%.2f", n)); //20,67 } public static void main(String[] args){ new MaxMinAverage().show(); } }
Si se trata de un stream genérico, tenemos disponibles métodos para calcular el máximo y el mínimo que reciben un Comparator para comparar los elementos del stream y así obtener en cada caso el valor mínimo o el máximo:
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)
public class MaxMinComparator{ public void show(){ Optional<Vehicle> minVehicle; Optional<Vehicle> maxVehicle; List<Vehicle> list = new ArrayList<>(); list.add(new Vehicle("1705UBG", 4, "blanco")); list.add(new Vehicle("1235GTR", 2, "rojo")); System.out.println("Vehículo con menor número de ruedas: "); minVehicle = list.stream() .min(Comparator.comparingInt(Vehicle::getWheelCount)); minVehicle.ifPresent(System.out::println); System.out.println("Vehículo con mayor matrícula alfabéticamente"); maxVehicle = list.stream() .max(Comparator.comparing(Vehicle::getRegistration)); maxVehicle.ifPresent(System.out::println); } public static void main(String[] args){ new MaxMinComparator().show(); } }
Métodos reduce¶
¿Y si queremos realizar una operación de reducción distinta a las anteriores? Para casos más genéricos usaremos el método Optional<T> reduce(BinaryOperator<T> accumulator)
que recibe una función acumuladora. La función acumuladora debe ser una función asociativa, es decir, da igual en qué orden se opere con los elementos porque siempre se va a obtener el mismo resultado. Se trata de una regla que se cumple en la suma y en la multiplicación. Por ejemplo, 1+2+3
da el mismo resultado que 2+3+1
.
En este caso, el proceso de reducción comienza cuando se obtiene el segundo elemento, ya que necesitamos al menos dos elementos para hacer la primera reducción. Por este motivo, el método retorna un Optional
, dado que si el stream no tiene elementos suficientes no se puede realizar la reducción ni producir ningún valor.
public class Reduce{
public void show(){
OptionalInt integerSum = IntStream.of(30, 23, 24, 57, 8, 15)
.reduce((subtotal, element) -> subtotal + element);
integerSum.ifPresent(System.out::println); // 157
integerSum = IntStream.of(30, 23, 24, 57, 8, 15)
.reduce(Integer::sum);
integerSum.ifPresent(System.out::println); // 157
integerSum = IntStream.empty()
.reduce(Integer::sum);
System.out.println(integerSum); // OptionalInt.empty
integerSum.ifPresent(System.out::println); // No hace nada
}
public static void main(String[] args){
new Reduce().show();
}
}
Este método está sobrecargado para pasarle como primer parámetro un valor conocido como identidad (identity), que es usado como valor inicial de la operación de reducción y el resultado por defecto si el stream está vacío: T reduce(T identity, BinaryOperator<T> accumulator)
.
Debemos elegir cuidadosamente el valor de identity atendiendo a la operación que se lleve a cada en la función binaryOperator. El valor de identity debe ser una identidad para la función acumuladora, es decir, si se aplica la función acumuladora a cualquier elemento con la identidad, debe devolver el mismo elemento. Por ejemplo, si utilizamos como operación una suma, la identidad debe ser 0 ya que cualquier número al que le sumemos 0, nos devuelve el mismo número. Por el mismo razonamiento, si la operación es una multiplicación, la identidad sobre ser 1.
public class ReduceIdentity{
public void show(){
Integer sum, mult;
sum = IntStream.of(30, 23, 24, 57, 8, 15)
.reduce(0, Integer::sum);
System.out.println(sum); // 157
sum = IntStream.empty()
.reduce(0, Integer::sum);
System.out.println(sum); // 0
mult = IntStream.of(2, 3, 4)
.reduce(1, (subtotal, element) -> subtotal * element);
System.out.println(mult); // 24
mult = IntStream.of(2, 3, 4)
.reduce(1, Math::multiplyExact);
System.out.println(mult); // 24
mult = IntStream.empty()
.reduce(1, Math::multiplyExact);
System.out.println(mult); // 1
}
public static void main(String[] args){
new ReduceIdentity().show();
}
}
Tenemos disponible una tercera versión del método, <U> U reduce(U identity, BiFunction<U, ? super T, u> accumulator, BinaryOperator<U> combiner)
, especialmente útil para streams paralelos, que se usa cuando queremos que el método retorne un valor de tipo diferente al del stream original. La función combiner es necesaria para indicar cómo se deben combinar acumuladores parciales realizadas en distintos hilos en streams paralelos.
public class ReduceCombiner{
public void show(){
int count = Stream.of("Juan", "Pepe", "Luis", "Ricardo", "Laura")
.reduce(0, (subtotal, element) -> {
if(element.length() % 2 == 0){
return subtotal + 1;
} else {
return subtotal;
}
}, Integer::sum);
System.out.printf("Cuántos nombres con número de caracteres pares: %d", count); // 3
}
public static void main(String[] args){
new ReduceCombiner().show();
}
}
Debemos tener encuentra que la función reduce()
en cualquiera de sus versiones respeta el orden del stream a la hora de combinar los cálculos intermedios.
Operaciones terminales de consulta¶
La clase Stream
también proporciona una serie de métodos de consulta sobre los elementos de un stream, denominadas operaciones de cortocircuito (short-circuit terminal operators). Se llaman así porque se deja de procesar el resto de elementos si con los elementos que ya han sido procesados se es capaz de determinar el resultado.
Tenemos un conjunto de métodos que permiten consultar, respectivamente, si todos, ninguno o algunos de los elementos del stream cumplen con un determinado predicado, retornado un valor booleano:
boolean allMatch(Predicate<? super T> predicate)
boolean noneMatch(Predicate<? super T> predicate)
boolean anyMatch(Predicate<? super T> predicate)
public class Match{
public void show(){
boolean match;
match = IntStream.of(1, 2, 4).allMatch(n -> n % 2 == 0);
System.out.println(match); // false
match = IntStream.of(2, 4, 6).allMatch(n -> n % 2 == 0);
System.out.println(match); // true
match = IntStream.of(1, 2, 4).noneMatch(n -> n % 2 == 0);
System.out.println(match); // false
match = IntStream.of(1, 3, 5).noneMatch(n -> n % 2 == 0);
System.out.println(match); // true
match = IntStream.of(1, 3, 5).anyMatch(n -> n % 2 == 0);
System.out.println(match); // false
match = IntStream.of(2, 3, 5).anyMatch(n -> n % 2 == 0);
System.out.println(match); // true
}
public static void main(String[] args){
new Match().show();
}
}
Por otro lado tenemos los métodos findFirst()
y findAny()
, que retornan un Optional<T>
con, respectivamente, el primer elemento del stream, o algún elemento del stream (no está indicado cuál), si es que existe. Un aspecto curioso es que estos métodos no reciben ningún predicado con el que indicar la condición de búsqueda por lo que normalmente se usan después de haber ejecutarlo el método filter
sobre el stream.
public class Find{
public void show(){
Optional<Integer> find;
find = Stream.of(1, 2, 4)
.filter(n -> n % 2 == 0)
.findFirst();
find.ifPresent(System.out::println); // 2
find = Stream.of(6, 2, 4)
.filter(n -> n % 2 == 0)
.findAny();
find.ifPresent(System.out::println); // 6
}
public static void main(String[] args){
new Find().show();
}
}
Un aspecto curioso es que no se proporciona ningún método para obtener el último elemento de un stream. sin embargo, podemos obtenerlo usando el método skip
(siempre y cuando se trate de un stream finito):
public class LastElement{
public void show(){
List<Integer> list = List.of(30, 23, 24, 57, 8, 15);
long count = list.stream().count();
Optional<Integer> last = list.stream()
.skip(count - 1)
.findFirst();
last.ifPresent(System.out::println); // 15
}
public static void main(String[] args){
new LastElement().show();
}
}
Otra manera de hacerlo es mediante la reducción en la que siempre nos quedemos con el segundo elemento:
public class LastReduce{
public void show(){
OptionalInt last;
last = IntStream.of(30, 23, 24, 57, 8, 15)
.reduce((first, second) -> second);
last.ifPresent(System.out::println); // 15
}
public static void main(String[] args){
new LastElement().show();
}
}
Debemos tener en cuenta que si ejecutamos estos métodos sobre streams paralelos el resultado puede ser distinto entre distintas llamadas.