------- 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
接下来,认识一下用于操作字节数组的流:ByteArrayInputStream与ByteArrayOutputStream。
- 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的排列,以便确定每次读取几个字节。见下图:
可以看出标识头信息与“联通”的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培训、期待与您交流! ----------