输入/输出
Posted 竹马今安在
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了输入/输出相关的知识,希望对你有一定的参考价值。
引言:
对一段看不到明显运行效果的代码,是比较乏味的,但是输入/输出却又是每个程序都必须要的。Java的IO通过java.io包下的类和接口来支持,在java.io包下主要包括输入、输出两种IO流,每种输入、输出流又可分为字节流和字符流两大类。除此之外,Java的IO流使用了一种装饰器设计模式,将IO流分成底层节点流和上层处理流。Java7在java.nio及其子包下提供了一系列全新的API,这些API对原有的IO进行了升级,因此也被称为NIO2,使程序更加高效
1.File类
File类是java.io包下代表与平台无关的文件和目录,换一句话说就是如果希望在程序中操作文件和目录,都可以通过File类来完成。但是访问文件内容本身,要通过输入/输出流
1)访问文件和目录
File类可以使用文件路径字符串来创建File实例,该文件路径字符字符创既可以是绝对路径,也可以是相对路径。在默认情况下,系统总会依据运行JVM时所在的路径来解释相对路径。
访问文件名相关的方法:
文件检测相关的方法:
获取常规文件信息:
文件操作相关方法:
目录操作相关的方法:
2)文件过滤器
2.理解Java的IO流
java的IO流是实现输入/输出的基础,那么什么是“流”呢,流是个抽象的概念,是对输入输出设备的抽象,当程序需要从某个数据源读入数据的时候,就会开启一个数据流,数据源可以是文件、内存或网络等等。相反地,需要写出数据到某个数据源目的地的时候,也会开启一个数据流,这个数据源目的地也可以是文件、内存或网络等等。
流具有方向性,至于是输入流还是输出流则是一个相对的概念,一般以程序为参考,如果数据的流向是程序至设备,我们成为输出流,反之我们称为输入流。
可以将流想象成一个“水流管道”,水流就在这管道中形成了,自然就出现了方向的概念。
这个时候,你就可以想象数据好像在其中流动一样,如下图:
你可以将流想象成一个“水流管道”,水流就在这管道中形成了,自然就出现了方向的概念,水可以流进也可以流出。当水从一处流进管道的时候,就相当于从数据源读入数据至流中,当水从管道流到某处的时候,就相当于从流中写出数据到某个数据源目的地。
刚刚说到流有方向的概念,在Java中将读入数据的流叫做输入流,将写出数据的流叫做输出流。
当程序需要从某个数据源读入数据的时候,就会开启一个输入流,数据源可以是文件、内存或网络等等。相反地,需要写出数据到某个数据源目的地的时候,也会开启一个输出流,这个数据源目的地也可以是文件、内存或网络等等。
1) 流的分类
按照流向来分:输入流(只能从中读取数据,不能向其写入数据)使用InpurtStream和Reader作为基类、输出流(只能向其写入数据,不能从中读取数据)使用OutputStream和Writer作为基类。
这里的方向理解起来应该是对于不同的对象不同,比如A给B一个苹果,对于A来说应该使用输出流,而对于B来说应该使用输入流。但是划分输入/输出流时是从程序运行的内存的角度来考虑
按照操作的数据单元来分:字节流(操作的数据单元是8位的字节)使用InpurtStream和OutputStream作为基类、字符流(数据单元是16位的字符)使用Reader和Writer作为基类
按照角色来分 :节点流(可以从/向一个特定的IO设备读/写数据的流)。节点流从一个特定的数据源读写数据。即节点流是直接操作文件,网络等的流,例如FileInputStream和FileOutputStream,他们直接从文件中读取或往文件中写入字节流。
处理流(过滤流)(用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能)
当使用处理流进行输入/输出时,程序并不会直接连接到实际的数据源,没有和实际的输入/输出节点连接。
通过使用处理流,程序无须理会输入/输出节点是啥,只要将这些节点流包装成处理流,就可以使用相同的输入输出代码来读写不同的输入/输出设备的数据
字节流/字符流
InputStream和Reader
所有输入流的抽象基类,本身并不能创建实例来执行输入,但他们将成为所有输入流的模板。
InputStream | Reader | |
输入流中读取单个数据单元的数据,返回读取的数据 | int read() | intread() |
读取b.length个数据单元的数据,并存储在数组b中,返回读取的数据单元数 | read(byte[] b) | read(char[] b) |
读取len个数据单元的数据,从off开始存储在数组b中,返回读取的数据单元数 | read(byte[] b,int off, intlen) | read(char[] b,int off, intlen) |
两者都是抽象类,本身不能创建实例,但他们分别由一个用于读取文件的输入流:FileInputStream和FileReader,他们都是节点流会一一和指定文件关联
读取中文注释时乱码问题的出现:创建较小字节的字节数组时候,因为本文件采用GBK编码方式,在这种方式下,每个中文字符占两个字节,如果read()方法读取时只取了半个中文字符,这将导致乱码
OutputStream和Writer
InputStream | Reader | |
将指定的数据单元数据单元输出到输出流中。 | int write(int c) | int write(int c) |
将数据单元数组输出到指定输出流中 | write(byte[] b) | write(char[] b) |
将数据单元数组从off位置开始,长度为len的数据单元数组输出到输出流中 | write(byte[] b,int off, intlen) | write(char[] b,int off, intlen) |
输入流/输出流体系
处理流用法:
只要流的构造器不是一个物理节点,而是已经存在的流,那么这种流就一定是处理流;而所有节点流都是以物理IO节点作为构造器参数的
输入/输出流体系:
深色的为节点流,浅色的为处理流
字节流比字符流功能更加强大,因为计算机所有数据都是二进制的。
计算机的文件通常beifenwei文本文件和二进制文件,但实质上,计算机里所有的文件都是二进制文件,文本文件只是二进制文件的一种特例,当二进制文件里的内容刚好能被正常解析成自复时,就变成了文本文件。
转换流
其实输入输入都是相对于内存来说,因为你无论输入还是输入都是经过内存来进行的。
源或目的对应的设备是字节流,但是操作的却是文本数据,可以使用转换作为桥梁,提高对文本操作的边界。另外转换流提供了编码解码的方法,如果不是使用默认编码表,就可以使用转换流
用于实现将字节流转换成字符流
其中InputStreamReader将字节输入流转换成字符输入流,
OutputStreamWrite将字符输出流转换成字节输出流
两个流的中文API说的很清楚了,之间的关系画图表示一下
推回输入流
PushbackInputStream | PushbackReader | |
将一个数据单元推回到推回缓冲区里,从而允许重复读取刚刚读取的内容 | void unread(int b) | void unread(int b) |
将一个数据单元数组推回到推回缓冲区里,从而允许重复读取刚刚读取的内容 | void unread(byte[] b) | void unread(char[] b) |
将一个数据单元数组从off开始,长度为len的内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容 | void unread(byte[] b,int off,int len) | void unread(char[] b,int off,int len) |
这两个推回输入流都带有一个推回缓冲区,当程序调用这两个推回输入流的unread()方法时,系统将会把指定数组的内容推回到该缓冲区里,二推回输入流每次调用read()方法时总是先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没有装满read()所需的数组时才会从原输入流中读取
重定向标准输入/输出
java的标准输入/输出通过System.in和System.out来代表,默认情况下他们分别代表键盘和显示器。
System类里提供了三个重定向标准输入/输出的方法
setErr(PrintStream err)
setIn(InputStream in)
setErr(PrintStream out)
改变由标准输出到输出到文件中
改变由标准输入到文件输入
RandomAccessFile
RandomAccessFile是java输入/输出流体系中功能最丰富的文件内容访问类,与普通的输入输出流不同的是,它可以“随机访问”("Random Access"自由访问、任意访问的意思),程序可以直接跳转到文件的任意地方来读写数据,
注意:!!!只能读写文件。
RandomAccessFile对象包含了一个记录指针,随读/写移动,也可以自由移动,操作方法时getFilePointer()返回文件记录指针的当前位置和seek(long pos)将文件记录指针定位到pos位置。
创建RandomAccessFile时需要一个mode参数,指定访问模式:
r:只读方式打开指定文件
rw:以读、写方式打开文件
rws:以读、写方式打开文件,对文件的内容或元数据的每个更新都同步写入到底层存储设备
rwd:以读、写方式打开文件,对文件内容的每个更新都同步写入到底层存储设备
访问指定的中间部分数据
向指定文件后追加内容
RandomAccessFile依然不能向文件的指定位置插入内容,如果将指针移动到中间某位置后开始输出,则心输出的内容会覆盖文件中原有的内容,程序需要先把插入点后面的内容读入缓冲区,等把需要插入的数据写入文件后,再将缓冲区的内容追加到文件后面
NIO.2:
1.提供了全面的文件IO和文件系统访问支持(java.nio.file及各个子包)
2.基于异步的Channel的IO(在java.nio.channels包下增加了多个以Asynchronous开头的Channel接口和类)
Path、Paths、Files核心API
原来Java只提供了一个File类来支持对文件系统的访问,但是File有如下诸多缺点:
i. 平台有关性:Windows的路径分隔符是反斜杠\'\\\',而Unix等系统的路径分隔符是正斜杠\'/\',这就导致了在不同平台上程序中的路径常量写法不同,最讨厌的是Windows中的反斜杠还需要转义,要用两个反斜杠来表示一个反斜杠!
ii. 不能利用特定文件系统的特性:为了使File类强行平台无关,就只能用各个OS文件系统的共有的功能,导致很多高级功能不支持(类似于AWT和Swing的关系);
iii. 大多数File的API在异常时只是返回失败但不会提示具体的异常信息;
NIO.2为弥补这种不足,引进了一个Path接口,Path接口代表一个与平台无关的平台路径,Paths包含了两个返回Path的静态工厂方法,Files包含了大量静态的工具方法来操作文件
Path的简单用法:
1) 肯定需要先用一个具体的路径来构造一个Path实例咯!但是Path只是一个接口,因此必然会想到用Paths工具类来构造咯!
i. Path构造方法:static Path Paths.get(String first, String... more);
ii. 直接用路径节点来构造一个Path,无需输入路径分隔符哦!完美解决了平台依赖的问题哟!例如:Path path = Paths.get("group", "user", "codes");
!!在Windows下表示的路径就是group\\user\\codes,在Unix等平台上的路径就是group/user/codes了,完美!
2) 和File类一样,Path中也保存着一个path成员,里面保存着初始化时传入的路径,只不过类型是String[],保存着各个节点的字符串名称;而Path对路径的操作也沿用了File对路径的操作,一切都是基于成员path的字符串操作!!接下来具体讲这些操作都有哪些,而这些方法都是Path的对象方法!!
3) 获取路径中有几个节点:
i. 原型:int Path.getNameCount();
ii. 返回的起始就是String[] path成员的length,在上面的例子中(group-user-codes)结果就是3;
iii. 注意!包括后面所有的方法都是直接对path的字面值进行解析,并不会深入文件系统,获取该路径所对应的完整的绝对路径或从根结点开始数共有几个节点!!
4) 获取根结点:
i. 原型:Path Path.getRoot();
ii. 根结点在Windows中就是指"x:"类型的结点,比如"c:"、"d:"等,在Unix等系统中就是指根目录"/";
iii. 使用该方法的前提是成员path的第一个节点必须是上述的根结点,这样返回的新的Path对象中的path成员里就只有一个根结点的字符串,否则该方法只能返回NULL;
!!不要指望该方法会自动深入OS中查找到该路径所对应的完整的绝对路径,然后返回给你一个根结点!错了!它仅仅就是根据path成员的字面值解析而已!!功能上还是比较原始和低级的!
iv. 例如上述的group-user-codes就只能返回null,但是如果是c:-group-user-codes那就返回c:了;
5) Path实现了toString方法,因此可以直接使用print等直接打印出其所代表的路径(即path成员),只不过打印出来的内容是带有和平台有关的分隔符的(Win下是\\,Uni下是/);
6) 获取绝对路径(真正深入OS中查询):
i. 原型:Path Path.toAbsolutePath();
ii. 如果你初始化时的path路径里已经包含根结点了,那它返回的还是原来的路径,因为包含根结点的路径就是绝对路径!
iii. 如果你初始化是的path是相对路径,那么它就将当前Java程序的路径作为上级路径合成一个绝对路径,例如初始时输入的路径是还是上面的例子,而当前Java程序的路径d:\\codes\\Test\\,那么将返回d:\\codes\\Test\\group\\user\\codes了;
7)getName(int index)
index 参数是要返回的 name 元素的索引。与目录层次结构中的根最近的元素有索引0。离根最远的元素具有索引 count-1。
4. Files工具类——全面使用OS文件系统:接下来介绍的全部都是Files类的静态工具方法,这里只介绍几个常用的工具方法
1) long copy(Path source, OutputStream out); // 从source拷贝到out
2) List<String> readAllLines(Path path[, Charset cs]); // 读取path指定的文件的所有行保存到String列表中返回,可以选择性指定编码
3) Path write(Path path, Iterable<? extends CharSequence> lines[, Charset cs], OpenOption... options); // 将字符串序列写入写入path指定的文件中
!!options暂时不管,一般用不到;
!!可以选择性指定编码表;
!!lines其实就是可迭代的字符串序列,比如List<String>、String[]什么都可以,只要是可迭代的就行;
4) boolean isHidden(Path path); // 判断path所指定的文件是否为隐藏文件
5) long size(Path path); // 返回指定文件的大小(字节)
5. Java8新加入Files的功能——Stream API:同样都是Files工具类的静态工具方法
1) 列出当前目录下所有的文件和子目录:
i. 原型:Stream<Path> list(Path dir);
ii. 返回的是Path的Stream列表,而Stream<Path>用其forEach方法遍历;
iii. void forEach(Consumer<? super T> action);
iv. 而action是一个函数式接口,里面只有一个接口方法:void accept(T t);,而在这里,T就是dir下的每个文件了(类型是Path);
v. 因此可以这样用:Files.list(".").forEach(path -> System.out.println(path)); // 打印当前目录下的每个子目录和文件的路径名
2) 列出文件中的每行:
i. Stream<String> lines(Path path[, Charset cs]);
ii. 同样可以使用Stream列表的forEach方法遍历,只不过参数变成了Stream里面的Path对象了,表示罗列出的每一行;
iii. 示例:Files.lines(Paths.get("."), Charset.forName("gbk")).forEach(line -> System.out.println(line));
3) 获得驱动盘的存储空间信息:
i. 首先通过Files的getFileStore工具方法获取驱动盘的存储空间句柄:static FileStore Files.getFileStore(Path path);
!!FileStore就是驱动器盘存储空间信息句柄(对象),它代表了path所在的驱动盘的存储空间信息;
ii. 接着调用FileStore的getTotalSpace和getUsableSpace来查询驱动盘的总空间和可用空间:
a. long FileStore.getTotalSpace(); // 返回FileStore所代表的驱动盘的总空间,单位是字节
b. long FilesStore.getUsableSpace(); // 返回FileStore所代表的驱动盘的剩余可用空间,单位是字节
!!注意:
a. 在使用getFileStore时path只需要指定根结点就行(盘符),例如"c:"、"d:"等;
b. 因为FileStore只能代表驱动盘,而驱动盘只需要用盘符来确定就行了,最终getTotalSpace等得到的结果也是整个驱动盘的;
c. 如果path指定的是一个完整的路径名(例如“c:\\intel"),那它最终还是会转化成"c:"这一个根结点,最终不会计算intel这个目录的存储空间,因为存储空间这个概念只适用于整个驱动盘;
d. 如果你path指定的是一个相对路径(路径中不包含根结点),那么它会将当前Java程序所在的驱动盘作为最终结果返回!
以上是关于输入/输出的主要内容,如果未能解决你的问题,请参考以下文章