2
\$\begingroup\$

I have the following class which represents a set of properties.

public class Properties
{
    /** String type properties. */
    private final List<String> m_stringProperties = Arrays.asList("str1", "str2", "str3");

    /** Float type properties. */
    private final List<String> m_floatProperties = Arrays.asList("float1", "float2", "float3");

    /** Integer type properties. */
    private final List<String> m_intProperties = Arrays.asList("int1", "int2");

    public class PropertyType
    {
        private final String m_name;
        private final Object m_value;

        public PropertyType(String name, Object value)
        {
            m_name = name;
            m_value = value;
        }

        public String getName()
        {
            return m_name;
        }

        public Object getValue()
        {
            return m_value;
        }
    }

    /** The container for the properties. */
    private final Map<String, PropertyType> m_properties = new HashMap<>();

    /**
     * Constructor.
     */
    public Properties()
    {
    }

    /**
     * Returns the value of the given property if it exists.
     *
     * @param name The name of the property.
     *
     * @return The property value if it exists, null otherwise.
     */
    public PropertyType getProperty(String name)
    {
        return m_properties.get(name);
    }

    /**
     * Sets the value of the given property. If the property does not exist, it is added first.
     *
     * @param name  The name of the property.
     * @param value The value of the property.
     *
     * @return None.
     */
    public void setProperty(String name, Object value)
    {
        if ((m_stringProperties.contains(name) && value instanceof String)
                || (m_floatProperties.contains(name) && value instanceof Float)
                || (m_intProperties.contains(name) && value instanceof Integer))
        {
            m_properties.put(name, new PropertyType(name, value));
        }

        else
        {
            assert false : "Invalid property name";
        }
    }

    /**
     * Gets the number of properties.
     *
     * @return The number of properties.
     */
    public int count()
    {
        return m_properties.size();
    }
}

Notes

  1. Each property has a name and a value.
  2. Property values can be of type String, Float or Integer.
  3. The names of properties is restricted to the values defined in the list at the top of the class.
  4. A given property can only be added to the map if it is the correct type for that property name.

The class could be used as follows:

Properties properties = new Properties();

// set properties
properties.setProperty("str1", "testId");
properties.setProperty("float1", 1.0f);

// get properties
Properties.PropertyType str1 = properties.getProperty("str1");
Properties.PropertyType float1 = properties.getProperty("float1");
Properties.PropertyType invalid = properties.getProperty("unknown");    // return null

System.out.println("str1: " + str1.getValue());
System.out.println("float1: " + float1.getValue());

float f1 = (float) properties.getProperty("float1").getValue();
Object o1 = properties.getProperty("float1").getValue();

System.out.println("f1: " + f1);
System.out.println("o1: " + o1);

properties.setProperty("str1", 1.0f);       // assertion - str1 property should be String, not Float

I'd like to know if there is a better way to implement this. Specifically, I'd like to avoid the use of Object and the casting that goes with it. I've experimented with a parametrised class and generic types and even a typesafe heterogenous container as described in Item 29 of Effective Java.

I would like to make it as typesafe as possible - i.e. enforce type checking by the compiler - so that if getProperty is called the return value is automatically the correct type.

I realise that I could overload setProperty for each type and that getProperty could just return an Object instead of the nested type PropertyType but that would still leave me with a container of <String, Object>.

I'm a Java newbie coming from C++. In C++ the map value would be a boost::variant.

\$\endgroup\$
6
  • \$\begingroup\$ Need more context to answer this one; How do you know it's a float/String/Integer right now? What would a magical class that would give you the value already casted give you as benefit? This looks like a design issue to me and I don't see a solution without knowing the use cases. \$\endgroup\$
    – Pimgd
    Commented Aug 17, 2016 at 16:39
  • \$\begingroup\$ I'm not sure I understand your question about how I know it's a float/String/Integer. The magical class would give me a compile time error if I tried to assign a returned property value to the wrong type (rather than a runtime error). \$\endgroup\$
    – ksl
    Commented Aug 17, 2016 at 17:06
  • \$\begingroup\$ I have rolled back the last edit. Please see what you may and may not do after receiving answers. \$\endgroup\$
    – Heslacher
    Commented Aug 18, 2016 at 8:45
  • \$\begingroup\$ why do you need this? Answerers have given you bodgy ways of doing it, but what is your end purpose here? \$\endgroup\$
    – Dan
    Commented Aug 18, 2016 at 11:12
  • \$\begingroup\$ @DanPantry Ultimately I want to put properties in a map. The key is the property name, which is a unique string. That values of the properties can be different types. \$\endgroup\$
    – ksl
    Commented Aug 18, 2016 at 11:43

2 Answers 2

3
\$\begingroup\$

What you are looking for cannot be done. The type of the value to be retrieved cannot be determined as long as you store it all in a Map<String, Object>.

Imagine you want to validate compile time that reads and writes use the correct type.

The writing could be easily done; you can simply wrap Map and provide 3 methods for put: put(String key, float value), put(String key, String value), put(String key, int value).

The reading could also be easily done, you just create a float getFloat(String key), int getInt(String key) and String getString(String key).

What you CANNOT validate is, compile time, that put("blah", 10) should not be combined with getFloat("blah").

Similarily, it is also impossible to specify that for a CERTAIN RANGE of input values, these are floats, and these are strings, and these are...

It cannot be done in a clean way.

...

Ah.

It can be done in a nasty way, though, by creating Enums to serve as keys.

First, create an interface for the key...

interface class StorageKey {
    String getKey();
}

Make enums implementing this interface...

public enum StorageKeyForString implements StorageKey {
    private final String key;
    private StorageKeyForString(String key){
        this.key = Objects.requireNotNull(key, "null not allowed for key");
    }
    public String getKey(){
        return key;
    }
}

And add static instances.

STR1("str1");

With a similar enum for Float values and Integer values, you could make everything compile-time safe... at the cost of so much flexibility that I personally think you'd be better off just... using a regular class.

I mean.... you have a bounded list of fields. You can literally store everything in a class, add getters and setters, and then just use the appropriate get and set call.

If you can choose runtime, you cannot validate compile time.


Failing all that, if you just want to improve your current setup, at least use overloading for that. Rename setProperty to _setProperty, ditch the type checks in _setProperty, and make 3 overloaded versions of setProperty which take a Integer value or a String value or a Float value. That'd at least get you type-safety for value writes.

\$\endgroup\$
3
  • \$\begingroup\$ What does renaming setProperty to _setProperty get me? \$\endgroup\$
    – ksl
    Commented Aug 18, 2016 at 6:46
  • \$\begingroup\$ Is there an alternative to Map<String, Object>? i.e. instead of Object some other type that can represent the types of the values? Maybe something on the lines of Class<T> as in the typesafe heterogenous container as described in Item 29 of Effective Java. \$\endgroup\$
    – ksl
    Commented Aug 18, 2016 at 6:49
  • \$\begingroup\$ By using a single function to write to the map, you only need to check for null once. There is no alternative to Object, as that's the first common parent of String, Integer and Float. \$\endgroup\$
    – Pimgd
    Commented Aug 18, 2016 at 15:48
1
\$\begingroup\$

You need the type (class) of the value, as the generic type parameter will be erased at run-time.

If you look at the following:

class TypedValue<T> {
    public final Class<T> type;
    public final T value;
    TypedValue(Class<T> type, T value) {
        this.type = type;
        this.value = value;
    }
}

private Map<String, TypedValue<?>> map = new HashMap<>(); // Loose typing

public void <T> putProperty(String name, T value, Class<T> type) {
    TypedValue<?> old = map.put(name, new TypedValue<type, value));
}

public <T> T getProperty(String name, Class<T> type) {
    TypedValue<?> tv = map.get(name);
    return type.cast(tv.value);
}

public Class<?> getPropertyType(String name) {
    TypedValue<?> tv = map.get(name);
    return tv.type;
}

The most painful is that the getProperty needs to pass the wanted type. For non-null values, non-child classes the value's type need not be stored. However:

Double x = properties.getProperty("string2", Double.class);

cannot be checked.

Of course, that is why we have compiled typed programming languages.

The "best" looking solution:

double x = properties.doubles.get("string2"); // double var "string2" does not exist
String s = properties.strings.get("string2"); // fine "string2" does exist
Foo foo = properties.byClass.get(Foo.class, "foo42"); // catch all others

with

public final Map<String, Double> doubles = new HashMap<>();
public final Map<String, String> strings = new HashMap<>();

In general it brings no computational synergy. For database access general purpose maps are used for record fields by some frameworks. For mixing symbolic access (strings) and calculation, the Java Scripting API might be used. There you create for example a JavaScript engine, and add a map of variables, and calculate there.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.