【Java基础】大家都在使用 @Builder ,我为什么建议你谨慎使用 @Builder

前言

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

场景复现

  • 先复现一下我开发中遇到的问题:
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 生成的构造器不是完美的,它不能区分哪些参数是必须的,哪些是可选的。如果没有提供必须的参数,构造器可能会创建出不完整或者不合法的对象。
less 复制代码
补充一点:很多人 喜欢 @Builder 和 @Data 搭配使用,包括我自己哈哈哈,这样会导致生成的构造器是可变的,这违反了构造器原理的,构造器应该是不可变的。
因此建议  @Builder 使用在一些不可变的对象中。
  • @Builder 生成的构造器不能处理抽象类型的参数,它只能接受具体类型的对象,限制了灵活性和拓展性。
  • 使用不当很容易报错,增加了使用的复杂性。
sql 复制代码
- 继承关系时,子类需要使用 @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 一把梭(包括我自己),但只有不断尝试、总结、尝试改变,才能成为更优秀的自己,水滴总有汇聚成大海的一天。

最后

  • 如果文章对你有帮助,请点赞、收藏、关注、谢谢大家。
相关推荐
九圣残炎4 分钟前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge6 分钟前
Netty篇(入门编程)
java·linux·服务器
LunarCod12 分钟前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
Re.不晚34 分钟前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐39 分钟前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
sszmvb123440 分钟前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
码农派大星。43 分钟前
Spring Boot 配置文件
java·spring boot·后端
测试杂货铺1 小时前
外包干了2年,快要废了。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
王佑辉1 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
顾北川_野1 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java