转载

Java8中两个优雅的特性

算是之前写代码的时候学到的两个比较有意思的特性,也是Java8新增的两个功能,其实说到底就是减少了一些nullptr的判断和for循环的遍历,让代码变得更加优雅,可读性更高。

  • Optional
  • Stream

主要就是如上两个特性,总结的过程中也学了一些以前不知道的特性。

Optional

先从Optional来吧,他其实是对原有的数据类型进行了一层包装,将nullptr判断的部分操作进行了一定的封装。

可以先来看一些常规的操作

public static void main(String[] args) {
    String r1 = getStrOpt().orElse("default string");
    String r2 = getStrOpt().orElseGet(() -> String.valueOf(Math.random()));
    String r3 = getStrOpt().orElseThrow(IllegalStateException::new);
    System.out.println(r1);
    System.out.println(r2);
    System.out.println(r3);
}

public static Optional<String> getStrOpt() {
    return Math.random() > 0.5 ? Optional.of("optional str") : Optional.empty();
}

可以看到可以通过如下三种方式来从Optional中获取值,并为没有值的情况选择一个默认的返回情况,可以是一个default值,也可以是通过调用某个函数返回的值,以及抛出一个异常

Optional.orElse
Optional.orElseGet
Optional.orElseThrow

无能为力之处

但是其实它并不能化简如下的这种操作,遇到这种情况时其实和判断!=null并没有什么太多的区别。

public static void main(String[] args) {
     String s = getStr();
     if(s != null){
         System.out.println(s);
     }

     Optional<String> sOpt = getStrOpt();
     if(sOpt.isPresent()){
         System.out.println(sOpt.get());
     }
 }

public static String getStr() {
    return Math.random() > 0.5 ? "str" : null;
}

public static Optional<String> getStrOpt() {
    return Math.random() > 0.5 ? Optional.of("optional str") : Optional.empty();
}

可以看到要进行的操作是差不多的,并不比null判断要方便多少(或许他的isPresent让人看的更有逼格?)

优势

那Optional的好处在于哪呢?他真正方便的地方可能在于一些链式的操作。

假设下面 flatMapgetUSBgetVersion 处理返回 的都是个Optional/<T> 对象,就可以用如下链式操作

String version = computer.flatMap(Computer::getSoundcard)
                   .flatMap(Soundcard::getUSB)
                   .flatMap(USB::getVersion)
                   .orElse("UNKNOWN");

否则需要进行多次的!=null的判断比较,才能进行下一步的操作。

对于一个empty的Optional,flatMap的调用之后的结果也是一个空的Optional,从而避免了nullptr的异常。

假如调用函数处理的不是Optional对象,则应该调用Optional.map来进行处理

Optional<String> a = getStrOpt().map(String::toUpperCase);
public<U> Optional<U> map(Function<? super T, ? extends U> mapper)
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper)

总之呢,个人感觉Optional确实解决了一部分空指针的烦恼,但也需要你的项目代码里有较多的Optional代码的支持,否则就会出现之前 isPresent 判断并没有什么优化的尴尬情况,反而让人觉得多套了一层这个Optional的麻烦。

Stream

流式操作, stream 这是一个在 Collection 接口的方法,可以将集合以一种很优雅且性能相当高的方式进行操作。

long count = words.stream()
    .filter(w -> w.length() > 12)
    .count()

可以很容易的看出这是从words中筛选长度大于12的字符串,并计算它的个数。

流的创建

可以通过两种方式创建流

  • Collection.stream() − 为集合创建串行流。

  • Collection.parallelStream() − 为集合创建并行流。

其实当数据量不大时,用stream完全没有问题,当数据量大到需要优化时,可以采用并行流的方式。

但是并行流由于是并行的关系,所以对于遍历顺序之类的操作是无法指定的,否则也失去了并行的优势。

或者通过Stream的一些静态方法

Stream.of("a","b","c")
Stream.generate(() -> "kingkk")
Stream.iterate(BigInterger.ZERO, n -> n.add(BigInter.ONE))
Stream.empty()

通过这种函数调用之类的方式,可以生成一个无限流,也就类似与Python中的yield生成器。

流式操作还有几个特点

  • 流并不存储元素
  • 流并不会修改数据源
  • 尽可能的惰性处理(也是提升性能的一个很重要的步骤)

终结操作和中间操作

流的操作主要分为 中间操作终结操作 ,他们之间的区别主要在于

  • 中间操作:中间操作在一个流上进行操作,返回结果是一个新的流。这些操作是 延迟执行 的。
  • 终结操作:终结操作遍历流来产生一个结果或是副作用。在一个流上执行终结操作之后,该流被消费, 无法再次被消费

其实直接看代码也很好区分一个stream上的操作是终结操作还是中间操作

Optional<T> findAny();
Stream<T> filter(Predicate<? super T> predicate);

可以看到中间操作的返回类型都是Stream,而终结操作的返回类型是其他的结果,也就不能在进行流的链式操作了。

有很多,我就列几个常用的吧

中间操作

  • map(lamda) 就是map,别问我是啥
  • flatMap(lamda) 将数据类型 铺平 之后进行map操作,类似于 [[a,b,c], [d,e] ] -> [func(a), func(b), func(c), func(d), func(e) ]
  • filter(lamda) 传入一个判断函数,过滤只符合条件的数据
  • distinct() 类似Mysql的distinct,去除重复的数据
  • imit(num) 截断流使其最多只包含指定数量的数据
  • skip(num) 返回一个新的流,并跳过原始流中的前 N 个元素。
  • sorted([lamda]) 对流进行排序,也可以传入一个函数,按指定方式排序
  • peek(lamda) 产生另一个流,通常用于调试之类的操作,由于它不是终结操作,流并不会终止
  • concat(stream) 连接流

终结操作

forEach(lamda)
forEachOrdered(lamda)
max()
min()
findFirst()
findAny()
anyMatch(lamda)
allMatch(lamda)
noneMatch(lamda)
reduce(lamda)

Collector

有时候对流进行操作之后只是想获得过滤之后的集合,就可以用

  • toArray 默认会返回一个Object[], 想指定类型的话可以使用stream.toArray(String[]::new)

Collectors 提供了一个更为便捷的方式

stream.collect(Collectors.toList());
stream.collect(Collectors.toSet());
stream.collect(Collectors.toMap(Person::getName, Person::getAge))
stream.collect(Collectors.toCollection(TreeSet::new));

通过如上方式都可以很简单将筛选后的流转换成Set、List、或者指定数据类型等形式。

甚至可以讲所有的结果以字符串的形式连接起来

stream.collect(Collectors.joining());
stream.collect(Collectors.joining(", "));

或者收集一个可以同时获取最大值、最小值、平均值的数据类型

IntSummarzingInt summary = stream.collect(Collectors.summarizingInt(String::length)); // 这里的Int也可以是Double|Long
summary.getAverage();
summary.getMax();
summary.getMin();

对toMap有更多的定制化的方式

// 当value的值是元素本身时
people.collect(Collectors.toMap(Person::getId, Function.identity()));

// 当存在key值冲突时
locales.collect(
    Collectors.toMap(
    	Locale::getDisplayLanguage,
        l -> l.getDisplayLanguage(l),
        (a, b) -> a
    )
);

// 并指定特定的数据结构
locales.collect(
    Collectors.toMap(
    	Locale::getDisplayLanguage,
        l -> l.getDisplayLanguage(l),
        (a, b) -> a,
        TreeMap::new
    )
);

还提供了一个mysql中group by的功能

Map<Integer, List<Person>> map = people.collect(
    Collectors.groupingBy(Person::getAge)
);

如果group by之后的结果希望是一个Set

people.collect(
    Collectors.groupingBy(Person::getAge, Collectors.toSet())
);

第二个当然不止可以传入 Collectors.toSet 也可以是一些

Collectorss.counting()
Collectors.maxBy(...)
Collectors.minBy(...)
Collectors.mapping(...)

之类其他下游收集器操作

常用的操作就到这吧,其他的一些需要时再查阅可能会更方便一些。

原文  https://www.kingkk.com/2019/12/Java8中两个优雅的特性/
正文到此结束
Loading...