《Java核心技术卷二》笔记(一)流与文件

时间:2024-11-28 15:03:55

一、流的概念

在Java中,可以提供字节序列的对象,或者可以接收字节序列的对象,都可以抽象成。系统中的文件,网络,内存这些设备都可以读入或者写入字节,自然也可以用流的方式来操作。能向程序中提供字节序列,即可以从其中读入字节序列,这样的对象显然就是输入流。相反的,能够接收程序送来的字节序列,也就是可以向其中写入字节序列,就是输出流

Java提供丰富的流类家族,实现了各种常用的输入输出操作功能,如文件、内存读写等。InputStream和OutputStream类分别是字节输入/输出流继承体系的基类。

字节流都是以字节为单位读写的,而Java中字符采用的是Unicode形式,一个Unicode元码是两个字节,不同字符的字节数目并不一致。有时候我们不关心字节与字符的转化,希望读写都按字符为单位进行,就可以使用字符流类家族,Reader和Writer是字符输入/输出流的基类。

二、流的基础方法

InputStream

    • abstract int read()  从输入源读取并返回一个字节,遇到输入源的结尾返回-1。执行时会阻塞。各个子类唯一必须覆盖的方法。
    • int available()  返回当前还可以读入的字节数量
    • close()    关闭流,释放系统资源。继承自Closeable接口。
    • int read(byte[] b)  读入一个字节数组,返回实际读入字节数,到达结尾返回-1。
    • int read(byte[] b, int off, int len)   读入一个字节数组,返回实际读入字节数,到达结尾返回-1。off为数组b中开始存放位置,len为读入最大长度。
    • long skip(long n)  跳过n个字节,返回实际跳过的字节数。
    • void mark(int readlimit)  在当前位置设置标记,调用reset()可以返回到前一处标记位置重新读入。如果从流中已经读入了超过readlimit个字节,可以忽略这个标记。
    • void reset()  返回到前一处标记位置重新读入。没有标记的话,没有影响。
    • boolean markSupported()  该流是否支持mark和reset功能。

OutputStream

    • abstract void write() 向目的地写入一个字节。执行时会阻塞。各个子类唯一必须覆盖的方法。
    • void flush()  使用缓冲时,将缓冲区的内容全部写入目的地。继承自Flushable接口。执行close()时也会保证冲刷缓冲区,即不调用flush(),也不调用close()可能导致最后的数据未能写出。
    • close()    关闭流,释放系统资源。继承自Closeable接口。
    • void write(byte[] b)  写出数组b中所有字符。
    • void write(byte[] b, int off, int len)  off为数组b中的开始位置,len为写出长度

Reader和Writer的大部分方法与InputStream和OutputStream类似,只不过参数类型从byte变成了char。

Reader  

    • int read()   返回一个Unicode码元(0-65535),或者碰到结尾返回-1
    • int read(char[] b)
    • abstract int read(char[] b,int off,int len)
    • int read(CharBuffer target)  继承自接口Readable
    • long skip(long n)
    • boolean ready()  下一次调用read()不阻塞则返回true,但返回false比一定会阻塞。
    • void mark(int readAheadLimit)
    • void reset()
    • boolean markSupported()
    • abstract void close()

Writer 

    • void write(int c)   写出一个码元
    • void write(char [] b)
    • abstract void write(char[] b, int off, int len)
    • void write(String str)
    • void write(String str, int off, int len)
    • Writer append(CharSequence csq)  继承自接口Appendable
    • Writer append(CharSequence csq, int start, int end)    继承自接口Appendable
    • Writer append(char c)  继承自接口Appendable
    • abstract void flush()
    • abstract void close()

各个子类中提供了更方便实用的方法,尽可能使用子类提供的那些方法。

三、完整流家族

InputStream

  • AudioInputStream
  • ByteArrayInputStream
  • FileInputStream
  • PipedInputStream  连接PipedOuputStream
  • FilterInputStream  过滤器流,包装另一个InputStream,并提供附加功能
    • BufferedInputStream   缓存
    • CheckedInputStream   校验相关
    • CipherInputStream   加密解密相关
    • DigestInputStream  计算摘要
    • InflaterInputStream  deflate格式解压
      • GZIPInputStream
      • ZipInputStream
        • JarInputStream
    • LineNumberInputStream     使用LineNumberReader替代
    • ProgressMonitorInputStream
    • PushbackInputStream
    • DataInputStream   支持java基本类型,实现接口DataInput
  • SequenceInputStream    连接多个InputStream
  • StringBufferInputStream   使用StringReader替代
  • ObjectInputStream    序列化相关,实现接口ObjectStreamConstants,ObjectInput(实现接口DataInput,AutoCloseable)
  • ......

OutputStream

  • ByteArrayOutputStream    写入内存字节数组
  • FileOutputStream
  • FilterOutputStream   过滤器流,包装另一个OutputStream,并提供附加功能
    • BufferedOutputStream
    • CheckedOutputStream     校验相关
    • CipherOutputStream    加密解密相关
    • DigestOutputStream  计算摘要相关
    • DeflaterOutputStream   deflate格式压缩
      • GZIPOutputStream
      • ZipOutputStream
        • JarOutputStream
    • PrintStream     实现了Appendable接口,还提供print系列方法
    • DataOutputStream  支持Java基本类型,实现了DataOuput接口
  • PipedOutputStream   连接PipedInputStream
  • ObjectOutputStream    序列化相关,实现接口ObjectStreamConstants,ObjectOutput(实现接口DataOutput,AutoCloseable)
  • ......

Reader

  • BufferedReader    包装类
    • LineNumberReader
  • CharArrayReader    从字符数组读入字符
  • FilterReader
    • PushbackReader
  • InputStreamReader   包装类,将字节流包装为字符流

    • FileReader
  • PipedReader    连接PipedWriter
  • StringReader     从字符串读入字符
  • ......

Writer

  • BufferedWriter
  • CharArrayWriter
  • FilterWriter
  • OutputStreamWriter  包装类,将字符流转化为字节流
    • FileWriter
  • PipedWriter
  • PrintWriter       提供printxxx(xxx)方法,print方法不抛出异常,可以调用checkError方法查看流是否出现了错误。
  • StringWriter
  • ......

接口和相关类

接口 AutoCloseable  声明void close()方法。 try-with-resource结构自动调用close()方法,可以抛出任何异常

    • Closeable     只抛出IOException异常
      • InputStream
      • OutputStream
      • Reader
      • Writer

接口 Flushable  声明void flush()方法

    • OutputStream
    • Writer

接口 Readable  声明int read(CharBuffer cb)方法

    • Reader
    • CharBuffer

接口 Appendable  声明Appendable append(xxx)方法

    • Writer
    • PrintStream
    • CharBuffer
    • AbstractStringBuilder
      • StringBuffer
      • StringBuilder

接口 DataInput  声明了xxx readxxx()方法

    • ObjectInput    声明与InputStream类似方法
      • ObjectInputStream
    • DataInputStream
    • RandomAccessFile  

接口 DataOutput  声明了void writexxx(xxxx)方法

    • ObjectOutput    声明与OutputStream类似方法
      • ObjectOutputStream
    • DataOutputStream
    • RandomAccessFile

DataOutput分别提供了写出字符串的方法。

  • void writeChars(String s)   通用情况,直接写出Unicode码元。
  • void writeUTF(String s)    写出为修改过的UTF-8编码格式(仅Java自己支持),必须使用DataInput的String readUTF()方法读取。

接口 CharSequence  声明了char序列的基本方法length,charAt...

    • CharBuffer
    • String
    • AbstractStringBuilder
      • StringBuffer
      • StringBuilder

下面是两个缓冲区类,关于缓冲区的详述说明可以看下一篇文章:http://www.cnblogs.com/pixy/p/4782272.html

java.nio.CharBuffer    抽象类,通过warp方法包装码元数组实例化对象。字符码元缓冲区,支持顺序或随机读写,表示内存缓冲区或内存映象文件

    • char[] array()    返回该缓冲区管理的码元数组
    • static CharBuffer wrap(char[] array)  将码元数组包装成字符缓冲区
    • static CharBuffer warp(char[] array, int offset, int length)  将码元数组的一部分包装成缓冲区
    • static CharBuffer allocate(int capacity)
    • String toString()     返回缓冲区码元构成的字符串
    • xxx get()/put(char)/get(char[] ...)/put(char[] ...)/put(String...)/put(CharBuffer) 向缓冲区读写内容,并自动移动读写位
    • CharBuffer append(CharSequence...)/append(char)   同上
    • xxx get(int index)/charAt(int index)/put(int index, char char)    通过index随机访问内容
    • ByteBuffer asReadOnlyBuffer()

java.nio.ByteBuffer    抽象类,通过allocate或warp方法实例化对象

    • byte[] array()  返回该缓冲区管理的字节数组
    • static ByteBuffer wrap(byte[] bytes)  将字节数组包装成字节缓冲区
    • static ByteBuffer warp(byte[] bytes, int offset, int length)  将字节数组的一部分包装成字节缓冲区
    • static ByteBuffer allocate(int capacity)
    • xxx get()/put(byte)/get(byte[]...)/put(byte[]...)/put(ByteBuffer)/getXXX()/putXXX(xxx)...  向缓冲区读写内容,并自动移动读写位置
    • xxx get(int index)/put(int index, byte)/getXXX(int index)/putXXX(int index, xxx)...    通过index随机访问内容
    • CharBuffer asChar/Short/Int/Long/Float/DoubleBuffer()    将字节缓冲区包装成其他类型缓冲区
    • ByteOrder order()     设置字节顺序(ByteOrder.BIG_ENDIAN|LITTLE_ENDIAN)
    • ByteBuffer order(ByteOrder order)   设置字节顺序
    • ByteBuffer asReadOnlyBuffer()

  

java.lang.StringBuffer

java.lang.StringBuilder

java.lang.String

四、流的使用

1.使用单个流对象

基本用法是使用构造器或其他方法获得一个流对象。然后使用流对象的方法读入或者写出字节或字符。

2.多个流对象包装嵌套

Java提供了很多过滤器流,过滤器流可以灵活的包装其他的流对象,为其提供额外的功能。流可以无限制的嵌套包装,以达到预期的效果。

文件字节流写出+按类型写出

  OutputStream is=new FileOutputStream("test.txt", true);
  DataOutputStream dis=new DataOutputStream(is);
  dis.writeDouble(3.14);
  dis.close();

文件字节流读入+转化为字符流+缓冲并按行读入字符串

  InputStreamReader ir=new InputStreamReader(new FileInputStream("test.txt"), "UTF-8");
  BufferedReader br=new BufferedReader(ir);
  br.readLine();
  br.close();

3.文件输入输出操作

二进制数据文件   数据文件是将各种数据在内存中的字节序列直接存储到文件中,效率很高。直接使用FileInputStream,FileOutputStream读写字节序列。使用DataInputStream和DataOutputStream可以更方便的读写基本类型数据。

字符文件   但是很多时候为方便人阅读,通常会将内存中Unicode字符按某种本地字符集(GBK,UTF-8等)编码,将编码后得到字节序列存储到文件中(可以按字符显示)。读入时还需要将字节序列解码为Unicode字符。

(1)字符流与字节流转化(字符编码/解码)

使用InputStreamReader(解码)和OutputStreamWriter(编码)可以自动将输入字节流解码成输入字符流,或者是把输出字符流编码成输出字节流。按某种编码得到的输出字节流逐字节写入的文件就是该编码格式的字符文件。 

    OutputStreamWriter oswriter=new OutputStreamWriter(new FileOutputStream("test.txt"),"UTF-8");
    oswriter.write("asdfdf");
oswriter.close();

(2)字符文本输出

PrintWriter类似于System.out,提供了一组print方法,print方法不会抛出异常,需要调用checkError方法检查流是否出现异常。

    PrintWriter pw=new PrintWriter(new FileWriter("test.txt"), true);   //第2个参数表示是否自动冲刷缓冲区,默认为false。
    PrintWriter pw1=new PrintWriter("test.txt"); //可以直接指定输出的文件,而不用创建文件流

PrintStream类也支持print系列方法,即可以输出字节也可以输出字符 ,是System.out,System.err所属类型。

(3)字符文本读入

BufferedReader可以按行读取。

Scanner是一个工具类,类似于DataInputStream,提供了按行按类型等读入数据的方法,Scanner可以使用Readable,InputStream,File,String等多种对象构造(内部都会转化为Readable对象)。

    Scanner sc=new Scanner(new FileInputStream("test.txt"));
    sc.nextLine();
    sc.nextDouble();

随机访问文件读写(二进制数据文件)

RandomAccessFile类实现了DataInput和DataOutput接口,即可以读入也可以写出,也可以在指定位置读入或写出字节数据。如果存储的数据格式大小一致,可以精确将文件指针移动到指定数据记录位置随机读写,效率非常高。

RandomAccessFile

    • void seek(long n)   移动文件指针到指定位置
    • void getFilePointer()   返回文件指针所在位置
    • long length()   文件总字节数
    • readXXX()/writeXXX(),,,
  RandomAccessFile in=new RandomAccessFile("test.txt","rw");   //r:只读, "rw":可读可写,"rws","rwd"
  in.seek(1000);
  in.writeDouble(3.14);
  in.seek(2000);
  double d=in.readDouble();
  in.close();

其他相关的问题

  换行符: System.getProperty("line.separator");   windows默认为"\r\n",   UNIX默认为"\n"

  相对路径解析:相对路径都是基于 System.getProperty("user.dir"),默认为程序的启动路径。

  路径内部分隔符:java.io.File.separator,  windows为"\\"(转义)或"/", Unix为"/"

  路径间分隔符(如Path环境变量):java.io.File.pathSepparator,   windows为";", Unix为":"

   Java中,所有值都是高位在前的方式写出,java数据文件与平台无关。

字符集

字符集建立了Java内部两个字节Unicode码元序列与本地字符编码的字节序列之间的映射。使用字符集可以在本地字符编码和Unicode编码之间进行转化。每个字符集有一个在IANA(字符集注册中心)注册的官方名字,还可能有若干个别名。Java中java.nio.Charset类实现了字符集相关的功能。

Charset

    • static Map<String,Charset>  availableCharsets()  列出本Java实现中支持的所有字符集。字符集可分为所有Java必须实现的(US-ASCII,ISO-8859-1,UTF-8,UTF-16,UTF-16BE,UTF-16LE),JDK默认安装的,非欧洲语言扩展的。
    • static Charset forName(String name)  根据官方名或别名(都不区分大小写)获取特定字符集对象。
    • Set<String> aliases()  列出某个字符集的所有别名
    • ByteBuffer encode(String str)  编码,返回将Unicode字符按该字符集编码后字节序列
    • CharBuffer decode(ByteBuffer buff)  解码,返回将字节序列按该字符集解码后的Unicode字符序列

String与byte数组转化

   byte[] byteArray=.....  
  String str1=new String(byteArray);
  String str2=new String(byteArray, Charset.forName("UTF-8"))
  String str3=new String(byteArray, "UTF-8")
  byte[] ba1=str1.getBytes();
  byte[] ba2=str1.getBytes("UTF-8");
  byte[] ba3=str1.getBytes(Charset.forName("UTF-8"));

五、Zip文档操作

Zip文档通常以压缩格式存储一个或多个文件。每个Zip文档有一个头好汉了每个文件名和使用的压缩算法等信息。Zip文件可以用ZipInputStream和ZipOutputStream类读写,Zip文档中的每个文件用ZipEntry描述。

Jar文档是一个带有清单项的Zip文件,使用JarInputStream和JarOutputStream可以读写Jar文档的清单项。

  ZipInputStream

    • ZipEntry  getNextEntry()      获取一个内容条目,并且定位输入流到该项目内容的开始位置,此时调用ZipInputStream的read()方法族读出的就是该项目的内容,该项目读取完后返回read()方法返回-1。没有更多项目时返回null。
    • void closeEntry()  关闭内容条目,并移动输入流到下一个条目信息开始位置。

ZipOutputStream

    • void putNextEntry()     向zip文档添加一个内容条目,并且定位输出流到新条目的写入位置,此时调用ZipOutputStream的write()方法族就可以写入新项目内容。
    • void closeEntry()    关闭内容条目,并移动输出流到下一个条目信息开始位置
    • void setLevel(int level)    设置后续DEFLATED项默认的压缩级别(0:NO_COMPRESSION-9:BEST_COMPRESSION)
    • void setMethod(int method)  设置默认的压缩方法(DEFLATED或STORED)

ZipEntry

    • ZipEntry(String name)
    • long getCrc()      返回该项的CRC32校验和
    • String getName()
    • long getSize()    未压缩大小,未知时返回-1
    • boolean isDirectory()
    • void setMethod(int method)  设置该项的压缩方法(DEFLATED或STORED)
    • void setSize(long size)   用STORED方法时设置该项大小
    • void setCrc(long crc)  用STORED方法时设置校验和(通过CRC32类计算)
        ZipInputStream zis=new ZipInputStream(new BufferedInputStream(new FileInputStream("H:\\zipTest\\zipTest.zip")));
ZipEntry entry=null;
while((entry=zis.getNextEntry())!=null)
{
System.out.println((entry.isDirectory()?"dir":"file")+": "+entry.getName()+" "+entry.getCompressedSize()+"/"+entry.getSize()); ByteArrayOutputStream baos=new ByteArrayOutputStream();
byte[] buf=new byte[1024];
int readSize=-1;
while((readSize=zis.read(buf))>=0)
baos.write(buf, 0, readSize);
System.out.println(baos.toString("GBK")); zis.closeEntry();
}
        ZipOutputStream zos=new ZipOutputStream(new BufferedOutputStream(new FileOutputStream("zipTest.zip")));

        Date now=new Date();

        ZipEntry entry=new ZipEntry("time/"+now.getTime()+".txt");
zos.putNextEntry(entry);
zos.write(Charset.defaultCharset().encode("current time: "+now.toString()).array());
zos.closeEntry(); zos.close();

Zip文件操作也可以使用ZipFile类替代ZipInputStream类。

ZipFile

    • ZipFile(String name)
    • ZipFile(File file)
    • Enumeration<? extends ZipEntry> entries()     枚举文件中所有项的ZipEntry对象
    • ZipEntry getEntry(String name)     通过名字获取ZipEntry项
    • InputStream getInputStream(ZipEntry ze)    获取ZipEntry项的内容流
    • String getName()    Zip文件路径
        ZipFile zfile=new ZipFile("zipTest.zip");
Enumeration<ZipEntry> enumers=(Enumeration<ZipEntry>) zfile.entries();
while(enumers.hasMoreElements())
{
ZipEntry entry=enumers.nextElement();
System.out.println((entry.isDirectory()?"dir":"file")+": "+entry.getName()+" "+entry.getMethod()+" "+entry.getCompressedSize()+"/"+entry.getSize()); InputStream is=zfile.getInputStream(entry);
ByteArrayOutputStream baos=new ByteArrayOutputStream();
byte[] buf=new byte[1024];
int readSize=-1;
while((readSize=is.read(buf))>=0)
baos.write(buf, 0, readSize);
System.out.println(baos.toString("GBK"));
is.close();
}
zfile.close();

六、对象流和序列化

有时我们需要将内存中的数据存储到文件系统中或者是变成数据流在网络间传输。如果是相同类型的数据,可以转化为固定长度的记录,读写还算方便。但是绝大多数情况下使用的数据都涉及各种不同的类型,不可能用固定长度来表示。

Java提供了一种通用的对象序列化机制,可以将任何对象序列化并写入到流中,或者是从流中读入序列化数据还原成对象。使用ObjectInputStream和ObjectOutputStream类可以将任何实现了Serializable接口(标记接口,无方法)的类对象序列化到流中或从流中反序列化。这两个类会自动浏览对象的所有域,并存储其中的内容。序列化的每个对象都有一个唯一的序列号作为标识,通过引用已有的序列号,一个对象被多个对象引用的关系也能完整保留和恢复。

序列化会存储对象的类,类指纹,该类及其超类中的所有非静态和非瞬时域的值。瞬时域是使用transient关键字标示的域,这种域在序列化时会被跳过,一个域属于不可序列化的类或者反序列化后会失效,则应该标为瞬时域。

序列化处理过程比较复杂,效率比较低。

  ObjectOutputStream

    • writeObject(Object obj)       序列化非基本类型对象,对象所属类需要实现Serializable接口,否则会抛出异常。
    • writeXXX(XXX)     序列化基本类型,(DataOuput接口)

  ObjectInputStream

    • Object readObject()  反序列化非基本类型对象
    • XXX readXXX()      反序列化基本类型对象,(DataInput接口)
        class TestClass implements Serializable
{
public TestClass(String name){this.name=name;}
private String name;
public String getName(){return name;}
}; ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(new TestClass("aaa")); ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
TestClass tc=(TestClass) ois.readObject();
System.out.print(tc.getName());

修改默认序列化规则

  1.使用transient关键字禁止一些域被序列化

  2.覆盖private void readObject(ObjectInputStream in)和private void writeObject(ObjectOutputStream out)方法,数据域部分的序列化和反序列化过程就可以在这两个方法中自定义。在这两个方法中还可以分别调用in.defaultReadObject()和out.defualtWriteObject()方法来完成默认的序列化过程,然后进行额外的操作(如将不可序列化的域用别的方式保存关键数据和恢复)。

  3.通过实现Externalizable接口(可外部化),类可以定义自己的序列化机制,其中定义了public void readExternal(ObjectInputStream in)和public void writeExternal(ObjectOutputStream out)方法。这两个方法对包括超类数据在内的整个对象的存储和恢复负全责,序列化机制仅仅在流中记录该对象所属的类,然后调用writeExternal方法。在读入时,对象流用无参构造器(无论类中是否提供)创建一个对象,然后调用readExternal方法。

序列化单例和类型安全的枚举

enum结果序列化不会产生问题。但是在enum为推出之前,枚举的就是将类的构造器设为私有,然后仅在类的内部创建几个唯一的对象,其他地方都只能引用类里固定的内部对象,不可能再创建新的对象,单例模式也是这个原理,使用==可以判断某个变量引用的是否就是类里固定的对象。但是反序列化时,可以无视类中的构造器,直接创建出新的对象类,使用==比较就会出现问题,例如下面的例子。单例模式也是类似的,单例类反序列化后会出现同时存在多个实例的情况。

        class State implements Serializable
{
public static final State GOOD=new State(1);
public static final State BAD=new State(2);
public static final State NORAML=new State(3); private State(int value){this.value=value;}
private int value;
}; State good=State.GOOD; ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(good); ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
State nstate= (State) ois.readObject(); assertTrue(good==nstate);

解决方法就是在这种类中实现protected Object readResolve()方法。单例模式只需要在该方法中返回内部实例即可。

class State implements Serializable
{
......
protected Object readResolve()
{
switch(value)
{
case 1:return GOOD;
case 2:return BAD;
case 3:return NORMAL;
}
return null;
}
}

版本管理

对象流中保存了类的指纹信息,指纹信息是对类、超类、接口、域类型、方法签名安装规范方式排序的SHA码的前8位。读入对象时,如果流中的类指纹与实际类计算出来的指纹不一致,序列化机制认为类型不兼容,会拒绝加载对象。但是一个类会不断演进,如果希望改进后的类仍然可以读取早起版本的序列化数据,就必须要在类中加入一个public static final long serialVersionUID=xxxxxxL  的域,以标明它支持早起版本的指纹xxxxxx。

计算一个类的指纹可以使用%JAVA_HOME%\bin目录下的serialver工具。 命令行: serial class_name。 GUI: serial -show

只要在类中加入这个静态域,并且该域值和流中的类指纹是一致的,序列化系统就认为他们是兼容的,如果类的实际结构有差别,序列化系统会尝试进行转换。转换时,如果同名域的类型不同,则转换会因为不兼容而失败;如果流中有多余的域,则忽略掉;如果流中没有某个域的值,该域就为默认值(null,0,false)。有些类会将每个域都初始化为特定的值,反序列化的得到的对象域中的默认值可能会导致问题,需要在类的readObject方法中进行修正,或者是在处理null数据时足够健壮。

序列化用于克隆

序列化然后反序列化对象,可以得到对象的深拷贝。但是序列化的效率较低,会比创建新对象然后复制数据域要慢的多。