------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------
IO(字符流和字节流)
字符流
一、字符流写入
IO就是Input和Output的简写,意思就是输入输入。IO流就是Input Output流,是用来处理设备之间的数据传输。不同设备之间是要有信息传输的,IO就是处理这信息传输的。在java中,对数据操作是通过流的方法,用于操作流的对象在IO包中。
流按操作分为两种数据:字节流和字符流。
按流向分为:输出流和输入流。
在操作数据中,字节流是通用的,因为计算机中,任何数据底层就是二进制的。而字符流是基于字节流才有的,具体实现方式是,把字符变成相对应的二进制数据,然后编成一张表,这样通过二进制数据就可以查到相对应的是哪个字符,比如ACSII表,GBK,UTF,这都是编码表。
IO流中常用基类:
字节流:InputStream(读数据)OutputStream(写数据)
字符流:Reader(读) Writer(写)
我们在查阅API文档的时候,就会知道,友这四个类派生的子类名称都是以这四个类的名称作为后缀名的,如:InputStream的子类,FileInputStream;Reader的子类FileReader。
关于IO流的使用,看一段代码
以操作文件为例,FileWriter
publicclass FileWriterDemo {
publicstaticvoid main(String[] args) throws IOException {
FileWriter fw = new FileWriter("Demo.java");
fw.write("abcdef");
fw.flush();
fw.close();
}
}
这里详细解释一下,FileWriter是Writer的一个现实子类,我们可以通过FileWriter创建对象。
FileWriter fw = new FileWriter("Demo.java");
该对象一被初始化就必须明确被操作的文件。建立对象时,会抛出IOException异常。而且该文件会被创建到指定目录下,如果该目录下已经有同名文件,那么原有文件将被覆盖,这一点要注意。其实该步骤就是明确数据要存放的目的地。
然后调用write方法写入一段数据
fw.write("abcdef");
用writer方法把字符串写入到流中。这里其实并没有把数据直接写到我们的目的地文件中,而是写入一个类似暂时存储数据的地方,这个地方就是流。
再调用flush方法,
fw.flush();
这个刷新对象中的缓冲中的数据,将数据刷入到我们制定的目的文件中。
最后我们应当关闭资源,
fw.close();
关闭流资源,但是关闭之前会刷新一次内部缓冲的数据,将数据刷到目的文件中。相比于flush方法,close刷洗后,会将流关闭,而flush刷新后则可以继续使用。
上面从创建流对象,到关闭流资源死条语句都会爆出异常,我们在使用这写语句的时候,要处理,不要抛出。对IO流异常的处理,这里详细描述一下。
首先,我们想到的都是用try-catch块包围语句(不算flush),因为如果我们用三个try-catch块包围三条语句的时候,当第一条语句出现错误,但这个错误被处理了,主程序会依旧执行下面的语句,而当一个流建立失败的时候,那么之后的语句是都是没有意义的,所以把三句话放到一个try-catch块中。
然后,我在考虑,如果三条语句中,如果写入语句错误抛出了异常,那么这是不管处理还是抛出,后面的关闭流动作就不会执行。我们在前面所知,运用系统资源完毕后,必须关系系统资源。所以就用到finall语句,因为这里面是必须之行的语句,所以,把关闭资源的动作放到这里来。关闭资源也有异常抛出,所以要try-catch块包围。
最后,考虑两点(其实是程序运行后,报出的异常,我们进行判断的),一是,建立流对象和写入语句都在try语句里,一旦报出异常,在finally与语句里的close就不能执行,因为流对象的引用只在try语句里,属于局部,到finally语句中就没有这个引用了。所以把引用建立在try语句之外,是引用指向为null。二、基于第一点,因为流引用的初始化为null,所以当建立流对象抛出异常时,finally里中引用调用的close方法关闭资源的动作就失败,此时引用为null,这就是null指针异常。所以要在关闭资源前,判断一下引用是否为null。
具体的程序是:
publicstaticvoid main(String[] args) {
FileWriter fw = null;
try {
fw = new FileWriter("Demo.java");
fw.write("aabbvvdd");
} catch (IOException e) {
e.printStackTrace();
}
finally{
try {
if((fw!=null))
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
现在知道Filewriter创建对象后,如果文件不存在则会创建文件,如果文件存在则会覆盖,那么如果想在原文件上续写我们需要的数据怎么办。查阅API文档后,就会发现,我们在建立建立对象时,在文件名后面加上true后,就可以对已有文件进行续写。API中是这样写,根据给定文件名及即是是否附加写入数据的boolean值来构造FileWriter对象。示例程序:
FileWriter fw = new FileWriter(“Demo.java”,true);
传递如一个参数true,代表不覆盖已有文件,并在已有文件的末尾进行数据续写。此时。我们在用fw.write(“heihei”),就会在原来文件上数据末尾加入这个字符串。
二、字符流读取
读取方式一:read,读一个
使用Reader子类,FileReader。
同样是加你一个文件读取流对象,和指定名称的文件相关联:
FileReader fr = new FileReader(“Demo.java”);
这里要保证该文件以及存在的,如果不存在,会发文件未找到的异常。然后调用read方法,这个方法会一次读取一个字符,而且会自动往下读,和迭代器的next有点类似。
int ch = fr.read();
注意的是,一、这个方法会返回int型的值
二、会自动往下读
三、当达到文件数据末尾的时候,则返回-1。
四、该方法在发生异常和到达数据末尾时,会一直阻塞。这个点后面要用到。
那么我们就可以用循环的方式来读取:
int ch;
while ((ch= fr.read()) != -1) {
System.out.println("ch=" + (char)ch);
}
读取方式二:read(char[] ch),通过字符数组进行读取。
将读取到的数据存入目标数组,那么建立读取流对象后,在建立一个数组
FileReader fr = new FileReader(“Demo.java”);
char[] ch = new char[1024];
因为方法返回的是int类型的值,返回的是读到的字符个数,当读取到文件最后的时候,就会返回-1。用while循环来完成。
int num;
while ((ch= fr.read()) != -1) {
System.out.println(newString(ch,0,num));
}
因为数据存储到数组中,所以把字符数组变成一个字符串对象,方便显示。这个方法把读取到的字符存入到字符数组中来,读取到几个,num就为几,为了打印有效位数,那么我们转成字符串的时候就用new String(ch,0,num),把字符数组中的有效位数转换成字符串。
通常,数组的长度定为1024的整数倍。注意如果结果输出到控制台上的时候,println不要加ln,因为一旦数据超过1024,那么你在打印某个数据后,不该换行的给换行了。
如果我们要把一个盘中的文件考到另一个盘中,那么就要现在目标地方建立一个文件接收数据,用FileWriter,也要建立和原文件的连接,用FileReader,然后源文件读取,目标文件写入,这就是copy。
字符流的缓冲区
缓冲区的出现提高了对数据的读写效率。缓冲区要结合流才能使用,是在流的基础上对流的功能进行了增强。
对应类:BufferedWriter,BufferedReader
对缓冲区的理解,就是之前我们读取数据的时候,是一个一个读取,然后一个一个写入的,缓冲区就是一个一个读取后,先存到一个地方,等这个地方存了很多数据后,在一次性的写入某个文件。这样就避免的频繁的读取动作,提高了效率。
缓冲区的出现是为了提高流的操作效率,所以在创建缓冲区之前,必须要先建立流对象。
FileWriterfw = new FileWriter(“Demo.java”);
然后将需要被提高效率的流对象作为参数传递给缓冲区的构造函数即可
BufferdeWriterbufw = new BufferedWriter(fw);
因为缓冲区是流的子类,所以使用和流中的方法时一样的。但是要注意一点,缓冲区要记得刷新,就是使用flush方法。当我们使用close()方法时,关闭缓冲区,其实就是关闭缓冲区中的流对象。
缓冲区中有一个新的方法,newLine(),意思就是换行。这个方法时跨平台的,通用的。
同样对于BufferedReader,有:
FileReader fw = new FileReader (“Demo.java”);
BufferedReaderbufw = new BufferdeReader(fw);
但BufferedReader中有一个读取方法,readLine(),就是读一行。这个方法返回值类型为String。因为文本文件中的数据一般都是一行一行的,但不包含终止符,所以这样读取效率很高。当读取到数据末位的时候,返回为null。这样我们就可以定义一个读取方法:
String line= null;
while((line=bufr.readLine())!=null){
System.out.println(line);
}
注意:一、readLIne返回值类型为String,二,当返回值为null时,表示读到文件末尾。
IO部分涉及到装饰设计模式,参见装饰设计模式部分。
IO流:字符流总结:
1、 IO流,就是用来处理设备之间数据传输的。流按操作分为两种数据:字节流和字符流。按流向分为:输出流和输入流。
2、 IO流中常用基类:字节流:InputStream(读数据)OutputStream(写数据)。字符流:Reader(读) Writer(写)。
3、 字节流中通用的,字符流是在字节流的基础上,配合码表进行字符数处理的。
4、 字符中常用子类FileWriter文件写入和FileReader文件读取,字符流中的子类很好辨认,通常都会有父类名后缀,如上述两者。
5、 FileWriter是现实子类,建立对象要与目标文件相关联。特点是:会创建目标文件,即使有同名文件存在,也会覆盖同名文件。这里就要注意,如果想在已有文件上进行续写,则要在构造函数中加上true关键字:FileWriter fr = new FileWriter(“Demo.java”,true)。
6、 FIleWriter写入方法为write,可写字符,字符串,字符数组。但并不是直接写入目标文件中,而是存入到流中,需再用方法flush方法刷新流,才会把数据写入到目标文件中去。
7、 FileReader建立时也要联系目标文件,读取方法为read(),但一次只会读取一个字符,且以int类型返回,读到文件数据末尾时,返回为-1,这要注意。
8、 字符流都有提高效率的缓冲区,分别为:BufferedWriter,BufferedReader
9、 BufferWriter对应Writer,会接收一个Writer的子类实例对象,有写入的方法,还一个newLine,就是换行的方法,这个方法跨平台通用。这个缓冲区,会提高原有写入的效率。
10、 BufferReader对已Reader,也接收一个Reader的子类示例对象,有读取的方法,还有另一个高效读取方法readLine,读一行的方法,且返回的是字符串,数据末尾返回null,方便操作。但是注意,不会返回结束字符,就是不会返回换行符。这个缓冲区提高了读取效率。
11、 因为ReadLIne底层实际上式对原有read方法的功能的增强,而且是传入原有功能的对象,这就是装饰设计模式。(参见设计模式之四装饰设计模式)。
12、 写程序时,如果继承父类,注意运用好父类方法,比如运用父类的构造函数,复写父类的方法等,简化代码书写,提高书写效率。
13、 非常重要的是:记得要关闭流资源!!!!方法close()。
14、 进行异常处理是要注意把close方法放到finally语句中,因为关流也抛异常,所以也要用try-catch包围,关流前要判断流对象引用是否为空,否则会抛出空指针异常。
15、 注意read方法时阻塞式的方法,就是说如果读到数据中间,后面的因为某种情况暂读取,则会一直处于阻塞状态。
字节流
一、字节流概述
除了文本格式的文件,一般像音频、视频、图片等数据,都是字节码数据,这就用到了IO中字节流的两个基类:InputStream OutputStream。注意一点,前者是读取,后者是写入,要明确了。
字节流中,操作数据的方法和字符流一致,也有相应的子类,FileInputStream和FileOutputStream,通过建立对象和目标文件相连接,也有相应的write和read方法。和字符流不同的是,write和read方法的重载方法,有接受数组参数的,这里的数组都是byte型的数组。同时,字节流没有flush方法,就是说,数据会直接写入到目标文件中去,不用刷新流,但必须关闭资源。在FileInputStream中,有一个方法available,这个方法会返回目标文件的数据长度,为int型。当我们要用read方法,把数据写入一个数组中,一般定义数据长度为1024,有这个方法后,就可以定义文件长度的数组,但要慎用:具体程序为:
//建立一个原数据长度的byte型数组
byte[ ] buf =new byte[ fis.available ];
//数组长度一次性满足,直接读取到数组中,就不用循环了
fis.read(buf);
System.out.println(newString(buf);
我们通常做法是建立1024长度的数组,进行循环读取。这里省略循环,但是一旦数组长度过长,比如我们要读取一个电影文件,这个电影文件数据太长,当我建立这个长度的数组超过内存大小的时候,就gameover了。所以,要注意available的使用环境,数据量不大的时候使用。
字节流也有缓冲区,对应的是BufferedInputStream,BufferedOutputStream。使用方法和字符流中的一致。这里就不赘述了。说一点模拟字节流缓冲区BufferedInputStream的方法。
我想要模拟缓冲区,先了解缓冲区是怎么工作原理的,大致步骤是这样的:1、将数据从硬盘上取出,这部有FileInputStream完成,2、将读取的数据存入缓冲区,我们模拟时将用数组代替,3、将数组中的数据一个个的返回。下面就看程序:
class MyBuffered {
//首先最为装饰类,要接受原有功能的对象
private FileInputStream in ;
MyBuffered(FileInputStream in){
this.in=in;
}
//定义接收从硬盘上取到的数据的数组
byte[] buf = newbyte[1024];
/*
定义指针和计数器。原因:一、我们自定义读取方法时用到read方法直接把数据存入数组,
返回的是int类型的数值,用count记录,所以从数组中往外读取过程中, count不断减少,
当见到0时,就意味着数组中的数据读取完毕。
二、读取数据时,要从前往后读取,所以要有指针,以便我们返回数据
*/
intpos=0,count=0;
//自定义read方法
publicint MyRead() throws IOException{
if(count==0){
count=in.read(buf);
//这里注意,一旦数组读取完毕,再读入一组数据时,要把指针归零
pos=0;
if(count<0){
return -1;
}
byte b = buf[pos];
count--;
pos++;
//这里进行与运算,将在下面讲到。
return b&255;
}
elseif(count>0){
byte b = buf[pos];
count--;
pos++;
return b&255;
}
return -1;
}
publicvoid myClose() throws IOException{
in.close();
}
}
这样一个自定义的缓冲区就形成了。注意一点是,返回int值是,与了一个值255。这里解释一下,也是一个小知识点:
read方法返回值为-1的时候,写入的循环程序就会终止。因为字节流每一次会读到一个字节,当我们把字节数据用int类型返回是,这里就有一个自动提升类型的动作。如果我们第一字节读到的是11111111,这个值是-1,当作为int型返回时,会有以前的一个八位变为四个八位:
11111111—->变为:
11111111-11111111-11111111-11111111
就是说,它会补上1,这样在int类型中,这个数据还是-1,所以就造成了循环可能在某个地方或一开始就会结束,导致文件没有被复制或数据不全。如果我们在前面补0,那么就可以保留原有字节部分数据不变,又可以避免-1出现。那么我们就可以对int数据与上255:
11111111-11111111-11111111-11111111
& 00000000-00000000-00000000-11111111
-------------------------------------------
00000000-00000000-00000000-11111111
这样返回的就不是-1了,而且write方法写入的时候,会自动把前面3个八位去掉,这样,理解起来就更容易了,因为我们改动返回的int值,只要最后八位和原始字节数据时一致的就可以了。
二、字节流特殊部分
1、读取键盘录入,转换流
首先知道两点:System.out对应标准输出设备,即控制台。
System.in对应标准输入设备,即键盘
System.in返回的是InputStream。所以连接动作是:InputStreamin = System.in。
然后我们用字节流中的方法读取键盘录入即可。但是我们在使用中,需要进行判断,判断换行符号,有换行符号我们才可以打印数据到控制台。判断换行符,\r\n,的动作其实不就是readLine方法么。所以,就用到下一个方法:
读取转换流:InputStreamReader,字节通向字符的桥梁,接收一个字节读取流
这个类可以把字节流转换为字符流。程序如下:
字节流读取键盘
InputStream in = System.in;
转换流转为字符流,接受一个InputStream对象
InputStreamReader isr = new InputStreamReader(in);
既然是字符流,就可以用字符流的装饰类,BufferReader
BufferedReader br = BufferedReader(isr);
剩下的读取方式就和字符流的做法一直,循环判断,最后关流即可。
写入转换流:OutputStreamWriter,字符通向字节的桥梁,接受一个字节输出流
这个类可以把字符流转换位字节流。程序如下:
连接控制台
OutpurStream out = System.in;
转换流转为字符流,接受一个OutpurStream对象
OutputStreamWriter isr = new OutputStreamWriter(out);
既然是字符流,就可以用字符流的装饰类,BufferReader
BufferedWriter br = BufferedWriter(isr);
剩下的读取方式就和字符流的做法一直,循环判断,最后关流即可。
流操作的基本规律和思路:
流既然是操作设备之间信息传输的,那么就要明确信息来源和信息目的。
首先:明确信息源。
信息源有两点:字节类的InputStream 字符类的Reader
其次:明确目的。
信息去往目的:字节类的OutputStream 字符类的 Writer
再次:明确操作数据类型
字符流 或 字节流
再次:明确使用哪个对象
源的对象:
键盘(System.in),硬盘(一般是FileInputStream和FileReader),内存
目的对象:
控制台(System.out),硬盘(一般是FileOutputStream和FileWriter),内存
最后:是否要提高效率
字符:BufferedReader BufferedWriter
字节:BufferedInputStream BufferedOutputStream
转换流的拓展:
现在的需求是,想要把录入的数据按照指定的编码表UTF-8,将数据存到文本文件中。
按照上面流操作的基本规律来说:
源是读取,InputStream Reader
目的是输出,OutputStream Writer
是文本文件 Writer
设备硬盘,FileWriter 设备键盘,InputStream。
提高效率BufferedWriter
其实在转换流中有默认的编码表,这样我们才可以把字节转换为相应的字符。系统中的FileWriter是使用默认编码表GBK。但是我们存储时,需要加入指定编码表UTF-8,而指定的编码表只有转换流可以指定。所以要使用的对象是OutputStreamWriter。而该转流对象还要接收一个字节输出流,而且可以操作文件的字节输出流,FileOutputStream。
则有:OutputStreamWriterosw =
new OutputStreamWriter(new FileOutputStream(”Demo.java”),”UTF-8”);
这就在写入的时候按照UTF-8的编码表输入。
那InputStreamReader来说,它是Reader的子类,InputStreamReader的子类是FileReader,FileReader可以直接读取文件,它的默认编码就是系统的默认编码,中国内一般都是GBK。如果想改变编码表的话,就的用到FileReader的父类InputStreamReader转换流,在转换流中才可以使用更改数据流入流出的编码表。
当然编码表也有不同,就像GBK是3字节的,UTF-8是4字节的。
改变标准输入输出
System中,的out表示标准输出,in表示输入,System.out即是控制台,System.in即是键盘。但这个默认的标准输出和输入时可以改变的。System.setIn(newFIleInputStream(“文件名”)),这个方法接受的是一个InputStream类型的对象,当在调用System.in时,输入设备就改变成那个文件了。System.setOut(PrintStreamout),这里接收的就是PrintStream的对象,而这个对象时OutputStream的子类,有一个可接收文件名的构造函数,这样建立新对象就可以改变原有标准输出,再调用System.out,标准输出就以一个文件了。
IO流字节流的总结:
1、 字节流,就是操作除了文本格式文件之外的数据,比如音频视频等数据。这些数据都是字节码文件。
2、 字节流两个基类:InputStream(读取)OutputStream(写入)
3、 同字符流相一致的是,也有操作文件的子类,同样也是read和write方法,但注意相关于数组的都是byte类型的数组。
4、 字节流没有flush方法,就是说数据会直接写到目标文件中去,不必刷新流,但必须关闭资源。
5、 字节流也有自己的装饰类,对应的是BufferedInputStream和BufferedOutputStream,注意没有读一行的方法了。其他基本使用方法和字符流一致。
6、 掌握模拟装饰类的思路和方法,小知识点是,read读取到11111111时的处理方式。因为从byte到int会自动提升类型,而且前3个八位补的是1,所以会返回-1。所以要与255,是返回值部位-1,可保留原有数据,也可实现我们定义的循环。write方法写入的时候,会自动去掉前3个八位。
7、 字符流重要部分:转换流。对应的是,InputStreamReader和OutputStreamWriter。InputStreamReader接收的是InputStream对象,OutputStreamWriter接受的是OutputStream对象,都是字节流对象。但其本身都是字符流的子类,所以可以使用字符流的装饰类。当然就可以使用字符装饰类中的方法了。使用BufferedWriter时要注意刷新流。
8、 操作流的基本操作规律:明确信息源和目的,是字节类的还是字符类的。明确设备,是键盘硬盘还是内存。明确操作数据,是字节还是字符。明确是否提高效率,是还是否。
9、 转换流的拓展,我们可以再读取和写入的时候,加入指定编码表,否则就是默认编码表。添加编码表只能是在转换流中使用,这一点要注意。相对应的,比如FileReader是InputStreamReader的子类,虽然FileReader可以直接读取文件的数据,但是只能按默认编码表。
10、 注意,装饰类(缓冲区),可以基类一下的子类,不要只局限与FileReader等例子之上。
11、 System中改变标准输入输出设备,用setIn和setOut,但是setOut需是PrintStream的对象。改变标准后,仍使用System.out和System.in。