您的位置:首页 > 编程语言 > Java开发

Java io功能总结分析(一):字节流读写

2013-06-13 20:47 447 查看
李青原(liqingyuan1986@aliyun.com)创作于博客园个人博客(http://www.cnblogs.com/liqingyuan/),转载请标明出处。

java.io是javaSE提供的基础功能,用以执行阻塞式的数据读取和写入操作。

java.io分为4个模块,字节流的读和写、字节流的读和写,分别对应4个基础抽象类:InputStream、OutputStream、Reader、Writer。

在这4个基础类之上,java使用装饰者模式,提供了面向多种应用场景的具体实现。本文主要是我对这些类个人学习分析的总结。

一.字节流读写:

1.UML:



2.代码分析:

  根据UML类图,我按照4个模块来分析:

  基础抽象类:定义基础API,实现部分默认实现,针对一个字节读写的原子操作方法为抽象方法,由核心功能类实现。

  核心功能实现类:针对内存和文件两种场景,实现针对一个字节的原子操作。

  装饰功能实现类:在核心功能实现类基础上,封装了一些可选的装饰功能。

  特殊实现类:针对特殊场景和特殊数据设计的特殊类,但是最基本的原子操作依然由核心功能类实现。

(1)基础抽象类:

  OutputStream和InputStream分别定义了3个读写方法,查看源代码会发现,3个方法都是以其中那个待实现的抽象方法为中心的,也就是write(int)和read()。

  这个方法就是字节流读写的原子操作,针对一个字节的读写,将由核心功能类来实现。

  区别在于,InputStream还规定了一种新的“标记和重置”功能,能够随时在流的当前读取位置打标记(mark方法),然后使用reset方法可以在读取到流的其他位置时重新回到标记位置,同时提供了markSupported方法供查看流对象是否支持此操作。

  不过InputStream本身只是定义了这些API,并未实现,markSupported方法返回的是false,需要子类自己实现。

  

  注意事项:

  ①write(int)的参数是4个字节的int类型,实现类在写入时会强转为1个字节的byte类型,这个强转会发生去掉溢出的3个字节,比如write(256),实际写入的是0x00,也就等效于write(0);

  ②read()方法返回的是4字节的int类型,API规定了这个方法会把读取的字节的高字节位置补零3个字节,因此如果读到数据,返回值必然在0到255之间,如果未读到返回-1;

  ③InputStream提供标记和重置功能API,是否支持由子类决定。

(2)核心功能实现类:

  所谓的核心功能实现类,也就是实现了最基础的写入功能——实现了原子操作write(int)和read()。

  A.内存读写:

  ByteArrayOutputStream实现内存字节流的写入。

  它内部持有一个数组,在构造方法中被初始化,默认是32长度,也可以通过构造方法指定长度。

  如何获得写入结果呢?

  原来,为了保证数据安全,ByteArrayOutputStream内部持有的数组是私有的,但它为我们提供了获得写入结果的方法:toByteArray(),获得内部数组的一个克隆,也可以使用toString方法获得编码后的字符流。

  示例:

public static void main(String[] args) throws UnsupportedEncodingException, IOException {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
byte[] bytes = "io测试".getBytes("utf-8");
byteStream.write(bytes);

System.out.println(new String(byteStream.toByteArray(),"utf-8"));//io测试
System.out.println(byteStream.toString("utf-8"));//io测试
}


  ByteArrayInputStream实现内存字节流的读取。

  因此ByteArrayInputStream构造方法必须至少有一个参数,传入数据来源byte数组;此核心类支持“标记和重置”功能。

  示例:

public static void main(String[] args){
byte[] sourceData = {1,2,3,4,5};
ByteArrayInputStream inputStream = new ByteArrayInputStream(sourceData);
byte[] targetData = new byte[5];

inputStream.read(targetData, 0, 3);            //从第一位读3个,分别是 1 2 3
inputStream.mark(-1);                        //打标记,mark方法的参数对ByteArrayInputStream无意义,可以随便传
targetData[3] = (byte)inputStream.read();    //继续读第四个数,4
inputStream.reset();                        //重置回标记位置,也就是3处
targetData[4] = (byte)inputStream.read();    //从标记出再开始读,依然是第四个数,4

System.out.println(Arrays.toString(targetData));//显示1,2,3,4,4
}


  

  B.文件读写:

  FileOutputStream实现文件写入。

  写入模式分为两种:覆盖或者追加,模式被构造方法的参数指定,默认是覆盖写入。

  示例:

public static void main(String[] args) throws UnsupportedEncodingException, IOException {
FileOutputStream fileStream = new FileOutputStream("d:/123.txt");//此时文件已经清空
fileStream.write(256);//256超出了byte类型的范围,发生溢出,实际写入的是0x00
fileStream.close();//释放系统文件资源和关闭FileChannel对象
}


  FileInputStream实现文件读取。

  因此构造方法必须至少有一个参数,指定数据来源文件,可以是文件对象,也可以是文件路径,还可以是FileDescriptor对象;此核心类不支持“标记和重置”功能。

  示例:

public static void main(String[] args) throws IOException{
FileOutputStream outputStream = new FileOutputStream("d:/123.txt");
outputStream.write(new byte[]{1,2,3,4,5});
FileInputStream inputStream = new FileInputStream("d:/123.txt");
byte[] targetData = new byte[4];

inputStream.read(targetData);//填满targetData,4个字节
System.out.println(inputStream.available());//写了4个字节,文件剩余可写数据为1
inputStream.close();//关闭文件资源
System.out.println(Arrays.toString(targetData));//1,2,3,4
}


  注意事项:

  ①内存读写已经保证了多线程安全,但是文件读写没有保证;

  ②内存读写不需要调用close方法;而文件读写使用文件系统,必须调用close方法释放文件资源;

  ③字节流核心写入类都不带缓存功能,因此直接使用时无需调用flush方法;

  ④[b]FileOutputStream对象在创建时,就会执行一个native的open操作,如果是覆盖模式,此操作会立刻清除原文件内容,因此创建覆盖模式的FileOutputStream对象要非常谨慎,就算我们没做任何写入操作,文件内容也会被覆盖。[/b]

  ⑤ByteArrayInputStream支持标记和重置功能,FileInputStream不支持;

  ⑥ByteArrayInputStream的mark(int)方法中,参数无意义,随便传。

(3)装饰功能类:

  装饰类本身并实现单字节写入的原子操作,而是通过持有2个核心类之一的对象来获得这种能力,并同时提供额外的附加功能。

  A.装饰类父类:

  作为装饰父类,FilterOutputStream和FilterInputStream最重要的作用就是把所有InputStream规定的API转到内部持有的InputStream对象上,本身只是一个很简单的封装层。  

  注意事项:

  FilterOutputStream在close方法中默认调用flush方法,这样在使用字节流写入的装饰子类时,无需在关闭前调用flush方法了。

  B.装饰功能——缓存读写:

  BufferedOutputStream实现带缓存功能的字节流写入。

  BufferedOutputStream通过内部持有一个字节数组来缓存外部请求写入的数据,以下三种情况,它都会清空数组并真正的把数据写入目标:

    缓存数组已满;

    缓存数组剩余空间不足以缓存新的请求写入的数据;

    flush或者close方法被调用时。

  BufferedInputStream实现带缓存功能的字节流读取。

  BufferedInputStream的缓存功能实现要复杂很多,主要是以下几个方面:

    BufferedInputStream构造方法中指定的缓存大小,只是初始大小;

    BufferedInputStream的缓存数组有一套“可伸缩”机制(参见源码fill方法);

    BufferedInputStream在以上基础上实现了基于缓存数组的标记和重置功能。

  考虑到内存读取和IO读取的消耗区别,BufferedInputStream通常是用来提升FileInputStream的读取性能的,而FileInputStream并未在JVM层面实现标记和重置功能,BufferedInputStream则在代码层面完善了这一功能。

  注意事项:

  ①缓存读写的装饰类都额外保证了读写操作的多线程安全性;

  ②缓存读写的缓存大小默认都是8KB,可以通过构造方法指定,但读取操作的缓存是可变的,使用中大小可能发生变化;

  ③缓存读取还附带实现了标记和重置功能,不同于ByteArrayInputStream的标记重置功能,BufferedInputStream读取提供的mark(int)的参数有意义,指定了标记在多少个字节内是起效的。

  示例:

public static void main(String[] args) throws IOException{
/*
* 缓存写入示范
*/
BufferedOutputStream bufferOutput = new BufferedOutputStream(new FileOutputStream("d:/123.txt"));
bufferOutput.write("IO测试".getBytes("utf-8"));
bufferOutput.close();//装饰类的close方法会先调用flush方法,所以无需再调用flush()

/*
* 缓存读取示范
*/
FileOutputStream outputStream = new FileOutputStream("d:/123.txt");
outputStream.write(new byte[]{1,2,3,4,5,6});

FileInputStream inputStream = new FileInputStream("d:/123.txt");
BufferedInputStream bufferInput = new BufferedInputStream(inputStream);

byte[] targetArray = new byte[6];
bufferInput.mark(targetArray.length);//在最开始位置标记
bufferInput.read(targetArray,0,3);//读取三个字节 123
bufferInput.reset();//重置,回到标记位置
bufferInput.read(targetArray,3,3);//再读取三个字节 还是123
bufferInput.close();
for(byte b : targetArray){
System.out.print(b);//循环输出为123123
}
}


  C.装饰功能——JAVA基本类型读写:

  DataOutputStream:提供JAVA基本类型的写入API,可以实现把JAVA基本类型的二进制数据写入目标,同时提供获得已写入数据字节大小的API。  

  DataInputStream:提供JAVA基础类型的读取API,会把读取的字节转为对应类型的数据,根据类型的长度,会选择连续操作几次原子操作。

  注意事项:

  ①JAVA基本类型读写的API都没有保证多线程安全,需要外部调用者自己保证;

  ②DataOutputStream的基本写入(3个write方法)额外保证了多线程安全,而DataInputStream的基本读取(3个read方法)则没有额外保证,完全取决于内部持有的核心实现类;

  ③DataOutputStream的size()方法最大只能提供2的31次方减1,超过这个依然只显示这个数值。

  示例:

public static void main(String[] args) throws IOException {
/*
* JAVA基本类型写入示范
*/
DataOutputStream dataOutput = new DataOutputStream(new FileOutputStream("d:/123.txt"));
dataOutput.writeInt(-1);//文件中写入的16进制数据是 FF FF FF FF,也就是32个1(-1的补码)
dataOutput.writeBoolean(true);//文件中写入的16进制数据是 01
System.out.println(dataOutput.size());;//已写字节,4+1 已写5字节,最大只能记录Integer.MAX_VALUE
dataOutput.close();

/*
* JAVA基本类型读取示范
*/
FileOutputStream outputStream = new FileOutputStream("d:/123.txt");
outputStream.write(new byte[]{0x00,0x31});//写入 0x0031,也就是49
FileInputStream inputStream = new FileInputStream("d:/123.txt");
DataInputStream dataInput = new DataInputStream(inputStream);
System.out.println(dataInput.readChar());//显示1 DataInputStream会连续读取2个字节,0x0031转为字符也就是'1'
dataInput.close();
}


  D.装饰功能——字符输出打印:

  PrintStream是一个非常特殊的类,它虽然和Writer的子类一样能打印字符流,但是它的底层却是以字节为原子操作的,效率上不如PrintWriter,但是它依然能操作字节写入,这点上又更灵活。

  作为打印类,PrintStream提供换行功能,除了调用flush和println类方法外,它还能自动识别换行字符'\n'(这点PrintWriter做不到);同时PrintStream也不会抛出IO异常,而是通过checkError方法获得是否存在错误。

  注意事项:

  ①PrintStream能够识别换行字符'\n',PrintWriter不能;

  ②System.out就是一个PrintStream对象;

  ③PrintStream额外保证了所有API的多线程安全。

  示例:

public static void main(String[] args) throws UnsupportedEncodingException, IOException {
PrintStream printStream = new PrintStream
(new BufferedOutputStream(new FileOutputStream("d:/123.txt")));//封装了缓存功能的打印流
printStream.print("IO测试\n");//包含换行符,此行已经输入文件
printStream.print("IO测试");//此行还未输入
System.out.println(printStream.checkError());//无错误,显示false
printStream.close();//关闭时默认调用了flush方法,第二行也被输入
}


  E.装饰功能——字节流读取时的回退功能:

  PushbackInputStream:提供回退功能。

  PushbackInputStream通过内部一个回退区的缓存数组,可以让我们把已经读出来的数据回写到流中,以供其他使用者或者下次读取使用。

  注意事项:

  ①PushbackInputStream可以设置回填区的大小(默认1),如果回填数据长度超过设置值,会抛出IO异常;

  ②PushbackInputStream没有提供额外多线程安全保证,完全取决于内部持有的核心实现类。

  

  示例:

public static void main(String[] args) throws IOException{
FileOutputStream outputStream = new FileOutputStream("d:/123.txt");
outputStream.write(new byte[]{1,2,3,4,5,6});

FileInputStream inputStream = new FileInputStream("d:/123.txt");
PushbackInputStream pushbackStream = new PushbackInputStream(inputStream);

System.out.print(pushbackStream.read());
System.out.print(pushbackStream.read());
System.out.print(pushbackStream.read());
System.out.print(pushbackStream.read());
System.out.print(pushbackStream.read());
System.out.print(pushbackStream.read());//读完六次 分别是123456

System.out.print(pushbackStream.read());//无数据,显示-1

pushbackStream.unread(0);//回填一个0
System.out.print(pushbackStream.read());//读出0

pushbackStream.close();
}


  F.装饰功能——字节流读取时的行数计数功能:

  LineNumberInputStream:一个过时的提供行数计算的装饰类,这个类是JAVA SE中罕见的存在错误的类,主要是把0x0A这个字节错误的和'\n'换行符等同处理了,'\r'也存在类似问题。

  注意事项:

  LineNumberInputStream在某些情况下存在错误,应当使用LineNumberReader替代。

  错误示例:

@SuppressWarnings("deprecation")
public static void main(String[] args) throws IOException{
FileOutputStream outputStream = new FileOutputStream("d:/123.txt");
outputStream.write(new byte[]{0x0a});
FileInputStream inputStream = new FileInputStream("d:/123.txt");
LineNumberInputStream lineStream = new LineNumberInputStream(inputStream);
System.out.println(lineStream.getLineNumber());//0
lineStream.read();
System.out.println(lineStream.getLineNumber());//1 很明显错误,0x0a并不一定是换行符,换行符应该是0x000a才对
lineStream.close();
}


(4)特殊实现:

  A.序列化对象二进制流的读写:

  ObjectOutputStream和ObjectInputStream依然通过核心类来实现字节写入的原子操作,但是不再直接参与装饰者模式,而是通过内部类来持有一个JAVA基本类型读写装饰类。

  由于目标不再是普通字节流,而是特殊的序列化对象的二进制字节流,所以它们的API也不一样,提供了针对各种类型的读取API。

  注意事项:

  ①ObjectOutputStream为了提高序列化效率,在序列化对象时,会通过内存地址来判断是否为同一个对象,重复对象的序列化会用引用来代替;

  ②规则①对于对象内部的对象一样适用;

  ③使用writeUnshared方法可以强制要求不使用引用方式序列化,但是对于对象内部的对象此方法无效;

  ④ObjectInputStream在读取时,同一类型数据会依次读取,比如连续2次调用readObject()会依次获得第一次写入和第二次写入的Object对象。

  示例:

@SuppressWarnings("deprecation")
public static void main(String[] args)
throws UnsupportedEncodingException, IOException, ClassNotFoundException {
ObjectOutputStream objectOutput = new ObjectOutputStream(new FileOutputStream("d:/123.txt"));
Date a = new Date("2013/01/01");
objectOutput.writeObject(a);//第一次写入对象本身
a.setHours(12);
objectOutput.writeObject(a);//第二次写入,是重复对象,无视修改,依然写入引用
objectOutput.writeUnshared(a);//第三次写入,不管是否重复都写入对象
objectOutput.close();

ObjectInputStream objectInput = new ObjectInputStream(new FileInputStream("d:/123.txt"));
System.out.println(objectInput.readObject());//Tue Jan 01 00:00:00 CST 2013
System.out.println(objectInput.readObject());//Tue Jan 01 00:00:00 CST 2013 第二次写入修改未起效,因为是引用
System.out.println(objectInput.readObject());//Tue Jan 01 12:00:00 CST 2013 第三次写入修改起效
}


  

  B.多来源读取:

  SequenceInputStream用来从多个数据来源,读取字节流,当一个来源读取到末尾,会自动切换到下个数据来源。

  示例:

public static void main(String[] args) throws IOException{
FileOutputStream outputStream = new FileOutputStream("d:/123.txt");
outputStream.write(new byte[]{1,2,3});//第一个文件写入123
FileOutputStream outputStream2 = new FileOutputStream("d:/456.txt");
outputStream2.write(new byte[]{4,5,6});//第二个文件写入456

FileInputStream inputStream = new FileInputStream("d:/123.txt");
FileInputStream inputStream2 = new FileInputStream("d:/456.txt");
InputStream[] array = new InputStream[]{inputStream,inputStream2};
ArrayEnumeration inputEnum = new ArrayEnumeration(array);
SequenceInputStream sequenceStream = new SequenceInputStream(inputEnum);
byte[] target = new byte[6];

for(int i = 0; i < target.length; i++){
System.out.println(sequenceStream.read());//循环输出123456
}
sequenceStream.close();
}


  注意事项:SequenceInputStream存在一个BUG,如果调用多字节读取的方法,自动切换功能会失效。

  错误示例:

public static void main(String[] args) throws IOException{
FileOutputStream outputStream = new FileOutputStream("d:/123.txt");
outputStream.write(new byte[]{1,2,3});//第一个文件写入123
FileOutputStream outputStream2 = new FileOutputStream("d:/456.txt");
outputStream2.write(new byte[]{4,5,6});//第二个文件写入456

FileInputStream inputStream = new FileInputStream("d:/123.txt");
FileInputStream inputStream2 = new FileInputStream("d:/456.txt");
InputStream[] array = new InputStream[]{inputStream,inputStream2};
ArrayEnumeration inputEnum = new ArrayEnumeration(array);
SequenceInputStream sequenceStream = new SequenceInputStream(inputEnum);
byte[] target = new byte[6];
sequenceStream.read(target);

for(byte b:target){
System.out.println(b);//错误显示123000 而不是123456
}
}


  查看源码,发现SequenceInputStream内部的错误:



  

  D.字符串读取操作:

  StringBufferInputStream也是一个过时的类,由于不恰当的混淆了双字节的字符类型和单字节的byte类型,读取的数据并不能反映真正的字符二进制数据。

  注意事项:

  应当使用StringReader替代StringBufferInputStream。

  E.管道读写操作:

  PipedOutputStream必须和PipedInputStream一一对应的使用,主要是用作线程之间的数据转移,具体的使用将在IO篇的后续第三篇文章中详细介绍。

3.总结:

  (1)字节流的读写在JAVA的最初版本就已经提供,但读取操作中,个别读取类不恰当的处理了字符和字节的关系,导致存在BUG,应当使用1.1版本以后提供的字符流替代类来操作。

  (2)由于使用装饰者模式,很多时候我们可以嵌套核心类和装饰类来使用,这样构建的对象应该同时小心核心类和装饰类的注意事项。

  (3)多线程环境下,内存字节流读写本身已经保证了多线程安全;

    而文件字节流读写则没有保证,但如果使用缓存读写装饰类封装,则能够通过缓存读写装饰类来保证多线程安全,前提是一个文件读写类只被一个缓存读写装饰类使用;

    装饰类提供的JAVA基本数据类型的各种读取API,都没有保证线程安全性,需要调用者自己保证。

  (4)IO功能中的字节流读写都是阻塞式的,会一直等到操作成功或者抛出IO异常,才会返回。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: