疯狂java


您现在的位置: 疯狂软件 >> 新闻资讯 >> 正文

Java IO字节流、字符流的选择规律


 

   

  字节流、字符流涉及的类比较多,比较容易混淆。因此,有必要针对何时使用字节流、何时使用字符流、何时使用Buffer类的流做一个归纳。要归纳它们,无需过多的语言,只需抓住它们的重点和特性即可。

  在决定何时使用何种类时,以下几个问题需要考虑清楚。

  是否有数据源、数据的流向是否有目标。

  数据源:表示输入,或称为读。可提供使用的两个父类为InputStream和Reader。

  有目标:表示输出,或称为写。可提供使用的两个父类为OutputStream和Writer。

  应该使用字节流还是字符流?如果源或目标包含非ascii字符,则采用字符流。

  源和目标是何种设备类型。

  源 :磁盘文件File,内存(字节/字符数组),键盘System.in,网络socket

  目标:磁盘文件File,内存(字节/字符数组),屏幕System.out,网络socket

  是否需要使用额外的特殊功能,包括操作行,字符集转换,使用缓冲区提高效率,串联多个字节输入流,保证数据类型不变,保证数据字面意义等等。最后还需要考虑字节流转换为字符流的问题。

  最后,需要知道的是对于使用BufferedReader的输入流,有时候可以考虑使用字符数组可能效果和性能更好。

  以下是一个应用以上规律的需求示例:读取包含gbk简体中文的文件数据,并以utf-8编码复制到另一个文件中。

  //1.有源有目标,且都是文件。

  //2.读取和写入都包含中文字符,所以采用字符流。

  //3.写入过程中需要转码,因此需要使用OutputStreamWriter。

  //4.可以使用缓冲区功能提高效率。

  import java.io.*;

  public class CP {

  public static void main(String[] args) throws IOException {

  File src = new File("d:/myjava/a.txt");

  File dest = new File("d:/myjava/a_bak.txt");

  cp(src,dest);

  }

  public static void cp(File src,File dest) throws IOException {

  BufferedReader bufr = new BufferedReader(new FileReader(src));

  BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(dest),"utf-8"));

  //按行读取

  String line = null;

  while((line=bufr.readLine())!=null) {

  bufw.write(line);

  bufw.newLine();

  bufw.flush();

  }

  bufw.close();

  }

  }

  上述代码执行后,目标文件中的末尾将比源文件多一个空行,上述方法对这个问题不是很好解决。但如果使用字符数组来替代BufferedReader,则没有这样的问题,如下。

  import java.io.*;

  public class CP {

  public static void main(String[] args) throws IOException {

  File src = new File("d:/myjava/a.txt");

  File dest = new File("d:/myjava/a_bak.txt");

  cp(src,dest);

  }

  public static void cp(File src,File dest) throws IOException {

  FileReader fr = new FileReader(src);

  BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(dest),"utf-8"));

  char[] buf = new char[1024];

  int len = 0;

  while ((len=fr.read(buf))!=-1) {

  bufw.write(buf,0,len);

  bufw.flush();

  }

  bufw.close();

  }

  }

  java IO(六):额外功能处理流

  额外功能处理流的意思是在基础流(InputStream/OutputStream/Reader/Writer)的基础上提供额外的功能。常见的额外功能可归纳为以下几种。

  是否使用缓冲功能:BufferedInputStream/BufferedOutputStream/BufferedReader/BufferedWriter(字符流的缓冲对象还提供了操作行的方法)

  是否串联多个字节输入流:SequenceInputStream

  是否使用对象序列化功能:ObjectInputStream/ObjectOutputStream(涉及序列化接口Serializable)

  是否让流保证数据类型不变:DataInputStream/DataOutputStream

  是否让输出流输出时保证输出字面符号:PrintStream/PrintWriter(打印流)

  是否要操作内存中的字符串和数组:ByteArrayInputStream/ByteArrayOutputStream/CharArrayReader/CharArrayWriter

  Bufferedxxx类和Array相关的功能此处不做介绍。本文将介绍除此之外的其余功能以及对象序列化时涉及到的序列化接口Serializable。

  1.输入流的串联:SequenceInputStream

  SequenceInputStream按照IO体系命名的特点来理解,大致是"将字节输入流存放到Sequence序列中",实际上,它可用来串联多个输入流。意思是:有输入流1、输入流2、输入流3,原本的行为是按照顺序先后读取输入流1、2、3,现在将这3个输入流按顺序连起来当作一个大输入流,直到输入流3读完后才到流的末尾。

  这个序列输入流类在IO体系里有点特立独行,它只有输入流,没有对应的输出流。它的作用是以操作一个输入流的方式来将多个输入流按序追加读取。例如,将多个文件的数据以追加的方式写入到一个目标文件中。

  当调用SequenceInputStream的close()方法时,它将会自动关闭所有它所串联的输入流。

  如下图:

  要使用SequenceInputStream,首先看构造方法SequenceInputStream(Enumeration e),可见它只能接收枚举出来的字节输入流。但如何获取到这些枚举元素?可以将各个输入流存放到一个集合中,然后使用Collections工具类中的enumeration(Collection c)方法将这个集合转换为Enumeration对象。在此还需说明的是,通常SequenceInputStream要串联的多个流都是有先后顺序的,例如1.txt,2.txt,3.txt依序串联下去,所以枚举时也要保证能够依序枚举出来,这也要求在Collection转换为Enumeration时,集合中的流对象在集合中也是有序的,这意味着使用List集合来存储这些流对象是最佳的。

  例如,下面的示例中将{1..6}.txt共6个txt文件按文件名排序先后串联成一个SequenceInputStream。

  //存储多个字节输入流对象到List集合中

  List list = new ArrayList();

  for(int i=1;i<=6;i++){

  list.add(new FileInputStream(i+".txt"));

  }

  //将List集合转换为枚举对象Enumeration

  Enumeration en = Collections.enumeration(list);

  //将枚举出来的各个字节输入流串联起来

  SequenceInputStream sis = new SequenceInputStream(en);

  2. ObjectInputStream/ObjectOutputStream和序列化接口Serializable

  输入流和输出流可以按字节、存储读取媒体类、文本类文件,但能否将java中的对象也作为数据持久化到文件中呢?io包中提供了ObjectInputStream和ObjectOutputStream来读、写对象。

  例如给定如下Student类,将以此类作为ObjectInputStream/ObjectOutputStream流读、写的对象。

  class Student {

  String name;

  int age;

  Student(String name,int age){

  this.name = name;

  this.age = age;

  }

  public String getName() {

  return name;

  }

  public int getAge() {

  return age;

  }

  public String toString(){

  return "{name="+name+",age="+age+"}";

  }

  }

  下面使用ObjectOutputStream将Student对象写入到文件中,这类文件的规范后缀名为".object"。该类的构造方法为ObjectOutputStream(OutputStream out)。

  import java.io.*;

  import java.util.*;

  public class ObjectStream {

  public static void main(String[] args) throws IOException {

  ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/temp/a.txt"));

  Student stu = new Student("malongshuai",22);

  oos.writeObject(stu);

  }

  }

  编译并执行上述代码,将抛出NotSerializableException异常,意思是未序列化。那么谁没有序列化?Student对象。要想让某对象序列化的方式很简单,只需让Student类实现Serializable接口即可。如下:

  class Student implements Serializable {

  String name;

  int age;

  Student(String name,int age){

  this.name = name;

  this.age = age;

  }

  public String getName() {

  return name;

  }

  public int getAge() {

  return age;

  }

  public String toString(){

  return "{name="+name+",age="+age+"}";

  }

  }

  可以使用ObjectInputStream从文件中读取曾被序列化的数据。这称为"反序列化"。

  ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/temp/a.txt"));

  Object obj = ois.readObject();

  System.out.println("read Object from file:"+stu1.toString()); //多态

  看上去序列化和反序列化是一件很简单的事情。确实如此,但其有不少知识点和需要注意的关键点。

  什么是序列化?

  从前面的例子中可以看出,序列化的方式非常简单,只需实现Serializable接口即可。它不提供任何方法。序列化的意义仅仅只是为类进行一种特殊的标识,即所谓的"盖戳"。就像夫妻如何证明他们是夫妻,颁发一个结婚证即可,再例如猪肉凭什么是合格的?给它贴一张合格标签即可。

  序列化的目的是什么?

  为了将某些对象持久化保存起来,供以后反序列化的时候读取。

  序列化后的对象在存储时会存储哪些数据?

  存储的内容包括:类名和类签名(类的序列化版本号SerialVersionUID)、对象的字段值和数组值,以及从初始对象中引用的其他所有对象的闭包。大致可以看作是存储了一个类的版本号、类名、某些字段的值以及引用的对象。当然,并非所有字段值都会存储,见下文的第6点。

  对某个对象序列化后,修改对象的属性(例如将成员变量的修饰符从public改为private),反序列化时将会如何?

  因为保存起来的序列化数据带有一个类签名SerialVersionUID,而修改类的定义后,编译时这个类会生成Class文件,而这个文件中的版本号将不再和之前序列化时保存的版本号相同。于是抛出异常。

  java.io.InvalidClassException: Student; local class incompatible: stream classdesc serialVersionUID = -9151998530267376490, local class serialVersionUID = -3521625297801190192

  由此可以明确一点:class文件中仅只存储类的定义语句,在new对象时将在堆内存中开辟一段空间并存储对象数据(如成员变量)。反序列化实际上是将保存起来的类对象数据加载到这个开辟出来的对象空间中。

  如何保证反序列化的成功?

  强烈建议显式在实现了Serializable接口的类中,声明一个固定的序列化。如:

  public/private/... static final long serialVersionUID = 123456L;

  这样一来,无论是序列化保存时,还是后来修改了类定义生成的class文件中,其版本号都是固定且相同的,也就是说不会因为序列化版本号不同而反序列化失败。

  类中所有属性都应该序列化吗?

  显然不是。有两类数据不会被保存:静态变量(static)、瞬态变量(transient)。例如密码字段、时间点等随时改变、有安全隐患的数据不应该被序列化保存。在进行序列化的时候,只是将堆内存中的数据保存起来,所以加了static关键字的静态属性不会被序列化。而加了transient关键字的(如为Student类的age加上瞬态属性public transient int age;)也不会被序列化。

  看上去说了一大堆,其实操作起来非常简单,只需为待序列化的对象实现Serializable接口并声明serialVersionUID就可以了。

  以下是ObjectInputStream和ObjectOutputStream序列化、反序列化多个对象的示例。序列化的时候使用了集合的方式,将多个Student对象存储到集合中,然后遍历集合来序列化各个Student对象。反序列化的时候,由于ObjectInputStream的readObject()一次读取一个对象示例的数据,且没有提供合适的判断流结尾的返回值,只是在读取到结尾时会抛出EOFException异常。因此此处采用while无限循环的方式,并通过抛出的EOFException异常来结束循环。

  import java.io.*;

  import java.util.*;

  public class ObjectStream {

  public static void main(String[] args) {

  //将各学生对象存放到集合中

  List list = new ArrayList();

  list.add(new Student("Malongshuai",22));

  list.add(new Student("Gaoxiaofang",22));

  //序列化

  //writeObj(list,"d:/temp/a.object");

  //反序列化

  readObj("d:/temp/a.object");

  }

  //序列化

  public static void writeObj(List list,String filename) {

  //遍历集合中的对象并将它们序列化

  ObjectOutputStream oos = null;

  try {

  oos = new ObjectOutputStream(new FileOutputStream(filename));

  for(Iterator it = list.iterator();it.hasNext();) {

  oos.writeObject(it.next());

  }

  } catch (FileNotFoundException f) {

  f.printStackTrace();

  } catch (IOException i) {

  i.printStackTrace();

  } finally {

  if(oos!=null) {

  try {

  oos.close();

  } catch(IOException i){

  i.printStackTrace();

  }

  }

  }

  }

  //反序列化:读取序列化数据

  public static void readObj(String filename) {

  ObjectInputStream ois = null;

  try {

  ois = new ObjectInputStream(new FileInputStream(filename));

  while(true) {

  Student student = (Student)ois.readObject();

  System.out.println(student.toString());

  }

  } catch (EOFException e){

  } catch (FileNotFoundException f) {

  f.printStackTrace();

  } catch (IOException i) {

  i.printStackTrace();

  } catch (ClassNotFoundException c){

  c.printStackTrace();

  } finally {

  if(ois!=null) {

  try {

  ois.close();

  } catch (IOException i) {

  i.printStackTrace();

  }

  }

  }

  }

  }

  class Student implements Serializable {

  static final long serialVersionUID = 123456l;

  String name = "hello";

  public int age;

  Student(String name,int age){

  this.name = name;

  this.age = age;

  }

  public String getName() {

  return name;

  }

  public int getAge() {

  return age;

  }

  public String toString(){

  return "{name="+name+",age="+age+"}";

  }

  }

  3.PrintStream/PrintWriter

  首先看一个容易出现疑惑的现象。

  import java.io.*;

  public class DataStream {

  public static void main(String[] args) throws IOException {

  FileOutputStream fos = new FileOutputStream("d:/temp/b.txt");

  fos.write(97);

  fos.write(353);

  fos.close();

  FileInputStream fis = new FileInputStream("d:/temp/b.txt");

  byte[] buf = new byte[10];

  int len = 0;

  while((len=fis.read(buf))!=-1) {

  String str = new String(buf);

  System.out.println(str);

  }

  fis.close();

  }

  }

  上面的代码中,向文件中写入的是数值97和353,但无论是用记事本解析还是从这里读取到的结果都是"aa"共两个字节的字母。为什么会如此?

  在write(Int i)方法写入数据时,它会将最低位字节写入,而忽略前三个字节。例如97的二进制码为"00000000 00000000 00000000 01100001",忽略前三个字节,写入到文件中的二进制数据就只剩下"01100001",而这被读取或被解析时正好解析为字母a。同理353,它的二进制数据为"00000000 00000000 00000001 01100001",虽然第三个字节最后一位为1,但它还是被忽略,导致写入到文件中的二进制数据仍然为"01100001",解析后就是字母a。

  要避免这种问题,可以使用字节打印流PrintStream或字符打印流PrintWriter,它会将数据按照字面展现形式输出。例如下面的例子中,将会向文件中分别写入"a97"。

  FileOutputStream fos = new FileOutputStream("d:/temp/a.txt");

  PrintStream ps = new PrintStream(fos);

  //PrintStream ps = new PrintStream("d:/temp/a.txt")

  ps.write(97); //它调用的其实还是fos.write(),所以仍然存储字母a

  ps.print(97); //存储字面符号97

  ps.close();

  使用println()可以换行,使用printf()可以以C语言的打印格式输出。

  另外,在PrintWriter中(不包括PrintStream),有一个自动更新autoFlush的概念,它表示每输出一次换行符就自动flush一次。但注意,PrintWriter的自动刷新只对println()和printf()方法有效,对print()无效。之所以不包括PrintStream,是因为PrintWriter因为字符集处理的原因在输出的时候涉及了一个额外的缓冲区,自动刷新就是将此缓冲区的数据flush,而PrintStream则没有这个额外的缓冲区,因此它是实时输出的。

  4.DataInputStream/DataOutputStream

  还是前面的问题,如何将97作为int数据类型保存到文件中(即将4个字节的97存到文件中)。也就是保证数据的数据类型不变。使用DataInputStream/DataOutputStream即可。

  DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/temp/a.txt"));

  dos.writeInt(97);

  DataInputStream dis = new DataInputStream(new FileInputStream("d:/temp/a.txt"));

  System.out.println(dis.readInt());

  这样将会把97的4个字节存储到文件中,如果使用记事本去解析,得到的结果会是" a",共4个字节,虽然得到的结果是a,但这只是用记事本解析的而已。使用上面的readInt()读取的结果则是正确的。