流与文件(二)
文本流
前面讨论的是二进制的输入输出,如果直接打开文件,会发现里面不是我们能读懂的内容。(用记事本打开里面是些空格)虽然二进制I/O速度快且效率高,但不易于人们阅读。java中的字符串,使用的是Unicode字符,例如字符串"1234"在字符编码中实际上是【00 31 00 32 00 33 00 34】,然而,Java所运行的环境有自己的字符编码,例如Windows用ASCII码,编码为【31 32 33 34】。为了在运行环境的编码和Unicode编码之间转换,Java提供了一套流过滤器。例如InputStreamReader/OutputStreamWriter
InputStreamReader in = new InputStreamReader(System.in);//从控制台读入,并自动将其转化为Unicode码。
字符集
在JDK1.4中引入的java.nio包通过引入Charset类来统一字符集的转换。
字符集给出了双字节Unicode码序列与在本地字符编码中采用的字节序列间的映射。一旦有了字符集,就可以用它在Unicode字符串和字节序列编码之间进行转换。
文本输出
进行文本输出时,应该使用PrintWriter。
PrintWriter out = new PrintWriter(new FileOutputStream("employee.txt"));
PrintWriter(OutputStream)构造器自动增加一个OutputStreamWriter来将Unicode字符转换为本地字符。
PrintWriter中有print方法和println方法,用以写入数据。
String name = "Harry Hacker"; double salary = 75000; out.print(name); out.print(' '); out.println(salary);
这将下列字符
Harry Hacker 75000
写入输出流out中。随后字符被转换为字节病最终进入文件employee.txt中。
PrintWriter总是缓冲的,可以通过PrintWriter(Writer, boolean)构造器中的第二个参数来开启或关闭自动刷新。如果开启,那么println将刷新缓冲区。
文本输入
BufferedReader类,readLine方法,以行的方式读取文本。
BufferedReader in = new BufferedReader(new FileReader("employee.txt"));
如果没有输入数据,readLine方法返回null。
FileReader类已经把本地字节转化为Unicode字符。对于其他输入源,需要使用InputStreamReader
BufferedReader in = BufferedReader(new InputStreamReader(System.in));
流的使用
分隔符输出,例如Employee类的如下记录:
Harry Hacker|35500|1989|10|1
Carl Cracker|75000|1987|12|15
Tony Tester|38000|1990|3|15
每个实例域由分隔符【|】隔开。
实现的方法是在Employee类中增加一个方法:writeData
public void writeData(PrintWriter out) throws IOException { GregorianCalendar calendar = new GregorianCalendar(); calendar.setTime(hireDay); out.println(name + "|" + salary + "|" + calendar.get(Calendar.YEAR) + "|" + (calendar.get(Calendar.MONTH) + 1) + "|" + calendar.get(Calendar.DAY_OF_MONTH)); }
Java中,处理带分隔符的字符串,使用StringTokenizer类。(C++中使用string类的find和substr方法)
StringTokenizer tokenizer = new StringTokenizer(line, "|");
也可以在一个字符串里指定多个分隔符,例如:
StringTokenizer tokenizer = new StringTokenizer(line, "|,;");
如果不指定分隔符集合,默认的就是"\t\n\r",即所有的空白字符(空格、tab、新行,回车)。
while(tokenizer.hasMoreTokens()) { String token = tokenizer.nextToken(); process token; }
这与C语言中的strtok函数类似
char *p = strtok("a.b.c.d.e",".") while(p != NULL) { printf("%s\n",p); p = strtok(NULL, "."); }
下面,在Employee类中实现一个readData的方法,来读取带分隔符的数据。
public void readData(BufferedReader in) throws IOException { String s = in.readLine(); StringTokenizer t = new StringTokenizer(s, "|"); name = t.nextToken(); salary = Double.parseDouble(t.nextToken()); int y = Integer.parseInt(t.nextToken()); int m = Integer.parseInt(t.nextToken()); int d = Integer.parseInt(t.nextToken()); GregorianCalendar calendar = new GregorianCalendar(y,m-1,d); //GregorianCalendar uses 0 = January hireDay = calendar.getTime(); }
随机存取流
如果每一条记录的长度不相等,就没法用RandomAccessFile类的seek方法来定位第n条记录。
为了让每一条记录长度相等,我们需要自己定义一个方法。
static void writeFixedString(String s, int size, DataOutput out) throws IOException { int i; for(int i=0; i<size; i++) { if(i<s.length()) { out.writeChar(s.charAt(i)); } else { out.writeChar(0); } } }
而读取则需要读到一个0值字符为止。
static String readFixedString(int size, DataInput in)throws IOException { StringBuilder b = new StringBuilder(size); int i=0; while(i<size) { char ch = in.readChar(); i++; if(0 == ch) { break; } else { b.append(ch); } } in.skipBytes(2*(size-i)); return b.toString(); }
这里的StringBuilder类比String类的优势在于,如果不断对String进行【+】拼接,那么字符串的空间要一次一次重新分配。
java.lang.StringBuilder
StringBuilder()
StringBuilder(int length)//初始长度length
StringBuilder(String str)//初始内容str
int length()//返回builder的长度
StringBuilder append(String str/char c)//追加字符串str或字符c
void setCharAt(int i, char c)//将第i个代码单元设置成c
StringBuilder insert(int offset, String str/char c)//在offset位置插入一个字符串str或者字符c
StringBuilder delete(int startIndex, int endIndex)//删除从startIndex到endIndex-1的内容
String toString()//返回一个与builder内容相同的字符串
对象流
如果存储同类型的数据,使用固定长度的记录格式是一个很好的选择。但当类型不同,例如既有Employee类,又有其子类Manager类时,就不能这么做了。
存储对象的方法是序列化:序列化就是将一个对象的状态(各个成员变量)保存起来,然后在适当的时候再获得。
序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。
序列化的什么特点:
如果某个类能够被序列化,其子类也可以被序列化。声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态, transient代表对象的临时数据。
什么时候使用序列化:
一:对象序列化可以实现分布式对象。主要应用例如:RMI要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。
二:java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。
需要序列化的类必须实现Serializable接口,该接口没有任何方法,所以不需要对类进行任何修改。
一个类实现了Serializable接口后,就可以通过ObjectOutputStream类的对象进行存储,通过ObjectInputStream类的对象读取:
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.dat")); Employee harry = new Employee("Harry Hacker",50000,1989,10,1); Manager boss = new Manager("Carl Cracker",80000,1987,12,25); out.writeObject(harry); out.writeObject(boss);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Employee.dat")); Employee e1 = (Employee) in.readObject(); Employee e2 = (Employee) in.readObject();
读取的时候需要注意,读的顺序与写入的顺序是一致的,且readObject方法返回的是Object类型的对象,需要进行转化。如果需要动态查询对象的类型,可以使用getClass方法。
对象流类实现了DataInput/DataOutput接口,对于基本类型(非对象)的值,可以使用writeInt/readInt等方法。
前面提到过,不想被序列化的成员变量要声明为transient类型,实际上,Java中有些包里的类是不能被序列化的,当这些类的对象是另一个类的数据成员时,在类中就要声明为transient。否则对整个类进行序列化时会抛出NotSerializableException异常。如果想记录这些不能被序列化的对象,可以通过自己定义readObject和writeObject方法,来取代默认的这两种方法:这两种方法定义在被序列化的类中。
例如:LabeledPoint类中包含不能被序列化的Point2D.Double类的对象,现在要序列化LabeledPoint类的对象,就需要两步:
1.将Point2D.Double类的对象声明为transient类型;
2.在LabeledPoint类中定义自己的readObject/writeObject方法。
public class LabeledPoint implements Serializable { ... private void writeObject(ObjectOutputStream out)throws IOException { out.defaultWriteObject(); out.writeDouble(point.getX()); out.writeDouble(point.getY()); } private void readObject(ObjectInputStream out)throws IOException { in.defaultWriteObject(); double x = in.readDouble(); double y = in.readDouble(); point = new Point2D.Double(x,y); } private String label; private transient Point2D.Double point; }
类可以定义自己的机制,而不需要让序列化机制存储和恢复对象数据。要做到这点,必须实现Externalizable接口。这就需要定义下面两个方法:
public void readExternal(ObjectInputStream in) throws IOException, ClassNotFoundException;
public void writeExternal(ObjectOutputStream out) throws IOException;
不同于上一节介绍的readObject和writeObject方法,这些方法将负责整个对象(包括超类数据)的保存和恢复。下面是Employee类实现的这些方法:
public void readExternal(ObjectInputStream in) throws IOException { name = s.readUTF(); salary = s.rreadDouble(); hireDay = new Date(s.readLong()); } public void writeExternal(ObjectOutputStream out) throws IOException { s.writeUTF(name); s.writeDouble(salary); s.writeLong(hireDay.getTime()); }
注意:readObject/writeObject方法是私有的,并且只能被序列化机制调用;readExternal/writeExternal方法是共有的。