这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战
归纳Java8新特性。
行为参数化
行为参数化并不是Java8的新特性,行为参数化是一种类似于策略模式的设计思路,以应对不断变化的需求。
行为参数化就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
在Java8之前会用匿名类去减少啰嗦的代码,有了Java8 使用Lambda表达式即可。
所谓行为,其实指的是一类操作,即某类的方法过程。将行为作为参数,即把方法过程当作参数传递进去,实际上就是把对应的方法实现类对象当作参数,在方法体中调用该对象的方法(行为)。也可以说一段代码块表示一个行为。
当有场景是需要同一个方法做不同的行为时,可以考虑将行为参数化,即将执行过程丢到具体实例的实现方法中。
常用的新特性
1. Lambda 表达式
Lambda表达式可以理解为一种简洁的可传递匿名函数,它没有名称,但有参数、函数主体、返回类型以及可抛出的异常列表。
组成共有三部分:
(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
参数列表 箭头 Lambda 主体
有效的五种Lambda表达式有:
1. (String s) -> s.length() //定义了String的参数并返回了int类型数据,该语句没有return语句,是因为已经隐含了return
2. (Apple a) -> a.getWeight() > 150 //定义了一个Apple对象的参数并返回一个boolean
3. (int x,int y) -> { //定义了两个int参数并没有返回值(void返回),使用{}块可包含多行语句
System.out.println("shuchu");
System.out.println(x + y);
}
4. () -> 42 //定义了一个空参数并返回了一个int值
5. () -> {return "zhangsan";} //定义了一个空参数并显示使用return返回了String
注意第五种表达形式,当需要显示使用return 时,需要使用花括号{} 将主体括起来才会有效,且注意这里字符串后面是带有分号的,因为return是一个控制流语句。等同于下面这条语句
() -> {return "zhangsan";} 等同于 () -> "zhangsan"
2. 函数式接口
使用Lambda表达式需要在函数式接口中使用。而函数式表达式其实就是只定义一个抽象方法的接口,注意是只定义了一个抽象方法,可以拥有默认方法。只定义一个抽象方法的目的很明显是为了让Lambda主体是单一的行为。
我们常用的Runnable、Comparator、Callable接口都是函数式接口。函数式接口的抽象方法的签名称为函数描述符,而这其实就是Lambda表达式的签名。所谓签名就是指一个方法的参数及返回值的特征。
例如Runnable接口中的run方法,这里的签名就是 () -> {} ,表达起来就是空参数返回void的函数。又比如
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
该函数式接口抽象方法的签名为(T t) -> {return true;},即Lambda表达式的签名。利用方法签名的规范,可有效识别出该使用哪个函数数接口以及函数式接口是否使用有误。
@FunctionalInterface 注解表示该接口为函数式接口,非必要,设计目的跟@Override注解类似,有多个抽象方法时编译器会报错。
3. 常见的函数式接口
当有场景需要将行为参数化时,就可以考虑使用Lambda表达式了。但是使用Lambda表达式需要用到函数式接口。一般情况下,我们可以使用Java API库中已经有的函数式接口(java.util.function)。
| 函数式接口 | 表达式 | 拓展 | 备注 |
|---|---|---|---|
| Predicate | T -> boolean | DoublePredicate IntPredicate LongPredicate | 谓词 |
| Consumer | T -> void | DoubleConsumer IntConsumer LongConsumer | 消费 |
| Function<T, R> | T -> R | DoubleFunction DoubleToIntFunction DoubleToLongFunction IntFunction IntToDoubleFunction IntToLongFunction LongFunction LongToDoubleFunction LongToIntFunction ToDoubleFunction ToIntFunction ToLongFunction | 类型转换 |
| Supplier | () -> T | BooleanSupplier DoubleSupplier IntSupplier LongSupplier | 生产 |
| UnaryOperator | T -> T | IntUnaryOperator LongUnaryOperator | 一元运算符 |
| BinaryOperator | (T, T) -> T | DoubleBinaryOperator IntBinaryOperator LongBinaryOperator | 二元运算符 |
| BiPredicate<T, U> | (T, U) -> boolean | 扩展Predicate | |
| BiConsumer<T, U> | (T, U) -> void | ObjDoubleConsumer ObjIntConsumer ObjLongConsumer | 扩展Consumer |
| BiFunction<T, U, R> | (T, U) -> R | ToDoubleBiFunction<T, U> ToIntBiFunction<T, U> ToLongBiFunction<T, U> | 扩展Function |
4. 方法引用
方法引用是Lambda表达式的语法糖,类似于for-each是for循环的语法糖,用于简化写法。
方法引用主要有三类:
(1) 指向静态方法的方法引用(例如Integer的parseInt方法,可以写作为 Integer::parseInt)
(2) 指向任意类型实例方法的方法引用(例如String的length方法,String::length)
(3) 指向现存对象或表达式实例方法的方法引用。
特殊的也可以对构造方法做方法引用,ClassName::new
5. Stream流
流的定义是"从支持数据处理操作的源生成的元素序列"。与集合不同,流讲的是计算,而集合讲的是数据。每一次流的操作会返回一个流,多个流操作链接起来就会一条流水线。流与集合之前可以相互转换。
流与集合不同的一点在于迭代方式不同,集合中的迭代属于外部迭代(使用for-each),而Stream库中的流使用内部迭代。这两种迭代方法的差异在于,内部迭代是透明处理的。
流操作大概可分为两大类,中间操作 和 终端操作。
中间操作即是指例如filter、map等操作会返回一个Stream流,而多个流结合在一起就形成一条流水线。终端操作会从流的流水线中生成结果,其结果是任何不是流的值,比如List、Integer甚至为void(单纯输出也有可能)。在没有触发终端操作前,中间操作不会进行任何处理的。
流的使用一般包括三件事:
-
一个数据源(集合)来执行一个查询;
-
一个中间操作链,形成一条流的流水线;
-
一个终端操作,执行流水线,并能生成结果。
流的使用
常用的几个流操作方法
1. 筛选与过滤
-
filter(T->boolean)筛选; -
distinct()去重; -
limit(n)切片,最多返回前n个元素; -
skip(n)跳过元素,跳过前n个元素,返回后面的所有元素,与limit互补;
2. 元素映射
map()映射,用于提取流中的数据元素。接受一个函数作为参数;
例如,想要提取员工列表中的所有名字列表,则可以这样使用
staffList.stream().map(Staff::getName).collect(Collectors.toList());
flatMap()流的扁平化,即将多个Stram流合并为一个流。在提取时出现 List<Stream> 类的流即可考虑使用该方法将流扁平化;
3. 查找与匹配
-
anyMatch()即表示流中是否有一个元素能匹配给定的谓词; -
allMatch()与anyMatch相似,但该方法表示流中的所有元素都要符合; -
noneMatch没有一个匹配; -
findAny()返回当前流中的任意元素,返回Optional 类; -
findFirst()返回第一个元素;
4. 归约计算
归约计算即是指在对列表操作中,将列表中的元素反复结合起来,最后得到一个新值。
例如用来求列表元素的和,最大/小数、以及拼接等操作。java8 中可以使用reduce方法.
//求和,第一个参数0表示初始值,第二个参数表示元素结合在一起的操作。
int sum = numbers.stream().reduce(0,(a,b)->a+b);
//java8中新增了Integer#sum方法用于求和,也可以这样表示
int sum = numbers.stream().reduce(0,Integer::sum);
//求最大值和最小值
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
5. 数值流
Java8 引入了三个原始类型特化流 IntStream、DoubleStream、LongStream,作用是分别将元素特化为int、double、long的基本类型,避免在对数值操作中暗含的装箱操作。
数值流的出现只是为了解决装箱带来的效率问题。
-
Stream流 -> 数值流 (
mapToInt、mapToDouble、mapToLong) -
数值流-> Stream流 (
boxed)
数值流 与 Stream流相互转化的例子:
IntStream intstream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> steam = intstream.boxed();
将Stream流转化为数值流后,即可使用数值流中的方法去计算总和、最大值、最小值、平均值等计算了。
例子:
IntStream intStream = menu.stream().mapToInt(Dish::getCalories)
int sum = intStream.sum();
OptionalInt max = intStream.max();
OptionalInt min = intStream.min();
OptionalInt average = intStream.average();
因为不一定有最大值,所以需要使用Optional的特化流。
数值范围的方法
rangerangeClosed
方法有两个参数,第一个为起始值,第二个参数为结束值。两者不同的是,rangeClosed包含结束值,而range不包含,即[n,n+100) 和[n,n+100] 的区别;
利用 IntStream.rangeClosed(1,100) 可以生成1-100的数值流。
6. 收集器的操作
收集器用来将流中的数据最后转化为集合。具体可见API。常用的有Collector.toList()、Collector.groupBy();
7. Optional类
Optional 类是用来解决空指针异常问题的。由于空指针异常排查起来太困难,在已知类实例可能为空的时,最好考虑使用Optional。
新的日期和时间API
1. LocalDate、LocalTime、LocalDateTime
//LocalDate
LocalDate date = LocalDate.of(2020,11,22);
int year = date.getYear(); //年份
Month month = date.getMonth(); //月份
int day = date.getDayOfMonth(); //天数
DayOfWeek dow = date.getDayOfWeek(); //周几
boolean leap = date.isLeepYear(); //是否为闰年
LocalDate now = LocalDate.now(); //当前日期
//也可以使用ChronoField的枚举属性获取对应的年月日
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);
//LocalTime是时分秒的类,与LocalDate使用类似
LocalTime time = LocalTime.of(22,21,20); //22:21:20
int hour = time.getHour();
int minute = time.getMinute();
int second = time.getSecond();
//创建LocalDate 和LocalTime也可以这样
LocalDate date = LocalDate.parse("2020-11-22");
LocalTime time = LocalTime.parse("22:21:20");
//LocalDateTime
LocalDateTime dt1 = LocalDateTime.of(2020,Month.SEPTEMBER,21,13,45,20);
LocalDateTime dt2 = LocalDateTime.of(date,time);
//三者之间的转换
LocalDateTime dt3 = date.atTime(time);
LocalDateTime dt4 = time.atDate(date);
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();
//时间日期的操作有很多方法都是相似且通用的,例如get方法。类似Calendar的方法设计
LocalDate d1 = LocalDate.of(2020,11,22);
d1.getYear() 与 d1.get(ChronoField.YEAR)
2. InStant 时间戳
用于表示计算机的时间戳,可用来做Canlendar 和新特性LocalDateTime做转化
Instant now = Instant.now(); //当前时间戳
System.out.println(now.getEpochSecond()); // 秒
System.out.println(now.toEpochMilli()); // 毫秒
// 以指定时间戳创建Instant:
Instant ins = Instant.ofEpochSecond(1568568760);
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
3. Duration 与 Period
两个类看表示时间量或两个日期之间的差,两者之间的差异为:Period基于日期值,而Duration基于时间值。
Duration d1 = Duration.between(time1,time2);
Duration d2 = Duration.between(dateTime1,dateTime2);
Duration d3 = Duration.between(instant1,instant2);
Period p1 = Period.between(LocalDate.of(2020,11,22),LocalDate.of(2020,11,30));
//获取值
p1.getYear();
p1.getMonth();
Duration 与 Period 有很多相似的方法,例如between,from,of,parse,get等。
4. 时间操作解析
由于LocalDate等新类都是不可变的,在修改时都会创建一个副本去保存;
LocalDate date1 = LocalDate.of(2020,11,22);
LocalDate date2 = date1.plusWeeks(1); //加一周的时间,即2020-11-29
LocalDate date3 = date2.minusYears(5); //减去5年的时间,即2015-11-29
LocalDate date4 = date3.plus(6,ChronoUnit.MONTHS); //加6个月的时间,即2016-05-29
5. 使用TemporalAdjuster
TemporalAdjuster 是用来计算特殊时间的类,里面定制了一些调整时间的复杂操作。例如调整日期到下周日,下个工作日或者本月的最后一天。
LocalDate d1 = LocalDate.of(2020,11,22);
LocalDate d2 = d1.with(TemporalAdjuster.nextOrSame(DayOfWeek.SUNDAY)); //下个周日,即11-29
LocalDate d3 = d2.with(TemporalAdjuster.lastDayOfMonth());//当月的最后一天,即11-30
还有其他已定制的方法,具体查看api;
如果已定制的方法里没有想要的,可以自己实现一个。TemporalAdjuster是一个函数式接口。
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
6. 时间的格式化
在之前使用DateFormat进行日期格式化,在Java8可以使用DateTimeFormatter,是线程安全的。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
String dateStr = date1.parse(formatter);
LocalDate date2 = LocalDate.parse(dateStr,formatter);
7. 使用时区ZoneId
ZoneId是用来替换老版的TimeZone。
默认方法
Java8 中允许接口有默认方法,即可在接口中有默认方法的实现,在不用修改实现类的情况下,实现多继承。接口中的默认方法实现跟模板设计模式有些相似,都是以其他方法需要实现的方法定义好框架算法。
由于Java允许多实现,也即意味着当实现类实现了含有相同签名的多个接口时,会发生冲突,这里可以依据三种规则去判断是否冲突;
类或者父类中声明的方法,优先级要高于所有的默认方法;
如果上一条无法解决,那就选择相同函数签名最具体接口的方法;(何谓最具体的接口,即C接口实现了B,A接口,B接口也实现了A接口,B接口就是最具体的接口)
如果还不能解决,则会提示编译错误;必须在实现类上覆盖这个方法,显示选择哪个接口提供的默认方法;即下面的例子。
public class C implements B,A { void hello() { B.super.hello(); } }
最后
使用Lambda表达式可以很好的利用设计模式,利用已有的函数式接口,将行为参数化,即将函数式接口作为参数实例的类。有了Lambda就不需要再创建多个实现类了。当然对于复杂的逻辑或者需要复用,还是创建子类实现会比较好的。
记住几种常用的设计模式,策略模式、模板模式、责任链模式、观察者模式、工厂模式。