Java8流式处理(Stream)使用详解
一、概述
Jdk 8
新增的Stream
,配合Lambda
,给操作集合(Collection
)提供了极大的便利。
那么什么是Stream
?Stream
将要处理的元素集合看作一种流,在流的过程中,借助Stream API
对流中的元素进行操作,比如:筛选、排序、聚合等。
Stream
可以由数组或集合创建,对流的操作分为两种:
- 中间操作,每次返回一个新的流,可以有多个。
- 终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。
另外,Stream
有几个特性:
stream
不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。stream
不会改变数据源,通常情况下会产生一个新的集合或一个值。stream
具有延迟执行特性,只有调用终端操作时,中间操作才会执行。
二、常用方法
2.1 创建
Stream
有三种创建方式
- 通过
java.util.Collection.stream()
方法用集合创建流。 - 使用
java.util.Arrays.stream(T[] array)
方法用数组创建流。 - 使用
Stream
的静态方法:of()
、iterate()
、generate()
。
2.1.1 通过集合创建流
List<String> list = Arrays.asList("a", "b", "c");
// 创建一个顺序流
Stream<String> stream = list.stream();
// 创建一个并行流
Stream<String> parallelStream = list.parallelStream();
- 串行流:适合存在线程安全问题、阻塞任务、重量级任务,以及需要使用同一事务的逻辑。
- 并行流:适合没有线程安全问题、较单纯的数据处理任务。
2.1.2 使用数组创建流
int[] array = {1, 3, 5, 6, 8};
IntStream stream = Arrays.stream(array);
2.1.3 使用静态方法
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4);
stream2.forEach(System.out::println);
Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println);
输出结果:
0 3 6 9
0.6796156909271994
0.1914314208854283
0.8116932592396652
stream
和parallelStream
的简单区分:stream
是顺序流,由主线程按顺序对流执行操作,而parallelStream
是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。例如: 筛选集合中的奇数,两者的处理不同之处: 如果流中的数据量足够大,并行流可以加快处速度。
除了直接创建并行流,还可以通过parallel()
把顺序流转换成并行流:
Optional<Integer> findFirst = list.stream().parallel().filter(x -> x > 6).findFirst();
关于更多的Parallel Stream
,请参考:Java Parallel Stream和Java Stream之Parallel Streams编程指南
2.2 中间操作
2.2.1 筛选与切片
- filter(Predicate): 筛选流中某些元素。
- limit(long val): 截断流,取流中前
val
个元素。 - skip(n): 跳过
n
元素,配合limit(n)
可实现分页。 - distinct(): 通过流所生成元素的
equals
和hashCode
去重。
2.2.2 映射
- map(Function f): 接收流中元素,并且将其映射成为新元素,例如:从
student
对象中取name
属性。 - flatMap(Function f): 将所有流中的元素并到一起连接成一个流。
2.2.3 消费
- peek(Consumer c): 获取流中元素,操作流中元素,与
foreach
不同的是不会截断流,可继续操作。
使用场景:当遍历完数组后还有后续操作时或list数组转stream时,不适合在用Iterable的foreach循环,这个时候peek就派上用场了。
2.2.4 排序
- sorted()/sorted(Comparator): 产生一个新流,按照自然顺序/比较器规则排序。
2.3 终止操作
2.3.1 匹配/聚合操作
匹配
- allMatch(Predicate): 当流中每个元素都符合该断言时才返回
true
,否则返回false
。 - noneMatch(Predicate): 当流中每个元素都不符合该断言时才返回
true
,否则返回false
。 - anyMatch(Predicate): 只要流中有一个元素满足该断言则返回
true
,否则返回false
。
寻找元素
- findFirst(): 返回流中第一个元素。
- findAny(): 返回流中的任意元素。
计数和极值
- count(): 返回流中元素的总个数。
- max(): 返回流中元素最大值。
- min():返回流中元素最小值。
2.3.2 归约操作
- Optional
reduce(BinaryOperator accumulator): 第一次执行时, accumulator
函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素;依次类推。 - T reduce(T identity, BinaryOperator
accumulator): 流程跟上面一样,只是第一次执行时, accumulator
函数的第一个参数为identity
,而第二个参数为流中的第一个元素。 - U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator combiner): 在串行流(
stream
)中,该方法跟第二个方法一样,即第三个参数combiner
不会起作用。在并行流(parallelStream
)中,我们知道流被fork join
出多个线程进行执行,此时每个线程的执行流程就跟第二个方法reduce(identity, accumulator)
一样,而第三个参数combiner
函数,则是将每个线程的执行结果当成一个新的流,然后使用第一个方法reduce(accumulator)
流程进行归约。
2.3.3 收集操作
collect
: 接收一个Collector
实例,将流中元素收集成另外一个数据结构。
Collector<T, A, R>
是一个接口,有以下5
个抽象方法:
- Supplier supplier(): 创建一个结果容器
A
。 - BiConsumer<A, T> accumulator(): 消费型接口,第一个参数为容器
A
,第二个参数为流中元素T
。 - BinaryOperator combiner(): 函数接口,该参数的作用跟上一个方法(
reduce
)中的combiner
参数一样,将并行流中各个子进程的运行结果(accumulator
函数操作后的容器A
)进行合并。 - Function<A, R> finisher(): 函数式接口,参数为:容器
A
,返回类型为:collect
方法最终想要的结果R
。 - Set
characteristics(): 返回一个不可变的 Set
集合,用来表明该Collector
的特征。有以下三个特征:CONCURRENT
: 表示此收集器支持并发。(官方文档还有其他描述,暂时没去探索,故不作过多翻译)UNORDERED
: 表示该收集操作不会保留流中元素原有的顺序。IDENTITY_FINISH
: 表示finisher
参数只是标识而已,可忽略。
collect
主要依赖java.util.stream.Collectors
类内置的静态方法。Collectors
具体方法如下:
归集
- toList(): 将元素收集到一个新的
List
。 - toMap(): 将元素收集到
Map
中,Map
其键和值是将提供的映射函数应用于元素的结果。 - toSet(): 将元素收集到一个新的
Set
。 - toCollection(): 将元素
Collection
按遇到顺序收集到一个new
中。 - toConcurrentMap(): 将元素收集到
ConcurrentMap
的并发对象,其键和值是将提供的映射函数应用于元素的结果。 - toUnmodifiableList(): 将元素收集到一个不可修改的
List
集合中。任何修改List
集合的操作都将导致UnsupportedOperationException
。 - toUnmodifiableSet(): 将元素收集到一个不可修改的
Set
集合中。任何修改Set
集合的操作都将导致UnsupportedOperationException
。
统计
- counting(): 返回计算元素数量,如果没有元素,则结果为
0
。 - averagingDouble(): 应用于元素的
double
值函数的算术平均值。如果没有元素,则结果为0
。 - averagingInt(): 应用于元素的
int
值函数的算术平均值。 - averagingLong(): 应用于元素的
long
值函数的算术平均值。 - summarizingDouble(): 将生成
double
映射函数应用于每个元素,并返回结果值的汇总统计信息。 - summarizingInt(): 将生成
int
映射函数应用于每个元素,并返回结果值的汇总统计信息。 - summarizingLong(): 将生成
long
映射函数应用于每个元素,并返回结果值的汇总统计信息。 - summingDouble(): 应用于元素的
double
值函数的总和。 - summingInt(): 应用于元素的
int
值函数的总和。 - summingLong(): 应用于元素的
long
值函数的总和。 - maxBy(): 根据给定的比较器产生最大元素。
- minBy(): 根据给定的比较器产生最小元素。
分组
- groupingBy(): 根据分类函数对元素进行分组,并返回结果
Map
。 - groupingByConcurrent(): 并发执行,根据分类函数对元素进行分组。
- partitioningBy(): 对元素进行分区
Predicate
,并将它们组织成Map<Boolean, List<T>>
。
接合
- joining(): 按遇到顺序将元素连接成
String
。
归约
- reducing(): 在指定的
BinaryOperator
下执行其元素的缩减。
映射
- mapping(): 通过在累加之前对每个元素应用映射函数。
归纳
- collectingAndThen(): 调整
Collector
执行其它的结束转换。
并集
- teeing():
Jdk 12
添加了一个新的teeing
方法。用于复杂的聚合操作。
三、使用案例
在使用Stream
之前,先理解一个概念:Optional
,Optional
类是一个可以为null
的容器对象。如果值存在则isPresent()
方法会返回true
,调用get()
方法会返回该对象。
案例使用的员工类,这是后面案例中使用的员工类:
private static List<Employee> init() {
List<Employee> employeeList = new ArrayList<>();
employeeList.add(new Employee("Tom", 8900, 23, "male", "New York"));
employeeList.add(new Employee("Jack", 7000, 25, "male", "Washington"));
employeeList.add(new Employee("Lily", 7800, 21, "female", "Washington"));
employeeList.add(new Employee("Anni", 8200, 24, "female", "New York"));
employeeList.add(new Employee("Owen", 9500, 25, "male", "New York"));
employeeList.add(new Employee("Alisa", 7900, 26, "female", "New York"));
return employeeList;
}
@Data
@AllArgsConstructor
class Employee {
private String name; // 姓名
private int salary; // 薪资
private int age; // 年龄
private String sex; // 性别
private String city; // 城市
}
3.1 遍历/匹配(foreach/find/match)
Stream
也是支持类似集合的遍历和匹配元素的,只是Stream
中的元素是以Optional
类型存在的。Stream
的遍历、匹配非常简单。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 9, 3, 8, 2, 1);
// 匹配第一个
Optional<Integer> findFirst = list.stream().filter(x -> x > 6).findFirst();
// 匹配任意(适用于并行流)
Optional<Integer> findAny = list.parallelStream().filter(x -> x > 6).findAny();
// 是否包含符合特定条件的元素
boolean anyMatch = list.stream().anyMatch(x -> x > 6);
System.out.println("匹配第一个值:" + findFirst.get());
System.out.println("匹配任意一个值:" + findAny.get());
System.out.println("是否存在大于6的值:" + anyMatch);
}
}
运行结果:
匹配第一个值:7
匹配任意一个值:8
是否存在大于6的值:true
一、list.forEach()与list.stream().forEach()区别:
1). list.forEach()
使用增强for
循环。list.stream().forEach()
首先将集合转换为流,然后对集合的流进行迭代。最后调用ReferencePipeline
类的forEach
方法。
public void forEach(Consumer<? super E_OUT> action) {
if (!isParallel()) {
sourceStageSpliterator().forEachRemaining(action);
} else {
super.forEach(action);
}
}
forEachRemaining
方法对集合中剩余的元素进行操作,也就是说只遍历一次集合元素。
2). 当一边遍历一边删除的时候,forEach
能够快速失败,而stream().forEach()
只有等到数组遍历完之后才会抛异常。
二、增强for
循环与stream().forEach区别:
1). 普通for
循环和增强for
循环: break
跳出整个for
循环,使用continue
跳出本次循环。
2). stream.forEach()
循环:
- 处理集合时不能使用
break
和continue
中止循环; - 可以使用关键字
return
跳出本次循环,并执行下一次遍历。 - 不能跳出整个
forEach
的循环。
3.2 筛选(filter)
筛选,是按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作。
案例一:筛选出Integer
集合中大于7
的元素,并打印出来。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(6, 7, 3, 8, 1, 2, 9);
Stream<Integer> stream = list.stream();
stream.filter(x -> x > 7).forEach(System.out::println);
}
}
预期结果:
8 9
案例二:筛选员工中工资高于8000
的人,并形成新的集合。形成新集合依赖collect
(收集)。
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
// 高于8000的员工姓名
List<String> filterList = employeeList.stream()
.filter(x -> x.getSalary() > 8000)
.map(Employee::getName).collect(Collectors.toList());
System.out.println("高于8000的员工姓名:" + filterList);
}
}
运行结果:
高于8000的员工姓名:[Tom, Anni, Owen]
3.3 聚合(max/min/count)
max
、min
、count
这些字眼你一定不陌生,没错,在mysql
中我们常用它们进行数据统计。Java stream
中也引入了这些概念和用法,极大地方便了我们对集合、数组的数据统计工作。
案例一:获取String
集合中最长的元素。
public class StreamTest {
public static void main(String[] args) {
List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd");
// 比较
Optional<String> max = list.stream().max(Comparator.comparing(String::length));
System.out.println("最长的字符串:" + max.get());
}
}
输出结果:
最长的字符串:weoujgsd
案例二:获取Integer
集合中的最大值。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 9, 4, 11, 6);
// 自然排序
Optional<Integer> max = list.stream().max(Integer::compareTo);
// 自定义排序
Optional<Integer> max2 = list.stream().max(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
System.out.println("自然排序的最大值:" + max.get());
System.out.println("自定义排序的最大值:" + max2.get());
}
}
输出结果:
自然排序的最大值:11
自定义排序的最大值:11
案例三:获取员工工资最高的人。
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
// 员工工资最大值
Optional<Employee> max = employeeList.stream()
.max(Comparator.comparingInt(Employee::getSalary));
System.out.println("员工工资最大值:" + max.get().getSalary());
}
}
输出结果:
员工工资最大值:9500
案例四:计算Integer
集合中大于6
的元素的个数。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 4, 8, 2, 11, 9);
// list中大于6的元素个数
long count = list.stream().filter(x -> x > 6).count();
System.out.println("list中大于6的元素个数:" + count);
}
}
输出结果:
list中大于6的元素个数:4
3.4 映射(map/flatMap)
映射,可以将一个流的元素按照一定的映射规则映射到另一个流中。分为map
和flatMap
:
map
:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。flatMap
:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
案例一:英文字符串数组的元素全部改为大写。整数数组每个元素加3。
public class StreamTest {
public static void main(String[] args) {
String[] strArr = {"abcd", "bcdd", "defde", "fTr"};
// 数组元素转大写
List<String> strList = Arrays.stream(strArr).map(String::toUpperCase)
.collect(Collectors.toList());
List<Integer> intList = Arrays.asList(1, 3, 5, 7, 9, 11);
// 元素加法
List<Integer> intListNew = intList.stream().map(x -> x + 3).collect(Collectors.toList());
System.out.println("每个元素大写:" + strList);
System.out.println("每个元素 + 3:" + intListNew);
}
}
输出结果:
每个元素大写:[ABCD, BCDD, DEFDE, FTR]
每个元素 + 3:[4, 6, 8, 10, 12, 14]
案例二:将员工的薪资全部增加1000。
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
// 不改变原来员工集合的方式
List<Employee> employeeListNew = employeeList.stream().map(employee -> {
Employee employeeNew = new Employee(employee.getName(), 0, 0, null, null);
employeeNew.setSalary(employee.getSalary() + 10000);
return employeeNew;
}).collect(Collectors.toList());
System.out.println("一次改动前:" + employeeList.get(0).getName()
+ "-->" + employeeList.get(0).getSalary());
System.out.println("一次改动后:" + employeeListNew.get(0).getName()
+ "-->" + employeeListNew.get(0).getSalary());
// 改变原来员工集合的方式
List<Employee> employeeListNew2 = employeeList.stream().map(employee -> {
employee.setSalary(employee.getSalary() + 10000);
return employee;
}).collect(Collectors.toList());
System.out.println("二次改动前:" + employeeList.get(0).getName()
+ "-->" + employeeListNew.get(0).getSalary());
System.out.println("二次改动后:" + employeeListNew2.get(0).getName()
+ "-->" + employeeListNew.get(0).getSalary());
}
}
输出结果:
一次改动前:Tom–>8900
一次改动后:Tom–>18900
二次改动前:Tom–>18900
二次改动后:Tom–>18900
案例三:将两个字符数组合并成一个新的字符数组。
public class StreamTest {
public static void main(String[] args) {
List<String> list = Arrays.asList("m, k, l, a", "1, 3, 5, 7");
List<String> listNew = list.stream().flatMap(s -> {
// 将每个元素转换成一个stream
String[] split = s.split(",");
Stream<String> s2 = Arrays.stream(split);
return s2;
}).collect(Collectors.toList());
System.out.println("处理前的集合:" + list);
System.out.println("处理后的集合:" + listNew);
}
}
输出结果:
处理前的集合:[m-k-l-a, 1-3-5-7]
处理后的集合:[m, k, l, a, 1, 3, 5, 7]
3.5 归约(reduce)
归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。
案例一:求Integer
集合的元素之和、乘积和最大值。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4);
// 求和方式1
Optional<Integer> sum = list.stream().reduce((x, y) -> x + y);
// 求和方式2
Optional<Integer> sum2 = list.stream().reduce(Integer::sum);
// 求和方式3
Integer sum3 = list.stream().reduce(0, Integer::sum);
// 求乘积
Optional<Integer> product = list.stream().reduce((x, y) -> x * y);
// 求最大值方式1
Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y);
// 求最大值写法2
Integer max2 = list.stream().reduce(1, Integer::max);
System.out.println("list求和:" + sum.get() + "," + sum2.get() + "," + sum3);
System.out.println("list求积:" + product.get());
System.out.println("list求和:" + max.get() + "," + max2);
}
}
输出结果:
list求和:29,29,29
list求积:2112
list求和:11,11
案例二:求所有员工的工资之和和最高工资。
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
// 求工资之和方式1:
Optional<Integer> sumSalary = employeeList.stream()
.map(Employee::getSalary).reduce(Integer::sum);
// 求工资之和方式4:
Integer sumSalary4 = employeeList.stream().map(Employee::getSalary).reduce(0, Integer::sum);
// 求工资之和方式2:
Integer sumSalary2 = employeeList.stream()
.reduce(0, (sum, p) -> sum += p.getSalary(), (sum1, sum2) -> sum1 + sum2);
// 求工资之和方式3:
Integer sumSalary3 = employeeList.stream()
.reduce(0, (sum, p) -> sum += p.getSalary(), Integer::sum);
// 求最高工资方式1:
Integer maxSalary = employeeList.stream()
.reduce(0, (max, p) -> max > p.getSalary() ? max : p.getSalary(), Integer::max);
// 求最高工资方式2:
Integer maxSalary2 = employeeList.stream()
.reduce(0, (max, p) -> max > p.getSalary() ? max : p.getSalary(),
(max1, max2) -> max1 > max2 ? max1 : max2);
System.out.println("工资之和:" + sumSalary.get() + "," + sumSalary2
+ "," + sumSalary3 + "," + sumSalary4);
System.out.println("最高工资:" + maxSalary + "," + maxSalary2);
}
}
输出结果:
工资之和:49300,49300,49300,49300
最高工资:9500,9500
3.6 收集(collect)
collect
(收集),可以说是内容最繁多、功能最丰富的部分了。从字面上去理解,就是把一个流收集起来,最终可以是收集成一个值也可以收集成一个新的集合。collect
主要依赖java.util.stream.Collectors
类内置的静态方法。
3.6.1 归集(toList/toMap/toSet)
因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toList
、toSet
和toMap
比较常用,另外还有toCollection
、toConcurrentMap
等复杂一些的用法。
案例一 toList
、toSet
的简单用法:
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 6, 3, 4, 6, 7, 9, 6, 20);
// 2的倍数 归集list
List<Integer> listNew = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList());
// 2的倍数 归集set
Set<Integer> set = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toSet());
System.out.println("toList:" + listNew);
System.out.println("toSet:" + set);
}
}
运行结果:
toList:[6, 4, 6, 6, 20]
toSet:[4, 20, 6]
案例二:toMap演示
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
Map<String, Employee> map = employeeList.stream()
.filter(p -> p.getSalary() > 8000)
.collect(Collectors.toMap(Employee::getName, p -> p));
System.out.println("toMap1:" + JSONObject.toJSONString(map));
System.out.println("--------------------------------------");
//当employeeList中有key重复时:会报错:Duplicate key Employee(name=Alisa, salary=7900, ...)
employeeList.add(new Employee("Alisa", 2900, 26, "female", "New York"));
employeeList.add(new Employee("Alisa", 3900, 26, "female", "New York"));
Map<String, Employee> map2 = employeeList.stream().collect(
Collectors.toMap(Employee::getName, Function.identity(), (p1, p2) -> p2));
// 注1:Function.identity() 等于 p->p
// 注2:(p1, p2) -> p1 取list中重复键中 前面的值
// (p1, p2) -> p2 取list中重复键中 后面的值
System.out.println("toMap2:" + JSONObject.toJSONString(map2));
System.out.println("--------------------------------------");
employeeList = init();
Map<String, String> map3 = employeeList.stream().collect(
Collectors.toMap(Employee::getName, Employee::getCity));
System.out.println("toMap3:" + JSONObject.toJSONString(map3));
// city可以为空字符串但不能为null,否则会报空指针错误,解决方案:
Map<String, String> map4 = employeeList.stream().collect(
Collectors.toMap(Employee::getName, p -> p.getCity() == null ? "" : p.getCity()));
System.out.println("toMap4:" + JSONObject.toJSONString(map4));
employeeList.add(new Employee("Alisa", 3900, 26, "female", "New York"));
//重复key,2个value映射到同一key里
Map<String, String> map5 = employeeList.stream().collect(
Collectors.toMap(Employee::getName, Employee::getCity,
(p1, p2) -> p1 +"," + p2));
System.out.println("toMap5:" + JSONObject.toJSONString(map5));
}
}
运行结果:
toMap1:{
"Tom":{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
"Owen":{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"},
"Anni":{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"}
}
--------------------------------------
toMap2:{
"Tom":{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
"Owen":{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"},
"Anni":{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
"Alisa":{"age":26,"city":"New York","name":"Alisa","salary":3900,"sex":"female"},
"Jack":{"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
"Lily":{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"}}
--------------------------------------
toMap3:{
"Tom":"New York",
"Owen":"New York",
"Anni":"New York",
"Alisa":"New York",
"Jack":"Washington",
"Lily":"Washington"}
toMap4:{
"Tom":"New York",
"Owen":"New York",
"Anni":"New York",
"Alisa":"New York",
"Jack":"Washington",
"Lily":"Washington"}
toMap5:{
"Tom":"New York",
"Owen":"New York",
"Anni":"New York",
"Alisa":"New York,New York",
"Jack":"Washington",
"Lily":"Washington"}
案例三:toMap演示
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
//按照员工姓名去重,求出总工资
List<Employee> collect = employeeList.stream().collect(
Collectors.toMap(Employee::getName, a -> a, (o1, o2) -> {
o1.setSalary(o1.getSalary() + o2.getSalary());
return o1;
})).values().stream().collect(Collectors.toList());
System.out.println("去重求和:" + JSONObject.toJSONString(collect));
}
运行结果:
去重求和:[{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"},
{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
{"age":26,"city":"New York","name":"Alisa","salary":11800,"sex":"female"},
{"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"}]
3.6.2 统计(count/averaging)
Collectors
提供了一系列用于数据统计的静态方法:
- 计数:
count
- 平均值:
averagingInt
、averagingLong
、averagingDouble
- 最值:
maxBy
、minBy
- 求和:
summingInt
、summingLong
、summingDouble
- 统计以上所有:
summarizingInt
、summarizingLong
、summarizingDouble
案例:统计员工人数、平均工资、工资总额、最高工资。
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
// 求总数 类似list.size()
Long count = employeeList.stream().collect(Collectors.counting());
// 将员工先按性别分组,再统计数量
Map<Integer, Long> count2 = employeeList.stream()
.collect(Collectors.groupingBy(Employee::getSex, Collectors.counting()));
// 求平均工资
Double average = employeeList.stream()
.collect(Collectors.averagingDouble(Employee::getSalary));
// 求最高工资
Optional<Integer> max = employeeList.stream()
.map(Employee::getSalary)
.collect(Collectors.maxBy(Integer::compare));
// 求工资之和
Integer sum = employeeList.stream().collect(Collectors.summingInt(Employee::getSalary));
// 一次性统计所有信息
DoubleSummaryStatistics collect = employeeList.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println("员工总数:" + count);
System.out.println("员工性别、总数:" + JSONObject.toJSONString(count2));
System.out.println("员工平均工资:" + average);
System.out.println("员工最高工资:" + max.get());
System.out.println("员工工资总和:" + sum);
System.out.println("员工工资所有统计:" + JSONObject.toJSONString(collect));
}
}
运行结果:
员工总数:6
员工性别、总数:{"female":3,"male":3}
员工平均工资:8216.666666666666
员工最高工资:9500
员工工资总和:49300
员工工资所有统计:{"average":8216.666666666666,"count":6,"max":9500.0,"min":7000.0,"sum":49300.0}
3.6.3 分组(partitioningBy/groupingBy)
- 分组:将集合分为多个
Map
,比如员工按性别分组。有单级分组和多级分组。类似于数据库中的group by
。 - 分区:将
stream
按条件分为两个Map
,比如员工按薪资是否高于8000
分为两部分。分区可以看做是分组的一种特殊情况,在分区中key
只有两种情况:true
或false
,目的是将待分区集合按照条件一分为二,java8
的流式处理利用Collectors.partitioningBy()
方法实现分区。
案例一:将员工按薪资是否高于8000
分为两部分;将员工按性别和城市分组
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
List<Employee> employeeList2 = init();
// 将员工按薪资是否高于8000分组
Map<Boolean, List<Employee>> part = employeeList.stream()
.collect(Collectors.partitioningBy(x -> x.getSalary() > 8000));
// 将员工按性别分组
Map<String, List<Employee>> group = employeeList.stream()
.collect(Collectors.groupingBy(Employee::getSex));
// 将员工先按性别分组,再按城市分组
Map<String, Map<String, List<Employee>>> group2 = employeeList.stream()
.collect(Collectors.groupingBy(Employee::getSex,
Collectors.groupingBy(Employee::getCity)));
// 多个数组中符合年龄要求的按照城市分组
Map<String, List<Employee>> group3 = Stream.of(employeeList, employeeList2)
.flatMap(e -> e.stream().filter(s -> s.getAge() < 17))
.collect(Collectors.groupingBy(Employee::getCity));
// 多个数组按照是否年龄大于23进行分组,key为true和false
ConcurrentMap<Boolean, List<Employee>> group4 = Stream.of(employeeList, employeeList2)
.flatMap(Collection::stream)
.collect(Collectors.groupingByConcurrent(p -> p.getAge() > 23));
System.out.println("员工按薪资是否大于8000分组情况:" + JSONObject.toJSONString(part));
System.out.println("员工按性别分组情况:" + JSONObject.toJSONString(group));
System.out.println("员工按性别、城市:" + JSONObject.toJSONString(group2));
System.out.println("多数组中年龄小于23的按照城市分组:" + JSONObject.toJSONString(group3));
System.out.println("多数组中年龄大于23进行分组:" + JSONObject.toJSONString(group4));
}
}
输出结果:
员工按薪资是否大于8000分组情况:{
false:[{"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"},
{"age":26,"city":"New York","name":"Alisa","salary":7900,"sex":"female"}],
true:[{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}]}
员工按性别分组情况:{
"female":[{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"},
{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
{"age":26,"city":"New York","name":"Alisa","salary":7900,"sex":"female"}],
"male":[{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
{"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}]}
员工按性别、城市:{
"female":{"New York":[{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
{"age":26,"city":"New York","name":"Alisa","salary":7900,"sex":"female"}],
"Washington":[{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"}]},
"male":{"New York":[{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}],
"Washington":[{"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"}]}}
多数组中年龄小于23的按照城市分组:{
"Washington":[{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"},
{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"}]}
多数组中年龄大于23进行分组:{
false:[{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
{"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"},
{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"},
{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
{"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
{"age":21,"city":"Washington","name":"Lily","salary":7800,"sex":"female"},
{"age":24,"city":"New York","name":"Anni","salary":8200,"sex":"female"},
{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}],
true:[{"age":26,"city":"New York","name":"Alisa","salary":7900,"sex":"female"},
{"age":26,"city":"New York","name":"Alisa","salary":7900,"sex":"female"}]}
案例二:重复key,取第一个值
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
List<Employee> employeeList = init();
Map<String, Employee> groupedItems = employeeList.stream()
.collect(Collectors.groupingBy(
Employee::getName, // 分组的键
Collectors.collectingAndThen( // 对分组结果进一步处理
Collectors.toMap( // 分组内容作为Map
item -> 0, // 映射函数,使用固定的键0
Function.identity(), // 映射函数,原样保留Item对象
(existing, replacement) -> existing), // 如果有冲突,保留现存的条目
map -> map.values().iterator().next() // 提取Map的第一个值,即分组中的一条数据
)
));
Map<String, Employee> result = employeeList.stream()
.collect(Collectors.groupingBy(
Employee::getName,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.get(0) // 这里取第一个对象
)
));
Map<String, Employee> result2 = employeeList.stream()
.collect(Collectors.toMap(
Employee::getName,// 分组的键
Function.identity(), // 映射函数,原样保留Item对象
(existing, replacement) -> existing)); // 如果有冲突,保留现存的条目
groupedItems.forEach((key, value) -> System.out.println(key + " -> " + value));
System.out.println("=====");
result.forEach((key, value) -> System.out.println(key + " -> " + value));
System.out.println("=====");
result2.forEach((key, value) -> System.out.println(key + " -> " + value));
}
}
输出结果:
Tom -> Employee(name=Tom, salary=8900, age=23, sex=male, city=New York)
Owen -> Employee(name=Owen, salary=9500, age=25, sex=male, city=New York)
Anni -> Employee(name=Anni, salary=8200, age=24, sex=female, city=New York)
Alisa -> Employee(name=Alisa, salary=7900, age=26, sex=female, city=New York)
Jack -> Employee(name=Jack, salary=7000, age=25, sex=male, city=Washington)
Lily -> Employee(name=Lily, salary=7800, age=21, sex=female, city=Washington)
3.6.4 接合(joining)
joining
可以将stream
中的元素用特定的连接符(没有的话,则直接连接)连接成一个字符串。
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
String names = employeeList.stream().map(Employee::getName).collect(Collectors.joining(","));
String names2 = employeeList.stream().map(Employee::getName)
.collect(Collectors.joining(",", "[", "]"));
System.out.println("所有员工的姓名:" + names);
System.out.println("所有员工的姓名2:" + names2);
List<String> list = Arrays.asList("A", "B", "C");
String string = list.stream().collect(Collectors.joining("-"));
System.out.println("拼接后的字符串:" + string);
}
}
运行结果:
所有员工的姓名:Tom,Jack,Lily,Anni,Owen,Alisa
所有员工的姓名2:[Tom,Jack,Lily,Anni,Owen,Alisa]
拼接后的字符串:A-B-C
3.6.5 归约(reducing)
Collectors
类提供的reducing
方法,相比于stream
本身的reduce
方法,增加了对自定义归约的支持。
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
// 每个员工减去起征点后的薪资之和(这个例子并不严谨,但一时没想到好的例子)
Integer sum = employeeList.stream()
.collect(Collectors.reducing(0, Employee::getSalary, (i, j) -> (i + j - 5000)));
Integer sum2 = employeeList.stream()
.map(Employee::getSalary)
.reduce(0, (i, j) -> (i + j - 5000));
System.out.println("员工扣税薪资总和:" + sum);
System.out.println("员工扣税薪资总和:" + sum2);
// stream的reduce
Optional<Integer> sum3 = employeeList.stream().map(Employee::getSalary).reduce(Integer::sum);
System.out.println("员工薪资总和:" + sum3.get());
}
}
运行结果:
员工扣税薪资总和:19300
员工扣税薪资总和:19300
员工薪资总和:49300
3.6.6 映射(mapping)
import java.util.stream.Collectors;
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
// 获得所有员工姓名
List<String> collect = employeeList.stream().map(CustomerVO::getName)
.collect(Collectors.toList());
String collect2 = employeeList.stream()
.collect(Collectors.mapping(CustomerVO::getName, Collectors.joining(",", "[", "]")));
System.out.println("获得所有员工姓名:" + JSONObject.toJSONString(collect));
Map<Integer, List<String>> mapping = employeeList.stream()
.collect(Collectors.groupingBy(CustomerVO::getAge,
Collectors.mapping(CustomerVO::getName, Collectors.toList())));
System.out.println("年龄分组后,获得所有员工姓名:" + JSONObject.toJSONString(mapping));
}
}
3.6.7 归纳(collectingAndThen)
先进行结果集的收集,然后将收集到的结果集进行下一步的处理。
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
// 根据城市去重,并且排序
List<Employee> unique = employeeList.stream()
.collect(Collectors.collectingAndThen(Collectors.toCollection(() ->
new TreeSet<>(Comparator.comparing(Employee::getCity))), ArrayList::new));
System.out.println("按照城市去重,排序:" + JSONObject.toJSONString(unique));
}
}
运行结果:
按照城市去重,排序:[{"age":21,"city":"Washington","name":"Lily","salary":8900,"sex":"female"},
{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"}]
如果多列去重,修改Comparator
条件
Comparator.comparing(o -> o.getName() + ";" + o.getCity()))
3.6.8 并集(teeing)
这是一种新的静态方法,用于加入java.util.stream.Collectors
接口,该接口允许使用两个独立的收集器进行收集,然后使用提供的BiFunction
合并其结果。传递给结果收集器的每个元素均由两个下游收集器处理,然后使用指定的合并函数将其结果合并为最终结果。
请注意,此函数有助于一步完成特定任务。如果不使用
teeing()
函数,可以分两步执行给定的任务。它只是一个帮助函数,可以帮助减少冗长程度。
- 查找最高和最低工资员工
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
// 查找最高和最低工资员工
HashMap<String, Employee> result = employeeList.stream()
.collect(Collectors.teeing(Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
Collectors.minBy(Comparator.comparing(Employee::getSalary)), (e1, e2) -> {
HashMap<String, Employee> map = new HashMap();
map.put("max", e1.get());
map.put("min", e2.get());
return map;
}
));
System.out.println("最高和最低工资员工:" + JSONObject.toJSONString(result));
}
}
运行结果:
最高和最低工资员工:
{
"min": {"age":25,"city":"Washington","name":"Jack","salary":7000,"sex":"male"},
"max": {"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}
}
- 过滤项目并计数
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
// 所有薪水高于8500的员工和数量
HashMap<String, Employee> result = employeeList.stream()
.collect(Collectors.teeing(
Collectors.filtering(e -> e.getSalary() > 8500, Collectors.toList()),
Collectors.filtering(e -> e.getSalary() > 8500, Collectors.counting()),
(list, count) -> {
HashMap<String, Object> map = new HashMap();
map.put("list", list);
map.put("count", count);
return map;
}
));
System.out.println("所有薪水高于8500的员工和数量:" + JSONObject.toJSONString(result));
}
}
运行结果:
所有薪水高于8500的员工和数量:
{
"count": 2,
"list": [{"age":23,"city":"New York","name":"Tom","salary":8900,"sex":"male"},
{"age":25,"city":"New York","name":"Owen","salary":9500,"sex":"male"}]
}
3.7 排序(sorted)
sorted
,中间操作。有两种排序:
sorted()
:自然排序,流中元素需实现Comparable
接口。sorted(Comparator com)
:Comparator
排序器自定义排序。
案例:将员工按工资由高到低(工资一样则按年龄由大到小)排序。
import java.util.Comparator;
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
// 按工资升序排序(自然排序)
// nullsLast: 表示将排序字段中的null值放到集合最后面
// nullsFirst: 表示将排序字段中的null值放到集合最前面
List<String> newList = employeeList.stream()
.sorted(Comparator.comparing(Employee::getSalary,
Comparator.nullsLast(Comparator.naturalOrder())))
.map(Employee::getName).collect(Collectors.toList());
// 按工资倒序排序
List<String> newList2 = employeeList.stream()
.sorted(Comparator.comparing(Employee::getSalary).reversed())
.map(Employee::getName).collect(Collectors.toList());
// 先按工资再按年龄升序排序
List<String> newList3 = employeeList.stream().sorted(Comparator.comparing(Employee::getSalary)
.thenComparing(Employee::getAge))
.map(Employee::getName).collect(Collectors.toList());
// 先按工资(降序)再按年龄(降序)自定义排序
List<String> newList4 = employeeList.stream().sorted((p1, p2) -> {
if (p1.getSalary() == p2.getSalary()) {
return p2.getAge() - p1.getAge();
} else {
return p2.getSalary() - p1.getSalary();
}
}).map(Employee::getName).collect(Collectors.toList());
System.out.println("按工资升序排序:" + newList);
System.out.println("按工资降序排序:" + newList2);
System.out.println("先按工资再按年龄升序排序:" + newList3);
System.out.println("先按工资(降序)再按年龄(降序)自定义排序:" + newList4);
}
}
运行结果:
按工资升序排序:[Jack, Lily, Alisa, Anni, Tom, Owen]
按工资降序排序:[Owen, Tom, Anni, Alisa, Lily, Jack]
先按工资再按年龄升序排序:[Jack, Lily, Alisa, Anni, Tom, Owen]
先按工资(降序)再按年龄(降序)自定义排序:[Owen, Tom, Anni, Alisa, Lily, Jack]
更多排序参考Java集合排序Comparable和Comparator
3.8 提取/组合
流也可以进行合并、去重、限制、跳过等操作。
public class StreamTest {
public static void main(String[] args) {
String[] arr1 = { "a", "b", "c", "d" };
String[] arr2 = { "d", "e", "f", "g" };
Stream<String> stream1 = Stream.of(arr1);
Stream<String> stream2 = Stream.of(arr2);
// concat: 合并两个流 distinct: 去重
List<String> newList = Stream.concat(stream1, stream2).distinct().collect(Collectors.toList());
// limit: 限制从流中获得前n个数据(从1开始每个元素加2)
List<Integer> collect = Stream.iterate(1, x -> x + 2).limit(10).collect(Collectors.toList());
// skip: 跳过前n个数据
List<Integer> collect2 = Stream.iterate(1, x -> x + 2).skip(1)
.limit(5).collect(Collectors.toList());
System.out.println("流合并:" + newList);
System.out.println("limit:" + collect);
System.out.println("skip:" + collect2);
}
}
运行结果:
流合并:[a, b, c, d, e, f, g]
limit:[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
skip:[3, 5, 7, 9, 11]
3.9 消费(peek)
peek
的操作是返回一个新的Stream
的,且设计的初衷是用来debug
调试的,因此使用steam.peek()
必须对流进行一次处理再产生一个新的Stream
。
public class StreamTest {
public static void main(String[] args) {
List<Employee> employeeList = init();
System.out.println(JSONObject.toJSONString(employeeList));
System.out.println(JSONObject.toJSONString(getEmployeeTreeList(init())));
System.out.println(JSONObject.toJSONString(getEmployeeTreeListPeek(employeeList)));
}
private static List<Employee> init() {
List<Employee> employeeList = Lists.newArrayList();
employeeList.add(new Employee(1001, "节点一", 0));
employeeList.add(new Employee(1002, "节点二", 0));
employeeList.add(new Employee(2001, "节点一的子节点1", 1001));
employeeList.add(new Employee(2002, "节点一的子节点2", 1001));
employeeList.add(new Employee(2003, "节点一的子节点3", 1001));
employeeList.add(new Employee(2004, "节点二的子节点1", 1002));
employeeList.add(new Employee(2005, "节点二的子节点2", 1002));
employeeList.add(new Employee(3001, "节点一的子节点1的子节点1", 2001));
employeeList.add(new Employee(3002, "节点一的子节点1的子节点2", 2001));
employeeList.add(new Employee(3003, "节点一的子节点2的子节点", 2002));
employeeList.add(new Employee(4001, "节点一的子节点1的子节点1的子节点", 3001));
return employeeList;
}
// 递归
private static List<Employee> getEmployeeTreeList(List<Employee> employeeList) {
List<Employee> rootMenus = employeeList.stream()
.filter(e -> e.getParentId().equals(0))
.collect(Collectors.toList());
rootMenus.forEach(employee -> {
getChildrenList(employee, employeeList);
});
return rootMenus;
}
private static void getChildrenList(Employee employee, List<Employee> employeeList) {
List<Employee> children = employeeList.stream()
.filter(e -> e.getParentId().equals(employee.getId()))
.collect(Collectors.toList());
children.stream().forEach(child -> {
List<Employee> cd = employeeList.stream()
.filter(e -> e.getParentId().equals(child.getId()))
.collect(Collectors.toList());
getChildrenList(child, employeeList);
child.setChildren(cd);
});
employee.setChildren(children);
}
// 以下是peek递归
private static List<Employee> getEmployeeTreeListPeek(List<Employee> employeeList) {
if (CollectionUtils.isNotEmpty(employeeList)) {
// 递归获取菜单树形结构
// 获取父节点,说明:父节点的parentId都是0
return employeeList.stream().filter((Employee m) -> m.getParentId() == 0)
.peek((Employee m) -> m.setChildren(getChildrenListPeek(m, employeeList)))
.collect(Collectors.toList());
}
return Collections.emptyList();
}
private static List<Employee> getChildrenListPeek(Employee employee, List<Employee> employeeList) {
// 子节点parentId = 父节点ID
return employeeList.stream().filter((Employee e) -> employee.getId() == (e.getParentId()))
.peek((Employee e1) -> e1.setChildren(getChildrenListPeek(e1, employeeList)))
.collect(Collectors.toList());
}
@Data
@AllArgsConstructor
public static class Employee {
private int id;
private String value;
private Integer parentId;
private List<Employee> children;
public Employee(int id, String value, int parentId) {
this.id = id;
this.value = value;
this.parentId = parentId;
}
}
}
peek
构建的tree
,工具类可以参考通用TreeUtil工具
四、拓展
4.1 peek与map的区别
Stream<T> peek(Consumer<? super T> action);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
peek
接收一个Consumer
,而map
接收一个Function
。
map
:用于对流中的每个元素进行映射处理,然后再形成新的流;peek
:用于debug
调试流中间结果,不能形成新的流,但能修改引用类型字段的值;
Consumer
是没有返回值的,它只是对Stream
中的元素进行某些操作,但是操作之后的数据并不返回到Stream
中,所以Stream
中的元素还是原来的元素。
而Function
是有返回值的,这意味着对于Stream
的元素的所有操作都会作为新的结果返回到Stream
中。
这就是为什么peek String
不会发生变化而peek Object
会发送变化的原因。
4.2 peek和foreach区别
peek
:会继续返回Stream
对象。forEach
:返回void
,结束Stream
操作。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器