Validation of the data type when creating a class

There is the following code:

public interface ISettings{
     int value();
}

public record ExampleSet1 (int value) implements ISettings{}
public record ExampleSet2 (int value) implements ISettings{}

public class ExampleClass {
    private final ExampleSet1 settingsGlobal;

    public ExampleClass(ISettings settingsLocal) {
        if (settingsLocal instanceof ExampleSet1 set) {
            settingsGlobal = set;
        } else {
            throw new IllegalArgumentException("Settings type mismatch");
        }
    }
}

In the constructor, you need to check that settingsLocal corresponds to the settingsGlobal type. I would like to put the check in a separate method, but I can’t get the settingsGlobal type to check it for compliance. There is an option to implement verification using getClass():

public class ExampleClass {
    private final ExampleSet1 settingsGlobal;

    public ExampleClass(ISettings settingsLocal) {
        if (!validateSettings(settingsLocal)) {
            throw new IllegalArgumentException("Settings type mismatch");
        }
        settingsGlobal = (ExampleSet1) settingsLocal;
    }

    private boolean validateSettings(ISettings settingsLocal) {
        return settingsLocal.getClass().equals(settingsGlobal.getClass());
    }
}

But I can’t call the getClass() method for a variable that is not initialized.

This is a simplified example. In reality, there may be many classes that contain different types of settings (but they all support the ISettings interface). I wanted to implement validation in an abstract class, so as not to do this in every class used.

How can I check the compliance of the settingsGlobal type?

To check the compliance of the settingsGlobal type with settingsLocal in a way that avoids initializing the settingsGlobal variable before the check, you could leverage generics and a parameterized abstract class. This way, each subclass can specify the type of ISettings it expects, allowing you to perform the type check generically and in a reusable manner.

Here’s how you could set up an abstract base class that validates the ISettings type before setting the settingsGlobal variable:

public interface ISettings {
    int value();
}

public record ExampleSet1(int value) implements ISettings {}
public record ExampleSet2(int value) implements ISettings {}

// Abstract base class that enforces type checks
public abstract class AbstractExampleClass<T extends ISettings> {
    private final T settingsGlobal;

    protected AbstractExampleClass(T settingsLocal) {
        if (!validateSettings(settingsLocal)) {
            throw new IllegalArgumentException("Settings type mismatch");
        }
        this.settingsGlobal = settingsLocal;
    }

    private boolean validateSettings(ISettings settingsLocal) {
        return settingsLocal.getClass().equals(getExpectedSettingsType());
    }

    // Abstract method to specify the expected class type
    protected abstract Class<T> getExpectedSettingsType();

    // Getter for settingsGlobal
    public T getSettingsGlobal() {
        return settingsGlobal;
    }
}

// Concrete subclass for ExampleSet1
public class ExampleClass1 extends AbstractExampleClass<ExampleSet1> {
    public ExampleClass1(ExampleSet1 settingsLocal) {
        super(settingsLocal);
    }

    @Override
    protected Class<ExampleSet1> getExpectedSettingsType() {
        return ExampleSet1.class;
    }
}

// Concrete subclass for ExampleSet2
public class ExampleClass2 extends AbstractExampleClass<ExampleSet2> {
    public ExampleClass2(ExampleSet2 settingsLocal) {
        super(settingsLocal);
    }

    @Override
    protected Class<ExampleSet2> getExpectedSettingsType() {
        return ExampleSet2.class;
    }
}

Explanation

  1. Generic Type Parameter: The abstract class AbstractExampleClass<T extends ISettings> uses a generic type T that extends ISettings. This lets subclasses specify which type of ISettings they expect (e.g., ExampleSet1, ExampleSet2).
  2. Validation Method: The validateSettings method checks if the class of settingsLocal matches the expected settings type by comparing settingsLocal.getClass() with the expected class returned by getExpectedSettingsType.
  3. getExpectedSettingsType Method: Each subclass implements the getExpectedSettingsType method to return its specific settings class type.
  4. Concrete Subclasses: ExampleClass1 and ExampleClass2 are concrete implementations that specify ExampleSet1 and ExampleSet2 as the expected types, respectively. This allows them to enforce the type check before setting settingsGlobal.

This setup allows you to centralize the validation logic in an abstract class and avoid duplicating code in each subclass. Each subclass simply specifies its expected settings type by overriding getExpectedSettingsType.