深入学习IO流

来源:互联网 发布:中国民航飞行学院知乎 编辑:程序博客网 时间:2024/06/05 19:54

在上一章介绍了IO流的一些基本的知识,但是由于其效率问题,在实际应用中我们都不是单独的应用,而是通过类装饰器,包装它们,提高它们的效率。其中的类装饰器指的是缓冲对象。

在解释类装饰器前,先介绍下装饰设计模式:这种模式是为了优化类而存在的,当想要对自己已有的对象进行功能增强时,可以重新定义类,将已有的对象传入构造函数,基于已有的功能,并提供加强功能。装饰类通常会通过构造方法接受被装饰的对象,并基于被装饰对象的功能,提供更加强大的功能,它比继承更加灵活,避免了类继承体系臃肿,而且降低了类之间的耦合性。通常装饰类和被装饰类同属于一个体系中,继承同一个父类或者接口。

不管是字节流还是字符流,都提供了相应的装饰类。BufferedInputStream,BufferedOutputStream,BufferedWriter,BufferedReader,四大装饰类,从其类名可以知道其功能是起到缓冲作用和它们所属的类体系结构以及对哪些类提供增强功能。

先看下BufferedInputStream类在文档里是这样描述的:为另一个输入流添加一些功能,即缓冲输入以及支持 markreset 方法的能力。在创建 BufferedInputStream 时,会创建一个内部缓冲区数组。在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。mark 操作记录输入流中的某个点,reset 操作使得在从包含的输入流中获取新字节之前,再次读取自最后一次mark 操作后读取的所有字节。

说通俗点,就是它在内部自己定义了一个缓冲区数组,用来缓存读取的数据,基本原理和上一节中我们定义的字节数组大体一样。通过一段代码可以了解更详细点。

这是BufferedInputStream和BufferedOutputStream以前应用

复制媒体文件:

//通过字节流缓冲区来复制文件public static void Copy_Mp3()throws IOException{ //运行1504msFileInputStream fs=new FileInputStream("E:/Wildlife.wmv");FileOutputStream fos=new FileOutputStream("E:/s.wmv");BufferedInputStream bs=new BufferedInputStream(fs);//装饰类进行封装,提供更强大的功能,注意两者都接受了相应的流对象作为参数BufferedOutputStream bos=new BufferedOutputStream(fos);MyBufferedInputStream bs=new MyBufferedInputStream(fs);//自定义的缓冲区类,运行986msint num=0;while((num=bs.read())!=-1){//read()方法是将byte类型的数据转换为int类型,避免目标文件的第一个字节数据全部为1,即连续八个1所带来的影响bos.write(num);//write()方法将int类型的数据强制转换为byte类型,即只保留低8位数据,丢弃高24位数据;}bs.close();bos.close();System.out.println("copy over!");}
我们也可以用这张原理图说明情况:

既然我们知道它的原理,就可以自定义一个缓冲对象

/* * 自定义缓冲区 */import java.io.*;public class MyBufferedInputStream {private FileInputStream fs;private int count;private int pes;private byte[]buf=new byte[1024];MyBufferedInputStream(FileInputStream r){this.fs=r;}//一次对取一个字节,从缓冲区(字节数组)public int read()throws IOException{if(count==0){count=fs.read(buf);pes=0;if(count==-1)return -1;byte by=buf[pes];count--;pes++;return by&255;//保留低八位,保证能正确读取时间,避免特殊情况}if(count>0){byte by=buf[pes];pes++;count--;return by&255;}return -1;}public void close()throws IOException{fs.close();}}
用我们自定义的缓冲区对象,读取同一个文件到目标文件所以时间为986ms,而用java提供的类所以时间为1504ms,可以看出我们自定义的缓冲区对象读取效率更高

那么,如果我用两个装饰类,在加上一个自定义的字节数组,结果又是怎样?

看代码

public static void Copy_Mp34()throws IOException{//运行138msFileInputStream fs=new FileInputStream("E:/Wildlife.wmv");FileOutputStream fos=new FileOutputStream("E:/s.wmv");BufferedInputStream bs=new BufferedInputStream(fs);BufferedOutputStream bos=new BufferedOutputStream(fos);int num=0;byte[]bt=new byte[1024];//自定义缓冲区数组,为了一次把一批缓冲区里的数据读到目标文件,而不是一次读取一个字节数据到目标文件,相当于双缓冲区,提高效率while((num=bs.read(bt))!=-1){bos.write(bt,0,num);//能及时保证读取字节数组的有效数据}bs.close();bos.close();System.out.println("copy over!");

可以惊人的发现运行时间只用来138ms,速度相当的快,我理解是双缓冲区,如果我们把字节数组适当的变大点,运行时间会达到几十毫米,我把它理解为双缓冲区

然后,研究一下

BufferedWriter和BufferedReader,它们分别和Writer体系的类及Reader体系的类相对性,也属于装饰类,主要用于文本操作,提高效率。

/* * 字符读取流缓冲区; * 该缓冲区提供了一个一次读一行的方法readine,方便于对文本数据的获取。 * 当返回null时,表示读到文件末尾 * readLine方法返回的时候只返回回车符之前的数据内容,并不返回回车符 */import java.io.*;public class BufferedReaderDemo {/** * @param args */public static void main(String[] args) throws IOException{// TODO Auto-generated method stub//创建一个读取流对象和文件相关联FileReader fr=new FileReader("E:/tttt.txt");//为了提高效率,加入了缓冲技术,将字符读取流对象作为参数传递给缓冲对象的构造函数MyBufferedReaderDemo br=new MyBufferedReaderDemo(fr);//测试自定义的缓冲器BufferedReader br=new BufferedReader(fr);//java提供的缓冲区类String str=null;while((str=br.ReadLine())!=null){//readLine()返回的是字符串,其实它是自行把字符数组转化为字符串System.out.println(str);}}}

import java.io.*;public class BufferWriterDemo {/** * @param args *///创建一个字符写入流对象//为了提高字符写入流数据效率,加入了缓冲去技术。//只要将需要被提高效率的流对象作为 参数传递给缓冲区的构造函数即可public static void main(String[]args)throws IOException{FileWriter fw=new FileWriter("e:/tttt.txt");BufferedWriter bw=new BufferedWriter(fw);for(int i=0;i<=10;i++){bw.write("sdfsdfsdf");bw.newLine();//提供一个换行字符,BufferedReader在读取字符时,会忽略换行符,所以用newline()方法人为的添加换行符,在windows里换行符为'\r''\n'在别的系统平台上换行符是不一样的,为了跨平台的需要,可以用此方法人为的提供换行符                bw.flush();//将其实时的写入指定的文件,防止意外事件导致数据没有及时的写入文件}bw.close();//其实关闭缓冲区,就是关闭缓冲区中的流对象}}
我们根据原理可以自定义一个缓冲器

/* * 明白了BufferedReader类中特有方法readline的原理后 * 我们可以自定义一个类中包含一个功能和readline一致的方法 * 来模拟一个BufferedReader *//* * 它也是一个装饰类,对FileReader类进行了装饰 * 装饰设计模式: * 当想要对自己已有的对象进行功能增强时 * 可以定义类,将已有的对象传入,基于已有的功能,并提供加强功能。 * 那么自定义的该类成为装饰类 * 装饰类通常会通过构造方法接受被装饰的对象。 * 并基于被装饰的对象的功能,提供更强的功能呢 */import java.io.*;public class MyBufferedReaderDemo {private FileReader r;public MyBufferedReaderDemo(FileReader r){this.r=r;}//可以一次读一行数据的方法public String ReadLine()throws IOException{//定义一个临时的容器,原BufferedReader封装的是字符数组。StringBuilder sb=new StringBuilder();int len=0;while((len=r.read())!=-1){if(len=='\r')continue;if(len=='\n')return sb.toString();sb.append((char)len);}if(sb.length()!=0)return sb.toString();return null;}public void close()throws IOException{r.close();}}

关于基本的装饰类就介绍到这里,接下来我们谈谈一种比较特殊的类InputStreamReader和OutputStreamWriter.

从类名的组织结构,我们可以猜测下,它和字节流以及字符流有关系。正如闻名如其人,它们两正是为字节流和字符流的转化而产生的。InputStreamReader是将字节流转化为字符流进行读取操作,OutputStreamWriter是将字符流转化为字节流进行操作。它们两是字符流和字节流的桥梁。

查看jdk文档,对InputStreamReader是这样定义的:

InputStreamReader 是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。

这样给我们带来很大的方便就是之前我们是用的FileInputStreamReader是使用的系统默认编码,我们无法改变,而InputStreamReader可以让我们自由的指定字符编码。

其构造函数:

可以看出除了一个必要的InputStream的参数之外,还带来一个可以指定的编码。

同理OutputStreamWriter也一眼,就不详细的介绍

我们看下具体的应用:

先看下InputStreamReader的简单实例

/* * 键盘录入数据 * 当键盘录入一行数据时,我将其打印在控制台上 * 当输入over时,录入工作结束 */import java.io.*;public class ReadIn {/** * @param args */public static void main(String[] args)throws IOException {// TODO Auto-generated method stubInputStream in=System.in;//System.in是InputStream类型,系统默认的数据源为键盘StringBuilder buf=new StringBuilder();int num;while(true){num=in.read();if(num=='\r')continue;if(num=='\n'){if("over".equals(buf.toString()))break;//当输入over是,程序退出System.out.println(buf.toString());buf.delete(0, buf.length());//每读完一次,则清楚buf里面的内容,为下一次读取做准备}else buf.append((char)num);}System.out.println("sdf");}}
看下两个的简单综合应用

/* * 字符流:FileReader,FileWriter * BufferedReader,BufferedWriter * 字节流:FileInputStream,FileOutPutStream * BufferedInputStream,BufferedOutputStream * 本功能实现用Readline方法从控制台一次读取一行数据 */import java.io.*;public class TransStreamDemo {/** * @param args */public static void main(String[] args) throws IOException{// TODO Auto-generated method stub//获取键盘录入对象。/*InputStream in=System.in;//将字节流对象转成字符流对象,使用转换流。InpuStreamReaderInputStreamReader is=new InputStreamReader(in);//为了提高效率,将字符串进行缓冲区技术提高操作BufferedReader os=new BufferedReader(is);*/BufferedReader os=new BufferedReader(new InputStreamReader(System.in));//简写String line;/*OutputStream out=System.out;OutputStreamWriter osw=new OutputStreamWriter(out);BufferedWriter buw=new BufferedWriter(osw);*/BufferedWriter buw=new BufferedWriter(new OutputStreamWriter(System.out));//简写,System.out的默认输出到控制台while((line=os.readLine())!=null){if("over".equals(line))break;//System.out.println(line);buw.write(line);buw.newLine();buw.flush();}System.out.print("it's over");}}

还有个可以输出行号的缓冲类LineNumBufferedWriter

/* * 带行号的缓冲区类,继承于BufferedReader */import java.io.*;public class LinkNumberReaderDemo  {/** * @param args */public static void main(String[] args) throws IOException{// TODO Auto-generated method stubFileReader fr=new FileReader("E:/tttt.txt");MyLineNumberReader lr=new MyLineNumberReader(fr);//测试自己模拟的类//LineNumberReader lr=new LineNumberReader(fr);String str=null;lr.setLineNumber(10);while((str=lr.readLine())!=null){System.out.println(lr.getLineNumber()+":"+str);}}}

用法都一样,多了一个可以输出行号的功能。

输入,输出流重定向问题探讨

众所周知,我们的系统默认的输入为键盘,默认的输出为控制台,但很多时候,我们想把程序的某些属性持久化都硬盘上,比如说程序的配置文件等。我们也想改变默认的键盘输入或者控制台输出,这时,我们就会和流重定向相关联。重定向流可以分为输入流重定向,输出流重定向,错误流重定向;

输入流重定向:

package com.IO;import java.util.*;import java.io.*;public class TestSetInput {/** * @param args */public static void main(String[] args) {// TODO Auto-generated method stubtry{FileInputStream fis=new FileInputStream("E:/zz.txt");System.setIn(fis);int avg=0;int num=0;int total=0;int i;BufferedReader br=new BufferedReader(new InputStreamReader(System.in));String s=br.readLine();while(s!=null&&!s.equals("")&&!s.equals("over")){i=Integer.parseInt(s);num++;total+=i;System.out.println("i="+i+"num="+num+"total="+total);s=br.readLine();}}catch(IOException e){}}}

输出流和错误流重定向

package com.IO;import java.io.*;import java.util.*;public class TestOutStream {/** * @param args */public static void main(String[] args) {// TODO Auto-generated method stubPrintStream fo=null;PrintStream err=null;try{fo=new PrintStream(new FileOutputStream("E:/tt.txt"),true);//true为设置自动刷新//f=new PrintWriter(new FileWriter(),true);System.setOut(fo);//输出流重定向err=new PrintStream(new FileOutputStream("E:/tt_error.txt"),true);System.setErr(err);//错误流重定向int numl=0;int total=0;int i;BufferedReader read=new BufferedReader(new FileReader("E:/zz.txt"));String s=read.readLine();while(s!=null&&!s.equals("")&&!s.equals("over")){i=Integer.parseInt(s);numl++;total+=i;System.out.println("i="+i+"numl="+numl+"total="+total);s=read.readLine();}}catch(Exception e){System.out.print("it's over!");将输入到目标文件中System.err.println("出错时间:"+new Date());//将指定字符串输入到目标文件中System.err.println("出错详情:");e.printStackTrace(System.err);//将错误信息打印到目标文件中}finally{fo.close();err.close();}}}
介绍PrintStream和PrintWriter两个类

PrintStream 为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。它还提供其他两项功能。与其他输出流不同,PrintStream 永远不会抛出IOException;而是,异常情况仅设置可通过checkError 方法测试的内部标志。另外,为了自动刷新,可以创建一个PrintStream;这意味着可在写入 byte 数组之后自动调用flush 方法,可调用其中一个 println 方法,或写入一个换行符或字节 ('\n')。

PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。

而PrintWriter类

向文本输出流打印对象的格式化表示形式。此类实现在 PrintStream 中的所有 print 方法。它不包含用于写入原始字节的方法,对于这些字节,程序应该使用未编码的字节流进行写入。

PrintStream 类不同,如果启用了自动刷新,则只有在调用printlnprintfformat 的其中一个方法时才可能完成此操作,而不是每当正好输出换行符时才完成。这些方法使用平台自有的行分隔符概念,而不是换行符。

它提供了比PrintStream的强的功能,它就支持字节流写入操作,也指出字符流写入操作,所以我们一般都用PrintWriter,此外他继承与Writer类,而PrintStream继承OutputStream类,只支持字节流写入操作。



可以看出它的构造函数接受File类型,outputstream类型,String类型,以及Writer类型,还带有自动刷新功能,但是自动刷新功能只支持在流操作上。

我们还是通过代码来理解比较深刻

import java.io.*;public class PrintSteamDemo {/** * @param args */public static void main(String[] args) throws IOException{// TODO Auto-generated method stubBufferedReader buf=new BufferedReader(new InputStreamReader(System.in));//因为控制台输入是以字节流的形式,所以用了InputStreamReader做桥梁//PrintWriter pr=new PrintWriter(System.out);//用outputstream为参数//PrintWriter pr=new PrintWriter(System.out,true);//也可以用自动刷新功能,自己设置为true//PrintWriter  pr=new PrintWriter(new FileOutputStream("d:/tt.txt"),true);//将控制台数据读入到文件里,也可以设置自动刷新功能//PrintWriter  pr=new PrintWriter("d:/tt.txt");//直接用路径//PrintWriter  pr=new PrintWriter(new BufferedOutputStream(new FileOutputStream("d:/tt.txt")),true);//用缓冲区对象PrintWriter  pr=new PrintWriter(new FileWriter("d:/tt.txt"),true);//也可以用Writer类做为对象,实现自动刷新,这也是和PrintStream的区别,PrintStream类没有这个方法String line=null;while((line=buf.readLine())!=null){pr.println(line);//pr.flush();}buf.close();pr.close();}}