Java中的IO流
# Java中的IO流
# 什么是IO流
Java中的I/O(输入/输出)流是用于处理设备间数据传输的类和接口集合
。换句话说,它是存储和读取数据的解决方案。
那IO分别对应英文中的input,output,那么要加上流呢?为什么叫IO流。
“流”这个词很好地描述了数据如何从一个地方流向另一个地方。就像水流一样,数据通过I/O流以连续的方式从源端(例如文件、网络连接等)流向目的端(例如程序内存)。无论是字节流还是字符流,数据都是以一种连续的序列形式被处理和传输的。
向程序中输入数据称为——输入流。反之,程序向外输出数据称为——输出流。
# File类
IO流跟file是息息相关的,关于它的相关概念以及常用用法你还清楚吗。
File,它是表示文件或者是文件夹的路径。
利用file我们可以获取文件的信息,判断文件的类型,还可以进行创建或者是删除的操作。但是这些操作只能是对文件本身进行操作,不能读写文件里面的数据。如果说我们想要读写数据,就必须要用到IO流。
# File的一些方法
File类的操作方法(一些方法)
- isDirectory() 是否为文件夹,isFile() 是否为文件
- getPath() 得到file的路径,
- getName() 得到最后一层的名字,getParent() 得到去掉最后一层的路径
- getParentFile() 得到父类路径的新文件
- renameTo() 改名
- mkdir() 创建新文件夹,只能创建一层,mkdirs() 创建新文件夹,可以多层
- exists() 路径是否存在,delete() 删除文件或者目录(为空的目录)
- list() 返回该路径下文件或者文件夹的名字数组,listFiles() 返回该路径下文件或者文件夹组成的File数组
- separator 代替文件或文件夹路径的斜线或反斜线,防止跨平台出现错误
# 一些例子
// D:\\my_mp3\\sugar.txt 用 “\\” 为了避免转义
// 或者 D:/my_mp3/sugar.txt
File file = new File("D:"+File.separator+"my_mp3"+File.separator+"sugar.txt");
if(!file.exists())
{
file.createNewFile();
// file.isDirectory();
// file.isFile();
System.out.println(file.getName()+"文件创建成功");
}
else
{
System.out.println(file.getName()+"已经存在了");
boolean result = file.delete();
System.out.println("删除文件成功");
}
File[] files = file.listFiles(); //列出当前目录下 所有文件 用File对象形式返回
for (File e:files) {
System.out.println("length:"+e.length());
System.out.println("是否为隐藏文件"+e.isHidden());
System.out.println("是否为可读文件"+e.canRead());
System.out.println("最后修改的时间为:"+e.lastModified());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
以上代码,Jdk为 1.8
// 找指定后缀结尾的文件
public static void findFile(File target,String str)
{
if(target==null)
return ;
if(target.isDirectory())
{
File[] files =target.listFiles();
if(files!=null)
{
for (File e:files) {
findFile(e,str);
}
}
}
else {
String sname = target.getName().toLowerCase();
if(sname.endsWith(str))
System.out.println(target.getAbsolutePath());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# IO流的分类
# 流向分类
IO流按照流的方向来分的话,可以分为两种。
- 一个是输入流,用来读取本地文件(或网络)中的数据。
- 还有一个是输出流,用来把数据写到本地文件(或网络)当中
# 操作的文件类型分类
除此之外它还有第二种分类方式,按照操作文件的类型来分的话,
可以分为:字节流,还有字符流
。
其中字节流
它可以操作所有类型的文件。比如说它可以操作图片文件、文本文件、音频文件、视频文件等等等等,所有类型的文件它都可以操作。
而字符流它只能操作纯文本文件。
字节流能操作所有类型文件,而字符流,只能操作一种字符流,这样设计岂不是多次一举,还有意义吗?还是说最初只有字符流,字节流是后来设计出来的?
# 对应的类
字节流有2个顶级的抽象类,字符流也有2个顶级抽象类,分别如下
// 输入字节流
InputStream inputStream;
// 输出字节流
OutputStream outputStream;
// 输入 字符流
Reader reader;
// 输出 字符流
Writer writer;
2
3
4
5
6
7
8
9
InputStream
和OutputStream
是位于java.io
包下的两个抽象类,它们构成了Java I/O系统的基础。这两个类分别用于处理输入(读取数据)和输出(写入数据),并且提供了多种具体的实现类来支持不同的数据源和目标。
# 字节流
# InputStream
基本用途:用于从各种输入源读取字节数据。
常用子类:
FileInputStream
: 用于从文件中读取字节。ByteArrayInputStream
: 用于从字节数组中读取数据。BufferedInputStream
: 提供缓冲功能以提高读取效率。PipedInputStream
: 可以与PipedOutputStream
一起使用来实现管道机制。
使用outputstream的主要流程为:
- 创建对象,指定要读取数据的目标文件
- 读取数据
- 关闭通道
以下是一个简单demo,使用的是实现类, FileInputStream
FileInputStream fileInputStream =
new FileInputStream("/Users/starlord/Downloads/githubs Downloads/"+"demo.md");
int len = 0;
// 缓冲区大小
byte[] buffer = new byte[100];
// 一次读取 100 Byte
while ((len =fileInputStream.read(buffer))!=-1){
System.out.print(new String(buffer, 0, len));
}
fileInputStream.close();
2
3
4
5
6
7
8
9
10
# 一些字节
- 如果文件不存在
如果目标文件不存在,则程序会直接报错,而不是想输出流那样,创建一个文件,因为读一个空文件没任何意义。而输出之所以要创建文件,则是因为有数据要进行写入。
- 读取内容的返回值
read方法的返回值,是int 类型,它会去判断你是不是读到了文件的末尾。如果返回值是负一就是到末尾,如果不是负一,它就不是末尾。
如果文件的最后内容是一个 -1 呢,它返回什么呢?读取的值是-1,读到 没内容了也是 -1.
解答:
如果说文件当中它是负一,那么在读取的时候它会把它分开。首先它会读这里的负号,也就是一个横线,然后才会去读这里的一。所以它是分开去读的,不会把那个负一当做整体
- 为什么使用循环读的模式
如果说本地文件当中有很多很多的数据,那我该怎么写呢?你不能一次读一个,太麻烦了。所以我们要学习循环读取,用循环的方式读取完毕。
fileInputStream.read(buffer)
方法尝试从文件中读取最多 buffer.length
(本例中是 100)个字节的数据,并将实际读取的字节数返回给 len
。
如果成功读取了数据,则 len
将大于 0;如果到达文件末尾,则 read()
方法返回 -1
,循环结束。
在循环体内,new String(buffer, 0, len)
将从 buffer
中读取的前 len
个字节转换为字符串,并立即打印出来。这里使用了 String
的构造函数,它接受三个参数:字节数组、起始位置和长度,用于创建一个新的字符串对象。
# OutputStream
基本用途:用于将字节序列写入特定的目的地。
常用子类:
FileOutputStream
: 用于将字节写入文件。ByteArrayOutputStream
: 用于将数据写入字节数组。BufferedOutputStream
: 提供缓冲功能以提高写入效率。PipedOutputStream
: 可以与PipedInputStream
一起使用来实现管道机制。
使用outputstream的主要流程为:
- 创建对象,指定数据写入的目标文件
- 写入数据
- 关闭写入
以下是一个简单demo,使用的是实现类, FileOutputStream
// 可以不存在该文件,但路径一定要正确 也就是说存在这些文件夹
// 即使没有该文件 也会自动创建该文件
File file = new File("/Users/Downloads/"+"demo.md");
String newData = "it is up to you";
try {
// 创建一个通道
FileOutputStream fileOutputStream = new FileOutputStream(file);
// write data to file
fileOutputStream.write(newData.getBytes("UTF-8"));
// close the channel
fileOutputStream.close();
} catch (FileNotFoundException | UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("程序写入完成");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
数据是如何写到这个文件里面了呢?是谁做的事情?
其实就是第一行代码创建对象的时候做的事情。它会根据我们书写的路径,让程序和文件之间产生一个数据传输的通道
,然后再去通过write方法写出数据。数据相当于就是在这个通道当中进行的传输。
最后当我们调用close方法释放资源的时候,相当于就是把这个通道给关闭了。
为什么要调用
close()
方法
- 释放资源:
close()
方法不仅关闭流,还会释放与该流相关的所有系统资源(如文件描述符)。每个操作系统对同时打开的文件描述符数量都有一定的限制,如果程序频繁地打开文件但不关闭它们,最终可能导致无法再打开新的文件。- 确保数据完全写入:虽然大多数情况下,调用
write()
方法会立即将数据发送到操作系统的缓冲区,但这并不意味着数据立即被物理地写入磁盘。调用close()
可以确保所有缓冲的数据都被刷新并安全地写入磁盘。此外,如果你使用的是带缓冲的输出流(例如BufferedOutputStream
),那么close()
也会自动调用flush()
来确保所有缓存的数据都被写出。- 防止文件锁定:在某些操作系统上,特别是Windows,如果你打开了一个文件但没有关闭它,那么该文件可能仍然被当前进程锁定,这将阻止其他进程或同一个进程中的其他部分对该文件进行读取或写入操作。
不调用
close()
的后果
- 资源泄露:最直接的影响是资源泄露,这意味着你的应用程序可能会耗尽可用的文件描述符或其他系统资源。
- 数据丢失或损坏:如果没有正确关闭流,最后一批已写入的数据可能不会被完全写入磁盘,从而导致数据丢失或文件损坏。
- 文件锁定问题:未关闭的文件流可能导致文件被锁定,影响后续对该文件的操作。
# 写入的细节
上述例子中,我们用的是字节流,写入数据时,一次是写多少数据到文件呢?这取决于我们调用的方法。
public void write(int b) throws IOException {
}
public void write(byte b[]) throws IOException {
}
public void write(byte b[], int off, int len) throws IOException {
}
2
3
4
5
6
7
8
9
# 一个问题
写入数据时,会把文件原有的数据给清除掉吗?还是会把新的数据追加到文件中?
在上述例子中,答案是:清除掉原数据,再写入新数据。那我们想用追加的方式写数据如何实现呢?
- 实现换行
Java提供了一个常量System.lineSeparator()
,它返回当前运行平台的行分隔符。
String newData = "work must come firest" + System.lineSeparator()+"nothing is impossble";
另一种方法是使用更高层次的API,如PrintStream
或BufferedWriter
,它们提供了更方便的方法来处理文本输出,包括自动处理换行符。
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
writer.write("第一行");
writer.newLine(); // 自动使用系统默认的换行符
writer.write("第二行");
writer.newLine();
} catch (IOException e) {
e.printStackTrace();
}
2
3
4
5
6
7
8
- 实现内容追加
其实很简单,我们在创建对象的时候,它后面还有第二个参数。第二个参数是叫做续写开关。
当为true时,表示,写入的内容为追加方式。
// content append
FileOutputStream fileOutputStream = new FileOutputStream(file,true);
fileOutputStream.write(newData.getBytes("UTF-8"));
fileOutputStream.close();
2
3
4
# 大文件的读写
上文中说,可以通过缓存数组来提高单次读取数据的大小。
那是不是数组越大拷贝的越快?是的。但是你也要考虑到数组它本身也是会占用内存空间的。如果说你创建了一个长度为十个亿的数组,内存可能会直接崩掉。所以我们在创建速度的时候,长度一般会用1024的整数倍。
byte[] buffer = new byte[1024*1024*5]
// 表示 5M
2
- 转换成字符串的小细节
while ((len =fileInputStream.read(buffer))!=-1){
// 表示每次获取的是这个数组当中,从零索引开始,一共要去把任意个元素变成字符串 ,这里是len个
System.out.print(new String(buffer, 0, len));
}
2
3
4
表示我每次获取的是这个数组当中,从零索引开始,一共要去把任何元素变成字符串,这样就能避免最后一次读取时,读到上一次遗留的数据
# 字符流
它只能读取纯文本文件,比起字节流有一些限制,但之所以还需要它,是因为比起字节流更为高效。
主要有以下几个原因:
# 字符编码支持
- 字节流:只处理原始字节,不理解字符集的概念。这意味着如果你直接使用字节流读写文本文件,你需要手动处理字符编码问题。
- 字符流:提供对字符编码的支持。
# 跨平台兼容性
不同操作系统可能使用不同的默认字符编码。使用字符流可以确保你的程序在不同的平台上都能正确地处理文本数据,而不需要担心底层平台的具体细节。
# 更高级别的文本操作
字符流提供了更高层次的API,便于进行文本处理:
- 缓冲功能:比如
BufferedReader
和BufferedWriter
提供了高效的批量读写操作,并且增加了诸如readLine()
这样的便捷方法来简化文本处理。 - 格式化输入输出:像
PrintWriter
提供了格式化输出的方法,例如println()
,这在处理文本时非常有用。
# 一些例子
字符流本质也是调用的字节流,不过对其进行了封装。
public static void main(String[] args) throws IOException {
FileReader fileReader =
new FileReader("/Users/starlord/Downloads/githubs Downloads/"+"demo.md");
int len = 0;
// 缓冲区大小
char[] buchar = new char[5];
while ((len =fileReader.read(buchar))!=-1){
System.out.println(new String(buchar,0,len));
}
fileReader.close();
}
2
3
4
5
6
7
8
9
10
11
12
下面是一个
File file = new File("D:\\my_mp3\\sugar.txt");
// second param is true ,means to append
Writer writer = new FileWriter(file,true);
String st = "我喜欢你";
writer.write(st);
writer.close(); //因为是字符流 可以直接用string
2
3
4
5
6
# 字节字符转换流
OutputStreamWriter
可以将输出的字符流转化成字节流的形式输出
InputStreamReader
将输入的字节流转化成字符流的形式
Reader reader = new InputStreamReader(InputStream input);
Writer writer = new OutputStreamWriter(OutputStream output)
2
# 缓冲流
作用:将数据先缓冲起来,然后一起写入或者读出,提高效率
# BufferedOutputStream
**BufferedOutputStream:**内部默认的缓存大小是8KB,每次写入时存储到缓存中的byte数组中,当数组存满时,会把数组中的数据写入文件,并且缓存下标归零
File file = new File("D:\\my_mp3\\sugar.txt");
OutputStream outStream = new FileOutputStream(file,true);
BufferedOutputStream bf = new BufferedOutputStream(outStream);
String S="我要找到小确幸";
bf.write(S.getBytes());
bf.close();
outStream.close();
2
3
4
5
6
7
8
# BufferedReader
默认缓存也是8k,但是可以手动指定大小
File file = new File("D:\\my_mp3\\sugar.txt");
Reader reader = new FileReader(file);
BufferedReader bf = new BufferedReader(reader);
char[] sc = new char[1024];
int len = -1;
StringBuilder st = new StringBuilder();
while ((len=bf.read(sc))!=-1)
{
st.append(sc,0,len);
}
System.out.println(st);
// Reader 会自动关闭
bf.close();
2
3
4
5
6
7
8
9
10
11
12
13