一、IO简介
数据流是一组有序,有起点和终点的字节的数据序列。包括输入流和输出流。
流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种:
- 字节流:数据流中最小的数据单元是字节
- 字符流:数据流中最小的数据单元是字符
Java中的字符是Unicode编码,一个字符占用两个字节。
Java.io包中最重要的就是5个类和一个接口。5个类指的是File
、OutputStream
、InputStream
、Writer
、Reader
;一个接口指的是Serializable
。掌握了这些就掌握了Java I/O的精髓了。
Java I/O主要包括如下3层次:
- 流式部分——最主要的部分。如:OutputStream、InputStream、Writer、Reader等
- 非流式部分——如:File类、RandomAccessFile类和FileDescriptor等类
- 其他——文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。
二、IO详细介绍
在Android 平台,从应用的角度出发,我们最需要关注和研究的就是 字节流(Stream)字符流(Reader/Writer)和 File/ RandomAccessFile。当我们需要的时候再深入研究也未尝不是一件好事。关于字符和字节,例如文本文件,XML这些都是用字符流来读取和写入。而如RAR,EXE文件,图片等非文本,则用字节流来读取和写入。面对如此复杂的类关系,有一个点是我们必须要首先掌握的,那就是设计模式中的修饰模式,学会并理解修饰模式是搞懂流必备的前提条件哦。
在具体的学习流之前,我们必须要学的一个设计模式是装饰模式。因为从流的整个发展历史,出现的各种类之间的关系看,都是沿用了修饰模式,都是一个类的功能可以用来修饰其他类,然后组合成为一个比较复杂的流。比如说:
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(new File(file))));
从上面的代码块中大家不难看出这些类的关系:为了向文件中写入数据,首先需要创建一个FileOutputStream,然后为了提升访问的效率,所以将它发送给具备缓存功能的BufferedOutput-Stream,而为了实现与机器类型无关的java基本类型数据的输出,所以,我们将缓存的流传递给了DataOutputStream。从上面的关系,我们可以看到,其根本目的都是为outputSteam添加额外的功能。而这种额外功能的添加就是采用了装饰模式来构建的代码。因此,学习流,必须要学好装饰模式。
三、字节流
字节流inputStream和outputStream的的对应关系1、字节流的学习过程
为什么要按照一个学习路线来呢?原因是他们的功能决定的。OutputStream -> FileOutputStream/FilterOutputStream ->DataOutputStream->bufferedOutputStream
相应的学习InputStream方法就好了。
2、FilterOutputStream
从学习的角度来,我们应该先掌握FilterOutputStream, 以及FileOutputStream,这两个类是基本的类,从继承关系可以不难发现他们都是对 abstract 类 OutputStream的拓展,是它的子类。然而,伴随着 对 Stream流的功能的拓展,所以就出现了 DataOutputStream,(将java中的基础数据类型写入数据字节输出流中、保存在存储介质中、然后可以用DataOutputStream从存储介质中读取到程序中还原成java基础类型)。这里多提一句、DataOutputStream、FilterOutputStream三个类的关系的这种设计既使用了装饰器模式 避免了类的爆炸式增长。
3、BufferedOutputStream
为了提升Stream的执行效率,所以出现了bufferedOutputStream。bufferedOutputStream就是将本地添加了一个缓存的数组。在使用bufferedOutputStream之前每次从磁盘读入数据的时候都是需要访问多少byte数据就向磁盘中读多少个byte的数据,而出现bufferedOutputSteam之后,策略就改了,会先读取整个缓存空间相应大小的数据,这样就是从磁盘读取了一块比较大的数据,然后缓存起来,从而减少了对磁盘的访问的次数以达到提升性能的目的。
另外一方面,我们知道了outputStream(输出流)的发展历史后,我们便可以知道如何使用outpuSteam了,同样的方法,我们可以运用到inputStream中来,这样对称的解释就出现到了inputStream相关的中来了,于是,我们对整个字节流就有了全方位的理解,所以这样子我们就不会感觉到流的复杂了。这个时候对于其他的一些字节流的使用(byteArrayOutputStream/PipeOutputStream/ObjectOutputStream)的学习就自需要在使用的时候看看API即可。
public class DataStreamTest {
public static void main(String[] args) throws IOException {
testDataOutPutStream();
testDataInputStreamI();
}
private static void testDataOutPutStream() {
try {
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(
new File("src/testtxt/tataStreamTest.txt"))));
out.writeBoolean(true);
out.writeByte((byte)0x41);
out.writeChar((char)0x4243);
out.writeShort((short)0x4445);
out.writeInt(0x12345678);
out.writeLong(0x987654321L);
out.writeUTF("abcdefghijklmnopqrstuvwxyz严12");
out.writeLong(0x023433L);
out.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
private static void testDataInputStreamI() {
try {
File file = new File("src/testtxt/tataStreamTest.txt");
DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream(file)));
System.out.println(Long.toHexString(in.readLong()));
System.out.println(in.readBoolean());
System.out.println(byteToHexString(in.readByte()));
System.out.println(charToHexString(in.readChar()));
System.out.println(shortToHexString(in.readShort()));
System.out.println(Integer.toHexString(in.readInt()));
System.out.println(Long.toHexString(in.readLong()));
System.out.println(in.readUTF());
System.out.println(Long.toHexString(in.readLong()));
in.close();
} catch (Exception e) {
// TODO: handle exception
}
}
// 打印byte对应的16进制的字符串
private static String byteToHexString(byte val) {
return Integer.toHexString(val & 0xff);
}
// 打印char对应的16进制的字符串
private static String charToHexString(char val) {
return Integer.toHexString(val);
}
// 打印short对应的16进制的字符串
private static String shortToHexString(short val) {
return Integer.toHexString(val & 0xffff);
}
}
四、字符流
字符流reader和writer的对应关系字符流的学习和字节流的学习是一样的,它和字节流有着同样的发展过程,只是,字节流面向的是我们未知或者即使知道了他们的编码格式也意义不大的文件(png,exe, zip)的时候是采用字节,而面对一些我们知道文件构造我们就能够搞懂它的意义的文件(json,xml)等文件的时候我们还是需要以字符的形式来读取,所以就出现了字符流。reader 和 Stream最大的区别我认为是它包含了一个readline()接口,这个接口标明了,一行数据的意义,这也是可以理解的,因为自有字符才具备行的概念,相反字节流中的行也就是一个字节符号。
1、字符流的学习历程:
Writer- >FilterWriter->BufferedWriter->OutputStreamWriter->FileWriter->其
2、FilterWriter/FilterReader
字符过滤输出流、与FilterOutputStream功能一样、只是简单重写了父类的方法、目的是为所有装饰类提供标准和基本的方法、要求子类必须实现核心方法、和拥有自己的特色。这里FilterWriter没有子类、可能其意义只是提供一个接口、留着以后的扩展。。。本身是一个抽象类。
3、BufferedWriter/BufferedReader
BufferedWriter是 Writer类的一个子类。他的功能是为传入的底层字符输出流提供缓存功能、同样当使用底层字符输出流向目的地中写入字符或者字符数组时、每写入一次就要打开一次到目的地的连接、这样频繁的访问不断效率底下、也有可能会对存储介质造成一定的破坏、比如当我们向磁盘中不断的写入字节时、夸张一点、将一个非常大单位是G的字节数据写入到磁盘的指定文件中的、没写入一个字节就要打开一次到这个磁盘的通道、这个结果无疑是恐怖的、而当我们使用BufferedWriter将底层字符输出流、比如FileReader包装一下之后、我们可以在程序中先将要写入到文件中的字符写入到BufferedWriter的内置缓存空间中、然后当达到一定数时、一次性写入FileReader流中、此时、FileReader就可以打开一次通道、将这个数据块写入到文件中、这样做虽然不可能达到一次访问就将所有数据写入磁盘中的效果、但也大大提高了效率和减少了磁盘的访问量!
private static void testWriterAndStream(){
try {
BufferedWriter bufferedWriter = new BufferedWriter(
// new FileWriter("src/testtxt/writerAndStream.txt"));
new OutputStreamWriter(
new FileOutputStream(
new File("src/testtxt/writerAndStream.txt")),"GBK"));
bufferedWriter.write("我 爱你中国,亲爱的母亲");
bufferedWriter.flush();
bufferedWriter.close();
System.out.println("end");
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
4、OutputStreamWriter/InputStreamReader
输入字符转换流、是输入字节流转向输入字符流的桥梁、用于将输入字节流转换成输入字符流、通过指定的或者默认的编码将从底层读取的字节转换成字符返回到程序中、与OutputStreamWriter一样、本质也是使用其内部的一个类来完成所有工作:StreamDecoder、使用默认或者指定的编码将字节转换成字符;OutputStreamWriter/InputStreamReader只是对StreamDecoder进行了封装、isr内部所有方法核心都是调用StreamDecoder来完成的、InputStreamReader只是对StreamDecoder进行了封装、使得我们可以直接使用读取方法、而不用关心内部实现。
public class InputStreamReaderTest {
public static void testISRDefaultEncoder(InputStream is){
try{
// InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(
new InputStreamReader(is));
String string;
while ((string = br.readLine()) != null) {
// System.out.println("code: " + isr.getEncoding());
System.out.println(string);
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void testISRGBK(InputStream is){
try {
InputStreamReader inputStreamReader = new InputStreamReader(is,"GBK");
BufferedReader gbkBr = new BufferedReader(inputStreamReader);
String string;
while ((string = gbkBr.readLine()) != null) {
System.out.println("code: " + inputStreamReader.getEncoding());
System.out.println(string);
}
gbkBr.close();
} catch (IOException e) {
// TODO: handle exception
}
}
public static void testISRUTF8(InputStream is){
try {
InputStreamReader inputStreamReader = new InputStreamReader(is,"UTF-8");
BufferedReader gbkBr = new BufferedReader(inputStreamReader);
String string;
while ((string = gbkBr.readLine()) != null) {
System.out.println("code: " + inputStreamReader.getEncoding());
System.out.println(string);
}
gbkBr.close();
} catch (IOException e) {
// TODO: handle exception
}
}
public static void main(String[] args) throws IOException {
testISRDefaultEncoder(
new FileInputStream(
new File("src/testtxt/OutputStreamWriter.txt")));
testISRGBK(
new FileInputStream(
new File("src/testtxt/OutputStreamWriter.txt")));
testISRUTF8(
new FileInputStream(
new File("src/testtxt/OutputStreamWriter.txt")));
}
}
5、FileReader/FileWriter
FileReader和FileWriter 继承于InputStreamReader/OutputStreamWriter。
从源码可以发现FileWriter 文件字符输出流、主要用于将字符写入到指定的打开的文件中、其本质是通过传入的文件名、文件、或者文件描述符来创建FileOutputStream、然后使用OutputStreamWriter使用默认编码将FileOutputStream转换成Writer(这个Writer就是FileWriter)。如果使用这个类的话、最好使用BufferedWriter包装一下、高端大气上档次、低调奢华有内涵!
FileReader 文件字符输入流、用于将文件内容以字符形式读取出来、一般用于读取字符形式的文件内容、也可以读取字节形式、但是因为FileReader内部也是通过传入的参数构造InputStreamReader、并且只能使用默认编码、所以我们无法控制编码问题、这样的话就很容易造成乱码。所以读取字节形式的文件还是使用字节流来操作的好、同样在使用此流的时候用BufferedReader包装一下、就算冲着BufferedReader的readLine()方法去的也要使用这个包装类、不说他还能提高效率、保护存储介质。
public static void main(String[] args) throws IOException {
File srcfile = new File("src/testtxt/BufferedReader.txt");
File dstFile = new File("src/testtxt/BufferedWrite.txt");
BufferedWriter bw = new BufferedWriter(new FileWriter(dstFile));
BufferedReader br = new BufferedReader(new FileReader(srcfile));
char[] string = new char[1024]; // 请注意此处不是byte,而是char
while ((br.read(string))!= -1) {
bw.write(string);
}
br.close();
bw.flush();
bw.close();
}
五、字符流最常见用法
字节与字符输出流字节的关系
字节输入流和字符输入流之间的关系
六、RandomAccessFile
1、构造方法
RandomAccessFile raf = newRandomAccessFile(File file, String mode);
其中参数 mode 的值可选 "r":可读,"w" :可写,"rw":可读性;
2、成员方法
-
seek(int index)
:可以将指针移动到某个位置开始读写; -
setLength(long len)
:给写入文件预留空间:
3、RandomAccessFile 特点和优势
-
既可以读也可以写
RandomAccessFile不属于InputStream和OutputStream类系的它是一个完全独立的类,所有方法(绝大多数都只属于它自己)都是自己从头开始规定的,这里面包含读写两种操作。 -
可以指定位置读写
RandomAccessFile能在文件里面前后移动,在文件里移动用的seek( ),所以它的行为与其它的I/O类有些根本性的不同。总而言之,它是一个直接继承Object的,独立的类。只有RandomAccessFile才有seek搜寻方法,而这个方法也只适用于文件。
public class RandomAccessFileTests {
private static final File file = new File("src\\testtxt\\raf.txt");
/**
* 向文件中写入内容
*/
public static void testRandomAccessFileWriter() throws IOException{
//要先将已有文件删除、避免干扰。
if(file.exists()){
file.delete();
}
RandomAccessFile rsfWriter = new RandomAccessFile(file, "rw");
//不会改变文件大小、但是他会将下一个字符的写入位置标识为10000、
//也就是说此后只要写入内容、就是从10001开始存、
rsfWriter.seek(10000);
printFileLength(rsfWriter); //result: 0
//会改变文件大小、只是把文件的size改变、
//并没有改变下一个要写入的内容的位置、
//这里注释掉是为了验证上面的seek方法的说明内容
rsfWriter.setLength(10000);
System.out.println("oo");
printFileLength(rsfWriter); //result: 0
System.out.println("xx");
//每个汉子占3个字节、写入字符串的时候会有一个记录写入字符串长度的两个字节
rsfWriter.writeUTF("哈哈啊哈哈");
printFileLength(rsfWriter); //result: 10014
//每个字符占两个字节
rsfWriter.writeChar('a');
rsfWriter.writeChars("abcde");
printFileLength(rsfWriter); //result: 10026
//再从“文件指针”为5000的地方插一个长度为100、内容全是'a'的字符数组
//这里file长依然是10026、因为他是从“文件指针”为5000的地方覆盖后面
//的200个字节、下标并没有超过文件长度
rsfWriter.seek(5000);
char[] cbuf = new char[100];
for(int i=0; i<cbuf.length; i++){
cbuf[i] = 'a';
rsfWriter.writeChar(cbuf[i]);
}
printFileLength(rsfWriter); //result: 10026
//再从“文件指针”为1000的地方插入一个长度为100、内容全是a的字节数组
//这里file长依然是10026、因为他是从“文件指针”为5000的地方覆盖后面
//的200个字节、下标并没有超过文件长度
byte[] bbuf = new byte[100];
for (int i = 0; i < bbuf.length; i++) {
bbuf[i] = 1;
}
rsfWriter.seek(1000);
rsfWriter.writeBytes(new String(bbuf));
printFileLength(rsfWriter);
}
/**
* 从文件中读取内容
* 这里我们要清楚现在文件中有什么内容、而且还要清楚这些内容起始字节下标、长度
*
* @throws IOException
*/
public static void testRandomAccessFileRead() throws IOException{
RandomAccessFile rsfReader = new RandomAccessFile(file, "r");
//可按照自己想读取的东西所在的位置、长度来读取
//读取"哈哈啊哈哈"
rsfReader.seek(10000);
System.out.println(rsfReader.readUTF());
//读取100个字符'a'
rsfReader.seek(5000);
byte[] bbuf = new byte[200];
rsfReader.read(bbuf);
System.out.println(new String(bbuf));
//读取100个1
byte[] bbuf2 = new byte[100];
rsfReader.seek(1000);
rsfReader.read(bbuf2, 0, 100);
for(byte b : bbuf2){
System.out.print(b);
}
//读取字符'aabcde'
byte[] bbuf3 = new byte[12];
rsfReader.seek(10014);
rsfReader.read(bbuf3);
System.out.println(new String(bbuf3));
}
/**
* 打印文件长度
* @param rsfWriter 指向文件的随机文件流
* @throws IOException
*/
private static void printFileLength(RandomAccessFile rsfWriter)
throws IOException {
System.out.println("file length: " + rsfWriter.length() + " file pointer: " + rsfWriter.getFilePointer());
}
public static void main(String[] args) throws IOException {
testRandomAccessFileWriter();
testRandomAccessFileRead();
}
}
七、NIO—FileChannel
Channel(管道)是对I/O操作的封装。
FileChannel配合着ByteBuffer,将读写的数据缓存到内存中,然后以批量/缓
存的方式read/write,省去了非批量操作时的重复中间操作,操纵大文件时可
以显著提高效率(和Stream以byte数组方式有什么区别?经过测试,效率上几
乎无区别)。
Channel 读写文件
private static void copyFileByFileChannel(File sourceFile,File targetFile){
long start = System.currentTimeMillis();
RandomAccessFile randomAccessSourceFile;
RandomAccessFile randomAccessTargetFile;
try {
randomAccessSourceFile = new RandomAccessFile(sourceFile, "r");
randomAccessTargetFile = new RandomAccessFile(targetFile, "rw");
} catch (Exception e) {
e.printStackTrace();
return;
}
FileChannel sourceFileChannel = randomAccessSourceFile.getChannel();
FileChannel targetFileChannel = randomAccessTargetFile.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024*1024);
try {
while(sourceFileChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
targetFileChannel.write(byteBuffer);
byteBuffer.clear();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
sourceFileChannel.close();
} catch (Exception e2) {
e2.printStackTrace();
}
try {
targetFileChannel.close();
}catch (Exception e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
用字节流读取文件
private static void copyFileByStream(File sourceFile,File targetFile) {
long start = System.currentTimeMillis();
FileInputStream fis;
FileOutputStream fos;
try {
fis = new FileInputStream(sourceFile);
fos = new FileOutputStream(targetFile);
} catch (FileNotFoundException e) {
e.printStackTrace();
return;
}
byte[] readed = new byte[1024*1024];
try {
while (fis.read(readed) != -1) {
fos.write(readed);
}
} catch( IOException e){
e.printStackTrace();
} finally {
try{
fos.close();
}catch (Exception e) {
e.printStackTrace();
}
try {
fis.close();
} catch (Exception e2) {
e2.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
测试
public static void main(String[] args) {
File sourceFile = new File("D://alvin//IOtest//file1.mp4");
File targetFile = new File("D://file1-1.mp4");
targetFile.deleteOnExit();
try {
targetFile.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
copyFileByStream(sourceFile, targetFile);
copyFileByFileChannel(sourceFile, targetFile);
}
经测试 Channel 读写比字节流要快。
总结
1、如果只用FileOutputStream fileOutputStream = new FileOutputStream("d:/text.txt");不是也能输出到"d:/text.txt"吗?为什么要用其它两个呢?能起到什么作用呢。
-
FileOutputStream
:是字节流,它一个字节一个字节的向外边送数据 -
OutputStreamWriter
:是字符流,它一个字符一个字符的向外边送数据
2、它们有什么区别么
因为计算机是洋鬼子发明的,它们的英文字符占一个字节,而我们的中文是一个字符,至少占俩字节。如果用stream,你读出来的英语再倒也罢了,读出来的中文可就是乱码或者一个个“????”。如果你用WRITER,就不会有乱码了
3、BufferedWriter Buffer是一个缓冲区,为什么要用BUFFER呢?
如果你直接用stream或者writer,你的硬盘可能就是读一个字符或者一个字节 就去读写硬盘一次,IO负担巨大。可是你用了Buffer,你的硬盘就是读了一堆数据之后,读写一下硬盘。这样对你硬盘有好处。
4、字节流与字符流的区别
字节流在操作的时候本身是不会用到缓冲区(内存)的,是与文件本身直接操作的,而字符流在操作的时候是使用到缓冲区的字节流在操作文件时,即使不关闭资源(close方法),文件也能输出,但是如果字符流不使用close方法的话,则不会输出任何内容,说明字符流用的是缓冲区,并且可以使用flush方法强制进行刷新缓冲区,这时才能在不close的情况下输出内容。
5、那开发中究竟用字节流好还是用字符流好呢
在所有的硬盘上保存文件或进行传输的时候都是以字节的方法进行的,包括图片也是按字节完成,而字符是只有在内存中才会形成的,所以使用字节的操作是最多的。
如果要java程序实现一个拷贝功能,应该选用字节流进行操作(可能拷贝的是图片),并且采用边读边写的方式(节省内存)。
网友评论