七的博客

Java8的新特性

Java

Java8的新特性

最近一年公司的项目都开始采用 JDK8了,一天天的积累下来对 Java8 一些新语法跟知识点都有了一定的积累。

打算用一篇文章总结下 JDK8 的一些写法跟之前的写法差别,同时对项目中经常用的一些语法做个总结。

1. JDK8 更新了些什么

通过 Oracle 上的技术文档,我找到了 JDK8 的更新内容,文档地址是 https://www.oracle.com/java/technologies/javase/8-whats-new.html。 稍微对文档的内容做一部分比较实用的截取展示下。

  • Lambda Expressions, a new language feature, has been introduced in this release. They enable you to treat functionality as a method argument, or code as data. Lambda expressions let you express instances of single-method interfaces (referred to as functional interfaces) more compactly.

引入了新的语言特性—Lambda 表达式。Lambda 可以作为方法参数,这在一些动态语言中已经出现了很多年了。

  • Method references provide easy-to-read lambda expressions for methods that already have a name.

方法引用是一种简洁的Lambda表达式,可直接引用现存的方法。

  • Default methods enable new functionality to be added to the interfaces of libraries and ensure binary compatibility with code written for older versions of those interfaces.

接口现在可以包含默认方法,在之前的版本中是不允许的。

  • Date-Time Package 。a new set of packages that provide a comprehensive date-time model.

引入了全新的日期和时间 API( java.time包),提供了更好的日期和时间处理能力,在项目里面可以少封装很多日期相关的工具类。

后续就不用单独引用 Joda-Time 了, JDK 中的库即是作者跟 JDK 团队协作开发的。

  • Classes in the new java.util.stream package provide a Stream API to support functional-style operations on streams of elements. The Stream API is integrated into the Collections API, which enables bulk operations on collections, such as sequential or parallel map-reduce transformations.

Stream API 提供了一种高效且易于使用的方式来处理集合数据。它支持多种操作如过滤、映射、合并等。以前总是说 Java 的代码又长又丑,有了 Stream API 后可以简洁很多。

  • Standard Encoding and Decoding

官方的 Base64 编码的支持。

  • Classes and interfaces have been added to the java.util.concurrent package.

java.util.concurrent 包中增加了很多的类以及接口,支持更多的并发特性。

2. 比较实用的新特性

2.1 lambda 表达式

Lambda 表达式是一种简洁的方式来定义匿名函数,匿名函数就是没有名字的函数。

通过这种方式可以快速定义一个函数,但是又不用创建一个完整的函数声明,而且不用费脑筋去想函数名。

在其他语言中,Lambda 已经已经应用的很广泛了,特别是在一些动态语言里面。

比如在 JavaScript 中,箭头函数跟 Lambda 就是类似的:

// 普通函数
function add(a, b) {
    return a + b;
}

// Lambda表达式(箭头函数)
const add = (a, b) => a + b;

TypeScript 中也是:

const add = (a: number, b: number): number => a + b;

从上面的例子中可以看到实现相同的功能,代码更少更简洁。

回想下写 Java 界面程序的时候,经常会出现大量匿名内部类。比如要给按钮绑定一个处理事件,通常都会创建一个匿名内部类 EventHandler

这是 Java8 之前的写法:

final Button button=new Button("点击这个按钮");
        button.setOnAction(new EventHandler<ActionEvent>(){
@Override
public void handle(ActionEvent event){
        System.out.println("按钮被点击!");
        }
        });

切换到使用 Java8 Lambda 的表达式后代码:

final Button button=new Button("点击这个按钮");
        button.setOnAction(event->System.out.println("按钮被点击!"));

可以看到代码已经简化了许多。猜测下应该是哪里特殊处理过才可以这么写,点进源码里面看看:


@FunctionalInterface
public interface EventHandler<T extends Event> extends EventListener {
    /**
     * Invoked when a specific event of the type for which this handler is
     * registered happens.
     *
     * @param event the event which occurred
     */
    void handle(T event);
}

跟之前版本的差异在于多了一个 @FunctionalInterface 注解,这个注解就是 Lambda 的核心。

使用了 @FunctionalInterface 这个注解表名这个接口是个函数式接口,满足函数式接口的条件还需要是当前接口中只能有一个抽象接口函数,不包括默认函数。

当接口中只有一个函数时,编译器在编译的时候就可以正确推导出使用的是调用哪个函数。

再比如平时创建线程的时候,代码也可以得到很大的简化。


// Java8 以前的创建形式
new Thread(new Runnable(){
@Override
public void run(){
        System.out.println("线程方法在执行!");
        }
        }).start();

// 使用 Java8 Lambda表达式方式
        new Thread(()->System.out.println("线程方法在执行!")).start();


像这样的用法还有很多,想知道能不能用 Lambda 代替的话可以去观察接口是否符合以下规则:

  • 接口上有注解 @FunctionalInterface
  • 接口中只声明了一个抽象接口函数,不包括默认函数。

2.2 方法引用

方法引用可以看做是一种特殊的 Lambda 表达式,有时候它可以比 Lambda 更加的简洁。

先来看个例子:


final List<String> list = Arrays.asList("JDK6", "JDK7", "JDK8");
list.forEach(s -> System.out.println(s));


final List<String> list = Arrays.asList("JDK6", "JDK7", "JDK8");
list.forEach(System.out::println);

差异就在于第二个打印每一个元素的地方,通过方法引用 Lambda 的参数之类的都直接简化掉了。

根据 Oracle 文档中描述,目前分为如下四种:

2.2.1 Reference to a static method 静态方法引用

  • 语法:ContainingClass::staticMethodName ( 类名::静态方法名 )
  • 示例:
import java.util.Arrays;
import java.util.List;


public class StaticMethodReferenceDemo {
    
    // 对一个数组进行排序,三种演变的写法
    public static void main(String[] args) {
        final List<Integer> numbers = Arrays.asList(5, 3, 8, 1);
        
        // 第一种匿名内部类形式
        numbers.sort(new Comparator<Integer>() {
            @Override
            public int compare(Integer x, Integer y) {
                return Integer.compare(x, y);
            }
        });
        
        // 第二种 lambda 形式
        numbers.sort((x, y) -> Integer.compare(x, y));
        
        
        // 第三种方法引用形式
        numbers.sort(Integer::compare);
        
    }
}

2.2.2 Reference to an instance method 实例方法引用

  • 语法:containingObject::instanceMethodName ( 实例对象引用::实例方法名 )
  • 示例:
import java.util.Collections;
import java.util.List;
import java.util.Arrays;
import java.util.Comparator;

public class InstanceMethodReferenceDemo {

    public static void main(String[] args) {
        final List<Integer> numbers = Arrays.asList(5, 3, 8, 1);

        // 先创建一个 Comparator 对象
        Comparator<Integer> normalOrder = Integer::compareTo;

        // 再使用实例方法引用对列表进行排序
        Collections.sort(numbers, normalOrder);

        System.out.println(numbers);
    }
}

注意上面的 Integer::compareTo 是一个实例方法,不是静态方法:

public int compareTo(Integer anotherInteger) {
    return compare(this.value, anotherInteger.value);
}

2.2.3 Reference to an instance method 特定类型的任意对象的实例方法引用

  • 语法:ContainingType::methodName ( 类名::实例方法名 )
  • 示例:
import java.util.Arrays;
import java.util.List;

public class SpecialInstanceMethodReferenceDemo {

    public static void main(String[] args) {
        final List<Integer> numbers = Arrays.asList(5, 3, 8, 1);

        final List<Integer> numbers = Arrays.asList(5, 3, 8, 1);
      
        // 这里 Integer::compareTo 方法引用直接用于排序,无需创建一个 Comparator 实例。
        Collections.sort(numbers, Integer::compareTo);
      
        // 输出: [1, 3, 5, 8]
        System.out.println(numbers);
    }
}

注意跟实例方法引用的区别:

  • 实例方法引用中,先创建一个 Comparator 对象,再调用这个对象 compare 方法进行排序。
  • 在特定类型实例方法引用中,直接使用 Integer::compareTo 进行排序。Integer::compareTo 是一个特定类型的任意对象的实例方法引用。它不依赖于某个特定的 Integer 实例,而是可以应用于任何 Integer 实例。

2.3.4 Reference to a constructor 构造器引用

  • 语法:ClassName::new ( 类名::new )
  • 示例:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ConstructMethodReferenceDemo {

    public static void main(String[] args) {
        final List<Integer> numbers = Arrays.asList(5, 3, 8, 1);

        // Lambda 表达式写法
        List<Integer> doubledNumbers1 = numbers.stream().map(n -> new Integer(n * 2)).collect(Collectors.toList());
        // 输出: [10, 6, 16, 2]
        System.out.println(doubledNumbers1);
        
        
        // 使用构造函数方法引用
        List<Integer> doubledNumbers2 = numbers.stream().map(Integer::new).collect(Collectors.toList());

        // 输出: [10, 6, 16, 2]
        System.out.println(doubledNumbers2); 
    }
}

本质上来说,方法引用其实就是引用一个现有的方法或函数,并在需要的时候调用它。

在其他语言里面也大量这种用法,比如在 JavaScript 中:

const numbers = [5, 3, 8, 1];

// 引用 Math.sqrt 这个方法对每一个元素计算平方根
const roots = numbers.map(Math.sqrt);

console.log(roots);

2.4 接口默认方法

在 Java8 以前的版本中,接口中只允许声明抽象接口函数,不允许有静态函数或者其他函数。

在 Java8 中,允许在接口中提供方法的默认实现。 可以在不破坏已有代码的情况下 ,为接口添加新的功能。因为接口中的函数必须由实现类去实现,所以导致接口一加函数,实现类就必须去实现。

使用例子如下:


public interface DefaultMethodDemo {
    
    void method1();

    default void defaultMethod() {
        System.out.println("Default method");
    }
}

2.5 Stream API 流

Stream API 是 Java 8 引入的一个强大的功能 , 它允许以一种更加函数式的方式来处理集合。

更简单点来说就是操作一些集合的时候,方法更加简洁,同时支持链式处理。 之前写 JavaScript 的时候,很多集合的方法就很方便。比如很常用的一个操作:

const names = ["zhangsan", "lisi", "wangwu", "zhangliu"];

names.filter(name => name.startsWith("zhang"))   // 过滤姓名以 zhang 开头的名称
     .map(name => name.toUpperCase())  // 姓名转成大写形式
     .sort() // 自然排序
     .forEach(name => console.log(name));  // 打印到控制台

几行代码就实现了,但是同样的逻辑,在 Java8 之前实现的话就会是下面的样子:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class BeforeStreamDemo {

    public static void main(String[] args) {
        final List<String> names = new ArrayList<>();
        names.add("zhangsan");
        names.add("lisi");
        names.add("wangwu");
        names.add("zhangliu");

        // 过滤姓名以 "zhang" 开头的名称
        final List<String> filteredNames = new ArrayList<>();
        for (String name : names) {
            if (name.startsWith("zhang")) {
                filteredNames.add(name);
            }
        }

        // 姓名转成大写形式
        final List<String> upperCaseNames = new ArrayList<>();
        for (String name : filteredNames) {
            upperCaseNames.add(name.toUpperCase());
        }

        // 自然排序
        Collections.sort(upperCaseNames);

        // 打印到控制台
        for (String name : upperCaseNames) {
            System.out.println(name);
        }
    }
}

java8 就可以改写成下面的形式:

package com.suny.stream;

import java.util.Arrays;
import java.util.List;

public class StreamDemo {

    public static void main(String[] args) {
        final List<String> names = Arrays.asList("zhangsan", "lisi", "wangwu", "zhangliu");

        names.stream()
                .filter(name -> name.startsWith("zhang"))   // 过滤姓名以 zhang 开头的名称
                .map(String::toUpperCase)  // 姓名转成大写形式
                .sorted()  // 自然排序
                .forEach(System.out::println);  // 打印到控制台
    }
}

Stream 的 API 实在是太多了,如果要展开的话篇幅特别长。

2.6 Optional类

Optional 类是 Java 8 引入的一个容器类,用于表示一个值可能存在或不存在。划重点: 这是一个 Java 类,并不是编译器提供的语法糖。

没有 Optional 时的代码示例:

public class User {

    private String username;
    private String password;
    .... 省略 set get
}

public class BeforeOptionalDemo {

    public static void main(String[] args) {

        // 这个 user 对象可能是从数据库查询出来的或者是传过来的
        final User user = null;

        // 获取用户名
        final String userName = getUserName(null);

        // 输出 default
        System.out.println(userName);
    }

    public static String getUserName(User user) {
        // 为了防止空指针判空,同时给默认值
        if (user != null) {
            return user.getUsername();
        } else {
            return "default";
        }
    }
}

有了 Optional 以后的代码:

import java.util.Optional;

public class OptionalDemo {

    public static void main(String[] args) {

        // 这个 user 对象可能是从数据库查询出来的或者是传过来的
        final User user = null;

        // 获取用户名
        final String userName = getUserName(null);

        // 输出 default
        System.out.println(userName);
    }


    public static String getUserName(User user) {
        // 为了防止空指针判空,同时给默认值
        return Optional.ofNullable(user)
                .map(User::getUsername)
                .orElse("default");
    }
}

上面是一种比较实用的 Optional 用法。一眼看上去,跟之前的手动空指针判断其实差别不是特别大,我个人觉得不是特别的实用。

特别是还需要合理的判断,在什么场合下使用。 滥用 Optional 只会让代码更加复杂化,还不如手动判空。

同时还需要注意的是,Optional 不应该出现在类的字段上,这个类创建出来的目的只是想用在返回返回值上。

3. 总结

Java8 引入的新特性远远不止上面写的这么点,还有一些不是特别实用或者说不是特别有亮点的特性没有在上面总结出来。

通过熟练使用 Java8 的一些语法,可以让我们的代码量明显更少,开发速度得到一定的提升,也不用一直羡慕动态语言的一些特性。

4. 参考链接