Streams

Using streams for I/O is a familiar model for Java programmers. The .NET Framework embraces the same approach, with a number of subtle differences. The most important change is that a stream can be used for both reading and writing data, in contrast with Java, which separates these operations by using input and output streams (and readers and writers since Java version 1.1).

The .NET stream classes are fewer in number than the Java equivalents, partly because of the bidirectional support and partly because .NET doesn’t offer the same range of specialized streams found in the java.io package. Java version 1.1 introduced the reader and writer classes, and this has further increased the number of choices for the programmer.

The Foundation of Streams

The abstract System.IO.Stream class is the basis for streaming in .NET. Similar to the java.io.InputStream and java.io.OutputStream classes, System.IO.Stream reads and writes bytes. The class also defines the methods required for asynchronous I/O.

Table 10-8 summarizes the methods of the Stream class. Bear in mind that stream implementations aren’t required to support all methods and might throw a NotSupportedException. Some of the methods and properties listed have particular reference to accessing streamed data randomly. Although the Java base classes don’t support these features, random access to files is available through the java.io.RandomAccessFile class.

Table 10-8. Comparison of .NET and Java Stream Classes

Java Streams

System.IO.Stream

Comments

InputStream.close()

OutputStream.close()

Close()

 

OutputStream.flush()

Flush()

 

InputStream.read()

Read()

Reads a sequence of bytes.

InputStream.read()

ReadByte()

Reads a single byte.

OutputStream.write()

Write()

Writes a sequence of bytes to the stream.

OutputStream.write()

WriteByte()

Writes a single byte.

N/A

CanRead

Returns true if a stream can be used for reading.

N/A

CanSeek

Returns true if a stream supports seeking to a specific location.

N/A

CanWrite

Returns true if a stream can be used for writing.

N/A

Length

Gets the number of bytes in the stream.

N/A

Position

Gets the current seek position within the stream.

N/A

BeginRead()

EndRead()

BeginWrite()

EndWrite()

Provide support for asynchronous I/O, detailed later in this chapter in Asynchronous I/O.

N/A

Seek()

Indirectly available in Java through the mark, reset, and skip methods in InputStream.

The Stream class also defines the static property Null, which is an analog of the /dev/null UNIX device. The result of getting this property is a stream in which calls to a read method will return without any data, and data written to this stream will be quietly discarded.

Base Streams

Streams representing a backing store are known as base streams. Examples of backing stores include a disk file, memory, and a network connection. Base stream classes have constructors that are used to configure the relationship between the backing store and the data stream; for example, the FileStream classes detailed earlier in this chapter accept constructor arguments that specify which file should be opened and which file modes will be used.

Three base stream classes are included in the .NET Framework, providing support for backing stores based on files, network connections, and bytes held in memory. These classes, and their Java counterparts, are described in Table 10-9.

Table 10-9. Comparison of the Base Stream Classes Provided by Java and .NET

Java

.NET

Backing Store

FileInputStream

System.IO.FileStream

Disk file

FileOutputStream

  

ByteArrayInputStream

System.IO.MemoryStream

Array of bytes held in memory

ByteArrayOutputStream

  

N/A

System.Net.Sockets.NetworkStream

Network connection

The Java java.net.Socket class exposes streams backed by network connections using the basic InputStream and OutputStream classes. In .NET, instances of network streams can be created using a System.Net.Sockets.Socket as a constructor argument but are more typically obtained indirectly through the Socket instance.

More Info

See Chapter 14, for more information about using sockets.

Creating instances of the FileStream class is discussed in the preceding section of this chapter, leaving only the MemoryStream class. The following example demonstrates a simple use of this class:

MemoryStream x_write_stream = new MemoryStream();
x_write_stream.WriteByte((byte)'.'),
x_write_stream.WriteByte((byte)'N'),
x_write_stream.WriteByte((byte)'E'),
x_write_stream.WriteByte((byte)'T'),
Console.WriteLine(Encoding.Default.GetString(x_write_stream.ToArray()));

The example creates a MemoryStream instance with the default constructor, which creates a stream with no initial stream data and no initial capacity. The overloaded constructor MemoryStream(int) creates a new instance with the specified capacity. The capacity refers to the size of the array in which byte information will be stored. When data is written to streams created with these constructors, the underlying array will be resized as needed to accommodate new bytes. The example writes 4 bytes into the stream and then uses the ToArray method to extract the data from the class and writes the contents to the console, using the Encoding class, which converts byte arrays to strings.

The next example demonstrates how to create a stream with initial data:

MemoryStream x_read_stream
    = new MemoryStream(Encoding.Default.GetBytes("Java"));

int x_byte;
while ((x_byte = x_read_stream.ReadByte()) != -1) {
    Console.Write((char)x_byte);
}

The stream is created with an array of bytes representing the word Java. Then each byte is read from the stream and written to the console. Instances of MemoryStream created with initial data do not resize the underlying array, and attempting to append more data will result in an exception. However, it’s possible to create a fixed-size stream that allows the array contents to be changed (where the CanWrite property returns true) by using different constructor forms. The following example demonstrates how this can be done:

byte[] x_initial_content = Encoding.Default.GetBytes("Java");
MemoryStream x_stream = new MemoryStream(x_initial_content, true);

byte[] x_new_content = Encoding.Default.GetBytes(".NET");
foreach (byte x_byte in x_new_content) {
    x_stream.WriteByte(x_byte);
}
        Console.WriteLine(Encoding.Default.GetString(x_stream.ToArray()));

The MemoryStream is created using the byte array representing the word Java. The second argument in the constructor indicates that the stream should allow write operations. A series of write operations alters the content of the stream, replacing Java with .NET. Finally the contents of the stream are written to the console.

Pass-Through Streams

Stream implementations that are backed by another stream are known as passthrough streams and take instances of System.IO.Stream as constructor arguments. Pass-though streams can transform data that is read or written or provide some additional functionality to the programmer. Several pass-though streams can be chained to combine specialized functionality for a base stream.

The java.io package contains several examples of pass-through streams, including LineNumberInputStream (which keeps track of line numbers) and PushbackInputStream (which provides the ability to push back or un-read a single byte of data). Java pass-through streams are derived from either FilterInputStream or FilterOutputStream. By contrast, the .NET System.IO namespace contains only one pass-though stream, BufferedStream, which buffers read and write operations.

The BufferedStream class is equivalent to the java.io.BufferedInputStream and java.io.BufferedOutputStream classes (because .NET streams can be used for reading and writing). The default constructor accepts an instance of System.IO.Stream to buffer against and will create a default buffer of 4096 bytes; a different buffer size can be specified as an integer argument.

Tip

The System.IO.FileStream class is already buffered and doesn’t benefit from being used with BufferedStream. The System.Net.Sockets.NetworkStream class isn’t buffered.

Readers and Writers

The .NET reader and writer classes are functionally similar to those in Java. The Java classes were introduced to provide character-based streams to better support internationalization, and implementations include both base and pass-through streams. The .NET reader and writer classes are also character oriented but can’t be used as pass-through streams. No base classes exist for the .NET readers and writers; each implementation stands alone, meaning that the programmer can’t cast to a single abstract type. The classes described in this section most closely resemble the Java I/O model, implementing a division of responsibility between reading and writing data.

BinaryReader and BinaryWriter

The BinaryReader and BinaryWriter classes read and write primitive data types and strings to a stream as binary data; however, they are not able to serialize object graphs. The following example demonstrates writing a series of integers, from 0 to 9, to a MemoryStream using a BinaryWriter and then using a BinaryReader to read the data back and write it to the console, one integer per line.

MemoryStream x_stream = new MemoryStream();

BinaryWriter x_writer = new BinaryWriter(x_stream);
for (int i = 0; i < 10; i++) {
    x_writer.Write(i);
}

x_stream.Seek(0, SeekOrigin.Begin);

BinaryReader x_reader = new BinaryReader(x_stream);
for (int i = 0; i < 10; i++) {
    int x_result = x_reader.ReadInt32();
    Console.WriteLine(x_result);
}

TextReader and TextWriter

The abstract TextReader and TextWriter classes are responsible for providing character-based I/O. The members provided by these classes are listed in Table 10-10.

Table 10-10. The TextReader and TextWriter Classes

Member

Description

TextReader.Null

Provides a TextReader with no data to read.

TextWriter.Null

Provides a TextWriter that acts as a data sink.

TextReader.Close()

Closes the reader or writer.

TextWriter.Close()

 

TextReader.Peek()

Returns the next character that will be returned with a Read method call.

TextReader.Read()

Reads either a single character or an array of characters.

TextReader.ReadBlock()

Performs a blocking read operation to obtain a specified number of characters.

TextReader.ReadLine()

Reads a line of text, returned as a string.

TextReader.ReadToEnd()

Reads all of the characters from the current position to the end of the reader, returned as a string.

TextReader.Synchronized

Creates a thread-safe wrapper around the reader or writer.

TextWriter.Synchronized

 

TextWriter.Encoding

Returns the Encoding that is used to write character data.

TextWriter.FormatProvider

Returns the object used to control formatting. See Chapter 7 for more information on formatting.

TextWriter.NewLine

Gets or sets the line separator string.

TextWriter.Flush()

Flushes any buffered data.

TextWriter.Write()

Writes a range of types, including primitives and strings. Although there is an overloaded form that accepts an object argument, the object isn’t serialized. The data written is the result of calling ToString on the argument value.

TextWriter.WriteLine()

Writes a range of types, including primitives and strings, followed by the line terminator string.

These classes provide the basis for character-oriented I/O, and the .NET Framework contains two sets of implementation classes, detailed in the following sections.

StreamReader and StreamWriter

The StreamReader and StreamWriter classes implement the functionality of the TextReader and TextWriter classes against streams, making character-based I/O available for all of the base stream types. As a convenience, overloaded constructors for these classes will accept file paths for input or output, allowing the programmer to use character I/O without having to explicitly create instances of FileStream. The following example demonstrates a simple use of these classes, following the model of the preceding example.

MemoryStream x_stream = new MemoryStream();

StreamWriter x_writer = new StreamWriter(x_stream);

x_writer.WriteLine("C# for Java Developers");
x_writer.Flush();

x_stream.Seek(0, SeekOrigin.Begin);

StreamReader x_reader = new StreamReader(x_stream);
Console.WriteLine(x_reader.ReadLine());

StringReader and StringWriter

The StringReader and StringWriter classes use strings held in memory as the backing store. The StringReader class takes a string as the constructor argument, and calls to Read return characters sequentially from the string. The StringWriter class uses an instance of System.Text.StringBuilder to store characters processed using the Write method. The contents of the string are available through the ToString method. These classes represent character-based implementation of the broad functionality provided by the System.IO.MemoryStream class.

Synchronizing Streams

The .NET stream classes do not provide synchronization support for instance members; the programmer must assume the responsibility for ensuring safe access to stream instances. Members that are public and static are guaranteed to be safe.

Streams Summary

Although the .NET stream classes are few in number, they manage to provide the majority of the features available in Java. Some of the specialized streams and readers are missing, but the ability of the same stream instance to both read and write data belies the sparse appearance of the class set. The implementation of the reader and writer classes seems out of keeping with the slick design of the stream classes, and the lack of a common base class leads to a lack of abstraction. The mixed model of defining classes that are both base and pass-through implementations is far from ideal.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset