The I/O libraries of programming languages often use the abstract concept of flow, which represents any object capable of producing a data source object or receiving data. “Flow” hides the details of how data is processed in the actual I/O device.

The direction of the flow

In Java1.0, the library designers first limited that all classes related to input should inherit from InputStream for reading a single byte or an array of bytes, and all classes related to output should inherit from OutputStream for writing a single byte or an array of bytes. Both read() and write() are abstract methods that specify how subclasses should be implemented.

InputStream:

    // Returns a byte to the caller from read() as an int, ignoring the high 24 bits
    // This method may block until data is available, EOF, IOE, -1 for EOF
    public abstract int read(a) throws IOException;
    
    // Return bytes from read() to array b[], up to len bytes from off
    // The return value represents the number of bytes actually read, and -1 represents EOF
    public int read(byte b[], int off, int len) throws IOException
Copy the code

OutputStream:

    // Write byte B to the byte stream, passed as int, ignoring the high 24 bits
    public abstract void write(int b) throws IOException;
    
    // Write the byte array b[] to the byte stream, at most len bytes starting with off
    public void write(byte b[], int off, int len) throws IOException 
Copy the code

Byte data flow chart:

graph TD
InputStream --> OuterCaller --> OutputStrem

So, where do InputStream’s bytes come from? Take ByteArrayInputStream as an example:

    // Byte data is passed in the constructor, called a BUF array
    public ByteArrayInputStream(byte buf[])
    public ByteArrayInputStream(byte buf[], int offset, int length)
    
    // Data is read from the buF array
    // ByteArrayInputStream maintains the read status of the BUF array internally and synchronizes it concurrently
    public synchronized int read(a) {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
    }
Copy the code

Similarly, FileInputStream specifies the File descriptor File at the constructor stage, and read() is then returned by the operating system from the File descriptor.

In another direction, ByteArrayOutputStream receives bytes from the outside and stores them internally:

    // Specifies the initial storage space for the internal BUF array
    public ByteArrayOutputStream(int size)
    
    // Each time you write data to the buF array, you need to ensure that its capacity is sufficient (of course overflow control is required).
    public synchronized void write(int b) {
        ensureCapacity(count + 1);
        buf[count] = (byte) b;
        count += 1;
    }
Copy the code

Similarly, FileOutputStream specifies the File descriptor File at the constructor stage, and subsequent write() will be performed by the operating system to write data to the File descriptor.

It can be seen from the above that the inherited class under Stream is different in the internal storage form of byte data, and the data flow direction is in accordance with the flow chart.

The adornment of the flow

We rarely use a single class to create a stream object, but instead stack multiple objects to provide the desired functionality, the decorator design pattern.

The JDK provides FilterInputStream as an adaptation base class for this purpose. For example, the Java Java Development Kit (JDK) provides FilterInputStream as an adaptation base class.


public class FilterInputStream extends InputStream {

    protected volatile InputStream in;
    
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }
    
    public int read(a) throws IOException {
        returnin.read(); }}Copy the code

The classic decorator class BufferedInputStream essentially caches the entire BUF array at once while reading to reduce calls to the proxy InputStream. If the data range of external requests exceeds the buffer size, the cache directly connected proxy is skipped.

public class BufferedInputStream extends FilterInputStream {
    
    public synchronized int read(a) throws IOException {
        if (pos >= count) {
            // As far as possible, the entire BUF array will be populated from the agent InputStream
            fill();
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff; }}Copy the code

At this point, there are FilterInputStream and FilterOutputStream inside the byte dataflow.

graph TD
ProxyInputStream --> FilterInputStream* --> OuterCaller --> FilterOutputStream* --> ProxyOutputStream

Reading from the call point of view is drill-up, writing is drill-down.

stateDiagram-v2
OuterCaller --> FilterInputStream*
FilterInputStream* --> ProxyInputStream
ProxyInputStream --> FilterInputStream*
FilterInputStream* --> OuterCaller

OuterCaller --> FilterOutputStream*
FilterOutputStream* --> ProxyOutputStream

As you’ve seen in previous articles, libraries can be enhanced by the multiple inheritance mechanism that interfaces provide. The JDK provides DataInputStream and DataOuputStream, which can convert bytes to other basic data types (int, char, long, and so on) for callers.

public class DataInputStream extends FilterInputStream implements DataInput {

    public final int readInt(a) throws IOException {
        As mentioned earlier, only the lower 8 bits are returned with real numeric significance
        int ch1 = in.read();
        int ch2 = in.read();
        int ch3 = in.read();
        int ch4 = in.read();
        if ((ch1 | ch2 | ch3 | ch4) < 0)
            throw new EOFException();
        return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0)); }}Copy the code

Reader and Writer

Java1.1 has made significant changes to the basic I/O stream library. InputStream and OutputStream can still provide valuable functionality in byte-oriented I/O, while Reader and Writer provide unicode-compatible and character-oriented I/O.

Sometimes we have to combine classes from the byte hierarchy with classes from the character hierarchy. To do this, we use adapter classes: InputStreamReader converts an InputStream to a Reader, while OutputStreamWriter converts an OutputStream to a Writer. PrintWriter provides the formatting mechanism.

public class BufferedInputFile {
    public static String read(String filename) throws IOException {
        // FileReader simply passes FileInputStream to its parent class InputStreamReader
        BufferedReader in = new BufferedReader(new FileReader(filename));
        String s;
        StringBuilder sb = new StringBuilder();
        // null represents EOF
        while((s = in.readLine()) ! =null) {
            sb.append(s + "\n");
        }
        in.close();
        returnsb.toString(); }}Copy the code

New I/O

The new Java I/O class library, introduced in the java.nio.* package of JDK1.4, aims to increase speed by using structures that more closely resemble the way operating systems perform I/O: channels and caches.

We don’t interact with the channel directly, we just interact with the cache and send the buffer to the channel, which either gets data from the buffer or sends data to the buffer. The only cache that interacts directly with the channel is the ByteBuffer.

Three classes in the old I/O library have been modified to produce FileChannel: FileInputStreeam, FileOutputStream, and RandomAccessFile. Another Java nio. Channels. Channels class provides a practical method for the Reader and Writer in the passage.

public class GetChannel {
    private static final int BSIZE = 1024;
    public static void main(String[] args) throws IOException {
        FileChannel fc = new FileOutputStream("data.txt").getChannel();
        fc.write(ByteBuffer.wrap("Some text".getBytes())); Fc. Close (); fc =new FileInputStream("data.txt").getChannel();
        ByteBuffer buff = new ByteBuffer.allocate(BSIZE);
        fc.read(buff);
        buff.flip();
        while (buff.hasReamining) {
            System.out.print((char)buff.get()); }}}Copy the code

ByteBuffer is based on a Buffer, which is essentially an array, with variable flags and switching methods that control reading and writing to the array.

The variable direction is 0 <= mark <= position <= limit <= capacity.

  • Capacity Maximum capacity of the array, which is regarded as the physical limit
  • Position Reads or writes a pointer that indicates the next readable or writable position
  • Limit Read/write limit position, which indicates the maximum position that read/write can reach
  • Mark is a backup bit of position that can be used to restore position

The control method

  • Mark () and reset() are used to back up and restore position
  • Clear () clears the entire buffer
  • Flip () indicates that the buffer is ready to read itself and can be read externally. Limit = position, position = 0
  • Rewind () indicates that buffer can be read or written from scratch. position = 0

As far as possible, ByteBuffer can only hold data of byte type, but it has methods to generate values of various primitive types from the bytes it holds, using the same byte array underneath. This use is called a view buffer.

public class GetClass {
    private static final int BSIZE = 1024;
    public static void main(String[] args) {
        ByteBuffer bb = ByteBuffer.allocate(BSIZE);
        bb.asCharBuffer().put("abcdef");
        print(Arrays.toString(bb.array(());
        bb.rewind();
        
        bb.order(ByteOrder.LITTLE_ENDIAN);
        
        bb.asCharBuffer().put("abcdef"); print(Arrays.toString(bb.array(()); }}Copy the code

When the storage capacity is greater than one byte, the order of the bytes becomes a concern. ByteBuffer stores data in BigEndian form, and data is often sent over the network in BigEndian form.

// Small endian: the logical high level is stored in the physical high level
static void putIntL(ByteBuffer bb, int bi, int x) {
    bb._put(bi + 3, int3(x));
    bb._put(bi + 2, int2(x));
    bb._put(bi + 1, int1(x));
    bb._put(bi    , int0(x));
}
// Big endian: the logical high is stored in the physical low, and the conceptual definition estimation is differentiated by increasing physical location
static void putIntB(ByteBuffer bb, int bi, int x) {
    bb._put(bi    , int3(x));
    bb._put(bi + 1, int2(x));
    bb._put(bi + 2, int1(x));
    bb._put(bi + 3, int0(x));
}
Copy the code

RandomAccessFile

RandomAccessFile works for files made up of records of known size, so we can use seek() to move records from one place to another and then read or modify them. It implements the DataInput and DataOutput interfaces, but does not use any of the functionality already in the InputStream and OutputStream classes. It is a completely separate class, derived directly from Object.

public class UsingRandomAccessFile {
    static String file = "rtest.dat";
    
    static void display(a) throws IOException {
        RandomAccessFile rf = new RandomAccessFile(file, "r");for(int i = 0; i < 7; i++) {
            System.out.println("Value " + i + ":" + rf.readDouble());
        }
        System.out.println(rf.readUTF());
        rf.close();
    }
    
    public static void main(String[] args) throws IOException {
        RandomAccessFile rf = new RandomAccessFile(file, "rw");
        for(int i = 0; i < 7; i++) {
            rf.writeDouble(i * 1.414);
        }
        rf.writeUTF("The end of the file");
        rf.close();
        
        display();
        
        rf = new RandomAccessFile(file, "rw");
        rf.seek(5 * 8);
        rf.writeDouble(47.00001); rf.close(); display(); }}Copy the code

Memory-mapped files allow you to create and modify files that are too big to fit into memory, start with RandomAccessFile, get channels on that file, and then call map() to produce MappedByteBuffer, a special type of direct buffer. You can map smaller portions of a larger file by specifying the initial location of the mapping file and the length of the mapping area.

public class LargeMappedFiles {
    static int length = 0x8FFFFFF;
    public static void main(String[] args) throw IOException {
        MappdedByteBuffer out = new RandomAccessFile("test.dat"."rw").getChannel().map(FileChannel.MapMode.READ_WRITE, 0, length);
        for(int i = 0; i< length; i++) {
            out.put((byte)'x');
        }
        print("Finished writing");
        for(int i = length /2; i< length/2 + 6; i++) {
            printlnb((char)out.get(i)); }}}Copy the code

Object serialization

The author has almost never used Serializable in programming practice. For network transmission serialization, PB, Thrift and custom protocols are mainly used, while data storage directly uses DB, so here is a brief introduction.

In restoring a Serializable object, no constructors are called, including the default constructor, and the entire object is restored by retrieving data from InputStream.

The TRANSIENT keyword ignores the field serialization.

The Externalizable interface inherits the Serializable interface and adds two methods: WriteExternal () and readExternal() are called automatically during serialization and deserialization to perform special operations. Notice that the default constructor is called.