【问题标题】:HashMap - contains and get methods should not be used togetherHashMap - contains 和 get 方法不应该一起使用
【发布时间】:2015-08-11 20:24:30
【问题描述】:

我在一次采访中得到了以下问题。

我得到了一个这样的字符数组:

char[] characters = {'u', 'a', 'u', 'i', 'o', 'f', 'u'};

我需要获取每个字符的不同字符和计数:

u = 3
a = 1
i = 1
o = 1
f = 1

所以我用 Java 回答了以下代码:

HashMap<Character, Integer> map = new HashMap<Character, Integer>();
int i = 1;
for (char c : characters) {             
    if (map.containsKey(c)) {
        int val = map.get(c);
        map.put(c, ++val);
    } else map.put(c, i);
}

面试官是一名解决方案架构师。他问我为什么在这里同时使用containsKey()get() 方法,并指出使用这两种方法是多余的。他的观点是什么?我在这里做错了什么?我的代码会导致性能问题等吗?

【问题讨论】:

  • 如果 HashMap 中没有这样的键,get 方法将返回 null,因此您可以直接调用它并检查结果,而不是额外调用函数,在本例中为 containsKey。这些至少是我在这个问题上的 2 美分。
  • 如果您已经知道要搜索的密钥,那为什么还要再次获取密钥?
  • 我看到的是你可以完全删除变量i,因为它在循环中是不变的。
  • 他的意思是,你可以只调用一次get,当且仅当get 的结果为非空时,键才会出现在映射中。
  • 据我所知,到目前为止所有的答案都是指特定的例子。一般来说,containsKeyget 的序列实际上 CAN 是有意义的 - 即,当键可以映射到值 null 时。 (这里并不严格相关,但我只想指出:不能盲目地将每个containsKey/get 替换为单个get,但始终要考虑是否可能 是映射中的null 值,即使在此示例中不是这种情况)

标签: java algorithm dictionary hash hashmap


【解决方案1】:

你可以像这样编写你的 for 循环 -

for (char c : characters) {             

   Integer val = map.get(c);
   if (null != val){
      map.put(c, ++val);
   } else {
      map.put(c, 1);
   }
}  

注意:我已将 int 修改为 Integer 以便我可以对照 null 检查它你声明的Integer 变量val。否则val 将是null。所以我认为你不需要使用Map.containsKey() 方法。

【讨论】:

    【解决方案2】:

    您的代码是多余的,因为 get 和 containsKey 的工作几乎相同。您可以检查 get 是否返回空值,而不是调用 containsKey。

    代码可以简化为:

    HashMap<Character, Integer> map = new HashMap<Character, Integer>();
    for (char c : characters) {   
        Integer val = map.get(c);          
        if (val == null)
            val = 0;
        map.put(c,++val);
    }
    

    【讨论】:

    • 更短:Integer val = map.get(c); map.put(c, 1 + (val == null ? 0 : val));
    • 从 java 7 开始,您可以使用菱形运算符,但不确定 OP 使用什么。
    • 你应该解释为什么 OP 的代码是多余的而你的不是。
    • @Captain Man 好吧,更多的代码比更少的代码更冗余,但我添加了一个解释让你开心。
    • 取消投票 :) 我觉得未说明的原因确实是 OP 所寻找的核心,为什么而不是如何/什么。
    【解决方案3】:
    for (char c : characters) {   
         Integer val = map.get(c);
         if(val != null){
            map.put(c, ++val); 
         }else{
            map.put(c, 1);
         }
     }
    

    这可能是最好的方法

    函数 get 和 contains 都做同样的工作...

    通过使用get函数而不是同时使用它的好处

    使用 get 函数时在此处检查 null 值。 通过避免这两个调用可以提高性能。

    注意:在这种情况下,性能可能没有任何改善,但在另一种情况下,数据量会很大。

    【讨论】:

    • 你能解释一下为什么会这样吗? (我知道,但读者可能不知道。)
    • ifelse 完成了相同的 put 操作。这会产生重复的行。您可以增加 val 并将其分配在一行中。
    • @NewUser:可以单行完成,但代码不可读。并且使用 if 和 else 使代码可读...否则您必须添加 cmets 以使其理解代码
    【解决方案4】:

    如果您想在 Map 中计算字符数,我通常会这样做。

    Map<Character, Integer> map = new HashMap();
    for (char c: cs) {
        Integer iCnt = map.get(c);
        if (iCnt ==  null) {
            map.put(c, 1);                
        } else {
            map.put(c, ++iCnt);
        }
    }
    

    Map.containsKey(key) 会从 map 中检查指定的 key,这与 Map.get(key) 非常相似。在您的代码中,您同时调用“containsKey”和“get”方法,这意味着您将通过条目两次,这可能会导致性能问题。

    【讨论】:

      【解决方案5】:

      问题在于 containskey 必须遍历 Map 的整个条目才能获取密钥(迭代 1)。下面是 containsKey 的代码。

      public boolean containsKey(Object key) {
          return getEntry(key) != null;
      }
      final Entry<K,V> getEntry(Object key) {
          if (size == 0) {
              return null;
          }
      
          int hash = (key == null) ? 0 : hash(key);
          for (Entry<K,V> e = table[indexFor(hash, table.length)];
               e != null;
               e = e.next) {
              Object k;
              if (e.hash == hash &&
                  ((k = e.key) == key || (key != null && key.equals(k))))
                  return e;
          }
          return null;
      }
      

      现在 get('') 必须再次迭代以获取键映射的值(迭代 2)。 get 的代码也调用 getEntry,如下所示。

      public V get(Object key) {
          if (key == null)
              return getForNullKey();
          Entry<K,V> entry = getEntry(key);
      
          return null == entry ? null : entry.getValue();
      }
      

      当不需要时,您不必要地遍历条目集 2 次,因此存在性能问题。 @Eran 在答案中给出了最好的方法。

      【讨论】:

      • Map.containsKey() 不会“遍历整个键集”
      • @NamshubWriter 很抱歉造成混乱,认为条目可能会造成混乱,这就是为什么使用术语密钥集修改了答案以包含正确的详细信息。
      • 这只是迭代一个桶。只有具有相同哈希值的键才会被迭代。
      【解决方案6】:

      让我们从您的代码开始,然后开始减少它。

      HashMap<Character, Integer> map = new HashMap<Character, Integer>();
      int i = 1;
      
      for (char c : characters)
      {             
          if (map.containsKey(c))
          {
              int val = map.get(c);
              map.put(c, ++val);
          }
          else map.put(c, i);
      }
      

      我要做的第一件事是使用 Java 7 菱形运算符,并删除变量 i

      Map<Character, Integer> map = new HashMap<>();
      
      for (char c : characters)
      {
          if (map.containsKey(c))
              map.put(c, ++map.get(c));
          else
              map.put(c, 1);
      }
      

      这是我的第一步,我们删除了变量i,因为它始终为1,并且在执行期间不会改变。我还精简了声明并将map.get 调用为map.put 调用。现在,当我们看到时,我们对 map 方法进行了三个调用。

      Map<Character, Integer> map = new HashMap<>();
      
      for (char c : characters)
      {
          Integer i = map.get(c);
      
          if (i == null) i = 0;
      
          map.put(c, ++i);
      }
      

      这是最好的方法,也是@Eran 在上面的答案中所说的。希望此细分对您有所帮助。

      【讨论】:

        【解决方案7】:

        架构师的意思是getcontainsKey的成本是一样的,可以累计成一张支票:

        Integer val = map.get(c);
        if (val != null) {
          ...
        } else {
          ...
        }
        

        但我想知道为什么架构师只关心这一点,因为还有更多需要改进的地方:

        • 通过接口引用对象(Effective Java 2nd Edition,Item 52
        • 从 Java 1.7 开始,您可以使用菱形运算符
        • 累积字符的自动装箱操作
        • 如果您使用AtomicInteger(或任何其他可修改的数字类)而不是Integer,您甚至可以将get 与puts 之一合并

        所以从我的角度来看,使用 HashMap 时的最佳性能将提供:

        Map<Character, AtomicInteger> map = new HashMap<>();
        for (Character c : characters) {
            AtomicInteger val = map.get(c);
            if (val != null) {
                val.incrementAndGet();
            } else {
                map.put(c, new AtomicInteger(1));
            }
        }
        

        如果您的字符范围很小(并且事先知道),您可以使用 int 数组进行计数。这将是所有可能的解决方案中最快的:

        char firstCharacter = 'a';
        char lastCharacter = 'z';
        int[] frequency = new int[lastCharacter - firstCharacter + 1];
        for (char c : characters) {
          frequency[c - firstCharacter]++;
        }
        

        【讨论】:

        • 如果您基于AtomicInteger 的解决方案更快,我会感到非常惊讶。此外,现在我们有了 Java8,这一切都可以在一行中完成......
        • 比什么更快?原来的问题?我敢打赌,正如“Most efficient way to increment a Map value in Java”已经解释的那样。我知道有更快的可修改整数实现——但这将是这里的主题。而单行代码并不意味着它更快。
        • 我还认为 AtomicInteger 应该用于其主要目的 - 作为并发实用程序。
        • 只要在 Java 中没有 MutableInteger,您就必须编写自己的实现,使用现有库(如 commons-lang)中的一个,或者使用 AtomicInteger... 不过这句话关于可修改数字只是顶部的樱桃 - 这不是我回答的主要观点。
        • 您提供的链接具有最令人震惊的基准 - 我将完全忽略它。我强烈假设 Java 8 的 Map.merge 比任何人都可以做出来的更快,因为它设计就是为了这个目的...
        【解决方案8】:

        从 Java 8 开始,您甚至可以执行以下操作:

        final Map<Character, Integer> map = new HashMap<>();
        for (char c : characters)
            map.merge(c, 1, Integer::sum);
        

        请注意,您使用此解决方案进行了大量装箱和拆箱。这应该不是问题,但最好能意识到这一点。

        上面的代码实际上做了什么(即手动装箱和拆箱):

        for (char c : characters)
            map.merge(
                Character.valueOf(c),
                Integer.valueOf(1),
                (a, b) -> Integer.valueOf(Integer.sum(a.intValue(), b.intValue())));
        

        【讨论】:

          【解决方案9】:

          嗯,我也是一名系统架构师,我认为您的代码没有任何问题,除了可能没有大括号 - 您通常应该始终使用它们。我认为这很好:

          for (char c : characters) {             
              if (map.containsKey(c)) {
                  int val = map.get(c);
                  map.put(c, ++val);
              } else {
                  map.put(c, 1);
              }
          }
          

          我个人会这样写,和你自己的版本很相似:

          for (char c : characters) {
              int val = map.containsKey(c) ? map.get(c) : 0;
              map.put(c, ++val);
          }
          

          为什么同时使用 containsKey()get() ?好吧,如果您只打算使用get(),那么您需要以某种方式进行空检查。阅读代码的其他人哪个更清楚,if (map.containsKey(c))if (val != null)?实际差别很小。

          哈希查找是O(log N),因此调用get() containsKey() 会导致两次查找而不是一次。可能会使用非常大的数据集运行,那么这将是相关的。

          最后,如果没有containtsKey() 检查,int val = map.get(c); 第一次会抛出 npe,因此您需要改用 Integer val = map.get(c);。哪个更清晰、更安全 - int valInteger val?我认为让自动装箱来做这件事并使用int val 并没有错,而且我通常尽可能在对象上使用原始类型,尽管对于intInteger 可能有很多不同的意见。

          【讨论】:

            【解决方案10】:

            另一个我还没有看到的 Java 8 解决方案:

            Character[] characters = {'u', 'a', 'u', 'i', 'o', 'f', 'u'};
            Map<Character, Integer> result = Arrays.asList(characters)
                    .stream()
                    .collect(Collectors.groupingBy(Function.identity(), Collectors.summingInt(c -> 1)));
            

            它确实需要使用盒装类型 Character,不过 -- Arrays.asList 不能很好地与 char[] 配合使用,并且 Arrays.stream() 没有 char[] 的重载。

            【讨论】:

              【解决方案11】:

              的确,答案很简单。包含方法检查元素是否存在于每次循环中的集合中。因此,集合越大,对每个下一个元素执行检查的时间就越长。包含对于散列集合很有用,其中不可能通过索引获取元素。但是对于这样的意图需要重写 hashCode 并且等于正确。在这种情况下,包含将花费 O(1)。

              【讨论】:

                猜你喜欢
                • 2015-11-25
                • 1970-01-01
                • 2012-10-31
                • 1970-01-01
                • 2017-10-25
                • 1970-01-01
                • 2018-03-22
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多