0%

JAVA IO流详解

JAVA IO流

基础

IO

数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为输入Input 和输出Output ,即流向内存是输入流,流出内存的输出流,统称为 IO流。 Java中I/O操作主要是指使用java.io包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。

流代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象;流的本质是数据传输,我们根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。流的具体作用是数据源和目的地建立一个输送通道。

流的分类

流向

根据数据流向不同分为:

  • 输入流:读取外部数据(磁盘、网卡等设备的数据)到程序(内存)中。如InputStream,Reader

  • 输出流:把程序(内存)中的内容输出到磁盘、网卡设备中。如OutputStream、Writer

类型

根据处理数据类型的不同分为:

  • 字节流:可以用于读写包括二进制文件在内的任何类型文件。
  • 字符流:可以用于读写文本文件

功能

根据功能的不同分为:

  • 节点流:可以从或向一个特定的地方(节点)读写数据。如FileInputStream,FileReader。
  • 处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。

装饰器模式

Java I/O 使用了装饰者模式来实现。以 InputStream 为例:

  • InputStream 是抽象组件;
  • FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
  • FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能

img

实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。

1
2
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

Java设计模式12:装饰器模式 - 五月的仓颉 - 博客园 (cnblogs.com)

编码与解码

编码就是把字符转换为字节,而解码是把字节重新组合成字符。如果编码和解码过程使用不同的编码方式,就出现了乱码。

编码与解码的过程需要遵循某种规则,这种规则就是不同的字符编码。我们在刚刚学习编程的时候最早接触就是ASCII码,它主要是用来显示英文和一些符号,到后面还有接触到别的编码规则常用的有:gb2312,gbk,utf-8等。它们分别属于不同的编码集。

我们需要明确的是字符编码和字符集是两个不同层面的概念。

  • encoding是charset encoding的简写,即字符集编码,简称编码。
  • charset是character set的简写,即字符集。

编码是依赖于字符集的,一个字符集可以有多个编码实现,就像代码中的接口实现依赖于接口一样。

String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。如果不指定编码解码方式,默认的编码解码方式与平台有关,一般为 UTF-8。

1
2
3
4
5
6
7
String str1 = "中文";
//编码
byte[] bytes = str1.getBytes("UTF-8");

//解码
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);

JAVA IO流结构

Java IO流中所有的接口和类都放在java.io这个包下。其中最重要的就是5个类和一个接口:

  • File(文件特征与管理):File类是对文件系统中文件以及文件夹进行封装的对象,可以通过对象的思想来操作文件和文件夹。 File类保存文件或目录的各种元数据信息,包括文件名、文件长度、最后修改时间、是否可读、获取当前文件的路径名,判断指定文件是否存在、获得当前目录中的文件列表,创建、删除文件和目录等方法。
  • InputStream(二进制格式操作):抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。
  • OutputStream(二进制格式操作):抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。
  • Reader(文件格式操作):抽象类,基于字符的输入操作。
  • Writer(文件格式操作):抽象类,基于字符的输出操作。
  • RandomAccessFile(随机文件操作):一个独立的类,直接继承至Object.它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。
  • Serializable(序列化操作):是一个空接口,为对象提供标准的序列化与反序列化操作。

Java IO流的整体架构图如下:

IO流

File类

File类是文件和目录路径名的抽象表示形式,主要用于文件和目录的创建、查找和删除等操作。即Java中把文件或者目录(文件夹)都封装成File对象。也就是说如果我们要去操作硬盘上的文件或者目录只要创建File这个类即可。不过要注意的是File类只是对文件的操作类,只能对文件本身进行操作,不能对文件内容进行操作。

File类常用方法

获取文件的相关信息

  • String getAbsolutePath() :获取绝对路径名字符串。
  • String getName():获取文件或目录的名称。
  • String getPath():获取路径名字符串。
  • String getParent() :获取路径名父目录的路径名字符串;如果此路径名没有指定父目录,则返回 null。
  • Long lastModified():获取最后一次修改的时间(返回毫秒)。
  • Long length():获取文件的长度,如果表示目录则返回值未指定。

判断功能

  • Boolean isDirectory():判断此路径是否为一个目录
  • Boolean isFile():判断是否为一个文件
  • Boolean exists():判断文件或目录是否存在
  • Boolean canExecute():判断文件是否可执行
  • Boolean canRead():判断文件是否可读
  • Boolean canWrite():判断文件是否可写
  • Boolean isHidden():判断是否为隐藏文件

新建和删除

  • Boolean createNewFile():创建文件,如果文件存在则不创建,返回false,反之返回true。
  • Boolean mkdir():创建文件目录。如果此文件目录存在则不创建,如果此文件目录的上层目录不存在也不创建。
  • Boolean mkdirs(): 创建文件目录。如果上层文件目录不存在也会创建。
  • Boolean delete():删除的文件或目录。如果目录下有文件或目录则不会删除。

目录下文件的获取

  • String[] list():返回一个字符串数组,获取指定目录下的所有文件或者目录名称的数组。
  • File[] listFiles():返回一个抽象路径名数组,获取指定目录下的所有文件或者目录的File数组。

重命名文件

  • Boolean renameTo(File dest):把文件重命名到指定路径。

目录递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//递归地列出一个目录下所有文件

public static void listAllFiles(File dir) {
if (dir == null || !dir.exists()) {
return;
}
if (dir.isFile()) {
System.out.println(dir.getName());
return;
}
for (File file : dir.listFiles()) {
listAllFiles(file);
}
}

字节操作

无论是文本、图片、音频还是视频,所有的文件都是以二进制(字节)形式存在的,IO流中针对字节的输入输出提供了一系列的流,统称为字节流。字节流是程序中最常用的流。

InputStream

InputStream是所有字节输入流的父类,定义了所有字节输入流都具有的共同特征。其内部提供的方法如下(重点关注**read()**方法):

变量和类型 方法 描述
int available() 返回可以从此输入流中无阻塞地读取(或跳过)的字节数的估计值,可以是0,或者在检测到流结束时为0。
void close() 关闭此输入流并释放与该流关联的所有系统资源。
void mark(int readlimit) 标记此输入流中的当前位置。
boolean markSupported() 测试此输入流是否支持 markreset方法。
static InputStream nullInputStream() 返回一个不读取任何字节的新 InputStream
abstract int read() 从输入流中读取下一个数据字节,如果没有字节可用,因为已经到达了流的末端,则返回值-1。这个方法会阻塞,直到输入数据可用,检测到流的结束,或者抛出一个异常。
int read(byte[] b) 从输入流中读取一些字节数并将它们存储到缓冲区数组 b
int read(byte[] b, int off, int len) 从输入流 len最多 len字节的数据读入一个字节数组。
byte[] readAllBytes() 从输入流中读取所有剩余字节。
int readNBytes(byte[] b, int off, int len) 从输入流中读取请求的字节数到给定的字节数组中。
byte[] readNBytes(int len) 从输入流中读取指定的字节数。
void reset() 将此流重新定位到上次在此输入流上调用 mark方法时的位置。
long skip(long n) 跳过并丢弃此输入流中的 n字节数据。
long transferTo(OutputStream out) 从该输入流中读取所有字节,并按读取顺序将字节写入给定的输出流。

OutputStream

OutputStream是所有字节输出流的父类,定义了所有字节输出流都具有的共同特征。其内部提供的方法如下(重点关注write方法):

变量和类型 方法 描述
void close() 关闭此输出流并释放与此流关联的所有系统资源。
void flush() 刷新此输出流并强制写出任何缓冲的输出字节。
static OutputStream nullOutputStream() 返回一个新的 OutputStream ,它丢弃所有字节。
void write(byte[] b) b.length字节从指定的字节数组写入此输出流。
void write(byte[] b, int off, int len) 将从偏移量 off开始的指定字节数组中的 len字节写入此输出流。
abstract void write(int b) 将指定的字节写入此输出流。

文件复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void copyFile(String src, String dist) throws IOException {
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dist);

byte[] buffer = new byte[20 * 1024];
int cnt;

// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof,即文件尾
while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
out.write(buffer, 0, cnt);
}

in.close();
out.close();
}

字符操作

Reader

Reader是所有字符输入流的父类,定义了所有字符输入流都具有的共同特征。其内部提供的方法如下:

变量和类型 方法 描述
abstract void close() 关闭流并释放与其关联的所有系统资源。
void mark(int readAheadLimit) 标记流中的当前位置。
boolean markSupported() 判断此流是否支持mark()操作。
static Reader nullReader() 返回不读取任何字符的新 Reader
int read() 读一个字符。
int read(char[] cbuf) 将字符读入数组。
abstract int read(char[] cbuf, int off, int len) 将字符读入数组的一部分。
int read(CharBuffer target) 尝试将字符读入指定的字符缓冲区。
boolean ready() 判断此流是否可以读取。
void reset() 重置流。
long skip(long n) 跳过字符。
long transferTo(Writer out) 读取此阅读器中的所有字符,并按照读取的顺序将字符写入给定的编写器。

Writer

Reader是所有字符输出流的父类,定义了所有字符输出流都具有的共同特征。其内部提供的方法如下:

变量和类型 方法 描述
Writer append(char c) 将指定的字符追加到此writer。
Writer append(CharSequence csq) 将指定的字符序列追加到此writer。
Writer append(CharSequence csq, int start, int end) 将指定字符序列的子序列追加到此writer。
abstract void close() 关闭流,先冲洗它。
abstract void flush() 刷新流。
static Writer nullWriter() 返回一个新的 Writer ,它丢弃所有字符。
void write(char[] cbuf) 写一个字符数组。
abstract void write(char[] cbuf, int off, int len) 写一个字符数组的一部分。
void write(int c) 写一个字符。
void write(String str) 写一个字符串。
void write(String str, int off, int len) 写一个字符串的一部分。

实现逐行输出文本文件的内容

1
2
3
4
5
6
7
8
9
10
11
public static void readFileContent(String filePath) throws IOException {

FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);

String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
}

对象操作

序列化与反序列化

序列化就是将一个对象转换成字节序列,方便存储和传输。

  • 序列化:ObjectOutputStream.writeObject()
  • 反序列化:ObjectInputStream.readObject()

不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。

transient

transient 关键字可以使一些属性不会被序列化。

ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。

1
private transient Object[] elementData;

Serializable示例

序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void main(String[] args) throws IOException, ClassNotFoundException {

A a1 = new A(123, "abc");
String objectFile = "file/a1";

ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
objectOutputStream.writeObject(a1);
objectOutputStream.close();

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
A a2 = (A) objectInputStream.readObject();
objectInputStream.close();
System.out.println(a2);
}

private static class A implements Serializable {

private int x;
private String y;

A(int x, String y) {
this.x = x;
this.y = y;
}

@Override
public String toString() {
return "x = " + x + " " + "y = " + y;
}
}

缓冲

缓冲流也叫高效流,是处理流的一种,即是作用在流上的流。其目的就是加快读取和写入数据的速度。

缓冲流本身并没有IO功能,只是在别的流上加上缓冲效果从而提高了效率。当对文件或其他目标频繁读写或操作效率低,效能差时。这时使用缓冲流能够更高效的读写信息。因为缓冲流先将数据缓存起来,然后一起写入或读取出来。所以说,缓冲流还是很重要的,在IO操作时加上缓冲流提升性能。

Java IO流中对应的缓冲流有以下四个:

  • 字节缓冲流:BufferedInputStream、BufferedOutputStream

  • 字符缓冲流:BufferedReader、BufferedWriter

实例化一个具有缓存功能的字节流对象:

1
2
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

转换

转换流也是一种处理流,它提供了字节流和字符流之间的转换。在Java IO流中提供了两个转换流:InputStreamReader 和 OutputStreamWriter,这两个类都属于字符流。其中InputStreamReader将字节输入流转为字符输入流,继承自Reader。OutputStreamWriter是将字符输出流转为字节输出流,继承自Writer。

转换流的原理是:字符流 = 字节流 + 编码表。在转换流中选择正确的编码非常的重要,因为指定了编码,它所对应的字符集自然就指定了,否则很容易出现乱码,所以编码才是我们最终要关心的。

img

InputStreamReader

InputStreamReader是字节流到字符流的桥梁:它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法:

  • InputStreamReader(InputStream in):创建一个默认字符集字符输入流。
  • InputStreamReader(InputStream in, String charsetName):创建一个指定字符集的字符流。

OutputStreamWriter

OutputStreamWriter是字符流通向字节流的桥梁:用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法:

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

转换文件编码

1
2
3
4
5
6
7
8
//将读入UTF-8文件转换为GBK
InputStreamReader isr = new InputStreamReader(new FileInputStream("utf8.txt"),"UTF-8");;
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("gbk.txt"),"GBK");
int len;
char[] buffer = new char[1024];
while ((len=isr.read(buffer))!=-1){
osw.write(buffer,0,len);
}