字符串连接器

Java8中的字符串连接收集器在JDK8中,可以采用函数式编程(使用 Collectors.joining 收集器)的方式对字符串进行更优雅的连接。
Collectors.joining 收集器 支持灵活的参数配置,可以指定字符串连接时的 分隔符,前缀 和 后缀 字符串。
代码参考如下:

  1. // 定义人名数组
  2. final String[] names = {"Zebe", "Hebe", "Mary", "July", "David"};
  3. Stream<String> stream1 = Stream.of(names);
  4. Stream<String> stream2 = Stream.of(names);
  5. Stream<String> stream3 = Stream.of(names);
  6. // 拼接成 [x, y, z] 形式
  7. String result1 = stream1.collect(Collectors.joining(", ", "[", "]"));
  8. // 拼接成 x | y | z 形式
  9. String result2 = stream2.collect(Collectors.joining(" | ", "", ""));
  10. // 拼接成 x -> y -> z] 形式
  11. String result3 = stream3.collect(Collectors.joining(" -> ", "", ""));
  12. System.out.println(result1);
  13. System.out.println(result2);
  14. System.out.println(result3);

程序输出结果如下:

  1. [Zebe, Hebe, Mary, July, David]
  2. Zebe | Hebe | Mary | July | David
  3. Zebe -> Hebe -> Mary -> July -> David

一般的做法(不推荐)
在JAVA8出现之前,我们通常使用循环的方式来拼接字符串,这样做不仅代码冗长丑陋,而且需要仔细阅读代码才知道代码的功能,例如下面的代码:

  1. final String[] names = {"Zebe", "Hebe", "Mary", "July", "David"};
  2. StringBuilder builder = new StringBuilder();
  3. for (int i = 0; i < names.length; i++) {
  4. if (builder.length() > 1) {
  5. builder.append(",");
  6. }
  7. builder.append(names[i]);
  8. }
  9. System.out.println(builder.toString());

惰性求值

什么是惰性求值(惰性计算)惰性求值也叫惰性计算、延迟求职,在函数式编程语言中随处可见。可以这样通俗地理解为:不对给出的表达式立即计算并返回值,而是在这个值需要被用到的时候才会计算。这个是个人理解,有关专业的术语定义请参考百度百科:https://baike.baidu.com/item/惰性计算
————————————————
什么是及早求值(热情求值)和惰性求值相反的是及早求值(热情求值),这是在大多数编程语言中随处可见的一种计算方式,例如:

  1. int x = 1;
  2. String name = getUserName();

上面的表达式在绑定了变量后就立即求值,得到计算的结果.

Java中的惰性求值
以下Java代码就是惰性求值的范例。这段代码在定义 nameStream 这个流的时候,System.out.println 语句不会被立即执行。

  1. package me.zebe.cat.java.lambda;
  2. import java.util.List;
  3. import java.util.stream.Collectors;
  4. import java.util.stream.Stream;
  5. /**
  6. * Java8惰性求值例子
  7. *
  8. * @author Zebe
  9. */
  10. public class LazyEvaluationDemo {
  11. /**
  12. * 运行入口
  13. *
  14. * @param args 运行参数
  15. */
  16. public static void main(String[] args) {
  17. // 定义流
  18. Stream<String> nameStream = Stream.of("Zebe", "July", "Yaha").filter(name -> {
  19. if (!name.isEmpty()) {
  20. System.out.println("过滤流,当前名称:" + name);
  21. return true;
  22. }
  23. return false;
  24. });
  25. // 取出流的值,这时候才会调用计算
  26. List<String> names1 = nameStream.collect(Collectors.toList());
  27. // 流只能被使用一次,下面这行代码会报错,提示流已经被操作或者关闭了
  28. List<String> names2 = nameStream.collect(Collectors.toList());
  29. }
  30. }

基本数据类型在高阶函数中的运用

众所周知,在Java中使用基本数据类型的性能和产效率远高于包装类型。由于装箱类型是对象,因此在内存中存在额外开销。比如,整型在内存中占用4 字节,整型对象却要占用 16 字节。这一情况在数组上更加严重,整型数组中的每个元素只占用基本类型的内存,而整型对象数组中,每个元素都是内存中的一个指针,指向 Java堆中的某个对象。在最坏的情况下,同样大小的数组, Integer[] 要比 int[] 多占用 6 倍内存。

将基本类型转换为装箱类型,称为装箱,反之则称为拆箱,两者都需要额外的计算开销。对于需要大量数值运算的算法来说,装箱和拆箱的计算开销,以及装箱类型占用的额外内存,会明显减缓程序的运行速度。

为了减小这些性能开销, Stream 类的某些方法对基本类型和装箱类型做了区分。Stream中的高阶函数 mapToLong 和其他类似函数即为该方面的一个尝试。在 Java 8 中,仅对整型、长整型和双浮点型做了特殊处理,因为它们在数值计算中用得最多,特殊处理后的系统性能提升效果最明显。

计算最大值、最小值、平均值、求和、计数统计
使用 mapToInt、mapToLong、mapToDouble 等高阶函数后,得到一个基于基本数据类型的流。针对这样的流,Java提供了一个摘要统计的功能(对应的类有:IntSummaryStatistics、LongSummaryStatistics 和 DoubleSummaryStatistics),如下代码所示:

  1. /**
  2. * 运行入口
  3. *
  4. * @param args 运行参数
  5. */
  6. public static void main(String[] args) {
  7. toInt();
  8. toLong();
  9. toDouble();
  10. }
  11. private static void toInt() {
  12. IntSummaryStatistics statistics = Stream.of(1L, 2L, 3L, 4L).mapToInt(Long::intValue).summaryStatistics();
  13. System.out.println("最大值:" + statistics.getMax());
  14. System.out.println("最小值:" + statistics.getMin());
  15. System.out.println("平均值:" + statistics.getAverage());
  16. System.out.println("求和:" + statistics.getSum());
  17. System.out.println("计数:" + statistics.getCount());
  18. }
  19. private static void toLong() {
  20. LongSummaryStatistics statistics = Stream.of(1L, 2L, 3L, 4000000000000000000L).mapToLong(Long::longValue).summaryStatistics();
  21. System.out.println("最大值:" + statistics.getMax());
  22. System.out.println("最小值:" + statistics.getMin());
  23. System.out.println("平均值:" + statistics.getAverage());
  24. System.out.println("求和:" + statistics.getSum());
  25. System.out.println("计数:" + statistics.getCount());
  26. }
  27. private static void toDouble() {
  28. DoubleSummaryStatistics statistics = Stream.of(1, 2, 3.0, 5.2).mapToDouble(Number::doubleValue).summaryStatistics();
  29. System.out.println("最大值:" + statistics.getMax());
  30. System.out.println("最小值:" + statistics.getMin());
  31. System.out.println("平均值:" + statistics.getAverage());
  32. System.out.println("求和:" + statistics.getSum());
  33. System.out.println("计数:" + statistics.getCount());
  34. }

ThreadLocal的Lambda构造方式

Java8中ThreadLocal对象提供了一个Lambda构造方式,实现了非常简洁的构造方法:withInitial。这个方法采用Lambda方式传入实现了 Supplier 函数接口的参数。写法如下:

  1. /**
  2. * 当前余额
  3. */
  4. private ThreadLocal<Integer> balance = ThreadLocal.withInitial(() -> 1000);

银行存款实例
附带一个银行存款的例子。

  1. package me.zebe.cat.java.lambda;
  2. /**
  3. * ThreadLocal的Lambda构造方式:withInitial
  4. *
  5. * @author Zebe
  6. */
  7. public class ThreadLocalLambdaDemo {
  8. /**
  9. * 运行入口
  10. *
  11. * @param args 运行参数
  12. */
  13. public static void main(String[] args) {
  14. safeDeposit();
  15. //notSafeDeposit();
  16. }
  17. /**
  18. * 线程安全的存款
  19. */
  20. private static void safeDeposit() {
  21. SafeBank bank = new SafeBank();
  22. Thread thread1 = new Thread(() -> bank.deposit(200), "张成瑶");
  23. Thread thread2 = new Thread(() -> bank.deposit(200), "马云");
  24. Thread thread3 = new Thread(() -> bank.deposit(500), "马化腾");
  25. thread1.start();
  26. thread2.start();
  27. thread3.start();
  28. }
  29. /**
  30. * 非线程安全的存款
  31. */
  32. private static void notSafeDeposit() {
  33. NotSafeBank bank = new NotSafeBank();
  34. Thread thread1 = new Thread(() -> bank.deposit(200), "张成瑶");
  35. Thread thread2 = new Thread(() -> bank.deposit(200), "马云");
  36. Thread thread3 = new Thread(() -> bank.deposit(500), "马化腾");
  37. thread1.start();
  38. thread2.start();
  39. thread3.start();
  40. }
  41. }
  42. /**
  43. * 非线程安全的银行
  44. */
  45. class NotSafeBank {
  46. /**
  47. * 当前余额
  48. */
  49. private int balance = 1000;
  50. /**
  51. * 存款
  52. *
  53. * @param money 存款金额
  54. */
  55. public void deposit(int money) {
  56. String threadName = Thread.currentThread().getName();
  57. System.out.println(threadName + " -> 当前账户余额为:" + this.balance);
  58. this.balance += money;
  59. System.out.println(threadName + " -> 存入 " + money + " 后,当前账户余额为:" + this.balance);
  60. try {
  61. Thread.sleep(1000);
  62. } catch (InterruptedException e) {
  63. e.printStackTrace();
  64. }
  65. }
  66. }
  67. /**
  68. * 线程安全的银行
  69. */
  70. class SafeBank {
  71. /**
  72. * 当前余额
  73. */
  74. private ThreadLocal<Integer> balance = ThreadLocal.withInitial(() -> 1000);
  75. /**
  76. * 存款
  77. *
  78. * @param money 存款金额
  79. */
  80. public void deposit(int money) {
  81. String threadName = Thread.currentThread().getName();
  82. System.out.println(threadName + " -> 当前账户余额为:" + this.balance.get());
  83. this.balance.set(this.balance.get() + money);
  84. System.out.println(threadName + " -> 存入 " + money + " 后,当前账户余额为:" + this.balance.get());
  85. try {
  86. Thread.sleep(1000);
  87. } catch (InterruptedException e) {
  88. e.printStackTrace();
  89. }
  90. }
  91. }

定制归一化收集器(reducing)得到自定义结果集

reducing简介reducing 是一个收集器(操作),从字面意义上可以理解为“减少操作”:输入多个元素,在一定的操作后,元素减少。

reducing 有多个重载方法,其中一个方法如下:

  1. public static <T> Collector<T,?,Optional<T>> reducing(BinaryOperator<T> op)

以上方法,JDK对其的描述是:
Returns a Collector which performs a reduction of its input elements under a specified BinaryOperator. The result is described as an Optional. (返回一个收集器,该收集器在指定的二进制操作符下执行其输入元素的减少。结果被描述为可选的 。)

reducing的应用reducing 是一个非常有用的收集器,可以用在多层流、下游数据分组或分区等场合。

下面是一个例子:
给定一组 Person 对象,每个 Person 都有 city(所在城市)和 height(身高)属性,编写一个程序,统计出每个不同的城市最大、最小身高值。

  1. import java.util.*;
  2. import java.util.function.BinaryOperator;
  3. import static java.util.stream.Collectors.groupingBy;
  4. import static java.util.stream.Collectors.reducing;
  5. /**
  6. * ReducingDemo
  7. *
  8. * @author Zebe
  9. */
  10. public class ReducingDemo {
  11. /**
  12. * 运行入口
  13. *
  14. * @param args 运行参数
  15. */
  16. public static void main(String[] args) {
  17. List<Person> personList = getPersonList(1000000);
  18. functionStyle(personList);
  19. normalStyle(personList);
  20. }
  21. /**
  22. * 函数式编程风格(3行处理代码)
  23. * @param personList Person 列表
  24. */
  25. private static void functionStyle(List<Person> personList) {
  26. long start = System.currentTimeMillis();
  27. // 创建一个比较器,取名为 byHeight (通过高度来比较)
  28. Comparator<Person> byHeight = Comparator.comparingInt(Person::getHeight);
  29. // 创建一个归一收集器
  30. Map<City, Optional<Person>> tallestByCity = personList.stream().
  31. collect(groupingBy(Person::getCity, reducing(BinaryOperator.maxBy(byHeight))));
  32. long usedTime = System.currentTimeMillis() - start;
  33. printResult("函数式编程风格", personList.size(), usedTime, tallestByCity);
  34. }
  35. /**
  36. * 普通编程风格(20行处理代码)
  37. * @param personList Person 列表
  38. */
  39. private static void normalStyle(List<Person> personList) {
  40. long start = System.currentTimeMillis();
  41. // 创建一个结果集
  42. Map<City, Optional<Person>> tallestByCity = new HashMap<>();
  43. // 第一步:找出所有的不同城市
  44. Set<City> cityList = new HashSet<>();
  45. for (Person person : personList) {
  46. if (!cityList.contains(person.getCity())) {
  47. cityList.add(person.getCity());
  48. }
  49. }
  50. // 第二部,遍历所有城市,遍历所有人找出每个城市的最大身高
  51. for (City city : cityList) {
  52. int maxHeight = 0;
  53. Person tempPerson = null;
  54. for (Person person : personList) {
  55. if (person.getCity().equals(city)) {
  56. if (person.getHeight() > maxHeight) {
  57. maxHeight = person.getHeight();
  58. tempPerson = person;
  59. }
  60. }
  61. }
  62. tallestByCity.put(city, Optional.ofNullable(tempPerson));
  63. }
  64. long usedTime = System.currentTimeMillis() - start;
  65. printResult("普通编程风格", personList.size(), usedTime, tallestByCity);
  66. }
  67. /**
  68. * 获取Person列表
  69. * @param numbers 要获取的数量
  70. * @return 返回指定数量的 Person 列表
  71. */
  72. private static List<Person> getPersonList(int numbers) {
  73. // 创建城市
  74. final City cityChengDu = new City("成都");
  75. final City cityNewYork = new City("纽约");
  76. List<Person> people = new ArrayList<>();
  77. // 创建指定数量的Person,并指定不同的城市和相对固定的身高值
  78. for (int i = 0; i < numbers; i++) {
  79. if (i % 2 == 0) {
  80. // 成都最大身高185
  81. people.add(new Person(cityChengDu, 185));
  82. } else if (i % 3 == 0) {
  83. people.add(new Person(cityChengDu, 170));
  84. } else if (i % 5 == 0) {
  85. // 成都最小身高160
  86. people.add(new Person(cityChengDu, 160));
  87. } else if (i % 7 == 0) {
  88. // 纽约最大身高200
  89. people.add(new Person(cityNewYork, 200));
  90. } else if (i % 9 == 0) {
  91. people.add(new Person(cityNewYork, 185));
  92. } else if (i % 11 == 0) {
  93. // 纽约最小身高165
  94. people.add(new Person(cityNewYork, 165));
  95. } else {
  96. // 默认添加纽约最小身高165
  97. people.add(new Person(cityNewYork, 165));
  98. }
  99. }
  100. return people;
  101. }
  102. /**
  103. * 输出结果
  104. * @param styleName 风格名称
  105. * @param totalPerson 总人数
  106. * @param usedTime 计算耗时
  107. * @param tallestByCity 统计好最大身高的城市分组MAP
  108. */
  109. private static void printResult(String styleName, long totalPerson, long usedTime, Map<City, Optional<Person>> tallestByCity) {
  110. System.out.println("\n" + styleName + ":计算 " + totalPerson + " 个人所在不同城市最大身高的结果如下:(耗时 " + usedTime + " ms)");
  111. tallestByCity.forEach((city, person) -> {
  112. person.ifPresent(p -> System.out.println(city.getName() + " -> " + p.getHeight()));
  113. });
  114. }
  115. }

Person类

  1. /**
  2. * Person
  3. *
  4. * @author Zebe
  5. */
  6. public class Person {
  7. /**
  8. * 所在城市
  9. */
  10. private City city;
  11. /**
  12. * 身高
  13. */
  14. private int height;
  15. /**
  16. * 构造器
  17. * @param city 所在城市
  18. * @param height 身高
  19. */
  20. public Person(City city, int height) {
  21. this.city = city;
  22. this.height = height;
  23. }
  24. public City getCity() {
  25. return city;
  26. }
  27. public void setCity(City city) {
  28. this.city = city;
  29. }
  30. public int getHeight() {
  31. return height;
  32. }
  33. public void setHeight(int height) {
  34. this.height = height;
  35. }
  36. }

City类

  1. /**
  2. * City
  3. *
  4. * @author Zebe
  5. */
  6. public class City {
  7. /**
  8. * 城市名
  9. */
  10. private String name;
  11. /**
  12. * 构造器
  13. * @param name 城市名
  14. */
  15. public City(String name) {
  16. this.name = name;
  17. }
  18. public String getName() {
  19. return name;
  20. }
  21. public void setName(String name) {
  22. this.name = name;
  23. }
  24. }

注:以上代码如果要计算最小值、平均值,将 maxBy 换成 minBy 就可以了。
输出结果程序输出结果如下:

  1. 函数式编程风格:计算 1000000 个人所在不同城市最大身高的结果如下:(耗时 149 ms
  2. 成都 -> 185
  3. 纽约 -> 200
  4. 普通编程风格:计算 1000000 个人所在不同城市最大身高的结果如下:(耗时 82 ms
  5. 成都 -> 185
  6. 纽约 -> 200

可以看出,函数式编程的效率不一定会比普通编程效率更高,甚至相对要慢一点,但是,函数式编程的好处在于:
把参数作为一个函数,而不是值,实现了只有在需要的时候才计算(惰性求值)。使用 lambda 表达式能够简化程序表达的含义,使程序更简洁明了。

Map新增的方法:computeIfAbsent

这个方法是JDK8中Map类新增的一个方法,用来实现当一个KEY的值缺失的时候,使用给定的映射函数重新计算填充KEY的值并返回结果。computeIfAbsent 方法的JDK源码如下:

  1. default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
  2. Objects.requireNonNull(mappingFunction);
  3. V v;
  4. // 尝试获取KEY的值,如果获取不到KEY
  5. if ((v = get(key)) == null) {
  6. V newValue;
  7. // 利用传入的计算函数,得到新的值
  8. if ((newValue = mappingFunction.apply(key)) != null) {
  9. // 将KEY的值填充为函数计算的结果
  10. put(key, newValue);
  11. // 返回计算的结果
  12. return newValue;
  13. }
  14. }
  15. // 如果KEY的值存在,则直接返回
  16. return v;
  17. }

computeIfAbsent使用例子

  1. Map<String, String> cache = new HashMap<>();
  2. // 如果键的值不存在,则调用函数计算并将计算结果作为该KEY的值
  3. final String key = "myKey";
  4. cache.computeIfAbsent(key, s -> {
  5. System.out.println("根据" + s + "计算新的值");
  6. return "myCacheValue";
  7. });
  8. // 第一次get,会调用映射函数
  9. System.out.println(cache.get(key));
  10. // 第二次get,不会调用映射函数
  11. System.out.println(cache.get(key));

程序输出结果如下:

  1. 根据myKey计算新的值
  2. myCacheValue
  3. myCacheValue

partitioningBy收集器
在JDK8中,可以对流进行方便的自定义分块,通常是根据某种过滤条件将流一分为二

例如:有一组人名,包含中文和英文,在 JDK8 中可以通过 partitioningBy 收集器将其区分开来。
下面是代码例子:

  1. // 创建一个包含人名称的流(英文名和中文名)
  2. Stream<String> stream = Stream.of("Alen", "Hebe", "Zebe", "张成瑶", "钟其林");
  3. // 通过判断人名称的首字母是否为英文字母,将其分为两个不同流
  4. final Map<Boolean, List<String>> map = stream.collect(Collectors.partitioningBy(s -> {
  5. // 如果是英文字母,则将其划分到英文人名,否则划分到中文人名
  6. int code = s.codePointAt(0);
  7. return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
  8. }));
  9. // 输出分组结果
  10. map.forEach((isEnglishName, names) -> {
  11. if (isEnglishName) {
  12. System.out.println("英文名称如下:");
  13. } else {
  14. System.out.println("中文名称如下:");
  15. }
  16. names.forEach(name -> System.out.println("\t" + name));
  17. });

程序输出结果如下:

  1. 中文名称如下:
  2. 张成瑶
  3. 钟其林
  4. 英文名称如下:
  5. Alen
  6. Hebe
  7. Zebe

高阶函数

什么是高阶函数高阶函数是指接受另外一个函数作为参数,或返回一个函数的函数。什么样的参数是函数类型的参数?要看该参数是否是一个函数式接口,函数式接口只会有一个方法,会使用 @FunctionalInterface 这个注解来修饰。

高阶函数在 Java8 中很常见,如以下的例子:

  1. Stream<Integer> numUp = Stream.of(1, 2, 5).map(num -> num += 1);
  2. Stream<Integer> numbers = Stream.of(1, 2, -1, -5).filter(n -> n > 0);

如何判断高阶函数
Stream 的 anyMatch 是高阶函数吗?是的。因为它的参数接收的是另外一个函数:Predicate。

boolean greaterThanZero = Stream.of(-1, -2, 0, -5).anyMatch(num -> num > 0);

Stream 的 limit 是高阶函数吗?是的。因为它的返回值是一个Stream。

Stream onlyTwoNumbers = Stream.of(-1, -2, 0, -5).limit(2);

原文链接: