(3)Java 8 实战第二版——使用流和Lambda进行高效编程 ...

打印 上一主题 下一主题

主题 902|帖子 902|积分 2706

集合工厂
  1. List<String> friends = Arrays.asList("Raphael", "Olivia");
  2. friends.set(0, "Richard");
  3. friends.add("Thibaut");       ←---- 抛出一个UnsupportedModificationException异常
复制代码
通过工厂方法创建的Collection的底层是大小固定的可变数组。
:::info
JAVA 11及之前无Java中还没有Arrays.asSet()这种工厂方法
Python、Groovy在内的多种语言都支持集合常量,可以通过譬如[42, 1, 5]这样的语法格式创建含有三个数字的集合
Java并没有提供集合常量的语法支持,缘故原由是这种语言上的变化每每伴随着高昂的维护成本,并且会限制将来可能使用的语法。与此相反,Java 9 +通过增强Collection API,另辟蹊径地增加了对集合常量的支持。
避免不可预知的缺陷,同时以更紧凑的方式存储内部数据,不要在工厂方法创建的列表中存放null元素
:::
重载(overloading)和变参(vararg)

如果进一步审阅List接口,会发现List.of包含了多个重载的版本,包括:
static  List of(E e1, E e2, E e3, E e4)
static  List of(E e1, E e2, E e3, E e4, E e5)
变参版本

static  List of(E... elements)        可变参
变参版本的函数需要额外分配一个数组,这个数组被封装于列表中。
使用变参版本的方法,你就要负担分配数组、初始化以及最后对它进行垃圾接纳的开销。
使用定长(最多为10个)元素版本的函数,就没有这部门开销。
:::info
注意,如果使用List.of创建超过10个元素的列表,这种情况下现实调用的照旧变参类型的函数。类似的情况也会出现在Set.of和Map.of中。
:::
SET工厂
  1. //类似于List.of 、创建不可变的Set集合
  2. Set<String> friends = Set.of("Raphael", "Olivia", "Thibaut");
  3. System.out.println(friends);       ←---- [Raphael,Olivia, Thibaut]
复制代码
Map工厂

Java 9中提供了两种初始化一个不可变Map的方式
  1. Map<String, Integer> ageOfFriends
  2.     = Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26);
  3. System.out.println(ageOfFriends);       ←---- {Olivia=25, Raphael=30, Thibaut=26}
复制代码
:::info
只需要创建不到10个键值对的小型Map,那么使用这种方法比力方便。
如果键值对的规模比力大,则可以考虑使用别的一种叫作Map.ofEntries的工厂方法,这种工厂方法接受以变长参数列表形式组织的Map.Entry对象作为参数。
使用第二种方法,你需要创建额外的对象,从而实现对键和值的封装
:::
  1. import static java.util.Map.entry;
  2. Map<String, Integer> ageOfFriends
  3.     = Map.ofEntries(entry("Raphael", 30),
  4.                     entry("Olivia", 25),
  5.                     entry("Thibaut", 26));
  6. System.out.println(ageOfFriends);       ←---- {Olivia=25, Raphael=30, Thibaut=26}
复制代码
  1. List<String> actors = List.of("Keanu", "Jessica")
  2. actors.set(0, "Brad");
  3. System.out.println(actors)
  4. /**
  5. *答案:执行该代码片段会抛出一个UnsupportedOperationException异常,
  6. *因为由List.of方法构造的集合对象是不可修改的。
  7. **/
复制代码
使用List和Set

Java 8在List和Set的接口中新引入了以下方法。

  • removeIf移除集合中匹配指定谓词的元素。实现了List和Set的所有类都提供了该方法(事实上,这个方法继承自Collection接口)。
  • replaceAll用于 List接口中,它使用一个函数(UnaryOperator)替换元素。
  • sort也用于List接口中,对列表自身的元素进行排序。
  1. for (Transaction transaction : transactions) {
  2.     if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {
  3.         transactions.remove(transaction);
  4.     }
  5. }
复制代码
removeIf方法

可代替上述繁琐步骤[]
  1. //发现其中的问题了吗?非常不幸,
  2. 这段代码可能导致ConcurrentModificationException。
  3. 为什么会这样?
  4. 因为在底层实现上,
  5. for-each循环使用了一个迭代器对象,所以代码的执行会像下面这样:
  6. for (Iterator<Transaction> iterator = transactions.iterator();
  7.     iterator.hasNext(); ) {
  8.   Transaction transaction = iterator.next();
  9.   if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {
  10.       transactions.remove(transaction);       ←---- 问题在这儿,我们使用了两个不同的对象来迭代和修改集合
  11.   }
  12. }
  13. //因此,迭代器对象的状态没有与集合对象的状态同步,反之亦然。
  14. //为了解决这个问题,只能显式地使用Iterator对象,并通过它调用remove()方法
  15. /**
  16. Iterator对象,它使用next()和hasNext()方法查询源;
  17. Collection对象,它通过调用remove()方法删除集合中的元素。
  18. **/
  19. for (Iterator<Transaction> iterator = transactions.iterator();
  20.     iterator.hasNext(); ) {
  21.   Transaction transaction = iterator.next();
  22.     if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {
  23.         iterator.remove();
  24.     }
  25. }
复制代码
  1. transactions.removeIf(transaction ->
  2.                       Character.isDigit(transaction.getReferenceCode().charAt(0)));
复制代码
replaceAll方法
  1. referenceCodes.stream()       ←---- [a12, C14, b13]
  2.               .map(code -> Character.toUpperCase(code.charAt(0)) +
  3.     code.substring(1))
  4.               .collect(Collectors.toList())
  5.               .forEach(System.out::println);       ←---- 输出A12, C14, B13
  6. for (ListIterator<String> iterator = referenceCodes.listIterator();
  7.     iterator.hasNext(); ) {
  8.   String code = iterator.next();
  9.   iterator.set(Character.toUpperCase(code.charAt(0)) + code.substring(1));
  10. }
  11. //缺点:把Iterator对象和集合对象混在一起使用比较容易出错,
  12. //特别是还需要修改集合对象的场景
复制代码
  1. referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) +
  2.     code.substring(1));
复制代码
使用Map

Foreach
  1. for(Map.Entry<String, Integer> entry: ageOfFriends.entrySet()) {
  2.     String friend = entry.getKey();
  3.     Integer age = entry.getValue();
  4.     System.out.println(friend + " is " + age + " years old");
  5. }
  6. ageOfFriends.forEach((friend, age) -> System.out.println(friend + " is " +
  7.     age + " years old"));
复制代码
排序
  1. Map<String, String> favouriteMovies
  2.         = Map.ofEntries(entry("Raphael", "Star Wars"),
  3.         entry("Cristina", "Matrix"),
  4.         entry("Olivia",
  5.         "James Bond"));
  6. favouriteMovies
  7.   .entrySet()
  8.   .stream()
  9.   .sorted(Entry.comparingByKey())
  10.   .forEachOrdered(System.out::println);      
  11. // ←---- 按照人名的字母顺序对流中的元素进行排序
  12. //Cristina=Matrix
  13. //Olivia=James Bond
  14. //Raphael=Star Wars
复制代码
getOrDefault方法

查找的键在Map中不存在该怎么办。新的getOrDefault方法可以解决这一标题。
  1. Map<String, String> favouriteMovies
  2.         = Map.ofEntries(entry("Raphael", "Star Wars"),
  3.         entry("Olivia", "James Bond"));
  4.         System.out.println(favouriteMovies.getOrDefault("Olivia", "Matrix"));       ←---- 输出James Bond
  5.         System.out.println(favouriteMovies.getOrDefault("Thibaut", "Matrix"));       ←---- 输出Matrix
  6. /**getOrDefault以接受的第一个参数作为键,
  7. 第二个参数作为默认值(在Map中找不到指定的键时,该默认值会作为返回值)
  8. **/
  9. //注意,如果键在Map中存在,但碰巧被赋予的值是null,那么getOrDefault还是会返回null。
  10. //此外,无论该键存在与否,你作为参数传入的表达式每次都会被执行。
  11. //判断有无KEY
复制代码
盘算模式

缓存某个昂贵操作的结果,将其保存在一个键对应的值中。如果该键存在,就不需要再次睁开盘算。解决这个标题有三种新的途径

  • computeIfAbsent——如果指定的键没有对应的值(没有该键或者该键对应的值是空),那么使用该键盘算新的值,并将其添加到Map中;

    • computeIfAbsent的一个应用场景是缓存信息。假设你要解析一系列文件中每一个行的内容并盘算它们的SHA-256值。如果你之前已经处理过这些数据,就没有必要重复盘算。
    • 没有则新增,有就替换。

  • computeIfPresent——如果指定的键在Map中存在,就盘算该键的新值,并将其添加到Map中;
  • compute——使用指定的键盘算新的值,并将其存储到Map中。
  1. import java.util.HashMap;
  2. class Main {
  3.     public static void main(String[] args) {
  4.         // 创建一个 HashMap
  5.         HashMap<String, Integer> prices = new HashMap<>();
  6.         // 往HashMap中添加映射项
  7.         prices.put("Shoes", 200);
  8.         prices.put("Bag", 300);
  9.         prices.put("Pant", 150);
  10.         System.out.println("HashMap: " + prices);
  11.         // 计算 Shirt 的值
  12.         int shirtPrice = prices.computeIfAbsent("Shirt", key -> 280);
  13.         System.out.println("Price of Shirt: " + shirtPrice);
  14.         // 输出更新后的HashMap
  15.         System.out.println("Updated HashMap: " + prices);
  16.     }
  17.     /**
  18. HashMap: {Pant=150, Bag=300, Shoes=200}
  19. Price of Shirt: 280
  20. Updated HashMap: {Pant=150, Shirt=280, Bag=300, Shoes=200}
  21. **/
  22.     public static void main(String[] args) {
  23.         // 创建一个 HashMap
  24.         HashMap<String, Integer> prices = new HashMap<>();
  25.         // 往HashMap中添加映射关系
  26.         prices.put("Shoes", 180);
  27.         prices.put("Bag", 300);
  28.         prices.put("Pant", 150);
  29.         System.out.println("HashMap: " + prices);
  30.         // Shoes中的映射关系已经存在
  31.         // Shoes并没有计算新值
  32.         int shoePrice = prices.computeIfAbsent("Shoes", (key) -> 280);
  33.         System.out.println("Price of Shoes: " + shoePrice);
  34.         // 输出更新后的 HashMap
  35.         System.out.println("Updated HashMap: " + prices);
  36.     }
  37.     /**
  38.     HashMap: {Pant=150, Bag=300, Shoes=180}
  39. Price of Shoes: 180
  40. Updated HashMap: {Pant=150, Bag=300, Shoes=180}
  41. **/
  42. }   
复制代码
删除模式
  1. String key = "Raphael";
  2. String value = "Jack Reacher 2";
  3. if (favouriteMovies.containsKey(key) &&
  4.         Objects.equals(favouriteMovies.get(key), value)) {
  5.     favouriteMovies.remove(key);
  6.     return true;
  7. }
  8. else {
  9.     return false;
  10. }
  11. //等效于
  12. favouriteMovies.remove(key, value);
  13. //如果找不到建议用K、V
复制代码
替换模式
  1. Map<String, String> favouriteMovies = new HashMap<>();       ←---- 因为要使用replaceAll方法,所以只能创建可变的Map
  2. favouriteMovies.put("Raphael", "Star Wars");
  3. favouriteMovies.put("Olivia", "james bond");
  4. favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());
  5. System.out.println(favouriteMovies);       ←---- {Olivia=JAMES BOND, Raphael=STAR WARS}
复制代码
merge方法
  1. //没有重复的KEY
  2. Map<String, String> family = Map.ofEntries(
  3.     entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
  4. Map<String, String> friends = Map.ofEntries(
  5.     entry("Raphael", "Star Wars"));
  6. Map<String, String> everyone = new HashMap<>(family);
  7. everyone.putAll(friends);       ←---- 复制friends的所有条目到everyone中
  8. System.out.println(everyone);       ←---- {Cristina=James Bond, Raphael= Star Wars, Teo=Star Wars}
  9. //可能含有重复的KEY
  10. Map<String, String> family = Map.ofEntries(
  11.     entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
  12. Map<String, String> friends = Map.ofEntries(
  13.     entry("Raphael", "Star Wars"), entry("Cristina", "Matrix"));
  14. Map<String, String> everyone = new HashMap<>(family);
  15. friends.forEach((k, v) ->
  16.     everyone.merge(k, v, (movie1, movie2) -> movie1 + " & " + movie2));      
  17. //←---- 如果存在重复的键,就连接两个值
  18. System.out.println(everyone);      
  19. //←---- 输出{Raphael=Star Wars, Cristina=JamesBond & Matrix, Teo=Star Wars}
  20. /**
  21. 如果指定的键并没有关联值,或者关联的是一个空值,那么[merge]会将它关联到指定的非空值。否则,[merge]会用给定映射函数的[返回值]替换该值,如果映射函数的返回值为空就删除[该键]
  22. **/
  23. Map<String, Long> moviesToCount = new HashMap<>();
  24. String movieName = "James Bond";
  25. long count = moviesToCount.get(movieName);
  26. if(count == null) {
  27.     moviesToCount.put(movieName, 1);
  28. }
  29. else {
  30.     moviesToCount.put(moviename, count + 1);
  31. }
  32. moviesToCount.merge(movieName, 1L, (key, count) -> count + 1L);
  33. /**
  34. 传递给merge方法的第二个参数是1L。Javadoc文档中说该参数是“与键关联的非空值,该值将与现有的值合并,如果没有当前值,或者该键关联的当今值为空,就将该键关联到非空值”。因为该键的返回值是空,所以第一轮里键的值被赋值为1。接下来的一轮,由于键已经初始化为1,因此后续的操作由BiFunction方法对count进行递增。
  35. **/
  36. Map<String, Long> moviesToCount = new HashMap<>();
  37.         String movieName = "James Bond";
  38.         Long count = moviesToCount.get(movieName);
  39.         if (count == null) {
  40.             moviesToCount.put(movieName, 1L);
  41.         }
  42.         else {
  43.             moviesToCount.put(movieName, count + 1);
  44.         }
  45.         moviesToCount.merge(movieName, 1L, (key, value) -> value + 1L);
  46.         System.out.println(moviesToCount);
  47. // {James Bond=2}
复制代码
总结
  1. Map<String, Integer> movies = new HashMap<>();
  2. movies.put("JamesBond", 20);
  3. movies.put("Matrix", 15);
  4. movies.put("Harry Potter", 5);
  5. Iterator<Map.Entry<String, Integer>> iterator =
  6.              movies.entrySet().iterator();
  7. while(iterator.hasNext()) {
  8.     Map.Entry<String, Integer> entry = iterator.next();
  9.     if(entry.getValue() < 10) {
  10.         iterator.remove();
  11.     }
  12. }
  13. System.out.println(movies);       ←---- {Matrix=15, JamesBond=20}
  14. /**
  15. 答案:可以对Map的集合项使用removeIf方法,该方法接受一个谓词,依据谓词的结果删除元素。
  16. **/
  17. movies.entrySet().removeIf(entry -> entry.getValue() < 10);
复制代码
改进的ConcurrentHashMap

引入ConcurrentHashMap类是为了提供一个更加当代的HashMap,以更好地应对高并发的场景。ConcurrentHashMap允许执行并发的添加和更新操作,其内部实现基于分段锁。与另一种解决方案——同步式的Hashtable相比力,ConcurrentHashMap的读写性能都更好(注意,标准的HashMap是不带同步的)。
归约和搜刮

已学

  • forEach——对每个(键, 值)对执行指定的操作;
  • reduce——依据归约函数整合所有(键, 值)对的盘算结果;
  • search——对每个(键, 值)对执行一个函数,直到函数取得一个非空值。
每种操作支持四种形式的参数,接受函数使用键、值、Map.Entry以及(键, 值)对作为参数:

  • 使用键(forEachKey,reduceKeys,searchKeys);
  • 使用值(forEachValue,reduceValues,searchValues);
  • 使用Map.Entry对象(forEachEntry,reduceEntries,searchEntries);
  • 使用键和值(forEach,reduce,search)
    :::info
    所有这些操作都不会对ConcurrentHashMap的状态上锁,它们只是在运行中动态地对对象加锁。执行操作的函数不应对执行顺序或其他对象或可能在运行中变化的值有任何的依赖。
规则!

  • 别的,还需要为所有操作设定一个并行阈值。如果当前Map的规模比指定的阈值小,方法就只能顺序执行。
  • 使用通用线程池时,如果把并行阈值设置为1将获得最大的并行度。
  • 将阈值设定为Long.MAX_VALUE时,方法将以单线程的方式运行。
除非软件架构经过高度的资源优化,否则通常情况下,建议服从这些原则。
:::
  1. ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();   ←---- 一个可能有多个键和值更新的ConcurrentHashMap对象
  2. long parallelismThreshold = 1;
  3. Optional<Integer> maxValue =
  4.     Optional.ofNullable(map.reduceValues(parallelismThreshold, Long::max));
  5. //请留意,int、long、double等基础类型的归约操作(reduceValuesToInt、reduceKeysToLong等)
  6. //会更加高效,因为它们没有额外的封装开销
复制代码
计数

ConcurrentHashMap类提供了一个新的mappingCount方法,能以长整形long返回Map中的映射数目。
应该尽量在新的代码中使用它,而不是继续使用返回int的size方法。
这样做能让你的代码更具扩展性,更好地适应将来的需要,因为总有一天Map中映射的数目可能会超过int能表现的范畴。
Set视图

ConcurrentHashMap类还提供了一个新的keySet方法,该方法以Set的形式返回ConcurrentHashMap的一个视图(Map中的变化会反映在返回的Set中,反之亦然)。
也可以使用新的静态方法newKeySet创建一个由ConcurrentHashMap构成的Set。
Collection API增强

Java 9支持集合工厂,使用List.of、Set.of、Map.of以及Map.ofEntries可以创建小型不可变的List、Set和Map。
集合工厂返回的对象都是不可变的,这意味着创建之后你不能修改它们的状态。
List接口支持默认方法removeIf、replaceAll和sort。
Set接口支持默认方法removeIf。
Map接口为常见模式提供了几种新的默认方法,并低落了出现缺陷的概率。
ConcurrentHashMap支持从Map中继承的新默认方法,并提供了线程安全的实现。
重构、测试和调试

:::info
如何使用Lambda表达式重构代码
Lambda表达式对面向对象的设计模式的影响
Lambda表达式的测试
如何调试使用Lambda表达式和Stream API的代码
:::
改善可读性


  • 重构代码,用Lambda表达式代替匿名类;
  • 用方法引用重构Lambda表达式;
  • 用Stream API重构下令式的数据处理。
从匿名类到Lambda表达式的转换
  1. Runnable r1 = new Runnable(){       ←---- 传统的方式,使用匿名类
  2.     public void run(){
  3.         System.out.println("Hello");
  4.     }
  5. };
  6. Runnable r2 = () -> System.out.println("Hello");       ←---- 新的方式,使用Lambda表达式
复制代码
  1. int a = 10;
  2. Runnable r1 = () -> {
  3.     int a = 2;       ←---- 编译错误
  4.     System.out.println(a);
  5. };
  6. Runnable r2 = new Runnable(){
  7.     public void run(){
  8.         int a = 2;       ←---- 一切正常!
  9.         System.out.println(a);
  10.     }
  11. };
  12. //在涉及重载的上下文里,将匿名类转换为Lambda表达式可能导致最终的代码更加晦涩。实际上,匿名类的类型是在初始化时确定的,而Lambda的类型取决于它的上下文
  13. interface Task{
  14.     public void execute();
  15. }
  16. public static void doSomething(Runnable r){ r.run(); }
  17. public static void doSomething(Task a){ a.execute(); }
  18. //这种匿名类转换为Lambda表达式时,就导致了一种晦涩的方法调用,因为Runnable和Task都是合法的目标类型:
  19. doSomething(() -> System.out.println("Danger danger!!"));      
  20. //←---- 麻烦来了:doSomething(Runnable)和doSomething(Task)都匹配该类型
  21. doSomething((Task)() -> System.out.println("Danger danger!!"));
  22. /**
  23. 大部分主流开发环境 支持自动检查重构
  24. **/
复制代码
从Lambda表达式到方法引用的转换
  1. Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =
  2.     menu.stream()
  3.         .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. //简约写法
  10. Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =
  11.     menu.stream().collect(groupingBy(Dish::getCaloricLevel));       ←---- 将Lambda表达式抽取到一个方法内
  12. //新增一个类  方法引用
  13. public class Dish{
  14.     ...
  15.     public CaloricLevel getCaloricLevel(){
  16.         if (this.getCalories() <= 400) return CaloricLevel.DIET;
  17.         else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;
  18.         else return CaloricLevel.FAT;
  19.     }
  20. }
复制代码
增加代码的机动性


  • 重构代码,用Lambda表达式代替匿名类;
  • 用方法引用重构Lambda表达式;
  • 用Stream API重构下令式的数据处理。
采用函数接口
  1. inventory.sort(
  2.   (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));       ←---- 你需要考虑如何实现比较算法
  3. inventory.sort(comparing(Apple::getWeight));       ←---- 读起来就像问题描述,非常清晰
复制代码
使用Lambda重构面向对象的设计模式


  • 访问者模式常用于分离程序的算法和它的操作对象。
  • 单例模式一般用于限制类的实例化,仅天生一份对象。
  • 其他21种设计模式...
策略模式

策略模式代表相识决一类算法的通用解决方案,你可以在运行时选择使用哪种方案。
可使用不同的标准来验证输入的有效性,使用不同的方式来分析或者格式化输入。
策略模式包含三部门内容

  • 一个代表某个算法的接口(Strategy接口)。
  • 一个或多个该接口的详细实现,它们代表了算法的多种实现(好比,实体类ConcreteStrategyA或者ConcreteStrategyB)。
  • 一个或多个使用策略对象的客户
  1. int totalCalories =
  2.     menu.stream().map(Dish::getCalories)
  3.                  .reduce(0, (c1, c2) -> c1 + c2);
  4. //内置的集合类,它能更清晰地表达问题陈述是什么。使用了集合类summingInt(方法的名词很直观地解释了它的功能):
  5. int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
复制代码
:::info
ValidationStrategy是一个函数接口了。
除此之外,它还与Predicate具有同样的函数描述。这意味着我们不需要声明新的类来实现不同的策略,通过直接传递Lambda表达式就能到达同样的目标,并且还更简洁
:::
  1. List<String> dishNames = new ArrayList<>();
  2. for(Dish dish: menu){
  3.     if(dish.getCalories() > 300){
  4.         dishNames.add(dish.getName());
  5.     }
  6. }
  7. //替换
  8. menu.parallelStream()
  9.     .filter(d -> d.getCalories() > 300)
  10.     .map(Dish::getName)
  11.     .collect(toList());
复制代码
模板方法

采用某个算法的框架,同时又盼望有肯定的机动度,能对它的某些部门进行改进,那么采用模板方法设计模式是比力通用的方案。
换句话说,模板方法模式在你“盼望使用这个算法,但是需要对其中的某些行进行改进,才能到达盼望的结果”时是非常有用的。
  1. String oneLine =
  2.     processFile((BufferedReader b) -> b.readLine());       ←---- 传入一个Lambda表达式
  3. String twoLines =
  4.     processFile((BufferedReader b) -> b.readLine() + b.readLine());       ←---- 传入另一个Lambda表达式
  5. public static String processFile(BufferedReaderProcessor p) throws
  6.     IOException {
  7.     try(BufferedReader br = new BufferedReader(new
  8.     FileReader("ModernJavaInAction/chap9/data.txt"))) {
  9.         return p.process(br);       ←---- 将BufferedReaderProcessor作为执行参数传入
  10.     }
  11. }
  12. public interface BufferedReaderProcessor {       ←---- 使用Lambda表达式的函数接口,该接口能够抛出一个IOException
  13.     String process(BufferedReader b) throws IOException;
  14. }
复制代码
等同于
  1. //假设希望验证输入的内容是否根据标准进行了恰当的格式化(比如只包含小写字母或数字)。
  2. //可以从定义一个验证文本(以String的形式表示)的接口入手
  3. public interface ValidationStrategy {
  4.     boolean execute(String s);
  5. }
  6. //其次,定义了该接口的一个或多个具体实现:
  7. public class IsAllLowerCase implements ValidationStrategy {
  8.     public boolean execute(String s){
  9.         return s.matches("[a-z]+");
  10.     }
  11. }
  12. public class IsNumeric implements ValidationStrategy {
  13.     public boolean execute(String s){
  14.         return s.matches("\\d+");
  15.     }
  16. }
  17. // 实际情况
  18. public class Validator{
  19.     private final ValidationStrategy strategy;
  20.     public Validator(ValidationStrategy v){
  21.         this.strategy = v;
  22.     }
  23.     public boolean validate(String s){
  24.         return strategy.execute(s);
  25.     }
  26. }
  27. Validator numericValidator = new Validator(new IsNumeric());
  28. boolean b1 = numericValidator.validate("aaaa");       ←---- 返回false
  29. Validator lowerCaseValidator = new Validator(new IsAllLowerCase ());
  30. boolean b2 = lowerCaseValidator.validate("bbbb");       ←---- 返回true
复制代码
观察者模式

某些事件发生时(好比状态转变),如果一个对象(通常称之为主题)需要自动地通知其他多个对象(称为观察者),就会采用该方案。
创建图形用户界面(GUI)程序时,常常会使用该设计模式。这种情况下,你会在图形用户界面组件(好比按钮)上注册一系列的观察者。
如果点击按钮,观察者就会收到通知,并随即执行某个特定的行为。但是观察者模式并不局限于图形用户界面。好比,观察者设计模式也适用于股票交易的情形,多个券商(观察者)可能都盼望对某一支股票价格(主题)的变动做出相应。
  1. Validator numericValidator =
  2.     new Validator((String s) -> s.matches("[a-z]+"));  (以下4行)直接传递Lambda表达式
  3. boolean b1 = numericValidator.validate("aaaa");              
  4. Validator lowerCaseValidator =                              
  5.     new Validator((String s) -> s.matches("\\d+"));   
  6. boolean b2 = lowerCaseValidator.validate("bbbb");
  7. //Lambda表达式避免了采用策略设计模式时僵化的模板代码。
  8. //Lambda表达式实际已经对部分代码(或策略)进行了封装,
  9. //而这就是创建策略设计模式的初衷
复制代码
:::info
是否随时随地都可以使用Lambda表达式呢?
答案是否定的!前文介绍的例子中,Lambda适配得很好,
那是因为需要执行的动作都很简单,因此才能很方便地消除僵化代码。
但是,观察者的逻辑有可能十分复杂,它们可能还持有状态,抑或定义了多个方法,
诸云云类。在这些情形下,照旧应该继续使用类的方式
:::
责任链模式

责任链模式是一种创建处理对象序列(好比操作序列)的通用方案。一个处理对象可能需要在完成一些工作之后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推。
通常,这种模式是通过定义一个代表处理对象的抽象类来实现的,在抽象类中会定义一个字段来记录后续对象。一旦对象完成它的工作,处理对象就会将它的工作转交给它的后继
  1. /**
  2. 需要编写一个简单的在线银行应用。通常,用户需要输入一个用户账户,
  3. 之后应用才能从银行的数据库中得到用户的详细信息,最终完成一些让用户满意的操作。
  4. 不同分行的在线银行应用让客户满意的方式可能略有不同,比如给客户的账户发放红利,
  5. 或者仅仅是少发送一些推广文件。
  6. 你可能通过下面的抽象类方式来实现在线银行应用
  7. **/
  8. abstract class OnlineBanking {
  9.   public void processCustomer(int id) {
  10.     Customer c = Database.getCustomerWithId(id);
  11.     makeCustomerHappy(c);
  12.   }
  13.   abstract void makeCustomerHappy(Customer c);
  14.   // dummy Customer class
  15.   static  class Customer {}
  16.   // dummy Database class
  17.   static private class Database {
  18.     static Customer getCustomerWithId(int id) {
  19.       return new Customer();
  20.     }
  21.   }
  22. }
  23. //继承
  24. public class MyOnlineBanking extends OnlineBanking {
  25.     @Override
  26.     void makeCustomerHappy(Customer c) {
  27.         // 实现具体的逻辑来使客户满意
  28.         System.out.println("Customer with ID " + c + " is happy now!");
  29.     }
  30. }
  31. //调用和使用
  32. public class Main {
  33.     public static void main(String[] args) {
  34.         MyOnlineBanking banking = new MyOnlineBanking();
  35.         banking.processCustomer(123); // 传入客户的ID
  36.     }
  37. }
复制代码

以UML的方式阐释了责任链模式
模板方法设计模式。handle方法提供了如何进行工作处理的框架。不同的处理对象可以通过继承ProcessingObject类,提供handleWork方法来进行创建。
  1. public class OnlineBankingLambda {
  2.     public static void main(String[] args) {
  3.         new OnlineBankingLambda().processCustomer(1337, (Customer c) -> System.out.println("Customer with ID " + c.toString() + " is happy now!"));
  4.     }
  5.     public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {
  6.         Customer c = Database.getCustomerWithId(id);
  7.         makeCustomerHappy.accept(c);
  8.     }
  9.     // dummy Customer class
  10.     static private class Customer {
  11.     }
  12.     // dummy Database class
  13.     static private class Database {
  14.         static Customer getCustomerWithId(int id) {
  15.             return new Customer();
  16.         }
  17.     }
  18. }
复制代码
  1. /**
  2. Twitter这样的应用设计并实现一个定制化的通知系统。
  3. 想法很简单:好几家报纸机构,比如美国《纽约时报》、英国《卫报》以及法国《世界报》都订阅了新闻推文,
  4. 他们希望当接收的新闻中包含他们感兴趣的关键字时,能得到特别通知。
  5. **/
  6. public class ObserverMain {
  7.   public static void main(String[] args) {
  8.     Feed f = new Feed();
  9.     //新闻中不同的关键字分别定义不同的行为
  10.     f.registerObserver(new NYTimes());
  11.     f.registerObserver(new Guardian());
  12.     f.registerObserver(new LeMonde());
  13.     f.notifyObservers("The queen said her favourite book is Java 8 & 9 in Action!");
  14.       
  15.     Feed feedLambda = new Feed();
  16.     //Observer接口的所有实现类都提供了一个方法:notify
  17.     feedLambda.registerObserver((String tweet) -> {
  18.      if (tweet != null && tweet.contains("money")) {
  19.        System.out.println("Breaking news in NY! " + tweet);
  20.      }
  21.     });
  22.     feedLambda.registerObserver((String tweet) -> {
  23.      if (tweet != null && tweet.contains("queen")) {
  24.        System.out.println("Yet another news in London... " + tweet);
  25.      }
  26.     });
  27.    
  28.     feedLambda.notifyObservers("Money money money, give me money!");
  29.   }
  30.   interface Observer {
  31.     void inform(String tweet);
  32.   }
  33.   interface Subject {
  34.     void registerObserver(Observer o);
  35.     void notifyObservers(String tweet);
  36.   }
  37.   static private class NYTimes implements Observer {
  38.     @Override
  39.     public void inform(String tweet) {
  40.       if (tweet != null && tweet.contains("money")) {
  41.         System.out.println("Breaking news in NY!" + tweet);
  42.       }
  43.     }
  44.   }
  45.   static private class Guardian implements Observer {
  46.     @Override
  47.     public void inform(String tweet) {
  48.       if (tweet != null && tweet.contains("queen")) {
  49.         System.out.println("Yet another news in London... " + tweet);
  50.       }
  51.     }
  52.   }
  53.   static private class LeMonde implements Observer {
  54.     @Override
  55.     public void inform(String tweet) {
  56.       if (tweet != null && tweet.contains("wine")) {
  57.         System.out.println("Today cheese, wine and news! " + tweet);
  58.       }
  59.     }
  60.   }
  61.   static private class Feed implements Subject {
  62.     private final List<Observer> observers = new ArrayList<>();
  63.     @Override
  64.     public void registerObserver(Observer o) {
  65.       observers.add(o);
  66.     }
  67.     @Override
  68.     public void notifyObservers(String tweet) {
  69.       observers.forEach(o -> o.inform(tweet));
  70.     }
  71.   }
  72. }
复制代码
:::info
处理对象作为Function的一个实例,或者更确切地说作为UnaryOperator的一个实例。andThen方法对其进行构造。
:::
工厂模式

无须向客户暴露实例化的逻辑就能完成对象的创建。假定你为一家银行工作,他们需要一种方式创建不同的金融产品:贷款、期权、股票,等等。
通常,你会创建一个工厂类,它包含一个负责实现不同对象的方法
  1. public abstract class ProcessingObject<T> {
  2.     protected ProcessingObject<T> successor;
  3.     public void setSuccessor(ProcessingObject<T> successor){
  4.         this.successor = successor;
  5.     }
  6.     public T handle(T input){
  7.         T r = handleWork(input);
  8.         if(successor != null){
  9.             return successor.handle(r);
  10.         }
  11.         return r;
  12.     }
  13.     abstract protected T handleWork(T input);
  14. }
复制代码
:::info
Java 8中的新特性到达了传统工厂模式同样的结果。
但是,如果工厂方法createProduct需要接受多个传递给产品构造方法的参数,那这种方式的扩展性不是很好。以是除了简单的Supplier接口外,你还必须提供一个函数接口。
假设盼望保存具有三个参数(两个参数为Integer类型,一个参数为String类型)的构造函数。为了完成这个任务,需要创建一个特殊的函数接口TriFunction。终极的结果是Map变得更加复杂。
:::
  1. public class ChainOfResponsibilityMain {
  2.   public static void main(String[] args) {
  3.     ProcessingObject<String> p1 = new HeaderTextProcessing();
  4.     ProcessingObject<String> p2 = new SpellCheckerProcessing();
  5.     p1.setSuccessor(p2);
  6.     String result1 = p1.handle("Aren't labdas really sexy?!!");
  7.     System.out.println(result1);
  8.     UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
  9.     UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");
  10.     Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
  11.     String result2 = pipeline.apply("Aren't labdas really sexy?!!");
  12.     System.out.println(result2);
  13.   }
  14.   private static abstract class ProcessingObject<T> {
  15.     protected ProcessingObject<T> successor;
  16.     public void setSuccessor(ProcessingObject<T> successor) {
  17.       this.successor = successor;
  18.     }
  19.     public T handle(T input) {
  20.       T r = handleWork(input);
  21.       if (successor != null) {
  22.         return successor.handle(r);
  23.       }
  24.       return r;
  25.     }
  26.     abstract protected T handleWork(T input);
  27.   }
  28.   private static class HeaderTextProcessing extends ProcessingObject<String> {
  29.     @Override
  30.     public String handleWork(String text) {
  31.       return "From Raoul, Mario and Alan: " + text;
  32.     }
  33.   }
  34.   private static class SpellCheckerProcessing extends ProcessingObject<String> {
  35.     @Override
  36.     public String handleWork(String text) {
  37.       return text.replaceAll("labda", "lambda");
  38.     }
  39.   }
  40. }
复制代码
测试Lambda表达式

将复杂的Lambda表达式分为不同的方法
高阶函数的测试
调试

程序员的武器库里有两大经典武器,分别是:

  • 查看栈跟踪;
  • 输出日志。
查看栈跟踪

程序突然克制运行(好比突然抛出一个非常),这时首先要调查程序在什么地方发生了非常以及为什么会发生该非常。这时栈帧就非常有用了。程序的每次方法调用都会产生相应的调用信息,包括程序中方法调用的位置、该方法调用使用的参数,以及被调用方法的当地变量。这些信息被保存在栈帧上。
程序失败时,会得到它的栈跟踪,通过一个又一个栈帧,可以相识程序失败时的概略信息。
通过这些能得到程序失败时的方法调用列表。这些方法调用列表终极会资助你发现标题出现的缘故原由。由于Lambda表达式没有名字,因此栈跟踪可能很难分析
  1. UnaryOperator<String> headerProcessing =
  2.     (String text) -> "From Raoul, Mario and Alan: " + text;       ←---- 第一个处理对象
  3. UnaryOperator<String> spellCheckerProcessing =
  4.     (String text) -> text.replaceAll("labda", "lambda");       ←---- 第二个处理对象
  5. Function<String, String> pipeline =
  6.     headerProcessing.andThen(spellCheckerProcessing);       ←---- 将两个方法结合起来,结果就是一个操作链
  7. String result = pipeline.apply("Aren't labdas really sexy?!!");
复制代码
使用日志调试
  1. public class FactoryMain {
  2.     public static void main(String[] args) {
  3.         Product p1 = ProductFactory.createProduct("loan");
  4.         System.out.printf("p1: %s%n", p1.getClass().getSimpleName());
  5.         Supplier<Product> loanSupplier = Loan::new;
  6.         Product p2 = loanSupplier.get();
  7.         System.out.printf("p2: %s%n", p2.getClass().getSimpleName());
  8.         Product p3 = ProductFactory.createProductLambda("loan");
  9.         System.out.printf("p3: %s%n", p3.getClass().getSimpleName());
  10.     }
  11.     static private class ProductFactory {
  12.         public static Product createProduct(String name) {
  13.             switch (name) {
  14.                 case "loan":
  15.                     return new Loan();
  16.                 case "stock":
  17.                     return new Stock();
  18.                 case "bond":
  19.                     return new Bond();
  20.                 default:
  21.                     throw new RuntimeException("No such product " + name);
  22.             }
  23.         }
  24.         public static Product createProductLambda(String name) {
  25.             Supplier<Product> p = map.get(name);
  26.             if (p != null) {
  27.                 return p.get();
  28.             }
  29.             throw new RuntimeException("No such product " + name);
  30.         }
  31.     }
  32.     static private interface Product {
  33.     }
  34.     static private class Loan implements Product {
  35.     }
  36.     static private class Stock implements Product {
  37.     }
  38.     static private class Bond implements Product {
  39.     }
  40.     final static private Map<String, Supplier<Product>> map = new HashMap<>();
  41.     static {
  42.         map.put("loan", Loan::new);
  43.         map.put("stock", Stock::new);
  44.         map.put("bond", Bond::new);
  45.     }
  46. }
复制代码
:::info
流操作方法peek大显身手
peek的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作。
但是它不像forEach那样恢复整个流的运行,
而是在一个元素上完成操作之后,只会将操作顺承到流水线中的下一个操作
:::
  1. public interface TriFunction<T, U, V, R>{
  2.     R apply(T t, U u, V v);
  3. }
  4. Map<String, TriFunction<Integer, Integer, String, Product>> map
  5.     = new HashMap<>();
复制代码

  • Lambda表达式能提升代码的可读性和机动性。
  • 如果你的代码中使用了匿名类,那么尽量用Lambda表达式替换它们,但是要注意二者间语义的玄妙差别,好比关键字this,以及变量隐蔽。
  • 跟Lambda表达式比起来,方法引用的可读性更好。
  • 尽量使用Stream API替换迭代式的集合处理。
  • Lambda表达式有助于避免使用面向对象设计模式时容易出现的僵化的模板代码,典范的好比策略模式、模板方法、观察者模式、责任链模式,以及工厂模式。
  • 即使采用了Lambda表达式,也同样可以进行单位测试,但是通常你应该关注使用了Lambda表达式的方法的行为。
  • 尽量将复杂的Lambda表达式抽象到普通方法中。
  • Lambda表达式会让栈跟踪的分析变得更为复杂。
  • 流提供的peek方法在分析Stream流水线时,能将中间变量的值输出到日志中,是非常有用的工具。
基于Lambda的领域特定语言


  • 领域特定语言(domain-specifc language, DSL)及其形式
  • 为你的API添加DSL都有哪些优缺点
  • 除了简单的基于Java的DSL之外,JVM还有哪些领域特定语言可供选择
  • 从当代Java接口和类中学习领域特定语言
  • 高效实现基于Java的DSL都有哪些模式和本领
  • 常见Java库以及工具是如何使用这些模式的
JVM提供了第三个备选项,这是一种介于内部DSL与外部DSL之间的解决方案:可以在JVM上运行另一种通用编程语言,而这种语言比Java自身更机动、更有表现力,譬如Scala,或者Groovy。
把这样的第三种选项称为“多语言DSL”(polyglot DSL)
DSL具有以下长处。

  • 简洁——DSL提供的API非常贴心地封装了业务逻辑,避免编写重复的代码,终极代码将会非常简洁。
  • 可读性——DSL使用领域中的术语描述功能和行为,让代码的逻辑很容易理解,即使是不懂代码的非领域专家也能轻松上手。由于DSL的这个特性,代码和领域知识能在你的组织内无缝地分享与沟通。
  • 可维护性——构建于设计良好的DSL之上的代码既易于维护又便于修改。可维护性对于业务相关的代码尤其重要,应用这部门的代码很可能需要常常变更。
  • 高层的抽象性——DSL中提供的操作与领域中的抽象在同一条理,因此隐蔽了那些与领域标题不直接相关的细节。
  • 专注——使用专门为表述业务领域规则而设计的语言,可以资助程序员更专注于代码的某个部门。结果是生产服从得到了提升。
  • 关注点隔离——使用专用的语言描述业务逻辑使得与业务相关的代码可以同应用的底子架构代码相分离。以这种方式设计的代码将更容易维护。
弊端

  • DSL的设计比力困难——要想用精简有限的语言描述领域知识本身就是件困难的事情。
  • 开辟代价——向代码库中参加DSL是一项恒久投资,尤其是其启动开销很大,这在项目标早期可能导致进度耽误。别的,DSL的维护和演化还需要占用额外的工程开销。额外的中间层——DSL会在额外的一层中封
  • 装领域模子,这一层的设计应该尽可能地薄,只有这样才能避免带来性能标题。
  • 又一门要把握的语言——当今期间,开辟者已经习惯了使用多种语言进行开辟。然而,在你的项目中参加新的DSL意味着你和你的团队又需要把握一门新的语言。如果你决定在你的项目中使用多个DSL以处理来自不同业务领域的作业,并将它们无缝地整合在一起,那这种代价就更大了,因为DSL的演化也是各自独立的。
  • 宿主语言的局限性——有些通用型的语言(好比Java)一向以其烦琐和僵硬而闻名。这些语言使得设计一个用户友爱的DSL变得相当困难。现实上,构建于这种烦琐语言之上的DSL已经受限于其痴肥的语法,使得其代码险些不具备可读性。好消息是,Java 8引入的Lambda表达式提供了一个强大的新工具可以缓解这个标题。
  1. import java.util.*;
  2. public class Debugging{
  3.     public static void main(String[] args) {
  4.         List<Point> points = Arrays.asList(new Point(12, 2), null);
  5.         points.stream().map(p -> p.getX()).forEach(System.out::println);
  6.     }
  7. }
  8. //错误异常
  9. Exception in thread "main" java.lang.NullPointerException
  10.     at Debugging.lambda$main$0(Debugging.java:6)       ←---- 这行中的$0是什么意思?
  11.     at Debugging$$Lambda$5/284720968.apply(Unknown Source)
  12.     at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline
  13.       .java:193)
  14.     at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators
  15.       .java:948)
  16. ...
  17. //如果方法引用指向的是同一个类中声明的方法,那么它的名称是可以在栈跟踪中显示的
  18. import java.util.*;
  19. public class Debugging{
  20.     public static void main(String[] args) {
  21.         List<Integer> numbers = Arrays.asList(1, 2, 3);
  22.         numbers.stream().map(Debugging::divideByZero).forEach(System
  23.             .out::println);
  24.     }
  25.     public static int divideByZero(int n){
  26.         return n / 0;
  27.     }
  28. }
  29. //方法divideByZero在栈跟踪中就正确地显示了:
  30. Exception in thread "main" java.lang.ArithmeticException: / by zero
  31.     at Debugging.divideByZero(Debugging.java:10)       ←---- divideByZero正确地输出到栈跟踪中
  32.     at Debugging$$Lambda$1/999966131.apply(Unknown Source)
  33.     at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline
  34.       .java:193)
  35. ...
复制代码
:::info
这段代码中,方法myFlow()构建IntegrationFlow时使用了Spring Integration DSL。它使用的是IntegrationFlow类提供的流畅构建器,该构建器采用的就是方法链接模式。
这个例子中,终极的流会以固定的频率轮询MessageSource,天生一个整数序列,过滤出其中的偶数,再将它们转化为字符串,终极将结果发送给输出管道,这种行为与Java 8原生的Stream API非常像。
该API允许你将消息发送给流中的任何一个组件,只要你知道它的inputChannel名。如果流始于一个直接管道(direct channel),而非一个MessageSource,你完全可以使用Lambda表达式定义该IntegrationFlow
:::
  1. //可以像下面的例子那样,使用forEach将流操作的结果日志输出到屏幕上或者记录到日志文件中:
  2. List<Integer> numbers = Arrays.asList(2, 3, 4, 5);
  3. numbers.stream()
  4.        .map(x -> x + 17)
  5.        .filter(x -> x % 2 == 0)
  6.        .limit(3)
  7.        .forEach(System.out::println);
  8. //一旦调用forEach,整个流就会恢复运行
  9. /**
  10. 到底哪种方式能更有效地帮助我们理解Stream流水线中的
  11. 每个操作(比如map、filter、limit)产生的输出呢?
  12. **/
复制代码
小结

引入DSL的主要目标是为了弥补程序员与领域专家之间对程序认知理解上的差异。对于编写实现应用程序业务逻辑的代码的程序员来说,很可能对程序应用领域的业务逻辑理解不深,甚至完全不相识。以一种“非程序员”也能理解的方式书写业务逻辑并不能把领域专家们变成专业的程序员,却使得他们在项目早期就能阅读程序的逻辑并对其进行验证。
DSL的两大主要分类分别是内部DSL(采用与开辟应用相同的语言开辟的DSL)和外部DSL(采用与开辟应用不同的语言开辟的DSL)。内部DSL所需的开辟代价比力小,不过它的语法会受宿主语言限制。外部DSL提供了更高的机动性,但是实现难度比力大。
可以利用JVM上已经存在的另一种语言开辟多语言DSL,譬如Scala或者Groovy。这些新型语言通常都比Java更加简洁,也更机动。然而,要将Java与它们整合在一起使用需要修改构建流程,而这并不是一项小工程,并且Java与这些语言的互操作也远没到达完全无缝的程度。
由于自身冗长、烦琐以及僵硬的语法,Java并非创建内部DSL的理想语言,然而随着Lambda表达式及方法引用在Java 8中的引入,这种情况有所好转。
当代Java语言已经以原生API的方式提供了很多小型DSL。这些DSL,譬如Stream和Collectors类中的那些方法,都非常有用,使用起来也极其方便,特殊是你需要对集合中的数据进行排序、过滤、转换或者分组的时间,非常值得一试。
在Java中实现DSL有三种主要模式,分别是方法链接、嵌套函数以及函数序列。每种模式都有其长处和弊端。不过,你可以在一个DSL中整合这三种DSL,尽量地扬长避短,充分发挥各种模式的长处。
很多Java框架和库都可以通过DSL使用其特性。本章介绍了其中的三种,分别是:jOOQ,一种SQL映射工具;Cucumber,一种基于行为驱动的开辟框架;Spring Integration,一种实现企业集成模式的Spring扩展库。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

大号在练葵花宝典

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表