《Effective Java》读书笔记

建立和销毁对象

静态工厂模式

  • 构造器里未传参的成员不会被初始化。int类型是0,布尔类型是false,String类型是null,List<>也是null。

重叠构造器

  • 进阶1:javabean模式,使用set方法来初始化成员,缺点是构造过程当中javabean可能处于不一致状态(能够理解成该模式下成员的设置的分步进行的,可能某处使用到该类的某个成员时其还未被初始化),而且该模式阻止了把类变成不可能的可能,须要考虑线程安全。
  • 进阶2: Builder模式:类里定义一个静态类builder(其实就是javabean),对builder初始化完成后使用build()返回该类,Buidler模式的状态不一致是builder,而不是类自己,而且类自身的成员也可设置成final。

修饰符

  • 长度非零的数组老是可变的,即便是final类型:
public static final int[] VALUES={...} //错误
//正确1:增长一个公有的不可变列表
private static final int[] VALUES=...
public static final List< intergeR > VALUES=
{Collections.unmodifiableList(Arrays.adList(PRIVATE_VALUES));
//正确2:返回私有数组的拷贝
private static final int[] VALUES=...
public static final int[] values() {
    return VALUES.clone();
}
复制代码
  • 若是是公有类,直接暴露数据会有很大的隐患,由于当你未来想改变其内部表示法时已经不可能了,由于共有类的客户端代码已经遍及各处了。
public class Point{ //错误
    public int x;
    public int y;
}

public class Point{ //正确
    private int x;
    private int y;
    
    public int getX() { return x;}
}
复制代码

类和接口

使可变性最小化

  • 线程安全最容易的作法:只提供访问方法,不提供设值方法,对对象的加减乘除都从新返回一个新的对象。对象不会变化,也就不要求同步。
  • 能够把开销昂贵的计算结果缓存起来,例如String的hashcode方法,第一次计算后会将结果保存在成员hashCode里。

复合优先继承

  • 子类脆弱:例如一个类继承HashSet,若是子类里重写了addAll和add方法来计数,就会致使错误,由于HashSet的addAll是基于add方法实现的。不能保证父类不随着版本而变化,所以extends 子类继承父类是很是脆弱的。
  • 只有当子类真正是超类的子类型,即A和B,二者确实存在B is A的关系时,类B才应该扩展A,若是答案是否认的,一般状况下B应该包含A的一个私有实例,而且暴露一个较小的,简单的API:A本质上不是B的一部分,只是它的实现细节而已。

装饰者模式(Decorator模式)

结合上面说到的,HashSet是implement Set类的,在HashSet里重写了Set接口定义的add,addAll等方法。所以新的子类继承Hashset重写add、addAll就不可避免会将HashSet里的实现继承下来。html

使用装饰者模式:ForwardingSet implements Set,该类有成员private final Set s s,构造器里就是传入一个Set ,该类不具体实现Set的任何方法,例如:java

public boolean add(E e) {
	return s.add(e);
}
复制代码

InstrumentedSet extends ForwardingSet,构造器super父类便可,在这个类里添加一些功能,例如:android

@Override
public boolean add(E e){
    count++;
    return super.add(e);
}
复制代码

这种模式下,InstrumentedSet 只是一个包装类,只是对其成员Set进行修饰,为它增长计数特性。包装类并不实现具体功能,构造器里传入的就是实现具体功能的Set,能够是HaseSet或者本身实现的Set。程序员

另可参考阅读: Android源码学习之装饰模式应用编程

继承后构造方法的调用

  1. 若是子类没有定义构造方法,则调用父类的无参数的构造方法。
  2. 若是子类定义了构造方法,不管是无参数仍是带参数,在建立子类的对象的时候,首先执行父类无参数的构造方法,而后执行本身的构造方法。
  3. 若是子类调用父类带参数的构造方法,能够经过super(参数)调用所须要的父类的构造方法,切该语句作为子类构造方法中的第一条语句。
  4. 若是某个构造方法调用类中的其余的构造方法,则能够用this(参数),切该语句放在构造方法的第一条。 说白了:原则就是,先调用父亲的。(没有就默认调,有了就按有的调,反正只要有一个就能够了)
public class Son extends Father {
	public Son() {
	//        super(); //没加默认调用父类无参构造方法
	    super("from son");
	    Log.e("zyz", "son-constructor");
	}

    public Son(String str) {
//        super(); //没加默认调用父类无参构造方法
        Log.e("zyz", str + " son-constructor-with-params");
    }

    @Override
    public void print() {
        Log.e("zyz", "son-print");
    }
}

public class Son extends Father {
    public Son() {
//        super(); //没加默认调用父类无参构造方法
        super("from son");
        Log.e("zyz", "son-constructor");
    }

    public Son(String str) {
//        super(); //没加默认调用父类无参构造方法
        Log.e("zyz", str + " son-constructor-with-params");
    }

    @Override
    public void print() {
        Log.e("zyz", "son-print");
    }
}
复制代码

接口优于抽象类

抽象类能够写实例方法,经过派生继承,实现代码复用(子类可直接调用父类方法),但因为重用方法增长了耦合度,接口的方法必定须要重写,最大程度实现了解耦。数组

类层次优于标签类

标签类: 例如使用枚举或常量定义了圆和矩形,成员里有半径、长、宽。在公共方法 计算面积里,使用switch来判断是那种形状,再分别计算。相似的把多个实现乱七八糟地挤在单个类中,破坏可读性,又增长了内存占用,由于实例承担着属于其余类型的域。缓存

应该使用类层次来优化: 定义一个抽象类,包含抽象方法:将共有的方法(计算面积),若是有公有的成员还能够将其放在抽象类中。以后不一样的类圆和矩形继承公共抽象类,另外添加本身的参数,并重写本身的计算面积的方法。安全

优先考虑静态成员

若是成员类不要求访问外围实例,就要定义成静态内部类。非静态内部类始终要保持外围对象的引用,不只消耗内存,还将致使外围实例没法被垃圾回收。 例如Map实现的内部都有Entry对象,每一个Entry都与Map关联,可是entry的方法(getKey/getValue)等并不须要访问Map,所以私有的静态成员类是最佳的选择。bash

  • 若是一个嵌套类须要在单个方法以外可见,或者它太长了不适合放在方法内部,就使用成员类。
  • 若是成员类的每一个实例都须要一个指向外围实例的应用,就使用非静态成员类。不然就使用静态成员类。
  • 若是嵌套类属于一个方法的内部,且你只须要在一个地方建立实例,而且已经有了一个预置的类型能够说明这个类的特征,就使用匿名类。不然就使用局部类。

泛型

列表优先于数组

两者的不一样点:数据结构

数组是协变的(covariant)

若是B是A的子类,那么B[]就是A[]的子类型。

//编译时不报错,运行时报错ArrayStoreException
Object[] test = new Long[1];
test[0] = "test";
复制代码

而两个不一样的类型A、B,List既不是List的子类也不是超类。

List<Object> test2 = new ArrayList<Long>(); //编译时报错
test2.add("123");
复制代码

数组是具体化的(reified)

数组在运行时才知道并检查他们的元素类型约束。泛型则是经过擦除(erasure)来实现的。泛型只在编译时强化类型信息,在运行时擦除元素类型信息。擦除就是使泛型能够与没有使用泛型的代码随意互用。

利用有限制通配符提高API的灵活性

PECE producer-extends,consumer-siper 若是参数化类型表示生产者T,就使用<? extends T>,若是表示消费者T,就使用<? super T>

//src产生E实例供使用,是生产者
public void pushAll(Iterable<? extands E> src) {
    for (E e : src) push(e);
}
//dst消费E实例,是消费者
public void popAll(Collection<E> dst) {
    while(!isEmpty()) {
        dst.add(pop());
    }
}
复制代码

不要用通配符类型做为返回参数

枚举和注解

用enum代替int常量

(android不推荐使用enum)

  • 枚举本质上是int值
  • 枚举容许添加任意的方法和域
public enum Test {
    APPLE("test1", 2),
    pen("test2", 1);

    private final String name;

    private final int num;

    Test(String name, int num) {
        this.name = name;
        this.num = num;
    }

    public void print() {
        Log.e("zyz", APPLE.name + APPLE.num);
    }
}
//遍历枚举
Test[] values = Test.values();
复制代码

用实例域代替序数

  • 全部枚举都有一个ordinal方法,返回每一个枚举常量在类型中的数字位置。避免使用ordinal方法,除非是编写EnumSet和EnumMap这种基于枚举的通用数据结构。使用实例域(相似成员变量)来保存与枚举相关的值。

注解

  • 注解类型声明
@Retention(RetentionPolicy.RUNTIME) //运行时保留
@Target(ElementType.METHOD) //只在方法声明中才是合适的
public @interface MyTest {
    
}
复制代码

坚持使用Override注解

覆盖equals时的参数是Object类型的,不然则变成了重载。但若是使用@Override注解后写错了编译器就会报错。

用标记接口定义类型

  • 标记接口是没有包含方法声明的接口,只是指名了某个类实现了具备某种属性的接口(例如Serializable接口)
  • 标记接口赛过标记注解的两点:
    1. 接口定义的类型是由被标记类的实例实现的,注解则没有定义这样的类型。这个类型容许你在编译时捕捉到错误,而不像注解须要在运行时才能捕捉到
    2. 接口能够被更加精确地锁定。假设一个标记只适用于特殊接口的实现,若是定义成标记接口就能够用它将惟一的接口扩展成它适用的接口。
  • 注解赛过接口的两点:
    1. 注解能够不断演变。而接口一般不可能在实现后再给它添加方法。
    2. 注解是注解机制的一部分。注解能够做为支持注解做为编程元素之一的框架中具备一致性。
  • 接口和注解使用场景:
    1. 若是标记是应用到任何程序元素而不是类或接口,就必须使用注解,由于只有类和接口能够用来实现或扩展接口。
    2. 若是标记只给类和接口,若要编写多个只接受有这种标记的方法则优先使用接口,这样能够在编译时进行类型检查。
    3. 若是要永远限制这个标记只用于特殊接口的元素,最好将标记定义成该接口的一个子接口。
    4. 若是2,3都是否认的,则应该使用注解。

方法

检查参数的有效性

  • assert 对于有些参数,方法自己没有用到,却被保存起来供之后使用,可使用断言检验这类参数的有效性。若是断言失败,则会抛AssertionError。

必要时进行保护性拷贝

慎用重载

  • 类型仍是父类,虽然调用父类方法指向子类引用。

  • 安全而保守的策略是:永远不要导出两个具备相同参数数目的重载方法。若是方法使用可变参数,保守的策略是根本不要重载它。

慎用可变参数

  • 若是客户端调用这个方法时并无传递参数进去,它就会在运行时而不是编译时失败。

    //带两个参数,避免没有传参致使的问题
    static init min(int firstArg, int... remainingArgs) {
        int min = firstArg;
        for(int arg : remainingArgs) {
            ...
        }
    }
    复制代码
  • 在重视性能的状况下,使用可变参数要特别小型,可变参数方法的每次调用都会致使进行一次数组分配和初始化。可使用多个重载方法,每一个重载方法带有0至3个普通参数,当参数数目超过3个时,就使用可变参数方法。

返回零长度的数组或集合,而不是null

通用程序设计

for each循环优于传统的for循环

  • 若是你在编写的类型是一组元素,实现Iterable能够容许用户利用for-each循环遍历你的类型。
  • 三种常见的没法使用for-each的状况:
    1. 过滤——须要遍历集合并删除选定元素
    2. 转换——须要遍历集合并取代它的部分或所有元素值
    3. 平行迭代——须要并行地遍历多个集合,就须要显式地控制迭代器或者索引变量以便全部迭代器或索引变量均可以获得同步前移

了解和使用类库

  • 伪随机数生成器

    //错误
    Math.abs(new Random().nextInt());
    //正确
    Random.nextInt(int)
    复制代码
  • 了解和使用标准类库提供的便利工具,而不用浪费时间为那些与工做不太相关的问题提供特别的解决方案。标准类库太庞大了,以致于不可能去学习全部文档,可是每一个程序员都应该熟悉java.lang,java.util,某种程度上还有java.io种的内容。有两种工具值得特别一提。

    • Collections Framework 用来表示和操做集合
    • java.util.concurrentbao'zhong包中增长了一组并发使用工具

    总而言之,不要从新发明轮子,若是你要作的事情看起来是十分常见的,有可能类库中已经有某个类完成了这样的工做。

若是须要精确的答案,请避免使用float和double

float和double类型尤为不适合用于货币计算,由于要让一个float或double精确地表示0.1(或者10的ren'he'qi'ta任何其余负数次方值)是不可能的。

改进

使用BigDecimal代替double:

BigDecimal bigDecimal = new BigDecimal(0.1); 
复制代码

BigDecimal容许你彻底控制舍入,每当一个操做设计舍入的时候,它容许你从8种舍入模式中选择其一。可是缺点是与基本运算类型比,不只不方便,并且很慢。若是性能很是关键,而且又不介意本身记录是金子小数点,并且涉及的数值又不太大,就可使用int或long(例如0.1改变单位计做10)。若是数值范围没超过9位十进制数字,就可使用int。若是不超过18位数值,就可使用long。若是数值超过18位数字,就必须使用BigDecimal。

基本类型优于装箱基本类型

当程序装箱了基本类型值时,会致使高开销和没必要要的对象建立。

小心字符串链接的性能

链接操做符不适合运用在大规模的场景中,为链接n个字符串而重复地使用字符串链接操做符,须要n的平方级的时间。这是因为字符串不可变,当两个字符串被链接在一块儿时,它们的内容都要被拷贝。

使用StringBuilder:

StringBuilder test = new StringBuilder("test");
test.append("test2")
复制代码

经过接口引用对象

若是有合适的接口类型存在,那么对于参数、返回值、变量和域来讲,就都应该使用接口类型进行声明。只有当你利用构造器建立某个对象的时候,才真正须要引用这个对象的类。

List<String> list = new ArrayList<>();
复制代码

这样会使程序更灵活,当你决定更换实现时,只须要改变构造器中类的名称:

List<String> list = new Vector<>();
复制代码

全部的代码均可以继续工做,代码并不知道原来的实现类型,因此对于这种变化并不在乎。

接口优先于反射机制

反射机制容许一个类使用另外一个类,即便当前者被编译的时候后者还根本不存在,然而这种能力也是要付出代价的:

  • 丧失了编译时类型检查的好处(包括异常检查)
  • 执行反射访问所须要的代码很是笨拙和冗长
  • 性能损失

异常

只针对异常的状况才使用异常

//Dont't do this try { int i = 0; while (true) { range[i++].climb(); } } catch (ArrayIndexOutOfBoundsException e) { } 复制代码

不要优先使用基于异常的模式:

  • 异常机制的设计初衷是用于不正常的情形,因此不多会有JVM实现试图对它们进行优化。
  • 代码块放在try-catch块中反而阻止了现代JVM实现原本可能要执行的某些特定优化。
  • 对数组进行比那里的标准模式并不会致使冗余的检查,有些现代的JVM实现会将它们优化掉。
  • 基于异常的循环模式不只模糊了代码的意图,还下降了性能,并且它还不能保证正常工做,若是出现不想关的bug,这个模式会悄悄地失效。

努力使失败保持原子性

通常而言,失败的方法调用应该使对象保持在被调用以前的状态。具备这种属性的方法被称为具备失败原子性(failure atomic)。有几种途径能够实现这种效果:

  1. 在执行操做前检查参数的有效性,这可使在对象状态被修改前先抛出适当的异常。
  2. 调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修以前发生。
  3. 编写一段恢复代码,由它来拦截操做过程发生的失败,以及使对象回滚到操做开始以前的状态,这种办法主要用于永久性的数据结构。
  4. 在对象的一份临时拷贝上执行操做,操做完成以后再用临时拷贝中的结果代替对象的内容。

不要忽略异常

忽略一个异常很是容易,只需将方法调用经过try语句包围起来,并包含一个空的catch块。空的catch块会使异常达不到应有的目的,至少,catch块也应该包含一条说明,解释为何能够忽略这个异常。

并发

正确地使用同步能够保证没有任何方法会看到对象处于不一致的状态中。它还能够保证刚进入同步方法或者同步代码块的每一个线程,都看到由同一个锁保护的以前全部的修改效果。换句话说,读取一个非long或double类型的变量,能够保证返回的值是某个线程保存在该变量中的,即便多个线程在没有同步的状况下并发地修改这个变量也是如此。

不要使用 Thread.stop方法。要阻止一个线程妨碍另外一个线程,建议作法是让第一个线程轮训一个boolean域,这个域一开始为false,可是能够经过第二个线程设置为true,以表示第一个线程将终止本身。因为boolean域的读写操做都是原子的,程序员在访问这个域的时候再也不使用同步。

实际上,若是读和写操做没有都被同步,同步就不会起做用。

若是变量修饰符是volatile,则读取变量时不须要锁,虽然volatile修饰符不执行互斥访问,但它能够保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值。

使用volatile的时候务必要当心。

//错误
private static volatile  int number = 0;
//须要使用synchronization
public static int getNumber() {
    return number++;
}
复制代码

虽然number是原子的,可是增量操做符不是原子的,它首先读取值,而后写回一个新值。若是第二个线程在第一个线程读取旧值和返回新值期间读取这个域就会出错。

避免过分同步

在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。这样的方法是外来的,这个类不知道方法会作什么事情,也没法控制它,从同步区域中调用它极可能会致使异常、死锁或者数据损坏。

一般,你应该在同步区域内作尽量少的工做。若是你必需要执行某个很耗时的动做,应该设法把这个动做移到同步区域的外面。

executor 和 task 优先于线程

Java1.5增长了java.util.concurrent,这个包中包含了一个Executor Framework:

ExecutorService executorService = Executors.newSingleThreadExecutor();
//执行提交一个runnable方法
executorService.execute(runnable);
//告诉executor如何优雅地终止
executor.shutdonw();
复制代码

你能够利用executor service完成更多的事情。例如,能够等待一个任务集合中的任何任务或全部任务完成(invokeAny或invokeAll),你能够等待executor service优雅地完成终止(awaitTermination),能够在任务完成时逐个地获取这些任务的结果(ExecutorCompletionService)等。

并发工具优于wait和notify

自从java1.5发型版本开始,java就提供了更高级的并发工具,他们能够完成之前必须在wait和notify上手写代码来完成的各项工做。其分红三类:

  • Executor Framework
  • 并发集合(Concurrent Collectionin)
  • 同步器(Synchronizer) 并发集合为标准的集合接口(如List、Queue、Mpa)提供了高性能的并发实现。为了提供高并发性,这些实如今内部本身管理同步,所以,并发集合中不可能排除并发活动,将它锁定没有什么做用,只会是程序的速度变慢。

同步器(Synchronizer)是一些使线程可以等待另外一个线程的对象,容许他们协调动做。最经常使用的同步器是CountDownLatch和Semaphore。

倒计数锁存器(CountDown Latch)是一次性的障碍,容许一个或者多个线程等待一个或者多个其余线程来作某些事情。CountDownLatch是惟一构造器带有一个int类型的参数,这个int参数是指容许全部在等待的线程被处理以前,必须在锁存器上调用countDown方法的次数。

例如:一个方法带有一个执行该动做的executor,一个并发级别(表示要并发执行该动做的次数),以及表示该动做的runnable。全部的工做线程自身都准备好,要在time线程启动时钟以前运行该动做(为了实现准确的定时)。当最后一个工做线程准备好运行该动做时,timer线程就“发起头炮”,同事容许工做线程执行该动做,一旦最后一个工做线程执行完该动做,timer线程就当即中止计时。直接在wait和notify上实现这个逻辑至少来讲会很混乱,而在CountDownLatch之上实现则至关简单:

public long getTime(Executor executor, int councurrency, final Runnable action) throws InterruptedException {
    final CountDownLatch ready = new CountDownLatch(councurrency);
    final CountDownLatch start = new CountDownLatch(1);
    final CountDownLatch done = new CountDownLatch(councurrency);
    for (int i = 0; i < councurrency; i++) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                ready.countDown();
                try {
                    start.await();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    done.countDown();
                }
            }
        });
    }
    ready.await();
    long startNano = System.nanoTime();
    start.countDown();
    done.await();
    return System.nanoTime() - startNano;
    }
复制代码

用ready来告诉timer线程他们已经准备好了。而后工做线程会在start上等待。当最后一个工做线程调用ready.countDown时,timer线程记录下起始时间,并调用start.countDown,容许全部的工做线程继续进行。而后timer线程在done上等待,直到最后一个工做线程运行完该动做,并调用donw.countDown。一旦调用这个,timer线程就会苏醒过来,并记录下结束时间。

wait方法的标准模式:

synchronized(obj) {
    while() {
        obj.wait(); //release lock, and reacquires on wakeup
    }
}
复制代码

始终应该使用wait循环模式来调用wait方法;永远不要在循环以外调用wait方法。循环会在等待以前和以后测试条件。

线程安全性的文档化

线程安全性的几种级别。(这份列表并无涵盖全部的可能,而只是些常见的情形:

  • 不可变的(immutable):这个类的实例是不变的。因此不须要外部的同步,例如String、Long、BigInteger。
  • 无条件的线程安全(unconditionnally thread-safe):这个类的实例是可变的,可是这个类有着足够的内部同步,因此它的实例能够被并发使用,无需任何外部同步。 例如:Random和ConcurrentHashMa
  • 有条件的线程安全(conditionally thread-safe):除了有些方法为进行安全的并发而使用须要外部同步
  • 非线程安全(not thread-safe):这个类的实例是可变的。为了并发地使用它们,客户必须利用本身选择的外部同步包围每一个方法调用(或者调用序列)。这样的例子包括通用的集合实现,例如ArrayList和HashMap。
  • 线程对立的(thread-hostile):这个类不能安全地被多个线程并发使用,即便全部的方法调用都被外部同步包围。线程对立的根源一般在于,没有同步地修改静态数据。Java平台类库中,线程对立的类或者方法很是少。System.runFinalizersOnExit方法是线程对立的,但已经被废除了。
//私有锁对象
private final Object lock = new Object();

public void foo() {
    synchronized(lock) {
        ...
    }
}
复制代码

私有锁对象模式只能用在无条件的线程安全类上。有条件的线程安全类不能使用这种模式,由于它们必须在文档中说明:在执行某些方法调用序列时,它们的客户端程序必须得到哪把锁。

私有锁对象模式特别适用于那些专门为继承而设计的类。若是这种类使用它的实例做为锁对象,之类可能很容易在无心中妨碍基类的操做,反之亦然,出于不一样的目的而使用相同的锁,子类和基类极可能会“互相绊住对方的脚”。

有条件的线程安全类必须在文档中指明哪些方法调用序列须要外部同步,以及在执行这些序列的时候须要得到哪把锁。若是你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法以防止客户端程序和子类的不一样步干扰。

慎用延迟初始化

若是处于性能的考虑须要对静态域使用延迟初始化:

private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}

static FieldHolder getField() {
      
复制代码

若是处于性能的考虑须要对实例域使用延迟初始化:

private volatile FieldType field;
    
FieldTpye getField() {
    FieldType result = field;
    if(result == null) { //First check(no locking)
        synchronized (this) {
            result = field;
            if(result == null) //Second check(with locking)
                field = result = computeFieldValue();
        }
    }
    return result;
}
复制代码

若是须要延迟初始化一个能够接受重复初始化的实例域:

private volatile FieldType field;
    
private FieldType getField() {
    FieldType result = field;
    if(result == null) {
        field = result = computeFiedlValue();
    }
    return  result;
}
复制代码

不要依赖于线程调度器

线程不该该一直处于忙-等状态,即反复地检查一个共享对象,以等待某些事情的发生。

不要让应用程序的正确性依赖于线程调度器,不然结果获得的应用程序将既不健壮,也不具备可移植性。不要依赖Thread.yield或者线程优先级。线程优先级能够用来提升一个已经可以正常工做的程序的服务质量,但永远不该该用来“修正”一个本来能不能工做的程序。

序列化

谨慎地实现Serializable接口

实现Serializable接口而付出的巨大代价是,一旦一个类被发布,就大大下降了“改变这个类的实现”的灵活性。 若是一个类实现了Serializable接口,它的字节流编码(序列化形式)就变成了它的导出的API的一部分,一旦这个类被普遍使用,每每必须永远支持这种序列化形式。

第二个代价是,它增长了出现bug和安全漏洞的可能性。你可能会忘记确保:反序列化过程必须也要保证全部“由真正的构造器创建起来的约束关系”,而且不容许攻击者访问正在构造过程当中的对象的内部信息。

第三个代价是,随着类发行新的版本,相关的测试负担也增长了。可序列化的类被修订后,你必须既要确保“序列化-反序列化”过程成功,也要确保结果产生的对象真正是原始对象的复制品。

内部类不该该实现Serializable。

若是一个类为了继承而设计,要更加当心。对于这样的类而言,在“容许子类实现Serializable接口”或者“禁止子类实现serialzable”二者间的一个折衷方案是:提供一个可访问的无参构造器,这种方案容许(但不要求)子类实现Serializable接口。

相关文章
相关标签/搜索