黑马程序员——IO流

------<a href="http://www.itheima.com"target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流! -------

我们在开发程序的时候,经常会需要传输大量的数据到指定的。那么数据从一个设备传输到另一个设备上,这就叫做IO流。(Input/Output)Java对数据的操作是通过流的方式。而Java用于操作流的对象都在IO包中。

按照所操作的数据,流分为两种:字节流与字符流。

而按照流向又分为输入流和输出流。

我们知道,所有存储在计算机中的数据都是二进制的电信号位0101,我们其实操作的就是它们,但是Java给操作这些数据的流分成了两种:字节流与字符流。其实本来所有操作的数据的流都是字节流,因为不论是电影,音乐,图片还是文字,底层都是二进制电信号位,但是我们在实际开发中,对于文字的操作特别多,而文字操作起来又比较繁琐,因为牵扯到不同的码表,需要编码以及译码才能把二进制数字转换为人类可识别的文字,因此Java后来就单独创立了一个字符流以简化对文字的流操作。这是因为而字符流的对象里面融合了编码表,会自动识别系统的编码表(底层是通过System的getProperties获得的)

字符流基于字节流,因为所有的数据都是二进制的,而字节是所有数据的基本单位。

 

对数据的操作就分两种:读和写。对应着上面提到的输入流和输出流。结合字节流和字符流后我们可以猜测,至少有以下几类:字节输入流,字节输出流,字符输入流和字符输出流。查阅API我们会发现:


四个基类都是抽象的,因为里面有抽象方法,需要被子类具体实现。

 

两个输出流FileOutputStream和FileWriter都有这种构造函数:可以往里面添加 boolean append来决定是覆盖还是续写。如果boolean append为false的话,一new对象就会覆盖文件。

数据最常见的形式是文件,先以操作文件为主演示。

需求:在硬盘上创建一个文件并写入一些文字数据。

找到一个专门用于操作文件的Writer子类对象。FileWriter,后缀名是父类名,前缀名是该流对象的功能。

Writer类没有空参数的构造函数,因为要先有文件才能操作文件。

注意:Writer类一创建好就会在目标位置创造文件,即使还没有数据写入也会把原有文件删掉,自己建立一个0KB的新文件,等待数据传入动作。所以说,如果从同一个文件提取数据最后再放回原文件的话需要一个第三方将所有数据都提取出来之后再创建Writer输出流对象,否则还没等提取完数据,数据源就已经不存在了。

FileWriter

FileWriter的一种构造函数是传入String类的文件名,这样就会创建一个FileWriter对象,该对象一被初始化就明确了被操作的文件,就会在目标目录下创建这么样一个文件,即使没有数据写入(文件大小为0字节)。而且该文件会被创建到指定目录下。如果该目录下已有同名文件,原来的文件会被覆盖。其实该步就是在明确数据要存放的目的地。

【注意】FileWriter的构造函数会产生IOException异常,因此要么抛要么try。

另外,要将字符串写入到流中,需要调用write方法,。

flush方法会将流对象的缓冲区里面的数据刷新到目的地里面去。

close方法会关闭流资源,但是关闭前会刷新一次内部缓冲区中的数据,将数据刷到目的地中。和flush的区别在于:flush刷新后,流可以继续使用,close刷新后,会将流关闭。如果此时再write数据会报异常,提示流已经关闭Stream Closed。

Java会调用系统中的内容来完成数据的建立。它自己无法独立完成,因为每个系统读写机制都不一样。因此这些调用方式都是在使用windows的资源,使用完了以后需要释放资源,因此close动作一定要做。

注意:这里只是释放操作的系统资源,但是本类的对象并没有关闭,需要等Java的垃圾回收机制回收掉,我们不用管。但是千万不能不close那样的话可能本类对象被回收了,但是其指向的操作系统的资源还在。

具体代码如下:

import java.io.*;
class FileWriterDemo 
{
	public static void main(String[] args) throws IOException
	{
		FileWriter fw = new FileWriter("写入文件测试.txt");//这里面写文件名
		fw.write("abcdefg");
		fw.flush();
		fw.write("hahaha");
		fw.close();
//		fw.write("eee");//流关闭后再write数据会报异常,提示流已经关闭Stream Closed。
	}
}
对IO异常的处理和说明详见:
import java.io.*;
class FileWriterExceptionDemo 
{
	public static void main(String[] args) 
	{
		FileWriter fw = null;//引用的建立要放在外面否则finally里面的fw不能被识别出来(同级的括号里)
		try
		{
			fw = new FileWriter("测试.txt");//对象的建立要在里面因为会出异常。而这里如果路径错误的话会出现FileNotFoundException,它是IOException的子类异常。
			fw.write("哈哈哈");
		}
		catch (IOException e)
		{
			sop("捕获:"+e);//加不加toString最后都会转成字符串打印。
		}
		finally
		{
			try
			{
				if (fw!=null)
					fw.close();//一定会进行的处理放在finally里面,通常这里面放的都是释放资源,但要进行判断,如果如上面所示,在构造函数那步就出问题了,对象都没有建立起来,就不可能释放资源。而如果不判断的话,会出现空指针异常,影响程序健壮性,因为这个异常不是用户能处理的。
			}
			catch (IOException e)
			{
				sop("catch"+e);
			}
		}
	}
	public static void sop(Object obj)
	{
		System.out.println(obj);
	}
}

一个要点就是要把类类型变量的引用和对象的建立分开来,在建立类类型变量的引用时我们一般都给赋个null值,因为局部变量最好都要初始化一下,否则底下finally代码块无法读到与它同级的try代码块内部定义的变量,编译失败。

FileNotFoundException是IOException的子类异常。

 

         fw= new FileWriter("测试.txt", true);//传递一个true参数,代表不覆盖已有的文件。并在已有文件的末尾处进行数据续写。

         fw.write("\r\nnewLine");//txt里面\r\n才是换行符,而其他软件的换行单独打一个\n即可,而txt里面\n是一个黑框,这就是为什么我们看很多别人写的程序里面许多黑框的原因。

FileReader

要调用读的方法首先需要创建一个读取流对象,和指定名称的文件相关联,并且要保证该文件是已经存在的,如果不存在,会发生FileNotFoundException。

此外与Writer不一样的是,Reader的close方法并不刷新,因为读压根不需要刷新就能自动将数据从源获取到。

之前提到过,字符流都包含了编码,通常使用系统默认的编码。一般Windows简体中文的操作系统都是GBK编码表。

为了节约空间,数据在硬盘上存储可能会一个紧挨着一个,因此每个文件在结尾处有分隔符以区分两个文件,这个分隔符是什么我们不用知道,Java在调用到最后一个有效数据后读到这个分隔符会返回-1,便于我们判断。

Reader的核心方法就是read了,不管是什么Reader操作的都是字符,所以read方法读的也是以字符为单位,其中read()的空参数方法读取的是按照编码表规定的单个字符(有的编码表一个字符是两个字节,有的编码表一个字符三个字节,都不一样,所以融合了编码表的Reader流就能根据编码表,一次读两个字节或者一次读三个字节),返回的是那个字符所对应的码表序号,int类型的(4个字节,所以绝对能装下),也就是说如果要输出的话需要强制转换为char类型才能被人类识别,因为Reader已经融合了系统的码表,编码和译码都是用同种码表的话就杜绝了乱码出现的可能。另外上面已经说过如果读到-1说明文件到了结尾。这种是一次读一个字符的方法,显然很麻烦。

下面是升级版:读取的第二种方式,通过字符数组进行读取。

【注意】read(char[] cbuf)方法也会返回一个int类型的值,但与单个读取一个字符返回的int值不同。单个读取,也就是空参数的read方法返回的int值是那个字符所对应的编码表序号,而字符数组读取的是所读到的字符的个数。读到结尾了返回-1,这一点二者是一致的。这里【注意】read([])意思是用数组装着传过来的数据,而不是对方传过来一个长度有限的数组,所以除非真的读到了文件的结尾否则不会返回-1的。

import java.io.*;
class FileReaderDemo2//第二种读的方法,通过字符数组进行读取。
{
	public static void main(String[] args) throws IOException
	{
		FileReader fr = new FileReader("测试.txt");

		//定义一个字符数组,用于存储读到的字符。
		//该read(char[])返回的是读到字符个数。如果没有读到任何字符返回-1。
		char[] buf = new char[3];//实际开发中通常把数组容量设为1024,而每个字符2字节所以一个数组2KB
		/*int num = fr.read(buf);//对于硬盘上的数据指针会保持在取走的最后一个元素后面,而数组的指针不会停留在最后一个记录的元素后面,下次再调用fr.read(buf)方法时,不管后面还有没有空位,都会从头开始重新覆盖,但没被覆盖到的元素会被保留。
		sop("num="+num+"..."+new String(buf));//将数组转为字符串的一种方法。
		*/
		int num = 0;
		while ((num = fr.read(buf))!=-1)
		{
			sop("num="+num+"..."+new String(buf,0,num));//最后一个参数是偏移量,表示读几个数,正好就是我们num的量。
		}
		//这种方式更常用一些,因为上一种是读一个处理一个,这种是一气读很多个,然后再处理。
		fr.close();
	}
	public static void sop(Object obj)
	{
		System.out.println(obj);
	}
}

这种方式更常用一些,因为上一种是读一个处理一个,这种是一气读一个数组,很多个(实际开发中通常把数组容量设为1024,而每个字符2字节所以一个数组2KB),然后再处理。

小练习:

将C盘一个文本文件复制到D盘。

原理:

其实就是将C盘下的文件数据存储到D盘的一个文件中。

步骤:

1.在D盘创建一个文件,用于存储C盘文件中的数据。

2.定义读取流和C盘文件关联。

3.通过不断的读写完成数据存储。

4.关闭资源。

代码如下:

import java.io.*;
class CopyText
{
	public static void main(String[] args) throws IOException
	{
		copy_2();
	}
	public static void copy_1() throws IOException//第一种方法,从C盘读一个字符,就往D盘写一个字符。
	{
		//创建一个文件读取流对象,和指定名称的文件相关联。
		FileReader fr = new FileReader("CopyText.java");
		//创建目的地。
		FileWriter fw = new FileWriter("D:\\CopyText_2.java");
		int ch = 0;
		while ((ch=fr.read())!=-1)
			fw.write(ch);
		fr.close();
		fw.close();
	}
	public static void copy_2()//第二种方法,从C盘一次读一个字符数组,然后再往D盘写一个字符数组。
	{
		FileReader fr = null;
		FileWriter fw = null;//不等于空也行,但局部变量最好都要初始化一下,否则finally里面操作不了这个变量(try里面的FileReader和FileWriter相对于finally来说在同级别别的代码块里的访问不到),编译失败。
		try
		{
			fr = new FileReader("CopyText.java");
			fw = new FileWriter("D:\\CopyText_2.java");
			char[] buf = new char[1024];
			int len = 0;
			while ((len=fr.read(buf))!=-1)
				fw.write(buf,0,len);
		}
		catch (IOException e)
		{
			throw new RuntimeException("读写失败");
		}
		finally
		{
			try
			{
				if (fr!=null)
					fr.close();
			}
			catch (IOException e)
			{
				throw new RuntimeException("读写失败");
			}
			finally
			{
				if (fw!=null)//注意fr和fw是互相独立的,不管另一个是否开启了,自己都有可能开启,所以我们都要判断,如果开启都必须结束。而如果我们写成这样finally里面嵌套finally的话需要把上面的if (fr!=null)放到try里面来,否则一旦fr异常的话fw也结束不了了。
				{
					try
					{
						fw.close();
					}
					catch (IOException e)
					{
						throw new RuntimeException("读写失败");
					}
				}
			}
		}
	}
}


字符流的缓冲区

我们在进行小练习的时候会发现,复制文件的速度非常慢,远远低于系统的速度,当文件较大的时候尤为明显。针对这个问题,Java发明了缓冲区技术。

缓冲区的出现提高了对数据的读写效率,流的操作效率。

对应类:BufferedWriter和BufferedReader。

缓冲区要结合流才可以使用,在流的基础上对流的功能进行了增强。

 

所以在创建缓冲区之前,必须要先有流对象。

用法:只要将需要被提高效率的流对象作为参数传递给缓冲区的构造函数即可。

关闭缓冲区,就相当于关闭缓冲区中的流对象,所以不用再对缓冲区和流对象分别关闭了,仅关闭缓冲区即可。其实BufferedReader的close方法底层调用的就是流的close方法,它自己本身没什么可关的,它仅仅是一个对流进行效率提升的对象,就像其他普通对象一样。

 

windows里面/r/n是换行,而linux里面/n是换行,所以/r就会成了多余字符,为了提高跨平台性,可以使用缓冲区BufferedWriter里面的newLine()方法写入一个行分隔符,它能自动根据系统加入相应换行符,所以它是跨平台的,在windows里面写的代码到其他系统里面也可以使用。

缓冲区在FileWriter里,而我们操作的是BufferedWriter,但是这里注意一点,只要用到缓冲区,不管是直接还是间接用到的,都要flush才能输出。

 

字符读取流缓冲区也是为了提高读的效率而出现的,该缓冲区提供了一次读一行的方法readLine,返回一行的所有数据并用String封装,但是不包含任何行终止符也就是换行符,仅仅是有效数据,这个方法方便了于对文本数据的获取。当返回null时,表示读到文件末尾。

练习:通过缓冲区复制一个.java文件:

import java.io.*;
class CopyTextByBuf
{
	public static void main(String[] args)
	{
		BufferedReader br = null;
		BufferedWriter bw = null;
		try
		{
			br = new BufferedReader(new FileReader("CopyTextByBuf.java"));
			bw = new BufferedWriter(new FileWriter("KB.TXT"));
			String line = null;
			while ((line=br.readLine())!=null)//readLine读一次指针往下移一位,所以如果不用line这个变量的话在下面直接write(readLine())会产生跳行的问题。
			{
				bw.write(line);
				bw.newLine();
				bw.flush();
			}
		}
		catch (IOException e)
		{
			throw new RuntimeException("读写失败");
		}
		finally
		{
			try
			{
				if (br!=null)
					br.close();
				if (bw!=null)
				{
					bw.close();
				}
			}
			catch (IOException e)
			{
				throw new RuntimeException("关闭读写失败");
			}
		}
	}
}

readLine方法的原理

无论是读一行,或者读取多个字符,其实最终都是在硬盘上一个一个读取。所以最终使用的还是read方法一次读一个的方法。底层其实创建了一个数组,把读到的数据一个一个往里面存,读到\r\n时不存并结束,把字符数组转换成字符串输出。

练习:自己做一个readLine方法。

//明白了BufferedReader类中特有方法readLine的原理后,可以自定义一个类中包含一个功能和readLine一致的方法,来模拟一下BufferedReader
import java.io.*;
class MyBufferedReader extends Reader//如果要继承Reader的话必须实现它里面的所有抽象方法close和read
{
	private Reader r;//定义在成员函数位置,方便所有对象调用。
	MyBufferedReader(Reader r)
	{
		this.r = r;
	}
	//可以一次读一行数据的方法。
	public String myReadLine() throws IOException//谁使用谁处理,这里定义方法,不用做错误处理try catch。
	{
		//定义一个临时容器,原BufferedReader的readLine方法封装的是字符数组。为了演示方便,定义一个StringBuilder容器,因为最终还是要将数据变为字符串。
		StringBuilder sb = new StringBuilder();//必须创建在里面作为局部变量存在,每次调用完该myReadLine函数就释放,否则主函数里的while循环每次调用的时候sb里面已经有值了,是上一次的残留值,不是我们需要的,也不利于跳出循环的判断。
		int ch = 0;
		while ((ch=r.read())!=-1)//调用本类的read方法也可以
		{
			if ((char)ch=='\r')
				continue;
			if ((char)ch=='\n')
				return sb.toString();//判断到换行符就输出
			//就算sb里面没东西,sb.toString()返回的也不是null而是""。
			sb.append((char)ch);
		}
		//如果循环结束读到这里说明r.read()==-1,说明read的指针已经到了文件结尾处。
		if (sb.length()!=0)//读到最后有可能没换行但有内容,也要输出。
			return sb.toString();
		return null;//实在没内容了才输出空。
	}
	public int read(char[] chbf, int start, int length) throws IOException//虽然这个方法我们不用,但是也得复写,因为父类的同名方法是抽象的,子类想要创建对象必须复写所有父类的抽象方法。
	{
		return r.read(chbf,start,length);
	}
	public void close() throws IOException
	{
		r.close();//其实BufferedReader的close方法底层调用的就是流的close方法,它自己本身没什么可关的,它仅仅是一个对流进行效率提升的对象,就像其他普通对象一样。
	}
}
class MyBufferedReaderDemo 
{
	public static void main(String[] args) throws IOException
	{
		MyBufferedReader mr = new MyBufferedReader(new FileReader("MyBufferedReaderDemo.java"));
		String line = null;
		while ((line=mr.myReadLine())!=null)
		{
			sop(line);//该方法已经加换行了,不用再newLine()
		}
		mr.close();

	}
	public static void sop(Object obj)
	{
		System.out.println(obj);
	}
}


装饰设计模式

当想要对已有对象进行功能增强时,可以定义类,将已有对象传入(装饰类通常通过构造函数接收被修饰的对象),基于已有对象的功能并提供加强功能(而它具备的功能和已有的类是相同的)。那么自定义的该类称为装饰类。装饰类就是提供功能加强。

通常情况下装饰类和被装饰的类会同属于一个接口或者一个类,是同一个体系中的成员。

继承也可以达到相同的效果,但是比如说如果子类有升级的算法(如缓存),那么可以再建立那些类的子类,用新技术(缓存)复写原有方法,但是那样的话每个子类都得创建新的子类然后复写,以后如果父类有了新的子类,我们还要对它建立子类复写功能,扩展性很差。

换种思路:我们可以找到那些子类的共同类型,通过多态的方式,将他们传入进我们的装饰类来,加入共性的性能提升方法,提升扩展性。

这样就从之前的臃肿结构:


变为了:


子类和装饰类是平行关系。

装饰设计模式使得体系从继承结构变成组合结构。(我中有你,共同结合起来发挥作用)

继承不要写太多,如果仅仅为了几个功能而就产生子类的话体系会非常臃肿。可以通过装饰的方式来扩展这些类的功能。降低了类与类之间的关系,这样很灵活。

 

我们自定义装饰类需要让它继承原集合的父类,如果父类是抽象类我们需要复写里面的抽象方法,一个简单的做法是调用传入进来的其他子类的相应方法。

 

装饰类BufferedReader 还有个子类,叫LineNumberReader,功能进一步增强,除了保留BufferedReader的readLine方法外还可以操作行号。

 

字节流

如果直接使用字节流来操作数据,而没有使用具体指定的缓冲区的话。字节流不需要缓冲,也就是不需要刷新就能进入目标。但close还是要写,虽然它不具备刷新功能但是关闭资源还是必须的。

 

FileInputStream的available()方法返回目标的剩余字节数(已经读过的不算),该方法是有指针标记的,也就是第一次运行完available()方法后,指针会停在最后一个字节后面,当目标第二次被available()会返回0(换行在windows里是/r/n所以算两个)。中文算两个自不必说。

 

练习:复制一个图片。代码如下:

import java.io.*;
class CopyPic 
{
	public static void main(String[] args) throws IOException
	{
		FileInputStream fis = null;
		FileOutputStream fos = null;
		try
		{
			fis = new FileInputStream("g:\\汇总.pdf");
			fos = new FileOutputStream("d:\\汇总.pdf");
			/*有溢出风险的一种方式
			byte [] buffer = new byte[fis.available()];
			fis.read(buffer);
			fos.write(buffer);*/
			byte [] buffer = new byte[1024];
			int length = 0;
			while ((length=fis.read(buffer))!=-1)
			{
				fos.write(buffer,0,length);
			}
		}
		catch (IOException e)
		{
			throw new RuntimeException("复制文件失败");
		}
		finally
		{
			try
			{
				if (fis!=null)
					fis.close();
			}
			catch (IOException e)
			{
				throw new RuntimeException("读取关闭失败");
			}
			try
			{
				if (fos!=null)
					fos.close();
			}
			catch (IOException e)
			{
				throw new RuntimeException("写入关闭失败");
			}
		}
	}
}

用字符流也可以复制但是可能打开看不了,因为字符流内部还整合了系统的编码表。不要拿字符流去拷贝媒体文件,它仅仅是用来处理文字数据。

 

字节流也有缓冲区,其目的也是为了提高字节流的效率。在代码上看起来通不通过缓冲区写法差不多,但是实际上缓冲区的read和write方法都复写了原来的FileInputStream和FileOutputStream的同名方法,机制不一样,效率提高很多。存储速度相差百倍。

练习:自定义字节流的缓冲区。代码详见:

import java.io.*;
class MyBufferedInputStream
{
	private InputStream in;
	private byte [] buffer = new byte[1024];
	private int length = 0, index = 0;
	private byte b=0;
	MyBufferedInputStream(InputStream in)
	{
		this.in = in;
	}
	//一次读一个字节,从缓冲区(字节数组)中读取。
	
	public int read() throws IOException
	{
		if (index == length)
		{
			length = in.read(buffer);
			if (length==-1)
				return -1;
			index = 0;
			b = buffer[index++];
			return b&255;//和int型运算,类型自动提升为int型,并保留byte的全部8位,前面24位用0补齐。
		}
		else
		{
			b = buffer[index++];
			return b&0xff;//16进制的255
		}
	}
	public void close() throws IOException
	{
		in.close();
	}
}
//如果上面不是return b&255而是return b 那么循环可能会停下,输出的文件是0字节。循环停下的原因就是读到了数据中连续的8个1.
//为什么不用byte而用int接收?就是为了避免byte数据中出现连续8个1时判断为-1,所以做类型提升时在byte前面补24个0.(int是4个字节32位)这样原本是-1的byte值转为了int后就变成了255,这样既可以保留原字节数据不变,又避免了跟我们系统判断读到结束的-1相冲突。
//write方法虽然传入的参数是int型,但它只将最低8位写出去,保证原数据的原样性。

读取键盘录入。

System.out:对应的是标准输出设备,控制台。该类是PrintStream的子类,而PrintStream的基类正是OutputStream字节输出流。

System.in:对应的标准输入设备:键盘。该类是InputStream字节输入流的子类。

read方法是一个阻塞式方法,没有读到数据就会等。(所有read都一样,不仅仅是System.in),可以看做有一个线程在操作它,当有录入的时候被唤醒读取数据。

 

小练习:

通过键盘录入数据,当录入一行数据后,就将该行数据进行打印。如果录入的数据是over,停止录入。代码如下:

import java.io.*;
class ReadIn 
{
	public static void main(String[] args) throws IOException
	{
		InputStream in = System.in;
		StringBuilder sb = new StringBuilder();
		int b = 0;
		while (true)
		{
			b=in.read();
			if (b==(int)'\r')
				continue;
			if (b==(int)'\n')
			{
				if (sb.toString().equals("over"))
					break;
				System.out.println(sb.toString().toUpperCase());//转成大写方便我们识别
				sb.delete(0,sb.length());//清空数据,比新建一个sb要好,否则整个内存里一堆对象。
			}
			else
				sb.append((char)b);
		}
	}
}


通过上例定义的键盘录入一行数据并打印其大写,发现其实就是读一行数据的原理,暨readLine方法。

思考能否直接使用readLine方法来完成键盘录入的一行数据的读取呢?

可是readLine方法是字符流BufferedReader类中的方法。

而键盘录入的read方法是字节流InputStream的方法。

那么能否将字节流转成字符流再使用字符流缓冲区的readLine方法呢?

这就涉及到InputStreamReader和OutputStreamWriter两个转换流,用来在字节流和字符流之间做转换。

将上例用两个转化流增强后的代码示例:

import java.io.*;
class TransStreamDemo 
{
	public static void main(String[] args) throws IOException
	{
		//获取键盘录入对象
		InputStream in = System.in;
		//将字节流对象转成字符流对象,使用转换流 InputStreamReader
		InputStreamReader isr = new InputStreamReader(in);
		//为了提高效率,将字符串进行缓冲区技术的高效操作,使用BufferedReader
		BufferedReader br = new BufferedReader(isr);

		//上面三句话合成一句话,就是键盘录入最常见写法。
		//BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		OutputStream os = System.out;
		OutputStreamWriter osw = new OutputStreamWriter(os);
		BufferedWriter bw = new BufferedWriter(osw);
		//相对应还有键盘输出最常见写法。就是把上面三句话合成一句话。
		//BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

		String line = null;
		while ((line = br.readLine())!=null)//System.in输入流不可能有结尾(-1)输出来,因为它不是文件系统,不存储在磁盘上,没有也不需要有结束标记(而如果我们手动输入-1会被拆分成-和1分别输出),因而readLine方法也不可能输出null出来。所以循环条件写成true也行,但这样的话别忘了在循环内部给line赋值。
		{
			if (line.equals("over"))
				break;
//			osw.write(line.toUpperCase());//字符输出流内部有缓冲区,它用的是字节流缓冲区,会把数据缓冲,因此使用OutputStreamWriter的write方法必须得flush一下才能把在缓冲区里的数据传到真正的输出端。
			//注意:这种方法写入没有包含换行符。而如果用"/r/n"又不具备跨平台性,所以想到newLine()方法,可此方法在BufferedWriter里面,可以用装饰类包装一下。
//			osw.flush();
			bw.write(line.toUpperCase());
			bw.newLine();
			bw.flush();//只要是输出,并且用到缓冲区,就要记得刷新。
		}
		br.close();
		bw.close();
	}
}

【再次强调】何时刷新的问题:

只要是输出,并且用到缓冲区,就要记得刷新,不论字节流还是字符流;字节输出流如果没用到缓冲区不用刷新,而字符输出流内部都有缓冲区,它用的是字节流缓冲区,会把数据缓冲,因此使用字符输出流都要刷新。

 

上例中,键盘录入要结束的话要么ctrl+c要么定义一个自定义结束标记,否则停不下来,read()是阻塞式方法,它一直在等待新的录入,直到出现文件的终止符。(其实ctrl+c的效果就是添加一个结尾符)

上例中我们把System.out(OutputStream类)转换为OutputStreamWriter,这样用write方法的话就相当于System.out.print了,因为输出语句底层用的就是流对象。相当于打印了。

 

通过上例我们发现,我们可以任意匹配输入端和输出端,不仅仅是在键盘上输入和在控制台上输出那么简单。


流操作的基本规律:三个明确

流操作最痛苦的就是流对象太多,不知道用哪个。

通过三个明确来完成。

1.      明确源和目的。

源:输入流。InputStream Reader

目的:输出流。OutputStream Writer

2.      操作的数据是否是纯文本。

是:字符流

不是:字节流

3.      当体系明确后,再明确要使用哪个具体的对象。

通过设备来区分:

源设备:内存,硬盘,键盘。

目的设备:内存,硬盘,控制台。

 

 

练习:

把录入的数据按照指定编码表(utf-8)将数据存到文件中

目的是否为纯文本:是,所以用Writer

设备:硬盘。一个文件 所以用FileWriter

但FileWriter使用的是默认的编码表GBK。我们存储时需要按照指定编码表utf-8,而指定编码表只有转换流可以指定。所以要使用的对象是: OutputStreamWriter。而该转换流对象要接收一个字节输出流,而且还要是可以操作文件的字节输出流FileOutputStream。

需要高效?是的,采用BufferedWriter。

代码详见:

import java.io.*;
class CodePractice 
{
	public static void main(String[] args) throws IOException
	{
		BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("输出.txt")));
		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("输出副本.txt"),"utf-8"));//输出后果然大小不一样了,虽然在记事本里看着还一样但是用有道记事本打开发现乱码。
		int i = 0;
		while ((i=br.read())!=-1)
		{
			bw.write(i);
		}
		br.close();
		bw.close();
	}
}

其实转换流真正比较重要的应用是可以指定编码表(只有转换流可以指定),而不是系统默认的。

 

另外有个小知识,System里面有两个方法,setIn和setOut可以篡改System.in和System.out的默认流改为不是通过键盘输入以及不是通过控制台输出的方式。

 

文件File

文件与流的区别:流只能操作数据,而要操作文件和文件夹的属性,用File。

File是用来将文件或者文件夹封装成对象的。方便对文件或文件夹的属性信息进行操作。

File对象可以作为参数传递给流的构造函数。

该类常见的构造方法有很多,一般用:

File f = newFile(String str);//构造函数里面传入一个指定的String类型的文件名。

如果str是null的话运行时会报空指针异常。

这里面的str如果不是null的话,是什么都无所谓,编译和运行都不会报错,但是当create创建它的具体文件时如果盘符有问题就会报异常。

【注意】创建File对象并不是在目标位置真实地创建一个文件或者文件夹。

File类常见方法:

1.      创建:

boolean createNewFile() 在指定位置创建文件,如果该文件已经存在则不创建,返回false。和输出流不一样,输出流对象一产生就会创建文件,如果已经存在目标文件会覆盖。

static File createTempFile(String prefix, String suffix)按照指定规则创建临时文件。

boolean mkdir():             创建文件夹。

boolean mkdirs():           创建多级文件夹。

【注意】mkdirs()可以跨级创建文件夹,但是文件是无法跨级创建的,只能在已经存在的路径下一层创建。

另外同一个目录下创建的文件夹和文件不可以同名,否则会报异常。(即使在Windows里面也不能这么创建,会提示已经存在)。

2.      删除

boolean delete();                删除失败,返回false。

void deleteOnExit();           在虚拟机退出时删除指定文件。

3.      判断

boolean exists():              文件是否存在。

boolean isFile():               判断是否是文件

boolean isDirectory():    判断是否是目录

在判断文件对象是否是文件还是目录时,必须要先判断该文件对象封装的内容是否存在,通过exists判断。

boolean isHidden():        判断文件或者目录是否隐藏

隐藏文件用流访问不了,对java没用,但是在创建File对象的时候还是能获取到,所以我们要进行判断,不是隐藏的才取过来。

boolean isAbsolute():     判断是否是绝对路径(文件不需要创建也能判断);

4.      获取

getName():                       仅仅返回文件的名称,其所有父目录都不要。

getParent():                      如果File以绝对路径创建,返回绝对路径中的文件的父目录(从根目录开始直到上一层目录),如果File以相对路径的形式书写,返回new File(“a/b/c”)中的a/b,如果没有指定父目录,就返回空null。

getPath():                          创建的时候newFile(“”);里面写什么,就输出什么,绝对路径相对路径都原样返回。

toString():                          与getPath()完全一样,我们直接打印File的时候底层调用的就是toString方法。

getAbsolutePath():         返回绝对路径,String形式

getAbsoluteFile():           返回绝对路径,File形式 与上面可以相互转化,上面的new一个File就成了底下的,底下的toString()一下就成了上面的。

获取路径的几种都不需要指定文件的存在,只要有File对象就可以。

 

long lastModified():        返回最后修改的时间。

long length():                    返回文件的长度或者叫大小,容量(类似于字节流里面的available)

boolean renameTo():      重命名,带有剪切的功能

static File[]listRoots()              静态方法,没有特有对象参与运算,返回当前机器的所有盘符,用数组形式。

String[] list()                                  用来获取指定目录下所有文件和文件夹名称。包含隐藏文件。注意:调用list方法的File对象必须是封装了一个目录,该目录必须存在,如果File对象封装的是文件或者封装的目录不存在的话返回来是null。只要目录存在,就会成功返回一个数组,就算如果目录里面没有文件,仅仅是长度为0。

String[]list(FilenameFilter filter)  上面方法的升级版,传参是一个过滤器FilenameFilter,这个过滤器是一个接口,里面只有一个方法boolean accept(File dir, String name)  两个参数,前面是指定目录,后面是指定名称。只有在(我们定义的)指定目录下按照(我们定义的)名称规则的文件才能通过,并收录进String[]数组里。

File[]listFiles()  上述String[]list()的升级版,返回类型是File数组,可以拥有更多操作。

File[]listFiles(FilenameFilter filter)  上述String[]list(FilenameFilter filter)的升级版,返回类型是File数组,可以拥有更多操作。

 

现有一个需求:列出指定目录下的所有文件(包含子目录及更多层级下的文件)。

因为目录中还有目录,只要使用同一个列出目录功能的函数完成即可,在列出过程中出现的还是目录的话,还可以再次调用本功能。也就是函数自身调用自身。这种表现形式,或者编程手法,称为递归。代码详见:

import java.io.*;
class FileDemo3
{
	public static void main(String[] args) 
	{
		File f = new File("g:\\homework");
		showDir(f,0);
	}
	public static void showDir(File dir, int level)
	{
		if (dir.isDirectory())
		{
			sop(getLevel(level++)+dir);
			File[] son = dir.listFiles();
			for (File f2:son)
			{
				if (f2.isDirectory())
					showDir(f2,level);//递归
				else sop(getLevel(level)+f2);
			}
		}
		else
		{
			throw new RuntimeException("让你传文件夹你瞎啊");
		}
	}
	public static String getLevel(int level)//定义一个添加目录标识的方法
	{
		StringBuilder sb = new StringBuilder();
		sb.append("|--");
		for (int x = 0; x<level ;x++ )
		{
			sb.append("--");
		}
		return sb.toString();
	}
	public static void sop(Object obj)
	{
		System.out.println(obj);
	}
}

【递归要注意】

1.      限定条件。(否则会无限循环)

2.      要注意递归的次数,尽量避免内存溢出。(每一次递归都会在内存中开辟一片新空间,而老空间在等待新空间返回的结果所以没办法释放,只有最末尾的空间运算出结果往回返之后才能释放已经结束的函数的空间)

 

练习:

删除一个带内容的目录

windows删除原理:删除目录从里面往外删除,先删除最里面的所有文件才能删文件夹否则delete返回false。

既然如此就要用到递归。

代码如下:

import java.io.*;
class RemoveDir 
{
	public static void main(String[] args) 
	{
		remover(new File("G:\\HOMEWORK\\teSt\\d21"));
	}
	public static void remover(File f)
	{
		if (f.isDirectory())
		{
			File[] arr = f.listFiles();
			for (File son:arr)
			{
				if (son.isDirectory())
					remover(son);
				else
					sop(son+"--:file delete:--"+son.delete());
			}
			sop(f+"--:dir delete:--"+f.delete());
		}
		else
		{
			throw new RuntimeException("让你传文件夹你瞎啊");
		}
	}
	public static void sop(Object obj)
	{
		System.out.println(obj);
	}
}

【注意】java的删除是不通过回收站的。所以注意备份,千万小心。

Properties

是hashtable的子类。也就是说它具备map集合的特点。而且它里面存储的键值对都是字符串,不需要泛型。

是集合中和IO技术相结合的集合容器,能直接操作各种流对象。

该对象的特点:可以用于键值对形式地配置文件。(键值对是配置信息中最常见的一类,也就是一个属性名称对应属性值,我们玩实况等体育类游戏中比较常见,某个球员的国籍,速度,体能等都是以键值对的形式存在的)

在加载数据时,需要数据有固定格式:键=值。

 

问:Properties键值对都是String类型的,为什么用get方法返回的值是Object类型的,还需要强转一下才能从Object类变为String类?

答:因为get方法是Properties继承自的父类Hashtable的方法(Properties类没有复写也不能复写此方法,因为父类定义的传入类型是Object类型了,子类无法用String类型的传参复写父类的方法,因为String也属于Object,在使用的时候如果传入String类型,Java不能确定具体调用谁的方法),这个方法有泛型,如果没有指定泛型的话返回结果就是Object类型。而Properties不能使用泛型。

 

问:为啥Map里面的get方法要定义成publicV get(Object key)传入参数是Object类型呢?而不直接传入泛型定义好的键的类型K呢?<K, V>

答:因为不是所有的情况下我们都会使用泛型的,如果没有使用泛型,Java工程师在编写源程序的时候不知道我们会传什么样的值进来,就只能用Object接收。

练习:

如何用流将info.txt中键值数据存到集合中进行操作。

1.      用一个流和info.txt文件关联。

2.      读取一行数据,将该行数据用”=”进行切割。

3.      等号左边作为键,右边作为值。存入到Properties集合中即可。

代码如下:

import java.util.*;
import java.io.*;
class PropertiesDemo
{
	public static void main(String[] args) throws IOException
	{
//		method_1();
		loadDemo();
//		setAndGet();
	}
	public static void loadDemo() throws IOException
	{
		BufferedInputStream bis= new BufferedInputStream(new FileInputStream("info.txt"));
		BufferedReader br = new BufferedReader(new FileReader("info.txt"));
		Properties prop = new Properties();
		prop.load(br);//将流中的数据加载进集合。
		prop.list(System.out);//将Properties里面的键值对列表输出给标准输出流也就是控制台。
		prop.setProperty("啊啊","我");
		BufferedWriter bw = new BufferedWriter(new FileWriter("info.txt"));
		prop.store(bw,"XXXXXXXXXXXXXX");
		prop.list(System.out);
		bis.close();
		br.close();
		bw.close();
	}
	public static void method_1() throws IOException//自己创造一个跟load一样的方法。
	{
		BufferedReader br = new BufferedReader(new FileReader("info.txt"));
		Properties prop = new Properties();
		String line = null;
		while ((line=br.readLine())!=null)
		{
			String[] str = line.split("=");
			prop.setProperty(str[0],str[1]);
		}
		br.close();
		sop(prop);
	}
	public static void setAndGet()//设置和获取的例子
	{
		Properties prop = new Properties();
		prop.setProperty("张三","17");
		prop.setProperty("李四","117");
		sop(prop.setProperty("李四","110"));
		Set<String> names = prop.stringPropertyNames();//获取所有的键
		for (String s : names)//用高级for循环取出每一个键
		{
			sop(s+"::"+prop.getProperty(s));//用键获得值
		}

	}
	public static void sop(Object obj)
	{
		System.out.println(obj);
	}
}

Properties的load和store方法能控制输入流和输出流。store方法强制要求往目的地里面写注释。并且还会自动加上修改时间,这两条前面都用”#”标注了,”#”标注的都是注释,在读取时不会读到,而且注释只会记录最后更新的一条。

Properties可以操作各种流(1.6版本新增字符流)

 

练习:

设计一个计数器,用于记录应用程序运行次数。

如果使用次数已到,那么给出注册提示。

很容易想到的是:计数器。

可是该计数器定义在程序中,随着程序的运行而在内存中存在,并进行自增。

可是随着该应用程序退出,该计数器也在内存中消失了。

下一次再启动该程序,又重新开始计数,这不是我们想要的。

我们要程序即使结束,该计数器的值也存在。下次程序启动会先加载该计数器的值,并加1后再重新存储起来。

所以要建立一个配置文件,用于记录该软件的使用次数。

该配置文件使用键值对的形式,这样便于阅读数据,并操作数据。

键值对数据是map集合,数据是以文件形式存储,使用io技术。那么map+ioàProperties

也就是说配置文件可以实现应用程序数据的共享。

代码详见:

import java.util.*;
import java.io.*;
class RunCount 
{
	public static void main(String[] args) throws IOException
	{


		Properties prop = new Properties();
		File f = new File("count.ini");
		if (!f.exists())
			f.createNewFile();
		FileReader fr = new FileReader(f);
		prop.load(fr);
		FileWriter fw = new FileWriter(f);//这一行如果写到上面load前面去的话不行,这相当于在取数据之前就让目标文件清空了,这样从一个空文件就取不到任何数据。
		int count = 0;
		String value = prop.getProperty("time");
		sop(prop);
		if (value!=null)
			count=Integer.parseInt(value);
		count++;
		prop.setProperty("time",count+"");
		prop.store(fw,"SILLY B");
		fr.close();
		fw.close();
		if (count>5)
		{
			sop("拿钱!!");
			return;
		}

/*视频里老师的写法
		Properties prop = new Properties();
		File file = new File("count.ini");
		if (!file.exists())
			file.createNewFile();
		FileInputStream fis = new FileInputStream(file);
		prop.load(fis);
		int count = 0;
		String value = prop.getProperty("time");
		
		sop(prop);
		if (value!=null)
		{
			count=Integer.parseInt(value);
			if (count>=5)
			{
				sop("拿钱!!");
				return;
			}
		}
		count++;
		prop.setProperty("time",count+"");
		FileOutputStream fos = new FileOutputStream(file);
		prop.store(fos,"");
		fos.close();
		fis.close();*/
	}
	public static void sop(Object obj)
	{
		System.out.println(obj);
	}
}

【经验】当初自己做的时候试了很多遍都不成功,time始终是1,因为源数据和目的地是同一个文件,所以应该先把源提取出来load到内存的Properties集合里面,再建立输出流,输出流一创建就会在目的地创建一个0KB的新文件,以前的东西立刻删除,如果放到上面,相当于在取数据之前就让目标文件清空了,这样从一个空文件就取不到任何数据。

 

另外:

Properties在使用输出流的时候貌似在内部自动刷新了,不用我们flush或者close数据也能顺利过去。但是为了优化还是要在结束的时候释放资源。

XML

XML比Properties更高级一些,因为很多数据用键值对表示很麻烦,如一个企业里所有部门的每个员工的各类信息(姓名,年龄,职务等等),用XML比较直观些。如:

<Corp>

<Dept1>

<Person1>

<name>张三</name>

<age>20</age>

</Person1>

<Person2>…</Person2>

</Dept1>

<Dept2>…</Dept2>

</Corp>

 

打印流

该流提供了打印方法,可以将各种数据类型的数据都原样打印。

 

字节打印流PrintStream

构造函数可以接受的参数类型:

1.       File对象。File

2.       字符串路径。String

3.       字节输出流。OutputStream

字符打印流:PrintWriter 既可以接收字节流又可以接收字符流,还可以自动刷新。

构造函数可以接受的参数类型:

1.       File对象。File

2.       字符串路径。String

3.       字节输出流。OutputStream

4.       字符输出流。Writer。

打印流可以在构造函数里面决定是否自动刷新(boolean autoFlush),但仅仅当传参是Writer或者OutputStream时有效。

而且只有在println等方法时才会自动刷新。

打印流的println方法几乎可以打印所有的数据类型。

 

PrintStream和PrintWriter可以通过构造函数指定字符集(编码表)

SequenceInputStream演示:多个源文件汇总为一个输入端,挨个输出。

有构造函数SequenceInputStream(InputStreams1, InputStream s2),只支持两个输入端。那如果有3个4个5个甚至更多输入端怎么办?继续复写吗?如果超过两个输入流,需要用到Vector把这些流装起来,并用elements方法获得Enumeration对象,把这个对象作为参数传递给SequenceInputStream的构造函数SequenceInputStream(Enumeration<? extends InputStream> e)。

代码详见:

import java.io.*;
import java.util.*;
class SequenceDemo 
{
	public static void main(String[] args) throws IOException
	{
		Vector<FileInputStream> v = new Vector<FileInputStream>();
		v.add(new FileInputStream("1.txt"));
		v.add(new FileInputStream("2.txt"));
		v.add(new FileInputStream("3.txt"));
		Enumeration<FileInputStream> e = v.elements();

		SequenceInputStream sis = new SequenceInputStream(e);
		FileOutputStream fos = new FileOutputStream("4.txt");

		byte[] buf = new byte[1024];
		int len = 0;
		while ((len=sis.read(buf))!=-1)
			fos.write(buf,0,len);
		fos.close();
		sis.close();
	}
}

小练习:文件的切割与合并(切忌把缓存用的数组定义得过大,容易溢出,系统默认只给Java虚拟机分配了64MB空间,就算我们手动把分配空间调大也不可能超过计算机的内存总空间,许多老旧计算机的闲置空间甚至不足100MB,所以我们定义10MB或者以下是相对安全而又高效的)。

代码如下:

import java.io.*;
import java.util.*;
class SplitFile
{
	public static void main(String[] args) throws IOException
	{		
		split("g:\\视频\\1.1.avi", 5);
		merge("g:\\","g:\\haha.avi");	
	}
	public static void split(String source, int size) throws IOException//切割:前面填入需要切割的文件的路径,后面添加希望切成的每个碎片文件的大小(MB)
	{
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source));//用FileInputStream太慢,加包装类,还是慢
		byte[] buf = new byte[1024*1024];
		int len = 0;
		FileOutputStream fos = null;
		for (int x=0; (len=bis.read(buf))!=-1; x++)
		{
			if (x%size==0)
			{
				fos = new FileOutputStream("g:\\"+(x/size+1)+".part");
			}
			fos.write(buf,0,len);
		}
		fos.close();
		bis.close();
	}
	public static void merge(String sourceDir,String destFile) throws IOException//合并:前面是搜索碎片文件区域,后面是生成还原的目的文件
	{
		File dir = new File(sourceDir);
		File[] fs = dir.listFiles();

		TreeSet<File> ts = new TreeSet<File>(new Comparator<File>()//用匿名内部类定义一个按名称顺序排列的TreeSet准备接收符合条件的part文件
		{
			public int compare(File f1, File f2)//复写compare方法,使其按照文件名进行排序,这里如果给取反,也就是顺序完全反过来存那文件恢复了会不正常,因为几块数据都打乱了。
			{
				int x = f1.getName().compareTo(f2.getName());
				return x;
			}
		});


		for (File f : fs )
		{
			if (f.getName().endsWith(".part"))//把符合条件的文件添加到集合里面
				ts.add(f);
		}
		if (ts.size()<1)//做一个判断,避免没有part文件程序还继续进行创造一个0KB的目标文件来。
		{
			System.out.println("此区域未找到.part碎片文件");
			return;
		}


		final Iterator<File> it = ts.iterator();//从内部类中访问局部变量 it;需要被声明为最终类型final

		Enumeration<FileInputStream> en = new Enumeration<FileInputStream>()//用该匿名内部类复写内部方法,好把iterator转化为Enumeration,因为SequenceInputStream只接收Enumeration
		{
			public boolean hasMoreElements()
			{
				return it.hasNext();
			}

			public FileInputStream nextElement()
			{
				FileInputStream fis = null;
				try//该方法只抛出NoSuchElementException,与IOException是平级关系,所以我们复写的时候只能try不能抛
				{
					fis = new FileInputStream(it.next());
//					return (new FileInputStream(it.next()));//把从Iterator里面取到的File转成FileInputStream
				}
				catch (IOException e)
				{
					System.out.println("Enumeration里面nextElement方法复写过程中创建FileInputStream出错了");
				}
				return fis;
			}
		};

		SequenceInputStream sis = new SequenceInputStream(en);

		byte[] buf = new byte[1024*1024];
		int len = 0;
		FileOutputStream fos = new FileOutputStream(destFile);
		while ((len=sis.read(buf))!=-1)
		{
			fos.write(buf,0,len);
		}
		sis.close();
		fos.close();
	}
}

ObjectInputStream与ObjectOutputStream,这两个流能操作对象的输入输出,并且只有它们能办到这一点。

读写各种基本数据类型。

为了满足我们这样的一个需求:对象都是在堆内存里,运行完就释放了。为了达到数据的持久化或者说是序列化(不同翻译),我们要用这两个类,他们的构造函数接收目标流进来,而他们特有的读和写方法能把对象写到硬盘的文件上或者把硬盘上的文件读到堆内存中,从而完成数据的持久化。因为对象不是字符形式的,所以要用字节流。

注意:所操作的对象必须实现Serializable接口,意为可序列化,这个接口中没有任何方法,所以我们不用复写,直接拿过来用即可,这种没有自己方法的接口通常称为标记接口,顾名思义,标记接口的作用就在于给需要的对象盖一个戳。

 

类在编译的时候会生成一个uid,这个id是一个long类型的。当它创建对象并持久化时,这个uid会被随之写到文件上,而此时如果原来的类改变了,类中的uid也会随之改变(根据类中的成员算出来,每个成员都有一个数字标识),可文件里的持久化对象的uid没有改变,这样java就能知道。如果判断出来uid变了那么就不能拿类类型变量(新的类)指向一个旧的对象。会报异常InvalidClassException。

解决办法是不要让Java帮我们算uid,我们自己掌握主动权设置一个固定的uid。

静态是不能被序列化的,序列化只对堆内存有效。

如果想让非静态内容不被序列化,需要加个关键字修饰一下:transient表示透明的,虽然被修饰的内容也在堆内存当中,但是不会被序列化写到文件里。

 

同一个Object的流对象可以操作输入输出多个对象。每使用writeObject()方法一次就把一个对象序列化到指定文件里,而每readObject()一次就把一个对象读出来(按顺序,先入先出)。注意读出来的都是Object类的,我们要使用特有方法得强制转型。

 

readObject方法可能会产生一个ClassNotFoundException,因为有这样一种隐患:装在文件里的数据根本不是对象。所以一定要处理要么抛。

代码示例:

import java.io.*;
class ObjectStreamDemo 
{
	public static void main(String[] args) throws Exception
	{
//		writeObj();
		readObj();
	}
	public static void readObj() throws Exception
	{
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.object"));
		Person p = (Person)ois.readObject();
		System.out.println(p);
		ois.close();
	}
	public static void writeObj() throws IOException
	{
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.object"));
		oos.writeObject(new Person("张三",35,"bbbbb"));
		oos.close();
	}
}

管道流

个人理解:我们之前所学的流知识都是在同一个类底下操作输入和输出的,而有的时候我们需要在不同类不同对象之间传输数据,这就是管道流技术,给类与类之间提供数据通道。

PipedInputStream与PipedOutputStream

两个流不建议用单个线程操作,否则容易死锁。因为如前文所述,read方法是一个阻塞式方法,没有读到数据就会等。(所有read都一样,不仅仅是System.in),可以看做有一个线程在操作它,当有录入的时候被唤醒读取数据。

这两个流可以用构造方法相连接,也可以用它们共有的connect方法。但是两个管子只能A连B或者B连A,不能先A连B再B连A,这相当于连两次,会出现已经连接的异常提示。

代码演示:

import java.io.*;
class PipedStreamDemo 
{
	public static void main(String[] args) throws IOException
	{
		PipedInputStream in = new PipedInputStream();
		PipedOutputStream out = new PipedOutputStream();
		in.connect(out);//out.connect(in);//哪个连哪个都一样
		Read r = new Read(in);
		Write w = new Write(out);
		new Thread(r).start();
		new Thread(w).start();
	}
}
class Read implements Runnable
{
	private PipedInputStream in;
	Read(PipedInputStream in)
	{
		this.in = in;
	}
	public void run()
	{
		byte[] buf = new byte[1024];
		System.out.println("读取前,没有数据就阻塞");
		try//run方法没有抛,这里只能try
		{
			int len = in.read(buf);
			System.out.println("读到数据,阻塞结束");
			String str =new String(buf,0,len);
			System.out.println("正在处理,敬请期待");
			Thread.sleep(1500);
			System.out.println(str);
			in.close();
		}
		catch (Exception e)
		{
			throw new RuntimeException("管道流读取失败");
		}
	}
}

class Write implements Runnable
{
	private PipedOutputStream out;
	Write(PipedOutputStream out)
	{
		this.out=out;
	}
	public void run()
	{
		try
		{
			System.out.println("开始写入数据,等待3秒");
			Thread.sleep(3000);//会产生InterruptedException异常,为啥不用抛呢?
		}
		catch (InterruptedException e)
		{
			throw new RuntimeException("睡觉失败");
		}
		try
		{
			out.write("给我打电话".getBytes());
			out.close();
		}
		catch (IOException e)
		{
			throw new RuntimeException("管道流写入失败");
		}
		
	}
}

补充知识

DataInputStream与DataOutputStream

读写各种基本数据类型。虽然ObjectIn(Out)putStream, RandomAccessFile也都有这个功能,但以后凡是操作基本数据类型,专门用它!

除此之外还有个和编码表结合的方法readUTF()与writeUTF()该方法能以与机器的操作系统无关的方式使用 UTF-8 修改版编码将一个字符串写入基础输出流。所以如果用常用的UTF-8普通版或者系统默认的GBK都读不出来。

该方法会抛EOFException(如果此输入流在读取所有字节之前到达末尾),原因是不同编码表的大小都不一样,同样的文字,修改版比普通可能会多几个字节,这样的话当该方法期望读8个字节时只读到6个文件就结束了的话就会产生这个异常。

ByteArrayInputStream与ByteArrayOutputStream

可以直接操作字节数组中数据。

字节流内部也封装了数组,那为啥还要这货呢?

这两个流对象都操作数组,并没有调用底层资源,因为没有操作文件。因此关闭两个流无效(关闭流的目的就是为了释放流对底层资源的控制权),即使关闭了还可以继续使用流内的方法并不会抛异常。所以就不用抛异常了。

这两个流源和目的都是内存,与前面学过的流不一样。

ByteArrayInputStream在构造的时候,需要接收数据源,而数据源是一个字节数组。

另外ByteArrayOutputStream中缓冲区会随着数据的不断写入而自动增长。可使用 toByteArray() 和 toString() 获取数据,而不用flush,因为数据都不涉及底层资源。

因此ByteArrayOutputStream在构造时就不用定义数据目的地了,因为目的其实就是封装在它内部的可变长度的字节数组。

我们创造这两个流不仅仅是为了使用里面已经封装好了的对数组操作更方便的方法,而且其实更是用流的读写思想来操作数组。

ByteArrayOutputStream接收到的数据可以通过byte[]toByteArray()  方法输出到一个指定数组(还是在内存中的)。

如果想要让内存中的数据输出到其他地方,仅需使用ByteArrayOutputStream的writeTo(OutputStream out)方法,但该方法因为涉及到底层资源所以会抛异常IOException。只有该方法抛异常。

 

与上面字节数组的两个流相对应的是可以操作字符数组的CharArrayReader和CharArrayWriter,可以操作字符串的StringReader和StringWriter,其中Writer甚至还支持append方法,在数组的后面添加数组,在字符串后面添加字符串。

字符编码

字符流的出现是为了更方便操作字符。因为加入了编码转换。

两个加入了编码表的流对象:

InputStreamReader和OutputStreamWriter,用来在字节和字符之间转换。

还有两个加入了编码表:

PrintStream和PrintWriter,但他们只能输出而不能输入。

常见编码表:

ASCII:用一个字节的7位可以表示所有的字符(这就是为什么英文的字符占1个字节)

ISO8859-1:欧洲编码表,用一个字节的8位表示。高位为1,避免与ASCII冲突。

GBK2312:中文码表(包括符号一共6000-7000字)

GBK:中文码表的升级,用两个字节,每个字节高位都是1,因为GBK包含了ASCII码表,避免冲突。(但是GBK没有包含欧洲码表,所以不会冲突)(中文用GBK一般占2个字节)

Unicode:国际标准码,融合多种文字。所有文字都用两个字节来表示,Java语言使用的就是Unicode,字符char类型用的就是Unicode编码形式。Unicode编码前面两个字节规定的-2,-1作为标识符。

UTF-8(UnicodeTransform Format-8bit最少用8位一个字节):最多用三个字节来表示一个字符。比Unicode类型要优化,需要用几个字节装就用几个字节装,不浪费。在每一个字节的开头都加了一个标识头,使得非常容易区分什么编码是UTF-8的编码。全世界通用。(中文用UTF-8一般占3个字节)

 

这里产生一个问题:GBK与UTF-8都识别中文,但是每个中文对应的数字不一样,会出现乱码。

我们系统默认的是GBK

如上所述,UTF-8有一个字节的有两个字节的也有三个字节的,需要区分开来,所以设计在每个字节的前面加上标识头:

单个字节:字节首位为0;

双字节:第一个字节首位为110,第二个字节首位为10

三字节:第一个字节首位为1110,第二个字节首位为10

 

中文简体“联通”的gbk编码正好第一个字节是110…第二个字节是10…所以记事本在解码的时候就自以为是的按照utf-8解了,结果是乱码。

纯粹的巧合,这种情况很少见。

编码:字符串变成字节数组。

str.getBytes(StringcharsetName);可以默认用GBK,也可以我们指定的码表。

解码:字节数组变成字符串。

newString(byte[] bytes,String charsetName); 可以默认用GBK,也可以我们指定的码表。

另外还可以用到集合框架工具类Arrays.toString(byte[] bytes)方法。但是二者有区别(前面已经提到)。

后者将byte里面存的数字提取出来,转成String类型。

而前者将byte里面存的数字进行查表,并把对应的字母转成String类型。

这里需要注意,如果byte[]数组里面只有几个有内容,那么转成字符串后,会把数组里面的空元素也转出来。所以一般使用下面这种构造函数更好一些,指定长度,避免取得过度:

String(byte[]bytes, int offset, int length)

 

上述两个涉及到编码表的方法和构造函数必须解决异常UnsupportedEncodingException。要么抛要么try

 

注意:这个范例里面是 gbk编码-->iso解码-->iso编码-->gbk解码,但如果上面iso换成了utf-8则不行,因为我们传入的gbk编码utf-8识别不了,它就会自己填入一个其他的字符进去,而这个字符对应它的编码表的数字不是我们之前指定的,数字变了,就不可能还原了,再用utf-8编码也不是原来的数了,自然用gbk再转成字符也不是原来的字符了。

但如果是utf-8编码-->gbk解码-->gbk编码-->utf-8解码,是可以的,因为gbk不会瞎TM给查不到的编码数字配别的字符,这样数字还没变,能还原。

这种情况很常见,Tomcat通常都是iso8859-1,而中文的网站通常都是GBK或者UTF-8,所以我们传到服务器的数据直接被iso8859-1解码为欧文了,而我们要做的是先用iso8859-1编码还原为原来的byte数值,然后再用正确的GBK或者UTF-8解码还原为中文。

 

练习:有五个学生,每个学生有3门课的成绩

从键盘输入以上数据(包括姓名,三门课成绩),输入的格式:张三,30,40,50,计算出总成绩。并把学生的信息和计算出的总成绩按从高到低的顺序放在磁盘文件"stud.txt"中

步骤

1.描述学生对象。

2.定义一个可操作学生对象的工具类。

思想:

1.通过获取键盘录入一行数据,并将该行中的信息取出封装成学生对象。

2.因为学生有很多,那么需要存储,使用到集合,又因为要对学生总分排序,所以可以使用TreeSet

3.将集合的信息写入到一个文件中。

代码如下:

import java.io.*;
import java.util.*;
class StudentInfoTest
{
	public static void main(String[] args) throws IOException
	{
		Comparator<Student> cmp = Collections.reverseOrder();//空参数的话是反转接收这个参数的方法的compareTo方法。
		StudentInfoTool.writeToFile(StudentInfoTool.getStudentsSet(cmp));
	}
}
class Student implements Comparable<Student>
{
	private String name;
	private int math;
	private int chinese;
	private int english;
	private int sum;
	Student(String name, int math, int chinese, int english)
	{
		this.name = name;
		this.math = math;
		this.chinese = chinese;
		this.english = english;
		sum = math+chinese+english;
	}
	public String getName()
	{
		return name;
	}
	public int getSum()
	{
		return sum;
	}
	public int hashCode()
	{
		return name.hashCode()+sum*37;
	}
	public boolean equals(Object obj)//复写这个方法和上面的hashCode方法是为了以后这个类的对象可能会放到HashSet里面而预备的。
	{
		if (!(obj instanceof Student))
			throw new ClassCastException("比较对象不是学生类");//这个异常是RuntimeException的子类,不用在函数上声明。
		Student s = (Student)obj;
		return this.name.equals(s.name) && this.sum==s.sum;
	}
	public int compareTo(Student s)//这个方法是让分数从低到高排序
	{
		int num = new Integer(this.sum).compareTo(new Integer(s.sum));//this.sum是一个int类型的数据,不能使用compareTo方法,必须先变成Integer对象才可以。
		if (num==0)
			return this.name.compareTo(s.name);
		return num;
	}
	public String toString()
	{
		return "Student:["+"name:"+name+","+"Math:"+math+","+"Chinese:"+chinese+","+"English:"+english+"]";
	}
}
class StudentInfoTool
{
	public static Set<Student> getStudentsSet() throws IOException
	{
		return getStudentsSet(null);
	}
	public static Set<Student> getStudentsSet(Comparator<Student> cmp) throws IOException//如果是静态方法要使用泛型,得把泛型定义在方法上而不能定义在类上,因为类晚于静态加载。
	{
		BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
		String line = null;
		Set<Student> studs = null;
		if (cmp==null)
			studs = new TreeSet<Student>();
		else
			studs = new TreeSet<Student>(cmp);
		while ((line=bufr.readLine())!=null)
		{
			if (line.equals("over"))
				break;
			String[] strs = line.split(",");
			studs.add(new Student(strs[0], Integer.parseInt(strs[1]), Integer.parseInt(strs[2]), Integer.parseInt(strs[3])));//把两句话合成一句话了,有点长
		}
		bufr.close();
		return studs;
	}
	public static void writeToFile(Set<Student> stud) throws IOException
	{
		BufferedWriter bufw = new BufferedWriter(new FileWriter("stud.txt"));
		for (Student s : stud)
		{
			bufw.write(s.toString()+"\t");//制表符
			bufw.write(s.getSum()+"");//数字要转成字符才能往目标正确输出,否则调用的就是write(int i)的方法,会把i按照系统默认的GBK翻译成具体字符再写入文件。
			bufw.newLine();
			bufw.flush();//只要是用到缓冲就必须刷新,用到字符流也得刷新,俩条件都满足了,妥妥滴刷,否则数据进不去
		}
		bufw.close();
	}
}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值