views:

89

answers:

3

I'm looking for clever ways to build dynamic Java classes, that is classes where you can add/remove fields at runtime. Usage scenario: I have an editor where users should be able to add fields to the model at runtime or maybe even create the whole model at runtime.

Some design goals:

  • Type safe without casts if possible for custom code that works on the dynamic fields (that code would come from plugins which extend the model in unforeseen ways).
  • Good performance (can you beat HashMap? Maybe use an array and assign indexes to the fields during setup?)
  • Field "reuse" (i.e. if you use the same type of field in several places, it should be possible to define it once and then reuse it).
  • Calculated fields which depend on the value of other fields
  • Signals should be sent when fields change value (no necessarily via the Beans API)
  • "Automatic" parent child relations (when you add a child to a parent, then the parent pointer in the child should be set for "free").
  • Easy to understand
  • Easy to use

Note that this is a "think outside the circle" question. I'll post an example below to get you in the mood :-)

A: 

The obvious answer is to use a HashMap (or a LinkedHashMap if you care for the order of fields). Then, you can add dynamic fields via a get(String name) and a set(String name, Object value) method.

This code can be implemented in a common base class. Since there are only a few methods, it's also simple to use delegation if you need to extend something else.

To avoid the casting issue, you can use a type-safe object map:

    TypedMap map = new TypedMap();

    String expected = "Hallo";
    map.set( KEY1, expected );
    String value = map.get( KEY1 ); // Look Ma, no cast!
    assertEquals( expected, value );

    List<String> list = new ArrayList<String> ();
    map.set( KEY2, list );
    List<String> valueList = map.get( KEY2 ); // Even with generics
    assertEquals( list, valueList );

The trick here is the key which contains the type information:

TypedMapKey<String> KEY1 = new TypedMapKey<String>( "key1" );
TypedMapKey<List<String>> KEY2 = new TypedMapKey<List<String>>( "key2" );

The performance will be OK.

Field reuse is by using the same value type or by extending the key class of the type-safe object map with additional functionality.

Calculated fields could be implemented with a second map that stores Future instances which do the calculation.

Since all the manipulation happens in just two (or at least a few) methods, sending signals is simple and can be done any way you like.

To implement automatic parent/child handling, install a signal listener on the "set parent" signal of the child and then add the child to the new parent (and remove it from the old one if necessary).

Since no framework is used and no tricks are necessary, the resulting code should be pretty clean and easy to understand. Not using String as keys has the additional benefit that people won't litter the code with string literals.

Aaron Digulla
This is my solution. I'm sure you can do better! :-)
Aaron Digulla
I don't understand. Doesn't the type of the type-safe object map need to be known at compile time? And since this approach relies on generics, isn't there a *hidden* typecast going on anyway ... that can result in a ClassCastException if you use "unsafe" typing?
Stephen C
1. You can't store anything in the map when the key isn't an instance of TypedMapKey. 2. If the type of the value doesn't match the type of the key, you get a compile error when you call `set()`. Hence the result of `get()` can't return anything but the correct type (unless you use reflection to put values in the map, of course or your cast the map to <Object,Object> and hacks like that).
Aaron Digulla
@Aaron: Your map is not type-safe, you are just fooling the compiler by using unchecked casts.For example, if you declare `TypedMapKey<List<String>> KEY2 = new TypedMapKey<List<String>>( "key1" );`, the code will compile but you will get a runtime class cast exception.
Chris
What if I add TypedMapKey<Long> KEY666 = new TypedMapKey<Long>( "key1" ); Long l = map . get ( KEY666 ) ;I think it will try to return the value "Hallo" cast as a Long and fail. If so, then your type map isn't really type safe.
emory
@Chris: That's true; if you define two keys with the same value, you get a collision. Thanks for the input, the solution is simple: Get rid of the String parameter or at least I shouldn't use it in hashCode(), so that every instance of `TypedMapKey` is unique. I've updated my blog.
Aaron Digulla
Or to put it another way: The TypedMap in my Blog post must *wrap* an existing map. In the context of this question, I could define a map which has TypedMapKey as the key (instead of a String) and use that. As long as you can't define two instances of TypedMapKey which are equal, this is safe. The wrapping implementation is still much safer and easier to use that a plain HashMap.
Aaron Digulla
+1  A: 

So basically you're trying to create a new kind of object model with more dynamic properties, a bit like a dynamic language?

Might be worth looking at the source code for Rhino (i.e. Javascript implemented in Java), which faces a similar challenge of implementing a dynamic type system in Java.

Off the top of my head, I suspect you will find that internal HashMaps ultimately work best for your purposes.

I wrote a little game (Tyrant - GPL source available) using a similar sort of dynamic object model featuring HashMaps, it worked great and performance was not an issue. I used a few tricks in the get and set methods to allow dynamic property modifiers, I'm sure you could do the same kind of thing to implement your signals and parent/child relations etc.

[EDIT] See the source of BaseObject how it is implemented.

mikera
I've added a link to your source. please check it. Thanks :-)
Aaron Digulla
Yep, that's the main class that implements this behaviour. The actual game objects are instances of Thing which extends BaseObject.
mikera
+2  A: 

Type safe without casts if possible for custom code that works on the dynamic fields (that code would come from plugins which extend the model in unforeseen ways)

AFAIK, this is not possible. You can only get type-safety without type casts if you use static typing. Static typing means method signatures (in classes or interfaces) that are known at compile time.

The best you can do is have an interface with a bunch of methods like String getStringValue(String field), int getIntValue(String field) and so on. And of course you can only do that for a predetermined set of types. Any field whose type is not in that set will require a typecast.

Stephen C
If you look at my type-safe object map, you'll see it's in fact possible to be dynamic and type safe - without casts and without one method per type. When that idea struck me, I was curious if there was an even better solution. Hence my question.
Aaron Digulla
Yes, but each of the attributes with a `TypedMapKey<T>` type must be statically declared to achieve static typing. And as I said, there is a hidden typecast in the generated code anyway. Hence, I don't see how what you proposed is dynamic fields without typecasts.
Stephen C
To clarify: The user of the map doesn't have to cast all the time plus code-completion will insert the correct type.
Aaron Digulla
Sorry, you won't convince me that this gives you typesafe dynamic fields w/o typecasts. The names of the fields have to be static, and the values are only typesafe because of the hidden typecasts. Its a sham.
Stephen C