wenmo8 发布的文章

本篇介绍
Manim
中两个和动画轨迹相关的类,
AnimatedBoundary

TracedPath

AnimatedBoundary
聚焦于图形边界的动态呈现,能精准控制边界绘制的每一帧,助力我们清晰展示几何图形的搭建流程。

TracedPath
则擅长实时追踪物体或点的运动轨迹,以直观且动态的方式呈现各类运动路径,为我们分析和展示复杂运动提供了强大支持 。

1. 动画概述

1.1. AnimatedBoundary

在讲解几何图形(如多边形、圆形等)的构造过程时,
AnimatedBoundary
可以逐帧展示图形边界的绘制,帮助我们理解图形是如何一步步形成的。

此外,当图形的边界随着某个参数或条件动态变化时,使用
AnimatedBoundary
也可以生动地呈现这种变化。

AnimatedBoundary
动画的主要特点在于图形边界的绘制,它能够精确控制边界的出现顺序和方式。

这使得在展示几何图形的构建过程时,能够突出边界这一关键元素,更清楚地展示图形的轮廓是如何形成的。

它的参数主要有:

参数名称 类型 说明
vmobject VMobject 要应用动画边界的
VMobject
colors [Color] 颜色列表,用于指定边界颜色变化的序列
max_stroke_width int 最大描边宽度
cycle_rate float 颜色循环速率
back_and_forth bool 是否来回循环颜色变化
draw_rate_func func 用于控制绘制速率的函数
fade_rate_func func 用于控制淡出速率的函数

1.2. TracedPath

在物理学或数学中,当需要展示物体的运动轨迹时,
TracedPath
是一个非常合适的工具。

例如,展示抛体运动、圆周运动等物体的运动路径时,能让我们直观地看到物体在空间中的运动轨迹。

此外,对于函数图像的绘制,也可使用
TracedPath
来模拟绘图过程,展示函数曲线是如何随着自变量的变化而逐步生成的。

这在演示函数的性质和图像绘制方法时非常有用,能够帮助学生更好地理解函数的变化规律。

TracedPath
动画的主要特点是能够实时跟踪物体或点的运动轨迹,并将其以动画的形式呈现出来。

这种实时跟踪的特性使得动画更加真实、生动,能够准确地反映物体的运动状态。

它的参数主要有:

参数名称 类型 说明
traced_point_func func 要跟踪的函数,该函数应返回一个点的坐标
stroke_width float 轨迹的线条宽度
stroke_color Color 轨迹的颜色
dissipating_time float 路径消散所需的时间


dissipating_time
参数为
None
时,表示路径轨迹不消散。

TracedPath
还有一个方法:

名称 说明
update_path 用于更新轨迹路径的方法,通常在动画过程中被调用,以实时跟踪点的移动并更新轨迹

2. 使用示例

下面通过几个根据实际应用场景简化而来的示例来演示两个动画类的使用。

2.1. 多边形绘制

这个示例中,首先创建了一个矩形多边形,然后使用
AnimatedBoundary
为其添加边界动画,

颜色在
蓝色

绿色

黄色
之间循环变化,循环速率为
3
,突出展示多边形边界的绘制动画。

polygon = Polygon(
    [-2, -1, 0],
    [2, -1, 0],
    [2, 1, 0],
    [-2, 1, 0],
)
boundary = AnimatedBoundary(
    polygon,
    colors=[BLUE, GREEN, YELLOW],
    cycle_rate=3,
)
self.add(polygon, boundary)

2.2. 动态更新圆形边界

先创建了一个圆形,其边界的颜色在
红色

黄色

绿色
之间循环,循环速率为 2。

然后通过动画将圆形的半径放大
2
倍,展示了圆形边界在动态变化过程中的动画效果。

circle = Circle(radius=1)
boundary = AnimatedBoundary(
    circle,
    colors=[RED, YELLOW, GREEN],
    cycle_rate=2,
)
self.add(circle, boundary)
self.play(circle.animate.scale(2), run_time=3)

2.3. 跟踪抛体运动轨迹

首先定义一个抛体运动的函数
move_path
,再创建了一个点
Dot
和一个
TracedPath
对象来跟踪点的运动轨迹。

轨迹颜色为
绿色
,宽度为
3
,展示了抛体运动的轨迹跟踪效果。

d = Dot().shift(LEFT * 2)
trace = TracedPath(
    d.get_center,
    stroke_color=GREEN,
    stroke_width=3,
)
self.add(d, trace)

def move_path(t):
    x = t
    y = 2 - 0.5 * t**2
    return np.array([x, y, 0])

f = ParametricFunction(
    move_path,
    t_range=(-3, 3),
)
self.play(MoveAlongPath(d, f), run_time=3)

2.4. 函数图像绘制过程

这个示例中,定义了一个正弦函数
move_path
,再创建一个点沿着这个正弦函数图像运动,同时创建了一个
TracedPath
对象来跟踪函数图像的绘制过程。

轨迹颜色为
紫色
,宽度为
2
,且设置轨迹在
1
秒后消失。

d = Dot(color=BLUE).shift([-PI, 0, 0])
trace = TracedPath(
    d.get_center,
    stroke_color=PURPLE,
    stroke_width=2,
    dissipating_time=1,
)
self.add(d, trace)

def move_path(x):
    return np.array([x, np.sin(x), 0])

f = ParametricFunction(move_path, t_range=(-PI, PI))
self.play(MoveAlongPath(d, f), run_time=3)

3. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
trace.py
),

下载地址:
完整代码
(访问密码: 6872)

思想:非比较而是划分值域

基数排序(Radix Sort)是一种非比较排序算法,它通过逐位对数据进行处理,依次按位从
最低有效位(Least Significant Digit, LSD)

最高有效位(Most Significant Digit, MSD)
或者反过来,对数据进行排序。

与常见的比较排序算法(如快速排序、归并排序)存在本质不同,基数排序不基于元素间的直接比较,而是依赖于元素的位权信息来排序。这使得基数排序能够在某些情况下实现线性时间复杂度,理论上达到
\(O(kn)\)
,其中
\(k\)
是位数,
\(n\)
是元素个数。即复杂度取决于值域规模。

基数排序的核心思想是分桶和合并:通过多次分桶操作,将元素按照某个位的值放入对应的桶中,然后再按照桶的顺序合并,逐步将数组排序。

简单的十进制基数排序

以下是 LSD 基数排序的简单流程,基于十进制:

  1. 找到数组中最大的数,确定需要处理的最大位数
    \(d\)
  2. 从最低有效位开始,依次对每一位执行以下步骤:
    • 使用计数排序(Counting Sort)等稳定排序算法,根据当前位的值对数据进行分桶。
    • 按分桶的顺序重新排列数组。

这种方式之所以有效,是因为每次分桶时,数据局部是有序的,每一位的排序是稳定的,因此较高位的排序不会改变低位已排序数字的相对顺序。通过逐位排序,逐渐将数字按整体大小排列。想象整理一叠卡片,先按卡片右侧的颜色分组,再按中间的图案分组,最后按左侧的形状分组。由于每一步都保留了之前分组的顺序,最终整理好的卡片是完全有序的。

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

void countingSort(vector<int>& arr, int exp) {
    int n = arr.size();
    vector<int> output(n);
    vector<int> count(10, 0); // count[i]: 第 exp 位为 i 的数有多少?

    for (int i = 0; i < n; i++)
        count[(arr[i] / exp) % 10]++;

    for (int i = 1; i < 10; i++)
        count[i] += count[i - 1];

    for (int i = n - 1; i >= 0; i--) {
        // 根据第 exp 位决定应该占用的位置,
        output[count[(arr[i] / exp) % 10] - 1] = arr[i];
        count[(arr[i] / exp) % 10]--;
    }

    for (int i = 0; i < n; i++)
        arr[i] = output[i];
}

void radixSort(vector<int>& arr) {
    int maxVal = *max_element(arr.begin(), arr.end());

    for (int exp = 1; maxVal / exp > 0; exp *= 10)
        countingSort(arr, exp);
}

int main() {
    vector<int> arr = {170, 45, 75, 90, 802, 24, 2, 66};
    radixSort(arr);

    for (int num : arr)
        cout << num << " ";
    return 0;
}

可以把百,十,个位依次认为是第一,第二,第三关键字,按关键字权重低到高多趟排序,而排到某一位时,其低位已经排序好,只要按顺序取出就可以保持顺序。

从中可以看出,基数排序通常需要
\(O(n+k)\)
的辅助空间。且在对每一位(如个位、十位)排序时,基数排序是稳定的,相同键值的元素在排序后相对顺序保持不变。而高位的排序不会改变低位已排好元素的顺序。因此,基数排序天然稳定。

高位优先(MSD)和低位优先(LSD)

上述是一个 LSD 的例子,其实从高位到低位也是可行的,且理解起来更加简单。MSD 基数排序从数字的最高位开始排序,将数字分组到不同的桶中(如按千位分组)。对每个桶分别递归排序,逐步向低位处理。每轮排序后,桶的内容会被按顺序合并。像按照城市、省份、街道逐级分类邮件。先按城市分,再在每个城市中按省份分,最后在每个省份中按街道分。高层次分类先决定大范围,递归细分确保每个细节都正确。

void msdRadixSortUtil(vector<int>& arr, int left, int right, int exp) {
    if (left >= right || exp == 0) return;

    vector<vector<int>> buckets(10);

    // Place elements into corresponding buckets based on the current significant digit
    for (int i = left; i <= right; i++) {
        int digit = (arr[i] / exp) % 10;
        buckets[digit].push_back(arr[i]);
    }

    // Merge buckets back into the array
    int index = left;
    for (int i = 0; i < 10; i++) {
        for (int num : buckets[i]) {
            arr[index++] = num;
        }
    }

    // Recursively sort each non-empty bucket
    index = left;
    for (int i = 0; i < 10; i++) {
        if (!buckets[i].empty()) {
            int bucketSize = buckets[i].size();
            msdRadixSortUtil(arr, index, index + bucketSize - 1, exp / 10);
            index += bucketSize;
        }
    }
}

void msdRadixSort(vector<int>& arr) {
    if (arr.empty()) return;

    // Find the maximum value to determine the number of digits
    int maxVal = *max_element(arr.begin(), arr.end());
    int maxExp = pow(10, static_cast<int>(log10(maxVal)));

    // Start MSD radix sort from the highest significant digit
    msdRadixSortUtil(arr, 0, arr.size() - 1, maxExp);
}

高位优先(MSD)排序
从最高有效位开始排序,递归地对子数组进行分桶,逐步细化到最后的排序结果,通常需要递归。MSD方法常用于字符串排序,因为它可以提前确定不同类别。

低位优先(LSD)排序
从最低有效位开始排序,逐步提升到最高有效位。它每次操作的排序范围是全数组,且每次排序不破坏之前的顺序(稳定性)。因此,对于整数排序,LSD方法更为常用。

两种方法都可行,但低位优先排序实现简单,且可以直接应用于数字,故在实践中更受欢迎。

二进制的基数排序

计算机中的数据都使用二进制(或者说十六进制)存储,十进制会导致每位信息利用不充分,且需要低效的模 10 运算,非常低效。

假设仅有正数,对于无符号32位整数,可以按二进制位分为多组。例如,每次处理8位(共分4组)。这样的处理方式仍然保持基数排序的思想,但使用更接近硬件位操作的方式,比十进制效率高得多,处理效率高。

void radixSortBinary(vector<uint32_t>& arr) {
    const int BITS = 32;
    const int RADIX = 256; // 每次处理8位
    const int MASK = RADIX - 1;

    vector<uint32_t> buffer(arr.size());

    // 四轮循环,分别处理0 - 7, 8 - 15, 16 - 23, 24 - 32 位。count 大小也增加到 256
    for (int shift = 0; shift < BITS; shift += 8) {
        array<int, RADIX> count = {0};

        for (uint32_t num : arr)
            count[(num >> shift) & MASK]++;

        for (int i = 1; i < RADIX; i++)
            count[i] += count[i - 1];

        for (int i = arr.size() - 1; i >= 0; i--) {
            uint32_t bucket = (arr[i] >> shift) & MASK;
            buffer[--count[bucket]] = arr[i];
        }

        arr.swap(buffer);
    }
}

int main() {
    vector<uint32_t> arr = {170, 45, 75, 90, 802, 24, 2, 66};
    radixSortBinary(arr);

    for (uint32_t num : arr)
        cout << num << " ";
    return 0;
}

每次处理位数和

上例处理 32 位整数,分四次排序,一次八位。称呼其位宽 8。实际上也可以选择一次排序 16 位,排序两次,可以减少一半的轮次,但创建 65536 个桶可能会导致内存压力,且桶分布不均时效率下降:如果数据的分布高度集中,某些桶可能会很大,导致操作不均衡。若位宽仅 4,则分桶的范围较小,分桶和合并过程相对快速,但排序的趟数太多,适合小规模数组或内存受限的场景。

拓展知识

基数排序与快速排序

基数排序和快速排序是两种经典的排序算法,适用于不同场景。
基数排序
是一种非比较排序算法,依赖数字的位数特性,通过按位分组排序实现有序,适合处理数字或固定长度的字符串,具有线性时间复杂度
\(O(n \cdot d)\)
(其中
\(d\)
是位数)。它对数据规模较大且值域较小的数据表现出色,但需要额外的空间来存储桶。相比之下,
快速排序
是最经典的基于比较的分治算法,通过选择一个基准值(pivot)将数组划分为两部分递归排序,平均时间复杂度为
\(O(n \log n)\)
。快速排序在大多数情况下效率极高,适用于通用数据类型,且原地排序所需额外空间较少,但其性能可能因基准选择不当而退化。简而言之,基数排序适合特定结构数据(如整数或字符串),而快速排序更通用,适合各种类型和规模的输入数据。

基数排序和桶排序

基数排序和桶排序虽然都是基于分组的非比较排序算法,但它们的目标和实现方式有所不同,且可以认为
基数排序是桶排序的延伸
。桶排序通过将数据分布到有限数量的桶中,每个桶内部再进行排序(通常使用插入排序或其他算法),最终将桶内容按顺序合并以获得排序结果;它主要依赖数据的分布特性,适用于数据均匀分布的场景,时间复杂度接近
\(O(n)\)
。而基数排序本质上可以看作是多轮的桶排序:在值域很大时,它通过按位(如个位、十位等)多次分桶并排序,逐步实现最终的全局有序性。基数排序的核心思想是通过多次分桶来解决单次分桶无法处理多位数据的问题。因此,可以理解为基数排序在设计上对桶排序的扩展,用于处理数字、固定长度字符串等多位特征的数据。

基数排序应用于非整数

某些场合下,
基数排序可以扩展应用于非整数(如浮点数)和结构体
,但需要对数据进行适当的预处理,使其特性适合基数排序的机制。以下是实现这些扩展的关键思路:


1. 处理浮点数

浮点数位码有一个特殊性质:IEEE 754 格式保证了从小到大的正数,其位模式从小到大单调递增。因此,可以直接将浮点数的位模式解释为无符号整数,然后按整数排序。也就是说,若不考虑符号可以直接视为整数排序。

void radixSortFloat(vector<float>& arr) {
    vector<uint32_t> bitPattern(arr.size());

    // 将浮点数解释为无符号整数,假设浮点数均是正数。
    for (size_t i = 0; i < arr.size(); ++i) {
        memcpy(&bitPattern[i], &arr[i], sizeof(float));
    }

    // 对无符号整数排序
    radixSort(bitPattern.begin(), bitPattern.end());

    // 排序完成后还原为浮点数
    for (size_t i = 0; i < arr.size(); ++i) {
        memcpy(&arr[i], &bitPattern[i], sizeof(float));
    }
}


2. 处理结构体

基数排序天然可以划分关键字,对于结构体,可以通过选择一个或多个键值(字段)作为排序依据,将结构体排序问题转化为对这些键的排序。

比如对于包含字段
age

salary
的结构体数组:

struct Employee {
    int age;
    double salary;
};

可先按
salary
使用浮点数处理方法进行基数排序。再按
age
使用整数直接排序。

想要开发AI产品的.Net程序员机会来了,这个项目应该好好研究。

虽然说大模型基本都有提供网络API,但肯定没有直接使用本地模型速度快。

最近微软官方新推出AI Dev Gallery开源项目,可以帮助Windows开发人员学习如何将具有本地模型和API的AI添加到Windows应用程序中。

图片

01 项目简介

AI Dev Gallery集成了来自微软自家和第三方平台(如Hugging Face、GitHub)的预训练AI模型。

该项目核心特点有:

1、支持从GitHub和HuggingFace等知名平台上下载AI模型。

2、超过25种不同场景下,通过交互式样本测试不同的AI模型,涵盖文本、图像、音频和视频等多种应用案例。

3、每一个示例都有完整的代码,查看每个示例的所有相关代码和库引用,方便开发者更好地理解和应用。

4、根据设备性能,可以灵活选择在CPU或GPU上运行模型。

02 运行环境要求

  • 最低操作系统版本:Windows 10, 版本1809 (10.0; 构建17763) 或更高版本。

  • 架构:支持x64和ARM64。

  • 内存:至少推荐16GB。

  • 磁盘空间:至少推荐20GB的空闲空间。

  • GPU:推荐8GB的VRAM用于在GPU上运行示例。

03 项目截图

1、示例分为文本、代码、图像等。

图片

2、下载模型

图片

3、方便查看最新模型列表,可以看到模型的大小,它是在CPU上运行还是在GPU上运行,以及相关的许可证。选择最适合您的机器的型号。

图片

4、C#示例代码

图片

5、模型详细介绍

图片

6、删除和管理模型

图片

AI Dev Gallery仍在开发中,计划添加更多示例和功能,以进一步提升体验。

04利用AI Dev Gallery,我们可以做什么?

  1. 图像识别与分析:
  • 使用 AI 模型识别图像中的对象、场景和活动。

  • 对图像进行分类和标注。

  • 实现图像增强和风格转换。

  1. 自然语言处理(NLP):
  • 文本翻译和语言检测。

  • 情感分析和文本摘要。

  • 聊天机器人和问答系统。

  1. 音频分析:
  • 语音识别和语音合成。

  • 音乐识别和音频分类。

  • 语音命令和交互式语音响应系统。

  1. 视频分析:
  • 视频内容分析,如动作识别和场景变化检测。

  • 实时视频处理和增强。

  • 视频内容的自动标注和分类。

  1. 数据增强:
  • 通过 AI 技术生成合成数据,用于训练和测试机器学习模型。

  • 数据去噪和异常检测。

  1. 个性化推荐系统:
  • 根据用户行为和偏好提供个性化内容推荐。

  • 产品推荐和用户行为预测。

  1. 自动化和机器人技术:
  • 利用 AI 模型进行路径规划和避障。

  • 机器人视觉和交互。

  1. 健康与医疗:
  • 医疗影像分析,如X光和MRI图像识别。

  • 疾病预测和健康监测。

  1. 教育与学习:
  • 智能教育软件,提供个性化学习路径。

  • 自动化评分和学习分析。

  1. 游戏和娱乐:
  • 游戏中的 AI 对手和 NPC(非玩家角色)行为。

  • 交互式故事讲述和动态内容生成。

  1. 安全与监控:
  • 人脸识别和生物识别技术。

  • 异常行为检测和安全警报。

  1. 商业智能与分析:
  • 市场趋势分析和预测。

  • 客户细分和销售预测。

05 项目地址

https://github.com/microsoft/ai-dev-gallery

- End -

更多开源项目:
https://github.com/bianchenglequ/NetCodeTop

前言

在 Java 开发中,解析 JSON 是一个非常常见的需求。

不管是和前端交互、调用第三方接口,还是处理配置文件,几乎都绕不开 JSON。

这篇文章总结了6种主流的 JSON 解析方法,希望对你会有所帮助。

(我最近开源了一个基于 SpringBoot+Vue+uniapp 的商城项目,欢迎访问和star。)[
https://gitee.com/dvsusan/susan_mall
]

1. 使用 Jackson:业界标配

功能特点

  • 强大的序列化和反序列化
    :支持将 JSON 字符串转为 Java 对象,也支持将 Java 对象转换为 JSON。
  • 支持复杂结构
    :处理嵌套对象、数组、泛型等场景非常轻松。
  • 支持注解
    :如
    @JsonIgnore

    @JsonProperty
    等,能精细控制序列化与反序列化的行为。
  • 性能高
    :Jackson 的性能非常出色,是很多企业级项目的首选。

代码示例

1. JSON 转对象(反序列化)

import com.fasterxml.jackson.databind.ObjectMapper;

public class JacksonExample {
    public static void main(String[] args) throws Exception {
        String json = "{\"id\":1,\"name\":\"张三\"}";

        ObjectMapper objectMapper = new ObjectMapper();
        User user = objectMapper.readValue(json, User.class);
        System.out.println(user.getName()); // 输出:张三
    }
}

class User {
    private int id;
    private String name;

    // Getters 和 Setters 省略
}

2. 对象转 JSON(序列化)

User user = new User();
user.setId(1);
user.setName("李四");

ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(user);
System.out.println(json); // 输出:{"id":1,"name":"李四"}

高级功能

  • 日期格式化

    @JsonFormat(pattern = "yyyy-MM-dd")
  • 忽略字段

    @JsonIgnore
  • 重命名字段

    @JsonProperty("custom_name")

优缺点

优点 缺点
功能全面,支持复杂场景 配置较多,学习成本稍高
性能高,社区活跃,企业级项目首选 过于强大,部分功能用不上
丰富的注解支持,便于控制序列化行为 库体积较大,对于小型项目略显笨重

2. 使用 Gson:轻量好用

功能特点

  • 轻量级
    :Gson 的设计非常简洁,代码量少,适合中小型项目。
  • 支持泛型
    :可以轻松解析带泛型的 JSON。
  • 注解控制
    :支持通过注解控制序列化行为,如
    @Expose
  • 易扩展
    :通过自定义序列化器和反序列化器,可以处理复杂的场景。

代码示例

1. JSON 转对象

import com.google.gson.Gson;

public class GsonExample {
    public static void main(String[] args) {
        String json = "{\"id\":1,\"name\":\"王五\"}";

        Gson gson = new Gson();
        User user = gson.fromJson(json, User.class);
        System.out.println(user.getName()); // 输出:王五
    }
}

2. 对象转 JSON

User user = new User();
user.setId(2);
user.setName("赵六");

Gson gson = new Gson();
String json = gson.toJson(user);
System.out.println(json); // 输出:{"id":2,"name":"赵六"}

高级功能

  • 忽略字段

    @Expose
    @Expose
    private String name;
    
  • 自定义序列化器/反序列化器

    Gson gson = new GsonBuilder()
            .registerTypeAdapter(CustomClass.class, new CustomSerializer())
            .create();
    

优缺点

优点 缺点
轻量级,简单易用,适合中小型项目 性能稍逊于 Jackson
学习曲线平滑,新手容易上手 功能不如 Jackson 丰富
提供良好的扩展能力 复杂对象处理起来较为麻烦

3. 使用 FastJSON:高性能

功能特点

  • 性能优异
    :FastJSON 的解析速度非常快,适合大数据量场景。
  • 支持动态字段
    :可以轻松处理动态 JSON 数据。
  • 强大的类型支持
    :支持嵌套对象、泛型、数组等复杂结构。
  • 注解控制
    :类似 Jackson 和 Gson,支持注解控制字段的序列化和反序列化。

代码示例

1. JSON 转对象

import com.alibaba.fastjson.JSON;

public class FastJsonExample {
    public static void main(String[] args) {
        String json = "{\"id\":1,\"name\":\"小明\"}";

        User user = JSON.parseObject(json, User.class);
        System.out.println(user.getName()); // 输出:小明
    }
}

2. 对象转 JSON

User user = new User();
user.setId(3);
user.setName("小红");

String json = JSON.toJSONString(user);
System.out.println(json); // 输出:{"id":3,"name":"小红"}

高级功能

  • 自动驼峰转下划线

    JSON.DEFFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
    
  • 动态字段解析

    Map<String, Object> map = JSON.parseObject(json, Map.class);
    

优缺点

优点 缺点
性能极高,解析速度快 曾有历史安全漏洞的争议
支持复杂的动态字段解析 社区活跃度稍逊于 Jackson 和 Gson
功能全面,适合大规模数据处理场景 配置选项和 API 比较多,稍显复杂

4. 使用 JsonPath:快速提取嵌套字段

功能特点

  • 高效字段提取
    :通过路径表达式(类似 XPath)快速提取嵌套字段。
  • 灵活性强
    :支持动态字段和条件过滤。
  • 轻量级
    :专注于字段提取,功能简单明确。

代码示例

import com.jayway.jsonpath.JsonPath;

public class JsonPathExample {
    public static void main(String[] args) {
        String json = """
            {
                "store": {
                    "book": [
                        {"title": "书1", "price": 10},
                        {"title": "书2", "price": 20}
                    ]
                }
            }
        """;

        // 提取第一个书籍的标题
        String title = JsonPath.read(json, "$.store.book[0].title");
        System.out.println(title); // 输出:书1

        // 提取所有书籍价格
        List<Integer> prices = JsonPath.read(json, "$.store.book[*].price");
        System.out.println(prices); // 输出:[10, 20]
    }
}

优缺点

优点 缺点
字段提取简洁高效 不支持序列化和反序列化
动态字段处理能力强 依赖 JsonPath 语法
适合快速提取嵌套字段 不适合全量 JSON 转换

5. 使用 org.json:轻量工具类

功能特点

  • 轻量级
    :核心是一个工具类,适合简单场景。
  • 构造和解析简单
    :适合快速创建 JSON 或提取字段。
  • 灵活性一般
    :不支持复杂对象映射。

代码示例

import org.json.JSONObject;

public class OrgJsonExample {
    public static void main(String[] args) {
        String json = "{\"id\":1,\"name\":\"张三\"}";

        // 提取字段
        JSONObject jsonObject = new JSONObject(json);
        System.out.println(jsonObject.getString("name")); // 输出:张三

        // 构造 JSON
        JSONObject newJson = new JSONObject();
        newJson.put("id", 2);
        newJson.put("name", "李四");
        System.out.println(newJson.toString()); // 输出:{"id":2,"name":"李四"}
    }
}

优缺点

优点 缺点
轻量级,适合简单场景 不支持复杂嵌套对象
使用简单,学习成本低 功能简单,扩展性差

6. 手动解析 JSON:灵活度最高

功能特点

  • 完全自由
    :不依赖第三方库,自己解析 JSON。
  • 动态处理
    :适合不规则字段结构的 JSON。
  • 代码复杂度高
    :适合特殊场景。

代码示例

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Map;

public class ManualParsing {
    public static void main(String[] args) throws Exception {
        String json = "{\"id\":1,\"name\":\"动态字段\"}";

        ObjectMapper objectMapper = new ObjectMapper();
        Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {});
        System.out.println(map.get("name")); // 输出:动态字段
    }
}

优缺点

优点 缺点
灵活性高,适合动态字段 代码复杂度高,不易维护
不依赖第三方库 性能和效率低于专业 JSON 库

总结

最后给大家对比一下文章中提到的6种方法各自的优缺点:

方法 适用场景 优点 缺点
Jackson 企业级项目,复杂序列化和反序列化场景 功能强大,性能优异,支持复杂结构 配置复杂,学习曲线高
Gson 中小型项目,简单的 JSON 转换场景 轻量级,简单易用 功能有限,性能略逊
FastJSON 高性能需求,大数据量的动态解析 性能极高,功能丰富 曾有安全漏洞争议,社区支持稍逊 Jackson
JsonPath 嵌套结构复杂、动态字段提取场景 字段提取语法简单,灵活性强 不支持序列化和反序列化
org.json 快速解析或构造 JSON 场景 轻量级,适合简单场景 功能单一,扩展性差
手动解析 动态 JSON 或字段不固定的场景 自由度高,灵活性强 代码复杂,效率低于专业工具

工具千千万,场景最重要!

选对了工具,才能省时省力,少踩坑多摸鱼。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

在开发Web项目时,使用Jedis客户端与Redis进行交互时,通常建议将
JedisPool
设置为单例或静态的,而
Jedis
实例则不应该是单例的。
之前写过jedis使用注意事项
,大家可以先阅读一下,然后再看下面的最佳实践:

1.
JedisPool

设置为单例或静态

  • 原因

    • JedisPool
      是一个线程安全的对象,可以被多个线程共享。它负责管理连接到 Redis 的所有
      Jedis
      实例。
    • 每个
      JedisPool
      实例可以创建并管理多个
      Jedis
      连接,因此将其设置为单例可以有效地重用连接池中的连接,减少资源开销。

示例代码:

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisConnectionPool {
    private static final JedisPool pool = createPool();

    private static JedisPool createPool() {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(100); // 最大连接数
        config.setMaxIdle(50);   // 最大空闲连接数
        config.setMinIdle(10);   // 最小空闲连接数
        return new JedisPool(config, "localhost", 6379);
    }

    public static JedisPool getPool() {
        return pool;
    }
}

2.
Jedis

不设置为单例

  • 原因


    • Jedis
      实例不是线程安全的,每个线程在使用
      Jedis
      时都应从
      JedisPool
      中获取一个新的实例。这是因为
      Jedis
      对象会维护自己的状态(如连接、事务等),如果多个线程共享同一个
      Jedis
      实例,会导致数据竞争和不可预期的行为。
  • 获取方式


    • 使用
      JedisPool
      获取
      Jedis
      实例后,完成操作后应及时关闭该实例,以将其返回连接池。

示例代码:

import redis.clients.jedis.Jedis;

public class RedisService {
    public void performOperation() {
        try (Jedis jedis = RedisConnectionPool.getPool().getResource()) {
            // 在这里执行 Redis 操作
            jedis.set("key", "value");
            String value = jedis.get("key");
            System.out.println(value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

总结

  • JedisPool
    :应设置为单例或静态,以便在整个应用程序中重用。
  • Jedis
    :不应设置为单例,应通过
    JedisPool
    获取,并在使用后及时关闭,以确保连接的正确管理和释放。

这种设计模式可以有效地管理 Redis 连接,提高性能并避免潜在的线程安全问题。如果你有其他问题或需要进一步的帮助,请随时提问!