java 不同类型 映射_如何使用Java泛型映射不同的值类型

java 不同类型 映射

有时,一般的开发人员会遇到这样的情况,即他必须在特定容器内映射任意类型的值。 但是,Java集合API仅提供与容器相关的参数化。 例如,这将HashMap的类型安全使用限制为单个值类型。 但是,如果您想混合苹果和梨怎么办?

幸运的是,有一个简单的设计模式允许使用Java泛型映射不同的值类型, 约书亚·布洛赫(Joshua Bloch)在他的《 有效的Java》 (第二版,项目29)中将其描述为类型安全的异构容器

最近,在有关该主题的一些不完全相同的解决方案中遇到了麻烦,这使我有了解释问题域的想法,并在这篇文章中详细说明了一些实现方面。

使用Java泛型映射不同的值类型

出于示例考虑,您必须提供某种允许将任意类型的值绑定到某些键的应用程序上下文。 使用由HashMap支持的String键的简单非类型安全实现可能如下所示:

public class Context {

  private final Map<String,Object> values = new HashMap<>();

  public void put( String key, Object value ) {
    values.put( key, value );
  }

  public Object get( String key ) {
    return values.get( key );
  }

  [...]
}

以下代码片段显示了如何在程序中使用此Context

Context context = new Context();
Runnable runnable = ...
context.put( "key", runnable );

// several computation cycles later...
Runnable value = ( Runnable )context.get( "key" );

这种方法的缺点可以在需要下浇的第六行看到。 显然,如果键值对已被其他值类型替换,则可能导致ClassCastException

Context context = new Context();
Runnable runnable = ...
context.put( "key", runnable );

// several computation cycles later...
Executor executor = ...
context.put( "key", executor );

// even more computation cycles later...
Runnable value = ( Runnable )context.get( "key" ); // runtime problem

由于相关的实现步骤可能会在您的应用程序中分散开来,因此很难跟踪此类问题的原因。 为了改善这种情况,将值不仅绑定到其键而且还绑定到其类型似乎是合理的。

我在采用这种方法的几种解决方案中看到的常见错误或多或少归结为以下Context变体:

public class Context {

  private final <String, Object> values = new HashMap<>();

  public <T> void put( String key, T value, Class<T> valueType ) {
    values.put( key, value );
  }

  public <T> T get( String key, Class<T> valueType ) {
    return ( T )values.get( key );
  }

  [...]
}

同样,基本用法可能如下所示:

Context context = new Context();
Runnable runnable = ...
context.put( "key", runnable, Runnable.class );

// several computation cycles later...
Runnable value = context.get( "key", Runnable.class );

乍一看,该代码可能会产生一种节省更多类型的幻想,因为它避免了第六行的转换。 但是运行下面的代码片段会使我们脚踏实地,因为在第十行的分配过程中我们仍然遇到ClassCastException场景:

Context context = new Context();
Runnable runnable = ...
context.put( "key", runnable, Runnable.class );

// several computation cycles later...
Executor executor = ...
context.put( "key", executor, Executor.class );

// even more computation cycles later...
Runnable value = context.get( "key", Runnable.class ); // runtime problem

那么出了什么问题呢?

首先,类型T Context#get中的向下强制转换无效,因为类型擦除将静态Object强制转换为Object从而替换了无边界参数。 但是更重要的是,该实现不使用Context#put提供的类型信息作为键。 至多它只是多余的美容效果。

类型安全的异构容器

尽管最后一个Context变体效果不佳,但它指出了正确的方向。 问题是如何正确设置密钥? 为了回答这个问题,请看一下Bloch描述的根据类型安全的异构容器模式精简的实现。

想法是使用class类型作为键本身。 由于Class是参数化类型,它使我们能够使Context类型的方法安全,而无需诉诸于T的未经检查的转换。 以这种方式使用的Class对象称为类型令牌。

public class Context {

  private final Map<Class<?>, Object> values = new HashMap<>();

  public <T> void put( Class<T> key, T value ) {
    values.put( key, value );
  }

  public <T> T get( Class<T> key ) {
    return key.cast( values.get( key ) );
  }

  [...]
}

请注意,如何使用有效的动态变体替换Context#get实现中的向下转换。 这是客户端可以使用上下文的方式:

Context context = new Context();
Runnable runnable ...
context.put( Runnable.class, runnable );

// several computation cycles later...    
Executor executor = ...
context.put( Executor.class, executor );

// even more computation cycles later...
Runnable value = context.get( Runnable.class );

这次,客户端代码将可以正常工作而不会产生类转换问题,因为不可能用具有不同值类型的一对交换某个键-值对。


有光的地方一定有阴影,有阴影的地方一定有光。 没有光就没有阴影,没有光就没有光……。

村上春树

Bloch提到了此模式的两个限制。 “首先,恶意客户端可以通过使用原始形式的类对象来轻易破坏类型安全性[...]。” 为了确保类型在运行时不变,可以在Context#put使用动态转换。

public <T> void put( Class<T> key, T value ) {
  values.put( key, key.cast( value ) );
}

第二个限制是该模式不能用于不可更改的类型(请参阅第25条,有效的Java)。 这意味着您可以通过类型安全的方式存储诸如RunnableRunnable[]类的值类型,但不能存储List<Runnable>

这是因为List<Runnable>没有特定的类对象。 所有参数化类型都引用相同的List.class对象。 因此,Bloch指出,对于这种限制没有令人满意的解决方法。

但是,如果您需要存储两个相同值类型的条目怎么办? 虽然可以想象将新的类型扩展名仅用于存储到类型安全的容器中,但这并不是最佳的设计决策。 使用自定义键实现可能是更好的方法。

多个相同类型的容器条目

为了能够存储相同类型的多个容器条目,我们可以将Context类更改为使用自定义键。 这样的密钥必须提供类型安全行为所需的类型信息,以及用于区分实际值对象的标识符。

使用String实例作为标识符的朴素键实现可能如下所示:

public class Key<T> {

  final String identifier;
  final Class<T> type;

  public Key( String identifier, Class<T> type ) {
    this.identifier = identifier;
    this.type = type;
  }
}

同样,我们使用参数化的Class作为类型信息的挂钩。 调整后的Context现在使用参数化的Key而不是Class

public class Context {

  private final Map<Key<?>, Object> values = new HashMap<>();

  public <T> void put( Key<T> key, T value ) {
    values.put( key, value );
  }

  public <T> T get( Key<T> key ) {
    return key.type.cast( values.get( key ) );
  }

  [...]
}

客户端将使用以下版本的Context

Context context = new Context();

Runnable runnable1 = ...
Key<Runnable> key1 = new Key<>( "id1", Runnable.class );
context.put( key1, runnable1 );

Runnable runnable2 = ...
Key<Runnable> key2 = new Key<>( "id2", Runnable.class );
context.put( key2, runnable2 );

// several computation cycles later...
Runnable actual = context.get( key1 );

assertThat( actual ).isSameAs( runnable1 );

尽管此代码片段有效,但实现仍有缺陷。 Key实现在Context#get用作查找参数。 使用用相同的标识符和类初始化的两个不同Key实例(一个实例与put一起使用,另一个实例与get一起使用)将在get上返回null 。 这不是我们想要的。

幸运的是,可以通过使用适当的equalsKey hashCode实现轻松解决此问题。 这使HashMap查找可以按预期工作。 最后,可以提供一种工厂创建密钥的方法,以最小化样板(与静态导入结合使用):

public static  Key key( String identifier, Class type ) {
  return new Key( identifier, type );
}

结论

'以集合API为例,泛型的正常使用将每个容器的类型参数限制为固定数量。 您可以通过将类型参数放在键而不是容器上来解决此限制。 您可以将Class对象用作此类类型安全的异构容器的键(Joshua Bloch,有效Java,第29项)。

鉴于这些结束语,除了祝您好运成功混合苹果和梨,别无他法……

翻译自: https://www.javacodegeeks.com/2015/03/how-to-map-distinct-value-types-using-java-generics.html

java 不同类型 映射

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值