Friday, October 26, 2007

Making BufferedReader Iterable

I was reading recently about a issues around reading an entire file into a string. I pretty much never do that, since I like my code to be robust even in the face of very large files. But it did get me thinking about reading in lines of a file one at a time. I do have reason to do that on occasion.

Originally, I might have done it something like this:

File file = ...
BufferedReader br = new BufferedReader(new FileReader(file));
String line = br.readLine();
while ( line != null ) {
// Do something with line
line = br.readLine();
}

That's not bad, but I've been working with Ruby lately and really like the terse yet easy-to-read nature of the code. We can improve the above code with a for loop:

File file = ...
BufferedReader br = new BufferedReader(new FileReader(file));
for (String line = br.readLine(); line != null; line = br.readLine()) {
// Do something with line
}

Better, but still not great. Now, what if the BufferedReader were Iterable? It isn't, but we can wrap an Iterable around it. First, let's look at usage:

File file = ...
BufferedReaderIterable bri = new BufferedReaderIterable(new BufferedReader(new FileReader(file)));
for (String read : bri) {
// Do something with the line
}

Very nice. So, all we need is to implement BufferedReaderIterable. Happily, it's quite simple:

public class BufferedReaderIterable implements Iterable<String> {

private Iterator<String> i;

public BufferedReaderIterable( BufferedReader br ) {
i = new BufferedReaderIterator( br );
}
public Iterator iterator() {
return i;
}

private class BufferedReaderIterator implements Iterator<String> {
private BufferedReader br;
private java.lang.String line;

public BufferedReaderIterator( BufferedReader aBR ) {
(br = aBR).getClass();
advance();
}

public boolean hasNext() {
return line != null;
}

public String next() {
String retval = line;
advance();
return retval;
}

public void remove() {
throw new UnsupportedOperationException("Remove not supported on BufferedReader iteration.");
}

private void advance() {
try {
line = br.readLine();
}
catch (IOException e) { /* TODO */}
}
}
}

It would be nice if we could make the construction cleaner. What if BufferedReaderIterable had a constructor that took a File?


public class BufferedReaderIterable implements Iterable<String> {

private BufferedReader mine;
private Iterator<String> i;

public BufferedReaderIterable( BufferedReader br ) {
i = new BufferedReaderIterator( br );
}

public BufferedReaderIterable( File f ) throws FileNotFoundException {
mine = new BufferedReader( new FileReader( f ) );
i = new BufferedReaderIterator( mine );
}
public Iterator<String> iterator() {
return i;
}

private class BufferedReaderIterator implements Iterator<String> {
private BufferedReader br;
private String line;

public BufferedReaderIterator( BufferedReader aBR ) {
(br = aBR).getClass();
advance();
}

public boolean hasNext() {
return line != null;
}

public String next() {
String retval = line;
advance();
return retval;
}

public void remove() {
throw new UnsupportedOperationException("Remove not supported on BufferedReader iteration.");
}

private void advance() {
try {
line = br.readLine();
}
catch (IOException e) { /* TODO */}
if ( line == null && mine != null ) {
try {
mine.close();
}
catch (IOException e) { /* Ignore - probably should log an error */ }
mine = null;
}
}
}
}

Now, usage is even cleaner:

File file = ...
BufferedReaderIterable bri = new BufferedReaderIterable(file);
for (String read : bri) {
// Do something with the line
}

[Editorial Note: Updated to put the template angle brackets back in after Eric Burke noticed Blogger was eating them]