Sun Java Solaris Communities My SDN Account Join SDN
 
Technical Articles and Tips

Piped Streams and Using Sets

 

Tech Tips Archive


WELCOME to the Java Developer Connection sm(JDC) Tech Tips, February 8, 2001. This issue covers:

This tip was developed using Java 2 SDK, Standard Edition, v 1.3.

This issue of the JDC Tech Tips is written by Glen McClusky.

Pixel

PIPED STREAMS

Piped streams are a mechanism in the Java I/O library to set up a stream of data between two threads. Pipes are input/output pairs. Data written on the output stream shows up on the input stream at the other end of the pipe. You can think of pipes as a buffer, with a connection at each end.

Before getting into the details of pipes, here is a simple example of how you can use them:

    import java.io.*;
    
    class MyThread extends Thread {
        private Writer out;
    
        public MyThread(Writer out) {
            this.out = out;
        }
    
        public void run() {
    
            // write into one end of pipe
    
            try {
                BufferedWriter bw = new
                  BufferedWriter(out);
                bw.write("testing");
                bw.newLine();
                bw.close();
            }
            catch (IOException e) {
                System.err.println(e);
            }
        }
    }
    
    public class PipeDemo1 {
        public static void main(String args[])
          throws IOException {
    
            // create a pipe
    
            PipedWriter writer = new PipedWriter();
            PipedReader reader = new
              PipedReader(writer);
    
            // start thread going and write into pipe

            new MyThread(writer).start();
    
            // read from other end of pipe
    
            BufferedReader br = new
              BufferedReader(reader);
            String str = br.readLine();
            br.close();
    
            System.out.println(str);
        }
    }


PipeDemo1 creates a pipe, consisting of a PipedWriter object, and a PipedReader on the other end of the pipe. The PipedReader reads data written by the PipedWriter.

PipeDemo1 then creates and starts an instance of MyThread. The MyThread thread writes into one end of the pipe, then the main thread reads the data that was written, that is:

    testing

When piped streams are used to communicate between threads, the action of the threads is coordinated. For example, if a thread tries to read from a pipe, and no input is available, the thread blocks, that is, it waits until input is available, preventing other activity on the thread. The thread also blocks if it writes into a pipe, and the pipe buffer fills up.

If both the pipe's reader and writer are the same thread, and the pipe fills up, the thread blocks permanently. In other words, if a thread writes into a pipe, and the pipe's buffer is full, the thread blocks until the reader on the other end can take some of the data from the buffer. But if the reader and writer are the same thread, the thread is blocked, and so the reader cannot do its job. Here's an example of a program that hangs because of a blocked thread:

    import java.io.*;
    
    public class PipeDemo2 {
        public static void main(String args[])
          throws IOException {
    
            // create a pipe
    
            PipedWriter writer = new PipedWriter();
            PipedReader reader = new
              PipedReader(writer);
    
            // write into one end of pipe, writing
            // more than the internal buffer size
    
            BufferedWriter bw = new
              BufferedWriter(writer);
            for (int i = 1; i <= 200; i++) {
                bw.write("testing");
                bw.newline();
            }
            bw.close();
    
            // read from the pipe, from within the
            // same thread as the writer above
    
            bufferedreader br = new
              bufferedreader(reader);
            string str = br.readline();
            br.close();
    
            system.out.println(str);
        }
    }


The internal buffer size is 1024, but you can't rely on this number in writing your programs. PipeDemo2 overflows this buffer, and because of this, the writer thread blocks. The program hangs because the writer and reader threads are the same, and there is no way for the reader to reduce the size of the buffer contents.

How can you use piped streams in practice? One example is an application that has a producer thread that generates data, and another thread that consumes the data. You could use a piped stream to communicate between the threads. Here's what this looks like:

    import java.util.*;
    import java.io.*;
    
    class ProdThread extends Thread {
        private static final int N = 100;
        private static final int MAXLEN = 63;
    
        private Writer out;
    
        public ProdThread(Writer out) {
            this.out = out;
        }
    
        public void run() {
            Random rn = new Random(0);
            char num[] = new char[MAXLEN];
    
            try {
    
                // write N lines of binary numbers,
                // each binary number
                // 1 - MAXLEN digits in length
    
                BufferedWriter bw = new
                  BufferedWriter(out);
                for (int i = 1; i <= n; i++) {
                    int len = rn.nextint(maxlen) + 1;
                    for (int j = 0; j < len; j++) {
                        num[j] =
                      (rn.nextboolean() ? '1' : '0');
                    }
                    bw.write(num, 0, len);
                    bw.newline();
                }
                bw.close();
            }
            catch (ioexception e) {
                system.err.println(e);
            }
        }
    }
    
    class consthread extends thread {
    
        private reader in;
    
        public consthread(reader r) {
            in = r;
        }
    
        public void run() {
    
            // read the generated test data that
            // was written into the pipe above
    
            try {
                bufferedreader br = new
                  bufferedreader(in);
                string str;
                while ((str = br.readline()) != null)
                  {
                    system.out.println(str);
                }
                br.close();
            }
            catch (ioexception e) {
                system.err.println(e);
            }
        }
    }
    
    public class pipedemo3 {
        public static void main(string args[])
          throws ioexception {
    
            // create a pipe
    
            pipedwriter writer = new pipedwriter();
            pipedreader reader = new
              pipedreader(writer);
    
            // create and start the generator
            // and consumer threads
    
            thread prodthread = new
              prodthread(writer);
            thread consthread = new
              consthread(reader);
            prodthread.start();
            consthread.start();
        }
    }


In this demonstration, the producer thread, ProdThread, generates random binary numbers to be used as test data for the consumer thread, ConsThread. The numbers are written into a pipe. Then ConsThread reads the values from the other end of the pipe. The first few of lines of output look like this:

    1011010110001111010000001001011001
    001111011111101011110100101110000001010
    0111111

There are other ways to produce the same results. For example, you could have the producer thread fill up a collection object such as an ArrayList, and pass it to the consumer. However, if the data is very large, this could chew up a lot of memory. Also this approach doesn't allow the threads to run simultaneously, something that might be important if you have multiple CPUs available. You could use temporary files, but this technique has many of the same problems.

Or you could use a limited size data structure, and coordinate the action of the threads so that when one thread fills the structure, it blocks. Then the other thread empties the structure. But if you're going to do it this way, you might as well use piped streams, and let them take care of the details for you.

For more information, see Section 15.4.4, Piped Streams, in "The Java Programming Language Third Edition" by Arnold, Gosling, and Holmes (http://java.sun.com/docs/books/javaprog/thirdedition/).

USING SETS

Set and SortedSet are Java collection interfaces. You use them to specify a collection that has no duplicate elements. These interfaces are implemented using the HashSet and TreeSet classes. Here's a simple example:

    import java.util.*;
    
    public class SetDemo1 {
        public static void main(String args[]) {
            boolean b;
    
            // create a set

            Set s = new HashSet();
    
            // add strings the first time
    
            b = s.add("string1");
            System.out.println("string1
              add returns " + b);
     
            b = s.add("string2");
            System.out.println("string2
              add returns " + b);
     
            b = s.add("string3");
            System.out.println("string3
              add returns " + b);
     
            // try to add some duplicate strings
    
            b = s.add("string1");
            System.out.println("string1
              add returns " + b);
     
            b = s.add("string2");
            System.out.println("string2
              add returns " + b);
     
            // dump out set contents
    
            Iterator i = s.iterator();
            while (i.hasNext()) {
                System.out.println(i.next());
            }
        }
    }

This example creates a set and then adds some strings to it. The add method returns true. So the output for the initial adds is:

    string1 add returns true
    string2 add returns true

Sets do not allow duplicate elements, so when an attempt is made to add duplicates, the add method returns false. The output for these attempts is:

    string1 add returns false
    string2 add returns false

After the elements are added, the contents of the set are displayed using an iterator. The output is:

    string1
    string3
    string2

This output illustrates an important fact about sets: by default, the elements retrieved by the iterator are not in sorted order. If you want to change that, you can use a SortedSet:

    import java.util.*;
    
    public class SetDemo2 {
        public static void main(String args[]) {
    
            // create a SortedSet implemented as a tree structure
    
            SortedSet s = new TreeSet();
    
            s.add("string1");
            s.add("string2");
            s.add("string3");
            s.add("string1");
            s.add("string2");
    
            Iterator i = s.iterator();
            while (i.hasNext()) {
                System.out.println(i.next());
            }
        }
    }


Using the SortedSet in SetDemo2 returns the elements in natural order:

    string1
    string2
    string3

You can also specify a Comparator object to order the set's elements.

How do sets differ from lists and maps? Sets are different from lists in that they have no duplicate elements. Unlike lists, there is no way to manipulate set elements based on their position in the set. For example, you can't retrieve the 17th element of a set by random access, or insert an element before the 59th element of the set. Sets are are different from maps in that a map contains pairs of elements (that is, key/value pairs), with each key mapped to a value.

An important concept relating to sets is defining what constitutes a duplicate element. Consider this example:

    import java.util.*;
    
    public class SetDemo3 {
        public static void main(String args[]) {
    
            // create a set and add two objects
            // to it; the objects are distinct
            // but have the same displayable string
    
            Set s = new HashSet();
            s.add("37");
            s.add(new Integer(37));
    
            Iterator i = s.iterator();
            while (i.hasNext()) {
                System.out.println(i.next());
            }
        }
    }

In this example, the two elements have the same printable string (37), but are distinct when compared using the equals method.

This example leads to an important question. If you use a SortedSet, where the elements must be ordered, what happens if you add elements that have nothing in common with each other, that is, the elements have object types that cannot be compared? Here's an example that tries to do that:

    import java.util.*;
    
    public class SetDemo4 {
        public static void main(String args[]) {
    
            // create a SortedSet
    
            SortedSet s = new TreeSet();
    
            // add two objects to the set;
            // the objects are not comparable
    
            s.add("37");
            s.add(new Integer(37));
    
            Iterator i = s.iterator();
            while (i.hasNext()) {
                System.out.println(i.next());
            }
        }
    }

SetDemo4 adds a string "37" and an Integer object "37" to a set. When the program runs, the result is a ClassCastException. The exception is thrown because an attempt is made to order the elements of the set. A String object has no relationship to an Integer object, so the relative order of the two objects cannot be determined.

Because of the ordering implied by SortedSets, there are some additional operations you can do with this type of set. Here's a program that illustrates these operations:

    import java.util.*;
    
    public class SetDemo5 {
        public static void main(String args[]) {
    
            // create a SortedSet
    
            SortedSet s = new TreeSet();
    
            // add some elements to it

            s.add("string1");
            s.add("STRING2");
            s.add("STRING3");
            s.add("string4");
            s.add("STRING5");
    
            // get the first/lowest object in the set
    
            System.out.println("first
              = " + s.first());
    
            // get a subset of the set and display it
    
            Set sub = s.subSet("A", "ZZZZZZ");
            Iterator i = sub.iterator();
            while (i.hasNext()) {
                System.out.println(i.next());
            }
        }
    }

This example creates a set, adds some elements to it, and retrieves the first element of the set. Then it retrieves and displays a subset. The subset consists of elements greater than or equal to a minimum element, and less than a maximum element. Here is the output:

    first = STRING2
    STRING2
    STRING3
    STRING5

A subset is an example of a "backing view," that is, it's not a copy of elements in the original set, but an actual view of the set that filters out some elements. This means that if the original set changes, so will the subset. Here's a program that illustrates this point:

    import java.util.*;

    public class SetDemo6 {
        public static void main(String args[]) {
    
            // create a sorted set
    
            SortedSet s = new TreeSet();
    
            s.add("string1");
            s.add("STRING2");
            s.add("STRING3");
            s.add("string4");
            s.add("STRING5");
    
            // get a subset of the set and display it
    
            Set sub = s.subSet("A", "ZZZZZZ");
            Iterator i = sub.iterator();
            while (i.hasNext()) {
                System.out.println(i.next());
            }
            System.out.println();
    
            // remove an element from the original set
    
            s.remove("STRING3");
    
            // display the subset again
    
            i = sub.iterator();
            while (i.hasNext()) {
                System.out.println(i.next());
            }
        }
    }

Here is the displayed output of the SetDemo6 program:

    STRING2
    STRING3
    STRING5

    STRING2
    STRING5

Notice that the second time the subset is displayed, the "STRING3" element is gone. That's because it was removed from the original set that backs the subset.

A final point about sets is illustrated by the following example of bad programming style:

    import java.util.*;
    
    class MyObject {
        public String s;
        public String toString() {
            return s;
        }
        public int hashCode() {
            return s.hashCode();
        }
        public boolean equals(Object o) {
            if (!(o instanceof MyObject)) {
                return false;
            }
            MyObject mo = (MyObject)o;
            boolean b = s.equals(mo.s);
            return b;
        }
    }
    
    public class SetDemo7 {
        public static void main(String args[]) {
            Set s = new HashSet();
    
            // create two MyObjects and
            // add them to a set
    
            MyObject obj1 = new MyObject();
            MyObject obj2 = new MyObject();
            obj1.s = "string1";
            obj2.s = "string2";
            s.add(obj1);
            s.add(obj2);
    
            // change one of the object's contents
    
            obj2.s = "string1";
    
            // remove both objects from the set
    
            s.remove(obj1);
            s.remove(obj2);
    
            // dump out the contents of the set
    
            Iterator i = s.iterator();
            while (i.hasNext()) {
                System.out.println(i.next());
            }
        }
    }

A set cannot have duplicate elements. This example shows what can happen if you violate this rule by changing an element's contents after the fact. The example adds two MyObject objects to a set. The objects are distinct from each other. Then one of the objects is changed so that it is no longer distinct. Then both objects are removed from the set, and the contents of the set are displayed. The set should be empty, but it's not, as you can see from the displayed result:

    string1

To understand why the set is not empty, consider some internal details of set implementation. A Set is implemented using a HashMap, with the set element as the key, and a dummy object as the value to be inserted in the map.

If you insert two elements into a map, they are inserted at random points in the map, based on their hash codes. If you change the key for one of the elements, without updating the map, the element in the map is "stranded." This means that the element is probably inaccessible except through a sequential scan of the map. That's why you see the anomalous behavior illustrated by the SetDemo7 program.

For more information about sets, see Section 16.5, Set and SortedSet, in "The Java Programming Language Third Edition" by Arnold, Gosling, and Holmes (http://java.sun.com/docs/books/javaprog/thirdedition/)

— Note —

Sun respects your online time and privacy. The Java Developer Connection mailing lists are used for internal Sun MicrosystemsTM purposes only. You have received this email because you elected to subscribe. To unsubscribe, go to the Subscriptions page (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), uncheck the appropriate checkbox, and click the Update button.

— Subscribe —

To subscribe to a JDC newsletter mailing list, go to the Subscriptions page (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), choose the newsletters you want to subscribe to, and click Update.

— Feedback —

Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster@sun.com

— Archives —

You'll find the JDC Tech Tips archives at:

http://java.sun.com/jdc/TechTips/index.html

— Copyright —

Copyright 2001 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.

This document is protected by copyright. For more information, see:

http://java.sun.com/jdc/copyright.html

JDC Tech Tips February 08, 2001