【连载】IO学习笔记——Java中的流

我是灼灼,一只初学Java的大一金渐层。
向往余秀华和狄兰·托马斯的疯狂,时常沉溺于将情感以诗相寄;追逐过王尔德、王小波的文字,后陷于毛姆和斯蒂芬·金不可自拔;热爱文学的浪潮,白日梦到底却总在现实里清醒;艳羡平静又极度渴盼奔跑的力量。
欢迎与我交流鸭· QQ:1517526827;
个人博客:https://blog.csdn.net/weixin_52777510?spm=1001.2101.3001.5343

IO——数据流与文件操作学习笔记

Java相关笔记正在连载中,欢迎来其他内容逛逛哟~

相关内容如下:
【连载1】Java笔记——基本结构与流程控制
【连载2】Java笔记——数组操作
【连载3】Java笔记——面向对象编程
【连载4】Java笔记——Java核心类
【连载5】Java笔记——异常处理
【连载6】Java笔记——反射和注解
【连载7】Java笔记——泛型
【连载8】Java笔记——集合
【连载9】MySQL学习笔记
【连载10】JDBC学习笔记
【连载11】Git和GitHub的使用笔记
【连载12】IO学习笔记


笔记内容来源于:
廖雪峰官方Java教程:IO部分~

IO

思维导图如下:

在这里插入图片描述

字符流:

例如把char[]数组Hi你好这4个字符用Writer字符流写入文件,并且使用UTF-8编码(防止乱码),得到的最终文件内容是8个字节,英文字符Hi各占一个字节,中文字符你好各占3个字节:

0x48
0x69
0xe4bda0
0xe5a5bd

反过来用Reader读取以UTF-8编码的这8个字节,会从Reader中得到Hi你好4个字符

File对象

思维导图如下:

在这里插入图片描述
创建和删除文件

File file = new File("/path/to/file");
if (file.createNewFile()) {
    
    
    // 文件创建成功:
    // TODO:
    if (file.delete()) {
    
    
        // 删除文件成功:
    }
}

读写临时文件

File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
        f.deleteOnExit(); // JVM退出时自动删除
        System.out.println(f.isFile());
        System.out.println(f.getAbsolutePath());

遍历文件和目录

 File f = new File("C:\\Windows");
        File[] fs1 = f.listFiles(); // 列出所有文件和子目录
        printFiles(fs1);
        File[] fs2 = f.listFiles(new FilenameFilter() {
    
     // 仅列出.exe文件
            public boolean accept(File dir, String name) {
    
    
                return name.endsWith(".exe"); // 返回true表示接受该文件
            }
        });
        printFiles(fs2);
    }

    static void printFiles(File[] files) {
    
    
        System.out.println("==========");
        if (files != null) {
    
    
            for (File f : files) {
    
    
                System.out.println(f);
            }
        }
        System.out.println("==========");
    }

Path对象

Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象
        System.out.println(p1);
        Path p2 = p1.toAbsolutePath(); // 转换为绝对路径
        System.out.println(p2);
        Path p3 = p2.normalize(); // 转换为规范路径
        System.out.println(p3);
        File f = p3.toFile(); // 转换为File对象
        System.out.println(f);
        for (Path p : Paths.get("..").toAbsolutePath()) {
    
     // 可以直接遍历Path
            System.out.println("  " + p);
        }

InputStream&OutputStream

思维导图如下:

在这里插入图片描述

InputStream

如何完整地读取一个FileInputStream的所有字节:

public void readFile() throws IOException {
    
    
    // 创建一个FileInputStream对象:
    InputStream input = new FileInputStream("src/readme.txt");
    for (;;) {
    
    
        int n = input.read(); // 反复调用read()方法,直到返回-1
        if (n == -1) {
    
    
            break;
        }
        System.out.println(n); // 打印byte的值
    }
    input.close(); // 关闭流
}

try ... finally来保证InputStream在无论是否发生IO错误的时候都能够正确地关闭:

public void readFile() throws IOException {
    
    
    InputStream input = null;
    try {
    
    
        input = new FileInputStream("src/readme.txt");
        int n;
        while ((n = input.read()) != -1) {
    
     // 利用while同时读取并判断
            System.out.println(n);
        }
    } finally {
    
    
        if (input != null) {
    
     input.close(); }
    }
}

利用Java 7引入的新的try(resource)的语法,只需要编写try语句,让编译器自动关闭资源,推荐写法:

public void readFile() throws IOException {
    
    
    try (InputStream input = new FileInputStream("src/readme.txt")) {
    
    
        int n;
        while ((n = input.read()) != -1) {
    
    
            System.out.println(n);
        }
    } // 编译器在此自动写入finally并调用close()
}

缓冲

利用缓冲区一次读取多个字节的代码:

public void readFile() throws IOException {
    
    
    try (InputStream input = new FileInputStream("src/readme.txt")) {
    
    
        // 定义1000个字节大小的缓冲区:
        byte[] buffer = new byte[1000];
        int n;
        while ((n = input.read(buffer)) != -1) {
    
     // 读取到缓冲区
            System.out.println("read " + n + " bytes.");
        }
    }
}

实现类

举个栗子:想从文件中读取所有字节,并转换成char然后拼成一个字符串,可以这么写:

public class Main {
    
    
    public static void main(String[] args) throws IOException {
    
    
        String s;
        try (InputStream input = new FileInputStream("C:\\test\\README.txt")) {
    
    
            int n;
            StringBuilder sb = new StringBuilder();
            while ((n = input.read()) != -1) {
    
    
                sb.append((char) n);
            }
            s = sb.toString();
        }
        System.out.println(s);
    }
}

要测试上面的程序,就需要在本地硬盘上放一个真实的文本文件。如果把代码稍微改造一下,提取一个readAsString()的方法:

public class Main {
    
    
    public static void main(String[] args) throws IOException {
    
    
        String s;
        try (InputStream input = new FileInputStream("C:\\test\\README.txt")) {
    
    
            s = readAsString(input);
        }
        System.out.println(s);
    }

    public static String readAsString(InputStream input) throws IOException {
    
    
        int n;
        StringBuilder sb = new StringBuilder();
        while ((n = input.read()) != -1) {
    
    
            sb.append((char) n);
        }
        return sb.toString();
    }
}

对这个String readAsString(InputStream input)方法进行测试相当简单,不一定要传入一个真的FileInputStream

public class Main {
    
    
    public static void main(String[] args) throws IOException {
    
    
        byte[] data = {
    
     72, 101, 108, 108, 111, 33 };
        try (InputStream input = new ByteArrayInputStream(data)) {
    
    
            String s = readAsString(input);
            System.out.println(s);
        }
    }

    public static String readAsString(InputStream input) throws IOException {
    
    
        int n;
        StringBuilder sb = new StringBuilder();
        while ((n = input.read()) != -1) {
    
    
            sb.append((char) n);
        }
        return sb.toString();
    }

}

这就是面向抽象编程原则的应用:接受InputStream抽象类型,而不是具体的FileInputStream类型,从而使得代码可以处理InputStream的任意实现类。

OutputStream

将若干个字节写入文件流:

public void writeFile() throws IOException {
    
    
    OutputStream output = new FileOutputStream("out/readme.txt");
    output.write(72); // H
    output.write(101); // e
    output.write(108); // l
    output.write(108); // l
    output.write(111); // o
    output.close();
}

一次性写入若干字节,用OutputStream提供的重载方法void write(byte[])来实现:

public void writeFile() throws IOException {
    
    
    OutputStream output = new FileOutputStream("out/readme.txt");
    output.write("Hello".getBytes("UTF-8")); // Hello
    output.close();
}

ByteArrayOutputStream在内存中模拟字节流的输出:

public class Main {
    
    
    public static void main(String[] args) throws IOException {
    
    
        byte[] data;
        try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
    
    
            output.write("Hello ".getBytes("UTF-8"));
            output.write("world!".getBytes("UTF-8"));
            data = output.toByteArray();
        }
        System.out.println(new String(data, "UTF-8"));
    }
}

同时读写两个文件

// 读取input.txt,写入output.txt:
try (InputStream input = new FileInputStream("input.txt");
     OutputStream output = new FileOutputStream("output.txt"))
{
    
    
    input.transferTo(output); // transferTo的作用是?
}

Filter模式

思维导图如下:

在这里插入图片描述
当需要给一个“基础”InputStream附加各种功能时,先确定能提供数据源的InputStream,因为需要的数据必须来自某个地方,例如,FileInputStream数据来源自文件:

InputStream file = new FileInputStream("test.gz");

如果希望FileInputStream能提供缓冲的功能来提高读取的效率,用BufferedInputStream包装这个InputStream,得到的包装类型是BufferedInputStream,但它仍然被视为一个InputStream

InputStream buffered = new BufferedInputStream(file);

假设该文件已经用gzip压缩,想要直接读取解压缩的内容,可以再包装一个GZIPInputStream

InputStream gzip = new GZIPInputStream(buffered);

无论包装多少次,得到的对象始终是InputStream,直接用InputStream来引用它,就可以正常读取:

┌─────────────────────────┐
│GZIPInputStream          │
│┌───────────────────────┐│
││BufferedFileInputStream││
││┌─────────────────────┐││
│││   FileInputStream   │││
││└─────────────────────┘││
│└───────────────────────┘│

Filter模式(或者装饰器模式:Decorator)图示:

                 ┌─────────────┐
                 │ InputStream │
                 └─────────────┘
                       ▲ ▲
┌────────────────────┐ │ │ ┌─────────────────┐
│  FileInputStream   │─┤ └─│FilterInputStream│
└────────────────────┘ │   └─────────────────┘
┌────────────────────┐ │     ▲ ┌───────────────────┐
│ByteArrayInputStream│─┤     ├─│BufferedInputStream│
└────────────────────┘ │     │ └───────────────────┘
┌────────────────────┐ │     │ ┌───────────────────┐
│ ServletInputStream │─┘     ├─│  DataInputStream  │
└────────────────────┘       │ └───────────────────┘
                             │ ┌───────────────────┐
                             └─│CheckedInputStream │
                               └───────────────────┘

类似的,OutputStream也是以这种模式来提供各种功能:

                  ┌─────────────┐
                  │OutputStream │
                  └─────────────┘
                        ▲ ▲
┌─────────────────────┐ │ │ ┌──────────────────┐
│  FileOutputStream   │─┤ └─│FilterOutputStream│
└─────────────────────┘ │   └──────────────────┘
┌─────────────────────┐ │     ▲ ┌────────────────────┐
│ByteArrayOutputStream│─┤     ├─│BufferedOutputStream│
└─────────────────────┘ │     │ └────────────────────┘
┌─────────────────────┐ │     │ ┌────────────────────┐
│ ServletOutputStream │─┘     ├─│  DataOutputStream  │
└─────────────────────┘       │ └────────────────────┘
                              │ ┌────────────────────┐
                              └─│CheckedOutputStream │
                                └────────────────────┘

编写一个CountInputStream对输入的字节进行计数:

public class Main {
    
    
    public static void main(String[] args) throws IOException {
    
    
        byte[] data = "hello, world!".getBytes("UTF-8");
        try (CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))) {
    
    
            int n;
            while ((n = input.read()) != -1) {
    
    
                System.out.println((char)n);
            }
            System.out.println("Total read " + input.getBytesRead() + " bytes");
        }
    }
}

class CountInputStream extends FilterInputStream {
    
    
    private int count = 0;

    CountInputStream(InputStream in) {
    
    
        super(in);
    }
    
    public int getBytesRead() {
    
    
        return this.count;
    }
    
    public int read() throws IOException {
    
    
        int n = in.read();
        if (n != -1) {
    
    
            this.count ++;
        }
        return n;
    }
    
    public int read(byte[] b, int off, int len) throws IOException {
    
    
        int n = in.read(b, off, len);
        if (n != -1) {
    
    
            this.count += n;
        }
        return n;
    }

}

在叠加多个FilterInputStream时,只需要持有最外层的InputStream,并且,当最外层的InputStream关闭时(在try(resource)块的结束处自动关闭),内层的InputStreamclose()方法也会被自动调用,并最终调用到最核心的“基础”InputStream,因此不存在资源泄露。

操作Zip|读取classpath资源|序列化

思维导图如下:

在这里插入图片描述

操作Zip

ZipInputStream是一种FilterInputStream,可以直接读取zip包的内容:

┌───────────────────┐
│    InputStream    │
└───────────────────┘
          ▲
          │
┌───────────────────┐
│ FilterInputStream │
└───────────────────┘
          ▲
          │
┌───────────────────┐
│InflaterInputStream│
└───────────────────┘
          ▲
          │
┌───────────────────┐
│  ZipInputStream   │
└───────────────────┘
          ▲
          │
┌───────────────────┐
│  JarInputStream   │
└───────────────────┘

读取Zip文件代码:

try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {
    
    
    ZipEntry entry = null;
    while ((entry = zip.getNextEntry()) != null) {
    
    
        String name = entry.getName();
        if (!entry.isDirectory()) {
    
    
            int n;
            while ((n = zip.read()) != -1) {
    
    
                ...
            }
        }
    }
}

写入Zip文件代码:

try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) {
    
    
    File[] files = ...
    for (File file : files) {
    
    
        zip.putNextEntry(new ZipEntry(file.getName()));//如果要实现目录层次结构,new ZipEntry(name)传入的name要用相对路径
        zip.write(getFileDataAsBytes(file));
        zip.closeEntry();
    }
}

读取class path资源

序列化

Java对象序列化:

public class Main {
    
    
    public static void main(String[] args) throws IOException {
    
    
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
    
    
            // 写入int:
            output.writeInt(12345);
            // 写入String:
            output.writeUTF("Hello");
            // 写入Object:
            output.writeObject(Double.valueOf(123.456));
        }
        System.out.println(Arrays.toString(buffer.toByteArray()));
    }
}

ObjectOutputStream既可以写入基本类型,如intboolean,也可以写入String(以UTF-8编码),还可以写入实现了Serializable接口的Object

反序列化:

try (ObjectInputStream input = new ObjectInputStream(...)) {
    
    
    int n = input.readInt();
    String s = input.readUTF();
    Double d = (Double) input.readObject();
}

除了能读取基本类型和String类型外,调用readObject()可以直接返回一个**Object对象**。要把它变成一个特定类型,必须强制转型。

Reader&Writer

思维导图如下:

在这里插入图片描述

Reader

区别:

InputStream Reader
字节流,以byte为单位 字符流,以char为单位
读取字节(-1,0~255):int read() 读取字符(-1,0~65535):int read()
读到字节数组:int read(byte[] b) 读到字符数组:int read(char[] c)

完整地读取一个FileReader所有字符

public void readFile() throws IOException {
    
    
    // 创建一个FileReader对象:
    Reader reader = new FileReader("src/readme.txt"); // 字符编码是???
    for (;;) {
    
    
        int n = reader.read(); // 反复调用read()方法,直到返回-1
        if (n == -1) {
    
    
            break;
        }
        System.out.println((char)n); // 打印char
    }
    reader.close(); // 关闭流
}

一次性读取若干字符并填充到char[]数组的方法:

public int read(char[] c) throws IOException

方法调用:先设置一个缓冲区,然后每次尽可能地填充缓冲区

public void readFile() throws IOException {
    
    
    try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
    
    
        char[] buffer = new char[1000];
        int n;
        while ((n = reader.read(buffer)) != -1) {
    
    
            System.out.println("read " + n + " chars.");
        }
    }
}

ReaderInputStream的关系?

除了特殊的CharArrayReaderStringReader普通的Reader实际上是基于InputStream构造的,因为**Reader需要从InputStream中读入字节流(byte**),然后,根据编码设置,再转换为char就可以实现字符流。如果查看FileReader的源码,它在内部实际上持有一个FileInputStream

InputStreamReader是一个可以把任何InputStream转换为Reader的转换器。

InputStream=>Reader

/ 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");

代码通过try (resource)改写(看起来有点像简单的合并):

try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
    
    
    // TODO:
}

实际上是FileReader的一种实现方式。

Writer

WriterOutputStream的区别如下:

OutputStream Writer
字节流,以byte为单位 字符流,以char为单位
写入字节(0~255):void write(int b) 写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b) 写入字符数组:void write(char[] c)
无对应方法

使用方法和FileReader类似:

try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
    
    
    writer.write('H'); // 写入单个字符
    writer.write("Hello".toCharArray()); // 写入char[]
    writer.write("Hello"); // 写入String
}

CharArrayWriter实现代码:

try (CharArrayWriter writer = new CharArrayWriter()) {
    
    
    writer.write(65);
    writer.write(66);
    writer.write(67);
    char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}

WriterOutputStream的关系?

除了CharArrayWriterStringWriter外,普通的Writer实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream。因此,OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器:

try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
    
    
    // TODO:
}

上述代码实际上就是FileWriter的一种实现方式。和上节的InputStreamReader一样。

PrintStream和PrintWriter|使用Files

思维导图如下:

在这里插入图片描述

PrintStream和PrintWriter

PrintWriter扩展了Writer接口,它的print()/println()方法最终输出的是char数据,使用方法如下:

public class Main {
    
    
    public static void main(String[] args)     {
    
    
        StringWriter buffer = new StringWriter();
        try (PrintWriter pw = new PrintWriter(buffer)) {
    
    
            pw.println("Hello");
            pw.println(12345);
            pw.println(true);
        }
        System.out.println(buffer.toString());
    }
}

使用Files

读写文件的简单方法:

例如把一个文件的全部内容读取为一个byte[]

byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));

如果是文本文件,可以把一个文件的全部内容读取为String

// 默认使用UTF-8编码读取:
String content1 = Files.readString(Paths.get("/path/to/file.txt"));
// 可指定编码:
String content2 = Files.readString(Paths.get("/path/to/file.txt"), StandardCharsets.ISO_8859_1);
// 按行读取并返回每行内容:
List<String> lines = Files.readAllLines(Paths.get("/path/to/file.txt"));

写入文件:

// 写入二进制文件:
byte[] data = ...
Files.write(Paths.get("/path/to/file.txt"), data);
// 写入文本并指定编码:
Files.writeString(Paths.get("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
// 按行写入文本:
List<String> lines = ...
Files.write(Paths.get("/path/to/file.txt"), lines);

笔记结束啦~
在这里插入图片描述
如果对你有帮助的话不要忘记一键三连噢~
谢谢鸭~

初次编写于2021/2/20日;

猜你喜欢

转载自blog.csdn.net/weixin_52777510/article/details/113886455