Data Formats
The two most basic stream classes in Java are InputStream and OutputStream.
All data that enters a process must pass through an InputStream object.
All data produced by a process must pass through an OutputStream object.
If you look at the list of methods in the OutputStream class, you will
see that the only type of data that an OutputStream object can write
is byte. Similarly for InputStream. So how do processes manage to
read and write all the kinds of data that we are used to, int,
double, String, Color, etc?
The answer to this question comes in two parts. First, every kind of data type that we work with can be converted to and from bytes. Second, Java provides a large number of specialized streams that do byte conversions automatically.
Why do InputStream and OutputStream only work with bytes? Why not make
these two classes more versatile? The answer is that these two classes are
the interface to the computer's physical devices, the memory (RAM), storage
drive, network interface card (NIC), graphics card (GPU), keyboard, etc.
All of these devices communicate using bytes. For example, everything on
a storage device is stored as a sequence of bytes. The storage device does
not have one way to store text and another way to store video. All data is
converted to bytes and then the storage device only know how to store bytes.
Same for the NIC. All data sent over a network is sent as a sequence of bytes.
The NIC never knows if it is transmitting a text document of a music stream.
The NIC only knows how to transmit and receive bytes.
InputStream and OutputStrea reflect this low level idea that everything
is a sequence of bytes. But that still doesn't answer the question of why
these two classes only read and write bytes. Why not give them methods to
read and write String objects and have those methods do the conversions
to and from sequences of bytes? The answer is a couple of related design
principles, the "Single-responsibility principle" and "Separation of concerns".
Classes should be as simple as possible and be designed to do just one task.
The InputStream class is designed to be the interface to the outside world
of bytes and so it only handles bytes. Some other class should have the task
(the responsibility) of converting a String object into a sequence of bytes.
- https://en.wikipedia.org/wiki/Single-responsibility_principle
- https://en.wikipedia.org/wiki/Separation_of_concerns
The Java library defines a large number of classes and methods with a responsibility to convert some data type into bytes. Java also has a library of layered stream classes that can use the byte conversion methods to make streams easier to use.
In this document we will look at the methods that convert data types into bytes.
In a later document we will look at Java's library of streams that build on the
InputStream and OutputStream classes.
Integers
In the Integer wrapper class, there is a built-in number that tells us how many bytes there are in an integer value.
Integer.BYTES
Most of the wrapper classes for the primitive types have a BYTES field
built into them. If you open a JShell session, then you can quickly see
what their values are.
jshell> Long.BYTES
jshell> Integer.BYTES
jshell> Short.BYTES
jshell> Byte.BYTES
jshell> Double.BYTES
jshell> Float.BYTES
jshell> Character.BYTES
The Integer class does not have a method that converts an integer value
into its four byte values. To do that, we use the java.nio.ByteBuffer
class.
The ByteBuffer class can be thought of as a factory class for constructing
arrays that hold the bytes from primitive values. The ByteByuffer class does
not have any constructors. It has static factory methods that build and return
ByteArray objects.
The static factory method allocate(int) builds and returns a ByteBuffer
of the specified size.
The static factory method wrap(byte[]) builds and returns a ByteBuffer
from the given byte array.
There is also an array() method that extracts a byte array from
a ByteBuffer object.
Here is an example of a line of code that creates a ByteBuffer object holding
the bytes from an int value and then converts the ByteBuffer into a byte
array.
byte[] bytes = ByteBuffer.allocate(Integer.BYTES)
.putInt(12345)
.array();
This line of code lets us see what the bytes are in the value 123456.
You can try this in JShell by entering the following line of code at the JShell prompt.
jshell> var bytes = java.nio.ByteBuffer.allocate(Integer.BYTES).putInt(12345).array()
We can also go in the other direction. Given a byte array holding four bytes,
we can find out what int value those bytes represent.
The following code defines an array of four bytes, wraps a ByteBuffer
around them, and then uses the ByteBuffer to get the int value that
the four bytes represent.
byte[] bytes = {0, 0, 48, 57};
int n = ByteBuffer.wrap(bytes)
.getInt();
You can try this in JShell by entering the following line of code at the JShell prompt.
jshell> var n = java.nio.ByteBuffer.wrap(new byte[]{0, 0, 48, 57}).getInt()
We can put the last two examples on either end of a pipe and create a "client/server" pair where the server sends an integer, as four bytes, to its client.
Here is the code for "Server.java". It reads an integer value from its
standard input stream, then uses a ByteBuffer object to get the four
bytes that make up that integer, and then writes those four bytes to
its standard output stream.
import java.nio.ByteBuffer;
public class Server
{
public static void main(String[] args)
{
final int n = new java.util.Scanner(System.in).nextInt();
byte[] bytes = ByteBuffer.allocate(Integer.BYTES)
.putInt(n)
.array();
System.out.write(bytes[0]);
System.out.write(bytes[1]);
System.out.write(bytes[2]);
System.out.write(bytes[3]);
System.out.flush(); // Try commenting this out.
}
}
Here is the code for "Client.java". It reads four bytes from its standard
input stream, then uses a ByteBuffer object to convert those four bytes
into an integer value, and then sends that integer value as a String
to its standard output stream.
import java.nio.ByteBuffer;
import java.io.IOException;
public class Client
{
public static void main(String[] args) throws IOException
{
final int b1 = System.in.read();
final int b2 = System.in.read();
final int b3 = System.in.read();
final int b4 = System.in.read();
final byte[] bytes = {(byte)b1,
(byte)b2,
(byte)b3,
(byte)b4};
int n = ByteBuffer.wrap(bytes)
.getInt();
System.out.println(n);
}
}
We can connect these two program to each other using a pipe at the command-line.
> java Server | java Client
Type an integer at the console and tap Enter. The server reads the integer,
sends four bytes over the pipe, and closes the pipe. The client reads four
bytes from the pipe, puts them together as an int, and prints the result.
Break up this pipeline into steps. Run the following command-line.
> java Server > temp
Type an integer at the console and tap Enter. The server reads the integer,
sends four bytes to the file "temp" and then closes the file. Use the Windows
Explorer view of your folder to see that the file "temp" holds exactly four
bytes.
Now run the following command-line.
> java Client < temp
The client reads four bytes from the file "temp", puts them together as an int, and prints the result, which should be the integer you entered into the server.
You can use a hex editor (or "TextPad") to open the "temp" file to see what
four bytes represented your integer value. For example, if your integer is
-1, then the four bytes are FF, FF, FF, and FF (in hexadecimal).
If your integer 0, then the four bytes are 00, 00, 00, and 00.
Notice that every integer value, no matter how large or small, will be
represented by exactly four bytes.