一文詳解Java中Stream流的使用
簡介
說明
本文用實例介紹stream的使用。
JDK8新增了Stream(流操作) 處理集合的數(shù)據(jù),可執(zhí)行查找、過濾和映射數(shù)據(jù)等操作。
使用Stream API 對集合數(shù)據(jù)進(jìn)行操作,就類似于使用 SQL 執(zhí)行的數(shù)據(jù)庫查詢??梢允褂?Stream API 來并行執(zhí)行操作。
簡而言之,Stream API 提供了一種高效且易于使用的處理數(shù)據(jù)的方式。
特點(diǎn)
不是數(shù)據(jù)結(jié)構(gòu),不會保存數(shù)據(jù)。
大部分不修改原來的數(shù)據(jù)源,它會將操作后的數(shù)據(jù)保存到另外一個對象中。
peek方法可以修改流中元素
惰性求值,流在中間處理過程中,只對操作進(jìn)行記錄,不會立即執(zhí)行,需等到執(zhí)行終止操作的時候才會進(jìn)行實際的計算。
Stream操作步驟
創(chuàng)建Stream=> 轉(zhuǎn)換Stream(中間操作)=> 產(chǎn)生結(jié)果(終止操作)
注意:這只是一般操作。實際編程時,創(chuàng)建必須有,而中間操作與終止操作是可選的。
操作分類

無狀態(tài):指元素的處理不受之前元素的影響;
有狀態(tài):指該操作只有拿到所有元素之后才能繼續(xù)下去。
非短路操作:指必須處理所有元素才能得到最終結(jié)果;
短路操作:指遇到某些符合條件的元素就可以得到最終結(jié)果,如 A || B,只要A為true,則無需判斷B的結(jié)果。
本文的公共代碼
?class User {
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
操作1:創(chuàng)建流
Collection下的 stream() 和 parallelStream() 方法
List<String> list = new ArrayList<>(); Stream<String> stream = list.stream(); //串行流 Stream<String> parallelStream = list.parallelStream(); //并行流
Arrays 中的 stream() 方法,將數(shù)組轉(zhuǎn)成流
Integer[] nums = new Integer[10]; Stream<Integer> stream = Arrays.stream(nums);
Stream中的靜態(tài)方法:of()、iterate()、generate()
Stream<Integer> stream = Stream.of(1,2,3,4,5,6); stream.forEach(System.out::println); // 輸出:1 2 3 4 5 6 Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 2).limit(6); stream2.forEach(System.out::println); // 輸出:0 2 4 6 8 10 Stream<Double> stream3 = Stream.generate(Math::random).limit(2); stream3.forEach(System.out::println); // 輸出:兩個隨機(jī)數(shù)
BufferedReader.lines() 方法,將每行內(nèi)容轉(zhuǎn)成流
BufferedReader reader = new BufferedReader(new FileReader("F:\\test_stream.txt"));
Stream<String> lineStream = reader.lines();
lineStream.forEach(System.out::println);
Pattern.splitAsStream() 方法,將字符串分隔成流
Pattern pattern = Pattern.compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");
stringStream.forEach(System.out::println);
//輸出:a b c d
操作2:中間操作
篩選(過濾)、去重
方法
| 方法 | 說明 |
|---|---|
| filter | 過濾流中的某些元素(只保留返回值為true的項) |
| limit(n) | 獲取前n個元素 |
| skip(n) | 跳過前n個元素,配合limit(n)可實現(xiàn)分頁 |
| distinct | 通過流中元素的 hashCode() 和 equals() 去除重復(fù)元素 |
單個元素篩選(過濾)、去重、跳過、獲取前n個
List<Integer> list = new ArrayList<>(Arrays.asList(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14));
List<Integer> newList = list.stream()
.filter(s -> s > 5) //6 6 7 9 8 10 12 14 14
.distinct() //6 7 9 8 10 12 14
.skip(2) //9 8 10 12 14
.limit(2) //9 8
.collect(Collectors.toList());
根據(jù)對象屬性去重
List<User> list = new ArrayList<User>() {{
add(new User("Tony", 20, "12"));
add(new User("Pepper", 20, "123"));
add(new User("Tony", 22, "1234"));
add(new User("Tony", 22, "12345"));
}};
//只通過名字去重
List<User> streamByNameList = list.stream().collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getName))), ArrayList::new
));
System.out.println(streamByNameList);
//[User{name='Pepper', age=20, Phone='123'},
// User{name='Tony', age=20, Phone='12'}]
//通過名字和年齡去重
List<User> streamByNameAndAgeList = list.stream().collect(Collectors.collectingAndThen(
Collectors.toCollection(
() -> new TreeSet<>(Comparator.comparing(o -> o.getName() + o.getAge()))), ArrayList::new
));
System.out.println(streamByNameAndAgeList);
//[User{name='Pepper', age=20, Phone='123'},
// User{name='Tony', age=20, Phone='12'},
// User{name='Tony', age=22, Phone='1234'}]
collectingAndThen 這個方法的意思是: 將收集的結(jié)果轉(zhuǎn)換為另一種類型。
因此上面的方法可以理解為:把 new TreeSet<>(Comparator.comparingLong(BookInfoVo::getRecordId))這個set轉(zhuǎn)換為 ArrayList。
映射
方法
| 方法 | 說明 |
|---|---|
| map | 函數(shù)作為參數(shù),該函數(shù)被應(yīng)用到每個元素,并將其映射成一個新的元素。新值類型可以和原來的元素的類型不同。 |
| flatMap | 函數(shù)作為參數(shù),將流中每個值換成另一個流,再把所有流連成一個流。 新值類型可以和原來的元素的類型不同。 |
| mapToInt/Long/Double | 跟map差不多。只是將其轉(zhuǎn)為基本類型。 |
| flatMapToInt/Long/Double | 跟flatMap差不多。只是將其轉(zhuǎn)為基本類型。 |
新值類型和原來的元素的類型相同示例
List<String> list = Arrays.asList("a,b,c", "1,2,3");
//將每個元素轉(zhuǎn)成一個新的且不帶逗號的元素
Stream<String> s1 = list.stream().map(s -> s.replaceAll(",", ""));
s1.forEach(System.out::println);
// abc 123
Stream<String> s2 = list.stream().flatMap(s -> {
//將每個元素轉(zhuǎn)換成一個stream
String[] split = s.split(",");
Stream<String> s3 = Arrays.stream(split);
return s3;
});
s2.forEach(System.out::println);
// a b c 1 2 3
新值類型和原來的元素的類型不同示例
User u1 = new User("aa", 10);
User u2 = new User("bb", 20);
User u3 = new User("cc", 10);
List<User> list = Arrays.asList(u1, u2, u3);
Set<Integer> ageSet = list.stream().map(User::getAge).collect(Collectors.toSet());
ageSet.forEach(System.out::println);
//20 10
int[] ageInt = list.stream().map(User::getAge).mapToInt(Integer::intValue).toArray();
//下邊這樣也可以
//Integer[] ages = list.stream.map(User::getAge).toArray(Integer[]::new);
for (int i : ageInt) {
System.out.println(i);
}
//10 20 10
map的原型為:<R> Stream<R> map(Function<? super T, ? extends R> mapper);
上邊例子中,將Student::getAge作為參數(shù),其實際為:<R> Stream<Integer> map(Function<? super Student, ? extends Integer> mapper);
排序
方法
| 方法 | 說明 |
|---|---|
| sorted() | 自然排序,流中元素需實現(xiàn)Comparable接口。 例:list.stream().sorted() |
| sorted(Comparator com) | 定制排序。常用以下幾種: list.stream().sorted(Comparator.reverseOrder()) list.stream().sorted(Comparator.comparing(Student::getAge)) list.stream().sorted(Comparator.comparing(Student::getAge).reversed()) |
示例
List<String> list = Arrays.asList("aa", "ff", "dd");
//String 類自身已實現(xiàn)Comparable接口
list.stream().sorted().forEach(System.out::println);
System.out.println("------------------------------------");
User u1 = new User("dd", 40);
User u2 = new User("bb", 20);
User u3 = new User("aa", 20);
User u4 = new User("aa", 30);
List<User> userList = Arrays.asList(u1, u2, u3, u4);
//按年齡升序
userList.stream().sorted(Comparator.comparing(User::getAge))
.forEach(System.out::println);
System.out.println("------------------------------------");
//先按年齡升序,年齡相同則按姓名升序
userList.stream().sorted(
(o1, o2) -> {
if (o1.getAge().equals(o2.getAge())) {
return o1.getName().compareTo(o2.getName());
} else {
return o1.getAge().compareTo(o2.getAge());
}
}
).forEach(System.out::println);
結(jié)果
aa
dd
ff
------------------------------------
User{name='bb', age=20}
User{name='aa', age=20}
User{name='aa', age=30}
User{name='dd', age=40}
------------------------------------
User{name='aa', age=20}
User{name='bb', age=20}
User{name='aa', age=30}
User{name='dd', age=40}
消費(fèi)
方法
| 方法 | 說明 |
|---|---|
| peek | 類似于map,能得到流中的每一個元素。 但map接收的是一個Function表達(dá)式,有返回值; 而peek接收的是Consumer表達(dá)式,沒有返回值。 |
示例
User u1 = new User("dd", 40);
User u2 = new User("bb", 20);
User u3 = new User("aa", 20);
User u4 = new User("aa", 30);
List<User> list = Arrays.asList(u1, u2, u3, u4);
List<User> list1 = list.stream()
.peek(o -> o.setAge(100))
.collect(Collectors.toList());
System.out.println(list1);
結(jié)果:
[User{name='dd', age=100}, User{name='bb', age=100}, User{name='aa', age=100}, User{name='aa', age=100}]
操作3:終止操作
匹配、最值、個數(shù)
方法
| 方法 | 說明 |
|---|---|
| allMatch | 接收一個 Predicate 函數(shù),當(dāng)流中每個元素都符合該斷言時才返回true,否則返回false |
| noneMatch | 接收一個 Predicate 函數(shù),當(dāng)流中每個元素都不符合該斷言時才返回true,否則返回false |
| anyMatch | 接收一個 Predicate 函數(shù),只要流中有一個元素滿足該斷言則返回true,否則返回false |
| findFirst | 返回流中第一個元素 |
| findAny | 返回流中的任意元素 |
| count | 返回流中元素的總個數(shù) |
| max | 返回流中元素最大值 |
| min | 返回流中元素最小值 |
實例1:單個類型
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); // 匹配 boolean allMatch = list.stream().allMatch(e -> e > 10); //false boolean noneMatch = list.stream().noneMatch(e -> e > 10); //true boolean anyMatch = list.stream().anyMatch(e -> e > 4); //true // 獲取第一個/第任意個 Integer findFirst = list.stream().findFirst().get(); //1 Integer findAny = list.stream().findAny().get(); //1 // 計數(shù)、最大值、最小值 long count = list.stream().count(); //5 Integer max = list.stream().max(Integer::compareTo).get(); //5 Integer min = list.stream().min(Integer::compareTo).get(); //1
實例2:獲取對象中的字段的最值
User u1 = new User("dd", 40);
User u2 = new User("bb", 20);
User u3 = new User("aa", 20);
User u4 = new User("aa", 30);
List<User> list = Arrays.asList(u1, u2, u3, u4);
//獲取最小年齡的用戶。
User user1 = list.stream()
.min(Comparator.comparing(User::getAge))
.get();
System.out.println(user1);
System.out.println("------------------------------------");
//獲取先按姓名升序,姓名相同則按年齡升序。然后獲取最小的那個(第一個)
User user = list.stream().min((o1, o2) -> {
if (o1.getAge().equals(o2.getAge())) {
return o1.getName().compareTo(o2.getName());
} else {
return o1.getAge().compareTo(o2.getAge());
}
}).get();
System.out.println(user);
結(jié)果
User{name='bb', age=20}
------------------------------------
User{name='aa', age=20}
收集
方法
| 方法 | 說明 |
|---|---|
| collect | 接收一個Collector實例,將流中元素收集成另外一個數(shù)據(jù)結(jié)構(gòu)。 |
Collector實例一般由Collectors的靜態(tài)方法取得。例如:Collectors.toList()
公共代碼
User u1 = new User("dd", 40);
User u2 = new User("bb", 20);
User u3 = new User("aa", 20);
User u4 = new User("aa", 30);
List<User> list = Arrays.asList(u1, u2, u3, u4);
轉(zhuǎn)換
字符串分隔符連接
String joinName = list.stream().map(User::getName).collect(Collectors.joining(",", "(", ")"));
System.out.println(joinName);
//(dd,bb,aa,aa)
轉(zhuǎn)成list
List<Integer> ageList = list.stream().map(User::getAge).collect(Collectors.toList()); System.out.println(ageList); //[40, 20, 20, 30]
轉(zhuǎn)成set
Set<Integer> ageSet = list.stream().map(User::getAge).collect(Collectors.toSet()); System.out.println(ageSet); //[20, 40, 30]
轉(zhuǎn)成map(注:key不能相同,否則報錯)
User s1 = new User("dd", 40);
User s2 = new User("bb", 20);
User s3 = new User("aa", 20);
List<User> list = Arrays.asList(s1, s2, s3);
Map<String, Integer> ageMap = list.stream().collect(Collectors.toMap(User::getName, User::getAge));
System.out.println(ageMap);
//{aa=20, bb=20, dd=40}
本處我將重復(fù)的名字給去掉了一個,因為如果key有重復(fù)的會報錯。
三個參數(shù)的map
第一個參數(shù)就是用來生成key值的,第二個參數(shù)就是用來生成value值的。
第三個參數(shù)用在key值沖突的情況下:若新元素產(chǎn)生的key在Map中已經(jīng)出現(xiàn)過了,第三個參數(shù)就會定義解決的辦法。
User u1 = new User("aa", 10);
User u2 = new User("bb", 20);
User u3 = new User("cc", 10);
User u4 = new User("bb", 30);
List<User> list = new ArrayList<>(Arrays.asList(u1, u2, u3, u4));
Map<String, List<User>> listMap = list.stream().collect(Collectors.toMap(User::getName,
o -> {
List<User> list1 = new ArrayList<>();
list1.add(o);
return list1;
},
(r1, r2) -> {
r1.addAll(r2);
return r1;
}
)
);
System.out.println(listMap);
結(jié)果
{aa=[User{name='aa', age=20}, User{name='aa', age=30}], bb=[User{name='bb', age=20}], dd=[User{name='dd', age=40}]}
聚合
聚合(總數(shù)、平均值、最大最小值等)
//1.用戶總數(shù)
Long count = list.stream().collect(Collectors.counting());
System.out.println(count);
//4
//2.最大年齡 (最小的minBy同理)
Integer maxAge = list.stream().map(User::getAge).collect(Collectors.maxBy(Integer::compare)).get();
System.out.println(maxAge);
//40
//3.所有人的年齡
Integer sumAge = list.stream().collect(Collectors.summingInt(User::getAge));
System.out.println(sumAge);
//110
//4.平均年齡
Double averageAge = list.stream().collect(Collectors.averagingDouble(User::getAge));
System.out.println(averageAge);
// 27.5
// 統(tǒng)計上邊所有數(shù)據(jù)
DoubleSummaryStatistics stat = list.stream().collect(Collectors.summarizingDouble(User::getAge));
System.out.println("count:" + stat.getCount() + " max:" + stat.getMax() + " sum:" + stat.getSum()
+ " average:" + stat.getAverage());
//count:4 max:40.0 sum:110.0 average:27.5
分組
//根據(jù)年齡分組
Map<Integer, List<User>> listMap = list.stream().collect(Collectors.groupingBy(User::getAge));
for (Map.Entry<Integer, List<User>> entry : listMap.entrySet()) {
System.out.println(entry.getKey() + "-->" + entry.getValue());
}
//20-->[User{name='bb', age=20}, User{name='aa', age=20}]
//40-->[User{name='dd', age=40}]
//30-->[User{name='aa', age=30}]
多重分組
// 先根據(jù)年齡分再根據(jù)
Map<Integer, Map<String, List<User>>> ageNameMap = list.stream().collect(
Collectors.groupingBy(User::getAge, Collectors.groupingBy(User::getName)));
for (Map.Entry<Integer, Map<String, List<User>>> entry : ageNameMap.entrySet()) {
System.out.println(entry.getKey() + "-->" + entry.getValue());
}
//20-->{aa=[User{name='aa', age=20}], bb=[User{name='bb', age=20}]}
//40-->{dd=[User{name='dd', age=40}]}
//30-->{aa=[User{name='aa', age=30}]}
分區(qū)
特殊的分組,分為true和false兩組
//分成兩部分,一部分大于10歲,一部分小于等于10歲
Map<Boolean, List<User>> partMap = list.stream().collect(Collectors.partitioningBy(v -> v.getAge() > 20));
for (Map.Entry<Boolean, List<User>> entry : partMap.entrySet()) {
System.out.println(entry.getKey() + "-->" + entry.getValue());
}
//false-->[User{name='bb', age=20}, User{name='aa', age=20}]
//true-->[User{name='dd', age=40}, User{name='aa', age=30}]
總結(jié)
Collector<T, A, R> 是一個接口,有以下5個抽象方法:
1.Supplier<A> supplier():創(chuàng)建一個結(jié)果容器A
2.BiConsumer<A, T> accumulator():消費(fèi)型接口,第一個參數(shù)為容器A,第二個參數(shù)為流中元素T。
3.BinaryOperator<A> combiner():函數(shù)接口,該參數(shù)的作用跟上一個方法(reduce)中的combiner參數(shù)一樣,將并行流中各個子進(jìn)程的運(yùn)行結(jié)果(accumulator函數(shù)操作后的容器A)進(jìn)行合并。
4.Function<A, R> finisher():函數(shù)式接口,參數(shù)為:容器A,返回類型為:collect方法最終想要的結(jié)果R。
5.Set<Characteristics> characteristics():返回一個不可變的Set集合,表明該Collector的特征。有以下三個特征:
- CONCURRENT:表示此收集器支持并發(fā)。(官方文檔還有其他描述,暫時沒去探索,故不作過多翻譯)
- UNORDERED:表示該收集操作不會保留流中元素原有的順序。
- IDENTITY_FINISH:表示finisher參數(shù)只是標(biāo)識而已,可忽略。
注:如果對以上函數(shù)接口不太理解的話,可參考:Java中Lambda表達(dá)式的使用詳細(xì)教程
Collectors.toList() 解析
//toList 源碼
public static <T> Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> {
left.addAll(right);
return left;
}, CH_ID);
}
//為了更好地理解,我們轉(zhuǎn)化一下源碼中的lambda表達(dá)式
public <T> Collector<T, ?, List<T>> toList() {
Supplier<List<T>> supplier = () -> new ArrayList();
BiConsumer<List<T>, T> accumulator = (list, t) -> list.add(t);
BinaryOperator<List<T>> combiner = (list1, list2) -> {
list1.addAll(list2);
return list1;
};
Function<List<T>, List<T>> finisher = (list) -> list;
Set<Collector.Characteristics> characteristics = Collections.unmodifiableSet
(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
return new Collector<T, List<T>, List<T>>() {
@Override
public Supplier supplier() {
return supplier;
}
@Override
public BiConsumer accumulator() {
return accumulator;
}
@Override
public BinaryOperator combiner() {
return combiner;
}
@Override
public Function finisher() {
return finisher;
}
@Override
public Set<Characteristics> characteristics() {
return characteristics;
}
};
}
規(guī)約
方法
| 方法 | 說明 |
|---|---|
| Optional<T> reduce(BinaryOperator<T> accumulator) | 第一次執(zhí)行時,accumulator函數(shù)的第一個參數(shù)為流中的第一個元素,第二個參數(shù)為流中元素的第二個元素; 第二次執(zhí)行時,第一個參數(shù)為第一次函數(shù)執(zhí)行的結(jié)果,第二個參數(shù)為流中的第三個元素; 依次類推。 |
| T reduce(T identity, BinaryOperator<T> accumulator) | 流程跟上面一樣,只是第一次執(zhí)行時,accumulator函數(shù)的第一個參數(shù)為identity,而第二個參數(shù)為流中的第一個元素。 |
| <U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner) | 在串行流(stream)中,該方法跟第二個方法一樣,即第三個參數(shù)combiner不會起作用。在并行流(parallelStream)中,我們知道流被fork join出多個線程進(jìn)行執(zhí)行,此時每個線程的執(zhí)行流程就跟第二個方法reduce(identity,accumulator)一樣,而第三個參數(shù)combiner函數(shù),則是將每個線程的執(zhí)行結(jié)果當(dāng)成一個新的流,然后使用第一個方法reduce(accumulator)流程進(jìn)行規(guī)約。 |
示例
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Integer v = list.stream().reduce((x1, x2) -> x1 + x2).get();
System.out.println(v);
// 15
Integer v0 = list.stream().reduce((x1, x2) -> x1 + x2).get();
System.out.println(v0);
//15
Integer v1 = list.stream().reduce(10, (x1, x2) -> x1 + x2);
System.out.println(v1);
//25
Integer v2 = list.stream().reduce(0,
(x1, x2) -> {
System.out.println("stream accumulator: x1:" + x1 + " x2:" + x2);
return x1 - x2;
},
(x1, x2) -> {
System.out.println("stream combiner: x1:" + x1 + " x2:" + x2);
return x1 * x2;
});
System.out.println(v2);
// -15
Integer v3 = list.parallelStream().reduce(0,
(x1, x2) -> {
System.out.println("parallelStream accumulator: x1:" + x1 + " x2:" + x2);
return x1 - x2;
},
(x1, x2) -> {
System.out.println("parallelStream combiner: x1:" + x1 + " x2:" + x2);
return x1 * x2;
});
System.out.println(v3);
//-120打印結(jié)果為:
15
15
25
stream accumulator: x1:0 x2:1
stream accumulator: x1:-1 x2:2
stream accumulator: x1:-3 x2:3
stream accumulator: x1:-6 x2:4
stream accumulator: x1:-10 x2:5
-15
parallelStream accumulator: x1:0 x2:3
parallelStream accumulator: x1:0 x2:5
parallelStream accumulator: x1:0 x2:4
parallelStream combiner: x1:-4 x2:-5
parallelStream accumulator: x1:0 x2:2
parallelStream accumulator: x1:0 x2:1
parallelStream combiner: x1:-3 x2:20
parallelStream combiner: x1:-1 x2:-2
parallelStream combiner: x1:2 x2:-60
-120
到此這篇關(guān)于一文詳解Java中Stream流的使用的文章就介紹到這了,更多相關(guān)Java Stream流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java基礎(chǔ)-Java基本數(shù)據(jù)類型
這篇文章主要介紹了Java基礎(chǔ)-Java基本數(shù)據(jù)類型,變量就是申請內(nèi)存來存儲值。也就是說,當(dāng)創(chuàng)建變量的時候,需要在內(nèi)存中申請空間,下面我們就來對Java基本數(shù)據(jù)類型作簡單的介紹,需要的朋友可以參考一下2022-01-01
Java實現(xiàn)根據(jù)前端所要格式返回樹形3級層級數(shù)據(jù)
這篇文章主要為大家詳細(xì)介紹了Java如何實現(xiàn)根據(jù)前端所要格式返回樹形3級層級數(shù)據(jù),文中的示例代碼講解詳細(xì),有需要的小伙伴可以了解下2024-02-02
Spring session實現(xiàn)共享單點(diǎn)登錄案例過程解析
這篇文章主要介紹了Spring session實現(xiàn)共享單點(diǎn)登錄案例過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-07-07

