目录

IO Stream(IO 流)

IO 是 Input 和 OutPut 简写,是一个抽象概念,比如只要向某个对象给出数据,这个过程就叫输入,输出数据那么过程叫输出,承载输入输出的叫流(Stream)是一种装数据的管道。比如输入是将文件读到内存中,输出是将内存写入硬盘上,键盘按下键输入字符,显示器显示字符叫输出等等。

IO 流相关类在 java.io.* 下主要有以下几种类型 IO 流,它们都是抽象类:

  • java.io.inputStream,字节输入流,按照字节读取。
  • java.io.outputStream,字节输出流,按照字节输出。
  • java.io.Reader,字符流,按照字符读取。
  • java.io.Writer,字符输出流。

类名以 Stream 结尾就是字节流,以 Reader/Writer 结尾是字符流。

四个抽象类都实现了 java.io.Closeable 接口,在使用完流要手动调用 close() 方法关闭,以节省资源。

所有输出流都实现了 java.io.Flushable 接口,都带有 flush() 方法。向硬盘写完数据时要调用此方法。

主要学它们的实现类:

  • java.io.FileInputStream(掌握),文件操作流
    java.io.FileOutputStream(掌握)
    java.io.FileReader
    java.io.FileWriter

  • java.io.InputStreamReader,转换流,主要用于将字节流转为字符流
    java.io.OutputStreamWriter

  • java.io.BufferedReader,缓冲流
    java.io.BufferedWriter
    java.io.BufferedInputStream
    java.io.BufferedOutputStream

  • java.io.DataInputStream,数据流
    java.io.DataOutputStream

  • java.io.PrintWriter,标准输出流
    java.io.PrintStream(掌握)

  • java.io.ObjectInputStream(掌握),对象流。用于序列化和反序列化对象。
    java.io.ObjectOutputStream(掌握)

文件操作流

FileInputStream

主要就是用于读取二进制文件啥的,当然文本字符文件也可读取。

常见方法:

  • FileInputStream(String name),建立文件原始字节流,目标文件不存在就抛 FileNotFoundException 异常。

  • int read(),读下一个字节并返回读取到字节数据,当没字节可读时就返回 -1。使用 read() 前指针是在字节前的,每次调 read() 后直接会向下移动一个字节并返回字节数据,当达到最后一个字节后再向下移动就返回 -1。

    int read(byte[] b),与上不同的是可以读取多个字节,避免多次操作文件。按照 b 数组长度读字节放到 b 数组里,并返回已经读取字节长度。返回值是读到了几个字节,同样当字节读完就返回 -1。

  • int available(),返回当前流剩余多少字节可以读取。

  • long skip(long n),跳过当前流几个字节不读。

创建 file.txt:

abcdefg啊

创建 FileInputStreamTest01:

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

public class FileInputStreamTest01 {
    public static void main(String[] args) {
        FileInputStream fileinputstream = null;
        try {
            // 实例化时要捕获 FileNotFoundException 异常。
            fileinputstream = new FileInputStream("src/learnIO/file.txt");
            int readCount = 0;
            // 读取字节,需要捕获 IOException 异常。
            while((readCount = fileinputstream.read()) != -1) {
                System.out.println(readCount);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 每次使用完流一定要手动关闭。
            if (fileinputstream != null) {
                try {
                    // 需要主动捕获 IOException 异常。
                    fileinputstream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行输出:

97
98
99
100
101
102
103
229
149
138

这样一个个读取不太方便,如果有 10000 个字符就要循环向硬盘读 10000 次,效率低,因此提供了 read(byte[] b) 方法可以把读取到的字节放入 byte 数组里。

这里是创建了 byte 类型数组长度是 1,每次 read(bytes) 就按照 bytes 数组长度往里存字节,第一次存 a 和 b,但是存第二次是就会从头开始把读到的字节给覆盖掉。

// 准备 byte 数组存放 byte 数据
byte[] bytes = new byte[2];

// 读取文件一个字节
int readCount = fileInputStream.read(bytes);
// 通过 String 把 byte 数组转为字符串,第一个参数是要转换的数组,第二个参数是从索引几开始,第三个参数是转换几个字节。
// String(byte[] bytes, int offset, int length)
System.out.println("字符是:" + new String(bytes, 0, readCount));

readCount = fileInputStream.read(bytes);
System.out.println("字符是:" + new String(bytes, 0, readCount));

运行输出:

字符是:ab
字符是:cd

稍微改进下读取方式:

// 使用循环的方式读
int readCount = 0;
while((readCount = fileInputStream.read(bytes)) != -1) {
    System.out.println("字符是:" + new String(bytes, 0, readCount));
}

运行输出:

字符是:ab
字符是:cd
字符是:ef
字符是:g�
字符是:��

可以看到中文 ”啊” 被读为 3 个字节,所有没显示成字符。

前面创建 byte 数组都需要提前预估要存的字节数,其实 available() 可以获取字节数,这样就不用猜了。而 skip() 可以选定跳过几个字节不读取,相当于指针下移。

创建 FileInputStreamTest02:

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

public class FileInputStreamTest03 {
    public static void main(String[] args) {
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream("src/learnIO/file.txt");

            // int available(),返回当前流剩余多少字节可以读取。
            // 需要主动捕获 IOException 异常。
            int availableByte = fileInputStream.available();
            System.out.println("剩余可读字节数:" + availableByte);

            // long skip(long n),跳过当前流几个字节不读。
            long skipByte = fileInputStream.skip(1);
            System.out.println("跳过去几个字节:" + skipByte);
            System.out.println("剩余可读字节数:" + fileInputStream.available());


            // 创建 byte 数组用于存放
            byte[] bytes = new byte[availableByte];

            int readCount = fileInputStream.read(bytes);
            System.out.println(new String(bytes, 0, readCount));

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

运行输出:

剩余可读字节数:10
跳过去几个字节:1
剩余可读字节数:9
bcdefg啊

FileOutputStream

常用方法:

  • FileOutputStream(File file),实例化时如果目标文件不存在就创建出来,文件要是存在就会将其数据清空。

    FileOutputStream(File file, boolean append),这个 append 参数就是为了解决此问题,在指定文件时决定是追加还是覆盖,为 true 就是追加到文件最后一个字节后面,那么就不会清空原有文件数据。

  • write(byte[] b),向文件写入所有 b 数组所有字节。

    write(byte[] b, int off, int len),向文件写入 b 数组部分字节,off 是从第几个字节索引开始,len 是取几位字节。

创建 FileOutputStreamTest01.java:

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

public class FileOutputStreamTest01 {
    public static void main(String[] args) {
        FileOutputStream fileOutputStream = null;
        try {
            // 创建文件输出流
            fileOutputStream = new FileOutputStream("src/learnIO/临时文件");

            // 要写入的字符
            byte[] bytes = "把字符串转为byte数组".getBytes();

            // 写入所有字节
            fileOutputStream.write(bytes);

            // 数据写入完成必须 flush
            fileOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

查看 “临时文件” 内容如下:

把字符串转为byte数组

这样重复运行程序也不会把数据追加进文件中,需要在创建文件输出流时指定 append 为 true 才行。

创建 FileOutputStreamTest02.java:

public class FileOutputStreamTest02 {
    public static void main(String[] args) {
        FileOutputStream fileOutputStream = null;
        try {
            // 创建文件输出流
            fileOutputStream = new FileOutputStream("src/learnIO/临时文件", true);

            // 要写入的字符
            byte[] bytes = "把字符串转为byte数组".getBytes();

            // 写入 3 个字节。在UTF-8里中文刚好是3个字节。
            fileOutputStream.write(bytes, 0, 3);

            // 数据写入完成必须 flush
            fileOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

查看 “临时文件” 内容如下:

把字符串转为byte数组把

成功将 “把” 字追加写入到文件末尾。多运行几次程序将会重复追加。

学完文件输入输出流就可以写一个简单的复制文件的功能啦,经过测试复制过 .jpg、.mp4 两种类型文件可以成功打开。

创建 CopyFile.java:

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

public class CopyFile {
    public static void main(String[] args) {
        FileInputStream input = null;
        FileOutputStream output = null;

        try {
            input = new FileInputStream("D:\\WeChat\\Document\\WeChat Files\\wxid_cxlennjuc68r12\\FileStorage\\Video\\2021-11\\c18dfc44070f2b1ed6a0d9024b153836.mp4");
            output = new FileOutputStream("src/learnIO/c18dfc44070f2b1ed6a0d9024b153836.mp4");

            byte[] strogeByte = new byte[100 * 1024];
            int readByteNum = 0;

            // 每次 read 方法读到几个字节就写入几个字节。
            while ((readByteNum = input.read(strogeByte)) != -1) {
                output.write(strogeByte, 0, readByteNum);
            }

            // 写完就手动刷新
            output.flush();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 分别 try-catch 防止任何一个出现 NPE 异常导致无法关闭流。
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

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

FileReader

FileReader 使用 int read(char cbuf[]) 把数据读到 char 类型数组里,只是它只能操作字符,而 FileInputStream 和 FileOutputStream 什么类型的文件都能操作。

创建 “临时文件”:

把字符串转为byte数组把把把把

创建 FileReaderTest01.java:

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

public class FileReaderTest01 {
    public static void main(String[] args) {
        FileReader file = null;
        try {
            file = new FileReader("src/learnIO/临时文件");

            char[] chars = new char[10];
            int readCharCount = 0;

            // 读到几个 char 就转几个 char 为字符串。
            while ((readCharCount = file.read(chars)) != -1) {
                System.out.println(new String(chars, 0, readCharCount));
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (file != null) {
                try {
                    file.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行输出:

把字符串转为byte
数组把把把把

FileWriter

常用方法:

  • FileWriter(File file),创建文件流默认覆盖现有文件,没有此文件就直接创建出来。

    FileWriter(File file, boolean append),创建文件流时是不是覆盖。和 FileInputStream 一致。

  • write(String str),写入字符串。

    write(char cbuf[]),直接写入 char 数组。

创建 FileWriterTest01.java:

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

public class FileWriterTest01 {
    public static void main(String[] args) {
        FileWriter file = null;
        try {
            file = new FileWriter("src/learnIO/fileWriter.txt");

            file.write("写入一堆文本1");

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

本来没有文件,运行完就创建出 fileWriter.txt 并写入字符 "写入一堆文本1"。

FileReader 和 FileWriter 除文本外的数据无法操作。

创建 CopyFile01.java:

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

public class CopyFile01 {
    public static void main(String[] args) {
        FileReader readStream = null;
        FileWriter writeStream = null;

        try {
            // 复制二进制文件失败
            readStream = new FileReader("D:\\滴答清单\\pomo.wav");
            writeStream = new FileWriter("src/learnIO/滴答清单音频.wav");
            // 复制文本文件成功
//            readStream = new FileReader("src/learnCollection/ArrayListTest01.java");
//            writeStream = new FileWriter("src/learnIO/tmpFile.txt");


            char[] chars = new char[100];
            int readCount = 0;

            while ((readCount = readStream.read(chars)) != -1) {
                writeStream.write(chars, 0, readCount);
            }
            writeStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (readStream != null) {
                try {
                    readStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

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

运行完确实复制成功,但是无法打开。文本文件就没这种问题,只要内容是文本字符就可以复制。

缓冲流

BufferedReader

FileInputStream、FileReader 需要手动创建个数组每次从硬盘读取数据并保存数组里,BufferedReader 和 BufferedWriter 为了解决多次硬盘和内存交互问题,它每次把数据暂存到长度为 8192 的 byte/char 数组里,所以读写性能比普通的一个个读取字节/符流效率高。

以后涉及读写文件就使用 BufferedReader、BufferedWriter、BufferedInputStream 和 BufferedOutputStream 缓冲流就完了,效率高才是王道。

常用方法:

  • BufferedReader(Reader in),传入 Reader,或者它的子类。这个传进来的 in 称作节点流,而 BufferedReader 对象本身称作包装/处理流。
  • String readLine(),返回 String 类型一行文本字符(不包含换行符),读不到字符返回 null。

创建 BufferdReaderTest01.java:

import java.io.*;

public class BufferdReaderTest01 {
    public static void main(String[] args) {
        BufferedReader bufferedReader = null;
        try {
            FileReader file = new FileReader("src/learnIO/BufferdReaderTest01.java");

            // bufferedReader 是包装流,file 是节点流。
            bufferedReader = new BufferedReader(file);

            // 将读到的一行字符,存到 lineStr,没数据可读就返回 null
            String lineStr = null;
            while ((lineStr = bufferedReader.readLine()) != null) {
                System.out.print(lineStr + "\n");
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 只用关闭 BufferedReader 的 close() 方法即可,
            // 它会调用 FileReader 的 close()。
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行输出:

package learnIO;

import java.io.*;

public class BufferdReaderTest01 {
    public static void main(String[] args) {
        BufferedReader bufferedReader = null;
        try {
            FileReader file = new FileReader("src/learnIO/BufferdReaderTest01.java");

            bufferedReader = new BufferedReader(file);

            // 将读到的一行字符,存到 lineStr,没数据可读就返回 null
            String lineStr = null;
            while ((lineStr = bufferedReader.readLine()) != null) {
                System.out.print(lineStr + "\n");
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

因为 BufferedReader(Reader in) 构造方法只能传 Reader,如果使用了 FileInputStream 就没法读文件了,因为是字节流。要想使用可以通过 InputStreamReader(InputStream in) 将 FileInputStream 转为 InputStreamReader,而 InputStreamReader 是 Reader 子类那么就可以直接作为参数传进 BufferedReader。

创建 BufferdReaderTest02.java:

import java.io.*;

public class BufferdReaderTest02 {
    public static void main(String[] args) {
        BufferedReader bufferedReader = null;
        try {
            FileInputStream files = new FileInputStream("src/learnIO/BufferdReaderTest01.java");

            // 使用 InputStreamReader 将 FileInputStream 转为 Reader。
            InputStreamReader streamReader = new InputStreamReader(files);

            bufferedReader = new BufferedReader(streamReader);

            // 将读到的一行字符,存到 lineStr,没数据可读就返回 null
            String lineStr = null;
            while ((lineStr = bufferedReader.readLine()) != null) {
                System.out.print(lineStr + "\n");
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 只用关闭 BufferedReader 的 close() 方法即可,
            // 它会调用 FileReader 和 files 的 close()。可以通过 Debug 强制进入查看。
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行输出:

package learnIO;

import java.io.*;

public class BufferdReaderTest01 {
    public static void main(String[] args) {
        BufferedReader bufferedReader = null;
        try {
            FileInputStream files = new FileInputStream("src/learnIO/BufferdReaderTest01.java");
            InputStreamReader streamReader = new InputStreamReader(files);

//            FileReader file = new FileReader("src/learnIO/BufferdReaderTest01.java");

            bufferedReader = new BufferedReader(streamReader);

            // 将读到的一行字符,存到 lineStr,没数据可读就返回 null
            String lineStr = null;
            while ((lineStr = bufferedReader.readLine()) != null) {
                System.out.print(lineStr + "\n");
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 只用关闭 BufferedReader 的 close() 方法即可,
            // 它会调用 FileReader 的 close()。
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

BufferedWriter

常用方法:

  • BufferedWriter(Writer out),接收 Writer 类型对象。如果使用 FileOutputStream 创建文件流那么需要使用 OutputStreamWriter 包装下。
  • newLine(),写入换行符,每个系统平台下换行符都不尽相同,避免统一使用 \n。
  • write(String str),使用 BufferedWriter 父类 java.io.Writer 方法。

创建 test.txt:

111\n222\r\n333

创建 BufferedWriterTest01.java:

import java.io.*;

public class BufferedWriterTest01 {
    public static void main(String[] args) {
        BufferedWriter bufferedwriter = null;
        try {
            FileOutputStream fileout = new FileOutputStream("src/learnIO/test.txt", true);
            // 使用 OutputStreamWriter 转换了下,OutputStreamWriter 对象就是 Writer 子类。
            bufferedwriter = new BufferedWriter(new OutputStreamWriter(fileout));

            // 输出一个空行和字符
            bufferedwriter.newLine();
            bufferedwriter.write("新输出字符");

            // 输出后需要刷新
            bufferedwriter.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bufferedwriter != null) {
                try {
                    bufferedwriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行后 test.txt 文件成功新增字符:

111\n222\r\n333
新输出字符

第二种不需要转换直接使用 FileWriter 就能输出

创建 BufferedWriterTest02.java:

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

public class BufferedWriterTest02 {
    public static void main(String[] args) {
        BufferedWriter buff = null;
        try {
            FileWriter file = new FileWriter("src/learnIO/test.txt");
            buff = new BufferedWriter(file);

            // 输出字符
            buff.newLine();
            buff.newLine();
            buff.write("BufferedWriterTest02使用FileWriter成功输出字符");

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

查看 test.txt 发现原有文本已经清空已经新增了两空行和一行字符:


BufferedWriterTest02使用FileWriter成功输出字符

其实对应的 BufferedInputStream 和 BufferedOutputStream 两个字节缓冲流使用方法也是一样,只不过构造方法传递的是 InputStream 和 OutputStream。

数据流

用于写基本数据类型和 String 类型数据到文件,写完也可以读出来。

DataOutputStream

常用方法:

  • DataOutputStream(OutputStream out),接收一个 OutputStream 具体实现类写数据到文件。

  • writeBoolean(boolean v),写入 boolean 数据。

  • writeByte(int v),写 byte 数据,用 int 表示。

    writeBytes(String s),多个 byte 用 String 表示写入每一个字符按照 byte 写。

  • writeChar(int v),写入 int 数据。

    writeChars(String s),当有多个字符时可以用字符串表示,写入时还是按照 char 写。

  • writeDouble(double v),写入 double 数据。

  • writeFloat(float v),写入 float 数据。

  • writeInt(int v),写入 int 数据。

  • writeLong(long v),写入 long 数据。

  • writeShort(int v),写入 short 数据。

  • writeUTF(String str),写入 String 数据。

创建 DataOutputStreamTest01.java:

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

public class DataOutputStreamTest01 {
    public static void main(String[] args) {
        DataOutputStream dos = null;
        try {
            // 创建 Data 输出字节流。
            dos = new DataOutputStream(new FileOutputStream("src/learnIO/Data.txt"));

            // 准备数据写入
            dos.writeBoolean(true);
            dos.writeByte(97);
            dos.writeBytes("将一堆字符串作为 byte 写入");
            dos.writeChar(98);
            dos.writeChars("将一堆字符串作为 char 写入");
            dos.writeDouble(3.14);
            dos.writeFloat(3.14F);
            dos.writeInt(9999);
            dos.writeLong(2999999);
            dos.writeShort(32768);
            dos.writeUTF("使用 UTF-8 编码将字符串写入");

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

运行完将会输出一个 Data.txt 的二进制文件,正常情况下无法正常阅读:

a W&2\: byte �e b\N X[W{&N2O\N:   c h a r  Q�Qe@   ��Q�@H��  '     -ƿ�  %使用 UTF-8 编码将字符串写入   字符串

DataInputStream

常用方法:

  • DataInputStream(InputStream in)
  • byte readByte(),读取一个 byte 并返回。
  • char readChar(),读取一个 char 并返回。
  • Double readDouble(),读取一个 double 数值并返回。
  • float readFloat(),读取 float 数据并返回。
  • int readInt(),读取 int 数据并返回。
  • long readLong(),读取 long 数值并返回。
  • short readShort(),读取 short 数值并返回。

要想读得用 DataInputStream。读取时按照写入顺序读,不然抛 EOFException 异常。

import java.io.*;

public class DataInputStreamTest01 {
    public static void main(String[] args) {
        DataInputStream dos = null;
        try {
            // 创建 Data 输入字节流。
            dos = new DataInputStream(new FileInputStream("src/learnIO/Data.txt"));

            // 读取 boolean 数据
            System.out.println(dos.readBoolean());
            System.out.println("-----");

            // 读取 byte 数据
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println(dos.readByte());
            System.out.println("-----");

            // 读取 char 数据
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println(dos.readChar());
            System.out.println("-----");

            // 读取 double 数据
            System.out.println(dos.readDouble());
            System.out.println("-----");

            // 读取 float 数据
            System.out.println(dos.readFloat());
            System.out.println("-----");

            // 读取 int 数据
            System.out.println(dos.readInt());
            System.out.println("-----");

            // 读取 long 数据
            System.out.println(dos.readLong());
            System.out.println("-----");

            // 读取 short 数据
            System.out.println(dos.readShort());
            System.out.println("-----");

            // 读取字符串
            System.out.println(dos.readUTF());
            System.out.println(dos.readUTF());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (dos != null) {
                try {
                    dos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行输出:

true
-----
97
6
0
6
87
38
50
92
58
32
98
121
116
101
32
-103
101
-----
b
将
一
堆
字
符
串
作
为

c
h
a
r

写
入
-----
3.24
-----
3.14
-----
9999
-----
2999999
-----
-32768
-----
使用 UTF-8 编码将字符串写入
字符串

对象流

数据流只能读写基本数据类型和 String 类型,无法读写一个对象,比较弱,而对象流可以操作各种对象,ObjectInputStream 将对象序列化(Serialization)写入到文件或者转为字节在网络中传输,ObjectInputStream 从文件/网络中读取对象称反序列化(Deserialization),这也是持久化存储数据的一种方式。

ObjectOutputStream 和 ObjectInputStream

ObjectOutputStream 常用方法:

  • ObjectOutputStream(OutputStream out),创建对象流,里面可以传入 OutputStream 对象。
  • writeObject(Object obj),将对象序列化。

通过 ObjectOutputStream 对象 writeObject(Object obj) 方法就可以序列化了,obj 本身和 obj 成员变量需要 implement Serialible 接口,不然抛 NotSerializableException 异常。

虽然说实现了 Serialible 接口,但并不需要实现具体方法,跟进 java.io.Serialible 接口没有任何内容,这种接口叫标识接口(marker interface)。那实现 Serialible 接口作用是什么?一旦实现此接口对象序列化会自动添加序列化版本号(serialVersionUID )。

transient 修饰的成员变量不参加序列化。另一个不参与序列化的是 static 修饰的成员变量,因为是类所有的,不是对象专属。所有不参与序列化的内容最终是其类型默认值,比如 int 是 0,基本数据类型是 null。

创建 SeriaiztionTest01.java:

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

public class SeriaiztionTest01 {
    public static void main(String[] args) {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("src/learnIO/serialize.data"));

            // 序列化
            oos.writeObject("String 也实现了 Serializable 接口");
            oos.writeObject(new Test08());

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

class Test08 implements Serializable {
    private Test08 test;
    private static final int statitVar = 1;
    public int out() {
        System.out.println("Test08 out 方法");
        return statitVar;
    }

    @Override
    public String toString() {
        return "Test08{" +
                "test=" + test +
                '}';
    }
}

运行完输出 serialize.data 二进制文件:

�� t 'String 也实现了 Serializable 接口sr learnIO.Test08ݬ7G[
, L testt LlearnIO/Test08;xpp

紧接着可以用 ObjectOutputStream 将对象反序列化出来再代码中使用。

ObjectOutputStream 常用方法:

  • ObjectInputStream(InputStream in),创建对象流,里面可以传入 InputStream 对象。
  • readObject(Object obj),将对象反序列化,从流里读取出来。

创建 DeSeriaiztionTest01.java:

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

public class DeSeriaiztionTest01 {
    public static void main(String[] args) {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("src/learnIO/serialize.data"));

            // 按照顺序反序列化获取对象
            Object obj = ois.readObject();
            Object obj2 = ois.readObject();
            System.out.println((String) obj);
            System.out.println(((Test08) obj2).toString());
            System.out.println(((Test08) obj2).out());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 关闭流
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行输出:

String 也实现了 Serializable 接口
learnIO.Test08@6574b225

序列化对象完成后把序列化对象所在的类更改了成员变量,在反序列化过程中会抛错:

java.io.InvalidClassException: learnIO.Test08; local class incompatible: stream classdesc serialVersionUID = -2473541316976637396, local class serialVersionUID = 1535234684309933040

这里有必要说一下反序列化过程,执行 Object obj2 = ois.readObject(); 时会首先确认序列化文件中对象所在类是不是存在,不存在就报 ClassNotFoundException 异常,因为有可能某些使用人员只拿到序列化后的文件,自己通过对文件反序列化,而文件中的对象又找不到对应类就会产生类找不到的异常。

另一种情况是存在,拿 serialize.data 里对象 serialVersionUID 和对应类 serialVersionUID 进行比对,不一致就抛 InvalidClassException 异常。

比较 serialVersionUID 原因是因为防止反序列化过后的对象使用过程中以前类更新了产生兼容问题(这里我也没太搞明白到底是什么意思,暂且不究这么细),为了避免这种问题要在类中定义个 serialVersionUID 常量,避免使用编译器自动生成的,这样以后比对就不会抛出异常:

private static final long serialVersionUID = 42L;

主要是 serialVersionUID 值不和其他类重复就行。

标准输出流

PrintStream

System.out 默认是 PrintStream 类型,代表输出流,设备是显示器。

如果把通过 setOut(PrintStream out) 把输出流换成其他地方那么数据就在更改的地方显示,可以作为日志功能使用。

setOut(PrintStream out) 接收一个 PrintStream 类型对象,在实例化 PrintStream 又可以传入 OutputStream 类型对象 PrintStream(OutputStream out, boolean autoFlush),那么就传各种输出字节流对象。

创建 PrintStreamTest01.java:

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

public class PrintStreamTest01 {
    public static void main(String[] args) {
        PrintStream so = System.out;
        // 实际运行对象类型是 PrintStream
        System.out.println(so.getClass().getName());
        // 标准输出流无序调用 close() 方法
        so.println("标准输出默认输出在屏幕上");
        System.out.println();

        try {
            // 自动 flush() 和 close()
            System.setOut(new PrintStream(new FileOutputStream("src/learnIO/log.txt", true)));
            System.out.println("日志第一条");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行完将创建 log.txt:

日志第一条

PrintWriter

File 类

在 java.io.File 类里文件和文件夹都是以文件的概念存在。这个类用来操作目录和文件的,比如创建文件夹和空文件,获取它们的属性等信息,但是不是用来向写数据。

常用方法:

  • File(String pathname),通过路径来创建 File 对象。

  • boolean exists(),文件或目录是否存在。

  • boolean createNewFile(),创建文件。

  • boolean mkdir(),创建目录。

    boolean mkdirs(),递归创建目录。比如 /a/b/c,其中 b/c 都不存在那么都会创建出来。

  • boolean isFile(),是否是文件。

  • boolean isDirectory(),是否是目录。

  • String getParent(),获取上一级路径名。

  • String getAbsolutePath(),获取 File 对象绝对路径。

  • String getName(),获取文件名。

  • long lastModified(),获取最后一次修改时间(毫秒)。

  • long length(),获取文件大小(字节)。

  • File getParentFile(),获取上一级文件 File 对象。

  • File[] listFiles(),获取 File 对象下所有文件和文件夹。

创建 FileTest01.java:

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;

public class FileTest01 {
    public static void main(String[] args) {
        File file = new File("src/learnIO/FileTest01.java");

        // boolean exists()
        System.out.println(file.exists());

        // boolean isFile()
        System.out.println(file.isFile());

        // boolean isDirectory
        System.out.println(file.isDirectory());

        // String getParent()
        System.out.println(file.getParent());

        // String getAbsolutePath()
        System.out.println(file.getAbsolutePath());

        // String getName()
        System.out.println(file.getName());

        // long lastModified()
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(file.lastModified()));

        // long length()
        System.out.println(file.length());

        // boolean createNewFile()
        // boolean mkdir()
        // boolean mkdirs()
        File tmpFile = new File("D:/raingray");
        if (!tmpFile.exists()) {
            if (tmpFile.mkdir()) {
                System.out.println("创建文件夹成功");
            } else {
                System.out.println("创建文件夹失败");
            }
        }

        File tmpFiles = new File("D:/raingray/a/b/c");
        if (!tmpFiles.exists()) {
            if (tmpFiles.mkdirs()) {
                System.out.println("递归创建文件夹成功");
            } else {
                System.out.println("递归创建文件夹失败");
            }
        }

        File tmpFiles1 = new File("D:/raingray/a/b/c/filename");
        if (!tmpFiles1.exists()) {
            try {
                if (tmpFiles1.createNewFile()) {
                    System.out.println("递归创建文件成功");
                } else {
                    System.out.println("递归创建文件失败");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // File getParentFile()
        // File[] listFiles()
        File parentFileObj = tmpFile.getParentFile();
        File[] files = parentFileObj.listFiles();

        if (files != null) {
            for (File f : files) {
                System.out.println(f.getName());
            }
            ;
        }
    }
}

结合 IO 流实现目录拷贝功能。

创建 DirectoryCopy.java:

import java.io.*;
import java.util.ArrayList;

public class DirectoryCopy {
    private ArrayList<String> fileAbsPath = new ArrayList<>();

    /**
     * 递归遍历目录
     *
     * @param filePath 要遍历的目录
     * @return 遍历完成的路径数组
     */
    public ArrayList<String> getAllFiles(String filePath) {
        File dir = new File(filePath);
        File[] files = dir.listFiles();

        if (files == null) {
            return null;
        }

        for (File f : files) {
            String tmpFilePath = f.getAbsolutePath();
            fileAbsPath.add(tmpFilePath);
            getAllFiles(tmpFilePath);
        }
        return fileAbsPath;
    }

    /**
     * 拷贝每个文件
     *
     * @param destDir 具体复制到哪个目录下
     * @return
     */
    public boolean copyFile(String sourceDir, String destDir) {
        getAllFiles(sourceDir);
        for (String s : fileAbsPath) {
            File f = new File(s);
            BufferedOutputStream fos = null;
            BufferedInputStream fis = null;
            try {
                String readFileName = f.getAbsolutePath();
                File writeFileName = new File(destDir, readFileName.substring(2));
                // 如果读取到的是目录就把目标目录先创建出来,不需要读写操作。
                if (f.isDirectory()) {
                    new File(writeFileName.getCanonicalPath()).mkdirs();
                    continue;
                } else if (!writeFileName.getParentFile().exists()) {
                    // 因为父目录不存在,无法创建文件,所以要先要将其创建出来。
                    writeFileName.getParentFile().mkdirs();
                }

                fis = new BufferedInputStream(new FileInputStream(readFileName));
                fos = new BufferedOutputStream(new FileOutputStream(writeFileName, true));
                int redaByte;
                while ((redaByte = fis.read()) != -1) {
                    fos.write(redaByte);
                }              
                fos.flush();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                return false;
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            } finally {
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return true;
    }

    public static void main(String[] args) {
        String sourceFile = "D:\\gbb\\project\\中央国债登记结算有限责任公司\\20211111_中债路演移动应用";
        String destDir = "C:\\Users\\gbb\\Desktop";

        DirectoryCopy directoryCopy = new DirectoryCopy();
        directoryCopy.copyFile(sourceFile, destDir);
    }
}

IO 配合 Propertis 操作配置文件

读取

文件操作流中字节/符流 和 Map 集合 Propertis 配合使用,这在后续的 JDBC 中连接数据库时会用到。

比如写个数据库配置文件,里面存放主机、账户、数据库名这几项信息,以后配置变化了,通过程序直接读取而不需要重新编译程序。像是游戏或者其他程序中的快捷键都是用此方法实现的。

更抽象来讲,把所有需要动态读取的数据通过配置文件写进去,后续通过程序来读就不需要改动代码。

Java 里配置文件大都约定并不强制以 .propertis 结尾,这种配置文件称属性配置文件。

Propertis 加载流的方法:

  • void load(Reader reader),读取字符流里的属性和值。

    void load(InputStream inStream),读取字节流里的属性和值。

  • String getProperty(String key),获取对应属性的值。

创建 conf.propertis:

key=value

属性配置文件编写时有些注意事项:

  1. 配置文件里使用注释要用井号,例如 # 我是注释信息
  2. 配置文件 key 指向另一个值,读取时会以新值为准。
  3. 配置文件 key 和 value 分隔符,一般都使用 = 隔开最好不要在等号左右有空格,也不好把分隔符换成 : 等其他字符。

创建 ReadConf.java:

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

public class ReadConf {
    public static void main(String[] args) {
        FileReader fileReader = null;
        FileInputStream fileInputStream = null;

        try {
            fileReader = new FileReader("src/learnIO/conf.properties");
            fileInputStream = new FileInputStream("src/learnIO/conf.properties");

            Properties properties = new Properties();

            // Properties 读取字符/字节流里的的属性和值
//            properties.load(fileReader);
            properties.load(fileInputStream);

            // 读取属性对应的值
            String value = properties.getProperty("key");
            System.out.println(value);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行输出:

value

写入

  • void store(OutputStream out, String comments),通过字节流写入已经设置好的属性和值

    void store(Writer writer, String comments),通过字符流写入已经设置好的属性和值

创建 WriteConf.java:

import java.io.*;
import java.util.Properties;

public class WriteConf {
    public static void main(String[] args) {
        FileWriter fileWriter = null;
        FileOutputStream fileOutputStream = null;

        try {
            fileWriter = new FileWriter("src/learnIO/conf1.properties");
            fileOutputStream = new FileOutputStream("src/learnIO/conf2.properties");

            Properties properties = new Properties();
            // 写入属性对应的值
            Object obj = properties.setProperty("key", "value");
            // 对同一个 key 写值会被覆盖
            Object obj1 = properties.setProperty("key", "value1");
            // 输出当前 key 被指定的前一个值。
            System.out.println(obj1);

            // Properties 向字符/字节流里的写属性和值。
            // 写中文会存 Unicode
            properties.store(fileWriter, "注释信息");
            properties.store(fileOutputStream, null);

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

运行完创建俩文件。

conf1.properties:

#\u6CE8\u91CA\u4FE1\u606F
#Sun Dec 05 16:30:02 CST 2021
key=value1

conf2.properties:

#Sun Dec 05 16:30:02 CST 2021
key=value1

如果是对一个现有 .properties 其中某个 key 的 value 进行修改也是和上面一样:

假设有个 conf.properties:

key=value
key1=value1

要把 key1 修改成 key1value1。

创建 WriteConf2.java:

import java.io.*;
import java.util.Properties;

public class WriteConf2 {
    public static void main(String[] args) {
        FileOutputStream fileOutputStream = null;
        FileInputStream fileInputStream = null;

        try {
            fileInputStream = new FileInputStream("src/learnIO/conf.properties");
            Properties properties = new Properties();

            // 将配置文件加载进来
            properties.load(fileInputStream);
            // 对同一个 key 写值会被覆盖
            properties.setProperty("key1", "key1value1");

            // Properties 向字符/字节流里的写属性和值。
            fileOutputStream = new FileOutputStream("src/learnIO/conf.properties");
            properties.store(fileOutputStream, null);

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

运行完 conf.properties 变成:

#Sun Dec 05 16:50:55 CST 2021
key1=key1value1
key=value

整个逻辑就是先把配置文件读进来,最后把指定项修改一下。不过运行完会把原有数据顺序打乱掉,不知为什么。

最近更新:

发布时间:

讨论讨论

Asia/Shanghai