文章目录
I/O 是什么
I/O 指的是输入/输出(Input/Output),在 Java 中 I/O 操作主要是指使用 Java 进行输入和输出操作
I/O 分类
IO可以按照数据处理方式分为以下两类
- 流 I/O:传输过程是以字节流形式进行的,这样的设备是不需要缓冲机制的,简单易用但效率较低
- 块 I/O:把数据打包成块进行传输,传输基本单位为块,传输过程中需要缓冲区(buffer)支持,读写也是以块作为基本单位,效率很高但编程比较复杂
数据流
数据流是一串连续不断的数据的集合,数据写入程序可以是一段、一段地向数据流管道中写入数据,这些数据段会按先后顺序形成一个长的数据流
对数据读取程序来说,看不到数据流在写入时的分段情况,每次可以读取其中的任意长度的数据,但只能先读取前面的数据后,再读取后面的数据
不管写入时是将数据分多次写入,还是作为一个整体一次写入,读取时的效果都是完全一样的
数据流可以分为两类(以内存为判断标准)
- 输入流:从外界读取数据写入内存
- 输出流:从内存中读取数据到外界
I/O 体系结构
整个 java.io 包中最重要的就是五个类和一个接口
- 五个类:File、OutputStream、InputStream、Writer、Reader
- 一个接口:Serializable
在学习过重很容易混淆,分不清是字节流还是字符流,看每个类的最后这个单词,如果是 Stream 的话就是字节流,如果是 Reader/Writer 的话就是字符流
Java 中字符是采用 Unicode 标准,一个字符是16位,即一个字符使用两个字节来表示,Java 中引入了处理字符的流
I/O 流分类
- 文件流
- 管道流
- 字节/字符数组流
- Buffered 缓冲流
- 转化流
- 数据流
- 打印流
- 对象流
- 序列化流
- 非流类
文件流
- FileInputStream(字节输入流)
- FileOutputStream(字节输出流)
- FileReader(字符输入流)
- FileWriter(字符输出流)
管道流
- PipedInputStream(字节输入流)
- PipedOutStream(字节输出流)
- PipedReader(字符输入流)
- PipedWriter(字符输出流)
PipedInputStream的一个实例要和PipedOutputStream的一个实例共同使用,共同完成管道的读取写入操作,主要用于线程操作
字节/字符数组流
- ByteArrayInputStream
- ByteArrayOutputStream
- CharArrayReader
- CharArrayWriter
在内存中开辟了一个字节或字符数组
Buffered 缓冲流
- BufferedInputStream
- BufferedOutputStream
- BufferedReader
- BufferedWriter
是带缓冲区的处理流,缓冲区的作用的主要目的是:避免每次和硬盘打交道,提高数据访问的效率
转化流
- InputStreamReader
- OutputStreamWriter
把字节转化成字符
数据流
- DataInputStream
- DataOutputStream
因为平时若是我们输出一个8个字节的long类型或4个字节的float类型,那怎么办呢?
可以一个字节一个字节输出,也可以把转换成字符串输出,但是这样转换费时间,若是直接输出该多好啊,因此这个数据流就解决了我们输出数据类型的困难
数据流可以直接输出float类型或long类型,提高了数据读写的效率
打印流
- PrintStream
- PrintWriter
一般是打印到控制台,可以进行控制打印的地方
对象流
- ObjectInputStream
- ObjectOutputStream
把封装的对象直接输出,而不是一个个在转换成字符串再输出
序列化流
- SequenceInputStream
对象序列化:把对象直接转换成二进制,写入介质中
非流类
- RandomAccessFile 从文件的任意位置进行存取(输入输出)操作
- File 提供了描述文件和目录的操作与管理方法,主要用于命名文件、查询文件属性和处理文件目录
InputStream
InputStream:为字节输入流,它本身为一个抽象类,必须依靠其子类实现各种功能,是所有字节输入流类的超类,继承自 InputStream 的流都是向内存中输入数据,且数据单位为字节(8bit)
常用方法
// 读取一个 byte 的数据,返回值是高位补0的 int 类型值。若返回值=-1说明没有读取到任何字节读取工作结束
abstract int read()
// 读取 b.length 个字节的数据放到b数组中,返回值是读取的字节数
int read(byte b[])
// 读取 len 个字节的数据,存放到偏移量为off的b数组中,返回值是读取的字节数
int read(byte b[], int off, int len)
// 返回输入流中可以读取的字节数。注意:若输入阻塞,当前线程将被挂起,如果 InputStream 对象调用这个方法的话
//它只会返回0,这个方法必须由继承 InputStream 类的子类对象调用才有用
int available()
// 忽略输入流中的 n 个字节,返回值是实际忽略的字节数, 跳过一些字节来读取
long skip(long n)
// 我们在使用完后,必须对我们打开的流进行关闭
int close()
源码解读
/**
* 读取 len 个字节的数据,存放到偏移量为off的b数组中,返回值是读取的字节数
*/
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) {
// 如果b[].length - 偏移量off < 读取字节数 len,抛出异常
// 如果读取字节数大于 b[] 从偏移量off到最后的元素个数,意味着无法把数据全部装入 b[]
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
// 父类调用子类的 read() 实现
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
// 依次循环从输入流中读取一个字节存储到 b[] 中
for (; i < len ; i++) {
c = read();
if (c == -1)
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
OutputStream
OutputStream:为字节输出流,它本身为一个抽象类,必须依靠其子类实现各种功能,是所有字节输出流类的超类,继承自 OutputStream 的流都是从内存中输出数据,且数据单位为字节(8bit)
常用方法
// 输出一个字节, 抽象方法,子类实现
void write(int b)
// 将字节数组 b[] 写入输出流
void write(byte b[])
// 将字节数组 b[],从 off 位置开始,把 len 个字节写入输出流
write(byte b[], int off, int len)
// 强制把缓冲区内容输出,刷空输出流
flush()
// 关闭
close()
源码解读
/**
* 输出字节到字节数组 b[],从 off 位置开始,数量为 len
*/
public void write(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
// 遍历一个个调用子类实现的 write(int b) 方法,输出字节
for (int i = 0 ; i < len ; i++) {
write(b[off + i]);
}
}
Reader
用于读取字符流的抽象类
常用方法
// 将字符流存入缓冲区 target,返回实际存入缓冲区的数量,返回 -1 表示已存满
int read(java.nio.CharBuffer target)
// 读取一个字符存入字符流,返回值为读取的字符
int read()
// 读取一系列字符存入数组 cbuf[] 中,返回值为实际读取的字符的数量
int read(char cbuf[])
// 读取 len 个字符,从数组 cbuf[] 的下标 off 处开始存放,返回值为实际读取的字符数量,该方法必须由子类实现
abstract int read(char cbuf[],int off,int len)
// 跳过指定的字符数 n,返回实际跳过的字符数
long skip(long n)
// 判断这个字节流是否能被读
boolean ready()
// 标记字符位置
void mark(int readAheadLimit)
// 重新回到标记的字符
void reset()
// 关闭字符输入流
abstract void close()
源码解读
/**
* 此方法可以把字符输入流的数据保存到缓冲区中,返回实际存进缓冲区的数量,如果为-1表示已经到底了
*/
public int read(java.nio.CharBuffer target) throws IOException {
// 获得缓冲区还可以缓冲字符的长度
int len = target.remaining();
char[] cbuf = new char[len];
// 调用 read(char cbuf[], int off, int len)方法,读取 len 个字符到字符数组 cbuf,偏移量 off,返回实际读取字符的数量 n
int n = read(cbuf, 0, len);
//如果有读取,就把读取到的字符存进字符缓冲区中
if (n > 0)
target.put(cbuf, 0, n);
return n;
}
/**
* 跳过指定的字符数 n,返回实际跳过的字符数
*/
public long skip(long n) throws IOException {
if (n < 0L)
throw new IllegalArgumentException("skip value is negative");
// nn 为一次跳过的字符数,不能大于maxSkipBufferSize。
int nn = (int) Math.min(n, maxSkipBufferSize);
// 做同步锁,防止多线程访问的时候出现奇怪的逻辑现象
synchronized (lock) {
if ((skipBuffer == null) || (skipBuffer.length < nn))
//就会重新创建一个nn长度的字符数组
skipBuffer = new char[nn];
long r = n;//用于储存还需要跳过的字符数
while (r > 0) {
//注意这里的(int)Math.min(r, nn),和前面的(int) Math.min(n, maxSkipBufferSize)搭配。
int nc = read(skipBuffer, 0, (int)Math.min(r, nn));
//如果输入流已经到底了,直接跳出循环
if (nc == -1)
break;
//减去已经跳过的字符数
r -= nc;
}
//需要跳过的字符数-还需跳过的字符数=实际跳过的字符数。
return n - r;
}
}
Writer
写入字符流的抽象类
常用方法
// 将整型值 c 写入输出流
void write(int c)
// 将字符数组 cbuf[] 写入输出流
void write(char cbuf[])
// 将字符数组 cbuf[] 中的从索引为 off 的位置处开始的 len 个字符写入输出流(子类实现)
abstract void write(char cbuf[],int off,int len)
// 将字符串 str 中的字符写入输出流
void write(String str)
// 将字符串str 中从索引off开始处的len个字符写入输出流
void write(String str,int off,int len)
// 刷空输出流,并输出所有被缓存的字节
abstract void flush( )
// 关闭流
abstract void close()
源码解读
/**
* 将整型 c 写入输出流
*/
public void write(int c) throws IOException {
synchronized (lock) {
if (writeBuffer == null){
writeBuffer = new char[WRITE_BUFFER_SIZE];
}
writeBuffer[0] = (char) c;
write(writeBuffer, 0, 1);
}
}
/**
* 将字符串str 中从索引off开始处的len个字符写入输出流
*/
public void write(String str, int off, int len) throws IOException {
synchronized (lock) {
char cbuf[];
// 判断写入元素个数 len 与 缓冲 buffer 大小,以较大的值为 buffer 容量
if (len <= WRITE_BUFFER_SIZE) {
if (writeBuffer == null) {
writeBuffer = new char[WRITE_BUFFER_SIZE];
}
cbuf = writeBuffer;
} else { // Don't permanently allocate very large buffers.
cbuf = new char[len];
}
// 调用 String 的 getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) 方法
// 此方法是把字符串的起始位置 srcBegin 到结束位置 srcEnd这个区间内的字符串,拷贝到字符数组中,并从偏移量 dstBegin 位置开始存储
str.getChars(off, (off + len), cbuf, 0);
// 把字符数组写入输出流
write(cbuf, 0, len);
}
}
/**
* 将指定的字符序列追加到输出流
*/
public Writer append(CharSequence csq) throws IOException {
if (csq == null)
write("null");
else
// 最终调用的是 write(String str, int off, int len)
write(csq.toString());
return this;
}
public Writer append(CharSequence csq, int start, int end) throws IOException {
CharSequence cs = (csq == null ? "null" : csq);
// CharSequence 的 subSequence(int start, int end) 方法是获取从起始位置 start 到结束位置 end之间的 CharSequence
write(cs.subSequence(start, end).toString());
return this;
}
CharSequence是一个描述字符串结构的接口,在这个接口里面一般发现有三种常用的子类:String, StringBuffer, StirngBuilder
File
IO流操作中大部分都是对文件的操作,所以Java就提供了File类供我们来操作文件
File: 文件和目录(文件夹)路径名的抽象表示形式
常用方法
/**
* 构造方法
**/
// 根据一个路径得到File对象
File(String pathname)
// 根据一个目录和一个子文件/目录得到File对象
File(String parent, String child)
// 根据一个父File对象和一个子文件/目录得到File对象
File(File parent, String child)
/**
* 创建功能
**/
// 创建文件 如果存在这样的文件,就不创建了
boolean createNewFile()
// 创建文件夹 如果存在这样的文件夹,就不创建了
boolean mkdir()
// 创建文件夹,如果父文件夹不存在,会帮你创建出来
boolean mkdirs()
/**
* 删除功能
**/
// 它可以删除文件和文件夹(目录),但要删除一个文件夹时,请注意该文件夹内不能包含文件或者文件夹
boolean delete()
/**
* 重命名功能
**/
// 如果路径名相同,就是改名,如果路径名不同,就是改名并剪切
boolean renameTo(File dest)
/**
* 判断功能
**/
// 判断是否是目录
boolean isDirectory()
// 判断是否是文件
boolean isFile()
// 判断是否存在
boolean exists()
// 判断是否可读
boolean canRead()
// 判断是否可写
boolean canWrite()
// 判断是否隐藏
boolean isHidden()
/**
* 获取功能
**/
// 获取绝对路径
String getAbsolutePath()
// 获取相对路径
String getPath()
// 获取名称
String getName()
// 获取长度。字节数
long length()
// 获取最后一次的修改时间,毫秒值
long lastModified()
/**
* 高级获取功能
**/
// 获取指定目录下的所有文件或者文件夹的名称数组
String[] list()
// 获取指定目录下的所有文件或者文件夹的File数组
File[] listFiles()
/**
* 过滤器功能
**/
String[] list(FilenameFilter filter)
File[] listFiles(FilenameFilter filter)
源码解读
再看 File 类源码之前需要先学习 FileSystem
FileSystem是一个文件创建的抽象类,jdk中是 UnixFileSystem 继承此类,并做了接口的实现,在 File 类中是以参数名 fs 存在
File类的四个构造方法
/**
* 根据一个路径得到File对象
**/
public File(String pathname) {
if (pathname == null) {
throw new NullPointerException();
}
// 校验路径的合法性
this.path = fs.normalize(pathname);
// 判断文件路径是否以 / 符号开始
this.prefixLength = fs.prefixLength(this.path);
}
/**
* 根据一个目录和一个子文件/目录得到 File 对象
**/
public File(String parent, String child) {
if (child == null) {
throw new NullPointerException();
}
if (parent != null) {
if (parent.equals("")) {
this.path = fs.resolve(fs.getDefaultParent(),
fs.normalize(child));
} else {
this.path = fs.resolve(fs.normalize(parent),
fs.normalize(child));
}
} else {
this.path = fs.normalize(child);
}
this.prefixLength = fs.prefixLength(this.path);
}
/**
* 根据一个父 File 对象和一个子文件/目录得到 File 对象
**/
public File(File parent, String child) {
if (child == null) {
throw new NullPointerException();
}
if (parent != null) {
if (parent.path.equals("")) {
this.path = fs.resolve(fs.getDefaultParent(),
fs.normalize(child));
} else {
this.path = fs.resolve(parent.path,
fs.normalize(child));
}
} else {
this.path = fs.normalize(child);
}
this.prefixLength = fs.prefixLength(this.path);
}
/**
* 根据 URI 得到 File 对象
**/
public File(URI uri) {
// Check our many preconditions
if (!uri.isAbsolute())
throw new IllegalArgumentException("URI is not absolute");
if (uri.isOpaque())
throw new IllegalArgumentException("URI is not hierarchical");
String scheme = uri.getScheme();
if ((scheme == null) || !scheme.equalsIgnoreCase("file"))
throw new IllegalArgumentException("URI scheme is not \"file\"");
if (uri.getAuthority() != null)
throw new IllegalArgumentException("URI has an authority component");
if (uri.getFragment() != null)
throw new IllegalArgumentException("URI has a fragment component");
if (uri.getQuery() != null)
throw new IllegalArgumentException("URI has a query component");
String p = uri.getPath();
if (p.equals(""))
throw new IllegalArgumentException("URI path component is empty");
// Okay, now initialize
p = fs.fromURIPath(p);
if (File.separatorChar != '/')
p = p.replace('/', File.separatorChar);
this.path = fs.normalize(p);
this.prefixLength = fs.prefixLength(this.path);
}