黑马程序员——IO流6:其他IO技术介绍-上

------ Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

IO流6:其他IO技术介绍-上


        由于《IO技术补充》这一部分篇幅较长,内容较杂,相互之间关联较少,因此我们分上下两篇博客进行介绍,分别是《IO流7:其他IO技术介绍-上》,以及《IO流8:其他IO技术介绍-下》。

1 Properties

1.1  Properties概述

        我们曾经在《其他7:若干工具类介绍》中简单介绍过Properties,而这一篇博客中我们将对这个类进行更为详细的说明和演示。

        我们曾经说过,Properties对象可通过System类的静态方法getProperties获得,并且Properties类是Hashtable的子类,具有Map集合的特点,而由于其中存储的键值对均默认为字符串,因此不需要定义泛型。Propertie类的主要用途是以键值对的形式存储应用程序的配置文件,并可以与IO流相结合,实现对配置文件的读写操作。

        那么所谓配置文件就是存储了某个应用程序基本配置信息的文件,比如我们按照自己的喜好和需求设置了Word中文字的字体、大小、颜色等,而在程序运行期间,对属性的改动设置仅仅存储在了内存中,如果希望下次打开Word时还是同样的设置,那么就需要将以上这些配置信息存储到一个硬盘文件——配置文件——中(这也就是所谓的持久化存储),这样一来应用程序每次启动时首先读取该配置文件,按照存储的配置信息将程序设置好,免去了每次都要重新设置属性的麻烦。

        而之所以要通过键值对的形式,是因为应用程序需要识别,存储的配置值究竟是用于设置哪一个属性,因此需要为每一个配置值起一个通俗易懂的名字,也就是键,那么相对应的配置值就是值。既然配置信息存储到了一个文件中,那么对该文件的读取和修改,就是对文件的读写操作,因此需要通过IO流来完成。

1.2  Properties方法介绍

1.2.1 主要方法简介

        在Properties类中,我们将重点介绍以下几个方法,

        public String getProperties(String key):用指定的键在此属性列表中搜索属性。返回指定键对应的值。

        public Object setProperties(String key,String value):调用Hashtable的方法put,将指定的键值对存储到Properties对象中(相当于一个Map集合)。

        publicSet<String> stringPropertyNames():返回此属性列表中的键集。也就是说返回包含有该Properties对象中所有键的Set集合。

        public void list(PrintStream out):将属性列表输出到指定的输出流。该方法可以将Properties对象中存储的键值对(配置信息),通过一个字节打印流对象,写入到与之关联的文件中。

        public voidlist(PrintWriter out):将属性列表输出到指定的输出流。该方法与上面的重载方法相同,区别在于是通过一个字符打印流(而不是字节打印流),将键值对写入到与之关联文件中。

        public voidload(InputStream inStream) throws IOException:从输入流中读取属性列表(键和元素对)。该方法与list方法相对,通过一个字节读取流,将与之关联的文件中的键值对存储到Properties对象中。

        public void load(Reader reader) throws IOException:按简单的面向行的格式从输入字符流中读取属性列表(键和元素对)。该方法与上面的重载方法相同,区别在于通过一个字符读取流,从与之关联的配置文件中读取配置信息,并存储到Properties对象中。

        public void store(OutputStream out, String comments) throws IOException:以适合使用load(InputStream)方法加载到Properties表中的格式,将此Properties表中的属性列表(键和元素对)写入输出流。其中,comments参数,表示对该属性列表的描述。

        public void store(Writer writer, String comments) throws IOException:与上面的重载方法相同,区别是通过一个字符写入流实现,以适合使用load(Reader)方法的格式,将此Properties表中的属性列表(键和元素对)写入到输出流。

1.2.2 方法演示

(1)   设置与获取元素

代码1:

import java.io.*;
import java.util.*;
 
class PropertiesDemo
{
	public static void main(String[] args)
	{
		Properties prop = new Properties();
 
		//向Properties对象中存储若干键值对
		prop.setProperty("David","26");
		prop.setProperty("Kate","19");
		prop.setProperty("Peter","34");
 
		//打印该Properties对象中所有键值对
		System.out.println(prop);
		System.out.println();
 
		//获取指定键对应的值
		String value = prop.getProperty("Kate");
		System.out.println("Kate="+value);
		System.out.println();
             
		//修改指定键对应的值
		prop.setProperty("Peter","29");
             
		//遍历获取Properties对象中所有的键值对
		Set<String> keys = prop.stringPropertyNames();//获取包含有所有键的Set集合
		String key = null, new_value = null;
		for(Iterator<String> it = keys.iterator(); it.hasNext(); )
		{
			key = it.next();//获取键
			new_value = prop.getProperty(key);//通过键获取值
			System.out.println(key+"="+new_value);
		}
	}
}
执行以上代码的结果为:

{Kate=19, David=26, Peter=34}

 

Kate=19

 

Kate=19

David=26

Peter=29

        以上这一部分方法较为简单,不再进行过多的描述,需要提一点的是,Properties从Hashtable继承而来的get也可以实现通过指定键获取对应的值,但是需要将其强转为字符串(如果只是打印则不需要),较为麻烦。

(2)   操作硬盘中的配置文件

        通常情况下,配置信息是以配置文件的形式存在于硬盘中的,因此在这一部分内容中我们将演示如何从配置文件中读取配置信息,以及向已有的配置文件中写入指定的配置信息。

1)    读取配置文件

        Properties类其实已经对外提供了用于读取配置的方法——load,但是为了弄清该方法的原理,我们首先按照以下思路,手动编写一个读取配置文件的方法。

思路:

        第一步,用一个读取流与一个配置文件进行关联,假设配置文件名为“info.txt”;

        第二步,读取一行键值对数据,然后用“=”将键与值分割为两个字符串;

        第三步,将分别代表键(等号左边的字符串)和值(等号右边的字符串)的字符串存储到Properties对象中。

        由于上例中,仅有对硬盘中文本文件的读取操作,因此读取流选择FileReader,并且希望提高读取效率使用缓冲字符流,代码如下,

代码2:

import java.io.*;
import java.util.*;
import static java.lang.System.out;
 
class PropertiesDemo2
{
	public static void main(String[] args) throws IOException
	{
		Properties prop = new Properties();
 
		//将配置文件与读取流进行关联
		BufferedReader bufr =
			new BufferedReader(new FileReader("E:\\info.txt"));
		
		Stringline = null;
		String[] couple = null;
		while((line = bufr.readLine()) != null)
		{
			//将每一行字符串键值对通过"="分割为两个字符串
			couple = line.split("=");
			//将键值对分别存储到Properties中
			prop.setProperty(couple[0],couple[1]);
		}
 
		out.println(prop);
 
		bufr.close();
	}
}
以上代码的执行结果为:

{Kate=19, David=26, Peter=34}

        虽然以上方法可以实现配置文件的读取,但是每次手动编写方法较为麻烦,因此下面我们来演示通过Properties类对外提供的方法直接读取配置文件,代码如下,

代码3:

import java.io.*;
import java.util.*;
 
class PropertiesDemo3
{
	public static void main(String[] args) throws IOException
	{
		Properties prop = new Properties();
 
		//创建一个文件字节读取流对象,并与一个配置文件进行关联
		FileInputStream fis =
			new FileInputStream("E:\\info.txt");
 
		//将与以上读取流关联起来的配置文件中的键值对读取加载到Properties对象中
		prop.load(fis);
		System.out.println(prop);
	}
}
以上代码的执行结果为:

{Kate=19, David=26, Peter=34}

        通过一个load方法,就简单地将指定配置文件中的配置信息(键值对)加载到了Properties集合对象中,因此在实际开发中我们推荐通过以上方法来读取配置文件。需要大家注意的是,只有符合一定的格式的配置文件,才能被load方法加载其中的配置信息,这一格式就是:“键=值”。

2)    将配置信息写入到配置文件

        能够读取现有配置文件中的配置信息,反过来我们也能够将Properties对象中以键值对形式存储的配置信息,通过写入流写入到文件,或其他目的中,Properties类的有两种方法可以实现该功能——list和store,代码如下所示,

代码4:

import java.io.*;
import java.util.*;
 
class PropertiesDemo4
{
	public static void main(String[] args) throws IOException
	{
		Properties prop = new Properties();
 
		//将水果名和对应价格作为键值对存储到Properties对象中
		prop.setProperty("apple","4.5");
		prop.setProperty("peach","3.6");
		prop.setProperty("pear","5.2");
 
		//通过list方法,将键值对写入到标准输出流——控制台
		prop.list(System.out);
		//通过list方法,将键值对写入到与打印流关联起来的文件中
		PrintWriter pw =
			new PrintWriter(new FileWriter("E:\\info.txt"),true);
		prop.list(pw);//这里也可以传入PrintStream对象
 
		//通过store方法,将键值对写入到与文件关联起来的文件字符写入流中
		FileWriter fw =
			new FileWriter("E:\\info2.txt");
		prop.store(fw,"listingproperties");//这里也可以传入FileStream对象
	}
}
以上代码的执行结果为:

-- listing properties --

apple=4.5

peach=3.6

pear=5.2

        结果表明,list方法可以按照方法内定义好的格式将键值对写入到打印流中。在第二次调用list方法时,我们传入了一个与文件关联起来的字符打印流,文件中的内容与控制太中的打印结果相同。需要注意的是,在创建PrintWriter对象时,必须向其构造方法传入值为true的自动刷新参数——autoFlush,这样才能将键值对写入到指定文件中,这是因为list方法内部仅仅会调用,PrintWriter或者PrintStream对象的println方法,但不调用flush方法,因此不设置为自动刷新将会写入失败。PrintWriter类我们将再后面的内容中进行更为详细的介绍。

        相比list方法,store方法能够更方便地将配置信息(键值对)写入到指定文件,中只需在调用store方法时,传入与该配置文件关联起来的字节流或者字符流即可,而第二个参数——注释信息可以省略。那么通过store方法写入到配置文件中的内容如下:

#listing properties

#Wed Mar 25 17:45:51 CST 2015

apple=4.5

peach=3.6

pear=5.2

可以看到,store方法分别将注释信息和配置文件创建时间写入到了配置文件的第一和第二行,并以“#”开头,这与list方法是有区别的,但是键值对的书写规律是相同的。

        此外,store方法更为常见的应用场景是:首先通过load方法将现有配置文件中的配置信息读取到一个Properties对象中,然后对其中的属性值进行修改后,再通过store方法将修改后的属性信息写回到原配置文件中,下面的代码就是这一过程的实现,

代码5:

import java.io.*;
import java.util.*;
 
class PropertiesDemo5
{
	public static void main(String[] args) throws IOException
	{
		Properties prop = new Properties();
 
		//首先读取一个现有的配置文件
		BufferedInputStream bis =
			new BufferedInputStream(new FileInputStream("E:\\info.txt"));
		prop.load(bis);
 
		//将苹果的价格修改为4.0
		prop.setProperty("apple","4.0");
 
		//通过store方法,将键值对写入到与文件关联起来的文件字符写入流中
		BufferedOutputStream bos =
			new BufferedOutputStream(new FileOutputStream("E:\\info.txt"));
		prop.store(bos,"");//省略了注释信息
	}
}
执行以上代码后,原info.txt文件中的内容由“apple=4.5”,修改为了“apple=4.0”。

        需要提醒大家的是,在以上例程中,为方便演示,均没有进行异常处理,仅仅在主函数中声明抛出异常。

1.3  Properties练习

需求:记录程序运行的次数。如果达到次数限制,给出注册提示,并退出程序。通常为了推广自己的软件,软件商家会允许用户进行一定程度的体验,这种体验通常会附带一些限制,要么限制一些功能,要么限制使用时长,抑或者限制使用次数,并希望有兴趣的用户购买注册码,以便继续使用。

思路:首先想到定义一个变量,用以记录程序运行的次数——程序每运行一次,该变量就自增一次。但是,如果该变量仅是程序在运行时存在并进行计数是起不到真正的效果的,因为计数变量会随着程序的退出而消失,再次执行程序将会继续从0开始计数。此时,就需要一个配置文件,在程序退出以前,将计数变量以键值对的形式存储到配置文件中(变量名为键,运行次数为值),程序再次执行时首先读取该配置文件,将计数变量值恢复到上次运行时的次数。若要实现这一功能,就需要Properties类与IO技术相结合。

代码:

代码6:

import java.io.*;
import java.util.*;
 
class RunCount
{
	public static void main(String[] args) throws IOException
	{
		Properties prop = new Properties();
 
		Fileconfig File = new File("D:\\count.ini");
		/*
			程序初次运行时,配置文件不应该存在
			因此需要进行存在性判断,然后创建一个空的配置文件
		*/
		if(!configFile.exists())
			configFile.createNewFile();
 
		FileInputStream fis = new FileInputStream(configFile);
 
		/*
			将配置文件加载Properties对象中
			第一次运行时,配置文件中不包含任何属性
		*/
		prop.load(fis);
 
		//定义计数变量
		int count = 0;
		//获取到表示次数的“times”属性对应的值
		String value = prop.getProperty("times");
 
		//若value不为空,表示程序曾经运行过
		//因此将value转换为int值,并赋给count
		if(value!= null)
		{    
			count = Integer.parseInt(value);
			//当次数达到5次后,提示用户付费,并退出程序
			if(count>= 5)
			{
				System.out.println("达到体现次数,请购买正式版!");
				return ;
			}
		}
 
		/*
			无论这是程序第几次运行,都令count自增一次
			表示程序运行了一次
		*/
		count++;
 
		//将自增后的键值对存储到Properties对象中
		prop.setProperty("times",String.valueOf(count));
 
		FileOutputStream fos = new FileOutputStream(configFile);
 
		//将修改后的Properties属性写入到配置文件中
		prop.store(fos,"");
 
		fis.close();
		fos.close();
	}
}
重复执行以上代码5此,第六次执行时,将会在控制台中显示“达到体现次数,请购买正式版!”,表示达到体验次数,随后直接退出程序。

        以上代码中配置文件的扩展名为“.ini”,也可以将扩展名定义为“.properties”,他们都是表示配置文件的扩展名。除此以外,还有一种配置文件格式为“xml”,这种配置文件能够存储较为复杂的配置信息,而不仅仅是“.properties”文件中简单的一一对应的键值对。

2 打印流

        打印流包含两种:PrintStrean和PrintWriter,显然前者是字节打印流,后者是字符打印流,这里我们重点介绍其中功能更为强大的字符打印流——PrintWriter。

2.1  PrintStream

        API文档介绍:PrintStream为其他输出流添加了功能,使它们能够方便地打印各种数据表示形式。通过API文档可知,PrintStream是OutputStream的子类,自然就继承了很多write重载方法,但该类最重要的功能是打印——对外提供了大量的print重载方法,用于对各种类型的变量进行原样打印操作。这里之所以强调原样,是因为字节流写入单个字节的方法(write(int b)),虽然需要传递int型参数,但是方法底层依然将其转换为byte型值写入文件中(原因请参考《IO流3:字节流》)。

        此外,打印流还提供了分行打印的println方法,以及按照指定格式打印的printf方法。

构造方法:PrintStream的构造方法主要可以接受三种类型的参数对象,

(1) File对象:

        public PrintStream(File file) throws IOException:创建具有指定文件且不带自动行刷新的新打印流。该构造方法的参数为一个File对象,可以对指定的文件进行操作。此外,这里还提到“自动行刷新”,我们将再稍后的内容中介绍这一功能。

        public PrintStream(File file, String csn) throws IOException:创建具有指定文件名和字符集且不带自动行刷新的新打印流。该方法与前者不同之处在于可以指定编码表。

(2)字节写入流:

        public PrintStream(OutputStream out) throws IOException:通过传递一个字节写入流创建新的打印流。此流将不会自动刷新。

        public PrintStream(OutputStream out, boolean autoFlush):创建新的打印流。第二个布尔型参数,用于设置是否开启“行自动刷新”,若为true,则每当写入byte数组、调用println方法或写入换行符或字节(’\n’)时都会刷新输出缓冲区,而不必手动调用flush方法。

第三个可接受字节写入流的构造方法与以上两个的区别仅在于,可以指定编码表,因此不再赘述。

(3) 字符串路径:

        public PrintStream(String fileName) throws IOExceptioni:创建具有指定文件且不带自动行刷新的新打印流。该构造方法与第一个类似,只不过这里的参数是表示文件路径的字符串。

        第二个可接受字节写入流的构造方法与以上方法的区别仅在于,可以指定编码表,同样不再赘述。

        方法:由于字节打印流的父类中包含OutputStream,因此同样继承了write重载方法,这里不再重复介绍,而其最具特色的方法就是大量的print、println和printf重载方法,由于方法较为简单这里不再引述API文档。

2.2  PrintWriter

        API文档介绍:向文本输出流打印对象的格式化表示形式。此类实现在PrintStream中的所有print方法。它不包含用于写入原始字节的方法,对于这些字节,程序应该使用未编码的字节流进行写入。PrintWriter相对于PrintStream而言更为常用一些,尤其在Web开发方面的应用更为常见。

        构造方法:PrintWriter的构造方法基本与PrintStream相同,只是多了一种可接受的参数类型——Writer,这也是PrintWriter更为常用的原因之一,

        public PrintWriter(Writer out):创建不带自动刷新的新PrintWriter。

        public PrintWriter(Writer out, boolean autoFlush()):创建新PrintWriter。第二个布尔型参数,用于设置是否开启“自动行刷新”,若为true,那么println、printf和format方法将自动刷新输出缓冲区。

        方法:与PrintStream对应,PrintWriter的父类是Writer,因此同样继承了其父类的write方法,包括直接写入字符串的write重载形式,除此以外与PrintStream基本一致,这里不再赘述。

2.3 打印流方法演示

        由于在实际开发中PrintWriter更为常用,因此这一部分,我们仅对PrintWriter的使用进行演示。

2.3.1 将键盘录入内容打印到控制台

代码7:

import java.io.*;
 
class PrintStreamDemo
{
	public static void main(String[] args) throws IOException
	{
		//将键盘录入的内容,通过PrintWriter打印到控制台
		BufferedReader bufr =
			new BufferedReader(newInputStreamReader(System.in));
 
		//PrintWriter构造方法可以直接接受字节写入流
		PrintWriter pw =
			new PrintWriter(System.out);
			//new PrintWriter(System.out, true);
 
		//常规的循环写入操作
		String line = null;
		while((line= bufr.readLine()) != null)
		{
			//定义结束标记
			if("over".equals(line))
				break;
 
			//将打印文本大写显示,以示区分
			pw.println(line.toUpperCase());
			//为实时显示键盘录入的内容,每写入一行文本必须进行刷新
			//若通过被注释的构造方法创建PrintWriter对象,则不必手动调用flush方法
			pw.flush();
		}
 
		pw.close();
		bufr.close();
	}
}
执行以上结果,就可以在控制台不断键入文本,并实时将键入内容显示在控制台中。

代码说明:

        (1)  PrintWriter的构造方法可以直接接受一个字节写入流对象,这样可以更为方便地将文本内容打印到控制台,而免去了通过OutputStreamWriter作为中转的麻烦。PrintWriter之所以可以这样做是因为,其构造方法在底层自动创建了一个OutputStreamWriter对象,并用该对象封装了传入的字节写入流对象。

(        2)  在while循环内,我们同样可以通过write方法直接将字符串写入到目的中。但是由于PrintWriter类并不具备newLine方法,因此若要实现换行写入就必须手动在每个字符串后面追加换行符,比较麻烦,此时就体现了打印流println方法的优点了——自动实现换行写入文本内容。

        (3)  若通过以上代码中被注释的构造方法创建PrintWriter对象,也即传递一个值为true的布尔型变量,将“自动行刷新”功能激活,那么调用println、print和format方法写入文本内容时,不需要手动调用flush方法,即可自动实现刷新,进一步提高了文本写入的便利性。

2.3.2 将键盘录入内容写入到文件

代码8:

import java.io.*;
 
class PrintStreamDemo2
{
	public static void main(String[] args) throws IOException
	{
		BufferedReader bufr =
			new BufferedReader(newInputStreamReader(System.in));
 
		PrintWriter pw =
			new PrintWriter(newFileWriter("D:\\dest.txt"),true);
 
		String line = null;
		while((line= bufr.readLine()) != null)
		{
			if("over".equals(line))
				break;
 
			//直接写入文本内容,并自动刷新写入目的文件
			pw.println(line.toUpperCase());
		}
 
		pw.close();
		bufr.close();
	}
}
        将表示文件路径的字符串封装进字符写入流对象后,与“自动行刷新”标记同时传递至PrintWriter构造方法,可以实现将键盘录入的文本内容自动刷新写入到目的文件中。当然,也可以直接将表示文件路径的字符串作为参数传递至PrintWriter构造方法,但是无法支持自动行刷新功能。

        如果需要提高写入效率,可以将FileWriter进一步封装至BufferedWriter对象内,再一并传递至PrintWriter构造方法。

3 SequenceInputStream——序列流

3.1 序列流概述

API文档描述:SequenceInputStream表示其他输入流的逻辑串联。它从输入流的有序集合开始,并从第一个输入流开始读取,直到到达文件末尾,接着从第二个输入流读取,以此类推,直到到达包含的最后一个输入流的文件末尾为止。简单一句话就是:SequenceInputStream可以将多个读取流拼成一个读取流。

        我们以字节文件读取流为例对以上描述进行说明。设想这样的应用场景,比如硬盘中存在两个文本文件,现在需要将这两个文本文件拼接起来。如果我们不使用序列流,那么首先需要两个读取流封装两个源文件,再通过一个写入流将目的文件封装起来。然后通过第一个读取流读取文本内容,并写入到目的文件中;接着第二个读取流重复这一过程,但需要注意的是,封装目的文件的写入流必须要实现写入内容的追加,否则写入第二个文件时,将会把之前写入的内容覆盖掉。显然这一过程比较麻烦。

        与以上过程相对,如果使用序列流将两个读取流都封装起来,然后控制该序列流对象,自动地先后读取两个文件,写入到同一个目的文件中,将会非常方便。在这一过程中,序列流将会忽略第一个文件的结束标记,并继续读取下一个文件,直到读取到第二个文件的结束标记才结束。   

构造方法:

        public SequenceInputStream(InputStream s1, InputStream s2):通过记住这两个参数来初始化新创建的SequenceInputStream(将按顺序读取这两个参数,先读取s1,然后读取s2),以提供从此SequenceInputStream读取的字节。该读取流的构造方法需要接收两个字节读取流对象,这也就体现了该读取流可以将其他读取流拼接起来的特点。

        pubic SequenceInputStream(Enumeration<?extends InputStream> e):前一个构造方法只能拼接两个字节读取流,具有一定的局限性,如果需要拼接多个读取流,就可以使用本构造方法——通过一个包含多个字节读取流的迭代器对象(Enumeration就是JDK早期版本的迭代器,因为序列流在JDK1.0版本时就出现了)来初始化序列流对象。

方法:

        序列流的直接父类就是InputStream,因此对外提供的方法与一般的字节流是想同的,主要就是两个read重载方法,一个是写入单个字节,另一个是写入字节数组(也即字节缓冲)。除此以外,调用序列流的close方法,将关闭封装其内部的读取流全部关闭。因为前面的内容详细介绍过这些方法,这里不再作详细说明。

        需要提醒大家的是,序列流的后缀是InputStream,表明是一种字节读取流,但是却并没有对应的字节写入流,也就是说并不存在SequenceOutputStream。

3.2 序列流应用演示

需求:现有三个“.txt”文件,分别为“1.txt”、“2.txt”和“3.txt”,其中的内容是重复的数字,比如“1.txt”中就是“1111111111”(共10个),“2.txt”中是“2222222222”等等。需要将这三个文件合并为一个文件。最终的效果为:

1111111111

2222222222

3333333333

思路:

        (1)  创建三个字节文件读取流将三个文本文件封装起来,再创建一个字节写入流对象作为写入目的;

        (2)  创建一个SequenceInputStream对象将以上三个读取流封装起来;

        (3)  操作该序列流对象,通过常规的读取循环,将三个文件中的内容,写入到目的文件中。若需要提高读写效率,可以使用缓冲字节流,分别封装序列流和写入流。

代码:

代码9:

import java.io.*;
importjava.util.*;
 
class SequenceDemo
{
	publicstatic void main(String[] args) throws IOException
	{
		Vector<FileInputStream> v = new Vector<FileInputStream>();
 
		for(int x=0; x<3; x++)
		{
			v.add(new FileInputStream("F:\\"+(x+1)+".txt"));
		}
 
		Enumeration<FileInputStream> enu= v.elements();
 
		//创建SequenceInputStream对象,相当于源
		SequenceInputStream sis = new SequenceInputStream(enu);
		//创建字节文件写入流对象,并与目的文件关联起来
		FileOutputStream fos = new FileOutputStream("F:\\ 123.txt");
 
		int len= 0;
		byte[] buf = new byte[1024];
		while((len= sis.read(buf)) != -1)
		{
			fos.write(buf,0,len);
		}
 
		//关闭序列流,同时将封装其内的三个读取流全部关闭
		sis.close();
		fos.close();
	}
}
执行以上代码,就可以将原本分开存在三个文本文件合并为一个文件。

3.3 切割文件

        既然可以将多个文件合并为一个,我们也可以将一个文件分割为若干文件。比如,在论坛发帖时,我们可以附带上传一些文件,但是可能会规定单个文件的大小不能超过某个值,因此上传之前需要将文件通过WinRAR等压缩工具进行分卷压缩后才能实现上传。

        虽然我们有这样的需求,但是Java标准类库中并没有单独提供专门用于分割文件的工具类,但是我们可以利用之前介绍的IO流类实现这一功能。

        其实,实现这一功能的原理非常简单,假设现有一个5Mb大小的图片,希望将其等分为5份,每份大小1Mb,我们只需创建一个1Mb大小的字节数组(缓存),然后进行5次for循环,每次循环时首先创建一个写入流对象,并与一个子文件关联(也就是说,需要分割成几个子文件,就创建几个写入流对象),然后通过读取流将将源文件中的内容写入到字节数组中,直到将字节数组填满,最后把字节数组中的内容再通过刚刚创建的写入流对象写入到子文件中,重复这一过程,每次循环都新创建一个写入流对象,实现了对文件的分割操作。

        以上原理的代码实现如下,

代码10:

import java.io.*;
importjava.util.*;
 
class SplitFile
{
	public static void main(String[] args)throws IOException
	{
		FileInputStream fis= new FileInputStream("F:\\ source.jpg");
		FileOutputStream fos= null;
 
		//创建大小为500Kb的字节缓冲
		byte[] buf = new byte[1024 * 500];
		int len = 0, count = 1;//count表示子文件序列
 
		while((len= fis.read(buf)) != -1)
		{
			//循环创建序号连续的子文件
			fos = new FileOutputStream("F:\\ source~"+(count++)+".part");
			fos.write(buf,0,len);
			//每创建一个写入流对象,并完成写入操作后,就要关流
			fos.close();
		}
 
		fis.close();
	}
}
执行以上代码,就可以将某个文件分割为指定大小的子文件。

        需要注意的是,分割图片后产生的子文件(或称为碎片文件)的文件格式不能定义为原文件格式(比如,原图片为“.jpg”格式),因为这样做用户就有可能双击打开它,但是由于文件的不完整,将会造成打开失败。

        下面我们将分割与合并结合起来,代码如下,

代码11:

import java.io.*;
importjava.util.*;
 
class Split_Merge
{
	public static void main(String[] args) throws IOException
	{
		split();//分割源文件
		merge();//合并子文件
	}
	public static void split()throws IOException
	{
		FileInputStream fis = new FileInputStream("F:\\ source.jpg");
		FileOutputStream fos = null;
 
		byte[] buf = new byte[1024 * 500];
		int len = 0, count = 1;
 
		while((len = fis.read(buf)) != -1)
		{
			fos = new FileOutputStream("F:\\ source~"+(count++)+".part");
			fos.write(buf,0,len);
			fos.close();
		}
 
		fis.close();
	}
	public static void merge()throws IOException
	{
		//为提高效率使用ArrayList(线程不安全),而不再使用Vector(线程安全)
		ArrayList<FileInputStream> al = new ArrayList<FileInputStream>();
 
		for(int x = 0; x<5; x++)
		{
			al.add(newFileInputStream("F:\\ source~"+(x+1)+".part"));
		}
 
		//由于需要被内部类对象访问,因此作为局部变量应被final修饰
		final Iterator<FileInputStream> it = al.iterator();
		//创建Enumeration接口的匿名子类对象
		Enumeration<FileInputStream> en = new Enumeration<FileInputStream>(){
			public boolean hasMoreElements()
			{
				return it.hasNext();
			}
			public FileInputStream nextElement()
			{
				return it.next();
			}
		};
 
		SequenceInputStream sis = new SequenceInputStream(en);
		FileOutputStream fos = new FileOutputStream("F:\\ new_source.jpg");
 
		int len = 0;
		byte[] buf = new byte[1024];
		while((len = sis.read(buf)) != -1)
		{
			fos.write(buf,0,len);
		}
 
		//关闭序列流,同时将封装其内的三个读取流全部关闭
		sis.close();
		fos.close();
	}
}
执行以上代码后,将会创建5个子文件和一个与原文件相同的新文件。

代码说明:

        (1)  我们知道Vector集合为了保证线程安全,因此其存取效率较低,为了提高效率,我们使用了ArrayList集合来存储与子文件关联起来的FileInputStream对象。但是,ArrayList集合并不能提供Enumeration迭代器,因此我们在以上代码中,创建了Enumeration接口的匿名子类对象。既然是匿名内部类,就需要复写接口的方法,但是既然Iterator的作用于之相似,就没有必要重新定义方法体,只需返回Iterator对象相同方法的返回值即可。另外,内部类若要方法局部变量(上例中是指向Iterator对象的变量),该变量需要被final修饰。

        (2)  如果需要分割一个较大的文件,比如将500Mb大小的视频文件等分为5份,千万不要将字节缓冲大小定义为100Mb,否则会内存溢出。正确的做法是,将视频文件通过File对象封装起来,获取其大小,然后根据该大小决定每个子文件的大小,比如100Mb,那么就要分割为5个子文件。接着,开启第一层循环,每个循环内创建一个与子文件关联的写入流对象,接着开启第二层循环,在该层循环中,使用1Mb大小的缓冲,向目的文件中写入数据,每写入一次就就记一次数,达到100次后,表示创建了一个100Mb大小的子文件,此时跳出内层循环,继续进行下一轮外层循环——创建第二个子文件,并重复以上动作,最后就可以创建5个100Mb大小的子文件了,参考代码如下,

代码12:

import java.io.*;
importjava.util.*;
 
class SplitVideo
{
	public static void main(String[] args) throws IOException
	{
		File video = new File("F:\\vedio.mp4");
 
		BufferedInputStream bis =
			new BufferedInputStream(newFileInputStream(video));
		BufferedOutputStream bos = null;
 
		//需要分割为几个子文件,就进行几次外层循环
		for(int x = 0; x < 6; x++)
		{
			//每次循环都创建一个新的写入流对象,并与一个子文件关联
			bos =
				new BufferedOutputStream(newFileOutputStream("F:\\video~"+(x+1)+".part"));
 
			int len= 0, count = 1;
			byte[] buf = new byte[1024 * 1024];
			while((len = bis.read(buf)) != -1)
			{
				//写入第100个1Mb数据后跳出循环
				if(count> 99)
					break;
 
				bos.write(buf,0,len);
 
				count++;
			}
 
			//由于第100个1Mb数据并未写入文件中
			//因此将这一部分数据写入到文件中,以保证文件的完整性
			if(len != -1)
				bos.write(buf);
 
			 bos.close();
		}
 
		bis.close();
	}
}

4 对象字节流——ObjectInputStream 和 ObjectOutputStream

4.1 对象字节流概述

ObjectInputStream类API文档介绍:ObjectInputStream对以前使用ObjectOutputStream写入的基本数据和对象进行反序列化。所谓序列化其实就是我们之前提到的数据持久化,换句话说,就是将内存中的数据存储到能够长久存储的介质中,比如硬盘文件;那么相应的反序列化,就是将硬盘文件中存在的数据读取到内存中,并对其进行操作。

ObjectOutputStream类API文档介绍:ObjectOutputStream将Java对象的基本数据类型和图形写入OutputStream。可以使用ObjectInputStream读取(重构)对象。

从这两个类的类名后缀可知,它们均属于字节读写流,而类名前缀表明它们是用于对对象进行操作的IO流。我们都知道,对象都是存在于堆内存中的,一旦这些对象不再被使用,都将被作为垃圾回收而消失,而如果希望创建某些特定对象后,即使关闭程序,下次还能够访问这一特定对象中的数据时,就需要对这些对象进行持久化存储(也称为序列化、可串行性),那么对象字节流的主要功能,就是将内存中的对象写入到硬盘文件中,并通过读取该文件再次获取到特定对象。

构造方法:

ObjectOutputStream:

        public ObjectOutputStream(OutStream out) throws IOException:创建写入指定OutputStream的ObjectOutputStream。该构造方法表明,需要在创建对象字节写入流时,为其初始化一个与硬盘文件关联起来的字节写入流对象(比如FileOutputStream),换句话说,需要为其初始化一个写入目的。

ObjectInputStream:

        public ObjectInpuStream(InputStream in) throws IOException:创建从指定InputStream读取的ObjectInputStream。与对象字节写入流相对应的,创建对象字节读取流也需要为其初始化一个字节读取流对象。

方法:

ObjectOutputStream:

        该类继承了OutputStream,因此常规的字节流方法也都是具备的,这里不再赘述,主要介绍其特有方法——writeObject(Object obj)。

        public final void writeObject(Object obj) throws IOException:将指定的对象写入ObjectOutputStream。该方法在每个被写入到文件中的对象数据后面都添加分隔标记,用于分隔同一文件中的不同对象,方便对象字节读取流读取这些对象。

ObjectInputStream:

        与对象字节写入流相对应,具备特有的读取对象的方法——readObject()。

        public final ObjectreadObject() throws IOException:从ObjectInputStream读取对象。注意到该方法的返回值类型为Object,也就是说无论向文件中写入了什么类型的对象,通过对象字节读取流读取到的都根父类Object对象,因此读取对象时,必须要进行向下转型为原类型对象。该方法通过分隔标记来区分不同对象,每调用一次readObject方法读取到一个对象后,指针就会自定指向下一个对象,这样就可以按照写入顺序,依次读取对象。

        除了写入和读取对象的方法以外,对象字节流类还提供了写入/读取基本数据类型的write和read方法。对于写入方法需要注意区分的是,write方法和writerInt方法,前者只写入传递的32位整型值后低八位(也就是一个字节),而后者是将全部32位全部写入。由于java.io包中其他专门用于操作基本数据类型的IO流类,因此对象字节流操作基本数据类型的方法不作详细介绍。

 

小知识点1:

        其实,对象字节流的读写操作,都是基于基本的字节写入流的读写方法(正如构造方法中需要为其初始化一个字节写入流对象),是在对字节的操作基础上衍生出对一些特殊数据的操作,那么对象字节流就是操作对象,还有专门用于操作数组和基本数据类型的IO流类。

 

4.2 对象字节流应用演示

        我们按照先写入对象,后读取的顺序对对象字节流进行演示,为演示方便以下例程中不进行异常处理。

4.2.1 写入对象

代码13:

import java.io.*;
 
//自定义一个类
class Person
{
	private String name;
	private int age;
	Person(String name, int age)
	{
		this.name = name;
	}
 
	public String toString()
	{
		return name+":"+age;
	}
}
class ObjectStreamDemo
{
	public static void main(String[] args) throws IOException
	{
		writeObj();
	}
	//为演示方便将写入对象操作封装为一个方法
	public static void writeObj() throws IOException
	{
		//创建对象字节写入流对象,并为其初始化写入目的
		ObjectOutputStream oos =
			new ObjectOutputStream(newFileOutputStream("E:\\ object.objects"));
 
		//将Person对象写入到硬盘文件中
		oos.writeObject(newPerson("Tom", 28));
 
		oos.close();
	}
}
代码说明:

        (1)  在创建对象字节写入流时需要为其初始化一个写入目的,而构造方法告诉我们需要传递一个字节写入流对象,由于原本存在于内存中的对象并非是纯文本,而是以二进制数(也可以理解为字节)的形式存在于内存中,并且需要将对象写入到硬盘文件中,因此写入目的应该是文件字节写入流——FileOutputStream。

        (2)  注意到,写入目的文件的后缀名为“objects”,这并不是什么特定的文件格式,而是为了防止用户双击打开。可能有的朋友下意识的将文件后缀名定义为“.txt”文件,但是原本存在堆内存中的对象,转换为字节并写入到文件中后,由于其中的字节组合并不能通过任何编码表进行读取,因此若双击打开,就会出现“乱码”,为了防止用户的误操作,可将写入目的文件后缀定义为非文本文件格式。

        以上代码能够通过编译,并且执行后在指定目录中创建了写入目的文件“object.objects”,但是同时提示了以下异常信息:

Exception in thread "main"java.io.NotSerializableException:Person

atjava.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1181)

atjava.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:347)

at ObjectStreamDemo.writeObj(ObjectStreamDemo.java:31)

at ObjectStreamDemo.main(ObjectStreamDemo.java:22)

        那么这里抛出的异常名称叫做“NotSerializableException”,我们查阅writeObject方法,在“抛出:”一栏中就有“NotSerializableException”——某个要序列化的对象不能实现java.io.Serializable接口,也就是说,若果某个对象想要实现序列化,就必须要实现一个叫做Serializable的接口。

        那么我们就通过API文档来了解这个接口,Serializable接口API文档描述为:类通过实现java.io.Serializable接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。换句话说,我们自定义的类只有使其实现Serializable接口,才能通过ObjectOutputStream流将其实例对象写入到硬盘文件中。

        此外,我们都知道,实现接口的类通常还要覆盖接口中的抽象方法,但是Serializable接口的API文档中并没有任何方法摘要,那么这一类没有定义任何抽象方法的接口称为标记接口。可以理解为,通过这个接口给实现它的类盖了一个戳,有了这个戳该类就可以被序列化。这个戳的表型形式是一个静态最终长整型变量(static final long)——serialVersionUID,所有实现Serializable接口的类都需要定义该变量,这就相当于一个类的ID号。

        那么这个ID号的作用是就是用来识别一个被反序列化的对象。比如,我们通过类A创建了一个对象,并将其写入到一个文件中(实现序列化)。随后,我们对类A的源代码进行了修改,然后对其进行编译,我们将修改后的类称为类B,此时类A就不存在了。如果从前述文件中读取对象,并用类B引用型变量去指向这个对象,并去调用类B的方法就会出现问题——因为变量所属类(类B)中的成员可能是对象所属类(类A)所不具备的。而前述ID号是通过每个类的成员所拥有的数字标识计算得来的,成员发生变化相应的类ID号也就随之变化。那么在读取对象时,就通过这个ID来判断能否通过指定类型的引用变量去指向被读取对象。

        接下来,我们就令代码7中的Person类实现Serializable接口,如下所示,

class Person implements Serializable

然后同样进行编译和运行,不再提示任何异常,并在指定路径下创建了“object.objects”文件。

4.2.2 读取对象

代码14:

import java.io.*;
 
//自定义一个类
class Person implements Serializable
{
	private String name;
	private int age;
	Person(String name, int age)
	{
		this.name = name;
		this.age = age;
	}
 
	public String toString()
	{
		return name+":"+age;
	}
}
class ObjectStreamDemo
{
	public static void main(String[] args) throws Exception
	{
		writeObj();
		readObj();
	}
	//为演示方便将写入对象操作封装为一个方法
	public static void writeObj() throws IOException
	{
		//创建对象字节写入流对象,并为其初始化写入目的
		ObjectOutputStream oos =
			new ObjectOutputStream(newFileOutputStream("E:\\object.objects"));
 
		//将Person对象写入到硬盘文件中
		oos.writeObject(newPerson("Tom", 28));
 
		oos.close();
	}
	public static void readObj() throws Exception//将读取对象的操作封装为一个方法
	{
		ObjectInputStream ois =
			new ObjectInputStream(newFileInputStream("E:\\object.objects"));
 
		//读取对象的同时,进行强制类型转换——向下转型
		Person p = (Person)ois.readObject();
 
		System.out.println(p);
 
		ois.close();
	}
}
执行以上代码的结果为:

Tom:28

        通过ObjectInputStream的readObject方法,从指定文件中读取到了Person对象。

代码说明:

        (1)  注意到,以上代码中main方法和readObj方法均声明抛出了Exception异常,而不仅仅是IOException,这是因为readObject方法会抛出“ClassNotFoundException”(该异常类并不是Exception的子类),意思是类不存在异常,之所以会抛出该异常是因为与对象字节读取流关联起来的文件中可能并没有存储任何对象,那么调用readObject方法就会出现问题——无对象可返回。

        (2)  如前所述,ObjectInputStream类readObject方法的返回值类型为Object,因此必须对其进行向下转型。

4.2.3 修改类

        在以上例程中,读取文件中对象前后,并没有对Person类进行任何修改,因此可以顺利的将Person对象读取出来。那么如果对象Person类中的成员进行修改,还能否正常读取对象呢?比如,将整型成员变量age的修饰符“public”删除掉(表示对原Person类进行修改),然后直接从前述例程中创建的“object.objects”文件中读取Person(以代码8为例,注释掉writeObj方法的调用语句),将会提示以下异常信息:

Exception in thread "main" java.io.InvalidClassException:Person; local class incompatible: stream classdescserialVersionUID =-7934218187833195549, local class serialVersionUID = -1006590648729977530

at java.io.ObjectStreamClass.initNonProxy(UnknownSource)

at java.io.ObjectInputStream.readNonProxyDesc(UnknownSource)

at java.io.ObjectInputStream.readClassDesc(UnknownSource)

at java.io.ObjectInputStream.readOrdinaryObject(UnknownSource)

atjava.io.ObjectInputStream.readObject0(Unknown Source)

at java.io.ObjectInputStream.readObject(UnknownSource)

at ObjectStreamDemo6.readObj(ObjectStreamDemo6.java:46)

at ObjectStreamDemo6.main(ObjectStreamDemo6.java:26)

以上异常信息的意思为:本地类不匹配,从流中读取到的类ID号,与本地类的ID号是不相同的。正如前所述,文件中Person对象的所属类是修改前的Person,而用来接收Person对象的变量类型是修改后的Person类,修改后Person类的ID号也将随之改变,ID号不同这两个类就不匹配,自然就会抛出异常。如果将“public”修饰符再添加回去,就又可以正常读取对象了。

4.2.4 自定义UID号

        可能有朋友会想,仅仅是修改了一个类的成员变量修饰符,就导致不能通过原有代码进行对象读取操作,显得比较死板。那么解决这一问题的方法就是,自定义UID号。如下代码所示,

代码15:

class Person implements Serializable
{
	//自定义类的UID号
	public static final long serialVersionUID = 42L;//UID值可以随意定义
 
	private String name;
	private int age;
	Person(String name, int age)
	{
		this.name = name;
		this.age = age;
	}
 
	public String toString()
	{
		return name+":"+age;
	}
}
以上代码中,我们在定义Person类的时候,自定义了UID号,那么这个UID号是不会随着类的改变而改变的(在不自定义UID号的情况下,类所拥有的UID号是通过成员的数字标识计算得来)。

        通过以上方式定义Person类,并创建一个Person对象存储到文件中,随后无论如何修改原有Person类,都可以正常读取到Person对象。大家可以自行尝试。

        那么自定义UID号的好处就在于,可以方便地对被序列化的类进行修改的前提下,能够正常读写被操作对象,提高了代码的灵活性。

4.2.5 不能被序列化的两个特例

        这里所指的“不能被序列化”,并不是说类的成员不被写入到文件中,而是说写入到文件中的仅仅是其默认初始化值,也就是说,即使该对象在其他操作中(比如构造方法初始化、类的其他方法体中)对“不能被序列化”的成员变量进行再次赋值,写入到文件中的值还是默认初始化值。

(1)      静态成员不能被序列化

        当类的成员变量被修饰为静态时,是不能被序列化的,这是因为被序列化的内容仅仅是堆内存中对象的特有数据,而不包括静态方法区中类的共享数据。我们在代码8的基础上,做一些修改,来对这个问题进行说明,

代码16:

import java.io.*;
 
class Person implements Serializable
{
	public static final long serialVersionUID = 42L;
 
	private String name;
	private int age;
	static String country = "CN";//定义一个静态成员变量country
	Person(String name, int age, String country)
	{
		this.name = name;
		this.age = age;
		this.country = country;//非静态访问静态
	}
 
	public String toString()
	{
		return name+":"+age+":"+country;
	}
}
class ObjectStreamDemo
{
	public static void main(String[] args) throws Exception
	{
		writeObj();
		readObj();
	}
	public static void writeObj() throws IOException
	{
		ObjectOutputStream oos =
			new ObjectOutputStream(newFileOutputStream("F:\\object.objects"));
 
		oos.writeObject(newPerson("Kate", 23, "USA"));
             
		oos.close();
	}
	public static void readObj() throws Exception
	{
		ObjectInputStream ois =
			new ObjectInputStream(newFileInputStream("F:\\object.objects"));
 
		Person p = (Person)ois.readObject();
		System.out.println(p.toString());
	}
}
执行以上代码时,先将主函数中的readObj方法注释掉,仅执行写入对象操作;然后再将writeObj方法注释掉,单独执行读取对象操作,执行结果为:

Kate:23:CN

        结果表明,writeObj方法中创建Person对象时,传入Person构造方法表示country的实际参数“USA”,并未真正被序列化,换句话说,并未真正被写入到文件中,因为读取到的Person对象,其静态变量country保持了加载类时,为其初始化的值——“CN”。

        那么之所以会造成上述原因正像前文所述那样,通过ObjectOutputStream的方法writeObject,写入到文件中的均是堆内存中对象的特有数据,但不包括对象所属类的共享数据——静态成员变量。这里指的“不包括”并不是说写入到文件中的对象将失去静态成员变量,而是说其值将保持初始化值,也就是说,假如不对country进行指定的初始化(为其赋值“CN”),那么读取对象时,打印的country值为“null”。

(2)    被transient修饰的成员变量不能被序列化

        如果我们希望某个类中的非静态成员也不要被序列化时,可以将其修饰为transient(瞬态)。例如在代码9的基础上,将成员变量“age”修饰为transient,如下代码所示,

代码17:

class Person implements Serializable
{
	public static final long serialVersionUID = 42L;
 
	private String name;
	private transient int age;//将age修饰为transient
	static String country;//不对静态成员变量country进行指定的默认初始化
	Person(String name, int age, String country)
	{
		this.name = name;
		this.age = age;
		this.country= country;
	}
 
	public String toString()
	{
		return name+":"+age+":"+country;
	}
}
在以上代码的基础上,按照代码10的顺序,先将对象写入到文件中,然后再读取,打印到控制台上的结果为:

Kate:0:null

        结果表名,被transient修饰的非静态成员变量,写入到文件中的值就是其默认初始化值0,同样静态成员变量country,由于也没有指定默认初始化值,因此其值为null。

5 管道流——PipedInputStream & PipedOutputStream

        在前面的内容中,当我们通过IO流实现对数据的读取和写入操作时,写入流在将读取流读取到的数据写入到目的(硬盘文件、控制台或内存)之前,通常都要将待写入数据通过一个变量或者一个缓冲存储起来,也就是说写入和读取之间需要一个中转的过程。而管道流的特点就是不需要中转——因为管道读取流就是管道写入流的写入目的。

5.1 管道流概述

API文档:

        PipedInputStream:管道输入流应该连接到管道输出流;管道输入流提供要写入到管道输出流的所有数据字节。通常,数据由某个线程从PipedInputStream对象读取,并由其他线程将其写入到相应的PipedOutputStream。不建议对这两个对象尝试使用单个线程,因为这样可能死锁线程。

        PipedOutputStream:可以将管道输出流连接到管道输入流来创建通信管道。管道输出流是管道的发送端。通常,数据由某个线程写入PipedOutputStream对象,并由其他线程从连接的PipedInputStream读取。不建议对这两个对象尝试使用单个线程,因为这可能会造成该线程死锁。

        在以上管道读取/写入流的API文档描述中,均提到了不要对两个对象使用单线程。我们都知道,读取流如果没有读取到数据那么就会进入到阻塞状态(阻塞式方法),也就是说,将一直等待数据的输入。如果管道写入流和管道读取流同属一个线程,并且读取流先执行的情况下,如果某个时刻没有数据输入,那么这个线程将一直处于等待状态——后续的读取命令也将无法执行,实际也就是死锁,也就无法实现读取数据的同时向目的写入数据的特色功能。

构造方法:

PipedInputStream:

        public PipedInputStream(PipedOuputStream src) throws IOExceptioin:创建PipedInputStream,使其连接到管道输出流src。写入src的数据字节可用作此流的输入。由于管道读取流就是管道写入流的写入目的,因此在创建管道读取流的同时为其初始化一个管道写入流对象,那么这一操作的底层就实现了读取流和写入流的连接动作。

PipedOutputStream:

        public PipedOutputStream(PipedInputStream snk) throws IOException:创建连接到指定管道流的管道输出流。写入此流的数据字节稍后将用作snk的输入。与管道读取流类似,在创建管道写入流的同时,为其初始化一个管道读取流对象,亦可实现读取流和写入流的连接。

方法:

PipedInputStream:

        public void connect(PipedOuputStream src) throws IOException:使此管道输入流连接到管道输出流src。如果此对象已经连接到其他某个管道输出流,则抛出IOException。若没有通过构造方法实现两管道读取/写入流的连接,也可在随后的代码中通过connect方法完成连接动作。

PipedOutputStream:

        public void connect(PipedInputStream snk) throws IOException:将此管道输出流连接到接收者(管道读取流)。功能与PipedInputStream的connect相同,不再赘述。

        那么通过以上对管道读取流和写入流的介绍可知,之所以将两个流连接起来时能够同时实现读取和写入,并能够避免死锁,就是因为读取流和写入流被两个线程操作,即使读取流等待,也不会妨碍写入流写入数据。

 

小知识点1:

        其实大家可以将读取流(无论字节读取流还是字符读取流)的read方法,理解为被另一个单独的线程(假设由主线程对读取流对象进行操作)所执行,为方便说明称其为读取线程。以键盘录入为例,当我们没有键入任何内容时,读取线程就相当于被“wait”而进入冻结状态,一旦有数据输入,就会被“notify”并将读取的数据存储起来。

 

5.2 管道流应用演示

        管道流的基本使用方法是:首先定义两个实现了Runnable接口的类,分别表示读取线程和写入线程,并分别通过构造方法为其初始化管道读取流对象和管道写入流对象;读取线程类的run方法中,调用管道读取流对象的read方法,获取来自管道写入流写入的数据,写入线程run方法中,调用管道写入流对象的write方法,向管道读取流写入数据。这里需要注意的是,因为管道流属于字节流,因此若要操作字符,必须将其转换为字节。主函数中,分别创建管道读取/写入流对象,然后或通过构造方法,或者通过connect方法将两者连接起来。随后,创建写入和读取线程对象,分别为其初始化管道写入和读取流对象,调用start方法启动两个线程。具体代码实现如下,

代码18:

import java.io.*;
 
//定义读取线程
classRead implements Runnable
{
	private PipedInputStream pis;
	Read(PipedInputStream pis)
	{
		this.pis = pis;
	}
	public void run()
	{
		try
		{
			byte[] buf = new byte[1024];
			//读取来自管道写入流的数据
			int len = pis.read(buf);
 
			System.out.println(newString(buf,0,len));
		}
		catch (IOException e)
		{
			throw new RuntimeException("管道读取流读取失败!");
		}
		finally
		{
			try
			{
				if(pis != null)
					pis.close();
			}
			catch (IOException e)
			{
				throw new RuntimeException("管道读取流关闭失败!");
			}
		}
	}
}
//定义写入线程
class Write implements Runnable
{
	private PipedOutputStream pos;
	Write(PipedOutputStream pos)
	{
		this.pos = pos;
	}
	public void run()
	{
		try
		{
			//将字符串按照默认的编码表转换为字节,并写入到管道读取流中
			pos.write("管道流测试".getBytes());
		}
		catch (IOException e)
		{
			throw new RuntimeException("管道写入流写入失败!");
		}
		finally
		{
			try
			{
				if(pos != null)
					pos.close();
			}
			catch (IOException e)
			{
				throw new RuntimeException("管道写入流关闭失败!");
			}
		}
	}
}
class PipedStreamDemo
{
	public static void main(String[] args) throws IOException
	{
		//创建管道读取/写入流,并完成两者的连接
		PipedInputStream in = newPipedInputStream();
		PipedOutputStream out = newPipedOutputStream();
		in.connect(out);
 
		Read r = new Read(in);
		Write w = new Write(out);
		new Thread(r).start();
		new Thread(w).start();
	}
}
以上代码的执行结果为:

管道流测试

代码说明:

        (1)  run方法中我们对read和write方法分别进行了try-catch异常处理,而并没有像之前的代码那样简单地在方法上声明抛出异常。这是因为异常处理规则中,子类继承父类时,子类方法不能抛出父类没有抛出的异常,也就是说,Runnable接口的run方法并没有抛出任何异常,那么子类复写该方法时,并可能发生异常时,只能通过try-catch处理。

        (2)  以上代码中,我们先后创建并启动了读取线程和写入线程,但执行权的分配是由CPU决定的。假设写入线程先执行,向读取流写入数据,随后读取线程获取到执行权,读取写入流写入的数据,并打印到控制台。当然也有可能读取线程先获取到执行权,但是由于无数据可读,因此进入等待状态,此时写入线程获取到执行权,写入数据后,执行权再次由读取线程获取,读取数据并打印。由此可知,由于多线程的执行特点,无论先执行写入还是读取,都可以实现数据的写入和读取。为了更明显的说明,写入/读取的实现与执行顺序的无关性,在写入数据和读取数据前后,向控制台打印说明信息,并令写入线程写入数据前冻结5秒钟,代码如下,

代码19:

class Read implements Runnable
{
	private PipedInputStream pis;
	Read(PipedInputStream pis)
	{
		this.pis = pis;
	}
	public void run()
	{
		try
		{
			byte[] buf = newbyte[1024];
 
			System.out.println("读取流:读取数据前,没有数据,进入阻塞状态");
 
			int len = pis.read(buf);
 
			System.out.println("读取流:读取数据,阻塞结束");
 
			System.out.println("数据:"+newString(buf,0,len));
		}
		catch (IOException e)
		{
			throw new RuntimeException("管道读取流读取失败!");
		}
		finally
		{
			try
			{
				if(pis != null)
				pis.close();
			}
			catch (IOException e)
			{
				throw new RuntimeException("管道读取流关闭失败!");
			}
		}
	}
}
class Write implements Runnable
{
	private PipedOutputStream pos;
	Write(PipedOutputStream pos)
	{
		this.pos = pos;
	}
	public void run()
	{
		try
		{
			System.out.println("写入流:5秒钟后,开始写入数据");
 
			//令写入线程冻结5秒钟
			try{Thread.sleep(5000);}catch(InterruptedExceptione){}
                    
			pos.write("管道流测试".getBytes());
		}
		catch (IOException e)
		{
			throw newRuntimeException("管道写入流写入失败!");
		}
		finally
		{
			try
			{
				if(pos != null)
					pos.close();
			}
			catch (IOException e)
			{
				throw newRuntimeException("管道写入流关闭失败!");
			}
		}
	}
}
以上代码的执行结果为:

读取流:读取数据前,没有数据,进入阻塞状态

写入流:5秒钟后,开始写入数据

读取流:读取数据,阻塞结束

数据:管道流测试

        其中,在第二行与第三行之间,等待了一段时间(约5秒半)。这一结果对应的执行顺序是,首先执行读取线程,但无数据可读,进入阻塞状态,释放执行资格和执行权,此时写入流获取执行权,但需要等待5秒钟。随后写入数据,读取线程执行完毕,执行权交由读取流,读取数据并打印到控制台。当然,也有可能写入线程先执行,但因为需要等待5秒,因此在此期间执行权交由读取线程。读取线程打印提示信息后,因无数据可读,进入阻塞状态,此时5秒已到,写入线程写入数据,随后读取线程读取数据并打印,其执行结果应为:

写入流:5秒钟后,开始写入数据

读取流:读取数据前,没有数据,进入阻塞状态

读取流:读取数据,阻塞结束

数据:管道流测试

6 RandomAccessFile——随机读取文件

        RandomAccessFile类也属于IO流类,但是相对其他IO流比较特殊。首先从类名来说,类名后缀既没有InputStream/OutputStream或Reader/Writer,也就是说既非字节流也非字符流,从其API文档可知,该类直接继承自Object,可以说自成一派。

6.1  RandomAccessFile概述

API文档:此类的实例支持对随机访问文件的读取和写入,随机访问文件的行为类似存储在文件系统中的一个大型byte数组,存在指向该隐含数组的光标或索引,称为文件指针;输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针,同样输出操作也从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。该文件指针可以通过getFilePointer方法读取,并通过seek方法设置。

        那么总结起来,RandomAccessFile类具有两大特点:第一,既支持读取,也支持写入;第二,通过文件指针,可以在关联文件指定字节位上实现读取和写入,这也是类名——随机访问文件的由来。而之所以具备这两个特点,正像上述API文档所说,该类内部封装有一个用于指向文件中字节数据的文件指针,通过该指针,可以读取到文件中指定位置处的字节数值(相当于读取),亦可将指定位置处的字节值设置为指定值(相当于写入),这也就同时实现了读取和写入操作,并且这样的读取和写入方式就像是在操作一个数组。实际上,RandomAccessFile类实现读取和写入操作的原理,就是在其内部封装了字节读取和写入流,并以byte数组作为中转。

构造方法:

        public RandomAccessFile(File file, String mode) throws FileNotFoundException:创建从中读取和向其写入(可选)的随机访问文件流,该文件由File参数指定。其中,参数file,表示与该RandomAccessFile对象相关联的文件对象,而mode表示的是对关联文件的访问模式,包括“r”,“rw”,“rws”,“rwd”,其中常用的模式为“r”和“rw”,分别表示只读模式与读写模式,其他模式的含义可以查阅API文档,在后面的应用演示部分,我们重点介绍只读和读写模式。需要注意的是,如果传递的表示模式的字符串并非以上四种之一,就会抛出IllegalArgumentException,非法参数异常。

        public RandomAccessFile(String name, String mode) throws FileNotFoundException:与以上构造方法类似,只是关联文件不再是File对象,而是文件地址/文件名字符串。

        那么从以上两个构造方法可知,RandomAccessFile类,只能用于操作硬盘文件,也就是说,读取源和写入目的均是硬盘,而不能是其他源和目的(比如,键盘和内存等)。

方法:

        RandomAccessFile类提供了基本的读取/写入单个字节、读取一个字节数组等方法,而这些方法其实都是通过封装于内部的字节读取/写入流实现的,这里不再赘述。在读取/写入字节方法的基础上,对其进行封装成读取/写入八种基本数据类型的方法,比如readBoolean/writeBoolean、readByte/writeByte等等,甚至提供了读取/写入一行文本的readLine/writeUTF(该方法按默认按照UTF-8编码表,将指定字符串对应的字节写入到文件中)方法,由于以上已到的方法我们在前面的内容中已经涉及过,因此这里不再重复介绍,重点说一说该类的特有方法,

        public void seek(long pos) throws IOException:设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读写或写入操作。也就是说,该方法用于设置前述的文件指针,参数pos的值就是指针指向的byte数组角标值。通过设施该角标值,可以获取数组指定位置处的字节,或向指定位置处写入字节。

        public int skipBytes(int n) throws IOException:尝试跳过输入的n格字节以丢弃跳过的字节。该方法与seek类似,跳过n个字节位相当于就是将字节位指定为当前字节位+n+1。但是该方法的缺点在于只能向下跳跃,而不能反方向跳跃。

        由于skipBytes方法的局限性,在以下例程中,我们重点介绍seek方法,大家可以自行尝试skipBytes。

6.2  RandomAccessFile应用演示

(1)  写入操作

代码20:

import java.io.*;
 
class RandomAccessFileDemo
{
	public static void main(String[] args) throws IOException
	{
		writeFile();
	}
	public static void writeFile()throws IOException
	{
		//创建RandomAccessFile对象,并与一个文本文件关联,指定为读写模式
		RandomAccessFile raf =
			new RandomAccessFile("D:\\DemoFile.txt","rw");
 
		//写入字符串对应的字节数组
		raf.write("David".getBytes());
		//写入一个整数
		//raf.write(97);
		raf.write(258);
 
		//关闭调用的底层IO资源
		raf.close();
	}
}
我们执行以上代码时,首先将“raf.write(258)”注释掉,那么执行后指定文件中的内容为“Davida”,那么这个结果是显然的——字符串“David”不必多说,而值为97的字节,经编码表(默认为GBK)转换后就是字母“a”。

        如果将“raf.write(97)”注释掉,执行第二个写入字节语句,写入文件中的内容为“David”,最后一个字符可以认为是乱码。而之所以造成这一现象的原因是,字节写入流的write方法仅写入指定数值对应的二进制数最低八位。而十进制数258对应的二进制表示形式为100000010,最低八位就是00000010,转换为十进制就是2,而GBK编码表中2所对应的字符是不能显示在文本中的,因此显示为乱码,那么在这一写入过程中就发生了数据的丢失。

        既然是由于数据丢失造成以上问题,那么只要将指定数值的全部四个字节都写入到文件中即可,比如我们可以将指定值“与”(“&”)上255,获取最低八位写入到文件中,然后将原值向右移八位,重复以上过程四次即可将所有四个字节写入到文件中。该方法虽然可行,但是较为麻烦,最好的办法就是使用RandomAccessFile类提供的方法——writeInt,那么修改后的语句为“raf.writeInt(258)”,最终显示在文件中的内容是“David  ”——表示两个空格,和两个乱码,前两个八位均0,因此对应了两个空格,后两个八位一个是1,另一个是00000010,通过编码表转换均为无法显示字符,因此对应两个乱码。那么如果我们调用writeInt方法并传入97,则显示在文件中的内容为“David   a”,在字母a之前就包含了三个空格。

(2)  读取操作

        假如事先通过上述的写入方法,向DemoFile文件中写入了两对姓名和年龄(David,23,以及Peter,26),在以下例程中将读取这些信息,并打印到控制台。

代码21:

import java.io.*;
 
class RandomAccessFileDemo
{
	public static void main(String[] args) throws IOException
	{
		readFile();
	}
	public static void readFile() throws IOException
	{
		RandomAccessFile raf =
			new RandomAccessFile("F:\\DemoFile.txt","r");
             
		//定义字节缓冲用于存储姓名字符串对应的字节数组
		byte[] buf = new byte[5];
		//从关联文件头部开始读取5个字节,并存入字节数组中
		raf.read(buf);
 
		//将字节数组转换为姓名字符串
		String name = new String(buf);
		//通过readInt方法直接读取年龄
		int age = raf.readInt();
 
		System.out.println("name= "+name+", age = "+age);
	}
}
执行以上代码的结果为:

name = David, age = 23

        在以上代码中,首先从文件头部开始读取了5个字节(表示姓名“David”中五个字符对应的GBK编码表中的字节值),并存入到了字节数组中,此时RandomAccessFile对象内的文件指针就指向了关联文件中的第6个字节,而调用readInt方法时就从第6个字节开始连续读取了4个字节,并将这四个字节整合为一个整数返回,也就是姓名“David”所对应的年龄了。

(3)  在指定位置读取/写入数据——随机读写

        以上内容所涉及的读写操作与其他IO流常规的读写操作是类似的,而RandomAccessFile类真正的特点需要将读写操作与seek方法结合起来,以此实现随机读写。比如,如果想要直接读取到第二个人的信息,只需在代码15的基础上添加一行代码“raf.seek(9);”即可,由于两个人的姓名和年龄都由9个字节组成,那么将文件指针直接指向第9个字节位,并执行同样的操作即可读取到第二个人的姓名和年龄,将readFile方法修改后的代码如下所示,

代码22:

public static void readFile() throwsIOException
{
	RandomAccessFile raf =
		new RandomAccessFile("F:\\DemoFile.txt","r");
             
	//将文件指针定位到第九个字节
	raf.seek(9);
 
	byte[] buf = new byte[5];
	int len = raf.read(buf);
 
	String name = new String(buf,0,len);
 
	int age = raf.readInt();
 
	System.out.println("name= "+name+", age = "+age);
}
执行结果为:

name = Peter, age = 26

        那么以上代码中的seek方法就体现了RandomAccessFile——随机访问文件类的特点,通过seek方法,将文件指针指向指定字节位,并从该字节位开始进行读取/写入操作。但若要通过该类实现大量数据的读取/写入,就对关联文件提出了一定的要求:希望存储的数据具有一定的规律,比如以上述例程为例,希望姓名和年龄所占有的字节数都是一样的,比如都是9个字节位,这样循环调用seek方法并传入9的倍数(从9*0开始,表示第一个人的信息),就可以依次获取到文件中所有的个人信息。为了满足不同名称之间长度上的差异,可以统一要求姓名都用16个字节存储(从末尾位置向前存储),年龄用4个字节存储(一个整型值),这样一个完整的个人信息就占用20个字节,实现该功能的例程放置在了6.3节后,供大家参考。

        与随机读取类似,将seek方法和write方法结合起来,就可以实现从指定字节位开始写入字节数据——随机写入。还是以前述代码中“DemoFile.txt”文件为例,该文件中已存储了两个个人信息——David,23和Peter,26,现在的需求是在以上内容后跳过9个字节再写入一个个人信息——Susen,32,代码如下所示,

代码23:

import java.io.*;
 
class RandomAccessFileDemo2
{
	public static void main(String[] args) throws IOException
	{
		randomWriteFile();
		readFile();
	}
	public static void randomWriteFile() throws IOException
	{
		RandomAccessFile raf =
			new RandomAccessFile("F:\\DemoFile.txt","rw");
 
		//令文件指针指向指定的字节位
		raf.seek(9*3);
             
		//在上述指定字节位写入第三个个人信息
		raf.write("Susen".getBytes());
		raf.writeInt(32);
 
		raf.close();
	}
	public static void readFile() throws IOException
	{
		RandomAccessFile raf =
			new RandomAccessFile("F:\\DemoFile.txt","r");
             
		//将文件指针指向第三个个人信息所在字节位
		raf.seek(9*3);
 
		byte[] buf = new byte[5];
		int len = raf.read(buf);
 
		String name = new String(buf,0,len);
 
		int age = raf.readInt();
 
		System.out.println("name= "+name+", age = "+age);
 
		raf.close();
	}
}
以上代码的执行结果为:

name = Susen, age = 32,而文件中的内容就是在第二个个人信息后留有9个空格,接着才是第三个个人信息,当然年龄是无法正常显示的。显然,在每次创建RandomAccessFile对象并关联文件时,并没有覆盖原有文件内容,这是与一般读取/写入流的另一个不同之处,而这个不同之处是与该类的读取/写入模式有关的:

只读模式下,指定文件存在,则直接操作现有文件而不覆盖,若不存在将抛出异常;

读写模式下,指定文件不存在时自动创建文件,若不存在则直接操作指定文件而不覆盖。

        以上例程就体现了RandomAccessFile类的特点——实现随机写入和随机读取。这里的随机不是说“随意”,而是可以指定在文件中对数据进行写入/读取操作的位置。当然,既然可以通过seek方法指定文件中任意的字节位,那么不仅可以将数据写入到空白处,也可以令文件指针指向已有数据的位置,对已有数据进行修改(或称为覆盖),大家可以自行尝试。

6.3 RandomAccessFile总结

        在以上内容中我们介绍了RandomAccessFile类的操作原理、特点,并演示了基本的使用方法,那么该类在实际开发中有哪些应用呢?比如,既然可以指定写入数据的位置,我们就可以通过该类实现对文件的分段写入。假设我们要向一个文件中写入100字节大小的数据,那么我们就可以将这100个字节分为5段,每段20个字节,将这5段数据分别交由5个线程中的5个随机访问文件对象同时向文件中写入,以达到提高写入效率的目的。为了保证数据的完整性,必须保证每个线程的头尾字节位是连续过渡的,也就是说,第一个线程负责写入0-19字节位上的数据,第二个线程负责写入20-39字节位上的数据,以此类推,那么想要实现这一功能,就必须依赖于seek方法,为每个线程指定其起始写入字节位。而实际上以上所说的写入原理就是下载软件的下载原理。那么为什么其他写入流不能实现下载功能呢?因为一般写入流不能指定写入位置,而总是从文件头部开始写入,因此即使使用5个线程分段写入数据,不同线程之间写入的数据会相互覆盖,而无法拼接成原有的完整文件,而导致在解码时出现错误。

        以下代码为,将学生信息(姓名+年龄)所占字节数固定为20位,实现学生信息读写操作的例程,仅供大家参考,

代码24:

import java.io.*;
import java.util.*;
 
class RandomAccessFileDemo3
{
	public static void main(String[] args) throws IOException
	{
		File file = new File("D:\\DemoFile2.txt");//用于存储学生信息的文件
		if(!file.exists())
			file.createNewFile();
 
		TreeMap<String,Integer> stuInfos = new TreeMap<String,Integer>();//用Map集合存储学生信息对
 
		stuInfos.put("David",32);
		stuInfos.put("小明",19);
		stuInfos.put("Kate",27);
		stuInfos.put("小红",21);
 
		//将集合中的学生信息写入到文件中
		write(stuInfos,file);
		//读取文件中的学生信息
		read(file);
	}
	public static void write(Map<String,Integer> stuInfos, File file)throwsIOException
	{
		RandomAccessFile raf =
			new RandomAccessFile(file,"rw");
 
		//循环获取Map集合中的个人信息,同时将信息写入到文件中
		Map.Entry<String,Integer> me = null;
		String name = null;
		int age = 0;
		for(Iterator<Map.Entry<String,Integer>> it = stuInfos.entrySet().iterator(); it.hasNext(); )
		{
			me = it.next();
			name = me.getKey();
			age = me.getValue();
 
			//将姓名转换为对应的字节
			byte[] nameData = name.getBytes();
			//将姓名字节转存到总长为16的字节缓冲中
			byte[] nameBuf = Arrays.copyOf(nameData,16);
 
			raf.write(nameBuf);
			raf.writeInt(age);
		}
 
		raf.close();
	}
	public static void read(File file) throws IOException
	{
		RandomAccessFile raf =
			new RandomAccessFile(file,"r");
		int stu_info_len = 20;
 
		/*
			获取到关联文件的长度——字节数,除以单个学生信息字节数
			即为学生信息个数
		*/
		long stuNum = raf.length() / stu_info_len;
		for(int x = 0; x< stuNum; x++)
		{
			raf.seek(stu_info_len* x);//每次读取前,定位到每个学生信息头字节位
 
			byte[] name_data = new byte[16];
			raf.read(name_data);
 
			String name = toName(name_data);//将字符串对应的字节数组转换为字符串
			int age = raf.readInt();
 
			System.out.println("name= "+name+", age = "+age);
		}
 
		raf.close();
	}
	private static String toName(byte[] name)
	{
		int index = 0;
		for(int x = 0; x< name.length; x++)
		{
			if(name[x] == 0)
			{    
				index = x;
				break;
			}
		}
 
		return new String(name,0,index+1);
	}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值