美文网首页
Effective Java 学习笔记

Effective Java 学习笔记

作者: Parallel_Lines | 来源:发表于2020-04-24 16:52 被阅读0次

Effective Java 一书不仅仅是对于 Java,即使对于整个编程学习来讲,也深有启发。

因此仅以此博客,每日记录一则 Effective Java 规则。

4.24

一、用静态工厂方法代替构造器

BigInteger.valueOf(long) 源码

public static BigInteger valueOf(long value) {
    if (value < 0) {
        if (value != -1) {
            return new BigInteger(-1, -value);
        }
        return MINUS_ONE;
    } else if (value < SMALL_VALUES.length) {
        return SMALL_VALUES[(int) value];
    } else {// (value > 10)
        return new BigInteger(1, value);
    }
}

即通过静态方法返回类的实例。

优势

1.有名称。

用户往往无法分辨多个构造器的区别,从而调用错误的构造器。

因此当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器。

注意 这不是工厂方法模式

2.不必每次创建新对象。

类似享元模式

3.可以返回子类型对象。

用户不需要知道子类有哪些,只需要知道被返回的对象是符合要求的即可。这对 API 来说是一种变相减少。

4.可以随参数而变化。

用户不需要也不关心返回的对象,它们只需要知道是某个子类即可。

5.方法返回对象所属的类,在编写包含该静态工厂方法的类时,可以不存在。

这是服务提供者框架的基础。

参考反射

缺点

1.类如果没有 public 或 protected 的构造器,就不能被子类化。

留坑...后续接规则 18

2.程序员很难注意到它们。

直观来讲,使用者更倾向于优先使用构造器,因为构造函数会在 API 文档明确标注,而静态工厂方法不会。

因此,Effective Java 规定一些命名规则,以更快速的发现一个类的静态工厂方法。

命名规则

命名:from
含义:类型转换方法,只有单个参数。
示例:Date d = Date.from(instant);

命名:of
含义:聚合方法,将多个参数合并为一个实例。
示例:Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);

命名:valueOf
含义:类型转换方法,只有单个参数。
示例:String oneString = String.valueOf(1);

命名:instance 或 getInstance
含义:返回的实例是通过参数来描述的。
示例:

命名:create 或 newInstance
含义:和上一条的区别是,每次调用都返回一个新实例。
示例:Object newArray = Array.newinstance(classObject, arrayLen);

命名:get<Type>
含义:同 getInstance,但用于工厂方法。
示例:FileStore fs = Files.getFileStore(path);

命名:new<Type>
含义:同 newInstance,但用于工厂方法。
示例:Files.newBufferedReader(buffer);

使用场景

多数情况下,静态工厂方法经常更加合适,因此在 提供公有构造函数之前,优先考虑静态工厂方法

二、多个构造器参数时优先使用 Builder 模式

背景

静态工厂方法和构造函数有共同的缺点,即不能很好的处理大量可选参数的情况。

举个例子,一本书有很多参数,包括书名、版号、作者、价格等等。

public class Book {

    private String name;
    private String author;
    private int price;
    private Date date;
    private int version;
}

这些参数有的是必须的,有的是可选的,一般情况下,程序员会选择 重叠构造器模式。第一个构造器拥有必须参数,第二个构造器拥有必须参数外的一个可选参数,后续依次类推。

结果如下:

public class Book {

    private String name;
    private String author;
    private int price;
    private Date date;
    private int version;
    
    public Book(String name, String author, int price) {
        this.name = name;
        this.author = author;
        this.price = price;
    }

    public Book(String name, String author, int price, Date date) {
        this.name = name;
        this.author = author;
        this.price = price;
        this.date = date;
    }

    public Book(String name, String author, int price, Date date, int version) {
        this.name = name;
        this.author = author;
        this.price = price;
        this.date = date;
        this.version = version;
    }
}

重叠模式可行,但在参数大量增加的情况下,将很难编写而且难以阅读。而且即使开发时参数颠倒,如果参数类型相同,在编译期也不会出错。

因此有第二种方式,JavaBeans 模式

public class Book {

    private String name; //必须
    private String author; //必须
    private int price; //必须
    private Date date;
    private int version = 1; // 默认值

    public void setName(String name) {this.name = name;}
    public void setAuthor(String author) {this.author = author;}
    public void setPrice(int price) {this.price = price;}
    public void setDate(Date date) {this.date = date;}
    public void setVersion(int version) {this.version = version;}
}

JavaBeans 模式有严重的缺点,它将构造过程分到了几个调用中,导致构造结果需要手动控制,并且多线程情况下存在安全问题。

解决

也就说,我们既需要 JavaBeans 模式的 可读性,同时需要重叠构造器模式的 安全性

这就需要 建造者模式

public class Book {

    private final String name; //必须
    private final String author; //必须
    private final int price; //必须
    private final Date date;
    private final int version;

    private Book(Builder builder) {
        name = builder.name;
        author = builder.author;
        price = builder.price;
        date = builder.date;
        version = builder.version;
    }

    public static class Builder {
        //必须
        private final String name; //必须
        private final String author; //必须
        private final int price; //必须
        //可选
        private Date date;
        private int version = 1; // 默认值

        public Builder(String name, String author, int price) {
            this.name = name;
            this.author = author;
            this.price = price;
        }

        public Builder date(Date date) {
            this.date = date;
            return this;
        }

        public Builder version(int version) {
            this.version = version;
            return this;
        }

        public Book build() {
            return new Book(this);
        }
    }
}

调用时,得到流式 API:

Book book = new Book.Builder("书籍", "作者", 100).date(new Date()).version(1).build();

Builder 中,还可以对参数进行有效性检查,及时抛出异常。通过 Builder 模式,既保证了类构造时的安全性(构造过程一体化,多线程安全),同时保证了可读性。

此外,Builder 还适用于类层次结构。即抽象类中有抽象的 Builder,具体类中有具体的 Builder,通过使用泛型,可以在具体类中实现具体 Builder 的同时,无需转换类型(协变返回类型)。
最后,使用 Builder 模式,还可以在传入集合参数时,以 add、put 的形式逐个传入集合元素。
由于这俩点不是使用 Builder 模式的主要原因,所以不再详细阐述,有兴趣可以查阅 Effective Java 一书。

使用场景

如果类的构造器或静态工厂方法中具有多个参数,设计这种类时,Builder 模式就是不错的选择。

4.26

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

有些 工具类 是不希望被实例化的,因为实例化对它没有任何意义。然而编译器会自动提供一个公有的、无参的缺省构造器。

为此,让类包含一个私有构造器,它就不能被实例化:

public class UtilClass {

    private UtilClass() {
        throw new AssertionError();
    }
}

AssertionError() 不是必须的,但是它可以避免不小心在类的内部调用构造器。

私有构造器用法的缺点是,它使得一个类不能再被子类化(因为所有的构造器必须显式或隐式的调用超类构造器)。

四、优先考虑依赖注入引入资源

背景

举个例子:

查词需要依赖词典,因此通常会通过静态工具类或单例来实现查词类。

静态工具类

public class DictChecker {

    private static final Dict DICT = ...;

    private DictChecker() {
        ...
    }

    // 词典里是否包含该词
    public static boolean contains(String word) {
        ...
    }

    // 词的翻译
    public static String translation(String word) {
        ...
    }
}

单例类

public class DictChecker {

    private static final Dict DICT = ...;

    private DictChecker() {
        ...
    }

    private static DictChecker INSTANCE = new DictChecker();

    // 词典里是否包含该词
    public boolean contains(String word) {
        ...
    }

    // 词的翻译
    public String translation(String word) {
        ...
    }
}

以上俩种实现方式的问题在于,它们都假设只有一本词典可用。

如果添加一个方法来修改词典,则这样的设置会显得很笨拙、容易出错、并且无法并行工作。

因此,静态工具类和 Singleton 类不适合需要引用底层资源的类

使用依赖注入解决此问题:

public class DictChecker {

    private final Dict dict;

    private DictChecker(Dict dict) {
        this.dict = Objects.requireNonNull(dict);
    }

    // 词典里是否包含该词
    public boolean contains(String word) {
        ...
    }

    // 词的翻译
    public String translation(String word) {
        ...
    }
}

使用依赖注入极大的提升了灵活性和可测试性,并且注入的对象具有不可变性,因此多个客户端可以共享依赖对象。

依赖注入也有缺点,即会导致项目凌乱不堪,但是可以通过依赖注入框架简单解决,如 Dagger。

使用场景

不要用 Singleton 或静态工具类来实现依赖一个或多个底层资源的类,且该资源会影响到类的行为。

应该将资源或者工厂传给构造器,通过它们来创建类。

[TOC]

相关文章

网友评论

      本文标题:Effective Java 学习笔记

      本文链接:https://www.haomeiwen.com/subject/nwnfwhtx.html