文章目录
1、IO流分类
- 按流的方向分为:输入流和输出流
- 按流的数据单位不同分为:字节流和字符流
- 按流的功能不同分为:节点流和处理流
字节流和字符流的区别
-
读写单位的不同:字节流以字节(8bit)为单位。字符流以字符(16bit)为单位,根据码表映射字符,一次可能读多个字节。
-
· 处理对象不同:字节流可以处理任何类型的数据,如图片、avi等,而字符流只能处理字符类型的数据。PS:用字节流处理字符时,要注意字符的编码
节点流和处理流
节点流是流最基本的实现,处理流是用来处理节点流,使它具有某种特性,比如加快读取速度的缓存流。
形象的来说,黑色的是节点流,米色的是处理流
2、抽象基类及IO框架的结构
输入流 | 输出流 | |
---|---|---|
字符流 | Reader | Writer |
字节流 | InputStream | OutputStream |
所有其他流都基于这四个基类
3、IO框架部分了解
首先来了解一下节点流—文件流,用来读取文件
- FileOutputStream
- FileInputStream
- FileReader
- FileWriter
1、节点流
1、文件流
1、FileReader
构造方法
public FileReader(String fileName) throws FileNotFoundException
public FileReader(File file) throws FileNotFoundException
public FileReader(FileDescriptor fd)
public FileReader(String fileName, Charset charset) throws IOException
public FileReader(File file, Charset charset) throws IOException
构造方法比较简单,这里不讲解
读取方法
public int read() throws IOException
public int read(char[] cbuf) throws IOException
public int read(char cbuf[], int offset, int length) throws IOException
- read():读取一个字符,并以int值返回。如果流读取完毕,就返回-1
- read(char[] cbuf):将字符读入一个char数组,返回读取字符的数量。如果流读取完毕,就返回-1
- read(char cbuf[], int offset, int length):将字符读入char数组,offset表示字符存入的起始位置,length表示一次读取多少个字符。返回读取字符的数量。如果流读取完毕,就返回-1。
注意:
1、read(char cbuf[], int offset, int length)
: length长度不对,会抛出异常
2、Reader读取char[ ] 时,会覆盖上一次读取的字符,而不是将数组重置,再读取,这会可能导致输出时,多输出了上一次读取的字符
File: abcde
FileReader fr = new FileReader(file);
char[] c = new char[3];
fr.read(c);//c = {a,b,c}
fr.read(c);//c = {d,e,c}
接下来写一个文件输入:
public static void main(String[] args) throws IOException {
FileReader reader = null;
try {
File file = new File("test.txt");
reader = new FileReader(file);
int data;
char[] chars = new char[3];
while ((data = reader.read(chars,0,4)) != -1){
for (int i = 0; i < data; i++) {
System.out.println(chars[i]);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader !=null){
try{
reader.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
分析:
1、reader = new FileReader(file);
如果文件不存在,会抛出异常
data = reader.read(chars,0,4)
读取文件时,可能会抛出异常
2、为了保证流一定会被关闭,将reader.close();
放入finally块中
2、FileWriter
构造函数
public FileWriter(String fileName) throws IOException
public FileWriter(File file) throws IOException
public FileWriter(File file, boolean append) throws IOException
public FileWriter(String fileName, Charset charset) throws IOException
public FileWriter(String fileName, Charset charset, boolean append) throws IOException
public FileWriter(File file, Charset charset) throws IOException
public FileWriter(File file, Charset charset, boolean append) throws IOException
注意:
- 当文件不存在时,
FileWriter(File file)
会自动创建一个文件 - FileWriter执行写入时,默认覆盖文件内容,重新写入(append = false)。构造FileWriter时,使
append = true
,则不覆盖文件的内容,而是追加
读取方法
public void write(int c) throws IOException
public void write(char cbuf[]) throws IOException
public void write(char cbuf[], int off, int len) throws IOException;
public void write(String str, int off, int len) throws IOException
public void write(int c) throws IOException
:c指定字符的字符值(eg:a 97)
介绍了上面两种流,剩下的流就简单了,就不一一阐述了。
3、 FileInputStream
public int read() throws IOException
public int read(byte b[]) throws IOException
public int read(byte b[], int off, int len) throws IOException
byte[] 存储的是文件中的字符对应的编码值,比如在utf-16编码格式下,‘A’为0X0041(16位长) ,在utf-8下为0X41(8位长)
4、FileOutputStream
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、处理流
1、缓冲流,加快读写操作
缓冲流多了一个缓冲区,将字节或字符先写入缓冲区,当到达缓冲区最大大小时,输出缓冲区的内容。
BufferedWriter writer = new BufferedWriter(new FileWriter(file));
BufferedReader reader = new BufferedReader(new FileReader(file));
BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file));
BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file));
需要注意的方法:
- 缓冲输出流的
public synchronized void flush() throws IOException
,手动刷新缓冲区 - BufferedReader的
public String readLine() throws IOException
,一次读取一行,以换行符为一行的结束标志,当达到EOF(end of file)时,返回null。注意:这个方法不会读取换行符 - BufferedWriter 的
public void newLine() throws IOException
,输出一个换行符
PS
- 不同系统下的换行符不同,在 UNIX 中, 换行符为
\n
,在Windows中,为\r\n
- 只需关闭缓冲流,就能同时关闭节点流。
2、转换流----提供字节流与字符流的转换
- InputStreamReader:将字节的输入流转换为字符的输入流 (解码)
- OutputStreamWriter:将字符的输出流转换为字节的输出流 (编码)
InputStreamReader:
public InputStreamReader(InputStream in)
指明读取文件的编码集,建立一个使用此编码的流
public InputStreamReader(InputStream in, String charsetName)throws UnsupportedEncodingException
public InputStreamReader(InputStream in, Charset cs)
返回此编码的历史名称,如果流已关闭,则为 null
public String getEncoding()
OutputStreamWriter:
public OutputStreamWriter(OutputStream in)
指明读取文件的编码集,建立一个使用此编码的流
public OutputStreamWriter(OutputStream in, String charsetName)throws UnsupportedEncodingException
public OutputStreamWriter(OutputStream in, Charset cs)
返回此编码的历史名称,如果流已关闭,则为 null
public String getEncoding()
- 不管是那种转换流,如果创建流时没有指定字符集编码,就使用系统默认的字符集编码
- 转换流属于字符流
3、标准输入输出流
- System.out:PrintStream,标准输出流,默认从屏幕输出
- System.in:InputStream,标准输入流,默认从键盘输入
- System.err:PrintStream
可以用System的静态方法,设置标准输出流
public static void setIn(InputStream in)
public static void setOut(PrintStream out)
public static void setErr(PrintStream err)
4、打印流
将基本数据类型转换为字符串输出
- PrintStream
- PrintWriter
PS
- 打印流有自动flush功能
- 存在一系列重载的
print
与println
方法 - PrintStream打印的字符使用默认的字符编码,将其转换为字节
5、数据流
用于读写基本数据类型变量或字符串
- DataOutputStream
- DataInputStream
使用数据流时,读取不同类型的数据要与当初写入文件的顺序一致
6、对象流
用于读取与存储基本数据类型与对象的处理流
- ObjectInputStream
- ObjectOutputStream
对象流的实现基于Java的序列化机制
- 序列化:用
ObjectOutputStream
把基本数据类型或对象保存进数据源的机制; - 反序列化:用
ObjectInputStream
从数据源中读取基本数据类型或对象的机制;
序列化机制
1、可以把Java对象转换成二进制流,并持久化地保存在磁盘或者通过网络传输给另一个网络节点
2、当其他Java程序获取到这种二进制流时,可以将其恢复成Java对象
PS:对象流不能序列化static
和transient
修饰的成员变量
上文提到了序列化idserialVersionUID
,那么这个id究竟是什么?
- 这个id用来表明类的不同版本的兼容性,简单的来说,是用来验证是否能够成功反序列化的标识
- 如果没有指定
serialVersionUID
,JVM就会根据类的结构自动生成一个serialVersionUID
,所以类的结构(比如变量的个数)变化会引起自动生成的serialVersionUID
变化。所以最好显式指定一个serialVersionUID
- 在反序列化时,JVM会将传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。
一个类可序列化的条件
- 实现必须实现
Serializable
接口,并且提供一个全局常量private static final long serialVersionUID
- 这个类中的所有成员变量必须可序列化
3、RandomAccessFile
1、RandomAccessFile简介
- 在java.io包下,但直接继承了Object。实现了
DataInput
与DataOutput
接口,所以RandomAccessFile
既可以输入也可以输出 - 支持“随机访问”的方式,程序可以跳到文件的任意地方来读与写文件
- 支持只访问文件的部分内容
- 可以向已存在的文件追加内容
RandomAccessFile
对象包含了一个记录指针,用以标记当前读写处的位置。
RandomAccessFile的方法虽然多,但它有一个最大的局限,就是只能读写文件,不能读写其他IO节点。RandomAccessFile的一个重要使用场景就是网络请求中的多线程下载及断点续传。
2、RandomAccessFile方法
- 构造方法
- 重要方法
构造方法:
public RandomAccessFile(String name, String mode) throws FileNotFoundException
public RandomAccessFile(File file, String mode) throws FileNotFoundException
RandomAccessFile的构造方法中的mode参数,指定RandomAccessFile是读还是写,或是读写
mode模式
r : 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
rw: 打开以便读取和写入。
rws: 打开以便读取和写入,对“文件的内容”或“元数据”的每个更新都同步写入到基础存储设备。
rwd : 打开以便读取和写入,对“文件的内容”的每个更新都同步写入到基础存储设备。
重要方法
设置与获取指针位置
public void seek(long pos) throws IOException
public native long getFilePointer() throws IOException
返回文件的大小(字节为单位)
public native long length() throws IOException
为文件设置一个新的大小(字节为单位)
public native void setLength(long newLength) throws IOException;
public native void setLength(long newLength) throws IOException
详解
- 当文件的length小于newLength,把文件大小设置为newLength。这种情况下,文件扩展部分的内容为空,是未定义的。
- 当文件的length大于newLength,文件将被截断。在这种情况下,如果
getFilePointer
返回的文件偏移量大于 newLength,则在此方法返回后,偏移量将等于 newLength。
注意事项:
- 如果写出的文件不存在,
RandomAccessFile
会自动创建一个文件 - 如果写出的文件存在,
RandomAccessFile
会从文件头开始覆盖,而不是把文件内容全部覆盖
4、总结
- 对于所有输入流,如果文件不存在会抛出异常
- 对于所有用数组读取的方法,要注意覆盖读取问题与数组溢出问题
- 一般来说,读取字符使用字符流,其他文件使用字节流。
- java.io包下的流实现了同步机制,如果有多线程共同持有一个流,那么同一时刻只有一个线程能读取或写出
- 除了
RandomAccessFile
的其他输入流,只能 ”读取一次“ ,从开始读取,直至流被关闭。流的读取是不可逆的,要想重新读取文件的某个部分,只能重新开始。 RandomAccessFile
由于是随机访问的,所以可以实现断点续传
字符流会把char[ ]中的字符编码,存放在自己内部的缓存区(byte[ ])
5、应用
使用输入流输出流多线程拷贝一份文件
class Task implements Runnable{
private FileWriter fileWriter;
private FileReader fileReader;
public Task(FileWriter fileWriter,FileReader fileReader) {
this.fileWriter = fileWriter;
this.fileReader = fileReader;
}
@Override
public void run() {
try {
char[] chars = new char[4];
int len = 0;
while ((len = fileReader.read(chars)) != -1){
fileWriter.write(chars,0,len);
}
} catch (IOException e) {
if (!e.getMessage().equals("Stream closed"))
e.printStackTrace();
} finally {
try {
if (fileReader != null)
fileReader.close();
} catch (Exception e) {
if (!e.getMessage().equals("Stream closed"))
e.printStackTrace();
}
try {
if (fileWriter != null) {
fileWriter.close();
}
} catch (Exception e) {
if (!e.getMessage().equals("Stream closed"))
e.printStackTrace();
}
}
}
}
注意点:
- 当流读取完毕,被关闭后如果执行
write或close
操作会报异常,所以对异常进行判断流是否读取完毕。
使用RandomAccessFile实现多线程下载(类似迅雷的多线程下载)
public class Test {
//定义一个线程读写500MB的文件大小
private static final long mem = 1024*1024*500;
public static void main(String[] args) throws IOException {
download(new File("E:\\新建文件夹 (2)\\新建文件夹\\花束般的恋爱 花束みたいな恋をした 2021.mp4"),
new File("D:\\users\\desktop\\test.mp4"));
}
public static void download(File src,File dest) throws IOException {
long start = System.currentTimeMillis();
RandomAccessFile reader = null;
try {
reader = new RandomAccessFile(dest,"rw");
long length = reader.length();
//预先开辟一片空间,创建临时文件
reader.setLength(length);
long l = splitFile(src);
Thread[] threads = new Thread[(int) l];
for (int i = 0; i < threads.length; i++) {
if (i == threads.length - 1){
//计算剩余需要读写的大小,进行读写
Thread thread = new Thread(new Task(new RandomAccessFile(src, "r"), new RandomAccessFile(dest, "rw"),
mem * i, length - l * mem));
thread.start();
thread.join();
}else {
Thread thread = new Thread(new Task(new RandomAccessFile(src, "r"), new RandomAccessFile(dest, "rw"),
mem * i, mem));
thread.start();
thread.join();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("当前文件下载时间为:" + ((System.currentTimeMillis() - start)/1000) + "秒");
}
//对文件分块,计算需要几个进程
private static long splitFile(File file){
long length = file.length();
return (length%mem == 0)? (length/mem) : (length/mem+1);
}
}
class Task implements Runnable{
private RandomAccessFile reader;
private RandomAccessFile writer;
private long startPosition; //定义每个线程从哪开始读取与写入
private long size; //定义读取与写入的大小,用于判断何时停止读写
private static final int l = 1024*1024*50; byte数组的大小
public Task(RandomAccessFile reader, RandomAccessFile writer, long startPosition, long size) {
this.reader = reader;
this.writer = writer;
this.startPosition = startPosition;
this.size = size;
}
@Override
public void run() {
try {
reader.seek(startPosition);
writer.seek(startPosition);
byte[] bytes = new byte[l];
int len = 0;
long data = 0;
while ((len = reader.read(bytes)) != -1){
writer.write(bytes,0,len);
data += len; //计算读写了多大空间
if (data == size) break;
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (writer != null)
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (reader != null)
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}