基于谓词的Java 8 Stream indexOf方法

8

我遇到了这样的情况,我需要知道列表中一个元素的索引(位置),但只有一个谓词表达式来识别该元素。我查找了类似于 Stream 的函数

int index = list.stream().indexOf(e -> "TESTNAME".equals(e.getName()));

但是没有用。当然,我可以这样写:
int index = list.indexOf(list.stream().filter(e -> "TESTNAME".equals(e.getName()))
    .findFirst().get());

但是这种方法会 a)在最坏情况下(元素是最后一个)遍历列表两次,b)如果没有元素与谓词匹配,则会失败(我宁愿使用 -1索引)。

我编写了一个实用程序方法来实现此功能:

public static <T> int indexOf(List<T> list, Predicate<? super T> predicate) {
    int idx = 0;
    for (Iterator<T> iter = list.iterator(); iter.hasNext(); idx++) {
        if (predicate.test(iter.next())) {
            return idx;
        }
    }

    return -1;
}

但是,由于这似乎是一个非常琐碎的算法,我本来期望在Java 8 Stream API中能找到它。我是错过了吗?还是真的没有这样的功能?(奖励问题:如果没有这样的方法,是否有很好的理由?在函数式编程中使用索引可能是一种反模式吗?)


@Florian Albrecht:JavaDoc 表明 findFirst() 返回任何匹配项,除非流已经有遇到顺序。 - Michal
在我看来,你的静态实用程序,使用for和if语句,既优雅又易读。使用流API也不可能比这更易读了。 - Bhesh Gurung
@JacobG。是的,但takeWhile()会,不是吗?这是我的错... - Florian Albrecht
@JacobG。啊,现在明白了。抱歉,我读Javadoc太快了。 - Florian Albrecht
@FlorianAlbrecht 经过思考,我认为如果您从Stream#count中减去结果,使用Stream#dropWhile将起作用,但不幸的是,您只能使用Java 8。 - Jacob G.
显示剩余11条评论
4个回答

14

你的循环不错,但是可以简化:

public static <T> int indexOf(List<T> list, Predicate<? super T> predicate) {
    for(ListIterator<T> iter = list.listIterator(); iter.hasNext(); )
        if(predicate.test(iter.next())) return iter.previousIndex();
    return -1;
}

你可以像使用流一样使用

public static <T> int indexOf(List<T> list, Predicate<? super T> predicate) {
    return IntStream.range(0, list.size())
        .filter(ix -> predicate.test(list.get(ix)))
        .findFirst().orElse(-1);
}

但是如果列表很大且不支持随机访问,则这种方法效率会变得相当低下。我建议使用循环。


从Java 9开始,有另一种选择。

public static <T> int indexOf(List<T> list, Predicate<? super T> predicate) {
    long noMatchPrefix = list.stream().takeWhile(predicate.negate()).count();
    return noMatchPrefix == list.size()? -1: (int) noMatchPrefix;
}

这里的表达方式非常生动形象,适用于“计算第一个匹配元素之前的元素数量”的任务,但并不完全等同于“获取第一个匹配元素的索引”,因为后者在没有匹配项时会显示出来,所以我们需要将结果替换为-1


1
感谢简化!使用listIterator()是个好主意。不过,我仍然希望在Stream或List接口上有这样一个简单的API可用... - Florian Albrecht

2
我认为一个简单的解决方案是使用IntStream
IntStream.range(0, list.size())
         .filter(i -> Objects.nonNull(list.get(i)))
         .filter(i -> "TESTNAME".equals(list.get(i).getName()))
         .findFirst()
         .orElse(-1);

过滤掉null元素可以避免抛出NullPointerException异常。

“null” 的限制也适用于我的当前解决方案,这是个好提示。 - Florian Albrecht
谢谢您告诉我,我会编辑答案以过滤掉“null”。 - Jacob G.
1
如果列表不是随机访问,则时间复杂度为O(N²)。 - DodgyCodeException
1
是的!而且给负分的人,解释一下你们自己吧! :P - Jacob G.
在你的第二段代码中,如果过滤掉了空值,你将得不到正确的索引。 - DodgyCodeException
显示剩余2条评论

1
    IntPredicate isNotNull = i -> Objects.nonNull(list.get(i));
    IntPredicate isEqualsTestName = i -> list.get(i).getName().equals("TESTNAME");

    int index = IntStream.range(0, list.size())
                .filter(isNotNull.and(isEqualsTestName))
                .findFirst()
                .orElse(-1);

1
原因在于“索引”的概念只有在“有序集合”中才有意义。一个Stream可以来自任何Collection,比如Set;拥有“indexOf”方法基本上意味着给源Set提供一个get(int index)操作。更不用说Stream还可以来自任何其他来源,而不仅仅是集合。

另一个原因是实现indexOf操作基本上需要一个状态机类型的Stream元素访问器;状态机类型是因为它必须计算已经访问过的成员数量。状态机操作和Stream不太匹配。

至于您的实用函数,我不明白为什么您要以这种方式编写它,而不是像这样:

static <T> int indexOf(List<T> findIn, Predicate<? super T> pred) {
    for (int i = 0; i < findIn.size(); i++) {
        if (pred.test(findIn.get(i))) {
            return i;
        }
    }
    return -1;
}

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