二,流的分类:
1,按照操作数据分为字节流和字符流。
2,按照流向分类分为输入流和输出流。
三,常用流的基类
1,字节流的抽象基类InputStream和OutputStream。
2,字符流的抽象基类Reader和Writer。
注意:这四种类派生出来的子类名称都是以其父类名作为子类名的后缀。
四,字符流的基本应用:
1,在io包中有一个Writer类,它的write(String str)方法可以把字符串写入文件。OutputStreamWriter 是字符流通向字节流的桥梁,OutputStreamWriter是一个抽象类它的一个子类FileWriter,其构造函数FileWriter(String fileName)可以通过文件的路径名称
创建一个文件,如果该路径中存在相同文件名的文件,则会覆盖原文件。
需求:在当前目录创建一个Demo.txt文本文件,向其中写入字符。
import java.io.*; public class WriterDemo { public static void main(String[] args) throws IOException{ //创建一个FileWriter对象,该对象一被创建就要明确该对象的文件名和路径;默认路径为该public类 FileWriter f = new FileWriter("Demo.txt"); //调用其父类的父类Writer中的write(String str) 方法,向流中写入要写入文件的内容,为String类型 f.write("sahfh"); //上一步只是将内容写入了流中,而并没有写入目标文件中,所以还要调用Writer的flush()方法,将流 //中的类容刷入到目标文件中; f.flush(); f.write("-----------kjlj"); f.flush(); //还有一个方法close()也可以将流中的内容刷入到目标文件中,只不过是刷入后,流关闭,不能再次写入内容 //而flush则不同,刷入一次后还可以刷入 f.close(); //f.write("nk,j");运行时会抛出异常,Stream closed } }
实际开发中对异常的处理是不建议直接抛给方法调用者的,有些异常是需要在方法内不进行处理的。所以针对上面需求的异常处理的正确做法请参看下面的示例:
import java.io.*; public class FileWriterDemo { public static void main(String[] args) { FileWriter f = null; //这三步操作都会抛出异常,因为这三部是关联的,所以放在一起处理 //但是如果将变量和对象都放在第一个try中,则close就不知道去关闭谁了,因为在不同代码块,不能引用 //所以将对象的初始化放在try中 try { f = new FileWriter("K:\\Demo.java");//K目录不存在,运行时异常,后面的操作都会停止,流没有被创建成功 //为空 f.write("sdaghkjg"); } catch (IOException e) { System.out.println("catch:" + e.toString()); } finally {//由于流在程序使用完后必须要关闭以释放资源,所以放到finally中,在前面出现异常后也会被执行; try { if(f != null) f.close();//只要创建了流就必须要在使用完后关闭 //但是有一个问题就是如果文件路径不存在,则说明流也就没有创建,那他关闭谁呢?所以要判断流是否为空。 //zai finally中的close也可能抛出异常,所以也要进行try。。。catch处理; } catch (IOException e) { System.out.println(e.toString()); } } } }
总结:通过上述程序基本了解怎样创建一个文本文件并且向里面写入字符,但是我们发现每次运行程序的时候总会创建一个新文件覆盖就的文件,怎么才能让程序每次运行时不会覆盖旧文件而是在旧文件后面续写内容呢?
示例:
import java.io.*; public class FileWriterDemo2 { public static void main(String[] args) { FileWriter f = null; try { f = new FileWriter("Demo.txt",true);//参数true表示允许往Demo.txt文件中续写内容 //没有此参数,则每次运行时都会创建一个新文件覆盖原文件 f.write("sdagskagnj+++++\r\n+++++++++sjgak\r\n");//windows系统的记事本换行是\r\n而不是\n //但是EditPlus中是可以识别的。 } catch (IOException e) { System.out.println(e.toString()); } finally { try { if(f != null) f.close(); } catch (IOException e) { System.out.println(e.toString()); } } } }
2,文件的读取
文件的读取方式和文件的写入有相同之处;也有不同之处;首先必须要有要读取的文件,创建FileReader对象,然后调用read方法,将文件的内容读取到控制台,读取完之后要关闭流。还要注意异常的处理;它于FileWriter的不同在于不用刷新流;
(1)public int read()throws IOException读取单个字符。在字符可用、发生 I/O 错误或者已到达流的末尾前,此方法一直阻塞。 用于支持高效的单字符输入的子类应重写此方法。
需求:从当前文件所在目录读取Demo.txt文件中的字符。这种方式读取一次只能读取到单个字符。
import java.io.*; public class FileReaderDemo { public static void main(String[] args) { //创建对象获取要读的文件 FileReader fr = null; try { fr = new FileReader("Demo.txt"); //调用父类Reader的read方法,读取单个字符,获取到的是单个字符对应的ASCII码,是int型,所以要装换类型 //char ch = (char)fr.read(); int ch = 0; while ((ch = fr.read())!=-1) { System.out.print((char)ch); } } catch (IOException e) { System.out.println(e.toString()); } finally { try { //关闭流文件 if(fr!=null) fr.close(); } catch (IOException e) { System.out.println(e.toString()); } } } }
(2)文件读取的第二种方式:
Reader的public int read(char[] cbuf)方法;
先定义一个char[]数组,数组的大小自己定义,每次调用read方法时,会根据数组的大小相应的从目标文件中读取这么多个字符,返回的是int型的读取字符的个数,当读取到文件的末尾时,会返回-1,但是char[]数组的指针还是会移动,没这次读取的字符存入到了char[]数组中,数组指针反复移动,然后第二次读取到的字符会覆盖掉上一次的字符。
import java.io.*; public class FileReaderDemo2 { public static void main(String[] args) { FileReader fr = null; try { fr = new FileReader("Demo.txt"); char[] ch = new char[1024];//大小设定为1024的好处是,每次读取之后就存入,减少了读取次数; //一个char是两个字节,一共是2k int num= 0; while ((num=fr.read(ch)) != -1) { System.out.println(new String(ch,0,num)); } } catch (IOException e) { System.out.println(e.toString()); } finally { try { if(fr!=null) fr.close(); } catch (IOException e) { System.out.println(e.toString()); } } } }
小例子:读取 java 文件中的数据,并将这些数据打印在控制台上。(为了简化代码,我们暂且将异常直接 throws 出去)
import java.io.*; public class FileReaderDemo3 { public static void main(String[] args) throws IOException { //读取当前文件所在目录下的FileReaderDemo.java文件 FileReader fr = new FileReader("FileReaderDemo.java"); char[] chs = new char[1024];//创建字符数组存储每次读取的单个字符 int num = 0; while ((num = fr.read(chs)) != -1) { System.out.println(new String(chs,0,num)); } fr.close();//关闭读取流,释放资源。 } }
五,字符流读取与写入方法的综合举例。
在四中已经总结了文件的创建与写入数据的方法和读取文件数据的方法,实际开发中文件的处理一般都是写入和读取双向操作的。下面是一个综合应用举例。
需求:将C盘下的一个java文件复制到D盘下。
其原理是: 1 ,在 D 盘创建一个目的文件,用于存储 C 盘目录的文件; 2 ,创建读取流,于 C 盘文件关联; 3 ,通过不断的读写,将 C 盘文件的内容复制到 D 盘的目的文件中;
import java.io.*; public class FileCopy { public static void main(String[] args) throws IOException { //copy_1(); copy_2(); } public static void copy_2() throws IOException { FileWriter fw = null; FileReader fr = null; try { fw = new FileWriter("D:\\System2.txt"); fr = new FileReader("C:\\WriterDemo.java"); char[] cbuf = new char[1024]; int len = 0; while ((len = fr.read(cbuf))!= -1) { fw.write(cbuf); } }//这种方式的读取效率比第一种方法要高,方法1每从WriterDemo.java读取一个字符就要往System2.txt文件中写入一个字符, //方法2则是每从WriterDemo.java中读取1024个字符才往System2.txt中写入一次,写入的是1024个字符。 catch (IOException e) { throw new RuntimeException("复制失败"); } finally { if(fw!=null) { try { fw.close(); } catch (IOException e) { throw new RuntimeException("流不存在"); } } if(fr!=null) { try { fr.close(); } catch (IOException e) { throw new RuntimeException("流不存在"); } } } } public static void copy_1() throws IOException { //创建D盘的目的文件 FileWriter fr = new FileWriter("D:\\System.java"); //获取要复制的原文件 FileReader fw = new FileReader("C:\\WriterDemo.java"); int len = 0; //将原文件的内容(字符)读取到流中 while ((len=fw.read()) != -1) { //将流中的内容写到目的文件中 fr.write(len); } //关闭流文件 fr.close(); fw.close(); }//这种方法效率比较低,是一个字符一个字符读取到流中然后从流中一个一个写入到目标文件中。请看方法二:copy_2(); }
六,字符流的缓冲区
通过上述的知识点归纳可以看出字符流的操作,底层都是对单个字符单个字符的操作,效率是比较低的,所以java为我们提供了另外一个工具,就是字符流的缓冲区,其目的就是提高对数据的读写效率。
对应的类:BufferedReader和BufferedWriter
注意:缓冲区必须要结合流才可以使用的,它只是在流的基础上对流的功能进行了增强。
1,BufferedWriter类的出现在Writer父类的基础上有增加了一个可以直接写入一个换行分隔符,如果不使用BufferedWriter类,那么我们如果要向一个文本文件写入字符,那么就必须通过:\r\n来换行,使用BufferedWriter类的newLine()方法就可以直接写入一个换行符,并且这个方法是跨平台的,在任何平台下都可以使用此方法换行。
需求:向 Demo.txt 文件中写入多行数据。
import java.io.*; public class BufferedWriterDemo { public static void main(String[] args) throws IOException { //创建一个字符写入流对象 FileWriter fw = new FileWriter("Demo.txt"); //为了提高字符写入的流效率,加入了缓冲技术; //只需要将需要提高效率的流对象作为参数传递给缓冲区的构造函数即可 BufferedWriter bf = new BufferedWriter(fw); for (int i=0;i<5 ;i++ ) { bf.write("sdagg" + i);//BufferedWriter也是Writer的子类,所以可以调用write子类。 bf.newLine(); bf.flush(); } //缓冲区用完后一定要记得关闭; // fw.close();其实关闭缓冲区就是关闭缓冲区中的流对象; bf.close(); } }
2,BufferedReader类是为了提高字符流的读取效率而出现的,它在Reader父类的基础上出现了一个功能很强大的读取一个文本行的方法readLine(),这就使得我们读取文本文件时便利了很多。
基本用法:
import java.io.*; public class BufferedReaderDemo { public static void main(String[] args) throws IOException { //创建一个读取流对象和文件关联 FileReader fr = new FileReader("Demo.txt"); //为了提高效率,加入了缓冲技术,将需要提高效率的流对象作为参数传递给缓冲对象的构造函数 BufferedReader bufR = new BufferedReader(fr); String line = null; //readLine方法返回的时候只返回回车符之前的内容,不返回回车符。 while ((line=bufR.readLine()) != null) { System.out.println(line); } bufR.close(); } }
需求:自定义一个类继承自 Reader 类,该类和 BufferedReader 类的功能一样。重点是写出 readLine() 方法。
import java.io.*; public class MyReadLine { public static void main(String[] args) throws IOException { FileReader fr = new FileReader("Demo.txt"); MyBufReadLine mr = new MyBufReadLine(fr); String line = null; while ((line=mr.myLine()) != null) { System.out.println(line); } mr.myClose(); } } class MyBufReadLine extends Reader { private Reader f = null; MyBufReadLine(Reader f) { this.f = f; } //定义一个临时容器。原BufferReader封装的是字符数组。 //为了演示方便。定义一个StringBuilder容器。因为最终还是要将数据变成字符串。 //BufferedReaer的底层也是读取单个字符,当遇到'\r\n'是会换行,此时返回读取到的一行的字符串 public String myLine() throws IOException { StringBuilder sb = new StringBuilder(); int ch = 0; while ((ch = f.read()) != -1) { if(ch == '\r') continue; if(ch == '\n') return sb.toString(); else sb.append((char)ch); } if (sb.length() != 0) return sb.toString(); return null; } public void close() throws IOException { f.close(); } public int read(char[] cbuf, int off, int len) throws IOException { return read(cbuf,off,len);//该抽象方法我们不知道如何去实现,所以有子类区实现,也就是我们传入的FileReader //趋去实现。 } public void myClose() throws IOException { f.close(); } }
七,字符流缓冲区的综合应用举例。
在总结五中写出了一个复制C盘的一个文本文件到D盘,使用的方法是从源读取单个字符然后写入到目的文件,这种效率是比较低的,学习了字符流缓冲区后就可以一行一行的读取,然后一行一行的写入目的文件。请看下面的示例:
需求:复制一个 java 文件,使用缓冲区 BufferedReader 和 BufferedWriter 。(注意异常的处理方法)
import java.io.*; public class BufferCopy { public static void main(String[] args) { BufferedReader bufR = null; BufferedWriter bufW = null; try { bufR = new BufferedReader(new FileReader("BufferedReaderDemo.java")); bufW = new BufferedWriter(new FileWriter("Demo.txt")); String line = null; while ((line=bufR.readLine())!=null) { bufW.write(line); bufW.newLine(); bufW.flush(); } } catch (IOException e) { throw new RuntimeException("文件读取失败"); } finally { try { if(bufR != null) bufR.close(); } catch (IOException e) { throw new RuntimeException("输入缓冲流不存在"); } try { if(bufW != null) bufW.close(); } catch (IOException e) { throw new RuntimeException("输出缓冲流不存在"); } } } }
八,读取带行号的文本。
LineNumberReader类:
LineNumberReader(Reader in)
使用默认输入缓冲区的大小创建新的行编号 reader。
之前我们读取文本都是单行单行的读取,那么怎样在读取单行文本时给每个文本行添加一个行号呢?
需求:从一个 java 文件中读取文本数据,打印到控制台,是每行都有一个行号。将行号的其实行设置为 100.
import java.io.*; public class LineNumberReaderDemo { public static void main(String[] args) throws IOException { FileReader fr = new FileReader("Demo.txt"); LineNumberReader lb = new LineNumberReader(fr); String line = null; lb.setLineNumber(100);//设置起始行号为100 while ((line=lb.readLine())!=null) { System.out.println(lb.getLineNumber() + ":" + line); } lb.close(); } }
总结:字符缓冲区 BufferedReader 和 BufferedWriter ,以及带行号的读取流 LineNumberReader 类内的实现原理都用到了装饰设计模式的原理,关于装饰设计模式,请参看装饰设计模式。