3分鍾看完Java 8——史上最強Java 8新特性總結之第二篇 Stream API


目錄

· 概況

· 切片(Slicing)

· 映射(Mapping)

· 匹配(Matching)

· 查找(Finding)

· 歸約(Reducing)

· 排序(Sorting)

· 數值流(Numeric streams)

    · 原始類型流(Primitive stream)

    · 數值范圍(Numeric ranges)

· 構建流

    · 由值創建流

    · 由數組創建流

    · 由文件生成流

    · 由函數生成流(創建無限流)

· collect()高級用法

    · 分組(Grouping)

    · 分區(Partitioning)

· 並行流


 

概況

1. Stream API:以聲明性方式處理數據集合,即說明想要完成什么(比如篩選熱量低的菜餚)而不是說明如何實現一個操作(利用循環和if條件等控制流語句)。

2. Stream API特點

    a) 流水線:很多流操作本身會返回一個流,這樣多個操作就可以鏈接起來,形成一個大的流水線。這讓可實現延遲和短路優化。

    b) 內部迭代:與使用迭代器顯式迭代的集合不同,流的迭代操作是在背后進行的。

3. Stream(流):從支持數據處理操作的源生成的元素序列(A sequence of elements from a source that supports data processing operations)。

    a) 元素序列:與集合類似,流也提供了一個接口(java.util.stream.Stream),可以訪問特定元素類型的一組有序值。因為集合是數據結構,所以它的主要目的是以特定的時間/空間復雜度存儲和訪問元素(如ArrayList、LinkedList);但流的目的在於表達計算,比如filter、sorted和map。

    b) 源:流會使用一個提供數據的源,如集合、數組或輸入/輸出。注意,從有序集合生成流時會保留原有的順序。

    c) 數據處理操作:流的數據處理功能支持類似於數據庫的操作,以及函數式編程語言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以順序執行,也可並行執行。

4. 流操作分類

    a) 中間操作(Intermediate Operations):可以連接起來的流操作,並不會生成任何結果。

    b) 終端操作(Terminal Operations):關閉流的操作,處理流水線以返回結果。

    c) 常用中間操作

操作

返回類型

操作參數

函數描述符

filter

Stream<T>

Predicate<T>

T -> boolean

map

Stream<R>

Function<T, R>

T -> R

limit

Stream<T>

 

 

sorted

Stream<T>

Comparator<T>

(T, T) -> R

distinct

Stream<T>

 

 

    d) 常用終端操作 

操作

目的

forEach

消費流中的每個元素並對其應用Lambda。這一操作返回void

count

返回流中元素的個數。這一操作返回long

collect

把流歸約成一個集合,比如ListMap甚至是Integer

5. 舉例

    a) Dish.java(后續舉例將多次使用到該類)

 1 public class Dish {
 2     private final String name;
 3     private final boolean vegetarian;
 4     private final int calories;
 5     private final Type type;
 6 
 7     public enum Type {MEAT, FISH, OTHER}
 8 
 9     public Dish(String name, boolean vegetarian, int calories, Type type) {
10         this.name = name;
11         this.vegetarian = vegetarian;
12         this.calories = calories;
13         this.type = type;
14     }
15 
16     public String getName() {
17         return name;
18     }
19 
20     public boolean isVegetarian() {
21         return vegetarian;
22     }
23 
24     public int getCalories() {
25         return calories;
26     }
27 
28     public Type getType() {
29         return type;
30     }
31 
32     @Override
33     public String toString() {
34         return name;
35     }
36 
37 }

    b) DishUtils.java(后續舉例將多次使用到該類)

 1 import java.util.Arrays;
 2 import java.util.List;
 3 
 4 public class DishUtils {
 5 
 6     public static List<Dish> makeMenu() {
 7         return Arrays.asList(
 8                 new Dish("pork", false, 800, Dish.Type.MEAT),
 9                 new Dish("beef", false, 700, Dish.Type.MEAT),
10                 new Dish("chicken", false, 400, Dish.Type.MEAT),
11                 new Dish("french fries", true, 530, Dish.Type.OTHER),
12                 new Dish("rice", true, 350, Dish.Type.OTHER),
13                 new Dish("season fruit", true, 120, Dish.Type.OTHER),
14                 new Dish("pizza", true, 550, Dish.Type.OTHER),
15                 new Dish("prawns", false, 300, Dish.Type.FISH),
16                 new Dish("salmon", false, 450, Dish.Type.FISH));
17     }
18 
19     public static <T> void printList(List<T> list) {
20         for (T i : list) {
21             System.out.println(i);
22         }
23     }
24 
25 }

    c) Test.java

 1 import java.util.List;
 2 
 3 import static java.util.stream.Collectors.toList;
 4 
 5 public class Test {
 6 
 7     public static void main(String[] args) {
 8         List<String> names = DishUtils.makeMenu().stream() // 獲取流
 9                 .filter(d -> d.getCalories() > 300) // 中間操作,選出高熱量菜
10                 .map(Dish::getName) // 中間操作,獲取菜名
11                 .limit(3) // 中間操作,選出前三
12                 .collect(toList()); // 終端操作,將結果保存在List中
13         DishUtils.printList(names);
14 
15         DishUtils.makeMenu().stream()
16                 .filter(d -> d.getCalories() > 300)
17                 .map(Dish::getName)
18                 .limit(3)
19                 .forEach(System.out::println); // 遍歷並打印
20     }
21 
22 }

    d) 示意圖

篩選(Filtering)

1. 篩選相關方法

    a) filter()方法:使用Predicate篩選流中元素。

    b) distinct()方法:調用流中元素的hashCode()和equals()方法去重元素。

2. 舉例

 1 import java.util.Arrays;
 2 import java.util.List;
 3 import static java.util.stream.Collectors.toList;
 4 // filter()方法
 5 List<Dish> vegetarianMenu = DishUtils.makeMenu().stream()
 6         .filter(Dish::isVegetarian)
 7         .collect(toList());
 8 DishUtils.printList(vegetarianMenu);
 9 System.out.println("-----");
10 // distinct()方法
11 List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
12 numbers.stream()
13         .filter(i -> i % 2 == 0)
14         .distinct()
15         .forEach(System.out::println);

切片(Slicing)

1. 切片相關方法

    a) limit()方法:返回一個不超過給定長度的流。

    b) skip()方法:返回一個扔掉了前n個元素的流。如果流中元素不足n個,則返回一個空流。

2. 舉例

 1 import java.util.List;
 2 import static java.util.stream.Collectors.toList;
 3 // limit()方法
 4 List<Dish> dishes1 = DishUtils.makeMenu().stream()
 5         .filter(d -> d.getCalories() > 300)
 6         .limit(3)
 7         .collect(toList());
 8 DishUtils.printList(dishes1);
 9 System.out.println("-----");
10 // skip()方法
11 List<Dish> dishes2 = DishUtils.makeMenu().stream()
12         .filter(d -> d.getCalories() > 300)
13         .skip(2)
14         .collect(toList());
15 DishUtils.printList(dishes2);

映射(Mapping)

1. 映射相關方法

    a) map()方法:接受一個函數作為參數,該函數用於將每個元素映射成一個新的元素。

    b) flatMap()方法:接受一個函數作為參數,該函數用於將每個數組元素映射成新的扁平化流。

    c) 注意:map()、flatMap()方法都不會修改原元素。

2. 舉例

 1 import java.util.Arrays;
 2 import java.util.List;
 3 import static java.util.stream.Collectors.toList;
 4 // map()方法
 5 List<Integer> dishNameLengths = DishUtils.makeMenu().stream()
 6         .map(Dish::getName)
 7         .map(String::length)
 8         .collect(toList());
 9 DishUtils.printList(dishNameLengths);
10 System.out.println("-----");
11 // flatMap()方法
12 String[] arrayOfWords = {"Goodbye", "World"};
13 Arrays.stream(arrayOfWords)
14         .map(w -> w.split("")) // 將每個單詞轉換為由其字母構成的數組
15         .flatMap(Arrays::stream) // 將各個生成流扁平化為單個流
16         .distinct() // 去重
17         .forEach(System.out::println);

匹配(Matching)

1. 匹配相關方法

    a) anyMatch()方法:檢查流中是否有一個元素能匹配給定的Predicate。

    b) allMatch()方法:檢查流中是否所有元素能匹配給定的Predicate。

    c) noneMatch()方法:檢查流中是否所有元素都不匹配給定的Predicate。

2. 舉例

 1 // anyMatch()方法
 2 if (DishUtils.makeMenu().stream().anyMatch(Dish::isVegetarian)) {
 3     System.out.println("The menu is (somewhat) vegetarian friendly!!");
 4 }
 5 // allMatch()方法
 6 boolean isHealthy1 = DishUtils.makeMenu().stream()
 7         .allMatch(d -> d.getCalories() < 1000);
 8 System.out.println(isHealthy1);
 9 // noneMatch()方法
10 boolean isHealthy2 = DishUtils.makeMenu().stream()
11         .noneMatch(d -> d.getCalories() >= 1000);
12 System.out.println(isHealthy2);

查找(Finding)

1. 查找相關方法

    a) findAny()方法:返回當前流中的任意元素,返回類型為java.util.Optional(Java 8用於解決NullPointerException的新類)。

    b) findFirst()方法:與findAny()方法類似,區別在於返回第一個元素。

2. 舉例

 1 import java.util.Arrays;
 2 import java.util.List;
 3 import java.util.Optional;
 4 // findAny()方法
 5 Optional<Dish> dish = DishUtils.makeMenu().stream()
 6         .filter(Dish::isVegetarian)
 7         .findAny();
 8 System.out.println(dish.get()); // french fries
 9 // findFirst()方法
10 List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
11 Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
12                 .map(x -> x * x)
13                 .filter(x -> x % 3 == 0)
14                 .findFirst(); // 9
15 System.out.println(firstSquareDivisibleByThree.get());

歸約(Reducing)

1. 歸約相關方法

    a) reduce()方法:把一個流中的元素組合起來,也叫折疊(fold)。

        i. 如果指定初始值,則直接返回歸約結果值。

        ii. 如果不指定初始值,則返回Optional。

2. 舉例

 1 import java.util.ArrayList;
 2 import java.util.List;
 3 import java.util.Optional;
 4 List<Integer> numbers = new ArrayList<>();
 5 for (int n = 1; n <= 100; n++) {
 6     numbers.add(n);
 7 }
 8 // 元素求和
 9 int sum1 = numbers.stream().reduce(0, (a, b) -> a + b); // 指定初始值0
10 System.out.println(sum1);
11 Optional<Integer> sum2 = numbers.stream().reduce((a, b) -> a + b); // 不指定初始值0
12 System.out.println(sum2);
13 int sum3 = numbers.stream().reduce(0, Integer::sum); // 方法引用
14 System.out.println(sum3);
15 // 最大值
16 Optional<Integer> max1 = numbers.stream().reduce((a, b) -> a < b ? b : a); // Lambda表達式
17 System.out.println(max1);
18 Optional<Integer> max2 = numbers.stream().reduce(Integer::max); // 方法引用
19 System.out.println(max2);
20 // 統計個數
21 int count1 = DishUtils.makeMenu().stream()
22         .map(d -> 1)
23         .reduce(0, (a, b) -> a + b); // MapReduce編程模型,更易並行化
24 System.out.println(count1);
25 long count2 = DishUtils.makeMenu().stream().count();
26 System.out.println(count2);

排序(Sorting)

1. 排序相關方法

    a) sorted()方法:根據指定的java.util.Comparator規則排序。

2. 舉例

1 import static java.util.Comparator.comparing;
2 DishUtils.makeMenu().stream()
3         .sorted(comparing(Dish::getCalories))
4         .forEach(System.out::println);

數值流(Numeric streams)

原始類型流(Primitive stream)

1. 使用目的:避免自動裝箱帶來的開銷。

2. 相關方法

    a) mapToInt():將流轉換為原始類型流IntStream。

    b) mapToDouble():將流轉換為原始類型流DoubleStream。

    c) mapToLong():將流轉換為原始類型流LongStream。

    d) boxed():將原始類型流轉換為對象流。

    3. Optional的原始類型版本:OptionalInt、OptionalDouble和OptionalLong。

    4. 舉例

 1 import java.util.OptionalInt;
 2 import java.util.stream.IntStream;
 3 import java.util.stream.Stream;
 4 // 映射到數值流
 5 int calories = DishUtils.makeMenu().stream() // 返回Stream<Dish>
 6         .mapToInt(Dish::getCalories) // 返回IntStream
 7         .sum();
 8 System.out.println(calories);
 9 // 轉換回對象流
10 IntStream intStream = DishUtils.makeMenu().stream().mapToInt(Dish::getCalories); // 將Stream 轉換為數值流
11 Stream<Integer> stream = intStream.boxed(); // 將數值流轉換為Stream
12 // OptionalInt
13 OptionalInt maxCalories = DishUtils.makeMenu().stream()
14         .mapToInt(Dish::getCalories)
15         .max();
16 int max = maxCalories.orElse(1); // 如果沒有最大值的話,顯式提供一個默認最大值
17 System.out.println(max);

數值范圍(Numeric ranges)

1. 數值范圍相關方法

    a) range()方法:生成起始值到結束值范圍的數值,不包含結束值。

    b) rangeClosed()方法:生成起始值到結束值范圍的數值,包含結束值。

2. 舉例

1 import java.util.stream.IntStream;
2 IntStream.range(1, 5).forEach(System.out::println); // 1~4
3 IntStream.rangeClosed(1, 5).forEach(System.out::println); // 1~5

構建流

由值創建流

1. 舉例

    a) Stream.of()方法

1 import java.util.stream.Stream;
2 Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
3 stream.map(String::toUpperCase).forEach(System.out::println);

    b) 空流

1 import java.util.stream.Stream;
2 Stream<String> emptyStream = Stream.empty();

由數組創建流

1. 舉例

1 int[] numbers = {2, 3, 5, 7, 11, 13};
2 int sum = Arrays.stream(numbers).sum();
3 System.out.println(sum); // 41

由文件生成流

1. 舉例

1 try (Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
2     long uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
3             .distinct()
4             .count();
5     System.out.println(uniqueWords);
6 } catch (IOException e) {
7     e.printStackTrace();
8 }

由函數生成流(創建無限流)

1. 無限流:沒有固定大小的流。

2. 相關方法

    a) Stream.iterate()方法:生成無限流,其初始值為第1個參數,下一個值由第2個參數的Lambda表達式生成。

    b) Stream.generate()方法:生成無限流,其值由參數的Lambda表達式生成。

3. 注意:一般,應該使用limit(n)對無限流加以限制,以避免生成無窮多個值。

4. 舉例

1 Stream.iterate(0, n -> n + 2)
2         .limit(5)
3         .forEach(System.out::println); // 0 2 4 6 8
4 Stream.generate(Math::random)
5         .limit(5)
6         .forEach(System.out::println);

collect()高級用法

歸約和匯總(Reducing and summarizing)

1. 舉例

    a) 按元素某字段查找最大值

1 import java.util.Comparator;
2 import java.util.Optional;
3 import static java.util.stream.Collectors.maxBy;
4 Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
5 Optional<Dish> mostCalorieDish = DishUtils.makeMenu().stream()
6         .collect(maxBy(dishCaloriesComparator));
7 System.out.println(mostCalorieDish);

    b) 按元素某字段求和

1 import static java.util.stream.Collectors.summingInt;
2 int totalCalories = DishUtils.makeMenu().stream().collect(summingInt(Dish::getCalories));
3 System.out.println(totalCalories);

    c) 按元素某字段求平均值

1 import static java.util.stream.Collectors.averagingInt;
2 double avgCalories = DishUtils.makeMenu().stream().collect(averagingInt(Dish::getCalories));
3 System.out.println(avgCalories);

    d) 連接字符串

1 import static java.util.stream.Collectors.joining;
2 String shortMenu = DishUtils.makeMenu().stream().map(Dish::getName).collect(joining(", "));
3 System.out.println(shortMenu);

    e) 廣義歸約

 1 // 所有熱量求和
 2 import static java.util.stream.Collectors.reducing;
 3 // i.e.
 4 // int totalCalories = DishUtils.makeMenu().stream()
 5 //         .mapToInt(Dish::getCalories) // 轉換函數
 6 //         .reduce(0, Integer::sum); // 初始值、累積函數
 7 int totalCalories = DishUtils.makeMenu().stream()
 8         .collect(reducing(
 9                 0, // 初始值
10                 Dish::getCalories, // 轉換函數
11                 Integer::sum)); // 累積函數
12 System.out.println(totalCalories);

分組(Grouping)

1. 分組:類似SQL語句的group by,區別在於這里的分組可聚合(即SQL的聚合函數),也可不聚合。

2. 舉例

    a) 簡單分組

1 Map<Dish.Type, List<Dish>> dishesByType = DishUtils.makeMenu().stream()
2                 .collect(groupingBy(Dish::getType));
3 System.out.println(dishesByType); // {FISH=[prawns, salmon], MEAT=[pork, beef, chicken], OTHER=[french fries, rice, season fruit, pizza]}

    b) 復雜分組

1 import static java.util.stream.Collectors.groupingBy;
2 public enum CaloricLevel {DIET, NORMAL, FAT}
3 Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = DishUtils.makeMenu().stream().collect(
4         groupingBy(dish -> {
5             if (dish.getCalories() <= 400) return CaloricLevel.DIET;
6             else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
7             else return CaloricLevel.FAT;
8         }));
9 System.out.println(dishesByCaloricLevel); // {NORMAL=[beef, french fries, pizza, salmon], DIET=[chicken, rice, season fruit, prawns], FAT=[pork]}

    c) 多級分組

 1 import static java.util.stream.Collectors.groupingBy;
 2 public enum CaloricLevel {DIET, NORMAL, FAT}
 3 Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = DishUtils.makeMenu().stream().collect(
 4         groupingBy(Dish::getType, // 一級分類函數
 5                 groupingBy(dish -> { // 二級分類函數
 6                     if (dish.getCalories() <= 400) return CaloricLevel.DIET;
 7                     else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
 8                     else return CaloricLevel.FAT;
 9                 })
10         )
11 );
12 System.out.println(dishesByTypeCaloricLevel);
13 // {FISH={NORMAL=[salmon], DIET=[prawns]}, MEAT={NORMAL=[beef], DIET=[chicken], FAT=[pork]}, OTHER={NORMAL=[french fries, pizza], DIET=[rice, season fruit]}}

    d) 分組聚合

 1 import static java.util.Comparator.comparingInt;
 2 import static java.util.stream.Collectors.groupingBy;
 3 import static java.util.stream.Collectors.counting;
 4 Map<Dish.Type, Long> typesCount = DishUtils.makeMenu().stream()
 5         .collect(groupingBy(Dish::getType, counting()));
 6 System.out.println(typesCount); // {FISH=2, MEAT=3, OTHER=4}
 7 
 8 Map<Dish.Type, Optional<Dish>> mostCaloricByType1 = DishUtils.makeMenu().stream()
 9                 .collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
10 System.out.println(mostCaloricByType1); // {FISH=Optional[salmon], MEAT=Optional[pork], OTHER=Optional[pizza]}
11 
12 Map<Dish.Type, Dish> mostCaloricByType2 = DishUtils.makeMenu().stream()
13                 .collect(groupingBy(Dish::getType, // 分類函數
14                         collectingAndThen(
15                                 maxBy(comparingInt(Dish::getCalories)), // 包裝后的收集器
16                                 Optional::get))); // 轉換函數
17 System.out.println(mostCaloricByType2); // {FISH=salmon, MEAT=pork, OTHER=pizza}

分區(Partitioning)

1. 分區:分區是分組的特殊情況,即根據Predicate<T>分組為true和false兩組,因此分組后的Map的Key是Boolean類型。

2. 舉例

 1 import java.util.List;
 2 import java.util.Map;
 3 import java.util.Optional;
 4 import static java.util.Comparator.comparingInt;
 5 import static java.util.stream.Collectors.*;
 6 Map<Boolean, List<Dish>> partitionedMenu = DishUtils.makeMenu().stream()
 7         .collect(partitioningBy(Dish::isVegetarian));
 8 System.out.println(partitionedMenu);
 9 // {false=[pork, beef, chicken, prawns, salmon], true=[french fries, rice, season fruit, pizza]}
10 
11 Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = DishUtils.makeMenu().stream()
12         .collect(partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType)));
13 System.out.println(vegetarianDishesByType);
14 // {false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}, true={OTHER=[french fries, rice, season fruit, pizza]}}
15 
16 Map<Boolean, Dish> mostCaloricPartitionedByVegetarian = DishUtils.makeMenu().stream()
17         .collect(partitioningBy(Dish::isVegetarian, collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get)));
18 System.out.println(mostCaloricPartitionedByVegetarian);
19 // {false=pork, true=pizza}

並行流

1. 並行流:一個把內容分成多個數據塊,並用不同的線程分別處理每個數據塊的流。

2. 並行流相關方法

    a) parallel()方法:將順序流轉換為並行流。

    b) sequential()方法:將並行流轉換為順序流。

    c) 以上兩方法並沒有對流本身有任何實際的變化,只是在內部設了一個boolean標志,表示讓調用parallel()/sequential()之后進行的所有操作都並行/順序執行。

3. 並行流原理:並行流內部默認使用ForkJoinPool,其默認的線程數為CPU核數(通過Runtime.getRuntime().availableProcessors()獲取),同時支持通過系統屬性設置(全局),比如:

System.setProperty('java.util.concurrent.ForkJoinPool.common.parallelism','12');

4. 何時並行流更有效?

    a) 實測:在待運行的特定機器上,分別用順序流和並行流做基准測試性能。

    b) 注意裝/拆箱:自動裝箱和拆箱會大大降低性能,應避免。

    c) 某些操作性能並行流比順序流差:比如limit()和findFirst(),因為在並行流上執行代價較大。

    d) 計算流操作流水線的總成本:設N是要處理的元素的總數,Q是一個元素通過流水線的大致處理成本,則N*Q就是這個對成本的一個粗略的定性估計。Q值較高就意味着使用並行流時性能好的可能性比較大。

    e) 數據量較小時並行流比順序流性能差:因為並行化會有額外開銷。

    f) 流背后的數據結構是否易於分解:見下表。

數據結構

可分解性

ArrayList

極佳

LinkedList

IntStream.range

極佳

Stream.iterate

HashSet

TreeSet

    g) 流自身特點、流水線的中間操作修改流的方式,都可能會改變分解過程的性能:比如未執行篩選操作時,流被分成大小差不多的幾部分,此時並行執行效率很高;但執行篩選操作后,可能導致這幾部分大小相差較大,此時並行執行效率就較低。

    h) 終端操作合並步驟的代價:如果該步驟代價很大,那么合並每個子流產生的部分結果所付出的代價就可能會超出通過並行流得到的性能提升。

5. 舉例

 1 // 順序流
 2 long sum1 = Stream.iterate(1L, i -> i + 1)
 3         .limit(8)
 4         .reduce(0L, Long::sum);
 5 System.out.println(sum1);
 6 // 並行流
 7 long sum2 = Stream.iterate(1L, i -> i + 1)
 8         .limit(8)
 9         .parallel()
10         .reduce(0L, Long::sum);
11 System.out.println(sum2);

 

作者:netoxi
出處:http://www.cnblogs.com/netoxi
本文版權歸作者和博客園共有,歡迎轉載,未經同意須保留此段聲明,且在文章頁面明顯位置給出原文連接。歡迎指正與交流。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM