Contents


Unit 21: I/O

Collect and manipulate external data in your Java progams

Comments

Before you begin

This unit is part of the "Intro to Java programming" learning path. Although the concepts discussed in the individual units are standalone in nature, the hands-on component builds as you progress through the units, and I recommend that you review the prerequisites, setup, and unit details before proceeding.

Unit objectives

  • Know the main uses of the java.io.File class
  • Understand how to use byte streams and character streams
  • Know how to read data from and write data to a File

Working with external data

More often than not, the data you use in your Java programs comes from an external data source, such as a database, direct byte transfer over a socket, or file storage. Most of the Java tools for collecting and manipulating external data are in the java.io package.

Files

Of all the data sources available to your Java applications, files are the most common and often the most convenient. If you want to read a file in your application, you must use streams that parse its incoming bytes into Java language types.

java.io.File is a class that defines a resource on your file system and represents that resource in an abstract way. Creating a File object is easy:

File f = new File("temp.txt");

File f2 = new File("/home/steve/testFile.txt");

The File constructor takes the name of the file it represents. The first call represents a file called temp.txt in the current directory. The second call represents a file in a specific location on my Linux system. You can pass any String to the constructor of File, provided that it's a valid file name for your operating system, whether or not the file that it references even exists.

This code asks the newly created File object if the file exists:

File f2 = new File("/home/steve/testFile.txt");
if (f2.exists()) {
  // File exists. Process it...
} else {
  // File doesn't exist. Create it...
  f2.createNewFile();
}

java.io.File has some other handy methods that you can use to:

  • Delete files
  • Create directories (by passing a directory name as the argument to File's constructor)
  • Determine if a resource is a file, directory, or symbolic link
  • More

The main action of Java I/O is in writing to and reading from data sources, which is where streams come in.

Using streams in Java I/O

You can access files on the file system by using streams. At the lowest level, streams enable a program to receive bytes from a source or to send output to a destination. Some streams handle all kinds of 16-bit characters (Reader and Writer types). Others handle only 8-bit bytes (InputStream and OutputStream types). Within these hierarchies are several flavors of streams, all found in the java.io package.

Byte streams read (InputStream and subclasses) and write (OutputStream and subclasses) 8-bit bytes. In other words, a byte stream can be considered a more raw type of stream. Here's a summary of two common byte streams and their usage:

  • FileInputStream / FileOutputStream: Reads bytes from a file, writes bytes to a file
  • ByteArrayInputStream / ByteArrayOutputStream: Reads bytes from an in-memory array, writes bytes to an in-memory array

Character streams

Character streams read (Reader and its subclasses) and write (Writer and its subclasses) 16-bit characters. Here's a selected listing of character streams and their usage:

  • StringReader / StringWriter: Read and write characters to and from Strings in memory.
  • InputStreamReader / InputStreamWriter (and subclasses FileReader / FileWriter): Act as a bridge between byte streams and character streams. The Reader flavors read bytes from a byte stream and convert them to characters. The Writer flavors convert characters to bytes to put them on byte streams.
  • BufferedReader / BufferedWriter: Buffer data while reading or writing another stream, making read and write operations more efficient.

Rather than try to cover streams in their entirety, I'll focus here on the recommended streams for reading and writing files. In most cases, these are character streams.

Reading from a File

You can read from a File in several ways. Arguably the simplest approach is to:

  1. Create an InputStreamReader on the File you want to read from.
  2. Call read() to read one character at a time until you reach the end of the file.

Listing 1 is an example in reading from a File:

Listing 1. Reading from a File
public List<Employee> readFromDisk(String filename) {
  final String METHOD_NAME = "readFromDisk(String filename)";
  List<Employee> ret = new ArrayList<>();
  File file = new File(filename);
  try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file))) {
    StringBuilder sb = new StringBuilder();
    int numberOfEmployees = 0;
    int character = reader.read();
    while (character != -1) {
        sb.append((char)character);
        character = reader.read();
    }
    log.info("Read file: \n" + sb.toString());
    int index = 0;
    while (index < sb.length()-1) {
      StringBuilder line = new StringBuilder();
      while ((char)sb.charAt(index) != '\n') {
        line.append(sb.charAt(index++));
      }
      StringTokenizer strtok = new StringTokenizer(line.toString(), Person.STATE_DELIMITER);
      Employee employee = new Employee();
      employee.setState(strtok);
      log.info("Read Employee: " + employee.toString());
      ret.add(employee);
      numberOfEmployees++;
      index++;
    }
    log.info("Read " + numberOfEmployees + " employees from disk.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

Writing to a File

As with reading from a File, you have several ways to write to a File. Once again, I go with the simplest approach:

  1. Create a FileOutputStream on the File you want to write to.
  2. Call write() to write the character sequence.

Listing 2 is an example of writing to a File:

Listing 2. Writing to a File
public boolean saveToDisk(String filename, List<Employee> employees) {
  final String METHOD_NAME = "saveToDisk(String filename, List<Employee> employees)";
  
  boolean ret = false;
  File file = new File(filename);
  try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file))) {
    log.info("Writing " + employees.size() + " employees to disk (as String)...");
    for (Employee employee : employees) {
      writer.write(employee.getState()+"\n");
    }
    ret = true;
    log.info("Done.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

Buffering streams

Reading and writing character streams one character at a time isn't efficient, so in most cases you probably want to use buffered I/O instead. To read from a file using buffered I/O, the code looks just like Listing 1, except that you wrap the InputStreamReader in a BufferedReader, as shown in Listing 3.

Listing 3. Reading from a File with buffered I/O
public List<Employee> readFromDiskBuffered(String filename) {
  final String METHOD_NAME = "readFromDisk(String filename)";
  List<Employee> ret = new ArrayList<>();
  File file = new File(filename);
  try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
    String line = reader.readLine();
    int numberOfEmployees = 0;
    while (line != null) {
      StringTokenizer strtok = new StringTokenizer(line, Person.STATE_DELIMITER);
      Employee employee = new Employee();
      employee.setState(strtok);
      log.info("Read Employee: " + employee.toString());
      ret.add(employee);
      numberOfEmployees++;
      // Read next line
      line = reader.readLine();
    }
    log.info("Read " + numberOfEmployees + " employees from disk.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

Writing to a file using buffered I/O is the same: You wrap the OutputStreamWriter in a BufferedWriter, as shown in Listing 4.

Listing 4. Writing to a File with buffered I/O
public boolean saveToDiskBuffered(String filename, List<Employee> employees) {
  final String METHOD_NAME = "saveToDisk(String filename, List<Employee> employees)";
  
  boolean ret = false;
  File file = new File(filename);
  try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)))) {
    log.info("Writing " + employees.size() + " employees to disk (as String)...");
    for (Employee employee : employees) {
      writer.write(employee.getState()+"\n");
    }
    ret = true;
    log.info("Done.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

Test your understanding

  1. True or false: A file must exist on disk before you can create a File object to represent it.
  2. Which statement best describes character streams?
    1. Character streams are 8-bit streams that are used to read and write from files into memory through ByteArrayProcessor interfaces.
    2. Character streams should not be used to read binary data.
    3. Character streams are mainly used to read and write text files.
    4. Character streams are 16-bit streams that are used to read and write data from files through Reader and Writer subclasses.
    5. None of the above
  3. Which statement best describes why you might use a BufferedReader?
    1. Using a BufferedReader to wrap an OutputStreamWriter helps it process 16-bit character streams more efficiently by buffering the input and output.
    2. When using a StreamReader and StreamWriter together, you must be careful not to cross the streams.
    3. A BufferedReader, when acting as a wrapper for InputStreamReader, helps it process 16-bit character streams more efficiently by buffering the input.
    4. A BufferedReader should never be used to read from an input stream.
    5. None of the above
  4. Create a file called lorem.txt that contains the first 250 words of lorem ipsem (you can use generator.lorem-ipsum.info or similar websites to generate this text). Save the file to the root directory of your Java project. Now write a class called Unit21 with a method called readFile() to read the file, print out its contents using a JDK Logger instance, and return the String (containing the file's contents) to the caller.

    Write a JUnit test class called Unit21Test as a test harness.

    Issues to consider:
    • How will you specify the file to be read by readFile()?
    • How will you handle exceptions?
    • How will you make sure to close any file resources when you're finished?
  5. Add a method to your Unit21 class from Question 4 called writeFile(), which writes the file you read in to a new file called lorem2.txt. Add a new test method to Unit21Test as a test harness. Note: The same types of issues that you addressed in your solution to Question 4 also apply to output streams.
  6. Augment your solution to Question 4 so that each line is no longer than maxCharactersPerLine characters long. (Don't worry about trying to preserve words when you reach the maxCharactersPerLineth character.) Write your JUnit test to specify 80 as the value for maxCharactersPerLine.
  7. Augment your solution to Question 6 so that you do not truncate a word that occurs when you hit the maxCharactersPerLineth character. Instead, output that word (and any words that follow) on the next line. This is similar to what a word processor does. Write your JUnit test to specify 80 as the value for maxCharactersPerLine. Hint: it might be easier to process the current line one word at a time, and check to see if the current word causes the line to exceed maxCharactersPerLine, and if so put that word on the next line.

Check your answers.

For further exploration

The Java Tutorials: I/O Streams

Java I/O Tutorial

Java in a Nutshell, 6th ed. by Benjamin J. Evans and David Flanagan (see Chapter 10, "File Handling and I/O")

Reading a plain text file in Java

Reading a binary input stream into a single byte array in Java

IBM Code: Java journeys

Previous: GenericsNext: Java serialization


Downloadable resources


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java development
ArticleID=1036644
ArticleTitle=Unit 21: I/O
publish-date=09142016