Java Stream API:高效数据处理的利器引言
Java Stream API:高效数据处理的利器引言
在 Java 编程中,数据处理是一项极为常见且关键的任务。传统的 for
循环在处理数据集合时,往往会导致代码变得冗长、复杂,这不仅增加了代码的编写难度,还降低了代码的可读性和可维护性。Java 8 引入的 Stream API 则为我们提供了一种全新的、更加高效和简洁的数据处理方式。Stream API 允许我们以声明式的方式处理数据集合,将数据处理的逻辑与数据的存储和遍历分离开来,使得代码更加清晰、易于理解和维护。本文将深入、详细地介绍 Stream API 的各个组成部分,包括数据源、中间操作和终止操作,并通过丰富多样的示例代码展示如何使用 Stream API 替代传统的 for
循环进行数据处理。
一、Stream 管道流的组成
Stream 管道流主要由三个核心部分构成:一个数据源、零个或多个中间操作以及一个终止操作。下面我们将对这三个部分进行全面且详细的介绍。
1.1 数据源
数据源是 Stream 的起始点,它可以是多种形式,如数组、集合、生成器函数、I/O 管道等。Stream API 提供了丰富的方法来从不同的数据源创建流。
1.1.1 从数组创建流
使用 Arrays.stream()
方法能够轻松地从数组创建流。数组是一种常见的数据存储形式,通过将其转换为流,我们可以利用 Stream API 提供的强大功能进行数据处理。以下是一个详细的示例代码:
import java.util.Arrays;public class StreamDataSourceFromArray {public static void main(String[] args) {String[] array = {"apple", "banana", "cherry"};// 使用 Arrays.stream() 方法从数组创建流Arrays.stream(array).forEach(System.out::println);}
}
在上述代码中,Arrays.stream(array)
方法将数组 array
转换为一个流。forEach
是一个终止操作,它会对流中的每个元素执行指定的操作,这里使用了方法引用 System.out::println
,表示将每个元素打印到控制台。通过这种方式,我们可以方便地遍历数组中的元素。
1.1.2 从集合创建流
集合类(如 List
、Set
等)提供了 stream()
方法来创建流。集合是 Java 中常用的数据结构,将其转换为流后,我们可以利用 Stream API 进行更高效的数据处理。以下是一个从 List
创建流的示例:
import java.util.Arrays;
import java.util.List;public class StreamDataSourceFromList {public static void main(String[] args) {List<String> list = Arrays.asList("dog", "cat", "elephant");// 使用 list.stream() 方法从集合创建流list.stream().forEach(System.out::println);}
}
在这个示例中,list.stream()
方法将 List
集合转换为一个流。同样,使用 forEach
方法遍历并打印集合中的元素。与传统的 for
循环相比,使用 Stream API 可以使代码更加简洁和易读。
1.1.3 从生成器函数创建流
除了数组和集合,我们还可以使用生成器函数创建流。Stream.generate()
方法可以接受一个 Supplier
接口的实现,用于生成流中的元素。以下是一个生成随机数流的示例:
import java.util.Random;
import java.util.stream.Stream;public class StreamDataSourceFromGenerator {public static void main(String[] args) {Random random = new Random();// 使用 Stream.generate() 方法创建一个无限流,生成随机数Stream.generate(random::nextInt).limit(5) // 限制流的元素数量为 5.forEach(System.out::println);}
}
在上述代码中,Stream.generate(random::nextInt)
创建了一个无限流,其中每个元素都是一个随机整数。为了避免无限循环,我们使用 limit(5)
方法限制流的元素数量为 5。最后,使用 forEach
方法打印这 5 个随机数。
1.1.4 从 I/O 管道创建流
在处理文件或网络数据时,我们可以从 I/O 管道创建流。例如,使用 Files.lines()
方法可以从文件中读取每一行并创建一个流。以下是一个简单的示例:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;public class StreamDataSourceFromIO {public static void main(String[] args) {try {// 从文件中读取每一行并创建一个流Stream<String> lines = Files.lines(Paths.get("example.txt"));lines.forEach(System.out::println);lines.close(); // 关闭流} catch (IOException e) {e.printStackTrace();}}
}
在这个示例中,Files.lines(Paths.get("example.txt"))
从名为 example.txt
的文件中读取每一行,并创建一个流。然后使用 forEach
方法打印每一行。最后,需要调用 close()
方法关闭流,以释放资源。
1.2 中间操作
中间操作是 Stream 管道流中的重要组成部分,它可以将一个流转换为另一个流。中间操作是惰性的,即只有在终止操作被调用时才会真正执行。这意味着我们可以链式调用多个中间操作,而不会立即进行数据处理,直到遇到终止操作。常见的中间操作包括 filter
、map
、flatMap
等,下面我们将逐一详细介绍。
1.2.1 filter
方法
filter
方法用于过滤流中的元素,只保留满足指定条件的元素。它接受一个 Predicate
函数式接口作为参数,该接口的 test
方法用于判断元素是否满足条件。以下是一个过滤偶数的示例:
import java.util.Arrays;
import java.util.List;public class FilterExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);numbers.stream().filter(n -> n % 2 == 0) // 过滤出偶数.forEach(System.out::println);}
}
在上述代码中,filter(n -> n % 2 == 0)
表示只保留流中能被 2 整除的元素,即偶数。最终,只有满足条件的元素会被传递给 forEach
方法进行打印输出。filter
方法可以帮助我们快速筛选出符合特定条件的数据,提高数据处理的效率。
1.2.2 map
方法
map
方法用于将流中的每个元素映射为另一个元素。它接受一个 Function
函数式接口作为参数,该接口的 apply
方法用于对每个元素进行转换。例如,将字符串转换为大写:
import java.util.Arrays;
import java.util.List;public class MapExample {public static void main(String[] args) {List<String> words = Arrays.asList("hello", "world");words.stream().map(String::toUpperCase) // 将每个单词转换为大写.forEach(System.out::println);}
}
在这个示例中,map(String::toUpperCase)
将流中的每个字符串元素转换为大写形式,然后传递给 forEach
方法进行打印。map
方法可以用于对数据进行各种转换,如类型转换、数据提取等。
1.2.3 mapToInt
方法
mapToInt
方法用于将流中的元素映射为 int
类型。它接受一个 ToIntFunction
函数式接口作为参数,该接口的 applyAsInt
方法用于将元素转换为 int
类型。例如,将字符串转换为整数:
import java.util.Arrays;
import java.util.List;public class MapToIntExample {public static void main(String[] args) {List<String> numbers = Arrays.asList("1", "2", "3");numbers.stream().mapToInt(Integer::parseInt) // 将字符串转换为整数.forEach(System.out::println);}
}
这里,mapToInt(Integer::parseInt)
将流中的每个字符串元素解析为整数,形成一个 IntStream
。IntStream
提供了一些专门用于处理整数的方法,如 sum()
、average()
等。与普通的 Stream<Integer>
相比,IntStream
可以更高效地处理整数数据。
1.2.4 flatMap
方法
flatMap
方法用于将流中的每个元素展开为多个元素。它接受一个 Function
函数式接口作为参数,该接口的 apply
方法返回一个流。例如,将嵌套列表展开:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;public class FlatMapExample {public static void main(String[] args) {List<List<Integer>> nestedList = Arrays.asList(Arrays.asList(1, 2),Arrays.asList(3, 4));List<Integer> flattenedList = nestedList.stream().flatMap(List::stream) // 展开嵌套列表.collect(Collectors.toList());System.out.println(flattenedList);}
}
在这个示例中,flatMap(List::stream)
将嵌套的 List
展开为一个包含所有元素的流。具体来说,对于每个内部列表,List::stream
方法会将其转换为一个流,然后 flatMap
方法会将这些流合并成一个新的流。最后,使用 collect(Collectors.toList())
方法将流中的元素收集到一个新的 List
中。flatMap
方法在处理嵌套数据结构时非常有用。
1.2.5 distinct
方法
distinct
方法用于去除流中的重复元素。它根据元素的 equals()
方法来判断元素是否重复。以下是一个示例:
import java.util.Arrays;
import java.util.List;public class DistinctExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3);numbers.stream().distinct() // 去除重复元素.forEach(System.out::println);}
}
在上述代码中,distinct()
方法会去除流中的重复元素,只保留每个不同元素的一个实例。最终,输出结果将是 1
、2
、3
。
1.2.6 sorted
方法
sorted
方法用于对流中的元素进行排序。它有两种重载形式:一种是无参的,使用元素的自然顺序进行排序;另一种是接受一个 Comparator
接口的实现,用于自定义排序规则。以下是一个使用自然顺序排序的示例:
import java.util.Arrays;
import java.util.List;public class SortedExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(3, 1, 2);numbers.stream().sorted() // 使用自然顺序排序.forEach(System.out::println);}
}
在这个示例中,sorted()
方法会将流中的元素按照自然顺序进行排序,最终输出结果将是 1
、2
、3
。如果需要自定义排序规则,可以传递一个 Comparator
接口的实现,例如:
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;public class SortedWithComparatorExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(3, 1, 2);numbers.stream().sorted(Comparator.reverseOrder()) // 按降序排序.forEach(System.out::println);}
}
在这个示例中,Comparator.reverseOrder()
表示按降序排序,最终输出结果将是 3
、2
、1
。
1.2.7 peek
方法
peek
方法用于在流的每个元素上执行一个操作,但不会改变流中的元素。它主要用于调试和监控流的处理过程。以下是一个示例:
import java.util.Arrays;
import java.util.List;public class PeekExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3);numbers.stream().peek(n -> System.out.println("Processing: " + n)) // 打印每个元素.map(n -> n * 2).forEach(System.out::println);}
}
在上述代码中,peek(n -> System.out.println("Processing: " + n))
会在每个元素被处理之前打印一条消息,方便我们监控流的处理过程。最终,流中的每个元素会被乘以 2 并打印输出。
1.2.8 limit
方法
limit
方法用于限制流的元素数量。它接受一个 long
类型的参数,表示要保留的元素数量。以下是一个示例:
import java.util.Arrays;
import java.util.List;public class LimitExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);numbers.stream().limit(3) // 只保留前 3 个元素.forEach(System.out::println);}
}
在这个示例中,limit(3)
方法会只保留流中的前 3 个元素,最终输出结果将是 1
、2
、3
。
1.2.9 skip
方法
skip
方法用于跳过流中的前几个元素。它接受一个 long
类型的参数,表示要跳过的元素数量。以下是一个示例:
import java.util.Arrays;
import java.util.List;public class SkipExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);numbers.stream().skip(2) // 跳过前 2 个元素.forEach(System.out::println);}
}
在上述代码中,skip(2)
方法会跳过流中的前 2 个元素,最终输出结果将是 3
、4
、5
。
1.3 终止操作
终止操作是 Stream 管道流的最后一步,它会触发中间操作的执行并产生最终结果。常见的终止操作包括 forEach
、collect
、count
等,下面我们将分别详细介绍。
1.3.1 forEach
方法
forEach
方法用于对流中的每个元素执行指定的操作。它接受一个 Consumer
函数式接口作为参数,该接口的 accept
方法用于定义要执行的操作。例如,遍历并打印元素:
import java.util.Arrays;
import java.util.List;public class ForEachExample {public static void main(String[] args) {List<String> names = Arrays.asList("Alice", "Bob", "Charlie");names.stream().forEach(System.out::println);}
}
在上述代码中,forEach(System.out::println)
表示对流中的每个元素执行打印操作。forEach
方法是一个终端操作,一旦调用,流的处理过程就会结束。
1.3.2 collect
方法
collect
方法用于将流中的元素收集到一个集合中。它接受一个 Collector
接口的实现作为参数,Collector
接口定义了如何将流中的元素收集到目标集合中。Java 提供了一些预定义的 Collector
实现,如 Collectors.toList()
、Collectors.toSet()
等。以下是一个将计算后的元素收集到一个新的 List
中的示例:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;public class CollectExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);List<Integer> squaredNumbers = numbers.stream().map(n -> n * n) // 计算每个数的平方.collect(Collectors.toList());System.out.println(squaredNumbers);}
}
在这个示例中,map(n -> n * n)
计算每个元素的平方,然后 collect(Collectors.toList())
将计算后的元素收集到一个新的 List
中。除了 toList()
,我们还可以使用 toSet()
将元素收集到一个 Set
中,使用 toMap()
将元素收集到一个 Map
中,等等。
1.3.3 count
方法
import java.util.Arrays;
import java.util.List;public class CountExample {public static void main(String[] args) {List<String> names = Arrays.asList("Alice", "Bob", "Charlie");long count = names.stream().count();System.out.println("Number of names: " + count);}
}
在上述代码里,count()
方法会对流中的元素进行计数。由于 names
列表中有 3 个元素,所以最终输出的结果是 Number of names: 3
。count
方法在需要快速知晓集合元素数量时非常实用,而且结合中间操作使用时,能统计出符合特定条件的元素数量。例如:
import java.util.Arrays;
import java.util.List;public class ConditionalCountExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);long evenCount = numbers.stream().filter(n -> n % 2 == 0).count();System.out.println("Number of even numbers: " + evenCount);}
}
这里先使用 filter
方法筛选出偶数,再用 count
方法统计偶数的数量,最终输出偶数的个数。
1.3.4 reduce
方法
reduce
方法用于将流中的元素进行合并,得到一个最终结果。它有几种重载形式,最常用的是接受一个二元操作符(BinaryOperator
)作为参数。以下是一个计算整数列表元素总和的示例:
import java.util.Arrays;
import java.util.List;
import java.util.Optional;public class ReduceExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);sum.ifPresent(result -> System.out.println("Sum: " + result));}
}
在上述代码中,reduce((a, b) -> a + b)
会将流中的元素依次进行累加。reduce
方法返回一个 Optional
对象,因为流可能为空,使用 ifPresent
方法可以安全地处理可能为空的结果。如果流不为空,就会打印出元素的总和。
还有一种重载形式可以提供一个初始值,如下所示:
import java.util.Arrays;
import java.util.List;public class ReduceWithInitialValueExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);int sum = numbers.stream().reduce(10, (a, b) -> a + b);System.out.println("Sum with initial value: " + sum);}
}
这里初始值为 10,最终结果是初始值加上流中元素的总和。
1.3.5 min
和 max
方法
min
和 max
方法分别用于找出流中的最小值和最大值。它们接受一个 Comparator
接口的实现作为参数,用于定义元素之间的比较规则。如果没有提供 Comparator
,则使用元素的自然顺序。以下是一个找出整数列表中最小值的示例:
import java.util.Arrays;
import java.util.List;
import java.util.Optional;public class MinExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5);Optional<Integer> min = numbers.stream().min(Integer::compareTo);min.ifPresent(result -> System.out.println("Minimum value: " + result));}
}
在上述代码中,min(Integer::compareTo)
使用 Integer
的自然顺序比较元素,找出最小值。同样,max
方法的使用方式类似:
import java.util.Arrays;
import java.util.List;
import java.util.Optional;public class MaxExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5);Optional<Integer> max = numbers.stream().max(Integer::compareTo);max.ifPresent(result -> System.out.println("Maximum value: " + result));}
}
1.3.6 anyMatch
、allMatch
和 noneMatch
方法
anyMatch
方法:用于判断流中是否至少有一个元素满足指定条件。它接受一个Predicate
函数式接口作为参数。以下是一个示例:
import java.util.Arrays;
import java.util.List;public class AnyMatchExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);System.out.println("Has even number: " + hasEven);}
}
在这个例子中,anyMatch(n -> n % 2 == 0)
检查流中是否有偶数,只要有一个偶数就返回 true
。
allMatch
方法:用于判断流中的所有元素是否都满足指定条件。示例如下:
import java.util.Arrays;
import java.util.List;public class AllMatchExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(2, 4, 6, 8);boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0);System.out.println("All numbers are even: " + allEven);}
}
这里 allMatch(n -> n % 2 == 0)
检查流中的所有元素是否都是偶数,只有当所有元素都满足条件时才返回 true
。
noneMatch
方法:用于判断流中是否没有元素满足指定条件。示例如下:
import java.util.Arrays;
import java.util.List;public class NoneMatchExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 3, 5, 7);boolean noEven = numbers.stream().noneMatch(n -> n % 2 == 0);System.out.println("No even numbers: " + noEven);}
}
noneMatch(n -> n % 2 == 0)
检查流中是否没有偶数,只有当没有一个元素满足条件时才返回 true
。
1.3.7 findFirst
和 findAny
方法
findFirst
方法:用于返回流中的第一个元素。它返回一个Optional
对象,因为流可能为空。以下是一个示例:
import java.util.Arrays;
import java.util.List;
import java.util.Optional;public class FindFirstExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);Optional<Integer> first = numbers.stream().findFirst();first.ifPresent(result -> System.out.println("First number: " + result));}
}
在上述代码中,findFirst()
方法返回流中的第一个元素,如果流不为空,就打印出该元素。
findAny
方法:用于返回流中的任意一个元素。在顺序流中,通常返回第一个元素;在并行流中,可能返回任意一个元素。示例如下:
import java.util.Arrays;
import java.util.List;
import java.util.Optional;public class FindAnyExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);Optional<Integer> any = numbers.stream().findAny();any.ifPresent(result -> System.out.println("Any number: " + result));}
}
findAny()
方法在并行处理时比较有用,因为它可以快速返回一个可用的元素,而不必等待找到第一个元素。
1.3.8 toArray
方法
toArray
方法用于将流中的元素收集到一个数组中。它有两种重载形式:一种是无参的,返回一个 Object
数组;另一种是接受一个 IntFunction<T[]>
作为参数,用于指定数组的类型。以下是一个示例:
import java.util.Arrays;
import java.util.List;public class ToArrayExample {public static void main(String[] args) {List<String> names = Arrays.asList("Alice", "Bob", "Charlie");String[] nameArray = names.stream().toArray(String[]::new);System.out.println(Arrays.toString(nameArray));}
}
在这个例子中,toArray(String[]::new)
将流中的元素收集到一个 String
数组中,并打印出数组的内容。
二、综合示例
下面通过一个综合示例,展示如何使用 Stream API 进行复杂的数据处理。假设我们有一个包含多个学生信息的列表,每个学生有姓名、年龄和成绩,我们要筛选出年龄大于 18 岁且成绩大于 80 分的学生,并将他们的姓名收集到一个新的列表中。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;class Student {private String name;private int age;private double score;public Student(String name, int age, double score) {this.name = name;this.age = age;this.score = score;}public String getName() {return name;}public int getAge() {return age;}public double getScore() {return score;}
}public class ComprehensiveExample {public static void main(String[] args) {List<Student> students = new ArrayList<>();students.add(new Student("Alice", 20, 85));students.add(new Student("Bob", 17, 75));students.add(new Student("Charlie", 22, 90));students.add(new Student("David", 19, 70));List<String> qualifiedStudents = students.stream().filter(student -> student.getAge() > 18 && student.getScore() > 80).map(Student::getName).collect(Collectors.toList());System.out.println("Qualified students: " + qualifiedStudents);}
}
在上述代码中,首先定义了一个 Student
类来表示学生信息。然后创建了一个包含多个学生的列表。接着使用 Stream API 进行数据处理:
filter(student -> student.getAge() > 18 && student.getScore() > 80)
筛选出年龄大于 18 岁且成绩大于 80 分的学生。map(Student::getName)
将筛选后的学生对象映射为他们的姓名。collect(Collectors.toList())
将姓名收集到一个新的列表中。
最后打印出符合条件的学生姓名列表。
三、总结
Stream API 为 Java 开发者提供了一种强大而简洁的数据处理方式。通过将数据处理逻辑封装在流管道中,我们可以避免传统 for
循环带来的代码冗余和复杂性,提高代码的可读性和可维护性。在实际开发中,我们应该尽量使用 Stream API 来替代传统的 for
循环,充分发挥其优势。同时,需要注意中间操作的惰性和终止操作的触发机制,合理组合各种操作来实现高效的数据处理。希望本文的详细介绍和丰富示例能够帮助你更好地理解和使用 Java Stream API。
四、并行流的使用及注意事项
4.1 并行流的基本概念
并行流是 Stream API 提供的一种能够充分利用多核处理器性能的数据处理方式。它将流中的元素分成多个部分,在多个线程中并行处理这些部分,最后将结果合并。通过并行流,可以显著提高大规模数据处理的效率。
4.2 并行流的创建与使用
可以使用 parallelStream()
方法直接从集合创建并行流,也可以通过 parallel()
方法将顺序流转换为并行流。以下是示例代码:
import java.util.Arrays;
import java.util.List;public class ParallelStreamExample {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);// 直接从集合创建并行流long sum1 = numbers.parallelStream().mapToInt(Integer::intValue).sum();System.out.println("Sum using parallelStream(): " + sum1);// 将顺序流转换为并行流long sum2 = numbers.stream().parallel().mapToInt(Integer::intValue).sum();System.out.println("Sum using parallel(): " + sum2);}
}
在上述代码中,parallelStream()
直接创建了一个并行流,而 parallel()
方法将原本的顺序流转换为并行流。两种方式最终都对元素进行求和操作。
4.3 并行流的性能考量
虽然并行流可以提高处理效率,但并非在所有情况下都适用。以下是一些需要考虑的因素:
- 数据规模:对于小规模数据,使用并行流可能会因为线程创建和管理的开销而导致性能下降。只有当数据规模足够大时,并行流才能发挥出优势。例如,处理少量元素的列表时,顺序流可能更快:
import java.util.Arrays;
import java.util.List;public class ParallelStreamPerformanceSmallData {public static void main(String[] args) {List<Integer> smallNumbers = Arrays.asList(1, 2, 3, 4, 5);long startTimeSeq = System.currentTimeMillis();int sumSeq = smallNumbers.stream().mapToInt(Integer::intValue).sum();long endTimeSeq = System.currentTimeMillis();System.out.println("Sequential sum: " + sumSeq + ", Time taken: " + (endTimeSeq - startTimeSeq) + " ms");long startTimePar = System.currentTimeMillis();int sumPar = smallNumbers.parallelStream().mapToInt(Integer::intValue).sum();long endTimePar = System.currentTimeMillis();System.out.println("Parallel sum: " + sumPar + ", Time taken: " + (endTimePar - startTimePar) + " ms");}
}
- 操作复杂度:如果流中的操作非常简单,如简单的映射或过滤,并行流的线程管理开销可能会超过并行处理带来的好处。而对于复杂的操作,并行流可能更具优势。
- 数据结构:不同的数据结构在并行流中的性能表现不同。例如,
ArrayList
等可随机访问的数据结构在并行流中表现较好,因为可以很容易地将其分割成多个部分;而LinkedList
等顺序访问的数据结构在并行流中的性能可能较差。
4.4 并行流的线程安全问题
使用并行流时,需要特别注意线程安全问题。如果在并行流的操作中涉及到共享可变状态,可能会导致数据不一致或其他并发问题。例如,以下代码存在线程安全问题:
import java.util.Arrays;
import java.util.List;public class ParallelStreamThreadSafetyIssue {private static int sharedSum = 0;public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);numbers.parallelStream().forEach(n -> sharedSum += n);System.out.println("Shared sum: " + sharedSum);}
}
在上述代码中,多个线程同时对 sharedSum
进行累加操作,可能会导致数据不一致。为了解决这个问题,可以使用线程安全的数据结构或同步机制,或者使用 reduce
等方法进行安全的聚合操作:
import java.util.Arrays;
import java.util.List;public class ParallelStreamThreadSafe {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);int sum = numbers.parallelStream().reduce(0, Integer::sum);System.out.println("Safe sum: " + sum);}
}
五、Stream API 与函数式编程的结合
5.1 函数式接口在 Stream API 中的应用
Stream API 大量使用了函数式接口,如 Predicate
、Function
、Consumer
等。这些函数式接口允许我们以简洁的方式定义数据处理逻辑。例如,在 filter
方法中使用 Predicate
接口来筛选元素:
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;public class FunctionalInterfaceInStream {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);Predicate<Integer> isEven = n -> n % 2 == 0;numbers.stream().filter(isEven).forEach(System.out::println);}
}
在上述代码中,定义了一个 Predicate
接口的实现 isEven
,用于判断一个数是否为偶数,然后将其传递给 filter
方法。
5.2 Lambda 表达式的优势
Lambda 表达式是函数式编程的核心特性之一,它与 Stream API 结合使用可以使代码更加简洁和易读。相比于传统的匿名内部类,Lambda 表达式减少了样板代码,让开发者可以更专注于业务逻辑。例如,比较使用匿名内部类和 Lambda 表达式的区别:
import java.util.Arrays;
import java.util.List;public class LambdaVsAnonymousClass {public static void main(String[] args) {List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);// 使用匿名内部类numbers.stream().filter(new java.util.function.Predicate<Integer>() {@Overridepublic boolean test(Integer n) {return n % 2 == 0;}}).forEach(System.out::println);// 使用 Lambda 表达式numbers.stream().filter(n -> n % 2 == 0).forEach(System.out::println);}
}
可以看到,使用 Lambda 表达式的代码更加简洁明了。
5.3 方法引用的使用
方法引用是 Lambda 表达式的一种简化形式,它允许我们直接引用已有的方法。在 Stream API 中,方法引用可以使代码更加简洁和直观。例如,使用方法引用进行元素的打印:
import java.util.Arrays;
import java.util.List;public class MethodReferenceInStream {public static void main(String[] args) {List<String> names = Arrays.asList("Alice", "Bob", "Charlie");names.stream().forEach(System.out::println);}
}
在上述代码中,System.out::println
是一个方法引用,它等价于 n -> System.out.println(n)
。
六、Stream API 在实际项目中的应用场景
6.1 数据筛选与过滤
在实际项目中,经常需要从大量数据中筛选出符合特定条件的数据。例如,从用户列表中筛选出年龄大于 18 岁的用户:
import java.util.ArrayList;
import java.util.List;class User {private String name;private int age;public User(String name, int age) {this.name = name;this.age = age;}public int getAge() {return age;}public String getName() {return name;}
}public class DataFilteringExample {public static void main(String[] args) {List<User> users = new ArrayList<>();users.add(new User("Alice", 20));users.add(new User("Bob", 15));users.add(new User("Charlie", 22));List<User> adultUsers = users.stream().filter(user -> user.getAge() > 18).collect(java.util.stream.Collectors.toList());adultUsers.forEach(user -> System.out.println(user.getName()));}
}
6.2 数据转换与映射
在处理数据时,可能需要将一种数据类型转换为另一种数据类型,或者提取数据中的某些信息。例如,从商品列表中提取商品的名称:
import java.util.ArrayList;
import java.util.List;class Product {private String name;private double price;public Product(String name, double price) {this.name = name;this.price = price;}public String getName() {return name;}
}public class DataMappingExample {public static void main(String[] args) {List<Product> products = new ArrayList<>();products.add(new Product("Apple", 2.5));products.add(new Product("Banana", 1.5));products.add(new Product("Cherry", 3.0));List<String> productNames = products.stream().map(Product::getName).collect(java.util.stream.Collectors.toList());productNames.forEach(System.out::println);}
}
6.3 数据聚合与统计
在数据分析和报表生成中,需要对数据进行聚合和统计操作,如求和、求平均值、求最大值等。例如,统计订单列表的总金额:
import java.util.ArrayList;
import java.util.List;class Order {private double amount;public Order(double amount) {this.amount = amount;}public double getAmount() {return amount;}
}public class DataAggregationExample {public static void main(String[] args) {List<Order> orders = new ArrayList<>();orders.add(new Order(100.0));orders.add(new Order(200.0));orders.add(new Order(300.0));double totalAmount = orders.stream().mapToDouble(Order::getAmount).sum();System.out.println("Total order amount: " + totalAmount);}
}
6.4 数据分组与分区
在处理数据时,可能需要根据某些条件对数据进行分组或分区。例如,将员工按部门进行分组:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;class Employee {private String name;private String department;public Employee(String name, String department) {this.name = name;this.department = department;}public String getDepartment() {return department;}public String getName() {return name;}
}public class DataGroupingExample {public static void main(String[] args) {List<Employee> employees = new ArrayList<>();employees.add(new Employee("Alice", "HR"));employees.add(new Employee("Bob", "IT"));employees.add(new Employee("Charlie", "HR"));Map<String, List<Employee>> employeesByDepartment = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment));employeesByDepartment.forEach((department, empList) -> {System.out.println("Department: " + department);empList.forEach(emp -> System.out.println(" " + emp.getName()));});}
}
七、总结与展望
7.1 总结
Java Stream API 为我们提供了一种强大而灵活的数据处理方式,通过将数据源、中间操作和终止操作组合成流管道,我们可以以声明式的方式处理数据,提高代码的可读性和可维护性。同时,并行流的支持使得我们能够充分利用多核处理器的性能,加速大规模数据的处理。函数式编程的特性,如 Lambda 表达式和方法引用,与 Stream API 紧密结合,进一步简化了代码的编写。
7.2 展望
随着 Java 技术的不断发展,Stream API 可能会进一步完善和扩展。例如,可能会提供更多的中间操作和终止操作,以满足更复杂的数据处理需求;并行流的性能可能会得到进一步优化,减少线程管理的开销;与其他 Java 特性(如模块化、响应式编程等)的集成也可能会更加紧密。开发者在实际项目中应该充分利用 Stream API 的优势,不断探索和创新,以提高开发效率和代码质量。
相关文章:
Java Stream API:高效数据处理的利器引言
Java Stream API:高效数据处理的利器引言 在 Java 编程中,数据处理是一项极为常见且关键的任务。传统的 for 循环在处理数据集合时,往往会导致代码变得冗长、复杂,这不仅增加了代码的编写难度,还降低了代码的可读性和…...
kubeadm构建k8s源码阅读环境
目标 前面看了minikube的源码了解到其本质是调用了kubeadm来启动k8s集群,并没有达到最初看代码的目的。 所以继续看看kubeadm的代码,看看能否用来方便地构建源码调试环境。 k8s源码编译 kubeadm源码在k8s源码库中,所以要先克隆k8s源码。之…...
Java架构设计亿级流量场景下的本地缓存方案选型
在当今的互联网时代,亿级流量的应用场景已经司空见惯。无论是大型电商平台的促销活动,还是热门社交应用的日常运营,都可能面临每秒数万甚至数十万的请求流量。在这样的高并发、高流量场景下,系统的性能和稳定性面临着巨大的挑战。…...
ChatGPT怎么回事?
纯属发现,调侃一下~ 这段时间deepseek不是特别火吗,尤其是它的推理功能,突发奇想,想用deepseek回答一些问题,回答一个问题之后就回复服务器繁忙(估计还在被攻击吧~_~) 然后就转向了GPT…...
离线安装Appium Server
1、问题概述? 安装Appium通常有两种方式: 第一种:下载exe安装包,这种是Appium Server GUI安装方式,缺点是通过命令启动不方便。 第二种:通过cmd安装appium server,可以通过命令方式启动,比较方便。 问题:在没有外网的情况下,无法通过命令在cmd中安装appium server…...
Jetpack ViewModel
private val deviceViewModel: IDeviceViewModel by viewModels<DeviceViewModel>() 这句代码是 Jetpack ViewModel 在 Fragment 或 Activity 中的标准用法,它的作用是 创建并获取 ViewModel 实例,同时确保 ViewModel 的生命周期与 UI 组件保持一…...
2025年2月9日(数据分析,在最高点和最低点添加注释,添加水印)
要在最高点和最低点添加文本注释,可以使用 plt.annotate() 函数。这个函数允许你在图表中的特定位置添加文本注释,并且可以指定箭头指向特定的数据点。 以下是修改后的代码,添加了在最高点和最低点的文本注释: from matplotlib import pyplot as plt from matplotlib imp…...
如何导入第三方sdk | 引入第三方jar 包
0. 背景1. 上传私有仓库2. 使用本地文件系统 0. 背景 对接一些第三方功能,会拿到第三方的sdk,也就是jar包,如何导入呢 1. 上传私有仓库 最好的方式就是将第三方jar包,上传到私有的仓库,这样直接正常在pom引用即可如果只…...
掌握内容中台与人工智能技术的新闻和应用场景分析
内容概要 在当今数字化快速发展的时代,内容中台与人工智能技术的结合为各行各业带来了新的机遇。这一切都源自于对内容生产和管理能力的需求不断提升,尤其在新闻行业中更是如此。内容中台作为一种集中管理内容资源的平台,能够有效整合与调配…...
c#-枚举
//可空类型:int? num 等价 Nullable<int> num Nullable<int> a null; a 99; Console.WriteLine(a);//合并运算符?? : a有值的话,赋值给b int b a ?? 1; Console.WriteLine(b); 枚举成员不能相同,但枚举的值可…...
青少年编程与数学 02-008 Pyhon语言编程基础 22课题、类的定义和使用
青少年编程与数学 02-008 Pyhon语言编程基础 22课题、类的定义和使用 一、类类的定义和使用示例 二、定义1. 类定义语法2. 属性和方法3. 构造器和初始化4. 实例化5. 类变量和实例变量6. 类方法和静态方法7. 继承8. 多态总结 三、使用1. 创建类的实例2. 访问属性3. 调用方法4. 修…...
【通俗易懂说模型】反向传播(附多元回归与Softmax函数)
🌈 个人主页:十二月的猫-CSDN博客 🔥 系列专栏: 🏀深度学习_十二月的猫的博客-CSDN博客 💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前言 2. …...
【人工智能】Python中的深度学习优化器:从SGD到Adam
《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 在深度学习模型的训练过程中,优化器起着至关重要的作用,它决定了模型的收敛速度以及最终的性能。本文将介绍深度学习中常用的优化器,从传…...
仅128个token达到ImageNet生成SOTA性能!MAETok:有效的扩散模型的关键是什么?(卡内基梅隆港大等)
论文链接:https://arxiv.org/pdf/2502.03444 项目链接:https://github.com/Hhhhhhao/continuous_tokenizer 亮点直击 理论与实验分析:通过实验和理论分析建立了潜空间结构与扩散模型性能之间的联系。揭示了具有更少高斯混合模型(G…...
Listener监听器和Filter过滤器
一.监听器 1.是javaweb的三大组件之一,分别是Servlet程序,Listener监听器,Filter过滤器 2.Listener是JvaEE的规范,就是接口,监听器的作用就是监听某种变化(一般是对象创建/销毁,属性变化),触发对应方法完成相应的任务 3.ServletContextListener:/*当一个类实现了ServletContex…...
我的年度写作计划
目录 计算机经典四件 数据结构 计算机网络体系 经典操作系统与计算机架构 嵌入式领域笔记 其他部分 私货部分 笔者打算在这里理一下今年的写作计划,如下所示: 计算机经典四件 数据结构 笔者因为冲刺面试需要,还是要更加扎实的掌握自…...
kafka专栏解读
kafka专栏文章的编写将根据kafka架构进行编写,即先编辑kafka生产者相关的内容,再编写kafka服务端的内容(这部分是核心,内容较多,包含kafka分区管理、日志存储、延时操作、控制器、可靠性等),最后…...
数据库操作与数据管理——Rust 与 SQLite 的集成
第六章:数据库操作与数据管理 第一节:Rust 与 SQLite 的集成 在本节中,我们将深入探讨如何在 Rust 中使用 SQLite 数据库,涵盖从基本的 CRUD 操作到事务处理、数据模型的构建、性能优化以及安全性考虑等方面。SQLite 是一个轻量…...
Linux文件目录基本操作
目录 目录概述相关操作函数相关数据结构体说明 目录概述 什么是目录? 在linux操作系统中其实目录也是一种文件,相对于普通文件,它的存储内容不同,它的存储内容主要是当前目录下的文件以及子目录文件信息。目录就像是一颗大树&a…...
TaskBuilder项目实战:创建项目
用TaskBuilder开发应用系统的第一步就是创建项目,项目可以是一个简单的功能模块,也可以是很多功能模块的集合,具体怎么划分看各位的实际需要,我们一般会将相互关联比较紧密的一组功能模块放到一个独立的项目内,以便打包…...
使用DeepSeek的技巧笔记
来源:新年逼自己一把,学会使用DeepSeek R1_哔哩哔哩_bilibili 前言 对于DeepSeek而言,我们不再需要那么多的提示词技巧,但还是要有两个注意点:你需要理解大语言模型的工作原理与局限,这能帮助你更好的知道AI可完成任务…...
使用Python实现PDF与SVG相互转换
目录 使用工具 使用Python将SVG转换为PDF 使用Python将SVG添加到现有PDF中 使用Python将PDF转换为SVG 使用Python将PDF的特定页面转换为SVG SVG(可缩放矢量图形)和PDF(便携式文档格式)是两种常见且广泛使用的文件格式。SVG是…...
idea整合deepseek实现AI辅助编程
1.File->Settings 2.安装插件codegpt 3.注册deepseek开发者账号,DeepSeek开放平台 4.按下图指示创建API KEY 5.回到idea配置api信息,File->Settings->Tools->CodeGPT->Providers->Custom OpenAI API key填写deepseek的api key Chat…...
java文件上传粗糙版
粗糙版图片上传 1.导入依赖 <dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.2</version> </dependency> 2.配置minio地址跟对应的桶 业务层实现类 import io.minio.MinioClient; /…...
一种基于Leaflet.Legend的图例动态更新方法
目录 前言 一、场景再现 1、需求描述 2、核心方法介绍 3、存在的问题 二、问题解决 1、重复解决办法 2、图例不展示解决办法 3、成果展示 三、总结 前言 在当今数字化时代,地理信息系统(GIS)技术已经广泛应用于各个领域,…...
Vue Dom截图插件,截图转Base64 html2canvas
安装插件 npm install html2canvas --save插件使用 <template><div style"padding: 10px;"><div ref"imageTofile" class"box">发生什么事了</div><button click"toImage" style"margin: 10px;&quo…...
安宝特方案 | AR眼镜:远程医疗的“时空折叠者”,如何为生命争夺每一分钟?
行业痛点:当“千里求医”遇上“资源鸿沟” 20世纪50年代,远程会诊的诞生曾让医疗界为之一振——患者不必跨越山河,专家无需舟车劳顿,一根电话线、一张传真纸便能架起问诊的桥梁。然而,传统远程医疗的局限也日益凸显&a…...
【人工智能】Python中的序列到序列(Seq2Seq)模型:实现机器翻译
《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 序列到序列(Seq2Seq)模型是自然语言处理(NLP)中一项核心技术,广泛应用于机器翻译、语音识别、文本摘要等任务。本文深入探讨Seq2Seq模…...
【批量获取图片信息】批量获取图片尺寸、海拔、分辨率、GPS经纬度、面积、位深度、等图片属性里的详细信息,提取出来后导出表格,基于WPF的详细解决方案
摄影工作室通常会有大量的图片素材,在进行图片整理和分类时,需要知道每张图片的尺寸、分辨率、GPS 经纬度(如果拍摄时记录了)等信息,以便更好地管理图片资源,比如根据图片尺寸和分辨率决定哪些图片适合用于…...
关于32位和64位程序的传参方法及虚拟机调试工具总结
一、传参方法对比 1. 32位程序 系统调用 (Linux) 使用int 0x80指令触发系统调用 寄存器传参顺序: eax 系统调用号 ebx 第1个参数 ecx 第2个参数 edx 第3个参数 esi 第4个参数 edi 第5个参数 普通函数调用 (C语言) 栈传递参数:参数从右向左压栈…...
【Windows】PowerShell 缓存区大小调节
PowerShell 缓存区大小调节 方式1 打开powershell 窗口属性调节方式2,修改 PowerShell 配置文件 方式1 打开powershell 窗口属性调节 打开 CMD(按 Win R,输入 cmd)。右键标题栏 → 选择 属性(Properties)…...
查看云机器的一些常用配置
云原生学习路线导航页(持续更新中) kubernetes学习系列快捷链接 Kubernetes架构原则和对象设计(一)Kubernetes架构原则和对象设计(二)Kubernetes架构原则和对象设计(三)Kubernetes常…...
约克VRF|冬日舒适新标杆,温暖每一寸空间
冬天来了,谁不想窝在家里,一边温暖舒适,一边畅享清新空气? 约克VRF中央空调——用科技为你打造全方位的冬季理想生活! 地暖空调二合一,暖从足起,养生更健康~ 普通取…...
【AI学习】关于 DeepSeek-R1的几个流程图
遇见关于DeepSeek-R1的几个流程图,清晰易懂形象直观,记录于此。 流程图一 来自文章《Understanding Reasoning LLMs》, 文章链接:https://magazine.sebastianraschka.com/p/understanding-reasoning-llms?continueFlagaf07b1a0…...
CNN卷积神经网络多变量多步预测,光伏功率预测(Matlab完整源码和数据)
代码地址:CNN卷积神经网络多变量多步预测,光伏功率预测(Matlab完整源码和数据) 标题:CNN卷积神经网络多变量多步预测,光伏功率预测 一、引言 1.1 研究背景及意义 随着全球能源危机的加剧和环保意识的提升ÿ…...
mapbox进阶,添加绘图扩展插件,绘制圆形
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:mapbox 从入门到精通 文章目录 一、🍀前言1.1 ☘️mapboxgl.Map 地图对象1.2 ☘️mapboxgl.Map style属性1.3 ☘️MapboxDraw 绘图控件二、🍀添加绘图扩…...
学习TCL脚本基础语法的几个步骤?
文章目录 前言1. 命令和参数1.1 Tcl 命令的基本结构1.2 示例1.2.1 puts 命令1.2.2 set 命令1.2.3 if 命令1.2.4 foreach 命令 1.3 参数的类型1.3.1 字符串1.3.2 变量1.3.3 表达式1.3.4 列表1.3.5 字典 1.4 命令的嵌套 二、变量1. 声明变量2. 使用变量3. 变量类型3.1 字符串3.2 …...
move_base全局路径规划震荡之参数调优
在使用 move_base 进行导航时,如果全局路径规划在遇到障碍物时频繁在障碍物左右两侧跳变,导致机器人绕障失败,通常可以通过调整参数优化来解决。以下是具体原因分析和解决方案: 问题原因分析: 全局路径规划的震荡&…...
Could not create task ‘:mainActivity:minifyReleaseWithR8‘.
最近接收了一个老项目,把项目clone下来后,总是报错,无法运行 Build-tool 33.0.1 is missing DX at D:\Android\Sdk\build-tools\33.0.1\dx.batFAILURE: Build failed with an exception.* What went wrong: Could not determine the depende…...
保姆级教程Docker部署Zookeeper官方镜像
目录 1、安装Docker及可视化工具 2、创建挂载目录 3、运行Zookeeper容器 4、Compose运行Zookeeper容器 5、查看Zookeeper运行状态 6、验证Zookeeper是否正常运行 1、安装Docker及可视化工具 Docker及可视化工具的安装可参考:Ubuntu上安装 Docker及可视化管理…...
人工智能-A* 算法与机器学习算法结合
以下将为你展示如何将 A* 算法与机器学习算法(这里以简单的神经网络为例)结合实现路径规划。我们会先使用 A* 算法生成一些路径规划数据,然后用这些数据训练一个简单的神经网络,让神经网络学习如何预测路径。最后,将训…...
如何在Python中使用Requests库?
在Python中,网络请求处理是很多应用开发中的常见需求。Requests库作为Python中最常用的第三方库之一,它能够简化HTTP请求的发送和响应的处理。无论是获取网页内容、与API接口交互,还是提交表单数据,Requests都可以帮助开发者轻松实…...
通过命令行运行py文件与通过ide运行py文件,对文件中模块的引用方式的影响
通过命令行运行 Python 文件和通过 IDE 运行 Python 文件时,模块的引用方式 会受到一些影响,主要体现在 工作目录 和 模块导入路径(sys.path)的设置上。下面详细介绍这两种方式的区别和它们如何影响模块引用。 1. 通过命令行运行…...
如何避免大语言模型中涉及丢番图方程的问题
希尔伯特第十问题是一个著名的数学问题,涉及不定方程(又称为丢番图方程)的可解答性。然而在大模型中,我们希望问题都是确定的可解的,或者说要尽可能的想办法避免不确定的不可解问题。由于丢番图方程问题是不可判定问题(即不存在一个有效的算法能够解决该类问题的所有实例…...
重新刷题求职2-DAY7
1.454. 四数相加 II 给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足: 0 < i, j, k, l < nnums1[i] nums2[j] nums3[k] nums4[l] 0 示例 1: 输入&#…...
Day 32 卡玛笔记
这是基于代码随想录的每日打卡 455. 分发饼干 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸ÿ…...
android studio无痛入门
在Android Studio中创建和管理项目主要涉及以下几个步骤: 1. 创建新项目 打开Android Studio,点击“Start a new Android Studio project”或者“File” > “New” > “New Project”。 选择一个模板,例如“Empty Activity”࿰…...
flutter安卓打包签名
flutter安卓打包签名 1.创建签名文件 keytool -genkeypair -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-aliaskeytool 是一个用于管理密钥和证书的命令行工具,通常与 Java 开发工具包 (JDK) 一起使用。my-release-…...
Windows编程:下载与安装 Visual Studio 2019
本节前言 在写作本节的时候,本来呢,我正在写的专栏,是 MFC 专栏。而 VS2010 和 VS2019,正是 MFC 学习与开发中,可以使用的两款软件。然而呢,如果你去学习 Windows API 知识的话,那么࿰…...
ElementUI的<el-image>组件引用网络图片加载失败
1. 验证图片 URL 是否有效 直接访问图片链接,确保 URL 正确且可公开访问 如果浏览器无法加载图片,可能是图片服务器限制了外链或已失效。 解决方法:更换为可用的图片 URL。 2. 检查浏览器开发者工具 打开浏览器开发者工具(F12…...