JAVA中的Stream流的使用详解

devtools/2024/10/17 17:08:05/

1.Stream的介绍

  • Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。
  • Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
  • Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码,它允许开发者以声明式的方式处理集合(如List,Set,Map等)
  • 这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
  • 元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

下面我们来举个例子来感受一下Stream有多优雅

问题:

从给定的语句中返回单词长度大于5的单词列表,按长度倒序进行输出,最多返回3个。

java7以及之前的代码中,我们只能通过这种方式实现

java">    public List<String> sortGetTop3LongWords(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;}

java8及以后的版本中,我们可以用Stream流,让代码变动优雅

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

Stream的类型 

 我们可以对流进行中间操作或者终端操作。小伙伴们可能会疑问?什么是中间操作?什么又是终端操作?

  • :中间操作会再次返回一个流,所以,我们可以链接多个中间操作,注意这里是不用加分号的。上图中的filter 过滤,map 对象转换,sorted 排序,就属于中间操作。
  • :终端操作是对流操作的一个结束动作,一般返回 void 或者一个非流的结果。上图中的 forEach循环 就是一个终止操作。

开始管道

主要负责新建一个Stream流,或者基于现有的数组、List、Set、Map等集合类型对象创建出新的Stream流。 

中间管道 

负责对Stream进行处理操作,并返回一个新的Stream对象,中间管道操作可以进行叠加

 终止管道

顾名思义,通过终止管道操作之后,Stream流将会结束,最后可能会执行某些逻辑处理,或者是按照要求返回某些执行后的结果数据。

Stream方法使用

不同类型的Stream流的使用:

java">Arrays.asList("a1", "a2", "a3").stream() // 创建流.findFirst() // 找到第一个元素.ifPresent(System.out::println);  // 如果存在,即输出// a1

在集合上调用stream()方法会返回一个普通的 Stream 流。但是, 您大可不必刻意地创建一个集合,再通过集合来获取 Stream 流,您还可以通过如下这种方式:

java">Stream.of("a1", "a2", "a3").findFirst().ifPresent(System.out::println);  // a1

例如上面这样,我们可以通过 Stream.of() 从一堆对象中创建 Stream 流。

除了常规对象流之外,Java 8还附带了一些特殊类型的流,用于处理原始数据类型intlong以及double。说到这里,你可能已经猜到了它们就是IntStreamLongStream还有DoubleStream

其中,IntStreams.range()方法还可以被用来取代常规的 for 循环, 如下所示:

java">IntStream.range(1, 4).forEach(System.out::println); // 相当于 for (int i = 1; i < 4; i++) {}// 1
// 2
// 3

上面这些原始类型流的工作方式与常规对象流基本是一样的,但还是略微存在一些区别:

  • 原始类型流使用其独有的函数式接口,例如IntFunction代替FunctionIntPredicate代替Predicate

  • 原始类型流支持额外的终端聚合操作,sum()以及average(),如下所示:

java">Arrays.stream(new int[] {1, 2, 3}).map(n -> 2 * n + 1) // 对数值中的每个对象执行 2*n + 1 操作.average() // 求平均值.ifPresent(System.out::println);  // 如果值不为空,则输出
// 5.0

但是,偶尔我们也有这种需求,需要将常规对象流转换为原始类型流,这个时候,中间操作 mapToInt()mapToLong() 以及mapToDouble就派上用场了:

java">Stream.of("a1", "a2", "a3").map(s -> s.substring(1)) // 对每个字符串元素从下标1位置开始截取.mapToInt(Integer::parseInt) // 转成 int 基础类型类型流.max() // 取最大值.ifPresent(System.out::println);  // 不为空则输出// 3

如果说,您需要将原始类型流装换成对象流,您可以使用 mapToObj()来达到目的:

java">IntStream.range(1, 4).mapToObj(i -> "a" + i) // for 循环 1->4, 拼接前缀 a.forEach(System.out::println); // for 循环打印// a1
// a2
// a3

下面是一个组合示例,我们将双精度流首先转换成 int 类型流,然后再将其装换成对象流:

java">Stream.of(1.0, 2.0, 3.0).mapToInt(Double::intValue) // double 类型转 int.mapToObj(i -> "a" + i) // 对值拼接前缀 a.forEach(System.out::println); // for 循环打印// a1
// a2
// a3

Stream流的顺序处理

在讨论处理顺序之前,您需要明确一点,那就是中间操作的有个重要特性 —— 延迟性。中间操作不会立即执行,它们是惰性化的,这意味着它们会在最终操作(如 collect、forEach 等)触发时才执行。这种延迟执行的策略可以提高性能,因为只有在需要最终结果时才会进行计算。

观察下面这个没有终端操作的示例代码:

java">Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> {System.out.println("filter: " + s);return true;});

 执行此代码段时,您可能会认为,将依次打印 "d2", "a2", "b1", "b3", "c" 元素。然而当你实际去执行的时候,它不会打印任何内容。

为什么呢?

原因是:当且仅当存在终端操作时,中间操作操作才会被执行。

是不是不信?接下来,对上面的代码添加 forEach终端操作:

java">Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> {System.out.println("filter: " + s);return true;}).forEach(s -> System.out.println("forEach: " + s));

再次执行,我们会看到输出如下: 

java">filter:  d2
forEach: d2
filter:  a2
forEach: a2
filter:  b1
forEach: b1
filter:  b3
forEach: b3
filter:  c
forEach: c

输出的顺序可能会让你很惊讶!你脑海里肯定会想,应该是先将所有 filter 前缀的字符串打印出来,接着才会打印 forEach 前缀的字符串。

事实上,输出的结果却是随着链条垂直移动的。比如说,当 Stream 开始处理 d2 元素时,它实际上会在执行完 filter 操作后,再执行 forEach 操作,接着才会处理第二个元素。

是不是很神奇?为什么要设计成这样呢?

原因是出于性能的考虑。这样设计可以减少对每个元素的实际操作数,看完下面代码你就明白了:

java">Stream.of("d2", "a2", "b1", "b3", "c").map(s -> {System.out.println("map: " + s);return s.toUpperCase(); // 转大写}).anyMatch(s -> {System.out.println("anyMatch: " + s);return s.startsWith("A"); // 过滤出以 A 为前缀的元素});// map:      d2
// anyMatch: D2
// map:      a2
// anyMatch: A2

终端操作 anyMatch()表示任何一个元素以 A 为前缀,返回为 true,就停止循环。所以它会从 d2 开始匹配,接着循环到 a2 的时候,返回为 true ,于是停止循环。

由于数据流的链式调用是垂直执行的,map这里只需要执行两次。相对于水平执行来说,map会执行尽可能少的次数,而不是把所有元素都 map 转换一遍。

中间操作顺序这么重要?

下面的例子由两个中间操作mapfilter,以及一个终端操作forEach组成。让我们再来看看这些操作是如何执行的:

java">Stream.of("d2", "a2", "b1", "b3", "c").map(s -> {System.out.println("map: " + s);return s.toUpperCase(); // 转大写}).filter(s -> {System.out.println("filter: " + s);return s.startsWith("A"); // 过滤出以 A 为前缀的元素}).forEach(s -> System.out.println("forEach: " + s)); // for 循环输出// map:     d2
// filter:  D2
// map:     a2
// filter:  A2
// forEach: A2
// map:     b1
// filter:  B1
// map:     b3
// filter:  B3
// map:     c
// filter:  C

mapfilter会对集合中的每个字符串调用五次,而forEach却只会调用一次,因为只有 "a2" 满足过滤条件。

如果我们改变中间操作的顺序,将filter移动到链头的最开始,就可以大大减少实际的执行次数:

java">Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> {System.out.println("filter: " + s)return s.startsWith("a"); // 过滤出以 a 为前缀的元素}).map(s -> {System.out.println("map: " + s);return s.toUpperCase(); // 转大写}).forEach(s -> System.out.println("forEach: " + s)); // for 循环输出// filter:  d2
// filter:  a2
// map:     a2
// forEach: A2
// filter:  b1
// filter:  b3
// filter:  c

 现在,map仅仅只需调用一次,性能得到了提升,这种小技巧对于流中存在大量元素来说,是非常很有用的。

接下来,让我们对上面的代码再添加一个中间操作sorted

java">Stream.of("d2", "a2", "b1", "b3", "c").sorted((s1, s2) -> {System.out.printf("sort: %s; %s\n", s1, s2);return s1.compareTo(s2); // 排序}).filter(s -> {System.out.println("filter: " + s);return s.startsWith("a"); // 过滤出以 a 为前缀的元素}).map(s -> {System.out.println("map: " + s);return s.toUpperCase(); // 转大写}).forEach(s -> System.out.println("forEach: " + s)); // for 循环输出

 sorted 是一个有状态的操作,因为它需要在处理的过程中,保存状态以对集合中的元素进行排序。

执行上面代码,输出如下:

java">sort:    a2; d2
sort:    b1; a2
sort:    b1; d2
sort:    b1; a2
sort:    b3; b1
sort:    b3; d2
sort:    c; b3
sort:    c; d2
filter:  a2
map:     a2
forEach: A2
filter:  b1
filter:  b3
filter:  c
filter:  d2

咦咦咦?这次怎么又不是垂直执行了。你需要知道的是,sorted是水平执行的。因此,在这种情况下,sorted会对集合中的元素组合调用八次。这里,我们也可以利用上面说道的优化技巧,将 filter 过滤中间操作移动到开头部分:

java">Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> {System.out.println("filter: " + s);return s.startsWith("a");}).sorted((s1, s2) -> {System.out.printf("sort: %s; %s\n", s1, s2);return s1.compareTo(s2);}).map(s -> {System.out.println("map: " + s);return s.toUpperCase();}).forEach(s -> System.out.println("forEach: " + s));// filter:  d2
// filter:  a2
// filter:  b1
// filter:  b3
// filter:  c
// map:     a2
// forEach: A2

 从上面的输出中,我们看到了 sorted从未被调用过,因为经过filter过后的元素已经减少到只有一个,这种情况下,是不用执行排序操作的。因此性能被大大提高了。

数据流复用问题

Java8 Stream 流是不能被复用的,一旦你调用任何终端操作,流就会关闭:

java">Stream<String> stream =Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("a"));stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception

当我们对 stream 调用了 anyMatch 终端操作以后,流即关闭了,再调用 noneMatch 就会抛出异常:

java">java.lang.IllegalStateException: stream has already been operated upon or closedat java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)at com.winterbe.java8.Streams5.test7(Streams5.java:38)at com.winterbe.java8.Streams5.main(Streams5.java:28)

为了克服这个限制,我们必须为我们想要执行的每个终端操作创建一个新的流链,例如,我们可以通过 Supplier 来包装一下流,通过 get() 方法来构建一个新的 Stream 流,如下所示:

java">Supplier<Stream<String>> streamSupplier =() -> Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("a"));streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

通过构造一个新的流,来避开流不能被复用的限制, 这也是取巧的一种方式。

高级操作     

Streams 支持的操作很丰富,除了上面介绍的这些比较常用的中间操作,如filtermap(参见Stream Javadoc)外。还有一些更复杂的操作,如collectflatMap以及reduce。接下来,就让我们学习一下:   

本小节中的大多数代码示例均会使用以下 List<Person>进行演示 

java">class Person {String name;int age;Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return name;}
}// 构建一个 Person 集合
List<Person> persons =Arrays.asList(new Person("Max", 18),new Person("Peter", 23),new Person("Pamela", 23),new Person("David", 12));

Collect

collect 是一个非常有用的终端操作,它可以将流中的元素转变成另外一个不同的对象,例如一个ListSetMap。collect 接受入参为Collector(收集器),它由四个不同的操作组成:

供应器(supplier)、累加器(accumulator)、组合器(combiner)和终止器(finisher)。

这些都是个啥?别慌,看上去非常复杂的样子,但好在大多数情况下,您并不需要自己去实现收集器。因为 Java 8通过Collectors类内置了各种常用的收集器,你直接拿来用就行了。

让我们先从一个非常常见的用例开始:

java">List<Person> filtered =persons.stream() // 构建流.filter(p -> p.name.startsWith("P")) // 过滤出名字以 P 开头的.collect(Collectors.toList()); // 生成一个新的 ListSystem.out.println(filtered);    // [Peter, Pamela]

你也看到了,从流中构造一个 List 异常简单。如果说你需要构造一个 Set 集合,只需要使用Collectors.toSet()就可以了。

接下来这个示例,将会按年龄对所有人进行分组:

java">Map<Integer, List<Person>> personsByAge = persons.stream().collect(Collectors.groupingBy(p -> p.age)); // 以年龄为 key,进行分组personsByAge.forEach((age, p) -> System.out.format("age %s: %s\n", age, p));// age 18: [Max]
// age 23: [Peter, Pamela]
// age 12: [David]

 除了上面这些操作。您还可以在流上执行聚合操作,例如,计算所有人的平均年龄:

java">Double averageAge = persons.stream().collect(Collectors.averagingInt(p -> p.age)); // 聚合出平均年龄System.out.println(averageAge);     // 19.0

如果您还想得到一个更全面的统计信息,摘要收集器可以返回一个特殊的内置统计对象。通过它,我们可以简单地计算出最小年龄、最大年龄、平均年龄、总和以及总数量。

java">IntSummaryStatistics ageSummary =persons.stream().collect(Collectors.summarizingInt(p -> p.age)); // 生成摘要统计System.out.println(ageSummary);
// IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}

 下一个这个示例,可以将所有人名连接成一个字符串:

java">String phrase = persons.stream().filter(p -> p.age >= 18) // 过滤出年龄大于等于18的.map(p -> p.name) // 提取名字.collect(Collectors.joining(" and ", "In Germany ", " are of legal age.")); // 以 In Germany 开头,and 连接各元素,再以 are of legal age. 结束System.out.println(phrase);
// In Germany Max and Peter and Pamela are of legal age.

 连接收集器的入参接受分隔符,以及可选的前缀以及后缀。

对于如何将流转换为 Map集合,我们必须指定 Map 的键和值。这里需要注意,Map 的键必须是唯一的,否则会抛出IllegalStateException 异常。

你可以选择传递一个合并函数作为额外的参数来避免发生这个异常:

java">Map<Integer, String> map = persons.stream().collect(Collectors.toMap(p -> p.age,p -> p.name,(name1, name2) -> name1 + ";" + name2)); // 对于同样 key 的,将值拼接System.out.println(map);
// {18=Max, 23=Peter;Pamela, 12=David}

既然我们已经知道了这些强大的内置收集器,接下来就让我们尝试构建自定义收集器吧。

比如说,我们希望将流中的所有人转换成一个字符串,包含所有大写的名称,并以|分割。为了达到这种效果,我们需要通过Collector.of()创建一个新的收集器。同时,我们还需要传入收集器的四个组成部分:供应器、累加器、组合器和终止器。

在 Java 8 引入的 Stream API 中,供应器(Supplier)、累加器(Accumulator)、组合器(Combiner)和终止器(Terminators)是实现流操作的关键组件。下面是对这些组件的简要介绍:
1.  供应器(Supplier):
供应器是一个提供新元素的函数接口。在 Stream API 中,供应器通常用于生成流的初始元素。例如,使用 Stream.generate(Supplier<T> s) 方法可以创建一个无限流,其中 s 是一个供应器,每次调用都会生成一个新的元素。


2.  累加器(Accumulator):
累加器是一个接受两个参数(当前元素和流中的下一个元素)并返回一个新值的函数接口。它用于将两个值合并为一个单一的结果,这个结果可以是一个新的对象或者是一个累加值。在 Stream API 中,累加器通常与 reduce 方法一起使用,用于将流中的所有元素累积成一个单一的结果。


3.  组合器(Combiner):
组合器是一个将两个累加的结果合并为一个单一结果的函数接口。在并行流操作中,组合器用于将不同线程中的结果合并起来。例如,在 reduce 方法中,如果流是并行的,每个线程都会独立地进行累加操作,然后使用组合器将这些累加的结果合并为最终结果。


4.  终止器(Terminators):
终止器是 Stream API 中的终端操作,它们会消耗流的元素以产生一个结果或者副作用。终止操作包括但不限于以下几种:
•  forEach:对流中的每个元素执行给定的操作。
•  reduce:将流中的元素反复应用一个累加器函数,得到一个单一的结果。
•  collect:将流中的元素收集到一个新集合中,可以使用 Collector 接口来自定义收集逻辑。
•  min 和 max:找到流中最小或最大的元素。
•  count:返回流中元素的数量。
•  anyMatch、allMatch、noneMatch:基于条件测试流中的元素,并返回布尔值结果。

java">Collector<Person, StringJoiner, String> personNameCollector =Collector.of(() -> new StringJoiner(" | "),          // supplier 供应器(j, p) -> j.add(p.name.toUpperCase()),  // accumulator 累加器(j1, j2) -> j1.merge(j2),               // combiner 组合器StringJoiner::toString);                // finisher 终止器String names = persons.stream().collect(personNameCollector); // 传入自定义的收集器System.out.println(names);  // MAX | PETER | PAMELA | DAVID
  •  由于Java 中的字符串是 final 类型的,我们需要借助辅助类StringJoiner,来帮我们构造字符串。
  • 最开始供应器使用分隔符构造了一个StringJointer
  • 累加器用于将每个人的人名转大写,然后加到StringJointer中。
  • 组合器将两个StringJointer合并为一个。
  • 最终,终结器从StringJointer构造出预期的字符串。

map与flatMap

mapflatMap都是用于转换已有的元素为其它元素,区别点在于:

  • map 必须是一对一的,即每个元素都只能转换为1个新的元素
  • flatMap 可以是一对多的,即每个元素都可以转换为1个或者多个新的元素

 比如:有一个字符串ID列表,现在需要将其转为User对象列表。可以使用map来实现:

用Stream来表达:

java">
/*** 演示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);
}

执行之后,会发现每一个元素都被转换为对应新的元素,但是前后总元素个数是一致的

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

再比如:现有一个句子列表,需要将句子中每个单词都提取出来得到一个所有单词列表。这种情况用map就搞不定了,需要flatMap上场了:

java">import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;public class FlatMapExample {public static void main(String[] args) {List<String> sentences = Arrays.asList("hello world", "Jia Gou Wu Dao");List<String> results = new ArrayList<>();for (String sentence : sentences) {// 分割句子为单词String[] words = sentence.split(" ");// 将分割后的数组添加到结果列表中for (String word : words) {results.add(word);}}System.out.println(results);}
}

用Stream表达式: 

java">
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);
}

 执行结果如下,可以看到结果列表中元素个数是比原始列表元素个数要多的:

java">[hello, world, Jia, Gou, Wu, Dao]

这里需要补充一句,flatMap操作的时候其实是先每个元素处理并返回一个新的Stream,然后将多个Stream展开合并为了一个完整的新的Stream,如下:        

peek和foreach方法

peekforeach,都可以用于对元素进行遍历然后逐个的进行处理。

但根据前面的介绍,peek属于中间方法,而foreach属于终止方法。这也就意味着peek只能作为管道中途的一个处理步骤,而没法直接执行得到结果,其后面必须还要有其它终止操作的时候才会被执行;而foreach作为无返回值的终止方法,则可以直接执行相关操作。

java">    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----");
}

输出结果可以看出,peek独自调用时并没有被执行、但peek后面加上终止操作之后便可以被执行,而foreach可以直接被执行:

java">----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----

并行流

使用并行流,可以有效利用计算机的多CPU硬件,提升逻辑的执行速度。并行流通过将一整个stream划分为多个片段,然后对各个分片流并行执行处理逻辑,最后将各个分片流的执行结果汇总为一个整体流。


http://www.ppmy.cn/devtools/99297.html

相关文章

【CAN总线测试】——CAN物理层测试

从0开始学习CANoe使用 从0开始学习车载测试 相信时间的力量 星光不负赶路者&#xff0c;时光不负有心人。 目录 1.最小通讯电压测试 2.最大通讯电压测试 3.显性位/隐性位输出电压测试 4.信号跳变沿测试 5.地偏移 6.终端电阻 1.7. CANH/CANL短路 1.8. CANH /GND短路 …

(24)(24.4) MultiWii/DJI/HDZero OSD (version 4.2 and later)(二)

文章目录 前言 2 未经WTFOS改装的DJI 2/V1/V2护目镜 3 参数说明 前言 除了基于本地 MSP 遥测的 OSD 显示外&#xff0c;在后来的型号中还添加了“自定义OSD”功能&#xff0c;允许显示和定位 ArduPilot 提供的任何或所有 OSD 信息面板以及警告、单位等。 2 未经WTFOS改装的…

EXCEL 分组后找出满足条件的行拼接起来

Excel某表格有四列&#xff0c;其中第2列是分组列。 ABCD11a1yet22a2done33a3yet44b1done55b2done66b3done77b4yet88b5done 现在要按第2列分组&#xff0c;找到每组第4列等于"done"的行&#xff0c;将这些行的第3列用逗号拼起来&#xff0c;再与分组名、行号组成新…

opencv中Core中的Norm函数解释

1. Norm的类型 NORM_L1&#xff1a; L1 范数&#xff08;曼哈顿范数&#xff09;。数组中所有元素绝对值之和。 NORM_L2&#xff1a; L2 范数&#xff08;欧几里得范数&#xff09;。数组中所有元素平方和的平方根。 NORM_INF&#xff1a;无穷范数&#xff08;最大绝对值范数&…

页面设计任务 商品详情页

目录 成品: 任务描述 源码&#xff1a; 详细讲解&#xff1a; 1.导航栏讲解 2.主体部分 3.图像部分 4.评分部分 5.按钮部分 6.配置信息部分 7.响应式设计 成品: 任务描述 创建一个产品展示页面&#xff0c;包括以下内容&#xff1a; 网页结构&#xff1a;使用 HTM…

stable diffusion和GAN网络的区别,优点缺点是什么

稳定扩散&#xff08;stable diffusion&#xff09;和生成对抗网络&#xff08;GAN&#xff09;是两种不同的深度学习方法。 稳定扩散是一种无监督学习方法&#xff0c;用于图像超分辨率重建。它基于扩散过程模型&#xff0c;通过在不同的时间步骤中对图像进行重建来增加分辨率…

在Flux和Ideogram 2.0的竞争压力下,Midjourney每日开放25张免费额度

在数字化浪潮的推动下&#xff0c;人工智能&#xff08;AI&#xff09;正成为塑造未来的关键力量。硅纪元视角栏目紧跟AI科技的最新发展&#xff0c;捕捉行业动态&#xff1b;提供深入的新闻解读&#xff0c;助您洞悉技术背后的逻辑&#xff1b;汇聚行业专家的见解&#xff0c;…

Spring-SpringUtils工具类

SpringUtils 通常是一个工具类,用于辅助与 Spring 框架相关的操作。它包含了多种方法,以方便 地从 Spring 容器中获取 Bean、处理事务、进行反射操作等。 下面是一个较为全面的 SpringUtils 工具类实现示例: import org.springframework.aop.framework.AopContext; impor…