现在我们在使用Java编程时,大多数使用的Java版本为 jdk1.8 可是你真的会使用 jdk1.8 么?你对 Java8 的新特性又了解多少呢?你是否还在用 jdk1.6 的写法来使用 jdk1.8 呢?本文将为你详细介绍 Java8 的新特性。
Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本。Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,它支持函数式编程,新的 JavaScript 引擎,新的日期 API,新的Stream API 等。Java8 的主要特性如下
-
Lambda 表达式
-
接口默认方法
-
方法引用
-
函数式接口
-
Stream API
-
Date Time API
-
Optional 类
一、Lambda表达式
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。它允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用 Lambda 表达式可以使代码变得更加简洁紧凑。
其语法格式如下
(params) -> { statements; }
// 当只有一个参数时可简化为
param -> { tatements; }
// 当tatements只有一句表达式时可简化为
param -> tatements
// 当没有参数时可写为
() -> expression
在最简单的形式中,一个lambda可以由用逗号分隔的参数列表、-> 符号与函数体三部分表示。简单示例
() -> return true // 直接返回true
(x) -> return x * x // 返回 x 的平方
(x, y) -> return x + y // 返回 x+y
(x, y) -> x + y // 返回 x+y
(s) -> System.out.println(s) // 不返回 直接打印
由示例可以看出,
- Lambda表达式并不需要显式声明形参类型,它会自动推断形参列表的类型。
- 如果没有参数,只需保留 () 即可
- 当方法体只有一句时,可省略 {}
- 若有返回值并想省去{},则必须同时省略return,且执行语句也只能有1句
注意:Lambda表达式若使用了局部变量,则局部变量必须是final关键字修饰的,若是局部变量没有加final关键字,jvm会默认添加,并且此后再修改该局部变量,会编译报错。
二、接口默认方法
我们都知道在Java8之前,接口中只能包含抽象方法。那么这样有什么局限呢?让我们假设这样一个场景:我们之前已经写好了一个接口 Animal,其中包含了 吃饭、喝水两个抽象方法,并且已有实现类Dog、Cat、Pig等多个实现类实现了Animal接口,这时如果 Animal 接口中增加了一个 walk方法,那么岂不是所有的实现类都要重新实现该方法?Java8 的默认方法就解决了这一问题。
public interface Animal {
// 抽象方法
void sayHello(String name);
// 默认方法
default void eat(String name){
System.out.println(name + " is eating");
}
}
public class Dog implements Animal {
@Override
public void sayHello(String name) {
System.out.println("Hello " + name);
}
public static void main(String args[]) {
Dog dog = new Dog();
dog.sayHello("coder的自我修养");
dog.eat("Dog");
}
}
// 以上结果输出
Hello coder的自我修养
Dog is eating
Java8接口中除了可以写默认方法外,还可以写静态方法。在Animal接口中增加呼吸的静态方法。
static void breath(){
System.out.println("我正在呼吸");
}
// 直接调用
Animal.breath();
// 输出
我正在呼吸
三、方法引用
方法引用能够进一步简化lambda表达式,通过类名或者实例名与方法名的组合来直接访问到类或者实例已经存在的方法或者构造方法。方法引用使用 :: 来定义, :: 的前半部分表示类名或者实例名,后半部分表示方法名,如果是构造方法就使用 NEW 来表示。
我们来修改 Dog 类,如下
public class Dog implements Animal {
private String name;
@Override
public void sayHello(String name) {
System.out.println("Hello " + name);
}
public static void main(String args[]) {
List<Dog> list = new ArrayList<>();
Dog dog1 = new Dog();
dog1.setName("旺财");
Dog dog2 = new Dog();
dog2.setName("大黄");
list.add(dog1);
list.add(dog2);
list.forEach(Dog::getName);
}
public void getName(){
System.out.println("我是" + this.name);
}
public void setName(String name){
this.name = name;
}
}
// 输出
我是旺财
我是大黄
方法引用可以看作Lambda表达式的更简洁的一种表达形式,方法引用主要有三类:
指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt);
指向任意类型实例方法的方法引用(例如String的length方法,写作String::length);
指向对象的实例方法的方法引用(例如 instanceName::methodName )。
四、函数式接口
1、什么是函数式接口**所谓的函数式接口就是
只有一个抽象方法的接口,注意这里说的是抽象方法,因为Java8 中加入了默认方法的特性,可以有一个或多个抽象方法。数式接口可以使用 @FunctionalInterface
注解,声明这个接口是函数式接口。如果一个接口满足函数式接口的定义,会默认转换成函数式接口。但是,最好是使用@FunctionalInterface
注解显式声明,防止开发人员在该接口中加入其他方法。
@FunctionalInterface
public interface Animal {
// 抽象方法
void sayHello(String name);
default void eat(String name){
System.out.println(name + " is eating");
}
static void breath(){
System.out.println("我正在呼吸");
}
}
当我们再加入一个抽象方法 sayHi() 时,编译器就会报错
2、为什么要使用函数式接口
函数式接口的使用,使得我们可以将方法当作参数来传递。其共有四种核心类型。
- 功能性接口:Function<T,R>
- 断言性接口:Predicate<T>
- 供给性接口:Supplier<T>
- 消费性接口:Consumer<T>
接口 | 参数 | 返回 | 用途 |
---|---|---|---|
Consumer | T | void | 对类型T参数操作,无返回结果,包含方法 void accept(T t) |
Supplier | 无 | T | 返回T类型参数,方法是 T get() |
Function | T | R | 对类型T参数操作,返回R类型参数,包含方法 R apply(T t) |
Predicate | T | boolean | 断言型接口,对类型T进行条件筛选操作,返回boolean,包含方法 boolean test(T t) |
简单示例如下
public class TestDemo {
public static void main(String args[]) {
// Function
String str = upperStr("coder的自我修养",(s)-> s.toUpperCase());
System.out.println(str);
// Predicate
List<String> list= Arrays.asList("hello","nihao","hi");
List<String> strings=filterStr(list,(s)->s.startsWith("h"));
System.out.println(strings);
// Consumer
printStr("coder的自我修养",(s)-> System.out.println("Hello," + s));
// Supplier
String rtn = getStr(() -> "coder的自我修养");
System.out.println(rtn);
}
public static String upperStr(String str,Function<String,String> function){
return function.apply(str);
}
public static List<String> filterStr(List<String> list, Predicate<String> predicate){
List<String> strings=new ArrayList<>();
for (String string : list) {
if(predicate.test(string)){
strings.add(string);
}
}
return strings;
}
public static void printStr(String name, Consumer<String> consumer){
consumer.accept(name);
}
public static String getStr(Supplier<String> supplier){
return supplier.get();
}
}
五、Stream API
Java8中有一种新的数据处理方式,那就是流Stream,结合lambda表达式能够更加简洁高效的处理数据。Stream使用一种类似于SQL语句从数据库查询数据的直观方式,对数据进行如筛选、排序以及聚合等多种操作。
Stream API极大简化了集合框架的处理,让开发者出高效率、干净、简洁的代码。
1、什么是Stream
Stream是一个来自数据源的元素队列并支持聚合操作,更像是一个更高版本的Iterator。使用Stream,只需要指定什么操作,Stream会内部遍历并完成指定操作。
2、Stream 数据源
所谓数据源,就是Stream的来源,可以是集合、数组、I/O channel等转换而成的Stream。常见的有一下几种:
-
Collection.stream()
-
Arrays.stream(T array)
-
Collection.parallelStream()
-
Stream.of()
-
Stream.generate(Supplier s)
-
Stream.iterate(T seed, UnaryOperator f)
-
JarFile.stream()
下面简单介绍一下生成示例
String[] strArr = new String[]{"coder", "的", "自我修养"};
// Arrays.stream(T array)
Stream<String> stream1 = Arrays.stream(strArr);
// 转为 List, Collection.stream()
List<String> list = Arrays.asList(strArr);
Stream<String> stream2 = list.stream();
// Stream.of()
Stream<String> stream3 = Stream.of(strArr);
// Stream.generate(Supplier s)
Stream<Double> stream4 = Stream.generate(Math::random);
// Stream.iterate(T seed, UnaryOperator f)
Stream.iterate(1, i -> i++);
3、Stream 常见操作
我们还以Dog类为例
过滤操作
List<Dog> list = new ArrayList<>();
Dog dog1 = new Dog();
dog1.setName("旺财");
dog1.setColor("黑色");
dog1.setAge(5);
Dog dog2 = new Dog();
dog2.setName("大黄");
dog2.setColor("黑色");
dog2.setAge(4);
Dog dog3 = new Dog();
dog3.setName("小黄");
dog3.setColor("白色");
dog3.setAge(6);
list.add(dog1);
list.add(dog2);
list.add(dog3);
List<Dog> filterList = list.stream().filter(d -> d.getColor() == "黑色").collect(Collectors.toList());
filterList.forEach(Dog::getName);
// 输出
我是旺财
我是大黄
去重操作
List<String> colors = list.stream().map(Dog::getColor).collect(Collectors.toList());
colors.stream().distinct().forEach(System.out::println);
// 输出
黑色
白色
排序操作
Stream<Integer> stream = Stream.of(3, 8, 5, 7, 4);
stream.sorted(Integer::compareTo).forEach(System.out::println);
// 输出
3
4
5
7
8
分组操作
Map<String,List<Dog>> groupMap = list.stream().collect(Collectors.groupingBy(Dog::getColor));
System.out.println(JSON.toJSONString(groupMap));
// 输出
{"黑色":[{"color":"黑色"},{"color":"黑色"}],"白色":[{"color":"白色"}]}
List 转 Map
/**
* 需要注意的是:toMap 如果集合对象有重复的key,会报错Duplicate key ....
* dog1,dog2的 color 都为黑色。
* 可以用 (k1,k2)->k1 来设置,如果有重复的key,则保留key1,舍弃key2
*/
Map<String, Dog> dogMap = list.stream().collect(Collectors.toMap(Dog::getColor, a -> a,(k1,k2)->k1));
System.out.println(JSON.toJSONString(dogMap));
// 输出
{"黑色":{"color":"黑色"},"白色":{"color":"白色"}}
求和
Integer totalAge = list.stream().mapToInt(Dog::getAge).sum();
System.out.println("totalAge:"+totalAge);
// 输出
totalAge:15
查找最大值、最小值
Optional<Dog> maxAge = list.stream().max(Comparator.comparing(Dog::getAge));
maxAge.ifPresent(dog -> System.out.println("max age "+dog.getAge()));
Optional<Dog> minAge = list.stream().min(Comparator.comparing(Dog::getAge));
minAge.ifPresent(dog -> System.out.println("min age "+dog.getAge()));
// 输出
max age 6
min age 4
六、Date Time API
Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。在Java8之前的版本中,日期时间API存在很多的问题,比如线程安全、时区处理麻烦等。Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影响,并且吸取了其精髓。新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。在设计新版API时,十分注重与旧版API的兼容性:不允许有任何的改变(从java.util.Calendar中得到的深刻教训)。如果需要修改,会返回这个类的一个新实例。以下为简单使用示例
// 获取当前的日期时间
LocalDateTime currentTime = LocalDateTime.now();
System.out.println("当前时间: " + currentTime);
LocalDate date1 = currentTime.toLocalDate();
System.out.println("date1: " + date1);
int year = currentTime.getYear();
Month month = currentTime.getMonth();
int m = month.getValue();
int day = currentTime.getDayOfMonth();
int hour = currentTime.getHour();
int minute = currentTime.getMinute();
int seconds = currentTime.getSecond();
System.out.println(year + "年" + m + "月" + day + "日" + hour + "时" + minute + "分" + seconds + "秒");
LocalDateTime date2 = currentTime.withDayOfMonth(15).withYear(2019);
System.out.println("date2: " + date2);
// 14 december 2019
LocalDate date3 = LocalDate.of(2019, Month.DECEMBER, 14);
System.out.println("date3: " + date3);
// 22 小时 15 分钟
LocalTime date4 = LocalTime.of(22, 15);
System.out.println("date4: " + date4);
// 解析字符串
LocalTime date5 = LocalTime.parse("14:15:36");
System.out.println("date5: " + date5);
// 输出结果
当前时间: 2019-11-19T14:47:54.155
date1: 2019-11-19
2019年11月19日14时47分54秒
date2: 2019-11-15T14:47:54.155
date3: 2019-12-14
date4: 22:15
date5: 14:15:36
七、Optional 类
在Java8之前,我们为了避免控制在异常,经常会使用if-else来判断是否为null,然后再进行下一步操作,如果一个对象的属性层级很多,那么代码写出来可能就会像下面这样。
if(a.getB() != null){
B b = a.getB();
if(b.getC() != null){
C c = b.getC();
if(c.getD() != null){
D d = c.getD();
if(d.getE() != null){
...
}
}
}
}
上面的代码使用了大段的判断逻辑避免空指针问题,这让我们的代码的可读性变得很差。那么还有更好的方法解决这个问题吗?答案是肯定的,我们可以使用Java8提供Optional免去代码中大段的判断逻辑,让代码变的更加简洁。
@Data
public class A {
private B b;
public Optional<B> getB() {
return Optional.ofNullable(b);
}
}
@Data
public class B {
private C c;
public Optional<C> getC() {
return Optional.ofNullable(c);
}
}
@Data
public class C {
private D d;
public Optional<D> getD() {
return Optional.ofNullable(d);
}
}
@Data
public class D {
private E e;
public Optional<E> getE() {
return Optional.ofNullable(e);
}
}
@Data
public class E {
private String name;
public Optional<String> getName() {
return Optional.ofNullable(name);
}
}
System.out.println(getEName(null));
static String getEName(A a) {
return Optional.ofNullable(a)
.flatMap(A::getB)
.flatMap(B::getC)
.flatMap(C::getD)
.flatMap(D::getE)
.flatMap(E::getName)
.orElse("null test");
}
// 输出
null test
上面的代码中我们并没有进行任何显式判断空值的操作,却仍然达到了避免空指针的目的,并且让代码看起来更加的优雅。那么如何创建呢?有以下几种方式
- Optional.empty():创建一个空的Optional对象。
- Optional of(T value):创建不为null的Optional对象,否则将会抛出空指针异常。
- Optional ofNullable(T value):创建一个允许null值的Optional对象。
Optional<Object> op1 = Optional.empty();
System.out.println(op1);
// 调用empty()方法创建Optional对象时,可以指定类型
Optional<Dog> empty = Optional.<Dog>empty();
System.out.println(empty);
Optional<Dog> op2 = Optional.of(new Dog());
System.out.println(op2);
// 报错:java.lang.NullPointerException
// Optional<Object> op3 = Optional.of(null);
Optional<Object> op4 = Optional.ofNullable(null);
System.out.println(op4);
// 输出
Optional.empty
Optional.empty
Optional[com.example.demo.test.Dog@544a2ea6]
Optional.empty
我们得到一个Optional对象,应该怎么获取它的值呢?有以下几种方法
- get():最简单,但若Optional对象无值会抛出NoSuchElementException异常。
- orElse():参数为一个实例对象,若Optional对象有值返回值,无值返回该对象。
- orElseGet():将Supplier接口作为参数,若Optional对象有值返回值,无值返回调用Supplier生成的实例。
- orElseThrow():将Supplier接口作为参数,若Optional对象有值返回值,无值抛出调用Supplier的生成的异常。
简单示例
Optional<Dog> op = Optional.<Dog>ofNullable(null);
Dog dog4 = op.orElse(new Dog());
System.out.println(dog4);
Dog dog5 = op.orElseGet(Dog::new);
System.out.println(dog5);
// 输出
com.example.demo.test.Dog@2e3fc542
com.example.demo.test.Dog@4524411f
既然调用get()方法可能会出现NoSuchElementException异常。那我们怎么避免这个问题呢?答案就是Optional提供isPresent()和ifPresent()方法。
- isPresent():返回布尔值表示Optional是否为有值状态。
- ifPresent():将Consumer接口作为参数,若Optional为有值状态则调用该接口。
简单示例
Dog dog6 = new Dog();
dog6.setName("旺财");
Optional<Dog> op3 = Optional.<Dog>ofNullable(dog6);
// ifPresent()方法
op3.ifPresent(dog -> dog.getName());
// isPresent()判断
if(op3.isPresent()){
op3.get().getName();
}
// 输出
我是旺财
我是旺财
注意:Optional不能序列化,最好避免将Optional用作类的字段(field)类型。关于Java 8的新特性就写到这了,如有不足之处,还请提出指正。