美文网首页
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