Effective java如何高效创建和销毁对象-上

2021-08-13  本文已影响0人  先生zeng

该篇幅文章主要记录Effective java这本书中的主要介绍的一些高效率写代码的一些代码编写规则,该书一共有将近100条规则,如果每条规则都写一篇文章,我觉得太多了,没必要,所以我在里面会结合实际的规则情况以及个人的经验,多一些总结跟思考,可能一章会分为几个篇幅与方向讲解,尽量多一些深入思考以及总结。

个人也工作三年多了,学习了不少技术知识以及理论,但是发现很多时候或者其他跟我一样经历的人,在写代码的时候,还是有很多不够规范以及不够好,经常会写代码的时候发现写出来的代码要不是效率很低,要不就是可读性很差,甚至不知不觉中,因为对于java语言特性不够理解透彻,导致写出来的代码,是错误的,"反模式"的,我在该篇幅中,都会做出总结以及提示。

下面开始该第一篇幅的介绍:

今天这篇文章的主题是创建和销毁对象:何时以及如何创建对象,何时以及如何避免创建对象,如何确保它们能够适时地销毁,以及如何管理对象销毁之前必须进行的各种清理动作。

第一条: 用静态工厂方法代替构造器

对于类而言,为了让客户端获取它自身的一个实例,最传统的方法就是提供一个公有
的构造器。还有一种方法,也应该在每个程序员的工具箱中占有一席之地,类可以提供一个公有的静态工厂的方法( static factory method ),它只是一个返回类的实例的静态方法。(书中说明了,这里静态工厂方法不对应设计模式中说明的工厂方法)。

书中举例了关于Boolean这个包装类的简单示例,valueOf(boolean a),翻看源码,可以做个体会:



对比下我们正常使用直接构建公有构造器的写法,

优势

我们会发现几点,因为Boolean是比较简单的一个类,我们使用valueOf的静态工厂方法的这种方式,具有以下几种优势:

  1. 更容易理解他的语义,他们是有名称的,一般构造器方法可能不能正确描述被返回的对象。深入一点的使用情况,我们会遇到需要使用多个构造器的情况,这时候如果只是改变参数数量顺序,我们客户端对于该使用哪个构造器永远也记不住,而使用静态工厂方法的方式,则不会有这种情况,可以在名称上加以区分。

  2. 在使用的时候,如果我们不必在每次使用都新建一个对象,可以使用缓存的对象重复使用,也类似享元模式这种情况(阅读到这里,我脑海想起了也可以说类似单例模式这种情况啊,但是这是有区别的,大家可以额外拓展下,为什么这里说是类似享元模式)。 重复利用对象,如果创建对象的代价很大,这对性能提升是有很大影响的,

静态工厂方法能够为重复的调用返回相同对象,这样有助于类总能严格控制在某个时·
刻哪些实例应该存在,这种类被称作实例受控的类

  1. 他们可以返回原返回类型的任何子类型的对象,这样我们在选择返回对象的类时有了更大的灵活性。

这种灵活性的应用是,我们API可以返回对象,同时又不会使对象的类变成公有的。以这种方式隐藏实现类会使API变得非常简洁。

在Java8 之前 ,接 口不能有静态方法,因此按照惯例,接口 Type 的静态工厂方法被放在一个名为 Types 不可 实例化的伴生类(详见第 4条)中 例如 Java Collections Framework 的集合接口有 45 个工具实现,分别提供了不可修改的集合、 同步集合,等等 几乎所有这些实现都通过静态工厂方法在-个不可 例化的类( java util. Collections 中导出所有返回对象的类都是非公有的(返回的对象是属于该collections的静态成员实例)。

现在的Collections API比导出45个独立公有类的那种实现方式要小多了,每种实现都可以对应一个类,这API的数量上就减少了很多,同时我们掌握的概念和数量难度上也减少了,程序员都知道,被返回的对象是由相关接口精确指定的,所以他们不需要阅读有关的类文档,此外,使用这种静态工厂方法时,甚至要求客户通过接口来引用被返回的对象,而不是通过它的实现来引用被返回的对象,这是一种良好的习惯,后面第46条概念会进行说明:这里我提前透露下其中的一点原因: 如果养成了用接口作为类型的习惯,程序会更加灵活,如果要变换实现时,所需要做的只是改变构造器中类的名称。

java8之后,接口中不能包含静态方法已经成为历史了。因此没有任何理由给接口提供一个不可实例化的伴生类,所以已经被放在这种类中的许多公有静态成员也应该被放到接口中,但是需要注意,任然有必要将这些静态方法背后的大部分实现代码,单独放进一个包级私有的类中,这是因为java8中,仍然要求接口的所有静态成员必须是公有的,java9允许接口有私有的静态方法,但是静态域与静态成员还是需要公有。
(大家随便学习了java9的一些语法糖)

  1. 第四个优势在于,所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。只要是已声明的返回类型的子类型,都是允许的,返回对象的类也可能随着发行版本的不同而不同。

该书籍中,举例了EnumSet中的例子,jdk中他的实现没有公有的构造器,只有静态工厂方法,他们返回两种子类之一的实例,具体会取决于底层枚举类型的大小。
这样要求的的实现是客户端不需要担心他们从工厂方法中得到的对象的类是哪个。

5、 第五大优势,方法返回的对象所属于的类,在编写包含该静态工厂方法的类时可以不存在。

这种灵活的工厂方法构成了服务提供者框架的基础,例如jdbc API。

缺点

讲了这么多,使用静态工厂方法的优点,书里同样总结了几点缺点:

  1. 类如果不包含公有的或者受保护的构造器,就不能被子类化。
  2. 程序员很难发现让门。静态工厂方法很难像构造器那样被明确的标识出来。javadoc工具类以后会慢慢注意到静态工厂方法的,同时,我们可以通过在类或者接口中注释中关注静态工厂方法。

下面列举了jdk实现中一些静态工厂方法的惯用名称。这里只列出了一小部分:



小结

简而言之,静态工厂方法和公有构造器都各有用处,我们要理解它们各自的长处
静态工厂经常更合适,切忌第一反应就是提供公有的构造器,不先考虑静态工厂。

第二条: 遇到多个构造器参数时要考虑使用构建器

静态工厂和构造器有个共同的局限性:它们都不能很好扩展到大量的可选参数.

书里举了一个例子:

使用一个类表示包装食品外面显示的营养成分标签 这些标签中有几个域是必需的:每份的含量、每罐的含量以及每份的卡路里。还有超过 20个的可选域 肪量、饱和脂肪量、转化脂肪、胆固醇、纳,等等 。大多数产品在某几个可选域中都会有非零的值。

这时候去选择创建的时候,如何构造参数不同的类呢?又是使用哪种方式构造呢?构造器还是静态工厂?

程序员一般会使用重叠构造器模式来创建不同的类。 这种模式吓,提供的第一个构造器只有必要的参数,第二个构造器会多一个可选参数,依次递增类推,最后一个一般会包含所有的。 那上面的例子就会有几十个构造器。

会遇到的问题,可能因为参数很多,客户端代码会很难编写,构建起来会很复杂,而且还难以阅读。

还有第二种解决方法: 使用javaBean的方式,用setter去设置每个必要参数及可选参数。这样实现的话,代码可读性上去了,但javaBean自身有很严重的问题,如果调用过程分配到几个调用中,再构造过程中JavaBean就可能处于不一致的状态。

分析该方式可能导致的问题:

1.类无法通过校验构造器参数保证一致性。
2.该处理方式,会使把类做成不可变的可能性不复存在,需要程序员做其他操作来保证线程安全。(可以通过“使类成为final的”、把类的构造器私有或包级私有的。)

当对象的 造完成,并且不允许在冻结之前使用 ,通过手工“冻结”对象可 弥补这些不足,但是这种方式十分笨拙,在实践中很少使用 此外,它甚至会在运行时导致错误,因为编译器无法确保程序员会在使用之前先调用对象上的 freeze 方法进行冻结。

以上两种方法都有各自的缺点跟可能导致的问题,接下来就是该准则说的方法: 建造者(Builder)模式的一种形式。

它不会直接生成我们想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂)得到一个builder对象。然后客户端在builder对象上调用类似与setter的方法,来设置每个相关的可选参数。最后客户端调用无参的build方法来生成同承是不可变的对象。这个builder通常使它构建的类的静态成员类。

代码示例:

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 {

        private int servingSize;
        private int servings;
        private int calories;
        private int fat;
        private int sodium;
        private int carbohydrate;

        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 Builder sodium(int val) {
            sodium = val;
            return this;
        }

        public Builder carbohydrate(int val) {
            carbohydrate = 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;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

    @Override
    public String toString() {
        return "NutritionFacts{" +
                "servingSize=" + servingSize +
                ", servings=" + servings +
                ", calories=" + calories +
                ", fat=" + fat +
                ", sodium=" + sodium +
                ", carbohydrate=" + carbohydrate +
                '}';
    }

    public static void main(String[] args) {
        NutritionFacts nutritionFacts = new Builder(0, 3).sodium(10).calories(20).fat(30).build();
        System.out.println(nutritionFacts);
    }

}

这种模式从代码上看,更加简洁,可读性好,同时可以确保NutritionFacts这个类,可以使其不可变类,也模拟了具名的可选参数。为了一些简洁起见,我们在代码中减少了参数的有效性校验。

为了确保某些参数的不变量,不会受到修改跟攻击,从builder复制完参数后,要检查对象域,如果检查失败,就抛出参数类型异常出 llegalArgumentExcept ion (详 72条),其中的详细信息会说明哪些参数是无效的。

Builder 模式也适用于类层次结构 使用平行层次结构的 builder 时, 各自嵌套在相应的类中,抽象类有抽象的 builder ,具体类有具体的 builder ,假设用类层次根部 的一个抽象类表示各式各样的披萨:

public abstract class Pizza {

    public enum Topping { HAM, MUSHROOM,ONION, PEPPER, SAUSAGE}
    final Set<Topping> toppings;

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }
        abstract Pizza build();
        // Subclasses must override this method to return "this"
        // 这个针对 Java 缺乏 self 型的解决方案,被称作模拟的 self型
        protected abstract T self();
    }

    Pizza(Builder<?> builder){
        toppings = builder.toppings.clone();
    }

}

注意, Pizza Builder类型是泛型,它和抽象的 self 方法一样,允许在子类中适当地进行方法链接,不需要转换类型 这个针对 Java 缺乏 self 型的解决方案,被称作模拟的 self型。

这里有两个具体的 Pizza 子类,其中 个表示经典纽约风味的比萨,另一个表示馅料的半月型( calzone )比萨 前者需要一个尺寸参数,后者则要你指定酱汁应该内置还是外置:

public class NyPizza extends Pizza{

    public enum Size { SMALL, MEDIUM,LARGE}

    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {

        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        @Override
        Pizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

public class Calzone extends Pizza {

    private final Boolean sauceInside;

    private static class Builder extends Pizza.Builder<Builder> {

        private Boolean sauceInside = false;

        public Builder(Boolean sauceInside) {
            this.sauceInside = Objects.requireNonNull(sauceInside);
        }

        @Override
        Pizza build() {
            return new Calzone(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    Calzone(Builder builder) {
        super(builder);
        this.sauceInside = builder.sauceInside;
    }
}

注意,每个子类的构建器中的 build 方法,都声明返回正确的子类: NyPizza.Bu lder的build 方法返回 NyPizza ,而 Calzone.Builder 中的则返回 Calzone.在该方法中,子类方法声明返回超级类中声明的返回类型的子类型,这被称作协变返回类型(covariant return type 它允许客户端无须转换类型就能使用这些构建器.

这些层次化构建器的客户端代码本质上与简单的 NutritionFacts 构建器一样。使用如下:

与构造器相比, builder 微略优势在于,它可 以有 个可变( varargs 参数 因为builder 是利用单独的方法来设置每一个参数 此外,构造器还可以将多次调用某一个方法而传人的参数集 中到一个域中.

Builder 模式的确 有它自身的不 为了创建对象 ,必须先 建它的构建器 虽然创建这个构建器的开销在实践中可能不那么明显 但是在某些十分注重性能的情况下,可能就成问题了 Builder 模式还 比重叠构造器模式更加冗 ,因此它只在有很多参数的时候才使用,比如 4个或者更多个参数 但是记住,将来你可能需要添加参数 如果一开始就使用构造器或者静态 厂,等到类需要多个参数时才 加构造器,就会无法控制,那些过时的构造器或者静态工厂显得十分不协调 此,通常最好一开始就使用构建器.

简而言之 如果类的构造器或者静态工厂中具有多个参数,设计这种类时, Builder模式就是一种不错的选择, 特别是当大多数参数都是可选或者类型相同的时候 与使用重叠构造器模式相比,使用 Builder 模式的客户端代码将更易于 阅读和编写,构建器也比JavaBeans 更加安全。

待续

上一篇下一篇

猜你喜欢

热点阅读