理解Java的IO流
1.Java的IO流是实现输入和输出的基础,它可以方便的实现数据的输入和输出操作,在Java中把不同的输入和输出源(键盘,文件,网络连接等)抽象表述为“流”通过流的方式允许Java程序使用相同的方式来访问不同的输入和输出源。
2.字节流和字符流的区别:字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。(字节流主要由InputStream和OutputStream作为基类,而字符流则主要由Reader和Writer作为基类。)
3.节点流:是从一个特定的IO设备(如磁盘,网络)读或写数据的流,是一种低级流;处理流:则用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读写功能,是一种高级流(由于增加了缓冲的方式来提高输入和输入的效率,故性能比节点流要高);两者关系:处理流通过包装节点流来达到用完全相同的输入和输出代码来访问不同的数据源(这是一种典型的装饰设计模式),如图:、
4.使用Java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了可以保证流的物理资源被回收之外,可能还可以将输出流缓冲区中的数据flush到物理节点里(因为在执行close()方法之前,自动执行输出流的flush方法。)
输入和输出流体系
1.在使用处理流包装了底层节点流之后,关闭输入和输出资源时,只要关闭最上层的处理流即可,关闭最上层的处理流时,系统会自动关闭被该处理流包装的节点流。
代码例子
public class PrintStreamTest
{
public static void main(String[] args)
{
try(
FileOutputStream fos = new FileOutputStream("test.txt");
//用printStream处理流包装FileOUtputStream节点流
PrintStream ps = new PrintStream(fos))
{
// 使用PrintStream执行输出
ps.println("普通字符串");
// 直接使用PrintStream输出对象
ps.println(new PrintStreamTest());
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
2.Java输入和输出流体系中常用的流分类如图:
**3.我们常常把计算机的文件
分为两类:文件文本和二进制文件两大类,所有能用记事本打开并看到其中字符内容的文件称为文本文件,反之则称为二进制文件。(实质计算机里的所有文件是二进制文件)**
重定向标准输入和输出
1.我们可以用System类里提供了3个重定向标准输入和输出的方法来设定其输出和输入。
代码例子:
重定向标准输出:
public class RedirectIn
{
public static void main(String[] args)
{
try(
FileInputStream fis = new FileInputStream("RedirectIn.java"))
{
// 将标准输入重定向到fis输入流
System.setIn(fis);
// 使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
// 增加下面一行将只把回车作为分隔符
sc.useDelimiter("\n");
// 判断是否还有下一个输入项
while(sc.hasNext())
{
// 输出输入项
System.out.println("键盘输入的内容是:" + sc.next());
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
重定向标准输入
public class RedirectIn
{
public static void main(String[] args)
{
try(
FileInputStream fis = new FileInputStream("RedirectIn.java"))
{
// 将标准输入重定向到fis输入流
System.setIn(fis);
// 使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
// 增加下面一行将只把回车作为分隔符
sc.useDelimiter("\n");
// 判断是否还有下一个输入项
while(sc.hasNext())
{
// 输出输入项
System.out.println("键盘输入的内容是:" + sc.next());
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
对象序列化
1.序列化机制允许将实现序列化的Java对象(该类必须实现Serizlizable或Externalizable接口)转换成字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以备以后重新恢复成原来的对象(序列化机制使得对象可以脱离程序的运行而独立存在。)。
2.反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供该Java对象所属类的class文件(反序列化机制无须通过构造器来初始化Java对象),否则将会引发ClassNotFoundException异常。
代码例子:
序列化对象:
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
保存序列化对象:
public class WriteObject
{
public static void main(String[] args)
{
try(
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("object.txt")))
{
Person per = new Person("孙悟空", 500);
// 将per对象写入输出流
oos.writeObject(per);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
恢复序列化对象:
public class ReadObject
{
public static void main(String[] args)
{
try(
// 创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("object.txt")))
{
// 从输入流中读取一个Java对象,并将其强制类型转换为Person类
Person p = (Person)ois.readObject();
System.out.println("名字为:" + p.getName()
+ "\n年龄为:" + p.getAge());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
3.当一个序列化类有多个父类时(包括直接父类和间接父类),这些父类要么有无参数的构造器,要么也是可无序列化的——否则反序列化时将抛出InvalidClassException异常,如果父类是不可序列化的,只是带有无参数的构造器,则该父类中定义的Field值不会序列化到二进制流中。
4.如果某个类的Field类型不是基本类型或String类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型的Field的类也是不可序列化的。
5.当序列化同一个对象时,Java序列化机制采用了一种特殊的序列化算法:
- 所有保存到磁盘中的对象都有一个序列化编号。
- 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出。
- 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。
代码例子:
序列化对象:
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
public class Teacher
implements java.io.Serializable
{
private String name;
private Person student;
public Teacher(String name , Person student)
{
this.name = name;
this.student = student;
}
//此处省略了name和student的setter和getter方法
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// student的setter和getter方法
public void setStudent(Person student)
{
this.student = student;
}
public Person getStudent()
{
return this.student;
}
}
保存序列化对象:
public class WriteTeacher
{
public static void main(String[] args)
{
try(
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("teacher.txt")))
{
Person per = new Person("scau", 500);
Teacher t1 = new Teacher("beyondboy" , per);
Teacher t2 = new Teacher("sungirl" , per);
// 依次将四个对象写入输出流
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);
oos.writeObject(t2);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
恢复序列化对象:
public class ReadTeacher
{
public static void main(String[] args)
{
try(
// 创建一个ObjectInputStream输出流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("teacher.txt")))
{
// 依次读取ObjectInputStream输入流中的四个对象
Teacher t1 = (Teacher)ois.readObject();
Teacher t2 = (Teacher)ois.readObject();
Person p = (Person)ois.readObject();
Teacher t3 = (Teacher)ois.readObject();
// 输出true
System.out.println("t1的student引用和p是否相同:"
+ (t1.getStudent() == p));
// 输出true
System.out.println("t2的student引用和p是否相同:"
+ (t2.getStudent() == p));
// 输出true
System.out.println("t2和t3是否是同一个对象:"
+ (t2 == t3));
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
运行结果:
t1的student引用和p是否相同:true
t2的student引用和p是否相同:true
t2和t3是否是同一个对象:true
从结果看出,很明显Person对象只被序列输出一次而已,也就是说文件只保存一个序列化Person对象,而不是四个,我来画个示意图来说明这个例子序列化机制:
6.当使用Java序列化机制序列化可变对象时一定要注意,只有第一次调用writeObject()方法来输出对象时才会将对象转换成字节序列,并写入到ObjectOutputStream,在后面程序中及时改对象的Field发生了改变,再次调用writeObject()方法输出该对象时,改变后的Field也不会被输出。
7.关于对象序列化的特点:
- 对象的类名,Field(包括基本类型,数组,对其他对象的引用)都会被序列化,方法,static Field(即静态Field),transient Field都不会被序列化。
- 实现Serializable接口的类如果需要让某个Field不被序列化,则可在该Field前加transient修饰符,而不是加static关键字,虽然static关键字也可达到这个效果,但static关键字不能这样用。
- 保证序列化对象的Field类型也是可序列化的,否则需要使用transient关键字来修饰该Field(瞬态Field),要不然,该类是不可序列化。
- 反序列化对象时必须有序列化对象的class文件。
- 当通过文件,网络来读取序列化后的对象时,必须按实际写入的顺序读取。
版本
1.为了在反序列化是确保序列化版本的兼容性,最后在每个要序列化的类中加入private static final long serialVersionUIDouble这个Field,具体数值自己定义,这样,即使在某个对象被序列化之后,它所对应的类被修改了,该对象也依然可以被正确地反序列化。(如果不显示定义serialVersionUID Field值,该Field值将由JVM根据类的相关(类的方法,非静态Field,非瞬态Field,Field类型等)信息计算,)
2.类的修改是否导致反序列化失败,分以下3中情况:
- 如果修改类时仅仅修改了方法,则反序列化不受任何影响,类定义无须修改serialVersionUID Field值。
- 如果修改类时仅仅修改了静态Field或瞬态Field,则反序列化不受任何影响,类定义无须修改serialVersionUID Field值。
- 如果修改类时修改了非静态Field,非瞬态Field,则可能导致序列化版本不兼容,如果对象流中的对象和新类中包含同名的Field,而Field类型不同,则反序列化失败,类定义应该更细serialVersionUID Field值,如果对象流中的对象比新类中包含更多的Field,则多出的Field值被忽略,序列化版本可以兼容,类定义可以不更新serialVersionUID Field值,如果新类比对象流中的对象包含更多的Field,则序列化版本也可以兼容类定义可以不更新serialVersionUID Field值,但反序列化得到的新对象中多出的Field值都是null(引用类型Field)或0(基本类型Field)。
在这里我就举一个版本值的其中一个作用:
代码例子:
没有显示定义serialVersionUID Field的修改前序列化对象:
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
保存序列化对象:
public class WriteTeacher
{
public static void main(String[] args)
{
try(
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("test.txt")))
{
Person person = new Person("scau", 500);
oos.writeObject(person);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
反序列化对象:
public class ReadTeacher
{
public static void main(String[] args)
{
try(
// 创建一个ObjectInputStream输出流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("test.txt")))
{
Person p = (Person)ois.readObject();
System.out.println(p.getName()+" "+p.getAge());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
运行结果:
scau 500
修改序列化对象(增加一个普通Field值):
public class Person
implements java.io.Serializable
{
private String name;
private int age;
//增加这个属性
private int id;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
再次运行反序列化测试,其结果:
java.io.InvalidClassException: beyondboy.Person; local class incompatible: stream classdesc serialVersionUID = 9185785025620194362, local class serialVersionUID = -2240902702621887025
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:617)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1622)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1517)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1771)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370)
at beyondboy.ReadTeacher.main(ReadTeacher.java:15)
那如何保其修改后的兼容性,那就是再类中添加一个serialVersionUID Field值。
显示定义serialVersionUID Field值的没修改前序列化对象:
public class Person
implements java.io.Serializable
{
private static final long serialVersionUID = -2240902702621887025L;
private String name;
private int age;
//增加这个属性
//private int id;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
执行保存序列化对象后,再修改序列化对象(增加一个普通Field值):
public class Person
implements java.io.Serializable
{
private static final long serialVersionUID = -2240902702621887025L;
private String name;
private int age;
//增加这个属性
private int id;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
执行反序列化结果:
scau 500
新的IO流
1.新IO采用内存映射文件的方式来处理输入和输出,新IO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了(这种方式模拟了操作系统上的虚拟内存的概念),通过这种方式来进行输入和输出比传统的输入和输入要快的多(传统的输入和输出是通过字节的移动来处理的,其方式是阻塞式的输入和输出。)。
2.Buffer(缓冲)是新IO中的两个核心对象,内部结构像一个数组,主要作用就是装入数据,然后输出数据,Buffer涉及了三个重要概念:
- 容量(capacity):缓冲区的容量(capacity)表示该Buffer的最大数据容量,即最多可以存储多少数据,缓冲区的容量不可能变为负值,创建后不能改变。
- 界限(limit):第一个不应该被读出或者写入的缓冲区位置索引,也就是说,位于limit后的数据即不可被读,也不可被写。
- 位置(position):用于指明下一个可以被读出的或者写入的缓冲区位置索引(类似于IO流中的记录指针)。
图示:
代码例子:
public class BufferTest
{
public static void main(String[] args)
{
// 创建Buffer
CharBuffer buff = CharBuffer.allocate(8);
System.out.println("capacity: " + buff.capacity());
System.out.println("limit: " + buff.limit());
System.out.println("position: " + buff.position());
// 放入元素
buff.put('a');
buff.put('b');
buff.put('c');
System.out.println("加入三个元素后,position = "
+ buff.position());
// 调用flip()方法,该limit会移到position位置,并position设为0
buff.flip();
System.out.println("执行flip()后,limit = " + buff.limit());
System.out.println("position = " + buff.position());
//相对取出第一个元素,position会向前移动
System.out.println("第一个元素(position=0):" + buff.get());
System.out.println("取出一个元素后,position = "
+ buff.position());
// 调用clear方法,不会清除Buffer的数据,仅仅将position设置为0,limit设置为capacity
buff.clear();
System.out.println("执行clear()后,limit = " + buff.limit());
System.out.println("执行clear()后,position = "
+ buff.position());
//根据索引绝对取出元素,position位置不会有任何移动
System.out.println("执行clear()后,缓冲区内容并没有被清除:"
+ "第三个元素为:" + buff.get(2));
System.out.println("执行绝对读取后,position = "
+ buff.position());
}
}
运行结果:
capacity: 8
limit: 8
position: 0
加入三个元素后,position = 3
执行flip()后,limit = 3
position = 0
第一个元素(position=0):a
取出一个元素后,position = 1
执行clear()后,limit = 8
执行clear()后,position = 0
执行clear()后,缓冲区内容并没有被清除:第三个元素为:c
执行绝对读取后,position = 0
3.Channel类类似传统流对象特点,但与传统的流对象有两个主要区别:
- Channel可以直接将指定文件的部分或全部直接映射成Buffer
- 程序不能直接访问Channel中的数据,包括读取,写入都不行,Channel只能与Buffer进行交互,也就是说,如果要从Channel中取得数据,必须先用Buffer从Channel中取出一些数据,然后让程序从Buffer中取出这些数据,如果要将程序中的数据写入Channel,一样先让程序将数据放入Buffer中,程序再将Buffer里的数据写入Channel中。
代码例子:
public class FileChannelTest
{
public static void main(String[] args)
{
File f = new File("FileChannelTest.java");
try(
// 创建FileInputStream,以该文件输入流创建FileChannel
FileChannel inChannel = new FileInputStream(f).getChannel();
// 以文件输出流创建FileBuffer,用以控制输出
FileChannel outChannel = new FileOutputStream("a.txt")
.getChannel())
{
// 将FileChannel里的全部数据映射成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel
.MapMode.READ_ONLY , 0 , f.length()); // ①
// 使用GBK的字符集来创建解码器
Charset charset = Charset.forName("GBK");
// 直接将buffer里的数据全部输出
outChannel.write(buffer); // ②
// 再次调用buffer的clear()方法,复原limit、position的位置
buffer.clear();
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 使用解码器将ByteBuffer转换成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
// CharBuffer的toString方法可以获取对应的字符串
System.out.println(charBuffer);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
如果使用Channel的map方法一次性将所有的文件内容映射到内存中可能会引起性能下降,这里可以结合Channel和Buffer来多次读取数据。
代码例子:
public class ReadFile
{
public static void main(String[] args)
throws IOException
{
try(
// 创建文件输入流
FileInputStream fis = new FileInputStream("ReadFile.java");
// 创建一个FileChannel
FileChannel fcin = fis.getChannel())
{
// 定义一个ByteBuffer对象,用于重复取水
ByteBuffer bbuff = ByteBuffer.allocate(64);
// 将FileChannel中数据放入ByteBuffer中
while( fcin.read(bbuff) != -1 )
{
// 锁定Buffer的空白区
bbuff.flip();
// 创建Charset对象
Charset charset = Charset.forName("GBK");
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 将ByteBuffer的内容转码
CharBuffer cbuff = decoder.decode(bbuff);
System.out.print(cbuff);
// 将Buffer初始化,为下一次读取数据做准备
bbuff.clear();
}
}
}
}
4.编码:是把明文的字符序列转换成计算机理解的二进制序列,解码:是把二进制序列转换成普通人能看懂的明文字符串,如图:
代码例子:
public class CharsetTransform
{
public static void main(String[] args)
throws Exception
{
// 创建简体中文对应的Charset
Charset cn = Charset.forName("GBK");
// 获取cn对象对应的编码器和解码器
CharsetEncoder cnEncoder = cn.newEncoder();
CharsetDecoder cnDecoder = cn.newDecoder();
// 创建一个CharBuffer对象
CharBuffer cbuff = CharBuffer.allocate(8);
cbuff.put('A');
cbuff.put('B');
cbuff.put('C');
cbuff.flip();
// 将CharBuffer中的字符序列转换成字节序列
ByteBuffer bbuff = cnEncoder.encode(cbuff);
// 循环访问ByteBuffer中的每个字节
for (int i = 0; i < bbuff.limit() ; i++)
{
System.out.print(bbuff.get(i) + " ");
}
// 将ByteBuffer的数据解码成字符序列
System.out.println("\n" + cnDecoder.decode(bbuff));
}
}
运行结果:
65 66 67
ABC
5.文件锁:多个运行的程序需要并发修改同一个文件时,程序之间需要某种机制来进行通信,使用文件锁可以有效地阻止多个进程并发修改同一个文件,FileChannel提供lock和tryLock方法获得文件锁,其主要区别:lock如果没获得文件锁,程序将一直阻塞,而tryLock方法尝试锁定文件,如果没获得文件锁,它不会阻塞,而是返回null。
代码例子:
public class FileLockTest
{
public static void main(String[] args)
throws Exception
{
try(
// 使用FileOutputStream获取FileChannel
FileChannel channel = new FileOutputStream("a.txt")
.getChannel())
{
// 使用非阻塞式方式对指定文件加锁
FileLock lock = channel.tryLock();
// 程序暂停10s
Thread.sleep(100000);
// 释放锁
lock.release();
}
}
}
6.当某个目录下的文件发生变化时,可以用WatchService监控文件变化。
代码例子:
public class WatchServiceTest
{
public static void main(String[] args)
throws Exception
{
// 获取文件系统的WatchService对象
WatchService watchService = FileSystems.getDefault()
.newWatchService();
// 为E:盘根路径注册监听,监听其目录下是否有文件产生,修改,删除
Paths.get("E:/").register(watchService
, StandardWatchEventKinds.ENTRY_CREATE
, StandardWatchEventKinds.ENTRY_MODIFY
, StandardWatchEventKinds.ENTRY_DELETE);
while(true)
{
// 这个方法会一直等待,直到获取下一个文件改动事件
WatchKey key = watchService.take(); //①
for (WatchEvent<?> event : key.pollEvents())
{
System.out.println(event.context() +" 文件发生了 "
+ event.kind()+ "事件!");
}
// 重设WatchKey,重新获取文件变化
boolean valid = key.reset();
// 如果重设失败,退出监听
if (!valid)
{
break;
}
}
}
}
当运行此程序时,我在E盘的路径下创建一个文件后,又删除其文件,其输出结果为:
新建文本文档.txt 文件发生了 ENTRY_CREATE事件!
新建文本文档.txt 文件发生了 ENTRY_DELETE事件!