Effective Java的学习记录

123 阅读10分钟

Java 四大名著之一

可以帮助读者更加有效地使用Java编程语言及其基本类库,java.lang、java.util、java.io,以及子包java.util.cunrrent和java.util.fucntion。 目前更新到第二章。。开始复更啦!!

一. 创建对象和销毁对象(1-9条)

1.1.使用静态工厂方式创建对象


静态工厂并不直接对应设计模式中的工厂模式。类可以提供一个公有的静态工厂方法(static factory method), 它只是一个返回类的实例的静态方法。在Lombok注解里(详情见注解笔记),就可以使用static

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

优点

  • 它有名字,可以更加确切的描述返回的对象。
  • 不必再每次调用它们的时候都创建一个对象。
  • 它可以返回原返回类型的任何子类型的对象。
  • 所返回的对象的类可以随着每次调用而发生变化,这取决于传参。
  • 返回的对象所属的类,可以在编写的静态工厂方法的类里不存在。(类似SPI接口里的类)

缺点

  • 类如果不包含公有的或受保护的构造器,就不能被子类化。(还不太理解,回头再看
  • 程序员很难发现这些方法。(说的可太对了,正经人谁看文档阿

静态工厂的一些管用名称

  • from 类型转换方法,单参
    Date d = Date.from(instant);
  • of 聚合方法,多参
    Set<Rank> faceCards = EnumSet.of(JAK, QUEEN, KING);
  • valueOf 也是一种类型转换
  • instance/getInstance 返回的实例是通过方法的参数来描述的,但是不能说与参数具有同样的值
    StackWalker luke = StackWalker.gerInstance(options) (不太理解,日后再看)
  • create/newInstance 确保每次调用都返回一个新的实例
    Object newArray = Array.newInstance(classObject, arrayLen)
  • getType/newType/type 工厂方法中处于不同类中的时候使用

1.2.遇到过多构造器参数时,考虑使用Builder来构造

这个没什么好说的,很容易理解,你也不想写N多个重构的构造器吧?
有些小天才可能就说了,那我使用JavaBean模式只给个无参的,然后通过setter来设置必要的参数。
确实很聪明啦,但是构造过程被分到了几个调用中,在构造过程中,JavaBean可能处于构造不一致的状态。类无法仅通过检验构造器参数的有效性来保证一致性。试图使用处于不一致状态的对象将会导致失败。此外,JavaBean模式使得把类做成不可变的可能性不复存在(17条),需要额外的努力确保线程安全
可以简单地使用Lombok注解的@Builder来实现。

1.3.用私有构造器或枚举类型强化Singleton属性

Singleton是指仅仅被实例化一次的类。使类成为Singleton会使得它的客户端测试十分困难,因为不可能给Singleton替换模拟实现,除非实现一个充当其他类型的接口。

实现Singleton有多种方法,但核心思想都是构造器私有。

  • 饿汉模式
class SingleTon1{
    private Singleton1(){};
    private static Singleton1 instance1 = new Singleton1();
    public static Singleton1 getInstance(){
        return instance1;
    }
}
  • 懒汉模式
class Singleton2{
    private Singleton2(){};
    private static Singleton2 instance2 = null;
    public static Singleton2 getInstance(){
        if(instance2 == null){
            instance2 = new Singleton2();
        }
        return instance2;
    }
}

但是,很少有人用第三种方法,声明一个包含单元素的枚举类型

public enum SingleTongue {
    INSTANCE;
    public void 方法(){
        ...
    }
}

这个方法更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是面对复杂的序列化或者反射的攻击时。

1.4通过私有构造器来强化不可实例化的能力

有时候你可能会编写一些只包含静态方法和静态域的类(工具类)。这样的类不希望实例化,实例化对他们没有任何意义。但是,在缺少显示构造器的情况下,编译器会自动提供一个缺省的构造器。因此,存在无意识实例化的类。 改成abstract类是完全不可接受的!!!,因为该类可以被子类化,而且该子类会实例化的。此外,甚至会诱导用户对其进行继承(详见第19条)。

只要让这个类包含一个私有构造器,它就不能被实例化。

public class UtilityClass {
    private UtilityClass(){
        throw new AssertionError();//防止有人想要实例化。
    };
}

但是有个副作用,它使得该类不能子类化。且此类所有的构造器都必须显示或隐式地调用超类构造器,在这种情形下,子类就没有可访问的超类构造器可以调用了。

1.5 优先考虑依赖注入来引用资源

许多类会依赖一个或多个底层的资源。例如,拼写检查器需要依赖字典。这种工具类可以写成静态类(见1.3)或者变成Singleton(1.4)

//静态类
public class SpellChecker{
    private stactic final Lexicon dictionary = ...; //lexicon 词汇
    private SpelllChecker(){}//禁止实例化
    public static boolean isValid(String word) {
    }
    ...
}

//Singleton
public class SpellChecker{
    private final Lexicon dictionary = ...;
    private SpelllChecker(...){}//构造器私有,建立Singleton
    public stactic SepllChecker getInstance() {
        return new SpellChecker(...);
    }
    public boolean isValid(String word) {
    }
    ...
}

这两种方法都不理想,因为他们都把字典写死了,假设一本字典能满足所有需求,简直痴心妄想。
若是设dictionary域为nonfinal,并添加一个方法用它来修改词典则会存在一个很大的问题。抛开显得代码很笨重不谈,最重要的就是无法并行工作!! (final值得深入学习)因此,1.4和1.3并不适合需要引用底层资源的类。

这种类需要能够产生多种实例,每个实例都能使用客户端制定的资源,最简单的方式,当创建一个新的实例时,就将该资源传到构造器中。这是依赖注入的一种形式。

//依赖注入
public class SpellChecker {
    private final Lexicon dictionary;
    public SpellChecker(Lexicon dictionary){
        this.dictionary = Objects.requireNonNull(dictionary);
    }
    ...
}

依赖注入使用任意数量的医院,且依赖注入的对象具有不可变性(详见第17条)。同时,该方法不仅使用构造器,也适用静态工厂(1.1)以及Builder(1.2)。

该程序模式另一个有用的变体是,将资源工厂传给构造器。(还不懂,后面懂了来进行补充)

1.6 避免创建不必要的对象

一般来说,最好能重用单个对象,而不是每次都创建一个同样的对象。如果对象是不可变的(详见17条),它就始终可以被重用。

举个例子,要使用String bestLanguage = "Java"而不是String bestLanguage = new String("Java") 前一个只用了一个String实例,而不是每次执行的时候都创建一个新的实例。而且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要包含相同的字符串常量,该对象就会被重用。

对于同时提供了静态工厂方法(1.1)和构造器的不可变类,优先使用静态工厂方法,避免创建不必要的对象。如Boolean.valueOf(String)来替换Boolean(String)

还有一些创建对象成本非常昂贵,重复使用时尽量不要多次创建。然而,这个需要经验来进行判断,并没有很好的判断方式。举例来说:

stactic boolean isRomanNumeral(String s) {
    return s.matches(.....)
}

这个方法依赖于String.matches() 方法,该方法不适合在注重性能的情形中重复使用。主要问题在于,其在内部 为正则表达式创建了一个Pattern实例,却只使用了一次,之后就可以进行垃圾回收了,然而创建Pattern实例的成本却很高。

为了提升性能,应该将正则表达式编译成一个Pattern实例(不可变)

public class RomanNumerals {
    private static final Pattern ROMAN = Pattern.compile(...);
    
    static boolen isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }    
}

此外,还有一种创建多余对象的方法,为自动装箱,尽量使用基本类型而不是装箱基本类型。

这里说的是尽量避免创建不必要对象,而不是尽量重用对象。 有时候,重用对象付出的代价要远远大于因创建重复对象而付出的代价。有时没能实施保护性拷贝,将会导致潜在的BUG和安全漏洞。

1.7 消除过期的对象引用

Java很容易给你留下这样的印象:认为自己不再需要考虑内存管理的事情了,其实不然。

有时候会产生内存泄露,出现不被清理的过期引用。但是也不必过分担心,只需要注意一点:只要类是自己管理内存,就应该警惕内存泄露问题。一旦元素被释放,则该元素中包含的任何对象引用都应该被清空。

内存泄露的第二个常见来源是缓存。

内存泄露的第三个常见来源是监听器和其他回调。

1.8 避免使用终结方法和清除方法

终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定、性能降低,以及可移植问题。

Java 9提供了清除方法(cleaner)来代替终结方法,虽然没终结方法那么危险,但仍然是不可预测、运行缓慢,在一般情况下也是不必要的。

终结和清除的缺点在于不能保证会被及时执行。从一个对象变得不可达,到它的终结方法被执行,所花费的时间时任意长的。因此,注重时间的任务不应该由终结或清除方法来执行。

而且,更过分的是,Java语言规范不仅不保证终结或清除方法会被及时地执行,而且根本不保证它们会被执行!

终结方法还有一个严重的安全问题:为终结方法攻击(finalizer attack)打开了类的大门。 终结方法攻击的基本思路:从构造器或者它的序列化对等体抛出异常,恶意子类的终结方法就可以在构造了一部分的应该已经半途夭折的对象上运行。这个终结方法会将该对象的引用记录在一个静态域中,阻止它被垃圾回收。一旦记录到异常的对象,就可以轻松地在该对象上调用任何原本永远不允许在这里出现的方法。从构造器抛出的异常原可以防止对象继续存在,有了终结方法在就做不到了。这种攻击可能造成致命后果。final类不会受到终结方法的攻击,没人可以编出final类的恶意子类。为了防止非final类受到终结方法攻击,要编写一个空的final 的finalize方法。

说了这么多坏话,它俩也有好处的。

  • 当资源所有者忘记使用close()方法时,终结和清除可以当作“安全网”。迟一点关比永远不关好。不过一些Java类如FileInputStream、FileOutputStream、ThreadPoolExecutorhe java.sql.Connection都可以充当安全网。
  • 清除方法可以终止非关键的本地资源。(还不太懂。。。)

总之,Java9之前,尽量不使用终结方法。

1.9 try-with-resource优于try-finally

try-finally较为常用,经常用于关闭资源,但是其也有着一定的问题,举例来说:

static void copy(String stc, 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();
    }
}

乍一看,是不存在问题的。但是这样写有个很大的问题。因为finally中的代码也会抛出异常,如果out.close()也出问题,会完全抹除in.read()的异常,这在现实的系统中会导致调试变得非常复杂。

使用try-with-finally就可以解决上述问题。Java 7引入该语法,并需要实现AutoCloseable接口。如果编写了一个类,它代表的是必须关闭的资源,那么这个类也应该实现AutoCloseable。还是上一个示例:

    static void copy(String stc, 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);
           }
     }

此时抛出两个异常时,后一个异常就会被禁止,以保留第一个异常。try-with-resource也可以和catch子句进行搭配使用。