Byte Streams and Character Streams in Java

InputStream and OutputStream carry raw bytes; Reader and Writer carry characters. Choosing wrongly corrupts data.

The two hierarchies

Bytes                          Characters
-----                          ----------
InputStream                    Reader
  FileInputStream                FileReader
  ByteArrayInputStream           StringReader
  BufferedInputStream            BufferedReader
  ObjectInputStream              InputStreamReader

OutputStream                   Writer
  FileOutputStream               FileWriter
  ByteArrayOutputStream          StringWriter
  BufferedOutputStream           BufferedWriter
  PrintStream                    PrintWriter
Byte streamsCharacter streams
Unit8 bit byte16 bit char
Base classesInputStream, OutputStreamReader, Writer
Encoding awareNoYes
Use forImages, audio, archives, any binary formatText of any kind
The rule is simple: text goes through a Reader or Writer, everything else through a byte stream. Reading text as bytes and assuming one byte per character breaks on any non ASCII content.

Reading bytes

try (InputStream in = Files.newInputStream(Path.of("logo.png"))) {
    byte[] header = in.readNBytes(8);
    System.out.println(Arrays.toString(header));
}

byte[] everything = Files.readAllBytes(Path.of("logo.png"));   // simplest

Writing bytes

try (OutputStream out = Files.newOutputStream(Path.of("copy.png"))) {
    out.write(everything);
}

// Copying without loading the whole file
try (InputStream in = Files.newInputStream(source);
     OutputStream out = Files.newOutputStream(target)) {
    in.transferTo(out);                 // Java 9, buffered internally
}

Reading characters

try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

Writing characters

try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
    writer.write("First line");
    writer.newLine();
    writer.write("Second line");
}

try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(path))) {
    out.printf("%s scored %d%n", "Ravi", 87);
}

Bridging the two

// Bytes in, characters out
Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);

// Characters in, bytes out
Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);

// The classic console reader
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));

InputStreamReader and OutputStreamWriter are the bridge classes. They are the only place the charset is applied, which is why it belongs there explicitly.

The decorator pattern

InputStream raw = Files.newInputStream(path);         // reads one byte at a time
InputStream buffered = new BufferedInputStream(raw);  // adds a buffer
DataInputStream data = new DataInputStream(buffered); // adds typed reads

int value = data.readInt();

Java I/O is built from wrappers. Each layer adds one capability, and closing the outermost one closes everything beneath it. That is the whole design, and it explains the long constructor chains found in older code.

Why encoding matters

String text = "café";

byte[] utf8 = text.getBytes(StandardCharsets.UTF_8);        // 5 bytes
byte[] latin1 = text.getBytes(StandardCharsets.ISO_8859_1); // 4 bytes

System.out.println(new String(utf8, StandardCharsets.UTF_8));        // café
System.out.println(new String(utf8, StandardCharsets.ISO_8859_1));   // mojibake

Bytes carry no record of their encoding. Writing with one charset and reading with another produces corrupted text, and the failure is silent.

FileReader and FileWriter

// Older code, charset was the platform default before Java 11
Reader old = new FileReader("notes.txt");

// Explicit, and preferred
Reader better = new FileReader("notes.txt", StandardCharsets.UTF_8);

// Better still
BufferedReader best = Files.newBufferedReader(Path.of("notes.txt"));

System streams

System.out    // a PrintStream to standard output
System.err    // a PrintStream to standard error
System.in     // an InputStream from standard input

System.out is a byte stream that happens to accept text, which is why it needs an internal encoder. For file output prefer a Writer.

A worked example

public static void copyAndCount(Path source, Path target) throws IOException {
    long characters = 0;

    try (BufferedReader reader = Files.newBufferedReader(source);
         BufferedWriter writer = Files.newBufferedWriter(target)) {

        char[] buffer = new char[8192];
        int read;
        while ((read = reader.read(buffer)) != -1) {
            writer.write(buffer, 0, read);
            characters += read;
        }
    }
    System.out.println("Copied " + characters + " characters");
}

Common mistakes

  • Reading text with a byte stream and assuming one byte per character.
  • Relying on the platform default charset.
  • Forgetting to close a stream, or closing it in the wrong order.
  • Reading a large file byte by byte without buffering, which is dramatically slower.
  • Ignoring the return value of read(byte[]), which may fill only part of the array.
  • Not flushing a writer before reading the file back, when it is not in a try with resources block.

Best practices

  • Use Reader and Writer for text, byte streams for binary data.
  • Always state the charset, or use the Files methods that default to UTF-8.
  • Always buffer, or use a Files method that already does.
  • Use try with resources for every stream.
  • Use transferTo for copying rather than a hand written loop.
  • Prefer the Files helpers to constructing streams directly.

Practice

  1. Copy a binary file and verify the sizes match.
  2. Write text as UTF-8 and read it back as ISO-8859-1, and explain the output.
  3. Why does read(byte[]) return a count rather than filling the array?
  4. Wrap System.in so lines of text can be read, and name each layer.
  5. Rewrite a byte by byte copy loop using transferTo and compare.

Conclusion

Byte streams move bytes and character streams move text through a charset. Choose by the data, always buffer, always close, and never let the platform decide the encoding for you.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Introduction to Java

Java is a statically typed, object oriented language that compiles to bytecode and runs on a virtual machine, which is what makes it portable.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.