上节回顾
File 构造方法中需要传入一个具体的文件路径
绝对路径:以盘符开头的
相对路径:以 . 或者 .. 开头的。 . 表示当前的路径。.. 表示当前目录的上级目录。
相对路径一定有一个基准目录(也叫做工作目录),对于IDEA来说,在IDEA中运行的程序的当前的工作目录就是项目所在目录
File中提供的一些方法。
File类中的方法演示
public static void main(String[] args) { File file = new File("test"); System.out.println(file.exists());//判断这个"text"是否存在 //mk 就是make dir就是dieection这个单词的缩写 file.mkdir();//创建目录 System.out.println(file.exists());// System.out.println(file.isDirectory());//判断是不是目录 }
打印结果
new File()括号里面如果传的是一个多层目录(就是嵌套式的目录,目录里面包含目录),mkdir()方法可能就会创建失败
public static void main(String[] args) { File file = new File("test/aaa/bbb"); System.out.println(file.exists());//判断这个"text"是否存在 //mk 就是make dir就是dieection这个单词的缩写 file.mkdir();//创建目录 System.out.println(file.exists());// System.out.println(file.isDirectory());//判断是不是目录 }
打印结果
如何解决创建多层目录失败的问题?
用mkdirs()方法
public static void main(String[] args) { File file = new File("test/aaa/bbb"); System.out.println(file.exists());//判断这个"text"是否存在 //mk 就是make dir就是dieection这个单词的缩写 //mkdir 不支持创建多级目录.但是 mkdirs能支持 file.mkdirs();//创建目录 System.out.println(file.exists());// System.out.println(file.isDirectory());//判断是不是目录 }
打印结果:
public static void main(String[] args) { File file2 = new File("hello.text"); File file1 = new File("test.txt"); file2.renameTo(file1);//更改文件名,移动文件 }
renameTo()方法不光可以用来改文件名,还可以用来移动文件(把一个文件从一个目录中移动到另外的目录中)
public static void main(String[] args) { File file2 = new File("test.txt"); File file1 = new File("./out/test.txt"); file2.renameTo(file1);//更改文件名,移动文件 }
所谓的文件移动(剪切粘贴),对于操作系统来说,其实是一个非常高效的操作。
每个文件,都有个属性,这个属性就是该文件的路径。移动操作,其实就只是修改了一下文件的属性而已。
如果要是把文件跨硬盘来移动,这个时候仍然会比较低效。
所谓的文件复制(复制粘贴),对于操作系统来说,很可能是一个非常低效的操作。
复制操作就需要把文件的内容读出来,然后再拷贝一份,写入到磁盘中。这个过程就需要消耗比较大的开销了(文件可能很大)
以上File这里提供的这些文件操作,都是一些操作文件的基础动作。
这些操作统称为“文件系统操作”。操作文件的属性
操作文件的内容
读 写 文件内容
Java标准库中,读写文件相关的类,有很多。
InputStream / FileputStream
文件读取操作,按照字节为单位进行读文件。
OutputStream / FileOutputStream
文件写入操作,按照字节为单位进行写文件。
这两组类称为字“节流”。
Stream翻译成中文就是“流”。把数据比作了水流一样。
例如:
我想从文件中读取100个字节的数据。
可以1次读100个字节。
也可以分2次读,每次读50个字节。或者1次读40,一次读60。
还可以分10次读,1次读10个字节。
上面的两组类正是具备这样的特点。
这两组类里面主要提供了两个方法:
read();
write();
由于这是字节流,最小单位就是字节。
Java中除了字节流之外,还有字符流,以字符为单位,进行读写了。
读操作
准备一个文件
添加文件内容
然后我们用代码来读取文件的内容
//FileNotFoundExeption其实是继承自IOException //一个 FileNotFoundExeption异常其实也是一个IOException public static void main(String[] args) throws IOException { //创建实例的过程,就相当于打开文件。 //要先打开文件,然后才能进行读写。 InputStream inputStream = new FileInputStream("./test.txt"); //逐个字节的方式把文件内容读取出来. while(true){ //每次调用 read 就可以读取一些数据出来. //read 提供了好几个版本,其中无参数版本就是一次读取一个字节 //对于这个无参版本的 read 来说,返回值就是这次操作读到的字节. //这个结果的范围就是 0~ 255. // 如果读到文件末尾了(EOF,and of File),此时继续进行 read,就会返回 -1 int a = inputStream.read(); if(a == -1){ break; } System.out.printf("%c",a); } inputStream.close(); }
打印结果
这个代码中还有一个非常重要的操作,除了打开文件,进行读操作,还得有一个关闭操作
inputStream.close()。
使用流对象,读写完文件之后,一定要记得及时关闭!!
如果没有关闭,就可能造成资源泄露。
关于资源泄露:
系统中的很多资源都是有限的:内存、文件描述符。
什么叫文件描述符:
每个进程都有自己的文件描述符表(是PCB的一个属性)。
好比我创建个进程1能打开个若干个文件,创建个2能打开个若干个文件。
每个进程能打开多少个文件都有一个上限。
这个上限具体是多少?不确定,或者说可配置。
windows系统咋配置,不清楚,也不需要关注。
重点关注,Linux咋搞,Linux有一个ulimit命令,能够查看当前的文件描述符最多能创建多少个,也可以修改这里的值。
这里的结果可以修改,因此并不能确定具体是多少,但是不管怎么改终究是有上限的,只要有上限就得注意资源泄露问题。
回到上面的代码,如果按照上面的方式来写,看起来从上往下执行到close()了,但其实不一定就真正能够执行到close(),这个代码不一定是顺序往下执行的,有可能会出现异常,一旦读取操作的时候出现了异常,一下就IOException了,此时就不会执行到close()了。因此最好的做法就是把close()操作放到finally语句中。我们来改一下代码
//FileNotFoundExeption其实是继承自IOException //一个 FileNotFoundExeption异常其实也是一个IOException public static void main(String[] args) { //创建实例的过程,就相当于打开文件。 //要先打开文件,然后才能进行读写。 InputStream inputStream = null; try { inputStream = new FileInputStream("./test.txt"); //逐个字节的方式把文件内容读取出来. while(true){ //每次调用 read 就可以读取一些数据出来. //read 提供了好几个版本,其中无参数版本就是一次读取一个字节 //对于这个无参版本的 read 来说,返回值就是这次操作读到的字节. //这个结果的范围就是 0~ 255. // 如果读到文件末尾了(EOF,and of File),此时继续进行 read,就会返回 -1 int a = inputStream.read(); if(a == -1){ break; } System.out.printf("%c",a); } } catch (IOException e) { e.printStackTrace(); } finally { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } }
改动之后的代码就能更加稳健的警觉close()的问题。
就不会发生读取异常调用不到close()的情况。
因此close代码写到finlaly中才是更科学的做法。
但是这样写很繁琐,很麻烦,我们还有一种更简单的写法:
Java中提供了 try with resources 语法,可以更好的解决这个问题。
改一下代码:
public static void main(String[] args) { try (InputStream inputStream = new FileInputStream("./test.txt")){ while(true){ int a = inputStream.read(); if(a == -1){ break; } System.out.printf("%c",a); } }catch (IOException e){ e.printStackTrace(); }
打印结果
这样的写法明显代码量少,简单的多,而且并没有显示的写close()方法,这个close会在执行完try catch之后自动调用inputStream的close方法。
上面写的是一次读取一个字节的代码。
除了一次读取一个字节之外,也可以一次读取多个字节。
public static void main(String[] args) { //一次读取1024个字节 try(InputStream inputStream = new FileInputStream("./test.txt")){ byte[] buffer = new byte[1024]; while(true){ int len = inputStream.read(buffer); if(len == -1){ break; } for (int i = 0; i < len; i++) { System.out.printf("%c",buffer[i]); } } }catch (IOException e){ e.printStackTrace(); } }
打印结果
这个代码中的read就会尝试把参数的这个buffer给填满。
当前buffer是1024个字节。
如果假设该文件的长度是2049,读取过程:
第一次循环,读取出1024个字节,放到buffer数组中,read返回一个1024.
第二次循环,读取出1024个字节,放到buffer数组中,read返回一个1024.
第三次循环,读取出1个字节,放到buffer数组中,read返回1.
第四次循环,此时已经读到文件末尾了(EOF),read返回-1.
假设我们下面不读取hello world了,我们写一个张三
读取这个张三
此时我们再用上面的代码打印就会抛出一个异常
中文的编码方式和英文不太一样。
英文的话,直接就是AscII码,就比较简单。
中文的话,就需要使用UTF-8或者GBK,就会更复杂。
代码中的%c其实相当于是按照AscII的方式来进行打印。
这个时候,如果读取的这个字节,只是UTF-8或者GBK的一个部分,此时很可能这个结果不是一个合法的AscII 值,于是这里就出现异常了。
如何读取正确:
一个汉字在UTF-8中,是由3个字节构成的
//一次读取1024个字节 try(InputStream inputStream = new FileInputStream("./test.txt")){ byte[] buffer = new byte[1024]; while(true){ int len = inputStream.read(buffer); if(len == -1){ break; } //此处这个地方就不能按照字节分别打印了,而需要把三个字节合并到一起,作为一个整体(String) 来打印 for (int i = 0; i < len; i += 3) { //这个写法是一个铤而走险的写法,如果文件中包含汉字和英文混合,这里就容易出现问题 //最好不要这样写 //其实在Java 标准库中,已经对于字符集有了内置的解决方案.不需要咱们在这里自己来实现. String s = new String(buffer,i,3,"UTF-8"); System.out.print(s); } } }catch (IOException e){ e.printStackTrace(); } }
打印结果
这个写法其实是一个铤而走险的写法,因为基于太多假设,如果是当前文件里面光是纯中文还好,要是什么中英混合,再这样来处理的话很可能出现问题。因此一般不建议直接这样写。
尝试从文件中读取中文,借助标准库内置的处理字符集方式:public static void main(String[] args) { //尝试从文件中读取中文,借助标准库内置的处理字符集方式 //Scanner不光能从控制台读取标准输入,也可以从文件中去读数据. try(InputStream inputStream = new FileInputStream("./test.txt")){ //Scanner 里面也有个 close方法,这个close其实也是用来关闭 Scanner 内包含的 InputStream try(Scanner scan = new Scanner(inputStream,"UTF-8");){ while (scan.hasNext()){ String s = scan.next(); System.out.println(s); } } }catch (IOException e){ e.printStackTrace(); } }
之前SE阶段也没少用Scanner,当时咋没有写 close 呢?
当时Scanner内部持有的 InputStream 是 System.in(标准输入)
标准输入这个文件,一般是不关闭.这个文件是每个进程创建出来之后,操作系统默认打开的文件.
写操作
OutputStream
flush()方法:
刷新缓冲区。计算机读写内存的速度比读写磁盘的速度快很多,所以有些时候为了能够提高效率,就可以减少直接访问磁盘的次数。使用缓冲区就能够很好的解决这个问题。
缓冲区其实i就是一段内存空间(这个内存是 OutputStream里面自带的)。
当我们使用write方法来写数据的时候,并不是直接把数据写到磁盘上,而是先放到缓冲区中(内存中)。
如果缓冲区满了,或者手动调用flush()方法,才会真的把数据写到磁盘上。
内存和磁盘之间的缓冲区,往往是一个内存空间。
CPU和内存之间,其实也有缓冲区。(L1,L2,L3 cache)
基本的写入文件操作
public static void main(String[] args) { //一旦按照 OutputStream 的方式打开文件,就会把文件的原来内容给清空掉。 try(OutputStream outputStream = new FileOutputStream("./test.txt")) { //写入一个字符 outputStream.write('a'); outputStream.write('b'); outputStream.write('c'); outputStream.write('d'); }catch (IOException e){ e.printStackTrace(); } }
此时张三已经不见了,变成了abcd
按照字节来写入
public static void main(String[] args) { //一旦按照 OutputStream 的方式打开文件,就会把文件的原来内容给清空掉。 try(OutputStream outputStream = new FileOutputStream("./test.txt")) { //按照字节来写入 byte[] buffer = new byte[]{(byte) 'a',(byte)'v',(byte)'e'}; outputStream.write(buffer); }catch (IOException e){ e.printStackTrace(); } }
写入一个字符串
public static void main(String[] args) { //一旦按照 OutputStream 的方式打开文件,就会把文件的原来内容给清空掉。 try(OutputStream outputStream = new FileOutputStream("./test.txt")) { //按照字符串来写 String s = "hello world"; outputStream.write(s.getBytes());//把字符串转换成字节数组 }catch (IOException e){ e.printStackTrace(); } }
PrintWriterpublic static void main(String[] args) { //使用PrintWriter 来包装一下OutputStream 然后更方便的写数据 try(OutputStream outputStream = new FileOutputStream("./test.txt")){ //使用PrintWriter 来包装一下 try(PrintWriter writer = new PrintWriter(outputStream)){ writer.println("你好世界"); } }catch (IOException e){ e.printStackTrace(); } }
关于读写文件还有很多其他的用法.
代码案例
案例1:指定一个目录,扫描这个目录(由于目录中又包含了其他的目录,一次就需要通过递归的方式,来把里面的内容也都获取到),找到文件名中包含了指定字符的条件,并提示用户是否要删除这个文件,根据用户的输入是否决定删除.
public static void main(String[] args) throws IOException { //1.让用户指定一个带扫描的根目录 和 要查询的关键词 System.out.println("请输入要扫描的根目录(绝对路径):"); Scanner scanner = new Scanner(System.in); String root = scanner.next(); File rootDir = new File(root); if(!rootDir.isDirectory()){ System.out.println("您输入的路径错误!程序直接退出"); return; } System.out.println("请输入要查找的文件名中包含的关键词:"); String token = scanner.next(); //2.递归的遍历目录 //result 表示递归遍历的结果 .就包含所有带有token 关键词的文件名 List<File> result = new ArrayList<>(); sanDir(rootDir,token,result); //3.遍历result 问用户是否要删除该文件,根据用户的输入决定是否删除 for (File f: result) { System.out.println(f.getCanonicalPath() + " 是否要删除? (Y/n)"); String input = scanner.next(); if(input.equals("Y")){ f.delete(); } } } //递归的来遍历目录,找出里面所有符合条件的文件. private static void sanDir(File rootDir, String token, List<File> result) throws IOException { //list 返回的是一个文件名(String),使用listFiles直接得到的是File对象,用起来更方便一些 File[] files = rootDir.listFiles(); if (files == null || files.length == 0){ //当前是一个空目录 return; } for ( File s: files) { if (s.isDirectory()){ //如果当前的文件是一个目录,就递归的进行查找 sanDir(s,token,result); }else { //如果当前的文件是一个普通的文件,就判定这个文件是否包含了带查找的关键字 if(s.getName().contains(token)){ result.add(s.getCanonicalFile()); } } } }
案例2:复制一个文件
启动程序之后,让用户输入一个文件的路径(绝对路径)要求这个文件是一个普通文件,不是目录.
然后再指定一个要复制过去的目标目录
通过程序,把这个文件给进行复制
public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入要复制的文件:(绝对路径)"); String srcPath = scanner.next(); File srcFile = new File(srcPath); if(!srcFile.isFile()){ System.out.println("文件路径错误!程序直接退出"); return; } System.out.println("请输入要复制的目标路径:(绝对路径)"); String destPath = scanner.next(); //要求这个destFile 必须不能存在 File destFile = new File(destPath); if(destFile.exists()){ System.out.println("目标路径已经存在!程序直接退出"); return; } if(!destFile.getParentFile().exists()){ //说明父级目录不存在,也提示一个报错,也可以不存在就创建出来,使用mkdirs 就能创建 System.out.println("目标文件的父目录不存在!程序直接退出"); return; } //具体进行复制操作 //复制就是打开待复制的文件,读取出每个字节,然后再把这些字节给写入到目标的文件中 try(InputStream inputStream = new FileInputStream(srcFile); OutputStream outputStream = new FileOutputStream(destFile)){ //从inputStream中按照字节来读,然后把结果写入到outputStream中 while(true){ byte[] buffer = new byte[1024]; int len = inputStream.read(buffer); if(len == -1){ break; } outputStream.write(buffer,0,len); } //如果这里不加flush,触发close操作,也会自动刷新缓冲区 outputStream.flush(); }catch (IOException e){ e.printStackTrace(); } System.out.println("复制完成!"); }
啥样的类可以放到这个try()中呢?
Closeable接口
实现了这个interface的类,就可以放到try中.
如果我们自己也想写一个类,让这个类能够放到try(),就实现这个Closeable接口即可