Java IO
# 初识
所谓IO,就是in和out,也就是输入和输出,指应用程序和外部设备之间的数据传输。
Java中通过流来处理IO,所谓流,就是一连串的数据。
流的几个特性:
- 先进先出:最先写入输出流的数据最先被输入流读取到
- 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)
- 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流
# 基本划分
如果按照传输方式来划分IO,可以分为字节流和字符流。
字节是面向计算机的,我们知道计算机是按位(bit)存储和计算的,一个字节有8 bit。
字符是面向人类的,比如“我喜欢看电影”就是一串字符。字符在存储的时候,还是要转换为字节,由于存在不同语言文字,所以需要字符和字节转换的对照表,这诞生了很多字符编码集。最常见的就是UTF-8编码,它对全球的语言文字都做了对照,使用比较广泛。
字节流按照输入输出,在Java中的抽象分别是:
- InputStream
- OutputStream
字符流按照输入输出,在Java中的抽象分别是:
- Reader
- Writer
以上就是Java IO的四个基本抽象类。
其中InputStream和Reader的核心方法是read(),这很好理解,所谓的输入流,就是供我们读取使用的。
而OutputStream和Writer的核心方法是write(),所谓的输出流,就是要向外部写东西。
# 操作对象划分
Java中,IO会有很多操作对象,最常见的就是文件,其次还有数组、管道、基本类型、缓冲、打印、序列化等。
# 1、文件流
首先是文件读的示例,分别演示了字节流读取和字符流读取:
int b;
FileInputStream fis1 = new FileInputStream("fis.txt");
// 循环读取
while ((b = fis1.read())!=-1) {
System.out.println((char)b);
}
fis1.close();
int b = 0;
// 注意此处会使用系统默认编码来读取文件
FileReader fileReader = new FileReader("read.txt");
// 循环读取
while ((b = fileReader.read())!=-1) {
System.out.println((char)b);
}
fileReader.close();
然后是写入指定内容到文件:
FileOutputStream fos = new FileOutputStream("fs.txt");
fos.write("爱看电影".getBytes());
fos.close();
FileWriter fileWriter = new FileWriter("fw.txt");
char[] chars = "爱看电影".toCharArray();
fileWriter.write(chars, 0, chars.length);
fileWriter.close();
# 2、数组流
数组流,也叫内存流:
InputStream is =new BufferedInputStream(new ByteArrayInputStream("爱看电影".getBytes(StandardCharsets.UTF_8)));
byte[] flush =new byte[1024];
int len =0;
while(-1!=(len=is.read(flush))){
System.out.println(new String(flush,0,len));
}
is.close();
ByteArrayOutputStream bos =new ByteArrayOutputStream();
byte[] info ="爱看电影".getBytes();
bos.write(info, 0, info.length);
byte[] dest =bos.toByteArray();
bos.close();
# 3、管道流
Java中的管道流不同于Linux用来进程间通信,它只能用来线程间通信。
final PipedOutputStream pipedOutputStream = new PipedOutputStream();
final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
pipedOutputStream.write("爱看电影".getBytes(StandardCharsets.UTF_8));
pipedOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] flush =new byte[1024];
int len =0;
while(-1!=(len=pipedInputStream.read(flush))){
System.out.println(new String(flush,0,len));
}
pipedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
# 4、基本类型流
用来写入和读取基本类型:
DataInputStream dis = new DataInputStream(new FileInputStream("fs.txt"));
// 读取接下来的一个字节,相当于 (byte)dis.read()
byte b = dis.readByte();
// 读取接下来的两个字节,转换为short
short s = dis.readShort();
// 读取接下来的四个字节,转换为int
int i = dis.readInt();
// ...
long l = dis.readLong();
float f = dis.readFloat();
double d = dis.readDouble();
boolean bb = dis.readBoolean();
char ch = dis.readChar();
dis.close();
DataOutputStream das = new DataOutputStream(new FileOutputStream("fs.txt"));
das.writeByte(10);
das.writeShort(100);
das.writeInt(1000);
das.writeLong(10000L);
das.writeFloat(12.34F);
das.writeDouble(12.56);
das.writeBoolean(true);
das.writeChar('A');
das.flush();
das.close();
# 5、缓冲流
缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。
缓冲输入流相对于普通输入流的优势是,它提供了一个缓冲数组(默认是8192,参考BufferedInputStream.DEFAULT_BUFFER_SIZE
),每次调用read方法的时候,它首先尝试从缓冲区里读取数据,若读取失败(缓冲区无可读数据),则选择从物理数据源(譬如文件)读取新数据(这里会尝试尽可能读取多的字节)放入到缓冲区中,最后再将缓冲区中的内容部分或全部返回给用户.由于从缓冲区里读取数据远比直接从物理数据源(譬如文件)读取速度快。
BufferedInputStream在模式上是一个包装类:
int bytesRead;
byte[] buffer = new byte[1024];
BufferedInputStream is = new BufferedInputStream(new FileInputStream("fis.txt"));
while ((bytesRead = is.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, bytesRead));
}
is.close();
# 6、打印流
打印流就是向控制台中打印信息,我们最常用的System.out.println()
用到的就是打印流。
打印流本身是输出流,字节流的实现是PrintStream
,System.out
返回的就是System.out
;
字符流的实现是PrintWriter
。
StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
pw.println("爱看电影");
}
System.out.println(buffer.toString());
# 7、序列化流
要将Java对象保存为流进行传输,需要使用到序列化流。
对象必须先实现Serializable接口,创建序列化流:
Employee e = new Employee();
e.name = "jack";
e.address = "beijing";
e.age = 30;
try {
// 创建序列化流对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt"));
// 写出对象
out.writeObject(e);
// 释放资源
out.close();
fileOut.close();
// Employee对象被保存到了文件中
} catch(IOException i) {
i.printStackTrace();
}
读取被序列化的对象:
// 创建反序列化流
FileInputStream fileIn = new FileInputStream("employee.txt");
ObjectInputStream in = new ObjectInputStream(fileIn);
// 读取一个对象
e = (Employee) in.readObject();
// 释放资源
in.close();
fileIn.close();
注意:Java本身的序列化性能较差,业界对此有很多优化方案,比如Hessian2、Protobuf等。
# 字节与字符流的转换
直接上代码:
InputStreamReader isr = new InputStreamReader(new FileInputStream("demo.txt"));
char []cha = new char[1024];
int len = isr.read(cha);
System.out.println(new String(cha,0,len));
isr.close();
try (ObjectInputStream input = new ObjectInputStream(new FileInputStream(
new File("Person.txt")))) {
String s = input.readUTF();
}
前面说到使用FileReader直接读取存在编码问题,要指定编码读取文件,可以这么写:
final InputStreamReader reader = new InputStreamReader(input, Charsets.toCharset(inputCharset));
while ((b = reader.read())!=-1) {
System.out.println((char)b);
}
fileReader.close();
# 工具类库
# 1、Scanner
从输入流获取扫描值,示例:
Scanner sc = new Scanner(System.in);
int i = sc.nextInt();
System.out.println(i);
Scanner sc = new Scanner(new File("num.txt"));
while (sc.hasNextLong()) {
long aLong = sc.nextLong();
System.out.println(aLong);
}
# 2、Apache Common IO
maven引入:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
IO相关的工具主要是IOUtils,有很多有用的方法。
copy和copyLarge用于拷贝流,比如把网络文件保存到本地:
InputStream is = new URL("http://t.com/1.jpg").openConnection().getInputStream();
IOUtils.copy(is, new FileOutputStream("1.jpg"));
按照指定编码读取文件:
IOUtils.readLines(is, StandardCharsets.UTF_8);
InputStream转换为byte[]或char[]:
IOUtils.toByteArray(is);
IOUtils.toCharArray(is, StandardCharsets.UTF_8);
IOUtils.toString(is, StandardCharsets.UTF_8);
写入文件:
IOUtils.write("爱看电影", new FileOutputStream("1.txt"), StandardCharsets.UTF_8);
IOUtils.writeLines(list, new FileOutputStream("1.txt"), StandardCharsets.UTF_8);
关闭资源:
IOUtils.closeQuietly(is);
# 新IO
在Java1.4中引入了nio,Java1.7又引入了aio。aio和Java网络编程关系很大,后面会有专题文章。
这里说一下nio,主要引入了两个接口:
- Channel: 流只能在一个方向上移动,而通道可以双向,可以用于读、写或者同时用于读写。主要实现类:
- FileChannel: 从文件中读写数据
- DatagramChannel: 通过 UDP 读写网络中数据
- SocketChannel: 通过 TCP 读写网络中数据
- ServerSocketChannel: 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel
- Buffer: 发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。主要实现类:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
# 1、文件拷贝对比
可以看到nio相对于传统io而言,更强调用快的方式来处理数据,这在性能上会有一定优势。
缺点是不够简单和优雅,其实对于文件读写来说,传统io使用缓冲区不一定比nio要慢。nio的新特性更多用在网络编程方面,后续的专题文章会讲到。
以文件拷贝为例,从理论上来说,传统io在操作系统层面会多次在用户态和内核态之间进行上下文切换,而nio的transferTo方法会用到零拷贝技术,也就是在内核态完成拷贝,省去了上下文切换和内存拷贝。
看例子比较一下:
public static void copyFileByStream(File source, File dest) throws
IOException {
try (InputStream is = new FileInputStream(source);
OutputStream os = new FileOutputStream(dest);){
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
}
}
public static void copyFileByChannel(File source, File dest) throws
IOException {
try (FileChannel sourceChannel = new FileInputStream(source)
.getChannel();
FileChannel targetChannel = new FileOutputStream(dest).getChannel
();){
for (long count = sourceChannel.size() ;count>0 ;) {
long transferred = sourceChannel.transferTo(sourceChannel.position(), count, targetChannel);
sourceChannel.position(sourceChannel.position() + transferred);
count -= transferred;
}
}
}
# 2、Buffer掌握
缓冲区有几个基本属性:
- capacity: 最大容量
- position: 当前已经读写的字节数
- limit: 还可以读写的字节数
Buffer的操作大概是:
- 创建Buffer之后,capacity是缓冲区大小,position=0,limit=capacity-1
- 写入字节,position增大
- 调用flip(),limit=position,position=0
- 取出字节到缓冲区,position=limit。想要重复读,调用rewind(),position归0
- 调用clear,position=limit=0
一个实际的文件复制例子大概是这样:
public static void fastCopy(String src, String dist) throws IOException {
/* 获得源文件的输入字节流 */
FileInputStream fin = new FileInputStream(src);
/* 获取输入字节流的文件通道 */
FileChannel fcin = fin.getChannel();
/* 获取目标文件的输出字节流 */
FileOutputStream fout = new FileOutputStream(dist);
/* 获取输出字节流的通道 */
FileChannel fcout = fout.getChannel();
/* 为缓冲区分配 1024 个字节 */
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
/* 从输入通道中读取数据到缓冲区中 */
int r = fcin.read(buffer);
/* read() 返回 -1 表示 EOF */
if (r == -1) {
break;
}
/* 切换读写 */
buffer.flip();
/* 把缓冲区的内容写入输出文件中 */
fcout.write(buffer);
/* 清空缓冲区 */
buffer.clear();
}
}
# Direct Buffer
当你使用普通的ByteBuffer的话,会被JVM管理起来,会进行自动垃圾回收
void change(){
ByteBuffer buf = ByteBuffer.allocate(1024);
...
}
当change()方法跳出的时候,buf会被自动回收。这里有一个问题:ByteBuffer里都是大量的字节,这些字节在JVM GC整理内存时就显得很笨重,把它们在内存中拷来拷去显然不是一个好主意。
在某些场景下,比如网络编程中,大量的字节在交互,可能导致GC很忙碌。
那么如果有一块内存可以脱离GC管理(此处指堆内GC),是不是更好呢?
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
ByteBuffer.allocateDirect
分配了一个Java堆外内存。
它的特点是:
- 生命周期内内存地址都不会再发生更改,进而内核可以安全地对其进行访问,很多 IO 操作会很高效
- 减少了堆内对象存储的可能额外维护工作,所以访问效率可能有所提高
- 创建和销毁的过程会增加开销,建议用于长期使用、数据较大的场景
- 堆外内存不受
-Xmx
参数的管理,而要使用-XX:MaxDirectMemorySize=512M
来设置 - ByteBuffer管理的堆外内存受到GC的控制,但只有在Full GC时回收。ByteBuffer的回收机制是:内部有一个低优先级线程Cleaner会执行clean(),内部还是调用System.gc(),所以一定不要
-XX:+DisableExplicitGC
# MappedByteBuffer
它将文件按照指定大小直接映射为内存区域,当程序访问这个内存区域时将直接操作这块儿文件数据,省去了将数据从内核空间向用户空间传输的损耗。
它本质上也是一种Direct Buffer,通过FileChannel.map
创建。
在处理大文件时使用MappedByteBuffer可能性能会提高,但也存在内存占用、文件关闭不确定(被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的)等问题。
# 总结
Java IO涉及到的类众多,如果限于细节,会导致整体感官上有些混乱。希望你学习本文之后,能站在新的高度认识Java IO,从而建立起自己的知识体系。
IO是Java基础知识点之一,会关联到文件操作、网络编程等,如果要追求一劳永逸的搞明白所有概念,无疑是困难的。
建议采用循序渐进的方式进行学习,后续我也会有文件操作和网络编程的相关文章,敬请期待!
祝你变得更强!