Java I/O(三)各种Reader和Writer读写器、RandomAccessFile随机访问文件、序列化
2019 01/01
八、Reader和Writer读写器
前面讲的输入输出流的基本单位都是字节,因此可以称为“字节流”,读写器是以字符为基本单位,可以称为“字符流”。它们的使用方法非常相似,因此我考虑有的地方就不再重点叙述甚至不再叙述。对于Reader和Writer还是要动一下手,编一下代码看一下字符和字节的区别。和InputStream相似,也是抽象类,不能实例化,所以直接使用最简单的CharArrayReader和CharArrayWriter代替。
CharArrayReader的API还是ByteArrayInputStream那几种方法,mark(),read(),skip()等,意外的是出现了一个ready()方法,应该和available()方法是一样的。该类主要作用是从字符数组中读取数据,具体信息如下:
CharArrayReader(char[] buf)
CharArrayReader(char[] buf, int offset, int length)
void close()
void mark(int readLimit)
boolean markSupported()
int read()
int read(char[] buffer, int offset, int len)
boolean ready()(新增)
void reset()
long skip(long charCount)
public static void main(String []args) { try { char [] a= {'A','B','C','D','E','a','b','c','d','e','武','一','鸣'}; CharArrayReader car=new CharArrayReader(a); for (int i=0;i<3;i++) { if (car.ready()) //判断是否能读
{ int c=car.read(); System.out.print((char) c); } //if此时必须加大括号,因为声明新的变量 } System.out.println(); if (!car.markSupported()) { System.out.println("mark not supported"); return ; } car.mark(0); car.skip(7); for (int i=0;i<3;i++) { if (car.ready()) { int c=car.read(); System.out.print((char) c); } } System.out.println(); car.reset(); for (int i=0;i<7;i++) { if (car.ready()) { int c=car.read(); System.out.print((char) c); } } }catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
输出结果:
ABC
武一鸣
DEabcde
和预期目标完全一致,同时祝贺嫦娥四号任务成功,我是航天爱好者(爱好比较杂,手动滑稽),期望今年的“胖五”和嫦娥五号也能成功完成任务。
言归正传,接下来了解CharArrayWriter,作用是将数据写入到字符数组中,而API方法可能较ByteArrayOutputStream多一些:
CharArrayWriter() CharArrayWriter(int initialSize) CharArrayWriter append(CharSequence csq, int start, int end) CharArrayWriter append(char c) CharArrayWriter append(CharSequence csq) void close() void flush() void reset() int size() char[] toCharArray() String toString() void write(char[] buffer, int offset, int len) void write(int oneChar) void write(String str, int offset, int count) void writeTo(Writer out)
可以看出新增方法比较多,不过不影响,没有特别难的方法:
public static void main(String []args) throws IOException { CharArrayWriter caw=new CharArrayWriter(); // caw.write(68); caw.write("ABCDE"); caw.write("FGHIJKLMN", 2, 5); caw.append("OPQ").append("RSTUVWXYZ武一鸣",9,12); System.out.print(caw.toString()); System.out.println(); System.out.print(caw.toCharArray()); System.out.println(); System.out.print(caw.size()); caw.close(); }
九、FileReader、FileWriter、BufferedReader、BufferedWriter、PrintWriter
可以看出和Stream完全相对应,所以混在一起简单讲解一下,FileReader和FileWriter能够更加适应中国人的使用习惯,因为Java把一个汉字也作为一个字符,所以不会像之前Stream涉及汉字出现一些奇怪的现象。
public static void main(String []args) throws IOException { FileWriter fw=new FileWriter("\\log.txt"); fw.write("我的名字叫做武一鸣"); fw.close(); FileReader fr=new FileReader("\\log.txt"); while(fr.ready()) { int i=fr.read(); System.out.print((char)i); } fr.close(); }
很简单的一个示例,但是没有关闭写入不会写入到文件,当然使用flush()也是相同的作用,同时也会覆盖原文件中的内容。
緩衝區的用法和Stream大致相同,但是多了个newLine()和readLine()方法:
public static void main(String []args) { try { BufferedWriter bw=new BufferedWriter(new FileWriter("\\text.txt")); bw.write("一二三四五六七"); bw.newLine(); bw.write("八九十"); bw.close(); BufferedReader br=new BufferedReader(new FileReader("\\text.txt")); String s=br.readLine(); System.out.println(s); String s2=br.readLine(); System.out.println(s2); br.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
输出结果:
一二三四五六七
八九十
至于PrintWriter,字符版PrintStream,构造函数比较多,甚至Stream都可,作用也是向“文件”中输入格式化内容。
PrintWriter(OutputStream out) PrintWriter(OutputStream out, boolean autoFlush) PrintWriter(Writer wr) PrintWriter(Writer wr, boolean autoFlush) PrintWriter(File file) PrintWriter(File file, String csn) PrintWriter(String fileName) PrintWriter(String fileName, String csn)
示例就不再做了,仅列出其API方法:
PrintWriter append(char c) PrintWriter append(CharSequence csq, int start, int end) PrintWriter append(CharSequence csq) boolean checkError() void close() void flush() PrintWriter format(Locale l, String format, Object... args) PrintWriter format(String format, Object... args) void print(float fnum) void print(double dnum) void print(String str) void print(Object obj) void print(char ch) void print(char[] charArray) void print(long lnum) void print(int inum) void print(boolean bool) PrintWriter printf(Locale l, String format, Object... args) PrintWriter printf(String format, Object... args) void println() void println(float f) void println(int i) void println(long l) void println(Object obj) void println(char[] chars) void println(String str) void println(char c) void println(double d) void println(boolean b) void write(char[] buf, int offset, int count) void write(int oneChar) void write(char[] buf) void write(String str, int offset, int count) void write(String str)
十、InputStreamReader和OutputStreamWriter
InputStreamReader是Reader和InputStream的桥接器,也即实现字符和字节的转换。InputStreamReader将从文件中读到的字节流转换成字符流,相似的,OutputStreamWriter将字符流转换为字节写入到文件中。
InputStreamReader有两个构造函数,第一个是InputStreamReader(InputStream in),第二个是
InputStreamReader(InputStream in, String charsetName)。
第一个能够自适应的检测文本的内容选择相应的字符集,例如:
public static void main(String []args) { try { InputStreamReader isr=new InputStreamReader(new FileInputStream("\\text.txt")); char []c = new char[28]; int i=isr.read(c); System.out.println(c); isr.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
使用三个文件实验,log.txt汉字数字混合,test.txt字母,text.txt纯汉字,如左图。使用第一种构造函数都能正常检测。(注,读取log时输出结果会出现换行,如右图)
接着测试第二种构造函数,第二个参数选择"gb2312"均能正确读出所有汉字、字母和数字,但不能识别繁体字。“utf-8”只能识别文件中的字母和数字,中文不能识别。这是常用的两种,还有繁体编码"Big5",但是没有实验成功,输出的是乱码。
对于OutputStreamWriter则是同样的,基本第一个构造函数和使用"gb2312"是一样的效果,奇怪的是utf-8和utf-16也能达到这样的效果,而当使用utf-32时,结果开始出现变化开始,数字和字母仍能正常显示,但汉字已经不能显示:
OutputStreamWriter osw =new OutputStreamWriter(new FileOutputStream("\\data.txt"),"utf-32"); osw.write("12345一二三四五ABCDE"); osw.close();
结果如图:
变宽了。。。
十一、RandomAccessFile随机访问文件
RandomAccessFile是一个类,即随机访问文件,它的作用支持随机访问文件某个位置,并且能同时进行读和写(不像那些FileInputStream只支持读,而FileOutputStream只支持写)
先从构造函数来看,有两个构造函数,分别书RandomAccessFile(String fileName,String mode)和RandomAccessFile(File file, String mode)。很自然的,就会关注到第二个参数,都有一个Mode,这个mode是描述打开的模式,常用的共有四个,分别是"r","rw","rws","rwd",相信学过C系的会有一些熟悉,使用fopen时的只读,只写等等,这个和意思那个差不多,r代表只读,写操作会抛出异常;rw是读写,既能读,又能写;rws和rwd牵涉到同步问题。
不再介绍单纯的读和写,转而实现在文件末尾或中间添加,并对同一个文件既读又写等特有功能:
public static void main(String []args) { try { RandomAccessFile raf=new RandomAccessFile("\\test.txt","rw"); System.out.print(raf.getFilePointer()); //指针位置开始都是0 raf.seek(1); //手动设为1 raf.writeBytes("123"); //在空一字符后写入“123” System.out.print(raf.getFilePointer()); //指针位置来到4 String s=raf.readLine(); //从位置4开始读取这一行剩下的内容 System.out.print(s); raf.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
getFilePointer()获取指针,指针在随机访问文件中是一个很重要的概念,seek()函数可以更改指针到指定位置,所有write和read操作都是从指针的位置开始。若想达到在文件末尾添加内容,直接将指针位置设置为文件长度即可,使用raf.length()即可获得。
但是在写入中文时总是会变为乱码,也就是只能写字母和数字,解决办法也有,就是不适用writeBytes(String s)和writeChars(String s),使用write(Bytes [] b),先将参数String使用getBytes转化为byte数组即可。
例如(文件初始内容为123456789):
raf.seek(1);
raf.writeBytes("一二三");
在位置1插入汉字,结果乱码:
改用(内容重置123456789):
raf.seek(1);
raf.write("一二三".getBytes());
结果正确,但是指针移动到位置7
十二、序列化Serializable和对象输入输出流ObjectInputStream、ObjectOutputStream
提到序列化就必然和对象流相关,对象流的作用就是可以传输对象,写入到文件中,而对象所属的类必须是实现Serializable接口的(但不需要实现任何方法)。序列化的作用就是保存对象的状态,通俗来说就是将对象转换为字节序列,方便传输和存储。
简单介绍完序列化的作用之后,了解以下序列化需要使用的场景:
a)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输对象的时候。
下面通过一个demo简单实现将序列化的对象传输存储到文件中然后读取出来。
try { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream("\\test.txt")); ArrayList<Integer> al=new ArrayList<Integer>(); al.add(1); al.add(2); oos.writeObject(al); System.out.println(al); oos.close(); ObjectInputStream ois=new ObjectInputStream (new FileInputStream("\\test.txt")); ArrayList a=(ArrayList)ois.readObject(); System.out.println(a.get(1).equals(a.get(1))); //不可直接使用等号 } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); }
[1, 2]
true
本例采用ArrayList类,该类已实现Serializable接口,将其写入test.txt再读取。文件中的内容:
很有可能是二进制文件。
接下来我们展示自己设计的一个类来试试(不要忘了实现Serializable)
try { ObjectOutputStream oo=new ObjectOutputStream(new FileOutputStream ("test2.txt")); cube c=new cube(1,2,3); oo.writeObject(c); System.out.println(c); ObjectInputStream oi=new ObjectInputStream(new FileInputStream ("test2.txt")); cube dup=(cube)oi.readObject(); System.out.println(dup); System.out.println(dup.equals(c)); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); }
其中cube类的定义
class cube implements Serializable { private int len; private int hei; private double wid; cube (int a,int b,double c) { len=a; hei=b; wid=c; } public String toString() { return len+" "+hei+" "+wid; } }
结果:
1 2 3.0
1 2 3.0
false
这里再提起一个equals的特性,这里引用一下定义:同一和相等,同一是指两个引用指向同一个对象,即“==”;而相等是一个方法,即equals()方法,由各类自己定义(覆盖),其中Object的equals方法等价于同一,因为没有任何覆盖它。
transient关键词的登场,专门为序列化而生的关键词,它修饰的变量不会被序列化存储,自然的反序列化是不会保存该变量的值,我们将上述的double变量加上修饰词transient。即
private int len; private int hei; private transient double wid;
其余代码均不变,结果变为:
1 2 3.0
1 2 0.0
false
为什么结果是0,那是因为不保存值之后就设置为默认值,那么再使用static呢?
private int len; private static int hei; private transient double wid;
结果并没有发生任何改变,但是我可是开了上帝视角的!重新设计实验,再写入文件后,修改引用所指的对象,
ObjectOutputStream oo=new ObjectOutputStream(new FileOutputStream ("test2.txt")); cube c=new cube(1,2,3); oo.writeObject(c); c=new cube(2,3,4); //重新赋值 System.out.println("c: "+c); ObjectInputStream oi=new ObjectInputStream(new FileInputStream ("test2.txt")); cube dup=(cube)oi.readObject(); System.out.println("dup: "+dup);
结果是:
c: 2 3 4.0
dup: 1 3 0.0
可以看见,即使你将对象写入完成了,静态变量发生变化也是会影响到反序列化的结果的,静态变量只会同类共享同一个值,但是普通变量则不会发生变化。
那么如何序列化transient和static变量呢?只需在cube类新增两个方法writeObject(ObjectOutputStream out)和readObject(ObjectInputStream in)
private void writeObject(ObjectOutputStream out) throws IOException{ out.defaultWriteObject();//使定制的writeObject()方法可以利用自动序列化中内置的逻辑。 out.writeInt(len); out.writeInt(hei); out.writeDouble(wid); } private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException{ in.defaultReadObject();//defaultReadObject()补充自动序列化 len=in.readInt(); hei = in.readInt(); wid = in.readDouble(); }
OK
此外,Thread和Socket类对象不支持序列化。
最后在I/O结束的之一历史性时刻(拖了将近4个月,,,)来介绍一下Externalizable
实现Externalizable接口必须实现writieExternal()和readExtermal()方法才能实现序列化,与Serializable不同自己负责序列化,不必赘述,一段代码即可解释
package Serializable; import java.io.Externalizable; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; class cube2 implements Externalizable { private int len; private int hei; private double wid; public cube2() {} //必须加上这个构造函数,否则下面标记的那一行出错 public cube2 (int a,int b,double c) { len=a; hei=b; wid=c; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(len); out.writeInt(hei); out.writeDouble(wid); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { len=in.readInt(); hei = in.readInt(); wid = in.readDouble(); } } public class External { public static void main(String []args) { try { ObjectOutputStream oo=new ObjectOutputStream(new FileOutputStream ("test2.txt")); cube2 c=new cube2(1,2,3); oo.writeObject(c); System.out.println("c: "+c); ObjectInputStream oi=new ObjectInputStream(new FileInputStream ("test2.txt")); cube2 dup=(cube2)oi.readObject(); //就是这一行 System.out.println("dup: "+dup); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
结果:
c: 1 2 3.0
dup: 1 2 3.0
而且transient和static也不会影响。