• 万字解析设计模式之 装饰者模式


    一·、装饰者模式

    1.1概述

    装饰者模式是一种结构型设计模式,它允许在运行时动态地为一个对象添加额外的职责。它以一种透明的方式来扩展对象的功能,而不需要通过子类来实现。在装饰者模式中,有一个基本对象,也称为组件,它可以被一个或多个装饰器包装。装饰器不改变基本对象本身的行为,而是在基本对象的行为之前或之后添加一些额外的行为。这样可以轻松地构建出复杂的对象,而不需要使用大量的子类。

    动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活

    我们先来看一个快餐店的例子。

    快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。

    使用继承的方式存在的问题:

    • 扩展性不好

      如果要再加一种配料(火腿肠),我们就会发现需要给FriedRice和FriedNoodles分别定义一个子类。如果要新增一个快餐品类(炒河粉)的话,就需要定义更多的子类。

    • 产生过多的子类

    1.2结构

    装饰(Decorator)模式中的角色:

    • 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
    • 具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
    • 抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
    • 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

     1.3实现

    我们使用装饰者模式对快餐店案例进行改进,体会装饰者模式的精髓。

    类图如下:

    抽象组件

    1. package com.yanyu.Decorator;
    2. // 定义一个抽象组件类,即快餐接口,包括价格和描述
    3. public abstract class FastFood {
    4. private float price; // 定义价格
    5. private String desc; // 定义描述
    6. public FastFood() {
    7. }
    8. public FastFood(float price, String desc) { // 构造函数,传入价格和描述
    9. this.price = price;
    10. this.desc = desc;
    11. }
    12. public void setPrice(float price) { // 设置价格
    13. this.price = price;
    14. }
    15. public float getPrice() { // 获取价格
    16. return price;
    17. }
    18. public String getDesc() { // 获取描述
    19. return desc;
    20. }
    21. public void setDesc(String desc) { // 设置描述
    22. this.desc = desc;
    23. }
    24. public abstract float cost(); // 获取价格的抽象方法
    25. }

    具体构件;

    1. package com.yanyu.Decorator;
    2. //炒饭
    3. public class FriedRice extends FastFood {
    4. public FriedRice() {
    5. super(10, "炒饭");
    6. }
    7. public float cost() {
    8. return getPrice();
    9. }
    10. }
    1. package com.yanyu.Decorator;
    2. //炒面
    3. public class FriedNoodles extends FastFood {
    4. public FriedNoodles() {
    5. super(12, "炒面");
    6. }
    7. public float cost() {
    8. return getPrice();
    9. }
    10. }

    抽象装饰者

    1. package com.yanyu.Decorator;
    2. // 定义一个抽象装饰者类,即配料类,继承自快餐接口
    3. public abstract class Garnish extends FastFood {
    4. private FastFood fastFood; // 定义一个快餐接口类型的对象,即被装饰的组件
    5. public FastFood getFastFood() { // 获取被装饰的组件
    6. return fastFood;
    7. }
    8. public void setFastFood(FastFood fastFood) { // 设置被装饰的组件
    9. this.fastFood = fastFood;
    10. }
    11. public Garnish(FastFood fastFood, float price, String desc) { // 构造函数,传入被装饰的组件、价格和描述
    12. super(price, desc); // 调用父类的构造函数,初始化价格和描述
    13. this.fastFood = fastFood; // 初始化被装饰的组件
    14. }
    15. }

    具体装饰者

    1. package com.yanyu.Decorator;
    2. //鸡蛋配料
    3. public class Egg extends Garnish {
    4. public Egg(FastFood fastFood) {
    5. super(fastFood,1,"鸡蛋");
    6. }
    7. public float cost() {
    8. return getPrice() + getFastFood().getPrice();
    9. }
    10. @Override
    11. public String getDesc() {
    12. return super.getDesc() + getFastFood().getDesc();
    13. }
    14. }
    1. package com.yanyu.Decorator;
    2. //培根配料
    3. public class Bacon extends Garnish {
    4. public Bacon(FastFood fastFood) {
    5. super(fastFood,2,"培根");
    6. }
    7. @Override
    8. public float cost() {
    9. return getPrice() + getFastFood().getPrice();
    10. }
    11. @Override
    12. public String getDesc() {
    13. return super.getDesc() + getFastFood().getDesc();
    14. }
    15. }

    客户端类

    1. // 测试类
    2. public class Client {
    3. public static void main(String[] args) {
    4. // 点一份炒饭
    5. FastFood food = new FriedRice();
    6. // 花费的价格
    7. System.out.println(food.getDesc() + " " + food.cost() + "元");
    8. System.out.println("========");
    9. // 点一份加鸡蛋的炒饭
    10. FastFood food1 = new FriedRice();
    11. food1 = new Egg(food1); // 使用装饰者模式给炒饭加上鸡蛋
    12. // 花费的价格
    13. System.out.println(food1.getDesc() + " " + food1.cost() + "元");
    14. System.out.println("========");
    15. // 点一份加培根的炒面
    16. FastFood food2 = new FriedNoodles();
    17. food2 = new Bacon(food2); // 使用装饰者模式给炒面加上培根
    18. // 花费的价格
    19. System.out.println(food2.getDesc() + " " + food2.cost() + "元");
    20. }
    21. }

    好处:

    • 饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则,继承是静态的附加责任,装饰者则是动态的附加责任。
    • 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。

    1.4使用场景

    • 如果你希望在无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的行为, 可以使用装饰模式。

    装饰能将业务逻辑组织为层次结构, 你可为各层创建一个装饰, 在运行时将各种不同逻辑组合成对象。 由于这些对象都遵循通用接口, 客户端代码能以相同的方式使用这些对象。

    • 如果用继承来扩展对象行为的方案难以实现或者根本不可行, 你可以使用该模式。

    许多编程语言使用 final最终关键字来限制对某个类的进一步扩展。 复用最终类已有行为的唯一方法是使用装饰模式: 用封装器对其进行封装。

    1.5JDK源码解析

    装饰者模式(Decorator Pattern)是一种结构型设计模式,它允许我们动态地给一个对象添加额外的行为,是继承关系的一种替代方案。

    在 JDK 中,装饰者模式被广泛地使用,比如 `java.io` 包中的输入输出流就是一个很好的例子。在这个包中,`InputStream` 和 `OutputStream` 是抽象基类,它们定义了基本的输入输出操作。`FilterInputStream` 和 `FilterOutputStream` 是装饰者类,它们继承了基类并添加了额外的行为,比如缓冲读写、数据压缩等。下面以 `BufferedInputStream` 为例进行介绍。

    1. public class BufferedInputStream extends FilterInputStream {
    2.     protected volatile byte buf[];
    3.     protected int count;
    4.     protected int pos;
    5.     protected int markpos = -1;
    6.     protected int marklimit;
    7.     // ...
    8.     public BufferedInputStream(InputStream in) {
    9.         this(in, DEFAULT_BUFFER_SIZE);
    10.     }
    11.     public BufferedInputStream(InputStream in, int size) {
    12.         super(in);
    13.         if (size <= 0) {
    14.             throw new IllegalArgumentException("Buffer size <= 0");
    15.         }
    16.         buf = new byte[size];
    17.     }
    18.     // ...
    19. }

    可以看到,`BufferedInputStream` 继承了 `FilterInputStream`,并重载了 `InputStream` 的部分方法。它包含一个字节数组 `buf`,用来缓存输入数据,从而减少底层输入流的读取次数,提高效率。

    1. public class FilterInputStream extends InputStream {
    2.     protected volatile InputStream in;
    3.     protected FilterInputStream(InputStream in) {
    4.         this.in = Objects.requireNonNull(in);
    5.     }
    6.     // ...
    7. }

    `FilterInputStream` 和 `FilterOutputStream` 都是抽象类,它们继承了基类并添加了构造函数和 `in` 或 `out` 属性,用于保存被装饰的基础流。

    在这个例子中,`BufferedInputStream` 充当了装饰者的角色,它在 `InputStream` 的基础上添加了缓存的功能,从而满足了更加复杂的输入需求。这个模式让我们能够在运行时动态地添加或修改对象的行为,而不需要修改原始对象的代码。

    1.6代理和装饰者的区别

    静态代理和装饰者模式的区别:

    • 相同点:

      • 都要实现与目标类相同的业务接口
      • 在两个类中都要声明目标对象
      • 都可以在不修改目标类的前提下增强目标方法
    • 不同点:

      • 目的不同 装饰者是为了增强目标对象 静态代理是为了保护和隐藏目标对象
      • 获取目标对象构建的地方不同 装饰者是由外界传递进来,可以通过构造方法传递 静态代理是在代理类内部创建,以此来隐藏目标对象

     

     二、装饰者模式实验

    任务描述

    某系统的读写文件模块,最初的业务逻辑类仅能读取和写入纯文本的数据。 现状系统需要进行安全升级,写数据需要先压缩再加密,读数据需要先解压缩再解密还原。

    本关任务:用装饰模式加强原来的文件读写模块。第一个封装器负责加密和解密数据, 而第二个则负责压缩和解压数据。

    实现方式

    1. 确保业务逻辑可用一个基本组件及多个额外可选层次表示;

    2. 找出基本组件和可选层次的通用方法。 创建一个组件接口并在其中声明这些方法;

    3. 创建一个具体组件类, 并定义其基础行为;

    4. 创建装饰基类, 使用一个成员变量存储指向被封装对象的引用。 该成员变量必须被声明为组件接口类型, 从而能在运行时连接具体组件和装饰。 装饰基类必须将所有工作委派给被封装的对象;

    5. 确保所有类实现组件接口;

    6. 将装饰基类扩展为具体装饰。 具体装饰必须在调用父类方法 (总是委派给被封装对象) 之前或之后执行自身的行为;

    7. 客户端代码负责创建装饰并将其组合成客户端所需的形式。

    编程提示

    1. //压缩函数
    2. String compress(String stringData) {
    3. byte[] data = stringData.getBytes();
    4. try {
    5. ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
    6. DeflaterOutputStream dos = new DeflaterOutputStream(bout, new Deflater(compLevel));
    7. dos.write(data);
    8. dos.close();
    9. bout.close();
    10. return Base64.getEncoder().encodeToString(bout.toByteArray());
    11. } catch (IOException ex) {
    12. return null;
    13. }
    14. }
    15. //解压缩函数
    16. String decompress(String stringData) {
    17. byte[] data = Base64.getDecoder().decode(stringData);
    18. try {
    19. InputStream in = new ByteArrayInputStream(data);
    20. InflaterInputStream iin = new InflaterInputStream(in);
    21. ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
    22. int b;
    23. while ((b = iin.read()) != -1) {
    24. bout.write(b);
    25. }
    26. in.close();
    27. iin.close();
    28. bout.close();
    29. return new String(bout.toByteArray());
    30. } catch (IOException ex) {
    31. return null;
    32. }
    33. }
    1. //加密函数
    2. String encode(String data) {
    3. byte[] result = data.getBytes();
    4. for (int i = 0; i < result.length; i++) {
    5. result[i] += (byte) 1;
    6. }
    7. return Base64.getEncoder().encodeToString(result);
    8. }
    9. //解密函数
    10. String decode(String data) {
    11. byte[] result = Base64.getDecoder().decode(data);
    12. for (int i = 0; i < result.length; i++) {
    13. result[i] -= (byte) 1;
    14. }
    15. return new String(result);
    16. }

    编程要求

    根据提示,在右侧编辑器 Begin-End 内补充代码: DataSource:抽象读写构件类; FileDataSource:具体文件读写构件类; DataSourceDecorator:抽象装饰类; CompressionDecorator:具体的字符串压缩/解压缩装饰类; EncryptionDecorator:具体的字符串加密/解密装饰类。

    "DataSourceDecorator.java"、"CompressionDecorator.java" 和 "EncryptionDecorator.java",请补全文件中的代码,其它文件的代码不需要修改。

    测试说明

    平台会自动从文件中读取数据,然后对你编写的代码进行测试:

    预期输出: Input ---------------- user,password John100000 Steven912000 Encoded -------------- Zkt4c01WNXUxam1KTUQ1dnt6Okw1Z01Mezloe09CQkNzdkRUMk1NVlFGdUVKekJJQlFjbEQ5Uj4= Decoded -------------- user,password John100000 Steven912000

    抽象构件

    1. package step1;
    2. // 定义数据源接口,包含写入数据和读取数据的方法
    3. public interface DataSource {
    4. void writeData(String data);
    5. String readData();
    6. }

    具体构件

    1. package step1;
    2. import java.io.*;
    3. // 具体组件,实现了数据源接口
    4. public class FileDataSource implements DataSource {
    5. private String name;
    6. public FileDataSource(String name) {
    7. this.name = name;
    8. }
    9. @Override
    10. public void writeData(String data) {
    11. // 将数据写入文件
    12. File file = new File(name);
    13. try (OutputStream fos = new FileOutputStream(file)) {
    14. fos.write(data.getBytes(), 0, data.length());
    15. } catch (IOException ex) {
    16. System.out.println(ex.getMessage());
    17. }
    18. }
    19. @Override
    20. public String readData() {
    21. // 从文件中读取数据
    22. char[] buffer = null;
    23. File file = new File(name);
    24. try (FileReader reader = new FileReader(file)) {
    25. buffer = new char[(int) file.length()];
    26. reader.read(buffer);
    27. } catch (IOException ex) {
    28. System.out.println(ex.getMessage());
    29. }
    30. return new String(buffer);
    31. }
    32. }

    抽象装饰

    1. package step1;
    2. // 定义一个抽象装饰者类
    3. public abstract class DataSourceDecorator implements DataSource {
    4. protected DataSource datasource; // 被装饰的数据源对象
    5. // 获取被装饰的组件
    6. public DataSource getDataSource() {
    7. return datasource;
    8. }
    9. // 设置被装饰的组件
    10. public void setDataSource(DataSource datasource) {
    11. this.datasource = datasource;
    12. }
    13. // 构造方法,接受被装饰的数据源对象
    14. public DataSourceDecorator(DataSource datasource) {
    15. this.datasource = datasource;
    16. }
    17. // 实现数据源接口中的读取数据方法
    18. @Override
    19. public String readData() {
    20. return datasource.readData();
    21. }
    22. // 实现数据源接口中的写入数据方法
    23. @Override
    24. public void writeData(String data) {
    25. datasource.writeData(data);
    26. }
    27. }

    具体装饰

    1. package step1;
    2. import java.io.ByteArrayInputStream;
    3. import java.io.ByteArrayOutputStream;
    4. import java.io.IOException;
    5. import java.io.InputStream;
    6. import java.util.Base64;
    7. import java.util.zip.Deflater;
    8. import java.util.zip.DeflaterOutputStream;
    9. import java.util.zip.InflaterInputStream;
    10. public class CompressionDecorator extends DataSourceDecorator {
    11. private int compLevel = 6; // 压缩级别,默认为6
    12. public CompressionDecorator(DataSource datasource) {
    13. super(datasource);
    14. }
    15. public int getCompressionLevel() {
    16. return compLevel;
    17. }
    18. public void setCompressionLevel(int value) {
    19. compLevel = value;
    20. }
    21. @Override
    22. public String readData() {
    23. // 对读取的数据进行解压缩
    24. return decompress(datasource.readData());
    25. }
    26. @Override
    27. public void writeData(String data) {
    28. // 对写入的数据进行压缩
    29. datasource.writeData(compress(data));
    30. }
    31. private String compress(String stringData) {
    32. // 使用压缩函数对字符串进行压缩
    33. byte[] data = stringData.getBytes();
    34. try {
    35. ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
    36. DeflaterOutputStream dos = new DeflaterOutputStream(bout, new Deflater(compLevel));
    37. dos.write(data);
    38. dos.close();
    39. bout.close();
    40. return Base64.getEncoder().encodeToString(bout.toByteArray());
    41. } catch (IOException ex) {
    42. return null;
    43. }
    44. }
    45. private String decompress(String stringData) {
    46. // 使用解压缩函数对字符串进行解压缩
    47. byte[] data = Base64.getDecoder().decode(stringData);
    48. try {
    49. InputStream in = new ByteArrayInputStream(data);
    50. InflaterInputStream iin = new InflaterInputStream(in);
    51. ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
    52. int b;
    53. while ((b = iin.read()) != -1) {
    54. bout.write(b);
    55. }
    56. in.close();
    57. iin.close();
    58. bout.close();
    59. return new String(bout.toByteArray());
    60. } catch (IOException ex) {
    61. return null;
    62. }
    63. }
    64. }
    1. package step1;
    2. import java.util.Base64;
    3. // 具体装饰者类,实现了数据加密和解密功能
    4. public class EncryptionDecorator extends DataSourceDecorator {
    5. // 构造方法,接收被装饰的数据源对象
    6. public EncryptionDecorator(DataSource datasource) {
    7. super(datasource);
    8. }
    9. // 重写读取数据方法,对读取的数据进行解密操作
    10. @Override
    11. public String readData() {
    12. String encryptedData = datasource.readData();
    13. return decode(encryptedData);
    14. }
    15. // 重写写入数据方法,对写入的数据进行加密操作
    16. @Override
    17. public void writeData(String data) {
    18. String encryptedData = encode(data);
    19. datasource.writeData(encryptedData);
    20. }
    21. // 加密方法,使用加密函数对字符串进行加密
    22. private String encode(String data) {
    23. byte[] result = data.getBytes();
    24. for (int i = 0; i < result.length; i++) {
    25. result[i] += (byte) 1;
    26. }
    27. return Base64.getEncoder().encodeToString(result);
    28. }
    29. // 解密方法,使用解密函数对字符串进行解密
    30. private String decode(String data) {
    31. byte[] result = Base64.getDecoder().decode(data);
    32. for (int i = 0; i < result.length; i++) {
    33. result[i] -= (byte) 1;
    34. }
    35. return new String(result);
    36. }
    37. }

    客户端类

    1. package step1;
    2. import org.w3c.dom.Document;
    3. import org.w3c.dom.Node;
    4. import org.w3c.dom.NodeList;
    5. import javax.xml.parsers.DocumentBuilder;
    6. import javax.xml.parsers.DocumentBuilderFactory;
    7. import java.io.File;
    8. public class XMLUtils {
    9. public static String getBean(String name) {
    10. try {
    11. //创建DOM文档对象
    12. DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
    13. DocumentBuilder builder = dFactory.newDocumentBuilder();
    14. Document doc;
    15. doc = builder.parse(new File("/data/workspace/myshixun/src/config.xml"));
    16. //获取包含类名的文本结点
    17. NodeList nl = doc.getElementsByTagName(name);
    18. Node classNode = nl.item(0).getFirstChild();
    19. String value = classNode.getNodeValue();
    20. return value;
    21. }
    22. catch(Exception e) {
    23. e.printStackTrace();
    24. return null;
    25. }
    26. }
    27. }
    1. package step1;
    2. public class Client {
    3. public static void main(String[] args) {
    4. // 从XML配置文件中获取薪资记录和文件路径
    5. String salaryRecords = XMLUtils.getBean("stringValue");
    6. String filePath = XMLUtils.getBean("filePath");
    7. // 创建一个装饰者对象,先进行加密再进行压缩,最后写入文件
    8. DataSourceDecorator encoded = new CompressionDecorator(
    9. new EncryptionDecorator(
    10. new FileDataSource(filePath)));
    11. encoded.writeData(salaryRecords);
    12. // 创建一个普通的数据源对象,用于输出原始数据
    13. DataSource plain = new FileDataSource(filePath);
    14. // 输出原始薪资记录
    15. System.out.println("Input ----------------");
    16. System.out.println(salaryRecords);
    17. // 输出经过装饰者加密和压缩后的数据
    18. System.out.println("Encoded --------------");
    19. System.out.println(plain.readData());
    20. // 输出经过装饰者解密和解压缩后的数据
    21. System.out.println("Decoded --------------");
    22. System.out.println(encoded.readData());
    23. }
    24. }

     

    1. 任务描述
    2. 某系统的读写文件模块,最初的业务逻辑类仅能读取和写入纯文本的数据。 现状系统需要进行安全升级,写数据需要先压缩再加密,读数据需要先解压缩再解密还原。
    3. 本关任务:用装饰模式加强原来的文件读写模块。第一个封装器负责加密和解密数据, 而第二个则负责压缩和解压数据。
    4. 实现方式
    5. 确保业务逻辑可用一个基本组件及多个额外可选层次表示;
    6. 找出基本组件和可选层次的通用方法。 创建一个组件接口并在其中声明这些方法;
    7. 创建一个具体组件类, 并定义其基础行为;
    8. 创建装饰基类, 使用一个成员变量存储指向被封装对象的引用。 该成员变量必须被声明为组件接口类型, 从而能在运行时连接具体组件和装饰。 装饰基类必须将所有工作委派给被封装的对象;
    9. 确保所有类实现组件接口;
    10. 将装饰基类扩展为具体装饰。 具体装饰必须在调用父类方法 (总是委派给被封装对象) 之前或之后执行自身的行为;
    11. 客户端代码负责创建装饰并将其组合成客户端所需的形式。
    12. 编程提示
    13. //压缩函数
    14. String compress(String stringData) {
    15. byte[] data = stringData.getBytes();
    16. try {
    17. ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
    18. DeflaterOutputStream dos = new DeflaterOutputStream(bout, new Deflater(compLevel));
    19. dos.write(data);
    20. dos.close();
    21. bout.close();
    22. return Base64.getEncoder().encodeToString(bout.toByteArray());
    23. } catch (IOException ex) {
    24. return null;
    25. }
    26. }
    27. //解压缩函数
    28. String decompress(String stringData) {
    29. byte[] data = Base64.getDecoder().decode(stringData);
    30. try {
    31. InputStream in = new ByteArrayInputStream(data);
    32. InflaterInputStream iin = new InflaterInputStream(in);
    33. ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
    34. int b;
    35. while ((b = iin.read()) != -1) {
    36. bout.write(b);
    37. }
    38. in.close();
    39. iin.close();
    40. bout.close();
    41. return new String(bout.toByteArray());
    42. } catch (IOException ex) {
    43. return null;
    44. }
    45. }
    46. //加密函数
    47. String encode(String data) {
    48. byte[] result = data.getBytes();
    49. for (int i = 0; i < result.length; i++) {
    50. result[i] += (byte) 1;
    51. }
    52. return Base64.getEncoder().encodeToString(result);
    53. }
    54. //解密函数
    55. String decode(String data) {
    56. byte[] result = Base64.getDecoder().decode(data);
    57. for (int i = 0; i < result.length; i++) {
    58. result[i] -= (byte) 1;
    59. }
    60. return new String(result);
    61. }
    62. 编程要求
    63. 根据提示,在右侧编辑器 Begin-End 内补充代码:
    64. DataSource:抽象读写构件类;
    65. FileDataSource:具体文件读写构件类;
    66. DataSourceDecorator:抽象装饰类;
    67. CompressionDecorator:具体的字符串压缩/解压缩装饰类;
    68. EncryptionDecorator:具体的字符串加密/解密装饰类。
    69. "DataSourceDecorator.java""CompressionDecorator.java""EncryptionDecorator.java",请补全文件中的代码,其它文件的代码不需要修改。

  • 相关阅读:
    【Flutter】支持多平台 多端保存图片到本地相册 (兼容 Web端 移动端 android 保存到本地)
    ARM中断实验
    在培训班学网络安全有用吗
    kafka操作的一些坑
    数据分析方法论和业务实战------数据分析概括与数据指标体系
    如何通过财务体系建设,助推企业数智化转型?
    【AI视野·今日Robot 机器人论文速览 第五十九期】Fri, 20 Oct 2023
    并发编程之ForkJoin框架
    C语言---二维数组&&指针
    jvm学习
  • 原文地址:https://blog.csdn.net/qq_62377885/article/details/134525541