Java I/O学习心得一
Java的I/O是一个庞大的文件操作系统,初学者往往对I/O的使用比较迷茫,优点丈二和尚摸不着头脑的感觉。即便是使用java I/O处理了自己的实际需求仍然不知其所以然。当然我也是这样,所以几天以前我决定好好地看看java的I/O系统,到现在感觉还行,当然用好不敢自夸,但是对于I/O的那个套路差不多已经走通了,并不像以前那样云里雾里不知所云了。我学习的资料是《java编程思想》,这个总结并没有多少我自己的东西,因为我的水平有限,倒是想自己造一个轮子但是毕竟能力有限吗。
好了废话不多说了,我下面说一下我的学习思路,只是一个思路,当然我已经按照这个路子能比较清楚的使用java的I/O了,所以当大家发现这都是我摘抄的编程思想的内容是大家不要诧异,欢迎拍砖...
先来说一下File类。File类一个容易让我们"顾名思义"的类。通常我们看到这个类,就会想到这个类可能指代的是一个文件。但是实际上不是这样子的,他指的是一组文件名或单个文件名。Java API上面说:"文件和目录路径名的抽象表示形式"。我们先来看一段简单的代码就可以一目了然了,心中的疑惑就烟消云散了:
package review; import java.io.File; public class TestFile { public static void main(String[] args) { File file = new File("D://test.txt"); System.out.println(file); System.out.println(file.isFile()); } } Result: D:\test.txt true
让我们看一下输出地结果,当我们打印file对象的时候输出了一个window下的文件路径,第二个打印结果是true。当然我提前已经在D盘创建了这个test.txt文件,第二个输出地意思是这个test.txt对象是不是在D盘下面。我们稍微看一下File类的源码也可以看到设计者设计File类的意图:
public class File implements Serializable, Comparable<File> { ... private String path; ... /** * Creates a new <code>File</code> instance by converting the given * pathname string into an abstract pathname. If the given string is * the empty string, then the result is the empty abstract pathname. * * @param pathname A pathname string * @throws NullPointerException * If the <code>pathname</code> argument is <code>null</code> */ public File(String pathname) { if (pathname == null) { throw new NullPointerException(); } this.path = fs.normalize(pathname); this.prefixLength = fs.prefixLength(this.path); } /** * Converts this abstract pathname into a pathname string. The resulting * string uses the {@link #separator default name-separator character} to * separate the names in the name sequence. * * @return The string form of this abstract pathname */ public String getPath() { return path; } /** * Returns the pathname string of this abstract pathname. This is just the * string returned by the <code>{@link #getPath}</code> method. * * @return The string form of this abstract pathname */ public String toString() { return getPath(); }
看了这样的一部分源码我们对File类的大概用途差不多已经知道了。首先声明了一个String类型的path对象,在File的构造方法中有对path的赋值,toString方法也是返回了getPath()取得path值,FIle类一直在和String类型的path对象打交道,所以,这个File类的功能我们也大概知道了。至于具体的File如何使用,如何创建一个目录,如何打印一个目录就没必要举例了吧!自己对照着API实验吧。不过我们要注意一下FilenameFiter这个类,他的用法下面举了一个例子:
package org.wk.io; import java.io.File; import java.io.FilenameFilter; import java.util.Arrays; import java.util.regex.Pattern; /** * 作用:初探File类,获取文件名称的小工具类,并检查某个文件名称是否在此文件集合中 */ public class DirectoryList { public static void main(String[] args) { File path = new File("."); String list[] = null; if (args.length == 0) { list = path.list(); } else list = path.list(new DirFilter(args[0])); Arrays.sort(list, String.CASE_INSENSITIVE_ORDER); for(String str : list) { System.out.println("File Name: " + str); } } } class DirFilter implements FilenameFilter { private Pattern pattern; public DirFilter(String regex) { this.pattern = Pattern.compile(regex); } @Override public boolean accept(File dir, String name) { return pattern.matcher(name).matches(); } }
下面是比较核心的部分,也是为什么我们在使用java I/O的时候比较迷惑的地方---java的输入输出流。
关于流的概念:它代表任何有能力产出数据的数据源对象或者有能力接受数据的接收端对象,"流"屏蔽了实际I/O设备中处理数据的细节。实际上是为用户提供了接口,用户只需关心使用"流"就行了,不必在操心数据的处理。
Java的I/O流分成输入和输出两大部分,但是输入和输出又有基于字符和字节之分,那我们就具体展开来看一下其中的细节。
基于字符的输入和输出:
输入流:任何由InputStream派生的类,他们均包含read()方法。
输出流:任何有OutputStream派生的类,他们均包含write()方法。
FileInputStream类型
类 |
功能 |
构造器 |
如何使用 |
||
ByteArrayInputStream |
允许将内存的缓冲区当做InputStream使用。关闭 ByteArrayInputStream 无效,此类中的方法在关闭此流后仍可被调用,而不会产生任何 IOException。 |
缓冲区,字节将从中取出作为数据源;配合FilterInputStream对象使用提供有用的接口。 |
StringBufferInputStream |
将String对象转换成InputStream |
字符串,底层实现实际使用StringBuffer作为数据源;配合FilterInputStream对象使用提供有用的接口。 |
FileInputStream |
用于从文件中读取信息 |
字符串,表示文件名,或FileDescriptor对象作为一种数据源;配合FilterInputStream对象使用提供有用的接口。 |
PipedInputStream |
产生用于写入相关PipedOutputStream的数据。实现了"管道化"的概念。 |
PipedOutputStream作为多线程中数据源;配合FilterInputStream对象使用提供有用的接口。 |
SequenceInputStream |
将两个或多个InputStream对象转换成为一个InputStream。 |
两个InputStream或者一个容纳InputStream的Enumeration作为一种数据源;配合FilterInputStream对象使用提供有用的接口。 |
FilterInputStream |
抽象类,作为装饰器的接口。其中为其他的InputStream提供有用的接口。 |
详细建见表"FileterInputStream类型"。 |
OutputStream类型
类 |
功能 |
构造器 |
如何使用 |
||
ByteArrayOutputStream |
在内存中创建缓冲区,所有送往"流"的数据都要放置在此缓冲区中。 |
缓冲区初始化大小(可选),用于指定数据的目的地;配合FilterOutputStream对象使用提供有用的接口。 |
FileOutputStream |
用于将信息写至文件 |
字符串,表示文件名,文件或FileDescriptor对象指定数据的目的地;配合FilterOutputStream对象使用提供有用的接口。 |
PipedOutputStream |
任何写入其中的信息都会自动作为相关PipedInputStream的输出。实现"管道化"概念。 |
PipedOutputStream指定用于多线程的数据目的地;配合FilterOutputStream对象使用提供有用的接口。 |
FilterOutputStream |
抽象类,作为装饰器的接口。其中为其他的OutputStream提供有用的接口。 |
见表"FilterOutputStream类型"。 |
前面的表中有好多的地方在卖关子,什么装饰器类的接口,什么配合FilterInputStream提供有用的接口。装饰器类式设计模式中比较重要和常用的一种设计模式,可以上网上搜一下了解一下装饰器模式。装饰器模式的特点之一是比较灵活这也导致了我们在使用java I/O的时候比较困惑。BufferedInputStream buffer = new BufferedInputStream(...);里面的内容我们到底如何决定呢,正是装饰器模式给我们带来了这种麻烦。那我们看看这两个装饰器类的子类到底有什么也方便我们的选择。
FileterInputStream类型
类 |
功能 |
构造器参数 |
如何使用 |
||
DataInputStream |
与DataOutputStream搭配之用,因此我们可以按照可移植的方式读取基本类型。 |
InputStream 包含用于读取基本数据类型的全部接口。 |
BufferInputStream |
使用它可以防止每次读取都进行实际写操作,代表使用缓冲区。 |
InputStream可以指定缓冲区大小 本质上不提供接口只不过是向进程中添加缓冲区是必需的。与接口对象搭配。 |
LineNumInputStream |
跟踪输入流中的行号;可调用getLineNum(),setLineNum()。 |
InputStream 仅增加了行号,与接口对象搭配之用。 |
PushBackInputStream |
具有"能弹出一个子节的缓冲区"的功能,因此可以将读到的最后一个字符回退。 |
inputStream 通常最为编译器的扫描器,之所以包含在内是因为java编译器的需要,我们可能不会用到。 |
所以,如果我们通常这样写程序
BufferedInputStream b = new BufferedInputStream(new DataInputStream(new FileInputStream("xxx")));
如果对装饰器模式稍微有点了解的话,就会很容易理解这段程序。首先new FileInputStream("xxx") 作为DataInputStream 的构造器参数new DataInputStream(new FileInputStream("xxx")) 又作为BufferedInputStream 的构造参数,非常明显的装饰器模式。
我们再来看一下FilterOutputStream的实现类
FilterOutputStream类型
类 |
功能 |
构造器参数 |
如何使用 |
||
DataOutputStream |
与DataIntputStream搭配之用,因此我们可以按照可移植的方式读取基本类型。 |
OutputStream 包含用于读取基本数据类型的全部接口。 |
PrintStream |
用于产生格式化输出。其中DataOutputStream处理存储,PrintStream处理显示。 |
OutputStream,可以用boolean值表示在每次换行的时候是否清空缓冲区 |
BufferedOutputStream |
使用它可以防止每次读取都进行实际写操作,代表使用缓冲区。代表使用缓冲区,可以用flush()清空缓冲区 |
OutputStream,指定缓冲区大小 本质上不是接口,只不过是向进程中添加缓冲区所必需的。与接口搭配之用。 |
所以,类比输入流的程序我们可以这样写:
DataOutputStream d = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("...")));
道理与输入流一样,所以两者掌握一个另一个也就可以掌握了,但是并不是所有的输入和输出流的API都是有对应的,灵活掌握才是硬道理。
基于字节流的输入输出:
Reader和Writer两个类的操作是基于字符,提供兼容Unicode的功能,同时也起到国际化的作用。但是这两个类又不是完全没有关联的,我们可以通过适配器模式将InputStream转换成为Reader,这个适配器类就是InputStreamReader;同样我们可以通过适配器类OutputStreamWriter将OutputStream转换成为Writer。
这样我们在操作文件的时候就拥有了两个利器,一个是面向字节的InputStream,OutputStream;和面向字符的Reader,Writer。那么究竟什么情况下使用Reader,Writer,什么情况下使用InputStream,OutputStream呢?由于Reader和Writer是后来添加上的类,所以它的操作效率比InputStream和OutputStream高,于是在编程思想中给出了这样的结论:尽量常识使用Reader和Writer,一旦程序无法成功的编译,那是在选择InputStream和OutputStream。
下面我们看一下这两个类层次结构的关系:
面向字节 |
面向字符 |
适配器 |
InputStream |
Reader |
InputStreamReader |
OutputStream |
Writer |
OutputStreamWriter |
FileInputStream |
FileReader |
|
FileOutputStream |
FileWriter |
|
StringBufferInputStream |
StringReader |
|
无 |
StringWriter |
|
ByteArrayInputStream |
CharArrayReader |
|
ByteArrayOutputStream |
CharArrayWriter |
|
PipedInputStream |
PipedReader |
|
PipedOutputStream |
PipedWriter |
所以刚才写的两段代码就可以这样来通过Reader和Writer来改写:
BufferedInputStream b = new BufferedInputStream(new DataInputStream(new FileInputStream("xxx"))); BufferedReader reader = new BufferedReader(new FileReader("...")); DataOutputStream d = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("..."))); BufferedWriter writer = new BufferedWriter(new FileWriter("..."));
下面我们来看两种典型的应用方式,以后我们在进行I/O操作是就可以直接使用代码,或者直接套用这样的格式。
应用一:
package typical.usage; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; /** * @author Administrator 一个读写文件的小工具 */ public class TextFile extends ArrayList<String> { private static final long serialVersionUID = 1L; // 读文件 public static String read(String file) { StringBuilder builder = new StringBuilder(); try { BufferedReader bf = new BufferedReader(new FileReader( new File(file).getAbsoluteFile())); try { String s = null; while ((s = bf.readLine()) != null) { builder = builder.append(s); builder.append("\n"); } } catch (IOException e) { e.printStackTrace(); } finally { try { bf.close(); } catch (IOException e) { e.printStackTrace(); } } } catch (FileNotFoundException e) { e.printStackTrace(); } return builder.toString(); } // 写文件 public static void write(String file, String content) { try { PrintWriter pr = new PrintWriter(new File(file).getAbsoluteFile()); try { pr.print(content); } finally { pr.close(); } } catch (FileNotFoundException e) { e.printStackTrace(); } } // 根据指定的正则表达式拆分文件 public TextFile(String file, String regex) { super(Arrays.asList(read(file).split(regex))); if (get(0).equals("")) remove(0); } // 读文件 public TextFile(String file) { this(file, "\n"); } // 写文件 public void write(String file) { try { PrintWriter pw = new PrintWriter(new File(file).getAbsoluteFile()); try { for (String item : this) { pw.print(item); } } finally { pw.close(); } } catch (FileNotFoundException e) { e.printStackTrace(); } } public static void main(String[] args) { String file = read("D://IAReg.txt"); System.out.println(file); write("D://b.txt", file); TextFile tf = new TextFile("D://b.txt"); tf.write("D://c.txt"); TextFile tf2 = new TextFile("D://b.txt", "\\W+"); System.out.println(tf2); } }
这个例子展示了读写文件的基本方法,同时还实现了一个文件的过滤器。
应用二:
package typical.usage; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; /** * * @author wangkang * 打开一个文件用于字符输入,同时为了提高速度运用了缓冲。 */ public class BufferedInputFile { public static String read(String fileName) throws IOException { BufferedReader bf = new BufferedReader(new FileReader(fileName)); String s = null; StringBuilder bs = new StringBuilder(); while ((s = bf.readLine()) != null) { bs = bs.append(s + "\n"); } //关闭文件输入流 bf.close(); return bs.toString(); } public static void main(String[] args) { try { System.out.println(read("D:\\IAReg.txt")); } catch (IOException e) { e.printStackTrace(); } } }
这个示例展示了利用缓冲打开一个文件,并将其内容防止字符串对象中,当然我们获取了文件内容以后操作是根据我们的实际需求来。
应用三:
package typical.usage; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; public class FormattedMemoryInputStream2 { public static void main(String[] args) throws IOException { DataInputStream in = new DataInputStream(new ByteArrayInputStream( BufferedInputFile.read("D:\\IAReg.txt").getBytes())); while (in.available() != 0) { System.out.println(in.readByte()); } } }
这个示例展示了格式化的内存输入,new ByteArrayInputStream(
BufferedInputFile.read("D:\\IAReg.txt").getBytes())产生了一个格式化的字节数组共DataInputStream使用。
应用四:
//基本的本件输出 package typical.usage; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.io.StringReader; public class BasicFileOutput { public static void main(String[] args) throws IOException { String file = "D:\\test.out"; BufferedReader in = new BufferedReader(new StringReader( BufferedInputFile.read("D:\\IAReg.txt"))); PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter( file))); int lineNum = 1; String s = null; while ((s = in.readLine()) != null) { lineNum++; System.out.println(lineNum + ":" + s); //将字符串的内容写进文件 out.write(s); } in.close(); out.close(); } } //文件输出的快捷方式 package typical.usage; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintWriter; import java.io.StringReader; public class ShortCutFileOutput { public static void main(String[] args) throws IOException { String file = "D:\\test.out"; BufferedReader in = new BufferedReader(new StringReader( BufferedInputFile.read("D:\\IAReg.txt"))); PrintWriter out = new PrintWriter(file); int lineNum = 1; String s = null; while ((s = in.readLine()) != null) { lineNum++; // 将字符串的内容写进文件 out.write(lineNum + ":" + s); } in.close(); out.close(); System.out.println(BufferedInputFile.read("D:\\test.out")); } }
应用五:
package typical.usage; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class StoringAndRecoveringData { public static void main(String[] args) throws IOException { DataOutputStream out = new DataOutputStream(new BufferedOutputStream( new FileOutputStream("D://a.java"))); out.writeInt(12345); out.writeDouble(12345.000); out.writeUTF("上面的是一个整数和一个浮点数!!"); out.writeChars("上面的是一个整数和一个浮点数!!"); out.close(); DataInputStream in = new DataInputStream(new BufferedInputStream( new FileInputStream("D://a.java"))); System.out.println(in.readInt()); System.out.println(in.readDouble()); System.out.println(in.readUTF()); System.out.println(in.readChar()); in.close(); } }
这个示例展示了如何存储和恢复数据。我们在程序中使用DataOutputStream来存储数据,用DataInputStream来读取恢复数据。我们需要知道,只要是DataOutputStream写入的数据都何以利用DataInputStream来准确的得到数据,尽管是在跨平台应用中,这也体现了java的跨平台性。writeUTF()以与机器无关方式使用 UTF-8 修改版编码将一个字符串写入基础输出流,所以我们可以将字符串和其他数据类型相混合。
应用六:
package typical.usage; import java.io.IOException; import java.io.RandomAccessFile; public class UsingRandomAccessingFile { static String file = "D://IAReg.txt"; static void play() throws IOException { RandomAccessFile rf = new RandomAccessFile(file, "r"); for(int i = 0; i < 11; i ++) { System.out.println(rf.readLine()); } rf.close(); } public static void main(String[] args) throws IOException { //读取数据 play(); RandomAccessFile rf = new RandomAccessFile(file, "rw"); for(int i = 0; i < 3; i ++) { rf.writeUTF("哈哈哈"); } rf.close(); //测试修改结果 play(); } }
这个示例展示了如何读写随机访问文件。我们是通过RandomAccessFile来实现这样的功能的,RandomAccessFile适用于大小已知的记录组成的文件。这个类的构造器的参数需要一个特别的String字符串,用来指定"随机读----r"或者"既读又写-----rw"
到这里就告一段落吧,说的都是一些皮毛的东西,至于一些高级的应用,我也是不太明白。但是只要入门了一切都好说了,面包会有的一切会有的。这一章我们必须要掌握装饰器和适配器模式,这样我们才能理解类之间纷繁复杂的配合使用。