Java8部分学习
Java 8Java 8 是 Java 语言开发的一个主要版本。是 Oracle 公司于 2014.3 发布,可以看成是自 Java 5 以来最具革命性的版本。java 8 为 Java 语言、编译器、类库、开发工具与 JVM 带来了大量新特性。特性速度更快代码更少(新语法:Lambda 表达式)强大的 Stream API便于并行最大化减少空指针异常:OptionalNashorn 引擎,允许在
Java 8
- Java 8 是 Java 语言开发的一个主要版本。是 Oracle 公司于 2014.3 发布,可以看成是自 Java 5 以来最具革命性的版本。java 8 为 Java 语言、编译器、类库、开发工具与 JVM 带来了大量新特性。
特性
- 速度更快
- 代码更少(新语法:Lambda 表达式)
- 强大的 Stream API
- 便于并行
- 最大化减少空指针异常:Optional
- Nashorn 引擎,允许在 JVM 上运行 JS 应用
Lambda 表达式
Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码,(将代码像数据一样进行传递)。使用他可以写出更简洁、更灵活的代码、作为一种更紧凑的代码风格,使 Java 的语言表达能力得到了提升(个人感觉和ES6箭头函数一样)
-
简单感受一下 Lambda 表达式
-
测试用法:无参的
-
@Test public void test1(){ Runnable r1 = new Runnable() { @Override public void run() { System.out.println("Not Lambda"); } }; r1.run(); //--------------------------------------------- Runnable r2 = () -> System.out.println("Lambda"); r2.run(); }
-
测试用法:有参的
-
@Test public void test2(){ Comparator<Integer> com1 = new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return Integer.compare(o1,o2); } }; com1.compare(10,20); Comparator<Integer> com2 = (o1,o2)->Integer.compare(o1,o2); com2.compare(10,20); //方法引用 Comparator<Integer> com3 = Integer::compare; com3.compare(10,20); }
-
解释一下上面的例子
(o1,o2)->Integer.compare(o1,o2);
- ->:lambda 操作符或箭头操作符
- 左边:lambda 形参列表(其实就是接口中的抽象方法的形参列表)
- 右边:lambda 体(其实就是重写的抽象方法的方法体)
需要注意的是,在其他地方 lambda 表达式的本质可能是函数,但是在 java 中 lambda 表达式的本质其实是接口的实例(再一次体现万物皆对象)
- 总结:对一个函数式接口实例化的时候可以使用 Lambda 表达式,所以说 lambda 表达式是函数式接口的实 例
六种使用形式
根据不同的情况,Lambda 表达式的使用 也不同
无参,无返回值:
- 其实上面的第一个例子就是,无参左边就空的括号,右边直接方法体
Runnable r2 = () -> System.out.println("Lambda");
- 其实本来方法体外面都有
{}
,但是这就一行代码,所以可以省略,本来应该是 Runnable r2 = () -> { System.out.println("Lambda");}
有参,无返回值
-
以 Consumer 为例
-
@Test public void test1(){ Consumer<String> con1 = new Consumer<String>(){ @Override public void accept(String s) { System.out.println(s); } }; con1.accept("con1"); //------------------------ Consumer<String> con2 = (String s)-> System.out.println(s); con2.accept("con2"); }
-
如上代码上面是普通的实现接口,重写方法,而下面只需要一行就能搞定,从这里其实也能看出 Lambda 表达式本质其实就是接口的实例,就像你创建了一个叫 Jack 的人类,这里是创建了一个 accept 方法为输出传参的 Consumer 类
数据类型可以省略
- 上面的代码改进一下:
Consumer<String> con2 = (s)-> System.out.println(s);
- 为啥可以省略,因为编译器会类型推断,比如
List<String> list = new ArrayList<>();
,你前面都写了是 String 的 List 了,我后面就不用写了。就像 Consumer 你都写了是 String 类型的了,所以我可以省略
若只需要一个参数,括号可以省略
- 上面的代码再再改进一下
Consumer<String> con2 = s-> System.out.println(s);
- 就一个参数确实没必要加括号,换我我也这么设计
两个以上参数,多条执行语句,有返回值
-
其实也就是参数的括号和方法体的花括号不能省略,直接拿上面的例子加个语句
-
Comparator<Integer> com2 = (o1,o2)->{ System.out.println(o1); return Integer.compare(o1,o2); }
Lambda 体只有一条执行语句
- 可以省略方法体的花括号,上面早就省略过了
总结
左边:类型可以省略,参数列表只有一个可以省略小括号否则不省
右边:Lambda 体只有一条执行代码可以省略大括号,如果 return xx,如果省略了大括号还得把 return 省了
- 上面的例子都有一个特点,实现的接口都只有一个函数,不然你用 Lambda 重写一个方法谁知道你重写了哪个方法,这就是函数式接口
函数式(Functional)接口
如果一个接口只声明了一个抽象方法,就是函数式接口。上面一般有 @FunctionalInterface 注解,但是不加也没事,加了也就是一个校验,比如你声明了两个方法就会报错,就像 @Override ,你不写这个但是你重写了方法你也还是重写
- 可以通过 Lambda 表达式来创建该接口对象,所以 Lambda 必须依赖于函数式接口
- 可以在接口上使用 @FunctionalInterface 注解没这样做可以检查他是否是一个函数式接口,同时 javadoc 也会包含一条声明,说明这个函数是一个函数式接口
- 在 java.util.function 包下定义了 Java8 丰富的函数式接口
Java 内置四大核心函数式接口
-
Consumer<T>
:消费型,接收参数但无返回值,方法为void accept(T t)
-
Supplier<T>
:供给型,不接受参数但有返回值,方法为T get()
-
Function<T,R>
:函数型,接收参数返回结果,方法为R apply(T t)
-
Predicate<T>
:断定型,判断 T 类型对象是否满足某种约束并返回 boolean 值,方法为boolean test()
-
玩一下断定型,或者叫断言型
-
@Test public void test2(){ List<String> list = Arrays.asList("小明","小光","小刚","大熊"); //不用 lambda List<String> list1 = filterList(list, new Predicate<String>() { @Override public boolean test(String s) { return s.contains("小"); } }); //用 lambda List<String> list2 = filterList(list, s -> s.contains("小")); } public List<String> filterList(List<String> list, Predicate<String> pre){ List<String> filter = new ArrayList<>(); list.forEach(str->{ if(pre.test(str)){ filter.add(str); } }); return filter; }
-
还会有一些 BiXXX的函数式接口,就是基于四种核心函数式接口的,只是传参可以为多个,还有一些子接口比如 Function 的传参和返回类型可以不一致,而他的子接口是传参和返回类型一致
方法引用与构造器引用
方法引用
当要传递给 Lambda 体的操作,已经有了实现的方法,就可以使用方法引用
-
方法引用可以看做是 Lambda 表达式深层次的的表达,换句话说,方法引用也是 Lambda 表达式,所以肯定是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是 Lambda 表达式的一个语法糖
-
要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致(比如两个方法的传参都是泛型且无返回值)
-
格式:使用操作符
::
将类(或对象)与方法名隔开 -
如下三种主要使用情况
对象::实例方法名
-
//Consumer 中的 void accept(T t) // PrintStream 中的 void println(T t) @Test public void test3(){ Consumer<String> con = s -> System.out.println(s); con.accept("无"); }
-
上面不就是我们说的情境,Lambda 操作符右边的不就是已经实现的函数式接口的一个实例,而我们说的要求不也满足了,accpet 和 println 传参返回值一致,所以可以写成如下
-
PrintStream ps = System.out; Consumer<String> con = ps::println;
-
其实这也是强行
对象::实例方法名
,可以用下面这种的 -
Consumer<String> con = System.out::println;
-
这里可以直接省略传参 s,那就可以看出为什么有上面的要求,因为如果你 accept 有两个传参,println 怎么知道输出哪个
类::静态方法名
-
@Test public void test4(){ // Comparator 的 compare(T t1,T t2) Comparator<Integer> com = (t1,t2)->Integer.compare(t1,t2); // Integer 的 compare(T t1,T t2) Comparator<Integer> com1 = Integer::compare; }
-
符合要求和使用情境,所以可以用方法引用
类::实例方法名(理解有难度)
-
简单看一个例子
-
Comparator<String> com = (s1,s2) -> s1.compareTo(s2); com.compare("abc","abd");
-
实现的方法为 Comparator 的
int compare(T t1,T t2)
-
但是右边的 Lambda 体用的方法为 String 的
int s1.compareTo(s2)
-
参照上面的例子,这里虽然返回值都是 int ,传参类型也可以都是 String,但是传参个数好像不太一致,一个是两个参数,一个是对象使用方法传一个参数,但是也可以对象引用
-
Comparator<String> com = String::compareTo;
-
我的理解。其实就是不管你什么形式,你用到的还是两个参数,本身 compare 也就是默认第一个传参和第二个传参比,compareTo 不也是第一个用方法的参数和第二个传进去的参数比,顺序个数其实也还是一致的,也就是上面的要求其实就是为了保证返回值以及传参的类型,还有传参个数和顺序的一致(个人理解仅供参考)
-
理解了上面这个那再来一个,比如我有一个 Employee 员工类,有 name 属性
-
Function<Employee,String> func = e->e.getName(); Function<Employee,String> func = Employee::getName;
-
这里一个是 Function 的 R apply(T t),一个是 Emmplyee 的 String getName(),看起来好像更加不符合要求,但是其实对应到这个例子还是一样。 String apply(Employee) 对应了 String Employee.getName(),返回类型都是 String,传参都是 Employee 类,只有一个传参也不存在顺序
-
所以还是可以写成
Function<Employee,String> func = Employee::getName;
-
总结:写 Lambda 表达式的时候如果和 Lambda 体的方法结构相同(传参和返回类型相同),那可以直接把 Lambda 表达式换成方法引用
构造器引用和数组引用
构造器引用
-
其实理解了方法引用就不难理解这个,和方法引用同理,举个例子
-
//最原始 Supplier<Employee> sup = new Supplier<>(){ @Override public Employee get() { return new Employee(); } } //lambda 表达式 Supplier<Employee> sup = ()->new Employee(); //构造器引用 Supplier<Employee> sup2 = Employee::new;
-
这里一个是 Supplier 的
T get()
,一个是 Employee 的Employee ()
-
对应到这里就是返回值都是 Employee,传参都是空参,所以可以使用
数组引用
-
@Test public void test6(){ Function<Integer,String[]> func1 = length -> new String[length]; String[] apply1 = func1.apply(5); System.out.println(Arrays.toString(apply1)); Function<Integer,String[]> func2 = String[]::new; String[] apply2 = func1.apply(5); System.out.println(Arrays.toString(apply2)); }
-
其实和构造器也一样,只不过把 String[] 看成整体,是调用数组的构造器
Stream API
Java8 中有两个最为重要的改变,第一个是 Lambda 表达式,另一个就是 Stream API
- 使用 Stream API 可以对集合进行查找、过滤、映射等操作,类似于使用 SQL 执行数据库的查询
- 现在的数据源还有类似 MongoDB、Redis 这种 NoSQL 的数据就需要 Java 层来处理
- Stream 和 Collection 的区别:Collection 是一种静态的内存数据结构,而 Stream 是有关计算的。前者主要面向内存,存储在内存中,后者主要面向 CPU,通过 CPU 实现计算
注意
- Stream 自己不会存储数据
- Stream 不会改变源对象,相反,会返回一个一个持有新结果的 Stream
- Stream 操作是延迟执行的,这意味着他们会等到需要结果的时候才执行
操作的三个步骤
- 创建 Stream:一个数据源(如:集合、数组),获取一个流
- 中间操作:一个中间操作链,对数据源的数据进行处理,比如数据映射,过滤
- 终止操作:一旦执行终止操作就执行中间操作链并产生结果,之后不会再被使用
- 这也就是为什么说 Stream 操作是延时的,因为不执行终止操作你中间的一堆操作都不会执行
Stream 的实例化
通过集合
List<String> list = Arrays.asList("aa","bb","cc");
- 返回顺序流:
Stream<String> stream = list.stream();
- 返回并行流:
Stream<String> stream2 = list.parallelStream():
通过数组
-
String[] array = new String[]{"aaa","bbb","ccc"}; Stream<String> stream1 = Arrays.stream(array);
通过 Stream.of
-
Stream<Integer> integerStream = Stream.of(1, 2, 3);
创建无限流
-
//生成前十个偶数 //迭代 Stream.iterate(0,s->s+2).limit(10).forEach(System.out::println);
-
//生成十个随机数 //生成 Stream.generate(Math::random).limit(10).forEach(System.out::println);
Stream 的中间操作
筛选与切片
-
filter(Predicate p):接收 Lambda,从流中排除某些元素
-
//筛选偶数 List<Integer> list = Arrays.asList(1, 2, 3, 4); Stream<Integer> stream = list.stream(); stream.filter(i -> i % 2 == 0).forEach(System.out::println); //实际可以合并起来,因为执行终止操作后 Stream 流就失效了,得重新创建,所以还不如每次都直接新建然后使用也就是 list.stream().xxx list.stream().filter(i -> i % 2 == 0).forEach(System.out::println);//=>2,4
-
-
distinct:筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
-
//去掉重复的元素 List<Integer> list2 = Arrays.asList(1, 2, 2, 3); list2.stream().distinct().forEach(System.out::println); //=>1,2,3
-
-
limit(long maxsize):截断流,使其元素不超过给定数量
-
//输出集合前两个元素 list.stream().limit(2).forEach(System.out::println);//=>1,2
-
-
skip(long n):跳过元素,返回一个扔掉了前 n 个元素的流,若流中元素不足 n 个,则返回空流,与 limit 互补
-
//输出跳过前两个元素的集合 list.stream().skip(2).forEach(System.out::println);//=>3,4
-
映射
-
map:接收一个函数作为参数,该函数被应用到每个元素上,并将其映射为一个新的元素
-
//所有元素乘以2 List<Integer> list = Arrays.asList(1, 2, 3); list.stream().map(i->i*2).forEach(System.out::println);//=>2,4,6
-
-
mapToDouble:接收一个函数作为参数,该函数被应用到每个元素上,产生一个新的 DoubleStream
-
mapToInt:接收一个函数作为参数,该函数被应用到每个元素上,产生一个新的 IntStream
-
mapToLong:接收一个函数作为参数,该函数被应用到每个元素上,产生一个新的 LongStream
-
flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
-
他和 map 就像 List 的 addAll 和 add,比如有一个返回字符流的方法
-
public static Stream<Character> stringToCharacter(String s){ char[] chars = s.toCharArray(); ArrayList ary = new ArrayList(); for (Character c:chars){ ary.add(c); } return ary.stream(); }
-
如果使用 map 输出一个字符串数组的所有字符
-
List<String> list = Arrays.asList("aa","bb"); Stream<Stream<Character>> mapStream = list.stream().map(StreamTest::stringToCharacter); mapStream.forEach(s->s.forEach(System.out::println));
-
因为流中是 {{a,a},{b,b}},所以每个元素还需要各自循环输出
-
而如果使用 flatMap
-
Stream<Character> flatStream = list.stream().flatMap(StreamTest::stringToCharacter); flatStream.forEach(System.out::println);
-
他在映射的时候会把每个流合并成一个流,也就是把 {a,a} 和 {b,b} 自动合并成了 {a,a,b,b} 所以直接循环输出即可
-
排序
-
sorted():自然排序
-
List<Integer> list = Arrays.asList(12, 43, 65, 34); list.stream().sorted().forEach(System.out::println);
-
默认会从小到大排序
-
但是如果换成一个实体类比如 Employee 的排序,就会报错,因为 Employee 未实现 Comparable 接口,那你要么实现接口,要么用下面一种,临时定制规则
-
-
sorted(Comparator com):定制排序
-
假设有一个 User 类
-
@Data @AllArgsConstructor @NoArgsConstructor public class User { private long id; private int age; public static List<User> getUsers(){ List<User> users = new ArrayList<>(); for (int i = 0; i < 5; i++) { char a = 'a'; User user = new User(i, i * 10); users.add(user); } return users; } }
-
如果要实现 Comparable 接口就要写成
public class User implements Comparable<User>
,然后重写 compareTo 方法,而定制排序就可以暂时定制规则,例如按照年龄从大到小排列 -
List<User> list = User.getUsers(); list.stream().sorted((u1,u2)->-Integer.compare(u1.getAge(),u2.getAge())).forEach(System.out::println);
-
如果是从小到大排序还能简化如下
-
list.stream().sorted(Comparator.comparingInt(User::getAge)).forEach(System.out::println);
-
Stream 的终止操作
匹配与查找
- allMatch(Predicate p):检查是否匹配所有元素
- anyMatch(Predicate p):检查是否至少匹配一个元素
- noneMatch(Predicate p):检查是否没有匹配的元素
- findFirst:返回第一个元素
- findAny:返回当前流中的任意元素
- count:返回流中元素的总个数
- max(Consumer c):返回流中最大值
- min(Consumer c):返回流中最小值
- forEach(Consumer c):内部迭代
- 集合的 forEach 是外部迭代,可以理解为外部有个指针然后指向一个个迭代的数据,而内部迭代就是内部的数据自动取到下一个下一个的。
归约
-
reduce(T identity,BinaryOperator):可以将流中的元素反复结合起来,得到一个值,返回一个 T
-
例如计算 1-10 的自然数的和
-
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Integer sum = list.stream().reduce(0, Integer::sum); System.out.println(sum);
-
0 代表初始值,从 0 开始先执行 sum,也就是 0+1,然后以 0+1 也就是 1 为起点执行 sum,也就是 1+2,然后以 1+2 也就是 3 为起点执行 sum,也就是 3+4 …
-
-
reduce(BinaryOperator):可以将流中的元素反复结合起来,得到一个值,返回一个
optional<T>
- 就是没有初始值的用法
收集
-
collect(Collector c):将流转换为其他形式。接收一个 Collector 接口的实现,用于给 Stream 元素做汇总
-
这里的 Collector 一般用 Collectors 提供实例:
- toList:把流中的元素收集到 List
- toSet:把流中的元素收集到 Set
- toCollection:把流中的元素收集到创建的集合
- counting:计算流中元素的个数
- summingInt:对流中元素的整数属性求和
- averagingInt:计算流中元素 Integer 属性的平均值
- summarizingInt:收集流中 Integer 属性的统计值。如:平均值
-
示例:
-
List<User> users = User.getUsers(); List<User> list = users.stream().filter(user -> user.getAge() > 20).collect(Collectors.toList());
-
Optional 类
Optional 类是一个容器类,它可以保存类型 T 的值,代表这个值存在。或者仅仅保存 null,表示这个值不存在。它可以避免空指针异常。Javadoc 描述如下:这是一个可以为 null 的容器对象。如果值存在则 isPresent() 方法会返回 true,调用 get() 方法会返回该对象。
创建 Optional 类对象的方法:
- Optional.of(T t):创建一个 Optional 实例,t 必须非空
- Optional.empty():创建一个空的 Optional 实例
- Optional.ofNullable(T t):t 可以为 null
判断 Optional 容器中是否包含对象:
-
boolean isPresent():判断是否包含对象
-
void ifPresent(Consumer<? super T> consumer):如果有值,就执行 Consumer接口的实现代码,并且该值会作为参数传给它
获取 Optional 容器的对象:
- T get():如果调用对象包含值,返回该值,否则抛异常
- T orElse(T other):如果有值则将其返回,否则返回指定的 other 对象(默认值的感觉)
- T orElseGet(Supplier<? extends T> other):如果有值则将其返回,否则返回由 Supplier 接口实现提供的对象
- T orElseThrow(Supplier<? extends X> exceptionSupplier):如果有值则将其返回,否则抛出由 Supplier 接口实现提供的异常
测试
项目常见情况
-
例如有两个类如下
-
@Data @AllArgsConstructor @NoArgsConstructor public class Girl { private String name; } @Data @AllArgsConstructor @NoArgsConstructor public class Boy { private Girl girl; }
-
此时我测试如下
-
public String getGirlName(Boy boy){ return boy.getGirl().getName(); } @Test public void test3(){ Boy boy = new Boy(); String girlName = getGirlName(boy); System.out.println(girlName); }
-
此时会报空指针,虽然 Boy 不为空,但是他的 girl 字段为空所以无法获取到 name
-
那么我们自己优化方法会用几个嵌套的 if 来判断是否为空
-
但是使用 Optional 就可以很好地如下优化:
-
public String getGirlName2(Boy boy){ //先创建 Boy 的 Optional Optional<Boy> boyOptional = Optional.ofNullable(boy); //Boy 为空就给个默认值,否则为原值 Boy boy1 = boyOptional.orElse(new Boy(new Girl("aaa"))); //此时 Boy 肯定不为空,就创建 Girl 的 Optional Optional<Girl> girlOptional = Optional.ofNullable(boy1.getGirl()); //如果 Girl 为空就给个默认值,否则为原值 Girl girl = girlOptional.orElse(new Girl("bbb")); //此时 Girl 是不可能为空了的,直接返回 girl 的名字就好 return girl.getName(); }
-
总结,确定有值的用 of 和 get,不确定就 ofNullable 和 orElse,把 Optional 看成容器就行,前面的方法都是存取
接口的增强
静态方法
默认方法
新的时间和日期 API
其他新特性
重复注解
类型注解
通用目标类型推断
JDK 的更新
集合的流式操作
并发
Arrays
Number 和 Math
IO/NIO 的改进
Reflection 获取形参名
String:join()
Files
新编译工具:jjs、jdeps
JVM 中 Metaspace 取代 PermGen 空间
更多推荐
所有评论(0)