相信很多刚毕业或者工作一两年的程序员在工作中都很少用到IO流,甚至跟我一样没有用过,但是面试中又经常问到,怎么办呢?下面是乔最近的一些学习心得,希望和大家一起探讨,有错的地方希望大家能给予指导,嘻嘻谢谢啦!
首先是流的概念:Java中对文件的操作是以流的形式进行的。什么是流?流是Java内存中的一组有序数据序列,Java将数据从源(包括文件呀、内存或者是键盘输入都可以)读到内存中,就形成了流,然后可以将读出来的这些流写入到另外一个地方去。概念性的东西可以第一眼看比较难以理解,我可以通俗一点讲给你听。
在本地也就是我们说的在文件夹里的一个文件,存储在硬盘中。当我们接到一个任务,要求我们要从本地文件中读取数据显示到屏幕上,这时,文件可不是直接从硬盘里就显示到屏幕上来的哦,而是文件的数据先转移到内存中,内存再显示到屏幕上,这个过程数据就是以流的形式转移,这也就涉及要流。有人就说了,这听起来不和数组啊这些一样吗?我把文件内容读到一个数组里面,想要什么再拿什么呗!
我们来看看代码
import java.io.FileReader;
import java.io.IOException;
public class intern {
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("F:\\dubbo\\20180808.txt");
int ch; // not char
ch = fr.read(); // throws IOEXCEPTION
while (ch > -1) { // if ch = -1, means got the end of the file
System.out.printf("%c", (char) ch);
ch = fr.read(); // throws IOEXCEPTION
}
}
}
这段代码很简单,我定义了一个FileReader对象fr,只想了我一个本地文件20180808.txt,在多次fr.read()之后,将放回的结果打印出来,直到返回值是-1,也就是读完最后一行。在读入内存的时候如果以整个数组的形式读进去,操作的时候又以数组的形式拿出来,非常耗时间,而且我们不一定在一个时间内需要那么多数据,所以引入流的思想。流提供了一种让我们在比集合更高的概念级别上指定计算的数据视图。
举个例子来讲,桌子上有两个杯子,A装满水,B是空的,现在要求不能一定A的位置,把水转移到B中,怎么做呢?我们可以拿个碗,将A的水倒到碗中,再将碗中的水倒到B,这就相当于一个强大的Read()方法就把整个文件读完了。这里碗就好比是内存,B好比是屏幕,A是硬盘。水量不多的情况下当然可以这么做,但是我现在改成两个水池的水,你去哪里找一个这么大的碗来?有的人就会说了,我不用那么大的碗,我就拿个普通的碗一碗一碗舀过去,只是时间的问题吧,毕竟你没有限制时间!好,即使你可以这样,上面我说了,碗相当于内存,你这样相当于要整个内存陪着你一点一点地传输东西,那留给其它程序的内存就很少了。想在有一个办法,就是我连接一个管道,安装一个开关,简单多了吧,想什么时候抽水就什么时候抽,想抽多少抽多少,是不是方便很多,这就相当于read()方法,一次读一行,把数据读到内存中。
FileReader这个对象就相当于我们使用了一条管道,我给它安装了一个按钮read(),每按一次就有一点数据流到内存。至于数据是怎么从内存显示到屏幕上,这个不是流的范围了。
读完上面的内容,我相信你对流已经有大致的认识了。
Stream是java的一个类,专门用于处理程序个外部设备的输入输出(简称IO),这里的外部设备可以是文件、键盘或者网络。基本所有的流都在这个包中。实际上流就是数据在程序和外部设备之间的管道,而类的方法则是管道上的各种按钮。
所以为什么需要流呢?
- 在数据量很大的情况下,节省时间
- 内存有限,需要“分时分步”进行传输
- 带宽有限
而stream可以分次传输数据,一点一点传,这就是Stream存在的意义。这里可以参照腾X的下载软件,每次下载一个文件都是在本地建一个cache文件,然后把文件分成很多个部分缓存到本地,等缓存完毕再以.qlv形式的文件保存在硬盘中。
流的重要性:
- 流是java的一个类,但类并不是流
- 流是单向的,要么input,要么output。
- 流不会改变原数据,也不能存储数据
Java的Stream有很多个子类,可以划分为若干类
1. 输入流和输出流
Java流的传输只有一个方向,所以我们把从外部设备流向程序的数据流称为输入流。
反之,把程序数据流向外部设备的流称为输出流。
2. 字符流和字节流
根据数据在Stream里的最小传输单位,可以将流分为两类:
字符流:最小的传输单元为一个字符(1个字符(char)=2个字节(byte)=16bit(位))。
字节流:最小传输单元为一个字节(byte)。
关于字符和字节,其实挺简单的,就是不容易理解,我会在另起文章说,这里简单说一下:字符流只能读写文本格式的外部设备,而字节流可以读写任何格式的外部设备(包括2进制文件、音视频等)
四大基本Stream:
Java流有很多种,但是基本都是继承自四个基本流(这四个类是抽象类,不能直接实例化new):
- InputStream:输入字节流
- OutputStream:输出字节流
- Reader:输入字符流
- Writer:输出字符流
1. Reader流:
上面提到的例子用到FileReader流就是继承自Reader流,Reader流属于输入字符流,也就是说Reader流的作用是从外部设备传输数据到程序,而且最小的传输单元是1个字符。
1.1 new Reader()
首先讲一下构造方法,上面这个方法并没有抛出异常(throws Exception),但是其子类的某些重载方法会抛出异常(throws Exception)
例如FileReader
public FileReader(String fileName)
throws FileNotFoundException
1.2 int read() throws IOException
read()方法可以将是Reader最基础的一个方法,它的作用是读取一个字符,然后把这个字符存储到一个整形(int)变量中。(至于为什么是这样,会有另一篇文章讲解),如果读到了输入流末尾则返回-1。这个方法可能会因为网络、或者数据源的原因导致读取失败,所以需要抛出异常。
1.3 int read(char[] charbuffer)throws IOException
如果你觉得上面一个read方法一次读取一个字符太过蛋疼,觉得速度是不是有点拖延,那么Reader流提供了另一个方法,可以一次读取若干个字符。这个方法就是上面方法的重载,但是意义大不相同。
上面的int read()方法返回值就是读取的字符数据,只不过是用int变量来储存,而这个int read(char[] charbuffer)有一个字符数组作为参数,这可不是作为数据源的数组,而是作为存储数组。read()将读取到的若干个字符从头数组头开始放进这个字符数组中,然后返回实际接受到字符的个数。如果读取到文件尾,则依旧返回-1.
例如下面的语句
len = ReaderA.read(charbuffer);
程序会从ReaderA这个流中读取一次若干(len)字符存到字符数组charbuffer中,len就是实际读取的字符数。
你可能就会问了,你上面老是说到读取若干个字符,到底是几个字符啊?能不能决定读取几个字符啊?一般来说啊,len是参数字符串的长度,也就是说一般来讲int read(char[])方法会填满这个字符串,但是也有例外呀,内存紧张,或者是外部设备中有49个字符,但是参数字符串的长度是20,那么前面两次,每次读取的都是20个字符,到最后一次读取的就是9个咯。这时候你也许心里也许就会有结论,说原来len就是返回读取字符的个数,也不重要嘛。错了,这个参数非常重要。原因是:int read(char[])这个方法呢有一个特点,就是每次读取字符放在参数数组里面的时候呢,是不会擦除掉参数数组上一次遗留下来的数组,而是从头覆盖上去,如果你每次都是读取20个,那 没什么好说的啦,每一次覆盖都是新的,但是就像上面提到的情况,前面都是读取20个,最后一次只读取9个,那么后面还有11个是上次的数据,所以len起到的是标识参数数组中有效读取的字符个数!至于能不能决定读取几个字符,这个有另外的方法,这个方法呢不能确定,但是能保证每次读取的字符不会超过参数数组的长度。
1.4 int read(char[] charbuffer,int offset,int length) throws IOException
这个方法也是重载,参数多了,但是有了上面的方法之后,就不难理解了。参数charbuffer用于存放读取的字符数据。第二个参数是int类型,可以猜到应该是个位置或者个数有关,所以这个是指定读取的字符数据从参数数组的第offset个位置开始存放,所以字符数组中可能有以前的数据。第三个就是指定每次最多读取的字符数据啦,必须不大于参数数组的长度,不然就和上面1.3的方法一样了,失去了参数本身的意义了。
1.5 void close() throws IOException
这个就是我们每次使用完一个流之后要把它关掉,释放出系统资源的方法了,一般放在try-catch语句的finally中,这是使用流的良好习惯之一。
2. Writer流
相对于Reader流,Writer流也是字符流,但是方向是从程序到外部文件。
2.1 new Writer()
这个方法就不再多说了,和new Reader()类似。
2.2 void write(int c) throws IOException
这里就要注意了,c并不是表示写入的个数,前面说了,c是用来存放char类型的参数字符,形式不同,原因在另一篇文章说。你就记住这个int就是传入的需要写到文件中的字符。当我们执行这个方法的时候,字符数据并没有立即写入到外部设备,而是保存在了输出流缓冲区中(内存),所以不可以一边写一边读。
例子
import java.io.*;
public class Writer1{
public static void f(){
int i;
String s = new String("Just a testing for writer stream!\n");
File fl = new File("/home/gateman/tmp/testwriter1.txt");
if (fl.exists()){
fl.delete();
}
try{
fl.createNewFile();
}catch(IOException e){
System.out.println("File created failed!");
}
System.out.println("File created!");
FileWriter fw = null;
try{
fw = new FileWriter(fl);
}catch(IOException e){
System.out.println("Create filewriter failed!");
if (null != fw){
try{
fw.close();
}catch(IOException e1){
System.out.println("Close the filewriter failed!");
return;
}
System.out.println("Close the filewriter successfully!");
}
return;
}
for (i=0; i<s.length(); i++){
try{
fw.write((int)s.charAt(i));
}catch(IOException e){
System.out.println("error occurs in fw.write()!");
}
}
System.out.println("done!");
}
}
上面我们可以看到fw在执行write方法的时候,传入的参数还特地将char类型转为int了。而且这段代码执行成功之后,你会发现文件中其实并没有内容,这就是我刚刚说到的问题了,写入的字符数据都在缓存区中,还没有真正写入到文件中,那怎么办呢?很简单,你执行close()方法把流关闭就可以了,这也就是为什么说要养成关闭流的好习惯了。
2.3 void write(char[] cbuffer) throws IOException
这个方法是将以个char数组写入到输出缓冲区中。
2.4 void write(String s) throws IOException
这个也很容易理解,就是把一个字符串写入到输出流缓冲区中。对比一下Reader流,为什么Reader流没有一个方法是把数据字符存放到String中的呢?这是因为,字符串其实是不可修改的字符常量,这里可以去了解一下java的字符串原理。
3.InputStream
所谓的InputStream就是字节输入流,与Reader最大的区别就是它不但支持文本的外部设备之后,还支持二进制外部设备。
3.1 new 方法
3.2 int read() throws IOException
3.3 int available() throws IOException
先看一下这个方法在jdk中的字面解释
返回下一次对此输入流调用的方法可以不受阻塞地从此输入流读取(或者跳过)的估计剩余字节数。下一次调用可能是同一个线程,也可能是另一个。一次读取或跳过次数量个字节不会发生阻塞,但是读取或者跳过的直接可能小于该数。
杂志某些情况下,非阻塞的读取(或跳过)操作再执行很慢时看起来就像被阻塞了,比如在网速很慢的网络中下载很大的文件。
简单地说,这个方法就是返回输入流中还有多少剩余字节数。其他方法基本都类似,就不再说了。
Java缓冲流
上面我们讲解方法的时候提到了一个缓冲流,在之前提到的Java流的分类上,按照流的功能可以将流分为原始流和处理流(包裹流),也就是说处理流是包裹在原始流对原始流数据进行进一步的处理,这时的流就有两层,缓冲流就是处理流的一种。
1.缓冲流的定义
缓冲流是处理流的一种,它以来原始的输入输出流,它令输入输出流具有一个缓冲区,显著减少与外部设备的IO次数,而且还提供一些额外的方法。可见,缓冲流最大的特点就是具有1个缓冲区,而我们使用缓冲流无非两个目的,减少IO的次数和使用缓冲流的一些额外方法。硬盘IO是整个计算机最慢的动作,所以我们要减少外部设备的IO,方法很简单,就是把每一次IO的数据缓存起来,自然就减少了IO次数。
缓冲流输入流BufferInputStream流的常用方法
1.new BufferInputStream(InputStream in,int bufferSize)
这个是缓冲输入流最常用的构造方法,它有两个参数,第一个就是要包裹的输入流,其中InputStream是一个抽象类,实际上我们传送的是其子类的对象,这里就用到了多态。
第二个参数也很重要,就是指定缓冲流缓冲区的初始大小,单位是kb。
缓冲流有预读机制,比起使用缓冲数组的缓冲效果更加明显,如果处理一些大数据文件,或者网络传输,使用缓冲流的效果会更好。