1.File对象
构造File对象时,既可以传入绝对路径,也可以传入相对路径。绝对路径是以根目录开头的完整路径,例如:
File f = new File("C:\\Windows\\notepad.exe");
注意Windows平台使用\作为路径分隔符,在Java字符串中需要用\表示一个\。Linux平台使用/作为路径分隔符:
File f = new File("/usr/bin/javac");
传入相对路径时,相对路径前面加上当前目录就是绝对路径:
// 假设当前目录是C:\Docs
File f1 = new File("sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File(".\\sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File("..\\sub\\javac"); // 绝对路径是C:\sub\javac
可以用.表示当前目录,..表示上级目录。
1.1.文件和目录
File对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。
用File对象获取到一个文件时,还可以进一步判断文件的权限和大小:
boolean canRead():是否可读;
boolean canWrite():是否可写;
boolean canExecute():是否可执行;
long length():文件字节大小。
对目录而言,是否可执行表示能否列出它包含的文件和子目录。
1.2.创建和删除文件
当File对象表示一个文件时,可以通过createNewFile()创建一个新文件,用delete()删除该文件:
public static void main(String[] args) throws IOException {
File file = new File("C:\\upload_file\\IOTest1.txt");
if(file.createNewFile()){
System.out.println("创建成功!");
if (file.delete()){
System.out.println("删除成功!");
}
}
}
1.3遍历文件和目录
当File对象表示一个目录时,可以使用list()和listFiles()列出目录下的文件和子目录名。listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录:
public static void main(String[] args) throws IOException {
File file = new File("C:\\upload_file");
//列出所有文件和子目录
File[] files = file.listFiles();
for (File f:files){
System.out.println(f);
}
System.out.println("======================");
//仅列出txt文件
File[] files1 = file.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".txt");
}
});
for (File f : files1){
System.out.println(f);
}
}
和文件操作类似,File对象如果表示一个目录,可以通过以下方法创建和删除目录:
boolean mkdir():创建当前File对象表示的目录;
boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。
2.InputStream输入流
2.1.InputStream
InputStream就是Java标准库提供的最基本的输入流。它位于java.io这个包里。java.io包提供了所有同步IO的功能。InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read(),签名如下:
public abstract int read() throws IOException;
这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。
FileInputStream是InputStream的一个子类。顾名思义,FileInputStream就是从文件流中读取数据。下面的代码演示了如何完整地读取一个FileInputStream的所有字节:
public void readFile() throws IOException {
// 创建一个FileInputStream对象:
InputStream input = new FileInputStream("src/readme.txt");
for (;;) {
int n = input.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println(n); // 打印byte的值
}
input.close(); // 关闭流
}
我们还要注意到在读取或写入IO流的过程中,可能会发生错误,例如,文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException异常并抛出。因此,所有与IO操作相关的代码都必须正确处理IOException。
2.2.缓冲
在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:
int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数
利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区,但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据了。
利用缓冲区一次读取多个字节的代码如下:
public static void main(String[] args) throws IOException {
InputStream inputStream = null;
try{
inputStream = new FileInputStream("C:\\upload_file\\IOTest.txt");
byte[] bytes = new byte[1000];
int n;
while( (n =inputStream.read(bytes)) != -1 ){
System.out.println(n);
}
}finally {
inputStream.close();
}
}
在调用InputStream的read()方法读取数据时,我们说read()方法是阻塞(Blocking)的。它的意思是,对于下面的代码:
int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;
执行到第二行代码时,必须等read()方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。
3.OutputStream输出流
3.1.OutputStream
和InputStream相反,OutputStream是Java标准库提供的最基本的输出流。
和InputStream类似,OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b),签名如下:
public abstract void write(int b) throws IOException;
OutputStream还提供了一个flush()方法,它的目的是将缓冲区的内容真正输出到目的地。
为什么要有flush()?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream有个flush()方法,能强制把缓冲区内容输出。
通常情况下,我们不需要调用这个flush()方法,因为缓冲区写满了OutputStream会自动调用它,并且,在调用close()方法关闭OutputStream之前,也会自动调用flush()方法。
实际上,InputStream也有缓冲区。例如,从FileInputStream读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read(),则会触发操作系统的下一次读取并再次填满缓冲区。
3.2.FileOutputStream
我们以FileOutputStream为例,演示如何将若干个字节写入文件流:
public void writeFile() throws IOException {
OutputStream output = new FileOutputStream("out/readme.txt");
output.write(72); // H
output.write(101); // e
output.write(108); // l
output.write(108); // l
output.write(111); // o
output.close();
}
每次写入一个字节非常麻烦,更常见的方法是一次性写入若干字节。这时,可以用OutputStream提供的重载方法void write(byte[])来实现:
public static void main(String[] args) throws IOException {
OutputStream outputStream = null;
try {
outputStream = new FileOutputStream("C:\\upload_file\\IOTest.txt");
outputStream.write("Hello World!".getBytes("UTF-8"));
outputStream.close();
}finally {
outputStream.close();
}
}
和InputStream一样,OutputStream的write()方法也是阻塞的。
4.Filter模式
当我们需要给一个“基础”InputStream附加各种功能时,我们先确定这个能提供数据源的InputStream,因为我们需要的数据总得来自某个地方,例如,FileInputStream,数据来源自文件:
InputStream file = new FileInputStream("test.gz");
紧接着,我们希望FileInputStream能提供缓冲的功能来提高读取的效率,因此我们用BufferedInputStream包装这个InputStream,得到的包装类型是BufferedInputStream,但它仍然被视为一个InputStream:
InputStream buffered = new BufferedInputStream(file);
最后,假设该文件已经用gzip压缩了,我们希望直接读取解压缩的内容,就可以再包装一个GZIPInputStream:
InputStream gzip = new GZIPInputStream(buffered);
无论我们包装多少次,得到的对象始终是InputStream,我们直接用InputStream来引用它,就可以正常读取:
┌─────────────────────────┐
│GZIPInputStream │
│┌───────────────────────┐│
││BufferedFileInputStream││
││┌─────────────────────┐││
│││ FileInputStream │││
││└─────────────────────┘││
│└───────────────────────┘│
└─────────────────────────┘
上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合。
我们也可以自己编写FilterInputStream,以便可以把自己的FilterInputStream“叠加”到任何一个InputStream中。
下面的例子演示了如何编写一个CountInputStream,它的作用是对输入的字节进行计数:
public class IOTest {
public static void main(String[] args) throws IOException {
CountInputStream stream = null;
try {
byte[] bytes = "Hello World!".getBytes("UTF-8");
stream = new CountInputStream(new ByteArrayInputStream(bytes));
int n;
while ((n = stream.read()) != -1){
System.out.println((char) n);
}
System.out.println("Total read:"+stream.getCount()+" bytes");
}finally {
stream.close();
}
}
}
class CountInputStream extends FilterInputStream{
private int count = 0;
public CountInputStream(InputStream inputStream) {
super(inputStream);
}
public int getCount(){
return this.count;
}
@Override
public int read() throws IOException {
int n = in.read();
if (n != -1){
this.count++;
}
return n;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int n = in.read(b,off,len);
this.count += n;
return n;
}
}
5.操作Zip
ZipInputStream是一种FilterInputStream,它可以直接读取zip包的内容:
┌───────────────────┐
│ InputStream │
└───────────────────┘
▲
│
┌───────────────────┐
│ FilterInputStream │
└───────────────────┘
▲
│
┌───────────────────┐
│InflaterInputStream│
└───────────────────┘
▲
│
┌───────────────────┐
│ ZipInputStream │
└───────────────────┘
▲
│
┌───────────────────┐
│ JarInputStream │
└───────────────────┘
另一个JarInputStream是从ZipInputStream派生,它增加的主要功能是直接读取jar文件里面的MANIFEST.MF文件。因为本质上jar包就是zip包,只是额外附加了一些固定的描述文件。
5.1.读取zip包
我们要创建一个ZipInputStream,通常是传入一个FileInputStream作为数据源,然后,循环调用getNextEntry(),直到返回null,表示zip流结束。
一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,我们就用read()方法不断读取,直到返回-1:
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {
ZipEntry entry = null;
while ((entry = zip.getNextEntry()) != null) {
String name = entry.getName();
if (!entry.isDirectory()) {
int n;
while ((n = zip.read()) != -1) {
...
}
}
}
}
5.2.写入zip包
ZipOutputStream是一种FilterOutputStream,它可以直接写入内容到zip包。我们要先创建一个ZipOutputStream,通常是包装一个FileOutputStream,然后,每写入一个文件前,先调用putNextEntry(),然后用write()写入byte[]数据,写入完毕后调用closeEntry()结束这个文件的打包。
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) {
File[] files = ...
for (File file : files) {
zip.putNextEntry(new ZipEntry(file.getName()));
zip.write(getFileDataAsBytes(file));
zip.closeEntry();
}
}
上面的代码没有考虑文件的目录结构。如果要实现目录层次结构,new ZipEntry(name)传入的name要用相对路径。