黑马程序员——IO流5:File类

------ Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

IO流5:File类

1 File概述

1.1 File简介

在前面的博客中,我们介绍了若干IO流类,这些类大多都有一个共同点——操作的都是硬盘文件中的数据(当然也有操作内存中数据的IO流,我们目前还没有介绍)。而文件这一类事物,就是作为数据的载体而存在的,它们与其他事物一样也具备很多的属性,比如文件名、文件格式、文件所在路径、文件创建时间以及文件大小等等。由此,我们可以说“文件”是一种复杂而常见的事物,那么按照面向对象的思想,有必要通过定义一个类来对其进行描述,进而创建它的对象,并对其进行操作,那么这个类就是File。当然,文件系统中除了有大量各种类型的文件以外,还有用于归纳整理这些文件的文件夹,文件夹也有其自身特有的属性,因此File类同样可以用于描述文件夹这一类事物。简言之,File类就是用来将文件或者文件封装成对象的类,以便于对文件与文件夹进行各种操作。

我们需要提醒大家的是,虽然IO流也能够创建文件,但是它最根本的功能是对文件中的数据进行操作,而不能对文件的各种属性信息进行操作,而这一任务就可以交由File类去完成,这也就是File与IO流的区别。

1.2 File类API文档

File类同样是包含在Java标准类库java.io包中,以下为其部分API文档内容,

API文档描述:

       文件和目录路径名的抽象表示形式。

       说明一点,以下将要介绍的字段和方法中,重点介绍的将会用黑体字标识,并在后面的内容中通过代码演示。

字段:

public static finalString separator:与系统有关的默认名称分隔符,也就是路径分隔符。为了方便,它被表示为一个字符串。该字段的作用就是用于提高Java程序的移植性,在不同的系统下,代表不同系统中的路径分隔符。

构造方法:

public File(String pathname):通过将给定路径名字符串转换为抽象路径名来创建一个新File实例。如果给定字符串是空字符串,那么结果是空抽象路径名。该方法可以用于创建文件,也可以用于创建文件夹,参数pathname就是用于表示文件的路径名或者文件的路径名+文件名。

public File(String parent,String child):根据parent路径名字符串和child路径名字符串创建一个新File实例。参数parent表示父目录路径,child表示子目录路径。

以上两个构造方法功能看似是重复的,但是如果我们需要在同一个目录下循环创建文件名连续(以连续数字结尾)的多个文件时,就可以使用第二种构造方法,把我们创建的用于表示文件名的字符串对象作为参数传递到第二个参数位置上。

public File(File parent,String child):根据parent抽象路径名和child路径名字符串创建一个新File实例。该构造方法与第二中类似,只是将父目录路径名封装为了File对象并传递。

方法:

(1)    创建:

public boolean createNewFile() throws IOException:当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件。也就是说,在创建了File对象以后,必须调用该方法才能真正在硬盘中创建指定的文件。创建成功的前提是在指定路径下不存在指定文件名的文件,这就需要在创建文件以前,调用exists(下面将要说到)方法进行判断。此外,由于Java语言需要调用系统底层资源来创建文件,因此若在创建过程中出现异常,就会抛出IOException。

pubic static File createTempFile(String prefix,String suffix) throws IOException:在默认临时文件目录中创建一个空文件,使用给定前缀和后缀生成其名称。所谓临时文件,就是在程序运行过程中,临时创建一些文件,用于记录一些数据信息,而在程序运行完毕后,这些文件也就失去了作用,应该被删掉。该方法还有一个重载形式,可以指定临时文件创建的目录,有兴趣的朋友可以自行查阅API文档,这里不再赘述。

public boolean mkdir():创建此抽象路径名指定的目录。该方法只能创建单级目录,也就是说,如果指定的路径中包含不存在的文件夹,那么子目录将创建失败。如果传递的路径时相对路径,那么将在当前目录下(源代码文件所在目录下)创建指定文件夹。

public boolean mkdirs():创建此抽象路径名指定的目录,包括所有必须但不存在的父目录。该方法同样用于创建目录,而与前一个方法的不同之处在于,它可以创建从根父目录开始到子目录之间所有不存在文件夹。传递相对路径时,与mkdir方法的作用相同。

(2)    删除:

public boolean delete():删除此抽象路径名表示的文件或目录。如果此路径名表示一个目录,则该目录必须为空才能删除。同样,该方法的返回值类型为boolean,以此表示是否删除成功。实际上该方法被用到了写入流的构造方法中,当我们创建写入流对象并为其初始化一个文件时,写入流构造方法内会将该路径封装为一个File对象,并判断该文件是否存在(通过exists方法),若存在就通过delete方法,将该文件删除,再调用createNewFile方法创建一个同名空文件,这也就是写入流覆盖源文件的原理。

public void deleteOnExit():在虚拟机终止时,请求删除此抽象路径名表示的文件或目录。该方法通常应用于临时文件的删除,原因是程序可能会由于一些问题的产生而无法正常关闭,那么通常最后执行的文件删除代码将会无法执行到。可能有的朋友想到将这一动作定义在finally代码块中,但是如果该临时文件还处于被使用的状态下(程序没有执行完毕时)时,是无法被删除的。因此,调用该方法,就可以保证虚拟机无论以什么方式退出,该文件都将被删除。注意,该方法的返回值为void。

(3)    判断:

public boolean canExecute():测试应用程序是否可以执行此抽象路径名表示的文件。

public boolean canRead():测试应用程序是否可以读取此路径名表示的文件。

public boolean Write():测试应用程序是否可以修改此路径名表示的文件。

public intcompareTo(File pathname):按字母顺序比较两个抽象路径名。该方法可以用于令指定的若干文件按照首字母顺序进行排序。

public boolean exists():测试此抽象路径名表示的文件或目录是否存在。该方法通常在创建新文件以前,对指定文件进行判断,只有返回值为false(也即,指定文件不存在),才继续调用createNewFile创建文件。

public boolean isDirectory():测试此抽象路径名表示的文件是否是一个目录。

public boolean isFile():测试此抽象路径名表示的文件是否是一个标准文件。

public boolean isHidden():测试此抽象路径名指定的文件是否是一个隐藏文件。隐藏的具体定义与系统有关。对于一些系统级目录或者系统相关文件,Java程序是不能访问的,而这些文件或文件夹通常都是隐藏的,因此当我们通过Java程序试图访问这一类路径时,应先进行判断是否是隐藏路径,以此提高访问效率。

public boolean isAbsolute():测试此抽象路径名是否为绝对路径名。在UNIX系统上,如果路径名的前缀是“/”,那么该路径名是绝对路径名。在Microsoft Windows系统上,如果路径名的前缀是后跟“\\”的盘符,或者是“\\\\”,那么该路径名是绝对路径名。该方法用于判断,用户指定的封装于File对象中的路径是否是绝对路径。

(4)    获取信息:

public String getName():返回此抽象路径名表示的文件或目录的名称。该名称是路径名名称序列中的最后一个名称。也就是说,无论此File对象内封装的是相对路径还是绝对路径,均亦字符串的形式返回末尾路径名(可能是文件名,可能是文件夹)。

public String getPath():将此抽象路径名转换为一个路径名字符串。该方法以字符串的形式,返回封装于此File对象内的路径,指定什么路径,返回什么路径。

public String getParent():返回此抽象路径名父目录路径名字符串;如果此路径名没有指定父目录,则返回null。该方法以字符串的形式返回此File对象中封装的除末尾路径以外的路径,若只包含一级路径,将返回null。

public String getAbsolutePath():返回此抽象路径名的绝对路径名字符串。该方法与getPath的区别在于,无论此File对象内封装的是相对路径还是绝对路径,均以字符串的形式返回绝对路径。

public File getAbsoluteFile():返回此抽象路径名的绝对路径名形式。与前一个方法的区别在于,该方法将绝对路径封装为了File对象。

public long lastModified():返回此抽象路径名表示的文件最后一次被修改的时间。该方法可以用于判断指定文件是否被修改过。注意该方法的返回值类型是long。

(5)    其他方法

public boolean renameTo(File dest):重新命名此抽象路径名表示的文件。

以上是File类中一些较为常用且相对简单的方法,我们将在下面内容中,对其中被标为黑体字的方法进行演示。

需要说明的是,除以上这些方法以外,还有若干重载方法——list系列重载方法,因此应用广泛且涉及较为复杂的应用,我们将单独开辟一节的内容,对其进行说明和演示。

2 File类应用演示

2.1 构造方法演示

代码1:

import java.io.*;
 
class FileDemo
{
	public static void main(String[] args)
	{
		//通过指定文件名(包含路径名)创建File对象
		//也就是说,将“Demo.txt”文件封装成了一个File对象
		File file1 = new File("D:\\java_samples\\20th_day\\files\\Demo.txt");
 
		//通过分别指定的父目录路径名和子目录路径名创建File对象
		File file2 = new File("D:\\java_samples\\20th_day\\files","Demo2.txt");
 
		//通过分别指定的父目录路径名和子目录路径名创建File对象
		//其中父目录路径名需通过File对象封装
		File file3 = new File(newFile("D:\\java_samples\\20th_day\\files"),"Demo3.txt");
 
		//通过代表路径分隔符的字段创建File对象
		File file4 = new File("D:"+File.separator+"java_samples"+
                                                 File.separator+"20th_day"+File.separator+"files"+
                                                 File.separator+"Demo4.txt");
 
		System.out.println("file1:"+file1);
		System.out.println("file2:"+file2);
		System.out.println("file3:"+file3);
		System.out.println("file4:"+file4);
	}
}
执行结果为:

file1:D:\java_samples\20th_day\files\Demo.txt

file2:D:\java_samples\20th_day\files\Demo2.txt

file3:D:\java_samples\20th_day\files\Demo3.txt

file3:D:\java_samples\20th_day\files\Demo4.txt

       以上代码中,分别通过四种不同的方式,创建了四个File对象,直接打印这四个对象,输出结果为封装于其中的相对路径。所谓相对路径,就是向构造方法中传递的路径,那么直接打印File对象,通过调用toString方法返回的就是相对路径。假如我们不为file1对象指定父目录路径名(比如,构造函数中只传递"Demo.txt "),那么打印结果就只有文件名。相对应的,绝对路径就是指的一个文件或文件夹的完整路径名,而当我们指定一个相对路径时,就表示在当前目录路径下,创建指定文件夹或者文件,那么此时绝对路径就是:当前目录路径名+相对路径名+文件名。

       此外,我们在创建file4指向的File对象时,使用了File字段separator,那么该语句就具备了可移植性,在任何系统中都可以直接运行,而不必修改代码。

最后需要提醒大家的是,并不是说创建了File对象,就等同于创建了文件,还需要在判断指定路径是否存在(exists方法)以后,再通过createNewFile方法创建文件。

2.2 方法演示

在前面的内容中,我们结合API文档简单介绍了File类的部分方法,下面我们选择其中的常用方法进行简单演示。

2.2.1 创建

代码2:

import java.io.*;
 
class FileDemo2
{
	public static void main(String[] args) throws IOException
	{
		File file = new File("D:\\java_samples\\20th_day\\files\\Demo.txt");
 
		//创建指定文件,并打印创建结果
		System.out.println("FileCreate:"+file.createNewFile());
		System.out.println("FileCreate:"+file.createNewFile());
 
		File file2 = new File("D:\\java_samples\\20th_day\\files\\parent\\sub");
 
		//创建单级目录
		System.out.println("mkdir:"+file2.mkdir());
		//创建多级目录
		System.out.println("mkdirs:"+file2.mkdirs());
	}
}
执行结果为:

FileCreate:true

FileCreate:false

mkdir:false

mkdirs:true

代码说明:

(1)  第一次创建file指向对象所封装的文件时,返回true表示创建成功,而再次创建同一个文件时,由于指定文件已存在而创建失败,这就体现了与写入流的不同。

(2)  file2指向的对象中封装的是单纯的目录,而其中parent和sub文件夹都是不存在的,因此用于创建单级目录的mkdir方法调用失败,而mkdirs成功创建了parent和sub文件夹。

2.2.2 删除

代码3:

import java.io.*;
 
class FileDemo3
{
	public static void main(String[] args) throws IOException
	{
		File file = new File("D:\\java_samples\\20th_day\\files\\Demo.txt");
		//只要退出虚拟机,就将该文件删除。
		file.deleteOnExit();
		file.createNewFile();
 
		//删除文件,并打印删除结果
		System.out.println("FileDelete:"+file.delete());
		//再次尝试删除
		System.out.println("FileDelete:"+file.delete());
	}
}
执行结果为:

FileDelete:true

FileDelete:false

代码说明:

       尝试删除已经被删除的文件,删除动作将会失败。若某个文件需要在虚拟机退出以后被删除掉,那么最好在创建File对象以后就调用其deleteOnExit方法(可将这一方法看做是一种声明),防止虚拟机异常退出而无法删除该文件。

2.2.3 判断

(1)    canExecute

代码4:

import java.io.*;
 
class FileDemo4
{
	public static void main(String[] args)
	{
		File file = new File("D:\\java_samples\\20th_day\\files\\Demo.txt");
 
		//判断指定文本文件能否执行
		System.out.println("canExecute:"+file.canExecute());
 
		File file2 = new File("D:\\软件\\QQ\\Bin\\QQ.exe");
		//判断指定的“.exe”文件能否执行
		System.out.println("canExecute:"+file2.canExecute());
	}
}
执行结果为:

canExecute:false

canExecute:true

代码说明:

       以上代码中,我们首先判断一个文本文件能够执行,结果显然是“false”;然后判断一个“.exe”文件能否执行,结果是“true”。那么由此,我们可以联想到:当我们需要通过Java程序来控制其他程序的运行时,只需要获取其主程序文件名和所在路径,并对其进行判断,如果是可执行文件,就通过Runtime对象的exec方法启动它即可。

(2)    exists

代码5:

import java.io.*;
 
class FileDemo5
{
	public static void main(String[] args)
	{
		File file = new File("D:\\java_samples\\20th_day\\files\\Demo.txt");
 
		//判断指定文件是否存在
		System.out.println("exists:"+file.exists());
 
		File file2 = new File("D:\\java_samples\\20th_day\\FileDemo5.java");
 
		//判断本源代码文件是否存在
		System.out.println("exists:"+file2.exists());
	}
}
执行结果为:

exists:false

exists:true

       由于并没有调用createNewFile方法,因此“Demo.txt”文件并不存在,exists调用结果为false;而以上代码所在源代码文件显然是存在的,因此调用该对象的exists方法返回true。由此,我们说exists方法与createNewFile方法的结合就可以避免创建写入流对象时,可能造成的文件覆盖问题,这也就体现了将文件封装为一个对象的好处。

(3)    isDirectory&isFile

代码6:

import java.io.*;
 
class FileDemo6
{
	public static void main(String[] args) throws IOException
	{
		File file = new File("D:\\java_samples\\20th_day\\files\\Demo.txt");
 
		//判断File对象所封装的是否是目录
		System.out.println("isDirectory:"+file.isDirectory());
		//判断File对象所封装的是否是目录
		System.out.println("isFile:"+file.isFile());
		System.out.println();
 
		//首先判断文件是否存在
		if(!file.exists())
			file.createNewFile();
 
		//判断File对象所封装的是否是目录
		System.out.println("isDirectory:"+file.isDirectory());
		//判断File对象所封装的是否是目录
		System.out.println("isFile:"+file.isFile());
	}
}
以上代码的执行结果为:

isDirectory:false

isFile:true

 

isDirectory:false

isFile:true

从结果上看,前后两次的判断结果是不同的,这是因为只要文件(或文件夹)不存在,无论判断其是否是文件还是文件夹,都将返回false;而进行第二次判断前,事先创建了“Demo.txt”文件,因此可以判断出它是一个文件。

可能有朋友想当然的认为,带有格式后缀名的必然是文件,比如“Demo.txt”,但这并不总是对的,比如下面的代码,

代码7:

import java.io.*;
 
class FileDemo7
{
	public static void main(String[] args)
	{
		File file = new File("D:\\java_samples\\20th_day\\files\\Demo.txt");
 
		//将file所封装的路径名创建为一个文件夹而不是文件
		file.mkdir();
 
		//判断File对象所封装的是否是目录
		System.out.println("isDirectory:"+file.isDirectory());
		//判断File对象所封装的是否是目录
		System.out.println("isFile:"+file.isFile());
	}
}
执行结果为:

isDirectory:true

isFile:false

       虽然File对象中封装的末尾路径看似是以“.txt”结尾的文件,但是通过调用mkdir,将其创建为了一个文件夹,因此判断isDirectory反而是true。

(4)    isAbsolute

代码8:

import java.io.*;
 
class FileDemo8
{
	public static void main(String[] args)
	{
		File file =
			new File("E\\java_samples\\20th_day\\Demo.txt");
 
		//判断该目录是否是绝对路径
		System.out.println("isAbsolute:"+file.isAbsolute());
 
		File file2 = new File("FileDemo");
 
		//判断该目录是否是绝对路径
		System.out.println("isAbsolute:"+file2.isAbsolute());
	}
}
执行结果为:

isAbsolute:true

isAbsolute:false

       由前述isAbsolute方法的API描述可知,在Windows系统下,只要File对象中封装的路径前缀包含盘符就判定为绝对路径,因此以上执行结果是显然的。需要注意的是,该方法的成功执行并不需要指定文件或文件夹一定存在。

2.2.4 获取

代码9:

import java.io.*;
 
class FileDemo9
{
	public static void main(String[] args)
	{
		//封装相对路径
		File file = new File("Demo.txt");
 
		System.out.println("name:"+file.getName());
		System.out.println("path:"+file.getPath());
		System.out.println("abspath:"+file.getAbsolutePath());
		System.out.println("parent:"+file.getParent());
		System.out.println();
 
		//封装绝对路径
		File file2 = new File("D:\\java_samples\\20th_day\\files\\Demo2.txt");
 
		System.out.println("name:"+file2.getName());
		System.out.println("path:"+file2.getPath());
		System.out.println("abspath:"+file2.getAbsolutePath());    
		System.out.println("parent:"+file2.getParent());
	}
}
执行结果为:

name:Demo.txt

path:Demo.txt

abspath:D:\java_samples\20th_day\Demo.txt

parent:null

 

name:Demo2.txt

path:D:\java_samples\20th_day\files\Demo2.txt

abspath:D:\java_samples\20th_day\files\Demo2.txt

parent:D:\java_samples\20th_day\files

       注意到当File对象中仅封装了一级路径时(比如file指向的对象,只封装了一个文件),调用其getParent方法将返回null。

2.2.5 其他方法

代码10:

import java.io.*;
 
class FileDemo10
{
	public static void main(String[] args) throws IOException
	{
		File file1 = new File("D:\\java_samples\\20th_day\\files\\Source.txt");
		if(!file1.exists())
			file1.createNewFile();
 
		//为了看到效果暂时将主线程冻结4秒钟
		try{Thread.sleep(4000);}catch(InterruptedExceptione){}
 
		File file2 = new File("D:\\java_samples\\20th_day\\files\\Dest.txt");
 
		System.out.println("rename:"+file1.renameTo(file2));
	}
}
执行结果为:

rename:true

rename:true

执行以上代码,首先在指定路径中创建了一个“Source.txt”文件,过了4秒后“Source.txt”文件消失,“Dest.txt”文件出现,再过4秒后,“Dest.txt”文件消失,在E盘根目录下将出现“Dest2.txt”文件。那么该方法其实并不仅仅是对文件夹或者文件名进行简单的修改,而是将封装于File对象中的路径修改为目标路径(目标路径也需要封装为File对象),并在修改的同时,将源文件或文件夹删除,依照目标路径创建新的文件或文件夹。

3 list系列重载方法

3.1 list方法简介及演示

list系列重载方法共有6个方法,下面分别对这六个方法进行说明,并通过例程进行方法演示。

(1)    listRoots()

public static File[]listRoots():列出可用的文件系统根。该方法分别将本机所有的有效磁盘分区封装为File对象并存储到一个File数组中,并放回该数组。由于该方法并不涉及特有数据,而是有本机的硬盘系统决定,因此该方法为静态方法。

方法演示:

代码11:

import java.io.*;
 
class FileDemo11
{
	public static void main(String[] args)
	{
		//获取有效盘符File对象数组
		File[] files = File.listRoots();
		//通过高级for循环,遍历数组,打印磁盘盘符
		for(Filefile : files)
		{
			System.out.println(file);
		}
	}
}
执行结果为:

C:\

D:\

E:\

F:\

H:\

       通过listRoots方法列出了本机所有的磁盘分区的盘符。当我们希望将指定文件创建到一个绝对路径中时,该路径首先要指定一个盘符,那么该方法的作用也就体现出来了。注意,若调用代表磁盘分区的File对象的length方法,返回值将是0,说明我们不能指望通过这个方法获取到某个硬盘分区的大小。

(2)    list()

public String[] list():返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录。如果此抽象路径名不表示一个目录(表示一个文件),那么此方法将返回null。若指定路径末尾的文件夹并不存在,将会抛出NullPointerException异常。

方法演示:

代码12:

import java.io.*;
 
class FileDemo12
{
	public static void main(String[] args)
	{
		//将E盘根目录作为路径封装到File对象中
		File file = new File("E:\\");
		//获取包含指定路径下所有文件和文件夹的名称的字符串数组
		String[] names = file.list();
		for(String name : names)
		{
			System.out.println(name);
		}
	}
}
执行以上代码,将会在控制台中列出E盘根目录下所有文件和文件夹的名称。那么将list方法和listRoots方法集合起来,就可以列出所有磁盘根目录下的文件夹和文件名称。需要注意的是,该方法默认列出指定路径中的隐藏文件。

(3)    list(FilenameFilter filter)

public String[] list(FilenameFilter filter):返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中满足指定过滤器的文件和目录。该list重载方法的参数类型为FilenamFilter,表示文件名过滤器类。

由于FilenameFilter对于此list重载方法的重要性,有必要通过其API文档对其进行了解,

API文档描述:

       实现此接口的类实例可用于过滤文件名。

方法:

boolean accept(Filedir,String name):测试指定文件是否应该包含在某一文件列表中。

       由以上内容可知,我们需要自定义一个实现FilenameFilter接口的文件名过滤器类,并复写其accept方法,并在该方法内定义过滤条件。该方法的运作原理是,在将某个文件名字符串存储到字符串数组以前,调用FilenameFilter实现类对象的accept方法,并对其返回值进行判断,只有当被传递到accept方法的文件名,满足过滤条件时才返回true,同时将被检测的文件名字符串存储到数组中。由于该接口中只定义了一个方法,因此可以通过匿名内部类的方式定义。

(4)    listFiles()&listFiles(FileFilter filter) & listFiles(FilenameFilter filter)

public File[] listFiles():返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件。

public File[] listFiles(FileFilter filter):返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。

public File[] listFiles(FilenameFilter filter):返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。

       以上这三个方法功能上与list()或者list(FilenameFilterfilter)有些类似,都是获取到指定路径下的文件和文件夹,区别在于list重载方法只能获取到文件或文件夹名称的字符串形式,而以上三个方法可以获取到File对象形式的文件或者文件夹,因此可以实现对这些文件更为复杂的操作。由于使用方法类似,这里仅对listFiles方法进行演示,

代码13:

import java.io.*;
 
class FileDemo13
{
	public static void main(String[] args)
	{
		File file = new File("D:\\java_samples\\20th_day");
		//通过匿名类的方式创建FilenameFilter实现类对象
		File[] files = file.listFiles();
		for(File singlefile : files)
		{
			//打印每个文件的名称及大小
			System.out.println("filename:"+singlefile.getName()+",length:"+singlefile.length());
		}
	}
}
执行以上代码将会把指定目录下所有文件和文件夹的名称及大小打印到控制台,当然文件夹的大小均为0。

3.2 list方法应用——利用递归遍历目录

(1)    利用递归遍历目录

在以上内容中,通过list方法我们只能列出某一级目录下的所有文件和文件夹,但很多时候我们希望指定一个路径以后,可以列出该路径内的所有内容——包括子目录下的文件,以及更深层目录的内容,以此类推。

要想实现以上功能,首先还是要从遍历第一级目录开始。我们假设第一级目录——起名为MainDir——中既包含文件也包含文件夹,在通过listFiles方法获取到代表其中所有内容的File数组后,开始遍历该数组,并判断每个File对象是否是文件夹,如果判断出文件夹——假设称为SubDir——我们就希望转而开始对这个File对象中的内容进行遍历。我们当然可以在if语句中继续嵌套一个循环以遍历SubDir中的内容,但是如果子目录很多我们将会面对非常庞大而复杂的嵌套循环,因此这种方式显然是不可取的。

那么针对以上需求一个简单的解决办法就是将遍历文件夹的功能定义为一个方法,比如称为iterateDir,方法参数为需要被遍历的路径File对象,那么当判断出某个File对象是文件夹以后,就在循环内继续调用iterateDir方法,并将该File对象作为参数传递,那么这种函数调用函数自身的方法称为递归,以上功能的代码体现如下,

代码14:

import java.io.*;
 
class FileDemo14
{
	public static void main(String[] args)
	{
		File file = new File("E:\\安奎星\\视频\\毕向东Java视频\\java_samples");
		iterateDir(file);
	}
	public static void iterateDir(File path)//递归方法
	{
		//首先打印被遍历的路径名
		System.out.println(path.getAbsolutePath());
 
		//获取到File数组以后开始遍历路径中的内容
		File[] files = path.listFiles();
		for(int x=0; x<files.length; x++)
		{
			//判断File对象是否是目录
			if(files[x].isDirectory())
				iterateDir(files[x]);//若为目录,继续调用iterateDir方法自身
			else
				System.out.println(files[x]);
		}
	}
}
通过执行以上代码就可以遍历某指定路径内的所有内容了。

(2)    递归原理举例演示

       那么为了更为清晰的了解递归调用函数的过程,我们举两个简单的例子进行说明。第一个例子是十进制转二进制的方法,方法体如下所示,

代码15:

public static void main(String[] args)
{
	toBin(6);
}
public static void toBin(intnum)
{
	//判断所传参数是否大于0
	if(num > 0)
	{
		//若大于0,继续调用toBin方法自身,并传递参数除以2的值
		toBin(num/ 2);
		System.out.print(num% 2);
	}
}
以上代码的执行过程如下如所示, 

 

上图中为演示方便,将变量替换为其代表的实际值,其中实线表示递归调用函数的顺序,虚线表示函数结束执行的顺序。

执行代码后,首先在主函数第一次调用toBin(简称第一toBin)方法并传递6,由于6大于0,再次调用toBin(简称第二toBin)方法,并传递6除以2的结果3,此时由于第二次调用toBin方法的语句并没有结束执行,因此下面的输出语句也没有被执行;在第二toBin方法内,3仍然大于0,再次调用toBin(第三toBin),传递1;第三toBin内,1大于0,继续调用toBin(第四toBin),传递0;第四toBin中,不满足判断条件,第四toBin方法执行结束,此时相当于第三toBin方法内的第四toBin调用语句执行完毕,这样才能继续执行下面的输出语句,打印1 % 2的值1;此时第二toBin中第三toBin调用语句执行结束,执行输出语句打印3 % 2的值1;最后,第一toBin中的第二toBin调用语句也执行完毕,打印6 % 2的值0,最终的打印结果为:110。

       第二个例子是通过递归的方式实现求和的方法,方法体如下,

代码16:

public static void sum(int num)
{
	if(num == 1)
		return1;
	returnn + sum(num -1);
}
假设我们第一次调用sum方法时,传递参数为3,那么其执行过程如下图所示, 

 

上例的执行过程较为简单,不再进行详细说明,最终的返回值为6。

那么通过以上三个例程,我们对于递归这种方法可以做出如下两点总结:

第一,无条件的递归将会进入无限递归状态,换句话说,由于没有定义结束条件,函数将不停的调用函数自身,直到内存溢出程序崩溃。比如,以下代码所示即为无条件递归,

代码15:

public static void method()
{
	method();
}
相反,在代码14、15、16中,均定义了递归结束条件,比如代码14中,总有一个子目录下是没有文件夹的,那么在遍历完该子目录内的文件后,将会进行跳转并继续对先前暂停的直接父目录进行遍历,并一层一层向外跳转,直到跳转至我们最初指定的路径为止,重复这一过程,最终结束循环;代码15的结束条件就是toBin方法在不断自我调用的同时,传递的参数不断减小,直到参数值等于0,停止递归;代码16与15类似——每调用一次sum方法传递的参数就减1,直到其值等于1为止。

第二,由第一点可以推断出,即使定义了递归结束条件,但如果递归调用函数的次数过多,同样会由于内存溢出造成程序崩溃,因此应尽可能控制递归次数。例如,假设我们通过代码16计算1到8000的总和,就会出现内存溢出的现象。这是因为方法的执行需要在栈内存中开辟一片空间供其进行运算,如果需要计算直到8000的总和,那意味着需要开辟8000个栈内存空间,并且先前开辟的空间由于方法未结束运算而不能得到释放,那么当方法所占的内存大小超过了Java虚拟机默认开辟的内存空间,就会导致内存溢出的结果。

(3)    优化递归遍历目录的显示效果

在前述代码14中,我们通过递归的方法,遍历并打印了指定路径中的所有文件和文件夹。但是由于并没有显示目录与文件之间的层级效果,因此打印结果显得比较乱。根据这一需求,对代码14做一简单优化。

我们希望的显示效果为:

主目录

|  |--子目录

| |  |--文件A

| |  |--文件B

根据这一显示效果,在每打印一个子目录或者文件前,首先要确定该目录或文件所属的层级。主目录默认为0级目录,第一子目录为1级,以此类推。确定了层级以后,就在目录名前添加n个“| ”符号和一个“|--”,其中n表示目录层级。按照这一思路,修改后的代码如下,

代码17:

import java.io.*;
 
class FileDemo15
{
	public static void main(String[] args)
	{
		File file = new File("E:\\安奎星\\视频\\毕向东Java视频\\java_samples");
		//主目录默认层级为0
		iterateDir(file,0);
	}
	public static void iterateDir(File path, int level)
	{
		//每调用一次iterateDir,首先打印层级符号和路径名
		System.out.println(getLevel(level)+path.getAbsolutePath());
		//层级数自增,以便继续传递并表示次级目录层级
		level++;
 
		File[] files = path.listFiles();
		for(int x=0; x<files.length; x++)
		{
			if(files[x].isDirectory())
				//再次调用iterateDir方法时,除传递路径名,同时传递层级
				iterateDir(files[x],level);
			else
				System.out.println(getLevel(level)+ files[x]);
		}
	}
	//根据层级数打印层级符号的方法
	public static String getLevel(int level)
	{
		StringBuilder sb= new StringBuilder();
		//在最靠近路径名处添加“|--”
		sb.append("|--");
		//某个路径时多少层级就添加多少个“|  ”符号
		for(int x=0; x<level; x++)
		{
			sb.insert(0,"|  ");
		}
		//将层级符号作为字符串返回
		return sb.toString();
	}
}
通过以上代码就可以显示出目录之间的层级效果。

(4)    递归的另一个应用——删除带内容的目录

在我们常用的Windows系统中,删除一个带内含子目录或文件的目录并不是表面上的操作——右键删除——那样简单,其底层的实现原理是由内到外依次删除(子目录和文件),直到最外层文件夹为止。当然,如果指定目录本身即是空的那么,直接删除即可。那么对于包含内容的目录,就需要用到递归的方法来时实现对它的删除。

基本原理与遍历指定路径中所有内容的方法是相似的,为使用递归原理,将删除目录的功能封装为一个方法,比如称其为deleteDir,方法体内通过listFiles方法获取到主目录下所有内容的File对象数组,然后开始对其进行遍历,遍历的同时判断是否是文件夹:如果是文件夹就继续调用deleteDir方法自身,并将这个表示文件夹的File对象作为参数传递;如果不是就调用delete方法,将该文件删掉,并打印删除结果。删除完某个文件夹中的内容后,最终还要将该目录本身也删除掉。

那么为了便于观察以上功能是否运行成功,可以在调用delete方法删除文件或文件夹的同时,将删除结果(也即delete方法的返回值)打印到控制台,如果删除结果为false,那就表明出现了误删除操作——对一个文件或文件夹进行了重复删除。代码如下所示,

代码18:

import java.io.*;
 
class FileDemo16
{
	public static void main(String[] args)
	{
		File path = new File("D:\\java_samples\\20th_day\\deleteTest");;
		deleteDir(path);
 
	}
	public static void deleteDir(File path)
	{
		File[] files = path.listFiles();
		for(int x=0; x<files.length; x++)
		{
			if(files[x].isDirectory())
				deleteDir(files[x]);
			else
				//删除文件,并打印删除结果
				System.out.println(files[x].toString()+"--file--"+files[x].delete());
		}
 
		//删除本文件夹,并打印删除结果
		System.out.println(path.toString()+"--path--"+path.delete());
	}
}
以上代码执行结果如下所示:

D:\java_samples\20th_day\deleteTest\新建文件夹\新建文件夹\新建文本文档 -副本 (2).txt--file--true

D:\java_samples\20th_day\deleteTest\新建文件夹\新建文件夹\新建文本文档 -副本.txt--file--true

D:\java_samples\20th_day\deleteTest\新建文件夹\新建文件夹\新建文本文档.txt--file--true

由于篇幅所限,这里只呈现部分结果。通过以上方法就,利用递归原理,就将指定目录中的所有内容没有重复的删除掉了。

       为了观察到错误现象,我们可以将deleteDir方法中的“else”关键字删除掉,如下代码所示,

代码19:

//else
System.out.println(files[x].toString()+"--file--"+files[x].delete());
也就是说,即使一个File对象被判断出是文件夹以后,通过递归调用deleteDir方法删除掉了,还要重复的被删除一次,那么其执行结果如下所示,

D:\java_samples\20th_day\deleteTest\新建文件夹\新建文件夹--path--true

D:\java_samples\20th_day\deleteTest\新建文件夹\新建文件夹--file—false

“新建文件夹”首先被当做文件成功删掉了,然后又作为文件再次被删除了一次,显然删除结果为false,也就是出现了误删操作。

       关于递归删除指定目录的方法,不建议大家去应用到系统所在目录(一般为C盘)。因为只要是系统相关文件或文件夹,不是被隐藏起来,就是无法被Java程序所访问,因此调用封装这些文件或文件夹的File对象listFiles方法,返回值将为空,最终导致发生空指针异常,程序停止。因此,为了避免这一错误的放生,在判断File对象是否是目录的同时,还要判断该File对象锁封装的路径是否是隐藏的,如下代码所示

代码20:

if(!files[x].isHidden() && files[x].isDirectory())
	deleteDir(files[x]);
注意在调用isHidden方法前加上取反符号“!”。

4 File类练习

需求:将一个指定目录下所有Java文件的绝对路径,存储到一个文本文件中,建立一个Java文件列表文件。

思路:

1. 对指定的目录通过递归的方法进行遍历;

2. 获取在遍历过程中判断出来的所有Java文件的File对象封装形式;

3. 将以上表示路径的File对象存储到一个集合中,这样的好处是可以对这些路径进行更为多样的操作;

4. 将上述集合中的路径写入到一个文本文件中。

在实现以上需求之前,我们再次复习一下IO流的操作规律,并按照这个规律选择我们应使用的流对象。

第一步,明确数据源和目的。由于本例中数据源是存在于内存中的集合,因此不需要读取流,只需要一个写入流即可;

第二步,写入流包括OutputStream和Writer,由于写入目的地是文本文件,因此选择Writer子类;

第三步,写入流操作的是一个文件,那么目的设备就是硬盘,因此最终选择的写入流类就是FileWriter。

第四步,我们希望提高写入效率,使用一个BufferedWriter对象将FileWriter对象封装起来,最终的流对象创建代码为:

BufferedWriterbufw = newBufferedWriter(new FileWriter((“路径”));

代码:

代码19:

import java.io.*;
import java.util.*;
 
class JavaFileList
{
	public static void main(String[] args)
	{
		File dir = new File("E:\\安奎星\\视频\\毕向东Java视频\\java_samples");
		ArrayList<File> list = new ArrayList<File>();
		fileToList(dir,list);
 
		//创建目的文件,用于存储所有的Java文件路径
		File destFile = new File(dir,"20th_day\\files\\JavaFileList.txt");
		try
		{
			if(!destFile.exists())
				System.out.println(destFile.createNewFile());
		}
		catch(IOExceptione)
		{
			throw new RuntimeException("文件创建失败!");
		}
 
		writeToFile(list,destFile);
	}
	//将需求功能封装为一个方法,该方法的形参列表中包含一个集合
	public static void fileToList(File dir, List<File> list)
	{
		File[] files = dir.listFiles();
		for(File file : files)
		{
			if(!file.isHidden() && file.isDirectory())
				fileToList(file,list);
			else
			{
				//判断是否是".java"文件
				if(file.getName().endsWith(".java"))
					//如果是".java"文件,则存储到指定集合中
					list.add(file);
			}
		}
	}
	//将路径写入到文件中的功能封装为一个方法,参数列表中的destFile为写入目的文件
	public static void writeToFile(List<File> list, File destFile)
	{
		BufferedWriter bufw = null;
		try
		{
			bufw = new BufferedWriter(new FileWriter(destFile));
			//遍历File数组,同时将其绝对路径写入到目的文件中
			for(File file : list)
			{
				//将每个文件的绝对路径写入到目的文件中
				bufw.write(file.getAbsolutePath());
				bufw.newLine();
				bufw.flush();
			}
		}
		catch(IOException e)
		{
			throw new RuntimeException("文件写入失败!");
		}
		finally
		{
			try
			{
				if(bufw != null)
					bufw.close();
			}
			catch(IOException e)
			{
				throw new RuntimeException("写入流关闭失败!");
			}
		}
	}
}
执行以上代码就可以将指定目录中所有的“.java”文件绝对路径写入到一个文本文件中了。

小知识点1:

在以上代码中,我们将原来存在于内存中的文件路径集合,存储到了一个文件中,也就是存储到了硬盘中,这一过程称之为数据的持久化。顾名思义,由于内存中的数据是不能长期存储的,一旦断电(比如重启或者关机)就会消失,因此需要被反复使用的数据存储到硬盘中是最方便的,也就是令其长久存在。光盘也是同样的道理。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值