JAVA重要知识点
IO介绍
IO是输入和输出。数据传输过程类似于水流,因此称为IO流。IO流在Java中分为输入流和输出流,根据数据的处理方式分为:字节流和字符流。
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
字节流
字节输入流
InputStream用于从源头(通常是文件)读取数据(字节信息)到内存中,java.io.InputStream抽象类是所有字节输入流的父类。
字节输入流常用方法
- read()
返回输入流中下一个字节的数据。返回值是0-255,如果没有读取任何字节,则返回-1,表示文件结束 - read(byte b[])
从输入流中读取一些字节存储到数组b中,如果数组b的长度为0,则不读取
如果没有可用字节读取,返回-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()
关闭输入流释放相关的系统资源
Java9开始,InputStream新增多个实用方法:
readAllBytes():
读取输入流中的所有字节,返回字节数组
readNBytes(byte[] b, int off, int len):
阻塞直到读取len个字节
transferTo(OutputStream out):
将所有字节从一个输入流传到一个输入流
FileInputStream
是一个比较常用的字节输入流,可以直接指定文件路径,也可以直接读取单字节数据,也可以读取到字节数组中。代码举例如下:
try (InputStream fis = new FileInputStream("input.txt")) {
System.out.println("Number of remaining bytes:"
+ fis.available());
int content;
long skip = fis.skip(2);
System.out.println("The actual number of bytes skipped:"
+ skip);
System.out.print("The content read from file:");
while ((content = fis.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
注意:
一般不直接单独使用FileInputStream,通常配合BufferedInputStream(字节缓冲输入流)
例如下面代码的示例,通过readAllBytes()读取输入流所有字节并将其直接赋值给一个String对象:
// 新建一个 BufferedInputStream 对象
BufferedInputStream bufferedInputStream =
new BufferedInputStream(new FileInputStream("input.txt"));
// 读取文件的内容并复制到 String 对象中
String result =
new String(bufferedInputStream.readAllBytes());
System.out.println(result);
注意:
- DataInputStream用于读取指定类型数据,不能单独使用,必须结合FileInputStream
FileInputStream fileInputStream =
new FileInputStream("input.txt");
//必须将fileInputStream作为构造参数才能使用
DataInputStream dataInputStream =
new DataInputStream(fileInputStream);
//可以读取任意具体的类型数据
dataInputStream.readBoolean();
dataInputStream.readInt();
dataInputStream.readUTF();
- ObjectInputStream用于从输入流中读取Java对象(反序列化),ObjectOutputStream用于将对象写入到输出流(序列化)
ObjectInputStream input =
new ObjectInputStream(new FileInputStream("object.data"));
//类名.属性名
MyClass object = (MyClass) input.readObject();
input.close();
另外,用于序列化和反序列化的类必须实现 Serializable 接口,对象中如果有属性不想被序列化,使用 transient 修饰。
字节输出流
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
FileOutputStream 是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。 举例代码如下:
try (FileOutputStream output = new FileOutputStream
("output.txt")) {
byte[] array = "Copy".getBytes();
output.write(array);
} catch (IOException e) {
e.printStackTrace();
}
注意:
- FileOutputStream 通常也会配合 BufferedOutputStream(字节缓冲输出流)来使用。类似于FileInputStream
FileOutputStream fileOutputStream =
new FileOutputStream("output.txt");
BufferedOutputStream bos =
new BufferedOutputStream(fileOutputStream);
- 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("cpoy", "cpoy");
output.writeObject(person);
字符流
文件读写和网络发送接收,信息的最小存储单元都是字节。
但是IO操作还是需要分为字节流操作和字符流操作,原因如下:
1、字符流是由Java虚拟机将字节转换得到的,这个过程比较耗时
2、如果不知道编码类型就很容易出现乱码问题
乱码问题这个很容易就可以复现,将上述FileInputStream 代码示例中的 input.txt 文件内容改为中文,原代码不需要改动
因为FileInputStream是字节输入流,中文需要用字符流写入
所以,IO流提供了一个直接操作字符的接口,方便用户对字符进行流操作。如果是音频文件,图片等媒体文件用字节流比较好,如果涉及到字符的话用字符流比较好
字符流默认采用Unicode编码,可以通过构造方法自定义编码。
常用字符编码所占字节数是多少?
utf8:英文1字节,中文3字节
unicode:任何字符2字节
gbk:英文1字节,中文2字节
字符输入流
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 是基于该基础上的封装,可以直接操作字符文件。代码如下:
// 字节流转换为字符流的桥梁
public class InputStreamReader extends Reader {
}
// 用于读取字符文件
public class FileReader extends InputStreamReader {
}
FileReader 举例代码如下:
try (FileReader fileReader = new FileReader("input.txt");) {
int content;
long skip = fileReader.skip(3);
System.out.println("The actual number of bytes skipped:"
+ skip);
System.out.print("The content read from file:");
while ((content = fileReader.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
字符输出流
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 是基于该基础上的封装,可以直接将字符写入到文件。
// 字符流转换为字节流的桥梁
public class OutputStreamWriter extends Writer {
}
// 用于写入字符到文件
public class FileWriter extends OutputStreamWriter {
}
FileWriter 代码举例如下:
try (Writer output = new FileWriter("output.txt")) {
output.write("copy+1008611话务直通车");
} catch (IOException e) {
e.printStackTrace();
}
字节缓冲流
IO操作比较消耗性能,缓冲流将数据加载到缓冲区,一次性读取、写入多个字节,从而避免频繁的IO操作,提高流的传输效率。
字节缓冲流采用了装饰器模式来增强InputStream和OutputStream子类对象的功能
举例如下:通过 BufferedInputStream(字节缓冲输入流)来增强 FileInputStream 的功能
// 新建一个 BufferedInputStream 对象
BufferedInputStream bufferedInputStream =
new BufferedInputStream(new FileInputStream("input.txt"));
字节流和字节缓冲流的性能差别主要体现在使用两者的时候都是调用 write(int b) 和 read() 这两个一次只读取一个字节的方法的时候。由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。
具体效率对比如下:使用write(int b)和read()方法,分别通过字节流和字节缓冲流复制一个524.9mb的PDF耗时
@Test
void copy_pdf_to_another_pdf_buffer_stream() {
// 记录开始时间
long start = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream
(new FileInputStream("深入理解计算机操作系统.pdf"));
BufferedOutputStream bos = new BufferedOutputStream
(new FileOutputStream("深入理解计算机操作系统-副本.pdf"))){
int content;
while ((content = bis.read()) != -1) {
bos.write(content);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("使用缓冲流复制PDF文件总耗时:" +
(end - start) + " 毫秒");
}
@Test
void copy_pdf_to_another_pdf_stream() {
// 记录开始时间
long start = System.currentTimeMillis();
try (FileInputStream fis =
new FileInputStream("深入理解计算机操作系统.pdf");
FileOutputStream fos =
new FileOutputStream("深入理解计算机操作系统-副本.pdf")) {
int content;
while ((content = fis.read()) != -1) {
fos.write(content);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("使用普通流复制PDF文件总耗时:" +
(end - start) + " 毫秒");
}
结果如下:
使用缓冲流复制PDF文件总耗时:15428 毫秒
使用普通字节流复制PDF文件总耗时:2555062 毫秒
两者耗时差别非常大,缓冲流耗费的时间是字节流的 1/165。
如果是调用 read(byte b[]) 和 write(byte b[], int off, int len) 这两个写入一个字节数组的方法的话,只要字节数组的大小合适,两者的性能差距其实不大,基本可以忽略。
字节缓冲输入流
BufferedInputStream 从源头(通常是文件)读取数据(字节信息)到内存的过程中不会一个字节一个字节的读取,而是会先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。这样大幅减少了 IO 次数,提高了读取效率。BufferedInputStream 内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组,通过阅读 BufferedInputStream 源码如下:
public
class BufferedInputStream extends FilterInputStream {
// 内部缓冲区数组
protected volatile byte buf[];
// 缓冲区的默认大小
private static int DEFAULT_BUFFER_SIZE = 8192;
// 使用默认的缓冲区大小
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
// 自定义缓冲区大小
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException
("Buffer size <= 0");
}
buf = new byte[size];
}
}
可以通过 BufferedInputStream(InputStream in, int size) 这个构造方法来指定缓冲区的大小。
字节缓冲输出流
BufferedOutputStream将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了读取效率
try (BufferedOutputStream bos = new BufferedOutputStream
(new FileOutputStream("output.txt"))) {
byte[] array = "copy".getBytes();
bos.write(array);
} catch (IOException e) {
e.printStackTrace();
}
BufferedOutputStream 内部也维护了一个缓冲区,并且,这个缓存区的大小也是 8192 字节。
字符缓冲流
BufferedReader (字符缓冲输入流)和 BufferedWriter(字符缓冲输出流)类似于 BufferedInputStream(字节缓冲输入流)和BufferedOutputStream(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。前者主要是用来操作字符信息
打印流
System.out.print("Hello!");
System.out.println("Hello!");
System.out 实际是用于获取一个 PrintStream 对象,print方法实际调用的是 PrintStream 对象的 write 方法。
PrintStream 属于字节打印流,与之对应的是 PrintWriter (字符打印流)。PrintStream 是 OutputStream 的子类,PrintWriter 是 Writer 的子类。
public class PrintStream extends FilterOutputStream
implements Appendable, Closeable {
}
public class PrintWriter extends Writer {
}
随机访问流
指的是支持随意跳转到文件的任意位置进行读写的 RandomAccessFile
RandomAccessFile 的构造方法如下,我们可以指定 mode(读写模式)。读写模式主要有下面四种:
r : 只读模式。
rw: 读写模式
rws: 相对于 rw,rws 同步更新对“文件的内容”或“元数据”的修改到外部存储设备。
rwd : 相对于 rw,rwd 同步更新对“文件的内容”的修改到外部存储设备。
文件内容指的是文件中实际保存的数据,元数据则是用来描述文件属性比如文件的大小信息、创建和修改时间。
RandomAccessFile 中有一个文件指针用来表示下一个将要被写入或者读取的字节所处的位置。可以通过 RandomAccessFile 的 seek(long pos) 方法来设置文件指针的偏移量(距文件开头 pos 个字节处)。如果想要获取文件指针当前的位置的话,可以使用 getFilePointer() 方法。
// openAndDelete 参数默认为 false
//表示打开文件并且这个文件不会被删除
public RandomAccessFile(File file, String mode)
throws FileNotFoundException {
this(file, mode, false);
}
// 私有方法
private RandomAccessFile(File file, String mode,
boolean openAndDelete) throws FileNotFoundException{
// 省略大部分代码
}
RandomAccessFile 代码举例如下:
RandomAccessFile randomAccessFile = new RandomAccessFile(new
File("input.txt"), "rw");
System.out.println("读取之前的偏移量:" +
randomAccessFile.getFilePointer() + ",当前读取到的字符" +
(char) randomAccessFile.read() + ",读取之后的偏移量:" +
randomAccessFile.getFilePointer());
// 指针当前偏移量为 6
randomAccessFile.seek(6);
System.out.println("读取之前的偏移量:" +
randomAccessFile.getFilePointer() +
",当前读取到的字符" + (char) randomAccessFile.read() +
",读取之后的偏移量:" + randomAccessFile.getFilePointer());
// 从偏移量 7 的位置开始往后写入字节数据
randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'});
// 指针当前偏移量为 0,回到起始位置
randomAccessFile.seek(0);
System.out.println("读取之前的偏移量:" +
randomAccessFile.getFilePointer() +
",当前读取到的字符" + (char) randomAccessFile.read() +
",读取之后的偏移量:" + randomAccessFile.getFilePointer());
文本内容为:ABCDEFG
读取之前的偏移量:0,当前读取到的字符A,读取之后的偏移量:1
读取之前的偏移量:6,当前读取到的字符G,读取之后的偏移量:7
读取之前的偏移量:0,当前读取到的字符A,读取之后的偏移量:1
---------------------------
input.txt 文件内容变为 ABCDEFGHIJK
RandomAccessFile 的 write 方法在写入对象的时候如果对应的位置已经有数据的话,会将其覆盖掉。
RandomAccessFile randomAccessFile = new RandomAccessFile(new
File("input.txt"), "rw");
randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'});
input.txt 文件内容变为 ABCD ,运行之后则变为 HIJK
常见的一个应用是实现大文件的断点续传
上传文件中途暂停或失败,不许重新上传,只需要上传那些未成功上传的文件分片即可。分片上传是断点续传的基础
分片:将文件切分成多个文件分片