File
File类代表的是文件或者文件夹(目录),将目录和文件抽象为一个类。File提供了很多方法用来操作文件夹或者文件。下面具体介绍该类。
静态成员变量
File类有四个成员变量
- pathSeparator
- pathSeparatorChar
- 这两个保存的路径分隔符
- Windows下为
;
,Linux下为:
- separator
- separatorChar
- 这两个保存的是文件名称分隔符
- Windows下为
\
反斜杠,Linux下为/
正斜杠
下面看这个例子
import java.io.File; |
输出为
; |
构造方法
File(String pathname)
- 传入一个字符串类型,这个字符串为第一个路径名
- 传入的路径可以是绝对路径,也可以是相对路径
- 传入的路径可以存在,也可以不存在
- 可以是文件结尾,也可以是文件夹结尾
\
下面来看看该方法的使用
File file1 = new File("G:\\JavaProject\\FirstProject"); //绝对路径写法 |
输出为
G:\JavaProject\FirstProject |
注意:
- 由于
\
代表的是转义,所以要写两个\\
- 可见File类重写了toString()方法
File(String parent,String child)
- 该构造方法需要传入两个参数,一个为父级目录的路径,一个为子级目录的路径
- 该种构造方法相比上面的要灵活一点
下面示例使用
String parent = "C:\\JavaProject\\"; |
输出为
C:\JavaProject\FirstProject |
File(File parent, String child)
- 同上个构造方法,传入也是父级和子级的路径,不过传入的父级路径是一个File对象
- 这样可以调用File类的方法,更加灵活
下面示例使用
File parent = new File("C:\\JavaProject"); |
输出为
C:\JavaProject\FirstProject |
常用方法
按照功能不同分为了三类
- 获取功能
- 判断功能
- 创建删除
获取功能
与获取功能有关的方法有四个
- getAbsolutePath()
- 获得文件或文件夹的绝对路径
- 不论在构造方法中传入的是绝对路径还是相对路径,获得都是绝对路径
- getPath()
- 在构造方法中传入的是什么,返回的就是什么
- toString()调用的就是这个方法
- getName()
- 获得文件或文件夹的名称,即路径的结尾部分
- length()
- 获得文件的长度,以字节为单位
- 如果该文件不存在,那么返回0
下面示例这四个方法的使用
import java.io.File; |
判断功能
与判断功能有关的方法有三个
- exists()
- 判断传入构造方法的路径是否存在
- isDirectory()
- 判断是不是目录(文件夹)
- 如果路径不存在,无论是否为目录(文件夹),返回false
- isFile()
- 判断是否为文件
- 如果路径不存在,无论是否为文件,返回false
下面进行示例
File file = new File("src\\Dog.java"); //传入的相对路径,获得是src目录下的Dog.java文件 |
创建删除
与创建和删除有关的方法有四个
- createNewFile()
- 如果该文件不存在,则创建该文件,如果存在则不创建
- 返回布尔值,创建返回true
- 不能创建目录,所以该文件所在目录必须存在,否则会抛出异常
- delete()
- 删除该类代表的文件或文件夹,直接从硬盘删除,不走文件夹,要小心
- 删除成功返回true
- makdir()
- 创建由此File类代表的目录,只能创建单级目录
- makdirs()
- 创建由此File类代表的目录,包括必需但不存在的父级目录
下面演示使用
import java.io.File; |
目录遍历
为了遍历目录,有两个方法
- list()
- 返回一个字符串数组,这些字符串是文件名或文件夹名
- listFiles()
- 返回时的File类对象数组
注意:
- 上面两个方法,如果目录不存在或者不是目录,那么会抛出空指针异常
- 隐藏文件和文件夹也能获取到
File file = new File("G:\\JavaProject\\SecondProject\\src"); |
输出为
com |
递归
递归就是在函数里面调用自己。
- 递归必须要用终止条件,否则会不断的调用自己,造成栈内存溢出
- 递归的次数也不能太多,否则也会造成栈内存溢出
- 构造方法
下面做几个小的练习来熟悉递归。
递归练习
第一个练习是根据函数接收的参数$n$,来计算$1 + 2 … + n$,因为我们是从递归的角度看,所以应当这么看
$$
sum(n) = \begin{cases}1, &if ,, n =1 \\
n + sum(n - 1), &if ,,n > 1 \end{cases}
$$
所以程序我们应该这么写
//这里没有考虑输入为负数时的处理 |
在main方法中调用该方法测试
int res = sum(100); |
输出为
5050 |
可见程序是正确的。
第二个练习是计算阶乘,思路同上面完全是一样的
$$
fac(n) = \begin{cases}1, &if ,, n =0 \\
n * fac(n - 1), &if ,,n > 1 \end{cases}
$$
代码如下
public static int fac(int n) { |
在main方法中调用测试
int res = fac(5); |
输出为
120 |
文件夹遍历
下面使用递归来实现文件夹的遍历,我们getFile()文件来进行文件夹的遍历,它接收一个File对象作为参数
public static void getFile(File file) { |
我在D盘下新建了一个Test文件夹,它的目录结构如下
Test |
我们在main方法中遍历该文件夹
getFile(new File("D:\\Test")); |
输出为
a |
过滤器
listFiles其实还可以接收一个叫做过滤器的参数。这个过滤器是一个接口,分为有FileFilter和FilenameFilter两个接口,这两个接口里面都只有一个方法accept(),listFiles会调用这个方法,将它获得的路径传入该方法中,如果得到的结果为true,那么就保留这个路径在最终返回的数组中,如果返回的是false,那么就不要这个路径。可见这样就起到了文件过滤的作用,而过滤什么样的文件,需要什么样的文件,完全由accept()方法决定,所以这也是它们为什么叫做过滤器的原因。
FileFilter
下面来介绍FileFilter,FileFilter里面的accept(File pathname)接收一个参数,这个参数就是路径名,listFiles()会将它获得的路径传入accept(),accept()进行过滤,以决定要什么样的路径。下面我们做一个示范,打印出一个图片文件夹里面以.png结尾的图片
//传入的是图片文件夹的路径 |
我们在main方法调用测试
printPNG(new File("D:\\images")); |
打印输出为
可见只有.png结尾的图片才被放入了files数组中。
FilenameFilter
它的accept(File dir, String name)接收两个参数,其余的与FileFilter相同,下面就同样的功能演示其代码
public static void printPNGAgain(File file) { |
在main方法中调用该方法
printPNGAgain(new File("D:\\images")); |
输出同上面一模一样
字节流
下面介绍两个类用来从文件中读取数据和写入数据到文件的类。不管是读取数据还是写入数据,都是以字节为单位的。
OutputStream
该类可以将内存中的数据写入到文件中。OutputStream类是一个抽象类,我们可以使用它的子类FileOutputStram,它的构造方法中传入的参数可以是一个File对象,也可以是一个代表路径的字符串。它里面主要有以下方法
- write(byte b)
- 写入一个字节到文件中
- write(byte[] bytes)
- 将一个字节数组写入到文件中
- 如果传入的byte是一个负数,那么该数与其后面的那个字节组成一个中文
- write(byte[] bytes, int off, int len)
- 写入字节数组索引从off开始,长度为len字节的数据
- close()
- 关闭流
下面示例其使用
//如果没有a.txt,会创建一个a.txt新文件 |
上面的程序是会把原来文件里面的内容情空,然后将数据写入了,如果想向文件中追加数据的话,那么就要在构造方法的第二个参数传入true,代表是追加。如
OutputStream fos = new FileOutputStream(new File("a.txt"), true); |
InputStream
与OutputStream相对的,该类的作用是读取文件中的内容,一般我们使用的是其子类FileInputStream,构造方法同OutputStream,包含的方法有
- read()
- 读取一个字节并返回,如果读到了文件的末尾,那么返回-1,我们可以通过返回的是否是-1来判断是否已经读到了文件的末尾
- read(byte[] bytes)
- 读取bytes大小的字节,返回的是读取的有效位数
- 假如文件有5个字节,我用长度为6的字节数组去读取,那么返回的就是5
- 如果已经读到了文件的末尾,不是返回0,而是返回-1。
- close()
- 关闭流
InputStream fis = new FileInputStream(new File("a.txt")); //可以传入"a.txt"字符串 |
文件复制
综合读取数据和写入数据,我们通过这两个流复制一个文件
OutputStream fos = new FileOutputStream(new File("copy.jpg")); |
字符流
字节流是一个字节一个字节读取的,但是对于中文,如果使用GBK编码的话由2个字节组成,如果使用UTF-8编码的话由3个字节组成。所以如果一个字节进行读取的话就得不到想要的字符。那么这里就需要引入一个新的流来读取字符,以字符为单位进行读取。
Reader
Reader就相当于是InputStream,它的作用就是从文件中读取数据到内存,不过不同的是,Reader读取的最小单位为字符,它的构造方法与InputStream一样,包含的方法也一样,不过参数有所不同,不在是字节,而是字符。Reader也是一个抽象类,我们在这里常使用它的子类FileReader。我们在本目录下新建一个文件b.txt,在里面写入你好,使用UTF-8编码(不要使用GBK)
Reader reader = new FileReader("b.txt"); //也可以传入一个File对象 |
输出为
你 |
FileReader其实也有新建一个字节流去读取数据,不过中间有一个将字节转为字符的过程,转换默认是按照UTF-8的方式解码的,所以如果你在Windows下直接新建文件,默认是GBK的,这样读取就会是乱码,可以直接在IDEA中新建文件,默认为UTF-8编码。
如果我们读取得到了一个字符数组,我们可以使用String类的构造方法
- String(char[] value)
- String(char [] value, int off, int len)
将字符数组转化为字符串,方便处理。
Writer
Writer就是向文件中写入一个字符,同OutputStream不同,Writer有一个特有的方法
- flush()
- 该方法的作用是将缓存区里的内容写入到文件中
这是怎么回事,其实Writer的底层是创建一个了字节流写数据的,我们直接写入字符不是直接写在文件中,而是写在缓冲区中,然后缓冲区中的内容转化为字节写入文件中。在调用close(),在关闭流之前,会把缓冲区中的内容写到文件中,如果既没有调用flush()也没有调用close(),那么写入的数据不会写入文件中。
还有一个方法是
- write(String str [, int off, int len])
- 可以直接写入字符串,不用写字符数组那么麻烦了
//如果没有c.txt,那么会新建一个 |
同OutputStream一样,这里的write也是覆盖重写,如果想要只是追加数据的话,在构造方法的第二个参数传入true
Writer writer = new FileWriter("c.txt",true); |
Properties属性集
Properties是继承至HashTable的一个集合。它是与IO流关联起来的,通过load()和store()方法可以读取特定格式的文件数据,和将Properties集合中的数据写入到文件中。
store
下面介绍将Properties中的数据写入到文件中。首先介绍如何向Properties集合中添加数据
- setProperty(String key, String value)
Properties集合中的键和值都是String类型的。store()分为两种,一种是字节流,传入的是OutputStream对象,一种是字符流,传入的是Writer对象
- store(OutputStream in, String comment)
- store(Writer writer, String comment)
第二个参数String comment是注释,写入文件会写在第一行,并且以#开头,一般我们传入一个空字符串
Properties properties = new Properties(); |
properties.txt文件里面的内容为
# |
load
在介绍load()之前,说明一下如何获取Properties中的元素
- getProperty(String key)
- 通过键获取值,相当于Map中的get()
- stringPropertiesName()
- 相当于Map中的 keySet(),返回一个Set集合,泛型为String
下面介绍load()方法,load()就是读取指定格式的文件,比如,指定格式如下两种
古力娜扎=18 |
或者
古力娜扎 18 |
中间的分隔符可以使用=或者空格,如果碰到以#开头的,则不会读取,我们来读取刚刚写的properties.txt文件
Properties properties = new Properties(); |
输出为
古力娜扎=18 |
缓冲流
之前我们使用过字节流InputStream和OutputStream进行过文件复制的练习,但是其实我们可以发现,使用这两个流的速度很慢,所以这里就引入了缓冲流BufferedInputStream和BufferedOutPutStream,还有BufferedReader和BufferedWriter。为了感受字节流和缓冲流的差异,我们这次来复制一个1MB的文件。
字节流复制
首先使用字节流,一个字节一个字节的读取
long start = System.currentTimeMillis(); |
输出为
共耗时11738毫秒 |
现在还是使用字节流,不过一次读取1KB
long start = System.currentTimeMillis(); |
输出为
共耗时203毫秒 |
缓冲流复制
这次使用缓冲流,先一个字节一个字节的读取
long start = System.currentTimeMillis(); |
输出为
共耗时94毫秒 |
这次一次读取一1KB
long start = System.currentTimeMillis(); |
输出为
共耗时16毫秒 |
下面我来做一个表格总结一下速度
缓冲流(ms) | 字节流(ms) | |
---|---|---|
1MB | 94 | 11738 |
1KB | 16 | 203 |
从这些数据看,就知道缓冲流的速度比字节流的快很多。
缓冲流的一些特有方法
缓冲流提供了几个特有的方法,如BufferedOutputStream和BufferedReader中有一个方法为
- readline()
- 一次读取一行数据,读到的数据不包括换行符
- 如果读到了末尾,那么返回null
BufferedInputStream和BufferedWriter也提供了一个方法叫做
- newline()
- 作用是换行,可以根据不同的操作系统进行换行
- 之前我们写的
\r\n
只适用于Windows系统
转换流
我们在使用字符流时,只能读取UTF-8格式的文件,写出的数据的格式也是UTF-8的,现在假设我要读取一个GBK的文件,里面有中文”你好”,看看会发生什么。
FileReader reader = new FileReader("text.txt"); |
发现读取的只是乱码
这是因为FileReader是按照UTF-8的格式读取的,但是文件的格式却是GBK,编码不一样,那么解码当然会出问题。所以就有了转换流的出现,有两个类是做这件事情的,分别是InputStreamReader和OutputStreamWriter,它们的构造方法是
- InputStreamReader(InputStream in, String charset)
- OutputStreamWriter(OutputStream out, String charset)
第二个参数就是用来指定字符集的,如果不指定字符集的话,就默认为UTF-8。
String path = "text.txt"; |
输出为
你好 |
下面做一个小的练习,将一个GBK格式的文件转换为一个UTF-8格式的文件
String path = "text.txt"; |
序列流
如果我们想将对象保存在硬盘中怎么办? 因为一旦断电,内存中的对象就会全部的消失。这个时候就需要序列流将对象保存在文件中,对应也有相应的序列流读取文件得到一个对象。
- ObjectOutputStream
- 将文件写到文件中的类
- writeObject(Object o)
- 该方法将对象写到文件中
- ObjectInputStream
- 从文件中读取对象的类
- readObject()
- 将文件中的对象读取出来,返回一个Object对象
下面介绍使用及注意事项。首先创建一个Person类
package IOProject |
然后在测试类中创建一个对象,并使用ObjectOutputStream类的对象将这个对象写到文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person")); |
这时抛出了一个异常
这时因为Person没有实现Serializable接口,这里谈第一个注意事项
- 只有实现了Serializable接口才能将其对象序列化和反序列化
- Serilaizable是一个标志性接口,所谓的标志性指的是只起一个标志的作用,Serializable接口里面什么都没有,我们不需要实现任何的方法
现在将Person实现Serializable接口然后执行上面的程序就不会有问题了。
下面使用ObjectInputStream读取刚刚序列化的对象,这个过程叫做反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person")); |
输出为
Person{age=18, name='迪丽热巴'} |
说明我们读取成功了。
下面添加几个注意事项
- static修饰的静态变量不能进行序列化
- 被transient修饰的成员变量也不能进行序列化
- 如果对象序列化后,修改了类文件,那么不能被序列化
现在我们修改Person类中的age使用transient修饰,进行序列化和反序列化操作,得到的结果为
Person{age=0, name='迪丽热巴'} |
age = 0并不等于18,说明age没有被序列化到文件中。
现在我们将Person类的对象序列化,然后修改Person类,接着在反序列化
Exception in thread "main" java.io.InvalidClassException: IOProject.Person; local class incompatible: stream classdesc serialVersionUID = -774581383406272369, local class serialVersionUID = -4004215360553243182 |
抛出了一个异常InvalidClassException,这是因为它们的serialVersionUID对不上。serialVersionUID是根据类文件自动计算的,当我们修改类文件时,serialVersionUID发生了改变,当我们反序列化时会比较serialVersionUID,由于这时它们的serialVersionUID不同,所以抛出了这个异常。我们可以在类里面为这个变量赋一个固定的值,这样serialVersionUID就不会发送改变,但是赋值也是有要求的
- 必须使用final static long修饰
我们在Person类中加入
private final static long serialVersionUID = 1L; |
然后执行上面的操作,发现没有问题。输出为
Person{age=18, name='迪丽热巴'} //这里age没有使用transient修饰 |
打印流
PrintStream是一个有关于打印流的类,我们一直使用的System.out其中的out就是一个PrintStream对象,下面是其在System类中的定义
public final static PrintStream out = null; |
打印流有以下的特点
- 它只负责数据的输出
- 永不抛出IOExecption
- 它继承了OutputStream类
- 它有自己的特有方法,如print(), println(),这两个方法可以打印出任意的数据类型
- 当它调用OutputStream类的方法write()时,打印数据时会查编码表,如write(97)会打印出a,但是print(),println()方法输入什么,打印出什么,如print(97)打印出97。
下面简单介绍使用,首先看PrintStream的构造方法
- PrintStream(File file)
- PrintStream(OutputStream out)
- PrintStream(String filename)
PrintStream printStream = new PrintStream("a.txt"); |
打开a.txt文件,里面的内容为
a97 |
System类有一个方法setOut(PrintStream out),可以用来改变System.out的指向,这样打印出的内容不会在控制台显示,而是会在文件中
PrintStream printStream = new PrintStream("a.txt"); |
运行程序发现控制台没有任何的输出
而在a.txt中出现了打印的语句。