什么是在对象中仅允许一个非null字段的好方法

Hie*_*uHT 33 java

我想编写一个类,其中包含不止一种类型的多个字段,但是在任何时候,实例对象中只有一个字段具有非null值。

到目前为止,我所做的事情看起来并不十分干净。

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Java是否以某种方式支持此模式,或者有更合适的方法来实现?

Hol*_*ger 29

一个对象只有一个非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);
    }
}
Run Code Online (Sandbox Code Playgroud)

如果类型不是唯一的区分符,则需要定义不同的键值。An 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 那是最简单的方法?:| (5认同)
  • @Eugene,别忘了,我已经展示了完整的类,这与其他答案仅显示实际解决方案的一部分不同。实际上,在我的解决方案中,簿记要比其他方法少。只需考虑添加新属性必须触摸多少代码,以及十,二十或三十个字段的解决方案是什么样的。 (4认同)
  • @Eugene好吧,是的。1)这是从“ *我想最多拥有一个值*”到“ *我只有一个字段可以容纳一个值*”的逻辑结论2)从技术上讲,违反约束是不可能的3)每个属性该方法是一种简单的单衬管,比任何显示的替代方法都更简单。4)当[OP说](/sf/ask/3922595561/ / 56042214?noredirect = 1#comment98721132_56039228),字段数可能会变得巨大,这是内存效率最高的解决方案。 (2认同)

And*_*ner 21

将您的unsetAll方法更改为setAll

private void setAll(BigInteger numericParam, String stringParam, LocalDateTime dateParam) {
    this.numericParam = numericParam;
    this.stringParam = stringParam;
    this.dateParam = dateParam;
}
Run Code Online (Sandbox Code Playgroud)

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

public void setNumericParam(BigInteger numericParam) {
    setAll(Objects.requireNonNull(numericParam), null, null);
}
Run Code Online (Sandbox Code Playgroud)

请注意,该值Objects.requireNonNull是在之前评估的setAll,因此如果要传递,则在null numericParam不更改任何内部状态的情况下将失败。


Ale*_*ica 6

前言:我的回答是理论上的,它所描述的实践在Java中并不实际。他们根本没有得到很好的支持,按照惯例,您将“违背原则”。无论如何,我认为这是一个整洁的模式,我想我会分享。

Java的类是产品类型。当class C包含类型的成员T1T2,..., Tn,则类对象的有效值C笛卡尔乘积的值T1T2,..., 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)(请注意乘积,这就是为什么它被称为乘积类型),但这并不是您真正想要的。您正在寻找消除大量这些组合的方法,以便唯一的值是一个字段为非空值而其他字段为null时。有numberOfValuesOf(BigInteger.class) + numberOfValuesOf(String) + numberOfValuesOf(LocalDateTime)。请注意附加项,这表明您要查找的是“求和类型”。

正式而言,您在这里寻找的是带标签的联合(也称为变体,变体记录,选择类型,有区别的联合,不相交的联合或求和类型)。带标记的联合是一种类型,其值是在成员的一个值之间进行选择。在前面的例子,如果C是和类型,将只有258可能的值:-128-127,..., ,0127,。truefalse

我建议您检查C中的并集,以了解其工作原理。C的问题在于,它的联合体无法“记住”在任何给定时间都处于活动状态的“案例”,这在很大程度上违背了“求和类型”的整个目的。为了解决这个问题,您可以添加一个“标签”,它是一个枚举,其值告诉您联合状态是什么。“联合”存储有效载荷,“标签”告诉您有效载荷的类型,因此“标记为联合”。

问题是,Java确实没有内置这样的功能。幸运的是,我们可以利用类层次结构(或接口)来实现此功能。从本质上讲,您每次需要时都必须自己滚动,这很痛苦,因为它需要很多样板,但是从概念上讲很简单:*对于n不同的情况,您将创建n不同的私有类,每个私有类都存储与该情况相关的成员*您可以将这些私有类统一在一个通用的基类(通常是抽象类)或接口下*将这些类包装在一个转发类中,该类公开了所有公共API,同时隐藏了私有内部(以确保没有其他人可以实现您的接口)。

您的界面可能包含n方法,每个方法都类似getXYZValue()。这些方法可以作为默认方法使用,其中默认实现返回null(对于Object值,但不适用于基元,Optional.empty()(对于Optional<T>值)或throw异常(粗略,但没有更好的方法来处理诸如int)。我不喜欢这种方法,因为接口相当不精巧。符合类型并不真正符合接口,只是接口的1 / n。

相反,您可以使用与uhhh模式匹配的模式。您将创建一个match采用n不同Function参数的方法(例如),其类型与已区分联合的案例的类型相对应。要使用已区分联合的值,请对其进行匹配并提供nlambda表达式,每个表达式的行为都类似于switch语句中的情况。当调用时,动态调度系统将调用match与特定storage对象关联的实现,该对象将调用正确的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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)