Java编程思想,Java学习必读经典,无论是初学者仍是大牛都值得一读,这里总结书中的重点知识,这些知识不只常常出如今各大知名公司的笔试面试过程当中,并且在大型项目开发中也是经常使用的知识,既有简单的概念理解题(好比is-a关系和has-a关系的区别),也有深刻的涉及RTTI和JVM底层反编译知识。java
Java中除了static方法和final方法(private方法本质上属于final方法,由于不能被子类访问)以外,其它全部的方法都是动态绑定,这意味着一般状况下,咱们没必要断定是否应该进行动态绑定—它会自动发生。程序员
class StaticSuper { public static String staticGet() { return "Base staticGet()"; } public String dynamicGet() { return "Base dynamicGet()"; } } class StaticSub extends StaticSuper { public static String staticGet() { return "Derived staticGet()"; } public String dynamicGet() { return "Derived dynamicGet()"; } } public class StaticPolymorphism { public static void main(String[] args) { StaticSuper sup = new StaticSub(); System.out.println(sup.staticGet()); System.out.println(sup.dynamicGet()); } }
Base staticGet()
Derived dynamicGet()面试
构造函数并不具备多态性,它们其实是static方法,只不过该static声明是隐式的。所以,构造函数不可以被override。算法
在父类构造函数内部调用具备多态行为的函数将致使没法预测的结果,由于此时子类对象还没初始化,此时调用子类方法不会获得咱们想要的结果。编程
class Glyph { void draw() { System.out.println("Glyph.draw()"); } Glyph() { System.out.println("Glyph() before draw()"); draw(); System.out.println("Glyph() after draw()"); } } class RoundGlyph extends Glyph { private int radius = 1; RoundGlyph(int r) { radius = r; System.out.println("RoundGlyph.RoundGlyph(). radius = " + radius); } void draw() { System.out.println("RoundGlyph.draw(). radius = " + radius); } } public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } }
输出:设计模式
Glyph() before draw()
RoundGlyph.draw(). radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(). radius = 5数组
为何会这样输出?这就要明确掌握Java中构造函数的调用顺序:安全
(1)在其余任何事物发生以前,将分配给对象的存储空间初始化成二进制0;
(2)调用基类构造函数。从根开始递归下去,由于多态性此时调用子类覆盖后的draw()方法(要在调用RoundGlyph构造函数以前调用),因为步骤1的缘故,咱们此时会发现radius的值为0;
(3)按声明顺序调用成员的初始化方法;
(4)最后调用子类的构造函数。cookie
只有非private方法才能够被覆盖,可是还须要密切注意覆盖private方法的现象,这时虽然编译器不会报错,可是也不会按照咱们所指望的来执行,即覆盖private方法对子类来讲是一个新的方法而非重载方法。所以,在子类中,新方法名最好不要与基类的private方法采起同一名字(虽然不要紧,但容易误解,觉得可以覆盖基类的private方法)。网络
Java类中属性域的访问操做都由编译器解析,所以不是多态的。父类和子类的同名属性都会分配不一样的存储空间,以下:
// Direct field access is determined at compile time. class Super { public int field = 0; public int getField() { return field; } } class Sub extends Super { public int field = 1; public int getField() { return field; } public int getSuperField() { return super.field; } } public class FieldAccess { public static void main(String[] args) { Super sup = new Sub(); System.out.println("sup.filed = " + sup.field + ", sup.getField() = " + sup.getField()); Sub sub = new Sub(); System.out.println("sub.filed = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSuperField() = " + sub.getSuperField()); } }
输出:
sup.filed = 0, sup.getField() = 1
sub.filed = 1, sub.getField() = 1, sub.getSuperField() = 0
Sub子类实际上包含了两个称为field的域,然而在引用Sub中的field时所产生的默认域并不是Super版本的field域,所以为了获得Super.field,必须显式地指明super.field。
is-a关系属于纯继承,即只有在基类中已经创建的方法才能够在子类中被覆盖,以下图所示:
基类和子类有着彻底相同的接口,这样向上转型时永远不须要知道正在处理的对象的确切类型,这经过多态来实现。
is-like-a关系:子类扩展了基类接口。它有着相同的基本接口,可是他还具备由额外方法实现的其余特性。
缺点就是子类中接口的扩展部分不能被基类访问,所以一旦向上转型,就不能调用那些新方法。
使用方式
Java是如何让咱们在运行时识别对象和类的信息的,主要有两种方式(还有辅助的第三种方式,见下描述):
Shape s = (Shape)s1;
Class.forName()
。instanceof
,它返回一个bool值,它保持了类型的概念,它指的是“你是这个类吗?或者你是这个类的派生类吗?”。而若是用==或equals比较实际的Class对象,就没有考虑继承—它或者是这个确切的类型,或者不是。工做原理
要理解RTTI在Java中的工做原理,首先必须知道类型信息在运行时是如何表示的,这项工做是由称为Class对象
的特殊对象完成的,它包含了与类有关的信息。Java送Class对象来执行其RTTI,使用类加载器的子系统实现。
不管什么时候,只要你想在运行时使用类型信息,就必须首先得到对恰当的Class对象的引用,获取方式有三种:
(1)若是你没有持有该类型的对象,则Class.forName()
就是实现此功能的便捷途,由于它不须要对象信息;
(2)若是你已经拥有了一个感兴趣的类型的对象,那就能够经过调用getClass()
方法来获取Class引用了,它将返回表示该对象的实际类型的Class引用。Class包含颇有有用的方法,好比:
package rtti; interface HasBatteries{} interface WaterProof{} interface Shoots{} class Toy { Toy() {} Toy(int i) {} } class FancyToy extends Toy implements HasBatteries, WaterProof, Shoots { FancyToy() { super(1); } } public class RTTITest { static void printInfo(Class cc) { System.out.println("Class name: " + cc.getName() + ", is interface? [" + cc.isInterface() + "]"); System.out.println("Simple name: " + cc.getSimpleName()); System.out.println("Canonical name: " + cc.getCanonicalName()); } public static void main(String[] args) { Class c = null; try { c = Class.forName("rtti.FancyToy"); // 必须是全限定名(包名+类名) } catch(ClassNotFoundException e) { System.out.println("Can't find FancyToy"); System.exit(1); } printInfo(c); for(Class face : c.getInterfaces()) { printInfo(face); } Class up = c.getSuperclass(); Object obj = null; try { // Requires default constructor. obj = up.newInstance(); } catch (InstantiationException e) { System.out.println("Can't Instantiate"); System.exit(1); } catch (IllegalAccessException e) { System.out.println("Can't access"); System.exit(1); } printInfo(obj.getClass()); } }
输出:
Class name: rtti.FancyToy, is interface? [false]
Simple name: FancyToy
Canonical name: rtti.FancyToy
Class name: rtti.HasBatteries, is interface? [true]
Simple name: HasBatteries
Canonical name: rtti.HasBatteries
Class name: rtti.WaterProof, is interface? [true]
Simple name: WaterProof
Canonical name: rtti.WaterProof
Class name: rtti.Shoots, is interface? [true]
Simple name: Shoots
Canonical name: rtti.Shoots
Class name: rtti.Toy, is interface? [false]
Simple name: Toy
Canonical name: rtti.Toy
(3)Java还提供了另外一种方法来生成对Class对象的引用,即便用类字面常量。好比上面的就像这样:FancyToy.class;
来引用。
这样作不只更简单,并且更安全,由于它在编译时就会受到检查(所以不须要置于try语句块中),而且它根除了对forName方法的引用,因此也更高效。类字面常量不只能够应用于普通的类,也能够应用于接口、数组以及基本数据类型。
注意:当使用“.class”来建立对Class对象的引用时,不会自动地初始化该Class对象,初始化被延迟到了对静态方法(构造器隐式的是静态的)或者非final静态域(注意final静态域不会触发初始化操做)进行首次引用时才执行:。而使用Class.forName时会自动的初始化。
为了使用类而作的准备工做实际包含三个步骤:
- 加载:由类加载器执行。查找字节码,并从这些字节码中建立一个Class对象
- 连接:验证类中的字节码,为静态域分配存储空间,而且若是必需的话,将解析这个类建立的对其余类的全部引用。
- 初始化:若是该类具备超类,则对其初始化,执行静态初始化器和静态初始化块。
这一点很是重要,下面经过一个实例来讲明这二者的区别:
package rtti; import java.util.Random; class Initable { static final int staticFinal = 47; static final int staticFinal2 = ClassInitialization.rand.nextInt(1000); static { System.out.println("Initializing Initable"); } } class Initable2 { static int staticNonFinal = 147; static { System.out.println("Initializing Initable2"); } } class Initable3 { static int staticNonFinal = 74; static { System.out.println("Initializing Initable3"); } } public class ClassInitialization { public static Random rand = new Random(47); public static void main(String[] args) { // Does not trigger initialization Class initable = Initable.class; System.out.println("After creating Initable ref"); // Does not trigger initialization System.out.println(Initable.staticFinal); // Does trigger initialization(rand() is static method) System.out.println(Initable.staticFinal2); // Does trigger initialization(not final) System.out.println(Initable2.staticNonFinal); try { Class initable3 = Class.forName("rtti.Initable3"); } catch (ClassNotFoundException e) { System.out.println("Can't find Initable3"); System.exit(1); } System.out.println("After creating Initable3 ref"); System.out.println(Initable3.staticNonFinal); } }
输出:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
能够突破这个限制吗?是的,突破它的就是反射机制。
Class
类与java.lang.reflect
类库一块儿对反射的概念进行了支持,该类库包含了Field
、Method
以及Constructor
类(每一个类都实现了Member
接口)。这些类型的对象是由JVM在运行时建立的,用以表示未知类里对应的成员。这样你就可使用Constructor
建立新的对象,用get()/set()
方法读取和修改与Field
对象关联的字段,用invoke()
方法调用与Method
对象关联的方法。另外,还能够调用getFields()、getMethods()和getConstructors()
等很便利的方法,以返回表示字段、方法以及构造器的对象的数组。这样,匿名对象的类信息就能在运行时被彻底肯定下来,而在编译时不须要知道任何事情。
####反射与RTTI的区别
当经过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪一个特定的类(就像RTTI那样),在用它作其余事情以前必须先加载那个类的Class
对象,所以,那个类的.class
文件对于JVM来讲必须是可获取的:要么在本地机器上,要么能够经过网络取得。因此RTTI与反射之间真正的区别只在于:对RTTI来讲,编译器在编译时打开和检查.class文件(也就是能够用普通方法调用对象的全部方法);而对于反射机制来讲,.class文件在编译时是不可获取的,因此是在运行时打开和检查.class文件。
下面的例子是用反射机制打印出一个类的全部方法(包括在基类中定义的方法):
package typeinfo; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.regex.Pattern; // Using reflection to show all the methods of a class. // even if the methods are defined in the base class. public class ShowMethods { private static String usage = "usage: \n" + "ShowMethods qualified.class.name\n" + "To show all methods in class or: \n" + "ShowMethods qualified.class.name word\n" + "To search for methods involving 'word'"; private static Pattern p = Pattern.compile("\\w+\\."); public static void main(String[] args) { if(args.length < 1) { System.out.println(usage); System.exit(0); } int lines = 0; try { Class<?> c = Class.forName(args[0]); Method[] methods = c.getMethods(); Constructor[] ctors = c.getConstructors(); if(args.length == 1) { for(Method method : methods) { System.out.println(p.matcher(method.toString()).replaceAll("")); } for(Constructor ctor : ctors) { System.out.println(p.matcher(ctor.toString()).replaceAll("")); } lines = methods.length + ctors.length; } else { for(Method method : methods) { if(method.toString().indexOf(args[1]) != -1) { System.out.println(p.matcher(method.toString()).replaceAll("")); lines++; } } for(Constructor ctor : ctors) { if(ctor.toString().indexOf(args[1]) != -1) { System.out.println(p.matcher(ctor.toString()).replaceAll("")); lines++; } } } } catch (ClassNotFoundException e) { System.out.println("No such Class: " + e); } } }
输出:
public static void main(String[])
public final native void wait(long) throws InterruptedException
public final void wait() throws InterruptedException
public final void wait(long,int) throws InterruptedException
public boolean equals(Object)
public String toString()
public native int hashCode()
public final native Class getClass()
public final native void notify()
public final native void notifyAll()
public ShowMethods()
代理模式
在任什么时候刻,只要你想要将额外的操做从“实际”对象中分离到不一样的地方,特别是当你但愿可以很容易地作出修改,从没有使用额外操做转为使用这些操做,或者反过来时,代理就显得颇有用(设计模式的关键是封装修改)。例如,若是你但愿跟踪对某个类中方法的调用,或者但愿度量这些调用的开销,那么你应该怎样作呢?这些代码确定是你不但愿将其合并到应用中的代码,所以代理使得你能够很容易地添加或移除它们。
interface Interface { void doSomething(); void somethingElse(String arg); } class RealObject implements Interface { @Override public void doSomething() { System.out.println("doSomething."); } @Override public void somethingElse(String arg) { System.out.println("somethingElse " + arg); } } class SimpleProxy implements Interface { private Interface proxy; public SimpleProxy(Interface proxy) { this.proxy = proxy; } @Override public void doSomething() { System.out.println("SimpleProxy doSomething."); proxy.doSomething(); } @Override public void somethingElse(String arg) { System.out.println("SimpleProxy somethingElse " + arg); proxy.somethingElse(arg); } } public class SimpleProxyDemo { public static void consumer(Interface iface) { iface.doSomething(); iface.somethingElse("bonobo"); } public static void main(String[] args) { consumer(new RealObject()); consumer(new SimpleProxy(new RealObject())); } }
输出:
doSomething.
somethingElse bonobo
SimpleProxy doSomething.
doSomething.
SimpleProxy somethingElse bonobo
somethingElse bonobo
动态代理
Java的动态代理比代理的思想更向前迈进了一步,由于它能够动态地建立代理并动态地处理对所代理方法的调用。
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; class DynamicProxyHandler implements InvocationHandler { private Object proxy; public DynamicProxyHandler(Object proxy) { this.proxy = proxy; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("*** proxy: " + proxy.getClass() + ". method: " + method + ". args: " + args); if(args != null) { for(Object arg : args) System.out.println(" " + arg); } return method.invoke(this.proxy, args); } } public class SimpleDynamicProxy { public static void consumer(Interface iface) { iface.doSomething(); iface.somethingElse("bonobo"); } public static void main(String[] args) { RealObject real = new RealObject(); consumer(real); // insert a proxy and call again: Interface proxy = (Interface)Proxy.newProxyInstance( Interface.class.getClassLoader(), new Class[]{ Interface.class }, new DynamicProxyHandler(real)); consumer(proxy); } }
输出:
doSomething.
somethingElse bonobo
*** proxy: class typeinfo.\$Proxy0. method: public abstract void typeinfo.Interface.doSomething(). args: null
doSomething.
*** proxy: class typeinfo.\$Proxy0. method: public abstract void typeinfo.Interface.somethingElse(java.lang.String). args: [Ljava.lang.Object;@6a8814e9
bonobo
somethingElse bonobo
Java虚拟机中有许多附加技术用以提高速度,尤为是与加载器操做相关的,被称为“即时”(Just-In-Time,JIT)编译器的技术。这种技术能够把程序所有或部分翻译成本地机器码(这原本是JVM的工做),程序运行速度所以得以提高。当须要装载某个类时,编译器会先找到其.class文件,而后将该类的字节码装入内存。此时,有两种方案可供选择:
(1)一种就是让即时编译器编译全部代码。但这种作法有两个缺陷:这种加载动做散落在整个程序生命周期内,累加起来要花更多时间;而且会增长可执行代码的长度(字节码要比即时编译器展开后的本地机器码小不少),这将致使页面调度,从而下降程序速度。
(2)另外一种作法称为惰性评估(lazy evaluation),意思是即时编译器只在必要的时候才编译代码,这样,从不会被执行的代码也许就压根不会被JIT所编译。新版JDK中的Java HotSpot技术就采用了相似方法,代码每次被执行的时候都会作一些优化,因此执行的次数越多,它的速度就越快。
package access.cookie; public class Cookie { public Cookie() { System.out.println("Cookie Constructor"); } void bite() { // 包访问权限,其它包即便是子类也不能访问它 System.out.println("bite"); } }
package access.dessert; import access.cookie.Cookie; public class ChocolateChip extends Cookie { public ChocolateChip() { System.out.println("ChocolateChip constructor"); } public void chomp() { bite(); // error, the method bite() from the type Cookie is not visible } }
在最近的Java版本中,虚拟机(特别是hotspot技术)能自动侦测这些状况,并颇为“明智”地决定是否嵌入一个final 方法。然而,最好仍是不要彻底相信编译器能正确地做出全部判断。一般,只有在方法的代码量很是少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。
类内全部private 方法都自动成为final。因为咱们不能访问一个private 方法,因此它绝对不会被其余方法覆盖(若强行这样作,编译器会给出错误提示)。可为一个private方法添加final指示符,但却不能为那个方法提供任何额外的含义。
为何须要内部类? — 主要是解决了多继承的问题,继承具体或抽象类
考虑这样一种情形:若是必须在一个类中以某种方式实现两个接口。因为接口的灵活性,你有两种选择:使用单一类或者使用内部类。但若是拥有的是抽象的类或具体的类,而不是接口,那就只能使用内部类才能实现多重继承。
使用内部类,还能够得到其余一些特性:
- 内部类能够有多个实例,每一个实例都有本身的状态信息,而且与其外围类对象的信息相互独立。
- 在单个外围类中,可让多个内部类以不一样的方式实现同一个接口或继承同一个类。
- 建立内部类对象的时刻并不依赖于外围类对象的建立。
- 内部类并无使人迷惑的is-a关系,它就是一个独立的实体。
StringBuilder
操做(javap -c class字节码文件名 命令
查看具体优化过程)。这让你以为能够随意使用String对象,反正编译器会为你自动地优化性能。但编译器能优化到什么程度还很差说,不必定能优化到使用StringBuilder代替String相同的效果。好比:public class WitherStringBuilder { public String implicit(String[] fields) { String result = ""; for(int i = 0; i < fields.length; i++) result += fields[i]; return result; } public String explicit(String[] fields) { StringBuilder result = new StringBuilder(); for(int i = 0; i < fields.length; i++) result.append(fields[i]); return result.toString(); } }
javap -c WitherStringBuilder
,能够看到两个方法对应的字节码。public java.lang.String implicit(java.lang.String[]);
Code:
0: ldc #16 // String
2: astore_2
3: iconst_0
4: istore_3
5: goto 32
8: new #18 // class java/lang/StringBuilder
11: dup
12: aload_2
13: invokestatic #20 // Method java/lang/String.valueOf:(
Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #26 // Method java/lang/StringBuilder.”<
init>”:(Ljava/lang/String;)V
19: aload_1
20: iload_3
21: aaload
22: invokevirtual #29 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: invokevirtual #33 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
28: astore_2
29: iinc 3, 1
32: iload_3
33: aload_1
34: arraylength
35: if_icmplt 8
38: aload_2
39: areturn
public java.lang.String implicit(java.lang.String[]);
Code:
0: ldc #16 // String
2: astore_2
3: iconst_0
4: istore_3
5: goto 32
8: new #18 // class java/lang/StringBuilder
11: dup
12: aload_2
13: invokestatic #20 // Method java/lang/String.valueOf:(
Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #26 // Method java/lang/StringBuilder.”<
init>”:(Ljava/lang/String;)V
19: aload_1
20: iload_3
21: aaload
22: invokevirtual #29 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: invokevirtual #33 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
28: astore_2
29: iinc 3, 1
32: iload_3
33: aload_1
34: arraylength
35: if_icmplt 8
38: aload_2
39: areturn
能够发现,StringBuilder是在循环以内构造的,这意味着每通过循环一次,就会建立一个新的StringBuilder对象。
explicit方法:
public java.lang.String explicit(java.lang.String[]);
Code:
0: new #18 // class java/lang/StringBuilder
3: dup
4: invokespecial #45 // Method java/lang/StringBuilder.”<
init>”:()V
7: astore_2
8: iconst_0
9: istore_3
10: goto 24
13: aload_2
14: aload_1
15: iload_3
16: aaload
17: invokevirtual #29 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: pop
21: iinc 3, 1
24: iload_3
25: aload_1
26: arraylength
27: if_icmplt 13
30: aload_2
31: invokevirtual #33 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
34: areturn
}
能够看到,不只循环部分的代码更简短、更简单,并且它只生成了一个StringBuilder对象。显式的建立StringBuilder还容许你预先为其指定大小。若是你已经知道最终的字符串大概有多长,那预先指定StringBuilder的大小能够避免屡次从新分配缓冲。
####总结
所以,当你为一个类重写toString()方法时,若是字符串操做比较简单,那就能够信赖编译器,它会为你合理地构造最终的字符串结果。可是,若是你要在toString()方法中使用循环,那么最好本身建立一个StringBuilder对象,用它来构造最终的结果。
System.out.printf()
和System.out.format()
方法模仿自C的printf
,能够格式化字符串,二者是彻底等价的。Java中,全部新的格式化功能都由java.util.Formatter
类处理。
String.format()
方法参考了C中的sprintf()
方法,以生成格式化的String对象,是一个static方法,它接受与Formatter.format()
方法同样的参数,但返回一个String对象。当你只需使用format()
方法一次的时候,该方法很方便。
import java.util.Arrays; import java.util.Formatter; public class SimpleFormat { public static void main(String[] args) { int x = 5; double y = 5.324667; System.out.printf("Row 1: [%d %f]\n", x, y); System.out.format("Row 1: [%d %f]\n", x, y); Formatter f = new Formatter(System.out); f.format("Row 1: [%d %f]\n", x, y); String str = String.format("Row 1: [%d %f]\n", x, y); System.out.println(str); Integer[][] a = { {1, 2, 3}, {4, 5, 6}, {7, 8, 3}, {9, 10, 6} }; System.out.println(Arrays.deepToString(a)); } }
当咱们对序列化进行控制时,可能某个特定子对象不想让Java序列化机制自动保存与恢复。若是子对象表示的是咱们不但愿将其序列化的敏感信息(如密码),一般会面临这种状况。即便对象中的这些信息是private属性,一经序列化处理,人们就能够经过读取文件或者拦截网络传输的方式来访问到它。有两种办法能够防止对象的敏感部分被序列化:
Externalizable
代替实现Serializable
接口来对序列化过程进行控制,Externalizable
继承了Serializable
接口,同时增添了两个方法:writeExternal()
和readExternal()
。二者在反序列化时的区别:
- 对Serializable对象反序列化时,因为Serializable对象彻底以它存储的二进制位为基础来构造,所以并不会调用任何构造函数,所以Serializable类无需默认构造函数,可是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,所以该父类必需有默认构造函数,不然会抛异常。
- 对Externalizable对象反序列化时,会先调用类的不带参数的构造方法,这是有别于默认反序列方式的。若是把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor
异常,所以Externalizable对象必须有默认构造函数,并且必需是public的。
- Externalizable
的替代方法:若是不是特别坚持实现Externalizable接口,那么还有另外一种方法。咱们能够实现Serializable
接口,并添加writeObject()
和readObject()
的方法。一旦对象被序列化或者从新装配,就会分别调用那两个方法。也就是说,只要提供了这两个方法,就会优先使用它们,而不考虑默认的序列化机制。
这些方法必须含有下列准确的签名:
private void writeObject(ObjectOutputStream stream) throws IOException; private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
- 能够用transient
关键字逐个字段地关闭序列化,它的意思是“不用麻烦你保存或恢复数据—我本身会处理的”。因为Externalizable对象在默认状况下不保存它们的任何字段,因此transient关键字只能和Serializable对象一块儿使用。