目录
为什么Java的文件操作的概念会比较乱?
历史遗留问题。文件主要还是要弄清楚这个嵌套流程。这就涉及到装饰者模式,所以弄清楚装饰者模式对理解文件的操作有很大的帮助。
wIO体系
装饰者模式,最大特点是灵活:
最大的缺点:类特别多。包括被装饰着和装饰者。
安卓中的装饰者:
装饰者模式详解可以看这篇文章:Java 设计模式之装饰者模式 - SegmentFault 思否
UML类图
说明:
- 车的类图结构为<<abstract>>,表示车是一个抽象类;
- 它有两个继承类:小汽车和自行车;它们之间的关系为实现关系,使用带空心箭头的虚线表示;
- 小汽车为与SUV之间也是继承关系,它们之间的关系为泛化关系,使用带空心箭头的实线表示;
- 小汽车与发动机之间是组合关系,使用带实心箭头的实线表示;
- 学生与班级之间是聚合关系,使用带空心箭头的实线表示;
- 学生与身份证之间为关联关系,使用一根实线表示;
-
学生上学需要用到自行车,与自行车是一种依赖关系,使用带箭头的虚线表示;
IO中的装饰者模式
作为安卓开发者,需要注意的主要的IO类是这些:
读文件,创建一个InputStream,然后创建一个DataIputStream,传入BufferdInputStream,然后再交给FileInputStrem中。
File->InputStream->FileInputStream->BufferdInputStream->DataInputStream
如何理解in和out?
因为我们是针对内存来说的,对于内存来说的,in就是进入内存,out就是从内存中出去。一个文件,我们在运行时,需要将它转化为byte[],int,char,string。文件是存储在外存中的。
写数据到文件:
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();
从文件中读取:
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();
读写顺序需要保持一一对应,否则无法读取到正确的数据,这也是序列化中大量使用的东西。
文件读写的整体流程
InputStream是装饰者的抽象基类。
BuffedInputStream如何提升性能?
注意这个BuffedInputStream里边的这个变量:
volatile的底层原理:今日头条
protected volatile byte buf[];
进入read方法,这里讲内容读入到内存中:
private int read1(byte[] b, int off, int len) throws IOException {
int avail = count - pos;
if (avail <= 0) {
/* If the requested length is at least as large as the buffer, and
if there is no mark/reset activity, do not bother to copy the
bytes into the local buffer. In this way buffered streams will
cascade harmlessly. */
if (len >= getBufIfOpen().length && markpos < 0) {
return getInIfOpen().read(b, off, len);
}
fill();
avail = count - pos;
if (avail <= 0) return -1;
}
int cnt = (avail < len) ? avail : len;
System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
pos += cnt;
return cnt;
}
所谓的增加效率,就是通过每次将一定数量的byte[]读入到内存中。不推荐每次就读一个字节,这个方式就是不使用缓存的方式,这样会导致读取1024个数据,就需要让磁头移动1024次,这回相当的耗时,利用空间换时间,利用内存来存储一部分的数据。减少磁头的移动次数。进而节省时间。这才是因为使用缓存所以读取速度变快的原理。
#java.base/java/io/BufferedInputStream.java
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0)
pos = 0; /* no mark: throw away the buffer */
else if (pos >= buffer.length) { /* no room left in buffer */
if (markpos > 0) { /* can throw away early part of the buffer */
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
markpos = -1; /* buffer got too big, invalidate mark */
pos = 0; /* drop buffer contents */
} else { /* grow buffer */
int nsz = ArraysSupport.newLength(pos,
1, /* minimum growth */
pos /* preferred growth */);
if (nsz > marklimit)
nsz = marklimit;
byte[] nbuf = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!U.compareAndSetReference(this, BUF_OFFSET, buffer, nbuf)) {
// Can't replace buf if there was an async close.
// Note: This would need to be changed if fill()
// is ever made accessible to multiple threads.
// But for now, the only way CAS can fail is via close.
// assert buf == null;
throw new IOException("Stream closed");
}
buffer = nbuf;
}
}
count = pos;
// 每次读取一个固定长度的数据到内存中。每次读取是把整个buffer读满。
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
#java.base/java/io/InputStream.java
public int read(byte[] b, int off, int len) throws IOException {
Objects.checkFromIndexSize(off, len, b.length);
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;
}
如果不用buffer来读取,直接使用FileInputStream来的话,那么会走这里:
一次就读一个字节。
字符流
为什么有字节流还需要字符流?
字节与字符的区别,主要是因为不同的字符需要的字节数可能不同,按照字节去读,可能存在乱码的情况。两者最大的区别:readLine(),读取的字节是没有特殊意义的。每一行的换行符都是bbyte构成的,自由字符流才有换行的概念。我们大部分的文件操作xml、json、zip、apk、exe,这些文件读成字节或者字符都是一样的,没有区别。我们读取的都是字节。
只有每一行都有自己的意义,用readLine()才有意义,字符流才有意义。
字符流与字节流整体的区别
有字符就有字节,我们需要让他们严格的对应起来。就需要严格的对应关系。
BufferedReader br = new BufferedReader(new InputStreamReader(is));
#java.base/java/io/OutputStreamWriter.java
private final StreamEncoder se;
/**
* Creates an OutputStreamWriter that uses the named charset.
*
* @param out
* An OutputStream
*
* @param charsetName
* The name of a supported {@link Charset charset}
*
* @throws UnsupportedEncodingException
* If the named encoding is not supported
*/
public OutputStreamWriter(OutputStream out, String charsetName)
throws UnsupportedEncodingException
{
super(out);
if (charsetName == null)
throw new NullPointerException("charsetName");
// 利用StreamEncoder将一个流变成编码,然后其余的read write方法都给予这个编码进行操作
se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);
}
字节流与字符流的区别:
对于中文来说,一个字符=两个字节,对于英文:一个字符=一个字节。所以出现了编码的格式:例如UTF-8,GBK,这些主要是为了兼容各种不同的文字。文字最终的构成还是字节,只是用不同的格式,方法拼接。因此我们读取的时候,还是需要通过字节流来获取,然后传递给字符流去进行处理:
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();
}
上边和下边这种方式再源码上看是没有区别的,FileWriter里边也是通过OutputStreamWriter来实现流的读取的:
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));
因此,整个字符流我们只需要关注红框里边的内容:
字符流与字节流的关系:
面试题
特殊的ObjectInputStream
读写的类必须向上图一一对应,否则无法正常读取。一个object有意的读写到文件,是必须要序列胡的。
有这样一个案例,对于一个类,将他序列化之后,然后移动它到其他的目录,然后再用在新的目录下的类进行反序列化,这是回报错的。因为我们在序列化的时候,往文件里边写了包名和雷鸣,都是包含路径的。这个文件的文件头包含了这些信息,如果类名都不一致了,就会报错,这个在安卓的AIDL中也是这样的,类路径是必须完全一致的。还有就是如果我们直接去读取一个文件,例如下边的代码,但是文件里边没有任何内容,也会报错,java.io.EOFExecption.英文没有文件头,无论是字节流还是字符流都会有同样的问题。
private static void writeObject(){
try {
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("src/testtxt/object.txt"));
for(int i = 0; i < 10; i++){
oos.writeObject(new Person("天下第一" + i +"]", i));
}
oos.writeObject(null);
oos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void readObject() {
try {
ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream(newFile("src/testtxt/object.txt"))))
while (ois.available() != -1) {
try {
Object object = ois.readObject();
// Person路径变化,将会在读取的时候报错。
Person person = (Person) object;
System.out.println(person.toString());
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
ois.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
File与RandomAccessFile
File:打哪指哪,只能从头开始读写。
RandomAccessFile:指哪打哪,可以读写到一个文件的任意位置。
这是两种读写文件的方式,与文件本身没有关系。
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);
网络数据的断点续传就可用RandomAccessFile,也就是多段下载的基本原理。由于网络带宽上线的限制,导致我们不可能在分段下载时,将段分的过多,这无法通过多个线程来解决。
多个服务器可以提速吗?不一定,最终网络传递到本地,仍然是同一个IP,仍旧会有带宽限制的问题。而且如果使用的多个服务器是,如果有的服务器距离我们过远的话,可能也会导致下载速度不够快。
NIO----FileChannel
在FileInputStream 定义了这个变量,读写的工作都使用了ByteBuffer,底层使用了管道,管道在NIO中定义的。进行大文件操作的时候可以用这个,NDK可能会有多用一些。特别是音视频这一块。
public class FileInputStream extends InputStream {
/* File Descriptor - handle to the open
private final FileDescriptor fd;
/**
* The path of the referenced file
* (null if the stream is created with a
*/
private final String path;
private FileChannel channel = null;
同一个文件直接使用FileInputStream和Channel对比,Channel相较会更快一些。
再提一遍:弄清楚装饰者模式在文件IO中的应用对理解整个文件的流程非常重要。
最后一个问题
DataOutPutStream,写文件之后,为什么我们打开写入的文件我们读不懂里边的内容?而用DataIinutStream却可以正确的读出来呢?DataOutPutStream写的格式是Java内存中的格式,写入文件后,是文件的编码格式,为什么DataIinutStream读入到内存后,还可以识别呢?这个就是序列化了,序列化怎么写,用什么算法读,所以是一定可以读出来的。写的数据是可序列的,为什么写入的数据人看不懂?因为是写给电脑的。