1.Input/Output Streams
java中input stream是我们用来读取数据字节的对象;output stream是我们用来输出数据字节的对象。输入输出流关注的是数据的读写过程,而具体数据存储可以在文件中,网络中,也可以在内存中。
抽象类InputStream和OutputStream是基本类。
字节流byte stream处理Unicode数据不方便,所以有专门的Reader和Writer来处理字符,基于char,而非byte。
读写字节:
InputStream中有read方法,返回读取的字节对应的int值,到末尾返回-1:
abstract
不同的子类根据需要重写该方法,比如FileInputStream从文件中读取,http://System.in从控制台或重定向文件中读取。
InputStream中还有其他的读方法,比如readAllBytes,都是在read基础上的扩展。
OutputStream中有write方法,每次将一个字节写入:
abstract
也可以传入一个字节数组,一次写入。
in.transferTo(out)将输入数据传递到输出。
read和write在没有完成工作时会阻塞,使用available检查可读数据数目,可以避免线程阻塞。
完成读写任务后,需要调用close来关闭资源,否则会浪费系统的I/O资源。调用close会自动flush输出的缓冲区,因为字节先被放在缓冲区,收集打包后再传递。所以关闭时会自动flush,也可以手动flush。
I/O流家族:
java根据流的使用方式,将流分为不同的类别。比如Date前缀允许以二进制读写基本类型流;Zip前缀,根据压缩格式读写数据;对于Unicode文本,使用Reader和Writer,不同之处在于读写方法处理char(UTF-16)。
还有四个额外的接口,Closeable,Flushable,Readable,Appendable。
Closeable包含void close() throws IOException方法,Flushable包含void flush()方法。java.io.Closeable接口继承自java.lang.AutoCloseable接口,可以使用try-with语句来控制资源。
Readable接口包含int read(CharBuffer cb)方法,CharBuffer支持连续或随机访问。
Appendable接口包含append(char c)和append(CharSequence s)用于处理字符串,String,CharBuffer,StringBuffer,StringBuilder实现了该接口。
![b1957b18ace4a45f566ed94c1ceb87ec.png](https://img-blog.csdnimg.cn/img_convert/b1957b18ace4a45f566ed94c1ceb87ec.png)
使用过滤器:
FileInputStream和FileOutputStream将输入输出绑定到文件,通过传入文件路径到构造器,获取流对象。io包使用相对路径,通过System.getProperty("user.dir")获取工作目录。注意在字符串中表示转义,需要来表示。
文件I/O也只提供字节处理。而Date前缀I/O可以直接处理类型。可以通过构造器将两者结合,将一个文件I/O通过构造器传入数据I/O进行处理,实现了获取与处理的分离。
数据I/O要先实现FilterInputStream和FilterOutputStream,过滤器提供了处理(process)加工字节流的方法。
使用Buffered缓冲I/O先将文件加载到内存,可以提高操作效率。
Pushback前缀允许读后一位read或者unread将其返回。
通过链式构造器,获取想要的I/O。
![ce583baac2af3b41b864422644a00c2c.png](https://img-blog.csdnimg.cn/img_convert/ce583baac2af3b41b864422644a00c2c.png)
文本I/O:
保存数据有两种方式,一种是二进制字节流,另一种是字符串。当使用字符串时,需要注意编码格式(character encoding),java语言常用的是以char为基础的UTF-16编码,而网络上流行的编码是UTF-8。
OutputStreamWriter将Unicode编码的输出流转变为字节流,根据选择的编码方式,InputStreamReader将字节流转化为字符流,在构造方法中传入编码方式。
写入文本输出:
使用PrintWriter,可以将数字和字符输出为文本格式,在构造器中创建文件名和编码方式。使用print,println,printf方法(重载方法适用于不同类型)写入数据,文本在内部会自动转换为字节流。println获取操作系统的换行符(Windows中的“rn”和UNIX中的“n”),通过System.getProperty("line.separator")获取。
Writer总是缓冲的,在PrintWriter中传入Writer和Boolean autoFlush(true)可以在每次调用println时,自动刷新缓冲区。
PrintStream和InputStream也就是对应的标准输入输出流和PrintWriter的区别在于:PrintStream保留了原始的write字节和字节数组的能力。且PrintStream默认编码为主机的编码标准,而字符流可以任意选择。
在使用一对输入输出时,不需要考虑编码的问题。
读取文本输入:
使用Files.readAllBytes读取字节数组,也可以通过readAllLines读取一行,返回一个List。lines返回一个Stream<String>。
使用Scanner来获取输入,next,nextLine读取数据。也可以读取token流(使用特定delimiter分离),使用useDelimiter方法分隔字符。tokens方法返回Stream<String>。
java早期,只能用BufferedReader的readLine读取,该方法返回读取的字符串或null。lines分返回Stream流但是该类没有读取数字的方法。
格式化保存对象:
写入对象需要在Employee中构建一个方法writeEmployee,传入PrintWriter和实际对象,通过out的println写入数据;
读取对象readEmployee需要传入一个Scanner,通过nextLine来获取文本,并处理,通过构造方法封装到一个新对象中。
也可以处理对象数组,在方法内部写一个循环调用上述方法即可。
字符编码:
java使用Unicode标准编码,每个字符(码点)有21位整数,不同的字符编码将其转化为不同的字节流。
UTF-8将一个Unicode字符转化为1~4个字节,ASCII字符只要一个字节。通过前缀来识别字节的长度。0,110,1110,11110分别表示1~4位。
UTF-16将Unicode转化为两个16位char,这是java字符串的编码方式。有大端(big-endian)和小端(little-endain)的区别。大端是从左到右的顺序,小端是从右到左的。使用0xFEFF测试,可以判断使用的是哪种方式。
使用字符编码时,需要显式表明编码方式,比如网络中查看Content-Type响应头。Charset.defaultCharset返回平台默认的编码方式。
StandardCharsets定义了一系列Charset类型的变量,表示虚拟机支持的编码方式。
使用自定义字符集,调用Charset.forName方法。
2.读写二进制数据
DataInput和DataOutput:
DateOutput接口定义了不同的写方法,用于写入不同基本类型的值。内存中有两种大端和小端两种方式写入数据,java所有的值都是按大端写入的,所以具有平台独立性。
writeUTF写入字符串时,使用修改的8位编码,先转化为UTF-16,再转化为UTF-8,这是为了向后兼容虚拟机(没有超过16字节的部分)。当产生虚拟机字节码时,使用该方法,否则使用writeChars方法。
DateInputStream实现了DateInput接口。不同的读方法定义在DataInput中。
随机访问文件:
RandomAccessFile允许在任意位置读写数据,磁盘是允许随机访问的,网络I/O却不可以。通过传入“r”或“rw”设置读写权限。
随机访问文件有一个文件指针(file pointer),指向下一个被读写的字节位置,seek方法设定指针位置;getFilePointer获取指针位置。
RnadomAccessFile继承了DataI/O可以读写基本类型。
在需要记录的对象中使用RECORD_SIZE来保证记录数的大小一致,length方法获取总字节长度。
对于字符串,超过实际数据的部分字节位用0填充,这样写入就有一个终止标志,读取直接抛弃多余位。
压缩文档:
压缩文档(ZIP Archives)使用压缩格式存储文件。使用Zip前缀的I/O来操作压缩文档。每个ZIP文档有一个头信息,描述文件名和压缩的方法。
输入流以ZipEntry为单位,getNextEntry获取Entry对象,closeEntry读取下一个Entry。
输出流将文件传入到ZipEntry构造器中,然后用putNextEntry操作Entry。记得调用关闭方法。
ZipI/O展示了操作流和获取流的分离,FileI/O获取流,也可以是其他的方式获取,而只要是符合Zip格式,都可以用ZipI/O来操作流。这就是细化抽象的好处。
3.对象输入输出流和序列化
当需要以相同的格式存储数据时,使用上述的定长的记录格式。但是对象就没有固定的格式了,比如staff可能是employee,也可能是manager。java提供了一个通用机制,对象序列化(object serialization)来实现对象的输入输出。
使用ObjectI/O的writeObject和readObject方法,读取对象时,要强制转换。实现了Serialization接口的对象都可以这样操作。读写基本类型使用基本类型方法。
输入流读取对象的所有域,保存其内容(只处理数据部分)。
当一个对象引用了另一个对象的域时,如何保存呢?不能使用地址,因为这是随机分配的。所以要使用序列号(serial number)。算法如下:
1.为每个遇到的引用对象设置一个序列号;
2.第一次遇到该对象时,将其数据写入输出流;
3.如果已经被保存,标记“same as the previously saved object with serial number x.”
读取时,操作相反:
1.输入时,构造器构建对象,记住此时引用和序列号的关系;
2.遇到标记,根据序列号获取对象引用。
序列化常常应用于集合,和网络传输,关键在于序列号制定引用对象的唯一性。
理解序列化格式:
理解实现过程可以更好地理解序列化。
每个字节码文件以AC ED开始,之后是序列化格式:00 05,然后是序列化的对象,
比如字符串:74 00 05 Harry。对象存储后,对应的类也要被存储。具体如下:
1.类名;
2.serial version unique ID作为数据和方法的唯一标识(fingerprint);
3.一系列flag描述序列化方法;
4.数据域的描述。
fingerprint和该类,父类,接口,域,方法有关,使用SHA(Secure Hash Algorithm)计算。
SHA是一种快捷的算法,产生20位数据,如果数据被改动,那么fingerprint必然会改变。序列化机制只用前8位来判断。
读取一个对象时,对象的fingerprint和该类的fingerprint对比,如果不匹配,说明该类的定义被改动了,抛出异常。类标识符如下:
72
类名(两字节)
类名
8字节标识
1字节flag
2字节数据域描述符
数据域描述符
78(结束标志)
父类(70如果没有)
flag由三位组成,定义于java.io.ObjectStreamContants。分别描述writeObject方法,Serializable接口和Externalizable接口。该接口提供读写方法接收数据域的输出。
每一个数据域描述符的形式为:
1字节类型代码
2字节数据域名
域名
类名(域表示对象时)
类型代码是java类型的缩写。Employee完整类描述如下:
72 00 08 Employee
E6 D2 86 7D AE AC 18 1B 02(fingerprint)
00 03 (数据域个数)
D 00 06 salary(数据域)
L 00 07 hireDay(数据域)
74 00 10 Ljava/util/Data;(数据域所属类)
L 00 04 name
74 00 12 Ljava/lang/String;
78
70
如果相同的类需要再次使用,使用简写71(4字节)表示Data类。
序列化形式包含了所有对象的类型和数据域,每个对象都有一个序列号,重复的对象引用储存类型代号。
修改默认的序列化机制:
有些数据只对本地方法起作用,不需要将这些数据序列化,使用transient关键词标记不需要序列化的域。
继承了Serializable的对象可以自己实现private读写对象方法,而不是使用默认方法。
实现了Externalizable接口可以读写对象及其父类,read/writeExternal。该方法是public的,可以任意调用。
序列化单例和类型安全枚举:
类型安全枚举通过私有构造方法,直接定义类型值来保证值的唯一性。但是序列化时,仍然会产生两个对象。解决该问题的方法是定义一个protected readResolve方法,依次判断类型的值,再获取对象。
版本:
当版本发生变化时,如何允许兼容性?使用serialver命令获取前期版本,之后的版本都要改为相同的值。
对于新版本,如果域增加了,设置为默认值,如果域减少了,直接忽略。
序列化用于克隆:
定义一个实现了Cloneable和Serailization接口的SerialCloneable类,通过ObjectI/O将this对象读写一遍,实现序列化。使用ByteArrayI/O作为缓冲。
只要在需要克隆的类上继承该工具类就可以实现克隆了。
4.使用文件
文件的管理也非常重要,java7引入的Path接口和Files类包装了处理文件系统需要的函数。使用它们比File类更加简单。
路径:
Path是文件夹名字的序列,第一部分是根目录(root component)。以根路径开始的Path是绝对路径(absolute)。
使用Paths.get方法获取由文件夹数组组成的路径path,通过分隔符连接(/unix, windows)。可以传入一个String。路径不一定对应文件,首先创建路径,然后再创建对应文件。
resolve方法组合路径p.resolve(q)返回q,如果q是绝对路径,返回p+q如果是相对路径。
resolveSibling方法产生一个替换最小目录的姊妹路径。
relativize方法根据传入的主目录产生一个相对路径。
normalize方法移除多余的..和.。
toAbsolutePath方法产生绝对路径。
Path中还有很多有用的方法,getParent返回主目录,getFileName返回文件名,getRoot返回根目录。
通过path可以创建Scanner对象,获取文件。
读写文件:
Flies提供了通用方法。readAllBytes(Path p)获取全部字节;readAllLines指定字符集,返回List<String>;write(Path p,byte[])写入字节数组,还可以指定StandardOpenOption.APPEND追加到文件末尾。
这些方法适用于文件长度不大时,也可以使用I/O读写文件数据。
创建文件和目录:
Flies.createDirectory创建目录,createFile创建文件。文件是单例模式,如果已经创建,抛出异常。
也可以创建临时文件。
操作文件:
Flies.copy复制文件,move复制并删除源文件。如果目标文件存在,使用REPLACE_EXISTING参数,COPYATTRIBUTES复制所有属性。ATOMIC_MOVE表示原子移动。可以将文件和I/O通过copy转换。
delete删除文件,不存在则抛出异常,deleteIfExist返回是否删除成功。
获取文件信息:
exists判断是否存在;isHidden是否隐藏;isReadable,isWritable是否可读;isExecutable是否可执行;size返回字节数;getOwner获取文件所属目录。
文件的属性被封装于BasicFileAttribute接口中,使用Files.readAttributes获取。
访问目录元素:
Files.list返回Stream<Path>,记录一个目录中的条目。使用try-with语句来处理流。由于目录是树形结构,要遍历所有目录使用Flies.walk方法。如果需要通过文件属性过滤path,使用find方法。
使用目录流:
使用Files.newDirectoryStream获取DirectoryStream<Path>对象,该流专门用于遍历目录结构,可以传入一个匹配模式。
*.java匹配当前目录下的所有java文件;
**.java匹配所有子文件夹下的java文件;
??.java文件名有两个字符;
[].java表示一个集合,任意一个元素与之相配;
.{java, class}匹配不同类型文件;
*表示转义。
如果需要遍历所有子目录,使用Files.walkFileTree方法,传入一个rootpath和FileVisitor。SimpleFileVisitor定义了visitFile,preVisitDirectory,postVisitDirectory和visitFileFailed方法,返回一个FileVisitResult标记。
CONTINUE表示继续访问,SKIPSUBTREE表示跳过子树,SKIP_SUBLINGS表示跳过姊妹树,TERMINATE表示终止。
通过定义的遍历方法和处理方式可以完整的遍历一棵树。
压缩文件系统:
Paths访问用户本地磁盘,而另一个文件系统是ZIP file system。使用FileSystems.newFileSystem来获取ZIP文件,返回FileSystem对象。使用fs.getPath来获取文件路径。通过流处理访问文件。
5.内存映射文件
操作系统使用虚拟内存(virtual memory)技术来映射文件到内存,提高文件的读写速度。
java.nio包使得内存映射很简单,首先为文件获取一个channal。channal是磁盘文件的抽象,允许访问操作系统特性,文件锁和传输文件。
FlieChannel
调用map方法获取ByteBuffer,指明需要映射的文件和mapping mode。
READ_ONLY表示只读,试图写文件抛出ReadOnlyBufferException。
READ_WRITE表示可以写如文件,是异步进行的。
PRIVATE表示可写如内存,但是不会通知文件。
使用Buffer和ByteBuffer类中的方法来读写。get可以顺序或随机访问字节,hasRemaining判断是否到末尾。也有对应的基本数据类型读写方法get, put。使用order设置大端和小端读写方式。
循环冗余校验和(cyclic redundancy checksum)用于跟踪文件是否被改动。java.util.zip中包含了CRC32类用于计算字节数组的校验和:
var
缓冲数据结构:
抽象类Buffer定义了缓冲数据结构,buffer实际上是一个类型数组,子类包括各种基本类型buffer。实际上ByteBuffer和CharBuffer是经常用到的。其结构如下:
容量(capacity)不变;
指针(position)指示下一个读写位置;
极限(limit)超过该位置读写没有意义;
标记(mark,optionally)用于重复读写。
设计buffer的目的在于先写再读,首先put数据,到达capacity后再get数据,如此反复。
flip设置limit为当前position,并且position指示为0。完成一轮读写后,clear清除数据。
需要重复读取,使用rewind,mark或reset方法。获取buffer,调用静态allocate和wrap方法。
将buffer传入channal,通过channal读写数据。
6.文件锁
当多线程试图修改同一个文件时,需要给文件上锁。通过文件锁来控制线程对文件的访问。通过FileChannal的lock和tryLock获取锁FileLock。第一个会在锁被使用时阻塞,第二个会返回null值。当channal关闭或调用release方法时,锁释放。也可以分段锁,一个标志shared为true时允许并发读,false允许读写。不是所有操作系统都支持共享,调用FileLock的isShared判断。
使用try-with保证锁能安全释放。FileLock是虚拟机设定的,只能持有唯一对象。对一个file只是用一个channal,因为某些操作系统会释放和file有关的所有锁,破坏其他线程的运行。对网络文件上锁对于不同操作系统是不确定的。