IO
6.1 IO 流简介
IO 即 Input/Output
,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
(抽象基类) | 输入流 | 输出流 |
---|---|---|
字节流 | InputStream | OutputStream |
字符流 | Reader | Writer |
由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。
常用的节点流:
- 文件流: FileInputStream、FileOutputStrean、FileReader、FileWriter
- 字节/字符数组流: ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter
- 对数组进行处理的节点流(对应的不再是文件,而是内存中的一个数组)。
常用处理流:
- 缓冲流:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
- 作用:增加缓冲功能,避免频繁读写硬盘,进而提升读写效率。
- 转换流:InputStreamReader、OutputStreamReader
- 作用:实现字节流和字符流之间的转换。
- 对象流:ObjectInputStream、ObjectOutputStream
- 作用:提供直接读写Java对象功能
6.2 java.io.File类的使用
- 一个File对象代表硬盘或网络中可能存在的一个文件或者文件目录(俗称文件夹),与平台无关。
- File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。File对象可以作为参数传递给流的构造器。
- 想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录。
关于路径:
- **绝对路径:**从盘符开始的路径,这是一个完整的路径。
- **相对路径:**相对于
项目目录
的路径,这是一个便捷的路径,开发中经常使用。- IDEA中,main中的文件的相对路径,是相对于"
当前工程
" - IDEA中,单元测试方法中的文件的相对路径,是相对于"
当前module
"
- IDEA中,main中的文件的相对路径,是相对于"
public class FileObjectTest {
public static void main(String[] args) throws IOException {
String path = "D://";
File file = new File(path,"a.txt");
System.out.println(file.getAbsolutePath()); // D:\a.txt
// 通过父路径和子路径字符串
String path = "D://a.txt";
File file = new File(path);
System.out.println(file.getAbsolutePath()); // D:\a.txt
System.out.println(file.getName()); //a.txt
}
}
获取文件和目录基本信息
- public String getName() :获取名称
- public String getPath() :获取路径
public String getAbsolutePath()
:获取绝对路径- public File getAbsoluteFile():获取绝对路径表示的文件
public String getParent()
:获取上层文件目录路径。若无,返回null- public long length() :获取文件长度(即:字节数)。不能获取目录的长度。
- public long lastModified() :获取最后一次的修改时间,毫秒值
判断功能的方法
public boolean exists()
:此File表示的文件或目录是否实际存在。public boolean isDirectory()
:此File表示的是否为目录。public boolean isFile()
:此File表示的是否为文件。- public boolean canRead() :判断是否可读
- public boolean canWrite() :判断是否可写
- public boolean isHidden() :判断是否隐藏
创建、删除功能
public boolean createNewFile()
:创建文件。若文件存在,则不创建,返回false。public boolean mkdir()
:创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。public boolean mkdirs()
:创建文件目录。如果上层文件目录不存在,一并创建。public boolean delete()
:删除文件或者文件夹
删除注意事项:① Java中的删除不走回收站。② 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。
6.3 字节流
InputStream(字节输入流)
InputStream
用于从源头(通常是文件)读取数据(字节信息)到内存中,java.io.InputStream
抽象类是所有字节输入流的父类。
InputStream
常用方法 :
read()
:返回输入流中下一个字节的数据。返回的值介于 0 到 255 之间。如果未读取任何字节,则代码返回-1
,表示文件结束。read(byte b[ ])
: 从输入流中读取一些字节存储到数组b
中。如果数组b
的长度为零,则不读取。如果没有可用字节读取,返回-1
。如果有可用字节读取,则最多读取的字节数最多等于b.length
, 返回读取的字节数。这个方法等价于read(b, 0, b.length)
。read(byte b[], int off, int len)
:在read(byte b[ ])
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字节数)。skip(long n)
:忽略输入流中的 n 个字节 ,返回实际忽略的字节数。available()
:返回输入流中可以读取的字节数。close()
:关闭输入流释放相关的系统资源。
从 Java 9 开始,InputStream
新增加了多个实用的方法:
readAllBytes()
:读取输入流中的所有字节,返回字节数组。readNBytes(byte[] b, int off, int len)
:阻塞直到读取len
个字节。transferTo(OutputStream out)
: 将所有字节从一个输入流传递到一个输出流。
FileInputStream
是一个比较常用的字节输入流对象,可直接指定文件路径,可以直接读取单字节数据,也可以读取至字节数组中。
java.io.FileInputStream
类是文件输入流,从文件中读取字节。
FileInputStream(File file)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。FileInputStream(String name)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。
举例:
//read.txt文件中的内容如下:
abcde
public class FileInputStreamRead {
@Test
public void test1(){
FileInputStream fileInputStream = null;
File file = new File("read.txt");
try {
fileInputStream = new FileInputStream(file);
int read;
while ((read = fileInputStream.read()) != -1){
System.out.print((char) read);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fileInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Test
public void test2(){
FileInputStream fileInputStream = null;
try {
File file = new File("read.txt");
fileInputStream = new FileInputStream(file);
byte[] b = new byte[2];
int len;
while ((len = fileInputStream.read(b)) != -1){
String s = new String(b, 0, len);
System.out.println(s);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fileInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
输出:
abcde
ab
cd
e
不过,一般我们是不会直接单独使用 FileInputStream
,通常会配合 BufferedInputStream
(字节缓冲输入流,后文会讲到)来使用。
OutputStream(字节输出流)
OutputStream
用于将数据(字节信息)写入到目的地(通常是文件),java.io.OutputStream
抽象类是所有字节输出流的父类。
OutputStream
常用方法 :
write(int b)
:将特定字节写入输出流。write(byte b[ ])
: 将数组b
写入到输出流,等价于write(b, 0, b.length)
。write(byte[] b, int off, int len)
: 在write(byte b[ ])
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字节数)。flush()
:刷新此输出流并强制写出所有缓冲的输出字节。close()
:关闭输出流释放相关的系统资源。
FileOutputStream
是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。
java.io.FileOutputStream
类是文件输出流,用于将数据写出到文件。
public FileOutputStream(File file)
:创建文件输出流,写出由指定的 File对象表示的文件。public FileOutputStream(String name)
: 创建文件输出流,指定的名称为写出文件。public FileOutputStream(File file, boolean append)
: 创建文件输出流,指明是否在现有文件末尾追加内容。
举例:
public class FileoutputStreamWrite {
@Test
public void test1(){
FileOutputStream fileOutputStream = null;
try {
File file = new File("write.txt");
fileOutputStream = new FileOutputStream(file);
byte[] b = "abcde".getBytes();
fileOutputStream.write(b);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fileOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Test
public void test2(){
FileOutputStream fileOutputStream = null;
try {
File file = new File("write.txt");
//这段程序如果多运行几次,每次都会在原来文件末尾追加abcde
fileOutputStream = new FileOutputStream(file,true);
byte[] b = "abcde".getBytes();
fileOutputStream.write(b);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fileOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
//使用FileInputStream\FileOutputStream,实现对文件的复制
@Test
public void test3(){
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(new File("read.txt"));
fos = new FileOutputStream(new File("read1.txt"));
byte[] b = new byte[1024];
int len = 0;
while ((len = fis.read()) != -1 ){
fos.write(b,0,len);
}
System.out.println("复制成功!");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
类似于 FileInputStream
,FileOutputStream
通常也会配合 BufferedOutputStream
(字节缓冲输出流,后文会讲到)来使用。
DataOutputStream 用于写入指定类型数据,不能单独使用,必须结合 FileOutputStream
// 输出流
FileOutputStream fileOutputStream = new FileOutputStream("out.txt");
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
// 输出任意数据类型
dataOutputStream.writeBoolean(true);
dataOutputStream.writeByte(1);
ObjectInputStream
用于从输入流中读取 Java 对象(ObjectInputStream
,反序列化),ObjectOutputStream
将对象写入到输出流(ObjectOutputStream
,序列化)。
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("file.txt")
Person person = new Person("张三", "作者");
output.writeObject(person);
6.4 字符流
为什么 I/O 流操作要分为字节流操作和字符流操作呢?
主要有两点原因:
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。
- 如果我们不知道编码类型就很容易出现乱码问题。
I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
字符流默认采用的是 Unicode
编码,我们可以通过构造方法自定义编码。顺便分享一下之前遇到的笔试题:常用字符编码所占字节数?utf8
:英文占 1 字节,中文占 3 字节,unicode
:任何字符都占 2 个字节,gbk
:英文占 1 字节,中文占 2 字节。
Reader(字符输入流)
Reader
用于从源头(通常是文件)读取数据(字符信息)到内存中,java.io.Reader
抽象类是所有字符输入流的父类。
Reader
用于读取文本, InputStream
用于读取原始字节。
Reader
常用方法 :
read()
: 从输入流读取一个字符。read(char[] cbuf)
: 从输入流中读取一些字符,并将它们存储到字符数组cbuf
中,等价于read(cbuf, 0, cbuf.length)
。read(char[] cbuf, int off, int len)
:在read(char[] cbuf)
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字符数)。skip(long n)
:忽略输入流中的 n 个字符 ,返回实际忽略的字符数。close()
: 关闭输入流并释放相关的系统资源。
InputStreamReader
是字节流转换为字符流的桥梁,其子类 FileReader
是基于该基础上的封装,可以直接操作字符文件。
java.io.FileReader
类用于读取字符文件,构造时使用系统默认的字符编码和默认字节缓冲区。
FileReader(File file)
: 创建一个新的 FileReader ,给定要读取的File对象。FileReader(String fileName)
: 创建一个新的 FileReader ,给定要读取的文件的名称。
// 字节流转换为字符流的桥梁
public class InputStreamReader extends Reader {
}
// 用于读取字符文件
public class FileReader extends InputStreamReader {
}
public class FileReaderWriterTest {
@Test
public void test1(){
FileReader fileReader = null;
try {
//创建创建File类的对象,对应着物理磁盘上的某个文件
File file = new File("hello.txt");
//创建FileReader流对象,将File类的对象作为参数传递到FileReader的构造器中
fileReader = new FileReader(file);
int data;
/**
* 通过相关流的方法,读取文件中的数据
* read():每次从对接的文件中读取一个字符。并将此字符返回。
* 如果返回值为-1,则表示文件到了末尾,可以不再读取。
*/
while ((data = fileReader.read()) != -1){
System.out.print((char) data);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
//关闭相关的流资源,避免出现内存泄漏
fileReader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
//调用read(char[] cbuf),每次从文件中读取多个字符
@Test
public void test2(){
FileReader fileReader = null;
try {
File file = new File("hello.txt");
fileReader = new FileReader(file);
char[] cbuf = new char[5];
//记录每次读入的字符的个数
int len;
/**
* read(char[] cbuf) : 每次将文件中的数据读入到cbuf数组中,并返回读入到数组中的个数
*/
while ((len = fileReader.read(cbuf))!= -1){
String s = new String(cbuf, 0, len);
System.out.print(s);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fileReader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
输出:你好
Writer(字符输出流)
Writer
用于将数据(字符信息)写入到目的地(通常是文件),java.io.Writer
抽象类是所有字符输出流的父类。
Writer
常用方法 :
write(int c)
: 写入单个字符。write(char[] cbuf)
:写入字符数组cbuf
,等价于write(cbuf, 0, cbuf.length)
。write(char[] cbuf, int off, int len)
:在write(char[] cbuf)
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字符数)。write(String str)
:写入字符串,等价于write(str, 0, str.length())
。write(String str, int off, int len)
:在write(String str)
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字符数)。append(CharSequence csq)
:将指定的字符序列附加到指定的Writer
对象并返回该Writer
对象。append(char c)
:将指定的字符附加到指定的Writer
对象并返回该Writer
对象。flush()
:刷新此输出流并强制写出所有缓冲的输出字符。close()
:关闭输出流释放相关的系统资源。
OutputStreamWriter
是字符流转换为字节流的桥梁,其子类 FileWriter
是基于该基础上的封装,可以直接将字符写入到文件。
java.io.FileWriter
类用于写出字符到文件,构造时使用系统默认的字符编码和默认字节缓冲区。
FileWriter(File file)
: 创建一个新的 FileWriter,给定要读取的File对象。FileWriter(String fileName)
: 创建一个新的 FileWriter,给定要读取的文件的名称。FileWriter(File file,boolean append)
: 创建一个新的 FileWriter,指明是否在现有文件末尾追加内容。
// 字符流转换为字节流的桥梁
public class OutputStreamWriter extends Writer {
}
// 用于写入字符到文件
public class FileWriter extends OutputStreamWriter {
}
FileWriter
代码示例:
public class FileWriterWriterTest {
@Test
public void test1(){
FileWriter fileWriter = null;
try {
//创建File的对象
File file = new File("fw.txt");
/**
* 创建FileWriter的对象,将File对象作为参数传递到FileWriter的构造器中
* 如果输出的文件已存在,则会对现有的文件进行覆盖
*/
fileWriter = new FileWriter(file);
//如果输出的文件已存在,则会在现有的文件末尾写入数据
//fileWriter = new FileWriter(file,false);
//调用相关的方法,实现数据的写出操作
fileWriter.write("I love you !");
fileWriter.write("so sad".toCharArray());
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
//关闭资源,避免内存泄漏
fileWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
小结
- 因为出现流资源的调用,为了避免内存泄漏,需要使用try-catch-finally处理异常。
- 对于输入流来说,File类的对象必须在物理磁盘上存在,否则执行就会报FileNotFoundException。如果传入的是一个目录,则会报IOException异常。
- 对于输出流来说,File类的对象是可以不存在的。
- 如果File类的对象不存在,则可以在输出的过程中,自动创建File类的对象。
- 如果File类的对象存在,
- 如果调用FileWriter(File file)或FileWriter(File file,false),输出时会新建File文件覆盖已有的文件。
- 如果调用FileWriter(File file,true)构造器,则在现有的文件末尾追加写出内容。
6.5 字节缓冲流
IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。
字节缓冲流这里采用了装饰器模式来增强 InputStream
和OutputStream
子类对象的功能。
举个例子,我们可以通过 BufferedInputStream
(字节缓冲输入流)来增强 FileInputStream
的功能。
// 新建一个 BufferedInputStream 对象
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));
字节流和字节缓冲流的性能差别主要体现在我们使用两者的时候都是调用 write(int b)
和 read()
这两个一次只读取一个字节的方法的时候。由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。
字节缓冲流:BufferedInputStream
,BufferedOutputStream
BufferedInputStream(字节缓冲输入流)
BufferedInputStream
从源头(通常是文件)读取数据(字节信息)到内存的过程中不会一个字节一个字节的读取,而是会先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。这样大幅减少了 IO 次数,提高了读取效率。
BufferedInputStream
内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组。
缓冲区的大小默认为 8192 字节,当然了,你也可以通过 BufferedInputStream(InputStream in, int size)
这个构造方法来指定缓冲区的大小。
BufferedOutputStream(字节缓冲输出流)
BufferedOutputStream
将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了读取效率。
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) {
byte[] array = "JavaGuide".getBytes();
bos.write(array);
} catch (IOException e) {
e.printStackTrace();
}
类似于 BufferedInputStream
,BufferedOutputStream
内部也维护了一个缓冲区,并且,这个缓存区的大小也是 8192 字节。
例子:
public class BufferedIn_outputStream {
@Test
public void test(){
FileInputStream fis = null;
FileOutputStream fos = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
fis = new FileInputStream(new File("write.txt"));
fos = new FileOutputStream(new File("write1.txt"));
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
byte[] b = new byte[4];
int len;
while ((len = bis.read(b)) != -1){
bos.write(b,0,len);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
bis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
bos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
6.6 字符缓冲流
BufferedReader
(字符缓冲输入流)和 BufferedWriter
(字符缓冲输出流)类似于 BufferedInputStream
(字节缓冲输入流)和BufferedOutputStream
(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。
我们来看它们具备的特有方法。
- BufferedReader:
public String readLine()
: 读一行文字。 - BufferedWriter:
public void newLine()
: 写一行行分隔符,由系统属性定义符号。
6.7 打印流
System.out
实际是用于获取一个 PrintStream
对象,print
方法实际调用的是 PrintStream
对象的 write
方法。
PrintStream
属于字节打印流,与之对应的是 PrintWriter
(字符打印流)。PrintStream
是 OutputStream
的子类,PrintWriter
是 Writer
的子类。
6.8 随机访问流
这里要介绍的随机访问流指的是支持随意跳转到文件的任意位置进行读写的 RandomAccessFile
。
RandomAccessFile
的构造方法如下,我们可以指定 mode
(读写模式)。
// openAndDelete 参数默认为 false 表示打开文件并且这个文件不会被删除
public RandomAccessFile(File file, String mode)
throws FileNotFoundException {
this(file, mode, false);
}
// 私有方法
private RandomAccessFile(File file, String mode, boolean openAndDelete) throws FileNotFoundException{
// 省略大部分代码
}
读写模式主要有下面四种:
r
: 只读模式。rw
: 读写模式rws
: 相对于rw
,rws
同步更新对“文件的内容”或“元数据”的修改到外部存储设备。rwd
: 相对于rw
,rwd
同步更新对“文件的内容”的修改到外部存储设备。
文件内容指的是文件中实际保存的数据,元数据则是用来描述文件属性比如文件的大小信息、创建和修改时间。
RandomAccessFile
中有一个文件指针用来表示下一个将要被写入或者读取的字节所处的位置。我们可以通过 RandomAccessFile
的 seek(long pos)
方法来设置文件指针的偏移量(距文件开头 pos
个字节处)。如果想要获取文件指针当前的位置的话,可以使用 getFilePointer()
方法。
RandomAccessFile
比较常见的一个应用就是实现大文件的 断点续传 。何谓断点续传?简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础。