看完博客收获的内容有:
- IO的基本定义
- Java IO里字节流和字符流的转换原理
- Java IO常用的API
- Java IO使用小Demo
- JavaIO模型请看这篇博客:BIO、NIO、AIO理解
一、什么是 IO?
IO(Input/Output)是计算机系统中用于处理数据传输的重要组成部分。从外部设备(如键盘、鼠标、硬盘、网络等)读取数据到内存叫输入(Input),将内存中的数据发送到外部设备或永久性存储介质中叫输出(Output)。这里可以看出输入输出是从内存的角度定义的。
二、计算机怎么控制IO?
生活场景中的IO操作
- 人工输入输出: 人们可以通过键盘输入数据,观察显示器上的信息触发输出操作。
- 使用操作系统及应用程序: 操作系统(如Windows)提供了文件管理和交互界面等功能,用户可以通过文件管理器创建、编辑和删除文件;通过文本编辑器创建、修改和保存文本文件等。
- 物理介质操作: 直接读取和写入物理存储介质(如硬盘、光盘、U盘)中的数据,例如使用磁盘操作系统命令或者物理按钮。
- 设备交互: 使用外部设备进行IO交互,比如打印机输出文档、扫描仪将纸质文件转换为数字格式等。
计算机做了什么?
上面的操作都依赖了计算机操作系统提供的工具和设备。
当用户在键盘上输入时,键盘会将按键的信号传输给计算机,计算机的输入设备驱动程序会将这些信号转换为字符或者其他数据格式,并将其存储在内存中,以便应用程序可以读取和处理。计算机会将数据从内存中发送到显卡,显卡负责将数据转换成图像信号并发送到显示器,最终在屏幕上显示出来。
当用户在操作系统的文件管理器中创建、编辑或删除文件时,计算机会对文件系统进行相应的操作,包括在磁盘上分配空间、写入数据、修改文件属性等。
当用户执行物理介质操作时(如插入U盘、打开光驱等),计算机会通过硬件控制器识别设备并与之进行通信,以便读取或写入数据。
三、Java IO
1. java IO和操作系统IO的关系
Java IO是通过底层操作系统提供的IO功能来实现的。Java通过JNI(Java Native Interface)和本地方法库与操作系统进行交互,利用操作系统提供的底层IO功能实现文件操作、网络通信等。下面是FileInputStream类的代码片段,可以看到read调用的是本地方法read0()方法(这个本地方法实现了JVM和底层操作系统的交互)。
public int read() throws IOException {
return read0();
}
private native int read0() throws IOException;
在执行Java IO操作时,Java虚拟机会调用操作系统的相关IO功能,比如在文件读写中会调用操作系统的文件系统接口,而在网络通信中会调用操作系统的网络协议栈。这样可以充分利用操作系统提供的高效IO机制,同时确保跨平台性能良好。
Java的IO类库对底层的IO功能进行了封装,并提供了更易用的API,使得开发人员能够以一致的方式进行IO操作,而无需关心底层操作系统的细节。这种方式既简化了开发,又保证了跨平台性和可移植性。
2. Java IO API结构
数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Writer/Reader
:字符流,基于字符处理数据
InputStream/OutputStream
: 字节流,基于字节处理数据
计算机里面存储数据的基本单位是字节,那为什么java还要基于字符流的API呢?
基于字节的IO流以字节为单位进行读取和写入,适用于处理任意类型的数据,包括文本数据和二进制数据。
基于字符的IO流则在底层使用基于字节的流,但提供了字符集编码解码的功能,更方便地处理文本数据,并且可以直接按照字符进行读取和写入。这种区分使得程序员可以根据需要选择合适的IO流来处理不同类型的数据。
字符流在Java中提供了编码解码功能,主要是通过使用InputStreamReader和OutputStreamWriter。这两个类包装了字节流并提供了字符集编码和解码的功能。
这些类内部会维护一个字节流(如FileInputStream或FileOutputStream),并且可以指定字符集(如UTF-8、GBK等)。当字符数据写入时,OutputStreamWriter会将字符使用指定的字符集编码成字节数据,然后通过底层的字节流进行写入;而InputStreamReader则会读取字节数据,并使用指定的字符集进行解码,最终输出字符数据。
这种设计使得字符流可以很方便地处理文本数据的编码和解码工作,程序员可以直接操作字符数据而不用关心具体的字节转换过程。
下面是一个字符流的使用案例:
import java.io.*;
public class CharacterStreamExample {
public static void main(String[] args) {
String data = "Hello, 你好";
try (OutputStream outputStream = new FileOutputStream("output.txt");
Writer writer = new OutputStreamWriter(outputStream, "UTF-8")) {
writer.write(data); //用字节输出流将data字符串写入output.txt文件中
} catch (IOException e) {
e.printStackTrace();
}
try (InputStream inputStream = new FileInputStream("output.txt");
//InutStreamReader可以将字节输入流解码成字符输入流(字节流和字符流的转换)
Reader reader = new InputStreamReader(inputStream, "UTF-8")) {
int character;
while ((character = reader.read()) != -1) {
System.out.print((char) character);//打印读到的字符
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
那么InputStreamReader是如何实现这种转换的呢?——看源码
public class InputStreamReader extends Reader {
private final StreamDecoder sd;
/**
* Creates an InputStreamReader that uses the
* {@link Charset#defaultCharset() default charset}.
*
* @param in An InputStream
*
* @see Charset#defaultCharset()
*/
public InputStreamReader(InputStream in) {
super(in);
sd = StreamDecoder.forInputStreamReader(in, this,
Charset.defaultCharset()); // ## check lock object
}
可以看到InputStreamReader里面有一个StreamDecoder对象,在构造函数中,传入了一个字节输入流对象,然后StreamDecoder对象是由它的forInputStreamReader方法创建的。从名字可以看出这个对象是用来将字节输入流转换成字符输入流的,还传入了一个默认的编码方式。接着我们进入StreamDecoder类
private final Charset cs;
private final CharsetDecoder decoder;
private final ByteBuffer bb;
// Exactly one of these is non-null
private final InputStream in;
private final ReadableByteChannel ch;
StreamDecoder(InputStream in, Object lock, Charset cs) {
this(in, lock,
cs.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE));
}
StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) {
super(lock);
this.cs = dec.charset();
this.decoder = dec; //重点是CharsetDecoder
this.in = in;
this.ch = null;
bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);
bb.flip(); // So that bb is initially empty
}
StreamDecoder里面有一个CharsetDecoder,这就是解码的关键。想了解具体是如何进行解码的,可以去看看相关源码。
总之,解码是StreamDecoder类来实现的,而InputStreamReader是一个统一入口。到这里就简要阐述了为什么说Java字符流是基于字节流实现的。
3. Java IO API的基本使用
下面是一些使用JAVA IO的小Demo,供参考
Demo01:读取文本文件的内容,并输出
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadFileDemo {
public static void main(String[] args) {
//BufferedReader装饰了FileReader,提供了缓冲来减少IO操作
try (BufferedReader reader = new BufferedReader(new FileReader("sample.txt"))) {
String line;
while ((line = reader.readLine()) != null) { //readLine() 一行行读取
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Demo02:将文本写入文件
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class WriteFileDemo {
public static void main(String[] args) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write("Hello, World!"); //将文本内容写入output.txt
} catch (IOException e) {
e.printStackTrace();
}
}
}
Demo03:用字节流复制文件内容
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopyDemo {
public static void main(String[] args) {
try (FileInputStream in = new FileInputStream("input.txt");
FileOutputStream out = new FileOutputStream("output.txt")) {
byte[] buffer = new byte[1024];// 创建了一个1024字节的缓冲区
int bytesRead;
//将字节读入缓冲区,byteRead == -1表示读到了文件末尾
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);//写入另一个文件
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Demo04:用字符流写入文件内容
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class WriteToFileDemo {
public static void main(String[] args) {
//PrintWriter相比于BufferedWriter提供了更丰富的输出
//(比如输出浮点数和长整型,这是BufferedWriter所不支持的)
try (PrintWriter writer = new PrintWriter(new FileWriter("output.txt"))) {
writer.println("Hello, World!");
writer.println("This is a sample text.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
Demo05: 将对象序列化输出到文件
import java.io.*;
//序列化必须实现Serializable接口,这个接口唯一的作用就是告诉JVM这个类可被序列化
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class ObjectSerializationDemo {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
//ObjectPutputStream 提供将对象序列化为字节流的功能,并输出到文件
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
out.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
}
}
下面是对常用的JAVA IO API整理
-
下表对Java I/O中一些常用的类进行了分类整理:
-
下面是对InputStream常用方法的整理
-
下面是对OutputStream常用方法的整理
-
下面是对Reader常用方法的整理
-
下面是对Writer常用方法的整理
总结
虽然Java IO的类很多,但基本的输入输出流就InputStream/Reader 和 OutputStream/Writer,其他的类基本都是这四个流的功能增强版(装饰者模式),平时使用可以根据需要分别选择不同的装饰器。