第7章 IO(输入输出)

7.1 File类

File类中java.io包中唯一代表磁盘文件本身的对象,它定义了一些与平台无关的方法用于操作文件。通过调用File类提供的各种方法,能够创建,删除或者重命名文件,判断硬盘上某个文件是否存在,查询文件最后修改时间等。

7.1.1 创建File对象

File类提供了专门创建File对象的构造方法,如下表。

方法声明 功能描述
File(String pathname) 通过指定的一个字符串类型的文件路径来创建一个新的File对象
File(String parent,String child) 根据指定的一个字符串类型的父路径和一个字符串类型的子路径(包括文件名称)创建一个File对象
File(File parent,String child) 根据指定的File类的父路径和字符串类型的子路径(包括文件名称)创建一个File对象

在上表中,所有的构造方法都需要传入文件路径。通常来讲,如果程序只处理一个目录或文件,并且知道该目录或文件的路径,使用第一个构造方法较方便。如果程序处理的是一个公共目录中的若干子目录或文件,那么使用第二个或者第三个构造方法会更方便。

下面通过一个案例演示如何使用File类的构造方法创建File对象。

1 import java.io.File;
2 public class Test {
3    public static void main(String[] args) {
4        File f = new File("D:\\file\\a.txt");//使用绝对路径构造File对象
5        File f1 = new File("src\\Hello.java");//使用相对路径构造File对象
6        System.out.println(f);
7       System.out.println(f1);
8   }
9 }

第7章 IO(输入输出)_第1张图片

上述代码中,第1行代码导入了java.io下的File类,第4~5行代码分别在构造方法中传入了绝对路径和相对路径的方式创建File对象。

需要注意的是,在创建File对象时传入的路径使用了“\”,这是因为在Windows中的目录符号为反斜线“\”,但反斜线“\”在Java中是特殊字符,表示转义符,所以使用反斜线“\”时,前面应该再添加一个反斜线,即为“\”。除此之外,目录符号还可以用正斜线“/”表示,如:“D:/file/a.txt”。

7.1.2 File类的常用方法

File类提供了一系列方法,用于操作其内部封装的路径指向的文件或者目录。File类中的常用方法如下表。

方法声明 功能描述
boolean exists() 判断File对象对应的文件或目录是否存在,若存在则返回ture,否则返回false
boolean delete() 删除File对象对应的文件或目录,若成功删除则返回true,否则返回false
boolean createNewFile() 当File对象对应的文件不存在时,该方法将新建一个此File对象所指定的新文件,若创建成功则返回true,否则返回false
String getName() 返回File对象表示的文件或文件夹的名称
String getPath() 返回File对象对应的路径
String getAbsolutePath() 返回File对象对应的绝对路径(在Unix/Linux等系统上,如果路径是以正斜线/开始,则这个路径是绝对路径;在Windows等系统上,如果路径是从盘符开始,则这个路径是绝对路径)
String getParentFile() 返回File对象对应目录的父目录(即返回的目录不包含最后一级子目录)
方法声明 功能描述
boolean canRead() 判断File对象对应的文件或目录是否可读,若可读则返回true,反之返回false
boolean canWrite() 判断File对象对应的文件或目录是否可写,若可写则返回true,反之返回false
boolean isFile() 判断File对象对应的是否是文件(不是目录),若是文件则返回true,反之返回false
boolean isDirectory() 判断File对象对应的是否是目录(不是文件),若是目录则返回true,反之返回false
boolean isAbsolute() 判断File对象对应的文件或目录是否是绝对路径
long lastModified() 返回1970年1月1日0时0分0秒到文件最后修改时间的毫秒值;
long length() 返回文件内容的长度
String[] list() 列出指定目录的全部内容,只是列出名称
File[] listFiles() 返回一个包含了File对象所有子文件和子目录的File数组

接下来,通过一个案例演示如何使用这些方法。

1 import java.io.IOException;
2 import java.io.File;
3 class Examole01 {
4    public static void main(String[] args) throws IOException {
5        //磁盘下创建文件
6        File file=new File("d:\\hello\\demo.txt");
7        if(file.exists()){                   //如果存在这个文件就删除,否则就创建
8            file.delete();
9        }else{
10            System.out.println(file.createNewFile());
11        }
12 //在磁盘下创建一层目录,并且在目录下创建文件文件
13        File fileDemo=new File("d:\\hello1\\demo.txt");
14        if(!(fileDemo.getParentFile().exists())){  //判断d:\demo目录是否存在
15            fileDemo.getParentFile().mkdir();
16        }
17        if(fileDemo.exists()){                //如果存在这个文件就删除,否则就创建
18            fileDemo.delete();
19        }else{
20            System.out.println(fileDemo.createNewFile());
21        }
22    }
23 }

第7章 IO(输入输出)_第2张图片

在上述代码中,第6行代码创建以了一个file对象,并定义了创建文件的路径以及文件名,第7~11行代码判断该路径下是否存在相同文件名的文件,如果存在,则删除此文件,否则创建文件。第13行代码创建了一个fileDemo对象,并定义了创建文件的路径为“d:\hello1\demo.txt”,第14~16行代码判断d:\hello1目录是否存在,若不存在则创建。第17~21行代码判断“d:\hello1”目录下是否存在文件demo.txt,若存在则删除,否则就创建该文件。

运行上述程序之后,在“d:\hello1”目录下会创建demo.txt文件,并且会创建hello1目录,并在hello1目录下创建demo.txt文件。

第7章 IO(输入输出)_第3张图片

多学一招: createTempFile()方法和deleteOnExit()方法

在一些特定情况下,程序需要读写一些临时文件,File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在Jvm退出时自动删除该文件。

下面我们通过一个案例来演示两个方法的使用。

1 public class Example02 {
2    public static void main(String[] args) throws IOException {
3         // 提供临时文件的前缀和后缀
4        File f = File.createTempFile("itcast-", ".txt"); 
5        f.deleteOnExit(); // JVM退出时自动删除
6        System.out.println(f.isFile());
7        System.out.println(f.getPath());
8    }
9 }

第7章 IO(输入输出)_第4张图片

7.1.3 遍历目录下的文件

File类的list()方法用于遍历指定目录下的所有文件,接下来通过一个案例来演示如何使用list()方法遍历目录文件。

1 import java.io.File;
2 public class Example03{
3	public static void main(String[] args) throws Exception {
4         // 创建File对象
5		File file = new File("D:/IdeaWorkspace/chapter07");  
6		if (file.isDirectory()) { // 判断File对象对应的目录是否存在
7			String[] names = file.list (); // 获得目录下的所有文件的文件名
8				for (String name : names) {
9					System.out.println(name); // 输出文件名
10				}
11		}
12	}
13 }		

第7章 IO(输入输出)_第5张图片

上述代码中,第5行代码创建了一个File对象,并指定了一个路径,通过调用File的isDirectory()方法判断路径指向的是否为存在的目录,如果存在就调用list()方法,获得一个String类型的数组names,数组中包含这个目录下所有文件的文件名。接着通过循环遍历数组names,依次打印出每个文件的文件名。

上述程序实现了遍历一个目录下所有文件的功能,然而有时程序只是需要得到指定类型的文件,如获取指定目录下所有的“.java”文件。针对这种需求,File类中提供了一个重载的list(FilenameFilter filter)方法,该方法接收一个FilenameFilter类型的参数。FilenameFilter是一个接口,被称作文件过滤器,当中定义了一个抽象方法accept(File dir,String name)。在调用list()方法时,需要实现文件过滤器FilenameFilter,并在accept()方法中做出判断,从而获得指定类型的文件。

为了让初学者更好地理解文件过滤的原理,下面分步骤分析list(FilenameFilter filter)方法的工作原理:

(1)调用list()方法传入FilenameFilter文件过滤器对象。

(2)取出当前File对象所代表目录下的所有子目录和文件。

(3)对于每一个子目录或文件,都会调用文件过滤器对象的accept(File dir,String name)方法,并把代表当前目录的File对象以及这个子目录或文件的名字作为参数dir和name传递给方法。

(4)如果accept()方法返回true,就将当前遍历的这个子目录或文件添加到数组中,如果返回false,则不添加。

接下来通过一个案例来演示如何遍历指定目录下所有扩展名为“.java”的文件。

1 import java.io.File;
2 import java.io.FilenameFilter;
3 public class Example04 {
4    public static void main(String[] args) throws Exception {
5        // 创建File对象
6        File file = new File("D:/IdeaProjects/text/com/itcast");
7        // 创建过滤器对象
8        FilenameFilter filter = new FilenameFilter() {
9 // 实现accept()方法
10            public boolean accept(File dir, String name) {
11                File currFile = new File(dir, name);
12                // 如果文件名以.java结尾返回true,否则返回false
13                if (currFile.isFile() && name.endsWith(".java")) {
14                    return true;
15                } else {
16                    return false;
17                }
18            }
19        };
20 if (file.exists()) { // 判断File对象对应的目录是否存在
21            String[] lists = file.list(filter); // 获得过滤后的所有文件名数组
22            for (String name : lists) {
23                System.out.println(name);
24            }
25        }
26    }
27 }         

第7章 IO(输入输出)_第6张图片

上述代码中,第8行代码定义了FilenameFilter文件过滤器对象filter,第10~8行代码实现了accept()方法。在accept()方法中,对当前正在遍历的currFile对象进行了判断,只有当currFile对象代表文件,并且扩展名为“.java”时,才返回true。第21行代码在调用File对象的list()方法时,将filter过滤器对象传入,就得到了包含所有“.java”文件名字的字符串数组。

前面的两个例子演示的都是遍历目录下文件的文件名,有时候在一个目录下,除了文件,还有子目录,如果想得到所有子目录下的File类型对象,list()方法显然不能满足要求,这时需要使用File类提供的另一个方法listFiles()。listFiles()方法返回一个File对象数组,当对数组中的元素进行遍历时,如果元素中还有子目录需要遍历,则需要使用递归。

接下来通过一个案例来实现遍历指定目录下的文件。

1 import java.io.File;
2 public class Example05{
3	public static void main(String[] args) {
4		// 创建一个代表目录的File对象
5        File file =
6           new File("D:\\eclipseWorkspace\\JavaBasicWorkspace\\chapter07"); 
7	    fileDir(file);				 // 调用FileDir方法
8	}
9	public static void fileDir(File dir) {
10		File[] files = dir.listFiles(); // 获得表示目录下所有文件的数组
11		for (File file : files) {		 // 遍历所有的子目录和文件
12			if (file.isDirectory()) {
13				fileDir(file);	 // 如果是目录,递归调用fileDir()
14			}
15		System.out.println(file.getAbsolutePath()); // 输出文件的绝对路径
16		}
17	}
18 }

第7章 IO(输入输出)_第7张图片

上述代码中,第9~18行代码定义了一个静态方法fileDir(),该方法接收一个表示目录的File对象。在fileDir()方法中,第10行代码通过调用listFiles()方法把该目录下所有的子目录和文件存到一个File类型的数组files中,第11~14行代码通过for循环遍历数组files,并对当前遍历的File对象进行判断,如果是目录就重新调用fileDir()方法进行递归,如果是文件就直接打印输出文件的路径,这样该目录下的所有文件就被成功遍历出来了。

7.1.4 删除文件及目录

在操作文件时,我们会遇到需要删除一个目录下的某个文件或者删除整个目录,这时需要使用到File的delete()方法。接下来通过一个案例来演示使用delete()方法删除文件或文件夹。

首先在电脑的D盘中创建一个名称为hello的文件夹,然后在文件夹中创建一个文本文件,接下来创建一个类Example06,在Example06类中使用delete()方法删除文件夹。

1 import java.io.*;
2 public class Example06 {
3	public static void main(String[] args) {
4		File file = new File("D:\\hello\\test");
5		if (file.exists()) {
6			System.out.println(file.delete());
7		}
8	}
9 }

第7章 IO(输入输出)_第8张图片

运行结果中输出了false,这说明删除文件失败了。原因是File类的delete()方法只能删除一个指定的文件,假如File对象代表目录,并且目录下包含子目录或文件,则File类的delete()方法不允许对这个目录直接删除。在这种情况下,需要通过递归的方式将整个目录以及其中的文件全部删除。

接下来通过递归的方式演示如何删除包含子文件的目录。

1 import java.io.*;
2 public class Example07 {
3	public static void main(String[] args) {
4		File file = new File("D:\\hello\\test");
5		deleteDir(file); // 调用deleteDir删除方法
6	}
7	public static void deleteDir(File dir) {
8		if (dir.exists()) { // 判断传入的File对象是否存在
9			File[] files = dir.listFiles(); // 得到File数组
10			for (File file : files) { // 遍历所有的子目录和文件
11				if (file.isDirectory()) {
12					deleteDir(file); // 如果是目录,递归调用deleteDir()
13				} else {
14					// 如果是文件,直接删除
15					file.delete();
16				}
17			}
18			// 删除完一个目录里的所有文件后,就删除这个目录
19			dir.delete();
20		}
21	}
22 }

运行程序,test文件夹会被删除。

第7章 IO(输入输出)_第9张图片

上述代码中,第4~5行代码定义了一个File对象并将File对象传入deleteDir()方法中,第7~21行代码定义了一个删除目录的静态方法deleteDir()来接收一个File对象,并将所有的子目录和文件对象放在一个File数组中,遍历这个File数组,如果是文件,则直接删除,如果是目录,则删除目录中的文件,当目录中的文件全部删除完之后,删除目录。

删除目录是从虚拟机直接删除而不放入回收站的,文件一旦删除就无法恢复,因此在进行删除操作的时候需要格外小心。

【案例7-1】 批量操作文件管理器

在日常工作中,经常会遇到批量操作系统文件的事情,通常情况下,只能手动重复的完成批量文件的操作,这样很是费时费力。本案例要求编写一个文件管理器,实现文件的批量操作。文件管理器具体功能要求如下:

(1)用户输入指令1,代表“指定关键字检索文件”,此时需要用户输入检索的目录和关键字,系统在用户指定的目录下检索出文件名中包含关键字的文件,并将其绝对路径展示出来。

(2)用户输入指令2,代表“指定后缀名检索文件”,此时需要用户输入检索的目录和后缀名(多个后缀名用逗号分隔),系统在用户指定的目录下检索出指定后缀名的文件,并将其绝对路径展示出来。

(3)用户输入指令3,代表“复制文件/目录”,此时需要用户输入源目录和目标目录,程序执行后会将源目录下的内容复制到目标目录下。

(4)用户输入指令4,代表“删除文件/目录”,此时需要用户输入需要删除掉的文件目录,程序执行后会将目录以及目录下的内容全部删除。

(5)用户输入指令5,代表“退出”,即退出该系统。

第7章 IO(输入输出)_第10张图片

 1	package chapter0705;
 2	import java.io.File;
 3	import java.util.ArrayList;
 4	import java.util.Scanner;
 5	public class DocumentManager {
 6		public static void main(String[] args) throws Exception {
 7			Scanner sc = new Scanner(System.in);
 8			System.out.println("--1:指定关键字检索文件  2:指定后缀名检索文件  "
 9					+ "3:复制文件/目录  4:删除文件/目录  5:退出--");
 10			while(true){
 11				System.out.print("请输入指令:");
 12				int command = sc.nextInt();
 13				switch (command) {
 14				case 1:
 15					searchByKeyWorld();//指定关键字检索文件
 16					break;
 17				case 2:
 18					searchBySuffix();//指定后缀名检索文件
 19					break;
 20				case 3:
 21					copyDirectory();//复制文件/目录
 22					break;
 23	              case 4:
 24					deleteDir();//删除文件/目录
 25					break;
 26				case 5:
 27					exit();//退出
 28					break;
 29				default:
 30					System.out.println("您输入的指令错误!");
 31					break;
 32				}
 33			}
 34		}
 35		// *********1.指定关键字检索文件*********
 36		private static void searchByKeyWorld() {
 37			Scanner sc = new Scanner(System.in);
 38			System.out.print("请输入要检索的目录位置:");
 39			String path = sc.next();//从控制台获取路径
 40			File file = new File(path);
 41		if (!file.exists() || !file.isDirectory()) {//判断目录是否存在,是否是目录
 42				System.out.println(path + " (不是有效目录)");
 43				return;
 44			}
 45			System.out.print("请输入搜索关键字:");
 46			String key = sc.next();//获取关键字
 47			//在输入目录下获取所有包含关键字的文件路径
 48			ArrayList list = FileUtils.listFiles(file,key);
 49			for (Object obj : list) {
 50				System.out.println(obj);//将路径打印到控制台
 51			}
 52		}
 53		// *********2.指定后缀名检索文件********//
 54		private static void searchBySuffix() {
 55			Scanner sc = new Scanner(System.in);
 56			System.out.print("请输入要检索的目录位置:");
 57			String path = sc.next();//从控制台获取路径
 58			File file = new File(path);
 59		if (!file.exists() || !file.isDirectory()) {//判断目录是否存在,是否是目录
 60				System.out.println(path + " (不是有效目录)");
 61				return;
 62			}
 63			System.out.print("请输入搜索后缀:");
 64			String suffix = sc.next();
 65			String[] suffixArray = suffix.split(",");//获取后缀字符串
 66			//在输入目录下获取所有指定后缀名的文件路径
 67			ArrayList list = FileUtils.listFiles(file, suffixArray);
 68			for (Object obj : list) {
 69				System.out.println(obj);//将路径打印到控制台
 70			}
 71		}
 72		// *********3.复制文件/目录**********//
 73		private static void copyDirectory() throws Exception {
 74			Scanner sc = new Scanner(System.in);
 75			System.out.print("请输入源目录:");
 76			String srcDirectory = sc.next();//从控制台获取源路径
 77			File srcFile = new File(srcDirectory);
 78	         //判断是否存在 是否是目录
 79			if (!srcFile.exists() || !srcFile.isDirectory()) {
 80				System.out.println("无效目录!");
 81				return;
 82			}
 83			System.out.print("请输入目标位置:");
 84			String destDirectory = sc.next();//从控制台获取目标路径
 85			File destFile = new File(destDirectory);
 86	         //判断是否存在 是否是目录
 87			if (!destFile.exists() || !destFile.isDirectory()) {
 88				System.out.println("无效位置!");
 89				return;
 90			} 
 91			//将源路径中的内容复制到目标路径下
 92			FileUtils.copySrcPathToDestPath(srcFile, destFile);
 93		}
 94		// *********4.删除文件/目录**********//
 95		public static void deleteDir() {
 96			Scanner sc = new Scanner(System.in);
 97			System.out.print("请输入需要删除的源目录:");
 98			String delpath = sc.next();// 从控制台获取源路径
 99			File dir = new File(delpath); // 创建一个代表目录的File对象
 100			if (dir.exists()) { // 判断传入的File对象是否存在
 101				File[] files = dir.listFiles(); // 得到File数组
 102				for (File file : files) { // 遍历所有的子目录和文件
 103					if (file.isDirectory()) {
 104						deleteDir(); // 如果是目录,递归调用deleteDir()
 105					} else {
 106						// 如果是文件,直接删除
 107						file.delete();
 108					}
 109				}
 110				// 删除完一个目录里的所有文件后,就删除这个目录
 111				dir.delete();
 112			}
 113		}
 114		// *********5.退出**********//
 115		private static void exit() {
 116			System.out.println("您已退出系统,谢谢使用!");
 117			System.exit(0);
 118		}
 119	}
 1	package cn.itcast.chapter07.task03;
 2	import java.io.File;
 3	import java.io.FileInputStream;
 4	import java.io.FileOutputStream;
 5	import java.io.FilenameFilter;
 6	import java.util.ArrayList;
 7	public class FileUtils {
 8		/**
 9		 * 指定关键字检索文件
 10		 * @param file File对象
 11		 * @param key  关键字
 12		 * @return 包含关键字的文件路径
 13		 */
 14	 public static ArrayList listFiles(File file, final String key){
 15			FilenameFilter filter = new FilenameFilter() { // 创建过滤器对象
 16			public boolean accept(File dir, String name) {// 实现accept()方法
 17					File currFile = new File(dir, name);
 18					// 如果文件名包含关键字返回true,否则返回false
 19					if (currFile.isFile() && name.contains(key)) {
 20						return true;
 21					}
 22					return false;
 23				}
 24			};
 25			//递归方式获取规定的路径
 26			ArrayList arraylist = fileDir(file, filter);
 27			return arraylist;
 28		}
 29		/**
 30		 * 指定后缀名检索文件
 31		 * @param file File对象
 32		 * @param suffixArray 后缀名数组
 33		 * @return 指定后缀名的文件路径
 34		 */
 35		public static ArrayList listFiles(File file, final String[] suffixArray) {
 36			FilenameFilter filter = new FilenameFilter() { // 创建过滤器对象
 37			public boolean accept(File dir, String name) {// 实现accept()方法
 38					File currFile = new File(dir, name);
 39					if (currFile.isFile()) {
 40						for (String suffix : suffixArray) {
 41							if (name.endsWith("." + suffix)) {
 42								return true;
 43							}
 44						}
 45					}
 46					return false;
 47				}
 48			};
 49			//递归方式获取规定的路径
 50			ArrayList arraylist = fileDir(file, filter);
 51			return arraylist;
 52		}
 53		/**
 54		 * 递归方式获取规定的路径
 55		 * @param dir File对象
 56		 * @param filter 过滤器
 57		 * @return 过滤器过滤后的文件路径
 58		 */
 59	 public static ArrayList fileDir(File dir, FilenameFilter filter){
 60			ArrayList arraylist = new ArrayList();
 61			File[] lists = dir.listFiles(filter); // 获得过滤后的所有文件数组
 62			for (File list : lists) {
 63				//将文件的绝对路径放到集合中
 64				arraylist.add(list.getAbsolutePath());
 65			}
 66			File[] files = dir.listFiles(); // 获得当前目录下所有文件的数组
 67			for (File file : files) { // 遍历所有的子目录和文件
 68				if (file.isDirectory()) {
 69					// 如果是目录,递归调用fileDir()
 70					ArrayList every = fileDir(file, filter); 
 71					arraylist.addAll(every);//将文件夹下的文件路径添加到集合中
 72				}
 73			}//此时的集合中有当前目录下的文件路径,和当前目录的子目录下的文件路径
 74			return arraylist;
 75		}	
 76		/**
 77		 * 复制文件/目录
 78		 * @param srcFile 源目录
 79		 * @param destFile 目标目录
 80		 */
 81		public static void copySrcPathToDestPath(File srcDir, File destDir) 
 82				throws Exception {
 83			File[] files = srcDir.listFiles();// 子文件目录
 84			for (int i = 0; i < files.length; i++) {
 85				File copiedFile = new File(destDir, files[i].getName());	
 86			if (files[i].isDirectory()) {// 如果是目录
 87					if(!copiedFile.mkdirs()){//创建文件夹
 88						System.out.println("无法创建:"+copiedFile);
 89						return;
 90					}
 91					// 调用递归,获取子文件夹下的文件路径
 92					copySrcPathToDestPath(files[i], copiedFile);
 93				} else {// 复制文件
 94		FileInputStream input = new FileInputStream(files[i]);//获取输入流
 95		FileOutputStream output = new FileOutputStream(copiedFile);//获取输出流
 96		byte[] buffer = new byte[1024];//创建缓冲区
 97					int n = 0;
 98					//循环读取字节
 99					while ((n = input.read(buffer))!= -1) {
 100						output.write(buffer, 0, n);
 101					}
 102					input.close();//关闭输入流
 103					output.close();//关闭输出流
 104				}
 105			}
 106		}
 107	}

7.2 字节流

7.2.1 字节流的概念

在程序的开发中,我们经常会需要处理设备之间的数据传输,而计算机中,无论是文本、图片、音频还是视频,所有文件都是以二进制(字节)形式存在的。而对于字节的输入输出IO流提供了一系列的流,统称为字节流,字节流是程序中最常用的流,根据数据的传输方向可将其分为字节输入流和字节输出流。

在JDK中,提供了两个抽象类InputStream和OutputStream,它们是字节流的顶级父类,所有的字节输入流都继承自InputStream,所有的字节输出流都继承自OutputStream。为了方便理解,可以把InputStream和OutputStream比作两根“水管”。

第7章 IO(输入输出)_第11张图片

在上图中,InputStream被看成一个输入管道,OutputStream被看成一个输出管道,数据通过InputStream从源设备输入到程序,通过OutputStream从程序输出到目标设备,从而实现数据的传输。由此可见,IO流中的输入输出都是相对于程序而言的。

在JDK中,InputStream和 OutputStream提供了一系列与读写数据相关的方法。

方法声明 功能描述
int read() 从输入流读取一个8位的字节,把它转换为0~255之间的整数,并返回这一整数
int read(byte[] b) 从输入流读取若干字节,把它们保存到参数b指定的字节数组中,返回的整数表示读取字节的数目
int read(byte[] b,int off,int len) 从输入流读取若干字节,把它们保存到参数b指定的字节数组中,off指定字节数组开始保存数据的起始下标,len表示读取的字节数目
void close() 关闭此输入流并释放与该流关联的所有系统资源

在上面表格中,第一个read()方法是从输入流中逐个读入字节,而第二个和第三个read()方法则将若干字节以字节数组的形式一次性读入,从而提高读数据的效率。在进行IO流操作时,当前IO流会占用一定的内存,由于系统资源宝贵,因此,在IO操作结束后,应该调用close()方法关闭流,从而释放当前IO流所占的系统资源。

与InputStream对应的是OutputStream。OutputStream是用于写数据的,因此OutputStream提供了一些与写数据有关的方法,OutputStream的常用方法如下表。

方法名称 方法描述
void write(int b) 向输出流写入一个字节
void write(byte[] b) 把参数b指定的字节数组的所有字节写到输出流
void write(byte[] b,int off,int len) 将指定byte数组中从偏移量off开始的len个字节写入输出流
void flush() 刷新此输出流并强制写出所有缓冲的输出字节
void close() 关闭此输出流并释放与此流相关的所有系统资源

上述表格列举了OutputStream类的五个常用方法。前三个是重载的write()方法,都是用于向输出流写入字节,其中,第一个方法逐个写入字节,后两个方法是将若干个字节以字节数组的形式一次性写入,从而提高写数据的效率。flush()方法用来将当前输出流缓冲区(通常是字节数组)中的数据强制写入目标设备,此过程称为刷新。close()方法是用来关闭流并释放与当前IO流相关的系统资源。

InputStream和OutputStream这两个类虽然提供了一系列和读写数据有关的方法,但是这两个类是抽象类,不能被实例化,因此,针对不同的功能,InputStream和OutputStream提供了不同的子类,这些子类形成了一个体系结构。

InputStream的子类如下图。

第7章 IO(输入输出)_第12张图片 

OnputStream的子类如下图。

第7章 IO(输入输出)_第13张图片 

7.2.2 InputStream读文件

InputStream就是JDk提供的基本输入流。但InputStream并不是一个接口,而是一个抽象类,它是所有输入流的父类,而FileInputStream是InputStream的子类,它是操作文件的字节输入流,专门用于读取文件中的数据。由于从文件读取数据是重复的操作,因此需要通过循环语句来实现数据的持续读取。

接下来通过一个案例来实现字节流对文件数据的读取。在实现案例之前,首先在Java项目的根目录下创建一个文本文件test.txt,在文件中输入内容“itcast”并保存;然后使用字节输入流对象来读取test.txt文本文件。

1 import java.io.*;
2 public class Example08 {
3	public static void main(String[] args) throws Exception {
4		// 创建一个文件字节输入流
5		FileInputStream in = new FileInputStream("test.txt");
6		int b = 0;           // 定义一个int类型的变量b,记住每次读取的一个字节
7		while (true) {
8			b = in.read(); // 变量b记住读取的一个字节
9			if (b == -1) { // 如果读取的字节为-1,跳出while循环
10				break;
11			}
12			System.out.println(b); // 否则将b写出
13		}
14		in.close();
15	}
16 }

 第7章 IO(输入输出)_第14张图片

上述代码中,第5行代码创建的字节流FileInputStream,第7~13行代码通过read()方法将当前项目中的文件“test.txt”中的数据读取并打印。

从运行结果可以看出,控制台打印的结果分别为105、116、99、97、115和116。由于计算机中的数据都是以字节的形式存在的。在“test.txt”文件中,字符‘i’、‘t’、‘c’、‘a’、‘s’、‘t’各占一个字节,因此,最终结果显示的就是文件“test.txt”中的六个字节所对应的十进制数。

有时,在文件读取的过程中可能会发生错误。例如,文件不存在导致无法读取,没有读取权限等,这些错都是由Java虚拟机自动封装成IOException异常并抛出,如下图所示。

第7章 IO(输入输出)_第15张图片 

文件不存在时控制台的报错信息对于上述异常错误,会有一个潜在的问题,如果读取过程中发生了IO错误,InputStream就无法正常关闭,资源也无法及时释放。对于这种问题我们可以使用try…finally来保证InputStream在无论是否发生IO错误的时候都能够正确关闭。

1 import java.io.FileInputStream;
2 import java.io.InputStream;
3 public class Example9 {
4    public static void main(String[] args) throws Exception {
5        InputStream input =null;
6        try {
7        // 创建一个文件字节输入流
8        FileInputStream in = new FileInputStream("test.txt");
9        int b = 0;           // 定义一个int类型的变量b,记住每次读取的一个字节
10 b = in.read(); // 变量b记住读取的一个字节
11            if (b == -1) { // 如果读取的字节为-1,跳出while循环
12                break;
13            }
14            System.out.println(b); // 否则将b写出
15        }
16        } finally {
17            if (input != null) {
18                input.close();
19            }
20        }
21    }
22 }

7.2.3 OnputStream写文件

OutputStream是JDK提供的最基本的输出流,与InputStream类似的是OutputStream也是抽象类,它是所有输出流的父类。OutputStream是一个抽象类,如果使用此类,则首先必须通过子类实例化对象。FileOutputStream是OutputStream的子类,它是操作文件的字节输出流,专门用于把数据写入文件。

接下来通过一个案例来演示如何使用FileOutputStream将数据写入文件。

1 import java.io.*;
2 public class Example10
3	public static void main(String[] args) throws Exception {
4		// 创建一个文件字节输出流
5		OutputStream out = new FileOutputStream("example.txt");
6		String str = "Java程序设计";
7		byte[] b = str.getBytes();
8		for (int i = 0; i < b.length; i++) {
9			out.write(b[i]);
10		}
11		out.close();
12	}
13 }

程序运行后,会在项目当前目录下生成一个新的文本文件example.txt,打开此文件

从运行结果可以看出,通过FileOutputStream写数据时,自动创建了文件example.txt,并将数据写入文件。需要注意的是,如果是通过FileOutputStream向一个已经存在的文件中写入数据,那么该文件中的数据首先会被清空,再写入新的数据。若希望在已存在的文件内容之后追加新内容,则可使用FileOutputStream的构造函数。

FileOutputStream(String fileName, boolean append)来创建文件输出流对象,并把append 参数的值设置为true。接下来通过一个案例来演示如何将数据追加到文件末尾。

1 import java.io.*;
2 public class Example11
3	public static void main(String[] args) throws Exception {
4		OutputStream out = new FileOutputStream("example.txt ", true);
5		String str = "欢迎你!";
6		byte[] b = str.getBytes();
7		for (int i = 0; i < b.length; i++) {
8			out.write(b[i]);
9		}
10		out.close();
11	}
12 }

程序运行后,查看项目当前目录下的文件“example.txt”。

从运行结果可以看出,程序通过字节输出流对象向文件“example.txt”写入“欢迎你!”后,并没有将文件之前的数据清空,而是将新写入的数据追加到了文件的末尾。

由于IO流在进行数据读写操作时会出现异常,为了代码的简洁,在上面的程序中使用了throws关键字将异常抛出。然而一旦遇到IO异常,IO流的close()方法将无法得到执行,流对象所占用的系统资源将得不到释放,因此,为了保证IO流的close()方法必须执行,通常将关闭流的操作写在finally代码块中。

finally{
		try{
			if(in!=null)             // 如果in不为空,关闭输入流 
				in.close();
		}catch(Exception e){
		   e.printStackTrace();
		}
		try{
			if(out!=null)            // 如果out不为空,关闭输出流
				out.close();
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}

7.2.4 文件的拷贝

在应用程序中,IO流通常都是成对出现的,即输入流和输出流一起使用。例如,文件的拷贝就需要通过输入流来读取文件中的数据,通过输出流将数据写入文件。

接下来通过一个案例来演示如何进行文件内容的拷贝。首先在Java项目的根目录下创建文件夹source和target,然后右击项目名称→【New】→【Directory】,最后在source文件夹中存放一个“五环之歌.doc”文件,拷贝文件的代码如下。

1 import java.io.*;
2 public class Example12
3	public static void main(String[] args) throws Exception {
4		// 创建一个字节输入流,用于读取当前目录下source文件夹中的文件
5		InputStream in = new FileInputStream("source/五环之歌.doc");
6		// 创建一个文件字节输出流,用于将读取的数据写入target目录下的文件中
7		OutputStream out = new FileOutputStream("target/五环之歌.doc");
8		int len; // 定义一个int类型的变量len,记住每次读取的一个字节
9         // 获取拷贝文件前的系统时间
10		long begintime = System.currentTimeMillis();
11		while ((len = in.read()) != -1) { // 读取一个字节并判断是否读到文件末尾
12			out.write(len); // 将读到的字节写入文件
13		}
14		// 获取文件拷贝结束时的系统时间
15		long endtime = System.currentTimeMillis();
16		System.out.println("拷贝文件所消耗的时间是:" + (endtime - begintime) + 
17		"毫秒");
18		in.close();
19		out.close();
20	}
21 }	

程序运行结束后,刷新并打开target文件夹,发现source文件夹中的“五环之歌.doc”文件被成功拷贝到了target文件夹。

第7章 IO(输入输出)_第16张图片

上述程序实现了文件的拷贝。在拷贝过程中,通过while循环将字节逐个进行拷贝。每循环一次,就通过FileInputStream的read()方法读取一个字节,并通过FileOutputStream的write()方法将该字节写入指定文件,循环往复,直到len的值为-1,表示读取到了文件的末尾,结束循环,完成文件的拷贝。

程序运行结束后,会在命令行窗口打印拷贝文件所消耗的时间。

第7章 IO(输入输出)_第17张图片

由运行结果可知,程序拷贝文件共消耗了2715毫秒。在拷贝文件时,由于计算机性能等各方面原因,会导致拷贝文件所消耗的时间不确定,因此每次运行程序结果未必相同。

上述实现的文件拷贝是一个字节一个字节的读写,需要频繁的操作文件,效率非常低。这就好比从北京运送烤鸭到上海,如果有一万只烤鸭,每次运送一只,就必须运输一万次,这样的效率显然非常低。为了减少运输次数,可以先把一批烤鸭装在车厢中,这样就可以成批的运送烤鸭,这时的车厢就相当于一个临时缓冲区。当通过流的方式拷贝文件时,为了提高效率也可以定义一个字节数组作为缓冲区。在拷贝文件时,可以一次性读取多个字节的数据,并保存在字节数组中,然后将字节数组中的数据一次性写入文件。

接下来通过上述程序来学习如何使用缓冲区拷贝文件。

1 import java.io.*;
2 public class Example13{
3	public static void main(String[] args) throws Exception {
4		// 创建一个字节输入流,用于读取当前目录下source文件夹中的文件
5		InputStream in = new FileInputStream("source/五环之歌.doc");
6		// 创建一个文件字节输出流,用于将读取的数据写入当前目录的target文件中
7		OutputStream out = new FileOutputStream("target/五环之歌.doc");
8		// 以下是用缓冲区读写文件
9		byte[] buff = new byte[1024]; // 定义一个字节数组,作为缓冲区
10		// 定义一个int类型的变量len记住读取读入缓冲区的字节数
11		int len;
12		long begintime = System.currentTimeMillis();
13		while ((len = in.read(buff)) != -1) { // 判断是否读到文件末尾
14			out.write(buff, 0, len); // 从第一个字节开始,向文件写入len个字节
15		}
16		long endtime = System.currentTimeMillis();
17		System.out.println("拷贝文件所消耗的时间是:" + (endtime - begintime) + 
18		"毫秒");
19		in.close();
20		out.close();
21	}
22 }

 同样实现了文件的拷贝。在拷贝过程中,第13~15行代码使用while循环语句逐渐实现字节文件的拷贝,每循环一次,就从文件读取若干字节填充字节数组,并通过变量len记住读入数组的字节数,然后从数组的第一个字节开始,将len个字节依次写入文件。循环往复,当len值为-1时,说明已经读到了文件的末尾,循环会结束,整个拷贝过程也就结束了,最终程序会将整个文件拷贝到目标文件夹。

第7章 IO(输入输出)_第18张图片

从运行结果可以看出,拷贝文件所消耗的时间明显减少了,这说明使用缓冲区读写文件可以有效的提高程序的效率。程序中的缓冲区就是一块内存,该内存主要用于存放暂时输入输出的数据,由于使用缓冲区减少了对文件的操作次数,所以可以提高读写数据的效率。

7.2.5 字节缓冲流

IO提供两个带缓冲的字节流,分别是BufferedInputStream和BufferedOutputStream,它们的构造方法中分别接收InputStream和OutputStream类型的参数作为对象,在读写数据时提供缓冲功能。应用程序、缓冲流和底层字节流之间的关系如下图。

 接下来通过一个案例学习BufferedInputStream和BufferedOutputStream 这两个流的用法。首先在Java项目的根目录下创建一个名称为src.txt的文件,并在该文件中随意写入一些内容;然后创建一个类,在类中使用FileOutputStream创建文件des.txt,并使用字节缓冲流对象将文件src.txt中的内容拷贝到文件des.txt中。

1 import java.io.*;
2 public class Example14 {
3	public static void main(String[] args) throws Exception {
4		// 创建一个带缓冲区的输入流
5		BufferedInputStream bis = new BufferedInputStream(new 
6					FileInputStream("src.txt"));
7		// 创建一个带缓冲区的输出流
8		BufferedOutputStream bos = new BufferedOutputStream(
9				new FileOutputStream("des.txt"));
10		int len;
11		while ((len = bis.read()) != -1) {
12			bos.write(len);
13		}
14		bis.close();
15		bos.close();
16	}
17 }		

上述代码中,第5~6行代码分别创建了BufferedInputStream和BufferedOutputStream两个缓冲流对象,这两个流内部都定义了一个大小为8192的字节数组,第11~12行代码中调用read()或者write()方法读写数据时,首先将读写的数据存入定义好的字节数组,然后将字节数组的数据一次性读写到文件中

【案例7-2】 商城进货交易记录

每个商城都需要进货,而这些进货记录整理起来很不方便,本案例要求编写一个记录商城进货交易的程序,使用字节流将商场的进货信息记录在本地的csv文件中。程序具体要求如下:

当用户输入商品编号时,后台会根据商品编号查询到相应商品信息,并打印商品信息。接着让用户输入需要进货的商品数量,程序将原有的库存数量与输入的数量相加,将相加后的结果信息保存至本地的csv文件中。

在csv文件中,每条记录包含商品编号、商品名称、购买数量、单价、总价、审核人等数据,每条记录的数据之间直接用英文逗号或空格分隔;每条记录之间由换行符分隔。文件命名格式为“进货记录”加上当天日期加上“.csv”后缀,如进货记录“20200430.csv”。保存文件时,需要判断本地是否存在当天的数据,如果存在则追加,不存在则新建。

第7章 IO(输入输出)_第19张图片

运行结束后在本地生成一个“进货记录20191209.csv”文件,用Excel方式打开此文件

第7章 IO(输入输出)_第20张图片

(1) 为了方便保存商品的相关信息,可以将图书信息封装为一个实体类。商品进货过程中可能会打印图书相关信息,所以需要对该实体类的toString()方法进行重写,使其能更清晰地显示商品信息,商品每次进货后要修改库存数量,需要在实体类中编写一个操作库存数量的方法。

(2) 对于一个超市,首先会有很多商品,商品需要不断进货。这里我们需要创建一个集合用于模拟超市仓库,然后向集合中添加有具体信息的商品对象,这样一个超市就有了商品。

(3) 管理员进货是通过在控制台键盘输入商品编号和购买数量的方式进行的,如果商品编号正确,且购买数量也正常,则商品进货成功,并将此商品的进货信息保存到csv文件中,同时要将库存数量增加。

(4) 查询商品信息时,可以通过Scanner类的nextInt()方法从控制台获取商品编号,之后根据这个编号到库存中查询此商品的信息,如果查到了商品的信息,从控制台获取进货的数量之后,将此商品的所有信息进行封装。

(5) 将商品的销售信息写入到csv文件之前,需先拼凑好csv文件名,再判断本地是否已存在此文件,这里可通过输入流尝试获取此文件的字节流,如果获取成功,则证明这个文件已存在,那么就通过输出流向文件末尾追加销售信息,如果获取失败,即异常,说明之前并没有生成当日的销售信息,则需要新建此文件。

(6) 将封装的信息写入csv文件中时,csv格式的文件以纯文本形式存储表格数据,写入文件时可以用图7-1的格式写入,当此类文件用Excel格式打开的时候,展现信息如图7-11。

(7) 在拼凑csv文件名时,需要获取当日的日期。这里可以通过以下代码来获取并拼凑csv文件名:

DateFormat format = new SimpleDateFormat("yyyyMMdd");// 定义日期格式
String name = "进货记录" + format.format(date) + ".csv";// 拼接文件名

 1 	package chapter0701;
 2 	public class Good {
 3 			int id;
 4 			String name;     //商品的价格
 5 			double price;   //商品的单价
 6 			int number;      //进货的数量
 7 			double money;   //总价
 8 			String people;  //审批人
 9 			public Good(int id, String name, double price, int number, double 
 10 	                   money, String people) {
 11 				this.id = id;
 12 				this.name = name;
 13 				this.price = price;
 14 				this.number = number;
 15 				this.money = money;
 16 				this.people = people;
 17 			}
 18 			@Override
 19 			public String toString() {
 20 		 String message="进货记录编号:"+id+"\n商品名称:"+name+"\n联系
 21 	              人:"+people+"\n单价:"+price+"\n库存数量:"+number+"\n";
 22 				return message;
 23 			}
 24 			public void setNumber(int number) {
 25 				this.number=number;
 26 			}
 27 	}
 1 	package chapter0701;
 2 	import java.util.ArrayList;
 3 	import java.util.Scanner;
 4 	public class RecordGoodOrder {
 5 	    //创建商品库存
 6 		static ArrayList goodsList=new ArrayList();
 7 		public static void main(String[] args) {
 8 			init();  //初始化商品库存
 9 			//将书架上所以商品信息打印出来
 10 			for (int i = 0; i < goodsList.size(); i++) {
 11 				System.out.println(goodsList.get(i));
 12 			}
 13 			while(true) {
 14 				//获取控制台输入的信息
 15 				Scanner scan=new Scanner(System.in);
 16 				System.out.println("请输入商品编号");
 17 				int goodId=scan.nextInt();
 18 				Good stockGood=getGoodsById(goodId);
 19 				if (stockGood != null) {// 判断是否存在此商品
 20 					System.out.println("当前商品库存信息" + stockGood);
 21 					System.out.print("请输入进货数量:");
 22 					int bookNumber = scan.nextInt();
 23 					// 将输入信息封装成Books对象
 24 					Good good = new Good(stockGood.id, stockGood.name,
 25 							stockGood.price, bookNumber, stockGood.price
 26 									* bookNumber, stockGood.people);
 27 					FileUtil.saveBooks(good);// 将本条数据保存至本地文件
 28 					// 修改库存
 29 					stockGood.setNumber(stockGood.number + bookNumber);
 30 				} else {
 31 					System.out.println("商品编号输入错误!");
 32 				}
 33 			}
 34 		}
 35 		/*
 36 		 * 初始化商品库存的信息 将商品存入库存
 37 		 */
 38 		private static void init() {
 39 			Good good1=new Good(1001,"百事可乐",4.5,100,450,"张三");
 40 			Good good2=new Good(1002,"可口可乐",4,100,400,"李四");
 41 			Good good3=new Good(1003,"百事雪碧",3.8,100,380,"张三");
 42 			goodsList.add(good1);
 43 			goodsList.add(good2);
 44 			goodsList.add(good3);
 45 		}
 46 	/*
 47 	 * 根据输入的商品编号查找图书信息,循环遍历库存中商品信息,找到商品编号相等的取出
 48 	 */
 49 		private static Good getGoodsById(int goodId) {
 50 			for (int i = 0; i < goodsList.size(); i++) {
 51 				Good thisGood=goodsList.get(i);
 52 				if (goodId==thisGood.id) {
 53 					return thisGood;
 54 				}
 55 			}
 56 			return null;
 57 		}
 58 	}
 1 	package chapter0701;
 2 	import java.io.BufferedOutputStream;
 3 	import java.io.FileInputStream;
 4 	import java.io.FileNotFoundException;
 5 	import java.io.FileOutputStream;
 6 	import java.io.IOException;
 7 	import java.io.InputStream;
 8 	import java.text.DateFormat;
 9 	import java.text.SimpleDateFormat;
 10 	import java.util.Date;
 11 	/**
 12 	 * 工具类
 13 	 */
 14 	public class FileUtil {
 15 		public static final String SEPARATE_FIELD = ",";// 字段分隔 英文逗号
 16 		public static final String SEPARATE_LINE = "\r\n";// 行分隔
 17 		/**
 18 		 * 保存商品信息
 19 		 */
 20 		public static void saveBooks(Good good) {
 21 			// 判断本地是否存在此文件
 22 			Date date = new Date();
 23 	         // 定义日期格式
 24 			DateFormat format = new SimpleDateFormat("yyyyMMdd");	
 25 	         // 拼接文件名	
 26 	         String name = "进货记录" + format.format(date) + ".csv";
 27 			InputStream in = null;
 28 			try {
 29 				in = new FileInputStream(name);// 判断本地是否存在此文件
 30 				if (in != null) {
 31 					in.close();// 关闭输入流
 32 	                   // 可获取输入流,则存在文件,采取修改文件方式
 33 					createFile(name, true, good);
 34 				}
 35 			} catch (FileNotFoundException e) {
 36 	              // 输入流获取失败,则不存在文件,采取新建新文件方式
 37 				createFile(name, false, good);
 38 			} catch (IOException e) {
 39 				e.printStackTrace();
 40 			}
 41 		}
 42 		/**
 43 		 * 将进货记录的信息保存到本地,可通过label标识来判断是修改文件还是新建文件
 44 		 * @param name  文件名
 45 		 * @param label 文件已存在的标识 true:已存在则修改; false:不存在则新建
 46 		 * @param good  商品信息
 47 		 */
 48 	public static void createFile(String name,boolean label,Good good) {
 49 			BufferedOutputStream out = null;
 50 			StringBuffer sbf = new StringBuffer();// 拼接内容
 51 			try {
 52 				if (label) {// 当已存在当天的文件,则在文件内容后追加
 53 	// 创建输出流,用于追加文件
 54 	out = new BufferedOutputStream(new FileOutputStream(name, true));
 55 				} else {// 不存在当天文件,则新建文件
 56 					// 创建输出流,用于保存文件
 57 	out = new BufferedOutputStream(new FileOutputStream(name));
 58 	String[] fieldSort = new String[] { "商品编号", "商品名称", "购买数量","
 59 	                                   单价", "总价", "联系人" };// 创建表头
 60 					for (String fieldKye : fieldSort) {
 61 						// 新建时,将表头存入本地文件
 62 						sbf.append(fieldKye).append(SEPARATE_FIELD);
 63 					}
 64 				}
 65 				sbf.append(SEPARATE_LINE);// 追加换行符号
 66 				sbf.append(good.id).append(SEPARATE_FIELD);
 67 				sbf.append(good.name).append(SEPARATE_FIELD);
 68 				sbf.append(good.number).append(SEPARATE_FIELD);
 69 				sbf.append((double) good.price).append(SEPARATE_FIELD);
 70 				sbf.append((double) good.money).append(SEPARATE_FIELD);
 71 				sbf.append(good.people).append(SEPARATE_FIELD);
 72 				String str = sbf.toString();
 73 				byte[] b = str.getBytes();
 74 				for (int i = 0; i < b.length; i++) {
 75 					out.write(b[i]);// 将内容写入本地文件
 76 				}
 77 			} catch (Exception e) {
 78 				e.printStackTrace();
 79 			} finally {
 80 				try {
 81 					if (out != null)
 82 						out.close();// 关闭输出流
 83 				} catch (Exception e2) {
 84 					e2.printStackTrace();
 85 				}
 86 			}
 87 		}
 88 	}

【案例7-3】 日记本

本案例要求编写一个程序实现日记本功能,使用字节流将日记的具体信息记录在本地的txt文件中。在写日记时,需要输入的数据项包括“姓名”,“天气”、“标题”、“内容”。文件命名格式为“黑马日记本”加上“.txt”后缀,如“黑马日记本.txt”。保存文件时需要判断本地是否已存在该文件,如果存在则追加,不存在则新建。

7.3 字符流

7.3.1 字符流定义及基本用法

前面已经讲解过InputStream类和OutputStream类在读写文件时操作的都是字节,如果希望在程序中操作字符,使用这两个类就不太方便,为此JDK提供了字符流。同字节流一样,字符流也有两个抽象的顶级父类,分别是Reader和Writer。其中Reader是字符输入流,用于从某个源设备读取字符。Writer是字符输出流,用于向某个目标设备写入字符。

Reader和Writer作为字符流的顶级父类,也有许多子类,接下来通过一张继承关系图列举Reader和Writer的一些常用子类。

第7章 IO(输入输出)_第21张图片

第7章 IO(输入输出)_第22张图片 

字符流的继承关系与字节流的继承关系有些类似,很多子类都是成对(输入流和输出流)出现的,其中FileReader和FileWriter用于读写文件,BufferedReader和BufferedWriter是具有缓冲功能的流,使用它们可以提高读写效率。

7.3.2 字符流操作文件

在程序开发中,经常需要对文本文件的内容进行读取,如果想从文件中直接读取字符便可以使用字符输入流FileReader,通过此流可以从关联的文件中读取一个或一组字符。接下来通过一个案例来学习如何使用FileReader读取文件中的字符。

首先在Java项目的根目录下新建文本文件“reader.txt”并在其中输入字符“itcast”,然后创建一个类Example15,在类Example15中创建字符输入流FileReader对象读取文件中的内容。

1 import java.io.*;
2 public class Example15 {
3	public static void main(String[] args) throws Exception {
4		// 创建一个FileReader对象用来读取文件中的字符
5		FileReader reader = new FileReader("reader.txt");
6		int ch; 				// 定义一个变量用于记录读取的字符
7		while ((ch = reader.read()) != -1) { 	// 循环判断是否读取到文件的末尾
8			System.out.println((char) ch); // 不是字符流末尾就转为字符打印
9		}
10		reader.close(); // 关闭文件读取流,释放资源
11	}
12 }

第7章 IO(输入输出)_第23张图片

上述代码中,第5行代码创建一个FileReader对象与文件关联,第7~9行代码通过while循环每次从文件中读取一个字符并打印,这样便实现了FileReader读文件字符的操作。需要注意的是,字符输入流的read()方法返回的是int类型的值,如果想获得字符就需要进行强制类型转换,如程序中第8行代码就是将变量ch转为char类型再打印。

在上述程序中,FileReader对象返回的字符流是char而InputStream对象返回的字符流是byte这就是两者之间最大的区别。下面我们讲解字符流怎么写入字符,如果要向文件中写入字符就需要使用FileWriter类,该类是Writer的一个子类。

接下来通过一个案例来学习如何使用FileWriter将字符写入文件。

1 import java.io.*;
2 public class Example16 {
3	public static void main(String[] args) throws Exception {
4		// 创建一个FileWriter对象用于向文件中写入数据
5		FileWriter writer = new FileWriter("writer.txt");
6		String str = "你好,Java程序设计";
7		writer.write(str);  // 将字符数据写入到文本文件中
8		writer.write("\r\n");  // 将输出语句换行
9		writer.close(); // 关闭写入流,释放资源
10	}
11 }

程序运行结束后,会在当前目录下生成一个名称为“writer.txt”的文件

FileWriter同FileOutputStream一样,如果指定的文件不存在,就会先创建文件,再写入数据,如果文件存在,则会首先清空文件中的内容,再进行写入。如果想在文件末尾追加数据,同样需要调用重载的构造方法,现将程序中的第5行代码进行如下修改:

FileWriter writer = new FileWriter("writer.txt",true);

再次运行程序,即可实现在文件中追加内容的效果。

包装流可以对一个已存在的流进行包装来实现数据读写功能,利用包装流可以有效地提高读写数据的效率。字符流同样提供了带缓冲区的包装流,分别是BufferedReader和BufferedWriter,其中BufferedReader用于对字符输入流进行包装,BufferedWriter用于对字符输出流进行包装。需要注意的是,在BufferedReader中有一个重要的方法readLine(),该方法用于一次读取一行文本。

接下来通过一个案例来学习如何使用这两个包装流实现文件的拷贝,首先我们在根目录下新建文件“src.txt”,然后编写代码实现文件拷贝。

1 import java.io.*;
2 public class Example17 {
3	public static void main(String[] args) throws Exception {
4		FileReader reader = new FileReader("src.txt");
5		// 创建一个BufferedReader缓冲对象
6		BufferedReader br = new BufferedReader(reader);
7		FileWriter writer = new FileWriter("des.txt");
8		// 创建一个BufferdWriter缓冲区对象
9		BufferedWriter bw = new BufferedWriter(writer);
10		String str;
11		// 每次读取一行文本,判断是否到文件末尾
12	     while ((str = br.readLine()) != null) {			
13		  bw.write(str);
14		  // 写入一个换行符,该方法会根据不同的操作系统生成相应的换行符
15 		  bw.newLine();
16	    }
17		br.close();
18		bw.close();
19	}
20 }		

程序运行结束后,打开当前目录下的文件“src.txt”和“des.txt”

第7章 IO(输入输出)_第24张图片

上述代码中,第6~9行代码分别使用了输入输出流缓冲区对象,第12~16行通过一个while循环实现了文本文件的拷贝。在拷贝过程中,每次循环都使用readLine()方法读取文件的一行,然后通过write()方法写入目标文件。其中readLine()方法会逐个读取字符,当读到回车符'\r'或换行符'\n'时会将读到的字符作为一行的内容返回。

由于字符缓冲流内部使用了缓冲区,在循环中调用BufferedWriter的write()方法写入字符时,这些字符首先会被写入缓冲区,当缓冲区写满时或调用close()方法时,缓冲区中的字符才会被写入目标文件。因此在循环结束时一定要调用close()方法,否则极有可能会导致部分存在缓冲区中的数据没有被写入目标文件。

7.3.3 转换流

前面提到IO流可分为字节流和字符流,有时字节流和字符流之间也需要进行转换。在JDK中提供了两个类可以将字节流转换为字符流,它们分别是InputStreamReader和OutputStreamWriter。

InputStreamReader是Reader的子类,它可以将一个字节输入流转换成字符输入流,方便直接读取字符。OutputStreamWriter是Writer的子类,它可以将一个字节输出流转换成字符输出流,方便直接写入字符。通过转换流进行数据读写的过程如下图。

 通过转换流进行数据读写的过程接下来通过一个案例来学习如何将字节流转为字符流,为了提高读写效率,可以通过BufferedReader和BufferedWriter实现转换工作。

1 import java.io.*;
2 public class Example18 {
3	public static void main(String[] args) throws Exception {
4		// 创建字节输入流
5		FileInputStream in = new FileInputStream("src.txt");
6		// 将字节流输入转换成字符输入流
7		InputStreamReader isr = new InputStreamReader(in); 
8		// 赋予字符流对象缓冲区
9		BufferedReader br = new BufferedReader(isr); 
10		FileOutputStream out = new FileOutputStream("des.txt");
11		          // 将字节输出流转换成字符输出流
12		OutputStreamWriter osw = new OutputStreamWriter(out);
13		// 赋予字符输出流对象缓冲区
14		BufferedWriter bw = new BufferedWriter(osw); 
15		String line;
16		while ((line = br.readLine()) != null) { // 判断是否读到文件末尾
17			bw.write(line); // 输出读取到的文件
18		}
19		br.close();
20		bw.close();
21	}
22 }

在src.txt文件中输入内容“你好,itcast。”,程序运行结束后,文件“src.txt”和“des.txt”

第7章 IO(输入输出)_第25张图片

上述代码实现了字节流和字符流之间的转换,将字节流转换为字符流,从而实现直接对字符的读写。需要注意的是,在使用转换流时,只能针对操作文本文件的字节流进行转换,如果字节流操作的是一张图片,此时转换为字符流就会造成数据丢失。

【案例7-4】 升级版日记本

本案例要求编写一个模拟日记本的程序,通过在控制台输入指令,实现在本地新建日记本、打开日记本和修改日记本等功能。

(1)用户输入指令1代表“新建日记本”,可以从控制台获取用户输入的日记内容。

(2)指令2代表“打开日记本”,读取指定路径的txt文件的内容并输出到控制台。

(3)指令3代表“修改日记本”,修改日记时,既可以修改新建日记本的内容,也可以修改已打开日记本的内容。

(4)指令4代表“保存”,如果是新建的日记本需要保存,则将日记本保存到用户输入的路径;如果是打开的日记本需要保存,则将原来内容覆盖;

(5)指令5代表“退出”,即退出本系统。

第7章 IO(输入输出)_第26张图片 

运行过程中,本地D:\chapter07\task02路径下生成了note.txt文件

运行结束后,本地D:\chapter07\task02路径下的note.txt文件已经修改

(1)创建记事本类,在类中编写执行程序的main方法

 1	package cn.itcast.chapter07.task02;
 2	import java.io.FileReader;
 3	import java.io.FileWriter;
 4	import java.io.IOException;
 5	import java.util.Scanner;
 6	public class Notepad {
 7		private static String filePath;
 8		private static String message = "";
 9		public static void main(String[] args) throws Exception {
 10			Scanner sc = new Scanner(System.in);
 11		System.out.println("--1:新建文件 2:打开文件 3:修改文件  4:保存 5:退出--");
 12			while (true) {
 13				System.out.print("请输入操作指令:");
 14				int command = sc.nextInt();
 15				switch (command) {
 16				case 1:
 17					createFile();//1:新建文件
 18					break;
 19				case 2:
 20					openFile();// 2:打开文件
 21					break;
 22				case 3:
 23					editFile();//3:修改文件
 24					break;
 25				case 4:
 26					saveFile();// 4:保存
 27					break;
 28				case 5:
 29					exit();//5:退出
 30					break;
 31				default:
 32					System.out.println("您输入的指令错误!");
 33					break;
 34				}
 35			}
 36		}
 37	}

(2)在Notepad中编写新建文件方法

/**
 2		 * 新建文件  从控制台获取内容
 3		 */
 4		private static void createFile() {
 5			message = "";//新建文件时,暂存文件内容清空
 6			Scanner sc = new Scanner(System.in);
 7			System.out.println("请输入内容,停止编写请输入\"stop\":");//提示
 8			StringBuffer stb = new StringBuffer();//用于后期输入内容的拼接
 9			String inputMessage = "";
 10			while (!inputMessage.equals("stop")) {//当输入“stop”时,停止输入
 11				if (stb.length() > 0) {
 12					stb.append("\r\n");//追加换行符
 13				}
 14				stb.append(inputMessage);//拼接输入信息
 15				inputMessage = sc.nextLine();//获取输入信息
 16			}
 17			message = stb.toString();//将输入内容暂存
 18		}

(3)在Notepad中编写打开文件方法

/**
 2		 * 打开文件 
 3		 */
 4		private static void openFile() throws Exception {
 5			message = "";//打开文件时,将暂存内容清空
 6			Scanner sc = new Scanner(System.in);
 7			System.out.print("请输入打开文件的位置:");
 8			filePath = sc.next();//获取打开文件的路径
 9			//控制只能输入txt格式的文件路径
 10			if (filePath != null && !filePath.endsWith(".txt")) {
 11				System.out.print("请选择文本文件!");
 12				return;
 13			}
 14			FileReader in = new FileReader(filePath);//实例化一个FileReader对象
 15			char[] charArray = new char[1024];//缓冲数组
 16			int len = 0;
 17			StringBuffer sb = new StringBuffer();
 18			// 循环读取,一次读取一个字符数组
 19			while ((len = in.read(charArray)) != -1) {
 20				sb.append(charArray);
 21			}
 22			message = sb.toString();//将打开文件内容暂存
 23			System.out.println("打开文件内容:" + "\r\n" + message);
 24			in.close();// 释放资源
 25		}

(4)在Notepad中编写修改文件方法

/**
 2		 * 修改文件内容    通过字符串替换的形式
 3		 */
 4		private static void editFile() {
 5			if(message == "" && filePath == null){
 6				System.out.println("请先新建文件或者打开文件");
 7				return;
 8			}
 9			Scanner sc = new Scanner(System.in);
 10			System.out.println("请输入要修改的内容(以 \"修改的目标文字:修改之后的文字\" 格式),"
 11					+ "停止修改请输入\"stop\":");
 12			String inputMessage = "";
 13			while (!inputMessage.equals("stop")) {//当输入stop时,停止修改
 14				inputMessage = sc.nextLine();
 15				if (inputMessage != null && inputMessage.length() > 0) {
 16					// 将输入的文字根据“:”拆分成数组
 17					String[] editMessage = inputMessage.split(":");
 18					if (editMessage != null && editMessage.length > 1) {
 19					//根据输入的信息将文件中内容替换
 20					message=message.replace(editMessage[0], editMessage[1]);
 21					}
 22				}
 23			}
 24			System.out.println("修改后的内容:" + "\r\n" + message);
 25		}

(5)编写保存文件方法

/**
 2		 * 保存  新建文件存在用户输入的路径  打开的文件将原文件覆盖
 3		 */
 4		private static void saveFile() throws IOException {
 5			Scanner sc = new Scanner(System.in);
 6			FileWriter out = null;
 7			if (filePath != null) {// 文件是由“打开”载入的
 8				out = new FileWriter(filePath);//将原文件覆盖
 9			} else {//新建的文件
 10				System.out.print("请输入文件保存的绝对路径:");
 11				String path = sc.next();//获取文件保存的路径
 12				filePath = path;
 13				//将输入路径中大写字母替换成小写字母后判断是不是文本格式
 14				if (!filePath.toLowerCase().endsWith(".txt")) {
 15					filePath += ".txt";
 16				}
 17				out = new FileWriter(filePath);// 构造输出流
 18			}
 19			out.write(message);//写入暂存的内容
 20			out.close();//关闭输出流
 21			message = "";// 修改文件前现将写入内容置空
 22			filePath = null;//将文件路径至null
 23		}

(6)编写退出程序的方法

/**
 2		 * 退出
 3		 */
 4		private static void exit() {
 5			System.out.println("您已退出系统,谢谢使用!");
 6			System.exit(0);
 7		}

【案例7-5】 微信投票

如今微信聊天已经普及到几乎每一个人,在聊天中,经常会有人需要帮忙在某个APP中投票。本案例要求编写一个模拟微信投票的程序,通过在控制台输入指令,实现添加候选人、查看当前投票和投票的功能。每个功能的具体要求如下:

(1)用户输入指令1代表”添加候选人”,可以在本地文件中添加被选举人。

(2)用户输入指令2代表”查看当前投票”,将本地文件中的数据打印到控制台。

(3)用户输入指令3 代表”投票”功能,在控制台输入被投票人的名字进行投票操作。

(4)用户输入指令4代表”退出”操作。

第7章 IO(输入输出)_第27张图片

运行过程中,本地D://下会生成一个count.txt文件

第7章 IO(输入输出)_第28张图片

 1 	package chapter0704;
 2 	import java.io.BufferedOutputStream;
 3 	import java.io.File;
 4 	import java.io.FileInputStream;
 5 	import java.io.FileNotFoundException;
 6 	import java.io.FileOutputStream;
 7 	import java.io.FileWriter;
 8 	import java.util.Scanner;
 9 	public class vote {
 10 		private static String message = "";
 11 		public static void main(String[] args) throws Exception {
 12 			Scanner sc = new Scanner(System.in);
 13 			System.out.println("1:添加候选人 2:查看当前投票  3:投票  4:退出");
 14 				while (true) {
 15 					System.out.print("请输入操作指令:");
 16 					int command = sc.nextInt();
 17 					switch (command) {
 18 					case 1:
 19 						System.out.println("请输入需要被投票的人:");
 20 						String name=sc.next();
 21 						System.out.println("请输入"+name+"的起始票数:");
 22 						int num=sc.nextInt();
 23 						addvote(name,num);// 1:添加投票人
 24 						break;
 25 					case 2:
 26 						String me = readvote();// 2:查看投票
 27 						System.out.println(me);
 28 						break;
 29 					case 3:
 30 						editvote();// 3:投票
 31 						break;
 32 					case 4:
 33 						System.out.println("您已退出系统,谢谢使用!");
 34 						System.exit(0);
 35 						break;
 36 					default:
 37 						System.out.println("您输入的指令错误!");
 38 						break;
 39 					}
 40 				}
 41 		}
 42 		/*
 43 		 * 查看投票
 44 		 */
 45 		public static String readvote() throws Exception{
 46 			FileInputStream in = new FileInputStream("D:\\count.txt");
 47 			byte[] b = new byte[in.available()];
 48 			in.read(b);
 49 			message =new String(b);
 50 			in.close();
 51 			return message;
 52 		}
 53 		/*
 54 		 * 添加投票
 55 		 */
 56 		public static void addvote(String name,int num) throws 
 57 	             FileNotFoundException {
 58 			String SEPARATE_FIELD = "\n";// 换行
 59 			BufferedOutputStream out = null;
 60 			StringBuffer sbf = new StringBuffer();// 拼接内容
 61 			File file=new File("D:\\count.txt");   //判断文件是否存在
 62 			try {
 63 	              // 当已存在count.txt文件,则在文件内容后追加
 64 				if (file.canExecute()) {
 65 					// 创建输出流,用于追加文件
 66 					out = new BufferedOutputStream(new 
 67 	                         FileOutputStream("D:\\count.txt", true));
 68 				} else {// 不存在当天文件,则新建文件
 69 					// 创建输出流,用于保存文件
 70 					out = new BufferedOutputStream(new 
 71 	                          FileOutputStream("D:\\count.txt"));
 72 				}
 73 				sbf.append(name).append(":");
 74 				sbf.append(num).append(","+SEPARATE_FIELD);
 75 				String str = sbf.toString();
 76 				byte[] b = str.getBytes();
 77 				for (int i = 0; i < b.length; i++) {
 78 					out.write(b[i]);// 将内容写入本地文件
 79 				}
 80 			} catch (Exception e) {
 81 				e.printStackTrace();
 82 			} finally {
 83 				try {
 84 					if (out != null)
 85 						out.close();// 关闭输出流
 86 				} catch (Exception e2) {
 87 					e2.printStackTrace();
 88 				}
 89 			}
 90 		}
 91 		/*
 92 		 * 投票 将控制台输入的姓名的票数加一后保存
 93 		 */
 94 		private static void editvote() throws Exception {
 95 			FileWriter out = null;
 96 	          // 将count.txt文字根据“,”拆分成数组
 97 			String[] editMessage = message.split(",");
 98 			Scanner sc = new Scanner(System.in);
 99 			System.out.println("请输入要投票人的姓名");
 100 			String inputMessage = sc.next();
 101 	         //for循环拆分后的数组
 102 			for (int i = 0; i < editMessage.length; i++) { 
 103 	              //当数组中有有包含输入的名字时   
 104 				if(editMessage[i].contains(inputMessage)) {   
 105 					//取出输入名字现在的票数
 106 			String a  = 
 107 	               editMessage[i].substring(editMessage[i].indexOf(":")+1, 
 108 	               editMessage[i].length());
 109 					//将取出的票数强转为int类型
 110 					int b = Integer.parseInt(a);
 111 					//将票数+1
 112 					b++;
 113 					//new出一个StringBuffer用于后面的字符拼接
 114 					StringBuffer sb = new StringBuffer();
 115 					//取出原文件中的姓名和:
 116 	String c=editMessage[i].substring(0,editMessage[i].indexOf(":")+1);
 117 					//使用StringBuffer拼接姓名和选票
 118 					sb.append(c);
 119 					sb.append(b);
 120 					sb.append(",");
 121 					//将拼接后的字符强转为String类型
 122 					String s = sb.toString();
 123 					//修改好的字符替换原有的字符
 124 					System.out.println("投票后的票数:");
 125 					System.out.println(s);
 126 					editMessage[i] = s;
 127 				}
 128 			}
 129 	//editMessage是数组,我们需要将editMessage数组拼接为StringBuffer类型。
 130 			StringBuffer sb1 = new StringBuffer();
 131 			for (int i = 0; i < editMessage.length; i++) {
 132 				sb1.append(editMessage[i]);
 133 			}
 134 			out = new FileWriter("D:\\count.txt");//覆盖原有文件
 135 			//sb1是StringBuffer类型,需要使用toString()方法
 136 			out.write(sb1.toString());// 写入暂存的内容
 137 			out.close();
 138 		}
 139 	}

7.4 本章小结

本章主要介绍了IO流的相关知识。首先讲解了如何创建File对象和File的常用方法,并以案例的形式讲解了如何对文件和文件夹进行相应的创建、查询、遍历以及删除等操作。最后使用字节流和字符流来对磁盘上的文件进行读写以及拷贝。通过本章的学习,希望读者可以认识IO流,并能够熟练掌握输入输出的相关知识。

你可能感兴趣的:(Java,java,开发语言,IO流)