【Effective Java】多构造器参数使用构建器 (快速上手)

Java系列文章目录

补充内容 Windows通过SSH连接Linux
第一章 Linux基本命令的学习与Linux历史


一、前言

对《Effective Java》每条规定总结

  • 第二条:遇到多个构造器参数考虑使用构建器
  • 后面看看能不能再简洁点

二、学习内容:

2.1 为什么引入构建器

🌟重叠构造器:通过构造函数传入参数

缺点如下:

  • 客户端代码会很难编写,并且仍然较难以阅读
  • 如果客户端不小心颠倒了参数的顺序,编译器也不会出错,但是程序在运行时会出现错误的行

🌟JavaBean模式:就是平时常用的getter与setter方法

缺点如下:

  • 在构造过程中JavaBean可能处于不一致的状态
  • JavaBeans模式使得把类做成不可变的可能性不复存在

2.2 建造者模式

🌟 建造者模式:构建器做类似JavaBean模式的操作,参数传递做类似重叠构造器的操作
注意使用条件是要有多个参数的时候

既保证了重叠构造器的安全性又实现了JavaBean模式的可读性

  • Builder模式模拟了具名的可选参数
  • Builder模式也适用于类层次结构

使用步骤:

  1. 不直接生成对象,让用户利用参数调用构造器(或者静态工厂),得到一个builder对象。
NutrutionFacts coke = new NutrutionFacts.Builder(1,2)
  1. 然后客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。
.setFat(1)
.setCalories(1)
  1. 最后,客户端调用无参的build方法来生成通常是不可变的对象。
.build();

.build();对应的构建器方法:

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

2.2.1书中的例子

为了简洁起见,示例中省略了有效性检查。要想尽快侦测到无效的参数,可以在
builder的构造器和方法中检查参数的有效性。查看不可变量,包括build方法调用的构造
器中的多个参数。为了确保这些不变量免受攻击,从builder复制完参数之后,要检查对象
域(详见第50条)。如果检查失败,就抛出IllegalArgumentException(详见第72
条),其中的详细信息会说明哪些参数是无效的(详见第75条)。
这些后面一条一条讲,之后会补充

  • 在构建器里使用setter方法设置,返回的时候直接通过.build();把设置的参数赋值给对象

注意构造方法是私有的

构建器:

package org.example.entity;

/**
 * 营养成分类,用于存储和管理食品的营养信息
 */
public class NutrutionFacts {

    // 营养成分属性,包括份量大小、份数、卡路里、脂肪、钠和碳水化合物
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    /**
     * 私有构造方法,通过构建器模式创建NutrutionFacts实例
     * @param builder 用于初始化NutrutionFacts实例的构建器对象
     */
    private NutrutionFacts(Builder builder) {
        this.servingSize = builder.servingSize;
        this.servings = builder.servings;
        this.calories = builder.calories;
        this.fat = builder.fat;
        this.sodium = builder.sodium;
        this.carbohydrate = builder.carbohydrate;
    }

    /**
     * 构建器类,用于构建NutrutionFacts实例
     */
    public static class Builder {
        // 必选参数:份量大小和份数
        private final int servingSize;
        private final int servings;

        // 可选参数,默认值为0
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;

        /**
         * 构造方法,初始化必选参数
         * @param servingSize 一份的容量(如毫升或克)
         * @param servings 包含的份数
         */
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        // 设置卡路里方法,返回当前构建器实例
        public Builder setCalories(int calories) {
            this.calories = calories;
            return this;
        }

        // 设置脂肪方法,返回当前构建器实例
        public Builder setFat(int fat) {
            this.fat = fat;
            return this;
        }

        // 设置钠含量方法,返回当前构建器实例
        public Builder setSodium(int sodium) {
            this.sodium = sodium;
            return this;
        }

        // 设置碳水化合物方法,返回当前构建器实例
        public Builder setCarbohydrate(int carbohydrate) {
            this.carbohydrate = carbohydrate;
            return this;
        }

        /**
         * 创建并返回一个NutrutionFacts实例
         * @return 使用当前Builder实例的配置创建的NutrutionFacts对象
         */
        public NutrutionFacts build() {
            return new NutrutionFacts(this);
        }
    }

    // 营养成分的getter方法
    public int getServingSize() {
        return servingSize;
    }

    public int getServings() {
        return servings;
    }

    public int getCalories() {
        return calories;
    }

    public int getFat() {
        return fat;
    }

    public int getSodium() {
        return sodium;
    }

    public int getCarbohydrate() {
        return carbohydrate;
    }
}

使用方法:

// 创建一个NutrutionFacts对象来记录可乐的营养信息
NutrutionFacts coke = new NutrutionFacts.Builder(1,2)
        .setFat(1)
        .setCalories(1)
        .build();
  • .build();返回的就是NutrutionFacts实例了

2.2.2 例子加上有效性检查

可以提前了解一下

  • 可以对比上面写法看看区别
package org.example.entity;

/**
 * 营养成分类,用于存储和管理食品的营养信息
 */
public class NutrutionFacts {

    // 营养成分属性,包括份量大小、份数、卡路里、脂肪、钠和碳水化合物
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    /**
     * 私有构造方法,通过构建器模式创建NutrutionFacts实例
     * @param builder 用于初始化NutrutionFacts实例的构建器对象
     */
    private NutrutionFacts(Builder builder) {
        validateServingSize(builder.servingSize);
        validateServings(builder.servings);
        validateCalories(builder.calories);
        validateFat(builder.fat);
        validateSodium(builder.sodium);
        validateCarbohydrate(builder.carbohydrate);

        this.servingSize = builder.servingSize;
        this.servings = builder.servings;
        this.calories = builder.calories;
        this.fat = builder.fat;
        this.sodium = builder.sodium;
        this.carbohydrate = builder.carbohydrate;
    }

    /**
     * 构建器类,用于构建NutrutionFacts实例
     */
    public static class Builder {
        // 必选参数:份量大小和份数
        private final int servingSize;
        private final int servings;

        // 可选参数,默认值为0
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;

        /**
         * 构造方法,初始化必选参数
         * @param servingSize 一份的容量(如毫升或克)
         * @param servings 包含的份数
         */
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
            validateServingSize(servingSize);
            validateServings(servings);
        }

        // 设置卡路里方法,返回当前构建器实例
        public Builder setCalories(int calories) {
            this.calories = calories;
            validateCalories(calories);
            return this;
        }

        // 设置脂肪方法,返回当前构建器实例
        public Builder setFat(int fat) {
            this.fat = fat;
            validateFat(fat);
            return this;
        }

        // 设置钠含量方法,返回当前构建器实例
        public Builder setSodium(int sodium) {
            this.sodium = sodium;
            validateSodium(sodium);
            return this;
        }

        // 设置碳水化合物方法,返回当前构建器实例
        public Builder setCarbohydrate(int carbohydrate) {
            this.carbohydrate = carbohydrate;
            validateCarbohydrate(carbohydrate);
            return this;
        }

        /**
         * 创建并返回一个NutrutionFacts实例
         * @return 使用当前Builder实例的配置创建的NutrutionFacts对象
         */
        public NutrutionFacts build() {
            return new NutrutionFacts(this);
        }
    }

    // 营养成分的getter方法
    public int getServingSize() {
        return servingSize;
    }

    public int getServings() {
        return servings;
    }

    public int getCalories() {
        return calories;
    }

    public int getFat() {
        return fat;
    }

    public int getSodium() {
        return sodium;
    }

    public int getCarbohydrate() {
        return carbohydrate;
    }

    // 参数有效性检查方法
    private static void validateServingSize(int servingSize) {
        if (servingSize <= 0) {
            throw new IllegalArgumentException("Serving size must be greater than 0");
        }
    }

    private static void validateServings(int servings) {
        if (servings <= 0) {
            throw new IllegalArgumentException("Number of servings must be greater than 0");
        }
    }

    private static void validateCalories(int calories) {
        if (calories < 0) {
            throw new IllegalArgumentException("Calories must be non-negative");
        }
    }

    private static void validateFat(int fat) {
        if (fat < 0) {
            throw new IllegalArgumentException("Fat must be non-negative");
        }
    }

    private static void validateSodium(int sodium) {
        if (sodium < 0) {
            throw new IllegalArgumentException("Sodium must be non-negative");
        }
    }

    private static void validateCarbohydrate(int carbohydrate) {
        if (carbohydrate < 0) {
            throw new IllegalArgumentException("Carbohydrate must be non-negative");
        }
    }
}
  • 方法使用
public class Main {
    public static void main(String[] args) {
        try {
            NutrutionFacts coke = new NutrutionFacts.Builder(1, 2)
                    .setFat(-1) // 无效参数
                    .setCalories(1)
                    .build();

            System.out.println("Serving Size: " + coke.getServingSize());
            System.out.println("Servings: " + coke.getServings());
            System.out.println("Calories: " + coke.getCalories());
            System.out.println("Fat: " + coke.getFat());
            System.out.println("Sodium: " + coke.getSodium());
            System.out.println("Carbohydrate: " + coke.getCarbohydrate());
        } catch (IllegalArgumentException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}

三、问题描述

上面已经讲解了建造者模式的使用方法,解决方案里面主要讲实践例子
本人已经加上注释方便理解

重叠构造器缺点如下:

  • 客户端代码会很难编写,并且仍然较难以阅读
  • 如果客户端不小心颠倒了参数的顺序,编译器也不会出错,但是程序在运行时会出现错误的行

JavaBean模式缺点如下:

  • 在构造过程中JavaBean可能处于不一致的状态
  • JavaBeans模式使得把类做成不可变的可能性不复存在

如学习内容所阐述的建造者模式保证了重叠构造器的安全性又实现了JavaBean模式的可读性


四、解决方案:

4.1 类层次结构

同样省略了有效性检查,这些后面几条的之后会合并阐述
上面也举例了有效性检查可以自己适当添加

类层次结构

  • 抽象类 Pizza:这个类可以定义比萨的通用特性,比如大小、配料等。
  • 具体类 Calzone:这些类继承自 Pizza,并实现具体的特性。
  • 抽象Builder:这个类定义了创建比萨的方法,比如配料等。
  • 具体Builder:这些类继承自 Builder,实现了构建各自具体比萨的方法。

4.2 父类

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

  • Pizza.Builder的类型是泛型,带有一个递归类型参数,(详见第30条)。它和抽象的 self 方法一样,允许在子类中适当地进行方法链接,不需要转换类型

上面那句话的内容进行简要解释

self() 方法的作用:

  • self() 方法返回当前 Builder 实例的引用,以支持链式调用。
  • 它确保了类型安全,并允许方法链的连续调用。

具体实现:

  • NyPizza.Builder 继承自 Pizza.Builder<Builder>
  • NyPizza.Builder 实现了 self() 方法,返回当前 Builder 实例。

addTopping 方法中的调用:

  • 在 addTopping 方法中,每次添加配料后返回当前 Builder 实例,使得可以继续调用其他方法。
  • addTopping 方法返回 self() 方法的结果,即当前 Builder 实例。
package org.example.entity;

import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

/**
 * 披萨类,用于表示一种披萨及其配料
 */
public class Pizza {
    // 披萨的配料集合,使用Set集合避免重复
    final Set<Topping> toppings;

    /**
     * 披萨的配料枚举,列出了可用的配料选项
     */
    public enum Topping {火腿, 奶酪, 香肠, 蔬菜}

    /**
     * 披萨的构造器类,用于构建披萨对象
     * 使用泛型设计,允许子类在构建过程中修改披萨对象
     */
    public abstract static class Builder<T extends Builder<T>> {
        // 使用EnumSet存储配料,提高效率
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

        /**
         * 向披萨中添加一种配料
         * @param topping 要添加的配料,不能为空
         * @return 返回Builder的实例,支持链式调用
         */
        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        /**
         * 构建并返回一个披萨对象
         * @return 返回构建完成的披萨对象
         */
        abstract Pizza build();

        /**
         * 返回当前Builder的实例,用于链式调用
         * @return 返回Builder的实例
         */
        protected abstract T self();
    }

    /**
     * 构造披萨对象
     * @param builder Builder对象,用于初始化披萨的配料
     */
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }
}

4.3 子类

  • 这里有两个具体的Pizza子类,其中一个表示经典纽约风味的比萨,另一个表示馅料 内置的半月型(calzone)比萨。

  • 前者需要一个尺寸参数,后者则要你指定酱汁应该内置还是外置

package org.example.entity;

import java.util.Objects;

/**
 * 纽约风格披萨类,继承自Pizza基类
 */
public class NyPizza extends Pizza {
    /**
     * 披萨尺寸枚举类,定义了小、中、大三种尺寸
     */
    public enum Size {,,}

    /**
     * 披萨的尺寸,一经设定不可更改
     */
    private final Size size;

    /**
     * NyPizza的构造器类,继承自Pizza.Builder<Builder>
     */
    public static class Builder extends Pizza.Builder<Builder> {
        /**
         * 披萨的尺寸,用于构建NyPizza实例时指定
         */
        private final Size size;

        /**
         * 构造函数,初始化披萨尺寸
         *
         * @param size 披萨的尺寸,不能为空
         */
        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        /**
         * 使用当前Builder实例配置构建并返回一个NyPizza实例
         *
         * @return 根据Builder实例配置创建的NyPizza对象
         */
        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        /**
         * 用于在递归调用或链式调用中保持当前Builder实例的类型安全
         *
         * @return 当前Builder实例,以继续链式调用
         */
        @Override
        protected Builder self() {
            return this;
        }
    }

    /**
     * 构造函数,用于实例化NyPizza对象
     *
     * @param builder NyPizza的Builder实例,包含所有构建所需的配置
     */
    private NyPizza(Builder builder) {
        super(builder); // 调用父类构造函数
        size = builder.size; // 从Builder中复制披萨尺寸
    }
}
  • 注意,每个子类的构建器中的buila方法,都声明返回正确的子类:NyPizza.Builder
    的build方法返回NyPizza,而Calzone.Builder中的则返回Calzone。

  • 在该方法中,子类方法声明返回超级类声明的返回类型的子类型,这被称作协变返回类型 。它允许客户端无须转换类型就能使用这些构建器。

package org.example.entity;

/**
 * Calzone类继承自Pizza类,代表一种特定的披萨类型 - 卡尔索恩披萨
 * 卡尔索恩披萨的特色之一是酱料可能被包裹在内部
 */
public class Calzone extends Pizza {
    // 指示酱料是否在披萨内部的标志
    private final boolean sauceInside;

    /**
     * Calzone的构造方法,使用Builder模式进行构建
     * @param builder 用于构建Calzone实例的Builder对象
     */
    private Calzone(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }

    /**
     * Builder类用于构建Calzone实例,继承自Pizza.Builder<Builder>
     * 该类提供了灵活的API来设置Calzone的属性
     */
    public static class Builder extends Pizza.Builder<Builder> {
        // 默认酱料不在内部的标志
        private boolean sauceInside = false;

        /**
         * 设置酱料在披萨内部的方法
         * @return 返回Builder实例,以支持链式调用
         */
        public Builder sauceInside() {
            sauceInside = true;
            return this;
        }

        /**
         * 构建并返回一个新的Calzone实例
         * @return 新建的Calzone实例
         */
        @Override
        public Calzone build() {
            return new Calzone(this);
        }

        /**
         * 返回当前Builder实例,用于Builder内部方法的类型安全
         * @return 当前Builder实例
         */
        @Override
        protected Builder self() {
            return this;
        }
    }
}

实现类

package org.example;

import org.example.entity.Calzone;
import org.example.entity.NutrutionFacts;
import org.example.entity.NyPizza;
import org.example.entity.Pizza;

public class Main {
    public static void main(String[] args) {
        // 创建一个NutrutionFacts对象来记录可乐的营养信息
        NutrutionFacts coke = new NutrutionFacts.Builder(1,2)
                .setFat(1)
                .setCalories(1)
                .build();
        // 打印可乐的卡路里信息
        System.out.println("卡路里: " + coke.getCalories());
        // 创建一个纽约风格的披萨对象,并添加火腿和奶酪作为配料
        NyPizza pizza = new NyPizza.Builder(NyPizza.Size.)
                .addTopping(Pizza.Topping.火腿)
                .addTopping(Pizza.Topping.奶酪)
                .build();
        // 创建一个Calzone对象,并添加蔬菜作为配料,且酱料在内部
        Calzone calzone = new Calzone.Builder()
                .addTopping(Pizza.Topping.蔬菜).sauceInside().build();

        // 打印披萨和Calzone的信息
        System.out.println("pizza: " + pizza);
        System.out.println("calzone: " + calzone);

    }
}

4.3 测试结果

  • 如果想显示内容可以自行实现toString方法

在这里插入图片描述


五、总结:

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

(后续有遇到问题再添加)


声明:如本内容中存在错误或不准确之处,欢迎指正。转载时请注明原作者信息(麻辣香蝈蝈)。

在这里插入图片描述

  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值