原创

    Java 基础教程【五】

    内容概要:

    File 类

    File 概述

    java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。具体功能如下:

    具体功能:
    创建一个文件/文件夹 删除文件/文件夹 获取文件/文件夹 判断文件/文件夹是否存在 对文件夹进行遍历 获取文件的大小

    输出路径分隔符和文件名称分隔符的代码如下:

    public static void main(String[] args) {
        // 01-路径分隔符【例如环境变量里的分隔符】 windows系统使用“分号”,linux系统使用“冒号”
        String pathSeparator = File.pathSeparator;
        System.out.println(pathSeparator);
    
        char pathSeparatorChar = File.pathSeparatorChar;
        System.out.println(pathSeparatorChar);
    
        // 02-文件名称分隔符  windows系统使用反斜杠\  linux系统使用正斜杠/
        String separator = File.separator;
        System.out.println(separator);
    
        char separatorChar = File.separatorChar;
        System.out.println(separatorChar);
    }
    
    

    构造方法

    构造方法:
    public File(String pathname) :通过将给定的路径名字符串转换为抽象路径名来创建新的 File 实例。 public File(String parent, String child) :从父路径名字符串和子路径名字符串创建新的 File 实例。 public File(File parent, String child) :从父抽象路径名和子路径名字符串创建新的 File 实例。

    构造方法代码如下:

    public static void main(String[] args) {
        // 01-public File(String pathname)
        File file01 = new File("F:\\00-BLOG-HOME\\themes\\icarus-latest\\layout\\widget");
        System.out.println(file01);    // 重写了 toString 方法,否则会打印内存地址
    
    
        // 02-public File(String parent, String child)
        File file02 = new File("F:\\", "00-BLOG-HOME");
        System.out.println(file02);
    
    
        // 03-public File(File parent, String child)
        File parent = new File("F:\\");
        File file03 = new File(parent, "00-BLOG-HOME");
        System.out.println(file03);
    }
    

    小贴士: 一个 File 对象代表硬盘中实际存在的一个文件或者目录,无论该路径下是否存在文件或者目录,都不影响 File 对象的创建。

    常用方法

    获取功能的方法

    获取功能的方法
    public String getAbsolutePath() :返回此 File 的绝对路径名字符串。 public String getPath() :将此 File 转换为路径名字符串。 public String getName() :返回由此 File 表示的文件或目录的名称。 public long length() :返回由此 File 表示的文件的长度。

    方法演示,代码如下:

    public static void main(String[] args) {
        // 01-public String getAbsolutePath()
        File file01 = new File("F:\\00-BLOG-HOME\\themes\\icarus-latest\\layout\\widget");
        System.out.println(file01.getAbsolutePath());
    
        File file02 = new File("00-BLOG-HOME");
        System.out.println(file02.getAbsolutePath());
    
    
        // 02-public String getPath()
        System.out.println(file01.getPath());
        System.out.println(file02.getPath());
    
        System.out.println(file01);
        System.out.println(file01.toString());   // 看源码知:toString 调用的就是 getPath() 方法
    
    
        // 03-public String getName() ,获取的就是构造方法传递路径的结尾部分(文件/文件夹)
        System.out.println(file01.getName());
        System.out.println(file02.getName());
    
    
        /**
         * 04-public long length()  :返回由此 File 表示的文件的长度。获取的是构造方法指定的文件的大小,以字节为单位。
         * 注意 : 1、文件夹是没有大小概念的,不能获取文件夹的大小。2、如果构造方法中给出的路径不存在,那么length方法返回0
         */
        // 获取文件大小,不存在返回 0
        File file04 = new File("F:\\00-BLOG-HOME\\source\\about\\index.md");
        System.out.println(file04.length());
    
        // 获取文件夹大小,直接返回 0 ,因为文件夹没有大小,只有文件有大小
        File file05 = new File("F:\\00-BLOG-HOME\\source\\about");
        System.out.println(file05.length());
    }
    

    判断功能的方法

    判断功能的方法
    public boolean exists() :此 File 表示的文件或目录是否实际存在。 public boolean isDirectory() :此 File 表示的是否为目录。 public boolean isFile() :此 File 表示的是否为文件。

    方法演示,代码如下:

    public static void main(String[] args) {
        // 01-public boolean exists() :用于判断构造方法中的路径是否存在
        File file01 = new File("F:\\00-BLOG-HOME\\themes\\icarus-latest\\layout\\widget");
        System.out.println(file01.exists());
    
        File file02 = new File("00-BLOG-HOME");
        System.out.println(file02.exists());
    
    
        /**
         * 02-public boolean isDirectory() :用于判断构造方法中给定的路径是否以文件夹结尾
         * 03-public boolean isFile() :用于判断构造方法中给定的路径是否以文件结尾
         * 注意 : 1、电脑的硬盘中只有文件/文件夹,两个方法是互斥。2、这两个方法使用前提,路径必须是存在的,否则都返回 false
         */
        if(file01.exists()){
            System.out.println(file01.isDirectory());
            System.out.println(file01.isFile());
    
            System.out.println(file02.isDirectory());
            System.out.println(file02.isFile());
        }
    }
    

    创建和删除功能的方法

    创建和删除功能的方法
    public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。 public boolean delete() :删除由此 File 表示的文件或目录。 public boolean mkdir() :创建由此 File 表示的目录。 public boolean mkdirs() :创建由此 File 表示的目录,包括任何必需但不存在的父目录。

    方法演示,代码如下:

    public static void main(String[] args) throws IOException {
        //demo01();
        //demo02();
        //demo03();
    }
    
    private static void demo03() {
        // 04-public boolean delete() :可以删除构造方法路径中给出的文件/文件夹
        // 注意:文件/文件夹删除成功,返回 true 。2、文件夹中有内容,不会删除返回 false ; 构造方法中路径不存在 false 。
        // 注意:delete 方法是直接在硬盘删除文件/文件夹,不走回收站,删除要谨慎。
        File file05 = new File("F:\\home\\111");
        System.out.println(file05.delete());
    }
    
    private static void demo02() {
        // 02-public boolean mkdir() :创建单级空文件夹
        File file03 = new File("F:\\home\\啊啊啊");
        System.out.println(file03.mkdir());
    
        // 03-public boolean mkdirs() :既可以创建单级空文件夹,也可以创建多级文件夹,创建文件夹的路径和名称在构造方法中给出(构造方法的参数)
        File file04 = new File("F:\\home\\111\\啊啊啊");
        System.out.println(file04.mkdirs());
    
        // 注意 : 1.此方法只能创建文件夹,不能创建文件。2、文件夹不存在,创建文件夹,返回 true 。
        // 3、文件夹存在,不会创建,返回 false;构造方法中给出的路径不存在返回 false
    }
    
    private static void demo01() throws IOException {
        // 01-public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。创建文件的路径和名称在构造方法中给出(构造方法的参数)
        // 注意 : 1、此方法只能创建文件,不能创建文件夹。2、创建文件的路径必须存在,否则会抛出异常。3、文件不存在,创建文件,返回 true ,文件存在,不会创建,返回 false 。
        File file01 = new File("F:\\home\\01.txt");
        System.out.println(file01.createNewFile());
    
        File file02 = new File("F:\\blog-home\\01.txt");
        System.out.println(file02.createNewFile());    // 路径不存在,抛异常:java.io.IOException: 系统找不到指定的路径。
    }
    

    目录的遍历

    详情看代码:

    public static void main(String[] args) {
        // 01-public String[] list() :返回一个 String 数组,表示该 File 目录中的所有子文件或目录。
        File file01 = new File("F:\\home");
        String[] list = file01.list();
        for (String str : list) {
            System.out.println(str);
        }
    
        // 02-public File[] listFiles() :返回一个 File 数组,表示该 File 目录中的所有的子文件或目录。
        File file02 = new File("F:\\home");
        File[] listFiles = file02.listFiles();
        for (File file : listFiles) {
            System.out.println(file);
        }
    
        // 03-注意事项:
        // 1、list 方法和 listFiles 方法遍历的是构造方法中给出的目录,如果构造方法中给出的目录的路径不存在,会抛出空指针异常。
        // 2、如果构造方法中给出的路径不是一个目录,也会抛出空指针异常。
    
    }
    

    调用 listFiles 方法的 File 对象,表示的必须是实际存在的目录,否则返回 null,无法进行遍历。

    递归

    递归概述

    递归: 指在当前方法内调用自己的这种现象。 递归的分类: 递归分为两种,直接递归和间接递归。 直接递归: 方法自身调用自己。 间接递归: 可以理解为 A 方法调用 B 方法,B 方法调用 C 方法,C 方法调用 A 方法。 注意事项: 1、递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。 2、在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。 3、构造方法,禁止递归。

    递归分析图解:

    public static void main(String[] args) {
        recursion(1);
    }
    
    private static void recursion(int i) {
        // 添加一个递归结束的条件,i == 500 的时候结束
        // 如果此递归没有结束条件,会报异常:Exception in thread "main" java.lang.StackOverflowError
        if(i > 500){
            return;    // 结束方法
        }
        System.out.println(i);
        recursion(++i);
    }
    

    递归累加求和

    public static void main(String[] args) {
        int sum = sum(100);
        System.out.println(sum);
    }
    
    public static int sum(int n) {
        if (n == 1) {
            return 1;
        }
        return n + sum(n - 1);
    }
    

    递归求阶乘

    public static void main(String[] args) {
            long mul = mul(10);
            System.out.println(mul);
        }
    
        public static long mul(int n){
            if(n == 1){
                return 1;
            }
            return n * mul(n - 1);
        }
    

    递归打印多级目录

    // 定义一个变量,记录文件数目
    static int count = 0;
    public static void main(String[] args) {
        File file = new File("F:\\home");
        getFiles(file);
    }
    
    public static void getFiles(File file){
        File[] list = file.listFiles();
        for (File s : list) {
            if(s.isDirectory()){
                getFiles(s);
            }
            System.out.println(s);
            System.out.println(++count);
        }
    }
    

    文件搜索

     // 定义一个变量,记录文件数目
    static int count = 0;
    
    public static void main(String[] args) {
        File file = new File("F:\\home");
        getFiles(file);
    }
    
    public static void getFiles(File file) {
        File[] list = file.listFiles();
        for (File s : list) {
            if (s.isDirectory()) {
                getFiles(s);
            } else {
                if (s.toString().endsWith(".xml")) {
                    System.out.println(s);
                    System.out.println(++count);
                }
            }
        }
    }
    

    文件搜索之过滤器

    文件过滤器:
    1、java.io.FileFilter 是一个接口,是 File 的过滤器。该接口的对象可以传递给 File 类的 listFiles(FileFilter) 作为参数。 2、接口中只有一个方法 boolean accept(File pathname) ,测试 pathname 是否应该包含在当前 File 目录中,符合则返回 true。
    // 定义一个变量,记录文件数目
    static int count = 0;
    
    public static void main(String[] args) {
        File file = new File("F:\\home");
        getFiles(file);
    }
    
    public static void getFiles(File file) {
        File[] list = file.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                if(pathname.isDirectory()){
                    return true;
                }
                return pathname.getName().toLowerCase().endsWith(".xml");
            }
        });
        for (File s : list) {
            if (s.isDirectory()) {
                getFiles(s);
            } else {
                if (s.toString().endsWith(".xml")) {
                    System.out.println(s);
                    System.out.println(++count);
                }
            }
        }
    }
    

    过滤器之 Lambda 优化

     // 定义一个变量,记录文件数目
        static int count = 0;
    
    public static void main(String[] args) {
        File file = new File("F:\\home");
        getFiles(file);
        System.out.println("\n匹配到的合适的文件个数:" + count + "个");
    }
    
    public static void getFiles(File file) {
        // 使用 Lambda 表达式优化了匿名内部类
        File[] list = file.listFiles(pathname-> pathname.isDirectory() || pathname.getName().toLowerCase().endsWith(".xml"));
        for (File s : list) {
            if (s.isDirectory()) {
                getFiles(s);
            } else {
                if (s.toString().endsWith(".xml")) {
                    System.out.println(s);
                    count++;
                }
            }
        }
    }
    

    IO 流概述

    什么是 IO 流

    当你电脑上插入一个U盘,可以把一个视频,拷贝到你的电脑硬盘里。我们把这种数据的传输,可以看做是一种数据的流动,称之为 IO 流 。按照流动的方向,以内存为基准,分为 输入流 input输出流 output ,即流向内存是输入流,流出内存的输出流。如下草图:

    Java 中 IO 操作主要是指使用 java.io 包下的内容所进行的输入、输出操作。输入 也叫做读取数据,输出 也叫做作写出数据。

    IO 流的分类

    根据数据的流向分:
    输入流 :把数据从其他设备上读取到内存中的流。 输出流 :把数据从内存中写出到其他设备上的流。
    根据数据的类型分:
    字节流 :以字节为单位,读写数据的流。 字符流 :以字符为单位,读写数据的流。

    根据分类不同,IO 流所对应的类也有所不同,如下表:

    输入流输出流
    字节流字节输入流
    InputStream
    字节输出流
    OutputStream
    字符流字符输入流
    Reader
    字符输出流
    Writer

    IO 流的简单介绍就到这里了,下面将详细介绍 Java 中的 IO 流。请往下阅读 ???

    字节流

    一切皆为字节

    一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都是一个一个的字节,那么在传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。

    字节输出流

    java.io.OutputStream 抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。

    字节输出流的通用方法:
    public void close() :关闭此输出流并释放与此流相关联的任何系统资源。 public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。 public void write(byte[] b) :将 b.length 字节从指定的字节数组写入此输出流。 public void write(byte[] b, int off, int len) :从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流。 public abstract void write(int b) :将指定的字节输出流。

    FileOutputStream 类

    OutputStream 有很多子类,我们从最简单的一个子类 java.io.FileOutputStream 开始。这个类是文件输出流,用于将数据写出到文件,也就是从内存把数据写出到硬盘上。

    写出数据的原理(内存 --> 硬盘):
    java 程序 --> JVM(java 虚拟机) --> OS(操作系统) --> OS 调用写数据的方法 --> 把数据写入到文件中
    字节输出流的使用步骤(重点):
    1、创建一个 FileOutputStream 对象,构造方法中传递写入数据的目的地 2、调用 FileOutputStream 对象中的方法 write ,把数据写入到文件中 3、释放资源(IO 流操作会占用一定的内存,使用完毕要把内存清空,提供程序的效率)

    构造方法

    构造方法:
    public FileOutputStream(File file) : 创建文件输出流以写入由指定的 File 对象表示的文件。 public FileOutputStream(String name) : 创建文件输出流以指定的名称写入文件。

    当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。上述两种构造方法举例,代码如下:

    public static void main(String[] args) throws Exception {
        // 01-创建文件对象
        File file = new File("F:\\home\\02.txt");
    
        // 02-创建输出流对象的第一种构造方法
        FileOutputStream fos = new FileOutputStream(file);
    
        // 03-创建输出流对象的第二种构造方法
        FileOutputStream fileOutputStream = new FileOutputStream("F:\\home\\03.txt");
    
        // 04-关闭流
        fileOutputStream.close();
        fos.close();
    }
    

    写出字节数据

    通过 write(int b) 方法,每次可以写出一个字节数据,代码使用演示:

    public static void main(String[] args) throws IOException {
        // 01-使用文件名称创建流对象
        FileOutputStream fos = new FileOutputStream("F:\\home\\fos.txt");
    
        // 02-写出数据
        fos.write(97);    // 写出第1个字节
        fos.write(98);    // 写出第2个字节
        fos.write(99);    // 写出第3个字节
    
        // 03-关闭资源
        fos.close();
    }
    
    // 输出结果 :abc
    

    小贴士:

    1、虽然参数为 int 类型四个字节,但是只会保留一个字节的信息写出。 2、流操作完毕后,必须释放系统资源,调用 close 方法,千万记得。

    写出字节数组

    通过 write(byte[] b) 方法,每次可以写出数组中的数据,代码使用演示:

    public static void main(String[] args) throws Exception {
    public static void main(String[] args) throws Exception {
        /**
         * 一次写多个字节的方法:
         * public void write(byte[] b):将 b.length 字节从指定的字节数组写入此输出流。
         * public void write(byte[] b, int off, int len) :从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流。
         */
        FileOutputStream fileOutputStream = new FileOutputStream("F:\\home\\03.txt");
        FileOutputStream fos = new FileOutputStream("F:\\home\\04.txt");
    
        // 01-如果写的第一个字节是正数(0 —— 127),那么显示的时候会查询 ASCII 表
        byte[] arr = {65, 66, 67, 68, 69};    // ABCDE
    
        // 02-向文件中写入数据
        fileOutputStream.write(arr);
    
        // 03-如果写的第一个字节是负数,那第一个字节会和第二个字节,两个字节组成一个中文显示,查询系统默认码表(GBK)
        byte[] bytes = {-65, -66, -67, 68, 69};    // 烤紻E
        fos.write(bytes);
    
        // 04-public void write(byte[] b, int off, int len
        fos.write(arr, 1, 2);    // 04.txt文件中:烤紻EBC
    
        // 05-写入字符串的方法 : 可以使用 String 类中的 getBytes() 方法把字符串,转换为字节数组
        byte[] str = "你好".getBytes();
        System.out.println(Arrays.toString(str));    // [-28, -67, -96, -27, -91, -67]
        fos.write(str);    // 04.txt文件中:烤紻EBC浣犲ソ
    
        // 06-关闭流
        fileOutputStream.close();
        fos.close();
    }
    

    写出指定长度字节数组

    通过 write(byte[] b, int off, int len) 方法,每次写出从 off 索引开始,len 个字节,代码使用演示:

    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("F:\\home\\06.txt");     
    
        // 字符串转换为字节数组
        byte[] b = "abcde".getBytes();
    
        // 写出从索引 2 开始,2 个字节。索引 2 是 c,两个字节,也就是 cd
        fos.write(b, 2, 2);
    
        // 关闭资源
        fos.close();
    }
    

    数据追加续写

    当我们创建输出流对象时,都会清空目标文件中的数据。那么如何保留目标文件中数据,并且还能继续添加新数据呢?使用以下两个构造方法便可以解决我们苦恼的问题。构造方法如下:

    构造方法:
    public FileOutputStream(File file, boolean append) : 创建文件输出流以写入由指定的 File 对象表示的文件。 public FileOutputStream(String name, boolean append) : 创建文件输出流以指定的名称写入文件。

    这两个构造方法的参数中,都需要传入一个boolean 类型的值,true 表示追加数据,false 表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了。代码演示如下:

    public static void main(String[] args) throws IOException {
        // 这样创建流对象,可以在 05.txt 文件中追加数据
        FileOutputStream fos = new FileOutputStream("F:\\home\\05.txt",true);     
    
        // 字符串转换为字节数组
        byte[] bytes = "abcde".getBytes();
    
        // 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。
        fos.write(bytes);
    
        // 关闭资源
        fos.close();
    }
    

    写出换行

    Windows 系统里,换行符号是 \r\n ,代码演示如下:

    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("F:\\home\\05.txt");
    
        // 定义字节数组
        byte[] words = {97, 98, 99, 100, 101};
    
        // 遍历数组
        for (int i = 0; i < words.length; i++) {
            // 写出一个字节
            fos.write(words[i]);
            // 写出一个换行, 换行符号转成数组写出
            fos.write("\r\n".getBytes());
        }
    
        // 关闭资源
        fos.close();
    }
    
    回车符和换行符:
    回车符:回到一行的开头(return)。 换行符:下一行(newline)。
    各种系统的回车换行:
    1、Windows 系统里,每行结尾是【回车 + 换行】 ,即 \r\n 2、Unix/Linux 系统里,每行结尾只有【换行】 ,即 \n 3、Mac 系统里,每行结尾是【回车】 ,即 \r 。从 Mac OS X 开始与 Linux 统一

    字节输入流

    java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

    基本共性功能方法:
    public void close() :关闭此输入流并释放与此流相关联的任何系统资源。 public abstract int read(): 从输入流读取数据的下一个字节。 public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。

    FileInputStream 类

    java.io.FileInputStream 类是文件输入流,从文件中读取字节。

    读入数据的原理(硬盘-->内存):
    java 程序 --> JVM --> OS --> OS读取数据的方法 --> 读取文件
    字节输入流的使用步骤(重点) :
    1、创建 FileInputStream 对象,构造方法中绑定要读取的数据源 2、使用 FileInputStream 对象中的方法 read , 读取文件 3、释放资源

    构造方法

    构造方法:
    FileInputStream(File file) : 创建一个 FileInputStream ,该文件由文件系统中的 File 对象 file 命名。 FileInputStream(String name) : 创建一个 FileInputStream ,该文件由文件系统中的路径名 name 命名。

    当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出 FileNotFoundException 。构造举例,代码如下:

    public static void main(String[] args) throws IOException{
        // 01-使用 File 对象创建流对象
        File file = new File("F:\\home\\05.txt");
        FileInputStream fos = new FileInputStream(file);
      
        // 02-使用文件名称创建流对象
        FileInputStream fileInputStream = new FileInputStream("F:\\home\\06.txt");
        
        // 03-关闭资源
        fileInputStream.close();
        fos.close();
    }
    

    读取字节数据

    通过 read 方法,每次可以读取一个字节的数据,提升为 int 类型,读取到文件末尾,返回 -1 ,代码演示如下:

    public static void main(String[] args) throws Exception {
        // 01-创建输入流对象
        FileInputStream fileInputStream = new FileInputStream("F:\\home\\04.txt");
    
        // 02-记录读取到的字节
        int read = fileInputStream.read();
    
        // 03-循环输出内容
        while (read != -1){
            System.out.print(read + " ");
            read = fileInputStream.read();
        }
    
        // 04-关闭资源
        fileInputStream.close();
    }
    

    小贴士:

    1、虽然读取了一个字节,但是会自动提升为 int 类型。 2、流操作完毕后,必须释放系统资源,调用 close 方法,千万记得。

    使用字节数组读取

    通过 read(byte[] b) ,每次读取 b 个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回 -1 ,代码使用演示:

    public static void main(String[] args) throws IOException{
        // 使用文件名称创建流对象
        FileInputStream fis = new FileInputStream("F:\\home\\06.txt");    // 文件中为 abcde
    
        // 定义变量,作为有效个数
        int len;
        
        // 定义字节数组,作为装字节数据的容器   
        byte[] b = new byte[2];
        
        // 循环读取
        while (( len = fis.read(b)) != -1) {
               // 每次读取后,把数组变成字符串打印
            System.out.println(new String(b));
        }
        
        // 关闭资源
        fis.close();
    }
    
    // 输出结果:
    // ab
    // cd
    // ed
    

    最后一次读取的 ed 是错误数据 ,原因是:在最后一次读取中,上次读取到的数据 c 被 e 替换了,而 d 没有被替换,只读取一个字节 e ,从而产生了错误。所以要通过 len 来获取有效的字节。代码使用演示:

    public static void main(String[] args) throws IOException {
        // 01-使用文件名称创建流对象.
        FileInputStream fis = new FileInputStream("F:\\home\\03.txt");    // 文件中为 abcde
        
        // 02-定义变量,作为有效个数
        int len;
        
        // 03-定义字节数组,作为装字节数据的容器
        byte[] b = new byte[1024];
        
        // 04-循环读取
        while ((len = fis.read(b)) != -1) {
            // 每次读取后,把数组的有效字节部分,变成字符串打印
            System.out.println(new String(b, 0, len));    // len 每次读取的有效字节个数
            System.out.println(len);
        }
        
        // 05-关闭资源
        fis.close();
    }
    
    // 输出结果:
    // abcde
    // 5
    

    小贴士: 使用数组读取,每次读取多个字节,减少了系统间的 IO 操作次数,从而提高了读写的效率,建议开发中使用。

    图片复制练习

    复制原理图解

    案例实现

    复制图片文件,代码演示如下:

    public static void main(String[] args) throws IOException {
        // 1、创建一个字节输入流对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("F:\\home\\01.png");
    
        // 2、创建一个字节输出流对象,构造方法中绑定要写入的目的地
        FileOutputStream fos = new FileOutputStream("F:\\01.png");
    
        // 一次读取一个字节写入一个字节的方式
        // 3、使用字节输入流对象中的方法read读取文件
        /*int len = 0;
        while((len = fis.read())!=-1){
            // 4、使用字节输出流中的方法write,把读取到的字节写入到目的地的文件中
            fos.write(len);
        }*/
    
        // 3、使用数组缓冲读取多个字节,写入多个字节
        byte[] bytes = new byte[1024 * 8];
    
        // 4、使用字节输入流对象中的方法 read 读取文件
        int len;    // 每次读取的有效字节个数
        while ((len = fis.read(bytes)) != -1) {
            // 5、使用字节输出流中的方法 write , 把读取到的字节写入到目的地的文件中
            fos.write(bytes, 0, len);
        }
    
        // 6、释放资源(先关写的,后关闭读的;如果写完了,肯定读取完毕了)
        fos.close();
        fis.close();
    }
    

    小贴士: 流的关闭原则:先开后关,后开先关。

    字符流

    当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以 Java 提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。

    使用字节流读取中文文件,1个中文所占用字节如下: GBK: 占用 2 字节 UTF-8: 占用 3 个字节

    字符输入流

    java.io.Reader 抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

    基本共性功能方法:
    public void close() :关闭此流并释放与此流相关联的任何系统资源。 public int read() : 从输入流读取一个字符。 public int read(char[] cbuf) : 从输入流中读取一些字符,并将它们存储到字符数组 cbuf 中 。

    FileReader 类

    java.io.FileReader 类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

    小贴士:

    **1、字符编码:**字节与字符的对应规则。Windows 系统的中文编码默认是 GBK 编码表。在 IDEA 中是 UTF-8 编码。 **2、字节缓冲区:**一个字节数组,用来临时存储字节数据。

    构造方法

    构造方法:
    FileReader(File file) : 创建一个新的 FileReader ,给定要读取的File对象。 FileReader(String fileName) : 创建一个新的 FileReader ,给定要读取的文件的名称。

    当你创建一个流对象时,必须传入一个文件路径。类似于 FileInputStream 。构造举例,代码如下:

    public static void main(String[] args)  throws IOException{
        // 01-使用 File 对象创建流对象
        File file = new File("F:\\home\\02.txt");
        FileReader fr = new FileReader(file);
      
        // 02-使用文件名称创建流对象
        FileReader fileReader = new FileReader("F:\\home\\03.txt");
    
        // 03-关闭资源
        fileReader.close();
        fr.close();
    }
    

    读取字符数据

    通过 read 方法,每次可以读取一个字符的数据,提升为 int 类型,读取到文件末尾,返回 -1 ,循环读取,代码使用演示:

    public static void main(String[] args) throws IOException {
        // 使用文件名称创建流对象
        FileReader fr = new FileReader("F:\\home\\02.txt");
    
        // 定义变量,保存数据
        int b;
    
        // 循环读取
        while ((b = fr.read()) != -1) {
            System.out.println((char)b);
        }
    
        // 关闭资源
        fr.close();
    }
    

    小贴士: 虽然读取了一个字符,但是会自动提升为 int 类型。

    通过 read(char[] cbuf) 方法,每次读取 b 个字符到数组中,返回读取到的有效字符个数,读取到末尾时,返回 -1 ,代码演示:

    public static void main(String[] args) throws IOException {
        // 1、创建 FileReader 对象,构造方法中绑定要读取的数据源
        FileReader fr = new FileReader("F:\\home\\01.txt");
    
        // 2、int read(char[] cbuf) 一次读取多个字符,将字符读入数组。
        char[] cs = new char[2];    // 存储读取到的多个字符
        int len;
        while((len = fr.read(cs)) != -1){
             // String类的构造方法:String(char[] value) 把字符数组转换为字符串
            System.out.println(new String(cs);
        }
    
        // 3、释放资源
        fr.close();
    }
    
    // 输出结果:
    // 我在
    // 这里
    // 呀里
    

    获取有效的字符改进,避免读取到无效的字符,代码使用演示:

    public static void main(String[] args) throws IOException {
        // 1、创建 FileReader 对象,构造方法中绑定要读取的数据源
        FileReader fr = new FileReader("F:\\home\\01.txt");
        
        // 2、int read(char[] cbuf) 一次读取多个字符,将字符读入数组。
        char[] cs = new char[2];    // 存储读取到的多个字符
        int len;
        while((len = fr.read(cs)) != -1){
            // String(char[] value, int offset, int count) 把字符数组的一部分转换为字符串 offset 数组的开始索引 count 转换的个数
            System.out.println(new String(cs, 0, len));
        }
    
        // 3、关闭资源
        fr.close();
    }
    
    // 输出结果:
    // 我在
    // 这里
    // 呀
    

    字符输出流

    java.io.Writer 抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。以下是字符输出流的基本共性功能方法。

    基本共性功能方法:
    void write(int c) ,写入单个字符。 void write(char[] cbuf) ,写入字符数组。 abstract void write(char[] cbuf, int off, int len) ,写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
    void write(String str) ,写入字符串。 void write(String str, int off, int len) ,写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
    void flush() ,刷新该流的缓冲。 void close() ,关闭此流,但要先刷新它。

    FileWriter 类

    java.io.FileWriter 类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

    构造方法

    构造方法:
    FileWriter(File file) : 创建一个新的 FileWriter,给定要读取的 File 对象。 FileWriter(String fileName) : 创建一个新的 FileWriter,给定要读取的文件的名称。

    当你创建一个流对象时,必须传入一个文件路径,类似于 FileOutputStream 。构造举例,代码如下:

    public static void main(String[] args) throws IOException {
        // 使用 File 对象创建流对象
        File file = new File("F:\\home\\05.txt");
        FileWriter fw = new FileWriter(file);
      
        // 使用文件名称创建流对象
        FileWriter fileWriter = new FileWriter("F:\\home\\06.txt");
    
        // 03-关闭资源
        fileWriter.close();
        fw.close();
    }
    

    基本写出数据

    通过 write(int b) 方法,每次可以写出一个字符数据,代码使用演示:

    public static void main(String[] args) throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter("F:\\home\\07.txt");     
        
        // 写出数据
        fw.write(97);      // 写出第1个字符
        fw.write('b');     // 写出第2个字符
        fw.write('C');     // 写出第3个字符
        fw.write(30000);   // 写出第4个字符,中文编码表中30000对应一个汉字。
      
        // 【注意】关闭资源时,与 FileOutputStream 不同。如果不关闭,数据只是保存到缓冲区,并未保存到文件。
        fw.close();
    }
    
    // 输出结果:
    // abC田
    

    小贴士:

    1、虽然参数为 int 类型四个字节,但是只会保留一个字符的信息写出。 2、未调用 close 方法,数据只是保存到了缓冲区,并未写出到文件中。

    关闭和刷新

    因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要使用 flush() 方法了。代码演示如下:

    /**
     * flush 方法和 close 方法的区别:
     * - flush :刷新缓冲区,流对象可以继续使用。
     * - close :先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
     */
    public static void main(String[] args) throws IOException {
        // 1、创建 FileWriter 对象,构造方法中绑定要写入数据的目的地
        FileWriter fw = new FileWriter("F:\\home\\08.txt");
    
        // 2、使用 FileWriter 中的方法 write , 把数据写入到内存缓冲区中(字符转换为字节的过程)
        // void write(int c) 写入单个字符。
        fw.write(97);
    
        // 3、使用 FileWriter 中的方法 flush , 把内存缓冲区中的数据,刷新到文件中
        fw.flush();
        fw.write(98);    // 刷新之后流可以继续使用
    
        // 4、释放资源(会先把内存缓冲区中的数据刷新到文件中)
        fw.close();
    
        // close 方法之后流已经关闭了,已经从内存中消失了,流就不能再使用了
        fw.write(99);    // IOException: Stream closed
    }
    

    小贴士: 即便是 flush 方法写出了数据,操作的最后还是要调用 close 方法,释放系统资源。

    写出其他数据

    写出字符数组 :通过 write(char[] cbuf) 方法和 write(char[] cbuf, int off, int len) 方法 ,每次可以写出字符数组中的数据,用法类似于 FileOutputStream ,代码演示如下:

    public static void main(String[] args) throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter("F:\\home\\09.txt");     
        
        // 字符串转换为字节数组
        char[] chars = "井冈山大学".toCharArray();
      
        // 写出字符数组
        fw.write(chars);    // 井冈山大学
        
        // 写出从索引3开始,写2个字节。索引3是'大',两个字节,也就是 大学
        fw.write(b, 3, 2);    // 大学
      
        // 关闭资源
        fos.close();
    }
    

    写出字符串:通过 write(String str) 方法和 write(String str, int off, int len) 方法,每次可以写出字符串中的数据,更为方便,代码演示如下:

    public static void main(String[] args) throws IOException {
        // 01-使用文件名称创建流对象
        FileWriter fw = new FileWriter("F:\\home\\10.txt");
    
        // 02-定义字符串
        String msg = "井冈山大学";
      
        // 03-写出字符串
        fw.write(msg);     // 井冈山大学
      
        // 04-写出从索引3开始,写2个字节。索引3是'大',两个字节,也就是 大学
        fw.write(msg, 3, 2);    // 大学
          
        // 05-关闭资源
        fw.close();
    }
    

    数据追加续写

    public static void main(String[] args) throws IOException {
        // 01-使用文件名称创建流对象,可以续写数据
        FileWriter fw = new FileWriter("F:\\home\\11.txt",true);     
    
        // 02-写出字符串
        fw.write("井冈山");
        
        // 03-写出换行
        fw.write("\r\n");
    
        // 04-写出字符串
        fw.write("大学");
    
        // 05-关闭资源
        fw.close();
    }
    
    // 输出结果:
    // 井冈山
    // 大学
    

    小贴士:

    1、字符流只能操作文本文件,不能操作图片、视频等非文本文件。 2、当我们单纯读或者写文本文件时可以使用字符流,其他情况使用字节流。

    IO 异常的处理

    JDK7 前处理

    之前的代码中,我们一直把异常抛出,而实际开发中并不能这样处理,而是建议使用 try...catch...finally 代码块处理异常部分,代码演示如下:

    /**
     * 在jdk1.7之前使用try catch finally 处理流中的异常
     * 格式:
     *    try{
     *        可能会产出异常的代码
     *    }catch(异常类变量 变量名){
     *        异常的处理逻辑
     *    }finally{
     *        一定会执行的代码
     *        资源释放
     *    }
     */
    public static void main(String[] args) {
        // 提高变量 fw 的作用域,让 finally 可以使用。变量在定义的时候,可以没有值,但是使用的时候必须有值
        // fw = new FileWriter("09_IOAndProperties\\g.txt",true); 执行失败,fw 没有值,fw.close 会报错
        FileWriter fw = null;
        try{
            // 可能会产出异常的代码
            fw = new FileWriter("F:\\home\\11.txt", true);
            for (int i = 0; i < 10 ; i++) {
                fw.write("HelloWorld " + i + "\r\n");
            }
        }catch(IOException e){
            // 异常的处理逻辑
            System.out.println(e);
        }finally {
            // 一定会执行的代码
            // 创建对象失败了,fw的默认值就是 null , null 是不能调用方法的,会抛出 NullPointerException , 需要增加一个判断,不是 null 在把资源释放
            if(fw != null){
                try {
                    // fw.close 方法声明抛出了 IOException 异常对象,所以我们就的处理这个异常对象,要么 throws , 要么 try catch
                    fw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
        }
    }
    

    JDK7 的处理

    还可以使用 JDK7 优化后的 try-with-resource 语句,该语句确保了每个资源在语句结束时关闭。所谓的资源(resource)是指在程序完成后,必须关闭的对象。代码演示如下:

    /**
     * JDK7 的新特性
     * 在try的后边可以增加一个(),在括号中可以定义流对象
     * 那么这个流对象的作用域就在try中有效
     * try中的代码执行完毕,会自动把流对象释放,不用写finally
     */
    public static void main(String[] args) {
        try(// 1、创建一个字节输入流对象,构造方法中绑定要读取的数据源
            FileInputStream fis = new FileInputStream("F:\\home\\12.txt");
            // 2、创建一个字节输出流对象,构造方法中绑定要写入的目的地
            FileOutputStream fos = new FileOutputStream("F:\\home\\13.txt");){
    
            // 可能会产出异常的代码
            // 一次读取一个字节写入一个字节的方式
            // 3、使用字节输入流对象中的方法 read 读取文件
            int len = 0;
            while((len = fis.read()) != -1){
                // 4、使用字节输出流中的方法 write , 把读取到的字节写入到目的地的文件中
                fos.write(len);
            }
    
        }catch (IOException e){
            // 异常的处理逻辑
            System.out.println(e);
        }
    }
    

    JDK9 的改进

    /**
     * JDK9 新特性
     * try的前边可以定义流对象
     * 在try后边的()中可以直接引入流对象的名称(变量名)
     * 在try代码执行完毕之后,流对象也可以释放掉,不用写finally
     * 格式:
     *     A a = new A();
     *     B b = new B();
     *     try(a,b){
     *         可能会产出异常的代码
     *     }catch(异常类变量 变量名){
     *         异常的处理逻辑
     *     }
     */
    public static void main(String[] args) throws IOException {
        // 1、创建一个字节输入流对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("F:\\home\\14.txt");
    
        // 2、创建一个字节输出流对象,构造方法中绑定要写入的目的地
        FileOutputStream fos = new FileOutputStream("F:\\home\\15.txt");
    
        try(fis; fos){
            // 一次读取一个字节写入一个字节的方式
            // 3、使用字节输入流对象中的方法 read 读取文件
            int len = 0;
            while((len = fis.read()) != -1){
                // 4、使用字节输出流中的方法write,把读取到的字节写入到目的地的文件中
                fos.write(len);
            }
        }catch (IOException e){
            System.out.println(e);
        }
    }
    

    Properties 属性集

    属性集概述

    java.util.Properties 继承于 Hashtable ,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多 Java 类使用,比如获取系统属性时,System.getProperties 方法就是返回一个 Properties 对象。

    Properties 类

    构造方法和其他方法

    构造方法:
    public Properties() : 创建一个空的属性列表。
    其他方法:
    public Object setProperty(String key, String value) : 保存一对属性。 public String getProperty(String key) :使用此属性列表中指定的键搜索属性值。 public Set stringPropertyNames() :所有键的名称的集合。

    上述方法代码演示如下:

    /**
     * 使用Properties 集合存储数据,遍历取出 Properties 集合中的数据。Properties 集合是一个双列集合,key 和 value 默认都是字符串
     *  Properties集合有一些操作字符串的特有方法
     *      Object setProperty(String key, String value) 调用 Hashtable 的方法 put。
     *      String getProperty(String key) 通过 key 找到 value 值,此方法相当于 Map 集合中的 get(key) 方法
     *      Set<String> stringPropertyNames() 返回此属性列表中的键集,其中该键及其对应值是字符串,此方法相当于 Map 集合中的 keySet 方法
     */
    public static void main(String[] args) {
        // 01-创建 Properties 集合对象
        Properties prop = new Properties();
    
        // 02-使用 setProperty 往集合中添加数据
        prop.setProperty("赵丽颖", "168");
        prop.setProperty("迪丽热巴", "165");
        prop.setProperty("古力娜扎", "160");
    
        // 03-使用 stringPropertyNames 把 Properties 集合中的键取出,存储到一个 Set 集合中
        Set<String> set = prop.stringPropertyNames();
    
        // 04-遍历 Set 集合,取出 Properties 集合的每一个键
        for (String key : set) {
            // 05-使用 getProperty 方法通过 key 获取 value
            String value = prop.getProperty(key);
            System.out.println(key + " = " + value);
        }
    }
    

    与流相关的方法

    这是保存的方法,知识点都在代码的注释里面了,请看代码演示:

    /**
     * 可以使用 Properties 集合中的方法 store,把集合中的临时数据,持久化写入到硬盘中存储
     * void store(OutputStream out, String comments)
     * void store(Writer writer, String comments)
     * 参数:
     * OutputStream out: 字节输出流,不能写入中文
     * Writer writer: 字符输出流,可以写中文
     * String comments: 注释,用来解释说明保存的文件是做什么用的,但是不能使用中文,会产生乱码,默认是 Unicode 编码,一般使用""空字符串
     */
    public static void main(String[] args) throws IOException {
        // 1、创建 Properties 集合对象并添加数据
        Properties prop = new Properties();
        prop.setProperty("赵丽颖", "168");
        prop.setProperty("迪丽热巴", "165");
        prop.setProperty("古力娜扎", "175");
    
        // 2、创建字符输出流对象,构造方法中绑定要输出的目的地,不要使用字节输出流,会乱码
        FileWriter fw = new FileWriter("F:\\home\\14.txt");
    
        // 3、使用 Properties 集合中的方法 store,把集合中的临时数据,持久化写入到硬盘中存储
        prop.store(fw,"save my data");
        
        // 4、释放资源
        fw.close();
    }
    

    这是读取的方法,知识点都在代码的注释里面了,请看代码演示:

    /**
     * 可以使用 Properties 集合中的方法 load,把硬盘中保存的文件(键值对),读取到集合中使用
     * void load(InputStream inStream)
     * void load(Reader reader)
     * 参数:
     *     InputStream inStream : 字节输入流,不能读取含有中文的键值对
     *     Reader reader : 字符输入流,能读取含有中文的键值对
     * 注意事项:
     *     1、存储键值对的文件中,键与值默认的连接符号可以使用=,空格(其他符号)
     *     2、存储键值对的文件中,可以使用#进行注释,被注释的键值对不会再被读取
     *     3、存储键值对的文件中,键与值默认都是字符串,不用再加引号
     */
    public static void main(String[] args) throws IOException {
        // 1、创建 Properties 集合对象
        Properties prop = new Properties();
    
        // 2、使用 Properties 集合对象中的方法 load 读取保存键值对的文件
        prop.load(new FileReader("F:\\home\\14.txt"));
    
        // 3、遍历 Properties 集合
        Set<String> set = prop.stringPropertyNames();
        for (String key : set) {
            String value = prop.getProperty(key);
            System.out.println(key + " = " + value);
        }
    }
    

    小贴士:文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。

    缓冲流

    上面介绍了基本的一些流,作为 IO 流的入门。现在我们要见识一些更强大的流,比如能够高效读写的缓冲流、能够转换编码的转换流、能够持久化存储对象的序列化流等等。这些功能更为强大的流,都是在基本的流对象基础之上创建而来的,就像穿上铠甲的武士一样,相当于是对基本流对象的一种增强。

    缓冲流概述

    缓冲流,也叫高效流,是对 4 个基本的 FileXxx 流的增强。缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统 IO 操作的次数,从而提高读写的效率。以下是缓冲流的分类:

    按照数据类型分类:
    字节缓冲流:BufferedInputStream 和 BufferedOutputStream 字符缓冲流:BufferedReader 和 BufferedWriter

    字节缓冲流

    构造方法

    构造方法:
    public BufferedInputStream(InputStream in) :创建一个新的缓冲输入流。 public BufferedOutputStream(OutputStream out) : 创建一个新的缓冲输出流。

    构造方法举例如下:

    // 01-创建字节缓冲输入流
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("F:\\home\\05.txt"));
    
    // 02-创建字节缓冲输出流
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\home\\05.txt"));
    

    字节缓冲输出流

    字节缓冲输出流【BufferedOutputStream】实际案例如下:

    /**
     * 继承自父类的共性成员方法:
     *     public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
     *     public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
     *     public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
     *     public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
     *     public abstract void write(int b) :将指定的字节输出流。
     *     构造方法:
     *     BufferedOutputStream(OutputStream out)  创建一个新的缓冲输出流,以将数据写入指定的底层输出流。
     *     BufferedOutputStream(OutputStream out, int size)  创建一个新的缓冲输出流,以将具有指定缓冲区大小的数据写入指定的底层输出流。
     *     参数:
     *     OutputStream out:字节输出流,我们可以传递FileOutputStream,缓冲流会给FileOutputStream增加一个缓冲区,提高FileOutputStream的写入效率
     *     int size:指定缓冲流内部缓冲区的大小,不指定默认
     */
    public static void main(String[] args) throws IOException {
        // 1、创建 FileOutputStream 对象,构造方法中绑定要输出的目的地
        FileOutputStream fos = new FileOutputStream("F:\\home\\05.txt");
    
        // 2、创建 BufferedOutputStream 对象,构造方法中传递 FileOutputStream 对象,提高效率
        BufferedOutputStream bos = new BufferedOutputStream(fos);
    
        // 3、使用 BufferedOutputStream 对象中的方法 write , 把数据写入到内部缓冲区中
        bos.write("井冈山大学".getBytes());
    
        // 4、使用 BufferedOutputStream 对象中的方法 flush , 把内部缓冲区中的数据,刷新到文件中
        bos.flush();
    
        // 5、释放资源(会先调用 flush 方法刷新数据,第4部可以省略)
        bos.close();
    }
    

    字节缓冲输入流

    字节缓冲输入流【BufferedInputStream】实际案例如下:

    /**
     * 继承自父类的成员方法:
     *     int read()从输入流中读取数据的下一个字节。
     *     int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
     *     void close() 关闭此输入流并释放与该流关联的所有系统资源。
     *     构造方法:
     *         BufferedInputStream(InputStream in) 创建一个 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。
     *         BufferedInputStream(InputStream in, int size) 创建具有指定缓冲区大小的 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。
     *     参数:
     *         InputStream in:字节输入流,我们可以传递FileInputStream,缓冲流会给FileInputStream增加一个缓冲区,提高FileInputStream的读取效率
     *         int size:指定缓冲流内部缓冲区的大小,不指定默认
     */
    public static void main(String[] args) throws IOException {
        // 1、创建 FileInputStream 对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("F:\\home\\05.txt");
       
        // 2、创建 BufferedInputStream 对象,构造方法中传递 FileInputStream 对象,提高读取效率
        BufferedInputStream bis = new BufferedInputStream(fis);
       
        // 3、使用 BufferedInputStream 对象中的方法 read ,读取文件
        // int read() 从输入流中读取数据的下一个字节,也就是一个字节一个字节地读,效率低
    //    int len;    // 记录每次读取到的字节
    //    while((len = bis.read()) != -1){
    //        System.out.println(len);
    //    }
    
        // int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
        byte[] bytes = new byte[1024 * 8];//存储每次读取的数据
        int len;    // 记录每次读取的有效字节个数
        while ((len = bis.read(bytes)) != -1) {
            System.out.println(new String(bytes, 0, len));
        }
    
        // 4、释放资源
        bis.close();
    }
    

    效率测试

    查询 API,缓冲流读写方法与基本的流是一致的。那我们通过复制大文件(229MB),来测试它的效率。

    基本流的复制,代码演示如下:

    public static void main(String[] args) {
        // 记录开始时间
        long start = System.currentTimeMillis();
        // 创建流对象
        try (
                FileInputStream fis = new FileInputStream("F:\\home\\01.pdf");
                FileOutputStream fos = new FileOutputStream("F:\\01.pdf")
        ) {
            // 读写数据
            int b;
            while ((b = fis.read()) != -1) {
                fos.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("普通流复制时间: " + (end - start) + " 毫秒");
    }
    
    // 普通流复制时间: 1278098 毫秒
    

    缓冲流的复制,代码演示如下:

    public static void main(String[] args) {
        // 记录开始时间
        long start = System.currentTimeMillis();
        // 创建流对象
        try (
                BufferedInputStream bis = new BufferedInputStream(new FileInputStream("F:\\home\\01.pdf"));
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\01.pdf"))
        ) {
            // 读写数据
            int b;
            while ((b = bis.read()) != -1) {
                bos.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("缓冲流复制时间: " + (end - start) + " 毫秒");
    }
    
    // 缓冲流复制时间: 10480 毫秒
    

    那么如何做才能更快呢?答案是:使用数组的方式,代码如下:

    public static void main(String[] args) {
        // 记录开始时间
        long start = System.currentTimeMillis();
        // 创建流对象
        try (
                BufferedInputStream bis = new BufferedInputStream(new FileInputStream("F:\\home\\01.pdf"));
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\02.pdf"))
        ) {
            // 读写数据
            int len;
            byte[] bytes = new byte[8 * 1024];
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("缓冲流使用数组复制时间: " + (end - start) + " 毫秒");
    }
    
    // 缓冲流使用数组复制时间: 566 毫秒
    

    字符缓冲流

    构造方法

    构造方法:
    public BufferedReader(Reader in) :创建一个新的缓冲输入流。 public BufferedWriter(Writer out) : 创建一个新的缓冲输出流。

    构造方法举例如下:

    // 01-创建字符缓冲输入流
    BufferedReader br = new BufferedReader(new FileReader("F:\\home\\05.txt"));
    
    // 02-创建字符缓冲输出流
    BufferedWriter bw = new BufferedWriter(new FileWriter("F:\\home\\05.txt"));
    

    特有方法

    字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。

    特有方法:
    BufferedReader:public String readLine() : 读一行文字。 BufferedWriter:public void newLine() : 相当于换行分隔符,由系统属性定义符号。

    public String readLine() 方法演示,代码如下:

    public static void main(String[] args) throws IOException {
        // 创建流对象
        BufferedReader br = new BufferedReader(new FileReader("F:\\home\\05.txt"));
    
        // 定义字符串,保存读取的一行文字
        String line;
    
        // 循环读取,读取到最后返回 null
        while ((line = br.readLine()) != null) {
            System.out.print(line);
            System.out.println("---");
        }
    
        // 释放资源
        br.close();
    }
    

    public void newLine() 方法演示,代码如下:

    public static void main(String[] args) throws IOException  {
        // 创建流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("F:\\home\\edu.txt"));
    
        // 写出数据
        bw.write("井冈山");
    
        // 写出换行
        bw.newLine();
        bw.write("大学");
        bw.newLine();
        bw.write("欢迎你");
        bw.newLine();
    
        // 释放资源
        bw.close();
    }
    
    // 输出效果:
    // 井冈山
    // 大学
    // 欢迎你
    

    字符缓冲输出流

    字符缓冲输出流【BufferedWriter】实际案例如下:

    /**
     * 继承自父类的共性成员方法:
     *    void write(int c) 写入单个字符。
     *    void write(char[] cbuf)写入字符数组。
     *    abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分, off 数组的开始索引, len 写的字符个数。
     *    void write(String str)写入字符串。
     *    void write(String str, int off, int len) 写入字符串的某一部分,off 字符串的开始索引, len 写的字符个数。
     *    void flush()刷新该流的缓冲。
     *    void close() 关闭此流,但要先刷新它。
     *
     *     构造方法:
     *         BufferedWriter(Writer out) 创建一个使用默认大小输出缓冲区的缓冲字符输出流。
     *         BufferedWriter(Writer out, int sz) 创建一个使用给定大小输出缓冲区的新缓冲字符输出流。
     *         参数:
     *             Writer out:字符输出流,我们可以传递FileWriter,缓冲流会给FileWriter增加一个缓冲区,提高FileWriter的写入效率
     *             int sz:指定缓冲区的大小,不写默认大小
     */
    public static void main(String[] args) throws IOException {
        // 1、创建字符缓冲输出流对象,构造方法中传递字符输出流
        BufferedWriter bw = new BufferedWriter(new FileWriter("F:\\home\\edu.txt"));
    
        // 2、调用字符缓冲输出流中的方法 write , 把数据写入到内存缓冲区中
        for (int i = 0; i < 10 ; i++) {
            bw.write("井冈山大学~~~");
            // bw.write("\r\n");
            bw.newLine();
        }
        
        // 3、调用字符缓冲输出流中的方法 flush ,把内存缓冲区中的数据,刷新到文件中
        bw.flush();
        
        // 4、释放资源
        bw.close();
    }
    

    字符缓冲输入流

    字符缓冲输入流【BufferedReader】实际案例如下:

    /**
     * 继承自父类的共性成员方法:
     *     int read() 读取单个字符并返回。
     *     int read(char[] cbuf)一次读取多个字符,将字符读入数组。
     *     void close() 关闭该流并释放与之关联的所有资源。
     *     构造方法:
     *         BufferedReader(Reader in)  创建一个使用默认大小输入缓冲区的缓冲字符输入流。
     *         BufferedReader(Reader in, int sz)     创建一个使用指定大小输入缓冲区的缓冲字符输入流。
     *         参数:
     *             Reader in:字符输入流,我们可以传递FileReader,缓冲流会给FileReader增加一个缓冲区,提高FileReader的读取效率
     *     特有的成员方法:
     *         String readLine() 读取一个文本行。读取一行数据
     *         行的终止符号:通过下列字符之一即可认为某行已终止:换行 ('\n')、回车 ('\r') 或回车后直接跟着换行(\r\n)。
     *         返回值:包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回 null
     */
    public static void main(String[] args) throws IOException {
        // 1、创建字符缓冲输入流对象,构造方法中传递字符输入流
        BufferedReader br = new BufferedReader(new FileReader("F:\\home\\05.txt"));
    
        // 2、使用字符缓冲输入流对象中的方法read/readLine读取文本
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    
        // 3、释放资源
        br.close();
    }
    

    练习:文本排序

    练习描述

    请将下列文本信息恢复顺序。

    3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。
    8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。
    4.将军向宠,性行淑均,晓畅军事,试用之于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
    2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
    1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
    9.今当远离,临表涕零,不知所言。
    6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。
    7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐付托不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
    5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。
    

    案例分析

    • 1、逐行读取文本信息。
    • 2、解析文本信息到集合中。
    • 3、遍历集合,按顺序,写出文本信息。

    代码实现

    public static void main(String[] args) throws IOException {
        // 1、创建一个 HashMap 集合对象,存储每行文本的序号(1,2,3,..);value:存储每行的文本
        HashMap<String, String> map = new HashMap<>();
    
        // 2、创建字符缓冲输入流对象,构造方法中绑定字符输入流
        BufferedReader br = new BufferedReader(new FileReader("F:\\home\\05.txt"));
    
        // 3、创建字符缓冲输出流对象,构造方法中绑定字符输出流
        BufferedWriter bw = new BufferedWriter(new FileWriter("F:\\05.txt"));
    
        // 4、使用字符缓冲输入流中的方法 readline ,逐行读取文本
        String line;
        while ((line = br.readLine()) != null) {
            // 5、对读取到的文本进行切割,获取行中的序号和文本内容
            String[] arr = line.split("\\.");
    
            // 6、把切割好的序号和文本的内容存储到 HashMap 集合中(key 序号是有序的,会自动排序 1,2,3,4..)
            map.put(arr[0], arr[1]);
        }
    
        // 7、遍历 HashMap 集合,获取每一个键值对
        for (String key : map.keySet()) {
            String value = map.get(key);
    
            // 8、把每一个键值对,拼接为一个文本行
            line = key + "." + value;
    
            // 9、把拼接好的文本,使用字符缓冲输出流中的方法 write ,写入到文件中
            bw.write(line);
            bw.newLine();    // 写换行
        }
        // 10、释放资源
        bw.close();
        br.close();
    }
    

    转换流

    字符编码和字符集

    字符编码

    计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为 编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为 解码 。比如说,按照 A 规则存储,同样按照 A 规则解析,那么就能显示正确的文本符号。反之,按照 A 规则存储,再按照 B 规则解析,就会导致乱码现象。

    各种概念:
    编码 : 字符(我们能看懂的) --> 字节(我们看不懂的) 解码 : 字节(我们看不懂的) --> 字符(我们能看懂的) 字符编码【Character Encoding】 : 就是一套自然语言的字符与二进制数之间的对应规则。 编码表 : 生活中文字和计算机中二进制的对应规则

    字符集

    字符集【Charset】 :也叫编码表,是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有 ASCII 字符集GBK 字符集Unicode 字符 集等。字符编码与字符集对应关系如下图:

    可见,当指定了 编码,它所对应的 字符集 自然就指定了,所以 编码 才是我们最终要关心的。接下来我们介绍各种字符集。

    ASCII 字符集
    1、 ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。 2、 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。
    ISO-8859-1 字符集
    拉丁码表,别名 Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。ISO-8859-1 使用单字节编码,兼容 ASCII 编码。
    GBxxx 字符集
    1、 GB 就是国标的意思,是为了显示中文而设计的一套字符集。 2、 GB2312 :简体中文码表。一个小于 127 的字符的意义与原来相同。但两个大于 127 的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。 3、 GBK :最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。 4、 GB18030 :最新的中文码表。收录汉字 70244 个,采用多字节编码,每个字可以由 1 个、2 个或 4 个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。
    Unicode字符集
    1、 Unicode 编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。它最多使用 4 个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16 和 UTF-32。最为常用的 UTF-8 编码。 2、 UTF-8 编码,可以用来表示 Unicode 标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持 UTF-8 编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则如下:     1、 128 个 US-ASCII 字符,只需一个字节编码。     2、 拉丁文等字符,需要二个字节编码。     3、 大部分常用字(含中文),使用三个字节编码。     4、 其他极少使用的 Unicode 辅助字符,使用四字节编码。

    编码引出的问题

    在 IDEA 中,可以使用 FileReader 读取项目中的文本文件。由于 IDEA 默认是 UTF-8 编码,所以读取的内容没有任何问题。但是,当读取 Windows 系统中创建的文本文件时,由于 Windows 系统的默认是 GBK 编码,就会出现乱码。代码演示如下:

    public static void main(String[] args) throws IOException {
        FileReader fileReader = new FileReader("F:\\GBK.txt");    // 井冈山大学
        int read;
        while ((read = fileReader.read()) != -1) {
            System.out.print((char)read);
        }
        fileReader.close();
    }
    
    // 输出结果:
    // ����ɽ��ѧ
    

    那么问题来了,我们要如何才能读取 GBK 编码的文件呢?这时就要用到转换流了。接着往下看 ???

    转换流图解

    InputStreamReader 类

    转换流 java.io.InputStreamReader ,是 Reader 的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

    构造方法

    构造方法
    InputStreamReader(InputStream in) : 创建一个使用默认字符集的字符流。 InputStreamReader(InputStream in, String charsetName) : 创建一个指定字符集的字符流。

    指定编码读取

    public static void main(String[] args) throws IOException {
        read_gbk();
        read_utf_8();
    }
    
    // 使用 InputStreamReader 读取 GBK 格式的文件
    private static void read_gbk() throws IOException {
        // 1、创建 InputStreamReader 对象,构造方法中传递字节输入流和指定的编码表名称
        InputStreamReader isr = new InputStreamReader(new FileInputStream("F:\\gbk.txt"),"GBK"); // 你好
    
        // 2、使用InputStreamReader对象中的方法 read 读取文件
        int len;
        while((len = isr.read())!=-1){
            System.out.println((char)len);
        }
        // 3、释放资源
        isr.close();
    }
    
    // 使用 InputStreamReader 读取 UTF-8 格式的文件
    private static void read_utf_8() throws IOException {
        // 1、创建 InputStreamReader 对象,构造方法中传递字节输入流和指定的编码表名称
        InputStreamReader isr = new InputStreamReader(new FileInputStream("F:\\utf_8.txt")); // 不指定默认使用 UTF-8
        
        // 2、使用 InputStreamReader 对象中的方法 read 读取文件
        int len;
        while((len = isr.read())!=-1){
            System.out.println((char)len);
        }
        // 3、释放资源
        isr.close();
    }
    

    OutputStreamWriter 类

    转换流 java.io.OutputStreamWriter ,是 Writer 的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

    构造方法

    构造方法
    OutputStreamWriter(OutputStream in) : 创建一个使用默认字符集的字符流。 OutputStreamWriter(OutputStream in, String charsetName) : 创建一个指定字符集的字符流。

    指定编码写出

    public static void main(String[] args) throws IOException {
        gbk();
        utf_8();
    }
    
    // 使用转换流 OutputStreamWriter 写 GBK 格式的文件
    private static void gbk() throws IOException {
        // 1、创建 OutputStreamWriter 对象,构造方法中传递字节输出流和指定的编码表名称
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("F:\\gbk.txt"),"GBK");
    
        // 2、使用 OutputStreamWriter 对象中的方法 write , 把字符转换为字节存储缓冲区中(编码)
        osw.write("你好");
    
        // 3、使用 OutputStreamWriter 对象中的方法 flush , 把内存缓冲区中的字节刷新到文件中(使用字节流写字节的过程)
        osw.flush();
    
        // 4、释放资源
        osw.close();
    }
    
    // 使用转换流 OutputStreamWriter 写 UTF-8 格式的文件
    private static void utf_8() throws IOException {
        // 1、创建 OutputStreamWriter 对象,构造方法中传递字节输出流和指定的编码表名称
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("F:\\utf_8.txt"),"utf-8"); // 不指定默认使用UTF-8
    
        // 2、使用 OutputStreamWriter 对象中的方法 write , 把字符转换为字节存储缓冲区中(编码)
        osw.write("你好");
    
        // 3、使用 OutputStreamWriter 对象中的方法 flush , 把内存缓冲区中的字节刷新到文件中(使用字节流写字节的过程)
        osw.flush();
    
        // 4、释放资源
        osw.close();
    }
    

    练习:转换文件编码

    将 GBK 编码的文本文件,转换为 UTF-8 编码的文本文件。代码演示如下:

    public static void main(String[] args) throws IOException {
        // 1、创建 InputStreamReader 对象,构造方法中传递字节输入流和指定的编码表名称 GBK
        InputStreamReader isr = new InputStreamReader(new FileInputStream("F:\\GBK.txt"),"GBK");
        
        // 2、创建 OutputStreamWriter 对象,构造方法中传递字节输出流和指定的编码表名称 UTF-8
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("F:\\utf_8.txt"),"UTF-8");
        
        // 3、使用 InputStreamReader 对象中的方法 read 读取文件
        int len;
        while((len = isr.read()) != -1){
            // 4、使用 OutputStreamWriter 对象中的方法 write , 把读取的数据写入到文件中
            osw.write(len);
        }
        
        // 5、释放资源
        osw.close();
        isr.close();
    }
    

    序列化流

    Java 提供了一种对象 序列化 的机制。用一个字节序列可以表示一个对象,该字节序列包含该 对象的数据对象的类型对象中存储的属性 等信息。字节序列写出到文件之后,相当于文件中 持久保存 了一个对象的信息。 反之,该字节序列还可以从文件中读取回来,重构对象,对它进行 反序列化对象的数据对象的类型对象中存储的数据 信息,都可以用来在内存中创建对象。请看下图理解序列化:

    ObjectOutputStream 类

    java.io.ObjectOutputStream 类,将 Java 对象的原始数据类型写出到文件,实现对象的持久存储。

    构造方法

    构造方法
    public ObjectOutputStream(OutputStream out) : 创建一个指定 OutputStream 的 ObjectOutputStream 。

    序列化操作

    一、对象要想序列化,必须满足两个条件:
    1、该类必须实现 java.io.Serializable 接口,这是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出 NotSerializableException 。 2、该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用 transient 关键字修饰。
    /*
     * 序列化和反序列化的时候,会抛出NotSerializableException没有序列化异常
     * 类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。
     * Serializable接口也叫标记型接口
     *     要进行序列化和反序列化的类必须实现Serializable接口,就会给类添加一个标记
     *     当我们进行序列化和反序列化的时候,就会检测类上是否有这个标记
     *         有:就可以序列化和反序列化
     *         没有:就会抛出 NotSerializableException异常
     * 去市场买肉-->肉上有一个蓝色章(检测合格)-->放心购买-->买回来怎么吃随意
     *
     *
     * static关键字:静态关键字
     *     静态优先于非静态加载到内存中(静态优先于对象进入到内存中)
     *     被static修饰的成员变量不能被序列化的,序列化的都是对象
     *     private static int age;
     *     oos.writeObject(new Person("小美女",18));
     *     Object o = ois.readObject();
     *     Person{name='小美女', age=0}
     *
     * transient关键字:瞬态关键字
     *     被transient修饰成员变量,不能被序列化
     *     private transient int age;
     *     oos.writeObject(new Person("小美女",18));
     *     Object o = ois.readObject();
     *     Person{name='小美女', age=0}
     */
    public class Person implements Serializable{
        // 防止出现 InvalidClassException
        private static final long serialVersionUID = 1L;
        private String name;
        //private static int age;
        //private transient int age;
        public int age;
    
        public Person() {
        }
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    }
    
    二、写出对象方法:
    通过 public final void writeObject (Object obj)` 方法,将指定的对象写出。
    /*
     * java.io.ObjectOutputStream extends OutputStream
     * ObjectOutputStream:对象的序列化流
     * 作用:把对象以流的方式写入到文件中保存
     *
     * 构造方法:
     *     ObjectOutputStream(OutputStream out) 创建写入指定 OutputStream 的 ObjectOutputStream。
     *     参数:
     *         OutputStream out:字节输出流
     * 特有的成员方法:
     *     void writeObject(Object obj) 将指定的对象写入 ObjectOutputStream。
     */
    public static void main(String[] args) throws IOException {
        // 1、创建 ObjectOutputStream 对象,构造方法中传递字节输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\person.txt"));
    
        // 2、使用 ObjectOutputStream 对象中的方法 writeObject ,把对象写入到文件中
        oos.writeObject(new Person("小美女", 18));
    
        // 3、释放资源
        oos.close();
    }
    

    ObjectInputStream 类

    ObjectInputStream 反序列化流,将之前使用 ObjectOutputStream 序列化的原始数据恢复为对象。

    构造方法

    构造方法:
    public ObjectInputStream(InputStream in) : 创建一个指定 InputStream 的 ObjectInputStream 。

    反序列化操作一

    反序列化操作一:
    如果能找到一个对象的 class 文件,我们可以进行反序列化操作,调用 ObjectInputStream 读取对象的 public final Object readObject() 方法,读取一个对象。
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 1、创建 ObjectInputStream 对象,构造方法中传递字节输入流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\person.txt"));
    
        // 2、使用 ObjectInputStream 对象中的方法 readObject 读取保存对象的文件
        Object o = ois.readObject();
    
        // 3、释放资源
        ois.close();
    
        // 4、使用读取出来的对象(打印)
        System.out.println(o);
        Person p = (Person)o;
        System.out.println(p.getName() + p.getAge());
    }
    

    JVM 可以反序列化对象的条件:

    它必须能够找到 class 文件的类,如果找不到该类的 class 文件,则会抛出一个 ClassNotFoundException 异常。

    反序列化操作二

    还有一种情况,当 JVM 反序列化对象时,能找到 class 文件,但是 class 文件在序列化对象之后发生了修改,那么反序列化操作也会失败,会抛出一个 InvalidClassException 异常。

    InvalidClassException 原因如下:
    1、该类的序列版本号与从流中读取的类描述符的版本号不匹配 2、该类包含未知数据类型 3、该类没有可访问的无参数构造方法

    Serializable 接口给需要序列化的类,提供了一个序列版本号。该版本号的目的在于验证序列化的对象和对应类是否版本匹配。只要给上述 Person 类加入下面代码就可以防止该异常发生。代码如下:

    // 防止出现 InvalidClassException
        private static final long serialVersionUID = 1L;
    

    练习:序列化集合

    • 1、将存有多个自定义对象的集合序列化操作,保存到 list.txt 文件中。
    • 2、反序列化 list.txt ,并遍历集合,打印对象信息。

    案例分析

    案例分析:
    1、把若干学生对象 ,保存到集合中。 2、把集合序列化。 3、反序列化读取时,只需要读取一次,转换为集合类型。 4、遍历集合,可以打印所有的学生信息。

    代码实现

    // 练习:序列化集合
    // 当我们想在文件中保存多个对象的时候,可以把多个对象存储到一个集合中,对集合进序列化和反序列化。
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 1、定义一个存储 Person 对象的 ArrayList 集合
        ArrayList<Person> list = new ArrayList<>();
    
        // 2、往 ArrayList 集合中存储 Person 对象
        list.add(new Person("张三",18));
        list.add(new Person("李四",19));
        list.add(new Person("王五",20));
    
        // 3、创建一个序列化流 ObjectOutputStream 对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\list.txt"));
    
        // 4、使用 ObjectOutputStream 对象中的方法 writeObject ,对集合进行序列化
        oos.writeObject(list);
    
        // 5、创建一个反序列化 ObjectInputStream 对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\list.txt"));
    
        // 6、使用 ObjectInputStream 对象中的方法 readObject 读取文件中保存的集合
        Object o = ois.readObject();
    
        // 7、把 Object 类型的集合转换为 ArrayList 类型
        ArrayList<Person> list2 = (ArrayList<Person>)o;
    
        // 8、遍历 ArrayList 集合
        for (Person p : list2) {
            System.out.println(p);
        }
    
        // 9、释放资源
        ois.close();
        oos.close();
    }
    
    class Person implements Serializable{
        // 防止出现 InvalidClassException
        private static final long serialVersionUID = 1L;
        private String name;
        //private static int age;
        //private transient int age;
        public int age;
    
        public Person() {
        }
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    }
    

    打印流

    打印流概述

    平时我们在控制台打印输出,是调用 print 方法和 println 方法完成的,这两个方法都来自于 java.io.PrintStream 类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。接下来我们介绍 PrintStream 类

    PrintStream 类

    构造方法

    构造方法:
    public PrintStream(String fileName) : 使用指定的文件名创建一个新的打印流。

    改变打印流向

    System.out 就是 PrintStream 类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩一个 "小把戏" ,改变它的流向。示例代码如下:

    /**
     * java.io.PrintStream:打印流
     *     PrintStream 为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。
     * PrintStream特点:
     *     1.只负责数据的输出,不负责数据的读取
     *     2.与其他输出流不同,PrintStream 永远不会抛出 IOException
     *     3.有特有的方法,print,println
     *         void print(任意类型的值)
     *         void println(任意类型的值并换行)
     * 构造方法:
     *     PrintStream(File file):输出的目的地是一个文件
     *     PrintStream(OutputStream out):输出的目的地是一个字节输出流
     *     PrintStream(String fileName) :输出的目的地是一个文件路径
     * PrintStream extends OutputStream
     * 继承自父类的成员方法:
     *     - public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
     *     - public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
     *     - public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
     *     - public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
     *     - public abstract void write(int b) :将指定的字节输出流。
     * 注意:
     *     如果使用继承自父类的write方法写数据,那么查看数据的时候会查询编码表 97->a
     *     如果使用自己特有的方法print/println方法写数据,写的数据原样输出 97->97
     */
    public static void main(String[] args) throws FileNotFoundException {
        // 创建打印流 PrintStream 对象,构造方法中绑定要输出的目的地
        PrintStream ps = new PrintStream("F:\\print.txt");
        
        // 如果使用继承自父类的write方法写数据,那么查看数据的时候会查询编码表 97->a
        ps.write(97);
        
        // 如果使用自己特有的方法 print/println 方法写数据,写的数据原样输出 97->97
        ps.println(97);
        ps.println(8.8);
        ps.println('a');
        ps.println("HelloWorld");
        ps.println(true);
    
        // 释放资源
        ps.close();
    }
    

    示例代码二如下:

    /**
     * 可以改变输出语句的目的地(打印流的流向)
     * 输出语句,默认在控制台输出
     * 使用 System.setOut 方法改变输出语句的目的地改为参数中传递的打印流的目的地
     *     static void setOut(PrintStream out) : 重新分配“标准”输出流。
     */
    public static void main(String[] args) throws FileNotFoundException {
        System.out.println("我是在控制台输出");
    
        PrintStream ps = new PrintStream("F:\\print-out.txt");
        System.setOut(ps);    // 把输出语句的目的地改变为打印流的目的地
        System.out.println("我在打印流的目的地中输出");    // 不会在控制台显示,会在 print-out.txt 文件里显示
    
        ps.close();
    }
    

    Java 中的 IO 流到此结束了,欢迎评论区留言!!!

    Java
    • 文章作者:GuoShiZhan
    • 创建时间:2021-08-16 11:41:51
    • 更新时间:2021-08-16 11:41:51
    • 版权声明:本文为博主原创文章,未经博主允许不得转载!
    请 在 评 论 区 留 言 哦 ~~~
    1024