黑马程序员 —— Java高级视频_IO输入与输出(第二十一天)

时间:2021-11-22 10:59:05

------- android培训java培训、期待与您交流! ----------

一    对象的序列化


1、什么是对象的序列化?


毕老师:“原来对象只能存在于内存中,程序推出对象也随之消失,序列化可以通过流将内存中的对象存到硬盘。也成为对象的持久化或者可串行性。”


网上资料:

Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,

只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长

Java 对象的序列化可以使你将一个对象的状态(即它的成员变量,由此可知,对象序列化不会关注类中的静态变量)写入一个Byte流里,

并且可以从其它地方把该Byte 流里的数据读出来,重新构造一个相同的对象

这种机制允许你将对象通过网络进行传播,并可以随时把对象持久化到数据库、文件等系统里。


2、ObjectOutputStream

要实现对象的序列化,就要用到ObjectOutputStream。

API文档:“ObjectOutputStream 将 Java 对象的基本数据类型和图形写入 OutputStream。

可以使用 ObjectInputStream 读取(重构)对象。通过在流中使用文件可以实现对象的持久存储。

如果流是网络套接字流,则可以在另一台主机上或另一个进程中重构对象。


查阅API文档中的方法进行了解。可知ObjectOutputStream可以操作基本数据类型

void  write(int val)  写入一个字节。
void  writeBoolean(boolean val)  写入一个 boolean 值。
void  writeByte(int val)  写入一个 8 位字节。


问:void write(int val)和void writeInt(int val)区别?

答:write(int val)写入一个字节,writeInt(int val)写入一个 32 位的 int 值。

    也就是前面只是写入低8位,后面全部四个8位都进行写入。


最后,我们留意到最重要的方法:void writeObject(Object obj) 将指定的对象写入 ObjectOutputStream。

对象的类、类的签名,以及类及其所有超类型的非瞬态和非静态字段的值都将被写入。

有了这个方法,我们就能开始上机了:

import java.io.*;

public class ObjectStreamDemo {
	public static void main(String[] args) throws IOException {
		writeObj();
	}

	public static void writeObj() throws IOException {
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("demo.txt"));
		oos.writeObject(new Person("lisi", 39, "kr"));
		oos.close();
	}
}

class Person {
	String name;
	int age;

	Person(String name, int age, String country) {
		this.name = name;
		this.age = age;
	}

	public String toString() {
		return name + ":" + age + ":";
	}
}

上面的程序,我们想把“lisi”这个对象存到硬盘中的demo.txt文件中,但却抛出了异常:

Exception in thread "main" java.io.NotSerializableException: Person

在API文档中找到一些这个接口的描述,NotSerializableException - 某个要序列化的对象不能实现 java.io.Serializable 接口。

当实例需要具有序列化接口时,抛出此异常。序列化运行时或实例的类会抛出此异常。参数应该为类的名称。


3、Serializable 接口


也就是: 想要序列化的类,必须实现java.io.Serializable 接口。


Serializable没有方法,只要实现这个接口就行,仅仅起标记作用。

没有方法的接口,通常称为标记接口。什么意思呢?就像我们办证,需要盖上公章别人才认可你符合资格。

Serializable接口使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,加了UID相当于给这个类盖了章(给类固定标识,方便序列化)。


修改程序,读出对象:


import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Demo {
	public static void main(String[] args) throws Exception {
		writeObj();
		readObj();
	}

	public static void writeObj() throws Exception {
		// 存的文件一般以object作为扩展名
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
				"demo.txt"));
		// 可以存入多个对象
		// 取的时候,readObject()几次就取几个对象
		oos.writeObject(new Person("lisi", 39, "kr"));
		oos.close();
	}

	public static void readObj() throws Exception {
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
				"demo.txt"));
		Person p = (Person) ois.readObject();
		sop(p);
		ois.close();
	}

	public static void sop(Object obj) {
		System.out.println(obj);
	}
}

class Person implements Serializable {
	String name;
	// transient int age;普通变量被transient修饰后,也不能被序列化
	int age;
	// 静态是不能静态化的,只能把堆内存序列化,但是静态在方法区内
	static String country = "cn";

	Person(String name, int age, String country) {
		this.name = name;
		this.age = age;
		this.country = country;
	}

	public String toString() {
		return name + ":" + age + ":" + country;
	}
}


序列化后的Person对象,如果用记事本打开就像一堆乱码,不过其实我们也不需要看懂:

 sr Person       ) I ageL namet Ljava/lang/String;xp   't lisi


4、要注意的地方

(1)上面的程序,如果你首先只用了writeObj()对Person类序列化,然后注释掉writeObj()方法后,

再修改Person类的属性(例如改为私有),然后去readObj()。这种做法,会抛这样的异常

Exception in thread "main" java.io.InvalidClassException: Person; local class incompatible: stream classdesc serialVersionUID = 896258474541313055, local class serialVersionUID = 4941641260026466005

原因是 序列号因为是根据成员得来的, 修改属性的做法 使该类的序列版本号与从流中读取的类描述符的版本号不匹配,UID改变了也就因此出错了。

那么,我们可以显式声明自己的serialVersionUID 值,API文档中也强烈建议我们这么做

在Person类中加上 static final long serialVersionUID = 42L;  //UID自己确定


(2)此外,上面的程序我们在类中加入了静态属性cn国籍,写入的时候传值“lisi:39:kr”,再读取发现输出“lisi:39:cn”。
这是因为:静态是不能被序列化的,因为静态在方法区中,而序列化操作的对象在堆内存中


(3)如果对非静态的成员也不想序列化,可以加上transient关键词,例如transientint age;

 

(4)对象真正存到硬盘的时候,一般不存txt格式的文件,可以存“person.object”这样的形式。

 

(5)ObjectOutputStream和ObjectInputStream一般是成对出现的。


二    管道流


之前我们操作的流,一般都有类似于数组这样的中转站,

这样读取流和写入流才会产生联系,否则它们之间没有关系。

而管道流就让它们产生关系,可以直接连接使用。管道流就涉及:PipedInputStream和PipedOutputStrea。

public class PipedInputStream extends InputStream

管道输入流应该连接到管道输出流;管道输入流提供要写入管道输出流的所有数据字节。
通常,数据由某个线程从 PipedInputStream 对象读取,并由其他线程将其写入到相应的 PipedOutputStream。
不建议对这两个对象尝试使用单个线程,因为这样可能死锁线程。
管道输入流包含一个缓冲区,可在缓冲区限定的范围内将读操作和写操作分离开。
如果向连接管道输出流提供数据字节的线程不再存在,则认为该管道已损坏。


public class PipedOutputStream extends OutputStream

可以将管道输出流连接到管道输入流来创建通信管道。管道输出流是管道的发送端。
通常,数据由某个线程写入 PipedOutputStream 对象,并由其他线程从连接的 PipedInputStream读取。
不建议对这两个对象尝试使用单个线程,因为这样可能会造成该线程死锁。
如果某个线程正从连接的管道输入流中读取数据字节,但该线程不再处于活动状态,则该管道被视为处于毁坏状态。


从两个类的介绍可知:涉及到输入和输出谁先执行的问题,需要使用多线程来解决这个问题,不建议使用单线程。这是涉及到多线程技术的IO流对象。


示例代码:

import java.io.*;

public class Demo {
	public static void main(String[] args) throws Exception {
		PipedInputStream in = new PipedInputStream();
		PipedOutputStream out = new PipedOutputStream();
		in.connect(out);
		Read r = new Read(in);
		Write w = new Write(out);
		new Thread(r).start();
		new Thread(w).start();
	}

	public static void sop(Object obj) {
		System.out.println(obj);
	}
}

class Read implements Runnable {
	private PipedInputStream in;

	Read(PipedInputStream in) {
		this.in = in;
	}

	public void run() {
		try {
			byte[] buf = new byte[1024];
			System.out.println("读取前...没有数据阻塞");
			int length = in.read(buf);
			System.out.println("读到数据,阻塞结束.");
			String s = new String(buf, 0, length);
			System.out.println(s);
			in.close();
		} catch (Exception e) {
			throw new RuntimeException("管道读取流失败");
		}
	}
}

class Write implements Runnable {
	private PipedOutputStream out;

	Write(PipedOutputStream out) {
		this.out = out;
	}

	public void run() {
		try {
			System.out.println("开始写入数据,等待3秒.");
			Thread.sleep(3000);
			out.write("piped lai la".getBytes());
			out.close();
		} catch (Exception e) {
			throw new RuntimeException("管道输出流失败");
		}
	}
}


最好上机观察程序的运行。

read()方法是阻塞式方法,没有内容时,必须等待。所以2个线程谁先执行没有影响,读取先执行的话会自动等待,直到有数据后再执行。

管道流是涉及到多线程技术的IO流对象。集合涉及IO流的是properties。


三    RandomAccessFile


RandomAccessFile即随机访问流。该类对象可随机访问文件,自身具备读写方法。

它不是IO体系中的子类,直接继承Object,自成一个体系。但又是IO包的成员,因为其具备读和写的功能,

对象内部封装有大型的byte数组和指针,而且通过指针对元素进行操作。获取元素就是读或者写。

完成读写的原理:内部封装了字节输入流和字节输出流。许多方法和流的操作一致。

通过构造函数,可以看出该类只能操作硬盘上的文件,其他如内存、键盘录入都不可以操作。构造函数也是将流作为参数。


(1) 操作文件的模式:只接受4种值:只读“r”、读写“rw”、“rws”、“rwd”。注意模式的设置。

(2) 可以直接写入基本数据类型。例如,字节流里的write()方法只能写出最后8位,即将int转成byte,只能写出1个字节。

     再就是自动通过GBK表进行编码。可以通过writeInt()进行修正,可以写出4个字节。同样,通过readInt()获得4个字节的数据。

     内部封装了字节数组和指针,通过设置指针获得对应的字节,从而获得任意(Random)的数据。

    但是数据需要有一定规律或次序,否则很难读写。而且数据需要分段,事先要考虑好单个数据的大小,

    例如,姓名需要留出16个字节,即8个汉字的位置。用空来补位。

    通过getFilePointer()获取指针位置。

(3) 最重要的是:通过seek()设置指针位置,进而读取或写入;向前向后都可以。

    通过skipBytes(intx)跳过指定的字节数,获得字节。但是只能向前跳,不能向后跳。

    可以随机读取,也可以随机写入到指定位置。重新写入的话,不会覆盖文件,只会覆盖部分数据。

    不同于输出流,输出流会直接覆盖整个文件。

    如果模式为只读r,不会创建文件,只会读取已有的文件;如果该文件不存在,则会出现异常。

    如果模式为读写rw,该对象的构造函数要操作的文件如果不存在,会自动创建;如果存在,则不会覆盖,只会不断的写入。

    可以实现数据的分段写入,用一个线程来负责一段数据的写入,互相直接没有干扰。

    例如,多线程下载。多个线程同时写入数据,写完后拼成一个完整的文件。

    普通流是必须从头到尾一次性写成。IO中只有这个类可以完成多线程写入


import java.io.*;

public class Demo {
	public static void main(String[] args) throws Exception {
		// write();
		// read();
		writeFile_2();
	}

	public static void writeFile_2() throws Exception {
		RandomAccessFile raf = new RandomAccessFile("demo.txt", "rw");
		// raf.seek(32);
		raf.seek(8);
		raf.write("周期".getBytes());
		// raf.writeInt(87);
		raf.close();
	}

	public static void write() throws Exception {
		RandomAccessFile raf = new RandomAccessFile("demo.txt", "rw");
		raf.write("李四".getBytes());
		// write写入int的最低八位
		// raf.write(97);
		// writeInt写入int 的 全部 4个八位
		raf.writeInt(97);
		raf.write("王五".getBytes());
		raf.writeInt(99);
		raf.close();
	}

	public static void read() throws Exception {
		RandomAccessFile raf = new RandomAccessFile("demo.txt", "rw");
		byte[] buf = new byte[4];
		// read为读单个字节 readInt() 为读4个字节
		raf.read(buf);
		String str = new String(buf);
		sop(str);
		// 调整对象中指针
		raf.seek(0);
		// 跳过指定的字节数,只能往后跳,不能往回跳
		raf.skipBytes(8);
		int num = raf.readInt();
		sop(num);

		raf.close();
	}

	public static void sop(Object obj) {
		System.out.println(obj);
	}
}


四    操作基本数据类型的流对象


接下来,认识一下操作基本数据类型的流:DataInputStream 与 DataOutputStream。

凡是操作基本数据类型 ,就用它们。先看一下它们的继承体系:


java.lang.Object
   |--- java.io.InputStream
      |--- java.io.FilterInputStream
         |--- java.io.DataInputStream


java.lang.Object
   |--- java.io.OutputStream
      |--- java.io.FilterOutputStream
         |--- java.io.DataOutputStream


DataInputStream和DataOutputStream是可以用于操作基本数据类型数据的流对象,也就是将基本数据类型和流关联起来

功能是操作基本数据类型,所以构造函数就需要传入流(而且是字节流),如下所示:

DataOutputStream(OutputStream out)、DataInputStream(InputStream in)


然后根据API文档中的方法描述,上机了解两个流:

import java.io.*;

public class Demo {
    public static void main(String[] args) throws Exception {
        writeData();
        readData();
        // writeUTFDemo(); UTF-8 修改版会多了点东西,只能用DataOutputStream对象读
        // readUTFDemo();
        // writeUTFNormal();
    }

    public static void writeData() throws IOException { //
        DataOutputStream dos = new DataOutputStream(new FileOutputStream(
                "demo.txt"));
        dos.writeInt(234);
        dos.writeBoolean(true);
        dos.writeDouble(9887.543);
        dos.close();
    }

    public static void readData() throws IOException {
        DataInputStream dos = new DataInputStream(new FileInputStream(
                "demo.txt"));
        sop(dos.readInt());
        sop(dos.readBoolean());
        sop(dos.readDouble());
        dos.close();
    }

    public static void writeUTFDemo() throws IOException {
        DataOutputStream dos = new DataOutputStream(new FileOutputStream(
                "demo.txt"));
        dos.writeUTF("你好");
    }

    public static void readUTFDemo() throws IOException {
        DataInputStream dis = new DataInputStream(new FileInputStream(
                "demo.txt"));
        sop(dis.readUTF());
        dis.close();
    }

    public static void writeUTFNormal() throws IOException {
        OutputStreamWriter dos = new OutputStreamWriter(new FileOutputStream(
                "demo.txt"), "utf-8");
        OutputStreamWriter dos2 = new OutputStreamWriter(new FileOutputStream(
                "demo.txt"), "gbk");
        dos.write("你好");
        dos2.write("我好");
        dos.close();
        dos2.close();
    }

    public static void sop(Object obj) {
        System.out.println(obj);
    }
}

存入文本文件中,记事本会将存入的字节按照GBK编码表翻译成字符,基本无法识别。

因为数据最重要的读取使用。读取数据时需要按照顺序,使用不同的数据类型读取。

写入的顺序:基本数据→字节字符(文本文件)。

读取的顺序:字符(文本文件)字节基本数据。

writeUTF(String str),使用UTF码写入。只能用readUTF()读取。


五    ByteArrayStream


接下来,认识一下用于操作字节数组的流ByteArrayInputStreamByteArrayOutputStream

  • ByteArrayInputStream:在构造的时候,需要接收数据源,而且数据源是一个字节数组
  • ByteArrayOutputStream:在构造的时候,不用定义数据目的,因为该对象中已经内部封装了可变长度的字节数组,这就是数据目的地

API文档: 

public class ByteArrayOutputStream extendsOutputStream

  此类实现了一个输出流,其中的数据被写入一个 byte 数组。

  缓冲区会随着数据的不断写入而自动增长。可使用 toByteArray()和 toString() 获取数据。

publicclass ByteArrayInputStream extends InputStream

  ByteArrayInputStream包含一个内部缓冲区,该缓冲区包含从流中读取的字节。内部计数器跟踪read 方法要提供的下一个字节。


因为这两个流对象都操作的是数组,按照之前讲解流的操作规律,它俩源和目的都是内存

所以,使用完后不需要进行close(),即使被close()它们仍可被调用,而不会产生任何IOException。


流操作规律:

源设备:键盘(System.in)、硬盘(FileStream)、内存(ArrayStream)

目的设备: 控制台(System.out)、硬盘(FileStream)、内存(Array.Stream)


import java.io.*;

public class Demo {
	public static void main(String[] args) throws Exception {
		// 数据源
		ByteArrayInputStream bis = new ByteArrayInputStream("abcdefg".getBytes());
		// 数据目的
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		int by = 0;
		while ((by = bis.read()) != -1) {
			bos.write(by);
		}
		sop(bos.size());
		sop(bos.toString());
	}

	public static void sop(Object obj) {
		System.out.println(obj);
	}
}

针对源、目的是内存的情况,使用字节数组流最方便。将字节数组封装在其中,提高了复用性。

对于数组的操作,要么设置角标值 ,要么获取角标那个值,反应到IO中就是读和写,

字节数组流的本质就是用流的思想操作数组。


还要留意一下,ByteArrayOutputStream类的public voidwriteTo(OutputStream out)throws IOException方法。
将此 byte 数组输出流的全部内容写入到指定的输出流参数中,这与使用 out.write(buf, 0, count) 调用该输出流的 write 方法效果一样。

若要操作文字,操作字符数组的流CharArrayReader和CharArrayWriter,

操作字符数组的流StringReader和StringWriter,它们的操作也类似于类似于字节数组流,自行参考学习。



六    转换流的字符编码


现在来说一下字符编码


1、初步认识:

  • 字符流的出现是为了方便操作字符(方便的原因是加了编码表),更重要的是加入了编码转换
  • 字节和字符的转换,要通过两个子类转换流来完成
  • |--- InputStreamReader  加了编码表
  • |---OutputStreamWriter  加了编码表
  • 另外,printStream和printWriter也都加了编码表,但它俩只能打印而不能读取
  • 在两个对象进行构造的时候可以加入字符集(编码表)


2、编码表的由来

计算机只能识别二进制数据,早期由来是电信号。

为了方便应用计算机,让它可以识别各个国家的文字,

就将各个国家的文字用数字来表示,并一一对应,形成一张表。这就是编码表。


3、各种编码表简介

  • ASCII:美国标准信息交换码,一个字节7位。
  • IOS8859-1:拉丁码表,欧洲码表,一个字节8位。
  • GB2312:中国码表,GBK,升级版中国码表,两个字节16位。
  • Unicode:各国码表之间有冲突,所以产生了Unicode国际码表,两个字节16位,java中的字符使用此码表。
  • UTF-8:国际码表的升级版,三个字节,加了标识头方便区分码表,目前的通用码表。

除了美国表,其他都是以1开头,所以数字都是负数。中国码表兼容ASCII码,因为有拼音。


示例代码:

import java.io.*;

public class Demo {
	public static void main(String[] args) throws Exception {
		writeText();
		readText();
	}

	public static void writeText() throws Exception {
		OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("demo.txt"), "UTF-8");
		osw.write("你好");
		osw.close();
	}

	public static void readText() throws Exception {
		InputStreamReader isw = new InputStreamReader(new FileInputStream("demo.txt"), "UTF-8");
		char[] buf = new char[20];
		int len = isw.read(buf);
		String str = new String(buf, 0, len);
		sop(str);
		isw.close();
	}

	public static void sop(Object obj) {
		System.out.println(obj);
	}
}


注意输出乱码,有可能是因为查了不对的表。


七    字符编码


对于字符,有编码和解码的操作,其实就是“看得懂的变成看不懂的,看不懂的变成看得懂的”。

编码:将字符串变成字节数组,String → byte[],用str.getBytes()方法;

解码:字节数组变成字符串,byte[]String,用new String(byte)方法。


示例代码:

import java.io.*;
import java.util.*;

public class Demo {
	public static void main(String[] args) throws Exception {
		String s = "你好";
		byte[] bs1 = s.getBytes("GBK");// 可以传入字符集。s.getBytes("utf-8");
		System.out.println(Arrays.toString(bs1));// [-60, -29, -70, -61]
		String s1 = new String(bs1, "iso8859-1");
		byte[] bs2 = s1.getBytes("iso8859-1");
		String s2 = new String(bs2, "GBK");
		sop(s2);
	}

	public static void sop(Object obj) {
		System.out.println(obj);
	}
}

存在硬盘的数据本质上都是二进制,然后转成编码表对应的数字。


使用a码表翻译b码表的文件,可以运行,但无法输出正确内容。

例如Tomcat服务器用ISO8859-1,编web应用程序往服务器保存中文信息(中国区常用的只有GBK和UTF-8),就可能会产生这样的情况。

对于这种已经编码错误的情况,此时需要使用ISO8859-1编码,再使用原码表解码,即可获得原有字符。

	String s1 = new String(bs1, "iso8859-1");
	byte[] bs2 = s1.getBytes("iso8859-1");
	String s2 = new String(bs2, "GBK");

但此方法不适用于UTF-8,因为UTF-8也识别中文,会产生不必要的字节。


八    字符编码 - “联通”


来讲一个特殊的现象,在Windows系统中新建一个记事本,然后只保存“联通”这两个字。重新打开记事本,会输出乱码。


import java.io.*;
import java.util.*;

public class Demo {
    public static void main(String[] args) throws Exception {
        String s = "联通";
        byte[] by = s.getBytes("gbk");
        for (byte b : by) {
            sop(Integer.toBinaryString(b & 255));
        }
    }

    public static void sop(Object obj) {
        System.out.println(obj);
    }
}


输出:

11000001
10101010
11001101
10101000

这是因为,“联通”这两个字的GBK编码字节的二进制,正好符合UTF-8编码的特征,使得记事本会按照UTF-8解码,导致了错误。

这是极少的GBK和UTF-8重复的地方,可以通过加上汉字来修正。例如像“啊联通”这样在前面随便加个中文字。


备注:UTF-8 会加标识头信息,通过不同的1和0的排列,以便确定每次读取几个字节。见下图:


黑马程序员 —— Java高级视频_IO输入与输出(第二十一天)

可以看出标识头信息与“联通”的GBK码二进制相符。


九    练习


练习:

有五个学生,每个学生有3们课程的成绩,从键盘输入以上数据(包括姓名,三门课程成绩)

输入格式:如:zhangsan,30,40,60。计算出中成绩。

并把学生的信息和计算出来的总分数按照顺序高低放在磁盘文件中

  • 描述学生对象
  • 定义一个可以操作学生对象的工具类


思路:

  • 通过获取键盘录入的一行数据,并将该行中的信息取出封装成学生对象
  • 因为学生对象有很多,那么就需要存储使用到集合,因为要对学生的总分排序,所以可以使用TreeSet
  • 将集合中的信息写入到一个文件中


import java.util.*;
import java.io.*;

class Student implements Comparable<Student> {
	private String name;
	private int ma, cn, en;
	private int sum;

	Student(String name, int ma, int cn, int en) {
		this.name = name;
		this.ma = ma;
		this.cn = cn;
		this.en = en;
		sum = ma + cn + en;
	}

	public String getName() {
		return name;
	}

	public int getSum() {
		return sum;
	}

	public int hashCode() {
		return name.hashCode() + (sum * 78);
	}

	public boolean equals(Object obj) {
		if (!(obj instanceof Student)) {
			throw new ClassCastException("类型不匹配");
		}
		Student stu = (Student) obj;
		return this.equals(stu.getName()) && this.sum == stu.getSum();
	}

	public int compareTo(Student s) {
		int num = new Integer(this.sum).compareTo(new Integer(s.sum));
		if (num == 0) {
			return this.name.compareTo(s.name);
		}
		return num;
	}

	public String toString() {
		return "student[name=" + name + ",ma=" + ma + ",cn=" + cn + ",en=" + en
				+ ",sum=" + sum + "]";
	}
}

class StudentInfoTool {
	public static Set<Student> getStudents() throws Exception {
		return getStudents(null);
	}

	public static Set<Student> getStudents(Comparator<Student> cmp)
			throws Exception {
		BufferedReader bufr = new BufferedReader(new InputStreamReader(
				System.in));
		String line = null;
		Set<Student> stus = null;
		if (cmp == null) {
			stus = new TreeSet<Student>();
		} else {
			stus = new TreeSet<Student>(cmp);
		}
		while ((line = bufr.readLine()) != null) {
			if ("over".equals(line)) {
				break;
			}
			String[] info = line.split(",");
			Student stu = new Student(info[0], Integer.parseInt(info[1]),
					Integer.parseInt(info[2]), Integer.parseInt(info[2]));
			stus.add(stu);
		}
		bufr.close();
		return stus;
	}

	public static void writeToFile(Set<Student> stus) throws Exception {
		BufferedWriter bufw = new BufferedWriter(new FileWriter("demo.txt"));
		for (Student stu : stus) {
			bufw.write(stu.toString());
			bufw.newLine();
			bufw.flush();
		}
		bufw.close();
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Comparator<Student> cmp = Collections.reverseOrder();
		Set<Student> stus = StudentInfoTool.getStudents(cmp);
		StudentInfoTool.writeToFile(stus);
	}

	public static void sop(Object obj) {
		System.out.println(obj);
	}
}


------- android培训java培训、期待与您交流! ----------