Java8新特性
前言:
北京时间2018年9月26日,Oracle官方发布Java 11。既然版本都更新到11了,现在才来学8是不是太晚了?其实不是的,目前应该大部分都还是使用的Java 7和Java 8,这两个应该还是主流。而Java 8 又有一些激动人心的新特性,所以还是值得学习的。Java 8 新特性主要有以下几点:
- Lambda表达式(重点);
- 函数式接口;
- 方法引用与构造器引用;
- Stream API(重点);
- 接口中的默认方法与静态方法;
- 新时间日期API;
- 其他新特性。
有了以上新特性,Java 8就可以做到:
- 速度更快;
- 代码更少(增加了新的语法 Lambda 表达式);
- 方便操作集合(Stream API)
- 便于并行;
- 最大化减少空指针异常 Optional。
接下来一起来了解一下Java 8的这些新特性。
一、Lambada表达式:
1、什么是lambda?
Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。
2、了解新操作符:
Java 8引入了新的操作符,->,叫箭头操作符或者叫lambda操作符。当使用lambda表达式时就需要使用这个操作符。
3、lambda表达式语法:
箭头操作符将lambda表达式分成了两部分:
- 左侧:lambda表达式的参数列表(接口中抽象方法的参数列表)
- 右侧:lambda表达式中所需执行的功能(lambda体,对抽象方法的实现)
语法有如下几种格式:
- 语法格式一(无参数无返回值): () -> 具体实现
- 语法格式二(有一个参数无返回值): (x) -> 具体实现 或 x -> 具体实现
- 语法格式三(有多个参数,有返回值,并且lambda体中有多条语句):(x,y) -> {具体实现}
- 语法格式四:若方法体只有一条语句,那么大括号和return都可以省略
注:lambda表达式的参数列表的参数类型可以省略不写,可以进行类型推断。
看几个例子:
例一:
@Test
public void test1(){
// 实现一个线程
int num = 0;//jdk1.8以前,这个必须定义为final,下面才能用,1.8后默认就为final
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("hello world"+ num);
}
};
runnable.run();
}
创建一个线程,重写run方法,在run方法里面打印一句话。我们想要的就是System.out.println("hello world"+ num);
这行代码,但是为了实现这行代码,不得不多写了好多行。lambda就可以解决这一点,看看用lambda如何实现:
Runnable runnable1 = () -> System.out.println("hello world"+num);
runnable1.run();
用lambda这样就搞定了。首先还是Runnable runnable1 =
,但是不用new了,右边就用lambda实现。我们要使用的是该接口的run方法,run方法不需要参数,所以lambda表达式左边就是(),lambda表达式右边是抽象方法的实现,也就是第一种方式中run方法的方法体写到lambda表达式右边就可以了。
例二:
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1,o2);//就这一行关键代码
}
};
以前写一个比较器就要像上面那样写,先new比较器类,然后在其compare方法里写核心代码。用lambda实现:
Comparator<Integer> comparator = (x,y) -> Integer.compare(x,y);
compare方法需要两个参数,所以箭头操作符左边写(x,y),右边是compare方法的实现,所以应该写return Integer.compare(o1,o2);
,但是根据上面的语法格式四可知,return可以省略,因此就写成了上面那样。
通过这两个例子可以感受到lambda表达式的简洁,但是问题来了:我们说lambda表达式就是一个匿名函数,我们只需要指定参数和lambda体即可,那么它是如何判断重写的是哪个方法呢?比如一个接口中有多个方法,如果使用lambda表达式来写,那么如何判断我们使用的是该接口的哪个方法?其实是不能判断的!通过上面两个例子可以发现,Runnable接口和Comparator接口都是只有一个方法的接口,所以可以使用lambda。
二、函数式接口:
1、什么是函数式接口?
像Runnable和Comparator这样只有一个方法的接口,称为函数式接口。也可以在接口上加上@FunctionalInterface
注解,如果编译通过,则该接口就是函数式接口。lambda表达式就需要函数式接口的支持。
2、看一个需求:
需求:需要对两个数进行加减乘除等运算,怎么实现?
- 传统做法:传统做法中,需要进行几种运算,我们就要写几个方法。一种运算对应一个方法。
- lambda做法:首先要定义一个函数式接口,接口中只有一个方法,接收两个参数。
@FunctionalInterface
public interface MyInterface {
public Integer getValue(Integer num1,Integer num2);
}
然后就可以使用了:
@Test
public void test5(){
MyInterface myInterface = (x,y) -> x*y;//乘法运算
MyInterface myInterface1 = (x,y) -> x+y;//加法运算
Integer result1 = myInterface.getValue(100,200);
Integer result2 = myInterface1.getValue(1024,2048);
System.out.println(result1);
System.out.println(result2);
}
所以用lambda的话,只需要定义一个函数式接口,不管进行什么操作,都可以用lambda解决,不用再一种运算对应一个方法。但是,还需要自己定义函数式接口,好像也没简单很多。Java考虑到这点了,所以内置了函数式接口。
3、四大内置函数式接口:
为了不需要我们自己定义函数式接口,Java内置了四大函数式接口,这四大接口加上它们的子类,完全满足我们的使用了。四大函数式接口是:
- Consumer<T>:消费型接口(void accept(T t)),接收一个参数,无返回值。
- Supplier<T>:供给型接口(T get()),无参数,有返回值。
- Function<T,R>:函数型接口(R apply(T t)),接收一个参数,有返回值。
- Predicate<T>:断言型接口(boolean test(T t)),接收一个参数,返回Boolean值。
4、四大函数式接口的使用:
接下来看看具体如何使用这四大函数式接口。
消费型接口的使用:
Consumer consumer = (x) -> System.out.println("消费了"+x+"元");
consumer.accept(100);
供给型接口的使用:
Supplier<Integer> supplier = () -> (int)(Math.random() * 100);//生成随机数
System.out.println(supplier.get());
函数型接口的使用:
Function<String,String> function = str -> str.toUpperCase();//将传入的字符串转成大写
String s = function.apply("adcdefggffs");
System.out.println(s);
断言型接口的使用:
//需求:将满足条件的字符串添加到集合中去
public List<String> filterString(List<String> strings, Predicate<String> predicate){
List<String> stringList = new ArrayList<>();
for (String string : strings) {
if (predicate.test(string)){
stringList.add(string);
}
}
return stringList;
}
//测试
@Test
public void test4(){
List<String> list = Arrays.asList("hello","world","niu","bi");
List<String> newList = filterString(list,str -> str.length() > 3);//选出长度大于3的字符串
newList.forEach(System.out::println);
}
三、方法引用与构造器引用:
当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用。不过实现抽象方法的参数列表,必须与引用方法的参数列表保持一致。
1、方法引用语法:
- 对象::实例方法
- 类::静态方法
- 类::实例方法
2、方法引用具体用法:
说了那么多可能还不清楚到底什么意思,一起来看几个例子。
语法一例子:
Consumer<String> consumer = x -> System.out.println(x);//传统写法
Consumer<String> consumer = System.out::println;//使用方法引用
println方法和Consumer的accept方法都是无返回值,接收一个参数,所以可以这样写。
语法二例子:
Comparator<Integer> comparator = (x,y) -> Integer.compare(x,y);
//因为compare方法已经被Integer实现了,且是静态的,所以这样用就行。
Comparator<Integer> comparator1 = Integer::compare;
语法三例子:
BiPredicate<String,String> biPredicate = (x,y) -> x.equals(y);
//可以改成如下写法
//不过要满足:第一个参数是实例方法的调用者,第二个参数是实例方法的参数时,就可以这样用
BiPredicate<String,String> biPredicate1 = String::equals;
3、构造器引用:
Supplier<Employee> supplier = () -> new Employee();
//可以改写成这样
//注意:需要调用的构造器的参数列表要与函数接口中抽象方法的参数列表一致
Supplier<Employee> supplier1 = Employee::new;
Employee employee = supplier.get();
四、Stream API:
Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
1、理解Stream:
Stream被称作流,是用来处理集合以及数组的数据的。它有如下特点:
- Stream 自己不会存储元素。
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
2、使用Stream的三个步骤:
- 创建Stream:一个数据源(如:集合、数组),获取一个流
- 中间操作:一个中间操作链,对数据源的数据进行处理
- 终止操作:一个终止操作,执行中间操作链,并产生结果
3、创建Stream:
直接看代码:
//1、通过集合提供的stream方法或parallelStream()方法创建
List<String> list = new ArrayList<>();
Stream<String> stringStream = list.stream();
//2、通过Arrays中的静态方法stream获取数组流
Employee[] employees = new Employee[10];
Stream<Employee> stream = Arrays.stream(employees);
//3、通过Stream类的静态方法of()创建流
Stream<String> stream1 = Stream.of("aa","bb","cc");
//4、创建无限流
//迭代方式创建无限流
//从0开始,每次加2,生成无限个
Stream<Integer> stream2 = Stream.iterate(0,(x) -> x+2);
//生成10个
stream2.limit(10).forEach(System.out::println);
//生成方式创建无限流
Stream.generate(() -> Math.random())
.limit(5)
.forEach(System.out::println);
上面介绍了集合、数组创建流的几种方式,都有对应的注解。
4、中间操作:
筛选与切片:
- filter -- 接收lambda,从流中排除某些数据。
- limit -- 截断流,使其元素不超过给定数量。
- skip(n) -- 跳过元素,返回一个扔掉了前n个元素的流,若不足n个元素,则返回空流。
- distinct -- 筛选,通过流所生成元素的hashCode()和equals()去除重复元素,所以对象必须重新hashCode方法和equals方法。
看代码:
employees.stream()//已有employees集合
.filter((e) -> e.getAge() > 18)//中间操作(选出年龄大于18的)
.limit(1)//中间操作(只返回一个)
.forEach(System.out::println);//终止操作
映射:
- map -- 接收lambda,将元素转换成其他形式或提取信息。接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
- flatMap -- 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所以流连接成一个流。
看例子:
List<String> list = Arrays.asList("aa","bb","cc","dd");
list.stream()
.map(str -> str.toUpperCase())//将所有的转成大写
.forEach(System.out::println);
排序:
- sorted() -- 自然排序(按照Comparable来排序)。
- sorted(Comparator com) -- 定制排序(按照Comparator来排序)。
看例子:
List<String> list = Arrays.asList("ccc","bbb","aaa","ddd");
list.stream()
.sorted()//自然排序
.forEach(System.out::print);//aaa,bbb,ccc,ddd
//定制排序
employees.stream()//employees是一个存有多名员工的集合
.sorted((e1, e2) -> {
if (e1.getAge().equals(e2.getAge())){ //如果年龄一样
return e1.getName().compareTo(e2.getName());//就比较姓名
}else {
return e1.getAge().compareTo(e2.getAge());//年龄不一样就比较年龄
}
}).forEach(System.out::println);
5、终止操作:
查找与匹配:
- allMatch -- 检查是否匹配所有元素。
- anyMatch -- 检查是否至少匹配一个元素。
- noneMatch -- 检查是否没有匹配所有元素。
- findFirst -- 返回第一个元素。
- findAny -- 返回当前流中任意元素。
- count -- 返回流中元素总个数。
- max -- 返回流中最大值。
- min -- 返回流中最小值。
//看看employee集合中是不是所有都是男的
boolean b = employees.stream()
.allMatch(e -> e.getGender().equals("男"));
System.out.println(b);
规约:
- reduce(T identity,BinaryOperator) -- 可以将流中元素反复结合起来,得到一个值。
//规约求和
List<Integer> list = Arrays.asList(1,3,5,4,4,3);
Integer sum = list.stream()
.reduce(0,(x,y) -> x+y);//首先把0作为x,把1作为y,进行加法运算得到1,把1再作为x,把3作为y,以此类推
System.out.println(sum);
//获取工资总和
Optional<Double> optional = employees.stream()
.map(Employee::getSalary)//提取工资
.reduce(Double::sum);//求工资总和
System.out.println(optional2.get());
收集:
- collect -- 将流转换为其他形式。接收一个Collector接口的实现,用于给Stream中元素做汇总的方法。
//把公司中所有员工的姓名提取出来并收集到一个集合中去
List<String> stringList = employees.stream()
.map(Employee::getName)//提取员工姓名
//.collect(Collectors.toList());//收集到list集合
//.collect(Collectors.toSet());//收集到set集合
.collect(Collectors.toCollection(LinkedList::new));//这种方式可收集到任意集合
stringList.forEach(System.out::println);//遍历集合
//计算工资平均值
Double avgSalary = employees.stream()
.collect(Collectors.averagingDouble(Employee::getSalary));
System.out.println(avgSalary);
//根据年龄分组
Map<Integer,List<Employee>> map = employees.stream()
.collect(Collectors.groupingBy(Employee::getAge));
System.out.println(map);
//先按性别分组,性别一样时按年龄分组
Map<String,Map<Integer,List<Employee>>> map1 = employees.stream()
.collect(Collectors.groupingBy(Employee::getGender,Collectors.groupingBy(Employee::getAge)));
System.out.println(map1);
//分区,满足条件的一个区,不满足的另一个区
Map<Boolean,List<Employee>> map2 = employees.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 6000));//工资大于6000的为true区,否则为false区
System.out.println(map2);
//获取工资的总额、平均值等
DoubleSummaryStatistics dss = employees.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println(dss.getSum());
System.out.println(dss.getAverage());
System.out.println(dss.getMax());
五、并行流与串行流:
1、fork/join框架:
此框架就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。
2、并行流与串行流:
通过上面的图可以知道,使用fork/join框架可以提高效率(运算量越大越明显,运算量可能反而更慢,因为拆分也需要时间),但是在Java 8之前需要自己实现fork/join,还是挺麻烦的,Java 8就方便多了,因为提供了并行流,底层就是使用了fork/join。Stream API 可以声明性地通过 parallel() 与 sequential() 在并行流与顺序流之间进行切换。
@Test
public void test(){
Instant start = Instant.now();
//普通做法求0加到10000000000的和
LongStream.rangeClosed(0,100000000000L)
.reduce(0,Long::sum);
Instant end = Instant.now();
System.out.println("耗费"+ Duration.between(end ,start) + "秒");//55秒
}
@Test
public void test2(){
Instant start = Instant.now();
//并行流求0加到10000000000的和
LongStream.rangeClosed(0,100000000000L)
.parallel()//使用并行流
.reduce(0,Long::sum);
Instant end = Instant.now();
System.out.println("耗费"+ Duration.between(end ,start) + "秒");//30秒
}
通过运行上面的程序可以明显感受到并行流的高效。
六、新时间日期API:
Java 8之前的Date和Calendar都是线程不安全的,而且使用起来比较麻烦,Java 8提供了全新的时间日期API,LocalDate(日期)、LocalTime(时间)、LocalDateTime(时间和日期) 、Instant (时间戳)、Duration(用于计算两个“时间”间隔)、Period(用于计算两个“日期”间隔)等。
1、LocalDate、LocalTime、LocalDateTime:
这三个用法一样。
//获取当前系统时间
LocalDateTime localDateTime = LocalDateTime.now();//当前时间日期
LocalDateTime localDateTime2 = localDateTime.plusYears(2);//加两年
System.out.println(localDateTime.getMonth());
System.out.println(localDateTime);
System.out.println(localDateTime2);
//指定时间
LocalDateTime localDateTime1 = LocalDateTime.of(2018,12,13,21,8);
System.out.println(localDateTime1);
2、Instant 时间戳:
时间戳就是计算机读的时间,它是以Unix元年(传统 的设定为UTC时区1970年1月1日午夜时分)开始算起的。
//计算机读的时间:时间戳(Instant),1970年1月1日0时0分0秒到此时的毫秒值
Instant instant = Instant.now();
System.out.println(instant);//默认是美国时区,8个时差
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));//加上时差
System.out.println(offsetDateTime);
System.out.println(instant.toEpochMilli());//显示毫秒值
3、Duration 和 Period:
LocalTime localTime = LocalTime.now();
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
LocalTime localTime1 = LocalTime.now();
System.out.println(Duration.between(localTime,localTime1).toMillis());
//获取两个日期之间的间隔
LocalDate localDate = LocalDate.of(2012,1,1);
LocalDate localDate1 = LocalDate.now();
Period period = Period.between(localDate,localDate1);
System.out.println(period);
System.out.println(period.getYears()+"年"+period.getMonths()+"月"+period.getDays()+"日");
4、时间校正器(TemporalAdjuster):
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime);
LocalDateTime localDateTime1 = localDateTime.withDayOfMonth(1);//localDate日期中月份的1号
System.out.println(localDateTime1);
localDateTime1.with(TemporalAdjusters.firstDayOfNextMonth());//下一个月的第一天
localDateTime.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));//下周日
5、格式化日期(.DateTimeFormatter ):
@Test
public void test6(){
//DateTimeFormatter:格式化
//使用预设格式
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME;
LocalDateTime localDateTime = LocalDateTime.now();
String str = localDateTime.format(dateTimeFormatter);
System.out.println(str);
System.out.println("==========================");
//自定义格式
DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
String str2 = localDateTime.format(dateTimeFormatter1);
//这样格式化也可以
String str3 = dateTimeFormatter1.format(localDateTime);
System.out.println(str2);
System.out.println(str3);
//退回到解析前的格式
LocalDateTime newDate = localDateTime.parse(str,dateTimeFormatter);
System.out.println(newDate);
}
6、时区的处理:
Java8 中加入了对时区的支持,带时区的时间为分别为:ZonedDate、ZonedTime、ZonedDateTime。
@Test
public void test7(){
//ZonedDate ZonedTime ZonedDateTime
LocalDateTime dateTime = LocalDateTime.now(ZoneId.of("Europe/Tallinn"));
System.out.println(dateTime);
}
七、接口中的默认方法和静态方法:
public interface MyInterface {
default String test(){
return "允许存在有具体实现的方法";
}
public static String test2(){
return "接口中还可以有静态方法";
}
}
如上所示,Java 8的接口中允许有默认方法和静态方法。如果一个类继承了一个类还实现了一个接口,而且接口中的默认方法和父类中的方法同名,这时采用类优先原则。也就是说,子类使用的是父类的方法,而不是接口中的同名方法。
八、其他新特性:
1、Optional类:
这个类是为了尽可能减少空指针异常的。就是把普通对象用Optional包起来,做了一些封装。看看其用法:
@Data
public class Man { //男人类
private Godness godness;//女神
}
@Data
public class Godness {
private String name;
public Godness(String name){
this.name = name;
}
public Godness(){
}
}
//获取男人心中的女神的名字(有的人不一定有女神,也就是说女神可能为空)
//常规做法要加很多判断
public String getGodnessName(Man man){
if (man != null){
Godness godness = man.getGodness();
if (godness != null){
return godness.getName();
}else{
return "我心中没有女神";
}
}else {
return "男人为空";
}
}
一个man类,有一个成员变量女神,女神也是一个类,有一个成员变量,名字。要获取man心中的女神,为了防止控制针异常,要做很多的判断。如果使用Optional呢?做法如下:
//新男人类
@Data
public class NewMan {
private Optional<Godness> godness = Optional.empty();
}
//使用optional后的方法
public String getGodnessName2(Optional<NewMan> man){
return man.orElse(new NewMan())
.getGodness()
.orElse(new Godness("我没有女神"))
.getName();
}
这样就简单多了。
2、重复注解与类型注解:
Java 8 可以使用重复注解和类型注解,如下图:
总结:
本文说了一些Java 8 的新特性,重点就是lambda表达式和Stream API,可以简化很多操作。肯可能还有些文中未涉及的,在此抛砖引玉,望各位大佬指点!