Byte Streams and Character Streams in Java
InputStream and OutputStream carry raw bytes; Reader and Writer carry characters. Choosing wrongly corrupts data.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
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 streams | Character streams | |
|---|---|---|
| Unit | 8 bit byte | 16 bit char |
| Base classes | InputStream, OutputStream | Reader, Writer |
| Encoding aware | No | Yes |
| Use for | Images, audio, archives, any binary format | Text of any kind |
The rule is simple: text goes through aReaderorWriter, 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")); // simplestWriting 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)); // mojibakeBytes 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 inputSystem.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
ReaderandWriterfor text, byte streams for binary data. - Always state the charset, or use the
Filesmethods that default to UTF-8. - Always buffer, or use a
Filesmethod that already does. - Use try with resources for every stream.
- Use
transferTofor copying rather than a hand written loop. - Prefer the
Fileshelpers to constructing streams directly.
Practice
- Copy a binary file and verify the sizes match.
- Write text as UTF-8 and read it back as ISO-8859-1, and explain the output.
- Why does
read(byte[])return a count rather than filling the array? - Wrap
System.inso lines of text can be read, and name each layer. - Rewrite a byte by byte copy loop using
transferToand 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.