Java基础
对象:
每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现有对象,可制作出新型对象。
每个对象都有一种类型。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。
同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收形状消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP 最重要的概念之一。
如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其做一些实际的事情,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”(Interface)定义的,对象的“类型”或“类”则规定了它的接口形式。“类型”与“接口”的等价或对应关系是面向对象程序设计的基础。
抽象类abstract继承了一个类后,那个方法就必须实现,否则继承的类也会变成“抽象”类。
垃圾收集器“知道”一个对象在什么时候不再使用,然后会自动释放那个对象占据的内存空间。采用这种方式,另外加上所有对象都从单个根类Object 继承的事实,而且由于我们只能在内存堆中以一种方式创建对象。Java 有一个特别的“垃圾收集器”,它会查找用new 创建的所有对象,并辨别其中哪些不再被引用。随后,它会自动释放由那些闲置对象占据的内存,以便能由新对象使用。
8种基本类型
一、4种整型
byte 1字节 -128——127
short 2 字节 -32,768 —— 32,767
int 4 字节 -2,147,483,648 ——2,147,483,647(超过20亿)
long 8 字节 -9,223,372,036,854,775,808——9,223,372,036854,775,807
注释:java中所有的数据类所占据的字节数量与平台无关,java也没有任何无符号类型
二、1种真值类型
boolean 1 位 True或者false
三、1种Unicode编码的字符单元
char 2 字节 整个Unicode字符集
四、 2种浮点类型
float 4 字节 32位IEEE 754单精度(有效位数 6 – 7位)
double 8 字节 64位IEEE 754双精度(有效位数15位)
3种引用类型
类class
接口interface
数组array
一、类Class引用
可以是我们创建的,这里我不多讲,主要是讲解几个java库中的类
Object :Object是一个很重要的类,Object是类层次结构的根类,每个类都使用Object作为超类,所有对象(包括数组)都实现这个类的方法。用Object可以定义所有的类。如:
Object object= new Integer(1); 来定义一个Interger类
Integer i=(Integer) object; 在来把这个Object强制转换成Interger类
String :String类代表字符串,Java 程序中的所有字符串字面值(如"abc")都作为此类的实例来实现。检查序列的单个字符、比较字符串、搜索字符串、提取子字符串、创建字符串副本、在该副本中、所有的字符都被转换为大写或小写形式。Date :Date表示特定的瞬间,精确到毫秒。Date的类一般现在都被Calendar 和GregorianCalendar所有代替
Void :Void 类是一个不可实例化的占位符类,它保持一个对代表 Java 关键字 void 的 Class 对象的引用。
同时也有对应的Class如:Integer Long Boolean Byte Character Double Float Short
二、接口interface引用
可以是我们创建的,这里我不多讲,主要是讲解几个java库中的接口interface
List:列表 ,此接口的用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引
(在列表中的位置)访问元素,并搜索列表中的元素。List 接口提供了两种搜索指定对象的方法。从性能的观点来看,应该小心使用这些方法。在很多实现中,它们将执行高开销的线性搜索。 List 接口提供了两 种在列表的任意位置高效插入和移除多个元素的方法。
add() : 在列表的插入指定元素。
remove():移除列表中指定位置的元素。
get(int index):返回列表中指定位置的元素。
Map:
K - 此映射所维护的键的类型
V - 映射值的类型 将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。
put(K key,V value):将指定的值与此映射中的指定键关联(可选操作)。如果此映射以前包含一个该键的映射关系,则用指定值替换旧值(当且仅当,返回 true 时,才能说映射 m 包含键 k 的映射关系)。
remove(Object key)如果存在一个键的映射关系,则将其从此映射中移除(可选操作)。更确切地讲,如果此映射包含从满足(key==null ? k==null :key.equals(k))的键 k 到值 v 的映射关系,则移除该映射关系。(该映射最多只能包含一个这样的映射关系。)
get(Object key):返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。
这里我们主要是用String List Map Object 是最常用Number ArrayListArrays等
可以查考jdk的api这些类和接口在java.lang :提供利用 Java 编程语言进行程序设计的基础类。和java.unit:包含 collection 框架、遗留的 collection 类、事件模型、日期和时间设施、国际化和各种实用工具类(字符串标记生成器、随机数生成器和位数组)。
1、public static void main(String [] args)中的string[]args是用来干什么的?
String[] args 这个字符串数组是保存运行main函数时输入的参数的,例如main函数所在的类名为test 那么你在cmd运行 java testa b c 时,args[0] = a ,args[1]=b, args[2]=c 你就可以在你的程序中调用你输入的这些变量了。参数间都是以空格隔开的,每个参数都被储存为String型的对象。可以换名字的,比如不想用args,可以用arg/agr/agg/arr/aag/arr,名字无所谓的。
2、为什么使用public static修饰main()
Java中,main()方法是java应用程序的入口方法。java虚拟机通过main方法找到需要启动的运行程序,并且检查main函数所在类是否被java虚拟机装载。如果没有装载,那么就装载该类,并且装载所有相关的其他类。因此程序在运行的时候,第一个执行的方法(注意是方法,而不是其他的,例如代码块)就是main()方法。通常情况下,如果要运行一个类的方法,必须首先实例化出来这个类的一个对象,然后通过"对象名.方法名()"的方式来运行方法,但是因为main是程序的入口,这时候还没有实例化对象,因此将main方法声明为static的,这样这个方法就可以直接通过“类名.方法名()”的方式来调用。
为什么使用void修饰main()
对于java中的main(),jvm有限制,不能有返回值,因此返回值类型为void。
关于参数String[]args的说明
main方法中还有一个输入参数,类型为String[],这个也是java的规范,main()方法中必须有一个入参,类细必须String[],至于字符串数组的名字,这个是可以自己设定的,根据习惯,这个字符串数组的名字一般和sun java规范范例中mian参数名保持一致,取名为args。
main()方法可以 throwsException
看到网上有说main()方法不能抛出异常,感觉有点疑惑,就亲自试验了一把,代码如下,运行正常。
public class ArgumentsOfMain {
public static void main(String[] args) throws Exception{
System.out.println("Hello World");
throw new Exception("hello world");
}
}
static ,final,static final 三个的区别
一、static
请先看下面这段程序:
public class Hello{
public static void main(String[] args){//(1)
System.out.println("Hello,world!");//(2)
}
}
看过这段程序,对于大多数学过Java 的来说,都不陌生。即使没有学过Java,而学过其它的高级语言,例如C,那你也应该能看懂这段代码的意思。它只是简单的输出“Hello,world”,一点别的用处都没有,然而,它却展示了static关键字的主要用法。
在1处,我们定义了一个静态的方法名为main,这就意味着告诉Java编译器,我这个方法不需要创建一个此类的对象即可使用。你还得你是怎么运行这个程序吗?一般,我们都是在命令行下,打入如下的命令(加下划线为手动输入):
javac Hello.java
java Hello
Hello,world!
这就是你运行的过程,第一行用来编译Hello.java这个文件,执行完后,如果你查看当前,会发现多了一个Hello.class文件,那就是第一行产生的Java二进制字节码。第二行就是执行一个Java程序的最普遍做法。执行结果如你所料。在2中,你可能会想,为什么要这样才能输出。好,我们来分解一下这条语句。(如果没有安装Java文档,请到Sun的官方网站浏览J2SE API)首先,System是位于java.lang包中的一个核心类,如果你查看它的定义,你会发现有这样一行:public static final PrintStream out;接着在进一步,点击PrintStream这个超链接,在METHOD页面,你会看到大量定义的方法,查找println,会有这样一行:
public void println(String x)。
好了,现在你应该明白为什么我们要那样调用了,out是System的一个静态变量,所以可以直接使用,而out所属的类有一个println方法。
静态方法:
通常,在一个类中定义一个方法为static,那就是说,无需本类的对象即可调用此方法。如下所示:
class Simple{
static void go(){
System.out.println("Go...");
}
}
public class Cal{
public static void main(String[] args){
Simple.go();
}
}
调用一个静态方法就是“类名.方法名”,静态方法的使用很简单如上所示。一般来说,静态方法常常为应用程序中的其它类提供一些实用工具所用,在Java的类库中大量的静态方法正是出于此目的而定义的。
静态变量
静态变量与静态方法类似。所有此类实例共享此静态变量,也就是说在类装载时,只分配一块存储空间,所有此类的对象都可以操控此块存储空间,当然对于final则另当别论了。看下面这段代码:
class Value{
static int c=0;
static void inc(){
c++;
}
}
class Count{
public static void prt(String s){
System.out.println(s);
}
public static void main(String[] args){
Value v1,v2;
v1=new Value();
v2=new Value();
prt("v1.c="+v1.c+"v2.c="+v2.c);
v1.inc();
prt("v1.c="+v1.c+"v2.c="+v2.c);
}
}
结果如下:
v1.c=0 v2.c=0
v1.c=1 v2.c=1
由此可以证明它们共享一块存储区。static变量有点类似于C中的全局变量的概念。值得探讨的是静态变量的初始化问题。我们修改上面的程序:
class Value{
static int c=0;
Value(){
c=15;
}
Value(int i){
c=i;
}
static void inc(){
c++;
}
}
class Count{
public static void prt(String s){
System.out.println(s);
}
Value v=new Value(10);
static Value v1,v2;
static{
prt("v1.c="+v1.c+"v2.c="+v2.c);
v1=new Value(27);
prt("v1.c="+v1.c+"v2.c="+v2.c);
v2=new Value(15);
prt("v1.c="+v1.c+"v2.c="+v2.c);
}
public static void main(String[] args){
Count ct=new Count();
prt("ct.c="+ct.v.c);
prt("v1.c="+v1.c+"v2.c="+v2.c);
v1.inc();
prt("v1.c="+v1.c+"v2.c="+v2.c);
prt("ct.c="+ct.v.c);
}
}
运行结果如下:
v1.c=0 v2.c=0
v1.c=27 v2.c=27
v1.c=15 v2.c=15
ct.c=10
v1.c=10 v2.c=10
v1.c=11 v2.c=11
ct.c=11
这个程序展示了静态初始化的各种特性。如果你初次接触Java,结果可能令你吃惊。可能会对static后加大括号感到困惑。首先要告诉你的是,static定义的变量会优先于任何其它非static变量,不论其出现的顺序如何。正如在程序中所表现的,虽然v出现在v1和v2的前面,但是结果却是v1和v2的初始化在v的前面。在static{后面跟着一段代码,这是用来进行显式的静态变量初始化,这段代码只会初始化一次,且在类被第一次装载时。如果你能读懂并理解这段代码,会帮助你对static关键字的认识。在涉及到继承的时候,会先初始化父类的static变量,然后是子类的,依次类推。非静态变量不是本文的主题,在此不做详细讨论,请参考Think in Java中的讲解。
静态类
通常一个普通类不允许声明为静态的,只有一个内部类才可以。这时这个声明为静态的内部类可以直接作为一个普通类来使用,而不需实例一个外部类。如下代码所示:
public class StaticCls{
public static void main(String[] args){
OuterCls.InnerCls oi=newOuterCls.InnerCls();
}
}
class OuterCls{
public static class InnerCls{
InnerCls(){
System.out.println("InnerCls");
}
}
}
输出结果会如你所料:
InnerCls
和普通类一样。内部类的其它用法请参阅Think in Java中的相关章节,此处不作详解。
二、this & super
在上一篇拙作中,我们讨论了static的种种用法,通过用static来定义方法或成员,为我们编程提供了某种便利,从某种程度上可以说它类似于C语言中的全局函数和全局变量。但是,并不是说有了这种便利,你便可以随处使用,如果那样的话,你便需要认真考虑一下自己是否在用面向对象的思想编程,自己的程序是否是面向对象的。好了,现在开始讨论this&super这两个关键字的意义和用法。
在Java中,this通常指当前对象,super则指父类的。当你想要引用当前对象的某种东西,比如当前对象的某个方法,或当前对象的某个成员,你便可以利用this来实现这个目的,当然,this的另一个用途是调用当前对象的另一个构造函数,这些马上就要讨论。如果你想引用父类的某种东西,则非super莫属。由于this与super有如此相似的一些特性和与生俱来的某种关系,所以我们在这一块儿来讨论,希望能帮助你区分和掌握它们两个。
在一般方法中
最普遍的情况就是,在你的方法中的某个形参名与当前对象的某个成员有相同的名字,这时为了不至于混淆,你便需要明确使用this关键字来指明你要使用某个成员,使用方法是“this.成员名”,而不带this的那个便是形参。另外,还可以用“this.方法名”来引用当前对象的某个方法,但这时this就不是必须的了,你可以直接用方法名来访问那个方法,编译器会知道你要调用的是那一个。下面的代码演示了上面的用法
public class DemoThis{
private String name;
private int age;
DemoThis(String name,int age){
setName(name); //你可以加上this来调用方法,像这样:this.setName(name);但这并不是必须的
setAge(age);
this.print();
}
public void setName(String name){
this.name=name;//此处必须指明你要引用成员变量
}
public void setAge(int age){
this.age=age;
}
public void print(){
System.out.println("Name="+name+"Age="+age);//在此行中并不需要用this,因为没有会导致混淆的东西
}
public static void main(String[] args){
DemoThis dt=new DemoThis("Kevin","22");
}
}
这段代码很简单,不用解释你也应该能看明白。在构造函数中你看到用this.print(),你完全可以用print()来代替它,两者效果一样。下面我们修改这个程序,来演示super的用法。
class Person{
public int c;
private String name;
private int age;
protected void setName(String name){
this.name=name;
}
protected void setAge(int age){
this.age=age;
}
protected void print(){
System.out.println("Name="+name+"Age="+age);
}
}
public class DemoSuper extends Person{
public void print(){
System.out.println("DemoSuper:");
super.print();
}
public static void main(String[] args){
DemoSuper ds=new DemoSuper();
ds.setName("kevin");
ds.setAge(22);
ds.print();
}
}
在DemoSuper中,重新定义的print方法覆写了父类的print方法,它首先做一些自己的事情,然后调用父类的那个被覆写了的方法。输出结果说明了这一点:
DemoSuper:
Name=kevin Age=22
这样的使用方法是比较常用的。另外如果父类的成员可以被子类访问,那你可以像使用this一样使用它,用“super.父类中的成员名”的方式,但常常你并不是这样来访问父类中的成员名的。
在构造函数中
构造函数是一种特殊的方法,在对象初始化的时候自动调用。在构造函数中,this和super也有上面说的种种使用方式,并且它还有特殊的地方,请看下面的例子:
class Person{
public static void prt(String s){
System.out.println(s);
}
Person(){
prt("A Person.");
}
Person(String name){
prt("A person name is:"+name);
}
}
public class Chinese extends Person{
Chinese(){
super(); //调用父类构造函数(1)
prt("A chinese.");//(4)
}
Chinese(String name){
super(name);//调用父类具有相同形参的构造函数(2)
prt("his name is:"+name);
}
Chinese(String name,int age){
this(name);//调用当前具有相同形参的构造函数(3)
prt("his age is:"+age);
}
public static void main(String[] args){
Chinese cn=new Chinese();
cn=new Chinese("kevin");
cn=new Chinese("kevin",22);
}
}
在这段程序中,this和super不再是像以前那样用“.”连接一个方法或成员,而是直接在其后跟上适当的参数,因此它的意义也就有了变化。super后加参数的是用来调用父类中具有相同形式的构造函数,如1和2处。this后加参数则调用的是当前具有相同参数的构造函数,如3处。当然,在Chinese的各个重载构造函数中,this和super在一般方法中的各种用法也仍可使用,比如4处,你可以将它替换为“this.prt”(因为它继承了父类中的那个方法)或者是“super.prt”(因为它是父类中的方法且可被子类访问),它照样可以正确运行。但这样似乎就有点画蛇添足的味道了。
最后,写了这么多,如果你能对“this通常指代当前对象,super通常指代父类”这句话牢记在心,那么本篇便达到了目的,其它的你自会在以后的编程实践当中慢慢体会、掌握。另外关于本篇中提到的继承,请参阅相关Java教程。
三、final
final在Java中并不常用,然而它却为我们提供了诸如在C语言中定义常量的功能,不仅如此,final还可以让你控制你的成员、方法或者是一个类是否可被覆写或继承等功能,这些特点使final在Java中拥有了一个不可或缺的地位,也是学习Java时必须要知道和掌握的关键字之一。
final成员
当你在类中定义变量时,在其前面加上final关键字,那便是说,这个变量一旦被初始化便不可改变,这里不可改变的意思对基本类型来说是其值不可变,而对于对象变量来说其引用不可再变。其初始化可以在两个地方,一是其定义处,也就是说在final变量定义时直接给其赋值,二是在构造函数中。这两个地方只能选其一,要么在定义时给值,要么在构造函数中给值,不能同时既在定义时给了值,又在构造函数中给另外的值。下面这段代码演示了这一点:
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
public class Bat{
final PI=3.14; //在定义时便给址值
final int i; //因为要在构造函数中进行初始化,所以此处便不可再给值
final List list; //此变量也与上面的一样
Bat(){
i=100;
list=new LinkedList();
}
Bat(int ii,List l){
i=ii;
list=l;
}
public static void main(String[] args){
Bat b=new Bat();
b.list.add(new Bat());
//b.i=25;
//b.list=new ArrayList();
System.out.println("I="+b.i+"List Type:"+b.list.getClass());
b=new Bat(23,new ArrayList());
b.list.add(new Bat());
System.out.println("I="+b.i+"List Type:"+b.list.getClass());
}
}
此程序很简单的演示了final的常规用法。在这里使用在构造函数中进行初始化的方法,这使你有了一点灵活性。如Bat的两个重载构造函数所示,第一个缺省构造函数会为你提供默认的值,重载的那个构造函数会根据你所提供的值或类型为final变量初始化。然而有时你并不需要这种灵活性,你只需要在定义时便给定其值并永不变化,这时就不要再用这种方法。在main方法中有两行语句注释掉了,如果你去掉注释,程序便无法通过编译,这便是说,不论是i的值或是list的类型,一旦初始化,确实无法再更改。然而b可以通过重新初始化来指定i的值或list的类型,输出结果中显示了这一点:
I=100 List Type:class java.util.LinkedList
I=23 List Type:class java.util.ArrayList
还有一种用法是定义方法中的参数为final,对于基本类型的变量,这样做并没有什么实际意义,因为基本类型的变量在调用方法时是传值的,也就是说你可以在方法中更改这个参数变量而不会影响到调用语句,然而对于对象变量,却显得很实用,因为对象变量在传递时是传递其引用,这样你在方法中对对象变量的修改也会影响到调用语句中的对象变量,当你在方法中不需要改变作为参数的对象变量时,明确使用final进行声明,会防止你无意的修改而影响到调用方法。
另外方法中的内部类在用到方法中的参变量时,此参变也必须声明为final才可使用,如下代码所示:
public class INClass{
void innerClass(final String str){
class IClass{
IClass(){
System.out.println(str);
}
}
IClass ic=new IClass();
}
public static void main(String[] args){
INClass inc=new INClass();
inc.innerClass("Hello");
}
}
final方法
将方法声明为final,那就说明你已经知道这个方法提供的功能已经满足你要求,不需要进行扩展,并且也不允许任何从此类继承的类来覆写这个方法,但是继承仍然可以继承这个方法,也就是说可以直接使用。另外有一种被称为inline的机制,它会使你在调用final方法时,直接将方法主体插入到调用处,而不是进行例行的方法调用,例如保存断点,压栈等,这样可能会使你的程序效率有所提高,然而当你的方法主体非常庞大时,或你在多处调用此方法,那么你的调用主体代码便会迅速膨胀,可能反而会影响效率,所以你要慎用final进行方法定义。
final类
当你将final用于类身上时,你就需要仔细考虑,因为一个final类是无法被任何人继承的,那也就意味着此类在一个继承树中是一个叶子类,并且此类的设计已被认为很完美而不需要进行修改或扩展。对于final类中的成员,你可以定义其为final,也可以不是final。而对于方法,由于所属类为final的关系,自然也就成了final型的。你也可以明确的给final类中的方法加上一个final,但这显然没有意义。
下面的程序演示了final方法和final类的用法:
final class final{
final String str="final Data";
public String str1="non finaldata";
final public void print(){
System.out.println("finalmethod.");
}
public void what(){
System.out.println(str+"\n"+str1);
}
}
public class FinalDemo { //extends final 无法继承
public static void main(String[] args){
final f=new final();
f.what();
f.print();
}
}
从程序中可以看出,final类与普通类的使用几乎没有差别,只是它失去了被继承的特性。final方法与非final方法的区别也很难从程序行看出,只是记住慎用。
final在设计模式中的应用
在设计模式中有一种模式叫做不变模式,在Java中通过final关键字可以很容易的实现这个模式,在讲解final成员时用到的程序Bat.java就是一个不变模式的例子。如果你对此感兴趣,可以参考阎宏博士编写的《Java与模式》一书中的讲解。
到此为止,this,static,supert和final的使用已经说完了,如果你对这四个关键字已经能够大致说出它们的区别与用法,那便说明你基本已经掌握。然而,世界上的任何东西都不是完美无缺的,Java提供这四个关键字,给程序员的编程带来了很大的便利,但并不是说要让你到处使用,一旦达到滥用的程序,便适得其反,所以在使用时请一定要认真考虑。
多线程
在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序设计问题都要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进程。
在一个程序中,这些独立运行的片断叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。多线程处理一个常见的例子就是用户界面。利用线程,用户可按下一个按钮,然后程序会立即作出响应,而不是让用户等待程序完成了当前任务以后才开始响应。
比较运算符:
public class Assignment {
public static void main(String[] args) {
Integer in1 = new Integer(47);
Integer in2 = new Integer(47);
System.out.println(in1 == in2); // false
System.out.println(in1 != in2); // true
System.out.println(in1.equals(in2)); // true
/**
两个Integer 对象都是相同的。但尽管对象的内容相同,句柄却是不同的,而==和!=比较的正好就是对象句柄。若想对比两个对象的实际内容是否相同,又该如何操作呢?此时,必须使用所有对象都适用的特殊方法equals()
*/
Number n1 = new Number();
Number n2 = new Number();
n1.i = 9;
n2.i = 47;
System.out.println("1: n1.i:" + n1.i +
", n2.i: " + n2.i);
n1 = n2;
//n1.i = n2.i;
System.out.println("2: n1.i:" + n1.i +
", n2.i: " + n2.i);
n1.i = 27;
System.out.println("3: n1.i:" + n1.i +
", n2.i: " + n2.i);
Value v1 = new Value();
Value v2 = new Value();
v1.i = v2.i = 100;
System.out.println(v1.equals(v2)); //false
/**这是由于equals()的默认行为是比较句柄。所以除非在自己的新类中改变了
equals(),否则不可能表现出我们希望的行为。大多数Java 类库都实现了equals(),所以它实际比较的是对象的内容,而非它们的句柄。*/
}
public static class Number {
int i;
}
public static class Value {
int i;
}
}
1: n1.i: 9, n2.i: 47
2: n1.i: 47, n2.i: 47
3: n1.i: 27, n2.i: 27
这是由于无论n1 还是n2都包含了相同的句柄,它指向相同的对象(最初的句柄位于n1 内部,指向容纳了值9的一个对象。在赋值过程中,那个句柄实际已经丢失;它的对象会由“垃圾收集器”自动清除)。
1: n1.i: 9, n2.i: 47
2: n1.i: 47, n2.i: 47
3: n1.i: 27, n2.i: 47
这样便可保留两个独立的对象,而不是将n1 和n2 绑定到相同的对象.
抽象方法:
即使不包括任何abstract 方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而且我们想禁止那个类的所有实例,这种能力就会显得非常有用。
“抽象方法”属于一种不完整的方法,只含有一个声明,没有方法主体。下面是抽象方法声明时采用的语法:abstract void X();
为什么需要有接口呢?
JAVA中的接口是一个抽象类型(abstract是抽象,总结的意思,论文中的ABSTRACT,是摘要的意思,那么),这种类型用来指定一个类(CLASS)必须实现(implement)(这种抽象定义)。
接口用interface关键字声明,并且只能包含方法签名和常量(即:用static final修饰的量)。
接口不能包含方法实体。(包括大括号在内的所有都不能有)
publicinterface FtRecordService {
public Long getRecordCount(RecordFilterfilter);
public RecordDetailgetDetailBySessionId(String tableName, String dstType, String sessionId,Integer uuid);
public RecordDetailgetDetailBySessionId4FulltextList(String tableName, String dstType, StringsessionId, Integer uuid);
}
在java中不允许一个类继承多个父类,但是要实现“一个类继承多个类的功能”就可以用接口,一个类实现多个多个接口从而间接实现多重继承。
@从本质上讲,接口也是是一种抽象类。这种抽象类中只包含常量和方法的定义,而没有变量和方法的实现。
@多个无关的类可以实现同一个接口。一个类可以实现多个无关的接口。与继承关系类似,接口与实现类之间存在多态性。
java提供的接口都在相应的包中,通过引入包可以使用java提供的接口。也可以自己定义接口,一个java源文件就是由类和接口做成的。
拿Thread类和Runnable接口为例:
Java中实现线程有两种方法,一种是继承Thread类,一种是实现Runnable接口
那么,对于某类A,其已经继承了某一父类B,有要求其实现线程相关功能,由于Java的单继承机制,A是无法继承Thread类的,此时就需要实现Runnable接口。
接口是抽象类的变体。在接口中,所有方法都是抽象的。多继承性可通过实现这样的接口而获得。接口中的所有方法都是抽象的,没有一个有程序体。接口只可以定义static final成员变量。
接口提供了很多的公共方法,而程序员自己编写的类可以有选择的去实现自己需要的接口,这样就比较有效率些,代码的可读性也好些,另外JAVA是单继承,但是可以实现多接口,所以接口的灵活性就更大了,另外,在一个团队里,每个人负责不同的模块,如果不使用接口,而用具体的类去实现不同的功能,那么一旦有修改,势必会影响到他人所编写的代码,而用接口,程序员修改的是自己编写的类,与他人的无关,这一切都因为接口提供了一套公共的方法,而每个程序员都有自己的一套实现方法,互不干扰。
线程创建与启动
1、 与线程编程有关的一些概念
2、 创建方式: 1 继承java.lang.Thread类 2 实现java.lang.Runnable接口
3、 线程体:public void run()方法,其内的程序代码决定了线程的行为和功能。线程启动: public void start (),线程启动后,需要获取cpu才能自动调用run()运行。 线程休眠: public void sleep(long ms), 线程将暂停,放弃cpu
Serializable接口
Serializable,就是java提供的通用数据保存和读取的接口。至于从什么地方读出来和保存到哪里去都被隐藏在函数参数的背后了。这样子,任何类型只要实现了Serializable接口,就可以被保存到文件中,或者作为数据流通过网络发送到别的地方。也可以用管道来传输到系统的其他程序中。
序列化定义:
序列化就是一种用来处理对象流的机制,所谓对象流就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化就是为了解决在对对象流进行读写操作时所引发的问题。
序列化的实现:将需要被序列化的类实现 Serializable 接口,该接口没有需要实现的方法, implementsSerializable 只是为了标注该对象是可被序列化的,然后使用输出流(FileOutputStream)来构造一个 ObjectOutputStream(对象流) 对象,接着,使用ObjectOutputStream 对象的 writeObject(Objectobj) 方法就可以将参数为obj的对象写出(即:保存其状态),要恢复的话则使用输入流。
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。
传一个对象的时候,对方读完字节流后,不知道你传的什么对象,所以没办法给转成原来的对象并解析对象的属性,这时候就要用到序列化和反序列化。
序列化的实现:将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implementsSerializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才是可序列化的。因此如果要序列化某些类的对象,这些类就必须实现Serializable接口。而实际上,Serializable是一个空接口,没有什么具体内容,它的目的只是简单的标识一个类的对象可以被序列化。
追问那进行序列化有什么好处呢?
回答什么情况下需要序列化
a)当你想把的内存中的对象写入到硬盘的时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输对象的时候;
再稍微解释一下:a)比如说你的内存不够用了,那计算机就要将内存里面的一部分对象暂时的保存到硬盘中,等到要用的时候再读入到内存中,硬盘的那部分存储空间就是所谓的虚拟内存。在比如过你要将某个特定的对象保存到文件中,我隔几天在把它拿出来用,那么这时候就要实现Serializable接口;
b)在进行java的Socket编程的时候,你有时候可能要传输某一类的对象,那么也就要实现Serializable接口;最常见的你传输一个字符串,它是JDK里面的类,也实现了Serializable接口,所以可以在网络上传输。
c)如果要通过远程的方法调用(RMI)去调用一个远程对象的方法,如在计算机A中调用另一台计算机B的对象的方法,那么你需要通过JNDI服务获取计算机B目标对象的引用,将对象从B传送到A,就需要实现序列化接口。
没有人说的话能全部准确,批判性的参考。
序列化定义:
序列化就是一种用来处理对象流的机制,所谓对象流就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化就是为了解决在对对象流进行读写操作时所引发的问题。
序列化的实现:将需要被序列化的类实现 Serializable 接口,该接口没有需要实现的方法, implementsSerializable 只是为了标注该对象是可被序列化的,然后使用输出流(FileOutputStream)来构造一个 ObjectOutputStream(对象流) 对象,接着,使用ObjectOutputStream 对象的 writeObject(Objectobj) 方法就可以将参数为obj的对象写出(即:保存其状态),要恢复的话则使用输入流。
摘要:本文比较全面的介绍了Java序列化技术方方面面的知识,从序列化技术的基础谈起,介绍了Java序列化技术的机制和序列化技术的原理。并在随后的部分详细探讨了序列货摊的高级主题--如何精确的控制序列化机制。通过阅读该文章,你可以了解如何使用Java序列化机制的方式和正确使用的方法,避免实际编程中对该技术的误用。并能掌握如何高效使用该技术来完成特殊的功能。
关键字:序列化、反序列化、类加载、指纹技术
key:Serialize、DeSerialize、ClassLoad、FingerPrint
1 Java 序列化技术概述
Java 序列化技术可以使你将一个对象的状态写入一个Byte 流里,并且可以从其它地方
把该Byte 流里的数据读出来。重新构造一个相同的对象。这种机制允许你将对象通过网络
进行传播,并可以随时把对象持久化到数据库、文件等系统里。Java的序列化机制是RMI、
EJB、JNNI等技术的技术基础。
1.1 序列化技术基础
并非所有的Java 类都可以序列化,为了使你指定的类可以实现序列化,你必须使该类
实现如下接口:
java.io.Serializable
需要注意的是,该接口什么方法也没有。实现该类只是简单的标记你的类准备支持序列
化功能。我们来看如下的代码:
/**
* 抽象基本类,完成一些基本的定义
*/
public abstract class Humanoid
{
protected int noOfHeads;
private static int totalHeads;
public Humanoid()
{
this(1);
}
public Humanoid(int noOfHeads)
{
如何正确的使用Java序列化技术 技术研究系列
if (noOfHeads > 10)
throw new Error("Be serious. More than 10heads?!");
this.noOfHeads = noOfHeads;
synchronized (Humanoid.class)
{
totalHeads += noOfHeads;
}
}
public int getHeadCount()
{
return totalHeads;
}
}
该类的一个子类如下:
/**
* Humanoid的实现类,实现了序列化接口
*/
import java.io.*;
public class Person extends Humanoid
implements java.io.Serializable
{
private String lastName;
private String firstName;
private transient Thread workerThread;
private static int population;
public Person(String lastName, String firstName)
{
this.lastName = lastName;
this.firstName = firstName;
synchronized (Person.class)
{
population++;
}
}
public String toString()
{
return "Person " + firstName + " "+ lastName;
}
static synchronized public int getPopulation()
{
return population;
}
}
1.2 对象的序列化及反序列化
上面的类Person 类实现了Serializable 接口,因此是可以序列化的。我们如果要把一个
可以序列化的对象序列化到文件里或者数据库里,需要下面的类的支持:
java.io.ObjectOutputStream
如何正确的使用Java序列化技术 技术研究系列
下面的代码负责完成Person类的序列化操作:
/**
* Person的序列化类,通过该类把Person写入文件系统里。
*/
import java.io.*;
public class WriteInstance
{
public static void main(String [] args) throwsException
{
if (args.length != 1)
{
System.out.println("usage: java WriteInstancefile");
System.exit(-1);
}
FileOutputStream fos = new FileOutputStream(args[0]);
ObjectOutputStream oos = new ObjectOutputStream(fos);
Person p = new Person("gaoyanbing","haiger");
oos.writeObject(p);
}
}
如果我们要序列化的类其实是不能序列化的,则对其进行序列化时会抛出下面的异常:
java.io.NotSerializableException
当我们把Person 序列化到一个文件里以后,如果需要从文件中恢复Person 这个对象,
我们需要借助如下的类:
java.io.ObjectInputStream
从文件里把Person类反序列化的代码实现如下:
/**
* Person的反序列化类,通过该类从文件系统中读出序列化的数据,并构造一个
* Person对象。
*/
import java.io.*;
public class ReadInstance
{
public static void main(String [] args) throwsException
{
if (args.length != 1)
{
System.out.println("usage: java ReadInstancefilename");
System.exit(-1);
}
FileInputStream fis = new FileInputStream(args[0]);
ObjectInputStream ois = new ObjectInputStream(fis);
Object o = ois.readObject();
如何正确的使用Java序列化技术 技术研究系列
System.out.println("read object " + o);
}
}
1.3 序列化对类的处理原则
并不是一个实现了序列化接口的类的所有字段及属性都是可以序列化的。我们分为以下
几个部分来说明:
u 如果该类有父类,则分两种情况来考虑,如果该父类已经实现了可序列化接口。则
其父类的相应字段及属性的处理和该类相同;如果该类的父类没有实现可序列化接
口,则该类的父类所有的字段属性将不会序列化。
u 如果该类的某个属性标识为static类型的,则该属性不能序列化;
u 如果该类的某个属性采用transient关键字标识,则该属性不能序列化;
需要注意的是,在我们标注一个类可以序列化的时候,其以下属性应该设置为transient
来避免序列化:
u 线程相关的属性;
u 需要访问IO、本地资源、网络资源等的属性;
u 没有实现可序列化接口的属性;(注:如果一个属性没有实现可序列化,而我们又
没有将其用transient 标识, 则在对象序列化的时候,会抛出
java.io.NotSerializableException 异常)。
1.4 构造函数和序列化
对于父类的处理,如果父类没有实现序列化接口,则其必须有默认的构造函数(即没有
参数的构造函数)。为什么要这样规定呢?我们来看实际的例子。仍然采用上面的Humanoid
和Person 类。我们在其构造函数里分别加上输出语句:
/**
* 抽象基本类,完成一些基本的定义
*/
public abstract class Humanoid
{
protected int noOfHeads;
private static int totalHeads;
public Humanoid()
{
this(1);
System.out.println("Human's default constructoris invoked");
}
public Humanoid(int noOfHeads)
{
if (noOfHeads > 10)
throw new Error("Be serious. More than 10heads?!");
如何正确的使用Java序列化技术 技术研究系列
this.noOfHeads = noOfHeads;
synchronized (Humanoid.class)
{
totalHeads += noOfHeads;
}
}
public int getHeadCount()
{
return totalHeads;
}
}
/**
* Humanoid的实现类,实现了序列化接口
*/
import java.io.*;
public class Person extends Humanoid
implements java.io.Serializable
{
private String lastName;
private String firstName;
private transient Thread workerThread;
private static int population;
public Person(String lastName, String firstName)
{
this.lastName = lastName;
this.firstName = firstName;
synchronized (Person.class)
{
population++;
}
System.out.println("Person's constructor isinvoked");
}
public String toString()
{
return "Person " + firstName + " "+ lastName;
}
static synchronized public int getPopulation()
{
return population;
}
}
在命令行运行其序列化程序和反序列化程序的结果为:
如何正确的使用Java序列化技术 技术研究系列
可以看到,在从流中读出数据构造Person对象的时候,Person 的父类Humanoid的默认
构造函数被调用了。当然,这点完全不用担心,如果你没有给父类一个默认构造函数,则编
译的时候就会报错。
这里,我们把父类Humanoid做如下的修改:
/**
* 抽象基本类,完成一些基本的定义
*/
public class Humanoid implements java.io.Serializable
{
protected int noOfHeads;
private static int totalHeads;
public Humanoid()
{
this(1);
System.out.println("Human's default constructoris invoked");
}
public Humanoid(int noOfHeads)
{
if (noOfHeads > 10)
throw new Error("Be serious. More than 10heads?!");
this.noOfHeads = noOfHeads;
synchronized (Humanoid.class)
{
totalHeads += noOfHeads;
}
}
public int getHeadCount()
{
return totalHeads;
}
}
我们把父类标记为可以序列化, 再来看运行的结果:
如何正确的使用Java序列化技术 技术研究系列
可以看到,在反序列化的时候,如果父类也是可序列化的话,则其默认构造函数也不会
调用。这是为什么呢?
这是因为Java 对序列化的对象进行反序列化的时候,直接从流里获取其对象数据来生
成一个对象实例,而不是通过其构造函数来完成,毕竟我们的可序列化的类可能有多个构造
函数,如果我们的可序列化的类没有默认的构造函数,反序列化机制并不知道要调用哪个构
造函数才是正确的。
1.5 序列化带来的问题
我们可以看到上面的例子,在Person 类里,其字段population 很明显是想跟踪在一个
JVM里Person类有多少实例,这个字段在其构造函数里完成赋值,当我们在同一个JVM 里
序列化Person 并反序列化时,因为反序列化的时候Person 的构造函数并没有被调用,所以
这种机制并不能保证正确获取Person在一个JVM的实例个数,在后面的部分我们将要详细
探讨这个问题及给出比较好的解决方案。
2 控制序列化技术
2.1 使用readObject 和writeObject方法
由于我们对于对象的序列化是采用如下的类来实现具体的序列化过程:
java.io.ObjectOutputStream
而该类主要是通过其writeObject 方法来实现对象的序列化过程,改类同时也提供了一
种机制来实现用户自定义writeObject 的功能。方法就是在我们的需要序列化的类里实现一
如何正确的使用Java序列化技术 技术研究系列
个writeObject方法,这个方法在ObjectOutputStream序列化该对象的时候就会自动的回调它。
从而完成我们自定义的序列化功能。
同样的,反序列化的类也实现了同样的回调机制,我们通过扩展其readObject来实现自
定义的反序列化机制。
通过这种灵活的回调机制就解决了上面提出的序列化带来的问题,针对上面的Person
的问题,我们编写如下的readObject方法就可以彻底避免population计数不准确的问题:
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException
{
ois.defaultReadObject();
synchronized (Person.class)
{
population++;
}
System.out.println("Adjusting population inreadObject");
}
2.2 序列化过程的类版本控制
本节讨论以下问题:
u 在对象反序列化过程中如何寻找对象的类;
u 如果序列化和反序列化两边的类不是同一个版本,如何控制;
2.2.1 序列化类的寻找机制
在对象的反序列化过程中,是一定需要被反序列化的类能被ClassLoader找到的,否则
在反序列化过程中就会抛出java.lang.ClassNotFoundException异常。关于ClassLoader 如何
寻找类,这里就不多说了,可以参考我的另一篇讨论ClassLoader 的文章《在非管理环境下
如何实现热部署》。我们这里只是关心该序列化对象对应的类是被哪个ClassLoader给Load
的。为此,我们修改上面的
/**
* 修改后的反序列化类
*/
import java.io.*;
public class ReadInstance
{
public void readPerson(String filename)
{
如何正确的使用Java序列化技术 技术研究系列
try{
FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis);
Object o = ois.readObject();
System.out.println("read object " + o);
System.out.println(this.getClass().getClassLoader());
Person person = (Person)o;
System.out.println(person.getClass().getClassLoader());
}catch(java.io.IOException ie)
{
ie.printStackTrace();
}catch(ClassNotFoundException ce)
{
ce.printStackTrace();
}
}
public static void main(String [] args) throwsException
{
if (args.length != 1)
{
System.out.println("usage: java ReadInstancefilename");
System.exit(-1);
}
ReadInstance readInstance = new ReadInstance();
readInstance.readPerson(args[0]);
}
我们主要通过背景为黄色的两行代码查看其类加载器,运行结果如下:
由此可以看出,序列化类的类加载器正式其反序列化实现类的类加载器。这样的话我们
就可以通过使最新的Person 类的版本发布为只有该反序列化器的ClassLoader可见。而较旧
的版本则不为该ClassLoader 可见的方法来避免在反序列化过程中类的多重版本的问题。当
然,下面就类的版本问题我们还要做专门的探讨。
如何正确的使用Java序列化技术 技术研究系列
2.2.2 序列化类多重版本的控制
如果在反序列化的JVM 里出现了该类的不同时期的版本,那么反序列化机制是如何处
理的呢?
为了避免这种问题,Java的序列化机制提供了一种指纹技术,不同的类带有不同版本的
指纹信息,通过其指纹就可以辨别出当前JVM 里的类是不是和将要反序列化后的对象对应
的类是相同的版本。该指纹实现为一个64bit的long 类型。通过安全的Hash算法(SHA-1)
来将序列化的类的基本信息(包括类名称、类的编辑者、类的父接口及各种属性等信息)处
理为该64bit的指纹。我们可以通过JDK自带的命令serialver来打印某个可序列化类的指纹
信息。如下:
当我们的两边的类版本不一致的时候,反序列化就会报错:
如何正确的使用Java序列化技术 技术研究系列
解决之道:从上面的输出可以看出,该指纹是通过如下的内部变量来提供的:
private static final long serialVersionUID;
如果我们在类里提供对该属性的控制,就可以实现对类的序列化指纹的自定义控制。为
此,我们在Person 类里定义该变量:
private static final long serialVersionUID=6921661392987334380L;
则当我们修改了Person 类,发布不同的版本到反序列化端的JVM,也不会有版本冲突
的问题了。需要注意的是,serialVersionUID 的值是需要通过serialver 命令来取得。而不能
自己随便设置,否则可能有重合的。
需要注意的是,手动设置serialVersionUID 有时候会带来一些问题,比如我们可能对类
做了关键性的更改。引起两边类的版本产生实质性的不兼容。为了避免这种失败,我们需要
知道什么样的更改会引起实质性的不兼容,下面的表格列出了会引起实质性不兼容和可以忽
略(兼容)的更改:
更改类型 例子
兼容的更改
u 添加属性(Adding fields)
u 添加/删除类(adding/removingclasses)
u 添加/删除writeObject/readObject方法(adding/removing
writeObject/readObject)
u 添加序列化标志(adding Serializable)
u 改变访问修改者(changing access modifier)
u 删除静态/不可序列化属性(removingstatic/transient from
a field)
不兼容的更改
u 删除属性(Deleting fields)
u 在一个继承或者实现层次里删除类(removing classes in a
hierarchy)
u 添加静态/不可序列化字段(addingstatic/transient to a
field)
u 修改简单变量类型(changing type of a primitive)
u switching between Serializable or Externalizable
u 删除序列化标志(removingSerializable/Externalizable)
u 改变readObject/writeObject对默认属性值的控制(changing
whether readObject/writeObject handles default field
data)
u adding writeReplace or readResolvethat produces
objects incompatible with older versions
另外,从Java 的序列化规范里并没有指出当我们对类做了实质性的不兼容修改后反序
列化会有什么后果。并不是所有的不兼容修改都会引起反序列化的失败。比如,如果我们删
除了一个属性,则在反序列化的时候,反序列化机制只是简单的将该属性的数据丢弃。从
JDK 的参考里,我们可以得到一些不兼容的修改引起的后果如下表:
如何正确的使用Java序列化技术 技术研究系列
不兼容的修改 引起的反序列化结果
删除属性
(Deleting a field) Silently ignored
在一个继承或者实现层次里删除类
(Moving classes in inheritance
hierarchy)
Exception
添加静态/不可序列化属性
(Adding static/transient)
Silently ignored
修改基本属性类型
(Changing primitive type)
Exception
改变对默认属性值的使用
(Changing use of default field data)
Exception
在序列化和非序列化及内外部类之间切换
(Switching Serializable and
Externalizable)
Exception
删除Serializable或者Externalizable标志
(Removing Serializable or
Externalizable)
Exception
返回不兼容的类
(Returning incompatible class)
Depends on incompatibility
2.3 显示的控制对属性的序列化过程
在默认的Java 序列化机制里,有关对象属性到byte 流里的属性的对应映射关系都是自
动而透明的完成的。在序列化的时候,对象的属性的名称默认作为byte 流里的名称。当该
对象反序列化的时候,就是根据byte 流里的名称来对应映射到新生成的对象的属性里去的。
举个例子来说。在我们的一个Person对象序列化的时候,Person的一个属性firstName就作
为byte 流里该属性默认的名称。当该Person 对象反序列化的时候,序列化机制就把从byte
流里得到的firstName 的值赋值给新的Person 实例里的名叫firstName的属性。
Java的序列化机制提供了相关的钩子函数给我们使用,通过这些钩子函数我们可以精确
的控制上述的序列化及反序列化过程。ObjectInputStream的内部类GetField提供了对把属性
数据从流中取出来的控制,而ObjectOutputStream的内部类PutField则提供了把属性数据放
入流中的控制机制。就ObjectInputStream来讲,我们需要在readObject方法里来完成从流中
读取相应的属性数据。比如我们现在把Person 类的版本从下面的表一更新到表二:
/**
* 修改前的老版本Person类,为了简化,我们删除了所有无关的代码
*/
import java.io.*;
public class Person extends Humanoid
implements java.io.Serializable
{
private String lastName;
如何正确的使用Java序列化技术 技术研究系列
private String firstName;
private static final long serialVersionUID=6921661392987334380L;
private Person()
{
}
public Person(String lastName, String firstName)
{
this.lastName = lastName;
this.firstName = firstName;
}
public String toString()
{
return "Person " + firstName + " "+ lastName;
}
}
修改后的Person为:
/**
* 修改后的Person类,我们将firstName和lastName变成了fullName
*/
import java.io.*;
public class Person extends Humanoid
implements java.io.Serializable
{
private String fullName;
private static final long serialVersionUID =6921661392987334380L;
private Person()
{
}
public Person(String fullName)
{
this.lastName = fullName;
}
public String toString()
{
return "Person " + fullName;
}
}
为此,我们需要编写Person类的readObject方法如下:
private void readObject(ObjectInputStream ois)
throws IOException,ClassNotFoundException
{
ObjectInputStream.GetField gf = ois.readFields();
fullName = (String) gf.get("fullName",null);
if (fullName == null)
{
String lastName = (String)gf.get("lastName", null);
如何正确的使用Java序列化技术 技术研究系列
String firstName = (String)gf.get("firstName", null);
if ( (lastName == null) || (firstName == null))
{
throw new InvalidClassException("invalidPerson");
}
fullName = firstName + " " + lastName;
}
}
我们的执行顺序是:
1) 编译老的Person及所有类;
2) 将老的Person序列化到文件里;
3) 修改为新版本的Person类;
4) 编译新的Person类;
5) 反序列化Person;
执行结果非常顺利,修改后的反序列化机制仍然正确的从流中获取了旧版本Person的
属性信息并完成对新版本的Person的属性赋值。
使用ObjectInputStream的readObject 来处理反序列化的属性时,有两点需要注意:
u 一旦采用自己控制属性的反序列化,则必须完成所有属性的反序列化(即要给所有
属性赋值);
u 在使用内部类GetField 的get 方法的时候需要注意,如果get 的是一个既不在老版
本出现的属性,有没有在新版本出现的属性,则该方法会抛出异常:
IllegalArgumentException: no such field,所以我们应该在一个try块里
来使用该方法。
同理,我们可以通过writeObject 方法来控制对象属性的序列化过程。这里就不再一一
举例了,如果你有兴趣的话,可以自己实现Person 类的writeObject 方法,并且使用
ObjectOutputStream的内部类PutField来完成属性的手动序列化操作。
3 总结
Java 序列化机制提供了强大的处理能力。一般来讲,为了尽量利用Java 提供的自动化
机制,我们不需要对序列化的过程做任何的干扰。但是在某些时候我们需要实现一些特殊的
功能,比如类的多版本的控制,特殊字段的序列化控制等。我们可以通过多种方式来实现这
些功能:
u 利用序列化机制提供的钩子函数readObject和writeObject;
u 覆盖序列化类的metaData 信息;
如何正确的使用Java序列化技术 技术研究系列
u 使类实现Externalizable 接口而不是实现Serializable接口。
关于Externalizable 接口更多的介绍,可以参考JDK 的帮助提供的详细文档,同时也可
以快速参考《Thinking in Java》这本书第十章-Java IO系统的介绍。
参考资料:
1、 SUN关于Java 的虚拟机规范《The Java Virtual Machine Specification》;
2、 《Thinking in Java》;
3、 《基于Java平台的组件化开发技术》。