1 IO
IO 是指 Input
/ Output
,即输入和输出。
Input
是指从外部读入数据到内存并以Java提供的某种数据形式表示,如读文件,从网络读取等。因为代码是在内存中运行的,所以数据也必须读取在内存中,最后表现的具体形式无非是byte
数组,字符串等等。output
指的是把数据从 内存 输出到外部,例如,写文件,输出到网络等等。
即可以看作输入,输出的参照物是 内存
IO 流是一种顺序读写数据的模式,它是单向流动的。
Java提供了 InputStream
和 OutputStream
表示字节流,它的最小单位为 byte
。
如果读写的文件是文本,那Java还提供了 Reader
和 Writer
表示字符流,字符流读取数据时得到的却是 char
字符,归咎原因还是 Reader
内部把读到的 byte
进行了编码。
Reader
和 Writer
可以看作是一个能自动编解码的 InputStream
和 OutputStream
,传输的最小单位为 char
,而它输出的 char
取决于编码方式。
当如果数据源不是文本那只能使用 InputStream
和 OutputStream
,如果是文本那么使用 Reader
和 Writer
就更方便一些。
1.1 File
对象
常用的
File
构造函数是传入pathName
字符串,这个pathName
是与操作系统有关的,window 下是使用两个反斜线\\
分隔目录(或者直接使用/
),而 Linus 使用一个一个反斜线\
。
java.io.File
表示文件系统的一个文件或目录,创建 File
对象本身并不涉及 IO 操作,通过 isFile()
实例方法判读 File
对象是否为文件,通过 isDirectory()
实例方法判断 File
对象是否为目录。
对 File
对象获取路径信息,可通过以下 实例方法,这三个方法一般对 文件的 File
对象 操作返回都是统一个字符串:
getPath()
:获取路径,一般的获取结果是创建目录File
对象时,传入的目录字符串getAbsolutePath()
:获取绝对路径getCanonicalPath()
:获取规范路径
eg:
File filePath = new File("./");
System.out.println("获取 filePath 的路径:" + filePath.getPath());
System.out.println("获取 filePath 的绝对路径:" + filePath.getAbsolutePath());
System.out.println("获取 filePath 的规范路径:" + filePath.getCanonicalPath());
// 获取 filePath 的路径:.
// 获取 filePath 的绝对路径:F:\javacode\ForLearningJava\.
// 获取 filePath 的规范路径:F:\javacode\ForLearningJava
1.1.1 文件 File
对象的操作
常用的实例方法(标了 static
的是静态方法):
canRead()
:是否可读canWrite()
:是否可写canExceute()
:是否可运行length()
:返回文件大小createNewFile()
:创建文件,返回一个布尔值,创建成功返回true
,如果存在与此文件名相同的文件,则返回false
delete()
:删除文件static createTempFile()
:创建临时文件deleteOnExit()
:退出 JVM 时删除该文件
1.1.2 目录 File
对象的操作
常用的实例方法:
String[] list()
:返回全部文件名和子目录的名称数组File[] listFiles()
:返回全部文件名和子目录的File
数组File[] list(FilenameFilter filter)
:返回过滤后的文件名和子目录的名称数组File[] listFiles(FilenameFilter filter)
:返回过滤后的文件名和子目录的名称File
数组mkdir()
:创建该目录mkdirs()
:创建该目录,必要时把父目录也创建出来delete()
:删除该目录
eg:
File filepath1 = new File("F:\\");
// 输出文件后缀为 pdf 的文件名
for (String item : filepath1.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
if (name.endsWith("pdf")) {
return true;
}
return false;
}
})) {
System.out.println(item);
}
1.2 IO 接口
Java的IO流的接口和实现是分离的,同步 IO 接口包括(它们都是抽象类):
- 字节流:
InputStream
:对应的实现类有FileInputStream
等等OutputStream
:对应的实现类有FileOutputStream
等等
- 字符流:
Reader
:对应的实现类有FileReader
等等Writer
:对应的实现类有FileWriter
等等
1.2.1 InputStream
InputStream
是一个抽象类,是所有输入流的超类:
abstract int read()
:抽象read()
方法是该类最重要的一个方法,它的作用是读取下一个字节,并返回字节代表的整数(0 ~ 255
),如果已经读到末尾,就返回-1
int read(byte[] b)
:read()
方法的重载,利用缓冲区一次接受多个字节,它接受一个byte
数组,读取若干个字节后填充到byte
数组中,返回读取的字节个数,当已经读到末尾,就返回-1
int read(byte[] b, int off, int len)
:指定byte
数组的最大偏移量和最大填充量。void close()
:关闭输入流
1.2.2 OutputStream
OutputStream
是一个抽象类,是所有输入流的超类,与 InputStream
类似:
abstract write(int b)
:写入一个字节void write(byte[] bytes)
:写入byte
数组中的所有字节void write(byte[] bytes, int off, int len)
:指定写入byte
数组中的范围void flush()
:将缓冲区内容输出,像磁盘和网络写入数据的时候,出于效率的考虑很多时候并不是输出一个字节就写入一个字节,因为对于很多设备来讲,一次输入一个字节和一次输入一千个字节,花费的时间是一样的,所以通常情况下它会将字节放进缓冲区里,等缓冲区满了再一次性写入void close()
:关闭输出流,关闭输入流之前会自动调用将缓冲区内容输出
eg:
public static void main(String[] args) throws FileNotFoundException, IOException {
// jdk1.7 支持的 try(resource) 语法自动调用 close() 方法
// 写入数据,使用 UTF-8 编码
try (OutputStream output = new FileOutputStream("src/forIO.txt")) {
byte[] outputString = "中文".getBytes("UTF-8");
output.write(outputString, 0, outputString.length);
}
// jdk1.7 支持的 try(resource) 语法自动调用 close() 方法
// 使用缓冲区读取字节
// FileInputStream 的路径参数可以使绝对路径或者是相对路径,相对路径以项目的根目录出发,如 F:/javaCode/ForLearningJava
try (InputStream fileInputStream = new FileInputStream("src/forIO.txt")) {
int n;
byte[] bytes = new byte[20]; // new byte[1024],一个中文占3个字节
while ((n = fileInputStream.read(bytes)) != -1) {
System.out.println("read " + n + " bytes");
for (byte item:bytes) {
System.out.println(item);
}
}
}
System.out.println("==========================");
// 使用 try finally 关闭资源
// 使用read直接读取字节
InputStream input = null;
try {
input = new FileInputStream("src/forIO.txt");
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
} finally {
if (input != null) {
input.close();
}
}
}
1.2.3 FilterInputStream
Java IO使用组合功能而非继承的设计模式即Filter 模式(也称 Decorator 模式)为 InputStream
和 OutputStream
增加功能,这是为了解决因想要增加功能依赖继承而新增的子类个数失控的问题。最终其实就是使用类的组合的方式去添加功能,而不是使用继承类的方式去添加功能。
以 InputStream
为例,JDK 把 InputStream
继承树分为两类:
- 一种是可以直接提供数据的
InputStream
,如FileInputStream
、ServletInputStream
等 - 另一种是提供额外附加功能的
FilterInputStream
(抽象类),其实现类如BufferedInputStream
,GZIPInputStream
等
可以将一个InputStream
和任意FilterInputStream
组合,OutputStream
同理
其实查看
FilterInputStream
抽象类的源码就明了了
实现这样的添加功能的 InputStream
只需继承 FilterInputStream
类,然后像俄罗斯套娃一样,将代表数据源的 InputStream
作为构造函数的参数传入即可,比方下例创建一个可以返回文件字节总数的 InputStream
类():
public class CountInputStream extends FilterInputStream {
// 定义构造函数
public CountInputStream(InputStream in) {
super(in);
}
// 用于存储字节总数
public int count = 0;
// 不用重写 read(byte[] b),因为那个方法是调用这个方法实现的
@Override
public int read(byte[] b, int off, int len) throws IOException {
int n = super.read(b, off, len);
count += n;
return n;
}
public static void main(String[] args) throws FileNotFoundException, IOException {
try (CountInputStream countInputStream = new CountInputStream(new FileInputStream("src/forIO.txt"))) {
byte[] bs = new byte[30];
countInputStream.read(bs);
System.out.println(countInputStream.count);
}
}
}
1.2.3.1 ZipInputStream
ZipInputStream
是 FilterInputStream
的子类,它用于读取 ZIP 文件。ZipInputStream
的基本用法:循环使用 getNextEntry
方法获取 ZipEntry
,直至返回 null
,其中 ZipEntry
表示一个压缩文件或目录,此时可通过调用它的 isDirectory
获取是否为目录,下面是解压一个加压了一份 TXT
文件的压缩包的代码:
public static void main(String[] args) throws FileNotFoundException, IOException {
try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream("src/forIO.zip"))) {
ZipEntry entry = null;
// 循环调用 getNextEntry 获取 ZipEntry,直至返回 null
while ((entry = zipInputStream.getNextEntry()) != null) {
// 解压
// 每个 entry 表示着一个压缩文件或目录
if (!entry.isDirectory()) {
try (OutputStream outputStream = new FileOutputStream("src/zipTotxt.txt")) {
int n;
byte[] bt = new byte[1024];
// 此时这个 zipInputStream 会随着循环获取 entry 而变化
while ((n = zipInputStream.read(bt)) != -1) {
outputStream.write(bt, 0, n);
}
}
}
}
}
}
1.2.3.2 ZipOutputStream
ZipInputStream
是 FilterInputStream
的子类,它用于制作 ZIP 文件。
它的基本用法:
try (ZipOutputStream zip = new ZipOutputStream("src/fotIO.zip")) {
File[] files = ...
for (File file:files) {
// 在 zip 中创建压缩文件区域
zip.putNextEntry(new ZipEntry(file.getName)));
// 压缩文件
zip.write(getFileDataAsBytes(file));
// 结束当前文件的打 包,继续循环
zip.closeEntry();
}
}
上述方法没有实现文件的层次结构,如果想要实现就需要传入相对路径。
1.2.4 Reader
Reader
类是所有字符输入流的父类,它是一个抽象类,它以字符 char
为最小单位实现字符流输入。
它是基于 InputStream
构造的,,任何 InputStram
都可以 指定编码 并通过 InputStreamReader
转换为 Reader
,注意 Reader
不是 InputStream
的子类,即不能向上转型为 InputStream
。
eg:
public static void main(String[] args) throws FileNotFoundException, IOException {
// 使用 char 数组接受读取字符
// 指定编码
try (Reader reader = new InputStreamReader(new FileInputStream("src/forIO.txt"), "UTF-8")) {
char[] chars = new char[20];
int n;
while ((n = reader.read()) != -1) {
System.out.println((char) n);
}
}
// 直接逐个读取字符的字节码,并转换为字符
try (Reader reader = new InputStreamReader(new FileInputStream("src/forIO.txt"), "UTF-8")) {
char[] chars = new char[20];
int n;
while ((n = reader.read(chars)) != -1) {
for (char item:chars) {
System.out.println(item);
}
}
}
}
1.2.5 Writer
Reader
类是所有字符输出流的父类,它是一个抽象类,它以字符 char
为最小单位实现字符流输出。
它是基于 OutputStream
构造的,,任何 OutputStram
都可以 指定编码 并通过 OutputStreamReader
转换为 Writer
,注意 Writer
不是 OutputStream
的子类,即不能向上转型为 OutputStream
。
主要用法其实与 OutputStream
写入字节大同小异,其中 Writer
有一个 void write(String s)
方法用于写入字符串。
1.3 classpath
资源
classpath
资源中可以包含任意类型的文件,从 classpath
资源读取文件可以避免不同环境下文件路径不一致的问题。
读取 classpath
资源(以下代码在 AboutClasspathDemo.java
):
// classpath 其实就是 buildpath
// 注意路径前要添加 /
try (InputStream inputStream = AboutClasspathDemo.class.getResourceAsStream("/log4j2.xml")) {
if (inputStream != null) {
System.out.println("找到log4j2.xml");
}
}
// classpath
System.out.println(AboutClasspathDemo.class.getResource(""));
//输出 file:/E:/javaCode/ForLearningJava/target/classes/top/Seiei/forIO/
System.out.println(AboutClasspathDemo.class.getResource("/log4j2.xml"));
//输出 file:/E:/javaCode/ForLearningJava/target/classes/log4j2.xml