一、IO简介
1. 流的概念
数据流是一组有序、有起点和终点的字节数据序列,包括输入流和输出流。
流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。
2. 字节流和字符流的区别
Java中的流分为两种:
- 字节流:处理的最小数据单元是字节,可操作字节、字节数组
- 字符流:处理的最小数据单元是字符,可操作字符、字符数组、字符串
Java中的字符是Unicode编码,一个Unicode字符占用两个字节。
所有的文件(如图片、视频等)在硬盘或在传输时都是以字节(byte)的方式存储的,可以直接处理二进制数据;但实际中很多数据是文本,涉及到中文,因此提出字符流的概念,它是按虚拟机的encode来处理,程序先把数据读入到缓冲区(一段特殊的内存),然后对字节进行字符集的转化,在缓冲区形成字符,然后将字符从缓冲区写到文件。
因此:
字节流在操作时本身不会用到缓冲区(内存),可以直接操作文件;
字符流在操作时使用缓冲区暂存数据,再通过缓冲区操作文件。
3. in和out
所谓输入、输出,是从 内存 的角度理解的,或者 程序代码 也可以理解为内存
InputStream 输入流,把file文件里的数据读到内存
OutputStream 输出流,把数据从内存写到file文件里
4. IO 总体框架图
二、装饰模式
学习流之前,了解一下装饰模式。因为IO体系里的类继承关系沿用了装饰模式。
1. 装饰模式图
Component:抽象构建接口。<人>
ConcreteComponent:被装饰的原始对象,完成独立功能。<睡衣>
Decorator: 所有装饰器的抽象父类, 内部持有Component对象。<内衣>
ConreteDecoratorA:实际的装饰器对象, 实现具体添加功能。<运动装>
ConreteDecoratorB: 实际的装饰器对象, 实现具体添加功能。 <休闲装>
2. IO中的装饰器模式
InputStream 与 OutputStream 类似,下面以 InputStream 为例,来看一段代码:
File file = new File(path);
DataInputStream in = new DataInputStream( // 3.
new BufferedInputStream( // 2.
new FileInputStream(file))); // 1.
in.readBoolean();
in.readInt();
...
in.close();
- 为了从文件中读取数据,首先创建一个文件输入流 FileInputStream;
- 为了提升访问的效率,将它发送给具备缓存功能的 BufferedInputStream;
- 为了实现与机器类型无关的java基本类型数据的读取,所以,将缓存的流传递给了DataInputStream。
从这段代码可以看出:
其根本目的都是为 InputSteam 添加额外的功能。而这种额外功能的添加就是采用了装饰模式来构建的代码。
现在我们来看一下IO中的装饰模式图:
三、字节流
1. InputStream 和 OutputStream
表示二进制输入流、输出流。对输入流只能进行读操作,对输出流只能进行写操作,程序中需要根据待传输数据的不同特性而使用不同的流。
InputStream:将文件数据读入到程序
outputStream:将数据写出到文件
2. FileInputStream 和 FileOutputStream
(1)文件输入流 FileInputStream
/**
* 读取文件中的数据
* @param path 文件路径
*/
public void readFile(String path) {
String str = null;
try {
// 1.根据path路径实例化一个输入流对象
FileInputStream in = new FileInputStream(path);
// 2.创建byte数组
byte[] buffer = new byte[1024];
// 3.把数据读取到数组buffer中,返回读取字节总数,返回-1表示读到文件结尾
int byteRead = in.read(buffer);
// 4.关闭输入流
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
(2)文件输出流FileOutputStream
/**
* 将数据写出到文件
* @param path 文件路径
* @param content 写出内容
*/
public void writeFile(String path, String content) {
try {
// 1.根据path路径创建文件输出流对象
FileOutputStream out = new FileOutputStream(path);
// 2.String转换为byte数组
byte[] array = content.getBytes();
// 3.将byte数组写到输出流out
out.write(array);
// 4.关闭输出流
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
(3)读写结合举例 - 复制文件
/**
* 复制文件,将path1路径的文件复制到path2
* @param path1
* @param path2
* @return 复制是否成功
*/
public boolean copyFile(String path1, String path2) {
try {
File oldFile = new File(path1);
if (!oldFile.exists() || !oldFile.isFile()) {
return false;
}
// 文件输入输出流也可以直接传文件路径
FileInputStream in = new FileInputStream(path1);
FileOutputStream out = new FileOutputStream(path2);
byte[] buffer = new byte[1024];
// 1.读取字节数byteRead,-1代表文件末尾
int byteRead;
// 2.将path1路径的文件数据读取到buffer中
while (-1 != (byteRead = in.read(buffer))) {
// 3.将buffer写到out输出流的path2文件
out.write(buffer, 0, byteRead);
}
in.close();
out.flush();
out.close();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
3. BufferedInputStream 和 BufferedOutputStream
以输入流为例,前面的 FileInputStream 是字节流,而 BufferedInputStream 是带缓冲区的字节流。默认缓冲区大小是8M,能够减少访问磁盘的次数,提高文件读取性能。
所谓"缓冲",本质上是通过一个内部缓冲区数组实现的。
例如,在新建某输入流对应的BufferedInputStream后,当我们通过read()读取输入流的数据时,BufferedInputStream会将该输入流的数据分批的填入到缓冲区中。
每当缓冲区中的数据被读完之后,输入流会再次填充数据缓冲区;如此反复,直到我们读完输入流数据。
BufferedInputStream 与 FileInputStream 的区别:
- 相同点:都可以一次读取多个字节
- 不同点:
FileInputStream 每次只读取固定的字节数,会造成资源浪费从而产生阻塞。
BufferedInputStream 的read方法会读取尽可能多的字节,读取资源的效率更高。
代码示例
BufferedInputStream in = new BufferedInputStream(new FileInputStream(file1));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file2));
4. DataInputStream 和 DataOutputStream
数据输入流/数据输出流,继承自FilterInputStream/FilterOutputStream。
以输入流为例,data表示序列,DataInputStream这个类增加了readBoolean、readInt等扩展方法,表示可以设置数据类型,实现与机器类型无关的java基本类型数据的读取。
public class FileIOTest {
public static void main(String[] args) {
writeFile();
readFile();
}
/**
* 写出数据到file文件
*/
private void writeFile() {
try {
File file = new File("src/test/dataStream.txt");
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(file)));
out.writeBoolean(true);
out.writeByte((byte)40);
out.writeInt(100);
out.close();
} catch(Exception exception) {
e.printStackTrace();
}
}
/**
* 读取file文件数据
*/
private void readFile() {
try {
File file = new File("src/test/dataStream.txt");
DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream(file)));
// 注意:写和读的顺序要一致,否则读出来的数据就不对了
System.out.println(in.readBoolean());
System.out.println(in.readByte());
System.out.println(in.readInt());
in.close();
} catch(Exception exception) {
e.printStackTrace();
}
}
}
读取数据结果:
true
40
100
5. 总结
上图总结了输入输出流中主要的类的对应关系,十分对称。我们在读写操作时,也要成对使用,比如用ObjectOutputStream来写,就要用ObjectOutputStream来读,使用的序列化实体类也要一致。
四、字符流
字符流与字节流最大的区别就是有readLine()方法,有行的意义。
字符流的两大抽象函数是Writer和Reader,其子类继承关系同样沿用了装饰模式,这里只列举了几个常用的类:
1. 字符集
是一个系统支持的所有字符的集合。计算机要准确的存储和识别各种字符集符号,就需要进行字符编码,一套字符集必然至少有一套字符编码。
常见字符集有ASCII字符集、GBXXX字符集、Unicode字符集等。
- ASCII:是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、 换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)
- GBK:最常用的中文码表。使用双字节编码方案,共收录了 21003个汉字,同时支持繁体汉字、日韩汉字等
- UTF-8:可以用来表示Unicode标准中任意字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码,所有互联网协议均需支持UTF-8编码。
2. OutputStreamWriter 和 InputStreamReader
(1)OutputStreamWriter
使用指定的字符集,将写入其中的字符编码为字节
OutputStreamWriter的构造函数有:
- OutputStreamWriter(OutputStream out)
- OutputStreamWriter(OutputStream out, String charsetName)
- OutputStreamWriter(OutputStream out, Charset cs)
- OutputStreamWriter(OutputStream out, CharsetEncoder enc)
可见,新建OutputStreamWriter对象,可以指定字符集,不指定时使用平台默认的字符集进行编码。
String filePath = "src\test.txt";
// 1. 使用平台默认的字符集utf-8
// OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(filePath));
// 2. 指定字符集 GBK
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(filePath), "GBK");
writer.write("今天是星期天");
writer.flush(); // 刷新流,还可以继续写数据
writer.close(); // 关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据
(2)InputStreamReader
将字节流中的字节解码成字符
InputStreamReader的构造函数有:
- InputStreamReader(InputStream in)
- InputStreamReader(InputStream in, String charsetName)
- InputStreamReader(InputStream in, Charset cs)
- InputStreamReader(InputStream in, CharsetDecoder dec)
String filePath = "src\test.txt";
// 1. 默认格式UTF-8
// InputStreamReader reader = new InputStreamReader(new FileInputStream(filePath));
// 2. 指定字符集 GBK
InputStreamReader reader = new InputStreamReader(new FileInputStream(filePath), "GBK");
/**
* 1、一次读单个字符
int ch;
while ((ch = reader.read()) != -1) {
System.out.print((char)ch);
}
reader.close();
*/
/**
* 2、一次读一个字符数组
*/
char[] array = new char[1024];
int len;
while ((len = reader.read(array)) != -1){
System.out.println(new String(array, 0, len));
}
reader.close();
(3)读写结合举例 - 复制文件
/**
* 复制文件,将path1路径的文件复制到path2
* @param path1
* @param path2
* @return 复制是否成功
*/
public boolean copyFile(String path1, String path2) {
try {
File oldFile = new File(path1);
if (!oldFile.exists() || !oldFile.isFile()) {
return false;
}
InputStreamReader reader = new InputStreamReader(new FileInputStream(path1));
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(path2));
/**
* 1、一次 读+写 单个字符
int ch;
while ((ch = reader.read()) != -1) {
writer.write(ch);
}
*/
/**
* 2、一次 读+写 一个字符数组
*/
char[] array = new char[1024];
int len;
while ((len = reader.read(array)) != -1) {
writer.write(array, 0, len);
}
reader.close();
writer.flush();
writer.close();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
3. FileWriter 和 FileReader
(1)FileWriter
构造函数
public class FileWriter extends OutputStreamWriter {
public FileWriter(File file) throws IOException {
super(new FileOutputStream(file));
}
public FileWriter(String fileName) throws IOException {
super(new FileOutputStream(fileName));
}
...
}
FileWriter继承了OutputStreamWriter,从其构造函数可以看出,里面调用了 new FileOutputStream(file) ,是对OutputStreamWriter的再封装,只需传入file,且使用默认字符集。
// 使用
OutputStreamWriter writer = new FileWriter(file);
(2)FileReader
FileReader同理。
public class FileReader extends InputStreamReader {
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));
}
public FileReader(File file) throws FileNotFoundException {
super(new FileInputStream(file));
}
public FileReader(FileDescriptor fd) {
super(new FileInputStream(fd));
}
}
// 使用
InputStreamReader reader = new FileReader(file);
4. BufferedWriter 和 BufferedReader
BufferedWriter:将文本写入字符输出流,提供缓冲区存放字符,实现单个字符、数组、字符串的高效写入。可以指定缓冲区大小,或者使用默认大小。
BufferedReader:从字符输入流读取文本,提供缓冲区存放字符,实现字符、数组、行的高效读取,可以指定缓 冲区大小,或者使用默认大小。
字符缓冲流的特有功能
BufferedWriter
- void newLine():换行,写行分隔符字符串,由系统属性定义。
BufferedReader
- String readLine() :读一行文字。 结果包含行的内容的字符串,不包括任何行终止字符,如果流的结尾已经到达,则为null。
使用字符缓冲流复制文件
public boolean copyFile(String path1, String path2) {
try {
File oldFile = new File(path1);
if (!oldFile.exists() || !oldFile.isFile()) {
return false;
}
// 第一种写法
// BufferedReader reader = new BufferedReader(new FileReader(path1));
// BufferedWriter writer = new BufferedWriter(new FileWriter(path2));
// 第二种写法
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(path1)));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(path2)));
String line;
// 每次读一行数据
while ((line = reader.readLine()) != null) {
writer.write(line); // 写一行数据
writer.newLine(); // 换行
writer.flush(); // 刷新
}
reader.close();
writer.close();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
五、Flie
java.io.File文件操作
1. 新建文件
(1)路径拼接
// 获取sdcard路径
/** /storage/emulated/0 */
String sdDir = Environment.getExternalStorageDirectory().getAbsolutePath();
/** /storage/emulated/0/Android/data/包名/files */
String dataDir = context.getExternalFilesDir(null);
/** 拼接文件路径建议使用文件分隔符 File.separator */
String testPath = sdDir + File.separator + "download" + File.separator + "test.txt";
(2)File file = new File(String pathname)
File file = new File(testPath);
(3)File file = new File(File parent,String child)
根据 parent抽象路径名和child路径名字符串创建一个新File实例。
// 创建file对象,代表父文件夹
File parent = new File(sdDir + File.separator + "download");
// 创建file对象,代表文件夹中的文件
File file = new File(parent, "test.txt");
(4)boolean createNewFile()
创建新文件
File file = new File(sdDir + File.separator + "download");
boolean success = file.createNewFile();
2. 获取文件路径
(1)String getAbsolutePath()
返回文件的绝对路径
(2)String getName()
获取文件名
File file = new File("src/download/test.txt");
// src/download/test.txt
String filePath = file.getAbsolutePath();
// test.txt
String fileName = file.getName();
3. 创建文件夹
比如一个指定路径:
(1)boolean success = file.mkdir();
创建此抽象路径名指定的目录。【注:】只能在已经存在的目录内创建文件夹。
(2)boolean success = file.mkdirs();
创建一系列目录,包括所有必需但不存在的父目录。即可以创建多级目录。一般常用这种方法,避免提示目录不存在。
String path = sdDir + File.separator + "download" + File.separator + "Msg";
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
4. 删除文件
boolean delete()
删除此对象指定的文件
boolean success = file.delete();
5. 判断文件是否存在
boolean exists()
boolean a = file.exists();
6. 判断File是否为一个目录
boolean isDirectory()
boolean a = file.isDirectory();
7. 判断File是否为一个文件
boolean isFile()
boolean a = file.isFile();
8. 列出文件夹的内容
File[] listFiles()
一般与isDirectory()搭配使用
if (file.isDirectory()) {
File[] files = file.listFiles();
for (int i = 0; i < files.length; i++) {
File f = files[i];
// 操作文件f
}
}
9. 获取父文件
(1)String getParent()
返回此File对象的上一级全路径名,若没有上一级,则返回null
(2)File getParentFile()
返回此File对象的上一级文件对象,若没有上一级,则返回null
File file = new File("src/download/test.txt");
// 路径:src/download
String parentPath = file.getParent();
// file:src/download
File parentFile = file.getParentFile();
10. 文件改名
boolean renameTo(File dest)
File file1 = new File("src/download/test.txt");
File file2 = new File("src/download/abc.docx");
// 将file1的名字改名为file2对应的名字
boolean a = file1.renameTo(file2);
11. 递归删除文件
/**
* 删除file目录下的所有文件和文件夹
*/
void deleteFile(File file) {
if (!file.exists()) {
return;
}
if (file.isFile()) {
file.delete(); // 删除文件
return;
}
if (file.isDirectory()) {
File[] childFiles = file.listFiles();
if (null == childFiles || childFiles.length == 0) {
file.delete();
return;
}
for (File f : childFiles) {
deleteFile(f); // 删除子目录
}
file.delete(); // 删除本目录
}
}
12. 递归寻找文件
/**
* 遍历rootFile目录,寻找文件fileName,结果保存在列表fileList
*/
public void searchFile(File rootFile, String fileName, List<File> fileList) {
try {
if (null == rootFile || !rootFile.exists()) {
return;
}
File[] childFiles = rootFile.listFiles();
if (childFiles == null || childFiles.length == 0) {
return;
}
for (File child : childFiles) {
if (child.isDirectory()) {
searchFile(child, fileName, fileList); // 继续遍历子目录
} else {
if (child.getName().contains(fileName)) {
fileList.add(child); // 找到文件
}
}
}
} catch (Exception e) {
//
}
}
六、RandomAccessFile
随机访问文件,基于指针操作,可以从文件的任意位置访问。操作文件时可以传入只读、读写等模式。非常适用于断点续传等。
构造函数
public class RandomAccessFile implements DataOutput, DataInput, Closeable {
public RandomAccessFile(String name, String mode) {}
public RandomAccessFile(File file, String mode) {}
}
mode模式介绍
mode = “r”
只读,调用任何write方法将抛出 IOException
mode = “rw”
读写, 如果该文件不存在,则创建它
“rws”
读写,与“rw”一样,并且还要求对文件内容或元数据的每次更新都同步写入底层存储设备。
“rwd”
读写,与“rw”一样,并且还要求对文件内容的每次更新都同步写入底层存储设备。
特殊方法
long getFilePointer() // 返回文件记录中指针的当前位置
void seek(long pos) // 将文件记录移动到指定的pos位置
举例
/**
* 断点下载
* 从指定位置startPoint写入数据到文件file
*/
public void randomWriteFile(Response response, File file, long startPoint) {
RandomAccessFile randomAccessFile = null;
InputStream inputStream = null;
try {
randomAccessFile = new RandomAccessFile(file, "rwd");
randomAccessFile.seek(startPoint); // 移动指针到指定位置
inputStream = response.body().byteStream(); // 响应的输入字节流
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inputStream.read(buffer)) != -1) {
randomAccessFile.write(buffer, 0, len); // 此时从指定位置开始写入数据
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != randomAccessFile) {
try {
randomAccessFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}