1
\$\begingroup\$

I tried to build a "typed" property system in Java and this is what I came up with: first the class Properties that any class can use as an attribute (demo code in the end).

import java.util.HashMap;
import java.util.Objects;

public class Properties {
    private HashMap<Type, Object> properties = new HashMap<>();
    public void add(Property property) {
        if(property != null) properties.put(property.getType(), property);
    }
    public void add(Type type, Object property) {
        Objects.requireNonNull(type, "null is not allowed as a type!");
        Objects.requireNonNull(property, "cannot add null as a property!");
        if(!property.getClass().isAssignableFrom(type.getPropertyClass())) {
            throw new IllegalArgumentException(
                    "the property is of "+property.getClass()+", but subtype of "+type.getPropertyClass()+ " needed");
        }
        properties.put(type, property);
    }
    public <P> P get(Type<P> type) {
        return get(type, type.getPropertyClass());
    }
    public <P> P get(Type<? super P> type, Class<P> subProperty) {
        var property = properties.get(type);
        if(property == null) return null;
        if(!subProperty.isAssignableFrom(property.getClass())) {
            throw new ClassCastException(
                    "the property is of "+property.getClass()+" which is not a subclass of "+subProperty);
        }
        return subProperty.cast(property);
    }

    public boolean has(Type type) {
        return properties.containsKey(type);
    }
    public <P> boolean has(Type<? super P> type, Class<P> subProperty) {
        var property = properties.get(type);
        return property!=null && subProperty.isAssignableFrom(property.getClass());
    }
} 

Type looks as follows:

public abstract class Type<P> {
    public final static Type<PropertyA> TYPE_A = new Type<>() {
        @Override
        public Class<PropertyA> getPropertyClass() {
            return PropertyA.class;
        }
    };
    public final static Type<PropertyB> TYPE_B = new Type<>() {
        @Override
        public Class<PropertyB> getPropertyClass() {
            return PropertyB.class;
        }
    };
    public final static Type<String> TYPE_STRING = new Type<>() {
        @Override
        public Class<String> getPropertyClass() {
            return String.class;
        }
    };
    public abstract Class<P> getPropertyClass();
}

And we can have Properties like that:

public abstract class Property<P extends Property> {
    public abstract Type<P> getType();
}
public class PropertyA extends Property<PropertyA> {
    @Override
    public Type<PropertyA> getType() {
        return Type.TYPE_A;
    }
}
public class PropertySubA extends PropertyA {
}
...

Any Type can have a dedicated Property-class (which may or may not be subclassed again) or the Type can be just any class, but in the latter case the one-argument add(Property)-method cannot be used.

I considered having Type<P extends Property> instead of just Type<P>, but then simple properties like TYPE_STRING would not be possible. Another idea is to omit the type altogether but use the class directly as key. However, that makes using instances of subclasses as properties much more complicated.

The following code demonstrates what one can do with this:

public class Main {
    public static void main(String... args) {
        Properties properties = new Properties();
        properties.add(new PropertyA());
        properties.add(new PropertySubB());

        PropertyA a = properties.get(Type.TYPE_A);
//        PropertySubA subA = properties.get(Type.TYPE_A, PropertySubA.class); // -> exception as expected

        PropertyB b = properties.get(Type.TYPE_B);
        PropertySubB subB = properties.get(Type.TYPE_B, PropertySubB.class);
        PropertySubB subBCast = (PropertySubB) properties.get(Type.TYPE_B);

//        PropertyA niceTry = properties.get(Type.TYPE_B); // -> does not compile
//        PropertyA niceTry2 = properties.get(Type.TYPE_B, PropertyA.class); // -> does not compile either

        Object propA = new PropertyA();
        properties.add(Type.TYPE_A, propA);
//        properties.add(Type.TYPE_B, propA); // -> exception as expected

        properties.add(Type.TYPE_STRING, "Hello world!");
        System.out.println(properties.get(Type.TYPE_STRING)); // -> prints "Hello world!"
    }
}

So we can avoid lots of casting with this system compared to using the HashMap directly. A cast and the one-argumetn get(...) can "replace" the two-argument get(...), but the latter has the advantage of compile-time checking.

Thanks already for looking over my code, now a few questions:

Is somthing wrong or unnecessarily complicated with my approach?

Is a similar system used in some standard libraries?

Am I missing some important functionality that a property-system should have? A first idea would be to allow multiple properties of the same type, but that is only a minor change.

I know that my Type-class is somewhat close to an Enum, but to my knowledge an Enum cannot be used together with generics. Therefore public abstract Class<P> getPropertyClass(); could only be declared as public abstract Class<?> getPropertyClass(); in an Enum. As a consequence I would lose the one-parameter get(...) method and some of the compile-time checks. Is there any way to get around this problem and still use an enum?

\$\endgroup\$
1
\$\begingroup\$

Is somthing wrong or unnecessarily complicated with my approach?

Requiring a dedicated class for each property is unnecessarily complicated.

Is a similar system used in some standard libraries?

No (standard library is a questionable term, the JRE doesn't come with such a featrue).

Am I missing some important functionality that a property-system should have?

Serialization.

Since your properties require dedicated code for each property, what benefits does it offer over simply serializing a dedicated property-object into a DataOutputStream? The code requires a lot of JavaDoccing and is a lot more complicated than, for example, Commons Configuration.

\$\endgroup\$
  • \$\begingroup\$ Thank you for your answer! Maybe "commonly used" would have been a better term than "standard". Having separate classes for each type is unnecessary indeed, not making Type abstract and having several instances works just as well. And Property itself would be better as an interface. As limited as the "concept idea" is, I agree that it does not have many benefits. A type could, e.g., come with a limit of how many properties it allows. Commons Configurations is mainly for configuration files with primitive (and String) entries which is a different usecase, I think. \$\endgroup\$ – floxbr Apr 23 at 7:23
  • \$\begingroup\$ Restrictions should be defined in the class that is being instantiated, not in the property framework. If you want a typed configuration system, you should look at Spring Configuration. \$\endgroup\$ – TorbenPutkonen Apr 23 at 7:57
  • \$\begingroup\$ Thanks, I'll have a look at how things are done there. I do not understand the first comment though: just because I have some Type-examples in my framework does not mean that any class can define their own Types to use? The Properties-object then should probably check that only allowed properties are added. \$\endgroup\$ – floxbr Apr 23 at 8:18
0
\$\begingroup\$

This approach looks more complicated than it is worth for the compile time checking you get out of it. Not allowing multiple properties of the same type makes this more difficult to use. You could use a function like the one below to avoid casting every time you retrieve an element from a map with different types of objects. This assumes that you know which type the element should be.

/**
 * Get a cast value from a map.
 * @param <T> the type of key.
 * @param <R> the type of cast value. 
 * @param key the key corresponding to a value.
 * @param map the containing the key value pairs.
 * @return the value cast to the expected type.
 */
@SuppressWarnings("unchecked")
public static <T,R> R get(T key, Map<T,? super R> map){
    Object value = map.get(key);
    return (R)value;
}

/**
 * Put a key value pair in a map.
 * @param <T> the type of key.
 * @param <R> the type of value.
 * @param key the key corresponding to a value.
 * @param value the value being mapped.
 * @param map the map.
 */
public static <T,R> void put(T key, R value, Map<T,? super R> map){
    map.put(key, value);
}
\$\endgroup\$
  • \$\begingroup\$ Thank you for your answer! Sure, as the class-information is included in my type, I could just use explicit casts everywhere. Allowing multiple elements for each type is a relatively easy change. The problem I see with you alternative is that, while they have to be present for several reason, I mostly prefer to use the one-arguement get and add/put methods over the two-argument ones (i.e. omit the "explicit" cast). \$\endgroup\$ – floxbr Apr 23 at 7:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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