Java 中的通用 Fluent Builder

2024-03-02

我知道也有过类似的问题。但我还没有看到我的问题的答案。

我将用一些简化的代码来展示我想要的东西。假设我有一个复杂的对象,它的一些值是通用的:

public static class SomeObject<T, S> {
    public int number;
    public T singleGeneric;
    public List<S> listGeneric;

    public SomeObject(int number, T singleGeneric, List<S> listGeneric) {
        this.number = number;
        this.singleGeneric = singleGeneric;
        this.listGeneric = listGeneric;
    }
}

我想用流畅的 Builder 语法来构造它。但我想让它变得优雅。我希望它像这样工作:

SomeObject<String, Integer> works = new Builder() // not generic yet!
    .withNumber(4) 

    // and only here we get "lifted"; 
    // since now it's set on the Integer type for the list
    .withList(new ArrayList<Integer>()) 

    // and the decision to go with String type for the single value
    // is made here:
    .withTyped("something") 

    // we've gathered all the type info along the way
    .create();

没有不安全的强制转换警告,也不需要预先指定泛型类型(在顶部,构造 Builder 的地方)。

相反,我们让类型信息显式地沿着链进一步流动 - 以及withList and withTyped calls.

现在,实现它的最优雅的方式是什么?

我知道最常见的技巧,例如使用递归泛型 https://vyazelenko.com/2012/03/02/recursive-generics-to-the-rescue/,但我玩了一段时间,无法弄清楚它如何应用于这个用例。

下面是一个普通的冗长的解决方案,它在满足所有要求的意义上起作用,但代价是非常冗长 - 它引入了四个构建器(在继承方面不相关),代表四种可能的组合T and S类型是否已定义。

它确实有效,但是这并不是一个值得骄傲的版本,如果我们期望更多的通用参数而不是两个,那么它就无法维护。

public static class Builder  {
    private int number;

    public Builder withNumber(int number) {
        this.number = number;
        return this;
    }

    public <T> TypedBuilder<T> withTyped(T t) {
        return new TypedBuilder<T>()
                .withNumber(this.number)
                .withTyped(t);
    }

    public <S> TypedListBuilder<S> withList(List<S> list) {
        return new TypedListBuilder<S>()
                .withNumber(number)
                .withList(list);
    }
}

public static class TypedListBuilder<S> {
    private int number;
    private List<S> list;

    public TypedListBuilder<S> withList(List<S> list) {
        this.list = list;
        return this;
    }

    public <T> TypedBothBuilder<T, S> withTyped(T t) {
        return new TypedBothBuilder<T, S>()
                .withList(list)
                .withNumber(number)
                .withTyped(t);
    }

    public TypedListBuilder<S> withNumber(int number) {
        this.number = number;
        return this;
    }
}

public static class TypedBothBuilder<T, S> {
    private int number;
    private List<S> list;
    private T typed;

    public TypedBothBuilder<T, S> withList(List<S> list) {
        this.list = list;
        return this;
    }

    public TypedBothBuilder<T, S> withTyped(T t) {
        this.typed = t;
        return this;
    }

    public TypedBothBuilder<T, S> withNumber(int number) {
        this.number = number;
        return this;
    }

    public SomeObject<T, S> create() {
        return new SomeObject<>(number, typed, list);
    }
}

public static class TypedBuilder<T> {
    private int number;
    private T typed;

    private Builder builder = new Builder();

    public TypedBuilder<T> withNumber(int value) {
        this.number = value;
        return this;
    }

    public TypedBuilder<T> withTyped(T t) {
        typed = t;
        return this;
    }

    public <S> TypedBothBuilder<T, S> withList(List<S> list) {
        return new TypedBothBuilder<T, S>()
                .withNumber(number)
                .withTyped(typed)
                .withList(list);
    }
}

我可以应用更聪明的技术吗?


好的,更传统的步骤构建器方法是这样的。

不幸的是,因为我们混合了泛型和非泛型方法,所以我们必须重新声明很多方法。我不think有一个很好的方法可以解决这个问题。

基本思想就是:在接口上定义每个步骤,然后在私有类上实现它们。我们可以通过继承原始类型来使用通用接口来做到这一点。它很丑,但很有效。

public interface NumberStep {
    NumberStep withNumber(int number);
}
public interface NeitherDoneStep extends NumberStep {
    @Override NeitherDoneStep withNumber(int number);
    <T> TypeDoneStep<T> withTyped(T type);
    <S> ListDoneStep<S> withList(List<S> list);
}
public interface TypeDoneStep<T> extends NumberStep {
    @Override TypeDoneStep<T> withNumber(int number);
    TypeDoneStep<T> withTyped(T type);
    <S> BothDoneStep<T, S> withList(List<S> list);
}
public interface ListDoneStep<S> extends NumberStep {
    @Override ListDoneStep<S> withNumber(int number);
    <T> BothDoneStep<T, S> withTyped(T type);
    ListDoneStep<S> withList(List<S> list);
}
public interface BothDoneStep<T, S> extends NumberStep {
    @Override BothDoneStep<T, S> withNumber(int number);
    BothDoneStep<T, S> withTyped(T type);
    BothDoneStep<T, S> withList(List<S> list);
    SomeObject<T, S> create();
}
@SuppressWarnings({"rawtypes","unchecked"})
private static final class BuilderImpl implements NeitherDoneStep, TypeDoneStep, ListDoneStep, BothDoneStep {
    private final int number;
    private final Object typed;
    private final List list;

    private BuilderImpl(int number, Object typed, List list) {
        this.number = number;
        this.typed  = typed;
        this.list   = list;
    }

    @Override
    public BuilderImpl withNumber(int number) {
        return new BuilderImpl(number, this.typed, this.list);
    }

    @Override
    public BuilderImpl withTyped(Object typed) {
        // we could return 'this' at the risk of heap pollution
        return new BuilderImpl(this.number, typed, this.list);
    }

    @Override
    public BuilderImpl withList(List list) {
        // we could return 'this' at the risk of heap pollution
        return new BuilderImpl(this.number, this.typed, list);
    }

    @Override
    public SomeObject create() {
        return new SomeObject(number, typed, list);
    }
}

// static factory
public static NeitherDoneStep builder() {
    return new BuilderImpl(0, null, null);
}

由于我们不希望人们访问丑陋的实现,因此我们将其设为私有,并让每个人都经历一个static method.

否则它的工作原理与您自己的想法几乎相同:

SomeObject<String, Integer> works =
    SomeObject.builder()
        .withNumber(4)
        .withList(new ArrayList<Integer>())
        .withTyped("something")
        .create();

// we could return 'this' at the risk of heap pollution

这是关于什么的?好吧,这里有一个一般性的问题,如下所示:

NeitherDoneStep step = SomeObject.builder();
BothDoneStep<String, Integer> both =
    step.withTyped("abc")
        .withList(Arrays.asList(123));
// setting 'typed' to an Integer when
// we already set it to a String
step.withTyped(123);
SomeObject<String, Integer> oops = both.create();

如果我们不创建副本,我们现在就会有123伪装成String.

(如果您仅使用构建器作为流畅的调用集,则不会发生这种情况。)

虽然我们不需要复制withNumber,我只是采取了额外的步骤并使构建器不可变。我们创建的对象超出了我们需要的数量,但确实没有其他好的解决方案。如果每个人都将以正确的方式使用构建器,那么我们可以使其可变并且return this.


由于我们对新颖的通用解决方案感兴趣,因此这里是单个类中的构建器实现。

这里的区别在于我们不保留类型typed and list如果我们第二次调用它们中的任何一个设置器。这本身并不是一个真正的缺点,我想只是有所不同。这意味着我们可以这样做:

SomeObject<Long, String> =
    SomeObject.builder()
        .withType( new Integer(1) )
        .withList( Arrays.asList("abc","def") )
        .withType( new Long(1L) ) // <-- changing T here
        .create();
public static class OneBuilder<T, S> {
    private final int number;
    private final T typed;
    private final List<S> list;

    private OneBuilder(int number, T typed, List<S> list) {
        this.number = number;
        this.typed  = typed;
        this.list   = list;
    }

    public OneBuilder<T, S> withNumber(int number) {
        return new OneBuilder<T, S>(number, this.typed, this.list);
    }

    public <TR> OneBuilder<TR, S> withTyped(TR typed) {
        // we could return 'this' at the risk of heap pollution
        return new OneBuilder<TR, S>(this.number, typed, this.list);
    }

    public <SR> OneBuilder<T, SR> withList(List<SR> list) {
        // we could return 'this' at the risk of heap pollution
        return new OneBuilder<T, SR>(this.number, this.typed, list);
    }

    public SomeObject<T, S> create() {
        return new SomeObject<T, S>(number, typed, list);
    }
}

// As a side note,
// we could return e.g. <?, ?> here if we wanted to restrict
// the return type of create() in the case that somebody
// calls it immediately.
// The type arguments we specify here are just whatever
// we want create() to return before withTyped(...) and
// withList(...) are each called at least once.
public static OneBuilder<Object, Object> builder() {
    return new OneBuilder<Object, Object>(0, null, null);
}

创建副本和堆污染也是如此。


现在我们得到really小说。这里的想法是,我们可以通过引起捕获转换错误来“禁用”每个方法。

解释起来有点复杂,但基本思想是:

  • 每个方法在某种程度上都依赖于在类上声明的类型变量。
  • 通过将其返回类型将该类型变量设置为“禁用”该方法?.
  • 如果我们尝试调用该返回值的方法,这会导致捕获转换错误。

此示例与上一个示例的区别在于,如果我们尝试第二次调用 setter,我们将收到编译器错误:

SomeObject<Long, String> =
    SomeObject.builder()
        .withType( new Integer(1) )
        .withList( Arrays.asList("abc","def") )
        .withType( new Long(1L) ) // <-- compiler error here
        .create();

因此,我们只能调用每个 setter 一次。

这里的两个主要缺点是:

  • 不能再次调用 setter合法的 reasons
  • and can第二次调用设置者null文字。

我认为这是一个非常有趣的概念验证,即使它有点不切实际。

public static class OneBuilder<T, S, TCAP, SCAP> {
    private final int number;
    private final T typed;
    private final List<S> list;

    private OneBuilder(int number, T typed, List<S> list) {
        this.number = number;
        this.typed  = typed;
        this.list   = list;
    }

    public OneBuilder<T, S, TCAP, SCAP> withNumber(int number) {
        return new OneBuilder<T, S, TCAP, SCAP>(number, this.typed, this.list);
    }

    public <TR extends TCAP> OneBuilder<TR, S, ?, SCAP> withTyped(TR typed) {
        // we could return 'this' at the risk of heap pollution
        return new OneBuilder<TR, S, TCAP, SCAP>(this.number, typed, this.list);
    }

    public <SR extends SCAP> OneBuilder<T, SR, TCAP, ?> withList(List<SR> list) {
        // we could return 'this' at the risk of heap pollution
        return new OneBuilder<T, SR, TCAP, SCAP>(this.number, this.typed, list);
    }

    public SomeObject<T, S> create() {
        return new SomeObject<T, S>(number, typed, list);
    }
}

// Same thing as the previous example,
// we could return <?, ?, Object, Object> if we wanted
// to restrict the return type of create() in the case
// that someone called it immediately.
// (The type arguments to TCAP and SCAP should stay
// Object because they are the initial bound of TR and SR.)
public static OneBuilder<Object, Object, Object, Object> builder() {
    return new OneBuilder<Object, Object, Object, Object>(0, null, null);
}

同样,创建副本和堆污染也是如此。


不管怎样,我希望这能给你一些想法,让你认真思考。 :)

如果你对这类事情普遍感兴趣,我建议学习带注释处理的代码生成 https://deors.wordpress.com/2011/10/08/annotation-processors/,因为你可以比手工编写更容易生成这样的东西。正如我们在评论中谈到的,手动编写这样的东西很快就会变得不切实际。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java 中的通用 Fluent Builder 的相关文章

  • 按下按钮并在java中的新窗口中打开文件

    我创建了一个 JFrame 并放置了一个文本字段和按钮 在文本字段中我放置了从文本文件读取的名称 我知道我想单击按钮并打开一个已知窗口 我想在其中放置名称 其他信息来自同一个文件 这是我的代码 这是我的主框架 package Fronten
  • 如何在java中将数组值排序为循环格式?

    我的数组值如下 String value 1 2 3 4 5 6 7 8 9 10 假设如果我将值 5 传递给 tat 数组 它应该按如下顺序排序 5 6 7 8 9 10 1 2 3 4 怎么办 有人帮忙吗 感谢你 你需要的就是所谓的轮换
  • 两个整数乘积的模

    我必须找到c c a b mod m a b c m 是 32 位整数 但 a b 可以超过 32 位 我正在尝试找出一种计算 c 的方法 而不使用 long 或任何 gt 32 位的数据类型 有任何想法吗 如果m是质数 事情可以简化吗 注
  • 与 Eclipse 中的 Java Content Assist 交互

    作为我的插件项目的一部分 我正在考虑与 Eclipse 在 Java 文件上显示的内容辅助列表进行交互 我正在尝试根据一些外部数据对列表进行重新排序 我看过一些有关创建新内容辅助的教程 但没有看到有关更改现有内容辅助的教程 这可能吗 如果是
  • eclipse行号状态行贡献项是如何实现的?

    我需要更新状态行编辑器特定的信息 我已经有了自己的实现 但我想看看 eclipse 贡献项是如何实现的 它显示状态行中的行号 列位置 谁能指点一下 哪里可以找到源代码 提前致谢 亚历克斯 G 我一直在研究它 它非常复杂 我不确定我是否了解完
  • 如何在 JPQL 或 HQL 中进行限制查询?

    在 Hibernate 3 中 有没有办法在 HQL 中执行相当于以下 MySQL 限制的操作 select from a table order by a table column desc limit 0 20 如果可能的话 我不想使用
  • 如何检查某个元素是否存在于一组项目中?

    In an ifJava中的语句如何检查一个对象是否存在于一组项目中 例如 在这种情况下 我需要验证水果是苹果 橙子还是香蕉 if fruitname in APPLE ORANGES GRAPES Do something 这是一件非常微
  • Java 中如何将 char 转换为 int? [复制]

    这个问题在这里已经有答案了 我是Java编程新手 我有例如 char x 9 我需要得到撇号中的数字 即数字 9 本身 我尝试执行以下操作 char x 9 int y int x 但没有成功 那么我应该怎么做才能得到撇号中的数字呢 ASC
  • 将非 Android 项目添加到 Android 项目

    我在 Eclipse 中有三个项目 Base Server 和 AndroidClient Base和Server是Java 1 7项目 而AndroidClient显然是一个android项目 基础项目具有在服务器和 Android 客户
  • Android 无法解析日期异常

    当尝试解析发送到我的 Android 客户端的日期字符串时 我得到一个无法解析的日期 这是例外 java text ParseException 无法解析的日期 2018 09 18T00 00 00Z 位于 偏移量 19 在 java t
  • 如何在字段值无效的情况下更改 Struts2 验证错误消息?

    我在 Web 表单上使用 Struts2 验证 如果字段假设为整数或日期 则
  • 如何仅从 Firestore 获取最新更新的数据?

    在 Firestore 上发现任何更改时始终获取整个文档 如何只获取最近更新的数据 这是我的数据 我需要在第一次加载时在聊天中按对象顺序 例如 2018 09 17 30 40 msg和sendby 并且如果数据更新则仅获取新的msg和se
  • 将人类日期(当地时间 GMT)转​​换为日期

    我正在服务器上工作 服务器正在向我发送 GMT 本地日期的日期 例如Fri Jun 22 09 29 29 NPT 2018在字符串格式上 我将其转换为日期 如下所示 SimpleDateFormat simpleDateFormat ne
  • 如何将 HTML 链接放入电子邮件正文中?

    我有一个可以发送邮件的应用程序 用 Java 实现 我想在邮件中放置一个 HTML 链接 但该链接显示为普通字母 而不是 HTML 链接 我怎样才能将 HTML 链接放入字符串中 我需要特殊字符吗 太感谢了 Update 大家好你们好 感谢
  • Hibernate 本机查询 - char(3) 列

    我在 Oracle 中有一个表 其中列 SC CUR CODE 是 CHAR 3 当我做 Query q2 em createNativeQuery select sc cur code sc amount from sector cost
  • partitioningBy 必须生成一个包含 true 和 false 条目的映射吗?

    The 分区依据 https docs oracle com javase 8 docs api java util stream Collectors html partitioningBy java util function Pred
  • Android View Canvas onDraw 未执行

    我目前正在开发一个自定义视图 它在画布上绘制一些图块 这些图块是从多个文件加载的 并将在需要时加载 它们将由 AsyncTask 加载 如果它们已经加载 它们只会被绘制在画布上 这工作正常 如果加载了这些图片 AsyncTask 就会触发v
  • MiniDFSCluster UnsatisfiedLinkError org.apache.hadoop.io.nativeio.NativeIO$Windows.access0

    做时 new MiniDFSCluster Builder config build 我得到这个异常 java lang UnsatisfiedLinkError org apache hadoop io nativeio NativeIO
  • Java 11 - 将 Spring @PostConstruct 替换为 afterPropertiesSet 或使用 initMethod

    我正在使用 spring 应用程序 有时会使用 PostConstruct用于代码和测试中的设置 看来注释将被排除在外Java 11 https www baeldung com spring postconstruct predestro
  • 由 Servlet 容器提供服务的 WebSocket

    上周我研究了 WebSockets 并对如何使用 Java Servlet API 实现服务器端进行了一些思考 我没有花费太多时间 但在使用 Tomcat 进行一些测试时遇到了以下问题 如果不修补容器或至少对 HttpServletResp

随机推荐

  • 如何通过 Android API 关闭所有声音和振动

    我正在构建一个 Android 应用程序 我试图在应用程序启动时禁用设备的所有声音和振动 我是新手 所以我找不到如何做到这一点 任何想法 提前致谢 谢谢 我自己回复以完成答案 AudioManager aManager AudioManag
  • apache prefork/mod_wsgi 产生的进程计数似乎过去的配置[关闭]

    Closed 这个问题是无关 help closed questions 目前不接受答案 在生产环境中运行 nginx 反转回 apache mpm prefork mod wsgi 我看到90apache 子进程 当我期望 40 是最大值
  • Apache http 基本身份验证?

    是否有一些简单的代码可以添加到 htaccess文件或我的虚拟主机文件来强制执行 http 基本身份验证 那这个呢 AuthUserFile my derectory htpasswd Require valid user AuthName
  • HTML CSS DIV 面板

    I want to make something like this 到目前为止我所做的 谁能告诉我如何实现这一目标 谢谢 提前 See jsFiddle http jsfiddle net SanaBukhari DAFW9 代码如下 H
  • 每个 Android 应用程序的网络流量监控

    我想知道我是否可以对每个 Android 应用程序进行网络流量监控 基本上看看哪个应用程序正在接收 发送多少数据 我知道有很多应用程序已经做到了这一点 但我想知道如何做到这一点 嗯 你当然可以 一个非常简单的方法是使用交通统计 http d
  • PostgreSQL 和 ActiveRecord 子选择竞争条件

    我在 ActiveRecord 和 PostgreSQL 中遇到竞争条件 我正在读取一个值 然后递增它并插入一条新记录 num Foo where bar id 42 maximum number Foo create bar id 42
  • jedi-vim 和 YouCompleteMe 冲突吗?

    YouCompleteMe YCM 和 jedi vim 是两个提供自动补全功能的 vim 插件 并且都在 Python 下使用 jedi 不过 两者兼得是件好事 因为 jedi vim 除了文档之外还提供了一些有用的功能 例如 转到定义
  • 访客模式中的泛型杀伤力过大

    我正在开发一个项目 将十年前编写的旧 java 1 2 代码转换为 java 7 该项目大量 过度 使用特定访问者 为了使事情在概念上简单 我们假设访问者是这样的 public interface RobotVisitor public O
  • Google Chrome 调试器跳过断点

    我目前正在尝试在 chrome 中调试 js 脚本 我在脚本中放置了一个断点 当我仅使用一个选项卡时 它会正确中断 但在第二个选项卡上 即使我在代码中看到断点 它也不会中断 查看源代码时是否使用了漂亮的打印选项 我注意到格式化的副本可能会不
  • 有非官方的 Flash .FLA 规范吗?

    是否有非官方规范解释如何对 Flash FLA 文件进行逆向工程 我对创建一个应用程序特别感兴趣 该应用程序可以通过编程方式 自动场景规划 Flash 文档 从其他文件中提取内容 将该内容排列到图层中 而无需打开 Flash IDE 然后动
  • 使用 IB 将分段控件添加到导航栏

    我目前正在创建一个UISegmentedControl以编程方式在视图控制器中viewDidLoad方法并将其分配给视图控制器的导航栏self navigationItem titleView 这很简单 但我也希望能够在 Interface
  • 与 pypi.org 的连接超时

    无法 pip 安装 3rd 方软件包 venv pip install django Collecting django Retrying Retry total 4 connect None read None redirect None
  • 使用 R Lubridate 提取会计年度

    我将创建几个日期 library lubridate x lt ymd c 2012 03 26 2012 05 04 2012 09 23 2012 12 31 我可以从中提取年份和季度x values quarter x with ye
  • 如何创建文件选择器以使用 REST API 将文件保存到 Google Drive?

    现在谷歌是不赞成 https developers google com drive android deprecationGoogle Drive Android API 我正在将代码迁移到 Drive REST API 我几乎已经完成了
  • 如何正确使用WriteConsoleOutputAttribute函数

    为什么下面的代码 const std string text str HANDLE stdout handle GetStdHandle STD OUTPUT HANDLE COORD coords 0 0 DWORD written 0
  • 如何强制子div的高度为父div高度的100%而不指定父div的高度?

    我有一个具有以下结构的网站 div div div div div div div div div div 导航位于左侧 内容 div 位于右侧 内容div的信息是通过PHP拉入的 所以每次都是不同的 如何垂直缩放导航 使其高度与内容 di
  • 如何仅显示多类别绘图条形图的类别刻度并旋转它们

    我希望不使用plotly graph objects 在多类别条形图中显示内部类别标签 此外 我希望旋转外部类别标签的标签 这是我的代码 import pandas as pd import plotly graph objects as
  • R 中多列的聚合和加权平均值

    问题基本上是这样的 R 中的聚合和加权平均值 https stackoverflow com questions 3367190 aggregate and weighted mean in r 但我希望它使用 data table 在几列
  • 如何在 laravel 或 php 中动态更改 php dotenv (.env) 变量?

    我想要这样的东西 env APP ENV setenv APP ENV testing env APP ENV Output staging testing 我找到一个答案如何在 Laravel 中动态更改 env 文件中的变量 https
  • Java 中的通用 Fluent Builder

    我知道也有过类似的问题 但我还没有看到我的问题的答案 我将用一些简化的代码来展示我想要的东西 假设我有一个复杂的对象 它的一些值是通用的 public static class SomeObject