一个Java码农眼中的技术世界我爱编程

java8实战学习心得

2018-04-08  本文已影响80人  crrrrw

为什么要用Java8

很多主流框架已经使用java8进行升级开发,java8是趋势,是时候一起拥抱java8了.

java8语言新特性

1.Lambda表达式

Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据。

1.1 lambda的基本语法是:
(parameters) -> expression

或者

(parameters) -> { statements; }

eg:java8中有效的lambda表达式:

  1. 一个String类型的参数并返回一个int。Lambda中没有return语句,因为已经隐含了return。
(String s) -> s.length()
  1. 两个int类型的参数而没有返回值。
(int x, int y) -> {
    System.out.println("Result:");
    System.out.println(x+y);
}
  1. 没有参数,返回一个int
() -> 1
1.2 函数式接口

java8中接口的变化:
Java 8用默认方法静态方法这两个新概念来扩展接口的声明。
此功能是为了向后兼容性增加,使旧接口可用于利用JAVA8,lambda表达式的能力
默认方法与抽象方法不同之处在于抽象方法必须要求实现,但是默认方法则没有这个要求。

在实际使用过程中,函数式接口是容易出错的:如有某个人在接口定义中增加了另一个方法,这时,这个接口就不再是函数式的了,并且编译过程也会失败。为了克服函数式接口的这种脆弱性并且能够明确声明接口作为函数式接口的意图,Java 8增加了一种特殊的注解@FunctionalInterface(Java 8中所有类库的已有接口都添加了@FunctionalInterface注解)。

目前已存在的函数式接口有:Comparable、Runnable和Callable等。
java8在java.util.function中有引入了很多个新的函数式接口:

1.3方法引用

方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。
当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。

三种方法引用:

  1. 指向静态方法的方法引用。
    lambda表达式: (args) -> ClassName.staticMethod(args)
    方法引用: ClassName::staticMethod

比如

s -> Integer.valueOf(s) 等价于 Integer::valueOf
  1. 指向任意类型实例方法的方法引用。
    lambda表达式: (arg0, rest) -> arg0.instanceMethod(rest)
    方法引用: ClassName::instanceMethod
    (上面args0是ClassName类型的)

比如

(str, integer) -> str.substring(integer) 等价于 String::substring
  1. 指向现有对象的实例方法的方法引用。
    lambda表达式: (args) -> expr.instanceMethod(args)
    方法引用: expr::instanceMethod

比如

(Apple arg) -> appleExpr.compareAppleWight(arg) 等价于 appleExpr::compareAppleWight

比如创建一个Apple对象。
空参构造器Apple():

Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();

等价于

Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();

两个参数的构造函数Apple(String color, Integer weight):

BiFunction<Integer, String, Apple> biFunction = Apple::new;
Apple apple2 = biFunction.apply(155, "green");

等价于

BiFunction<Integer, String, Apple> biFunction = (weight, color) -> new Apple(weight, color);
Apple apple2 = biFunction.apply(155, "green");
1.4 复合lambda表达式
  1. 比较器(Comparator<T>)复合(reversed(),thenComparing())
  2. 谓词(Predicate<T>)复合(negate(),and(),or())
  3. 函数(Function<T, V>)复合(compose(),andThen(),identity())

2.Stream API

Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。

Stream API(java.util.stream.*)可以让你的代码具备:

2.1 什么是流

流不是一种数据结构,而是处理集合元素的相关计算,更像一个高级的 Iterator。单向,不可往复,数据只能遍历一次。

如何使用流?

  1. 一个数据源(如集合)来执行一个查询;
  2. 一个中间操作链,形成一条流的流水线;
  3. 一个终端操作,执行流水线,并能生成结果。
2.2 使用流
2.2.1 筛选,切片
2.2.2 映射
2.2.3 查找和匹配
2.2.4 reduce

这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。

2.2.5 数值流
2.2.6 创建流
2.3 收集器
2.3.1 归约和汇总

Stream.reduce 与 Stream.collect的区别:
Stream.reduce,常用的方法有average, sum, min, max, and count,返回单个的结果值,并且reduce操作每处理一个元素总是创建一个新值。
Stream.collect修改现存的值,而不是每处理一个元素,创建一个新值。

2.3.2 数据分组
Map<Dish.Type, List<Dish>> groupByType = menuList.stream().collect(groupingBy(Dish::getType));
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> groupByTypeAndCalories = menuList.stream().collect(
            groupingBy(Dish::getType,
                    groupingBy(dish -> {
                        if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                        else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                        else return CaloricLevel.FAT;
                    }))
    );
Map<Dish.Type, Long> groupByTypeToCount = menuList.stream().collect(groupingBy(Dish::getType, counting()));
Map<Boolean, List<Dish>> partitionByVegeterian =
                menuList.stream().collect(partitioningBy(Dish::isVegetarian));
2.3.3 Collector接口
public interface Collector<T, A, R> {
    Supplier<A> supplier()
    BiConsumer<A, T> accumulator()
    Function<A, R> finisher()
    BinaryOperator<A> combiner()
    Set<Characteristics> characteristics()
}

Collector接口的三个泛型:

通过自定义ToList收集器理解接口方法:

2.4 并行数据处理

并行流:
可以通过对收集源调用parallelStream方法来把集合转换为并行流。并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。

问题:并行流用的线程是从哪儿来的?有多少个?怎么自定义这个过程呢?
并行流内部使用了默认的ForkJoinPool,它默认的线程数量就是你的处理器数量,这个值是由Runtime.getRuntime().available-Processors()得到的。
但是你可以通过系统属性java.util.concurrent.ForkJoinPool.common.parallelism 来改变线程池大小,如下所示:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
这是一个全局设置,因此它将影响代码中所有的并行流。反过来说,目前还无法专为某个并行流指定这个值。一般而言,让ForkJoinPool的大小等于处理器数量是个不错的默认值,除非你有很好的理由,否则我们强烈建议你不要修改它。

使用并行流:
本地测试的过程中,并行流比顺序流效果差。原因可能与机器,处理的数据量,使用并行流的方式等有关系。

是否使用并行流需考虑如下几种情况:

  1. 留意装箱。(使用(IntStream、LongStream、DoubleStream来避免装箱拆箱)
  2. 有些操作本身在并行流上的性能就比顺序流差。(limit,findFirst等依赖于元素顺序的操作)
  3. 考虑流的操作流水线的总计算成本。设N是要处理的元素的总数,Q是一个元素通过流水线的大致处理成本,则N*Q就是这个对成本的一个粗略的定性估计。Q值较高就意味着使用并行流时性能好的可能性比较大。
  4. 数据量较小的情况不适合并行流。
  5. 考虑流背后的数据结构是否易于分解。
  6. 流自身的特点,以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能。
  7. 考虑终端操作中合并步骤的代价是大是小(例如Collector中的combiner方法)。
可分解性
ArrayList 极佳
LinkedList
IntStream.range 极佳
Stream.iterate
HashSet
TreeSet

流的数据源

可分解性
ArrayList 极佳
LinkedList
IntStream.range 极佳
Stream.iterate
HashSet
TreeSet

3.Optional使用

java中的null带来了种种问题:典型常见,使代码膨胀,自身无意义等等等。

方法 描述
empty 返回一个空的Optional 实例
filter 如果值存在并且满足提供的谓词,就返回包含该值的Optional 对象;否则返回一个空的Optional 对象
flatMap 如果值存在,就对该值执行提供的mapping函数调用,返回一个Optional 类型的值,否则就返回一个空的Optional 对象
get 如果该值存在,将该值用Optional封装返回,否则抛出一个NoSuchElementException 异常
ifPresent 如果值存在,就执行使用该值的方法调用,否则什么也不做
isPresent 如果值存在就返回true,否则返回false
map 如果值存在,就对该值执行提供的mapping 函数调用
of 将指定值用Optional封装之后返回,如果该值为null,则抛出一个NullPointerException异常
ofNullable 将指定值用Optional封装之后返回,如果该值为null,则返回一个空的Optional 对象
orElse 如果有值则将其返回,否则返回一个默认值
orElseGet 如果有值则将其返回,否则返回一个由指定的Supplier接口生成的值
orElseThrow 如果有值则将其返回,否则抛出一个由指定的Supplier接口生成的异常

注意:Optional 无法序列化

4.新的日期和时间API

新的 java.time 中包含了所有关于:
时钟(Clock)、本地日期(LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)、时区(ZonedDateTime)和持续时间(Duration)的类。

历史悠久的 Date 类新增了 toInstant() 方法,用于把 Date 转换成新的表示形式。这些新增的本地化时间日期 API 大大简化了了日期时间和本地化的管理。
目前Java8新增了java.time包定义的类表示日期-时间概念的规则,很方便使用;最重要的一点是值不可变,且线程安全

本地日期时间API:

时区API:

时钟API:

计算日期时间差API:

时间格式化API

5. 其他

  1. java类库标准base64编码使用方式:
final String text = "测试Abc123!!¥¥";
final String encoded = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8)); 
System.out.println(encoded); // 5rWL6K+VQWJjMTIzISHvv6Xvv6U=
final String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
System.out.println(decoded); // 测试Abc123!!¥¥


// url encode
final String url = "http://www.jinhui365.com/abc?foo=中文&¥%&bar=hello123&baz=http://abc/def123";
final String encoded2 = Base64.getUrlEncoder().encodeToString(url.getBytes(StandardCharsets.UTF_8));
System.out.println(encoded2); // aHR0cDovL3d3dy5qaW5odWkzNjUuY29tL2FiYz9mb2895Lit5paHJu-_pSUmYmFyPWhlbGxvMTIzJmJhej1odHRwOi8vYWJjL2RlZjEyMw==
final String decoded2 = new String(Base64.getUrlDecoder().decode(encoded2), StandardCharsets.UTF_8);
System.out.println(decoded2); // http://www.jinhui365.com/abc?foo=中文&¥%&bar=hello123&baz=http://abc/def123

  1. jvm的变化:
    PermGen空间被移除了,取而代之的是Metaspace(JEP 122)。 JVM选项 -XX:PermSize与-XX:MaxPermSize分别被
    -XX:MetaSpaceSize与-XX:MaxMetaspaceSize所代替。

最后

Java8 作为 Java 语言的一次重大发布,包含语法上的更改、新的方法与数据类型,以及一些能默默提升应用性能的隐性改善。而且java8有利于提高开发生产力,对于开发者来说是好事,也是趋势。但是生产中使用java8可能存在风险,在正式使用Java8之前,不妨先体验一下java8的神奇。

附《java8 in action》源码:https://github.com/java8/Java8InAction.git

上一篇 下一篇

猜你喜欢

热点阅读