优先考虑类型安全的异构容器

栏目: Java · 发布时间: 6年前

内容简介:一般来说,开发人员偶尔会遇到这样的情形: 在一个特定容器中映射任意类型的值。然而Java 集合API只提供了参数化的容器。这限制了类型安全地使用HashMap,如单一的值类型。但如果想混合苹果和梨,该怎样做呢?考虑一个例子,你需要提供某种应用程序的上下文,它可以将特定的键绑定到任意类型的值。利用String作为键的HashMap,一个简单的、非类型安全(type safe)的实现可能是这样的:接下来的代码片段展示了怎样在程序中使用Context :

一般来说,开发人员偶尔会遇到这样的情形: 在一个特定容器中映射任意类型的值。然而 Java 集合API只提供了参数化的容器。这限制了类型安全地使用HashMap,如单一的值类型。但如果想混合苹果和梨,该怎样做呢?

考虑一个例子,你需要提供某种应用程序的上下文,它可以将特定的键绑定到任意类型的值。利用String作为键的HashMap,一个简单的、非类型安全(type safe)的实现可能是这样的:

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" );
复制代码

可以看出,这种方法的缺点是在第6行需要进行向下转型(down cast)。如果替换键值对中值的类型,显然会抛出一个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
复制代码

产生这种问题的原因是很难被跟踪到的,因为相关的实现步骤可能已经广泛分布在你的程序各个部分中。

为了改善这种情况,貌似将value和它的key、它的value都进行绑定是合理的。

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 );
复制代码

乍一看,这段代码可能会给你更类型安全的错觉,因为其在第6行避免了向下转型(down cast)。但是运行下面的代码将使我们重返现实,因为我们仍将在最后一行赋值语句处跌入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 ); 
复制代码

哪里出问题了呢?

首先,Context.get中的向下转型是无效的,因为类型擦除会使用静态转型的Object来代替无界参数(unbonded parameters)。此外更重要的是,这个实现根本就没有用到由Context.put提供的类型信息。这充其量是多此一举的美容罢了。

静态转型

Object obj; // may be an integer
if (obj instanceof Integer) {
    Integer objAsInt = (Integer) obj;
    // do something with 'objAsInt'
}
复制代码

这里使用了 instanceof 和转型操作符,这些操作符已经融入到语言当中了。对象转换的类型(这个例子中是Integer)必须是在编译期静态确定的,所以我们将这种转型称为静态转型。

类型安全的异构容器

虽然上面Context 的变种不起作用,但却指明了方向。接下来的问题是:怎样合理地参数化这个key? 为了回答这个问题,让我们先看看一个根据Bloch所描述的类型安全异构容器模式(typesafe heterogenous container pattern)的简装实现吧。

我们的想法是用key自身的class 类型作为key。因为Class 是参数化的类型,它可以确保我们使Context方法是类型安全的,而无需诉诸于一个未经检查的强制转换为T。这种形式的一个Class 对象称之为类型令牌(type token)。

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 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指出这种模式有两个局限性 。“首先,恶意的客户端可以通过以原生态形式(raw form)使用class对象轻松地破坏类型安全。”为了确保在运行时类型安全可以在Context#put中使用动态转换(dynamic cast)。

优先考虑类型安全的异构容器

Paste_Image.png

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

第二个局限 在于它不能用在不可具体化(non-reifiable )的类型中(见《Effective Java》第25项)。换句话说,你可以保存Runnable 或Runnable[],但是不能保存List<Runnable>。

这是因为List<Runnable>没有特定class对象,所有的参数化类型指的是相同的List.class 对象。因此,Bloch指出对于这种局限性没有满意的解决方案。

但是,假如你需要存储两个具有相同值类型的条目该怎么办呢?如果仅为了存入类型安全的容器,可以考虑创建新的类型扩展,但这显然不是最好的设计。使用定制的Key也许是更好的方案。

多条同类型容器条目

为了能够存储多条同类型容器条目,我们可以用自定义key改变Context 类。这种key必须提供我们类型安全所需的类型信息,以及区分不同的值对象(value objects)的标识。一个以String 实例为标识的、幼稚的key实现可能是这样的:

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 );
复制代码

虽然这个代码片段可用,但仍有缺陷。在Context#get中,Key被用作查询参数。用相同的identifier和class初始化两个不同的Key的实例,一个用于put,另一个用于get,最后get操作将返回null 。这不是我们想要的……

//译者附代码片段
Context context = new Context();
 
Runnable runnable1 = ...
Key<Runnable> key1 = new Key<>( "same-id", Runnable.class );
Key<Runnable> key2 = new Key<>( "same-id", Runnable.class );
context.put( key1, runnable1 );//一个用于put
 
context.get(key2); //另一个用于get --> return null;
复制代码

幸运的是,为Key设计合适的equals 和hashCode 可以轻松解决这个问题,进而使HashMap 查找按预期工作。最后,你可以为创建key提供一个工厂方法以简化其创建过程(与static import一起使用时有用):

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

结论

“集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这个限制。对于这种类型安全的 异构容器,可以用Class对应作为键。”(Joshua Bloch,《Effective Java》第29项)。

给出上述闭幕词,也没有什么要补充的了,除了祝愿你成功混合苹果和梨……

转自: www.importnew.com/15556.html


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Ethnography and Virtual Worlds

Ethnography and Virtual Worlds

Tom Boellstorff、Bonnie Nardi、Celia Pearce、T. L. Taylor / Princeton University Press / 2012-9-16 / GBP 21.00

"Ethnography and Virtual Worlds" is the only book of its kind - a concise, comprehensive, and practical guide for students, teachers, designers, and scholars interested in using ethnographic methods t......一起来看看 《Ethnography and Virtual Worlds》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具