大家都在使用 @Builder ,我为什么建议你谨慎使用 @Builder

2023-11-10 22:01:30 浏览数 (1)

前言

  • hello,大家好,我是你们的老朋友 Lorin,本周在开发中使用 @Builder (@Builder 是一个注解,通常与 Lombok 这种 Java 代码生成工具一起使用,可以帮助简化 Java 类的构建器模式(Builder Pattern)的使用和生成)的时候出现了一个默认值丢失事件,顺便借这个机会研究了一下 @Builder ,特此分享给大家,先说结论:建议在日常开发中谨慎使用 @Builder,为什么呢?废话不多说,发车:
认真听课.png认真听课.png

场景复现

  • 先复现一下我开发中遇到的问题:
代码语言:Java复制
@Builder
@Getter
class Student implements {
    private String num;

    private String name;

    private String address = "default";
}

public class TestExample {
    public static void main(String[] args) {
        Student student = Student.builder().name("Student").num("123").build();
        System.out.println(JSON.toJSONString(student));
    }
}

{"name":"Student","num":"123"}
  • 我们会发现我们设置的默认值 address = "default" 丢失了,我大感疑惑,因为我也是第一次遇到,打开编译生成的 class 文件一看豁然开朗:
代码语言:Java复制
class Student  {
    private String num;
    private String name;
    private String address = "default";

    Student(String num, String name, String address) {
        this.num = num;
        this.name = name;
        this.address = address;
    }

    // 生成的 StudentBuilder 对象把我们的默认值丢失了
    public static StudentBuilder builder() {
        return new StudentBuilder();
    }

    public String getNum() {
        return this.num;
    }

    public String getName() {
        return this.name;
    }

    public String getAddress() {
        return this.address;
    }

    public static class StudentBuilder {
        private String num;
        private String name;
        private String address;

        StudentBuilder() {
        }

        public StudentBuilder num(String num) {
            this.num = num;
            return this;
        }

        public StudentBuilder name(String name) {
            this.name = name;
            return this;
        }

        public StudentBuilder address(String address) {
            this.address = address;
            return this;
        }

        public Student build() {
            return new Student(this.num, this.name, this.address);
        }

        public String toString() {
            return "Student.StudentBuilder(num="   this.num   ", name="   this.name   ", address="   this.address   ")";
        }
    }
}

解决问题

  • 知道原因后解决当然很简单,lombok 提供了 @Builder.Default 来设置默认值:
代码语言:Java复制
@Builder
@Getter
class Student {
    private String num;

    private String name;

    @Builder.Default
    private String address = "default";
}

public class TestExample {
    public static void main(String[] args) {
        Student student = Student.builder().name("Student").num("123").build();
        System.out.println(JSON.toJSONString(student));
    }
}


{"address":"default","name":"Student","num":"123"}

// 来看看 @Builder.Default 是怎么实现的:
class Student {
    private String num;
    private String name;
    private String address;

    private static String $default$address() {
        return "default";
    }

    Student(String num, String name, String address) {
        this.num = num;
        this.name = name;
        this.address = address;
    }

    public static StudentBuilder builder() {
        return new StudentBuilder();
    }

    public String getNum() {
        return this.num;
    }

    public String getName() {
        return this.name;
    }

    public String getAddress() {
        return this.address;
    }

    public static class StudentBuilder {
        private String num;
        private String name;
        private boolean address$set;
        private String address$value;

        StudentBuilder() {
        }

        public StudentBuilder num(String num) {
            this.num = num;
            return this;
        }

        public StudentBuilder name(String name) {
            this.name = name;
            return this;
        }

        public StudentBuilder address(String address) {
            this.address$value = address;
            this.address$set = true;
            return this;
        }

        public Student build() {
            String address$value = this.address$value;
            // 如果没有设置该属性则使用默认值
            if (!this.address$set) {
                address$value = Student.$default$address();
            }

            return new Student(this.num, this.name, address$value);
        }

        public String toString() {
            return "Student.StudentBuilder(num="   this.num   ", name="   this.name   ", address$value="   this.address$value   ")";
        }
    }
}

我为什么建议你谨慎不使用 @Builder

  • 上面的问题只要知道原理就很好的解决了,那我为什么还建议不使用 @Builder 呢?我们都知道 @Builder 可以简化我们代码的生成,让我们轻松的使用构造器。但 @Builder 同样有很多的不足。

@Builder 的不足

  • @Builder 生成的构造器不是完美的,它不能区分哪些参数是必须的,哪些是可选的。如果没有提供必须的参数,构造器可能会创建出不完整或者不合法的对象。
代码语言:txt复制
补充一点:很多人 喜欢 @Builder 和 @Data 搭配使用,包括我自己哈哈哈,这样会导致生成的构造器是可变的,这违反了构造器原理的,构造器应该是不可变的。
因此建议  @Builder 使用在一些不可变的对象中。
  • @Builder 生成的构造器不能处理抽象类型的参数,它只能接受具体类型的对象,限制了灵活性和拓展性。
  • 使用不当很容易报错,增加了使用的复杂性。
代码语言:txt复制
- 继承关系时,子类需要使用 @SuperBuilder
- 设置默认值需要使用 @Builder.Default
  • 需要额外创建 Builder 对象。

@Builder 适用的场景

  • 从上面我们可以看出,@Builder 不适合使用在短暂对象上,而是应该使用在长期、固定不变的对象上。

不使用 @Builder 我们如何实现对象构造

  • 下面只是一些可以参考的思路,在实际开发中大家可以根据根据自己的需求灵活运用。

@Data final 实现字段必填

  • 下面是一个简单的示例:
代码语言:Java复制
@Data
class Student {
    /**
     * 设置为 final 构造必填
     */
    private final String num;

    /**
     * 设置为 final 构造必填
     */
    private final String name;

    private int age;

    private String address = "default";

    Student(String num, String name) {
        this.num = num;
        this.name = name;
    }
}
public class TestExample {
    public static void main(String[] args) {
        Student student = new Student("123", "小明");
        student.setAge(12);
        System.out.println(student);
    }
}

升级版:使用 @Accessors 实现链式构造 final 实现字段必填

  • 上面的方法我们发现无法实现类似 @Builder 的链式构造,我们可以结合 @Accessors 实现链式构造
  • @Accessors 是 Lombok 提供的一个注解,用于配置生成的 getter 和 setter 方法的样式和命名方式。

@Accessors 的定义

代码语言:Java复制
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Accessors {
    // 与chain=true类似,区别在于getter和setter不带set和get前缀
    boolean fluent() default false;

    // 设置chain=true,生成setter方法返回对象,代替了默认的返回void
    boolean chain() default false;

    // set、get方法忽略指定的前缀(驼峰式) 
    // 注:必须所有字段都有前缀
    String[] prefix() default {};
}

使用示例

代码语言:Java复制
// fluent = true 日常开发中我们一般使用这个配置就行
@Data
@Accessors(fluent = true)
class Student {
    /**
     * 设置为 final 构造必填
     */
    private final String num;

    /**
     * 设置为 final 构造必填
     */
    private final String name;

    private int age;

    private String address = "default";

    public Student(String num, String name) {
        this.num = num;
        this.name = name;
    }
}

public class TestExample {
    public static void main(String[] args) {
        Student student = new Student("123", "小明").age(1).address("北京");
        System.out.println(JSON.toJSONString(student));
    }
}

{"address":"北京","age":1,"name":"小明","num":"123"}

// chain = true
@Data
@Accessors(chain = true)
class Student {
    /**
     * 设置为 final 构造必填
     */
    private final String num;

    /**
     * 设置为 final 构造必填
     */
    private final String name;

    private int age;

    private String address = "default";

    public Student(String num, String name) {
        this.num = num;
        this.name = name;
    }
}

public class TestExample {
    public static void main(String[] args) {
        Student student = new Student("123", "小明").setAge(1);
        System.out.println(JSON.toJSONString(student));
    }
}

{"address":"default","age":1,"name":"小明","num":"123"}

// prefix = "prefix"
@Data
@Accessors(chain = true,prefix = "prefix")
class Student {
    /**
     * 设置为 final 构造必填
     */
    private final String prefixNum;

    /**
     * 设置为 final 构造必填
     */
    private final String prefixName;

    private int prefixAge;

    private String prefixAddress = "default";

    public Student(String prefixNum, String prefixName) {
        this.prefixNum = prefixNum;
        this.prefixName = prefixName;
    }
}

public class TestExample {
    public static void main(String[] args) {
        Student student = new Student("123", "小明").setAge(1).setAddress("北京");
        System.out.println(JSON.toJSONString(student));
    }
}

{"address":"北京","age":1,"name":"小明","num":"123"}

实现原理

  • 同样我们可以查看编译后的 class 文件看一下背后是如何实现的(以上面的案例三为例):
代码语言:Java复制
class Student {
    private final String prefixNum;
    private final String prefixName;
    private int prefixAge;
    private String prefixAddress = "default";

    public Student(String prefixNum, String prefixName) {
        this.prefixNum = prefixNum;
        this.prefixName = prefixName;
    }

    public String getNum() {
        return this.prefixNum;
    }

    public String getName() {
        return this.prefixName;
    }

    public int getAge() {
        return this.prefixAge;
    }

    public String getAddress() {
        return this.prefixAddress;
    }

    public Student setAge(int prefixAge) {
        this.prefixAge = prefixAge;
        return this;
    }

    public Student setAddress(String prefixAddress) {
        this.prefixAddress = prefixAddress;
        return this;
    }
    // 省略一些其他方法
}

总结

  • @Builder 是一个好用的工具,但是我们不能滥用。在构建一些长期、固定不可变的对象时我们可以适当使用 @Builder 进行构建;当构建一些短暂存活的对象时我们可以尝试 使用 @Accessors 实现链式构造 final 实现字段必填 的方式。
  • 也许大多数朋友其实在日常开发中都是 @Builder 和 @Data 一把梭(包括我自己),但只有不断尝试、总结、尝试改变,才能成为更优秀的自己,水滴总有汇聚成大海的一天。
  • 补充一点:@Builder 我们可以用上面的的两种方式进行替代,在一些字段不可变的场景你甚至可以使用 @Getter @Setter 进行细化处理字段,毕竟 @Data 会暴露所有字段的访问和修改。

个人简介

0 人点赞