Thinking In Java
- 相关推荐
1 File类
File(文件)类这个名字有一定的误导性;我们可能会认为它指代的是文件,实际上却并非如此。它既能代表一个特定文件的名称,又能代表一个目录下的一组文件的名称。如果它指的是一个文件集,我们就可以对此集合调用list()方法,这个方法会返回一个字符数组。我们很容易就可以理解返回的是一个数组而不是某个更具灵活性的类容器,因为元素的个数是固定的,所以如果我们想取得不同的目录列表,只需要再创建一个不同的File对象就可以了。实际上,FilePath(文件路径)对这个类来说是个更好的名字。File类不仅仅只代表存在的文件或目录。也可以用File对象来创建新的目录或尚不存在的整个目录路径。我们还可以查看文件的特性(如:大小,最后修改日期,读写),检查某个File对象代表的是一个文件还是一个目录,并可以删除文件。
2 输入和输出
编程语言的I/O类库中常使用流这个抽象概念,它代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。”流“屏蔽了实际的I/O设备中处理数据的细节。Java类库中的I/O类分成输入和输出两部分,可以在JDK文档里的类层次结构中查看到。通过继承,任何自Inputstream或Reader派生而来的类都含有名为read()的基本方法,用于读取单个字节或者字节数组。同样,任何自Outputstream或Writer派生而来的类都含有名为write()的基本方法,用于写单个字节或者字节数组。但是,我们通常不会用到这些方法,它们之所以存在是因为别的类可以使用它们,以便提供更有用的接口。因此,我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能(这是装饰器设计模式)。实际上,Java中”流“类库让人迷惑的主要原因就在于:创建单一的结果流,却需要创建多个对象。
2.1 InputStream类型
InputStream的作用是用来表示那些从不同数据源产生输入的类。这些数据源包括:
字节数组。String对象。文件。“管道”,工作方式与实际管道相似,即,从一端输入,从另一端输出。一个有其他种类的流组成的序列,以便我们可以将它们收集合并到一个流内。其他数据源,如Internet连接等。
每一种数据源都有相应的InputStream子类。另外,FilterInputStream也属于一种InputStream,为“装饰器”(decorator)类提供基类,其中,“装饰器”类可以把属性或有用的接口与输入流连接在一起。
2.2 OutputStream类型
该类别的类决定了输出所要去往的目标:字节数组(但不是String,不过你当然可以用字节数组自己创建)、文件或管道。
另外,FilterOutputStream为“装饰器”类提供了一个基类,“装饰器”类把属性或者有用的接口与输出流连接了起来。
3 Reader和Writer
Java 1.1对基本的I/O流类库进行了重大的修改。当我们初次看见Reader和Writer类时,可能会以为这是两个用来替代InputStream和OutputStream的类;但实际上并非如此。尽管一些原始的“流”类库不再使用(如果使用它们,则会收到编译器的警告信息),但是InputStream和OutputStream在以面向字节形式的I/O中仍可以提供极具价值的功能,Reader和Writer则提供兼容Unicode与面向字符的I/O功能。另外:
Java 1.1向InputStream和OutputStream继承层次结构中添加了一些新类,所以显然这两个类是不会被取代的。有时我们必须把来自于“字节”层次结构中的类和“字符”层次结构中的类结合起来使用。为了实现这个目的,要用到“适配器”(adapter)类:InputStreamReader可以把InputStream转换为Reader,而OutputStreamWriter可以把OutputStream转换为Writer.
设计Reader和Writer继承层次结构主要是为了国际化。老的I/O流继承层次结构仅支持8位字节流,并且不能很好的处理16为的Unicode。由于Unicode用于字符国际化(Java本身的char也是16位的Unicode),所以添加Reader和Writer继承层次结构就是为了在所有的I/O操作中都支持Unicode。另外,新类库的设计使得它的操作比旧类库更快。
4 自我独立的类:RandomAccessFile
RndomAccessFile适用于由大小已知的记录组成的文件,所以我们可以使用seek()将记录从一处转移到另一处,然后读取或者修改记录。文件中记录的大小不一定都相同,只要我们能够确定哪些记录有多大以及它们在文件中的位置即可。最初,我们可能难以相信RandomAccessFile不是InputStream或者OutputStream继承层次结构中的一部分。除了实现了DataInput和DataOutput接口(DataInputStream和DataOutputStream也实现了这两个接口)之外,它和这两个继承层次结构没有任何关联。它甚至不使用InputStream和OutputStream类中已有的任何功能。它是完全独立的类,从头开始编写其所有的方法(大多数都是本地的)。这么做是因为RandomAccessFile拥有和别的I/O类型本质不同的行为,因为我们可以在一个文件内向前和向后移动。在任何情况下,它都是自我独立的,直接从Object派生而来。从本质上来说,RandomAccessFile的工作方式类似于把DataInputStream和DataOutStream组合起来使用,还添加了一些方法。其中方法getFilePointer()用于查找当前所处的文件位置,seek()用于在文件內移至新的位置,length()用于判断文件的最大尺寸。另外,其构造器还需要第二个参数(和C中的fopen()相同)用来指示我们只是“随机读”(r)还是“既读又写”(rw)。它并不支持只写文件,这表明RandomAccessFile若是从DataInputStream继承而来也可能会运行得很好。只有RandomAccessFile支持搜寻方法,并且只适用于文件。在JDK1.4中,RnadomAccessFile的大多数功能(但不是全部)由nio存储映射文件所取代。
5 标准I/O
标准I/O这个术语参考的是Unix中“程序所使用的单一信息流”这个概念。程序的所有输入都可以来自于标准输入,它的所有输出也都可以发送到标准输出,以及所有的错误信息都可以发送到标准错误。标准I/O的意义在于:我们可以很容易地把程序串联起来,一个程序的标准输出可以成为另一个程序的标准输入。按照标准I/O模型,Java提供了System.in、System.out和System.err。其中System.out已经事先被包装成了printStream对象。System.err同样也是PrintStream,但System.in却是一个没有被包装过的未经加工的InputStream。Java的System类提供了一些简单的静态方法调用,以允许我们对标准输入、输出和错误I/O流进行重定向:setIn(InputStream)setOn(InputStream)setErr(InputStream)
6 新I/O
6.1 内存映射文件
内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件。有了内存映射文件,我们就可以假定整个文件都放在内存中,而且可以完全把它当作非常大的数组来访问。这种方法极大的简化了用于修改文件的代码。
6.2 文件加锁
JDK 1.4引入了文件加锁机制,它允许我们同步访问某个作为共享资源的文件。不过,竞争同一文件的两个线程可能在不同的Java虚拟机上;或者一个是Java线程,另一个是操作系统中其他的某个本地线程。文件锁对其他的操作系统进程是可见的,因为Java的文件加锁直接映射到了本地操作系统的加锁工具。通过对FileChannel调用tryLock()或lock(),就可以获得整个文件的FileLock。(Socket-Channel、DatagramChannel和ServerSocketChannel不需要加锁,因为它们是从单进程实体继承而来;我们通常不在两个进程之间共享网络socket。)tryLock()是非阻塞式的,它设法获取锁,但是如果不能获得(当其他一些进程已经拥有相同的锁,并且不共相时),它将直接从方法调用返回。lock()则是阻塞式的,它要阻塞进程直至锁可以获得,或调用lock()的县城中断,或调用lock的通道关闭。使用FileLock.release()可以释放锁。也可以使用如下方法对文件的一部分上锁:
tryLock(longposition, longsize, boolean shared)
或
lock(longposition, longsize, boolean shared)
其中,加锁的区域由size-position决定。第三个参数指定是否是共享锁。尽管无参数的加锁方法将根据文件尺寸的变化而变化,但是具有固定尺寸的锁不随文件尺寸的变化而变化。如果你获得了某一区域(从position到position+size)上的锁,当文件增大超出position+size时,那么在position+size之外的部分不会被锁定。无参数的加锁方法会对整个文件进行加锁,甚至文件变大后也是如此。对独占锁或者共享锁的支持必须由底层的操作系统提供。如果操作系统不支持共享锁并为每个请求都创建一个锁,那么他就会使用独占锁。锁的类型(共享或独占)可以通过FileLock.isShared()进行查询。
7 对象序列化
对于Serializable对象,对象完全以它存储的二进制位为基础来构造,而不调用构造器。而对于一个Externalizable对象,所有普通的构造函数都会被调用(包括在字段定义时的初始化),然后调用readExternal()。必须注意这一点----所有默认的构造器都会被调用,才能使Externalizable对象产生正确的行为。
7.1 transient(瞬时)关键字
当我们对序列化进行控制时,可能某个特定子对象不想让Java的序列化机制自动保存与恢复。如果子对象表示的是我们不希望将其序列化的敏感信息(如密码),通常就会面临这种情况。即使对象中的这些信息是private(私有)属性,一经序列化处理,人们就可以通过读取文件或者拦截网络传输的方式来访问到它。有一种办法可以防止对象的敏感部分被序列化,就是将类实现为Externalizable,在writeExternal()内部只对所需部分进行显示的序列化。然而,如果我们正在操作的是一个Serializable对象,那么所有序列化操作都会自动进行。为了能够予以控制,可以用transient(瞬时)关键字逐个字段的关闭序列化,它的意思是“不用麻烦你保存或恢复数据----我自己会处理的”。由于Externalizable对象在默认情况下不保存它们的任何字段,所以transient关键字只能和Serializable对象一起使用。
7.2 Externalizable的替代方法
我们可以实现Serializable接口,并添加(注意是“添加”,而非“覆盖”或者“实现”)名为writeObject()和readObject()的方法。这样一旦对象被序列化或者被反序列化还原,就会自动的分别调用这两个方法。也就是说,只要我们提供了这两个方法,就会使用它们而不是默认的序列化机制。这些方法必须具有准确的方法特征签名:
privatevoidwriteObject(ObjectOutputStream stream)throws IOException;
privatevoidreadObject(ObjectInputStream stream)throws IOException, ClassNotFoundException;
从设计的观点来看,现在事情变得真是不可思议。首先,我们可能回认为由于这些方法不是基类或者Serializable接口的一部分,所以应该在它们自己的接口中进行定义。但是注意他们被定义成了private,这意味着它们仅能被这个类的其他成员调用。然而,实际上我们并没有从这个类的其他方法中调用它们,而是ObjectOutputStream和ObjectInputStream对象的writeObject()和readObject()方法调用你的对象的writeObject()和readObject()方法。在接口中定义的所有东西都自动是public的,因此如果writeObject()和readObject()必须是private的,那么它们不会是接口的一部分。因为我们必须要完全遵循其方法特征签名,所以其效果就和实现了接口一样。在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,看看是否实现了它自己的writeObject()。如果是这样,就跳过正常的序列化过程并调用它的writeObject()。readObject()的情形与此相同。还有另外一个技巧。在你的writeObject()内部,可以调用defaultWriteObject()来选择执行默认的writeObject()。类似的,在readObject()内部,我们可以调用defaultReadObject()。尽管Class类时Serializable的,但它却不能按我们所期望的方式运行。static数据根本没有被序列化。
8 XML
对象序列化的一个重要限制是它只是Java的解决方案:只有Java程序才能反序列化这种对象。一种更具互操作性的解决方案是将数据转换为XML格式,这可以使其被各种各样的平台和语言使用。
9 Preferences
Preferences API与对象序列化相比,前者与对象持久性更密切,因为它可以自动存储和读取信息。不过,它只能用于小的、受限的数据集合----我们只能存储基本类型和字符串,并且每个字符串的存储长度不能超过8K(不是很小,但我们也并不想用它来创建任何重要的东西)。顾名思义,Preferences API用于存储和读取用户的偏好(preferences)以及程序配置项的设置。Preferences是一个键-值集合(类似映射),存储在一个节点层次结构中。尽管节点层次结构可用来创建更为复杂的结构,但通常是创建以你的类名命名的单一节点,然后将信息存储于其中。