【问题标题】:What is a good way to allow only one non null field in an object什么是在对象中只允许一个非空字段的好方法
【发布时间】:2021-07-09 01:20:46
【问题描述】:

我想编写一个具有多个不同类型字段的类,但在任何时候,实例对象的一个​​且只有一个字段具有非空值。

到目前为止我所做的看起来并不干净。

class ExclusiveField {

    private BigInteger numericParam;
    private String stringParam;
    private LocalDateTime dateParam;

    public void setNumericParam(BigInteger numericParam) {
        unsetAll();
        this.numericParam = Objects.requireNonNull(numericParam);
    }

    public void setStringParam(String stringParam) {
        unsetAll();
        this.stringParam = Objects.requireNonNull(stringParam);
    }

    public void setDateParam(LocalDateTime dateParam) {
        unsetAll();
        this.dateParam = Objects.requireNonNull(dateParam);
    }

    private void unsetAll() {
        this.numericParam = null;
        this.stringParam = null;
        this.dateParam = null;
    }
}

Java 是否以某种方式支持这种模式,还是有更体面的方法来实现它?

【问题讨论】:

  • 1.如果您将空值传递给其中任何一个,all 字段将保留为空,这可能不是故意的 2. 您需要对象是可变的吗?提供 3 个不同的构造函数(或工厂方法,如果您愿意的话)会使这变得更加简洁,并保证每个可访问的 ExclusiveField 对象始终有效。
  • @HieuHT 但即便如此,最好不要在发生异常时更改状态。这个想法被称为failure atomicity
  • 这感觉像是一个破碎的要求。我非常非常好奇这个类实际上做了什么......我几乎从没想过一个类会有这样的行为。 What are you ultimately trying to accomplish?
  • 请注意,对于通过子类型更恰当地处理的情况,这是一种常见的反模式——您正在输入值,它们将使用哪个指针,也就是说,选择一个指针,每个类型价值是。
  • 我的意思是,您在概念上将非空值划分为不相交的类别/子类型/子类。我并不是说这样的划分标准一定是每个语言原始子类型或您已经拥有的某些子类。我说这可能是一个标准,应该由代码中的子类/继承来体现。问题是,您是否可以更好地使用单个指针,该指针将属于多个子类之一,而不是您当前为每个对象类别设计的单选按钮。

标签: java


【解决方案1】:

一个对象只有一个非null 字段的最简单方法是实际上只有一个字段并假设所有其他字段都是null 隐式。只需要另外一个标签字段,就可以判断哪个字段不是null

由于在您的示例中,所有替代方案似乎都与值的类型有关,类型本身可能是标记值,例如

class ExclusiveField {
    private Class<?> type;
    private Object value;

    private <T> void set(Class<T> t, T v) {
        value = Objects.requireNonNull(v);
        type = t;
    }
    private <T> T get(Class<T> t) {
        return type == t? t.cast(value): null;
    }

    public void setNumericParam(BigInteger numericParam) {
        set(BigInteger.class, numericParam);
    }

    public BigInteger getNumericParam() {
        return get(BigInteger.class);
    }

    public void setStringParam(String stringParam) {
        set(String.class, stringParam);
    }

    public String getStringParam() {
        return get(String.class);
    }

    public void setDateParam(LocalDateTime dateParam) {
        set(LocalDateTime.class, dateParam);
    }

    public LocalDateTime getDateParam() {
        return get(LocalDateTime.class);
    }
}

如果类型不是唯一的区别,您需要定义不同的键值。 enum 将是一个自然的选择,但不幸的是,enum 常量不能提供类型安全。所以,替代方案看起来像:

class ExclusiveField {
    private static final class Key<T> {
        static final Key<String>        STRING_PROPERTY_1 = new Key<>();
        static final Key<String>        STRING_PROPERTY_2 = new Key<>();
        static final Key<BigInteger>    BIGINT_PROPERTY   = new Key<>();
        static final Key<LocalDateTime> DATE_PROPERTY     = new Key<>();
    }
    private Key<?> type;
    private Object value;

    private <T> void set(Key<T> t, T v) {
        value = Objects.requireNonNull(v);
        type = t;
    }

    @SuppressWarnings("unchecked") // works if only set() and get() are used
    private <T> T get(Key<T> t) {
        return type == t? (T)value: null;
    }

    public void setNumericParam(BigInteger numericParam) {
        set(Key.BIGINT_PROPERTY, numericParam);
    }

    public BigInteger getNumericParam() {
        return get(Key.BIGINT_PROPERTY);
    }

    public void setString1Param(String stringParam) {
        set(Key.STRING_PROPERTY_1, stringParam);
    }

    public String getString1Param() {
        return get(Key.STRING_PROPERTY_1);
    }

    public void setString2Param(String stringParam) {
        set(Key.STRING_PROPERTY_2, stringParam);
    }

    public String getString2Param() {
        return get(Key.STRING_PROPERTY_2);
    }

    public void setDateParam(LocalDateTime dateParam) {
        set(Key.DATE_PROPERTY, dateParam);
    }

    public LocalDateTime getDateParam() {
        return get(Key.DATE_PROPERTY);
    }
}

【讨论】:

  • 这是最简单的方法吗? :|
  • @Eugene 好吧,是的。 1) 这是从“我最多有一个值”到“我只有一个可以保存一个值的字段”的逻辑结论 2) 技术上是不可能的约束是否被违反 3)每个属性方法都是一个简单的单行,比任何显示的替代方法都简单。 4)the OP says时,字段数可能会变大,这是最节省内存的方案。
  • @Eugene 不要忘记,我展示了完整的课程,不像其他答案只显示实际解决方案的一部分。实际上,在我的解决方案中,簿记少于其他方法。只需考虑添加另一个属性需要修改多少代码,以及十个、二十个或三十个字段的解决方案是什么样的。
  • +1 我同意互斥变量应保存在单个字段中。我可能会使用更少的方法来访问它,使用泛型类型参数。
  • @JohnWu 这就是(在我的示例中为privategetset 方法已经做到的;其他方法只是为了方便;如果您更喜欢将泛型方法作为 API,则可以省略它们。
【解决方案2】:

将您的unsetAll 方法更改为setAll

private void setAll(BigInteger numericParam, String stringParam, LocalDateTime dateParam) {
    this.numericParam = numericParam;
    this.stringParam = stringParam;
    this.dateParam = dateParam;
}

然后从您的公共设置器中调用,例如:

public void setNumericParam(BigInteger numericParam) {
    setAll(Objects.requireNonNull(numericParam), null, null);
}

请注意,Objects.requireNonNullsetAll 之前进行评估,因此如果您要传入null numericParam,这将在不更改任何内部状态的情况下失败。

【讨论】:

  • @dan1st your edit 不正确。 OP 要求强制大多数(或者可能恰好)一个字段是非空的。您的检查将确保至少一个字段不为空。
  • 我故意没有添加任何检查,因为检查setAll 的一个参数是否为非空所需的代码比目视检查两个参数更复杂且容易出错空。
  • 这真的不能很好地扩展......添加一个新字段,您必须更改每个方法!事实上,我认为这使得它不如原来的...
  • @Mars 除了示例之外,问题中没有说明所需的比例(根据类中的字段数或此类类的数量)。对于三个字段来说,这很好,并且在故障原子性方面是一个改进。但可以肯定的是,Holger 的方法可能更好。
【解决方案3】:

前言:我的回答更具理论性,它所描述的实践在 Java 中并不真正实用。他们根本没有得到很好的支持,按照惯例,你会“违背常规”。无论如何,我认为这是一个值得了解的简洁模式,我想我会分享。

Java 的类是product types。当class C 包含T1T2、...、Tn 类型的成员时,则C 类的对象的有效值是Cartesian product 的值T1、@ 987654334@, ..., Tn。例如,如果class C 包含bool(具有2 值)和byte(具有256 值),那么512 可能的C 对象值:

  • (false, -128)
  • (false, -127)
  • ...
  • (false, 0) ...
  • (false, 127)
  • (true, -128)
  • (true, -127)
  • ...
  • (true, 0) ...
  • (true, 127)

在您的示例中,ExclusiveField 的理论可能值等于 numberOfValuesOf(BigInteger.class) * numberOfValuesOf(String) * numberOfValuesOf(LocalDateTime)(注意乘法,这就是为什么它被称为产品类型),但这并不是您真正想要的。您正在寻找消除大量这些组合的方法,以便唯一的值是一个字段为非空,而其他字段为空。有numberOfValuesOf(BigInteger.class) + numberOfValuesOf(String) + numberOfValuesOf(LocalDateTime)。请注意添加,这表明您正在寻找的是“总和类型”。

正式地说,您在这里寻找的是tagged union(也称为变体、变体记录、选择类型、可区分联合、不相交联合或总和类型)。标记联合是一种类型,其值是成员的一个值之间的选择。在前面的示例中,如果 C 是 sum 类型,则只有 258 个可能的值:-128-127、...、0127truefalse

我建议您查看unions in C,以了解其工作原理。 C 的问题在于它的联合无法“记住”在任何给定点哪个“案例”处于活动状态,这在很大程度上违背了“总和类型”的全部目的。为了解决这个问题,您将添加一个“标签”,它是一个枚举,其值告诉您联合的状态是什么。 “Union”存储了payload,“tag”告诉你payload的类型,因此是“tagged union”。

问题是,Java 并没有真正内置这样的功能。幸运的是,we can harness class hierarchies (or interfaces) to implement this。基本上每次需要时您都必须自己滚动,这很痛苦,因为它需要 很多 样板,但它在概念上很简单:

  • 对于n 不同的情况,您可以创建n 不同的私有类,每个类都存储与该情况相关的成员
  • 您将这些私有类统一在一个公共基类(通常是抽象的)或接口下
  • 您将这些类封装在一个转发类中,该类公开一个公共 API,同时隐藏私有内部(以确保没有其他人可以实现您的接口)。

您的界面可以n 方法,每个都类似于getXYZValue()。这些方法可以设为default methods,其中默认实现返回null(用于Object 值,但不适用于基元、Optional.empty()(用于Optional&lt;T&gt; 值)或throw 异常(恶心,但是对于像int 这样的原始值没有更好的方法)。我不喜欢这种方法,因为接口相当虚伪。符合类型真的不符合接口,只有 ¹/n 次。

相反,您可以使用匹配 uhhh, 模式的模式。您创建了一个方法(例如match),它采用n 不同的Function 参数,其类型对应于可区分联合的案例类型。要使用可区分联合的值,您需要匹配它并提供n lambda 表达式,每个表达式的行为类似于switch 语句中的情况。调用时,动态调度系统调用与特定storage 对象关联的match 实现,该对象调用正确的n 函数之一并传递其值。

这是一个例子:

import java.util.Optional;
import java.util.Arrays;
import java.util.List;

import java.util.function.Function;
import java.util.function.Consumer;

import java.time.LocalDateTime;
import java.time.LocalDateTime;
import java.math.BigInteger;

class Untitled {
    public static void main(String[] args) {
        List<ExclusiveField> exclusiveFields = Arrays.asList(
            ExclusiveField.withBigIntegerValue(BigInteger.ONE),
            ExclusiveField.withDateValue(LocalDateTime.now()),
            ExclusiveField.withStringValue("ABC")
        );

        for (ExclusiveField field : exclusiveFields) {
            field.consume(
                i -> System.out.println("Value was a BigInteger: " + i),
                d -> System.out.println("Value was a LocalDateTime: " + d),
                s -> System.out.println("Value was a String: " + s)
            );
        }
    }
}

class ExclusiveField {
    private ExclusiveFieldStorage storage;

    private ExclusiveField(ExclusiveFieldStorage storage) { this.storage = storage; }

    public static ExclusiveField withBigIntegerValue(BigInteger i) { return new ExclusiveField(new BigIntegerStorage(i)); }
    public static ExclusiveField withDateValue(LocalDateTime d) { return new ExclusiveField(new DateStorage(d)); }
    public static ExclusiveField withStringValue(String s) { return new ExclusiveField(new StringStorage(s)); }

    private <T> Function<T, Void> consumerToVoidReturningFunction(Consumer<T> consumer) {
        return arg -> { 
            consumer.accept(arg);
            return null;
        };
    }

    // This just consumes the value, without returning any results (such as for printing)
    public void consume(
        Consumer<BigInteger> bigIntegerMatcher,
        Consumer<LocalDateTime> dateMatcher,
        Consumer<String> stringMatcher
    ) {
        this.storage.match(
            consumerToVoidReturningFunction(bigIntegerMatcher),
            consumerToVoidReturningFunction(dateMatcher),
            consumerToVoidReturningFunction(stringMatcher)
        );
    }   

    // Transform 'this' according to one of the lambdas, resuling in an 'R'.
    public <R> R map(
        Function<BigInteger, R> bigIntegerMatcher,
        Function<LocalDateTime, R> dateMatcher,
        Function<String, R> stringMatcher
    ) {
        return this.storage.match(bigIntegerMatcher, dateMatcher, stringMatcher);
    }   

    private interface ExclusiveFieldStorage {
        public <R> R match(
            Function<BigInteger, R> bigIntegerMatcher,
            Function<LocalDateTime, R> dateMatcher,
            Function<String, R> stringMatcher
        );
    }

    private static class BigIntegerStorage implements ExclusiveFieldStorage {
        private BigInteger bigIntegerValue;

        BigIntegerStorage(BigInteger bigIntegerValue) { this.bigIntegerValue = bigIntegerValue; }

        public <R> R match(
            Function<BigInteger, R> bigIntegerMatcher,
            Function<LocalDateTime, R> dateMatcher,
            Function<String, R> stringMatcher
        ) {
            return bigIntegerMatcher.apply(this.bigIntegerValue);
        }
    }

    private static class DateStorage implements ExclusiveFieldStorage {
        private LocalDateTime dateValue;

        DateStorage(LocalDateTime dateValue) { this.dateValue = dateValue; }

        public <R> R match(
            Function<BigInteger, R> bigIntegerMatcher,
            Function<LocalDateTime, R> dateMatcher,
            Function<String, R> stringMatcher
        ) {
            return dateMatcher.apply(this.dateValue);
        }
    }

    private static class StringStorage implements ExclusiveFieldStorage {
        private String stringValue;

        StringStorage(String stringValue) { this.stringValue = stringValue; }

        public <R> R match(
            Function<BigInteger, R> bigIntegerMatcher,
            Function<LocalDateTime, R> dateMatcher,
            Function<String, R> stringMatcher
        ) {
            return stringMatcher.apply(this.stringValue);
        }
    }
}

【讨论】:

  • 您可以用 lambda 表达式替换 ExclusiveFieldStorage 实现类,但是,当您需要将 FunctionConsumer 中的每一个指定为方法参数。而当你不提供变异方法时,就没有必要将ExclusiveFieldExclusiveFieldStorage 分开。
  • 哇,这是一个很酷的想法(切换到 lambdas)。有机会我会玩弄它
  • @Holger 之所以将ExclusiveFieldExclusiveFieldStorage 分开是因为我希望接口是私有的,所以没有其他人可以在其中添加新的“案例”。
  • 好吧,方法签名决定了存在哪些情况,因为您不能添加新参数。但是,是的,分居可能有助于执行合同。关于 lambda 表达式,最大的障碍是它们不能实现泛型方法,所以我能想到的最好的方法是使 consume 成为函数方法,将 map 设为 default 方法,参见 ideone.com/gdw8ek
  • @Holger 实际上,因为我有这个“直通”泛型R,所以我不能用 lambdas 替换类(不适用于泛型函数)。我可以使用内部类,但这并没有太大的改进。
【解决方案4】:

为什么不简单呢?

public void setNumericParam(BigInteger numericParam) { 
     this.numericParam = Objects.requireNonNull(numericParam); 
     this.stringParam = null; 
     this.dateParam = null; 
}

【讨论】:

  • 是的,但是有很多代码重复。想象一下当字段的数量很大时
  • @HieuHT if 它很大,从你的问题来看,它不是。
  • 是的,我应该将其添加到我的问题中
【解决方案5】:

你的目标

您在 cmets 中提到您的目标是为遗留数据库编写 SQL 请求:

类型:VARCHAR,数字:INT,字符串:VARCHAR,日期:DATETIME 和 ExclusiveField 将用作 getQueryRunner().query("CALL sp_insert_parameter(?, ?, ?, ?, ?)", param.getNumericParam(), id, 类型,param.getStringParam(),param.getDateParam())

所以你的目标真的不是创建一个只有一个非空字段的类。

另类

您可以使用id, type, value 属性定义一个抽象类Field

public abstract class Field
{
    private int id;
    private Class<?> type;
    private Object value;

    public Field(int id, Object value) {
        this.id = id;
        this.type = value.getClass();
        this.value = value;
    }

    public abstract int getPosition();
}

为数据库中的每一列创建一个小的对应类,扩展Field。每个类都定义了它想要的类型和它在 SQL 命令中的位置:

import java.math.BigInteger;


public class BigIntegerField extends Field
{
    public BigIntegerField(int id, BigInteger numericParam) {
        super(id, numericParam);
    }

    @Override
    public int getPosition() {
        return 0;
    }
}

你可以定义Field#toSQL:

public String toSQL(int columnsCount) {
    List<String> rows = new ArrayList<>(Collections.nCopies(columnsCount, "NULL"));
    rows.set(getPosition(), String.valueOf(value));
    return String.format("SOME SQL COMMAND (%d, %s, %s)", id, type.getName(), String.join(", ", rows));
}

它将在除所需位置之外的所有位置输出 NULLS。

就是这样。

完整代码

Field.java

package com.stackoverflow.legacy_field;

import java.math.BigInteger;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;


public abstract class Field
{
    private int id;
    private Class<?> type;
    private Object value;

    public Field(int id, Object value) {
        this.id = id;
        this.type = value.getClass();
        this.value = value;
    }

    public abstract int getPosition();

    public static void main(String[] args) {
        List<Field> fields = Arrays.asList(new BigIntegerField(3, BigInteger.TEN),
                new StringField(17, "FooBar"),
                new DateTimeField(21, LocalDateTime.now()));
        for (Field field : fields) {
            System.out.println(field.toSQL(3));
        }
    }

    public String toSQL(int columnsCount) {
        List<String> rows = new ArrayList<>(Collections.nCopies(columnsCount, "NULL"));
        rows.set(getPosition(), String.valueOf(value));
        return String.format("SOME SQL COMMAND (%d, %s, %s)", id, type.getName(), String.join(", ", rows));
    }
}

BigIntegerField.java

package com.stackoverflow.legacy_field;

import java.math.BigInteger;


public class BigIntegerField extends Field
{
    public BigIntegerField(int id, BigInteger numericParam) {
        super(id, numericParam);
    }

    @Override
    public int getPosition() {
        return 0;
    }
}

StringField.java

package com.stackoverflow.legacy_field;

public class StringField extends Field
{
    public StringField(int id, String stringParam) {
        super(id, stringParam);
    }

    @Override
    public int getPosition() {
        return 1;
    }
}

DateTimeField.java

package com.stackoverflow.legacy_field;

import java.time.LocalDateTime;

public class DateTimeField extends Field
{

    public DateTimeField(int id, LocalDateTime value) {
        super(id, value);
    }

    @Override
    public int getPosition() {
        return 2;
    }
}

结果

启动Field#main 输出:

SOME SQL COMMAND (3, java.math.BigInteger, 10, NULL, NULL)
SOME SQL COMMAND (17, java.lang.String, NULL, FooBar, NULL)
SOME SQL COMMAND (21, java.time.LocalDateTime, NULL, NULL, 2019-05-09T09:39:56.062)

这应该非常接近您想要的输出。如果需要,您可能会找到更好的名称并定义特定的 toString() 方法。

【讨论】:

    【解决方案6】:

    你可以使用反射。两个函数,你就完成了。添加新字段?没问题。你甚至不需要改变任何东西。

    public void SetExclusiveValue(String param, Object val){
        this.UnsetAll();
        Class cls = this.getClass();
        Field fld = cls.getDeclaredField(param); //Maybe need to set accessibility temporarily? Or some other kind of check.
        //Also need to add check for fld existence!
        fld.set(this, Objects.requireNonNull(val));
    }
    
    private void UnsetAll(){
        Class cls = this.getClass();
        Field[] flds = cls.getDeclaredFields();
        for (Field fld : flds){
            fld.set(this,null);
        }
    }
    

    如果可访问性是一个问题,您可以简单地添加一个可访问字段列表并检查param

    【讨论】:

      【解决方案7】:
      class Value<T> {
          T value;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-05-11
        • 2015-01-01
        • 2019-07-14
        • 2019-02-04
        • 2014-02-24
        • 1970-01-01
        • 2020-01-10
        • 1970-01-01
        相关资源
        最近更新 更多