`
0428loveyu
  • 浏览: 29128 次
  • 性别: Icon_minigender_2
  • 来自: 西安
文章分类
社区版块
存档分类
最新评论

Java I/O

阅读更多

Java对文件磁盘、网络连接、内存缓存等输入输出采用统一的抽象实体进行处理,这种抽象就是流。流通过Java的I/O系统连接到物理设备,而所有流采用相同的方式工作。

1. I/O流

I/O流代表输入源或者输出目标,流可以表示很广泛的输入源或者输出目标,包括磁盘文件、外部设备、其他程序、网络连接(socket)等。另一方面,流支持许多种不同的数据,比如简单的字节、基本的数据类型、本地化字符、对象等。有些流简单传送数据,而其他一些流进行更多的操作和转换。I/O流能够处理的数据从简单的基本类型到复杂的对象。Java中所有I/O流可以分为两大类:字节流和字符流。

1.1 字节流(byte stream)

字节流用于处理字节的输入和输出。所有的字节流都继承自InputStream和OutputStream。字节流的类有许多,我们先以FileInputStream和FileOutPutStream为例,其他字节流都大同小异,主要差别在于如何构造字节流。
在下面的例子中,我们首先从txt文件中读入数据,然后再写到另一个文件中,即实现复制。先给出代码:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * @author Brandon B. Lin
 * 
 */
public class CopyBytes {

	/**
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
		FileInputStream in = null;
		FileOutputStream out = null;

		try {
			in = new FileInputStream("xanadu.txt");
			out = new FileOutputStream("outagain.txt");
			int c;

			while ((c = in.read()) != -1) {
				out.write(c);
			}
		} finally {
			if (in != null) {
				in.close();
			}
			if (out != null) {
				out.close();
			}
		}

	}

}
为了方便,我们没有对异常进行处理,而是直接抛出异常。上面代码的核心是while循环,输入流从数据源读入数据,一次一个字节,然后写入到目标文件,直到输入文件的数据都读取完毕,即in.read = -1。在finally中,如果流没有被关闭,则关闭流,防止资源泄露。表面上看,一次读入一个字节,然后写入,似乎很正常,但实际上,由于输入文件中包含的是字符数据,所以采用字符流更加方便。那么为什么还要使用字节流,因为所有其他流都是建立在字节流的基础之上。

1.2 字符流

Java采用的是Unicode字符集,对于本地化的字符集,比如ASCII字符集,字符流自动完成两者之间的转换。所有字符流都继承自Reader和Writer这两个类。对于文件,字节流中有对应的FileInputStream和FileOutputStream这两个类来完成读写,同样,字符流也有对应的FileReader和FileWriter两个类用于处理文件。还是完成上面例子的复制工作,采用字符流的代码如下:
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/**
 * @author Brandon B. Lin
 * 
 */
public class CopyCharacter {

	/**
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
		FileReader reader = null;
		FileWriter writer = null;

		try {
			reader = new FileReader("xanadu.txt");
			writer = new FileWriter("characteroutput.txt");
			int c;

			while ((c = reader.read()) != -1) {
				writer.write(c);
			}
		} finally {
			if (reader != null) {
				reader.close();
			}
			if (writer != null) {
				writer.close();
			}
		}

	}

}
除了使用的读写流不一样之外,代码十分类似。我们还发现,无论是字节流还是字符流,我们都采用int类型的变量c读入和写入流,但是区别在于,字节流的情况下,c用第八位存储一个字节,而在字符流当中,c使用低16位存储一个字符(2个字节)。

1.3 字节流与字符流

实际上,字符流的工作是建立在字节流的基础之上的,比如读入数据的Reader,首先使用InputStream流读入字节,然后完成字节到字符的转换,所有物理层的I/O由字节流来完成。可以这么理解,字符流只是把字节流做了包装,加入一些转换功能,但是这样我们使用起来具更方便。从字节流到字符流的一个“桥梁”是InputStreamReader和OutputStreamWriter,这两个类分别继承自Reader和Writer类,在构造这两种流的时候,分别向构造方法传递一个InputStream和OutputStream,从而建立它们之间的联系。如果没有合适的字符流可供使用,可以选择这两个类来完成工作。另外,也可以在socket类提供的字节流基础之上创建字符流。

1.4 读/写行

每次读入一个字符固然可以,但是Java提供了面向“行”(line-oriented)的操作流。每次遇到一个换行符(可能是"\r\n",或者“\r”或者“\n”),意味着一行结束,新的一行开始。为了支持面向行的操作,我们使用BufferedReader和PrintWriter这两个类来完成,有关这两个类的详细内容后面介绍。面向字节的可以被封装成面向字符,同样的,面向字符的也可以进一步封装成面向行的,代码如下:
public class CopyLine {

	/**
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
		BufferedReader br = null;
		PrintWriter pr = null;

		try {
			br = new BufferedReader(new FileReader("xanadu.txt"));
			pr = new PrintWriter(new FileWriter("characteroutput.txt"));
			String line;
			
			while( (line = br.readLine()) != null) {
				pr.println(line);
			}
		} finally {
			if (br != null) {
				br.close();
			}
			if (pr != null) {
				pr.close();
			}
		}

	}
}
调用readLine返回一个表示一行的字符串,然后使用println输出一行,十分方便。

2. 缓冲流(buffered)

2.1 为何需要缓冲流

上面例子中,除了写入行的例子外,采用的都不是缓冲方式的I/O操作,也就是说每一次读/写请求都是由操作系统直接相应的,这使得程序运行效率低下,因为每一次请求都要触发磁盘访问,网络访问等其他比较昂贵的操作。为了解决这个问题,java实现了缓冲读/写机制,缓冲输入流从内存缓冲区域中读入数据,而本地的输入API只在缓存为空的情况下调用,也就是说先把数据从磁盘读入到缓存中,然后输入流从缓存中读入数据,数据完了,本地API再从磁盘或其他设备把数据读入到缓存。举个也许不太恰当的比喻,缓存相当于电商在一个城市的大型仓库,磁盘或者其他设备比作产品生产商,而来自Java的读写请求相当于快递员去仓库取货。如果没有仓库这一“缓冲内存”,那么每个快递员要取货,都派一辆大卡车(你可不能说我就开个三轮车去,操作系统调用是很昂贵的,只能是卡车)去生产商哪里取货,这是很耗时、很耗费人力财力的,一个城市可能有成千上万个快递员,不得了。如果有了仓库这一缓冲地带,每次卡车都把货物提前运到仓库,然后所有的快递员过来仓库直接取走,如果仓库没有库存了,卡车再去生产商那里批量取货,这样一样效率就高多了。而对于缓冲输出,输出流先把数据写入到缓存,当缓存满的时候,本地API被调用,写入磁盘设备等。这就相当于我们寄快递了,快递公司收集来自很多个人的包裹,然后统一运输。只有数量到一定程度才会由卡车统一发往外地。

2.2 使用缓冲流

有了缓冲机制,Java还允许我们把一个非缓冲的流转换成缓冲的流。具体地,把非缓冲的流传递给缓冲流的构造器,所以在上面的例子我们看到这样的代码:
        br = new BufferedReader(new FileReader("xanadu.txt"));
	pr = new PrintWriter(new FileWriter("characteroutput.txt"));
Java提供4个缓冲流用于包装非缓冲流:BufferedInputStream、BufferOutputStream、BufferedReader、BufferWriter。前面两个用于包装非缓冲的字节流,后两个包装非缓冲的字符流。

2.3 刷新缓存(flush buffer)

有时候我们不想等到缓存全部满了才写入数据,这可以通过刷新缓存(flushing buffer)来完成,就是在缓存还未满的时候,就强制把数据赶入(冲洗)到磁盘或者设备中。有些缓冲的输出流支持自动刷新(autoflush),如果在构造器参数中启动了自动缓冲,当某些动作发生时,会导致缓存刷新。比如对于PrintWriter的对象,每当调用println或者format方法时或者遇到换行符,缓存就会刷新。如果想要手动刷新(flush也许翻译成冲洗更形象一些),调用缓冲流的flush方法,可以在任何时刻调用flush,只要缓冲流存在。但是如果流不支持缓存或者缓存功能被关,调用这个方法则无效。

3. Scanning and formatting

在有些情况下,一次读入一个字节、一个字符或者一行都不能满足我们的要求,比如对于一个英文文本,我们想要一次读入一个单词,这时候需要更高级的流来满足要求,Java提供了一个Scanner类来完成这一功能。同样的,对于输出我们可能也想要按照一定的格式输出,格式化流(stream that implement formatting)提供这一功能,PrintWriter提供字符流的格式化输出,PrintStream实现字节流的格式化输出。

3.1 scanning: Scanner

我们可以使用FileInputStream一次读入一个字节,FileReader一次读入一个字符(保存在int后16 bit),通过使用缓冲流BufferedReader我们可以一次读入一行,但是对于每次读入的控制,显然不够精确,有时候我们想要一次读入一个int,或者读入double,甚至我们想要输入流根据某些分隔符(比如空白,逗号)来一次读入数据,更复杂的,我们想要使用正则表达式来精确控制每次输入。Scanner就是为这个而设计的。
Scanner来在java.util包中定义,它能够根据正则表达式解析基本的数据类型和字符串。Scanner使用定界符模式(delimiter pattern)将它的输入分隔成小的片段(break its input into tokens)。默认的定界符为空白符(whitespace),包括空格符、制表符、换行符,可以使用Character类的isWhitespace来判断某个字符是否为空白符。比如,一个指定定界符的输入流数据可能像这样的:
其中,箭头指向的红色区域为定界符,非红色的区域被定界符分隔成token,是一个个的片段。分隔之后,可以使用各种next方法将片段转换成不同的数据类型。例如,
Scanner sc = new Scanner(System.in);
int i = sc.nextInt();
这两个语句可以用于重标准输入流读取一个int类型的数。可以使用正则表达式对输入实现精确复杂的控制。
	public static void main(String[] args) {
		String input = "1 fish 2 fish read fish blue fish";
		String delimiter = "\\s*fish\\s*";
		Scanner sc = new Scanner(input).useDelimiter(delimiter);
		System.out.println(sc.nextInt());
		System.out.println(sc.nextInt());
		System.out.println(sc.next());
		System.out.println(sc.next());
		sc.close();

	}
输出如下:
1
2
read
blue
下面我们使用Scanner从xanadu.txt中一次读入一个单词,使用默认的定界符。
	public static void main(String[] args) throws FileNotFoundException {
		Scanner sc = null;

		try {
			sc = new Scanner(new BufferedReader(new FileReader("xanadu.txt")));

			while (sc.hasNext()) {
				System.out.println(sc.next());
			}
		} finally {
			if (sc != null) {
				sc.close();
			}
		}

	}
注意到finally语句中关闭了sc,虽然Scanner不是一个流,但是必须关闭它“幕后”真正的流。结果依次输出每一个单词。
接下来我们深入研究一下Scanner这个类。

3.1.1 Scanner构造函数

跟其他互相嵌套的流一样,在创建Scanner对象的时候,通过传递参数指定其输入源,Scanner支持十分广泛的输入源。Scanner构造函数如下表:

我们看到,Scanner的输入源可以是字节流,文件,NIO中的Path(文件系统的路径),也可以是实现Readable这个接口的任何类,也可以使用ReadableByteChannel(这又是NIO中引入的)作为输入源,甚至字符串都是可以的。因此,使用Scannel很重要的一步就是要构造Scanner对象,传递一个输入源,同时可以指定字符编码方式,即指定字符集。
其中的Readable接口在java.lang包中定义,Reader、FileReader、BufferedReader、InputStreamReader等许多字符流都实现了这个接口。因此将字符流作为构造器参数是完全可以的。例如:
Scanner sc = new Scanner(System.in);
Scanner sc = new Scanner(new FileReader("test.txt");
创建Scanner对象之后,就可以使用Scanner提供的丰富的方法来获取我们想要的格式输入。

3.1.2 Scanner方法

在读入指定格式的数据之前,我们必须先指定定界符(delimiter),如果不指定,则使用默认的空白符。也可以获取当前Scanner使用的定界符。有关定界符的方法如下:
public <span style="background-color: rgb(255, 255, 0);">Scanner </span>useDelimiter(String pattern);
public <span style="background-color: rgb(255, 255, 0);">Scanner </span>useDelimiter(Pattern pattern);
public Pattern delimiter()
前两个用于设置定界符,可以传入字符串,也可以传入Pattern的对象,Pattern是在正则表达式包java.util.regex中定义的。注意到这两个方法返回的都是设置新的定界符之后的Scanner对象,许多地方都用到这种方式,比如Throwable类中的initCause方法,该方法签名如下:
public <span style="background-color: rgb(255, 255, 0);">Throwable</span> initCause(Throwable cause)
首先为异常对象指定背后异常,然后返回新的、被改变了的Throwable对象。
delimiter返回当前Scanner使用的定界符,为Pattern对象。除了可以设置定界符以外,还可以调用放啊useLocale设置本地,调用useRadix()设置进制。这三种设置可以通过调用reset方法恢复默认形式。

在获取指定格式(类型)的数据之前,检查是否还有可用的片段(token)是很有用的。如果流中还存在我们想要的类型(片段),那么我们才提取数据。比如,我们在读取下一个片段之前,想要检测一下当前流中是否还有下一个片段(token),我们使用hasNext()判断一下,如果还有片段,再调用next方法获取下一个片段。是不是觉得这样的模式在哪见过?没错,在集合框架的迭代器Iterator中,我们也有hasNext和next。事实上,Scanner实现了Iterator<String>接口,这两个方法就是在接口中定义的。hasNext方法声明如下:
public boolean hasNext()
如果还有下一个片段,返回true。否则返回false。注意,如果当前Scanner是关闭的,那么会抛出unckecked异常IllegalStateException,因为是unckecked,所以编译器不强制要求处理该异常。另外,这个方法可能会出现阻塞(block),等待scanner对象扫描输入源。
public String next()
这个方法返回下一个完整的片段(complete token),所谓完整片段,就是片段前后都有定界符(如果最后没有界定符呢?出现什么情况?)。返回的是字符串类型。该方法同样可能出现阻塞,即使hasNext方法已经返回true(为什么??)。如果已经没有片段可以返回,则抛出NoSuchElementException(继承自RuntimeException)。流被关闭的时候调用这个方法会抛出IllegalStateException。
我们可能想要检测流中是否还有特定格式(符合正则表达式)的片段,此时有以下方法:
public boolean hasNext(String pattern)
public String next(String pattern)
public boolean hasNext(Pattern pattern)
public String next(Pattern pattern)
此外,还可以检测是否存在基本类型等其他类型的数据,包括byte/short/int/long/float/double/BigInteger/BigDecimal。其中对于byte、short、int、long、BigInteger,还可以指定数值(即2、8、10、16进制)。

此外,Scanner还支持一些跟行有关的操作,
public boolean hasNextLine()
public String nextLine()
hasNextLine检测是否还有下一行,而nextLine不是返回下一行,而是将scanner的当前位置移到下一行的开始,同时返回当前行剩下的内容(包括所有定界符)。下面的例子展示了这个方法:
public static void main(String[] args) {
		Scanner s = new Scanner("A B C D E \n F G");
		int count = 1;
		while (s.hasNext()) {
			out.print("第" + count + "个片段:");
			out.println(s.next());
			count++;
			if (count == 3 && s.hasNextLine()) {
				out.println("当前行剩余内容:" + s.nextLine());
			}
		}
		
		s.close();

	}
读取前两个字符之后,调用hasNextLine,打印出当前行剩余内容(包括最开始的定界符),然后跳到下一行,输出结果如下:
第1个片段:A
第2个片段:B
當前行剩餘內容:  C D E 
第3个片段:F
第4个片段:G

另外两个有关行的方法是:
public String findInLine(Pattern pattern)
public String findInLine(String pattern)
这两个方法试图在当前行寻找匹配pattern的内容,如果匹配成功,返回匹配的内容。如果在当前行没有找到匹配的,返回null,而当前行的位置保持不变。下面的例子使用这个方法:
 public static void main(String[] args) {

      String s = "Hello World! 3+3.0=6";

      // create a new scanner with the specified String Object
      Scanner scanner = new Scanner(s);

      // find a string "World"
      System.out.println("" + scanner.findInLine("World"));

      // print the rest of the string
      System.out.println("" + scanner.nextLine());

      // close the scanner
      scanner.close();
   }
输出为:
World
! 3+3.0=6
匹配到World之后,跳过world之前的所有内容,所以nextLine输出的只有World之后的内容,注意到输出是从!开始的,也就是说在寻找匹配的时候,把定界符也看做匹配目标,在所有剩下的内容中进行匹配的。

还有两个方法可以跳过匹配的片段,
public Scanner skip(Pattern pattern)
public Scanner skip(String pattern)
这个方法跟findInLine一样,寻找匹配的时候把定界符也看做匹配目标的一部分,在剩下的内容中进行匹配,注意匹配的是第一个出现的,一旦第一个出现,跳过第一个,移动当前位置,这个方法就结束了。比如:
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		 String s = "Hello  World! Hello 3 + 3.0 = 6.0 true ";

	      // create a new scanner with the specified String Object
	      Scanner scanner = new Scanner(s);

	      // skip the word "Hello"
	      scanner.skip("Hello");

	      // print a line of the scanner
	      System.out.println("" + scanner.nextLine());

	      // close the scanner
	      scanner.close();
	   

	}
这个例子中寻找“Hello”,如果匹配成功,输出当前行的剩余部分,输出如下:
  World! Hello 3 + 3.0 = 6.0 true
注意到有两个Hello,但是匹配第一个之后方法就结束了,第二个Hello会被输出,注意到World之前其实有空格的,也就是说nextLine会连同定界符一并输出。如果我们把匹配模式改成“Hell”,没有o,那么也能匹配成功,并且最后输出是从Hello的最后一个字符o开始的。
总之,在匹配过程中,把定界符也看做匹配目标的一部分,在剩下的所有内容中匹配,此时没有定界符的概念。下面是一个例子:
	public static void main(String[] args) {
		 String s = "Hello  World! Hello 3 + 3.0 = 6.0 true ";

	      // create a new scanner with the specified String Object
	      Scanner scanner = new Scanner(s);

	      // skip the word "Hello"
	      scanner.skip("Hello  W");

	      // print a line of the scanner
	      System.out.println("" + scanner.nextLine());

	      // close the scanner
	      scanner.close();
	   

	}
匹配模式为“Hello W”,输出为:orld! Hello 3 + 3.0 = 6.0 true 可以看到不是以片段进行匹配的,而是忽略定界符的概念,把定界符(这里是空白符)也看做匹配目标的一部分。这跟匹配基本数据类型时候以片段为单位是不一样的,跟hasNext(Pattern)也是不一样的,注意区别!!!

3.2 formatting: PrintWriter & PrintStream

这两个类允许我们在写入数据的时候,指定各种输出格式。除了基本的写入功能外,最主要的是三个方法:print、println、format。具体参考API文档。

4. 命令行I/O

我们经常从命令行运行程序,或者通过命令行和程序交互。在这方面,Java提供两种命令行交互方式:标准量(standar stream)和控制台(console)

4.1 标准流(standard stream)

标准流一般从键盘读取输入,然后输出到显示器上。Java提供三种标准流:1)标准输入:System.in 2)标准输出:System.out; 3)标准错误:System.err.这三个标准流都在System类中定义好的,作为静态域存在,可以直接使用。out和err用于输出信息,之所以分开是方便将错误信息通过err重定向输出到文件中。这两个输出流都输PrintStream,是字节流。虽然是字节流,但是PrintStream在内部使用了一个字符流,实现很多字符流的特性。
而输入流in是地道的字节流InputStream,没有字符流的特性。如果想要拥有字符流特性,使用InputStreamReader这个桥梁:
InputStreamReader in = new InputStreamReader(System.in);

4.2 控制台(console)

标准流的一种更高级替代方式是控制台,控制台由java.io.Console类定义,控制台还实现其他一些标准输入输出没有的功能。它对于安全密码的输入特别实用,从控制台读取字符串特别方便。因此虽然Console类的大部分功能都能够通过标准输入输出实现,还是有必要介绍一下。
Console类没有提供构造方法,因此不能实例化。要想获取Console对象,通过对用java.lang.System.console()这个静态:
public static Console console()
需要注意的是,控制台并不总是存在或者可用的。如果没有跟虚拟机相关联的控制台,则返回null。对于从命令行运行的虚拟机,如果标准输入输出流没有被重定向,那么控制台一般为键盘和启动虚拟机的显示界面。但是如果虚拟机是自动启动的,比如说通过后台作业调度程序运行的,那么通常没有控制台。比如在Eclipse中,获取console一般都是失败的。Console的reader方法和writer方法分别返回与控制台相关联的Reader对象和PrinterWriter对象,用于完成标注输入输出完成的工作。有两个方法可以从控制台读入行:
public String readLine()
public char[] readPassword(String fmt,
                           Object... args)
第二个方法使用的是可变方法参数,用于提示控制台输入的格式。
值得一提的是控制台用于读取密码的方法,输入时候不会回显密码,有两个方法:
public char[] readPassword()
public char[] readPassword(String fmt,
                           Object... args)
参数和readLine完全一样。下面是一个例子,使用readLine和readPassword。
public class ConsoleTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		String input;
		Console con;
		char[] pw;
		con = System.console();
		if (con == null) {
			System.out.println("Not console found!");
			return;
		}

		input = con.readLine("Enter a String: ");
		con.printf("here is your string: %s\n", input);

		pw = con.readPassword("Enter you password: ");
		con.printf("First character is : %s\n", pw[0]);
		// use the password
		java.util.Arrays.fill(pw, ' ');
		con.printf("Now nothing : %s\n", pw[0]);

	}

}
为了安全,获取密码使用完毕之后应该立即将密码清空,上面代码使用了Arrays类的fill方法,用空白符填充。总之要让敏感信息在程序中停留的时间尽可能短。另外,上面的代码在Eclipse中运行失败,因为没有获取控制台。在命令行中运行该代码,可以成功,并且输入密码时不会显示。

5. 数据流(Data Stream)

数据流提供java基本类型和字符串的I/O操作:读入基本类型和写入基本类型,还包括字符串。所有数据流都实现DataInput或者DataOutput接口。其中使用最广泛的为DataInputStream和DataOutputStream。

5.1 DataInputStream

DataInputStream实现了DataInput接口,它最大的贡献在于允许我们在获取输入的时候站在基本数据类型或者字符串的层面上。通过readInt、readLong、readChar、readUTF等方法,我们得到的就已经是对应的基本类型了,十分方便。

5.2 DataOutputStream

跟输入一样,这个输出流通过writeInt、writeLong、writeDouble等方法,我们在写入的时候只需要传递基本数据类型,具体的转换工作流自动完成。很好。

6. 对象流(object stream)

正如data stream实现基本数据类型的I/O操作一样,对象数据流实现对象的I/O操作。对象流有ObjectInputStream和ObjectOutputStream,它们分别实现ObjectInput和ObjectOutput接口,而这两个接口又扩展自DataInput和DataOutput接口。所以说对象流能够处理基本数据类型和对象的I/O操作。

6.1 对象序列化

对象序列化就是将对象在某一时刻的状态保存到存储介质中,比如说磁盘、文件等。保存的是对象的“快照”,然后可以通过反序列化恢复对象。在实现远程方法调用(RMI)的时候,也需要序列化。远程方法调用允许一台机器调用另外一台机器上Java对象的方法,如果需要为方法提供的参数是对象,则调用端将对象序列化,通过网络传送到被调用端,被调用的一端反序列化,恢复对象作为方法参数。
对象之间的引用关系可能十分复杂,这就形成了一个有向图,甚至可能还存在环形引用,比如对象A引用了对象B,同时对象B也引用了对象A,对象还可能包含对自身的引用。序列化要完整保存这些引用网络。
只有实现Serializable接口的类,它的对象才能够通过序列化被保存和恢复。Serializable接口没有定义成员,它作为一个标记接口(maker),简单地用于指示一个类的对象能否被序列化。大多数类都能被序列化。
声明为transient的变量不能通过序列化被保存,序列化也不保存static变量。
序列化工作经过精心的设计,可以通过简单的方法调用来完成,但是有时候我们可能想要控制序列化的过程,做些自己需要的工作,比如压缩或者加密。接口Externalizable就是为了实现这个特性而设计的。序列化通过ObjectInputStream和ObjectOutStream两个类来完成。

6.2 对象流

对象流和数据流(data stream)很相似,但它能读写的不仅是基本数据类型和字符串,还能读写对象。下面是一个例子,首先使用ObjectOutputStream把对象和基本类型数据写入到文件,然后再使用ObjectInputStream读入这个文件,提取对象和其他基本类型数据,显示出来:
public class ObjectStreams {

	private static final String dataFile = "invoicedata";
	private static final BigDecimal[] prices = { new BigDecimal("9.99"),
			new BigDecimal("15.99"), new BigDecimal("3.99"),
			new BigDecimal("4.99"), };
	private static final int[] units = { 12, 8, 13, 29, 50 };
	private static final String[] descs = { "Java T-shirt", "Java Mug",
			"Duke Juggl", "Java Pin", "Java key chain" };

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		ObjectOutputStream out = null;
		try {   //写
			out = new ObjectOutputStream(new BufferedOutputStream(
					new FileOutputStream(dataFile)));
			out.writeObject(Calendar.getInstance());
			for (int i = 0; i < prices.length; i++) {
				out.writeObject(prices[i]);
				out.writeInt(units[i]);
				out.writeUTF(descs[i]);
			}
		} finally {
			out.close();
		}
                // 读
		ObjectInputStream in = null;
		try {
			in = new ObjectInputStream(new BufferedInputStream(
					new FileInputStream(dataFile)));
			Calendar date = null;
			BigDecimal price;
			int unit;
			String desc;
			BigDecimal total = new BigDecimal(0);

			date = (Calendar) in.readObject();

			System.out.format("One %tA, %<tB %<te, %<tY:%n", date);

			try {
				while (true) {
					price = (BigDecimal) in.readObject();
					unit = in.readInt();
					desc = in.readUTF();
					System.out.format("You ordered %d units of %s at $%.2f%n",
							unit, desc, price);
					total = total.add(price.multiply(new BigDecimal(unit)));
				}
			} catch (EOFException e) {
				System.out.format("For a TAOTAL of: $%.2f%n", total);
			}
		} finally {
			in.close();
		}

	}
}
代码上很清楚,无需多说。
writeObject和readObject方法调用很简单,但是其背后包含着很复杂的对象管理逻辑。对于简单的只包含基本数据类型的类(比如Calendar类),容易理解。但是在实际中,许多对象中都包含对其他对象的引用,在使用readObject读入数据重构对象的时候,必须能够完整重现原来的对象引用关系。对象中包含的引用可能还包含更深层次的引用,所以在写入对象的时候,必须完整保存这些引用网络,因此调用writeObject方法可能会导致许多对象被写入。
在下面这张图中,

对象a持有对象b和c的引用,而b又持有d和e的引用,所以调用writeObject方法的时候不仅写入对象a,还写入了其他4个对象。readObject时,所有对象都被读入,因此能够重现原来的对象引用关系。
如果同一个流中的两个对象(t和r)都持有对另一个对象(f)的引用,那么被写入的时候,会不会写入两个相同的f对象呢?不会,同一个流只写入一个f对象,但是写入两个队f的引用。所以如果像下面的代码那样对同一个对象写入两次,然后再读取:
Object ob = new Object();
out.writeObject(ob);
out.writeObject(ob);
---------------------------
Object ob1 = in.readObject();
Object ob2 = in.readObject();
此时ob1和ob2引用的是同一个对象。但是如果同一个对象被写入两个不同的流,那么就存在两个副本,那么再次被读入的时候,将得到两个不同的对象,即使它们的内容是相同的,但不在同一内存区域。

分享到:
评论

相关推荐

    java I/O内容

    这是一个关于Java I/O的知识点总结,希望大家共同学习,共同进步

    Java I/O 第二版

    OReilly.Java.I.O.2nd.Edition.May.2006 Java的io包主要包括: 1. 两种流:字节流(byte Stream)和字符流(character stream),这两种流不存在所谓的谁代替谁、谁比谁高级之说,它们互为补充,只是侧重点不同...

    java i/o 实例 编程 学习 教程 复习

    java i/o 实例 编程 学习 教程 复习,,,,,,,,,,,,,,,,,,,,,

    Java I/O, 2nd Edition

    Java I/O, 2nd Edition

    Java I/O: Tips and Techniques for Putting I/O to Work, 2nd Edition

    Elliotte Rusty Harold 的《Java I/O, 2nd Edition》。通常找到的版本是.chm格式的电子书转成的Pdf,而且效果不好。我将.chm文件和一个自己转成的pdf文件一起打包放上来,以便大家查阅。第一版的一个pdf文件也一起...

    Java I/O 流代码实例大全(01~09)

    Java I/O 流代码实例大全(01~09) File、FileInputStream、FileOutputStream、FileReader、FileWriter、BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter

    Java I/O详细笔记

    Java I/O详细笔记

    Java I/O总结

    Java I/O总结  从new BufferedReader(new InputStreamReader(conn.getInputStream()))想到的  Java I/O总结——InputStream  Java I/O总结——OutputStream  Java I/O总结——Reader  Java I/O总结——...

    Java I/O学习笔记: 磁盘操作 字节操作 字符操作 对象操作 网络操作 NIO & AIO Java I/O

    Java I/O学习笔记: 磁盘操作 字节操作 字符操作 对象操作 网络操作 NIO & AIO Java I/O Java是一种面向对象的编程语言,由Sun Microsystems于1995年推出。它是一种跨平台的语言,意味着可以在不同的操作系统上运行...

    Java I/O编程 java

    数据流的概念及输入输出方法 字节流和字符流 文件、文件流 随机访问文件 过滤流 管道流 对象流 数据流小结

    java I/o 详细介绍课件

    java I/o java I/o 详细介绍课件

    java I/O类的使用

    简单介绍了java 的输入输出系统中类的使用,java1.0和java1.1中的类的来源去向

    Java 新I/O

    NULL 博文链接:https://xace.iteye.com/blog/706935

    Java I/O流通讯录

    使用Java编写的一个可使用I/O读写文件的通讯录,具体可实现添加联系人、删除联系人、修改联系人、查看联系人等,无前端页面控制台输入输出 学习JavaI/O流的可下载学习

    javaI/O示例---简易聊天程序代码

    一个java输入输出流的小例子,适合初学者

    java i/0习题

    java i/o练习题,附带答案 主要练习输入,输出流...

    java I/O流总结

    这是一篇关于javaSe的基础班关于IO流的全套总结,希望能对你的基础学习有所帮助。

    java对I/O流的处理

    java I/O流处理的ppt,详细描述了I/O的各个流,对学习很有帮助!

    Java I/O 过滤流-带格式的读写操作

    NULL 博文链接:https://zhycaf.iteye.com/blog/981738

    Java I/O, NIO and NIO.2

    本资源转载自网络,如有侵权,请联系上传者或csdn删除。 本资源转载自网络,如有侵权,请联系上传者或csdn删除。 本资源转载自网络,如有侵权,请联系上传者或csdn删除。

Global site tag (gtag.js) - Google Analytics