【都 Java17 了,还不了解 Java 8 ? 】一文带你深入了解 Java 8 新特性

在这里插入图片描述

Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本。 Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,它支持函数式编程,新的 JavaScript 引擎,新的日期 API,新的Stream API 等。(文章很长,建议点赞收藏

新特性

以下是Java 8 新增的部分特性,更多新特性了解请详细参考:What’s New in JDK 8

• Lambda 表达式
• 方法引用
• 函数式接口
• 默认方法
• Stream
• Optional 类
• Nashorn, JavaScript 引擎
• Date/Time API 新的日期时间 API
• Base64
• 新工具 − 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。

一、Lambda 表达式

Lambda 表达式(也可称为闭包),它是推动 Java 8 发布的最重要新特性。
Lambda 表达式允许把函数作为一个方法的参数,可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。

什么是 Lambda 表达式

先来看看 Lambda 表达式的官方解释
Lambda 表达式(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。

这样的解释还是让人摸不着头脑,那我们接着往下看。

首先介绍, Lambda 表达式的语法格式() -> {}

其中 () 用来描述参数列表,{} 用来描述方法体,-> 为 lambda运算符 ,读作(goes to)简单例子:
(int x, int y) -> x + y  //接收2个int型整数,返回他们的和
(String s) -> System.out.print(s) // 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void) 

前面我们说了 Lambda 表达式可以取代大部分的匿名内部类,举个例子:

@FunctionalInterface
public interface NoReturnMultiParam {
    void method(int a, int b);
}
public class Test {

    public void sayHello() {
        // 匿名类实现NoReturnMultiParam接口
        NoReturnMultiParam noReturnMultiParam = new NoReturnMultiParam() {
            @Override
            public void method(int a, int b) {
                System.out.println("param:{a=" + a + ",b=" + b + "}");
            }
        };
        // 调用接口
        noReturnMultiParam.method(1,2);
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.sayHello();
    }
}

运行结果:param:{a=1,b=2}

接着,我们将匿名类实现替换为 Lambda表达式

 public class Test {

    public void sayHello() {
        // Lambda实现NoReturnMultiParam接口
        NoReturnMultiParam lambda = (a, b) -> System.out.println("param:{a=" + a + ",b=" + b + "}");
        // 调用接口
        lambda.method(1,2);
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.sayHello();
    }
}

运行结果与之前相同,可以看到,Lambda 表达式可以来定义行内执行的方法类型接口,免去了使用匿名方法的麻烦。
简单来说,在 Java 中可以将 Lambda 表达式看成一个接口的实现,但并不是所有的接口都可以使用 Lambda 表达式来实现。

Lambda 规定接口中只能有一个需要被实现的方法,即函数式接口,不是规定接口中只能有一个方法。

闭包问题

lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。

public static void main(String[] args) {
        int c = 10;
        NoReturnMultiParam lambda = (a, b) -> System.out.println("param:{a=" + a + ",b=" + b + "},c="+c);
        lambda.method(1,2);
}

这里c没有标识为final,但是没有被后续代码修改,所以在编译期间虚拟机会帮我们加上 final 修饰关键字(即隐性的具有 final 的语义)

修改代码,出现错误java: 从lambda 表达式引用的本地变量必须是最终变量或实际上的最终变量

public static void main(String[] args) {
        int c = 10;
        NoReturnMultiParam lambda = (a, b) -> System.out.println("param:{a=" + a + ",b=" + b + "},c="+c);
        c = c + 2;
        lambda.method(1,2);
}

二、方法引用

方法引用通过方法的名字来指向一个方法,在使用 Lambda 表达式时,有时候我们不是必须要自己重写某个匿名内部类的方法,而是可以利用 Lambda 表达式的接口快速指向一个已经被实现的方法。

语法:​ 方法归属者::方法名 静态方法的归属者为类名,普通方法归属者为对象

Java 中 4 种不同方法的引用:
构造器引用 ClassName::new
静态方法引用 Class::static_method
特定类的任意对象的方法引用 Class::method
特定对象的方法引用 instance::method

代码示例:

package com.local.springboot.springbootservice.sysuser;

interface InstanceCreate {
    Test get();
}

@FunctionalInterface
interface ReturnMultiParam {
    int method(int a, int b);
}
public class Test {
    public static int addNum(int a, int b) {
        return a + b;
    }

    public int deleteNum(int a, int b) {
        return a - b;
    }
    public void sayHello() {
        System.out.println("Hello World");
    }
    public static void main(String[] args) {

        ReturnMultiParam lambda = (a, b) -> addNum(a, b);
        System.out.println(lambda.method(1, 2));


        // 构造器引用
        InstanceCreate create = Test::new;
        System.out.println(create.get());

        // 静态方法引用
        ReturnMultiParam result = Test::addNum;
        System.out.println(result.method(1, 2));

        // 特定对象的方法引用
        Test test = new Test();
        ReturnMultiParam result2 = test::deleteNum;
        System.out.println(result2.method(2, 2));
    }

}

运行结果:

3
com.local.springboot.springbootservice.sysuser.Test@179d3b25
3
0

三、函数式接口

上面提到接口中只有一个需要被实现的方法的接口,叫做函数式接口

函数式接口需要用@FunctionalInterface注解修饰,要求接口中的抽象方法只有一个,但可以有多个非抽象方法

函数式接口实例

函数式接口可以对现有的函数友好地支持 lambda。比如常用的Comparator或者Consumer接口。

比如常见的集合内元素的排序

List<Cat> list = new ArrayList<>();
list.add(new Cat(5,"Tom"));
list.add(new Cat(2,"Aimi"));
list.add(new Cat(3,"Doe"));

list.sort(new Comparator<Cat>() {
    @Override
    public int compare(Cat o1, Cat o2) {
        return o1.getIndex()- o2.getIndex();
    }
});
list.forEach(item -> {
    System.out.println(item.toString());
});

在以前我们若要为集合内的元素排序,就必须调用 sort 方法,传入比较器匿名内部类重写 compare 方法,我们现在可以使用 lambda 表达式来简化代码。

list.sort((o1,o2)->o1.getIndex()-o2.getIndex());
//list.sort((Comparator.comparing(Cat::getIndex)));
list.forEach(item -> {
    System.out.println(item.toString());
});

更多函数式接口

JDK 1.8 之前已有的函数式接口:

java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener

JDK 1.8 新增加的函数接口:

java.util.function
java.util.function 它包含了很多类,用来支持 Java的 函数式编程,详细的函数式接口请查看源码。

四、默认方法

简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。
用法:只需在方法名前面加个 default 关键字即可实现默认方法

被 default 修饰的方法会有默认实现,不是必须被实现的方法,所以不影响 Lambda 表达式的使用

相同的默认方法

当一个类实现多个接口,而且这些接口存在相同的默认方法,会发生什么情况呢?

interface Interface1{
    default void print() {
        System.out.println("我是接口1默认方法实现!");
    }
}

interface Interface2{
    default void print() {
        System.out.println("我是接口2默认方法实现!");
    }
}


public class InterfaceImpl implements Interface1, Interface2{
    @Override
    public void print() {
    	// 可以使用 super 来调用指定接口的默认方法
    	// Interface1.super.print();
        System.out.println("我是接口实现类!");
    }

    public static void main(String[] args) {
        InterfaceImpl interfaceImpl = new InterfaceImpl();
        interfaceImpl.print();
    }
}

运行结果:

我是接口实现类!

当出现这种接口冲突,一般有两种解决方案 覆盖重写接口的默认方法使用 super 来调用指定接口的默认方法

静态默认方法

Java 8 中接口是可以声明静态方法(并且可以提供实现)的。例如:

interface Interface2{
    default void print() {
        System.out.println("我是接口2默认方法实现!");
    }
   // 静态方法
   static void doSomething(){
      System.out.println("我是接口2中的静态方法");
   }
}

为什么要有这个特性

说了这么多,那么问题来了,为什么要新增这个特性?

新增默认方法是为了解决接口的修改与现有的实现不兼容的问题
我们都知道接口是面向抽象而不是面向具体编程的,所以当需要修改接口时,就需要修改全部实现改接口的类。
所以对于以前发布的版本,是做不到在修改接口的同时不影响已有的实现。

五、Stream

Java 8 API 添加了一个新的抽象称为流 Stream,可以让你以一种声明的方式处理数据。

语法糖 Stream 以一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。简单来说流是Java 8 中对Collection对象功能的加强。

流操作

【数据集合】 ->【数据源】 - > 【转换(聚合)操作】 ->【终点操作】

• Intermediate(转换操作):中间操作都会返回流对象本身。就是说,仅仅调用到这类方法,并没有真正开始流的遍历。多次的转换操作只会在遇到终点操作之后,才会依次执行。

转换操作:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel…

• Terminal(终点操作):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

终点操作:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny…

• Short-Circuiting(短路操作):短路操作其实和终点操作也是一样的,可能不再返回一个流,或是返回一个被截取过的流。比如anyMatch方法,通过Predicate接口返回了一个真值。由于流Stream在理论上是无限大的,短路操作被用以对流进行截取,把无限的变成有限的流,比如limit方法,可以限制获取数据源的数量。

短路操作:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit…

外部迭代与内部迭代

这里值得一提的是以前对集合遍历都是通过Iterator或者for-each的方式,显式的在集合外部进行迭代, 这叫做外部迭代。与以前的Collection操作不同,流操作提供了内部迭代的方式, 通过访问者模式(Visitor)实现

那什么是外部迭代和内部迭代呢?举个简单的列子:

比如你请人打扫房间,但有觉得不放心,于是你觉得现场指示工人先擦桌子,再拖地,最后洗碗…直到打扫完毕,这就是所谓的外部迭代,即显示地取出元素进行处理。
后来你和清洁工人熟悉之后,你只需要和她说把房间打扫干净,清洁工人自己选择先做什么,再做什么,你等着接收成果就行了。这就是内部迭代

生成流

顶层集合类Collection添加了两个方法:stream()parallelStream()
• stream() − 为集合创建串行流。
• parallelStream() − 为集合创建并行流。

开启流计算时根据操作的数据量选择调用stream()或者parallelStream()

使用实例

forEach

Stream 提供了新的方法 ‘forEach’ 来迭代流中的每个数据。

List<String> strings = Arrays.asList("abc", "bc", "efg", "abd","jkl");
strings.forEach(System.out::println); 
map

map 方法用于映射每个元素到对应的结,以下代码片段使用 map 输出了元素对应的大写

List<String> strings = Arrays.asList("abc", "bc", "efg", "abd","jkl");
strings.stream().map(String::toUpperCase).sorted((a, b) -> b.compareTo(a)).forEach(System.out::println);
filter

filter 方法用于通过设置的条件过滤出元素。把除了abc的字符串过滤出来

List<String> strings = Arrays.asList("abc", "bc", "efg", "abd","jkl");
strings.stream().filter(string -> !"abc".equals(string)).forEach(System.out::println);
limit

limit 方法用于获取指定数量的流。 打印3条数据

List<String> strings = Arrays.asList("abc", "bc", "efg", "abd","jkl");
strings.stream().limit(3).forEach(System.out::println);

流操作简单说明

filter:用于过滤出满足条件的元素
distinct:去重,需要重写equals()和hashCode()
sorted:对元素进行排序
limit:返回前n个元素
skip:去掉前n个元素
map:方法用于映射每个元素对应的结果
flapMap:将流中的每一个元素T映射成为一个流,再把每一个流连接成一个流
anyMatch:是否存在任意一个元素满足条件(返回布尔值)
allMatch:是否所有元素都满足条件(返回布尔值)
noneMatch:是否所有元素都不满足条件(返回布尔值)
findAny:找到其中一个元素 (使用 stream() 时找到的是第一个元素;使用 parallelStream() 并行时找到的是其中一个元素)
findFirst:找到第一个元素
reduce:用于组合流中的元素,如求和,求积,求最大值等
count:返回流中元素个数,结果为 long 类型
collect:收集方法,我们很常用的是 collect(toList()),当然还有 collect(toSet()) 等,参数是一个收集器接口

Stream流式计算的使用

说了这么多,那么流使用好处以及对性能的影响如何呢?

Stream API 可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

通过实际示例对比了常规的集合类的过滤、封装、统计操作,几百的小数据量操作,常规外部迭代更快;数据量再大一点,stream()串行的流式计算会更快;上万级别的数据量后,parallelStream()并行流式计算会更快。

六、Optional 类

相信大家在编码中最常遇见的就是空指针异常,而Optional 类的引入就是为了很好地解决空指针异常。

Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测

常用类方法

方法描述
T get()如果在这个Optional中包含这个值,返回值,否则抛出异常:NoSuchElementException
void ifPresent(Consumer<? super T> consumer)如果值存在则使用该值调用 consumer , 否则不做任何事情。
boolean isPresent()如果值存在则方法会返回true,否则返回 false。
Optional map(Function<? super T,? extends U> mapper)如果有值,则对其执行调用映射函数得到返回值。如果返回值不为 null,则创建包含映射返回值的Optional作为map方法返回值,否则返回空Optional。
T orElse(T other)如果存在该值,返回值, 否则返回 other。
T orElseGet(Supplier<? extends T> other)如果存在该值,返回值, 否则触发 other,并返回 other 调用的结果。

简单举例

orElse 存在则返回aa,不存在则返回bb

Optional<String> string= Optional.of("aa");
string.orElse("bb");

七、日期时间 API

旧版日期时间API问题:
非线程安全: java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
日期/时间类的定义并不一致:在java.util和java.sql的包中都有日期类
时区处理麻烦:日期类并不提供国际化,没有时区支持

Java 8 在 java.time 包下提供了很多新的 API:
Local(本地):简化了日期时间的处理,没有时区的问题。
Zoned(时区:通过制定的时区处理日期时间。

新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。

使用时区的日期时间API

时区使用 ZoneId 来表示,使用静态方法of来获取时区

 // 获取当前时间日期
ZonedDateTime date = ZonedDateTime.parse("2021-11-13T10:15:30+05:30[Asia/Shanghai]");
System.out.println("date: " + date);
        
ZoneId id = ZoneId.of("Europe/Paris");
System.out.println("ZoneId: " + id);
        
ZoneId currentZone = ZoneId.systemDefault();
System.out.println("当期时区: " + currentZone);

本地化日期时间API

LocalDate、LocalTime 、LocalDateTime 都是用于处理日期时间的 API,在处理日期时间时可以不用强制性指定时区

// 获取当前的日期时间
LocalDateTime currentTime = LocalDateTime.now();
System.out.println("当前时间: " + currentTime);//当前时间: 2016-04-15T16:55:48.668
  
LocalDate date1 = currentTime.toLocalDate();
System.out.println("date1: " + date1);//date1: 2016-04-15
  
Month month = currentTime.getMonth();
int day = currentTime.getDayOfMonth();
int seconds = currentTime.getSecond();
  
System.out.println("月: " + month +", 日: " + day +", 秒: " + seconds);//月: APRIL, 日: 15, 秒: 48
  
LocalDateTime date2 = currentTime.withDayOfMonth(10).withYear(2012);
System.out.println("date2: " + date2);//date2: 2012-04-10T16:55:48.668
  
// 12 december 2014
LocalDate date3 = LocalDate.of(2014, Month.DECEMBER, 12);
System.out.println("date3: " + date3);//date3: 2014-12-12
  
// 22 小时 15 分钟
LocalTime date4 = LocalTime.of(22, 15);
System.out.println("date4: " + date4);//date4: 22:15
  
// 解析字符串
LocalTime date5 = LocalTime.parse("20:15:30");
System.out.println("date5: " + date5);//date5: 20:15:30

自定义格式使用DateTimeFormatter,它是不可变的(线程安全

LocalDateTime currentTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(formatter.format(currentTime));// 2021-11-13 10:34:55.731

八、Base64

在Java 8中,Base64编码已经成为Java类库的标准。
至于它的使用则十分简单,来看个例子:

// 编码
String base64encodedString = Base64.getEncoder().encodeToString("Java8-Base64".getBytes("utf-8"));
System.out.println(base64encodedString);

// 解码
String base64decodedString = new String(Base64.getDecoder().decode(base64encodedString), "utf-8");
System.out.println(base64decodedString);

输出结果为:

SmF2YTgtQmFzZTY0
Java8-Base64

此外,Base64工具类还提供了URL、MIME编解码器

方法描述
static Base64.Decoder getMimeDecoder()返回一个 Base64.Decoder ,解码使用 MIME 型 base64 编码方案。
static Base64.Encoder getMimeEncoder()返回一个 Base64.Encoder ,编码使用 MIME 型 base64 编码方案。
static Base64.Decoder getUrlDecoder()返回一个 Base64.Decoder ,解码使用 URL 和文件名安全型 base64 编码方案。
static Base64.Encoder getUrlEncoder()返回一个 Base64.Encoder ,编码使用 URL 和文件名安全型 base64 编码方案。

九、Nashorn JavaScript引擎

Nashorn 是一个 javascript 引擎,使得JavaScript 代码可以在 Java 中执行,如下:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine nashorn = scriptEngineManager.getEngineByName("JavaScript");
System.out.println(engine.eval("function f(){return 10;}; f() + 1;"));//12

jjs

jjs是个基于Nashorn引擎的命令行工具。它接受一些JavaScript源代码为参数,并且执行这些源代码。

使用Nashorn运行脚本的示例

jjs script.js

在交互模式下运行Nashorn的示例

jjs
jjs> println(“Hello, World!”)
Hello, World!
jjs> quit()

将参数传递给Nashorn的示例

$ jjs – a b c
jjs> arguments.join(", ")
a, b, c
jjs>

值得注意的是:

随着ECMAScript语言标准的快速发展,维护Nashorn引擎变得越发挑战,因此该引擎将在Java中废弃。Java11将声明弃用Nashorn JavaScript脚本引擎,被标注为@Deprecated(forRemoval=true)

十、类依赖分析器jdeps

jdeps是一个相当棒的命令行工具,它可以展示包层级和类层级的Java类依赖关系,它以.class文件、目录或者Jar文件为输入,然后会把依赖关系输出到控制台。

我们可以利用jedps分析下org.springframework.core-3.0.5.RELEASE.jar ,这个命令会输出很多结果,我们仅看下其中的一部分:依赖关系按照包分组,如果在classpath上找不到依赖,则显示not found。

org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
   org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.io                                            
      -> java.lang                                          
      -> java.lang.annotation                               
      -> java.lang.ref                                      
      -> java.lang.reflect                                  
      -> java.util                                          
      -> java.util.concurrent                               
      -> org.apache.commons.logging                         not found
      -> org.springframework.asm                            not found
      -> org.springframework.asm.commons                    not found
   org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.lang                                          
      -> java.lang.annotation                               
      -> java.lang.reflect                                  
      -> java.util

参考资料:
Lambda表达式详解
Java8 新特性之八---------类依赖分析器:jdeps

创作不易,关注、点赞就是对作者最大的鼓励,欢迎在下方留言
欢迎关注微信公众号键指JAVA,定期分享Java知识,一起学习,共同成长

扫码关注,共同成长!
在这里插入图片描述

posted on 2022-06-09 23:18  猫的树kireCat  阅读(309)  评论(0编辑  收藏  举报