Java IO流:字节流实战与性能优化

二、IO流(字节流)

2.1 IO流概述

各位小伙伴,在前面我们已经学习过File类。但是我们知道File只能操作文件,但是不能操作文件中的内容。我们也学习了字符集,不同的字符集存字符数据的原理是不一样的。有了前面两个知识的基础,接下来我们再学习IO流,就可以对文件中的数据进行操作了。

IO流的作用:就是可以对文件或者网络中的数据进行读、写的操作。如下图所示

  • 把数据从磁盘、网络中读取到程序中来,用到的是输入流。

  • 把程序中的数据写入磁盘、网络中,用到的是输出流。

  • 简单记:输入流(读数据)、输出流(写数据)

IO流在Java中有很多种,不同的流来干不同的事情。

Java把各种流用不同的类来表示,这些流的分类和继承体系如下图所示:

复制代码
IO流分为两大派系:
    1.字节流:字节流又分为字节输入流、字节输出流
    2.字符流:字符流由分为字符输入流、字符输出流
复制代码
总结流的四大类:
    字节输入流:以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到内存中去的流
    字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流。
    字符输入流:以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流。
    字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络介质中去的流。
复制代码
总结:
1. IO流的作用?
    读写文件数据的
​
2. IO流是怎么划分的,大体分为几类,各自的作用?
    字节输入流 InputStream(读字节数据的)
    字节输出流 OutputStream(写字节数据出去的)
    字符输入流 Reader(读字符数据的)
    字符输出流 Writer(写字符数据出去的)

2.2 FileInputStream读取一个字节

同学们,在上节课认识了什么是IO流,接下来我们学习字节流中的字节输入流,用InputStream来表示。但是InputStream是抽象类,因为抽象类不能被实例化对象,我们用的是它的子类,叫FileInputStream。

需要用到的方法如下图所示:有构造方法、成员方法

使用FileInputStream读取文件中的字节数据,步骤如下

复制代码
第一步:创建FileInputStream文件字节输入流管道,与源文件接通。
第二步:调用read()方法开始读取文件的字节数据(一个字节一个字节读取)。
第三步:调用close()方法释放资源

代码如下:

复制代码
/**
 * 目标:掌握文件字节输入流,每次读取一个字节。
 */
public class FileInputStreamTest1 {
    public static void main(String[] args) throws Exception {
        // 创建文件字节输入流管道, 与源文件接通
        // InputStream is = new FileInputStream(new File("day11_io\\src\\sy.txt"));
        // 简化写法(推荐使用)
        InputStream is = new FileInputStream(("day11_io\\src\\sy.txt"));
        // 开始读取文件的字节数据
        // read() 每次读取一个字节返回,如果没有数据了,返回-1.
        /*int b1 = is.read(); // 读第一个字节
        System.out.println((char) b1); // 转字符
​
        int b2 = is.read(); // 读第二个字节
        System.out.println((char) b2);
​
        int b3 = is.read(); // 如果没有数据了,返回-1
        System.out.println(b3);*/
​
        // 如果文件中数据太大, 以上代码实现比较臃肿, 可以使用循环来读取
        int b; // 用于记住读取的字节
        while ((b = is.read()) != -1){
            System.out.print((char) b);
        }
​
        /**
         * 注意事项:
         * 1. 读取数据的性能很差
         * 2. 读取汉字输出会乱码, 无法避免的
         * 3. 流使用完毕之后,必须关闭!释放系统资源!
         */
        is.close();
    }
}
复制代码
总结:
这里需要注意三个问题:
    1. 此方式在读取数据的性能很差,因为每次读取一个字节,读取文件的字节数据时,文件是在硬盘中,需要去硬盘中找文件读取,由于程序在内存中执行的,是不能直接去找硬盘中的文件,每次读取文件的字节时,程序实际是调用相关的硬件资源去读取硬盘文件的数据,程序调用硬件资源的开销以及去硬盘中读取数据的性能,相对内存来说是很慢的,因此在开发中让程序尽量减少调用硬件相关的资源,读取数据的频次,这种方案每次读取一个字节,假如文件中有十万个字节,一百万个字节,就意味着程序要调用十万次硬件相关资源,性能肯定会很差的。
    2. 由于一个中文在UTF-8编码方案中是占3个字节,采用一次读取一个字节的方式,读一个字节就相当于读了1/3个汉字,此时将这个字节转换为字符,是会有乱码的。
    3. 释放资源

2.3 FileInputStream读取多个字节

各位同学,在上一节我们学习了FileInputStream调用read()方法,可以一次读取一个字节。但是这种读取方式效率太太太太慢了。 为了提高效率,我们可以使用另一个read(byte\[\] bytes)的重载方法,可以一次读取多个字节,至于一次读多少个字节,就在于你传递的数组有多大。

使用FileInputStream一次读取多个字节的步骤如下

复制代码
第一步:创建FileInputStream文件字节输入流管道,与源文件接通。
第二步:调用read(byte[] bytes)方法开始读取文件的字节数据。
第三步:调用close()方法释放资源

代码如下:

复制代码
/**
 * 目标:掌握使用FileInputStream每次读取多个字节。
 */
public class FileInputStreamTest2 {
    public static void main(String[] args) throws Exception {
        // 创建一个字节输入流对象代表字节输入流管道与源文件接通。
        InputStream is = new FileInputStream("day11_io\\src\\sy01.txt");
​
        // 开始读取文件的字节数据: 每次读取多个字节
        // 每次读取多个字节到字节数组中去,返回读取的字节数量,读取完毕会返回-1.
        /*byte[] buffer = new byte[3];
        int len1 = is.read(buffer);
        String str1 = new String(buffer);
        System.out.println(str1);
        System.out.println("当次读取的字节数量: " + len1);
​
        *//**
         * buffer = [abc]
         * buffer = [66c]
         *//*
        int len2 = is.read(buffer);
        // String str2 = new String(buffer);
        // 注意: 读取多少, 倒出多少  buffer[66]
        String str2 = new String(buffer, 0, len2);
        System.out.println(str2);
        System.out.println("当次读取的字节数量: " + len2);
​
        int len3 = is.read(buffer);
        System.out.println(len3); // -1*/
​
        // 使用循环改造
        byte[] buffer = new byte[3];
        int len; // 记住每次读取了多少个字节
        while ((len = is.read(buffer)) != -1){
            // 注意: 读取多少, 倒出多少
            String str = new String(buffer, 0, len);
            System.out.print(str);
        }
​
        // 释放资源
        is.close();
​
        /**
         * 性能得到了明显的提升!!
         * 这种方案也不能避免读取汉字输出乱码的问题!!
         */
    }
}
  • 需要我们注意的是:read(byte\[\] bytes)它的返回值,表示当前这一次读取的字节个数。

假设有一个a.txt文件如下:

复制代码
abc66

每次读取过程如下:

复制代码
也就是说,并不是每次读取的时候都把数组装满,比如数组是 byte[] bytes = new byte[3];
第一次调用read(bytes)读取了3个字节(分别是97,98,99),并且往数组中存,此时返回值就是3
第二次调用read(bytes)读取了2个字节(分别是99,100),并且往数组中存,此时返回值是2
第三次调用read(bytes)文件中后面已经没有数据了,此时返回值为-1
  • 还需要注意一个问题:采用一次读取多个字节的方式,也是可能有乱码的。因为也有可能读取到半个汉字的情况。
复制代码
问题: 每次读取一个字节数组有什么好处? 存在什么问题?
    读取的性能得到了提升
    读取中文字符输出无法避免乱码问题。

2.4 FileInputStream读取全部字节

同学们,前面我们到的读取方式,不管是一次读取一个字节,还是一次读取多个字节,都有可能有乱码。那么接下来我们介绍一种,不出现乱码的读取方式。

我们可以一次性读取文件中的全部字节,然后把全部字节转换为一个字符串,就不会有乱码了。

复制代码
public class FileInputStreamTest3 {
    public static void main(String[] args) throws Exception {
        // 一次性读取完文件的全部字节到一个字节数组中去。
        // 创建一个字节输入流管道与源文件接通
        InputStream is = new FileInputStream("day11_io\\src\\sy02.txt");
​
        // 准备一个字节数组,大小与文件的大小正好一样大。
        File f = new File("day11_io\\src\\sy02.txt");
        long size = f.length();
        byte[] buffer = new byte[(int) size];
​
        // 读取文件信息
        int len = is.read(buffer);
        System.out.println(new String(buffer));
​
        // 打印文件大小和文件的字节信息
        System.out.println(size);
        System.out.println(len);
​
        // 关闭流
        is.close();
    }
}
复制代码
public class FileInputStreamTest4 {
    public static void main(String[] args) throws Exception {
        // 一次性读取完文件的全部字节到一个字节数组中去。
        // 创建一个字节输入流管道与源文件接通
        InputStream is = new FileInputStream("day11_io\\src\\sy02.txt");
​
        // 调用方法读取所有字节,返回一个存储所有字节的字节数组。
        byte[] buffer = is.readAllBytes();
        System.out.println(new String(buffer));
​
        // 关闭流
        is.close();
    }
} 

最后,还是要注意一个问题:一次读取所有字节虽然可以解决乱码问题,但是文件不能过大,如果文件过大,可能导致内存溢出。

复制代码
总结:
1. 如何使用字节输入流读取中文内容输出时不乱码呢?
    一次性读取完全部字节。
    可以定义与文件一样大的字节数组读取,也可以使用官方API.
2. 直接把文件数据全部读取到一个字节数组可以避免乱码,是否存在问题?
    如果文件过大,定义的字节数组可能引起内存溢出。

2.5 FileOutputStream写字节

各位同学,前面我们学习了使用FileInputStream读取文件中的字节数据。然后有同学就迫不及待的想学习往文件中写入数据了。

往文件中写数据需要用到OutputStream下面的一个子类FileOutputStream。写输入的流程如下图所示

使用FileOutputStream往文件中写数据的步骤如下:

复制代码
第一步:创建FileOutputStream文件字节输出流管道,与目标文件接通。
第二步:调用wirte()方法往文件中写数据
第三步:调用close()方法释放资源

代码如下:

复制代码
/**
 * 目标:掌握文件字节输出流FileOutputStream的使用。
 */
public class FileOutputStreamTest1 {
    public static void main(String[] args) throws Exception {
        // 创建一个字节输出流管道与目标文件接通。
        // 覆盖管道:覆盖之前的数据
        // OutputStream os = new FileOutputStream("day11_io\\src\\sy03.txt.txt");
​
        // 追加数据的管道
        OutputStream os = new FileOutputStream("day11_io\\src\\sy03.txt", true);
​
        // 2、开始写字节数据出去了
        os.write(97); // 97就是一个字节,代表a
        os.write('b'); // 'b'也是一个字节
        // os.write('东'); // [ooo] 默认只能写出去一个字节
​
        byte[] bytes = "我爱你中国abc".getBytes();
        os.write(bytes);
​
        os.write(bytes, 0, 15);
​
        // 换行符
        os.write("\r\n".getBytes());
​
        os.close(); // 关闭流
    }
}

2.6 字节流复制文件

同学们,我们在前面已经把字节输入流和字节输出流都学习完了。

现在我们就可以用这两种流配合起来使用,做一个文件复制的综合案例。

比如:我们要复制一张图片,从磁盘D:/resource/meinv.png的一个位置,复制到C:/data/meinv.png位置。

复制文件的思路如下图所示:

复制代码
1.需要创建一个FileInputStream流与源文件接通,创建FileOutputStream与目标文件接通
2.然后创建一个数组,使用FileInputStream每次读取一个字节数组的数据,存如数组中
3.然后再使用FileOutputStream把字节数组中的有效元素,写入到目标文件中

代码如下:

复制代码
/**
 * 目标:使用字节流完成对文件的复制操作。
 */
public class CopyDemo {
    public static void main(String[] args) throws Exception {
        // 需求:复制照片。
        // 1. 创建一个字节输入流管道与源文件接通
        InputStream is = new FileInputStream("D:/resource/meinv.png");
        // 2. 创建一个字节输出流管道与目标文件接通。
        OutputStream os = new FileOutputStream("E:/data/meinv.png");
​
        // 3. 创建一个字节数组,负责转移字节数据。
        byte[] buffer = new byte[1024]; // 1KB.
        // 4. 从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
        int len; // 记住每次读取了多少个字节。
        while ((len = is.read(buffer)) != -1){
            os.write(buffer, 0, len);
        }
        
        // 5. 释放资源
        os.close();
        is.close();
        System.out.println("复制完成!!");
    }
}
复制代码
字节流非常适合做一切文件的复制操作:
    任何文件的底层都是字节,字节流做复制,是一字不漏的转移完全部字节,只要复制后的文件格式一致就没问题!

三、IO流资源释放

各位同学,上面我们已经学习了字节流,也给同学们强调过,流使用完之后一定要释放资源。如下图一旦程序出现问题输入流和输出流得不到释放,这样系统的性能会受到一定的影响,所以我们之前的代码并不是很专业。

我们现在知道这个问题了,那这个问题怎么解决呢? 在JDK7以前,和JDK7以后分别给出了不同的处理方案。

3.1 JDK7以前的资源释放

在JDK7版本以前,我们可以使用try...catch...finally语句来处理。格式如下

复制代码
try{
    // 有可能产生异常的代码
}catch(异常类 e){
    // 处理异常的代码
}finally{
    // 释放资源的代码
    // finally里面的代码有一个特点,无论try中的程序是正常执行了,还是出现了异常,最后都一定会执行finally区,除非JVM终止。
    // 作用:一般用于在程序执行完成后进行资源的释放操作(专业级做法)
}

目标对finally的认识:

复制代码
public class FinallyTest {
    public static void main(String[] args) {
        try {
            System.out.println(10 / 2);
            // System.out.println(10 / 0);
            // return; // 跳出方法的执行 finally也会执行
            // System.exit(0); // 只要终止虚拟机finally就不再执行了
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("-------finally-------");
        }
        // 思考: 如果不添加finally 放在try...catch...外边是否可以呢?   一旦出现return就不行了
        // System.out.println("-------Finally-------");
​
        System.out.println(chu(10, 5));
    }
​
    public static int chu(int a, int b){
        try {
            return a / b;
        } catch (Exception e){
            e.printStackTrace();
            return -1; // 代表的是出现异常
        } finally {
            // 千万不要在finally中返回数据
            // return 111;
        }
    }
}

改造上面的代码:

复制代码
public class CopyDemo1 {
    public static void main(String[] args) {
        // 需求:复制照片。
        InputStream is = null;
        OutputStream os = null;
        try {
            // 1、创建一个字节输入流管道与源文件接通
            is = new FileInputStream("D:/resource/meinv.png");
            // 2、创建一个字节输出流管道与目标文件接通。
            os = new FileOutputStream("E:/data//meinv.png");
​
            System.out.println(10 / 0);
​
            // 3、创建一个字节数组,负责转移字节数据。
            byte[] buffer = new byte[1024];
            // 4、从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
            int len;
            while ((len = is.read(buffer)) != -1){
                os.write(buffer, 0, len);
            }
            System.out.println("复制完成!!");
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            /**
             * 在释放资源时需要考虑两个问题:
             *  1. 在try代码块中是否存在关闭流的情况
             *  2. 在创建流对象时是否存在异常的情况
             */
            try {
                if (os != null) os.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

代码写到这里,有很多同学就已经看不下去了。是的,我也看不下去,本来几行代码就写完了的,加上try...catch...finally之后代码多了十几行,而且阅读性并不高。难受....

3.2 JDK7以后的资源释放

刚才很多同学已经发现了try...catch...finally处理异常,并释放资源代码比较繁琐,臃肿,不优雅。

Java在JDK7版本为我们提供了一种简化的释放资源的操作,它会自动释放资源。代码写起来也相当简单。

格式如下:

复制代码
try(资源对象1; 资源对象2;){
    可能出现异常的代码;
}catch(异常类 e){
    处理异常的代码
}
() 中只能放置资源,否则报错
    
什么是资源呢?
    资源一般指的是最终实现了AutoCloseable接口。
public abstract class InputStream implements Closeable{ }
public abstract class OutputStream implements Closeable, Flushable { }
public interface Closeable extends AutoCloseable { }
​
//注意:这里没有释放资源的代码。该资源使用完毕后,会自动调用其close()方法,完成对资源的释放!

代码如下:

复制代码
/**
 * 目标:掌握释放资源的方式:try-with-resource
 */
public class CopyDemo2 {
    public static void main(String[] args) {
        // 需求:复制照片。
        try (
                // 1、创建一个字节输入流管道与源文件接通
                InputStream is = new FileInputStream("D:/resource/meinv.png");
                // 2、创建一个字节输出流管道与目标文件接通。
                OutputStream os = new FileOutputStream("E:/data//meinv.png");
            ){
            // 3、创建一个字节数组,负责转移字节数据。
            byte[] buffer = new byte[1024];
            // 4、从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
            int len; // 记住每次读取了多少个字节。
            while ((len = is.read(buffer)) != -1){
                os.write(buffer, 0, len);
            }
            System.out.println("复制完成!!");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

四、字符流

同学们,我们学习了字节流,使用字节流可以读取文件中的字节数据。但是如果文件中有中文,使用字节流来读取,就有可能读到半个汉字的情况,这样会导致乱码。虽然使用读取全部字节的方法不会出现乱码,但是如果文件过大又不太合适。

所以Java专门为我们提供了另外一种流,叫字符流,字符流是专门为读取文本数据而生的。

4.1 FileReader类

先类学习字符流中的FileReader类,这是字符输入流,用来将文件中的字符数据读取到程序中来。

FileReader读取文件的步骤如下:

复制代码
第一步:创建FileReader对象与要读取的源文件接通
第二步:调用read()方法读取文件中的字符
第三步:调用close()方法关闭流

需要用到的方法:先通过构造器创建对象,再通过read方法读取数据(注意:两个read方法的返回值,含义不一样

复制代码
/**
 * 目标:掌握文件字符输入流。
 */
public class FileReaderTest {
    public static void main(String[] args) {
        try (
             // 1、创建一个文件字符输入流管道与源文件接通
             Reader fr = new FileReader("day11_io\\src\\sy01.txt");
        ){
            // 2、一个字符一个字符的读(性能较差)
            /*int len; // 记住每次读取的字符编号。
            while ((len = fr.read()) != -1){
                System.out.print((char) len);
            }*/
​
            /**
             * 每次读取一个字符的形式,性能肯定是比较差的。
             * 3、每次读取多个字符。(性能是比较不错的!)
             */
            char[] buffer = new char[3];
            int len; // 记住每次读取了多少个字符。
            while ((len = fr.read(buffer)) != -1){
                // 读取多少倒出多少
                String str = new String(buffer, 0, len);
                System.out.print(str);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.2 FileWriter类

我们学习了FileReader,它可以将文件中的字符数据读取到程序中来。接下来,我们就要学习FileWriter了,它可以将程序中的字符数据写入文件。

FileWriter往文件中写字符数据的步骤如下:

复制代码
第一步:创建FileWirter对象与要读取的目标文件接通
第二步:调用write(字符数据/字符数组/字符串)方法读取文件中的字符
第三步:调用close()方法关闭流

需要用到的方法如下:构造器是用来创建FileWriter对象的,有了对象才能调用write方法写数据到文件。

接下来,用代码演示一下:

复制代码
/**
 * 目标:掌握文件字符输出流:写字符数据出去
 */
public class FileWriterTest {
    public static void main(String[] args) {
        try (
            // 0、创建一个文件字符输出流管道与目标文件接通。
            // 覆盖管道
            //Writer fw = new FileWriter("day11_io\\src\\sy02.txt");
            // 追加数据的管道
            Writer fw = new FileWriter("day11_io\\src\\sy02.txt", true);
        ){
            // 1、写一个字符出去
            fw.write('a');
            fw.write(97);
            //fw.write('东'); // 写一个字符出去
            fw.write("\r\n"); // 换行
​
            // 2、写一个字符串出去
            fw.write("我爱你中国abc");
            fw.write("\r\n");
​
            // 3、写字符串的一部分出去
            fw.write("我爱你中国abc", 0, 5);
            fw.write("\r\n");
​
            // 4、写一个字符数组出去
            char[] buffer = {'胜', '雅', 'a', 'b', 'c'};
            fw.write(buffer);
            fw.write("\r\n");
​
            // 5、写字符数组的一部分出去
            fw.write(buffer, 0, 2);
            fw.write("\r\n");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.3 FileWriter写的注意事项

刚才我们已经学习了FileWriter字符输出流的基本使用。但是,这里有一个小问题需要和同学们说下一:FileWriter写完数据之后,必须刷新或者关闭,写出去的数据才能生效。

比如:下面的代码只调用了写数据的方法,没有关流的方法。当你打开目标文件时,是看不到任何数据的。

复制代码
public class FileWriterTest2 {
    public static void main(String[] args) throws Exception {
​
        // 1. 创建一个文件字符输出流管道与目标文件接通。
        Writer fw = new FileWriter("day11_io\\src\\sy03.txt");
​
        // 2. 写字符出去
        fw.write('a');
        fw.write('b');
        fw.write('c');
        fw.write('d');
        fw.write("\r\n"); // 换行
​
        fw.write("我爱你中国abc");
        fw.write("\r\n");
​
        fw.write("我爱你中国abc", 0, 5);
        fw.write("\r\n");
    }
}

而下面的代码,加上了flush()方法之后,数据就会立即到目标文件中去。

复制代码
public class FileWriterTest3 {
    public static void main(String[] args) throws Exception {
​
        // 1. 创建一个文件字符输出流管道与目标文件接通。
        Writer fw = new FileWriter("day11_io\\src\\sy03.txt");
​
        // 2. 写字符出去
        fw.write('a');
        fw.write('b');
        fw.write('c');
        fw.write('d');
        fw.write("\r\n"); // 换行
​
        fw.write("我爱你中国abc");
        fw.write("\r\n");
​
        fw.write("我爱你中国abc", 0, 5);
        fw.write("\r\n");
​
        // 3. 刷新   每写入数据时都要进行刷新
        fw.flush();
        fw.write("柳岩");
        fw.write("\r\n");
        fw.flush();
    }
}

下面的代码,调用了close()方法,数据也会立即到文件中去。因为close()方法在关闭流之前,会将内存中缓存的数据先刷新到文件,再关流。

复制代码
public class FileWriterTest4 {
    public static void main(String[] args) throws Exception {
​
        // 1. 创建一个文件字符输出流管道与目标文件接通。
        Writer fw = new FileWriter("day11_io\\src\\sy03.txt");
​
        // 2. 写字符出去
        fw.write('a');
        fw.write('b');
        fw.write('c');
        fw.write('d');
        fw.write("\r\n"); // 换行
​
        fw.write("我爱你中国abc");
        fw.write("\r\n");
​
        fw.write("我爱你中国abc", 0, 5);
        fw.write("\r\n");
​
        // 3. 刷新
        // fw.flush();
        
        // 或者关闭流  关闭流是包含刷新操作
        fw.close();
        
        // 注意: 如果关闭流后再写入数据就会报错
        // 可能会有同学问: 如果缓存区的内存不够怎么办? 之前写的数据会自动同步到文件中去
    }
}

但是需要注意的是,关闭流之后,就不能在对流进行操作了。否则会出异常

相关推荐
糯米团子7491 小时前
javascript高频知识点
开发语言·前端·javascript
January12071 小时前
IDEA 快捷键
java·ide·intellij-idea
周杰伦fans1 小时前
C# 异常继承深度解析:从设计原则到 sealed 关键字的奥秘
java·jvm·c#
搬石头的马农1 小时前
从零配置Claude自动修Bug:6步打造全自动开发流程
java·人工智能·python·bug·ai编程
小马爱打代码1 小时前
Redis Key 过期后会立刻删除吗?过期删除与内存淘汰策略详解
java·redis·缓存
Wonderful U1 小时前
基于Python+Django的在线题库与智能阅卷系统:从痛点分析到完整实现
开发语言·python·django
码语智行1 小时前
拦截器、接口限流、过滤器、防重发/幂等性功能说明
开发语言·网络·python
liulilittle1 小时前
麻将牌堆渲染(Lua)
开发语言·lua
鱼鳞_1 小时前
苍穹外卖-Day10(Spring task)
java·后端·spring