Java中的IO(Input/Output,输入/输出)是处理数据流的一种方式,它允许我们从文件、网络或其他数据源读取数据,或者将数据写入到文件、网络或其他数据源。从Java 1.0开始,Java IO API就是以流的方式来处理输入和输出。
Java IO分为字节流和字符流两种类型。字节流(Byte Stream)是以字节为单位进行读写的,可以用于读写任意类型的数据;而字符流(Character Stream)是以字符为单位进行读写的,主要用于读写字符数据。
字节流和字符流
字节流类主要包括InputStream
和OutputStream
,而字符流类主要包括Reader
和Writer
。
InputStream
和OutputStream
InputStream
和OutputStream
是抽象类,分别代表输入流和输出流。常用的实现类有FileInputStream
、FileOutputStream
、ByteArrayInputStream
和ByteArrayOutputStream
等。
// 使用InputStream读取文件
try (InputStream input = new FileInputStream("filePath")) {
int data;
while ((data = input.read()) != -1) {
// 处理读取到的字节数据
}
} catch (IOException e) {
// 处理异常
}
// 使用OutputStream写入文件
try (OutputStream output = new FileOutputStream("filePath")) {
byte[] data = "Hello, Java IO".getBytes();
output.write(data);
} catch (IOException e) {
// 处理异常
}
Reader
和Writer
Reader
和Writer
也是抽象类,代表字符输入流和字符输出流。常用的实现类有FileReader
、FileWriter
、BufferedReader
和BufferedWriter
等。
// 使用Reader读取文件
try (Reader reader = new FileReader("filePath")) {
int data;
while ((data = reader.read()) != -1) {
// 处理读取到的字符数据
}
} catch (IOException e) {
// 处理异常
}
// 使用Writer写入文件
try (Writer writer = new FileWriter("filePath")) {
String data = "Hello, Java IO";
writer.write(data);
} catch (IOException e) {
// 处理异常
}
File类
File类是Java表示文件和目录路径的标准类,它提供了一系列方法来管理文件系统中的文件和目录。
// 创建File对象
File file = new File("filePath");
// 判断文件是否存在
if (file.exists()) {
// 文件存在
} else {
// 文件不存在
}
// 创建新文件
try {
boolean success = file.createNewFile();
if (success) {
// 文件创建成功
} else {
// 文件创建失败
}
} catch (IOException e) {
// 处理异常
}
// 删除文件或目录
boolean success = file.delete();
if (success) {
// 文件或目录删除成功
} else {
// 文件或目录删除失败
}
// 判断是否是目录
boolean isDirectory = file.isDirectory();
// 判断是否是文件
boolean isFile = file.isFile();
// 获取文件名
String fileName = file.getName();
// 获取文件路径
String filePath = file.getAbsolutePath();
InputStream/OutputStream和Reader/Writer接口
在Java中,IO类的基础是byte和char数据类型,而不是对象数据类型。Java使用InputStream、OutputStream、Reader和Writer接口来实现IO操作。
InputStream和OutputStream
InputStream和OutputStream是Java IO中最基本的输入输出抽象,用于读写字节流。InputStream接口中的read()方法可以读取一个字节,返回值为读取到的字节数据,如果已经到达流的末尾,则返回-1;而OutputStream接口中的write()方法用于写入一个字节,参数为一个byte类型的数据。
// 使用InputStream读取文件
try (InputStream input = new FileInputStream("filePath")) {
int data;
while ((data = input.read()) != -1) {
// 处理读取到的字节数据
}
} catch (IOException e) {
// 处理异常
}
// 使用OutputStream写入文件
try (OutputStream output = new FileOutputStream("filePath")) {
byte[] data = "Hello, Java IO".getBytes();
output.write(data);
} catch (IOException e) {
// 处理异常
}
Reader和Writer
Reader和Writer是用于读写字符流的Java IO接口,它们可以自动将字节转换为字符。Reader接口中的read()方法可以读取一个字符,返回值为读取到的字符,如果已经到达流的末尾,则返回-1;而Writer接口中的write()方法用于写入一个字符,参数为一个char类型的数据。
// 使用Reader读取文件
try (Reader reader = new FileReader("filePath")) {
int data;
while ((data = reader.read()) != -1) {
// 处理读取到的字符数据
}
} catch (IOException e) {
// 处理异常
}
// 使用Writer写入文件
try (Writer writer = new FileWriter("filePath")) {
String data = "Hello, Java IO";
writer.write(data);
} catch (IOException e) {
// 处理异常
}
NIO
在Java 1.4中引入的NIO(New IO)是一组针对Java IO进行改进的API,主要由Channel
、Buffer
和Selector
三个主要模块组成。它提供了一种基于缓冲区(Buffer)和通道(Channel)的IO方式,与传统IO流(Stream)完全不同,提供更高效的数据操作和更好的可伸缩性。
Channel
Channel 是 NIO中的一个抽象概念,它代表着数据源或数据目标的连接,类似于传统 IO 中的流。 Channel 接口提供了数据连接的独立性,例如可以打开到 TCP 网络套接字的 Channel,读写此套接字中的数据,而不需要考虑底层操作系统传输数据需要的套接字相关 API、传输速度的限制等。
NIO 中最常使用的 Channel 类型有如下几种:
- FileChannel:用于文件的读取和写入。
- DatagramChannel:用于UDP协议进行数据读取和写入。
- SocketChannel:用于TCP协议中的数据读取和写入。
- ServerSocketChannel:可以监听新进来的TCP连接,对每个新进来的连接都会创建一个 SocketChannel。
// 创建Channel
try (SocketChannel channel = SocketChannel.open()) {
channel.configureBlocking(false); // 将Channel设置为非阻塞模式
channel.connect(new InetSocketAddress("example.com", 80));
// 检查是否连接成功
if (channel.finishConnect()) {
// 连接成功,可以进行读写操作
}
} catch (IOException e) {
// 处理异常
}
// 使用Channel读取数据
try (RandomAccessFile file = new RandomAccessFile("filePath", "rw")) {
FileChannel channel = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = channel.read(buffer);
}
} catch (IOException e) {
// 处理异常
}
// 使用Channel写入数据
try (RandomAccessFile file = new RandomAccessFile("filePath", "rw")) {
FileChannel channel = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello, Java NIO".getBytes());
buffer.flip();
channel.write(buffer);
} catch (IOException e) {
// 处理异常
}
Buffer
Buffer 是一个对象,包含一些要写入或者要读出的数据。在 NIO 中,所有数据都是通过 Buffer 对象来处理的。它是一个线性的、有限的数据区域,可以用来存储数据。Buffer 类型包括 ByteBuffer、CharBuffer、DoubleBuffer等等。
- ByteBuffer:用于读写字节数据。
- CharBuffer:用于读写字符数据,底层实现是使用的是 char 数组,但是在底层,仍然是通过 ByteBuffer 实现的。
- DoubleBuffer:用于读写 double 类型数据。
// 创建Buffer
CharBuffer buffer = CharBuffer.allocate(1024);
// 写入数据到Buffer
buffer.put("Hello, Java NIO");
// 切换Buffer为读取模式
buffer.flip();
// 从Buffer中读取数据
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
Selector
Selector 是 NIO 的核心组件之一,用于检测多个 Channel 的事件状态。一个单独的线程可以检查多个 Channel 的状态,从而处理多个 Channel 的网络请求。 Java NIO 的 Selector 实现类似于 UNIX/Linux的 epoll 机制。
// 创建Selector
Selector selector = Selector.open();
// 注册Channel到Selector,并监听感兴趣的事件
channel.register(selector, SelectionKey.OP_READ);
while (true) {
// 阻塞等待就绪的Channel
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
// 获取就绪的Channel的Key,进行读写操作
Set<SelectionKey> selectedKeys = selector.selectedKeys();
for (SelectionKey key: selectedKeys) {
if (key.isReadable()) {
// 可读事件
} else if (key.isWritable()) {
// 可写事件
}
}
// 处理完后,记得清空Key集合
selectedKeys.clear();
}
什么是序列化?
在Java编程中,序列化(serialization)是指将对象转换为字节流的过程,可以将对象在网络中传输或者保存到磁盘中,使得对象的状态能够被保存和恢复。序列化之后的字节流可以在网络上传输,也可以保存到本地的文件系统中。
为什么需要序列化?
Java序列化技术的出现主要是为了解决对象的存储、传输和分布式计算的问题。通过序列化,可以将对象以字节流的形式进行传输,实现对象在网络中的传输。另外,当涉及到分布式系统、持久化存储等场景时,序列化同样具有重要作用。
如何进行序列化?
在Java中,要使一个对象能够被序列化,需要满足两个条件:
- 类必须实现
java.io.Serializable
接口; - 所属类的所有属性必须是可序列化的(基本类型和包装类、String等都可以序列化)。
接下来我们通过一个例子来演示序列化的过程:
import java.io.*;
public class SerializationExample implements Serializable {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public static void main(String[] args) {
SerializationExample obj = new SerializationExample();
obj.setName("John");
obj.setAge(25);
try {
FileOutputStream fileOut = new FileOutputStream("object.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
System.out.println("对象被序列化并保存到 object.ser 文件");
} catch (IOException i) {
i.printStackTrace();
}
}
}
在这个例子中,我们创建了一个 SerializationExample
类,它实现了 Serializable
接口。我们创建了一个对象 obj
,设置了其属性值,并且将其序列化并保存到 object.ser
文件中。
什么是反序列化?
反序列化(deserialization)是指将字节流转换为对象的过程,通过反序列化,可以将对象从字节流中重新恢复出来。这样就可以在网络中传输对象,或者从磁盘中读取对象并进行恢复。
如何进行反序列化?
要进行反序列化,需要使用 ObjectInputStream
类,通过调用 ObjectInputStream
的 readObject()
方法实现反序列化。
以下是一个反序列化的例子:
import java.io.*;
public class DeserializationExample {
public static void main(String[] args) {
SerializationExample obj = null;
try {
FileInputStream fileIn = new FileInputStream("object.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
obj = (SerializationExample) in.readObject();
in.close();
fileIn.close();
} catch (IOException i) {
i.printStackTrace();
return;
} catch (ClassNotFoundException c) {
System.out.println("class not found");
c.printStackTrace();
return;
}
System.out.println("对象被反序列化:");
System.out.println("Name: " + obj.getName());
System.out.println("Age: " + obj.getAge());
}
}
在上面的例子中,我们使用 FileInputStream
来读取 object.ser
文件的字节流,并使用 ObjectInputStream
的 readObject()
方法进行反序列化。最终我们将恢复出的对象的属性值进行打印。
需要注意的是,在进行反序列化时,类的定义必须是可用的,否则会抛出 ClassNotFoundException
异常。
一些额外注意事项
在进行序列化和反序列化时,有一些需要额外注意的事项:
- 静态变量不会被序列化,因为它们属于类而不是对象;
- 被
transient
修饰的变量不会被序列化,因此在反序列化时会被赋予默认值; - 序列化和反序列化的类必须有相同的
serialVersionUID
,否则会导致反序列化失败。