Java8新特性
Java8中经常使用的有Lambda表达式、Stream流、日期与时间API
Lambda表达式
快速入门
- 无参数和返回值的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表达式可以简化匿名内部类的使用,使语法更加简洁。
- 有参数和返回值的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标准格式的基础上,使用省略写法的规则为:
小括号内参数的类型可以省略
如果小括号内有且仅有一个参数,则小括号可以省略
如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
使用Lambda的前提
Lambda表达式不是随便使用的,使用时有几个条件要特别注意:
方法的参数或局部变量类型必须为接口才能使用Lambda
接口中有且仅有一个抽象方法
函数式接口
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
FunctionalInterface注解
与@Override
注解的作用类似,Java8中专门为函数式接口引入了一个新的注解:@FunctionalInterface
。该注解可用于一个接口的定义上:
@FunctionalInterface public interface Operator { void myMethod(); }
|
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
常用的函数式接口
Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。
- Supplier接口
java.util.function.Supplier接口,它意味着”供给”,对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
@FunctionalInterface public interface Supplier<T> { public abstract T get(); }
|
- Consumer接口
消费一个数据,其数据类型由泛型参数决定。
@FunctionalInterface public interface Consumer<T> { public abstract void accept(T t); }
|
- Function接口
java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。
@FunctionalInterface public interface Function<T, R> { public abstract R apply(T t); }
|
- 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中使用方式相当灵活,有以下几种形式:
instanceName::methodName
对象::方法名
ClassName::staticMethodName
类名::静态方法
ClassName::methodName
类名::普通方法
ClassName::new
类名::new 调用的构造器
TypeName[]::new String[]::new
调用数组的构造器
常用格式解析
- 对象::方法名
这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法
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()); }
|
方法引用的注意事项
- 类名::静态方法
由于在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()); }
|
- 类名::普通方法
由于构造器的名称与类名完全一样。所以构造器引用使用类名称::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)); }
|
- 类名::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) { 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<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<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常用方法
方法名 |
方法作用 |
返回值类型 |
方法种类 |
count |
统计个数 |
long |
终结 |
forEach |
逐一处理 |
void |
终结 |
filter |
过滤 |
Stream |
函数拼接 |
limit |
取用前几个 |
Stream |
函数拼接 |
skip |
跳过前几个 |
Stream |
函数拼接 |
map |
映射 |
Stream |
函数拼接 |
concat |
组合 |
Stream |
函数拼接 |
注意:
Stream只能操作一次
Stream方法返回的是新的流
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) .noneMatch(e -> e < 0); 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"); 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() .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()) { 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); 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方法会创建对象的一个副本,并按照需要修改它的属性。以下所有的方法都返回了一个修改属性的对象,他们不会影响原来的对象。
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() { LocalDate now = LocalDate.now(); LocalDate date = LocalDate.of(2018, 8, 8); System.out.println(now.isBefore(date)); System.out.println(now.isAfter(date)); }
|
时间格式化与解析
通过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); }
|