本节从三个方面介绍Java输入输出体系,第一部分介绍Java IO,第二部分介绍Java对象的序列化机制,第三部分介绍JavaNIO。
一、Java IO
Java的IO是通过java.io包下的类和接口来支持的,主要包括输入、输出两种IO流,没种输入、输出流又分为字节流和字符流,所以Java的输入/输出体系会显得如此复杂。Java的IO流使用了一种装饰器设计模式,它将Io流分成底层的节点流和上层处理流。首先我们从File类开始。
1、File类
File类是java.io包下代表平台无关的文件和目录,如果希望在程序中操作文件或者是目录,都可以通过File类来完成(新建、删除、重命名文件和目录)。但是File类不能访问文件内容本身,如果需要访问,那么就需要后面的输入/输出流了。FIle类的API在这里不去重复了,去查阅API手册,每个方法都挺简单的。在这里要说一下的是,文件过滤器,File类中有一个list()方法接受一个FilenameFileter参数,这是一种典型的Command设计模式,在FilenameFilter接口中包含一个accept(File dir,String name)方法。例如:过滤d盘中后缀名为".sql"的文件。
public static void main(String[] args) { File file=new File("d:"); String[] files2=file.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".sql") ; } }); for(String file2:files2){ System.out.println(file2); } }
2、Java中的IO流
Java的IO流式实现输入/输出的基础,他可以方便的实现数据的输入/输出的操作。
流的分类:有三种分类方法。
①可以分为:输入流、输出流。Java的输入流主要由InputStream和Reader作为基类,输出流有OuputStream和Writer作为基类。他们都是一些抽象基类,无法直接创建实例。
②可以分为:字节流、字符流。他们之间的区别在于操作的数据单元不同,字节流操作的数据单元式8位的字节,而字符流操作的16位的字符。字节流主要由InputStream和OutputStream作为基类,字符流主要由Reader和Writer作为基类。
③可以分为节点流、处理流。Java使用处理流包装节点流式一种典型的装饰器设计模式。
处理流的功能主要体现在:
①性能的提高,增加缓冲的方式来提高输入/输出的效率。
②操作的便捷,处理流提供了一系列便捷的方法来一次输入/输出大批量的内容。
通过使用处理流,Java程序无需理会输入/输出节点是磁盘、网络还是其他输入/输出设备,程序只要将这些节点流包装成处理流,就可以使用相同的输入/输出代码来读写数据。
3、字节流和字符流
InputStream和Reader
他们所提供的方法,基本的功能是一样,可以去查看API手册。程序如何判断文件是否读到最后,通过read(char[] c)或read(byte[] b)方法返回-1来判断。InputStream和Reader是基本抽象类,本身不能够创建实例,但他们分别有一个用于读取文件的输入流,节点流,FileInputStream和FileReader
//使用FileInputStream public static void main(String[] args) { File file=new File("wang.txt"); FileInputStream inStream=null; try{ inStream=new FileInputStream(file); byte[] buff=new byte[8]; int hasRead=0; while((hasRead=inStream.read(buff))!=-1){ System.out.println(new String(buff, 0, hasRead)); } }catch(Exception e){ e.printStackTrace(); }finally{ if(inStream!=null){ try { inStream.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
//FileReader public static void main(String[] args) { File file=new File("wang.txt"); FileReader reader=null; try{ reader=new FileReader(file); char[] c=new char[16]; int hasRead=0; while((hasRead=reader.read(c))!=-1){ System.out.println(new String(c, 0, hasRead)); } }catch(Exception e){ e.printStackTrace(); }finally{ if(reader!=null){ try { reader.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
同样OutputStream和Writer,提供了FileOutputStream和FileWriter两个节点流。
4、输入/输出流体系
首先说有的输入输出流都是从InputStream、OutputStream、Reader、Writer抽象基类派生来的,然后如果访问文件则提供了FileInputStream,FileOutputStream,FileReader,FileWriter。访问字符串,提供了StringReader,StringWriter。
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
打印流 | PrintStream | PrintWriter | ||
推回输入流 | PushbackInputStream | PushBackReader | ||
特殊流 | DataInputStream | DataOutputStream |
访问管道流 | PipedInputStream | PipedOutputStream | PipedReader | PepedWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
另外还有一些AudioInputStream等访问音频文件等功能的字节流。
5、处理流
我们使用处理流的典型思路是,使用处理流来封装节点流。
如何区分是处理流还是节点流?
只要流的构造器参数不是一个物理节点,而是一个已经存在的流,那么这种流就是处理流。而所有节点流都是直接以IO节点作为构造器参数的。
下面,使用PrintStream来包装OutputStream,使用处理流后的输出流将更加方便:
public static void main(String[] args) { File file=new File("test.txt"); OutputStream outStream=null; PrintStream ps=null; try{ if(file.exists()){ file.createNewFile(); } outStream=new FileOutputStream(file); ps=new PrintStream(outStream); ps.println("wangningnihao"); }catch(Exception e){ e.printStackTrace(); }finally{ ps.close(); try { outStream.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
由于PrintStream类的输出功能非常强大,通常如果我们需要输出文本内容,都应该将输出流包装成PrintStream后进行输出。
6、StringReader和StringWriter访问字符串
public static void main(String[] args) { String str="大家好,我是王宁"; StringReader reader=new StringReader(str); char[] buffer=new char[12]; int hasRead=0; try{ while((hasRead=reader.read(buffer))!=-1){ System.out.println(new String(buffer,0,hasRead)); } }catch(Exception e){ } StringWriter writer=new StringWriter(20); try{ writer.write("哈哈1"); writer.write("哈哈2"); System.out.println(writer.toString()); }catch(Exception e){ } }
在这里,StringWriter实际上以一个StringBuffer作为输出点。
7、转换流
输入/输出体系中,还提供了两个转换流InputStreamReader和OutStreanWriter。Java中的System.in代表标准的输入,即键盘输入,这个标准的输入流是InputStream类的实例,我们可以使用InputStreamReader将其转换成字符输入流。
public static void main(String[] args) { InputStream in=System.in; InputStreamReader reader=new InputStreamReader(in); BufferedReader bufferReader=new BufferedReader(reader); String buffer=null; try{ while((buffer=bufferReader.readLine())!=null){ if(buffer.equals("exit")){ System.exit(1); } System.out.println(buffer); } }catch(Exception e){ e.printStackTrace(); } }
我们将System.in用InputStreamReader转换为字符流,然后使用BufferdReader封装字节流,方便读取。BufferedReaer具有缓冲功能,可以一次读取一行文本。
8、缓冲流
BufferdInputStream、BufferddOutputStream、BufferedReader、BufferedWriter
Io的缓冲区的存在就是为了提高效率,把要操作的数据放进缓冲区,然后一次性把缓冲区的内容写到目的地。
9、DataInputStream和DataOutputStream 数据输入流,数据输出流
DataOutputStream:数据输出流允许应用程序以适当的方式将基本Java数据类型写入输出流中。然后应用程序可以使用数据输入流将数据读入。
DataInputStream:数据输入流允许应用程序与机器无关方式从底层输入流中读取基本Java数据类型。
10、推回输入流
PushBackInputStream 和PushBackReader
他们都unread()方法,将指定的内容推回到缓冲区内,从而允许重复读取刚刚读取的内容。这两个推回输入流都有一个推回缓冲区,当程序调用这个两个推回输入流的unreade()方法时,系统将会把指定数组的内容推回到该缓冲区里,而推回输入流每次调用read()方法时总是先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没用装满read()所需的数组时,才会从原输入流中读取。默认的推回缓冲区的长度为1.如果程序中推回到缓冲区的内容超出了推回缓冲区的大小,将会引发IOExcception异常。
11、重定向标准输入输出
Java的标准输入/输出分别通过System.in和System.out来代表,在默认情况下它们分别代表键盘和显示器。在System类提供了3个重定向标准输入/输出的方法:
①、setErr(PrintStream ps):重定向“标准”错误输出流。
②、setIn(InputStream in):重定向"标准"输入流。
③、setOut(PrintStream out):重定向标准输出流。
public static void main(String[] args) { try{ //重定向“ 标准”输入 InputStream in=new FileInputStream(new File("test.txt")); System.setIn(in); Scanner sc=new Scanner(System.in); sc.useDelimiter("\n"); while(sc.hasNext()){ System.out.println(sc.next()); } //重定向输出到文件中 PrintStream out=new PrintStream("test.txt"); System.setOut(out); System.out.println("hahhaha"); }catch(Exception e){ e.printStackTrace(); } }
12、Java虚拟机读写其他进程大的数据
Runtime对象的exec()方法可以运行平台上的其他程序,该方法产生一个Process对象,Process对象代表由该Java程序启动的子进程。Process类提供了如下3个方法,用于让程序和其子程序进行通信。
getErrorStream():获取子进程的错误流
getInputStream():获取子进程的输入流
getOutputStream():获取子进程的输出流
public static void main(String[] args) { try{ Process process=Runtime.getRuntime().exec("javac"); BufferedReader reader=new BufferedReader(new InputStreamReader(process.getErrorStream())); String line=null; while((line=reader.readLine())!=null){ System.out.println(line); } }catch(Exception e){ e.printStackTrace(); } }
13、RandomAccessFile
RandomAccessFile是Java输入/输出体系中功能最丰富的文件内容访问类,提供了许多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据,与普通的输入/输出流不同的是,RandomAccessFIle支持随机访问的方式,程序可以直接跳转到文件的任意地方来读写数据。RandomAccessFIle类允许自由定位文件记录指针,所以RandomAccessFIle可以不从开始的位置开始输出,如果程序需要向已经存在的文件后追加内容,则应该使用RandomAccessFile。
RandomAccesFile包含一个记录指针,用以标识当前读写处的位置,当程序新创建一个RandomAccessFIle对象时,该对象的文件记录指针位于文件头,当读写n个字节后,文件记录指针会向后移动,另外,RandomAccessFile可以自由移动记录指针,既可以向前移动,也可以向后移动。
getFilePointer():返回文件记录指针的当前位置。
seek(long pos):将文件记录指针定位到pos位置。
创建RandomAccessFIle对象时,需要指定Mode参数,该参数有4个值,
① r:以只读的方式打开指定文件,如果试图对该RandomAccessFIle执行写入方法,都将抛出IOException异常。
② rw:以读、写方式打开指定文件。如果该文件尚不存在,则尝试创建该文件。
③ rws:以读写方式打开指定文件,相对于rw模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
④ rwd :以读写方式打开指定文件,相对于rw模式,还要求对文件的内容的每个更新都同步写入到底层存储设备。
public static void main(String[] args) { try{ RandomAccessFile randomFile=new RandomAccessFile(new File("test.txt"), "rw"); System.out.println("指针的初始位置:"+randomFile.getFilePointer()); randomFile.seek(300); byte[] buff=new byte[1024]; int hadRead=0; while((hadRead=randomFile.read(buff))!=-1){ System.out.println(new String(buff,0,hadRead)); } //在文件的最后写入内容 randomFile.seek(randomFile.length()); randomFile.write("王宁你好".getBytes()); }catch(Exception e){ e.printStackTrace(); } }
RandomAccessFile依然不能向文件的制定位置插入内容,如果直接将文件记录指针一道道中间某位置后,则新输出的内容会覆盖文件中原有的内容,如果需要向指定位置插入内容,程序需要先把插入点后面的内容读入缓冲区,等把需要插入的数据写入文件后,再将缓冲区的内容追加到文件的后面。
public static void main(String[] args) { try{ File file=File.createTempFile("temp", null); file.deleteOnExit(); RandomAccessFile randomFile=new RandomAccessFile(new File("wang.txt"),"rw"); randomFile.seek(300); byte[] buff=new byte[1024]; int hasRead=0; FileOutputStream tmpOut=new FileOutputStream(file); FileInputStream temIn=new FileInputStream(file); while((hasRead=randomFile.read(buff))!=-1){ tmpOut.write(buff, 0, hasRead); } String str="王宁你好!王宁你好!王宁你好!王宁你好!王宁你好!王宁你好!王宁你好!" + "王宁你好!王宁你好!王宁你好!王宁你好!王宁你好!王宁你好!王宁你好!王宁你好!" + "王宁你好!王宁你好!王宁你好!王宁你好!王宁你好!王宁你好!王宁你好!王宁你好!"; randomFile.write(str.getBytes()); while((hasRead=temIn.read(buff))!=-1){ randomFile.write(buff, 0, hasRead); } }catch(Exception e){ } }
多线程断点的网络下载工具(FlashGet)就可通过RandomAccessFile类来实现,所有的下载工具在下载开始时都会创建两个文件,一个是与被下载文件大小相同的空文件,一个是记录文件指针的位置文件,下载工具用多线程启动输入流来读取网络数据,并使用RandomAccessFile将从网络上读取的数据写入前面建立的空文件中,每写一些数据后,记录文件指针的文件就分别记下每个RandomAccessFile当前的文件指针位置,网络断开后,再次开始下载时,每个RandomAccessFile都根据记录文件中记录的位置继续往下写。
二、对象序列化
1、序列化的含义和意义
对象序列化的目标的将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流永久的保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种二进制流(无论是磁盘上获取的还是通过网络获取的),都可以将这种二进制流恢复成原来的Java对象。
对象的序列化指将一个Java对象写入IO流中,于此对应的是,对象的反序列化则指从IO流中恢复该Java对象。如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的。为了让某个类是可序列化的,该类必须实现如下两个接口之一:
Serializable
Externalizable
Java中的很多类已经实现了Serializable,该接口是一个标记接口,实现该接口无需实现任何方法,它只是表明该类的实例是可序列化的。
2、实现序列化的方式
<1>、使用对象流实现序列化
前面我们在介绍Java的IO体系中有这样一个ObjectInputStream、ObjectOuputStream对象流。利用这两个对象流来实现对象的序列化。使用ObjectOutInputStream的writeObject()方法将对象序列化,使用ObjectInputStream的readObject()方法将对象发序列化。
public static void main(String[] args) { Person p=new Person("wangning", 22); try{ //使用ObjectOutputStream将person对象序列化写入test.txt文件中 OutputStream out=new FileOutputStream(new File("test.txt")); ObjectOutputStream objOut=new ObjectOutputStream(out); objOut.writeObject(p); //使用ObjectInputStream将Person对象从test.txt文件中反序列化出来 InputStream in=new FileInputStream(new File("test.txt")); ObjectInputStream objIn=new ObjectInputStream(in); Person person=(Person)objIn.readObject(); System.out.println(person.getName()); }catch(Exception e){ e.printStackTrace(); } }
注意:
①、反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供该Java对象所属类的class文件,如果当反序列化时找不到对应的Java类时将会引发ClassNotFoundException异常。同时,反序列化机制无需通过构造器来初始化Java对象。
②、如果使用序列化机制向文件中写入了多个Java对象,使用反序列化机制恢复对象时必须按实际写入的顺序读取。当一个可序列化类有多个父类时(包括直接父类和间接父类),这些父类要么有无参数的构造器,要么也是可序列化的,否则反序列化时将抛出InvalidClassException异常,如果父类是不可序列化的,只是带有无参数的构造器,则该父类中定义的Field值不会序列化到二进制流中。
对象引用的序列化:
如果某个类的Field类型不是基本类型或String类型,而是另一个引用类型,那么这个引用类必须是可序列化,否则拥有该类型的FIeld的类也是不可序列化的。
另外,Java的序列化机制采用了一种特殊的序列化算法,
①、所有保存到磁盘中的对象都有一个序列化编号。
②、当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未被序列化过,系统才会将该对象转换成字节序列化并输出。
③、如果某个对象已经序列化过,程序将只是输出一个序列化编号,而不是再次重新序列化该对象。
例如在下面的例子中,两个TestObjectInputStream对象都引用同一个person对象,在序列化的过程中,person对象只被序列化一次。
package com.edu.io; import java.io.*; import java.io.ObjectInputStream; import java.io.Serializable; class Person implements Serializable{ private String name; private int age; public Person(String name,int age){ System.out.println("有参数的构造器"); this.name=name; this.age=age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } public class TestObjectInputStream implements Serializable{ public TestObjectInputStream(Person p){ this.person=p; } public Person getPerson() { return person; } public void setPerson(Person person) { this.person = person; } private Person person; /** * @param args */ public static void main(String[] args) { Person p=new Person("wangning", 22); TestObjectInputStream t1=new TestObjectInputStream(p); TestObjectInputStream t2=new TestObjectInputStream(p); try{ //使用ObjectOutputStream将person对象序列化写入test.txt文件中 OutputStream out=new FileOutputStream(new File("test.txt")); ObjectOutputStream objOut=new ObjectOutputStream(out); objOut.writeObject(t1); objOut.writeObject(t2); //使用ObjectInputStream将Person对象从test.txt文件中反序列化出来 InputStream in=new FileInputStream(new File("test.txt")); ObjectInputStream objIn=new ObjectInputStream(in); t1=(TestObjectInputStream) objIn.readObject(); t2=(TestObjectInputStream) objIn.readObject(); System.out.println(t1.getPerson().getName()); System.out.println(t2.getPerson().getName()); }catch(Exception e){ e.printStackTrace(); } } }
注意:由于Java的序列化机制:如果多次序列化同一个对象,只有第一次序列化时,才会把该Java对象转换成字节序列并输出,这样可能引起一个潜在的问题:当程序序列化一个可变对象时,只有在第一次使用writeObject()方法时,才将该对象序列化,当程序再次使用writeObject()方法时,程序只是输出前面序列化的编号,即使后面对象的Filed值已经改变,改变的Filed也无法输出。
<2>、自定义序列化
当对某个对象进行序列化时,系统会自动把该对象的所有FIeld依次进行序列化,如果某个Field引用到另一个对象,则被引用的对象也会被序列化,这种情况称为递归序列化。当不希望系统对某个类的某个Filed进行序列化时,可以再Field前面加上transient关键字修饰。此关键字只能修饰Field。
使用transient关键字修饰Field虽然简单、方便,但被transient修饰的field将被完全隔离在序列化机制外,这样导致反序列化机制恢复Java对象时,无法取得该Field值。
Java提供了一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制入股序列化各Field,甚至完全不序列化某些Field。
实现下面的方法,用以实现自定义序列化
private void writeObject(ObjectOuputStream out)
private void readObject(ObjectInputStream in)
private void readObjectNoData():如果序列化流不完整的时,readObjectNoData()可以用来正确的初始化反序列化的对象。例如接收方使用的反序列化类的版本不同于发送方,或者接收方版本扩展的类不是发送方版本扩展的类,或者序列化流被篡改是,系统都会调用readObjectNoData()方法来初始化反序列化的对象。
package com.edu.io; import java.io.*; public class TestWirteObject implements Serializable{ private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } private void writeObject(ObjectOutputStream out) throws Exception{ //name翻转后序列化写入流中 out.writeObject(new StringBuffer(name).reverse()); out.writeInt(age); } private void readObject(ObjectInputStream in) throws Exception, IOException{ this.name= in.readObject().toString(); this.age=in.readInt(); } /** * @param args * @throws Exception * @throws FileNotFoundException */ public static void main(String[] args) throws FileNotFoundException, Exception { TestWirteObject t=new TestWirteObject(); t.setName("wangning"); t.setAge(33); ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream(new File("test.txt"))); out.writeObject(t); ObjectInputStream in=new ObjectInputStream(new FileInputStream(new File("test.txt"))); t=(TestWirteObject) in.readObject(); System.out.println(t.getName()); } }
还有一种更彻底的自定义机制,它甚至可以在序列化对象时将该对象替换成其他对象。writeReplace(),此方法将有序列化机制调用,只要该方法存在。Java序列化机制保证在序列化某个对象之前,先调用该对象的writeReplace()方法,如果该方法返回另一个Java对象,则系统转为序列化另一个对象。
例如下面例子,表面上是序列化Person对象,但实际上序列化的是ArrayList。
package com.edu.io; import java.util.*; import java.io.*; class Person2{ private String name; private int age; public Person2(String name,int age){ this.name=name; this.age=age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } private Object writePlace(){ ArrayList<Object> list=new ArrayList<Object>(); list.add(name); list.add(age); return list; } } public class TestWriteReplace { /** * @param args */ public static void main(String[] args) { try{ ObjectOutputStream objOut=new ObjectOutputStream(new FileOutputStream(new File("test.txt"))); ObjectInputStream objIn=new ObjectInputStream(new FileInputStream(new File("test.txt"))); Person2 per=new Person2("wangning",22); objOut.writeObject(per); ArrayList list=(ArrayList) objIn.readObject(); System.out.println(list); }catch(Exception e){ } } }
系统在序列化某个对象之前,总是先调用writeReplace()方法,如果该方法返回另一个对象,系统再次调用另一个对象的writeReplace()方法,直到该方法不再返回另外一个对象,然后在调用该方法的writeObject()方法。
与writeReplace()方法相对的是,序列化机制还有一个特殊的方法,他可以实现保护性复制整个对象。readReplace()方法。这个方法会紧接着readObject()之后被调用,方法的返回值将会代替原来反序列化得对象,而原来readObject()反序列化的对象将会被立即丢弃。readResolve()在序列化单例类和枚举类尤其有用。
<3>、实现Externalizable实现序列化
Java类实现Externalizable接口,这种方式,完全由程序员决定存储和恢复对象数据。
void readExternal(ObjectInput in)实现反序列化
void writeExternal(ObjectOutput out)实现序列化
采用实现Externalizable接口方式与前面的自定义序列化非常相似,只是Externalizable借口强制自定义序列化。
Serializable和Externaliazable的对比
实现Serializable | 实现Externaliazable |
系统自动存储必须要信息 | 程序员决定存储哪些信息 |
Java内建支持,易于实现,只要实现该接口即可 | 仅仅提供两个方法,实现该接口必须为两个空方法提供实现 |
性能略差 | 性能略好 |
版本
反序列化Java对象时必须提供该对象的class文件,现在问题是,随着项目升级,系统的class文件也会升级,Java如何保证两个class文件的兼容性?
Java序列化机制允许为序列化类提供给一个private static final 的serailVersionUID值,该Field值标示Java类的序列化版本。
Java NIO在后面的文章介绍。