前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >大家都在使用 @Builder ,我为什么建议你谨慎使用 @Builder

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

原创
作者头像
Lorin 洛林
发布2023-11-10 22:01:30
发布2023-11-10 22:01:30
6330
举报
文章被收录于专栏:Java 技术小屋Java 技术小屋

前言

  • 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 会暴露所有字段的访问和修改。

个人简介

👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.

🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。

🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。

💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。

🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。

📖 保持关注我的博客,让我们共同追求技术卓越。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 场景复现
  • 解决问题
  • 我为什么建议你谨慎不使用 @Builder
    • @Builder 的不足
    • @Builder 适用的场景
  • 不使用 @Builder 我们如何实现对象构造
    • @Data + final 实现字段必填
    • 升级版:使用 @Accessors 实现链式构造 + final 实现字段必填
      • @Accessors 的定义
      • 使用示例
    • 实现原理
  • 总结
  • 个人简介
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档