Java 8中的集合映射:从集合中删除元素并在为空时删除条目

11

我有一个值为集合类型的地图。给定一个键,我想要删除集合中的一个元素并返回它,但如果集合为空,我也想要删除对应的键值对。在Java 8的众多新的Map方法中,是否有一种简短的方法可以做到这一点?

这里有一个简单的例子(我使用了一个Stack,但也可以是List、Set等等)。为了举例,假设已经检查过地图包含该键。

public static String removeOne(Map<Integer, Stack<String>> map, int key) {
    Stack<String> stack = map.get(key);
    String result = stack.pop();
    if(stack.isEmpty()){
        map.remove(key);
    }
    return result;
}

我尝试着做了一些类似于

map.compute(1, (k, v) -> {v.pop(); return v.size() == 0 ? null : v;});

但是虽然它确实会在空时删除条目,但我不知道如何获取pop()返回的值。


3
这里实际上没有必要使用streams/lambda - Nicholas K
1
@NicholasK 我认为唯一美妙的一点就是映射到 null 并且自动删除 Entry,否则我会同意。 - Eugene
我不确定这个是否够通用,但是类似这样的东西怎么样? - Naman
6个回答

5

好的,我想有一种方法可以解决这个问题,虽然它比你现在使用的方式更丑陋:

public static String removeOne(Map<Integer, Stack<String>> map, int key) {
    String[] removed = new String[1];
    map.compute(key, (k, v) -> {
        removed[0] = v.pop();
        return v.size() == 0 ? null : v;
    });
    return removed[0];
}

问题在于merge / compute等方法返回的是该集合的,而在您的情况下,该值是一个Stack/Set/List,而不是该集合中的单个元素。

2
哈哈哈,我其实也考虑过这个解决方案,但正如你所说,它真的很丑陋。 - Ricola

4

或者你可以使用size重写它:

public static String removeOne(Map<Integer, Stack<String>> map, int key) {
    return map.get(key).size() == 1 ? map.remove(key).pop() : map.get(key).pop();
}

1
这种方法的遗憾之处在于它仅在映射中存在该条目时才起作用。我知道OP已经说过我们可以假设它已经存在,但我只是想说添加一个键检查会使代码变得更丑陋,因为在这种情况下该怎么办?返回null?也许...我看到的另一个缺点是你使用了两次get,不过这很容易解决。无论如何,对于一个好而简洁的答案还是要点赞的。 - fps
1
嗯,是的,空值检查可能会使情况更加混乱,但是功能实现以回答在这种情况下该怎么做也需要更改OP现有的代码。由于这可能会导致这些情况下的NPE。使用get是我怀疑性能是否会受到影响的部分。但是,O(1)的查找使我感到忽略了它,此外,我认为如果我存储这样的值,不会有类似的空间成本吗?@FedericoPeraltaSchaffner - Naman

2
有没有一种使用Java 8的新Map方法来简化代码的方式?
截至到JDK8,没有任何新的方法能够在可读性或效率方面提高您的代码。如果您只是为了练习而这样做,我可以理解您想要缩短代码的想法(如果可能)。但是,在生产代码中应避免代码冗长,而应该采用最易读和易于维护的方法;长度不重要。
您现在的方法已经很好了。

2

Guava的Multimap可以为您处理删除空集合的逻辑。您可以使用两行代码获得与您方法等效的行为:

public static String removeOne(ListMultimap<Integer, String> map, int key) {
    List<String> stack = map.get(key);
    return stack.remove(stack.size() - 1);
}

如果给定的键没有条目,您现有的解决方案和上述解决方案都会抛出异常。您可以选择更改代码来处理此问题:

public static String removeOne(ListMultimap<Integer, String> map, int key) {
    List<String> stack = map.get(key);
    if (stack.isEmpty()) {
        return null;
    }
    return stack.remove(stack.size() - 1);
}

当然,您可以将其变成通用的:
public static <K, V> V removeOne(ListMultimap<K, V> map, K key) {
    List<V> stack = map.get(key);
    if (stack.isEmpty()) {
        return null;
    }
    return stack.remove(stack.size() - 1);
}

谢谢你提供关于外部库的提示!顺便说一下,它可以放在一行中:map.get(key).pop(); - Ricola

1
我完全同意@NicholasK的观点。这里没有使用任何流或lambda的理由。
你的方法相当不错。唯一需要补充的是使其通用:
public static <K, E, C extends Collection<E>> E removeOne(Map<K, C> map, K key) {
    C col = map.get(key);
    Iterator<E> it = col.iterator();
    E e = it.next();
    it.remove();
    if (!it.hasNext()) {
        map.remove(key);
    }
    return e;
}

这种方法适用于返回有效迭代器的任何集合(映射值)。

我没有找到在集合为null时删除条目的部分。此外,您必须在所有情况下执行it.remove(),为什么要进行!it.hasNext()检查? - Ricola
!it.hasNext() 检查我们移除一个元素后集合是否为空,因为它是由 OP 在他自己的示例中实现的。 - ETO
我是OP,我可以百分之百确定这个代码并不做同样的事情。我刚刚测试过了。 正如我所说,你从来没有把空的集合从Map中移除。我认为你的意思是:if (!it.hasNext()) { map.remove(key); } it.remove(); - Ricola
好的,我改正了。现在更新后的版本应该对于任何集合类型作为映射值执行与您示例相同的操作。 - ETO

-1
/* quite ugly
String rv = Optional.ofNullable(map.get(1)).map(stack -> {
            if (!stack.isEmpty()) {
                String v = stack.pop();
                if (stack.isEmpty()) {
                    map.remove(1);
                }
                return v;
            }
            return null;
        }).orElse(null);
*/ 

@Test
public void test() {
    {
        Map<Integer, Stack<String>> map = new HashMap<>();
        Stack<String> s = new Stack<String>();
        s.addAll(Arrays.asList("a", "b"));
        map.put(1, s);
        String rv = Optional.ofNullable(map.get(1)).map(stack -> {
            if (!stack.isEmpty()) {
                String v = stack.pop();
                if (stack.isEmpty()) {
                    map.remove(1);
                }
                return v;
            }
            return null;
        }).orElse(null);
        Assert.assertEquals("b", rv);
        Assert.assertEquals(1, map.get(1).size());
        Assert.assertEquals("a", map.get(1).iterator().next());
    }
    {
        Map<Integer, Stack<String>> map = new HashMap<>();
        Stack<String> s = new Stack<String>();
        s.add("a");
        map.put(1, s);
        String rv = Optional.ofNullable(map.get(1)).map(stack -> {
            if (!stack.isEmpty()) {
                String v = stack.pop();
                if (stack.isEmpty()) {
                    map.remove(1);
                }
                return v;
            }
            return null;
        }).orElse(null);
        Assert.assertEquals("a", rv);
        Assert.assertNull(map.get(1));
    }
}

4
您需要先执行“pop”操作,然后再检查它是否为空。请注意保持原意,使语言更加通俗易懂,不提供解释或其他额外信息。 - Eugene
它完全按照他的要求执行,并且使用了所有新的Java 8功能。 - Jurgen De Landsheer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接