1. Introduction
In this tutorial, we’re going to compare the PrintStream and PrintWriter Java classes. This article will help programmers find a suitable use case for each of these classes.
Before diving into the content, we suggest looking at our previous articles, where we demonstrate how to use PrintStream and PrintWriter.
2. Similarities Between PrintStream and PrintWriter
Because PrintStream and PrintWriter share some of their functionalities, programmers sometimes struggle to find the appropriate use case for these classes. Let’s first identify their similarities; then, we’ll look at the differences.
2.1. Character Encoding
Regardless of the system, character encoding allows programs to manipulate text so that it is interpreted consistently across platforms.
After the JDK 1.4 release, the PrintStream class included a character encoding parameter in its constructor. This allows the PrintStream class to encode/decode text in cross-platform implementations. On the other hand, PrintWriter has always had a character encoding functionality since its beginning.
We can refer to the official Java code to confirm:
public PrintStream(OutputStream out, boolean autoFlush, String encoding) throws UnsupportedEncodingException {
this(requireNonNull(out, "Null output stream"), autoFlush, toCharset(encoding));
}
Similarly, the PrintWriter constructor has a charset parameter to specify the Charset for encoding purposes:
public PrintWriter(OutputStream out, boolean autoFlush, Charset charset) {
this(new BufferedWriter(new OutputStreamWriter(out, charset)), autoFlush);
// save print stream for error propagation
if (out instanceof java.io.PrintStream) {
psOut = (PrintStream) out;
}
}
If a character encoding is not provided to either of these classes, they will use the default platform encoding.
2.2. Writing to a File
To write text to a file, we can pass a String or File instance to the corresponding constructors. Also, we can pass the charset for character encoding.
As an example, we’ll refer to the constructor having a single File parameter. In this case, the character encoding will default to the platform:
public PrintStream(File file) throws FileNotFoundException {
this(false, new FileOutputStream(file));
}
Similarly, the PrintWriter class has a constructor to specify a file to write to:
public PrintWriter(File file) throws FileNotFoundException {
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))), false);
}
As we can see, both classes provide the functionality to write to files. However, their implementations differ by using different stream parent classes. We’ll dive into why this is the case in the differences section of this article.
3. Differences Between PrintStream and PrintWriter
In the previous section, we showed that PrintStream and PrintWriter share some functionalities that may suit our case. Nonetheless, even though we can do the same things with these classes, their implementations vary, making us evaluate which class fits better.
Now, let’s look at the differences between PrintStream and PrintWriter.
3.1. Data Processing
In the last section, we showed how both classes can write to files. Let’s look at how their implementations differ.
In the case of PrintStream, it is a subclass of OutputStream, which is defined in Java as a byte stream. In other words, data is processed byte-by-byte. PrintWriter, on the other hand, is a character stream that processes each character at a time and uses Unicode to automatically translate to and from each character set we specified.
We’ll show each of these implementations in two different cases.
3.2. Processing Non-Text Data
Because both classes process data differently, we can make a distinction specifically when dealing with non-text files. In this example, we’ll use a png file to read data, and subsequently see the difference after writing its content to another file with each class:
public class DataStream {
public static void main (String[] args) throws IOException {
FileInputStream inputStream = new FileInputStream("image.png");
PrintStream printStream = new PrintStream("ps.png");
int b;
while ((b = inputStream.read()) != -1) {
printStream.write(b);
}
printStream.close();
FileReader reader = new FileReader("image.png");
PrintWriter writer = new PrintWriter("pw.png");
int c;
while ((c = reader.read()) != -1) {
writer.write(c);
}
writer.close();
inputStream.close();
}
}
In this example, we use FileInputStream and FileReader to read the content of an image. Then, we write the data to different output files.
As a result, the ps.png and the pw.png files will contain data according to how their content was processed by the stream. We know that PrintStream processes the data by reading one byte at a time. Therefore, the resulting file contains the same raw data as the original file.
Unlike the PrintStream class, PrintWriter interprets the data as characters. This results in a corrupted file whose content the system is not able to understand. Alternatively, we could change the extension of pw.png to pw.txt and check how the PrintWriter tried to translate the raw data of the image into illegible symbols.
3.3. Processing Text Data
Now, let’s look at an example where we use OutputStream, the parent class of PrintStream, to demonstrate how strings are processed when writing to a file:
public class PrintStreamWriter {
public static void main (String[] args) throws IOException {
OutputStream out = new FileOutputStream("TestFile.txt");
out.write("foobar");
out.flush();
out.close();
}
}
The code above will not compile since OutputStream doesn’t know how to deal with strings. To write to a file successfully, the input data must be a sequence of raw bytes. The following change will make our code write to the file successfully:
out.write("foobar".getBytes());
Going back to PrintStream, although this class is a subclass of OutputStream, Java internally calls the getBytes() method. This allows PrintStream to accept strings when calling the print method*.* Let’s see an example:
public class PrintStreamWriter {
public static void main (String[] args) throws IOException {
PrintStream out = new PrintStream("TestFile.txt");
out.print("Hello, world!");
out.flush();
out.close();
}
}
Now, because PrintWriter knows how to handle strings, we call the print method passing the string input. However, in this case, Java does not convert the string into bytes, but it internally translates each character in the stream into its corresponding Unicode encoding:
public class PrintStreamWriter {
public static void main (String[] args) throws IOException {
PrintWriter out = new PrintWriter("TestFile.txt");
out.print("Hello, world!");
out.flush();
out.close();
}
}
Based on how these classes process text data internally, a character stream class such as PrintWriter handles this content better when doing I/O operations with text. Besides, translating the data into Unicode during the encoding process of the local character set makes internationalization simpler for the application.
3.4. Flushing
In our previous example, notice how we had to call the flush method explicitly*.* According to the Java documentation, this process works differently between these two classes.
For PrintStream, we can specify flushing to be automatic only when writing a byte array, calling the println method, or writing the newline character. However, PrintWriter can also have automatic flushing, but only when we invoke the println, printf, or format methods.
This distinction is hard to prove since the documentation mentions flushing will happen in the cases explained above, but it does not mention when it will not happen. Therefore, we can demonstrate how automatic flushing works in both classes, but we cannot guarantee it will behave as expected.
In this example, we’ll enable the automatic flushing feature and write a string with the newline character at the end:
public class AutoFlushExample {
public static void main (String[] args) throws IOException {
PrintStream printStream = new PrintStream(new FileOutputStream("autoFlushPrintStream.txt"), true);
printStream.write("Hello, world!\n".getBytes());
printStream.close();
PrintWriter printWriter = new PrintWriter(new FileOutputStream("autoFlushPrintWriter.txt"), true);
printWriter.print("Hello, world!");
printWriter.close();
}
}
It is guaranteed that the file autoFlushPrintStream.txt will contain content written to the file since we enabled the automatic flushing feature. Additionally, we’re calling the write method with a string containing the newline character to force flushing.
However, we expect to see the autoFlushPrintWriter.txt file empty, although this is not guaranteed. After all, flushing could have happened during the execution of the program.
If we want to force flushing when using PrintWriter, the code must meet all requirements we mentioned above, or we can add a line of code to flush the writer explicitly:
printWriter.flush();
4. Conclusion
In this article, we compared the two data stream classes PrintStream and PrintWriter. First, we looked at their similarities and capabilities to use a local character set. Also, we covered examples of how to read from and write to external files. Although we can achieve similar things with both classes, after looking at the differences, we demonstrated that each class performs better in different scenarios.
For example, we benefit from PrintStream when writing all types of data because PrintStream deals with raw bytes. PrintWriter as a character stream, on the other hand, is best suited for text when executing I/O operations. Further, it helps with complex software implementations such as internationalization due to its internal format in Unicode. Lastly, we compared how the flushing implementation works differently in both classes.
As always, the complete code for this article is available over on GitHub.