1.流的概念
一个对输入输出设备的抽象概念,JAVA中文件的操作都是以“流”的方式进行。可以把流理解成“管道”,数据通过管道得以从一端传输到另一端。
流具有方向性:一般而言这个方向是以程序作为参照,所以,输入流是指输向程序的流,输出流是指从程序输出的流;也可以理解成,程序从输入流获取数据,向输出流写数据。
流式输入输出的特点为:数据的获取和发送沿数据序列的顺序进行,即每一个数据都必须等待排在它前面的数据,等前面的数据读入或送出后才能被读写。因此,流和队列一样,只能以“先进先出”的方式对其中的数据进行读写,而不能随意选择读写位置。
2.JAVA IO库的设计原则
两个原则
1.输入输出对称性。如:InputStream和OutputStream各自占据字节流的输入和输出的两个平行等级结构的根部,Reader和Writer则对应字符流输入和输出的两个平行等级结构的根部。
2.byte和char的对称性。如:InputSteam和Reader,OutputSteam和Writer。
两种设计模式
1.装饰者模式(Decorator)。装饰者又称为包装器,其作用类似子类实现父类并自定义更多更丰富的方法,但是装饰者模式比子类更灵活。
上图以InputStream为例,描述了装饰者模式的实现案例。下面说一下个人对上图的简单理解:InputStream作为抽象component根节点,FileInputStream、StringBufferInputStream、ByteArrayInputStream三者作为被装饰对象的具体实现,而FilterInputStream及其子类则是装饰者。由于所有元素都是component根节点的子孙类,所以他们在方法调用上是一致的,装饰者通过在构建过程中调用被装饰对象,从而在实现具体方法的时候,可以在被装饰者具体方法前加入自己的逻辑,这样就实现了对原有方法基础上的自定义。(具体可检索"装饰者模式")
2.适配器模式(Adapter)。以FileInputStream为例。
public class FileInputStream extends InputStream { /* File Descriptor - handle to the open file */ private FileDescriptor fd; ...... public FileInputStream(FileDescriptor fdObj) { SecurityManager security = System.getSecurityManager(); if (fdObj == null) { throw new NullPointerException(); } if (security != null) { security.checkRead(fdObj); } fd = fdObj; } ......
先理解下Java磁盘IO实现机制:文件是操作系统与磁盘驱动器交互的最小单元,程序也只能通过文件来操作磁盘上的数据。在Java中,文件操作通过File类完成,但是一个File类其实并不代表一个真实存在的文件对象,当new一个File类的时候,返回的只是跟所传递的路径描述符相关联的虚拟对象,而真正要操作这个文件对象,则是通过File类中引用的FileSystem属性来进行底层系统交互来实现的(当然,这也与Java本身不能对操作系统底层进行访问,只能通过JNI接口调用其他语言来实现对底层访问有关)。
通过上面分析可知,同理,FileInputStream在调用系统文件方面,其实现适配器模式可以这样理解:通过引用FileDescriptor类,并将其适配成InputSteam类型的对象,实现文件操作输入流。
3.流的分类
按处理的数据单位分:字节流、字符流
按数据方向(相对于程序本身)分:输入流、输出流
按功能分:节点流、处理流
其中第三点的解释为:节点流是指从某个特定数据源读写数据的流;而处理流是基于已存在的流(节点流或处理流)之上,为数据处理提供更强大功能的流,又称为“过滤流”,它是对原有流的包装。
四种基本抽象类:输入流——InputStream、Reader;输出流——OutputStream、Writer
实现类如图,深色为节点流,浅色为处理流:
4.read和write方法
- InputStream:Java中,InputStream类定义了三个read方法:
- public abstract int read() throws IOException;从输入流中读取数据的下一个字节,返回读到的字节值。这个返回值是0-255的int值,如果到达流的末尾,则返回-1。
- public int read(byte b[]) throws IOException;从输入流中读取b.length个字节的数据,并存储在缓冲区数据b中,返回的是实际读取到的字节数。
- public int read(byte b[], int off, int len) throws IOException;从输入流中读取len个字节的数据,并从b的off位置开始写入到b中。
- OutputStream:同样是三个write方法:
- public abstract void write(int b) throws IOException;将指定字节(b)写入此输出流,其实这里写入的b的8个低阶比特,它的24个高阶比特将被忽略。
- public void write(byte b[]) throws IOException;将b.length个字节从指定的byte数组(b)写入此输出流。
- public void write(byte b[], int off, int len) throws IOException;将指定数组b中从off开始的len个字节写入此输出流。
5.字节流和字符流的实现
①基于字节的输入流:
值得注意的地方:1、节点流(Level2)直接与数据源交互,所以其大多会指明数据源的形式:如ByteArray、File、Piped;2、过滤流(Level3)是基于其他流的包装(都集成自FilterInputStream),为其提供更丰富的功能,所以过滤流则多是指明功能:如Buffered、LineNumber。
Level2:
- ByteArrayInputStream:从内存中读取一个字节的数据,在创建其实例的时候,程序内部会创建一个byte数组类型的缓冲区,将读取的数据保存在该缓冲区中,并用一个计数器记录从数据源中读取的字节数目。需要注意的是,在源码中,ByteArrayInputStream的close()方法是空的,没有任何实际作用,这是因为它是从内存读取数据,不存在关闭连接的说法。
- FileInputStream:从文件中读取一个字节的数据。FileInputStream将文件操作类FileDescriptor适配成InputStream,从而实现了对文件的读取。
- PipedInputStream:管道输入流可以与管道输出流相连接,它从管道输出流中读取一个字节的数据,主要用于多线程情况。输入流和输出流分成两个线程,避免线程的死锁。
- SequenceInputStream:把多个输入流按顺序合并成一个输入流。
Level3:
- DataInputStream:数据流用于操作基本类型的数据,将获取到的数据流转换成原始数据类型。
- BufferedInputStream:缓冲流提供了读取数据的内部缓冲区,这样在读数据的时候,可以一次读取更多数据存放在缓冲区,然后再返回。通过内部缓冲区,BufferedInputStream提供了对数据读取的更优化方式,减少了IO操作。此外,BufferedInputStream还提供了mark(标记方法)、reset(重置标记方法)、skip(跳过指定字节)、available(预估可用字节数方法)。
②基于字节的输出流:
Level2:
- PrintStream:字节打印流,提供了一系列print和println方法,可以将到底层的输出流输出成被转换成字符串的数据。打印流不会抛出IO异常,出现异常会将其trouble属性设为true,使用者可以调用checkError方法检查是否有异常出现。并且,PrintStream自备自动flush功能,在它的输出方法被调用后,会自动执行flush清空缓存。
- DataOutputSteam:与DataInputStream类似,它可以将内存中的基本类型数据直接写入底层输出流。
- BufferedOutputStream:为输出流提供缓冲功能,所有的输出请求会先缓冲在BufferedOutputStream的缓冲区,待到合适时机再统一写出。所以其write方法可能会先缓存数据,如果需要立即写入,可调用flush()方法。
Level3:
- ByteArrayOutputStream:该输出流用于将数据输出到内存中,在输出前会将数据缓存在自身的缓冲区里 ,其close方法也是空的,因为它是往内存写数据,不存在关闭链接的说法。
- FileOutputStream:用于往文件中输出数据字节。有些系统下,同一文件会只允许打开一个输出流。
③基于字符的输入流:
需要注意的地方:实际上所有对文件的IO操作都是通过字节的形式实现的,所谓的字符流只不过逻辑上的一种说法。
上述类不做过多介绍,基本与字节输入流对应,其中StringReader是从一个字符串中读取内容,LineNumberReader是一个可以跟踪读入的“行数据”的字符输入流,它内置一个指示器用于跟踪读入数据的行数。
④基于字符的输出流:
⑤字节流和字符流之间的转换:
InputStreamReader和OutputStreamWriter分别提供了字节输入流到字符输入流和字符输出流到字节输出流的转换,转换可指定编码。
6.RandomAccessFile
RandomAccessFile是独立实现的JavaIO操作类,它并不在上面所说的输入输出流结构中。作为扩充了IO框架的随机文件流,它可以在文件的任意位置读取或写入数据。其在文件系统中建立一个文件指针,用于标记将要进行读写操作的下一字节的位置,seek方法可以将指针移动到文件内部的任意位置,从而实现文件的随机读取。
7.实例代码
代码1:将网络上的某个页面(资源)复制到本地(页面静态化)
public static void main(String[] args) throws Exception { InputStream in = new BufferedInputStream(new URL("http://www.jd.com").openStream()); File bdFile = new File("D:\\jd.html"); if (!bdFile.exists()) { bdFile.createNewFile(); } OutputStream ou = new BufferedOutputStream(new FileOutputStream(bdFile)); byte[] b = new byte[1024]; int len; while((len = in.read(b, 0, b.length)) != -1){ ou.write(b, 0, len); } }
代码2:将某个网络资源通过多线程下载到本地(使用RandomAccessFile类)
public class Main { /** * @param args */ public static void main(String[] args) throws Exception{ String path = "xxx";//网络资源URL String filePath = "D:\\";//本地磁盘位置 new Main().threadDownload(path, 5, filePath); } public void threadDownload(String path, int threadSize, String filePath) throws Exception{ URL url = new URL(path); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setRequestMethod("GET"); if (httpURLConnection.getResponseCode() != 200) { throw new Exception("响应异常"); } int dataLength = httpURLConnection.getContentLength(); String fileName = Main.getFileName(httpURLConnection); httpURLConnection.disconnect(); threadSize = threadSize == 0 ? 5 : threadSize; int blockSize = dataLength/threadSize; int left = dataLength % threadSize; for (int i = 0; i < threadSize; i++) { if (i == threadSize) { blockSize += left; } new Thread(new DownloadThread(path, new RandomAccessFile(new File(filePath + fileName), "rw"), blockSize * i, blockSize, i + 1)).start(); } } private static String getFileName(HttpURLConnection httpURLConnection) throws Exception{ Map<String, List<String>> headerFields = httpURLConnection.getHeaderFields(); Set<Entry<String, List<String>>> set = headerFields.entrySet(); for (Entry<String, List<String>> entry : set) { if (entry.toString().contains("filename")) { List<String> list = entry.getValue(); for (String string : list) { if (string.contains("filename")) { String fileNameResult = new String(string.getBytes(), "UTF-8"); return fileNameResult.substring(fileNameResult.indexOf("\"") + 1, fileNameResult.lastIndexOf("\"")); } } } } return null; } } public class DownloadThread implements Runnable{ private String path; private RandomAccessFile randomAccessFile; private int startSize; private int blockSize; private int threadName; public DownloadThread(String path, RandomAccessFile randomAccessFile, int startSize, int blockSize, int threadName) { this.path = path; this.randomAccessFile = randomAccessFile; this.startSize = startSize; this.blockSize = blockSize; this.threadName = threadName; } @Override public void run() { HttpURLConnection httpURLConnection = null; InputStream inputStream = null; try { System.out.println("线程"+threadName+"开始下载"); URL url = new URL(path); httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setRequestMethod("GET"); httpURLConnection.setReadTimeout(10 * 1000); httpURLConnection.setRequestProperty("Range", "bytes=" + startSize + "-"); if (httpURLConnection.getResponseCode() != 206) {//206 Partial Content 客户发送了一个带有Range头的GET请求,服务器完成了它(HTTP 1.1新) System.out.println("线程" + threadName + "响应异常,代码为" + httpURLConnection.getResponseCode()); throw new Exception("响应异常"); } inputStream = httpURLConnection.getInputStream(); byte[] b = new byte[1024]; int len = -1; int length = 0; randomAccessFile.seek(startSize); while(((len = inputStream.read(b)) != -1) && length < blockSize){ randomAccessFile.write(b, 0, len); length += len; } randomAccessFile.close(); inputStream.close(); httpURLConnection.disconnect(); System.out.println("线程"+threadName+"下载完成"); } catch (Exception e) { e.printStackTrace(); } finally { randomAccessFile = null; inputStream = null; httpURLConnection = null; } } }