java IO流(上)

本文总结java IO流对象的学习,java中IO流对象种类繁多,很容易搞混不知道该用哪一个,所以这里总结学习时,将每种流对象的构造方法和读写方法都记录在此,以方便查阅,加强记忆。

IO流
IO流用来处理设备(硬盘,内存等)之间的数据。
java对数据的处理是通过流的方式,java操作流的对象都在IO包中。

流按流向分为输入流和输出流,按操作数据分为字节流和字符流,字节流是二进制流,可以处理一切数据,字符流是为了方便处理我们平时常见的文本数据而分裂出来的。

IO流基类
字节流的抽象基类:InputStream, OutputStream.
字符流的抽象基类: Reader, Writer.
由这4个子类派生出来的子类名称以其父类名作为其后缀,子类的前缀表示该流对象的功能。

字符流
字符流抽象基类是Writer, 构造方法是protected权限的,只供子类使用。常见的字符流有FileWriter和FileReader。

FileWriter
从继承关系上往上追溯,依次是FileWriter->OutputStreamWriter->Writer.
FileWriter使用步骤:
1. 创建一个FileWriter对象,并明确数据要存放的目的地。
FileWriter常用构造函数:
FileWriter(File file), 根据指定的文件对象创建FileWriter对象。
FileWriter(File file, boolean append),  append代表是覆盖已有文件,还是不覆盖在原文件上追加文本。
FileWriter(String fileName), 根据指定的文件名创建FileWriter对象。如果fileName中包含路径,windows下路径分隔符要用“\\”,linux下路径分隔符是"/",或者通用File.separator, 它代表了不同系统的默认分隔符。
FileWriter(String fileName, boolean),  根据指定文件名创建FileWriter对象,不覆盖原文件。

2. 写入数据
调用write()方法,它有5种重载形式,都是从父类继承的,可以接收不同形式的数据,这5种重载形式在Writer类中都有定义,OutputStreamWriter类中复写了其中3个。
void write(char[] cbuf, int off, int len),Writer类中是抽象方法,在OutputStreamWriter类中被复写。
void write(char c), 写入一个字符,在OutputStreamWriter类中被复写。
void write(String str, int off, int len), 写入字符串的一个子串,在OutputStreamWriter类中被复写
void write(char[] cbuf), 写入一个字符数组。
void write(String str), 写入一个字符串。

3. 刷新流对象缓冲区中的数据到目的地
void flush(), 继承自OutputStreamWriter类,是Writer类中的抽象方法,刷新后流可以继续使用。

4. 关闭流对象
 java是在调用windows系统内部方法写入硬盘数据,写入后使用的windows资源需要关闭,调用close()方法。
close()关闭流对象,但是关闭前会刷新一次内部缓冲区中的数据,将数据刷到目的地中。

FileWriter的以上方法使用时都会抛出IOException异常,对IOException的专业处理在后面的例子中一并演示。

FileReader
从继承关系上往上追溯,依次是FileReader->InputStreamReadr->Reader.
FileReader使用步骤
1. 创建一个文件读取流对象,和指定名称的文件相关联
FileReader常用构造函数:
FileReader(File file), 根据指定的File对象创建文件读取流。
FileReader(String filename), 根据指定的文件名创建文件读取流。

2. 读取流对象中的数据
调用read方法,该方法有4种重载形式,继承自InputStreamReader或Reader类,下面前2种方法在InputStreamReader类中已被重写:
int read(), 读取字符,并返回字符的ASCII码值,一次读一个字符,并自动往下读,如果已到达流的末尾,则返回-1.
int read(char[] cbuf, int off, int len), 将字符读到目的数组中的某一部分,返回读取到的字节数,如果已到达流的末尾,返回-1。
int read(char[]), 将字符读到目的数组,返回读取到的字节数,如果已到达流的末尾,返回-1。
int read(CharBuffer target), 将字符读入指定的字符缓冲区,返回添加到缓冲区的字符数量,如果此字符源位于缓冲区末端,则返回 -1 。

3. 关闭输入流
调用void close()方法。

FileReader的以上方法使用时也都会抛出IOException异常。

字符流对象都是有缓冲区的,字符流底层读写也是一个字节一个字节地来读写的,但因为字符流有编码表,读数据时需要将读到的字节先缓存起来,等凑够指定编码表中一个字符所需求的字节数时,才对照查看编码表,返回这些字节对应的字符;类似地,写数据时,是先查码表得到一个字符对应的二进制字节数据,写到缓冲区中,然后刷新后缓冲区后再一个字节一个字节的写到目的地。
FileWriter和FileReader中固定使用默认编码,也就是调用System.getProperties()方法得到的encoding属性值,要使用非系统默认的其它编码,需要使用InputStreamReader和OutputStreamWriter.

字符流的缓冲区-BufferedWriter和BufferedReader
BufferedWriter和BufferedReader类分别是Writer和Reader类的直接子类。
BufferedWriter,将文本数据写入到字符输出流,并缓存各个字符,提供字符、字符数组、字符串的较高效率写入方式。对应的有BufferedReader类。
缓冲区的出现避免了磁盘磁头(或其它设备)的频繁擦写,提高了对数据的读写效率。
字符流缓冲区使用步骤:
1. 创建一个字符输出/输入流,如FileWriter/FileReader对象。
缓冲区的出现是为了提高流的操作效率,所以在创建缓冲区之前,必须先有流对象。
BufferedWriter类的构造函数有:
BufferedWriter(Writer w), 接收任何Writer的子类对象。

BufferedWriter(Writer w, int sz), 创建指定缓冲区大小的缓冲字符输出流。
BufferedReader类的构造函数有:
BufferedReader(Reader in), 接收任何Reader的子类对象。
BufferedReader(Reader in, int sz), 创建指定缓冲区大小的缓冲字符输入流。

2. 创建缓冲区
BufferedWriter有2种构造函数,都要传入上一步中创建的输出流对象:
BufferedWriter(Writer out), 可以接收Writer的任意子类对象。
BufferedWriter(Writer out, int sz), sz指定缓冲区的大小。
BufferedReader类也是有如上2种构造函数。

3. 写入/读取数据
BufferedWriter和BufferedReader直接继承自字符流基类,并如FileWriter类和FileReader类一样,都复写了部分write和read方法,前面说的FileWriter和FileReader类的所有读写方法,对缓冲区都是可以一样使用。
此外,BufferedWriter类还有一个特有方法:void  newLine(), 写入一个行分隔符,windows下写入\r\n, linux写入\n.
BufferedReader类有一个特有方法是:String readLine()方法。
readLine()方法一次读取一行文本,不包含任何行终止符(\r,\n), 返回读到的文本行字符串,读到末尾时返回null.

4. 刷新数据
void flush(), 只要用到缓冲区,就要刷新。

5. 关闭缓冲区
void close(), 关闭缓冲区,实际就是在关闭缓冲区中的流对象,所以关闭缓冲区后,不用再关闭流对象。

BufferedReader是对Reader功能的一种增强。

下面用一个自定义缓冲字符输入流来演示readLine()的原理,以及IOException异常的专业处理方式。

import java.io.*;
class MyBufferedReader{
	private FileReader fr;
	MyBufferedReader(FileReader fr){
		this.fr=fr;
	}
	public String myReadLine() throws IOException
	{
		//定义一个临时容器。原BufferedReader封装的是字符数组,
		//这里为了方便,直接定义一个StringBuilder容器,因为最终还是要将数据变成字符串。
		StringBuilder sb=new StringBuilder();
		int ch=0;
		while((ch=fr.read())!=-1){
			if(ch=='\r')
				continue;
			if(ch=='\n')
					return sb.toString();
			else
				sb.append((char)ch);
		}
		//处理字符流最后没有换行符的情况,读到最后一行时,只要sb不为空就返回。
		if(sb.length()!=0)
			return sb.toString();
		return null;
	}
	public void myClose() throws IOException
	{
		fr.close();
	}	
}
public class MyBufferedReaderDemo{
	public static void main(String[] args){
		MyBufferedReader myBuf=null;
		try{
			FileReader fr=new FileReader("buf.txt");
		    myBuf=new MyBufferedReader(fr);
		    String line=null;
		    while((line=myBuf.myReadLine())!=null){
		    	System.out.println(line);
		    }			
		}
		//以下是IOException异常的专业处理。
		catch(IOException e){
			throw new RuntimeException(e);
		}
		finally{
			if(myBuf!=null){//如果myBuf为null,是不能调用myClose()方法的,所以这里需要判断;如果程序中打开多个流,需要分开多个if语句判断,不能写在一块。
				try{
					myBuf.myClose();
				}
				catch(IOException e){
					throw new RuntimeException(e);
				}				
			}
		}		
	}
}

装饰设计模式
当想要对已有的对象进行功能增强时,可以定义类,将已有对象传入,基于已有的功能,并提供加强功能,这种方式称为装饰设计模式,新定义的类称为装饰类。装饰类和被装饰的类通常都属于同一个体系中(归属同一个类或接口)。
装饰与继承的区别:子类继承父类,子类复写父类中的方法也可以增强父类的某功能,但如果该功能体系中又新增一个类时,要增强这个新增类的该功能,又要重新定义子类继承该类,这样使用扩展性极差,体系臃肿;而装饰设计模式,只需找到参数的共同类型,通过多态的形式,传入被增强的对象,提高了扩展性。

带行号的缓冲区-LineNumberReader
LineNumberReader类,是BufferedReader类的子类,也是一个装饰类,构造方法同BufferedReader类。
LineNumberReader类中定义了方法setLineNumber(int)和getLineNumber(),可分别用于设置和获取当前行号。默认情况下,行号从0开始编号,行号随数据读取在每个行结束符处(\n,\r,\r\n)递增;setLineNumber()不会实际改变流中的当前位置,只更改getLineNumber()的返回值。

可以自己实现一个MyLineNumberReader类,继承MyBufferedReader类:

import java.io.*;
public class MyLineNumberReader extends MyBufferedReader
{
	private int lineNumber;
	MyLineNumberReader(FileReader fr){
		super(fr);
	}
	public String myReadLine() throws IOException
	{		
		lineNumber++;
		return super.myReadLine();		
	}
	public void setLineNumber(int lineNumber){
		this.lineNumber=lineNumber;
	}
	public int getLineNumber(){
		return lineNumber;
	}
}
字节流
字节流的抽象基类是InputStream和OutputStream. 字节流可以处理任意形式的数据,包括文本、图片、声音等。

文件字节流-FileInputStream和FileOutprtStream
分别直接继承自InputStream和OutputStream类,这2个类的构造函数形式与FileReader和FileWriter类完全相似。

FileOutputStream类,写入字节流,类中write()方法的重载形式有:
void write(byte[] b), 写入字节数组;
void write(byte[] b, int off, int len), 写入字节数组的一部分;
void write(int b), 写入指定的字节,强制类型转换,将int类型数值的最低8位写入。
字节流写入时不需要刷新,每读取到一个字节时不缓存,直接写入到磁盘等目的地。

FileInpotStream类,读取字节流,类中read()方法的重载形式有:
int read(), 读取一个字节,自动提升为int类型值返回,之所以返回int值,是为了在读取到值为11111111的字节时,可以将返回值与255进行&运算,这样不会直接返回-1而导致文件循环读取异常终止;
int read(byte[] b), 将数据读入到一个字节数组中,返回读入到缓冲数组的字节数,到达文件末尾时返回-1;
int read(byte[] b, int off, int len), 将数据读入到一个字节数组的指定位置上,返回读入到缓冲数组的字节数,到达文件末尾时返回-1。
FileInputStream字节读取流的一个特有方法:int available(), 返回流中可读取的剩余字节数,包括换行和回车符
可以使用available()方法指定一个大小刚刚好的字节缓冲数组,不用再循环读取,但待读取文件较大时,用此方法可能导致内存溢出,慎用,建议还是用指定1024大小的字节数组加循环的方式。

/*
 复制图片
 */
import java.io.*;
public class CopyPicture {
	public static void main(String[] args) {
		FileOutputStream fos=null;
		FileInputStream fis=null;
		try{
			fos=new FileOutputStream("F:\\2.jpg");
			fis=new FileInputStream("F:\\1.jpg");
			byte[] buf=new byte[1024];
			int len=0;
			while((len=fis.read(buf))!=-1){
				fos.write(buf,0,len);
			}
		}
		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("写入文件失败");
			}			
		}
	}
}
用字符流也可以复制图片,但复制得到的图片可能是乱码或打不开,因为字符流需要查编码表,几个字节在编码表中找不到对应字符时,进入编码表的未知编码区域,就导致编码错乱,且写入时无法复原成原字节。

字节流的缓冲区
BufferedInputStream和BufferedOutputStream, 分别是FileInputStream和FileOutputStream类的子类。 
字节流缓冲区原理:BufferedInputStream对象内部有一个缓冲区数组--byte[1024](默认大小),读取输入流中数据时会根据需要一次填充多个字节到该数组中;BufferedOutputStream可以将各个字节写入底层输出流,不必针对每次字节写入调用底层系统。使用时不需要再手动定义字节数组,直接调用write()和read()方法即可,都是在内存中的操作。

下面的代码中演示了自定义字节流缓冲区,从中可以深入的理解字节流的write()方法的为什么使用int型参数,read()方法的返回值为什么也是int型值:

import java.io.*;
class MyBufferedInputStream{
	private InputStream in;
	private byte[] buf=new byte[1024];
	private int pos=0, count=0;
	MyBufferedInputStream(InputStream in){
		this.in=in;
	}
	public int myRead() throws IOException
	{
		if(count==0){
			count=in.read(buf);
			if(count<0)
				return -1;
			pos=0;
			byte b=buf[pos];
			count--;
			pos++;
			return b&255;//in字节流读到一个字节为11111111时,将这个字节提升为32位的int类型返回,但byte提升为int时,高位补的是1,所以值还是-1,而字节流实际还没读到结尾处,所以这里read()返回的int值必须与255进行&运算,这样既保留原字节数据不变,又可以避免-1的出现。
		}
		else if(count>0){
			byte b=buf[pos];
			count--;
			pos++;
			return b&0xff;
		}
		return -1;
	}
	public void myClose() throws IOException
	{
		in.close();
	}
}

public class MyBufferedInputStreamDemo{
	public static void main(String[] args) throws IOException
	{
		long start=System.currentTimeMillis();
		copy_1();
		long end=System.currentTimeMillis();
		System.out.println((end-start)+ "毫秒");
	}
	public static void copy_1() throws IOException
	{
		MyBufferedInputStream bis=new MyBufferedInputStream(new FileInputStream("F:\\1.jpg"));
		BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream("F:\\2.jpg"));
		int by=0;
		while((by=bis.myRead())!=-1){
			bos.write(by);
		}
		bis.myClose();
		bos.close(); 
	}
}
其它几个常用的流对象
标准输入输出流-System.in和System.out
System.in, 对应的标准输入设备:键盘, 是System类中InputStream类型的静态成员。
System.out, 对应的标准输出设备:控制台,是System类PrintStream类型的静态成员。
System类中还提供了改变标准输入输出设备的方法: setIn(InputStream in)和setOut(PrintStream out).

转换流-InputStreamReader和OutputStreamWriter
InputStreamReader是字节流通向字符流的桥梁,可以使用指定的字符集读取字节并将其编码为字符。
构造函数重载形式有:
InputStreamReader(InputStream in), 使用系统默认字符集。
InputStreamReader(InputStream in, charSet cs), 使用给定字符集cs。
InputStreamReader(InputStream in, String charsetName), 用字符串形式指定字符集。
InputStreamlReader(InputStream in,charsetDecoder), 使用给定字符集解码器。
OutputStreamWriter是字符流通向字节流的桥梁,可使用指定的字符集将要写入流中的字符编码成字节,构造函数重载形式与InputStreamReader一致。
下面是通过转换流来实现键盘录入及控制台打印的一个例子,其实转换流最重要的用法是指定编码表读写数据:

/*
 * 读取键盘输入,每读取一行就转换成大写打印在控制台上,读到"over"时结束
 */
import java.io.*;
public class TransStreamDemo {
	public static void main(String[] args) throws IOException
	{
		//键盘录入都是文本数据,首先将字节流对象System.in封装成字符流对象,为了提高效率将字符流使用缓冲区技术进行装饰,这样可以直接使用BufferedReader对象的readLine()方法。
		BufferedReader bufr=new BufferedReader(new InputStreamReader(System.in));
		//对标准输出流对象System.out进行类似的封装和装饰
		BufferedWriter bufw=new BufferedWriter(new OutputStreamWriter(System.out));
		String line=null;
		while((line=bufr.readLine())!=null){
			if("over".equals(line))//读取屏幕上键盘输入时,必须设定结束标志,否则只能通过ctrl+C来结束程序了。
				break;
			bufw.write(line.toUpperCase());
			bufw.newLine();
			bufw.flush();
		}
	}
}
打印流-PrintStream和PrintWriter
类中的print()和println()方法有多种重载形式,可以将各种数据类型的数据都原样打印。
PrintStream是FileOutputStream的子类,其构造函数的重载形式有:
PrintStream(File file), 创建具有指定文件且不带自动行刷新的新打印流。
PrintStream(File file, String csn), csn表示指定字符集,不带自动行刷新。
PrintStream(OutputStream out), 接收字节流的任意子类对象。
PrintlStream(OutputStream out, boolean autoFlush), 自动行刷新。
PrintStream(OutputStream out, boolean autoFlush, String encoding), 自动行刷新,指定字符集。
PrintStream(String fileName), 指定文件名称,不带自动行刷新。
PrintStream(String fileName, String csn), 指定文件名称,自动行刷新。
上述构造函数中,autoFlush为true时,每当写入 byte 数组、调用其中一个println 方法或写入换行符或字节 ('\n') 时都会刷新输出缓冲区。
PrintWriter是Writer的子类,其构造函数的重载形式除可接收上述所有类型数据外,还可接收字符输出流:
PrintWriter(Writer out), 不带自动行刷新。
PrintWriter(Writer out, boolean auotFlush), 可以自动行刷新。
PrintWriter对象的自动刷新只针对println、printf、或format方法打印数据时才会自动刷新,调用PrintWriter的write()方法时,缓冲区不会自动刷新,还是需要手动调用flush()方法刷新。

下面一个例子中演示之前学过的Exception异常信息如果打印到指定位置,这里面就用到PrintStream流:

/*
 * 将角标越界异常信息打印到指定文件中。
 */
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ExceptionInfo {
	public static void main(String[] args) {
		try{
			int[] arr=new int[2];
			System.out.println(arr[3]);
		}
		catch(Exception e){
			try{
				Date d=new Date();
				SimpleDateFormat sdf=new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
				String s=sdf.format(d);
				PrintStream ps=new PrintStream("exception.log");
				ps.println(s);
			}
			catch(IOException ex){
				throw new RuntimeException("日志文件创建失败!");
			}
			/*
			 * Throwable接口有中printStackTrace方法有3种重载形式,分别可接收空参数、PrintStream对象、PrintWriter对象,下面这句代码就是接收的PrintStream对象。
			 */
			e.printStackTrace(System.out);
		}
	}
}
最后的总结--流操作规律
java IO流使用最痛苦的就是流对象很多,不知道该用哪一个。需要通过三个明确来完成。
1. 明确源和目的
源:是输入流,可用InputStream、Reader
目的:是输出流,可用OutputStream、Writer

2. 操作的数据是否是纯文本
是:用字符流
不是:用字节流

3. 当体系明确后,再明确使用哪个具体的对象
通过设备来进行区分:
源设备:内存(ByteArrayInputStream等)、硬盘(FileReader/FileInputStream)、键盘(System.in)
目的设备:内存(ByteArrayOutputStream等)、硬盘(FIleWriter/FileOutputStream)、控制台(System.out)

另,还得明确是否需要提高效率,读取数据较少时,一般不需要注重效率,如果需要就要加入缓冲技术进行装饰;如果读取或写入数据时需要指定编码表,只能使用转换流InputStreamReader或OutputStreamWriter.




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值