Java语法相关知识汇总(七)

第一章、IO流

一、概述

1、IO流概念

主要用于保存数据,将内存中的数据保存到硬盘上,方便读取,实现类似于数据库的功能

2、IO流的分类

        在Java中,I/O(输入/输出)流是处理数据输入和输出的机制。它们用于从文件、网络连接、内存等源读取数据,或将数据写入到这些目标中。I/O流以字节流和字符流的形式存在。

        Java中的I/O流分为两个基本类型:字节流和字符流。字节流以字节为单位进行读取和写入,而字符流以字符为单位进行读取和写入。每种类型又分为输入流和输出流。

  • 按照流的方向进行分类(以内存作为参照物)
    • 输入流:往内存中区,叫做输入(Input)。或者叫做读(Read)
    • 输出流:从内存中出来,叫做输出(Ootput)。或者叫做写(Write)
  • 按照读取数据方式不同进行分类
    • 字节流:按照字节的方式读取数据,一次读取1个字节byte,等同于一次读取8个二进制位。这种流是万能的,什么类型的文件都可以读取。包括:文本文件,图片,声音文件,视频文件等
    • 中文字符在Windows中占用3个字节
      • 假设文件file.txt,采用字节流的话是这样读的:
      • a中国
      • 第一次读:一个字节(正好读到“a”)
      • 第二次读:一个字节(正好读到“中”字符的 1/ 3)
      • 第三次读:一个字节(正好读到“中”字符的 2/ 3)
      • 第四次读:一个字节(正好读到“中”字符的 3/ 3)
    • 字符流:按照字符的方式读取数据的,一次读取一个字符,这种流是为了方便读取普通文本文件而存在的,这种流不能读取:图片,声音,视频等文件,只能读取纯文本文件,连word文件都无法读取
      • 假设文件file.txt,采用字符流的话是这样读的:
      • a中国
      • 第一次读:“a”字符(“a”字符在wndows系统中占用1个字节)
      • 第二次读:“中”字符(“中”字符在windows系统中占用2个字节)

3、java中所有的流都在:java.io.*;下

        java中所有的IO流都已经写好了,我们不需要关心,我们最主要还是掌握,在java中已经提供了哪些流,每个流的特点是什么,怎么new流对象,每个流对象上常用的方法

二、java IO流的四大家族

1、概述

以下四个都是顶级类:都是抽象类(public abstract  class)

  • java.io.InputStream:字节输入流
  • java.io.OutputStream:字节输出流
  • java.io.Reader:字符输入流
  • java.io.Writer:字符输出流

注意:在java中只要“类名”以Stream结尾的都是字节流。以“Reader/Writer”结尾的都是字符流

  • java.io.*Stream:字节输入流(看结尾)
  • java.io.*Stream:字节输出流(看结尾)
  • java.io.*Reader:字符输入流(看结尾)
  • java.io.*Writer:字符输出流(看结尾)

2、所有的流都实现了java.io.Closeable.java接口

java.io.Closeable.java接口,都是可关闭的,都有close方法。

流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费(浪费)很多资源。

3、所有的输出流都实现了java.io.Flushable.java接口

  • java.io.Flushable接口,都是可刷新的,都有flush()方法。
  • 养成一个好习惯,输出流在最终输出之后,一定要记得flush(),刷新一下。
  • 这个刷新表示将通道/管道当中剩余未输出的数据强行进行输出完(情空管道!)
  • 刷新的作用就是清空管道

注意:如果没有flush()可能会导致丢失数据

三、java.io包下需要掌握的流(16个)

1、文件专属

java文件也是普通文本文件,只要能用记事本打开编辑的都是普通文本文件,并不一定是.txt后缀文件

  • java.io.FileInputStream
  • java.io.FileOutputStream
  • java.io.FileReader
  • java.io.FileWriter

2、转换流(将字节流转换成字符流

  • java.io.InputStreamReader
  • java.io.OutputStreamWriter

3、缓冲流专属

  • java.io.BufferedReader
  • java.io.BufferedWriter
  • java.io.BufferedInputStream
  • java.io.BufferedOutputStream

4、数据流专属

  • java.io.DataInputStream
  • java.io.DataOutputStream

5、标准输出流

  • java.io.PrintWriter
  • java.io.PrintStream

6、对象专属流

  • java.io.ObjectInputStream
  • java.io.ObjectOutputStream

四、IOException异常与流的基本使用

        使用I/O流时,通常的流程是创建适当的流对象,然后使用读取和写入方法对数据进行操作。完成操作后,应关闭流以释放系统资源。

注意:在处理I/O流时,需要处理可能抛出的异常,例如IOException。通常使用try-catch块来捕获和处理这些异常。

第二章、FileInputStream文件字节输入流,FileReader文件字符输入流(硬盘 -> 内存)

一、概述

FileInputStream是Java中用于从文件中读取字节的输入流类。它继承自InputStream类,并提供了一些额外的方法来支持文件读取操作

二、创建FileInputStream对象

使用FileInputStream可以打开一个文件并从中读取数据。以下是FileInputStream的常用构造方法:

1、FileInputStream(String fileName):根据指定的文件名创建一个FileInputStream对象。

1.1、绝对路径

//首先声明一个FileInputStream类型的变量为null,方便后期进行判断
FileInputStream fis = null;
try{
     //创建文件字节输入流对象
     //文件路径 C:\Users\13476\Desktop\JavaSE\Java进阶\新建 文本文档.txt (IDEA会自动把\变成\\)
//   FileInputStream fis = new FileInputStream("C:\\Users\\13476\\Desktop\\JavaSE\\Java进阶\\新建 文本文档.txt");
     //写成这个/也是可以的
     fis = new FileInputStream("C:/Users/13476/Desktop/JavaSE/Java进阶/新建 文本文档.txt");

}catch (FileNotFoundException e){
     e.printStackTrace();
}

1.2、相对路径

在IEDA中默认当前路径为工程project的根目录下

//尝试相对路径:相对路径一定是从当前的位置作为起点开始找
            //IDEA中默认当前路径是:工程project的根
//            fis = new FileInputStream("tempfile");

//            fis = new FileInputStream("java_advanced/tempfile2");

//            fis = new FileInputStream("java_advanced/src/tempfile3");

            fis = new FileInputStream("java_advanced/src/com/javase/io/tempfile4");

三、常用实例方法

        一旦创建了FileInputStream对象,就可以使用其提供的方法读取文件的内容。以下是一些常用的方法:

1、void close() throws IOException:关闭输入流,释放与其关联的系统资源

一般在finally语句块中进行关闭

finally {
            if(null != fis){
                //关闭流的前提是:流不是空
                //流是null的时候没必要关闭
                try {
                    fis.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

2、int read() throws IOException 从输入流中读取一个字节的数据,并返回读取的字节值(0-255)。如果已经达到文件末尾,则返回-1

FileInputStream 类的 read 方法返回的 int 类型是在 0 到 255 之间的,是因为它是用来读取字节数据的方法,而字节是一个 8 位的数据类型,范围是从 0 到 255(2^8 - 1)。

具体来说,FileInputStreamread 方法的工作方式如下:

  • 当它成功读取一个字节时,它将返回表示该字节的整数值(0 到 255),这意味着一个字节的所有可能值都可以通过正常的 int 类型来表示。
  • 当已经到达文件的末尾时,read 方法将返回 -1,表示没有更多的数据可供读取。

        这种设计的好处是,你可以用 int 类型来接收 read 方法的返回值,并且能够轻松地检查是否已经到达文件的末尾(通过检查是否返回 -1)。这种方法允许你有效地读取文件中的每个字节,同时提供了一种方式来检测文件的结束。

        要注意的是,如果你需要将读取的字节数据用于其他目的(例如构建字符串或解析二进制数据),你可能需要将返回的 int 值强制转换为 byte 类型,以便正确处理字节数据。

//开始读  读取a字符
int readData = fis.read(); //这个方法的返回值是:读取到一个字节所对应数据的字节值(0~255)
System.out.println(readData); //97

 案例一:

package com.javase.io;

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


/**
 * java.io.FileInputStram
 * 1、文件字节流,万能流,任何类型的文件都可以采用这个流来读
 * 2、字节的方式,完成输入的操作,完成流的操作(硬盘 --》 内存)
 */
public class FileInputStreamTest01 {
    public static void main(String[] args) {
        //提前声明一个FileInputStream类型的变量,并赋值为null
        FileInputStream fis = null;
        try{
            //创建文件字节输入流对象
            //文件路径 C:\Users\13476\Desktop\JavaSE\Java进阶\新建 文本文档.txt (IDEA会自动把\变成\\)
//            FileInputStream fis = new FileInputStream("C:\\Users\\13476\\Desktop\\JavaSE\\Java进阶\\新建 文本文档.txt");
            //写成这个/也是可以的
            fis = new FileInputStream("C:/Users/13476/Desktop/JavaSE/Java进阶/新建 文本文档.txt");

            //开始读  读取a字符
            int readData = fis.read(); //这个方法的返回值是:读取到一个字节所对应数据的字节值(0~255)
            System.out.println(readData); //97

            readData = fis.read(); //这个方法的返回值是:读取到一个字节所对应数据的字节值(0~255)
            System.out.println(readData); //98

            readData = fis.read(); //这个方法的返回值是:读取到一个字节所对应数据的字节值(0~255)
            System.out.println(readData); //99

            readData = fis.read(); //这个方法的返回值是:读取到一个字节所对应数据的字节值(0~255)
            System.out.println(readData); //100

            readData = fis.read(); //这个方法的返回值是:读取到一个字节所对应数据的字节值(0~255)
            System.out.println(readData); //101

            readData = fis.read(); //这个方法的返回值是:读取到一个字节所对应数据的字节值(0~255)
            System.out.println(readData); //102

            //已经读到文件末尾了,再读都是返回-1
            readData = fis.read(); //这个方法的返回值是:读取到一个字节所对应数据的字节值(0~255)
            System.out.println(readData); //-1

        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
        finally {
            if(null != fis){
                //关闭流的前提是:流不是空
                //流是null的时候没必要关闭
                try {
                    fis.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

案例二:

package com.javase.io;

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

/**
 * 对第一个程序进行改进
 *
 * 分析这个程序的缺点:
 *    一次读取一个字节byte,这样内存和硬盘交互太频繁,基本上时间都耗费在交互上了
 *    所以尽可能一次读取多个字节
 */
public class FileInputStreamTest02 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        int readData = 0;
        try {
            fis = new FileInputStream("C:/Users/13476/Desktop/JavaSE/Java进阶/新建 文本文档.txt");

//            while (true){
//                int readData = fis.read();
//                if(readData == -1){
//                    break;
//                }
//                System.out.println(readData);
//            }

            //改进while循环
            while ((readData = fis.read()) != -1){
                System.out.println(readData);
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
        finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

3、int read(byte[ ] b) throws IOException 从输入流中读取最多b.length个字节的数据,并将其存储在给定的字节数组b中。返回实际读取的字节数量(不是字节值),如果已经达到文件末尾,则返回-1。

1、数组中元素类型一致,每个元素所占的空间大小相同,是根据元素的类型所确定的,不一定是1byte

2、在该方法中byte[ ]数组中每个元素占1byte,因为字节输入流就是一个字节一个字节读取的。

3、该方法返回值不是具体的数据所对应的字节值,而是将数据拆分需要几个字节(总的字节数),返回的是本次读取到的字节的数量,且最大数量为b.length

汉字占三个字节,因此在初始化byte数组时最好是3的倍数,一般创建容量为1024的byte数组,这样会保证对同一个汉字可以完全读取不会乱码。

package com.javase.io;

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

/**
 * public int read(byte[] b) throws IOException;
 *     一次最多读取 b.length 个字节
 *     减少硬盘和内存的交互,提高程序的执行效率
 *     往byte[]数组中读
 */
public class FileInputStreamTest03 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            //尝试相对路径:相对路径一定是从当前的位置作为起点开始找
            //IDEA中默认当前路径是:工程project的根
//            fis = new FileInputStream("tempfile");

//            fis = new FileInputStream("java_advanced/tempfile2");

//            fis = new FileInputStream("java_advanced/src/tempfile3");

            fis = new FileInputStream("java_advanced/src/com/javase/io/tempfile4");

            //开始读,采用byte[]数组,一次读取多个字节,最多读取b.length个字节
            byte[] bytes = new byte[4]; //准备一个容量为4的byte[]数组

            int readCount = fis.read(bytes);
            System.out.println(readCount); //第一次读取4个字节
//            System.out.println(new String(bytes)); //abcd

            //不应该全部转换,应该是读取了多少字节,转换多少字节
            System.out.println(new String(bytes,0,readCount));


            readCount = fis.read(bytes);
            System.out.println(readCount); //第二次读取2个字节
//            System.out.println(new String(bytes)); //efcd

            //不应该全部转换,应该是读取了多少字节,转换多少字节
            System.out.println(new String(bytes,0,readCount));

            readCount = fis.read(bytes);
            System.out.println(readCount); //第三次读取,一个字节都读不到,返回-1
//            System.out.println(new String(bytes));//efcd
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

重点案例:

package com.javase.io;

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

/**
 * public int read(byte[] b) throws IOException;
 *     一次最多读取 b.length 个字节
 *     减少硬盘和内存的交互,提高程序的执行效率
 *     往byte[]数组中读
 */
public class FileInputStreamTest04 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("java_advanced/src/com/javase/io/tempfile4");

            byte[] bytes = new byte[4];
            int readCount = 0;

            while ((readCount = fis.read(bytes)) != -1){
                //将byte数组转换为字符串,读到多少个转多少个
                System.out.print(new String(bytes,0,readCount));
            }

//            int readData = 0;
//            while ((readData = fis.read()) != -1){
//                System.out.println(readData);
//            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 4、int available() throws IOException 返回剩下未读的字节数量

可以获取目标数据的总字节数,创建指定数量的byte[]数组,就可以不使用循环了

不适用于大文件,因为byte[]数组不能太大

fis = new FileInputStream("java_advanced/src/com/javase/io/tempfile4");

//获取总字节数量
System.out.println("总字节数量:" + fis.available()); //6
//创建总字节数量的byte[]数组,就不需要循环了
byte[] bytes = new byte[fis.available()];
fis.read(bytes);
System.out.println(new String(bytes)); //abcdef

5、long skip(long n) throws IOException 跳过n个字节不读取,从n+1个字节开始读取

//跳过n个字节不读取
fis.skip(3l);
System.out.println(fis.read()); //100

四、FileReader

1、用法和FileInputStream完全一样,只是有byte[]变为char[]

package com.javase.io.FileReader;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

/**
 * FileReader:
 * 文件字符输入流,只能读取普通文本
 * 读取文本内容时比较方便,快捷
 */
public class FileReaderTest {
    public static void main(String[] args) {
        FileReader reader = null;
        try {
            //创建文件字符输入流
            reader = new FileReader("C:\\java文件拷贝测试\\test.txt");

            /*//开始读
            char[] chars = new char[4];
            int readCount = 0;
            while ((readCount = reader.read(chars)) != -1) {
                System.out.print(new String(chars,0,readCount));
            }*/

            //遍历char[]数组
            char[] chars = new char[4];
            //将字符读取到char[]数组中
            reader.read(chars);

            for (char ele:
                 chars) {
                System.out.println(ele);
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

第三章、FileOutputStream 文件字节输出流,FileWriter文件字符输出流(内存 --> 硬盘)

一、概述

在Java中,FileOutputStream是用于将数据写入文件的类。它继承自OutputStream类,并提供了一组用于写入字节数据的方法。

1、文件字节输出流特点:没有指定的文件时会自动新建

没有指定的文件时会自动新建

2、文件名可跟绝对路径或相对路径

实例字节输出流对象时传入的文件名可以是绝对路径也可以是相对路径

3、输出流一定要在写入完成后进行flush()刷新

在写入完成后使用该方法void flush() throws IOException进行刷新

4、 输出流只会创建对应的目标文件而不会创建目标文件的父文件夹

需要提前创建出目标文件的父文件夹,否则会报FileNotFoundException异常

二、创建FileOutputStream对象

1、FileOutputStream(String name) throws FileNotFoundException 使用指定的File对象创建文件输出流。它将创建一个与指定文件关联的输出流(会将原文件内容清空再写入)

//myFile文件不存在时会新建
//这种方式慎用,会将原文件清空,然后重新写入
//使用相对路径,IDEA默认在工程根目录下
fos = new FileOutputStream("myFile");

2、FileOutputStream(File file, boolean append): 使用指定的File对象创建文件输出流,并指定是否在写入数据时追加到文件末尾。如果append参数为true,则数据将追加到文件末尾;如果为false,则会覆盖文件中的内容

//以追加的方式在文件末尾写入,不会清空原文件内容
fos = new FileOutputStream("java_advanced/src/com/javase/io/FileOutputStream/jzq1",true);

三、常用实例方法

1、void write(byte[] b) throws IOException 将字节数组全部写入文件

byte[] data = "Hello, World!".getBytes();
fos.write(data);
byte[] bytes = {97,98,99,100};

//将byte[]数组全部写入
fos.write(bytes);

2、void write(byte[] b, int off, int len) throws IOException 将字节数组的一部分写入文件,从指定的偏移量开始,写入指定长度的字节。

byte[] data = "Hello, World!".getBytes();
fos.write(data, 0, 5); // 写入 "Hello"

3、void flush() throws IOException: 刷新缓冲区,将缓冲区中的数据立即写入文件。

//写完之后一定要进行刷新
fos.flush();

四、FileWriter

java文件也是普通文本文件,只要能用记事本打开编辑的都是普通文本文件,并不一定是.txt后缀文件

1、用法和FileOutputStream一样,只是将byte[]转换为char[]数组

package com.javase.io.FileWriter;

import java.io.FileWriter;
import java.io.IOException;

/**
 * FileWriter
 * 文件字符输出流,写
 * 只能输出普通文本
 */
public class FileWriterTest {
    public static void main(String[] args) {
        FileWriter writer = null;
        try {
            //创建文件字符输出流对象
            writer = new FileWriter("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\FileWriter\\测试.txt",true);

            //开始写
            char[] chars = {'贾','卓','群'};
            //可以将char[]数组直接写入
            writer.write(chars);
            writer.write(chars,1,2);

            //也可以直接写入字符串
            writer.write("我是一名java软件工程师");

            //写入换行符
            writer.write("\n");

            writer.write("999999999999");

            writer.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

五、文件拷贝

1、文件字节流完成文件拷贝

1、原理

2、思路

使用FileInputSteam和FileOutputStream完成文件的拷贝

拷贝的过程是一边读,一边写

使用以上的字节流拷贝文件的时候,文件类型随意,是万能的,什么样的文件都可以

3、实现
package com.javase.io.文件拷贝;

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

/**
 * 使用FileInputSteam和FileOutputStream完成文件的拷贝
 * 拷贝的过程是一边读,一边写
 * 使用以上的字节流拷贝文件的时候,文件类型随意,是万能的,什么样的文件都可以贝
 */
public class CopyTest01 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            //创建输入流对象
            fis = new FileInputStream("C:\\java文件拷贝测试\\test.txt");

            //创建输出流对象
            fos = new FileOutputStream("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\文件拷贝\\测试.txt",true);

            //最核心的:一边读,一边写
            //1MB,一次最多拷贝1MB
            byte[] bytes = new byte[1024 * 1024];
            int readCount = 0;
            while ((readCount = fis.read(bytes)) != -1){
                fos.write(bytes,0,readCount);
            }

            //输出流刷新
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //分开try catch,不要一起try
            //一起try的时候,其中一个出现异常,可能会影响到另一个流的关闭
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2、文件字符流完成文件拷贝

只是将字节流中的  byte[]数组  换为  char[]数组

package com.javase.io.文件拷贝;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CopyTest02 {
    public static void main(String[] args) {
        FileReader reader = null;
        FileWriter writer = null;
        try {
            //读
            reader = new FileReader("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\文件拷贝\\CopyTest01.java");
            //写
            writer = new FileWriter("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\文件拷贝\\CopyTest0199asdf999.java",true);

            //一边读,一边写
            char[] chars = new char[1024 * 1024];
            int readCount = 0;
            while ((readCount = reader.read(chars)) != -1){
                writer.write(chars,0,readCount);
            }
            //刷新
            writer.flush();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e){
            e.printStackTrace();
        }finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

第四章、缓冲流

一、概述

        在Java中,缓冲流是用于提高输入和输出操作性能的一种机制。它们通过在内存中创建缓冲区来减少对底层资源(例如磁盘或网络)的直接访问次数,从而实现更高效的读写操作。Java提供了两种主要的缓冲流类:BufferedInputStream和BufferedOutputStream,它们分别用于输入和输出操作。

二、java.io.BufferedReader

1、概述

        在Java中,BufferedReader是一个用于读取文本数据的缓冲字符输入流。它继承自Reader类,并提供了一些额外的方法来提高读取性能和方便读取文本数据。

2、BufferedReader的主要特点如下:

2.1、缓冲功能

        BufferedReader内部维护了一个字符缓冲区,可以减少对底层资源(如文件或网络连接)的直接读取次数,提高读取效率。

2.2、读取行数据

        BufferedReader提供了readLine()方法,可以一次读取一行文本数据。这对于处理文本文件和网络协议等场景非常方便。

2.3、自动解码

        BufferedReader可以自动处理字符编码,根据指定的编码格式将字节数据转换为字符数据。这样可以避免手动处理编码转换的麻烦。

3、节点流与处理流

节点流和处理流是相对的,看在什么面前了

1、节点流

当一个流的构造方法中需要一个流对象的时候,这个被传进来的流叫做:节点流

2、处理流

外部负责包装的这个流,叫做:包装流,又叫,处理流

4、创建BufferedReader对象

1、BufferedReader(Reader in) 根据Reader节点流对象创建一个BufferedReader处理流对象

BufferedReader br = null;
FileReader reader = null;
reader = new FileReader("");
//当一个流的构造方法中需要一个流的时候,这个被传进来的流称为“节点流”
//外部负责包装的这个流,叫做:包装流,或者叫做处理流
//像当前程序来说:FileReader就是节点流,BufferedReader就是处理流
br = new BufferedReader(reader);

2、流关闭时只需要关闭处理流即可,节点流会自动关闭

5、常用实例方法

1、String readLine() throws IOException 返回读取到的一行文本,当读取到文件末尾时返回null

读取一个文本行,但不带换行符

//读取一个文本行,但不带换行符
            String s;
            while ((s = br.readLine()) != null) {
                System.out.println(s);
            }

案例:

package com.javase.io.缓冲流专属.BufferedReader;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

/**
 * 带有缓冲区的字符输入流
 * 使用这个流的时候不需要自定义char数组,或者说不需要自定义byte数组,自带缓冲
 */
public class BufferedReaderTest01 {
    public static void main(String[] args) {
        BufferedReader br = null;
        FileReader reader = null;
        try {
            reader = new FileReader("copy02.java");
            //当一个流的构造方法中需要一个流的时候,这个被传进来的流称为“节点流”
            //外部负责包装的这个流,叫做:包装流,或者叫做处理流
            //像当前程序来说:FileReader就是节点流,BufferedReader就是处理流
            br = new BufferedReader(reader);

            //读一行
//            System.out.println(br.readLine()); //第一行
//            System.out.println(br.readLine()); //第二行
//            System.out.println(br.readLine()); //第三行

            //读取一个文本行,但不带换行符
            String s;
            while ((s = br.readLine()) != null) {
                System.out.println(s);
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭
            //对于包装流来说:只需要关闭处理流即可,节点流会自动关闭
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

6、转换流:InputStreamReader

package com.javase.io.缓冲流专属.BufferedReader;

import java.io.*;

/**
 * InputStreamReader转换流:将字节流转换成字符流
 */
public class BufferedReaderTest02 {
    public static void main(String[] args) {
        FileInputStream in = null;
        BufferedReader br = null;
        try {
            /*//字节输入流
            in = new FileInputStream("copy02.java");

            //通过转换流将字节流转换成字符流
            //在这里in是节点流,reader是包装流
            InputStreamReader reader = new InputStreamReader(in);

            //这个构造方法只能传一个字符流,不能传字节流
            //在这里reader是节点流,br是包装流
            br = new BufferedReader(reader);*/

            br = new BufferedReader(new InputStreamReader(new FileInputStream("copy02.java")));

            String s;
            while ((s = br.readLine()) != null){
                System.out.println(s);
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

三、BufferedWriter

package com.javase.io.缓冲流专属.BufferedWriter;

import java.io.*;

/**
 * BufferedWriter:带有缓冲的字符输出流
 */
public class BufferedWriterTest {
    public static void main(String[] args) {
        BufferedWriter out = null;
        try {
            //带有缓冲区的字符输出流
            out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("copy", true)));
            //开始写
            out.write("hello");
            out.write("\n");
            out.write("world");
            //刷新
            out.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

第五章、数据流

一、概述

        在Java中,数据流是用于读取和写入基本数据类型和对象的机制。Java提供了两种主要的数据流类:DataInputStream和DataOutputStream。它们是字节流,继承自InputStream和OutputStream,用于读写基本数据类型和字符串等数据。

二、DataOutputStream(数据字节输出流)

        DataOutputStream提供了写入基本数据类型和字符串等数据的方法。它可以将Java中的各种数据类型转换为字节数据,并写入底层输出流(如文件或网络连接)。

try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("output.dat"))) {
    int intValue = 42;
    double doubleValue = 3.14;
    String stringValue = "Hello, World!";
    dos.writeInt(intValue);
    dos.writeDouble(doubleValue);
    dos.writeUTF(stringValue);
} catch (IOException e) {
    e.printStackTrace();
}

 数据字节流写入:

package com.javase.io.数据流;

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

/**
 * java.io.DataOutputStream:数据专属的流
 * 这个流可以将数据连同数据的类型一并写入文件
 * 注意:这个文件不是普通文本文档(这个文件使用记事本打不开)
 */
public class DataOutputStreamTest {
    public static void main(String[] args) {
        DataOutputStream dos = null;
        try {
            dos = new DataOutputStream(new FileOutputStream("data"));

            //写数据
            byte b = 100;
            short s = 200;
            int i = 300;
            long l = 400L;
            float f = 3.0F;
            double d = 3.14;
            boolean boo = false;
            char c = 'a';

            //写
            //将数据以及数据的类型一并写入文件当中
            dos.writeByte(b);
            dos.writeShort(s);
            dos.writeInt(i);
            dos.writeLong(l);
            dos.writeFloat(f);
            dos.writeDouble(d);
            dos.writeBoolean(boo);
            dos.writeChar(c);


            //刷新
            dos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (dos != null) {
                try {
                    dos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

三、DataInputStream(数据字节输入流)

DataOutputStream写的文件,只能使用DataInputStream去读,并且读的时候需要提前知道写入的顺序,读的顺序和写的顺序一致,才可以正常取出数据

        DataInputStream提供了读取基本数据类型和字符串等数据的方法。它可以从底层输入流(如文件或网络连接)中读取原始数据,并将其解释为Java中的各种数据类型。

try (DataInputStream dis = new DataInputStream(new FileInputStream("input.dat"))) {
    int intValue = dis.readInt();
    double doubleValue = dis.readDouble();
    String stringValue = dis.readUTF();
    // 处理读取的数据
} catch (IOException e) {
    e.printStackTrace();
}

数据字节流读取:

package com.javase.io.数据流;

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

/**
 * DataInputStream:数据字节输入流
 * DataOutputStream写的文件,只能使用DataInputStream去读,并且读的时候需要提前知道写入的顺序
 * 读的顺序和写的顺序一致,才可以正常取出数据
 */
public class DataInputStreamTest01 {
    public static void main(String[] args) {
        DataInputStream dis = null;
        try {
            dis = new DataInputStream(new FileInputStream("data"));
            //开始读
            byte b = dis.readByte();
            short s = dis.readShort();
            int i = dis.readInt();
            long l = dis.readLong();
            float f = dis.readFloat();
            double d = dis.readDouble();
            boolean boo = dis.readBoolean();
            char c = dis.readChar();

            System.out.println(b);
            System.out.println(s);
            System.out.println(i);
            System.out.println(l);
            System.out.println(f);
            System.out.println(d);
            System.out.println(boo);
            System.out.println(c);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (dis != null) {
                try {
                    dis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

四、数据流原理

        无论是使用DataInputStream还是DataOutputStream,它们的工作原理都是类似的。当执行读取或写入操作时,数据流会将基本数据类型转换为字节数据,并进行读取或写入操作。这样可以方便地处理基本数据类型和字符串等数据的输入输出。

        需要注意的是,在读取和写入数据时,应确保读写的顺序和数据类型的匹配,以免出现数据解析错误。同时,在使用数据流进行读写操作后,应该关闭流来释放资源。可以使用try-with-resources语句来自动关闭流,如上面的示例代码所示。

第六章、标准输出流 PrintStream

一、概述

java.io.PrintStream:标准的字节输出流,默认输出到控制台

        在Java中,标准输出流(Standard Output Stream)是指向标准输出设备的输出流,通常指向控制台。Java中的标准输出流用于将文本数据输出到控制台或其他标准输出设备。

        在Java中,标准输出流由System.out表示,它是一个PrintStream对象。PrintStream类提供了一系列用于输出文本数据的方法。

1、标准输出流不需要手动close关闭

        标准输出流(System.out)在Java中是一个特殊的输出流,它与控制台或其他标准输出设备相关联。与其他输出流不同,标准输出流通常不需要手动关闭。

        标准输出流在程序运行期间是一直开放的,并且通常由Java虚拟机(JVM)管理和控制。因此,在使用标准输出流时,无需手动关闭它。当程序执行完毕或JVM终止时,标准输出流会被自动关闭。

        关闭标准输出流可能会导致不可预测的结果。例如,如果手动关闭了标准输出流,后续的输出语句将无法将内容输出到控制台,这可能会导致程序的正常输出被丢失或出现异常。

        因此,通常情况下,我们无需显式地关闭标准输出流。Java虚拟机会在适当的时候自动关闭它。如果需要关闭其他输出流或资源,应该在使用完毕后显式地调用相应的关闭方法(如close())来释放资源。

        需要注意的是,标准输出流并不是不可关闭的,但是在大多数情况下,手动关闭它是不必要的,甚至是不推荐的。只有在特殊情况下,例如在将标准输出流重定向到其他输出目标时,才需要手动关闭标准输出流。

二、标准输出流默认会输出到控制台。可以通过System.setOut()方法将标准输出流重定向到其他输出流

        需要注意的是,默认情况下,标准输出流会输出到控制台。但是,可以通过System.setOut()方法将标准输出流重定向到其他输出流,例如文件输出流,从而将输出内容写入文件。

try (PrintStream ps = new PrintStream(new FileOutputStream("output.txt"))) {
    System.setOut(ps);  // 将标准输出流重定向到文件输出流
    System.out.println("Hello, World!");
} catch (IOException e) {
    e.printStackTrace();
}
package com.javase.io.标准输出流;

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

/**
 * java.io.PrintStream:标准的字节输出流,默认输出到控制台
 */
public class PrintStreamTest {
    public static void main(String[] args) {
        //联合起来写
        System.out.println("hello world");

        //分开写
        PrintStream ps1 = System.out;
        ps1.println("hello jzq");

        //标准输出流不需要手动关闭
        //标准输出流默认会输出到控制台。可以通过System.setOut()方法将标准输出流重定向到其他输出流

        /*//之前学的System类的方法和属性
        System.gc();
        System.currentTimeMillis();
        System.exit(0);
        System.arraycopy();*/

        try {
            //标准输出流不再指向控制台,指向了“log.txt”文件
            PrintStream ps = new PrintStream(new FileOutputStream("log.txt"));
            //修改输出方向,将输出方向修改到log文件
            System.setOut(ps);
            //再输出
            //本身就有换行符,不用在添加 \n 了
            System.out.println("hello world");
            System.out.println("hello jzq");
        
            //刷新
            System.out.flush();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

三、封装日志记录工具

package com.javase.io.标准输出流;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 日志工具
 */
public class Logger {

    public static void log(String msg){
        try {
            //指向一个日志文件
            PrintStream ps = new PrintStream(new FileOutputStream("日志.txt",true));
            //改变输出方向
            System.setOut(ps);
            //获取当前时间
            Date nowTime = new Date();
            //当前时间格式化
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
            String strTime = simpleDateFormat.format(nowTime);

            //输出到日志文件
            System.out.println(strTime + ": " + msg);

            //刷新
            System.out.flush();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class LoggerTest{
    public static void main(String[] args) {
        Logger.log("调用了System类的gc方法,建议启动垃圾回收");
        Logger.log("用户登录成功");
    }
}

第七章、File类

一、概述

1、文件路径名和目录路径名的抽象表示形式

        文件和目录路径名的抽象表示是指将文件和目录在计算机系统中的路径表示为一个抽象的Java对象。在Java中,使用File类来表示文件和目录的路径名

        路径名是用于定位文件或目录在文件系统中位置的字符串。它可以是绝对路径(从根目录开始的完整路径)或相对路径(相对于当前工作目录的路径)。

        File类提供了构造函数和方法,用于创建File对象并操作路径名。它将路径名封装在一个对象中,使得我们可以方便地对文件和目录进行各种操作,如创建、删除、重命名、判断存在性等。

        通过File类,我们可以使用不同的方法来获取路径名的各种属性,例如文件名、路径、绝对路径等。这样,我们可以以一种便捷的方式操作文件和目录,而不需要直接操作字符串路径。

        使用文件和目录路径名的抽象表示可以提高代码的可读性和可维护性,同时也使得跨平台的开发更加方便。无论在哪个操作系统上运行,File类都能够正确地处理文件和目录的路径。

C:\Users\13476\Desktop\JavaSE\Java概述   File对象(目录路径名)

C:\Users\13476\Desktop\JavaSE\Java概述\A.class  File对象(文件路径名)

2、File类和四大家族没得关系

File类和四大家族没有关系,所以File类不能完成文件的读写

二、创建File对象

1、File(String pathName):通过指定路径名创建一个File对象(若不带后缀则默认为目录路径名)

//目录路径名
File f1 = new File("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\File");
//文件路径名
File f2 = new File("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\File.txt");

创建 java.io.File 对象时,并不一定需要使用绝对路径,还可以使用相对路径。

  1. 绝对路径: 包含文件或目录的完整路径,从文件系统的根目录开始描述。例如,在 Windows 系统下,一个文件的绝对路径可能是 C:\Users\username\Documents\file.txt

  2. 相对路径: 相对于当前工作目录的路径。当前工作目录是程序运行时所处的目录。例如,如果你的 Java 程序运行在 /home/username 目录下,文件 file.txt 相对于当前工作目录的路径可能是 Documents/file.txt

你可以在创建 File 对象时使用相对路径,只需提供相对于当前工作目录的路径即可。例如:

File relativeFile = new File("Documents/file.txt");

        这样会在当前工作目录下寻找名为 file.txt 的文件。如果需要使用绝对路径,可以直接提供文件的完整路径。

        需要注意的是,相对路径会受到程序运行时所处的当前工作目录的影响。因此,确保理解当前工作目录在你的程序中的含义,以及相对路径是相对于该目录而言的,有助于正确使用相对路径来创建 File 对象。

IO流在IDEA中的相对路径是相对于工程根目录来说的

package com.chess.demo;

import java.io.File;

public class FileTest {
    public static void main(String[] args) {
        File file1 = new File("笔记/io流.txt");
        System.out.println(file1.exists()); //true
        File file2 = new File("C:\\Users\\13476\\Desktop\\java项目\\Java-Chinese-Chess-Complex\\笔记\\io流.txt");
        System.out.println(file2.exists()); //true
    }
}

三、常用的实例方法

1、boolean exists():判断文件/目录是否存在(不论文件路径名或目录路径名,只要在指定路径处存在,则返回true)

//目录路径名
File f1 = new File("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\File");
//文件路径名
File f2 = new File("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\File.txt");
//判断文件/目录是否存在
System.out.println(f1.exists()); //true
System.out.println(f2.exists()); //false

2、boolean createNewFile() throws IOException:创建一个新的空文件。如果文件已存在,则返回false。

File f2 = new File("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\File\\jzq");

//如果D:\JavaSE\JavaSE_demo1\java_advanced\src\com\javase\io\File\jzq不存在,则以文件的形式创建出来
if (!(f2.exists())) {
   try {
       boolean result = f2.createNewFile();
       System.out.println(result);
   } catch (IOException e) {
       e.printStackTrace();
   }
}

3、boolean mkdir():创建一个新的目录。如果目录已存在或者创建失败,则返回false

File f2 = new File("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\File\\jzq");

//如果D:\JavaSE\JavaSE_demo1\java_advanced\src\com\javase\io\File\jzq不存在,则以目录的形式创建出来
if (!(f2.exists())) {
   //以目录的形式新建
   boolean result = f2.mkdir();
   System.out.println(result);
}

4、boolean mkdirs():创建一个新的目录,包括其所有不存在的父目录。如果目录已存在或者创建失败,则返回false

File f3 = new File("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\File\\jzq\\1\\2\\3\\4");
//以多重目录新建
if(!(f3.exists())){
    f3.mkdirs();
}

5、String getParent():获取父目录的路径

File f4 = new File("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\File\\jzq");
System.out.println(f4.exists());
//获取文件的父路径
String parentPath = f4.getParent();
System.out.println(parentPath); //D:\JavaSE\JavaSE_demo1\java_advanced\src\com\javase\io\File

6、File getParentFile():获取父目录的File对象

File parentFile = f4.getParentFile();
        System.out.println("获取绝对路径:" + parentFile.getAbsolutePath()); //获取绝对路径:D:\JavaSE\JavaSE_demo1\java_advanced\src\com\javase\io\File

7、String getAbsolutePath():获取文件/目录的绝对路径

File f5 = new File("copy");
        String absolutePath = f5.getAbsolutePath();
        System.out.println(absolutePath); //D:\JavaSE\JavaSE_demo1\copy

8、String getName():获取文件/目录的名称

File f1 = new File("D:\\JavaSE\\JavaSE_demo1\\copy");

        //获取文件名:
        System.out.println("文件名:" + f1.getName()); //文件名:copy

9、boolean isDirectory():判断是否为目录

//判断是否是一个目录
        System.out.println(f1.isDirectory()); //false

10、boolean isFile():判断是否为文件

//判断是否是一个文件
        System.out.println(f1.isFile()); //true

11、long lastModified():获取文件/目录的最后修改时间

long time = f1.lastModified();
        Date nowTime = new Date(time);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
        String strTime = simpleDateFormat.format(nowTime);
        System.out.println("最后修改时间为:" + strTime); //最后修改时间为:2023-06-22 09:24:49:466

12、long length():获取文件的大小(字节数)

System.out.println("字节数为:" + f1.length()); //字节数为:11

13、File[] listFiles():返回目录中所有的(当前临近)子文件/(当前临近)子目录的File对象数组

//获取当前目录下所有的子文件
        File f = new File("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\File");
        File[] files = f.listFiles();
        System.out.println(Arrays.toString(files));
        for (File file:
             files) {
            System.out.println(file.getAbsolutePath());
            System.out.println(file.getName());
            /**
             * D:\JavaSE\JavaSE_demo1\java_advanced\src\com\javase\io\File\FileTest01.java
             * FileTest01.java
             * D:\JavaSE\JavaSE_demo1\java_advanced\src\com\javase\io\File\FileTest02.java
             * FileTest02.java
             * D:\JavaSE\JavaSE_demo1\java_advanced\src\com\javase\io\File\FileTest03.java
             * FileTest03.java
             * D:\JavaSE\JavaSE_demo1\java_advanced\src\com\javase\io\File\jzq
             * jzq
             */
        }

四、常用属性

1、File.separator

        在 Java 的 java.io.File 类中,separator 是一个静态常量,表示操作系统的文件路径分隔符。这个属性的值根据运行程序的操作系统而变化,因为不同的操作系统使用不同的字符来分隔文件路径。

        在 Windows 系统中,文件路径分隔符是反斜杠 \。例如:C:\Users\username\Documents\file.txt

        而在类Unix(包括 Linux 和 macOS)系统中,文件路径分隔符是正斜杠 /。例如:/home/username/Documents/file.txt

        为了保证跨平台的兼容性,Java 提供了 separator 常量,使得在编写路径相关的代码时,不需要关心实际的操作系统,而是使用这个常量来拼接路径。这样做有助于使 Java 程序在不同操作系统上都能正确地处理文件路径,而不会因为路径分隔符不同而出现问题。

以下是 java.io.File 类中 separator 常量的使用示例:

import java.io.File;

public class FilePathExample {
    public static void main(String[] args) {
        String path = "C:" + File.separator + "Users" + File.separator + "username" + File.separator + "Documents" + File.separator + "file.txt";
        File file = new File(path);

        // 在不同操作系统下,path 的分隔符会自动替换为相应的操作系统分隔符
        System.out.println("File path: " + file.getPath());
    }
}

        这样做使得代码更加可移植和灵活,因为它能够适应不同操作系统的文件路径格式而不需要手动硬编码分隔符。 

五、拷贝目录

package IO流.目录拷贝;

import java.io.*;

public class CopyAll {
    public static void main(String[] args) {
        copyDir("C:\\Users\\yuliang\\Desktop\\学习\\JavaSE\\Java练习代码\\IO流\\标准输出流",
                "C:\\Users\\yuliang\\Desktop\\学习\\JavaSE\\Java练习代码\\IO流\\文件拷贝目录");

//        System.out.println("D:\\JavaSE\\JavaSE_demo1\\java_advanced\\src\\com\\javase\\io\\文件专属\\文件拷贝".length());
    }

    public static void copyDir(String srcDir, String desDir) {
        //拷贝源
        File src = new File(srcDir);
        //拷贝目标
        File des = new File(desDir);

        //调用方法拷贝
        copyAll(src, des);
    }

    /**
     * 拷贝目录
     *
     * @param src 拷贝源
     * @param des 拷贝目标
     */
    private static void copyAll(File src, File des) {
        //如果拷贝源是一个文件,则方法结束
        if (src.isFile()) {
            //src是一个文件的话,递归结束
            //是文件才需要拷贝
            //一边读,一边写
            FileInputStream in = null;
            FileOutputStream out = null;

            try {
                //读取这个文件
                in = new FileInputStream(src);

                //写到该文件中
                String newFilePath = (des.getAbsolutePath().endsWith("\\") ? des.getAbsolutePath() : des.getAbsolutePath() + "\\") + src.getAbsolutePath().substring(48);
                System.out.println("==================拷贝后的文件=======================");
                System.out.println(newFilePath);

                out = new FileOutputStream(newFilePath);

                byte[] bytes = new byte[1024 * 1024]; //一次赋值1MB
                int readCount;
                while ((readCount = in.read(bytes)) != -1) {
                    out.write(bytes, 0, readCount);
                }

                //刷新
                out.flush();

            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (out != null) {
                    try {
                        out.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            // 程序执行到此处时方法执行结束
            return;
        }else {
            //是为了在目标目录中 创建原始的目录
            String primaryDir = ((des.getAbsolutePath().endsWith("\\")) ? des.getAbsolutePath() : des.getAbsolutePath() + "\\") + src.getAbsolutePath().substring(48);
            File newDirFile = new File(primaryDir);
            if(!(newDirFile.exists())){
                newDirFile.mkdirs();
            }
        }
        //获取拷贝源下的子目录/文件
        File[] files = src.listFiles();
        //循环
        for (File file : //file有可能是个文件,也有可能是个目录
                files) {
//            //获取所有目录或文件的绝对路径
//            System.out.println(file.getAbsolutePath());

            if (file.isDirectory()) {
//                System.out.println(file.getAbsolutePath()); //D:\JavaSE\JavaSE_demo1\java_advanced\src\com\javase\io\文件专属\文件拷贝\aaa
                //获取源目录绝对路径
                String srcDir = file.getAbsolutePath();
                //将原目录中要拷贝的部分路径进行切割
                String cutSrcDir = srcDir.substring(48);
//                System.out.println(cutSrcDir);
//                System.out.println(des.getAbsolutePath());

                //在目标路径下降源目录中的要拷贝的路径进行拼接
                String desDir = (des.getAbsolutePath().endsWith("\\") ? des.getAbsolutePath() : des.getAbsolutePath() + "\\") + cutSrcDir;
//                System.out.println(desDir);

                //根据新拼接的目标路径,创建File对象
                File newDir = new File(desDir);
                //若目标路径不存在,则创建多重目录
                if (!newDir.exists()) {
                    newDir.mkdirs();
                }
            }
            /**
             * 递归调用
             * 重点是:src一直变化,des一直没变
             */
            copyAll(file, des);
        }
    }
}
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class CopyAll{
	public static void main(String[] args){
		CopyAll.copyDir("C:\\Users\\yuliang\\Desktop\\学习\\JavaSE\\Java练习代码\\IO流\\复制文件", "C:\\Users\\yuliang\\Desktop\\学习\\JavaSE\\Java练习代码\\IO流\\文件拷贝目录");
	}

	public static void copyDir(String srcDir, String desDir){
		File src = new File(srcDir);
		File des = new File(desDir);
		
		CopyAll.copyAll(src, des);
	}

	public static void copyAll(File src, File des){
		String cutDir = src.getAbsolutePath().substring(48);
		String decoratedDes = (des.getAbsolutePath().endsWith("\\")) ? des.getAbsolutePath() : des.getAbsolutePath() +"\\";
		String newFilePath = decoratedDes + cutDir;

		if(src.isFile()){
			
			try(FileInputStream in = new FileInputStream(src); FileOutputStream out = new FileOutputStream(newFilePath )){
				byte[] bytes = new byte[1024 * 1024];
				int readNum;
				while((readNum = in.read(bytes)) != -1){
					out.write(bytes, 0, readNum);
				}
				System.out.println("复制成功:" + newFilePath);
			}catch(FileNotFoundException e){
				e.printStackTrace();
			}catch(IOException e){
				e.printStackTrace();
			}
			return;
		}else{
			File primaryDir = new File(newFilePath );
			if(!primaryDir.exists()){
				primaryDir.mkdir();
			}
		}

		File[] files = src.listFiles();
		
		for(File file: files){
			if(file.isDirectory()){
				String newDir = decoratedDes + file.getAbsolutePath().substring(48);
				File newDirFile = new File(newDir);
				if(!newDirFile.exists()){
					newDirFile.mkdir();
				}
			}
			CopyAll.copyAll(file, des);
		}
	}
}

第八章、对象流

一、概述

        在Java中,对象流(Object Streams)是一种用于序列化和反序列化对象的机制。对象流提供了将Java对象转换为字节流并写入文件或网络,并将字节流转换回Java对象的能力。

1、序列化与反序列化

在 Java 中,序列化是将对象转换为字节流的过程,以便将其存储到文件、传输到网络或者在内存中进行持久化。反序列化是将字节流转换回对象的过程,使其能够在程序中重新使用。 

2、参与序列化和反序列化的对象,必须实现 java.io.Serializable接口

        需要注意的是,要使一个类的对象能够被序列化,该类必须实现Serializable接口。此外,静态成员变量和transient关键字修饰的成员变量在对象序列化过程中可能会有特殊处理。 

注意:通过源代码发现,Serializable 接口只是一个标志接口:

public interface Serializable {

}

这个接口当中什么代码也没有,只是起到标识的作用,JVM看到这个类实现了这个接口,可能会对这个类进行特殊待遇

Serializable这个标志接口是给JVM参考的,JVM看到Serializable接口之后,会自动为该类生成一个序列化版本号。(虽然在编译后的字节码文件中没有写出来,但是JVM会默认提供这个序列化版本号)

3、java.io.NotSerializableException:不支持序列化异常

若类没有实现Serializable接口,则会抛出该异常

4、 transient关键字

        在 Java 中,transient 是一个关键字,用于修饰类的成员变量当一个成员变量被标记为 transient 时,它将不会参与对象的序列化和反序列化过程。

4.1、transient 关键字的作用

        当一个成员变量被标记为 transient 时,它将被视为临时的,不参与对象的序列化和反序列化过程。这意味着在将对象序列化为字节流时,transient 成员变量的值将被忽略,并且在反序列化对象时,transient 成员变量将被赋予默认值(如数值类型为 0,引用类型为 null)。

4.2、应用场景

transient 关键字通常用于标记一些不需要被序列化的敏感信息或临时数据。以下是一些常见的应用场景:

  • 敏感信息:例如密码、安全令牌等,避免将这些信息存储到持久化介质中或在网络传输时暴露。
  • 计算得到的数据:例如缓存数据、临时计算结果等,这些数据可以根据需要重新计算,不需要进行序列化和传输。
  • 不可序列化的对象引用:某些对象引用无法被序列化(如线程、文件句柄等),将其标记为 transient 可避免序列化过程中出现问题。

4.3、注意事项

在使用 transient 关键字时,需要注意以下几点:

  • 序列化自定义过程:如果类实现了自定义的序列化和反序列化方法(writeObject()readObject()),需要在这些方法中手动处理 transient 成员变量的读写。
  • 对象完整性:被标记为 transient 的成员变量在反序列化后会恢复为默认值,因此在使用这些对象时需要注意其完整性,避免出现空指针异常或其他逻辑错误。

        总结: transient 关键字用于修饰类的成员变量,表示该成员变量不参与对象的序列化和反序列化过程。它通常用于标记敏感信息、临时数据或不可序列化的对象引用。在使用 transient 关键字时,需要注意自定义序列

5、静态成员和transient修饰的实例成员不参与序列化和反序列化

在 Java 中,静态成员和被 transient 修饰的实例成员都不会参与对象的序列化和反序列化过程。

5.1. 静态成员不参与序列化和反序列化

        静态成员属于类级别的成员,而不是实例级别的成员。在序列化过程中,只有实例的状态(实例变量)会被序列化,而静态成员属于类的状态,不会被序列化到字节流中。当对象被反序列化时,静态成员的状态将保持在内存中,因为它们与类的定义相关,而不是特定对象的状态。

5.2. transient 修饰的实例成员不参与序列化和反序列化

        当一个实例成员被标记为 transient 时,它将被视为临时的,不参与对象的序列化和反序列化过程。在将对象序列化为字节流时,被 transient 修饰的成员变量的值将被忽略。在反序列化对象时,transient 成员变量将被赋予默认值,如数值类型为 0,引用类型为 null。

        需要注意的是,被 transient 修饰的实例成员变量必须在对象的自定义序列化方法(writeObject())中进行手动处理,如果没有自定义序列化方法,则该成员变量在反序列化后将被赋予默认值。

        总结: 静态成员和被 transient 修饰的实例成员都不会参与对象的序列化和反序列化过程。静态成员属于类级别的状态,而 transient 修饰的实例成员在序列化过程中被忽略,不会保存到字节流中,并在反序列化时被赋予默认值。

6、序列化版本号的作用

        比如过了很久,有一个类的源代码改动了,源代码改动之后,需要重新编译,编译之后生成了全新的字节码文件,并且class文件再次运行的时候,JVM生成的序列化版本号也会发生相应的变化

java.io.InvalidClassException:

com.javase.io.对象流.bean.Student; local class incompatible:

stream classdesc serialVersionUID = 791370652993768717, (改动前class文件的序列化版本号)

local class serialVersionUID = 415237976666516511(改动后class文件的序列化版本号)

在 Java 中,序列化版本号(Serialization Version ID)是一个与序列化类相关的标识符。它在对象序列化和反序列化过程中起着重要的作用。以下是序列化版本号的主要作用:

  1. 确保版本一致性:序列化版本号用于确保序列化和反序列化的类版本一致。如果类的结构在序列化后发生了更改(例如添加、删除或修改字段或方法),则反序列化可能会导致不兼容的问题。通过显式指定序列化版本号,可以在反序列化时检查版本一致性,从而防止不一致的类结构导致的问题。

  2. 防止反序列化攻击:序列化版本号还可以用于增强安全性。如果一个恶意程序尝试反序列化一个带有不匹配版本号的对象,它可能会引发异常,从而防止潜在的安全漏洞。

  3. 允许类的演化:有时候,类的结构需要进行演化,但仍然要保持向后兼容性。通过明确指定序列化版本号,你可以告诉 Java 序列化机制哪些更改是有意的,哪些是无意的,以及如何处理这些更改。

6.1、java中区分类的机制

第一:首先通过类名进行比对,如果类名不一样,肯定不是同一个类

第二:如果类名一样,在通过序列化版本号进行比对区分

6.2、序列化版本号的优点

比如:

小明编写了一个类:com.javase.bean.Student  implements  Serializable

小李编写了一个类:com.javase.bean.Student  implements  Serializable

不同的人编写了同一个类,但“这两个类确实不是同一个类”。这个时候序列化版本号就起作用了

对于JVM来说,JVM是可以区分开这两个类的,因为这两个类都实现了Serializable接口,都有默认的序列化版本号,他们的序列化版本号不一样,所以区分开了(这是序列化版本号的好处)

6.3、序列化版本号的缺点

这种自动生成的序列化版本号的缺点是:一旦代码确定之后,不能进行后续的修改,因为只要修改源代码,必然会重新编译,此时会生成全新的class文件,JVM在类加载后,会对该类生产全新的序列化版本号,这个时候JVM会认为这个是一个全新的类。(这样就不好了)

7、固定不变的序列化版本号

最终结论:

凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号。

这样,以后这个类即使代码修改了,但是版本号不变,JVM会认为是同一个类

7.1、手动指定序列化版本号(不建议JVM自动生成)

//java虚拟机看到Serializable接口之后,会自动生成一个序列化版本号。
    //这里没有手动写出来,java虚拟机会默认提供这个序列化版本号
    //建议将序列化版本号手动的写出来,不建议自动生成

    /**
     * 必须叫serialVersionUID 这个名字
     * 值须具有唯一性
     */
    @java.io.Serial
    private static final long serialVersionUID = 1L; //JVM标识一个类的时候先通过类名,如果类名一致,则根据序列化版本号判断是否为同一个类

7.2、IDEA工具自动生成序列化版本号

8、普通父类实现Serializable接口,其子类不会自动实现该接口

        当一个类实现了Serializable接口,它的子类并不会自动地实现Serializable接口。在Java中,如果一个类实现了Serializable接口,它表明该类的对象可以被序列化,即可以被转换成字节流进行传输或保存。但是,这个特性并不会自动继承给子类。

        如果你希望一个父类实现了Serializable接口的特性被其子类继承,子类也能够被序列化,那么子类也需要显式地实现Serializable接口。如果子类不实现Serializable接口,即使父类实现了,子类对象也不会被视为可序列化的。

import java.io.Serializable;

// 父类实现了 Serializable 接口
class Parent implements Serializable {
    // ...
}

// 子类也实现了 Serializable 接口
class Child extends Parent implements Serializable {
    // ...
}

        在上面的例子中,即使Parent类实现了Serializable接口,但如果Child类没有显式地实现Serializable接口,Child类的对象仍然不能被序列化。 

9、抽象父类实现Serializable接口,其子类会自动实现该接口

        如果一个抽象类实现了Serializable接口,那么它的子类在没有显式实现Serializable接口的情况下也会自动地具有序列化的能力。

import java.io.Serializable;

// 抽象类实现了 Serializable 接口
abstract class AbstractParent implements Serializable {
    // ...
}

// 子类继承自抽象类,在不显式实现 Serializable 接口的情况下也具有序列化能力
class Child extends AbstractParent {
    // ...
}

        在这个例子中,Child类继承自AbstractParent抽象类,而AbstractParent实现了Serializable接口。因此,Child类的对象会自动地具有序列化的能力,即使Child类本身没有显式地实现Serializable接口。 

二、java.io.ObjectOutputStream

1、概述

        ObjectOutputStream(对象输出流):用于将Java对象转换为字节流并写入输出流,以便进行持久化或传输。可以将对象的状态保存到文件或将其发送到网络。

   ObjectOutputStream 类是 Java 中用于将对象序列化为字节流的类。它继承自 OutputStream 类并实现了 ObjectOutput 接口。

2、序列化

ObjectOutputStream 类的主要功能是将 Java 对象序列化为字节流。通过使用 writeObject() 方法,可以将一个对象写入到输出流中。序列化的过程会将对象的状态转换为字节流,以便在网络传输、持久化到文件或者存储到内存中进行后续操作。

3、构造方法

通常需要将输出流包装在FileOutputStreamSocketOutputStream等其他输出流中。

3.1、ObjectOutputStream(OutputStream out)

使用指定的输出流创建对象输出流。该构造方法用于将对象序列化后的字节写入到给定的输出流中。

4、写入对象

4.1、void writeObject(Object obj) throws IOException

要将对象写入到输出流中,可以使用 writeObject() 方法。该方法会将给定的对象进行序列化,并将序列化后的字节写入到输出流中。被写入的对象必须实现 Serializable 接口。

5、刷新和关闭

在写入对象后,可以使用 flush() 方法将缓冲区的内容刷新到底层流中,确保所有数据都被写入。另外,可以使用 close() 方法关闭对象输出流。关闭输出流会自动调用 flush() 方法,并释放相关资源。

三、java.io.ObjectInputStream

1、概述

ObjectInputStream(对象输入流):用于从输入流中读取字节流并将其转换回Java对象。

ObjectInputStream 类是 Java 中用于从字节流中反序列化对象的类。它继承自 InputStream 类并实现了 ObjectInput 接口。

2、反序列化

ObjectInputStream 类的主要功能是从字节流中反序列化对象。通过使用 readObject() 方法,可以从输入流中读取字节,并将其反序列化为对象。反序列化的过程将字节流转换为对象的状态,使其能够在程序中被使用。

3、构造方法

通常需要将输入流包装在FileInputStreamSocketInputStream等其他输入流中。

3.1、ObjectInputStream(InputStream in)

使用指定的输入流创建对象输入流。该构造方法用于从给定的输入流中读取字节并进行反序列化

4、读取对象

4.1、Object readObject()

要从输入流中读取对象,可以使用 readObject() 方法。该方法会从输入流中读取字节,并将其反序列化为对象。读取的对象必须是之前通过 ObjectOutputStream 序列化的对象。

5、关闭

在读取对象后,可以使用 close() 方法关闭对象输入流。关闭输入流会释放相关资源。

四、对象流的使用

1、使用步骤

对象流的使用步骤如下:

  1. 创建相应的对象流,如ObjectOutputStreamObjectInputStream
  2. 使用对象流的方法将Java对象写入输出流(序列化)或从输入流中读取字节流并将其转换回Java对象(反序列化)。
  3. 关闭对象流和相关的输入/输出流。

2、单个对象进行序列化和反序列化

自定义类型

package com.javase.io.对象流.bean;

import java.io.Serializable;

public class Student  implements Serializable{

    //java虚拟机看到Serializable接口之后,会自动生成一个序列化版本号。
    //这里没有手动写出来,java虚拟机会默认提供这个序列化版本号

    private int id;
    private String name;

    public Student() {
    }

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

序列化

package com.javase.io.对象流;

import com.javase.io.对象流.bean.Student;

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

/**
 * java.io.NotSerializableException:不支持序列化异常
 */
public class ObjectOutputStreamTest01 {
    public static void main(String[] args) {
//        String s = "a" + "b" + "c";
        //创建java对象
        Student s = new Student(1, "zhangsan");

        ObjectOutputStream oos = null;
        //序列化
        try {
            oos = new ObjectOutputStream(new FileOutputStream("student"));

            //序列化对象
            //java.io.NotSerializableException
            oos.writeObject(s);

            //刷新
            oos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

反序列化

package com.javase.io.对象流;

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

/**
 * 反序列化
 */
public class ObjectInputStreamTest01 {
    public static void main(String[] args) {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("student"));

            //开始反序列化,读
            Object obj = ois.readObject();

            //反序列化回来是一个学生对象,所以会调用学生对象的toString方法
            System.out.println(obj.toString()); //Student{id=1, name='zhangsan'}


        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

写法二:

import java.io.Serializable;

public class Student implements Serializable{
	
	@java.io.Serial
	private static final long serialVersionUID = 1L;
	
	private int id;
	private String name;
	private int a;
	int b;
	
	public Student(){}
	
	public Student(int id, String name){
		this.id = id;
		this.name = name;
	}
	
	public String toString(){
		return "Student对象{ id:" + this.id + ", name:" + this.name + "}";
	}
	
	public void m(){
		System.out.println("实例方法");
	}
}
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ObjectOutputStreamTest{
	public static void main(String[] args){
		Student s = new Student(1, "zhangsan");
		try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("student"))){
			out.writeObject(s);
			
			out.flush();
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class ObjectInputStreamTest{
	public static void main(String[] args){
		try(ObjectInputStream in = new ObjectInputStream(new FileInputStream("student"))){
			Object obj = in.readObject();
			if(obj instanceof Student){
				System.out.println(obj); //Student对象{ id:1, name:zhangsan}
				Student s = (Student) obj;
				s.m(); //实例方法
			}
		}catch(ClassNotFoundException e){
			e.printStackTrace();
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}

3、多个对象进行序列化和反序列化

自定义类型

package com.javase.io.对象流.bean;

import java.io.Serializable;

public class User implements Serializable {
    int id;
    //transient关键字表示临时的,不参与序列化与反序列化,
    //反序列化时会赋默认值,如String类型默认为null,int类型默认为0
    transient String name;

    public User() {
    }

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

序列化

package com.javase.io.对象流;

import com.javase.io.对象流.bean.User;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * 一次序列化对个对象:
 *    可以将对象放到集合中,序列化集合
 *
 *    注意:参与序列化的ArrayList集合以及集合中的元素User均需要实现java.io.Serializable接口。
 */
public class ObjectOutputStreamTest02 {
    public static void main(String[] args) {
        List<User> userList = new ArrayList<>();
        userList.add(new User(1,"zhangsan"));
        userList.add(new User(2,"lisi"));
        userList.add(new User(3,"wangwu"));

        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("users"));
            //序列化一个集合,这个集合对象中放了很多其他对象
            oos.writeObject(userList);
            oos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

反序列化

package com.javase.io.对象流;

import com.javase.io.对象流.bean.User;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.List;

/**
 * 反序列化
 */
public class ObjectInputStreamTest02 {
    public static void main(String[] args) {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("users"));

            Object obj = ois.readObject();

            //User类中的name实例变量未使用transient修饰前
//            if (obj instanceof List<?>){
//                List<?> objList = (List<?>) obj;
//                for (int i = 0; i < objList.size(); i++) {
//                    System.out.println(objList.get(i));
//                    /**
//                     * User{id=1, name='zhangsan'}
//                     * User{id=2, name='lisi'}
//                     * User{id=3, name='wangwu'}
//                     */
//                }
//            }

            //User类中的name实例变量使用transient修饰后
            List<User> userList = (List<User>) obj;
            for (User user:
                    userList) {
                System.out.println(user);
                /**
                 * User{id=1, name='null'}
                 * User{id=2, name='null'}
                 * User{id=3, name='null'}
                 */
            }

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

写法二:

import java.io.Serializable;

public class User implements Serializable{
	
	//指定序列化版本号
	@java.io.Serial
	private static final long serialVersionUID = 1L;
	
	int id;
	String name;
	
	//以下为后续新增的
	int a;
	int b;
	
	
	public User(){}
	
	public User(int id, String name){
		this.id = id;
		this.name = name;
	}
	
	public String toString(){
		return "User对象: " + this.id + ", " + this.name;
	}
	
	public void m(){
		System.out.println(this.name + "的实例方法");
	}
}
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;

public class ObjectOutputStreamTest{
	public static void main(String[] args){
		User u1 = new User(1,"张三");
		User u2 = new User(2,"李四");
		User u3 = new User(3,"王五");
		
		ArrayList<User> list = new ArrayList<>();
		list.add(u1);
		list.add(u2);
		list.add(u3);
		
		try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("users"))){
			out.writeObject(list);
			
			out.flush();
		}catch(FileNotFoundException e){
			e.printStackTrace();
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;

public class ObjectInputStreamTest{
	public static void main(String[] args){
		try(ObjectInputStream in = new ObjectInputStream(new FileInputStream("users"))){
			Object obj = in.readObject();
			
			//类型缩小,对于集合泛型来说,要一步一步逐渐进行缩小
			if(obj instanceof List<?>){
				List<?> userList = (List<?>)obj;
				for(Object u: userList){
					System.out.println(u);
					if(u instanceof User){
						User user = (User)u;
						user.m();
					}
					
				}
			}
		}catch(ClassNotFoundException e){
			e.printStackTrace();
		}catch(FileNotFoundException e){
			e.printStackTrace();
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}

第九章、IO和Properties的联合使用

一、设计理念

        以后对于经常变化的数据,可以单独写到一个文件中,使用程序动态读取,将来只需要修改这个文件中的内容,java代码不需要改动,不需要重新编译,服务器也不需要重启,就可以拿到动态的信息

1、属性配置文件

类似于以上机制的这种文件被称为属性配置文件。

并且当配置文件中的内容格式为:

        key1=value1

        key2=value2

则称为属性配置文件。

java规范要求:属性配置文件建议以properties结尾,但这不是强制的。

这种以properties后缀的文件在java中成为:属性配置文件

其中java.util.Properties.java这个类是专门存放属性配置文件内容的一个类

二、案例

1、属性配置文件

userinfo.properties


#建议key和value之间使用 = 隔开
#=左边是key
#=右边是value
username=root
######################### 在属性配置文件中#是注释 ##############################

#属性配置文件的key重复的话,value会自动覆盖
#password=123
password=123456

#最好不要有空格
data   =   100

#也可以用 : 隔开,但不建议
age:27

2、读取属性

package com.javase.io.io和properties联合使用;

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

/**
 * IO和Properti的联合使用
 */
public class IoPropertiesTest01 {
    public static void main(String[] args) {
        /**
         * Properties是一个Map集合,key和value都是String类型
         * 想把userinfo文件中的数据加载到Properties对象当中
         */

        //新建一个输入流对象
        FileInputStream in =  null;
        try {
            in = new FileInputStream("D:\\java_demo\\java_demo1\\java_advanced\\src\\com\\javase\\io\\io和properties联合使用\\userinfo.properties");

            //新建一个Map集合
            Properties properties = new Properties();

            //调用Properties对象的load方法将文件中的数据加载到Map集合中
            properties.load(in); //文件中的数据顺着管道加载到Map集合中,其中等号左边为key,等号右边为value

            //通过key来获取value
            String username = properties.getProperty("username");
            System.out.println(username);

            String password = properties.getProperty("password");
            System.out.println(password);

            System.out.println(properties.getProperty("data"));

            System.out.println(properties.getProperty("age"));


        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

三、属性配置文件中中文字符编码问题

        Java的Properties类是用来读取和操作属性配置文件的,但它默认使用ISO-8859-1字符编码来处理文件中的内容。这就导致如果配置文件中包含非ASCII字符(比如中文字符),在读取时就会出现乱码问题。

解决这个问题有两种方式:

1、设置字符编码为UTF-8

        使用正确的字符编码加载配置文件:可以通过使用InputStreamReader来指定正确的字符编码加载配置文件。例如,如果你的配置文件使用UTF-8编码,可以这样加载:

Properties properties = new Properties();
try (InputStream inputStream = new FileInputStream("config.properties")) {
    properties.load(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
}

这样就能够正确地读取包含中文字符的属性配置文件。

2、将中文修改为unicode编码(详见java基础语法一)

        将配置文件中的中文字符进行转义:如果你无法改变配置文件的字符编码,你可以使用Unicode转义序列来表示中文字符。将中文字符转换为Unicode转义序列后,再写入配置文件。例如,"中文"可以转义为"\u4e2d\u6587"。在读取时,Properties类会将转义序列还原为相应的中文字符。

无论使用哪种方式,都能够正确处理包含中文字符的属性配置文件,并避免乱码问题。

userinfo.properties

\u6570\u636e\u5e93=Mysql
username=root
password=123456
import java.util.Properties;
import java.io.FileInputStream;
import java.io.FileNotFoundException;


public class IOProperties{
	public static void main(String[] args){
		Properties p = new Properties();
		
		try(FileInputStream in = new FileInputStream("userinfo.properties")){
			p.load(in);
			
			String dbName = p.getProperty("数据库");
			System.out.println(dbName); //Mysql
			
			System.out.println(p.getProperty("username")); //root
			
			System.out.println(p.getProperty("password")); //123456
			
		}catch(FileNotFoundException e){
			e.printStackTrace();
		}catch(java.io.IOException e){
			e.printStackTrace();
		}
	}
}

第十章、多线程

一、概述

在Java中,进程和线程都是用于并发执行任务的概念,但它们在实现方式和作用上有所不同。

1、进程(Process)

        进程是指计算机中正在运行的程序的实例。每个进程都拥有自己独立的内存空间和系统资源,它们之间相互隔离,彼此独立运行。每个进程都有自己的地址空间、文件描述符、环境变量等。

  • 进程是操作系统分配资源的基本单位,每个进程都有自己的内存空间、代码和数据。
  • 每个进程在独立的内存空间中运行,它们之间互相隔离,一个进程的崩溃通常不会影响其他进程。
  • 进程通常用于执行独立的任务,例如运行独立的应用程序或服务。
  • 在Java中,可以通过ProcessBuilderRuntime.exec()等类和方法来创建和管理进程。每个进程都有自己的 Java解释器,因此可以充分利用多核处理器。

进程是一个应用程序(1个进程是一个软件)

2、线程(Thread)

        线程是在进程内部执行的独立单元,它是进程中的一个执行路径。一个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源。线程之间可以并发执行,共享数据,使得程序可以同时执行多个任务。

  • 线程是进程内的执行单元,一个进程可以包含多个线程,它们共享同一进程的内存空间和资源。
  • 线程更轻量级,创建和销毁线程的开销较小,线程之间的切换速度较快。
  • 线程通常用于执行并发的任务,例如在图形用户界面 (GUI) 应用程序中处理用户输入、执行网络请求等。
  • 在Java中,可以通过Thread类或者实现Runnable接口来创建和管理线程。

线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程

对于java程序来说,当在DOS命令窗口中输入:

java HelloWorld 回车之后

会先启动JVM,而JVM就是一个进程。

JVM再启动一个主线程调用main方法

同时再启动一个垃圾回收线程负责看护,回收垃圾

最起码,现在的java程序中有两个线程并发。一个是垃圾回收线程,一个是执行main方法的主线程

3、进程和线程的关系

阿里巴巴:进程

  • 马云:阿里巴巴的一个线程
  • 员工A:阿里巴巴的一个线程

京东:进程

  • 强东:京东的一个线程
  • 员工B:京东的一个线程

进程可以看做是现实生活中的公司;线程可以看作是公司当中的某个员工

注意:

进程A和进程B的内存独立不共享(阿里巴巴和京东资源不会共享)

线程A和线程B:

  • 在java中:线程A和线程B,堆内存和方法区内存共享,但是栈内存是独立的,一个线程一个栈内存
  • 假设启动10个线程,会有10个栈内存空间,每个栈和每个栈之间互不干扰,各自执行各自的,这就是多线程并发

火车站可以看做是一个进程。

火车站中的每一个售票窗口可以看做是一个线程

        我在窗口1购票,你可以在窗口2中购票,你不需要等我,我也不需要等你。所以多线程并发可以提高效率

java中之所以有多线程机制,目的就是为了提高程序的处理效率

4、线程与进程的主要区别如下:

  • 资源占用:进程之间相互独立,每个进程拥有自己的资源。而线程共享进程的资源,包括内存空间、文件等。
  • 执行单元:进程是一个程序的执行实例,有自己的独立执行环境。线程是进程内部的执行单元,可以并发执行多个线程。
  • 切换开销:线程切换的开销比进程切换要小,因为线程共享同一进程的资源,切换时只需保存和恢复线程的执行上下文。
  • 通信和同步:线程之间可以通过共享内存进行通信和数据交换,但也需要考虑线程同步和互斥的问题。进程之间通信相对复杂,需要使用特定的进程间通信机制。

        在Java中,多线程编程可以提高程序的并发性能和资源利用率,但也需要注意线程安全和同步的问题,避免出现竞态条件和数据一致性问题。

5、程序,进程和线程的区别和联系

程序,进程,线程的区别和联系_程序和进程-CSDN博客

程序、进程、线程的概念、区别与联系_程序进程线程的区别与联系_今天不想敲代码!的博客-CSDN博客

6、main方法结束程序不一定结束

使用了多线程机制之后,main方法结束,只是主线程结束了,主栈空了,其他的栈(线程)可能还在压栈弹栈

7、单核的CPU不能做到真正的多线程并发

对于多核的CPU来说,可以做到真正的多线程并发

  • 4核CPU表示在同一个时间点上,可以真正的有4个进程并发执行,每一个进程中至少有一个线程

什么是真正的多线程并发?

  • t1线程执行t1的
  • t2线程执行t2的
  • t1不会影响t2,t2也不会影响t1,这叫做真正的 多线程并发

单核的CPU表示只有一个大脑:

  • 不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉
  • 对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个进程之间频繁切换执行,对人感觉来说,多个事情同时在做
    • 进程A:播放音乐
    • 进程B:运行游戏
    • 线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行。给我们的感觉是同时并发的

        电影院采用胶卷播放电影,一个胶卷一个胶卷播放速度达到一定程度之后,人类的眼睛产生了错觉,感觉是动画的。这说明人类的反应速度很慢,就像一根钢针扎到手上,到最终感觉到疼,这个过程需要“很长的”的时间,在这个期间计算机可以进行亿万次的循环,所以计算机的执行速度很快。

二、实现多线程的方式

1、编写一个类,直接继承java.lang.Thread,重写run方法

java中支持多线程机制,并且java已经将多线程实现了,我们只需要继承就行了

//定义线程类

public class MyThread extends java.lang.Thread{

        public void run(){

        }

        public static void main(String[] args){         

                //创建线程对象

                MyThread t = new MyThread();

                //启动线程

                t.start();

        }

}

package com.javase.多线程;

/**
 * 实现线程的第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法
 *
 * 怎么创建线程对象? new就行了
 * 怎么启动线程尼? 调用线程对象的start()方法
 *
 * 注意:
 * 亘古不变的道理:
 *     方法体当中的代码永远都是自上而下的顺序依次逐行执行的。
 *     
 * 以下程序的输出结果有这样的特点:
 *    有先有后
 *    有多有少
 *
 */
public class ThreadTest02 {
    public static void main(String[] args) {
        //这里是main方法,这里的代码属于主线程,在主栈中运行
        //新建一个分支栈对象
        MyThread myThread = new MyThread();

        //启动线程
//        myThread.run(); //不会启动线程,不会分配新的分支栈(这种方式就是单线程)
        /**
         * start()方法的作用:
         *     启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
         *     这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了,线程就启动成功了
         *     启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)
         *     run方法在分支栈的栈底部,main方法在主栈的栈底部,run和main是平级的
         */
        myThread.start();

        //这里的代码还是运行在主线程中
        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程--->" + i);
        }
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        //编写程序,这段程序运行在分支线程中(分支栈)
        for (int i = 0; i < 1000; i++) {
            System.out.println("分支线程 --->" + i);
        }
    }
}

1.1、run()和start()方法的区别

2、编写一个类,实现java.lang.Runnable接口

第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他的类,更灵活

//定义一个可运行的类

public class MyRunnable  implements java.lang.Runnable{

        @override

        public void run(){

        }

}

//创建线程对象

Thread t = new Thread(new MyRunnable());

//启动线程

t.start();

package com.javase.多线程;

/**
 * 实现多线程的第二种方法:
 * 编写一个类实现java.lang.Runnable 接口
 */
public class ThreadTest03 {
    public static void main(String[] args) {
        //创建一个可运行的对象
//        MyRunnable r = new MyRunnable();
        //将可运行的对象封装成一个线程对象
//        Thread t = new Thread(r);

        //合并为一行
        Thread t = new Thread(new MyRunnable());
        //启动线程
        t.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("主线程-->" + i);
        }
    }
}

//这并不是一个线程类,是一个可运行的类。它还不是一个线程
class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("分支线程-->" + i);
        }
    }
}

2.1、采用匿名内部类方式

package com.javase.多线程;

/**
 * 采用匿名内部类
 */
public class ThreadTest04 {
    public static void main(String[] args) {
        //创建线程对象,采用匿名内部类方式
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("分支线程-->" + i);
                }
            }
        });
        //启动线程
        t.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("主线程-->" + i);
        }
    }
}

3、编写一个类,实现java.util.concurrent.Callable接口(新特性)

        这种方式实现的线程可以获取线程的返回值。之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void。

思考:

系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,我们怎么能拿到这个执行结果尼?

使用第三种方式:实现Callable接口

优点:可以获取到线程的执行结果

缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低

package com.javase.多线程.实现多线的第三种方法;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask; //JUC包下的,属于java的并发包,老JDK中没有这个包,新特性

/**
 * 实现线程的第三种方法
 * 实现Callable接口
 */
public class ThreadTest03 {
    public static void main(String[] args) {
        //第一步:创建一个“未来任务类”对象
        //参数非常重要,需要给一个Callable接口实现类对象
//        FutureTask task = new FutureTask(new MyCallable());

        //采用匿名内部类方式
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                //线程执行一个任务,执行之后可能之后会有一个执行结果
                //模拟执行
                System.out.println("call method begin");
                Thread.sleep(1000 * 10);
                System.out.println("call method over");
                int a = 100;
                int b = 200;
                return a + b; //自动装箱,300结果变为Integer
            }
        });

        //创建线程对象
        Thread t = new Thread(task);

        //启动线程
        t.start();

        //这里是main线程
        //在main线程中,如何获取t线程的返回结果?
        try {
            //get()的执行会导致“当前线程阻塞”
            Object obj = task.get();
            System.out.println("线程执行结果:" + obj);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        //main方法这里的程序想要执行必须等待get()方法的结束
        //而get()方法可能需要很久,因为get()方法是为了拿另一个线程的执行结果
        //另一个线程执行是需要时间的
        System.out.println("hello world!");
    }
}

class MyCallable implements Callable{

    @Override
    public Object call() throws Exception { //call()方法就相当于run方法
        //线程执行一个任务,执行之后可能之后会有一个执行结果
        //模拟执行
        System.out.println("call method begin");
        Thread.sleep(1000 * 10);
        System.out.println("call method over");
        int a = 100;
        int b = 200;
        return a + b; //自动装箱,300结果变为Integer
    }
}

三、线程的生命周期

在Java中,线程的生命周期可以被描述为以下几个状态:

1、新建状态(New)

        当创建一个Thread对象时,线程处于新建状态。此时线程尚未开始运行。

2、就绪状态(Runnable)

        在新建状态下,调用线程的start()方法会使线程进入可运行状态。处于可运行状态的线程可能正在等待系统资源,例如处理器时间片,以便开始执行。

3、运行状态(Running)

        处于可运行状态的线程获得了处理器时间片并开始执行线程的run()方法。线程在运行状态下执行具体的任务。

4、阻塞状态(Blocked)

        线程可能会在某些条件下被阻塞。当线程等待某个操作完成、等待输入/输出、等待获取锁或等待其他线程释放锁时,它将进入阻塞状态。当条件满足时,线程将从阻塞状态转换为可运行状态。

5、死亡状态(Terminated)

        线程完成了其run()方法的执行或者因异常而终止后,进入终止状态。线程一旦进入终止状态,就无法再回到任何其他状态。

        需要注意的是,线程的状态转换是由Java虚拟机和操作系统来管理和控制的,并且具体的实现可能会因不同的Java版本或操作系统而有所差异。

        因为运行一个java程序,就会启动JVM,启动JVM就是一个进程,相当于单核,在同一时间只能干一个事情,但在一个进程中,可以有多个线程,这些线程争夺进程资源,给人感觉像是实现了并发

6、线程生命周期状态图示

四、线程的简单操作

1、public final String getName(): 获取线程对象的名字

String name = 线程对象.getName();

第一个分支线程默认名为:Thread-0

第二个分支线程默认名为:Thread-1

第三个分支线程默认名为:Thread-2  依次类推

2、public final synchronized void setName(String name): 修改线程对象的名字

线程对象.setName("线程名字");

3、public static native Thread currentThread(): 获取当前线程对象

Thread  t  = Thread.currentThread();  //静态方法

返回值t就是当前线程对象

这段代码不止出现main方法或run方法中,也可以作为成员变量或局部变量,有哪个线程对象开启的这个线程,执行了这行代码,获取的就是该线程对象

package com.javase.多线程;

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

        ThreadTest05 threadTest05 = new ThreadTest05();
        threadTest05.doSome();

        //currentThread就是当前线程
        //这个代码出现在main方法当中,所以当前线程就是主线程
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName()); //main

        //创建线程对象
        Thread t = new Thread(new MyThread2());

        //设置线程的名字
        t.setName("jzq");

        //获取线程的名字
        String tName = t.getName();
        //默认名字
        System.out.println(tName); //Thread-0

        //修改后的名字
        System.out.println(t.getName()); //jzq
        
        //在创建一个线程对象
        Thread t2 = new Thread(new MyThread2());
        //默认名字
        System.out.println(t2.getName()); //Thread-1

        t2.setName("888");
        System.out.println(t2.getName()); //888
        
        //启动线程
        t.start();

        t2.start();
    }

    public void doSome(){
        //这样就不行了
//        this.getName();
//        super.getName();
        //这样就可以
        String name = Thread.currentThread().getName();
        System.out.println("当前线程名字:" + name);
    }
}

class MyThread2 implements Runnable{
    @Override
    public void run() {
        //当t1线程执行run方法,则当前线程就是t1
        //当t2线程执行run方法,则当前线程就是t2
        Thread currentThread = Thread.currentThread();

        for (int i = 0; i < 10; i++) {
            System.out.println(currentThread.getName() + "--->" + i);
        }
        System.out.println("====================");
    }
}

五、线程阻塞sleep

1、用法

static  void  sleeo(long  millis)

静态方法:Thread.sleep(1000);

参数是毫秒

作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有的CPU时间片,让给其他线程使用

这行代码出现在A线程中,A线程就会进入休眠

这行代码出现在B线程中,B线程就会进入休眠

Thread.sleep()方法,可以做到这种效果:间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。

package com.javase.多线程;

public class ThreadTest06 {
    public static void main(String[] args) {
//        //让当前线程进入休眠,睡眠5秒
//        //当前线程为主线程!!!
//        try {
//            Thread.sleep(5000);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
//
//        //5秒之后执行这里的代码
//        System.out.println("hello");

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
            //睡眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2、面试题:尝试用线程类对象调用sleep方法

package com.javase.多线程;

/**
 * 关于Thread.sleep()方法的面试题:
 */
public class ThreadTest07 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread3());
        t1.setName("t1");
        t1.start();

        //调用sleep方法
        try {
            //问题:这行代码会让线程t1进入休眠状态吗?  不会
            //在实行的时候还是会转换成:Thread.sleeo(5000);
            //这行代码的作用是:让当前线程进入休眠,也就是说main线程进入休眠。
            //这行代码出现在main方法中,main线程睡眠
            t1.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //5秒之后这里才会执行
        System.out.println("Hello World");
    }
}

class MyThread3 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

3、终止线程的睡眠(线程对象.interrupt( ))

注意:不是中断线程的执行,而是终止线程的睡眠。

这种中断方式依靠了java的异常处理机制

package com.javase.多线程;

/**
 * sleep睡眠太久了,需要终止睡眠
 */
public class ThreadTest08 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable2());
        t.setName("t");
        t.start();

        //希望main线程5秒之后醒来(5秒之后main线程手里的活干完了)
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //中断t线程的睡眠
        //这种中断方式依靠了java的异常处理机制
        t.interrupt();
    }
}

class MyRunnable2 implements Runnable {

    @Override
    public void run() throws RuntimeException {
        //重点:run()方法中的异常不能throws,只能try catch
        //因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。
        System.out.println(Thread.currentThread().getName() + "--> begin");
        //睡眠一年
        try {
            Thread.sleep(1000 * 60 * 60 * 24 * 365);
        } catch (InterruptedException e) {
            //打印异常信息
//            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--> end");
    }
}

4、强行终止线程

4.1、线程对象.stop()

这种方式存在很大的缺点,容易丢失数据,因为这种方式是直接将线程杀死了,线程没有保存的数据会丢失,不建议使用。

package com.javase.多线程;

/**
 * 强行终止一个线程的执行
 */
public class ThreadTest09 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable3());
        t.setName("t");
        t.start();

        //模拟5秒
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //5秒之后强行终止t线程
        t.stop(); //已过时,不建议使用
    }
}

class MyRunnable3 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 4.2、采用合理标记终止线程

package com.javase.多线程;

/**
 * 采用合法标记终止线程
 */
public class ThreadTest10 {
    public static void main(String[] args) {
        MyRunnable4 r = new MyRunnable4();
        Thread t = new Thread(r);
        t.setName("t");
        t.start();

        //模拟5秒
        try {
            Thread.sleep(1000 * 3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //终止线程
        //想要什么时候终止t线程的执行,就把标记改为false,线程就结束了
        r.run = false;
    }
}

class MyRunnable4 implements Runnable {
    //打一个布尔标记
    boolean run = true;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (run) {
                System.out.println(Thread.currentThread().getName() + "-->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                //终止当前线程
                //return就结束了,在结束之前还有什么没保存的,可以在此处进行保存
                return;
            }
        }
    }
}

第十一章、线程的调度

一、常见的线程调度模型

1、抢占式调度模型

哪个线程的优先级比较高,抢到的CPU时间的概率就高一些/多一些。java采用的就是抢占式调度模型。

2、均分式调度模型

平均分配CPU时间片,每个线程占有的CPU时间片长度一样,平均分配,一切平等。

有一些编程语言,线程调度模型采用的就是这种方式

二、常见的线程调度方法

1、void setPriority (int newPriority):设置线程的优先级

最低优先级是1

最高优先级是10

优先级比较高的线程获取的CPU时间片可能会多一些,(但也不完全是,大概率是多的)

package com.javase.多线程;

/**
 * 设置线程的优先级
 * 优先级高的线程抢到CPU时间片的概率会大一些
 * 不是谁先谁后的问题,只是抢到CPU时间片的概率大小
 */
public class ThreadTest11 {
    public static void main(String[] args) {
        //设置main线程优先级为1
        Thread.currentThread().setPriority(1);
        //获取当前线程对象,获取当前线程的优先级
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName() + "线程的优先级为:" + currentThread.getPriority());

        Thread t1 = new Thread(new T1());
        t1.setName("t1");
        //设置t1线程优先级为10
        t1.setPriority(10);
        t1.start();

        //优先级搞得,相对抢到CPU时间片多一些
        //大概率偏向优先级比较高的
        for (int i = 0; i < 10000; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

class T1 implements Runnable{

    @Override
    public void run() {
        //获取当前线程对象,获取当前线程的优先级
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName() + "线程的优先级为:" + currentThread.getPriority());

        for (int i = 0; i < 10000; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

2、void getPriority():获取线程优先级

package com.javase.多线程;

/**
 * 获取线程的优先级
 *
 * 线程最高优先级:10
 * 线程最低优先级:1
 * 线程默认优先级:5
 * 
 * 未设置线程优先级直接默认为5
 */
public class ThreadTest11 {
    public static void main(String[] args) {
        System.out.println("线程最高优先级:" + Thread.MAX_PRIORITY);
        System.out.println("线程最低优先级:" + Thread.MIN_PRIORITY);
        System.out.println("线程默认优先级:" + Thread.NORM_PRIORITY);

        //获取当前线程对象,获取当前线程的优先级
        Thread currentThread = Thread.currentThread();
        //main线程的默认优先级为:5
        System.out.println(currentThread.getName() + "线程的默认优先级为:" + currentThread.getPriority());

        Thread t1 = new Thread(new T1());
        t1.setName("t1");
        t1.start();
    }
}

class T1 implements Runnable{

    @Override
    public void run() {
        //获取当前线程对象,获取当前线程的优先级
        Thread currentThread = Thread.currentThread();
        //t1线程的默认优先级为:5
        System.out.println(currentThread.getName() + "线程的默认优先级为:" + currentThread.getPriority());
    }
}

3、static void yield():让位方法

暂停当前正在执行的线程对象,并执行其他线程

yeild()方法不是阻塞方法,让当前线程让位,让给其他线程使用

yield()方法的执行会让当前线程从“运行状态”转换到“就绪状态”

注意:在回到就绪状态之后,有可能还会再次抢到时间片

package com.javase.多线程;

/**
 * 让位:当前线程暂停,回到就绪状态,让给其他线程
 */
public class ThreadTest12 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyT1());
        t.setName("t");
        t.start();

        for (int i = 1; i <= 1000; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

class MyT1 implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            //每100个让位一次
            if(i % 100 == 0){
                Thread.yield(); //当前线程暂停一下,让给主线程
            }
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

4、void join():合并线程

        当一个线程调用另一个线程的 join() 方法时,它会暂停自己的执行,等待被调用线程执行完毕后再继续执行。

package com.javase.多线程;

public class JoinExample {
    public static void main(String[] args) {
        // 创建线程对象
        Thread thread1 = new Thread(new MyRunnable5("Thread 1"));
        Thread thread2 = new Thread(new MyRunnable5("Thread 2"));

        // 启动线程
        thread1.start();
        thread2.start();

        try {
            System.out.println("Main thread is waiting for thread1 to complete.");
            thread1.join(); // 主线程等待 thread1 执行完毕
            System.out.println("Main thread resumes execution after thread1 completes.");

            System.out.println("Main thread is waiting for thread2 to complete.");
            thread2.join(); // 主线程等待 thread2 执行完毕
            System.out.println("Main thread resumes execution after thread2 completes.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("All threads have finished executing.");
    }
}

class MyRunnable5 implements Runnable {
    private String name;

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

    @Override
    public void run() {
        System.out.println(name + " started.");

        try {
            // 模拟线程执行一段时间
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(name + " finished.");
    }
}

        在上述示例中,我们创建了两个线程 thread1thread2,它们分别执行一个 MyRunnable 对象。在主线程中,我们启动了这两个线程,并调用它们的 join() 方法来等待它们执行完毕。

        当主线程执行到 thread1.join() 时,主线程会暂停执行,等待 thread1 执行完毕后再继续执行。类似地,主线程执行到 thread2.join() 时,会暂停执行,等待 thread2 执行完毕后再继续执行。

        通过 join() 方法的调用,我们可以确保在主线程输出 "All threads have finished executing." 之前,两个线程都已经完成了它们的执行。

        需要注意的是,如果被等待的线程已经执行完毕,调用其 join() 方法不会有任何影响,主线程会继续执行。

package com.javase.多线程;

/**
 * 线程合并
 */
public class ThreadTest13 {
    public static void main(String[] args) {
        System.out.println("main begin");

        Thread t = new Thread(new MyRunnable7());
        t.setName("t");
        t.start();

        try {
            //合并线程
            t.join(); //t线程合并到当前main线程中,当前main线程会阻塞,t线程执行直到结束
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("main over");
    }
}

class MyRunnable7 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

第十二章、线程安全

一、概述

        以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。

        最重要的是:要知道,你编写的程序需要放到一个多线程的环境下执行,更需要关注的是这些数据在多线程并发的环境下是否是安全的。

1、多线程并发条件下数据存在安全问题的条件

条件1:多线程并发

条件2:有共享数据

条件3:共享数据有修改的行为

满足以上三个条件之后,就会存在线程安全问题

2、局部变量和常量永远不可能存在线程安全问题

java中的三大变量

  • 实例变量:在堆中
  • 静态变量:在方法区中
  • 局部变量:在栈中

        以上三大变量中,局部变量永远都不会存在线程安全问题。因为局部变量不共享(一个线程一个栈),局部变量在栈中,所以局部变量永远都不会共享

实例变量在堆中,堆只有一个

静态变量在方法区中,方法区只有一个

所以堆和方法区都是多线程共享的,可能存在线程安全问题

局部变量和常量:永远不会有现成安全问题

成员变量:可能会存在线程安全问题

3、线程同步机制:解决线程安全问题

        当多线程并发的环境下,有共享数据,并且这个共享数据还会被修改,此时就会存在线程安全问题。

解决线程安全问题:

线程排队执行(不能并发)

用排队执行解决线程安全问题,这种机制被称为:线程同步机制

专业数据叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行

线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全是第一位,只有数据安全了,我们才可以谈效率,数据不安全,没有效率的事儿

二、线程同步

1、异步编程模型

异步就是并发

线程t1和线程t2,各自执行各自的,t1不管t2,t2也不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率较高)

2、同步编程模型

同步就是排队

线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低,线程排队执行。

三、synchronized详解(排他锁)

1、synchronized同步代码块

synchronized(){

        //线程同步代码块

}

synchronized后面小括号中传的这个“数据”是相当关键的

这个数据必须是多线程共享的数据,才能达到多线程排队

2、synchronized后小括号实参详解

()中写什么?

那要看你想让哪些线程同步。

假设 t1,t2,t3,t4,t5,有5个线程

你只希望t1,t2,t3排队,t4,t5不需要排队,怎么办?

  • 一定要在()中写一个t1,t2,t3共享的对象,而这个对象对于t4,t5来说不是共享的
  • 共享对象的实例变量(必须是对象)也可以作为多线程共享的对象
  • 当共享对象传入字符串时,因为字符串对象是在字符串常量池中,只有一个,也是共享对象,并且是多有线程所共享的

3、synchronized代码块执行原理(对象锁,类锁)

对象锁:一个对象能调用的代码块只有一把锁

synchronized既可以给一个线程类的多个线程对象的同一个共享对象上锁

也可以给多个线程类的多个线程对象的同一个共享对象上锁

        在java语言中,synchronized关键字相当于给每个java对象调用的那一部分代码块都上了“一把锁”,其实这把锁就是标记(只是吧它叫做锁)

100个对象,100把锁。1个对象1把锁

注意:

1、可理解为哪个线程有该对象的对象锁,表示该线程拥有控制该对象来调用那一部分代码块执行的权利,其他线程只能在锁池中等待

2、JVM只是一个进程,一个进程有多个线程,事实上同一时间内只能有一个线程在工作,因为CPU是有一个核为JVM进程服务,多个线程只能通过抢夺CPU资源来工作,由于速度很快,给人感觉像是同时在工作。synchronized可以可以通过锁机制实现真正意义上的同一时间内只能有一个线程在工作,因为其他线程会在锁池排队。

3、在有线程有锁的前提下,可以有就绪,运行,阻塞多种状态,没有锁,就只能有一种状态,就是锁池(阻塞状态)。

  • 1、假设t1和t2线程并发,开始执行相同代码的时候,肯定会有一个先一个后。
  • 2、假设t1先执行了,遇到了synchronized,这个时候自动找“共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块执行结束,这把锁才会释放
  • 3、假设t1已经占有这把锁,此时t2也遇到了synchronized关键字 ,也会去占有共享对象的这把锁,但是这把锁已经被t1线程占有了,t2只能在同步代码块外面等待t1线程执行结束,直到t1把同步代码块执行结束了,t1才会归还这把锁,此时t2终于等到了这把锁,然后t2线程占有这把锁之后,进入同步代码块执行程序。

这样就达到了线程排队执行的目的

这里需要注意的是:这个共享对象一定要选好了,这个共享对象一定是你需要排队执行的线程所共享的。

4、synchronized直接修饰实例方法

synchronized出现在实例方法上,一定锁的是this,只能是this不能是其他对象,这种方式不灵活

缺点:

synchronized出现在实例方法上,表示整个方法都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低,所以这种方法不常用

优点:

代码写的少了,节俭了

注意:如果共享的对象就是this,并且需要同步的代码块就是整个方法体,建议使用这种方式

如果使用局部变量的话:

建议使用StringBuilder,因为局部变量不存在线程安全问题,选择StringBuilder。StringBuffer效率比较低

ArrayList是非线程安全的

Vector是线程安全的

HashSet,HashMap是非线程安全的

Hashtable是线程安全的

5、synchronized的三种写法

  • 第一种:同步代码块,灵活

synchronized(线程共享对象){

//同步代码块

}

  • 第二种:在实例方法上使用synchronized,简单

表示共享对象一定是this

并且同步代码块是整个方法体

  • 第三种:在静态方法上使用synchronized

类锁:一个类能调用的代码块的类锁只有一把,不论有多少个对象

表示类锁,类锁永远只有一把,就算创建100个对象,类锁也只有一把

对象锁:1个对象1把锁,100个对象100把锁

类锁:100个对象,也只有1把类锁

6、面试题

package com.javase.多线程.模拟线程安全.synchronized详解.面试题;

/**
 * 面试题1:doOther方法没有synchronized,doSome方法有synchronized,则在执行的时候需要等待doSome方法执行结束吗?
 * 不需要,因为doOther方法没有synchronized,没有锁
 *
 * 面试题2:doOther方法有synchronized,doSome方法有synchronized,则在执行的时候需要等待doSome方法执行结束吗?
 * 需要,因为一个对象只有一把锁,都有synchronized,说明把这些代码块都用一把锁锁到一块了
 *
 * 面试题3:doOther方法有synchronized,doSome方法有synchronized,则在执行的时候需要等待doSome方法执行结束吗?
 * 不需要,因为是2个MyClass对象,有两把锁,不影响
 *
 * 面试题4:doOther方法有synchronized,static,doSome方法有synchronized,static,则在执行的时候需要等待doSome方法执行结束吗?
 * 需要,因为synchronized修饰的静态方法表示类锁,类级别能调用的代码块只有一把锁,不论有多少个实例对象
 */
public class Exam1 {
    public static void main(String[] args) {

        //以下为面试题1和面试题2
//        MyClass mc = new MyClass();
//        Thread t1 = new Thread(new MyThread(mc));
//        Thread t2 = new Thread(new MyThread(mc));

        //面试题3
        MyClass mc1 = new MyClass();
        MyClass mc2 = new MyClass();

        Thread t1 = new Thread(new MyThread(mc1));
        Thread t2 = new Thread(new MyThread(mc2));

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        try {
            //睡眠保证t1线程先执行
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

class MyThread implements Runnable{
    private MyClass mc;

    public MyThread(MyClass mc) {
        this.mc = mc;
    }

    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("t1")){
            this.mc.doSome();
        }

        if(Thread.currentThread().getName().equals("t2")){
            this.mc.doOther();
        }
    }
}
class MyClass{
    //synchronized出现在实例方法上,表示锁this

    //以下为面试题1,2,3
//    public synchronized void doSome(){
//        System.out.println("doSome begin");
//        try {
//            Thread.sleep(1000 * 10);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
//        System.out.println("doSome over");
//    }
//
//    public synchronized void doOther(){
//        System.out.println("doOther begin");
//        System.out.println("doOther over");
//
//    }

    //synchronized出现在静态方法上是类锁:锁住类级别调用的代码块
    public synchronized static void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    public synchronized static void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

四、银行账户取款现实线程安全

1、不使用线程同步,线程不安全

Account.java

package com.javase.多线程.模拟线程安全.不使用线程同步机制;

/**
 * 银行账户类
 * 不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题
 */
public class Account {
    //账号
    private String actno;

    //余额
    private double balance;

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款的方法
    public void withdraw(double money){
        //t1和t2并发执行这个方法。。。(t1和t2是两个栈,两个栈操作堆中同一个对象)
        //取款之前的余额
        double before = this.getBalance(); //10000
        //取款之后的余额
        double after = before - money;

        //模拟网络延迟
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //更新余额
        //思考:t1线程执行到这里了,但还没有来得及执行这行代码,此时t2线程进入withdraw方法了,此时一定出问题
        this.setBalance(after);
    }
}

AccountThread.java

package com.javase.多线程.模拟线程安全.不使用线程同步机制;

public class AccountThread implements Runnable {
    //两个线程必须共享同一个账户
    private Account act;

    //通过构造方法传递过来账户对象
    public AccountThread(Account act) {
        this.act = act;
    }

    @Override
    public void run() {
        //run方法执行表示取款操作
        //假设取款5000
        double money = 5000;
        //取款
        //多线程并发执行这个方法
        this.act.withdraw(money);
        System.out.println(Thread.currentThread().getName() + "对" + this.act.getActno() + "取款" + money + "成功,余额:" + this.act.getBalance());
    }
}

Test.java

package com.javase.多线程.模拟线程安全.不使用线程同步机制;

public class Test {
    public static void main(String[] args) {
        //创建账户对象(只创建1个)
        Account account = new Account("act-001",10000);

        //创建两个线程
        Thread t1 = new Thread(new AccountThread(account));
        Thread t2 = new Thread(new AccountThread(account));

        //设置name
        t1.setName("t1");
        t2.setName("t2");

        //启动线程
        t1.start();
        t2.start();
    }
}

2、使用线程同步,线程安全

类似于取款操作,只有对同一个账户进行操作时,多线程才需要等待,对不同的账户进行操作,则不需要等待。

Account.java

package com.javase.多线程.模拟线程安全.使用线程同步机制;

/**
 * 银行账户类
 * 使用线程同步机制,解决线程安全问题
 */
public class Account {
    //账号
    //只要是引用数据类型,java对象,也都可以作为共享对象的子共享对象
    private String actno;

    //余额
    private double balance;

    //定义一个对象
    //实例变量。Account对象时对线程共享的,Account对象中的实例变量obj也是多线程共享的
    Object obj = new Object();

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款的方法
    public void withdraw(double money){
        //以下这几行代码必须是线程排队的,不能并发
        //一个线程把这里的代码全部执行结束之后,另一个线程才能进来

        /**
         * 这里的共享对象时:账户对象
         * 账户对象是共享的,那么this就是账户对象
         * 不一定是this,只要是多线程共享的那个对象就行
         *
         * 以下代码的执行原理:
         *
         * 在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记(只是吧它叫做锁)
         *
         * 100个对象,100把锁。1个对象1把锁
         *
         * 1、假设t1和t2线程并发,开始执行相同代码的时候,肯定会有一个先一个后。
         *
         * 2、假设t1先执行了,遇到了synchronized,这个时候自动找“共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块执行结束,这把锁才会释放
         *
         * 3、假设t1已经占有这把锁,此时t2也遇到了synchronized关键字 ,也会去占有共享对象的这把锁,但是这把锁已经被t1线程占有了,t2只能在同步代码块外面等待t1线程执行结束,直到t1把同步代码块执行结束了,t1才会归还这把锁,此时t2终于等到了这把锁,然后t2线程占有这把锁之后,进入同步代码块执行程序。
         *
         * 这样就达到了线程排队执行的目的
         *
         * 这里需要注意的是:这个共享对象一定要选好了,这个共享对象一定是你需要排队执行的线程所共享的。
         */
        //局部变量不是共享对象
//        Object object = new Object();
//        synchronized (this){ //此时的this对t1和t2来说是共享的,t1和t2会排队,t3和t4是共享的,所以t3和t4会排队
//        synchronized ("abc"){ //因为“abc”在字符串常量池中,也算共享对象,并且所有线程都会同步
//
//        Object o = null;
//        synchronized (o){ //这个不行,会NullPointException

//        synchronized (actno){

//        synchronized (obj){

//        synchronized (object){  //这样编写就不安全了,因为object不是共享对象
            double before = this.getBalance(); //10000
            //取款之后的余额
            double after = before - money;

            //模拟网络延迟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //更新余额
            this.setBalance(after);
//        }
    }
}

AccountThread.java

package com.javase.多线程.模拟线程安全.使用线程同步机制;

public class AccountThread implements Runnable {
    //两个线程必须共享同一个账户
    private Account act;

    //通过构造方法传递过来账户对象
    public AccountThread(Account act) {
        this.act = act;
    }

    @Override
    public void run() {
        //run方法执行表示取款操作
        //假设取款5000
        double money = 5000;
        //取款
        //多线程并发执行这个方法

//        synchronized (this){ //这里的this是指线程对象,这个对象不共享
        synchronized (act){ //这种方式也可以,只不过扩大了同步的范围,效率更低了
            this.act.withdraw(money);

        }
        System.out.println(Thread.currentThread().getName() + "对" + this.act.getActno() + "取款" + money + "成功,余额:" + this.act.getBalance());
    }
}

Test.java

package com.javase.多线程.模拟线程安全.使用线程同步机制;

public class Test {
    public static void main(String[] args) {
        //创建账户对象(只创建1个)
        Account account = new Account("act-001",10000);

        //创建两个线程
        Thread t1 = new Thread(new AccountThread(account));
        Thread t2 = new Thread(new AccountThread(account));

        Account account1 = new Account("act-002",20000);
        Thread t3 = new Thread(new AccountThread(account1));
        Thread t4 = new Thread(new AccountThread(account1));

        //设置name
        t1.setName("t1");
        t2.setName("t2");

        t3.setName("t3");
        t4.setName("t4");

        //启动线程
        t1.start();
        t2.start();

        t3.start();
        t4.start();
    }
}

五、死锁(deadlock)

1、死锁现象

2、案例

synchronized在开发中最好不要嵌套使用,很有可能导致死锁现象的发生

package com.javase.多线程.死锁;

/**
 * 死锁代码要会写
 * 一般面试官要求你会写
 * 只有会写的,才会在以后得开发中注意这个事儿
 * 因为死锁很难调试
 * 
 * synchronized在开发中最好不要嵌套使用,很有可能导致死锁现象的发生
 */
public class DeadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        //t1和t2两个线程共享o1和o2
        Thread t1 = new Thread(new MyThread1(o1,o2));
        Thread t2 = new Thread(new MyThread2(o1,o2));

        t1.start();
        t2.start();

    }
}

class MyThread1 implements Runnable{
    Object o1;
    Object o2;

    public MyThread1(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        synchronized (o1){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){

            }
        }
    }
}

class MyThread2 implements Runnable{
    Object o1;
    Object o2;

    public MyThread2(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        synchronized (o2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){

            }
        }
    }
}

六、解决线程安全问题的原则

不能一上来就是利用synchronized,这样会让程序的执行效率降低,用户体验不好。系统的用户吞吐量降低,用户体验差。在不得已的情况下再选择synchronized线程同步机制

1、第一种方案

尽量使用局部变量代替“实例变量和静态变量”

2、第二种方案

如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应一个对象,100个线程就对应100个对象,对象不共享,就没有数据安全问题了)

3、第三种方案

如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了,线程同步机制

第十三章、守护线程

一、概述

        在 Java 中,守护线程(Daemon Thread)是一种特殊类型的线程,其生命周期依赖于其他非守护线程。当所有非守护线程都执行完毕时,守护线程会自动结束,无论其执行状态

java语言中的线程分为两大类:

一类是:用户线程(非守护线程)

一类是:守护线程(后台线程)

守护线程的特点:

一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束

注意:主线程main方法是一个用户线程

守护线程使用在什么地方?

例如每天00:00的时候系统自动备份,这个时候需要使用到定时器,并且我们可以将定时器设置为守护线程,一直在那里看着,每到00:00的时候就备份一次,所有的用户线程如果结束了,守护线程就会自动退出,没有必要在进行数据备份了。

1、守护进程和守护线程

        守护进程(daemon)是在计算机系统后台运行的一种进程,通常独立于控制终端并在系统启动时启动。它们执行各种任务,如服务请求、处理输入/输出请求等。守护进程没有用户交互界面,通常在系统启动时启动,并持续运行以提供特定的功能或服务,例如网络服务(如 web 服务器、数据库服务器等)。
        守护线程是类似概念的概念,但它们是在程序内部运行的线程,而不是独立的进程。它们也是在后台执行任务,不需要用户交互,并且可以在应用程序运行期间启动和停止。它们用于执行诸如后台任务处理、定时任务等工作,而不会阻塞主线程或影响用户界面的响应性。
总体而言,守护进程和守护线程都是在后台运行的实体,用于执行某些特定任务,但它们的执行环境和作用范围有所不同。

2、守护线程的开始和结束

        在 Java 中,守护线程(Daemon Thread)是通过 setDaemon(true) 方法设置为守护线程的线程。它们通常被用于在程序运行时执行一些任务,但并不阻止 JVM(Java 虚拟机)退出。
当所有的非守护线程结束时,JVM 会检查是否存在任何守护线程。如果只剩下守护线程,JVM 将会自动退出,即使守护线程尚未执行完任务。因此,守护线程的生命周期取决于是否存在正在运行的非守护线程。
        守护线程在程序运行期间可以随时创建,但在程序退出时,如果只剩下守护线程,它们将被强制结束。这意味着守护线程无法完成其任务,因为它们依赖于非守护线程的存在来保持 JVM 的运行。

二、创建守护线程

        要创建守护线程,可以通过 Thread 类的 setDaemon(true) 方法将线程设置为守护线程。这个方法必须在线程启动之前调用,否则会抛出 IllegalThreadStateException 异常。

Thread daemonThread = new Thread(runnable);
daemonThread.setDaemon(true);
daemonThread.start();

三、特性和用途

守护线程具有以下特性和用途:

1、后台服务支持

        守护线程通常用于支持后台服务或任务。例如,在 Java 中,垃圾回收器(Garbage Collector)就是一个守护线程,它在后台自动回收不再使用的内存。

2、资源管理

        守护线程可以用于监控和管理资源。例如,一个守护线程可以定期清理临时文件或关闭不再使用的连接。

3、非关键任务

        守护线程通常用于执行一些非关键的、可选的任务,这些任务的执行对于程序的核心功能不是必需的。

四、生命周期

        守护线程的生命周期与其他非守护线程的生命周期有所不同。当所有非守护线程都执行完毕时,JVM 将会自动关闭所有守护线程,然后程序终止。这意味着守护线程的执行时间不确定,可能在任何时候被中断。

五、注意事项

在使用守护线程时,需要注意以下几点:

  • 守护线程通常不应该访问共享资源或执行关键任务,因为无法保证其执行的可靠性和完整性。
  • 守护线程中的异常不会被捕获和处理,因此需要确保守护线程的代码正确且不会抛出致命错误。
  • 守护线程的优先级较低,因此在与其他线程竞争CPU资源时,可能会受到其他线程的优先调度。

总结: 守护线程是一种特殊类型的线程,其生命周期依赖于其他非守护线程。当所有非守护线程执行完毕时,守护线程会自动结束。它通常用于支持后台服务、资源管理和执行非关键任务。在使用守护线程时,需要注意其执行时间的不确定性和对共享资源的访问限制。

六、模拟守护线程

package com.javase.多线程.守护线程;

/**
 * 守护线程
 */
public class 守护线程 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new BakDataThread());
        t1.setName("数据备份的守护线程");

        //启动现成之前将t1线程设置为守护线程
        t1.setDaemon(true);

        t1.start();

        //main线程是用户线程
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class BakDataThread implements Runnable{

    @Override
    public void run() {
        int i = 0;
        //即使是死循环,但又与该线程是守护线程,当用户线程结束后,守护线程自动终止
        while (true){
            System.out.println(Thread.currentThread().getName() + "-->" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

第十四章、定时器

一、概述

        在 Java 中,定时器(Timer)是一种用于在指定时间间隔执行任务的工具类。它允许开发者安排任务在未来的某个时间点或以固定的时间间隔重复执行。

1、定时器的作用

间隔特定的时间,执行特定的任务

例如:

每周要进行银行账户的总账操作

每天要进行数据的备份操作

2、实现定时器的多种方式

在实际的开发中,每个多久执行一段特定的程序,这种需求是很常见的。

在java中可以采用多种方式实现:

  • 第一种方式:
    • 使用Thread.sleep()方法,设置睡眠时间,每到这个时间点醒来,执行任务,这种方式是最原始的定时器(比较low)
  • 第二种方式:
    • 使用java.util.Timer类,可以直接使用,不过这种方式在目前的开发中也很少用,因为现在有很多高级框架都支持定时任务的
  • 第三种方式:
    • 在实际开发中,目前使用比较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。

二、使用定时器

        要使用定时器,需要使用 Java 提供的 java.util.Timer 类。定时器可以执行多个任务,并且可以按照不同的调度策略进行安排。

1、案例一

import java.util.Timer;
import java.util.TimerTask;

public class TimerExample {
    public static void main(String[] args) {
        Timer timer = new Timer();
        
        TimerTask task = new TimerTask() {
            public void run() {
                // 定时任务的具体逻辑
                System.out.println("Timer task executed.");
            }
        };
        
        // 安排任务在指定延迟后执行
        timer.schedule(task, 5000); // 5秒后执行
        
        // 终止定时器
        // timer.cancel();
    }
}

2、案例二

package com.javase.多线程.定时器;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 使用定时器指定定时任务
 */
public class TimerTest {
    public static void main(String[] args) {
        //创建定时器对象
        Timer timer = new Timer();
//        Timer timer = new Timer(true); //守护线程的方式

        //指定定时任务
        //获取时间
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        Date firstTime = null;
        try {
            firstTime = simpleDateFormat.parse("2023-07-03 21:40:00");
        } catch (ParseException e) {
            e.printStackTrace();
        }

//        timer.schedule(new LogTimeTask(),firstTime,1000 * 10);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                //采用匿名内部类方式
            }
        }, firstTime, 1000 * 10);

    }
}

//定时任务类
//假设这是一个记录日志的定时任务
//class LogTimeTask extends TimerTask{
//
//    @Override
//    public void run() {
//        //编写需要执行的任务
//        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        String strTime = simpleDateFormat.format(new Date());
//        System.out.println(strTime + ":成功完成了一次数据备份!");
//    }
//}

三、定时器任务

        定时器任务(Timer Task)是一个实现了 java.util.TimerTask 抽象类的具体任务。开发者需要通过继承 TimerTask 并实现 run() 方法来定义定时任务的具体逻辑。

四、调度策略

定时器可以按照不同的调度策略安排任务的执行,常见的调度策略有以下两种:

  • schedule(TimerTask task, Date time):在指定的时间点执行任务。
  • schedule(TimerTask task, long delay):在指定的延迟时间后执行任务。

此外,定时器还提供了其他调度策略,例如:

  • schedule(TimerTask task, long delay, long period):在指定的延迟时间后开始执行任务,并在每次执行后按指定的时间间隔重复执行。
  • scheduleAtFixedRate(TimerTask task, long delay, long period):在指定的延迟时间后开始执行任务,并按指定的时间间隔重复执行,无论每次任务的执行时间长短。

五、注意事项

在使用定时器时,需要注意以下几点:

  • 定时器是单线程的,如果一个任务的执行时间过长,可能会影响其他任务的执行。
  • 定时器不适用于需要高精度和高并发的定时任务,对于这些场景,可以考虑使用 ScheduledExecutorService 类。
  • 定时器的任务执行时间应尽量短,避免阻塞定时器线程。

        总结: 定时器(Timer)是 Java 中用于安排任务在指定时间间隔执行的工具类。通过 Timer 类和 TimerTask 抽象类,可以创建定时任务并按照不同的调度策略进行安排。在使用定时器时,需要注意任务的执行时间和对定时器线程的影响

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值