在 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”;相反,“星期一”是。我们怎样才能使信息更准确?在这种情况下,我们可以利用 TreeSet
是 NavigableSet
的事实创建一个帮助函数(实际上,类似的技巧适用于 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<Foo, Foo>
。没有比这更好的了,对不起,如果您希望得到更多。没有更好的原因是因为实际上没有人需要更好的东西,这对于不太标准的Map
实现来说甚至没有意义,例如那些甚至不保留其密钥身份的人。
叹息。不幸的。似乎 API 中有一个漏洞,有一天可以填补。
旁注:equals()
的合同在 java 文档中明确说明。没有要求equals()
捕获您可能关心的所有内容,并且确实有时有充分的理由不这样做(我承认非常罕见,但确实发生了)。我可能很关心我是否有一个 ArrayList 或一个 LinkedList(例如,如果我正在考虑是否进行二进制搜索),但根据 List 合同,任何两个具有相同元素且顺序相同的列表必须是认为相等()。
请注意,您可以创建不区分大小写的HashMap
/HashSet
,方法是使用具有适当hashCode
/equals
实现的特殊键类型(可能是字符串的包装器),其中将引入对问题的问题;您不能在 Set
中查询最初用于创建密钥的字符串。但值得注意的是,从 Java 2 开始,参考实现(现在的 OpenJDK 以及在它之上构建的所有实现)都将 HashSet
实现为 HashMap
的包装器。所以使用HashMap
来解决这个问题不会改变性能特征。以上是关于在 Java 映射或集合中逐键查找的主要内容,如果未能解决你的问题,请参考以下文章