Java8新特性

文章目录


前言

在学习Java8的新特性之前,需要你对Java SE的基本语法有一定的了解,因此这篇文章不适合对于Java完全不了解的小白。在Java8中,新增了Lambda、Stream API等常用的语法,请阅读下面的文章学习。


一、Lambda表达式

1、Lambda初体验

    public static void main(String[] args) {
        //使用匿名内部类开启线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("新线程中的代码:" + Thread.currentThread().getName());
            }
        }).start();

        System.out.println("主线程中的代码:" + Thread.currentThread().getName());

        //使用Lambda表达式写法,开启线程
        new Thread(() -> {
            System.out.println("新线程Lambda的代码:" + Thread.currentThread().getName());
        }).start();
    }

Lambda表达式的优点:简化了匿名内部类的使用,语法更加简单。
匿名内部类语法冗余,体验了Lambda表达式后,发现Lambda表达式是简化匿名内部类的一种方式。

2、Lambda语法规则

Lambda省去了面向对象的条条框框,在没有Lambda之前,我们声明方法是这样的 private void test () { 逻辑代码等 } ,或者像Main方法一样, public static void main(String[] args) { 逻辑代码等 } ,但是有了Lambda之后,我们不需要关注publib访问修饰符、void返回值、test方法名等,我们只需要关心方法的参数列表及方法体,因此,Lambda的标准格式由3个部分组成:

(参数类型 参数名称) -> {
  业务逻辑代码;
}
  • (参数类型 参数名称) :参数列表
  • { 业务逻辑代码 } :方法体
  • ->:分割参数列表和方法体

2.1、无参无返回值的Lambda

首先定义interface和抽象方法。

public interface UserService {
    //接口中,随便定义一个方法
    void show();
}

然后创建Main方法,对比匿名内部类与Lambda的语法区别。

public class App {

    //定义一个静态方法,入参为UserService接口
    public static void goShow(UserService userService) {
        userService.show();
    }
    
    public static void main(String[] args) {
        goShow(new UserService() {
            @Override
            public void show() {
                System.out.println("匿名内部类的方式重写show方法...");
            }
        });
        System.out.println("-----------分割符----------");
        goShow(() -> {
            System.out.println("Lambda的方式重写show方法...");
        });
    }
}

2.2、有参有返回值的Lambda

首先定义一个Person对象。

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

然后创建多个Person对象存入List集合,并使用匿名内部类的方式实现Comparator,对List进行排序。

public class App {
    public static void main(String[] args) {
        List<Person> list = new ArrayList<>();
        list.add(new Person("周杰伦", 33));
        list.add(new Person("刘德华", 43));
        list.add(new Person("郭富城", 36));
        list.add(new Person("周星驰", 48));
        //使用匿名内部类实现Comparator,从而排序
        Collections.sort(list, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        });
        //打印查看效果
        for (Person person : list) {
            System.out.println(person);
        }
    }
}

最后,我们使用Lambda代替匿名内部类,来达到排序效果。查看Collections.sort的源码可以发现,它需要一个Comparator的实现类,而我们通过Lambda implements了 Comparator,因此可以得出一个结论,Lambda简化了匿名内部类的语法。

public class App {
    public static void main(String[] args) {
        List<Person> list = new ArrayList<>();
        list.add(new Person("周杰伦", 33));
        list.add(new Person("刘德华", 43));
        list.add(new Person("郭富城", 36));
        list.add(new Person("周星驰", 48));
        //使用Lambda实现Comparator,达到排序效果
        Collections.sort(list, (Person o1, Person o2) -> {
            return o1.getAge() - o2.getAge();
        });
        //打印查看效果
        for (Person person : list) {
            System.out.println(person);
        }
    }
}

3、@FunctionalInterface注解

使用Lambda和匿名内部类都可以实现某个接口,虽然Lambda表达式比匿名内部类语法简洁,但也不能完全代替匿名内部类。因为当使用Lambda实现某个接口时,Lambda只能重写父接口的一个抽象方法,而匿名内部类却可以重写父接口的所有抽象方法,这就决定了,由Labmda实现的接口只能包含一个抽象方法。
我们回到刚才的UserService来举例说明。

public interface UserService {
    //接口中,随便定义一个方法
    void show();
}
public class App {

    //定义一个静态方法,入参为UserService接口
    public static void goShow(UserService userService) {
        userService.show();
    }
    
    public static void main(String[] args) {
        goShow(new UserService() {
            @Override
            public void show() {
                System.out.println("匿名内部类的方式重写show方法...");
            }
        });
        System.out.println("-----------分割符----------");
        goShow(() -> {
            System.out.println("Lambda的方式重写show方法...");
        });
    }
}

重点来了,此时我们在UserService接口中,再新增一个show2()抽象方法。

public interface UserService {
    //接口中,随便定义一个方法
    void show();
    void show2();
}

父接口新增show2()抽象方法,基于Java的继承语法规范,那么它的所有子类,也必须重写show2()方法才行。结果可想而知,我们在Main方法调用goShow()的时候使用了匿名内部类,因此,这个匿名内部类也要重写show2()方法。

public class App {

    //定义一个静态方法,入参为UserService接口
    public static void goShow(UserService userService) {
        userService.show();
    }
    
    public static void main(String[] args) {
        goShow(new UserService() {
            @Override
            public void show() {
                System.out.println("匿名内部类的方式重写show方法了...");
            }
            @Override
            public void show2() {
                System.out.println("匿名内部类的方式重写show2方法了...");
            }
        });
        System.out.println("-----------分割符----------");
        //此处报错了,一条红色的杠杠!! ~~~ 0.0  >.< ~~~
        goShow(() -> {
            System.out.println("Lambda的方式重写show方法了...");
        });
    }
}

问题出现了,我们使用匿名内部类重写show()和show2()时轻而易举,但是使用Lambda却报错了,无法重写。因此得出一个结论,如果想使用Lambda实现某个接口,那么该接口必须只能包含一个抽象方法,因为Lambda无法重写多个抽象方法。
最后介绍一个注解,@FunctionalInterface,这个注解是一个标志注解,被该注解修饰的接口只能声明一个抽象方法。
如下所示:

//被@FunctionalInterface修饰的接口只能有一个抽象方法
@FunctionalInterface 
public interface UserService {
    //接口中,随便定义一个方法
    void show();
    //void show2(); 当声明show2()时,@FunctionalInterface会报错。
}

4、Lambda的省略写法

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

  1. 小括号内的参数类型可以省略
  2. 如果小括号内有且仅有一个参数,则小括号可以省略
  3. 如果大括号内有且仅有一个语句,可以同时省略大括号、return语句和分号

首先新建2个interface。

@FunctionalInterface
public interface TeacherService {
    Integer getTeacher(String name);
}
@FunctionalInterface
public interface StudentService {
    String getStudent(String name, Integer age);
}

然后使用Lambda表达式来实现这2个接口,演示完整写法与省略写法。

public class App {

    public static void getTeacher(TeacherService teacherService) {
        teacherService.getTeacher("李四");
    }

    public static String getStudent(StudentService studentService, String name, Integer age) {
        return studentService.getStudent(name, age);
    }

    public static void main(String[] args) {
        //teacher1
        getTeacher((String myName) -> {
            System.out.println("name:" + myName);
            return 666;
        });

        //teacher2
        getTeacher(myName -> {
            System.out.println("name:" + myName);
            return 666;
        });
        
        //student1写法
        String student1 = getStudent((String myName, Integer myAge) -> {
            return "姓名:" + myName+ ";年龄:" + myAge;
        }, "张三", 23);
        System.out.println("student1:" + student1);

        //student2写法,省略写法
        String student2 = getStudent((myName, myAge) -> "姓名:" + myName+ ";年龄:" + myAge, "王五", 35);
        System.out.println("student2:" + student2);
    }
}

App的getStudent方法需要3个参数:StudentService参数、String参数、Integer参数,因此在调用getStudent时需要传入这3个参数,此处使用Lambda表达式的方式来实现StudentService接口。
teacher1写法处,使用了Lambda表达式的完整写法:Lambda小括号传入了String类型的myName,返回Integer类型的值,这是其父接口TeacherService的getTeacher方法规定的。我们通过 Ctrl + 左键 点击 -> 跳转到TeacherService接口,便可查看getTeacher方法的出入参要求。
teacher1写法处,使用了Lambda表达式的省略写法:因为Lambda内只有一个参数,所以可以省略小括号。
student1写法处,使用了Lambda表达式的完整写法:传入了String myName, Integer myAge,并返回了String类型的结果。之所以入参需要String和Interger的参数,返回值需要String类型的结果,那是由StudentService接口的getStudent抽象方法决定的。因为Lambda表达式本质上是代替了匿名内部类,实现了StudentService接口并重写getStudent方法,因此要遵命父接口的规定。我们通过 Ctrl + 左键 点击 -> 跳转到StudentService接口,便可查看getStudent方法的出入参要求。
student2写法处,使用了Lambda表达式的省略写法:省略了Lambda表达式的参数类型、大括号及return关键字。

通过上面的例子,我们对Lambda表达式有了更清晰的认识。Lambda表达式简化了匿名内部类语法,Lambda表达式小括号内的入参,以及大括号内的出参,都是由它实现的父接口的抽象方法决定的。Lambda表达式的作用是实现了某个接口,因此,若想使用Lambda表达式,就要求方法的入参是某个接口,由Lambda来实现。

虽然Lambda表达式提供了省略写法,使我们的代码看起来更简洁,但是我却不建议过度使用省略写法,至少省略大括号和return关键字这点我是极度反对的,因为这会导致代码的可读性降低,给开发人员造成困扰。因此,省略写法要酌情考虑。


二、接口中新增的方法

1、JDK8中接口增强

在JDK8之前针对接口做了增强,在JDK8之前,接口中只能包含静态常量和抽象方法。

interface 接口名{
    静态常量;
    抽象方法;
}
public interface TeacherService {
    String name = "杰克";
    Integer getTeacher(String name);
}

JDK8之后对接口做了增加,接口中可以有默认方法静态方法

interface 接口名{
    静态常量;
    抽象方法;
    默认方法;
    静态方法;
}

2、默认方法

2.1、为什么要增加默认方法

在JDK8以前,接口中只能有抽象方法和静态变量,会存在以下问题:
如果接口中新增抽象方法,那么实现类都必须要重写这个抽象方法,不利于接口扩展。
比如,假设有个A接口,有数十个类实现了A接口并重写了它的所有抽象方法,这时因为业务拓展,需要在A接口中新增一个抽象方法,我们难道要在A接口的所有实现类(子类)中都重写这个新增的抽象方法吗?那代码量未免也太大了些!在JDK8以前,实现类是必须要重写父接口的所有抽象方法的,可是A接口新增的抽象方法所涉及的业务,未必就是实现类需要的,重写也就没有意义,那么有没有一种更好的方式,既然在A接口中增加方法,又不需要它的所有实现类重写呢?答案是肯定的,基于此,JDK8新增了默认方法。

2.2、接口默认方法的格式

接口中默认方法的语法格式是:

interface 接口名{
	修饰符 default 返回值类型 方法名{
		方法体;
	}
}

2.3、举例说明默认方法

我们举个例子类说明,首先声明一个动物接口,狗狗类和鸽子类都实现动物接口,并重写它的抽象方法。

public interface Animal {
    void eat();
}
public class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("狗狗会吃饭...");
    }
}
public class Pigeon implements Animal{
    @Override
    public void eat() {
        System.out.println("鸽子会吃饭...");
    }
}

再写一个main方法,测试eat方法。

public class TestInter {
    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.eat();
        Animal pigeon = new Pigeon();
        pigeon.eat();
    }
}

这是一个很简单的案例,控制台输出结果如下所示:

狗狗会吃饭...
鸽子会吃饭...

接下来,我们改写Animal接口。要在Animal接口中新增一个swim方法,我认为狗狗是会游泳的,而鸽子是不会游泳的,基于这种情况,狗狗类应该重写父接口的swim方法,鸽子类不应该重写swim方法。这种场景下,就要使用默认方法了!

public interface Animal {
    void eat();

    //游泳
    public default void swim() {
        System.out.println("除了少数动物外,大多数动物都应该是会游泳的。。。");
    }
}

狗狗会游泳,因此Dog类重写Animal接口的swim方法。

public class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("狗狗会吃饭...");
    }

    @Override
    public void swim(){
        System.out.println("狗狗不仅会游泳,而且技术相当不错的。。");
    }
}

鸽子不会游泳,所以代码不做任何改动。

public class Pigeon implements Animal{
    @Override
    public void eat() {
        System.out.println("鸽子会吃饭...");
    }
}

最后我们在main方法中测试并打印结果。

public class TestInter {
    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.eat();
        Animal pigeon = new Pigeon();
        pigeon.eat();

        //测试狗狗和鸽子的游泳
        dog.swim();
        pigeon.swim();
    }
}

输出结果如下所示:

狗狗会吃饭...
鸽子会吃饭...
狗狗不仅会游泳,而且技术相当不错的。。
除了少数动物外,大多数动物都应该是会游泳的。。。

我们发现,Dog类重写了Animal接口的swim方法,执行了Dog重写后的逻辑;而Pigeon类没有重写swim方法,执行了Animal的默认逻辑。

因此得出一个结论,接口中的默认方法有两种使用方式:

  1. 实现类直接调用接口的默认方法,参考Pigeon类。
  2. 实现类重写接口的默认方法,参考Dog类。

3、静态方法

JDK8中为接口新增了静态方法,作用也是为了接口的扩展

3.1、语法规则

顾名思义,所谓静态方法,就是被static修饰的方法,可以直接通过接口名.静态方法名直接调用,和类中的静态方法使用方式相同。需要注意的是,接口中的静态方法,不可以被实现类(子类)重写。

interface 接口名{
	修饰符 static 返回值类型 方法名{
		方法体;
	}
}

3.2、举例说明静态方法

首先,改写Animal接口,新增breathe静态方法。Dog类和Pigeon类没有任何修改。

public interface Animal {
    void eat();

    //游泳
    public default void swim() {
        System.out.println("除了少数动物外,大多数动物都应该是会游泳的。。。");
    }

    //呼吸
    public static void breathe() {
        System.out.println("任何动物都需要呼吸。。。");
    }
}

然后再main方法中测试静态方法,并观察效果。

public class TestInter {
    public static void main(String[] args) {
        //调用Animal的静态方法
        Animal.breathe();
    }
}

注意,Dog类和Pigeon类并没有重写新增的breathe静态方法,那是因为静态方法归父接口所有,实现类无法重写,也无法通过实现类调用静态方法。只能通过接口名.静态方法名的方式调用。

4、默认方法和静态方法的区别

  1. 默认方法通过对象实例调用,静态方法用过接口名调用。
  2. 默认方法可以被实现类继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法。
  3. 静态方法不能被实现类继承,实现类不能重写接口的静态方法,只能使用接口名调用。

三、函数式接口

1、函数式接口的由来

我们知道使用Lambda表达式的前提是需要有函数式接口(即被@FunctionalInterface修饰的接口),而Lambda表达式使用时不关心接口名、也不关心抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让Lambda表达式使用更多的方法,在JDK8中提供了大量常用的函数式接口。

我们通过一段代码,来理解函数式接口。

首先定义一个函数式接口,Operator:

//被@FunctionalInterface修饰的接口,就是函数式接口
@FunctionalInterface
public interface Operator {

    //某个操作,可以实现加法或者减法
    int operate(int[] arr);
}

然后定义一个调用Operator的地方:

public class Demo01Fun {
    //调用Operator函数式接口
    public static void fun1(Operator operator) {
        int[] arr = {1, 2, 3, 4};
        int operate = operator.operate(arr);
        System.out.println("operate = " + operate);
    }

    public static void main(String[] args) {
        //eg1:调用Operator接口实现相加法
        fun1((arr) -> {
            int sum = 0;
            for (int item : arr) {
                sum += item;
            }
            return sum;
        });
        
        //eg2:调用Operator接口实现减法
        fun1((arr) -> {
            int sum = 0;
            for (int item : arr) {
                sum -= item;
            }
            return sum;
        });
    }
}

最后输出结果,如下:

operate = 10
operate = -10

在上面的案例中,main方法使用Lambda表达式重写Operator函数式接口实现了相加与相减的数学运算。我们发现,加法与减法,两个截然相反的算法,竟然通过重写同一个抽象方法实现了。在实现数学运算的过程中,业务代码对Operator接口名和operate方法名毫不关心,只需要关注入参以及返回值就行,甚至只要入参是int数组类型、返回值是int类型的函数式接口,都可以代替Operator接口实现相加相减的数学运算,那么这就意味着Operator接口是一个可有可无的接口,白白增加了代码量。为了解决这种没必要的代码,JDK8提供了很多函数式接口供Java程序员使用,程序员们再也不必为了实现业务而重复定义多余的接口。

2、函数式接口介绍

在JDK8中为我们提供了函数式接口,主要是在rt.jar文件下的java.util.function包中。有了这些函数式接口,我们在使用Lambda表达式的时候,可以根据不同的业务场景使用不同的接口,不必再自己编写多余的函数式接口。

下面给大家罗列几个常用的函数式接口,更多接口大家可以去rt.jar文件下的java.util.function包下自行寻找。

2.1、Supplier

无参有返回值的接口,使用的时候需要指定一个泛型来定义方法的返回值类型。源码如下:

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

通过一则案例来演示Supplier的用法,利用Supplier实现数组排序,并返回最大值:

public class SupplierTest {
    private static void fun1(Supplier<Integer> supplier) {
        Integer integer = supplier.get();
        System.out.println("result = " + integer);
    }

    public static void main(String[] args) {
        fun1(() -> {
            int[] arr = {34, 2, 47, 15, 99, 67};
            //从小到大排序后返回最大值
            Arrays.sort(arr);
            return arr[arr.length - 1];
        });
    }
}

fun1()方法需要Supplier类型的参数,所以在main方法调用fun1()方法时,使用Lambda表达式实现了Supplier接口并重写了它的get方法,完成排序并返回最大值的功能。

2.2、Consumer

有参无返回值的接口,Supplier接口是用来生产数据的,而Consumer接口则是用来消费数据的,使用的时候需要指定一个泛型来定义方法的参数类型。源码如下:

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
}

Consumer函数式接口在日常开发中经常使用,比如集合的forEach循环时,如:

        List<Person> list = new ArrayList<>();
        list.add(new Person("周杰伦", 33));
        list.add(new Person("刘德华", 43));
        
        list.forEach(new Consumer<Person>() {
            @Override
            public void accept(Person o) {
                System.out.println("bb->" + o);
            }
        });

List利用Consumer实现了遍历后的业务逻辑,我们仿照List,实现大写转换成小写的业务,当然,你也可以根据自己的需求实现其他的业务逻辑。

public class ConsumerTest {
    private static void fun1(Consumer<String> action) {
        for(int i =0;i<10;i++){
            action.accept("HELLO WORLD...");
        }
    }

    public static void main(String[] args) {
        fun1((msg) -> {
            String s = msg.toLowerCase();
            System.out.println("转换为小写 :" + s);
        });
    }
}

2.3、Function

有参有返回值的接口,源码如下:

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}

2.4、Predicate

有参有返回值的接口,源码如下:

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
}

四、方法引用

1、为什么要用方法引用

1.1、Lambda冗余

Lambda表达式诞生之初便是为了解决匿名内部类语法冗余的问题,可是当Lambda表达式也出现冗余,又该如何解决呢?
请观看下列案例:

public class Demo1 {
    //调用Consumer的accept方法
    private static void printCount(Consumer<int[]> consumer) {
        int[] a = {10, 20, 30};
        consumer.accept(a);
    }

    //数组求和
    public static void getTotal(int arr[]) {
        int sum = 0;
        for (int item : arr) {
            sum += item;
        }
        System.out.println("数组相加之和:" + sum);
    }

    public static void main(String[] args) {
        //Lambda重写Consumer实现数组求和
        printCount((arr) -> {
            int sum = 0;
            for (int item : arr) {
                sum += item;
            }
            System.out.println("数组相加之和:" + sum);
        });
    }
}

在上述的案例中,main方法重写Consumer实现了数组求和,getTotal也实现了数组求和,这是两块业务逻辑完全一样的代码。那么能不能在man方法的Lambda表达式中直接使用getTotal方法的逻辑呢?这样的话岂不是节省了代码,答案是肯定的,为了解决这种现象,JDK8新增了方法引用。

1.2、解决Lambda冗余

我们改写上面的案例,来初步认识下方法引用。

public class Demo1 {
    //调用Consumer的accept方法
    private static void printCount(Consumer<int[]> consumer) {
        int[] a = {10, 20, 30};
        consumer.accept(a);
    }

    //数组求和
    public static void getTotal(int arr[]) {
        int sum = 0;
        for (int item : arr) {
            sum += item;
        }
        System.out.println("数组相加之和:" + sum);
    }

    public static void main(String[] args) {
        //Lambda使用方法引用,重写Consumer实现数组求和
        printCount(Demo1::getTotal);
    }
}

改写案例后,使用Demo1::getTotal代替了Labmda表达式,实现了同样的业务逻辑。
需要注意的是,这里并不是方法调用。通常情况下,调用getTotal方法是 Demo1.getTotal(new int[]{1, 2, 3}) 这样的写法,需要传一个int类型的数组,但是方法引用不同于过去的方法调用,因为 Demo1::getTotal 并没有传递任何参数,你可以把它理解成复制一块相同的代码,而不是调用。

1.3、匿名内部类 VS Lambda表达式 VS 方法引用

我觉得有必要给大家对比下匿名内部类、Lambda表达式和方法引用的语法区别,让大家对方法引用更清晰的认识。

public class Demo1 {

    //调用Consumer的accept方法
    private static void printCount(Consumer<int[]> consumer) {
        int[] a = {10, 20, 30};
        consumer.accept(a);
    }

    //数组求和
    public static void getTotal(int arr[]) {
        int sum = 0;
        for (int item : arr) {
            sum += item;
        }
        System.out.println("数组相加之和:" + sum);
    }

    public static void main(String[] args) {
        //eg1:调用getTotal方法
        Demo1.getTotal(new int[]{1, 2, 3});

        //eg2:匿名内部类重写Consumer实现数组求和
        printCount(new Consumer<int[]>() {
            @Override
            public void accept(int[] arr) {
                int sum = 0;
                for (int item : arr) {
                    sum += item;
                }
                System.out.println("数组相加之和:" + sum);
            }
        });

        //eg3:Lambda重写Consumer实现数组求和
        printCount((arr) -> {
            int sum = 0;
            for (int item : arr) {
                sum += item;
            }
            System.out.println("数组相加之和:" + sum);
        });

        //eg4:Lambda使用方法引用,重写Consumer实现数组求和
        printCount(Demo1::getTotal);
    }
}

Lambda产生的目的在于简化匿名内部类的写法,方法引用产生的目的在于简化Lambda的写法,我们继续通过之前的案例,来对比一下三者的语法区别:

eg1调用getTotal方法,传入int类型的数组;
eg2使用匿名内部类重写Consumer,实现了数组求和;
eg3使用Lambda表达式简化了匿名内部类的写法,重写Consumer,实现了数组求和,使代码更加简洁;
eg4是本次的重点,我们发现eg3的代码逻辑和getTotal方法的代码逻辑一模一样,没必要再写重复的代码,故而使用方法引用,直接引用了getTotal方法的逻辑,实现了同样的功能,最大程度地简化了Lambda的代码。

注意:方法引用和方法调用是两回事,不能混为一谈!

2、方法引用的格式

符号表示:::

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

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

常见的引用方式:

  1. 对象名::方法名
  2. 类名::静态方法名
  3. 类名::普通方法名
  4. 类名::new
  5. 数组::new

常用的引用方式就是这5种,我们一一举例说明。

2.1、对象名::方法名

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

通过下面这块代码演示:

    public static void main(String[] args) {
        Date now = new Date();

        //eg1
        Supplier<Long> supplier1 = new Supplier<Long>() {
            @Override
            public Long get() {
                return now.getTime();
            }
        };
        System.out.println(supplier1.get());

        //eg2
        Supplier<Long> supplier2 = () -> {
            return now.getTime();
        };
        System.out.println(supplier2.get());
    }

此处还没有使用方法引用代替Lambda表达式,我先用匿名内部类和Lambda表达式预热一下,以防大家看不懂后面的代码。

Supplier接口是java.util.function包下的函数式接口,eg1使用匿名内部类的方式重写了Supplier接口,eg2使用lambda表达式重写了Supplier接口。

在上面的代码中,我们发现eg2 Lambda表达式中重写Supplier接口时,方法体内使用了now.getTime()方法,因此,我们可以使用方法引用来代替Lambda表达式。

    public static void main(String[] args) {
        Date now = new Date();

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

方法引用的注意事项:

  1. 被引用的方法,参数要和函数式接口中的抽象方法参数一样。
  2. 当接口抽象方法有返回值时,被引用的方法也必须有返回值。

2.2、类名::静态方法名

    public static void main(String[] args) {
        //eg1 Lambda表达式
        Supplier<Long> supplier1 = () -> {
            return System.currentTimeMillis();
        };
        System.out.println(supplier1.get());

        //eg2 方法引用
        Supplier<Long> supplier2 = System::currentTimeMillis;
        System.out.println(supplier2.get());
    }

eg1使用Lambda表达式输出了当前毫秒值,在Lambda表达式的方法体中没有其他业务逻辑,和System类的currentTimeMillis静态方法业务逻辑完全一致,因此在eg2中使用方法引用,引用System的currentTimeMillis方法代替了Lambda表达式。

2.3、类名::普通方法名

    public static void main(String[] args) {
        Function<String, Integer> function1 = (s) -> {
            return s.length();
        };
        System.out.println(function1.apply("hello"));

        //通过方法引用来实现
        Function<String, Integer> function2 = String::length;
        System.out.println(function2.apply("hello world"));
    }

2.4、类名::new

由于构造器的名称和类名完全一致,所以构造器引用使用:: new 的格式使用。

    public static void main(String[] args) {
        Supplier<Person> supplier = () -> {
            return new Person();
        };
        System.out.println(supplier.get());

        //方法引用
        Supplier<Person> supplier2 = Person::new;
        System.out.println(supplier2.get());
    }

2.5、数组::new

    public static void main(String[] args) {
        Function<Integer, String[]> fun1 = (len) -> {
            return new String[len];
        };
        String[] a1 = fun1.apply(3);
        System.out.println("数组a1的长度是" + a1.length);

        //方法引用
        Function<Integer, String[]> fun2 = String[]::new;
        String[] a2 = fun2.apply(4);
        System.out.println("数组a2的长度是" + a2.length);
    }

3、方法引用小结

方法引用是对Lambda表达式符合特定情况下的一种缩写方式,它使得我们的Lambda表达式更加得精简,也可以理解为Lambda表达式的缩写形式,不过要注意的是方法引用只能引用已经存在的方法。


五、Stream API

1、Stream流的概念

Stream流是对Java集合(Collection)对象功能的增强,与Lambda表达式结合,可以提高编程效率。 Stream API能让我们快速完成许多复杂的操作,如筛选、查找、去重、统计、匹配等,逐渐代替之前for循环的写法。

2、Stream流的获取方式

2.1、根据Collection接口的stream默认方法获取

	public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        Stream<Object> stream = list.stream();

        Set<Object> set = new HashSet<>();
        Stream<Object> stream1 = set.stream();

        Vector<Object> vector = new Vector<>();
        Stream<Object> stream2 = vector.stream();

        Map<String, Object> map = new HashMap<>();
        Stream<String> stream3 = map.keySet().stream();
        Stream<Object> stream4 = map.values().stream();
    }

Collection的实现类,都可以通过stream方法获取Stream对象,比如List,Set等;Map不属于Collection,所以不能获取Stream对象,但是Map的Key和Value可以。

2.2、根据Stream的of方法

    public static void main(String[] args) {
        Stream<String> a1 = Stream.of("a1", "a2", "a3");
        Integer[] arr = {1, 2, 3, 4};
        Stream<Integer> arr1 = Stream.of(arr);
    }

Stream提供了of方法,可以操作数组,获取Stream对象。

3、Stream常用方法介绍

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

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

Stream注意事项:

  1. Stream只能操作一次
  2. Stream方法返回的是新的流
  3. Stream不调用终结方法,中间调用的函数拼接方法都不会调用(Stream调用必须以终结方法结尾才会执行中间调用的非终结方法)

3.1、forEach

forEach方法用来遍历流中的数据。

    public static void main(String[] args) {
        Stream<String> a1 = Stream.of("a1", "a2", "b1", "b2");
        a1.forEach(System.out::println);
    }

3.2、count

count方法用来统计流中的元素个数。

    public static void main(String[] args) {
        Stream<String> a1 = Stream.of("a1", "a2", "b1", "b2");
        long count = a1.count();
        System.out.println("a1流元素中的个数:"+count);
    }

3.3、filter

filter方法用来过滤数据,返回符合条件的数据。

    public static void main(String[] args) {
        Stream<String> a1 = Stream.of("a1", "a2", "b1", "b2");
        a1.filter((item) -> {
            return item.contains("a");
        }).forEach(System.out::println);
		
		//缩写
        Stream<String> a2 = Stream.of("a1", "a2", "b1", "b2");
        a2.filter(item -> item.contains("b")).forEach(System.out::println);
    }

3.4、limit

limit方法可以对流进行截取处理,只取前n个数据。

    public static void main(String[] args) {
        Stream<String> a1 = Stream.of("a1", "a2", "b1", "b2");
        a1.limit(3).forEach(System.out::println);
    }

3.5、skip

skip方法可以跳过前n个元素。

    public static void main(String[] args) {
        Stream<String> a1 = Stream.of("a1", "a2", "b1", "b2");
        a1.skip(3).forEach(System.out::println);
    }

3.6、map

如果需要将流中的元素映射到另一个流中,可以使用map方法。

    public static void main(String[] args) {
        Stream<String> a1 = Stream.of("1", "2", "3", "4", "5", "6", "7");
        a1.map(msg -> {
            return Integer.parseInt(msg);
        }).forEach(System.out::println);

        //缩写形式
        //a1.map(Integer::parseInt).forEach(System.out::println);
    }

3.7、sorted

sorted方法可以对流数据排序。

    public static void main(String[] args) {
        Stream<String> a1 = Stream.of("2", "1", "3", "7", "4", "6", "5");
        a1.sorted(((o1, o2) -> {
            return Integer.parseInt(o2) - Integer.parseInt(o1);
        })).forEach(System.out::println);

        //缩写形式
        //a1.sorted(((o1, o2) -> Integer.parseInt(o2) - Integer.parseInt(o1))).forEach(System.out::println);
    }

3.8、distinct

如果要去掉重复数据,可以使用distinct方法。

    public static void main(String[] args) {
        Stream<String> a1 = Stream.of("2", "1", "3", "3", "3", "6", "5");
        a1.distinct().forEach(System.out::println);
    }

对于某个实体类,如果想根据某个字段去重,需要重写实体类的equals方法和hashCode方法。
下例根据Person的name属性实现去重。

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
    
    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}
public class demo8 {
    public static void main(String[] args) {
        Stream<Person> personStream = Stream.of(new Person("张三", 23),
                new Person("李四", 23),
                new Person("张三", 24));
        personStream.distinct().forEach(System.out::println);
    }
}

输出结果如下:

Person{name='张三', age=23}
Person{name='李四', age=23}

结论: Stream流中的distinct方法对于基本数据类型是可以直接去重的,但是对于自定义类型,需要重写hashCode和equals方法来移除重复元素。

3.9、reduce

如果需要将所有数据归纳得到一个数据,可以使用reduce方法。

    public static void main(String[] args) {
        //求和
        Stream<Integer> a1 = Stream.of(2, 10, 15, 50);
        Integer sum = a1.reduce(0, (x, y) -> {
            System.out.println("x:" + x + ";y=" + y);
            return x + y;
        });
        System.out.println("sum:" + sum);

        System.out.println("**********分隔符*******");

        //求最大值
        Stream<Integer> a2 = Stream.of(23, 65, 14, 45);
        Integer max = a2.reduce(0, (x, y) -> {
            return x > y ? x : y;
        });
        System.out.println("max:" + max);
    }

输出如下:

x:0;y=2
x:2;y=10
x:12;y=15
x:27;y=50
sum:77
**********分隔符*******
max:65

结合输出结果,分析得出:第一次循环时,会把默认值赋值给x,之后每次循环会将上一次操作的结果赋值给x。y就是每次从流中获取的元素。

4、数据收集

4.1、collect

通过collect方法,我们可以把流中的数据,收集到集合中。

    public static void main(String[] args) {
        //Stream转List
        List<String> list = Stream.of("aaa", "bbb", "ccc", "aaa")
                .collect(Collectors.toList());

        //Stream转ArrayList
        ArrayList<String> arrayList = Stream.of("aaa", "bbb", "ccc", "aaa")
                .collect(Collectors.toCollection(() -> new ArrayList<>()));

        //Stream转Set
        Set<String> set = Stream.of("aaa", "bbb", "ccc", "aaa")
                .collect(Collectors.toSet());

        //Stream转HashSet
        HashSet<String> hashSet = Stream.of("aaa", "bbb", "ccc", "aaa")
                .collect(Collectors.toCollection(() -> new HashSet<>()));
    }

如果想把流中的数据收集到Map中,也是可以的。

    public static void main(String[] args) {
        List<Student> list = new ArrayList<>();
        list.add(new Student("张三", 15, 170));
        list.add(new Student("李四", 18, 174));
        list.add(new Student("王五", 21, 173));
		
		//收集到Map,注意不能出现相同的key
        Map<String, Student> collect = list.stream().collect(Collectors.toMap(Student::getName, item -> item));
        System.out.println(collect);
    }

输出如下:

{李四=Student{name='李四', age=18, height=174}, 张三=Student{name='张三', age=15, height=170}, 王五=Student{name='王五', age=21, height=173}}

需要注意的是,如果在收集到Map的过程中,出现相同的key,是会报错的。需要采用如下方式:

    public static void main(String[] args) {
        List<Student> list = new ArrayList<>();
        list.add(new Student("张三", 15, 170));
        list.add(new Student("张三", 15, 172));
        list.add(new Student("李四", 18, 174));
        list.add(new Student("王五", 21, 173));

		//收集到Map,可以出现相同的key
        Map<String, Student> collect = list.stream().collect(Collectors.toMap(Student::getName, item -> item,(k, v) -> k));
        System.out.println(collect);
    }

4.2、toArray

通过toArray方法,我们可以把流中的数据,收集到数组中。

    public static void main(String[] args) {
        //默认转成Object类型的数组
        Object[] objects = Stream.of("aaa", "bbb", "ccc", "aaa")
                .toArray();

        //使用匿名内部类转成String类型的数组
        String[] strings = Stream.of("aaa", "bbb", "ccc", "aaa")
                .toArray(new IntFunction<String[]>() {
                    @Override
                    public String[] apply(int value) {
                        return new String[value];
                    }
                });

        //使用Lambda转成String类型的数组
        String[] strings2 = Stream.of("aaa", "bbb", "ccc", "aaa")
                .toArray((value -> new String[value]));

        //使用方法引用转成String类型的数组
        String[] strings3 = Stream.of("aaa", "bbb", "ccc", "aaa")
                .toArray((String[]::new));
    }

5、数据聚合与分组

为了方便演示,先定义一个学生类:

//学生类
public class Student {

    //姓名
    private String name;

    //年龄
    private Integer age;

    //身高
    private Integer height;

	//省略set、get方法等
}

5.1、数据聚合

在一个集合中,如果想根据某个字段,查询出最大值、或平均值,不再需要for循环,Stream提供了更简洁的方式:

public static void main(String[] args) {
        List<Student> list = new ArrayList<>();
        list.add(new Student("张三", 15, 170));
        list.add(new Student("张三", 16, 175));
        list.add(new Student("张三", 16, 173));
        list.add(new Student("李四", 18, 174));
        list.add(new Student("李四", 18, 168));
        list.add(new Student("李四", 20, 178));
        list.add(new Student("王五", 21, 173));

        //查询年龄最大的学生
        Optional<Student> maxAgeStudent = list.stream()
                .collect(Collectors.maxBy((o1, o2) -> o1.getAge() - o2.getAge()));
        System.out.println("年龄的最大值:" + maxAgeStudent.get().getAge());

        System.out.println("**************分隔符*************");

        //查询年龄的平均数
        Double avg = list.stream().collect(Collectors.averagingLong(item -> item.getAge()));
        System.out.println("年龄的平均值:" + avg);
    }

输出如下:

年龄的最大值:21
**************分隔符*************
年龄的平均值:17.714285714285715

5.2、数据分组

Stream提供了对流中数据分组的方式,利用它,可以根据某个字段进行分组操作。

    public static void main(String[] args) {
        List<Student> list = new ArrayList<>();
        list.add(new Student("张三", 15, 170));
        list.add(new Student("张三", 16, 175));
        list.add(new Student("张三", 16, 173));
        list.add(new Student("李四", 18, 174));
        list.add(new Student("李四", 18, 168));
        list.add(new Student("李四", 20, 178));
        list.add(new Student("王五", 21, 173));

        //按照学生姓名进行分组
        Map<String, List<Student>> collect = list.stream().collect(Collectors.groupingBy(item -> item.getName()));
        collect.forEach((key, value) -> {
            System.out.println("名字是 " + key + " 的学生有:");
            value.forEach(student -> {
                System.out.println("\t" + student.toString());
            });
            System.out.println("-----------------");
        });

        System.out.println("**************分隔符*************");

        //先按照学生姓名分组,再按照年龄分组
        Map<String, Map<Integer, List<Student>>> collect1 = list.stream()
                .collect(Collectors.groupingBy(Student::getName, Collectors.groupingBy(Student::getAge)));
        collect1.forEach((key, value) -> {
            System.out.println("名字是 " + key + " 的学生有:");
            value.forEach((k1, v1) -> {
                System.out.println("\t年龄是 " + k1 + " 的学生有:");
                v1.forEach(student -> {
                    System.out.println("\t\t" + student.toString());
                });
            });
            System.out.println("-----------------");
        });
    }

输出如下:

名字是 李四 的学生有:
	Student{name='李四', age=18, height=174}
	Student{name='李四', age=18, height=168}
	Student{name='李四', age=20, height=178}
-----------------
名字是 张三 的学生有:
	Student{name='张三', age=15, height=170}
	Student{name='张三', age=16, height=175}
	Student{name='张三', age=16, height=173}
-----------------
名字是 王五 的学生有:
	Student{name='王五', age=21, height=173}
-----------------
**************分隔符*************
名字是 李四 的学生有:
	年龄是 18 的学生有:
		Student{name='李四', age=18, height=174}
		Student{name='李四', age=18, height=168}
	年龄是 20 的学生有:
		Student{name='李四', age=20, height=178}
-----------------
名字是 张三 的学生有:
	年龄是 16 的学生有:
		Student{name='张三', age=16, height=175}
		Student{name='张三', age=16, height=173}
	年龄是 15 的学生有:
		Student{name='张三', age=15, height=170}
-----------------
名字是 王五 的学生有:
	年龄是 21 的学生有:
		Student{name='王五', age=21, height=173}
-----------------

6、数据分区

可以通过Collectors.partitioningBy()方法,把流中的数据依据某个字段分成true和false两个列表。该效果和Collectors.groupingBy()类似。

    public static void main(String[] args) {
        List<Student> list = new ArrayList<>();
        list.add(new Student("张三", 15, 170));
        list.add(new Student("张三", 16, 175));
        list.add(new Student("张三", 16, 173));
        list.add(new Student("李四", 18, 174));
        list.add(new Student("李四", 18, 168));
        list.add(new Student("李四", 20, 178));
        list.add(new Student("王五", 21, 173));

        //根据年龄来分区
        Map<Boolean, List<Student>> collect = list.stream().collect(Collectors.partitioningBy(item -> item.getAge() > 18));
        collect.forEach((key, value) -> {
            System.out.println("分区是 " + key + " 的学生有:");
            value.forEach(student -> {
                System.out.println("\t" + student.toString());
            });
            System.out.println("-----------------");
        });
    }

输出如下:

分区是 false 的学生有:
	Student{name='张三', age=15, height=170}
	Student{name='张三', age=16, height=175}
	Student{name='张三', age=16, height=173}
	Student{name='李四', age=18, height=174}
	Student{name='李四', age=18, height=168}
-----------------
分区是 true 的学生有:
	Student{name='李四', age=20, height=178}
	Student{name='王五', age=21, height=173}
-----------------

7、数据拼接

使用Collectors.joining()方法,可以把流中的数据拼接到一个字符串中。下例读取集合中的名字,拼接到一个字符串中,使用、分隔:

    public static void main(String[] args) {
        List<Student> list = new ArrayList<>();
        list.add(new Student("张三", 15, 170));
        list.add(new Student("李四", 20, 178));
        list.add(new Student("王五", 21, 173));

        //读取集合中的姓名,拼接到一个字符串中,使用、分隔
        String collect = list.stream().map(item -> item.getName())
                .collect(Collectors.joining("、"));
        System.out.println("拼接后的结果:" + collect);
    }
拼接后的结果:张三、李四、王五

8、并行流

之前使用的Stream流是单线程的,为了提高流的处理效率,Stream提供了并行流,多线程地处理数据,提升效率。下面分别演示并行流和串行流。

8.1、串行流

Stream默认的是串行流,单线程的处理方式。请看下面的案例:

    public static void main(String[] args) {
        Stream.of("a", "b", "c", "d", "a")
                //随便使用filter方法,为了给大家看输出的线程名
                .filter(item -> {
                    System.out.println("当前线程名:" + Thread.currentThread().getName() + ";item:" + item);
                    return item.equals("a");
                }).count();
    }

输出如下:

当前线程名:main;item:a
当前线程名:main;item:b
当前线程名:main;item:c
当前线程名:main;item:d
当前线程名:main;item:a

在上面的案例中,我们输出了线程名,发现是一样的,这证明Stream默认是单线程的。

8.2、并行流

可以通过2种方式开启并行流:

  1. Stream的parallel()方法。
  2. Collection的parallelStream()方法。
    public static void main(String[] args) {
        Stream.of("a", "b", "c", "d", "a")
                .parallel()
                //随便使用filter方法,为了给大家看输出的线程名
                .filter(item -> {
                    System.out.println("当前线程名:" + Thread.currentThread().getName() + ";item:" + item);
                    return item.equals("a");
                }).count();
                
        System.out.println("-------------分割线-----------");
        
        List<String> list = new ArrayList<>();
        list.add("H");
        list.add("I");
        list.add("J");
        list.parallelStream()
        	.filter(item -> {
            System.out.println("当前线程名:" + Thread.currentThread().getName() + ";item:" + item);
            return item.equals("a");
        }).count();
    }

输出如下:

当前线程名:main;item:c
当前线程名:ForkJoinPool.commonPool-worker-9;item:b
当前线程名:ForkJoinPool.commonPool-worker-11;item:a
当前线程名:ForkJoinPool.commonPool-worker-2;item:a
当前线程名:ForkJoinPool.commonPool-worker-4;item:d
-------------分割线-----------
当前线程名:main;item:I
当前线程名:ForkJoinPool.commonPool-worker-2;item:J
当前线程名:ForkJoinPool.commonPool-worker-4;item:H

本文只罗列了一部分Stream API,大家可以参考另一篇文章,了解更多API用法。Java Stream流详解


六、Optional类

Optional是用来存储对象的容器,使用这个容器可以避免空指针异常。

1、为什么使用Optional

观察下列代码,在没有Optional类之前,为了避免空指针异常,需要在代码中使用 if 来判断,不够简洁。

    public static void main(String[] args) {
        String name = null;
        if (name != null) {
            System.out.println("name的长度是:" + name.length());
        } else {
            System.out.println("name为空");
        }
    }

2、创建Optional容器的方式

Optional提供了3个静态方法来创建Optional容器:

  1. Optional.of(T t) of()方法需要传入一个参数,且参数不能为null,否则会被空指针异常。
  2. Optional.empty() empty()方法用来创建一个空的Optional容器,不需要传入参数。
  3. Optional.ofNullable(T t) ofNullable()方法是of()和empty()的整合版,需要传入一个参数,允许参数为空。
    public static void main(String[] args) {
        //使用of方法创建Optional对象
        Optional<String> jack = Optional.of("jack");

        //使用empty方法创建Optional对象
        Optional<Object> empty = Optional.empty();

        //使用ofNullable方法创建Optional对象
        Optional<String> tom = Optional.ofNullable("Tom");
        Optional<String> o = Optional.ofNullable(null);
    }

ofNullable()方法会根据传入的参数是否为null,调用of()或empty(),因此更加灵活,工作中建议使用ofNullable()来创建Optional对象。

3、Optional的常用方法

3.1、get方法

gat用来获取Optional容器中的对象。

    public static void main(String[] args) {
        String name = "jack";
        //使用of方法创建Optional对象
        Optional<String> nameOpt = Optional.ofNullable(name);
        System.out.println("name的值是:" + nameOpt.get());
    }
name的值是:jack

如果Optional容器中的对象是null,get时会抛出 No value present 错误提示。因此,get方法常和isPresent方法一起用。

3.2、isPresent方法

isPresent方法用来判断容器中的值是否为null,如果有值返回true,无值返回false。

    public static void main(String[] args) {
        String name = null;
        Optional<String> nameOpt = Optional.ofNullable(name);
        if (nameOpt.isPresent()) {
            System.out.println("name的值是:" + nameOpt.get());
        } else {
            System.out.println("nameOpt是一个空Optional对象");
        }
    }
nameOpt是一个空Optional对象

3.3、orElse方法

orElse会返回容器中的值,如果值为null,orElse会返回一个默认值。

    public static void main(String[] args) {
        Optional<String> o1 = Optional.ofNullable("jack");
        Optional<String> o2 = Optional.ofNullable(null);

        String q1 = o1.orElse("默认值");
        String q2 = o2.orElse("默认值");

        System.out.println("o1的值:" + q1);
        System.out.println("o2的值:" + q2);
    }
o1的值:jack
o2的值:默认值

3.4、orElseGet方法

orElseGet与orElse作用相同,唯一的区别在于orElseGet需要传入Supplier函数式接口,可以使用Lambda表达式来入参。当业务逻辑比较复杂时,可以使用orElseGet方法来代替orElse方法。

    public static void main(String[] args) {
        Optional<String> o = Optional.ofNullable(null);
        String s = o.orElseGet(() -> {
            //其他业务逻辑
            return "默认值";
        });
        System.out.println("o的值是:" + s);
    }
o的值是:默认值

3.5、ifPresent方法

    public static void main(String[] args) {
        Optional<String> op1 = Optional.ofNullable("jack");
        //如果容器有值,就执行某些操作
        op1.ifPresent((s) -> {
            System.out.println(s + "666");
        });
    }
jack666

七、新时间日期API

1、常用API介绍

JDK8中增加了一套全新的日期时间API,是线程安全的,位于java.time包下,下面是一些关键类:

  1. LocalDate:表示日期,包含年月日,格式为2022-12-18
  2. LocalTime:表示时间,包含时分秒,格式为16:38:54.158549300
  3. LocalDataTime:表示日期时间,包含年月日时分秒,格式为2018-09-06T15:33:56.750
  4. DataTimeFormatter:日期时间格式化类
  5. Instant:时间戳,表示一个特定的时间瞬间
  6. Duration:用于计算2个时间(LocalTime,时分秒)的距离
  7. Period:用于计算2个日期(LocalDate,年月日)的距离
  8. ZonedDateTime:包含时区的时间

Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公历。平年有365天,闰年有366天。此外Java8还提供了4套其他历法,分别是:

  1. ThaiBuddhistDate:泰国佛教历
  2. MinguoDate:中华民国历
  3. JapaneseDate:日本历
  4. HijrahDate:伊斯兰历

2、日期时间类的常用操作

2.1 LocalDate类

    public static void main(String[] args) {
        //创建指定的日期
        LocalDate date1 = LocalDate.of(2022, 3, 1);
        System.out.println("date1:" + date1);

        //得到当前的日期
        LocalDate date2 = LocalDate.now();
        System.out.println("date2:" + date2);

        //根据LocalDate对象获取对应的日期信息
        System.out.println("year:"+date2.getYear());
        System.out.println("month:"+date2.getMonthValue());
        System.out.println("day:"+date2.getDayOfMonth());
    }
date1:2022-03-01
date2:2022-12-19
year:2022
month:12
day:19

2.1 LocalTime类

    public static void main(String[] args) {
        //创建指定时间
        LocalTime time1 = LocalTime.of(10, 35, 54);
        System.out.println("time1:" + time1);

        //得到当前时间
        LocalTime time2 = LocalTime.now();
        System.out.println("time2:" + time2);

        //根据LocalTime对象获取对应的时间信息
        System.out.println("hour:" + time2.getHour());
        System.out.println("minute:" + time2.getMinute());
        System.out.println("second:" + time2.getSecond());
    }
time1:10:35:54
time2:21:02:45.501
hour:21
minute:2
second:45

2.1 LocalDateTime类

    public static void main(String[] args) {
        //获取指定的日期时间
        LocalDateTime dateTime1 = LocalDateTime.of(2022, 4, 5, 13, 43, 40);
        System.out.println("dateTime1:" + dateTime1);

        //获取当前的日期时间
        LocalDateTime dateTime2 = LocalDateTime.now();
        System.out.println("dateTime2:" + dateTime2);

        //根据LocalDateTime获取数据
        System.out.println("year:" + dateTime2.getYear());
        System.out.println("month:" + dateTime2.getMonthValue());
        System.out.println("day:" + dateTime2.getDayOfMonth());
        System.out.println("hour:" + dateTime2.getHour());
        System.out.println("minute:" + dateTime2.getMinute());
        System.out.println("second:" + dateTime2.getSecond());
    }
dateTime1:2022-04-05T13:43:40
dateTime2:2022-12-19T21:03:28.279
year:2022
month:12
day:19
hour:21
minute:3
second:28

3、修改日期时间

3.1、withXXX方法,修改为指定的日期时间

LocalDateTime的withYear方法用来修改年份,还可以使用withMonth修改月份,withHour修改小时等…此处不一一演示了。withXXX方法,会返回一个新的LocalDateTime对象,新老LocalDateTime对象之间数据不相同。

    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("当前时间:" + now);
        LocalDateTime dateTime = now.withYear(1995).withMonth(6).withDayOfMonth(8);
        System.out.println("修改年份后的时间:" + dateTime);
    }

输出如下:

当前时间:2022-12-20T21:25:15.224
修改年份后的时间:1995-06-08T21:25:15.224

3.2、plusXXX/minusXXX,加减日期时间

    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("当前时间:" + now);
        LocalDateTime dateTime = now.plusYears(10).minusMonths(1);
        System.out.println("修改后的时间:" + dateTime);
    }

输出如下:

当前时间:2022-12-20T21:27:18.773
修改后的时间:2032-11-20T21:27:18.773

4、日期时间比较

LocalDateTime提供了isAfter、isBefore和isEqual方法,用来比较两个日期时间的大小,返回布尔值。

    public static void main(String[] args) throws InterruptedException {
        //获取当前时间dateTime1
        LocalDateTime dateTime1 = LocalDateTime.now();

        //睡眠2秒,获取当前时间dateTime2
        Thread.sleep(2000);
        LocalDateTime dateTime2 = LocalDateTime.now();

        System.out.println("dateTime1在dateTime2之后吗?答案:" + dateTime1.isAfter(dateTime2));//false
        System.out.println("dateTime1在dateTime2之前吗?答案:" + dateTime1.isBefore(dateTime2));//true
        System.out.println("dateTime1与dateTime2相等吗?答案:" + dateTime1.isEqual(dateTime2));//false
    }
dateTime1在dateTime2之后吗?答案:false
dateTime1在dateTime2之前吗?答案:true
dateTime1与dateTime2相等吗?答案:false

5、日期格式化

5.1、把日期时间对象转成字符串

    public static void main(String[] args) throws InterruptedException {
        //获取当前时间
        LocalDateTime now = LocalDateTime.now();

        //使用默认格式
        DateTimeFormatter dtf1 = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
        String format = now.format(dtf1);
        System.out.println("默认格式:"+format);

        //自定义格式
        DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String format1 = now.format(dtf2);
        System.out.println("自定义格式:"+format1);
    }

输出如下:

默认格式:2022-12-21T21:43:00.052
自定义格式:2022-12-21 21:43:00

5.2、把字符串转成日期时间对象

    public static void main(String[] args) throws InterruptedException {
        String date= "1995-06-08 13:43:26";

        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime parse = LocalDateTime.parse(date, dtf);
        System.out.println(parse);
    }

输出如下:

1995-06-08T13:43:26

6、Instant类(时间戳)

Instant类内部保存了从1970年1月1日 00:00:00以来的秒和纳秒。

6.1、Instance获取毫米值

    public static void main(String[] args) {
        Instant now = Instant.now();
        System.out.println("now = " + now);

        long epochSecond = now.getEpochSecond();
        long milli = now.toEpochMilli(); //和 System.currentTimeMillis() 获取的毫秒值相同

        System.out.println("当前时间秒:" + epochSecond);
        System.out.println("当前毫秒值:" + milli);
    }

输出如下:

now = 2022-12-21T14:11:38.076Z
当前时间秒:1671631898
当前毫秒值:1671631898076

6.2、获取某个时间点的毫秒值

    public static void main(String[] args) {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        //将 string 装换成带有 T 的国际时间,但是没有进行,时区的转换,即没有将时间转为国际时间,只是格式转为国际时间
        LocalDateTime parse = LocalDateTime.parse("1995-06-08 10:03:46", dateTimeFormatter);
        //+8 小时,offset 可以理解为时间偏移量
        ZoneOffset offset = OffsetDateTime.now().getOffset();
        //转换为 通过时间偏移量将 string -8小时 变为 国际时间,因为亚洲上海是8时区
        Instant instant = parse.toInstant(offset);
        long l = instant.toEpochMilli();
        System.out.println("毫秒值:"+l);
    }
毫秒值:802577026000

7、计算日期时间差

7.1、Duration计算时间差

    public static void main(String[] args) {
        LocalTime now = LocalTime.now();
        System.out.println("时间1:" + now);

        //22点48分59秒
        LocalTime of = LocalTime.of(23, 0, 0);
        System.out.println("时间2:" + of);

        Duration between = Duration.between(now, of);
        System.out.println("两时间相隔的小时数:" + between.toHours());
        System.out.println("两时间相隔的分钟数:" + between.toMinutes());
    }

输出如下:

时间1:21:26:37.159
时间2:23:00
两时间相隔的小时数:1
两时间相隔的分钟数:93

7.2、Period计算日期差

    public static void main(String[] args) {
        LocalDate now = LocalDate.now();
        LocalDate of = LocalDate.of(2022, 10, 15);

        System.out.println("日期1:" + now);
        System.out.println("日期2:" + of);

        Period between = Period.between(of, now);
        int months = between.getMonths();
        int days = between.getDays();

        System.out.println("两日期相隔 " + months +" 个月零 "+days+" 天");
    }

输出如下:

日期1:2022-12-22
日期2:2022-10-15
两日期相隔 2 个月零 7 天

如果要计算两日期相隔的总天数:

    public static void main(String[] args) {
        LocalDate start = LocalDate.of(2022, 10, 1);//开始时间
        LocalDate end = LocalDate.now();//当前时间
        Long cha = end.toEpochDay() - start.toEpochDay();//天数差

        System.out.println("日期1:" + start);
        System.out.println("日期2:" + end);
        System.out.println("量日期相隔的总天数:" + cha);
    }

输出如下:

日期1:2022-10-01
日期2:2022-12-22
量日期相隔的总天数:82

8、新API与数据库的对应关系

在数据库中,时间类型有date、datetime、time、timestamp。很多人不知道在Java中如何去接收,我的建议是:
date类型使用LocalDate接收;
time类型使用LocalTime接收;
datetime类型使用LocalDateTime接收;
timestamp类型使用LocalDateTime接收。


总结

JDK8新特性到这里就结束了,还有某些API没有介绍到,需要大家在工作中慢慢积累。加油!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小Y先生。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值