开源轮子 - Apache Common

Apache Common

文章目录

Apache Commons是对JDK的拓展,包含了很多开源的工具,用于解决平时编程经常会遇到的问题,减少重复劳动。

官网网址:http://commons.apache.org

一:BeanUtils

xml 复制代码
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
</dependency>

针对Bean的一个工具集。由于Bean往往是有一堆get和set组成,所以BeanUtils也是在此基础上进行一些包装。

它利用Java的反射机制,从动态的生成对bean的getter和setter的调用代码,到模拟创建一个动态的bean,等等。

这个包看似简单,却是很多开源项目的基石:如在著名的Struts和Spring Framework中,我们都能找到BeanUtils的影子。

一个比较常用的功能是Bean Copy,也就是copy bean的属性。

如果做分层架构开发的话就会用到,比如从PO(Persistent Object)拷贝数据到VO(Value Object)

java 复制代码
org.apache.commons.beanutils // 核心包,定义一组Utils类和需要用到的接口规范
org.apache.commons.beanutils.converters // 转换String到需要类型的类,实现Converter接口
org.apache.commons.beanutils.locale  // beanutils的locale敏感版本
org.apache.commons.beanutils.locale.converters// converters的locale敏感版本
org.apache.commons.collections // beanutils使用到的Collection类

1:核心功能预览

java 复制代码
import org.apache.commons.beanutils.BeanUtils;
public class Demo {
    public static void main(String[] args) {
        Person person = new Person();
        try {
            // 使用BeanUtils设置属性
            BeanUtils.setProperty(person, "name", "张三");
            BeanUtils.setProperty(person, "age", 30);
            // 获取并打印属性
            String name = BeanUtils.getProperty(person, "name");
            String age = BeanUtils.getProperty(person, "age");
            System.out.println("姓名: " + name + ", 年龄: " + age);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Person {
    private String name;
    private int age;
    // 省略getter和setter方法
}

2:深入PropertyUtils

PropertyUtils主要提供了读取和设置JavaBean属性的功能。

如果咱们需要从一个对象中读取某个属性的值,或者要把值设置到对象的某个属性上,用PropertyUtils就能轻松搞定

java 复制代码
import org.apache.commons.beanutils.PropertyUtils;
public class PropertyUtilsDemo {
    public static void main(String[] args) {
        User user = new User();
        user.setName("李雷");
        user.setAge(25);
        try {
            // 读取属性值
            String name = (String) PropertyUtils.getProperty(user, "name");
            Integer age = (Integer) PropertyUtils.getProperty(user, "age");
            System.out.println("姓名: " + name + ", 年龄: " + age);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
class User {
    private String name;
    private int age;
    // 省略getter和setter方法
}

如果咱们想要设置对象的属性值,PropertyUtils同样能派上用场。

比如小黑现在要把Username改成"韩梅梅",age改成30。

java 复制代码
public class PropertyUtilsDemo {
    public static void main(String[] args) {
        User user = new User();

        try {
            // 设置属性值
            PropertyUtils.setProperty(user, "name", "韩梅梅");
            PropertyUtils.setProperty(user, "age", 30);

            // 验证结果
            System.out.println("姓名: " + user.getName() + ", 年龄: " + user.getAge());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // User类和前面一样,这里就不重复了
}

动态属性操作:灵活性的体现

PropertyUtils的魔力还不止于此。它还支持动态属性操作,这意味着咱们可以在运行时动态地读取和设置属性,而不必在编码时就确定属性名。这在处理动态数据结构时特别有用

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class DynamicPropertyDemo {
    public static void main(String[] args) {
        Map&lt;String, Object&gt; map = new HashMap&lt;&gt;();
        map.put("username", "小明");
        map.put("age", 20);

        try {
            // 动态读取属性
            String username = (String) PropertyUtils.getProperty(map, "username");
            Integer age = (Integer) PropertyUtils.getProperty(map, "age");
            System.out.println("用户名: " + username + ", 年龄: " + age);

            // 动态设置属性
            PropertyUtils.setProperty(map, "age", 21);
            System.out.println("更新后年龄: " + map.get("age"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3:BeanUtils的高级应用

属性复制,简化对象的迁移工作

属性复制是BeanUtils的一大亮点。在实际开发中,经常会遇到从一个对象复制属性到另一个对象的场景,尤其是在处理类似DTO(数据传输对象)和Entity(实体)转换的时候。

如果手动一个属性一个属性地复制,既麻烦又容易出错。这时候,BeanUtils的copyProperties方法就能大显身手了。

java 复制代码
import org.apache.commons.beanutils.BeanUtils;
public class CopyPropertiesDemo {
    public static void main(String[] args) {
        UserDTO userDTO = new UserDTO("王小明", 28);
        UserEntity userEntity = new UserEntity();
        try {
            // 从DTO复制到实体
            BeanUtils.copyProperties(userEntity, userDTO);
            // 验证结果
            System.out.println("用户实体:姓名 - " + userEntity.getName() + ", 年龄 - " + userEntity.getAge());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
class UserDTO {
    private String name;
    private int age;
    UserDTO(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 省略getter和setter方法
}
class UserEntity {
    private String name;
    private int age;
    // 省略getter和setter方法
}

动态Bean操作:更多的可能性

动态Bean操作是BeanUtils中另一个很酷的功能。

它允许咱们在运行时动态创建和操作Bean,这在处理不确定的数据结构或者动态生成对象的场景下特别有用。

java 复制代码
import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.DynaClass;
import org.apache.commons.beanutils.LazyDynaBean;
import org.apache.commons.beanutils.LazyDynaClass;
public class DynamicBeanDemo {
    public static void main(String[] args) {
        // 创建一个动态Bean
        DynaClass dynaClass = new LazyDynaClass();
        DynaBean dynaBean = new LazyDynaBean(dynaClass);
        try {
            // 动态添加属性
            dynaBean.set("name", "李华");
            dynaBean.set("age", 30);
            // 读取属性值
            String name = (String) dynaBean.get("name");
            Integer age = (Integer) dynaBean.get("age");
            System.out.println("动态Bean:姓名 - " + name + ", 年龄 - " + age);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4:ConvertUtils的威力

4.1:基本使用

ConvertUtils,这个小玩意儿主要负责类型转换,特别是在处理JavaBean的属性时,经常会遇到需要把一种类型转换成另一种类型的情况。ConvertUtils就是用来解决这类问题的。

在Java开发中,类型转换无处不在。比如从字符串转换成整数,或者从整数转换成布尔值等等。这些操作听起来简单,但如果每次都手动写转换代码,不仅麻烦,而且容易出错。ConvertUtils提供了一种统一的解决方案,可以自动完成这些转换,简化开发流程。

基本转换

java 复制代码
package com.example.bootrocketmq.study.wheel.beanutils;

import org.apache.commons.beanutils.ConvertUtils;

/**
 * @author cui haida
 * 2024/12/17
 */
public class TransDemo {
    public static void main(String[] args) {
        // 字符串转换为整数
        String ageStr = "25";
        Integer age = (Integer) ConvertUtils.convert(ageStr, Integer.class);
        System.out.println("转换后的年龄: " + age);
    }
}

自定义转换器

但ConvertUtils的真正魅力在于它的可扩展性。

如果咱们需要处理一些特殊的转换,比如把字符串转换成日期类型,或者把数字转换成自定义的枚举类型,这时就可以自定义转换器。

java 复制代码
package com.example.bootrocketmq.study.wheel.beanutils;

import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.Converter;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * @author cui haida
 * 2024/12/17
 */
public class TransDemo {
    /**
     * 程序的主入口点
     * 本程序演示了如何使用Apache Commons BeanUtils库中的自定义类型转换器将字符串转换为日期
     * @param args 命令行参数,未使用
     */
    public static void main(String[] args) {
        // 自定义转换器:字符串转日期
        // 创建一个匿名内部类实例,用于定义字符串到日期的转换逻辑
        Converter dtConverter = new Converter() {
            private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());

            /**
             * 将给定的字符串转换为指定类型的日期对象
             * 
             * @param type 期望转换到的日期类型,例如Date、LocalDate等
             * @param value 待转换的字符串表示的日期
             * @return 转换后的日期对象,如果转换失败则返回null
             * 
             * 此方法主要用于将字符串表示的日期转换为程序中使用的日期对象
             * 它通过解析字符串并将其转换为指定类型的日期对象来实现这一功能
             * 如果提供的值不是字符串或者为null,则认为无法进行转换,返回null
             * 如果解析过程中出现错误,将抛出运行时异常
             */
            @Override
            public <T> T convert(Class<T> type, Object value) {
                // 检查输入值是否为null或不是字符串,如果是,则返回null
                if (value == null || !(value instanceof String)) {
                    return null;
                }
                try {
                    // 使用DateFormat解析字符串为日期对象,并将其转换为指定类型
                    return type.cast(dateFormat.parse((String) value));
                } catch (ParseException e) {
                    // 如果解析失败,抛出运行时异常,包装原始的解析异常
                    throw new RuntimeException("无法将字符串转换为日期", e);
                }
            }

        };

        // 注册自定义转换器到ConvertUtils
        // 这样就可以使用ConvertUtils.convert方法进行字符串到日期的转换
        ConvertUtils.register(dtConverter, Date.class);

        // 使用自定义转换器
        // 定义一个日期字符串,准备进行转换
        String dateStr = "2023-12-25";
        // 将字符串转换为日期使用之前注册的自定义转换器
        Date date = (Date) ConvertUtils.convert(dateStr, Date.class);
        // 输出转换后的日期
        System.out.println("转换后的日期: " + date);
    }
}
4.2:我要的异常呢?

先测了一下,一个乱码String转Boolean,看看会不会抛出异常。结果出乎意外的是,它居然没有抛出异常,还返回了false。认真看了一下,才发现它直接把异常打印出来了,还返回了默认值。

java 复制代码
package com.example.bootrocketmq.study.wheel.beanutils;
import org.apache.commons.beanutils.ConvertUtils;

/**
 * @author cui haida
 * 2024/12/18
 */
public class TransErrorDemo {
    public static void main(String[] args) {
        Object obj;
        obj = ConvertUtils.convert("hahaha", Boolean.class);
        System.out.println(obj);
    }
}

通过日志信息可以发现,无法进行指定的转换之后将会使用默认的false作为结果。

这就非常的尴尬了,因为没有异常的抛出,那怎么知道原来的值到底是错误的还是bool false。

4.3:探究内核 - ConvertUtilsBean

command + 左键进入ConvertUtils.convert() ,可以发现底层是ConvertUtilBean

java 复制代码
public static Object convert(String[] values, Class<?> clazz) {
    return ConvertUtilsBean.getInstance().convert(values, clazz);
}

进入ConvertUtilBean可以发现在内部的register方法中处理了异常

java 复制代码
public void register(boolean throwException, boolean defaultNull, int defaultArraySize) {
    this.registerPrimitives(throwException);
    this.registerStandard(throwException, defaultNull);
    this.registerOther(throwException);
    this.registerArrays(throwException, defaultArraySize);
}

这里可以配置是否要抛出异常。于是我就用这个bean,测了一下,果然可以

java 复制代码
package com.example.bootrocketmq.study.wheel.beanutils;

import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.ConvertUtilsBean;

/**
 * @author cui haida
 * 2024/12/18
 */
public class TransErrorDemo {
    public static void main(String[] args) {
//        Object obj;
//        obj = ConvertUtils.convert("hahaha", Boolean.class);
//        System.out.println(obj);
        ConvertUtilsBean convertUtilsBean = new ConvertUtilsBean();
        // 注册转换器
        // 三个参数:是否忽略转换失败的异常,是否忽略空值,空值转换成的默认值
        convertUtilsBean.register(true, false, 0);
        try {
            // 转换失败,会抛出异常
            Object obj = convertUtilsBean.convert("hahaha", Boolean.class);
            System.out.println(obj);
        } catch (Exception e) {
            System.out.println("捕获到了异常: " + e.getMessage());
            System.out.println("转换失败");
        }
    }
}
4.4:CovertUtilsBean可以任意转换?

实际上,它也不是想转什么就转什么。初始条件下,它内部只注册了基本类型的转换器

java 复制代码
public ConvertUtilsBean() {
    // 基本类型转换器
    this.defaultBoolean = Boolean.FALSE;
    this.defaultByte = new Byte((byte)0);
    this.defaultCharacter = new Character(' ');
    this.defaultDouble = new Double((double)0.0F);
    this.defaultFloat = new Float(0.0F);
    this.defaultInteger = new Integer(0);
    this.defaultLong = new Long(0L);
    this.converters.setFast(false);
    // 反注册已注册的转换器
    this.deregister();
    this.converters.setFast(true);
}


/**
 * 反注册已注册的转换器
 * 
 * 此方法旨在清除当前对象中已注册的所有类型转换器,并根据需要重新注册特定类型的转换器
 * 它首先清空converters集合,然后重新注册基本类型、标准类型、其他类型和数组类型的转换器
 * 最后,为BigDecimal和BigInteger类型注册专门的转换器
 */
public void deregister() {
    // 清空所有已注册的转换器
    this.converters.clear();
    
    // 重新注册基本类型转换器,但不包括本次反注册操作中已移除的类型
    this.registerPrimitives(false);
    
    // 重新注册标准类型转换器,同样不包括本次反注册操作中已移除的类型
    // 第二个参数指示是否也重新注册基本类型,这里选择不注册
    this.registerStandard(false, false);
    
    // 重新注册其他类型转换器,包括那些在本次反注册操作中被移除的类型
    this.registerOther(true);
    
    // 重新注册数组类型转换器,但不包括本次反注册操作中已移除的类型
    // 第二个参数指定数组维度,这里设为0表示处理所有维度的数组
    this.registerArrays(false, 0);
    
    // 为BigDecimal类型注册一个专门的转换器
    this.register(BigDecimal.class, new BigDecimalConverter());
    
    // 为BigInteger类型注册一个专门的转换器
    this.register(BigInteger.class, new BigIntegerConverter());
}

注册?

"注册"这个词,看上去挺玄乎的,其实一般就是写到一个注册表里面,然后需要的时候从表中检索。在这个实现中,注册表,不过就是一张HashMap。而注册操作,就是把Converter对象放到这个哈希表中。

java 复制代码
private final WeakFastHashMap<Class<?>, Converter> converters = new WeakFastHashMap();

我们可以看到,这个表的Key是类型。也就是说,我们在使用convert方法的时候,已经指定了key,自然就找到了对应的Converter。

因为map的性质 -> key的唯一性 -> 新注册的Converter会覆盖同类型的Converter。

二:Codec

xml 复制代码
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.15</version>
</dependency>

是编码和解码组件,提供常用的编码和解码方法,如DES、SHA1、MD5、Base64、URL和Soundx等。

1:base64

java 复制代码
package com.example.bootrocketmq.study.wheel.codec;

import org.apache.commons.codec.binary.Base64;

import java.nio.charset.StandardCharsets;

/**
 * @author cui haida
 * 2024/12/19
 */
public class Base64Demo {
    /**
     * @author cui haida
     * 方法编写日期: 2024/12/19
     * desc ->  进行base64编码和解码
     */
    public static void main(String[] args) {
        Base64 base64 = new Base64();
        String str = "AAaaa我";
        try {
            // 字符串进行指定符号的编码
            String result = base64.encodeToString(str.getBytes(StandardCharsets.UTF_8));//编码
            System.out.println(result);
            // 解码成为字节数组
            byte[] decode = base64.decode(result.getBytes());//解码
            System.out.println(new String(decode));
        } catch (Exception e) {
            System.out.println("转换失败");
        }
    }
}

2:Hex

java 复制代码
package com.example.bootrocketmq.study.wheel.codec;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;

import java.nio.charset.StandardCharsets;

/**
 * @author cui haida
 * 2024/12/19
 */
public class HexDemo {
    /**
     * @author cui haida
     * 方法编写日期: 2024/12/19
     * desc ->  Hex编码解码
     */
    public static void main(String[] args) throws DecoderException {
        String str1 = "test";
        /* 编码 */
        String hexString = Hex.encodeHexString(str1.getBytes(StandardCharsets.UTF_8));//直接一步到位
        System.out.println(hexString);
        char[] encodeHex = Hex.encodeHex(str1.getBytes(), true);//先转换成char数组,第二个参数意思是是否全部转换成小写
        System.out.println(new String(encodeHex));
        /* 解码 */
        byte[] decodeHex = Hex.decodeHex(encodeHex);//char数组型的
        System.out.println(new String(decodeHex));
        byte[] decodeHex2 = Hex.decodeHex(hexString.toCharArray());//字符串类型的,该方法要求传入的是char[]
        System.out.println(new String(decodeHex2));
    }
}

3:各种加密解密

md5加密

java 复制代码
package com.example.bootrocketmq.study.wheel.codec;

import org.apache.commons.codec.digest.DigestUtils;

import java.nio.charset.StandardCharsets;

/**
 * @author cui haida
 * 2024/12/19
 */
public class Md5Demo {
    /**
     * @author cui haida
     * 方法编写日期: 2024/12/19
     * desc ->  md5加密
     */
    public static void main(String[] args) {
        String str2 = "test";
        String md5 = DigestUtils.md5Hex(str2.getBytes(StandardCharsets.UTF_8));
        System.out.println(md5);
    }
}
java 复制代码
package com.example.bootrocketmq.study.wheel.codec;

import org.apache.commons.codec.digest.DigestUtils;

import java.nio.charset.StandardCharsets;

/**
 * @author cui haida
 * 2024/12/19
 */
public class ShaDemo {
    /**
     * @author cui haida
     * 方法编写日期: 2024/12/19
     * desc ->  SHA加密
     */
    public static void main(String[] args) {
        String str3 = "test中国";
        String sha1Hex = DigestUtils.sha1Hex(str3.getBytes(StandardCharsets.UTF_8));
        System.out.println(sha1Hex);
    }
}
java 复制代码
package com.example.bootrocketmq.study.wheel.codec;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.net.URLCodec;

/**
 * @author cui haida
 * 2024/12/19
 */
public class UrlcodecDemo {
    /**
     * @author cui haida
     * 方法编写日期: 2024/12/19
     * desc ->  URLCodec 编码解码
     */
    public static void main(String[] args) throws EncoderException, DecoderException {
        String url = "https://baidu.com?name=你好";
        URLCodec codec = new URLCodec();
        String encode = codec.encode(url);
        System.out.println(encode);
        String decodes = codec.decode(encode);
        System.out.println(decodes);
    }
}   

三:Collections

xml 复制代码
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</version>
</dependency>

是一个集合组件,扩展了Java标准Collections API,对常用的集合操作进行了很好的封装、抽象和补充,在保证性能的同时大大简化代码。

共有12个包:

java 复制代码
org.apache.commons.collections // CommonsCollections自定义的一组公用的接口和工具类
org.apache.commons.collections.bag // 实现Bag接口的一组类
org.apache.commons.collections.bidimap // 实现BidiMap系列接口的一组类
org.apache.commons.collections.buffer // 实现Buffer接口的一组类
org.apache.commons.collections.collection // 实现java.util.Collection接口的一组类
org.apache.commons.collections.comparators// 实现java.util.Comparator接口的一组类
org.apache.commons.collections.functors // Commons Collections自定义的一组功能类
org.apache.commons.collections.iterators // 实现java.util.Iterator接口的一组类
org.apache.commons.collections.keyvalue // 实现集合和键/值映射相关的一组类
org.apache.commons.collections.list // 实现java.util.List接口的一组类
org.apache.commons.collections.map // 实现Map系列接口的一组类
org.apache.commons.collections.set // 实现Set系列接口的一组类
  • 作为容器类的补充,我们可以找到Bag、Buffer、BidiMap、OrderedMap等等;
  • 作为操作类的补充,我们可以找到CollectionUtils、IteratorUtils、ListUtils、SetUtils等等;
  • 作为辅助类的补充,我们可以找到MapIterator、Closure、Predicate、Transformer等等;

下面简单的介绍一下部分补充的用法

更多详细用法参见博客:https://blog.csdn.net/Li_WenZhang/article/details/143034738

1:双向映射map

java 复制代码
/**
 * @author cui haida
 * 方法编写日期: 2024/12/19
 * BidiMap 是 Apache Commons Collections 中提供的一种双向映射结构
 * 允许你不仅可以通过 key 查找 value,还可以通过 value 查找 key。
 * 相比于 Java 标准库中的 HashMap,它提供了双向查找的能力,非常适合用于需要频繁进行 key-value 对称操作的场景。
 * 常见的实现包括:
 *  - DualHashBidiMap:使用 HashMap 实现的双向 Map,适合大多数通用场景,查找性能优异。
 *  - DualLinkedHashBidiMap:基于 LinkedHashMap 的实现,维护插入顺序的双向映射。
 *  - TreeBidiMap:基于 TreeMap,支持按自然顺序或自定义比较器排序的双向映射
 *  使用说明分析
 *  - DualHashBidiMap:不保证顺序,但性能优越,适合大多数需要高效查找和插入的场景。
 *  - DualLinkedHashBidiMap:维护插入顺序,适合对顺序有要求的场景,如日志或展示数据。
 *  - TreeBidiMap:适用于需要排序的场景,如按字母顺序展示数据或构建有序索引。
 */
private static void bidiMapTest() {
    BidiMap<String, String> bidiMap = new DualHashBidiMap<>();
    bidiMap.put("a", "1");
    bidiMap.put("b", "2");

    System.out.println(bidiMap.get("a"));  // 输出 "1"
    System.out.println(bidiMap.getKey("2"));  // 输出 "b"

    // 删除操作演示
    bidiMap.removeValue("1");  // 同时移除 key "a" 和 value "1"
}

2:有序map和set

java 复制代码
/**
 * @author cui haida
 * 方法编写日期: 2024/12/19
 * =====================================
 * 双向排序map
 * - DualTreeBidiMap:这是一个基于 TreeMap 实现的双向 Map,它不仅允许根据键来排序,还可以根据值来排序。
 * - DualTreeBidiMap 保证了键和值的唯一性,并且提供了快速的键值对反转操作
 * =====================================
 * 按插入顺序的双向 Map
 * - LinkedMap:这是一个在 Apache Commons Collections4 中的有序 Map,类似于 LinkedHashMap,它维护了插入顺序。
 * 与 LinkedHashMap 不同的是,LinkedMap 具有额外的遍历功能和方法
 * 如 getFirstKey() 和 getLastKey(),可以快速获取第一个和最后一个键
 * =====================================
 * 多值map
 * - MultiKeyMap<K, V>:这个类允许将多个键组合起来映射到一个值上。
 * - 它对多个键的顺序也进行了维护,类似于 Map<K, List<V>> 的更高效实现,特别适用于组合键需要有序的情况
 * =====================================
 */
private static void orderMapAndSetTest() {
    // =========== 双向排序map ==============
    BidiMap<String, Integer> bidiMap = new DualTreeBidiMap<>();
    bidiMap.put("apple", 1);
    bidiMap.put("banana", 2);
    bidiMap.put("cherry", 3);

    System.out.println(bidiMap);          // {apple=1, banana=2, cherry=3}
    // 获取反向映射,就是翻转value - key
    System.out.println(bidiMap.inverseBidiMap());  // {1=apple, 2=banana, 3=cherry}

    // =========== 按插入顺序的双向 Map ============
    LinkedMap<String, Integer> linkedMap = new LinkedMap<>();
    linkedMap.put("one", 1);
    linkedMap.put("two", 2);
    linkedMap.put("three", 3);

    System.out.println(linkedMap);  // {one=1, two=2, three=3}
    System.out.println(linkedMap.firstKey());  // one
    System.out.println(linkedMap.lastKey());   // three

    // =========== 多值map ============
    MultiKeyMap<String, Integer> multiKeyMap = new MultiKeyMap<>();
    multiKeyMap.put("apple", "red", 1);
    multiKeyMap.put("banana", "yellow", 2);

    System.out.println(multiKeyMap.get("apple", "red"));  // 1
    System.out.println(multiKeyMap.get("banana", "yellow"));  // 2
}

3:Bag

Bag 是 Apache Commons Collections4 提供的一种特殊集合,它允许在集合中存储相同元素的多个实例,并记录每个元素的出现次数。Bag 是对 Java 标准集合的一种扩展,适合用于处理多重集合或计数集合。

Commons Collections4 中的 Bag 提供了多种实现,并有不同的优化特性

bag接口

Bag 接口继承自 Collection,提供了额外的方法来处理元素的计数。与 Set 不同,Bag 允许存储相同元素的多个实例,但与 List 不同,Bag 关注的是元素的频率而非顺序。常用的方法包括:

  • add(E object, int n):添加 n 个给定的对象。
  • getCount(Object object):获取给定对象的出现次数。
  • remove(Object object, int n):删除指定数量的对象实例。
java 复制代码
/**
 * @author cui haida
 * 方法编写日期: 2024/12/19
 * desc -> 测试bag
 */
private static void bagTest() {
    // HashBag 基于 HashMap 实现,提供了高效的插入、删除和计数操作。
    // 它是一个无序的 Bag,适合不关心元素顺序的场景。HashBag 是最常用的 Bag 实现,具有较高的性能。
    Bag<String> bag = new HashBag<>();
    bag.add("apple", 3);   // 添加 3 个 "apple"
    bag.add("banana", 2);  // 添加 2 个 "banana"
    System.out.println(bag.getCount("apple"));  // 输出 3
    System.out.println(bag);  // 输出 [2:banana,3:apple]

    // TreeBag 是 Bag 的有序实现,基于 TreeMap,它确保元素按自然顺序或提供的比较器顺序进行存储。
    // 适合需要排序并统计元素出现次数的场景
    Bag<String> treeBag = new TreeBag<>();
    treeBag.add("cherry", 2);
    treeBag.add("apple", 3);
    System.out.println(treeBag);  // 输出 [3:apple 2:cherry]

    // SynchronizedBag 是线程安全的 Bag 实现,它通过对底层 Bag 进行包装,提供同步操作。
    // 适合多线程并发访问的场景。
    Bag<String> syncBag = SynchronizedBag.synchronizedBag(new HashBag<>());
    syncBag.add("apple", 1);
    syncBag.add("bag", 2);
    System.out.println(syncBag.getCount("apple"));

    // PredicatedBag 是一个包装类,它允许你指定一个 Predicate,用于在添加元素时进行验证。
    // 如果添加的元素违反了 Predicate 的条件,则会抛出 IllegalArgumentException。
    Predicate<String> noNull = Objects::nonNull;
    Bag<String> predicatedBag = PredicatedBag.predicatedBag(new HashBag<>(), noNull);
    predicatedBag.add("apple");  // 成功
    try {
        predicatedBag.add(null);     // 抛出 IllegalArgumentException
    } catch (Exception e) {
        System.out.println("添加 null 元素时抛出了异常:" + e.getMessage());
    }

    // TransformedBag 是一个包装类,它允许你指定一个 Transformer,用于在添加元素时进行转换。
    // 例如,你可以将 TransformedBag 应用于 HashBag,使其在添加元素时自动转换为小写字母。
    Bag<String> transformedBag = TransformedBag.transformingBag(new HashBag<>(), String::toLowerCase);
    transformedBag.add("APPLE");
    transformedBag.add("Apple");
    transformedBag.add("apple");
    System.out.println(transformedBag);  // 输出 [3:apple]
    System.out.println(predicatedBag.getCount("apple")); // 3
}

bag的局限性

虽然 Bag 提供了对多重集合的高效支持,但它并不适用于所有场景。需要注意的是:

  • 顺序问题:默认情况下,Bag 并不维护元素的插入顺序,如果需要顺序性,可以选择 TreeBag 或自行管理顺序。
  • 线程安全性:大多数 Bag 实现并非线程安全,除非显式使用 SynchronizedBag。

四:Compress

xml 复制代码
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-compress</artifactId>
    <version>1.19</version>
</dependency>

是一个压缩、解压缩文件的组件,可以操作rar、cpio、Unix dump、tar、zip、gzip、XZ、Pack200和bzip2格式的压缩文件

1:核心API

  • 压缩输入流,用于解压压缩文件:ArchiveInputStream
  • 压缩输出流,用于压缩文件:ArchiveOutputStream
  • 压缩文件内部存档的条目,压缩文件内部的每一个被压缩文件都称为一个条目:ArchiveEntry

2:本地文件压缩和解压

java 复制代码
package com.example.bootrocketmq.study.wheel.compress;


import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.Zip64Mode;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;

import java.io.*;
import java.nio.file.Files;

/**
 * @author cui haida
 * 2024/12/20
 */
public class CompressDemo {
    public static void main(String[] args) {
        // 三大核心API
        // 压缩输入流,用于解压压缩文件:public abstract class ArchiveInputStream extends java.io.InputStream
        // 压缩输出流,用于压缩文件:public abstract class ArchiveOutputStream extends java.io.OutputStream
        // 压缩文件内部存档的条目,压缩文件内部的每一个被压缩文件都称为一个条目:public interface ArchiveEntry
    }

    /**
     * 将文件打包成 zip 压缩包文件
     *
     * @param sourceFiles        待压缩的多个文件列表。只支持文件,不能有目录,否则抛异常。
     * @param zipFile            压缩文件。文件可以不存在,但是目录必须存在,否则抛异常。如 C:\Users\Think\Desktop\aa.zip
     * @param isDeleteSourceFile 是否删除源文件(sourceFiles)
     * @return 是否压缩成功
     */
    public static boolean archiveFiles2Zip(File[] sourceFiles, File zipFile, boolean isDeleteSourceFile) {
        InputStream inputStream = null;//源文件输入流
        ZipArchiveOutputStream zipArchiveOutputStream = null; //压缩文件输出流
        // 如果灭有要进行压缩的文件 - 返回 false
        if (sourceFiles == null || sourceFiles.length == 0) {
            return false;
        }
        try {
            // ZipArchiveOutputStream(File file) :根据文件构建压缩输出流,将源文件压缩到此文件.
            zipArchiveOutputStream = new ZipArchiveOutputStream(zipFile);
            //setUseZip64(final Zip64Mode mode):是否使用 Zip64 扩展。
            // Zip64Mode 枚举有 3 个值:
            // Always:对所有条目使用 Zip64 扩展
            // Never:不对任何条目使用Zip64扩展
            // AsNeeded:对需要的所有条目使用Zip64扩展
            zipArchiveOutputStream.setUseZip64(Zip64Mode.AsNeeded);
            // 遍历每一个文件,对文件进行遍历。
            for (File file : sourceFiles) {
                //将每个源文件用 ZipArchiveEntry 实体封装,然后添加到压缩文件中. 这样将来解压后里面的文件名称还是保持一致.
                ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(file.getName());
                zipArchiveOutputStream.putArchiveEntry(zipArchiveEntry);
                inputStream = Files.newInputStream(file.toPath());//获取源文件输入流
                byte[] buffer = new byte[1024 * 5];
                int length = -1;//每次读取的字节大小。
                while ((length = inputStream.read(buffer)) != -1) {
                    //把缓冲区的字节写入到 ZipArchiveEntry
                    zipArchiveOutputStream.write(buffer, 0, length);
                }
            }
            zipArchiveOutputStream.closeArchiveEntry();//写入此条目的所有必要数据。如果条目未压缩或压缩后的大小超过4 GB 则抛出异常
            zipArchiveOutputStream.finish();//压缩结束.
            
            if (isDeleteSourceFile) {
                //为 true 则删除源文件.
                for (File file : sourceFiles) {
                    file.deleteOnExit();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            //关闭输入、输出流,释放资源.
            try {
                if (null != inputStream) {
                    inputStream.close();
                }
                if (null != zipArchiveOutputStream) {
                    zipArchiveOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    /**
     * 将 zip 压缩包解压成文件到指定文件夹下
     *
     * @param zipFile   待解压的压缩文件。亲测  .zip 文件有效;.7z 压缩解压时抛异常。
     * @param targetDir 解压后文件存放的目的地. 此目录必须存在,否则异常。
     * @return 是否成功
     */
    public static boolean decompressZip2Files(File zipFile, File targetDir) {
        InputStream inputStream = null;//源文件输入流,用于构建 ZipArchiveInputStream
        OutputStream outputStream = null;//解压缩的文件输出流
        ZipArchiveInputStream zipArchiveInputStream = null;//zip 文件输入流
        ArchiveEntry archiveEntry = null;//压缩文件实体.
        try {
            inputStream = Files.newInputStream(zipFile.toPath());//创建输入流,然后转压缩文件输入流
            zipArchiveInputStream = new ZipArchiveInputStream(inputStream, "UTF-8");
            //遍历解压每一个文件.
            while (null != (archiveEntry = zipArchiveInputStream.getNextEntry())) {
                String archiveEntryFileName = archiveEntry.getName();//获取文件名
                File entryFile = new File(targetDir, archiveEntryFileName);//把解压出来的文件写到指定路径
                byte[] buffer = new byte[1024 * 5];
                outputStream = Files.newOutputStream(entryFile.toPath());
                int length = -1;
                while ((length = zipArchiveInputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, length);
                }
                outputStream.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            try {
                if (null != outputStream) {
                    outputStream.close();
                }
                if (null != zipArchiveInputStream) {
                    zipArchiveInputStream.close();
                }
                if (null != inputStream) {
                    inputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }
}

3:网络文件的压缩和解压缩

java 复制代码
package com.example.bootrocketmq.study.wheel.compress;


import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.Zip64Mode;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @author cui haida
 * 2024/12/20
 */
public class ZipFileDownloadDemo {
    /**
     * 压缩本地多个文件并提供输出流下载
     *
     * @param filePaths   :本地文件路径,如 ["C:\\wmx\\temp\\data1.json","C:\\wmx\\temp\\data2.json"]。只支持文件,不能有目录,否则抛异常。
     * @param zipFileName :压缩文件输出的名称,如 "年终总结" 不含扩展名。默认文件当前时间。如 20200108111213.zip
     * @param response    :提供输出流
     */
    public static void zipFileDownloadByFile(Set<String> filePaths, String zipFileName, HttpServletResponse response) {
        try {
            //1)参数校验
            if (filePaths == null || filePaths.size() <= 0) {
                throw new RuntimeException("待压缩导出文件为空.");
            }
            if (zipFileName == null || zipFileName.isEmpty()) {
                zipFileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".zip";
            } else {
                zipFileName = zipFileName + ".zip";
            }
            //2)设置 response 参数。这里文件名如果是中文,则导出乱码,可以
            response.reset();
            response.setContentType("content-type:octet-stream;charset=UTF-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(zipFileName, "utf-8"));

            //3)通过 OutputStream 创建 zip 压缩流。如果是压缩到本地,也可以直接使用 ZipArchiveOutputStream(final File file)
            ServletOutputStream servletOutputStream = response.getOutputStream();
            ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(servletOutputStream);
            //4)setUseZip64(final Zip64Mode mode):是否使用 Zip64 扩展。
            // Zip64Mode 枚举有 3 个值:Always:对所有条目使用 Zip64 扩展、Never:不对任何条目使用Zip64扩展、AsNeeded:对需要的所有条目使用Zip64扩展
            zipArchiveOutputStream.setUseZip64(Zip64Mode.AsNeeded);

            for (String filePath : filePaths) {
                File file = new File(filePath);
                String fileName = file.getName();
                InputStream inputStream = Files.newInputStream(file.toPath());
                //5)使用 ByteArrayOutputStream 读取文件字节
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int readLength = -1;
                while ((readLength = inputStream.read(buffer)) != -1) {
                    byteArrayOutputStream.write(buffer, 0, readLength);
                }
                byteArrayOutputStream.flush();
                byte[] fileBytes = byteArrayOutputStream.toByteArray();//整个文件字节数据

                //6)用指定的名称创建一个新的 zip 条目(zip压缩实体),然后设置到 zip 压缩输出流中进行写入.
                ArchiveEntry entry = new ZipArchiveEntry(fileName);
                zipArchiveOutputStream.putArchiveEntry(entry);
                //6.1、write(byte b[]):从指定的字节数组写入 b.length 个字节到此输出流
                zipArchiveOutputStream.write(fileBytes);
                //6.2、写入此条目的所有必要数据。如果条目未压缩或压缩后的大小超过4 GB 则抛出异常
                zipArchiveOutputStream.closeArchiveEntry();
                byteArrayOutputStream.close();
            }
            //7)最后关闭 zip 压缩输出流.
            zipArchiveOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 压缩本地多个文件并提供输出流下载。只支持文件,不能有目录,否则抛异常。
     *
     * @param fileLists   :本地文件,如 ["C:\\wmx\\temp\\data1.json","C:\\wmx\\temp\\data2.json"]。
     * @param zipFileName :压缩文件输出的名称,如 "年终总结" 不含扩展名。默认文件当前时间。如 20200108111213.zip
     * @param response    :提供输出流
     */
    public static void zipFileDownloadByFile(List<File> fileLists, String zipFileName, HttpServletResponse response) {
        if (fileLists == null || fileLists.isEmpty()) {
            throw new RuntimeException("待压缩导出文件为空.");
        }
        Set<String> filePaths = new HashSet<>();
        for (File file : fileLists) {
            filePaths.add(file.getAbsolutePath());
        }
        zipFileDownloadByFile(filePaths, zipFileName, response);
    }

    /**
     * 压缩网络文件。
     *
     * @param urlLists,待压缩的网络文件地址,如 ["http://www.baidu.com/img/bd_logo1.png"]
     * @param zipFileName
     * @param response
     */
    public static void zipFileDownloadByUrl(List<URL> urlLists, String zipFileName, HttpServletResponse response) {
        try {
            //1)参数校验
            if (urlLists == null || urlLists.size() <= 0) {
                throw new RuntimeException("待压缩导出文件为空.");
            }
            if (zipFileName == null || zipFileName.isEmpty()) {
                zipFileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".zip";
            } else {
                zipFileName = zipFileName + ".zip";
            }
            //2)设置 response 参数
            response.reset();
            response.setContentType("content-type:octet-stream;charset=UTF-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(zipFileName, "utf-8"));

            //3)通过 OutputStream 创建 zip 压缩流。如果是压缩到本地,也可以直接使用 ZipArchiveOutputStream(final File file)
            ServletOutputStream servletOutputStream = response.getOutputStream();
            ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(servletOutputStream);
            //4)setUseZip64(final Zip64Mode mode):是否使用 Zip64 扩展。
            // Zip64Mode 枚举有 3 个值:Always:对所有条目使用 Zip64 扩展、Never:不对任何条目使用Zip64扩展、AsNeeded:对需要的所有条目使用Zip64扩展
            zipArchiveOutputStream.setUseZip64(Zip64Mode.AsNeeded);

            for (URL url : urlLists) {
                String fileName = getNameByUrl(url);
                InputStream inputStream = url.openStream();
                //5)使用 ByteArrayOutputStream 读取文件字节
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int readLength = -1;
                while ((readLength = inputStream.read(buffer)) != -1) {
                    byteArrayOutputStream.write(buffer, 0, readLength);
                }
                byteArrayOutputStream.flush();
                byte[] fileBytes = byteArrayOutputStream.toByteArray();//整个文件字节数据

                //6)用指定的名称创建一个新的 zip 条目(zip压缩实体),然后设置到 zip 压缩输出流中进行写入.
                ArchiveEntry entry = new ZipArchiveEntry(fileName);
                zipArchiveOutputStream.putArchiveEntry(entry);
                //6.1、write(byte b[]):从指定的字节数组写入 b.length 个字节到此输出流
                zipArchiveOutputStream.write(fileBytes);
                //6.2、写入此条目的所有必要数据。如果条目未压缩或压缩后的大小超过4 GB 则抛出异常
                zipArchiveOutputStream.closeArchiveEntry();
                byteArrayOutputStream.close();
            }
            //7)最后关闭 zip 压缩输出流.
            zipArchiveOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 压缩网络文件
     *
     * @param urlPaths    待压缩的网络文件地址,如 ["http://www.baidu.com/img/bd_logo1.png"]
     * @param zipFileName
     * @param response
     */
    public static void zipFileDownloadByUrl(Set<String> urlPaths, String zipFileName, HttpServletResponse response) {
        if (urlPaths == null || urlPaths.size() <= 0) {
            throw new RuntimeException("待压缩导出文件为空.");
        }
        List<URL> urlList = new ArrayList<>();
        for (String urlPath : urlPaths) {
            try {
                urlList.add(new URL(urlPath));
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
        }
        zipFileDownloadByUrl(urlList, zipFileName, response);
    }

    /**
     * 通过 url 获取文件的名称
     *
     * @param url,如 http://www.baidu.com/img/bd_logo1.png
     * @return
     */
    private static String getNameByUrl(URL url) {
        String name = url.toString();
        int lastIndexOf1 = name.lastIndexOf("/");
        int lastIndexOf2 = name.lastIndexOf("\\");
        if (lastIndexOf1 > 0) {
            name = name.substring(lastIndexOf1 + 1);
        } else if (lastIndexOf2 > 0) {
            name = name.substring(lastIndexOf1 + 2);
        }
        return name;
    }
}

五:IO

xml 复制代码
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

是处理IO的工具类包,对java.io进行扩展,提供了更加方便的IO操作

1:IO常量

java 复制代码
public static final long ONE_KB = 1024;
public static final BigInteger ONE_KB_BI = BigInteger.valueOf(ONE_KB);

public static final long ONE_MB = ONE_KB * ONE_KB;
public static final BigInteger ONE_MB_BI = ONE_KB_BI.multiply(ONE_KB_BI);

private static final long FILE_COPY_BUFFER_SIZE = ONE_MB * 30;

public static final long ONE_GB = ONE_KB * ONE_MB;
public static final BigInteger ONE_GB_BI = ONE_KB_BI.multiply(ONE_MB_BI);

public static final long ONE_TB = ONE_KB * ONE_GB;
public static final BigInteger ONE_TB_BI = ONE_KB_BI.multiply(ONE_GB_BI);

public static final long ONE_PB = ONE_KB * ONE_TB;
public static final BigInteger ONE_PB_BI = ONE_KB_BI.multiply(ONE_TB_BI);

public static final long ONE_EB = ONE_KB * ONE_PB;
public static final BigInteger ONE_EB_BI = ONE_KB_BI.multiply(ONE_PB_BI);

public static final BigInteger ONE_ZB = BigInteger.valueOf(ONE_KB).multiply(BigInteger.valueOf(ONE_EB));

public static final BigInteger ONE_YB = ONE_KB_BI.multiply(ONE_ZB);

public static final File[] EMPTY_FILE_ARRAY = new File[0];

2:API及其常用工具类

所有的API -> https://commons.apache.org/proper/commons-io/apidocs/index.html

IOUtils 通用 IO

IOUtils 与FileUtils 位于同一个包下,FileUtils 底层也是使用 IOUtils

IOUtils 工具类提供方法与 FileUtils 基本类似,只是更面向底层的 OutputStream、InputStream、FileInputStream、FileOutputStream、BufferedOutputStream 等等 IO 流

下面只是简单抽取几个方法 ->

https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/IOUtils.html

java 复制代码
closeQuietly()  
toString()  
copy()  
toByteArray()  
write()  
toInputStream()  
readLines()  
copyLarge()  
lineIterator()  
readFully() 

六:Lang3

xml 复制代码
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

是处理Java基本对象方法的工具类包,该类包提供对字符、数组等基本对象的操作

弥补了java.lang api基本处理方法上的不足。

java 复制代码
ArrayUtils // 用于对数组的操作,如添加、查找、删除、子数组、倒序、元素类型转换等;
BitField // 用于操作位元,提供了一些方便而安全的方法;
BooleanUtils // 用于操作和转换boolean或者Boolean及相应的数组;
CharEncoding // 包含了Java环境支持的字符编码,提供是否支持某种编码的判断;
CharRange // 用于设定字符范围并做相应检查;
CharSet // 用于设定一组字符作为范围并做相应检查;
CharSetUtils // 用于操作CharSet;
CharUtils // 用于操作char值和Character对象;
ClassUtils // 用于对Java类的操作,不使用反射;
ObjectUtils // 用于操作Java对象,提供null安全的访问和其他一些功能;
RandomStringUtils // 用于生成随机的字符串;
SerializationUtils // 用于处理对象序列化,提供比一般Java序列化更高级的处理能力;
StringEscapeUtils // 用于正确处理转义字符,产生正确的Java、JavaScript、HTML、XML和SQL代码;
StringUtils // 处理String的核心类,提供了相当多的功能;
SystemUtils // 在java.lang.System基础上提供更方便的访问,如用户路径、Java版本、时区、操作系统等判断;
Validate // 提供验证的操作,有点类似assert断言;
WordUtils // 用于处理单词大小写、换行等。
相关推荐
isolusion7 分钟前
Springboot的创建方式
java·spring boot·后端
zjw_rp36 分钟前
Spring-AOP
java·后端·spring·spring-aop
忆源37 分钟前
3.3.2.3 开源项目有锁队列实现--魔兽世界tinityCore
开源
鹏大师运维1 小时前
聊聊开源的虚拟化平台--PVE
linux·开源·虚拟化·虚拟机·pve·存储·nfs
Oneforlove_twoforjob1 小时前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
TodoCoder1 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
向宇it1 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行1 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
星河梦瑾2 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富2 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua