文件(file)这个概念,在计算机中是“一词多用”的,有狭义的文件,也有广义的文件。
狭义的文件:计算机硬盘上的 文件 和 目录(日常用语即称文件夹)
广义的文件:泛指计算机中的很多软硬件资源。也就是操作系统中把很多硬件设备和软件资源都抽象成了文件,按照文件的方式进行统一管理。
本文只讨论狭义的文件。
之前所介绍的代码中,存储数据主要是靠变量,而变量是在内存中的,我们可以直接对内存进行操作。
现在的文件则是在硬盘上。在硬盘上操作数据则没那么直接,更麻烦一些。
每个文件在硬盘上都有一个具体的“路径”,通过文件右键菜单的属性中可以知道文件的路径。
如图我们读到了代码的路径为C:\Users\25803\Desktop,那么其第一个“大写字母:”为盘符,我们计算机有许多硬盘,比如说C:、D:、E:等,这样的盘符是通过“硬盘分区”来的。每个盘符可以是一个单独的硬盘,也可以是若干个盘符对应一个硬盘。
计算机中有两种表示路径的风格
绝对路径:以盘符开头的路径
相对路径:以当前所在目录为基准,以.或者..开头(.开头有时候可以省略),找到指定 路径
当前所在目录称为工作目录。每个程序运行的时候都有一个工作目录(在控制台通过命令操作的时候,是特别明显的,后来进化到图形化界面了,工作目录就不那么直观了)
假设我们假设当前的工作目录为d:/tmp,如果要定位到111这个目录,就可以表示成./111。同理./222和./111/aaa就能找到222、aaa所在目录。
文件的类型
在我们计算机中有许多中类型的文件,例如word、exe、图片、视频、音频、源代码、动态库……那么这些不同的文件,整体可以归纳到两类中。一类是文本文件,存的是文本、字符串(字符串,是由字符构成的。每个字符,都是通过一个数字表示的。这个文本文件里存的数据,一定是合法的字符,都是我们程序员指定字符编码的码表之内的数据。),一类是二进制文件,存的是二进制数据,不一定是字符串了(没有任何限制,可以存储任何需要的数据)。
那么如何区分一个文件是文本还是二进制?
直接使用记事本打开,如果乱码了,就说明是二进制,如果没乱,说明就是文本。
实际些代码的时候,这两类文件的处理方式略有差别。
了解了背景知识之后,我们来看一下Java中对于文件的操作。
针对文件系统操作(文件的创建、删除、重命名……)。
针对文件内容操作(文件的读和写)。
Java中操作文件
文件系统操作
Java 中通过 java.io.File 类来对一个文件(包括目录)进行抽象的描述。注意,有 File 对象,并不
代表真实存在该文件。
File概述
我们先来看File 类中的常见属性、构造方法和方法
属性
修饰符及类型 | 属性 | 说明 |
static String | pathSeparator | 依赖于系统的路径分隔符,String类型的表示 |
static char | pathSeparator | 依赖于系统的路径分隔符,char类型的表示 |
构造方法
方法名 | 说明 |
File(File parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例 |
File(String pathname) | 根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者相对路径 |
File(String parent, String child) | 根据父目录 + 孩子文件路径,创建一个新的 File 实例,父目录用路径表示 |
注:
在new File对象的时候,构造方法参数中,可以指定一个路径。此时File对象就代表这个路径对应的文件了;
这里的路径可以是绝对路径,也可以是相对路径
parent为当前文件所在目录,child为自身的文件名
方法
修饰符及返回值类型 | 方法签名 | 说明 |
String | getParent() | 返回File对象的父目录文件路径 |
String | getName() | 返回File对象的纯文件名称 |
String | getPath() | 返回File对象的文件路径 |
String | getAbsolutePath() | 返回File对象的绝对路径 |
String | getCanonicalPath() | 返回File对象的修饰过的绝对路径 |
boolean | exists() | 判断File对象描述的文件是否真实存在 |
boolean | isDirectory() | 判断File对象代表的文件是否是一个目录 |
boolean | isFile() | 判断File对象代表的文件是否是一个普通文件 |
boolean | createNewFile() | 根据File对象,自动创建一个空文件。成功创建后返回true |
boolean | delete() | 根据File对象,删除该文件。成功删除后返回true |
void | deleteOnExit() | 根据File对象,标注文件将被删除,删除动作会到JVM结束时才会进行 |
String[] | list() | 返回File对象代表的目录下的所有文件名 |
File[] | listFiles() | 返回File对象代表的目录下的所有文件,以File对象表示 |
boolean | mkdir() | 创建File对象代表的目录 |
boolean | mkdirs() | 创建File对象代表的目录,如果必要,会创建中间目录 |
boolean | renameTo(File dest) | 进行文件改名,也可以视为我们平时的剪切、粘贴操作 |
boolean | canRead() | 判断用户是否对文件有可读权限 |
boolean | canWrite() | 判断用户是否对文件有可写权限 |
代码示例
示例一
import java.io.File;
import java.io.IOException;
public class IODemo1 {
public static void main(String[] args) throws IOException {
File file = new File("./test.txt");
System.out.println(file.getName());
System.out.println(file.getParent());
System.out.println(file.getPath());
System.out.println(file.getAbsolutePath());
System.out.println(file.getCanonicalPath());
}
}
示例二
import java.io.File;
import java.io.IOException;
public class IODemo2 {
public static void main(String[] args) throws IOException {
File file = new File("./test.txt");
file.createNewFile();
System.out.println(file.exists());
System.out.println(file.isFile());
System.out.println(file.isDirectory());
}
}
示例三
import java.io.File;
public class IODemo3 {
public static void main(String[] args) {
File file = new File("./test.txt");
file.delete();
}
}
示例四
import java.io.File;
public class IODemo4 {
public static void main(String[] args) {
File dir = new File("./test/aaa/bbb");
dir.mkdirs();
}
}
示例五
import java.io.File;
public class IODemo5 {
public static void main(String[] args) {
File file = new File("./test");
File dest = new File("./testAAA");
file.renameTo(dest);
}
}
文件内容操作
我们使用一组“流对象”针对文件内容进行操作。
什么叫做“流”?这是一个形象的比喻。计算机里面很多概念都采用了一定的修辞手法,比喻是一种常见的方式。再比如使用一个链表的头节点/二叉树的根节点来表示整个链表/二叉树,这也是属于一种借代的修辞手法。
谈到“流”我们可能想到的是水流,水流的特点是生生不息,绵延不断,这是我们对于水流的一个感受。
那么这个“流对象”也跟水流差不多,想象一下,有个水龙头,通过这个水龙头可以接水,比如说要接100ml水,我们可以一次接100ml,一次接完;也可以一次接50ml,分两次接,还可以一次接10ml,分10次来接……。
所以文件的读写也是类似的,比如说要从文件中读100个字节,就可以一次读100字节,一次读完,一次读50字节,两次读完……也是可以随心所欲的读的。所以我们就给它起了一个形象的名字“流”。写文件同理。
因此我们就把读写文件的相关对象称为“流对象”,这个比喻并非是Java独有的,其实是操作系统的api就是这样设定的,进一步的各种的编程语言,操作文件也是继承了这个概念。
Java标准库的流对象
从类型上,分成两个大类,每个大类里面有很多小类,这里虽然涉及到的类很多,但是规律性很强。
字节流:以字节为单位,操作二进制数据
字节流中提供的主要的类有InputStream和OutputStream,那么由于这两个类都是抽象类,我们在实现的时候都要new其实现类FileInputStream或OutputStream
字符流:以字符为单位操作文本数据
字符流主要提供的类有Reader和Writer,那么由于这两个类都是抽象类,我们在实现的时候都要new其实现类FileReader或FileReader
这些类的使用方式是非常固定的,核心就是四个操作
(1)打开文件(构造对象)
(2)关闭文件(close)
(3)读文件(read)=>针对InputStream / Reader
(4)写文件(write)=>针对OutputStream / Writer
使用字节流操作文件
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Description:使用字节流读取文件
*/
public class IODemo6 {
public static void main(String[] args) throws IOException {
//创建InputStream对象的时候,使用绝对路径或者相对路径,或者File对象都是可以的
InputStream inputStream = new FileInputStream("d:/test.txt");
//进行读操作
//read():一次读取一个字节
while (true) {
int b = inputStream.read();
if (b == -1) {
//读取完毕
break;
}
System.out.printf("%x ", (byte)b);
}
inputStream.close();
}
}
//read的第二个用法,一次读取若干个字节
while (true) {
byte[] buffer = new byte[1024];//read的第二个版本,需要调用者提前准备好一个数组
int len = inputStream.read(buffer);
//表示实际上读到几个字节
System.out.println("len:" + len);
if (len == -1) {
break;
}
//此时读取结果就被放到byte数组中
for (int i = 0; i < len; i++) {
System.out.printf("%x ", buffer[i]);
}
}
注:注意理解第二种用法中read的行为和返回值,read会尽可能的把参数传进来的数组都填满。
上面代码中给的数组长度是1024,read就会尽可能的读取1024个字节,填到数组里。但实际上,文件的剩余长度是有限的,如果剩余长度超过1024,此时1024个字节就都会填满,返回值就是1024了。如果当前剩余的长度不如1024,此时有多少就填多少,read方法就会返回当前实际读取的长度。
import java.io.IOException;
import java.io.OutputStream;
public class IODemo7 {
//进行写文件
public static void main(String[] args) throws IOException {
try (OutputStream outputStream = new FileOutputStream("d:/test1.txt")) {
outputStream.write(97);
outputStream.write(98);
outputStream.write(99);
outputStream.write(100);
}
}
}
对于OutputStream来说,默认情况下,打开一个文件,会先清空文件原有的内容。
如果不想清空,流对象还提供了一个“追加写”对象,通过这个对象就可以实现不清空文件,把新的内容追加写到后面。
注意:input和output的方向,是以CPU为中心来看待这个方向的。
一般我们认为,内存更接近于CPU,硬盘离CPU更远。
以CPU为中心,数据朝着CPU的方向流向就是输入,所以就把数据从硬盘到内存的这个过程称为读(input);那么数据朝远离CPU的方向流向,就是输出,所以就把数据从内存到硬盘的这个过程称为写(write)。
使用字符流操作文件
字符流和字节流用法类似。
以下代码可以看到reader的用法,基本的用法也和前文的inputStream操作类似,也是通过read方法进行操作。
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
public class IODemo8 {
//字符流的操作
public static void main(String[] args) {
try (Reader reader = new FileReader("d:/test.txt")) {
while (true) {
int ch = reader.read();
if (ch == -1) {
break;
}
System.out.print(" " + (char) ch);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
字符流的写操作也是与前文outputStream类似的。
package IO;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
public class IODemo9 {
public static void main(String[] args) {
try(Writer writer = new FileWriter("d:/test.txt")){
writer.write("hello world");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Scanner是搭配流对象进行使用的。
我们以前经常使用的下面这句代码中的System.in就是一个输入流对象。
Scanner scanner = new Scanner(System.in);
所以我们可以知道在创建scanner对象的时候是对构造方法传输入流类型的参数,那么我们就可以对文件进行写入操作。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class IODemo10 {
public static void main(String[] args) {
try (InputStream inputStream = new FileInputStream("d:/test1.txt")){
Scanner scanner = new Scanner(inputStream);
scanner.next();
} catch (IOException e) {
e.printStackTrace();
}
}
}
小程序练习
给定一个目录,目录里包含很多的文件和子目录。用户输入一个要查询的词,看看当前目录下(以及子目录里)是否有匹配的结果,如果有匹配结果,就进行删除。
import java.io.File;
import java.util.Scanner;
public class IODemo11 {
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
System.out.println("请输入要搜索的路径:");
String basePath = scanner.next();
File root = new File(basePath);
if (!root.isDirectory()) {
System.out.println("输入的目录有误!");
return;
}
System.out.println("请输入要删除的文件名:");
String nameToDelete = scanner.next();
//递归查找文件
scanDir(root, nameToDelete);
}
private static void scanDir(File root, String nameToDelete) {
File[] files = root.listFiles();
if (files == null) {
return;
}
for (File f : files) {
if (f.isDirectory()) {
scanDir(f, nameToDelete);
} else {
if (f.getName().contains(nameToDelete)) {
System.out.println("请确认是否要删除" + f.getAbsolutePath() + "吗?");
String choice = scanner.next();
if (choice.equals("y") || choice.equals("Y")) {
f.delete();
System.out.println("删除成功");
} else {
System.out.println("删除取消");
}
}
}
}
}
}
把一个文件拷贝成另一个文件
把第一个文件按照字节依次读取,把结果写入另一个文件中。
import javax.sound.midi.Soundbank;
import java.io.*;
import java.sql.SQLOutput;
import java.util.Scanner;
public class IODemo12 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入你要拷贝哪些文件");
String srcPath = scanner.next();
System.out.println("请输入你要拷贝到的位置");
String destPath = scanner.next();
File srcFile = new File(srcPath);
if (!srcFile.isFile()) {
System.out.println("源路径有误");
return;
}
File destFile = new File(destPath);
if (destFile.isFile()) {
System.out.println("文件已存在,无法拷贝");
return;
}
try (InputStream inputStream = new FileInputStream(srcFile);
OutputStream outputStream = new FileOutputStream(destFile)) {
while (true) {
int b = inputStream.read();
if (b == -1) {
break;
}
outputStream.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}