La secuencia Stream de Java explicada en detalle

1. ¿Qué es la transmisión?

Stream es una nueva característica importante de Java 8. Proporciona soporte de programación funcional y permite que las operaciones de canalización operen colecciones. Las operaciones de flujo atraviesan la fuente de datos, utilizan operaciones de canalización para procesar los datos y generar una colección de resultados. Este proceso generalmente no afecta la fuente de datos causar impacto.

Al mismo tiempo, el flujo no es una estructura de datos, es solo una vista de una determinada fuente de datos. La fuente de datos puede ser una matriz, un contenedor Java o un canal de E/S  , etc. Cada operación en Stream generará una nueva secuencia. Internamente, el valor no se obtendrá inmediatamente como las operaciones de recopilación ordinarias. En cambio, el valor se obtendrá de forma perezosa y no se ejecutará hasta que el usuario realmente necesite el resultado .

Stream representa un flujo de datos y la cantidad de elementos de datos en el flujo puede ser limitada o ilimitada.

La diferencia entre transmisiones y colecciones.
  • No se almacenan datos . Una secuencia es un objeto basado en una fuente de datos. No almacena elementos de datos en sí, sino que pasa elementos de la fuente de datos a las operaciones a través de una canalización.

  • Programación funcional . Las operaciones de flujo no modifican la fuente de datos; por ejemplo, filterno eliminan datos en la fuente de datos.

  • Operación de retraso . Muchas operaciones de la secuencia, como filtro, mapa y otras operaciones intermedias, se retrasan y las operaciones solo se ejecutarán secuencialmente cuando se alcance la operación final.

  • Se puede desatar . Para un número ilimitado de flujos, algunas operaciones se pueden completar en un tiempo limitado, como limit(n) o  findFirst()Estas operaciones pueden implementar un "cortocircuito" y regresar después de acceder a un número limitado de elementos.

  • Consumo puro . Solo se puede acceder a los elementos de la secuencia una vez, similar al Iterador, y no hay vuelta atrás en la operación. Si desea volver a visitar los elementos de la secuencia desde el principio, lo siento, debe regenerar una nueva secuencia.

Las colecciones se tratan de datos, las transmisiones se tratan de cálculos.

 

 Stream en Java 8 es una mejora de la funcionalidad de los objetos de colección y se centra en realizar varias operaciones de agregación (operaciones agregadas) u operaciones de datos masivos (operaciones de datos masivos) muy convenientes y eficientes en objetos de colección. Stream API mejora enormemente la eficiencia de la programación y la legibilidad del programa con la ayuda de la expresión Lambda recientemente surgida.

Al mismo tiempo, proporciona modos en serie y paralelo para operaciones de agregación. El modo concurrente puede aprovechar al máximo las ventajas de los procesadores multinúcleo y utilizar métodos paralelos de bifurcación/unión para dividir tareas y acelerar el proceso de procesamiento. Escribir código paralelo suele ser difícil y propenso a errores, pero el uso de Stream API facilita la escritura de programas simultáneos de alto rendimiento sin escribir una sola línea de código multiproceso. Por lo tanto, java.util.stream, que apareció por primera vez en Java 8, es producto de la influencia combinada del lenguaje funcional + la era de múltiples núcleos.

2. Descripción general de las operaciones del flujo

Hay dos tipos de operaciones de flujo: operaciones intermedias, operaciones de terminación y operaciones de cortocircuito.

1. Operaciones intermedias

Una secuencia puede ir seguida de cero o más operaciones intermedias. Su propósito es principalmente abrir la secuencia, realizar cierto grado de mapeo/filtrado de datos y luego devolver una nueva secuencia para usar en la siguiente operación. Este tipo de operación es diferida, es decir, simplemente llamar a este tipo de método en realidad no inicia el recorrido de la secuencia.

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
2. Terminar la operación

Una secuencia solo puede tener una operación de terminal. Una vez ejecutada esta operación, la secuencia se "ilumina" y ya no se puede operar. Entonces esta debe ser la última operación en la transmisión. La ejecución de la operación Terminal en realidad iniciará el recorrido de la secuencia y generará un resultado o efecto secundario.

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

3. Operación de cortocircuito
  • Para una operación intermedia, si acepta una secuencia infinita, puede devolver una nueva secuencia finita.

  • Para una operación de terminal, si acepta un flujo infinito, pero puede calcular el resultado en un tiempo limitado.

anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
Corriente de tipo básico

IntStream, LongStream, DoubleStream , Java proporciona los Streams correspondientes para estos tres tipos numéricos básicos.

Otras secuencias numéricas no se proporcionan en Java 8 porque esto daría como resultado un contenido más amplificado. Las operaciones de agregación numérica convencionales se pueden realizar a través de las tres corrientes anteriores.

La construcción de corrientes numéricas.
IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);

range() necesita pasar dos parámetros: el nodo inicial y el nodo final, y devuelve un LongStream ordenado. Contiene todos los parámetros entre el nodo inicial y el nodo final, con un intervalo de 1.

La función de rangeClosed es similar a range. La diferencia es que rangeClosed incluye el nodo final, mientras que range no.

3. Características del flujo

Paralelismo  

Todas las operaciones de flujo se pueden ejecutar en serie o en paralelo.

 A menos que cree secuencias paralelas explícitamente, todas las bibliotecas Java crean secuencias en serie. Collection.stream()Crea una secuencia en serie para colecciones y Collection.parallelStream()una secuencia paralela para colecciones. IntStream.range(int, int)Lo que se crea es una secuencia en serie. Se pueden utilizar métodos parallel()para convertir secuencias en serie en secuencias paralelas, y sequential()se pueden utilizar métodos para convertir secuencias en secuencias en serie.

A menos que el Javadoc del método indique que el resultado del método es incierto cuando se ejecuta en paralelo (como findAny, forEach), los resultados de la ejecución en serie y en paralelo deben ser los mismos.

no puedo interferir 

Las transmisiones se pueden crear a partir de colecciones que no son seguras para subprocesos y las fuentes de datos no simultáneas no se deben cambiar mientras se ejecuta la canalización de la transmisión.

El siguiente código generará java.util.ConcurrentModificationExceptionuna excepción:

List<String> l = new ArrayList(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
sl.forEach(s -> l.add("three"));

Al configurar operaciones intermedias, puede cambiar la fuente de datos. Los problemas de simultaneidad (provocar excepciones o resultados inesperados) pueden ocurrir solo al ejecutar operaciones de punto final. Por ejemplo, el siguiente código no generará excepciones:

List l = new ArrayList(Arrays.asList("one", "two"));
Stream sl = l.stream();
l.add("three");
sl.forEach(System.out::println);

Para fuentes de datos concurrentes, no habrá tal problema, por ejemplo, el siguiente código es normal:

List l = new CopyOnWriteArrayList<>(Arrays.asList("one", "two"));
Stream sl = l.stream();
sl.forEach(s -> l.add("three"));

Aunque nuestro ejemplo anterior es modificar la fuente de datos no concurrente en la operación del terminal, la fuente de datos no concurrente también se puede modificar en otros subprocesos y también habrá problemas de concurrencia.

sin Estado 

Los parámetros de la mayoría de las operaciones de transmisión son interfaces funcionales que se pueden implementar mediante expresiones Lambda. Se utilizan para describir el comportamiento del usuario y se denominan parámetros de comportamiento.

Si estos parámetros de comportamiento tienen estado, los resultados de las operaciones de flujo pueden no estar definidos, como el siguiente código:

List<String> l = new ArrayList(Arrays.asList("one", "two", ……));
class State {
    boolean s;
}
final State state = new State();
Stream<String> sl = l.stream().map(e -> {
    if (state.s)
        return "OK";
    else {
        state.s = true;
        return e;
	}
});
sl.forEach(System.out::println);

Los resultados de múltiples ejecuciones del código anterior pueden ser diferentes cuando se ejecutan en paralelo. Esto se debe a que esta expresión lambda tiene estado.

efecto secundario

Después de ejecutar todas las operaciones en la tubería, ¿dónde están los resultados (si los hay) requeridos por el usuario? Lo primero que hay que explicar es que no todas las operaciones finales de Stream necesitan devolver resultados. Algunas operaciones son solo para usar sus efectos secundarios ( Side-effects ). Por ejemplo, usar Stream.forEach()un método para imprimir los resultados es un escenario común de uso de efectos secundarios. efectos.

Se desaconseja el uso de parámetros de comportamiento con  , de hecho, el uso de efectos secundarios debe evitarse en otros escenarios excepto en la impresión . Puede pensar que recopilar elementos en Stream.forEach() es una buena opción, como en el código siguiente, pero desafortunadamente no se puede garantizar la corrección y eficiencia de dicho uso porque Stream puede ejecutarse en paralelo. La mayoría de los usos de los efectos secundarios se pueden realizar de manera más segura y eficiente mediante operaciones de reducción .

Muchos parámetros de comportamiento con efectos secundarios se pueden convertir en implementaciones libres de efectos secundarios.

ArrayList<String> list = Lists.newArrayList();
for (int i = 0;i<1000;i++) {
    list.add(i+"");
}
ArrayList<String> list2 = Lists.newArrayList();
// 副作用代码
list.parallelStream().forEach(s -> list2.add(s));
System.out.println(list2);

 El resultado del código anterior es que se produjo un error en la operación ArrayList en estado de subprocesos múltiples. Al mismo tiempo, si no se especifica el tamaño de list2, se puede informar una excepción fuera de límites de subíndice ArrayIndexOutOfBoundsException cuando la lista se expande.

Puede cambiarlo al siguiente código sin efectos secundarios o utilizar la clase de colección concurrente CopyOnWriteArrayList en su lugar.

1 
2 
3 
4 
5 
6
COPIAR
ArrayList<String> lista = Lists.newArrayList(); 
for (int i = 0;i<1000;i++) { 
    list.add(i+""); 
} 
Lista<Cadena> lista2 = lista.parallelStream().collect(Collectors.toList()); 
System.out.println(lista2);
clasificar

Los elementos devueltos por algunas secuencias están en un orden determinado, al que llamamos  orden de encuentro . Este orden es el orden en el que la secuencia proporciona sus elementos. Por ejemplo, el orden de encuentro de una matriz es el orden de clasificación de sus elementos y la Lista es su orden de iteración. Para HashSet, no tiene orden de encuentro en sí.

El orden de encuentro de una secuencia depende principalmente de la fuente de datos y sus operaciones intermedias. Por ejemplo, las secuencias creadas en la Lista y la Matriz de la fuente de datos están ordenadas (ordenadas), pero las secuencias creadas en HashSet no están ordenadas.

El método sorted() puede convertir la secuencia en orden de encuentro y unorderedpuede convertir la secuencia en orden de encuentro .

Tenga en cuenta que este método no ordena ni dispersa los elementos, sino que devuelve una secuencia de orden de encuentro .

Además, una operación puede afectar el orden de la secuencia, como mapun método, que reemplazará los elementos de la secuencia con diferentes valores o incluso tipos, por lo que el orden de los elementos de entrada ya no tiene sentido, pero para filtermétodos En otras palabras, simplemente descarta algunos valores y el orden de los elementos de entrada aún está garantizado.

Para las transmisiones en serie, si la transmisión está ordenada o no, no afectará su rendimiento, pero sí afectará el determinismo. Los resultados de una secuencia desordenada pueden ser diferentes cuando se ejecuta varias veces.

Para flujos paralelos, eliminar la restricción de orden puede mejorar el rendimiento, por ejemplo distinct, groupingByen estas operaciones de agregación.

asociatividad

Una operación o función opes asociativa si satisface las siguientes condiciones:

1
COPIAR
(a en b) en c == a en (b en c)

Para flujos concurrentes, si las operaciones satisfacen la asociatividad, podemos calcular en paralelo:

1
COPIAR
a a b a c a d == (a a b) a (c a d)

Por ejemplo min, maxy la concatenación de cadenas son todas asociativas.

objeto de función

Cuando se utiliza Stream para programación funcional, a menudo es necesario pasar operaciones como parámetros al método de flujo, y el objeto de función es el método o la expresión lambda como objeto.

1
COPIAR
Lista<String> strArray = Arrays.asList(stringArrays).stream().filter(x>x.contains("Tomas")).collect(Collectors.toList());

filterEl parámetro en el ejemplo anterior x>x.contains("Tomas")es una expresión lambda.

Creación de flujo

Las transmisiones se pueden crear de varias maneras:

Crear una transmisión a partir de una colección

La interfaz Collection en Java8 se ha ampliado para proporcionar dos métodos para obtener secuencias:

  • Stream  stream()  : devuelve un flujo secuencial
  • Stream  paraleloStream()  : Devuelve un flujo paralelo
1 
2
COPIAR
Stream<Integer> stream1 = Arrays.asList(1,2,3,4).stream(); 
Stream<Integer> stream2 = Arrays.asList(1,2,3,4).parallelStream();

java.util.stream.StreamEs uno interface, y los valores de retorno de varias operaciones intermedias de canalización son sus clases de implementación, lo que nos permite pasar parámetros convenientemente.

Crear una secuencia a partir de una matriz

El método estático stream() de Arrays en Java8 puede obtener un flujo de matriz: static Stream  stream( T[] array )  : devuelve una forma sobrecargada de un flujo que puede manejar matrices de los tipos básicos correspondientes.

Flujo de matrices 也提供了创建流的静态方法():

1
COPIAR
Arrays.stream(nuevo int[]{1,2,3})
Crear una secuencia a partir de un valor

Puede utilizar el método estático Stream.of() para crear una secuencia mostrando valores, que pueden recibir cualquier número de parámetros.

Flujo estático público de (valores T...): Devuelve un flujo

StreamLos métodos estáticos of()también se pueden utilizar para crear transmisiones:

1
COPIAR
Stream<String> stream3 = Stream.of(new String[]{"1","2","3","4"});
Crear una secuencia a partir de un método

Puede utilizar los métodos estáticos Stream.iterate() y Stream.generate() para crear secuencias infinitas.

迭代流: iteración de flujo estático público (semilla T final, Operador Unario final f)

1 
2
COPIAR
//Secuencia geométrica infinita con valor inicial 1 
Stream.iterate(1, n -> n * 2);

Generación de flujo: generación de flujo estático público (proveedores)

1 
2
COPIAR
// Flujo de números aleatorios infinitos 
Stream.generate(Math::random)

Utilice los métodos estáticos de IntStream, LongStream y DoubleStream para crear transmisiones finitas.

1 
2 
3
COPIAR
IntStream.of(nuevo int[]{1, 2, 3}); 
IntStream.range(1, 3); 
IntStream.rangeClosed(1, 3);

Cree un flujo infinito de valores usando el método ints() de la clase de números aleatorios

1 
2
COPIAR
Aleatorio aleatorio = nuevo Aleatorio(); 
IntStream enteros = random.ints();
Obtener transmisión desde archivo

Obtenga un flujo de líneas de un archivo usando el método de líneas de BufferedReader

1 
2
COPIAR
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("file.txt"))); 
Stream<String> líneas = bufferedReader.lines();

Files类的操作路径的方法,如listfindwalk等。

其他类提供的创建流

一些类也提供了创建流的方法:

BitSet数值流

1
COPY
IntStream stream = new BitSet().stream();

Pattern 将字符串分隔成流

1
2
3
COPY
Pattern pattern = compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");
stringStream.forEach(System.out::println);

JarFile 读取jar文件流

1
COPY
Stream<JarEntry> stream = new JarFile("").stream();

更底层的使用StreamSupport,它提供了将Spliterator转换成流的方法,目前还不了解

中间操作

​ 流操作是惰性执行的, 中间操作会返回一个新的流对象, 当执行终点操作时才会真正进行计算,下面介绍流的中间操作,除非传入的操作函数有副作用, 函数本身不会对数据源进行任何修改。

​ 这个Scala集合的转换操作不同,Scala集合转换操作会生成一个新的中间集合,显而易见Java的这种设计会减少中间对象的生成。

distinct 唯一

distinct保证数据源中的重复元素在结果中只出现一次, 它使用equals()方法判断两个元素是否相等.

1
2
COPY
Stream<String> stream3 = Stream.of(new String[]{"1", "2", "3", "4", "1", "2", "3", "4"});
System.out.println(stream3.distinct().collect(Collectors.toList()));
filter 过滤

filter根据传入的断言函数对所有元素进行检查, 只有使断言函数返回真的元素才会出现在结果中. filter不会对数据源进行修改.

1
2
3
COPY
Stream<String> stream3 = Stream.of(new String[]{"1", "2", "3", "4", "6", "7", "8", "9"});
List<String> stringList = stream3.filter(x-> Integer.parseInt(x)%2==0).collect(Collectors.toList());
System.out.println(stringList);
map 映射

map方法根据传入的mapper函数对元素进行一对一映射, 即数据源中的每一个元素都会在结果中被替换(映射)为mapper函数的返回值,也可以根据处理返回不同的数据类型。

1
2
3
COPY
Stream<String> stream3 = Stream.of(new String[]{"1", "2", "3", "4", "6", "7", "8", "9"});
List<Integer> integerList = stream3.map(x -> Integer.parseInt(x)).collect(Collectors.toList());
System.out.println(integerList);
flatmap 映射汇总

flatmap方法混合了map + flattern的功能,同时扩展flatMapToDouble、flatMapToInt、flatMapToLong提供了转换成特定流的方法。它将映射后的流的元素全部放入到一个新的流中。它的方法定义如下:

1
COPY
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

​ flatmap适用于多对多或者一对多的映射关系,mapper函数会将每一个元素转换成一个流对象,而flatMap方法返回的一个流包含所有mapper转换后的元素。

下面举个例子来详细说明:

给定一个列表{“aaa”,”bbb”,”ddd”,”eee”,”ccc”}。需要在控制台直接输出aaabbbdddeeeccc字样采用map来做

1
2
3
4
5
6
7
8
9
10
COPY
 List<String> list = Arrays.asList("aaa", "bbb", "ddd", "eee", "ccc");
//这里采用了两次forEach循环进行输出,显然不太优雅
list.stream().map(x -> {
    List<Character> characterList = new ArrayList<>();
    char[] chars = x.toCharArray();
    for (char c : chars) {
        characterList.add(c);
    }
    return characterList.stream();
}).forEach(xStream -> xStream.forEach(System.out::print)); //aaabbbdddeeeccc

采用flatMap来做

1
2
3
4
5
6
7
8
9
10
COPY
 List<String> list = Arrays.asList("aaa", "bbb", "ddd", "eee", "ccc");
//采用flatMap来做  体会一下flatMap的魅力吧
list.stream().flatMap(x -> {
    List<Character> characterList = new ArrayList<>();
    char[] chars = x.toCharArray();
    for (char c : chars) {
        characterList.add(c);
    }
    return characterList.stream();
}).forEach(System.out::print); //aaabbbdddeeeccc
limit 截断

limit方法指定数量的元素的流。对于串行流,这个方法是有效的,这是因为它只需返回前n个元素即可,但是对于有序的并行流,它可能花费相对较长的时间,如果你不在意有序,可以将有序并行流转换为无序的,可以提高性能。

limit(int n)当流中元素数大于n时丢弃超出的元素, 否则不进行处理, 达到限制流长度的目的.

1
2
3
COPY
Stream<Integer> stream3 = Stream.of(3,5,1,4,2,6,8,7);
List<Integer> integerList = stream3.limit(3).collect(Collectors.toList());
System.out.println(integerList);
peek 观察者

生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;这里所说的消费函数有点类似于钩子,每个元素被消费时都会执行这个钩子

​ peek方法会对数据源中所有元素进行给定操作, 但在结果中仍然是数据源中的元素. 通常我们利用操作的副作用, 修改其它数据或进行输入输出.

​ peek接收一个没有返回值的λ表达式,可以做一些输出,外部处理等。map接收一个有返回值的λ表达式,之后Stream的泛型类型将转换为map参数λ表达式返回的类型

1
2
3
COPY
Stream<Integer>  stream3 = Stream.of(1,2,3,4,5,6,7,8,9);
List<Integer>  integerList = stream3.peek(x-> System.out.println(x)).collect(Collectors.toList());
System.out.println(integerList);
sorted 排序

sorted()将流中的元素按照自然排序方式进行排序,如果元素没有实现Comparable,则终点操作执行时会抛出java.lang.ClassCastException异常。

​ sorted(Comparator<? super T> comparator)可以指定排序的方式。

​ 对于有序流,排序是稳定的。对于非有序流,不保证排序稳定。

sorted方法用于对数据源进行排序:

1
2
3
COPY
Stream<Integer> stream3 = Stream.of(4, 5, 2, 6, 9, 0, 1, 3, 6, 8);
        List<Integer> integerList = stream3.sorted((x, y) -> x - y).collect(Collectors.toList());
System.out.println(integerList);

使用java.util.Comparator是更方便的方法, 默认进行升序排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
COPY
class Item {
            public Item(int value) {
                this.value = value;
            }
            private int value;
            public int getValue() {
                return value;
            }
            public void setValue(int value) {
                this.value = value;
            }
        }

Stream<Item> stream3 = Stream.of(new Item(4), new Item(3), new Item(6), new Item(9));
List<Item> itemList = stream3.sorted(Comparator.comparingInt(Item::getValue)).collect(Collectors.toList());
itemList.forEach(x -> System.out.print(x.getValue()+","));

使用reversed()方法进行降序排序:

1
2
3
COPY
Stream<Item> stream3 = Stream.of(new Item(4), new Item(3), new Item(6), new Item(9));
List<Item> itemList = stream3.sorted(Comparator.comparingInt(Item::getValue).reversed()).collect(Collectors.toList());
itemList.forEach(x -> System.out.print(x.getValue()+","));
skip 跳过

skip(int)返回丢弃了前n个元素的流. 如果流中的元素小于或者等于n,则返回空的流

1
2
3
COPY
Stream<Integer> stream3 = Stream.of(3,5,1,4,2,6,8,7);
List<Integer> integerList = stream3.skip(3).collect(Collectors.toList());
System.out.println(integerList);

终点操作

match 断言
1
2
3
COPY
public boolean 	allMatch(Predicate<? super T> predicate)
public boolean 	anyMatch(Predicate<? super T> predicate)
public boolean 	noneMatch(Predicate<? super T> predicate)

这一组方法用来检查流中的元素是否满足断言。

  • allMatch只有在所有的元素都满足断言时才返回true,否则flase,流为空时总是返回true

  • anyMatch只有在任意一个元素满足断言时就返回true,否则flase,

  • noneMatch只有在所有的元素都不满足断言时才返回true,否则flase,

1
2
3
4
5
6
COPY
System.out.println(Stream.of(1,2,3,4,5).allMatch( i -> i > 0)); //true
System.out.println(Stream.of(1,2,3,4,5).anyMatch( i -> i > 0)); //true
System.out.println(Stream.of(1,2,3,4,5).noneMatch( i -> i > 0)); //false
System.out.println(Stream.<Integer>empty().allMatch( i -> i > 0)); //true
System.out.println(Stream.<Integer>empty().anyMatch( i -> i > 0)); //false
System.out.println(Stream.<Integer>empty().noneMatch( i -> i > 0)); //true
count 计数

count方法返回流中的元素的数量。

1
2
COPY
String[] arr = new String[]{"a","b","c","d"};
long count = Arrays.stream(arr).count();

你也可以手动来实现它

1
2
COPY
String[] arr = new String[]{"a","b","c","d"};
long count = Arrays.stream(arr).mapToLong(x->1L).sum();
collect 收集

collect(Collector c) 将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法。辅助类Collectors提供了很多的collector收集器,可以满足我们日常的需求,你也可以创建新的collector实现特定的需求。它是一个值得关注的类,你需要熟悉这些特定的收集器,如聚合类averagingInt、最大最小值maxBy minBy、计数counting、分组groupingBy、字符串连接joining、分区partitioningBy、汇总summarizingInt、化简reducing、转换toXXX等。

收集器

Collectors里常用搜集器介绍:

方法 返回类型 作用
toList() List 把流中元素收集到List
List result = list.stream().collect(Collectors.toList());
toSet() Set 把流中元素收集到Set
Set result = list.stream().collect(Collectors.toSet());
toCollection() Collection 把流中元素收集到集合
Collection result = lsit.stream().collect(Collectors.toCollection(ArrayListL::new));
counting() Long 计算流中元素的个数
long count = lsit.stream().collect(Collectors.counting());
summingInt() Integer 对流中元素的整数属性求和
int total = lsit.stream().collect(Collectors.counting());
averagingInt Double 计算元素Integer属性的均值
double avg = lsit.stream().collect(Collectors.averagingInt(Student::getAge));
summarizingInt IntSummaryStatistics 收集元素Integer属性的统计值
IntSummaryStatistics result = list.stream().collect(Collectors.summarizingInt(Student::getAge));
**joining ** Stream 连接流中的每个字符串
String str = list.stream().map(Student::getName).collect(Collectors.joining());
**maxBy ** Optional 根据比较器选择最大值
Opetional max = list.stream().collect(Collectors.maxBy(comparingInt(Student::getAge)))
**minBy ** Optional 根据比较器选择最小值
Optional min= list.stream().collect(Collectors.minBy(comparingInt(Student::getAge)));
**reducing ** 规约产生的类型 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值
int total = list.stream().collect(Collectors.reducing(0, Student::getAge, Integer::sum));
collectingAndThen 转换函数返回的类型 包裹另一个收集器,对其结果转换
int how = list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
groupingBy Map<K, List> 根据某属性值对流分组,属性为K,结果为V
Map<Integer, List> map = list.stream().collect(Collectors.groupingBy(Student::getStatus));
partitioningBy Map<Boolean, List> 根据true或false进行分区
Map<Boolean, List> map = list.stream().collect(Collectors.partitioningBy(Student::getPass));
示例

collect是使用最广泛的终点操作, 也上文中多次出现:

1
2
3
COPY
List<String> list = Stream.of("a","b","c","b")
        .distinct()
        .collect(Collectors.toList())

​ toList()将流转换为List实例, 是最常见的用法, java.util.Collectors类中还有求和, 计算均值, 取最值, 字符串连接等多种收集方法。

find 返回
findAny()

返回任意一个元素,如果流为空,返回空的Optional,对于并行流来说,它只需要返回任意一个元素即可,所以性能可能要好于findFirst(),但是有可能多次执行的时候返回的结果不一样。

findFirst()

返回第一个元素,如果流为空,返回空的Optional。

forEach 遍历

forEach遍历流的每一个元素,执行指定的action。它是一个终点操作,和peek方法不同。这个方法不担保按照流的encounter order顺序执行,如果对于有序流按照它的encounter order顺序执行,你可以使用forEachOrdered方法。

forEach方法对流中所有元素执行给定操作, 没有返回值.

1
COPY
Stream.of(1,2,3,4,5).forEach(System.out::println);
嵌套遍历(不推荐)

如果要对两个集合进行遍历操作,可以将流嵌套,但是这种遍历的性能跟跟foreach嵌套一样,而且不能进行更复杂的操作,不推荐。

1
2
3
4
5
6
7
COPY
ArrayList<String> list = Lists.newArrayList("1", "2");
ArrayList<String> list2 = Lists.newArrayList("一", "二");
list.stream().forEach(str1->{
    list2.stream().forEach(str2->{
        System.out.println(str1+str2);
    });
});
max、min 最大最小值

max返回流中的最大值,

min返回流中的最小值。

1
2
3
4
5
6
7
COPY
ArrayList<Integer> list = Lists.newArrayList(3,5,2,1);
Integer max = list.stream().max(new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1 - o2;
    }
}).get();
concat 组合

concat(Stream a, Stream b)用来连接类型一样的两个流。

1
2
3
COPY
List<Integer> list1 = Arrays.asList(1,2,3);
List<Integer> list2 = Arrays.asList(4,3,2);
Stream.concat(list1.stream(),list2.stream()).forEach(System.out::print);
toXXX 转换

toArray方法将一个流转换成数组,而如果想转换成其它集合类型,西需要调用collect方法,利用Collectors.toXXX方法进行转换。

toArray()

将流中的元素放入到一个数组中,默认为Object数组

他还有一个重载方法可以返回指定类型的数组

1
2
COPY
Object[] objects = Stream.of(1, 2, 3, 4, 5).toArray();
Integer[] integers = Stream.of(1, 2, 3, 4, 5).toArray(Integer[]::new);
reduce 归约

reduce是常用的一个方法,事实上很多操作都是基于它实现的。

方法重载

它有几个重载方法:

方法 描述
reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值,返回 Optional
reduce(T iden, BinaryOperator b) 可以将流中元素反复结合起来,得到一个值,返回 T
reduce(U identity, BiFunction a, BinaryOperator combiner) 可以将流中元素反复结合起来,得到

PS: BinaryOperator 函数式接口,也即Lambada表达式

reduce思想

reduce是很重要的一种编程思想。这里重点介绍一下。reduce的作用是把stream中的元素给组合起来。至于怎么组合起来:

​ 它需要我们首先提供一个起始种子,然后依照某种运算规则使其与stream的第一个元素发生关系产生一个新的种子,这个新的种子再紧接着与stream的第二个元素发生关系产生又一个新的种子,就这样依次递归执行,最后产生的结果就是reduce的最终产出,这就是reduce的算法最通俗的描述;

​ 所以运用reduce我们可以做sum,min,max,average,所以这些我们称之为针对具体应用场景的reduce,这些常用的reduce,stream api已经为我们封装了对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
COPY
//求和 sum
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
// 没有起始值时返回为Optional类型
Optional<Integer> sumOptional = integers.stream().reduce(Integer::sum);
System.out.println(sumOptional.get()); //15

// 可以给一个起始种子值
Integer sumReduce = integers.stream().reduce(0, Integer::sum);
System.out.println(sumReduce); //15

//直接用sum方法
Integer sum = integers.stream().mapToInt(i -> i).sum();
System.out.println(sum); //15
第三个重载

前面两个方法比较简单,重点说说三个参数的reduce(U identity, BiFunction a, BinaryOperator combiner)

三个参数时是最难以理解的。 分析下它的三个参数:

  • identity: 一个初始化的值;这个初始化的值其类型是泛型U,与Reduce方法返回的类型一致;注意此时Stream中元素的类型是T,与U可以不一样也可以一样,这样的话操作空间就大了;不管Stream中存储的元素是什么类型,U都可以是任何类型,如U可以是一些基本数据类型的包装类型Integer、Long等;或者是String,又或者是一些集合类型ArrayList等;后面会说到这些用法。
  • accumulator: 其类型是BiFunction,输入是U与T两个类型的数据,而返回的是U类型;也就是说返回的类型与输入的第一个参数类型是一样的,而输入的第二个参数类型与Stream中元素类型是一样的
  • combiner: 其类型是BinaryOperator,支持的是对U类型的对象进行操作,第三个参数combiner主要是使用在并行计算的场景下;如果Stream是非并行时,它实际上是不生效的。

因此针对这个方法的分析需要分并行与非并行两个场景。

​ 就是因为U和T不一样,所以给了我们更多的发挥。比如设U的类型是ArrayList,那么可以将Stream中所有元素添加到ArrayList中再返回了,如下示例:

1
2
3
4
5
6
COPY
ArrayList<String> result = Stream.of("aa", "ab", "c", "ad").reduce(new ArrayList<>(),
                (u, s) -> {
                    u.add(s);
                    return u;
                }, (strings, strings2) -> strings);
System.out.println(result); //[aa, ab, c, ad]

​ 注意由于是非并行的,第三个参数实际上没有什么意义,可以指定r1或者r2为其返回值,甚至可以指定null为返回值。下面看看并行的情况:

​ 当Stream是并行时,第三个参数就有意义了,它会将不同线程计算的结果调用combiner做汇总后返回。注意由于采用了并行计算,前两个参数与非并行时也有了差异! 看个例子:

1
2
3
4
5
COPY
Integer reduce = Stream.of(1, 2, 3).parallel().reduce(
                4,
               (integer, integer2) -> integer + integer2,
               (integer, integer2) -> integer + integer2);
       System.out.println(reduce); //18

输出:18

​ omg,结果竟然是18。显然串行的话结果是10;这个不太好理解,但是我下面写一个等价的方式,可以帮助很好的理解这个结果:

1
2
COPY
Optional<Integer> reduce = Stream.of(1, 2, 3).map(n -> n + 4).reduce((s1, s2) -> s1 + s2);
System.out.println(reduce.get()); //18

​ 这种方式有助于理解并行三个参数时的场景,实际上就是第一步使用accumulator进行转换(它的两个输入参数一个是identity, 一个是序列中的每一个元素),由N个元素得到N个结果;第二步是使用combiner对第一步的N个结果做汇总。

reduce能干什么

好了,三个参数的reduce先介绍到这。下面继续看看reduce能为我们做什么?

1
2
3
4
5
6
7
8
COPY
//构造字符串流
List<String> strs = Arrays.asList("H", "E", "L", "L", "O");
// reduce
String concatReduce = strs.stream().reduce("", String::concat);
System.out.println(concatReduce); //HELLO
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
Integer minReduce = integerStream.reduce(Integer.MAX_VALUE, Integer::min);
System.out.println(minReduce); //1

并发问题

除非显式地创建并行流, 否则默认创建的都是串行流.Collection.stream()为集合创建串行流,而Collection.parallelStream()创建并行流.

stream.parallel()方法可以将串行流转换成并行流,stream.sequential()方法将流转换成串行流.

1
2
COPY
Stream<Integer> stream3 = Stream.of(1,2,3,4,5,6,7,8,9);
stream3.forEach(x-> System.out.print(x+","));

输出

1,2,3,4,5,6,7,8,9,

流可以在非线程安全的集合上创建, 流操作不应该对非线程安全的数据源产生任何副作用, 否则将发生java.util.ConcurrentModificationException异常.

1
2
COPY
List<String> list = new ArrayList(Arrays.asList("x", "y"));
list.stream().forEach(x-> list.add("z"));

输出

1
2
3
4
COPY
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1388)
	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
	at com.test.lambda.LambdaTest.main(LambdaTest.java:15)

对于线程安全的容器不会存在这个问题:

1
2
3
4
5
COPY
List<String> list = new CopyOnWriteArrayList(Arrays.asList("x", "y"));
list.stream().forEach(x->{
	list.add("z");
	System.out.println(list);
});

输出

[x, y, z]
[x, y, z, z]

当然作者建议Stream操作不要对数据源进行任何修改. 当然, 修改其它数据或者输入输出是允许的:

1
2
3
4
5
COPY
Set<String> set = new HashSet<String>();
List<String> list = new CopyOnWriteArrayList(Arrays.asList("x", "y"));
list.stream().forEach(x->{
    set.add(x);
});

理想的管道操作应该是无状态且与访问顺序无关的. 无状态是指操作的结果只与输入有关, 下面即是一个有状态的操作示例:

1
2
3
4
5
6
7
8
9
10
COPY
State state = getState();
List<String> list = new ArrayList(Arrays.asList("a", "b"));
list = list.stream().map(s -> {
  if (state.isReady()) {
    return s;
  }
  else {
    return null;
  }
});

无状态的操作保证无论系统状态如何管道的行为不变, 与顺序无关则有利于进行并行计算.

函数式接口

函数式接口会将签名匹配的函数对象(lambda表达式或方法)视作接口的实现。

1
2
3
4
5
COPY
@FunctionalInterface
interface Greeter
{
    void hello(String message);
}

函数式接口中有且只有一个非抽象方法。

1
COPY
Greeter greeter = message -> System.out.println("Hello " + message);

这在 Java 8 之前通常使用匿名内部类实现的:

1
2
3
4
5
6
COPY
Greeter greeter = new Greeter() {
            @Override
            public void hello(String message) {
                System.out.println("Hello " + message);
            }
        };

Java 8 将已有的一些接口实现为函数式接口:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.util.Comparator
  • java.lang.reflect.InvocationHandler
  • java.io.FileFilter
  • java.nio.file.PathMatcher

java.util.function中定义了一些常用的函数式接口:

  • Consumer: 接受参数无返回
    • Consumer<T> -> void accept(T t);
    • BiConsumer<T,U> -> void accept(T t, U u);
    • DoubleConsumer -> void accept(double value);
  • Supplier: 不接受参数有返回
    • Supplier<T> -> T get();
    • DoubleSupplier -> double getAsDouble();
  • Function: 接受参数并返回
    • Function<T, R> -> R apply(T t);
    • BiFunction<T, U, R> -> R apply(T t, U u);
    • DoubleFunction<R> -> R apply(double value);
    • DoubleToIntFunction -> int applyAsInt(double value);
    • BinaryOperator<T> extends BiFunction<T,T,T>
  • Predicate: 接受参数返回boolean
    • Predicate<T> -> boolean test(T t);
    • BiPredicate<T, U> -> boolean test(T t, U u);
    • DoublePredicate -> boolean test(double value);

默认构造器可以作为supplier: Supplier<Item> supplier = Item::new;

Supongo que te gusta

Origin blog.csdn.net/qq_45443475/article/details/131437679
Recomendado
Clasificación