au加载默认的输入和输出设备失败_输入输出流

1.Input/Output Streams

java中input stream是我们用来读取数据字节的对象;output stream是我们用来输出数据字节的对象。输入输出流关注的是数据的读写过程,而具体数据存储可以在文件中,网络中,也可以在内存中。

抽象类InputStreamOutputStream是基本类。

字节流byte stream处理Unicode数据不方便,所以有专门的ReaderWriter来处理字符,基于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

使用过滤器:

FileInputStreamFileOutputStream将输入输出绑定到文件,通过传入文件路径到构造器,获取流对象。io包使用相对路径,通过System.getProperty("user.dir")获取工作目录。注意在字符串中表示转义,需要来表示。

文件I/O也只提供字节处理。而Date前缀I/O可以直接处理类型。可以通过构造器将两者结合,将一个文件I/O通过构造器传入数据I/O进行处理,实现了获取与处理的分离。

数据I/O要先实现FilterInputStreamFilterOutputStream,过滤器提供了处理(process)加工字节流的方法。

使用Buffered缓冲I/O先将文件加载到内存,可以提高操作效率。

Pushback前缀允许读后一位read或者unread将其返回。

通过链式构造器,获取想要的I/O。

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早期,只能用BufferedReaderreadLine读取,该方法返回读取的字符串或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方法,传入一个rootpathFileVisitor。SimpleFileVisitor定义了visitFile,preVisitDirectory,postVisitDirectoryvisitFileFailed方法,返回一个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表示可写如内存,但是不会通知文件。

使用BufferByteBuffer类中的方法来读写。get可以顺序或随机访问字节,hasRemaining判断是否到末尾。也有对应的基本数据类型读写方法get, put。使用order设置大端和小端读写方式。

循环冗余校验和(cyclic redundancy checksum)用于跟踪文件是否被改动。java.util.zip中包含了CRC32类用于计算字节数组的校验和:

var 

缓冲数据结构:

抽象类Buffer定义了缓冲数据结构,buffer实际上是一个类型数组,子类包括各种基本类型buffer。实际上ByteBufferCharBuffer是经常用到的。其结构如下:

容量(capacity)不变;

指针(position)指示下一个读写位置;

极限(limit)超过该位置读写没有意义;

标记(mark,optionally)用于重复读写。

设计buffer的目的在于先写再读,首先put数据,到达capacity后再get数据,如此反复。

flip设置limit为当前position,并且position指示为0。完成一轮读写后,clear清除数据。

需要重复读取,使用rewindmarkreset方法。获取buffer,调用静态allocatewrap方法。

将buffer传入channal,通过channal读写数据。

6.文件锁

当多线程试图修改同一个文件时,需要给文件上锁。通过文件锁来控制线程对文件的访问。通过FileChannal的locktryLock获取锁FileLock。第一个会在锁被使用时阻塞,第二个会返回null值。当channal关闭或调用release方法时,锁释放。也可以分段锁,一个标志shared为true时允许并发读,false允许读写。不是所有操作系统都支持共享,调用FileLock的isShared判断。

使用try-with保证锁能安全释放。FileLock是虚拟机设定的,只能持有唯一对象。对一个file只是用一个channal,因为某些操作系统会释放和file有关的所有锁,破坏其他线程的运行。对网络文件上锁对于不同操作系统是不确定的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值