6

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 :-)

casperOne
  • 73,706
  • 19
  • 184
  • 253
Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820

5 Answers5

2

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
  • 321,842
  • 108
  • 597
  • 820
  • This is my solution. I'm sure you can do better! :-) – Aaron Digulla Jun 27 '10 at 11:30
  • 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 Jun 27 '10 at 11:42
  • 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 and hacks like that). – Aaron Digulla Jun 27 '10 at 11:46
  • 2
    @Aaron: Your map is not type-safe, you are just fooling the compiler by using unchecked casts. For example, if you declare `TypedMapKey> KEY2 = new TypedMapKey>( "key1" );`, the code will compile but you will get a runtime class cast exception. – Kru Jun 27 '10 at 12:18
  • 1
    What if I add TypedMapKey KEY666 = new TypedMapKey( "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 Jun 27 '10 at 13:06
  • @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 Jun 28 '10 at 09:47
  • 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 Jun 28 '10 at 09:58
2

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
  • 698,415
  • 94
  • 811
  • 1,216
  • 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 Jun 27 '10 at 11:46
  • Yes, but each of the attributes with a `TypedMapKey` 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 Jun 27 '10 at 12:53
  • 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 Jun 28 '10 at 09:45
  • 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 Jun 28 '10 at 10:01
1

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.

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
mikera
  • 105,238
  • 25
  • 256
  • 415
1

You can use the bytecode manipulation libraries for it. Shortcoming of this approach is that you need to do create own classloader to load changes in classes dynamically.

MaSEL
  • 505
  • 1
  • 5
  • 20
1

I do almost the same, it's pure Java solution:

  1. Users generate their own models, which are stored as JAXB schema.
  2. Schema is compiled in Java classes on the fly and stored in user jars
  3. All classes are forced to extend one "root" class, where you could put every extra functionality you want.
  4. Appropriate classloaders are implemented with "model change" listeners.

Speaking of performance (which is important in my case), you can hardly beat this solution. Reusability is the same of XML document.

andbi
  • 4,426
  • 5
  • 45
  • 70
  • Just to make sure I understood correctly: You generate Java bytecode at runtime? If so, can you elaborate on it? How stable/resilient is it? Re #3: Usually, code generators generate base classes which the user has to extend to add functionality. You seem to do it the other way around. Why? Re #4: How do you make sure that no one keeps a reference to an outdated model class? – Aaron Digulla Mar 28 '11 at 09:18
  • @Aaron Digulla, yes, on the fly, here are related posts: http://stackoverflow.com/questions/4248099/jaxb-dynamically-generate-java-sources-without-xjc and http://stackoverflow.com/questions/4556179/how-to-force-schema-compiled-classes-to-extend-specific-class-outside-schema . In my case only getters/setters are important, cause all the logic is managed either by rule engine or in 'root class', common for all users. As for references, user jars are isolated, one can not use other's jars at runtime. One can only exchange schema and rebuild it's own classes upon it, which is user's concern. – andbi Mar 28 '11 at 09:47