Java输入输出
File类
File能新建,删除,重命名文件和目录,但是File不能访问文件内容本身,如果需要访问文件内容本身,需要使用输入输出流。
访问文件和目录
File类可以使用文件路径字符串创建File实例,该文件路径可以是绝对路径,也可以是相对路径。
一旦创建了File对象后,就可以调用File对象的方法来访问。
访问文件名相关的方法
- String getName()
- String getPath()
- File getAbsoluteFile()
- String getAbsolutePath()
- String getParent()
- boolean renameTo(File newName)
文件检测相关方法
- boolean exists()
- boolean canWrite()
- boolean canRead()
- boolean isFile()
- boolean isDirectory()
- boolean isAbsolute()
获取常规文件信息
- long lastModified()
- long length()
文件操作相关方法
- boolean createNewFile()
- boolean delete()
- void deletOnExit()
目录操作相关方法
- boolean mkdir()
- String[] list()
- File[] listFiles()
- static File[] listRoots()
1 | public class FileTest |
文件过滤器
File类的list()方法可以接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。
FilenameFileter接口包含一个accept(File dir, String name)方法,该放阿飞将依次对指定File的所有子目录或者文件进行迭代,如果该方法返回true,则list()方法会列出该子目录或者文件。
理解Java的IO流
在Java中把不同的输入输出源(键盘,文件,网络连接等)抽象为流,通过流的方式允许Java使用相同的方式来访问不同输入输出源。
流的分类
输入流和输出流
输入流主要由InputStream和Reader作为基类,而输出流则主要有OutputStream和Writer作为基类。它们都是一些抽象基类无法直接创建实例。字节流和字符流
字节流和字符流的用法几乎完全一样,区别在于操作的数据单元不同,字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16为的字符。
字节流主要有InputStream和OutputStream作为基类,而字符流主要有Reader和Writer作为基类。
- 节点流和处理流(装饰器设计模式)
通过使用处理流来包装不同的节点流,即可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入输出功能,因此处理流也被称为包装流。
流的概念模型
- InputStream/Reader: 所有输入流的基类
- OutputStream/Writer:所有输出流的基类
水管,输入流—>从水管中取水滴
输出流—> 将水滴放入水管
处理流的功能:
- 性能的提高:主要以增加缓冲的方式提高输入/输出效率
- 便捷的操作:提供一系列方法依次输入/输出大批量的数据,而不是当个水滴。
字节流和字符流
InputStream和Reader
抽象基类,本身并不能创建实例来执行输入。
InputStream方法:
- int read()
- int read(byte[] b)
- int read(byte[] b, int off, int len)
Reader方法:
- int read()
- int read(char[] cbuf)
- int read(char[] cbuf, int off, int len)
用于读取文件的输入流: FileInputStream和FileReader,它们都是节点流,会直接和指定文件关联。
1 | public class FileInputStreamTest |
OutputStream和Writer
OutputStream和Writer共有的方法:
- void write(int c), c可以代表字节,也可以代表字符
- void write(byte[]/char[] buf)
- void write(bype[]/char[] buf, int off, int len)
Write另外的两个方法:
- void write(String str)
- void write(String str, int off, int len)
使用FileOutStream输出:
1 | public class FileOutputStreamTest |
关闭输出流除了可以保证流的物理资源回收以外,还可以将输出缓冲区中的数据flush到屋里节点。
如果希望直接输入字符串内容,则使用FileWriter会有更好的效果。
1 | public class FileWriterTest |
输入/输出流体系
处理流的用法
处理流可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入输出方法。
使用处理流的典型思路是,使用处理流来包装节点流,程序通过处理流来执行输入输出功能,让节点流与底层的IO设备,文件交互。
下面程序使用PrintStream处理流来包装OutputStream,使用处理流后输出将更加方便:
1 | public class PrintStreamTest |
PrintStream类的输出功能非常强大,如果需要输出文本内容,都应该将输出流包装成PrintStream后进行输出
关闭高层流时,系统会自动关闭被包装的节点流。
输入/输出流体系
分类 | 字节输入流 | 字节输出流 | 字节输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputSteam | ByteArrayOutputSteam | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputSteam | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 | - | - | StringReader | StringWriter |
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | - | - | InputStreamReader | OutputStreamWriter |
对象流 | ObjectInputStream | ObjectOutputStream | ||
抽象基类 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
打印流 | - | PrintStream | - | PrintWriter |
推回输入流 | PushbackInputSteam | - | PushbackReader | |
特殊流 | DataInputStream | DataOutputStream |
1 | public class StringNodeTest |
转换流
1 | public class KeyinTest |
由于Buffered具有一个readLine()方法,可以非常方便地一次读入一行内容,所有经常包读取文件内容的输入包装为BufferedReader,用来方便地读取输入流的文本内容
推回输入流
PushbackInputStream和PushbackReader
- void unread(byte[]/char[] buf):将一个字节/字符数组内容推回到缓冲区里,从而允许重复度去刚刚读取的数据
- void unread(byte[]/char[] buf, int off, int len)
- void unread(int b)
推回的数据存储在推回缓冲区里,read()方法先会从推回缓冲区里读,没有数据才从原输入流中读取。
1 | public class PushbackTest |
重定向标准输入/输出
Java的标准输入输出分别通过System.in和System.out来代表,默认情况分别是键盘和显示器。但可以通过下面方法重定向标准输入输出。
- static void setErr(PrintStream err),重定向标准错误输出流
- static void setIn(InputStream in),重定向标准输入流
- static void setOut(PrintStream out),重定向标准输出流
1 | public class RedirectOut |
Java虚拟机读取其他进程的数据
由Runtime对象的exec()方法可以运行平台上的其他程序,该方法产生一个Process对象,代表有该Java程序启动的子进程。Process类提供了如下三个方法,用于让程序和其子进程进行通信。
- InputStream getRerrorStream()
- InputSteam getInputStream()
- OutputStream getOutputStream()
如果要让子进程读取程序中的数据,应该是输出流,而不是输入流。要站在主java程序的角度看。
1 | public class ReadFromProcess |
1 | public class WriteToProcess |
RandomAccessFile
RandomAccessFile是功能最丰富的文件内容访问类,但是只能读写文件,不能读写其他IO节点。
与普通输入输出流不同的是,RandomAccessFile支持随机访问的方式,程序可以直接跳转到文件的任何位置读写数据。如果只需要访问文件部分内容,使用RandomAccessFile将是更好的选择。
RandomAccessFile允许自由定位文件记录指针,以及相关操作。
- long getFilePointer
- void seek(long pos),移动文件记录指针到pos位置
RandomAccessFile同时包含InputStream的三个read()方法,以及OutputStream的三个write()方法
Random这里指的是任意访问,而不是随机访问。四个访问模式:
- r
- rw
- rws
- rwd
1 | public class RandomAccessFileTest |
1 | public class AppendContent |
1 | public class InsertContent |
对象序列化
序列化的含义和意义
对象序列话的目标是将对象保存到磁盘中,或者允许在网络中直接传输对象。对象序列化机制允许把内容的Java对象转换成平台无关的二进制流,程序也可以通过反序列化恢复该Java对象。
如果需要让某个对象支持序列化,必须实现如下两个借口之一:
- Serializable
- Externalizable
Java很多类已经实现Serializable,该借口是一个标记借口,实现该接口无需实现任何方法,只是表明是可序列化的。
所有可能在网络上传输或需要存储到磁盘的的类都应该是可以序列化的,否则会出现异常。
使用对象流实现序列化
ObjectOutputStream,处理流,必须建立在其他节点流的基础上。
1 | ObjectOutputSream oos = new ObjectOutputSteam(new FileOutputStream("test.txt")) |
1 | public class Person |
ObjectInputSteam,处理流,需要建立在其他节点流基础上。
1 | ObjectInputStream ois = new ObjectInputStream(new FileInputSteam("test.txt")) |
反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供Java对象的类class文件。另外,反序列化无需通过构造器来初始化Java对象。
如果使用序列化机制向文件写入多个java对象,使用反序列化机制恢复对象时必须按照实际写入的顺序读取。
当一个可序列化类有多个父类时(直接和间接),这些父类要么有无参数的构造器,要么也是可序列化的。如果父类是不可序列化的,只是带有无参构造器,则父类中定义的成员变量值不会序列化到二进制流中。
对象引用的序列化
如果某个类的成员变量是另一个类的对象的引用,也就是引用类型,那么这个引用类必须是可序列化,否则拥有该类对象的类也是不可序列化的。
当对某个对象序列化时,系统会自动把该对象的所有实例变量依次进行序列化,如果被引用的实例变量也引用了另一个对象,则该对象也会被序列化。这称为递归序列化。
如果多个Student引用同一个Class对象,这时在恢复时,这个Class对象应该只存在一份,而不是独立的两份。为了实现这点,Java序列化采用了一种特殊的序列化算法:
- 所有保存磁盘中的对象都有一个序列化编号
- 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未在本次虚拟机中被序列化过,系统才会将该对象转换成字节序列输出。
- 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是重新序列化该对象。
这意味着某个对象被序列化后,改变其状态(成员变量值),再次序列化,其改变后的状态不会在被序列化到文件中。
1 | public class WriteTeacher |
1 | public class ReadTeacher |
1 | public class SerializeMutable |
自定义序列化
通过在实例变量前使用transient
关键字修饰,可以制定Java序列化时无需理会该实例变量。只能用来修饰实例变量。
1 | public class Person |
在序列化和反序列化需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法可以用来实现自定义序列化:
- private void writeObject(ObjectOutputStream)
- private void readObject(ObjectInputStream in)
- private void readObjectNoData()
1 | public class Person |
writeOject()方法存储实例变量的顺序应该和readOject()方法中恢复实例变量的顺序一致,否则将不能正常恢复该java对象。
更彻底的自定义机制,甚至可以在序列化对象时将对象替换为其他对象:Object writeReplace()
1 | public class Person |
1 | public class ReplaceTest |
相对应的另一个可以实现保护性复制整个对象:Object readResolve()
另一种自定义个序列化机制
Java类实现Externalizable接口,完全由程序员决定存储和恢复对象数据。
- readExternal(ObjectInput in)
- void writeExternal(ObjectOutput out)
实际上采用Externalizable接口方式实现的序列化与前面的介绍的自定义序列化非常相似,只是这里是强制的。
1 | public class Person |
另外要注意的几点
- 对象的实例变量(包括基本类型,数组,其他对象的引用)都会被序列化,方法,类变量,transitent遍历不会被序列化。
- 反序列化必须有序列化对象的class文件
- 当通过文件网络来读取序列化后的对象时,必须按照实际的写入的顺序读取。
- Java的class文件的版本,可以通过为序列化类提供一个private static final的serialVersionUID值,用于表示该Java类的序列化版本,也就是说一个类升级后,只要它的serialVersionUI值不变,序列化机制也会把它们当做同一个序列化版本。
NIO
前面的面向流的IO都是阻塞式的,且都是通过字节移动来处理(即使不直接处理字节流,其底层的实现还是依赖字节处理),也就是面向流的输入输出系统一次只能处理一个字节,通常效率不高。
JDK1.4开始Java提供了一系列的改进的输入输出处理的新功能,统称为NIO。
Java NIO概述
NIO和传统IO目的相同,都是用于输入输出,但NIO采用了不同的方式来处理输入输出,NIO采用内存映射文件的方式来处理输入输出,将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了,效率比传统IO快很多。
Channel和Buffer是NIO中的两个核心对象,Channel是对传统IO的模拟,NIO所有数据都需要通过Channel传输。Channel与传统IO Stream最大的区别是提供了一个map()方法,通过该方法可以直接将一块数据映射到内存中。如果说传统的输入输出是面向流的处理,则NIO则是面向块的处理。
Buffer可以理解为一个容器,本质上是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中。
除了Channel和Buffer之外,NIO还提供了用于将Unicode字符映射成字节系列以及逆映射操作的Charset类,也提供了用于支持非阻塞式IO的Selector类。
使用Buffer
从内部结构上来看,Buffer就像一个数组,它可以保存多个类型相同的数据。Buffer是一个抽象类,其最常用的子类是ByteBuffer,它可以在底层自己数组上进行get/set操作。另外对应基本类型都有相应的Buffer类(除boolean):CharBuffer,ShortBuffer,IntBuffer…
除ByteBuffer以外,都采用相同或相似的方法来管理数据。Buffer类都没有提供构造器,使用下面方法来得到Buffer对象:
static XxxBuffer allocate(int capacity)
用的比较多的是ByteBuffer和CharBuffer。ByteBuffer还有一个子类:MappedByteBuffer,用于表示Channel将磁盘文件的部分或全部映射到内存中后得到的结果,通常MappedByteBuffer对象有Channel的map()方法返回。
重要概念:
- capacity:该Buffer的最大数据容量,创建后不能改变
- limit:第一个不应该被读出或写入的缓冲区的位置。也就是说,limit后的数据既不可被读,也不可被写。
- position:下一个可被读出或写入的位置,也就是前面是已经读写的区域。
- mark:允许直接将position定位到mark处
0<=mark<=position<=limit<=capacity
Buffer的主要作用就是装入数据,然后输出数据。
开始时Buffer的position为0,limit为capacity,通过put()方法向Buffer放入一些数据,每放入一些数据,Buffer的position相应的向后移动一些位置。
当Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position所在位置,并将position设为0。也就是Buffer调用flip()方法之后,Buffer为输出数据做好准备,
当Buffer输出结束后,Buffer调用clear()方法,仅仅将position置为0,将limit置为capacity,这样为再次向Buffer中装入数据做好准备。
flip(), 为从Buffer取出数据做好准备
clear(), 为再次向Buffer中装入数据做好准备
其他方法:
- int capacity()
- boolean hasRemaining()
- int limit()
- Buffer limit()
- Buffer mark()
- int position()
- Buffer position(int newPs)
- int remaining()
- Buffer reset():将position转到mark所在位置
- Buffer rewind():将位置设置为0,取消设置的mark
另外还有put(), get()方法,既支持对单个数据的访问,也支持对批量数据的访问(以数组为参数),方式:
- 相对,从当前position处开始,并移动position
- 绝对,直接更具索引读写,不影响position值。
1 | public class BufferTest |
通过allocate()方法创建的Buffer对象是普通Buffer,ByteBuffer还提供了一个allocateDirect()方法来直接创建Buffer,成本更高,但读取效率更高,一般只用于长生存期的Buffer。另外只有ByteBuffer提供直接创建。
使用Channel
Channel类似与传统的流对象,但与传统的流对象有两个主要区别
- Channel可以直接将指定文件的部分或全部映射成Buffer。
- 程序不能直接访问Channel中的数据,包括读取写入都不行,Channel只能与Buffer进行交互。也就是说,如果要从Channel中取得数据,必须先用Buffer从Channel中取出一些数据,然后让程序从Buffer中取出这些数据;如果要将程序中的数据写入Channel,一样先让程序将数据放入Buffer中,程序再将Buffer里的数据写入Channel中。
Java为Channel接口提供了DatagramChannel, FileChannel, Pipe.SinkChannel, Pipe.SourceChannel, SelectableChannel, ServerSocketChannel, SocketChannel等实现类。下面主要介绍FileChannel。
所有的Channel都不应该通过构造器直接创建,而是通过传统的节点流的getChannel()方法来返回对应的Channel。
最常用方法:
- map(),将Channel对应的部分或全部数据映射为ByteBuffer
- read()
- write()
MappedByteBuffer map(FileChannel.MapMode mode, long position, long size)
, 模式分别有只读,读写等,第二第三个参数控制将Channel的哪些数据映射为ByteBuffer。
1 | public class FileChannelTest |
1 | public class RandomFileChannelTest |
1 | public class ReadFile |
字符集和Charset
Java提供了Charset来处理字节序列和字符序列之间的转换关系,该类包含了用于创建解码器和编码器的方法。提供了一个avaiableCharsets()来获取当前JDK所支持的字符集。
1 | public class CharsetTest |
一旦直到了字符集的别名后,程序就可以调用Charset的forName方法来创建对应Charset对象:
1 | Charset cs = Charset.forName("GBK"); |
然后就可以通过该对象的newDecoder(), newEncode()这两个方法分别返回CharsetDecoder和CharsetEncode对象,代表该Charset的解码器和编码器。
调用CharsetEncoder的encode方法就可以将CharBuffer或String转换成ByteBuffer。
1 | public class CharsetTransform |