JDK新特性(JavaSE - JDK7、JDK8)(持续更新迭代)

目录

JDK7新特性

一、AutoCloseable

1. 简介

2. 案例

2.1. Redis的分布式锁

2.2. 上锁的例子

资源释放的问题?

如何解决遗忘的问题呢?

改进步骤

JDK8新特性

一、前言

二、Lambda表达式

1. 简介

2. 体验

3. 优点

4. 标准格式

5. 方法的对比

6. 无参数无返回值

7. 有参数有返回值

8. 实现原理

8.1 准备

8.2 总结

9. 省略格式

10. 前提条件

10.1. 准备

10.2. 总结

11. 函数式接口

12. Lambda和匿名内部类对比

13. 何时使用Lambda表达式?何时使用Lambda表达式?

三、接口新增方法

1. 新增默认方法

1.1. 原因

1.2. 接口默认方法的定义格式

1.3. 接口默认方法的使用

2. 新增静态方法

2.1. 原因

2.2. 接口静态方法的定义格式

2.3. 接口静态方法的使用

2.4. 默认方法和静态方法的区别

3. 如何选择这两个新增方法

四、常用内置函数式接口

1. 内置函数式接口来由来

2. 常用内置函数式接口

2.1. 生产型:Supplier

2.2. 消费型:Consumer

默认方法:andThen

2.3. 函数式型:Function

默认方法:andThen

2.4. 断言型:Predicate

默认方法:and

默认方法:or

默认方法:negate

2.5. 二进制操作:BinaryOperator

BinaryOperator

BinaryOperator作为参数

IntBinaryOperator

BinaryOperator::maxBy()和BinaryOperator.minBy()

五、方法引用

1. Lambda的冗余场景

2. 体验方法引用简化Lambda

3. 方法引用的格式

4. 常见引用方式

对象名::引用成员方法

类名::引用静态方法

类名::引用实例方法

类名::new引用构造器

数组::new 引用数组构造器

知识小结

六、Stream流

1. 集合处理数据的弊端

1.1 前言

1.2 循环遍历的弊端

1. 分析

2. Stream的更优写法

1.3 Stream流式思想

1. 概述

2. 总结

2. 获取Stream流的方式:两种

2.1 根据Collection获取流

2.2 Stream中的静态方法of获取流

3. Stream常用方法和注意事项

3.1 Stream常用方法

3.2 Stream注意事项

4. Stream流各方法使用

4.1 forEach

4.2 count

4.3 filter

4.4 limit

4.5 skip

4.6 map

4.7 sorted

4.8 distinct

4.9 match

4.10 find

4.11 max/min

4.12 reduce

4.13 map/reduce

4.14 mapToInt

4.15 concat

4.16 综合案例

1. 需求

2. 实现方式

2.1 传统方式

2.2 Stream方式

5. Stream流其他操作

5.1 收集Stream流中的结果

1. Stream流中的结果到集合中

2. Stream流中的结果到数组中

5.2 对流中数据进行聚合计算

5.3 对流中数据进行分组

5.4 对流中数据进行多级分组

5.5 对流中数据进行分区

5.6 对流中数据进行拼接

5.7 总结

七、并行的Stream流

1. 串行的Stream流

2. 并行的Stream流

3. 获取并行Stream流的两种方式

4. 并行和串行Stream流的效率对比

5. parallelStream线程安全问题

6. parallelStream背后的技术

6.1 Fork/Join框架介绍

6.2 Fork/Join原理

1. 分治法

2. 工作窃取算法

6.3 Fork/Join案例

7. 使用并行流时候的注意事项

八、Optional类

1. 以前对null的处理方式

2. Optional类介绍

3. Optional的基本使用

4. Optional的高级使用

5. 总结


JDK7新特性

一、AutoCloseable

1. 简介

JDK在1.7之后出现了自动关闭类的功能,该功能的出现为各种关闭资源提供了相当大的帮助,这里我们谈一谈自

动关闭类。

JDK1.7之后出现了一个重要的接口,以及改造了一个重要的方法结构:

  1. AutoCloseable自动关闭接口
  2. try(){}--catch{}--finally{}

相应的 一些资源也实现了该接口,如 preparedStatement、Connection、InputStream、outputStream 等等

资源接口。

一句话: 实现 AutoCloseable 接口,覆盖 close 方法,把原来要写 finally 中释放资源的动作,放入到 close 方

法中去执行,而这个执行是 jvm 自己去执行.

什么样子的情况下可以去做呢?--如果有try(){}--catch{}--finally{}

  • 接口的实现类要重写 close() 方法,
  • 将要关闭的资源定义在 try() 中,这样当程序执行完毕之后,资源将会自动关闭。
  • 自定义类如果要进行自动关闭,只需要实现 AutoCloseable 接口重写 close() 方法即可,

同时也只有实现了 AutoCloseable 接口才能将,自定义类放入到try()块中,否则编译不能通过,举例说明

public class ReadTxt extends AutoClassable {
    @Override
    public void close() throws Exception {
        System.out.println("ReadTxt close");
    }
 
    public String readTextValue(String path){
        StringBuffer sb = new StringBuffer();
        try(BufferedReader br = new BufferedReader(new FileReader(path))){
            int line;
            while((line = br.read())!=-1){
                sb.append(br.readLine()+"\n")
            }
        }
        return sb.toString();
    }
}
 
class MainTest {
    public static void main(String[] args) {
        try (ReadTxt rt = new ReadTxt()) {
            String line = rt.readTextValue("G:\\学习文档\\test.txt");
            System.out.println(line);
        }
    }
}

2. 案例

2.1. Redis的分布式锁

比如:未来的Redis的分布式锁等

分布式锁类:

package com.zheng.lock;


public class RedisLock implements AutoCloseable {

    // 加锁
    public void lock(){
        System.out.println("加锁了....");
    }

    // 释放锁
    public void unlock(){
        System.out.println("释放锁了....");
    }

    // 自动释放资源
    @Override
    public void close() throws Exception {
        this.unlock();
    }
}

测试类:

package com.zheng.lock;

public class RedisLockTest {

    public static void main(String[] args) {
        try (RedisLock redisLock = new RedisLock()) {
            redisLock.lock();
        } catch (Exception e) {
            e.printStackTrace();
        }
//        finally {
//            // 不需要定义了,因为会自动去释放资源
//            redisLock.unlock();
//        }
    }

}

执行结果如下:

加锁了....
释放锁了...

2.2. 上锁的例子

package com.zheng.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyLockDemo {
    static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        try {
            lock.lock();
            System.out.println("1-----加锁成功!!!");
            System.out.println("2-----开始执行业务逻辑");
            Thread.sleep(3000);
            System.out.println("3-----业务执行完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            System.out.println("4----释放锁资源");
        }
    }

}
资源释放的问题?

通过上面的代码我们发现,在开发中锁是释放必须要做得事情,所以就把放在finally中来执行。

但是在开发中往往很多的开发中,都会忘记释放锁或者忘记把锁的释放放入finally中,就造成死锁现象,这个很危

险的操作和行为。

如何解决遗忘的问题呢?
  • 使用AutoCloseable接口覆盖close方法
改进步骤

定义类MyLock类

package com.zheng.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyLock implements AutoCloseable{

    Lock lock = new ReentrantLock();

    // 加锁
    public void lock() {
        lock.lock();
    }

    // 释放锁
    public void unlock() {
        lock.unlock();
    }


    @Override
    public void close() throws Exception {
        unlock();
        System.out.println("4----释放锁资源");
    }
}

改进

package com.zheng.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyLockDemo {
    public static void main(String[] args) {
        try (MyLock lock = new MyLock();){
            lock.lock();
            System.out.println("1-----加锁成功!!!");
            System.out.println("2-----开始执行业务逻辑");
            Thread.sleep(3000);
            System.out.println("3-----业务执行完毕");
        } catch (Exception e) {
            e.printStackTrace();
        }//        finally {
//            // 不需要定义了.因为会自动去释放资源
//            lock.unlock();
//        }
    }
}
1-----加锁成功!!!
2-----开始执行业务逻辑
3-----业务执行完毕
4----释放锁资源  -- 这里的执行就是AutoCloseable中的close方法自动执行的

JDK8新特性

一、前言

首先,Java 8发布于2014-03-18,是目前企业中使用最广泛的一个版本。

Java 8是一次重大的版本升 级,带来了许多新特性,目前主流的版本主要是JDK1.8,JDK11, JDK17也逐渐流行 , 目

前最新的版本是18,企业用的最多的仍然是JDK1.8,因为JDK1.8相比较其他JDK版本要稳定的多,其实自己开发

也完全没关系。

自己可以多安装几个JDK版本,说到JDK版本的新特性的话,我主要了解的内容如下:

二、Lambda表达式

1. 简介

Lambda表达式主要是用来解决匿名内部类存在的问题,

就比如,当需要启动一个线程去完成任务时,我们通常会通过Runnable接口来定义任务内容,并使用Thread类来

启动该线程

传统写法,代码如下:

public class Demo01LambdaIntro {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("新线程执行代码啦");
            }
        }).start();

        new Thread(() -> {
            System.out.println("Lambda表达式执行啦");
        }).start();
    }
}

由于面向对象的语法要求,首先创建一个 Runnable 接口的匿名内部类对象来指定线程要执行的任务内容,再将

其交 给一个线程来启动。

代码分析:

对于 Runnable 的匿名内部类用法,可以分析出几点内容:

  • Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心
  • 为了指定 run 的方法体,不得不需要 Runnable 接口的实现类
  • 为了省去定义一个 Runnable 实现类的麻烦,不得不使用匿名内部类
  • 必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错 、
  • 而实际上,似乎只有方法体才是关键所在。

2. 体验

Lambda是一个匿名函数,可以理解为一段可以传递的代码。

Lambda表达式写法,代码如下:

借助Java 8的全新语法,上述 Runnable 接口的匿名内部类写法可以通过更简单的Lambda表达式达到相同的效果

public class Demo01LambdaIntro {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("新线程任务执行!")).start(); // 启动线程

    }
}

这段代码和刚才的执行效果是完全一样的,可以在JDK 8或更高的编译级别下通过。

从代码的语义中可以看出:我们 启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。

我们只需要将要执行的代码放到一个Lambda表达式中,不需要定义类,不需要创建对象。

3. 优点

简化匿名内部类的使用,语法更加简单。

4. 标准格式

Lambda省去面向对象的条条框框,Lambda的标准格式格式由3个部分组成:

(参数类型 参数名称) -> {
    代码体;
}

格式说明:

  • (参数类型 参数名称):参数列表
  • {代码体;}:方法体
  • -> :箭头,分隔参数列表和方法体

5. 方法的对比

匿名内部类

public void run() {
    System.out.println("aa");
}

Lambda

() -> System.out.println("bb!")

6. 无参数无返回值

public interface Swimmable {
    public abstract void swimming();
}
/*
    Lambda的标准格式:
        (int a) -> {
            要执行的代码
        }
 */
public class Demo02LambdaUse {
    public static void main(String[] args) {
        goSwimming(new Swimmable() {
            @Override
            public void swimming() {
                System.out.println("凤姐 自由泳.");
            }
        });

        // 小结:Lambda表达式相当于是对接口抽象方法的重写
        goSwimming(() -> {
            System.out.println("如花 蛙泳");
        });
    }

    // 练习无参数无返回值的Lambda
    public static void goSwimming(Swimmable s) {
        s.swimming();
    }

    public static void test(int a) {

    }
}

7. 有参数有返回值

就拿java.util.Comparator 比较器接口为例,它里面有个抽象方法public abstract int compare(T o1, T o2);

我们知道当需要对一个对象集合进行排序时, Collections.sort 方法需要一个 Comparator 接口实例来指定排序

的规则。

我们的传统写法如下:

public class Person {
    private String name;
    private int age;
    private int height;
    // 省略其它
}

如果使用传统的代码对 ArrayList 集合进行排序,写法如下:

// 练习有参数有返回值的Lambda
public class Demo03LambdaParam {
    public static void main(String[] args) {
        ArrayList<Person> persons = new ArrayList<>();
        persons.add(new Person("刘德华", 58, 174));
        persons.add(new Person("张学友", 58, 176));
        persons.add(new Person("刘德华", 54, 171));
        persons.add(new Person("黎明", 53, 178));

        // 对集合中的数据进行排序
        /*Collections.sort(persons, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge(); // 升序排序
            }
        });*/

        Collections.sort(persons, (Person o1, Person o2) -> {
            return o2.getAge() - o1.getAge(); // 降序
        });

        for (Person person : persons) {
            System.out.println(person);
        }

        System.out.println("-----------");
        persons.forEach((t) -> {
            System.out.println(t);
        });
    }
}

8. 实现原理

8.1 准备

我们现在已经会使用Lambda表达式了。现在同学们肯定很好奇Lambda是如何实现的,

现在我们就来探究Lambda表达式的底层实现原理。

@FunctionalInterface
public interface Swimmable {
    public abstract void swimming();
}	
public class Demo04LambdaImpl {
    public static void main(String[] args) {
        // 匿名内部类在编译后会形成一个新的类.$
/*        goSwimming(new Swimmable() {
            @Override
            public void swimming() {
                System.out.println("使用匿名内部类实现游泳");
            }
        });*/

        goSwimming(() -> {
            System.out.println("Lambda表达式中的游泳");
        });
    }

    public static void goSwimming(Swimmable swimmable) {
        swimmable.swimming();
    }
}

我们可以看到匿名内部类会在编译后产生一个类: Demo04LambdaImpl$1.class

使用XJad反编译这个类,得到如下代码:

package com.zheng.d1_lambda;

import java.io.PrintStream;

// Referenced classes of package com.zheng.demo01lambda: 
// Swimmable, Demo04LambdaImpl 

static class Demo04LambdaImpl$1 implements Swimmable {
    public void swimming() {
        System.out.println("使用匿名内部类实现游泳");
    }

    Demo04LambdaImpl$1() {
    }
} 

我们再来看看Lambda的效果,修改代码如下:

public class Demo04LambdaImpl {
    public static void main(String[] args) {
        goSwimming(() -> {
            System.out.println("Lambda游泳");
        });
    }

    public static void goSwimming(Swimmable swimmable) {
        swimmable.swimming();
    }
} 

运行程序,控制台可以得到预期的结果,但是并没有出现一个新的类,也就是说Lambda并没有在编译的时候产生一 个新的类。

使用XJad对这个类进行反编译,发现XJad报错。使用了Lambda后XJad反编译工具无法反编译。

我们使用JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令。

在DOS命令行输入:

javap -c -p 文件名.class 

-c:表示对代码进行反汇编 

-p:显示所有类和成员 

反汇编后效果如下:

public class Demo06LambdaCondition {
    public static void main(String[] args) {
        // 方法的参数或局部变量类型必须为接口才能使用Lambda
        test(() -> {
        });

        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("aa");
            }
        };

        Flyable f = () -> {
            System.out.println("我会飞啦");
        };
    }

    C:\Users\>javap -c -
    p Demo04LambdaImpl.

    class
    Compiled from "Demo04LambdaImpl.java"

    public class com.zheng.demo01lambda.Demo04LambdaImpl

    { 
 public com.zheng.demo01lambda.Demo04LambdaImpl();
        Code:
        0:aload_0
        1:invokespecial #1 // Method java/lang/Object."<init>":()V 
        4:return

        public static void main (java.lang.String[]);
        Code:
        0:invokedynamic #2, 0 // InvokeDynamic #0:swimming:
        () Lcom / zheng / demo01lambda / Swimmable;
        5:invokestatic #3 // Method goSwimming:
        (Lcom / zheng / demo01lambda / Swimmable;)V
        8:return

        public static void goSwimming (com.zheng.demo01lambda.Swimmable);
        Code:
        0:aload_0
        1:invokeinterface #4, 1 // InterfaceMethod 
        com / zheng / demo01lambda / Swimmable.swimming:() V
        6:return

        private static void lambda$main$0 ();
        Code:
        0:getstatic #5 // Field 
        java / lang / System.out:Ljava / io / PrintStream;
        3:ldc #6 // String Lambda游泳 
        5:invokevirtual #7 // Method java/io/PrintStream.println:
        (Ljava / lang / String;)V
        8:return
    }

    public static void test(Flyable a) {
        new Person() {

        };
    }

}

// 只有一个抽象方法的接口称为函数式接口,我们就能使用Lambda
@FunctionalInterface // 检测这个接口是不是只有一个抽象方法
interface Flyable {
    // 接口中有且仅有一个抽象方法
    public abstract void eat();
//     public abstract void eat2();
}

可以看到在类中多出了一个私有的静态方法 lambda

0 。这个方法里面放的是什么内容呢?我们通过断点

调试 来看看:

可以确认 lambda

0 里面放的就是Lambda中的内容,我们可以这么理解 lambda

0 方法:

public class Demo04LambdaImpl {
    public static void main(String[] args) {
        // ...
    }

    private static void lambda$main$0() {
        System.out.println("Lambda游泳");
    }
}

关于这个方法 lambda

0 的命名:以lambda开头,因为是在main()函数里使用了lambda表达式,所以带

0。 如何调用这个方法呢?

其实Lambda在运行的时候会生成一个内部类,为了验证是否生成内部类,可以在运行时加 上

-Djdk.internal.lambda.dumpProxyClasses ,加上这个参数后,运行时会将生成的内部类class码输出到一个文

件中。

使用java命令如下:

java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名 

根据上面的格式,在命令行输入以下命令:

C:\Users\>java -Djdk.internal.lambda.dumpProxyClasses 
com.zheng.demo01lambda.Demo04LambdaImpl 
Lambda游泳 

执行完毕,可以看到生成一个新的类,效果如下:

反编译 Demo04LambdaImpl$

1.class 这个字节码文件,内容如下:

// Referenced classes of package com.itheima.demo01lambda: 
// Swimmable, Demo04LambdaImpl 

final class Demo04LambdaImpl$$Lambda$1 implements Swimmable {

    public void swimming() {
        Demo04LambdaImpl.lambda$main$0();
    }

    private Demo04LambdaImpl$$Lambda$1() {
        
    }
} 

可以看到这个匿名内部类实现了 Swimmable 接口,并且重写了 swimming 方法, swimming 方法调用

Demo04LambdaImpl.lambda

0() ,也就是调用Lambda中的内容。最后可以将Lambda理解为:

public class Demo04LambdaImpl {
    public static void main(String[] args) {


        goSwimming(new Swimmable() {
            public void swimming() {
                Demo04LambdaImpl.lambda$main$0();
            }
        });
    }

    private static void lambda$main$0() {
        System.out.println("Lambda表达式游泳");
    }

    public static void goSwimming(Swimmable swimmable) {
        swimmable.swimming();
    }
}

8.2 总结

匿名内部类在编译的时候会一个class文件

Lambda在程序运行的时候形成一个类

  1. 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码
  2. 还会形成一个匿名内部类,实现接口,重写抽象方法
  3. 在接口的重写方法中会调用新生成的方法.

9. 省略格式

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

  1. 小括号内参数的类型可以省略
  2. 如果小括号内有且仅有一个参数,则小括号可以省略
  3. 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
(int a) -> { 
    return new Person(); 
} 

省略后

a -> new Person() 

10. 前提条件

10.1. 准备

Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:

  1. 方法的参数或局部变量类型必须为接口才能使用Lambda
  2. 接口中有且仅有一个抽象方法
public interface Flyable { 
    public abstract void flying(); 
} 
public class Demo05LambdaCondition {
    public static void main(String[] args) {
        test01(() -> {
        });

        Flyable s = new Flyable() {
            @Override
            public void flying() {
            }
        };

        Flyable s2 = () -> {
        };
    }

    public static void test01(Flyable fly) {
        fly.flying();
    }
}

10.2. 总结

Lambda表达式的前提条件:

  1. 方法的参数或变量的类型是接口
  2. 这个接口中只能有一个抽象方法

11. 函数式接口

函数式接口在Java中是指:有且仅有一个抽象方法的接口

函数式接口,即适用于函数式编程场景的接口。

而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以 适用于Lambda使用的接口。

只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

FunctionalInterface注解 与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。

该注 解可用于一个接口的定义上:

@FunctionalInterface 
public interface Operator { 
    void myMethod(); 
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。

不过,即 使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

12. Lambda和匿名内部类对比

Lambda和匿名内部类在使用上的区别

  1. 所需的类型不一样匿名内部类,需要的类型可以是类,抽象类,接口Lambda表达式,需要的类型必须是接口
  2. 抽象方法的数量不一样匿名内部类所需的接口中抽象方法的数量随意Lambda表达式所需的接口只能有一个抽象方法
  3. 实现原理不同匿名内部类是在编译后会形成classLambda表达式是在程序运行的时候动态生成class

13. 何时使用Lambda表达式?何时使用Lambda表达式?

当接口中只有一个抽象方法时,建议使用Lambda表达式,其他其他情况还是需要使用匿名内部类

三、接口新增方法

我们知道JDK 8以前的接口只能包含静态常量和抽象方法

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

但是JDK8出现后,我们对接口进行了增强,也就是接口还可以有默认方法和静态方法,接口规范就变成了如下定

义方式:

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

1. 新增默认方法

1.1. 原因

在JDK 8以前接口中只能有抽象方法,这样就会存在这样的一个问题:

如果给接口新增方法,只能新增抽象方法,新增抽象方法,实现类就必须重写这个抽象方法,这就会导致系统扩

展性差。

interface A {
    public abstract void test1();

    // 接口新增抽象方法,所有实现类都需要去重写这个方法,非常不利于接口的扩展 
    public abstract void test2();
}

class B implements A {
    @Override
    public void test1() {
        System.out.println("BB test1");
    }

    // 接口新增抽象方法,所有实现类都需要去重写这个方法 
    @Override
    public void test2() {
        System.out.println("BB test2");
    }
}

class C implements A {
    @Override
    public void test1() {
        System.out.println("CC test1");
    }

    // 接口新增抽象方法,所有实现类都需要去重写这个方法 
    @Override
    public void test2() {
        System.out.println("CC test2");
    }
}

例如,JDK 8 时在Map接口中增加了 forEach 方法:

public interface Map<K, V> { 
    ... 
    abstract void forEach(BiConsumer<? super K, ? super V> action); 
}

通过API可以查询到Map接口的实现类如:

如果在Map接口中增加一个抽象方法,所有的实现类都需要去实现这个方法,那么工程量时巨大的。

因此,在JDK 8时为接口新增了默认方法,效果如下:

public interface Map<K, V> { 
    ... 
    default void forEach(BiConsumer<? super K, ? super V> action) { 
    ... 
    } 
}

接口中的默认方法实现类不必重写,可以直接使用,实现类也可以根据需要重写。这样就方便接口的扩展。

1.2. 接口默认方法的定义格式

interface 接口名 { 
    修饰符 default 返回值类型 方法名() { 
         代码; 
     } 
} 

1.3. 接口默认方法的使用

方式一:实现类直接调用接口默认方法

方式二:实现类重写接口默认方法,但是重写后,不能包含default关键字,也就是实现类重写这个方法后使用缺

省修饰

public class Demo02UseDefaultFunction {
    public static void main(String[] args) {
        BB bb = new BB();
        bb.test01();

        CC cc = new CC();
        cc.test01();
    }
}

interface AA {
    public default void test01() {
        System.out.println("我是接口AA默认方法");
    }
}
// 默认方法使用方式一: 实现类可以直接使用
class BB implements AA {
}

// 默认方法使用方式二: 实现类可以重写接口默认方法
class CC implements AA {
    @Override
    public void test01() {
        System.out.println("我是CC类重写的默认方法");
    }
}

2. 新增静态方法

2.1. 原因

引入静态方法后,让我们也可以像工具类一样可以直接使用接口提供的功能,让我们接口扩展性更强!

2.2. 接口静态方法的定义格式

interface 接口名 {  
    修饰符 static 返回值类型 方法名() {  
        代码;
    } 
} 

2.3. 接口静态方法的使用

直接使用接口名调用即可:接口名.静态方法名();

代码:

public class Demo04UseStaticFunction {
    public static void main(String[] args) {
        // 直接使用接口名调用即可:接口名.静态方法名(); 
        AAA.test01();
    }
}

interface AAA {
    public static void test01() {
        System.out.println("AAA 接口的静态方法");
    }
}

class BBB implements AAA {
   /* @Override
    静态方法不能重写

    public static void test01() {
        System.out.println("AAA 接口的静态方法");
    }*/
}

2.4. 默认方法和静态方法的区别

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

3. 如何选择这两个新增方法

如果这个方法需要被实现类继承或重写,使用默认方法,如果接口中的方法不需要被继承就使用静态方法

四、常用内置函数式接口

1. 内置函数式接口来由来

我们知道使用Lambda表达式的前提是需要有函数式接口。

而Lambda使用时不关心接口名,抽象方法名,只关心抽 象方法的参数列表和返回值类型。

因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。

public class Demo01UserFunctionalInterface {
    public static void main(String[] args) {
        test((int a, int b) -> {
            System.out.println(a + b);
        });
    }

    public static void test(Operation op) {
        op.getSum(1, 2);
    }
}

interface Operation {
    public abstract void getSum(int a, int b);
}

2. 常用内置函数式接口

它们主要在 java.util.function 包中。下面是最常用的几个接口。

2.1. 生产型:Supplier

java.util.function.Supplier 接口,它意味着"供给" , 对应的Lambda表达式需要“对外提供”一个符合泛型类 型

的对象数据。

@FunctionalInterface 
public interface Supplier<T> {  
    public abstract T get(); 
}

供给型接口,通过Supplier接口中的get方法可以得到一个值,无参有返回的接口。

使用Lambda表达式返回数组元素最大值

使用 Supplier 接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。

提示:接口的泛型请使用

java.lang.Integer 类。

代码示例:

public class Demo05Supplier {
    public static void main(String[] args) {
        printMax(() -> {
            int[] arr = {10, 20, 100, 30, 40, 50};
            // 先排序,最后就是最大的 
            Arrays.sort(arr);
            return arr[arr.length - 1]; // 最后就是最大的 
        });
    }

    private static void printMax(Supplier<Integer> supplier) {
        int max = supplier.get();
        System.out.println("max = " + max);
    }
} 

2.2. 消费型:Consumer

java.util.function.Consumer 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参

数决定。

@FunctionalInterface 
public interface Consumer<T> {  
    public abstract void accept(T t); 
}

使用Lambda表达式将一个字符串转成大写和小写的字符串

Consumer消费型接口,可以拿到accept方法参数传递过来的数据进行处理, 有参无返回的接口。

基本使用如:

public class Demo02Supplier {
    // 使用Lambda表达式返回数组元素最大值
    public static void main(String[] args) {
        System.out.println("开始了");
        printMax(() -> {
            int[] arr = {11, 99, 88, 77, 22};
            Arrays.sort(arr); // 升序排序
            return arr[arr.length - 1];
        });
        public class Demo06Consumer {
            public static void main(String[] args) {
                // Lambda表达式 
                test((String s) -> {
                    System.out.println(s.toLowerCase());
                });
            }

            public static void test(Consumer<String> consumer) {
                consumer.accept("HelloWorld");
            }
        }
    }

    public static void printMax(Supplier<Integer> supplier) {
        System.out.println("aa");
        int max = supplier.get();
        System.out.println("max = " + max);
    }
}
默认方法:andThen

如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:

消费一个数据的时候,首先做一个操 作,然后再做一个操作,实现组合。

而这个方法就是 Consumer 接口中的default方法 andThen 。

下面是JDK的源代码:

default Consumer<T> andThen(Consumer<? super T> after) { 
    Objects.requireNonNull(after); 
    return (T t) -> { accept(t); after.accept(t); }; 
} 

备注:

java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出NullPointerException 异常。

这省去了重复编写if语句和抛出空指针异常的麻烦。

要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是“一步接一步”操作。

例如两个步骤组合 的情况:

public class Demo07ConsumerAndThen {
    public static void main(String[] args) {
        // Lambda表达式 
        test((String s) -> {
            System.out.println(s.toLowerCase());
        }, (String s) -> {
            System.out.println(s.toUpperCase());
        });

        // Lambda表达式简写 
        test(s -> System.out.println(s.toLowerCase()), s ->
                System.out.println(s.toUpperCase()));
    }

    public static void test(Consumer<String> c1, Consumer<String> c2) {
        String str = "Hello World";
        c1.accept(str); // 转小写 
        c2.accept(str); // 转大写
        c1.andThen(c2).accept(str);
        c2.andThen(c1).accept(str);
    }
} 

运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。

当然,通过链式写法可以实现更多步骤的组 合。

2.3. 函数式型:Function

java.util.function.Function 接口用来根据一个类型的数据得到另一个类型的数据,

前者称为前置条件, 后者称为后置条件。

有参数有返回值。

@FunctionalInterface 
public interface Function<T, R> {  
    public abstract R apply(T t); 
}

使用Lambda表达式将字符串转成数字

Function转换型接口,对apply方法传入的T类型数据进行处理,返回R类型的结果,有参有返回的接口。

使用的场景

例如:将 String 类型转换为 Integer 类型。

public class Demo08Function {
    public static void main(String[] args) {
        // Lambda表达式 
        test((String s) -> {
            return Integer.parseInt(s); // 10 
        });
    }

    public static void test(Function<String, Integer> function) {
        Integer in = function.apply("10");
        System.out.println("in: " + (in + 5));
    }
} 
默认方法:andThen

Function 接口中有一个默认的 andThen 方法,用来进行组合操作。

JDK源代码如:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { 
    Objects.requireNonNull(after); 
    return (T t) -> after.apply(apply(t)); 
}

该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多:

public class Demo09FunctionAndThen {
    public static void main(String[] args) {
        // Lambda表达式 
        test((String s) -> {
            return Integer.parseInt(s);
        }, (Integer i) -> {
            return i * 10;
        });
    }

    public static void test(Function<String, Integer> f1, Function<Integer, Integer> f2) {
// Integer in = f1.apply("66"); // 将字符串解析成为int数字 
// Integer in2 = f2.apply(in);// 将上一步的int数字乘以10 
        Integer in3 = f1.andThen(f2).apply("66");

        System.out.println("in3: " + in3); // 660 
    }
}

第一个操作是将字符串解析成为int数字,第二个操作是乘以10。

两个操作通过 andThen 按照前后顺序组合到了一 起。

请注意,Function的前置条件泛型和后置条件泛型可以相同。

2.4. 断言型:Predicate

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。

这时可以使用 java.util.function.Predicate 接口。

@FunctionalInterface 
public interface Predicate<T> {  
    public abstract boolean test(T t); 
} 

使用Lambda判断一个人名如果超过3个字就认为是很长的名字

对test方法的参数T进行判断,返回boolean类型的结果。

用于条件判断的场景:

public class Demo10Predicate {
    public static void main(String[] args) {
        test(s -> s.length() > 3, "迪丽热巴");
    }

    private static void test(Predicate<String> predicate, String str) {
        boolean veryLong = predicate.test(str);
        System.out.println("名字很长吗:" + veryLong);
    }
}

条件判断的标准是传入的Lambda表达式逻辑,只要名称长度大于3则认为很长。

默认方法:and

既然是条件判断,就会存在与、或、非三种常见的逻辑关系。

其中将两个 Predicate 条件使用“与”逻辑连接起来实 现“并且”的效果时,可以使用default方法 and 。

其JDK源码为:

default Predicate<T> and(Predicate<? super T> other) { 
    Objects.requireNonNull(other); 
    return (t) -> test(t) && other.test(t); 
} 

使用Lambda表达式判断一个字符串中即包含W,也包含H

使用Lambda表达式判断一个字符串中包含W或者包含H

使用Lambda表达式判断一个字符串中即不包含W

public class Demo10Predicate_And_Or_Negate {
    public static void main(String[] args) {
        // Lambda表达式 
        test((String s) -> {
            return s.contains("H");
        }, (String s) -> {
            return s.contains("W");
        });
    }

    public static void test(Predicate<String> p1, Predicate<String> p2) {
        String str = "HelloWorld";
        boolean b1 = p1.test(str); // 判断包含大写“H” 
        boolean b2 = p2.test(str); // 判断包含大写“W” 
// if (b1 && b2) { 
// System.out.println("即包含W,也包含H"); 
// } 

        boolean bb = p1.and(p2).test(str);
        if (bb) {
            System.out.println("即包含W,也包含H");
        }
    }
}
默认方法:or

使用Lambda表达式判断一个字符串中包含W或者包含H

与 and 的“与”类似,默认方法 or 实现逻辑关系中的“或”。

JDK源码为:

default Predicate<T> or(Predicate<? super T> other) { 
    Objects.requireNonNull(other); 
    return (t) -> test(t) || other.test(t); 
} 

如果希望实现逻辑“字符串包含大写H或者包含大写W”,那么代码只需要将“and”修改为“or”名称即可,

其他都不变:

public class Demo10Predicate_And_Or_Negate {
    public static void main(String[] args) {
        // Lambda表达式 
        test((String s) -> {
            return s.contains("H");
        }, (String s) -> {
            return s.contains("W");
        });
    }

    public static void test(Predicate<String> p1, Predicate<String> p2) {
        String str = "HelloWorld";
        boolean b1 = p1.test(str); // 判断包含大写“H” 
        boolean b2 = p2.test(str); // 判断包含大写“W” 

// if (b1 || b2) { 
// System.out.println("有H,或者W"); 
// } 
        boolean bbb = p1.or(p2).test(str);
        if (bbb) {
            System.out.println("有H,或者W");
        }
    }
}
默认方法:negate

使用Lambda表达式判断一个字符串中即不包含W

“与”、“或”已经了解了,剩下的“非”(取反)也会简单。

默认方法 negate 的JDK源代码为:

default Predicate<T> negate() { 
    return (t) -> !test(t); 
}

从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行“!”取反而已。

一定要在 test 方法调用之前调 用 negate 方法,正如 and 和 or 方法一样:

public class Demo10Predicate_And_Or_Negate {
    public static void main(String[] args) {
        // Lambda表达式 
        test((String s) -> {
            return s.contains("H");
        }, (String s) -> {
            return s.contains("W");
        });
    }

    public static void test(Predicate<String> p1, Predicate<String> p2) {
        String str = "HelloWorld";
        boolean b1 = p1.test(str); // 判断包含大写“H” 
        boolean b2 = p2.test(str); // 判断包含大写“W” 

        // 没有H,就打印 
// if (!b1) { 
// System.out.println("没有H"); 
// } 
        boolean test = p1.negate().test(str);
        if (test) {
            System.out.println("没有H");
        }
    }
} 

2.5. 二进制操作:BinaryOperator

BinaryOperator是一个函数接口,它集成BiFunction

BinaryOperator接收两个同样类型的实参,返回和参数同样类型的结果类型

BinaryOperator.java

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
    
}

BiFunction接收两个任意类型的实参,返回任意类型的结果

BiFunction.java

@FunctionalInterface
public interface BiFunction<T, U, R> {
      R apply(T t, U u);
}
BinaryOperator

下面这个示例中,BiFunction<Integer, Integer, Integer>接收参数类型和返回结果的类型是一样的,因此可以用

BianryOperator替换

import java.util.function.BiFunction;
import java.util.function.BinaryOperator;

public class Java8BinaryOperator1 {

    public static void main(String[] args) {

        // BiFunction
        BiFunction<Integer, Integer, Integer> func = (x1, x2) -> x1 + x2;

        Integer result = func.apply(2, 3);

        System.out.println(result); // 5

        // BinaryOperator
        BinaryOperator<Integer> func2 = (x1, x2) -> x1 + x2;

        Integer result2 = func.apply(2, 3);

        System.out.println(result2); // 5

    }
}

输出

5
5
BinaryOperator作为参数

下面示例演示了使用stream.reduce()来对所有Integer进行求和

import java.util.Arrays;
import java.util.List;
import java.util.function.BinaryOperator;

public class Java8BinaryOperator2 {

    public static void main(String[] args) {

        Integer[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

        Integer result = math(Arrays.asList(numbers), 0, (a, b) -> a + b);

        System.out.println(result); // 55

        Integer result2 = math(Arrays.asList(numbers), 0, Integer::sum);

        System.out.println(result2); // 55

    }

    public static <T> T math(List<T> list, T init, BinaryOperator<T> accumulator) {
        T result = init;
        for (T t : list) {
            result = accumulator.apply(result, t);
        }
        return result;
    }
}

输出

55
55
IntBinaryOperator

如果数学操作基本数据类型,比如int,那么使用IntBinaryOperator可以获得更好的性能。

import java.util.function.IntBinaryOperator;
import java.util.stream.IntStream;

public class Java8BinaryOperator3 {

    public static void main(String[] args) {

        int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

        int result = math((numbers), 0, (a, b) -> a + b);

        System.out.println(result); // 55

        int result2 = math((numbers), 0, Integer::sum);

        System.out.println(result2); // 55
    }

    public static int math(int[] list, int init, IntBinaryOperator accumulator) {
        int result = init;
        for (int t : list) {
            result = accumulator.applyAsInt(result, t);
        }
        return result;
    }
}

输出

55
55
BinaryOperator::maxBy()和BinaryOperator.minBy()

下面示例展示了使用BinaryOperator和自定义的比较器从开发者列表中找到薪资最好和最低的开发者

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.BinaryOperator;

public class Java8BinaryOperator4 {

    public static void main(String[] args) {

        Developer dev1 = new Developer("jordan", BigDecimal.valueOf(9999));
        Developer dev2 = new Developer("jack", BigDecimal.valueOf(8888));
        Developer dev3 = new Developer("jaden", BigDecimal.valueOf(10000));
        Developer dev4 = new Developer("ali", BigDecimal.valueOf(2000));
        Developer dev5 = new Developer("mkyong", BigDecimal.valueOf(1));

        List<Developer> list = Arrays.asList(dev1, dev2, dev3, dev4, dev5);

        // 1. Create a Comparator
        Comparator<Developer> comparing = Comparator.comparing(Developer::getSalary);

        // 2. BinaryOperator with a custom Comparator
        BinaryOperator<Developer> bo = BinaryOperator.maxBy(comparing);

        Developer result = find(list, bo);

        System.out.println(result);     // Developer{name='jaden', salary=10000}

        // one line

        // find developer with highest pay
        Developer developer = find(list, BinaryOperator.maxBy(Comparator.comparing(Developer::getSalary)));
        System.out.println(developer);  // Developer{name='jaden', salary=10000}

        // find developer with lowest pay
        Developer developer2 = find(list, BinaryOperator.minBy(Comparator.comparing(Developer::getSalary)));
        System.out.println(developer2); // Developer{name='mkyong', salary=1}

    }

    public static Developer find(List<Developer> list, BinaryOperator<Developer> accumulator) {
        Developer result = null;
        for (Developer t : list) {
            if (result == null) {
                result = t;
            } else {
                result = accumulator.apply(result, t);
            }
        }
        return result;
    }
}

Developer.java

import java.math.BigDecimal;

public class Developer {

    String name;
    BigDecimal salary;

    public Developer(String name, BigDecimal salary) {
        this.name = name;
        this.salary = salary;
    }

    //...
}

输出

Developer{name='jaden', salary=10000}
Developer{name='jaden', salary=10000}
Developer{name='mkyong', salary=1}

五、方法引用

1. Lambda的冗余场景

使用Lambda表达式求一个数组的和

public class Demo11MethodRefIntro {
    public static void getMax(int[] arr) {
        int sum = 0;
        for (int n : arr) {
            sum += n;
        }
        System.out.println(sum);
    }

    public static void main(String[] args) {
        printMax((int[] arr) -> {
            int sum = 0;
            for (int n : arr) {
                sum += n;
            }
            System.out.println(sum);
        });
    }

    private static void printMax(Consumer<int[]> consumer) {
        int[] arr = {10, 20, 30, 40, 50};
        consumer.accept(arr);
    }
}

2. 体验方法引用简化Lambda

如果我们在Lambda中所指定的功能,已经有其他方法存在相同方案,那是否还有必要再写重复逻辑?

可以直接“引 用”过去就好了:

public class DemoPrintRef {
    public static void getMax(int[] arr) {
        int sum = 0;
        for (int n : arr) {
            sum += n;
        }
        System.out.println(sum);
    }

    public static void main(String[] args) {
        printMax(Demo11MethodRefIntro::getMax);
    }

    private static void printMax(Consumer<int[]> consumer) {
        int[] arr = {10, 20, 30, 40, 50};
        consumer.accept(arr);
    }
} 

请注意其中的双冒号 :: 写法,这被称为“方法引用”,是一种新的语法。

3. 方法引用的格式

符号表示:::

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

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

4. 常见引用方式

方法引用在JDK 8中使用方式相当灵活,有以下几种形式:

  1. instanceName::methodName 对象::方法名
  2. ClassName::staticMethodName 类名::静态方法
  3. ClassName::methodName 类名::普通方法
  4. ClassName::new 类名::new 调用的构造器
  5. TypeName[]::new String[]::new 调用数组的构造器

对象名::引用成员方法

这是最常见的一种用法,与上例相同。

如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代 码为:

    // 对象::实例方法 
    @Test
    public void test01() {
        Date now = new Date();
        Supplier<Long> supp = () -> {
            return now.getTime();
        };

        System.out.println(supp.get());

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

方法引用的注意事项

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

类名::引用静态方法

由于在 java.lang.System 类中已经存在了静态方法 currentTimeMillis ,

所以当我们需要通过Lambda来调用该 方法时,可以使用方法引用 , 写法是:

    // 类名::静态方法 
    @Test
    public void test02() {
        Supplier<Long> supp = () -> {
            return System.currentTimeMillis();
        };
        System.out.println(supp.get());

        Supplier<Long> supp2 = System::currentTimeMillis;
        System.out.println(supp2.get());
    }

类名::引用实例方法

Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,

实际上是拿第一个参数作为方法的调用者。

    // 类名::实例方法 
    @Test
    public void test03() {
        Function<String, Integer> f1 = (s) -> {
            return s.length();
        };
        System.out.println(f1.apply("abc"));

        Function<String, Integer> f2 = String::length;
        System.out.println(f2.apply("abc"));

        BiFunction<String, Integer, String> bif = String::substring;
        String hello = bif.apply("hello", 2);
        System.out.println("hello = " + hello);
    }

类名::new引用构造器

由于构造器的名称与类名完全一样。所以构造器引用使用 类名称::new 的格式表示。

首先是一个简单的 Person 类:

    public class Person {
        private String name;
        public Person(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
    }

要使用这个函数式接口,可以通过方法引用传递:

    // 类名::new 
    @Test
    public void test04() {
        Supplier<Person> sup = () -> {
            return new Person();
        };
        System.out.println(sup.get());

        Supplier<Person> sup2 = Person::new;
        System.out.println(sup2.get());

        BiFunction<String, Integer, Person> fun2 = Person::new;
        System.out.println(fun2.apply("张三", 18));
    }

数组::new 引用数组构造器

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。

    // 类型[]::new 
    @Test
    public void test05() {
        Function<Integer, String[]> fun = (len) -> {
            return new String[len];
        };
        String[] arr1 = fun.apply(10);
        System.out.println(arr1 + ", " + arr1.length);

        Function<Integer, String[]> fun2 = String[]::new;
        String[] arr2 = fun.apply(5);
        System.out.println(arr2 + ", " + arr2.length);
    }

知识小结

方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解

为Lambda表达式的缩写形式 , 不过要注意的是方法引用只能"引用"已经存在的方法!

六、Stream流

1. 集合处理数据的弊端

1.1 前言

当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。

我们来体验集合操作数据的弊端,需求如下:

一个ArrayList集合中存储有以下数据:张无忌,周芷若,赵敏,张强,张三丰

需求:

  1. 拿到所有姓张的
  2. 拿到名字长度为3个字的 3.打印这些数据

代码如下:

    public static void main(String[] args) {
        // 一个ArrayList集合中存储有以下数据:张无忌,周芷若,赵敏,张强,张三丰 
        // 需求:1.拿到所有姓张的 2.拿到名字长度为3个字的 3.打印这些数据 
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");

        // 1.拿到所有姓张的 
        ArrayList<String> zhangList = new ArrayList<>(); // {"张无忌", "张强", "张三丰"} 
        for (String name : list) {
            if (name.startsWith("张")) {
                zhangList.add(name);
            }
        }

        // 2.拿到名字长度为3个字的 
        ArrayList<String> threeList = new ArrayList<>(); // {"张无忌", "张三丰"} 
        for (String name : zhangList) {
            if (name.length() == 3) {
                threeList.add(name);
            }
        }

        // 3.打印这些数据 
        for (String name : threeList) {
            System.out.println(name);
        }
    }

1.2 循环遍历的弊端

1. 分析

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

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

**每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?**不是

循环是做事情的方式,而不是目的。每个需求都要循环一次,还要搞一个新集合来装数据,

如果希望再次遍历,只能再使用另一个循环从头开始。

那Stream能给我们带来怎样更加优雅的写法呢?

2. Stream的更优写法

下面来看一下借助Java 8的Stream API,修改后的代码:

    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、逐一打印

我们真正要做的事情内容被更好地体现在代码中。

1.3 Stream流式思想

1. 概述

注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象!

Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工 处理。

Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。

Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。

2. 总结

首先我们了解了集合操作数据的弊端,每次都需要循环遍历,还要创建新集合,很麻烦

Stream是流式思想,相当于工厂的流水线,对集合中的数据进行加工处理

2. 获取Stream流的方式:两种

java.util.stream.Stream 是JDK 8新加入的流接口。

获取一个流非常简单,有以下几种常用的方式:

  • 所有的 Collection 集合都可以通过 stream 默认方法获取流;
  • Stream 接口的静态方法 of 可以获取数组对应的流。

2.1 根据Collection获取流

首先, java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流。

public interface Collection {
    default Stream<E> stream()
}
public class Demo04GetStream {
    public static void main(String[] args) {
        // 集合获取流
        // Collection接口中的方法: default Stream<E> stream() 获取流
        List<String> list = new ArrayList<>();
        // ...
        Stream<String> stream1 = list.stream();

        Set<String> set = new HashSet<>();
        // ...
        Stream<String> stream2 = set.stream();

        Vector<String> vector = new Vector<>();
        // ...
        Stream<String> stream3 = vector.stream();
    }
}

java.util.Map 接口不是 Collection 的子接口,所以获取对应的流需要分key、value或entry等情况:

public class Demo05GetStream {
    public static void main(String[] args) {
        // Map获取流 
        Map<String, String> map = new HashMap<>();
        // ... 
        Stream<String> keyStream = map.keySet().stream();
        Stream<String> valueStream = map.values().stream();
        Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
    }
} 

2.2 Stream中的静态方法of获取流

由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单:

public class Demo06GetStream {
    public static void main(String[] args) {
        // Stream中的静态方法: static Stream of(T... values) 
        Stream<String> stream6 = Stream.of("aa", "bb", "cc");

        String[] arr = {"aa", "bb", "cc"};
        Stream<String> stream7 = Stream.of(arr);

        Integer[] arr2 = {11, 22, 33};
        Stream<Integer> stream8 = Stream.of(arr2);

        // 注意:基本数据类型的数组不行 
        int[] arr3 = {11, 22, 33};
        Stream<int[]> stream9 = Stream.of(arr3);
    }
}

备注: of 方法的参数其实是一个可变参数,所以支持数组。

3. Stream常用方法和注意事项

3.1 Stream常用方法

Stream流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

方法名

方法作用

返回值类型

方法种类

count

统计个数

long

终结

forEach

逐一处理

void

终结

filter

过滤

Stream

函数拼接

limit

取用前几个

Stream

函数拼接

skip

跳过前几个

Stream

函数拼接

map

映射

Stream

函数拼接

concat

组合

Stream

函数拼接

  • **终结方法:**返回值类型不再是 Stream 类型的方法,不再支持链式调用。本小节中,终结方法包括 count 和forEach 方法。
  • **非终结方法:**返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结 方法。)

备注:本小节之外的更多方法,请自行参考API文档。

3.2 Stream注意事项

  1. Stream只能操作一次
  2. Stream方法返回的是新的流
  3. Stream不调用终结方法,中间的操作不会执行

4. Stream流各方法使用

4.1 forEach

forEach 用来遍历流中的数据

void forEach(Consumer<? super T> action); 

该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。

例如:

    @Test
    public void testForEach() {
        List<String> one = new ArrayList<>();
        Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子"); 
 
 /*one.stream().forEach((String s) -> { 
 System.out.println(s); 
 });*/

        // 简写 
        // one.stream().forEach(s -> System.out.println(s)); 

        one.stream().forEach(System.out::println);
    }

4.2 count

Stream流提供 count 方法来统计其中的元素个数:

long count(); 

该方法返回一个long值代表元素个数。

基本使用:

    @Test
    public void testCount() {
        List<String> one = new ArrayList<>();
        Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");

        System.out.println(one.stream().count());
    }

4.3 filter

filter用于过滤数据,返回符合过滤条件的数据

可以通过 filter 方法将一个流转换成另一个子集流。

方法声明:

Stream<T> filter(Predicate<? super T> predicate); 

该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。

Stream流中的 filter 方法基本使用的代码如下:

    @Test
    public void testFilter() {
        List<String> one = new ArrayList<>();
        Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");

        one.stream().filter(s -> s.length() == 2).forEach(System.out::println);
    }

在这里通过Lambda表达式来指定了筛选的条件:姓名长度为2个字。

4.4 limit

limit 方法可以对流进行截取,只取用前n个。

方法签名:

Stream<T> limit(long maxSize); 

参数是一个long型,如果集合当前长度大于参数则进行截取。否则不进行操作。

基本使用:

    @Test
    public void testLimit() {
        List<String> one = new ArrayList<>();
        Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");

        one.stream().limit(3).forEach(System.out::println);
    }

4.5 skip

如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:

Stream<T> skip(long n); 

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。

基本使用:

    @Test
    public void testSkip() {
        List<String> one = new ArrayList<>();
        Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");

        one.stream().skip(2).forEach(System.out::println);
    }

4.6 map

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

方法签名:

<R> Stream<R> map(Function<? super T, ? extends R> mapper); 

该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

Stream流中的 map 方法基本使用的代码如下:

    @Test
    public void testMap() {
        Stream<String> original = Stream.of("11", "22", "33");
        Stream<Integer> result = original.map(Integer::parseInt);
        result.forEach(s -> System.out.println(s + 10));
    }

这段代码中, map 方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为 Integer 类对象)。

4.7 sorted

如果需要将数据排序,可以使用 sorted 方法。

方法签名:

Stream<T> sorted(); 
Stream<T> sorted(Comparator<? super T> comparator); 

基本使用

Stream流中的 sorted 方法基本使用的代码如下:

    @Test
    public void testSorted() {
        // sorted(): 根据元素的自然顺序排序 
        // sorted(Comparator<? super T> comparator): 根据比较器指定的规则排序 
        Stream.of(33, 22, 11, 55)
                .sorted()
                .sorted((o1, o2) -> o2 - o1)
                .forEach(System.out::println);
    }

这段代码中, sorted 方法根据元素的自然顺序排序,也可以指定比较器排序。

4.8 distinct

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

方法签名:

Stream<T> distinct(); 

基本使用

Stream流中的 distinct 方法基本使用的代码如下:

    @Test
    public void testDistinct() {
        Stream.of(22, 33, 22, 11, 33)
                .distinct()
                .forEach(System.out::println);
    }

如果是自定义类型如何是否也能去除重复的数据呢?

    @Test
    public void testDistinct2() {
        Stream.of(
                new Person("刘德华", 58),
                new Person("张学友", 56),
                new Person("张学友", 56),
                new Person("黎明", 52))
                .distinct()
                .forEach(System.out::println);
    }
public class Person {
    private String name;
    private int age;
    // 省略其他 
} 

自定义类型是根据对象的hashCode和equals来去除重复元素的。

4.9 match

如果需要判断数据是否匹配指定的条件,可以使用 Match 相关方法。

方法签名:

boolean allMatch(Predicate<? super T> predicate); 
boolean anyMatch(Predicate<? super T> predicate); 
boolean noneMatch(Predicate<? super T> predicate);

基本使用

Stream流中的 Match 相关方法基本使用的代码如下:

    @Test
    public void testMatch() {
        boolean b = Stream.of(5, 3, 6, 1)
                // .allMatch(e -> e > 0); // allMatch: 元素是否全部满足条件 
                // .anyMatch(e -> e > 5); // anyMatch: 元素是否任意有一个满足条件 
                .noneMatch(e -> e < 0); // noneMatch: 元素是否全部不满足条件 
        System.out.println("b = " + b);
    }

4.10 find

如果需要找到某些数据,可以使用 find 相关方法。

方法签名:

Optional<T> findFirst(); 
Optional<T> findAny(); 

基本使用

Stream流中的 find 相关方法基本使用的代码如下:

    @Test
    public void testFind() {
        Optional<Integer> first = Stream.of(5, 3, 6, 1).findFirst();
        System.out.println("first = " + first.get());

        Optional<Integer> any = Stream.of(5, 3, 6, 1).findAny();
        System.out.println("any = " + any.get());
    }

4.11 max/min

如果需要获取最大和最小值,可以使用 max 和 min 方法。

方法签名:

Optional<T> max(Comparator<? super T> comparator); 
Optional<T> min(Comparator<? super T> comparator); 

基本使用

Stream流中的 max 和 min 相关方法基本使用的代码如下:

    @Test
    public void testMax_Min() {
        Optional<Integer> max = Stream.of(5, 3, 6, 1).max((o1, o2) -> o1 - o2);
        System.out.println("first = " + max.get());
        Optional<Integer> min = Stream.of(5, 3, 6, 1).min((o1, o2) -> o1 - o2);
        System.out.println("any = " + min.get());
    }

4.12 reduce

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

方法签名:

T reduce(T identity, BinaryOperator<T> accumulator); 

基本使用

Stream流中的 reduce 相关方法基本使用的代码如下:

    @Test
    public void testReduce() {
        int reduce = Stream.of(4, 5, 3, 9)
                .reduce(0, (a, b) -> {
                    System.out.println("a = " + a + ", b = " + b);
                    return a + b;
                });
        // reduce: 
        // 第一次将默认做赋值给x, 取出第一个元素赋值给y,进行操作 
        // 第二次,将第一次的结果赋值给x, 取出二个元素赋值给y,进行操作 
        // 第三次,将第二次的结果赋值给x, 取出三个元素赋值给y,进行操作 
        // 第四次,将第三次的结果赋值给x, 取出四个元素赋值给y,进行操作 
        System.out.println("reduce = " + reduce);

        int reduce2 = Stream.of(4, 5, 3, 9)
                .reduce(0, (x, y) -> {
                    return Integer.sum(x, y);
                });

        int reduce3 = Stream.of(4, 5, 3, 9).reduce(0, Integer::sum);
        int max = Stream.of(4, 5, 3, 9)
                .reduce(0, (x, y) -> {
                    return x > y ? x : y;
                });
        System.out.println("max = " + max);
    }

4.13 map/reduce

    @Test
    public void testMapReduce() {
        // 求出所有年龄的总和 
        int totalAge = Stream.of(
                new Person("刘德华", 58),
                new Person("张学友", 56),
                new Person("郭富城", 54),
                new Person("黎明", 52))
                .map((p) -> p.getAge())
                .reduce(0, (x, y) -> x + y);
        System.out.println("totalAge = " + totalAge);

        // 找出最大年龄 
        int maxAge = Stream.of(
                new Person("刘德华", 58),
                new Person("张学友", 56),
                new Person("郭富城", 54),
                new Person("黎明", 52))
                .map((p) -> p.getAge())
                .reduce(0, (x, y) -> x > y ? x : y);
        System.out.println("maxAge = " + maxAge);

        // 统计 数字2 出现的次数 
        int count = Stream.of(1, 2, 2, 1, 3, 2)
                .map(i -> {
                    if (i == 2) {
                        return 1;
                    } else {
                        return 0;
                    }
                })
                .reduce(0, Integer::sum);
        System.out.println("count = " + count);
    }

4.14 mapToInt

如果需要将Stream中的Integer类型数据转成int类型,可以使用 mapToInt 方法。方法签名:

IntStream mapToInt(ToIntFunction<? super T> mapper); 

基本使用

Stream流中的 mapToInt 相关方法基本使用的代码如下:

    @Test
    public void test1() {
        // Integer占用的内存比int多,在Stream流操作中会自动装箱和拆箱 
        Stream<Integer> stream = Arrays.stream(new Integer[]{1, 2, 3, 4, 5});

        // 把大于3的和打印出来 
        // Integer result = stream 
        // .filter(i -> i.intValue() > 3) 
        // .reduce(0, Integer::sum); 
        // System.out.println(result); 

        // 先将流中的Integer数据转成int,后续都是操作int类型 
        IntStream intStream = stream.mapToInt(Integer::intValue);
        int reduce = intStream
                .filter(i -> i > 3)
                .reduce(0, Integer::sum);
        System.out.println(reduce);

        // 将IntStream转化为Stream<Integer> 
        IntStream intStream1 = IntStream.rangeClosed(1, 10);
        Stream<Integer> boxed = intStream1.boxed();
        boxed.forEach(s -> System.out.println(s.getClass() + ", " + s));
    }

4.15 concat

如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

备注:这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的。

该方法的基本使用代码如下:

    @Test
    public void testContact() {
        Stream<String> streamA = Stream.of("张三");
        Stream<String> streamB = Stream.of("李四");
        Stream<String> result = Stream.concat(streamA, streamB);
        result.forEach(System.out::println);
    }

4.16 综合案例

1. 需求

现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下 若干操作步骤:

  1. 第一个队伍只要名字为3个字的成员姓名;
  2. 第一个队伍筛选之后只要前3个人;
  3. 第二个队伍只要姓张的成员姓名;
  4. 第二个队伍筛选之后不要前2个人;
  5. 将两个队伍合并为一个队伍;
  6. 根据姓名创建 Person 对象;
  7. 打印整个队伍的Person对象信息。

两个队伍(集合)的代码如下:

    public class DemoArrayListNames {
        public static void main(String[] args) {
            List<String> one = List.of("迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七
                    公"); 
                    List<String> two = List.of("古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱",
                            "张三");

            // .... 
        }
    }

而 Person 类的代码为:

public class Person {

    private String name;

    public Person() {}

    public Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "'}";
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
2. 实现方式
2.1 传统方式

使用for循环 , 示例代码:

public class DemoArrayListNames {
    public static void main(String[] args) {
        List<String> one = List.of("迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七
                公"); 
                List<String> two = List.of("古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱",
                        "张三");

        // 第一个队伍只要名字为3个字的成员姓名; 
        List<String> oneA = new ArrayList<>();
        for (String name : one) {
            if (name.length() == 3) {
                oneA.add(name);
            }
        }

        // 第一个队伍筛选之后只要前3个人; 
        List<String> oneB = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            oneB.add(oneA.get(i));
        }

        // 第二个队伍只要姓张的成员姓名; 
        List<String> twoA = new ArrayList<>();
        for (String name : two) {
            if (name.startsWith("张")) {
                twoA.add(name);
            }
        }

        // 第二个队伍筛选之后不要前2个人; 
        List<String> twoB = new ArrayList<>();
        for (int i = 2; i < twoA.size(); i++) {
            twoB.add(twoA.get(i));
        }

        // 将两个队伍合并为一个队伍; 
        List<String> totalNames = new ArrayList<>();
        totalNames.addAll(oneB);
        totalNames.addAll(twoB);

        // 根据姓名创建Person对象; 
        List<Person> totalPersonList = new ArrayList<>();
        for (String name : totalNames) {
            totalPersonList.add(new Person(name));
        }

        // 打印整个队伍的Person对象信息。 
        for (Person person : totalPersonList) {
            System.out.println(person);
        }
    }
}

运行结果为:

Person{name='宋远桥'} 
Person{name='苏星河'} 
Person{name='洪七公'} 
Person{name='张二狗'} 
Person{name='张天爱'} 
Person{name='张三'} 
2.2 Stream方式

等效的Stream流式处理代码为:

public class DemoStreamNames {
    public static void main(String[] args) {
        List<String> one = List.of("迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七
                公"); 
                List<String> two = List.of("古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱",
                        "张三");

        // 第一个队伍只要名字为3个字的成员姓名; 
        // 第一个队伍筛选之后只要前3个人; 
        Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);

        // 第二个队伍只要姓张的成员姓名; 
        // 第二个队伍筛选之后不要前2个人; 
        Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("张")).skip(2);

        // 将两个队伍合并为一个队伍; 
        // 根据姓名创建Person对象; 
        // 打印整个队伍的Person对象信息。 
        Stream.concat(streamOne, streamTwo).map(Person::new).forEach(System.out::println);
    }
}

运行效果完全一样:

Person{name='宋远桥'} 
Person{name='苏星河'} 
Person{name='洪七公'} 
Person{name='张二狗'} 
Person{name='张天爱'} 
Person{name='张三'} 

5. Stream流其他操作

5.1 收集Stream流中的结果

IntStream intStream = Stream.of(1, 2, 3, 4, 5).mapToInt(Integer::intValue); 
intStream.filter(n -> n > 3).forEach(System.out::println); 
intStream.filter(n -> n > 3).count; 
intStream.filter(n -> n > 3).reduce(0, Integer::sum); 

对流操作完成之后,如果需要将流的结果保存到数组或集合中,可以收集流中的数据

1. Stream流中的结果到集合中

Stream流提供 collect 方法,其参数需要一个 java.util.stream.Collector 接口对象来指定收集到哪 种集合中。

java.util.stream.Collectors 类提供一些方法,可以作为 Collector 接口的实例:

  • public static Collector> toList() :转换为 List 集合。
  • public static Collector> toSet() :转换为 Set 集合。

下面是这两个方法的基本使用代码:

    // 将流中数据收集到集合中 
    @Test
    public void testStreamToCollection() {
        Stream<String> stream = Stream.of("aa", "bb", "cc");
        // List<String> list = stream.collect(Collectors.toList()); 
        // Set<String> set = stream.collect(Collectors.toSet()); 

        ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
        HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));
    }
2. Stream流中的结果到数组中

Stream提供 toArray 方法来将结果放到一个数组中,返回值类型是Object[]的:

Object[] toArray(); 

其使用场景如下:

    @Test
    public void testStreamToArray() {
        Stream<String> stream = Stream.of("aa", "bb", "cc");

        // Object[] objects = stream.toArray(); 
        // for (Object obj : objects) { 
        // System.out.println(); 
        // } 

        String[] strings = stream.toArray(String[]::new);
        for (String str : strings) {
            System.out.println(str);
        }
    }

5.2 对流中数据进行聚合计算

当我们使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作。

比如获取最大值,获取最小 值,求总和,平均值,统计数量。

    @Test
    public void testStreamToOther() {
        Stream<Student> studentStream = Stream.of(
                new Student("赵丽颖", 58, 95),
                new Student("杨颖", 56, 88),
                new Student("迪丽热巴", 56, 99),
                new Student("柳岩", 52, 77));
        // 获取最大值 
        // Optional<Student> collect = studentStream.collect(Collectors.maxBy((o1, o2) -> 
        o1.getSocre() - o2.getSocre()));

        // 获取最小值 
        // Optional<Student> collect = studentStream.collect(Collectors.minBy((o1, o2) -> 
        o1.getSocre() - o2.getSocre()));
        // System.out.println(collect.get()); 

        // 求总和 
        // int sumAge = studentStream.collect(Collectors.summingInt(s -> s.getAge())); 
        // System.out.println("sumAge = " + sumAge); 

        // 平均值 
        // double avgScore = studentStream.collect(Collectors.averagingInt(s -> s.getSocre())); 
        // System.out.println("avgScore = " + avgScore); 

        // 统计数量 
        // Long count = studentStream.collect(Collectors.counting()); 
        // System.out.println("count = " + count); 
    }

5.3 对流中数据进行分组

当我们使用Stream流处理数据后,可以根据某个属性将数据分组:

    // 分组 
    @Test
    public void testGroup() {
        Stream<Student> studentStream = Stream.of(
                new Student("赵丽颖", 52, 95),
                new Student("杨颖", 56, 88),
                new Student("迪丽热巴", 56, 55),
                new Student("柳岩", 52, 33));

        // Map<Integer, List<Student>> map = 
        studentStream.collect(Collectors.groupingBy(Student::getAge));

        // 将分数大于60的分为一组,小于60分成另一组 
        Map<String, List<Student>> map = studentStream.collect(Collectors.groupingBy((s) -> {
            if (s.getSocre() > 60) {
                return "及格";
            } else {
                return "不及格";
            }
        }));

        map.forEach((k, v) -> {
            System.out.println(k + "::" + v);
        });
    }

效果:

不及格::[Student{name='迪丽热巴', age=56, socre=55}, Student{name='柳岩', age=52, socre=33}] 
及格::[Student{name='赵丽颖', age=52, socre=95}, Student{name='杨颖', age=56, socre=88}] 

5.4 对流中数据进行多级分组

还可以对数据进行多级分组:

    // 多级分组 
    @Test
    public void testCustomGroup() {
        Stream<Student> studentStream = Stream.of(
                new Student("赵丽颖", 52, 95),
                new Student("杨颖", 56, 88),
                new Student("迪丽热巴", 56, 99),
                new Student("柳岩", 52, 77));

        Map<Integer, Map<String, List<Student>>> map =
                studentStream.collect(Collectors.groupingBy(s -> s.getAge(), Collectors.groupingBy(s -> {
                    if (s.getSocre() >= 90) {
                        return "优秀";
                    } else if (s.getSocre() >= 80 && s.getSocre() < 90) {
                        return "良好";
                    } else if (s.getSocre() >= 80 && s.getSocre() < 80) {
                        return "及格";
                    } else {
                        return "不及格";
                    }
                })));

        map.forEach((k, v) -> {
            System.out.println(k + " == " + v);
        });
    }

效果:

52 == {不及格=[Student{name='柳岩', age=52, socre=77}], 优秀=[Student{name='赵丽颖', age=52, 
socre=95}]} 
56 == {优秀=[Student{name='迪丽热巴', age=56, socre=99}], 良好=[Student{name='杨颖', age=56, 
socre=88}]} 

5.5 对流中数据进行分区

Collectors.partitioningBy 会根据值是否为true,把集合分割为两个列表,一个true列表,一个false列表。

    // 分区 
    @Test
    public void testPartition() {
        Stream<Student> studentStream = Stream.of(
                new Student("赵丽颖", 52, 95),
                new Student("杨颖", 56, 88),
                new Student("迪丽热巴", 56, 99),
                new Student("柳岩", 52, 77));

        // partitioningBy会根据值是否为true,把集合分割为两个列表,一个true列表,一个false列表。 
        Map<Boolean, List<Student>> map = studentStream.collect(Collectors.partitioningBy(s ->
                s.getSocre() > 90));

        map.forEach((k, v) -> {
            System.out.println(k + " == " + v);
        });
    }

效果:

false == [Student{name='杨颖', age=56, socre=88}, Student{name='柳岩', age=52, socre=77}] 
true == [Student{name='赵丽颖', age=52, socre=95}, Student{name='迪丽热巴', age=56, socre=99}] 

5.6 对流中数据进行拼接

Collectors.joining 会根据指定的连接符,将所有元素连接成一个字符串。

    // 拼接 
    @Test
    public void testJoining() {
        Stream<Student> studentStream = Stream.of(
                new Student("赵丽颖", 52, 95),
                new Student("杨颖", 56, 88),
                new Student("迪丽热巴", 56, 99),
                new Student("柳岩", 52, 77));
        String collect = studentStream
                .map(Student::getName)
                .collect(Collectors.joining(">_<", "^_^", "^v^"));
        System.out.println(collect);
    }

效果:

^_^赵丽颖>_<杨颖>_<迪丽热巴>_<柳岩^v^ 

参考链接

5.7 总结

收集Stream流中的结果 到集合中: Collectors.toList()/Collectors.toSet()/Collectors.toCollection()到数组中: toArray()/toArray(int[]::new)

聚合计算: Collectors.maxBy/Collectors.minBy/Collectors.counting/Collectors.summingInt/Collectors.averagingInt

分组: Collectors.groupingBy

分区: Collectors.partitionBy

拼接: Collectors.joinging

七、并行的Stream流

1. 串行的Stream流

目前我们使用的Stream流是串行的,就是在一个线程上执行。

    @Test
    public void test0Serial() {
        long count = Stream.of(4, 5, 3, 9, 1, 2, 6)
                .filter(s -> {
                    System.out.println(Thread.currentThread() + ", s = " + s);
                    return true;
                })
                .count();

        System.out.println("count = " + count);
    }

效果:

Thread[main,5,main], s = 4 
Thread[main,5,main], s = 5 
Thread[main,5,main], s = 3 
Thread[main,5,main], s = 9 
Thread[main,5,main], s = 1 
Thread[main,5,main], s = 2 
Thread[main,5,main], s = 6 

2. 并行的Stream流

parallelStream其实就是一个并行执行的流。它通过默认的ForkJoinPool,可能提高多线程任务的速度

3. 获取并行Stream流的两种方式

  1. 直接获取并行流: parallelStream()
  2. 将串行流转成并行流: parallel()
    @Test
    public void testgetParallelStream() {
        ArrayList<Integer> list = new ArrayList<>();
        // 直接获取并行的流 
        // Stream<Integer> stream = list.parallelStream(); 
        // 将串行流转成并行流 
        Stream<Integer> stream = list.stream().parallel();
    }

并行操作代码:

    @Test
    public void test0Parallel() {
        long count = Stream.of(4, 5, 3, 9, 1, 2, 6)
                .parallel() // 将流转成并发流,Stream处理的时候将才去 
                .filter(s -> {
                    System.out.println(Thread.currentThread() + ", s = " + s);
                    return true;
                })
                .count();

        System.out.println("count = " + count);
    }

效果:

Thread[ForkJoinPool.commonPool-worker-13,5,main], s = 3 
Thread[ForkJoinPool.commonPool-worker-19,5,main], s = 6 
Thread[main,5,main], s = 1 
Thread[ForkJoinPool.commonPool-worker-5,5,main], s = 5 
Thread[ForkJoinPool.commonPool-worker-23,5,main], s = 4 
Thread[ForkJoinPool.commonPool-worker-27,5,main], s = 2 
Thread[ForkJoinPool.commonPool-worker-9,5,main], s = 9 
count = 7

4. 并行和串行Stream流的效率对比

使用for循环,串行Stream流,并行Stream流来对5亿个数字求和。看消耗的时间。

public class Demo06 {
    private static long times = 50000000000L;
    private long start;
    @Before
    public void init() {
        start = System.currentTimeMillis();
    }

    @After
    public void destory() {
        long end = System.currentTimeMillis();
        System.out.println("消耗时间: " + (end - start));
    }

    // 测试效率,parallelStream 120 
    @Test
    public void parallelStream() {
        System.out.println("serialStream");
        LongStream.rangeClosed(0, times)
                .parallel()
                .reduce(0, Long::sum);
    }

    // 测试效率,普通Stream 342 
    @Test
    public void serialStream() {
        System.out.println("serialStream");
        LongStream.rangeClosed(0, times)
                .reduce(0, Long::sum);
    }


    // 测试效率,正常for循环 421 
    @Test
    public void forAdd() {
        System.out.println("forAdd");
        long result = 0L;
        for (long i = 1L; i < times; i++) {
            result += i;
        }
    }
}

我们可以看到parallelStream的效率是最高的。

Stream并行处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作。

5. parallelStream线程安全问题

解决parallelStream线程安全问题

    // 并行流注意事项 
    @Test
    public void parallelStreamNotice() {
        ArrayList<Integer> list = new ArrayList<Integer>();
        for (int i = 0; i < 1000; i++) {
            list.add(i);
        }

        List<Integer> newList = new ArrayList<>();
        // 使用并行的流往集合中添加数据 
        list.parallelStream()
                .forEach(s -> {
                    newList.add(s);
                });

        System.out.println("newList = " + newList.size());
    }

运行效果:

newList = 903

我们明明是往集合中添加1000个元素,而实际上只有903个元素。

解决方法: 加锁、使用线程安全的集合或者调用Stream的 toArray() / collect() 操作就是满足线程安全的了。

6. parallelStream背后的技术

6.1 Fork/Join框架介绍

parallelStream使用的是Fork/Join框架,Fork/Join框架自JDK 7引入,Fork/Join框架可以将一个大任务拆分为很多小任务来异步执行

Fork/Join框架主要包含三个模块:

  1. 线程池:ForkJoinPool
  2. 任务对象:ForkJoinTask
  3. 执行任务的线程:ForkJoinWorkerThread

6.2 Fork/Join原理

1. 分治法

ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。

典型的应用比如快速排序算法,ForkJoinPool需要使用相对少的线程来处理大量的任务。

比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。

以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。

比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。

那么到最后,所有的任务加起来会有大概2000000+个。

问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。

2. 工作窃取算法

Fork/Join最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的cpu,那么如何利用好这个空闲的cpu就成了提高性能

的关键,而这里我们要提到的工作窃取(work-stealing)算法就是整个Fork/Join框架的核心理念。

Fork/Join工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。

那么为什么需要使用工作窃取算法呢?

假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖 的子任务,为了减少线程间的竞争,于是把这些子任务分

别放到不同的队列里,并为每个队列创建一个单独的线程来 执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任

务。但是有的线程会先把自己队列里的 任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他

线程干活,于是它就 去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取

任 务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永 远从双端队列的

尾部拿任务执行。 工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,

比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

上文中已经提到了在Java 8引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行,也就是我们使用了

ForkJoinPool的ParallelStream。

对于ForkJoinPool通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。

可以通过设置 系统属性:java.util.concurrent.ForkJoinPool.common.parallelism=N(N为线程数量),来调整ForkJoinPool的线程数

量,可以尝试调整成不同的参数来观察每次的输出结果。

6.3 Fork/Join案例

需求:使用Fork/Join计算1-10000的和,当一个任务的计算数量大于3000时拆分任务,数量小于3000时计算。

public class Demo07ForkJoin {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        ForkJoinPool pool = new ForkJoinPool();
        SumRecursiveTask task = new SumRecursiveTask(1, 10000L);
        Long result = pool.invoke(task);
        System.out.println("result = " + result);

        long end = System.currentTimeMillis();

        System.out.println("消耗的时间为: " + (end - start));
    }
}

class SumRecursiveTask extends RecursiveTask<Long> {
    private static final long THRESHOLD = 3000L;
    private final long start;
    private final long end;

    public SumRecursiveTask(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        long length = end - start;

        if (length <= THRESHOLD) {
            // 任务不用再拆分了.可以计算了 
            long sum = 0;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            System.out.println("计算: " + start + " -> " + end + ",结果为: " + sum);
            return sum;
        } else {
            // 数量大于预定的数量,任务还需要再拆分 
            long middle = (start + end) / 2;
            System.out.println("拆分: 左边 " + start + " -> " + middle + ", 右边 " + (middle +
                    1) + " -> " + end);

            SumRecursiveTask left = new SumRecursiveTask(start, middle);
            left.fork();
            SumRecursiveTask right = new SumRecursiveTask(middle + 1, end);
            right.fork();
            return left.join() + right.join();
        }
    }
}

7. 使用并行流时候的注意事项

  1. parallelStream是线程不安全的
  2. parallelStream适用的场景是CPU密集型的,只是做到别浪费CPU,

假如本身电脑CPU的负载很大,如果还到处用并行流,那并不能起到作用

  1. I/O密集型 磁盘I/O、网络I/O都属于I/O操作,这部分操作是较少消耗CPU资源,

一般并行流中不适用于I/O密集型的操作,就比如使用并流行进行大批量的消息推送,涉及到了大量I/O,使用并行流反而慢了很多

  1. 在使用并行流的时候是无法保证元素的顺序的,也就是即使你用了同步集合也只能保证元素都正确但无法保证其中的顺序

八、Optional类

1. 以前对null的处理方式

    @Test
    public void test01() {
        String userName = "凤姐";
        // String userName = null; 
        if (userName != null) {
            System.out.println("用户名为:" + userName);
        } else {
            System.out.println("用户名不存在");
        }
    }

2. Optional类介绍

Optional是一个没有子类的工具类,Optional是一个可以为null的容器对象。

它的作用主要就是为了解决避免Null检 查,防止NullPointerException。

3. Optional的基本使用

Optional类的创建方式

Optional.of(T t) : 创建一个 Optional 实例 
Optional.empty() : 创建一个空的 Optional 实例 
Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例 

Optional类的常用方法

isPresent() : 判断是否包含值,包含值返回true,不包含值返回false 
get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException 
orElse(T t) : 如果调用对象包含值,返回该值,否则返回参数t 
orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值 
map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty() 
    @Test
    public void test02() {
        // Optional<String> userNameO = Optional.of("凤姐"); 
        // Optional<String> userNameO = Optional.of(null); 
        // Optional<String> userNameO = Optional.ofNullable(null); 
        Optional<String> userNameO = Optional.empty();

        // isPresent() : 判断是否包含值,包含值返回true,不包含值返回false。 
        if (userNameO.isPresent()) {
            // get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException。 
            String userName = userNameO.get();
            System.out.println("用户名为:" + userName);
        } else {
            System.out.println("用户名不存在");
        }
    }

4. Optional的高级使用

    @Test
    public void test03() {
        Optional<String> userNameO = Optional.of("凤姐");
        // Optional<String> userNameO = Optional.empty(); 

        // 存在做的什么 
        // userNameO.ifPresent(s -> System.out.println("用户名为" + s)); 

        // 存在做的什么,不存在做点什么 
        userNameO.ifPresentOrElse(s -> System.out.println("用户名为" + s)
                , () -> System.out.println("用户名不存在"));
    }

    @Test
    public void test04() {
        // Optional<String> userNameO = Optional.of("凤姐"); 
        Optional<String> userNameO = Optional.empty();
        // 如果调用对象包含值,返回该值,否则返回参数t 
        System.out.println("用户名为" + userNameO.orElse("null"));
        // 如果调用对象包含值,返回该值,否则返回参数Supplier得到的值 
        String s1 = userNameO.orElseGet(() -> {return "未知用户名";});
        System.out.println("s1 = " + s1);
    }

    @Test
    public void test05() {
        // User u = new User("凤姐", 18); 
        // User u = new User(null, 18); 
        // User u = null; 
        // System.out.println(getUpperCaseUserName1(u)); 

        // 我们将可能会为null的变量构造成Optional类型 
        // User u = new User("凤姐", 18); 
        User u = new User(null, 18);
        Optional<User> uO = Optional.of(u);
        System.out.println(getUpperCaseUserName2(uO));
    }

    public String getUpperCaseUserName2(Optional<User> uO) {
        return uO.map(u -> u.getUserName())
                .map(name -> name.toUpperCase())
                .orElse("null");
    }

    /*public String getUpperCaseUserName1(User u) {
        if (u != null) {
            String userName = u.getUserName();
            if (userName != null) {
                return userName;
            } else {
                return null;
            }
        } else {
            return null;
        }
    }*/

5. 总结

Optional是一个可以为null的容器对象。orElse,ifPresent,ifPresentOrElse,map等方法避免对null的判断,写出 更加优雅的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

W哥教你学后端

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

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

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

打赏作者

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

抵扣说明:

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

余额充值