Java流修剪列表

时间:2018-06-05 14:56:14

标签: java java-stream trim

我有以下实现,用于根据给定谓词修剪元素列表(即删除前导和尾随空元素)。

我想让实现更具可读性,最好使用Java的流API。

/**
 * Trim a List based on a given predicate, that is, remove leading
 * and trailing elements that match the predicate (but not in-between
 * non-matching elements).
 *
 * @param list the list to trim
 * @param trimPredicate the predicate for trimming
 * @param <T> type of the list
 * @return the same list minus the trimmed elements.
 * @throws NullPointerException if the list is {@code null}
 * @throws UnsupportedOperationException if the {@code remove}
 *      operation is not supported by the list iterator
 */
public static <T> List<T> trim(List<T> list, Predicate<T> trimPredicate)
        throws NullPointerException, UnsupportedOperationException {
    if (list == null) throw new NullPointerException("list is null");
    ListIterator<T> it = list.listIterator();
    while (it.hasNext() && trimPredicate.test(it.next())) it.remove();
    it = list.listIterator(list.size());
    while (it.hasPrevious() && trimPredicate.test(it.previous())) it.remove();

    return list;
}

有什么建议吗?

一个例子,让事情更清楚:

对于List<Integer>,将0视为空值,请输入以下列表:

[0, 0, 3, 5, 0, 4, 0, -3, 0, 0]

将被修剪为:

[3, 5, 0, 4, 0, -3]

(而且这里至少有两位不同的读者弄错了,这表明了我对代码可读性的看法:)。

3 个答案:

答案 0 :(得分:3)

您的原始代码非常易读且高效。推荐使用。

static <T> List<T> trim2(List<T> list, Predicate<T> isEmpty) {
    ListIterator<T> it = list.listIterator();
    while (it.hasNext() && isEmpty.test(it.next())) {
        it.remove();
    }
    it = list.listIterator(list.size());
    while (it.hasPrevious() && isEmpty.test(it.previous())) {
        it.remove();
    }
    return list;
}

使用java的流版本9.只是为了它,不推荐。

static <T> List<T> trim3(List<T> list, Predicate<T> isEmpty) {
    Collection<T> ltrimreverse = list.stream().dropWhile(isEmpty)
        .collect(ArrayDeque::new, ArrayDeque::push, ArrayDeque::addAll);
    Collection<T> rtrim = ltrimreverse.stream().dropWhile(isEmpty)
        .collect(ArrayDeque::new, ArrayDeque::push, ArrayDeque::addAll);
    return new ArrayList<>(rtrim);
}

答案 1 :(得分:1)

一点点hackery使它成为可能,但我不确定它是否更清楚。

/**
 * Stateful predicate to only match until matching fails.
 * It seems this is not necessary in Java 9.
 */
static class MatchWhile<T> implements Predicate<T> {
    final Predicate<T> matcher;
    boolean match = true;

    MatchWhile(Predicate<T> matcher) {
        this.matcher = matcher;
    }

    @Override
    public boolean test(T t) {
        return match && (match = matcher.test(t));
    }
}

// Hides the horrible stuff.
static <T> Stream<T> asStream(Iterator<T> it) {
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(it,Spliterator.ORDERED), false);
}

<T> List<T> trim2(List<T> list, Predicate<T> isEmpty) {
    // Trim right using a Deque to reverse it.
    Deque<T> reversedAndTrimmedAtEnd = asStream(new ArrayDeque<>(list).descendingIterator())
            .filter(new MatchWhile<>(isEmpty).negate())
            .collect(Collectors.toCollection(ArrayDeque::new));
    // Reverse it again to trim left.
    List<T> leftTrimmed = asStream(reversedAndTrimmedAtEnd.descendingIterator())
            .filter(new MatchWhile<>(isEmpty).negate())
            .collect(Collectors.toList());

    return leftTrimmed;
}

答案 2 :(得分:1)

如果您关注效率,则应避免重复单remove次操作,尤其是在List的开头,因为最常用的实现ArrayList不能很好地执行好吧,因为它必须在删除条目时复制其内部数组中的所有剩余元素。

最坏的情况,即以这种方式删除所有元素,将具有二次时间复杂度。

public static <T> List<T> trim(List<T> list, Predicate<T> trimPredicate) {

    Objects.requireNonNull(list, "list is null");
    Objects.requireNonNull(trimPredicate, "trimPredicate is null");

    int lastMatch;

    for(ListIterator<T> it = list.listIterator(lastMatch = list.size());
        it.hasPrevious() && trimPredicate.test(it.previous());) lastMatch = it.nextIndex();

    if(lastMatch < list.size()) list.subList(lastMatch, list.size()).clear();

    for(ListIterator<T> it = list.listIterator(lastMatch = 0);
        it.hasNext() && trimPredicate.test(it.next()); ) lastMatch = it.previousIndex();

    if(lastMatch > 0) list.subList(0, lastMatch+1).clear();

    return list;
}

List.subList(…).clear()是有效删除一系列项目的正确习惯用法。在ArrayList的情况下,它意味着对整个范围移除仅进行单个复制操作。所以我们只是迭代而不删除,以识别范围,然后执行单个删除操作。

由于最后删除没有额外费用,因为没有剩余的要复制的元素,此解决方案首先删除最后的匹配,以便可能减少后续删除匹配的剩余元素数量列表。

对于将直接修改列表的就地操作,没有可以改进它的Stream解决方案。

即使您想要返回新列表,最有效的解决方案之一也将基于subList

public static <T> List<T> trim(List<T> list, Predicate<T> trimPredicate) {

    Objects.requireNonNull(list, "list is null");
    Objects.requireNonNull(trimPredicate, "trimPredicate is null");

    int firstToKeep = 0, lastToKeep = list.size();
    for(T t: list) if(trimPredicate.test(t)) firstToKeep++; else break;
    for(ListIterator<T> it = list.listIterator(lastToKeep);
        lastToKeep > firstToKeep && it.hasPrevious() && trimPredicate.test(it.previous());)
        lastToKeep = it.nextIndex();

    return new ArrayList<>(list.subList(firstToKeep, lastToKeep));
}