------- android培训、java培训、期待与您交流! ----------
毕老师:“原来对象只能存在于内存中,程序推出对象也随之消失,序列化可以通过流将内存中的对象存到硬盘。也成为对象的持久化或者可串行性。”
网上资料:
Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,
只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。
Java 对象的序列化可以使你将一个对象的状态(即它的成员变量,由此可知,对象序列化不会关注类中的静态变量)写入一个Byte流里,
并且可以从其它地方把该Byte 流里的数据读出来,重新构造一个相同的对象。
这种机制允许你将对象通过网络进行传播,并可以随时把对象持久化到数据库、文件等系统里。
要实现对象的序列化,就要用到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 接口。
当实例需要具有序列化接口时,抛出此异常。序列化运行时或实例的类会抛出此异常。参数应该为类的名称。
也就是: 想要序列化的类,必须实现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
(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即随机访问流。该类对象可随机访问文件,自身具备读写方法。
它不是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()读取。
接下来,认识一下用于操作字节数组的流: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开头,所以数字都是负数。中国码表兼容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。计算出中成绩。
并把学生的信息和计算出来的总分数按照顺序高低放在磁盘文件中。
思路:
import java.util.*;
import java.io.*;
class Student implements Comparable {
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 getStudents() throws Exception {
return getStudents(null);
}
public static Set getStudents(Comparator cmp)
throws Exception {
BufferedReader bufr = new BufferedReader(new InputStreamReader(
System.in));
String line = null;
Set stus = null;
if (cmp == null) {
stus = new TreeSet();
} else {
stus = new TreeSet(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 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 cmp = Collections.reverseOrder();
Set stus = StudentInfoTool.getStudents(cmp);
StudentInfoTool.writeToFile(stus);
}
public static void sop(Object obj) {
System.out.println(obj);
}
}
------- android培训、java培训、期待与您交流! ----------