Java8新特性

Java8中经常使用的有Lambda表达式、Stream流、日期与时间API

Lambda表达式

快速入门

  1. 无参数和返回值的Lambda

需要启动一个线程去完成某些任务时,通常会通过Runnable接口来定义任务内容,使用Thread类启动线程。

传统写法为:

public class Test{
public static void main(String[] args){
new Thread(new Runnable() (
@Override
public void run(){
//...
System.out.println("新线程任务执行");
}
}).start();
}
}

Lambda是一个匿名函数,可以理解为一段可以传递的代码。使用Java8的写法,上述写法可以重写为:

public class Test{
public static void main(String[] args){
new Thread(() -> {
//...
System.out.println("新线程任务执行");
}).start();
}
}

使用Lambda表达式可以简化匿名内部类的使用,使语法更加简洁。

  1. 有参数和返回值的Lambda

当需要对一个对象集合进行排序时,传统写法:

public class Person {
private String name;
private int age;
private int height;
}

public class Test {
public static void main(String[] args) {
ArrayList<Person> persons; // 假设里面已经有很多数据
Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
});
}
}

使用Lambda后的写法:

public class Person {
private String name;
private int age;
private int height;
}

public class Test {
public static void main(String[] args) {
ArrayList<Person> persons; // 假设里面已经有很多数据
Collections.sort(persons, (o1, o2) -> {o1.getAge() - o2.getAge()});
}
}

Lambda标准格式

Lambda由3部分组成:

(参数类型 参数名称) -> {
代码体;
}

Lambda省略格式

在Lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略

  2. 如果小括号内有且仅有一个参数,则小括号可以省略

  3. 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号

使用Lambda的前提

Lambda表达式不是随便使用的,使用时有几个条件要特别注意:

  1. 方法的参数或局部变量类型必须为接口才能使用Lambda

  2. 接口中有且仅有一个抽象方法

函数式接口

函数式接口在Java中是指:有且仅有一个抽象方法的接口

函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

FunctionalInterface注解

@Override注解的作用类似,Java8中专门为函数式接口引入了一个新的注解:@FunctionalInterface。该注解可用于一个接口的定义上:

@FunctionalInterface
public interface Operator {
void myMethod();
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

常用的函数式接口

Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。

  1. Supplier接口

java.util.function.Supplier接口,它意味着”供给”,对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

@FunctionalInterface
public interface Supplier<T> {
public abstract T get();
}
  1. Consumer接口

消费一个数据,其数据类型由泛型参数决定。

@FunctionalInterface
public interface Consumer<T> {
public abstract void accept(T t);
}
  1. Function接口

java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。

@FunctionalInterface
public interface Function<T, R> {
public abstract R apply(T t);
}
  1. Predicate接口

java.util.function.Predicate 接口用于做判断,返回boolean类型的值,有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。

@FunctionalInterface
public interface Predicate<T> {
public abstract boolean test(T t);
}

方法引用

如果在Lambda中所指定的功能,已经有其他方法存在相同方案,则没有必要再写重复逻辑,可以直接“引用”过去

public class Test {
public static void getMax(int[] arr) {
int sum = 0;
for (int n: arr) {
sum += n;
}
System.out.println(sum);
}

public static void main(String[] args) {
printMax(Test::getMax); // 方法引用
}

private static void printMax(Consumer<int[]> consumer) {
int[] arr = {10, 20, 30, 40, 50};
consumer.accept(arr);
}
}
方法引用的格式

符号表示::

符号说明: 双冒号为方法引用运算符,而它所在的表达式被称为方法引用

应用场景: 如果Lambda所要实现的方案, 已经有其他方法存在相同方案,那么则可以使用方法引用

常用引用方式

方法引用在JDK8中使用方式相当灵活,有以下几种形式:

  1. instanceName::methodName 对象::方法名

  2. ClassName::staticMethodName 类名::静态方法

  3. ClassName::methodName 类名::普通方法

  4. ClassName::new 类名::new 调用的构造器

  5. TypeName[]::new String[]::new 调用数组的构造器

常用格式解析
  1. 对象::方法名

这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法

public void test() {
Date now = new Date();
Supplier<Long> supp = () -> {
return now.getTime();
};
System.out.println(supp.get());

Supplier<Long> supp2 = now::getTime;
System.out.println(supp2.get());
}

方法引用的注意事项

  • 被引用的方法,参数要和接口中抽象方法的参数一样

  • 当接口抽象方法有返回值时,被引用的方法也必须有返回值

  1. 类名::静态方法

由于在java.lang.System类中已经存在了静态方法currentTimeMillis,要通过Lambda来调用该方法时,可以使用方法引用

public void test() {
Supplier<Long> supp = () -> {
return System.currentTimeMillis();
};
System.out.println(supp.get());

Supplier<Long> supp2 = System::currentTimeMillis;
System.out.println(supp2.get());
}
  1. 类名::普通方法

由于构造器的名称与类名完全一样。所以构造器引用使用类名称::new的格式表示。

首先是一个Person类:

@Data
public class Person {
private String name;
private Integer age;

public Person(){}

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

要使用这个函数式接口,可以通过方法引用传递:

public void test() {
Supplier<Person> sup = () -> {
return new Person();
};
System.out.println(sup.get());

Supplier<Person> sup2 = Person::new;
System.out.println(sup2.get());

BiFunction<String, Integer, Person> fun2 = Person::new;
System.out.println(fun2.apply("张三", 18));
}
  1. 类名::new调用的构造器
public void test() {
Function<Integer, String[]> fun = (len) -> {
return new String[len];
};

String[] arr1 = fun.apply(10);
System.out.println(arr1 + ", " + arr1.length);

Function<Integer, String[]> fun2 = String[]::new;
String[] arr2 = fun.apply(5);
System.out.println(arr2 + ", " + arr2.length);
}

Stream流

Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。

Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。

获取Stream流

根据Collection获取流

java.util.Collection接口中加入了default stream方法用来获取流,所以其所有实现类均可获取流。

public interface Collection {
default Stream<E> stream()
}
public class Test {
public static void main(String[] args) {
// 集合获取流
// Collection接口中的方法: default Stream<E> stream() 获取流
List<String> list = new ArrayList<>();
// ...
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
// ...
Stream<String> stream2 = set.stream();
Vector<String> vector = new Vector<>();
// ...
Stream<String> stream3 = vector.stream();
}
}

Map不是 Collection的子接口,所以获取对应的流需要分key、value或entry等情况:

public class Test {
public static void main(String[] args) {
// Map获取流
Map<String, String> map = new HashMap<>();
// ...
Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
}
}

Stream中的静态方法of获取流

由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单:

public class Test {
public static void main(String[] args) {
// Stream中的静态方法: static Stream of(T... values)
Stream<String> stream6 = Stream.of("aa", "bb", "cc");
String[] arr = {"aa", "bb", "cc"};
Stream<String> stream7 = Stream.of(arr);
Integer[] arr2 = {11, 22, 33};
Stream<Integer> stream8 = Stream.of(arr2);
}
}

Stream常用方法

  • 终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。

  • 非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。

方法名 方法作用 返回值类型 方法种类
count 统计个数 long 终结
forEach 逐一处理 void 终结
filter 过滤 Stream 函数拼接
limit 取用前几个 Stream 函数拼接
skip 跳过前几个 Stream 函数拼接
map 映射 Stream 函数拼接
concat 组合 Stream 函数拼接

注意:

  1. Stream只能操作一次

  2. Stream方法返回的是新的流

  3. Stream不调用终结方法,中间的操作不会执行

count方法

count方法可以统计其中的元素个数,该方法返回一个long值代表元素个数。

public void testCount() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "a", "b", "c", "d");
System.out.println(one.stream().count());
}

forEach方法

forEach用来遍历流中的数据,该方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理

public void testForEach() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "a", "b", "c", "d");
one.stream().forEach(System.out::println);
}

Filter方法

filter用于过滤数据,返回符合过滤条件的数据,可以将一个流转换成另一个子集流。需要接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。

public void testFilter() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "a", "b", "c", "d");
one.stream().filter(s -> s.length() == 2).forEach(System.out::println);
}

limit方法

limit方法可以对流进行截取,只取用前n个。参数是一个long型,如果集合当前长度大于参数则进行截取。否则不进行操作。

public void testLimit() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "a", "b", "c", "d");
one.stream().limit(3).forEach(System.out::println);
}

skip方法

如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流。如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。

public void testSkip() {
List<String> one = new ArrayList<>();
Collections.addAll(one, "a", "b", "c", "d");
one.stream().skip(2).forEach(System.out::println);
}

map方法

map方法可以将流中的元素映射到另一个流中,该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

@Test
public void testMap() {
Stream<String> original = Stream.of("11", "22", "33");
Stream<Integer> result = original.map(Integer::parseInt);
result.forEach(s -> System.out.println(s + 10));
}

distinct方法

distinct方法可以去除重复数据。

public void testDistinct() {
Stream.of(22, 33, 22, 11, 33)
.distinct()
.forEach(System.out::println);
}

match方法

如果需要判断数据是否匹配指定的条件,可以使用 Match 相关方法。

public void testMatch() {
boolean b = Stream.of(5, 3, 6, 1)
// .allMatch(e -> e > 0); // allMatch: 元素是否全部满足条件
// .anyMatch(e -> e > 5); // anyMatch: 元素是否任意有一个满足条件
.noneMatch(e -> e < 0); // noneMatch: 元素是否全部不满足条件
System.out.println("b = " + b);
}

concat方法

有两个流需要合并成为一个流,可以使用 Stream 接口的静态方法 concat

public void testContact() {
Stream<String> streamA = Stream.of("张三");
Stream<String> streamB = Stream.of("李四");
Stream<String> result = Stream.concat(streamA, streamB);
result.forEach(System.out::println);
}

Stream流中的结果到集合中

Stream流提供collect方法,其参数需要一个java.util.stream.Collector<T, A, R>接口对象来指定收集到哪种集合中。java.util.stream.Collectors类提供一些方法,可以作为Collector接口的实例:

public static <T> Collector<T, ?, List<T>> toList():转换为 List 集合。

public static <T> Collector<T, ?, Set<T>> toSet():转换为 Set 集合

public void testStreamToCollection() {
Stream<String> stream = Stream.of("aa", "bb", "cc");
// List<String> list = stream.collect(Collectors.toList());
// Set<String> set = stream.collect(Collectors.toSet());
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));
}

Stream流中的结果到数组中

Stream提供toArray方法来将结果放到一个数组中,返回值类型为Object[]

public void testStreamToArray() {
Stream<String> stream = Stream.of("aa", "bb", "cc");
String[] strings = stream.toArray(String[]::new);
for (String str : strings) {
System.out.println(str);
}
}

对流中数据进行拼接

Collectors.joining会根据指定的连接符,将所有元素连接成一个字符串。

public void testJoining() {
Stream<Student> studentStream = Stream.of(
new Student("a", 52, 95),
new Student("b", 56, 88),
new Student("c", 56, 99),
new Student("d", 52, 77));

String collect = studentStream
.map(Student::getName)
.collect(Collectors.joining(">_<", "^_^", "^v^"));

System.out.println(collect);
}

并行的Stream流

parallelStream是一个并行执行的流。它通过默认的ForkJoinPool,可能提高多线程任务的速度。

public void testgetParallelStream() {
ArrayList<Integer> list = new ArrayList<>();
// 直接获取并行的流
Stream<Integer> stream1 = list.parallelStream();
// 将串行流转成并行流
Stream<Integer> stream2 = list.stream().parallel();
}

并行操作代码:

public void test0Parallel() {
long count = Stream.of(4, 5, 3, 9, 1, 2, 6)
.parallel() // 将流转成并发流,Stream处理的时候将才去
.filter(s -> {
System.out.println(Thread.currentThread() + ", s = " + s);
return true;
})
.count();
System.out.println("count = " + count);
}

Optional类

Optional是一个没有子类的工具类,是一个可以为null的容器对象。它的作用主要就是为了解决避免Null检查,防止NullPointerException。

基本使用

创建方式:

Optional.of(T t): 创建一个Optional实例

Optional.empty(): 创建一个空的Optional实例

Optional.ofNullable(T t):若t不为null,创建Optional实例,否则创建空实例

常用方法:

isPresent(): 判断是否包含值,包含值返回true,不包含值返回false

get(): 如果Optional有值则将其返回,否则抛出NoSuchElementException

orElse(T t): 如果调用对象包含值,返回该值,否则返回参数t

orElseGet(Supplier s):如果调用对象包含值,返回该值,否则返回s获取的值

map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty()

示例:

public void test() {
Optional<String> userName = Optional.empty();
if (userNameO.isPresent()) {
// get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException。
String name = userName.get();
System.out.println("用户名为:" + name);
} else {
System.out.println("用户名不存在");
}
}

日期和时间API

JDK 8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于java.time包中,下面是一些关键类。

LocalDate:表示日期,包含年月日,格式为 2019-10-16

LocalTime:表示时间,包含时分秒,格式为 16:38:54.158549300

LocalDateTime:表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750

DateTimeFormatter:日期时间格式化类。

Instant:时间戳,表示一个特定的时间瞬间。

Duration:用于计算2个时间(LocalTime,时分秒)的距离

Period:用于计算2个日期(LocalDate,年月日)的距离

ZonedDateTime:包含时区的时间

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

示例

public void test01() {
// 创建指定日期
LocalDate fj = LocalDate.of(1970, 1, 1);
System.out.println("fj = " + fj); // 1985-09-23
// 得到当前日期
LocalDate nowDate = LocalDate.now();
System.out.println("nowDate = " + nowDate);
// 获取日期信息
System.out.println("年: " + nowDate.getYear());
System.out.println("月: " + nowDate.getMonthValue());
System.out.println("日: " + nowDate.getDayOfMonth());
System.out.println("星期: " + nowDate.getDayOfWeek());
}


public void test02() {
// 得到指定的时间
LocalTime time = LocalTime.of(12,15, 28, 129_900_000);
System.out.println("time = " + time);
// 得到当前时间
LocalTime nowTime = LocalTime.now();
System.out.println("nowTime = " + nowTime);
// 获取时间信息
System.out.println("小时: " + nowTime.getHour());
System.out.println("分钟: " + nowTime.getMinute());
System.out.println("秒: " + nowTime.getSecond());
System.out.println("纳秒: " + nowTime.getNano());
}


public void test03() {
LocalDateTime fj = LocalDateTime.of(1970, 1, 1, 9, 10, 20);
System.out.println("fj = " + fj);
// 得到当前日期时间
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now);
System.out.println(now.getYear());
System.out.println(now.getMonthValue());
System.out.println(now.getDayOfMonth());
System.out.println(now.getHour());
System.out.println(now.getMinute());
System.out.println(now.getSecond());
System.out.println(now.getNano());
}

对日期时间的修改,对已存在的LocalDate对象,创建它的修改版,最简单的方式是使用withAttribute方法。withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。以下所有的方法都返回了一个修改属性的对象,他们不会影响原来的对象。

// LocalDateTime类: 对日期时间的修改
public void test05() {
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now);
// 修改日期时间
LocalDateTime setYear = now.withYear(2077);
System.out.println("修改年份: " + setYear);
System.out.println("now == setYear: " + (now == setYear));
System.out.println("修改月份: " + now.withMonth(6));
System.out.println("修改小时: " + now.withHour(9));
System.out.println("修改分钟: " + now.withMinute(11));
// 再当前对象的基础上加上或减去指定的时间
LocalDateTime localDateTime = now.plusDays(5);
System.out.println("5天后: " + localDateTime);
System.out.println("now == localDateTime: " + (now == localDateTime));
System.out.println("10年后: " + now.plusYears(10));
System.out.println("20月后: " + now.plusMonths(20));
System.out.println("20年前: " + now.minusYears(20));
System.out.println("5月前: " + now.minusMonths(5));
System.out.println("100天前: " + now.minusDays(100));
}

日期的比较

public void test06() {
// 在JDK8中,LocalDate类中使用isBefore()、isAfter()、equals()方法来比较两个日期,可直接进行比较。
LocalDate now = LocalDate.now();
LocalDate date = LocalDate.of(2018, 8, 8);
System.out.println(now.isBefore(date)); // false
System.out.println(now.isAfter(date)); // true
}

时间格式化与解析

通过java.time.format.DateTimeFormatter类可以进行日期时间解析与格式化。

public void test04() {
// 得到当前日期时间
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 将日期时间格式化为字符串
String format = now.format(formatter);
System.out.println("format = " + format);
// 将字符串解析为日期时间
LocalDateTime parse = LocalDateTime.parse("1970-01-01 12:12:12", formatter);
System.out.println("parse = " + parse);
}