39

I want to write a class with more than 1 fields of different types but at any time, there is one and only one field of an instance object having non null value.

What I did so far does not look really clean.

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;
    }
}

Does Java support this pattern somehow or is there a more decent way to do it?

HieuHT
  • 459
  • 5
  • 13
  • 12
    1. if you pass in a null value to any of those, *all* fields will be left as null, which is probably not intentional 2. do you need the object to be mutable? Providing 3 different constructors (or factory methods if you prefer) would make this quite a bit cleaner and guarantee that every `ExclusiveField` object that's accessible is always valid. – Joachim Sauer May 08 '19 at 09:02
  • 1
    NPE will be thrown if a null value passed in. However, it is also my intent that all fields can be null. I thought of having different constructors but when there are lots more fields or some fields have the same type it will no longer work – HieuHT May 08 '19 at 09:18
  • 4
    @HieuHT but even so, it is better not to change the state when an exception occurs. This idea is called [*failure atomicity*](https://stackoverflow.com/questions/29842845/what-is-failure-atomicity-used-by-j-bloch-and-how-its-beneficial-in-terms-of-i). – Andy Turner May 08 '19 at 10:01
  • 1
    Indeed, I missed that point even though just went through Joshua's book – HieuHT May 08 '19 at 10:11
  • 3
    This feels like a broken requirement. I'm very *very* curious about what this class actually does ... I would almost never expect a class to behave like this. [What are you ultimately trying to accomplish?](http://xyproblem.info/) – svidgen May 08 '19 at 15:33
  • Could you not simply store one `Object`, and use `instanceof` to figure out what type it is later? Your setters could keep the same signatures, but internally the value is stored as just `Object`, which would prevent someone from setting it to an unsupported class. – Darrel Hoffman May 08 '19 at 16:56
  • @svidgen I have to adapt Java code to a legacy stored procs to insert a business parameter to a table. The table contains 5 columns of `id, type, numeric, string, date`. Numeric, string or date is set depending on type and by default NULL. The stored procs is like `sp_insert_parametter(id, type, numeric, string, date)` – HieuHT May 08 '19 at 18:45
  • @HieuHT ... I'm not sure that clarifies the problem, really. I'm assuming you're not at liberty to change the SP?? ... What do the SP params represent? When does each get set? ... Do you *have to use* the SP??? – svidgen May 08 '19 at 19:00
  • 2
    Beware that this is a common anti-pattern for cases more appropriately dealt with via subtyping--you are typing values by which pointer they would use, which is to say, choosing a pointer per which type a value is. – philipxy May 08 '19 at 19:06
  • @svidgen Table schema `type:VARCHAR, numeric: INT, string: VARCHAR, date: DATETIME` and `ExclusiveField` will be used as `getQueryRunner().query("CALL sp_insert_parametter(?, ?, ?, ?, ?)", param.getNumericParam(), id, type, param.getStringParam(), param.getDateParam())`. I have no control on either DB or SP. – HieuHT May 08 '19 at 20:03
  • @philipxy I was thinking about broader cases when some fields can have the same type. Important constraint is only one field having non-null value. – HieuHT May 08 '19 at 20:07
  • 2
    @HieuHT The field *types* aren't really that important, AFAIK. What are their names? Where does the data come from? How does it get used? What's their "business" value? What uses `ExclusiveField`? What do calls to `ExclusiveField` look like? Where do they come from? Etc... Rinse and repeat the same questions for the SP you're not allowed to change... The two top-voted answers below each sound reasonable to me -- *depending on what this class is ultimately **used for.*** – svidgen May 08 '19 at 21:11
  • 3
    My point is that you are conceptually partitioning non-null values into disjoint categories/subtypes/subclasses. I am not saying that such a partitioning criterion is necessarily per language primitive subtypes or per some subclassing that you already have. I am saying it may be a criterion that should be embodied by subclasses/inheritance in your code. The question is are you better served by a single pointer that will be of one of multiple subclasses, instead of your current design of a radio button for each category of object. – philipxy May 08 '19 at 21:20
  • @philipxy: Indeed. Does this [code](https://stackoverflow.com/a/56054535/6419007) implement your idea? – Eric Duminil May 09 '19 at 07:46
  • 2
    Ha. @EricDuminil's answer says "You mention in the comments that your goal is to write SQL requests for a legacy DB". I hadn't seen those comments. The place I am used to seeing this as anti-pattern for subtyping/inheritance is ... DB designs. [How can you represent inheritance in a database?](https://stackoverflow.com/q/3579079/3404097) Not only are your DB's radio-button fields a DB subtyping anti-pattern, the subtyping seems to be for that link's anti-pattern EAV--tables holding untyped metadata & data for the tables that *should* have been designed. PS **Clarify via edits, not comments.** – philipxy May 09 '19 at 09:47
  • @EricDuminil Your code seems a variant on what I suggest. But at first it is creepy--When radio buttons are part of the asker's *specification* per their legacy DB, simplicity says to expect something similar in the code; since you're not *replacing* but *implementing* radio buttons, inheritance seems a needless addition. On the other hand--*when you use these classes to interface with the DB* this is exaclty ["defining appropriate abstractions (types \[...\]) reconstructing functionality of a DBMS" when doing the wrong thing--EAV--the right way](https://stackoverflow.com/a/23950836/3404097). – philipxy May 09 '19 at 10:17
  • @philipxy: I'm not sure I understand your point. Specifically for inheritance : all the classes have some methods in common (e.g. `getPosition`), and it allows to work with a list of `Field`s, without caring if they're `StringField` or `BigIntegerField`. The specifications are tough and half-broken anyway, I don't expect to see a perfect solution to this problem. – Eric Duminil May 09 '19 at 10:32
  • 2
    @EricDuminil I don't understand. I was supporting you. I originally commented thinking the radio buttons were only in the asker's code. Just now I said: If all the asker was doing was echoing a radio-button subrecord in the system that they are coding a layer over then a superclass & subclasses (inheritance) implementing it is overkill; but if however that system has radio buttons *as a shitty abstraction of subtyping* (which it does as an EAV DB) (indeed they are types of values in the DB) & *you use the classes to access that system* (which you do) then those classes are helpful. – philipxy May 09 '19 at 11:15
  • 1
    @philipxy No wonder you don't understand my comment if I didn't understand your comment either XD thanks for the clarification. – Eric Duminil May 09 '19 at 11:28

7 Answers7

30

The simplest approach for an object to have only one non-null field, is to actually have only one field and assume all others to be null implicitly. You only need another tag field, to determine which field is non-null.

Since in your example, all alternatives seem to be about the type of the value, the type itself could be the tag value, e.g.

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);
    }
}

If the type is not the only differentiator, you need to define distinct key values. An enum would be a natural choice, but unfortunately, enum constants can not provide the type safety. So, the alternative would look like:

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);
    }
}
Holger
  • 285,553
  • 42
  • 434
  • 765
  • 5
    that's the simplest approach? :| – Eugene May 08 '19 at 14:05
  • 2
    @Eugene well, yes. 1) It’s the logical conclusion from “*I want to have at most one value*” to “*I have only one field that can hold a value*” 2) it is technically impossible that the constraint is ever violated 3) each property method is a simple single liner, simpler than any of the shown alternatives. 4) when [the OP says](https://stackoverflow.com/questions/56037079/what-is-a-good-way-to-allow-only-one-non-null-field-in-an-object/56042214?noredirect=1#comment98721132_56039228), the number of fields could become huge, this is the most memory efficient solution. – Holger May 08 '19 at 14:22
  • 4
    @Eugene and don’t forget, I’ve shown the complete class, unlike the other answers showing only a fragment of the actual solution. Actually, in my solution, the bookkeeping is less than in the other approaches. Just consider how much code has to be touched to add another property and how the solution looks like for ten, twenty, or thirty fields. – Holger May 08 '19 at 14:28
  • 1
    +1 I agree that a mutually-exclusive variable should be held in a single field. I probably would have used fewer methods to access it, using a generic type parameter. – John Wu May 08 '19 at 21:01
  • Another advantage to the second approach is you can then `switch` your key, rather than having tons of gnarly if-elses! – Mars May 09 '19 at 04:02
  • 1
    @JohnWu that’s what the (`private` in my example) `get` and `set` methods already do; the other methods are just for convenience; you could omit them if you prefer the generic methods as the API. – Holger May 09 '19 at 06:07
22

Change your unsetAll method to setAll:

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

Then invoke from your public setters like:

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

Note that Objects.requireNonNull is evaluated before setAll, so if you were to pass in a null numericParam, this would fail without changing any internal state.

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • 1
    @dan1st [your edit](https://stackoverflow.com/revisions/56037151/2) was not correct. OP is asking to enforce that at *most* (or perhaps *exactly*) one field is non-null. Your check would have ensured that at *least* one field is non-null. – Andy Turner May 08 '19 at 09:46
  • 1
    And I intentionally did not add any check because the code required to check that only one parameter of `setAll` is non-null is more complicated and error-prone than visual inspection that two of the params are null. – Andy Turner May 08 '19 at 09:49
  • This really doesn't scale well... Add a new field and you have to change every single method! In fact, I think that makes it less suitable than the original... – Mars May 09 '19 at 02:08
  • 1
    @Mars the required scale (in terms of either the number of fields in the class, or the number of such classes) is not stated in the question, beyond the example. For three fields, this is fine, and it is an improvement in terms of the failure atomicity. But sure, Holger's approach is probably better. – Andy Turner May 09 '19 at 07:33
7

preface: My answer is more theoretical, and the practices it describes aren't really practical in Java. They're simply not as well supported, and you would be "going against the grain", conventionally speaking. Regardless, I think it's a neat pattern to know about, and I thought I would share.

Java's classes are product types. When a class C contains members of types T1, T2, ..., Tn, then the valid values for objects of class C are the Cartesian product of the values of T1, T2, ..., Tn. For example, if class C contains a bool (which has 2 values) and byte (which has 256 values), then there are 512 possible values of C objects:

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

In your example, the theoretical possible values of ExclusiveField is equal to numberOfValuesOf(BigInteger.class) * numberOfValuesOf(String) * numberOfValuesOf(LocalDateTime) (notice the multiplication, that's why it's called a product type), but that's not really what you want. You're looking for ways to eliminate a huge set of these combinations so that the only values are when one field is non-null, and the others are null. There are numberOfValuesOf(BigInteger.class) + numberOfValuesOf(String) + numberOfValuesOf(LocalDateTime). Notice the addition, this indicates that what you're looking for is a "sum type".

Formally speaking, what you're looking for here is a tagged union (also called a variant, variant record, choice type, discriminated union, disjoint union, or sum type). A tagged union is a type whose values are a choice between one value of the members. In the previous example, if C was a sum type, there would be only 258 possible values: -128, -127, ..., 0, 127, true, false.

I recommend you check out unions in C, to build an understanding of how this works. The issue with C is that its unions had no way of "remembering" which "case" was active at any given point, which mostly defeats the whole purpose of a "sum type". To remedy this, you would add a "tag", which was an enum, whose value tells you what the state of the union is. "Union" stores the payload, and the "tag" tells you to the type of the payload, hence "tagged union".

The problem is, Java doesn't really have such a feature built in. Luckily, we can harness class hierarchies (or interfaces) to implement this. You essentially have to roll your own every time you need it, which is a pain because it takes a lot of boilerplate, but it's conceptually simple:

  • For n different cases, you make n different private classes, each storing the members pertinent to that case
  • You unify these private classes under a common base class (typically abstract) or interface
  • You wrap these classes in a forwarding class that exposes a public API all while hiding the private internals (to ensure that no one else can implement your interface).

Your interface could have n methods, each something like getXYZValue(). These methods could be made as default methods, where the default implementation returns null (for Object values, but doesn't work for primitives, Optional.empty() (for Optional<T> values), or throw an exception (gross, but there's no better way for primitive values like int). I don't like this approach, because the interface is rather disingenuous. Conforming types don't really conform to the interface, only ¹/n th of it.

Instead, you can use a pattern matching uhhh, pattern. You make a method (e.g. match) that takes n different Function parameters, whose types correspond to the types of cases of the discriminated union. To use a value of the discriminated union, you match it and provide n lambda expressions, each of which acts like the cases in a switch statement. When invoked, the dynamic dispatch system calls the match implementation associated with the particular storage object, which calls the correct one of the n functions and passes its value.

Here's an example:

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);
        }
    }
}
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • You can replace the `ExclusiveFieldStorage` implementation classes with lambda expressions, however, this approach is limited to 256 cases, when you require a `Function` or `Consumer` for each of them to be specified as method parameter. And when you don’t offer mutation methods, there is no need for separating `ExclusiveField` and `ExclusiveFieldStorage`. – Holger May 08 '19 at 14:52
  • Woah, that's a cool idea (switching to lambdas). I'll play around with it when I get a chance – Alexander May 09 '19 at 03:15
  • @Holger The reason I separated `ExclusiveField` and `ExclusiveFieldStorage` is because I want the interface to be private, so no one else can add new "cases" to this. – Alexander May 09 '19 at 03:17
  • Well, the method signature determines which cases exist, as you can’t add new parameters. But yes, the separation may help enforcing the contract. Regarding lambda expressions, the biggest obstacle is that they can’t implement generic methods, so the best, I could come up with, was to make `consume` the functional method and `map` a `default` method, see https://ideone.com/gdw8ek – Holger May 09 '19 at 07:30
  • @Holger Actually, because I have this "passthrough" generic `R`, I can't replace the classes with lambdas (which aren't applicable to generic functions). I could use inner classes, but it's not much of an improvement. – Alexander May 12 '19 at 05:44
  • That’s what I already addressed in my previous comment, “regarding lambda expressions, the biggest obstacle is that they can’t implement generic methods”. I also linked to an example solution that doesn’t need inner classes. I think, there are even better solutions possible. – Holger May 13 '19 at 06:10
  • @Holger Ah, I missed that comment entirely. That use of `Stream.Builder` is interesting... though I can't say I like it haha. Very clever! – Alexander May 13 '19 at 18:02
3

Why not simply?

public void setNumericParam(BigInteger numericParam) { 
     this.numericParam = Objects.requireNonNull(numericParam); 
     this.stringParam = null; 
     this.dateParam = null; 
}
Eugene
  • 117,005
  • 15
  • 201
  • 306
3

Your goal

You mention in the comments that your goal is to write SQL requests for a legacy DB:

type:VARCHAR, numeric: INT, string: VARCHAR, date: DATETIME and ExclusiveField will be used as getQueryRunner().query("CALL sp_insert_parametter(?, ?, ?, ?, ?)", param.getNumericParam(), id, type, param.getStringParam(), param.getDateParam())

So your goal really isn't to create a class with only one non-null field.

Alternative

You could define an abstract class Field with id, type, value attributes:

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();
}

For each column in your database, you create a small corresponding class, extending Field. Each class defines its desired type and its position in the SQL command:

import java.math.BigInteger;


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

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

You can define 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));
}

Which will output NULLS everywhere except at the desired position.

That's it.

Complete code

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;
    }
}

Result

Launching Field#main outputs:

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)

Which should be really close to your desired output. You could probably find better names and define specific toString() methods if needed.

Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
1

You could use reflection. Two functions and you're done. Add a new field? No problem. You don't even have to change anything.

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);
    }
}

If accessibiility is an issue, you could simply add a list of accessible fields and check param against that

Mars
  • 2,505
  • 17
  • 26
0
class Value<T> {
    T value;
}
General Grievance
  • 4,555
  • 31
  • 31
  • 45
JBourne
  • 335
  • 1
  • 3
  • 12