用于特殊情况下处理最后一个元素的最佳循环习惯用语

52

在进行简单的文本处理和打印语句时,我经常遇到这种情况:循环遍历集合并对最后一个元素进行特殊处理(例如,除了最后一个元素以外,每个普通元素都会用逗号分隔)。

有没有一些最佳实践习惯或优雅的形式,不需要复制代码或将 if else 语句嵌入循环中。

例如,我有一个字符串列表,我想以逗号分隔的列表形式打印出来。(do while 解决方案已经假定列表有两个或更多元素,否则与条件正确的 for 循环一样糟糕)。

例如 List = ("dog", "cat", "bat")

我想打印“[dog,cat,bat]”

我提供了两种方法:

  1. 带条件的 for 循环

    public static String forLoopConditional(String[] items) {
    
    String itemOutput = "[";
    
    for (int i = 0; i < items.length; i++) {
        // Check if we're not at the last element
        if (i < (items.length - 1)) {
            itemOutput += items[i] + ", ";
        } else {
            // last element
            itemOutput += items[i];
        }
    }
    itemOutput += "]";
    
    return itemOutput;
     }
    
  2. 使用do while循环时为循环做准备

  3. public static String doWhileLoopPrime(String[] items) {
    String itemOutput = "[";
    int i = 0;
    
    itemOutput += items[i++];
    if (i < (items.length)) {
        do {
            itemOutput += ", " + items[i++];
        } while (i < items.length);
    }
    itemOutput += "]";
    
    return itemOutput;
    }
    

    测试类:

    public static void main(String[] args) {
        String[] items = { "dog", "cat", "bat" };
    
        System.out.println(forLoopConditional(items));
        System.out.println(doWhileLoopPrime(items));
    
    }
    
    在Java的AbstractCollection类中,它有以下实现(因为包含所有边缘情况的错误检查,所以有点啰嗦,但并不糟糕)。
    public String toString() {
        Iterator<E> i = iterator();
    if (! i.hasNext())
        return "[]";
    
    StringBuilder sb = new StringBuilder();
    sb.append('[');
    for (;;) {
        E e = i.next();
        sb.append(e == this ? "(this Collection)" : e);
        if (! i.hasNext())
        return sb.append(']').toString();
        sb.append(", ");
    }
    }
    

值得注意的是,Java类java.util.AbstractCollection在第1节中使用了[ while(all) $value if(not_last) $separator ]的习惯用法。 - corsiKa
3
除了用于在文本中添加分隔符之外,还有什么其他用途?实际上有那么多吗?换句话说,这种操作是否应该成为一个众所周知的习语,或者我们应该只使用已经为我们实现了此功能的库? - Kevin Bourrillion
我认为在构建协议时,它已经超越了简单的文本处理,因为二进制数据具有分隔符和特定的格式(就像正则表达式比匹配有效电子邮件更强大一样)。甚至更复杂的文本处理,例如构建由“&”分隔的查询字符串,除了最后一个元素。 - Dougnukem
相关:https://dev59.com/-XNA5IYBdhLWcg3wKae3 - finnw
18个回答

1

...

String[] items = { "dog", "cat", "bat" };
String res = "[";

for (String s : items) {
   res += (res.length == 1 ? "" : ", ") + s;
}
res += "]";

或者说相当易读。当然,您可以将条件放在单独的if子句中。我认为它的惯用方式是使用foreach循环,而不是使用复杂的循环头。

此外,没有重复逻辑(即只有一个地方将items中的项目实际附加到输出字符串中 - 在实际应用中,这可能是更复杂和冗长的格式化操作,因此我不想重复代码)。


+1 我认为这种方法很优雅,因为测试 res.length 比在特定迭代期间检测第一个或最后一个迭代并表现不同更好。即使每个迭代都有可能不向列表中添加任何项,上面的代码也可以正常工作。 - Heath Hunnicutt
1
在循环中使用字符串+=运算符会导致问题,因为您每次都要重建字符串,将一项O(n)的操作变成了O(n²)的操作。 :-( - corsiKa

1
如果您正在动态构建字符串,那么就不应该使用 += 运算符。 StringBuilder 类对于重复的动态字符串连接效果更好。
public String commaSeparate(String[] items, String delim){
    StringBuilder bob = new StringBuilder();
    for(int i=0;i<items.length;i++){
        bob.append(items[i]);
        if(i+1<items.length){
           bob.append(delim);
        }
    }
    return bob.toString();
}

然后调用就像这样

String[] items = {"one","two","three"};
StringBuilder bob = new StringBuilder();
bob.append("[");
bob.append(commaSeperate(items,","));
bob.append("]");
System.out.print(bob.toString());

1
在这种情况下,您实际上是使用某个分隔符字符串连接字符串列表。您可以自己编写一些代码来完成此操作。然后您将得到类似以下的结果:
String[] items = { "dog", "cat", "bat" };
String result = "[" + joinListOfStrings(items, ", ") + "]"

使用

public static String joinListOfStrings(String[] items, String sep) {
    StringBuffer result;
    for (int i=0; i<items.length; i++) {
        result.append(items[i]);
        if (i < items.length-1) buffer.append(sep);
    }
    return result.toString();
}

如果您有一个Collection而不是String[],您也可以使用迭代器和hasNext()方法来检查是否为最后一个。

你应该使用 StringBuilder 而不是 StringBuffer,因为它更快,可以避免同步的开销。 - Sean Patrick Floyd

1

一般来说,我最喜欢的是多级退出。更改

for ( s1; exit-condition; s2 ) {
    doForAll();
    if ( !modified-exit-condition ) 
        doForAllButLast();
}

for ( s1;; s2 ) {
    doForAll();
if ( modified-exit-condition ) break;
    doForAllButLast();
}

它可以消除任何重复的代码或冗余的检查。

您的示例:

for (int i = 0;; i++) {
    itemOutput.append(items[i]);
if ( i == items.length - 1) break;
    itemOutput.append(", ");
}

它对某些事情比其他事情更有效。 对于这个特定的例子,我不是很喜欢它。

当退出条件取决于发生在doForAll()而不仅仅是s2的情况时,情况变得非常棘手。 使用Iterator就是这种情况。

这里有一篇论文来自教授,他无耻地向他的学生推销它 :-). 阅读第5节,了解您所讨论的内容。


1
我认为对于这个问题有两个答案:在任何语言中解决此问题的最佳成语,以及在Java中解决此问题的最佳成语。我也认为,这个问题的意图不是将字符串拼接起来,而是模式的一般性,因此展示那些可以做到这一点的库函数并没有什么帮助。

首先,将一个字符串用[]包围和创建一个由逗号分隔的字符串是两个不同的操作,理想情况下应该是两个不同的函数。

对于任何语言,我认为使用递归和模式匹配的组合效果最好。例如,在Haskell中,我会这样做:

join [] = ""
join [x] = x
join (x:xs) = concat [x, ",", join xs]

surround before after str = concat [before, str, after]

yourFunc = surround "[" "]" . join

-- example usage: yourFunc ["dog", "cat"] will output "[dog,cat]"

写成这样的好处是清晰地列举了函数将面对的不同情况以及如何处理它们。
另一种非常好的方法是使用累加器类型函数。例如:
join [] = ""
join strings = foldr1 (\a b -> concat [a, ",", b]) strings 

这也可以用其他语言来实现,例如C#:

public static string Join(List<string> strings)
{
    if (!strings.Any()) return string.Empty;
    return strings.Aggregate((acc, val) => acc + "," + val);
}

在这种情况下效率不是很高,但在其他情况下可能会有用(或者效率可能无关紧要)。

不幸的是,Java不能使用这两种方法。因此,在这种情况下,我认为最好的方法是在函数顶部进行异常情况(0或1个元素)的检查,然后使用for循环来处理多于1个元素的情况:

public static String join(String[] items) {
    if (items.length == 0) return "";
    if (items.length == 1) return items[0];

    StringBuilder result = new StringBuilder();
    for(int i = 0; i < items.length - 1; i++) {
        result.append(items[i]);
        result.append(",");
    }
    result.append(items[items.length - 1]);
    return result.toString();
}

此函数清楚地展示了两种边缘情况(0或1个元素)发生的情况。然后,它对除最后一个元素以外的所有元素使用循环,并最终添加最后一个元素而不使用逗号。处理非逗号元素的反向方式也很容易。

请注意,if (items.length == 1) return items[0]; 行实际上并不是必要的,但是我认为它使函数在一眼看上去更容易确定其功能。

(如果有人想要关于 Haskell / C#函数的更多解释,请询问,我会添加进来)


0
第三种选择是以下内容
StringBuilder output = new StringBuilder();
for (int i = 0; i < items.length - 1; i++) {
    output.append(items[i]);
    output.append(",");
}
if (items.length > 0) output.append(items[items.length - 1]);

但最好使用类似于join()的方法。对于Java,第三方库中有一个String.join,这样你的代码就变成了:

StringUtils.join(items,',');

就算是 FWIW,Apache Commons 中的 join() 方法(从第 3232 行开始)确实在循环中使用了 if 语句:

public static String join(Object[] array, char separator, int startIndex, int endIndex)     {
        if (array == null) {
            return null;
        }
        int bufSize = (endIndex - startIndex);
        if (bufSize <= 0) {
            return EMPTY;
        }

        bufSize *= ((array[startIndex] == null ? 16 : array[startIndex].toString().length()) + 1);
        StringBuilder buf = new StringBuilder(bufSize);

        for (int i = startIndex; i < endIndex; i++) {
            if (i > startIndex) {
                buf.append(separator);
            }
            if (array[i] != null) {
                buf.append(array[i]);
            }
        }
        return buf.toString();
    }

这里有一个关于Java join的讨论:https://dev59.com/aXRA5IYBdhLWcg3w4SDo - Paul Rubel
output.append(items[i]).append(','); 应该更快,因为它避免了中间字符串的创建。 - Stephen C

0

我通常会像这样编写一个for循环:

public static String forLoopConditional(String[] items) {
    StringBuilder builder = new StringBuilder();         

    builder.append("[");                                 

    for (int i = 0; i < items.length - 1; i++) {         
        builder.append(items[i] + ", ");                 
    }                                                    

    if (items.length > 0) {                              
        builder.append(items[items.length - 1]);         
    }                                                    

    builder.append("]");                                 

    return builder.toString();                           
}       

如果由于某种原因您事先不知道长度,您可以在项目之前添加逗号,除非它是第一个项目。 - Brian T Hannan
在Java中,您始终知道数组的大小。在Java 5中,您还可以知道可变参数的大小(例如“void functionName(String ... args)”),因为可变参数是具有简化符号的数组。对于列表,您可以使用size()和get()方法来实现相同的结果。集合的唯一问题是使用正确的算法来使用所使用的实现。即我的算法将使用ArrayList更有效,但使用LinkedList将效率低下。 - frm

0

如果你只是想要一个逗号分隔的列表,像这样:"[The, Cat, in, the, Hat]",那么不要浪费时间编写自己的方法。直接使用List.toString即可:

List<String> strings = Arrays.asList("The", "Cat", "in", "the", "Hat);

System.out.println(strings.toString());

如果List的泛型有一个toString方法可以返回你想要显示的值,那么只需要调用List.toString即可:

public class Dog {
    private String name;

    public Dog(String name){
         this.name = name;
    }

    public String toString(){
        return name;
    }
}

然后,您可以这样做:

List<Dog> dogs = Arrays.asList(new Dog("Frank"), new Dog("Hal"));
System.out.println(dogs);

你将会得到: [Frank, Hal]

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