Java: `InputStream` line iterator

Published:

Wanted to create an easy interface for reading lines from a stream. It should take care of all the annoying Java IO nitty-gritty for me and I wanted to use it simply by throwing it into a for loop.

Found some pieces of code here and there, added some of my own and ended up with a LineReader class. Works as far as I can see, but let me know if you test it out and you find any bugs 😛

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * Represents the lines found in an {@link InputStream}. The lines are read
 * one at a time using {@link BufferedReader#readLine()} and may be streamed
 * through an iterator or returned all at once.
 *
 * <p>This class does not handle any concurrency issues.
 *
 * <p>The stream is closed automatically when the for loop is done :)
 *
 * <pre>{@code
 * for(String line : new LineReader(stream))
 *      // ...
 * }</pre>
 *
 * <p>An {@link IllegalStateException} will be thrown if any {@link IOException}s
 * occur when reading or closing the stream.
 *
 * @author    Torleif Berger
 * @license   http://creativecommons.org/licenses/by/3.0/
 * @see       http://www.geekality.net/?p=1614
 */
public class LineReader implements Iterable<String>, Closeable
{
   private BufferedReader reader;

   /**
    * Creates a new {@link LineReader}.
    *
    * <p>Uses a {@link FileReader} to read the file.
    *
    * @param file Path to file with lines to read.
    * @throws FileNotFoundException
    */
   public LineReader(String file) throws FileNotFoundException
   {
      this(new FileReader(file));
   }

   /**
    * Creates a new {@link LineReader}.
    *
    * @param stream The {@link Reader} containing the lines to read.
    */
   public LineReader(Reader reader)
   {
      this.reader = new BufferedReader(reader);
   }

   /**
    * Creates an empty {@link LineReader} with no content.
    */
   public LineReader()
   {
      this(new StringReader(""));
   }

   /**
    * Closes the underlying stream.
    */
   @Override
   public void close() throws IOException
   {
      reader.close();
   }

   /**
    * Makes sure the underlying stream is closed.
    */
   @Override
   protected void finalize() throws Throwable
   {
      close();
   }

   /**
    * Returns an iterator over the lines remaining to be read.
    *
    * <p>The underlying stream is closed automatically once {@link Iterator#hasNext()}
    * returns false, so closing it manually after using a for loop shouldn't be necessary.
    *
    * @return This iterator.
    */
   @Override
   public Iterator<String> iterator()
   {
      return new LineIterator();
   }

   /**
    * Returns all lines remaining to be read and closes the stream.
    *
    * @return The lines read from the stream.
    */
   public Collection<String> readLines()
   {
      Collection<String> lines = new ArrayList<String>();
      for(String line : this)
         lines.add(line);
      return lines;
   }

   private class LineIterator implements Iterator<String>
   {
      private String nextLine;

      public String bufferNext()
      {
         try
         {
            return nextLine = reader.readLine();
         }
         catch (IOException e)
         {
            throw new IllegalStateException("I/O error while reading stream.", e);
         }
      }

      public boolean hasNext()
      {
         boolean hasNext = nextLine != null || bufferNext() != null;

         if ( ! hasNext)
            try
            {
               reader.close();
            }
            catch (IOException e)
            {
               throw new IllegalStateException("I/O error when closing stream.", e);
            }

         return hasNext;
      }

      public String next()
      {
          if ( ! hasNext())
              throw new NoSuchElementException();

          String result = nextLine;
          nextLine = null;
          return result;
      }

      public void remove() {
          throw new UnsupportedOperationException();
      }
  }
}

You can use it in a streaming fashion:

for(String line : new LineReader(stream))
{
   // ...
}

Or grab all the lines at once:

Collection<String> lines = new LineReader(stream).readLines();

Let me know what you think! Feedback is welcome as always 🙂