内容简介:Java 8小记 - Stream
本文主要介绍 Java 8
的 Stream
的简单使用
简介
流与集合的区别
计算的时机
是否全部载入内存 | 能否添加或删除元素 | 类似于 | |
---|---|---|---|
集合 | 是 | 能 | DVD |
流 | 否,按需计算 | 不能 | 网络流媒体 |
消费一次
流 只能遍历一次
,遍历后即被消费,类似于 网络流
相关代码托管在 java8_demo
List<String> strs = Arrays.asList("zhong", "ming", "mao"); Stream<String> stream = strs.stream(); stream.forEach(s -> System.out.println(s)); // Lambda // throw java.lang.IllegalStateException: stream has already been operated upon or closed stream.forEach(System.out::println); // 方法引用,相关内容请参照「Java8回忆录 - Lambda」
迭代方式
迭代方式 | 编程方式 | 并行 | 目的 | |
---|---|---|---|---|
集合 | 外部迭代 | 自行选择数据表示 | 自行实现并行 | 以特定时间/空间复杂度存储和访问元素 |
流 | 内部迭代 | 声明式编程 | 几乎免费的并行 | 计算 |
流操作
流操作类似于 流水线
操作
中间操作
- 返回一个流(
Stream
)的操作 - 中间操作
不会执行任何处理
,直到触发了一个终端操作
- 中间操作一般是可以进行
合并
的,这会在终端操作进行处理
常用中间操作
操作 | 出参 | 入参 | 函数描述符(入参) |
---|---|---|---|
filter | Stream<T> |
Predicate<T> |
T -> boolean |
distinct | Stream<T> |
||
skip | Stream<T> |
long | |
limit | Stream<T> |
long | |
map | Stream<R> |
Function<T,R> |
T -> R |
flatMap | Stream<R> |
Function<T,Stream<R>> |
T -> Stream<R> |
sorted | Stream<T> |
Comparator<T> |
(T,T) -> int |
终端操作
-
关闭流
的操作 - 从流的流水线
生成结果
(List、Integer、void等)
常用终端操作
操作 | 出参 | 入参 | 函数描述符(入参) |
---|---|---|---|
anyMatch | boolean | Predicate<T> |
T -> boolean |
noneMatch | boolean | Predicate<T> |
T -> boolean |
allMatch | boolean | Predicate<T> |
T -> boolean |
findAny | Optional<T> |
||
findFirst | Optional<T> |
||
forEach | void | Consumer<T> |
T -> void |
collect | R | Collector<T,A,R> |
|
count | long | ||
reduce | Optional<T> |
BinaryOperator<T> |
(T,T) -> T |
筛选与切片
谓词筛选
@Data @AllArgsConstructor @NoArgsConstructor public class User{ public enum TYPE { OLD, YOUNG; } private String name; private int age; private boolean isStudent; public User(intage,booleanisStudent){ this.age = age; this.isStudent = isStudent; } }
@Test public void filterCountTest(){ // 初始化代码实际在@Before中,这里仅为了行文方便 List<User> users = Arrays.asList(new User(10, true), new User(20, true), new User(30, false), new User(40, false), new User(50, false), new User(60, false), new User(70, false)); assertEquals(2, users.stream() // Stream<User> .filter(User::isStudent) // 谓词筛选,方法引用,Stream<User> .count()); // 统计,long }
筛选各异元素
@Test public void filterDistinctTest(){ List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 3, 2, 1); assertEquals(2, numbers.stream() // Stream<Integer> .filter(n -> n % 2 == 0) // 谓词过滤,Lambda,Stream<Integer> .distinct() // 筛选各异的元素,Stream<Integer> .count()); // 统计,long }
截断流
@Test public void filterLimitTest(){ Predicate<User> isStudent = User::isStudent; assertEquals(3, users.stream() // Stream<User> .filter(isStudent.negate()) // 谓词筛选,Stream<User> .limit(3) // 截断流,Stream<User> .count()); // 统计,long }
跳过N个元素
@Test public void filterSkipTest(){ Predicate<User> isStudent = User::isStudent; assertEquals(4, users.stream() // Stream<User> .filter(isStudent.negate()) // 谓词筛选,Stream<User> .skip(1) // 跳过前N个元素,Stream<User> .count()); // 统计,long }
映射
map
@Test public void mapTest(){ List<String> words = Arrays.asList("zhong", "ming", "mao"); assertEquals(3, words.stream() // Stream<String> .map(String::length) // 映射,Stream<Integer> .distinct() // 筛选各异的元素,Stream<Integer> .count()); // 统计,long }
flatMap
@Test public void flatMapTest(){ List<String> words = Arrays.asList("zhong", "ming", "mao"); Stream<String[]> stream = words.stream() // Stream<String> .map(s -> s.split("")); // 映射,Stream<String[]>,此时流中的元素是String[] // <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) // public static <T> Stream<T> stream(T[] array) assertEquals(8, stream.flatMap(Arrays::stream) // 映射,Stream<String>,此时流中的元素恢复为String .distinct() // 筛选各异的元素,Stream<String> .collect(toList()) // 收集为List,List<String> .size()); }
flatMap 从定义上理解有点晦涩,做简单解释
-
flatMap
方法定义可简单理解为Stream<R> (Function<T, Stream<R>> mapper)
,mapper
的函数描述符简单理解为T -> Stream<R>
- Lambda表达式
Arrays::stream
的签名为T[] -> Stream<T>
-
stream
为Stream<String[]>
,元素类型为String[]
,通过Arrays::stream
会变成String[] -> Stream<String>
,依据flatMap
方法的定义,将返回Stream<String>
,流被扁平化
下面是 flatmap
的 另一个实例
List<Integer> numbers1 = Arrays.asList(1, 2, 3); List<Integer> numbers2 = Arrays.asList(3, 4); Stream<Stream<Integer[]>> mapStream = numbers1.stream() // Stream<Integer> .map(i -> numbers2.stream() // Stream<Integer> .map(j -> new Integer[]{i, j}) // Stream<Integer[]> ); // Stream<Stream<Integer[]>> // flatMap 相对于 map,相当于抹去了一层 Stream Stream<Integer[]> flatMapStream = numbers1.stream() // Stream<Integer> .flatMap(i -> numbers2.stream() // Stream<Integer> .map(j -> new Integer[]{i, j}) // Stream<Integer[]> ); // Stream<Integer[]> assertEquals(2, numbers1.stream() // Stream<Integer> .flatMap(i -> numbers2.stream() // Stream<Integer> .map(j -> new Integer[]{i, j})) // Stream<Integer[]> .filter(ints -> (ints[0] + ints[1]) % 3 == 0) // Stream<Integer[]> ,只有(2,4)和(3,3)匹配 .count()); // long
查找与匹配
查找
@Test(expected = IllegalArgumentException.class) public void findTest(){ List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> first = numbers.stream() // Stream<Integer> .map(x -> x * x + 2) // Stream<Integer> .filter(x -> x % 3 == 0) // Stream<Integer> .findFirst(); // Optional<Integer> first.orElseThrow(() -> new RuntimeException("error")); Optional<Integer> any = numbers.stream() // Stream<Integer> .map(x -> x * x) // Stream<Integer> .filter(x -> x % 7 == 0) // Stream<Integer> .findAny(); // Optional.EMPTY any.orElseThrow(() -> new IllegalArgumentException("error")); }
匹配
@Test public void matchTest(){ List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); assertFalse(numbers.stream() // Stream<Integer> .anyMatch(x -> x > 10)); // boolean assertFalse(numbers.stream() // Stream<Integer> .noneMatch(x -> x < 10)); // boolean assertTrue(numbers.stream() // Stream<Integer> .allMatch(x -> x < 10)); // boolean }
归约
求和
@Test(expected = RuntimeException.class) public void sumTest(){ List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); assertEquals(Integer.valueOf(15), numbers.stream() // Stream<Integer> .reduce(0, (x, y) -> x + y)); // Integer // 更简洁的写法Integer::sum(方法引用) assertEquals(Integer.valueOf(15), numbers.stream().reduce(0, Integer::sum)); // Integer::sum从 Jdk8 开始引入 numbers.clear(); Optional<Integer> sum = numbers.stream() // Stream<Integer> .reduce((x, y) -> x + y); // Optional<Integer>,无初始值,当numbers为空时,返回Optional.EMPTY sum.orElseThrow(() -> new RuntimeException("no init value")); }
最大值和最小值
@Test public void maxMinTest(){ List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); assertEquals(Integer.valueOf(5), numbers.stream().reduce((x, y) -> Integer.max(x, y)).get()); // 方法引用,Lambda语法糖 assertEquals(Integer.valueOf(5), numbers.stream() // Stream<Integer> .reduce(Integer::max) // Optional<Integer> .get()); assertEquals(Integer.valueOf(1), numbers.stream().reduce(Integer::min).get()); assertEquals(Integer.valueOf(5), numbers.stream().max((x, y) -> x.compareTo(y)).get()); // 方法引用 assertEquals(Integer.valueOf(5), numbers.stream() // Stream<Integer> .max(Integer::compareTo) // Optional<Integer> .get()); assertEquals(Integer.valueOf(1), numbers.stream().min(Integer::compareTo).get()); }
总数
@Test public void countTest(){ List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // Map-Reduce assertEquals(Integer.valueOf(5), numbers.stream() // Stream<Integer> .map(integer -> 1) // Stream<Integer>,Map .reduce(0, Integer::sum)); // Integer,Reduce // 通过reduce 实现与 count 一样的功能 assertEquals(5, numbers.stream().count()); }
数值流
- 前面涉及到
Stream<String>
、Stream<Integer>
的都是对象流
,隐含装箱
和拆箱
成本 -
数值流
是对象流的原始类型特化
,如IntStream
,没有装箱
和拆箱
成本
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Stream<Integer> objStream = numbers.stream(); // 对象流,隐含装箱和拆箱成本 Optional<Integer> reduce = objStream.reduce(Integer::max); // Optional<Integer> assertEquals(Integer.valueOf(5), reduce.get()); IntStream intStream = numbers.stream().mapToInt(Integer::intValue); OptionalInt max = intStream.max(); assertEquals(5, max.getAsInt()); // 数值流,没有装箱和拆箱成本 Stream<Integer> boxed = numbers.stream().mapToInt(Integer::intValue).boxed(); // 对象流
构建流
有限流
值
@Test public void buildFromValueTest(){ // <T> Stream<T> of(T... values) Stream<String> stringStream = Stream.of("zhong", "ming", "mao"); assertEquals(3, stringStream.count()); // <T> Stream<T> empty() Stream<Object> objectStream = Stream.empty(); assertEquals(0, objectStream.count()); // IntStream of(int... values) { return Arrays.stream(values); } IntStream intStream = IntStream.of(1, 2, 3); assertEquals(3, intStream.max().getAsInt()); }
数组
@Test public void buildFromArrayTest(){ int[] intArray = {1, 2, 3, 4, 5, 6, 7}; // IntStream stream(int[] array) IntStream intStream = Arrays.stream(intArray); assertEquals(3, intStream.filter(x -> x % 2 == 0).count()); Integer[] integerArray = {1, 2, 3, 4, 5, 6, 7}; // <T> Stream<T> stream(T[] array) Stream<Integer> integerStream = Arrays.stream(integerArray); assertEquals(4, integerStream.filter(x -> x % 2 == 1).count()); }
文件
@Test public void buildFromFileTest()throwsIOException{ String relativePath = "/tmp.txt"; // src/test/resources/tmp.txt String absPath = this.getClass().getResource(relativePath).getPath(); // 绝对路径 // Stream<String> lines(Path path, Charset cs) throws IOException Stream<String> stringStream = Files.lines(Paths.get(absPath), Charset.defaultCharset()); // Stream<String>,元素为文件中的每一行 assertEquals(9, stringStream.flatMap(s -> Arrays.stream(s.split("\\s+"))) // 扁平化流,Stream<String>,元素为单词 .distinct() // Stream<String> .count()); // long }
range + rangeClosed
@Test public void buildFromRangeTest(){ IntStream intStream = IntStream.range(0, 10); assertEquals(5, intStream.filter(value -> value % 2 == 0).count()); intStream = IntStream.rangeClosed(0, 10); assertEquals(6, intStream.filter(value -> value % 2 == 0).count()); }
无限流
iterate
@Test public void buildFromIterateTest(){ // 偶数数列 // <T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) Stream<Integer> integerStream = Stream.iterate(0, x -> x + 2); assertEquals(20, integerStream.limit(5).mapToInt(Integer::intValue).sum()); // 偶数数列 // IntStream iterate(final int seed, final IntUnaryOperator f) IntStream intStream = IntStream.iterate(0, x -> x + 2); assertEquals(20, intStream.limit(5).sum()); // 斐波那契数列 Stream<int[]> intArrayStream = Stream.iterate(new int[]{0, 1}, // T fibArray -> new int[]{fibArray[1], fibArray[0] + fibArray[1]}); // UnaryOperator<T> assertEquals(5, intArrayStream.mapToInt(fib -> fib[1]).limit(5).max().getAsInt()); }
generate
@Test public void buildFromGenerateTest(){ // <T> Stream<T> generate(Supplier<T> s) Stream<Integer> integerStream = Stream.generate(() -> (int) (Math.random() * 1000)); // IntStream generate(IntSupplier s) IntStream intStream = IntStream.generate(() -> (int) (Math.random() * 1000)); // 斐波那契数列 integerStream = Stream.generate(new Supplier<Integer>() { private int pre = 0; private int cur = 1; @Override publicIntegerget(){ int next = pre + cur; pre = cur; cur = next; return pre; } }); assertEquals(5, integerStream.mapToInt(Integer::intValue).limit(5).max().getAsInt()); intStream = IntStream.generate(new IntSupplier() { private int pre = 0; private int cur = 1; @Override public int getAsInt(){ int next = pre + cur; pre = cur; cur = next; return pre; } }); assertEquals(5, intStream.limit(5).max().getAsInt()); }
收集
收集器
( Collector
)会对 流中的元素
应用一个 转换函数
,并将结果 累积
在一个数据结构中
汇总
求和
@Test public void sumTest(){ List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); assertEquals(15, integerList.stream() .collect(Collectors.summingInt(Integer::intValue)) .intValue()); }
<R, A> R collect(Collector<? super T, A, R> collector)
<T> Collector<T, ?, Integer> summingInt(ToIntFunction<? super T> mapper)
总数
@Test public void countTest(){ List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); assertEquals(3, integerList.stream().filter(n -> n % 2 == 1) .collect(Collectors.counting()) .intValue()); }
最大值最小值
@Test public void maxMinTest(){ List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); assertEquals(Integer.valueOf(5), integerList.stream().filter(n -> n % 2 == 1) .collect(Collectors.maxBy(Comparator.comparingInt(Integer::intValue))) .get()); assertEquals(Integer.valueOf(1), integerList.stream().filter(n -> n % 2 == 1) .collect(Collectors.minBy(Comparator.comparing(Integer::intValue))) .get()); }
平均数
@Test public void avgTest(){ List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); assertEquals(Double.valueOf("3.0"), integerList.stream() .collect(Collectors.averagingInt(Integer::intValue))); }
统计数
@Test public void statisticsTest(){ List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); IntSummaryStatistics statistics = integerList.stream() .collect(Collectors.summarizingInt(Integer::intValue)); assertEquals(5, statistics.getCount()); assertEquals(5, statistics.getMax()); assertEquals(1, statistics.getMin()); assertEquals(15, statistics.getSum()); assertEquals(3, (int) statistics.getAverage()); }
连接字符串
@Test public void joinStrTest(){ List<String> stringList = Arrays.asList("zhong", "ming", "mao"); String delimiter = ","; String expectedStr = ""; for (int i = 0, len = stringList.size(); i < len; i++) { expectedStr += stringList.get(i); if (i != len - 1) { expectedStr += delimiter; } } assertEquals(expectedStr, stringList.stream() .collect(Collectors.joining(","))); }
广义归约
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5); // <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op) assertEquals(Integer.valueOf(15), integerList.stream() .collect(Collectors.reducing(Integer::sum)) .get()); // <T> Collector<T, ?, T> reducing(T identity, BinaryOperator<T> op) assertEquals(Integer.valueOf(25), integerList.stream() .collect(Collectors.reducing(10, Integer::sum))); // <T, U> Collector<T, ?, U> reducing(U identity, Function<? super T, ? extends U> mapper, BinaryOperator<U> op) assertEquals(Integer.valueOf(50), integerList.stream() .collect(Collectors.reducing(20, // identity n -> n * 2, // mapper Integer::sum))); // op
分组
单级分组
@Test public void singleGroupTest(){ List<User> users = Arrays.asList(new User("a", 10, true), new User("b", 20, true), new User("c", 30, false), new User("d", 40, false), new User("e", 50, false), new User("f", 60, false), new User("g", 70, false)); // <T, K> Collector<T, ?,Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier) Map<Boolean, List<User>> listMap = users.stream() .collect(Collectors.groupingBy(User::isStudent)); assertEquals(2, listMap.size()); assertEquals(5, listMap.get(false).size()); assertEquals(2, listMap.get(true).size()); }
多级分组
@Test public void multiGroupTest(){ List<User> users = Arrays.asList(new User("a", 10, true), new User("b", 20, true), new User("c", 30, false), new User("d", 40, false), new User("e", 50, false), new User("f", 60, false), new User("g", 70, false)); // <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) Map<User.TYPE, Map<Boolean, List<User>>> mapMap = users.stream().collect( Collectors.groupingBy(user -> user.getAge() > 50 ? User.TYPE.OLD : User.TYPE.YOUNG, // 第一层 Key Collectors.groupingBy(User::isStudent))); // 第二层 Key assertEquals(2, mapMap.size()); assertEquals(1, mapMap.get(User.TYPE.OLD).size()); assertEquals(2, mapMap.get(User.TYPE.YOUNG).size()); assertFalse(mapMap.get(User.TYPE.OLD).containsKey(Boolean.TRUE)); assertEquals(2, mapMap.get(User.TYPE.OLD).get(Boolean.FALSE).size()); assertEquals(2, mapMap.get(User.TYPE.YOUNG).get(Boolean.TRUE).size()); assertEquals(3, mapMap.get(User.TYPE.YOUNG).get(Boolean.FALSE).size()); }
按子组收集
@Test public void groupCollectTest(){ Map<User.TYPE, Long> typeLongMap = users.stream().collect( Collectors.groupingBy(user -> user.getAge() > 50 ? User.TYPE.OLD : User.TYPE.YOUNG, Collectors.counting())); // 子组内总数 assertEquals(2, typeLongMap.size()); assertEquals(2, typeLongMap.get(User.TYPE.OLD).intValue()); assertEquals(5, typeLongMap.get(User.TYPE.YOUNG).intValue()); Map<User.TYPE, Double> typeDoubleMap = users.stream().collect( Collectors.groupingBy(user -> user.getAge() > 50 ? User.TYPE.OLD : User.TYPE.YOUNG, Collectors.averagingInt(User::getAge))); // 子组内平均年龄 assertEquals(2, typeDoubleMap.size()); assertEquals(1, typeDoubleMap.get(User.TYPE.OLD).compareTo(typeDoubleMap.get(User.TYPE.YOUNG))); }
分区
@Test public void partitioningTest(){ Map<Boolean, List<User>> listMap = users.stream().collect( Collectors.partitioningBy(User::isStudent)); assertEquals(2, listMap.size()); assertEquals(2, listMap.get(Boolean.TRUE).size()); assertEquals(5, listMap.get(Boolean.FALSE).size()); Map<Boolean, Map<Boolean, List<User>>> map = users.stream().collect( Collectors.partitioningBy(user -> user.getAge() > 50 ? true : false, Collectors.partitioningBy(User::isStudent))); assertEquals(2, map.size()); assertEquals(2, map.get(Boolean.TRUE).size()); assertEquals(2, map.get(Boolean.FALSE).size()); assertEquals(2, map.get(Boolean.FALSE).get(Boolean.TRUE).size()); assertEquals(3, map.get(Boolean.FALSE).get(Boolean.FALSE).size()); }
收集器
接口定义
public interface Collector<T,A,R>{ // T:流中的元素; A:用于累加器处理对象的类型; R:返回类型 Supplier<A>supplier(); // 生成累加器 BiConsumer<A, T>accumulator(); // 累加操作 BinaryOperator<A>combiner(); // 合并操作,对流的子部分如何进行并行合并 Function<A, R>finisher(); // 最终转换 Set<Characteristics>characteristics(); // 定义收集器的行为 enum Characteristics { // 不是很理解,请大神指教 CONCURRENT, UNORDERED, IDENTITY_FINISH } }
自定义收集器
// 自定义收集器,最终转换为 Set<T> public class CustomCollector<T>implements Collector<T,List<T>,Set<T>>{ @Override public Supplier<List<T>> supplier() { // 累加器实例 return ArrayList::new; } @Override public BiConsumer<List<T>, T> accumulator() { // 将流中的元素添加到容器 return List::add; } @Override public BinaryOperator<List<T>> combiner() { // 合并两个(部分)结果容器,主要用于并发 return (list1, list2) -> { list1.addAll(list2); return list1; }; } @Override public Function<List<T>, Set<T>> finisher() { // 对结果容器的最终转换 return list -> { Set<T> finalSet = new HashSet<>(); finalSet.addAll(list); return finalSet; }; } @Override publicSet<Characteristics>characteristics(){ return Collections.unmodifiableSet(EnumSet.of(Characteristics.CONCURRENT)); } }
@Test public void customCollectorTest(){ List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 3, 2, 1); Set<Integer> set = integerList.stream().collect(new CustomCollector<>()); assertEquals(4, set.size()); }
并行流
这里仅简单介绍 并行流的使用
,平时一般不使用(因为数据量往往不够大,发挥并行优势有比较多的限制条件, 顺序流
基本能应付日常开发的需求)
简单使用
public static long sum(intm){ return Stream.iterate(1, n -> n + 1).limit(m) .parallel() // 标记为顺序流,只是设置boolean标志位, 只有最后一个parallel或sequential有意义 .filter(n -> n % 2 == 0) .sequential() // 标记为并行流,冗余设置 .map(integer -> integer * 3) .parallel() // 最后一个parallel或sequential影响整个流水线 .reduce(0, Integer::sum); }
性能比较
public static int iterativeSum(intn){ // 原始类型,无需拆箱和装箱操作 int result = 0; for (int i = 0; i < n; i++) { result += i; } return result; } public static int sequentialSum(intm){ // 对象顺序流,需要拆箱和装箱操作 return Stream.iterate(0, n -> n + 1).limit(m).reduce(0, Integer::sum); } public static int iterativeParallelSum(intm){ // 对象并行流,需要拆箱和装箱操作 // 很难将iterate操作划分成独立块来并行处理,因为每次计算依赖上一次的计算结果 // 将流标记为并行,反而增加了开销 // 最慢 return Stream.iterate(0, n -> n + 1).limit(m).parallel().reduce(0, Integer::sum); } public static int rangesequentialSum(intm){ // 数值顺序流,无需拆箱和装箱操作 // 比iterativeParallelSum快 -> 首先选择合适的数据结构,再考虑是否使用并行流 return IntStream.range(0, m).reduce(0, Integer::sum); } public static int rangeParallelSum(intm){ // 数值并行流,无需拆箱和装箱操作 // range操作能够很方便地进行并行处理(RangeIntSpliterator,属于内部原理 Fork/Join的范畴,后续研究) // 最快 return IntStream.range(0, m).parallel().reduce(0, Integer::sum); }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。