1. IO 流简介
何为 IO 流?
在计算机中,内存和磁盘需要进行数据传输(内存从磁盘中读入数据,进行处理,然后写入磁盘中),而数据传输需要通道。所以,这里的 “IO 流”就是数据传输的通道。
内存和磁盘数据交互图如下:
大致意思:内存通过输入流从磁盘中读取数据,这个过程也称为读;内存通过输出流将数据保存到磁盘中,这个过程也称为写。
所以,简单来说,IO 流的作用就是 通过 IO 流,可以完成对磁盘文件的读和写。
2. IO 流的分类
- 按流的方向分(以内存为参照物):输入流(往内存中去)、输出流(从内存中出来)
- 按数据的读取方式:字节流、字符流
字节流和字符流的区别?
- 字节流:按照字节方式读取,一次读一个字节,可以读取任意类型的文件。如:文本文件、图片、视频文件,等…
- 字符流:按照字符方式读取,一次读取一个字符。它是为了更方便读取普通的文本文件,无法读取图片、word文件、视频文件等…
举个例子:
有一个 test.txt 文件,它里面的内容是:a中国
用字节流读:
第一次读:读一个字节,正好读到 ‘a’
第二次读:读一个字节,正好读到 ‘中’ 的一半
第三次读:读一个字节,读到 ‘中’ 的另一半
Tips:在 Windows 系统中,字母只占一个字节(在 Java 中,字母(char) 占用两个字节,但这个文件与 Java 无关,它只是 Windows 操作系统上的一个文件),而汉字占两个字节。
用字符流读:
第一次读:读一个字符,正好读到 ‘a’
第二次读:读一个字节,正好读到 ‘中’
3. Java 中的 IO 流
Java 中的 IO 流都在 java.io.* 包下,这块被称为“四大家族”(所有的 IO 类只有四类),四大家族的首领(这四类 IO 流的顶级父类)如下:
java.io.InputStream
:字节输入流java.io.OutputStream
:字节输出流java.io.Reader
:字符输入流java.io.Writer
:字符输出流
特点:
- 四大家族的首领都是抽象类
- 所有的 IO 流都实现了
java.io.Closeable
接口,都是可关闭的(close()
方法),用完之后要关闭,不然会占用很多资源 - 所有的输出流都实现了
java.io.Flushable
接口,都是可刷新的(flush()
方法),用完之后,记得刷新
Java 中需要掌握的 IO 流有16个,如下:
文件专属(操作文件):
java.io.FileInputStream
java.io.FileOutputStream
java.io.FileReader
java.io.FileWriter
转换流(将字节流转换为字符流):
java.io.InputStreamReader
java.io.OutputStreamWriter
缓冲专属:
java.io.BufferedInputStream
java.io.BufferedOutputStream
java.io.BufferedReader
java.io.BufferedWriter
数据流专属:
java.io.DataInputStream
java.io.DataOutputStream
标准输出流:
java.io.PrintStream
java.io.PrintWriter
对象专属流:
java.io.ObjectInputStream
java.io.ObjectOutputStream
这里重点以“FileInputStream/FileOutputStream”举例,因为其它流与之相似
3.1 FileInputStream/FileOutputStream
3.1.1 FileInputStream
FileInputStream
:文件字节输入流
FileInputStream
类的 read()
方法有三个重载形式:
public int read()
:读取单个字节,返回值为 int 类型,代表用该字节编码的的 ascii 码值public int read(byte [] b, int offset, int len)
:读取若干字节到字节数组中,返回值为读取的字节数public int read(char cbuf[])
:它的父类Reader
的方法。读取若干字节到字节数组中,返回值为读取的字节数
场景:使用文件字节流读取磁盘中的一个文件temp.txt,其内容为:abcdef
示例一: 读取文件内容(英文),一个字节一个字节地读取。
public class FileStreamDemo {
public static void main(String[] args) {
// 文件绝对路径名
String name = "E:\\zzc\\temp.txt";
FileInputStream fis = null;
try {
fis = new FileInputStream(name);
// 开始读
// read() 方法返回的是:读取到的“字节”本身。
// 例如:读取到的字符是'a',则返回97
int data = fis.read();
System.out.println(data); // 97
data = fis.read();
System.out.println(data); // 98
data = fis.read();
System.out.println(data); // 99
data = fis.read();
System.out.println(data); // 100
data = fis.read();
System.out.println(data); // 101
data = fis.read();
System.out.println(data); // 102
// 读取到文件末尾返回 -1
data = fis.read();
System.out.println(data); // -1
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != fis) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
注意:如果文件中的内容包含中文,再一个字节一个字节地读取,那么输出出来会出现乱码。因为,在 Java 中,汉字至少用了 2 个字节编码。如果按照上面示例,一个字节一个字节地输出,那么,这样只是截取了汉字的一部分,肯定会乱码。
【扩展知识】:
内码:String
、char
运行在内存中的编码
外码:!内码。如:class 文件中的 char/String
类型、序列化的 char/String
类型
UTF-8:英文字符占用一个字节;绝大多数汉字占用三个字节,个别汉字占用四个字节
UTF-16:英文字符占两个字节;绝大多数汉字(尤其是常用汉字)占用两个字节,个别汉字占用四个字节
在 Java 中,内码使用 UTF-16;外码使用 UTF-8。char
类型数据占用 2 个字节是针对内码的。
temp.txt 文件中只有一个汉字“我”,然后用
try (FileInputStream in = new FileInputStream("temp.txt")) {
int len = in.read();
System.out.println(len);
len = in.read();
System.out.println(len);
len = in.read();
System.out.println(len);
} catch (IOException e) {
e.printStackTrace();
}
它会输出:
230
136
145
结果表明:汉字“我”用了3个字节编码,每一个字节对应的 ASCII 码值分别为:230、136、145。
示例二: 读取文件内容(英文)。循环地一个字节一个字节地读取。
如果一个文件内容巨多,照上面那种方式肯定就不行了。所以,下面用循环的方式进行读取。
public class FileStreamDemo {
public static void main(String[] args) {
// 文件绝对路径名
String name = "E:\\zzc\\temp.txt";
FileInputStream fis = null;
try {
fis = new FileInputStream(name);
int data = 0;
while ((data = fis.read()) != -1) {
System.out.println(data);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != fis) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
但是,上面还会有一个问题:就是一次只读取一个字节,那么要读取所有字节,就要读取多次。这就会意味着内存要和磁盘频繁交互,这样性能会严重降低。所以,就要考虑一次能否读取多个字节。
查看 API 文档,可知:调用 read(byte[] b)
方法
示例三: 读取文件内容(英文)。多个字节多个字节地读取
public class FileStreamDemo {
public static void main(String[] args) throws Exception{
// 文件绝对路径名
String name = "E:\\zzc\\temp.txt";
FileInputStream fis = new FileInputStream(name);
byte[] bytes = new byte[4];
int length = 0;
while ((length = fis.read(bytes)) != -1) {
// 将 byte 数组转换成字符串
System.out.print(new String(bytes, 0, length));
}
if (null != fis) {
fis.close();
}
}
}
说明:
new String(bytes, 0, length)
:将字节转换为字符串
【总结】:使用文件字节输入流读取文件有三种:
int len = in.read();
:一次只读取一个字节,返回值是该字节对应的 ASCII 码值。如:读取了一个英文字符I
,则它返回73
。因为大写的I
对应的 ASCII 码为 73。但是,如果读取的是一个汉字,则需要读取三次才能读完,因为常用的汉字用 3 个字节编码,不常用的汉字用 4 个字节编码。while ((len = in.read()) != -1) {}
:一次只读一个字节,然后,循环读取。byte[] bytes = new byte[1024]; while ((len = in.read(bytes)) != -1) {}
:循环多个字节读取
3.1.2 FileOutputStream
FileOutputStream
:文件字节输出流
FileOutputStream
类的 write()
方法有四个重载形式:
public void write(int b)
:将指定的字节写入到文件输出流private native void write(int b, boolean append)
:是否追加public void write(byte b[])
:将字节数组写入到文件输出流public void write(byte b[], int off, int len)
:将字节数组中的一部分写入到文件输出流
场景:使用文件输出流向磁盘某个文件写入数据
示例一:
public class FileOutputStreamDemo {
public static void main(String[] args) throws Exception{
// temp.txt 文件不存在时,会自动新建
FileOutputStream fos = new FileOutputStream("temp.txt");
byte[] bytes = {97, 98, 99, 100};
fos.write(bytes);
// 写完之后,一定要刷新
fos.flush();
fos.close();
}
}
注意:如果文件不存在,则会新建文件。但是有一个前提,此文件所在的父级目录必须存在,否则,就会报错!!
如:FileOutputStream fos = new FileOutputStream("E:/zzc/temp.txt");
如果 E 盘下面没有 zzc 文件夹,那么,这行代码就会报错 java.io.FileNotFoundException: E:\zzc\temp.txt (系统找不到指定的路径。)
。所以,必须保证文件的父级目录存在。
而且每次向此文件中写入数据之前,都会清空此文件。
那么,每次向此文件写入数据时,能不能向此文件中追加内容呢?
使用这个构造方法:
- FileOutputStream(String name, boolean append):创建一个向具有指定 name 的文件中写入数据的输出文件流。如果第二个参数为 true,则将字节写入文件末尾处,而不是写入文件开始处。
如何向文件中写入汉字呢?
使用如下方式:
String data = "我爱中国";
// 将字符串转换为字节数组
byte[] bytes = data.getBytes();
fos.write(bytes);
说明:
byte[] bytes = data.getBytes();
将String
转换为字节类型
3.1.3 使用文件字节流
场景:使用文件字节流实现文件的赋值:把文件 temp.txt 中的内容复制到 test.txt 文件中去。
示例:
public class FileStreamDemo {
public static void main(String[] args) throws Exception{
FileInputStream fis = new FileInputStream("E:\\zzc\\temp.txt");
FileOutputStream fos = new FileOutputStream("E:\\zzc\\test.txt");
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
if (null != fos) {
fos.flush();
}
if (null != fis) {
fis.close();
}
fos.close();
}
}
不想手动关闭流,即:调用 close()
方法,可使用 try-with-resources
结构改进,如下:
public class FileStreamDemo {
public static void main(String[] args) throws Exception{
FileInputStream fis = new FileInputStream("E:\\zzc\\temp.txt");
FileOutputStream fos = new FileOutputStream("E:\\zzc\\test.txt");
try (FileInputStream in = new FileInputStream("E:\\zzc\\temp.txt");
FileOutputStream out = new FileOutputStream("E:\\zzc\\test.txt")) {
int length = 0;
byte[] bytes = new byte[1024];
while ((length = in.read(bytes)) != -1) {
out.write(bytes, 0 , length);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
如果还有不清楚 try-with-resources
结构的同学,可查看博文:【Exception】如何使用 try-with-resources 优雅地关闭资源
3.2 FileReader/FileWriter
3.2.1 FileReader
FileReader:文件字符输入流,只能读取普通文本,方便、快捷
FileReader
类的 read()
方法有两个重载形式:
public int read()
:读取单个字符,返回值为 int 类型,代表该字符的 ascii 码值public int read(char [] c, int offset, int len)
:读取若干字符到字符数组中,返回值为读取的字符数public int read(char cbuf[])
:它的父类Reader
的方法。读取若干字符到字符数组中,返回值为读取的字符数
场景:使用文件字符输入流读取一个txt文件(包含中文)
示例:
public class FileReaderDemo {
public static void main(String[] args) throws Exception{
FileReader fr = new FileReader("temp.txt");
char[] chars = new char[4];
int length = 0;
while ((length = fr.read(chars)) != -1) {
System.out.print(new String(chars, 0, length));
}
fr.close();
}
}
3.2.2 FileWriter
FileWriter
:文件字符输出流,只能写普通文本
FileWriter
的父类 Writer
的 write()
方法重载形式:
public void write(int c)
:public void write(char cbuf[])
:public void write(char cbuf[], int off, int len)
:public void write(String str)
:public void write(String str, int off, int len)
:
场景:使用文件字符输出流向一个txt文件写入数据(包含中文)
public class FileWriterDemo {
public static void main(String[] args) throws Exception {
FileWriter fw = new FileWriter("fw.txt");
char[] chars = {'我', '是', '中', '国', '人'};
fw.write(chars);
fw.flush();
fw.close();
}
}
3.3 BufferedReader/BufferedWriter
3.3.1 BufferedReader
BufferedReader:带有缓冲区的字符输入流。使用此流时,不需要自定义 byte/char 数组。自带缓冲。
示例一:
public class BufferedReaderDemo {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new FileReader("temp.txt"));
String line = null;
while (null != (line = br.readLine())) {
System.out.print(line);
}
br.close();
}
}
示例二:
public class BufferedReaderDemo {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("temp.txt")));
String line = null;
while (null != (line = br.readLine())) {
System.out.print(line);
}
br.close();
}
}
示例一与示例二的区别:示例二中使用了字节流来读取 temp.txt,但 BufferedReader 的构造方法中只能传入 Reader 类型的参数。所以,这里使用了 字节输入流来转换。
这里 BufferedWriter 就不说了。
3.4 DataOutputStream/DataInputStream
DataOutputStream:此流可以将数据以及数据类型写入文件中。
注意:此文件不是普通文件(无法用记事本打开)
3.5 PrintStream/PrintWriter
此类流是标准输出流。
3.5.1 PrintStream
PrintStream 是字节输出流,默认输出到控制台。
示例:
public class PrintStreamDemo {
public static void main(String[] args) {
String msg = "Hello PS";
PrintStream out = System.out;
// System.out.println(msg);
out.println(msg);
}
}
既然是默认输出到控制台。那么可以改变标准输出流的方向吗?
当然可以。
public class PrintWriterDemo {
public static void main(String[] args) throws Exception{
// 标准输出流不再指向控制台,而是指向 log 文件
PrintStream ps = new PrintStream(new FileOutputStream("log"));
// 修改输出方向,将输出方向从控制台修改为 log 文件
System.setOut(ps);
ps.print("我是中国人");
}
}
3.5.2 PrintWriter
示例:
public class PrintWriterDemo {
public static void main(String[] args) throws Exception{
// 若此文件不存在,则创建
PrintWriter pw = new PrintWriter("pw.txt");
String data = "Hello PrintWriter";
pw.write(data);
// 使用字符输出流,则需要调用 flush() 方法
// 字符内部也是用字节。相当于字符内部有缓存区,如果不调用 flush() 方法,那么数据只会存留在缓冲区中,
// 并不会输出到文本中去
pw.flush();
// 当调用 close() 时,它会先调用 flush() 方法。即使不显示调用 flush() 方法
pw.close();
}
}
3.6 ObjectOutputStream/ObjectInputStream
主要用于:对象的序列化和反序列化
示例(序列化):
public class User implements Serializable {
private static final long serialVersionUID = -2002612691013855369L;
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
...
}
User 类实现了 Serializable 接口
public class ObjectOutputStreamDemo {
public static void main(String[] args) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.txt"));
User user = new User("zzc", 18);
oos.writeObject(user);
oos.flush();
oos.close();
}
}
注意:参与序列化和反序列化的对象都要实现 Serializable 接口。
3.7 关于内存的流
public class MemoryStream {
public static void main(String[] args) throws Exception{
// 1. 内存字节流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String data = "Hello Memory";
// 将数据写入内存
baos.write(data.getBytes());
// 获取内存中的数据
byte[] bytes = baos.toByteArray();
//System.out.println(new String(bytes));
baos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
int ch;
while ((ch = bais.read()) != -1) {
System.out.print((char) ch);
}
bais.close();
// 2. 内存字符数组流
CharArrayWriter caw = new CharArrayWriter();
char[] chs = {'H', 'e', 'l', 'l', 'o'};
caw.write(chs);
caw.close();
// 返回内存中数据的引用
char[] data = caw.toCharArray();
CharArrayReader car = new CharArrayReader(data);
int ch;
while ((ch = car.read()) != -1) {
System.out.print((char) ch);
}
car.close();
// 3. 内存字符串流
StringWriter sw = new StringWriter();
String data = "Hello World";
sw.write(data);
sw.close();
String s = sw.toString();
StringReader sr = new StringReader(s);
int ch;
while((ch = sr.read()) != -1) {
System.out.print((char) ch);
}
sr.close();
}
}
好了,一些流的基本使用就到这儿了。多看看它们的 API 文档吧。