Java I/O系统(1):File类

    对程序语言的设计者来说,创建一个好的输入/输出(I/O)系统是一项艰难的任务。现有的大量不同方案已经说明了这一点。挑战似乎来自于要涵盖所有的可能性。不仅存在各种I/O源端和想要与之通信的接收端(文件、控制台、网络链接等),而且还需要以多种不同的方式与它们进行通信(顺序、随机存取、缓冲、二进制、按字符、按行、按字等)。

    java类库的设计者通过创建大量的类来解决这个难题。一开始,可能会对java I/O系统提供了如此多的类感到不知所措(具有讽刺意味的是,java I/O设计的初衷是为了避免过多的类)。自从java 1.0版本以来,java 的 I/O类库发生了明显改变,在原来面向字节的类中添加了面向字符和基于Unicode的类。在JDK1.4中,添加了nio类(对于“新I/O”来说,这是一个从现在起我们将要使用若干年的名称,即使它们在JDK1.4中就已经引入了,因此它们已经“旧”了)添加进来是为了改进性能及功能。因此,在充分理解java I/O系统以便正确的运用之前,我们需要学习相当数量的类。另外,很有必要理解I/O类库的演化过程,即使我们的第一反应是“不要用历史打扰我,只需告诉我怎么用。”问题是,如果缺乏历史的眼光,很快我们就会对什么时候该使用哪些类,以及什么时候不该使用它们而感到迷惑。

    本章就介绍java标准类库中各种各样的类以及它们的用法。

一、File类

    在学习那些真正用于在流中读写数据的类之前,让我们先看一个实用类库工具,它可以帮助我们处理文件目录问题。

    File(文件)类这个名字有一定误导性;我们可能会认为它指代的是文件,实际上却并非如此。它既能代表一个特定文件的名称,又能代表一个目录下的一组文件的名称。如果它指的是一个文件集,我们就可以对此集合调用list()方法,这个方法会返回一个字符数组。我们很容易就可以理解返回的是一个数组而不是某个更灵活性的类容器,因为元素的个数是固定的,所以如果我们想要取得不同的目录列表,只需要再创建一个不同的File对象就可以了。实际上,FilePath(文件路径)对这个类来说是个更好的名字。本节举例示范了这个类的用法,包括了与它相关的FilenameFilter接口。

二、目录列表器

    假设我们想查看一个目录列表,可以用两种方法来使用File对象。如果我们调用不带参数的list()方法,便可以获得此File对象包含的全部列表。然而,如果我们想获得一个受限列表,例如,想得到所有扩展名为.java的文件,那么我们就要用到“目录过滤器”,这个类会告诉我们怎样显示符合条件的File对象。

    下面是一个示例,注意,通过使用java.utils.Arrays.sort()和String.CASE_INSENSITIVE.ORDERComparator,可以很容易的对结果进行排序(按字母顺序)。

import java.io.File;
import java.util.Arrays;
import java.util.regex.Pattern;

public class DirList {
	public static void main(String[] args) {
		File path = new File(".");
		String[] list;
		if (args.length == 0) {
			list = path.list();
		} else {
			list = path.list((dir, name) -> {
				Pattern pattern = Pattern.compile(args[0]);
				return pattern.matcher(name).matches();
			});
		}
		Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
		for (String dirItem : list) {
			System.out.println(dirItem);
		}
	}
}

    这里,使用Lambda“实现”了FilenameFilter接口。请注意FilenameFilter接口是多么的简单:

@FunctionalInterface
public interface FilenameFilter { 
    boolean accept(File dir, String name);
}

    通过Lambda表达式实现accept()方法提供list()使用,使list()可以回调accept(),进而决定哪些文件包含在列表中。因此,这种结构常常称为回调。更具体的说,这是一个策略模式的例子,因为list()实现了基本的功能,而且按照FilenameFilter的形式提供了这个策略,以便完善list()在提供服务时所需的算法。因为list()接受FilenameFilter对象作为参数,这意味着我们可以传递实现了FilenameFilter接口的任何类的对象,用以选择(甚至在运行时)list()方法的行为方式。策略的目的就是提供了代码行为的灵活性。

    accept()方法必须接受一个代表某个特定文件所在目录的File对象,以及包含了那个文件名的一个String。记住一点:list()方法会为此目录对象下的每个文件名调用accept(),来判断该文件是否包含在内;判断结果由accept()返回的布尔值表示。

    accept()方法必须接受一个代表某个特定文件所在目录的File对象,以及包含了那个文件名的一个String。记住一点:list()方法会为此目录对象下的每个文件名调用accept(),来判断该文件是否包含在内;判断结果由accept()返回的布尔值表示。

    accept()会使用一个正则表达式的matcher对象,来查看此正则表达式regex是否匹配这个文件的名字。通过使用accept(),list()方法会返回一个数组。

三、目录实用工具

    程序设计中一项常见的任务就是在文件集上执行操作,这些文件要么在本地目录中,要么遍布于整个目录树中。如果有一种工具能够为你产生这个文件集,那么它会非常有用。下面的使用工具类就可以通过使用local()方法产生由本地目录中的文件构成的File对象数组,或者通过使用walk()方法产生给定目录下的由整个目录树中所有文件构成的List(File对象比文件名更有用,因为File对象包含更多的信息)。这些文件是基于你提供的正则表达式而被选中的:

import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;

public class Directory {
	public static File[] local(File dir, String regex) {
		return dir.listFiles((directory, name) -> {
			return Pattern.compile(regex).matcher(new File(name).getName()).matches();
		});
	}

	public static File[] local(String path, String regex) {
		return local(new File(path), regex);
	}

	public static class TreeInfo implements Iterable {

		public List files = new ArrayList<>();
		public List dirs = new ArrayList<>();

		@Override
		public Iterator iterator() {
			return files.iterator();
		}

		void addAll(TreeInfo other) {
			files.addAll(other.files);
			dirs.addAll(other.dirs);
		}

		public String toString() {
			return "dirs: " + PPrint.pformat(dirs) + "\n\nfiles: " + PPrint.pformat(files);
		}
	}

	public static TreeInfo walk(String start, String regex) {
		return recurseDirs(new File(start), regex);
	}

	public static TreeInfo walk(File start, String regex) {
		return recurseDirs(start, regex);
	}

	public static TreeInfo walk(File start) {
		return recurseDirs(start, ".*");
	}

	public static TreeInfo walk(String start) {
		return recurseDirs(new File(start), ".*");
	}

	static TreeInfo recurseDirs(File startDir, String regex) {
		TreeInfo result = new TreeInfo();
		for (File item : startDir.listFiles()) {
			if (item.isDirectory()) {
				result.dirs.add(item);
				result.addAll(recurseDirs(item, regex));
			} else if (item.getName().matches(regex)) {
				result.files.add(item);
			}
		}
		return result;
	}

	public static void main(String[] args) {
		if (args.length == 0) {
			System.out.println(walk("."));
		} else {
			for (String arg : args) {
				System.out.println(walk(arg));
			}
		}
	}
}

    local()方法使用被称为listFile()的File.list()的变体来产生File数组。可以看到,它还使用了FilenameFilter。如果需要List而不是数组,你可以使用Arrays.asList()自己对结果进行转换。

    walk()方法将开始目录的名字转换为File对象,然后调用recurseDirs(),该方法将递归地遍历目录,并在每次递归中都收集更多的信息。为了区分普通文件和目录,返回值实际上是一个对象“元组”——一个List持有所有普通文件,另一个持有目录。这里,所有的域都被有意识的设置成了public,因为TreeInfo的使命只是将对象收集起来——如果你只是返回List,那么就不需要将其设置为private,因为你只是返回一个对象对,不需要将它们设置为private。注意,TreeInfo实现了Iterable,它将产生文件,使你拥有在文件列表上的“默认迭代”,而你可以通过声明“.dirs”来指定目录。

    TreeInfo.toString()方法使用了一个“灵巧打印机”类,以使输出更容易浏览。容器默认的toString()会在单个行中打印容器的所有元素,对于大型集合来说,这回变得难以阅读,因此你可能希望使用可替换的格式化机制。下面是一个可以添加新行并缩排所有元素的工具:

import java.util.Arrays;
import java.util.Collection;

public class PPrint {
	public static String pformat(Collection c) {
		if (c.size() == 0) {
			return "[]";
		}
		StringBuilder result = new StringBuilder("[");
		for (Object elem : c) {
			if (c.size() != 1) {
				result.append("\n ");
			}
			result.append(elem);
		}
		if (c.size() != 1) {
			result.append("\n");
		}
		result.append("]");
		return result.toString();
	}

	public static void pprint(Collection c) {
		System.out.println(pformat(c));
	}

	public static void pprint(Object[] c) {
		System.out.println(pformat(Arrays.asList(c)));
	}
}

    pformat()方法可以从Collection中产生格式化的String,而pprint()方法使用pformat()来执行其任务。注意,没有任何元素和只有一个元素这两种特例进行了不同的处理。上面还有一个用于数组的pprint()版本。

    Directory实用工具放在了util包中,以使其可以更容易的被获得。下面的例子说明了你可以如何使用它的样本:

import java.io.File;

import com.buba.util.Directory;
import com.buba.util.PPrint;

public class DirectoryDemo {
	public static void main(String[] args) {
		PPrint.pprint(Directory.walk(".").dirs);
		for (File file : Directory.local(".", "T.*")) {
			System.out.println(file);
		}
		System.out.println("----------------------");
		for (File file : Directory.walk(".", "T.*\\.java")) {
			System.out.println(file);
		}
		System.out.println("=======================");
		for (File file : Directory.walk(".", ".*[zZ].*\\.class")) {
			System.out.println(file);
		}
	}
}

    你可能需要更新一下在字符串章节中学习到的有关正则表达式的知识,以理解在local()和walk()中的第二个参数。

    我们可以更进一步,创建一个工具,它可以在目录中穿行,并且根据Strategy对象来处理这些目录中的文件(这是策略设计模式的另一个示例):

import java.io.File;
import java.io.IOException;

public class ProcessFiles {
	public interface Strategy {
		void process(File file);
	}

	private Strategy strategy;
	private String ext;

	public ProcessFiles(Strategy strategy, String ext) {
		this.strategy = strategy;
		this.ext = ext;
	}

	public void start(String[] args) {
		try {
			if (args.length == 0) {
				processDirectoryTree(new File("."));
			} else {
				for (String arg : args) {
					File fileArg = new File(arg);
					if (fileArg.isDirectory()) {
						processDirectoryTree(fileArg);
					} else {
						if (!arg.endsWith("." + ext)) {
							arg += "." + ext;
						}
						strategy.process(new File(arg).getCanonicalFile());
					}
				}
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	public void processDirectoryTree(File root) throws IOException {
		for (File file : Directory.walk(root.getAbsolutePath(), ".*\\." + ext)) {
			strategy.process(file.getCanonicalFile());
		}
	}

	public static void main(String[] args) {
		new ProcessFiles(new ProcessFiles.Strategy() {
			@Override
			public void process(File file) {
				System.out.println(file);
			}
		}, "java").start(args);
	}
}

    Strategy接口内嵌在ProcessFiles中,使得如果你希望实现它,就必须实现ProcessFiles.Strategy,它为我们提供了更多上下文信息。ProcessFiles执行了查找具有特定扩展名(传递给构造器的ext参数)的文件所需的全部工作,并且当它找到匹配的文件时,将直接把文件传递给Strategy对象(也是传递给构造器的参数)。

    如果你没有提供任何参数,那么ProcessFiles就假设你希望遍历当前目录下的所有目录。你也可以指定特定的文件,带不带扩展名都可以(如果必需的话,它会添加上扩展名),或者指定一个或多个目录。

    在main()方法中,你看到了如何使用这个工具的基本示例,它可以根据你提供的命令行来打印所有的java源代码文件的名字。

四、目录的检查及创建

    File类不仅仅只代表存在的文件或目录。也可以用File对象来创建新的目录或尚不存在的整个目录路径。我们还可以查看文件的特性(如:大小,最后修改日期,读/写),检查某个File对象代表的是一个文件还是一个目录,并可以删除文件。下面的示例展示了File类的一些其他方法(更多请查看JDK文档):

import java.io.File;

public class MakeDirectories {
	private static void usage() {
		System.err.println("Usage:MakeDirectories path1 ...\n" + "Creates each path\n"
				+ "Usage:MakeDirectories -d path1 ...\n" + "Deletes each path\n"
				+ "Usage:MakeDirectories -r path1 ...\n" + "Renames from path1 to path2");
		System.exit(1);
	}

	private static void fileData(File f) {
		System.out.println("Absolute path: " + f.getAbsolutePath() + "\n Can read: " + f.canRead() + "\n Can write: "
				+ f.canWrite() + "\n getName: " + f.getName() + "\n getParent: " + f.getParent() + "\n getPath: "
				+ f.getPath() + "\n length: " + f.length() + "\n lastModified: " + f.lastModified());
		if (f.isFile()) {
			System.out.println("It's a file");
		} else if (f.isDirectory()) {
			System.out.println("It's a directory");
		}
	}

	public static void main(String[] args) {
		if (args.length < 1) {
			usage();
		}
		if (args[0].equals("-r")) {
			if (args.length != 3)
				usage();
			File old = new File(args[1]), rname = new File(args[2]);
			old.renameTo(rname);
			fileData(old);
			fileData(rname);
			return;
		}
		int count = 0;
		boolean del = false;
		if (args[0].equals("-d")) {
			count++;
			del = true;
		}
		count--;
		while (++count < args.length) {
			File f = new File(args[count]);
			if (f.exists()) {
				System.out.println(f + " exists");
				if (del) {
					System.out.println("deleting..." + f);
					f.delete();
				}
			} else {
				if (!del) {
					f.mkdirs();
					System.out.println("created " + f);
				}
			}
			fileData(f);
		}
	}
}

    在fileData()中,可以看到用到了多中不同的文件特征查询方法来显示文件或目录路径的信息。main()方法首先调用的是renameTo(),用来把一个文件重命名(或移动)到由参数所指示的另一个完全不同的新路径(也就是另一个File对象)下面。这同样适用于任意长度的文件目录。

    实践上面的程序可以发现,我们可以产生任意复杂的目录路径,因为mkdirs()可以为我们做好这一切。

如果本文对您有很大的帮助,还请点赞关注一下。

你可能感兴趣的:(JAVA)