I/O操作给人感觉倒不是很难,就是设计到的类和方法太多,太繁琐了,如何辨别这些方法以及如何合理的对文件进行操作就显得很重要了。本文就来详细的介绍和总结Java I/o操作设计的相关内容
1)输入和输出
首先让我们来认识一个在I/O操作中经常会提及的名词“流。什么叫“流”呢?按照”Thinking in java“中的解释为”它(流)代表任何有能力产出数据的数据源对象或者有能力接受数据的接收端对象,'流'拼·屏蔽了实际的I/O设备中处理数据的细节“。
很显然I/0操作肯定是要涉及两个方面的即输入和输出 InputStream作用来表示那些从不同数据源产生输入的类,这些数据源包括(及其相应的类)
1)字节数组 ByteArrayInputStream
允许将内存中的缓冲区当做InputStream使用
2) String对象 StringBufferInputStream
将String转换成InputStream
3)文件 FileInputStream
从文件中读取信息
4)管道 PipeInputStream(作为多线程中的数据源) 工作方式与实际的管道相似,即都是从一端输入,另一端输出
5)一个由其他种类的流组成的序列,以便我们可以把这些InputStream流集合起来,转换成一个单一的InoutStream流中 SequenceInputStream
6)其他数据源,如网络等。这个暂时就不扯了
上述所有的数据源所对应的类都是InputStream的一个子类,除了上述子类外,我们还要知道一个重要的子类,就是FilterInputStream,但是它与上述的集中InputStream有有一点不一样,下面的代码是其构造函数
protected FilterInputStream(InputStream in) { this.in = in; }
看见没?它的构造函数还是InputStream .这是典型的”装饰者(或者叫包装者)“模式,它能给其他的InputStream提供额外的有用功能。
FilterInputStream 这个类非常的重要
有输入就会有输出咯,OutPutStream这个类决定了输出所要去往的目标,这些个目标有:
1)字节数组 ByteArrayOutputStream
在内存中创建缓存区,所有送往“流”的数据都会要放置在此缓冲区
2)文件 FileOutputStream
用户将信息写到文件中
3)管道 PipeStreamStream
任何写入其中的信息都会自动作为相关PipedInputStream的输出,实现“管道化”概念。
4)FilterOutputStream,跟FilterInputStream是一样的,也是一个典型的“装饰者”模式。
下面的一段代码简单简单的现实了FileInputStream 何FIleOutStream的用法
public void writeFile(String pathName, String fileName, InputStream in) { if (in == null) { return; } File file = new File(pathName, fileName); File parentFile = file.getParentFile(); if (parentFile != null && !parentFile.exists()) { parentFile.mkdirs(); } OutputStream out = null; int len = -1; byte[] buf = new byte[2048];//每次读取2K的字节到内存中 try { out = new FileOutputStream(file, false); while ((len = in.read(buf)) != -1) {//每次读取2K的字节到内存中 out.write(buf, 0, len);//将内存中的数据写到文件中 } out.flush(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { if (out != null) { out.close(); } } catch (IOException e) { System.out.println(e); } } } public InputStream readFile(String filePath, String fileName) { File file = new File(filePath, fileName); if (!file.exists()) { return null; } InputStream in = null; try { in = new FileInputStream(file); } catch (IOException e) { System.out.println(e); } return in; }
上面的代码只是最简单,最基本的数输入输出操作,但是呢,我们在I/O操作的时候,可能需要额外的增加一些动作或者控制,如定位输入流中的行号、增加缓存区,防止每次都进行实际的写操作等等,这些额外的功能当然可以通过继承的方式得到,但是继承的方式不好(这个就不用多说了吧,设计模式和effective java都反复的讲到 ),我们知道“装饰者”模式是一种替代继承方式的很好的策略,而在Java的I/O操作中,正式这种思想,其中所有装饰器的基类就是前面讲的FilterInput(Output)Stream咯。
但是呢,装饰者模式也有不好的地方,最显著的地方就是由于及其的灵活,导致产生了大量的对象(但是类的数目不多,我们可以自由的组合这些类,从而实现不同的功能),因此有的时候增加了代码的读复杂度。这也是Java i/o操作让人感觉不好用的原因。
1)通过FilterInputStream从InputStrem 读取数据
主要类型有:
DataInputStream 与DataOutputStream配合使用,我们可以从流中读取基本的数据类型如int,float,byte等等,对应的方法是reedInt() readFloat() readByte()
BufferedInputStream 可以防止每次读取都得进行实际的读操作,取而代之的是使用缓冲区,BufferedInputStream对外提供滑动读取的功能实现,通过预先读入一整段原始输入流数据至缓冲区中,而外界对BufferedInputStream的读取操作实际上是在缓冲区上进行,如果读取的数据超过了缓冲区的范围,那么BufferedInputStream负责重新从原始输入流中载入下一截数据填充缓冲区,然后外界继续通过缓冲区进行数据读取。这样的设计的好处是:避免了大量的磁盘IO,因为原始的InputStream类实现的read是即时读取的,即每一次读取都会是一次磁盘IO操作(哪怕只读取了1个字节的数据)。
LineNumberInputStream 可以用于跟踪六种的行号,但是这个其实没太大用处了,因为其他形式的inputstream得到行号的操作还是比较简单的
PushbackInputStream 这个用起来感觉更少了,不说了
2)通过FilterOutStream向OutputStream中写入
主要类型有:
DataOutputStream 通过它可以向流中写入基本的数据类型。
PrintStream 它最初的目的便是为了以可视化格式打印所有的基本数据类型和Sreing对象,有两个重要的方法print()和println() 它与DataOutStream的区别是(都是处理基本数据类型嘛),PrintStream处理数据的显示,而后者处理的是数据的存储。
BufferedOutputStream 避免每次发送数据时都要进行实际的写操作。使用flush()方法可以清空缓冲区。
ps:我在工作的过程中,碰到需要计算输入流大小。下面是我原始的代码
//下面这种做法效率是很低的,不可采纳。我之所以这么写,也是因为其他操作会在我们的项目中有问题。 private int getLength(InputStream in) { if(in == null) { return 0; } int tatal =0 ; while(in.read()!=-1) { total ++; } return total; }
后来我又在将上面的InputStream写到数据库中,使用的是下面的代码
1. PreparedStatement ps = conn.prepareStatement("INSERT INTO images VALUES (?, ?)"); 2. ps.setString(1, file.getName()); 3. ps.setBinaryStream(2, fis,getlength(fis));
发现第3行报错,报出错大致意思是需要读的是getLength大小的数据快,但是实际读的是0。看了源码,才知道在ps.setBinaryStream(2, fis,getlength(fis))这个方法里发现又对文件进行了read操作,而之前我们为了得到输入流的长度,已经read了一次,即已经到了输入流的末尾位置了,此时再度的话,直接就结束了,即in.read()==-1;所以我出现实际读的是-1。那么怎么处理呢,下面是改进后的代码
private int getLength(InputStream in) { if(in == null) { return 0; } in.mark(0)//1.先设置标记为mark=0; int tatal =0 ; while(in.read()!=-1) { total ++; } in.reset();//reset方法主要是将position设置为mark的值,本例中就是将position置为0。但是看了源码之后,发现FileInputStream不支持reset方法。 return total; }
经过上述的操作后,我们就可以重新对一个输入流进行正常的read操作了。