《Effective Java》读书笔记(1-2章)

第一章 创建和销毁对象

1. 考虑用静态代替构造方法

想要获取一个类的实例,一种传统的方式是通过共有的构造器,当然还可以使用另一种技术:提供共有的静态工厂方法。
什么是静态工厂?

public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

为什么要用静态工厂替换构造方法?有什么优点

  1. 静态工厂相比构造器来讲,有名字并且通俗易懂,构造器的名字必须和类名一致
  2. 静态工厂每次调用不必新建对象。所以适用于不可变的类,单例,初始化就缓存好,避免重复创建。
  3. 静态工厂方法能够返回原先返回类型的任意子类型的对象,更加灵活的选择返回对象。例如Collection有32个实现类在Collections中可以返回。
  4. 静态工厂可以根据调用传入的不同参数返回不同的对象。

静态工厂的不足之处?

  1. 静态工厂没有public和protected的方法,因此不能被子类化。
    一般静态工厂方法名字的含义
fromValue(value) //这种通过传入单个参数返回相应类型的实例对象
of(v1,v2,v3) // 传入多个参数,返回报站这些参数的实例。
// valueOf是from of更详细的替代方案
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
Object newArray = Array.newInstance(classObject, arrayLen);//每次返回的对象都是新的实例

2. 当遇到多个构造器使用构建者

构造方法和静态工厂共有的限制:不能很好的扩展很多可选参数的场景 。因此对于多个可选参数,考虑使用构建者模式。
其实对于等多个可选参数可以使用新建JavaBean 使用set方法创建实例,这样更通俗易懂但是会很冗长。
builder结合了构造方法的安全性和JavaBean 模式的可读性。

// Builder Pattern
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;
        // Optional parameters - initialized to default values
        private int calories      = 0;
        private int fat           = 0;
        
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }
        public Builder calories(int val) { 
            calories = val;      
            return this;
        }
        public Builder fat(int val) { 
           fat = val;           
           return this;
        }
       
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
    }
}

为了确保多个参数的不变性不受攻击,可以在builder复制参数后对对象属性进行检查。检查失败抛出非法参数异常(IllegalArgumentException)。
单个builder可以重复使用构建多个不同的对象,对象的参数可以灵活调整,适用多个可选参数。
协变返回类型:一个子类的方法被声明为返回在父类中声明的返回类型的子类型,称为协变返回类型(covariant return typing)。 它允许客户端使用这些 builder,而不需要强制转换。(这个比较有意思)

3. 使用私有构造器或者枚举实现单例

单例对象通常表示无状态,不可变对象。
实现单例的几种方式,其中枚举方式最佳,无偿提供了序列化机制,防止多个实例化。可以阻止反射创建实例。

// 单例模式的实现方式一 Singleton with public final field
public class Elvis {
    // 公共静态成员变量
    public static final Elvis INSTANCE = new Elvis();
    // 私有化构造函数
    private Elvis() { ... }
    public void leaveTheBuilding() { ... }
}

// 单例模式的实现方式二 Singleton with static factory
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    // 每次调用该方法否返回同一个对象引用
    public static Elvis getInstance() { return INSTANCE; }
    public void leaveTheBuilding() { ... }
}

// 单例模式的实现方式三 Enum singleton - the preferred approach(最佳方式)
public enum Elvis {
    INSTANCE;
    public void leaveTheBuilding() { ... }
}
// readResolve method to preserve singleton property
private Object readResolve() {
     // Return the one true Elvis and let the garbage collector
     // take care of the Elvis impersonator.
    return INSTANCE;
}

在实例化过程中为了保证单例不被破坏,生命所有的字段为transient,并提供readResolve方法。否则当序列化实例被反序列化时,就会创建一个新的实例。

4. 使用私有构造器实现非实例化

试图通过创建抽象类来实现非实例化是行不通的,因为该类的子类可以被实例化,并且他还可能会误导用户该类是为了继承而设计的。因此使用简单的方式——私有化构造函数实现类的非实例化。

5. 依赖注入优于硬链接资源

当多个类依赖于同一个或者多个底层资源时,静态工具和单例模式对于这种场景是不适用的。因为这两种方式再并发场景中变得不可用,更容易出错。
其实每个实例在使用客户端的资源时,可以在创建时将资源的参数传入构造函数中,这就是依赖注入的一种形式(构造方法注入)。这种方式保证了资源的不可变性,依赖注入不仅适用于构造器,也同样适用于静态工厂和Builder。Supplier这个接口就可以很好的标识这些工厂,客户端传入一个工厂,工厂负责创建指定类型的实例。

// 例如生成马赛克的工厂
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

依赖注入可以大幅度提升类的灵活性,可测试性,复用性。

6. 避免创建不必要的对象

如果一个对象是不可变的,那么他总能被复用,复用相比于新建更快速。
当不可变类同时提供了构造器和静态工厂方法时,优先使用静态方法来避免创建不必要对象。
自动装箱可能会创建不必要的对象,他模糊了基本类型和装箱类型之间的区别,但是没有消除这种区别,有可能会导致一些性能问题,因此优先使用基本类型而不是装箱类型。

private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        // Long => long
        sum += i;
    return sum; 
}

注意相比于创建新对象,复用的代价更高,如果没能暴增拷贝安全,将会导致潜在的bug和安全漏洞。

7. 消除过时的对象引用

内存泄漏:指程序再申请内存后,无法释放已申请的内存空间,内存泄漏堆积后就会发生内存溢出。
内存溢出:报错OOM,没有足够的内存供申请者使用。
一般来讲当一个类自己管理自己的内存时,程序员就要注意内存内存泄露问题了,只要一个元素被释放了,那这个元素包含的所有对象应用都应该被清空。

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference help gc
    return result;
}

消除过期引用的最佳方式是将每个变量定义在最小的作用域中。
缓存是内存泄漏的另一个来源。当将一个对象放到缓存中取,时间长了很容易忘记他还在那,剞劂方法可以使用WeakHashMap来充当缓存,只要key过期后就会被自动清除。

8. 避免使用终结方法和清理方法

在Java9中,finalizers方法已经过时了,替代的是清理方法,清理方法比终结方法危险性更低,但仍然是不可预测的,性能比较低,并且也是非必要不使用。
不使用的原因:
● finalizers方法和清理方法的缺点在于不能保证被及时执行或会被执行,当一个对象变得不可达,到执行终结方法或清理方法时,这个时间段是任意长的。因此当有对时间要求的任务不应该调用终结或清理方法完成,不应该依赖终结方法或者清理方法来更新重要的持久态。
● finalizers方法的另一个问题是在终结过程中会忽略掉抛出的异常,并且不会打印线程终止的堆栈信息。如果另一个线程企图使用这种未捕获异常的对象可能会发生不确定的行为。
● finalizers方法和清理方法会严重影响性能。
清理方法和终结方法的两种用途:

  1. 当对象的持有者忘记调用终止方法的情况下充当安全网。如 FileInputStream、FileOutputStream、ThreadPoolExecutor、和 java.sql.Connection具有充当安全网终结方法。
  2. 本地对灯体是普通对象通过本机方法委托的非Java对象,因为本地对等体不是普通Java对象,因此垃圾收集器不会识别它,当性能可接受且本地对等体没有关键的资源,则可以用清理或者终结方法回收。

9. 使用try-with-resources代替try-finally

Java中有许多必须通过调用close方法手动关闭的资源,比如InputStream,OutputStream.
在之前,即使是程序抛出异常或者返回的情况下,try-finally是保证资源正确关闭的最佳方式。但是当处理多个资源关闭时,情况就会变糟。

// try-finally is ugly when used with more than one resource!
static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        } finally {
            out.close();
        }
    } finally {
        in.close();
    }
}

// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
    try (InputStream   in = new FileInputStream(src);
         OutputStream out = new FileOutputStream(dst)) {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0)
            out.write(buf, 0, n);
    }
}

这个时候,当最里层的finally的close方法关闭失败,外层的的异常就会覆盖掉里层的异常,导致调试过程会很困难。但使用try-with-resourses就可以避免这个问题,并且try-with-resourses代码更加简洁易读。

第二章 所有对象都通用的方法

10. 覆盖equals方法时请遵守通用约定

因为Object主要是为继承设计的,它的所有非final方法都有清晰地约定,任何类要重写这些方法时,都有义务去遵守这些约定否则其他依赖这些约定的类就不会正常工作。
什么时候不需要覆盖equals方法?
● 每个类的实例都是固有唯一的。如Thread
● 类不需要提供逻辑相等的功能。
● 父类已经重写过equals方法,父类的行为完全适合子类。
● 类是私有的,并且equals方法永远不会被调用。
什么时候需要重写equals方法?
如果一个类需要一个逻辑相等的概念,并且父类没有重写过这个方法,需要在该类中重写equals方法。通常这种类是值类。如Integer,String。
重写equals方法必须遵守的约定。

  1. 自反性:对任何费控引用x,x.equals(x)必须返回true。
  2. 对称性:对于任何非空引用x和y,如果y.equals(x)=true,则x.equals(y)=true
  3. 传递性: x,y,z都不为null,如果x.equals(y)=true,y.equals(z)=true,则z.equals(x)=true。
  4. 一致性:如果x,y非空,并且equals比较中的信息没有修改,多次调用x.equals(y)都要始终返回true或false。
  5. 对任何非空引用x,x.equals(null)必须返回false。
    重写equals方法时,要重写hashCode方法,不要让equals方法干太多事,不要将Object参数类型替换成其他类型。

11. 重写equals方法时总要重写hashCode

重写equals方法必须重写hashCode方法,如果没有重写,在使用HasMap或HashSet时无法正常运作。
重写的equals方法必须遵守 Object 中指定的规定。
● 在应用程序的执行期间,只要对象的 equals 方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode 方法都必须始终如一地返回相同的值。
● 如果两个对象调用 equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的 hashCode 方法都必须产生相同的整数结果。
● 如果两个对象根据 equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的 hashCode 方法,则不一定要产生不同的结果。无论如何,开发者应该知道,不相等的对象产生截然不同的结果,有可能提高散列表(hash tables)的性能。

12. 始终覆盖toString

默认的Object的toString方法返回的是:类名+@+无符号16进制散列码。
toString 方法应该返回对象中包含的所有值得关注的信息。
在实现toString时,需要判断是否处理返回值的格式。
● 指定字符串的格式的好处:更易读。
● 指定字符串的格式的坏处:一旦被广泛使用,必须始终坚持这种格式。如果改变格式,将会破坏代码和数据。
无论是否指定格式,都为 toString 返回值中包含的所有信息,提供一种编程式的访问路径。否则不得不自己去解析,解析过程可能会出错。

13. 谨慎覆盖clone

假设我们需要为一个类实现Cloneable接口,这个类的父类提供了一个良好的clone方法。我们从super.clone中得到的对象将会是原始对象的一个完整克隆。类中声明的任一属性的值将会和原始类对应的属性的值相等。如果每个属性包含了基本类型值或者不过变对象的引用,那么返回的对象可能正是我们要的,在这种情况下,不需要进一步的处理。
如果一个类它的所有父类获取clone的对象是通过调用super.clone,那么

x.clone().getClass() == x.getClass()

对于不可变的类不应该提供clone方法,因为这会造成无意义的拷贝。对于final修饰的属性,克隆不会成功因为禁止向final修饰的属性二次赋值。
实际上,clone方法就是另一个构造器,我们必须保证它不会破坏原始对象而且能恰当创建被克隆对象的约束条件。

14. 考虑是否实现comparable

compareTo方法并没有在Object里被声明,而是在Comparable接口中声明的唯一方法。
假如一个类实现了Comparable接口,那就表明了这个类的各个实例之间是有顺序的。几乎所有的Java类库,包括枚举类型(条目34),都实现了Comparable接口。
在下面的表述中,符号sgn(expression)表示数学中的signum函数,返回值为-1,0,1。
● 实现类必须确保对于所有的x和y,sgn(x.compareTo(y)) == -sgn(y. compareTo(x))成立。(这意味着,当且仅当y.compareTo(x)抛出了异常,x.compareTo(y)也必须抛出异常。)(自反性)
● 实现类必须确保关系的传递性:若(x. compareTo(y) > 0 && y.compareTo(z) > 0),则x.compareTo(z)>0。
● 最后,实现类必须确保若x.compareTo(y) == 0,则对于所有的z,sgn(x.compareTo(z)) == sgn(y.compareTo(z))成立。(一致性)
● 强烈建议(x.compareTo(y) == 0) == (x.equals(y))成立,但这并不是必须的,通常来说,任何实现了Comparable接口的类如果违反了这个条件,那么应该做个说明。推荐的说法是“注意:该类具有自然排序,但是与equals方法不一致。”
当遇到不同类型的实例比较时,会抛出ClassCastException异常。
在compareTo方法里,我们对属性进行比较是为了得到一个顺序而不是看其是否相等。为了比较对象引用的属性,我们可以递归地调用compareTo方法。如果一个属性没有实现Comparable接口或者我们需要一个非标准的顺序,可以使用Comparator来替代。

private static final Comparator<PhoneNumber> COMPARATOR = comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)  
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) { 
    return COMPARATOR.compare(this, pn);
}

compareTo或compare方法依赖于两个值之间的差值,若第一个值小于第二个值,则为负,若两个值相等,则为0,若第一个值大于第二个值,则为正。

在实现compareTo方法时,避免使用 < 和 > 运算符来进行属性值的比较。相反,我们应该使用封箱基本类型的静态比较方法(例如Integer.compare),或者Comparator接口里的比较器构造方法。
(例如Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值