Java I/0在安卓中的运用

目录

为什么Java的文件操作的概念会比较乱?

wIO体系

IO中的装饰者模式

如何理解in和out?

文件读写的整体流程 

BuffedInputStream如何提升性能?

 字符流

字符流与字节流整体的区别

 面试题 

File与RandomAccessFile

NIO----FileChannel


为什么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读入到内存后,还可以识别呢?这个就是序列化了,序列化怎么写,用什么算法读,所以是一定可以读出来的。写的数据是可序列的,为什么写入的数据人看不懂?因为是写给电脑的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值