流与文件(二)

文本流

前面讨论的是二进制的输入输出,如果直接打开文件,会发现里面不是我们能读懂的内容。(用记事本打开里面是些空格)虽然二进制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, "|,;");

如果不指定分隔符集合,默认的就是" ",即所有的空白字符(空格、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
",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方法是共有的。

原文地址:https://www.cnblogs.com/johnsblog/p/4163121.html