在 Java 映射或集合中逐键查找

Posted

技术标签:

【中文标题】在 Java 映射或集合中逐键查找【英文标题】:lookup key by key in Java map or set 【发布时间】:2021-12-23 12:47:09 【问题描述】:

在包括 Java 在内的大多数语言中,都有一个类似于 java.util.Map 的 API,它旨在简化循环一个值,给定映射到它的键。但是并不总是有一种方便的方法来查找密钥,给定密钥(我很确定 Python 使它变得困难,C++ 使它变得容易(只需要一个迭代器),这个问题是关于 Java 的,我怀疑它是和 Python 一样糟糕)。起初这听起来很愚蠢:为什么需要查找已有的密钥?但是考虑这样的事情(下面的例子使用Set而不是Map,但同样的想法):

TreeSet<String> dictionary = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
dictionary.add("Monday"); // populate dictionary
String word = "MONDAY"; // user input, or something
if(dictionary.contains(word)) System.out.println(word + " already in dictionary");

上面的代码 sn-p 将打印MONDAY already in dictionary。这当然是错误的,因为字典里没有“MONDAY”;相反,“星期一”是。我们怎样才能使信息更准确?在这种情况下,我们可以利用 TreeSetNavigableSet 的事实创建一个帮助函数(实际上,类似的技巧适用于 SortedSet,虽然它不太方便。):

String lookup(NavigableSet<String> set, String key) 
    assert set.contains(key) : key + " not in set";
    return set.floor(key);

现在我们可以修复之前代码sn -p的最后一行了:

if(dictionary.contains(word)) System.out.println(lookup(word) + " already in dictionary");

这将打印正确的内容。但是现在让我们尝试一个带有哈希集的示例:

import java.util.HashSet;
/** Maintains a set of strings; useful as a replacement for String.intern() */
class StringInterner 
    private final HashSet<String> set = new HashSet<>();
    /** use this instead of String.intern() */
    String intern(String s) 
         if(!set.contains(s)) 
             s.add(s);
             return s;
         
         for(String str : set) // linear scan!!
             if(str.equals(s)) return str;
         throw new AssertionError("something went very wrong");
    

上面的代码使用线性扫描来查找它已经知道的东西。请注意HashSet 可以很容易地给我们我们正在寻找的东西,因为它需要能够做到这一点只是为了实现contains()。但是它没有API,所以我们甚至不能问这个问题。 (实际上,HashMap 有一个名为 getNode 的内部方法,这几乎是我们想要的,但它是内部的。)在这种情况下,一个简单的解决方法是使用映射而不是集合:我们不是 set.add(s)可以改为使用map.put(s,s)。但是如果我们已经在使用地图了,因为我们已经有了想要与我们的键关联的数据呢?然后我们可以使用两个映射,并小心地保持它们同步,或者在我们的映射中存储一个大小为 2 的元组作为“值”,其中元组中的第一项只是映射键。这两种解决方案似乎都不必要地笨拙。

有没有更好的办法?

【问题讨论】:

使用 hashmap... 遍历 EntrySet。 当然,您可以随时进行线性扫描。但是哈希映射的好处是它们通常具有快速查找功能。似乎很遗憾失去它并使用暴力扫描。 【参考方案1】:

有没有更好的办法?

不,没有。

对于 HashMaps,这无关紧要,因为您的“两个等效键”参数对 HashMap 没有意义。这两个键总是必须是equals,就Java 而言,这意味着它们应该在任何方面都可以替代。

【讨论】:

并非如此。有时您关心对象身份(也就是说,有时您关心事物是否为==,而不仅仅是equals())。例如,String.intern() 的正确实现必须尊重对象身份。 (要了解原因,请考虑实习字符串的目的:您通常需要类似“如果我以前见过这样的字符串,丢弃这个并使用之前的字符串”这样的逻辑。这可以节省内存,因为您避免存储多个相等的字符串。) 是的。对于类似内部人员的事情,您能做的最好的事情是Map&lt;Foo, Foo&gt;。没有比这更好的了,对不起,如果您希望得到更多。没有更好的原因是因为实际上没有人需要更好的东西,这对于不太标准的Map 实现来说甚至没有意义,例如那些甚至不保留其密钥身份的人。 叹息。不幸的。似乎 API 中有一个漏洞,有一天可以填补。 旁注:equals() 的合同在 java 文档中明确说明。没有要求equals() 捕获您可能关心的所有内容,并且确实有时有充分的理由不这样做(我承认非常罕见,但确实发生了)。我可能很关心我是否有一个 ArrayList 或一个 LinkedList(例如,如果我正在考虑是否进行二进制搜索),但根据 List 合同,任何两个具有相同元素且顺序相同的列表必须是认为相等()。 请注意,您可以创建不区分大小写的HashMap/HashSet,方法是使用具有适当hashCode/equals 实现的特殊键类型(可能是字符串的包装器),其中将引入对问题的问题;您不能在 Set 中查询最初用于创建密钥的字符串。但值得注意的是,从 Java 2 开始,参考实现(现在的 OpenJDK 以及在它之上构建的所有实现)都将 HashSet 实现为 HashMap 的包装器。所以使用HashMap 来解决这个问题不会改变性能特征。

以上是关于在 Java 映射或集合中逐键查找的主要内容,如果未能解决你的问题,请参考以下文章

Java中的集合框架(上)

Java笔记-泛型与集合框架

Java 集合快速失败异常

Java中的集合框架(中)

Java 之集合框架 中(10)

JAVA集合框架之Map