建议阅读:
Java输入输出流
看完这个,Java IO从此不在难
java 字节流与字符流的区别
廖雪峰的官方网站
一、IO体系
Java中I/O操作主要是指使用Java进行输入,输出操作。Java所有的I/O机制都是基于数据流进行输入输出,这些数据流表示了字符或者字节数据的流动序列。
在 Java 中引入了 “流” 的概念,它表示任何有能力产生数据源或有能力接收数据源的对象。任何Java中表示数据源的对象都会提供以数据流的方式读写它的数据的方法。
1、从数据传输方式
或者说是运输方式
角度看,可以将 IO 类分为:
1)字节流
2)字符流
字节流和字符流的区别:
字节流中最小的数据单元是字节
,字符流中最小的数据单元是字符
(一个字符根据编码的不同,对应的字节也不同
,如 UTF-8 编码是 3 个字节,中文编码是 2 个字节。)字节流用来处理二进制文件
(图片、MP3、视频文件),字符流用来处理文本文件
(可以看做是特殊的二进制文件,使用了某种编码,人可以阅读)。简而言之,字节是个计算机看的,字符才是给人看的。
Java IO 相关的类确实很多,但我们常用的也就是文件相关的几个类,如文件最基本的读写类 File 开头的、文件读写带缓冲区的类 Buffered 开头的类,对象序列化反序列化相关的类 Object 开头的类。
2、从数据来源
或者说是操作对象
角度看,IO 类可以分为:
- 文件(file):FileInputStream、FileOutputStream、FileReader、FileWriter
- 数组([]):
- 字节数组(byte[]):ByteArrayInputStream、ByteArrayOutputStream
- 字符数组(char[]):CharArrayReader、CharArrayWriter
- 管道操作:PipedInputStream、PipedOutputStream、PipedReader、PipedWriter
- 基本数据类型:DataInputStream、DataOutputStream
- 缓冲操作:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
- 打印:PrintStream、PrintWriter
- 对象序列化反序列化:ObjectInputStream、ObjectOutputStream
- 转换:InputStreamReader、OutputStreamWriter
字符串(String)Java8中已废弃:StringBufferInputStream、StringBufferOutputStream、StringReader、StringWriter
二、IO类和相关方法
IO中最基本的是 4 个抽象类
:InputStream、OutputStream、Reader、Writer。最基本的方法也就是一个读 read() 方法、一个写 write() 方法。方法具体的实现还是要看继承这 4 个抽象类的子类。
输入/输出是相对于内存/程序来说的!!
InputStream 类:
抽象类
,基于字节
的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。
方法 | 方法介绍 |
---|---|
public abstract int read() | 读取一个byte的数据,返回值是高位补0的int类型值。若返回值=-1说明没有读取到任何字节读取工作结束。 |
public int read(byte b[]) | 读取b.length个字节的数据放到b数组中。返回值是读取的字节数。该方法实际上是调用下一个方法实现的 |
public int read(byte b[], int off, int len) | 从输入流中最多读取len个字节的数据,存放到偏移量为off的b数组中 |
public long skip(long n) | 忽略输入流中的n个字节,返回值是实际忽略的字节数, 跳过一些字节来读取 |
public int available() | 返回输入流中可以读取的字节数。注意:若输入阻塞,当前线程将被挂起,如果InputStream对象调用这个方法的话,它只会返回0,这个方法必须由继承InputStream类的子类对象调用才有用 |
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[]) | 将数组b中的所有字节写到输出流,实际调用的是下面的方法。 |
public void write(byte b[], int off, int len) | 将参数b的从偏移量off开始的len个字节写到输出流 |
public void flush() | 将数据缓冲区中数据全部输出,并清空缓冲区 |
public void close() | 关闭输出流并释放与流相关的系统资源 |
Reader 类:
抽象类
,基于字符
的输入操作。
方法 | 方法介绍 |
---|---|
public int read(java.nio.CharBuffer target) | 读取字节到字符缓存中 |
public int read() | 读取单个字符,返回值为读取的字符 |
public int read(char cbuf[]) | 读取一系列字符到数组cbuf[]中,返回值为实际读取的字符的数量 |
abstract public int read(char cbuf[], int off, int len) | 读取len个字符,从数组cbuf[]的下标off处开始存放,返回值为实际读取的字符数量,该方法必须由子类实现 |
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() | 关闭流释放相关资源 |
流结束的判断:方法read()的返回值为-1时;readLine()的返回值为null时
。
Writer 类:
抽象类
,基于字符
的输出操作。
方法 | 方法介绍 |
---|---|
public void write(int c) | 写入一个字符 |
public void write(char cbuf[]) | 将字符数组cbuf[]写入输出流 |
abstract public void write(char cbuf[], int off, int len) | 将字符数组cbuf[]中的从索引为off的位置处开始的len个字符写入输出流 |
public void write(String str) | 将字符串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() | 关闭输出流,流被关闭后就不能再输出数据了 |
另外,在java.io包中,由File
类提供了描述文件和目录的操作与管理方法
。但File类不是上述四个流式抽象类的子类,它不负责数据的输入输出,而专门用来管理磁盘文件与目录。
//File类主要用于命名文件、查询文件属性和处理文件目录。
public class File extends Object
implements Serializable,Comparable
{}
File类共提供了三个不同的构造函数,以不同的参数形式灵活地接收文件和目录名信息。构造函数:
1)File (String pathname)
//创建文件对象f1,f1所指的文件是在当前目录下创建的FileTest1.txt
File f1 = new File("FileTest1.txt");
2)File (String parent , String child)
//注意:D:\\dir1目录事先必须存在,否则异常
File f2 = new File(“D:\\dir1","FileTest2.txt") ;
3)File (File parent , String child)
//在如果 \\dir3目录不存在使用f4.mkdir()先创建
File f4 = new File("\\dir3");
File f5 = new File(f4,"FileTest5.txt");
一个对应于某磁盘文件或目录的File对象一经创建, 就可以通过调用它的方法来获得文件或目录的属性。
方法 | 方法描述 |
---|---|
public boolean exists( ) | 判断文件或目录是否存在 |
public boolean isFile( ) | 判断是文件还是目录 |
public boolean isDirectory( ) | 判断是文件还是目录 |
public String getName( ) | 返回文件名或目录名 |
public String getPath( ) | 返回文件或目录的路径 |
public long length( ) | 获取文件的长度 |
public String[ ] list ( ) | 将目录中所有文件名保存在字符串数组中返回 |
File类中还定义了一些对文件或目录进行管理、操作的方法,常用的方法有:
方法 | 方法描述 |
---|---|
public boolean renameTo( File newFile ) | 重命名文件 |
public void delete( ) | 删除文件 |
public boolean mkdir( ) | 创建目录 |
三、一些常用输入/输出流
接下来介绍下很常用的输出/输出流:
3.1 文件输入/输出FileInputStream/FileOutputStream类(字节)
1、FileInputStream类
FileInputStream可以使用read()方法一次读入一个字节,并以int类型返回;或者是使用read()方法时读入至一个byte数组,byte数组的元素有多少个,就读入多少个字节
。在将整个文件读取完成或写入完毕的过程中,这么一个byte数组通常被当作缓冲区,因为这么一个byte数组通常扮演承接数据的中间角色。
使用方法:
- 方式1
File fin = new File("d:/abc.txt");
FileInputStream in = new FileInputStream(fin);
- 方式2
FileInputStream in = new FileInputStream(“d: /abc.txt”);
2、FileOutputStream类
FileOutputStream类用来处理以文件作为数据输出目的数据流;一个表示文件名的字符串,也可以是File或FileDescriptor对象。
创建一个文件流对象有以下几种方法:
- 方式1
File f = new File (“d:/myjava/write.txt ");
FileOutputStream out = new FileOutputStream (f);
- 方式2
FileOutputStream out = new FileOutputStream(“d:/myjava/write.txt ");
- 方式3:构造函数将 FileDescriptor()对象作为其参数
FileDescriptor fd = new FileDescriptor();
FileOutputStream f2 = new FileOutputStream(fd);
- 方式4:构造函数将文件名作为其第一参数,将布尔值作为第二参数
FileOutputStream f = new FileOutputStream("d:/abc.txt",true);
注意:
(1)文件中写数据时,若文件已经存在,则覆盖存在的文件;
(2)读/写操作结束时,应调用close方法关闭流。
3.2 缓冲输入输出流 BufferedInputStream/ BufferedOutputStream
BufferedInputStream:当向缓冲流写入数据时候,数据先写到缓冲区,待缓冲区写满后,系统一次性将数据发送给输出设备。
BufferedOutputStream :当从向缓冲流读取数据时候,系统先从缓冲区读出数据,待缓冲区为空时,系统再从输入设备读取数据到缓冲区。
1)将文件读入内存:
将BufferedInputStream与FileInputStream相接
FileInputStream in = new FileInputStream( “file1.txt ” );
BufferedInputStream bin = new BufferedInputStream( in);
2)将内存写入文件:
将BufferedOutputStream与 FileOutputStream相接
FileOutputStream out = new FileOutputStream(“file1.txt”);
BufferedOutputStream bin = new BufferedInputStream(out);
3)键盘输入流读到内存
将BufferedReader与标准的数据流相接
InputStreamReader sin = new InputStreamReader (System.in) ;
BufferedReader bin = new BufferedReader(sin);
3.3 文件输入/输出FileReader/FileWriter类(字符)
1、FileReader :与FileInputStream对应,主要用来读取字符文件,使用缺省的字符编码,有三种构造函数:
- 方式1:将文件名作为字符串
FileReader f = new FileReader(“c:/temp.txt”);
- 方式2:构造函数将File对象作为其参数
File f = new file(“c:/temp.txt”);
FileReader f1 = new FileReader(f);
- 方式3:构造函数将File对象作为其参数
FileDescriptor() fd = new FileDescriptor()
FileReader f2 = new FileReader(fd);
2、FileWriter:两种构造函数
- 方式1:创建字符输出流类对象和已存在的文件相关联。文件不存在的话,并创建
FileWriter fw = new FileWriter(String fileName);
- 方式2:创建字符输出流类对象和已存在的文件相关联,并设置该流对文件的操作是否为续写。
//表示在fw对文件再次写入时,会在该文件的结尾续写,并不会覆盖掉。
FileWriter fw = new FileWriter("C:\\1.txt",ture);
四、一些示例
1、读取控制台中的输入
import java.io.*;
public class IOTest {
public static void main(String[] args) throws IOException {
// 三个测试方法
// test01();
// test02();
test03();
}
public static void test01() throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入一个字符");
char c;
c = (char) bufferedReader.read();
System.out.println("你输入的字符为"+c);
}
public static void test02() throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入一个字符,按 q 键结束");
char c;
do {
c = (char) bufferedReader.read();
System.out.println("你输入的字符为"+c);
} while (c != 'q');
}
public static void test03() throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入一行字符");
String str = bufferedReader.readLine();
System.out.println("你输入的字符为" + str);
}
}
至于控制台的输出,我们其实一直都在使用呢,System.out.println() ,out 其实是 PrintStream 类对象的引用,PrintStream 类中当然也有 write() 方法,但是我们更常用 print() 方法和 println() 方法,因为这两个方法可以输出的内容种类更多,比如一个打印一个对象,实际调用的对象的 toString() 方法。
2、二进制文件的写入和读取
注意这里文件的路径,可以根据自己情况改一下,虽然这里的文件后缀是txt,但该文件却是一个二进制文件,并不能直接查看。
@Test
public void test04() throws IOException {
byte[] bytes = {12,21,34,11,21};
FileOutputStream fileOutputStream = new FileOutputStream(new File("").getAbsolutePath()+"/io/test.txt");
// 写入二进制文件,直接打开会出现乱码
fileOutputStream.write(bytes);
fileOutputStream.close();
}
@Test
public void test05() throws IOException {
FileInputStream fileInputStream = new FileInputStream(new File("").getAbsolutePath()+"/io/test.txt");
int c;
// 读取写入的二进制文件,输出字节数组
while ((c = fileInputStream.read()) != -1) {
System.out.print(c);
}
}
3、文本文件的写入和读取
write() 方法和 append() 方法并不是像方法名那样,一个是覆盖内容,一个是追加内容,append() 内部也是 write() 方法实现的,也非说区别,也就是 append() 方法可以直接写 null,而 write() 方法需要把 null 当成一个字符串写入,所以两者并无本质的区别。需要注意的是这里并没有指定文件编码,可能会出现乱码的问题。
@Test
public void test06() throws IOException {
FileWriter fileWriter = new FileWriter(new File("").getAbsolutePath()+"/io/test.txt");
fileWriter.write("Hello,world!\n欢迎来到 java 世界\n");
fileWriter.write("不会覆盖文件原本的内容\n");
// fileWriter.write(null); 不能直接写入 null
fileWriter.append("并不是追加一行内容,不要被方法名迷惑\n");
fileWriter.append(null);
fileWriter.flush();
System.out.println("文件的默认编码为" + fileWriter.getEncoding());
fileWriter.close();
}
@Test
public void test07() throws IOException {
FileWriter fileWriter = new FileWriter(new File("").getAbsolutePath()+"/io/test.txt", false); // 关闭追加模式,变为覆盖模式
fileWriter.write("Hello,world!欢迎来到 java 世界\n");
fileWriter.write("我来覆盖文件原本的内容");
fileWriter.append("我是下一行");
fileWriter.flush();
System.out.println("文件的默认编码为" + fileWriter.getEncoding());
fileWriter.close();
}
@Test
public void test08() throws IOException {
FileReader fileReader = new FileReader(new File("").getAbsolutePath()+"/io/test.txt");
BufferedReader bufferedReader = new BufferedReader(fileReader);
String str;
while ((str = bufferedReader.readLine()) != null) {
System.out.println(str);
}
fileReader.close();
bufferedReader.close();
}
@Test
public void test09() throws IOException {
FileReader fileReader = new FileReader(new File("").getAbsolutePath()+"/io/test.txt");
int c;
while ((c = fileReader.read()) != -1) {
System.out.print((char) c);
}
}
使用字节流和字符流的转换类 InputStreamReader 和 OutputStreamWriter 可以指定文件的编码
,使用 Buffer 相关的类来读取文件的每一行。
@Test
public void test10() throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(new File("").getAbsolutePath()+"/io/test2.txt");
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "GBK"); // 使用 GBK 编码文件
outputStreamWriter.write("Hello,world!\n欢迎来到 java 世界\n");
outputStreamWriter.append("另外一行内容");
outputStreamWriter.flush();
System.out.println("文件的编码为" + outputStreamWriter.getEncoding());
outputStreamWriter.close();
fileOutputStream.close();
}
@Test
public void test11() throws IOException {
FileInputStream fileInputStream = new FileInputStream(new File("").getAbsolutePath()+"/io/test2.txt");
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "GBK"); // 使用 GBK 解码文件
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str;
while ((str = bufferedReader.readLine()) != null) {
System.out.println(str);
}
bufferedReader.close();
inputStreamReader.close();
}
4、复制文件
这里笔者做了一些测试,不使用缓冲对文件复制时间的影响,文件的复制实质还是文件的读写。缓冲流是处理流,是对节点流的装饰。
注:这里的时间是在我这台华硕笔记本上测试得到的,只是为了说明使用缓冲对文件的读写有好处。
@Test
public void test12() throws IOException {
// 输入和输出都使用缓冲流
FileInputStream in = new FileInputStream("E:\\视频资料\\大数据原理与应用\\1.1大数据时代.mp4");
BufferedInputStream inBuffer = new BufferedInputStream(in);
FileOutputStream out = new FileOutputStream("1.1大数据时代.mp4");
BufferedOutputStream outBuffer = new BufferedOutputStream(out);
int len = 0;
byte[] bs = new byte[1024];
long begin = System.currentTimeMillis();
while ((len = inBuffer.read(bs)) != -1) {
outBuffer.write(bs, 0, len);
}
System.out.println("复制文件所需的时间:" + (System.currentTimeMillis() - begin)); // 平均时间约 200 多毫秒
inBuffer.close();
in.close();
outBuffer.close();
out.close();
}
@Test
public void test13() throws IOException {
// 只有输入使用缓冲流
FileInputStream in = new FileInputStream("E:\\视频资料\\大数据原理与应用\\1.1大数据时代.mp4");
BufferedInputStream inBuffer = new BufferedInputStream(in);
FileOutputStream out = new FileOutputStream("1.1大数据时代.mp4");
int len = 0;
byte[] bs = new byte[1024];
long begin = System.currentTimeMillis();
while ((len = inBuffer.read(bs)) != -1) {
out.write(bs, 0, len);
}
System.out.println("复制文件所需时间:" + (System.currentTimeMillis() - begin)); // 平均时间约 500 多毫秒
inBuffer.close();
in.close();
out.close();
}
@Test
public void test14() throws IOException {
// 输入和输出都不使用缓冲流
FileInputStream in = new FileInputStream("E:\\视频资料\\大数据原理与应用\\1.1大数据时代.mp4");
FileOutputStream out = new FileOutputStream("1.1大数据时代.mp4");
int len = 0;
byte[] bs = new byte[1024];
long begin = System.currentTimeMillis();
while ((len = in.read(bs)) != -1) {
out.write(bs, 0, len);
}
System.out.println("复制文件所需时间:" + (System.currentTimeMillis() - begin)); // 平均时间 700 多毫秒
in.close();
out.close();
}
@Test
public void test15() throws IOException {
// 不使用缓冲
FileInputStream in = new FileInputStream("E:\\视频资料\\大数据原理与应用\\1.1大数据时代.mp4");
FileOutputStream out = new FileOutputStream("1.1大数据时代.mp4");
int len = 0;
long begin = System.currentTimeMillis();
while ((len = in.read()) != -1) {
out.write(len);
}
System.out.println("复制文件所需时间:" + (System.currentTimeMillis() - begin)); // 平均时间约 160000 毫秒,约 2 分多钟
in.close();
out.close();
}