Java基础(四)-IO / NIO
在Java程序中,以“流”(stream)的方式对数据进行I/O操作。
一、流分类
大方向分:
类型 | ||
---|---|---|
按数据流方向不同分 | 输入流 | 输出流 |
按功能不同分 | 节点流 | 处理流 |
按处理单位不同分 | 字节流 | 字符流 |
具体分:
注:直接套在数据源上获取数据的管道是节点流,节点流上套的其他的增强管道是处理流。
下面看一张完整的划分:
二、字节流
API:
InputStream 类
方法 | 解释 |
---|---|
public abstract int read() | 一个字节一个字节地读数据。费磁盘 |
public int read(byte b[]) | 以byte 数组位位单位来读取字节数据。相当于拿个桶舀水。 |
public int read(byte b[], int off, int len) | 从第 off 位置读取 len 长度字节的数据放到 byte 数组中,以 -1 来判断是否读取结束。 |
public long skip(long n) | 跳过指定个数的字节。 |
public int available() | 返回可读的字节数量。 |
public void close() | 读取完关闭流,释放资源。 |
public synchronized void mark(int readlimit) | 标记读取位置,下次还可以从这里开始读取(使用前要看当前流是否支持,可以使用 markSupport() 方法判断)。 |
public synchronized void reset() | 重置读取位置为上次 mark 标记的位置。 |
public boolean markSupported() | 判断当前流是否支持标记流,和上面两个方法配套使用。 |
OutputStream 类
方法 | 解释 |
---|---|
public abstract void write(int b) | 写入一个字节,参数是一个 int 类型,对应上面的读方法,int 类型的 32 位,只有低 8 位才写入,高 24 位将舍弃。 |
public void write(byte b[]) | 以byte 数组为单位来写入字节数据。 |
public void write(byte b[], int off, int len) | 将 byte 数组从 off 位置开始,len 长度的字节写入 |
public void flush() | 强制刷新,将缓冲中的数据写入 |
public void close() | 关闭输出流,流被关闭后就不能再输出数据了 |
问题1:为什么要使用flash?
flash把缓冲区的数据强行输出,主要用在IO中,即清空缓冲区数据,一般在读写流(stream)的时候,数据是先被读到了内存中,再把数据写到文件中,当你数据读完的时候不代表你的数据已经写完了,因为还有一部分有可能会留在内存这个缓冲区中。这时候如果你调用了close()方法关闭了读写流,那么这部分数据就会丢失,所以应该在关闭读写流之前先flush()。
问题2:为什么InputStream.read()读取一个byte却返回一个int呢?
从先从源码来看,read方法在java层经过IoBridge和LibCore,最终会JNI到native来实现,而native是c++实现的,返回的是unsigned byte,取值范围为[0~255],在java中没有对应的类型,在java中byte范围是[-128-127],无符号范围是[0-127],所以[128-255]只能由int来接收。
另外,在读取byte数据时,我们知道127+1 = -128(0111 1111 -> 1000 0000,首位是1表示负数,负数转int:先对各位取反,将其转换为十进制数,加上负号,再减去1即-128),那么会有一种情况是连续8个1:1111 1111,按前面的计算公式,最终会转为-1,而-1表示读取结束,显然此时并没有读完,为了避免这种情况,需要将byte提升为int,即向前补0来避免此问题发生。
因此:
FileInputStream的read方法实际是在做类型提升(将byte提升为int),避免java byte无法对应[128-255]范围问题,避免-1问题。
FileOutputStream的write的方法实际在做类型强转(将int强转为byte),只有低 8 位才写入,高 24 位将舍弃。
三、字符流
与字节流相比,字符流是按一个个字符来读写。适合文本文件的读写。
Reader 类
方法 | 解释 |
---|---|
public int read(java.nio.CharBuffer target) | 读取字节到字符缓存中 |
public int read() | 读取单个字符 |
public int read(char cbuf[]) | 读取字符到指定的 char 数组中 |
abstract public int read(char cbuf[], int off, int len) | 从 off 位置读取 len 长度的字符到 char 数组中 |
public long skip(long n) | 跳过指定长度的字符数量 |
public boolean ready() | 和上面的 available() 方法类似 |
public boolean markSupported() | 判断当前流是否支持标记流 |
public void mark(int readAheadLimit) | 标记读取位置,下次还可以从这里开始读取,使用前要看当前流是否支持,可以使用 markSupport() 方法判断 |
public void reset() | 重置读取位置为上次 mark 标记的位置 |
abstract public void close() | 关闭流释放相关资源 |
Writer 类
方法 | 解释 |
---|---|
public void write(int c) | 写入一个字符 |
public void write(char cbuf[]) | 以字符数组为单位写入 |
abstract public void write(char cbuf[], int off, int len) | 从字符数组的 off 位置写入 len 数量的字符 |
public void write(String str) | 写入一个字符串 |
public void write(String str, int off, int len) | 从字符串的 off 位置写入 len 数量的字符 |
public Writer append(CharSequence csq) | 追加写入一个字符序列 |
public Writer append(CharSequence csq, int start, int end) | 追加写入一个字符序列的一部分,从 start 位置开始,end 位置结束 |
public Writer append(char c) | 追加写入一个 16 位的字符 |
abstract public void flush() | 强制刷新,将缓冲中的数据写入 |
abstract public void close() | 关闭输出流,流被关闭后就不能再输出数据了 |
问题:一个字符是几个字节?
这个不同的编码格式有差别,索性就打印一下看:
汉字:
try {
String str = "你";
System.out.println("UTF-16:"+str.getBytes("UTF-16").length);
System.out.println("UTF-8:"+str.getBytes("UTF-8").length);
System.out.println("GBK:"+str.getBytes("GBK").length);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
结果:
UTF-16:4
UTF-8:3
GBK:2
英文字母:
try {
String str = "A";
System.out.println("UTF-16:"+str.getBytes("UTF-16").length);
System.out.println("UTF-8:"+str.getBytes("UTF-8").length);
System.out.println("GBK:"+str.getBytes("GBK").length);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
结果:
UTF-16:4
UTF-8:1
GBK:1
四、处理流
简单总结几个常用的处理流:
字节:
-
字节流转换为字符流:
InputStreamReader & OutputStreamWriter -
字节管道加buff:
BufferedInputStream & BufferedOutputStream
字符:
-
字符管道加buff:
BufferedRead & BufferedWriter -
PrintWriter
字符包装写PrintWriter 与 BufferedWriter区别:- PrintWriter 可以接受更多类型的参数,在循环读的过程中,方便拓展写的内容,而BufferedWriter的write方法只能接受字符、字符数组和字符串;
- PrintWriter的println方法自动添加换行,BufferedWriter需要显示调用newLine方法;
- PrintWriter的方法不会抛异常,若关心异常,需要调用checkError方法看是否有异常发生;
- PrintWriter构造方法可指定参数,实现自动刷新缓存(autoflush);
- BufferedWriter可以任意设定缓冲大小
个人感觉,PrintWriter比BufferedWriter更灵活,推荐使用PrintWriter。
五、代码案例
/**
* 字节流文件复制
* @param oriFile
* @param destFile
*/
public static void byteCopy(File oriFile, File destFile) {
if(oriFile == null || !oriFile.exists()){
return;
}
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(oriFile);
fos = new FileOutputStream(destFile, true);//第二个参数:是否追加写。
byte[] bytes = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
fos.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 字符流文件复制
* @param oriFile
* @param destFile
*/
public static void charCopy(File oriFile, File destFile) {
if(oriFile == null || !oriFile.exists()){
return;
}
FileReader fr = null;
FileWriter fw = null;
try {
fr = new FileReader(oriFile);
fw = new FileWriter(destFile, true);
char[] chars = new char[1024];
int len;
while ((len = fr.read(chars)) != -1) {
fw.write(chars, 0, len);
System.out.println(String.valueOf(chars));
fw.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fw != null) {
fw.close();
}
if (fr != null) {
fr.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 字节流Buffered复制
* @param oriFile
* @param destFile
*/
public static void bufferedByteCopy(File oriFile, File destFile) {
if(oriFile == null || !oriFile.exists()){
return;
}
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(new FileInputStream(oriFile));
bos = new BufferedOutputStream(new FileOutputStream(destFile));
int len;
while ((len = bis.read()) != -1) {
bos.write(len);
System.out.print((char) len);
bos.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (bis != null) {
bis.close();
}
if (bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 字符流Buffered复制
* @param oriFile
* @param destFile
*/
public static void bufferedCharCopy(File oriFile, File destFile) {
if(oriFile == null || !oriFile.exists()){
return;
}
BufferedReader br = null;
BufferedWriter bw = null;
try {
br = new BufferedReader(new FileReader(oriFile));
bw = new BufferedWriter(new FileWriter(destFile));
//int len = 0;
// while ((len = br.read()) != -1) {
// bw.write(len);
// System.out.print((char) len);
// }
//有更高级的读法:
String buffer;
while ((buffer = br.readLine()) != null) { //一行行读,判断从-1变为null了
bw.write(buffer);
System.out.print(buffer);
bw.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
if (bw != null) {
bw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 字符流换printWrite实现复制
* @param oriFile
* @param destFile
*/
public static void printWriteCharCopy(File oriFile, File destFile) {
if(oriFile == null || !oriFile.exists()){
return;
}
BufferedReader br = null;
PrintWriter pw = null;
try {
br = new BufferedReader(new FileReader(oriFile));
pw = new PrintWriter(new FileWriter(destFile));
String buffer;
while ((buffer = br.readLine()) != null) {
pw.print(buffer);
System.out.print(buffer);
pw.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
if (pw != null) {
pw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 写了一个相对比较万能的文件复制方法
* @param oriFile
* @param destFile
* @param byByte
* @param isAppend
*/
public static void fileCopy(File oriFile, File destFile, boolean byByte, boolean isAppend) {
if(oriFile == null || !oriFile.exists()){
return;
}
try {
FileInputStream fis = new FileInputStream(oriFile);
FileOutputStream fos = new FileOutputStream(destFile, isAppend);//isAppend 追加
if (byByte) {
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
bos.flush();
}
bos.close();
bis.close();
} else {
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
//PrintWriter 构造方法第二个参数:autoFlash
PrintWriter pw = new PrintWriter(new OutputStreamWriter(fos), true);
String buffer;
while ((buffer = br.readLine()) != null) {
pw.println(buffer);
}
pw.close();
br.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
六、byte[] 、char[] 、String相互转换
顺便总结下byte[] 、char[] 、String相互转换的API。
/**
* String 转 char[]
*/
public static char[] stringToCharArray(String str) {
return str.toCharArray();
}
/**
* char[] 转 String
*/
public static String charArrayToString(char[] chars) {
//return String.valueOf(chars);
return new String(chars);
}
/**
* String 转 byte[]
*/
public static byte[] stringToByteArray(String str) throws Exception {
return str.getBytes("UTF-8");
}
/**
* byte[] 转 String
*/
public static String byteArrayToString(byte[] bytes) {
return new String(bytes);
}
/**
* byte[] 转 char[]
*/
public static char[] byteArrayToCharArray(byte[] bytes) {
Charset cs = Charset.forName("UTF-8");
ByteBuffer bb = ByteBuffer.allocate(bytes.length);
bb.put(bytes);
bb.flip();
CharBuffer cb = cs.decode(bb);
return cb.array();
}
/**
* char[] 转 byte[]
*/
public static byte[] charArrayToByteArray(char[] chars) {
Charset cs = Charset.forName("UTF-8");
CharBuffer cb = CharBuffer.allocate(chars.length);
cb.put(chars);
cb.flip();
ByteBuffer bb = cs.encode(cb);
return bb.array();
}
七、NIO
JDK 1.4后,Java提供了一个全新的IO API,即 Java New IO,特点是面向缓冲区,提供多路非阻塞式的IO操作。
核心组件:
- 通道(Channel)
- 缓冲区(Buffer)
- 选择器(Selectors)
详细介绍:
实例:实现文件复制
// 设置输入源 & 输出地 = 文件
String infile = “XXX";
String outfile = “XXX";
// 1. 获取数据源 和 目标传输地的输入输出流(此处以数据源 = 文件为例)
FileInputStream fin = new FileInputStream(infile);
FileOutputStream fout = new FileOutputStream(outfile);
// 2. 获取数据源的输入输出通道
FileChannel fcin = fin.getChannel();
FileChannel fcout = fout.getChannel();
// 3. 创建缓冲区对象
ByteBuffer buff = ByteBuffer.allocate(1024);
while (true) {
// 4. 从通道读取数据 & 写入到缓冲区
// 注:若 以读取到该通道数据的末尾,则返回-1
int r = fcin.read(buff);
if (r == -1) {
break;
}
// 5. 传出数据准备:调用flip()方法
buff.flip();
// 6. 从 Buffer 中读取数据 & 传出数据到通道
fcout.write(buff);
// 7. 重置缓冲区
buff.clear();
}
}
与 Java IO区别:
NIO部分内容向Carson_Ho学习:https://www.jianshu.com/p/d30893c4d6bb