CodeGym /Java Course /Module 1. Java Syntax /ByteArrayOutputStream

ByteArrayOutputStream

Module 1. Java Syntax
Level 25 , Lesson 1
Available

The ByteArrayOutputStream class implements an output stream that writes data to a byte array. The buffer grows automatically as data is written to it.

The ByteArrayOutputStream class creates a buffer in memory, and all the data sent to the stream is stored in the buffer.

ByteArrayOutputStream constructors

The ByteArrayOutputStream class has the following constructors:

Constructor
ByteArrayOutputStream() This constructor creates an in-memory buffer that is 32 bytes long.
ByteArrayOutputStream(int a) This constructor creates an in-memory buffer with a specific size.

And this is what the class looks like inside:


// The buffer itself, where the data is stored.
protected byte buf[];

// Current number of bytes written to the buffer.
protected int count;

public ByteArrayOutputStream() {
    this(32);
}

public ByteArrayOutputStream(int size) {
    if (size < 0) {
        throw new IllegalArgumentException("Negative initial size: "
                                           + size);
    }
    buf = new byte[size];
}
    

Methods of the ByteArrayOutputStream class

Let's talk about the methods we can use in our class.

Let's try to put something in our stream. To do this, we'll use the write() method — it can accept one byte or a set of bytes for writing.

Method
void write(int b) Writes one byte.
void write(byte b[], int off, int len) Writes an array of bytes of a specific size.
void writeBytes(byte b[]) Writes an array of bytes.
void writeTo(OutputStream out) Writes all data from the current output stream to the passed output stream.

Method implementation:


public static void main(String[] args) throws IOException {
   ByteArrayOutputStream outputByte = new ByteArrayOutputStream();
   // Write one byte
   while(outputByte.size()!= 7) {
      outputByte.write("codegym".getBytes());
   }

   // Write array of bytes
   String value = "\nWelcome to Java\n";
   byte[] arrBytes = value.getBytes();
   outputByte.write(arrBytes);

   // Write part of an array
   String codeGym = "CodeGym";
   byte[] b = codeGym.getBytes();
   outputByte.write(b, 4, 3);

   // Write to a file
   FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
   outputByte.write(80);
   outputByte.writeTo(fileOutputStream);
}
    

The result is a new output.txt file that looks like this:

The toByteArray() method returns the current contents of this output stream as an array of bytes. And you can use the toString() method to get the buf byte array as text:


public static void main(String[] args) throws IOException {
    ByteArrayOutputStream outputByte = new ByteArrayOutputStream();

    String value = "CodeGym";
    outputByte.write(value.getBytes());

    byte[] result = outputByte.toByteArray();
    System.out.println("Result: ");

    for(int i = 0 ; i < result.length; i++) {
        // Display the characters
        System.out.print((char)result[i]);
    }
}
    

Our buffer contains the byte array that we passed to it.

The reset() method resets the number of valid bytes in the byte array output stream to zero (so everything accumulated in the output is reset).


public static void main(String[] args) throws IOException {
   ByteArrayOutputStream outputByte = new ByteArrayOutputStream(120);

   String value = "CodeGym";
   outputByte.write(value.getBytes());
   byte[] result = outputByte.toByteArray();
   System.out.println("Output before reset: ");

   for (byte b : result) {
      // Display the characters
      System.out.print((char) b);
   }

   outputByte.reset();

   byte[] resultAfterReset = outputByte.toByteArray();
   System.out.println("\nOutput after reset: ");

   for (byte b : resultAfterReset) {
      // Display the characters
      System.out.print((char) b);
   }
}
    

When we display our buffer after calling the reset() method, we get nothing.

Specific features of the close() method

This method deserves special attention. To understand what it does, let's take a peek inside:


/**
 * Closing a {@code ByteArrayOutputStream} has no effect. The methods in
 * this class can be called after the stream has been closed without
 * generating an {@code IOException}.
 */
public void close() throws IOException {
}
    

Note that the ByteArrayOutputStream class's close() method doesn't actually do anything.

Why is that? A ByteArrayOutputStream is a memory-based stream (that is, it is managed and populated by the user in code), so calling close() has no effect.

Practice

Now let's try to implement a file system using our ByteArrayOutputStream and ByteArrayInputStream.

Let's write a FileSystem class using the singleton design pattern and use a static HashMap<String, byte[]>, where:

  • String is the path to a file
  • byte[] is the data in the saved file

import java.io.*;
import java.util.HashMap;
import java.util.Map;

class FileSystem {
   private static final FileSystem fileSystem = new FileSystem();
   private static final Map<String, byte[]> files = new HashMap<>();

   private FileSystem() {
   }

   public static FileSystem getFileSystem() {
       return fileSystem;
   }

   public void create(String path) {
       validateNotExists(path);
       files.put(path, new byte[0]);
   }

   public void delete(String path) {
       validateExists(path);
       files.remove(path);
   }

   public boolean isExists(String path) {
       return files.containsKey(path);
   }

   public InputStream newInputStream(String path) {
       validateExists(path);
       return new ByteArrayInputStream(files.get(path));
   }

   public OutputStream newOutputStream(String path) {
       validateExists(path);
       return new ByteArrayOutputStream() {
           @Override
           public void flush() throws IOException {
               final byte[] bytes = toByteArray();
               files.put(path, bytes);
               super.flush();
           }

           @Override
           public void close() throws IOException {
               final byte[] bytes = toByteArray();
               files.put(path, bytes);
               super.close();
           }
       };
   }

   private void validateExists(String path) {
       if (!files.containsKey(path)) {
           throw new RuntimeException("File not found");
       }
   }

   private void validateNotExists(String path) {
       if (files.containsKey(path)) {
           throw new RuntimeException("File exists");
       }
   }
}
    

In this class, we created the following public methods:

  • standard CRUD methods (create, read, update, delete),
  • a method to check if a file exists,
  • a method to get an instance of the file system.

To read from a file, we return an InputStream. Under the hood is the ByteArrayInputStream implementation. The buffer is a byte array stored in the files map.

Another interesting method is newOutputStream. When this method is called, we return a new ByteArrayOutputStream object that overrides two methods: flush and close. Calling either of these methods should cause the write to take place.

And that is exactly what we do: we get the byte array that the user has written to, and store a copy as the value in the files map with an appropriate key.

We use the following code to test our file system (FS):


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import static java.nio.charset.StandardCharsets.UTF_8;

public class MyFileSystemTest {
   public static void main(String[] args) throws IOException {
       FileSystem fileSystem = FileSystem.getFileSystem();
       final String path = "/user/bin/data.txt";

       // Create a file
       fileSystem.create(path);
       System.out.println("File created successfully");

       // Make sure it's empty
       try (InputStream inputStream = fileSystem.newInputStream(path)) {
           System.out.print("File contents:\t");
           System.out.println(read(inputStream));
       }

       // Write data to it
       try (final OutputStream outputStream = fileSystem.newOutputStream(path)) {
           outputStream.write("CodeGym".getBytes(UTF_8));
           System.out.println("Data written to file");
       }

       // Read data
       try (InputStream inputStream = fileSystem.newInputStream(path)) {
           System.out.print("File contents:\t");
           System.out.println(read(inputStream));
       }

       // Delete the file
       fileSystem.delete(path);

       // Verify that the file does not exist in the FS
       System.out.print("File exists:\t");
       System.out.println(fileSystem.isExists(path));

   }

   private static String read(InputStream inputStream) throws IOException {
       return new String(inputStream.readAllBytes(), UTF_8);
   }
}
    

During the test, we verify the following actions:

  1. We create a new file.
  2. We check that the created file is empty.
  3. We write some data to the file.
  4. We read back the data and verify that it matches what we wrote.
  5. We delete the file.
  6. We verify that the file has been deleted.

Running this code gives us this output:

File created successfully
File contents:
Data written to file
File contents: CodeGym
File exists: false

Why was this example necessary?

Put simply, data is always a set of bytes. If you need to read/write a lot of data from/to disk, your code will run slowly due to I/O problems. In this case, it makes sense to maintain a virtual file system in RAM, working with it in the same way you would with a traditional disk. And what could be simpler than InputStream and OutputStream?

Of course, this is an example for instruction, not production-ready code. It does NOT account for (the following list is not comprehensive):

  • multithreading
  • file size limits (the amount of available RAM for a running JVM)
  • verification of the path structure
  • verification of method arguments

Interesting insider information:
The CodeGym task verification server uses a somewhat similar approach. We spin up a virtual FS, determine which tests need to be run for task verification, run the tests, and read the results.

Conclusion and the big question

The big question after reading this lesson is "Why can't I just use byte[], since it's more convenient and doesn't impose restrictions?"

The advantage of ByteArrayInputStream is that it strong indicates that you're going to use read-only bytes (because the stream doesn't provide an interface to changing its content). That said, it is important to note that a programmer can still access the bytes directly.

But if sometimes you have a byte[], sometimes you have a file, sometimes you have a network connection, and so on, you'll need some kind of abstraction for "a stream of bytes, and I don't care where they come from". And that's what InputStream is. When the source happens to be a byte array, ByteArrayInputStream is a good InputStream to use.

This is helpful in many situations, but here are two specific examples:

  1. You're writing a library that receives bytes and processes them somehow (for example, suppose it's a library of image processing utilities). Users of your library can provide bytes from a file, from an in-memory byte[], or from some other source. So you provide an interface that accepts an InputStream, which means that if they have a byte[], they need to wrap it in a ByteArrayInputStream.

  2. You're writing code that reads a network connection. But to perform unit tests on this code, you don't want to open a connection — you just want to feed a few bytes to the code. So the code takes an InputStream and your test passes in a ByteArrayInputStream.

Comments (1)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
Parsa Level 62, Bangalore, India Expert
12 January 2024
"Conclusion and the big question" should have been at the beginning of the previous lesson. It's better to tell students why they're learning something instead of dragging them and telling them the use at the end.