JavaSE(线程池方式、死锁、线程状态、定时器、Lambda表达式、Stream流)

线程池方式

线程池的概念

线程池的思想

使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。

线程池概念

  • **线程池:**其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

由于线程池中有很多操作都是与优化资源相关的,通过一张图来了解线程池的工作原理:
在这里插入图片描述

线程池的好处

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工厂类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行任务

  • public <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行任务

    Future接口:用来记录线程任务执行完毕后产生的结果。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

Runnable实现代码

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        //任务
        System.out.println(Thread.currentThread().getName()+":开始执行实现Runnable方式的任务....");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":执行完毕....");
    }
}

线程池测试类:

public class Test1_Runnable {
    public static void main(String[] args) {
        /*
            线程池使用一:任务是通过实现Runnable的方式创建
              1.使用Executors工厂类中的静态方法来创建线程池:
                    public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象,通过参数指定线程池中的线程数量
              2..提交并执行任务:
                    - public Future<?> submit(Runnable task):通过参数传入任务,获取线程池中的某一个线程对象,并执行任务
         */
        // 1.创建线程池,初始化线程
        ExecutorService es = Executors.newFixedThreadPool(3);// 创建一个线程池对象,该线程池中有3条线程

        // 2.创建任务
        MyRunnable mr = new MyRunnable();

        // 3.提交并执行任务
        es.submit(mr);
        es.submit(mr);
        es.submit(mr);
        es.submit(mr);

        // 4.销毁线程池(一般不操作)
        //es.shutdown();
    }
}

Callable实现代码

  • <T> Future<T> submit(Callable<T> task) : 获取线程池中的某一个线程对象,并执行.

    Future : 表示计算的结果.

  • V get() : 获取计算完成的结果。

public class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        // 线程需要执行的任务
        System.out.println(Thread.currentThread().getName()+":开始执行任务...");
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName()+":任务执行完毕...");
        return "青年节快乐";// 返回任务执行完毕后的结果
    }
}
public class Test2_Callable {
    public static void main(String[] args) throws Exception{
        /*
            线程池使用二: 任务是通过实现Callable的方式创建
                1.使用Executors工厂类中的静态方法来创建线程池:
                        public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象,通过参数指定线程池中的线程数量
                2.提交并执行任务:
                      public <T> Future<T> submit(Callable<T> task):通过参数传入任务,获取线程池中的某一个线程对象,并执行任务
                      返回值:
                        Future接口:用来记录线程任务执行完毕后产生的结果。
                             V get(): 可以获取线程执行完任务后返回的结果
         */
        // 1.创建线程池,初始化线程
        ExecutorService es = Executors.newFixedThreadPool(3);

        // 2.创建任务
        MyCallable mc = new MyCallable();

        // 3.提交并执行任务
        Future<String> future = es.submit(mc);
        es.submit(mc);
        es.submit(mc);
        es.submit(mc);
        es.submit(mc);

        System.out.println("获取任务执行完毕后的结果:"+future.get());

        // 4.销毁线程池(一般不操作)
    }
}

线程池的练习

  • 需求
    使用线程池方式执行任务,返回1-n的和
  • 分析
    因为需要返回求和结果,所以使用Callable方式的任务
  • 实现
public class Demo04 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(3);

        SumCallable sc = new SumCallable(100);
        Future<Integer> fu = pool.submit(sc);
        Integer integer = fu.get();
        System.out.println("结果: " + integer);
        
        SumCallable sc2 = new SumCallable(200);
        Future<Integer> fu2 = pool.submit(sc2);
        Integer integer2 = fu2.get();
        System.out.println("结果: " + integer2);

        pool.shutdown();
    }
}

SumCallable.java

public class SumCallable implements Callable<Integer> {
    private int n;

    public SumCallable(int n) {
        this.n = n;
    }

    @Override
    public Integer call() throws Exception {
        // 求1-n的和?
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return sum;
    }
}

死锁

什么是死锁

在多线程程序中,使用了多把锁,造成线程之间相互等待,程序不往下走了。

产生死锁的条件

  1. 有多把锁
  2. 有多个线程
  3. 有同步代码块嵌套

死锁代码

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

         new Thread(new Runnable() {
            @Override
            public void run() {
                // 任务
                synchronized ("锁1"){
                    System.out.println("小明线程: 拿到了锁1,等待获取锁2...");
                    synchronized ("锁2"){
                        System.out.println("小明线程: 拿到了锁1,锁2,进入了房间");
                    }
                }
            }
        }).start();

        // 创建并执行小红线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 任务
                synchronized ("锁2"){
                    System.out.println("小红线程:拿到了锁2,等待获取锁1...");
                    synchronized ("锁1"){
                        System.out.println("小红线程: 拿到了锁1,锁2,进入了房间");
                    }
                }
            }
        }).start();
    }
}

注意:我们应该尽量避免死锁

线程状态

线程状态

线程状态概述

线程由生到死的完整过程:

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态

这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。创建线程对象时
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪。调用start方法时
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。等待锁对象时
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。调用wait()方法时
Timed Waiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。调用sleep()方法时
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。run方法执行结束时

线程状态的切换

在这里插入图片描述
不需要去研究这几种状态的实现原理,只需知道在做线程操作中存在这样的状态。那怎么去理解这几个状态呢,新建与被终止还是很容易理解的,就研究一下线程从Runnable(可运行)状态与非运行状态之间的转换问题。

等待唤醒机制

子线程: 打印1000次i循环

主线程:打印1000次j循环

规律: 打印1次i循环,就打印1次j循环,以此类推…

假如子线程先执行,打印1次i循环,让子线程进入无限等待,执行j循环,唤醒子线程,主线程就进入无限等待

值班: 2个人值班

什么是等待唤醒机制

这是多个线程间的一种协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

就是在一个线程进行了规定操作后,就进入无限等待状态(wait()),调用notfiy()方法唤醒其他线程来执行,其他线程执行完后,进入无限等待,唤醒等待线程执行,依次类推… 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify 就是线程间的一种协作机制。

等待唤醒机制相关方法介绍

  • public void wait() : 让当前线程进入到等待状态 此方法必须锁对象调用.
  • public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.
  • 案例一:
public class Test {
    static Object obj = new Object();
    public static void main(String[] args)  {
        // 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行.
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("准备进入无限等待状态...");
                synchronized (obj){
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}
  • 案例二:
public class Test {
    static Object obj = new Object();
    public static void main(String[] args)  {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("准备进入无限等待状态...");
                synchronized (obj){
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("被唤醒了,继续执行");
            }
        }).start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj){
                    System.out.println("准备唤醒无限等待线程...");
                    obj.notify();
                }
            }
        }).start();
    }
}

等待唤醒案例

需求
  • 等待唤醒其实就是的“生产者与消费者”的问题。

  • 就拿生产包子消费包子来说等待唤醒如何有效利用资源:
    在这里插入图片描述

分析

创建一个包子类,并拥有一个状态属性,通过判断包子的状态属性,如果为true,包子铺生产包子,否则吃货吃包子。
包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。

实现

包子类:

public class BaoZi {
    public boolean flag;// 默认值为false,表示没有包子
    public String xian;// 包子馅
}

生成包子类:

public class BaoZiPu extends Thread {

    BaoZi bz;

    public BaoZiPu(BaoZi bz) {
        this.bz = bz;
    }

    @Override
    public void run() {
        // 生产包子
        // 加锁
        while (true) {
            synchronized (bz){
                // 如果有包子,就进入无限等待
                if (bz.flag == true){
                    // 进入无限等待
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 来到这里,说明一定没有包子
                System.out.println("包子铺线程:开始生产包子...");
                // 设置包子馅
                bz.xian = "牛肉";
                // 生产完包子,改变包子的状态
                bz.flag = true;
                System.out.println("包子铺线程:生产好了包子,唤醒吃货来吃...");
                // 唤醒吃货线程
                bz.notify();
            }
        }
    }
}

消费包子类:

public class ChiHuo extends Thread {

    BaoZi bz;

    public ChiHuo(BaoZi bz) {
        this.bz = bz;
    }

    @Override
    public void run() {
        // 吃包子
        // 加锁
        while (true) {
            synchronized (bz){
                // 如果没有包子,进入无限等待
                if (bz.flag == false){
                    try {
                        bz.wait();// 无限等待,醒了
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 来到这里,说明一定有包子
                System.out.println("吃货线程:吃包子,包子馅是:"+bz.xian);
                // 吃完包子,改变包子的状态
                bz.flag = false;
                System.out.println("吃货线程:吃完包子了=====================");
                // 唤醒包子铺线程
                bz.notify();
            }
        }
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        /*
            实现:
                包子铺线程生产包子,生产完后就进入无限等待,唤醒吃货线程
                吃货线程吃包子,吃完了包子就进入无限等待,唤醒包子铺线程
         */
        // 创建包子对象
        BaoZi bz = new BaoZi();

        // 创建包子铺线程对象
        BaoZiPu baoZiPu = new BaoZiPu(bz);

        // 创建吃货线程对象
        ChiHuo ch = new ChiHuo(bz);

        // 启动包子铺线程
        baoZiPu.start();

        // 启动吃货线程
        ch.start();
    }
}

定时器

定时器功能介绍

定时器,可以设置线程在某个时间执行某件事情,或者某个时间开始,每间隔指定的时间反复的做某件事情

定时器方法介绍

  1. 构造方法
public Timer():构造一个定时器
  1. 成员方法
void  schedule(TimerTask task, long delay)
                              在指定的延迟时间之后执行指定的任务。
void schedule(TimerTask task, long delay,  long period)
                              在指定的延迟时间之后开始执行任务,每隔指定毫秒重新执行任务。
void schedule(TimerTask task, Date time)
                              在指定日期时间执行指定任务。
void  schedule(TimerTask task, Date firstTime,  long period)
                              在指定日期时间执行指定任务,每隔指定毫秒重新执行任务。

示例代码

public class Test {
    public static void main(String[] args) throws ParseException {
        
        // 1.创建定时器
        Timer timer = new Timer();

        // 2.设置定时器
        //  void  schedule(TimerTask task, long delay)
        // 设置3秒后,定时器开始执行指定任务,只执行一次
       /* timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("起床啦...");
                // 执行完任务后,取消定时器
                timer.cancel();
            }
        },3000);*/

        // void schedule(TimerTask task, long delay,  long period)
        // 设置3秒后,定时器开始执行指定任务,每隔1秒重复执行任务
        /*timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("起床啦...");
            }
        },3000,1000);*/

        // void schedule(TimerTask task, Date time)
       /* // 设置2020-05-04 14:19:00执行指定的任务
        String str = "2020-05-04 14:19:00";
        // 把String对象转换为Date对象
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = sdf.parse(str);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("起床啦...");
                timer.cancel();
            }
        },date);*/

       // void schedule(TimerTask task, Date firstTime,  long period)
        // 设置2020-05-04 14:22:00执行指定的任务,每隔1秒重复执行
        String str = "2020-05-04 14:22:00";
        // 把String对象转换为Date对象
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = sdf.parse(str);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("起床啦...");
            }
        },date,1000);
    }
}

Lambda表达式

函数式编程思想概述

面向对象编程思想

面向对象强调的是对象 , “必须通过对象的形式来做事情”,相对来讲比较复杂,有时候只是为了做某件事情而不得不创建一个对象 , 例如线程执行任务,不得不创建一个实现Runnable接口对象,但真正希望的是将run方法中的代码传递给线程对象执行

函数编程思想

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。例如线程执行任务 , 使用函数式思想 , 就可以通过传递一段代码给线程对象执行,而不需要创建任务对象

Lambda表达式的体验

实现Runnable接口的方式创建线程执行任务

 实现类:
1.创建一个实现类,实现Runnable接口
2.在实现类中,重写run()方法,把任务放入run()方法中
3.创建实现类对象
4.创建Thread线程对象,传入实现类对象
5.使用线程对象调用start()方法,启动并执行线程
 总共需要5个步骤,一步都不能少,为什么要创建实现类,为了得到线程的任务
 
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("实现的方法创建线程的任务执行了...");
    }
}
public class Demo {
    public static void main(String[] args) {
          // 实现类的方式:
            MyRunnable mr = new MyRunnable();
            Thread t = new Thread(mr);
            t.start();
    }
}

匿名内部类方式创建线程执行任务

匿名内部类:
 1.创建Thread线程对象,传入Runnable接口的匿名内部类
 2.在匿名内部类中重写run()方法,把任务放入run()方法中
 3.使用线程对象调用start()方法,启动并执行线程
总共需要3个步骤,一步都不能少,为什么要创建Runnable的匿名内部类类,为了得到线程的任务
public class Demo {
    public static void main(String[] args) {
          // 匿名内部类的方式:
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类的方式创建线程的任务执行了");
            }
        });
        t2.start();
    }
}

Lambda方式创建线程执行任务

  • Lambda表达式的概述:
    它是一个JDK8开始一个新语法。它是一种“代替语法”——可以代替上面编写的“面向某种接口”编程的情况
public class Demo {
    public static void main(String[] args) {
  		// 体验Lambda表达式的方式:
        Thread t3 = new Thread(()->{System.out.println("Lambda表达式的方式");});
        t3.start();
    }
}

Lambda表达式的格式

标准格式

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

  • 一些参数
  • 一个箭头
  • 一段代码

Lambda表达式的标准格式为:

(参数类型 参数名称) -> { 代码语句 }

格式说明

  • 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
  • ->是新引入的语法格式,代表指向动作。
  • 大括号内的语法与传统方法体要求基本一致。

示例代码

  • 线程案例演示

    public class Test {
        public static void main(String[] args) {
            /*
                Lambda表达式的标准格式:
                    - 标准格式:  (参数列表)->{ 代码 }
                    - 格式说明:
                        - 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
                        - ->是新引入的语法格式,代表指向动作。
                        - 大括号内的语法与传统方法体要求基本一致。
    
                    - 案例演示:
                        线程案例
                        比较器案例
    
                  格式解释:
                    1.小括号中书写的内容和接口中的抽象方法的参数列表一致
                    2.大括号中书写的内容和实现接口中的抽象方法的方法体一致
                    3.箭头就是固定的
             */
            //  线程案例
            // 面向对象编程思想:
            // 匿名内部类方式创建线程执行任务
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程需要执行的任务代码1...");
                }
            });
            t1.start();
    
            // 函数式编程思想: Lambda表达式
            Thread t2 = new Thread(()->{ System.out.println("线程需要执行的任务代码2...");});
            t2.start();  
        } 
    }  
    
  • 比较器案例演示

    public class Test {
        public static void main(String[] args) {
            /*
                Lambda表达式的标准格式:
                    - 标准格式:  (参数列表)->{ 代码 }
                    - 格式说明:
                        - 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
                        - ->是新引入的语法格式,代表指向动作。
                        - 大括号内的语法与传统方法体要求基本一致。
    
                    - 案例演示:
                        线程案例
                        比较器案例
    
                  格式解释:
                    1.小括号中书写的内容和接口中的抽象方法的参数列表一致
                    2.大括号中书写的内容和实现接口中的抽象方法的方法体一致
                    3.箭头就是固定的
             */
            //  比较器案例
            // Collections.sort(List<?> list,Comparator<?> comparator);
            List<Integer> list = new ArrayList<>();
            Collections.addAll(list,100,200,500,300,400);
            System.out.println("排序之前的集合:"+list);// [100, 200, 500, 300, 400]
    
            // 面向对象编程思想:
            /*Collections.sort(list, new Comparator<Integer>() {
                @Override
                public int compare(Integer o1, Integer o2) {
                    // 降序: 后减前
                    return o2 - o1;
                }
            });
            System.out.println("排序之后的集合:"+list);// [500, 400, 300, 200, 100]*/
    
            // 函数式编程思想:Lambda表达式
            Collections.sort(list,(Integer o1, Integer o2)->{return o2 - o1;});
            System.out.println("排序之后的集合:"+list);// [500, 400, 300, 200, 100]  
        }  
    }
    

Lambda表达式省略格式

省略规则

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

  1. 小括号内参数的类型可以省略;
  2. 如果小括号内有且仅有一个参数,则小括号可以省略;
  3. 如果大括号内有且仅有一条语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。

示例代码

  • 线程案例演示

    public class Demo_线程演示 {
        public static void main(String[] args) {
           
            //Lambda表达式省略规则
            Thread t2 = new Thread(()-> System.out.println("执行了"));
            t2.start();
        }
    }
    
  • 比较器案例演示

    public class Demo_比较器演示 {
        public static void main(String[] args) {
            //比较器
            ArrayList<Integer> list = new ArrayList<>();
            //添加元素
            list.add(324);
            list.add(123);
            list.add(67);
            list.add(987);
            list.add(5);
            System.out.println(list);
    
            //Lambda表达式
            Collections.sort(list, ( o1,  o2 )-> o2 - o1);
    
            //打印集合
            System.out.println(list);
        }
    }
    

Lambda的前提条件和表现形式

Lambda的前提条件

  • 使用Lambda必须具有接口,且要求接口中的抽象方法有且仅有一个。(别的方法没有影响)

  • 使用Lambda必须具有上下文推断。

    • 如果一个接口中只有一个抽象方法,那么这个接口叫做是函数式接口。

      @FunctionalInterface这个注解 就表示这个接口是一个函数式接口
      

Lambda的表现形式

  • 变量形式
  • 参数形式
  • 返回值形式
public class Demo {
    public static void main(String[] args) {
        /*
            Lambda表达式其实就是用来替换函数式接口的对象
            Lambda表达式的标准格式
            Lambda表达式的省略格式
            Lambda的几种使用形式: 使用场景
                1.变量的形式:变量的类型为函数式接口类型,那么可以赋值一个Lambda表达式
                2.参数的形式:方法的形参类型为函数式接口类型,那么就可以传入一个Lambda表达式
                3.返回值的形式:方法的返回值类型为函数式接口类型,那么就可以返回一个Lambda表达式

         */
        // 变量的形式:
        Runnable r = ()->{System.out.println("变量的形式");};// 需要Runnable函数式接口的对象,所以可以使用Lambda表达式替换
        r.run();

        // 参数的形式:
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list,"小明","小红","小白","小黑");
        System.out.println("排序之前:"+list);// 排序之前:[小明, 小红, 小白, 小黑]
        Collections.sort(list,(String o1,String o2)->{return o2.length() - o1.length();});
        System.out.println("排序之后:"+list);// 排序之后:[小黑, 小红, 小明, 小白]
    }

    // 返回值的形式: 方法的返回值类型为函数式接口类型,那么就可以返回一个Lamdba表达式
    public static Comparator<String> getComparator(){
        return (String o1,String o2)->{return o2.length() - o1.length();};
    }

    public static Runnable getRunnable(){
        return ()->{System.out.println("====");};
    }
}

Stream

Stream流的引入

例如: 有一个List集合,要求:

  1. 将List集合中姓张的的元素过滤到一个新的集合中
  2. 然后将过滤出来的姓张的元素,再过滤出长度为3的元素,存储到一个新的集合中
  • 传统方式操作集合
public class Demo {
    public static void main(String[] args) {
        // 传统方式操作集合:
        List<String> list = new ArrayList<>();
        list.add("张张");
        list.add("小明");
        list.add("小红");
        list.add("小白");
        list.add("小黑");

        // 1.将List集合中姓张的的元素过滤到一个新的集合中
        // 1.1 创建一个新的集合,用来存储所有姓张的元素
        List<String> listB = new ArrayList<>();

        // 1.2 循环遍历list集合,在循环中判断元素是否姓张
        for (String e : list) {
            // 1.3 如果姓张,就添加到新的集合中
            if (e.startsWith("张")) {
                listB.add(e);
            }
        }

        // 2.然后将过滤出来的姓张的元素,再过滤出长度为3的元素,存储到一个新的集合中
        // 2.1 创建一个新的集合,用来存储所有姓张的元素并且长度为3
        List<String> listC = new ArrayList<>();

        // 2.2 循环遍历listB集合,在循环中判断元素长度是否为3
        for (String e : listB) {
            // 2.3 如果长度为3,就添加到新的集合中
            if(e.length() == 3){
                listC.add(e);
            }
        }

        // 3.打印所有元素---循环遍历
        for (String e : listC) {
            System.out.println(e);
        }
    }
}
  • Stream流操作集合
public class Demo {
    public static void main(String[] args) {
        // 体验Stream流:
        list.stream().filter(e->e.startsWith("张")).filter(e->e.length()==3).forEach(e-> System.out.println(e));
        System.out.println(list);
    }
}

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

流式思想概述

整体来看,流式思想类似于工厂车间的“生产流水线”。
在这里插入图片描述

获取流方式

根据Collection获取流

Collection接口中有一个stream()方法,可以获取流 , default Stream stream():获取一个Stream流

  1. 通过List集合获取:
  2. 通过Set集合获取

根据Map获取流

  • 使用所有键的集合来获取流
  • 使用所有值的集合来获取流
  • 使用所有键值对的集合来获取流

根据数组获取流

Stream流中有一个static Stream of(T… values)

  • 通过数组获取:
  • 通过直接给多个数据的方式

示例代码

public class Test {
    public static void main(String[] args) {
     
        // 通过List集合获取流
        List<String> list = new ArrayList<>();
        list.add("小明");
        list.add("小红");
        list.add("小白");
        list.add("小黑");
        list.add("小刚");

        Stream<String> stream1 = list.stream();

        // 通过Set集合获取流
        Set<String> set = new HashSet<>();
        set.add("小明");
        set.add("小红");
        set.add("小白");
        set.add("小黑");
        set.add("小刚");

        Stream<String> stream2 = set.stream();

        System.out.println("=====================");
        Map<String,String> map = new HashMap<>();
        map.put("小明","小红");
        map.put("小白","小黑");
        map.put("小刚","小智");
        map.put("小张","小黄");

        // 通过获取Map集合的键来获取流
        Set<String> keys = map.keySet();
        Stream<String> stream3 = keys.stream();

        // 通过获取Map集合的值来获取流
        Collection<String> values = map.values();
        Stream<String> stream4 = values.stream();

        // 通过获取Map集合的键值对对象来获取流
        Set<Map.Entry<String, String>> entrySet = map.entrySet();
        Stream<Map.Entry<String, String>> stream5 = entrySet.stream();

        System.out.println("==========================");
        // 通过数组获取流
        String[] arr = {"小明","小红","小白","小黑"};
        Stream<String> stream6 = Stream.of(arr);

        // 通过直接传入多个元素获取流
        Stream<String> stream7 = Stream.of("小明","小红","小白","小黑");
    }
}

Stream流常用方法

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

  • 终结方法:返回值类型不再是Stream接口自身类型的方法,因此不再支持类似StringBuilder那样的链式调用。这里终结方法包括countforEach方法。
  • 非终结方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)

函数拼接与终结方法

在上述介绍的各种方法中,凡是返回值仍然为Stream接口的为函数拼接方法,它们支持链式调用;而返回值不再为Stream接口的为终结方法,不再支持链式调用。如下表所示:

方法名方法作用方法种类是否支持链式调用
count统计个数终结
forEach逐一处理终结
filter过滤函数拼接
limit取用前几个函数拼接
skip跳过前几个函数拼接
map映射函数拼接
concat组合函数拼接

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

forEach : 逐一处理

虽然方法名字叫forEach,但是与for循环中的“for-each”昵称不同,该方法并不保证元素的逐一消费动作在流中是被有序执行的

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

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

import java.util.stream.Stream;

public class Demo12StreamForEach {
    public static void main(String[] args) {
         Stream<String> stream1 = Stream.of("小明", "小白", "小红", "小黑");
        // 对stream1这个流中的每个元素进行打印输出
        // stream1.forEach((String e)->{System.out.println(e);});
        stream1.forEach( e->System.out.println(e));
    }
}

count:统计个数

正如旧集合Collection当中的size方法一样,流提供count方法来数一数其中的元素个数:

long count();

该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。基本使用:

public class Demo09StreamCount {
    public static void main(String[] args) {
        // Stream<String> stream1 = Stream.of("小明", "小白", "小红", "小黑");
        // long count = stream1.count();
        
        // 链式编程
        long count = Stream.of("小明", "小白", "小红", "小黑").count();
        System.out.println("流中元素的个数: "+count);
    }
}

filter:过滤

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

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

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

基本使用

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

public class Demo07StreamFilter {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张张", "张天", "小明");
        Stream<String> result = original.filter(s -> s.startsWith("张"));
    }
}

在这里通过Lambda表达式来指定了筛选的条件:必须姓张。

limit:取用前几个

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

Stream<T> limit(long maxSize);

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

import java.util.stream.Stream;

public class Demo10StreamLimit {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张张", "张天", "小明");
        Stream<String> result = original.limit(2);
        System.out.println(result.count()); // 2
    }
}

skip:跳过前几个

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

Stream<T> skip(long n);

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

import java.util.stream.Stream;

public class Demo11StreamSkip {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张张", "张天", "小明");
        Stream<String> result = original.skip(2);
        System.out.println(result.count()); // 1
    }
}

map:映射

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

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

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

基本使用

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

import java.util.stream.Stream;

public class Demo08StreamMap {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("10", "12", "18");
        Stream<Integer> result = original.map(s->Integer.parseInt(s));
    }
}

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

concat:组合

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

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

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

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

import java.util.stream.Stream;

public class Demo12StreamConcat {
    public static void main(String[] args) {
        Stream<String> streamA = Stream.of("小明");
        Stream<String> streamB = Stream.of("小红");
        Stream<String> result = Stream.concat(streamA, streamB);
    }
}

收集Stream结果

收集到集合中

  • Stream流中提供了一个方法,可以把流中的数据收集到单列集合中
    • <R,A> R collect(Collector<? super T,A,R> collector): 把流中的数据收集到单列集合中
      • 参数Collector<? super T,A,R>: 决定把流中的元素收集到哪个集合中
      • 返回值类型是R,也就是说R指定为什么类型,就是收集到什么类型的集合
      • 参数Collector如何得到? 使用java.util.stream.Collectors工具类中的静态方法:
        • public static Collector<T, ?, List> toList():转换为List集合。
        • public static Collector<T, ?, Set> toSet():转换为Set集合。

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

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Demo15StreamCollect {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("10", "20", "30", "40", "50");
        List<String> list = stream.collect(Collectors.toList());
        Set<String> set = stream.collect(Collectors.toSet());
    }
}

收集到数组中

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

Object[] toArray();

其使用场景如:

import java.util.stream.Stream;

public class Demo16StreamArray {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("10", "20", "30", "40", "50");
        Object[] objArray = stream.toArray();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值