40
\$\begingroup\$

The specific issue is this: I am writing to a file and want to output a new line after each line written. If I use a normal loop without any further checks, this will create a blank line at the very end of the file for no reason. So I need to do this every time except the last one.

Despite the fact that this is a specific problem, I'm actually looking for a better general coding practice to deal with scenarios like this, since it isn't the first and won't be the last time I have to deal with it.

Here's my unsatisfying solution:

//Write contents to the file
BufferedWriter writer = new BufferedWriter(new FileWriter(file));
for(int i = 0; i < lines.size(); i++) {
    writer.write(lines.get(i));
    if(i < lines.size() - 1) writer.newLine();
}

It seems wasteful to check the conditions twice through each iteration of the loop, and I feel like there ought to be a better way to accomplish what I want without the vague code smell. Anyone have any cool tips or tricks to make this more elegant?

It also prevents me from using an enhanced for loop, which makes me sad.

lines is a List<String>.

Also, for those saying I should just join all the Strings with \n, that's not an adequate solution. Firstly, it doesn't actually address the general coding practice. Secondly, when writing to a file with a BufferedWriter, it's important to use newLine() rather than writing a \n.

\$\endgroup\$
11
  • 7
    \$\begingroup\$ *Somebody has to say this... a text file is a series of lines. A line includes a newline terminator. A file that does not have a newline as its final character is not a text file. A file that ends in a blank line has 2 consecutive newlines as its last 2 characters: one to terminate the next-to-last line and one to terminate the empty line. "Text files" with an unterminated last line are a disease. Don't catch it! \$\endgroup\$
    – user33484
    Dec 13, 2013 at 19:36
  • 3
    \$\begingroup\$ @WumpusQ.Wumbley In what circumstances would that ever be necessary? I'm not saying there are none, I just don't know of any. Arbitrarily declaring a format that all text files must follow seems overly ambitious. \$\endgroup\$
    – Daniel
    Dec 13, 2013 at 20:15
  • 2
    \$\begingroup\$ @WumpusQ.Wumbley It may be a windows thing, from wikipedia Text file : "MS-DOS and Windows use a common text file format, with each line of text separated by a two-character combination: CR and LF, which have ASCII codes 13 and 10. It is common for the last line of text not to be terminated with a CR-LF marker, and many text editors (including Notepad) do not automatically insert one on the last line." \$\endgroup\$
    – Daniel
    Dec 13, 2013 at 20:17
  • \$\begingroup\$ @DanielCook It's not just a Windows thing. Many Unix shell utilities, especially the older ones, behave unpredictably if the last line doesn't end in LF. On the other hand, sometimes the absence of a final LF is essential, as when using PHP to generate XML - if the PHP source file has a LF after the final ?>, it will be copied to the output and probably wind up in a place where the XML parser doesn't like it. So it's not by any means a hard rule. \$\endgroup\$
    – zwol
    Dec 13, 2013 at 20:31
  • 1
    \$\begingroup\$ @WumpusQ.Wumbley - One of the 16-bit Visual C++ compilers had a bug that would manifest when the source file did not end with CR/LF. But I haven't encountered anything else with such an issue in the past 15 years. \$\endgroup\$ Dec 13, 2013 at 20:45

11 Answers 11

24
\$\begingroup\$

I presume lines is a Collection of some sort. One option that has slightly less of a smell (although it still is odoriferous), is to use an iterator, which will essentially do the same work, but will be more readable:

for (Iterator<String> it = lines.iterator(); it.hasNext();) {
    writer.write(it.next());
    if (it.hasNext()) {
        writer.newline();
    }
}

As I say, all this does is make it more readable....

Other options are to duplicate the write - once in the loop, and then the last one outside the loop:

if (!lines.isEmpty()) {
    int limit = lines.size() - 1;
    for (int i = 0; i < limit; i++) {
        ....
    }
    writer.write(lines.get(limit));
}

EDIT: @konijn suggested reversing the newline to happen only after the first line as follows:

if (!lines.isEmpty()) {
    writer.write(lines.get(0));
    // start index at 1 instead of 0.
    for (int i = 1; i < lines.size(); i++) {
        writer.newline();
        writer.write(lines.get(limit));
    }
}
\$\endgroup\$
6
  • \$\begingroup\$ Yep, lines is a List<String>. Nice. I like the idea of performing the last write outside of the loop rather than constantly checking inside of the loop. Still seems vaguely funky to me, but better than what I've got now. Thanks. :) \$\endgroup\$
    – asteri
    Dec 13, 2013 at 15:15
  • 9
    \$\begingroup\$ I tend to write the first one without newline, then write newline + nextline for the rest, starting loop from 1. \$\endgroup\$
    – konijn
    Dec 13, 2013 at 15:22
  • 1
    \$\begingroup\$ @tomdemuyt - put that in an answer - it's a good solution. \$\endgroup\$
    – rolfl
    Dec 13, 2013 at 15:23
  • \$\begingroup\$ @tomdemuyt That's actually probably what I'll end up doing, now that you mention it. You definitely should post that as an answer. \$\endgroup\$
    – asteri
    Dec 13, 2013 at 15:34
  • \$\begingroup\$ Reason I did not is because in my case I always have minimum 2 lines, now the code can fail if there is only 1 line, and then it gets ugly again. \$\endgroup\$
    – konijn
    Dec 13, 2013 at 21:03
13
\$\begingroup\$

This case is usually concerning joining strings. There's a method for that in Apache Commons Lang:

StringUtils.join(lines, "\n");

Also here's a pattern that can be used with a foreach loop

StringBuilder buf = new StringBuilder();
for (String line : lines) {
    if(buf.length() > 0) {
        buf.append("\n");
    }
    buf.append(line);
}
\$\endgroup\$
2
  • \$\begingroup\$ I am not sure this code solves the problem ... !lines.isEmpty() will return true on every run through the loop. \$\endgroup\$
    – rolfl
    Dec 13, 2013 at 15:39
  • \$\begingroup\$ My mistake: I meant buf.isEmpty() \$\endgroup\$
    – rzymek
    Dec 13, 2013 at 15:46
8
\$\begingroup\$

You can do it by just removing the last seperator when you are done:

CharSequence concatSep(Iterable<?> items, CharSequence separator){
    if(!lines.iterator().hasNext()) return "";

    StringBuilder b = new StringBuilder();
    for(Object item: items)
        b.append(item.toString()).append(separator);
    return b.delete(b.length() - separator.length(), b.length());
}

where separator is the desired item seperator, whether newline, comma, semicolon, tab, or more than one character.

In your case: concatSep(lines, System.getProperty("line.seperator")).

\$\endgroup\$
2
  • \$\begingroup\$ This does not work if lines.Length == 0. \$\endgroup\$
    – Matt
    Dec 14, 2013 at 5:59
  • \$\begingroup\$ @Matt Fixed, also put them in functions. \$\endgroup\$ Dec 14, 2013 at 17:23
6
\$\begingroup\$

For the general case you can either pull the first or last line out of the loop, or move the loop exit into the middle of the loop using a break statement - modifying rolfl's example:

Iterator<String> it = lines.iterator()
if (it.hasNext()) {
    while (true) {
        writer.write(it.next());
        if (!it.hasNext()) 
            break;
        writer.newline();
    }
}

'Structured programming with goto statements' is a classic paper on handling non-standard looping cases.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ This pattern is my favorite because: a. It doesn't require you to duplicate code - "writer.write" appears only once. b. It doesn't require you to duplicate tests inside the loop - the loop exit test is performed only once, in the middle of the loop. \$\endgroup\$ Dec 15, 2013 at 15:24
  • \$\begingroup\$ A better idiom for this needs to be added to java: just let the user have both a block before and after the same while. Like this: do { doSomething(); } while (condition()) { doSomethingElse(); } \$\endgroup\$ Dec 17, 2013 at 2:39
5
\$\begingroup\$

A small twist on @rolfls' answer:

BufferedWriter writer = new BufferedWriter(new FileWriter(file));
if ( lines.size() > 0 ) {
    writer.write(lines.get(0));
}
for (int i = 1; i < lines.size(); i++) {
    writer.newLine();
    writer.write(lines.get(i));
}

Exact same idea though, move the extra check outside of the loop.

\$\endgroup\$
4
  • \$\begingroup\$ Hm... are you coming from JavaScript? I don't think if(lines.size()) will compile. \$\endgroup\$
    – asteri
    Dec 13, 2013 at 15:51
  • \$\begingroup\$ Indeed -- It's the logic I'd like to express, not the syntax ;) \$\endgroup\$
    – smassey
    Dec 13, 2013 at 16:05
  • \$\begingroup\$ FWIW, I personally feel this may be the best approach. I was about to post the same idea, when I finally noticed someone already had. The reason I like this better is that there is only one if check done at the beginning. Most of the other solutions require doing some kind of comparison each run through. In this case, we can eliminate unnecessary comparisons through the for loop. My two cents... \$\endgroup\$
    – Charlie74
    Dec 13, 2013 at 21:12
  • 1
    \$\begingroup\$ lines.size() > 0 is lame and potentially harmful (if lines is a singly linked list, knowing its size might be O(n)). Use lines.nonEmpty() or !lines.isEmpty() if the earlier is not available. \$\endgroup\$
    – scand1sk
    Dec 14, 2013 at 15:44
5
\$\begingroup\$

Just a little simpler:

BufferedWriter writer = new BufferedWriter(new FileWriter(file));
for(int i = 0; i < lines.size(); i++) {
    if(i > 0) writer.newLine();
    writer.write(lines.get(i));
}
\$\endgroup\$
5
\$\begingroup\$

why not reverse it: write a newline first except on the first line:

boolean newline = false;
for(int i = 0; i < lines.size(); i++) {
    if(newline) writer.newLine();
    else newline = true;
    writer.write(lines.get(i));
}
\$\endgroup\$
5
\$\begingroup\$

There are two issues in your code:

  1. As you noticed, the if is checked at each loop although you know that it will be invalidated only on the last loop. A good way to avoid the problem is to treat the first or last element of your list specifically before (or after) entering the loop. Treating the first element separately is much easier.

    Note that your if check may be quite cheap if you assume that calculating size is constant-time. (Note that size could be calculated once before the loop.) The optimization will probably be completely negligible (especially since you are performing costly I/O in the loop).

  2. Maybe more subtle: if lines is a List, is may not be indexed (e.g., lines is a LinkedList). Calling lines.get(i) may then be O(i), and your whole loop be O(n²) although O(n) is doable. Using an Iterator as @rolfl suggested is the best way to avoid this problem. It may or may not improve readibility depending on your experience, but it will surely improve performance drastically depending on the nature of your List.

BTW, this problem is solved generically in the very Java API: look for the implementation of toString in AbstractCollection (just replace separators with your own, and remove the test for e == this which is quite specific):

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(' ');
    }
}

I would be surprised if a more general implementation with user-definable separators would not to be found in Apache Commons or Google Guava.

Anyway, here is the final code, using BufferedWriter instead of StringBuilder:

private static void <E> write(BufferedWriter writer, List<E> lines) {
    Iterator<E> it = lines.iterator();
    if (!it.hasNext()) return;

    for (;;) {
        E e = it.next();
        writer.write(e);
        if (!it.hasNext()) return;
        writer.newLine();
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ You're right, I knew I could simply join the various Strings together with \n as you suggest. But when writing to a file with BufferedWriter, you're supposed to use newLine() instead because it's system agnostic. \$\endgroup\$
    – asteri
    Dec 14, 2013 at 17:16
  • \$\begingroup\$ Well, you can still use the above structure, replacing references to StringBuilder by BufferedWriter. I edited my answer above to include the final code. \$\endgroup\$
    – scand1sk
    Dec 14, 2013 at 21:58
2
\$\begingroup\$
BufferedWriter writer = new BufferedWriter(new FileWriter(file));
   int i =0;
   for(;i < lines.size()-1; i++)
   {
     writer.write(lines.get(i));
     writer.newLine();
   }
   if(lines.size()>0)
   {
   writer.write(lines.get(i));
   }

This way you can avoid the conditional statement every time, keeping your code the same.

\$\endgroup\$
0
2
\$\begingroup\$

Definitely, definitely take the if statement out of the loop. People always talk about "the optimizer" but optimizers are different, and whatever you can do to help it is probably a good idea.

//Write contents to the file
BufferedWriter writer = new BufferedWriter(new FileWriter(file));
for(int i = 0; i < lines.size() - 1; i++) {
    writer.write(lines.get(i));
    writer.newLine();
}

// Write the last one without extra newline
if( lines.size() )
  writer.write(lines.get(lines.size()-1));
\$\endgroup\$
2
  • \$\begingroup\$ This does not work if lines.Length == 0. \$\endgroup\$
    – Matt
    Dec 14, 2013 at 6:01
  • \$\begingroup\$ @Matt Good point. Fixed. \$\endgroup\$
    – bobobobo
    Dec 14, 2013 at 14:03
1
\$\begingroup\$

You do not need condition check if you change the loop limit from lines.size() to lines.size() -1. This ensures that the last entry in the lines is skipped. Next, after the loop, you write line's last content.

//Write contents to the file
 BufferedWriter writer = new BufferedWriter(new FileWriter(file));
 int last = lines.size()-1;
 for(int i = 0; i < last; i++) {
     writer.write(lines.get(i));
     writer.newLine();  //no condition check anymore
  }

 //write last line content
  writer.write(lines.get(last)); 
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.