资讯专栏INFORMATION COLUMN

Java8 实战总结

lookSomeone / 2002人阅读

摘要:一个抽象类可以通过实例变量字段保存一个通用状态,而接口是不能有实例变量的。分组和分区分组多级分组分区是分组的特殊情况由一个谓词返回一个布尔值的函数作为分类函数,它称分区函数。

一、基本概念

两个新特性:

1、函数式编程简化代码复杂度(引入了Lambda表达式)
2、更高效的利用多核CPU
1、基本概念:
1、Lambda基本语法
(parameters) -> expression
对应:参数->表达式
 或(请注意语句的花括号)
(parameters) -> { statements; }
对应:参数->语句

根据上述语法规则,以下哪个不是有效的Lambda表达式?

(1) () -> {}
(2) () -> "Raoul"
(3) () -> {return "Mario";}
(4) (Integer i) -> return "Alan" + i;
(5) (String s) -> {"IronMan";}

答案:只有4和5是无效的Lambda。

(1) 这个Lambda没有参数,并返回void。它类似于主体为空的方法:public void run() {}
(2) 这个Lambda没有参数,并返回String作为表达式。
(3) 这个Lambda没有参数,并返回String(利用显式返回语句)。
(4) return是一个控制流语句。要使此 Lambda有效,需要使花括号,如下所示:
(Integer i) -> {return "Alan" + i;}。
(5)"Iron Man"是一个表达式,不是一个语句。要使此 Lambda有效,你可以去除花括号
和分号,如下所示:(String s) -> "Iron Man"。或者如果你喜欢,可以使用显式返回语
句,如下所示:(String s)->{return "IronMan";}

2、函数式接口
函数式接口就是只定义一个抽象方法的接口。

 
 下列哪些式函数式接口:

(1)public interface Adder{
   int add(int a, int b);
   }
(2)public interface SmartAdder extends Adder{
  int add(double a, double b);
  }
(3)public interface Nothing{
 
  }
  

只有(1)是函数式接口,按照定义其他都不是。   
3、Java 8中的抽象类和抽象接口

(1)一个类只能继承一个抽象类,但是一个类可以实现多个接口。
(2)一个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变量的。

4、多个接口都实现同样的默认方法,子类继承时的规则:
(1)  类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
(2)  如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,
优先选择拥有最具体实现的默认方法的接口,即如果B 继承了A ,那么B 就比A 更加具体。
(3)  最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,
显式地选择使用哪一个默认方法的实现。
5、教你一步步完成普通方法到Lambda方法的改造
目标:对于一个`Apple`列表(`inventory`)按照重量进行排序
最终实现:`inventory.sort(comparing(Apple::getWeight))`

实现分析:
Java 8API已经为你提供了一个List可用的sort方法,你不用自己去实现它。
那么最困难的部分已经搞定了!但是,如何把排序策略传递给sort方法呢?你看,sort方法的签名是这样的:

void sort(Comparator c)

它需要一个Comparator对象来比较两个Apple!这就是在 Java中传递策略的方式:它们必须包裹在一个对象里。
我们说sort的行为被参数化了:传递给它的排序策略不同,其行为也会不同。

step1:
public class AppleComparator implements Comparator {
    
    public int compare(Apple a1, Apple a2){
      return a1.getWeight().compareTo(a2.getWeight());
    }
  
 }
 inventory.sort(new AppleComparator());
step2:
   你在前面看到了,你可以使用匿名类来改进解决方案,
   而不是实现一个`Comparator`却只实例化一次:
inventory.sort(new Comparator() {
   public int compare(Apple a1, Apple a2){
    return a1.getWeight().compareTo(a2.getWeight());
  }
});
step3:
使用Lambda 表达式

你的解决方案仍然挺啰嗦的。Java 8引入了Lambda表达式,
它提供了一种轻量级语法来实现相同的目标:传递代码。

你看到了,在需要函数式接口的地方可以使用Lambda表达式。
我们回顾一下:函数式接口就是仅仅定义一个抽象方法的接口。

抽象方法的签名(称为函数描述符)描述了Lambda表达式的签名。
在这个例子里,Comparator代表了函数描述符(T, T) -> int。

因为你用的是苹果,所以它具体代表的就是(Apple, Apple) -> int。

改进后的新解决方案看上去就是这样的了:
inventory.sort((Apple a1, Apple a2)->a1.getWeight().compareTo(a2.getWeight()));


 我们前面解释过了,Java 编译器可以根据Lambda 出现的上下文来推断
 Lambda 表达式参数的类型 。那么你的解决方案就可以重写成这样:

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));


 你的代码还能变得更易读一点吗?Comparator 具有一个叫作comparing
 的静态辅助方法,它可以接受一个Functio 来提取Comparable 键值,并
 生成一个Comparator 对象。
 
 它可以像下面这样用(注意你现在传递的Lambda 只有一个参数:Lambda 
 说明了如何从苹果中提取需要比较的键值):

Comparator c = Comparator.comparing((Apple a) -> a.getWeight());

现在你可以把代码再改得紧凑一点了:


 import static java.util.Comparator.comparing;
 inventory.sort(comparing((a) -> a.getWeight()));

step4:
使用方法引用
前面解释过,方法引用就是替代那些转发参数的Lambda表达式的语法糖。你可以用方法引用让你的代码更简洁

假设你静态导入了
 import java.util.Comparator.comparing;
 inventory.sort(comparing(Apple::getWeight));






二、流简介
  流是`Java API`的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。
  流可以并行的处理集合数据,无需编写复杂的多线程代码。
public class Dish {
   private final String name;
   private final boolean vegetarian;
   private final int calories;
   private final Type type;

   public Dish(String name, boolean vegetarian, int calories, Type type) {
       this.name = name;
       this.vegetarian = vegetarian;
       this.calories = calories;
       this.type = type;
   }

   public String getName() {
       return name;
   }

   public boolean isVegetarian() {
       return vegetarian;
   }

   public int getCalories() {
       return calories;
   }

   public Type getType() {
       return type;
   }

   public enum Type { MEAT, FISH, OTHER }

}

目标:筛选出盘子中热量高于400的食物的名称,并按按照热量高低排序

方法一:用普通集合方式处理
List lowCaloricDishes = new ArrayList<>();
 //1、筛选 
 for(Dish d: menu){
    if(d.getCalories() < 400){
     lowCaloricDishes.add(d);
    }
 }
  //2、排序
  Collections.sort(lowCaloricDishes, new Comparator() {
  
    public int compare(Dish d1, Dish d2){
        return Integer.compare(d1.getCalories(), d2.getCalories());
    }
});

 //3、统计
 List lowCaloricDishesName = new ArrayList<>();

 for(Dish d: lowCaloricDishes){
    lowCaloricDishesName.add(d.getName());
 }
方法二:用流处理

List lowCaloricDishesName = menu.stream()
         .filter(d->d.getCalories()<400)// 筛选
         .sorted(comparing(Dish::getCalories))//排序
         .map(d->d.getName())
         .collect(Collectors.toList());//统计
流的特点:
 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元
素(如`ArrayList` 与 `LinkedList`)。但流的目的在于表达计算,比如 `filter`、`sorted`和`map`。集合讲的是数据,流讲的是计算。
 源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
 数据处理操作——流的数据处理功能支持类似于数据库的操作,如`filter`、`map`、`reduce`、`find`、`match`、`sort`等。流操作可以顺序执行,也可并行执行。
 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。流水线的操作可以看作对数据源进行数据库式查询。
 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

流与集合的区别:
 1、集合是一个内存中的数据结构,它包含数据结构中目前所有的值—— 集合中的每个元素都得先算出来才能添加到集合中。
 2、流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。



流的操作:
1、中间操作:中间操作会返回另一个流,比如`filter、map、sort、distinct`等操作
2、终端操作: 终端操作会从流的流水线生成结果,比如`forEach、count、collect`
三、使用流
常用流的API介绍

1、筛选流

List numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);

2、截断流

List dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
.collect(toList());

3、跳过元素

List dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());

4、映射 对流中每一个元素应用函数

List dishNames = menu.stream()
.map(Dish::getName)
.collect(toList());
List dishNameLengths = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(toList());

5、流的扁平化

 例如, 给定单词列表["Hello","World"] ,你想要返回列表["H","e","l", "o","W","r","d"]
 

方式一:这个并不能返回正确的数据,解释看图5-5

words.stream()
.map(word -> word.split(""))
.distinct()
.collect(toList());

方式二:使用`flatMap` 方法,把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。解释看图5-6
List uniqueCharacters =
 words.stream()
.map(w -> w.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());

6、查找和匹配

Stream API通过allMatch 、anyMatch 、noneMatch 、findFirst 和findAny 
方法提供了查找和匹配的工具方法。

1、anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。

if(menu.stream().anyMatch(Dish::isVegetarian)){
   System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
anyMatch方法返回一个boolean,因此是一个终端操作。

2、allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词。

boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);

3、noneMatch没有任何元素与给定的谓词匹配。

boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() < 1000);

4、findAny方法将返回当前流中的任意元素。

    Optional dish =menu.stream().filter(Dish::isVegetarian).findAny();
    
  Optional简介
    Optional类(java.util.Optional)是一个容器类,
    代表一个值存在或不存在。在上面的代码中,findAny可能
    什么元素都没找到。Java 8的库设计人员引入了Optional,
    这样就不用返回众所周知容易出问题的null了。
 

5、findFirst查找第一个元素

List someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional firstSquareDivisibleByThree =someNumbers.stream()
                                                 .map(x -> x * x)
                                             .filter(x -> x % 3 == 0)
                                             .findFirst(); // 9

6、规约

解决了如何把一个流中的元素组合起来,常用函数reduce

数据求和实现
方式一:
int sum = 0;
for (int x : numbers) {
  sum += x;
}
方式二:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);

解释:
reduce接受两个参数:
 一个初始值,这里是0;
 一个BinaryOperator 来将两个元素结合起来产生一个新的值,这里我们用的是lambda (a, b) -> a + b 。


求最大值和最小值
int max = numbers.stream().reduce(0, Integer::max);


归约方法的优势与并行化

相比于前面写的逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,
这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum,
这不是那么容易并行化的。
如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!这种计算的并
行化需要另一种办法:将输入分块,分块求和,最后再合并起来。但这样的话代码看起来就
完全不一样了。



 使用流来对所有的元素并行求和时,你的代码几乎不用修改:stream()换成了`parallelStream()。
 int sum = numbers.parallelStream().reduce(0, Integer::sum);


7、构建流

从数值、数组、序列、文件以及函数来构建流 

7.1、数值构建流

Stream stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);

7.2、数组构建流

int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();

7.3、函数构建流 Stream API提供了两个静态方法来从函数生成流:

 Stream.iterate和 Stream.generate。
 Stream.iterate迭代构建流:
 Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);

解释:

 iterate方法接受一个初始值(在这里是0),
 还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator类型)。
 这里,我们使用Lambda n -> n + 2,返回的是前一个元素加上2。

  Stream.generate生成流:

  Stream.generate(Math::random).limit(5).forEach(System.out::println);
  IntStream 的generate 方法会接受一个IntSupplier,可以这样来生成一个全是1 的无限流。
  IntStream ones = IntStream.generate(() -> 1);
四、流的收集器
收集器非常有用,因为用它可以简洁而灵活地定义collect 用来生成结果集合的标准,一个普通的收集器
收集器的主要功能:
 将流元素归约和汇总为一个值
 元素分组
 元素分区
 


首先导入类:import static java.util.stream.Collectors.*;

1、归约和汇总

long howManyDishes = menu.stream().count();
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories))

2、连接字符串

String shortMenu = menu.stream().map(Dish::getName).collect(joining());
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));

3、广义的归约汇总

int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));


解释它需要三个参数。
 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
 第二个参数就是你在6.2.2节中使用的函数,将菜肴转换成一个表示其所含热量的int。
 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是对两个int求和。


4、分组和分区groupingBy

4.1、分组
Map> dishesByType =menu.stream().collect(groupingBy(Dish::getType));
4.2、多级分组
Map>> groupByTypeAndCaloriesMap=menu.stream()
        .collect(
         Collectors.groupingBy(
             Dish::getType,
             Collectors.groupingBy(dish -> {
                 if(dish.getCalories()<=400){
                     return CaloricLevel.DIET;
                 }else if(dish.getCalories()<=700){
                     return CaloricLevel.NORMAL;
                 }else{
                     return CaloricLevel.FAT;
                 }
         })
 ));

5、分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数 。

分区函数返回一个布尔值,这意味着得到的分组Map 的键类型是Boolean ,于是它最多可以分
为两组——true 是一组,false 是一组。
Map> partitionedMenu =menu.stream().collect(partitioningBy(Dish::isVegetarian));

五、代码重构
 Lambda表达式对面向对象的设计模式
 说明:需要执行的动作都很简单,使用Lambda方法能很方便地消除僵化代码。但是,
 观察者的逻辑有可能十分复杂,它们可能还持有状态,或定义了多个方法,在这些情
 形下,你还是应该继续使用类的方式。
 

1、使用Lambda 表达式重构代码

//具体处理文件接口
public interface BufferReaderProcessor{
    String process(BufferedReader br) throws IOException;
}
//公共逻辑抽离:文件的打开、资源关闭
public static String processFile(String filePath,BufferReaderProcessor     processor) throws IOException {
    try(BufferedReader br=new BufferedReader(new FileReader(filePath))){
        return processor.process(br);
    }
}
//客户端调用
String s1= processFile("data.txt",br->br.readLine());
String s2= processFile("data.txt",br->br.readLine()+br.readLine());

2、策略模式替换

  //策略接口
  public interface ValidationStrategy{
    boolean execute(String  s);
  }
  //策略执行
  public class Validator{
    private  ValidationStrategy strategy;
    public  Validator(ValidationStrategy strategy){
        this.strategy=strategy;
    }
    public boolean validator(String  s){
        return strategy.execute(s);
    }
  }
  //客户端调用
   Validator numberValidator=new Validator(s->s.matches("d+"));
   Validator lowerCaseValidator=new Validator(s->s.matches("[a-z]+"));
   numberValidator.validator("12345");
   lowerCaseValidator.validator("abcdefg");
六、异步编程
在Java 8中, 新增加了一个包含50个方法左右的类: CompletableFuture,
提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,
提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和
组合CompletableFuture的方法。

1、基本用法

CompletableFuture实现了Future接口,因此可以和使用Future一样使用它。
    
    1.1complete方法使用
    
    CompletableFuture completableFuture=new CompletableFuture();
    Thread thread=new Thread(()->{
        System.out.println("task doing...");
        try{
            Thread.sleep(1000);
        }catch (Exception ex){
            ex.printStackTrace();
        }
        System.out.println("task done");
        completableFuture.complete("finished");//设置任务完成标识,否则@1会一直傻等下去
    });
    thread.start();
    String result=completableFuture.get();//@1
    System.out.println("执行结果:"+result);//返回finished
    
    1.2 completeExceptionally 方法使用
    
    CompletableFuture completableFuture=new CompletableFuture();
    Thread thread=new Thread(()->{
        try{
            Thread.sleep(1000);
            throw new RuntimeException("抛异常了!");
        }catch (Exception ex){
            completableFuture.completeExceptionally(ex);//异常处理
        }

    });
    thread.start();
    String result=completableFuture.get();

2、静态方法

CompletableFuture 类自身提供了大量静态方法,使用这些方法能更方便地进行异步编程。
  

2.1、allOf和anyOf的使用

allOf要求所有任务都执行完成,最后汇总执行结果;anyOf只要执行最快的线程返回,汇总结果。
  
  
   1、task1
       CompletableFuture completableFuture1=CompletableFuture.supplyAsync(()->{
        try{
            Thread.sleep(1500);
        }catch (Exception ex){
            ex.printStackTrace();
        }
        System.out.println("finished 1 ");
        return "completableFutue1";
    });

  2、task2
    CompletableFuture completableFuture2=CompletableFuture.supplyAsync(()->{
        try{
            Thread.sleep(1000);
        }catch (Exception ex){
            ex.printStackTrace();
        }
        System.out.println("finished 2 ");
        return "completableFuture2";
    });


    //两个任务都要完成才能结束
    CompletableFuture allResult=CompletableFuture.allOf(completableFuture1,completableFuture2);
    allResult.join();

    //任一个任务结束就能返回
    CompletableFuture anyResult=CompletableFuture.anyOf(completableFuture1,completableFuture2);
    System.out.println("anyOf return :"+anyResult.get());

2.2 thenCompose

允许你对两个异步操作进行流水线, 第一个操作完成时,将其结果作为参数传递给第二个操作。

 CompletableFuture completedFuture1=CompletableFuture.supplyAsync(()->{
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Hello";
    });
    
    CompletableFuture completableFuture2=completedFuture1
            .thenCompose(result->CompletableFuture.supplyAsync(()->result+" world"));
    String result=completableFuture2.get();
    System.out.println("compose return:"+result);

3、ThenCombine

 将两个完  全不相干的 CompletableFuture 对象的结果整合起来,
 而且你也不希望等到第一个任务完全结束才开始第二项任务。
 
  CompletableFuture completableFuture1=CompletableFuture.supplyAsync(()->{
        System.out.println("in completableFuture1");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("finished completableFuture1");
        return "Hello";
    });

    CompletableFuture completableFuture2=completableFuture1
            .thenCombine(CompletableFuture.supplyAsync(()-> {
                System.out.println("in completableFuture2");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("finished completableFuture2");
                return " World";
            }),(result1,result2)->result1+result2);

    System.out.println(completableFuture2.get());


4、thenAccept

 在每个CompletableFuture 上注册一个操作,
该操作会在 CompletableFuture 完成执行后调用它。
 
 CompletableFuture completabledFuture1=CompletableFuture.supplyAsync(()->{
            System.out.println("in completabledFuture1");
            return "Hello";
        });
        completabledFuture1.thenAccept(result-> System.out.println("执行结果:"+result));

 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70633.html

相关文章

  • Java8实战》-读书笔记第二章

    摘要:但是到了第二天,他突然告诉你其实我还想找出所有重量超过克的苹果。现在,农民要求需要筛选红苹果。那么,我们就可以根据条件创建一个类并且实现通过谓词筛选红苹果并且是重苹果酷,现在方法的行为已经取决于通过对象来实现了。 通过行为参数化传递代码 行为参数化 在《Java8实战》第二章主要介绍的是通过行为参数化传递代码,那么就来了解一下什么是行为参数化吧。 在软件工程中,一个从所周知的问题就是,...

    Astrian 评论0 收藏0
  • Java8实战》-读书笔记第一章(01)

    摘要:依旧使用刚刚对苹果排序的代码。现在,要做的是筛选出所有的绿苹果,也许你会这一个这样的方法在之前,基本上都是这样写的,看起来也没什么毛病。但是,现在又要筛选一下重量超过克的苹果。 《Java8实战》-读书笔记第一章(01) 最近一直想写点什么东西,却不知该怎么写,所以就写写关于看《Java8实战》的笔记吧。 第一章内容较多,因此打算分几篇文章来写。 为什么要关心Java8 自1996年J...

    codeGoogle 评论0 收藏0
  • 乐字节-Java8核心特性实战-接口默认方法

    摘要:语法中接口可以包含实现方法,需要使用修饰,此类方法称为默认方法。核心特性接口默认方法就介绍到这里了,后续小乐会继续讲述核心特性。 JAVA8已经发布很久,是自java5(2004年发布)之后Oracle发布的最重要的一个版本。其中包括语言、编译器、库、工具和JVM等诸多方面的新特性,对于国内外互联网公司来说,Java8是以后技术开发的趋势。这里主要讲解在开发中几个核心的新特性。(主要从...

    lbool 评论0 收藏0
  • Java8实战》-读书笔记第一章(02)

    摘要:实战读书笔记第一章从方法传递到接着上次的,继续来了解一下,如果继续简化代码。去掉并且生成的数字是万,所消耗的时间循序流并行流至于为什么有时候并行流效率比循序流还低,这个以后的文章会解释。 《Java8实战》-读书笔记第一章(02) 从方法传递到Lambda 接着上次的Predicate,继续来了解一下,如果继续简化代码。 把方法作为值来传递虽然很有用,但是要是有很多类似与isHeavy...

    lushan 评论0 收藏0
  • 乐字节Java8核心特性实战之方法引用

    摘要:大家好,我是乐字节的小乐,上一次我们说到了核心特性之函数式接口,接下来我们继续了解又一核心特性方法引用。方法引用是一种更简洁易懂的表达式。感谢光临阅读小乐的,敬请关注乐字节后续将继续讲述等前沿知识技术。 大家好,我是乐字节的小乐,上一次我们说到了Java8核心特性之函数式接口,接下来我们继续了解Java8又一核心特性——方法引用。 showImg(https://segmentfaul...

    lakeside 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<