<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Sun Blog</title><description>只有走在路上，才能摆脱局限，摆脱执着，让所有的选择、探寻、猜测、想象都生气勃勃</description><link>https://blog.csun.site</link><item><title>基于 Redis 发布／订阅机制的轻量级配置中心</title><link>https://blog.csun.site/blog/2026-03-10-redis-dynamic-configuration-center</link><guid isPermaLink="true">https://blog.csun.site/blog/2026-03-10-redis-dynamic-configuration-center</guid><description>在微服务和分布式系统中，动态配置是非常重要的功能。本文将介绍如何使用 Redis 的发布/订阅机制，结合 Java 注解和反射，实现一个轻量级的动态配置中心。</description><pubDate>Tue, 10 Mar 2026 20:09:00 GMT</pubDate><content:encoded>&lt;p&gt;在微服务和分布式系统中，动态配置是非常重要的功能。本文将介绍如何使用 &lt;strong&gt;Redis 的发布/订阅机制&lt;/strong&gt;，结合 Java 注解和反射，实现一个轻量级的&lt;strong&gt;动态配置中心&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;自定义注解标记动态配置字段&lt;/h2&gt;
&lt;p&gt;首先，我们定义一个自定义注解 &lt;code&gt;@DCCValue&lt;/code&gt;，用于标记需要动态配置的字段。在程序启动时，这些字段会被自动扫描并注册到配置中心。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@Documented
public @interface DCCValue {

    /** Redis 中存储的 key */
    String key();

    /** 默认值 */
    String value();

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当有些字段需要动态去配置时，只需要在这个字段上加上这个注解，并指定对应的 key 和 默认值，例如&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@DCCValue(key = &quot;database_url&quot;, value = &quot;jdbc:mysql://localhost:3306/test&quot;)
private String dbUrl;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;动态配置服务实现&lt;/h2&gt;
&lt;p&gt;我们通过 Java 的反射机制，实现字段值的动态配置。核心类为 &lt;code&gt;DynamicConfigCenterService&lt;/code&gt;，主要提供两个功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;注册 Bean 字段&lt;/strong&gt;：扫描带有 &lt;code&gt;@DCCValue&lt;/code&gt; 注解的字段，并设置初始值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态更新字段&lt;/strong&gt;：当监听到配置更新消息时，刷新对应字段的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;扫描并注册 Bean 字段&lt;/h3&gt;
&lt;p&gt;使用 Spring 的 &lt;code&gt;BeanPostProcessor&lt;/code&gt; 接口，在 Bean 初始化后扫描注解字段，并注册到配置中心。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class DynamicConfigCenterAutoConfig implements BeanPostProcessor {

    private final IDynamicConfigCenterService dynamicConfigCenterService;

    public DynamicConfigCenterAutoConfig(IDynamicConfigCenterService dynamicConfigCenterService) {
        this.dynamicConfigCenterService = dynamicConfigCenterService;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return dynamicConfigCenterService.proxyObject(bean);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;proxyObject(Object bean) &lt;/code&gt; 首先获取 bean 所属的类，注意如果这个 bean 被 AOP 代理了，就需要通过 &lt;code&gt;AopProxyUtils.getTargetClass(bean);&lt;/code&gt; 来获取原始的 Bean。&lt;/p&gt;
&lt;p&gt;然后通过反射获取类上的所有字段，遍历所有字段判断是否有 &lt;code&gt;@DCCValue&lt;/code&gt; 注解，如果存在这个注解，则说明是需要被配置中心管理的字段。&lt;/p&gt;
&lt;p&gt;最后获取注解的 &lt;code&gt;key&lt;/code&gt; 和 &lt;code&gt;value&lt;/code&gt; 值，判断 Redis 中是否存在 &lt;code&gt;key&lt;/code&gt;，如果存在则获取这个 &lt;code&gt;key&lt;/code&gt; 对应的值，更新到字段，否则就使用默认值，并将默认值存入 Redis。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Object proxyObject(Object bean) {
    Class &amp;#x3C;?&gt; targetBeanClass = bean.getClass();
    Object targetBeanObject = bean;
    if (AopUtils.isAopProxy(bean)) {
        targetBeanClass = AopUtils.getTargetClass(bean);
        targetBeanObject = AopProxyUtils.getSingletonTarget(bean);
    }

    Field[] fields = targetBeanClass.getDeclaredFields();
    for (Field field: fields) {
        if (!field.isAnnotationPresent(DCCValue.class)) {
            continue;
        }

        DCCValue dccValue = field.getAnnotation(DCCValue.class);
		
		String key = dccValue.key();
        String value = dccValue.value();

        try {
            // 如果为空则抛出异常
            if (StringUtils.isBlank(value)) {
                throw new RuntimeException(&quot;dcc config error &quot; + key + &quot; is not null - 请配置默认值！&quot;);
            }

            // Redis 操作，判断配置Key是否存在，不存在则创建，存在则获取最新值
            RBucket &amp;#x3C;String&gt; bucket = redissonClient.getBucket(key);
            boolean exists = bucket.isExists();
            if (!exists) {
                bucket.set(value);
            } else {
                value = bucket.get();
            }

            field.setAccessible(true);
            field.set(targetBeanObject, value);
            field.setAccessible(false);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        dccBeanGroup.put(key, targetBeanObject);
    }

    return bean;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后将 bean 放入一个 Map 中，方便下次取用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private final Map&amp;#x3C;String, Object&gt; dccBeanGroup = new ConcurrentHashMap&amp;#x3C;&gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 动态更新字段值&lt;/h3&gt;
&lt;p&gt;当 Redis 发布更新消息时，&lt;code&gt;adjustAttributeValue()&lt;/code&gt; 方法会被调用，接收一个 &lt;code&gt;AttributeVO&lt;/code&gt; 参数，记录了要更新的字段名和对应的值，用于刷新对应字段的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class AttributeVO {
    /** 键 - 属性 filedName */
    private String attribute;

    /** 值 */
    private String value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取到要更新的字段名和对应的值后，先更新 Redis 中相应的值，然后从 Map 中获取到这个字段所属的 bean，使用反射去刷新 bean 的值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void adjustAttributeValue(AttributeVO attributeVO) {
    // 属性信息
    String key = properties.getKey(attributeVO.getAttribute());
    String value = attributeVO.getValue();

    // 设置值
    RBucket &amp;#x3C;String&gt; bucket = redissonClient.getBucket(key);
    boolean exists = bucket.isExists();
    if (!exists) return;
    bucket.set(attributeVO.getValue());

    Object objBean = dccBeanGroup.get(key);
    if (null == objBean) return;

    Class &amp;#x3C;?&gt; objBeanClass = objBean.getClass();
    // 检查 objBean 是否是代理对象
    if (AopUtils.isAopProxy(objBean)) {
        // 获取代理对象的目标对象
        objBeanClass = AopUtils.getTargetClass(objBean);
    }

    try {
        Field field = objBeanClass.getDeclaredField(attributeVO.getAttribute());
        field.setAccessible(true);
        field.set(objBean, value);
        field.setAccessible(false);

        log.info(&quot;DCC 节点监听，动态设置值 {} {}&quot;, key, value);

    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Redis 的订阅/发布机制&lt;/h2&gt;
&lt;p&gt;Redis 的 &lt;code&gt;Pub/Sub&lt;/code&gt; 是一种消息通信机制，用于在不同客户端之间实现消息的实时传递和广播。客户端可以订阅一个或多个频道，当有其他客户端向这些频道发布消息时，所有订阅了该频道的客户端都会立即收到消息。基本命令如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;subscribe channel&lt;/code&gt; 订阅某个频道&lt;/li&gt;
&lt;li&gt;&lt;code&gt;publish channel message&lt;/code&gt; 向某个频道发送消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unsubscribe channel&lt;/code&gt; 取消订阅&lt;/li&gt;
&lt;li&gt;&lt;code&gt;psubscribe pattern&lt;/code&gt; 模式匹配订阅，比如 &lt;code&gt;aaa.*&lt;/code&gt; 能订阅所有 &lt;code&gt;aaa&lt;/code&gt; 开头的频道&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Redisson 使用订阅/发布机制&lt;/h3&gt;
&lt;p&gt;Redisson 提供 &lt;code&gt;RTopic&lt;/code&gt; 对象用于发布和订阅消息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public RTopic dynamicConfigCenterRedisTopic(RedissonClient redissonClient，
	DynamicConfigCenterAdjustListener dynamicConfigCenterAdjustListener) {
	// 获取 Topic
    RTopic topic = redissonClient.getTopic(&quot;TEST&quot;);
	// 添加监听器
    topic.addListener(AttributeVO.class, dynamicConfigCenterAdjustListener);
    return topic;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;监听器是一个实现了 &lt;code&gt;MessageListener&lt;/code&gt; 接口的类，实现 &lt;code&gt;onMessage()&lt;/code&gt; 方法，当有消息的时候会回调 &lt;code&gt;onMessage()&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DynamicConfigCenterAdjustListener implements MessageListener&amp;#x3C;AttributeVO&gt; {

    private final Logger log = LoggerFactory.getLogger(DynamicConfigCenterAdjustListener.class);

    private final IDynamicConfigCenterService dynamicConfigCenterService;

    public DynamicConfigCenterAdjustListener(IDynamicConfigCenterService dynamicConfigCenterService) {
        this.dynamicConfigCenterService = dynamicConfigCenterService;
    }

    @Override
    public void onMessage(CharSequence charSequence, AttributeVO attributeVO) {
        try {
            log.info(&quot;xfg-wrench dcc config attribute:{} value:{}&quot;, attributeVO.getAttribute(), attributeVO.getValue());
            dynamicConfigCenterService.adjustAttributeValue(attributeVO);
        } catch (Exception e) {
            log.error(&quot;xfg-wrench dcc config attribute:{} value:{}&quot;, attributeVO.getAttribute(), attributeVO.getValue(), e);
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当需要更新配置的时候，只需要向频道中发送一条消息即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void test_publish() throws InterruptedException {
    dynamicConfigCenterRedisTopic.publish(new AttributeVO(&quot;downgradeSwitch&quot;, &quot;4&quot;));
    new CountDownLatch(1).await();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?redis-dynamic-configuration-center"/><enclosure url="http://wallpaper.csun.site/?redis-dynamic-configuration-center"/></item><item><title>单例模式的几种写法</title><link>https://blog.csun.site/blog/2026-01-12-singleton-pattern</link><guid isPermaLink="true">https://blog.csun.site/blog/2026-01-12-singleton-pattern</guid><description>介绍在 Java 中单例模式的几种写法</description><pubDate>Mon, 12 Jan 2026 19:33:00 GMT</pubDate><content:encoded>&lt;p&gt;在 Java 中，单例模式（Singleton Pattern）是一种常用的设计模式，用于确保一个类在整个应用中&lt;strong&gt;只有一个实例&lt;/strong&gt;，并提供全局访问点。单例模式主要有「饿汉式」和「懒汉式」两种写法。&lt;/p&gt;
&lt;h2&gt;饿汉式&lt;/h2&gt;
&lt;p&gt;饿汉式是不管需不需要，直接在类加载的时候就会创建实例，可能造成资源浪费，但是实现简单，天热线程安全。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Singleton {
    // 饿汉式，类加载时就初始化
    private static final Singleton INSTANCE = new Singleton();

    // 私有化构造方法，防止外部实例化
    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;懒汉式&lt;/h2&gt;
&lt;p&gt;懒汉式则是延迟加载，只有需要使用这个实例的时候，才会去创建实例，主要有下面这几种写法&lt;/p&gt;
&lt;h3&gt;线程不安全的懒汉式&lt;/h3&gt;
&lt;p&gt;这种方式可以实现懒加载，在使用的时候才创建实例，但是线程不安全，在多线程情况下可能会创建多个实例。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Singleton {
    private static Singleton INSTANCE;

    // 私有化构造方法，防止外部实例化
    private Singleton() {}

    public static Singleton getInstance() {
        if(INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;线程安全的懒汉式&lt;/h3&gt;
&lt;p&gt;这种方式通过 &lt;code&gt;synchronized&lt;/code&gt; 锁解决了线程安全问题大，但是每次获取实例的时候都要加锁，性能较低。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Singleton {
    private static Singleton INSTANCE;

    // 私有化构造方法，防止外部实例化
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if(INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双重检查锁&lt;/h3&gt;
&lt;p&gt;这种方式实现了懒加载且线程安全，并且只有在实例没有创建时才进入同步块，减少每次调用都加锁的性能开销。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Singleton {
    // 饿汉式，类加载时就初始化
    private static volatile Singleton INSTANCE;

    // 私有化构造方法，防止外部实例化
    private Singleton() {}

    public static Singleton getInstance() {
        if(INSTANCE == null) { // 第一次检查
            synchronized (Singleton.class) {
                if(INSTANCE == null) { // 第二次检查
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;为什么要双重检查？&lt;/h4&gt;
&lt;p&gt;可能线程 1 第一次检查的时候发现没有实例，开始抢锁，此时线程 2 恰好创建了实例，释放了锁，如果不做第二次检查，线程 1 又会创建一次实例&lt;/p&gt;
&lt;h4&gt;为什么要用 volatile 关键字修饰&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;volatile&lt;/code&gt; 关键字是为了避免指令重排序带来的线程安全问题。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;INSTANCE = new Singleton();&lt;/code&gt; 这条语句，在 JVM 里面可能被拆成三步：&lt;/p&gt;
&lt;p&gt;（1）分配内存空间&lt;/p&gt;
&lt;p&gt;（2）初始化对象&lt;/p&gt;
&lt;p&gt;（3）将对象引用赋值给 &lt;code&gt;INSTANCE&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果不使用 &lt;code&gt;volatile&lt;/code&gt; 关键字，可能导致（2）（3）两步重排序，另一个线程在第一次检查时可能看到 &lt;code&gt;INSTANCE != null&lt;/code&gt;，但对象还没初始化完成，导致&lt;strong&gt;访问未初始化对象。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;静态内部类（推荐）&lt;/h3&gt;
&lt;p&gt;利用静态内部类和 JVM 的类加载机制，可以实现天然线程安全的懒汉式单例，并且代码简单，十分推荐&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;外部类 &lt;code&gt;Singleton&lt;/code&gt; 加载时，并不会立即加载静态内部类 &lt;code&gt;Holder&lt;/code&gt;，只有在 &lt;strong&gt;第一次调用&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;getInstance()&lt;/code&gt;&lt;/strong&gt;  时，才会触发 &lt;code&gt;Holder&lt;/code&gt; 类的加载。&lt;/p&gt;
&lt;p&gt;根据 JVM 规范，&lt;strong&gt;类加载的过程是线程安全的，&lt;/strong&gt; 类在加载和初始化时，JVM 会保证 &lt;strong&gt;只有一个线程去执行静态初始化，&lt;/strong&gt; 其他线程必须等待，从而确保 &lt;code&gt;INSTANCE&lt;/code&gt; 只会被创建一次。&lt;/p&gt;
&lt;h2&gt;拓展：如何实现一个分布式单例对象&lt;/h2&gt;
&lt;p&gt;普通的单例模式的进程类唯一，而分布式单例对象需要跨多个进程唯一，要解决的问题主要有两点：&lt;/p&gt;
&lt;p&gt;（1）对象得放到外部存储中，让所有进程都能访问到；&lt;/p&gt;
&lt;p&gt;（2）要保证同一时刻只能有一个进程创建该对象。&lt;/p&gt;
&lt;p&gt;为此可以使用 redis 来存放对象，并借助 redis 实现分布式锁来保证同一时刻只能有一个进程创建该对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DistributedSingleton&amp;#x3C;T&gt; {
    private final RedissonClient redisson;
    private final String lockKey;
    private final String dataKey;
    private final Supplier&amp;#x3C;T&gt; creator;
    private final Class&amp;#x3C;T&gt; clazz;

    public T getInstance() {
        // 先尝试获取对象
        String data = redisson.getBucket(dataKey).get();
        if (data != null) {
            return JSON.parseObject(data, clazz);
        }
        
        // 对象不存在则尝试获取锁去创建对象
        RLock lock = redisson.getLock(lockKey);
        try {
            lock.lock();
            // 双重检查
            data = redisson.getBucket(dataKey).get();
            if (data != null) {
                return JSON.parseObject(data, clazz);
            }
            // 创建并存储
            T instance = creator.get();
            redisson.getBucket(dataKey).set(JSON.toJSONString(instance));
            return instance;
        } finally {
            lock.unlock();
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?SingletonPattern"/><enclosure url="http://wallpaper.csun.site/?SingletonPattern"/></item><item><title>打家劫舍问题</title><link>https://blog.csun.site/blog/2025-12-03-house-robber</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-12-03-house-robber</guid><description>一文弄懂打家劫舍系列问题</description><pubDate>Wed, 03 Dec 2025 13:14:12 GMT</pubDate><content:encoded>&lt;h2&gt;基础打家劫舍&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;你是一个专业的小偷，计划偷窃沿街的房屋。每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，&lt;strong&gt;如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;给定一个代表每个房屋存放金额的非负整数数组，计算你不触动警报装置的情况下，一夜之内能够偷窃到的最高金额。&lt;/p&gt;
&lt;h3&gt;题解&lt;/h3&gt;
&lt;p&gt;设 dp[i] 为只考虑前 i 间房屋能偷到的最高金额，对于第 i 间房屋&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不偷第 i 间，相当于只考虑前 i-1 间房屋，则 dp[i] = dp[i-1]&lt;/li&gt;
&lt;li&gt;如果偷第 i 间，则不能偷第 i-1 间房屋，则 dp[i] = dp[i-2] + nums[i]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两者取较大值，所以 &lt;code&gt;dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i])&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public int rob(int[] nums) {
        int n = nums.length;

        if(n == 1)
            return nums[0];

        int[] dp = new int[n];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);

        for(int i = 2; i &amp;#x3C; n; i++)
            dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);

        return dp[n-1];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;空间优化&lt;/h3&gt;
&lt;p&gt;dp[i] 只需要从 dp[i-1] 和 dp[i-2] 推导得到，所以可以只用两个变量记录 dp[i-1] 和 dp[i-2]&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public int rob(int[] nums) {

        int n = nums.length;

        if(n == 1)
            return nums[0];
        
        int f0 = nums[0], f1 = Math.max(nums[0], nums[1]);

        for(int i = 2; i &amp;#x3C; n; i++) {
            int newF = Math.max(f1, f0 + nums[i]);
            f0 = f1;
            f1 = newF;
        }
            

        return f1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;环形数组打家劫舍&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/house-robber-ii/description/&quot;&gt;213. 打家劫舍 II - 力扣（LeetCode）&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;你是一个专业的小偷，计划偷窃沿街的房屋，每间房内都藏有一定的现金。这个地方所有的房屋都 &lt;strong&gt;围成一圈&lt;/strong&gt; ，这意味着第一个房屋和最后一个房屋是紧挨着的。同时，相邻的房屋装有相互连通的防盗系统，&lt;strong&gt;如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;给定一个代表每个房屋存放金额的非负整数数组，计算你 &lt;strong&gt;在不触动警报装置的情况下&lt;/strong&gt; ，今晚能够偷窃到的最高金额。&lt;/p&gt;
&lt;h3&gt;题解&lt;/h3&gt;
&lt;p&gt;这道题与打家劫舍的区别就是所有的房屋都 &lt;strong&gt;围成一圈，是一个环形数组，要考虑第 1 间房屋和第 n 间房屋不能同时偷窃的问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以分成两种情况分别计算，然后取两者最大值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;偷第 0 间房屋，则不能偷第 n 间房子，转换成在 &lt;code&gt;nums[:n-1]&lt;/code&gt; 上的打家劫舍问题&lt;/li&gt;
&lt;li&gt;偷第 n 间房子，则不能偷第 0 间房子，转换成在 &lt;code&gt;nums[1:]&lt;/code&gt; 上的打家劫舍问题&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n == 1)
            return nums[0];
        else if(n == 2)
            return Math.max(nums[0], nums[1]);

        return Math.max(robRange(nums, 0, n - 1), robRange(nums, 1, n));
    }

    public static int robRange(int[] nums, int left, int right) {
        if(left == right)
            return nums[left];

        int[] dp = new int[nums.length];
        dp[left] = nums[left];
        dp[left + 1] = Math.max(nums[left], nums[left + 1]);

        for(int i = left + 2; i &amp;#x3C; right; i++) {
            dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
        }

        return dp[right - 1];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;值域打家劫舍&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-total-damage-with-spell-casting/description/&quot;&gt;3186. 施咒的最大总伤害 - 力扣（LeetCode）&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;一个魔法师有许多不同的咒语。&lt;/p&gt;
&lt;p&gt;给你一个数组 &lt;code&gt;power&lt;/code&gt; ，其中每个元素表示一个咒语的伤害值，可能会有多个咒语有相同的伤害值。&lt;/p&gt;
&lt;p&gt;已知魔法师使用伤害值为 &lt;code&gt;power[i]&lt;/code&gt; 的咒语时，他们就 &lt;strong&gt;不能&lt;/strong&gt; 使用伤害为 &lt;code&gt;power[i] - 2&lt;/code&gt; ，&lt;code&gt;power[i] - 1&lt;/code&gt; ，&lt;code&gt;power[i] + 1&lt;/code&gt; 或者 &lt;code&gt;power[i] + 2&lt;/code&gt; 的咒语。&lt;/p&gt;
&lt;p&gt;每个咒语最多只能被使用 &lt;strong&gt;一次&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;请你返回这个魔法师可以达到的伤害值之和的 &lt;strong&gt;最大值&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;题解&lt;/h3&gt;
&lt;p&gt;对于每一个咒语，只要用了，与其伤害值相同的咒语肯定都会使用，所以对于同一伤害值的咒语用不用只需要考虑一次，然后计算伤害的伤害将其伤害值乘上出现次数即可&lt;/p&gt;
&lt;p&gt;使用了 &lt;code&gt;power[i]&lt;/code&gt; 就不能使用 &lt;code&gt;power[i] - 2&lt;/code&gt; ，&lt;code&gt;power[i] - 1&lt;/code&gt; ，&lt;code&gt;power[i] + 1&lt;/code&gt; 或者 &lt;code&gt;power[i] + 2&lt;/code&gt; ，典型的打家劫舍问题&lt;/p&gt;
&lt;p&gt;可以将所有出现的伤害值排序，然后设 dp[i] 为只考虑前 i 个咒语的最大伤害值&lt;/p&gt;
&lt;p&gt;因为排完序了，i 前面只有比 i 小的咒语，只需要考虑 &lt;code&gt;power[i] - 2&lt;/code&gt; ，&lt;code&gt;power[i] - 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;dp[i] = Math.max(dp[i-1], dp[j] + power[i] * count)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;j 为小于 &lt;code&gt;power[i]-2&lt;/code&gt; 的最大伤害值的下标，count 为 &lt;code&gt;power[i]&lt;/code&gt; 出现的次数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public long maximumTotalDamage(int[] power) {
        Map&amp;#x3C;Integer, Integer&gt; map = new HashMap&amp;#x3C;&gt;();

        // 使用 map 存储所有伤害值出现的次数
        for(int x : power) {
            map.merge(x, 1, Integer::sum);
        }

        int n = map.size();
        // 存储所有出现的伤害值
        int[] a = new int[n];
        int cnt = 0;
        for(int x : map.keySet()) {
            a[cnt++] = x;
        }
        Arrays.sort(a); // 排序

        long[] dp = new long[n];
        dp[0] = (long) a[0] * map.get(a[0]);
        int j = 0;  // j - 1 指向第一个小于 a[i] - 2 的伤害值
        for(int i = 1; i &amp;#x3C; n; i++) {
            while(a[j] &amp;#x3C; a[i] - 2)
                j++;
            if(j == 0) // 说明找不到比 a[i] - 2 小的伤害值
                dp[i] = Math.max(dp[i-1], (long) a[i] * map.get(a[i]));
            else
                dp[i] = Math.max(dp[i-1], dp[j-1] + (long) a[i] * map.get(a[i]));
        }

        return dp[n-1];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?dajiajieshe"/><enclosure url="http://wallpaper.csun.site/?dajiajieshe"/></item><item><title>并查集</title><link>https://blog.csun.site/blog/2025-11-20-unionfind</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-11-20-unionfind</guid><description>并查集的基本原理和 Java 代码实现</description><pubDate>Thu, 20 Nov 2025 13:14:12 GMT</pubDate><content:encoded>&lt;p&gt;并查集是一种支持&lt;strong&gt;快速合并集合、判断元素是否属于同一集合&lt;/strong&gt;的数据结构&lt;/p&gt;
&lt;h2&gt;基本原理&lt;/h2&gt;
&lt;p&gt;并查集将每个集合用一颗&lt;strong&gt;树&lt;/strong&gt;来表示，&lt;strong&gt;使用一个一维数组来记录每个节点的父节点&lt;/strong&gt;，根节点的父节点是它自己，例如&lt;/p&gt;
&lt;p&gt;如果需要合并两个集合，只需要找到两个集合的根节点，&lt;strong&gt;将其中一个根节点设为另外一个根节点的父节点&lt;/strong&gt;即可&lt;/p&gt;
&lt;p&gt;合并操作的代码如下，首先查找 &lt;code&gt;u&lt;/code&gt;​ 和 &lt;code&gt;v&lt;/code&gt;​ 两个节点所在集合的根节点，如果根节点相同说明他俩在同一个集合中，不需要合并，否则设置 &lt;code&gt;father[u] = v&lt;/code&gt;​，将 &lt;code&gt;u&lt;/code&gt;​ 的根节点的父节点设置为 &lt;code&gt;v&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean union(int u, int v) {
    u = find(u);
    v = find(v);
    if (u == v)
        return false;
    father[u] = v;
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数同样可以用于判断 &lt;code&gt;u&lt;/code&gt;​ 和 &lt;code&gt;v&lt;/code&gt;​ 两个节点是不是在同一个集合中，如果返回 &lt;code&gt;false&lt;/code&gt; 说明他俩在同一个集合中&lt;/p&gt;
&lt;p&gt;寻找根节点的过程可以通过&lt;strong&gt;递归查询&lt;/strong&gt; &lt;strong&gt;​&lt;code&gt;father&lt;/code&gt;​&lt;/strong&gt;​ &lt;strong&gt;数组&lt;/strong&gt;来实现，根节点的父节点它自己，所以递归的终止条件是  &lt;code&gt;x = father[x]&lt;/code&gt;，代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int find(int x) {
    if (x != father[x]) 
		return find(father[x]);
    return father[x]; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化的时候，可以认为每个节点都是一个独立的集合，都是根节点，父节点是自己&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void init() {
    for (int i = 0; i &amp;#x3C; n; ++i) {
        father[i] = i;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;路径压缩&lt;/h2&gt;
&lt;p&gt;在寻找根节点的过程，如果这颗树的高度很深的话，每次都要递归很多次，但是我们只需要知道这些节点在同一个根下即可，所以可以对树进行下图所示的优化，这就是&lt;strong&gt;路径压缩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;具体代码实现，我们只需要在递归的过程中，让 &lt;code&gt;father[x]&lt;/code&gt;​ 接住递归函数 &lt;code&gt;find(father[x])&lt;/code&gt;​ 的返回结果，&lt;code&gt;find(father[x])&lt;/code&gt; 返回的是根节点，这样就相当于让根节点成为当前节点的父节点&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int find(int x) {
    if (x != father[x]) 
		father[x] = find(father[x]);
    return father[x]; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;按秩合并&lt;/h2&gt;
&lt;p&gt;合并两个集合的时候，让较「矮」的树挂到较「高」的树上，可以避免树变得太深，进一步优化递归查找根节点的效率，但是需要额外维护一个 &lt;code&gt;size[]&lt;/code&gt; 数组，记录每个集合的高度&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean union(int a, int b) {
    int pa = find(a), pb = find(b);
    if (pa == pb) {
        return false;
    }
    if (size[pa]&gt; size[pb]) {
        father[pb] = pa;
        size[pa] += size[pb];
    } else {
        father[pa] = pb;
        size[pb] += size[pa];
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完整模板&lt;/h2&gt;
&lt;p&gt;并查集的完整模板如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class UnionFind {
    private final int[] father;
    private final int[] size;

    public UnionFind(int n) {
        father = new int[n];
        size = new int[n];
        for (int i = 0; i &amp;#x3C; n; ++i) {
            father[i] = i;
            size[i] = 1;
        }
    }

    public int find(int x) {
        if (father[x] != x) {
            father[x] = find(father[x]);
        }
        return father[x];
    }

    public boolean union(int a, int b) {
        int pa = find(a), pb = find(b);
        if (pa == pb) {
            return false;
        }
        if (size[pa] &gt; size[pb]) {
            father[pb] = pa;
            size[pa] += size[pb];
        } else {
            father[pa] = pb;
            size[pb] += size[pa];
        }
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;应用&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://kamacoder.com/problempage.php?pid=1179&quot;&gt;寻找存在的路径&lt;/a&gt;&lt;/h3&gt;
&lt;h4&gt;题目描述&lt;/h4&gt;
&lt;p&gt;给定一个包含 n 个节点的无向图中，节点编号从 1 到 n （含 1 和 n ）。&lt;/p&gt;
&lt;p&gt;你的任务是判断是否有一条从节点 source 出发到节点 destination 的路径存在。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第一行包含两个正整数 N 和 M，N 代表节点的个数，M 代表边的个数。&lt;/p&gt;
&lt;p&gt;后续 M 行，每行两个正整数 s 和 t，代表从节点 s 与节点 t 之间有一条边。&lt;/p&gt;
&lt;p&gt;最后一行包含两个正整数，代表起始节点 source 和目标节点 destination。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个整数，代表是否存在从节点 source 到节点 destination 的路径。如果存在，输出 1；否则，输出 0。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5 4
1 2
1 3
2 4
3 4
1 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;数据范围&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1 &amp;#x3C;= M, N &amp;#x3C;= 100。&lt;/p&gt;
&lt;h4&gt;题解&lt;/h4&gt;
&lt;p&gt;并查集模板题，如果节点 s 和 节点 t 之间有一条边，则将 s 和 t 所在的集合合并，最后查询的时候只需要判断两个节点是否在同一个集合中即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.util.*;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt(), m = sc.nextInt();

        int[] father = new int[n+1];
        for(int i = 1; i &amp;#x3C;= n; i++)
            father[i] = i;

        for(int i = 0; i &amp;#x3C; m; i++)
            add(father, sc.nextInt(), sc.nextInt());

        int u = sc.nextInt(), v = sc.nextInt();
        if(find(father, u) == find(father, v))
            System.out.print(&quot;1&quot;);
        else
            System.out.print(&quot;0&quot;);
    }

    public static void add(int[] father, int u, int v) {
        u = find(father, u);
        v = find(father, v);
        if(u == v)
            return;
        father[u] = v;

    }


    public static int find(int[] father, int u) {
        if (father[u] != u) {
            father[u] = find(father, father[u]);
        }
        return father[u];
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a href=&quot;https://kamacoder.com/problempage.php?pid=1182&quot;&gt;冗余连接 II&lt;/a&gt;&lt;/h3&gt;
&lt;h4&gt;题目描述&lt;/h4&gt;
&lt;p&gt;在本问题中，有根树指满足以下条件的有向图。该树只有一个根节点，所有其他节点都是该根节点的后继。该树除了根节点之外的每个节点都只有一个父节点，而根节点没有父节点。&lt;/p&gt;
&lt;p&gt;输入一个有向图，该图由一个有 $n$ 个节点（节点值不重复，从 $1$ 到 $n$）的树及一条附加的有向边构成。附加的边包含在 $1$ 到 $n$ 中的两个不同顶点间，这条附加的边不属于树中已存在的边。&lt;/p&gt;
&lt;p&gt;结果图是一个以边组成的二维数组 &lt;code&gt;edges&lt;/code&gt;​。每个元素是一对 &lt;code&gt;[u_i, v_i]&lt;/code&gt;​，用以表示 有向图中连接顶点 &lt;code&gt;u_i&lt;/code&gt;​ 和顶点 &lt;code&gt;v_i&lt;/code&gt;​ 的边，其中 &lt;code&gt;u_i&lt;/code&gt;​ 是 &lt;code&gt;v_i&lt;/code&gt; 的一个父节点。&lt;/p&gt;
&lt;p&gt;返回一条能删除的边，使得剩下的图是有 $n$ 个节点的有根树。若有多个答案，返回最后出现在给定二维数组的答案。&lt;/p&gt;
&lt;p&gt;示例 1:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/75dcd8b3ebf9edd642481a31aa836f8d.png&quot; alt=&quot;image&quot;&gt;
输入: &lt;code&gt;edges = [[1,2],[1,3],[2,3]]&lt;/code&gt;
输出: &lt;code&gt;[2,3]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;示例 2:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/b25c7d7b7aa7e411f6b992605bee178b.png&quot; alt=&quot;image&quot;&gt;
输入: &lt;code&gt;edges = [[1,2],[2,3],[3,4],[4,1],[1,5]]&lt;/code&gt;
输出: &lt;code&gt;[4,1]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;提示:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;n == edges.length&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;$3 \le n \le 1000$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;edges[i].length == 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;$1 \le u_i, v_i \le n$&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;题解&lt;/h4&gt;
&lt;p&gt;对于一颗 n 个节点，n-1 条边的有向树，添加一条有向边可能有以下三种情况：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（1）有一个节点的入度变成 2，但是没有形成环&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这种情况下删除 &lt;code&gt;1-3&lt;/code&gt;​ 或者 &lt;code&gt;2-3&lt;/code&gt; 两条边均可，题目要求删除标准输入中最后出现的一条边&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（2）有一个节点的入度变成 2，但是形成了环&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这种情况下只能删除 &lt;code&gt;3-&gt;2&lt;/code&gt;​ 这条边，如果删除 &lt;code&gt;1-&gt;2&lt;/code&gt; 这条边就会形成一个环&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（3）多余的边指向根节点形成了环，没有入度为 2 的节点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这种情况下删除任意一条边均可，按题目要求删除标准输入中最后出现的一条边&lt;/p&gt;
&lt;p&gt;所以，可以先计算每个节点的入度，找出是否存在入度为 2 节点，如果存在，就属于情况 （1）和（2），用一个并查集维护节点直接的连通性。如果不存在，则说明是情况（3），完整代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {

    static class UnionFind {
        private final int[] p;

        public UnionFind(int n) {
            this.p = new int[n+1];

            for(int i = 1; i &amp;#x3C;= n; i++)
                this.p[i] = i;
        }

        public int find(int x) {
            if(p[x] != x) {
                p[x] = find(p[x]);
            }

            return p[x];
        }

        public boolean add(int u, int v) {
            u = find(u);
            v = find(v);
            if(u == v)
                return false;
            p[u] = v;
            return true;
        } 
    }

    public int[] findRedundantDirectedConnection(int[][] edges) {
        int n = edges.length;
        int[] in = new int[n+1];  // 记录每个节点的入度

        List&amp;#x3C;Integer&gt; doubleInNode = new ArrayList&amp;#x3C;&gt;(); 

        for(int i = 0; i &amp;#x3C; n; i++) {
            in[edges[i][1]] ++;  // 统计每个节点的入度
        }

        for(int i = 0; i &amp;#x3C; n; i++) {
            if(in[edges[i][1]] &gt;= 2)
                doubleInNode.add(i);  // 记录入度为 2 的边
        }

        UnionFind uf = new UnionFind(n); // 并查集

        if(!doubleInNode.isEmpty()) {
            // 存在入度为 2 的节点
            for(int i = 0; i &amp;#x3C; n; i++) {
                if(i == doubleInNode.get(1))
                    continue;  // 假删除这条边，不加入并查集

                if(!uf.add(edges[i][0], edges[i][1]))  // 检查是否有环
                    return edges[doubleInNode.get(0)];
            }

            return edges[doubleInNode.get(1)];
        }

        for(int i = 0; i &amp;#x3C; n; i++) {
            if(!uf.add(edges[i][0], edges[i][1]))  // 检查是否有环
                return edges[i];
        }

        return new int[n];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;‍&lt;/h6&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?UnionFind"/><enclosure url="http://wallpaper.csun.site/?UnionFind"/></item><item><title>Java 字符串常量池详解</title><link>https://blog.csun.site/blog/2025-11-18-stringtable</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-11-18-stringtable</guid><description>系统解析 Java 字符串常量池的结构与演变</description><pubDate>Tue, 18 Nov 2025 17:14:12 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;字符串常量池&lt;/strong&gt; 是 JVM 为了提升性能和减少内存消耗针对字符串（String 类）专门开辟的一块区域，主要目的是为了避免字符串的重复创建。&lt;/p&gt;
&lt;p&gt;HotSpot 中字符串常量池的实现是 &lt;code&gt;StringTable&lt;/code&gt;，本质是一个固定大小的 &lt;code&gt;HashTable&lt;/code&gt;，容量为 &lt;code&gt;StringTableSize&lt;/code&gt;（可以通过 &lt;code&gt;-XX:StringTableSize&lt;/code&gt; 参数来设置）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;StringTable&lt;/code&gt; 保存的是字符串（key）和 &lt;strong&gt;字符串对象引用&lt;/strong&gt;（value）的映射关系&lt;/p&gt;
&lt;h2&gt;字符串常量池的位置&lt;/h2&gt;
&lt;p&gt;在 JDK1.7 以前，字符串常量池存放在方法区（ HotSpot 虚拟机中的永久代）中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/524cd02b5b923674b87f8f86abead551.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;使用以下代码可以验证 JDK1.6 中字符串常量池在永久代中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) throws InterruptedException {
    List&amp;#x3C;String&gt; list = new ArrayList&amp;#x3C;&gt;();
    int i = 0;
    try {
        for (int j = 0; j &amp;#x3C; 260000; j++) {
            list.add(String.valueOf(j).intern());
            i++;
        }
    } catch (Throwable e) {
        e.printStackTrace();
    } finally {
        System.out.println(i);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 JDK 1.6 环境下运行上述代码，使用参数 &lt;code&gt;-XX:MaxPermSize=10m&lt;/code&gt; 设置永久代大小为 &lt;code&gt;10M&lt;/code&gt;，当产生大量字符串存储到常量池中后，永久代会发生内存溢出问题，输出如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at cn.itcast.jvm.Demo1_6.main(Demo1_6.java from InputFileObject:18)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PermGen space&lt;/code&gt; 说明永久代发生了内存溢出问题，证明在 JDK 1.6 中字符串常量池存放在永久代中&lt;/p&gt;
&lt;p&gt;到 JDK 1.7 以后，字符串常量池就从永久代移动到了堆中。主要是因为永久代的 GC 回收效率太低，只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收，将字符串常量池放到堆中，能够&lt;strong&gt;更高效及时地回收字符串内存&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/7720480f58534c6c7d500ba74f26b887.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;同样在 JDK 1.8 环境下运行上述代码，使用参数 &lt;code&gt;-Xmx10m&lt;/code&gt; 设置堆的大小为 &lt;code&gt;10M&lt;/code&gt;，使用参数 &lt;code&gt;-XX:-UseGCOverheadLimit&lt;/code&gt; 关闭 GC Overhead Limit 检查，会得到以下输出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;java.lang.OutOfMemoryError: Java heap space
	at java.lang.Integer.toString(Integer.java:401)
	at java.lang.String.valueOf(String.java:3099)
	at jvm.JVMStack.main(JVMStack.java:17)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Java heap space&lt;/code&gt; 说明发生了堆内存溢出，证明了 JDK 1.7 以后字符串常量池移动到了堆内存中&lt;/p&gt;
&lt;h2&gt;字符串何时进入 StringTable&lt;/h2&gt;
&lt;p&gt;对于字符串字面量而言，编译后会被存放到 Class 文件的常量池表中，例如下面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TestString {
    public static void main(String[] args) {
        System.out.println(&quot;Hello Wolrd&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;javap -v TestString.class&lt;/code&gt; 命令输出反编译结果可以看到 &lt;code&gt;&quot;Hello World&quot;&lt;/code&gt; 被存储在 &lt;code&gt;Constant pool&lt;/code&gt; 常量池表中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/3051c9ba89bef87c3d8cc6d60ee3998e.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Class 文件中每条指令都会对应常量池表中一个地址，常量池表中的地址可能对应着一个类名、方法名、参数类型等信息，JVM 在类加载的解析阶段会把这些地址转换为真实的内存地址&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/e1f782f8e73700f12b7ee0bd6b0d84a3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是字符串存在&lt;strong&gt;懒加载&lt;/strong&gt;机制，会在实际使用的时候才会创建对象，进入 StringTable&lt;/p&gt;
&lt;p&gt;通过下面的代码在 IDEA 的 debug 模式下可以验证这一点&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    System.out.println(&quot;1&quot;);
    System.out.println(&quot;2&quot;);
    System.out.println(&quot;3&quot;);
    System.out.println(&quot;4&quot;);
    System.out.println(&quot;5&quot;);
    System.out.println(&quot;1&quot;);
    System.out.println(&quot;2&quot;);
    System.out.println(&quot;3&quot;);
    System.out.println(&quot;4&quot;);
    System.out.println(&quot;5&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始 &lt;code&gt;String&lt;/code&gt; 的数量为 2100&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/6748b845db9724e6f55f874d90d0b5fc.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;代码每往下执行一步，&lt;code&gt;String&lt;/code&gt; 的数量加一&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/5715d4fccc51b5f2bee86550fc7a82af.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;执行到重复的字符串时，&lt;code&gt;String&lt;/code&gt; 数量不再增加&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/79cff8a28a100d7255b7984166a57f52.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/f6527414ef8bed64201b8c11d208f855.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这证明了字符串是在实际使用的时候才进入 &lt;code&gt;StringTable&lt;/code&gt;，并且 &lt;code&gt;StringTable&lt;/code&gt; 中的字符串是重复使用的&lt;/p&gt;
&lt;h2&gt;字符串变量拼接&lt;/h2&gt;
&lt;p&gt;假设有如下代码，拼接了两个字符串变量 &lt;code&gt;s1&lt;/code&gt; 和 &lt;code&gt;s2&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    String s1 = &quot;a&quot;;
    String s2 = &quot;b&quot;;
    String s3 = s1 + s2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看这段代码的反编译结果&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/c44c0ebee7ac8e63deda322aa2f28d85.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;从反编译结果可以看出，字符串变量的拼接被编译器优化成使用 &lt;code&gt;StringBuilder&lt;/code&gt; 进行拼接，最后调用 &lt;code&gt;StringBuilder&lt;/code&gt; 的 &lt;code&gt;toString()&lt;/code&gt; 方法转换为字符串。&lt;/p&gt;
&lt;p&gt;下面的代码会创建几个对象？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String s3 = new String(&quot;a&quot;) + new String(&quot;b&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先 &lt;strong&gt;&lt;code&gt;new String(&quot;a&quot;)&lt;/code&gt;&lt;/strong&gt;  &lt;strong&gt;会在&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;StringTable&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;中寻找有没有&lt;/strong&gt;  &lt;strong&gt;&lt;code&gt;&quot;a&quot;&lt;/code&gt;&lt;/strong&gt;  &lt;strong&gt;，如果没有会创建一个&lt;/strong&gt;  &lt;strong&gt;&lt;code&gt;&quot;a&quot;&lt;/code&gt;&lt;/strong&gt;  &lt;strong&gt;，并将其引用放入&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;StringTable&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;中，如果存在则不会创建&lt;/strong&gt;；然后再在堆上创建一个对象 &lt;code&gt;new String(&quot;a&quot;)&lt;/code&gt;；&lt;code&gt;new String(&quot;b&quot;)&lt;/code&gt; 同理。&lt;/p&gt;
&lt;p&gt;这两个字符串对象的拼接过程会被优化成使用 &lt;code&gt;StringBuilder&lt;/code&gt; 进行拼接，会创建一个 &lt;code&gt;StringBuilder&lt;/code&gt; 对象。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;StringBuilder&lt;/code&gt; 的 &lt;code&gt;toString()&lt;/code&gt; 方法如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public String toString() {
    // Create a copy, don&apos;t share the array
    return new String(value, 0, count);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个方法中又会在堆上创建一个新的 &lt;code&gt;String(&quot;ab&quot;)&lt;/code&gt; 对象，注意这&lt;strong&gt;里不涉及到字面量&lt;/strong&gt;  &lt;strong&gt;&lt;code&gt;&quot;ab&quot;&lt;/code&gt;&lt;/strong&gt;  &lt;strong&gt;，所以不会在&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;StringTable&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;中创建对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;综上，这段代码最多会创建 6 个对象，最少会创建 4 个对象&lt;/p&gt;
&lt;h2&gt;字符串常量拼接&lt;/h2&gt;
&lt;p&gt;假设有如下代码，拼接了两个字符串常量 &lt;code&gt;&quot;a&quot;&lt;/code&gt; 和 &lt;code&gt;&quot;b&quot;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    String s1 = &quot;a&quot; + &quot;b&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察反编译结果可以发现，这个拼接过程直接被编译器优化成了字面量 &lt;code&gt;&quot;ab&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/11/9c295d016f4d32b151c80cec8c8a9528.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;intern 方法&lt;/h2&gt;
&lt;p&gt;调用 &lt;code&gt;String&lt;/code&gt; 的 &lt;code&gt;intern()&lt;/code&gt; 方法会将该字符串对象尝试放入到 &lt;code&gt;StringTable&lt;/code&gt; 中，如果 &lt;code&gt;StringTable&lt;/code&gt; 中没有该字符串对象，则放入成功，有则放入失败，&lt;strong&gt;无论放入成功或者失败，都会返回&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;StringTable&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;中的字符串对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 JDK1.8 的环境下看下面的代码会输出什么&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    String str = new String(&quot;a&quot;) + new String(&quot;b&quot;);
    String st2 = str.intern();
    String str3 = &quot;ab&quot;;
    System.out.println(str == st2);
    System.out.println(str == str3);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先 &lt;code&gt;str&lt;/code&gt; 是在堆上被创建，然后调用其 &lt;code&gt;intern()&lt;/code&gt; 方法尝试将其放入到 &lt;code&gt;StringTable&lt;/code&gt; 中，此时 &lt;code&gt;StringTable&lt;/code&gt; 没有这个对象，放入成功&lt;/p&gt;
&lt;p&gt;在 JDK1.8 中这个放入的过程是&lt;strong&gt;将堆中字符串对象的引用直接放入到&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;StringTable&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;中&lt;/strong&gt;，&lt;code&gt;str&lt;/code&gt; 和 &lt;code&gt;StringTable&lt;/code&gt; 中的 &lt;code&gt;&quot;ab&quot;&lt;/code&gt; 指向同一个对象&lt;/p&gt;
&lt;p&gt;&lt;code&gt;str3&lt;/code&gt; 会直接返回 &lt;code&gt;StringTable&lt;/code&gt; 中的 &lt;code&gt;&quot;ab&quot;&lt;/code&gt;，所以两个输出语句输出的都是 &lt;code&gt;true&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果将 &lt;code&gt;String str3 = &quot;ab&quot;;&lt;/code&gt; 放到前面&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    String str3 = &quot;ab&quot;;
    String str = new String(&quot;a&quot;) + new String(&quot;b&quot;);
    String str2 = str.intern();
    System.out.println(str == str2);
    System.out.println(str == str3);
    System.out.println(str2 == str3);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用 &lt;code&gt;intern()&lt;/code&gt; 方法时 &lt;code&gt;StringTable&lt;/code&gt; 中已经有了 &lt;code&gt;&quot;ab&quot;&lt;/code&gt;，放入失败，但是仍然会返回 &lt;code&gt;StringTable&lt;/code&gt; 中的字符串对象，所以 &lt;code&gt;str2&lt;/code&gt; 指向 &lt;code&gt;StringTable&lt;/code&gt; 中的字符串对象，但是 &lt;code&gt;str&lt;/code&gt; 指向的是堆中的字符串对象。&lt;/p&gt;
&lt;p&gt;所以输出为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;false
false
true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在 JDK 1.6 和 JDK 1.8 中 &lt;code&gt;intern()&lt;/code&gt; 方法的表现略有不同，放入成功时&lt;strong&gt;会将堆中的字符串拷贝一份，再将其引用放入&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;StringTable&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;中，不会引用堆中的同一个对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以下面的代码在 JDK1.6 的环境下两个输出语句都会输出 &lt;code&gt;false&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    String str = new String(&quot;a&quot;) + new String(&quot;b&quot;);
    String st2 = str.intern();
    String str3 = &quot;ab&quot;;
    System.out.println(str == st2);
    System.out.println(str == str3);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?string"/><enclosure url="http://wallpaper.csun.site/?string"/></item><item><title>一文玩转 Stream API</title><link>https://blog.csun.site/blog/2025-11-10-streamapi</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-11-10-streamapi</guid><description>Java 8 Stream API 使用指南</description><pubDate>Mon, 10 Nov 2025 22:14:12 GMT</pubDate><content:encoded>&lt;p&gt;针对常见的集合数据处理, Java 8 引入了一套新的类库, 位于包 &lt;code&gt;java.util.stream&lt;/code&gt; 下&lt;/p&gt;
&lt;p&gt;Java 8 给 Collection 接口增加了两个默认方法, 它们可以返回一个 Stream&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;default Stream stream() {
    return StreamSupport.stream(spliterator(), false);
}
default Stream parallelStream() {
    return StreamSupport.stream(spliterator(), true);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;stream()&lt;/code&gt;​ 返回的是一个顺序流 , &lt;code&gt;parallelStream()&lt;/code&gt; 返回的是一个并行流&lt;/p&gt;
&lt;h2&gt;基本使用&lt;/h2&gt;
&lt;h3&gt;过滤&lt;/h3&gt;
&lt;p&gt;返回学生列表中 90 分以上的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List above90List = students.stream()
    .filter(t - &gt; t.getScore() &gt; 90).collect(Collectors.toList());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先通过 &lt;code&gt;stream()&lt;/code&gt;​ 得到一个 Stream 对象, 然后调用 Stream 上的方法, &lt;code&gt;filter()&lt;/code&gt; ​过滤得到 90 分以上的, 它的返回值依然是一个 Stream, 为了转换为 List, 调用了 collect 方法并传递了一个 &lt;code&gt;Collectors.toList()&lt;/code&gt;, 表示将结果收集到一个 List 中。&lt;/p&gt;
&lt;h3&gt;转换&lt;/h3&gt;
&lt;p&gt;根据学生列表返回名称列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List nameList = students.stream()
    .map(Student::getName).collect(Collectors.toList());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Stream 的 &lt;code&gt;map()&lt;/code&gt; 函数, 它的参数是一个 Function 函数式接口&lt;/p&gt;
&lt;h3&gt;过滤和转换组合&lt;/h3&gt;
&lt;p&gt;返回 90 分以上的学生名称列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List above90Names = students.stream()
    .filter(t - &gt; t.getScore() &gt; 90).map(Student::getName)
    .collect(Collectors.toList());
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;@tip&lt;/p&gt;
&lt;p&gt;调用 &lt;code&gt;filter()&lt;/code&gt; ​和 &lt;code&gt;map()&lt;/code&gt; ​都不会执行任何实际的操作, 它们只是在构建操作的流水线, 调用 &lt;code&gt;collect()&lt;/code&gt; ​才会触发实际的遍历执行, 在一次遍历中完成过滤、转换以及收集结果的任务。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;像 &lt;code&gt;filter()&lt;/code&gt;​ 和 &lt;code&gt;map()&lt;/code&gt;​ 这种不实际触发执行、用于构建流水线、返回 Stream 的操作称为&lt;strong&gt;中间操作&lt;/strong&gt; (intermediate operation), 而像 &lt;code&gt;collect()&lt;/code&gt;​ 这种触发实际执行、返回具体结果的操作称为&lt;strong&gt;终端操作&lt;/strong&gt; (terminal operation)&lt;/p&gt;
&lt;h2&gt;中间操作&lt;/h2&gt;
&lt;p&gt;中间操作不触发实际的执行, 返回值为 Stream&lt;/p&gt;
&lt;h3&gt;distinct&lt;/h3&gt;
&lt;p&gt;​&lt;code&gt;distinct()&lt;/code&gt; 用于去重&lt;/p&gt;
&lt;p&gt;例如返回字符串列表中长度小于 3 的字符串、转换为小写、只保留唯一的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List list = Arrays.asList(new String[] {
    &quot;abc&quot;,
    &quot;def&quot;,
    &quot;hello&quot;,
    &quot;Abc&quot;
});
List retList = list.stream()
    .filter(s - &gt; s.length() &amp;#x3C;= 3).map(String::toLowerCase).distinct()
    .collect(Collectors.toList());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;distinct()&lt;/code&gt;​ 判断是否重复是根据 &lt;code&gt;equals()&lt;/code&gt;​ 方法来比较的, 同时还需要记录之前出现过的元素, 对于顺序流, &lt;strong&gt;如果元素是无序的会使用&lt;/strong&gt; &lt;strong&gt;​&lt;code&gt;HashSet&lt;/code&gt;​&lt;/strong&gt;​ &lt;strong&gt;记录, 如果是有序的, 会使用&lt;/strong&gt; &lt;strong&gt;​&lt;code&gt;LinkedHashSet&lt;/code&gt;​&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;sorted&lt;/h3&gt;
&lt;p&gt;有两个 &lt;code&gt;sorted()&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream sorted()
Stream sorted(Comparator comparator)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如, 过滤得到 90 分以上的学生, 然后按分数从高到低排序&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List list = students.stream().filter(t - &gt; t.getScore() &gt; 90)
    .sorted(Comparator.comparing(Student::getScore)
        .reversed().thenComparing(Student::getName))
    .collect(Collectors.toList());
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;@info&lt;/p&gt;
&lt;p&gt;sorted 为了排序, 它需要&lt;strong&gt;先在内部数组中保存碰到的每一个元素, 到流结尾时再对数组排序&lt;/strong&gt;, 然后再将排序后的元素逐个传递给流水线中的下一个操作&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;skip/limit&lt;/h3&gt;
&lt;p&gt;​&lt;code&gt;skip()&lt;/code&gt; 跳过流中的 n 个元素, 如果流中元素不足 n 个, 返回一个空流&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream skip(long n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;limit()&lt;/code&gt; 限制流的长度为 maxSize&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream limit(long maxSize)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如, 将学生列表按照分数排序, 返回第 3 名到第 5 名&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List list = students.stream()
    .sorted(Comparator.comparing(Student::getScore).reversed())
    .skip(2).limit(3).collect(Collectors.toList());
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;@info&lt;/p&gt;
&lt;p&gt;​&lt;code&gt;limit()&lt;/code&gt;​ 是一种&lt;strong&gt;短路操作&lt;/strong&gt;, 它不需要处理流中的所有元素, 只要处理的元素个数达到 maxSize, 后面的元素就不需要处理了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Java 9 增加了两个新方法, 相当于更为通用的 &lt;code&gt;skip()&lt;/code&gt;​ 和 &lt;code&gt;limit()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;//通用的skip, 在谓词返回为true的情况下一直进行skip操作, 直到某次返回false
default Stream dropWhile(Predicate predicate)
//通用的limit, 在谓词返回为true的情况下一直接受, 直到某次返回false
default Stream takeWhile(Predicate predicate)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;peek&lt;/h3&gt;
&lt;p&gt;​&lt;code&gt;peek()&lt;/code&gt; 用于提供一个 Consumer, 会将流中的每一个元素传给该 Consumer&lt;/p&gt;
&lt;p&gt;例如 使用该方法观察在流水线中流转的元素&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List above90Names = students.stream().filter(t -&gt; t.getScore()&gt; 90)
    .peek(System.out::println).map(Student::getName)
    .collect(Collectors.toList());
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;mapToLong/mapToInt/mapToDouble&lt;/h3&gt;
&lt;p&gt;map 函数接受的参数是一个 &lt;code&gt;Function&amp;#x3C;T, R&gt;&lt;/code&gt;, 为避免装箱/拆箱, 提高性能, Stream 还有如下返回基本类型特定流的方法:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;
DoubleStream mapToDouble(ToDoubleFunction mapper)
IntStream mapToInt(ToIntFunction mapper)
LongStream mapToLong(ToLongFunction mapper)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;flatMap&lt;/h3&gt;
&lt;p&gt;​&lt;code&gt;flatMap()&lt;/code&gt; 接受一个函数 mapper, 对流中的每一个元素, mapper 会将该元素转换为一个流 Stream, 然后把新生成流的每一个元素传递给下一个操作&lt;/p&gt;
&lt;p&gt;例如将一行字符串按空白符分隔为了一个单词流&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;String&gt; lines = Arrays.asList(new String[] {
    &quot;hello abc&quot;,
    &quot;老马 编程&quot;
});
List words = lines.stream()
    .flatMap(line -&gt; Arrays.stream(line.split(&quot;\\s+&quot;)))
    .collect(Collectors.toList());
System.out.println(words);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;[hello, abc, 老马, 编程]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;终端操作&lt;/h2&gt;
&lt;p&gt;终端操作触发执行, 返回一个具体的值或对象&lt;/p&gt;
&lt;h3&gt;max/min&lt;/h3&gt;
&lt;p&gt;max/min 返回流中的最大值/最小值, 定义为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Optional max(Comparator comparator)
Optional min(Comparator comparator)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其返回值类型是 &lt;code&gt;Optional&amp;#x3C;T&gt;&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;@info&lt;/p&gt;
&lt;p&gt;​&lt;code&gt;java.util.Optional&lt;/code&gt;​ 是 Java 8 引入的一个泛型容器类，内部只有一个类型为 T 的单一变量 &lt;code&gt;value&lt;/code&gt;，可能为 null，也可能不为 null。&lt;/p&gt;
&lt;p&gt;​&lt;code&gt;Optional&lt;/code&gt; 用于准确地表明，其代表的值可能为 null，程序员应该进行适当的处理&lt;/p&gt;
&lt;p&gt;​&lt;code&gt;Optional&lt;/code&gt; 定义了一些方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;//value不为null时返回true
public boolean isPresent()
//返回实际的值，如果为null，抛出异常NoSuchElementException
public T get()
//如果value不为null，返回value，否则返回other
public T orElse(T other)
//构建一个空的Optional，value为null
public static Optional empty()
//构建一个非空的Optional, 参数value不能为null
public static  Optional of(T value)
//构建一个Optional，参数value可以为null，也可以不为null
public static  Optional ofNullable(T value)
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如返回分数最高的学生&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Student student = students.stream()
    .max(Comparator.comparing(Student::getScore).reversed()).get();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;count&lt;/h3&gt;
&lt;p&gt;返回流中元素的个数。比如，统计大于 90 分的学生个数，代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;long above90Count = students.stream().filter(t -&gt; t.getScore()&gt; 90).count();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;allMatch/anyMatch/noneMatch&lt;/h3&gt;
&lt;p&gt;这几个函数都接受一个谓词 Predicate，返回一个 boolean 值，用于判定流中的元素是否满足一定的条件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;​&lt;code&gt;allMatch()&lt;/code&gt;：只有在流中所有元素都满足的情况下才返回 true&lt;/li&gt;
&lt;li&gt;​&lt;code&gt;anyMatch()&lt;/code&gt;：只要流中有一个元素满足条件就返回 true&lt;/li&gt;
&lt;li&gt;​&lt;code&gt;noneMatch()&lt;/code&gt;：只有流中所有元素都不满足条件才返回 true&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果流为空，都返回 true&lt;/p&gt;
&lt;p&gt;比如，判断是不是所有学生都及格了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;boolean allPass = students.stream().allMatch(t -&gt; t.getScore()&gt;= 60);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;@important&lt;/p&gt;
&lt;p&gt;这几个操作都是短路操作，不一定需要处理所有元素就能得出结果，比如，对于 &lt;code&gt;all-Match()&lt;/code&gt;，只要有一个元素不满足条件，就能返回 false。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;findFirst/findAny&lt;/h3&gt;
&lt;p&gt;​&lt;code&gt;findFirst()&lt;/code&gt; 返回第一个元素&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Optional findFirst()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;findAny&lt;/code&gt; 返回任意一个元素&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Optional findAny()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;他们都是短路操作，找到元素后就不再处理所有元素&lt;/p&gt;
&lt;p&gt;例如随便找一个不及格的学生&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Optional student = students.stream().filter(t -&gt; t.getScore() &amp;#x3C;60).findAny();
if (student.isPresent()) {
    //处理不及格的学生
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;forEach&lt;/h3&gt;
&lt;p&gt;有两个 forEach 方法，都接受一个 Consumer，对流中的每一个元素，传递元素给 Consumer&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void forEach(Consumer action)
void forEachOrdered(Consumer action)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;@important&lt;/p&gt;
&lt;p&gt;二者区别在于：在并行流中，&lt;code&gt;forEach()&lt;/code&gt;​ 保证处理的顺序，而 &lt;code&gt;foreEachOrdered()&lt;/code&gt; 会保证按照流中元素的出现顺序进行处理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如，逐行打印大于 90 分的学生&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;students.stream().filter(t -&gt; t.getScore()&gt; 90).forEach(System.out::println);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;toArray&lt;/h3&gt;
&lt;p&gt;​&lt;code&gt;toArray()&lt;/code&gt; 将流转换为数组&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Object[] toArray()
A[] toArray(IntFunction generator)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不带参数的 &lt;code&gt;toArray()&lt;/code&gt;​ 返回的数组类型为 &lt;code&gt;Object[]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果要得到指定类型的数组，需要传递一个 &lt;code&gt;IntFunction generator&lt;/code&gt;​，&lt;code&gt;IntFunction&lt;/code&gt; 的定义为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface IntFunction {
    R apply(int value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如，获取 90 分以上的学生数组，代码可以为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Student[] above90Arr = students.stream().filter(t -&gt; t.getScore()&gt; 90)
    .toArray(Student[]::new);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器会自动把 &lt;code&gt;String[]::new&lt;/code&gt; 解释为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;i -&gt; new String[i]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;reduce&lt;/h3&gt;
&lt;p&gt;​&lt;code&gt;reduce()&lt;/code&gt; 代表归约或者叫折叠，将流中的元素归约为一个值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Optional reduce(BinaryOperator accumulator);
T reduce(T identity, BinaryOperator accumulator);
U reduce(U identity, BiFunction accumulator, BinaryOperator combiner);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个函数没有初始值，从流的第一个元素开始累计，返回 &lt;code&gt;Optional&lt;/code&gt; 例如&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;Integer&gt; list = Arrays.asList(1, 2, 3, 4);
Optional&amp;#x3C;Integer&gt; result = list.stream()
    .reduce((a, b) -&gt; a + b);
System.out.println(result.get()); // 输出 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二个函数有初始值 &lt;code&gt;identity&lt;/code&gt;，从该值开始累计，返回类型是流中元素的类型&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;Integer&gt; list = Arrays.asList(1, 2, 3, 4);
int sum = list.stream()
    .reduce(1, (a, b) -&gt; a + b);
System.out.println(sum); // 输出 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三个函数有初始值 &lt;code&gt;identity&lt;/code&gt;​，从该值开始累计，返回类型是 &lt;code&gt;identity&lt;/code&gt;​ 的类型，&lt;code&gt;combiner&lt;/code&gt; 用于在并行流中合并多个部分结果&lt;/p&gt;
&lt;p&gt;比如，使用 &lt;code&gt;reduce()&lt;/code&gt; 函数计算学生分数的和&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;double sumScore = students.stream().reduce(0 d,
    (sum, t) -&gt; sum += t.getScore(),
    (sum1, sum2) -&gt; sum1 += sum2
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;构建流&lt;/h2&gt;
&lt;p&gt;除了通过 &lt;code&gt;Collection&lt;/code&gt;​ 接口的 &lt;code&gt;stream/parallelStream&lt;/code&gt; 获取流，还有一些其他方式可以获取流&lt;/p&gt;
&lt;p&gt;​&lt;code&gt;Arrays&lt;/code&gt;​ 有一些 &lt;code&gt;stream&lt;/code&gt; 方法，可以将数组或子数组转换为流&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static IntStream stream(int[] array)
public static DoubleStream stream(double[] array, int startInclusive, int endExclusive)
public static Stream stream(T[] array)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;Stream&lt;/code&gt; 也有一些静态方法，可以构建流&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;//返回一个空流
public static Stream empty()
//返回只包含一个元素t的流
public static Stream of(T t)
//返回包含多个元素values的流
public static Stream of(T... values)
//通过Supplier生成流，流的元素个数是无限的
public static Stream generate(Supplier s)
//同样生成无限流，第一个元素为seed，第二个为f(seed)，第三个为f(f(seed))，以此类推
public static Stream iterate(final T seed, final UnaryOperator f)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;collect&lt;/h2&gt;
&lt;p&gt;​&lt;code&gt;collect()&lt;/code&gt; 方法的定义&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;R collect(Collector collector)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其接受一个 &lt;code&gt;Collector&lt;/code&gt;  类型的收集器作为参数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface Collector {
    Supplier supplier();
    BiConsumer accumulator();
    BinaryOperator combiner();
    Function finisher();
    Set characteristics();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;@important&lt;/p&gt;
&lt;p&gt;顺序流中，&lt;code&gt;collect()&lt;/code&gt; 方法与这些接口方法的交互大概是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;//首先调用工厂方法supplier创建一个存放处理状态的容器container，类型为A
A container = collector.supplier().get();

//对流中的每一个元素t，调用累加器accumulator，参数为累计状态container和当前元素t
for (T t: data)
    collector.accumulator().accept(container, t);

//最后调用finisher对累计状态container进行可能的调整，类型转换(A转换为R)，返回结果
return collector.finisher().apply(container);
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;​&lt;code&gt;combiner()&lt;/code&gt; 只在并行流中有用，用于合并部分结果&lt;/p&gt;
&lt;p&gt;​&lt;code&gt;characteristics()&lt;/code&gt; 用于标示收集器的特征，是一个枚举，有三个值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;CONCURRENT  
UNORDERED  // 收集器不会保留顺序
IDENTITY_FINISH  // 直接返回 container finisher 不做处理
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以过滤得到 90 分以上的学生列表为例&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List above90List = students.stream().filter(t -&gt; t.getScore()&gt; 90)
    .collect(Collectors.toList());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;Collectors.toList()&lt;/code&gt; 是一个静态方法，代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static &amp;#x3C;T&gt;
    Collector &amp;#x3C;T, ? , List &amp;#x3C;T&gt;&gt; toList() {
        return new CollectorImpl &amp;#x3C;&gt; ((Supplier &amp;#x3C;List &amp;#x3C;T&gt;&gt; ) ArrayList::new, List::add,
            (left, right) -&gt; {
                left.addAll(right);
                return left;
            },
            CH_ID);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要是创建了一个 &lt;code&gt;Collector&lt;/code&gt;​ 接口的实现类 &lt;code&gt;CollectorImpl&lt;/code&gt; 对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;​&lt;code&gt;supplier()&lt;/code&gt;​ 的实现是 &lt;code&gt;ArrayList::new&lt;/code&gt;​ ，也就是创建一个 &lt;code&gt;ArrayList&lt;/code&gt; 作为容器&lt;/li&gt;
&lt;li&gt;​&lt;code&gt;accumulator()&lt;/code&gt;​ 的实现是 &lt;code&gt;List::add&lt;/code&gt;，也就是将每一个元素加到列表中&lt;/li&gt;
&lt;li&gt;第三个参数是 &lt;code&gt;combiner()&lt;/code&gt; 表示合并结果&lt;/li&gt;
&lt;li&gt;第四个参数 &lt;code&gt;CH_ID&lt;/code&gt;​ 是一个静态变量，只有一个特征 &lt;code&gt;IDENTITY_FINISH&lt;/code&gt;​，表示 &lt;code&gt;finisher()&lt;/code&gt; 没有什么事情可以做，就是把累计状态 container 直接返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;容器收集器&lt;/h3&gt;
&lt;p&gt;与 &lt;code&gt;toList()&lt;/code&gt;​ 类似的容器收集器还有 &lt;code&gt;toSet()&lt;/code&gt;​、&lt;code&gt;toCollection()&lt;/code&gt;​、&lt;code&gt;toMap()&lt;/code&gt; 等&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;​&lt;code&gt;toSet()&lt;/code&gt;​ 与 &lt;code&gt;toList()&lt;/code&gt;​ 类似，只是其可以去重，其背后的容器为 &lt;code&gt;HashSet&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static
Collector&gt; toSet() {
    return new CollectorImpl &amp;#x3C;&gt; ((Supplier&gt; ) HashSet::new, Set::add,
        (left, right) -&gt; {
            left.addAll(right);
            return left;
        },
        CH_UNORDERED_ID);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;CH_UNORDERED_ID&lt;/code&gt; 有两个特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;​&lt;code&gt;IDENTITY_FINISH&lt;/code&gt;​，表示返回结果即为 &lt;code&gt;Supplier&lt;/code&gt;​ 创建的 &lt;code&gt;HashSet&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;​&lt;code&gt;UNORDERED&lt;/code&gt;，表示收集器不会保留顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;​&lt;code&gt;toCollection&lt;/code&gt;​ 是一个通用的容器收集器，可以用于任何 &lt;code&gt;Collection&lt;/code&gt;​ 接口的实现类，其接受一个工厂方法 &lt;code&gt;Supplier&lt;/code&gt; 作为参数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static&gt;
    Collector toCollection(Supplier collectionFactory) {
        return new CollectorImpl &amp;#x3C;&gt; (collectionFactory, Collection::add,
            (r1, r2) -&gt; {
                r1.addAll(r2);
                return r1;
            },
            CH_ID);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，如果希望去重但又希望保留出现的顺序，可以使用 &lt;code&gt;LinkedHashSet&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Collectors.toCollection(LinkedHashSet::new)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;​&lt;code&gt;toMap()&lt;/code&gt;​ 将元素流转换为一个 &lt;code&gt;Map&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static &amp;#x3C;T, K, U, M extends Map &amp;#x3C;K, U&gt;&gt;
    Collector &amp;#x3C;T, ? , M&gt; toMap(
		Function &amp;#x3C;? super T, ? extends K&gt; keyMapper,
        Function &amp;#x3C;? super T, ? extends U&gt; valueMapper,
        BinaryOperator &amp;#x3C;U&gt; mergeFunction,
        Supplier &amp;#x3C;M&gt; mapSupplier
	) {
        BiConsumer &amp;#x3C;M, T&gt; accumulator = (map, element) -&gt; map.merge(keyMapper.apply(element),
            valueMapper.apply(element), mergeFunction);
        return new CollectorImpl &amp;#x3C;&gt; (mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;keyMapper&lt;/code&gt;​ 将元素转换为键，&lt;code&gt;valueMapper&lt;/code&gt; 将元素转换为值&lt;/p&gt;
&lt;p&gt;比如，将一个对象列表按主键转换为一个 Map，以便以后按照主键进行快速查找&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map byIdMap = students.stream().collect(
    Collectors.toMap(Student::getId, t -&gt; t));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;t-&gt;t&lt;/code&gt;​ 是 &lt;code&gt;valueMapper&lt;/code&gt;​，表示值就是元素本身，接口 &lt;code&gt;Function&lt;/code&gt;​ 定义了一个静态函数 &lt;code&gt;identity&lt;/code&gt; 表示它&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map byIdMap = students.stream().collect(
	Collectors.toMap(Student::getId, Function.identity()));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;Map&lt;/code&gt;​ 的键是不能重复的，否则会抛出异常，如果我们希望忽略后面重复出现的元素，可以使用参数 &lt;code&gt;mergeFunction&lt;/code&gt; 处理冲突，例如&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map strLenMap = Stream.of(&quot;abc&quot;, &quot;hello&quot;, &quot;abc&quot;).collect(
    Collectors.toMap(Function.identity(),
        t -&gt; t.length(), (oldValue, value) -&gt; value));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;mapSupplier&lt;/code&gt;​ 是 Map 的工厂方法，前面的例子都是 &lt;code&gt;HashMap::new&lt;/code&gt;​，如果希望保持元素出现的顺序，可以替换为 &lt;code&gt;LinkedHashMap::new&lt;/code&gt;​，如果希望收集的结果排序，可以使用 &lt;code&gt;TreeMap::new&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;字符串收集器&lt;/h3&gt;
&lt;p&gt;​&lt;code&gt;Collectors&lt;/code&gt;​ 提供了 &lt;code&gt;joining()&lt;/code&gt; 收集器，其作用是将元素流收集为一个字符串&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 简单的把元素连接
public static Collector &amp;#x3C;CharSequence, ? , String&gt; joining() {
    return new CollectorImpl &amp;#x3C;CharSequence, StringBuilder, String&gt; (
        StringBuilder::new, StringBuilder::append,
        (r1, r2) -&gt; {
            r1.append(r2);
            return r1;
        },
        StringBuilder::toString, CH_NOID);
}

// 支持一个分隔符
public static Collector &amp;#x3C;CharSequence, ? , String&gt; joining(CharSequence delimiter) {
    return joining(delimiter, &quot;&quot;, &quot;&quot;);
}

// 支持给整个结果字符串加前缀和后缀
public static Collector &amp;#x3C;CharSequence, ? , String&gt; joining(CharSequence delimiter,
    CharSequence prefix,
    CharSequence suffix) {
    return new CollectorImpl &amp;#x3C;&gt; (
        () -&gt; new StringJoiner(delimiter, prefix, suffix),
        StringJoiner::add, StringJoiner::merge,
        StringJoiner::toString, CH_NOID);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;joining()&lt;/code&gt;​ 的内部是利用了 &lt;code&gt;StringBuilder&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;​&lt;code&gt;supplier()&lt;/code&gt;​ 是 &lt;code&gt;StringBuilder::new&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;​&lt;code&gt;accumulator()&lt;/code&gt;​ 是 &lt;code&gt;StringBuilder::append&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;​&lt;code&gt;finisher()&lt;/code&gt;​ 是 &lt;code&gt;StringBuilder::toString&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​&lt;code&gt;CH_NOID&lt;/code&gt; 表示特征集为空&lt;/p&gt;
&lt;h2&gt;分组&lt;/h2&gt;
&lt;p&gt;分组类似于 SQL 中的 &lt;code&gt;group by&lt;/code&gt; 语句，它将元素流中的每个元素分到一个组&lt;/p&gt;
&lt;h3&gt;基本用法&lt;/h3&gt;
&lt;p&gt;最基本的分组收集器为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public static &amp;#x3C;T, K&gt; Collector&amp;#x3C;T, ?, Map&amp;#x3C;K, List&amp;#x3C;T&gt;&gt;&gt;
    groupingBy(Function&amp;#x3C;? super T, ? extends K&gt; classifier) {
        return groupingBy(classifier, toList());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数是一个类型为 &lt;code&gt;Function&lt;/code&gt; ​的分组器 &lt;code&gt;classifier&lt;/code&gt;​，它将类型为 &lt;code&gt;T&lt;/code&gt; ​的元素转换为类型为 &lt;code&gt;K&lt;/code&gt; ​的一个值，这个值表示分组值&lt;/p&gt;
&lt;p&gt;所有分组值一样的元素会被归为同一个组，放到一个列表中，所以返回值类型是 &lt;code&gt;Map&amp;#x3C;K, List&amp;#x3C;T&gt;&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;比如，将学生按照年级进行分组，代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&amp;#x3C;Map&gt; groups = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个分组收集器调用了 &lt;code&gt;groupingBy(classifier, toList())&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static &amp;#x3C;T, K, A, D&gt;
    Collector &amp;#x3C;T, ? , Map &amp;#x3C;K, D&gt;&gt; groupingBy(Function &amp;#x3C;? super T, ? extends K&gt; classifier,
        Collector &amp;#x3C;? super T, A, D&gt; downstream) {
        return groupingBy(classifier, HashMap::new, downstream);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法接受一个下游收集器 &lt;code&gt;downstream&lt;/code&gt; 作为参数，然后传递给下面更通用的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static &amp;#x3C;T, K, D, A, M extends Map &amp;#x3C;K, D&gt;&gt;
    Collector &amp;#x3C;T, ? , M&gt; groupingBy(Function &amp;#x3C;? super T, ? extends K&gt; classifier,
        Supplier &amp;#x3C;M&gt; mapFactory,
        Collector &amp;#x3C;? super T, A, D&gt; downstream) {

        Supplier &amp;#x3C;A&gt; downstreamSupplier = downstream.supplier();
        BiConsumer &amp;#x3C;A, ? super T&gt; downstreamAccumulator = downstream.accumulator();
        BiConsumer &amp;#x3C;Map &amp;#x3C;K, A&gt; , T&gt; accumulator = (m, t) -&gt; {
            K key = Objects.requireNonNull(classifier.apply(t), &quot;element cannot be mapped to a null key&quot;);
            A container = m.computeIfAbsent(key, k -&gt; downstreamSupplier.get());
            downstreamAccumulator.accept(container, t);
        };
        BinaryOperator &amp;#x3C;Map &amp;#x3C;K, A&gt;&gt; merger = Collectors. &amp;#x3C;K, A, Map &amp;#x3C;K, A&gt;&gt; mapMerger(downstream.combiner());
        @SuppressWarnings(&quot;unchecked&quot;)
        Supplier &amp;#x3C;Map &amp;#x3C;K, A&gt;&gt; mangledFactory = (Supplier &amp;#x3C;Map &amp;#x3C;K, A&gt;&gt; ) mapFactory;

        if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
            return new CollectorImpl &amp;#x3C;&gt; (mangledFactory, accumulator, merger, CH_ID);
        } else {
            @SuppressWarnings(&quot;unchecked&quot;)
            Function &amp;#x3C;A, A&gt; downstreamFinisher = (Function &amp;#x3C;A, A&gt; ) downstream.finisher();
            Function &amp;#x3C;Map &amp;#x3C;K, A&gt; , M&gt; finisher = intermediate -&gt; {
                intermediate.replaceAll((k, v) -&gt; downstreamFinisher.apply(v));
                @SuppressWarnings(&quot;unchecked&quot;)
                M castResult = (M) intermediate;
                return castResult;
            };
            return new CollectorImpl &amp;#x3C;&gt; (mangledFactory, accumulator, merger, finisher, CH_NOID);
        }
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;code&gt;classifier&lt;/code&gt; ​还是分组器, &lt;code&gt;mapFactory&lt;/code&gt; ​是返回 Map 的工厂方法, 默认是 &lt;code&gt;HashMap::new&lt;/code&gt;​, &lt;code&gt;downstream&lt;/code&gt; ​表示下游收集器, &lt;strong&gt;下游收集器负责收集同一个分组内元素的结果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;收集元素的基本过程为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;//先创建一个存放结果的Map
Map map = mapFactory.get();
for (T t: data) {
    //对每一个元素，先分组
    K key = classifier.apply(t);
    //找存放分组结果的容器，如果没有，让下游收集器创建，并放到Map中
    A container = map.get(key);
    if (container == null) {
        container = downstream.supplier().get();
        map.put(key, container);
    }
    //将元素交给下游收集器(即分组收集器)收集
    downstream.accumulator().accept(container, t);
}
//调用分组收集器的finisher方法，转换结果
for (Map.Entry entry: map.entrySet()) {
    entry.setValue(downstream.finisher().apply(entry.getValue()));
}
return map;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分组数值统计&lt;/h3&gt;
&lt;p&gt;将元素按一定标准分为多组，然后计算每组的个数，按一定标准找最大或最小元素，求和，求平均等&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;//计数
public static Collector counting()
//计算最大值
public static Collector maxBy(Comparator comparator)
//计算最小值
public static Collector minBy(Comparator comparator)
//求平均值
public static Collector averagingDouble(ToDoubleFunction mapper)
//求和
public static Collector summingInt(ToIntFunction mapper)
//求多种汇总信息 包括个数、最大值、最小值、和、平均值等多种信息
public static Collector summarizingLong(ToLongFunction mapper)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如统计每个年级的学生个数，代码可以为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map gradeCountMap = students.stream().collect(
	groupingBy(Student::getGrade, Collectors.counting())
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按年级统计学生分数信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map gradeScoreStat =
    students.stream().collect(groupingBy(Student::getGrade,
        summarizingDouble(Student::getScore)));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分组内的 map&lt;/h3&gt;
&lt;p&gt;对于每个分组内的元素，我们感兴趣的可能不是元素本身，而是它的某部分信息。&lt;/p&gt;
&lt;p&gt;​&lt;code&gt;Collectors&lt;/code&gt;​ 也为分组元素提供了函数 &lt;code&gt;mapping()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Collector mapping(Function mapper, Collector downstream)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;交给下游收集器 &lt;code&gt;downstream&lt;/code&gt;​ 的不再是元素本身，而是应用转换函数 &lt;code&gt;mapper&lt;/code&gt; 之后的结果&lt;/p&gt;
&lt;p&gt;比如，对学生按年级分组，得到学生名称列表，代码可以为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map gradeNameMap = students.stream().collect(groupingBy(Student::getGrade,
        mapping(Student::getName, toList())));
System.out.println(gradeNameMap);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分组结果处理&lt;/h3&gt;
&lt;p&gt;对分组后的元素，也可以进行排序（sort）、过滤（filter）、限制返回元素（skip/limit）&lt;/p&gt;
&lt;p&gt;​&lt;code&gt;Collectors&lt;/code&gt;​ 提供了一个通用的收集器，接受一个下游收集器 &lt;code&gt;downstream&lt;/code&gt;​ 和一个 &lt;code&gt;finisher&lt;/code&gt;，返回一个收集器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Collector collectingAndThen(Collector downstream, Function finisher)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其在下游收集器的结果上又调用了 &lt;code&gt;finisher&lt;/code&gt;​。利用这个 &lt;code&gt;finisher&lt;/code&gt;，我们可以实现多种功能&lt;/p&gt;
&lt;p&gt;例如，收集完再排序，可以定义如下方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Collector collectingAndSort(Collector&gt; downstream, Comparator comparator) {
    return Collectors.collectingAndThen(downstream, (r) -&gt; {
        r.sort(comparator);
        return r;
    });
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分区&lt;/h3&gt;
&lt;p&gt;分组的一个特殊情况是分区，就是将流按 &lt;code&gt;true/false&lt;/code&gt; ​分为两个组，&lt;code&gt;Collectors&lt;/code&gt; ​有专门的分区函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;
public static &amp;#x3C;T&gt; Collector &amp;#x3C;T, ? , Map &amp;#x3C;Boolean, List &amp;#x3C;T&gt;&gt;&gt; partitioningBy(Predicate &amp;#x3C;? super T&gt; predicate) {
	return partitioningBy(predicate, toList());
}

public static &amp;#x3C;T, D, A&gt; Collector &amp;#x3C;T, ? , Map &amp;#x3C;Boolean, D&gt;&gt; partitioningBy(Predicate &amp;#x3C;? super T&gt; predicate,
	Collector &amp;#x3C;? super T, A, D&gt; downstream)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如，将学生按照是否及格（大于等于 60 分）分为两组，代码可以为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map byPass = students.stream().collect(
    partitioningBy(t -&gt; t.getScore()&gt;= 60));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按是否及格分组后，计算每个分组的平均分，代码可以为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;
Map avgScoreMap = students.stream().collect(partitioningBy(t-&gt;t.getScore()&gt;=60, 
	averagingDouble(Student::getScore)));

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?stream"/><enclosure url="http://wallpaper.csun.site/?stream"/></item><item><title>CopyOnWrite：写时复制机制详解</title><link>https://blog.csun.site/blog/2025-10-28-copyonwrite</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-10-28-copyonwrite</guid><description>详解 CopyOnWrite 写时复制机制</description><pubDate>Tue, 28 Oct 2025 22:14:12 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;CopyOnWrite&lt;/code&gt;（写时复制）是一种并发容器的实现思想，主要应用于以下两个类中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CopyOnWriteArrayList&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CopyOnWriteArraySet&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其核心理念是：&lt;br&gt;
当有线程要修改容器时，不直接操作原容器，而是&lt;strong&gt;先复制一份副本，在副本上进行修改&lt;/strong&gt;，待修改完成后再用新副本替换旧容器的引用。&lt;/p&gt;
&lt;p&gt;这意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读操作无需加锁&lt;/strong&gt;：读线程始终访问的是稳定的旧副本，因此不会受到写操作的影响；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写操作成本较高&lt;/strong&gt;：每次写入都需要复制整个底层数组，以保证线程安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CopyOnWriteArrayList&lt;/h2&gt;
&lt;p&gt;与 &lt;code&gt;ArrayList&lt;/code&gt; 类似，&lt;code&gt;CopyOnWriteArrayList&lt;/code&gt; 的核心数据结构同样是一个数组，同时额外使用一个 &lt;code&gt;ReentrantLock&lt;/code&gt; 来保护写操作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private transient volatile Object[] array;
final transient ReentrantLock lock = new ReentrantLock();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;读操作&lt;/h3&gt;
&lt;p&gt;常见的读操作如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;final Object[] getArray() {
    return array;
}

public E get(int index) {
    return get(getArray(), index);
}

public boolean contains(Object o) {
    Object[] elements = getArray();
    return indexOf(o, elements, 0, elements.length) &gt;= 0;
}

public int indexOf(E e, int index) {
    Object[] elements = getArray();
    return indexOf(e, elements, index, elements.length);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，所有读操作都&lt;strong&gt;没有加锁&lt;/strong&gt;。&lt;br&gt;
那么，它是如何保证线程安全的呢？&lt;br&gt;
关键在于写操作的实现：读线程总是读取当前的数组副本，而写线程在更新时会创建新的副本，因此读写不会互相干扰。&lt;/p&gt;
&lt;h2&gt;写操作&lt;/h2&gt;
&lt;p&gt;以 &lt;code&gt;add(E e)&lt;/code&gt; 方法为例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

final void setArray(Object[] a) {
    array = a;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写操作流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取独占锁；&lt;/li&gt;
&lt;li&gt;复制原数组；&lt;/li&gt;
&lt;li&gt;在新数组上执行写入；&lt;/li&gt;
&lt;li&gt;将新数组替换为当前数组引用；&lt;/li&gt;
&lt;li&gt;释放锁。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其他写操作（如 &lt;code&gt;remove()&lt;/code&gt;、&lt;code&gt;set()&lt;/code&gt;）的实现逻辑也类似。&lt;/p&gt;
&lt;h2&gt;CopyOnWriteArraySet&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CopyOnWriteArraySet&lt;/code&gt; 是基于数组实现的 &lt;code&gt;Set&lt;/code&gt;，其内部实际封装了一个 &lt;code&gt;CopyOnWriteArrayList&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private final CopyOnWriteArrayList&amp;#x3C;E&gt; al;

/**
 * Creates an empty set.
 */
public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList&amp;#x3C;E&gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;区别在于，&lt;code&gt;CopyOnWriteArraySet&lt;/code&gt; 会确保&lt;strong&gt;元素不重复&lt;/strong&gt;。&lt;br&gt;
添加元素时，会先检查元素是否已存在：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean add(E e) {
    return al.addIfAbsent(e);
}

public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) &gt;= 0 ? false :
           addIfAbsent(e, snapshot);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CopyOnWrite&lt;/code&gt; 是一种典型的&lt;strong&gt;以空间换时间&lt;/strong&gt;的并发策略。它通过牺牲写性能与内存开销，换取了极高的读性能和线程安全的简洁实现。在高并发、读操作频繁写操作较少的场景中，&lt;code&gt;CopyOnWriteArrayList&lt;/code&gt; 和 &lt;code&gt;CopyOnWriteArraySet&lt;/code&gt; 都是不二之选。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?copyOnWirte"/><enclosure url="http://wallpaper.csun.site/?copyOnWirte"/></item><item><title>LeetCode 135 分发糖果</title><link>https://blog.csun.site/blog/2025-10-22-leetcode-135-candy</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-10-22-leetcode-135-candy</guid><description>LeetCode 135 分发糖果题解</description><pubDate>Wed, 22 Oct 2025 14:14:12 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/candy/&quot;&gt;LeetCode 题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;n 个孩子站成一排。给你一个整数数组 &lt;code&gt;ratings&lt;/code&gt; 表示每个孩子的评分。&lt;/p&gt;
&lt;p&gt;你需要按照以下要求，给这些孩子分发糖果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个孩子至少分配到 1 个糖果。&lt;/li&gt;
&lt;li&gt;相邻两个孩子中，评分更高的那个会获得更多的糖果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请你给每个孩子分发糖果，计算并返回需要准备的最低糖果数目。&lt;/p&gt;
&lt;p&gt;示例 1:&lt;/p&gt;
&lt;p&gt;输入： &lt;code&gt;ratings = [1,0,2]&lt;/code&gt;&lt;br&gt;
输出： 5&lt;br&gt;
解释： 你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。&lt;/p&gt;
&lt;p&gt;示例 2:&lt;/p&gt;
&lt;p&gt;输入： &lt;code&gt;ratings = [1,2,2]&lt;/code&gt;&lt;br&gt;
输出： 4&lt;br&gt;
解释： 你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。&lt;br&gt;
第三个孩子只得到 1 颗糖果，这满足题面中的两个条件。&lt;/p&gt;
&lt;p&gt;提示:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;n == ratings.length&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;#x3C;= n &amp;#x3C;= 2 * 10^4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;#x3C;= ratings[i] &amp;#x3C;= 2 * 10^4&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;我们首先来看一个典型的 &lt;strong&gt;「山峰」型评分序列&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ratings = [1, 2, 3, 4, 5, 3, 2, 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于每个孩子至少应获得一个糖果，我们首先为每个孩子分配一个糖果，即糖果的初始总数为 &lt;code&gt;n = ratings.length&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于该「山峰」序列，我们可以分为两个部分进行分析：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;递增部分：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 0 个孩子可额外分配 0 个糖果（不计初始分配的 1 个糖果）&lt;/li&gt;
&lt;li&gt;第 1 个孩子可额外分配 1 个糖果&lt;/li&gt;
&lt;li&gt;第 2 个孩子可额外分配 2 个糖果&lt;/li&gt;
&lt;li&gt;第 3 个孩子可额外分配 3 个糖果&lt;/li&gt;
&lt;li&gt;第 4 个孩子可额外分配 4 个糖果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;递减部分：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 7 个孩子可额外分配 0 个糖果&lt;/li&gt;
&lt;li&gt;第 6 个孩子可额外分配 1 个糖果&lt;/li&gt;
&lt;li&gt;第 5 个孩子可额外分配 2 个糖果&lt;/li&gt;
&lt;li&gt;第 4 个孩子可额外分配 3 个糖果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于第 4 个孩子处于 &lt;strong&gt;「峰顶」&lt;/strong&gt; ，在递增和递减序列中均被计入，因此其糖果数取两部分的最大值，即 &lt;code&gt;max(4, 3) = 4&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;因此，总糖果数为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;n + (0 + 1 + 2 + 3) + (0 + 1 + 2) + max(4, 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以将评分序列划分为多个“山峰”结构，并依据上述规则贪心地求解每一段的局部最优，从而获得整体的全局最优解&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;算法流程如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始下标 &lt;code&gt;start = 0&lt;/code&gt;，通过循环 &lt;code&gt;ratings[i] &amp;#x3C; ratings[i+1]&lt;/code&gt; 找到递增序列，循环结束时的下标即为峰顶 &lt;code&gt;top&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;接着通过循环 &lt;code&gt;ratings[i] &gt; ratings[i+1]&lt;/code&gt; 找到递减序列，循环结束时的下标即为谷底。&lt;/li&gt;
&lt;li&gt;设 &lt;code&gt;up = top - start &lt;/code&gt; 为递增序列的长度（不考虑峰顶），则分到的糖果依次为 &lt;code&gt;0, 1, 2, ..., up - 1&lt;/code&gt;, 等差数列求和结果为 &lt;code&gt;up * (up - 1) / 2&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;设 &lt;code&gt;down = i - top&lt;/code&gt; 为递减序列的长度（不考虑峰顶），则分到的糖果依次为 &lt;code&gt;down - 1, down - 2, ..., 0&lt;/code&gt;，等差数列求和结果为 &lt;code&gt;down * (down - 1) / 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;峰顶元素的糖果数为 &lt;code&gt;max(up, down)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;继续处理下一段「山峰」序列，令 &lt;code&gt;start = i&lt;/code&gt;，两段序列共用谷底元素。&lt;/li&gt;
&lt;li&gt;若存在“平峰”（如 &lt;code&gt;[1, 2, 2, 3]&lt;/code&gt;），则划分为 &lt;code&gt;[1, 2]&lt;/code&gt; 和 &lt;code&gt;[2, 3]&lt;/code&gt; 两段，此时第一段的递减长度为 0，第二段的递增长度为 0，两者不共享谷底元素。因此，需通过如下判断更新 &lt;code&gt;start&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;start = i + 1 &amp;#x3C; n &amp;#x26;&amp;#x26; ratings[i] &amp;#x3C; ratings[i + 1] ? i : i + 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;完整代码如下：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public int candy(int[] ratings) {
        int n = ratings.length;
        int res = n;
        int start = 0;

        for(int i = 0; i &amp;#x3C; n; i++) {
            // 找到峰顶
            while(i + 1 &amp;#x3C; n &amp;#x26;&amp;#x26; ratings[i] &amp;#x3C; ratings[i+1])
                i++;
            int top = i;

            // 找到谷底
            while(i + 1 &amp;#x3C; n &amp;#x26;&amp;#x26; ratings[i] &gt; ratings[i+1])
                i++;
            
            int up = top - start;
            int down = i - top;

            res += (up * (up - 1) + down * (down - 1)) / 2 + Math.max(up, down);

            // 处理平峰情况
            start = i + 1 &amp;#x3C; n &amp;#x26;&amp;#x26; ratings[i] &amp;#x3C; ratings[i + 1] ? i : i + 1;
        }

        return res;
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?candy"/><enclosure url="http://wallpaper.csun.site/?candy"/></item><item><title>LeetCode122 买卖股票的最佳时机 II</title><link>https://blog.csun.site/blog/2025-10-21-best-time-to-buy-and-sell-stock-ii</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-10-21-best-time-to-buy-and-sell-stock-ii</guid><description>LeetCode122 买卖股票的最佳时机 II 题解</description><pubDate>Tue, 21 Oct 2025 11:21:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/&quot;&gt;LeetCode 题目链接&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;题目描述&lt;/h2&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;prices&lt;/code&gt; ，其中 &lt;code&gt;prices[i]&lt;/code&gt; 表示某支股票第 &lt;code&gt;i&lt;/code&gt; 天的价格。&lt;/p&gt;
&lt;p&gt;在每一天，你可以决定是否购买和/或出售股票。你在任何时候 &lt;strong&gt;最多&lt;/strong&gt; 只能持有 &lt;strong&gt;一股&lt;/strong&gt; 股票。然而，你可以在 &lt;strong&gt;同一天&lt;/strong&gt; 多次买卖该股票，但要确保你持有的股票不超过一股。&lt;/p&gt;
&lt;p&gt;返回 &lt;em&gt;你能获得的 &lt;strong&gt;最大&lt;/strong&gt; 利润&lt;/em&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [7,1,5,3,6,4]
输出：7
解释：在第 2 天（股票价格 = 1）的时候买入，在第 3 天（股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
随后，在第 4 天（股票价格 = 3）的时候买入，在第 5 天（股票价格 = 6）的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。
最大总利润为 4 + 3 = 7 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [1,2,3,4,5]
输出：4
解释：在第 1 天（股票价格 = 1）的时候买入，在第 5 天 （股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
最大总利润为 4 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [7,6,4,3,1]
输出：0
解释：在这种情况下, 交易无法获得正利润，所以不参与交易可以获得最大利润，最大利润为 0。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;#x3C;= prices.length &amp;#x3C;= 3 * 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;#x3C;= prices[i] &amp;#x3C;= 104&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;思路一：峰谷法（模拟交易过程）&lt;/h3&gt;
&lt;p&gt;本题最直观的思路是&lt;strong&gt;从前往后遍历数组&lt;/strong&gt;，找到「谷底」买入，再找到「峰顶」卖出，从而获取利润。&lt;/p&gt;
&lt;p&gt;可以使用两个变量 &lt;code&gt;up&lt;/code&gt; 和 &lt;code&gt;down&lt;/code&gt; 来表示当前价格区间的趋势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;up = true&lt;/code&gt; 时，说明当前区间正在上升；如果此时出现 &lt;code&gt;prices[i] - prices[i - 1] &amp;#x3C; 0&lt;/code&gt;，说明 &lt;code&gt;prices[i - 1]&lt;/code&gt; 是一个峰值。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;down = true&lt;/code&gt; 时，说明当前区间正在下降；如果此时出现 &lt;code&gt;prices[i] - prices[i - 1] &gt; 0&lt;/code&gt;，说明 &lt;code&gt;prices[i - 1]&lt;/code&gt; 是一个谷底。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于一开始要找到谷底买入，因此初始化为 &lt;code&gt;down = true, up = false&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当找到谷底时，用变量 &lt;code&gt;in&lt;/code&gt; 记录买入价格，然后设置 &lt;code&gt;down = false, up = true&lt;/code&gt;，表示行情开始上升。&lt;/p&gt;
&lt;p&gt;当找到峰值时，计算本次交易利润 &lt;code&gt;res += prices[i - 1] - in&lt;/code&gt;，并将趋势重新设置为 &lt;code&gt;down = true, up = false&lt;/code&gt;，表示行情开始下跌。&lt;/p&gt;
&lt;p&gt;需要注意的是，如果整个数组单调递增（或最后一段单调递增），会导致找不到最后一个峰值。&lt;/p&gt;
&lt;p&gt;因此，需要加上特殊判断：若 &lt;code&gt;up &amp;#x26;&amp;#x26; i == n - 1&lt;/code&gt;，则直接以 &lt;code&gt;prices[n - 1]&lt;/code&gt; 为卖出价计算利润。&lt;/p&gt;
&lt;p&gt;完整代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        if(n &amp;#x3C; 2)
            return 0;

        // 谷底买进 峰顶卖出
        int res = 0, in = 0;
        boolean up = false, down = true;
        
        for(int i = 1; i &amp;#x3C; n; i++) {
            if(down &amp;#x26;&amp;#x26; prices[i] - prices[i - 1] &gt; 0) {
                in  = prices[i-1];
                down = false;
                up = true;
            } else if(up &amp;#x26;&amp;#x26; prices[i] - prices[i-1] &amp;#x3C; 0) {
                res += prices[i-1] - in;
                down = true;
                up = false;
            }
            
            if(up &amp;#x26;&amp;#x26; i == n - 1) {
                res += prices[n-1] - in; 
            }
    
        }
        return res;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;思路二：贪心法（累加每日正收益）&lt;/h3&gt;
&lt;p&gt;我们也可以换一种思路。&lt;/p&gt;
&lt;p&gt;假设第 0 天买入、第 3 天卖出，则总利润为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;prices[3] - prices[0]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实这可以分解为每天的差值之和：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;换句话说，&lt;strong&gt;最大总利润等于每天正收益的累加&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，只要收集所有正收益即可，这就是贪心思想的核心。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;prices = [7, 1, 5, 3, 6, 4]

第1天利润：1 - 7 = -6
第2天利润：5 - 1 = 4
第3天利润：3 - 5 = -2
第4天利润：6 - 3 = 3
第5天利润：4 - 6 = -2

收集所有正利润：4 + 3 = 7，即最大利润
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public int maxProfit(int[] prices) {
        int result = 0;
        for (int i = 1; i &amp;#x3C; prices.length; i++) {
            result += Math.max(prices[i] - prices[i - 1], 0);
        }
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?leetocde122"/><enclosure url="http://wallpaper.csun.site/?leetocde122"/></item><item><title>二叉树的几种遍历方式</title><link>https://blog.csun.site/blog/2025-10-13-types-of-binary-tree-traversal</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-10-13-types-of-binary-tree-traversal</guid><description>系统讲解二叉树的递归遍历、迭代遍历以及层序遍历</description><pubDate>Mon, 13 Oct 2025 19:33:00 GMT</pubDate><content:encoded>&lt;p&gt;二叉树的遍历是理解与操作二叉树的第一步，无论是树的构建、查找还是算法题中的路径计算、深搜广搜，都离不开遍历的思想。&lt;/p&gt;
&lt;p&gt;本文将从二叉树的节点定义出发，系统讲解二叉树的递归遍历、迭代遍历以及层序遍历。&lt;/p&gt;
&lt;h2&gt;二叉树节点的定义&lt;/h2&gt;
&lt;p&gt;二叉树由若干个节点组成，每个节点最多有两个子节点：左子节点（left）和右子节点（right）。&lt;/p&gt;
&lt;p&gt;在 Java 中，通常这样定义一个二叉树节点：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    public TreeNode() {
    }

    public TreeNode(int val) {
        this.val = val;
    }

    public TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;递归遍历&lt;/h2&gt;
&lt;p&gt;递归几乎是处理二叉树相关问题时的「本能思路」，因为二叉树树本身就是一个递归定义的结构：每个节点的左右子树又都是一棵树。&lt;/p&gt;
&lt;p&gt;因此我们可以用递归函数的自调用来自然地访问整个树，根据访问根节点的先后顺序，递归遍历分为三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前序遍历（根 → 左 → 右）&lt;/li&gt;
&lt;li&gt;中序遍历（左 → 根 → 右）&lt;/li&gt;
&lt;li&gt;后序遍历（左 → 右 → 根）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面分别来看。&lt;/p&gt;
&lt;h3&gt;前序遍历：先根再左右&lt;/h3&gt;
&lt;p&gt;前序遍历的思路最直接：&lt;/p&gt;
&lt;p&gt;当访问一个节点时，先处理它自己（根），再进入左子树，最后右子树。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public List&amp;#x3C;Integer&gt; preorderTraversal(TreeNode root) {
        List&amp;#x3C;Integer&gt; res = new ArrayList&amp;#x3C;&gt;();
        traversal(root, res);
        return res;
    }

    public static void traversal(TreeNode node, List&amp;#x3C;Integer&gt; res) {
        if (node == null)
            return;
        res.add(node.val);
        traversal(node.left, res);
        traversal(node.right, res);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行过程中根节点最先被访问，因此称为前序。&lt;/p&gt;
&lt;p&gt;前序遍历非常适合用于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;打印树结构；&lt;/li&gt;
&lt;li&gt;序列化（保存树的形状与数值）；&lt;/li&gt;
&lt;li&gt;快速定位根节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;中序遍历&lt;/h3&gt;
&lt;p&gt;中序遍历访问顺序为：左子树 → 根节点 → 右子树。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public List&amp;#x3C;Integer&gt; inorderTraversal(TreeNode root) {
        List&amp;#x3C;Integer&gt; res = new ArrayList&amp;#x3C;&gt;();
        traversal(root, res);
        return res;
    }

    public static void traversal(TreeNode node, List&amp;#x3C;Integer&gt; res) {
        if (node == null)
            return;
        traversal(node.left, res);
        res.add(node.val);
        traversal(node.right, res);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为中序遍历的根节点总是在左右子树之间被访问，所以称为中序。&lt;/p&gt;
&lt;p&gt;中序遍历的一个重要应用是对**二叉搜索树（BST）**进行排序输出，BST 的中序遍历结果是一个递增序列。&lt;/p&gt;
&lt;h3&gt;后序遍历&lt;/h3&gt;
&lt;p&gt;后序遍历的顺序是左 → 右 → 根，即必须等左右子树都处理完，最后才轮到根节点。&lt;/p&gt;
&lt;p&gt;这种遍历往往用于删除树节点、计算子树值、求树高等需要「自底向上」的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public List&amp;#x3C;Integer&gt; postorderTraversal(TreeNode root) {
        List&amp;#x3C;Integer&gt; res = new ArrayList&amp;#x3C;&gt;();
        traversal(root, res);
        return res;
    }

    public static void traversal(TreeNode node, List&amp;#x3C;Integer&gt; res) {
        if (node == null)
            return;
        traversal(node.left, res);
        traversal(node.right, res);
        res.add(node.val);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种遍历方式可以理解为一种「收尾型」策略，只有当左右子问题都完成后，才能解决当前节点。&lt;/p&gt;
&lt;p&gt;例如在求树的深度时，每个节点的深度依赖于子树深度，这正是后序思想的典型应用。&lt;/p&gt;
&lt;h2&gt;迭代遍历&lt;/h2&gt;
&lt;p&gt;递归虽然优雅，但会占用系统调用栈，当树过深时可能导致 &lt;strong&gt;栈溢出&lt;/strong&gt;，为了更灵活、更安全，我们可以使用**显式栈（Stack）**结构来手动模拟递归过程。&lt;/p&gt;
&lt;h3&gt;前序遍历（迭代版）&lt;/h3&gt;
&lt;p&gt;前序遍历的顺序为根 → 左 → 右。&lt;/p&gt;
&lt;p&gt;要保持这一顺序，只需在栈中先压右节点，再压左节点，这样出栈时的顺序就是「根左右」。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public List&amp;#x3C;Integer&gt; preorderTraversal(TreeNode root) {
        List&amp;#x3C;Integer&gt; res = new ArrayList&amp;#x3C;&gt;();
        Deque&amp;#x3C;TreeNode&gt; stack = new LinkedList&amp;#x3C;&gt;();

        if (root == null)
            return res;
        stack.push(root);

        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            res.add(node.val);
            if (node.right != null)
                stack.push(node.right);
            if (node.left != null)
                stack.push(node.left);
        }

        return res;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;中序遍历（迭代版）&lt;/h3&gt;
&lt;p&gt;中序遍历的迭代实现要略微复杂，因为我们必须先访问到最左节点，然后再开始处理根和右子树。&lt;/p&gt;
&lt;p&gt;核心思路：&lt;/p&gt;
&lt;p&gt;用一个指针 cur 不断移动到最左节点；&lt;/p&gt;
&lt;p&gt;途中遇到的节点都暂时压入栈；&lt;/p&gt;
&lt;p&gt;当无法再往左走时，弹出栈顶节点并访问，然后转向右子树。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public List&amp;#x3C;Integer&gt; inorderTraversal(TreeNode root) {
        List&amp;#x3C;Integer&gt; res = new ArrayList&amp;#x3C;&gt;();
        if (root == null)
            return res;

        Deque&amp;#x3C;TreeNode&gt; stack = new LinkedList&amp;#x3C;&gt;();
        TreeNode cur = root;
        while (cur != null || !stack.isEmpty()) {
            if (cur != null) {
                stack.push(cur);
                cur = cur.left;
            } else {
                TreeNode node = stack.pop();
                res.add(node.val);
                cur = node.right;
            }
        }

        return res;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;后序遍历（迭代版）&lt;/h3&gt;
&lt;p&gt;后序遍历的特点是「左右根」，我们可以反向思考：&lt;/p&gt;
&lt;p&gt;如果前序遍历是「根 → 左 → 右」，那只要把左右入栈顺序反过来（右在前、左在后），结果就会变成「根 → 右 → 左」。&lt;/p&gt;
&lt;p&gt;最后再把结果列表反转，就得到了「左 → 右 → 根」的后序遍历。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public List&amp;#x3C;Integer&gt; postorderTraversal(TreeNode root) {
        List&amp;#x3C;Integer&gt; res = new ArrayList&amp;#x3C;&gt;();
        if (root == null)
            return res;

        Deque&amp;#x3C;TreeNode&gt; stack = new LinkedList&amp;#x3C;&gt;();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            res.add(node.val);
            if (node.left != null)
                stack.push(node.left);
            if (node.right != null)
                stack.push(node.right);
        }
        Collections.reverse(res);
        return res;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;层序遍历（BFS）&lt;/h2&gt;
&lt;p&gt;前面的遍历都是采用深度优先遍历（DFS）的方式，与之不同的是层序遍历采用**广度优先搜索（BFS）**策略。&lt;/p&gt;
&lt;p&gt;它一层层地访问节点，从上到下、从左到右逐级展开。&lt;/p&gt;
&lt;p&gt;实现方法通常使用**队列（Queue）**来保证先进先出。&lt;/p&gt;
&lt;p&gt;每次循环代表树的一层，len 记录当前层节点数量，从而确保逐层收集节点值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    public List&amp;#x3C;List&amp;#x3C;Integer&gt;&gt; levelOrder(TreeNode root) {
        Queue&amp;#x3C;TreeNode&gt; q = new LinkedList&amp;#x3C;&gt;();
        q.add(root);
        List&amp;#x3C;List&amp;#x3C;Integer&gt;&gt; res = new ArrayList&amp;#x3C;&gt;();

        if (root == null)
            return res;

        while (!q.isEmpty()) {
            List&amp;#x3C;Integer&gt; record = new ArrayList&amp;#x3C;&gt;();
            int len = q.size();
            for (int i = 0; i &amp;#x3C; len; i++) {
                TreeNode cur = q.remove();
                record.add(cur.val);
                if (cur.left != null)
                    q.add(cur.left);
                if (cur.right != null)
                    q.add(cur.right);
            }

            res.add(record);
        }

        return res;
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?threadLocal"/><enclosure url="http://wallpaper.csun.site/?threadLocal"/></item><item><title>责任链模式在业务流程中的应用</title><link>https://blog.csun.site/blog/2025-10-12-chain-of-responsibility-template-method-java-business-process</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-10-12-chain-of-responsibility-template-method-java-business-process</guid><description>责任链模式在复杂业务流程中的应用</description><pubDate>Sun, 12 Oct 2025 15:33:00 GMT</pubDate><content:encoded>&lt;p&gt;在复杂的业务场景中，我们经常需要将一个流程拆解为多个节点，每个节点负责处理特定的业务逻辑。为了降低节点之间的耦合度，同时提供灵活的扩展能力，可以结合 &lt;strong&gt;责任链模式&lt;/strong&gt;、&lt;strong&gt;工厂模式&lt;/strong&gt; 和 &lt;strong&gt;模板方法&lt;/strong&gt; 来实现这一目标。&lt;/p&gt;
&lt;p&gt;本文将以「查询拼团优惠」为例，演示如何构建一个可扩展、解耦的多节点业务处理框架。&lt;/p&gt;
&lt;h2&gt;定义接口&lt;/h2&gt;
&lt;p&gt;首先定义两个核心泛型接口 &lt;code&gt;StrategyMapper&lt;/code&gt; 和 &lt;code&gt;StrategyHandler&lt;/code&gt; ，其中:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;StrategyMapper&lt;/code&gt; 接口提供一个 &lt;code&gt;get()&lt;/code&gt; 方法用于获取当前的业务节点;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StrategyHandler&lt;/code&gt; 接口提供一个 &lt;code&gt;apply()&lt;/code&gt; 方法用于处理当前节点的业务逻辑。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface StrategyMapper&amp;#x3C;T, D, R&gt; {

    /**
     * 获取当前业务节点
     *
     * @param requestParameter 入参
     * @param dynamicContext   上下文
     * @return 返参
     * @throws Exception 异常
     */
    StrategyHandler&amp;#x3C;T, D, R&gt; get(T requestParameter, D dynamicContext) throws Exception;

}

public interface StrategyHandler&amp;#x3C;T, D, R&gt; {

    StrategyHandler DEFAULT = (T, D) -&gt; null;

    R apply(T requestParameter, D dynamicContext) throws Exception;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;泛型参数 &lt;code&gt;T&lt;/code&gt; 代表执行整个业务流程的入参，&lt;code&gt;D&lt;/code&gt; 代表整个业务流程执行中的上下文，&lt;code&gt;R&lt;/code&gt; 代表业务流程执行完成后的返回值&lt;/p&gt;
&lt;h2&gt;抽象模板类&lt;/h2&gt;
&lt;p&gt;为了简化每个业务节点的实现，我们可以定义一个抽象类，同时实现上述两个接口。&lt;/p&gt;
&lt;p&gt;实现一个业务节点只需要继承这个抽象类，然后重写 &lt;code&gt;doApply()&lt;/code&gt; 和 &lt;code&gt;multiThread()&lt;/code&gt; 两个抽象方法即可。。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;router()&lt;/code&gt; 是一个模板方法，用于路由到下一个节点，并执行下一个节点的 &lt;code&gt;apply()&lt;/code&gt; 方法，从而将多个节点串联成一个责任链；&lt;code&gt;doApply()&lt;/code&gt; 方法用于实现真正的业务逻辑，交由节点重写，而 &lt;code&gt;multiThread()&lt;/code&gt; 方法提供了异步加载数据的能力。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public abstract class AbstractMultiThreadStrategyRouter&amp;#x3C;T, D, R&gt; implements StrategyMapper&amp;#x3C;T, D, R&gt;, StrategyHandler&amp;#x3C;T, D, R&gt; {

    @Getter
    @Setter
    protected StrategyHandler&amp;#x3C;T, D, R&gt; defaultStrategyHandler = StrategyHandler.DEFAULT;

    public R router(T requestParameter, D dynamicContext) throws Exception {
        StrategyHandler&amp;#x3C;T, D, R&gt; strategyHandler = get(requestParameter, dynamicContext);
        if(null != strategyHandler) return strategyHandler.apply(requestParameter, dynamicContext);
        return defaultStrategyHandler.apply(requestParameter, dynamicContext);
    }

    @Override
    public R apply(T requestParameter, D dynamicContext) throws Exception {
        // 异步加载数据
        multiThread(requestParameter, dynamicContext);
        // 业务流程受理
        return doApply(requestParameter, dynamicContext);
    }

    /**
     * 异步加载数据
     */
    protected abstract void multiThread(T requestParameter, D dynamicContext) throws ExecutionException, InterruptedException, TimeoutException;

    /**
     * 业务流程受理
     */
    protected abstract R doApply(T requestParameter, D dynamicContext) throws Exception;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实际使用场景：查询拼团优惠&lt;/h2&gt;
&lt;p&gt;在查询拼团优惠的业务流程中，可能涉及 &lt;strong&gt;切量降级&lt;/strong&gt;、&lt;strong&gt;优惠试算&lt;/strong&gt;、&lt;strong&gt;人群标签过滤&lt;/strong&gt;等多个处理环节。通过责任链模式，我们可以将每个处理环节抽象为一个节点。&lt;/p&gt;
&lt;h3&gt;抽象节点类&lt;/h3&gt;
&lt;p&gt;首先定义一个抽象节点类 &lt;code&gt;AbstractGroupBuyMarketSupport&lt;/code&gt;，继承自上述的 &lt;code&gt;AbstractMultiThreadStrategyRouter&lt;/code&gt; 类，注入这些节点需要用到的共同依赖，同时提供一个缺省的 &lt;code&gt;multiThread()&lt;/code&gt; 方法，因为不是所有的节点都需要多线程加载数据。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public abstract class AbstractGroupBuyMarketSupport&amp;#x3C;MarketProductEntity, DynamicContext, TrialBalanceEntity&gt; extends AbstractMultiThreadStrategyRouter&amp;#x3C;cn.bugstack.domain.activity.model.entity.MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, cn.bugstack.domain.activity.model.entity.TrialBalanceEntity&gt; {

    protected long timeout = 500;
    @Resource
    protected IActivityRepository repository;

    @Override
    protected void multiThread(cn.bugstack.domain.activity.model.entity.MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws ExecutionException, InterruptedException, TimeoutException {
        // 缺省的方法
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;根节点与责任链工厂&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class RootNode extends AbstractGroupBuyMarketSupport&amp;#x3C;MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity&gt; {

    @Resource
    private SwitchNode switchNode;

    @Override
    protected TrialBalanceEntity doApply(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
        log.info(&quot;拼团商品查询试算服务-RootNode userId:{} requestParameter:{}&quot;, requestParameter.getUserId(), JSON.toJSONString(requestParameter));
        // 参数判断
        if (StringUtils.isBlank(requestParameter.getUserId()) || StringUtils.isBlank(requestParameter.getGoodsId()) ||
                StringUtils.isBlank(requestParameter.getSource()) || StringUtils.isBlank(requestParameter.getChannel())) {
            throw new AppException(ResponseCode.ILLEGAL_PARAMETER.getCode(), ResponseCode.ILLEGAL_PARAMETER.getInfo());
        }
        return router(requestParameter, dynamicContext);
    }

    @Override
    public StrategyHandler&amp;#x3C;MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity&gt; get(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
        return switchNode;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根节点 &lt;code&gt;RootNode&lt;/code&gt; 主要做一些参数的校验，并作为整个责任链的入口，通过一个责任链工厂对外暴露，当需要执行这个责任链的时候，只需要调用责任链工厂的 &lt;code&gt;strategyHandler()&lt;/code&gt; 方法获取到根节点，然后调用根节点的 &lt;code&gt;apply()&lt;/code&gt; 方法传入入参和空上下文即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DefaultActivityStrategyFactory {

    private final RootNode rootNode;

    public DefaultActivityStrategyFactory(RootNode rootNode) {
        this.rootNode = rootNode;
    }

    public StrategyHandler&amp;#x3C;MarketProductEntity, DynamicContext, TrialBalanceEntity&gt; strategyHandler() {
        return rootNode;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 获取执行策略
StrategyHandler&amp;#x3C;MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity&gt; strategyHandler = defaultActivityStrategyFactory.strategyHandler();
// 受理试算操作
return strategyHandler.apply(marketProductEntity, new DefaultActivityStrategyFactory.DynamicContext());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根节点的 &lt;code&gt;apply()&lt;/code&gt; 方法是继承自 &lt;code&gt;AbstractMultiThreadStrategyRouter&lt;/code&gt; 抽象类的模板方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public R apply(T requestParameter, D dynamicContext) throws Exception {
        // 异步加载数据
        multiThread(requestParameter, dynamicContext);
        // 业务流程受理
        return doApply(requestParameter, dynamicContext);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会首先调用 &lt;code&gt;multiThread()&lt;/code&gt; 方法，由于 &lt;code&gt;RootNode&lt;/code&gt; 没重写这个方法，只是一个缺省的方法，然后调用 &lt;code&gt;doApply()&lt;/code&gt; 方法，&lt;code&gt;RootNode&lt;/code&gt; 重写了这个方法去校验参数是否合理&lt;/p&gt;
&lt;p&gt;然后这个方法返回 &lt;code&gt;router(requestParameter, dynamicContext)&lt;/code&gt; 调用了 &lt;code&gt;AbstractMultiThreadStrategyRouter&lt;/code&gt; 的 &lt;code&gt;router()&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public R router(T requestParameter, D dynamicContext) throws Exception {
        StrategyHandler&amp;#x3C;T, D, R&gt; strategyHandler = get(requestParameter, dynamicContext);
        if(null != strategyHandler) return strategyHandler.apply(requestParameter, dynamicContext);
        return defaultStrategyHandler.apply(requestParameter, dynamicContext);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法会调用 &lt;code&gt;get()&lt;/code&gt; 方法得到下一个业务节点，&lt;code&gt;RootNode&lt;/code&gt; 重写的 &lt;code&gt;get()&lt;/code&gt; 方法返回了 &lt;code&gt;SwitchNode&lt;/code&gt; 节点，然后接着调用 &lt;code&gt;SwitchNode&lt;/code&gt; 节点的 &lt;code&gt;apply()&lt;/code&gt; 方法，从而实现业务节点之间的流转。&lt;/p&gt;
&lt;h3&gt;结束节点&lt;/h3&gt;
&lt;p&gt;对于结束节点 &lt;code&gt;EndNode&lt;/code&gt; 的 &lt;code&gt;doApply()&lt;/code&gt; 方法就不是调用 &lt;code&gt;router()&lt;/code&gt; 方法返回下一个节点了，而是直接返回的业务执行结果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class EndNode extends AbstractGroupBuyMarketSupport&amp;#x3C;MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity&gt; {

    @Override
    public TrialBalanceEntity doApply(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
        log.info(&quot;拼团商品查询试算服务-EndNode userId:{} requestParameter:{}&quot;, requestParameter.getUserId(), JSON.toJSONString(requestParameter));

        // 拼团活动配置
        GroupBuyActivityDiscountVO groupBuyActivityDiscountVO = dynamicContext.getGroupBuyActivityDiscountVO();

        // 商品信息
        SkuVO skuVO = dynamicContext.getSkuVO();

        // 折扣金额
        BigDecimal deductionPrice = dynamicContext.getDeductionPrice();
        // 支付金额
        BigDecimal payPrice = dynamicContext.getPayPrice();

        // 返回空结果
        return TrialBalanceEntity.builder()
                .goodsId(skuVO.getGoodsId())
                .goodsName(skuVO.getGoodsName())
                .originalPrice(skuVO.getOriginalPrice())
                .deductionPrice(deductionPrice)
                .payPrice(payPrice)
                .targetCount(groupBuyActivityDiscountVO.getTarget())
                .startTime(groupBuyActivityDiscountVO.getStartTime())
                .endTime(groupBuyActivityDiscountVO.getEndTime())
                .isVisible(dynamicContext.isVisible())
                .isEnable(dynamicContext.isEnable())
                .groupBuyActivityDiscountVO(groupBuyActivityDiscountVO)
                .build();
    }

    @Override
    public StrategyHandler&amp;#x3C;MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity&gt; get(MarketProductEntity requestParameter, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
        return defaultStrategyHandler;
    }

}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?strategyChain"/><enclosure url="http://wallpaper.csun.site/?strategyChain"/></item><item><title>深入理解 ThreadLocal</title><link>https://blog.csun.site/blog/2025-10-10-deep-understanding-of-threadlocal</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-10-10-deep-understanding-of-threadlocal</guid><description>一文搞懂 ThreadLocal 的使用方式与底层原理</description><pubDate>Fri, 10 Oct 2025 10:33:00 GMT</pubDate><content:encoded>&lt;p&gt;在多线程环境中，不同线程共享同一个变量会引发线程安全问题。有没有一种方式能让「每个线程访问到的，都是自己那份独立的变量副本」，从而避免加锁与竞争？答案就是：ThreadLocal。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ThreadLocal&lt;/code&gt; 是 Java 提供的一种 &lt;strong&gt;线程本地变量&lt;/strong&gt;（Thread-local variable），它可以让每个线程都拥有自己独立的变量副本，从而实现线程隔离。&lt;/p&gt;
&lt;h2&gt;使用示例&lt;/h2&gt;
&lt;p&gt;下面的代码展示了如何使用 &lt;code&gt;ThreadLocal&lt;/code&gt; 为每个线程设置与获取独立的变量值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    static ThreadLocal&amp;#x3C;String&gt; tl = new ThreadLocal&amp;#x3C;&gt;();

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                tl.set(&quot;线程 1 的 ThreadLocal&quot;);
                System.out.println(&quot;线程 1：&quot; + tl.get());
                tl.remove();
                System.out.println(&quot;线程 1 tl remove 之后: &quot; + tl.get());
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                tl.set(&quot;线程 2 的 ThreadLocal&quot;);
                System.out.println(&quot;线程 2：&quot; + tl.get());
                tl.remove();
                System.out.println(&quot;线程 2 tl remove 之后: &quot; + tl.get());
            }
        });

        thread1.start();
        thread2.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;线程 1：线程 1 的 ThreadLocal
线程 2：线程 2 的 ThreadLocal
线程 1 tl remove 之后: null
线程 2 tl remove 之后: null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个示例中，&lt;code&gt;ThreadLocal&lt;/code&gt; 用于存储每个线程独立的字符串值。每个线程都可以通过 &lt;code&gt;get()&lt;/code&gt; 方法获取自己的值，而不会受到其他线程的影响。&lt;/p&gt;
&lt;p&gt;调用 &lt;code&gt;remove()&lt;/code&gt; 方法可以清除当前线程的 &lt;code&gt;ThreadLocal&lt;/code&gt; 变量，避免占用线程的生命周期内存。&lt;/p&gt;
&lt;h2&gt;实现原理&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Thread&lt;/code&gt; 类中有 &lt;code&gt;threadLocals&lt;/code&gt; 和 &lt;code&gt;inheritableThreadLocals&lt;/code&gt; 两个成员变量，它们都是 &lt;code&gt;ThreadLocalMap&lt;/code&gt; 类型的对象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/10/574f4c9d157f084c915d182db336e56e.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ThreadLocalMap&lt;/code&gt; 是一个 &lt;code&gt;ThreadLocal&lt;/code&gt; 的内部类，是一个定制化的 &lt;code&gt;HashMap&lt;/code&gt;，默认情况下，这两个变量的值都是 &lt;code&gt;null&lt;/code&gt;，当第一次调用 &lt;code&gt;ThreadLocal&lt;/code&gt; 的 &lt;code&gt;set()&lt;/code&gt; 方法或 &lt;code&gt;get()&lt;/code&gt; 方法时，才会创建它们。&lt;/p&gt;
&lt;p&gt;而每个线程的本地变量并不是直接存放在 &lt;code&gt;ThreadLocal&lt;/code&gt; 实例中，而是存放在调用线程自身的 &lt;code&gt;threadLocals&lt;/code&gt; 里。该 Map 的 key 是 &lt;code&gt;ThreadLocal&lt;/code&gt; 实例（更准确地说是对它的弱引用），value 是线程本地变量的值。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;ThreadLocal&lt;/code&gt; 更像一个「工具壳」：通过 &lt;code&gt;set()&lt;/code&gt; 把 &lt;code&gt;value&lt;/code&gt; 放入当前线程的 &lt;code&gt;threadLocals&lt;/code&gt;，通过 &lt;code&gt;get()&lt;/code&gt; 再取出来。只要线程还活着，本地变量就会跟着线程存在。所以一旦不再需要该变量，应尽早 &lt;code&gt;remove()&lt;/code&gt;，特别是在「线程池」场景中，线程会被复用，泄漏风险更高。&lt;/p&gt;
&lt;p&gt;另外，&lt;code&gt;ThreadLocalMap.Entry&lt;/code&gt; 的 key 是对 &lt;code&gt;ThreadLocal&lt;/code&gt; 的弱引用，key 被 GC 后会变成「陈旧条目」，但其 value 不是弱引用，仍可能占用内存，直到下一次访问触发清理。因此养成 &lt;code&gt;try { set/use } finally { remove }&lt;/code&gt; 的习惯非常重要。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面简单分析 &lt;code&gt;ThreadLocal&lt;/code&gt; 的 &lt;code&gt;set()&lt;/code&gt; &lt;code&gt;get()&lt;/code&gt; &lt;code&gt;remove()&lt;/code&gt; 方法的实现逻辑&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;set()&lt;/code&gt; 方法&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先获取当前调用线程，然后使用它作为参数调用 &lt;code&gt;getMap()&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;getMap()&lt;/code&gt; 方法返回当前线程的 &lt;code&gt;threadLocals&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;getMap(t)&lt;/code&gt; 的返回值不为空，则把 &lt;code&gt;value&lt;/code&gt; 设置到 &lt;code&gt;threadLocals&lt;/code&gt; 中；否则说明是第一次使用，调用 &lt;code&gt;createMap(t, value)&lt;/code&gt; 创建 &lt;code&gt;ThreadLocalMap&lt;/code&gt; 并放入 &lt;code&gt;(this, value)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;getMap(t)&lt;/code&gt; 返回空值则说明是第一次调用 &lt;code&gt;set&lt;/code&gt; 方法，这时会调用 &lt;code&gt;createMap(t, value)&lt;/code&gt; 方法创建当前线程的 &lt;code&gt;threadLocals&lt;/code&gt; 变量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;get()&lt;/code&gt; 方法&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings(&quot;unchecked&quot;)
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;get()&lt;/code&gt; 方法首先获取当前线程的 &lt;code&gt;threadLocals&lt;/code&gt;，若不为 &lt;code&gt;null&lt;/code&gt;，则直接获取当前 &lt;code&gt;ThreadLocal&lt;/code&gt; 实例对应的值；&lt;/p&gt;
&lt;p&gt;否则执行 &lt;code&gt;setInitialValue()&lt;/code&gt; 进行懒初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
        return value;
    }

    protected T initialValue() {
        return null;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;initialValue()&lt;/code&gt; 默认返回 &lt;code&gt;null&lt;/code&gt;，可被子类重写。&lt;/p&gt;
&lt;p&gt;然后获取线程的 &lt;code&gt;threadLocals&lt;/code&gt; 变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果线程已有 &lt;code&gt;ThreadLocalMap&lt;/code&gt;，就放入一对 &lt;code&gt;(this, value)&lt;/code&gt; 键值对；&lt;/li&gt;
&lt;li&gt;如果这是线程第一次使用 &lt;code&gt;ThreadLocal&lt;/code&gt;，则新建 &lt;code&gt;ThreadLocalMap&lt;/code&gt; 并放入 &lt;code&gt;(this, value)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;remove&lt;/code&gt; 方法&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上代码所示，如果当前线程的 &lt;code&gt;threadLocals&lt;/code&gt; 不为空，则删除当前线程中指定 &lt;code&gt;ThreadLocal&lt;/code&gt; 实例的本地变量。&lt;/p&gt;
&lt;h2&gt;ThreadLocal 默认不支持继承&lt;/h2&gt;
&lt;p&gt;看下面这个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    static ThreadLocal&amp;#x3C;String&gt; tl = new ThreadLocal&amp;#x3C;&gt;();

    public static void main(String[] args) {
        tl.set(&quot;hello world&quot;);
        System.out.println(&quot;父线程：&quot; + tl.get());
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(&quot;子线程：&quot; + tl.get());
            }
        });
        thread.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;父线程将 &lt;code&gt;ThreadLocal&lt;/code&gt; 的值设为 &lt;code&gt;hello world&lt;/code&gt; 并打印，然后创建一个子线程去获取该值，输出如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;父线程：hello world
子线程：null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，同一个 &lt;code&gt;ThreadLocal&lt;/code&gt; 在父线程中设置的值，子线程默认是获取不到的。那有没有办法让子线程能够访问到父线程中的值呢？&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;InheritableThreadLocal&lt;/code&gt; 类&lt;/h2&gt;
&lt;p&gt;让子线程能够访问到父线程中设置的值，&lt;code&gt;InheritableThreadLocal&lt;/code&gt; 应运而生。它继承自 &lt;code&gt;ThreadLocal&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class InheritableThreadLocal&amp;#x3C;T&gt; extends ThreadLocal&amp;#x3C;T&gt; {
    protected T childValue(T parentValue) {
        return parentValue;
    }


    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }


    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由上可知，&lt;code&gt;InheritableThreadLocal&lt;/code&gt; 重写了 3 个方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;createMap()&lt;/code&gt;：第一次调用 &lt;code&gt;set&lt;/code&gt; 时创建的是 &lt;code&gt;inheritableThreadLocals&lt;/code&gt; 而非 &lt;code&gt;threadLocals&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getMap()&lt;/code&gt;：获取的是 &lt;code&gt;inheritableThreadLocals&lt;/code&gt; 而非 &lt;code&gt;threadLocals&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;childValue()&lt;/code&gt;：决定父值拷贝到子线程时的转换策略（默认直接返回父值）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那 &lt;code&gt;childValue()&lt;/code&gt; 在什么时候执行？&lt;/p&gt;
&lt;p&gt;当我们创建线程的时候会调用一个 &lt;code&gt;init()&lt;/code&gt; 方法，&lt;code&gt;init()&lt;/code&gt; 方法中会获取当前线程，也就是父线程，并判断父线程的 &lt;code&gt;inheritableThreadLocals&lt;/code&gt; 属性是否为 &lt;code&gt;null&lt;/code&gt;，不为 &lt;code&gt;null&lt;/code&gt;，则会调用 &lt;code&gt;ThreadLocal.createInheritedMap (parent.inheritableThreadLocals)&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public Thread(Runnable target) {
        init(null, target, &quot;Thread-&quot; + nextThreadNum(), 0);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private void init (ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc) {
    ...
    // 获取当前线程
    Thread parent = currentThread();
    ...
    // 如果父线程的inheritableThreadLocals变量不为null
    if (parent.inheritableThreadLocals != null)
    // 设置子线程中的inheritableThreadLocals变量
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap (parent.inheritableThreadLocals);
    this.stackSize = stackSize;
    tid = nextThreadID();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;createInheritedMap&lt;/code&gt; 内部使用父线程的 &lt;code&gt;inheritableThreadLocals&lt;/code&gt; 变量作为构造函数创建了一个新的 &lt;code&gt;ThreadLocalMap&lt;/code&gt; 变量，然后赋值给了子线程的 &lt;code&gt;inheritableThreadLocals&lt;/code&gt; 变量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面我们看看在 &lt;code&gt;ThreadLocalMap&lt;/code&gt; 的构造函数内部都做了些什么&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j &amp;#x3C; len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings(&quot;unchecked&quot;)
                    ThreadLocal&amp;#x3C;Object&gt; key = (ThreadLocal&amp;#x3C;Object&gt;) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode &amp;#x26; (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在该构造函数内部，会把父线程的 &lt;code&gt;inheritableThreadLocals&lt;/code&gt; 的条目复制到新的 &lt;code&gt;ThreadLocalMap&lt;/code&gt; 中，并对每一项调用 &lt;code&gt;InheritableThreadLocal#childValue()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;将上一小节的示例代码改为使用 &lt;code&gt;InheritableThreadLocal&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    static ThreadLocal&amp;#x3C;String&gt; tl = new InheritableThreadLocal&amp;#x3C;&gt;();

    public static void main(String[] args) {
        tl.set(&quot;hello world&quot;);
        System.out.println(&quot;父线程：&quot; + tl.get());
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(&quot;子线程：&quot; + tl.get());
            }
        });
        thread.start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时子线程可以获取父线程中的值，输出如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;父线程：hello world
子线程：hello world
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?threadLocal"/><enclosure url="http://wallpaper.csun.site/?threadLocal"/></item><item><title>剖析 HashMap</title><link>https://blog.csun.site/blog/2025-09-23-analysis-hashmap</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-09-23-analysis-hashmap</guid><description>深入解析 HashMap 结构</description><pubDate>Tue, 23 Sep 2025 19:14:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;HashMap&lt;/code&gt; 是一个键值对存储结构，通过一个「键」（Key）可以快速地查找到对应的「值」（Value）&lt;/p&gt;
&lt;h2&gt;Map 接口&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Map&lt;/code&gt; 有键和值的概念。一个键映射到一个值，&lt;code&gt;Map&lt;/code&gt; 按照键存储和访问值，键不能重复，即一个键只会
存储一份，给同一个键重复设值会覆盖原来的值，使用 &lt;code&gt;Map&lt;/code&gt; 可以方便地处理需要根据键访问对象的场景&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Map&lt;/code&gt; 接口的定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface Map&amp;#x3C;K,V&gt; {
	int size();  // 返回 Map 中键值对的数量
	boolean isEmpty();  // 判断 map 是否为空
	boolean containsKey(Object key);  // 判断 map 是否包含指定的键
	boolean containsValue(Object value);  // 判断 map 是否包含指定的值
	V get(Object key);  // 根据键获取值，不存在键返回 null
	V put(K key, V value);  // 存入键值对，如果原来 key 存在，返回原来的值
	V remove(Object key);  // 删除键值对，返回值
	void putAll(Map&amp;#x3C;? extends K, ? extends V&gt; m);  // 保存 m 中所有键值对到当前 map
	void clear();  // 清空 map
	Set&amp;#x3C;K&gt; keySet(); // 返回一个 Set 包含 map 中所有的键
	Collection&amp;#x3C;V&gt; values();  // 返回一个 Collection 包含 map 中所有的值
	Set&amp;#x3C;Map.Entry&amp;#x3C;K, V&gt;&gt; entrySet();  // 返回一个 Entry 集合

	interface Entry&amp;#x3C;K,V&gt; {  // 键值对接口
		K getKey();
		V getValue();
		V setValue(V value);
		boolean equals(Object o);
		int hashCode();
		// 返回一个比较器，默认按照键的字典序排序
		public static &amp;#x3C;K extends Comparable&amp;#x3C;? super K&gt;, V&gt; Comparator&amp;#x3C;Map.Entry&amp;#x3C;K,V&gt;&gt; comparingByKey() {
            return (Comparator&amp;#x3C;Map.Entry&amp;#x3C;K, V&gt;&gt; &amp;#x26; Serializable)
                (c1, c2) -&gt; c1.getKey().compareTo(c2.getKey());
        }
		// 返回一个比较器，按照值的字典序排序
        public static &amp;#x3C;K, V extends Comparable&amp;#x3C;? super V&gt;&gt; Comparator&amp;#x3C;Map.Entry&amp;#x3C;K,V&gt;&gt; comparingByValue() {
            return (Comparator&amp;#x3C;Map.Entry&amp;#x3C;K, V&gt;&gt; &amp;#x26; Serializable)
                (c1, c2) -&gt; c1.getValue().compareTo(c2.getValue());
        }
		// 返回一个比较器，使用给定的 Comparator 按键比较
        public static &amp;#x3C;K, V&gt; Comparator&amp;#x3C;Map.Entry&amp;#x3C;K, V&gt;&gt; comparingByKey(Comparator&amp;#x3C;? super K&gt; cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator&amp;#x3C;Map.Entry&amp;#x3C;K, V&gt;&gt; &amp;#x26; Serializable)
                (c1, c2) -&gt; cmp.compare(c1.getKey(), c2.getKey());
        }
        public static &amp;#x3C;K, V&gt; Comparator&amp;#x3C;Map.Entry&amp;#x3C;K, V&gt;&gt; comparingByValue(Comparator&amp;#x3C;? super V&gt; cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator&amp;#x3C;Map.Entry&amp;#x3C;K, V&gt;&gt; &amp;#x26; Serializable)
                (c1, c2) -&gt; cmp.compare(c1.getValue(), c2.getValue());
        }
	}
	
	boolean equals(Object o);
	int hashCode();
	// 根据键返回值，如果键不存在，返回给定的默认值 defaultValue
    default V getOrDefault(Object key, V defaultValue);
	// 对 map 中的每个键值对执行给定的操作
	default void forEach(BiConsumer&amp;#x3C;? super K, ? super V&gt; action)；
	// 将每个键值对的值替换为对该键值对调用给定函数的结果
	default void replaceAll(BiFunction&amp;#x3C;? super K, ? super V, ? extends V&gt; function)；
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HashMap&lt;/h2&gt;
&lt;h3&gt;内部组成&lt;/h3&gt;
&lt;p&gt;在 Java8 中 HashMap 底层的数据结构是 &lt;strong&gt;数组 + 链表/红黑树&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HashMap&lt;/code&gt; 内部主要有以下实例变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;transient Node&amp;#x3C;K,V&gt;[] table;
transient Set&amp;#x3C;Map.Entry&amp;#x3C;K,V&gt;&gt; entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;

// 继承自 AbstractMap&amp;#x3C;K,V&gt;
transient Set&amp;#x3C;K&gt; keySet;
transient Collection&amp;#x3C;V&gt; values;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;entrySet&lt;/code&gt; &lt;code&gt;keySet&lt;/code&gt; &lt;code&gt;values&lt;/code&gt; 分别缓存了 &lt;code&gt;HashMap&lt;/code&gt; 的键值对、键和值，size 表示实际键值对的个数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;threshold&lt;/code&gt; 表示阈值，当键值对个数 &lt;code&gt;size&lt;/code&gt; 大于等于 &lt;code&gt;threshold&lt;/code&gt; 时考虑进行扩展，一般而言，&lt;code&gt;threshold&lt;/code&gt; 等于 &lt;code&gt;table.length&lt;/code&gt; 乘以 &lt;code&gt;loadFactor&lt;/code&gt;。&lt;code&gt;loadFactor&lt;/code&gt; 是负载因子，表示整体上 &lt;code&gt;table&lt;/code&gt; 被占用的程度，是一个浮点数，默认为 0.75, 可以通过构造方法进行修改。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;table&lt;/code&gt; 是一个 &lt;code&gt;Node&lt;/code&gt; 类型的数组，每一个元素称为一个&lt;strong&gt;哈希桶&lt;/strong&gt;，其中的每个元素指向一个单向链表，链表的每个节点表示一个键值对。&lt;code&gt;Node&lt;/code&gt; 是一个静态内部类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    static class Node&amp;#x3C;K,V&gt; implements Map.Entry&amp;#x3C;K,V&gt; {
        final int hash;
        final K key;
        V value;
        Node&amp;#x3C;K,V&gt; next;

        Node(int hash, K key, V value, Node&amp;#x3C;K,V&gt; next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + &quot;=&quot; + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry&amp;#x3C;?,?&gt; e = (Map.Entry&amp;#x3C;?,?&gt;)o;
                if (Objects.equals(key, e.getKey()) &amp;#x26;&amp;#x26;
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;key&lt;/code&gt; 和 &lt;code&gt;value&lt;/code&gt; 分别表示键和值，&lt;code&gt;next&lt;/code&gt; 指向下一个 &lt;code&gt;Node&lt;/code&gt;，&lt;code&gt;hash&lt;/code&gt; 是键的哈希值，直接存储 &lt;code&gt;hash&lt;/code&gt; 值便于在比较的时候加快计算&lt;/p&gt;
&lt;h3&gt;构造函数&lt;/h3&gt;
&lt;p&gt;默认的无参构造函数，构建一个空的 HashMap，使用默认初始容量（16）和默认负载因子（0.75）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

	static final int DEFAULT_INITIAL_CAPACITY = 1 &amp;#x3C;&amp;#x3C; 4;
	static final float DEFAULT_LOAD_FACTOR = 0.75f;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以指定初始容量和负载因子&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity &amp;#x3C; 0)
            throw new IllegalArgumentException(&quot;Illegal initial capacity: &quot; +
                                               initialCapacity);
        if (initialCapacity &gt; MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor &amp;#x3C;= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException(&quot;Illegal load factor: &quot; +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
	}
   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;this.threshold = tableSizeFor(initialCapacity);&lt;/code&gt; 的目的是&lt;strong&gt;计算出大于或等于给定容量 &amp;#x3C;&lt;code&gt;initialCapacity&lt;/code&gt; 的最小的 2 的幂次方数&lt;/strong&gt;作为 table 容量 暂存到 &lt;code&gt;threshold&lt;/code&gt; 字段里&lt;/p&gt;
&lt;h3&gt;保存键值对 put&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;put&lt;/code&gt; 方法用于将一个键值对保存到 HashMap 中，代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要是先调用 &lt;code&gt;hash&lt;/code&gt; 方法计算 key 的 hash 值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;key&lt;/code&gt; 是 &lt;code&gt;null&lt;/code&gt;：直接返回 &lt;code&gt;0&lt;/code&gt;。这是 &lt;code&gt;HashMap&lt;/code&gt; 的规定，&lt;code&gt;null&lt;/code&gt; key 的哈希值就是 0。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;key&lt;/code&gt; 不是 &lt;code&gt;null&lt;/code&gt;：执行 &lt;code&gt;(h = key.hashCode()) ^ (h &gt;&gt;&gt; 16)&lt;/code&gt;，调用 key 对象自身的 &lt;code&gt;hashCode()&lt;/code&gt; 方法，然后将 &lt;code&gt;h&lt;/code&gt; 的二进制表示向右移动 16 位，再与 h 异或，&lt;strong&gt;将原始哈希码的高 16 位信息与低 16 位信息混合在了一起，减少 hash 冲突&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h &gt;&gt;&gt; 16);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后调用 &lt;code&gt;putVal&lt;/code&gt; 方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;boolean onlyIfAbsent&lt;/code&gt;&lt;/strong&gt;: 一个非常重要的标志位。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果为 &lt;code&gt;true&lt;/code&gt;（对应 &lt;code&gt;putIfAbsent&lt;/code&gt; 方法），那么只有当 &lt;code&gt;key&lt;/code&gt; 不存在时才会插入 &lt;code&gt;value&lt;/code&gt;。如果 &lt;code&gt;key&lt;/code&gt; 已存在，则不做任何操作。&lt;/li&gt;
&lt;li&gt;如果为 &lt;code&gt;false&lt;/code&gt;（对应 &lt;code&gt;put&lt;/code&gt; 方法），那么无论 &lt;code&gt;key&lt;/code&gt; 是否存在，都会用新的 &lt;code&gt;value&lt;/code&gt; 覆盖旧的 &lt;code&gt;value&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;boolean evict&lt;/code&gt;&lt;/strong&gt;: 这个参数主要用于 &lt;code&gt;LinkedHashMap&lt;/code&gt;（&lt;code&gt;HashMap&lt;/code&gt; 的子类）。在 &lt;code&gt;HashMap&lt;/code&gt; 本身中，这个值总是 &lt;code&gt;true&lt;/code&gt;。在 &lt;code&gt;LinkedHashMap&lt;/code&gt; 中，当它被用作实现 LRU 缓存时，&lt;code&gt;evict&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt; 可能表示这是一个「预备」插入，暂时不触发淘汰策略。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先判断 &lt;code&gt;table&lt;/code&gt; 是不是空表，是的话调用 &lt;code&gt;resize()&lt;/code&gt; 方法创建一个新表&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;tab[i = (n - 1) &amp;#x26; hash]) == null&lt;/code&gt; 表示无哈希冲突，就直接创建一个新的 Node 节点并放入这个桶中&lt;/p&gt;
&lt;p&gt;否则表示存在哈希冲突，又分为几种情况&lt;/p&gt;
&lt;p&gt;1）桶的第一个节点就是目标 key&lt;/p&gt;
&lt;p&gt;&lt;code&gt;(p.hash == hash &amp;#x26;&amp;#x26; ((k = p.key) == key || (key != null &amp;#x26;&amp;#x26; key.equals(k))))&lt;/code&gt; 首先比较哈希值，如果哈希值不同肯定不同，即使哈希值相同了可能存在哈希冲突也不一定相同；接下来检查两个 key 的引用是否相同，如果引用不同再调用 key 的 &lt;code&gt;equals()&lt;/code&gt; 方法进行比较&lt;/p&gt;
&lt;p&gt;如果确认是同一个 key，就把 &lt;code&gt;p&lt;/code&gt; 赋值给 &lt;code&gt;e&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;2）桶内是红黑树且第一个节点不是目标 key&lt;/p&gt;
&lt;p&gt;调用 &lt;code&gt;((TreeNode&amp;#x3C;K,V&gt;)p).putTreeVal(this, tab, hash, key, value);&lt;/code&gt; 方法&lt;/p&gt;
&lt;p&gt;3）桶内是链表且第一个节点不是目标 key&lt;/p&gt;
&lt;p&gt;&lt;code&gt;binCount&lt;/code&gt; 是一个无限循环，同时还记录了链表长度，通过 &lt;code&gt;e=p.next&lt;/code&gt; &lt;code&gt;p=e&lt;/code&gt; 遍历链表，如果 &lt;code&gt;e==null&lt;/code&gt; 说明遍历结束了，仍然没有找到相同的 key，就将新节点插入到链表后面 &lt;code&gt;p.next = newNode(hash, key, value, null)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果插入之后 &lt;code&gt;binCount &gt;= TREEIFY_THRESHOLD-1&lt;/code&gt; &lt;code&gt;TREEIFY_THRESHOLD&lt;/code&gt; 是&lt;strong&gt;一个常量值为 8，也就是链表长度达到 8&lt;/strong&gt; 时需要对链表进行树化，转换为红黑树，调用 &lt;code&gt;treeifyBin(tab, hash)&lt;/code&gt; 方法，&lt;strong&gt;该方法内部还会检查当前 table 数组的长度是否小于 MIN_TREEIFY_CAPACITY（默认 64）&lt;/strong&gt;，如果小于，它会优先选择扩容 resize() 而不是树化。&lt;/p&gt;
&lt;p&gt;如果遍历过程中找到了相同的 key，就直接退出循环&lt;/p&gt;
&lt;p&gt;最后判断如果 &lt;code&gt;e!=null&lt;/code&gt; 则说明存在一个相同 key 的节点，就根据 &lt;code&gt;onlyIfAbsent&lt;/code&gt; 的值判断是否要替换节点的值&lt;/p&gt;
&lt;p&gt;&lt;code&gt;afterNodeAccess(e)&lt;/code&gt;: 这是一个回调方法，&lt;code&gt;HashMap&lt;/code&gt; 本身是空实现。但 &lt;code&gt;LinkedHashMap&lt;/code&gt; 会重写它，用来将访问过的节点移动到链表末尾，以实现 LRU 顺序&lt;/p&gt;
&lt;p&gt;如果 e 是 null，说明插入了一个全新的节点，需要修改 &lt;code&gt;modCount&lt;/code&gt; 和 &lt;code&gt;size&lt;/code&gt; 的值，如果 &lt;code&gt;size &gt; threshold&lt;/code&gt; 还需要进行扩容&lt;/p&gt;
&lt;p&gt;&lt;code&gt;afterNodeInsertion(evict)&lt;/code&gt;: 另一个回调方法。&lt;code&gt;HashMap&lt;/code&gt; 是空实现，&lt;code&gt;LinkedHashMap&lt;/code&gt; 会重写它来处理新插入节点的链接关系&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node&amp;#x3C;K,V&gt;[] tab; Node&amp;#x3C;K,V&gt; p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length; // 创建 table
        if ((p = tab[i = (n - 1) &amp;#x26; hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node&amp;#x3C;K,V&gt; e; K k;
            if (p.hash == hash &amp;#x26;&amp;#x26;
                ((k = p.key) == key || (key != null &amp;#x26;&amp;#x26; key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode&amp;#x3C;K,V&gt;)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount &gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &amp;#x26;&amp;#x26;
                        ((k = e.key) == key || (key != null &amp;#x26;&amp;#x26; key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size &gt; threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;resize()&lt;/code&gt; 方法负责在 HashMap 的容量不足时进行扩容和数据迁移&lt;/p&gt;
&lt;p&gt;整个方法可以分成两个部分：计算新容量和阈值并扩容、迁移数据&lt;/p&gt;
&lt;p&gt;先看计算新容量和阈值，分为三种情况：&lt;/p&gt;
&lt;p&gt;1）Map 已经有数据了，需要扩容，此时 &lt;code&gt;oldCap &gt; 0&lt;/code&gt; 即 &lt;code&gt;oldTab.length &gt; 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;首先判断容量是否达到最大值，即 &lt;code&gt;oldCap &gt;= MAXIMUM_CAPACITY&lt;/code&gt; 如果是的话就将阈值设置为 &lt;code&gt;Integer.MAX_VALUE&lt;/code&gt; 这样以后就不会触发扩容了&lt;/p&gt;
&lt;p&gt;如果容量没有达到最大值，就&lt;strong&gt;将新容量设置成原来的 2 倍&lt;/strong&gt;，&lt;code&gt;newCap = oldCap &amp;#x3C;&amp;#x3C; 1&lt;/code&gt;，并且新阈值也变为老阈值的 2 倍&lt;/p&gt;
&lt;p&gt;2）Map 未初始化但是指定了初始容量&lt;/p&gt;
&lt;p&gt;此时 &lt;code&gt;oldThr &gt; 0&lt;/code&gt; 因为在构造函数中将初始容量存在了阈值中，新容量就等于阈值 &lt;code&gt;newCap = oldThr&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;3）Map 未初始化，且未指定初始容量，即使用无参构造函数&lt;/p&gt;
&lt;p&gt;此时 &lt;code&gt;newCap = DEFAULT_INITIAL_CAPACITY&lt;/code&gt; 默认初始容量为 16，&lt;code&gt;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)&lt;/code&gt; 阈值 = 容量 * 负载因子&lt;/p&gt;
&lt;p&gt;在 1) 2) 两种情况下都没有对 &lt;code&gt;newThr&lt;/code&gt; 赋值，所以接下来判断 &lt;code&gt;if (newThr == 0)&lt;/code&gt; 则要计算新阈值&lt;/p&gt;
&lt;p&gt;最后使用 &lt;code&gt;newCap&lt;/code&gt; 创建一个新数组，将 &lt;code&gt;table&lt;/code&gt; 设置为新数组&lt;/p&gt;
&lt;p&gt;再来看数据迁移，数据迁移时遍历旧数组 &lt;code&gt;oldTab&lt;/code&gt;，将所有元素移动到新数组 &lt;code&gt;newTab&lt;/code&gt;，也分为三种情况&lt;/p&gt;
&lt;p&gt;1）如果桶中只有一个元素&lt;/p&gt;
&lt;p&gt;直接用元素的 &lt;code&gt;hash&lt;/code&gt; 值和新容量 &lt;code&gt;newCap&lt;/code&gt; 计算出它在新数组中的索引，然后放进去即可&lt;/p&gt;
&lt;p&gt;2）如果桶的数据结构是红黑树，调用 &lt;code&gt;((TreeNode&amp;#x3C;K,V&gt;)e).split(this, newTab, j, oldCap);&lt;/code&gt; 方法&lt;/p&gt;
&lt;p&gt;&lt;code&gt;spilt()&lt;/code&gt; 方法中有一句&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (lc &amp;#x3C;= UNTREEIFY_THRESHOLD)
	tab[index] = loHead.untreeify(map);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如果红黑树的节点数小于等于 &lt;code&gt;UNTREEIFY_THRESHOLD&lt;/code&gt; (值为 6) 需要退化成链表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;3）如果桶的数据结构是链表&lt;/p&gt;
&lt;p&gt;这部分利用了一个非常巧妙的位运算技巧来避免重新计算每个元素的 hash 索引&lt;/p&gt;
&lt;p&gt;扩容时，新容量 &lt;code&gt;newCap&lt;/code&gt; 是旧容量 &lt;code&gt;oldCap&lt;/code&gt; 的两倍，&lt;code&gt;oldCap&lt;/code&gt; 是一个 2 的幂（例如 16，二进制为 &lt;code&gt;00010000&lt;/code&gt;）。&lt;code&gt;newCap&lt;/code&gt; 是 &lt;code&gt;oldCap&lt;/code&gt; 的两倍（例如 32，二进制为 &lt;code&gt;00100000&lt;/code&gt;），所以 &lt;code&gt;newCap - 1&lt;/code&gt; 比 &lt;code&gt;oldCap - 1&lt;/code&gt; 在二进制表示上多了一个高位的 &lt;code&gt;1&lt;/code&gt;，并且这个 &lt;code&gt;1&lt;/code&gt; 就是 &lt;code&gt;oldCap&lt;/code&gt; 的唯一一个 &lt;code&gt;1&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;旧索引：&lt;code&gt;index = hash &amp;#x26; (oldCap - 1)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新索引：&lt;code&gt;index = hash &amp;#x26; (newCap - 1)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这意味着，一个元素在新表中的位置，&lt;strong&gt;要么 和它在旧表中的位置一样，要么是 旧位置 + oldCap&lt;/strong&gt;，这取决于 &lt;code&gt;newCap - 1&lt;/code&gt; 中多的那一位 1 对应元素 hash 值的位置是 0 还是 1。这个位置恰好就是 oldCap 的二进制表示中 1 所在的那一位，所以判断 &lt;code&gt;(e.hash &amp;#x26; oldCap)&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果结果为 &lt;code&gt;0&lt;/code&gt;：说明 &lt;code&gt;hash&lt;/code&gt; 值在那一位上是 &lt;code&gt;0&lt;/code&gt;。该元素的新索引与旧索引相同。&lt;/li&gt;
&lt;li&gt;如果结果不为 &lt;code&gt;0&lt;/code&gt;：说明 &lt;code&gt;hash&lt;/code&gt; 值在那一位上是 &lt;code&gt;1&lt;/code&gt;。该元素的新索引是 &lt;code&gt;旧索引 + oldCap&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码遍历链表，根据 &lt;code&gt;(e.hash &amp;#x26; oldCap)&lt;/code&gt; 的结果，将节点分别串联到两个新链表上：&lt;code&gt;lo&lt;/code&gt; 链表（低位，位置不变）和 &lt;code&gt;hi&lt;/code&gt; 链表（高位，位置偏移）。遍历完成后，&lt;code&gt;lo&lt;/code&gt; 链表被放到新数组的 &lt;code&gt;j&lt;/code&gt; 位置，&lt;code&gt;hi&lt;/code&gt; 链表被放到新数组的 &lt;code&gt;j + oldCap&lt;/code&gt; 位置。这样就高效地完成了整个链表的数据迁移。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    final Node&amp;#x3C;K,V&gt;[] resize() {
        Node&amp;#x3C;K,V&gt;[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
		// Map 已经有数据了，需要扩容
        if (oldCap &gt; 0) {
            if (oldCap &gt;= MAXIMUM_CAPACITY) {
				// 容量已经达到了最大值，将阈值设为最大值，以后永远不会触发扩容
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
			// 新容量设置为原来容量的 2 倍
            else if ((newCap = oldCap &amp;#x3C;&amp;#x3C; 1) &amp;#x3C; MAXIMUM_CAPACITY &amp;#x26;&amp;#x26;
                     oldCap &gt;= DEFAULT_INITIAL_CAPACITY)
				// 新阈值也变为老阈值的 2 倍
                newThr = oldThr &amp;#x3C;&amp;#x3C; 1; // double threshold
        }
		// Map 未初始化但是指定了初始容量，初始容量是存在阈值里面的
        else if (oldThr &gt; 0) // initial capacity was placed in threshold
            newCap = oldThr;
		// 使用默认的无参构造函数
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
			// 阈值 = 容量 * 负载因子
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {  // 指定了初始容量的情况
            float ft = (float)newCap * loadFactor;  // 计算阈值
            newThr = (newCap &amp;#x3C; MAXIMUM_CAPACITY &amp;#x26;&amp;#x26; ft &amp;#x3C; (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({&quot;rawtypes&quot;,&quot;unchecked&quot;})
        Node&amp;#x3C;K,V&gt;[] newTab = (Node&amp;#x3C;K,V&gt;[])new Node[newCap];
        table = newTab;
		// 数据迁移
        if (oldTab != null) {
            for (int j = 0; j &amp;#x3C; oldCap; ++j) {  // 遍历旧数组的每个桶
                Node&amp;#x3C;K,V&gt; e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null; // 释放旧数组的引用，便于GC
                    if (e.next == null) // 桶中只有一个元素
						// 直接用元素的 hash 值和新容量 newCap 计算出它在新数组中的索引，然后放进去。
                        newTab[e.hash &amp;#x26; (newCap - 1)] = e;
					// 桶中是红黑树
                    else if (e instanceof TreeNode)
                        ((TreeNode&amp;#x3C;K,V&gt;)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node&amp;#x3C;K,V&gt; loHead = null, loTail = null;
                        Node&amp;#x3C;K,V&gt; hiHead = null, hiTail = null;
                        Node&amp;#x3C;K,V&gt; next;
                        do {
                            next = e.next;
							// 元素的新索引与旧索引相同，链表放在数组的原始位置
                            if ((e.hash &amp;#x26; oldCap) == 0) {
                                if (loTail == null)  // 尾插法插入到链表
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
							// 否则链表放在高位
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查找方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;get()&lt;/code&gt; 方法通过键获取值，代码为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public V get(Object key) {
        Node&amp;#x3C;K,V&gt; e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先调用 &lt;code&gt;hash()&lt;/code&gt; 方法计算 key 的 hash，然后调用 &lt;code&gt;getNode()&lt;/code&gt; 方法获取 key 对应的节点&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    final Node&amp;#x3C;K,V&gt; getNode(int hash, Object key) {
        Node&amp;#x3C;K,V&gt;[] tab; Node&amp;#x3C;K,V&gt; first, e; int n; K k;
        if ((tab = table) != null &amp;#x26;&amp;#x26; (n = tab.length) &gt; 0 &amp;#x26;&amp;#x26;
            (first = tab[(n - 1) &amp;#x26; hash]) != null) {
            if (first.hash == hash &amp;#x26;&amp;#x26; // always check first node
                ((k = first.key) == key || (key != null &amp;#x26;&amp;#x26; key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode&amp;#x3C;K,V&gt;)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &amp;#x26;&amp;#x26;
                        ((k = e.key) == key || (key != null &amp;#x26;&amp;#x26; key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先检查 HashMap 中是否有数据，没有直接返回 null。然后根据检查对应哈希桶的第一个节点是否是所需的 key，是则直接返回。&lt;/p&gt;
&lt;p&gt;如果第一个节点不是，且有下一个节点，先判断桶内是不是红黑树，是则调用 &lt;code&gt;((TreeNode&amp;#x3C;K,V&gt;)first).getTreeNode(hash, key);&lt;/code&gt; 如果桶内是链表，就遍历链表知道找到对应的 key&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;code&gt;containsKey()&lt;/code&gt; 方法判断是否包含某个 key，逻辑与 &lt;code&gt;get()&lt;/code&gt; 方法相同，如果返回的节点不为 &lt;code&gt;null&lt;/code&gt; 说明存在这个 key&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‍&lt;/p&gt;
&lt;p&gt;&lt;code&gt;containsValue()&lt;/code&gt; 方法判断是否包含某个 value，HashMap 可以方便高效地按照键进行操作，但如果需要根据值进行操作，则需要遍历，从 table 的第一个链表开始，从上到下，从左到右逐个节点进行访问,比较值，直到找到为止&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public boolean containsValue(Object value) {
        Node&amp;#x3C;K,V&gt;[] tab; V v;
        if ((tab = table) != null &amp;#x26;&amp;#x26; size &gt; 0) {
            for (int i = 0; i &amp;#x3C; tab.length; ++i) {
                for (Node&amp;#x3C;K,V&gt; e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null &amp;#x26;&amp;#x26; value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;根据键删除键值对 remove&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;remove()&lt;/code&gt; 方法用于根据 key 删除键值对并返回删除的值，主要是调用了 &lt;code&gt;removeNode()&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public V remove(Object key) {
        Node&amp;#x3C;K,V&gt; e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;matchValue&lt;/code&gt; 为 true 时仅当值相等时移除，&lt;code&gt;movable&lt;/code&gt; 为 false 时移除过程中不移动其他节点&lt;/p&gt;
&lt;p&gt;一开始是查找 key 对应的节点，逻辑和 &lt;code&gt;getNode()&lt;/code&gt; 方法中相同，如果没找到节点就直接返回 null&lt;/p&gt;
&lt;p&gt;找到节点之后，判断下桶内是不是红黑树，是就调用 &lt;code&gt;((TreeNode&amp;#x3C;K,V&gt;)node).removeTreeNode(this, tab, movable);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果是链表，且是第一个节点，就 &lt;code&gt;tab[index] = node.next;&lt;/code&gt; 不是就 &lt;code&gt;p.next = node.next;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    final Node&amp;#x3C;K,V&gt; removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node&amp;#x3C;K,V&gt;[] tab; Node&amp;#x3C;K,V&gt; p; int n, index;
        if ((tab = table) != null &amp;#x26;&amp;#x26; (n = tab.length) &gt; 0 &amp;#x26;&amp;#x26;
            (p = tab[index = (n - 1) &amp;#x26; hash]) != null) {
            Node&amp;#x3C;K,V&gt; node = null, e; K k; V v;
            if (p.hash == hash &amp;#x26;&amp;#x26;
                ((k = p.key) == key || (key != null &amp;#x26;&amp;#x26; key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode&amp;#x3C;K,V&gt;)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &amp;#x26;&amp;#x26;
                            ((k = e.key) == key ||
                             (key != null &amp;#x26;&amp;#x26; key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null &amp;#x26;&amp;#x26; (!matchValue || (v = node.value) == value ||
                                 (value != null &amp;#x26;&amp;#x26; value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode&amp;#x3C;K,V&gt;)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?hashMap"/><enclosure url="http://wallpaper.csun.site/?hashMap"/></item><item><title>剖析 ArrayDeque</title><link>https://blog.csun.site/blog/2025-09-22-analysis-arraydeque</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-09-22-analysis-arraydeque</guid><description>高效双端队列 ArrayDeque 解析</description><pubDate>Mon, 22 Sep 2025 17:14:00 GMT</pubDate><content:encoded>&lt;p&gt;除了 &lt;code&gt;LinkedList&lt;/code&gt;，Java 容器类中还有一个双端队列的实现类 &lt;code&gt;ArrayDeque&lt;/code&gt;，它是基于数组实现的。一般而言，由于需要移动元素，数组的插入和删除效率比较低，但 &lt;code&gt;ArrayDeque&lt;/code&gt; 的效率却非常高。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ArrayDeque&lt;/code&gt; 实现了 &lt;code&gt;Deque&lt;/code&gt; 接口，同 &lt;code&gt;LinkedList&lt;/code&gt; 一样，它的队列长度也是没有限制的，&lt;code&gt;Deque&lt;/code&gt; 扩展了 &lt;code&gt;Queue&lt;/code&gt;，有队列的所有方法，还可以看作栈，栈的基本方法 &lt;code&gt;push/pop/peek&lt;/code&gt;，还有明确的操作两端的方法如 &lt;code&gt;addFirst/removeLast&lt;/code&gt; 等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ArrayDeque&lt;/code&gt; 内部有以下实例变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;transient Object[] elements;

transient int head;

transient int tail;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;elements&lt;/code&gt; 是存储元素的数组。&lt;code&gt;ArrayDeque&lt;/code&gt; 的高效来源于 &lt;code&gt;head&lt;/code&gt; 和 &lt;code&gt;tail&lt;/code&gt; 这两个变量，它们使得物理上简单的从头到尾的数组变为了一个逻辑上循环的数组，避免了在头尾操作时的移动。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在两端添加、删除元素的效率很高，动态扩展需要的内存分配以及数组复制开销可以被平摊，具体来说，添加$N$个元素的效率为$O(N)$。&lt;/li&gt;
&lt;li&gt;根据元素内容查找和删除的效率比较低，为$O(N)$。&lt;/li&gt;
&lt;li&gt;与 &lt;code&gt;ArrayList&lt;/code&gt; 和 &lt;code&gt;LinkedList&lt;/code&gt; 不同，没有索引位置的概念，不能根据索引位置进行操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;循环数组&lt;/h2&gt;
&lt;p&gt;循环数组是指元素到数组尾部之后可以接着从数组头开始，数组的长度、第一个和最后一个元素都与 &lt;code&gt;head&lt;/code&gt; 和 &lt;code&gt;tail&lt;/code&gt; 这两个变量有关:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;head&lt;/code&gt; 和 &lt;code&gt;tail&lt;/code&gt; 相同且数组为空，说明队列长度为 0&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;tail&lt;/code&gt; 大于 &lt;code&gt;head&lt;/code&gt;，则第一个元素为 &lt;code&gt;elements[head]&lt;/code&gt;，最后一个元素为 &lt;code&gt;elements[tail-1]&lt;/code&gt;，队列长度为 &lt;code&gt;tail-head&lt;/code&gt;，元素索引从 &lt;code&gt;head&lt;/code&gt; 到 &lt;code&gt;tail-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;tail&lt;/code&gt; 小于 &lt;code&gt;head&lt;/code&gt;, 且为 0, 则第一个元素为 &lt;code&gt;elements[head]&lt;/code&gt;, 最后一个为 &lt;code&gt;elements[elements.length-1]&lt;/code&gt;, 元素索引从 &lt;code&gt;head&lt;/code&gt; 到 &lt;code&gt;elements.length-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;tail&lt;/code&gt; 小于 &lt;code&gt;head&lt;/code&gt;, 且大于 0, 则会形成循环, 第一个元素为 &lt;code&gt;elements[head]&lt;/code&gt;, 最后一个是 &lt;code&gt;elements[tail-1]&lt;/code&gt;, 元素索引从 &lt;code&gt;head&lt;/code&gt; 到 &lt;code&gt;elements.length-1&lt;/code&gt;, 然后再从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;tail-1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/09/c55e3e037628dae1a373a387ced12005.png&quot; alt=&quot;QQ_1758457583201&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;D:%5Ctool%5CTypora%5Cassets%5CQQ_1758457606535-20250921202647-y1g7qrq.png&quot; alt=&quot;QQ_1758457606535&quot;&gt;&lt;/p&gt;
&lt;h2&gt;构造方法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ArrayDeque&lt;/code&gt; 有如下构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public ArrayDeque()
public ArrayDeque(int numElements)
public ArrayDeque(Collection&amp;#x3C;? extends E&gt; c)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先看无参构造函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public ArrayDeque() {
        elements = new Object[16];
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认会创建一个长度为 16 的数组&lt;/p&gt;
&lt;p&gt;如果有参数 &lt;code&gt;numElements&lt;/code&gt;，会调用 &lt;code&gt;allocateElements&lt;/code&gt; 方法构造一个数组&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public ArrayDeque(int numElements) {
        allocateElements(numElements);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;allocateElements&lt;/code&gt; 方法会创建一个数组，数组的长度由 &lt;code&gt;calculateSize&lt;/code&gt; 方法传入 &lt;code&gt;numElements&lt;/code&gt; 参数得到，计算逻辑如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;numElements&lt;/code&gt; 小于 8, 就是 8。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;numElements&lt;/code&gt; 大于等于 8 的情况下, 分配的实际长度是严格大于 numElements 并且为 2 的整数次幂的最小值。例如, 如果 numElements 为 10, 则实际分配 16, 如果 numElements 为 32, 则为 64。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    private void allocateElements(int numElements) {
        elements = new Object[calculateSize(numElements)];
    }

	private static final int MIN_INITIAL_CAPACITY = 8;

    private static int calculateSize(int numElements) {
        int initialCapacity = MIN_INITIAL_CAPACITY;
        // Find the best power of two to hold elements.
        // Tests &quot;&amp;#x3C;=&quot; because arrays aren&apos;t kept full.
        if (numElements &gt;= initialCapacity) {
            initialCapacity = numElements;
            initialCapacity |= (initialCapacity &gt;&gt;&gt;  1);
            initialCapacity |= (initialCapacity &gt;&gt;&gt;  2);
            initialCapacity |= (initialCapacity &gt;&gt;&gt;  4);
            initialCapacity |= (initialCapacity &gt;&gt;&gt;  8);
            initialCapacity |= (initialCapacity &gt;&gt;&gt; 16);
            initialCapacity++;

            if (initialCapacity &amp;#x3C; 0)   // Too many elements, must back off
                initialCapacity &gt;&gt;&gt;= 1;// Good luck allocating 2 ^ 30 elements
        }
        return initialCapacity;
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2 的幂次数会使得很多操作的效率很高，因为循环数组必须时刻至少留一个空位, &lt;code&gt;tail&lt;/code&gt; 变量指向下一个空位, 为了容纳 &lt;code&gt;numElements&lt;/code&gt; 个元素, 至少需要 &lt;code&gt;numElements + 1&lt;/code&gt; 个位置，所以要严格大于 &lt;code&gt;numElements&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;最后一个构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public ArrayDeque(Collection&amp;#x3C;? extends E&gt; c) {
        allocateElements(c.size());
        addAll(c);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样调用 &lt;code&gt;allocateElements&lt;/code&gt; 创建数组，随后调用了 &lt;code&gt;addAll&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public boolean addAll(Collection&amp;#x3C;? extends E&gt; c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;addAll&lt;/code&gt; 方法只是循环调用了 &lt;code&gt;add&lt;/code&gt; 方法&lt;/p&gt;
&lt;h2&gt;从尾部添加 add&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;add&lt;/code&gt; 方法用于向队列尾部添加元素，调用的是 &lt;code&gt;addLast&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public boolean add(E e) {
        addLast(e);
        return true;
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;addLast&lt;/code&gt; 方法将指定元素插入到此双端队列的末尾，将元素添加到 &lt;code&gt;tail&lt;/code&gt; 处，然后将 &lt;code&gt;tail&lt;/code&gt; 指向下一个位置&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;tail&lt;/code&gt; 到达数组的最后一个位置后，下一个元素应该被放入数组的起始位置，最直观的实现方式是将 &lt;code&gt;tail&lt;/code&gt; 对数组长度取模，即 &lt;code&gt;tail = (tail + 1) % elements.length;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;一个数 $X$ 对 $2^n$ 取模，等价于取 $X$ 的二进制表示的低 $n$ 位，而 $2^n - 1$ 是一个低 $n$ 位全为 $1$ 的二进制数。将任何数与 ($2^n - 1$) 进行按位与 $(&amp;#x26;)$ 运算，其效果就是保留这个数的低 $n$ 位，而将所有高位清零。&lt;/p&gt;
&lt;p&gt;数组长度为 2 的整数次幂，所以 &lt;code&gt;tail = (tail + 1) &amp;#x26; (elements.length - 1)&lt;/code&gt; 就能起到 &lt;code&gt;tail&lt;/code&gt; 对数组长度取模的效果，并且性能更好&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 &lt;code&gt;e==null&lt;/code&gt; 会抛出 &lt;code&gt;NullPointerException&lt;/code&gt; 异常，说明 &lt;code&gt;ArrayDeque&lt;/code&gt; 不能存储 &lt;code&gt;null&lt;/code&gt; 值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[tail] = e;
        if ( (tail = (tail + 1) &amp;#x26; (elements.length - 1)) == head)
            doubleCapacity();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果此时 &lt;code&gt;tail == head&lt;/code&gt; 说明队列已经满了，要调用 &lt;code&gt;doubleCapacity()&lt;/code&gt; 方法对数组进行扩容，&lt;code&gt;doubleCapacity()&lt;/code&gt; 方法将数组扩容为原来的两倍&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    private void doubleCapacity() {
        assert head == tail;
        int p = head;
        int n = elements.length;
        int r = n - p; // number of elements to the right of p
        int newCapacity = n &amp;#x3C;&amp;#x3C; 1;
        if (newCapacity &amp;#x3C; 0)
            throw new IllegalStateException(&quot;Sorry, deque too big&quot;);
        Object[] a = new Object[newCapacity];
        System.arraycopy(elements, p, a, 0, r);
        System.arraycopy(elements, 0, a, r, p);
        elements = a;
        head = 0;
        tail = n;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分配一个长度翻倍的新数组 &lt;code&gt;a&lt;/code&gt; ,将 &lt;code&gt;head&lt;/code&gt; 右边的元素复制到新数组开头处,再复制左边的元素到新数组中,最后重新设置 &lt;code&gt;head&lt;/code&gt; 和 &lt;code&gt;tail&lt;/code&gt;,&lt;code&gt;head&lt;/code&gt; 设为 $0$,&lt;code&gt;tail&lt;/code&gt; 设为 $n$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/09/5e458ea0ebbf70e057fc00817742b3b7.png&quot; alt=&quot;QQ_1758461177028&quot;&gt;&lt;/p&gt;
&lt;h2&gt;从头部添加 addFirst&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;addFirst()&lt;/code&gt; 方法的代码为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[head = (head - 1) &amp;#x26; (elements.length - 1)] = e;
        if (head == tail)
            doubleCapacity();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在头部添加，要先让 &lt;code&gt;head&lt;/code&gt; 指向前一个位置，然后再赋值给 &lt;code&gt;head&lt;/code&gt; 所在位置。head的前一个位置是 &lt;code&gt;(head-1) &amp;#x26; (elements.length-1)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;刚开始head为 &lt;code&gt;0&lt;/code&gt;，如果&lt;code&gt;elements.length&lt;/code&gt;为 &lt;code&gt;8&lt;/code&gt;，则&lt;code&gt;(head-1) &amp;#x26; (elements.length-1)&lt;/code&gt;的结果为 &lt;code&gt;7&lt;/code&gt;。比如，执行如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Deque&amp;#x3C;String&gt; queue = new ArrayDeque&amp;#x3C;&gt;(7);
queue.addFirst(&quot;a&quot;);
queue.addFirst(&quot;b&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完成后内部结构如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/09/1e321a4586edcca8eef9a55b19504896.png&quot; alt=&quot;QQ_1758531670286&quot;&gt;&lt;/p&gt;
&lt;h2&gt;从头部删除 &lt;code&gt;removeFirst&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;removeFirst()&lt;/code&gt; 方法的代码为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public E removeFirst() {
        E x = pollFirst();
        if (x == null)
            throw new NoSuchElementException();
        return x;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要调用了 &lt;code&gt;pollFirst()&lt;/code&gt; 方法，得到 &lt;code&gt;result&lt;/code&gt; 后将原头部位置置为 &lt;code&gt;null&lt;/code&gt;，然后 &lt;code&gt;head&lt;/code&gt; 置为下一个位置，即 &lt;code&gt;(h + 1) &amp;#x26; (elements.length - 1)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public E pollFirst() {
        int h = head;
        @SuppressWarnings(&quot;unchecked&quot;)
        E result = (E) elements[h];
        // Element is null if deque empty
        if (result == null)
            return null;
        elements[h] = null;     // Must null out slot
        head = (h + 1) &amp;#x26; (elements.length - 1);
        return result;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尾部删除元素的代码类似，只不过将 &lt;code&gt;tail&lt;/code&gt; 置为前一个位置，即 &lt;code&gt;(tail - 1) &amp;#x26; (elements.length - 1)&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;查看长度 size&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ArrayDeque&lt;/code&gt; 没有单独的字段维护长度，其 &lt;code&gt;size&lt;/code&gt; 方法的代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public int size() {
        return (tail - head) &amp;#x26; (elements.length - 1);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;检查指定元素是否存在 contains&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;contains&lt;/code&gt; 方法的代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public boolean contains(Object o) {
        if (o == null)
            return false;
        int mask = elements.length - 1;
        int i = head;
        Object x;
        while ( (x = elements[i]) != null) {
            if (o.equals(x))
                return true;
            i = (i + 1) &amp;#x26; mask;
        }
        return false;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是从 &lt;code&gt;head&lt;/code&gt; 开始遍历并进行对比，循环过程中没有使用 &lt;code&gt;tail&lt;/code&gt;，而是到元素为 &lt;code&gt;null&lt;/code&gt; 就结束了，这是因为在 &lt;code&gt;ArrayDeque&lt;/code&gt; 中，有效元素不允许为 &lt;code&gt;null&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?arrayDeque"/><enclosure url="http://wallpaper.csun.site/?arrayDeque"/></item><item><title>剖析 LinkedList</title><link>https://blog.csun.site/blog/2025-09-21-analysis-linkedlist</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-09-21-analysis-linkedlist</guid><description>深入解析 Java LinkedList</description><pubDate>Sun, 21 Sep 2025 18:14:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;LinkedList&lt;/code&gt; 是 Java 中 &lt;code&gt;java.util&lt;/code&gt; 包提供的一个类，它实现了 &lt;code&gt;List&lt;/code&gt; 接口，并且提供了双向链表的结构，维护了长度、头节点和尾节点，此外，还实现了 &lt;code&gt;Deque&lt;/code&gt; 和 &lt;code&gt;Queue&lt;/code&gt; 接口，可以按照队列、栈和双端队列的方式进行操作&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LinkedList&lt;/code&gt; 的特点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按需分配空间，不需要预先分配很多空间。&lt;/li&gt;
&lt;li&gt;不可以随机访问，按照索引访问效率比较低，必须从头或尾顺着链表查找，时间复杂度为 $O(N/2)$ 。&lt;/li&gt;
&lt;li&gt;不管列表是否已排序，只要是按照内容查找元素，效率都比较低，必须逐个比较，时间复杂度为 $O(N)$ 。&lt;/li&gt;
&lt;li&gt;在两端添加、删除元素的效率很高，时间复杂度为 $O(1)$ 。&lt;/li&gt;
&lt;li&gt;在中间插入、删除元素，要先定位，效率比较低，时间复杂度为 $O(N)$ ，但修改本身的效率很高，时间复杂度为 $O(1)$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;用法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;LinkedList&lt;/code&gt; 的构造方法与 &lt;code&gt;ArrayList&lt;/code&gt; 类似，有两个：一个是默认构造方法，另外一个可以接受一个已有的 &lt;code&gt;Collection&lt;/code&gt;，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public LinkedList();
public LinkedList(Collection&amp;#x3C;? extends E&gt; c);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以这样创建 &lt;code&gt;LinkedList&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;Integer&gt; list1 = new LinkedList&amp;#x3C;&gt;();
List&amp;#x3C;String&gt; list2 = new LinkedList&amp;#x3C;&gt;(
                Arrays.asList(new String[] {&quot;a&quot;, &quot;b&quot;, &quot;c&quot;}));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;LinkedList&lt;/code&gt; 与 &lt;code&gt;ArrayList&lt;/code&gt; 一样，同样实现了 &lt;code&gt;List&lt;/code&gt; 接口，而 &lt;code&gt;List&lt;/code&gt; 接口扩展了 &lt;code&gt;Collection&lt;/code&gt; 接口，&lt;code&gt;Collection&lt;/code&gt; 又扩展了 &lt;code&gt;Iterable&lt;/code&gt; 接口，所有这些接口的方法都是可以使用的。&lt;/p&gt;
&lt;h3&gt;队列&lt;/h3&gt;
&lt;p&gt;此外，&lt;code&gt;LinkedList&lt;/code&gt; 还实现了队列 &lt;code&gt;Queue&lt;/code&gt; 接口，支持先进先出，在尾部添加元素，从头部删除元素，&lt;code&gt;Queue&lt;/code&gt; 接口的定义为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface Queue&amp;#x3C;E&gt; extends Collection&amp;#x3C;E&gt; {
    boolean add(E e);
    boolean offer(E e);
    E remove();
    E poll();
    E element();
    E peek();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Queue&lt;/code&gt; 接口继承自 &lt;code&gt;Collection&lt;/code&gt; 接口，主要操作有三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在尾部添加元素&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt; 方法，队列为满时会抛出 &lt;code&gt;IllegalStateException&lt;/code&gt; 异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;offer&lt;/code&gt; 方法，队列为满时只是返回 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;返回头部元素，但不改变队列&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;element&lt;/code&gt; 方法，队列为空时抛出 &lt;code&gt;NoSuchElementException&lt;/code&gt; 异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;peek&lt;/code&gt; 方法，队列为空时返回 &lt;code&gt;null&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;返回头部元素，并且从队列中删除&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;remove&lt;/code&gt; 方法，队列为空时抛出 &lt;code&gt;NoSuchElementException&lt;/code&gt; 异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;poll&lt;/code&gt; 方法，队列为空时返回 &lt;code&gt;null&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把 &lt;code&gt;LinkedList&lt;/code&gt; 当作队列使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Queue&amp;#x3C;Integer&gt; queue = new LinkedList&amp;#x3C;&gt;();
queue.offer(1);
queue.offer(2);
queue.offer(3);
while(queue.peek() != null)
    System.out.println(queue.poll());
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;栈&lt;/h3&gt;
&lt;p&gt;Java 中没有单独的栈接口，栈相关方法放在了表示双端队列的接口 &lt;code&gt;Deque&lt;/code&gt; 中，&lt;code&gt;Deque&lt;/code&gt; 接口提供了很多方法，栈相关的主要有三个：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void push(E e);
E pop();
E peek();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push&lt;/code&gt; 方法表示入栈，如果栈满会抛出 &lt;code&gt;IllegalStateException&lt;/code&gt; 异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop&lt;/code&gt; 方法表示出栈，如果栈空会抛出 &lt;code&gt;NoSuchElementException&lt;/code&gt; 异常&lt;/li&gt;
&lt;li&gt;&lt;code&gt;peek&lt;/code&gt; 方法查看栈顶元素，如果栈为空返回 &lt;code&gt;null&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把 &lt;code&gt;LinkList&lt;/code&gt; 当栈使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Deque&amp;#x3C;Integer&gt; stack = new ArrayDeque&amp;#x3C;&gt;();
stack.push(1);
stack.push(2);
stack.push(3);
while(stack.peek() != null) {
    System.out.println(stack.pop());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双端队列&lt;/h3&gt;
&lt;p&gt;还一个更为通用的操作两端的接口 &lt;code&gt;Deque&lt;/code&gt;，&lt;code&gt;Deque&lt;/code&gt; 扩展了 &lt;code&gt;Queue&lt;/code&gt;，除了栈的操作方法，还有如下更为明确的操作两端的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface Deque&amp;#x3C;E&gt; extends Queue&amp;#x3C;E&gt; {
    void addFirst(E e);

    void addLast(E e);

    boolean offerFirst(E e);

    boolean offerLast(E e);

    E removeFirst();

    E removeLast();

    E pollFirst();

    E pollLast();

    E getFirst();

    E getLast();

    E peekFirst();

    E peekLast();

    boolean removeFirstOccurrence(Object o);

    boolean removeLastOccurrence(Object o);

    // *** Queue methods ***
    boolean add(E e);

    boolean offer(E e);

    E remove();

    E poll();

    E element();

    E peek();


    // *** Stack methods ***
    void push(E e);

    E pop();

    boolean remove(Object o);

    boolean contains(Object o);

    public int size();

    Iterator&amp;#x3C;E&gt; iterator();

    Iterator&amp;#x3C;E&gt; descendingIterator();

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;xxxFirst&lt;/code&gt; 方法操作头部，&lt;code&gt;xxxLast&lt;/code&gt; 方法操作尾部，每种操作有两种形式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;队列为空时 &lt;code&gt;getXXX/removeXXX&lt;/code&gt; 会抛出异常，而 &lt;code&gt;peekXXX/pollXXX&lt;/code&gt; 会返回 &lt;code&gt;null&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;队满时 &lt;code&gt;addXXX&lt;/code&gt; 会抛出异常，&lt;code&gt;offerXXXX&lt;/code&gt; 只是返回false。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Deque&lt;/code&gt; 接口还有个迭代器方法，可以从后往前遍历&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Iterator&amp;#x3C;E&gt; descendingIterator();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args) {
        Deque&amp;#x3C;Integer&gt; dq = new LinkedList&amp;#x3C;&gt;(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));

        Iterator&amp;#x3C;Integer&gt; iterator = dq.descendingIterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next());
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;10987654321
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实现原理&lt;/h2&gt;
&lt;h3&gt;LinkedList 的内部组成&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LinkedList&lt;/code&gt; 的内部实现是双向链表，每个元素在内存中单独存放，通过链接连在一起&lt;/p&gt;
&lt;p&gt;为了表示链接的关系，需要一个节点的概念，节点内部包括实际存放的元素和两个指针，分别指向前一个节点（前驱）和后一个节点（后继）。节点是 &lt;code&gt;LinkedList&lt;/code&gt; 的一个静态内部类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static class Node&amp;#x3C;E&gt; {
  E item;
  Node&amp;#x3C;E&gt; next;
  Node&amp;#x3C;E&gt; prev;
  Node(Node&amp;#x3C;E&gt; prev, E element, Node&amp;#x3C;E&gt; next) {
      this.item = element;
      this.next = next;
      this.prev = prev;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Node&lt;/code&gt; 类表示节点，&lt;code&gt;item&lt;/code&gt; 指向实际的元素，&lt;code&gt;next&lt;/code&gt; 指向后一个节点，&lt;code&gt;prev&lt;/code&gt; 指向前一个节点&lt;/p&gt;
&lt;p&gt;LinkedList 内部组成就是如下三个实例变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;transient int size = 0;
transient Node&amp;#x3C;E&gt; first;
transient Node&amp;#x3C;E&gt; last;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;size&lt;/code&gt; 表示链表长度，默认为 &lt;code&gt;0&lt;/code&gt;，&lt;code&gt;first&lt;/code&gt; 指向链表的头节点，&lt;code&gt;last&lt;/code&gt; 指向链表的尾节点，初始值都为 &lt;code&gt;null&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;构造函数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LinkedList&lt;/code&gt; 有两个构造函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public LinkedList();
public LinkedList(Collection&amp;#x3C;? extends E&gt; c);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无参构造函数是空的，实例变量全使用默认值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public LinkedList() {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外一个构造函数调用 &lt;code&gt;addAll&lt;/code&gt; 方法将传入的集合元素加入到链表中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public LinkedList(Collection&amp;#x3C;? extends E&gt; c) {
	this();
	addAll(c);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;addAll&lt;/code&gt; 方法调用了重载的 &lt;code&gt;addAll(int index, Collection&amp;#x3C;? extends E&gt; c)&lt;/code&gt; 将参数 &lt;code&gt;size&lt;/code&gt; 作为插入位置的索引&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean addAll(Collection&amp;#x3C;? extends E&gt; c) {
	return addAll(size, c);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该方法首先调用 &lt;code&gt;checkPositionIndex&lt;/code&gt; 方法检查 &lt;code&gt;index &gt;=0 &amp;#x26;&amp;#x26; index &amp;#x3C;= size&lt;/code&gt;，如果不是会抛出 &lt;code&gt;IndexOutOfBoundsException&lt;/code&gt; 异常&lt;/p&gt;
&lt;p&gt;然后将传入的集合转换成 &lt;code&gt;Object&lt;/code&gt; 数组，计算数组的长度，如果数组长度为 0 说明没有元素可以插入，返回 &lt;code&gt;false&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;声明两个指针，&lt;code&gt;pred&lt;/code&gt; 指向插入位置的前一个节点，&lt;code&gt;succ&lt;/code&gt; 指向插入位置。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;index == size&lt;/code&gt; 说明插入位置在链表末尾，&lt;code&gt;succ=null&lt;/code&gt;，&lt;code&gt;pred&lt;/code&gt; 指向链表的尾部 &lt;code&gt;last&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;否则需要调用 &lt;code&gt;node()&lt;/code&gt; 方法查找插入位置的节点，&lt;code&gt;node&lt;/code&gt; 方法中会先判断 &lt;code&gt;index&lt;/code&gt; 是在链表的前半部分还是后半部分，然后决定是从前到后遍历还是从后往前遍历去查找节点。找到 &lt;code&gt;succ&lt;/code&gt; 节点后，&lt;code&gt;pred = succ.prev&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后遍历数组，创建新节点，将 &lt;code&gt;pred&lt;/code&gt; 作为新节点的前驱，如果前驱为 &lt;code&gt;null&lt;/code&gt;，则说明链表为空，新节点就是头节点，将 &lt;code&gt;first&lt;/code&gt; 指向新节点，否则 &lt;code&gt;pred.next = newNode&lt;/code&gt;，再下一轮循环中 &lt;code&gt;pred&lt;/code&gt; 为 &lt;code&gt;newNode&lt;/code&gt;，会给 &lt;code&gt;pred.next&lt;/code&gt; 赋值，所以创建新节点的时候，&lt;code&gt;newNode.next&lt;/code&gt; 的传参为 &lt;code&gt;null&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;插入完成后如果 &lt;code&gt;succ&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt; 说明插入之前链表为空，尾节点就是插入的最后一个节点，所以 &lt;code&gt;last = pred&lt;/code&gt;，否则需要将 &lt;code&gt;pred&lt;/code&gt; 的后继指向 &lt;code&gt;succ&lt;/code&gt;，&lt;code&gt;succ&lt;/code&gt; 的前驱指向 &lt;code&gt;pred&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;最后修改 &lt;code&gt;size&lt;/code&gt; 和 &lt;code&gt;modCount&lt;/code&gt; 的值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean addAll(int index, Collection&amp;#x3C;? extends E&gt; c) {
        checkPositionIndex(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node&amp;#x3C;E&gt; pred, succ;
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }

        for (Object o : a) {
            @SuppressWarnings(&quot;unchecked&quot;) E e = (E) o;
            Node&amp;#x3C;E&gt; newNode = new Node&amp;#x3C;&gt;(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }

        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }

Node&amp;#x3C;E&gt; node(int index) {
  // assert isElementIndex(index);
  if (index &amp;#x3C; (size &gt;&gt; 1)) {
      Node&amp;#x3C;E&gt; x = first;
      for (int i = 0; i &amp;#x3C; index; i++)
          x = x.next;
      return x;
  } else {
      Node&amp;#x3C;E&gt; x = last;
      for (int i = size - 1; i &gt; index; i--)
          x = x.prev;
      return x;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加元素 add&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;add&lt;/code&gt; 方法用于向链表中添加元素，有两个重载&lt;/p&gt;
&lt;p&gt;第一个 &lt;code&gt;add(E e)&lt;/code&gt; 是向链表尾部插入元素，主要调用的是 &lt;code&gt;linkLast(e)&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public boolean add(E e) {
        linkLast(e);
        return true;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;linkLast&lt;/code&gt; 方法首先创建一个新节点 &lt;code&gt;newNode&lt;/code&gt;。&lt;code&gt;l&lt;/code&gt; 和 &lt;code&gt;last&lt;/code&gt; 指向原来的尾节点，如果原来链表为空，则为 &lt;code&gt;null&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后修改尾节点 &lt;code&gt;last&lt;/code&gt; 指向新的节点 &lt;code&gt;newNode&lt;/code&gt;，并修改前驱节点的后向指针，如果原来链表为空，则让头节点指向新节点，否则让前一个节点的next指向新节点&lt;/p&gt;
&lt;p&gt;最后修改 &lt;code&gt;size&lt;/code&gt; 和 &lt;code&gt;modCount&lt;/code&gt; 的值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    void linkLast(E e) {
        final Node&amp;#x3C;E&gt; l = last;
        final Node&amp;#x3C;E&gt; newNode = new Node&amp;#x3C;&gt;(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;add(int index, E element)&lt;/code&gt; 方法则是在链表指定位置插入元素，首先检查插入位置的合法性&lt;/p&gt;
&lt;p&gt;然后如果 &lt;code&gt;index == size&lt;/code&gt; 说明是在链表尾部插入，直接调用 &lt;code&gt;linkLast(element);&lt;/code&gt; 方法&lt;/p&gt;
&lt;p&gt;否则调用 &lt;code&gt;linkBefore(element, node(index))&lt;/code&gt; 方法，并调用 &lt;code&gt;node&lt;/code&gt; 方法获取插入位置的节点&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;linkBefore&lt;/code&gt; 方法的实现与 &lt;code&gt;addAll&lt;/code&gt; 中大同小异&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    void linkBefore(E e, Node&amp;#x3C;E&gt; succ) {
        // assert succ != null;
        final Node&amp;#x3C;E&gt; pred = succ.prev;
        final Node&amp;#x3C;E&gt; newNode = new Node&amp;#x3C;&gt;(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;根据索引访问元素 get&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LinkedList&lt;/code&gt; 提供了 &lt;code&gt;get(int index)&lt;/code&gt; 方法根据索引访问元素&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先调用 &lt;code&gt;checkElementIndex&lt;/code&gt; 方法检查索引位置的有效性&lt;/p&gt;
&lt;p&gt;然后调用 &lt;code&gt;node&lt;/code&gt; 方法返回索引位置的节点，获取节点的 &lt;code&gt;item&lt;/code&gt; 值即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    private boolean isPositionIndex(int index) {
        return index &gt;= 0 &amp;#x26;&amp;#x26; index &amp;#x3C;= size;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;根据内容查找元素 indexOf&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LinkedList&lt;/code&gt; 提供了 &lt;code&gt;indexOf(Object o)&lt;/code&gt; 方法根据内容查找元素，代码也很简单，从头节点顺着链接后找，如果要找的是 &lt;code&gt;null&lt;/code&gt;，则找第一个 &lt;code&gt;item&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt; 的节点，否则使用 &lt;code&gt;equals&lt;/code&gt; 方法进行比较。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public int indexOf(Object o) {
        int index = 0;
        if (o == null) {
            for (Node&amp;#x3C;E&gt; x = first; x != null; x = x.next) {
                if (x.item == null)
                    return index;
                index++;
            }
        } else {
            for (Node&amp;#x3C;E&gt; x = first; x != null; x = x.next) {
                if (o.equals(x.item))
                    return index;
                index++;
            }
        }
        return -1;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;删除元素 remove&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;remove(int index)&lt;/code&gt; 方法用于删除指定位置的元素，通过 &lt;code&gt;node&lt;/code&gt; 方法找到节点后，调用了 &lt;code&gt;unlink&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;unlink&lt;/code&gt; 方法的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    E unlink(Node&amp;#x3C;E&gt; x) {
        // assert x != null;
        final E element = x.item;
        final Node&amp;#x3C;E&gt; next = x.next;
        final Node&amp;#x3C;E&gt; prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除x节点，基本思路就是让x的前驱和后继直接链接起来，next是x的后继，prev是x的前驱，具体分为两步：&lt;/p&gt;
&lt;p&gt;1)让x的前驱的后继指向x的后继。如果x没有前驱，说明删除的是头节点，则修改头节点指向x的后继。&lt;/p&gt;
&lt;p&gt;2)让x的后继的前驱指向x的前驱。如果x没有后继，说明删除的是尾节点，则修改尾节点指向x的前驱。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?linkedlist"/><enclosure url="http://wallpaper.csun.site/?linkedlist"/></item><item><title>剖析 ArrayList</title><link>https://blog.csun.site/blog/2025-09-17-analysis-arraylist</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-09-17-analysis-arraylist</guid><description>剖析 Java ArrayList 特性</description><pubDate>Wed, 17 Sep 2025 20:14:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;ArrayList&lt;/code&gt; 是 Java 中的一个动态数组实现类，可以根据需要自动调整数组的大小&lt;/p&gt;
&lt;h2&gt;基本用法&lt;/h2&gt;
&lt;p&gt;ArrayList 是一个泛型容器，内部采用动态数组实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以随机访问，按照索引位置进行访问的时间复杂度是 $O(1)$；&lt;/li&gt;
&lt;li&gt;除非数组已排序，否则按照内容查找元素效率比较低，时间复杂度是 $O(N)$；&lt;/li&gt;
&lt;li&gt;添加元素时重新分配和复制数组的开销被摊平了，添加 N 个元素的效率为 $O(N)$；&lt;/li&gt;
&lt;li&gt;插入和删除元素的因为需要移动元素，时间复杂度为 $O(N)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新建 ArrayList 需要实例化泛型参数，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ArrayList&amp;#x3C;Integer&gt; intList = new ArrayList&amp;#x3C;Integer&gt;();
ArrayList&amp;#x3C;String&gt; strList = new ArrayList&amp;#x3C;String&gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ArrayList 的主要方法有：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean add(E e) //添加元素到末尾
public boolean isEmpty() //判断是否为空
public int size() //获取长度
public E get(int index) //访问指定位置的元素
public int indexOf(Object o) //查找元素，如果找到，返回索引位置，否则返回-1
public int lastIndexOf(Object o) //从后往前找
public boolean contains(Object o) //是否包含指定元素，依据是equals方法的返回值
public E remove(int index) //删除指定位置的元素，返回值为被删对象
//删除指定对象，只删除第一个相同的对象，返回值表示是否删除了元素
//如果o为null，则删除值为null的对象
public boolean remove(Object o)
public void clear() //删除所有元素
//在指定位置插入元素，index为0表示插入最前面，index为ArrayList的长度表示插到最后面
public void add(int index, E element)
public E set(int index, E element) //修改指定位置的元素内容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ArrayList&amp;#x3C;String&gt; strList = new ArrayList&amp;#x3C;String&gt;();
strList.add(&quot;Hello&quot;);
strList.add(&quot;ArrayList&quot;);
for(int i=0; i&amp;#x3C;strList.size(); i++){
    System.out.println(strList.get(i));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基本原理&lt;/h2&gt;
&lt;p&gt;ArrayList 内部主要包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个 &lt;code&gt;Object&lt;/code&gt; 数组 &lt;code&gt;elementData&lt;/code&gt; 记录存入的元素；&lt;/li&gt;
&lt;li&gt;一个 &lt;code&gt;int&lt;/code&gt; 类型的变量 &lt;code&gt;size&lt;/code&gt; 记录实际存储的元素个数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * The array buffer into which the elements of the ArrayList are stored.
 * The capacity of the ArrayList is the length of this array buffer. Any
 * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * will be expanded to DEFAULT_CAPACITY when the first element is added.
 */
transient Object[] elementData; // non-private to simplify nested class access

/**
 * The size of the ArrayList (the number of elements it contains).
 *
 * @serial
 */
private int size;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;构造函数&lt;/h3&gt;
&lt;p&gt;ArrayList 的构造函数主要有三个：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public ArrayList();  // 无参构造函数
public ArrayList(int initialCapacity); // 指定初始容量
public ArrayList(Collection&amp;#x3C;? extends E&gt; c)； // 传入一个集合
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先看无参构造函数，无参构造函数很简单，将 &lt;code&gt;elementData&lt;/code&gt; 数组赋值为常量 &lt;code&gt;DEFAULTCAPACITY_EMPTY_ELEMENTDATA&lt;/code&gt;，这是一个空数组&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;指定初始容量的构造函数如下，如果初始容量大于 0 就创建一个 &lt;code&gt;Object&lt;/code&gt; 类型的数组，容量为 &lt;code&gt;initialCapacity&lt;/code&gt; 并赋值给 &lt;code&gt;elementData&lt;/code&gt;；如果初始容量等于 0 就将 &lt;code&gt;elementData&lt;/code&gt; 数组赋值为常量 &lt;code&gt;EMPTY_ELEMENTDATA&lt;/code&gt;，这也是一个空数组&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public ArrayList(int initialCapacity) {
    if (initialCapacity &gt; 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException(&quot;Illegal Capacity: &quot;+
                                           initialCapacity);
    }
}

private static final Object[] EMPTY_ELEMENTDATA = {};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;传入集合的构造函数如下，用于将一个现有的 &lt;code&gt;Collection&lt;/code&gt;（集合）转换为一个新的 &lt;code&gt;ArrayList&lt;/code&gt; 对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public ArrayList(Collection&amp;#x3C;? extends E&gt; c) {
    Object[] a = c.toArray();  // 将传入的集合 &apos;c&apos; 转换成一个数组 &apos;a&apos;。
    if ((size = a.length) != 0) {  // 获取数组 &apos;a&apos; 的长度并赋值给 &apos;size&apos;，如果数组不为空，进入if块。
        if (c.getClass() == ArrayList.class) {  // 如果传入的集合 &apos;c&apos; 是一个 ArrayList 实例
            elementData = a;  // 直接使用传入集合的数组 &apos;a&apos;，没有复制数据
        } else {  // 如果传入的集合不是 ArrayList
            elementData = Arrays.copyOf(a, size, Object[].class);  // 复制 &apos;a&apos; 数组，创建一个新的数组 &apos;elementData&apos;
        }
    } else {  // 如果数组 &apos;a&apos; 是空的
        elementData = EMPTY_ELEMENTDATA;  // 将 &apos;elementData&apos; 赋值为一个空数组常量 EMPTY_ELEMENTDATA
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;add&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;add&lt;/code&gt; 方法用于向 &lt;code&gt;ArrayList&lt;/code&gt; 末尾添加元素&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先调用 &lt;code&gt;ensureCapacityInternal(int minCapacity)&lt;/code&gt; 方法检查容量是否足够，当前只添加一个元素，所以需要的最小容量为 &lt;code&gt;size + 1&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private void ensureCapacityInternal(int minCapacity) {
	ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该方法先调用了 &lt;code&gt;calculateCapacity()&lt;/code&gt; 方法计算所需容量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;calculateCapacity()&lt;/code&gt; 方法中，如果当前 &lt;code&gt;elementData&lt;/code&gt; 数组等于 &lt;code&gt;DEFAULTCAPACITY_EMPTY_ELEMENTDATA&lt;/code&gt;，即还是个空数组，容量就取 &lt;code&gt;DEFAULT_CAPACITY&lt;/code&gt; 和 &lt;code&gt;minCapacity&lt;/code&gt; 中较大的一个，&lt;code&gt;DEFAULT_CAPACITY&lt;/code&gt; 是一个常量，值为 &lt;code&gt;10&lt;/code&gt;，否则就返回 &lt;code&gt;minCapacity&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;ArrayList&lt;/code&gt; 的初始容量为 &lt;code&gt;0&lt;/code&gt;，添加第一个元素时才会扩容到 &lt;code&gt;10&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;接着 &lt;code&gt;calculateCapacity()&lt;/code&gt; 方法调用 &lt;code&gt;ensureExplicitCapacity()&lt;/code&gt; 方法，判断所需最小容量是否大于当前数组的长度，如果是则调用 &lt;code&gt;grow()&lt;/code&gt; 方法扩容，否则无需扩容直接返回&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length &gt; 0)
        grow(minCapacity);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;modCount&lt;/code&gt;&lt;/strong&gt; 这个变量用来记录集合被修改的次数。每当集合结构发生变化（例如添加、删除元素等），&lt;code&gt;modCount&lt;/code&gt; 就会增加 1，在使用迭代器时，迭代器会检查 &lt;code&gt;modCount&lt;/code&gt; 是否发生变化，如果变化了，就会抛出 &lt;code&gt;ConcurrentModificationException&lt;/code&gt;，以防止在遍历集合时，集合被修改，导致不一致的结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;grow()&lt;/code&gt; 函数正式对数组进行扩容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity &gt;&gt; 1);
    if (newCapacity - minCapacity &amp;#x3C; 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE &gt; 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;newCapacity  = oldCapacity + (oldCapacity &gt;&gt; 1);&lt;/code&gt; 即新的容量是原容量的 1.5 倍。&lt;/p&gt;
&lt;p&gt;如果新容量小于所需最小容量，则将所需的最小容量赋值给新容量；&lt;/p&gt;
&lt;p&gt;如果新容量大于 &lt;code&gt;MAX_ARRAY_SIZE&lt;/code&gt; 需要调用 &lt;code&gt;hugeCapacity()&lt;/code&gt; 方法，该方法将新容量设置为 &lt;code&gt;Integer&lt;/code&gt; 类型的最大值&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;ArrayList&lt;/code&gt; 的容量是有限的，最大为 Int 类型的最大值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private static int hugeCapacity(int minCapacity) {
    if (minCapacity &amp;#x3C; 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity &gt; MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后 &lt;code&gt;grow()&lt;/code&gt; 方法使用 &lt;code&gt;Arrays.copyOf(elementData, newCapacity)&lt;/code&gt; 方法创建一个新的数组，容量为 &lt;code&gt;newCapacity&lt;/code&gt; 并赋值给 &lt;code&gt;elementData&lt;/code&gt;，完成扩容。&lt;/p&gt;
&lt;p&gt;扩容完成后，&lt;code&gt;add()&lt;/code&gt; 方法执行 &lt;code&gt;elementData[size++] = e;&lt;/code&gt; 将元素添加到数组末尾并将 &lt;code&gt;size + 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;同时整个过程没有对 &lt;code&gt;null&lt;/code&gt; 值做任何处理，所以 &lt;code&gt;ArrayList&lt;/code&gt; 是可以添加 &lt;code&gt;null&lt;/code&gt; 值的&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;remove&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;remove()&lt;/code&gt; 方法用于删除指定下标处的元素并返回删除的元素&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved &gt; 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先会计算需要移动的元素数量 &lt;code&gt;size - index - 1&lt;/code&gt;，然后调用 &lt;code&gt;System.arraycopy()&lt;/code&gt; 方法移动元素。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;elementData[--size]=null；&lt;/code&gt; 这行代码将 &lt;code&gt;size&lt;/code&gt; 减 1，同时将最后一个位置设为 &lt;code&gt;null&lt;/code&gt;，设为 &lt;code&gt;null&lt;/code&gt; 后不再引用原来对象，如果原来对象也不再被其他对象引用，就可以被垃圾回收。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ensureCapacity&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ArrayList()&lt;/code&gt; 还有一个 &lt;code&gt;ensureCapacity()&lt;/code&gt; 方法是提供给用户调用的，用于在向 &lt;code&gt;ArrayList&lt;/code&gt; 添加大量元素之前对数组进行扩容，以减少增量重新分配的次数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void ensureCapacity(int minCapacity) {
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        // any size if not default element table
        ? 0
        // larger than default for default empty table. It&apos;s already
        // supposed to be at default size.
        : DEFAULT_CAPACITY;
        
    if (minCapacity &gt; minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;trimToSize&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;trimToSize&lt;/code&gt; 方法会重新分配一个数组，大小刚好为实际内容的长度。调用这个方法可以节省数组占用的空间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void trimToSize() {
    modCount++;
    if (size &amp;#x3C; elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;System.arraycopy()&lt;/code&gt; 和 &lt;code&gt;Arrays.copyOf()&lt;/code&gt; 方法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ArrayList&lt;/code&gt; 中大量调用了这两个方法&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;System.arraycopy()&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;System.arraycopy()&lt;/code&gt; 是一个用于快速复制数组元素的本地(native)方法，它提供了一种高效的方式来复制一个数组的部分或全部内容到另一个数组中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
* 复制数组
* @param src 源数组
* @param srcPos 源数组中的起始位置
* @param dest 目标数组
* @param destPos 目标数组中的起始位置
* @param length 要复制的数组元素的数量
*/
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;Arrays.copyOf()&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Arrays.copyOf()&lt;/code&gt; 方法用于创建一个新的类型为 &lt;code&gt;T[]&lt;/code&gt; 的数组，长度为 &lt;code&gt;newLength&lt;/code&gt;，并复制原始数组 &lt;code&gt;original&lt;/code&gt; 中的元素到新数组。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static &amp;#x3C;T,U&gt; T[] copyOf(U[] original, int newLength, Class&amp;#x3C;? extends T[]&gt; newType) {
    @SuppressWarnings(&quot;unchecked&quot;)
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先检查要创建的新数组类型是否是 &lt;code&gt;Object[]&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是 &lt;code&gt;Object[]&lt;/code&gt;，直接创建一个 &lt;code&gt;Object&lt;/code&gt; 数组&lt;/li&gt;
&lt;li&gt;如果不是，则使用反射 API 的 &lt;code&gt;Array.newInstance&lt;/code&gt; 方法，根据类型创建特定类型的数组&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后使用 &lt;code&gt;System.arraycopy()&lt;/code&gt; 方法将原始数组中的数据复制到新数组&lt;/p&gt;
&lt;h2&gt;迭代&lt;/h2&gt;
&lt;p&gt;接下来，来看一个 &lt;code&gt;ArrayList&lt;/code&gt; 的常见操作：迭代。&lt;/p&gt;
&lt;p&gt;下面的例子是循环打印 ArrayList 中的每个元素，ArrayList 支持 foreach 语法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ArrayList&amp;#x3C;Integer&gt; intList = new ArrayList&amp;#x3C;Integer&gt;();
intList.add(123);
intList.add(456);
intList.add(789);
for(Integer a : intList){
    System.out.println(a);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种循环也可以使用如下代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for(int i=0; i&amp;#x3C;intList.size(); i++){
    System.out.println(intList.get(i));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是 &lt;code&gt;foreach&lt;/code&gt; 看上去更为简洁，而且它适用于各种容器，更为通用。&lt;/p&gt;
&lt;p&gt;在底层，编译器会将 &lt;code&gt;foreach&lt;/code&gt; 转换为类似如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Iterator&amp;#x3C;Integer&gt; it = intList.iterator();
while(it.hasNext()){
    System.out.println(it.next());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本质上，这其实是一个迭代器&lt;/p&gt;
&lt;h3&gt;迭代器接口&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ArrayList&lt;/code&gt; 实现了 &lt;code&gt;Iterable&lt;/code&gt; 接口，&lt;code&gt;Iterable&lt;/code&gt; 接口表示可迭代，定义为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface Iterable&amp;#x3C;T&gt; {
    /**
     * Returns an iterator over elements of type {@code T}.
     *
     * @return an Iterator.
     */
    Iterator&amp;#x3C;T&gt; iterator();

    default void forEach(Consumer&amp;#x3C;? super T&gt; action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    default Spliterator&amp;#x3C;T&gt; spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;foreach()&lt;/code&gt; 方法是 Java 8 引入的默认方法，接受一个 &lt;code&gt;Consumer&amp;#x3C;? super T&gt;&lt;/code&gt; 函数式接口作为参数，用于对集合中的每个元素执行指定的操作，例如&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;String&gt; list = Arrays.asList(&quot;apple&quot;, &quot;banana&quot;, &quot;cherry&quot;);
// 使用 forEach 打印每个元素
list.forEach(System.out::println);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Spliterator&lt;/code&gt; 是 「splitable iterator」 的缩写，是 Java 8 引入的用于支持并行处理的迭代器。&lt;/p&gt;
&lt;p&gt;这两个方法都是默认方法，不用实现类去实现，要实现 &lt;code&gt;Iterable&lt;/code&gt; 接口只需要实现 &lt;code&gt;Iterator&amp;#x3C;T&gt; iterator();&lt;/code&gt; 方法即可，该方法返回一个实现了 &lt;code&gt;Iterator&lt;/code&gt; 接口的对象&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Iterator&lt;/code&gt; 接口定义如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface Iterator&amp;#x3C;E&gt; {
    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException(&quot;remove&quot;);
    }

    default void forEachRemaining(Consumer&amp;#x3C;? super E&gt; action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hasNext()&lt;/code&gt; 方法检查迭代器是否还有下一个元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next()&lt;/code&gt; 方法返回迭代器的下一个元素，并将游标向前移动&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remove()&lt;/code&gt; 方法用于删除迭代器最后返回的元素，默认会抛出 &lt;code&gt;UnsupportedOperationException&lt;/code&gt; 异常，子类可以重写此方法来支持删除操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forEachRemaining()&lt;/code&gt; 方法对剩余的所有元素执行指定的操作&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;迭代的陷阱&lt;/h3&gt;
&lt;p&gt;关于迭代器，有一种常见的误用，就是在迭代的过程中调用容器的删除方法。比如，要删除一个整数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void remove(ArrayList&amp;#x3C;Integer&gt; list){
	for(Integer a : list){
		if(a&amp;#x3C;=100){
			list.remove(a);
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但运行时会发生并发修改异常：&lt;code&gt;java.util.ConcurrentModificationException&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;因为迭代器内部会维护一些索引位置相关的数据，要求在迭代过程中，容器不能发生结构性变化，否则这些索引位置就失效了。所谓结构性变化就是添加、插入和删除元素，只是修改元素内容不算结构性变化。&lt;/p&gt;
&lt;p&gt;如何避免异常呢？可以使用迭代器的 &lt;code&gt;remove&lt;/code&gt; 方法，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void remove(ArrayList&amp;#x3C;Integer&gt; list){
    Iterator&amp;#x3C;Integer&gt; it = list.iterator();
    while(it.hasNext()){
        if(it.next()&amp;#x3C;=100){
            it.remove();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;ArrayList&lt;/code&gt; 的迭代器&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;Iterator&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;ArrayList&lt;/code&gt; 的 &lt;code&gt;iterator()&lt;/code&gt; 方法如下，返回了一个 &lt;code&gt;Itr&lt;/code&gt; 对象&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Iterator&amp;#x3C;E&gt; iterator() {
        return new Itr();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Itr&lt;/code&gt; 是 &lt;code&gt;ArrayList&lt;/code&gt; 的一个内部类&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private class Itr implements Iterator&amp;#x3C;E&gt; {
    int cursor;       // 游标，指向下一个要返回的元素位置
    int lastRet = -1; // 记录最后一次返回的元素索引，用于 remove() 操作
    int expectedModCount = modCount; // 创建迭代器时的 modCount 快照，用于检测迭代过程中有没有结构性修改

    Itr() {}

    public boolean hasNext() {
        return cursor != size;  // 当 cursor 等于 size 时表示没有更多元素
    }

    @SuppressWarnings(&quot;unchecked&quot;)
    public E next() {
		// 检测有没有并发修改
        checkForComodification();
        int i = cursor;
        if (i &gt;= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i &gt;= elementData.length)  // 数组被修改
            throw new ConcurrentModificationException();
        cursor = i + 1;  // 游标前移
        return (E) elementData[lastRet = i]; // 返回元素并更新lastRet
    }

	// 只能删除最后一次 next() 返回的元素
    public void remove() {
        if (lastRet &amp;#x3C; 0)  // 必须先调用 next()
            throw new IllegalStateException();
        checkForComodification();
        try {
            ArrayList.this.remove(lastRet);  // 删除元素
            cursor = lastRet;  // 游标回退到上一次 next() 的索引
            lastRet = -1; // 重置lastRet
            expectedModCount = modCount;  // 同步修改计数
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings(&quot;unchecked&quot;)
    public void forEachRemaining(Consumer&amp;#x3C;? super E&gt; consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i &gt;= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i &gt;= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size &amp;#x26;&amp;#x26; modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // 在每次迭代结束时统一更新，以减少堆写入
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

	// ArrayList 每次结构性修改都会增加 modCount
	// 迭代器保存创建时的 modCount 快照
	// 如果两者不一致，说明在迭代过程中集合被其他线程修改了
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;ListIterator&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;除了 &lt;code&gt;Iterator()&lt;/code&gt; 方法外，&lt;code&gt;ArrayList&lt;/code&gt; 还提供了两个方法用于返回 &lt;code&gt;ListIterator&lt;/code&gt; 接口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public ListIterator&amp;#x3C;E&gt; listIterator(int index) {
    if (index &amp;#x3C; 0 || index &gt; size)
        throw new IndexOutOfBoundsException(&quot;Index: &quot;+index);
    return new ListItr(index);
}

public ListIterator&amp;#x3C;E&gt; listIterator() {
        return new ListItr(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;listlterator ()&lt;/code&gt; 方法返回的迭代器从 &lt;code&gt;0&lt;/code&gt; 开始，而 &lt;code&gt;listlterator (int index)&lt;/code&gt; 方法返回的迭代器从指定位置 &lt;code&gt;index&lt;/code&gt; 开始。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ListIterator&lt;/code&gt; 接口，扩展了 &lt;code&gt;Iterator&lt;/code&gt; 接口，增加了向前遍历、添加元素、修改元素、返回索引位置等方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface ListIterator&amp;#x3C;E&gt; extends Iterator&amp;#x3C;E&gt; {
    boolean hasNext();
    E next();
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();
    void remove();
    void set(E e);
    void add(E e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ListItr&lt;/code&gt; 是 &lt;code&gt;ArrayList&lt;/code&gt; 的一个内部类，继承了 &lt;code&gt;Itr&lt;/code&gt; 类并实现了 &lt;code&gt;ListIterator&lt;/code&gt; 接口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private class ListItr extends Itr implements ListIterator&amp;#x3C;E&gt; {
    ListItr(int index) {
        super();
        cursor = index;
    }

    public boolean hasPrevious() {
        return cursor != 0;
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor - 1;
    }

    @SuppressWarnings(&quot;unchecked&quot;)
    public E previous() {
        checkForComodification();
        int i = cursor - 1;
        if (i &amp;#x3C; 0)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i &gt;= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i;
        return (E) elementData[lastRet = i];
    }

    public void set(E e) {
        if (lastRet &amp;#x3C; 0)
            throw new IllegalStateException();
        checkForComodification();
        try {
            ArrayList.this.set(lastRet, e);
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    public void add(E e) {
        checkForComodification();
        try {
            int i = cursor;
            ArrayList.this.add(i, e);
            cursor = i + 1;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数组和 &lt;code&gt;ArrayList&lt;/code&gt; 互相转换&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ArrayList&lt;/code&gt; 中有两个方法可以返回数组：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Object[] toArray()
public &amp;#x3C;T&gt; T[] toArray(T[] a)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个方法返回的是 &lt;code&gt;Object&lt;/code&gt; 数组，代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二个方法需要传递一个数组作为参数，返回对应类型的数组，如果参数数组长度足以容纳所有元素，就使用该数组，否则就新建一个数组&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public &amp;#x3C;T&gt; T[] toArray(T[] a) {
    if (a.length &amp;#x3C; size)
        // 参数数组长度不够创建一个新数组
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
	// 使用参数中的数组，将 list 数据拷贝到参数数组
    System.arraycopy(elementData, 0, a, 0, size);
	// 标记数组结束位置
    if (a.length &gt; size)
        a[size] = null;
    return a;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Arrays&lt;/code&gt; 中有一个静态方法 &lt;code&gt;asList()&lt;/code&gt; 可以返回对应的 &lt;code&gt;List&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static &amp;#x3C;T&gt; List&amp;#x3C;T&gt; asList(T... a) {
	return new ArrayList&amp;#x3C;&gt;(a);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：这个方法返回的 &lt;code&gt;ArrayList&lt;/code&gt; 是 &lt;code&gt;Arrays&lt;/code&gt; 类的一个内部类&lt;/p&gt;
&lt;p&gt;内部类使用的数组就是传入的数组，没有拷贝，也不会动态改变大小&lt;/p&gt;
&lt;p&gt;对原来数组的修改也会反映到 &lt;code&gt;List&lt;/code&gt; 中，也不能使用 &lt;code&gt;add&lt;/code&gt; &lt;code&gt;remove&lt;/code&gt; 等方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static class ArrayList&amp;#x3C;E&gt; extends AbstractList&amp;#x3C;E&gt;
    implements RandomAccess, java.io.Serializable
{
    private static final long serialVersionUID = -2764017481108945198L;
    private final E[] a;

    ArrayList(E[] array) {
		// 直接使用的传入的数组
        a = Objects.requireNonNull(array);
    }

    @Override
    public int size() {
        return a.length;
    }

    @Override
    public Object[] toArray() {
        return a.clone();
    }

    @Override
    @SuppressWarnings(&quot;unchecked&quot;)
    public &amp;#x3C;T&gt; T[] toArray(T[] a) {
        int size = size();
        if (a.length &amp;#x3C; size)
            return Arrays.copyOf(this.a, size,
                                 (Class&amp;#x3C;? extends T[]&gt;) a.getClass());
        System.arraycopy(this.a, 0, a, 0, size);
        if (a.length &gt; size)
            a[size] = null;
        return a;
    }

    @Override
    public E get(int index) {
        return a[index];
    }

    @Override
    public E set(int index, E element) {
        E oldValue = a[index];
        a[index] = element;
        return oldValue;
    }

    @Override
    public int indexOf(Object o) {
        E[] a = this.a;
        if (o == null) {
            for (int i = 0; i &amp;#x3C; a.length; i++)
                if (a[i] == null)
                    return i;
        } else {
            for (int i = 0; i &amp;#x3C; a.length; i++)
                if (o.equals(a[i]))
                    return i;
        }
        return -1;
    }

    @Override
    public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

    @Override
    public Spliterator&amp;#x3C;E&gt; spliterator() {
        return Spliterators.spliterator(a, Spliterator.ORDERED);
    }

    @Override
    public void forEach(Consumer&amp;#x3C;? super E&gt; action) {
        Objects.requireNonNull(action);
        for (E e : a) {
            action.accept(e);
        }
    }

    @Override
    public void replaceAll(UnaryOperator&amp;#x3C;E&gt; operator) {
        Objects.requireNonNull(operator);
        E[] a = this.a;
        for (int i = 0; i &amp;#x3C; a.length; i++) {
            a[i] = operator.apply(a[i]);
        }
    }

    @Override
    public void sort(Comparator&amp;#x3C;? super E&gt; c) {
        Arrays.sort(a, c);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要将数组转换成 &lt;code&gt;ArrayList&lt;/code&gt; 并使用 &lt;code&gt;ArrayList&lt;/code&gt; 完整的方法，应该新建一个 &lt;code&gt;ArrayList&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;Integer&gt; list = new ArrayList&amp;#x3C;Integer&gt;(Arrays.asList(a));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?arraylist"/><enclosure url="http://wallpaper.csun.site/?arraylist"/></item><item><title>Redis 怎么模糊查询</title><link>https://blog.csun.site/blog/2025-09-16-redis-fuzzy-query</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-09-16-redis-fuzzy-query</guid><description>Redis 模糊查询方法解析</description><pubDate>Tue, 16 Sep 2025 11:14:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. KEYS pattern 模糊查询&lt;/h2&gt;
&lt;p&gt;Redis 的 &lt;code&gt;KEYS&lt;/code&gt; 命令用于按模式匹配查找所有符合条件的 key：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;KEYS pattern
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;pattern&lt;/code&gt; 可以使用 &lt;strong&gt;通配符:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;|符号|含义|示例|
| ----| -----------------------------| -----------------------------|
|​&lt;code&gt;*&lt;/code&gt;​|匹配任意数量（包括 0 个）字符|​&lt;code&gt;KEYS user:*&lt;/code&gt; ​匹配所有以 &lt;code&gt;user:&lt;/code&gt; ​开头的 key|
|​&lt;code&gt;?&lt;/code&gt;​|匹配任意一个字符|​&lt;code&gt;KEYS user:??&lt;/code&gt; ​匹配 &lt;code&gt;user:01&lt;/code&gt;、&lt;code&gt;user:ab&lt;/code&gt;​|
|​&lt;code&gt;[]&lt;/code&gt;​|匹配指定范围内的一个字符|​&lt;code&gt;KEYS user:[0-9]&lt;/code&gt; ​匹配 &lt;code&gt;user:0&lt;/code&gt;​~&lt;code&gt;user:9&lt;/code&gt;​|
|​&lt;code&gt;\&lt;/code&gt;​|转义字符|​&lt;code&gt;KEYS foo\*bar&lt;/code&gt; ​匹配键名 &lt;code&gt;foo*bar&lt;/code&gt;​|&lt;/p&gt;
&lt;p&gt;但是 &lt;code&gt;KEYS&lt;/code&gt; 会 &lt;strong&gt;遍历整个数据库&lt;/strong&gt;，在 key 很多（百万级）时会阻塞 Redis，导致线上性能问题，不推荐线上使用&lt;/p&gt;
&lt;h2&gt;2. SCAN 游标迭代模糊查询&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SCAN&lt;/code&gt; ​命令是为了解决 &lt;code&gt;KEYS&lt;/code&gt; ​命令的阻塞问题而设计的。它是一种基于游标的迭代器，每次只返回一小部分结果，不会阻塞服务器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SCAN cursor [MATCH pattern] [COUNT count]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cursor&lt;/code&gt;​&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;游标位置，第一次调用必须传 &lt;code&gt;0&lt;/code&gt;，之后用上一次返回结果里的游标继续迭代。&lt;/li&gt;
&lt;li&gt;当返回的游标再次为 &lt;code&gt;0&lt;/code&gt;，说明已经遍历完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;MATCH pattern&lt;/code&gt;​&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可选参数，用于模糊匹配，语法与 &lt;code&gt;KEYS&lt;/code&gt; 一致。&lt;/li&gt;
&lt;li&gt;常见通配符：&lt;code&gt;*&lt;/code&gt;（任意字符串）、&lt;code&gt;?&lt;/code&gt;（单个字符）、&lt;code&gt;[abc]&lt;/code&gt;（集合）、&lt;code&gt;[a-z]&lt;/code&gt;（范围）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;COUNT count&lt;/code&gt;​&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次迭代的​&lt;strong&gt;预期数量&lt;/strong&gt;（hint，不保证返回正好这么多）。&lt;/li&gt;
&lt;li&gt;一般用于控制扫描批次大小，建议在 &lt;code&gt;100 ~ 10000&lt;/code&gt; 之间，根据业务性能调节。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;SCAN&lt;/code&gt; ​的返回结果是一个二元组，例如执行 &lt;code&gt;scan 0 MATCH user:*&lt;/code&gt; 返回&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;1) &quot;1441792&quot;
2) 1) &quot;user:209543&quot;
   2) &quot;user:379429&quot;
   3) &quot;user:235282&quot;
   4) &quot;user:598444&quot;
   5) &quot;user:673366&quot;
   6) &quot;user:339760&quot;
   7) &quot;user:294641&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;&quot;1441792&quot;&lt;/code&gt; 是下一个游标，用于下一次迭代&lt;/p&gt;
&lt;p&gt;迭代时 Redis 仍然可写，所以可能会漏掉新增的 key 或重复扫描&lt;/p&gt;
&lt;h2&gt;3. 使用 ZSET + 前缀匹配&lt;/h2&gt;
&lt;p&gt;如果是前缀模糊查询（如查找所有以 &quot;abc&quot; 开头的 key 或值），可以把值存入有序集合 &lt;code&gt;ZSET&lt;/code&gt;，&lt;code&gt;ZSET&lt;/code&gt; ​具有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;成员（member）是字符串；&lt;/li&gt;
&lt;li&gt;每个成员有一个分数（score），集合会按照分数排序，如果分数相同会按照字典序排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以将需要索引的字符串全部加入到 ZSET 中，&lt;code&gt;score&lt;/code&gt; 统一设置为 0，这样会按照字典序排序&lt;/p&gt;
&lt;p&gt;然后通过 &lt;code&gt;ZRANGEBYLEX&lt;/code&gt; 命令来做前缀匹配，&lt;code&gt;ZRANGEBYLEX&lt;/code&gt; 命令是按照成员字典序返回有序集合中指定字典序范围的成员，这些这些成员&lt;strong&gt;分数必须相同&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ZRANGEBYLEX key [prefix [prefix+\xff
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;prefix&lt;/code&gt; 是查询的前缀。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prefix+\xff&lt;/code&gt; 表示前缀的上界（因为 &lt;code&gt;\xff&lt;/code&gt; 是字典序里最大的字符）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设我们有一批用户名字：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;alice
alex
allen
bob
bobby
carl

# 写入到 Redis
ZADD users 0 alice 0 alex 0 allen 0 bob 0 bobby 0 carl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询前缀 &lt;code&gt;&quot;al&quot;&lt;/code&gt;​&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ZRANGEBYLEX users [al [al\xff
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 redis-cli 里测试时，终端会把 &lt;code&gt;\xff&lt;/code&gt; 当作四个字符，所以会查不到数据，也可以执行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ZRANGEBYLEX users [al [am
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;m &gt; l&lt;/code&gt; 所以可以查出来所有前缀为 &lt;code&gt;&quot;al&quot;&lt;/code&gt; 的字符串&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?redislike"/><enclosure url="http://wallpaper.csun.site/?redislike"/></item><item><title>枚举的本质</title><link>https://blog.csun.site/blog/2025-09-14-nature-of-enumeration</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-09-14-nature-of-enumeration</guid><description>枚举在Java中的应用解析</description><pubDate>Sun, 14 Sep 2025 14:14:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 枚举的基本使用&lt;/h2&gt;
&lt;p&gt;枚举使用 &lt;code&gt;enum&lt;/code&gt; 这个关键字来定义，例如为了表示衣服的尺寸定义一个枚举类型 &lt;code&gt;Size&lt;/code&gt;​&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public enum Size {
    SMALL, MEDIUM, LARGE
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Size&lt;/code&gt; 包括三个值，分别表示小、中、大，值一般是大写字母，多个值之间以逗号分隔&lt;/p&gt;
&lt;p&gt;可以这样使用 &lt;code&gt;Size&lt;/code&gt;​&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Size size = Size.MEDIUM;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Size size&lt;/code&gt; 声明了一个变量 &lt;code&gt;size&lt;/code&gt;，它的类型是 &lt;code&gt;Size&lt;/code&gt;，&lt;code&gt;size=Size.MEDIUM&lt;/code&gt; 将枚举值 &lt;code&gt;MEDIUM&lt;/code&gt; 赋值给 &lt;code&gt;size&lt;/code&gt; 变量&lt;/p&gt;
&lt;p&gt;枚举变量的 &lt;code&gt;toString()&lt;/code&gt; 方法返回其字面值，枚举类型还有一个 &lt;code&gt;name()&lt;/code&gt; 方法也返回其字面值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Size size = Size.MEDIUM;
System.out.println(size.toString());
System.out.println(size.name());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;MEDIUM
MEDIUM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;枚举值可以使用 &lt;code&gt;equals()&lt;/code&gt;​​ 方法和 &lt;code&gt;==&lt;/code&gt;​​ 进行比较，结果相同&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Size size = Size.MEDIUM;
System.out.println(size == Size.MEDIUM);
System.out.println(size == Size.LARGE);
System.out.println(size.equals(Size.MEDIUM));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;true
false
true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;枚举类型都有一个方法 &lt;code&gt;int ordinal()&lt;/code&gt;，返回枚举值在声明时的顺序，从 &lt;code&gt;0&lt;/code&gt; 开始&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Size s0 = Size.SMALL;
Size s1 = Size.MEDIUM;
Size s2 = Size.LARGE;
System.out.println(s0.ordinal());
System.out.println(s1.ordinal());
System.out.println(s2.ordinal());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;0
1
2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;枚举类型都实现了 &lt;code&gt;Comparable&lt;/code&gt; 接口，可以通过方法 &lt;code&gt;compareTo&lt;/code&gt;​​ 与其他枚举值进行比较，其实就是比较 &lt;code&gt;ordinal()&lt;/code&gt;​​ 方法返回值的大小&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Size s0 = Size.SMALL;
System.out.println(s0.compareTo(Size.MEDIUM));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出 &lt;code&gt;-1&lt;/code&gt; 表明 &lt;code&gt;SMALL&lt;/code&gt; ​小于 &lt;code&gt;MEDIUM&lt;/code&gt;​&lt;/p&gt;
&lt;p&gt;枚举变量可以用于方法参数、类变量、实例变量等，最常用的还是用于 &lt;code&gt;switch&lt;/code&gt; 语句&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Size s0 = Size.SMALL;
switch (s0){
	case SMALL:
		System.out.println(&quot;chose small&quot;);
		break;
	case MEDIUM:
		System.out.println(&quot;chose medium&quot;);
		break;
	case LARGE:
		System.out.println(&quot;chose large&quot;);
		break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意在 &lt;code&gt;switch&lt;/code&gt; ​语句内部，枚举值不能带枚举类型前缀，例如，直接使用 &lt;code&gt;SMALL&lt;/code&gt;，不能使用 &lt;code&gt;Size.SMALL&lt;/code&gt;​&lt;/p&gt;
&lt;p&gt;枚举类型还有一个静态方法 &lt;code&gt;values()&lt;/code&gt; 返回一个包含所有枚举值的数组，顺序和声明时的顺序一致&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for (Size size : Size.values()) {
	System.out.println(size);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;SMALL
MEDIUM
LARGE
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 枚举的好处&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;定义枚举的语法更为简洁&lt;/li&gt;
&lt;li&gt;枚举更为安全。一个枚举类型的变量，它的值要么为 &lt;code&gt;null&lt;/code&gt;，要么为枚举值之一，不可能为其他值，但使用整型变量，它的值就没有办法强制，值可能就是无效的。&lt;/li&gt;
&lt;li&gt;枚举类型自带很多便利方法（如 &lt;code&gt;values&lt;/code&gt;、&lt;code&gt;valueOf&lt;/code&gt;、&lt;code&gt;toString&lt;/code&gt; ​等），易于使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 枚举的底层原理&lt;/h2&gt;
&lt;p&gt;枚举类型实际上会被 Java 编译器转换成一个对应的类，这个类型继承了 &lt;code&gt;java.lang.Enum&lt;/code&gt;​​ 类。&lt;/p&gt;
&lt;p&gt;Enum 类有 &lt;code&gt;name&lt;/code&gt; 和 &lt;code&gt;ordinal&lt;/code&gt; 两个实例变量，在构造方法中需要传递。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;name()&lt;/code&gt;、&lt;code&gt;toString()&lt;/code&gt;、&lt;code&gt;ordinal()&lt;/code&gt;、&lt;code&gt;compareTo()&lt;/code&gt;、&lt;code&gt;equals()&lt;/code&gt; 方法都是由 &lt;code&gt;Enum&lt;/code&gt; 类根据其实例变量 &lt;code&gt;name&lt;/code&gt; 和 &lt;code&gt;ordinal&lt;/code&gt; 实现的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;values()&lt;/code&gt; 和 &lt;code&gt;valueOf()&lt;/code&gt; 方法是编译器给每个枚举类型自动添加的。&lt;/p&gt;
&lt;p&gt;编译器转换后的代码大致如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public final class Size extends Enum {
    public static final Size SMALL = new Size(&quot;SMALL&quot;,0);
    public static final Size MEDIUM = new Size(&quot;MEDIUM&quot;,1);
    public static final Size LARGE = new Size(&quot;LARGE&quot;,2);
    private static final Size[] VALUES = new Size[]{SMALL,MEDIUM,LARGE};
    private Size(String name, int ordinal){
        super(name, ordinal);
    }
    public static Size[] values(){
        Size[] values = new Size[VALUES.length];
        System.arraycopy(VALUES, 0, values, 0, VALUES.length);
        return values;
    }
    public static Size valueOf(String name){
        return Enum.valueOf(Size.class, name);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Size&lt;/code&gt; 是 &lt;code&gt;final&lt;/code&gt; 的，所以枚举类型不能被继承，枚举值也都是 &lt;code&gt;final&lt;/code&gt; 的，所以定义出来了就不能被修改&lt;/p&gt;
&lt;h2&gt;4. 更多使用场景&lt;/h2&gt;
&lt;p&gt;实际使用时枚举中可能会有关联的实例变量和方法，例如&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public enum Size {
    SMALL(&quot;S&quot;, &quot;小号&quot;), 
    MEDIUM(&quot;M&quot;, &quot;中号&quot;), 
    LARGE(&quot;L&quot;, &quot;大号&quot;),
    ;

    private String abbr;
    private String title;

    private Size(String abbr, String title) {
        this.abbr = abbr;
        this.title = title;
    }

    public String getAbbr() {
        return abbr;
    }
    public String getTitle() {
        return title;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码定义了两个实例变量 &lt;code&gt;abbr&lt;/code&gt; 和 &lt;code&gt;title&lt;/code&gt;，以及对应的 get 方法，分别表示缩写和中文名称；定义了一个私有构造方法，接受缩写和中文名称，每个枚举值在定义的时候都传递了对应的值。&lt;/p&gt;
&lt;p&gt;注意： 枚举值的定义需要放在最上面，枚举值写完之后，要以分号 &lt;code&gt;;&lt;/code&gt; 结尾，然后才能写其他代码 &lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?enum"/><enclosure url="http://wallpaper.csun.site/?enum"/></item><item><title>继承中的类加载、对象创建、方法调用和变量访问的过程</title><link>https://blog.csun.site/blog/2025-09-08-class-loading-object-creation-method-calls-variable-access-process</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-09-08-class-loading-object-creation-method-calls-variable-access-process</guid><description>继承中的类加载与对象创建</description><pubDate>Mon, 08 Sep 2025 20:14:00 GMT</pubDate><content:encoded>&lt;p&gt;通过下面这个 demo 来介绍继承中的类加载、对象创建、方法调用和变量访问的过程&lt;/p&gt;
&lt;p&gt;Base 类&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Base&lt;/code&gt; 包括一个静态变量 &lt;code&gt;s&lt;/code&gt;，一个实例变量 &lt;code&gt;a&lt;/code&gt;，一段静态初始化代码块，一段实例初始化代码块，一个构造方法，两个方法 &lt;code&gt;step&lt;/code&gt; 和 &lt;code&gt;action&lt;/code&gt;​&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Base {

    public static int s;
    private int a;

    static {
        System.out.println(&quot;基类静态代码块，s：&quot; + s);
        s = 1;
    }

    {
        System.out.println(&quot;基类实例代码块，a：&quot; + a);
        a = 1;
    }

    public Base(){
        System.out.println(&quot;基类构造方法，a：&quot; + a);
        a = 2;
    }

    protected void step(){
        System.out.println(&quot;base s: &quot; + s + &quot;, a: &quot; + a);
    }

    public void action(){
        System.out.println(&quot;start&quot;);
        step();
        System.out.println(&quot;end&quot;);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Child 类&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Child&lt;/code&gt; 类继承 &lt;code&gt;Base&lt;/code&gt; 类，定义了和父类同门的静态变量 &lt;code&gt;s&lt;/code&gt; 和实例变量 &lt;code&gt;a&lt;/code&gt;，还包含一段静态初始化代码块，一段实例初始化代码块，一个构造方法，并重写了 &lt;code&gt;step&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Child extends Base {

    public static int s;
    private int a;

    static {
        System.out.println(&quot;子类静态代码块，s：&quot; + s);
        s = 10;
    }

    {
        System.out.println(&quot;子类实例代码块，a：&quot; + a);
        a = 10;
    }

    public Child(){
        System.out.println(&quot;子类构造方法，a：&quot; + a);
        a = 20;
    }
	
	@Override
    protected void step(){
        System.out.println(&quot;child s：&quot; + s + &quot;，a：&quot; + a);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;main 方法&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;main&lt;/code&gt; 方法中创建了 &lt;code&gt;Child&lt;/code&gt; 类型的对象，并赋值给 &lt;code&gt;Child&lt;/code&gt; 类型的引用变量 &lt;code&gt;c&lt;/code&gt;，通过 &lt;code&gt;c&lt;/code&gt; 调用 &lt;code&gt;action&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;然后又将 &lt;code&gt;c&lt;/code&gt; 赋值给了 &lt;code&gt;Base&lt;/code&gt; 类型的引用变量 &lt;code&gt;b&lt;/code&gt;，通过 &lt;code&gt;b&lt;/code&gt; 也调用了 &lt;code&gt;action&lt;/code&gt; 方法，最后通过 &lt;code&gt;b&lt;/code&gt; 和 &lt;code&gt;c&lt;/code&gt; 分别访问静态变量 &lt;code&gt;s&lt;/code&gt; 并输出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    System.out.println(&quot;--- new Child()&quot;);
    Child c = new Child();
    System.out.println(&quot;\n--- c.action()&quot;);
    c.action();
    Base b = c;
    System.out.println(&quot;\n--- b.action()&quot;);
    b.action();
    System.out.println(&quot;\n--- b.s: &quot; + b.s);
    System.out.println(&quot;\n--- c.s: &quot; + c.s);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序的执行结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;--- new Child()
基类静态代码块, s: 0
子类静态代码块, s: 0
基类实例代码块, a: 0
基类构造方法, a: 1
子类实例代码块, a: 0
子类构造方法, a: 10

--- c.action()
start
child s: 10, a: 20
end

--- b.action()
start
child s: 10, a: 20
end

--- b.s: 1
--- c.s: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们一步步来看这段代码的背后发生了什么&lt;/p&gt;
&lt;h2&gt;1. 类加载过程&lt;/h2&gt;
&lt;p&gt;执行 &lt;code&gt;new Child()&lt;/code&gt; 会先进行类加载，Class 文件需要加载到虚拟机中才能运行和使用，类加载过程就是虚拟机加载 Class 文件的过程，主要分为三步：加载-&gt;连接-&gt;初始化，其中连接又可以分为：验证-&gt;准备-&gt;解析&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/09/c819b03385b2dd0f46eafeb934a1ec0d.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1.1 加载&lt;/h3&gt;
&lt;p&gt;加载这一步主要是通过类加载器完成的，主要完成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过全类名获取定义该类的二进制字节流；&lt;/li&gt;
&lt;li&gt;将字节流所代表的静态存储结构转换为方法区的运行时数据结构；&lt;/li&gt;
&lt;li&gt;在内存中生成一个代表该类的 &lt;code&gt;Class&lt;/code&gt; 对象，作为方法区这些数据的访问入口。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;方法区属于是 JVM 运行时数据区域的一块逻辑区域，是各个线程共享的内存区域。当虚拟机要使用一个类时，它需要读取并解析 Class 文件获取相关信息，再将信息存入到方法区。方法区会存储已被虚拟机加载的 &lt;strong&gt;类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一个类的信息主要包括以下部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;类变量（静态变量）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类初始化代码；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义静态变量时的赋值语句；&lt;/li&gt;
&lt;li&gt;静态初始化代码块。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类方法（静态方法）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实例变量；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实例初始化代码；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义实例变量时的赋值语句；&lt;/li&gt;
&lt;li&gt;实例初始化代码块；&lt;/li&gt;
&lt;li&gt;构造方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实例方法；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;父类信息引用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 验证&lt;/h3&gt;
&lt;p&gt;验证是连接阶段的第一步，这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求&lt;/p&gt;
&lt;p&gt;加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的，加载阶段尚未结束，连接阶段可能就已经开始了&lt;/p&gt;
&lt;p&gt;验证阶段主要由四个检验阶段组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件格式验证：验证字节流是否符合 Class 文件格式的规范，基于该类的二进制字节流进行，保证输入的字节流能正确的解析并存储在方法区&lt;/li&gt;
&lt;li&gt;元数据验证：对字节码描述的信息进行语义分析，以保证其描述的信息符合《Java 语言规范的要求》，基于方法区的存储结构进行；&lt;/li&gt;
&lt;li&gt;字节码验证：对代码的语义进行检查，例如：函数传参是否正确，对象转换是否合理，基于方法区的存储结构进行；&lt;/li&gt;
&lt;li&gt;符号引用验证：验证该类的正确性，发生在类加载过程中的解析阶段，例如：该类使用的其他类方法是都存在，基于方法区的存储结构进行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3 准备&lt;/h3&gt;
&lt;p&gt;准备阶段是正式为类变量分配内存并设置类变量初始值的阶段，这些内存都在方法区中分配。&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这时候进行内存分配的仅包括类变量；&lt;/li&gt;
&lt;li&gt;JDK 7 之前，HotSpot 使用永久代来实现方法区，类变量所使用的内存都在方法区；在 JDK 7 及之后，HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中，类变量则会随着 Class 对象一起存放在 Java 堆中；&lt;/li&gt;
&lt;li&gt;这里所设置的初始值是数据类型的默认值，除非 &lt;code&gt;public static final int value = 111&lt;/code&gt;，初始值为 &lt;code&gt;111&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.4 解析&lt;/h3&gt;
&lt;p&gt;解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程，也就是得到类或者字段、方法在内存中的指针或者偏移量。&lt;/p&gt;
&lt;p&gt;解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;p&gt;在程序执行方法时，JVM 为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候，只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置，从而使得方法可以被调用。&lt;/p&gt;
&lt;h3&gt;1.5 初始化&lt;/h3&gt;
&lt;p&gt;初始化阶段是执行初始化方法 &lt;code&gt;&amp;#x3C;clinit&gt; ()&lt;/code&gt;​​方法的过程，&lt;code&gt;&amp;#x3C;clinit&gt;（）&lt;/code&gt; 方法是编译后自动生成的，并且是带锁线程安全的。&lt;/p&gt;
&lt;p&gt;只有下面 6 种情况，才会对类进行初始化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;遇到 &lt;code&gt;new&lt;/code&gt;​​、&lt;code&gt;getstatic&lt;/code&gt;​​、&lt;code&gt;putstatic&lt;/code&gt;​​ 或 &lt;code&gt;invokestatic&lt;/code&gt;​ 这 4 条字节码指令时&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new&lt;/code&gt; 创建一个类的实例对象；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getstatic&lt;/code&gt;、&lt;code&gt;putstatic&lt;/code&gt; 读取或设置一个类型的静态字段（被 &lt;code&gt;final&lt;/code&gt; 修饰、已在编译期把结果放入常量池的静态字段除外）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;invokestatic&lt;/code&gt;: 调用类的静态方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;java.lang.reflect&lt;/code&gt; 包的方法对类进行反射调用时，如果类没初始化，需要触发初始化；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化一个类，如果其父类还未初始化，则先触发该父类的初始化；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;虚拟机启动时会先初始化含 &lt;code&gt;main&lt;/code&gt;​​ 方法的类；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要使用 &lt;code&gt;MethodHandle&lt;/code&gt; ​和 &lt;code&gt;VarHandle&lt;/code&gt; ​这 2 个调用，就必须先使用 &lt;code&gt;findStaticVarHandle&lt;/code&gt; 来初始化要调用的类；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个接口中定义了默认方法时，如果有这个接口的实现类发生了初始化，那该接口要在其之前被初始化。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;开头的例子，执行完类加载后，内存布局大致如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/09/c7dbb94075f25912841eed7607e051f6.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;h2&gt;2. 对象创建的过程&lt;/h2&gt;
&lt;p&gt;在类加载之后，&lt;code&gt;new Child()&lt;/code&gt; 就是创建 &lt;code&gt;Child&lt;/code&gt; 对象，创建对象过程包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分配内存；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;包括本类和所有父类的实例变量，但是不包括任何静态变量（已经在类加载中分配了）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对所有实例变量赋默认值；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行实例初始化代码&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从父类开始，再执行子类&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Child c=new Child()&lt;/code&gt; 会将新创建的 &lt;code&gt;Child&lt;/code&gt; 对象引用赋给变量 &lt;code&gt;c&lt;/code&gt;​&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Base b = c&lt;/code&gt; 让 &lt;code&gt;b&lt;/code&gt; 也引用这个 &lt;code&gt;Child&lt;/code&gt; 对象，完成对象创建后，大致内存布局如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/09/3841b46fde3891aa53beb06e12c0f3b8.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;引用型变量 &lt;code&gt;c&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 分配在栈中，它们指向相同的堆中的 &lt;code&gt;Child&lt;/code&gt; 对象。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Child&lt;/code&gt; 对象存储着方法区中 &lt;code&gt;Child&lt;/code&gt; 类型的地址，还有 &lt;code&gt;Base&lt;/code&gt; 中的实例变量 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;Child&lt;/code&gt; 中的实例变量 a&lt;/p&gt;
&lt;h2&gt;3. 方法访问的过程&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;c.action()&lt;/code&gt; 的执行过程如下：&lt;/p&gt;
&lt;p&gt;1）查看 &lt;code&gt;c&lt;/code&gt; 的对象类型，找到 &lt;code&gt;Child&lt;/code&gt; 类型，在 &lt;code&gt;Child&lt;/code&gt; 类型中找 &lt;code&gt;action&lt;/code&gt; 方法，发现没有，到父类中寻找；&lt;/p&gt;
&lt;p&gt;2）在父类 &lt;code&gt;Base&lt;/code&gt; 中找到了方法 &lt;code&gt;action&lt;/code&gt;，开始执行 &lt;code&gt;action&lt;/code&gt; 方法；&lt;/p&gt;
&lt;p&gt;3）&lt;code&gt;action&lt;/code&gt; 先输出了 &lt;code&gt;start&lt;/code&gt;，然后发现需要调用 &lt;code&gt;step()&lt;/code&gt; ​方法，就从 Child 类型开始寻找 &lt;code&gt;step()&lt;/code&gt; 方法；&lt;/p&gt;
&lt;p&gt;4）在 &lt;code&gt;Child&lt;/code&gt; 类型中找到了 &lt;code&gt;step()&lt;/code&gt; 方法，执行 &lt;code&gt;Child&lt;/code&gt; 中的 &lt;code&gt;step()&lt;/code&gt; 方法，执行完后返回 &lt;code&gt;action&lt;/code&gt; 方法；&lt;/p&gt;
&lt;p&gt;5）继续执行 &lt;code&gt;action&lt;/code&gt; 方法，输出 end。&lt;/p&gt;
&lt;p&gt;寻找要执行的实例方法的时候，是从对象的实际类型信息开始查找的，找不到的时候，再查找父类类型信息。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;b.action()&lt;/code&gt; 的输出和 &lt;code&gt;c.action()&lt;/code&gt; 相同，这称为动态绑定，动态绑定实现的机制就是根据对象的实际类型查找要执行的方法，子类型中找不到的时候再查找父类&lt;/p&gt;
&lt;p&gt;为了提高动态绑定的效率，会采用虚方法表来优化，就是在类加载的时候为每个类创建一个表，记录该类的对象所有动态绑定的方法签名（方法名 + 参数类型）（包括父类的方法）及其地址，但一个方法只有一条记录，子类重写了父类方法后只会保留子类的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/09/9a9b7ac1f5494a3a42d04ceb12e25ff6.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;h2&gt;4. 变量访问的过程&lt;/h2&gt;
&lt;p&gt;对变量的访问是静态绑定的，无论是类变量还是实例变量。&lt;/p&gt;
&lt;p&gt;代码中演示的是类变量：&lt;code&gt;b.s&lt;/code&gt; 和 &lt;code&gt;c.s&lt;/code&gt;，通过对象访问类变量，系统会转换为直接访问类变量 &lt;code&gt;Base.s&lt;/code&gt; 和 &lt;code&gt;Child.s&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例子中的实例变量都是 &lt;code&gt;private&lt;/code&gt; 的，不能直接访问；如果是 &lt;code&gt;public&lt;/code&gt; 的，则 &lt;code&gt;b.a&lt;/code&gt; ​访问的是对象中 &lt;code&gt;Base&lt;/code&gt; ​类定义的实例变量 &lt;code&gt;a&lt;/code&gt;，而 &lt;code&gt;c.a&lt;/code&gt; ​访问的是对象中 &lt;code&gt;Child&lt;/code&gt; ​类定义的实例变量 &lt;code&gt;a&lt;/code&gt;​&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?extend"/><enclosure url="http://wallpaper.csun.site/?extend"/></item><item><title>为什么 switch 语句效率比 if-else 语句高</title><link>https://blog.csun.site/blog/2025-09-07-why-switch-statement-is-more-efficient-than-if-else-statement</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-09-07-why-switch-statement-is-more-efficient-than-if-else-statement</guid><description>switch 语句效率分析</description><pubDate>Sun, 07 Sep 2025 20:14:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;switch&lt;/code&gt; 语句和 &lt;code&gt;if-else&lt;/code&gt; 语句都是条件控制语句，为什么说 &lt;code&gt;switch&lt;/code&gt; 语句效率比 &lt;code&gt;if-else&lt;/code&gt; 语句高呢?&lt;/p&gt;
&lt;p&gt;要回答这个问题，我们先要了解条件控制语句的底层原理是什么。&lt;/p&gt;
&lt;p&gt;在计算机底层程序最终都会转换成一条条的指令，CPU 有一个&lt;strong&gt;程序计数器（PC）&lt;/strong&gt;，指向下一条要执行的指令，CPU 根据程序计数器的指示加载指令并且执行。&lt;/p&gt;
&lt;p&gt;指令大部分是具体的操作和运算，执行完一条指令后，程序计数器会自动指向挨着的下一条指令。&lt;/p&gt;
&lt;p&gt;但有一些特殊的指令，称为&lt;strong&gt;跳转指令&lt;/strong&gt;，&lt;code&gt;if-else&lt;/code&gt; 语句实际上就会转换为这些跳转指令。&lt;/p&gt;
&lt;p&gt;跳转指令会修改程序计数器的值，让 CPU 跳到一个指定的地方执行。&lt;/p&gt;
&lt;p&gt;跳转指令有两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;条件跳转&lt;/strong&gt;：检查某个条件，满足则进行跳转；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无条件跳转&lt;/strong&gt;：直接进行跳转。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面一个简单的if语句：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (a &gt; b) {
    c = 1;
} else {
    c = 2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能被编译为类似这样的汇编代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-assembly&quot;&gt;; 假设a在eax，b在ebx，c在ecx
CMP eax, ebx      ; 比较a和b
JLE else_branch   ; 如果a&amp;#x3C;=b，跳转到else分支
MOV ecx, 1        ; a&gt;b，执行c=1
JMP end_if        ; 跳过else分支
else_branch:
MOV ecx, 2        ; 执行c=2
end_if:
; 继续执行后续代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;JLE&lt;/code&gt;（Jump if Less or Equal）是条件跳转指令，根据标志位决定是否跳转&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JMP&lt;/code&gt; 是无条件跳转指令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;if&lt;/code&gt;、&lt;code&gt;if/else&lt;/code&gt;、&lt;code&gt;if/else if/else&lt;/code&gt;、&lt;code&gt;三元运算符&lt;/code&gt; 都会转换为条件跳转和无条件跳转，但 &lt;code&gt;switch&lt;/code&gt; 语句不太一样。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;switch&lt;/code&gt; 的转换和具体系统实现有关：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果分支比较少，可能会转换为跳转指令；&lt;/li&gt;
&lt;li&gt;如果分支比较多，使用条件跳转会进行很多次的比较运算，效率比较低，可能会使用一种更为高效的方式，叫&lt;strong&gt;跳转表&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;跳转表是一个映射表，&lt;strong&gt;存储了可能的值以及要跳转到的地址&lt;/strong&gt;，如下表所示：&lt;/p&gt;
&lt;p&gt;| 条件值 | 跳转地址      |
| ------ | ------------- |
| 值 1   | 指令 1 的地址 |
| 值 2   | 指令 2 的地址 |
| ...    | ...           |
| 值 n   | 指令 n 的地址 |&lt;/p&gt;
&lt;p&gt;跳转表中的值必须为&lt;strong&gt;整数&lt;/strong&gt;，&lt;strong&gt;且按大小顺序排序&lt;/strong&gt;，这样就可以使用高效的&lt;strong&gt;二分查找&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果值是连续的，则跳转表还会进行特殊优化，优化为一个&lt;strong&gt;数组，值就是数组的下标索引&lt;/strong&gt;。即使值不是连续的，但数字比较密集，编译器也可能会优化为一个数组型的跳转表，没有的值指向 &lt;code&gt;default&lt;/code&gt; 分支。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;switch&lt;/code&gt; 值的类型可以是 &lt;code&gt;byte&lt;/code&gt;、&lt;code&gt;short&lt;/code&gt;、&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;char&lt;/code&gt;、&lt;code&gt;enum&lt;/code&gt; 和 &lt;code&gt;String&lt;/code&gt;，其中 &lt;code&gt;byte/short/int&lt;/code&gt; 本来就是整数，&lt;code&gt;char&lt;/code&gt; 本质上也是整数，而 &lt;code&gt;enum&lt;/code&gt; 类型也有对应的整数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt; 用于 &lt;code&gt;switch&lt;/code&gt; 时会通过 &lt;code&gt;hashCode()&lt;/code&gt; 方法转换为整数，但不同 &lt;code&gt;String&lt;/code&gt; 的 &lt;code&gt;hashCode&lt;/code&gt; 可能相同，跳转后会再次根据 &lt;code&gt;String&lt;/code&gt; 的内容进行比较判断。&lt;/p&gt;
&lt;p&gt;但是 &lt;strong&gt;&lt;code&gt;switch&lt;/code&gt; 不可以使用 &lt;code&gt;long&lt;/code&gt;&lt;/strong&gt;，因为跳转表值的存储空间一般为 32 位，容纳不下 &lt;code&gt;long&lt;/code&gt;。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?switchif"/><enclosure url="http://wallpaper.csun.site/?switchif"/></item><item><title>公式识别神器 —— Pot QwenOCR 插件</title><link>https://blog.csun.site/blog/2025-09-07-official-recognition-tool-pot-qwenocr-plugin</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-09-07-official-recognition-tool-pot-qwenocr-plugin</guid><description>Pot QwenOCR：全能OCR插件</description><pubDate>Sun, 07 Sep 2025 15:14:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一款完全免费的，基于阿里云通义千问（Qwen）AI 的 OCR 文字识别插件，为 Pot-APP 提供强大的图像文字识别能力，该插件具备：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全能识别&lt;/strong&gt;：普通文本/数学公式/代码块/验证码一网打尽&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;智能优化&lt;/strong&gt;：自动保留 Markdown 格式，LaTeX 公式精准转换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多账号护航&lt;/strong&gt;：Cookie 智能轮换机制保障服务稳定性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深度定制&lt;/strong&gt;：支持自定义 AI 提示词和模型选择&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安装指南&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;基础准备&lt;/strong&gt;&lt;br&gt;
访问 &lt;a href=&quot;https://pot-app.com/&quot;&gt;Pot 官网&lt;/a&gt; 下载安装客户端&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;获取插件&lt;/strong&gt;&lt;br&gt;
前往 &lt;a href=&quot;https://github.com/sun-i/pot-app-recognize-plugin-qwen-ocr/releases&quot;&gt;GitHub Releases&lt;/a&gt; 下载最新插件包&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;插件安装&lt;/strong&gt;&lt;br&gt;
打开 Pot 设置 → 文字识别 → 添加外部插件 → 选择下载得到的 &lt;code&gt;plugin.com.pot-app.qwen-ocr.potext&lt;/code&gt; 文件&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/ab995281f9d7f9cdac04a55e0dd48d68.png&quot; alt=&quot;插件安装示意图&quot;&gt;&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;h3&gt;Cookie 获取&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;登录 &lt;a href=&quot;https://chat.qwenlm.ai/&quot;&gt;Qwen 官网&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;按 &lt;code&gt;F12&lt;/code&gt; 打开开发者工具&lt;/li&gt;
&lt;li&gt;发起任意对话后，在「网络」标签中捕获 &lt;code&gt;completions&lt;/code&gt; 请求的 Cookie&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://s2.loli.net/2025/02/11/Dr9xnSGzqVXgceW.png&quot; alt=&quot;Cookie获取演示&quot;&gt;&lt;/p&gt;
&lt;h3&gt;参数设置建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多账号配置&lt;/strong&gt;：使用英文逗号分隔多个 Cookie 提升稳定性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型选择&lt;/strong&gt;：默认 &lt;code&gt;qwen-max-latest&lt;/code&gt; 已优化识别效果&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提示词定制&lt;/strong&gt;：专业用户可自定义识别指令优化特定场景效果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/09/4132ddcac3805a64f0cc6e1bb235006d.png&quot; alt=&quot;参数设置界面&quot;&gt;&lt;/p&gt;
&lt;h2&gt;使用技巧&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;快捷键设置&lt;/strong&gt;&lt;br&gt;
在「热键设置」中绑定顺手的触发快捷键&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/297511e346842b7cc15a35f6f0bf2161.png&quot; alt=&quot;热键配置示例&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;效果演示&lt;/strong&gt;&lt;br&gt;
截图后自动识别并生成规范格式内容：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/1d80cd8843fff9889de4de90b6729568.png&quot; alt=&quot;识别效果展示&quot;&gt;&lt;/p&gt;
&lt;h3&gt;常见错误及解决方案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&quot;所有 Cookie 均已失效&quot;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;解决方案：重新获取 Cookie 并更新配置&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Cookie 格式无效&quot;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;检查 Cookie 是否包含 &lt;code&gt;token=&lt;/code&gt; 字段&lt;/li&gt;
&lt;li&gt;确保 Cookie 完整且没有被截断&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;无法读取截图文件&quot;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;检查 Pot-APP 是否有足够的文件访问权限&lt;/li&gt;
&lt;li&gt;尝试重新截图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;网络连接失败&quot;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;检查网络连接&lt;/li&gt;
&lt;li&gt;确认能正常访问 &lt;code&gt;chat.qwenlm.ai&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?qwenocr"/><enclosure url="http://wallpaper.csun.site/?qwenocr"/></item><item><title>配置 SSH 密钥登录</title><link>https://blog.csun.site/blog/2025-08-03-configure-ssh-key-login</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-08-03-configure-ssh-key-login</guid><description>SSH密钥登录配置指南</description><pubDate>Wed, 20 Aug 2025 20:55:00 GMT</pubDate><content:encoded>&lt;p&gt;使用 SSH 密钥登录比密码登录更安全、更便捷。配置 SSH 密钥登录主要分为两个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成密钥对&lt;/li&gt;
&lt;li&gt;将公钥上传到服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;生成密钥对&lt;/h2&gt;
&lt;p&gt;首先需要&lt;strong&gt;在自己的电脑上&lt;/strong&gt;生成密钥对，密钥对由一个私钥和一个公钥组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;私钥 (&lt;code&gt;id_ed25519&lt;/code&gt;)&lt;/strong&gt;: 必须严格保密，留存在你的本地电脑上，相当于你的“身份证明”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公钥 (&lt;code&gt;id_ed25519.pub&lt;/code&gt;)&lt;/strong&gt;: 可以安全地分享，需要被放置在你想登录的服务器上，相当于一把“锁”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打开终端，使用 ssh-keygen 命令来生成密钥&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen -t ed25519 -C &quot;your_email@example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-t ed25519&lt;/code&gt;: 使用 Ed25519 算法。如果你的系统很老不支持，可以换成 &lt;code&gt;rsa -b 4096&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-C &quot;your_email@example.com&quot;&lt;/code&gt;: 添加一段注释，通常用邮箱来标识这个密钥是谁的、用在哪台电脑上，方便管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运行命令后，根据系统提示进行操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enter file in which to save the key (…): 保存密钥的位置，直接回车即可，会使用默认路径（通常是 ~/.ssh/id_ed25519）。&lt;/li&gt;
&lt;li&gt;Enter passphrase (empty for no passphrase): 提示为私钥设置一个密码，不设置登录会更方便，设置了登录需要再输入密码&lt;/li&gt;
&lt;li&gt;Enter same passphrase again: 再次输入你设置的密码进行确认。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完成后，会在 &lt;code&gt;~/.ssh&lt;/code&gt; 目录下看到两个新文件：&lt;code&gt;id_ed25519&lt;/code&gt; (私钥) 和 &lt;code&gt;id_ed25519.pub&lt;/code&gt; (公钥)。&lt;/p&gt;
&lt;h2&gt;将公钥复制到服务器&lt;/h2&gt;
&lt;p&gt;要实现免密登录，需要把刚刚生成的&lt;strong&gt;公钥&lt;/strong&gt; (&lt;code&gt;id_ed25519.pub&lt;/code&gt;) 的内容，添加到服务器上登录用户的 &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; 文件中，有两种方法：&lt;/p&gt;
&lt;h3&gt;方法 1：使用 &lt;code&gt;ssh-copy-id&lt;/code&gt; 命令&lt;/h3&gt;
&lt;p&gt;这个命令会自动完成所有操作，包括在服务器上创建目录、设置文件和修正权限，能有效避免手动操作的失误。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：Windows 需要打开 Git bash 执行这个命令，亲测 PowerShell 和 CMD 都不支持这个命令&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;ssh-copy-id -p 端口号 username@server_ip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你的密钥不在默认路径，可以使用 &lt;code&gt;-i&lt;/code&gt; 参数指定路径&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-copy-id -i ~/.ssh/other_key.pub username@server_ip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;系统会提示输入服务器密码，输入密码后会自动将公钥内容追加到服务器上的 &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; 文件中&lt;/p&gt;
&lt;h3&gt;方法 2：手动复制粘贴&lt;/h3&gt;
&lt;p&gt;ssh 连接上服务器，执行以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 确保 .ssh 目录存在
mkdir -p ~/.ssh

# 将你复制的公钥内容粘贴到这里，然后回车
echo &quot;在这里粘贴你的公钥内容&quot; &gt;&gt; ~/.ssh/authorized_keys

# 修正关键的目录和文件权限，这一步至关重要！
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后将本地电脑 &lt;code&gt;~/.ssh/id_ed25519.pub&lt;/code&gt; 文件中的内容完整复制到服务器上的&lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; 文件中&lt;/p&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;p&gt;退出服务器，然后再次尝试登录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh username@server_ip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一切顺利，系统会直接让你登录，而不会再询问密码&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?serverkey"/><enclosure url="http://wallpaper.csun.site/?serverkey"/></item><item><title>Github 主页美化</title><link>https://blog.csun.site/blog/2025-08-03-github-homepage-beautification</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-08-03-github-homepage-beautification</guid><description>美化GitHub主页指南</description><pubDate>Sun, 03 Aug 2025 10:55:00 GMT</pubDate><content:encoded>&lt;p&gt;闲来无事，想要折腾下 GitHub 的主页，虽然没什么代码，但是漂亮了才有生产力嘛&lt;/p&gt;
&lt;h2&gt;创建仓库&lt;/h2&gt;
&lt;p&gt;美化 GitHub 的主页很简单，只需要新建一个和我们&lt;strong&gt;用户名同名&lt;/strong&gt;的仓库，并且添加一个 &lt;code&gt;README.md&lt;/code&gt; 文件即可&lt;/p&gt;
&lt;p&gt;我们后续在这个 &lt;code&gt;README.md&lt;/code&gt; 文件中写的内容都会展现在主页上&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/08/84e0b9d00482870d8dc7bcffc043ba25.png&quot; alt=&quot;image-20250803110413293&quot;&gt;&lt;/p&gt;
&lt;p&gt;GitHub 会提示我们这是一个 ✨ special ✨ 仓库，您可以使用它来为您的 GitHub profile 添加 &lt;code&gt;README.md&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;创建成功后，GitHub 已经为我们自动添加了一些初始化内容，接下来我们就可以通过&lt;strong&gt;修改这个文件&lt;/strong&gt;来美化我们的主页&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/08/1fcfbd2c035419e788e060e45d109a30.png&quot; alt=&quot;image-20250803110619203&quot;&gt;&lt;/p&gt;
&lt;h2&gt;基本信息&lt;/h2&gt;
&lt;p&gt;编辑个人基本信息可以借助 &lt;a href=&quot;https://profilinator.rishav.dev/&quot;&gt;profilinator.rishav.dev&lt;/a&gt; 这个网站&lt;/p&gt;
&lt;p&gt;这是一个可视化 profile 生成工具，仅需要在对应窗口中输入或者选择相应的内容，工具会自动生成 Markdown 脚本&lt;/p&gt;
&lt;p&gt;脚本编辑完成以后，直接复制粘贴到我们的 &lt;code&gt;README.md&lt;/code&gt; 即可，可以帮助我们便捷的生成美观的个人基本信息&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/08/3a61912e3709a57d8a0ee475557e769f.png&quot; alt=&quot;image-20250803111124217&quot;&gt;&lt;/p&gt;
&lt;h3&gt;贪吃蛇&lt;/h3&gt;
&lt;p&gt;默认情况下，GitHub 主页的提交热力图是这样的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/08/148c2d6a6d85d9fbbf03a0c7c1cfb143.png&quot; alt=&quot;image-20250803111256129&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可以将其变成一个有趣的贪吃蛇动画&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/08/99a582f5cb6d5d317fe16eb794fc6ae2.gif&quot; alt=&quot;动画&quot;&gt;&lt;/p&gt;
&lt;p&gt;在仓库中先新建一个 workflow 文件，点击 &lt;code&gt;Actions -&gt; New workflow&lt;/code&gt;，选择 &lt;code&gt;set up a workflow yourself&lt;/code&gt;，文件名可以随便取一个&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/08/7810fe82a3c82a70679f311350921da5.png&quot; alt=&quot;image-20250803145440830&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/08/833645d2adafb3e0cab4637735257b11.png&quot; alt=&quot;image-20250803145458424&quot;&gt;&lt;/p&gt;
&lt;p&gt;将以下代码复制到 workflow 中，然后点击 &lt;code&gt;Commit Changes&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: generate animation

on:
  # run automatically every 2 hours
  schedule:
    - cron: &quot;0 */2 * * *&quot; 
  
  # allows to manually run the job at any time
  workflow_dispatch:
  
  # run on every push on the master branch
  push:
    branches:
    - master
  
  

jobs:
  generate:
    permissions: 
      contents: write
    runs-on: ubuntu-latest
    timeout-minutes: 5
  
    steps:
      # generates a snake game from a github user (&amp;#x3C;github_user_name&gt;) contributions graph, output a svg animation at &amp;#x3C;svg_out_path&gt;
      - name: generate github-contribution-grid-snake.svg
        uses: Platane/snk/svg-only@v3
        with:
          github_user_name: ${{ github.repository_owner }}
          outputs: |
            dist/github-contribution-grid-snake.svg
            dist/github-contribution-grid-snake-dark.svg?palette=github-dark
  
  
      # push the content of &amp;#x3C;build_dir&gt; to a branch
      # the content will be available at https://raw.githubusercontent.com/&amp;#x3C;github_user&gt;/&amp;#x3C;repository&gt;/&amp;#x3C;target_branch&gt;/&amp;#x3C;file&gt; , or as github page
      - name: push github-contribution-grid-snake.svg to the output branch
        uses: crazy-max/ghaction-github-pages@v3.1.0
        with:
          target_branch: output
          build_dir: dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/08/2f56f203adb221a7d0dab65b1a365cc4.png&quot; alt=&quot;image-20250803145728204&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个 workflow 的作用是每隔 2 个小时执行一次，&lt;strong&gt;在仓库中生成一个贪吃蛇的 svg 动画&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;刚创建完 workflow 我们可以先手动执行一次，点击 &lt;code&gt;generate animation -&gt; Run workflow -&gt; Run workflow&lt;/code&gt; 即可手动执行&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/08/0cb0f7a815dd500702a18e5954763f68.png&quot; alt=&quot;image-20250803150217119&quot;&gt;&lt;/p&gt;
&lt;p&gt;等待执行完成后，&lt;strong&gt;切换到 output 分支&lt;/strong&gt;即可看到两个贪吃蛇动画的 svg 文件，分别对应暗色主题和亮色主题&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/08/4015854eab79cecb9541b6788a4e85d9.png&quot; alt=&quot;image-20250803150312377&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来修改 &lt;code&gt;README.md&lt;/code&gt; 文件，添加下述内容，注意将用户名修改成你的 GitHub 用户名&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;picture&gt;
  &amp;#x3C;source media=&quot;(prefers-color-scheme: dark)&quot; srcset=&quot;https://raw.githubusercontent.com/SunXin121/SunXin121/output/github-contribution-grid-snake-dark.svg&quot;&gt;
  &amp;#x3C;source media=&quot;(prefers-color-scheme: light)&quot; srcset=&quot;https://raw.githubusercontent.com/SunXin121/SunXin121/output/github-contribution-grid-snake.svg&quot;&gt;
  &amp;#x3C;img alt=&quot;github contribution grid snake animation&quot; src=&quot;https://raw.githubusercontent.com/SunXin121/SunXin121/output/github-contribution-grid-snake.svg&quot;&gt;
&amp;#x3C;/picture&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;提交之后就能在主页看到有趣的贪吃蛇动画了&lt;/p&gt;
&lt;h3&gt;同步博客文章&lt;/h3&gt;
&lt;p&gt;如果你有博客网站，且网站带有 RSS 功能，就可以配置此功能，它能在你的 GitHub 首页上显示最近更新的博客&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/08/0f23a0144bd6a1c6232dc5bab31cafcd.png&quot; alt=&quot;image-20250803150627612&quot;&gt;&lt;/p&gt;
&lt;p&gt;首先还是需要创建一个 workflow，代码如下，注意&lt;strong&gt;需要修改最后一行的 &lt;code&gt;feedlist&lt;/code&gt; 为你的博客的 RSS 链接&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: Latest blog post workflow
on:
  schedule: # Run workflow automatically
    - cron: &apos;0 */2 * * *&apos; # Runs every hour, on the hour
  workflow_dispatch: # Run workflow manually (without waiting for the cron to be called), through the GitHub Actions Workflow page directly
permissions:
  contents: write # To write the generated contents to the readme

jobs:
  update-readme-with-blog:
    name: Update this repo&apos;s README with latest blog posts
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Pull in blog&apos;s posts
        uses: gautamkrishnar/blog-post-workflow@v1
        with:
          feed_list: &quot;https://blog.csun.site/atom.xml&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时这个还支持一些自定义参数，如显示文章数量、主题等，更多构建参数可以查看 &lt;a href=&quot;https://github.com/gautamkrishnar/blog-post-workflow&quot;&gt;blog-post-workflow&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;然后在 &lt;code&gt;README.md&lt;/code&gt; 文件中添加下列内容，workflow 会自动抓取文章标题、链接等并替换这两个注释&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;!-- BLOG-POST-LIST:START --&gt;
&amp;#x3C;!-- BLOG-POST-LIST:END --&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理，刚创建完我们也需要手动执行一次 workflow&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?github"/><enclosure url="http://wallpaper.csun.site/?github"/></item><item><title>基于 RBAC 的权限模型优化实践</title><link>https://blog.csun.site/blog/2025-07-27-rbac-permission-model-optimization-allow-deny-bitmask</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-07-27-rbac-permission-model-optimization-allow-deny-bitmask</guid><description>本文在经典 RBAC 的基础上，引入 用户级 Allow/Deny 覆盖机制，并结合 Bitmask 位掩码优化权限存储与鉴权性能，实现 API 级细粒度控制，满足多业务线、复杂授权场景下的权限管理需求。</description><pubDate>Sun, 27 Jul 2025 23:14:12 GMT</pubDate><content:encoded>&lt;p&gt;传统 RBAC（Role-Based Access Control）模型在企业系统中被广泛应用，但在复杂业务场景下容易出现 &lt;strong&gt;「角色爆炸」&lt;/strong&gt; 问题。本文在经典 RBAC 的基础上，引入 &lt;strong&gt;用户级 Allow / Deny 覆盖机制&lt;/strong&gt;，并结合 &lt;strong&gt;Bitmask 位掩码&lt;/strong&gt; 优化权限存储与鉴权性能，实现 API 级细粒度控制，满足多业务线、复杂授权场景下的权限管理需求。&lt;/p&gt;
&lt;h2&gt;RBAC 权限模型简介&lt;/h2&gt;
&lt;p&gt;RBAC（Role-Based Access Control，基于角色的访问控制）是一种通过 &lt;strong&gt;「角色」&lt;/strong&gt; 来组织和管理权限的访问控制模型。其核心思想是：&lt;strong&gt;用户不直接拥有权限，而是通过角色间接获得权限&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这种设计将「用户」和「权限」解耦，大幅提升了权限管理的可维护性。&lt;/p&gt;
&lt;h3&gt;核心组成&lt;/h3&gt;
&lt;p&gt;典型 RBAC 模型包含以下几个核心概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User（用户）&lt;/strong&gt; ：系统的使用者，如管理员、员工、主管等；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Role（角色）&lt;/strong&gt; ：权限的集合，是权限的载体；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permission（权限）&lt;/strong&gt; ：系统允许执行的具体操作（如访问某个 API、执行某个按钮操作等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据库模型设计&lt;/h3&gt;
&lt;p&gt;标准 RBAC 的数据库模型通常包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;user&lt;/code&gt; 表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;role&lt;/code&gt; 表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;permission&lt;/code&gt; 表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user_role&lt;/code&gt; 表（用户与角色，多对多）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;role_permission&lt;/code&gt; 表（角色与权限，多对多）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关系结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User  &amp;#x3C;---&gt;  User_Role  &amp;#x3C;---&gt;  Role  &amp;#x3C;---&gt;  Role_Permission  &amp;#x3C;---&gt;  Permission
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt; 优缺点&lt;/h3&gt;
&lt;p&gt;RBAC 模型的优点是权限分配清晰、管理结构规范，维护成本较低且用户与权限解耦&lt;/p&gt;
&lt;p&gt;缺点是当某个用户需要「特殊权限」，需要给该用户单独新增一个角色，久而久之角色数量呈指数级增长，这就是所谓的 &lt;strong&gt;「角色爆炸」&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;优化方案设计&lt;/h2&gt;
&lt;p&gt;为了解决角色爆炸问题，并提升权限系统性能，本文在 RBAC 基础上做了两点核心优化：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入用户级 Allow / Deny 覆盖机制&lt;/li&gt;
&lt;li&gt;使用 Bitmask 位掩码优化权限存储与鉴权计算&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;引入用户级 Allow / Deny 覆盖机制&lt;/h3&gt;
&lt;p&gt;在保留原有 RBAC 模型的前提下，新增一张 &lt;code&gt;user_permission&lt;/code&gt; 表，用于记录用户级别的权限覆盖。&lt;/p&gt;
&lt;p&gt;该表支持两种类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Allow（允许）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deny（拒绝）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当某个用户需要特殊权限时，无需新增角色，只需在 &lt;code&gt;user_permission&lt;/code&gt; 表中新增一条记录，即可实现对角色权限的局部覆盖。&lt;/p&gt;
&lt;p&gt;最终权限可通过以下规则计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;最终权限 = (角色权限 ∪ 用户Allow) - 用户Deny
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样既保留了 RBAC 的结构优势，又解决了「角色爆炸」问题。&lt;/p&gt;
&lt;h3&gt;使用 Bitmask 优化权限存储与鉴权性能&lt;/h3&gt;
&lt;p&gt;在 API 级细粒度权限控制场景下，如果每个用户和 API、角色和 API 都单独存储一条权限数据，会产生庞大的数据量，为此，引入 Bitmask 进行优化。&lt;/p&gt;
&lt;p&gt;将某个模块下的所有 API 权限映射为一个 64 位整数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认值为 &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;每一位代表一个 API 权限&lt;/li&gt;
&lt;li&gt;若允许访问，则将对应位置为 &lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;第 0 位 -&gt; 查询接口
第 1 位 -&gt; 新增接口
第 2 位 -&gt; 删除接口
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个角色或用户在某模块下的所有权限，&lt;strong&gt;仅需一个&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;BIGINT&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;字段存储&lt;/strong&gt;，一行数据即可表示完整权限集合，大幅降低存储空间占用&lt;/p&gt;
&lt;p&gt;在实际鉴权时流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从拦截器中获取当前 API 的 URL&lt;/li&gt;
&lt;li&gt;解析其对应模块及位序号&lt;/li&gt;
&lt;li&gt;计算用户最终权限掩码 &lt;code&gt;finalMask= (roleMask | userAllowMask) &amp;#x26; ~uerDenyMask&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;判断对应位是否为 &lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;核心逻辑示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;boolean hasPermission = (finalMask &amp;#x26; (1L &amp;#x3C;&amp;#x3C; apiIndex)) != 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整套鉴权逻辑仅通过位运算完成，无需多表 join，无需大量权限比对，时间复杂度接近 O(1)。&lt;/p&gt;
&lt;h2&gt;扩展：ABAC 权限模型&lt;/h2&gt;
&lt;p&gt;ABAC（Attribute-Based Access Control，&lt;strong&gt;基于属性的访问控制&lt;/strong&gt;）是一种基于 &lt;strong&gt;「属性 + 策略规则」&lt;/strong&gt; 进行动态授权的权限模型。&lt;/p&gt;
&lt;p&gt;其核心思想是：是否允许访问，取决于「&lt;strong&gt;你是谁 + 你在什么场景 + 访问什么资源 + 满足什么规则&lt;/strong&gt;」。&lt;/p&gt;
&lt;p&gt;ABAC 权限模型是一种&lt;strong&gt;更灵活、更细粒度、更动态&lt;/strong&gt;的权限控制模型。&lt;/p&gt;
&lt;h3&gt;ABAC 的核心组成&lt;/h3&gt;
&lt;p&gt;ABAC 的决策基于四类属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户属性&lt;/strong&gt;：描述访问者本身的属性；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源属性&lt;/strong&gt;：描述被访问对象的属性；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;环境属性&lt;/strong&gt;：描述当前访问环境，例如访问时间、IP 地址；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作属性&lt;/strong&gt;：描述执行的动作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ABAC 的决策方式&lt;/h3&gt;
&lt;p&gt;ABAC 的核心是&lt;strong&gt;用策略定义一组规则，当属性满足规则时才允许访问。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例如，允许访问的条件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;用户.department == 资源.department
AND
资源.level != &quot;机密&quot;
AND
当前时间 在 9:00 - 18:00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果全部条件满足则允许访问，否则拒绝访问。&lt;/p&gt;
&lt;h3&gt;ABAC 的优缺点&lt;/h3&gt;
&lt;p&gt;ABAC 具有极强的灵活性，支持数据级权限控制，支持上下文感知（IP/权限），天然适合多组织多租户系统，但是实现复杂、策略管理困难、性能压力较大、规则冲突处理复杂。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?rbac"/><enclosure url="http://wallpaper.csun.site/?rbac"/></item><item><title>vercel 部署 Hexo 时安装 pandoc</title><link>https://blog.csun.site/blog/2025-07-26-vercel-deployment-install-pandoc</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-07-26-vercel-deployment-install-pandoc</guid><description>Vercel上Hexo部署Pandoc</description><pubDate>Sat, 26 Jul 2025 15:33:00 GMT</pubDate><content:encoded>&lt;p&gt;有时写的文章会有很多数学公式，放到 Hexo 中却出现了公式不全，公式超出文章边界等一系列问题显示上的问题。&lt;/p&gt;
&lt;p&gt;pandoc 是一款强大的渲染工具，可以完美处理文章中的数学公式，Hexo 提供了 &lt;a href=&quot;https://github.com/hexojs/hexo-renderer-pandoc&quot;&gt;hexo-renderer-pandoc&lt;/a&gt; 插件来使用 pandoc 渲染公式。&lt;/p&gt;
&lt;h2&gt;安装插件&lt;/h2&gt;
&lt;h3&gt;hexo-renderer-pandoc&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install hexo-renderer-pandoc --save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后需要配置 Hexo 根目录的 &lt;code&gt;_config.yml&lt;/code&gt; 配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;pandoc:
  args:
    - &apos;-f&apos;
    - &apos;commonmark_x&apos;
    - &apos;-t&apos;
    - &apos;html&apos;
    - &apos;--mathjax&apos;
  extensions:
    - &apos;-implicit_figures&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-f&lt;/code&gt; 表示输入格式，&lt;code&gt;-t&lt;/code&gt; 表示输出格式，这里的 &lt;code&gt;commonmark_x&lt;/code&gt;，是带有扩展的 &lt;a href=&quot;https://commonmark.org/&quot;&gt;CommonMark&lt;/a&gt; 风格&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--mathjax&lt;/code&gt; 用于添加 MathJax 数学公式的支持&lt;/p&gt;
&lt;p&gt;对于更多插件的配置，具体参考 &lt;a href=&quot;https://pandoc.org/MANUAL.html&quot;&gt;Pandoc User’s Guide&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/hexojs/hexo-renderer-pandoc&quot;&gt;hexo-renderer-pandoc&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;hexo-filter-mathjax&lt;/h3&gt;
&lt;p&gt;此外，还需要安装 &lt;a href=&quot;https://github.com/next-theme/hexo-filter-mathjax&quot;&gt;hexo-filter-mathjax&lt;/a&gt; 插件，用于&lt;strong&gt;后端渲染&lt;/strong&gt; mathjax&lt;/p&gt;
&lt;p&gt;hexo是静态博客，需要经过编译之后才能展现在页面中，这个编译的过程就包括了 mathjax 的后端渲染，经过后端渲染后，页面上已经是渲染好的公式了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install hexo-filter-mathjax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后配置 &lt;code&gt;_config.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yml&quot;&gt;mathjax:
  tags: none # or &apos;ams&apos; or &apos;all&apos;
  single_dollars: true # enable single dollar signs as in-line math delimiters
  cjk_width: 0.9 # relative CJK char width
  normal_width: 0.6 # relative normal (monospace) width
  append_css: true # add CSS to pages rendered by MathJax
  every_page: true # if true, every page will be rendered by MathJax regardless the `mathjax` setting in Front-matter
  packages:
    - physics
    - mathtools
    - color
    - noerrors
    - amsmath
  extension_options: {}
    # you can put your extension options here
    # see http://docs.mathjax.org/en/latest/options/input/tex.html#tex-extension-options for more detail
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里只添加了常用的几种扩展包（&lt;code&gt;packages&lt;/code&gt;），更多的 &lt;code&gt;packages&lt;/code&gt; 可以查看 &lt;a href=&quot;https://docs.mathjax.org/en/latest/input/tex/extensions/index.html&quot;&gt;The TeX/LaTeX Extension List&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;packages&lt;/code&gt; 不是越多越好，满足基本使用即可，太多会拖慢渲染速度&lt;/p&gt;
&lt;h2&gt;安装 pandoc&lt;/h2&gt;
&lt;h4&gt;本地部署&lt;/h4&gt;
&lt;p&gt;上述插件安装完成后，仍然需要安装 pandoc 的程序本体，如果是本地部署，那很简单，只要进入 &lt;a href=&quot;https://pandoc.org/installing.html&quot;&gt;pandoc 官网&lt;/a&gt; 下载相应版本并在电脑上安装即可&lt;/p&gt;
&lt;p&gt;pandoc 支持 Windows、mac、linux，对于 Windows ，可能需要重启一次电脑使配置生效。&lt;/p&gt;
&lt;h4&gt;vercel 部署&lt;/h4&gt;
&lt;p&gt;但是可能有很多人跟我一样，将博客部署在 vercel 上，而 vercel 默认的环境是没有 pandoc 的&lt;/p&gt;
&lt;p&gt;此时就需要我们&lt;strong&gt;在构建命令中加入下载安装 pandoc 的命令&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先，在根目录下新建一个 &lt;code&gt;build.sh&lt;/code&gt; 文件，写入以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;yum install wget
mkdir pandoc
wget -qO- https://github.com/jgm/pandoc/releases/download/3.7.0.2/pandoc-3.7.0.2-linux-amd64.tar.gz | \
   tar xvzf - --strip-components 1 -C ./pandoc
export PATH=&quot;./pandoc/bin:$PATH&quot;

yarn run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面的命令是用来安装 pandoc 的，而最后一条 &lt;code&gt;yarn run build&lt;/code&gt; 是用来执行&lt;code&gt;package.json&lt;/code&gt; 文件中 &lt;code&gt;scripts&lt;/code&gt; 部分定义的 &lt;code&gt;build&lt;/code&gt; 脚本，即用来启动 hexo 的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;hexo server -p $PORT&quot;,
    &quot;build&quot;: &quot;hexo generate&quot;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后需要修改 vercel 的部署命令为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ./build.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/07/3f7e9236f44e2bac0ff340421753427d.png&quot; alt=&quot;image-20250726154905428&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?pandoc"/><enclosure url="http://wallpaper.csun.site/?pandoc"/></item><item><title>多线程异步数据加载：Java 8 CompletableFuture 实战</title><link>https://blog.csun.site/blog/2025-07-10-java-completablefuture-async-data-loading</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-07-10-java-completablefuture-async-data-loading</guid><description>在现代系统开发中，接口响应速度直接影响用户体验和系统性能。为了提高接口的响应效率，我们通常会采用**异步线程加载数据**的方式，将耗时操作并行执行，然后在统一的逻辑处理中汇总结果。</description><pubDate>Thu, 10 Jul 2025 19:33:00 GMT</pubDate><content:encoded>&lt;p&gt;在现代系统开发中，接口响应速度直接影响用户体验和系统性能。为了提高接口的响应效率，我们通常会采用&lt;strong&gt;异步线程加载数据&lt;/strong&gt;的方式，将耗时操作并行执行，然后在统一的逻辑处理中汇总结果。这种方式不仅能够充分利用多核 CPU 的计算能力，还可以显著降低接口的响应时间。&lt;/p&gt;
&lt;p&gt;Java 8 引入了 &lt;code&gt;CompletableFuture&lt;/code&gt; 类，为我们提供了比传统 &lt;code&gt;Future&lt;/code&gt; 更强大、更灵活的异步编程能力。它不仅支持&lt;strong&gt;函数式编程风格&lt;/strong&gt;，还能实现复杂的&lt;strong&gt;异步任务组合和链式调用&lt;/strong&gt;，让多线程开发变得更加简单、高效。&lt;/p&gt;
&lt;h2&gt;CompletableFuture 基础操作&lt;/h2&gt;
&lt;h3&gt;创建 CompletableFuture&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CompletableFuture&lt;/code&gt; 提供了多种创建方式:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.util.concurrent.*;

public class Demo {
    public static void main(String[] args) {
        // 1. 已完成的 CompletableFuture
        CompletableFuture&amp;#x3C;String&gt; cf1 = CompletableFuture.completedFuture(&quot;Hello&quot;);

        // 2. 异步执行任务，返回结果（默认使用 ForkJoinPool.commonPool()）
        CompletableFuture&amp;#x3C;String&gt; cf2 = CompletableFuture.supplyAsync(() -&gt; &quot;World&quot;);

        // 3. 异步执行 Runnable（不返回结果）
        CompletableFuture&amp;#x3C;Void&gt; cf3 = CompletableFuture.runAsync(() -&gt; System.out.println(&quot;Run async task&quot;));

        // 4. 使用自定义线程池执行异步任务
        ExecutorService executor = Executors.newFixedThreadPool(2);
        CompletableFuture&amp;#x3C;String&gt; cf4 = CompletableFuture.supplyAsync(() -&gt; &quot;Hello&quot;, executor);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 转换操作&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CompletableFuture&lt;/code&gt; 提供了丰富的&lt;strong&gt;转换方法&lt;/strong&gt;，用于在任务完成后对结果进行处理：&lt;/p&gt;
&lt;p&gt;| 方法                   | 描述                                                         |
| ---------------------- | ------------------------------------------------------------ |
| &lt;code&gt;thenApply(Function)&lt;/code&gt;  | 接收前一个结果，返回新结果                                   |
| &lt;code&gt;thenAccept(Consumer)&lt;/code&gt; | 接收前一个结果，但不返回新结果（返回&lt;code&gt;CompletableFuture&amp;#x3C;Void&gt;&lt;/code&gt;） |
| &lt;code&gt;thenRun(Runnable)&lt;/code&gt;    | 不关心前一个结果，直接执行                                   |&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;CompletableFuture&amp;#x3C;String&gt; cf = CompletableFuture.supplyAsync(() -&gt; &quot;Hello&quot;);

cf.thenApply(s -&gt; s + &quot; World&quot;)   // 转换结果
  .thenAccept(System.out::println) // 输出: Hello World
  .thenRun(() -&gt; System.out.println(&quot;Done&quot;)); // 输出: Done
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 组合操作&lt;/h3&gt;
&lt;p&gt;当有多个异步任务时，可以通过组合操作将它们整合：&lt;/p&gt;
&lt;p&gt;| 方法             | 描述                                      |
| ---------------- | ----------------------------------------- |
| &lt;code&gt;thenCombine&lt;/code&gt;    | 两个 CF 都完成后，合并结果                |
| &lt;code&gt;thenAcceptBoth&lt;/code&gt; | 两个 CF 都完成后，消费结果，无返回值      |
| &lt;code&gt;runAfterBoth&lt;/code&gt;   | 两个 CF 都完成后，执行 Runnable，无返回值 |
| &lt;code&gt;applyToEither&lt;/code&gt;  | 任意一个完成后，处理结果                  |
| &lt;code&gt;acceptEither&lt;/code&gt;   | 任意一个完成后，消费结果                  |&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;CompletableFuture&amp;#x3C;String&gt; cf1 = CompletableFuture.supplyAsync(() -&gt; &quot;Hello&quot;);
CompletableFuture&amp;#x3C;String&gt; cf2 = CompletableFuture.supplyAsync(() -&gt; &quot;World&quot;);

cf1.thenCombine(cf2, (s1, s2) -&gt; s1 + &quot; &quot; + s2)
   .thenAccept(System.out::println); // 输出: Hello World

cf1.applyToEither(cf2, s -&gt; s + &quot;!!!&quot;)
   .thenAccept(System.out::println); // 输出: Hello!!! 或 World!!!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 阻塞获取结果&lt;/h3&gt;
&lt;p&gt;有时候，我们需要等待异步任务完成并获取结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;CompletableFuture&amp;#x3C;String&gt; cf = CompletableFuture.supplyAsync(() -&gt; &quot;Hello World&quot;);

// 阻塞等待完成
String result1 = cf.get();        // 可能抛出 checked 异常
String result2 = cf.join();       // RuntimeException 包装，通常更方便
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;join()&lt;/code&gt; 不会强制要求捕获 checked 异常，通常更适合链式调用。&lt;/p&gt;
&lt;h3&gt;5. 异常处理&lt;/h3&gt;
&lt;p&gt;异步任务可能会抛出异常，&lt;code&gt;CompletableFuture&lt;/code&gt; 提供了多种处理方式：&lt;/p&gt;
&lt;p&gt;| 方法            | 描述                                         |
| --------------- | -------------------------------------------- |
| &lt;code&gt;exceptionally&lt;/code&gt; | 捕获异常并返回默认值                         |
| &lt;code&gt;handle&lt;/code&gt;        | 捕获异常并处理，无论正常或异常都能处理       |
| &lt;code&gt;whenComplete&lt;/code&gt;  | 任务完成时处理，无论成功或失败，但不修改结果 |&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;CompletableFuture&amp;#x3C;Integer&gt; cf = CompletableFuture.supplyAsync(() -&gt; 1 / 0);

cf.exceptionally(ex -&gt; {
        System.out.println(&quot;Error: &quot; + ex);
        return 0;
    })
  .thenAccept(System.out::println); // 输出: 0

cf.handle((res, ex) -&gt; ex != null ? -1 : res * 2)
  .thenAccept(System.out::println); // 输出: -1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 批量组合操作&lt;/h3&gt;
&lt;p&gt;在实际业务中，我们经常需要同时处理多个异步任务：&lt;/p&gt;
&lt;p&gt;| 方法    | 描述                                |
| ------- | ----------------------------------- |
| &lt;code&gt;allOf&lt;/code&gt; | 等待多个 CompletableFuture 全部完成 |
| &lt;code&gt;anyOf&lt;/code&gt; | 等待任意一个 CompletableFuture 完成 |&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;CompletableFuture&amp;#x3C;String&gt; cf1 = CompletableFuture.supplyAsync(() -&gt; &quot;A&quot;);
CompletableFuture&amp;#x3C;String&gt; cf2 = CompletableFuture.supplyAsync(() -&gt; &quot;B&quot;);
CompletableFuture&amp;#x3C;String&gt; cf3 = CompletableFuture.supplyAsync(() -&gt; &quot;C&quot;);

// 等待所有任务完成
CompletableFuture&amp;#x3C;Void&gt; all = CompletableFuture.allOf(cf1, cf2, cf3);

all.thenRun(() -&gt; {
    String r1 = cf1.join();
    String r2 = cf2.join();
    String r3 = cf3.join();
    System.out.println(r1 + r2 + r3); // 输出: ABC
});

// 任意一个完成就返回
CompletableFuture&amp;#x3C;String&gt; cf4 = CompletableFuture.supplyAsync(() -&gt; {
    sleep(3000);
    return &quot;A&quot;;
});
CompletableFuture&amp;#x3C;String&gt; cf5 = CompletableFuture.supplyAsync(() -&gt; &quot;B&quot;);

CompletableFuture.anyOf(cf4, cf5)
    .thenAccept(result -&gt; System.out.println(&quot;First finished: &quot; + result)); 
// 输出: First finished: B
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;异步数据加载实战&lt;/h2&gt;
&lt;p&gt;下面是一个实际业务场景示例，展示如何使用 &lt;code&gt;CompletableFuture&lt;/code&gt; 并行加载多组数据，并统一填充到上下文对象 &lt;code&gt;dynamicContext&lt;/code&gt; 中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void loadData(ArmoryCommandEntity armoryCommandEntity,
                     DefaultArmoryStrategyFactory.DynamicContext dynamicContext) {

    List&amp;#x3C;String&gt; clientIdList = armoryCommandEntity.getCommandIdList();

    // 异步查询各类配置数据，并直接填充 dynamicContext
    CompletableFuture&amp;#x3C;Void&gt; aiClientApiFuture = CompletableFuture.supplyAsync(
        () -&gt; repository.queryAiClientApiVOListByClientIds(clientIdList), threadPoolExecutor)
        .thenAccept(result -&gt; dynamicContext.setValue(AiAgentEnumVO.AI_CLIENT_API.getDataName(), result));

    CompletableFuture&amp;#x3C;Void&gt; aiClientModelFuture = CompletableFuture.supplyAsync(
        () -&gt; repository.AiClientModelVOByClientIds(clientIdList), threadPoolExecutor)
        .thenAccept(result -&gt; dynamicContext.setValue(AiAgentEnumVO.AI_CLIENT_MODEL.getDataName(), result));

    CompletableFuture&amp;#x3C;Void&gt; aiClientToolMcpFuture = CompletableFuture.supplyAsync(
        () -&gt; repository.AiClientToolMcpVOByClientIds(clientIdList), threadPoolExecutor)
        .thenAccept(result -&gt; dynamicContext.setValue(AiAgentEnumVO.AI_CLIENT_TOOL_MCP.getDataName(), result));

    CompletableFuture&amp;#x3C;Void&gt; aiClientSystemPromptFuture = CompletableFuture.supplyAsync(
        () -&gt; repository.queryAiClientSystemPromptMapByClientIds(clientIdList), threadPoolExecutor)
        .thenAccept(result -&gt; dynamicContext.setValue(AiAgentEnumVO.AI_CLIENT_SYSTEM_PROMPT.getDataName(), result));

    CompletableFuture&amp;#x3C;Void&gt; aiClientAdvisorFuture = CompletableFuture.supplyAsync(
        () -&gt; repository.AiClientAdvisorVOByClientIds(clientIdList), threadPoolExecutor)
        .thenAccept(result -&gt; dynamicContext.setValue(AiAgentEnumVO.AI_CLIENT_ADVISOR.getDataName(), result));

    CompletableFuture&amp;#x3C;Void&gt; aiClientFuture = CompletableFuture.supplyAsync(
        () -&gt; repository.AiClientVOByClientIds(clientIdList), threadPoolExecutor)
        .thenAccept(result -&gt; dynamicContext.setValue(AiAgentEnumVO.AI_CLIENT.getDataName(), result));

    // 等待所有异步任务完成
    CompletableFuture.allOf(
            aiClientApiFuture,
            aiClientModelFuture,
            aiClientToolMcpFuture,
            aiClientSystemPromptFuture,
            aiClientAdvisorFuture,
            aiClientFuture
    ).join(); // 阻塞直到全部完成
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个数据源的查询都是独立的异步任务，互不阻塞，调用 &lt;strong&gt;&lt;code&gt;supplyAsync()&lt;/code&gt;&lt;/strong&gt;  提交一个有返回值的异步任务，这里调用仓储层的查询方法，当查询结果返回后，调用 &lt;strong&gt;&lt;code&gt;thenAccept()&lt;/code&gt;&lt;/strong&gt;  直接将结果写入 &lt;code&gt;dynamicContext&lt;/code&gt; 对应的字段，&lt;strong&gt;注意&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;dynamicContext&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;需要是线程安全的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;strong&gt;&lt;code&gt;CompletableFuture.allOf(...)&lt;/code&gt;&lt;/strong&gt;  等待传入的所有 &lt;code&gt;CompletableFuture&lt;/code&gt; 完成，** 调用 &lt;code&gt;allOf()&lt;/code&gt; 本身并不会阻塞 **，只是创建了一个表示「所有任务完成」的 Future，所以还需要使用 &lt;code&gt;join()&lt;/code&gt; 确保在继续业务逻辑前，&lt;code&gt;dynamicContext&lt;/code&gt; 已经填充完整，数据安全可靠。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?CompletableFuture"/><enclosure url="http://wallpaper.csun.site/?CompletableFuture"/></item><item><title>Linux统计文件夹下的文件数目</title><link>https://blog.csun.site/blog/2025-04-21-linux-count-files-in-directory</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-04-21-linux-count-files-in-directory</guid><description>Linux文件夹文件计数技巧</description><pubDate>Mon, 21 Apr 2025 13:41:59 GMT</pubDate><content:encoded>&lt;p&gt;在Linux中，有几种方法可以统计文件夹下的文件数目：&lt;/p&gt;
&lt;h2&gt;使用&lt;code&gt;ls&lt;/code&gt;命令结合&lt;code&gt;wc&lt;/code&gt;命令&lt;/h2&gt;
&lt;p&gt;统计当前目录下的文件数（不包括子目录中的文件）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -l | grep ^- | wc -l
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ls -l&lt;/code&gt; 列出详细信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep ^-&lt;/code&gt; 过滤出以&quot;-&quot;开头的行（即普通文件）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wc -l&lt;/code&gt; 计算行数&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;使用&lt;code&gt;find&lt;/code&gt;命令&lt;/h2&gt;
&lt;p&gt;统计指定目录及其子目录中的所有文件数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find /path/to/directory -type f | wc -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只统计指定目录（不包括子目录）中的文件数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find /path/to/directory -maxdepth 1 -type f | wc -l
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;按文件类型统计&lt;/h2&gt;
&lt;p&gt;统计指定目录中特定类型的文件数（例如.txt文件）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find /path/to/directory -name &quot;*.txt&quot; | wc -l
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?a58992b7-0d2e-4186-af76-03c4f44fdb0c"/><enclosure url="http://wallpaper.csun.site/?a58992b7-0d2e-4186-af76-03c4f44fdb0c"/></item><item><title>Win 下使用 Git 自动同步笔记</title><link>https://blog.csun.site/blog/2025-04-18-win-git-auto-sync-notes</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-04-18-win-git-auto-sync-notes</guid><description>Git自动同步笔记方案</description><pubDate>Fri, 18 Apr 2025 17:14:00 GMT</pubDate><content:encoded>&lt;p&gt;折腾了很久笔记系统后，觉得还是大道至简，回归到 &lt;strong&gt;Typora + MarkDown&lt;/strong&gt;，但是这样就要面对一个笔记多端同步的问题。&lt;/p&gt;
&lt;p&gt;因为主要还是在 Win 下写笔记比较多，最终决定使用 &lt;strong&gt;GitHub 同步笔记&lt;/strong&gt;，手机端和 IPad 端只用 GitHub APP 查看笔记。&lt;/p&gt;
&lt;p&gt;同时为了避免手动同步笔记的麻烦，本文就介绍了一种使用 Git 自动同步笔记的方案。&lt;/p&gt;
&lt;h2&gt;自动同步脚本&lt;/h2&gt;
&lt;p&gt;首先写一个 &lt;code&gt;auto_save.bat&lt;/code&gt; 脚本用于 &lt;code&gt;commit&lt;/code&gt; 并 &lt;code&gt;push&lt;/code&gt; 到 GitHub 仓库，脚本内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bat&quot;&gt;D:  
cd D:\\study\\note
git add .                           
git commit -m &quot;auto save&quot;   
git push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将脚本中的&lt;strong&gt;盘符（我这里是 &lt;code&gt;D:&lt;/code&gt;）和路径&lt;/strong&gt;更换成自己的就行，&lt;code&gt;&quot;auto save&quot;&lt;/code&gt; 可以更换成别的 Git Message&lt;/p&gt;
&lt;p&gt;但是这个脚本会有一个问题，每次执行的时候都会弹出来 &lt;code&gt;cmd&lt;/code&gt; 窗口，为了解决这个问题，我们还需要写一个 &lt;code&gt;auto_save.vbs&lt;/code&gt; 脚本，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vbscript&quot;&gt;set ws=WScript.CreateObject(&quot;WScript.Shell&quot;)
ws.Run &quot;D:\study\note\auto_save.bat&quot;,0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 VBS 脚本的作用是创建一个 &lt;code&gt;WScript.Shell&lt;/code&gt; 对象并使用该对象&lt;strong&gt;在后台&lt;/strong&gt;来运行 &lt;code&gt;auto_save.bat&lt;/code&gt;，这样就可以避免弹出来 &lt;code&gt;cmd&lt;/code&gt; 窗口&lt;/p&gt;
&lt;p&gt;注意 &lt;code&gt;ws.Run&lt;/code&gt; 后面的路径要换成上面 &lt;code&gt;auto_save.bat&lt;/code&gt; 的路径&lt;/p&gt;
&lt;h2&gt;Win 定时任务&lt;/h2&gt;
&lt;p&gt;接下来就要设置一个定时任务来定时执行上面的脚本。&lt;/p&gt;
&lt;p&gt;打开 &lt;strong&gt;控制面板 –&gt; Windows 工具 –&gt; 任务计划程序&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/04/5aca1648e6383bc4ccaa22ed0be4585b.png&quot; alt=&quot;image-20250418174425022&quot;&gt;&lt;/p&gt;
&lt;p&gt;点击 &lt;strong&gt;创建任务&lt;/strong&gt;，自定义一个任务名称&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/04/2921ca44a866bed366d38b466bf3114a.png&quot; alt=&quot;image-20250418174635029&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后在触发器中，设置一个触发器，也就是任务的定时执行时间，像下图我设置的就是&lt;strong&gt;每小时执行一次&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/04/e14fdae37f3a9ff562bfd5a0679ec5a0.png&quot; alt=&quot;image-20250418174821675&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后点击操作，新建，选择启动程序，浏览，选择刚才写的 &lt;code&gt;auto_save.vbs&lt;/code&gt; 脚本，点击确定，保存即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/04/563e12e51af24987ece145840952d80c.png&quot; alt=&quot;image-20250418174902781&quot;&gt;&lt;/p&gt;
&lt;p&gt;这样，每隔一个小时就会自动同步我们的笔记到 GitHub 仓库&lt;/p&gt;
&lt;p&gt;目前 GitHub APP 还能直连，直接用手机查看笔记也是非常方便&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?gitnote"/><enclosure url="http://wallpaper.csun.site/?gitnote"/></item><item><title>零拷贝实现高效文件传输</title><link>https://blog.csun.site/blog/2025-04-15-zero-copy-efficient-file-transfer</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-04-15-zero-copy-efficient-file-transfer</guid><description>零拷贝提升文件传输效率</description><pubDate>Tue, 15 Apr 2025 15:32:00 GMT</pubDate><content:encoded>&lt;h2&gt;传统文件传输&lt;/h2&gt;
&lt;p&gt;传统 I/O 的工作方式是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据的读取是在用户态发起 &lt;code&gt;read()&lt;/code&gt; 系统调用，从内核空间将数据拷贝到用户空间，而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘中读取；&lt;/li&gt;
&lt;li&gt;数据的写入是在用户态发起 &lt;code&gt;write()&lt;/code&gt; 系统调用，从用户空间将数据拷贝到内核空间，再通过操作系统层面的 I/O 接口写入到磁盘。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果我们想要用传统的 I/O 方式来实现服务端的文件传输，我们需要先将磁盘上的文件读取出来，然后写入到网卡，由网卡通过网络协议将文件发给客户端。&lt;/p&gt;
&lt;p&gt;一般会涉及到两个系统调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;read(file, tmp_buf, len)
write(socket, tmp_buf, len)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，期间共发生了 &lt;strong&gt;4次内核态与用户态的上下文切换&lt;/strong&gt;，因为涉及到两个系统调用，每次发起系统调用时，都会从用户态切换到内核态，系统调用结束后又会切换回用户态，频繁的上下文切换会带来极大的开销。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/04/75246d88dd573d95a199f6d9a3041a99.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;其次，整个过程涉及到  &lt;strong&gt;4次数据拷贝&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，从磁盘中读取文件时，DMA 会将数据&lt;strong&gt;从磁盘文件拷贝到内核缓冲区&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;然后，CPU 会将数据&lt;strong&gt;从内核缓冲区拷贝到用户缓冲区&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;发送文件时，CPU 又会将数据&lt;strong&gt;从用户缓冲区拷贝到 socket 缓冲区&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;最后，DMA 会将数据&lt;strong&gt;从socket 缓冲区拷贝到网卡&lt;/strong&gt;，由网卡通过网络协议将文件传输到客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整个过程 1 份数据拷贝了 4 次，无疑消耗了大量的 CPU 资源，降低了系统性能。&lt;/p&gt;
&lt;h2&gt;零拷贝&lt;/h2&gt;
&lt;p&gt;要想提高文件传输的性能，就得减少 &lt;strong&gt;「内核态与用户态的上下文切换次数」&lt;strong&gt;和&lt;/strong&gt;「数据拷贝的次数」&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要减少「内核态与用户态的上下文切换次数」，就得减少系统调用的次数；&lt;/li&gt;
&lt;li&gt;要减少「数据拷贝的次数」，在文件传输场景中，一般不会在程序中对文件进行二次加工，所以用户缓冲区其实是没有必要的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;零拷贝技术&lt;/strong&gt;就可以减少 「内核态与用户态的上下文切换次数」和「数据拷贝的次数」。&lt;/p&gt;
&lt;p&gt;零拷贝主要有两种实现方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mmap + write&lt;/li&gt;
&lt;li&gt;sendfile&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;mmap + write&lt;/h3&gt;
&lt;p&gt;前文提到，&lt;code&gt;read()&lt;/code&gt; 系统调用会将数据从内核缓冲区域拷贝到用户缓冲区域，为了减少这一步的开销，可以使用 &lt;code&gt;mmap()&lt;/code&gt; 系统调用代替 &lt;code&gt;read()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;buf = mmap(file, len)
write(socket, buf, len)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户程序发起 &lt;code&gt;mmap()&lt;/code&gt; 系统调用后，从用户态切换到内核态，DMA 将数据从磁盘文件拷贝到内核缓冲区，执行完成后切换回用户态，用户程序和操作系统**「共享」**这个缓冲区，这样就减少了从内核缓冲区到用户缓冲区的数据拷贝开销；&lt;/li&gt;
&lt;li&gt;用户程序再发起 &lt;code&gt;write()&lt;/code&gt; 系统调用，切换到内核态，CPU 将内核缓冲区中的数据直接拷贝到 socket  缓冲区；&lt;/li&gt;
&lt;li&gt;最后，DMA 再将 socket 缓冲区的数据拷贝到网卡。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/04/3371519343b66205c9be449b3074292a.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;整个过程减少了一次数据拷贝，但是这还不是最理想的零拷贝。&lt;/p&gt;
&lt;h3&gt;sendfile&lt;/h3&gt;
&lt;p&gt;在 Linux 内核版本 2.1 中，提供了一个专门发送文件的系统调用 &lt;code&gt;sendfile()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前两个参数分别是目标端和源端的文件描述符，&lt;code&gt;offset&lt;/code&gt; 是源端的起始位置偏移量，&lt;code&gt;count&lt;/code&gt; 是需要发送数据的长度，返回值是实际发送数据的长度。&lt;/p&gt;
&lt;p&gt;首先，&lt;code&gt;sendfile()&lt;/code&gt; 可以代替 &lt;code&gt;read()&lt;/code&gt; 和 &lt;code&gt;write()&lt;/code&gt;，从而减少一次系统调用，也就减少了两次「内核态与用户态上下文转换」的开销；其次，&lt;code&gt;sendfile()&lt;/code&gt; 可以直接把数据从内核缓冲区拷贝到 socket 缓冲区，无需经过用户缓冲区，可以减少一次数据拷贝。这样整个过程就只有 &lt;strong&gt;2 次上下文切换和 3 次数据拷贝&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/04/c2209a458b4e0eb22f9574f091fee06e.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是这还不是真正的零拷贝，如果网卡支持 &lt;strong&gt;SG-DMA&lt;/strong&gt; 技术，可以进一步减少把数据从内核缓冲区拷贝到 socket 缓冲区的开销&lt;/p&gt;
&lt;p&gt;从 &lt;code&gt;Linux 2.4&lt;/code&gt; 开始，如果网卡支持 SG-DMA 技术，&lt;code&gt;sendfile()&lt;/code&gt; 系统调用的过程发生了一些变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户程序发起 &lt;code&gt;sendfile()&lt;/code&gt; 系统调用后，切换到内核态，DMA 将数据从磁盘文件拷贝到内核缓冲区，然后将 &lt;strong&gt;缓冲区描述符和数据长度&lt;/strong&gt; 发送到 socket 缓冲区&lt;/li&gt;
&lt;li&gt;接下来网卡的 &lt;strong&gt;SG-DMA 控制器&lt;/strong&gt; 就可以直接将数据从内核缓冲区拷贝到网卡，从而减少一次数据拷贝&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就零拷贝技术，整个过程没有通过 CPU 来拷贝数据，都是通过 DMA，只需要 &lt;strong&gt;2 次上下文切换 和 2 次数据拷贝&lt;/strong&gt;！&lt;/p&gt;
&lt;h2&gt;Java 实现 sendfile 零拷贝&lt;/h2&gt;
&lt;p&gt;Java 可以通过 &lt;code&gt;transferFrom()&lt;/code&gt; 和 &lt;code&gt;transferTo()&lt;/code&gt; 方法实现 sendfile 零拷贝&lt;/p&gt;
&lt;p&gt;&lt;code&gt;transferFrom&lt;/code&gt; 和 &lt;code&gt;transferTo&lt;/code&gt; 是 Java NIO (&lt;code&gt;java.nio.channels&lt;/code&gt;) 中 &lt;code&gt;FileChannel&lt;/code&gt; 类的两个方法，用于在文件通道之间高效地转移字节。它们是实现零拷贝的核心方法，允许在通道之间直接传输数据，避免了数据在用户空间和内核空间之间的多次拷贝，进而提高了性能。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;transferTo&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;transferTo&lt;/code&gt; 方法用于将数据从一个 &lt;code&gt;FileChannel&lt;/code&gt; 直接写入到另一个通道。它的基本语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public long transferTo(long position, long size, WritableByteChannel target) throws IOException
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;参数：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;position&lt;/code&gt;: 从源通道的哪个位置开始读取数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt;: 要传输的字节数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;target&lt;/code&gt;: 目标 &lt;code&gt;WritableByteChannel&lt;/code&gt;，通常是输出流或其他可写通道。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;返回值：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回实际传输的字节数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用场景：&lt;/strong&gt;
&lt;code&gt;transferTo&lt;/code&gt; 适用于将文件中的数据写入到网络套接字、输出流等写通道。它是一个简化数据传输的方式，减少了数据的拷贝次数。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;transferFrom&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;transferFrom&lt;/code&gt; 方法用于将数据从一个可读的 &lt;code&gt;ReadableByteChannel&lt;/code&gt; 读取到 &lt;code&gt;FileChannel&lt;/code&gt; 中。它的基本语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public long transferFrom(ReadableByteChannel src, long position, long size) throws IOException
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;参数：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src&lt;/code&gt;: 源 &lt;code&gt;ReadableByteChannel&lt;/code&gt;，通常是输入流或其他可读通道。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;position&lt;/code&gt;: 在目标通道中写入数据的起始位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt;: 要读取的字节数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;返回值：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回实际传输的字节数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;使用场景：&lt;/strong&gt;
&lt;code&gt;transferFrom&lt;/code&gt; 适用于将数据从输入流或其他通道读取并写入本地文件。它同样减少了数据在用户空间和内核空间之间的拷贝。&lt;/p&gt;
&lt;h3&gt;文件上传&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
@RequestMapping(&quot;/api/files&quot;)
public class FileUploadController {

    @PostMapping(&quot;/upload&quot;)
    public ResponseEntity&amp;#x3C;String&gt; uploadFile(@RequestParam(&quot;file&quot;) MultipartFile file) throws IOException {
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body(&quot;Please select a file to upload&quot;);
        }
        
        String fileName = file.getOriginalFilename();
        File dest = new File(&quot;/path/to/uploads/&quot; + fileName);
        
        // 确保目录存在
        dest.getParentFile().mkdirs();
        
        // 使用零拷贝技术保存文件
        try (ReadableByteChannel srcChannel = Channels.newChannel(file.getInputStream());
             FileChannel destChannel = new FileOutputStream(dest).getChannel()) {
            // transferFrom方法实现零拷贝
            destChannel.transferFrom(srcChannel, 0, file.getSize());
        }
        
        return ResponseEntity.ok(&quot;File uploaded successfully: &quot; + fileName);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;文件下载&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
@RequestMapping(&quot;/api/files&quot;)
public class FileDownloadController {

    @GetMapping(&quot;/download/{fileName}&quot;)
    public void downloadFile(@PathVariable String fileName, HttpServletResponse response) throws IOException {
        File file = new File(&quot;/path/to/files/&quot; + fileName);
        
        if (!file.exists()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, &quot;File not found&quot;);
            return;
        }
        
        response.setContentType(&quot;application/octet-stream&quot;);
        response.setContentLength((int) file.length());
        response.setHeader(&quot;Content-Disposition&quot;, &quot;attachment; filename=\&quot;&quot; + fileName + &quot;\&quot;&quot;);
        
        // 使用零拷贝技术传输文件
        try (FileChannel fileChannel = new FileInputStream(file).getChannel()) {
            WritableByteChannel outputChannel = Channels.newChannel(response.getOutputStream());
            // transferTo方法实现零拷贝
            fileChannel.transferTo(0, fileChannel.size(), outputChannel);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?zero"/><enclosure url="http://wallpaper.csun.site/?zero"/></item><item><title>MySQL 是如何实现事务的</title><link>https://blog.csun.site/blog/2025-03-15-how-mysql-transactions-work</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-03-15-how-mysql-transactions-work</guid><description>介绍 MySQL 的事务实现原理</description><pubDate>Sat, 15 Mar 2025 22:33:00 GMT</pubDate><content:encoded>&lt;h2&gt;事务的四大特性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A 原子性&lt;/strong&gt;：同一个事务中的所有操作要么全部成功，要么全部失败，靠 undo log 实现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C 一致性&lt;/strong&gt;：事务操作前和操作后，数据满足完整性约束，数据库保持一致性状态，是通过持久性+原子性+隔离性来保证&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I 隔离性&lt;/strong&gt;：隔离性：数据库允许多个并发事务同时对其数据进行读写和修改的能力，隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致，依靠 MCVV 和 锁机制实现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D 持久性&lt;/strong&gt;：事务处理结束后，对数据的修改就是永久的，即便系统故障也不会丢失，通过 redo log （重做日志）来保证的&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;undo log 保证事务的原子性&lt;/h2&gt;
&lt;p&gt;undo log 是一种用于回滚数据的日志，在事务未提交之前，MySQL 会先记录更新前的数据到 undo log 日志文件中，当需要回滚事务的时候，可以利用 undo log 回滚，从而保障了事务的原子性。&lt;/p&gt;
&lt;p&gt;针对不同的操作类型，undo log 记录的内容也不相同，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入一条数据时，只需要记录主键值，回滚的时候把这个主键对应的数据删掉即可；&lt;/li&gt;
&lt;li&gt;删除一条数据时，把这条数据中的内容记录下来，回滚的时候重新插入即可，&lt;code&gt;delete&lt;/code&gt; 操作实际上不会立即直接删除数据，而是打上一个 &lt;code&gt;delete flag&lt;/code&gt;，然后由 &lt;code&gt;purge&lt;/code&gt; 线程完成删除操作；&lt;/li&gt;
&lt;li&gt;更新一条数据时，&lt;code&gt;update&lt;/code&gt; 的列如果是主键列，则先删除该条数据，再插入；如果不是主键列，则在 undo log 中直接反向记录是如何 &lt;code&gt;update&lt;/code&gt; 的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是每一次操作产生的 undo log 都包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;roll_pointer&lt;/code&gt; 指针，将 undo log 串成一个链表，称为版本链；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trx_id&lt;/code&gt; 记录该数据是被哪个事务所修改的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个字段是实现 MCVV 的关键&lt;/p&gt;
&lt;h2&gt;并发事务引发的问题&lt;/h2&gt;
&lt;h3&gt;脏读&lt;/h3&gt;
&lt;p&gt;一个事务读到了另一个未提交事务修改过的数据&lt;/p&gt;
&lt;h3&gt;不可重复读&lt;/h3&gt;
&lt;p&gt;同一个事务中两次执行同样的查询语句，得到的结果不一样&lt;/p&gt;
&lt;h3&gt;幻读&lt;/h3&gt;
&lt;p&gt;同一个事务中两次执行同样的范围查询，得到的记录数量不一样&lt;/p&gt;
&lt;h2&gt;事务的隔离级别&lt;/h2&gt;
&lt;h3&gt;读未提交&lt;/h3&gt;
&lt;p&gt;最低的隔离级别，可以读取其他事务&lt;strong&gt;未提交的数据&lt;/strong&gt;，并发度最高，但是可能出现脏读、不可重复读和幻读问题。&lt;/p&gt;
&lt;h3&gt;读已提交&lt;/h3&gt;
&lt;p&gt;只能读取其他事务已经提交的数据，可能出现不可重复读和幻读问题。&lt;/p&gt;
&lt;h3&gt;可重复读&lt;/h3&gt;
&lt;p&gt;一个事务执行过程中看到的数据一直跟这个事务启动时看到的数据是一致的，&lt;strong&gt;InnoDB 存储引擎默认的事务隔离级别。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以解决脏读和不可重复读问题，&lt;strong&gt;InnoDB 存储引擎利用 MVCC + 临键锁可以很大程度上避免幻读问题&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;串行化&lt;/h3&gt;
&lt;p&gt;会对记录加上读写锁，所有事务按顺序执行，完全解决并发事务引发的问题，但是并发度极低。&lt;/p&gt;
&lt;h2&gt;MVCC + 锁保证事务的隔离性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;对于「读未提交」隔离级别来说，只要每次读取最新的数据即可；&lt;/li&gt;
&lt;li&gt;对于「串行化」隔离级别来说，在事务启动时候对记录加上读写锁，提交事务的时候释放锁，即可实现；&lt;/li&gt;
&lt;li&gt;对于「读已提交」和「可重复读」，是通过 &lt;strong&gt;Read View&lt;/strong&gt; 来实现的，只不过创建 Read View 的时机不同&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Read View 和 MVCC 是如何工作的&lt;/h3&gt;
&lt;p&gt;Read View 有四个重要的字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;m_ids&lt;/code&gt;：创建该 Read View 的时候数据库中「&lt;strong&gt;已经启动但是还未提交的&lt;/strong&gt;」事务 id 列表；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min_trx_id&lt;/code&gt;：&lt;code&gt;m_ids&lt;/code&gt; 的最小值；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_trx_id&lt;/code&gt;：创建该 Read View 的时候数据库应该给下一个事务分配的 id 值；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;creator_trx_id&lt;/code&gt;：创建该 Read View 的事务 id&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每一条聚簇索引中，会有两个隐藏字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;trx_id&lt;/code&gt;：当一个事务修改了这条记录后，把该事务的事务 id 记录在 &lt;code&gt;trx_id&lt;/code&gt; 中;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;roll_pointer&lt;/code&gt;：每次修改记录时，会把旧版本的记录写入到 undo log 中，&lt;code&gt;roll_pointer&lt;/code&gt; 是&lt;strong&gt;指向旧版本的指针&lt;/strong&gt;，多个旧版本记录形成一条版本链。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当一个事务去访问数据库中的数据的时候，会根据 Read View 和聚簇索引中的这些字段，来决定数据的可见性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;trx_id&lt;/code&gt; 等于 &lt;code&gt;creator_trx_id&lt;/code&gt;，说明这个版本数据的创建者/修改者就是这个事务，自然是&lt;strong&gt;可见&lt;/strong&gt;的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;trx_id&lt;/code&gt; 小于 &lt;code&gt;min_trx_id&lt;/code&gt;，说明创建/修改这个版本数据的事务在这个 Read View 创建之前就已经提交了，&lt;strong&gt;可见&lt;/strong&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;trx_id&lt;/code&gt; 大于 &lt;code&gt;max_trx_id&lt;/code&gt;，说明创建/修改这个版本数据的事务在这个 Read View 创建后才启动，&lt;strong&gt;不可见&lt;/strong&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;trx_id&lt;/code&gt; 在 &lt;code&gt;min_trx_id&lt;/code&gt; 和 &lt;code&gt;max_trx_id&lt;/code&gt; 之间：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;trx_id&lt;/code&gt; 在 &lt;code&gt;m_ids&lt;/code&gt; 中，说明创建/修改这个版本数据的事务还未提交，&lt;strong&gt;不可见&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;trx_id&lt;/code&gt; 不在 &lt;code&gt;m_ids&lt;/code&gt; 中，说明创建/修改这个版本数据的事务已经提交，&lt;strong&gt;可见。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样事务只能看见自己可见的版本，&lt;strong&gt;这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC（多版本并发控制）&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;可重复读是如何实现的&lt;/h3&gt;
&lt;p&gt;可重复读是在&lt;strong&gt;每次启动事务的时候创建一个 Read View&lt;/strong&gt;，然后整个事务期间都在使用这个 Read View&lt;/p&gt;
&lt;p&gt;这样保证了即使在事务执行过程中有新的事务修改了数据并提交了，也读不到&lt;/p&gt;
&lt;h3&gt;读已提交是如何实现的&lt;/h3&gt;
&lt;p&gt;读提交隔离级别是在&lt;strong&gt;每次读取数据时，都会生成一个新的 Read View&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;事务期间的多次读取同一条数据，前后两次读的数据可能会出现不一致，因为可能这期间另外一个事务修改了该记录，并提交了事务。&lt;/p&gt;
&lt;h3&gt;可重复读是如何避免幻读问题的&lt;/h3&gt;
&lt;p&gt;对于&lt;strong&gt;快照读&lt;/strong&gt;，即普通 &lt;code&gt;select&lt;/code&gt; 语句，是通过 MVCC 的方式解决幻读的，在可重复读隔离级别下，整个事务期间都在使用同一个 Read View，是看不到其他事务提交的新数据的&lt;/p&gt;
&lt;p&gt;对于当前读，即 &lt;code&gt;select ... for update&lt;/code&gt; 语句，是通过&lt;strong&gt;临键锁&lt;/strong&gt;的方式解决幻读问题&lt;/p&gt;
&lt;h2&gt;redo log 保证事务的持久性&lt;/h2&gt;
&lt;p&gt;InnoDB修改数据时不会直接写磁盘上的数据页，那样随机 IO太多，性能扛不住。它用的是WAL策略：先把修改操作&lt;strong&gt;顺序写到redo log&lt;/strong&gt;，再找机会把数据页刷到磁盘。顺序写比随机写快几个数量级。&lt;br&gt;
redo log 也是循环写的方式，有两个指针：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;writepos&lt;/code&gt; 表示当前写到哪了&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkpoint&lt;/code&gt; 表示已经刷盘的位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个指针之间就是待刷盘的脏数据&lt;/p&gt;
&lt;h3&gt;redo log 什么时候写入到磁盘&lt;/h3&gt;
&lt;p&gt;redo log 也有自己的缓存 redo log buffer，每当产生一条 redo log 时，会先写&lt;br&gt;
入到 redo log buffer，后续在持久化到磁盘，主要有几个刷盘的时机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MySQL 正常关闭的时候；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当 redo log buffer 中的写入量大于缓冲区的一般时&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;InnoDB 的后台线程&lt;strong&gt;每隔 1 秒&lt;/strong&gt;刷盘；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每次事务提交的时候（由 &lt;code&gt;innodb_flush_log_at_trx_commit&lt;/code&gt;）参数控制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置该参数为 0，每次事务提交的时候不会刷盘；只靠后台线程每隔 1s 执行 &lt;code&gt;write()&lt;/code&gt; 写操作写到 Page Cache，然后调用 &lt;code&gt;fsync()&lt;/code&gt; 持久化到磁盘。&lt;strong&gt;MySQL 崩溃会导致丢失 1s 数据&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;设置该参数为 1，每次事务提交的时候都刷盘；&lt;/li&gt;
&lt;li&gt;设置该参数为 2，只写入到内核缓冲区 Page Cache，靠后台线程每隔 1s 执行 &lt;code&gt;fsync()&lt;/code&gt; 持久化到磁盘。&lt;strong&gt;只有操作系统崩溃才会丢失数据&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;binlog&lt;/h3&gt;
&lt;p&gt;MySQL 在完成一条更新操作后，&lt;strong&gt;Server 层&lt;/strong&gt;还会生成一条 binlog，记录了所有数据库表变更和表数据修改，有 3 种格式类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;STATEMENT（默认）&lt;/strong&gt; ：记录每条修改数据的 SQL;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROW&lt;/strong&gt;：记录行数据最终被修改成什么样子；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MIXED&lt;/strong&gt;：根据不同情况自动使用 ROW 模式和 STATEMENT 模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;binlog 主要用于备份恢复和主从复制，保存的是全量的日志，写满一个文件，就会创建一个新的文件继续写。&lt;/p&gt;
&lt;p&gt;binlog 也会先写到 binlog cache 中，每个线程都会有一个单独的 binlog cache，参数 &lt;code&gt;sync_binlog&lt;/code&gt; 控制数据库的 binlog 刷到磁盘上的频率：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置为 0，每次提交事务的时候都会 &lt;code&gt;write&lt;/code&gt; 但不 &lt;code&gt;fsync&lt;/code&gt;，交由操作系统决定何时持久化到磁盘。&lt;/li&gt;
&lt;li&gt;设置为 1，每次提交事务的时候都会 &lt;code&gt;write&lt;/code&gt; 并马上 &lt;code&gt;fsync&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;设置为 N，每次提交事务的时候都会 &lt;code&gt;write&lt;/code&gt;，累积 N 个事务后才 &lt;code&gt;fsync&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;两阶段提交&lt;/h3&gt;
&lt;p&gt;事务提交后，redo log 和 binlog 都要持久化到磁盘，但是可能出现以下半成功状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;redo log 刷盘后，MySQL 宕机，binlog 还没来得及写入，MySQL 重启后会从 redo log 中恢复数据，但是从库由于 binlog 还没记录，会丢失这条数据，导致主从数据不一致；&lt;/li&gt;
&lt;li&gt;binlog 刷盘后，MySQL 宕机，redo log 还没来得及写入，会导致主库丢失这条数据，从库以为有 binlog 会同步这条数据，导致主从数据不一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了避免这种情况，使用了「&lt;strong&gt;两阶段提交&lt;/strong&gt;」来解决，把单个事务的提交过程分为&lt;strong&gt;准备阶段&lt;/strong&gt;和&lt;strong&gt;提交阶段&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MySQL 使用&lt;strong&gt;内部 XA 事务&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;准备阶段：将 &lt;strong&gt;XID 写入到 redo log&lt;/strong&gt;，同时将 redo log 对应的事务状态设置为 &lt;strong&gt;prepare&lt;/strong&gt;，然后&lt;strong&gt;将 redo log 持久化到磁盘&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;提交阶段：&lt;strong&gt;将 XID 写入到 binlog&lt;/strong&gt;，将 binlog 持久化到磁盘，然后&lt;strong&gt;将 redo log 的状态设置为 commit&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于处于 prepare 阶段的 redo log，即可以提交事务，也可以回滚事务，这取&lt;br&gt;
决于是否能在 binlog 中查找到与 redo log 相同的XID，如果有就提交事务，如果没有就回滚事务。这样就可以保证 redo log 和 binlog 这两份日志的一致性了。&lt;/p&gt;
&lt;p&gt;‍&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?acid"/><enclosure url="http://wallpaper.csun.site/?acid"/></item><item><title>Java 的三种代理模式：静态代理，动态代理，CGLIB 代理</title><link>https://blog.csun.site/blog/2025-02-15-java-three-proxy-patterns-static-dynamic-cglib</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-02-15-java-three-proxy-patterns-static-dynamic-cglib</guid><description>Java 代理模式详解</description><pubDate>Sat, 15 Feb 2025 22:33:00 GMT</pubDate><content:encoded>&lt;h2&gt;代理模式介绍&lt;/h2&gt;
&lt;p&gt;代理模式是一种设计模式，通过代理对象访问目标对象，这样可以在不对原来的目标对象作修改的情况下扩展其功能。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/02/1a37556b2684798871f47abd952c9959.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;代理对象在客户端和目标对象之间充当中介，负责将客户端的请求转发给目标对象，同时可以在转发请求前后进行额外的处理。&lt;/p&gt;
&lt;p&gt;Java 提供了三种代理模式，分别是静态代理，动态代理，CGLIB 代理。&lt;/p&gt;
&lt;h2&gt;静态代理&lt;/h2&gt;
&lt;p&gt;这种代理模式需要&lt;strong&gt;代理对象和目标对象实现一样的接口&lt;/strong&gt;，从而实现多态，并保证代理类和被代理类的方法一致性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：可以在不修改目标对象的前提下扩展目标对象的功能；&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ul&gt;
&lt;li&gt;代码冗余，代理对象要实现与目标对象一致的接口，会产生过多的代理类；&lt;/li&gt;
&lt;li&gt;不易维护，一旦接口增加方法，目标对象和代理对象都要进行修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;举例：计算方法执行时间的静态代理实现&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UserService&lt;/code&gt; 接口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface UserService {
    void insert();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UserServiceImpl&lt;/code&gt; 实现类  &lt;code&gt;insert&lt;/code&gt; 方法模拟插入用户数据&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class UserServiceImpl implements UserService{
    @Override
    public void insert() {
        try {
            sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(&quot;插入 10000 条用户数据成功！&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ProxyUser&lt;/code&gt; 代理类，实现计算插入用户方法耗时，需要实现 &lt;code&gt;UserService&lt;/code&gt; 接口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ProxyUser implements UserService{

    private UserService userService;

    public ProxyUser(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void insert() {
        LocalDateTime start = LocalDateTime.now();
        userService.insert();
        LocalDateTime end = LocalDateTime.now();
        System.out.println(&quot;耗时：&quot; + (end.getSecond() - start.getSecond()) + &quot;秒&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Test {
    public static void main(String[] args) {
        UserServiceImpl user = new UserServiceImpl();
        UserService proxyUser = new ProxyUser(user);
        proxyUser.insert();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;插入 10000 条用户数据成功！
耗时：1秒
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;动态代理&lt;/h2&gt;
&lt;p&gt;动态代理利用 JDK 提供的 API 动态的在内存中构建代理对象，从而实现对目标对象的代理功能，又被称为 JDK 代理或接口代理&lt;/p&gt;
&lt;p&gt;动态代理对象不需要实现接口，但是要求&lt;strong&gt;目标对象必须实现接口&lt;/strong&gt;，否则不能使用动态代理。&lt;/p&gt;
&lt;p&gt;动态代理主要设计的类和方法有：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;java.lang.reflect.Proxy&lt;/code&gt; 类，主要方法为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 返回一个指定接口的代理类实例，该接口可以将方法调用指派到指定的调用处理程序
static Object newProxyInstance(ClassLoader loader, Class&amp;#x3C;?&gt;[] interfaces, InvocationHandler h) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ClassLoader loader&lt;/code&gt;：定义代理类的类加载器；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Class&amp;#x3C;?&gt;[] interfaces&lt;/code&gt;：代理类要实现的接口列表，即目标对象实现的接口列表；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InvocationHandler h&lt;/code&gt;：指派方法调用的调用处理程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;java.lang.reflect.InvocationHandler&lt;/code&gt; 接口，主要方法有&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 在代理实例上处理方法调用并返回结果
Object invoke(Object proxy, Method method, Object[] args) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;举例：计算方法执行时间的动态代理实现&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ProxyUserFactory&lt;/code&gt; 代理工厂类，提供一个静态方法创建一个代理实例&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ProxyUserFactory {
    public static UserService createProxyUser(UserService userService){
        return (UserService) Proxy.newProxyInstance(
            ProxyUserFactory.class.getClassLoader(),
            userService.getClass().getInterfaces(),
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    LocalDateTime start = LocalDateTime.now();
                    Object rs = method.invoke(userService, args);
                    LocalDateTime end = LocalDateTime.now();
                    System.out.println(&quot;耗时：&quot; + (end.getSecond() - start.getSecond()) + &quot;秒&quot;);
                    return rs;
                }
            }
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;测试&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Test {
    public static void main(String[] args) {
        UserService user = new UserServiceImpl();
        UserService proxyUser = ProxyUserFactory.createProxyUser(user);
        proxyUser.insert();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动态代理必须实现InvocationHandler接口，通过反射代理方法，比较&lt;strong&gt;消耗系统性能&lt;/strong&gt;，但可以减少代理类的数量，使用更灵活。&lt;/p&gt;
&lt;h2&gt;CGLIB 代理&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CGLIB&lt;/code&gt;（Code Generation Library）是一个强大的 Java 字节码生成库，常用于创建运行时动态代理。它主要用于代理没有实现接口的类。&lt;/p&gt;
&lt;p&gt;CGLIB 通过 &lt;strong&gt;继承目标类&lt;/strong&gt; 并 &lt;strong&gt;重写方法&lt;/strong&gt; 来实现代理，它使用 ASM（Java 字节码操纵框架）在运行时动态生成子类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心机制：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;字节码增强&lt;/strong&gt;：CGLIB 通过 ASM 动态修改字节码，在运行时创建一个子类。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方法拦截&lt;/strong&gt;：代理类重写目标类的方法，并在调用前后插入拦截逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FastClass 机制&lt;/strong&gt;（优化调用速度）：CGLIB 生成的代理类使用 &lt;strong&gt;索引表&lt;/strong&gt; 来加快方法调用，而不是通过反射调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;引入依赖，如果是 &lt;strong&gt;Spring Boot&lt;/strong&gt; 项目，CGLIB 已经内置，不需要额外引入。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependency&gt;
    &amp;#x3C;groupId&gt;cglib&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;cglib&amp;#x3C;/artifactId&gt;
    &amp;#x3C;version&gt;3.3.0&amp;#x3C;/version&gt;
&amp;#x3C;/dependency&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;举例：计算方法执行时间的 CGLIB 代理实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class CglibProxy {
    public static UserServiceImpl createProxyUser(UserServiceImpl userService)
    {
        Enhancer enhancer = new Enhancer();  // 工具类
        enhancer.setSuperclass(userService.getClass());  // 设置父类
        enhancer.setCallback(new MethodInterceptor() {  // 回调函数
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                LocalDateTime start = LocalDateTime.now();
                Object rs = method.invoke(userService, objects);
                LocalDateTime end = LocalDateTime.now();
                System.out.println(&quot;耗时：&quot; + (end.getSecond() - start.getSecond()) + &quot;秒&quot;);
                return rs;
            }
        });
        return (UserServiceImpl) enhancer.create();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;cglib代理无需实现接口，通过生成类字节码实现代理，比反射稍快，不存在性能问题，但cglib会继承目标对象，需要重写方法，所以&lt;strong&gt;目标对象不能为final类&lt;/strong&gt;。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?daili"/><enclosure url="http://wallpaper.csun.site/?daili"/></item><item><title>解决 win11 中 Zsh 与 Conda 兼容性问题</title><link>https://blog.csun.site/blog/2025-02-01-fix-win11-zsh-conda-compatibility-issues</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-02-01-fix-win11-zsh-conda-compatibility-issues</guid><description>解决Win11 Zsh与Conda兼容问</description><pubDate>Sat, 01 Feb 2025 10:33:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题描述&lt;/h2&gt;
&lt;p&gt;当在 Windows 11 系统通过 Git Bash 使用 Zsh 时，会出现以下 &lt;code&gt;Conda&lt;/code&gt; 相关异常：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;命令行提示符（prompt）无法显示当前 Conda 环境&lt;/li&gt;
&lt;li&gt;执行 &lt;code&gt;conda activate/deactivate&lt;/code&gt; 命令失效&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;conda init zsh&lt;/code&gt; 初始化后报错：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;(eval):10: parse error near `^M&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;问题根源&lt;/h2&gt;
&lt;p&gt;Windows 与 Unix 系统的换行符差异导致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows 使用 &lt;code&gt;\r\n&lt;/code&gt;（回车+换行）作为换行符&lt;/li&gt;
&lt;li&gt;Unix/Linux/macOS 仅使用 &lt;code&gt;\n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Zsh 将 &lt;code&gt;\r&lt;/code&gt; 解析为 &lt;code&gt;^M&lt;/code&gt; 字符引发语法错误&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;核心解决方案&lt;/h2&gt;
&lt;h3&gt;步骤 1：修改 Conda 初始化配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;打开 &lt;code&gt;~/.zshrc&lt;/code&gt; 配置文件&lt;/li&gt;
&lt;li&gt;定位由 &lt;code&gt;conda init zsh&lt;/code&gt; 生成的配置块（通常标记为 &lt;code&gt;# &gt;&gt;&gt; conda initialize &gt;&gt;&gt;&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;替换为以下优化后的配置：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;# &gt;&gt;&gt; conda initialize &gt;&gt;&gt;
# !! Contents within this block are managed by &apos;conda init&apos; !!
eval &quot;$(&apos;/c/path/to/miniconda3/Scripts/conda.exe&apos; &apos;shell.zsh&apos; &apos;hook&apos; | sed -e &apos;s/&quot;$CONDA_EXE&quot; $_CE_M $_CE_CONDA &quot;$@&quot;/&quot;$CONDA_EXE&quot; $_CE_M $_CE_CONDA &quot;$@&quot; | tr -d \x27\\r\x27/g&apos;)&quot;
# &amp;#x3C;&amp;#x3C;&amp;#x3C; conda initialize &amp;#x3C;&amp;#x3C;&amp;#x3C;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sed&lt;/code&gt; 流编辑器处理 Conda 生成的 hook 代码&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s/.../.../g&lt;/code&gt; 正则表达式替换：
&lt;ul&gt;
&lt;li&gt;在原有命令后追加 &lt;code&gt;| tr -d &apos;\r&apos;&lt;/code&gt; 管道操作&lt;/li&gt;
&lt;li&gt;移除 Windows 换行符 &lt;code&gt;\r&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\x27&lt;/code&gt; 表示单引号的十六进制转义，避免 shell 解析错误&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;步骤 2：应用配置变更&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编码问题解决方案&lt;/h2&gt;
&lt;p&gt;应用上述修改后可能出现的编码错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;UnicodeEncodeError: &apos;gbk&apos; codec can&apos;t encode character &apos;\u279c&apos;...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;打开 Windows 系统环境变量设置&lt;/li&gt;
&lt;li&gt;新建系统变量：
&lt;ul&gt;
&lt;li&gt;变量名：&lt;code&gt;PYTHONUTF8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;变量值：&lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;重启终端会话&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/02/b1355f8872428f94a9d41f746edbc94d.png&quot; alt=&quot;环境变量设置示意图&quot;&gt;&lt;/p&gt;
&lt;h2&gt;最终效果&lt;/h2&gt;
&lt;p&gt;成功配置后，Zsh 终端将具备：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正常显示 Conda 环境状态&lt;/li&gt;
&lt;li&gt;支持完整的 Conda 命令操作&lt;/li&gt;
&lt;li&gt;无报错的 UTF-8 编码支持&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/02/8ff1b83b16af769a91bcb4af4add4077.png&quot; alt=&quot;美化后的 Zsh 终端示例&quot;&gt;&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;路径 &lt;code&gt;/c/path/to/miniconda3/&lt;/code&gt; 需替换为实际的 Conda 安装路径&lt;/li&gt;
&lt;li&gt;建议使用 VSCode 或 Notepad++ 等支持换行符转换的编辑器修改配置文件&lt;/li&gt;
&lt;li&gt;若使用其他 shell 主题，可能需要额外配置环境提示符&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;本文解决方案参考自 &lt;a href=&quot;https://github.com/conda/conda/issues/9922#issuecomment-1361695031&quot;&gt;Conda GitHub Issue #9922&lt;/a&gt;，经实践验证适用于 Windows 11 (22H2) + Git Bash (2.41.0) + Zsh (5.9) 环境组合。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?zsh"/><enclosure url="http://wallpaper.csun.site/?zsh"/></item><item><title>MySQL 三层 B+ 树能存多少数据</title><link>https://blog.csun.site/blog/2025-01-29-mysql-b-store-data</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-01-29-mysql-b-store-data</guid><description>估算 MySQL 三层 B+ 树能存多少数据</description><pubDate>Wed, 29 Jan 2025 23:08:00 GMT</pubDate><content:encoded>&lt;p&gt;在 InnoDB 存储引擎中，默认页大小是 &lt;strong&gt;16KB&lt;/strong&gt;。对于&lt;strong&gt;聚簇索引&lt;/strong&gt;来说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;非叶子节点&lt;/strong&gt;：存放「主键值 + 子节点指针」&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;叶子节点&lt;/strong&gt;：存放完整的数据行（实际记录）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们通过一个简单的估算，看看三层 B+ 树大概能存多少数据。&lt;/p&gt;
&lt;h2&gt;非叶子节点能存多少索引项？&lt;/h2&gt;
&lt;p&gt;假设：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主键类型为 &lt;code&gt;BIGINT&lt;/code&gt;：占 &lt;strong&gt;8 字节&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;子节点指针占 &lt;strong&gt;6 字节&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;每个索引项大小：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么一个非叶子节点能存的索引项数量为&lt;/p&gt;
&lt;p&gt;$$16 \times 1024 \div (8 + 6) \approx  1170$$ 个&lt;/p&gt;
&lt;h2&gt;叶子节点能存多少行数据？&lt;/h2&gt;
&lt;p&gt;假设每条数据记录占 1KB，则一个叶子节点能存放的记录数量为：&lt;/p&gt;
&lt;p&gt;$$16 \div 1 = 16 $$ 条&lt;/p&gt;
&lt;h2&gt;三层 B+ 树能存多少数据？&lt;/h2&gt;
&lt;p&gt;三层 B+ 树的结构为&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2026/02/857fcd2860b2e820e3cca6c669513be9.png&quot; alt=&quot;image-20260219201040249&quot;&gt;&lt;/p&gt;
&lt;p&gt;总记录数为&lt;/p&gt;
&lt;p&gt;$$ 1170 \times 1170 \times 16 \approx 2190 $$ 万条&lt;/p&gt;
&lt;p&gt;所以三层 B+ 树大约能存&lt;strong&gt;两千多万条数据&lt;/strong&gt;&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?B+"/><enclosure url="http://wallpaper.csun.site/?B+"/></item><item><title>三斤周刊-第 2 期</title><link>https://blog.csun.site/blog/2025-01-19-san-jin-zhou-kan-di-2-qi</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-01-19-san-jin-zhou-kan-di-2-qi</guid><description>每周推荐实用资源</description><pubDate>Sun, 19 Jan 2025 11:14:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;每周为您推荐最新、最有趣的项目、工具、网站和资源，在快节奏的生活中发现实用的宝藏资源！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;项目&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/imanoop7/Ollama-OCR&quot;&gt;Ollama-OCR&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;一个强大的 OCR 包，通过 Ollama 使用最先进的视觉语言模型从图像中提取文本。既可用作 Python 包，也可用作 Streamlit 网络应用程序。&lt;/p&gt;
&lt;p&gt;支持多种视觉模型，多种输出格式以及批量处理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/0528b35f951be1ab660118785b868a10.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/saveweb/review-2024&quot;&gt;saveweb/review-2024: 你在期待什么？你在失望什么？&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;大家的 2024 年度总结。可以看看别人上一年都在做什么，从什么方向去总结，或许可以给自己的生活找找新方向。如果你还没有写，现在动笔还不晚，这真的很值得。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/720fd05cd1415223b842a64d867782bf.png&quot; alt=&quot;image-20250119104116906&quot;&gt;&lt;/p&gt;
&lt;h2&gt;工具&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/sun-i/pot-app-recognize-plugin-qwen-ocr&quot;&gt;📋Qwen OCR Pot 插件&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;基于 &lt;a href=&quot;https://chat.qwenlm.ai/&quot;&gt;Qwen Chat&lt;/a&gt; 提供的强大 OCR 功能所开发的 &lt;a href=&quot;https://pot-app.com/&quot;&gt;Pot&lt;/a&gt; 插件，能够基准识别公式，输出 Latex 代码。&lt;/p&gt;
&lt;p&gt;完全免费，无本地硬件要求，并且对公式和文字的混合识别作了优化，能够无缝粘贴到 Markdown 编辑器中，完美渲染。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/65deac6f207621f8581391820df1cff7.png&quot; alt=&quot;image-20250119103224339&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://dnd-resume.com/&quot;&gt;在线简历生成工具&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;一个模块化的在线简历生成工具，可通过拖拽模块的方式快速生成简历，支持静态部署。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/0720b2d875e2c091028719b150019e45.png&quot; alt=&quot;image-20250119102313929&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/web-infra-dev/midscene&quot;&gt;Midscene.js：浏览器自动化利器&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;由自然语言驱动（需要自行&lt;a href=&quot;https://midscenejs.com/zh/model-provider.html&quot;&gt;接入 AI 模型&lt;/a&gt;）的浏览器自动化利器。&lt;/p&gt;
&lt;p&gt;通过编写 &lt;a href=&quot;https://midscenejs.com/zh/automate-with-scripts-in-yaml.html&quot;&gt;YAML 脚本&lt;/a&gt;，操作浏览器，理解网页内容，将结果以 JSON 数据返回，自动化脚本变得容易维护、效果更稳定。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/8bca3fda28e41ec6750bf6202c8ce0b2.webp&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h2&gt;网站&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://raphael.app/zh&quot;&gt;Raphael - 免费无限制 AI 图像生成器&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;100% 免费，由 Flux.1 Dev 提供支持，无需登录，无限生成。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/19e2a8fdec082d7512349401de8d06f0.png&quot; alt=&quot;image-20250119101952310&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://orangesai.com/catemoji/&quot;&gt;猫猫表情包生成器&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;输入文字描述，AI帮你生成可爱的猫猫表情包～&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/79b7b7d9cfc2bfd455f3e000dd256c5a.png&quot; alt=&quot;image-20250119102633677&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://orangesai.com/chinesename/&quot;&gt;Chinese Name Generator&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;中文名生成器，帮助涌入小红书的 Tiktok 难民取一个中文名吧！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/35791f3c3c32cc3ce60bed84ff74c2fa.png&quot; alt=&quot;image-20250119102749631&quot;&gt;&lt;/p&gt;
&lt;h2&gt;资源&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://aibydoing.com/&quot;&gt;动手实战人工智能 AI By Doing — 动手实战人工智能 AI By Doing&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;动手实战人工智能系列实验教程，从监督学习开始，带你入门机器学习和深度学习。尝试剖析和推导每一个基础算法的原理，将数学过程写出来，同时基于 Python 代码对公式进行实现，做到公式和代码的一一对应。也会利用主流的开源框架重复同样的过程，帮助读者看出手动实现和主流框架实现之间的区别。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.aibydoing.com/aibydoing/covers/aibydoing-thumbnail.png&quot; alt=&quot;cover&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.lookai.top/cn&quot;&gt;LookAI - 最适合零编程基础普通人的 Cursor AI 编程教程&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;在当下的 AI 时代，Cursor 能够极大地赋能每一个普通人，该网站致力于帮助零基础的普通人认识、学会使用 Cursor，做出自己的产品。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/b3200defb1ca6942fe64ebb5390a5f64.png&quot; alt=&quot;image-20250119101428539&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://superhuang.feishu.cn/wiki/CBBPwvgEuicVhFkx0s7cPmhpn4e&quot;&gt;AI 编程蓝皮书&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;没有技术背景？不会写代码？照样能开发出自己的产品！&lt;/p&gt;
&lt;p&gt;✨ 内容亮点：
· 4 个实战案例完整拆解（持续更新）
· 手把手教你用 Windsurf+Coze 开发产品
· 从浏览器插件到小程序，从网页到公众号，全方位覆盖
· 获得智谱 AI 官方认证，可享 6 个月无限量 API 额度支持&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/4c9e350354dc61b688b5a1bf10e6cdc4.png&quot; alt=&quot;image-20250119101626905&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?sanjin2"/><enclosure url="http://wallpaper.csun.site/?sanjin2"/></item><item><title>三斤周刊-第 1 期</title><link>https://blog.csun.site/blog/2025-01-10-san-jin-weekly-issue-1</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-01-10-san-jin-weekly-issue-1</guid><description>每周推荐实用项目与工具</description><pubDate>Fri, 10 Jan 2025 11:14:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;每周为您推荐最新、最有趣的项目、工具、网站和资源，在快节奏的生活中发现实用的宝藏资源！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;项目&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/ammaarreshi/Gemini-Search&quot;&gt;Gemini Search - 一个基于 Gemini 的 AI 搜索引擎&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;一个由 Google 的 Gemini 2.0 Flash 模型支持的 Perplexity 风格搜索引擎，通过 Google 搜索进行基础支持。获取由 AI 驱动的答案，包含实时网络来源和引用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/48b1b68a1e279b0aa008d7d4b134fe4d.png&quot; alt=&quot;image-20250110112112278&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/chclt/oh-my-wechat/&quot;&gt;chclt/oh-my-wechat: 微信备份与年度数据报告&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;这是一个为微信设计的备份阅读器，总体上还原了微信，但又有一些新的设计，并提供了微信年度数据报告功能。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/f12160b02c6fb5dbd5c7fa87f6ec9edb.jpg&quot; alt=&quot;399847789-191963f6-e3f7-48e4-85c4-25c723451b8d&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/Byaidu/PDFMathTranslate&quot;&gt;PDFMathTranslate&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;一款开源的 PDF 文档翻译及双语对照工具&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;📊 保留公式、图表、目录和注释&lt;/li&gt;
&lt;li&gt;🌐 支持多种语言和诸多翻译服务&lt;/li&gt;
&lt;li&gt;🤖 提供命令行工具图形交互界面以及容器化部署&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/Byaidu/PDFMathTranslate/raw/main/docs/images/preview.gif&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/slidevjs/slidev&quot;&gt;Slidev&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;一款基于 Markdown 的演示工具，可使用 Vue 组件创建交互式幻灯片，具有实时编码、LaTeX 支持和各种格式导出选项等功能。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/cb324f3adddfdf218c43a1b8cd312e23.png&quot; alt=&quot;image-20250110102627047&quot;&gt;&lt;/p&gt;
&lt;h2&gt;工具&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://faces.notion.com/&quot;&gt;Notion 推出的头像生成工具&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Notion 推出的一款头像生成工具，支持自定义肤色、五官、头发、配色、背景颜色等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/b4b116d2d9bfa9ceaf0125048ffe1a89.png&quot; alt=&quot;image-20250110100329332&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://edgeone.ai/products/pages&quot;&gt;EdgeOne Pages: Fast build and deploy your web app&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;腾讯云国际站推出的免费网页部署工具，不限流量，支持函数与 KV 存储，提供模板一键部署，也支持直接部署 github 项目，无需信用卡手机号&lt;strong&gt;仅需邮箱&lt;/strong&gt;与 &lt;strong&gt;github 账号&lt;/strong&gt;即可免费使用。&lt;/p&gt;
&lt;p&gt;EdgeOne Pages 的 CDN 为腾讯云国际 CDN，国内访问速度比 Vercel、Cloudflare Pages、Github Pages 更快，可作为替代品。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/e1fc974bf625e20581fcf81b77b0ffed.png&quot; alt=&quot;image-20250110102908691&quot;&gt;&lt;/p&gt;
&lt;h2&gt;网站&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://dialect.quotemap.site/game&quot;&gt;方言地图游戏&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;通过收听一段十几秒的音频，在地图上找出是哪里的方言，目前支持中国各大省方言体系。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/4b5e4cb393245a94284666bfa6fb48e3.png&quot; alt=&quot;image-20250110101153293&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://yi.isstudio.cc/&quot;&gt;释易&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;AI 占卜助手，利用周易六爻对要询问的事情进行占卜。&lt;/p&gt;
&lt;p&gt;享赛博算命，看机械飞升！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/a21deca93c8972d3777e2103d6a3d3d7.png&quot; alt=&quot;image-20250110103222054&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://tempmail100.com/zh-cn/&quot;&gt;tempmail100&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;一个无广告且颜值还可以的临时邮箱服务，薅羊毛必备。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/9f04b595834090dce1e6df895b5e8807.png&quot; alt=&quot;image-20250110103654898&quot;&gt;&lt;/p&gt;
&lt;h2&gt;资源&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/xTEluwBU91Hf4fpwG_UF8g&quot;&gt;与 AI 一起编程-面向非工程师的入门指南&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;带你了解 AI 编程工具的三次进化：从 GitHub Copilot 彻底改变代码输入方式，到 Cursor 开创 AI 原生交互新范式，再到 Windsurf 实现命令行闭环。&lt;/p&gt;
&lt;p&gt;同时不止于工具本身的介绍，更分享了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI 编程助手使用秘籍;&lt;/li&gt;
&lt;li&gt;用 AI 赋能你的生活、学习、工作;&lt;/li&gt;
&lt;li&gt;实用案例：文件处理、浏览器插件开发、音视频处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/bd0682212e080572b680dcfdb164873f.png&quot; alt=&quot;image-20250110101955909&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://d.injdk.cn/download&quot;&gt;各种JDK的镜像下载&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;各种版本 JDK 的镜像分发，不用每次下载都去官网注册个账号了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/86c0878696a6f0ccdf99d31845e088cd.png&quot; alt=&quot;image-20250110104234425&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?sanjin1"/><enclosure url="http://wallpaper.csun.site/?sanjin1"/></item><item><title>查找Linux占用内存最多的进程并 kill 包含指定字符串的进程</title><link>https://blog.csun.site/blog/2025-01-08-find-linux-processes-with-high-memory-usage-and-kill-specified-strings-2025-01-08</link><guid isPermaLink="true">https://blog.csun.site/blog/2025-01-08-find-linux-processes-with-high-memory-usage-and-kill-specified-strings-2025-01-08</guid><description>查找并终止指定进程</description><pubDate>Wed, 08 Jan 2025 10:33:00 GMT</pubDate><content:encoded>&lt;p&gt;最近发现在使用下列命令启用单机多卡训练模型时，&lt;code&gt;nohup.pid&lt;/code&gt; 文件只会记录下主进程的 pid。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;python -m torch.distributed.launch xxx.py &gt; nohup.log 2&gt;&amp;#x26;1&amp;#x26; echo $! &gt; nohup.pid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 &lt;code&gt;kill&lt;/code&gt; 掉主进程时，仍然会有如下图所示的子进程残留&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2025/01/488fb0fef159748897455f81f3e16f25.png&quot; alt=&quot;image-20250108104326728&quot;&gt;&lt;/p&gt;
&lt;p&gt;使用下列命令可以查看当前占用内存最多的 10 个进程并得到上图的结果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ps -aux | sort -k4nr | head -10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是对命令的逐步解析：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ps -aux&lt;/code&gt;&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ps&lt;/code&gt;：显示当前系统中运行的进程信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-aux&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;a&lt;/code&gt;：显示所有用户的进程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;u&lt;/code&gt;：以用户友好的格式显示（包括用户名、CPU、内存使用率等）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt;：显示不依赖于终端的进程（如后台进程）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;|&lt;/code&gt;&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;管道符，将前一个命令的输出传递给下一个命令作为输入。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sort -k4nr&lt;/code&gt;&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sort&lt;/code&gt;：对输入内容进行排序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-k4&lt;/code&gt;：按第 4 列进行排序（&lt;code&gt;ps -aux&lt;/code&gt; 的第 4 列通常是 内存 使用率 &lt;code&gt;%MEM&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt;：按数值排序（而不是按字母表顺序）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;r&lt;/code&gt;：按降序排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;head -10&lt;/code&gt;&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;head&lt;/code&gt;：显示前几行内容。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-10&lt;/code&gt;：只显示前 10 行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;观察输出结果可以发现，这些进程的启动命令都包含同样的字符串 &lt;code&gt;from multiprocessing.forkserver import main;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;使用下列命令可以批量 &lt;code&gt;kill&lt;/code&gt; 包含同样字符串的进程&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ps aux | grep &quot;multiprocessing.forkserver import main&quot; | grep -v grep | awk &apos;{print $2}&apos; | xargs kill -9
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ps aux&lt;/code&gt; 显示所有进程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep &quot;multiprocessing.forkserver import main&quot;&lt;/code&gt; 过滤包含目标字符串的行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -v grep&lt;/code&gt; 排除 grep 命令本身&lt;/li&gt;
&lt;li&gt;&lt;code&gt;awk &apos;{print $2}&apos;&lt;/code&gt; 提取进程 ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xargs kill -9&lt;/code&gt; 终止这些进程&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?pid"/><enclosure url="http://wallpaper.csun.site/?pid"/></item><item><title>python 如何生成 requirements.txt 文件</title><link>https://blog.csun.site/blog/2024-12-29-python-generate-requirements-txt-file</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-12-29-python-generate-requirements-txt-file</guid><description>Python 生成精确依赖文件</description><pubDate>Sun, 29 Dec 2024 23:08:00 GMT</pubDate><content:encoded>&lt;h2&gt;方法一&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pip freeze &gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式会将当前虚拟环境中的所有依赖加入到 requirements.txt 文件 中&lt;/p&gt;
&lt;p&gt;可能会存在一些我们项目中没有使用的依赖&lt;/p&gt;
&lt;p&gt;不推荐使用&lt;/p&gt;
&lt;h2&gt;方法二（推荐）&lt;/h2&gt;
&lt;p&gt;推荐使用 &lt;a href=&quot;https://github.com/bndr/pipreqs&quot;&gt;pipreqs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;可以指定我们需要生成 requirements.txt 的项目，只添加项目使用的依赖&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 安装
pip install pipreqs

# 使用
pipreqs /home/project/location
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果出现报错 &lt;code&gt;UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0xae in position 406: illegal multibyte sequence &lt;/code&gt;&lt;/p&gt;
&lt;p&gt;则需要指定项目编码为 &lt;code&gt;utf-8&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pipreqs /home/project/location --encoding=utf8--force
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?pipreg"/><enclosure url="http://wallpaper.csun.site/?pipreg"/></item><item><title>使用适配器模式对接多种 OSS</title><link>https://blog.csun.site/blog/2024-12-22-adapter-pattern</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-12-22-adapter-pattern</guid><description>适配器模式简化 OSS 接入</description><pubDate>Sun, 22 Dec 2024 15:32:00 GMT</pubDate><content:encoded>&lt;p&gt;如果直接将 OSS 的代码写到业务逻辑中，当我们需要切换不同的 OSS 时，就要直接修改业务代码，极为不便。&lt;/p&gt;
&lt;p&gt;此时可以使用适配器模式，为不同的 OSS 创建不同的适配器，切换时只要通过配置文件启用不同的适配器即可。&lt;/p&gt;
&lt;h2&gt;创建 OSS 接口&lt;/h2&gt;
&lt;p&gt;首先创建一个 OSS 的接口，声明一些在业务中会调用的方法，例如上传文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component
public interface OSSAdapter {
    String upload(MultipartFile file);
    
    // 业务中会调用的方法......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个接口会被不同的 OSS 适配器实现&lt;/p&gt;
&lt;p&gt;在实际业务逻辑中，直接通过 &lt;code&gt;OSSAdapter&lt;/code&gt; 调用相关方法即可，更换 OSS 也不用修改实际业务代码。&lt;/p&gt;
&lt;h2&gt;OSS 适配器&lt;/h2&gt;
&lt;p&gt;假设现在有阿里云的 OSS 和 MinIO 的 OSS，分别创建他们的适配器并实现 &lt;code&gt;OSSAdapter&lt;/code&gt; 接口。&lt;/p&gt;
&lt;p&gt;在适配器中，调用对应的 SDK 实现 &lt;code&gt;OSSAdapter&lt;/code&gt; 接口中的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class AliOSSAdapter implements OSSAdapter {
    @Override
    public String upload(MultipartFile file) {
        // 调用阿里云的 SDK 实现该方法
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class MinIOAdapter implements OSSAdapter {

    @Override
    public String upload(MultipartFile file) {
       // 调用 MinIO 的 SDk 实现改方法
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置文件&lt;/h2&gt;
&lt;p&gt;通过在配置文件中设置 &lt;code&gt;type&lt;/code&gt; 的值，可以控制 &lt;code&gt;OSSAdapter&lt;/code&gt; 的具体实现是哪个适配器，从而实现通过配置文件控制启用哪个 OSS&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class OSSAdapterConfig {

    @Value(&quot;${sky.oss.type}&quot;)
    private String type;

    @Bean
    public OSSAdapter ossAdapter() {
        if(type.equals(&quot;alioss&quot;)) {
            return new AliOSSAdapter();
        } else if(type.equals(&quot;minio&quot;))
            return new MinIOAdapter();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过使用适配器模式，当我们需要更换 OSS 时，只需要创建一个 OSS 的适配器实现&lt;code&gt;OSSAdapter&lt;/code&gt; 接口，然后在 &lt;code&gt;OSSAdapterConfig&lt;/code&gt; 配置启用该适配器即可，不用去实际业务代码中修改，避免耦合。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?oss"/><enclosure url="http://wallpaper.csun.site/?oss"/></item><item><title>MySQL 基础</title><link>https://blog.csun.site/blog/2024-10-27-mysql-basics</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-10-27-mysql-basics</guid><description>MySQL基础：DDL与数据库操作</description><pubDate>Sun, 27 Oct 2024 15:32:00 GMT</pubDate><content:encoded>&lt;h2&gt;DDL&lt;/h2&gt;
&lt;p&gt;数据库定义语言，用来定义数据库对象（数据库，表，字段）&lt;/p&gt;
&lt;h3&gt;数据库操作&lt;/h3&gt;
&lt;h4&gt;查询&lt;/h4&gt;
&lt;p&gt;查询所有数据库&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;show databases
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询当前数据库&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select database();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;创建&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;create database [if not exists] 数据库名 [default charset 字符集] [collate 排序规则];
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;删除&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;drop database [if exists] 数据库名;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;use 数据库名;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;表操作&lt;/h3&gt;
&lt;h4&gt;查询&lt;/h4&gt;
&lt;p&gt;查询当前数据库所有表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;show tables;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询表结构&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;desc 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询指定表的建表语句&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;show create table 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;创建&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;CREATE TABLE 表名(
    字段1 字段1类型[COMMENT 字段1注释],
    字段2 字段2类型[COMMENT 字段2注释],
    字段3 字段3类型[COMMENT 字段3注释],
    .......,
    字段n 字段n类型[COMMENT 字段n注释 ]
)[COMMENT 表注释];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：最后一个字段结尾没有逗号&lt;/p&gt;
&lt;h4&gt;数据类型&lt;/h4&gt;
&lt;h5&gt;数值类型&lt;/h5&gt;
&lt;p&gt;| 类型         | 大小    | 有符号 (SIGNED) 范围                                | 无符号 (UNSIGNED) 范围                                  | 描述                |
| ------------ | ------- | --------------------------------------------------- | ------------------------------------------------------- | ------------------- |
| TINYINT      | 1 byte  | (-128, 127)                                         | (0, 255)                                                | 小整数值            |
| SMALLINT     | 2 bytes | (-32768, 32767)                                     | (0, 65535)                                              | 大整数值            |
| MEDIUMINT    | 3 bytes | (-8388608, 8388607)                                 | (0, 16777215)                                           | 大整数值            |
| INT或INTEGER | 4 bytes | (-2147483648, 2147483647)                           | (0, 4294967295)                                         | 大整数值            |
| BIGINT       | 8 bytes | (-2^63, 2^63-1)                                     | (0, 2^64-1)                                             | 极大整数值          |
| FLOAT        | 4 bytes | (-3.402823466E+38, 3.402823466E+38)                 | 0 和 (1.175494351E-38, 3.402823466E+38)                 | 单精度浮点数值      |
| DOUBLE       | 8 bytes | (-1.7976931348623157E+308, 1.7976931348623157E+308) | 0 和 (2.2250738585072014E-308, 1.7976931348623157E+308) | 双精度浮点数值      |
| DECIMAL      |         | 依赖于 M(精度) 和 D(标度) 的值                      | 依赖于 M(精度) 和 D(标度) 的值                          | 小数值 (精确定点数) |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;DECIMAL&lt;/code&gt; 中精度指定是整个数的位数，标度指的是小数的位数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;字符串类型&lt;/h5&gt;
&lt;p&gt;| 类型       | 大小                  | 描述                          |
| ---------- | --------------------- | ----------------------------- |
| CHAR       | 0-255 bytes           | 定长字符串                    |
| VARCHAR    | 0-65535 bytes         | 变长字符串                    |
| TINYBLOB   | 0-255 bytes           | 不超过 255 个字符的二进制数据 |
| TINYTEXT   | 0-255 bytes           | 短文本字符串                  |
| BLOB       | 0-65 535 bytes        | 二进制形式的长文本数据        |
| TEXT       | 0-65 535 bytes        | 长文本数据                    |
| MEDIUMBLOB | 0-16 777 215 bytes    | 二进制形式的中等长度文本数据  |
| MEDIUMTEXT | 0-16 777 215 bytes    | 中等长度文本数据              |
| LONGBLOB   | 0-4 294 967 295 bytes | 二进制形式的极大文本数据      |
| LONGTEXT   | 0-4 294 967 295 bytes | 极大文本数据                  |&lt;/p&gt;
&lt;p&gt;&lt;code&gt;char(10)&lt;/code&gt; 无论存储多长的内容，都会占用 10 个字节的空间，但是性能好&lt;/p&gt;
&lt;p&gt;&lt;code&gt;varchar(10)&lt;/code&gt; 会根据实际存储的内容计算占用空间，但是性能较差&lt;/p&gt;
&lt;h5&gt;日期时间类型&lt;/h5&gt;
&lt;p&gt;| 类型      | 大小 | 范围                                       | 格式                | 描述                     |
| --------- | ---- | ------------------------------------------ | ------------------- | ------------------------ |
| DATE      | 3    | 1000-01-01 至 9999-12-31                   | YYYY-MM-DD          | 日期值                   |
| TIME      | 3    | -838:59:59 至 838:59:59                    | HH:MM:SS            | 时间值或持续时间         |
| YEAR      | 1    | 1901 至 2155                               | YYYY                | 年份值                   |
| DATETIME  | 8    | 1000-01-01 00:00:00 至 9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值         |
| TIMESTAMP | 4    | 1970-01-01 00:00:01 至 2038-01-19 03:14:07 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值，时间戳 |&lt;/p&gt;
&lt;h4&gt;修改&lt;/h4&gt;
&lt;p&gt;添加字段&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;alter table 表名 add 字段名 类型(长度) [comment 注释] [约束];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改数据类型&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;alter table 表名 modify 字段名 新数据类型(长度);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改字段名和字段类型&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;alter table 表名 change 旧字段名 新字段名 类型(长度) [comment 注释] [约束];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除字段&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;alter table 表名 drop 字段名;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改表名&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;alter table 表名 rename to 新表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;删除&lt;/h4&gt;
&lt;p&gt;删除表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;drop table [if exists] 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除指定表, 并重新创建该表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;truncate table 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DML&lt;/h2&gt;
&lt;p&gt;数据操作语言，用来对数据库表种的数据进行增删改&lt;/p&gt;
&lt;h3&gt;添加数据&lt;/h3&gt;
&lt;p&gt;给指定字段添加数据&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;INSERT INTO 表名 (字段名1, 字段名2, ...) VALUES (值1, 值2, ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给全部字段添加数据&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;INSERT INTO 表名 VALUES (值1, 值2, ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;批量添加数据&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;INSERT INTO 表名 VALUES (值1, 值2, ...), (值1, 值2, ...);
INSERT INTO 表名 (字段名1, 字段名2, ...) VALUES (值1, 值2, ...), (值1, 值2, ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入数据时，指定的字段顺序需要与值的顺序一一对应&lt;/li&gt;
&lt;li&gt;字符串和日期类型数据应该包含在引号中&lt;/li&gt;
&lt;li&gt;插入的数据大小应该在字段的规定范围之内&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;修改数据&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;update 表名 set 字段名1 = 值1, 字段名2 = 值2,...[where 条件];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;删除数据&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;delete from 表名 [where 条件];
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DQL&lt;/h2&gt;
&lt;p&gt;数据查询语言，用来查询数据库种表的记录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SELECT 
	字段列表
FROM 
	表名列表
WHERE 
	条件列表
GROUP BY 
	分组字段列表
HAVING 
	分组后条件列表
ORDER BY 
	排序字段列表
LIMIT 
	分页参数
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基本查询&lt;/h3&gt;
&lt;p&gt;查询多个字段&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SELECT * FROM 表名;
SELECT 字段1, 字段2, 字段3 ... FROM 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置别名&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SELECT 字段1 [AS 别名1], 字段2 [AS 别名2] ... FROM 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;去除重复记录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SELECT DISTINCT 字段列表 FROM 表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;条件查询&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select 字段列表 from 表名 where 条件列表;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;条件&lt;/h4&gt;
&lt;p&gt;| 比较运算符          | 功能                                    |
| ------------------- | --------------------------------------- |
| &gt;                   | 大于                                    |
| &gt;=                  | 大于等于                                |
| &amp;#x3C;                   | 小于                                    |
| &amp;#x3C;=                  | 小于等于                                |
| =                   | 等于                                    |
| &amp;#x3C;&gt; 或 !=            | 不等于                                  |
| BETWEEN ... AND ... | 在某个范围之内 (含最小、最大值)         |
| IN(...)             | 在 in 之后的列表中的值，多选一          |
| LIKE 占位符         | 模糊匹配 (_匹配单个字符, %匹配任意字符) |
| IS NULL             | 是 NULL                                 |&lt;/p&gt;
&lt;p&gt;| 逻辑运算符 | 功能                        |
| ---------- | --------------------------- |
| AND 或 &amp;#x26;&amp;#x26;  | 并且 (多个条件同时成立)     |
| OR 或 || | 或者 (多个条件任意一个成立) |
| NOT 或 ！  | 非, 不是                    |&lt;/p&gt;
&lt;h3&gt;聚合函数&lt;/h3&gt;
&lt;p&gt;将一列数据作为一个整体，进行纵向计算&lt;/p&gt;
&lt;p&gt;| 函数  | 功能     |
| ----- | -------- |
| count | 统计数量 |
| max   | 最大值   |
| min   | 最小值   |
| avg   | 平均值   |
| sum   | 求和     |&lt;/p&gt;
&lt;h3&gt;分组查询&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select 字段列表 from 表名 [where 条件] GROUP BY 分组字段名 [HAVING 分组后过滤条件];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;where 和 having 区别&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行时机不同: &lt;code&gt;where&lt;/code&gt; 是分组之前进行过滤, 不满足 &lt;code&gt;where&lt;/code&gt; 条件不参与分组，而 &lt;code&gt;having&lt;/code&gt; 是分组之后对结果进行过滤&lt;/li&gt;
&lt;li&gt;判断条件不同：&lt;code&gt;where&lt;/code&gt; 不能对聚合函数进行判断，而 &lt;code&gt;having&lt;/code&gt; 可以&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select 字段列表 from 表名 order by 字段1 排序方式1, 字段2 排序方式2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;排序方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ASC&lt;/code&gt;: 升序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DESC&lt;/code&gt;: 降序&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;分页查询&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select * from 表名 limit 起始索引, 查询记录数;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;起始索引从 0 开始，起始索引 = (查询页码 - 1) * 每页显示记录数&lt;/li&gt;
&lt;li&gt;如果查询第一页数据，起始索引可以省略，直接简写为 &lt;code&gt;limit 10&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;执行顺序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;from 表名列表 where 条件列表 group 分组字段列表 having 分组后条件列表 select 字段列表 order by 排序字段 limit 分页参数
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DCL&lt;/h2&gt;
&lt;p&gt;数据控制语言，用来创建数据库用户、控制数据库的访问权限&lt;/p&gt;
&lt;h3&gt;管理用户&lt;/h3&gt;
&lt;p&gt;查询用户&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;USE mysql;
SELECT * FROM user;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建用户&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;CREATE USER &apos;用户名&apos;@&apos;主机名&apos; IDENTIFIED BY &apos;密码&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改用户密码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;ALTER USER &apos;用户名&apos;@&apos;主机名&apos; IDENTIFIED WITH mysql_native_password BY &apos;新密码&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除用户&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;DROP USER &apos;用户名&apos;@&apos;主机名&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;权限控制&lt;/h3&gt;
&lt;p&gt;查询权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SHOW GRANTS FOR &apos;用户名&apos;@&apos;主机名&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;授予权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;GRANT 权限列表 ON 数据库名.表名 TO &apos;用户名&apos;@&apos;主机名&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;撤销权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;REVOKE 权限列表 ON 数据库名.表名 FROM &apos;用户名&apos;@&apos;主机名&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;函数&lt;/h2&gt;
&lt;h3&gt;字符串函数&lt;/h3&gt;
&lt;p&gt;| 函数                     | 功能                                                      |
| :----------------------- | :-------------------------------------------------------- |
| CONCAT(S1,S2,...Sn)      | 字符串拼接，将S1，S2，... Sn拼接成一个字符串              |
| LOWER(str)               | 将字符串全部转为小写                                      |
| UPPER(str)               | 将字符串全部转为大写                                      |
| LPAD(str,n,pad)          | 左填充，用字符串pad对str的左边进行填充，达到n个字符串长度 |
| RPAD(str,n,pad)          | 右填充，用字符串pad对str的右边进行填充，达到n个字符串长度 |
| TRIM(str)                | 去掉字符串头部和尾部的空格                                |
| SUBSTRING(str,start,len) | 返回从字符串str从start位置起的len个长度的字符串           |&lt;/p&gt;
&lt;h3&gt;数值函数&lt;/h3&gt;
&lt;p&gt;| 函数       | 功能                                   |
| ---------- | -------------------------------------- |
| CEIL(x)    | 向上取整                               |
| FLOOR(x)   | 向下取整                               |
| MOD(x,y)   | 返回 x/y 的模                          |
| RAND()     | 返回 0~1 内的随机数                    |
| ROUND(x,y) | 求参数 x 的四舍五入的值，保留 y 位小数 |&lt;/p&gt;
&lt;h3&gt;日期函数&lt;/h3&gt;
&lt;p&gt;| 函数                               | 功能                                                |
| ---------------------------------- | --------------------------------------------------- |
| CURDATE()                          | 返回当前日期                                        |
| CURTIME()                          | 返回当前时间                                        |
| NOW()                              | 返回当前日期和时间                                  |
| YEAR(date)                         | 获取指定 date 的年份                                |
| MONTH(date)                        | 获取指定 date 的月份                                |
| DAY(date)                          | 获取指定 date 的日期                                |
| DATE_ADD(date, INTERVAL expr type) | 返回一个日期/时间值加上一个时间间隔 expr 后的时间值 |
| DATEDIFF(date1, date2)             | 返回起始时间 date1 和结束时间 date2 之间的天数      |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select date_add(now(), INTERVAL 70 DAY);  # 当前时间往后 70 天
select date_add(now(), INTERVAL 70 MONTH);  # 当前时间往后 70 月
select date_add(now(), INTERVAL 70 YEAR);  # 当前时间往后 70 年
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;流程控制函数&lt;/h3&gt;
&lt;p&gt;| 函数                                                       | 功能                                                         |
| ---------------------------------------------------------- | ------------------------------------------------------------ |
| IF(value, t, f)                                            | 如果 value 为 true，则返回 t，否则返回 f                     |
| IFNULL(value1, value2)                                     | 如果 value1 不为空，返回 value1，否则返回 value2             |
| CASE WHEN [val1] THEN [res1] ... ELSE [default] END        | 如果 val1 为 true，返回 res1，... 否则返回 default 默认值    |
| CASE [expr] WHEN [val1] THEN [res1] ... ELSE [default] END | 如果 expr 的值等于 val1，返回 res1，... 否则返回 default 默认值 |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;-- 查询学生的成绩，&gt;=85 展示优秀，&gt;=60 展示及格，否则展示不及格
select name, (case when score &gt;= 85 then &apos;优秀&apos; when score &gt;= 60 then &apos;及格&apos; else &apos;不及格&apos; end) as grade from students;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;约束&lt;/h2&gt;
&lt;p&gt;约束是作用于表中字段上的规则，用于限制存储在表中的数据，保证数据库中数据的正确性、有效性和完整性&lt;/p&gt;
&lt;h3&gt;约束的分类&lt;/h3&gt;
&lt;p&gt;| 约束                       | 描述                                                     | 关键字      |
| -------------------------- | -------------------------------------------------------- | ----------- |
| 非空约束                   | 限制该字段的数据不能为 null                              | NOT NULL    |
| 唯一约束                   | 保证该字段的所有数据都是唯一、不重复的                   | UNIQUE      |
| 主键约束                   | 主键是一行数据的唯一标识，要求非空且唯一                 | PRIMARY KEY |
| 默认约束                   | 保存数据时，如果未指定该字段的值，则采用默认值           | DEFAULT     |
| 检查约束 (8.0.16 版本之后) | 保证字段值满足某一个条件                                 | CHECK       |
| 外键约束                   | 用来让两张表的数据之间建立连接，保证数据的一致性和完整性 | FOREIGN KEY |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;create table user(
    id int primary key auto_increment comment &apos;主键&apos;,  -- 主键并且自动增长
    name varchar(10) not null unique comment &apos;姓名&apos;,  -- 非空且唯一
    age int check (age &gt; 0 &amp;#x26;&amp;#x26; age &amp;#x3C;= 120) comment &apos;年龄&apos;, -- 大于 0 小于 120
    status char(1) default &apos;1&apos; comment &apos;状态&apos;  -- 默认值为 1
) comment &apos;用户表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;外键约束&lt;/h3&gt;
&lt;p&gt;外键用来让两张表的数据之间建立连接，从而保证数据的一致性和完整性&lt;/p&gt;
&lt;h4&gt;添加外键&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE 表名 (
    字段名 数据类型,
    ...
    [CONSTRAINT] [外键名称] FOREIGN KEY (外键字段名) REFERENCES 主表(主表列名)
);

ALTER TABLE 表名 ADD CONSTRAINT 外键名称 FOREIGN KEY (外键字段名) REFERENCES 主表(主表列名);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;删除外键&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;ALTER TABLE 表名 DROP FOREIGN KEY 外键名称;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;删除/更新行为&lt;/h4&gt;
&lt;p&gt;| 行为        | 说明                                                         |
| ----------- | ------------------------------------------------------------ |
| NO ACTION   | 当在父表中删除/更新对应记录时，首先检查该记录是否有对应外键，如果有则不允许删除/更新。（与 RESTRICT 一致） |
| RESTRICT    | 当在父表中删除/更新对应记录时，首先检查该记录是否有对应外键，如果有则不允许删除/更新。（与 NO ACTION 一致） |
| CASCADE     | 当在父表中删除/更新对应记录时，如果有，则删除/更新外键在子表中的记录。 |
| SET NULL    | 当在父表中删除对应记录时，首先检查该记录是否有外键，如果有则设置子表中该外键值为 null（这就要求该外键允许取 null）。 |
| SET DEFAULT | 父表有变更时，子表将外键列设置成一个默认的值（InnoDB 不支持）。 |&lt;/p&gt;
&lt;p&gt;前两种是默认行为&lt;/p&gt;
&lt;p&gt;指定行为语法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;ALTER TABLE 表名 ADD CONSTRAINT 外键名称 FOREIGN KEY (外键字段名) REFERENCES 主表(主表列名) ON UPDATE CASCADE ON DELETE CASCADE;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多表查询&lt;/h2&gt;
&lt;h3&gt;多表关系&lt;/h3&gt;
&lt;h4&gt;一对多（多对一）&lt;/h4&gt;
&lt;p&gt;例如部门与员工的关系，一个部门对应多个员工，一个员工对应一个部门&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现&lt;/strong&gt;：在多的一方建立外键，指向一的一方的主键&lt;/p&gt;
&lt;h4&gt;多对多&lt;/h4&gt;
&lt;p&gt;例如学生与课程的关系，一个学生可以选修多门课程，一门课程也可以供多个学生选择&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现&lt;/strong&gt;：建立第三张中间表，中间表至少包含两个外键，分别关联两方主键&lt;/p&gt;
&lt;h4&gt;一对一&lt;/h4&gt;
&lt;p&gt;一对一关系多用于&lt;strong&gt;单表拆分&lt;/strong&gt;，将一张表的基础字段放在一张表中，其他详情字段放在另一张表中，以提升操作效率&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现&lt;/strong&gt;：在任意一方加入外键，关联另外一方的主键，并设置外键唯一&lt;/p&gt;
&lt;h3&gt;内连接&lt;/h3&gt;
&lt;p&gt;内连接查询的是两张表的&lt;strong&gt;交集部分&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;隐式内连接&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select 字段 from 表1, 表2 where 条件...;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;显式内连接&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select 字段列表 from 表1 [inner] join 表2 on 连接条件...;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;外连接&lt;/h3&gt;
&lt;h4&gt;左外连接&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select 字段 from 表1 left [outer] join 表2 on 条件; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相当于查询&lt;strong&gt;左表(表 1)的所有数据和两张表的交集数据&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;右外连接&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select 字段 from 表1 right [outer] join 表2 on 条件; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相当于查询&lt;strong&gt;右表(表 2)的所有数据和两张表的交集数据&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;自连接&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select 字段 from 表A 别名A join 表A 别名B on 条件；
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自连接可以是内连接也可以是外连接&lt;/p&gt;
&lt;h3&gt;联合查询&lt;/h3&gt;
&lt;p&gt;把多次查询的结果合并起来，形成一个新的查询结果集&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select 字段 from 表A
UNION [ALL]
select 字段 from 表B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UNION ALL&lt;/code&gt; 会直接对查询的结果进行合并, 而 &lt;code&gt;UNION&lt;/code&gt; 会先进行去重再合并&lt;/p&gt;
&lt;h3&gt;子查询&lt;/h3&gt;
&lt;p&gt;SQL 语句中嵌套 select 语句，成为嵌套查询，又称子查询&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select * from t1 where column1 = (select column1 from t2);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;标量子查询&lt;/h4&gt;
&lt;p&gt;子查询返回的结果是单个值(数字、字符串、日期等)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常用的操作符&lt;/strong&gt;: &lt;code&gt;=&lt;/code&gt; &lt;code&gt;&amp;#x3C;&lt;/code&gt; &lt;code&gt;&gt;&lt;/code&gt; &lt;code&gt;&gt;=&lt;/code&gt; &lt;code&gt;&amp;#x3C;=&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;列子查询&lt;/h4&gt;
&lt;p&gt;子查询返回结果是 1 列，可以是多行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常用操作符&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;| 操作符 | 描述                                        |
| ------ | ------------------------------------------- |
| IN     | 在指定的集合范围之内，多选一                |
| NOT IN | 不在指定的集合范围之内                      |
| ANY    | 子查询返回列表中，有任意一个满足即可        |
| SOME   | 与 ANY 等同，使用 SOME 的地方都可以使用 ANY |
| ALL    | 子查询返回列表的所有值都必须满足            |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;-- ALL 使用案例：查询比财务部所有人工资都高的员工信息
SELECT * FROM emp 
WHERE salary &gt; all (
    SELECT salary 
    FROM emp 
    WHERE dept_id = (
        SELECT id 
        FROM dept 
        WHERE name = &apos;财务部&apos;
    )
);

-- ANY 使用案例：查询比财务部任意一个人工资高的员工信息
SELECT * FROM emp 
WHERE salary &gt; any (
    SELECT salary 
    FROM emp 
    WHERE dept_id = (
        SELECT id 
        FROM dept 
        WHERE name = &apos;财务部&apos;
    )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;行子查询&lt;/h4&gt;
&lt;p&gt;子查询返回结果是 1 行，可以是多列&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常用操作符&lt;/strong&gt;：&lt;code&gt;=&lt;/code&gt; &lt;code&gt;&amp;#x3C;&lt;/code&gt; &lt;code&gt;&gt;&lt;/code&gt; &lt;code&gt;IN&lt;/code&gt; &lt;code&gt;NOT IN&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;-- 案例：查询与 ‘张三’ 的薪资及直属领导相同的员工信息
SELECT * 
FROM emp 
WHERE (salary, managerid) = (
    SELECT salary, managerid 
    FROM emp 
    WHERE name = &apos;张无忌&apos;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;表子查询&lt;/h4&gt;
&lt;p&gt;子查询返回的结果是多行多列&lt;/p&gt;
&lt;p&gt;一般把返回结果&lt;strong&gt;作为一个新表&lt;/strong&gt;，与其他表进行联合查询&lt;/p&gt;
&lt;h2&gt;事务&lt;/h2&gt;
&lt;p&gt;事务是一组操作的集合，是一个不可分割的工作单位&lt;/p&gt;
&lt;p&gt;事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求，即这些操作&lt;strong&gt;要么同时成功，要么同时失败&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;事务操作&lt;/h3&gt;
&lt;p&gt;查看事务提交方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;select @@autocommit;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MySQL 默认是自动提交事务，即执行完一条 DML 语句后，会自动隐式的提交事务&lt;/p&gt;
&lt;p&gt;设置事务提交方式:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;set @@autocommit=0;  -- 设置手动提交事务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;提交事务：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;commit;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回滚事务：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;rollback;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;事务四大特性（ACID）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原子性&lt;/strong&gt;（&lt;strong&gt;A&lt;/strong&gt;tomicity）：事务是不可分割的最小单元，要么全部成功，要么全部失败；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性&lt;/strong&gt;（&lt;strong&gt;C&lt;/strong&gt;onsistency）：事务完成时，必须使所有的数据都保持一致状态；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隔离性&lt;/strong&gt;（&lt;strong&gt;I&lt;/strong&gt;solation）：数据库系统提供的隔离机制，保证事务在不受外部并发操作影响的独立环境下运行；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久性&lt;/strong&gt;（&lt;strong&gt;D&lt;/strong&gt;urability）：事务一旦提交或回滚，它对数据库中的数据改变就是永久的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;并发事务问题&lt;/h3&gt;
&lt;h4&gt;脏读&lt;/h4&gt;
&lt;p&gt;一个事务读到另外一个事务还没有提交的数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/4af67b63e2024f7d43a4e7e46c1ad29b.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;不可重复读&lt;/h4&gt;
&lt;p&gt;一个事务先后读取同一条记录，但两次读取的数据不同&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/9d665660ed967e3f9495cda912b994cc.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;幻读&lt;/h4&gt;
&lt;p&gt;一个事务按照条件查询数据时，没有对应的数据行，但是在插入数据时，又发现这行数据已经存在了，好像出现了”幻影“&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/e33f979592a3b962d66c405cb40db973.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;事务的隔离级别&lt;/h3&gt;
&lt;p&gt;| 隔离级别               | 脏读 | 不可重复读 | 幻读 |
| ---------------------- | ---- | ---------- | ---- |
| Read uncommitted       | √    | √          | √    |
| Read committed         | ×    | √          | √    |
| Repeatable Read (默认) | ×    | ×          | √    |
| Serializable           | ×    | ×          | ×    |&lt;/p&gt;
&lt;p&gt;查看事务隔离级别&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SELECT @@TRANSACTION_ISOLATION;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置事务隔离级别&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务隔离级别越高，数据越安全，但是性能越低&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?ddl"/><enclosure url="http://wallpaper.csun.site/?ddl"/></item><item><title>如何用本机代理实现服务器代理</title><link>https://blog.csun.site/blog/2024-10-24-how-to-use-local-proxy-for-server-proxy</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-10-24-how-to-use-local-proxy-for-server-proxy</guid><description>本机代理实现服务器代理</description><pubDate>Thu, 24 Oct 2024 15:32:00 GMT</pubDate><content:encoded>&lt;p&gt;在实验室服务器上，我们往往需要使用代理，但是又不能安装代理软件，这个时候我们可以使用本机代理来实现服务器代理，从而曲线救国&lt;/p&gt;
&lt;p&gt;首先，打开本地代理软件的全局模式（以 Mihomo Party 为例）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/327ff7a2a99f340e963e8441f89afb52.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后查看本机代理的端口(一般 clash 内核的代理软件都是走的 &lt;code&gt;7890&lt;/code&gt; 端口)，使用管理员权限打开本地终端设置代理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cmd&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;set http_proxy=http://127.0.0.1:7890
set https_proxy=http://127.0.0.1:7890
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;powershell&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$env:HTTP_PROXY=&quot;http://127.0.0.1:7890&quot;; $env:HTTPS_PROXY=&quot;http://127.0.0.1:7890&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用下列命令连接服务器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ssh -R 2333:127.0.0.1:7890 username@ip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样远程服务器上访问 &lt;code&gt;localhost:2333&lt;/code&gt; 的流量会通过 SSH 隧道被转发到你本地的 &lt;code&gt;127.0.0.1:7890&lt;/code&gt; 端口&lt;/p&gt;
&lt;p&gt;然后在服务器端，我们设置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;export http_proxy=http://127.0.0.1:2333
export https_proxy=http://127.0.0.1:2333
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样会使得服务器的流量全部走 &lt;code&gt;2333&lt;/code&gt; 端口，然后转发到你本地的 &lt;code&gt;127.0.0.1:7890&lt;/code&gt;，即可实现代理功能&lt;/p&gt;
&lt;p&gt;可使用下列命令验证代理是否成功&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;curl cip.cc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/5b8163d2a391d9d998093f9a2d26d1ae.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果没有成功，请检查是否打开了全局模式，端口号是否正确，有没有多打空格等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;ssh -R&lt;/code&gt; 连接后可以使用另外的会话，但是这个会话不能关闭，否则流量不能转发&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每次设置的 &lt;code&gt;export&lt;/code&gt; 只对当前会话生效，新会话要重新设置&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果是 python 程序，需要在代码中添加&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;os.environ[&apos;http_proxy&apos;] = &apos;http://127.0.0.1:2333&apos;
os.environ[&apos;https_proxy&apos;] = &apos;http://127.0.0.1:2333&apos;
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?dl"/><enclosure url="http://wallpaper.csun.site/?dl"/></item><item><title>Redis 入门</title><link>https://blog.csun.site/blog/2024-10-10-redis-introduction</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-10-10-redis-introduction</guid><description>Redis入门与安装指南</description><pubDate>Thu, 10 Oct 2024 21:03:00 GMT</pubDate><content:encoded>&lt;p&gt;Redis是一个基于&lt;strong&gt;内存&lt;/strong&gt;的key-value结构数据库，是互联网技术领域使用最为广泛的&lt;strong&gt;存储中间件&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Linux 安装 Redis&lt;/h2&gt;
&lt;p&gt;以 Ubuntu/Debian 为例，更多系统可以查看 &lt;a href=&quot;https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/&quot;&gt;Redis 官方文档&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo apt-get install lsb-release curl gpg
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
sudo chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg
echo &quot;deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main&quot; | sudo tee /etc/apt/sources.list.d/redis.list
sudo apt-get update
sudo apt-get install redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完以上命令后，Redis 将会自动启动&lt;/p&gt;
&lt;h3&gt;配置远程访问&lt;/h3&gt;
&lt;p&gt;修改 Redis 的配置文件，一般位于 &lt;code&gt;/etc/redis/redis.conf&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;修改 &lt;code&gt;bind&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;打开配置文件，将 &lt;code&gt;bind&lt;/code&gt; 属性从 &lt;code&gt;127.0.0.1&lt;/code&gt; 修改成 &lt;code&gt;0.0.0.0&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# bind 127.0.0.1 
bind 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;关闭保护模式&lt;/h4&gt;
&lt;p&gt;将 &lt;code&gt;protected-mode&lt;/code&gt; 属性从 &lt;code&gt;yes&lt;/code&gt; 修改成 &lt;code&gt;no&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected-mode no 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;放通端口&lt;/h4&gt;
&lt;p&gt;Redis 默认运行在服务器的 &lt;code&gt;6379&lt;/code&gt; 端口，需要在服务器的安全组中放通这个端口&lt;/p&gt;
&lt;p&gt;完成上述操作后，重启 Redis 服务&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo systemctl restart redis
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果上述这些操作都完成后远程仍然连接不上 Redis&lt;/p&gt;
&lt;p&gt;请尝试更换 Redis 的端口&lt;/p&gt;
&lt;p&gt;玄学问题，不知原因，但博主亲测有效&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Redis 数据类型&lt;/h2&gt;
&lt;p&gt;Redis存储的是 &lt;strong&gt;key-value 结构&lt;/strong&gt;的数据，其中 key 是字符串类型，value 有 5 种常用的数据类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字符串 string：普通字符串，Redis 中最简单的数据类型&lt;/li&gt;
&lt;li&gt;哈希 hash：也叫散列，类似于 Java 中的 HashMap 结构&lt;/li&gt;
&lt;li&gt;列表 list：按照插入顺序排序，可以有重复元素，类似于 Java 中的 LinkedList&lt;/li&gt;
&lt;li&gt;集合 set：无序集合，没有重复元素，类似于 Java 中的 HashSet&lt;/li&gt;
&lt;li&gt;有序集合 sorted set / zset：集合中每个元素关联一个分数 (score)，根据分数升序排序，没有重复元素&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/fd5217a6f951068adc2a9cd56e46f9be.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Redis 常用命令&lt;/h2&gt;
&lt;h3&gt;字符串&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SET [key] [value]&lt;/code&gt;  设置指定key的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET [key]&lt;/code&gt;  获取指定key的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SETEX [key] [seconds] [value]&lt;/code&gt;  设置指定key的值，并将 key 的过期时间设为 seconds 秒&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SETNX [key] [value]&lt;/code&gt; 只有在 key 不存在时设置 key 的值&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;哈希&lt;/h3&gt;
&lt;p&gt;Redis hash 是一个 string 类型的 field 和 value 的映射表，&lt;strong&gt;hash 特别适合用于存储对象&lt;/strong&gt;，常用命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HSET [key] [field] [value]&lt;/code&gt;  将哈希表 key 中的字段 field 的值设为 value&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HGET [key] [field]&lt;/code&gt;  获取存储在哈希表中指定字段的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HDEL [key] [field]&lt;/code&gt;  删除存储在哈希表中的指定字段&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HKEYS [key]&lt;/code&gt;  获取哈希表中所有字段&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HVALS [key]&lt;/code&gt;  获取哈希表中所有值&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;列表&lt;/h3&gt;
&lt;p&gt;Redis 列表是简单的字符串列表，按照插入顺序排序，常用命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LPUSH [key] [value1] [value2]&lt;/code&gt; 将一个或多个值插入到列表头部&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LRANGE [key] [start] [stop]&lt;/code&gt;   获取列表指定范围内的元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RPOP [key]&lt;/code&gt;  移除并获取列表最后一个元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LLEN [key]&lt;/code&gt;  获取列表长度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BRPOP [key1] [key2] timeout&lt;/code&gt;  移出并获取列表的最后一个元素，如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;集合&lt;/h3&gt;
&lt;p&gt;Redis set 是 String 类型的&lt;strong&gt;无序集合&lt;/strong&gt;。集合成员是&lt;strong&gt;唯一&lt;/strong&gt;的，常用命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SADD [key] [member1] [member2]&lt;/code&gt;  向集合添加一个或多个成员&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SMEMBERS [key]&lt;/code&gt;  返回集合中的所有成员&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SCARD [key]&lt;/code&gt;   获取集合的成员数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SINTER [key1] [key2]&lt;/code&gt;  返回给定所有集合的交集&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUNION [key1] [key2]&lt;/code&gt;  返回所有给定集合的并集&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SREM [key] [member1] [member2]&lt;/code&gt;  移除集合中一个或多个成员&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;有序集合&lt;/h3&gt;
&lt;p&gt;Redis 有序集合是 String 类型元素的集合，且不允许有重复成员。每个元素都会&lt;strong&gt;关联一个 double 类型的分数&lt;/strong&gt;。常用命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ZADD [key] [score1 member1] [score2 member2]&lt;/code&gt;  向有序集合添加一个或多个成员&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ZRANGE [key] [start] [stop] [WITHSCORES]&lt;/code&gt;  通过索引区间返回有序集合中指定区间内的成员&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ZINCRBY [key] [increment] [member]&lt;/code&gt;  有序集合中对指定成员的分数加上增量 increment&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ZREM [key] [member] [member ...]&lt;/code&gt;  移除有序集合中的一个或多个成员&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;通用命令&lt;/h3&gt;
&lt;p&gt;Redis的通用命令是不分数据类型的，都可以使用的命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KEYS pattern&lt;/code&gt; 查找所有符合给定模式 (pattern) 的 key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EXISTS key&lt;/code&gt; 查给定 key 是否存在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TYPE key&lt;/code&gt; 返回 key 所储存的值的类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DEL key&lt;/code&gt; 该命令用于在 key 存在时删除 key&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;在 Java 中操作 Redis&lt;/h2&gt;
&lt;p&gt;Spring 对 Redis 客户端进行了整合，提供了 Spring Data Redis，在Spring Boot项目中还提供了对应的 Starter，即 spring-boot-starter-data-redis 用于操作 Redis&lt;/p&gt;
&lt;h3&gt;环境配置&lt;/h3&gt;
&lt;h4&gt;在 pom 文件中导入Spring Data Redis&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;dependency&gt;
     &amp;#x3C;groupId&gt;org.springframework.boot&amp;#x3C;/groupId&gt;
     &amp;#x3C;artifactId&gt;spring-boot-starter-data-redis&amp;#x3C;/artifactId&gt;
&amp;#x3C;/dependency&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;配置 Redis 数据源&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  redis:
    host: localhost 
    port: 6379
    database: 10 # 指定使用Redis的哪个数据库，Redis服务启动后默认有16个数据库，编号分别是从0到15
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编写配置类&lt;/h3&gt;
&lt;p&gt;Spring Data Redis中提供了一个高度封装的类：&lt;strong&gt;RedisTemplate&lt;/strong&gt;，对相关 api 进行了归类封装,将同一类型操作封装为 operation 接口，具体分类如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ValueOperations&lt;/code&gt;：string 数据操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SetOperations&lt;/code&gt;：set 类型数据操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ZSetOperations&lt;/code&gt;：zset 类型数据操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HashOperations&lt;/code&gt;：hash 类型的数据操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ListOperations&lt;/code&gt;：list 类型的数据操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是这个类默认的 key 序列化器为 &lt;code&gt;JdkSerializationRedisSerializer&lt;/code&gt;，导致我们存到 Redis 中后的数据和原始数据有差别&lt;/p&gt;
&lt;p&gt;故我们手动编写一个配置类，设置为 &lt;code&gt;StringRedisSerializer&lt;/code&gt; 序列化器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info(&quot;开始创建redis模板对象...&quot;);
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;操作常见类型数据&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;//string数据操作
ValueOperations valueOperations = redisTemplate.opsForValue();
//hash类型的数据操作
HashOperations hashOperations = redisTemplate.opsForHash();
//list类型的数据操作
ListOperations listOperations = redisTemplate.opsForList();
//set类型数据操作
SetOperations setOperations = redisTemplate.opsForSet();
//zset类型数据操作
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;字符串&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;	/**
     * 操作字符串类型的数据
     */
    @Test
    public void testString(){
        // set get setex setnx
        redisTemplate.opsForValue().set(&quot;name&quot;,&quot;小明&quot;);
        String city = (String) redisTemplate.opsForValue().get(&quot;name&quot;);
        System.out.println(city);
        redisTemplate.opsForValue().set(&quot;code&quot;,&quot;1234&quot;,3, TimeUnit.MINUTES);
        redisTemplate.opsForValue().setIfAbsent(&quot;lock&quot;,&quot;1&quot;);
        redisTemplate.opsForValue().setIfAbsent(&quot;lock&quot;,&quot;2&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;哈希&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;	/**
     * 操作哈希类型的数据
     */
    @Test
    public void testHash(){
        //hset hget hdel hkeys hvals
        HashOperations hashOperations = redisTemplate.opsForHash();

        hashOperations.put(&quot;100&quot;,&quot;name&quot;,&quot;tom&quot;);
        hashOperations.put(&quot;100&quot;,&quot;age&quot;,&quot;20&quot;);

        String name = (String) hashOperations.get(&quot;100&quot;, &quot;name&quot;);
        System.out.println(name);

        Set keys = hashOperations.keys(&quot;100&quot;);
        System.out.println(keys);

        List values = hashOperations.values(&quot;100&quot;);
        System.out.println(values);

        hashOperations.delete(&quot;100&quot;,&quot;age&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;列表&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;	/**
     * 操作列表类型的数据
     */
    @Test
    public void testList(){
        //lpush lrange rpop llen
        ListOperations listOperations = redisTemplate.opsForList();

        listOperations.leftPushAll(&quot;mylist&quot;,&quot;a&quot;,&quot;b&quot;,&quot;c&quot;);
        listOperations.leftPush(&quot;mylist&quot;,&quot;d&quot;);

        List mylist = listOperations.range(&quot;mylist&quot;, 0, -1);
        System.out.println(mylist);

        listOperations.rightPop(&quot;mylist&quot;);

        Long size = listOperations.size(&quot;mylist&quot;);
        System.out.println(size);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;集合&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;	/**
     * 操作集合类型的数据
     */
    @Test
    public void testSet(){
        //sadd smembers scard sinter sunion srem
        SetOperations setOperations = redisTemplate.opsForSet();

        setOperations.add(&quot;set1&quot;,&quot;a&quot;,&quot;b&quot;,&quot;c&quot;,&quot;d&quot;);
        setOperations.add(&quot;set2&quot;,&quot;a&quot;,&quot;b&quot;,&quot;x&quot;,&quot;y&quot;);

        Set members = setOperations.members(&quot;set1&quot;);
        System.out.println(members);

        Long size = setOperations.size(&quot;set1&quot;);
        System.out.println(size);

        Set intersect = setOperations.intersect(&quot;set1&quot;, &quot;set2&quot;);
        System.out.println(intersect);

        Set union = setOperations.union(&quot;set1&quot;, &quot;set2&quot;);
        System.out.println(union);

        setOperations.remove(&quot;set1&quot;,&quot;a&quot;,&quot;b&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;有序集合&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;	/**
     * 操作有序集合类型的数据
     */
    @Test
    public void testZset(){
        //zadd zrange zincrby zrem
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();

        zSetOperations.add(&quot;zset1&quot;,&quot;a&quot;,10);
        zSetOperations.add(&quot;zset1&quot;,&quot;b&quot;,12);
        zSetOperations.add(&quot;zset1&quot;,&quot;c&quot;,9);

        Set zset1 = zSetOperations.range(&quot;zset1&quot;, 0, -1);
        System.out.println(zset1);

        zSetOperations.incrementScore(&quot;zset1&quot;,&quot;c&quot;,10);

        zSetOperations.remove(&quot;zset1&quot;,&quot;a&quot;,&quot;b&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;通用命令&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;	/**
     * 通用命令操作
     */
    @Test
    public void testCommon(){
        //keys exists type del
        Set keys = redisTemplate.keys(&quot;*&quot;);
        System.out.println(keys);

        Boolean name = redisTemplate.hasKey(&quot;name&quot;);
        Boolean set1 = redisTemplate.hasKey(&quot;set1&quot;);

        for (Object key : keys) {
            DataType type = redisTemplate.type(key);
            System.out.println(type.name());
        }

        redisTemplate.delete(&quot;mylist&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?redis"/><enclosure url="http://wallpaper.csun.site/?redis"/></item><item><title>矩形自校准模块 RCM</title><link>https://blog.csun.site/blog/2024-10-07-rectangular-self-calibrating-module-rcm</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-10-07-rectangular-self-calibrating-module-rcm</guid><description>矩形自校准模块概述</description><pubDate>Mon, 07 Oct 2024 19:14:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;论文：&lt;a href=&quot;https://arxiv.org/pdf/2405.06228&quot;&gt;https://arxiv.org/pdf/2405.06228&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;代码：&lt;a href=&quot;https://github.com/nizhenliang/CGRSeg/blob/main/models/decode_heads/rcm.py&quot;&gt;https://github.com/nizhenliang/CGRSeg/blob/main/models/decode_heads/rcm.py&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rectangular Self-Calibration Module (RCM)&lt;/strong&gt; 可以捕捉轴向全局上下文，旨在使模型聚焦前景。&lt;/p&gt;
&lt;p&gt;其由矩形自校准注意力 和 MLP 组成，主要结构如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/3c79f87d7dd3108f7354f76b9d1c7717.png&quot; alt=&quot;image-20241007191926653&quot;&gt;&lt;/p&gt;
&lt;h2&gt;矩形自校准注意力&lt;/h2&gt;
&lt;p&gt;矩形自校准注意力可表述如下:&lt;/p&gt;
&lt;p&gt;$$\xi_{C}(\bar{y})=\delta(\psi_{k\times1}(\phi(\psi_{1\times k}(\bar{y}))))$$&lt;/p&gt;
&lt;p&gt;其中 $\psi$ 代表卷积操作, $k$ 是卷积核的大小，$\phi$ 表示批量归一化和 ReLU 激活函数，$\delta$ 表示 Sigmoid 函数&lt;/p&gt;
&lt;p&gt;然后将注意力特征与输入特征进行融合，通过一个卷积层提取输入特征的局部细节，然后与得到的注意力权重&lt;strong&gt;相乘&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$\xi_{F}(x,y)=\psi_{3\times3}(x)\odot y,$$&lt;/p&gt;
&lt;p&gt;完整代码实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class RCA(nn.Module):
    def __init__(self, inp, kernel_size=1, ratio=1, band_kernel_size=11, dw_size=(1, 1), padding=(0, 0), stride=1,
                 square_kernel_size=2, relu=True):
        super(RCA, self).__init__()
        self.dwconv_hw = nn.Conv2d(inp, inp, square_kernel_size, padding=square_kernel_size // 2, groups=inp)
        self.pool_h = nn.AdaptiveAvgPool2d((None, 1))
        self.pool_w = nn.AdaptiveAvgPool2d((1, None))

        gc = inp // ratio
        self.excite = nn.Sequential(
            nn.Conv2d(inp, gc, kernel_size=(1, band_kernel_size), padding=(0, band_kernel_size // 2), groups=gc),
            nn.BatchNorm2d(gc),
            nn.ReLU(inplace=True),
            nn.Conv2d(gc, inp, kernel_size=(band_kernel_size, 1), padding=(band_kernel_size // 2, 0), groups=gc),
            nn.Sigmoid()
        )

    def sge(self, x):
        x_h = self.pool_h(x)
        x_w = self.pool_w(x)
        x_gather = x_h + x_w
        ge = self.excite(x_gather)

        return ge

    def forward(self, x):
        loc = self.dwconv_hw(x)
        att = self.sge(x)
        out = att * loc

        return out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先通过一个卷积层提取空间局部特征 &lt;code&gt;loc&lt;/code&gt;，输出的特征图形状为 &lt;code&gt;[B, C, H, W]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后通过&lt;strong&gt;分别沿着 H 和 W 两个方向进行平均池化&lt;/strong&gt;，获取这两个方向的全局特征，并将这两个特征相加，得到新特征 &lt;code&gt;x_gather&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;两个方向的全局特征形状分别为 &lt;code&gt;[B, C, H, 1]&lt;/code&gt; 和 &lt;code&gt;[B, C, 1, W]&lt;/code&gt;，相加后会自动进行广播，得到的特征图形状为 &lt;code&gt;[B, C, H, W]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后通过一个 $1 \times 11$ 的卷积层将通道数缩减到 &lt;code&gt;gc&lt;/code&gt;，然后进行批归一化和 ReLU 激活函数&lt;/p&gt;
&lt;p&gt;随后再使用一个 $11 \times 1$ 的卷积层恢复原始的通道数，利用 Sigmoid 函数将特征图的每个元素映射到 &lt;code&gt;[0, 1]&lt;/code&gt; 之间得到注意力权重&lt;/p&gt;
&lt;p&gt;最后将权重与提取的特征 &lt;code&gt;loc&lt;/code&gt; 相乘，完成特征融合，重新加权输入特征，增强特征图的表达能力&lt;/p&gt;
&lt;h2&gt;MLP&lt;/h2&gt;
&lt;p&gt;通过两个 $1 \times 1$ 的卷积层来替代传统的全连接层，并在两个卷积层之间添加了归一化、激活函数和 Dropout&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class ConvMlp(nn.Module):
    &quot;&quot;&quot; 使用 1x1 卷积保持空间维度的 MLP
    &quot;&quot;&quot;
    def __init__(
            self, in_features, hidden_features=None, out_features=None, act_layer=nn.ReLU,
            norm_layer=None, bias=True, drop=0.):
        super().__init__()
        out_features = out_features or in_features
        hidden_features = hidden_features or in_features
        bias = to_2tuple(bias)

        self.fc1 = nn.Conv2d(in_features, hidden_features, kernel_size=1, bias=bias[0])
        self.norm = norm_layer(hidden_features) if norm_layer else nn.Identity()
        self.act = act_layer()
        self.drop = nn.Dropout(drop)
        self.fc2 = nn.Conv2d(hidden_features, out_features, kernel_size=1, bias=bias[1])

    def forward(self, x):
        x = self.fc1(x)
        x = self.norm(x)
        x = self.act(x)
        x = self.drop(x)
        x = self.fc2(x)
        return x
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;RCM 的完整实现&lt;/h2&gt;
&lt;p&gt;在矩形自校准注意力之后添加了批量归一化和 MLP 来完善特征，最后通过残差连接进一步加强特征重用，其可以描述为&lt;/p&gt;
&lt;p&gt;$$\mathbf{F}&lt;em&gt;{\mathrm{out}}=\rho(\xi&lt;/em&gt;{\mathrm{F}}(\mathrm{x},\xi_{\mathrm{C}}(\mathrm{H}&lt;em&gt;{\mathrm{P}}(\mathrm{x})\oplus\mathrm{V}&lt;/em&gt;{\mathrm{P}}(\mathrm{x}))))+\mathrm{x}$$&lt;/p&gt;
&lt;p&gt;其中 $H_p$ 和 $V_p$ 分别代表 H 和 W 方向的平均池化，$\rho$ 代表 BN 和 MLP&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class RCM(nn.Module):
    &quot;&quot;&quot; MetaNeXtBlock 块
    参数:
        dim (int): 输入通道数.
        drop_path (float): 随机深度率。默认: 0.0
        ls_init_value (float): 层级比例初始化值。默认: 1e-6.
    &quot;&quot;&quot;
    def __init__(
            self,
            dim,
            token_mixer=RCA,
            norm_layer=nn.BatchNorm2d,
            mlp_layer=ConvMlp,
            mlp_ratio=2,
            act_layer=nn.GELU,
            ls_init_value=1e-6,
            drop_path=0.,
            dw_size=11,
            square_kernel_size=3,
            ratio=1,
    ):
        super().__init__()
        self.token_mixer = token_mixer(dim, band_kernel_size=dw_size, square_kernel_size=square_kernel_size,
                                       ratio=ratio)
        self.norm = norm_layer(dim)
        self.mlp = mlp_layer(dim, int(mlp_ratio * dim), act_layer=act_layer)
        self.gamma = nn.Parameter(ls_init_value * torch.ones(dim)) if ls_init_value else None
        self.drop_path = DropPath(drop_path) if drop_path &gt; 0. else nn.Identity()

    def forward(self, x):
        shortcut = x
        x = self.token_mixer(x)
        x = self.norm(x)
        x = self.mlp(x)
        if self.gamma is not None:
            x = x.mul(self.gamma.reshape(1, -1, 1, 1))
        x = self.drop_path(x) + shortcut
        return x
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?rcm"/><enclosure url="http://wallpaper.csun.site/?rcm"/></item><item><title>FBSNet</title><link>https://blog.csun.site/blog/2024-10-7-fbsnet</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-10-7-fbsnet</guid><description>FBSNet：深度学习图像分割新方法</description><pubDate>Mon, 07 Oct 2024 11:03:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;论文: https://arxiv.org/abs/2109.00699v1&lt;/p&gt;
&lt;p&gt;代码: https://github.com/IVIPLab/FBSNet&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;FBSNet 的网络结构可分为三部分:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;initial block&lt;/li&gt;
&lt;li&gt;dual-branch backbone&lt;/li&gt;
&lt;li&gt;feature aggregation module&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/17700a2be6e75d6b1ab22c5e8696e41a.png&quot; alt=&quot;FBSNet&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Initial Block&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Initial Block&lt;/strong&gt; 包括三个 $3 \times 3$ 的卷积层，并在每一个卷积层之后添加了 Batch Normalization 和 PReLU 激活函数，在三层卷积层结束之后，又进行了一次 BN 和 PReLU&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Conv(nn.Module):
    def __init__(self, nIn, nOut, kSize, stride, padding, dilation=(1, 1), groups=1, bn_acti=False, bias=False):
        super().__init__()

        self.bn_acti = bn_acti

        self.conv = nn.Conv2d(nIn, nOut, kernel_size=kSize,
                              stride=stride, padding=padding,
                              dilation=dilation, groups=groups, bias=bias)

        if self.bn_acti:
            # Batch Normalization 和 PReLU 激活函数
            self.bn_prelu = BNPReLU(nOut)

    def forward(self, input):
        output = self.conv(input)

        if self.bn_acti:
            output = self.bn_prelu(output)

        return output


class BNPReLU(nn.Module):
    def __init__(self, nIn):
        super().__init__()
        self.bn = nn.BatchNorm2d(nIn, eps=1e-3)
        self.acti = nn.PReLU(nIn)

    def forward(self, input):
        output = self.bn(input)
        output = self.acti(output)

        return output

# --------------- class FBSNet ----------------------------
self.init_conv = nn.Sequential(
            Conv(3, 16, 3, 2, padding=1, bn_acti=True),
            Conv(16, 16, 3, 1, padding=1, bn_acti=True),
            Conv(16, 16, 3, 1, padding=1, bn_acti=True),
        )
        # 1/2
self.bn_prelu_1 = BNPReLU(16)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conv 1&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;Conv(3, 16, 3, 2, padding=1, bn_acti=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一层的输入通道数为 3，输出通道数为 16，卷积核大小为 3，步长为 2，padding 为 1&lt;/p&gt;
&lt;p&gt;由于步长为 2，这一层会对图像进行&lt;strong&gt;下采样&lt;/strong&gt;，将输入的宽高减半&lt;/p&gt;
&lt;p&gt;并通过 16 个 $3 \times 3$ 的卷积核对图像特征进行初步提取&lt;/p&gt;
&lt;p&gt;在卷积操作后，进行批归一化和 PReLU 激活，提高模型的训练稳定性和非线性表达能力&lt;/p&gt;
&lt;h3&gt;Conv 2 and Conv 3&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;Conv(16, 16, 3, 1, padding=1, bn_acti=True)
Conv(16, 16, 3, 1, padding=1, bn_acti=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二层和第三层具有相同的结构，通过 16 个 $3 \times 3$ 的卷积核提取更高级的图像特征&lt;/p&gt;
&lt;p&gt;但是这两层的步长均为 1，不会改变特征图的尺寸&lt;/p&gt;
&lt;h3&gt;Batch Normalization&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;BN&lt;/strong&gt; 是深度学习中一种用于加速神经网络训练，稳定网络训练过程的方法&lt;/p&gt;
&lt;p&gt;在训练过程中，随着网络参数的更新，前一层的参数变化会导致后一层的输入分布发生变化，这种现象称为&lt;strong&gt;内部协变量偏移（Internal Covariate Shift）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;内部协变量偏移会导致训练过程不稳定，收敛速度变慢，需要更小的学习率和更仔细的参数初始化&lt;/p&gt;
&lt;p&gt;BN 通过对&lt;strong&gt;每个小批次的数据进行归一化&lt;/strong&gt;，使得每一层的输入保持稳定的分布&lt;/p&gt;
&lt;h4&gt;原理&lt;/h4&gt;
&lt;p&gt;首先计算批次的&lt;strong&gt;均值&lt;/strong&gt;和&lt;strong&gt;方差&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$$\mu_B=\frac{1}{m}\sum_{i=1}^mx_i,\quad\sigma_B^2=\frac{1}{m}\sum_{i=1}^m(x_i-\mu_B)^2$$&lt;/p&gt;
&lt;p&gt;然后使用均值和方差对输入进行归一化，得到&lt;strong&gt;零均值&lt;/strong&gt;，&lt;strong&gt;单位方差&lt;/strong&gt;的输入&lt;/p&gt;
&lt;p&gt;$$\hat{x}_i=\frac{x_i-\mu_B}{\sqrt{\sigma_B^2+\epsilon}}$$&lt;/p&gt;
&lt;p&gt;这里的 $\epsilon$ 是一个很小的常数, 防止除以零&lt;/p&gt;
&lt;p&gt;最后引入**可训练的参数 $\gamma$ 和 $\beta$ **对输入进行缩放和平移，恢复数据的表达能力&lt;/p&gt;
&lt;p&gt;$$y_i=\gamma\hat{x}_i+\beta $$&lt;/p&gt;
&lt;h4&gt;注意事项&lt;/h4&gt;
&lt;p&gt;BN 通常放在全连接层或卷积层之后，激活函数之前&lt;/p&gt;
&lt;p&gt;对批大小敏感，批量太小可能导致估计的均值和方差不准确，影响模型性能&lt;/p&gt;
&lt;p&gt;在训练阶段，使用当前批次的数据计算均值和方差，在测试阶段，使用在训练过程中累积的全局均值和方差&lt;/p&gt;
&lt;h3&gt;PReLU 激活函数&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;PReLU&lt;/strong&gt;（Parametric Rectified Linear Unit）是 ReLU（Rectified Linear Unit）激活函数的改进版本，它在 ReLU 的基础上增加了一个&lt;strong&gt;可学习的参数&lt;/strong&gt;，用于调整负半轴的斜率。&lt;/p&gt;
&lt;h4&gt;ReLU&lt;/h4&gt;
&lt;p&gt;ReLU 激活函数的定义是 $f(x) = max(0, x)$，其简单高效，计算速度快，但是存在以下问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;死亡 ReLU 问题&lt;/strong&gt;：当权重更新后，某些神经元可能永远输出 0，不再更新。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;负半轴信息丢失&lt;/strong&gt;：对于输入小于 0 的值，梯度为 0，可能导致有用的信息被忽略。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;PReLU&lt;/h4&gt;
&lt;p&gt;PReLU 定义如下:&lt;/p&gt;
&lt;p&gt;$$f(x)=\begin{cases}x,&amp;#x26;\text{当 }x&gt;0\ax,&amp;#x26;\text{当 }x\leq0\end{cases}$$&lt;/p&gt;
&lt;p&gt;其中 $a$ 是一个可学习的参数，初始值通常为一个小的正数&lt;/p&gt;
&lt;p&gt;PReLU 由于负半轴的斜率 $a$ 可学习，且通常不为零，避免了神经元输出恒为零的情况&lt;/p&gt;
&lt;p&gt;同时允许负值通过激活函数，保留了输入为负值时的有用信息&lt;/p&gt;
&lt;h2&gt;dual-branch backbone&lt;/h2&gt;
&lt;p&gt;dual-branch backbone 采用双分支结构，由 &lt;strong&gt;Semantic Information Branch (SIB)&lt;/strong&gt; 和 &lt;strong&gt;Spatial Detail Branch (SDB)&lt;/strong&gt; 组成&lt;/p&gt;
&lt;h3&gt;SIB&lt;/h3&gt;
&lt;p&gt;SIB 整体采用&lt;strong&gt;编码器--解码器&lt;/strong&gt;结构，通过两个下采样块将特征图尺寸下采样到原始图像尺寸的 1/8，然后通过两个上采样块恢复原始图像尺寸&lt;/p&gt;
&lt;p&gt;在下采样和上采样操作中间穿插了 &lt;strong&gt;Channel Attention Module (CAM)&lt;/strong&gt; 和 &lt;strong&gt;Bottleneck Residual Unit (BRU)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/7cc0fb3737fe2435fb31febf3667f099.png&quot; alt=&quot;SIB&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Channel Attention Module (CAM)&lt;/h4&gt;
&lt;p&gt;由于通道中包含丰富的特征信息和干扰噪声，SIB 使用 CAM 来强调需要突出的特征，在实现时采用 &lt;strong&gt;[ECA-Net](&lt;a href=&quot;https://arxiv.org/pdf/1910.03151&quot;&gt;1910.03151 (arxiv.org)&lt;/a&gt;)&lt;/strong&gt; 中提出的 &lt;strong&gt;Efficient Channel Attention Module(ECA)&lt;/strong&gt; 模块，其过程如下:&lt;/p&gt;
&lt;p&gt;$$M_{c}\left( X \right) =\sigma\left( C_{k\times k}\left( f_{Trans}\left( f_{AvgPool}(X) \right) \right) \right)$$&lt;/p&gt;
&lt;p&gt;其中，$M_c\in\mathbb{R}^{C\times1\times1}$ 为通道注意力图，$X\in\mathbb{R}^{C\times H \times W}$ 为输入特征，$C_{k \times k}$ 表示卷积核大小为 $k$ 的卷积运算&lt;/p&gt;
&lt;p&gt;$f_{AugPool}(\cdot)$ 表示平均池化，$f_{Trans}(\cdot)$ 表示压缩和重新加权，$\sigma$ 为 Sigmoid 激活函数&lt;/p&gt;
&lt;p&gt;其 &lt;code&gt;pytorch&lt;/code&gt; 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class eca_layer(nn.Module):
    &quot;&quot;&quot;Constructs a ECA module.
    Args:
        channel: Number of channels of the input feature map
        k_size: Adaptive selection of kernel size
    &quot;&quot;&quot;

    def __init__(self, channel, k_size=3):
        super(eca_layer, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.conv = nn.Conv1d(1, 1, kernel_size=k_size, padding=(k_size - 1) // 2, bias=False)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        b, c, h, w = x.size()

        # feature descriptor on the global spatial information
        y = self.avg_pool(x)

        # Two different branches of ECA module
        y = self.conv(y.squeeze(-1).transpose(-1, -2)).transpose(-1, -2).unsqueeze(-1)

        # Multi-scale information fusion
        y = self.sigmoid(y)

        return x * y.expand_as(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先通过一个&lt;strong&gt;自适应的2D平均池化层&lt;/strong&gt;，对输入特征图在空间维度上进行全局平均池化，得到每个通道的全局平均值，将输入特征图的尺寸变为 $1 \times 1$ ,每个通道通过池化被压缩成一个值，输出维度为 &lt;code&gt;[batch_size, channels, 1, 1]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后通过 &lt;code&gt;y.squeeze(-1).transpose(-1, -2)&lt;/code&gt; 去除最后一个维度并交换最后两个维度，得到 &lt;code&gt;[batch_size, 1, channels]&lt;/code&gt;，从而将 &lt;strong&gt;1D 卷积操作应用到通道维度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后再通过 &lt;code&gt;transpose(-1, -2).unsqueeze(-1)&lt;/code&gt; 恢复原有形状 &lt;code&gt;[batch_size, channels, 1, 1]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;Sigmoid&lt;/code&gt;激活函数将输出值归一化到 0 到 1 之间，得到每个通道的&lt;strong&gt;权重&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最后将得到的权重 &lt;code&gt;y&lt;/code&gt; 与输入的特征图 &lt;code&gt;x&lt;/code&gt; &lt;strong&gt;相乘完成对通道的加权&lt;/strong&gt;，调整各通道的贡献程度&lt;/p&gt;
&lt;h4&gt;Bottleneck Residual Unit (BRU)&lt;/h4&gt;
&lt;p&gt;BRU 是一个三分支模块，左分支负责&lt;strong&gt;提取局部信息和短距离信息&lt;/strong&gt;，右分支用于&lt;strong&gt;扩大感受野以获取长距离特征信息&lt;/strong&gt;，中间分支专门用于&lt;strong&gt;保存输入信息&lt;/strong&gt;，示意图如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/4e734b1515d97fa076003b9aefa81eea.png&quot; alt=&quot;BRU&quot;&gt;&lt;/p&gt;
&lt;p&gt;图中使用的 $3 \times 1$ 和 $1 \times 3$ 卷积是采用&lt;strong&gt;卷积因子化策略&lt;/strong&gt;，将标准的 $3 \times 3$ 卷积分解成两个一维卷积核，可以在保证模型性能的同时显著减少模型参数&lt;/p&gt;
&lt;p&gt;其代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class BRUModule(nn.Module):
    def __init__(self, nIn, d=1, kSize=3, dkSize=3):  #
        super().__init__()
        self.bn_relu_1 = BNPReLU(nIn) 
        self.conv1x1_init = Conv(nIn, nIn // 2, 1, 1, padding=0, bn_acti=True)
        
        # -------------------------- 左分支 ----------------------------
        self.dconv3x1 = Conv(nIn // 2, nIn // 2, (dkSize, 1), 1, padding=(1, 0), groups=nIn // 2, bn_acti=True)
        self.dconv1x3 = Conv(nIn // 2, nIn // 2, (1, dkSize), 1, padding=(0, 1), groups=nIn // 2, bn_acti=True)
		self.ca11 = eca_layer(nIn // 2)
        self.dconv1x3_l = Conv(nIn // 2, nIn // 2, (1, dkSize), 1, padding=(0, 1), groups=nIn // 2, bn_acti=True)
        self.dconv3x1_l = Conv(nIn // 2, nIn // 2, (dkSize, 1), 1, padding=(1, 0), groups=nIn // 2, bn_acti=True)
        
		# -------------------------- 右分支 ----------------------------
        self.ddconv3x1 = Conv(nIn // 2, nIn // 2, (dkSize, 1), 1, padding=(1 * d, 0), dilation=(d, 1), groups=nIn // 2, bn_acti=True)
        self.ddconv1x3 = Conv(nIn // 2, nIn // 2, (1, dkSize), 1, padding=(0, 1 * d), dilation=(1, d), groups=nIn // 2, bn_acti=True)
        self.ca22 = eca_layer(nIn // 2)
        self.ddconv1x3_r = Conv(nIn // 2, nIn // 2, (1, dkSize), 1, padding=(0, 1 * d), dilation=(1, d), groups=nIn // 2, bn_acti=True)
        self.ddconv3x1_r = Conv(nIn // 2, nIn // 2, (dkSize, 1), 1, padding=(1 * d, 0), dilation=(d, 1), groups=nIn // 2, bn_acti=True)
        self.bn_relu_2 = BNPReLU(nIn // 2)
        
        # -------------------------- 中间分支 ----------------------------
        self.ca0 = eca_layer(nIn // 2)
        
        self.ca = eca_layer(nIn // 2)
        self.conv1x1 = Conv(nIn // 2, nIn, 1, 1, padding=0, bn_acti=False)
        self.shuffle_end = ShuffleBlock(groups=nIn // 2)

    def forward(self, input):
        output = self.bn_relu_1(input)
        output = self.conv1x1_init(output)

        br1 = self.dconv3x1(output)
        br1 = self.dconv1x3(br1)
        b1 = self.ca11(br1)
        br1 = self.dconv1x3_l(b1)
        br1 = self.dconv3x1_l(br1)

        br2 = self.ddconv3x1(output)
        br2 = self.ddconv1x3(br2)
        b2 = self.ca22(br2)
        br2 = self.ddconv1x3_r(b2)
        br2 = self.ddconv3x1_r(br2)


        output = br1 + br2 + self.ca0(output )+ b1 + b2

        output = self.bn_relu_2(output)

        output = self.conv1x1(output)
        output = self.ca(output)
        out = self.shuffle_end(output + input)
        return out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先对输入进行批归一化和 PReLU 激活，经过 $1 \times 1$ 卷积减半通道数&lt;/p&gt;
&lt;h5&gt;左分支&lt;/h5&gt;
&lt;p&gt;经过 &lt;code&gt;(3,1)&lt;/code&gt; 和 &lt;code&gt;(1,3)&lt;/code&gt; 的卷积操作，再经过 ECA 注意力层，并重复一次 &lt;code&gt;(1,3)&lt;/code&gt; 和 &lt;code&gt;(3,1)&lt;/code&gt; 卷积，提取局部信息和短距离信息&lt;/p&gt;
&lt;h5&gt;右分支&lt;/h5&gt;
&lt;p&gt;使用膨胀卷积扩展感受野，再经过 ECA 注意力层并重复膨胀卷积，获取长距离特征信息&lt;/p&gt;
&lt;h5&gt;中间分支&lt;/h5&gt;
&lt;p&gt;使用 ECA 层增强特征，然后再与左右分支 ECA 层的输出，最后的输出三者进行加和&lt;/p&gt;
&lt;p&gt;最后，输出重复开头的过程，进行批归一化和 PReLU 激活并经过 $1 \times 1$ 卷积，再通过一个 ECA 层增强特征，然后通过 &lt;code&gt;ShuffleBlock&lt;/code&gt; 进行通道混洗&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;
class ShuffleBlock(nn.Module):
    def __init__(self, groups):
        super(ShuffleBlock, self).__init__()
        self.groups = groups

    def forward(self, x):
        &apos;&apos;&apos;Channel shuffle: [N,C,H,W] -&gt; [N,g,C/g,H,W] -&gt; [N,C/g,g,H,w] -&gt; [N,C,H,W]&apos;&apos;&apos;
        N, C, H, W = x.size()
        g = self.groups
        #
        return x.view(N, g, int(C / g), H, W).permute(0, 2, 1, 3, 4).contiguous().view(N, C, H, W)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ShuffleBlock &lt;/code&gt;将通道维度分为 g 组，每组的通道数为 c/g，然后使用 &lt;code&gt;permute()&lt;/code&gt; 方法将通道和组的维度交换，最后恢复原始形状&lt;/p&gt;
&lt;p&gt;从而将不同组的通道顺序打乱，使得下一层的卷积能够处理不同组之间混合后的通道，增加特征的表达能力和相互依赖性。&lt;/p&gt;
&lt;h3&gt;SDB&lt;/h3&gt;
&lt;p&gt;在 SDB 分支中，使用了 &lt;strong&gt;Detail Residual Module (DRM)&lt;/strong&gt; 和 &lt;strong&gt;Spatial Attention Module (SAM)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/6be81bb9f04185e3c9e88bff204611fc.png&quot; alt=&quot;SDB&quot;&gt;&lt;/p&gt;
&lt;h4&gt;DRM&lt;/h4&gt;
&lt;p&gt;DRM 是专门为补充语义分支中丢失的空间细节而设计的，由 3 个 $3 \times 3$ 的卷积层和一个 $1 \times 1$ 的卷积层构成&lt;/p&gt;
&lt;p&gt;为了获得更多特征，将第二和第三个卷积层的通道数增加到原始输入的 4 倍（4C），最后使用一个 $1 \times 1$ 的卷积层将通道数再次减少到 C&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://5a352de.webp.li/2024/10/3ab6e0f0f39c394223f4b2922825193e.png&quot; alt=&quot;DRM&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是在 FBSNet 开源的代码中，只使用了 3 个 $3 \times 3$ 的卷积层&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;self.conv_sipath1 = Conv(16, 32, 3, 1, 1, bn_acti=True)
self.conv_sipath2 = Conv(32, 128, 3, 1, 1, bn_acti=True)
self.conv_sipath3 = Conv(128, 32, 3, 1, 1, bn_acti=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;SAM&lt;/h4&gt;
&lt;p&gt;空间注意力模块 SAM 是&lt;strong&gt;沿通道轴应用最大池化和平均池化，然后通过标准卷积对其进行串联&lt;/strong&gt;，从而生成有效的特征描述&lt;/p&gt;
&lt;p&gt;其过程可描述如下：&lt;/p&gt;
&lt;p&gt;$$M_{s}\left( X \right) =\sigma\left( C_{k\times k}\left( \left[ f_{AvgPool}(X),f_{MaxPool}(X) \right] \right) \right)$$&lt;/p&gt;
&lt;p&gt;其中，$M_s\in\mathbb{R}^{1\times H\times W}$ 为所需的空间注意力图，$X\in\mathbb{R}^{C\times H\times W}$ 为输入特征，$C_{k \times k}$ 表示卷积核大小为 $k$ 的卷积层，$[]$ 表示连接操作，$f_{AugPool}(\cdot)$ 表示平均池化操作，$f_{MaxPool}(\cdot)$ 表示最大池化操作，$\sigma$ 为 Sigmoid 函数&lt;/p&gt;
&lt;p&gt;其代码实现如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class SpatialAttention(nn.Module):
    def __init__(self, kernel_size=7):
        super(SpatialAttention, self).__init__()
        assert kernel_size in (3, 7), &apos;kernel size must be 3 or 7&apos;
        padding = 3 if kernel_size == 7 else 1
        self.conv1 = nn.Conv2d(2, 1, kernel_size, padding=padding, bias=False)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        avg_out = torch.mean(x, dim=1, keepdim=True)
        max_out, _ = torch.max(x, dim=1, keepdim=True)
        x = torch.cat([avg_out, max_out], dim=1)
        x = self.conv1(x)
        return self.sigmoid(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先沿着通道维度计算每个像素的&lt;strong&gt;通道均值&lt;/strong&gt;，得到一个均值特征图，再沿着通道维度计算每个像素的&lt;strong&gt;通道最大值&lt;/strong&gt;，得到一个最大值特征图&lt;/p&gt;
&lt;p&gt;然后将均值特征图和最大值特征图沿着通道维度进行拼接，生成一个具有两个通道的特征图&lt;/p&gt;
&lt;p&gt;对拼接后的特征图应用卷积操作，降维到单个通道，进一步提取空间特征&lt;/p&gt;
&lt;p&gt;对卷积后的输出通过 Sigmoid 激活函数，将结果限制在 &lt;code&gt;[0, 1]&lt;/code&gt; 之间，形成空间注意力权重矩阵&lt;/p&gt;
&lt;h2&gt;feature aggregation module&lt;/h2&gt;
&lt;p&gt;首先将语义分支和空间分支的输出加和，并进行进行批归一化和 PReLU 激活&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;output = self.bn_prelu_8(output + output_sipath)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用 &lt;strong&gt;&lt;a href=&quot;http://arxiv.org/abs/2103.02907&quot;&gt;CoordAttention&lt;/a&gt;&lt;/strong&gt; 对特征进行融合，其代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class CoordAtt(nn.Module):
    def __init__(self, inp, oup, reduction=4):
        super(CoordAtt, self).__init__()
        self.pool_h = nn.AdaptiveAvgPool2d((None, 1))
        self.pool_w = nn.AdaptiveAvgPool2d((1, None))

        mip = max(8, inp // reduction)

        self.conv1 = nn.Conv2d(inp, mip, kernel_size=1, stride=1, padding=0)
        self.bn1 = nn.BatchNorm2d(mip)
        self.act = h_swish()

        self.conv_h = nn.Conv2d(mip, oup, kernel_size=1, stride=1, padding=0)
        self.conv_w = nn.Conv2d(mip, oup, kernel_size=1, stride=1, padding=0)

    def forward(self, x):
        identity = x

        n, c, h, w = x.size()
        x_h = self.pool_h(x)
        x_w = self.pool_w(x).permute(0, 1, 3, 2)

        y = torch.cat([x_h, x_w], dim=2)
        y = self.conv1(y)
        y = self.bn1(y)
        y = self.act(y)

        x_h, x_w = torch.split(y, [h, w], dim=2)
        x_w = x_w.permute(0, 1, 3, 2)

        a_h = self.conv_h(x_h).sigmoid()
        a_w = self.conv_w(x_w).sigmoid()

        out = identity * a_w * a_h

        return out
    
class h_swish(nn.Module):
    def __init__(self, inplace=True):
        super(h_swish, self).__init__()
        self.sigmoid = h_sigmoid(inplace=inplace)

    def forward(self, x):
        return x * self.sigmoid(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先对于输入特征，分别在 H 和 W 两个方向进行平均池化，得到两个大小分别为 &lt;code&gt;[B, C, H, 1]&lt;/code&gt; 和 &lt;code&gt;[B, C, W, 1]&lt;/code&gt;(x_w 使用 &lt;code&gt;permute()&lt;/code&gt; 做了转置)的特征图&lt;/p&gt;
&lt;p&gt;然后将 &lt;code&gt;x_h&lt;/code&gt; 和 &lt;code&gt;x_w&lt;/code&gt; 沿着高度和宽度的方向拼接在一起，得到大小为 &lt;code&gt;[B, C, H+W, 1]&lt;/code&gt; 的特征图&lt;/p&gt;
&lt;p&gt;随后对其进行 $1 \times 1$ 卷积、批归一化和激活函数处理，融合特征信息&lt;/p&gt;
&lt;p&gt;然后通过 &lt;code&gt;torch.split()&lt;/code&gt; 将 &lt;code&gt;y&lt;/code&gt; 在拼接的维度上分割，得到分别对应原始的高度和宽度方向的特征图&lt;/p&gt;
&lt;p&gt;再分别通过 $1 \times 1$ 卷积得到高度和宽度方向的注意力权重，并通过 &lt;code&gt;sigmoid()&lt;/code&gt; 激活函数将其压缩到 &lt;code&gt;[0, 1]&lt;/code&gt; 范围，表示每个通道在高度和宽度方向的权重&lt;/p&gt;
&lt;p&gt;最终的输出是输入乘以高度和宽度的权重，根据权重调整特征图的不同部分&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?fbsnet"/><enclosure url="http://wallpaper.csun.site/?fbsnet"/></item><item><title>SpringBoot 公共字段自动填充</title><link>https://blog.csun.site/blog/2024-09-29-spring-boot-public-field-auto-fill</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-09-29-spring-boot-public-field-auto-fill</guid><description>SpringBoot 自动填充公共字段</description><pubDate>Sun, 29 Sep 2024 22:03:00 GMT</pubDate><content:encoded>&lt;p&gt;在使用 SpringBoot 框架开发项目时，经常会遇到 「创建时间」「修改时间」等公共字段，这些字段每次都需要我们手动去设置，十分麻烦。&lt;/p&gt;
&lt;p&gt;本文使用 SpringBoot 中的切面功能来实现这些公共字段的自动填充&lt;/p&gt;
&lt;h2&gt;定义注解&lt;/h2&gt;
&lt;p&gt;首先我们定义一个注解用于标记哪些方法需要实现自动填充&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 标识需要自动填充公共字段的方法
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    /**
     * 数据库操作类型
     * @return
     */
    OperationType value();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@Target(ElementType.METHOD)&lt;/code&gt; 标记该注解用于方法上面&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Retention(RetentionPolicy.RUNTIME)&lt;/code&gt; 指定注解在运行阶段可用&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OperationType value();&lt;/code&gt; 函数指定注解需要指定一个参数 &lt;code&gt;value&lt;/code&gt;，值为 &lt;code&gt;OperationType&lt;/code&gt; 类型，&lt;code&gt;OperationType&lt;/code&gt;是定义的一个枚举类，用于指定数据库操作的类型&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 数据库操作类型
 */
public enum OperationType {

    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;定义切面类&lt;/h2&gt;
&lt;h3&gt;切入点&lt;/h3&gt;
&lt;p&gt;首先使用切入点表达式标记这个切面类会在哪些方法上执行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 切入点
 */
@Pointcut(&quot;execution(* com.sky.mapper.*.*(..)) &amp;#x26;&amp;#x26; @annotation(com.sky.annotations.AutoFill)&quot;)
public void autoFillPointCut(){}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该表达式说明切入点是 mapper 包下的任意函数，但是需要使用了上述定义的 &lt;code&gt;AutoFill&lt;/code&gt; 注解&lt;/p&gt;
&lt;h3&gt;通知方法&lt;/h3&gt;
&lt;p&gt;使用前置通知，在 mapper 函数执行之前，完成字段自动填充过程&lt;/p&gt;
&lt;p&gt;首先, 我们需要获取到数据库操作类型，使用 &lt;code&gt;joinPoint.getSignature()&lt;/code&gt; 先获取需要填充字段的方法的签名，然后利用 java 的反射机制获取到该方法上的注解对象 &lt;code&gt;signature.getMethod().getAnnotation(AutoFill.class)&lt;/code&gt;, 从而获取到该注解对象携带的数据库操作类型。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 获取方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取方法注解对象
AutoFill annotation = signature.getMethod().getAnnotation(AutoFill.class);
// 获取数据库操作类型
OperationType op = annotation.value();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后获取该方法的参数，即我们需要填充字段的实体对象， 我们约定实体对象放在方法的第一个参数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 获取需要填充的参数
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0)
	return;
// 获取需要填充的实体对象, 约定实体对象放在方法的第一个参数
Object entity = args[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后根据数据库操作类型判断需要填充哪些字段，利用反射获取到实体对象中相应字段的 &lt;code&gt;set&lt;/code&gt; 方法并 &lt;code&gt;invoke&lt;/code&gt; 该方法设置相应字段的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 给参数赋值
if(op == OperationType.INSERT){
    // 插入
    try {
        entity.getClass().getDeclaredMethod(&quot;setCreateTime&quot;, LocalDateTime.class).invoke(entity, now);
        entity.getClass().getDeclaredMethod(&quot;setUpdateTime&quot;, LocalDateTime.class).invoke(entity, now);
        entity.getClass().getDeclaredMethod(&quot;setCreateUser&quot;, Long.class).invoke(entity, currentId);
        entity.getClass().getDeclaredMethod(&quot;setUpdateUser&quot;, Long.class).invoke(entity, currentId);
    } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
} else if(op == OperationType.UPDATE){
    // 更新
    try {
        entity.getClass().getDeclaredMethod(&quot;setUpdateTime&quot;, LocalDateTime.class).invoke(entity, now);
        entity.getClass().getDeclaredMethod(&quot;setUpdateUser&quot;, Long.class).invoke(entity, currentId);
    } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;完整代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 自动填充切面类
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {

    /**
     * 切入点
     */
    @Pointcut(&quot;execution(* com.sky.mapper.*.*(..)) &amp;#x26;&amp;#x26; @annotation(com.sky.annotations.AutoFill)&quot;)
    public void autoFillPointCut(){}

    /**
     * 前置通知方法实现自动填充
     * @param joinPoint
     */
    @Before(&quot;autoFillPointCut()&quot;)
    public void autoFill(JoinPoint joinPoint){
        // 获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取方法注解对象
        AutoFill annotation = signature.getMethod().getAnnotation(AutoFill.class);
        // 获取数据库操作类型
        OperationType op = annotation.value();

        // 获取需要填充的参数
        Object[] args = joinPoint.getArgs();
        if(args == null || args.length == 0)
            return;
        // 获取需要填充的实体对象, 约定实体对象放在方法的第一个参数
        Object entity = args[0];

        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        // 给参数赋值
        if(op == OperationType.INSERT){
            // 插入
            try {
                entity.getClass().getDeclaredMethod(&quot;setCreateTime&quot;, LocalDateTime.class).invoke(entity, now);
                entity.getClass().getDeclaredMethod(&quot;setUpdateTime&quot;, LocalDateTime.class).invoke(entity, now);
                entity.getClass().getDeclaredMethod(&quot;setCreateUser&quot;, Long.class).invoke(entity, currentId);
                entity.getClass().getDeclaredMethod(&quot;setUpdateUser&quot;, Long.class).invoke(entity, currentId);
            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        } else if(op == OperationType.UPDATE){
            // 更新
            try {
                entity.getClass().getDeclaredMethod(&quot;setUpdateTime&quot;, LocalDateTime.class).invoke(entity, now);
                entity.getClass().getDeclaredMethod(&quot;setUpdateUser&quot;, Long.class).invoke(entity, currentId);
            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;要使用自动填充功能，只需要在 &lt;code&gt;mapper&lt;/code&gt; 方法上加上 &lt;code&gt;@AutoFill&lt;/code&gt; 注解即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@AutoFill(value = OperationType.UPDATE)
void update(Category category);
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?spring"/><enclosure url="http://wallpaper.csun.site/?spring"/></item><item><title>COCO+LVIS 数据集</title><link>https://blog.csun.site/blog/2024-07-24-coco-lvis-dataset</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-07-24-coco-lvis-dataset</guid><description>提升LVIS数据集的交互分割能力</description><pubDate>Wed, 24 Jul 2024 15:32:00 GMT</pubDate><content:encoded>&lt;p&gt;LVIS 数据集存在一个不足之处:该数据集呈现出长尾分布特性,导致普遍物种类别缺失,这可能会对训练出的模型精度与泛化能力造成影响.&lt;/p&gt;
&lt;p&gt;针对这一问题,&lt;a href=&quot;https://arxiv.org/pdf/2102.06583&quot;&gt;Reviving Iterative Training with Mask Guidance for Interactive Segmentation&lt;/a&gt; 对 LVIS 标签进行补充,加入 COCO 数据集中的掩码信息.最终得到了包含 104k 张图片 和 1.6M instance-level masks 的 COCO+LVIS 数据集.&lt;/p&gt;
&lt;p&gt;数据集链接:&lt;a href=&quot;https://github.com/SamsungLabs/ritm_interactive_segmentation?tab=readme-ov-file#datasets&quot;&gt;SamsungLabs/ritm_interactive_segmentation: Reviving Iterative Training with Mask Guidance for Interactive Segmentation (github.com)&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;数据集组合&lt;/h2&gt;
&lt;p&gt;首先将下载的 &lt;a href=&quot;https://github.com/saic-vul/ritm_interactive_segmentation/releases/download/v1.0/cocolvis_annotation.tar.gz&quot;&gt;cocolvis_annotation.tar.gz&lt;/a&gt; 解压会得到两个文件夹 train 和 val&lt;/p&gt;
&lt;p&gt;将从 LVIS 官网下载的 &lt;a href=&quot;http://images.cocodataset.org/zips/train2017.zip&quot;&gt;train2017.zip&lt;/a&gt; 和 &lt;a href=&quot;http://images.cocodataset.org/zips/val2017.zip&quot;&gt;val2017.zip&lt;/a&gt; 解压并重命名为 images 分别放到 tain 和 val 两个文件夹中&lt;/p&gt;
&lt;h2&gt;读取数据集&lt;/h2&gt;
&lt;p&gt;数据集图片和掩码的对应信息存放在 hannotation.pickle 文件中, 读取该文件会得到一个列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;with open(&apos;/content/train/hannotation.pickle&apos;, &apos;rb&apos;) as f:
  dataset_samples = sorted(pickle.load(f).items())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;列表的每一项是一个元组, 表示一个样本的信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(dataset_samples[2])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;(
	&apos;000000000036&apos;,
	{
	  &quot;num_instance_masks&quot;: 8,
	  &quot;hierarchy&quot;: {
	    &quot;0&quot;: {
	      &quot;children&quot;: [4, 5, 7, 2],
	      &quot;parent&quot;: None,
	      &quot;node_level&quot;: 0
	    },
	    &quot;1&quot;: None,
	    &quot;2&quot;: {
	      &quot;children&quot;: [],
	      &quot;parent&quot;: 0,
	      &quot;node_level&quot;: 1
	    },
	    &quot;3&quot;: None,
	    &quot;4&quot;: {
	      &quot;children&quot;: [6],
	      &quot;parent&quot;: 0,
	      &quot;node_level&quot;: 1
	    },
	    &quot;5&quot;: {
	      &quot;children&quot;: [],
	      &quot;parent&quot;: 0,
	      &quot;node_level&quot;: 1
	    },
	    &quot;6&quot;: {
	      &quot;children&quot;: [],
	      &quot;parent&quot;: 4,
	      &quot;node_level&quot;: 2
	    },
	    &quot;7&quot;: {
	      &quot;children&quot;: [],
	      &quot;parent&quot;: 0,
	      &quot;node_level&quot;: 1
	    }
	  }
	}
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;元组的第一项是图片(000000000036.jpg)和掩码(000000000036.pickle)的文件名&lt;/p&gt;
&lt;p&gt;第二项是一个字典, &lt;code&gt;num_instance_masks&lt;/code&gt; 表示 instance mask 的数量, hierarchy 存储了 instance 的对应关系, 前面的数字 &lt;code&gt;&quot;0&quot;, &quot;1&quot;&lt;/code&gt; 表示 instance id, &lt;code&gt;children&lt;/code&gt; 和 &lt;code&gt;parent&lt;/code&gt; 表示了 instance 之间的包含关系, 例如 &lt;code&gt;&quot;0&quot;: &lt;/code&gt;{&quot;children&quot;: [4, 5, 7, 2]} 表示 instance 0 包含了 4, 5, 7, 2&lt;/p&gt;
&lt;h3&gt;读取图像&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import matplotlib.pyplot as plt
from PIL import Image

# 定义图片路径
image_path = &apos;/content/train/images/000000000036.jpg&apos;

# 使用 PIL 打开图片
image = Image.open(image_path)

# 使用 matplotlib 显示图片
plt.imshow(image)
plt.axis(&apos;off&apos;)  # 不显示坐标轴
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://p1.meituan.net/csc/df833aed780d275aed3ccca7d8723e37268446.png&quot; alt=&quot;undefined&quot;&gt;&lt;/p&gt;
&lt;h3&gt;读取 masks&lt;/h3&gt;
&lt;p&gt;000000000036.pickle 文件存储了分层的 masks &lt;code&gt;layers&lt;/code&gt;, 以及一个列表 &lt;code&gt;objs_mapping&lt;/code&gt; , 这个列表存储了 instance 对应的层数和 mask_id&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pickle_path = &apos;/content/train/masks/000000000036.pickle&apos;
with open(pickle_path, &apos;rb&apos;) as f:
    encoded_layers, objs_mapping = pickle.load(f)
    layers = [cv2.imdecode(x, cv2.IMREAD_UNCHANGED) for x in encoded_layers]
    layers = np.stack(layers, axis=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如, &lt;code&gt;objs_mapping&lt;/code&gt; 的值为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[(0, 2), (0, 1), (3, 1), (1, 2), (1, 1), (1, 3), (2, 2), (2, 1), (0, 5), (0, 6), (0, 4), (0, 3)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;列表的索引对应着 instance id, 如 &lt;code&gt;objs_mapping[0]&lt;/code&gt; 对应 instance 0 的 mask 层数是 0, mask_id 为 2&lt;/p&gt;
&lt;p&gt;绘制出每层的 mask 并标记出 mask_id&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pickle_path = &apos;/content/train/masks/000000000036.pickle&apos;
with open(pickle_path, &apos;rb&apos;) as f:
    encoded_layers, objs_mapping = pickle.load(f)
    layers = [cv2.imdecode(x, cv2.IMREAD_UNCHANGED) for x in encoded_layers]
    layers = np.stack(layers, axis=2)
       
# 确定有多少层
num_layers = layers.shape[2]
# 绘制每一层
fig, axes = plt.subplots(1, num_layers, figsize=(15, 15))

for i in range(num_layers):
    ax = axes[i]
    ax.imshow(layers[:, :, i])
    ax.axis(&apos;off&apos;)
    ax.set_title(f&apos;Layer {i}&apos;)
    # 找到每个区域并标记值
    layer = layers[:, :, i]
    unique_values = np.unique(layer)
    for value in unique_values:
      mask = (layer == value)
      y, x = np.where(mask)
      if len(x) &gt; 0 and len(y) &gt; 0:
        rand_idx = np.random.randint(len(x))
        rand_x, rand_y = x[rand_idx], y[rand_idx]
        ax.text(rand_x, rand_y, str(value), color=&apos;white&apos;, fontsize=12, ha=&apos;center&apos;, va=&apos;center&apos;)

plt.tight_layout()
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://p0.meituan.net/csc/34365102ccb2c9405e5a1ae0639f700455971.png&quot; alt=&quot;undefined&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以直观的看出 instance 与 mask 的对应关系以及 instance 之间的包含关系&lt;/p&gt;
&lt;p&gt;例如 instance 0 代表那个女人, 其对应的 mask 层数是 layer 0, mask_id 是 2, 并且包含了 4, 5, 7, 2 这几个 instance&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?coco"/><enclosure url="http://wallpaper.csun.site/?coco"/></item><item><title>89 things I know about Git commits</title><link>https://blog.csun.site/blog/2024-07-22-89-things-i-know-about-git-commits</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-07-22-89-things-i-know-about-git-commits</guid><description>Git 提交的89个要点</description><pubDate>Mon, 22 Jul 2024 11:33:00 GMT</pubDate><content:encoded>&lt;p&gt;本文译自: &lt;a href=&quot;https://www.jvt.me/posts/2024/07/12/things-know-commits/&quot;&gt;89 things I know about Git commits · Jamie Tanna | Software Engineer (jvt.me)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;1.Git 有不同的用途——协作工具、备份工具、文档工具&lt;/p&gt;
&lt;p&gt;2.Git 的 commit messages 堪称出色&lt;/p&gt;
&lt;p&gt;3.我从没遇到过谁像我一样喜欢阅读 commit messages&lt;/p&gt;
&lt;p&gt;4.通过提交记录查找变更原因比通过 issue/bug tracker 更容易&lt;/p&gt;
&lt;p&gt;5.标注为 ‘Various fixes. DEV-123’ 的 commit, 要比只写 ‘Various fixes’ 的更好&lt;/p&gt;
&lt;p&gt;6.如果 issue 本身没有任何有用信息，那么提交说明“Various fixes. DEV-123”是更糟糕的&lt;/p&gt;
&lt;p&gt;7.Rebase-merging 是我的偏好. 然后是 squash-merge, 再然后是 merge&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rebase-merging&lt;/strong&gt; 指先 rebase 再 merge, 将当前分支的所有提交 &quot;移植&quot; 到目标分支的最新提交, 会生成一个线性的提交历史, 例如原来的分支可能是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main:    A---B---C
               \
feature:         D---E---F
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;rebase 后变成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main:    A---B---C
                    \
feature:             D&apos;---E&apos;---F&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再 merge 后变成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main:    A---B---C-------M
                         / \
feature:                 D&apos;---E&apos;---F&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以使提交历史变得线性，更加整洁，便于阅读，并且仍然保留了分支合并的历史&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;squash-merge&lt;/strong&gt; 会把分支上的所有更改压缩成一个未提交的快照，并合并到目标分支&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git merge --squash feature
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终的提交历史会变成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main:    A---B---C---G
feature:        \    
                D---E---F
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;G&lt;/code&gt; 是压缩后的单个提交，包含了 &lt;code&gt;D&lt;/code&gt;, &lt;code&gt;E&lt;/code&gt;, &lt;code&gt;F&lt;/code&gt; 的所有更改&lt;/p&gt;
&lt;p&gt;8.如果你不学习如何 rebase，你就错失了一个很好的技能&lt;/p&gt;
&lt;p&gt;9.当事情出错时说“只要删除库”的人真的让我很烦&lt;/p&gt;
&lt;p&gt;10.学习如何使用 &lt;code&gt;git reflog&lt;/code&gt;，它会帮你恢复删除的仓库&lt;/p&gt;
&lt;p&gt;11.掌握如何使用 &lt;code&gt;git reflog&lt;/code&gt; ，你就能避免一些不算太严重的错误&lt;/p&gt;
&lt;p&gt;12.学习各种复杂工具和命令并不能避免你时不时犯错&lt;/p&gt;
&lt;p&gt;13.我最近一次拙劣的 rebase 发生在上周，我需要 &lt;code&gt;git reflog&lt;/code&gt; 来帮我搞定&lt;/p&gt;
&lt;p&gt;14.学习如何撤销 &lt;code&gt;force push&lt;/code&gt;，然后学习如何更安全地 &lt;code&gt;force push&lt;/code&gt;（记住 &lt;code&gt;=ref&lt;/code&gt; ！）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jvt.me/posts/2021/10/23/undo-force-push/&quot;&gt;How to Undo a &lt;code&gt;git push --force&lt;/code&gt; · Jamie Tanna | Software Engineer (jvt.me)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jvt.me/posts/2018/09/18/safely-force-git-push/&quot;&gt;Safely Force Pushing with Git using &lt;code&gt;--force-with-lease=ref&lt;/code&gt; · Jamie Tanna | Software Engineer (jvt.me)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;15.Squashing是对精心编写的原子提交的一种浪费&lt;/p&gt;
&lt;p&gt;16.Squashing 比100次糟糕的提交要好&lt;/p&gt;
&lt;p&gt;17.Squash时，先 squash，再写一条好的 commit message，这是很好的做法&lt;/p&gt;
&lt;p&gt;18.直接删除，然后不重新编辑 commit message 是最糟糕的&lt;/p&gt;
&lt;p&gt;19.Squash时, 当你有100个糟糕的提交时，不重新编写 commit message 是一种犯罪&lt;/p&gt;
&lt;p&gt;20.Squash时， 不重新编辑 commit message， 这比从包含100个垃圾提交的分支出合并提交更糟糕&lt;/p&gt;
&lt;p&gt;21.撰写一份内容翔实的 PR/MR description， 却不用于通知 squash-merge message， 这是浪费时间&lt;/p&gt;
&lt;p&gt;22.编写 commit message 有助于我找出遗漏的测试用例、遗漏的文档或无效的思考过程，因为它有助于我重新编写更改的原因&lt;/p&gt;
&lt;p&gt;23.使用您的 &lt;code&gt;git log&lt;/code&gt; 作为更新的指示是有效的&lt;/p&gt;
&lt;p&gt;24.我不会费心在我的提交上签名（除非迫不得已）&lt;/p&gt;
&lt;p&gt;25.如果需要签署提交，SSH密钥签名几乎不会让人感到讨厌&lt;/p&gt;
&lt;p&gt;26.如果您需要在存储库之间移动文件，则需要使用 &lt;code&gt;git subtree&lt;/code&gt; 保持历史记录的完整性&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jvt.me/posts/2018/06/01/git-subtree-monorepo/&quot;&gt;Merging multiple repositories into a monorepo, while preserving history, using &lt;code&gt;git subtree&lt;/code&gt; · Jamie Tanna | Software Engineer (jvt.me)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;27.提交应该是原子性的——所有的代码、测试和配置更改都应该包含在内&lt;/p&gt;
&lt;p&gt;28.我花费了大量时间来确保每个提交都能自动通过CI检查。&lt;/p&gt;
&lt;p&gt;29.有些人会做出可怕的事情，比如将实现代码和测试代码分开&lt;/p&gt;
&lt;p&gt;30.将文档放在单独的提交中是可以的——我们不必在单个提交中提供完整的端到端功能&lt;/p&gt;
&lt;p&gt;31.使用 squash-merges 的仓库很糟糕&lt;/p&gt;
&lt;p&gt;32.作为开源项目的维护者，我喜欢使用“squash-merge”，这样我就可以重写贡献者的提交消息&lt;/p&gt;
&lt;p&gt;33.有时，指导如何撰写特定的 commit message 并不值得。&lt;/p&gt;
&lt;p&gt;34.你身边的人塑造了你的写作风格。&lt;/p&gt;
&lt;p&gt;35.提前做好工作，使你的提交历史具有原子性&lt;/p&gt;
&lt;p&gt;36.事后将一个巨大的提交拆分成多个原子提交，这要痛苦得多&lt;/p&gt;
&lt;p&gt;37.将工作原子化有助于提高你的回报动力——你可以完成更多的事情&lt;/p&gt;
&lt;p&gt;38.原子提交与 &lt;a href=&quot;https://www.jvt.me/posts/2022/04/12/prefactor/&quot;&gt;Prefactoring&lt;/a&gt;配合得非常好&lt;/p&gt;
&lt;p&gt;39.有时 prefactoring 提交可以进入单独的PR（特别是使用合并时）&lt;/p&gt;
&lt;p&gt;40.编写 commit message 可能比实现本身花费的时间更长&lt;/p&gt;
&lt;p&gt;41.commit message 的长度可能比提交中修改的行数多一个数量级&lt;/p&gt;
&lt;p&gt;42.如果您在 commit message 中写了很多“和”或“也”，可能是因为您想做的事情太多了&lt;/p&gt;
&lt;p&gt;43.翻阅 Git 的提交历史，我解开了一些谜团，让我理解了为什么没有原作者来回答我的问题。&lt;/p&gt;
&lt;p&gt;44.commit message 不仅能够反映你做了什么，还能反映你为什么这么做，值得深思&lt;/p&gt;
&lt;p&gt;45.为什么比什么更重要——任何人都可以查看差异，并大致弄清楚做了哪些改动，但背后的意图才是关键&lt;/p&gt;
&lt;p&gt;46.如果你只写发生了什么变化，那你就很烦人，我不喜欢你&lt;/p&gt;
&lt;p&gt;47.一个解释说明的提交比一个只有“修复”的提交要好&lt;/p&gt;
&lt;p&gt;48.Chris Beams 的文章 &lt;a href=&quot;https://cbea.ms/git-commit/&quot;&gt;How to Write a Git Commit Message&lt;/a&gt; 在近10年后仍然是一篇优秀的文章，也是很好的入门读物！&lt;/p&gt;
&lt;p&gt;49.提交是对提交者假设和世界状态的特定时间点的解释。不要对他们过于苛刻&lt;/p&gt;
&lt;p&gt;50.我不想看AI/LLM重写你的修改——要么自己写，要么把它称为 &lt;code&gt;Various fixes&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;51.需要有一种方法（也许使用 &lt;code&gt;git notes&lt;/code&gt; ）在之前的留言中添加注释，以纠正假设&lt;/p&gt;
&lt;p&gt;52.我不会事先写完美的提交消息——有时它们会像 &lt;code&gt;rew! add support for SBOMs&lt;/code&gt; 或 &lt;code&gt;sq&lt;/code&gt; 一样长，或者使用 &lt;code&gt;git commit --fixup&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;53.一般来说，我会将非常优秀的原子工作分解提交&lt;/p&gt;
&lt;p&gt;54.我将把原子提交拆分成多个提交，有时&lt;/p&gt;
&lt;p&gt;55.在发送给合作者审阅之前，请务必检查自己的代码更改&lt;/p&gt;
&lt;p&gt;56.查看提交消息与查看代码更改同样重要&lt;/p&gt;
&lt;p&gt;57.让所有贡献者对提交历史投入同样的关注，这是一场必输的战争&lt;/p&gt;
&lt;p&gt;58.试图规范历史将会是痛苦的&lt;/p&gt;
&lt;p&gt;59.试图强制要求对提交消息进行审查，将其作为代码审查的一部分，这将会非常痛苦&lt;/p&gt;
&lt;p&gt;60.试图监管历史的确会导致对代码库中的更改进行更详细的记录和考虑&lt;/p&gt;
&lt;p&gt;61.将隐含的假设明确化确实很有用&lt;/p&gt;
&lt;p&gt;62.介绍 &lt;code&gt;commitlint&lt;/code&gt; 可能有用，但也可能令人沮丧&lt;/p&gt;
&lt;p&gt;63.让您的合作者主动撰写优秀的提交消息，总比您强迫他们写要好。&lt;/p&gt;
&lt;p&gt;64.有些人不写，这没关系&lt;/p&gt;
&lt;p&gt;65.写作是一种技能&lt;/p&gt;
&lt;p&gt;66.我的写作（commit message）水平并不完美&lt;/p&gt;
&lt;p&gt;67.有时我懒得写完美的信息&lt;/p&gt;
&lt;p&gt;68.有时我写一些非常棒的提交消息，我自己都印象深刻&lt;/p&gt;
&lt;p&gt;69.使用模板来编写 Git 提交消息是正确操作的良好开端&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jvt.me/posts/2017/04/17/commit-templates/&quot;&gt;Saving Repetition with Git Commit Templates · Jamie Tanna | Software Engineer (jvt.me)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;70.&lt;code&gt;fixup&lt;/code&gt; 提交和 &lt;code&gt;git rebase --autosquash&lt;/code&gt; 是我学到的最好的Git技巧之一&lt;/p&gt;
&lt;p&gt;71.我珍视与拥有不同视角、技能和工作方法的团队共事的机会&lt;/p&gt;
&lt;p&gt;72.但我也很珍视拥有一支团队，他们撰写原子提交时附上了精心撰写的提交消息&lt;/p&gt;
&lt;p&gt;73.撰写任务信息与撰写精心设计的用户故事/工单一样有用&lt;/p&gt;
&lt;p&gt;74.&lt;code&gt;git commit -m sq&lt;/code&gt; 可能是我最常用的命令&lt;/p&gt;
&lt;p&gt;75.使用 &lt;code&gt;git add -p&lt;/code&gt; 和 &lt;code&gt;git commit -p&lt;/code&gt; 对于原子提交非常重要&lt;/p&gt;
&lt;p&gt;76.切勿使用 &lt;code&gt;git add -u&lt;/code&gt; 或 &lt;code&gt;git add .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;77.了解何时可以使用 &lt;code&gt;git add -u&lt;/code&gt; 或 &lt;code&gt;git add .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;78.我确实需要研究一下Graphite、 &lt;code&gt;git-branchless&lt;/code&gt; 等工具，以及其它提供 stacked PR 设置的方法&lt;/p&gt;
&lt;p&gt;79.当需要自动发布时时，使用 &lt;a href=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot;&gt;conventional commits&lt;/a&gt;与 &lt;a href=&quot;https://github.com/semantic-release/semantic-release&quot;&gt;semantic-release&lt;/a&gt;或&lt;a href=&quot;https://github.com/go-semantic-release/semantic-release&quot;&gt;go-semantic-release&lt;/a&gt;会有很大的不同。&lt;/p&gt;
&lt;p&gt;80.将  &lt;a href=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot;&gt;conventional commits&lt;/a&gt; 作为提交框架确实很有用&lt;/p&gt;
&lt;p&gt;81.对于多动症患者来说，使用  &lt;a href=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot;&gt;conventional commits&lt;/a&gt; 有时可以减少思考，让你更专注于更改的内容&lt;/p&gt;
&lt;p&gt;82.当您尝试在一次提交中完成过多操作时，使用  &lt;a href=&quot;https://www.conventionalcommits.org/en/v1.0.0/&quot;&gt;conventional commits&lt;/a&gt; 可以帮助您解决问题&lt;/p&gt;
&lt;p&gt;83.我认为通过写作传达信息有助于理解我做事的原因&lt;/p&gt;
&lt;p&gt;84.写一条好的提交消息比写一份文档要好，文档可以存储在其他地方&lt;/p&gt;
&lt;p&gt;85.写一个好的提交消息比写代码注释要好&lt;/p&gt;
&lt;p&gt;86.给人们学习的空间&lt;/p&gt;
&lt;p&gt;87.给人们失败的空间&lt;/p&gt;
&lt;p&gt;88.请记住，你曾经也不是那么优秀&lt;/p&gt;
&lt;p&gt;89.文档制作很棒。多做一些&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?89"/><enclosure url="http://wallpaper.csun.site/?89"/></item><item><title>给博客添加一个 AI 摘要</title><link>https://blog.csun.site/blog/2024-06-25-add-ai-summary-to-blog</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-06-25-add-ai-summary-to-blog</guid><description>为博客实现AI摘要功能</description><pubDate>Tue, 25 Jun 2024 10:32:00 GMT</pubDate><content:encoded>&lt;p&gt;本文介绍一种基本通用的方法,为博客添加一个酷炫的 AI 摘要功能.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p0.meituan.net/csc/87b07e67ffbda41349ce156e12ad5233126214.png&quot; alt=&quot;undefined&quot;&gt;&lt;/p&gt;
&lt;p&gt;感谢 &lt;a href=&quot;https://linux.do/u/mcenjoy/summary&quot;&gt;@enjoy&lt;/a&gt; 大佬开源的后端代码和 &lt;a href=&quot;https://github.com/qxchuckle&quot;&gt;@qxchuckle&lt;/a&gt; 大佬开源的前端代码,本文在两位大佬的代码基础上修改完成.&lt;/p&gt;
&lt;h2&gt;AI 摘要后端搭建&lt;/h2&gt;
&lt;p&gt;使用 Cloudflare Workers 搭建 AI 摘要的后端,进入 cloudflare 的 Workers 和 Pages,创建 worker,输入下面的代码,然后保存并部署&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function addHeaders(response) {
	response.headers.set(&apos;Access-Control-Allow-Origin&apos;, &apos;*&apos;)
	response.headers.set(&apos;Access-Control-Allow-Credentials&apos;, &apos;true&apos;)
	response.headers.set(
		&apos;Access-Control-Allow-Methods&apos;,
		&apos;GET,HEAD,OPTIONS,POST,PUT&apos;,
	)
	response.headers.set(
		&apos;Access-Control-Allow-Headers&apos;,
		&apos;Origin, X-Requested-With, Content-Type, Accept, Authorization&apos;,
	)
}
async function sha256(message) {
	// encode as UTF-8
	const msgBuffer = await new TextEncoder().encode(message);
	// hash the message
	const hashBuffer = await crypto.subtle.digest(&quot;SHA-256&quot;, msgBuffer);
	// convert bytes to hex string
	return [...new Uint8Array(hashBuffer)]
		.map((b) =&gt; b.toString(16).padStart(2, &quot;0&quot;))
		.join(&quot;&quot;);
}
export default {
	async fetch(request, env, ctx) {
		const url = new URL(request.url);
		if (url.pathname.startsWith(&apos;/api/summary&apos;)) {
			let response
			if (request.method == &apos;OPTIONS&apos;) {
				response = new Response(&apos;&apos;)
				addHeaders(response)
				return response
			}
			if (request.method !== &apos;POST&apos;) {
				return new Response(&apos;error method&apos;, { status: 403 });
			}
			if (url.searchParams.get(&apos;token&apos;) !== env.TOKEN) {
				return new Response(&apos;error token&apos;, { status: 403 });
			}
			let body = await request.json()
			const hash = await sha256(body.content)
			const cache = caches.default
			let cache_summary = await cache.match(`http://objects/${hash}`)
			if (cache_summary) {
				response = new Response(
					JSON.stringify({
						summary: (await cache_summary.json()).choices[0].message.content
					}),
					{ headers: { &apos;Content-Type&apos;: &apos;application/json&apos; } },
				)
				addHeaders(response)
				return response
			}
			const cache_db = await env.DB.prepare(&apos;Select summary from posts where hash = ?&apos;).bind(hash).first(&quot;summary&quot;)
			if (cache_db) {
				response = new Response(
					JSON.stringify({
						summary: cache_db
					}),
					{ headers: { &apos;Content-Type&apos;: &apos;application/json&apos; } },
				)
				addHeaders(response)
				ctx.waitUntil(cache.put(hash, new Response(
					JSON.stringify({
						choices: [
							{
								message: {
									content: cache_db,
								}
							}
						]
					}),
					{ headers: { &apos;Content-Type&apos;: &apos;application/json&apos; } },
				)))
				return response
			}
			const init = {
				body: JSON.stringify({
					&quot;model&quot;: env.MODEL,
					&quot;messages&quot;: [
						{
							&quot;role&quot;: &quot;system&quot;,
							&quot;content&quot;: &quot;你是一个摘要生成工具,你需要解释我发送给你的内容,不要换行,不要超过200字,不要包含链接,只需要简单介绍文章的内容,不需要提出建议和缺少的东西,不要提及用户.请用中文回答,这篇文章讲述了什么?&quot;
						},
						{
							&quot;role&quot;: &quot;user&quot;,
							&quot;content&quot;: body.content
						}
					],
					&quot;safe_mode&quot;: false
				}),
				method: &quot;POST&quot;,
				headers: {
					&quot;content-type&quot;: &quot;application/json;charset=UTF-8&quot;,
					&quot;Authorization&quot;: env.AUTH
				},
			};
			const response_target = await fetch(env.API, init);
			const resp = await response_target.json()
			response = new Response(
				JSON.stringify({
					summary: resp.choices[0].message.content
				}),
				{ headers: { &apos;Content-Type&apos;: &apos;application/json&apos; } },
			)
			ctx.waitUntil(cache.put(`http://objects/${hash}`, response_target))
			await env.DB.prepare(&apos;INSERT INTO posts (hash, summary) VALUES (?1, ?2)&apos;).bind(hash, resp.choices[0].message.content).run()
			addHeaders(response)
			return response
		}
		return new Response(&apos;Hello World!&apos;);
	},
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加环境变量&lt;/h3&gt;
&lt;p&gt;进入 worker 的设置-&gt;变量添加几个环境变量&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1.meituan.net/csc/eed0286cdf839dda3adbf9d575c06d9d25876.png&quot; alt=&quot;undefined&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;API&lt;/code&gt;: openai 或其他镜像站地址, &lt;code&gt;https://xxxx/v1/chat/completions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AUTH&lt;/code&gt;: api key, &lt;code&gt;Bearer sk-*******&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MODEL&lt;/code&gt;: 使用的模型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Token&lt;/code&gt;: 自定义,用于请求生成摘要时鉴权&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;绑定 D1 数据库&lt;/h3&gt;
&lt;p&gt;为了不重复生成摘要,使用了 Cloudflare &lt;code&gt;caches.default&lt;/code&gt;  和  &lt;code&gt;D1&lt;/code&gt;  作为缓存,只有同一篇文章第一次才会消耗&lt;/p&gt;
&lt;p&gt;新建 D1 数据库,创建一个 &lt;code&gt;posts&lt;/code&gt; 表,字段分别为 &lt;code&gt;hash&lt;/code&gt; 和 &lt;code&gt;summary&lt;/code&gt; ,表结构如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p0.meituan.net/csc/418337f0050653172ced0778a1004e8921614.png&quot; alt=&quot;undefined&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 Workers 中绑定 D1 数据库&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p0.meituan.net/csc/8589534daa461f82f39241be83c925de15588.png&quot; alt=&quot;undefined&quot;&gt;&lt;/p&gt;
&lt;h2&gt;前端搭建&lt;/h2&gt;
&lt;p&gt;新建一个 js 文件,粘贴下列代码:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/sun-i/summary/blob/main/summary.js&quot;&gt;summary/summary.js at main · sun-i/summary (github.com)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;并将 381 行的链接修改为上述搭建的后端 woker 链接,token 修改为自定义的 token&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p0.meituan.net/csc/29f12264c59806222f5c1830ffbbba2618077.png&quot; alt=&quot;undefined&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后在 blog 页面内引入下列代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- 可以在网页结构的任何位置插入,只要你能够 --&gt;
&amp;#x3C;script src=&quot;你新建的js文件&quot;&gt;&amp;#x3C;/script&gt;

&amp;#x3C;!-- 但要确保的是,下列代码一定要在上述 js 之后插入 --&gt;
&amp;#x3C;script data-pjax defer&gt;
  new ChucklePostAI({
    // 文章内容所在的元素属性的选择器,也是AI挂载的容器,AI将会挂载到该容器的最前面
    el: &apos;#post&gt;#article-container&apos;,
    summary_directly: true,
    rec_method: &apos;web&apos;,
    // 若网站开启了 PJAX, 则开启
    pjax: true,
  })
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;el&lt;/code&gt; 参数不同 blog 会有不同,可以参考 &lt;a href=&quot;https://postsummary.zhheo.com/theme/custom.html&quot;&gt;通用教程 | TianliGPT (zhheo.com)&lt;/a&gt;获取&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://linux.do/t/topic/119621&quot;&gt;博客 AI 摘要生成 - 软件开发 - LINUX DO&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/qxchuckle/Post-Summary-AI/tree/master&quot;&gt;qxchuckle/Post-Summary-AI: 一个较通用的,生成网站内文章摘要,并推荐相关文章的 AI (github.com)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://postsummary.zhheo.com/theme/custom.html&quot;&gt;通用教程 | TianliGPT (zhheo.com)&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>总结目前国内加速拉取 docker 镜像的几种方法</title><link>https://blog.csun.site/blog/2024-06-13-summary-of-methods-for-accelerating-docker-image-pulls-in-china</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-06-13-summary-of-methods-for-accelerating-docker-image-pulls-in-china</guid><description>国内加速拉取Docker镜像方法总结</description><pubDate>Thu, 13 Jun 2024 23:14:00 GMT</pubDate><content:encoded>&lt;h2&gt;目前仍可用的镜像(随时可能失效)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json &amp;#x3C;&amp;#x3C;-&apos;EOF&apos;
{
    &quot;registry-mirrors&quot;: [
        &quot;https://docker.m.daocloud.io&quot;,
        &quot;https://huecker.io&quot;,
        &quot;https://dockerhub.timeweb.cloud&quot;,
        &quot;https://noohub.ru&quot;
    ]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用 Nginx&lt;/h2&gt;
&lt;p&gt;需要有一台国外服务器, 按下面添加 Nginx 配置即可:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
            listen 443 ssl;
            server_name 域名;

            ssl_certificate 证书地址;
            ssl_certificate_key 密钥地址;

            proxy_ssl_server_name on; # 启用SNI

            ssl_session_timeout 24h;
            ssl_ciphers &apos;ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256&apos;;
            ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;

            location / {
                    proxy_pass https://registry-1.docker.io;  # Docker Hub 的官方镜像仓库

                    proxy_set_header Host registry-1.docker.io;
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    proxy_set_header X-Forwarded-Proto $scheme;

                    # 关闭缓存
                    proxy_buffering off;

                    # 转发认证相关的头部
                    proxy_set_header Authorization $http_authorization;
                    proxy_pass_header  Authorization;

                    # 对 upstream 状态码检查，实现 error_page 错误重定向
                    proxy_intercept_errors on;
                    # error_page 指令默认只检查了第一次后端返回的状态码，开启后可以跟随多次重定向。
                    recursive_error_pages on;
                    # 根据状态码执行对应操作，以下为301、302、307状态码都会触发
                    #error_page 301 302 307 = @handle_redirect;

                    error_page 429 = @handle_too_many_requests;
            }
            #处理重定向
            location @handle_redirect {
                    resolver 1.1.1.1;
                    set $saved_redirect_location &apos;$upstream_http_location&apos;;
                    proxy_pass $saved_redirect_location;
            }
            # 处理429错误
            location @handle_too_many_requests {
                    proxy_set_header Host 替换为在CloudFlare Worker设置的域名;  # 替换为另一个服务器的地址
                    proxy_pass http://替换为在CloudFlare Worker设置的域名;
                    proxy_set_header Host $http_host;
            }
    }


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用代理&lt;/h2&gt;
&lt;p&gt;主要是设置让服务器的 docker 走代理&lt;/p&gt;
&lt;p&gt;首先新建目录和文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;mkdir -p /etc/systemd/system/docker.service.d
vim /etc/systemd/system/docker.service.d/http-proxy.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在文件中粘贴以下内容, 注意代理地址需要换成你自己服务器的内网 ip 和代理端口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;[Service]
Environment=&quot;HTTP_PROXY=http://192.168.8.125:10819&quot;
Environment=&quot;HTTPS_PROXY=http://192.168.8.125:10819&quot;
Environment=&quot;NO_PROXY=your-registry.com,10.10.10.10,*.example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启 docker&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;systemctl daemon-reload
systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查环境变量是否生效&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;systemctl show --property=Environment docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用 Cloudflare 反向代理&lt;/h2&gt;
&lt;p&gt;登录到&lt;a href=&quot;https://dash.cloudflare.com/&quot;&gt;Cloudflare&lt;/a&gt;控制台, 新建 worker, 在 worker.js 文件中输入以下代码, 注意需要自行修改代码中的域名&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&apos;use strict&apos;

const hub_host = &apos;registry-1.docker.io&apos;
const auth_url = &apos;https://auth.docker.io&apos;
const workers_url = &apos;https://你的域名&apos;
const workers_host = &apos;你的域名&apos;
const home_page_url = &apos;https://qninq.cn/file/html/dockerproxy.html&apos;

/** @type {RequestInit} */
const PREFLIGHT_INIT = {
    status: 204,
    headers: new Headers({
        &apos;access-control-allow-origin&apos;: &apos;*&apos;,
        &apos;access-control-allow-methods&apos;: &apos;GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS&apos;,
        &apos;access-control-max-age&apos;: &apos;1728000&apos;,
    }),
}

/**
 * @param {any} body
 * @param {number} status
 * @param {Object&amp;#x3C;string, string&gt;} headers
 */
function makeRes(body, status = 200, headers = {}) {
    headers[&apos;access-control-allow-origin&apos;] = &apos;*&apos;
    return new Response(body, {status, headers})
}


/**
 * @param {string} urlStr
 */
function newUrl(urlStr) {
    try {
        return new URL(urlStr)
    } catch (err) {
        return null
    }
}


addEventListener(&apos;fetch&apos;, e =&gt; {
    const ret = fetchHandler(e)
        .catch(err =&gt; makeRes(&apos;cfworker error:\n&apos; + err.stack, 502))
    e.respondWith(ret)
})


/**
 * @param {FetchEvent} e
 */
async function fetchHandler(e) {
    const getReqHeader = (key) =&gt; e.request.headers.get(key);

    let url = new URL(e.request.url);

    if (url.pathname === &apos;/&apos;) {
        // Fetch and return the home page HTML content with replacement
        let response = await fetch(home_page_url);
        let text = await response.text();
        text = text.replace(/{workers_host}/g, workers_host);
        return new Response(text, {
            status: response.status,
            headers: response.headers
        });
    }

    if (url.pathname === &apos;/token&apos;) {
        let token_parameter = {
            headers: {
                &apos;Host&apos;: &apos;auth.docker.io&apos;,
                &apos;User-Agent&apos;: getReqHeader(&quot;User-Agent&quot;),
                &apos;Accept&apos;: getReqHeader(&quot;Accept&quot;),
                &apos;Accept-Language&apos;: getReqHeader(&quot;Accept-Language&quot;),
                &apos;Accept-Encoding&apos;: getReqHeader(&quot;Accept-Encoding&quot;),
                &apos;Connection&apos;: &apos;keep-alive&apos;,
                &apos;Cache-Control&apos;: &apos;max-age=0&apos;
            }
        };
        let token_url = auth_url + url.pathname + url.search
        return fetch(new Request(token_url, e.request), token_parameter)
    }

    url.hostname = hub_host;

    let parameter = {
        headers: {
            &apos;Host&apos;: hub_host,
            &apos;User-Agent&apos;: getReqHeader(&quot;User-Agent&quot;),
            &apos;Accept&apos;: getReqHeader(&quot;Accept&quot;),
            &apos;Accept-Language&apos;: getReqHeader(&quot;Accept-Language&quot;),
            &apos;Accept-Encoding&apos;: getReqHeader(&quot;Accept-Encoding&quot;),
            &apos;Connection&apos;: &apos;keep-alive&apos;,
            &apos;Cache-Control&apos;: &apos;max-age=0&apos;
        },
        cacheTtl: 3600
    };

    if (e.request.headers.has(&quot;Authorization&quot;)) {
        parameter.headers.Authorization = getReqHeader(&quot;Authorization&quot;);
    }

    let original_response = await fetch(new Request(url, e.request), parameter)
    let original_response_clone = original_response.clone();
    let original_text = original_response_clone.body;
    let response_headers = original_response.headers;
    let new_response_headers = new Headers(response_headers);
    let status = original_response.status;

    if (new_response_headers.get(&quot;Www-Authenticate&quot;)) {
        let auth = new_response_headers.get(&quot;Www-Authenticate&quot;);
        let re = new RegExp(auth_url, &apos;g&apos;);
        new_response_headers.set(&quot;Www-Authenticate&quot;, response_headers.get(&quot;Www-Authenticate&quot;).replace(re, workers_url));
    }

    if (new_response_headers.get(&quot;Location&quot;)) {
        return httpHandler(e.request, new_response_headers.get(&quot;Location&quot;))
    }

    let response = new Response(original_text, {
        status,
        headers: new_response_headers
    })
    return response;

}


/**
 * @param {Request} req
 * @param {string} pathname
 */
function httpHandler(req, pathname) {
    const reqHdrRaw = req.headers

    // preflight
    if (req.method === &apos;OPTIONS&apos; &amp;#x26;&amp;#x26;
        reqHdrRaw.has(&apos;access-control-request-headers&apos;)
    ) {
        return new Response(null, PREFLIGHT_INIT)
    }

    let rawLen = &apos;&apos;

    const reqHdrNew = new Headers(reqHdrRaw)

    const refer = reqHdrNew.get(&apos;referer&apos;)

    let urlStr = pathname

    const urlObj = newUrl(urlStr)

    /** @type {RequestInit} */
    const reqInit = {
        method: req.method,
        headers: reqHdrNew,
        redirect: &apos;follow&apos;,
        body: req.body
    }
    return proxy(urlObj, reqInit, rawLen, 0)
}


/**
 *
 * @param {URL} urlObj
 * @param {RequestInit} reqInit
 */
async function proxy(urlObj, reqInit, rawLen) {
    const res = await fetch(urlObj.href, reqInit)
    const resHdrOld = res.headers
    const resHdrNew = new Headers(resHdrOld)

    // verify
    if (rawLen) {
        const newLen = resHdrOld.get(&apos;content-length&apos;) || &apos;&apos;
        const badLen = (rawLen !== newLen)

        if (badLen) {
            return makeRes(res.body, 400, {
                &apos;--error&apos;: `bad len: ${newLen}, except: ${rawLen}`,
                &apos;access-control-expose-headers&apos;: &apos;--error&apos;,
            })
        }
    }
    const status = res.status
    resHdrNew.set(&apos;access-control-expose-headers&apos;, &apos;*&apos;)
    resHdrNew.set(&apos;access-control-allow-origin&apos;, &apos;*&apos;)
    resHdrNew.set(&apos;Cache-Control&apos;, &apos;max-age=1500&apos;)

    resHdrNew.delete(&apos;content-security-policy&apos;)
    resHdrNew.delete(&apos;content-security-policy-report-only&apos;)
    resHdrNew.delete(&apos;clear-site-data&apos;)

    return new Response(res.body, {
        status,
        headers: resHdrNew
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部署完成后，点击设置-&gt;触发器-&gt;添加自定义域，绑定自己的域名即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p0.meituan.net/csc/a3719b4893c57a0bde64412a04baf3c73415492.png&quot; alt=&quot;undefined&quot;&gt;&lt;/p&gt;
&lt;h2&gt;使用 Github Action + 阿里云私有仓库&lt;/h2&gt;
&lt;p&gt;使用 Github Action 将国外的 Docker 镜像转存到阿里云私有仓库，供国内服务器使用&lt;/p&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/tech-shrimp/docker_image_pusher&quot;&gt;tech-shrimp/docker_image_pusher: 使用 Github Action 将国外的 Docker 镜像转存到阿里云私有仓库，供国内服务器使用，免费易用&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;项目的文档写的很详细，不再赘述&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.lty520.faith/%E5%8D%9A%E6%96%87/%E8%87%AA%E5%BB%BAdocker-hub%E5%8A%A0%E9%80%9F%E9%95%9C%E5%83%8F/&quot;&gt;自建 Docker Hub 加速镜像 (lty520.faith)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/tech-shrimp/docker_image_pusher&quot;&gt;tech-shrimp/docker_image_pusher: 使用 Github Action 将国外的 Docker 镜像转存到阿里云私有仓库，供国内服务器使用，免费易用&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://linux.do/t/topic/107726&quot;&gt;都在蹭 CF 搭建 dockerhub 镜像代理，基于论坛看到的一个代码糊了个前端 - 软件分享 - LINUX DO&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Sun API 使用教程</title><link>https://blog.csun.site/blog/2024-05-22-sun-api-tutorial</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-05-22-sun-api-tutorial</guid><description>低价Sun API使用指南</description><pubDate>Wed, 22 May 2024 00:38:00 GMT</pubDate><content:encoded>&lt;h2&gt;Sun API 介绍&lt;/h2&gt;
&lt;p&gt;Sun API 是一个低价的 gpt 中转 API，支持 gpt3.5 gpt4 Claude3 全系列模型。&lt;/p&gt;
&lt;p&gt;价格优惠，仅需 &lt;strong&gt;0.8元&lt;/strong&gt;即可购买 1 美刀额度，只要官方价格的 &lt;strong&gt;十分之一&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;官方同等计费方式，不限时间，按量计费，明细可查，每一笔消耗都公开透明。&lt;/p&gt;
&lt;p&gt;官网地址：&lt;a href=&quot;https://api.csun.site/&quot;&gt;Sun API (api.csun.site)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;支持模型&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p0.meituan.net/csc/0b74bcb1b78f6750300bef70619f27f320740.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;如何充值&lt;/h2&gt;
&lt;p&gt;支持微信、支付宝付款，前往 &lt;a href=&quot;https://api.csun.site/console/topup&quot;&gt;充值页面&lt;/a&gt; 输入金额，点击相应付款方式，付款成功即可完成充值。
&lt;img src=&quot;https://p0.meituan.net/csc/556bc0c55a3c3f42c26989e552e39e3520706.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;计费规则&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;总的来讲，就是我们后台用美元计费，与 open AI 的模型价格保持一致，折扣体现在充值的时候。目前是 1 折 1 美元只需要 0.8 人民币，详细以&lt;a href=&quot;https://api.csun.site/console/topup&quot;&gt;充值&lt;/a&gt;页面价格为准。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;请求明细查看&lt;/h3&gt;
&lt;p&gt;在本网站的&lt;a href=&quot;https://api.csun.site/console/log&quot;&gt;日志&lt;/a&gt;界面可以查看到每一次调用的明细&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p0.meituan.net/csc/f84271c8850124497c22e8f9d884891422285.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;提示是用户使用时输入到模型的所有信息消耗的 token 数，补全是模型输出的所有信息消耗的 token 数 ，提示和补全都是要扣费的。&lt;/p&gt;
&lt;p&gt;所有模型的计费方式，就是基于消耗的多少token来计算价格。 大部分情况下，你都可以使用 1 汉字 = 2 token 来近似估算中文聊天的中文所需 token 数。但这并不是绝对的，因为不同的字实际token不一样，官方只按token计算。&lt;/p&gt;
&lt;h3&gt;关于倍率&lt;/h3&gt;
&lt;p&gt;倍率是用来计算模型价格的，从而计算额度消耗&lt;/p&gt;
&lt;p&gt;额度消耗 = 分组倍率 * 基准价格 * 模型倍率 * （提示 token 数 + 补全 token 数 * 补全倍率）&lt;/p&gt;
&lt;p&gt;基准价格是 1 美元 50w token&lt;/p&gt;
&lt;p&gt;正常来说用户可以不用管倍率问题，因为在&lt;a href=&quot;https://api.csun.site/pricing&quot;&gt;模型价格&lt;/a&gt;页面已经详细列出了每个模型提示和补全的价格&lt;/p&gt;
&lt;p&gt;额度消耗 = 提示 token 数 * 提示价格 + 补全 token 数 * 补全价格&lt;/p&gt;
&lt;p&gt;将鼠标移到日志界面详情上，也可以查看计算过程&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1.meituan.net/csc/171bad7e9265c4256bd7b74fe7ff4bcc12790.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;如何使用 API&lt;/h2&gt;
&lt;p&gt;使用与官方类似，首先需要获取 api key，即令牌，前往&lt;a href=&quot;https://api.csun.site/console/token&quot;&gt;令牌&lt;/a&gt;页面，点击添加令牌。
&lt;img src=&quot;https://p1.meituan.net/csc/d226b9872fffa4a1895b8e28ed51078534693.png&quot; alt=&quot;image.png&quot;&gt;
设置好令牌名称、过期时间、令牌额度，点击提交即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：令牌额度决定了你这个令牌能使用多少额度，但会受到你的余额限制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://p0.meituan.net/csc/2c2855e7ccdbb871ff0c07b947b9878930501.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;创建令牌后，点击复制按钮，即可复制令牌的值，也就是 api key，令牌形如sk-xxxxxx。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1.meituan.net/csc/389071b5503c1b6c25339763546eadfb24405.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;在需要使用API的平台，将 &lt;code&gt;BASE_URL&lt;/code&gt; 改为中转API调用地址 https://api.csun.site/ ，不同的客户端可能需要填写不同的BASE_URL，请尝试如下地址：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://api.csun.site/&lt;/li&gt;
&lt;li&gt;https://api.csun.site/v1/&lt;/li&gt;
&lt;li&gt;https://api.csun.site/v1/chat/completions&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;在 Chat-Next-Web 中使用&lt;/h3&gt;
&lt;p&gt;访问 &lt;a href=&quot;https://chat.api.csun.site/&quot;&gt;https://chat.csun.site/&lt;/a&gt; （国内稳定访问）或点击侧边栏 聊天&lt;/p&gt;
&lt;p&gt;进入页面后在设置页面勾选自定义接口，模型服务商选择 openai，并将接口地址更改为https://api.csun.site/, API Key 填写上述创建的 API Key。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：本站所有模型都使用 openai 标准接口，即使是使用 claude3 系列模型，模型服务商也要选择 openai&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果有需要使用的模型，但是 Chat-Next-Web 提供的下拉列表里面没有的话，请自定义模型，不同模型之间用英文逗号隔开，如 gpt-4-turbo-2024-04-09,gpt-4o,claude-3-sonnet-20240229,claude-3-opus-20240229,claude-3-haiku-20240307&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p1.meituan.net/csc/1d2bef43db45b91933cc173ccdde207d33650.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后点击新的聊天之间使用即可，模型可点击输入框上方小机器人图标切换。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p0.meituan.net/csc/77b8d15a86b060c2d440e32707d709e889068.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;在沉浸式翻译中使用&lt;/h3&gt;
&lt;p&gt;沉浸式翻译 &lt;a href=&quot;https://immersivetranslate.com/&quot;&gt;https://immersivetranslate.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;一款免费的，好用的，没有废话的，革命性的，饱受赞誉的，AI 驱动的双语网页翻译扩展，你可以完全免费地使用它来实时翻译外语网页，PDF文档，ePub 电子书，字幕文件等。&lt;/p&gt;
&lt;p&gt;在基本设置中翻译服务选择 openai，勾选自定义 API Key，API Key 填写创建的令牌，模型建议选择 gpt-3.5 系列，自定义 API 地址填写 https://api.csun.site/v1/chat/completions&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p0.meituan.net/csc/016dea6d302c6d5ed07c6b30db50554488996.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;在 LangChain 中使用&lt;/h3&gt;
&lt;p&gt;最简单的就是：直接设置环境变量代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;API_SECRET_KEY = &quot;sk-pvMtoVO******66249058b93C766F2D70167&quot; # 你创建的令牌
BASE_URL = &quot;https://api.csun.site/v1&quot;; 

os.environ[&quot;OPENAI_API_KEY&quot;] = API_SECRET_KEY
os.environ[&quot;OPENAI_BASE_URL&quot;] = BASE_URL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：openai_api_base 的末尾要加上 /v1，&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;llm = ChatOpenAI(
    openai_api_base=&quot;https://api.csun.site/v1&quot;, # 注意，末尾要加 /v1
    openai_api_key=&quot;sk-3133f******fee269b71d&quot;,
)

res = llm.predict(&quot;hello&quot;)

print(res)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例代码，使用LLM进行预测&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import requests
import time
import json
import time

from langchain.llms import OpenAI

API_SECRET_KEY = &quot;创建的令牌&quot;;
BASE_URL = &quot;https://api.csun.site/v1&quot;;

def text():
    llm = OpenAI(temperature=0.9)
    text = &quot;What would be a good company name for a company that makes colorful socks?&quot;
    print(llm(text))

if __name__ == &apos;__main__&apos;:
    text();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;在官方 openai 库中使用&lt;/h3&gt;
&lt;p&gt;其他的方式和官方是一样的，只是改一个URL和key用我们的；具体API调用方法请查看官方文档即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from openai import OpenAI

client = OpenAI(
    # #将这里换成你创建的令牌
    api_key=&quot;sk-xxx&quot;,
    # 这里将官方的接口访问地址，替换成 sun api的接口地址
    base_url=&quot;https://api.csun.site/v1&quot;
)

chat_completion = client.chat.completions.create(
    messages=[
        {
            &quot;role&quot;: &quot;user&quot;,
            &quot;content&quot;: &quot;Say this is a test&quot;,
        }
    ],
    model=&quot;gpt-3.5-turbo&quot;,
)

print(chat_completion)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Claude Code 教程&lt;/h2&gt;
&lt;h3&gt;安装 Node.js&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;访问 &lt;a href=&quot;https://nodejs.org/&quot;&gt;https://nodejs.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;下载 LTS 版本的 Windows Installer (.msi)&lt;/li&gt;
&lt;li&gt;运行安装程序，按默认设置完成安装&lt;/li&gt;
&lt;li&gt;安装程序会自动添加到 PATH 环境变量&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;安装 Claude Code CLI&lt;/h3&gt;
&lt;p&gt;以&lt;strong&gt;管理员身份&lt;/strong&gt;打开 CMD 或 PowerShell，执行以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g @anthropic-ai/claude-code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证安装&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置 API&lt;/h3&gt;
&lt;p&gt;访问 &lt;a href=&quot;https://api.csun.site/console/token&quot;&gt;令牌管理&lt;/a&gt; 页面，点击添加令牌，&lt;strong&gt;令牌分组选择&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;claude-code&lt;/code&gt; 或 &lt;code&gt;az-cc&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;claude-code&lt;/code&gt; 分组来自官方的 claude code max 账号，更稳定速度更快但是价格稍贵，1.6 元 = 1 美元额度&lt;/p&gt;
&lt;p&gt;&lt;code&gt;az-cc&lt;/code&gt; 分组来自 azure 的 claude code 渠道，速度略慢，但是价格便宜，0.8 元 = 1 美元额度&lt;/p&gt;
&lt;p&gt;两者都是满血，都支持缓存，按需选择&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后配置 claude 的环境变量，打开 &lt;code&gt;%USERPROFILE%\.claude\settings.json&lt;/code&gt; 文件，添加以下配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;env&quot;: {
    &quot;ANTHROPIC_AUTH_TOKEN&quot;: &quot;添加的令牌&quot;,
    &quot;ANTHROPIC_BASE_URL&quot;: &quot;https://api.csun.site/&quot;,
    &quot;API_TIMEOUT_MS&quot;: &quot;600000&quot;,
    &quot;BASH_DEFAULT_TIMEOUT_MS&quot;: &quot;600000&quot;,
    &quot;BASH_MAX_TIMEOUT_MS&quot;: &quot;600000&quot;,
    &quot;MCP_TIMEOUT&quot;: &quot;30000&quot;,
    &quot;MCP_TOOL_TIMEOUT&quot;: &quot;600000&quot;,
    &quot;CLAUDE_API_TIMEOUT&quot;: &quot;600000&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动 Claude Code&lt;/h3&gt;
&lt;p&gt;配置完成后，先进入到工程目录，然后运行以下命令启动&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Codex 教程&lt;/h2&gt;
&lt;h3&gt;安装 Node.js&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;访问 &lt;a href=&quot;https://nodejs.org/&quot;&gt;https://nodejs.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;下载 LTS 版本的 Windows Installer (.msi)&lt;/li&gt;
&lt;li&gt;运行安装程序，按默认设置完成安装&lt;/li&gt;
&lt;li&gt;安装程序会自动添加到 PATH 环境变量&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;安装 CodeX CLI&lt;/h3&gt;
&lt;p&gt;以&lt;strong&gt;管理员身份&lt;/strong&gt;打开 CMD 或 PowerShell，执行以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g @openai/codex@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;验证安装：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codex --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置 API&lt;/h3&gt;
&lt;h4&gt;获取令牌&lt;/h4&gt;
&lt;p&gt;访问 &lt;a href=&quot;https://api.csun.site/console/token&quot;&gt;令牌管理&lt;/a&gt; 页面，点击添加令牌，&lt;strong&gt;令牌分组选择&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;codex&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;创建配置文件夹&lt;/h4&gt;
&lt;p&gt;在 &lt;code&gt;%USERPROFILE%&lt;/code&gt; 目录下，创建 &lt;code&gt;.codex&lt;/code&gt; 文件夹&lt;/p&gt;
&lt;h4&gt;创建配置文件&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;在 &lt;code&gt;.codex&lt;/code&gt; 文件夹下创建创建 config.toml 文件：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model_provider = &quot;sunapi&quot;
model = &quot;gpt-5.1-codex&quot;
model_reasoning_effort = &quot;high&quot;
network_access = &quot;enabled&quot;
disable_response_storage = true
windows_wsl_setup_acknowledged = true

[model_providers.sunapi]
name = &quot;sunapi&quot;
base_url = &quot;https://api.csun.site/v1&quot;
wire_api = &quot;responses&quot;
requires_openai_auth = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;在同一目录下创建 auth.json 文件：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;OPENAI_API_KEY&quot;: &quot;粘贴为CodeX专用分组令牌key&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动 CodeX&lt;/h3&gt;
&lt;p&gt;配置完成后，先进入到工程目录，然后运行以下命令启动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;codex
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?rand=true"/><enclosure url="http://wallpaper.csun.site/?rand=true"/></item><item><title>Word 图表自动编号</title><link>https://blog.csun.site/blog/2024-05-16-word-chart-auto-numbering</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-05-16-word-chart-auto-numbering</guid><description>Word图表自动编号技巧</description><pubDate>Thu, 16 May 2024 16:49:00 GMT</pubDate><content:encoded>&lt;p&gt;最近忙着写论文，苦于图表一旦有所增加或删除就要全部重新编号，就研究了下 Word 怎么对图表进行自动编号。&lt;/p&gt;
&lt;h2&gt;设置章节编号&lt;/h2&gt;
&lt;p&gt;一般来说，图表编号是以章节为分割，例如「图 1-1」代表第二章第一张图，所以为了设置每一章节的编号与章节相关联，我们先要设置章节编号。&lt;/p&gt;
&lt;p&gt;选中我们的章节标题，将鼠标移动到 样式&gt;标题1，在下拉菜单中选择「更新 标题 1 以匹配所选内容」，将其设置为标题1&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162041149.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后点击列表，在下拉菜单中点击「定义新的多级列表」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162044030.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;在弹出的窗口中点击「更多」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162046969.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后设置「将级别链接到样式」为标题1，并设置好我们需要的编号样式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162048593.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;设置图表编号&lt;/h2&gt;
&lt;p&gt;插入一张图片，选中图片，在引用里面选择「插入题注」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162055907.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;标签设置为「图表」，位置选择「所选项目下方」，点击编号，勾选「包含章节号」，章节起始样式选择「标题 1 」，分隔符根据自己需要设置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162054956.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;点击确认之后，可以发现在我们的图片下方已经插入了一条编号，我们还可以修改图片标注和格式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162059051.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是如果我们设置的章节编号样式是汉字「一、二、三……」，就会出现编号是「一.1」，而不是我们想要的「1.1」。&lt;/p&gt;
&lt;h2&gt;将「一.1」转换为「1.1」&lt;/h2&gt;
&lt;p&gt;本质上，插入题注插入的其实是 word 的&lt;strong&gt;域代码&lt;/strong&gt;，选中图片编号，点击 &lt;code&gt;Alt+F9&lt;/code&gt; 就可以查看其域代码。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162103594.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来我们就对这行域代码进行修改，将「一.1」转换为 「1.1」&lt;/p&gt;
&lt;p&gt;将 &lt;code&gt;{STYLEREF 1 \s}&lt;/code&gt; 修改为 &lt;code&gt;{ QUOTE &quot;一九一一年一月{ STYLEREF 1 \s }日&quot; \@&quot;D&quot; }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：这里所有的 &lt;code&gt;{}&lt;/code&gt; 都需要使用 &lt;code&gt;Ctrl+F9&lt;/code&gt; 生成，不能手打！！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162109968.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后选中域代码，按 &lt;code&gt;Alt+F9&lt;/code&gt; 恢复成原来的图片编号，再点击 &lt;code&gt;F9&lt;/code&gt; 更新域，即可将 「一.1」转换为「1.1」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162111073.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;优化&lt;/h2&gt;
&lt;p&gt;每次这样操作将会过于繁琐，word提供了另一个好用的功能——构建基块。&lt;/p&gt;
&lt;p&gt;选中我们刚刚写好的图片标号，按下 &lt;code&gt;Alt+F3&lt;/code&gt;，会弹出一个新建构建基块窗口，设置构建基块名称为图注，其他保持默认即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162113198.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后我们再插入一张图片，在需要插入图片编号时输入我们刚才设置的构建基块名称 「图注」，word 会提醒我们按 「Enter 键插入」&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162117498.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;按下 &lt;code&gt;Enter&lt;/code&gt; 键，神奇的事情发生了，word 自动给我插入了一个图片编号，然后我们只需要修改内容即可，编号是自动编好的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162118164.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;交叉引用&lt;/h2&gt;
&lt;p&gt;一般在论文中，我们需要写上「如图1.2所示」，为了同步更新这个和相应图片的编号，我们可以使用交叉引用。&lt;/p&gt;
&lt;p&gt;点击 引用&gt;交叉引用，设置引用类型为「图表」，引用内容 「仅标签和编号」，选择我们需要的图片，点击插入即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405162123313.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;当我们写完所有的内容之后，可以 &lt;code&gt;Ctrl+A&lt;/code&gt; 全选内容，按下 &lt;code&gt;F9&lt;/code&gt; 键更新域，就会自动更新所有的图表编号，保证不会出现错乱。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?word"/><enclosure url="http://wallpaper.csun.site/?word"/></item><item><title>vercel 部署 Nextra 文档站点</title><link>https://blog.csun.site/blog/2024-05-13-vercel-deploy-nextra-docs-site</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-05-13-vercel-deploy-nextra-docs-site</guid><description>Vercel 部署 Nextra 文档站</description><pubDate>Mon, 13 May 2024 02:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Nextra 是 Next.js 上的一个框架，可构建以内容为重点的网站。它拥有 Next.js 的所有强大功能，还能轻松创建基于 Markdown 的内容。Nextra Docs Theme 是一款包含几乎所有现代文档网站所需内容的主题，包括顶部导航栏、搜索栏、页面侧边栏、TOC 侧边栏和其他内置组件等，使用 Nextra + vercel 可以轻松搭建起一个文档站。&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;p&gt;fork Nextra 的仓库 &lt;a href=&quot;https://github.com/shuding/nextra-docs-template&quot;&gt;shuding/nextra-docs-template: Nextra docs template (github.com)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405130009642.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;fork 完成后，打开 &lt;a href=&quot;https://vercel.com/&quot;&gt;vercel&lt;/a&gt;，切换到 &lt;code&gt;Overview&lt;/code&gt; 页面，点击 &lt;code&gt;Add New...&lt;/code&gt;，选择 &lt;code&gt;Project&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405111119097.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Import Git Repository&lt;/code&gt; 中选择我们刚刚 fork 的仓库，点击 &lt;code&gt;Import&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405130010055.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来点击 &lt;code&gt;Deploy&lt;/code&gt; 按钮等待部署完成即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405130018527.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;部署完成后点击 &lt;code&gt;Continue to Dashboard&lt;/code&gt;，可以看到 vercel 为我们提供的域名。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405130019584.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;打开这个域名，即可访问我们搭建的文档站点了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405130020051.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;由于 vercel 检测到仓库更新自动重新部署时会使用上一次部署的缓存，会导致 Nextra 首页的侧边栏有时无法更新，为了解决这一问题，添加一个环境变量使得每次部署时不使用上一次的缓存。&lt;/p&gt;
&lt;p&gt;前往 Setting&gt;Environment Variables，在 Value 中输入 &lt;code&gt;1&lt;/code&gt;，然后在 Key 中输入 &lt;code&gt;VERCEL_FORCE_NO_BUILD_CACHE&lt;/code&gt;，点击 `Save 按钮。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/image-20240515150556101.png&quot; alt=&quot;image-20240515150556101&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后前往 Deployments 页面点击 &lt;code&gt;Redeploy&lt;/code&gt; 重新部署即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/image-20240515150735255.png&quot; alt=&quot;image-20240515150735255&quot;&gt;&lt;/p&gt;
&lt;h2&gt;自定义域名&lt;/h2&gt;
&lt;p&gt;由于 vercel 提供的域名被墙了，为了提供国内访问，我们需要绑定自己的域名。&lt;/p&gt;
&lt;p&gt;切换到 Setting&gt;Domains 页面，在输入框中输入我们的域名，点击 &lt;code&gt;Add&lt;/code&gt; 按钮。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405130023142.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个时候 vercel 会要求我们验证域名的所有权，将域名 CNAME 到指定 url。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405130025729.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;来到我们的域名 DNS 解析界面，这里以 cf 为例，添加 CNAME 记录，目标指向 vercel 给定的 url。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405130026236.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;等待 DNS 解析完成后，回到 vercel 就可以看到域名已经绑定成功了，以后就可以使用这个域名访问站点了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405130029216.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;修改配置&lt;/h2&gt;
&lt;p&gt;参考官方文档 &lt;a href=&quot;https://nextra.site/docs/&quot;&gt;https://nextra.site/docs/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;添加文档&lt;/h2&gt;
&lt;p&gt;将写好的 md / mdx 文档放到 &lt;code&gt;pages&lt;/code&gt; 页面中，即可新增文档。&lt;/p&gt;
&lt;p&gt;提交到 github 仓库后，vercel 会自动拉取更新，重新部署。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?mextra"/><enclosure url="http://wallpaper.csun.site/?mextra"/></item><item><title>vercel 部署网站统计工具 Umami</title><link>https://blog.csun.site/blog/2024-05-11-vercel-deploy-website-statistics-tool-umami</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-05-11-vercel-deploy-website-statistics-tool-umami</guid><description>Vercel 部署 Umami 统计工具</description><pubDate>Sat, 11 May 2024 10:28:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://umami.is/&quot;&gt;Umami&lt;/a&gt; 是一款开源网站统计工具，可以通过插入一行前端代码来实现网站访问量统计。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405111107640.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;本文介绍如何使用 vercel 部署 Umami。&lt;/p&gt;
&lt;h2&gt;Fork Umami 官方仓库&lt;/h2&gt;
&lt;p&gt;Umami 的官方仓库地址：&lt;a href=&quot;https://github.com/umami-software/umami&quot;&gt;umami-software/umami: Umami is a simple, fast, privacy-focused alternative to Google Analytics. (github.com)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;点击 &lt;code&gt;Fork&lt;/code&gt;，将该仓库 Fork 到自己的 github 账号中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405111109196.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;创建数据库&lt;/h2&gt;
&lt;p&gt;Umami 需要数据库，支持 postgresql、mysql 等数据库，这里我们使用 vercel 提供的 postgresql 数据库服务。&lt;/p&gt;
&lt;p&gt;登录&lt;a href=&quot;https://vercel.com/&quot;&gt;vercel&lt;/a&gt;，切换到 &lt;code&gt;Storage&lt;/code&gt; 界面。点击 &lt;code&gt;Create Database&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405111112010.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;选择 &lt;code&gt;Postgres&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405111115150.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;出现以下界面则说明创建成功，复制 postgres url&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405111117548.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;部署项目&lt;/h2&gt;
&lt;p&gt;切换到 &lt;code&gt;Overview&lt;/code&gt; 页面，点击 &lt;code&gt;Add New...&lt;/code&gt;，选择 &lt;code&gt;Project&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405111119097.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Import Git Repository&lt;/code&gt; 中选择我们刚刚 fork 的仓库，点击 &lt;code&gt;Import&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405111120755.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Environment Variables&lt;/code&gt; 中添加环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Key：DATABASE_URL
Value (Will Be Encrypted): 刚才复制的 postgres url
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405111123097.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;点击 &lt;code&gt;Deploy&lt;/code&gt;，等待部署完成即可，大约需要两分钟。部署完成后会显示 Congratulation 页面，点击右上角 &lt;code&gt;Go to Dashboard&lt;/code&gt; 可以看到 vercel 提供的访问域名。&lt;/p&gt;
&lt;p&gt;访问该域名，打开 Umami，初次登录输入默认用户名 &lt;code&gt;admin&lt;/code&gt; 与默认密码 &lt;code&gt;umami&lt;/code&gt;，即可使用网站统计服务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405111129223.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?umami"/><enclosure url="http://wallpaper.csun.site/?umami"/></item><item><title>白嫖 Kaggle 部署 stable-diffusion</title><link>https://blog.csun.site/blog/2024-05-04-free-kaggle-deploy-stable-diffusion</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-05-04-free-kaggle-deploy-stable-diffusion</guid><description>免费部署稳定扩散模型</description><pubDate>Sat, 04 May 2024 00:10:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.kaggle.com/&quot;&gt;Kaggle&lt;/a&gt; 每周有 30 个小时的免费 GPU 资源，可以使用 Kaggle 来部署 stable-diffusion 免费享受 AI 绘画服务。&lt;/p&gt;
&lt;h2&gt;部署代码&lt;/h2&gt;
&lt;p&gt;注册 Kaggle 后，打开这个链接：&lt;a href=&quot;https://www.kaggle.com/code/sungpu/stable-diffusion-webui&quot;&gt;stable-diffusion-webui (kaggle.com)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;点击左上角 &lt;code&gt;Cpoy &amp;#x26; Edit&lt;/code&gt; 按钮&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405032358098.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;在打开的页面侧边栏 &lt;code&gt;Session options&lt;/code&gt; 中按照下图所示设置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405032359052.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;配置内网穿透&lt;/h2&gt;
&lt;p&gt;由于 Kaggle 没有提供外网访问的端口，所以需要配置内外穿透工具，这里使用 ngrok。&lt;/p&gt;
&lt;p&gt;通过这个链接获取 authtoken &lt;a href=&quot;https://dashboard.ngrok.com/get-started/your-authtoken&quot;&gt;Your Authtoken - ngrok&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405040004627.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后将 authtoken 填入到下图所示位置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405040005461.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;添加模型&lt;/h2&gt;
&lt;p&gt;如果需要添加模型，可以将模型的下载链接填入到下图所示位置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405040006464.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;即可在启动后自动下载模型.&lt;/p&gt;
&lt;p&gt;部分模型可能无法下载，可以手动上传到 kaggle 的数据集，然后在下图位置设置数据集路径&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405040007800.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;启动&lt;/h2&gt;
&lt;p&gt;点击工具栏 &lt;code&gt;Run -&gt; Run all&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405040008029.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;等待下载相关依赖和模型文件，这个过程可能需要十几分钟，然后浏览器打开下图所示的链接即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405040009603.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?21"/><enclosure url="http://wallpaper.csun.site/?21"/></item><item><title>自动备份数据库到阿里云盘</title><link>https://blog.csun.site/blog/2024-05-03-automatic-backup-database-to-alicloud-drive</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-05-03-automatic-backup-database-to-alicloud-drive</guid><description>数据库自动备份至阿里云盘</description><pubDate>Fri, 03 May 2024 17:41:00 GMT</pubDate><content:encoded>&lt;p&gt;为了保证数据不丢失，需要定时备份数据，但是如果仅仅是将数据库备份到服务器本地，万一服务器数据损坏，依然无法恢复数据库，本文介绍一种将数据库备份到阿里云盘的方法，保障数据不会丢失。&lt;/p&gt;
&lt;h2&gt;配置阿里云盘&lt;/h2&gt;
&lt;h3&gt;安装阿里云盘客户端&lt;/h3&gt;
&lt;p&gt;使用下面的一键安装脚本，安装阿里云盘客户端&lt;/p&gt;
&lt;h4&gt;Debian / Ubuntu&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;sudo curl -fsSL http://file.tickstep.com/apt/pgp | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg &gt; /dev/null &amp;#x26;&amp;#x26; echo &quot;deb [signed-by=/etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg arch=amd64,arm64] http://file.tickstep.com/apt aliyunpan main&quot; | sudo tee /etc/apt/sources.list.d/tickstep-aliyunpan.list &gt; /dev/null &amp;#x26;&amp;#x26; sudo apt-get update &amp;#x26;&amp;#x26; sudo apt-get install -y aliyunpan
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Centos&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;sudo curl -fsSL http://file.tickstep.com/rpm/aliyunpan/aliyunpan.repo | sudo tee /etc/yum.repos.d/tickstep-aliyunpan.repo &gt; /dev/null &amp;#x26;&amp;#x26; sudo yum install aliyunpan -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;登录云盘&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;aliyunpan login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在浏览器中打开输出的网址，扫码登录即可&lt;/p&gt;
&lt;h2&gt;设置自动备份&lt;/h2&gt;
&lt;h3&gt;自动备份数据库到本地&lt;/h3&gt;
&lt;p&gt;使用定时任务和 &lt;code&gt;mysqldump&lt;/code&gt; 定时备份数据库到本地&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;crontab -e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在文件末尾添加：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;0 * * * * mysqldump -u &amp;#x3C;username&gt; -p&amp;#x3C;password&gt; &amp;#x3C;database_name&gt; &gt; /path/backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述定时任务会每个小时备份数据库一次&lt;/p&gt;
&lt;h3&gt;自动上传到阿里云盘&lt;/h3&gt;
&lt;p&gt;同样使用定时任务，在文件末尾添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;30 * * * * /bin/bash -c &apos;aliyunpan upload /path//backup/ /backup/$(date +\%Y\%m\%d)&apos;

0 20 * * * /bin/bash -c &apos;aliyunpan rm /backup/$(date --date=&quot;1 days ago&quot; +\%Y\%m\%d)&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样每小时会自动上传备份文件到阿里云盘，第二行每天会自动删除前一天的备份文件，避免文件冗余。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?aliyun"/><enclosure url="http://wallpaper.csun.site/?aliyun"/></item><item><title>如何使用 vercel 部署旧版开源项目</title><link>https://blog.csun.site/blog/2024-05-02-how-to-deploy-legacy-open-source-projects-with-vercel</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-05-02-how-to-deploy-legacy-open-source-projects-with-vercel</guid><description>Vercel 部署旧版项目技巧</description><pubDate>Thu, 02 May 2024 21:54:00 GMT</pubDate><content:encoded>&lt;p&gt;事情的起因是 &lt;a href=&quot;https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web&quot;&gt;ChatGPT-Next-Web&lt;/a&gt; 这一开源项目更新到 v2.12.2 版本时，默认 Claude3 的请求走官方接口，导致无法使用 one-api、new-api 等中转的 Claude3 服务。而使用 vercel 部署时，会自动拉取最新版本的代码，无法指定版本，导致无法部署旧版本的项目。&lt;/p&gt;
&lt;p&gt;那么我们如何能够使用 vercel 部署旧版开源项目呢？其实很简单，一般开源项目都是通过 &lt;code&gt;tag&lt;/code&gt; 来管理不同版本的代码的，我们只需要根据相应版本的 &lt;code&gt;tag&lt;/code&gt; 创建分支，然后让 vercel 拉取指定分支的代码即可。&lt;/p&gt;
&lt;p&gt;首先，我们 fork 该仓库并将 fork 后的仓库 clone 到本地。&lt;/p&gt;
&lt;p&gt;然后查看所有的 &lt;code&gt;tag&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git tag
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来根据先要的 &lt;code&gt;tag&lt;/code&gt; 来创建新分支&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout tags/&amp;#x3C;tag_name&gt; -b &amp;#x3C;branch_name&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后推送到远程仓库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push origin &amp;#x3C;branch_name&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后在 vercel 的项目 setting 中修改拉取的分支名称即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/202405021748112.png&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?ru"/><enclosure url="http://wallpaper.csun.site/?ru"/></item><item><title>程序语言中分号的起源和优点</title><link>https://blog.csun.site/blog/2024-04-29-origin-and-advantages-of-semicolon-in-programming-languages</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-04-29-origin-and-advantages-of-semicolon-in-programming-languages</guid><description>分号在编程语言中的重要性</description><pubDate>Mon, 29 Apr 2024 10:16:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文译自：&lt;a href=&quot;https://www.ntietz.com/blog/researching-why-we-use-semicolons-as-statement-terminators/&quot;&gt;The origin and virtues of semicolons in programming languages | nicole@web&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在为我的编程语言 Lilac 编写语法时，我正在探索语句终止符的不同选择。 &lt;code&gt;.&lt;/code&gt; 很有吸引力，或者 &lt;code&gt;!&lt;/code&gt; 。最终，我可能会做出 &quot;无聊 &quot;的选择，即使用 &lt;code&gt;;&lt;/code&gt; 或大量空白。&lt;/p&gt;
&lt;p&gt;但我不禁要问：为什么这么多语言的语句终止符都使用分号？我找到了一些关于为什么要使用语句结束符的好文章，但很少有人讨论分号相对于其他选择的具体优点。&lt;/p&gt;
&lt;p&gt;为了弄清分号在编程语言中的起源，我查阅了历史资料。早期的编程语言非常少，因此向前追溯并查看所有早期语言相对容易。这样，我们就能找到第一种将分号作为语句分隔符的语言：&lt;a href=&quot;https://en.wikipedia.org/wiki/ALGOL_58&quot;&gt;ALGOL 58&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;在 ALGOL 之前，语言通常使用空白来标记语句，每条语句都在自己的行上。ALGOL 引入了语句分隔符，使程序员可以更灵活地将多条语句放在一行，或将一条语句分散到多行。遗憾的是，当我们深入探究为什么要使用分号时，答案并不多！最初的相关论文只是描述了分号是语句分隔符，却没有说明为什么要使用分号。&lt;/p&gt;
&lt;p&gt;那我们该怎么办呢？那就是去猜测！&lt;/p&gt;
&lt;h2&gt;猜测&lt;/h2&gt;
&lt;p&gt;有几个原因可以解释我们为什么会使用分号，或者它为什么会出现在我们语言中的某个地方。这些都是猜测，但这些猜测是正确的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可用&lt;/strong&gt;。早期计算机的字符集非常有限，而分号通常是可用的。一些早期的输入设备是由雷明顿键盘改装而成的，这些设备（根据我能找到的图片）确实包含分号和冒号。这是有道理的，因为如果要输入英文文本，可能偶尔会遇到分号！这不是最常用的标点符号，但很有用。既然它存在，就一定会出现在语言的某个地方，因为我们可选择的字符很少。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方便&lt;/strong&gt;。在现代键盘上，分号位于主行，不需要换挡，我想这也是分号一直被广泛使用的部分原因。 在主行的位置使其非常容易输入，因此与 &lt;code&gt;!&lt;/code&gt; 这样需要敲两下并伸展一下的分号相比，您只需用右手小指就能输入 &lt;code&gt;;&lt;/code&gt; 。说到这，分号是主要的，而更常用的冒号需要移位，这不是很奇怪吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其用法与英语相似&lt;/strong&gt;。在英语中，分号的作用之一是划分独立分句；这些分句是句子中可以独立存在但又密切相关的部分。这与语句分隔符的作用非常相似。更相似的是 &lt;code&gt;.&lt;/code&gt; ，因为每个语句都可以被看作是一个句子，但这也给了我们另一个喜欢用分号的理由。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不太可能发生冲突&lt;/strong&gt;。如果使用句号，即不起眼的 &lt;code&gt;.&lt;/code&gt; ，一不小心就会在解析时遇到困难。正如我的朋友最近对我说的那样，句号是一个价值很高的符号，你必须明智地选择它的用途。在现代语言中，我们用它来访问字段和方法，定义浮点字面量，以及使用范围运算符和展开运算符。相比之下，分号......除了偶尔用于注释的开头，其他地方都没有。&lt;/p&gt;
&lt;p&gt;这些都是选择分号作为语句分隔符的令人信服的理由！你能选择什么来代替呢？在所有候选项中， &lt;code&gt;!@#$%^&amp;#x26;*,./;:|-_&lt;/code&gt; ，我想不出一个明显更好的选择！如果你能解决解析问题，我个人可能更倾向于 &lt;code&gt;.&lt;/code&gt; ；如果你想要一种令人兴奋的语言， &lt;code&gt;!&lt;/code&gt; 可能会非常有趣，但谦逊的 &lt;code&gt;;&lt;/code&gt; 似乎是一个可靠的好决定，而不仅仅是为了连续性。&lt;/p&gt;
&lt;p&gt;至于我在我的编程语言Lilac中做什么？我还不完全确定！分号是安全的选择，但其他选择（或根本没有分号）也有美学上的吸引力。我很想听听在你的构想中，你会如何选择！&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?chen"/><enclosure url="http://wallpaper.csun.site/?chen"/></item><item><title>jetbrains 全家桶激活</title><link>https://blog.csun.site/blog/2024-04-12-jetbrains-all-in-one-activation</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-04-12-jetbrains-all-in-one-activation</guid><description>JetBrains全家桶激活指南</description><pubDate>Fri, 12 Apr 2024 21:22:12 GMT</pubDate><content:encoded>&lt;p&gt;访问链接&lt;a href=&quot;https://3.jetbra.in/&quot;&gt;JETBRA.IN CHECKER | IPFS&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/imagesimage-20240411100319420.png&quot; alt=&quot;image-20240411100319420&quot;&gt;&lt;/p&gt;
&lt;p&gt;挑选一个存活链接，点击进入下述页面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/imagesimage-20240411100447549.png&quot; alt=&quot;image-20240411100447549&quot;&gt;&lt;/p&gt;
&lt;p&gt;点击左上角的&lt;code&gt;jetbra.zip&lt;/code&gt;下载激活工具并解压&lt;/p&gt;
&lt;p&gt;使用 vscode 或记事本打开 &lt;code&gt;C:\Users\用户名\AppData\Roaming\JetBrains\产品名\产品名.exe.vmoptions&lt;/code&gt;文件&lt;/p&gt;
&lt;p&gt;在文件末尾添加 &lt;code&gt;-javaagent:/path/to/ja-netfilter.jar=jetbrains&lt;/code&gt;，保存文件&lt;/p&gt;
&lt;p&gt;或者解压&lt;code&gt;jetbra.zip&lt;/code&gt;后，双击&lt;code&gt;scripts\install-all-users.vbs&lt;/code&gt; 或 &lt;code&gt;scripts\install-current-users.vbs&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;scripts\install-all-users.vbs&lt;/code&gt;: 为所有用户安装&lt;/p&gt;
&lt;p&gt;&lt;code&gt;scripts\install-all-users.vbs&lt;/code&gt;: 为当前用户安装&lt;/p&gt;
&lt;p&gt;选择哪个取决于安装 IDE 时选择的方式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;执行此脚本会在 IDE 的 vmoptions 文件中添加 &lt;code&gt;-javaagent:/path/to/ja-netfilter.jar=jetbrains&lt;/code&gt;，&lt;strong&gt;但该方法有一定概率会失效&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后打开 IDE，这里以 IDEA 为例，复制页面中的激活码粘贴到 IDEA 中，点击 Activate 即可激活成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/imagesimage-20240411101716160.png&quot; alt=&quot;image-20240411101716160&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/imagesimage-20240411101519569.png&quot; alt=&quot;image-20240411101519569&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/imagesimage-20240411101524380.png&quot; alt=&quot;image-20240411101524380&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;激活成功后显示的过期时间仅仅是个示例，理论上该许可证永不过期&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?je"/><enclosure url="http://wallpaper.csun.site/?je"/></item><item><title>各类教育优惠总结</title><link>https://blog.csun.site/blog/2024-03-21-various-education-discounts-summary</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-03-21-various-education-discounts-summary</guid><description>教育优惠汇总与使用指南</description><pubDate>Thu, 21 Mar 2024 23:14:12 GMT</pubDate><content:encoded>&lt;p&gt;本文将介绍利用我们的学生身份、教育邮箱可以享受到的各类教育优惠，希望大家享受优惠的同不要滥用、售卖、转手自己的学生优惠资格，以免影响后续他人使用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下优惠均可能随着时间推移而改变，详细规则请看优惠详情页面。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;学生包&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://education.github.com/pack&quot;&gt;Github 学生包&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://education.github.com/pack&lt;/p&gt;
&lt;p&gt;高质量的开发工具合集，&lt;strong&gt;强烈推荐&lt;/strong&gt;，可享受 Github Copilot 等 Github 福利，并提供 DIgitalocean 的 100 美元代金券，免费域名一枚。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于 Github 学生包被大量滥用，倒卖，现在申请十分困难，需要在学校附近的 ip，最好使用校园网，除了教育邮箱外，还需要准备英文在读证明（南昌大学教务管理系统公众号可申请）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.jetbrains.com/student/&quot;&gt;JetBrains 学生包&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://www.jetbrains.com/student/&lt;/p&gt;
&lt;p&gt;免费使用 JetBrains 旗下各类开发工具，如 IDEA、PyCharm 等，&lt;strong&gt;强烈推荐&lt;/strong&gt;，这些强大的开发工具将在我们的学习、工作中发挥巨大作用。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://www.navicat.com/en/sponsorship/education/student&quot;&gt;Navicat 学术伙伴计划&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://www.navicat.com/en/sponsorship/education/student&lt;/p&gt;
&lt;p&gt;提供一年的 Navicat Premium 非商业授权，这是一款强大的图形化数据库管理工具。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/azure/education-hub/azure-dev-tools-teaching/program-faq&quot;&gt;Azure 学生包&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://learn.microsoft.com/zh-cn/azure/education-hub/azure-dev-tools-teaching/program-faq&lt;/p&gt;
&lt;p&gt;免费使用微软的各种开发软件，包括 Windows Server 系统，以及 Azure 学生订阅。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://aws.amazon.com/cn/education/awseducate/&quot;&gt;AWS 学生包&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://aws.amazon.com/cn/education/awseducate/&lt;/p&gt;
&lt;p&gt;亚马逊云计算的学生包，可以获得至少 $40 的优惠和教育培训课程。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;http://www.autodesk.com.cn/education/home&quot;&gt;Autodesk 学生包&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://aws.amazon.com/cn/education/awseducate/&lt;/p&gt;
&lt;p&gt;AutoCAD , 3DMAX , Maya 等软件，其（教育版）免费试用期可延长至三年。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;http://www.lindo.com/index.php?option=com_content&amp;#x26;view=article&amp;#x26;id=120&amp;#x26;Itemid=45&quot;&gt;LINGO Educational Research License&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：http://www.lindo.com/index.php?option=com_content&amp;#x26;view=article&amp;#x26;id=120&amp;#x26;Itemid=45&lt;/p&gt;
&lt;p&gt;LINGO 教育授权，著名线性与非线性求解器，求解优化模型的最佳选择，数学建模必备。&lt;/p&gt;
&lt;h2&gt;云服务优惠&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.aliyun.com/plan/grow-up?userCode=oitiwrd3&quot;&gt;阿里云高校计划&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://developer.aliyun.com/plan/grow-up?userCode=oitiwrd3&lt;/p&gt;
&lt;p&gt;阿里云的高校计划，提供高配云服务器7个月的免费使用和系列免费云计算课程+实验。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://cloud.tencent.com/act/cps/redirect?redirect=10004&amp;#x26;cps_key=6df162ea83f16c8735081aa3200eef38&quot;&gt;腾讯云+校园计划&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://cloud.tencent.com/act/cps/redirect?redirect=10004&amp;#x26;cps_key=6df162ea83f16c8735081aa3200eef38&lt;/p&gt;
&lt;p&gt;2C2G4M 云服务器 112元/年，还有云数据库和域名等优惠， 25岁及以下免学生认证 (活动规则多建议买之前先看一下) ，变更配置容易失去资格。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://www.ctyun.cn/activity/#/campus&quot;&gt;天翼云+云创校园&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://www.ctyun.cn/activity/#/campus&lt;/p&gt;
&lt;p&gt;服务器优惠，具体看活动详情&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.huaweicloud.com/campus&quot;&gt;华为云+云创校园计划&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://developer.huaweicloud.com/campus&lt;/p&gt;
&lt;p&gt;9元/月 24岁及以下免学生认证，实名认证即可购买，需要抢购。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://cloud.baidu.com/campaign/campus-2018/index.html&quot;&gt;百度云启航校园计划&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://cloud.baidu.com/campaign/campus-2018/index.html&lt;/p&gt;
&lt;p&gt;74元/半年，1C2G1M40G，143元/半年，2C4G1M40G。24岁及以下免学生认证。&lt;/p&gt;
&lt;h2&gt;硬件优惠&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://c.duomai.com/track.php?site_id=288567&amp;#x26;aid=612&amp;#x26;euid=&amp;#x26;t=https%3A%2F%2Fwww.apple.com.cn%2Fcn-k12%2Fshop&quot;&gt;苹果教育商店&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;链接：https://c.duomai.com/track.php?site_id=288567&amp;#x26;aid=612&amp;#x26;euid=&amp;#x26;t=https%3A%2F%2Fwww.apple.com.cn%2Fcn-k12%2Fshop&lt;/p&gt;
&lt;p&gt;除 iPhone 外的大部分设备优惠，Music 和 Pro APP 优惠，暑假末返校季还会有学生优惠大促销。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://tb.g2h3.com/3WRrt&quot;&gt;微软教育商店&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;链接：https://tb.g2h3.com/3WRrt&lt;/p&gt;
&lt;p&gt;9折购买微软旗下的各种硬件设备。翻新更超值！&lt;/p&gt;
&lt;h2&gt;软件优惠&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://www.axure.com/edu&quot;&gt;Axure&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://www.axure.com/edu&lt;/p&gt;
&lt;p&gt;原型设计工具，产品、运营必备，学生、教师可免费使用。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://lastpass.com/edupromo.php&quot;&gt;LastPass&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://lastpass.com/edupromo.php&lt;/p&gt;
&lt;p&gt;方便好用的跨平台密码管理工具，免费赠送半年&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://www.tableau.com/zh-cn/academic&quot;&gt;Tableau&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://www.tableau.com/zh-cn/academic&lt;/p&gt;
&lt;p&gt;数据可视化分析软件，对学生、教师以及教育组织提供免费使用。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://www.originlab.com/OriginProLearning.aspx&quot;&gt;Origin&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://www.originlab.com/OriginProLearning.aspx&lt;/p&gt;
&lt;p&gt;一款强大的作图软件，对中国学生提供6个月的免费试用。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;a href=&quot;https://www.notion.so/student&quot;&gt;Notion&lt;/a&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://www.notion.so/student&lt;/p&gt;
&lt;p&gt;一个将笔记、知识库和任务管理无缝整合的协作平台，通过教育邮箱验证后能获得付费个人版的功能。&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.netsarang.com/zh/free-for-home-school/&quot;&gt;&lt;strong&gt;XShell + XFTP&lt;/strong&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;申请链接：https://www.netsarang.com/zh/free-for-home-school/&lt;/p&gt;
&lt;p&gt;Win下最好用的SSH和FTP管理工具，免费许可涵盖任何认证教育机构的学生、教师和员工。使用 XShell 和 XFTP 进行教学、学习和管理。&lt;/p&gt;
&lt;h2&gt;吃喝玩乐&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;海底捞&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;69折或者88折，支付宝学生特惠认证后可以享受，受时间段限制。&lt;/p&gt;
&lt;h3&gt;迪士尼&lt;/h3&gt;
&lt;p&gt;支付宝校园生活认证后可以享受，受时间段限制。&lt;/p&gt;
&lt;h3&gt;美团学生专享&lt;/h3&gt;
&lt;p&gt;学信网认证后有不同的折扣和优惠活动。&lt;/p&gt;
&lt;h3&gt;支付宝学生特惠&lt;/h3&gt;
&lt;p&gt;支付宝APP中搜索学生特惠，集成一站式学生优惠场景，话费、娱乐、消费等。&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://u.jd.com/tW2QIS6&quot;&gt;京东校园&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;学生特价、专属优惠券等，基于学信网认证。&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://tb.g2h3.com/3WRo8&quot;&gt;网易严选&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;95折全场优惠，下载网易严选客户端并登录后，进入个人页面，便能看到底部「大学生认证」选项。&lt;/p&gt;
&lt;h3&gt;顺丰学生会员&lt;/h3&gt;
&lt;p&gt;享受每年最高208元的寄件福利，进入顺丰小程序提交认证可以免费享受。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?ge"/><enclosure url="http://wallpaper.csun.site/?ge"/></item><item><title>巨魔商店安装教程</title><link>https://blog.csun.site/blog/2024-03-08-trollstore-installation-guide-no-jailbreak-no-signature-install-third-party-ipa</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-03-08-trollstore-installation-guide-no-jailbreak-no-signature-install-third-party-ipa</guid><description>巨魔商店安装第三方应用</description><pubDate>Fri, 08 Mar 2024 23:05:12 GMT</pubDate><content:encoded>&lt;p&gt;众所周知，安卓可以通过下载 &lt;code&gt;.apk&lt;/code&gt; 文件来安卓应用，但是 ios 由于安全不允许随意安装软件，只能通过 App Store 下载和安装应用。如果想要像安卓一样通过 &lt;code&gt;.ipa&lt;/code&gt; 文件安装软件，则需要进行签名，但是个人签名只能维持 7 天，而企业签名价格昂贵且容易掉签。&lt;/p&gt;
&lt;p&gt;巨魔商店可以利用 ios 系统中的一个安全漏洞来绕过代码签名验证过程，从而允许用户通过 &lt;code&gt;.ipa&lt;/code&gt; 文件安装第三方应用程序。&lt;/p&gt;
&lt;p&gt;本文将以我的 iPad (iPadOS 16.0， A12) 为例介绍如何安装巨魔商店。&lt;/p&gt;
&lt;p&gt;截至本文发布日期，巨魔商店已经支持 ios 15.5 - 16.6.1，更多机型安装教程请参考官方文档：https://ios.cfw.guide/installing-trollstore/&lt;/p&gt;
&lt;h2&gt;前置要求&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;确保你的 ios 设备上已经安装了 提示/Tips app&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保可以通过数据线连接你的 ios 设备和电脑&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保安装了爱思助手&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安装 TrollStar&lt;/h2&gt;
&lt;p&gt;从 &lt;a href=&quot;https://github.com/34306/TrollStar/releases/tag/1.2&quot;&gt;Release TrollStar 1.2 · 34306/TrollStar &lt;/a&gt; 下载 TrollStar 的 &lt;code&gt;.ipa&lt;/code&gt; 文件，通过爱思助手自签名安装到你的 ios 设备。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/608795076&quot;&gt;爱思助手iPA - 自签教程 - 知乎 (zhihu.com)&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;注入 TrollStore Helper&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;这个方法可能一次不成功，需要多尝试几次&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;打开 TrollStar，点击 &lt;code&gt;Click here to start!&lt;/code&gt;，如果点击之后你的 ios 设备出现重启，请等待一段时间后再次重试。&lt;/p&gt;
&lt;p&gt;然后点击 &lt;code&gt;Install TrollStore Helper to Tips&lt;/code&gt;，再点击 &lt;code&gt;Respring to Apply&lt;/code&gt; ，你的设备将再次重启，不过没关系这是正常现象，这个时候我们的 TrollStore Helper 就已经注入成功了。&lt;/p&gt;
&lt;h2&gt;安装 TrollStore&lt;/h2&gt;
&lt;p&gt;打开 提示/Tips APP，此时你的 提示/Tips APP 已经被注入了 TrollStore Helper，点击 &lt;code&gt;Install TrollStore&lt;/code&gt; ，这时你的设备将会再次重启，重启完成后，TrollStore 已经出现在你的桌面上了。&lt;/p&gt;
&lt;p&gt;然后我们还需要进行一些配置，打开 TrollStore，进入 &lt;code&gt;setting&lt;/code&gt; 界面，然后点击 &lt;code&gt;Install Persistence Helper&lt;/code&gt; ，从下拉列表中选择 &lt;code&gt;Tips&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;自此 TrollStore 配置完成，我们可以将下载的 &lt;code&gt;.ipa&lt;/code&gt; 文件通过 TrollStore 打开完成安装，从而实现不用签名安装第三方应用。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?jumo"/><enclosure url="http://wallpaper.csun.site/?jumo"/></item><item><title>踩坑日记：MySQL 远程访问配置</title><link>https://blog.csun.site/blog/2024-01-30-mysql-remote-access-configuration</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-01-30-mysql-remote-access-configuration</guid><description>MySQL远程访问配置经验分享</description><pubDate>Tue, 30 Jan 2024 22:34:12 GMT</pubDate><content:encoded>&lt;p&gt;我们在项目开发过程中，不可避免会使用到数据库，如果数据库部署在本地，当项目在别的机器上启动时就不能访问原来的数据库了，这对我们的开发带来了极大的不便，所以通常会将数据库部署在远程服务器上，便于在不同机器上都能访问数据库。&lt;/p&gt;
&lt;p&gt;MySQL 作为常用数据库之一，其远程访问的配置有点坑，折腾了好久才配置成功，本文就记录下本次踩坑经历。&lt;/p&gt;
&lt;h2&gt;安装 MySQL&lt;/h2&gt;
&lt;p&gt;首先我们需要在服务器上安装 MySQL，我使用的服务器镜像为 &lt;code&gt;Ubuntu 22.04.3 LTS&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;首先，更新一下软件包列表，便于安装较新的 MySQL。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo apt-get update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用以下命令即可安装 &lt;code&gt;mysql-server&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo apt-get install mysql-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装过程中出现上述界面，直接回车选择 &lt;code&gt;dbus.service&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;之后，MySQL 服务就安装完成并启动了，输入下列命令可以查看服务状态。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo systemctl status mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/images20240129233954.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;看到 &lt;code&gt;active (running)&lt;/code&gt; 字样说明 MySQL 服务运行正常。&lt;/p&gt;
&lt;h2&gt;配置远程访问&lt;/h2&gt;
&lt;h3&gt;创建用户&lt;/h3&gt;
&lt;p&gt;一般来说，不建议使用 root 用户作为远程访问的用户，可以单独创建一个用户。&lt;/p&gt;
&lt;p&gt;首先，使用下列命令以 root 用户身份进入 MySQL 客户端&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后创建一个新用户：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE USER &apos;newuser&apos;@&apos;%&apos; IDENTIFIED BY &apos;password&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@&apos;%&apos;&lt;/code&gt; 表示可以从任何地方连接，如果使用 &lt;code&gt;@localhost&lt;/code&gt; 则只能从本机连接，也可以设置为 &lt;code&gt;@&apos;你的ip&apos;&lt;/code&gt; 使得只能你的机器连接，从而确保数据库安全。&lt;/p&gt;
&lt;h3&gt;赋予用户权限&lt;/h3&gt;
&lt;p&gt;为了让用户有权限操作数据库，我们需要对用户赋予权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;GRANT ALL PRIVILEGES ON *.* TO &apos;newuser&apos;@&apos;%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ON *.*&lt;/code&gt; 代表授予所有数据库完全的权限，你也可以指定数据库，只给用户特定的权限。&lt;/p&gt;
&lt;p&gt;然后执行&lt;code&gt;FLUSH PRIVILEGES&lt;/code&gt;命令来使权限更改生效：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;FLUSH PRIVILEGES;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;到这一步就结束了吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当然没有！&lt;/p&gt;
&lt;p&gt;网上很多教程到这一步就结束了，然后当我们用这个用户去连接远程数据库时，却怎么也连接不上。&lt;/p&gt;
&lt;p&gt;因为我们还缺失了最重要的一步。&lt;/p&gt;
&lt;h3&gt;修改配置文件&lt;/h3&gt;
&lt;p&gt;msyql 在配置文件中设置了 &lt;code&gt;bind-address = 127.0.0.1&lt;/code&gt;，限制了只能本机连接。&lt;/p&gt;
&lt;p&gt;打开 &lt;code&gt;/etc/mysql/mysql.conf.d/mysqld.cnf&lt;/code&gt; 文件，可以看到 &lt;code&gt;bind-address&lt;/code&gt; 被设置成了 &lt;code&gt;127.0.0.1&lt;/code&gt;，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/sun-i/pic/images20240130222425.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们将其修改成 &lt;code&gt;bind-address = 0.0.0.0&lt;/code&gt;，即可远程访问数据库了。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?mysql"/><enclosure url="http://wallpaper.csun.site/?mysql"/></item><item><title>服务器虚拟内存启动！小鸡轻松化身大盘鸡</title><link>https://blog.csun.site/blog/2024-01-27-server-virtual-memory-launch-chicken-dish</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-01-27-server-virtual-memory-launch-chicken-dish</guid><description>虚拟内存优化服务器性能</description><pubDate>Sat, 27 Jan 2024 23:14:12 GMT</pubDate><content:encoded>&lt;p&gt;服务器的内存资源是十分昂贵的，我们在使用服务器运行程序时，经常遇到内存不足，运行崩溃的情况。为了减少购买昂贵的内存资源，又能流畅的运行程序，可以使用&lt;strong&gt;虚拟内存&lt;/strong&gt;来代替。&lt;/p&gt;
&lt;p&gt;简单来说，虚拟内存就是操作系统在硬盘上为程序设置的一块“伪内存”，它允许我们假装自己的服务器拥有的物理内存远超于实际情况。当物理内存不够用时，操作系统就会调用虚拟内存，把一些不常用的数据暂时存储到硬盘上去，释放出物理内存空间给予新的数据。&lt;/p&gt;
&lt;p&gt;一般来说，服务器都没有开启虚拟内存，需要我们手动开启，本文就教大家如何开启虚拟内存，让小鸡轻松化身大盘鸡！&lt;/p&gt;
&lt;h2&gt;创建 swap 文件&lt;/h2&gt;
&lt;h3&gt;1.在用户目录下创建 &lt;code&gt;swap&lt;/code&gt; 文件夹，并进入该文件夹&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mkdir swap
cd swap
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 创建 &lt;code&gt;swapfile&lt;/code&gt; 文件&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;dd if=/dev/zero of=/用户目录/swap/swapfile bs=1M count=4096
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dd&lt;/code&gt;是一个用于复制文件的命令， &lt;code&gt;if&lt;/code&gt;用于指定输入文件，&lt;code&gt;of&lt;/code&gt;用于指定输出文件， &lt;code&gt;/dev/zero&lt;/code&gt; 是一个特殊的设备文件，会不断产生字节值为 0 的数据，&lt;code&gt;bs=1M&lt;/code&gt; 指定了每次读取或写入的数据块大小为 1M 字节，&lt;code&gt;count=4096&lt;/code&gt; 指定了要复制的块数。&lt;/p&gt;
&lt;p&gt;这行命令创建了一个大小为 4096M 字节的交换文件。&lt;/p&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;记录了4096+0的读入
记录了4096+0的写出
4294967296字节(4.3 GB)已复制，15.7479 秒，273 MB/秒
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;将 &lt;code&gt;swapfile&lt;/code&gt; 设置为 swap 分区文件&lt;/h2&gt;
&lt;h3&gt;1.设置文件权限&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;chmod 0600 swapfile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;swapfile&lt;/code&gt; 文件的权限为 0600 即允许文件所有者读写。&lt;/p&gt;
&lt;h3&gt;2.设置 swap 分区文件&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mkswap swapfile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 &lt;code&gt;swapfile&lt;/code&gt; 设置为 swap 分区文件&lt;/p&gt;
&lt;h2&gt;激活 swap 区并启用交换区文件&lt;/h2&gt;
&lt;h3&gt;1.激活 swap 区&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;swapon swapfile
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.查看现有内存&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;free -m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用上述命令可以查看现在的内存，可以看到里面的 swap 分区变成了 4095M，也就是 4G 内存。&lt;/p&gt;
&lt;h2&gt;设置开机自动启动虚拟内存&lt;/h2&gt;
&lt;p&gt;打开 &lt;code&gt;/etc/fstab&lt;/code&gt; 文件，在文件中加入如下内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;/用户目录/swap/swapfile swap swap defaults 0 0
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?server"/><enclosure url="http://wallpaper.csun.site/?server"/></item><item><title>SQL JOIN 解析</title><link>https://blog.csun.site/blog/2024-01-27-sql-join-deep-analysis-double-your-query-efficiency</link><guid isPermaLink="true">https://blog.csun.site/blog/2024-01-27-sql-join-deep-analysis-double-your-query-efficiency</guid><description>SQL JOIN 操作深度解析</description><pubDate>Sat, 27 Jan 2024 22:38:12 GMT</pubDate><content:encoded>&lt;p&gt;SQL中的 &lt;code&gt;JOIN&lt;/code&gt; 操作是根据两个或多个表之间的相关列将它们合并在一起的查询操作，能够大大提高查询效率，本文将对常用的几种 &lt;code&gt;JOIN&lt;/code&gt; 操作进行深度解析。&lt;/p&gt;
&lt;h2&gt;INNER JOIN&lt;/h2&gt;
&lt;p&gt;只返回两个表中匹配的行，如果某个表中的行在另一个表中没有匹配的行，则这些行不会出现在结果集中，即求两个表的&lt;strong&gt;交集&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM A
INNER JOIN B
ON A.key = B.key;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;INNER JOIN 可以用于整合不同表中有关联的数据，例如有一个员工表和一个员工薪资表，需要同时查询员工信息和薪资，就可以使用 INNER JOIN 整合两个表的数据。&lt;/p&gt;
&lt;h2&gt;LEFT JOIN&lt;/h2&gt;
&lt;p&gt;返回左表（第一个表）的所有行，即使右表（第二个表）中没有匹配的行。如果右表中没有匹配的行，结果集中的右表列将填充NULL值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM A
LEFT JOIN B
ON A.key = B.key;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样是员工表和薪资表，如果并不是所有员工都有薪资记录，因为他们刚刚加入公司，或者薪资数据还未更新等，我们想要查询所有员工的信息和薪资，就得允许结果集中存在薪资为 &lt;code&gt;NULL&lt;/code&gt; 的数据，这个时候就需要用到 LEFT JOIN。&lt;/p&gt;
&lt;h2&gt;LEFT JOIN (sans l&apos;intersection de B)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sans l&apos;intersection de B&lt;/code&gt; 是法语，意为没有B的交集，即在 &lt;code&gt;LEFT JOIN&lt;/code&gt; 的结果集中，排除在两个表中都有匹配的记录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM A
LEFT JOIN B
ON A.key = B.key
WHERE B.key IS NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设我们有两个表，一个是客户表，一个订单表。我们想要查询那些在订单表中没有订单的客户信息，就需要使用 &lt;code&gt;LEFT JOIN (sans l&apos;intersection de B)&lt;/code&gt; 排除在两个表中都有匹配的数据。&lt;/p&gt;
&lt;h2&gt;RIGHT JOIN&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;RIGHT JOIN&lt;/code&gt; 的使用与 &lt;code&gt;LEFT JOIN&lt;/code&gt; 相反，它保留右表的所有记录，即使左表中没有匹配的记录。在结果集中，右表中的所有记录都会显示，而左表中没有匹配的记录对应的列将显示为NULL。&lt;/p&gt;</content:encoded><h:img src="http://wallpaper.csun.site/?sqljoin"/><enclosure url="http://wallpaper.csun.site/?sqljoin"/></item></channel></rss>