目录

有关Java中的IO1-字节流和File类

有关Java中的IO(1) –字节流和File类

学习目标

● 掌握常用的File类常用的方法

● 掌握字节字符流的基本使用方法

1.File

1.1为什么要了解File

● 因为数据很重要所以我们要把数据永久化/持久化存储。

● 之前开发都把数据存入了 内存

● 存储内存
优势
性能快

弊端 : 程序结束,数据消失了。不能保证数据的持久化

● 数据只有存在计算机磁盘/缓存中 将数据持久化。

● 我们有必要学习对目录以及文件的相关操作。

1.2 File的概念

● File类位于java.io包下,表示文件或者目录,也就是说如果希望在程序中操作文件或者目录都可以通过File类来完成;

● 一个File对象代表硬盘或网络中可能存在/或者不存在的一个文件或者目录

● File类能做到新建、删除、重命名文件或者目录;

1.3 层级

public class File extends Object  implements Serializable, Comparable<File>{
    
}
//代表磁盘/网络里面任意一个存在/不存在的文件/文件夹的抽象标识

1.4 常用构造

有关文件路径

● window的路径分隔符使用“\”,而Java中的“\”表示转义字符,所以在Windows中表示路径,需要用两个反斜杠。

或者直接使用“/”也可以,Java程序支持将“/”当成平台无关的路径分隔符。或者直接使用File.separator常量值表示;

● 绝对路径:从盘符开始的路径,这是一个完整的路径;例如 “E:\java\io\resources\a.txt” 就是个绝对路径;

● 相对路径:相对于项目目录的路径,一种简洁的写法,开发中经常使用。

1. File(String pathname);//根据指定文件路径/目录路径(路径可以存在 也可以不存在)创建File类对象

2. File(String parent, String child);//根据指定的父级目录路径以及指定的子级文件/目录路径创建File类对象
  //1. parent 父级目录路径
  //2. child 子级文件/目录路径
3. File(File parent, String child);
方法描述
public File(String pathname)根据给定的 路径名字符串 创建新的 File实例。
public File(String parent, String child)根据 父路径名字符串和子路径名字符串创建新的 File实例。
public File(File parent, String child)从父路径对应的File对象和子路径名字符串 创建新的 File实例。
private static void demo2() {
    File file = new File("D:\\demo\\a.txt");
    File file1 = new File("d:\\demo", "a.txt");

    File parentDirectory = new File("D:\\demo");
    File file2 = new File(parentDirectory, "a.txt");


    File file3 = new File("/a");// E:\\
    File file4 = new File("src/com/java/file/FileDemo.java");// E:\\
 }

1.5 常用属性

static String pathSeparator  
static char pathSeparatorChar
    //代表的是磁盘中系统环境变量的分隔符。windows: ;  unix: :
    
static String separator  ;
static char separatorChar ;
    //代表路径盘符分割  windows: \   unix: /
    //路径拼接 
private static void demo1() {
    System.out.println(File.pathSeparator);
    System.out.println(File.pathSeparatorChar);

    System.out.println(File.separator);
    System.out.println(File.separatorChar);// \
}

1.6 常用方法

1.6.1 作为文件

方法描述
public String getName()返回由此File表示的文件名称。
public long length()返回由此File表示的文件的长度。 如果此路径名表示一个目录,则返回值是不确定的。
public long lastModified()返回File对象对应的文件或目录的最后修改时间(毫秒值)。
public boolean exists()此File表示的文件是否实际存在。
public boolean isFile()此File表示的是否为文件。
public boolean isHidden()此File表示的是否为隐藏文件。
public boolean canExecute()测试应用程序是否可以执行此抽象路径名表示的文件。
public boolean canRead()测试应用程序是否可以读取此抽象路径名表示的文件。
public boolean canWrite()测试应用程序是否可以修改此抽象路径名表示的文件。
public boolean delete()删除指定文件
public boolean setLastModified(long time)修改最后修改文件的时间
public boolean setReadOnly()修改文件为只读
public boolean createNewFile()当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。(一定要保证父级目录存在)
public boolean renameTo(File dest)重新命名此抽象路径名表示的文件或目录。但是此方法行为的许多方面都是与平台有关的:重命名操作无法将一个文件从一个文件系统移动到另一个文件系统。
public String getParent()返回此抽象路径名父目录的路径名字符串
public File getParentFile()返回此抽象路径名父目录的抽象路径名
public String getAbsolutePath()返回文件的绝对路径
public String getPath()返回文件的相对路径
private static void demo3() {
    //当File代表的是文件的时候
    File file = new File("D:\\demo\\a.txt");
    System.out.println(file);
    //获得文件/目录的相关属性的信息
    System.out.println("文件名称:" + file.getName());
    System.out.println("相对路径:" + file.getPath());
    System.out.println("绝对路径:" + file.getAbsolutePath());
    System.out.println("获得父级目录路径:" + file.getParent());
    System.out.println("获得父级目录对象:" + file.getParentFile());
    System.out.println("获得文件大小(字节):" + file.length());
    System.out.println("获得上一次操作文件的时间:" + file.lastModified());//毫秒数
    System.out.println("获得上一次操作文件的时间:" + new Date(file.lastModified()));
    // 将毫秒数转换成具体的日期类对象  Date  LocalDate  LocalDateTime

    //判断
    System.out.println("判断文件是否存在:" + file.exists());
    //手动创建文件
    //System.out.println(file.createNewFile());// 1. try...catch...finally  2. throws

    System.out.println(file.isFile());
    System.out.println(file.isDirectory());

    System.out.println(file.canRead());
    System.out.println(file.isHidden());
    System.out.println(file.setReadOnly());//更改为只读文件

    //删除文件
    System.out.println(file.delete());
}
private static void demo5() {
    File file = new File("./src/user.txt");//相对于当前的项目  day17
    System.out.println(file.exists());
    System.out.println(file.getAbsolutePath());
    try {
        file.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

1.6.2 作为目录

方法描述
public String getName()返回由此File表示的目录名称。
public boolean exists()此File表示的目录是否实际存在。
public boolean isDirectory()判断File对象是否是目录
public boolean mkdir()创建由此File表示的目录。
public boolean mkdirs()创建多级目录
public File getParentFile()返回此抽象路径名父目录的抽象路径名
public String getAbsolutePath()返回目录的绝对路径
public String getPath()返回目录的相对路径
public String[] list()获得File目录中的一级子文件名称或目录名称。
public File[] listFiles()获得File目录中的一级子文件对象或目录对象。
public File[] listFiles(FileFilter filter)返回所有满足指定过滤器的文件和目录。如果给定 filter 为 null,则接受所有路径名。否则,当且仅当在路径名上调用过滤器的 FileFilter.accept(File pathname)方法返回 true 时,该路径名才满足过滤器。
private static void demo6() {
    //File 代表目录-----> 目录路径
    File directory = new File("/demo", "b/c/d/e");
    System.out.println(directory.getName());
    System.out.println(directory.getPath());
    System.out.println(directory.getAbsolutePath());
    System.out.println(directory.getParent());

    //目录不存在  要创建一个目录
    if (!directory.exists()) {
        //System.out.println(directory.mkdir());
        System.out.println(directory.mkdirs());//创建>=1目录
    }


    System.out.println(directory.isDirectory());
    //System.out.println(directory.exists());
    //System.out.println(directory.delete());  只能删除空目录
}
private static void demo7() {
    //一般创建的目录都是以当前时间为准
    //获得当前此刻的时间: 目录名称 
    String dateStr = LocalDate.now().toString();
    File dir = new File("src" + File.separator + dateStr);
    System.out.println(dir.mkdirs());
}
private static void demo8() {
    File dir = new File("E:\\workspace\\JavaTest");
    //查询展示指定目录下的所有的子级资源
    //删除dir里面后缀是txt所有的文件----> file.delete()
    String[] list = dir.list();//获得1级资源名称(目录名/文件名)
    System.out.println(Arrays.toString(list));
    File[] files = dir.listFiles();//获得1级资源对象(目录对象/文件对象)
    System.out.println(Arrays.toString(files));
}

1.7 递归-斐波那契数列

● 什么是方法的递归?

● 方法的递归是指在一个方法的内部调用自身的过程。 递归必须要有结束条件,不然就会陷入无限递归的状态,永远无法结束调用;

public static void main(String[] args) {
    File dir = new File("E:\\workspace\\JavaTest\\demo");
    //System.out.println(dir.delete());
    //删除目录 只能删除空目录
    deleteFileTxt(dir);
}
  • 案例: 删除指定目录所有的文件的txt资源
private static void deleteFileTxt(File parentDirectory) {
   //删除的文件后缀是txt
   for (File child : childFiles) {
        //child 可能是子级目录  也可能是子级文件
        if (child.isFile()) {
            //if (child.getName().endsWith("txt")) child.delete();
        } else {
            //是目录
            //继续查询子集  继续执行删除txt
            //递归
            deleteFileTxt(child);
        }
    }
}

public static void main(String[] args) {
    File dir = new File("E:\\workspace\\day17\\demo");
    listChild(dir,"|-");
}

private static void listChild(File dir, String s) {
    String[] childNameArray = dir.list();
    for (String name : childNameArray) {
        System.out.println(s + name);
        //如果是目录 需要再次递归
        //创建一个File 作为目录对象
        File child = new File(dir, name);
        if (child.isDirectory()) listChild(child, "| " + s);
    }
}

2.IO

2.1 为什么要学习IO?

● 由于我们在开发中,需要将数据持久化存储。必须存储磁盘或者缓存进行维护,因此我们学习了File类相关功能。

● 但是File类只能获得文件或者目录相关的属性,不能读写文件存储的数据。

● 数据持久存储,就离不开2个功能: 1.将数据存储文件 2.获得文件数据

● 也就是要实现读写功能,这个时候必须使用IO实现。

2.2 概念

I input 输入 O output 输出

● 数据的传输,可以看做是一种数据的流动,按照流动的方向,分为输入input 和输出output ,即流向Java程序的是输入流,流出Java程序的是输出流;

● Java中I/O操作主要是指使用java.io包下的类与接口进行输入、输出操作。

● 输入也叫做读取数据,输出也叫做作写出数据。

● File对象不能直接读取和写入数据,如果要操作数据,需要IO流。

● File对象好比是到水库的“路线地址”以及水库的描述信息(包括水库的大小、位置、建造时间等),要“存取”里面的水到你“家里”,需要“管道”,IO流就好比是管道。

2.3 流的分类:

  1. 从传输方向上看:
  • 输入流: 将本地磁盘数据传输至JVM
  • 输出流: 将JVM数据传输到本地磁盘
  1. 从传输单位上看:
  • 字节流: 以字节为单位进行传输, 可以传输任意类型的文件, 如文本|图片|视频|音频等
  • 字符流: 以字符为单位记性传输, 只能传输文本类型的文件, 如.txt | .java | .excel等
  1. 从传输功能上看:
  • 节点流: 是具备实际传输功能的流
  • 过滤流: 不具备实际传输作用, 功能为给节点流添加附加功能|增强节点流传输能力

2.4 有关字节流

● 对流中数据 一个字节一个字节的处理;

● 字节输入流是 InputStream ;字节输出流是OutputStream

● 都是抽象类。封装了所有的字节流里面最基本的读写操作

2.4.1 字节流体系

https://i-blog.csdnimg.cn/direct/2327dda8396c43b6a30268dc90a76f73.png

2.4.2 InputStream

1.层级
public abstract class InputStream extends Object implements Closeable{
    
}

注意 :类实现Closeable 证明他就是一个资源。使用完毕之后 需要释放物理资源

不管程序是否出现了异常 都需要释放物理资源,释放方式有

1.finally

2.try…with…resoucres

3.@CleanUp

2.常用方法
方法描述
public int read()从输入流读取一个字节。返回读取的字节值。虽然读取了一个字节,但是会自动提升为int类型。如果已经到达流末尾,没有数据可读,则返回-1。
public int read(byte[] b)从输入流中读取一些字节数,并将它们存储到字节数组 b中 。每次最多读取b.length个字节。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1。
public int read(byte[] b,int off,int len)从输入流中读取一些字节数,并将它们存储到字节数组 b中,从b[off]开始存储,每次最多读取len个字节 。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1。
public void close()关闭此输入流并释放与此流相关联的任何系统资源。
public byte[] readAllBytes()JDK9中新增的方法;从输入流读取所有字节放到一个字节数组中,返回此字节数组;如果数据过大有可能会发生OOM错误,使用时要注意;
public byte[] readNBytes(int len)JDK11中新增的方法;从输入流总读取指定长度的字节放入字节数组中进行放回;
int available();获得流中有效字节数量
long transferTo(OutputStream out);JDK9新增的方法。将字节流输入内容转换到字节输出流对象中
3.常用子类构造
FileInputStream(String filePath);//name: 文件路径-----> 文件存在
FileInputStream(File file);
BufferedInputStream(InputStream in);
4.使用方法
4.1 read()
private static void testRead1() {
        //目的: 读取指定文件里面的数据。
        //1.指定文件
        String filePath = "src/a.txt";
        InputStream inputStream = null;
        try {
            //2.创建字节输入流对象
            inputStream = new FileInputStream(filePath);//文件的数据都在inputStream
            System.out.println(inputStream.available());
            /*
            //3.读取文件数据
            int data = inputStream.read();//一次读取一个字节 -1
            System.out.println("读取到的1个字节数据:"+(char)data);
            System.out.println(inputStream.available());*/

            //3.读取文件所有的内容
            /*int available = inputStream.available();
            for (int i = 0; i < available; i++) {
                System.out.print((char) inputStream.read());
            }*/
            /*int data = inputStream.read();
            while (data!=-1){
                System.out.print((char)data);
                data = inputStream.read();
            }*/
            int len;
            while ((len = inputStream.read()) != -1) {
                System.out.print((char) len);
            }
            //不管使用哪一个字节流  只要操作中文的数据  肯定会乱码的问题
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null) inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
}
4.2 try…with…resources

● 在上面的read功能里面,使用流结束之后,我们需要释放资源,调用close方法

● 代码里面嵌套了很多try…catch语句,很臃肿。

● 在JDK1.7之后,建议我们使用try…with…resources替换finally代码块

● 一种更加优雅的方式释放流资源对象。

● 想使用try…with…resources 一定要保证资源类型必须实现Closeable接口,否则程序会报错。

public class MyClass implements Closeable {
    @Override
    public void close() throws IOException {
        System.out.println("我被关闭了.....");
    }
}
private static void testRead2() throws FileNotFoundException {
        //使用更加优雅的方式释放流对象  try...with...resources
        //无需再编写finally  自动调用close
        InputStream inputStream = new FileInputStream("src/a.txt");
        MyClass myClass = new MyClass();
        try (inputStream; myClass) {
            //循环读取文件数据
            int len;
            while ((len = inputStream.read()) != -1) {
                System.out.print((char) len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
4.3 read(byte[] bytes)
private static void testRead2() throws FileNotFoundException {
    //1.指定文件路径
    String path = "src/com/java/io/InputStreamDemo.java";
    //2.创建字节输入流对象
    InputStream inputStream = new FileInputStream(path);
    //3.操作流---->读取文件数据
    try (inputStream) {
        /*System.out.println("字节数量:" + inputStream.available());
            byte[] bytes = new byte[1024];//1024的整数倍  理论上length越大 读取的性能高
            int len = inputStream.read(bytes);//读取到的数据都在bytes
            System.out.println("len:" + len);//读到的有效的字节数量  11
            //字节数组转String
            System.out.println(Arrays.toString(bytes));
            String str = new String(bytes,0,len);
            System.out.println(str);*/
        //读取src/com/java/io/InputStreamDemo.java
        //在控制台打印输出
        byte[] bytes = new byte[500];
        int len;
        while ((len = inputStream.read(bytes)) != -1) {
            System.out.println(new String(bytes, 0, len));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

}
4.4 read(byte[] b, int index, int len)
  • 这个方法咱们一般不用, 了解即可
private static void testRead3() throws FileNotFoundException {
    InputStream inputStream = new FileInputStream("src/a1.txt");
    try (inputStream) {
        byte[] bytes = new byte[10];

        // public int read(byte b[], int off, int len)
        //off: index 数组的索引位置
        //len: 等价于read方法的返回值的结果数据  存储数组里面的真实的字节数量
        int read = inputStream.read(bytes, 0, bytes.length);
        System.out.println(Arrays.toString(bytes));
        System.out.println(read);

    } catch (IOException e) {
        e.printStackTrace();
    }

}

2.4.3 OutputStream

1.层级
public abstract class OutputStream extends Object implements Closeable, Flushable{
    //实现Closeable  也是资源  需要释放物理资源
    //实现Flushable  底层有缓冲区  在某些情况下需要调用flush()方法
}
2.常用方法
方法描述
public void write(int b)将指定的字节输出流
public void write(byte[] b)将 b.length字节从指定的字节数组写入此输出流。
public void write(byte[] b, int off, int len)从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
public void flush()刷新此输出流并强制任何缓冲的输出字节被写出。
public void close()关闭此输出流并释放与此流相关联的任何系统资源。
3.常用子类构造
public FileOutputStream(String filePath) ;
public FileOutputStream(File file) 
public FileOutputStream(String name, boolean append) ;// append 追加  默认值false 
public FileOutputStream(File file, boolean append);   

public BufferedOutputStream(OutputStream out) {
        this(out, 8192);
}    
4.使用方法
public static void main(String[] args) throws FileNotFoundException {

        //将指定的数据写入指定文件中
        //对于字节输入流而言 执行read  保证文件必须存在
        //对于字节输出流而言 执行write  文件不必存在(会自动的创建新的文件----> File.createNewFile())
        //前提: 文件的父级目录必须存在

        String path = "src/b.txt";
        //1.创建字节输出流对象
        OutputStream outputStream = new FileOutputStream(path,true);
        try(outputStream){
            //2.将数据写入文件中
            outputStream.write('a');
            outputStream.write(97);
            outputStream.write('我');
            outputStream.write(65);

            outputStream.write('\n');

            byte[] bytes = {98,99,100,102};
            outputStream.write(bytes);
            outputStream.write('\n');

            outputStream.write("你太美".getBytes());
            outputStream.write('\n');
            outputStream.write("你太美".getBytes(),0,2);

        } catch (IOException e) {
            e.printStackTrace();
        }
}

2.3.4 案例: 用户上传头像

  • 思考
在开发中  哪些功能需要使用IO实现?
 //文件上传与下载、文件导入导出等
 //1.选择本地一张图片资源
 //2.将本地图片的资源复制到另外一台计算中的另外一个文件中   ---> 读取本地资源  写入到另外一个文件中
 //3.图片是回显成功的(图片服务器里面的资源) ----> 获得资源存在服务器的路径   

● 一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。

● 所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。

  • 1.基础版
/**
     * 上传文件
     *
     * @param sourceFilePath 原文件路径
     * @return 上传成功之后的服务器路径
     */
public static String uploadImage(String sourceFilePath) throws FileNotFoundException {
    if (sourceFilePath == null || sourceFilePath.isBlank()) {
        throw new RuntimeException("文件路径不合法");
    }
    //读取原文件数据  写入到服务器中目标文件
    InputStream inputStream = new FileInputStream(sourceFilePath);
    //获得原文件的名称
    String fileName = sourceFilePath.substring(sourceFilePath.lastIndexOf(File.separator) + 1);
    String targetFilePath = PARENT_DIRECTORY + fileName;
    OutputStream outputStream = new FileOutputStream(targetFilePath);

    //读写
    try (inputStream; outputStream) {
        int len;
        while ((len = inputStream.read()) != -1) {
            outputStream.write(len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return targetFilePath;
}

2.问题

//以上的上传可以成功。
public static void main(String[] args) throws FileNotFoundException {
        // A用户上传
        //System.out.println(uploadImage("D:\\demo\\a.jpg"));

        //B用户
        System.out.println(uploadImage("E:\\demo\\a.jpg"));

        //存在一些问题
        //1. 用户上传重名的文件 会出现文件被覆盖的问题
        //2. 遇见大文件的读写  性能低
        //3. 用户变多  资源变多  查询一个文件 性能低
}
  • 3.升级版

    1.解决文件覆盖/重名

public static String uploadImage(String sourceFilePath) throws FileNotFoundException {
        if (sourceFilePath == null || sourceFilePath.isBlank()) {
            throw new RuntimeException("文件路径不合法");
        }
        //读取原文件数据  写入到服务器中目标文件
        InputStream inputStream = new FileInputStream(sourceFilePath);
        //获得原文件的名称----> 目前就不使用原文件名称  可能出现重名问题----> 文件就会被覆盖
        //String fileName = sourceFilePath.substring(sourceFilePath.lastIndexOf(File.separator) + 1);
        //使用UUID
        String name = UUID.randomUUID().toString().replaceAll("-", "");
        //获得原文件的后缀(扩展名)
        String extension = sourceFilePath.substring(sourceFilePath.lastIndexOf("."));

        String targetFilePath = PARENT_DIRECTORY + name + extension;//建议使用StringBuilder
        OutputStream outputStream = new FileOutputStream(targetFilePath);

        //读写
        try (inputStream; outputStream) {
            int len;
            while ((len = inputStream.read()) != -1) {
                outputStream.write(len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return targetFilePath;
}

2.解决读写慢(自定义缓冲区)

public static String uploadImage(String sourceFilePath) throws FileNotFoundException {
        if (sourceFilePath == null || sourceFilePath.isBlank()) {
            throw new RuntimeException("文件路径不合法");
        }
        //读取原文件数据  写入到服务器中目标文件
        InputStream inputStream = new FileInputStream(sourceFilePath);
        //获得原文件的名称----> 目前就不使用原文件名称  可能出现重名问题----> 文件就会被覆盖
        //String fileName = sourceFilePath.substring(sourceFilePath.lastIndexOf(File.separator) + 1);
        //使用UUID
        String name = UUID.randomUUID().toString().replaceAll("-", "");
        //获得原文件的后缀(扩展名)
        String extension = sourceFilePath.substring(sourceFilePath.lastIndexOf("."));

        String targetFilePath = PARENT_DIRECTORY + name + extension;//建议使用StringBuilder
        OutputStream outputStream = new FileOutputStream(targetFilePath);

        //读写
       //大文件读写----> 自定义缓冲
        try (inputStream; outputStream) {
            byte[] bytes = new byte[1024 * 20];
            int len;
            while ((len = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return targetFilePath;
    }

● 缓冲流,也叫高效流,按照数据类型分类:

○ 字节缓冲流:BufferedInputStream,BufferedOutputStream

○ 字符缓冲流:BufferedReader,BufferedWriter

● 缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

● 缓冲字节输入输出流:BufferedInputStream/BufferedOutputStream;

public static String upload1(String sourceFilePath) throws FileNotFoundException {
        String name = UUID.randomUUID().toString().replaceAll("-", "");
        String extension = sourceFilePath.substring(sourceFilePath.lastIndexOf("."));
        String targetFilePath = PARENT_DIRECTORY + name + extension;

        // 可以使用高效字节流解决读写慢----> 底层自带缓冲----> 8192  byte[]
        BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(sourceFilePath));
        BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(targetFilePath));
        try (inputStream; outputStream) {
            int len;
            byte[] bytes = new byte[1024 * 20];//自定义的缓冲数组>8192  这是时候 底层的缓冲无效了
            while ((len = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return targetFilePath;
    }

3.多目录存储

//用户变多  资源变多  查询一个文件 性能低
//  解决: 多目录存储
//      1. 可以以当前日期为目录 维护用户在今日上传的图片资源
//      2. 目录打散---> 获得文件名称16进制的hash数据  获得前几个16进制的字符的数据 作为新的目录的名称

//        String name = "a.jpg";
//        String hexString = Integer.toHexString(name.hashCode());
//        System.out.println(hexString);
//        String childPath = hexString.charAt(0) + "/" + hexString.charAt(1);// 5/6

//System.out.println(new Object().toString());
/**
     * 上传文件
     *
     * @param sourceFilePath 原文件路径
     * @return 上传成功之后的服务器路径
     */
public static String uploadImage(String sourceFilePath) throws FileNotFoundException {
    if (sourceFilePath == null || sourceFilePath.isBlank()) {
        throw new RuntimeException("文件路径不合法");
    }
    //读取原文件数据  写入到服务器中目标文件
    InputStream inputStream = new FileInputStream(sourceFilePath);
    //获得原文件的名称----> 目前就不使用原文件名称  可能出现重名问题----> 文件就会被覆盖
    //String fileName = sourceFilePath.substring(sourceFilePath.lastIndexOf(File.separator) + 1);
    //使用UUID
    String name = UUID.randomUUID().toString().replaceAll("-", "");
    //获得原文件的后缀(扩展名)
    String extension = sourceFilePath.substring(sourceFilePath.lastIndexOf("."));

    //upload/user/2022-12-01/a.jpg
    /*String curDateStr = LocalDate.now().toString();
        //目录不存在  自动创建----> File.mkdirs()
        File childDir = new File(PARENT_DIRECTORY, curDateStr);
        if (!childDir.exists()) {
            childDir.mkdirs();
        }*/
    String fileName = sourceFilePath.substring(sourceFilePath.lastIndexOf(File.separator) + 1);
    String hexString = Integer.toHexString(fileName.hashCode());
    String childPath = hexString.charAt(0)+"/"+hexString.charAt(1);
    File childDir = new File(PARENT_DIRECTORY, childPath);
    if (!childDir.exists()) {
        childDir.mkdirs();
    }

    String targetFilePath = PARENT_DIRECTORY + childPath + "/" + name + extension;//建议使用StringBuilder
    OutputStream outputStream = new FileOutputStream(targetFilePath);

    //大文件读写----> 自定义缓冲
    try (inputStream; outputStream) {
        byte[] bytes = new byte[1024 * 20];
        int len;
        while ((len = inputStream.read(bytes)) != -1) {
            outputStream.write(bytes, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

    //小文件读写
    /* try (inputStream; outputStream) {
            int len;
            while ((len = inputStream.read()) != -1) {
                outputStream.write(len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }*/
    return targetFilePath;
}