【问题标题】:How to check for key in a Map irrespective of the case? [duplicate]无论情况如何,如何检查 Map 中的密钥? [复制]
【发布时间】:2010-06-22 11:41:51
【问题描述】:

我想知道 HashMap 中是否存在特定键,所以我使用 containsKey(key) 方法。但它是区分大小写的,即如果有一个带有名称的键并且我正在搜索名称,它不会返回 true。那么有什么方法可以让我知道而不用担心钥匙的情况吗?

谢谢

【问题讨论】:

    标签: java


    【解决方案1】:

    传统地图不行。

    “abc”是与“ABC”不同的字符串,它们的哈希码不同,它们的equals()方法将返回false。

    最简单的解决方案是在插入/检查之前简单地将所有输入转换为大写(或小写)。您甚至可以编写自己的 Map 包装器来确保一致性。

    如果您想保持键的大小写,但不区分大小写比较,您可以考虑使用TreeMap 并提供您自己的比较器,该比较器将不区分大小写进行比较。但是,在走这条路之前要好好想想,因为您最终遇到一些不可调和的不一致 - 如果有人打电话给map.put("abc", 1)然后map.put("ABC", 2),那么地图中存储的密钥是​​什么情况?你能说得通吗?如果有人将您的地图包装在标准中,您是否对此感到满意? HashMap 你会失去功能吗?或者,如果有人碰巧遍历您的密钥集,并且他们自己使用equals() 进行快速“包含”检查,您会得到不一致的结果?类似的情况还会有很多。 请注意,您这样做违反了 Map 的合同(因为密钥相等是 defined in terms of the equals() method on the keys),所以它在任何意义上确实不可行。

    维护一个严格的大写地图更容易使用和维护,并且具有实际上是合法地图实现的优势。

    【讨论】:

    • 同意最好的方法是使用大写或小写插入。
    • 不幸的是,转换为大写/小写的“土耳其测试”失败(谷歌它,看看字母“i”会发生什么)。如果国际化很重要,最好使用TreeMap,是的 - 请注意合同违规问题。 (IIRC,使用某些集合类调用 remove/retainAll 可能会产生意想不到的结果。)更好的是,尽可能使用 Guava 的 ImmutableSortedMap/Set,这样可以避免在给定自定义比较器时出现不一致的行为。
    【解决方案2】:

    使用由String#CASE_INSENSITIVE_ORDER 构造的TreeMap

    Map<String, String> map = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
    map.put("FOO", "FOO");
    
    System.out.println(map.get("foo")); // FOO
    System.out.println(map.get("Foo")); // FOO
    System.out.println(map.get("FOO")); // FOO
    

    【讨论】:

    • 这里潜伏着一个错误,虽然不常见但已损坏:对 map.keyset().removeAll(c) 的调用可能会或可能不会使用地图的比较器 - 取决于 c 的大小!见Sun bug 6394757。 (我仍然使用它。)除此之外:Guava 的 ImmutableSorted(Map|Set) 集合让您放心,严格只使用比较器,从不等于。
    • 如果您不关心复杂性,请使用 TreeMap,与 HashMap 相比,性能为 O(log n) 而为 O(1)
    【解决方案3】:

    您可以将TreeMap 与自定义的不区分大小写的Comparator(使用String.compareToIgnoreCase())一起使用

    例如:

    Map<String, Something> map = 
        new TreeMap<String, Something>(CaseInsensitiveComparator.INSTANCE);
    
    class CaseInsensitiveComparator implements Comparator<String> {
        public static final CaseInsensitiveComparator INSTANCE = 
               new CaseInsensitiveComparator();
    
        public int compare(String first, String second) {
             // some null checks
             return first.compareToIgnoreCase(second);
        }
    }
    

    更新:String 似乎已经将此 Comparator 定义为常量。

    【讨论】:

    • TreeMap 在这种情况下有点狡猾,因为很容易违反Map 的合同。 containsKey() 应该“当且仅当这个映射包含一个键 k 的映射使得 (key==null ? k==null : key.equals(k)) 时返回 true”,但在这种情况下它当然不会。如果地图以最微不足道的方式使用,这在某些时候导致不一致。
    • @Andrzej 是的,但并不比其他违反Map 合同的行为更糟​​糕,例如IdentityHashMap&lt;Object, V&gt;, TreeMap&lt;Double, V&gt;
    • @Andrzej:只要你承认它不具有Map 定义的语义就可以了。 SortedMap javadoc 中已经说明了这一点:请注意,如果排序映射要正确实现 Map 接口,则排序映射维护的顺序(无论是否提供显式比较器)必须与 equals 一致。 。如果它们不一致,它仍然有效,只是不再遵守Map
    • 有趣的是,您提到 String 定义了这个 Comparator,但不要告诉我们它是什么:String.CASE_INSENSITIVE_ORDER
    【解决方案4】:

    【讨论】:

    • commons-collections 的坏处是它们不支持泛型,但最好不要重新发明轮子。
    【解决方案5】:

    要保留Map 不变量,您可以自己制作密钥。实施明智的hashCode/equals,你就可以开始了:

    final class CaseInsensitive {
        private final String s;
        private final Local lc;
        public CaseInsensitive (String s, Locale lc) { 
            if (lc == null) throw new NullPointerException();
            this.s = s; 
            this.lc = lc; 
        }
    
        private s(){ return s == null ? null : s.toUpperCase(lc); }
    
        @Override
        public int hashCode(){ 
            String u = s();
            return (u == null) ? 0 : u.hashCode(); 
        }
    
        @Override
        public boolean equals(Object o){ 
            if (!getClass().isInstance(o)) return false;
            String ts = s(), os = ((CaseInsensitive)other).s();
            if (ts == null) return os == null;
            return ts.equals(os);
        }
    }
    
    // Usage:
    Map<CaseInsensitive, Integer> map = ...;
    map.put(new CaseInsensitive("hax", Locale.ROOT), 1337);
    assert map.get(new CaseInsensitive("HAX", Locale.ROOT) == 1337;
    

    注意:并不是全世界的每个人都同意什么是大写的——一个著名的例子是土耳其语中“i”的大写版本是“İ”,而不是“I ”。

    【讨论】:

    • 添加一个Locale 参数,我会支持这个
    • 完成。我什至在早期版本中也有一个...
    • 小问题(可能还很幼稚): 1. 为什么不使用 ts.s().equalsIgnoreCase()? 2. 为什么不用 instanceof 操作符?
    • @Nivas,没有特别的原因,真的。代码就是这样出来的。
    【解决方案6】:

    Map 使用equalshashCode 来测试密钥是否相等,您不能为String 覆盖这些。您可以做的是定义您自己的包含字符串值的 Key 类,但以不区分大小写的方式实现 equalshashCode

    【讨论】:

      【解决方案7】:

      最简单的方法是在插入和查找时自行折叠钥匙。即

      map.put(key.toLowerCase(), value);
      

      map.get(key.toLowerCase());
      

      你可以继承例如HashMap 来获得你自己的类,如果你想自动完成的话。

      【讨论】:

        【解决方案8】:

        创建你自己的字符串类包装器,实现equals和hashcode,使用它作为hashmap中的键:

           class MyStringKey
           {
              private String string;
              public String getString()
              {
                 return string;
              }
              public void setString(String string)
              {
                 this.string = string;
              }
        
              public boolean equals(Object o)
              {
                 return o instanceof MyStringKey && this.equalsIgnoreCase(((MyStringKey)o).getString());
              }
        
              public boolean hashCode()
              {
                 return string.toLowerCase().hashcode(); //STRING and string may not have same hashcode
              }
           }
        

        【讨论】:

        • 让密钥不可变可能会更好
        【解决方案9】:

        试图提供符合您问题要求的答案“不打扰密钥的大小写”...

        如果您在很多很多地方添加到地图中,这个答案可能会很乏味。在我的示例中,它仅在用户创建新角色时发生(在我的游戏中)。以下是我的处理方式:

        boolean caseInsensitiveMatch = false;
        for (Map.Entry<String, Character> entry : MyServer.allCharacterMap.entrySet()) {
            if (entry.getKey().toLowerCase().equals(charNameToCreate.toLowerCase())){
                caseInsensitiveMatch = true;
                break;
            }
        }
        

        当然,这需要遍历我的大型 ConcurrentHashMap,但对我有用。

        【讨论】:

          猜你喜欢
          • 2013-10-09
          • 1970-01-01
          • 2011-04-20
          • 2010-09-11
          • 1970-01-01
          • 2018-06-09
          • 1970-01-01
          • 2016-11-06
          • 2020-09-02
          相关资源
          最近更新 更多