关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » JAVA »如何使用Java泛型映射不同的值类型

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

    2015-04-02 00:00:00 出处:ImportNew
    分享

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

    幸运的是,有一个简单的设计模式允许使用Java泛型映射不同的值类型,Joshua Bloch在其《Effective Java》(第二版,第29项)中将其描述为类型安全的异构容器(typesafe hetereogeneous container)。

    关于这个主题,最近碰到一些不太合适的解决方案。它给了我在这篇文章中解释这个问题域,并阐述一些实现细节的想法。

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

    考虑一个例子,你需要提供某种应用程序的上下文,它可以将特定的键绑定到任意类型的值。利用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都进行绑定是合理的。

    在我看到的、按照这种方法的多种解决方案中,常见的错误或多或少归结于下面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 );

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

    哪里出问题了呢?

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

    类型安全的异构容器

    虽然上面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)。

    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项)。

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

    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    正版 Windows 11产品密钥怎么查找/查看?

    还有3个月,微软将停止 Windows 10 的更新

    Windows 10 终止支持后,企业为何要立即升级?

    Windows 10 将于 2025年10 月终止技术支持,建议迁移到 Windows 11

    Windows 12 发布推迟,微软正全力筹备Windows 11 25H2更新

    Linux 退出 mail的命令是什么

    Linux 提醒 No space left on device,但我的空间看起来还有不少空余呢

    hiberfil.sys文件可以删除吗?了解该文件并手把手教你删除C盘的hiberfil.sys文件

    Window 10和 Windows 11哪个好?答案是:看你自己的需求

    盗版软件成公司里的“隐形炸弹”?老板们的“法务噩梦” 有救了!

    IT头条

    公安部:我国在售汽车搭载的“智驾”系统都不具备“自动驾驶”功能

    02:03

    液冷服务器概念股走强,博汇、润泽等液冷概念股票大涨

    01:17

    亚太地区的 AI 驱动型医疗保健:2025 年及以后的下一步是什么?

    16:30

    智能手机市场风云:iPhone领跑销量榜,华为缺席引争议

    15:43

    大数据算法和“老师傅”经验叠加 智慧化收储粮食尽显“科技范”

    15:17

    技术热点

    商业智能成CIO优先关注点 技术落地方显成效(1)

    用linux安装MySQL时产生问题破解

    JAVA中关于Map的九大问题

    windows 7旗舰版无法使用远程登录如何开启telnet服务

    Android View 事件分发机制详解

    MySQL用户变量的用法

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。