MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java 输入输出流核心原理剖析

2021-09-073.0k 阅读

Java 输入输出流基础概念

在Java编程中,输入输出流(I/O Streams)是一个至关重要的概念,用于处理程序与外部设备(如文件、网络连接、控制台等)之间的数据传输。输入流用于从外部设备读取数据到程序中,而输出流则用于将程序中的数据写入到外部设备。

Java的I/O流体系非常庞大且灵活,它基于字节流和字符流两种基本类型构建。字节流用于处理8位字节数据,适用于处理二进制数据,如图片、音频、视频等;字符流则用于处理16位Unicode字符数据,主要用于处理文本数据。

字节流

字节流的基类是InputStreamOutputStreamInputStream提供了一系列从输入源读取字节数据的方法,而OutputStream则提供了将字节数据写入输出目标的方法。

下面是一个简单的示例,从控制台读取字节数据并输出到控制台:

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

public class ByteStreamExample {
    public static void main(String[] args) {
        InputStream inputStream = System.in;
        OutputStream outputStream = System.out;

        try {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

在这个示例中,System.in是一个InputStream实例,代表标准输入(通常是键盘),System.out是一个OutputStream实例,代表标准输出(通常是控制台)。我们通过read方法从inputStream读取数据到缓冲区buffer,然后使用write方法将缓冲区中的数据写入outputStream。注意,在使用完流后,需要关闭流以释放资源。

字符流

字符流的基类是ReaderWriter。与字节流不同,字符流处理的是字符数据,这使得处理文本更加方便。Reader提供了读取字符数据的方法,Writer则提供了写入字符数据的方法。

以下是一个从文件读取文本并输出到控制台的字符流示例:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;

public class CharacterStreamExample {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"));
             PrintWriter writer = new PrintWriter(System.out)) {
            String line;
            while ((line = reader.readLine()) != null) {
                writer.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,BufferedReader包装了FileReader,用于从文件example.txt中读取文本行。PrintWriter用于将读取到的文本行输出到控制台。try-with-resources语句确保流在使用完毕后自动关闭,简化了资源管理。

字节流的核心原理

InputStream 原理

InputStream是所有字节输入流的抽象基类,它定义了一系列读取字节数据的方法。其中,最核心的方法是read(),该方法有以下几种重载形式:

  1. int read():从输入流中读取一个字节的数据,并返回该字节数据(以0到255之间的整数表示)。如果到达流的末尾,则返回 -1。
  2. int read(byte[] b):从输入流中读取最多b.length个字节的数据,并将其存储到字节数组b中。返回实际读取的字节数,如果到达流的末尾,则返回 -1。
  3. int read(byte[] b, int off, int len):从输入流中读取最多len个字节的数据,并将其存储到字节数组b中,从偏移量off开始存储。返回实际读取的字节数,如果到达流的末尾,则返回 -1。

InputStream的具体实现类(如FileInputStreamByteArrayInputStream等)会根据不同的输入源来实现这些读取方法。例如,FileInputStream会从文件中读取字节数据,而ByteArrayInputStream则从字节数组中读取数据。

下面是一个自定义的ByteArrayInputStream示例,展示了read方法的实现原理:

import java.io.IOException;

public class MyByteArrayInputStream {
    private byte[] buf;
    private int pos;
    private int mark = 0;

    public MyByteArrayInputStream(byte[] buf) {
        this.buf = buf;
        this.pos = 0;
    }

    public int read() throws IOException {
        return (pos < buf.length)? (buf[pos++] & 0xff) : -1;
    }

    public int read(byte[] b, int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        }
        if (pos >= buf.length) {
            return -1;
        }
        int avail = buf.length - pos;
        if (len > avail) {
            len = avail;
        }
        if (len <= 0) {
            return 0;
        }
        System.arraycopy(buf, pos, b, off, len);
        pos += len;
        return len;
    }

    public void close() throws IOException {
        // 对于ByteArrayInputStream,关闭操作实际上不做任何事情,因为数据在内存中
    }
}

在这个自定义的MyByteArrayInputStream中,read方法根据当前位置pos从字节数组buf中读取数据,并更新posread(byte[] b, int off, int len)方法则会将数据从字节数组复制到传入的字节数组b中,从偏移量off开始,最多复制len个字节。

OutputStream 原理

OutputStream是所有字节输出流的抽象基类,它定义了一系列写入字节数据的方法。核心方法包括:

  1. void write(int b):将指定的字节数据(参数b的低8位)写入输出流。
  2. void write(byte[] b):将字节数组b中的所有字节数据写入输出流。
  3. void write(byte[] b, int off, int len):将字节数组b中从偏移量off开始的len个字节数据写入输出流。

InputStream类似,OutputStream的具体实现类(如FileOutputStreamByteArrayOutputStream等)会根据不同的输出目标来实现这些写入方法。例如,FileOutputStream会将字节数据写入文件,而ByteArrayOutputStream则将数据写入字节数组。

下面是一个自定义的ByteArrayOutputStream示例,展示了write方法的实现原理:

import java.io.IOException;

public class MyByteArrayOutputStream {
    private byte[] buf;
    private int count;

    public MyByteArrayOutputStream() {
        this(32);
    }

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

    public void write(int b) {
        int newcount = count + 1;
        if (newcount > buf.length) {
            buf = expandCapacity(newcount);
        }
        buf[count] = (byte) b;
        count = newcount;
    }

    public void write(byte[] b, int off, int len) {
        if ((off < 0) || (off > b.length) || (len < 0) ||
            ((off + len) > b.length) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }
        int newcount = count + len;
        if (newcount > buf.length) {
            buf = expandCapacity(newcount);
        }
        System.arraycopy(b, off, buf, count, len);
        count = newcount;
    }

    private byte[] expandCapacity(int minimumCapacity) {
        int newCapacity = (buf.length << 1) + 2;
        if (newCapacity - minimumCapacity < 0) {
            newCapacity = minimumCapacity;
        }
        if (newCapacity < 0) {
            if (minimumCapacity < 0) {
                throw new OutOfMemoryError();
            }
            newCapacity = Integer.MAX_VALUE;
        }
        byte[] newbuf = new byte[newCapacity];
        System.arraycopy(buf, 0, newbuf, 0, count);
        return newbuf;
    }

    public byte[] toByteArray() {
        byte[] newbuf = new byte[count];
        System.arraycopy(buf, 0, newbuf, 0, count);
        return newbuf;
    }
}

在这个自定义的MyByteArrayOutputStream中,write(int b)方法将单个字节写入字节数组buf,如果数组空间不足,则通过expandCapacity方法扩展数组。write(byte[] b, int off, int len)方法则将字节数组b中的部分数据写入buf,同样会处理数组空间不足的情况。toByteArray方法用于获取当前输出流中的所有字节数据。

字符流的核心原理

Reader 原理

Reader是所有字符输入流的抽象基类,它定义了读取字符数据的方法。主要方法包括:

  1. int read():从输入流中读取一个字符的数据,并返回该字符(以0到65535之间的整数表示)。如果到达流的末尾,则返回 -1。
  2. int read(char[] cbuf):从输入流中读取最多cbuf.length个字符的数据,并将其存储到字符数组cbuf中。返回实际读取的字符数,如果到达流的末尾,则返回 -1。
  3. int read(char[] cbuf, int off, int len):从输入流中读取最多len个字符的数据,并将其存储到字符数组cbuf中,从偏移量off开始存储。返回实际读取的字符数,如果到达流的末尾,则返回 -1。

Reader的具体实现类(如FileReaderCharArrayReader等)会根据不同的输入源来实现这些读取方法。例如,FileReader用于从文件中读取字符数据,它内部实际上是通过InputStreamReader将字节流转换为字符流来实现的。

下面是一个自定义的CharArrayReader示例,展示了read方法的实现原理:

import java.io.IOException;

public class MyCharArrayReader {
    private char[] buf;
    private int pos;
    private int markedPos;

    public MyCharArrayReader(char[] buf) {
        this.buf = buf;
        this.pos = 0;
        this.markedPos = 0;
    }

    public int read() throws IOException {
        return (pos < buf.length)? buf[pos++] : -1;
    }

    public int read(char[] cbuf, int off, int len) throws IOException {
        if (cbuf == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > cbuf.length - off) {
            throw new IndexOutOfBoundsException();
        }
        if (pos >= buf.length) {
            return -1;
        }
        int avail = buf.length - pos;
        if (len > avail) {
            len = avail;
        }
        if (len <= 0) {
            return 0;
        }
        System.arraycopy(buf, pos, cbuf, off, len);
        pos += len;
        return len;
    }

    public void close() throws IOException {
        // 对于CharArrayReader,关闭操作实际上不做任何事情,因为数据在内存中
    }
}

在这个自定义的MyCharArrayReader中,read方法根据当前位置pos从字符数组buf中读取字符,并更新posread(char[] cbuf, int off, int len)方法则会将字符从字符数组复制到传入的字符数组cbuf中,从偏移量off开始,最多复制len个字符。

Writer 原理

Writer是所有字符输出流的抽象基类,它定义了写入字符数据的方法。主要方法包括:

  1. void write(int c):将指定的字符数据(参数c的低16位)写入输出流。
  2. void write(char[] cbuf):将字符数组cbuf中的所有字符数据写入输出流。
  3. void write(char[] cbuf, int off, int len):将字符数组cbuf中从偏移量off开始的len个字符数据写入输出流。
  4. void write(String str):将字符串str中的所有字符数据写入输出流。
  5. void write(String str, int off, int len):将字符串str中从偏移量off开始的len个字符数据写入输出流。

Writer的具体实现类(如FileWriterCharArrayWriter等)会根据不同的输出目标来实现这些写入方法。例如,FileWriter用于将字符数据写入文件,它内部实际上是通过OutputStreamWriter将字符流转换为字节流来实现的。

下面是一个自定义的CharArrayWriter示例,展示了write方法的实现原理:

import java.io.IOException;

public class MyCharArrayWriter {
    private char[] buf;
    private int count;

    public MyCharArrayWriter() {
        this(32);
    }

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

    public void write(int c) {
        int newcount = count + 1;
        if (newcount > buf.length) {
            buf = expandCapacity(newcount);
        }
        buf[count] = (char) c;
        count = newcount;
    }

    public void write(char[] cbuf, int off, int len) {
        if ((off < 0) || (off > cbuf.length) || (len < 0) ||
            ((off + len) > cbuf.length) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }
        int newcount = count + len;
        if (newcount > buf.length) {
            buf = expandCapacity(newcount);
        }
        System.arraycopy(cbuf, off, buf, count, len);
        count = newcount;
    }

    public void write(String str, int off, int len) {
        if (str == null) {
            throw new NullPointerException();
        }
        if ((off < 0) || (off > str.length()) || (len < 0) ||
            ((off + len) > str.length()) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        }
        int newcount = count + len;
        if (newcount > buf.length) {
            buf = expandCapacity(newcount);
        }
        str.getChars(off, off + len, buf, count);
        count = newcount;
    }

    private char[] expandCapacity(int minimumCapacity) {
        int newCapacity = (buf.length << 1) + 2;
        if (newCapacity - minimumCapacity < 0) {
            newCapacity = minimumCapacity;
        }
        if (newCapacity < 0) {
            if (minimumCapacity < 0) {
                throw new OutOfMemoryError();
            }
            newCapacity = Integer.MAX_VALUE;
        }
        char[] newbuf = new char[newCapacity];
        System.arraycopy(buf, 0, newbuf, 0, count);
        return newbuf;
    }

    public char[] toCharArray() {
        char[] newbuf = new char[count];
        System.arraycopy(buf, 0, newbuf, 0, count);
        return newbuf;
    }
}

在这个自定义的MyCharArrayWriter中,write(int c)方法将单个字符写入字符数组buf,如果数组空间不足,则通过expandCapacity方法扩展数组。write(char[] cbuf, int off, int len)方法和write(String str, int off, int len)方法分别将字符数组和字符串中的部分字符写入buf,同样会处理数组空间不足的情况。toCharArray方法用于获取当前输出流中的所有字符数据。

缓冲流原理

缓冲输入流原理

在Java的I/O流体系中,缓冲流是非常重要的一类流,它通过在内存中设置缓冲区,减少了对底层设备的直接读写次数,从而提高了I/O操作的效率。缓冲输入流的基类是BufferedInputStream(字节缓冲输入流)和BufferedReader(字符缓冲输入流)。

BufferedInputStream为例,它内部维护了一个字节数组作为缓冲区。当调用read方法时,首先从缓冲区中读取数据。如果缓冲区中没有数据或者数据不足,则从底层的输入流(如FileInputStream)中读取一定量的数据填充缓冲区。这样,对于多次读取操作,大部分数据可以直接从缓冲区中获取,减少了对底层设备的I/O操作次数。

下面是一个简单的示例,展示BufferedInputStream的使用和原理:

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class BufferedInputStreamExample {
    public static void main(String[] args) {
        try (InputStream inputStream = new FileInputStream("example.txt");
             BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream)) {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = bufferedInputStream.read(buffer)) != -1) {
                // 处理读取到的数据
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,BufferedInputStream包装了FileInputStream。当调用bufferedInputStream.read(buffer)时,首先检查缓冲区中是否有足够的数据。如果有,则直接从缓冲区中复制数据到buffer;如果没有,则从FileInputStream中读取数据填充缓冲区,然后再从缓冲区中复制数据。

缓冲输出流原理

缓冲输出流的基类是BufferedOutputStream(字节缓冲输出流)和BufferedWriter(字符缓冲输出流)。与缓冲输入流类似,缓冲输出流也在内存中维护一个缓冲区。当调用write方法时,数据首先被写入缓冲区。只有当缓冲区满了或者调用flush方法时,缓冲区中的数据才会被真正写入到底层的输出流(如FileOutputStream)。

以下是一个BufferedOutputStream的示例:

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class BufferedOutputStreamExample {
    public static void main(String[] args) {
        try (OutputStream outputStream = new FileOutputStream("output.txt");
             BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
            byte[] data = "Hello, BufferedOutputStream!".getBytes();
            bufferedOutputStream.write(data);
            bufferedOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,调用bufferedOutputStream.write(data)时,数据被写入缓冲区。调用bufferedOutputStream.flush()时,缓冲区中的数据被写入FileOutputStream,从而真正保存到文件output.txt中。如果不调用flush方法,并且程序结束时缓冲区没有满,数据可能不会被写入文件,这就是为什么在使用缓冲输出流时,通常需要在适当的时候调用flush方法,或者在使用try-with-resources语句时确保流会自动关闭并调用flush

转换流原理

字节流到字符流的转换

在Java中,字节流和字符流之间的转换通过InputStreamReaderOutputStreamWriter来实现。InputStreamReader用于将字节输入流转换为字符输入流,而OutputStreamWriter用于将字符输出流转换为字节输出流。

InputStreamReader的构造函数接受一个InputStream实例,并根据指定的字符编码将字节数据转换为字符数据。例如,以下代码将FileInputStream转换为BufferedReader,以便以字符形式读取文件:

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class InputStreamReaderExample {
    public static void main(String[] args) {
        try (FileInputStream fileInputStream = new FileInputStream("example.txt");
             InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8");
             BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,InputStreamReaderFileInputStream转换为字符流,并使用UTF-8编码。BufferedReader则用于方便地读取文本行。

字符流到字节流的转换

OutputStreamWriter的构造函数接受一个OutputStream实例,并根据指定的字符编码将字符数据转换为字节数据。例如,以下代码将BufferedWriter转换为FileOutputStream,以便将字符数据写入文件:

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class OutputStreamWriterExample {
    public static void main(String[] args) {
        try (FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
             OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "UTF-8");
             BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter)) {
            bufferedWriter.write("Hello, OutputStreamWriter!");
            bufferedWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,OutputStreamWriterBufferedWriter的字符数据转换为字节数据,并使用UTF-8编码写入FileOutputStream。同样,flush方法用于确保数据被真正写入文件。

对象流原理

对象序列化原理

对象流(ObjectInputStreamObjectOutputStream)用于将Java对象进行序列化和反序列化。序列化是将对象转换为字节流的过程,以便可以将对象保存到文件、通过网络传输等。反序列化则是将字节流重新转换为对象的过程。

要使一个类的对象能够被序列化,该类必须实现Serializable接口。ObjectOutputStream提供了writeObject方法来将对象写入输出流。在序列化过程中,ObjectOutputStream会递归地处理对象的所有字段,包括基本类型和引用类型。对于引用类型,它会序列化引用所指向的对象。

以下是一个简单的对象序列化示例:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

class Person implements java.io.Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

public class ObjectSerializationExample {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            objectOutputStream.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,Person类实现了Serializable接口。ObjectOutputStreamPerson对象写入文件person.ser

对象反序列化原理

ObjectInputStream提供了readObject方法来从输入流中读取对象并进行反序列化。在反序列化过程中,ObjectInputStream会根据字节流中的信息重新构建对象及其所有字段。

以下是与上述序列化示例对应的反序列化示例:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ObjectDeserializationExample {
    public static void main(String[] args) {
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("person.ser"))) {
            Person person = (Person) objectInputStream.readObject();
            System.out.println("Name: " + person.getName() + ", Age: " + person.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,ObjectInputStream从文件person.ser中读取字节流,并将其反序列化为Person对象。注意,在反序列化时需要进行类型转换,并且可能会抛出ClassNotFoundException,如果反序列化时找不到对应的类定义。

数据流原理

数据输入流原理

数据流用于以特定格式读写基本数据类型和字符串。DataInputStream是数据输入流的类,它提供了一系列读取基本数据类型和字符串的方法,如readIntreadLongreadUTF等。DataInputStream通常包装在其他输入流(如FileInputStream)之上,以便从输入源中按特定格式读取数据。

以下是一个从文件中读取整数和字符串的示例:

import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class DataInputStreamExample {
    public static void main(String[] args) {
        try (DataInputStream dataInputStream = new DataInputStream(new FileInputStream("data.txt"))) {
            int number = dataInputStream.readInt();
            String str = dataInputStream.readUTF();
            System.out.println("Number: " + number + ", String: " + str);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,DataInputStream从文件data.txt中按顺序读取一个整数和一个UTF-8编码的字符串。

数据输出流原理

DataOutputStream是数据输出流的类,它提供了一系列写入基本数据类型和字符串的方法,如writeIntwriteLongwriteUTF等。DataOutputStream通常包装在其他输出流(如FileOutputStream)之上,以便将数据按特定格式写入输出目标。

以下是一个将整数和字符串写入文件的示例:

import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class DataOutputStreamExample {
    public static void main(String[] args) {
        try (DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream("data.txt"))) {
            int number = 12345;
            String str = "Hello, DataOutputStream!";
            dataOutputStream.writeInt(number);
            dataOutputStream.writeUTF(str);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,DataOutputStream将整数和字符串按特定格式写入文件data.txt。注意,在读取和写入数据时,必须按照相同的顺序和格式进行操作,否则可能会导致数据读取错误。

通过对Java输入输出流各个方面核心原理的剖析以及丰富的代码示例,相信读者对Java的I/O流体系有了更深入的理解,能够在实际编程中更有效地运用I/O流进行数据处理和传输。