最近,团队里边一个兄弟突然叫我:快来看,有个奇怪的事情,无法解释… 跑过去一看,是这么一段代码:
private static class Person { private int age; private String name; public Person(int age, String name) { this.age = age; this.name = name; } public void setAge(int age) { this.age = age; } @Override public String toString() { return name + ":" + age; } } @Test public void test_collections2_filter() { Person lxy = new Person(35, "lxy"); Person xhf = new Person(34, "xhf"); Person nws = new Person(31, "nws"); List<Person> names = Lists.newArrayList(lxy, xhf, nws); Collection<Person> personAgeOver30 = Collections2.filter(names, p -> p.age > 30); System.out.println(personAgeOver30);//[lxy:35, xhf:34, nws:31] nws.setAge(25); System.out.println(personAgeOver30);//[lxy:35, xhf:34] }
确实是比较奇怪,personAgeGt30中的元素怎么会少了一个呢? 本着任何表现奇怪的程序都有其背后原因的指导思想,打开了Guava Collections2类的源代码。 其实,源代码的注释已经解释得非常清楚了:returned collection is a live view of {@code unfiltered}; changes to one affect the other.
/** * Returns the elements of {@code unfiltered} that satisfy a predicate. The returned collection is * a live view of {@code unfiltered}; changes to one affect the other. * * <p>The resulting collection's iterator does not support {@code remove()}, but all other * collection methods are supported. When given an element that doesn't satisfy the predicate, the * collection's {@code add()} and {@code addAll()} methods throw an {@link * IllegalArgumentException}. When methods such as {@code removeAll()} and {@code clear()} are * called on the filtered collection, only elements that satisfy the filter will be removed from * the underlying collection. * * <p>The returned collection isn't threadsafe or serializable, even if {@code unfiltered} is. * * <p>Many of the filtered collection's methods, such as {@code size()}, iterate across every * element in the underlying collection and determine which elements satisfy the filter. When a * live view is <i>not</i> needed, it may be faster to copy {@code Iterables.filter(unfiltered, * predicate)} and use the copy. * * <p><b>Warning:</b> {@code predicate} must be <i>consistent with equals</i>, as documented at * {@link Predicate#apply}. Do not provide a predicate such as {@code * Predicates.instanceOf(ArrayList.class)}, which is inconsistent with equals. (See {@link * Iterables#filter(Iterable, Class)} for related functionality.) * * <p><b>{@code Stream} equivalent:</b> {@link java.util.stream.Stream#filter Stream.filter}. */
Collections2.filter方法返回的只是原有列表的一个视图。所以:改变被过滤列表会影响过滤后列表,反之 亦然。并且我们从这段文字中还能get到下面几个注意事项:
- 过滤后列表的迭代器不支持remove()操作
- add不符合过滤条件的元素,会抛出IllegalArgumentException
3.当过滤后列表中的元素不再满足过滤条件时,会影响到已经过滤出来的列表出于程序员的本能,决定还是看下源码心里更踏实。核心源码如下:
1. add方法: public boolean add(E element) { //不符合过滤条件抛IllegalArgumentException的原因 checkArgument(predicate.apply(element)); return unfiltered.add(element); } 不支持通过迭代器删除的原因: public abstract class UnmodifiableIterator<E> implements Iterator<E> { /** Constructor for use by subclasses. */ protected UnmodifiableIterator() {} /** * Guaranteed to throw an exception and leave the underlying data unmodified. * * @throws UnsupportedOperationException always * @deprecated Unsupported operation. */ @Deprecated @Override public final void remove() { throw new UnsupportedOperationException(); } 打印结果变化的原因: public String toString() { Iterator<E> it = iterator(); if (! it.hasNext()) return "[]"; StringBuilder sb = new StringBuilder(); sb.append('['); for (;;) { E e = it.next(); sb.append(e == this ? "(this Collection)" : e); if (! it.hasNext()) return sb.append(']').toString(); sb.append(',').append(' '); } } @Override public Iterator<E> iterator() { return Iterators.filter(unfiltered.iterator(), predicate); }
问题已经弄清楚了,怎么修改这个问题呢?
方案一(可行):
干脆不用guava,
List<Person> names = Lists.newArrayList(lxy, xhf, nws); ArrayList<Person> persons = new ArrayList<>(); for (Person p : names) { if(p.age > 30) { persons.add(p); } }
方案二(可行):
用个容器再包装一下:
Collection<Person> personAgeGt30 = new ArrayList<(Collections2.filter(names, p -> p.age > 30));
方案三(可行,改用Java8的过滤器):
List<Person> personAgeGt30 = names.stream().filter((Predicate<Person>) p -> p.age >30).collect(Collectors.toList());
方案四(可行,改用Guava的连贯接口,IDEA编辑器会提示你替换成Java8 API)
ImmutableList<Person> personAgeGt30 = FluentIterable.from(names).filter(p -> p.age > 30).toList();
上述方案中,支持java8的生产环境推荐方案三,不支持java8的生产环境推荐方案二。
总结
其实,Java语言中类似的坑还有很多,比如:
- 1.Arrays.asList()生成的列表是不可变的。
- 2.subList生成的子列表,对原列表元素的修改,会导致子列表的遍历、增加、删除抛出ConcurrentModificationException,
- 3.subList对子列表的修改会影响到原列表数据
- 4.修改Map的keySet()方法和values()生成的容器,都会影响Map本身。
总之,使用任何API之前多看看源码,至少看看源码的注释。