第12张 流于文件
1. 流
字节序列的源和目的地可以是文件,也可以是网络连接甚至是内存块,但存储在文件中的信息和从网络连接中接受的信息,从本质上来说,处理方法的相同的
一个可以读取字节序列的对象被称为输入流,一个可以写入字节序列的对象被称为输出流。
InputStream和OutputStream以字节为单位进行流处理
Reader和Writer专门用来处理Unicode字符,它们的读写操作都是基于双字节的Unicode代码单元,而不是基于单字节
读取的时候,如果读到输入源的末尾,将返回-1
System.in是一个预定的InputStream的子类对象,使用它可以从键盘读取信息
无论是read还是write方法都能阻塞一个线程只到字节被真正地读取或者写入。这意味着如果线程不能立即被读取或者写入,Java就会挂起这个调用的线程
Available方法能够检查目前可以读取的字节数
当完成了一个流的读取或者写入操作时,就应该调用close方法将它关闭,这样可以释放流所占用的操作系统资源。关闭一个流也可以刷新(flush)输出流占用的缓存区,即临时存储在缓存区中等待形成较大的数据包后再发送的那些字符,此时将它们发送出去。特别要注意的是,如果不关闭文件,最后一个字节包可能永远也不会被发送
2. 完整的流结构
四个基础抽象类:InputStream OutputStream Reader Writer 。不用创建这些类型的对象,而是其他方法返回这些对象。
InputStream和OutputStream类仅仅读取和写入单个的字节和字节数组,它们没有读取和写入字符串和字符的方法
四个新接口:Closeable Flushable Readable Appendable .
四个基础抽象类都实现了Closrable接口
OutputStream和Writer都实现了Flushable接口
只有Writer实现Appendable接口
l 流过滤器的分层
FileInputStream和FileOutputStream能够把输入和输出流与磁盘文件关联起来,这些类只在字节层次上支持读写,意味着只能从对象中读取字节和字节数组
DateInputStream和DateOutputStream 进行java数据类型的读与写
PushbackInputStream 可以查看下一字节是否是想要的值
Java划分的两种职责:一些流可以从文件以及其它地方接收字节。另一些流可以将字节组合成更加有用的数据类型。
Java程序员采用将一个已经存在的流传递给另一个流的构造器方法,将这两种流结合起来,结合后的流被称为过滤流
流封装的目的:必须将流构造器连续分层,直到能够使用所有的方法为止
能够将流进行混合与匹配是Java非常使用的特性之一
可以通过System.getProperty(“user.dir”)来获得当前目录的相对路径
l 数据流
尽管产生的输出不适合人们阅读,但是每一个指定类型的值所需要的空间都是相同的,而且这样也能更加快速的读取到它们
l 随即存取文件流
RandomAccessFile流类能够在文件的任何位置查找或者写入数据。它同时实现了DataInput和DataOutput接口
打开一个随即存取文件,要么进行只读操作,要么进行读写操作,可以通过构造器的第二个参数带入”r”和”rw”指定
随即读取文件同时提供了一个文件指针。文件指针总是指向下一条要进行读写操作记录的位置。Seek方法将文件指针设定在文件内部的任一字节位置。getFilePoint方法返回的是文件指针的当前位置
让RandomAccessFile 同时实现DataInput和DataOutput 的好处在于,可以使用或者编写那些参数类型为DataInput和DataOutput接口的方法
l 文本流
Java提供了一套流过滤器作为Unicode字符码和本地操作系统使用的字符码间的桥梁.所有这些类都从抽象类Reader和Writer类派生出来
经常需要将一个读取器或写入器同文件联系在一起,Java为此提供了两个很方便的类,FileReader和FileWriter
l 字符集
字符集给出了双字节Unicode码序列与在本地字符编码中采用的字节序列
字符集的名称不分大小写
为了找出在特定的实现中可用的字符集,可以调用静态的availableCharsets
l 文本输出
进行文本输出时,应该采用PrintWriter,PrintWriter可以以文本格式打印字符串和数值
一个PrintWriter对象仅仅提供了一些输出方法,但是并没有定义目的地,所以一个PrintWriter必须与一个目标Writer相结合,这个目标Writer可以是文件也可以使流
写入 print writer 可以使用Sysout.out中的print和println方法,也可以使用这些方法来打印数值、字符、布尔值、字符串和对象
Println方法自动添加目标系统的正确行结束字符
如果Writer被设为自动刷新模式,任何时候调用println,缓冲区内的所有字符都会被送到其目的地。默认情况下,自动刷新不开启。可以使用PrintWriter(Writer,Boolean)构造器并使用适当的布尔值作为第二个参数来开启或关闭自动刷新
l 文本输入
当以二进制格式写入数据时,应当使用DataOutputStream
当以文本格式写入数据时,应该使用PrintWriter
处理文本输入,应该使用BufferedReader,这个类中的readLine方法,能以行的方式读取文本,并需要将一个缓冲的读取器同一个输入源结合起来
要从文本中读取一个数字,首先需要读取一个字符串并转换它。但是记住此时有必要抛出一个异常,否则数据类型类很有可能会因为格式不正确产生异常
l
3. ZIP文件流
Zip文件的类在javca.util.zip中而不在java.io中
每个ZIP文件都有头文件,其中包含了诸如文件名和使用的压缩方法等信息
通过将一个FileInputStream传给ZipInputStream构造器,可以用ZipInputStream类读取一个ZIP文件
ZipInputStream类有getNextEntry和closeEntry方法
4. 流的使用
l 分隔符输出
l 字符串记号处理和带分隔符的文本
StringTokenizer类分割用某个符号分开的字符串
思路:将字符串记号处理器对象附在字符串上,当构造字符串记号处理器对象时,要指定哪些字符是分隔符。一旦构造好了一个字符串记号处理器,就可以使用它的方法从字符串中快速找出记号。nextToken 和 hasMoreTokens方法
StringTokenizer的替代方案是String类的slipt方法
l 读取带分隔符的输入
l StringBulider类
在处理输入时,经常需要由单个字符或者Unicode代码单元构造字符串。但是用字符串拼接效率太低,而且每次向字符串后面添加字符时,字符串对象就需要寻找新的内存空间来存储更大的字符串,这样很浪费时间,同时又要一次一次的重新分配空间,很浪费空间。此时使用StringBuilder类,它的工作过程更像ArrayList。它管理者一个可以根据需求增长或缩短的字符数组char[]。
StringBuffered类是StringBuilder类的前身,StringBuffere效率稍微低一点,但StringBuffere可以允许多线程进行增加或移除字符操作,而如果所有的字符串编辑出现在一个线程中,应该改用StringBuilder
l 随机存取流
使用RandomAccessFile类来实现随机存取方法,该方法可以读回任意位置的信息,并且读取任意一条记录所花费的时间是相同的
如果在读字符串的时候使用while循环,并且要一次一次的读数据添加到字符串中的时候,我们最好使用StringBulider类的append方法,这样不仅能提高速度,也能节省空间
一般要在某个文件中写入定常的数据,我们使用DateOutput,但是DataOutput只能写基本数据类型,对于String类型或者其他的只能使用自己写的方法强制让它写一个定常的数据到流中
l
l
l
l
5. 对象流
l 存储可变类型的对象
要存储对象数据,首先需要打开一个ObjectOutputStream对象,通过调用其中的writeObject方法存储对象
要读取对象,首先要取得一个ObjectInputStream对象,然后调用readObject方法取回对象
读取对象时,必须要小心地跟踪储存的对象的数量、他们的顺序以及他们的类型。对于readObject的每一次调用都会读取类型为Object的另一个对象,需要将它转换为正确的类型。
只能使用writeObject/readObject方法对对象进行读写操作,对象内部的值会自动地进行保存
对于任何需要在对象流中存储和恢复的类需要做一个修改,该类必须实现Serializable接口。Serializable接口没有任何方法,所以不需要对自己的类进行任何的修改
l 理解对象序列化文件格式
对象序列化使用一种特殊的文件格式来存储对象
对象被存储时该对象的类也必须同时被存储。类的描述包括:
1) 类的名称
2) 唯一的版本序列ID,这是数据域类型和方法签名的指纹
3) 一系列用来对序列化方法加以描述的标志
4) 对数据域的描述
Java从以下途径获得指纹:
1) 首先用标准的方式对类、超类、接口、域类型和方法签名的描述进行排序
2) 然后对那些数据应用一种所谓的“安全散列算法”。它是一种可以从大块的信息中获取指纹的快速的算法。
Java使用检查指纹类的方法来避免以下情况:一个对象被保存到磁盘文件。后来类的设计者做了一些改变,此时磁盘上的数据布局不再与内存中的数据布局相符合。于是Java使用指纹检查,以保证类的定义在恢复一个对象时没有改变
Externalizable接口,提供了定制的读/写的方法,这些方法负责输出该类的实例域。
l 保存对象引用问题的解决
持久性:希望磁盘上的对象布局与内存中的对象布局完全一致,在面向对象的程序设计中,称其为持久性
对象序列化机制:
1) 所有保存到磁盘的对象都有一个序列号
2) 当向磁盘存储一个对象时,先检查相同的对象是否已经保存
3) 如果已经存储过,只需要写入“与已经存储的对象具有序列号X”。如果没有,保存所有数据。
对象并不需要按照特定的顺序存储
当一个对象流被使用时,所有的处理都是自动的。对象流分配序列号并且保持跟踪复制的对象。
对象序列化的重要应用:通过网络连接传输对象集合到另一台电脑,就像原始内存地址在文件中无意义一样,原始的内存地址在不同的处理器间也是毫无意义的
l 理解对象引用的输出格式
所有的对象以及类描述符在保存到输出文件时,都会获得一个序列号,这个处理流程被称为序列化
任何给定类的完整的类描述符只会出现一次,后面的描述符只是简单的引用它
通常,不必知道准确的文件格式,需要记住的是:
对象流输出包含所有对象的类型和数据域
每个对象都会分配一个序列号
重复出现的同一个对象会存储为其序列号的引用
l 修改默认的序列化机制
有些数据域不应该被序列化,Java使用一个简单的机制来避免这些域被序列化,即使用关键字transient标注他们。还需要将那些属于非序列化类中的域标注为transient。当对象被序列化时,标注为transient的域是会跳过的
序列化机制为特殊的类提供了一种方法,用来对默认的读、写行为增加验证或者其他合适的动作。在writeObject中,首先调用defaultWriteObject方法来写入对象描述符、String域和状态。然后通过标准的DataOutput调用来写入不应该被序列化的部分
readObject和writeObject方法只需要保存和加载自己的数据域。它们不关心超类的数据以及其他类的信息。
类可以定义自己的机制,而不需要让序列化机制存储和恢复对象数据,要做到这个类必须实现Externalizable接口,这个接口有两个方法readExternal() writeExternal()。这些方法将负责整个对象(包括超类数据)的保存和恢复
序列化有些慢,因为虚拟机必须弄清每个对象的结构,如果关心性能,并且需要读写大量的特定的类对象,就应该考虑使用Externaizable接口
l 单元素与类型安全枚举序列化
此时我们关注的是那些假设为唯一的对象进行序列化和逆序列化的过程
要解决类中有final定义的参数的问题,我们使用readResolve方法,如果该方法被定义了,它将在对象被序列化后被调用。它必须返回一个对象,也就是readObject方法返回的值。
l 版本
支持大规模的对象集合,用户可通过对writeObject的一次调用,将整个对象写入流中。程序的一个用户可以将输出的文件传给另一个用户,只要那个用户也有该程序的拷贝,小横须就可以用过readObject载入整个文档。
看似不可能,因为如果类的定义发生了任何形式的变化,其SHA指纹也会改变。
但是我们可是使用一个独立的程序serialver来获得该类的早期版本的值,所有的后续版本的类必须把seriaVersionUID常量和原来的指纹定义成一样的。
当一个类具有一个名为seriaVersionUID 的静态数据成员时,它将不会再次计算指纹,而是使用那个值
一旦那个静态数据成员放入类中,通过序列化系统就可以读取那个类的不同版本的对象。
处理数据域发生变化的状况:如果流内的对象有当前版本不存在的数据,那么对象流会忽略该数据。加入当前版本拥有的数据域在对象流中不存在,则增加的域会设置为默认值。最后由类的设计者在readObject方法中增加代码来实现版本兼容,或者确保方法具有足够的健壮性来处理那些Null数据
l 使用序列化机制进行克隆
用法:提供一种简单的方法来克隆一个对象,只要被提供的类是序列化的
要克隆一个对象,只需要将对象序列化到输出流中,然后读回即可。其结果是一个新的对已存在对象进行了深拷贝的对象
这个方法尽管它很高明,但是比显式的构造一个新的对象,然后拷贝或者克隆数据域的克隆方法要慢很多
6. 文件管理
流类关注的是文件的内容,而File类关注的是文件在磁盘上的存储
File类仅仅有最少的公共的操作方法
用一个File对象创建文件,需要使用一个流类的构造器或者使用由File类提供的creatNewFile方法。只有当前不存在指定名字的文件时,creatNewFile方法才会创建文件,而且返回boolean值表示是否成功
File类中的exists方法告诉我们是否存在指定名字的文件
一个File对象即可表示文件,也可表示路径,使用isDirectory和isFile方法来判断文件对象表示的是文件还是路径
创建路径使用 mkdir方法
子目录的处理:
如果在Windows中构造File对象时,geiAbsolutePath方法将返回一个包括正斜杠的文件名(这是在UNIX中使用的),如果使用getCanonicalPath方法,将使用反斜杠。最好使用File类的一个被称为separator方法,该域保存着与当前目录分隔符的相关信息
要定义实现FilenameFilter接口,需要定义一个称为accept的方法
7. 新的IO
l 内存映射文件:
大多数操作系统可以利用虚拟内存将文件或文件的一段区域映射到内存中
内存映射比使用缓冲区顺序输入要快得多,比使用RandomAccessFile 还要快。
如果使用随机访问,内存映射文件性能的提高是相当可观的。然后对于顺序读取大小适中的文件,就可以不使用内存映射
使用java.nio包建立内存映射:
首先,为文件获取通道。通道是对于磁盘文件的抽象,允许访问操作系统的特性。通过调用增加到FileInputStream/FileOutpurstream和RandonAccessFile类中的getChannel方法就可以获得通道
然后,通过调用FileChannel类的map方法可以获得MapperByteBuffer,可以指定需要进行映射的文件区域和映射模式,三种映射模式 :
1) FileChannel.MapMode.READ_ONLY:结果缓冲区是只读的
2) FileChannel.MapMode.READ_WRITE:结果缓冲区是可写的,这些改变会及时写回文件中。多个程序同时对同一文件进行映射的具体行为以来操作系统
3) FileChannel.MapMode.PRIVATE:结果缓冲区可写,但是任何改变都是私有的,仅仅对该缓冲区有效,并不会写入文件中
一旦获得了缓冲区,可以用ByteBuffer类和Buffer超类读写数据
缓冲区支持顺序和随机数据访问。缓冲区有一个被get和put操作使用的位置
l 缓冲区数据结构:
Buffer类是一个抽象类,包含子类:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer。使用最多的是ByteBuffer和CharBuffer
一个缓冲区具有:
1) 一个绝不会改变的容量
2) 一个下一数值读取或写入的位置
3) 一个限制,超出这个限制的读写是无意义的
4) 可选的,一个重复进行读写操作的标志
一个缓冲区的主要目标就是“写,然后读”循环。
调用flip来设置目前的位置,并将posion置到 0 ,现在当保持的(remaining)方法(将返回limit_position)是正值的时候保持调用get,当读完缓冲区内的所有数值后,调用clear来为缓冲区的下次读写做准备
l 文件锁定
文件锁控制对文件或者文件的一部分字节的访问。文件锁在不同的操作系统上区别很大
应用程序中使用文件锁是不普遍的,因为数据库系统已经有解决并发存取问题的机制。
要锁住一个文件,调用FileChannel类的lock或tryLock方法,第一个调用一直处于阻塞状态直到该锁可用。第二个调用立刻返回,返回该锁或者是null。还可以使用多参数的锁
文件锁是与系统相关的:
1) 在一些系统中,文件锁仅仅是建议性的。因此如果一个应用程序未能获得锁,它也可以对文件进行读写操作,尽管其他应用程序已经锁住文件
2) 在一些系统中,不能同步的锁住一个文件并把它映射到内存中
3) 文件锁是整个Java虚拟机特有的。如果两个程序使用同一个虚拟机执行,则他们不能对同一个文件获得锁
4) 在某些系统上,关闭通道会释放Java虚拟机拥有的文件上的全部锁
5) 对网络文件系统的文件锁是和系统高度相关的,因此应避免这么做
l
l
l
l
l
8. 正则表达式
正则表达式的规则
1) 一个字符类是可选的字符集和,用括号括起来,[Jj] [0-9] [^0-9]
2) 有许多预先定义的字符类,/d(数字) /p{Sc} (Unicode货币符号)
3) 大多数字符都和自己匹配
4) 符号符合任何字符
5) 使用/做转义符
6) ^和$分别匹配行首和行尾
7) 如果XY是正则表达式,则XY意味着“任何符合X后面跟符合Y的字符”,X|Y意味着“任何符合X或者符合Y的字符”。
8) 可以对任何表达式使用量词:X+(1或者多个) X*(0个或者多个) X?(1个或者1个)
9) 在默认时,量词是保证整体匹配成功情况下的最大可能数目。也可以修改后缀,?(勉强的,即不足的匹配——最小匹配数目),+(积极的,即贪心匹配—最大匹配数目,可能导致整体匹配失败
10) 可以使用组来定义子表达式。用()来定义组
测试一个字符串是否与某个正则表达式匹配的方法:
1) 使用一个表达正则表达式的字符串来构造Pattern对象
2) 从Pattern中获取一个Matcher对象
3) 调用Matcher对象的matches方法
正则表达式的语法