Java笔记十二——IO

IO
IO即Input/Output,内存<——>外部(磁盘,网络等)交互
InputStream / OutputStream是最基本的两种IO流
IO流顺序读写,单向流动,以byte(字节)为最小单位。

Reader/Writer
读写的是字符,按照char读写,字符流传输的最小数据单位是char

Reader和Writer本质上是一个能自动编解码的InputStream和OutputStream。
使用Reader,数据源虽然是字节,但我们读入的数据都是char类型的字符,原因是Reader内部把读入的byte做了解码,转换成了char。使用InputStream,我们读入的数据和原始二进制数据一模一样,是byte[]数组,但是我们可以自己把二进制byte[]数组按照某种编码转换为字符串如果数据源不是文本,就只能使用InputStream,如果数据源是文本,使用Reader更方便一些。Writer和OutputStream是类似的。

同步和异步
同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。

而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。

Java标准库的包java.io提供了同步IO,而java.nio则是异步IO。上面我们讨论的InputStream、OutputStream、Reader和Writer都是同步IO的抽象类,对应的具体实现类,以文件为例,有FileInputStream、FileOutputStream、FileReader和FileWriter。

IO流按照操作对象分类结构图如下
在这里插入图片描述

File对象

File对象可以表示文件,也可以表示目录
构造File对象,需要传入文件路径(可以是绝对路径也可以是相对路径)
FIle f = new File("C:\\Windows\\notepad.exe");
Windows以 \ 作为路径分隔符,所以字符串中需要用 \\ 表示一个 \
Linux平台使用 / 作为路径分隔符。
File对象3种形式表示的路径:

  • getPath(),返回构造方法传入的路径
  • getAbsolutePath(),返回绝对路径
  • getCanonicalPath,返回规范路径

文件和目录

调用isFile(),判断该File对象是否是一个已存在的文件,调用isDirectory(),判断该File对象是否是一个已存在的目录。
用File对象获取到一个文件时,还可以进一步判断文件的权限和大小:

  • boolean canRead():是否可读;
  • boolean canWrite():是否可写;
  • boolean canExecute():是否可执行;
  • long length():文件字节大小。

对目录而言,是否可执行表示能否列出它包含的文件和子目录。

创建和删除文件

当File对象表示一个文件时,
createNewFile():创建一个新文件
delete():删除该文件

File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件。

遍历文件和目录

当File对象表示一个目录时,

  • list()和listFiles():列出目录下的文件和子目录名
  • boolean mkdir():创建当前File对象表示的目录;
  • boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
  • boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。

Path

Path对象,它位于java.nio.file包,和File对象类似,但操作更简单,如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。

import java.io.*;
import java.nio.file.*;

public class Main {
    public static void main(String[] args) throws IOException {
        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

InputStream是一个抽象类,是所有输入流的超类。这个抽象类定义的一个最重要方法就是int read(),签名如下:
public abstract int read() throws IOException;
read():读取输入流的下一个字节,并返回字节表示的int值(0~255)。读到末尾返回-1,无法继续读取。
InputStream和OutputStream都是通过close()方法来关闭流。关闭流就会释放对应的底层资源。
文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException异常并抛出。所有与IO操作相关的代码都必须正确处理IOException。

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()
}

缓冲

很多流支持利用缓冲区一次读取多个字节。InputStream提供了两个重载方法支持读取多个字节:(返回值是实际读取的字节数)

  • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
  • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数
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.");
        }
    }
}

阻塞

InputStream的read()方法是阻塞的,必须等方法返回后才能继续执行下一行代码。

InputStream实现类

用FileInputStream可以从文件获取输入流,这是InputStream常用的一个实现类。此外,ByteArrayInputStream可以在内存中模拟一个InputStream,ByteArrayInputStream实际上是把一个byte[]数组在内存中变成一个InputStream:

import java.io.*;

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)) {
            int n;
            while ((n = input.read()) != -1) {
                System.out.println((char)n);
            }
        }
    }
}

小结
Java标准库的java.io.InputStream定义了所有输入流的超类:
FileInputStream实现了文件流输入;
ByteArrayInputStream在内存中模拟一个字节流输入。
总是使用try(resource)来保证InputStream正确关闭。

OutputStream

OutputStream是最基本的输出流,抽象类,所有输出类的超类。定义的最重要方法是void write(int b),签名如下:
public abstract void write(int b) throws IOException;
write方法只会写入一个字节到输出流。write()重载的一个方法void write(byte [ ])可以一次性写入若干字节。

OutputStream提供了close()方法关闭输出流,释放系统资源。也提供了**flush()**方法将缓冲区的内容真正地输出到目的地(磁盘、网络等,联系操作系统的知识)。
操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。flush()方法,能强制把缓冲区内容输出。

一般不需要手动调用flush()方法,某些情况下必须手动调用,例如在线即时聊天。通过OutputStream的write()方法将用户输入的一句话写入内存缓冲区,缓冲区满了(可能得用户输入N句话)才会再写入网络流,接收方等得花都谢了。解决方法是每输入一句话就调用flush()。

FileOutputStream

public void writeFile() throws IOException {
    try (OutputStream output = new FileOutputStream("out/readme.txt")) {
        output.write("Hello".getBytes("UTF-8")); // Hello
    } // 编译器在此自动为我们写入finally并调用close()
}

阻塞

write()方法也是阻塞的,参考read()方法

OutputStream实现类

用FileOutputStream可以从文件获取输出流。ByteArrayOutputStream可以在内存中模拟一个OutputStream,实际上是把一个byte[ ]数组在内存中变成一个OutputStream。

import java.io.*;

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"));
    }
}

同时操作多个AutoCloseable资源时,在try(resource) { … }语句中可以同时写出多个资源,用;隔开。例如,同时读写两个文件:

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

Filter模式(装饰器模式)

InputStream根据来源可以包括:

  • FileInputStream:从文件读取数据,是最终数据源;
  • ServletInputStream:从HTTP请求获取数据,是最终数据源;
  • Socket.getInputStream():从TCP连接读取数据,是最终数据源

如果要给InputStram添加缓冲、签名、加解密功能的话就要派生出很多子类,爆炸了啊(一个功能一个子类,功能叠加又很多子类,累死程序员?)。所以不使用继承机制来添加功能,而是使用Filter模式(或者装饰器模式:Decorator)。
JDK将InputStream分为两大类:

  • 直接提供数据的基础InputStream,例如:FileInputStream;ByteArrayInputStream;ServletInputStream等。
  • 提供额外附加功能的InputStream,例如BufferedInputStream;DigestInputStream;CipherInputStream等。
//基础InputStream,数据来源是文件
InputStream file = new FileInputStream("test.gz");
//用BufferedInputStream包装file这个InputStream,提供缓冲功能
InputStream buffered = new BufferedInputStream(file);
//假设该文件已经用gzip压缩了,我们希望直接读取解压缩的内容,就可以再包装一个GZIPInputStream:
InputStream gzip = new GZIPInputStream(buffered);

无论我们包装多少次,得到的对象始终是InputStream,我们直接用InputStream来引用它,就可以正常读取:
在这里插入图片描述
Filter模式通过少量的类实现就能实现各种功能的组合:
在这里插入图片描述
在这里插入图片描述

编写FilterInputStream

编写FilterInputStream,以便可以把自己的FilterInputStream“叠加”到任何一个InputStream中。
编写一个CountInputStream,对输入的字节进行计数:

import java.io.*;
public class Main {
	public static void main(String[] args) throws IOException {
		byte[] byte = "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 getByteRead() {
		return this.count;
	}
	
	public int read() throw IOException {
		int n = in.read();
		if(n != -1){
			this.count ++;
		}
		return n;
	}
	
	public int read(byte[] b, int off, int len) {
		int n = in.read(b, off, len);
		this.count += n;
		return n;
	}
}

小结
Java的IO标准库使用Filter模式为InputStream和OutputStream增加功能:

  • 可以把一个InputStream和任意个FilterInputStream组合;
  • 可以把一个OutputStream和任意个FilterOutputStream组合。

Filter模式可以在运行期动态增加功能(又称Decorator模式)

操作Zip

ZipInputStream是一种FilterInputStream流,可以直接读取zip包的内容
在这里插入图片描述

读取zip包

创建ZipInputStream,通常传入一个FileInputStream作为数据源,循环调用getNextEntry(),直到返回null,表示zip流结束。
一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,就用read()方法不断读取,直到返回-1。

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包

ZipOutputStream是一种FilterOutputStream,可以直接写入内容到zip包。先创建一个ZipOutputStream,通常是包装一个FileOutputStream,然后,每写入一个文件前,先调用putNextEntry(),然后用write()写入byte[]数据,写入完毕后调用closeEntry()结束这个文件。
小结
ZipInputStream可以读取zip格式的流,ZipOutputStream可以把多份数据写入zip包;
配合FileInputStream和FileOutputStream就可以读写zip文件。

读取classpath资源

很多java程序启动的时候都需要读取配置文件。

把资源存储在classpath中可以避免文件路径依赖;
Class对象的getResourceAsStream()可以从classpath中读取指定资源;
根据classpath读取资源时,需要检查返回的InputStream是否为null。

序列化

序列化:java对象——>二进制内容(byte[ ]数组)
反序列化:二进制内容(byte[ ]数组)——>java对象
好处:byte[ ]保存到文件或者通过网络传输到远程

Java对象序列化

java对象序列化前提:实现java.io.Serializable接口:没有定义任何方法,只是空接口。
使用ObjectOutputStream,将Java对象变为byte[ ]数组,写入字节流:

public class Main {
	public static void main(String[] args) {
		ByteArrayOutputStream buffer = new ByteArrayOutputStream();
		try(ObjectOutputStream output = new ObjectOutputStream(buffer)) {
			//写入int
			output.writerInt(12345);
			//写入String,以UTF-8编码
			output.writerUTF("Hello");
			//写入Object
			output.writerObject(Double.valueOf(123.456));
		}
		System.out.println(Arrays.toString(buffer.toByteArray()));
	}
}

反序列化

ObjectInputStream负责从一个字节流读取Java对象:

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

readObject()返回一个Object对象,可以通过强制类型转换转变成特定类型。
readObject()可能抛出的异常有:

  • ClassNotFoundException:没有找到对应的Class;
    常见于一台电脑上的Java程序把一个Java对象,例如,Person对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person类,所以无法反序列化。
  • InvalidClassException:Class不匹配。
    常见于序列化的Person对象定义了一个int类型的age字段,但是反序列化时,Person类定义的age字段被改成了long类型,所以导致class不兼容。

解决办法:java的序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识Java类的序列化“版本”,通常由IDE自动生成。

反序列化特点:由JVM直接构造出对象,不调用构造方法

安全性

Java的序列化机制可以导致一个实例能直接从byte[]数组创建,而不经过构造方法。(一看就是流氓行为)。一个精心构造的byte[]数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。
更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。

Reader

在这里插入图片描述
java.io.Reader是所有字符输入流的超类,最主要的方法是:
public int read() throws IOException; //读取字符流的下一个字符,返回字符表示的int,范围是0~65535,读到末尾返回-1
重载方法:int read(char[ ] c):一次性读取若干字符并填充到char[ ]数组
public int read(char[] c) throws IOException;

FileReader

FileReader是Reader的一个子类,它可以打开文件并获取Reader。

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

CharArrayReader

CharArrayReader可以在内存中模拟一个Reader,实际上是把一个char[]数组变成一个Reader,这和ByteArrayInputStream非常类似:

try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}

StringReader

StringReader可以直接把String作为数据源,它和CharArrayReader几乎一样:

try (Reader reader = new StringReader("Hello")) {
}

InputStreamReader

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

既然Reader本质上是一个基于InputStream的byte到char的转换器,那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。构造InputStreamReader时,我们需要传入InputStream,还需要指定编码,就可以得到一个Reader对象。

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

Writer

Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。
在这里插入图片描述

FileWriter

FileWriter就是向文件中写入字符流的Writer。

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

CharArrayWriter

CharArrayWriter可以在内存中创建一个Writer,它的作用实际上是构造一个缓冲区,可以写入char,最后得到写入的char[]数组,这和ByteArrayOutputStream非常类似:

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

StringWriter

StringWriter也是一个基于内存的Writer,它和CharArrayWriter类似。实际上,StringWriter在内部维护了一个StringBuffer,并对外提供了Writer接口。

OutputStreamWriter

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

PrintStream和PrintWriter

PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法:

写入int:print(int)
写入boolean:print(boolean)
写入String:print(String)
写入Object:print(Object),实际上相当于print(object.toString())

以及对应的一组println()方法,它会自动加上换行符。

我们经常使用的System.out.println()实际上就是使用PrintStream打印各种数据。其中,System.out是系统默认提供的PrintStream,表示标准输出:
PrintStream和OutputStream相比,除了添加了一组print()/println()方法,可以打印各种数据类型,比较方便外,它还有一个额外的优点,就是不会抛出IOException,这样我们在编写代码的时候,就不必捕获IOException。

PrintWriter

rintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/println()方法最终输出的是char数据。

使用Files

Java7开始,提供了Files和Paths这两个工具类。

import java.nio.*;

//把一个文件的全部内容读取为一个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);

Files工具类还有copy()、delete()、exists()、move()等快捷方法操作文件和目录。
Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值