Java Streams Overview, Part II

In my previous article, I wrote about the fundamentals of streams in Java 8. Now, let’s augment our skills with some additional information about streams, like how we can chain them, and we can use them to access files.

Chaining Streams

When working with streams, they are often chained together.

Let us see what are the advantages of using chained streams:

  • One stream instance leverages another stream instance.
  • This creates a higher level of functionality. We can have one stream accessing the data, then we have another stream that takes the results of that and processes more complex functionality.
  • This simplifies reusability because you can organize your streams in a way that allows each of them performs a specific job. In this way, they do not need to know each other’s inner workings.

We perform chaining using a constructor. We construct a higher level instance of the stream and then pass an instance of a lower level stream.

A good example of a chained stream is the InputStreamReader class, which is what we talked about in my previous article.

This class leverages chaining by providing reader behavior over an  InputStream. It translates the binary response to character behavior.

Let us see how it works.

void doChain(InputStream in) throws IOException{
int length;
char[] buffer = new char[128];
try(InputStreamReader rdr = new InputStreamReader(in)) {
while((length = rdr.read(buffer)) >= 0) {
//do something
}
}
}

As you can see, we do not need to care about how the  InputStream works. Whether it is backed by a file or network, it does not matter.

The only thing we know that it gives us binary data, we will pass it to our InputStreamReader and it converts it and can work with it as a character data.

Notice that we use try-with-resources here as well. If we close the InputStreamReader, it automatically closes theInputStream as well. This a very powerful concept, that you should know about.

File and Buffered Streams

We often use streams for accessing files.

There are several classes for that in the java.io package to use, like:

  • FileReader
  • FileWriter 
  • FileInputStream 
  • FileOutputStream 

The real thing is that these file streams are deprecated now. Despite that, they are still widely used in codes. So, you probably will face them in the near future, so it’s worth a notation.

Let us look at new ways to interact with files.

Buffered Streams

Buffered Streams are introduced to replace the FileStream classes in the java.io package. These new Streams are placed under the java.nio package.

It was necessary because direct file access can be inefficient and buffered streams can significantly improve efficiency with the following:

  • Buffer content in memory
  • Perform reads/writes in large chunks
  • Reduce underlying stream interaction

Buffering available for all four stream types:

  • BufferReader 
  • BufferWriter 
  • BufferedInputStream 
  • BufferedOutputStream 

Using them is very straightforward.

try(BufferedReader br = new BufferedReader(new FileReader("file.txt"))){
int value;
while((value = br.read()) >= 0) {
char charValue = (char)value;
//do something
}
}

Additional benefits to using BufferedStreams includes:

  • It handles linebreaks for various platforms like Windows or Unix
  • Uses correct value for the current platform
  • The BufferedWriter has a method:newLine(). It will create a new line with the appropriate character.
  • TheBufferedReader has a method for line based read: readLine().

Let us see how they work.

BufferedWriter:

void writeData(String[] data) throws IOException {
try(BufferedWriter bw = new BufferedWriter(new FileWriter("file.txt"))){
int value;
for(String str : data) {
bw.write(str);
bw.newLine();
}
}

BufferedReader:

void readData(String[] data) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("file.txt"))){
String inValue;
while((inValue = br.readLine()) != null) {
System.out.println(inValue);
}
}

The code above will write out the file’s content line by line.

Accessing Files With the java.nio.file package

In Java 8, the java.io.FileXXX streams are deprecated. There is a new package to handle file streams called the java.nio.file package.

The package has several benefits over java.io:

  • Better exception reporting
  • Greater scalability, they work much better with large files
  • More file system feature support
  • Simplifies common tasks

Bellow, we will talk about the most fundamental Types In this new package.

Paths and Path Types

Path

  • Used to locate a file system item
  • It can be a file or directory

Paths

  • Used to get the Path objects through static Path factory methods
  • It translates a string-based hierarchical path or URI to Path.

Example: Path p = Paths.get(“\\documents\\foo.txt”)

Files Type

  • Static methods for interacting with files
  • Create, copy, delete, etc…
  • Open files streams
    • newBufferedReader 
    • newBufferedWriter 
    • newInputStream 
    • newOutputStream 
  • Read/Write file contents
    • readAllLines
    • write 

Reading Lines With BufferedReader

Let us see some quick example of how you can use it.

void readData(String[] data) throws IOException {
try(BufferedReader br = Files.newBufferedReader(Paths.get("data.txt")){
String inValue;
while((inValue = br.readLine()) != null) {
System.out.println(inValue);
}
}
}

Read All lines

void readAllLines(String[] data) throws IOException {
List<String> lines = Files.readAllLines(Paths.get("data.txt"));
for(String line:lines) {
System.out.println();
}
}

File Systems

When we work with files from a Java program, those files are contained within a file system. Most commonly, we use the computer’s default file system.

Java also supports specialized file systems, such as the Zip file system.

Path instances are tied to a file system and thePath class works only for the default one. So, we need another solution. Fortunately, in the Java.nio package, we have the opportunity to deal with this.

File System Types

FileSystem

  • Represents an individual file system
  • Factory for Path instances

FileSystems

  • Used to get the FileSystem objects through static FileSystem factory methods
  • Open or create a file system
    •  newFileSystem

Accessing File Systems

File systems identified by URIs

  • Specifics of URI vary greatly among the file systems
  • Zip file system uses “jar:file” scheme
    • jar:file:/documents/data.zip

File systems support custom properties

  • Different for each file system type
  • Examples: String encoding, whether to create if it does not exist

Creating a Zip Filesystem

public static void main(String[] args) throws FileNotFoundException, IOException {
try (FileSystem zipFileSystem = openZip(Paths.get("data.zip"))){ //pass the Path where we would like to create our FileSystem
}catch (Exception e) {
System.out.println(e.getClass().getSimpleName() + " - " + e.getLocalizedMessage());;
}
}
private static FileSystem openZip(Path path) throws URISyntaxException, IOException {
Map<String, String> properties = new HashMap<>();
properties.put("create", "true"); //set the property to allow creating
URI zipUri = new URI("jar:file", path.toUri().getPath(), null); //make a new URI from the path
FileSystem zipFileSystem = FileSystems.newFileSystem(zipUri, properties); //create the filesystem
return zipFileSystem;
}

After the code above, you should see your data.zip file in your directory.

Copying Files to Zip Filesystem

Let us augment the above example with a File copy operation.

In this example, I created a file called file.txt in my project library. We will copy this file to our data.zip Filesystem.

Streams in Java 8
public static void main(String[] args) throws FileNotFoundException, IOException {
try (FileSystem zipFileSystem = openZip(Paths.get("data.zip"))){
copyFileToZip(zipFileSystem); //Here we call the file copy
}catch (Exception e) {
System.out.println(e.getClass().getSimpleName() + " - " + e.getLocalizedMessage());;
}
}
private static FileSystem openZip(Path path) throws URISyntaxException, IOException {
Map<String, String> properties = new HashMap<>();
properties.put("create", "true");
URI zipUri = new URI("jar:file", path.toUri().getPath(), null);
FileSystem zipFileSystem = FileSystems.newFileSystem(zipUri, properties);
return zipFileSystem;
}
static void copyFileToZip(FileSystem zipFileSystem) throws IOException{
Path sourceFile = FileSystems.getDefault().getPath("file.txt"); //Read the file to copy
Path destFile = zipFileSystem.getPath("/fileCopied.txt"); //get the path of the new file
Files.copy(sourceFile, destFile);//Copy the file to our zip FileSystem
}

After you run the code, you should see the fileCopied.txt int our zip-file. Its context should be the same as in our file.txt.

Summary

In this article, we went further into streams in Java 8. I demonstrated how stream chaining works, as well as how you can deal with files through the new java.nio package. We also touched on why you should use more up-to-date, buffered versions of the Filestreams.

Hope you enjoyed!

Java Streams Overview

For a long time, I had a gap in my knowledge of Java streams. On a basic level, I could use them, but I did not a deep understanding of them. So, I decided to do a quick overview of Java streams.

In this article, I will deal with many everything from the fundamentals to chaining and clean-up.

Hope this helps you broaden your knowledge of Java streams.

What Are Streams?

A stream is an ordered sequence of data that…

  • Provides a common I/O model
  • Abstracts details from an underlying source or destination

Whether you use streams to take data from memory, storage, or your network, it will hide the implementation details from you. The details are abstracted away, so in every situation, you can look at it as an ordered sequence of data.

storages
  • Stream types are uni-directional

This means that if you create an instance of a Java stream, you decide whether you would like to write to it or read from it. You can’t do both at the given time on a single stream.

Read / Write

We can divide the streams into two categories:

  • Byte streams – Interacts as binary data
  • Text streams – Interacts as unicode characters

The general interaction is the same for both Java stream types

Reading With Streams

As we mentioned, each stream is used either to read from or write to.

Firstly, let us see how we can read data from Java streams.

Java streams

The base class to read binary data in Java is the InputStreamAnd the base class to read text data is called the Readerclass.

Both classes almost have the same two methods:

  •  int read()
  •  int read(byte/char[] buff)

Notice that in both scenarios, they give back an Integer value. These are interpreted values. An integer is a 32-bit container, so it will work in both cases.

The difference between the two:

  • The InputStream works with bytes, which are 8-bits.
  • The Reader works with unicode characters, which are 16-bits.

Read Bytes With InputStream

InputStream input = // create input stream
int result;
while(result = input.read() >= 0) //Indicates the end-of-stream with a return value of -1
byte byteVal = (byte)result;
// do something with byteVal
};

Read Text with Reader

Reader reader = // create reader
int result;
while(result = reader.read() >= 0)//Indicates the end-of-stream with a return value of -1
char charVal = (char)result;
// do something with charVal
};

Note that if you would like to retrieve the value, you simply need to cast the result to the appropriate type — in this case, byte or char.

Writing With Streams

To write data, there are two base classes similar to the read streams.

  •  OutputStream (for bytes)
  •  Writer (for text)
Java Text streams
To write with Java streams is more straightforward than reading them. Both classes have a few   write  methods with the  void return type.

Writing Bytes With OutputStream

To write with OutputStream, you can pass a single byte the write method, or you can pass a byte array as well.

OutputStream output // create output stream;
Byte byteVal = 100;
output.write(byteVal);
byte[] byteBuff = {0, 10, 20};
output.write(byteBuff);

Writing Characters With Writer

To write with the Writer class, you can pass a simple character, character array, or String to its  write method.

Writer writer // create output stream;
char charVal = 'c';
writer.write(charVal);
char[] charArray = {'c', 'h', 'a', 'r'};
writer.write(charArray);
String stringVal = "String";
writer.write(stringVal);

As you can see, you need much less code to write than to read.

Common Java Stream Classes

Above, I wrote about the base stream classes. Now, let us go a bit deeper and talk about the different implementations.

Common Input/OutputStream Derived Classes

Java streams
  •  ByteArrayInputStream /  ByteArrayOutputStream – Enables us to create a stream over a byte array
  •  PipedInputStream PipedOutputStream – This is much like a producer-consumer concept. A piped output stream can be connected to a piped input stream to create a communications pipe. The piped output stream is the sending end of the pipe. Typically, data is written to a PipedOutputStream object by one thread and data is read from the connected PipedInputStream by some other thread.
  •  FileInputStream /  FileOutputStream – These allow us to create streams over files.

Common Reader/Writer Derived Classes

Java Reader/ Writer streams
Above are examples of Reader/Writer stream implementations.
  •  CharArrayReader / Writer – Allows creating streams over characters
  •  StringReader / Writer – Allows creating stream over Strings
  •  PipedReader / Writer – Allows creating a stream in a Producer/Consumer relationship over text. Similarly to the PipedOutput InputStream 
  •  InputStreamReader / Writer – Allows us to create a stream over an Input OutputStream 
  •  FileReader / Writer – These are delivered from the least mentioned above. It allows us to make a stream over text files.

Stream Errors and Clean-Up

So far, we looked at the general features of streams, but we have not considered all the realities of working with them.

Stream Realities

Java streams

Let us see the two main groups here.

Clean-Up

Problems

  • Streams are backed by physical storage, which often exists outside the Java runtime, like files or network connections.
  • Hence, Java may not reliably clean up, so we need to do our own reliable clean-up. We need to close the Streams when we are done with them.

Solutions

  • Streams implement the Closeable interface, which implements one single close method. So, this is our responsibility to call it.

Let us see a simple solution:

Reader reader;
try{
reader = // create output stream;
// do something with reader;
}catch (IOException e) {
//handle exception
}finally {
if(reader != null)
reader.close();
}

The problem with the above example is that you always need to implement it. Usually, we use Streams frequently, so it should be done automatically. Let us see how we can achieve it.

Automating Clean-Up

  •  AutoClosable interface
    • One method: close
    • The base interface of the Closable interface, so every Stream supports it.
    • Provides support for try-with-resources

Try-With-Resources

  • Automates the clean-up of one or more resources
    • A “resource” is any type that implements  AutoClosable
  • Syntax similar to traditional try statement
  • Optionally includes catch block(s)
    • Handle try body
    • Handle close method call

Working With Try-With-Resources

Here, I provided a simple example of how we can use the automatic close of streams with try-with-resources block.

I will use it through a FileInputStream. We will talk about this specific stream later.

 try(FileInputStream input = new FileInputStream("file1.txt")) {
        int data = input.read();
        while(data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    }

With the above approach, you do not need to investigate further work to close your streams.

Summary

In this article, we talked about the fundamentals of Java streams, what are they, how they work, and how you can use them. In my next article, I will dig a bit deeper, and I will write about more advanced Stream topics like chaining, buffered streams, and how to use file systems with it. So stay tuned!

See more about streams in the official Java documentation here.