I/O
Table of Contents
Introduction
A computer would be all but useless if there was no way of retrieving the information it produced. Similarly, a computer would be incredibly difficult to use if there was no way to send information into it. This is why all computers provide components for input and output (IO). Java supplies a variety of abstractions to facilitate taking input and giving output. In this chapter, we'll go over a sampling of different objects built into the standard library.
PrintStream
First up is the PrintStream. Java's PrintStream is used for printing characters to files. You've actually been using a PrintStream since your first program! System.out is actually a PrintStream object:
1: public class SystemOut{ 2: public static void main(String[] args){ 3: System.out.println(System.out.getClass()); 4: } 5: }
If we compile and run the above we get:
josephraskind@stargazer:/tmp/IO$ java SystemOut.java class java.io.PrintStream
We can easily create a new PrintStream object, attach it to a file, and write to it:
1: import java.io.PrintStream; 2: import java.io.FileNotFoundException; 3: public class UsingPrintStream{ 4: public static void main(String[] args) throws FileNotFoundException{ 5: PrintStream myStream = new PrintStream("./output.txt"); 6: myStream.println("Hello CS210! I'm in a file!"); 7: } 8: }
After compiling and running we get:
josephraskind@stargazer:/tmp/IO$ ls UsingPrintStream.java josephraskind@stargazer:/tmp/IO$ java UsingPrintStream.java josephraskind@stargazer:/tmp/IO$ ls output.txt UsingPrintStream.java josephraskind@stargazer:/tmp/IO$ cat output.txt Hello CS210! I'm in a file!
During the instantiation a file descriptor is opened up by Java and is wrapped around in the trappings of the PrintStream object. We can see this play out in (somewhat) real time in Linux:
1: import java.io.PrintStream; 2: import java.io.FileNotFoundException; 3: import java.lang.Thread; 4: public class UsingPrintStream{ 5: public static void main(String[] args) throws FileNotFoundException, InterruptedException{ 6: PrintStream myStream = new PrintStream("./output.txt"); 7: myStream.println("Hello CS210! I'm in a file!"); 8: while(true){ 9: Thread.sleep(10000); 10: } 11: } 12: }
In Linux, we can track open file descriptors of processes if we have their process id's:
josephraskind@stargazer:/tmp/IO$ java UsingPrintStream.java & [1] 6515 josephraskind@stargazer:/tmp/IO$ ls /proc/6515/fd/ 0 1 2 3 4 josephraskind@stargazer:/tmp/IO$ ls -l /proc/6515/fd/4 l-wx------ 1 josephraskind josephraskind 64 Aug 2 17:41 /proc/6515/fd/4 -> /tmp/IO/output.txt
We can see that the latest file descriptor associated with the UsingPrintStream program is 4 which is tied to /tmp/IO/output.txt!
InputStream
PrintStream is an example of one of Java's OutputStream variants. The flip-side to the OutputStream is the InputStream. System.in is an example of an InputStream object:
public class SystemIn{
public static void main(String[] args){
System.out.println(System.in.getClass());
}
}
After compiling and running we get:
josephraskind@stargazer:/tmp/IO$ java SystemIn.java class java.io.BufferedInputStream
We can see that System.in is a BufferedInputStream object which is a descendant of InputStream. A quick trip to the documentation tells us that BufferedInputStream implements a read() method which will read a single byte from the connected stream. We can use System.in to check that out:
1: import java.io.IOException; 2: public class UsingInputStream{ 3: public static void main(String[] args) throws IOException{ 4: System.out.println(System.in.read()); 5: } 6: }
After compiling and running:
1: josephraskind@stargazer:/tmp/IO$ java UsingInputStream.java 2: a 3: 97
Line 2 shows my input, Line 3 shows the println output. We can see that the "a" character value matches the equivalent value in ASCII1.
FileReader
The InputStream variants are nice, but they're often not that convenient for handling files. That's because they were made for other IO objects to use, like FileReader. FileReader, as the name suggests, is explicitly meant to be used to read files. Let's take a look at an example of using it in tandem with BufferedReader to print out one of my favourite Shakespeare sonnets:
1: import java.io.*; 2: public class UsingFileReader{ 3: public static void main(String[] args){ 4: String filename = args[0]; 5: String input; 6: try(BufferedReader fp = new BufferedReader(new FileReader(filename))){ 7: while((input = fp.readLine()) != null){ 8: System.out.println(input); 9: } 10: }catch(IOException e){ 11: System.err.println(e); 12: } 13: } 14: }
There's some new syntax being introduced here, so I'll explain that first. Line 6 shows a try keyword with a set of parentheses. Inside those parentheses we can see a variable declaration and assignment. Variables which are declared within the try() are guaranteed to be closed regardless of whether or not an error occurs—every declared variable must be an AutoCloseable. It's considered good style to use that syntax whenever applicable. Line 7 invokes the BufferedReader instance method readLine() which does what it says on the tin.
Here's what happens when we run the code:
josephraskind@stargazer:/tmp/IO$ java UsingFileReader.java ./sonnet_60.txt Like as the waves make towards the pebbl'd shore, So do our minutes hasten to their end; Each changing place with that which goes before, In sequent toil all forwards do contend. Nativity, once in the main of light, Crawls to maturity, wherewith being crown'd, Crooked eclipses 'gainst his glory fight, And Time that gave doth now his gift confound. Time doth transfix the flourish set on youth And delves the parallels in beauty's brow, Feeds on the rarities of nature's truth, And nothing stands but for his scythe to mow: And yet to times in hope my verse shall stand, Praising thy worth, despite his cruel hand.
The BufferedFileReader recognizes the end of a line due to the special "\n" character and converts it to a Java String which is what allows us to print it using println().
Exercises
- Compile and run Listing 11. Then modify it to print the total number of lines in the file alongside the contents. Next, modify it again to print only lines that contain a specific word of your choosing. What class and method are you using to check whether a line contains the word?
- The chapter shows that
FileReaderthrows a checkedIOException. Write a program that attempts to open a file that does not exist and catches the exception gracefully, printing a friendly error message rather than a stack trace. Then write a second version usingthrowsinstead of try-catch. In your own words explain the practical difference between the two approaches from the perspective of whoever calls your method. - The chapter introduces
BufferedWriterfor writing to files. Write a program that takes the contents of one file, converts every line to uppercase, and writes the result to a new file. Verify the output by reading the new file back withBufferedReaderand printing its contents. What would happen if you forgot to callcloseon the writer? - The chapter shows that
BufferedReaderwraps aFileReaderto add buffering. Write a program that reads the same file twice: once usingFileReaderdirectly character by character, and once usingBufferedReaderwithreadLine. Measure the time taken for each approach on a large file. What difference do you observe? In your own words explain what buffering does and why it improves performance when reading from disk. - The chapter introduces the try-with-resources syntax as a cleaner alternative to manually closing streams. Rewrite Listing 11 using try-with-resources. Then deliberately throw an exception inside the try block and verify that the file is still closed correctly by adding a print statement to a
finallyblock. Compare the two approaches and explain what try-with-resources guarantees that a manualclosecall does not.