Java类与动态字段

6
我正在寻找一种巧妙的方式来构建动态Java类,即在运行时可以添加/删除字段的类。使用场景:我有一个编辑器,用户应该能够在运行时添加模型的字段,甚至可能在运行时创建整个模型。
一些设计目标:
- 如果可能,自定义代码(该代码来自扩展模型的插件)在处理动态字段时应无需强制转换即可安全使用。 - 良好的性能(能否击败HashMap?也许可以在设置期间使用数组并为字段分配索引?) - 字段“重用”(即如果您在多个地方使用相同类型的字段,则应该可以定义它一次然后重复使用它)。 - 依赖于其他字段值的计算字段 - 当字段更改值时应发送信号(不一定通过Beans API) - “自动”父子关系(当您向父项添加子项时,子项中的父指针应该免费设置)。 - 易于理解 - 易于使用
请注意,这是一个“跳出思维定势”的问题。我会在下面发布一个示例以激发您的灵感 :-)
5个回答

2
显而易见的答案是使用HashMap(如果您关心字段的顺序,则使用LinkedHashMap)。然后,您可以通过get(String name)set(String name, Object value)方法添加动态字段。
此代码可以在一个公共基类中实现。由于只有很少的方法,如果您需要扩展其他内容,则使用委托也很简单。
为避免类型转换问题,您可以使用类型安全的对象映射
    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 );

这里的关键在于包含类型信息的密钥:
TypedMapKey<String> KEY1 = new TypedMapKey<String>( "key1" );
TypedMapKey<List<String>> KEY2 = new TypedMapKey<List<String>>( "key2" );

性能将会不错。

通过使用相同的值类型或通过扩展类型安全对象映射的键类来实现字段重用并添加额外的功能。

可以使用第二个存储Future实例的映射来实现计算字段。

由于所有操作都在两个(或至少几个)方法中完成,因此发送信号很简单,可以按照任何喜欢的方式进行。

要实现自动父/子处理,请在子项的“设置父项”信号上安装信号侦听器,然后将子项添加到新的父项中(如果必要,则从旧父项中删除)。

由于不使用框架且不需要技巧,因此生成的代码应该非常干净且易于理解。不使用字符串作为键还有一个额外的好处,即人们不会在代码中滥用字符串文字。


这是我的解决方案。我相信你可以做得更好! :-) - Aaron Digulla
  1. 当键不是 TypedMapKey 的实例时,您无法在 Map 中存储任何内容。
  2. 如果值的类型与键的类型不匹配,则在调用“set()”时会出现编译错误。因此,“get()”的结果除了正确类型之外不能返回任何东西(当然,您可以使用反射将值放入映射中,或者将映射强制转换为 <Object,Object> 等类似方法)。
- Aaron Digulla
2
@Aaron:你的 map 不是类型安全的,你只是通过使用未检查的转换来愚弄编译器。 例如,如果你声明 TypedMapKey<List<String>> KEY2 = new TypedMapKey<List<String>>( "key1" );,代码会编译通过,但是你会得到一个运行时类转换异常。 - Kru
1
如果我添加TypedMapKey<Long> KEY666 = new TypedMapKey<Long>("key1"); Long l = map.get(KEY666);会怎么样?我认为它会尝试将值“Hallo”强制转换为Long并失败。如果是这样,那么您的类型映射并不真正是类型安全的。 - emory
@Chris:没错,如果你定义了两个具有相同值的键,就会发生冲突。感谢您的建议,解决方案很简单:去掉String参数或者至少不要在hashCode()中使用它,这样每个TypedMapKey实例都是唯一的。我已经更新了我的博客。 - Aaron Digulla
显示剩余2条评论

2
如果可能的话,对于处理动态字段的自定义代码,请尽量避免使用强制类型转换来保证类型安全(这些代码将来自于以不可预见的方式扩展模型的插件)。
据我所知,这是不可能的。只有在使用静态类型时,您才能获得不需要类型转换的类型安全性。静态类型意味着在编译时已知方法签名(在类或接口中)。
最好的做法是使用一个接口,其中包含一堆方法,如String getStringValue(String field)int getIntValue(String field)等。当然,您只能为预定的一组类型执行此操作。任何类型不属于该集合的字段都需要进行类型转换。

如果您查看我的类型安全对象映射,您会发现实际上可以在不使用强制转换和每种类型一个方法的情况下同时实现动态性和类型安全。当我想到这个想法时,我很好奇是否有更好的解决方案。因此,我提出了这个问题。 - Aaron Digulla
是的,但是每个具有“TypedMapKey <T>”类型的属性都必须静态声明以实现静态类型。就像我所说的,在生成的代码中有一个隐藏的类型转换。因此,我不明白你提出的动态字段没有类型转换的方式。 - Stephen C
澄清一下:地图的用户不必一直进行强制转换,而且代码自动完成会插入正确的类型。 - Aaron Digulla
抱歉,你无法说服我这样做可以让你获得类型安全的动态字段而不需要类型转换。字段的名称必须是静态的,而值之所以具有类型安全性,是因为存在隐藏的类型转换。这是一种欺骗。 - Stephen C

1
你可以使用字节码操作库来实现。这种方法的缺点是需要创建自己的类加载器以动态加载类中的更改。

1

我做的几乎一样,这是一个纯Java解决方案:

  1. 用户生成自己的模型,存储为JAXB模式。
  2. 模式在Java类中编译并即时存储在用户jar中。
  3. 所有类都被强制扩展一个“根”类,在其中可以放置任何额外的功能。
  4. 使用“模型更改”侦听器实现适当的类加载器。

说到性能(在我的情况下很重要),你几乎无法击败这个解决方案。可重用性与XML文档相同。


只是为了确保我理解正确:您在运行时生成Java字节码吗?如果是这样,您能详细说明一下吗?它有多稳定/弹性?关于#3:通常,代码生成器会生成用户必须扩展以添加功能的基类。您似乎是反过来做的。为什么?关于#4:您如何确保没有人保留对过时模型类的引用? - Aaron Digulla
@Aaron Digulla,是的,在运行时,这里有相关的帖子:https://dev59.com/g1LTa4cB1Zd3GeqPXBbU和https://dev59.com/rVLTa4cB1Zd3GeqPfP28。在我的情况下,只有getter/setter很重要,因为所有逻辑都由规则引擎或“根类”管理,对于所有用户都是通用的。至于引用,用户JAR包是隔离的,不能在运行时使用其他人的JAR包。一个人只能交换模式并根据它重新构建自己的类,这是用户的责任。 - andbi

1

所以基本上你正在尝试创建一种新的对象模型,具有更多动态属性,有点像动态语言?

值得看看Rhino的源代码(即Java中实现的Javascript),它面临着在Java中实现动态类型系统的类似挑战。

我猜想,从我的经验来看,内部HashMap最终对你的目的最好。

我写了一个小游戏(Tyrant - GPL source available),使用了类似的动态对象模型,其中包含HashMaps,它运行良好,性能也不是问题。我在get和set方法中使用了一些技巧,以允许动态属性修改器,我相信你可以做同样的事情来实现你的信号和父/子关系等。

[编辑]请参见BaseObject的源代码如何实现。


我已经添加了您源代码的链接,请检查一下。谢谢 :-) - Aaron Digulla
是的,这就是实现此行为的主类。实际的游戏对象是Thing的实例,它继承自BaseObject。 - mikera

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接