Java8新特性一览

JDK8

1、接口

Java8对接口做了进一步的增强。

  • 在接口中添加使用default关键字修饰的非抽象方法,即:默认方法。
  • 接口中可以声明静态方法,并且可以实现 。

默认方法和静态方法:

Java8 允许我们通过default关键字对接口中定义的抽象方法提供一个默认的实现。

public interface MyInterface {

// 一个普通的方法
void method(int var);

// 一个默认的方法
default void defaultMethod(int var) {
System.out.println("执行接口的 default 方法");
}

// 一个静态方法
static void staticMethod() {
System.out.println("执行接口的 static 方法");
}
}

在上面这个接口中,我们除了定义了一个抽象方法 calculate,还定义了一个带有默认实现的方法 sqrt。 我们在实现这个接口时,可以只需要实现 calculate 方法,默认方法 sqrt 可以直接调用即可,也就是说我们可以不必强制实现 sqrt 方法。

通过default关键字的特性,我们可以很方便的对之前的接口进行扩展,而此接口的实现类不必做出任何改动。

public static void main(String[] args) {
MyInterface anInterface = new MyInterface() {
@Override
public void method(int a) {
System.out.println("");
}
};
anInterface.method(0);
anInterface.defaultMethod(1);
}

接口默认方法的类优先原则:

若一个接口中定义了一个默认方法,而另外一个父类或者接口中又定义了一个同名的方法时:

  • 选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。
  • 接口冲突。如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法),那么必须覆盖该方法来解决冲突。

2、Lambda表达式

Lambda是一个匿名函数(没有函数名的函数),直接对应于其中的Lambda抽象,Lambda 表达式可以表示闭包。

可以理解为一段可以传递的代码(将代码像数据一样传递),可以写出更简洁、更灵活的代码;作为一种更紧凑的代码风格,是Java语言表达能力得到提升。

Lambda表达式允许把函数作为一个方法的参数,基本表达如下:

(parameters) -> expression 或 (parameters) -> {statements;}

(1)无参数,无返回值

例如,Runnable接口:

Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("java8之前");
}
};
Runnable r2 = () -> {
System.out.println("Lambda表达式写法");
};

(2)有一个参数,无返回值

Consumer<String> con1 = (a) -> System.out.println(a);
con1.accept("lambda");
// 小括号可以省略不写
Consumer<String> con1 = a -> System.out.println(a);
con1.accept("lambda");

(3)有两个及以上的参数,有返回值

Comparator<Integer> com1 = (a, b) -> {
System.out.println("lambda 比较");
return Integer.compare(a, b);
};
// 方法体内部只有一条语句的时候,大括号可以省略
Comparator<Integer> com2 = (a, b) -> Integer.compare(a, b);

3、函数式接口

Lambda表达式是用过函数式接口(只有一个方法的普通接口)来实现的,函数式接口可以被隐式转换为Lambda表达式,为了与普通的接口区分开,JDK1.8新增加了一种特殊的注解@FunctionalInterface 如下:

@FunctionalInterface
public interface Function<T, R> {

/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}

(1)四大核心函数式接口

函数式接口 参数类型 返回类型 用途
Consumer消费型接口 T void 对类型为T的对象应用操作:viod accept(T t)
Supplier提供型接口 T 返回类型为T的对象:T get()
Function<T, R>函数型接口 T R 对类型为T的对象应用操作,并返回结果为R类型的对象:R apply(T t)
Predicate断言型接口 T boolean 确定类型为T的对象是否满足某约束,并返回boolean值:boolean test(T t)

(2)Consumer消费型接口

Consumer<Integer> consumer = x -> System.out.println("消费型接口" + x);
consumer.accept(1);

(3)Supplier提供型接口

Supplier<Integer> supplier = () -> (int)(Math.random() * 10);
System.out.println(supplier.get());

(4)Function函数型接口

String str = "123xyz";
Function<String, String> function = s -> s.substring(1, 5);
System.out.println(function.apply(str));

(5)Predicate断言型接口

Integer age = 30;
Predicate<Integer> predicate = i -> i >= 35;
if (predicate.test(age)) {
System.out.println("退休");
} else {
System.out.println("继续加油");
}

4、引用

(1) 方法引用

若Lambda表达式中的内容已经有方法实现,则我们可以使用方法引用。

语法格式:

对象::实例方法
类::静态方法
类::实例方法

注意:Lambda表达实体中调用的方法参数列表,返回类型必须和函数式接口中抽象方法保持一致。

① 对象::实例方法

Consumer<String> con1 = s -> System.out.println(s);
con1.accept("123");

Consumer<String> con2 = System.out::println;
con2.accept("456");

② 类::静态方法

Comparator<Integer> com1 = (x, y) -> Integer.compare(x, y);
com1.compare(1, 2);
Comparator<Integer> com2 = Integer::compare;
com2.compare(3, 4);

③ 类::实例方法

BiPredicate<String, String> bp1 = (x, y) -> x.equals(y);
bp1.test("a", "b");
BiPredicate<String, String> bp2 = String::equals;
bp2.test("a", "b");

(2) 构造器引用

格式:

ClassName::New
Supplier<List> sup1 = () -> new ArrayList<>();
Supplier<List> sup2 = ArrayList::new;

(3) 数组引用

Function<Integer, String[]> fun1 = count -> new String[count];
Function<Integer, String[]> fun2 = String[]::new;

5、Stream API

(1) 什么是Stream

流(Stream)是数据渠道,是用于操作数据源(集合、数组等 )所生成的元素序列。、

简单来说,就是我们对一个包含一个或者多个元素的集合做各种操作。

注意:

  • Stream不会存储元素 。
  • Stream不会改变源对象,会返回一个持有结果的新的Stream。
  • Stream的操作是延迟执行的。需要返回结果的时候才会执行。

Stream的操作过程如下:

(1)创建Stream。一个数据源(如:数组、集合),获取一个流。

(2)中间操作。一个中间操作链,对数据源的数据进行处理。

(3)终止操作(终端操作)。一个终止操作,执行中间操作链,并产生结果。

(2) 创建

创建流的几种方式如下:

// 1、通过集合获取流        
ArrayList<Object> list = new ArrayList<>();
Stream<Object> s1 = list.stream();
// 2、通过数组获取流
String[] strings = new String[10];
Stream<String> s2 = Arrays.stream(strings);
// 3、通过Stream的静态方法获取
Stream<Integer> s3 = Stream.of(1, 2, 3);
// 4、通过迭代获取流
Stream<Integer> s4 = Stream.iterate(0, i -> i++);
// 5、生成流
Stream.generate(() -> Math.random())
.limit(5)
.forEach(System.out::println);

(3) 筛选/ 切片

中间操作:

  • filter:接收lambda,从流中排除元素。
  • limit:截断流,使其元素不超过给定数量。
  • skip(n):跳过元素,返回一个舍弃前n个元素的流;若流中元素不足n个,则返回一个空流。
  • distinct:筛选,通过流生成的hashCode()和equals()去除重复元素。
import java.util.Arrays;
import java.util.List;

public class Main {
public static void main(String[] args) {
List<Person> list = Arrays.asList(
new Person(1, "a", 20),
new Person(2, "b", 21),
new Person(3, "c", 22),
new Person(4, "d", 23),
new Person(5, "e", 24),
new Person(6, "f", 25));
list.stream()
.filter(x -> x.getAge() > 20) // 条件过滤
.limit(3) // 限制条数
.distinct() // 去除重复
.skip(1) // 跳过指定的条数
.forEach(System.out::println); // 循环打印
}
}

class Person {
private int id;
private String name;
private int age;

public Person(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}

// getter and setter
}

多个中间操作可以连接起来形成一个流水线,除非流水线上的操作触发终止操作,否则中间操作 不会 执行任何的处理。而在终止操作时一次性全部处理,称为“惰性求值”。

(4) 映射

  • map:接收lambda,将元素转换为其他形式或者提取信息;接受一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
  • flatMap:接收一个函数作为参数,将流中每一个值都转换成另一个流,然后把所有流重新连接成一个流。
List<String> list = Arrays.asList("a", "b", "c");
list.stream()
.map(str -> str.toUpperCase())
.forEach(System.out::println);
public class Main {
public static void main(String[] args) {
List<String> list = Arrays.asList("a", "b", "c");
list.stream()
.flatMap(Test::filterCharacter)
.forEach(System.out::println);
}


}

class Test {
public static Stream<Character> filterCharacter(String str) {
List<Character> list = new ArrayList<>();
for (char c : str.toCharArray()) {
list.add(c);
}
return list.stream();
}
}

(5) 排序

  • sorted():自然排序
  • sorted(Comparator c):自定义排序

(6) 查找 / 匹配

终止操作:

  • allMatch:检查是否匹配所有元素
  • anyMatch:检查是否至少匹配一个元素
  • noneMatch:检查是否没有匹配所有元素
  • findFirst:返回第一个元素
  • findAny:返回当前流中的任意元素
  • count:返回流中元素的总个数
  • max:返回流中的最大值
  • min:返回流中的最小值
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class Main {
public static void main(String[] args) {
List<Status> list = Arrays.asList(Status.FREE, Status.BUSY, Status.VOCATION);
// 匹配所有元素
System.out.println(list.stream()
.allMatch(s -> s.equals(Status.BUSY)));
// 至少匹配一个元素
System.out.println(list.stream()
.anyMatch(s -> s.equals(Status.BUSY)));
// 所有元素都不匹配
System.out.println(list.stream()
.noneMatch(s -> s.equals(Status.BUSY)));
// 返回第一个元素
Optional<Status> first = list.stream().findFirst();
// 避免空指针异常,如果Optional、为空,则找一个替换的对象
Status status = first.orElse(Status.BUSY);
System.out.println(status);

// 返回任意一个元素
Optional<Status> any = list.stream()
.findAny();
System.out.println(any);

// 返回流中元素的总个数
System.out.println(list.stream()
.count());
}
}

enum Status {
FREE, BUSY, VOCATION;
}

(7) 归约 / 收集

  • reduce(T identity, BinaryOperator) / reduce(BinaryOperator):归约,可以将流中的数据反复结合起来,得到一个值。
  • collect:收集,将流转换成其他形式;接收一个Controller接口实现,用于给流中元素做汇总的方法。
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
System.out.println(list.stream().
reduce(0, (x, y) -> x + y));
import java.util.*;
import java.util.stream.Collectors;

public class Main {
public static void main(String[] args) {
List<Person> persons = Arrays.asList(
new Person(1, "a", 20),
new Person(2, "b", 21),
new Person(3, "c", 22),
new Person(4, "d", 23),
new Person(5, "e", 24),
new Person(6, "f", 25));

// 放入list
List<String> list = persons.stream()
.map(Person::getName)
.collect(Collectors.toList());
list.forEach(System.out::println);
// 放入set
Set<String> set = persons.stream()
.map(Person::getName)
.collect(Collectors.toSet());
set.forEach(System.out::println);
// 放入LinkedHash
LinkedHashSet<String> linkedHashSet = persons.stream()
.map(Person::getName)
.collect(Collectors.toCollection(LinkedHashSet::new));
linkedHashSet.forEach(System.out::println);

// 总数
System.out.println(persons.stream()
.collect(Collectors.counting()));
// 平均值
System.out.println(persons.stream()
.collect(Collectors.averagingInt(Person::getAge)));
// 总和
System.out.println(persons.stream()
.collect(Collectors.summingInt(Person::getId)));
// 最大值
System.out.println(persons.stream()
.collect(Collectors.maxBy((a, b) -> Integer.compare(a.getAge(), b.getAge()))));
// 最小值
System.out.println(persons.stream()
.collect(Collectors.minBy((a, b) -> Integer.compare(a.getAge(), b.getAge()))));

// 分组
Map<Integer, List<Person>> map = persons.stream()
.collect(Collectors.groupingBy(Person::getId));
System.out.println(map);
// 多级分组
Map<Integer, Map<String, List<Person>>> mapMap = persons.stream()
.collect(Collectors.groupingBy(Person::getId, Collectors.groupingBy(e -> {
if (e.getAge() > 20) {
return "符合条件";
} else {
return "不符合条件";
}
})));
System.out.println(mapMap);
// 分区
Map<Boolean, List<Person>> listMap = persons.stream()
.collect(Collectors.partitioningBy(e -> e.getAge() > 21));
System.out.println(listMap);

// 总结
IntSummaryStatistics collect = persons.stream()
.collect(Collectors.summarizingInt(Person::getAge));
System.out.println(collect.getMax());
System.out.println(collect.getMin());
System.out.println(collect.getSum());
System.out.println(collect.getAverage());
System.out.println(collect.getCount());
// 连接
String connect = persons.stream()
.map(Person::getName)
.collect(Collectors.joining("-"));

}
}

class Person {
private int id;
private String name;
private int age;

public Person(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
// getter and setter
// toString
}

(8) 并行流

并行流:就是把一个内容分成几个数据块,并用不同的线程分别处理每个数据块的流。

Java8中将并行流进行了优化,我们可以很容易的对数据进行操作;Stream API可以声明性地通过parallel()与sequential()在并行流与串行流之间切换。

// 串行流(单线程):切换为并行流parallel()
// 并行流(多线程):切换为串行流sequential()
LongStream.rangeClosed(0, 10000000L)
.parallel() // 底层使用的是Fork/Join框架
.reduce(0, Long::sum);

6、Optional

Optional类是一个容器类,代表一个值存在或者不存在,原来使用null表示一个值不存在,现在可以用Optional可以更好的表达这个概念,并且可以避免空指针异常。

常用方法:

  • Optional.of(T t):创建一个Optional实例。
  • Optional'empty(T t):创建一个空的Optional实例。
  • OpTional.OfNullable(T t):若t不为null,则创建Optional实例,否则空实例。
  • isPresent():判断是否包含某值。
  • orElse(T t):如果调用对象包含值,返回该值,否则返回t。
  • orElseGet(Supplier s):如果调用对象包含值,返回该值,否则返回s获取的值。
  • map(Function f):如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty()。
  • flatmap(Function mapper):与map相似,要求返回值必须是Optional。

7、新时间日期API

(1) 安全问题

Java之前的Date时间类,存在很多的缺点 。

(1)最开始的时候,Date既要承载日期信息,又要做日期之间的转换,还要做不同日期格式的显示,职责较繁杂,从JDK1.1开始,这三项职责分开了:

  • 使用Calendar类实现日期和时间字段之间转换
  • 使用DateFormat类来格式化和分析日期字符串
  • Date只用来承载日期和时间信息

(2)对year和month的表达。

Date date = new Date(2012,1,1);
System.out.println(date);
//输出Thu Feb 01 00:00:00 CST 3912

观察输出结果,year是2012 + 1900,而month是 1+1,如果要设置日期,我们应该使用java.util.Calendar,如下:

Calendar calendar = Calendar.getInstance();
calendar.set(2013, 8, 2);
// 输出 Mon Sep 02 17:11:58 CST 2013
System.out.println(calendar.getTime());
calendar.set(2013, Calendar.AUGUST, 2);
// 输出 Fri Aug 02 17:13:53 CST 2013
System.out.println(calendar.getTime());

不过这样,month的表示还是从0开始的,除非我们直接使用枚举类型表示月份。

(3)java.util.Datejava.util.Calendar中的所有属性都是可变的。

(4)SimpleDateTimeFormat是非线程安全的。

基于上述问题,在 Java 8 对日期进行了优化,首先是将日期和时间设计为不可变类型,就像String类型一样,这样就避免了多线程下对日期的修改导致的线程安全问题,每次对日期的操作都会生成一个新的日期对象;另外还把功能进一步细化了,对日期的运算更加便利,输出也更加人性化。

(2) 本地时间 / 日期

LocalDate、LocalTime、 LocalDateTime类的实例是不可变的对象,分别表示使用ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息。也不包含与时区相关的信息。

常用方法:

方法名 返回值类型 解释
now() static ,LocalDate Time 从默认时区的系统时钟获取当前日期
of(int year, int month, int dayOfMonth, int hour, int minute, int second) static,LocalDateTime 从年月日时分秒获得LocalDATe Time的实例,将纳秒设置为零
plus(long amountToAdd, TemporalUnit unit) LocalDateTime 返回此日期时间的副本,并添加指定的数量
get(TemporaField field) int 从此日期时间获取指定字段的值为int
// 获取当前时间
System.out.println(LocalDateTime.now());
// 获取指定时间
LocalDateTime of = LocalDateTime.of(2023, 3, 27, 17, 30, 18);
System.out.println(of);
// 加 plus
System.out.println(of.plusMonths(1));
// 减 minus
System.out.println(of.minusMonths(3));
// 获取指定给的年月日时分秒
System.out.println(of.getDayOfMonth());
System.out.println(of.getHour());

(3) 时间戳

时间戳是以Unix元年1970-01-01 00:00:00到某个时间之间的毫秒数。

// 默认获取UTC时区(UTC:世界协调时间)
Instant now = Instant.now();
System.out.println(now);
// 带偏移量的时间日期(如: UTC + 8)
System.out.println(now.atOffset(ZoneOffset.ofHours(1)));
// 转换成对应的毫秒值
System.out.println(now.toEpochMilli());
// 构建时间戳
System.out.println(Instant.ofEpochSecond(60));

(4) 时间/日期差

  • Duration:计算两个时间之间的间隔
  • Period:计算两个日期之间的间隔
Instant ins1 = Instant.now();
Instant ins2 = Instant.now();
Duration d = Duration.between(ins1, ins2);
System.out.println(d.getSeconds()); // 秒
System.out.println(d.getUnits()); // 时间戳

LocalDate ld1 = LocalDate.of(2023, 9, 1);
LocalDate ld2 = LocalDate.now();
Period p = Period.between(ld2, ld1);
System.out.println(p.getYears()); // 年
System.out.println(p.toTotalMonths()); // 月

(5) 时间校正器

  • TemporalAdjuster:时间校正器。有时我们可能需要获取例如:将日期调整到下个周日等操作。
  • TemporalAdjusters:该类通过静态方法提供了大量的常用 TemporalAdjuster的实现

例如:获取下个周日

LocalDate nextSunday = LocalDate.now().with(
TemporaAdjusters.next(DayOfWeek.SUNDAY)
);

一些时间校正器的使用如下:

LocalDateTime t1 = LocalDateTime.now();
System.out.println(t1);
// 指定日期时间中的年月日
LocalDateTime t2 = t1.withDayOfMonth(10);
System.out.println(t2);
// 指定时间校正器
LocalDateTime t3 = t1.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
System.out.println(t3);
// 自定义时间矫正器
LocalDateTime t5 = t1.with(ta -> {
LocalDateTime t4 = (LocalDateTime) ta;
DayOfWeek dow = t4.getDayOfWeek();
if (dow.equals(DayOfWeek.MONDAY)) {
return t4.plusDays(6);
} else {
// 其他时间逻辑...
return t4;
}
});
System.out.println(t5);

(6) 时间格式化

  • DateTimeFormatter:格式化日期或时间
//默认格式化
DateTimeFormatter dtf1 = DateTimeFormatter.ISO_DATE_TIME;
LocalDateTime ldt1 = LocalDateTime.now();
String str1 = ldt1.format(dtf1);
System.out.println(str1);

//自定义格式化 ofPattern
DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt2 = LocalDateTime.now();
String str2 = ldt2.format(dtf2);
System.out.println(str2);

//解析
LocalDateTime newDate = LocalDateTime.parse(str1, dtf1);
System.out.println(newDate);

(7) 时区

  • ZonedDate
  • ZonedTime
  • ZonedDateTime
//查看支持的时区
Set<String> set = ZoneId.getAvailableZoneIds();
set.forEach(System.out::println);

//指定时区
LocalDateTime ldt1 = LocalDateTime.now(ZoneId.of("Europe/Tallinn"));
System.out.println(ldt1);

//在已构建好的日期时间上指定时区
LocalDateTime ldt2 = LocalDateTime.now(ZoneId.of("Europe/Tallinn"));
ZonedDateTime zdt1 = ldt2.atZone(ZoneId.of("Europe/Tallinn"));
System.out.println(zdt1);

8、Annotations注解

在Java8中,对注解的改进主要有两点:类型注解和重复注解。

(1) 类型注解

新增ElementType.TYPE_USEElementType.TYPE_PARAMETER(在Target上)

新增的两个注释的程序元素类型 ElementType.TYPE_USEElementType.TYPE_PARAMETER 用来描述注解的新场合 。
ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中。
ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中(eg:声明语句、泛型和强制转换语句中的类型)。

类型注解的作用:

类型注解被用来支持在Java的程序中做强类型检查。配合第三方插件工具Checker Framework(注:此插件so easy,这里不介绍了),可以在编译的时候检测出runtime error(eg:UnsupportedOperationException; NumberFormatException;NullPointerException异常等都是runtime error),以提高代码质量。这就是类型注解的作用。

Note:

使用Checker Framework可以找到类型注解出现的地方并检查。

import checkers.nullness.quals.*;
public class TestDemo{
void sample() {
@NonNull Object my = new Object();
}
}

编译上面的类,上面编译是通过的,但若修改代码:

@NonNull Object my = null;

但若不想使用类型注解检测出来错误,则不需要processor,正常javac TestDemo.java是可以通过编译的,但是运行时会报 NullPointerException 异常。

为了能在编译期间就自动检查出这类异常,可以通过类型注解结合 Checker Framework 提前排查出来错误异常。

注意java 5,6,7版本是不支持注解@NonNull,但checker framework 有个向下兼容的解决方案,就是将类型注解@NonNull 用/**/注释起来。

import checkers.nullness.quals.*;
public class TestDemo{
void sample() {
/*@NonNull*/ Object my = null;
}
}

这样javac编译器就会忽略掉注释块,但用checker framework里面的javac编译器同样能够检测出@NonNull错误。
通过 类型注解 + checker framework 可以在编译时就找到runtime error。

(2) 重复注解

允许在同一声明类型(类,属性,或方法)上多次使用同一个注解。

Java8以前的版本使用注解有一个限制是相同的注解在同一位置只能使用一次,不能使用多次。Java 8 引入了重复注解机制,这样相同的注解可以在同一地方使用多次。重复注解机制本身必须用 @Repeatable 注解。

实际上,重复注解不是一个语言上的改变,只是编译器层面的改动,技术层面仍然是一样的。

(1)定义一个注解:

@Repeatable(MyAnnotations.class)
// TYPE_PARAMETER 类型参数
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, TYPE_PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {

String value() default "java";
}

(2)定义一个注解容器

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotations {
MyAnnotation[] value();
}

(3)使用

@MyAnnotation("hello")
@MyAnnotation("java")
public static void show(@MyAnnotation("abc") String str) {
System.out.println(str);
}