java8新特性,不一样的全新体验(手码万字长文)

515 阅读31分钟

1995 年 5 月 23 日,Oak 语言改名为 Java,然后有了那句著名的口号——“Write Once,Run Anywhere”。

1996 年 JDK1.0发布了,标志着一个新的时代已经到来。

1998 年 Java 迎来 1.2 版本,有此诞生了我熟知的 J2ME、J2SE 以及 J2EE。ME 主要用于移动端(还记得大明湖畔诺基亚 S40 吗?不小心暴露了年龄),而 SE 做为标准版主要用于桌面程序,EE 则主要针对企业应用所打造,也是我们主要的研究对象。1.2 的发布标志着Java开始普及。

2000 年 1.3 发布,并得到了 Mac OS X 工业标准的支持。

2002 年,也就是中国首次进入世界杯(也是至今唯一的一次,扎铁了老心)的那年。这一年 1.4 问世,这一版本极大的丰富了 Java 的类库,如:XML、Socket、全新的 I/O API、正则、日志、断言等如今我们耳熟能详的功能。

2004 年,我们刚刚经历完非典,Java 也迎来了重要更新,为了突出这次更新的重要性,命名方式从原来的 J2XE 1.X 变成了现在的 JavaXE X,于是有了 JavaSE 5。这一版本增加了泛型、自动拆装箱、循环增强(foreach)、枚举、注解、可变参数等,堪称有史以来最重大的更新,Java 5 应该有姓名!

接下来(2006年)又发布了 Java 6 ,据说这个版本在国内很流行(不要告诉我你们公司还在用 JDK1.6 )。同年,发生了一件大事——Java 开源了!我爱开源!

2009 年 Oracle(就是前段时间裁员 N+6 的那家公司)收购了 Sun , Java 从此跟了后妈,过着寄人篱下的日子。

后来,在 2011 年发布了 Java 7 。

三年后发布了 Java 8 ,为我们带来了 Lambda 表达式、Stream 以及新的日期时间 API 。Java 8 应该也是目前被使用最多的版本。

后来相继发布了 9、10、11、12、13,今年(2020 年)3 月 17 日 Oracle 发布了 JDK 14。

在Java8中产生了许多重大更新。

1、Lambda表达式

1.1 初识lambda表达式

Lambda 是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

上述的简介是不是听着有些晕头转向,别急,下面通过一个简单的例子去说明:

需求:有一张员工表,两个字段,name和age,如果我们需要获取年龄大于18岁的员工,有多少种做法呢?

 //模仿从数据库获取的数据源
List<Employee> list= Arrays.asList(
           new Employee("张三","15"),
           new Employee("李四","45"),
           new Employee("王五","55"),
           new Employee("小刘","65")
    );

方案一:遍历输出。缺点:当我需要修改需求,比如查找姓名为XXX的员工信息,得修改代码,其中大多代码重复,存在代码冗余。

@Test
    public void test03(){
        for (Employee employee:list) {
            if(Integer.parseInt(employee.getAge())>=35){
                System.out.println(employee);
            }
        }
    }

方案二:使用策略设计模式。

接口中定义一个查询方法,当我需要查询某个需求时,再定义一个实现类去实现此接口。根据不同的需求,传递的参数也不同,其相同之处在于:传递的实现类均实现了同一个方法,由于其方法名相同,故需要修改的代码很小

public interface FilterEmployee<T> {
    public boolean find(T t);
}
//实现类一
public class FilterEmployeeByName implements FilterEmployee<Employee> {
    @Override
    public boolean find(Employee employee) {
        if(employee.getName().contains("王")){
            return true;
        }
        return false;
    }
}
//实现类二
public class FilterEmployeeByAge implements FilterEmployee<Employee> {
    @Override
    public boolean find(Employee employee) {
        if(Integer.parseInt(employee.getAge())<=35){
            return true;
        }
        return false;
    }
}
 @Test
    public void test04(){
        List<Employee> lists = find(this.list, new FilterEmployeeByAge());
        for (Employee e:lists) {
            System.out.println(e);
        }
        //找到name中包含‘王’的员工信息
        List<Employee> list2 = find(this.list, new FilterEmployeeByName());
        for (Employee e:list2) {
            System.out.println(e);
        }
    }
//根据条件对数据集进行筛选
    public List<Employee> find(List<Employee>employees,FilterEmployee<Employee> t){
        ArrayList<Employee> list = new ArrayList<>();
        for(Employee e:employees){
            if(t.find(e)){
                list.add(e);
            }
        }
        return list;
    }

方案三:上述方式中每次都需要重新创建一个新的类,使用匿名内部类

  @Test
    public void test06(){
        List<Employee> lists = find(this.list, new FilterEmployee<Employee>() {
            @Override
            public boolean find(Employee employee) {
                if(employee.getName().contains("王")){
                    return true;
                }
                return false;
            }
        });
        for (Employee e:lists) {
            System.out.println(e);
        }
    }

优点:方案三相比方案二,好处在于当新的需求来到时,不需要重新创造一个对象去继承接口FilterEmployee,直接使用匿名类;相比方案一的好处在于更加灵活,代码冗余低。

缺点:可以发现其有用的代码仅仅为6-9行是业务逻辑判断。

方案四:使用lambda表达式。

 @Test
    public void test07(){
        List<Employee> lists = find(this.list,(e)-> Integer.parseInt(e.getAge())>=20);
        for (Employee e:lists) {
            System.out.println(e);
        }
    }
//根据条件对数据集进行筛选
public List<Employee> find(List<Employee>employees,FilterEmployee<Employee> t){
        ArrayList<Employee> list = new ArrayList<>();
        for(Employee e:employees){
            if(t.find(e)){
                list.add(e);
            }
        }
        return list;
    }

小总结:通过上述比较,我们可以发现lambda表达式相当于将对接口匿名类创建的一种简单的写法。

1.2、lambda表达式的规范

上述小案例讲述了lambda表达式的用法,那么其具体的语法规则的如何的呢?

/**
 * lambda表达式的基础语法:Java8中引入的新的操作符:“->”,该操作符称为箭头操作符或lambda操作符
 *                     将lambda表达式拆分成两部分:
 *           左侧:Lambda表达式的参数列表   对应接口中抽象方法的参数列表
 *           右侧:Lambda表达式中所需要执行的功能,即Lambda体
 * 语法格式一:
 *      接口中的抽象方法,无参数,无返回值
 *      ()->System.out.println("world");
 * 语法格式二:
 *      接口中的抽象方法,有一个参数,无返回值
 *      接口中有一个参数,若只有一个参数,小括号可以不写
 *      (t)-> System.out.println(t)
 *       t-> System.out.println(t)
 * 语法格式三:
 *      有多个参数,有返回值,并且Lambda体中有多条语句
 *          (x,y)->{
 *             System.out.println("helloworld");
 *             return Integer.compare(x,y);
 *         };
 * 语法格式四:
 *      有多个参数,有返回值,但是lambda体中只有一条语句
 *      那么return和{}都可以省略
 *      (x,y)->Integer.compare(x,y);
 * 语法格式五:
 *      Lambda表达式参数的数据类型可以省略不写,因为JVM编译器通过上下文推断出数据类型,即“类型推断”
 *      (Integer x,Integer y)->Integer.compare(x,y);
 * 左右遇一括号省
 * 左侧推断类型省
 *
 * Lambda表达式需要“函数式接口”的支持
 * 函数式接口:接口中只有一个抽象方法,称之为函数式接口
 * 使用注解@FunctionalInterface修饰,可以检查是否为函数式接口
 */

上面的子太多,看的晕头转向?别急,案例马上就到!

咱们先回顾一下上一步中案例的内容,记住一点:lambda表达式是原有的匿名内部类的简写方式,我们可以理解为新的语法糖。

**注:函数式接口:**接口中只有一个抽象方法,称之为函数式接口使用注解@FunctionalInterface修饰,可以检查是否为函数式接口

首先记住基础语法:“->”,该操作符称为箭头操作符或lambda操作符

将lambda表达式拆分成两部分:

左侧:Lambda表达式的参数列表 对应接口中抽象方法的参数列表

右侧:Lambda表达式中所需要执行的功能,即Lambda体

①、Lambda表达式:无参数无返回值

@Test
    public void test01(){
        int num=0;//在jdk1.7之前,必须是final,但是其实默认已经是final了,在内部类中无法修改其值
        //传统方式
        Runnable r=new Runnable() {
            @Override
            public void run() {
                System.out.println("hello"+num);
            }
        };
        r.run();
        System.out.println("----------------");
        //使用lambda表达式
        Runnable r1=()-> System.out.println("world"+num);
        r1.run();
    }

②、Lambda表达式:有参数,有返回值

public interface Consumer<T> {
    void accept(T t);
}
@Test
    public void test02(){
        Consumer<String> c=(t)-> System.out.println(t);
        c.accept("helloworld");
    }

③、Lambda表达式:有多个参数,有返回值

 @Test
    public void test03(){
        Comparator<Integer> com=(x,y)->{
            System.out.println("helloworld");
            return Integer.compare(x,y);
        };
    }

有没有疑问:(x,y)中没有指明参数类型,那么lambda表达式是如果知道的?

上述 Lambda 表达式中的参数类型都是由编译器推断得出的。 Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。 Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断” 。

通俗一点讲,就是在Comparator《Integer》中指明了参数的类型。

④、Lambda表达式:单参数并返回值

//需求:对一个数进行运算,没有指出做何运算
    @Test
    public void test05(){
        /**
         * 单参数并返回值
         *把这个lambda表达式看作匿名内部类,那等式左边部分
         *相当于该类的引用变量,而lambda体中则定义出了该接口的方法实现
         */
        LambdaFunc01<Integer> option=(num)->num*num;
        System.out.println(option.getvalue(100));
        Integer result = get(100, (num ->num+10 ));
        System.out.println(result);
    }
    public Integer get(Integer m,LambdaFunc01<Integer> t){
        return t.getvalue(m);
    }

⑤、Lambda小练习

功能一:根据员工的联络进行递减排序,如果年龄相同则比较姓名(多参数且有多个返回值)

功能二:处理字符串大小写转换(单参数且返回值)

功能三:处理参数的和与积

功能1:根据员工的联络进行递减排序,如果年龄相同则比较姓名(多参数且有多个返回值)

List<Employee> list= Arrays.asList(
           new Employee("张三","75"),
           new Employee("张三1","85"),
           new Employee("王五","55"),
           new Employee("李四","55"),
           new Employee("小刘","65")
   );

   /**
    * 需求:根据年龄进行递减排序,如果年龄相同则比较姓名
    * 多个参数且有返回值
    */
   @Test
   public void test01(){
       Collections.sort(list,(x,y)->{
           if(x.getAge().equals(y.getAge())){
               return  -x.getName().compareTo(y.getName());
           }else{
           return  -(Integer.parseInt(x.getAge())-Integer.parseInt(y.getAge()));
           }
       });
       list.stream().forEach(System.out::println);
   }

**注:**Collections.sort(List list, Comparator<? super T> c),其中lambda表达式相当于是对接口Comparator中【函数式接口】的compare方法的实现。

功能二:处理字符串大小写转换(单参数且返回值)

@FunctionalInterface//标明这是一个函数式接口
public interface LambdaFunc02<T>{
    public String getvalue(String str);
}
public String strHandle(String str,LambdaFunc02<String> t){
       return t.getvalue(str);
}
@Test
    public void test02(){
        String s = strHandle("   abcdef     ", (str) -> str.trim().toUpperCase());
        System.out.println(s);//ABCDEF
        String s1 = strHandle("abcdefghjk", (str) -> str.substring(2, 4));
        System.out.println(s1);//cd
        //使用内置接口
        Function<String,String>fun1=(str)->str.trim().toUpperCase();
        System.out.println(fun1.apply("   abcdef     "));//ABCDEF

    }

注:内置接口:java中内置的函数式接口,下午会讲。

功能三:处理参数的和与积

@FunctionalInterface
public interface LambdaFunc03<T,R>{
    public R getvalue(T t1,T t2);
}
//处理参数的和与积
    public Long op(Long l1,Long l2,LambdaFunc03<Long,Long>t){
        return t.getvalue(l1,l2);
    }
@Test
    public void test03(){
        System.out.println(op(100L,135L,(x,y)->x+y));
        System.out.println(op(100L,135L,(x,y)->x*y));
        //直接使用接口也可以,不需要下面的函数定义
        LambdaFunc03<Long,Long> t=(x,y)->x/y;
        System.out.println(t.getvalue(100L,10L));
        //使用内置接口
        BiFunction<Integer,Integer,Integer>fun=(x,y)->x*y;
        System.out.println(fun.apply(100,100));
    }

**小结:**lambda表达式是对函数式接口中的一种匿名类的实现方式,其lambda体相当于是实现了接口的方法。

1.3 Lambda内置核心接口

在上述案例中,我们有自定义下面三种接口,但是事实上这些接口的作用仅仅是定义了参数和返回值的类型、数量等, 因为我们所需要的仅仅是:这个函数式接口是否有返回值,形参有几个,具体接口是啥名字,方法是啥名字,我们不需要管因为其具体的实现内容在lambda体中实现。

@FunctionalInterface
public interface LambdaFunc01<T>{
    public Integer getvalue(Integer num );
}
@FunctionalInterface
public interface LambdaFunc02<T>{
    public String getvalue(String str);
}
@FunctionalInterface
public interface LambdaFunc03<T,R>{
    public R getvalue(T t1,T t2);
}

在Java8中的四大内置核心函数式接口:

/**

 * Consumer<T>:消费型接口
 *    void accept(T t)   传进去一个参数没有任何的返回结果
 * Supplier<T>:共给型接口
 *    T get()
 *
 * Function<T,R> :函数型接口
 *    R apply(T t)
 * Predicate<T>:断言型接口
 *    boolean test(T t)
 *
 */

通过下面的例子加深理解:

需求一:把字符串去掉空格并转为大写打印。

 @Test
    public void test01(){
        Consumer<String> con=(ch)-> {
            String s = ch.trim().toUpperCase();
            System.out.println(s);
        };
        con.accept("   abnd  ");
    }

需求二:生成一些数,将结果放在集合中。

 public List<Integer> getNumberList(Integer num, Supplier<Integer> t){
        List<Integer> list = new ArrayList<>();
        for (int i=0;i<num;i++){
            list.add(t.get());
        }
        return list;
    }

需求三:处理字符串。

public String stringHandle(String str, Function<String,String>t){
        return t.apply(str);
    }
    @Test
    public void test03(){
        String ch="      coderxz的博客     ";
        String s = stringHandle(ch, (str) -> str.trim().toUpperCase());
        System.out.println(s);
        System.out.println(stringHandle(ch,(str)->str.trim().substring(2,5)));
    }

需求四:将满足条件的字符串放入集合中。

  public List<String> filterString(List<String> list, Predicate<String> t){
        List<String> list1 = new ArrayList<>();
        for(String ch:list){
            if(t.test(ch)){
                list1.add(ch);
            }
        }
        return list1;
    }
 @Test
    public void test04(){
        List<String> list1 = new ArrayList<>();
        list1.add("张三");
        list1.add("ab");
        list1.add("1234");
        list1.add("12");
        //将长度大于3的过滤出来
        List<String> list = filterString(list1, (str) -> str.length() > 3);
        System.out.println(list);
    }

**小结:**lambda表达式相当于就是对函数式接口中方法的实现。

除了上述接口,还提供了其他多参数的接口:

函数式接口

1.4 Lambda方法引用(提高篇)

当要传递给Lambda体的操作,已经有实现的方法了,那么我们可以直接调用此方法的引用!【实现抽象方法的参数列表,必须与方法引用方法的参数列表保持一致】

方法引用:使用操作符 :: 将发那个发明和对象或类的名字分隔。

主要有三种语法格式:

对象::实例方法名

类::静态方法名

类::实例方法名

是不是对上述的使用前提条件感觉很绕,这样理解:lambda表达式相当于是实现接口吧,那么当我们使用方法引用作为lambda表达式的时候,这个方法引用肯定是调用了实例方法(或静态方法),那么调用的实例方法的参数、返回值必须与接口中的参数、返回值个数保持一致。还是有点难以理解?那么就看下面这段代码吧:

/**
     * 对象::实例方法名
     * 注意:lambda体中调用方法的参数列表与返回值类型,要与函数式接口中抽象方法的函数列表和返回值类型保持一致!
     * 比如说: void accept(T t)与 void println(T t)
     */ 
@Test
    public void test01(){
        Consumer<String> con=(x)-> System.out.println(x);
        con.accept("hello");

        PrintStream s=System.out;
        Consumer<String> con2=s::println;
        con2.accept("world");
    }

PrintStream中的方法: void println(String x),其参数个数、返回值类型均与接口Consumer中void accept(T t)相同。lambda体中调用方法的参数列表与返回值类型,要与函数式接口中抽象方法的函数列表和返回值类型保持一致!

如果还不能理解,再看看下面这段代码:

 @Test
    public void test02(){
        Employee employee = new Employee("小刘", "65");
        Supplier<String> sup=()-> employee.getName();
        System.out.println(sup.get());
        Supplier<String> sup2=employee::getAge;
        System.out.println(sup2.get());
    }

String getAge()与接口Supplier中的T get()。

下面的两种调用方式:

 /**
     * 类::静态方法名
     */
    @Test
    public void test03(){
        Comparator<Integer>com=(x,y)->Integer.compare(x,y);
        Comparator<Integer>com2=Integer::compare;
    }
    /**
     * 类::实例方法名
     */
    @Test
    public void test04(){
        BiPredicate<String,String>bp=(x,y)->x.equals(y);
        System.out.println(bp.test("hello","hello"));
        BiPredicate<String,String>bp2=String::equals;
        System.out.println(bp2.test("hello","hello1"));

    }

1.5 Lambda构造器引用(提高篇)

构造器引用:

ClassName :: new

注意:需要调用的构造器参数列表需要与函数式接口中的抽象方法的参数保持一致!

数组引用:type::new

对于数组的引用:

 @Test
    public void test04(){
        Function<Integer,String[]>fun=(x)->new String[x];
        String[] str = fun.apply(10);
        System.out.println(str.length);
        Function<Integer,String[]>fun2=String[]::new;
        String[] str2 = fun.apply(10);
        System.out.println(str2.length);
    }

对于自定义对象的引用:

 @Test
    public void test01(){
        Supplier<Employee> sup=()->new Employee();
        Employee employee = sup.get();
        //构造器引用
        /**
         * 疑问:此时Employee中有很多构造方法,那么调用的是哪一个呢?
         *      调用的是无参构造方法。
         *      因为lambda体中调用方法的参数列表与返回值类型,要与函数式接口中抽象方法的函数列表和返回值类型保持一致
         */
        Supplier<Employee>sip2=Employee::new;
    }

    /**
     * 此时调用的一个参数的构造方法
     * lambda体中调用方法的参数列表与返回值类型,要与函数式接口中抽象方法的函数列表和返回值类型保持一致
     */
    @Test
    public void test02(){
        Function<String,Employee>func1=(x)->new Employee(x);
        Function<String,Employee>func=Employee::new;
        Employee employee = func.apply("rxz");
        System.out.println(employee);
    }

2、Stream API

说到Stream便容易想到I/O Stream,而实际上,谁规定“流”就一定是“IO流”呢?在Java 8中,得益于Lambda所带 来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。

2.1 入门介绍

几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元 素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:

import java.util.ArrayList;
import java.util.List;
public class Demo01ForEach {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");
        for (String name : list) {
        	System.out.println(name);
        }
    }
}

这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。

循环遍历的弊端 :

Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行 了对比说明。现在,我们仔细体会一下上例代码,可以发现:

  • for循环的语法就是**“怎么做”**
  • for循环的循环体才是**“做什么”**

为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从 **第一个到最后一个顺次处理的循环。**前者是目的,后者是方式。

试想一下,如果希望对集合中的元素进行筛选过滤:

  1. 将集合A根据条件一过滤为子集B;

  2. 然后再根据条件二过滤为子集C。

可以进行多次遍历后筛选:

import java.util.ArrayList;
import java.util.List;
public class Demo02NormalFilter {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");
        List<String> zhangList = new ArrayList<>();
        //第一次筛选以‘张‘开始的name
        for (String name : list) {
            if (name.startsWith("张")) {
            	zhangList.add(name);
            }
        } 
        //以第一次筛选的结果作为第二次筛选的输入
        List<String> shortList = new ArrayList<>();
        for (String name : zhangList) {
            if (name.length() == 3) {
            	shortList.add(name);
            }
        } 
        for (String name : shortList) {
       		System.out.println(name);
        }
    }
}

这段代码中含有三个循环,每一个作用不同:

  1. 首先筛选所有姓张的人;
  2. 然后筛选名字有三个字的人;
  3. 最后进行对结果进行打印输出。

每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循 环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使 用另一个循环从头开始。 那,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?

下面来看一下借助Java 8的Stream API,如果优雅的写代码:

import java.util.ArrayList;
import java.util.List;
public class Demo03StreamFilter {
public static void main(String[] args) {
  List<String> list = new ArrayList<>();
  list.add("张无忌");
  list.add("周芷若");
  list.add("赵敏");
  list.add("张强");
  list.add("张三丰");
  list.stream()
      .filter(s ‐> s.startsWith("张"))
      .filter(s ‐> s.length() == 3)
      .forEach(System.out::println);
  }
}

直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:**获取流、过滤姓张、过滤长度为3、逐一打印。**代码 中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。

2.2、什么是Stream流

流(Stream) 到底是什么呢?是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。“集合讲的是数据,流讲的是计算! ”

注意:

  1. Stream不会存储元素。
  2. Stream不会改变元对象,相反,他们会返回一个持有结果的新Strean【类似建造者模式】。
  3. Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

Stream的操作三部曲

  • 创建Stream:一个数据源(如:集合、数组),获取一个流。
  • 中间操作:一个中间操作链,对数据源的数据进行处理。
  • 终止操作:一个终止操作,执行中间操作链,得到结果。

在这里插入图片描述

2.3、Stream的创建

①、通过Collection接口获取

  • default Stream stream() : 返回一个顺序流
  • default Stream parallelStream() : 返回一个并行流
//1.通过Collection系列集合提供的Stream()[串行流]方法或者parallelStream()[并行流]
        List<Integer>list=new ArrayList<>();
        Stream<Integer> stream1 = list.stream();

②、由数组创建流

Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:

  • static Stream stream(T[] array): 返回一个流
 //2.通过Arrays中的静态方法Stream()获取数组流,各种数组转为Stream
        int[] arr={1,2,3,4};
        IntStream stream2=  Arrays.stream(arr);

③、由值创建流

可以使用静态方法 Stream.of(), 通过显示值创建一个流。它可以接收任意数量的参数。

  • public static Stream of(T... values) : 返回一个流
 Stream<String> stream3 = Stream.of("a", "b");

④、由函数创建流:创建无限流

可以使用静态方法 Stream.iterate() 和Stream.generate(), 创建无限流。 【流的初始大小未固定】

  • ①、迭代

    public static Stream iterate(final T seed, finalUnaryOperator f)

  • ②、生成

    public static Stream generate(Supplier s)

/**
         * public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
         * 参数 seed:初始值
         * UnaryOperator<T> f函数式接口,单参数有返回值
         *
         */
		//①迭代
        Stream<Integer> stream4 = Stream.iterate(0, (x) -> x + 2);
        stream4.limit(10)
                .forEach(System.out::println);
        //②生成
        Stream.generate(()->Math.random())
                .limit(10)
                .forEach(System.out::println);

2.4、Stream的中间操作

多个中间操作可以连接起来形成一个流水线,==除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”== 。

①、筛选与切片

  • filter-接受lambda表达式,从流中排除某些元素;
  • limit-截断流,试元素不超过给定的数量;
  • skip(n)-跳过元素,返回一个扔掉了前n个元素的流。若流中的元素不足n个,则返回一个空流,与limit(n)互补;
  • distinct-筛选,通过流所生成的hashCode()和equls()去掉重复元素。
 @Test
    public void test01(){
        list.stream()
                .filter((x)->Integer.parseInt(x.getAge())>50)
                .limit(1)
                .forEach(System.out::println);
        //终止操作:最后一次性执行全部内容,并不会先执行某句话再执行
    }
    @Test
    public void test02(){
        list.stream()
                .skip(2)//跳过前两个
                .distinct()//去重   其中对象得重写hashcode和equls
                .forEach(System.out::println);
    }

说明:比如filter(Predicate<? super T> predicate)需要传递的是一个函数式接口,而上述代码使用lambda表达式去实现。

筛选与切片

②、映射

  • flatMap(Function<? super T, ? extends Stream<? extends R>> mapper):【多个流合并为一个流】把流中的元素一个个加入到当前的流中。
  • map(Function<? super T, ? extends R> mapper):函数会对每一个元素进行映射得到一个新的元素。

下面为了测试这两种的区别,我们自定义一个返回Stream流的方法:

@Test
    public void test04(){
        List<String>str=Arrays.asList("aaa","bbb","ccc");
        //直接使用map map本地得到的就是一个新的Stream流,而map执行多次得到多个流,最终Stream流存放的依然是			Stream流【Stream流中存放的是Stream流】
        //map:不会合并流,仅仅是对元素输入函数进行映射,得到一个个Stream加入到当前的流中
        Stream<Stream<Character>> sm = str.stream().map(TestStream::filterCharter);//直接调用此类的方法
        sm.forEach((ssmm)->{
            ssmm.forEach(System.out::println);
        });
        //flatMap:将返回的流进行合并,得到一个流
        str.stream()
                .flatMap(TestStream::filterCharter)//lambda表达式的类::静态方法调用 直接调用此类的方法
                .forEach(System.out::println);
    }
   //将字符串切割得到字符
    public static Stream<Character>filterCharter(String str){
        List<Character> list=new ArrayList<>();
        for (Character c:str.toCharArray()) {
            list.add(c);
        }
        return list.stream();
    }

映射

③、排序

 List<Employee> list= Arrays.asList(
            new Employee("张三","15"),
            new Employee("李四","45"),
            new Employee("王五","35"),
            new Employee("王五","35"),
            new Employee("王六","35"),
            new Employee("小刘","65")
    );
    /**
     * 排序:
     * sorted()-自然排序-(Comparable)
     * sorted(Comparator com)-定制排序
     */
    @Test
    public void test05(){
        List<String>str=Arrays.asList("ddd","aaaaa","bbb","ccccc");
        str.stream()
                .sorted()
                .forEach(System.out::println);
        list.stream()
                .sorted((x,y)->{
                    if(x.getAge().equals(y.getAge())){
                        return  -x.getName().compareTo(y.getName());
                    }else{
                        return  -(Integer.parseInt(x.getAge())-Integer.parseInt(y.getAge()));
                    }
                })
                .forEach(System.out::println);
    }

排序

2.4、Stream的终止操作

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如: List、 Integer,甚至是 void 。

①、查找与匹配

  • allMatch-检查是否匹配所有元素
  • anyMatch-检查是否至少匹配一个元素
  • noneMatch-检查是否没有匹配所有元素
  • findFirst-返回第一个元素
  • findAny-返回当前流中的任意元素
  • count-返回流中元素的总个数
  • max-返回流中最大值
  • min-返回流中的最小值

是不是已经感觉到迷糊了,这么多!别急,一个小案例就清晰了。

 List<Student> list= Arrays.asList(
           new Student("张三",18, Student.Status.FREE),
           new Student("李四",13, Student.Status.BUSY),
           new Student("李四",13, Student.Status.BUSY),
           new Student("王五",11, Student.Status.BUSY),
           new Student("刘六",55, Student.Status.VOCATION),
           new Student("王麻子",77, Student.Status.FREE)
    );
//注:Student.Status是一个enum
    @Test
    public void test01(){
        //是否匹配所有的
        boolean match = list.stream()
                .allMatch((t) -> Student.Status.BUSY.equals(t.getStatus()));
        System.out.println(match);//false
        //至少有一个匹配
        boolean match1 = list.stream()
                .anyMatch((t) -> Student.Status.BUSY.equals(t.getStatus()));
        System.out.println("至少有一个元素与之匹配"+match1);//至少有一个元素与之匹配true
        //没有匹配的元素
        boolean match2 = list.stream()
                .noneMatch((t) -> Student.Status.BUSY.equals(t.getStatus()));
        System.out.println("没有与之匹配的元素:"+match2);//没有与之匹配的元素:false
        //避免空指针异常,将结果封装到Optional,一旦有可能为空就封装到Optional中
        Optional<Student> op = list.stream()
                .sorted((x, y) ->Integer.compare(x.getAge(),y.getAge()))
                .findFirst();
        System.out.println("排序后拿到第一个元素:"+op.get());//Student{name='王五', age=11, Status=BUSY}
        //随便找到一个空闲状态的人,先过滤出来,再随便找一个
        Optional<Student> any = list.stream()
                .filter((e) -> e.getStatus().equals(Student.Status.FREE))
                .findAny();
        System.out.println("随便-找到一个status为空闲的人"+any.get());//Student{name='张三', age=18, Status=FREE}
        System.out.println("人员总数"+list.stream().count());//人员总数6
        Optional<Student> max = list.stream()
                .max((x, y) -> Integer.compare(x.getAge(), y.getAge()));
        System.out.println("获取年龄最大的:"+max.get());//Student{name='王麻子', age=77, Status=FREE}
    }

下面对上述方法的具体描述:

查找与匹配

查找与匹配

②、归约

  • reduce(T identity,BinaryOperator) T identity 初始值 BinaryOperator->继承BiFunction<T,U, R> 将identity作为起始值,做为x,再从流中取出一个元素作为y;
  • reduce(BinaryOperator)--可以将流中元素反复结合起来,得到一个值
  @Test
    public void test02(){
        List<Integer>list=Arrays.asList(1,2,3,4,5,6,7,8);
        Integer sum = list.stream()
                .reduce(0, (x, y) -> x + y);//初始值,一开始x=初始值,再从流中拿到第一个值作为y
        System.out.println(sum);//36
        /**
         * 下列包含Lambda表达式中的方法方法引用的 类::实例方法
         * 此种情况较为特殊:
         *      若lambda参数列表中的第一个参数是实例方法的调用者,
         *      而第二个参数是实例方法的参数时或者没有第二个参数,
         *      可以使用ClassName::method
         */
        Optional<Integer> op = this.list.stream()
                .map((x) -> x.getAge())
                .reduce(Integer::sum);
        System.out.println(op.get());//187
        Optional<Integer> op2 = this.list.stream()
                .map(Student::getAge)
                .reduce(Integer::sum);
        System.out.println(op2.get());//187
    }

归约

③、收集

Collector 接口中方法的实现决定了如何对流执行收集操作(如收集到 List、 Set、 Map)。但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例, 具体方法与实例如下表:

收集

收集

  • collect-将流转换为其他形式。接受一个Collector接口的实现,用于Stream中元素做汇总的方法
 List<Student> list= Arrays.asList(
           new Student("张三",18, Student.Status.FREE),
           new Student("李四",13, Student.Status.BUSY),
           new Student("李四",13, Student.Status.BUSY),
           new Student("王五",11, Student.Status.BUSY),
           new Student("刘六",55, Student.Status.VOCATION),
           new Student("王麻子",77, Student.Status.FREE)
    );
//需求:将当前学生的名字提取出后放在一个集合里面
    @Test
    public void test03(){
        System.out.println("-----------将结果收集到list集合并返回--------------------");
        List<String> list1 = list.stream()
                .map(Student::getName)
                .collect(Collectors.toList());
        list1.forEach(System.out::println);
        System.out.println("-----------将结果收集到set集合并返回-----------------------");
        Set<String> set1 = list.stream()
                .map(Student::getName)
                .collect(Collectors.toSet());
        set1.stream().forEach(System.out::println);
        System.out.println("-----------自定义返回的集合类型---------------------------");
        HashSet<String> set2 = list.stream()
                .map(Student::getName)
                .collect(Collectors.toCollection(HashSet::new));
        set2.forEach(System.out::println);
        System.out.println("-----------将结果收集为总数数量---------------------------");
        Long number = list.stream()
                .collect(Collectors.counting());
        System.out.println(number);
        System.out.println("-----------得到结果的平均值---------------------------");
        Double av = list.stream()
                .collect(Collectors.averagingInt(value -> value.getAge()));
        System.out.println(av);
        System.out.println("-----------得到结果的总和---------------------------");
        Integer sum = list.stream()
                .collect(Collectors.summingInt(Student::getAge));
        System.out.println(sum);
        System.out.println("-----------得到结果的最大值---------------------------");
        Optional<Student> student = list.stream()
                .collect(Collectors.maxBy((o1, o2) -> Double.compare(o1.getAge(), o2.getAge())));
        System.out.println(student.get());
        System.out.println("-----------连接字符串---------------------------");
        String s = list.stream()
                .map(Student::getName)
                .collect(Collectors.joining("中间连接符,","首","尾"));
        System.out.println(s);

    }

需求一:按照状态进行分组 对应sql的分组

@Test
    public void test04(){
        Map<Student.Status, List<Student>> map = list.stream()
                .collect(Collectors.groupingBy(Student::getStatus));
        System.out.println(map);
    }
//结果:
{VOCATION=[Student{name='刘六', age=55, Status=VOCATION}], 
BUSY=[Student{name='李四', age=13, Status=BUSY}, Student{name='李四', age=13, Status=BUSY}, Student{name='王五', age=11, Status=BUSY}], 
FREE=[Student{name='张三', age=18, Status=FREE}, Student{name='王麻子', age=77, Status=FREE}]}

需求二:多级分组

注意:分组后一般返回的是一个map,其中key作为分组的依据,value就是分组后的值

 /**
     *
     * Collectors.groupingBy(Function,Collectors)
     * 分组后,对已分组的数据进行二次分组
     */
    @Test
    public void test05(){
        Map<Student.Status, Map<String, List<Student>>> map = list.stream()
                .collect(Collectors.groupingBy(Student::getStatus, Collectors.groupingBy(o -> {
                    if (((Student) o).getAge() <= 18) {
                        return "青年";
                    } else if (((Student) o).getAge() <= 50) {
                        return "中年";
                    } else {
                        return "老年";
                    }
                })));
        System.out.println(map);
    }
//结果:
{BUSY={青年=[Student{name='李四', age=13, Status=BUSY}, Student{name='李四', age=13, Status=BUSY}, Student{name='王五', age=11, Status=BUSY}]},
VOCATION={老年=[Student{name='刘六', age=55, Status=VOCATION}]}, 
FREE={青年=[Student{name='张三', age=18, Status=FREE}], 老年=[Student{name='王麻子', age=77, Status=FREE}]}}

分片和分区:根据处理的结果ture/false进行分区

 @Test
    public void test06(){
        Map<Boolean, List<Student>> map = list.stream()
                .collect(Collectors.partitioningBy(o -> o.getAge() > 50));
        System.out.println(map);
    }
//结果:
{false=[Student{name='张三', age=18, Status=FREE}, Student{name='李四', age=13, Status=BUSY}, Student{name='李四', age=13, Status=BUSY}, Student{name='王五', age=11, Status=BUSY}], 
true=[Student{name='刘六', age=55, Status=VOCATION}, Student{name='王麻子', age=77, Status=FREE}]}

3、新的时间日期API

3.1、LocalDate、 LocalTime、 LocalDateTime

LocalDate、 LocalTime、 LocalDateTime 类的实例是不可变的对象【线程安全】,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息。也不包含与时区相关的信息。 新的日期类将统一放在java.time包下。

注意:三者使用的方式完全相同,只是一个表示日期,一个表示时间,一个表示时间和日期

下面让我们快速入门吧!

 @Test
    public void test01(){
        System.out.println("---------------------获取当前系统时间----------------------------");
        LocalDateTime dateTime1 = LocalDateTime.now();
        System.out.println(dateTime1);
        System.out.println("---------------------指定时间,年月日时分秒----------------------------");
        LocalDateTime dateTime2 = LocalDateTime.of(2015, 10, 16, 13, 22, 33);
        System.out.println(dateTime2);
        //时间运算,添加一日
        System.out.println("---------------------日期运算,当前系统时间往后添加一日-----------------");
        LocalDateTime dateTime3 = dateTime1.plusDays(1);
        System.out.println(dateTime3);
        System.out.println("---------------------日期运算,当前系统时间往后减少一个月---------------");
        LocalDateTime dateTime4 = dateTime1.minusMonths(1);
        System.out.println(dateTime4);
        //获取单独的年月日
        //可以直接获取值,也可以获取年月日对象
        System.out.println("年:"+dateTime1.getYear());
        System.out.println("月:"+dateTime1.getMonthValue());
        System.out.println("日:"+dateTime1.getDayOfMonth());
        System.out.println("时:"+dateTime1.getHour());
        System.out.println("分:"+dateTime1.getMinute());
        System.out.println("秒:"+dateTime1.getSecond());
    }
输出:
---------------------获取当前系统时间---------------------------------
2020-08-13T22:36:07.123
---------------------指定时间,年月日时分秒----------------------------
2015-10-16T13:22:33
---------------------日期运算,当前系统时间往后添加一日-----------------
2020-08-14T22:36:07.123
---------------------日期运算,当前系统时间往后减少一个月---------------
2020-07-13T22:36:07.123
年:2020
月:8
日:13
时:22
分:36
秒:7

新的日期API

①、Instant时间戳

Instant 时间戳 给机器读的用于“时间戳”的运算。它是以Unix元年(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的描述进行运算。

 @Test
    public void test02(){
        Instant instant = Instant.now();//默认获取的是UTC时区为基础的
        System.out.println("默认获取的是UTC时区为基础的时间:"+instant);
        //对时区做一个偏移量运算
        OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));
        System.out.println("带偏移量的时区时间:"+offsetDateTime);

        //输出时间格式为毫秒格式
        System.out.println("输出时间格式为毫秒格式"+instant.toEpochMilli());
        //相较1970年的时间
        System.out.println("相较1970年的时间 向后添加60秒"+Instant.ofEpochSecond(60));//1970-01-01T00:01:00Z
    }
输出:
    默认获取的是UTC时区为基础的时间:2020-08-15T02:49:41.828Z
	带偏移量的时区时间:2020-08-15T10:49:41.828+08:00
	输出时间格式为毫秒格式1597459781828
	相较1970年的时间 向后添加601970-01-01T00:01:00Z

②、Duration 和 Period

  • Duration:计算两个时间之间的间隔。
  • Period:计算两个日期之间的间隔。
@Test
    public void test03() throws InterruptedException {
        Instant instant1 = Instant.now();
        Thread.sleep(100);
        Instant instant2 = Instant.now();
        //计算两个时间戳之间的间隔
        Duration duration = Duration.between(instant1, instant2);
        //获取毫秒
        System.out.println(duration.toMillis());//119
        //获取纳秒
        System.out.println(duration.getNano());//119000000
        //获取秒
        System.out.println(duration.getSeconds());//0
        //获取纳秒第二种方式
        System.out.println(duration.toNanos());//119000000

        LocalDateTime dateTime1 = LocalDateTime.now();
        Thread.sleep(1);
        LocalDateTime dateTime2 = LocalDateTime.now();
        System.out.println(duration.between(dateTime1,dateTime2).toMillis());//1
    }
  @Test
    public void test04(){
        LocalDate date1 = LocalDate.of(2018,1,1);
        LocalDate date2 = LocalDate.now();
        Period period = Period.between(date1, date2);
        System.out.println(period);//P2Y5M13D
        //直接输出格式不是很明显
        System.out.println("相差几年:"+period.getYears());//
        System.out.println("相差多少月:"+period.getMonths());//
        System.out.println("相差多少天:"+period.getDays());//
    }
输出:
    P2Y7M14D
	相差几年:2
	相差多少月:7
	相差多少天:14

③、日期的操纵

  • TemporalAdjuster : 时间校正器。有时我们可能需要获取例如:将日期调整到“下个周日”等操作。
  • TemporalAdjusters : 该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现。

方法public LocalDate with(TemporalAdjuster adjuster) ,传入接口TemporalAdjuster 对日期进行调整,我们查看TemporalAdjuster 接口的源码:

@FunctionalInterface
public interface TemporalAdjuster {
     Temporal adjustInto(Temporal temporal);
}

可以发现其是一个函数式接口

  @Test
    public void test05(){
        //下一个周五是啥时候
        LocalDate date = LocalDate.now();
        //下一个周五是啥时候
        LocalDate date1 = date.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
        System.out.println("下一个周五是啥时候"+date1);

        //自定义指定时间,获取当前时间距离周六的天数,
        LocalDate date2 = date.with(t -> {
            //LocalDate实现了接口Temporal,所以可以实现强转
            LocalDate lt = (LocalDate) t;
            //获取今天是周几
            //lt.plusDays():向当前日期添加几天
            DayOfWeek dayOfWeek = lt.getDayOfWeek();
            if (dayOfWeek.equals(DayOfWeek.SATURDAY)) {
                return lt.plusDays(0);//其实这一句是多余的
            } else {
                return lt.plusDays(DayOfWeek.SATURDAY.getValue() - dayOfWeek.getValue());
            }
        });
        System.out.println("前时间距离周六的时间"+date2);
    }
输出:
    下一个周五是啥时候2020-08-21
	前时间距离周六的时间2020-08-15

说明:DayOfWeek是java.time包下的一个enum类型,其中定义好了周一~周日的枚举值。

④、解析与格式化

java.time.format.DateTimeFormatter 类:该类提供了三种格式化方法:

  • 预定义的标准格式
  • 语言环境相关的格式
  • 自定义的格式
 @Test
    public void test07(){
        System.out.println("-------------------------使用自带的各种格式---------------");
        DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;

        LocalDateTime dateTime = LocalDateTime.now();
        String strtime = formatter.format(dateTime);
        System.out.println(strtime);
        System.out.println("-------------------------自定义日期格式---------------");
        DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
        String strtime2 = formatter2.format(dateTime);
        System.out.println(strtime2);
        System.out.println("--------------------把字符串解析成为时间LocalDateTime---");
        LocalDateTime dateTime2 = dateTime.parse(strtime2, formatter2);
        System.out.println(dateTime2);
    }
输出:
   -------------------------使用自带的各种格式---------------
	2020-08-15T11:14:51.74
	-------------------------自定义日期格式---------------
	2020年08月1511:14:51
	--------------------把字符串解析成为时间LocalDateTime---
	2020-08-15T11:14:51 

注意:根据字符串解析LocalDateTime时,其formatter需保持一致,否则会报错java.time.format.DateTimeParseException

⑤、时区的处理

Java8 中加入了对时区的支持,带时区的时间为分别为: ZonedDate、 ZonedTime、 ZonedDateTime

其中每个时区都对应着 ID,地区ID都为 “{区域}/{城市}”的格式例如 : Asia/Shanghai 等。

ZoneId:该类中包含了所有的时区信息

  • getAvailableZoneIds() : 可以获取所有时区时区信息
  • of(id) : 用指定的时区信息获取 ZoneId 对象
@Test
    public void test08(){
        System.out.println("---------------获取支持的所有时区----------------------");
        Set<String> ids = ZoneId.getAvailableZoneIds();
        ids.forEach(System.out::println);
        System.out.println("---------------获取指定时区地区的时间信息---------------");
        LocalDateTime now = LocalDateTime.now(ZoneId.of("Europe/London"));
        System.out.println(now);//2020-06-15T08:18:27.396
        System.out.println("---------------获取带时区信息的时间,包含了与UTC标准的时差---");
        LocalDateTime now1 = LocalDateTime.now();
        ZonedDateTime zone = now1.atZone(ZoneId.of("Europe/London"));
        System.out.println(zone);//2020-06-15T15:18:27.515+01:00[Europe/London]
    }
输出:
    ---------------获取支持的所有时区----------------------
    Asia/Aden
    America/Cuiaba
    Etc/GMT+9
    Etc/GMT+8
    Asia/Shanghai
    等等.....
    ---------------获取指定时区地区的时间信息---------------
    2020-08-15T04:22:09.009
    ---------------获取带时区信息的时间,包含了与UTC标准的时差---
    2020-08-15T11:22:09.029+01:00[Europe/London]

⑥、与传统时器处理与转换

在这里插入图片描述

4、接口中的默认方法和静态方法

我们都知道,当实体类(非抽象类)A继承了某个接口B,那么类A必须重写接口中的方法。此模式在springboot 1.X的版本中均使用此模式,其解决方式是在接口和实体类中添加了一层:抽象类,使之抽象类继承接口,实体类再去继承抽象类。比如:

在springboot 1.X版本时,当需要对springmvc进行扩展时,相关配置需要继承抽象类WebMvcConfigureAdapter:

@Deprecated
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {
    /**
	 * {@inheritDoc}
	 * <p>This implementation is empty.
	 */
	@Override
	public void configurePathMatch(PathMatchConfigurer configurer) {
	}
    ...WebMvcConfigurer接口的其他方法
}   

进入源码我们可以发现其对WebMvcConfigurer实现,但是均没有具体实现。

当springboot 2.X版本时,此时我们进入接口WebMvcConfigurer中查看:

public interface WebMvcConfigurer {

	/**
	 * Helps with configuring HandlerMappings path matching options such as trailing slash match,
	 * suffix registration, path matcher and path helper.
	 * Configured path matcher and path helper instances are shared for:
	 * <ul>
	 * <li>RequestMappings</li>
	 * <li>ViewControllerMappings</li>
	 * <li>ResourcesMappings</li>
	 * </ul>
	 * @since 4.0.3
	 */
	default void configurePathMatch(PathMatchConfigurer configurer) {
	}
    @Nullable
	default MessageCodesResolver getMessageCodesResolver() {
		return null;
	}
    ...等等...
}

对比一下:此接口中定义方法与我们平时定义的有何不同?

  • 接口中的方法前多了一个关键字default,而且对其进行了实现。

最后插入一下springboot中对于springmvc扩展时的部分配置:

@Configuration
//@EnableWebMvc
public class MyMvcConfig implements WebMvcConfigurer {
    //需要什么方法,从写什么方法

    /**
     * 配置嵌入式servlet容器
     * @return
     */
//    @Bean //一定要将这个定制器加入到容器中
    public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
            //定制嵌入式的Servlet容器相关的规则
       /* return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
            @Override
            public void customize(ConfigurableWebServerFactory factory) {
                factory.setPort(8081);
            }
        };*/
        return (factory)->factory.setPort(8081);
    }
    /**
     * 自定义一个controller
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //返回的路径也是经过thymeleaf模板引擎的
        //这个相当于在controller中映射了一个@RequestMapping()
        registry.addViewController("/page03").setViewName("thymeleaf01");
    }
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
    /**
     * 自定义一个视图解析器
     * 将被ContentNegotiatingViewResolver自动加载
     */
    @Bean
    public ViewResolver myViewResolver(){
       return new MyViewResolver();
    }
    private static class MyViewResolver implements ViewResolver{
        @Override
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            return null;
        }
    }

    /**
     *注册拦截器
     */
   /* @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
           .excludePathPatterns(Arrays.asList("/index.html","/","/user/login","/assert/**","/css/**","/js/**","/img/**","/webjars/**"))
                .addPathPatterns("/**");
    }*/
}

好了,上述听着可能比较繁琐,而我们比较直观的看见的是:当接口中的方法使用default修饰后,其实现类不强制去实现,也只是为了引入default关键字,那么接口中的默认方法和静态方法有啥注意事项呢?

接口默认方法的” 类优先” 原则 :

若一个接口中定义了一个默认方法,而另外一个父类或接口中又定义了一个同名的方法时 :

  • 选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。
  • 接口冲突。如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法), 那么必须覆盖该方法来解决冲突 。

好了,下面使用一个例子来说明上述两条规则:

定义一个接口Myfun:

/**
 * 在以前接口中只能有:全局静态常量和抽象方法
 * 在java8中添加默认方法、静态方法
 */
public interface Myfun {
    default String getName(){
        return "这是接口Myfun!";
    }
    public static void shwo(){
        System.out.println("这是Myfun接口中的静态方法");
    }
}

定义一个实体类TestDefault,其下有一个与接口Myfun同名方法:

public class TestDefault  {
    public  String getName(){
        return "这是类testDefault!";
    }
}

定义一个实现类MyClass去继承第接口和实现类:

public class MyClass extends TestDefault implements Myfun{
}

此时若调用MyClass的getName()方法,那么调用的是接口中的方法,或者是实体类终的方法呢???

public class TestDefaultInterface {
    /**
     * 类MyClass同时继承了TestDefault、Myfun
     * 在类TestDefault和接口Myfun均存在方法getName()
     * 问题:当子孙类同时继承时,调用getName()方法时,调用的是谁的方法?
     */
   
     //注意:当继承的多个接口中存在同名的默认方法时,那么实现类中必须对此方法进行重写
    
    public static void main(String[] args) {
        //调用的是类中的方法
        System.out.println(new MyClass().getName());
        //接口直接调用静态方法执行
        Myfun.shwo();
    }
}
结果:
    这是类testDefault!
    这是Myfun接口中的静态方法

5、Optional类

Optional 类(java.util.Optional) 是一个容器类,代表一个值存在或不存在,原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。

常用方法:

  • Optional.of(T t) : 创建一个 Optional 实例
  • Optional.empty() : 创建一个空的 Optional 实例
  • Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
  • isPresent() : 判断是否包含值
  • orElse(T t) : 如果调用对象包含值,返回该值,否则返回t
  • orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
  • map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()
  • flatMap(Function mapper):与 map 类似,要求返回值必须是Optional