【Java成王之路】EE初阶第五篇:(文件操作 —— IO) 2

发布于:2023-02-17 ⋅ 阅读:(547) ⋅ 点赞:(0)

上节回顾

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();
        }
    }

PrintWriter
 public 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接口即可


网站公告

今日签到

点亮在社区的每一天
去签到