java.io 包几乎包含了所有操作输入、输出需要的类。所有这些流类代表了输入源和输出目标。java.io 包中的流支持很多种格式,比如:基本类型、对象、本地化字符集等等。一个流可以理解为一个数据的序列。输入流表示从一个源读取数据,输出流表示向一个目标写数据。Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。
java.io包中最重要的就是5个类和一个接口。5个类分别是File,OutputStream,InputStream,Writer,Reader,一个接口指的是Serializable。
Java的IO主要包括三个部分:
- 流式部分--IO的主体部分;
- 非流式部分--主要包含一些辅助流式的类,如:File类、RandomAccessFile类和FileDescriptor等类;
- 其他类--文件读取部分的与安全相关的类,如SerializablePermission类,以及与本地操作系统相关的文件系统的类,如FileSystem类和Win32FileSystem类等。
Java的IO大概可以分为以下几类:
- 磁盘操作:File
- 字节操作:InputStream和OutputStream
- 字符操作:Reader和Writer
- 对象操作:Serializable
- 网络操作:Socket
- 新的输入输出:NIO
接着我们来看看Java中IO操作的过程:
- 使用File类打开一个文件
- 通过字节流和字符流的子类,指定输出位置
- 进行读写操作
- 关闭输入/输出
File
File类通过抽象的方式代表文件名和目录路径名,这个类主要用于获取文件和目录的属性,文件和目录的创建、查找、删除、重命名等,但不能进行文件的读写操作。File对象代表磁盘中实际存在的文件和目录。
创建一个File对象的方法如下:(摘自Java 8 API)
File(File parent, String child) 从父抽象路径名和子路径名字符串创建新的 |
File(String pathname) 通过将给定的路径名字符串转换为抽象路径名来创建新的 |
File(String parent, String child) 从父路径名字符串和子路径名字符串创建新的 |
File(URI uri) 通过将给定的 file: URI转换为抽象路径名来创建新的 File实例。 |
注意:
1.在各个操作系统中,路径的分隔符是不一样的,例如:Windows中使用反斜杠:"\
",Linux|Unix中使用正斜杠:"/
"。在使用反斜杠时要写成"\\
"的形式,因为反斜杠要进行转义。如果要让Java保持可移植性,应该使用File类的静态常量File.pathSeparator。
2.构建一个File实例并不会在机器上创建一个文件。不管文件是否存在,都可以创建任意文件名的File实例。可以调用File实例上的exists()方法来判断这个文件是否存在。通过后续的学习我们会知道,当把一个输出流绑定到一个不存在的File实例上时,会自动在机器上创建该文件,如果文件已经存在,把输出流绑定到该文件上则会覆盖该文件,但这些都不是在创建File实例时进行的。
创建File对象成功后,我们可以通过Java 8 API中的方法使用这个对象。下面我举几个比较常用的方法:
boolean | delete() 删除由此抽象路径名表示的文件或目录。 |
void | deleteOnExit() 请求在虚拟机终止时删除由此抽象路径名表示的文件或目录。 |
boolean | equals(Object obj) 测试此抽象路径名与给定对象的相等性。 |
boolean | exists() 测试此抽象路径名表示的文件或目录是否存在。 |
String | getName() 返回由此抽象路径名表示的文件或目录的名称。 |
String | getParent() 返回此抽象路径名的父 |
File | getParentFile() 返回此抽象路径名的父,或抽象路径名 |
String | getPath() 将此抽象路径名转换为路径名字符串。 |
boolean | isAbsolute() 测试这个抽象路径名是否是绝对的。 |
boolean | isDirectory() 测试此抽象路径名表示的文件是否为目录。 |
boolean | isFile() 测试此抽象路径名表示的文件是否为普通文件。 |
boolean | mkdir() 创建由此抽象路径名命名的目录。 |
boolean | mkdirs() 创建由此抽象路径名命名的目录,包括任何必需但不存在的父目录。 |
从上边的方法我们也可以早知道File类中有两个方法来创建目录:
- mkdir():该方法创建一个文件夹,返回一个boolean类型。若返回false则表明该对象指定的路径已存在(即目录已存在),或者路径中有某个父文件夹不存在,该文件夹无法被创建。
- mkdirs():该方法创建一个文件夹和它的所有父文件夹。
删除文件和目录我们则可以通过delete()方法来实现,调用delete()方法时,即使目录不为空,也会执行删除操作。如果目录下存在文件,则需要先删除文件再重新删除目录。
RandomAccessFile
RandomAccessFile不同于File,他是一个独立的类,直接继承自Object类,它提供了对文件内容的访问,可以读写文件且支持随机访问文件的任意位置。RandomAccessFile读写使用到文件指针,它的初始位置为0,可以使用getFilePointer()方法获取文件指针的位置。详情请查找Java 8 API。
摘自https://www.jianshu.com/p/751bd3cbe5a7
流
流是Java IO一个很重要的概念,在Java程序中所有数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来时,就要使用输出流完成。流中保存的实际上都是字节文件,流设计的领域很广:标准输入输出,文件的操作,网络上的数据流,字符串流等等。
流具有方向性,输入流还是输出流则是一个相对的概念,一般以程序为参考,如果数据的流向是程序到设备,则是输出流,又设备到程序则是输入流。
我们来补充一个Java IO流的类层次图:
(摘自https://www.jianshu.com/p/751bd3cbe5a7)
首先要理解编码和解码的概念:
- 编码:编码就是把字符转换为字节
- 解码:解码就是将字节重新组合成字符
在编码和解码的过程中,一定要使用相同的编码方式,一旦使用了不同的编码方式,就会出现乱码。编码方式主要有以下三种
- GBK编码中,中文字符占2个字节,英文字符占1个字节
- UTF-8编码中,中文字符占3个字节,英文字符占1个字节
- UTF-16be编码中,中文字符和英文字符各占2个字节。
UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
Java 使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
了解了编码和解码的概念,我们就可以开始来了解字符流和字节流。
字符流构建在字节流的基础上,字符流专门用于读取文本文件,字节流则适用于所有文件,但在文本文件上方便性不及字符流。
字符流
Java中的字符流处理的最基本的单元是2字节的Unicode码元(char),它通常用来处理文本数据,如字符、字符数组或者字符串等。所谓Unicode码元,也就是一个Unicode代码单元,范围时0x0000~0xFFFF。在以上范围内的每个数字都与一个字符相对应。Java中的String类型默认就把字符以Unicode规则编码而后存储在内存中。然而与存储在内存中不同,存储在磁盘上的数据通常有着各种各样的编码方式,使用不同的编码方式,相同的字符会有不同的二进制表示。因此字符流实际上是这样工作的:
- 输出字符流(Writer):把写入的字符序列(实际上是Unicode码元序列)转为指定编码方式下的字节序列,然后再写入到文件。
- 输入字符流(Reader):把要读取的字节序列按指定编码方式解码为相应字符序列(实际上是Unicode码元序列)从而可以存在内存中。
也就是说,所有文件再硬盘火灾传输时都是以字节的方式进行的,包括图片等都是按字节的方式存储的,而字符是只有在内存中才会形成。
字符输入流Reader
从上边的类层次图我们可以了解到:
- Reader是所有的输入字符流的父类,它是一个抽象类。
- CharArrayReader、StringArrayReader是两种基本的介质流,它们分别从Char数组、String对象中读取数据。Pipe的Reader是从与其它线程公用的管道中读取数据。
- BufferedReader很明显就是一个装饰器,它和其子类负责装饰其它Reader对象。
- FilterReader是所有自定义具体装饰流的父类,其子类PushbackReader对Reader对象进行装饰,会增加一个行号。
- InputStreamReader是一个连接字符流和字节流的桥梁,它将字节流转变为字符流。FileReader可以说是第一个达到此功能、常用的工具类,在其源代码中明显使用了将FileInputStream转变为Reader的方法。
Reader有三个主要的读取方法:
- public int read() throws IOException; 读取一个字符,返回值为读取的字符
- public int read(char cbuf[]) throws IOException; 读取一系列字符到数组cbuf[]中,返回值为实际读取的字符的数量
- public abstract int read(char cbuf[],int off,int len) throws IOException; 读取len个字符,从数组cbuf[]的下标off处开始存放,返回值为实际读取的字符数量,该方法必须由子类实现
字符输出流Writer
从上边的类层次图我们可以了解到:
- Writer是所有的输出字符流的父类,它是一个抽象类。
- CharArrayWriter、StringWriter是两种基本的介质流,他们分别向Char数组、String对象中写入数据。
- BufferedWriter是一个装饰器为Writer提供缓冲功能。
- PrintWriter和PritStream极其类似,功能与使用也非常相似。
- OutputStreamWriter是OutputStream到Writer转换的桥梁,它的子类FileWriter其实就是一个实现此功能的具体类。
Writer主要的写入方法有以下五种:
- public void write(int c) throws IOException; 将整型值c的低16位写入输出流
- public void write(char cbuf[]) throws IOException; 将字符数组cbuf[]写入输出流
- public abstract void write(char cbuf[],int off,int len) throws IOException; 将字符数组cbuf[]中的从索引为off的位置处开始的len个字符写入输出流
- public void write(String str) throws IOException; 将字符串str中的字符写入输出流
- public void write(String str,int off,int len) throws IOException; 将字符串str 中从索引off开始处的len个字符写入输出流
字节流
Java中的字节流处理的最基本单位是单个字节(byte),它通常用来处理二进制数据。如果要得到字节对应的字符需要强制类型转换。
输入字节流InputStream
从上边的类层次图我们可以了解到:
- InputStream是所有输入字节流的父类,它是一个抽象类。
- ByteArrayInputStream、StringBufferInputStream(上图的StreamBufferInputStream)、FileInputStream是三种基本的介质流,它们分别从Byte数组、StringBuffer和本地文件中读取数据。
- PipedInputStream是从与其它线程公用的管道中读取数据。
- ObjectInputStream和所有FilterInputStream的子类都是装饰流(装饰器模式的主角)。
InputStream有三个基本的读取方法
abstract int read():读取一个字节数据,并返回读到的数据,如果返回-1,表示读到了输入流的末尾。
int read(byte[] b):将数据读入一个字节数组,同时返回实际读取的字节数。如果返回-1,则表示读到了输入流的末尾。
int read(byte[] b,int off,int len):将数据读入一个字节数组,同时返回实际读取的字节数。如果返回-1,表示读到了输入流的末尾。off指定在数组b中存放数据的其实偏移位置,len指定读取的最大字节数。
通过这三个方法我们也可以知道判断流结束的标志就是read的返回值为-1或者readLine的返回值为null。
还有一些常用的方法:
long skip(long?n):在输入流中跳过n个字节,并返回实际跳过的字节数。
int available() :返回在不发生阻塞的情况下,可读取的字节数。
void close() :关闭输入流,释放和这个流相关的系统资源。
voidmark(int?readlimit) :在输入流的当前位置放置一个标记,如果读取的字节数多于readlimit设置的值,则流忽略这个标记。
void reset() :返回到上一个标记。
booleanmarkSupported() :测试当前流是否支持mark和reset方法。如果支持,返回true,否则返回false
输出字节流OutputStream
从上边的类层次图我们可以了解到:
- OutputStream是所有输出字节流的父类,它是一个抽象类。
- ByteArrayOutputStream、FileOutputStream是两种基本的介质流,他们分别向Byte数组和本地文件写入数据。PipedOutputStream是向与其它线程共用的管道中写入数据。
- ObjectOutputStream和所有FilterOutputStream的子类都是装饰流。
OutputStream有三个基本的写入方法:
abstract void write(int?b):往输出流中写入一个字节。
void write(byte[]?b) :往输出流中写入数组b中的所有字节。
void write(byte[] b, int off, int len) :往输出流中写入数组b中从偏移量off开始的len个字节的数据。
OutputStream有两个很重要的方法:
void flush() :刷新输出流,强制缓冲区中的输出字节被写出。
void close() :关闭输出流,释放和这个流相关的系统资源。
要特别注意,flush()方法用来刷新缓冲区的,刷新后可以再次写出(字节缓冲流内置缓冲区,如果没有读取出来,可以使用flush()刷新出来)。而close()方法用来关闭流释放资源的,如果是带缓冲区的流对象的close()方法,会在关闭流之前刷新缓冲区,关闭流之后不能再次写出。
字节流和字符流的区别
字节流与字符流的使用非常相似,两者除了操作代码上的不同之处外是否有其他的不同?
实际上字节流在操作时本身不会用到缓冲区(内存),是文件本身直接操作,而字符流在操作时使用了缓冲区,通过缓冲区再操作文件。
什么是缓冲区呢?我们可以把缓冲区理解为一段特殊的内存,某些情况下,如果一个程序频繁地操作一个资源(如文件或者数据库,则性能会很低,此时为了提升性能,就可以将一部分数据暂时读入到内存的一块区域之中,以后直接从此区域中读取数据即可,因为读取内存速度会比较快,这样可以提升程序的性能。
在字符流的操作中,所有的字符都是在内存中形成的,在输出前会将所有的内容暂时保存在内存之中,所以使用了缓冲区暂存数据。上文也讲述过,如果想在不关闭时将字符流的内容全部输出,可以使用Writer类中的flush()方法来完成。
因此字节流与字符流除了读写对象不同之外(字节流为byte对象,字符流为char对象),最大的区别就在于字节流没有缓冲区而字符流有缓冲区。
本文主要讲了File、InputStream、OutputStream、Writer和Reader这五个IO中最重要的类,其派生类有兴趣的可以查看JavaAPI进行了解。由于目前对NIO的相关知识没有足够的了解,后期完善知识点后会补充NIO的相关内容。
参考: