迭代列表并修改每个元素:是否有更快的方法?该怎么做?

6
我有一个包含字符串的List,我想要将列表中的每个元素使用trim()方法进行处理。
当前,我正在使用ArrayList,通过简单地循环遍历元素,并将修剪后的元素添加到返回列表中,如下所示:
int listLen = listToTrim.size();

List<String> trimmedList = new ArrayList<String>( listLen );

for ( int i = 0; i < listLen; i++ ) {
    trimmedList.add( listToTrim.get( i ).trim() );
}

return trimmedList;

对于大型列表,是否有更有效率的方法来完成这个任务?

3
更高效的方法是首先创建一个修剪过的字符串列表。你也可以修改现有列表,除非你还需要未修剪的原始列表。 - Peter Lawrey
如果它开始影响性能/生产力,你可以尝试进行多线程处理。这个任务很简单,“独立”足够容易分配给多个工作者。但这只对足够大的工作量提高了性能。 - sheltem
2
不要使用你编辑中的组合解决方案!那是一种非常糟糕的处理方式。只需使用迭代器,它适用于LinkedList和ArrayList(以及所有其他类型的List)。如果不必使用它,请避免索引for循环。 - Petr Janeček
1
另外,请不要在问题中尝试回答,这会让未来的访问者感到困惑。 - Joachim Sauer
3
稍微慢一些。这种“慢”程度相当于你头发稀少的程度。这种“慢”程度其实并不重要。说真的,它非常微小,你可以认为这两种方法是相等的(除非进行一些非常专业的工作)。与trim()方法相比,循环开销微不足道。 - Petr Janeček
显示剩余3条评论
5个回答

14

没问题,这就是它能更高效的方式了,没有什么神奇的方法可以避免迭代。

需要记住一点:如果 listToTrim 不是随机访问列表(即它没有实现 RandomAccess 接口),那么使用迭代器(或增强型 for 循环,它在内部使用迭代器)而不是传统的 for 循环通常更高效。最显著的未实现 RandomAccess 接口的列表是 LinkedList。在具有 600 个元素的 LinkedList 上调用 l.get(300) 将不得不迭代大约 300 个元素才能得到正确的元素!

将您的代码修改为使用增强型 for 循环将如下所示:

public List<String> trimStrings(Listy<String> listToTrim) {
    List<String> trimmedList = new ArrayList<String>(listToTrim.size());
    for (String str : listToTrim) {
      trimmedList.add(str.trim());
    }
    return trimmedList;
}
如果您不再需要原始列表,那么重用原始列表可以节省内存并提高性能:

如果您不再需要原始列表,那么重用原始列表可以节省内存并提高性能:

public void trimStringsInPlace(List<String> listToTrim) {
    ListIterator<String> it = listToTrim.listIterator();
    while (it.hasNext()) {
      it.set(it.next().trim());
    }
}

1
如果我重复使用原始列表,我需要返回它吗?它不会已经被修改了吗? - evanjdooner
2
@evanjdooner:不需要返回它。我只是假设你想保持方法的签名相同,但如果你不这样做,那就不会让人感到困惑(因为这样很明显传入的列表被修改了),所以我会将其删除。 - Joachim Sauer
好的,我明白了。谢谢!我对迭代器的使用不太熟悉,所以我想知道返回列表是否有什么原因。现在一切清楚了! - evanjdooner
我使用迭代器提供了答案,但看看发生了什么。它被踩了,但这个答案得到了赞。我想这里有些人不是地球人。 - Ruchira Gayan Ranaweera

3
此外,您可以使用ArrayList#set()而无需创建新的ArrayList。对于更大的列表,这可以显著减少内存占用。
for ( int i = 0; i < listLen; i++ ) {
    listToTrim.set(i,listToTrim.get( i ).trim());
}

如果您不需要保留未修剪字符串的原始列表,并且愿意就地更改列表(这会增加与其他代码部分共享列表时出现错误的风险),那么这是前进的方式。 - Tom Anderson
请注意,您可以使用 ListIterator 来以迭代器的方式修改列表,而不必使用 get/set。 - Tom Anderson
1
调用 get(i)set(i) 可以在使用任何非随机访问列表(例如 LinkedList)时真正破坏您的运行时!尝试在 LinkedList 中使用几千个字符串进行基准测试,并将其与 ArrayList 上的相同代码进行比较。差异应该是非常明显的。 - Joachim Sauer
1
如果它不是RandomAccess,我同意。这就是为什么我在答案中坚持使用ArrayList的原因。 - rocketboy

3

Joachim已经回答了这个问题。但有一个建议-

listToTrim - 当您向listToTrim中添加项目时,请在添加之前进行修剪。这样,您就不必迭代和修改或创建另一个列表来完成此操作。这听起来并不合乎逻辑。

根据评论编辑:

String fruits = "Apple, Banana   , Mango, Passion Fruit, Grapes  ";

List<String> fruitList = Arrays.asList((fruits.trim()).split("\\s*,\\s*")); // Trim first and then regex matches spaces before and after comma

for(String fruit : fruitList){
    System.out.println("Fruit: " + fruit + "\tLength: " + fruit.length());
}

输出:

Fruit: Apple           Length: 5
Fruit: Banana          Length: 6
Fruit: Mango           Length: 5
Fruit: Passion Fruit   Length: 13
Fruit: Grapes          Length: 6

listToTrim是从通过分割逗号分隔字符串产生的数组创建的。我不确定如何预先修剪它,除非在由split()返回的数组上执行类似的操作。 - evanjdooner
1
@evanjdooner:你可以使用split("\\s*,\\s*")。然后你只需要对第一个和最后一个值进行trim()处理。 - Joachim Sauer
@JoachimSauer 你比我先说了这个。 - Sajal Dutta
1
@evanjdooner:那么您从中学到了什么?如果您问“我该如何做Y?”最好要提到为什么您想要做Y,因为有时您可以修改如何执行之前的X来使Y变得不必要(这被称为XY问题)。将来,请尝试包含更多上下文以允许这些类型的答案。 - Joachim Sauer
@evanjdooner 你应该选择Joachim Sauer的答案作为采纳答案,因为他的解决方案对于你在问题中指定的问题来说非常明确。我的只是一个建议。 - Sajal Dutta
显示剩余2条评论

3

Joachim说得对。那些说你应该在将字符串放入第一个列表之前修剪它们的人也是正确的。

无论如何,如果前者不适用于您,可能会有另一种替代方法:您确定会使用所有修剪过的字符串吗?还是只会使用其中几个,比如可能只使用前五个?那么修剪它们全部可能会过度了。

您可以设计一个特殊的 List 实现,它将存储原始列表,但会输出修剪后的元素。这就是我的意思:

public class ImmutableStringTrimmingList extends AbstractList<String> {

    private final List<String> stringList;

    public ImmutableStringTrimmingList(List<String> stringList) {
        this.stringList = stringList;
    }

    @Override
    public String get(int index) {
        return stringList.get(index).trim();
    }

    @Override
    public int size() {
        return stringList.size();
    }

}
ImmutableStringTrimmingList 存储原始列表(因此对该列表所做的任何更改也将传播到此处),并懒惰地分发修剪后的 String 对象。这在您不想进行不必要工作时可能很有帮助,因为它只修剪由 get() 请求的字符串。它还可以被适应以缓存修剪后的对象,以便每次都不必重新修剪。但如果您觉得这个类有用,那么具体实践是由您决定的。
或者,如果您是 Guava 用户,则可以使用 Lists.transform() ,它基本上执行相同的操作,也是懒加载的。
List<String> trimmedList = Lists.transform(list, new Function<String, String>() {
    @Override
    public String apply(String input) {
        return input.trim();
    }
});

所有的字符串都会被使用,是的。这很不幸,因为我真的很喜欢这个答案。 - evanjdooner
1
@evanjdooner 如果您只访问所有字符串一次,它将执行与所有其他解决方案相同的工作(修剪字符串)。如果您要多次访问它们,则可能需要添加一些缓存(例如,用修剪后的字符串替换未修剪的字符串)等,这就是开始浪费的地方 :)。 - Petr Janeček
2
不错的选择!如果你偶尔需要这样做,那么Guava [Lists.transform()](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Lists.html#transform(java.util.List, com.google.common.base.Function))可能会引起你的兴趣,因为它几乎完全可以实现这个功能。 - Joachim Sauer

3

不完全是;你必须在每个元素上调用trim,并且预先分配正确大小的ArrayList与此速度相同。Java 8将允许您使语法更紧凑,但迭代和trim是此处最小的工作量。


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