Binary I/O Classes
Tags: java, programming, learning, beginners
Published: 2024-07-01 12:52:41

The abstract **InputStream** is the root class for reading binary data, and the abstract **OutputStream** is the root class for writing binary data. The design of the Java I/O classes is a good example of applying inheritance, where common operations are generalized in superclasses, and subclasses provide specialized operations.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z4z9jqvxwbgfzn9siah8.png)

Figure above lists some of the classes for performing binary I/O. **InputStream** is the root for binary input classes, and **OutputStream** is the root for binary output classes. Figures below list all the methods in the classes **InputStream** and **OutputStream**. All the methods in the binary I/O classes are declared to throw **java.io.IOException** or a subclass of **java.io.IOException**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8kgcxd4bumuaogvldn1v.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/24lsnhtrn012kghfav2k.png)

## FileInputStream/FileOutputStream

**FileInputStream**/**FileOutputStream** is for reading/writing bytes from/to files. All the methods in these classes are inherited from **InputStream** and **OutputStream**. **FileInputStream**/**FileOutputStream** does not introduce new methods. To construct a **FileInputStream**, use the constructors shown in Figure below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqkzu427no1jk2yncsho.png)

A **java.io.FileNotFoundException** will occur if you attempt to create a
**FileInputStream** with a nonexistent file.

To construct a **FileOutputStream**, use the constructors shown in Figure below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bj85b6kpmoj411b4tl6x.png)

If the file does not exist, a new file will be created. If the file already exists, the first two constructors will delete the current content of the file. To retain the current content and append new data into the file, use the last two constructors and pass **true** to the **append** parameter.

Almost all the methods in the I/O classes throw **java.io.IOException**. Therefore, you have to declare to throw **java.io.IOException** in the method or place the code in a try-catch block, as shown below:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/juz6glbxdfpcw0iq06sa.png)

The code below uses binary I/O to write ten byte values from **1** to **10** to a file named temp.dat and reads them back from the file.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/36bqmn2zk4ymc50hn815.png)

The program uses the try-with-resources to declare and create input and output streams so that they will be automatically closed after they are used. The **java.io.InputStream** and **java.io.OutputStream** classes implement the **AutoClosable** interface. The **AutoClosable** interface defines the **close()** method that closes a resource. Any object of the **AutoClosable** type can be used with the try-with-resources syntax for automatic closing.

A **FileOutputStream** is created for the file **temp.dat** in line 8. The **for** loop writes ten byte values into the file (lines 11–12). Invoking **write(i)** is the same as invoking **write((byte)i)**. Line 17 creates a **FileInputStream** for the file **temp.dat**. Values are read from the file and displayed on the console in lines 20–22. The expression **((value = input.read()) != -1)** (line 21) reads a byte from **input.read()**, assigns it to **value**, and checks whether it is **–1**. The input value of –1 signifies the end of a file.

The file **temp.dat** created in this example is a binary file. It can be read from a Java program but not from a text editor. 

When a stream is no longer needed, always close it using the **close()** method or automatically close it using a try-with-resource statement. Not closing streams may cause data corruption in the output file, or other programming errors.

The root directory for the file is the classpath directory. For the example in this book, the root directory is c:\book, so the file **temp.dat** is located at c:\book. If you wish to place **temp.dat** in a specific directory, replace line 6 with

`FileOutputStream output = new FileOutputStream ("directory/temp.dat");`

An instance of **FileInputStream** can be used as an argument to construct a **Scanner**, and an instance of **FileOutputStream** can be used as an argument to construct a **PrintWriter**. You can create a **PrintWriter** to append text into a file using

`new PrintWriter(new FileOutputStream("temp.txt", true));`

If **temp.txt** does not exist, it is created. If **temp.txt** already exists, new data are appended to the file.

## FilterInputStream/FilterOutputStream

_Filter streams_ are streams that filter bytes for some purpose. The basic byte input stream provides a **read** method that can be used only for reading bytes. If you want to read integers, doubles, or strings, you need a filter class to wrap the byte input stream. Using a filter class enables you to read integers, doubles, and strings instead of bytes and characters. **FilterInputStream** and **FilterOutputStream** are the base classes for filtering data. When you need to process primitive numeric types, use **DataInputStream** and **DataOutputStream** to filter bytes.

## DataInputStream/DataOutputStream

**DataInputStream** reads bytes from the stream and converts them into appropriate primitive-type values or strings. **DataOutputStream** converts primitive-type values or strings into bytes and outputs the bytes to the stream.

**DataInputStream** extends **FilterInputStream **and implements the **DataInput** interface, as shown in Figure below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fee4xynxnvwe5g0v21xb.png)

**DataOutputStream** extends **FilterOutputStream** and implements the **DataOutput** interface, as shown in Figure below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mhji8v8oh6d1ypmgd7cg.png)

**DataInputStream** implements the methods defined in the **DataInput** interface to read primitive data-type values and strings. **DataOutputStream** implements the methods defined in the **DataOutput** interface to write primitive data-type values and strings. Primitive values are copied from memory to the output without any conversions. Characters in a string may be written in several ways, as discussed in the next section.

## Characters and Strings in Binary I/O

A Unicode character consists of two bytes. The **writeChar(char c)** method writes the Unicode of character **c** to the output. The **writeChars(String s)** method writes the Unicode for each character in the string s to the output. The **writeBytes(String s)** method writes the lower byte of the Unicode for each character in the string **s** to the output. The high byte of the Unicode is discarded. The **writeBytes** method is suitable for strings that consist of ASCII characters, since an ASCII code is stored only in the lower byte of a Unicode. If a string consists of non-ASCII characters, you have to use the **writeChars** method to write the string.

The **writeUTF(String s)** method writes two bytes of length information to the output stream, followed by the modified UTF-8 representation of every character in the string **s**. UTF-8 is a coding scheme that allows systems to operate with both ASCII and Unicode. Most operating systems use ASCII. Java uses Unicode. The ASCII character set is a subset of the Unicode character set. Since most applications need only the ASCII character set, it is a waste to represent an 8-bit ASCII character as a 16-bit Unicode character. The modified UTF-8 scheme stores a character using one, two, or three bytes. Characters are coded in one byte if their code is less than or equal to **0x7F**, in two bytes if their code is greater than **0x7F** and less than or equal to **0x7FF**, or in three bytes if their code is greater than **0x7FF**.

The initial bits of a UTF-8 character indicate whether a character is stored in one byte, two bytes, or three bytes. If the first bit is **0**, it is a one-byte character. If the first bits are **110**, it is the first byte of a two-byte sequence. If the first bits are **1110**, it is the first byte of a three byte sequence. The information that indicates the number of characters in a string is stored in the first two bytes preceding the UTF-8 characters. For example, **writeUTF("ABCDEF")** actually writes eight bytes (i.e., **00 06 41 42 43 44 45 46**) to the file, because the first two bytes store the number of characters in the string.

The **writeUTF(String s)** method converts a string into a series of bytes in the UTF-8 format and writes them into an output stream. The **readUTF()** method reads a string that has been written using the **writeUTF** method.

The UTF-8 format has the advantage of saving a byte for each ASCII character, because a Unicode character takes up two bytes and an ASCII character in UTF-8 only one byte. If most of the characters in a long string are regular ASCII characters, using UTF-8 is more efficient.

## Creating DataInputStream/DataOutputStream

**DataInputStream**/**DataOutputStream** are created using the following constructors:

`public DataInputStream(InputStream instream)
public DataOutputStream(OutputStream outstream)`

The following statements create data streams. The first statement creates an input stream for the file **in.dat**; the second statement creates an output stream for the file **out.dat**.

`DataInputStream input = new DataInputStream(new FileInputStream("in.dat"));
DataOutputStream output = new DataOutputStream(new FileOutputStream("out.dat"));`

The code below writes student names and scores to a file named **temp.dat** and reads the data back from the file.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f95lwa8q12uog5jxhppc.png)

A **DataOutputStream** is created for file **temp.dat** in lines 8. Student names and scores are written to the file in lines 11–16. A **DataInputStream** is created for the same file in lines 20. Student names and scores are read back from the file and displayed on the console in lines 23–25.

**DataInputStream** and **DataOutputStream** read and write Java primitive-type values and strings in a machine-independent fashion, thereby enabling you to write a data file on one machine and read it on another machine that has a different operating system or file structure. An application uses a data output stream to write data that can later be read by a program using a data input stream.

**DataInputStream** filters data from an input stream into appropriate primitive-type values or strings. **DataOutputStream** converts primitive-type values or strings into bytes and outputs the bytes to an output stream. You can view **DataInputStream**/**FileInputStream** and **DataOutputStream**/**FileOutputStream** working in a pipe line as shown in Figure below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zimzaz5828m2wudbguzt.png)

You have to read data in the same order and format in which they are stored. For example, since names are written in UTF-8 using **writeUTF**, you must read names using **readUTF**.

## Detecting the End of a File

If you keep reading data at the end of an **InputStream**, an **EOFException** will occur. This exception can be used to detect the end of a file, as shown in the code below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i2itwsx9hh2oj9u5g8ho.png)

The program writes three double values to the file using **DataOutputStream** (lines 8–12) and reads the data using **DataInputStream** (lines 14–17). When reading past the end of the file, an **EOFException** is thrown. The exception is caught in line 19.

## BufferedInputStream/BufferedOutputStream

**BufferedInputStream**/**BufferedOutputStream** can be used to speed up input and output by reducing the number of disk reads and writes. Using **BufferedInputStream**, the whole block of data on the disk is read into the buffer in the memory once. The individual data are then delivered to your program from the buffer, as shown in Figure below (a). Using **BufferedOutputStream**, the individual data are first written to the buffer in the memory. When the buffer is full, all data in the buffer are written to the disk once, as shown in Figure below (b).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ult6p4pp1meh9jz0paey.png)

**BufferedInputStream**/**BufferedOutputStream** does not contain new methods. All the methods in **BufferedInputStream**/**BufferedOutputStream** are inherited from the **InputStream**/**OutputStream** classes. **BufferedInputStream**/**BufferedOutputStream** manages a buffer behind the scene and automatically reads/writes data from/to disk on demand.

You can wrap a **BufferedInputStream**/**BufferedOutputStream** on any **InputStream**/**OutputStream** using the constructors shown in Figures below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zat8qefm2vdsgjrszh7g.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b8tc32k6uozg3u55daya.png)

If no buffer size is specified, the default size is **512** bytes. You can improve the performance of the **TestDataStream** program in TestDataStream.java above by adding buffers in the stream in lines 8 and lines 20, as follows:

`DataOutputStream output = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("temp.dat")));
DataInputStream input = new DataInputStream(
new BufferedInputStream(new FileInputStream("temp.dat")));`

You should always use buffered I/O to speed up input and output. For small files, you may not notice performance improvements. However, for large files—over 100 MB—you will see substantial improvements using buffered I/O.