Java 输入输出流核心原理剖析
Java 输入输出流基础概念
在Java编程中,输入输出流(I/O Streams)是一个至关重要的概念,用于处理程序与外部设备(如文件、网络连接、控制台等)之间的数据传输。输入流用于从外部设备读取数据到程序中,而输出流则用于将程序中的数据写入到外部设备。
Java的I/O流体系非常庞大且灵活,它基于字节流和字符流两种基本类型构建。字节流用于处理8位字节数据,适用于处理二进制数据,如图片、音频、视频等;字符流则用于处理16位Unicode字符数据,主要用于处理文本数据。
字节流
字节流的基类是InputStream
和OutputStream
。InputStream
提供了一系列从输入源读取字节数据的方法,而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
。注意,在使用完流后,需要关闭流以释放资源。
字符流
字符流的基类是Reader
和Writer
。与字节流不同,字符流处理的是字符数据,这使得处理文本更加方便。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()
,该方法有以下几种重载形式:
int read()
:从输入流中读取一个字节的数据,并返回该字节数据(以0到255之间的整数表示)。如果到达流的末尾,则返回 -1。int read(byte[] b)
:从输入流中读取最多b.length
个字节的数据,并将其存储到字节数组b
中。返回实际读取的字节数,如果到达流的末尾,则返回 -1。int read(byte[] b, int off, int len)
:从输入流中读取最多len
个字节的数据,并将其存储到字节数组b
中,从偏移量off
开始存储。返回实际读取的字节数,如果到达流的末尾,则返回 -1。
InputStream
的具体实现类(如FileInputStream
、ByteArrayInputStream
等)会根据不同的输入源来实现这些读取方法。例如,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
中读取数据,并更新pos
。read(byte[] b, int off, int len)
方法则会将数据从字节数组复制到传入的字节数组b
中,从偏移量off
开始,最多复制len
个字节。
OutputStream 原理
OutputStream
是所有字节输出流的抽象基类,它定义了一系列写入字节数据的方法。核心方法包括:
void write(int b)
:将指定的字节数据(参数b
的低8位)写入输出流。void write(byte[] b)
:将字节数组b
中的所有字节数据写入输出流。void write(byte[] b, int off, int len)
:将字节数组b
中从偏移量off
开始的len
个字节数据写入输出流。
与InputStream
类似,OutputStream
的具体实现类(如FileOutputStream
、ByteArrayOutputStream
等)会根据不同的输出目标来实现这些写入方法。例如,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
是所有字符输入流的抽象基类,它定义了读取字符数据的方法。主要方法包括:
int read()
:从输入流中读取一个字符的数据,并返回该字符(以0到65535之间的整数表示)。如果到达流的末尾,则返回 -1。int read(char[] cbuf)
:从输入流中读取最多cbuf.length
个字符的数据,并将其存储到字符数组cbuf
中。返回实际读取的字符数,如果到达流的末尾,则返回 -1。int read(char[] cbuf, int off, int len)
:从输入流中读取最多len
个字符的数据,并将其存储到字符数组cbuf
中,从偏移量off
开始存储。返回实际读取的字符数,如果到达流的末尾,则返回 -1。
Reader
的具体实现类(如FileReader
、CharArrayReader
等)会根据不同的输入源来实现这些读取方法。例如,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
中读取字符,并更新pos
。read(char[] cbuf, int off, int len)
方法则会将字符从字符数组复制到传入的字符数组cbuf
中,从偏移量off
开始,最多复制len
个字符。
Writer 原理
Writer
是所有字符输出流的抽象基类,它定义了写入字符数据的方法。主要方法包括:
void write(int c)
:将指定的字符数据(参数c
的低16位)写入输出流。void write(char[] cbuf)
:将字符数组cbuf
中的所有字符数据写入输出流。void write(char[] cbuf, int off, int len)
:将字符数组cbuf
中从偏移量off
开始的len
个字符数据写入输出流。void write(String str)
:将字符串str
中的所有字符数据写入输出流。void write(String str, int off, int len)
:将字符串str
中从偏移量off
开始的len
个字符数据写入输出流。
Writer
的具体实现类(如FileWriter
、CharArrayWriter
等)会根据不同的输出目标来实现这些写入方法。例如,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中,字节流和字符流之间的转换通过InputStreamReader
和OutputStreamWriter
来实现。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();
}
}
}
在这个示例中,InputStreamReader
将FileInputStream
转换为字符流,并使用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();
}
}
}
在这个示例中,OutputStreamWriter
将BufferedWriter
的字符数据转换为字节数据,并使用UTF-8
编码写入FileOutputStream
。同样,flush
方法用于确保数据被真正写入文件。
对象流原理
对象序列化原理
对象流(ObjectInputStream
和ObjectOutputStream
)用于将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
接口。ObjectOutputStream
将Person
对象写入文件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
是数据输入流的类,它提供了一系列读取基本数据类型和字符串的方法,如readInt
、readLong
、readUTF
等。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
是数据输出流的类,它提供了一系列写入基本数据类型和字符串的方法,如writeInt
、writeLong
、writeUTF
等。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流进行数据处理和传输。