摘要
输入输出(I/O)是程序编写时不可避免的重要基础操作。但是各种计算语言对于I/O操作都有自己的优劣。本文主要介绍了Java的I/O流,作为自己的阶段学习理解。
简介
本文主要介绍Java I/O流体系结构,分为如下几个部分。
- 第一部分 流以及I/O流的相关概念
- 第二部分 Java的I/O流概述
- 第三部分 磁盘操作I/O
- 第四部分 字节流的理解
- 第五部分 字符流的理解
- 第六部分 其他I/O类的理解
- 参考文献与文章
(一) 流概念与I/O流
输入输出与流的概念
流是一种抽象概念,它代表了数据的无结构化传递。按照流的方式进行输入输出,数据被当成无结构的字节或者字符序列。输入和输出操作的流称之为IO流。
输入输出(I/O)指的是计算机同设备之间的数据传递。常见的输入输出设备有文件,键盘,打印机和屏幕等。数据可以按照记录的形式(或称数据块)的方式传递,也可以按照流的方式传递。所谓记录,指的是有用内部机构的数据块。记录内部除了需要处理的实际数据以外,还可能包含附加信息,这些附加信息通常包含对本记录的描述。
数据的表示形式——编码
IO操作过程中,任何被传递的数据,在经过I/O类库处理前后是不同的。我们可以将其分为两种:内部表示和外部表示。
数据的内部表示便于程序进行数据处理。典型的内部表示二进制,浮点的IEEE表示,字符的ASCII或者Unicode表示。数据的外部表示则是由外部设备决定的。如果外部数据表示是可读的字符序列,则称为文本IO;否则为二进制表示。表示IO主要支持文本IO,而非二进制IO。
虽然IO流是以流的方式进行数据传递,但这并不表明传递的数据不能有任何结构,而是指IO流的概念是以流的方式进行输入输出,所传递数据的内部结构隐藏在对流数据的解释中。
注 : 这一部分的内容,主要来自百度百科,链接为IO流
(二)Java的I/O流体系
Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。下图,是自己理解的Java I/O相关的内容:
![]()
其中特殊的流:标准输入流(System.in),标准输出流(System.out),标准错误流(System.err) 都可以与其他相应的输入输出流进行对应套接。
同时,提及I/O与之相关的不得不提及的便是对象的序列化与反序列化操作,该部分在另外一篇文章中进行了较为详细的讨论。
## 字节流与字符流
二者的主要区别在于处理的数据类型与处理数据块的大小。
- 读写单位不同: 字节流以字节(8bit)为单位进行读取;字符流以字符为单位,根据编码表的不同字符包含的字节数量不同,在Java中字节流单位是byte,字符流的读取单位是char.
- 处理对象不同: 字节流能处理所有的数据类型;字符流仅仅能够处理字符类型的数据。例如,图片,视频等字节流可以处理,但是字符流不能。
(三)磁盘操作I/O
文本与文本文件
java的文本(char)指的是16位无符号整数,是字符的unicode编码(双字节编码)。文件指的是字节序列, 文本文件是文本(char)序列按照某种编码方案(utf-8, utf-16be,gbk)序列化为byte的存储结果。
相关操作,如下代码所示:
public class FileTest {
public static void main(String[] args) throws Exception{
/**
* /F:/Source/eclipse/java-base/target/
* /F:/Source/eclipse/java-base/target/
* 获取当前项目根目录的两种方式, 注意在静态方法中不能使用this关键字代替类名, 项目编译输出的根目录
*/
// String str = FileTest.class.getClassLoader().getResource("").getPath();
// System.out.println(str);
// String str1 = FileTest.class.getClassLoader().getResource(".").getPath();
// System.out.println(str1);
String path = FileTest.class.getClassLoader().getResource(".").getPath() + "res/";
File file = new File(path);
if(!file.exists()) file.mkdirs();
if(file.isDirectory()){
System.out.println("This is a directory");
}else if(file.isFile()){
System.out.println("This is a file");
}else{
System.out.println("What ??");
}
File file1 = new File(file, "new.txt");
if(!file1.exists()){
boolean fileExist = file1.createNewFile();
System.out.println("Not exist, create = " + fileExist);
}else{
System.out.println("Exist, delete = " + file1.delete());
}
System.out.println(file1); // file对象,也就是toString() , 绝对路径,
System.out.println(file1.getCanonicalPath()); // 打印file的绝对路径
System.out.println(file1.getAbsolutePath()); // 打印file的绝对路径
//这里需要说明的是Java项目和Java Web项目中以上两个路径的打印结果不同
System.out.println(File.separator); //打印file路径使用的分割符
System.out.println("File 测试");
/***************************************************************
* File常用API 以及 自己给出简单的常用封装 测试
**************************************************************/
listDirectory(file);
}
/***************************************************************
* File常用API 以及 自己给出简单的常用封装
**************************************************************/
/**
* 遍历所有的文件和目录
* @param dir
* @throws IllegalArgumentException if {@code dir} does not exist
* @throws IllegalArgumentException if {@code dir}
*/
public static void listDirectory(File dir) throws IllegalArgumentException{
if(!dir.exists()){
throw new IllegalArgumentException("目录:" + dir + "不存在");
}
if(!dir.isDirectory()){
throw new IllegalArgumentException(dir + "不是一个目录");
}
// for(String s : dir.list()){ // list() // 返回文件路径的字符串数组
// System.out.println(dir + s);
// //System.out.println(s);
// }
for(File file : dir.listFiles()){ // listFiles() 返回该目录下的所有文件对象(文件和文件夹抽象成的文件对象)
if(file.isDirectory()){
listDirectory(file); // 递归调用
}else{
System.out.println(file);
}
}
}
}
有关磁盘文件的读写曹祖,在下面的代码中有所体现,这里就不再重复。
(四) 字节流
在字节流的类继承体系中,Inpustream和OutputStream是所有字节I/O流类的父类,它们是字节流的基本抽象类,同时也是最重要的两个类,在这里将着重讨论。InputStream(OutputStream)定义了对于数据的基本读(写)操作,后续继承该类的具有特定功能的读(写)操作都是对与该类中基本操作的组合封装。其中ByteArrayInputStream、StringBufferInputStream、FileInputStream是三种基本的介质流,它们分别从Byte数组、串缓冲区(StringBuffer)、和本地文件中读取数据。PipedInputStream从与其它线程共用的管道中读取数据;ObjectInputStream主要用于数据的序列化与反序列化操作,它和所有FilterInputStream的子类一样,都是装饰流(通过装饰器模式实现),也就是说FileInputStream类可以通过一个String路径名创建一个对象,例如,FileInputStream(String name),而DataInputStream必须装饰一个类才能返回一个对象(也就是传入一个相应的对象),例如,DataInputStream(InputStream in)。与字节输入流相对应的,ByteArrayOutputStream、FileOutputStream是两种基本的介质流,它们分别向Byte数组和本地文件中写入数据。PipedOutputStream 是向与其它线程共用的管道中写入数据,ObjectOutputStream 和所有FilterOutputStream的子类都是装饰流。
下面,我们将通过源码以及代码示例着重分析InpuStream与OutputStream,从而分析字节流的基本操作。
首先, 分析InpuStream的源代码,为了凸显关注的重点,这里忽略了一些代码与注释。
public abstract class InputStream implements Closeable {
/**
* Reads the next byte of data from the input stream. The value byte is
* returned as an <code>int</code> in the range <code>0</code> to
* <code>255</code>. If no byte is available because the end of the stream
* has been reached, the value <code>-1</code> is returned. This method
* blocks until input data is available, the end of the stream is detected,
* or an exception is thrown.
*
* <p> A subclass must provide an implementation of this method.
*/
public abstract int read() throws IOException;
/**
* Reads some number of bytes from the input stream and stores them into
* the buffer array <code>b</code>. The number of bytes actually read is
* returned as an integer. This method blocks until input data is
* available, end of file is detected, or an exception is thrown.
*
*/
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
/**
* Reads up to <code>len</code> bytes of data from the input stream into
* an array of bytes. An attempt is made to read as many as
* <code>len</code> bytes, but a smaller number may be read.
* The number of bytes actually read is returned as an integer.
*
*/
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();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
}
注重关注三个方法抽象方法:
- int read() : 读取一个字节
- int read(byte[] b[]) : 读取特定个字节,放入缓存数组中
- int read(byte[], int off, int len) : 读取特定长度len个字节,从位置off开始放入缓存数组中
第一个空参read方法为抽象方法,这是考虑到不同的子类默认情况下可能具有不同的实现。例如,ObjectInputStream的默认空参read方法是调用其内部私有读方法实现,而FileInputStream则是调用具有naive关键字修饰的本地read方法实现,这中实现方式是出于性能和子类的特点考虑的。与空参read方法相对,其他两个带参read方法则是所有字节输入流共有的且功能和性能一致,因此,字节输入流的顶层父类InputStream负责实现了这两个方法。同样,与之对应的在字节输出流的类继承体系中,顶层父类OutputStream的有一个抽象的write(int b)方法(注意这里实现仅仅写入一个字节),以及两个不同签名的带参写方法write(byte[] b)和write(byte[] b, int off, int len)。
- void write(int b) : 将一个字节写入输出流
- void write(byte b[]) : 将字节数组中的所有数据写入输出流
- void write(byte b[], int off, int len) : 将在字节数组中从off位置开始且长度为len的数据写入输出流
以下是相关的代码示例,通过输入流进行数据读写:
public class InputStreamTest {
/**
* 读取指定文件内容,按照16进制输出到控制台
* 并且每输出10个byte换行
* @param fileName
* 单字节读取不适合大文件,大文件效率很低
*/
public static void printHex(String fileName)throws IOException{
//把文件作为字节流进行读操作
InputStream in = new FileInputStream(fileName);
int b ;
int i = 1;
while((b = in.read())!=-1){
if(b <= 0xf){
//单位数前面补0
System.out.print("0");
}
System.out.print(Integer.toHexString(b)+" ");
if(i++%10==0){
System.out.println();
}
}
in.close();
}
/**
* 批量读取,对大文件而言效率高,也是我们最常用的读文件的方式
* @param fileName
* @throws IOException
*/
public static void printHexByByteArray(String fileName)throws IOException{
InputStream in = new FileInputStream(fileName);
byte[] buf = new byte[8 * 1024];
/*从in中批量读取字节,放入到buf这个字节数组中,
* 从第0个位置开始放,最多放buf.length个
* 返回的是读到的字节的个数
*/
/*int bytes = in.read(buf,0,buf.length);//一次性读完,说明字节数组足够大
int j = 1;
for(int i = 0; i < bytes;i++){
System.out.print(Integer.toHexString(buf[i] & 0xff)+" ");
if(j++%10==0){
System.out.println();
}
}*/
int bytes = 0;
int j = 1;
while((bytes = in.read(buf,0,buf.length))!=-1){
for(int i = 0 ; i < bytes;i++){
System.out.print(Integer.toHexString(buf[i] & 0xff)+" ");
if(j++%10==0){
System.out.println();
}
}
}
in.close();
}
/**
* 文件拷贝,字节批量读取
* @param srcFile
* @param destFile
* @throws IOException
*/
public static void copyFile(File srcFile,File destFile)throws IOException{
if(!srcFile.exists()){
throw new IllegalArgumentException("文件:"+srcFile+"不存在");
}
if(!srcFile.isFile()){
throw new IllegalArgumentException(srcFile+"不是文件");
}
InputStream in = new FileInputStream(srcFile);
OutputStream out = new FileOutputStream(destFile);
byte[] buf = new byte[8*1024];
int b ;
while((b = in.read(buf,0,buf.length))!=-1){
out.write(buf,0,b);
out.flush();//最好加上
}
in.close();
out.close();
}
/**
* 进行文件的拷贝,利用带缓冲的字节流
* @param srcFile
* @param destFile
* @throws IOException
*/
public static void copyFileByBuffer(File srcFile,File destFile)throws IOException{
if(!srcFile.exists()){
throw new IllegalArgumentException("文件:"+srcFile+"不存在");
}
if(!srcFile.isFile()){
throw new IllegalArgumentException(srcFile+"不是文件");
}
InputStream bis = new BufferedInputStream(
new FileInputStream(srcFile)); // IO流的套接
OutputStream bos = new BufferedOutputStream(
new FileOutputStream(destFile));
int c ;
while((c = bis.read())!=-1){
bos.write(c);
bos.flush();//刷新缓冲区
}
bis.close();
bos.close();
}
/**
* 单字节,不带缓冲进行文件拷贝
* @param srcFile
* @param destFile
* @throws IOException
*/
public static void copyFileByByte(File srcFile,File destFile)throws IOException{
if(!srcFile.exists()){
throw new IllegalArgumentException("文件:"+srcFile+"不存在");
}
if(!srcFile.isFile()){
throw new IllegalArgumentException(srcFile+"不是文件");
}
InputStream in = new FileInputStream(srcFile);
OutputStream out = new FileOutputStream(destFile);
int c ;
while((c = in.read())!=-1){
out.write(c);
out.flush();
}
in.close();
out.close();
}
public static void main(String[] args) throws Exception{
String filePath = "res/ost.dat";
String distPath = "res/dist.dat";
printHex(filePath);
printHexByByteArray(filePath);
copyFile(new File(filePath), new File(distPath));
copyFileByBuffer(new File(filePath), new File(distPath));
copyFileByByte(new File(filePath), new File(distPath));
}
}
这其中体现了Java I/O的套接思想,也就是采用了装饰模式。
(五) 字符流的理解
与字节流类似,字符流类继承体系中同样有两个顶层父类: Reader和Writer。与字节流以字节为单位读取(写入),字符流是以文本为单位的,也就是字符为单位(Unicode编码)。抽象类Reader(Writer)定义了输入流(输出流)类继承体系中的基本读取(写入)方法。
Reader :
- int read(): 读取单个字符
- int read(char[] cBuf) : 读取特定长度的字符串放入缓存数组
- int read(char[] cBuf, int off, int len) : 读取特定长度len的字符串,从位置off开始放入缓存数组
Writer :
- void write(char[] cbuf) 将字符数组写入输出流
- void write(int c) 将单个字符写入输出流
- void write(String str) 将字符串写入输出流
- void write(String str, int off, int len) 从位置off开始,将str长度为len的字串写入输出流
其中,第一个方法为抽象方法,其原因同InputStream的抽象方法一样。同样,后两者的也进行了相应的实现。
如下代码,展示了字符流的相关操作:
public class IsrAndOswDemo {
public static void main(String[] args)throws IOException {
FileInputStream in = new FileInputStream("res/osd.dat");
InputStreamReader isr = new InputStreamReader(in,"utf-8");//默认项目的编码,操作的时候,要写文件本身的编码格式
FileOutputStream out = new FileOutputStream("res/dist.dat");
OutputStreamWriter osw = new OutputStreamWriter(out,"utf-8");
/*int c ;
while((c = isr.read())!=-1){
System.out.print((char)c);
}*/
char[] buffer = new char[8*1024];
int c;
/*批量读取,放入buffer这个字符数组,从第0个位置开始放置,最多放buffer.length个
返回的是读到的字符的个数
*/
while(( c = isr.read(buffer,0,buffer.length))!=-1){
String s = new String(buffer,0,c);
System.out.print(s);
osw.write(buffer,0,c);
osw.flush();
}
isr.close();
osw.close();
}
}
(六) 其他I/O流的理解
Java的I/O操作类在包java.io下,大概拥有近80个类。理解部分仅仅是字节流和字符流的部分。其中管道流仅仅是提及并没有深入探讨,因为把自己这部分并没有深入了解。同时,对于套接字(Socket),网络编程接口I/O,也没有提及,这两个部分将会单独成文,深度理解。
总结
本文仅仅学习了几年Java后,才敢写出自己对于Java I/O的简单理解,但是仍旧不全面,不深刻,同样也会有错误出现,后续会陆续出现补文。
更加详细的有关性能调优的资料可以看官网,以及下列文章。