Comprender a fondo el funcionamiento de Stream de JAVA


En JAVA , cuando se trata de operaciones sobre elementos en clases de colección, como matrices y colecciones , generalmente se procesan uno por uno mediante bucles o mediante Stream .

Por ejemplo, ahora existe tal requisito:

Devuelve una lista de palabras cuya longitud de palabra es mayor que 5 de una oración dada, muestra en orden inverso de longitud y devuelve como máximo 3

En JAVA7 y códigos anteriores , podemos implementarlo de la siguiente forma:

public List<String> sortGetTop3LongWords(@NotNull String sentence) {
    
    
    // 先切割句子,获取具体的单词信息
    String[] words = sentence.split(" ");
    List<String> wordList = new ArrayList<>();
    // 循环判断单词的长度,先过滤出符合长度要求的单词
    for (String word : words) {
    
    
        if (word.length() > 5) {
    
    
            wordList.add(word);
        }
    }
    // 对符合条件的列表按照长度进行排序
    wordList.sort((o1, o2) -> o2.length() - o1.length());
    // 判断list结果长度,如果大于3则截取前三个数据的子list返回
    if (wordList.size() > 3) {
    
    
        wordList = wordList.subList(0, 3);
    }
    return wordList;
}

En JAVA8 y versiones posteriores , con la ayuda de Streamflow, podemos escribir el siguiente código de manera más elegante:

public List<String> sortGetTop3LongWordsByStream(@NotNull String sentence) {
    
    
    return Arrays.stream(sentence.split(" "))
            .filter(word -> word.length() > 5)
            .sorted((o1, o2) -> o2.length() - o1.length())
            .limit(3)
            .collect(Collectors.toList());
}

Intuitivamente, la implementación del código Stream es más concisa y completa de una sola vez. Muchos estudiantes a menudo usan Stream en el código, pero su comprensión de Stream a menudo se limita a algunas operaciones simples como filtrar , mapear , recopilar , etc., pero los escenarios aplicables y las capacidades de JAVA Stream son mucho más que estos.

Entonces, aquí viene la pregunta: en comparación con el método foreach tradicional, ¿qué ventajas tiene Stream?

Aquí podemos dejar de lado este problema primero, primero tener una comprensión integral de Stream y luego discutir este problema.

Primer contacto con Stream y las API relacionadas

En pocas palabras, las operaciones de Stream se pueden dividir en 3 tipos:

  • Crear transmisión
  • Procesamiento intermedio de flujo
  • Terminar Steam
    inserte la descripción de la imagen aquí
    Cada tipo de operación de canalización de Stream contiene varios métodos de API, y la introducción de la función de cada método de API se enumera primero.

iniciar canalización

Es principalmente responsable de crear una nueva secuencia, o crear una nueva secuencia basada en una matriz, lista, conjunto, mapa y otros objetos de tipo colección existentes.

API Función descriptiva
arroyo() Crear un nuevo objeto de flujo en serie de flujo
flujoParalelo() Cree un objeto de flujo que se pueda ejecutar en paralelo
Corriente de() Crea un nuevo objeto Stream a partir de la secuencia dada de elementos

tubo medio

Es responsable de procesar el Stream y devolver un nuevo objeto Stream, y las operaciones de canalización intermedias se pueden superponer.

API Función descriptiva
filtrar() Filtre los elementos que cumplen los requisitos de acuerdo con las condiciones y devuelva una nueva secuencia
mapa() Convierta un elemento existente en otro tipo de objeto, lógica uno a uno, y devuelva una nueva secuencia
mapa plano() Convierta un elemento existente en otro tipo de objeto, lógica de uno a muchos, es decir, un objeto de elemento original se puede convertir en uno o más elementos de un nuevo tipo y devolver una nueva secuencia
límite() Conservar solo el número especificado de elementos delante de la colección y devolver una nueva secuencia
saltar() Omita el número especificado de elementos delante de la colección y devuelva una nueva secuencia
concat() Combinar los datos de los dos flujos en un nuevo flujo y devolver el nuevo flujo
distinto() Deduplicar todos los elementos en el Stream y devolver un nuevo flujo
ordenado () Ordene todos los elementos en la secuencia de acuerdo con las reglas especificadas y devuelva una nueva secuencia
ojeada() Recorra cada elemento en la secuencia uno por uno y devuelva la secuencia procesada

terminar la tubería

Como su nombre lo indica, después de que finaliza la operación de canalización, la secuencia Stream finalizará y se puede realizar algún procesamiento lógico al final, o se pueden devolver algunos datos de resultados de ejecución según sea necesario.

API Función descriptiva
contar() Devuelve el número final de elementos después del procesamiento de flujo
máx() Devuelve el valor máximo de los elementos después del procesamiento de flujo
min() Devuelve el valor mínimo del elemento después del procesamiento de flujo
encontrarPrimero() Terminar el procesamiento de flujo cuando se encuentra el primer elemento coincidente
encontrar cualquier() Salga del procesamiento de flujo cuando se encuentre cualquier elemento que cumpla con las condiciones. Esto es lo mismo que findFirst para flujos en serie. Es más eficiente para flujos en paralelo. Encontrar cualquier fragmento terminará la lógica de cálculo posterior.
CualquierCoincidencia() Devuelve un valor booleano, similar a isContains(), que se usa para determinar si hay elementos elegibles
todas las coincidencias () Devuelve un valor booleano utilizado para determinar si todos los elementos cumplen los criterios
ningunaCoincidencia() Devuelve un valor booleano utilizado para determinar si todos los elementos no cumplen las condiciones
recolectar() Convierta la transmisión al tipo especificado, especificado por Collectors
aArray() convertir flujo a matriz
iterador() Convierta la secuencia en un objeto Iterator
para cada() Sin valor de retorno, recorra los elementos uno por uno y luego ejecute la lógica de procesamiento dada

El método Stream utiliza

mapa y planoMapa

Tanto map como flatMap se utilizan para convertir elementos existentes en otros elementos, la diferencia es:

  • el mapa debe ser uno a uno, es decir, cada elemento solo se puede transformar en 1 elemento nuevo
  • flatMap puede ser de uno a muchos, es decir, cada elemento se puede convertir en uno o más elementos nuevos.
    inserte la descripción de la imagen aquí
    Por ejemplo: hay una lista de ID de cadena, y ahora debe convertirse en una lista de objetos de usuario. Puede usar el mapa para lograr:
/**
 * 演示map的用途:一对一转换
 */
public void stringToIntMap() {
    
    
    List<String> ids = Arrays.asList("205", "105", "308", "469", "627", "193", "111");
    // 使用流操作
    List<User> results = ids.stream()
            .map(id -> {
    
    
                User user = new User();
                user.setId(id);
                return user;
            })
            .collect(Collectors.toList());
    System.out.println(results);
}

Después de la ejecución, encontrará que cada elemento se convierte en un nuevo elemento correspondiente, pero el número total de elementos antes y después es el mismo:

[User{
    
    id='205'}, 
 User{
    
    id='105'},
 User{
    
    id='308'}, 
 User{
    
    id='469'}, 
 User{
    
    id='627'}, 
 User{
    
    id='193'}, 
 User{
    
    id='111'}]

Otro ejemplo: hay una lista de oraciones, y cada palabra de la oración debe extraerse para obtener una lista de todas las palabras. En este caso, no puedes manejarlo con el mapa y necesitas flatMap para jugar:

public void stringToIntFlatmap() {
    
    
    List<String> sentences = Arrays.asList("hello world","Jia Gou Wu Dao");
    // 使用流操作
    List<String> results = sentences.stream()
            .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
            .collect(Collectors.toList());
    System.out.println(results);
}

El resultado de la ejecución es el siguiente, puede ver que la cantidad de elementos en la lista de resultados es mayor que la cantidad de elementos en la lista original:

[hello, world, Jia, Gou, Wu, Dao]

Es necesario agregar aquí que cuando se opera flatMap, cada elemento se procesa primero y se devuelve un nuevo Stream, y luego se expanden múltiples Streams y se fusionan en un Stream completamente nuevo, de la siguiente manera:
inserte la descripción de la imagen aquí

métodos peek y foreach

Tanto peek como foreach se pueden usar para recorrer elementos y luego procesarlos uno por uno.
Pero según la introducción anterior, peek es un método intermedio , mientras que foreach es un método de terminación . Esto significa que peek solo se puede usar como un paso de procesamiento en el medio de la canalización y no se puede ejecutar directamente para obtener el resultado. Se ejecutará solo cuando haya otras operaciones de terminación detrás de él; y foreach es un método de terminación con sin valor de retorno Puede realizar directamente operaciones relacionadas.

public void testPeekAndforeach() {
    
    
    List<String> sentences = Arrays.asList("hello world","Jia Gou Wu Dao");
    // 演示点1: 仅peek操作,最终不会执行
    System.out.println("----before peek----");
    sentences.stream().peek(sentence -> System.out.println(sentence));
    System.out.println("----after peek----");
    // 演示点2: 仅foreach操作,最终会执行
    System.out.println("----before foreach----");
    sentences.stream().forEach(sentence -> System.out.println(sentence));
    System.out.println("----after foreach----");
    // 演示点3: peek操作后面增加终止操作,peek会执行
    System.out.println("----before peek and count----");
    sentences.stream().peek(sentence -> System.out.println(sentence)).count();
    System.out.println("----after peek and count----");
}

A partir de los resultados de salida, se puede ver que peek no se ejecuta cuando se llama solo, pero se puede ejecutar después de que peek vaya seguido de una operación de finalización, y foreach se puede ejecutar directamente:

----before peek----
----after peek----
----before foreach----
hello world
Jia Gou Wu Dao
----after foreach----
----before peek and count----
hello world
Jia Gou Wu Dao
----after peek and count----

filtro, ordenado, distinto, límite

Estos son los métodos de operación intermedia comúnmente utilizados de Stream, y el significado de los métodos específicos se explica en la tabla anterior. Al usarlo específicamente, puede elegir uno o más para usar en combinación según sus necesidades, o usar múltiples combinaciones del mismo método al mismo tiempo:

public void testGetTargetUsers() {
    
    
    List<String> ids = Arrays.asList("205","10","308","49","627","193","111", "193");
    // 使用流操作
    List<Dept> results = ids.stream()
            .filter(s -> s.length() > 2)
            .distinct()
            .map(Integer::valueOf)
            .sorted(Comparator.comparingInt(o -> o))
            .limit(3)
            .map(id -> new Dept(id))
            .collect(Collectors.toList());
    System.out.println(results);
}

La lógica de procesamiento del fragmento de código anterior es clara:

  1. Usar filtro para filtrar datos no calificados
  2. Usar distintos para deduplicar elementos almacenados
  3. Convierta una cadena a un tipo entero a través de una operación de mapa
  4. Use ordenados para especificar que están dispuestos en orden positivo según el tamaño del número
  5. Use el límite para interceptar los 3 elementos principales
  6. Use el mapa nuevamente para convertir la identificación al tipo de objeto Dept
  7. Use recopilar para finalizar la operación para recopilar los datos procesados ​​finales en la lista

Resultado de salida:

[Dept{
    
    id=111},  Dept{
    
    id=193},  Dept{
    
    id=205}]

Método de terminación de resultado simple

De acuerdo con la introducción anterior, métodos como count, max, min, findAny, findFirst, anyMatch, allMatch, nonneMatch y otros métodos en el método de terminación pertenecen al método de terminación de resultado simple mencionado aquí. El llamado simple significa que la forma de resultado es un número, un valor booleano o un valor de objeto opcional.

public void testSimpleStopOptions() {
    
    
    List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193");
    // 统计stream操作后剩余的元素个数
    System.out.println(ids.stream().filter(s -> s.length() > 2).count());
    // 判断是否有元素值等于205
    System.out.println(ids.stream().filter(s -> s.length() > 2).anyMatch("205"::equals));
    // findFirst操作
    ids.stream().filter(s -> s.length() > 2)
            .findFirst()
            .ifPresent(s -> System.out.println("findFirst:" + s));
}

El resultado después de la ejecución es:

6
true
findFirst:205

Recordatorio para evitar pits

Aquí debemos agregar un recordatorio de que una vez que se finaliza una transmisión, no es posible leer la transmisión y realizar otras operaciones; de lo contrario, se informará un error, vea el siguiente ejemplo:

public void testHandleStreamAfterClosed() {
    
    
    List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193");
    Stream<String> stream = ids.stream().filter(s -> s.length() > 2);
    // 统计stream操作后剩余的元素个数
    System.out.println(stream.count());
    System.out.println("-----下面会报错-----");
    // 判断是否有元素值等于205
    try {
    
    
        System.out.println(stream.anyMatch("205"::equals));
    } catch (Exception e) {
    
    
        e.printStackTrace();
    }
    System.out.println("-----上面会报错-----");
}

Cuando se ejecuta, el resultado es el siguiente:

6
-----下面会报错-----
java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
	at java.util.stream.ReferencePipeline.anyMatch(ReferencePipeline.java:449)
	at com.veezean.skills.stream.StreamService.testHandleStreamAfterClosed(StreamService.java:153)
	at com.veezean.skills.stream.StreamService.main(StreamService.java:176)
-----上面会报错-----

Debido a que el flujo ya ha sido terminado por el método count(), cuando el método anyMatch se ejecuta en el flujo, informará que ya se ha operado o cerrado un flujo de error , lo que requiere atención especial al usarlo.

Método de finalización de la recopilación de resultados

Debido a que Stream se usa principalmente para procesar datos de recopilación, además de los métodos de finalización anteriores para obtener resultados simples, hay más escenarios para obtener un objeto de resultado de tipo de recopilación, como List, Set o HashMap. El método de recopilación
es necesario aquí y puede admitir la generación de los siguientes tipos de datos de resultados:

  • Una clase de colección , como List, Set o HashMap, etc.
  • Objeto StringBuilder, que admite el empalme de varias cadenas y la salida del resultado empalmado
  • Un objeto que puede registrar el número o calcular la suma ( estadísticas de operaciones por lotes de datos )

generar colección

Debe considerarse como uno de los escenarios de recopilación más utilizados:

public void testCollectStopOptions() {
    
    
    List<Dept> ids = Arrays.asList(new Dept(17), new Dept(22), new Dept(23));
    // collect成list
    List<Dept> collectList = ids.stream().filter(dept -> dept.getId() > 20)
            .collect(Collectors.toList());
    System.out.println("collectList:" + collectList);
    // collect成Set
    Set<Dept> collectSet = ids.stream().filter(dept -> dept.getId() > 20)
            .collect(Collectors.toSet());
    System.out.println("collectSet:" + collectSet);
    // collect成HashMap,key为id,value为Dept对象
    Map<Integer, Dept> collectMap = ids.stream().filter(dept -> dept.getId() > 20)
            .collect(Collectors.toMap(Dept::getId, dept -> dept, (e1, e2) -> e1)); //加上(e1, e2) -> e1 防止key冲突
    System.out.println("collectMap:" + collectMap);
}

El resultado es el siguiente:

collectList:[Dept{
    
    id=22}, Dept{
    
    id=23}]
collectSet:[Dept{
    
    id=23}, Dept{
    
    id=22}]
collectMap:{
    
    22=Dept{
    
    id=22}, 23=Dept{
    
    id=23}}

Generar una cadena concatenada

Concatenar los valores de una Lista o arreglo en una cadena y separarlos con comas, creo que todos conocen esta escena, ¿no?
Si usa el bucle for y StringBuilder para empalmar bucles, debe considerar cómo lidiar con la última coma, que es muy engorrosa:

public void testForJoinStrings() {
    
    
    List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193");
    StringBuilder builder = new StringBuilder();
    for (String id : ids) {
    
    
        builder.append(id).append(',');
    }
    // 去掉末尾多拼接的逗号
    builder.deleteCharAt(builder.length() - 1);
    System.out.println("拼接后:" + builder.toString());
}

Pero ahora, con Stream, se puede implementar fácilmente usando recopilar:

public void testCollectJoinStrings() {
    
    
    List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193");
    String joinResult = ids.stream().collect(Collectors.joining(","));
    System.out.println("拼接后:" + joinResult);
}

Puede obtener exactamente el mismo resultado de cualquier manera, pero la forma Stream es más elegante:

拼接后:205,10,308,49,627,193,111,193

Toca en la pizarra:
La explicación aquí es que este escenario en realidad se puede hacer usando String.join(), y no necesita usar el método de transmisión anterior. Aquí quiero declarar que el encanto de Stream es que se puede combinar con otra lógica empresarial para el procesamiento, lo que hace que la lógica del código sea más natural y completa de una sola vez. Si es puramente un atractivo de empalme de cuerdas, realmente no hay necesidad de usar Stream para lograrlo. Después de todo, es un mazo ~ Pero puede mirar el ejemplo que se da a continuación, y puede sentir el verdadero encanto del empalme de cuerdas usando Corriente donde.
inserte la descripción de la imagen aquí

Operaciones matemáticas por lotes de datos

Hay otro escenario, que puede tener menos uso real, es decir, utilizar recopilar para generar la información de suma de datos digitales. También puede comprender el método de implementación:

public void testNumberCalculate() {
    
    
    List<Integer> ids = Arrays.asList(10, 20, 30, 40, 50);
    // 计算平均值
    Double average = ids.stream().collect(Collectors.averagingInt(value -> value));
    System.out.println("平均值:" + average);
    // 数据统计信息
    IntSummaryStatistics summary = ids.stream().collect(Collectors.summarizingInt(value -> value));
    System.out.println("数据统计信息: " + summary);
}

En el ejemplo anterior, el método de recopilación se utiliza para realizar operaciones matemáticas en los valores de los elementos de la lista y los resultados son los siguientes:

平均值:30.0
总和: IntSummaryStatistics{
    
    count=5, sum=150, min=10, average=30.000000, max=50}

Corriente paralela

Descripción del mecanismo

El uso de flujos paralelos puede utilizar efectivamente el hardware de múltiples CPU de la computadora y mejorar la velocidad de ejecución de la lógica. El flujo paralelo divide un flujo completo en múltiples fragmentos, luego ejecuta la lógica de procesamiento en cada flujo fragmentado en paralelo y finalmente agrega los resultados de ejecución de cada flujo fragmentado en un flujo general.
inserte la descripción de la imagen aquí

Restricciones y Restricciones

Los flujos paralelos son similares al procesamiento paralelo de subprocesos múltiples, por lo que también existen algunos problemas relacionados con los escenarios de subprocesos múltiples, como interbloqueos y otros problemas, por lo que la lógica de la función que finaliza la ejecución en flujos paralelos debe garantizar la seguridad de los subprocesos.

responde la pregunta inicial

En este punto, la introducción a los conceptos relacionados y el uso de JAVA Stream está básicamente terminada. Volvamos a centrarnos en una pregunta mencionada al principio de este artículo:

En comparación con el método tradicional foreach de procesamiento de secuencias, ¿qué ventajas tiene Stream?

De acuerdo con la introducción anterior, deberíamos poder extraer las siguientes respuestas:

  • El código es más conciso y el estilo de codificación declarativo hace que sea más fácil reflejar la intención lógica del código.
  • Desacoplamiento entre lógicas, lógica de procesamiento intermedio de un flujo, sin necesidad de prestar atención al contenido de aguas arriba y aguas abajo, solo es necesario implementar su propia lógica de acuerdo con el acuerdo
  • Los escenarios de flujo paralelo son más eficientes que los iteradores que se repiten uno por uno
  • Interfaz funcional, la función de ejecución retrasada, la operación de tubería intermedia no se ejecutará de inmediato sin importar cuántos pasos haya, y solo se ejecutará cuando se encuentre la operación de finalización, lo que puede evitar un consumo de operación innecesario en el medio.

Por supuesto, Stream no es todo ventajas, y también tiene sus desventajas en algunos aspectos:

  • La depuración de código es inconveniente
  • Cuando los programadores pasan de la escritura histórica a Stream, se necesita cierto tiempo para adaptarse.

Supongo que te gusta

Origin blog.csdn.net/doublepg13/article/details/128577400
Recomendado
Clasificación