深入分析 Java I/O 工作机制

    作者: egg

    邮箱: xtfggef@gmail.com

    微博: http://weibo.com/xtfggef

    博客: http://blog.csdn.net/zhangerqing

    论坛: http://www.qtlife.net (随便建了一个,方便大家一起交流!)

    IO Java 及众多编程语言很重要的一块,同时很多程序的瓶颈和耗时操作也都在 IO 这块。所以能够很好的解决 IO 问题对提高程序性能有很大的帮助!本章我们将要系统的对 Java IO 做个分析,通过理论加实践,希望读者朋友们能真正彻底的理解并且掌握了它。本章系 Java 之美 [ 从菜鸟到高手演变 ] 系列 Java IO ,通过本章的学习,读者朋友们能基本了解到关于 IO 的很多知识。日后加以理解、分析、在项目中实践,定能灵活运用!

    一、简介

    IO 操作面临很多问题,信息量的巨大,网络的环境等等,因为 IO 不仅仅是对本地文件、目录的操作,有时对二进制流、还有一部分是网络方面的资源,所以多种原因直接造成 IO 操作无疑是耗时且复杂多变的。 Java IO 的支持是个不断的演变过程,经过了很多的优化,直到 JDK1.4 以后,才趋于稳定,在 JDK1.4 中,加入了 nio 类,解决了很多性能问题,虽然我们有足够的理由不去了解关于 Java IO 以前的情况,但是为了学好现在的类,我们还是打算去研究下,通过掌握类的优化情况来彻底理解 IO 的机制! Java IO 主要主要在 java.io 包下,分为四大块近 80 个类:

    1 、基于字节操作的 I/O 接口: InputStream OutputStream

    2 、基于字符操作的 I/O 接口: Writer Reader

    3 、基于磁盘操作的 I/O 接口: File

    4 、基于网络操作的 I/O 接口: Socket (不在 java.io 包下)

    影响 IO 性能的无非就是两大因素:数据的格式及存储的方式,前两类主要是数据格式方面的,后两个类是存储方式方面的:本地和网络。所以策划好这两个方面的活动,有助于我们合理使用 IO

    二、基于字节的 I/O 操作 (InputStream OutputStream)

    我们先来看看类图:

    图1

    图2

    二者类似,我只详细讲解 InputStream 类, OutputStream 留给大家自己去学习。 InputStream 类是个抽象类,里面核心的方法就是 read()、read(byte b[])、read(byte b[], int off, int len),这三个方法是用于读取数据的底层的方法,他们可以用来读取一下这些类型的数据:

    A. 字节数组

    B. String 对象

    C. 文件

    D. 管道,从一端进入,从另一端输出

    E. 流

    F. internet 资源

    每一种数据源都有相应的 InputStream 子类,因为 InputStream 是个处于顶层的类,用来处理各种数据源的类都继承了 InputStream 类,我们来看看这些类:

    ByteArrayInputStream:处理字节数组的类,允许将内存的缓冲区当做 InputStream 使用。

    StringBufferInputStream:将 String 转换成 InputStream ,内部实现用的是 StringBuffer

    FileInputStream:从文件中读取数据。

    PipedInputStream :用于从管道中读取数据。

    SequenceInputStream :将多个流对象转化成一个 InputStream

    FilterInputStream :装饰器类,为其它 InputStream 类提供功能。

    做过关于 IO 操作的读者知道,我们很少单独使用哪个类来实现 IO 操作,平时都是几个类合起来使用,这其实体现了一种 装饰器模式 (详见: http://blog.csdn.net/zhangerqing )的思想,在后面的分析中我们会详细的分析。从上面的图 1 中我们可以看出, FilterInputStream 虽说是 Inputstream 的子类,但它依然是 BufferedInputStream DataInputStream LineNumberInputStream PushbackInputStream 类的父类,这四个类分别提供了最贴近我们程序员使用的方法,如: readInt() 、readInt()、readInt()等等。对于 IO 操作,不管是磁盘还是网络,最终都是对字节的操作,而我们平时写的程序都是字符形式的,所以在传输的过程中需要进行转换。在 字符到字节 的转换过程中,我们需要用到一个类: InputStreamReader

    三、基于字符的 I/O 操作 (Writer Reader)

    图3

    图4

    Writer Reader 操作的目的就是操作字符和不是字节,和 InputStream OutputStream 配合增加 IO 效果。通过 InputStreamReader OutputStreamReader 可以进行字节和字符的转换,设计 Writer Reader 的目的是国际化,使 IO 操作支持 16 位的 Unicode 我把它们单独的画出来,因为要是全画的话,太大了放不下,有兴趣的 TX 可以在 rational rose 中导入其带的 JDK 类图看看,很过瘾的!

    四、基于磁盘的 I/O 操作 (File)

    五、基于网络的 I/O 操作 (Socket)

    六、 NIO

    四-六部分由于时间关系,还没有整理完,后续会补出来!

    七、经典 IO 操作

    1 、缓冲输入文件。

      import java.io.BufferedReader;
    import java.io.FileReader;
    
    public class InputStreamTest {
    
    	public static String read(String filename) throws Exception {
    		BufferedReader br = new BufferedReader(new FileReader(filename));
    		String s;
    		StringBuffer sb = new StringBuffer();
    		while ((s = br.readLine()) != null) {
    			sb.append(s + "
    ");
    		}
    		br.close();
    		return sb.toString();
    	}
    
    	public static void main(String[] args) throws Exception {
    		System.out.println(read("src/InputStreamTest.java"));
    	}
     }
     

    这段代码是从磁盘读入 InputStreamTest.java 文件,然后转换成字符串。输出就是将源文件原样输出。

    2 、从内存中读取。

      import java.io.StringReader;
    
    public class MemoryInput {
    
    	public static void main(String[] args) throws Exception {
    		StringReader in = new StringReader(
    				InputStreamTest.read("src/MemoryInput.java"));
    		int c;
    		while ((c = in.read()) != -1)
    			System.out.println((char) c);
    	}
    
    }
     

    read 返回的是 int 类型的数据,所以在输出语句中用 char 做了强类型转换。该程序将一个一个的输出字符。

    3 、基本的文件输出。

      import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.FileWriter;
    import java.io.PrintWriter;
    import java.io.StringReader;
    
    public class BasicFileOutput {
    
    	static String file = "basie.out";
    
    	public static void main(String[] args) throws Exception {
    		BufferedReader in = new BufferedReader(new StringReader(
    				InputStreamTest.read("src/BasicFileOutput.java")));
    		PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(
    				file)));
    		int lineCount = 1;
    		String s;
    		while ((s = in.readLine()) != null) {
    			out.println(lineCount++ + ": " + s);
    		}
    		out.close();
    		System.out.println(InputStreamTest.read(file));
    	}
    }
     

    输出:

    1: import java.io.BufferedReader;

    2: import java.io.BufferedWriter;

    3: import java.io.FileWriter;

    4 RandomAccessFile

    RandomAccessFile 被我们称为 ”自我独立的类”,因为它独立于我们前面说的 IO 类,与 InputStream OutputStream 没什么关系,除了实现了 DataOutput, DataInput两个接口外。所有方法都是重新编写,而且很多都是 native 方法,我们来看个例子,了解下这个类:

    5 、管道流

    八、标准 I/O

    就是我们最原始的使用的从控制台输入或者输出的那些类和方法,如 System.in System.out 等。

      public class StandardIO {
    
    	public static void main(String[] args) throws IOException {
    		BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    		String s;
    		while ((s = in.readLine()) != null && s.length() != 0)
    			System.out.println(s);
    	}
    }
     

    System.in 返回的是未经包装的 InputStream 对象,所以需要进行装饰,经 InputStreamReader转换为 Reader 对象,放入 BufferedReader 的构造方法中。除此之外, System.out System.err 都是直接的 PriintStream 对象,可直接使用。我们也可以使用 java.util 包下的 Scanner 类来代替上述程序:

      public class StandardIO {
    
    	public static void main(String[] args) throws IOException {
    		Scanner in = new Scanner(System.in);
    		String s;
    		while((s = in.next()) != null && s.length() != 0){
    			System.out.println(s);
    		}
    	}
    }
     

    九、性能分析及总结

    1 、一个文件读写工具类。

      import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileReader;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.util.ArrayList;
    import java.util.Arrays;
    
    /**
     * 一个非常实用的文件操作类 . 2012-12-19
     * 
     * @author Bruce Eckel , edited by erqing
     * 
     */
    public class TextFile extends ArrayList<String> {
    
    	private static final long serialVersionUID = -1942855619975438512L;
    
    	// Read a file as a String
    	public static String read(String filename) {
    		StringBuilder sb = new StringBuilder();
    		try {
    			BufferedReader in = new BufferedReader(new FileReader(new File(
    					filename).getAbsoluteFile()));
    			String s;
    			try {
    				while ((s = in.readLine()) != null) {
    					sb.append(s);
    					sb.append("
    ");
    				}
    			} finally {
    				in.close();
    			}
    
    		} catch (IOException e) {
    			throw new RuntimeException(e);
    		}
    		return sb.toString();
    	}
    
    	// Write a single file in one method call
    	public static void write(String fileName, String text) {
    		try {
    			PrintWriter out = new PrintWriter(
    					new File(fileName).getAbsoluteFile());
    			try {
    				out.print(text);
    			} finally {
    				out.close();
    			}
    		} catch (IOException e) {
    			throw new RuntimeException(e);
    		}
    	}
    
    	// Read a file,spilt by any regular expression
    	public TextFile(String fileName, String splitter) {
    		super(Arrays.asList(read(fileName).split(splitter)));
    		if (get(0).equals(""))
    			remove(0);
    	}
    
    	// Normally read by lines
    	public TextFile(String fileName) {
    		this(fileName, "
    ");
    	}
    
    	public void write(String fileName) {
    		try {
    			PrintWriter out = new PrintWriter(
    					new File(fileName).getAbsoluteFile());
    			try {
    				for (String item : this)
    					out.println(item);
    			} finally {
    				out.close();
    			}
    
    		} catch (IOException e) {
    			throw new RuntimeException(e);
    		}
    
    	}
    
    	// test,I have generated a file named data.d at the root
    	public static void main(String[] args) {
    
    		/* read() test */
    		System.out.println(read("data.d")); // testing is OK!
    
    		/* write() test */
    		write("out.d", "helloworld
    egg"); // testing is OK!
    
    		/* constractor test */
    		TextFile tf = new TextFile("data.d"); // testing is OK!
    
    	}
    
    }
     

    2 、读取二进制文件。

      import java.io.BufferedInputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    
    /**
     * to read the binary file
     * 
     * @author erqing
     * 
     */
    public class BinaryFile {
    
    	/* the parametre is a file */
    	public static byte[] read(File file) throws IOException {
    		BufferedInputStream bf = new BufferedInputStream(new FileInputStream(
    				file));
    		try {
    			byte[] data = new byte[bf.available()];
    			bf.read(data);
    			return data;
    		} finally {
    			bf.close();
    		}
    	}
    
    	/* the param is the path of a file */
    	public static byte[] read(String file) throws IOException {
    		return read(new File(file).getAbsoluteFile());
    	}
    }
     

    本章节未完,因为最近较忙,所以先发出一部分,后续会将缺少的补齐!