一、概述
Java
中与IO
相关的类有很多,都集中在java.io
中,都是以流的形式操作的,流是有一定的顺序,像一个管道一样,它的本质是传输数据。根据数据类型的不同可以分为字节流和字符流,根据流向的不同可以分为输入流和输出流。
- 字符流:因为数据有不同的编码,可以对字符进行不同的操作,其本质还是基于字节流,然后再查询相应的码表。一般用于处理纯文本数据。
- 字节流:可以处理所有类型数据,二进制文件(图片,音频等)。
- 输入流:读入数据,也就是数据的源,如键盘,磁盘文件,网络文件,内存等。字符流对应的基本输入流为
Reader
,字节流对应的基本输入流为InputStream
。 - 输出流:输出数据,数据的目的地,如控制台,磁盘文件,内存等。字符流对应的基本输出流为
Writer
,字节流对应的基本输出流为OutputStream
。
Java 所有IO流的关系如下如:
Java IO流的关系结构 |
二、字符流
字符流的基本输入输出流是Reader
和Writer
,两者有一些常用的方法:
Reader
的常用读取操作:
int read()
读取单个字符。int read(char[] cbuf)
将字符读入数组。abstract int read(char[] cbuf, int off, int len)
将字符读入数组的某一部分。
Writer
的常用写入操作:
abstract void flush()
刷新该流的缓冲。void write(char[] cbuf)
写入字符数组。void write(int c)
写入单个字符。void write(String str)
写入字符串。void write(String str, int off, int len)
写入字符串的某一部分。
在使用Reader
和Writer
进行数据流的读入和写入操作时是不会直接创建Reader
和Writer
对象的,一般都是使用其子类,如FileReader
和FileWriter
,示例代码如下:
import java.io.*;
class ReaderDemo {
public static void main(String[] args) {
FileReader reader = null;
FileWriter writer = null;
try {
writer = new FileWriter("file.txt");
// 向流中写入一个char[],内容为[‘H‘,‘e‘,‘l‘,‘l‘,‘o‘]
writer.write("Hello".toCharArray());
writer.flush(); // 刷新内容到磁盘上
} catch (IOException e) {
// IO异常在这里处理
} finally {
try {
// 在finally中关闭流,并判断是否为null
if(writer != null) {
writer.close();
}
} catch (IOException e) {}
}
try{
reader = new FileReader("file.txt");
char[] buf = new char[1024];
int n = 0;
// 从流中读入一个char[],然会读入的长度,-1表示到达流尾
while((n = reader.read(buf)) != -1) {
// 输出到屏幕上
System.out.println(new String(buf, 0, n));
}
} catch (IOException e) {
// IO异常在这里处理
} finally {
try {
// 在finally中关闭流,并判断是否为null
if(reader != null) {
reader.close();
}
} catch (IOException e) {}
}
}
}
上面代码有些需要注意的地方,如不管是流的创建,打开,写入,读取,刷新,关闭等操作,一般都会抛出IOException
,因为对磁盘进行操作(一般都是)都有可能产生错误,如磁盘满了,文件被占用等等,所以必须对其进行捕获,并处理。流的关闭一般放在try
中的finally
中,原因是为防止出错后无法及时释放资源。在进行数据的写入时,当执行完write
方法后,数据可能不会立即被写入到目的地,这时可以使用flush
功能进行立即完成写入功能,也可以在close
时自定完成内容的写入。
还有一点关于数据的读入过程,如使用int read()
一个字节一个字节的读入,到达流尾时,返回-1,这种方式,需要注意其返回int
需要强制转换一下,如int data = reader.read();
,那么读到的数据便是(char)data
;如果使用int read(char[])
方式读取数据,那么就如上面代码示例的读取方式操作即可。
BufferedWriter
和BufferedReader
使用简介
BufferedInputStream
和BufferedOutputStream
是为提高读取和写入的效率而出现的,当我们读取和写入数据时,可以现在内存中建立一个缓冲区,加快数据流的读写,其使用非常简单,将Reader或者Writer对象做为参数传递给其构造函数即可。功能和类似,示例代码如下:
import java.io.*;
class Demo {
public static void main(String[] args) {
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new FileWriter("file.txt"));
// 向流中写入一个char[],内容为[‘H‘,‘e‘,‘l‘,‘l‘,‘o‘]
bw.write("Hello".toCharArray());
bw.newLine();
bw.flush();
} catch (IOException e) {
// IO异常在这里处理
} finally {
try {
// 在finally中关闭流,并判断是否为null
if(bw != null) {
bw.close();
}
} catch (IOException e) {}
}
}
}
其中newLine()
是输出一个换行,这种操作是跨平台的,即在Windows下输出\r\n
,而在Linux下输出\n
。
LineNumberReader
使用简介
LineNumberReader
单从类名上看,大致也知道了类的功能,便是可以输出文本文件的行号。这里行号是从1开始的,也可以使用setLineNumber(int)
功能为其设置一个偏移值,如设置100,那么行号就会从101开始输出。示例代码如下:
import java.io.*;
class Demo {
public static void main(String[] args) throws IOException {
LineNumberReader reader = new LineNumberReader(new FileReader("file.txt"));
String line = null;
// 如果读到文件末尾则返回null
while((line=reader.readLine()) != null ) {
// 获取行号并输出
System.out.println(reader.getLineNumber()+": " + line);
}
reader.close();
}
}
// 执行结果为
1: Hello
2: haha
3: Hi
4: good meeas
5:
6: sa
三、字节流
字符流操作的是纯文本内容,而字节流则是所有二进制文件都可以操作,如图片,视频,当然文件文件也是可以的。与字符流中读出和写入的类型为char
型相对应,字节流读出和写入的是byte
类型。字节流的两个基本输入输出流为InputStream
和OutputStream
,其功能与字符流的功能类似。对于文件的操作的流是相应的FileInputStream
和FileOutputStream
,下面通过一个图片拷贝功能作为字节流的一个示例代码:
import java.io.*;
class Demo {
public static void main(String[] args) throws IOException {
FileInputStream fin = new FileInputStream("pic.png");
FileOutputStream fout = new FileOutputStream("pic2.png");
// 与Reader不同的是这里使用的是byte类型
byte[] buf = new byte[1024];
int n = 0;
while((n=fin.read(buf)) != -1) {
fout.write(buf, 0, n);
}
// 关闭流
fin.close();
fout.close();
}
}
BufferedInputStream
和BufferedOutputStream
使用简介
为了提高流操作的效率,这里也用相应的缓冲流,到底使用缓冲流与不使用缓冲流在效率上有多大的差别,可以通过比较得出结果。从下面代码的比较结果可以明显的发现,加入缓冲机制会大大提高程序的运行效率,原因大致解释为,未加入缓冲机制,每次读取read()
都会调用系统底层读取磁盘操作,每次读取一个字节,非常耗时;而加入缓冲机制后,系统会一次将很多内容读取到内存,而调用read()
时,只需要从内存中返回数据内容即可,大大减少了系统底层访问磁盘的次数,所以速度会加快很多。代码示例如下:
import java.io.*;
class Demo {
public static void main(String[] args) throws IOException {
// 未加缓冲机制
FileInputStream fin = new FileInputStream("movie.avi");
FileOutputStream fout = new FileOutputStream("movie2.avi");
int data = 0;
long time1 = System.currentTimeMillis();
while((data = fin.read()) != -1) {
fout.write(data);
}
System.out.println("普通:" + (System.currentTimeMillis()-time1) + "毫秒");
fin.close();
fout.close();
// 加上缓冲机制
BufferedInputStream bfin =
new BufferedInputStream(new FileInputStream("movie.avi"));
BufferedOutputStream bfout =
new BufferedOutputStream(new FileOutputStream("movie3.avi"));
long time2 = System.currentTimeMillis();
while((data = bfin.read()) != -1) {
bfout.write(data);
}
System.out.println("缓冲:" + (System.currentTimeMillis()-time2) + "毫秒");
bfin.close();
bfout.close();
}
}
// 执行结果为
普通:50052毫秒
缓冲:61毫秒
读取转换流和写入转换流
这里涉及到的两个流是关于字符流和字节流的转换的操作,去两者名称为:InputStreamReader
和OutputStreamWriter
,InputStreamReader
是将InputStream
(字节流)流作为参数构造出输入字符流。而OutputStreamWriter
则是将OutStream
(字节流)作为参数构造出输出字符流。如此一来,我们读取键盘(System.in
)数据时,便可以使用Reader
的功能,也可以使用Writer
功能将数据输出到控制台(System.out
),示例代码如下:
import java.io.*;
class Demo {
public static void main(String[] args) throws IOException {
// 键盘的最常见写法。
BufferedReader bufr =
new BufferedReader(new InputStreamReader(System.in));
// 使用字符输出方式到控制台
BufferedWriter bufw =
new BufferedWriter(new OutputStreamWriter(System.out));
String line = null;
while((line=bufr.readLine()) != null) {
// 输入end时退出
if("end".equals(line)) break;
bufw.write(line.toUpperCase());
bufw.newLine();
bufw.flush();
}
bufr.close();
bufw.close();
}
}
// 执行结果为
hello
HELLO
Hi
HI
good
GOOD
end
四、流的总结
Java
将IO
单独封装在一个包内,其中不同功能的流数不胜数,使用起来很容易混乱,所以,在使用java
io
流时需要按照一定的规则,如按照流的流向可以分为两大类,即输入流和输出流,输入流来自数据源,输出流有目的地。常见的源和目的地有如下:
- 源:键盘录入,磁盘读入,网络文件,内存。
- 目的地:控制台输出,磁盘文件,内存。
在明确流向后,在看流的数据是否为纯文本类型,若是则优先选用Reader
或Writer
字符流来操作,若是二进制类型,则使用InputStream
和OutputStream
字节流来操作。
提高流的效率,可以使用BufferedXXX
来包装流。对于编码问题,可以使用InputStreamReader
和OutputStreamWriter
,其可以指定编码类型,如utf-8
或者GBK
等。
五、File的使用简介
是将文件和文件夹封装成的对象,方便对文件或者文件夹的属性信息进行操作,也可以作为参数传递。
File常用内容
static String separator
为一种夸平台文件路径分隔符。
文件的创建和删除
File file = new File("file.txt");
file.createNewFile(); // 用于创建一个空文件,成功返回true,若已存在,则不会创建并返回false,
file.delete(); // 删除成功返回true,否则返回false
file.deleteOnExit(); // 退出时删除,一般用于系统退出时删除临时文件
文件的判断
// 通过文件的目录和文件名创建一个File对象
File file = new File("c:" + File.separator + "java", "info.txt");
file.canExecute(); // 是否可被执行
file.exists(); // 是否存在
file.mkdir(); // 创建目录,一层(成功返回true,失败返回false)
file.mkdirs(); // 创建多级目录
file.isDirectory(); // 是否为目录文件,不存在或者不是目录返回false
file.isFile(); // 是否为文件,不存在或不是文件返回false
file.isHidden(); // 是否为隐藏文件
file.isAbsolute(); // 是否为据对路径
file.getName(); // 获取文件名
file.getParh(); // 获取文件路径
file.getParent(); // 父目录
file.lastModified(); // 最后修改时间
文件列表
File[] files = File.listRoots();
列出有效盘符,如(C:\,D:\等)。获取文件夹内的所有文件,以及自定义指定文件,可以使用FileFilter
来过滤文件。若要删除内容不为空的文件夹时,需要递归删除文件夹内的所有内容。示例代码如下:
import java.io.*;
class Demo {
public static void main(String[] args) {
// 列出javas\\day20目录下的java文件
File file = new File("javas\\day20");
File[] files = file.listFiles(new FileFilter() {
public boolean accept(File filename) {
// 以.java结尾的都接受
return filename.getName().endsWith(".java");
}
});
for(File f : files) {
System.out.println(f.getName());
}
// 删除javas内的所有内容
delete(new File("javas"));
}
/**
* 递归删除内容不为空的文件夹或文件
*/
public static void delete(File file) {
if(file.isDirectory()) {
File[] files = file.listFiles();
for(File f : files) {
if(f.isDirectory()) {
delete(f);
} else {
f.delete();
}
}
}
file.delete();
}
}
// 执行结果为
FileDemo.java
FileDemo2.java
FileDemo3.java
JavaFileList.java
PrintStreamDemo.java
PropertiesDemo.java
RemoveDir.java
RunCount.java
SequenceDemo.java
SplitFile.java
六、其他IO内容
1、改变标准输入输入流,可以使用System.setIn(InputStream)
和System.setOut(PrintStream)
来设置标准输入输出流,可以将标准输入设置成从键盘读入,或者将标准输出设置成输出到文件等。
2、异常信息输出到文件,在捕获异常后一般使用e.printStackTrace()
将异常信息输出,但是这样对用户来说是没有任何意义的,所以可以使用e.printStackTrace(new FileOutputStream("log.txt"))
将异常信息输出到文件。
3、Properties
简介,可以使用Properties
存取配置文件,其内部有相关流操作,即load(InputStream inStream)
和store(OutputStream out, String comments)
用于从本地读取配置文件和将配置文件保存至本地。
4、合并流SequenceInputStream
可以将多个流合并成一个整体,有两种构造函数,一是将两个字节流合并成一个流(较为易懂),二是传递一个Enumeration<? extends InputStream> e
类型参数,示例代码如下:
import java.io.*;
import java.util.*;
class SequenceDemo {
public static void main(String[] args) throws IOException {
// Vector 具有获取Enumeration功能
Vector<FileInputStream> v = new Vector<FileInputStream>();
v.add(new FileInputStream("1.txt"));
v.add(new FileInputStream("2.txt"));
v.add(new FileInputStream("3.txt"));
// 获取流枚举
Enumeration<FileInputStream> en = v.elements();
// 构造合并流
SequenceInputStream sin = new SequenceInputStream(en);
// 将内容都写入fout
FileOutputStream fout = new FileOutputStream("123.txt");
byte[] buf = new byte[1024];
int len = 0;
while((len=sis.read(buf))!=-1) {
fos.write(buf,0,len);
}
fout.close();
sin.close();
}
}
5、对象的序列化,实现Serializable
接口,序列化即使将一个对象整个存储到文件中保存,可以实现数据的保存,下次可以将对象从文件中恢复出,只需要此对象实现了Serializable
接口即可,注意的两点是,一是为对象添加static long serialVersionUID = 42L;
随便指定一个数值,这是这个对象的唯一标识,即有相同标识才可以从文件中恢复出对象;二是在不需要序列化的对象前加上transient
关键字。序列化只会序列化堆中的数据,静态数据不会被序列化,transient
关键字的也不会。
6、管道流,PipedInputStream
和PipedOutputStream
,管道流就好像输入输出为一条管道,输入流可以从管道中读取数据,而输出流可以向管道中输入。可以通过两条线程同时操作,当输入流没有内容可以读取时,会柱塞线程等待输入数据。通过connect()
来关联相关流。
7、RandomAccessFile
用与文件的随机访问,自身具备读写方法,有r
、rw
等打开方式,常用方法如skipBytes(int)
跳过一部分内容,seek(index)
将文件指针移到指定位置,适用于文件的断点写入,和多线程分段写入等。
8、ByteArrayStream
是一种基于数组的流,不用close
操作,也不会抛出IOException
,其简单来说就是一个数组操作工具。和其类似的有CharArrayInputStream、CharArrayOutputStream
针对字符数组操作的流和StringReader
、StringWriter
针对字符串操作的流。