Java基础巩固(二)异常,多线程,线程池,IO流,Properties集合,IO工具类,字符流,对象流,Stream,Lambda表达式

一、异常,多线程

学习目标 :

  • 异常的概述
  • 异常的分类
  • 异常的处理方式
  • 自定义异常
  • 多线程入门

1 异常的概述

1.1 什么是异常?

  • 异常就是程序出现了不正常情况 , 程序在执行过程中 , 数据导致程序不正常 , 最终导致了JVM的非正常停止
  • 注意 : 语句错误不算在异常体系中

1.2 异常的存在的形式

  • 异常有类型之分,比如我们之前有接触过的比较熟悉的数组越界异常(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException),类型转换异常(ClassCastException)。当程序中产生异常时,其实就是在异常的位置创建了一个该异常的对象,该对象携带了相关的异常信息。
    • 简单来说 : 异常就是Java中提供的类的对象

1.3 程序中异常产生后,是如何处理的

  • 程序中一旦产生异常,首先会中断向下执行。异常的传递要根据处理方式(后面章节会讲到)而定,如果没有处理,默认是将异常传递给本方法的调用者。不断往回传递,直到JVM收到该异常,此时程序终止执行

2 异常的分类

  • 编译时期异常
    • 非RuntimeException及其子类 : 编译时异常就是在编译的时候可能出现的异常, 编译时期必须处理,否则程序无法执行
  • 运行时期异常
    • RuntimeException及其子类 : 运行时异常就是在运行时可能出现的异常, 在编译时期不需要处理

3 异常的处理方式

3.1 JVM处理异常的方式

  • 如果程序出现了问题,我们没有做任何处理,最终JVM会做默认的处理 , 那么JVM是如何处理的呢 ?

    • 把异常的类型 , 原因 , 位置打印在控制台
    • 程序停止执行
  • 注意 : 程序中出现了异常 , 会在当前位置创建此异常的对象 , 对象中包含了异常的信息 , 并把此异常交给本方法的调用者处理

  • 缺点 : 用户体验不好

3.2 手动处理异常方式

3.2.1 声明异常
  • 声明异常—— throws

    • 修饰符 返回值类型 方法名(参数列表) throws 异常类型1 , 异常的类型2… { … }
    • 举例 : public void show() throws NullPointerException , ArrayIndexOutOfBoundsException { … }
  • 作用 :

    • 表示调用当前的方法可能会出现某些异常,使用时需要注意哦!
    • 如果当前方法没有出现任何异常, 那么代码会正常执行
    • 如果当前方法中出现了异常 , 会把异常交给本方法调用者处理(甩锅)
  • package com.itheima.exception_demo;
    
    import sun.java2d.pipe.SpanShapeRenderer;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /*
        声明异常—— throws
            格式 : 修饰符   返回值类型  方法名(参数列表) throws 异常类型1 , 异常的类型2...   {  ...  }
            举例 : public void show() throws NullPointerException , ArrayIndexOutOfBoundsException  { .... }
            作用 :
                1 表示告知调用者当前的方法可能会出现某些异常,使用时需要注意哦!
                2 如果当前方法没有出现任何异常, 那么代码会正常执行
                3 如果当前方法中出现了异常 , 会把异常交给本方法调用者处理(甩锅)
    
        需求 :
            练习 : 定义两个方法一个运行时期异常 , 一个声明编译时期异常 !
    
        注意 :
            1 编译时异常因为在编译时就会检查,所以必须要写在方法后面进行显示声明
            2 运行时异常因为在运行时才会发生,所以在方法后面可以不写
            3 如果声明多个异常有子父类关系 , 那么只要声明一个父类即可(多态)
     */
    public class Exception_Throws {
        public static void main(String[] args) throws ParseException{
            printArr();// 如果此方法出现了异常 , 会交给jvm进行处理
            StringToDate();// 如果此方法出现了异常 , 会交给jvm进行处理
        }
    
        // 1 告诉调用者 , 此方法可能会出现异常哦
        // 2 如果此方法没有出现异常 , 那么会正常执行
        // 3 如果此方法中出现了异常 , 会把此异常交给调用者处理
        // 注意 : 如果声明的异常是一个运行时期异常 , 那么此声明可以省略
        public static void printArr() /*throws NullPointerException*/ {
            int[] arr = null;
            for (int i = 0; i < arr.length; i++) {
                System.out.println(arr[i]);
            }
        }
    
        // 1 告诉调用者 , 此方法可能会出现异常哦
        // 2 如果此方法没有出现异常 , 那么会正常执行
        // 3 如果此方法中出现了异常 , 会把此异常交给调用者处理
        // 注意 : 如果声明的异常 是一个编译时期异常 , 那么在编译时期必须处理 , 要么程序无法执行
        public static void StringToDate() throws ParseException {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date date = sdf.parse("2000-03-11 12:12:12");
        }
    }
    
    
3.2.2 抛出异常
  • 思考:
    • 以前出现了异常,虚拟机帮我们创建一个异常对象,抛给调用者。但是如果我们需要自己手动创建一个异常对象该如何写?
  • 格式 :
    • 修饰符    返回值类型    方法名(参数列表)  {
          throw new 异常对象();
      }
      
  • 注意 :
    • 抛出异常的格式必须在方法的内部完成
    • 如果手动抛出一个异常,下面的代码无法执行
  • package com.itheima.exception_demo;
    
    /*
        抛出异常演示 :
            格式 :
                修饰符  返回值类型  方法名(参数列表)  {
                    throw new 异常对象();
                }
    
            注意 :
                1 抛出异常的格式必须在方法的内部完成
                2 如果手动抛出一个异常,下面的代码无法执行
     */
    public class Exception_Throw {
        public static void main(String[] args) {
            System.out.println("家里有一个貌美如花的老婆");
            System.out.println("还有一个当官的兄弟");
            System.out.println("自己还有一个买卖");
            System.out.println("这样的生活你要不要?");
    
            // 程序不想往下执行了 ,怎么做 ???
            // 1 自己手动制造出一个异常
            // 2 当前异常也是交给了方法的调用者处理 , 也就是jvm处理
            // 3 下面代码无法执行
            throw new RuntimeException();
    
            // System.out.println("武大郎的标准生活!");
        }
    }
    
    
  • 作用 :
    • 在方法中,当传递的参数有误,没有继续运行下去的意义了,则采取抛出处理,表示让该方法结束运行。
    • 告诉调用者方法中出现的问题原因
    package com.itheima.exception_demo;
    
    /*
        抛出异常存在的意义所在 :
            1 在方法中,当传递的参数有误,没有继续运行下去的意义了,则采取抛出处理,表示让该方法结束运行。
            2 告诉调用者方法中出现了问题
    
        练习   : 定义一个方法 , 方法的参数接收一个数组 , 在方法中遍历数组 .
        需求1  : 如果方法接收的数组为null  , 使用输出语句提示
        需求2  : 如果方法接收的数组为null  , 使用抛出异常解决
        思考   : 两种方式的区别在哪里 ?
     */
    public class Exception_Throw2 {
        public static void main(String[] args) {
            int[] arr = {1, 2, 3, 4, 5};
            arr = null;
            // printArr1(arr);
            printArr2(arr);// 接收方法返回的异常 , 但是此异常有jvm进行处理
        }
    
        // 需求1  : 如果方法接收的数组为null  , 使用输出语句提示
        public static void printArr1(int[] arr) {
            if (arr == null) {
                System.out.println("数组为null");
            } else {
                for (int i = 0; i < arr.length; i++) {
                    System.out.println(arr[i]);
                }
            }
        }
    
        // 需求2  : 如果方法接收的数组为null  , 使用抛出异常解决
        public static void printArr2(int[] arr) {
            if (arr == null) {
                throw new RuntimeException();
            } else {
                for (int i = 0; i < arr.length; i++) {
                    System.out.println(arr[i]);
                }
            }
        }
    }
    
3.2.3 throws和throw的区别
  • throws :
    • 用在方法声明后面,跟的是异常类名
    • 表示声明异常,告知调用者调用该方法有可能会出现这样的异常
  • throw :
    • 用在方法体内,跟的是异常对象名
    • 表示手动抛出异常对象,告知调用者数据传入有误
3.2.4 捕获异常
  • 捕获处理异常介绍 : try, catch

    • 之前的声明或者抛出都是将异常传递出去,让调用者知道异常信息。
      而捕获处理是本方法内部进行处理 , 能够阻止异常的传递,从而保证程序能够继续往下执行。
  • 捕获异常的格式

    • try {
          try中存放可能会出现问题的代码
              1.代码...
              2.代码...
              3.代码...
      } catch (异常类型 变量名) {
          	4.处理异常方案
             打印异常,获取异常原因记录日志......}
      5.其他代码...
      
  • 执行方式

    • 如果 try 中没有遇到问题,怎么执行?
      • 从上往下依次执行 , catch中不执行
    • 如果 try 中代码2遇到了问题,问题下面的代码还会执行吗?
      • 不会执行,会拿当前异常对象和异常类型匹配,匹配成功执行处理异常代码
    • 如果出现的问题没有被捕获,那么程序如何运行?
      • 如果异常没有捕获到 , 虚拟机会帮助我们处理
  • 多异常捕获处理方案

    • 多个异常,每个异常单独处理

      • try{
            异常1
        }catch(异常1){
        
        } 
        try{
            异常2
        }catch(异常2){
        
        }
        
    • 多个异常,一次捕获,多次处理

      • try{
            异常1
            异常2
        }catch(异常1){
        }catch(异常2){
        }
        
    • 多个异常,异常一次捕获,一次处理

      • try{
            异常1
            异常2
        }catch(Exception e){
        }
        

3.3 Throwable 的成员方法

方法名说明
public String getMessage()返回此 throwable 的详细消息字符串
public String toString()返回此可抛出的简短描述
public void printStackTrace()把异常的错误信息输出在控制台

3.4 异常练习

package com.itheima.exception_demo;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;

/*
    定义一个方法接收一个生日日期字符串(xxxx年xx月xx)
    main方法中让用户输入一个生日日期字符串,调用设计好的方法计算在地球上活了多少天。

    要求:如果解析发生异常,捕获异常,提示用户要重新输入生日日期字符串,直到输入正确的日期为止。
    思考:设计代此码的过程中想想什么时候捕获异常,什么时候声明异常?

 */
public class ExceptionTest {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入生日(xxxx-xx-xx):");
        
        while (true) {
            String birthday = sc.nextLine();
            try {
                method(birthday);
                break;// 如果生日没有问题结束死循环
            } catch (ParseException e) {
                System.out.println("录入生日格式有误!");
            }
        }
    }

    public static void method(String strDate) throws ParseException {
        // 创建日期模板对象
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

        // 解析字符串
        Date date = sdf.parse(strDate);

        // 获取生日到1970/1/1 经历的毫秒值
        long time1 = date.getTime();

        // 当前系统时间到1970/1/1 经历的毫秒值
        Date d2 = new Date();
        long time2 = d2.getTime();


        System.out.println("活了" + (time2 - time1) / (1000L * 60 * 60 * 24) + "天");
    }
}

4 自定义异常

4.1 概述

  • 当JDK中的异常类型,不满足实际的业务需要时。就可以自己定义异常。例如,学生的年龄数据,如果是负数或者数据 超过了150认为是不合法的,就需要抛出异常。JDK中就没有表示年龄的异常,就需要自己定义异常了

4.2 实现步骤

  • 定义异常类
  • 写继承关系
  • 空参构造
  • 带参构造

4.3 自定义异常注意

  • 如果要自定义编译时异常,就继承Exception
  • 如果要自定义运行时异常,就继承RuntimeException

5 多线程入门

5.1 多线程相关的概念

  • 并发与并行
    • 并行:在同一时刻,有多个任务在多个CPU上同时执行。
    • 并发:在同一时刻,有多个任务在单个CPU上交替执行。
  • 进程与线程
    • 进程:就是操作系统中正在运行的一个应用程序。
    • 线程:就是应用程序中做的事情。比如:360软件中的杀毒,扫描木马,清理垃圾。

5.2 什么是多线程

  • 是指从软件或者硬件上实现多个线程并发执行的技术。
    具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
  • 好处 : 提高任务的执行性能

5.3 多线程的创建方式

5.3.1 继承Thread方式
  • 基本步骤:

    • 创建一个类继承Thread类。
    • 在类中重写run方法(线程执行的任务放在这里)
    • 创建线程对象,调用线程的start方法开启线程。
    • 执行程序,观察控制台的打印数据的现象
    package com.itheima.thread_demo;
    
    /*
        线程的创建方式1:继承Thread方式
    
        基本步骤 :
            1 创建一个类继承Thread类。
            2 在类中重写run方法(线程执行的任务放在这里)
            3 创建线程对象,调用线程的start方法开启线程。
    
        需求 :
            我们启动一个Java程序,其实默认就存在一个主线程(main方法所在线程)
            接下来,我们在主线程启动一个线程,打印1到100的数字,主线程启动完线程后又打印1到100的数字。
            此时主线程和启动的线程在并发执行,观察控制台打印的结果。
     */
    public class MyThread01 {
        public static void main(String[] args) {
            // 创建线程对象,调用线程的start方法开启线程。
            MyThread mt = new MyThread();
            mt.start();
    
            // main方法中的任务
            for (int i = 1; i <= 100; i++) {
                System.out.println("i:" + i);
            }
        }
    }
    // 创建一个类继承Thread类。
    class MyThread extends Thread {
        // 在类中重写run方法(线程执行的任务放在这里)
        @Override
        public void run() {
            for (int i = 1; i <= 100; i++) {
                System.out.println("i:" + i);
            }
        }
    }
    
5.3.2 实现Runable方式
  • 构造方法

    • public Thread(Runnable target)
    • public Thread(Runnalbe target , String name)
  • 实现步骤

    • 定义任务类实现Runnable,并重写run方法
    • 创建任务对象
    • 使用含有Runnable参数的构造方法,创建线程对象并指定任务。
    • 调用线程的start方法,开启线程
    package com.itheima.thread_demo;
    
    /*
         线程的创建方式2:实现Runnable方式
    
        基本步骤 :
            1 定义任务类实现Runnable,并重写run方法
            2 创建任务对象
            3 使用含有Runnable参数的构造方法,创建线程对象并指定任务。
            4 调用线程的start方法,开启线程
    
        需求 :
            我们启动一个Java程序,其实默认就存在一个主线程(main方法所在线程)
            接下来,我们在主线程启动一个线程,打印1到100的数字,主线程启动完线程后又打印1到100的数字。
            此时主线程和启动的线程在并发执行,观察控制台打印的结果。
     */
    public class MyThread02 {
        public static void main(String[] args) {
            // 创建线程对象,调用线程的start方法开启线程。
            MyRunnable mr = new MyRunnable();
            Thread thread= new Thread(mr);
            thread.start();
    
            // main方法中的任务
            for (int i = 1; i <= 100; i++) {
                System.out.println("i:" + i);
            }
        }
    
    }
    
    // 1 定义任务类实现Runnable,并重写run方法
    class MyRunnable implements Runnable {
        // 在类中重写run方法(线程执行的任务放在这里)
        @Override
        public void run() {
            for (int i = 1; i <= 100; i++) {
                System.out.println("i:" + i);
            }
        }
    }
    
    

5.3.3 Thread类中常用方法

  • String getName():返回此线程的名称

  • Thread类中设置线程的名字

    • void setName(String name):将此线程的名称更改为等于参数 name
    • 通过构造方法也可以设置线程名称
  • public static Thread currentThread():返回对当前正在执行的线程对象的引用

  • public static void sleep(long time):让线程休眠指定的时间,单位为毫秒

  • 线程有两种调度模型

    • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
      抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程
    • 获取的 CPU 时间片相对多一些
    package com.itheima.thread_demo.thread_method;
    
    /*
        线程有两种调度模型
            1 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
            2 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程
               获取的 CPU 时间片相对多一些
    
            注意 : Java使用的是抢占式调度模型
    
    
            优先级高 , 只是抢夺到cpu执行的概率高而已 , 只是一种概率问题
     */
    public class PriorityDemo {
        public static void main(String[] args) {
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            });
            // 优先级最低
            thread1.setPriority(1);
            thread1.start();
    
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            });
            // 优先级最高
            thread2.setPriority(10);
            thread2.start();
        }
    }
    

二、线程安全,死锁,状态,通讯,线程池

学习目标

  • 线程安全
  • 线程死锁
  • 线程的状态
  • 线程间通讯
  • 线程池

1 线程安全

1.1 线程安全产生的原因

  • 多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了
package com.itheima.ticket_demo;

/*
    电影院
 */
public class Ticket implements Runnable {
    private int ticketCount = 100; // 一共有一百张票

    @Override
    public void run() {
        while (true) {
            // 如果票的数量为0 , 那么停止买票
            if (ticketCount == 0) {
                break;
            } else {
                // 有剩余的票 , 开始卖票
                ticketCount--;
                System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张");
            }
        }
    }
}

package com.itheima.ticket_demo;

/*
    1 定义一个类Ticket实现Runnable接口,里面定义一个成员变量:private int ticketCount = 100;
    2 在Ticket类中重写run()方法实现卖票,代码步骤如下
        A:判断票数大于0,就卖票,并告知是哪个窗口卖的
        B:票数要减1
        C:卖光之后,线程停止
    3 定义一个测试类TicketDemo,里面有main方法,代码步骤如下
        A:创建Ticket类的对象
        B:创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
        C:启动线程

 */
public class TicketDemo {
    public static void main(String[] args) {
        // 创建任务类对象
        Ticket ticket = new Ticket();

        // 创建三个线程类对象
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        // 给三个线程命名
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        // 开启三个线程
        t1.start();
        t2.start();
        t3.start();
    }
}

注意 : 以上代码是有问题 , 接下来继续改进
  • 因为出票是有时间的 , 所有现在在每次买票之前, 休眠100毫秒 , 尝试执行代码
package com.itheima.ticket_demo;

/*
    电影院
 */
public class Ticket implements Runnable {
    private int ticketCount = 100; // 一共有一百张票

    @Override
    public void run() {
        while (true) {
            // 如果票的数量为0 , 那么停止买票
            if (ticketCount <= 0) {
                break;
            } else {

                // 模拟出票的时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 有剩余的票 , 开始卖票
                ticketCount--;
                System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张");
            }
        }
    }
}

package com.itheima.ticket_demo;

/*
    1 定义一个类Ticket实现Runnable接口,里面定义一个成员变量:private int ticketCount = 100;
    2 在Ticket类中重写run()方法实现卖票,代码步骤如下
        A:判断票数大于0,就卖票,并告知是哪个窗口卖的
        B:票数要减1
        C:卖光之后,线程停止
    3 定义一个测试类TicketDemo,里面有main方法,代码步骤如下
        A:创建Ticket类的对象
        B:创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
        C:启动线程

 */
public class TicketDemo {
    public static void main(String[] args) {
        // 创建任务类对象
        Ticket ticket = new Ticket();

        // 创建三个线程类对象
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        // 给三个线程命名
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        // 开启三个线程
        t1.start();
        t2.start();
        t3.start();
    }
}

  • 通过上述代码的执行结果 , 发现了出现了负号票 , 和相同的票 , 数据有问题
    • 问题出现的原因 : 多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了

1.2 线程的同步

  • 概述 : java允许多线程并发执行,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性

  • 分类

    • 同步代码块
    • 同步方法
    • 锁机制。Lock

1.3 同步代码块

同步代码块 : 锁住多条语句操作共享数据,可以使用同步代码块实现

第一部分 : 格式
           synchronized(任意对象) {
                   多条语句操作共享数据的代码
           }

第二部分 : 注意
           1 默认情况锁是打开的,只要有一个线程进去执行代码了,锁就会关闭
           2 当线程执行完出来了,锁才会自动打开

第三部分 : 同步的好处和弊端
            好处 : 解决了多线程的数据安全问题
            弊端 : 当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
public class Ticket implements Runnable {
    private int ticketCount = 100; // 一共有一百张票

    @Override
    public void run() {
        while (true) {
            synchronized (Ticket.class) {
                // 如果票的数量为0 , 那么停止买票
                if (ticketCount <= 0) {
                    break;
                } else {
                    // 模拟出票的时间
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 有剩余的票 , 开始卖票
                    ticketCount--;
                    System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张");
                }
            }
        }
    }
}
package com.itheima.synchronized_demo1;

/*
    1 定义一个类Ticket实现Runnable接口,里面定义一个成员变量:private int ticketCount = 100;
    2 在Ticket类中重写run()方法实现卖票,代码步骤如下
        A:判断票数大于0,就卖票,并告知是哪个窗口卖的
        B:票数要减1
        C:卖光之后,线程停止
    3 定义一个测试类TicketDemo,里面有main方法,代码步骤如下
        A:创建Ticket类的对象
        B:创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
        C:启动线程

 */
public class TicketDemo {
    public static void main(String[] args) {
        // 创建任务类对象
        Ticket ticket = new Ticket();

        // 创建三个线程类对象
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        // 给三个线程命名
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        // 开启三个线程
        t1.start();
        t2.start();
        t3.start();
    }
}

1.4 同步方法

同步方法:就是把synchronized关键字加到方法上

格式:修饰符 synchronized 返回值类型 方法名(方法参数) {    }

同步代码块和同步方法的区别:
    1 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
    2 同步代码块可以指定锁对象,同步方法不能指定锁对象

注意 : 同步方法时不能指定锁对象的 , 但是有默认存在的锁对象的。
    1 对于非static方法,同步锁就是this。
    2 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。   Class类型的对象
package com.itheima.synchronized_demo2;

/*
    同步方法:就是把synchronized关键字加到方法上

    格式:修饰符 synchronized 返回值类型 方法名(方法参数) {    }

    同步代码块和同步方法的区别:
        1 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
        2 同步代码块可以指定锁对象,同步方法不能指定锁对象

    注意 : 同步方法时不能指定锁对象的 , 但是有默认存在的锁对象的。
        1 对于非static方法,同步锁就是this。
        2 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。   Class类型的对象

 */
public class Ticket implements Runnable {
    private int ticketCount = 100; // 一共有一百张票

    @Override
    public void run() {
        while (true) {
            if (method()) {
                break;
            }
        }
    }

    private synchronized boolean method() {
        // 如果票的数量为0 , 那么停止买票
        if (ticketCount <= 0) {
            return true;
        } else {
            // 模拟出票的时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 有剩余的票 , 开始卖票
            ticketCount--;
            System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张");
            return false;
        }
    }
}

package com.itheima.synchronized_demo2;

/*
    1 定义一个类Ticket实现Runnable接口,里面定义一个成员变量:private int ticketCount = 100;
    2 在Ticket类中重写run()方法实现卖票,代码步骤如下
        A:判断票数大于0,就卖票,并告知是哪个窗口卖的
        B:票数要减1
        C:卖光之后,线程停止
    3 定义一个测试类TicketDemo,里面有main方法,代码步骤如下
        A:创建Ticket类的对象
        B:创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
        C:启动线程

 */
public class TicketDemo {
    public static void main(String[] args) {
        // 创建任务类对象
        Ticket ticket = new Ticket();

        // 创建三个线程类对象
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        // 给三个线程命名
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        // 开启三个线程
        t1.start();
        t2.start();
        t3.start();
    }
}

1.5 Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock中提供了获得锁和释放锁的方法
    void lock():获得锁
    void unlock():释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
    ReentrantLock的构造方法
    ReentrantLock():创建一个ReentrantLock的实例

注意:多个线程使用相同的Lock锁对象,需要多线程操作数据的代码放在lock()和unLock()方法之间。一定要确保unlock最后能够调用
package com.itheima.synchronized_demo3;

import java.util.concurrent.locks.ReentrantLock;

/*
    虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,
    为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

    Lock中提供了获得锁和释放锁的方法
        void lock():获得锁
        void unlock():释放锁

    Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
        ReentrantLock的构造方法
        ReentrantLock​():创建一个ReentrantLock的实例

    注意:多个线程使用相同的Lock锁对象,需要多线程操作数据的代码放在lock()和unLock()方法之间。一定要确保unlock最后能够调用

 */
public class Ticket implements Runnable {
    private int ticketCount = 100; // 一共有一百张票
    ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();// 加锁
                // 如果票的数量为0 , 那么停止买票
                if (ticketCount <= 0) {
                    break;
                } else {
                    // 模拟出票的时间
                    Thread.sleep(100);
                    // 有剩余的票 , 开始卖票
                    ticketCount--;
                    System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();// 释放锁
            }
        }
    }
}

package com.itheima.synchronized_demo3;

/*
    1 定义一个类Ticket实现Runnable接口,里面定义一个成员变量:private int ticketCount = 100;
    2 在Ticket类中重写run()方法实现卖票,代码步骤如下
        A:判断票数大于0,就卖票,并告知是哪个窗口卖的
        B:票数要减1
        C:卖光之后,线程停止
    3 定义一个测试类TicketDemo,里面有main方法,代码步骤如下
        A:创建Ticket类的对象
        B:创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
        C:启动线程

 */
public class TicketDemo {
    public static void main(String[] args) {
        // 创建任务类对象
        Ticket ticket = new Ticket();

        // 创建三个线程类对象
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        // 给三个线程命名
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        // 开启三个线程
        t1.start();
        t2.start();
        t3.start();
    }
}

2 线程死锁

2.1 概述 :

  • 死锁是一种少见的,而且难于调试的错误,在两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。我们要避免死锁的产生。否则一旦死锁,除了重启没有其他办法的

2.2 产生条件 :

  • 多个线程
  • 存在锁对象的循环依赖

2.3 代码实践

package com.itheima.deadlock_demo;

/*
    死锁 :
        死锁是一种少见的,而且难于调试的错误,在两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。
        我们要避免死锁的产生。否则一旦死锁,除了重启没有其他办法的
 */
public class DeadLockDemo {
    public static void main(String[] args) {
        String 筷子A = "筷子A";
        String 筷子B = "筷子B";

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (筷子A) {
                        System.out.println("小白拿到了筷子A ,等待筷子B....");
                        synchronized (筷子B) {
                            System.out.println("小白拿到了筷子A和筷子B , 开吃!!!!!");
                        }
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "小白").start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (筷子B) {
                        System.out.println("小黑拿到了筷子B ,等待筷子A....");
                        synchronized (筷子A) {
                            System.out.println("小黑拿到了筷子B和筷子A , 开吃!!!!!");
                        }
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "小黑").start();
    }
}

3 线程的状态

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TPfOWK9P-1678071252289)(C:\Users\Ford\AppData\Roaming\Typora\typora-user-images\image-20230306004450550.png)]

4 线程通信

  • 线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例。等待唤醒机制其实就是让线程进入等待状态或者让线程从等待状态中唤醒,需要用到两种方法,如下:

  • 等待方法 :

    • void wait() 让线程进入无限等待。
    • void wait(long timeout) 让线程进入计时等待
    • 以上两个方法调用会导致当前线程释放掉锁资源。
  • 唤醒方法 :

    • void notify() 唤醒在此对象监视器(锁对象)上等待的单个线程。
    • void notifyAll() 唤醒在此对象监视器上等待的所有线程。
    • 以上两个方法调用不会导致当前线程释放掉锁资源
  • 注意

    • 等待和唤醒的方法,都要使用锁对象调用(需要在同步代码块中调用)
    • 等待和唤醒方法应该使用相同的锁对象调用
  • package com.itheima.waitnotify_demo;
    
    /*
        1 线程进入无限等待
            注意:进入无限等待需要使用锁在同步代码中调用wait方法
     */
    public class Test1 {
        public static void main(String[] args) {
            Object obj = new Object(); // 作为锁对象
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (obj) {
                        System.out.println("线程开始执行");
                        System.out.println("线程进入无线等待....");
                        try {
                            obj.wait(); // 进入无线等待状态 , 并释放锁
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("无线等待被唤醒....");
                    }
                }
            }).start();
        }
    }
    
    
  • package com.itheima.waitnotify_demo;
    
    /*
        线程进入无限等待后被唤醒
        注意:等待和唤醒是两个或多个线程之间实现的。进入无限等待的线程是不会自动唤醒,只能通过其他线程来唤醒。
     */
    public class Test2 {
        public static void main(String[] args) {
            Object obj = new Object(); // 作为锁对象
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (obj) {
                        System.out.println("线程开始执行");
                        System.out.println("线程进入无线等待....");
                        try {
                            obj.wait(); // 进入无线等待状态 , 并释放锁
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("无线等待被唤醒....");
                    }
                }
            }).start();
    
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj) {
                        obj.notify();// 随机唤醒此监视器中等待的线程 , 不会释放锁
                        System.out.println("唤醒后 , 5秒钟后释放锁");
                        try {
                            Thread.sleep(5000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }// 释放锁
                }
            }).start();
        }
    }
    
    
  • package com.itheima.waitnotify_demo;
    
    /*
        3 线程进入计时等待并唤醒
            注意:进入计时等待的线程,时间结束前可以被其他线程唤醒。时间结束后会自动唤醒
     */
    public class Test3 {
        public static void main(String[] args) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (Test3.class) {
                        System.out.println("获取到锁 , 开始执行");
                        try {
                            System.out.println("进入计时等待...3秒");
                            Test3.class.wait(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("自动唤醒.");
                    }
                }
            }).start();
        }
    }
    
    
  • 生产者和消费者案例
    package com.itheima.waitnotify_demo2;
    
    import sun.security.krb5.internal.crypto.Des;
    
    /*
        生产者步骤:
            1,判断桌子上是否有汉堡包
                如果有就等待,如果没有才生产。
            2,把汉堡包放在桌子上。
            3,叫醒等待的消费者开吃
     */
    public class Cooker implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (Desk.lock) {
                    if (Desk.count == 0) {
                        break;
                    } else {
                        if (Desk.flag) {
                            // 桌子上有食物
                            try {
                                Desk.lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        } else {
                            // 桌子上没有食物
                            System.out.println("厨师生产了一个汉堡包...");
                            Desk.flag = true;
                            Desk.lock.notify();
                        }
                    }
                }
            }
        }
    }
    
    
    package com.itheima.waitnotify_demo2;
    
    import sun.security.krb5.internal.crypto.Des;
    
    /*
        消费者步骤:
            1,判断桌子上是否有汉堡包。
            2,如果没有就等待。
            3,如果有就开吃
            4,吃完之后,桌子上的汉堡包就没有了
                叫醒等待的生产者继续生产
                汉堡包的总数量减一
     */
    public class Foodie implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (Desk.lock) {
                    if (Desk.count == 0) {
                        break;
                    } else {
                        if (Desk.flag) {
                            // 桌子上有食物
                            System.out.println("吃货吃了一个汉堡包...");
                            Desk.count--; // 汉堡包的数量减少一个
                            Desk.flag = false;// 桌子上的食物被吃掉 , 值为false
                            Desk.lock.notify();
                        } else {
                            // 桌子上没有食物
                            try {
                                Desk.lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    }
    
    
    package com.itheima.waitnotify_demo2;
    
    public class Test {
        public static void main(String[] args) {
            new Thread(new Foodie()).start();
            new Thread(new Cooker()).start();
        }
    }
    
    

5 线程池

5.1 线程使用存在的问题

  • 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
    如果大量线程在执行,会涉及到线程间上下文的切换,会极大的消耗CPU运算资源

5.2 线程池的介绍

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

5.3 线程池使用的大致流程

  • 创建线程池指定线程开启的数量
  • 提交任务给线程池,线程池中的线程就会获取任务,进行处理任务。
  • 线程处理完任务,不会销毁,而是返回到线程池中,等待下一个任务执行。
  • 如果线程池中的所有线程都被占用,提交的任务,只能等待线程池中的线程处理完当前任

5.4 线程池的好处

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

5.4 Java提供好的线程池

  • java.util.concurrent.ExecutorService 是线程池接口类型。使用时我们不需自己实现,JDK已经帮我们实现好了
  • 获取线程池我们使用工具类java.util.concurrent.Executors的静态方
    • public static ExecutorService newFixedThreadPool (int num) : 指定线程池最大线程池数量获取线程池
  • 线程池ExecutorService的相关方法
    • Future submit(Callable task)
    • Future<?> submit(Runnable task)
  • 关闭线程池方法(一般不使用关闭方法,除非后期不用或者很长时间都不用,就可以关闭)
    • void shutdown() 启动一次顺序关闭,执行以前提交的任务,但不接受新任务

5.5 线程池处理Runnable任务

package com.itheima.threadpool_demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
    1 需求 :
        使用线程池模拟游泳教练教学生游泳。
        游泳馆(线程池)内有3名教练(线程)
        游泳馆招收了5名学员学习游泳(任务)。

    2 实现步骤:
        创建线程池指定3个线程
        定义学员类实现Runnable,
        创建学员对象给线程池
 */
public class Test1 {
    public static void main(String[] args) {
        // 创建指定线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        // 提交任务
        threadPool.submit(new Student("小花"));
        threadPool.submit(new Student("小红"));
        threadPool.submit(new Student("小明"));
        threadPool.submit(new Student("小亮"));
        threadPool.submit(new Student("小白"));

        threadPool.shutdown();// 关闭线程池
    }
}

class Student implements Runnable {
    private String name;

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

    @Override
    public void run() {
        String coach = Thread.currentThread().getName();
        System.out.println(coach + "正在教" + name + "游泳...");

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(coach + "教" + name + "游泳完毕.");
    }
}

5.6 线程池处理Callable任务

package com.itheima.threadpool_demo;

import java.util.concurrent.*;

/*
    需求: Callable任务处理使用步骤
        1 创建线程池
        2 定义Callable任务
        3 创建Callable任务,提交任务给线程池
        4 获取执行结果

    <T> Future<T> submit(Callable<T> task) : 提交Callable任务方法    
    返回值类型Future的作用就是为了获取任务执行的结果。
    Future是一个接口,里面存在一个get方法用来获取值

    练一练:使用线程池计算 从0~n的和,并将结果返回
 */
public class Test2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建指定线程数量的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        Future<Integer> future = threadPool.submit(new CalculateTask(100));
        Integer sum = future.get();
        System.out.println(sum);
    }
}

// 使用线程池计算 从0~n的和,并将结果返回
class CalculateTask implements Callable<Integer> {
    private int num;

    public CalculateTask(int num) {
        this.num = num;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;// 求和变量
        for (int i = 0; i <= num; i++) {
            sum += i;
        }
        return sum;
    }
}

6.线程池详讲

6.1 线程池-基本原理

概述 :

​ 提到池,大家应该能想到的就是水池。水池就是一个容器,在该容器中存储了很多的水。那么什么是线程池呢?线程池也是可以看做成一个池子,在该池子中存储很多个线程。

线程池存在的意义:

​ 系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系

​ 统资源的消耗,这样就有点"舍本逐末"了。针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就

​ 会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。

线程池的设计思路 :

  1. 准备一个任务容器
  2. 一次性启动多个(2个)消费者线程
  3. 刚开始任务容器是空的,所以线程都在wait
  4. 直到一个外部线程向这个任务容器中扔了一个"任务",就会有一个消费者线程被唤醒
  5. 这个消费者线程取出"任务",并且执行这个任务,执行完毕后,继续等待下一次任务的到来

6.2 线程池-Executors默认线程池

概述 : JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。

我们可以使用Executors中所提供的静态方法来创建线程池

​ static ExecutorService newCachedThreadPool() 创建一个默认的线程池
​ static newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池

代码实现 :

package com.itheima.mythreadpool;


//static ExecutorService newCachedThreadPool()   创建一个默认的线程池
//static newFixedThreadPool(int nThreads)	    创建一个指定最多线程数量的线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {

        //1,创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
        ExecutorService executorService = Executors.newCachedThreadPool();
        //Executors --- 可以帮助我们创建线程池对象
        //ExecutorService --- 可以帮助我们控制线程池

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

        //Thread.sleep(2000);

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

        executorService.shutdown();
    }
}

6.3 线程池-Executors创建指定上限的线程池

使用Executors中所提供的静态方法来创建线程池

​ static ExecutorService newFixedThreadPool(int nThreads) : 创建一个指定最多线程数量的线程池

代码实现 :

package com.itheima.mythreadpool;

//static ExecutorService newFixedThreadPool(int nThreads)
//创建一个指定最多线程数量的线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class MyThreadPoolDemo2 {
    public static void main(String[] args) {
        //参数不是初始值而是最大值
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
        System.out.println(pool.getPoolSize());//0

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

        System.out.println(pool.getPoolSize());//2
//        executorService.shutdown();
    }
}

6.4 线程池-ThreadPoolExecutor

创建线程池对象 :

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);

代码实现 :

package com.itheima.mythreadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolDemo3 {
//    参数一:核心线程数量
//    参数二:最大线程数
//    参数三:空闲线程最大存活时间
//    参数四:时间单位
//    参数五:任务队列
//    参数六:创建线程工厂
//    参数七:任务的拒绝策略
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2,5,2,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());

        pool.shutdown();
    }
}

6.5 线程池-参数详解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHE4wqip-1678109998840)(.\img\1591165506516.png)]

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
    
corePoolSize:   核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime:  空闲线程最大存活时间,不能小于0
unit:           时间单位
workQueue:      任务队列,不能为null
threadFactory:  创建线程工厂,不能为null      
handler:        任务的拒绝策略,不能为null  

6.6 线程池-非默认任务拒绝策略

RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。

ThreadPoolExecutor.AbortPolicy: 		    丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 		   丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy:    抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy:        调用任务的run()方法绕过线程池直接执行。

注:明确线程池对多可执行的任务数 = 队列容量 + 最大线程数

案例演示1:演示ThreadPoolExecutor.AbortPolicy任务处理策略

public class ThreadPoolExecutorDemo01 {

    public static void main(String[] args) {

        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy()) ;

        // 提交5个任务,而该线程池最多可以处理4个任务,当我们使用AbortPolicy这个任务处理策略的时候,就会抛出异常
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }
    }
}

控制台输出结果

pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务
pool-1-thread-3---->> 执行了任务

控制台报错,仅仅执行了4个任务,有一个任务被丢弃了

案例演示2:演示ThreadPoolExecutor.DiscardPolicy任务处理策略

public class ThreadPoolExecutorDemo02 {
    public static void main(String[] args) {
        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardPolicy()) ;

        // 提交5个任务,而该线程池最多可以处理4个任务,当我们使用DiscardPolicy这个任务处理策略的时候,控制台不会报错
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }
    }
}

控制台输出结果

pool-1-thread-1---->> 执行了任务
pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务

控制台没有报错,仅仅执行了4个任务,有一个任务被丢弃了

案例演示3:演示ThreadPoolExecutor.DiscardOldestPolicy任务处理策略

public class ThreadPoolExecutorDemo02 {
    public static void main(String[] args) {
        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor;
        threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardOldestPolicy());
        // 提交5个任务
        for(int x = 0 ; x < 5 ; x++) {
            // 定义一个变量,来指定指定当前执行的任务;这个变量需要被final修饰
            final int y = x ;
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务" + y);
            });     
        }
    }
}

控制台输出结果

pool-1-thread-2---->> 执行了任务2
pool-1-thread-1---->> 执行了任务0
pool-1-thread-3---->> 执行了任务3
pool-1-thread-1---->> 执行了任务4

由于任务1在线程池中等待时间最长,因此任务1被丢弃。

案例演示4:演示ThreadPoolExecutor.CallerRunsPolicy任务处理策略

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

        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor;
        threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.CallerRunsPolicy());

        // 提交5个任务
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }
    }
}

控制台输出结果

pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务
pool-1-thread-1---->> 执行了任务
main---->> 执行了任务

通过控制台的输出,我们可以看到次策略没有通过线程池中的线程执行任务,而是直接调用任务的run()方法绕过线程池直接执行。

三、Lambda,Stream,File,递归

学习目标

  • Lambda表达式
  • Stream流
  • File类
  • 递归

1 Lambda表达式

1.1 体验Lambda表达式

  • package com.itheima.lambda_demo;
    
    /*
        Lambda表达式体验 :
     */
    public class LambdaDemo {
        public static void main(String[] args) {
            // 匿名内部类方式完成
            goSwimming(new Swimming() {
                @Override
                public void swim() {
                    System.out.println("铁汁 , 我们去游泳吧....");
                }
            });
    
            // lambda表达式的方式完成
            goSwimming(() -> System.out.println("铁汁 , 我们去游泳吧...."));
    
        }
    
        public static void goSwimming(Swimming swimming) {
            swimming.swim();
        }
    
    }
    
    interface Swimming {
        public abstract void swim();
    }
    
  • lambda表达式可以理解为对匿名内部类的一种简化 , 但是本质是有区别的

  • 面向对象思想 :

    • 强调的是用对象去完成某些功能
  • 函数式编程思想 :

    • 强调的是结果 , 而不是怎么去做

1.2 函数式接口

  • 只有一个抽象方法需要重写的接口,函数式接口。函数式接口是允许有其他的非抽象方法的存在例如静态方法,默认方法,私有方法。
  • 为了标识接口是一个函数式接口,可以在接口之上加上一个注解: @FunctionalInterface 以示区别
  • 在JDK中 java.util.function 包中的所有接口都是函数式接口。我们之前学习线程时学习的Runnable也是函数式接口
    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-10mjQIRh-1678071520534)(D:\传智播客\上海-JavaSE进阶面授\day10【Lambda表达式,Stream流,File类,递归】\笔记\img\image-20210412213315776.png)]

1.3 Lambda表达式的使用

  • 使用前提
    • 必须存在一个接口
    • 接口中有且只有一个抽象方法
  • 格式 : ( 形式参数 ) -> { 代码块 }
    • 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可
    • ->:由英文中画线和大于符号组成,固定写法。代表指向动作
    • 代码块:是我们具体要做的事情,也就是以前我们写的方法体内容

1.4 Lambda表达式的案例

package com.itheima.lambda_test;

/*
    练习1:
        1 编写一个接口(ShowHandler)
        2 在该接口中存在一个抽象方法(show),该方法是无参数无返回值
        3 在测试类(ShowHandlerDemo)中存在一个方法(useShowHandler)
        	方法的的参数是ShowHandler类型的,在方法内部调用了ShowHandler的show方法
 */
public class LambdaTest1 {
    public static void main(String[] args) {
        useShowHandler(() -> {
            System.out.println("我是一个lambda表达式....");
        });
    }

    public static void useShowHandler(ShowHandler showHandler) {
        showHandler.show();
    }
}

interface ShowHandler {
    public abstract void show();
}
package com.itheima.lambda_test;

/*
    1 首先存在一个接口(StringHandler)
    2 在该接口中存在一个抽象方法(printMessage),该方法是有参数无返回值
    3 在测试类(StringHandlerDemo)中存在一个方法(useStringHandler),
        方法的的参数是StringHandler类型的,
        在方法内部调用了StringHandler的printMessage方法
 */
public class LambdaTest2 {
    public static void main(String[] args) {
        useStringHandler((String msg) -> {
            System.out.println(msg);
        });
    }

    public static void useStringHandler(StringHandler stringHandler){
        stringHandler.printMessage("今天天气不错 , 挺风和日丽的...");
    }
}

@FunctionalInterface
interface StringHandler {
    public abstract void printMessage(String msg);
}

package com.itheima.lambda_test;

import java.util.Random;

/*
    1 首先存在一个接口(RandomNumHandler)
    2 在该接口中存在一个抽象方法(getNumber),该方法是无参数但是有返回值
    3 在测试类(RandomNumHandlerDemo)中存在一个方法(useRandomNumHandler),方法的的参数是RandomNumHandler类型的
        在方法内部调用了RandomNumHandler的getNumber方法

 */
public class LambdaTest3 {
    public static void main(String[] args) {
        useRandomNumHandler(() -> {
            return new Random().nextInt(10) + 1;
        });
    }

    public static void useRandomNumHandler(RandomNumHandler randomNumHandler) {
        int number = randomNumHandler.getNumber();
        System.out.println(number);
    }
}

interface RandomNumHandler {
    public abstract int getNumber();
}

package com.itheima.lambda_test;

/*
    1 首先存在一个接口(Calculator)
    2 在该接口中存在一个抽象方法(calc),该方法是有参数也有返回值
    3 在测试类(CalculatorDemo)中存在一个方法(useCalculator)
        方法的的参数是Calculator类型的
        在方法内部调用了Calculator的calc方法

 */
public class LambdaTest4 {
    public static void main(String[] args) {
        useCalculator((int a , int b) -> { return a + b;});
    }

    public static void useCalculator(Calculator calculator) {
        int calc = calculator.calc(10, 20);
        System.out.println(calc);
    }
}

@FunctionalInterface
interface Calculator {
    public abstract int calc(int a, int b);
}

2 Stream流

2.1 Stream的体验

package com.itheima.stream_demo;

import java.util.ArrayList;

/*
    体验Stream流的好处

    需求:按照下面的要求完成集合的创建和遍历

        1 创建一个集合,存储多个字符串元素
            "张无忌" , "张翠山" , "张三丰" , "谢广坤" , "赵四" , "刘能" , "小沈阳" , "张良"
        2 把集合中所有以"张"开头的元素存储到一个新的集合
        3 把"张"开头的集合中的长度为3的元素存储到一个新的集合
        4 遍历上一步得到的集合

 */
public class StreamDemo {
    public static void main(String[] args) {
        // 传统方式完成
        ArrayList<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("张翠山");
        list.add("张三丰");
        list.add("谢广坤");
        list.add("赵四");
        list.add("刘能");
        list.add("小沈阳");
        list.add("张良");

        // 把集合中所有以"张"开头的元素存储到一个新的集合
        ArrayList<String> list2 = new ArrayList<>();
        for (String s : list) {
            if (s.startsWith("张")) {
                list2.add(s);
            }
        }

        // 把"张"开头的集合中的长度为3的元素存储到一个新的集合
        ArrayList<String> list3 = new ArrayList<>();
        for (String s : list2) {
            if (s.length() == 3) {
                list3.add(s);
            }
        }
        // 遍历list3集合
        for (String s : list3) {
            System.out.println(s);
        }

        System.out.println("================================");

        // Stream流的方式
        list.stream().filter(s -> s.startsWith("张") && s.length() == 3).forEach(s -> System.out.println(s));
    }
}

2.2 Stream流的三类方法

  • 获取Stream流
    • 创建一条流水线,并把数据放到流水线上准备进行操作
  • 中间方法
    • 流水线上的操作。
    • 一次操作完毕之后,还可以继续进行其他操作
  • 终结方法
    • 一个Stream流只能有一个终结方法
    • 是流水线上的最后一个操作

2.3 第一类 : 获取方法

package com.itheima.stream_demo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

/*
    Stream流中三类方法之一 :  获取方法

    1 单列集合
       可以使用Collection接口中的默认方法stream()生成流
       default Stream<E> stream()
    2 双列集合
        双列集合不能直接获取 , 需要间接的生成流
        可以先通过keySet或者entrySet获取一个Set集合,再获取Stream流
    3 数组
       Arrays中的静态方法stream生成流
 */
public class StreamDemo2 {
    public static void main(String[] args) {
        // 单列集合的获取
        // method1();

        // 双列集合的获取
        // method2();

        // 数组获取
        // method3();

    }

    private static void method3() {
        int[] arr = {1, 2, 3, 4, 5, 6};
        Arrays.stream(arr).forEach(s -> System.out.println(s));
    }

    private static void method2() {
        HashMap<String, String> hm = new HashMap<>();
        hm.put("it001", "曹植");
        hm.put("it002", "曹丕");
        hm.put("it003", "曹熊");
        hm.put("it004", "曹冲");
        hm.put("it005", "曹昂");

        // 获取map集合的健集合 , 在进行输出打印
        hm.keySet().stream().forEach(s -> System.out.println(s));
        // 获取map集合的entry对象 , 在输出打印
        hm.entrySet().stream().forEach(s -> System.out.println(s));
    }

    private static void method1() {
        // 可以使用Collection接口中的默认方法stream()生成流
        ArrayList<String> list = new ArrayList<>();
        list.add("迪丽热巴");
        list.add("古力娜扎");
        list.add("马尔扎哈");
        list.add("欧阳娜娜");
        list.stream().forEach(s -> System.out.println(s));
    }
}

2.4 第二类 : 中间方法

package com.itheima.stream_demo;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.function.Predicate;
import java.util.stream.Stream;

/*
    Stream流中三类方法之一 :  中间方法

    1 Stream<T> filter(Predicate predicate):用于对流中的数据进行过滤
        Predicate接口中的方法 : boolean test(T t):对给定的参数进行判断,返回一个布尔值
    2 Stream<T> limit(long maxSize):截取指定参数个数的数据
    3 Stream<T> skip(long n):跳过指定参数个数的数据
    4 static <T> Stream<T> concat(Stream a, Stream b):合并a和b两个流为一个流
    5 Stream<T> distinct():去除流中重复的元素。依赖(hashCode和equals方法)
    6 Stream<T> sorted () : 将流中元素按照自然排序的规则排序
    7 Stream<T> sorted (Comparator<? super T> comparator) : 将流中元素按照自定义比较器规则排序

 */
public class StreamDemo3 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("张翠山");
        list.add("张三丰");
        list.add("谢广坤");
        list.add("赵四");
        list.add("刘能");
        list.add("小沈阳");
        list.add("张良");
        list.add("张良");
        list.add("张良");
        list.add("张良");

        // Stream<T> limit(long maxSize):截取指定参数个数的数据
        // list.stream().limit(3).forEach(s -> System.out.println(s));

        // Stream<T> skip(long n):跳过指定参数个数的数据
        // list.stream().skip(3).forEach(s-> System.out.println(s));

        // Stream<T> distinct():去除流中重复的元素。依赖(hashCode和equals方法)
        // list.stream().distinct().forEach(s->{System.out.println(s);});
        
    }

    // // Stream<T> sorted (Comparator<? super T> comparator) : 将流中元素按照自定义比较器规则排序
    private static void method3(ArrayList<String> list) {
        list.stream().sorted(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.length() - o2.length();
            }
        }).forEach(s->{
            System.out.println(s);
        });
    }

    // Stream<T> sorted () : 将流中元素按照自然排序的规则排序
    private static void method3() {
        ArrayList<Integer> list2 = new ArrayList<>();
        list2.add(3);
        list2.add(1);
        list2.add(2);
        list2.stream().sorted().forEach(s->{
            System.out.println(s);
        });
    }

    // // static <T> Stream<T> concat(Stream a, Stream b):合并a和b两个流为一个流
    private static void method2(ArrayList<String> list) {
        ArrayList<String> list2 = new ArrayList<>();
        list2.add("迪丽热巴");
        list2.add("古力娜扎");
        list2.add("欧阳娜娜");
        list2.add("马尔扎哈");

        Stream.concat(list.stream(), list2.stream()).forEach(s -> {
            System.out.println(s);
        });
    }

    // Stream<T> filter(Predicate predicate):用于对流中的数据进行过滤
    private static void method1(ArrayList<String> list) {
        // filter方法会获取流中的每一个数据
        // s就代表的是流中的每一个数据
        // 如果返回值为true , 那么代表的是数据留下来
        // 如果返回值的是false , 那么代表的是数据过滤掉
//        list.stream().filter(new Predicate<String>() {
//            @Override
//            public boolean test(String s) {
//                boolean result = s.startsWith("张");
//                return result;
//            }
//        }).forEach(s -> System.out.println(s));

        list.stream().filter(s ->
                s.startsWith("张")
        ).forEach(s -> System.out.println(s));
    }
}

2.5 第三类 : 终结方法

package com.itheima.stream_demo;

import java.util.ArrayList;
import java.util.function.Consumer;

/*
    Stream流中三类方法之一 :  终结方法
    1 void forEach(Consumer action):对此流的每个元素执行操作
        Consumer接口中的方法 void accept(T t):对给定的参数执行此操作
    2 long count():返回此流中的元素数

 */
public class StreamDemo4 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("张翠山");
        list.add("张三丰");
        list.add("谢广坤");

        // long count():返回此流中的元素数
        long count = list.stream().count();
        System.out.println(count);
    }
    // void forEach(Consumer action):对此流的每个元素执行操作
    private static void method1(ArrayList<String> list) {
        // 把list集合中的元素放在stream流中
        // forEach方法会循环遍历流中的数据
        // 并循环调用accept方法 , 把数据传给s
        // 所以s就代表的是流中的每一个数据
        // 我们只要在accept方法中对数据做业务逻辑处理即可
        list.stream().forEach(new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        });

        System.out.println("=====================");

        list.stream().forEach( (String s) -> {
            System.out.println(s);
        });

        System.out.println("=====================");

        list.stream().forEach(  s -> { System.out.println(s); });
    }

}

2.6 Stream流中的收集方法

package com.itheima.stream_demo;

import java.util.ArrayList;

/*
    Stream流的收集操作 : 第一部分

    需求:过滤元素并遍历集合
        定义一个集合,并添加一些整数1,2,3,4,5,6,7,8,9,10
        将集合中的奇数删除,只保留偶数。
        遍历集合得到2,4,6,8,10

    结论:在Stream流中无法直接修改集合,数组等数据源中的数据。
 */
public class StreamDemo5 {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();

        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }

        list.stream().filter(num -> num % 2 == 0).forEach(num -> System.out.println(num));

        System.out.println("=============");

        // 结论:在Stream流中无法直接修改集合,数组中的数据。
        System.out.println(list);
    }
}
  • 结论:在Stream流中无法直接修改集合,数组中的数据
package com.itheima.stream_demo;

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

/*

    Stream流的收集操作 : 第二部分

    使用Stream流的方式操作完毕之后,我想把流中的数据起来,该怎么办呢?

    Stream流的收集方法
    R collect(Collector collector) : 此方法只负责收集流中的数据 , 创建集合添加数据动作需要依赖于参数

    工具类Collectors提供了具体的收集方式
    public static <T> Collector toList():把元素收集到List集合中
    public static <T> Collector toSet():把元素收集到Set集合中
    public static  Collector toMap(Function keyMapper,Function valueMapper):把元素收集到Map集合中


    需求 :
        定义一个集合,并添加一些整数1,2,3,4,5,6,7,8,9,10
        将集合中的奇数删除,只保留偶数。
        遍历集合得到2,4,6,8,10
 */
public class StreamDemo6 {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }
        list.add(10);
        list.add(10);
        list.add(10);
        list.add(10);
        list.add(10);

        // collect只负责收集流中的数据
        // Collectors.toList()会负责在底层创建list集合 ,并把数据添加到集合中 , 返回集合对象
        List<Integer> list2 = list.stream().filter(num -> num % 2 == 0 ).collect(Collectors.toList());
        System.out.println(list2);

        Set<Integer> set = list.stream().filter(num -> num % 2 == 0 ).collect(Collectors.toSet());
        System.out.println(set);

    }
}
package com.itheima.stream_demo;

import java.util.ArrayList;
import java.util.Map;
import java.util.stream.Collectors;

/*
    Stream流的收集操作 : 第三部分

    1 创建一个ArrayList集合,并添加以下字符串。字符串中前面是姓名,后面是年龄
        "zhangsan,23"
        "lisi,24"
        "wangwu,25"
    2 保留年龄大于等于24岁的人,并将结果收集到Map集合中,姓名为键,年龄为值

    收集方法 :
    public static  Collector toMap(Function keyMapper  ,   Function valueMapper):把元素收集到Map集合中
 */
public class StreamDemo7 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("zhangsan,23");
        list.add("lisi,24");
        list.add("wangwu,25");

        // collect 只负责收集数据
        // Collectors.toMap负责在底层创建集合对象 , 添加元素
        // toMap方法中的s就是代表的是集合中的每一个元素
        // 第一个参数 : 如何获取map集合中的键
        // 第二个参数 : 如何获取map集合中的值
        Map<String, String> map = list.stream().filter(s -> Integer.parseInt(s.split(",")[1]) > 23).collect(
                Collectors.toMap(
                        (String s) -> {
                            return s.split(",")[0];
                        }
                        ,
                        (String s) -> {
                            return s.split(",")[1];
                        }
                )
        );
        System.out.println(map);
    }
}

3 File类

3.1 File类的介绍

  • java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作

3.2 构造方法

package com.itheima.file_demo;

import java.io.File;

/*
    File:它是文件和目录路径名的抽象表示
        文件和目录可以通过File封装成对象
        File封装的对象仅仅是一个路径名。它可以是存在的,也可以是不存在的。
    构造方法 :
        1 File(String pathname)   通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例
        2 File(String parent, String child)   从父路径名字符串和子路径名字符串创建新的 File实例
        3 File(File parent, String child) 从父抽象路径名和子路径名字符串创建新的 File实例
 */
public class FileDemo1 {
    public static void main(String[] args) {
//        1 File(String pathname) 通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例
        File f1 = new File("D:\\abc.txt");
        System.out.println(f1);
//        2 File(String parent, String child) 从父路径名字符串和子路径名字符串创建新的 File实例
        File f2 = new File("D:\\aaa", "bbb.txt");
        System.out.println(f2);

//        3 File(File parent, String child)   从父抽象路径名和子路径名字符串创建新的 File实例
        File f3 = new File(new File("D:\\aaa"), "bbb.txt");
        System.out.println(f3);
    }
}

3.3 File类的创建功能

package com.itheima.file_demo;


import java.io.File;
import java.io.IOException;

/*
    File类的创建功能 :
        1 public boolean createNewFile()    : 创建一个新的空的文件
        2 public boolean mkdir()   : 创建一个单级文件夹
        3 public boolean mkdirs() : 创建一个多级文件夹
 */
public class FileDemo2 {
    public static void main(String[] args) throws IOException {
        File f1 = new File("D:\\a.txt");
        // 1 public boolean createNewFile()     : 创建一个新的空的文件
        System.out.println(f1.createNewFile());

        File f2 = new File("day10_demo\\aaa");
        //  2 public boolean mkdir()   : 创建一个单级文件夹
        System.out.println(f2.mkdir());

        File f3 = new File("day10_demo\\bbb\\ccc");
        // 3 public boolean mkdirs() : 创建一个多级文件夹
        System.out.println(f3.mkdirs());
    }
}

3.4 File类的删除功能

package com.itheima.file_demo;

import java.io.File;
import java.io.IOException;

/*
    File类删除功能 :
        public boolean delete()    删除由此抽象路径名表示的文件或目录

        注意 : 
            1 delete方法直接删除不走回收站。
            2 如果删除的是一个文件,直接删除。
            3 如果删除的是一个文件夹,需要先删除文件夹中的内容,最后才能删除文件夹
 */
public class FileDemo3 {

    public static void main(String[] args) throws IOException {
        File f1 = new File("D:\\a.txt");
        // 1 public boolean createNewFile()     : 创建一个新的空的文件
        System.out.println(f1.delete());

        File f2 = new File("day10_demo\\aaa");
        //  2 public boolean mkdir()   : 创建一个单级文件夹
        System.out.println(f2.delete());

        File f3 = new File("day10_demo\\bbb");
        // 3 public boolean mkdirs() : 创建一个多级文件夹
        System.out.println(f3.delete());
    }
}

3.5 File类的判断和获取功能

package com.itheima.file_demo;

import java.io.File;

/*
    File类判断和获取功能
        public boolean isDirectory()   测试此抽象路径名表示的File是否为目录
        public boolean isFile()    测试此抽象路径名表示的File是否为文件
        public boolean exists()    测试此抽象路径名表示的File是否存在
        public String getAbsolutePath()    返回此抽象路径名的绝对路径名字符串
        public String getPath()    将此抽象路径名转换为路径名字符串
        public String getName()    返回由此抽象路径名表示的文件或目录的名称
 */
public class FileDemo4 {
    public static void main(String[] args) {
        File f1 = new File("day10_demo\\aaa");
        File f2 = new File("day10_demo\\a.txt");

//        public boolean isDirectory() 测试此抽象路径名表示的File是否为目录
        System.out.println(f1.isDirectory());
        System.out.println(f2.isDirectory());

//        public boolean isFile()  测试此抽象路径名表示的File是否为文件
        System.out.println(f1.isFile());
        System.out.println(f2.isFile());

//        public boolean exists()  测试此抽象路径名表示的File是否存在
        System.out.println(f1.exists());

//        public String getAbsolutePath()  返回此抽象路径名的绝对路径名字符串
        System.out.println(f1.getAbsolutePath());

//        public String getPath()  将此抽象路径名转换为路径名字符串
        System.out.println(f1.getPath());

//        public String getName()  返回由此抽象路径名表示的文件或目录的名称
        System.out.println(f2.getName());
    }
}

3.6 File类高级获取功能

package com.itheima.file_demo;

import java.io.File;

/*
    File类高级获取功能
        public File[] listFiles()  返回此抽象路径名表示的目录中的文件和目录的File对象数组

    listFiles方法注意事项:
        1 当调用者不存在时,返回null
        2 当调用者是一个文件时,返回null
        3 当调用者是一个空文件夹时,返回一个长度为0的数组
        4 当调用者是一个有内容的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回
        5 当调用者是一个有隐藏文件的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回,包含隐藏内容
 */
public class FileDemo5 {
    public static void main(String[] args) {
        File file = new File("day10_demo\\bbb\\ccc");

        // 返回此抽象路径名表示的目录中的文件和目录的File对象数组
        File[] files = file.listFiles();

        // 遍历File类的数组
        for (File f : files) {
            // 拿到每一个文件的文字
            System.out.println(f.getName());
        }
    }
}
练习 :
package com.itheima.file_demo;

import java.io.File;
import java.util.HashMap;

/*
    练习二 :统计一个文件夹中每种文件的个数并打印。

   打印格式如下:
      txt:3个
      doc:4个
      jpg:6个
      …
 */
public class FileTest2 {
    public static void main(String[] args) {
        File file = new File("day10_demo\\统计文件个数文件夹");

        getFileCount(file);
    }

    public static void getFileCount(File f) {

        HashMap<String, Integer> hm = new HashMap<>();

        // 是文件夹在获取所有的子文件
        if (f.isDirectory()) {
            File[] files = f.listFiles();
            // 遍历数组
            for (File file : files) {
                String fileName = file.getName();
                String name = fileName.split("\\.")[1];
                if (hm.containsKey(name)) {
                    hm.put(name, hm.get(name));
                } else {
                    hm.put(name, 1);
                }
            }
        }

        System.out.println(hm);
    }
}

4 递归

  • 概述
    • 递归概述:以编程的角度来看,递归指的是方法定义中调用方法本身的现象
  • 好处
    • 把一个复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解
      小的问题解决 , 大的问题也会逐一进行解决

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uhNIM0vP-1678071520536)(D:\传智播客\上海-JavaSE进阶面授\day10【Lambda表达式,Stream流,File类,递归】\笔记\img\image-20210413021516935.png)]

  • 注意
    • 递归出口:否则会出现内存溢出
    • 递归规则:需要找到与原问题相似的规模较小的问题
  • 案例
    • package com.itheima.recursion_demo;
      
      
      /*
              需求:用递归求5的阶乘,并把结果在控制台输出
      
              思路:
                  1 定义一个方法,用于递归求阶乘,参数为一个int类型的变量
                  2 在方法内部判断该变量的值是否是1
                      是:返回1
                      不是:返回n*(n-1)!
                  3 调用方法
                  4 输出结果
      
       */
      public class Demo1 {
          public static void main(String[] args) {
              int result = jc(5);
              System.out.println("5的阶乘是:" + result);
          }
      
          private static int jc(int n) {
              if (n == 1) {
                  return 1;
              }
              return n * jc(n - 1);
          }
      }
      
    • package com.itheima.recursion_demo;
      
      import java.io.File;
      
      /*
          需求 : 使用递归删除计算机中指定的文件夹
      
          删除D盘中的aaa文件夹!
      
       */
      public class Demo2 {
          public static void main(String[] args) {
              File f = new File("D:\\aaa");
              deleteFiles(f);
          }
      
          private static void deleteFiles(File f) {
      
              File[] files = f.listFiles();
      
              for (File file : files) {
                  if (file.isDirectory()) {
                      // 递归
                      deleteFiles(file);
                  } else {
                      // 删除文件
                      file.delete();
                  }
              }
              // 删除当前文件夹
              f.delete();
          }
      }
      

四、IO流,Properties集合,IO工具类

学习目标

  • IO流的介绍
  • IO流的分类
  • 字节输出流
  • 字节输入流
  • 字节缓冲区流
  • Properties集合

1 IO流的介绍

1.1 为什么要学习IO流

  • 通过变量,数组,或者集合存储数据
    • 都是不能永久化存储 , 因为数据都是存储在内存中
    • 只要代码运行结束,所有数据都会丢失
  • 使用IO流
    • 1,将数据写到文件中,实现数据永久化存储
    • 2,把文件中的数据读取到内存中(Java程序)

1.2 什么是IO流

  • I 表示intput ,是数据从硬盘进内存的过程,称之为读。
  • O 表示output ,是数据从内存到硬盘的过程。称之为写
  • IO的数据传输,可以看做是一种数据的流动,按照流动的方向,以内存为参照物,进行读写操作
    • 简单来说:内存在读,内存在写

1.3 IO流的分类

  • 按照流向区分
    • 输入流 : 用来读数据
    • 输出流 : 用来写数据
  • 按照类型区分
    • 字节流
    • 字符流
  • 注意 :
    • 字节流可以操作任意文件
    • 字符流只能操作纯文本文件
    • 用windows记事本打开能读的懂,那么这样的文件就是纯文本文件。

2 字节流输出流

2.1 字节输出流入门

  • FileOutputStream类 :
    • OutputStream有很多子类,我们从最简单的一个子类开始。
    • java.io.FileOutputStream 类是文件输出流,用于将数据写出到文件
  • 构造方法 :
    • public FileOutputStream(File file):创建文件输出流以写入由指定的 File对象表示的文件。
    • public FileOutputStream(String name): 创建文件输出流以指定的名称写入文件。
    public class FileOutputStreamConstructor throws IOException {
        public static void main(String[] args) {
       	 	// 使用File对象创建流对象
            File file = new File("a.txt");
            FileOutputStream fos = new FileOutputStream(file);
          
            // 使用文件名称创建流对象
            FileOutputStream fos = new FileOutputStream("b.txt");
        }
    }
    
  • 字节输出流写数据快速入门
    • 创建字节输出流对象。
    • 写数据
    • 释放资源
    package com.itheima.outputstream_demo;
    
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    
    /*
        字节输出流写数据快速入门 :
            1 创建字节输出流对象。
            2 写数据
            3 释放资源
     */
    public class OutputStreamDemo1 {
        public static void main(String[] args) throws IOException {
            // 创建字节输出流对象
            // 如果指定的文件不存在 , 会自动创建文件
            // 如果文件存在 , 会把文件中的内容清空
            FileOutputStream fos = new FileOutputStream("day11_demo\\a.txt");
    
            // 写数据
            // 写到文件中就是以字节形式存在的
            // 只是文件帮我们把字节翻译成了对应的字符 , 方便查看
            fos.write(97);
            fos.write(98);
            fos.write(99);
    
            // 释放资源
            // while(true){}
            // 断开流与文件中间的关系
            fos.close();
        }
    }
    

2.2 字节输出流写数据的方法

  • 字节流写数据的方法

    • 1 void write(int b) 一次写一个字节数据
    • 2 void write(byte[] b) 一次写一个字节数组数据
    • 3 void write(byte[] b, int off, int len) 一次写一个字节数组的部分数据
    package com.itheima.outputstream_demo;
    
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    
    /*
        字节流写数据的3种方式
            1 void write​(int b)	一次写一个字节数据
            2 void write​(byte[] b)	一次写一个字节数组数据
            3 void write​(byte[] b, int off, int len)	一次写一个字节数组的部分数据
     */
    public class OutputStreamDemo2 {
        public static void main(String[] args) throws IOException {
            // 创建字节输出流对象
            FileOutputStream fos = new FileOutputStream("day11_demo\\a.txt");
    
            // 写数据
    //        1 void write​(int b)	一次写一个字节数据
            fos.write(97);
            fos.write(98);
            fos.write(99);
    
    //        2 void write​(byte[] b)	一次写一个字节数组数据
            byte[] bys = {65, 66, 67, 68, 69};
            fos.write(bys);
    
    //        3 void write​(byte[] b, int off, int len)	一次写一个字节数组的部分数据
            fos.write(bys, 0, 3);
    
            // 释放资源
            fos.close();
        }
    }
    

2.3 写数据的换行和追加写入

package com.itheima.outputstream_demo;

import java.io.FileOutputStream;
import java.io.IOException;

/*
    字节流写数据的换行和追加写入

    1 字节流写数据如何实现换行呢?
        写完数据后,加换行符
        windows : \r\n
        linux : \n
        mac : \r

    2 字节流写数据如何实现追加写入呢?
        通过构造方法 : public FileOutputStream(String name,boolean append)
        创建文件输出流以指定的名称写入文件。如果第二个参数为true ,不会清空文件里面的内容
 */
public class OutputStreamDemo3 {
    public static void main(String[] args) throws IOException {
        // 创建字节输出流对象
        FileOutputStream fos = new FileOutputStream("day11_demo\\a.txt");

        // void write(int b)  一次写一个字节数据
        fos.write(97);
        // 因为字节流无法写入一个字符串 , 把字符串转成字节数组写入
        fos.write("\r\n".getBytes());
        fos.write(98);
        fos.write("\r\n".getBytes());
        fos.write(99);
        fos.write("\r\n".getBytes());

        // 释放资源
        fos.close();
    }
}
package com.itheima.outputstream_demo;

import java.io.FileOutputStream;
import java.io.IOException;

/*
    字节流写数据的换行和追加写入

    1 字节流写数据如何实现换行呢?
        写完数据后,加换行符
        windows : \r\n
        linux : \n
        mac : \r

    2 字节流写数据如何实现追加写入呢?
        通过构造方法 : public FileOutputStream​(String name,boolean append)
        创建文件输出流以指定的名称写入文件。如果第二个参数为true ,不会清空文件里面的内容
 */
public class OutputStreamDemo3 {
    public static void main(String[] args) throws IOException {
        // 创建字节输出流对象
        // 追加写数据
        // 通过构造方法 : public FileOutputStream​(String name,boolean append) : 追加写数据
        FileOutputStream fos = new FileOutputStream("day11_demo\\a.txt" , true);

        // void write​(int b)	一次写一个字节数据
        fos.write(97);
        // 因为字节流无法写入一个字符串 , 把字符串转成字节数组写入
        fos.write("\r\n".getBytes());
        fos.write(98);
        fos.write("\r\n".getBytes());
        fos.write(99);
        fos.write("\r\n".getBytes());

        // 释放资源
        fos.close();
    }
    // 写完数据换行操作
    private static void method1() throws IOException {
        // 创建字节输出流对象
        FileOutputStream fos = new FileOutputStream("day11_demo\\a.txt");

        // void write​(int b)	一次写一个字节数据
        fos.write(97);
        // 因为字节流无法写入一个字符串 , 把字符串转成字节数组写入
        fos.write("\r\n".getBytes());
        fos.write(98);
        fos.write("\r\n".getBytes());
        fos.write(99);
        fos.write("\r\n".getBytes());

        // 释放资源
        fos.close();
    }
}

3 字节输入流

3.1 字节输入流介绍

  • 字节输入流类
    • InputStream类 : 字节输入流最顶层的类 , 抽象类
      — FileInputStream类 : FileInputStream extends InputStream
  • 构造方法
    • public FileInputStream(File file) : 从file类型的路径中读取数据
    • public FileInputStream(String name) : 从字符串路径中读取数据
  • 步骤
    • 创建输入流对象
    • 读数据
    • 释放资源
  • package com.itheima.inputstream_demo;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    
    /*
        字节输入流写数据快速入门 : 一次读一个字节
                第一部分 : 字节输入流类
                    InputStream类 : 字节输入流最顶层的类 , 抽象类
                    --- FileInputStream类 : FileInputStream extends InputStream
                第二部分 : 构造方法
                    public FileInputStream(File file) :  从file类型的路径中读取数据
                    public FileInputStream(String name) : 从字符串路径中读取数据
                第三部分 : 字节输入流步骤
                    1 创建输入流对象
                    2 读数据
                    3 释放资源
     */
    public class FileInputStreamDemo1 {
        public static void main(String[] args) throws IOException {
            // 创建字节输入流对象
            // 读取的文件必须存在 , 不存在则报错
            FileInputStream fis = new FileInputStream("day11_demo\\a.txt");
    
            // 读数据 , 从文件中读到一个字节
            // 返回的是一个int类型的字节
            // 如果想看字符, 需要强转
            int by = fis.read();
            System.out.println((char) by);
    
            // 释放资源
            fis.close();
        }
    }
    

3.2 字节输入流读多个字节

package com.itheima.inputstream_demo;

import java.io.FileInputStream;
import java.io.IOException;

/*
    字节输入流写数据快速入门 : 读多个字节
            第一部分 : 字节输入流类
                InputStream类 : 字节输入流最顶层的类 , 抽象类
                --- FileInputStream类 : FileInputStream extends InputStream
            第二部分 : 构造方法
                public FileInputStream(File file) :  从file类型的路径中读取数据
                public FileInputStream(String name) : 从字符串路径中读取数据
            第三部分 : 字节输入流步骤
                1 创建输入流对象
                2 读数据
                3 释放资源
 */
public class FileInputStreamDemo2 {
    public static void main(String[] args) throws IOException {
        // 创建字节输入流对象
        // 读取的文件必须存在 , 不存在则报错
        FileInputStream fis = new FileInputStream("day11_demo\\a.txt");

        // 读数据 , 从文件中读到一个字节
        // 返回的是一个int类型的字节
        // 如果想看字符, 需要强转
//        int by = fis.read();
//        System.out.println(by);
//        by = fis.read();
//        System.out.println(by);
//        by = fis.read();
//        System.out.println(by);
//
//        by = fis.read();
//        System.out.println(by);
//        by = fis.read();
//        System.out.println(by);
//        by = fis.read();
//        System.out.println(by);

        // 循环改进
        int by;// 记录每次读到的字节
        while ((by = fis.read()) != -1) {
            System.out.print((char) by);
        }

        // 释放资源
        fis.close();
    }
}

3.3 图片的拷贝

package com.itheima.inputstream_demo;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/*
    需求 : 把 "图片路径\xxx.jpg" 复制到当前模块下

    分析:
        复制文件,其实就把文件的内容从一个文件中读取出来(数据源),然后写入到另一个文件中(目的地)
        数据源:
            xxx.jpg  --- 读数据 --- FileInputStream
        目的地:
            模块名称\copy.jpg --- 写数据 --- FileOutputStream

 */
public class FileInputStreamDemo2 {
    public static void main(String[] args) throws IOException {
        // 创建字节输入流对象
        FileInputStream fis = new FileInputStream("D:\\传智播客\\安装包\\好看的图片\\liqin.jpg");

        // 创建字节输出流
        FileOutputStream fos = new FileOutputStream("day11_demo\\copy.jpg");

        // 一次读写一个字节
        int by;
        while ((by = fis.read()) != -1) {
            fos.write(by);
        }

        // 释放资源
        fis.close();
        fos.close();
    }
}

3.4 异常的捕获处理

  • JDK7版本之前处理方式 : 手动释放资源
    package com.itheima.inputstream_demo;
    
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    
    /*
        需求 : 对上一个赋值图片的代码进行使用捕获方式处理
     */
    public class FileInputStreamDemo4 {
        public static void main(String[] args) {
            FileInputStream fis = null ;
            FileOutputStream fos = null;
            try {
                // 创建字节输入流对象
                fis = new FileInputStream("D:\\传智播客\\安装包\\好看的图片\\liqin.jpg");
    
                // 创建字节输出流
                fos = new FileOutputStream("day11_demo\\copy.jpg");
    
                // 一次读写一个字节
                int by;
                while ((by = fis.read()) != -1) {
                    fos.write(by);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                // 释放资源
                if(fis != null){
                    try {
                        fis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                // 释放资源
                if(fos != null){
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
    
  • JDK7版本优化处理方式 : 自动释放资源
    • JDK7优化后可以使用 try-with-resource 语句 , 该语句确保了每个资源在语句结束时自动关闭。
      简单理解 : 使用此语句,会自动释放资源 , 不需要自己在写finally代码块了

    • 格式 :

      格式 :  
      try (创建流对象语句1 ; 创建流对象语句2 ...) {
              // 读写数据
          } catch (IOException e) {
              处理异常的代码...
          }
      
      举例 :
          try ( 
              FileInputStream fis1 = new FileInputStream("day11_demo\\a.txt") ; 
      	    FileInputStream fis2 = new FileInputStream("day11_demo\\b.txt") ) 
          {
              // 读写数据
          } catch (IOException e) {
              处理异常的代码...
          }
      
      
  • 代码实践
    package com.itheima.inputstream_demo;
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    
    /*
        JDK7版本优化处理方式
    
        需求 : 对上一个赋值图片的代码进行使用捕获方式处理
     */
    public class FileInputStreamDemo5 {
        public static void main(String[] args) {
            try (
                    // 创建字节输入流对象
                    FileInputStream fis = new FileInputStream("D:\\传智播客\\安装包\\好看的图片\\liqin.jpg");
                    // 创建字节输出流
                    FileOutputStream fos = new FileOutputStream("day11_demo\\copy.jpg")
            ) {
                // 一次读写一个字节
                int by;
                while ((by = fis.read()) != -1) {
                    fos.write(by);
                }
                // 释放资源 , 发现已经灰色 , 提示多余的代码 , 所以使用 try-with-resource 方式会自动关流
                // fis.close();
                // fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

3.4 字节输入流一次读一个字节数组

  • FileInputStream类 :

    • public int read(byte[] b) : 从输入流读取最多b.length个字节的数据, 返回的是真实读到的数据个数
    package com.itheima.inputstream_demo;
    
    
    import javax.sound.midi.Soundbank;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    
    /*
       FileInputStream类 :
            public int read​(byte[] b):
            1 从输入流读取最多b.length个字节的数据
            2 返回的是真实读到的数据个数
     */
    public class FileInputStreamDemo6 {
        public static void main(String[] args) throws IOException {
            // 创建字节输入流对象
            FileInputStream fis = new FileInputStream("day11_demo\\a.txt");
    
    //        public int read​(byte[] b):
    //        1 从输入流读取最多b.length个字节的数据
    //        2 返回的是真实读到的数据个数
    
            byte[] bys = new byte[3];
    
    //        int len = fis.read(bys);
    //        System.out.println(len);// 3
    //        System.out.println(new String(bys));// abc
    //
    //        len = fis.read(bys);
    //        System.out.println(len);// 2
    //        System.out.println(new String(bys));// efc
    
            System.out.println("==========代码改进===============");
    
    //        int len = fis.read(bys);
    //        System.out.println(len);// 3
    //        System.out.println(new String(bys, 0, len));// abc
    //
    //        len = fis.read(bys);
    //        System.out.println(len);// 2
    //        System.out.println(new String(bys, 0, len));// ef
    
            System.out.println("==========代码改进===============");
    
            int len;
            while ((len = fis.read(bys)) != -1) {
                System.out.print(new String(bys , 0 , len));
            }
    
            fis.close();
        }
    }
    
    
  • 对复制图片的代码进行使用一次读写一个字节数组的方式进行改进

    package com.itheima.inputstream_demo;
    
    
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    
    /*
        需求 : 对复制图片的代码进行使用一次读写一个字节数组的方式进行改进
    
        FileInputStream类 :
            public int read​(byte[] b):
            1 从输入流读取最多b.length个字节的数据
            2 返回的是真实读到的数据个数
     */
    public class FileInputStreamDemo7 {
        public static void main(String[] args) throws IOException {
            // 创建字节输入流对象
            FileInputStream fis = new FileInputStream("D:\\传智播客\\安装包\\好看的图片\\liqin.jpg");
            // 创建字节输出流
            FileOutputStream fos = new FileOutputStream("day11_demo\\copy.jpg");
    
            byte[] bys = new byte[1024];
            int len;// 每次真实读到数据的个数
            int by;
            while ((len = fis.read(bys)) != -1) {
                fos.write(bys, 0, len);
            }
    
            // 释放资源
            fis.close();
            fos.close();
        }
    }
    
    

4 字节缓冲区流

4.1 字节缓冲流概述

  • 字节缓冲流:

    • ​ BufferOutputStream:缓冲输出流
    • ​ BufferedInputStream:缓冲输入流
  • 构造方法:

    • ​ 字节缓冲输出流:BufferedOutputStream(OutputStream out)
    • ​ 字节缓冲输入流:BufferedInputStream(InputStream in)
  • 为什么构造方法需要的是字节流,而不是具体的文件或者路径呢?

    • ​ 字节缓冲流仅仅提供缓冲区,不具备读写功能 , 而真正的读写数据还得依靠基本的字节流对象进行操作

4.2 字节缓冲流案例

package com.itheima.bufferedstream_demo;

import java.io.*;

/*
    字节缓冲流:
        BufferOutputStream:缓冲输出流
        BufferedInputStream:缓冲输入流

    构造方法:
        字节缓冲输出流:BufferedOutputStream​(OutputStream out)
        字节缓冲输入流:BufferedInputStream​(InputStream in)

    为什么构造方法需要的是字节流,而不是具体的文件或者路径呢?
        字节缓冲流仅仅提供缓冲区,不具备读写功能 , 而真正的读写数据还得依靠基本的字节流对象进行操作

    需求 : 使用缓冲流进行复制文件

 */
public class BufferedStreamDemo1 {
    public static void main(String[] args) throws IOException {
        // 创建高效的字节输入流对象
        // 在底层会创建一个长度为8192的数组
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\传智播客\\安装包\\好看的图片\\liqin.jpg"));
        // 创建高效的字节输出流
        // 在底层会创建一个长度为8192的数组
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("day11_demo\\copy.jpg"));

        // 使用高效流 , 一次读写一个字节
        int by;
        while ((by = bis.read()) != -1) {
            bos.write(by);
        }

//        byte[] bys = new byte[1024];
//        int len;// 每次真实读到数据的个数
//        while ((len = bis.read(bys)) != -1) {
//            bos.write(bys, 0, len);
//        }

        // 释放资源
        // 在底层会把基本的流进行关闭
        bis.close();
        bos.close();
    }
}

4.3 缓冲流一次读写一个字节原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AUoPbLEg-1678071707226)(\img\image-20210415002851261.png)]

4.4 缓冲流一次读写一个字节数组原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vyItpGAJ-1678071707227)(\img\image-20210415002914607.png)]

  • 四种方式复制视频文件
package com.itheima.bufferedstream_demo;

import java.awt.image.DataBufferDouble;
import java.io.*;

/*
    需求:把“xxx.avi”复制到模块目录下的“copy.avi” , 使用四种复制文件的方式 , 打印所花费的时间

    四种方式:
        1 基本的字节流一次读写一个字节          : 花费的时间为:196662毫秒
        2 基本的字节流一次读写一个字节数组      : 花费的时间为:383毫秒
        3 缓冲流一次读写一个字节                : 花费的时间为:365毫秒
        4 缓冲流一次读写一个字节数组            : 花费的时间为:108毫秒

    分析 :
        数据源 : "D:\a.wmv"
        目的地 : "day11_demo\copy.wmv"
 */
public class BufferedStreamDemo2 {
    public static void main(String[] args) throws IOException {
        long startTime = System.currentTimeMillis();

        // method1();
        // method2();
        // method3();
        method4();

        long endTime = System.currentTimeMillis();
        System.out.println("花费的时间为:" + (endTime - startTime) + "毫秒");
    }

    // 4 缓冲流一次读写一个字节数组
    private static void method4() throws IOException {
        // 创建高效的字节输入流
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\a.wmv"));
        // 创建高效的字节输出流
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("day11_demo\\copy.wmv"));

        // 一次读写一个字节数组
        byte[] bys = new byte[1024];
        int len;// 每次真实读到数据的个数
        while ((len = bis.read(bys)) != -1) {
            bos.write(bys, 0, len);
        }

        // 释放资源
        bis.close();
        bos.close();
    }

    //  3 缓冲流一次读写一个字节
    private static void method3() throws IOException {
        // 创建高效的字节输入流
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\a.wmv"));
        // 创建高效的字节输出流
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("day11_demo\\copy.wmv"));

        // 一次读写一个字节
        int by;
        while ((by = bis.read()) != -1) {
            bos.write(by);
        }


        // 释放资源
        bis.close();
        bos.close();
    }

    // 2 基本的字节流一次读写一个字节数组
    private static void method2() throws IOException {
        // 创建基本的字节输入流对象
        FileInputStream fis = new FileInputStream("D:\\a.wmv");
        // 创建基本的字节输出流对象
        FileOutputStream fos = new FileOutputStream("day11_demo\\copy.wmv");

        // 一次读写一个字节数组
        byte[] bys = new byte[1024];
        int len;// 每次真实读到数据的个数
        while ((len = fis.read(bys)) != -1) {
            fos.write(bys, 0, len);
        }

        // 释放资源
        fis.close();
        fos.close();
    }

    // 1 基本的字节流一次读写一个字节
    private static void method1() throws IOException {
        // 创建基本的字节输入流对象
        FileInputStream fis = new FileInputStream("D:\\a.wmv");
        // 创建基本的字节输出流对象
        FileOutputStream fos = new FileOutputStream("day11_demo\\copy.wmv");

        // 一次读写一个字节
        int by;
        while ((by = fis.read()) != -1) {
            fos.write(by);
        }

        // 释放资源
        fis.close();
        fos.close();
    }
}

5.1 Properties集合的概述

  • properties是一个Map体系的集合类

    • public class Properties extends Hashtable <Object,Object>
  • 为什么在IO流部分学习Properties

    • Properties中有跟IO相关的方法
  • 当做双列集合使用

    • 不需要加泛型 , 工作中只存字符串
    package com.itheima.properties_demo;
    
    import java.util.Map;
    import java.util.Properties;
    import java.util.Set;
    
    /*
        1 properties是一个Map体系的集合类
            - `public class Properties extends Hashtable <Object,Object>`
        2 为什么在IO流部分学习Properties
            - Properties中有跟IO相关的方法
        3 当做双列集合使用
            - 不需要加泛型 , 工作中只存字符串
     */
    public class PropertiesDemo1 {
        public static void main(String[] args) {
            // 创建集合对象
            Properties properties = new Properties();
    
            // 添加元素
            properties.put("it001" , "张三");
            properties.put("it002" , "李四");
            properties.put("it003" , "王五");
    
            // 遍历集合 : 键找值
            Set<Object> set = properties.keySet();
            for (Object key : set) {
                System.out.println(key + "---" + properties.get(key));
            }
    
            System.out.println("========================");
            
            // 遍历集合 : 获取对对象集合 , 获取键和值
            Set<Map.Entry<Object, Object>> set2 = properties.entrySet();
            for (Map.Entry<Object, Object> entry : set2) {
                Object key = entry.getKey();
                Object value = entry.getValue();
                System.out.println(key + "---" + value);
            }
        }
    }
    

5.2 Properties作为集合的特有方法

  • Object setProperty(String key, String value) 设置集合的键和值,都是String类型,相当于put方法
  • String getProperty(String key) 使用此属性列表中指定的键搜索属性 , 相当于get方法
  • Set stringPropertyNames() 从该属性列表中返回一个不可修改的键集,其中键及其对应的值是字符串 , 相当于keySet方法
package com.itheima.properties_demo;

import java.util.Properties;
import java.util.Set;

/*
    Properties作为集合的特有方法
        Object setProperty(String key, String value)	设置集合的键和值,都是String类型,相当于put方法
        String getProperty(String key)	使用此属性列表中指定的键搜索属性 , 相当于get方法
        Set<String> stringPropertyNames​()	从该属性列表中返回一个不可修改的键集,其中键及其对应的值是字符串 , 相当于keySet方法
 */
public class PropertiesDemo2 {

    public static void main(String[] args) {
        // 创建集合对象
        Properties properties = new Properties();

        // 添加元素
        properties.setProperty("it001", "张三");
        properties.setProperty("it002", "李四");
        properties.setProperty("it003", "王五");

        // 遍历集合 : 键找值
        Set<String> set = properties.stringPropertyNames();
        for (String key : set) {
            System.out.println(key + "---" + properties.getProperty(key));
        }
    }
}

5.3 properties中和IO相关的方法

  • void load(InputStream inStream) 以字节流形式 , 把文件中的键值对, 读取到集合中
  • void load(Reader reader) 以字符流形式 , 把文件中的键值对, 读取到集合中
  • void store(OutputStream out, String comments) 把集合中的键值对,以字节流形式写入文件中 , 参数二为注释
  • void store(Writer writer, String comments) 把集合中的键值对,以字符流形式写入文件中 , 参数二为注释
package com.itheima.properties_demo;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

/*
   Properties和IO流结合的方法
        void load​(InputStream inStream)	以字节流形式 , 把文件中的键值对, 读取到集合中
        //void load​(Reader reader)	以字符流形式 , 把文件中的键值对, 读取到集合中
        void store​(OutputStream out, String comments)	把集合中的键值对,以字节流形式写入文件中 , 参数二为注释
        //void store​(Writer writer, String comments)	把集合中的键值对,以字符流形式写入文件中 , 参数二为注释
 */
public class PropertiesDemo3 {
    public static void main(String[] args) throws IOException {
        // 创建Properties集合对象
        Properties properties = new Properties();

        // void load​(InputStream inStream)	以字节流形式 , 把文件中的键值对, 读取到集合中
        properties.load(new FileInputStream("day11_demo\\prop.properties"));

        // 打印集合中的数据
        System.out.println(properties);
    }
}

package com.itheima.properties_demo;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;

/*
   Properties和IO流结合的方法
        void load​(InputStream inStream)	以字节流形式 , 把文件中的键值对, 读取到集合中
        //void load​(Reader reader)	以字符流形式 , 把文件中的键值对, 读取到集合中
        void store​(OutputStream out, String comments)	把集合中的键值对,以字节流形式写入文件中 , 参数二为注释
        //void store​(Writer writer, String comments)	把集合中的键值对,以字符流形式写入文件中 , 参数二为注释
 */
public class PropertiesDemo3 {
    public static void main(String[] args) throws IOException {
        Properties properties = new Properties();
        properties.setProperty("zhangsan" , "23");
        properties.setProperty("lisi" , "24");
        properties.setProperty("wangwu" , "25");
        properties.store(new FileOutputStream("day11_demo\\prop2.properties") , "userMessage");
    }

    private static void method1() throws IOException {
        // 创建Properties集合对象
        Properties properties = new Properties();

        // void load​(InputStream inStream)	以字节流形式 , 把文件中的键值对, 读取到集合中
        properties.load(new FileInputStream("day11_demo\\prop.properties"));

        // 打印集合中的数据
        System.out.println(properties);
    }
}

6 ResourceBundle加载属性文件

学习目标
  • 能够熟练使用ResourceBundle工具类快速读取属性文件的值
内容讲解
【1】API介绍

java.util.ResourceBundle它是一个抽象类,我们可以使用它的子类PropertyResourceBundle来读取以.properties结尾的配置文件。

通过静态方法直接获取对象:
static ResourceBundle getBundle(String baseName) 可以根据名字直接获取默认语言环境下的属性资源。
参数注意: baseName 
  	1.属性集名称不含扩展名。
    2.属性集文件是在src目录中的
  
比如:src中存在一个文件 user.properties
ResourceBundle bundle = ResourceBundle.getBundle("user");

ResourceBundle中常用方法:

 String getString(String key) : 通过键,获取对应的值
【2】代码实践

通过ResourceBundle工具类

将一个属性文件 放到src目录中,使用ResourceBundle去获取键值对数据

package com.itheima.resourcebundle_demo;

import java.util.ResourceBundle;

/*
    1   java.util.ResourceBundle : 它是一个抽象类
        我们可以使用它的子类PropertyResourceBundle来读取以.properties结尾的配置文件

    2   static ResourceBundle getBundle(String baseName) 可以根据名字直接获取默认语言环境下的属性资源。
        参数注意: baseName
            1.属性集名称不含扩展名。
            2.属性集文件是在src目录中的
        比如:src中存在一个文件 user.properties
        ResourceBundle bundle = ResourceBundle.getBundle("user");

    3 ResourceBundle中常用方法:
         String getString(String key) : 通过键,获取对应的值

    优点 : 快速读取属性文件的值

     需求 :
        通过ResourceBundle工具类
        将一个属性文件 放到src目录中,使用ResourceBundle去获取键值对数据
 */
public class ResourceBundleDemo {
    public static void main(String[] args) {
        // 获取ResourceBundle类的对象
        ResourceBundle resourceBundle = ResourceBundle.getBundle("user");

        //  String getString(String key) : 通过键,获取对应的值
        String username = resourceBundle.getString("username");
        System.out.println(username);
        String password = resourceBundle.getString("password");
        System.out.println(password);

    }
}
内容小结
  1. 如果要使用ResourceBundle加载属性文件,属性文件需要放置在哪个位置?

    src的根目录
    
  2. 请描述使用ResourceBundle获取属性值的大致步骤是怎样的?

    1 获取ResourceBundle对象
    2 通过ResourceBundle类中的getString(key) : 根据键找值
    

    五、编码表,字符流,对象流,其他流

今日目标

  • 编码表
  • 字符输出流
  • 字符输入流
  • 字符缓冲流
  • 转换流
  • 对象操作流
  • 打印流
  • 装饰模式
  • commons-iojar包

1 编码表

1.1 思考:

  • 既然字节流可以操作所有文件,那么为什么还要学习字符流 ?
    • 如果使用字节流 , 把文本文件中的内容读取到内存时, 可能会出现乱码
    • 如果使用字节流 , 把中文写入文本文件中 , 也有可能会出现乱码

1.2 编码表介绍

  • 计算机中储存的信息都是用二进制数据表示的;我们在屏幕上看到的英文、汉字等字符是二进制数转换之后的结果

  • 按照编码表规则,将字符存储到计算机中,称为编码。

  • 按照同样的编码表规则,将存储在计算机中的二进制数据解析显示出来,称为解码 。

  • 编码和解码使用的码表必须一致,否则会导致乱码。

  • 简单理解:
    • 存储一个字符a,首先需在码表中查到对应的数字是97,然后按照转换成二进制的规则进行存储。称为编码
    • 读取的时候,先把二进制解析出来,再转成97,通过97查找早码表中对应的字符是a。称为解码
  • ASCII码表:
    • ASCII(American Standard Code for Information Interchange,美国信息交换标准码表):
    • 包括了数字字符,英文大小写字符和一些常见的标点符号字符。
    • 注意:ASCII码表中是没有中文的。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SnSWdZCc-1678072096693)(C:\Users\Ford\AppData\Roaming\Typora\typora-user-images\image-20230306110632819.png)]

  • GBK码表:
    • window系统默认的码表。兼容ASCII码表,也包含了21003个汉字,并支持繁体汉字以及部分日韩文字
    • 注意:GBK是中国的码表,一个中文以两个字节的形式存储。但不包含世界上所有国家的文字。
  • Unicode码表:
    • 由国际组织ISO 制定,是统一的万国码表,计算机科学领域里的一项业界标准,容纳世界上大多数国家的所有常见文字和符号。
    • 但是因为表示的字符太多,所以Unicode码表中的数字不是直接以二进制的形式存储到计算机的,会先通过UTF-7,UTF-7.5,UTF-8,UTF-16,以及 UTF-32的编码方式再存储到计算机,其中最为常见的就是UTF-8。
    • 注意: Unicode是万国码表,以UTF-8编码后一个中文以三个字节的形式存储

1.3 编码表小结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qrnxTJJk-1678072096693)(C:\Users\Ford\AppData\Roaming\Typora\typora-user-images\image-20230306110531519.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TkugVe0N-1678072096694)(C:\Users\Ford\AppData\Roaming\Typora\typora-user-images\image-20230306110546745.png)]

1.4 字节流读中文出现乱码的原码

  • 因为字节流一次读一个字节,而不管GBK还是UTF-8一个中文都是多个字节,用字节流每次只能读其中的一部分,所以就会出现乱码问题

2 字符输出流

2.1 字符流输出介绍

  • Writer类
    • 写入字符流的最顶层的类 , 是一个抽象类 ,不能实例化 , 需要使用其子类FileWriter类
  • FileWriter类 : 用来写入字符文件的便捷类

2.2 FileWriter的成员

  • 构造方法 :

    • public FileWriter(File file) : 往指定的File路径中写入数据
    • public FileWriter(String fileName) : 往指定的String路径中写入数据
  • 成员方法 :

    • void write(int c)写一个字符
      void write(char[] cbuf)写入一个字符数组
      void write(char[] cbuf, int off, int len)写入字符数组的一部分
      void write(String str)写一个字符串
      void write(String str, int off, int len)写一个字符串的一部分
    • flush()刷新流,还可以继续写数据
      close()关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据

2.3 FileWriter写数据的步骤

  • 1 创建字符输出流对象
    • 注意事项:
      如果文件不存在,就创建。但是要保证父级路径存在。
      如果文件存在就清空
  • 2 写数据
    • 注意事项:
      写出int类型的整数,实际写出的是整数在码表上对应的字母。
      写出字符串数据,是把字符串本身原样写出。
  • 3 释放资源
    • 注意事项:
      每次使用完流必须要释放资源。
  • package com.itheima.writer_demo;
    
    import java.io.FileWriter;
    import java.io.IOException;
    
    /*
        Writer类 : 写入字符流的最顶层的类 , 是一个抽象类 ,不能实例化
        需要使用其子类FileWriter类
    
        FileWriter类 : 用来写入字符文件的便捷类
        构造方法 :
            public FileWriter(File file) : 往指定的File路径中写入数据
            public FileWriter(String fileName) : 往指定的String路径中写入数据
        成员方法
            void write​(int c)	写一个字符
            void write​(char[] cbuf)	写入一个字符数组
            void write​(char[] cbuf, int off, int len)	写入字符数组的一部分
            void write​(String str)	写一个字符串
            void write​(String str, int off, int len)	写一个字符串的一部分
            flush()	刷新流,还可以继续写数据
            close()	关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据
     */
    public class WriterDemo1 {
        public static void main(String[] args) throws IOException {
            // 创建字符输出流对象
            // 如果文件不存在会创建一个空的文件
            // 如果文件存在 , 会把文件中的内容清空
            FileWriter fw = new FileWriter("day12_demo\\charstream2.txt");
    
            // 写数据
            fw.write('a');
            fw.write('b');
            // 刷新流 , 把流中的数据刷到硬盘中 , 刷新之后可以继续写数据
            // fw.flush();
    
            // 释放资源
            // 关闭流 , 但是会先刷新流
            fw.close();
            // 一旦关闭流无法写数据
            // fw.write('c');
        }
    }
    
    
    package com.itheima.writer_demo;
    
    import java.io.FileWriter;
    import java.io.IOException;
    
    /*
        Writer类 : 写入字符流的最顶层的类 , 是一个抽象类 ,不能实例化
        需要使用其子类FileWriter类
    
        FileWriter类 : 用来写入字符文件的便捷类
        构造方法 :
            public FileWriter(File file) : 往指定的File路径中写入数据
            public FileWriter(String fileName) : 往指定的String路径中写入数据
        成员方法
            void write​(int c)	写一个字符
            void write​(char[] cbuf)	写入一个字符数组
            void write​(char[] cbuf, int off, int len)	写入字符数组的一部分
            void write​(String str)	写一个字符串
            void write​(String str, int off, int len)	写一个字符串的一部分
            flush()	刷新流,还可以继续写数据
            close()	关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据
     */
    public class WriterDemo2 {
        public static void main(String[] args) throws IOException {
            // 创建字符输出流对象
            FileWriter fw = new FileWriter("day12_demo\\charstream2.txt");
    
            // 写数据
    //        void write​(int c)	写一个字符
    //        fw.write('a');
    //        fw.write('b');
    //        fw.write('c');
    
    //        void write​(char[] cbuf)	写入一个字符数组
            char[] chs = {'a', 'b', 'c', 'd', 'e'};
    //        fw.write(chs);
    
    //        void write​(char[] cbuf, int off, int len)	写入字符数组的一部分
    //        fw.write(chs , 2 , 3);
    
    //        void write​(String str)	写一个字符串
    //        fw.write("abcadaasda");
    
    //        void write​(String str, int off, int len)	写一个字符串的一部分
    //        fw.write("abnacna", 3, 2);
    
            // 释放资源
            fw.close();
        }
    }
    
    

2.4 字符输出流练习

  • package com.itheima.writer_demo;
    
    import java.io.FileWriter;
    import java.io.IOException;
    import java.util.Scanner;
    
    /*
        需求 : 将用户键盘录入的用户名和密码保存到本地实现永久化存储。
        要求 : 用户名和密码在文件中各占一行
    
        步骤:
            1 用户键盘录入用户名
            2 创建字符输出流对象
            3 将用户名和密码写到本地文件中
     */
    public class WriterTest {
        public static void main(String[] args) throws IOException {
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入用户名:");
            String username = sc.nextLine();
    
            System.out.println("请输入密码:");
            String password = sc.nextLine();
    
            // 创建字符输出流对象
            FileWriter fw = new FileWriter("day12_demo\\user.txt");
            // 往文件中写入用户名和密码
            fw.write(username);
            // 换行
            fw.write("\r\n");
            fw.write(password);
            // 刷新
            fw.flush();
            // 释放资源
            fw.close();
        }
    }
    
    

3 字符输入流

3.1 字节输入流介绍

  • Reader类 :
    • 读取字符流的最顶层的类 , 是一个抽象类 ,不能实例化
    • 需要使用其子类FileReader类
  • FileReader类 :
    • 用来读取字符文件的便捷类

3.2 FileReader的成员

  • 构造方法 :

    • public FileReader(File file) : 从指定的File路径中读取数据
    • public FileReader(String fileName) : 从指定的String路径中读取数据
  • 成员方法 :

    • int read()一次读一个字符数据
      int read(char[] cbuf)一次读一个字符数组数据
  • package com.itheima.reader_demo;
    
    import java.io.FileNotFoundException;
    import java.io.FileReader;
    import java.io.IOException;
    
    /*
        Reader类 : 读取字符流的最顶层的类 , 是一个抽象类 ,不能实例化
        需要使用其子类FileReader类
    
        FileReader类 : 用来读取字符文件的便捷类
    
        构造方法 :
            public FileReader(File file) : 从指定的File路径中读取数据
            public FileReader(String fileName) : 从指定的String路径中读取数据
    
        成员方法 :
            int read​() : 一次读一个字符数据
            int read​(char[] cbuf)	 : 一次读一个字符数组数据
     */
    public class ReaderDemo1 {
        public static void main(String[] args) throws IOException {
            // 创建字符输入流对象
            FileReader fr = new FileReader("day12_demo\\charstream.txt");
    
            // 一次读一个字符数据
            int ch;
            while ((ch = fr.read()) != -1) {
                System.out.print((char) ch);
            }
    
            // 释放资源
            fr.close();
        }
    }
    
    
  • package com.itheima.reader_demo;
    
    import java.io.FileNotFoundException;
    import java.io.FileReader;
    import java.io.IOException;
    
    /*
        Reader类 : 读取字符流的最顶层的类 , 是一个抽象类 ,不能实例化
        需要使用其子类FileReader类
    
        FileReader类 : 用来读取字符文件的便捷类
    
        构造方法 :
            public FileReader(File file) : 从指定的File路径中读取数据
            public FileReader(String fileName) : 从指定的String路径中读取数据
    
        成员方法 :
            int read​()	一次读一个字符数据
            int read​(char[] cbuf)	一次读一个字符数组数据
     */
    public class ReaderDemo2 {
        public static void main(String[] args) throws IOException {
            // 创建字符输入流对象
            FileReader fr = new FileReader("day12_demo\\charstream.txt");
    
            // 一次读一个字符数组数据
            char[] chs = new char[1024];
            int len;
            while ((len = fr.read(chs)) != -1) {
                System.out.println(new String(chs, 0, len));
            }
            // 释放资源
            fr.close();
        }
    }
    
    

4 字符缓冲流

4.1 字符缓冲流

  • BufferedWriter:可以将数据高效的写出
  • BufferedReader:可以将数据高效的读入到内存
  • 注意 : 字符缓冲流不具备读写功能 , 只提供缓冲区 , 真正读写还是需要依赖于构造接收的基本的字符流
  • 构造方法:
    • public BufferedWriter(Writer out) : 构造方法中需要接收一个基本的字符输出流
    • public BufferedReader(Reader in) : 构造方法中需要接收一个基本的字符输入流
package com.itheima.bufferedstream_demo;

import java.io.*;

/*
    需求 : 使用字符缓冲流复制纯文本文件
    将当日课程资料中的 ‘斗罗大陆.txt’ 复制到当前模块下 'copy.txt'
 */
public class BufferedStreamDemo1 {
    public static void main(String[] args) throws IOException {
        // 创建高效的字符输入流对象
        BufferedReader br = new BufferedReader(new FileReader("D:\\传智播客\\上海-JavaSE进阶面授\\day12【缓冲流、转换流、序列化流、装饰者模式、commons-io工具包】\\资料\\斗罗大陆.txt"));
        // 创建高效的字符输出流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("day12_demo\\copy.txt"));

        // 一次读写一个字符
//        int ch;
//        while ((ch = br.read()) != -1) {
//            bw.write(ch);
//        }

        // 一次读写一个字符数组
        char[] chs = new char[1024];
        int len;
        while ((len = br.read(chs)) != -1) {
            bw.write(chs, 0, len);
        }
        // 释放资源
        br.close();
        bw.close();
    }
}

4.2 字符缓冲流特有的功能

  • BufferedWriter类
    • void newLine():写一个行分隔符,会根据操作系统的不同,写入不同的行分隔符
  • BufferedReader类
    • public String readLine() :读取文件一行数据, 不包含换行符号 , 读到文件的末尾返回null
  • package com.itheima.bufferedstream_demo;
    
    import java.io.*;
    
    /*
        1 字符缓冲流:
            BufferedWriter:可以将数据高效的写出
            BufferedReader:可以将数据高效的读入到内存
    
        2 字符缓冲流特有功能
            BufferedWriter类
                void newLine​():写一个行分隔符,会根据操作系统的不同,写入不同的行分隔符
            BufferedReader类
                public String readLine​() :读取文件一行数据, 不包含换行符号 ,  读到文件的末尾返回null
    
            远桥之下泛莲舟
            岱岩石上松溪流
            万仞翠山梨亭在
            莫闻空谷声悠悠
     */
    public class BufferedStreamDemo2 {
        public static void main(String[] args) throws IOException {
    
            // 创建高效的字符输出流对象
            BufferedWriter bw = new BufferedWriter(new FileWriter("day12_demo\\abc.txt"));
            // void newLine​():写一个行分隔符,会根据操作系统的不同,写入不同的行分隔符
            bw.write("远桥之下泛莲舟");
            bw.newLine();
            bw.write("岱岩石上松溪流");
            bw.newLine();
            bw.write("万仞翠山梨亭在");
            bw.newLine();
            bw.write("莫闻空谷声悠悠");
            bw.flush();
    
            // 创建高效的字符输入流对象
            BufferedReader br = new BufferedReader(new FileReader("day12_demo\\abc.txt"));
            // public String readLine​() :读取文件一行数据, 不包含换行符号 ,  读到文件的末尾返回null
    //        String s = br.readLine();
    //        System.out.println(s);
    //        s = br.readLine();
    //        System.out.println(s);
    //        s = br.readLine();
    //        System.out.println(s);
    //        s = br.readLine();
    //        System.out.println(s);
    //        System.out.println("============");
    //        s = br.readLine();
    //        System.out.println(s);
    //        s = br.readLine();
    //        System.out.println(s);
    
            // 循环改进
            String line;
            while((line = br.readLine()) != null){
                System.out.println(line);
            }
    
            // 释放资源
            br.close();
            bw.close();
        }
    }
    
    

4.3 字符缓冲流练习

package com.itheima.bufferedstream_demo;

import java.io.*;
import java.util.Arrays;

/*
    需求:读取文件中的数据 : 33 22 11 55 44
    排序后 : 11 22 33 44 55  再次写到本地文件

    步骤 :
        1 创建高效的字符输入流对象
        2 读取文件中的一行数据
        3 将数据按照空格切割
        4 把字符串数组转成int类型数组
        5 对int类型的数组进行排序
        6 创建高效的字符输出流对象
        7 遍历数组,把数组中的数据写入到文件中
        8 释放资源
 */
public class BufferedStreamDemo3 {
    public static void main(String[] args) throws IOException {
        // 1 创建高效的字符输入流对象
        BufferedReader br = new BufferedReader(new FileReader("day12_demo\\sort.txt"));
        // 2 读取文件中的一行数据
        String line = br.readLine();
        // 3 将数据按照空格切割
        String[] strs = line.split(" ");
        // 4 把字符串数组转成int类型数组
        int[] arr = new int[strs.length];
        for (int i = 0; i < strs.length; i++) {
            arr[i] = Integer.parseInt(strs[i]);
        }
        // 5 对int类型的数组进行排序
        Arrays.sort(arr);
        // 6 创建高效的字符输出流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("day12_demo\\sort.txt"));
        // 7 遍历数组,把数组写入到文件中
        for (int i = 0; i < arr.length; i++) {
            bw.write(arr[i] + " ");
            bw.flush();
        }
        // 8 释放资源
        br.close();
        bw.close();
    }
}

5 转换流

5.1 转换流介绍

  • 转换流就是来进行字节流和字符流之间转换的桥梁

5.2 转换流分类

  • InputStreamReader是从字节流到字符流的桥梁

    • public InputStreamReader(InputStream in) : 创建一个使用默认编码的 InputStreamReader。
    • public InputStreamReader(InputStream in , String charsetName) : 创建使用指定编码的 InputStreamReader。
  • OutputStreamWriter是从字符流到字节流的桥梁

    • public OutputStreamWriter(OutputStream out) : 创建使用默认字符编码的 OutputStreamWriter
    • public OutputStreamWriter(OutputStream out, String charsetName) : 创建使用指定编码的 OutputStreamWriter。
  • 练习
    package com.itheima.conversion_demo;
    
    import java.io.*;
    
    /*
        转换流就是来进行字节流和字符流之间转换的桥梁
    
        InputStreamReader是从字节流到字符流的桥梁
            public InputStreamReader(InputStream in) : 创建一个使用默认编码的 InputStreamReader。
            public InputStreamReader(InputStream in ,  String charsetName) : 创建使用指定编码的 InputStreamReader。
    
        OutputStreamWriter是从字符流到字节流的桥梁
            public OutputStreamWriter(OutputStream out) : 创建使用默认字符编码的 OutputStreamWriter
            public OutputStreamWriter(OutputStream out,  String charsetName) : 创建使用指定编码的 OutputStreamWriter。
    
    
        需求1 : 使用转换流 , 把以下数据按照GBK的编码写入文件 , 在使用GBK的编码读取数据
        数据如下 :
            远桥之下泛莲舟
            岱岩石上松溪流
            万仞翠山梨亭在
            莫闻空谷声悠悠
     */
    public class ConversionDemo2 {
        public static void main(String[] args) throws IOException {
            // 创建转换输出流
            OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("day12_demo\\conversion.txt"), "GBK");
            osw.write("远桥之下泛莲舟");
            osw.write("\r\n");
            osw.write("岱岩石上松溪流");
            osw.write("\r\n");
            osw.write("万仞翠山梨亭在");
            osw.write("\r\n");
            osw.write("莫闻空谷声悠悠");
            osw.write("\r\n");
            osw.close();
    
            // 创建转换输入流
            InputStreamReader isr = new InputStreamReader(new FileInputStream("day12_demo\\conversion.txt"), "GBK");
            int ch;
            while ((ch = isr.read()) != -1) {
                System.out.print((char) ch);
            }
            isr.close();
        }
    }
    
    
    package com.itheima.conversion_demo;
    
    import java.io.*;
    
    /*
        转换流就是来进行字节流和字符流之间转换的桥梁
    
        InputStreamReader是从字节流到字符流的桥梁
            public InputStreamReader(InputStream in) : 创建一个使用默认编码的 InputStreamReader。
            public InputStreamReader(InputStream in ,  String charsetName) : 创建使用指定编码的 InputStreamReader。
    
        OutputStreamWriter是从字符流到字节流的桥梁
            public OutputStreamWriter(OutputStream out) : 创建使用默认字符编码的 OutputStreamWriter
            public OutputStreamWriter(OutputStream out,  String charsetName) : 创建使用指定编码的 OutputStreamWriter。
    
        需求2 :  将模块根目录中GBK编码的文本文件 , 转换为UTF-8编码的文本文件
     */
    public class ConversionDemo2 {
        public static void main(String[] args) throws IOException {
    
            // 创建转换输入流
            InputStreamReader isr = new InputStreamReader(new FileInputStream("day12_demo\\GBK编码的文件.txt"), "GBK");
    
            // 创建转换输出流
            OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("day12_demo\\UTF编码的文件.txt"), "UTF-8");
    
            int ch;
            while ((ch = isr.read()) != -1) {// 以GBK编码进去读取
                osw.write(ch);// 以UTF-8编码进行写入
            }
    
            // 释放资源
            isr.close();
            osw.close();
        }
    }
    
    

6 对象操作流

6.1 对象操作流介绍

  • 可以把对象以字节的形式写到本地文件,直接打开文件,是读不懂的,需要再次用对象操作流读到内存中

6.2 对象操作流的分类

  • ObjectOutputStream :
    • ​ 对象操作输出流(对象序列化流):就是将对象写到本地文件中,或者在网络中传输对象
  • ObjectInputStream :
    • ​ 对象操作输入流(对象反序列化流):把写到本地文件中的对象读到内存中,或者接收网络中传输的对象

6.3 对象操作流的注意事项

  • 注意 : 如果一个类对象想要被序列化 , 那么此类需要实现Serializable接口

    • Serializable接口的含义 :
      • ​ 1 是一个标记性接口 , 里面没有任何抽象方法
      • ​ 2 只要一个类实现了此接口 , 表示此类的对象可以被序列化
  • 用对象序列化流序列化了一个对象后,假如我们修改了对象所属的Javabean类,读取数据会不会出问题呢?

    • 会出问题,会抛出InvalidClassException异常
  • 如果出问题了,如何解决呢?

    • 给对象所属的类加一个serialVersionUID
    • private static final long serialVersionUID = 42L;
  • 如果一个对象中的某个成员变量的值不想被序列化,又该如何实现呢?

  • 给该成员变量加transient关键字修饰,该关键字标记的成员变量不参与序列化过程

  • package com.itheima.objectstream_demo;
    
    
    import java.io.Serializable;
    
    /*
        如果此类对象想要被序列化 , 那么此类需要实现Serializable接口
        Serializable接口的含义 :
            是一个标记性接口 , 里面没有任何抽象方法
            只要一个类实现了此接口 , 表示此类的对象可以被序列化
     */
    public class User implements Serializable {
        /*
            问题分析 :
                serialVersionUID : 序列号
                序列号是根据类的信息进行生成的
                如果没有自己给出序列号 , JVM会根据类的信息自动计算一个序列号
                如果改动了类的信息 , 那么JVM会重新计算一个序列号
    
                第一步 : 把对象序列化到本地中 , 序列号为 -4446663370728791812 也会存储到本地中
                第二步 : 我们自己修改了类 , 会重新计算一个新的序列号 2908680347500030933
                第三步 : 当把对象读到内存中时 , 本地中的序列号和类中的序列号不一致就会发生 InvalidClassException异常
    
            解决方案 :
                我们自己手动给出序列号, 不让虚拟机自动生成 , 并且这个值恒久不变
                private static final long serialVersionUID = 值L;
         */
    
        private static final long serialVersionUID = 111L;
    
        private String username;
        private transient String password;
    
        public User() {
        }
    
        public User(String username, String password) {
            this.username = username;
            this.password = password;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }
    }
    

6.4 对象操作流的练习

7 打印流

学习目标
  • 了解打印流的使用
内容讲解
【1】API

PrintStream类 打印

作用 : 就是为了方便记录数据(日志)

1)构造方法

public PrintStream(String filePath) : 构建一个打印流对象,传入接收数据的文件路径

2)方法

public void println(数据)   打印后换行
public void print(数据)     打印不换行
【2】代码实践

创建打印流对象,记录一些数据到指定文件

package com.itheima.printstream_demo;

import java.io.FileNotFoundException;
import java.io.PrintStream;

/*
    PrintStream类 : 打印流

    构造方法 :
        public PrintStream(String filePath) : 构建一个打印流对象,传入接收数据的文件路径
    成员方法 :
        public void println(数据)   打印后换行
        public void print(数据)     打印不换行

     需求 : 创建打印流对象,记录一些数据到指定文件
 */
public class PrintStreamDemo {
    public static void main(String[] args) throws FileNotFoundException {
        // public PrintStream(String filePath) : 构建一个打印流对象,传入接收数据的文件路径
        PrintStream printStream = new PrintStream("day12_demo\\print.txt");
        // public void println(数据)   打印后换行
        printStream.println(123);
        printStream.println("abc");
        printStream.println(true);
        printStream.println(new Object());

        printStream.close();
    }
}

System.out 对象类型,其实就是PrintStream类型。

可以改变系统输出的流向:System.setOut(打印流)


内容小结
  • 打印流使用很简单,print/println方法
  • 作用:就是为了方便记录数据(日志)

8 装饰设计模式

  • 设计模式 : 一套良好的编码风格 , 经过众多的开发人员不断的测试总结而来
学习目标
  • 熟悉装饰设计模式的使用
内容讲解
【1】概述
  • 装饰模式指的是在不改变原类, 不使用继承的基础上,动态地扩展一个对象的功能。

  • 不使用继承技术扩展功能, 可以降低耦合

  • 使用原则:
    • 装饰类和被装饰类需要有共同的父类型。
      • 在之前学习过的 BufferedWriter 和 FileWriter 就是装饰设计模式
      • BufferedWriter的父类为Writer
      • FileWriter的父类也是Writer
      • 我们把FileWriter的对象传递到BufferedWriter的构造中 , 那么可以理解为BufferedWriter是装饰类 , FileWriter是被装饰类
      • BufferedWriter对FileWriter的功能做了增强
    • 装饰类的构造要接收被装饰类的对象
      • FileWriter fw = new FileWriter(“路径”);
      • BufferedWriter bw = new BufferedWriter(fw);
    • 在装饰类中把要增强扩展的功能进行扩展
      • BufferedWriter和FileWriter的功能一样, 都具备Writer中写数据的功能
      • 但是BufferedWriter提供了缓冲区 , 相当于对FileWriter功能做了扩展
    • 对于不要增强的功能直接调用
      • 不需要增强的功能直接继承父类的即可
【2】代码实践

已知有接口Star和其子类型LiuDeHua。

public interface Star {
    public abstract void sing();
    public abstract void dance();
}

需求 :在不改变LiuDeHua类,及不使用继承的技术前提下,动态的扩展LiuDeHua的sing功能。

LiuDeHua就是一个被装饰类 , 需要对唱歌的功能进行扩展

思路 :

定义一个装饰类,去装饰增强 LiuDehua类。

步骤:

  • 创建LiuDeHua类并实现接口Star【被装饰类】

  • 定义一个装饰类LiuDeHuaWrapper实现Star 【装饰类】

  • 在装饰类里面定义一个成员变量类型是LiuDeHua,可以使用构造方法进行传入被装饰类对象。

  • 在装饰类中对sing方法进行功能扩展

  • 对dance不做改动

  • 测试类分别创建装饰类的对象和被装饰类的对象。将被装饰类对象刘德华对象设置给装饰类对象

package com.itheima.design_demo;

/*
    装饰模式指的是在不改变原类, 不使用继承的基础上,动态地扩展一个对象的功能

    使用原则 :
        1. 装饰类和被装饰类需要有共同的父类型。
        2. 装饰类要传入被装饰类的对象
        3. 在装饰类中把要增强扩展的功能进行扩展
        4. 对于不要增强的功能直接调用


    需求 : 在不改变LiuDeHua类,及不使用继承的技术前提下,动态的扩展LiuDeHua的sing功能。
            LiuDeHua就是一个被装饰类 , 需要对唱歌的功能进行扩展
    步骤:
        1. 创建LiuDeHua类并实现接口Star【被装饰类】
        2. 定义一个装饰类LiuDeHuaWrapper实现Star 【装饰类】
        3. 在装饰类里面定义一个成员变量类型是LiuDeHua,可以使用构造方法进行传入被装饰类对象。
        4. 在装饰类中对sing方法进行功能扩展
        5. 对dance不做改动
        6. 测试类分别创建装饰类的对象和被装饰类的对象。将被装饰类对象刘德华对象设置给装饰类对象
 */
public class Test {
    public static void main(String[] args) {
        //  6. 测试类分别创建装饰类的对象和被装饰类的对象。将被装饰类对象刘德华对象设置给装饰类对象

        // 创建被装饰的类对象
        LiuDeHua huaZai = new LiuDeHua();// 0x001
        // 创建装饰类对象
        LiuDeHuaWrapper liuDeHuaWrapper = new LiuDeHuaWrapper(huaZai);

        liuDeHuaWrapper.sing();
        liuDeHuaWrapper.dance();
    }
}

// 明星接口 , 装饰类和被装饰类的父类型
interface Star {
    public abstract void sing(); // 唱歌

    public abstract void dance();// 跳舞
}

// 1. 创建LiuDeHua类并实现接口Star【被装饰类】
class LiuDeHua implements Star {

    @Override
    public void sing() {
        System.out.println("唱忘情水....");
    }

    @Override
    public void dance() {
        System.out.println("华仔在跳老年迪斯高...");
    }
}

// 2. 定义一个装饰类LiuDeHuaWrapper实现Star 【装饰类】
class LiuDeHuaWrapper implements Star {
    // 3. 在装饰类里面定义一个成员变量类型是LiuDeHua,可以使用构造方法进行传入被装饰类对象
    private LiuDeHua huaZai;// 0x001

    public LiuDeHuaWrapper(LiuDeHua huaZai) {// 0x001
        this.huaZai = huaZai;
    }

    // 4. 在装饰类中对sing方法进行功能扩展
    @Override
    public void sing() {
        System.out.print("华仔正在深情的");
        huaZai.sing();
    }

    // 5. 对dance不做改动
    @Override
    public void dance() {
        huaZai.dance();
    }
}
内容小结
  1. 装饰类和被装饰类需要有共同的父类型。
  2. 装饰类要传入被装饰类的对象
  3. 在装饰类中把要增强扩展的功能进行扩展
  4. 对于不要增强的功能直接调用

9 commons-io工具包(对文件的拷贝做优化)

学习目标
  • 能够熟悉导入commons-io工具包,并使用
内容讲解
【1】三方库的导入
  1. 下载commons-io相关jar包;http://commons.apache.org/proper/commons-io/

  2. 把commons-io-2.6.jar包复制到指定的Module的lib目录中

  3. 将commons-io-2.6.jar加入到项目中

【2】API

1)org.apache.commons.io.IOUtils类

public static int copy(InputStream in, OutputStream out):
	把input输入流中的内容拷贝到output输出流中,返回拷贝的字节个数(适合文件大小为2GB以下)
    
public static long copyLarge(InputStream in, OutputStream out):
	把input输入流中的内容拷贝到output输出流中,返回拷贝的字节个数(适合文件大小为2GB以上)

代码实践 : 
package com.itheima.commons_io;

import org.apache.commons.io.IOUtils;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/*
    org.apache.commons.io.IOUtils类

    public static int copy(InputStream in, OutputStream out):
	把input输入流中的内容拷贝到output输出流中,返回拷贝的字节个数(适合文件大小为2GB以下)

    public static long copyLarge(InputStream in, OutputStream out):
	把input输入流中的内容拷贝到output输出流中,返回拷贝的字节个数(适合文件大小为2GB以上)
 */
public class Test1 {
    public static void main(String[] args) throws IOException {
        IOUtils.copy(new FileInputStream("D:\\传智播客\\安装包\\好看的图片\\liqin2.jpg"), new FileOutputStream("day12_demo\\liqin.jpg"));
    }
}

2)org.apache.commons.io.FileUtils

public static void copyFileToDirectory(final File srcFile, final File destFile): 
	复制文件到另外一个目录下。
public static void copyDirectoryToDirectory(File src , File dest ):
	复制src目录到dest位置。

代码实践:

package com.itheima.commons_io;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;

/*
    org.apache.commons.io.FileUtils

    public static void copyFileToDirectory(final File srcFile, final File destFile):
	复制文件到另外一个目录下。

    public static void copyDirectoryToDirectory(File src , File dest ):
	复制src目录到dest目录中。
 */
public class Test2 {
    public static void main(String[] args) throws IOException {
        FileUtils.copyDirectoryToDirectory(new File("D:\\传智播客\\安装包\\好看的图片") , new File("D:\\图片"));
    }
}

内容小结

commons-io可以简化IO操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值