ArrayDeque集合源码分析

news2025/5/26 8:49:49

ArrayDeque集合源码分析

文章目录

  • ArrayDeque集合源码分析
  • 一、字段分析
  • 二、构造函数分析
  • 方法、方法分析
  • 四、总结


在这里插入图片描述

  • 实现了 Deque,说面该数据结构一定是个双端队列,我们知道 LinkedList 也是双端队列,并且是用双向链表 存储结构的。而 ArrayDeque 则是使用 环形数组 来实现双端队列。各有优缺点。
  • 实现代码比较少,难点在于使用了很多位运算进行了优化,来学习下ArrayDueue如何使用位运算代替 运算符的从而实现优化的。

一、字段分析

//用来存储元素的数组,这个数组可循环利用,有效数据范围为 [head, tail)注意tail 取不到
//也会出现tail < head 的情况,即前面的数据被废弃了,且tail到已经过了数组尾部了,然后跳到开始位置
//开始循环利用前面废弃的位置。
transient Object[] elements;
//指向头部有效数据
transient int head;
//指向尾部有效数据 + 1,或者理解为下一个添加元素的位置。整个过程,head 和 tail 都是不断变化的,head ~ tail - 1 范围内为有效数据。
//并不是tail 到达数组尾部即 elements.lengt - 1 就会扩容的。因为他可能复用前面可能发生废弃数组位置。
transient int tail;
//默认初始容量
private static final int MIN_INITIAL_CAPACITY = 8;

二、构造函数分析

//调用无参构造函数,直接创建一个容量为 16 的数组作为初始数组。
public ArrayDeque() {
        elements = new Object[16];
    }

//给定初始容量构建ArrayDeque,但并不是给 numElements 值为多少,就会创建多大的数组作为初始数组。
//给是判断并计算 最接近numElements的值,且为 2的幂次方的数作为初始容量。
public ArrayDeque(int numElements) {
		//分配容量
        allocateElements(numElements);
    }

private void allocateElements(int numElements) {
		//确定初始容量并构建数组,赋值给实际存储元素的 elements 数组
        elements = new Object[calculateSize(numElements)];
    }

//用来计算最终的容量的,计算结果为最接近 numElements 的数,且为 2的幂次方。
//比如 numElements = 15,则计算结果为 16(2^4)
//那么为什么一定要为 2的幂次方呢?为了在做取余运算时用位运算代替运算符运算,从而提高效率。
private static int calculateSize(int numElements) {
		//拿到默认最小的初始值,值为8
        int initialCapacity = MIN_INITIAL_CAPACITY;
        // Find the best power of two to hold elements.
        // Tests "<=" because arrays aren't kept full.
        //传入的 参数 numElements 比默认最小值还小,则直接使用默认的最小值为初始容量。,否则进行
        //计算,得到最接近 numElements ,且为 2 的次方的数。
        if (numElements >= initialCapacity) {
            initialCapacity = numElements;
            //为什么这样的计算能够得到 最接近 numElements ,且为 2 的幂次方的数呢?
            //首先列举下2的幂次方的二进制表示,来看下规律
            //0001  : 2^0
            //0010  :  2^1
            //0100	: 2^3
            //1000  : 2^4
            //上面例子可以看出,如果想要是 2的幂次方,那么整个二进制表示只能有一个1,其他的
            //1应该被消除,如果数中有不止一个1,那么最接近的应该是最高位的1前进一位,
            //且其他未都是0,该数就是最接近,且为2的幂次方。   
            //消除1的方法有所有位 都为1 在 + 1,则得到结果就有一个1,
            //如0111 + 1 =》 1000
            //我们知道一个数的最高位那一定是1了,其他位可能有1或0,而这个算法就是将其位
            //全部转化为1,之后再 + 1就是答案。
            //我们知道 Integer一共是4个字节,即32 位。
           	//如果极限状态第31位是1(32位为1位负数,不会走当前逻辑,会给默认的最小初始容量)
           	//我们需要将31位右边所有位都转化为1,我换一种说话,将右边连续的三十一位都转化为1。
           	//将右边连续的2位转化为1
            initialCapacity |= (initialCapacity >>>  1);
         	//将右边连续的4位转化为1
            initialCapacity |= (initialCapacity >>>  2);
       		//将右边连续的8位转化为1
            initialCapacity |= (initialCapacity >>>  4);
         	//将右边连续的16位转化为1
            initialCapacity |= (initialCapacity >>>  8);
      		//将右边连续的32位转化为1,都能将连续的32位都转为1,你少于32的难道还不能转化为1吗。
            initialCapacity |= (initialCapacity >>> 16);
            //都转为1的基础上+1,就得到大于且最接近当前数,且为2的幂次方的数。
            initialCapacity++;
            //可能计算结果为负数,比如第31位为1,计算后变成最后一位(即符号位)变为1,成了负数
            //所以Good luck allocating 2 ^ 30 elements 注释也给出了我们最大容量就是2^30
            if (initialCapacity < 0)   // Too many elements, must back off
                initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
        }
        return initialCapacity;
    }

public ArrayDeque(Collection<? extends E> c) {
		//判断是否需要扩容
        allocateElements(c.size());
        //将集合c中所有元素添加到ArrayDeque中
        addAll(c);
    }

//将集合c中所有元素添加到ArrayDeque中
public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        //循环遍历添加集合c中的元素
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }
    
//添加元素e
public boolean add(E e) {
		//添加元素到尾部
        addLast(e);
        return true;
    }

//添加元素到尾部,即tail位置
public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        //呼应我前面介绍tail的,tail 为下一个添加元素的位置,所以直接在该位置添加。 
        elements[tail] = e;
        //判断队列是否已满了,其实效果就是 tail + 1 后是否和 head 重合,即 tail + 1 == head ???
        //如何理解这段代码呢,就要结合我上面说的 为什么 elements 的数组长度设计为 2的幂次方有关了!!
        //正因为 elements.length 是2的幂次方,那么它 - 1 后二进制的最高位(该位置记为@)由 1 -> 0,而所有的低位变为1
        //在和tail 做 & 运算时,就会将tail二进制 @ 位及更高位全部转化为0,而@后面的全部转化为1,
        //相当于 tail % elements 做取余运算。
        //所以将elements.length设为2的幂次方好处就是:在判断tail 与 head 的关系是,
        //可用二进制运算取余代替 运算符取余运算,提高效率。
        //name 有一个疑问了,为什么不直接用 tail + 1 == head ? 不就行了吗??算都不用算不是更快吗??
        //因为要考虑越界的情况!!elements 是一个可循环使用的环形数组,比如 tail = elements.lenth - 1,tail + 1后越界了
        //所以要做 & 运算!!
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        	//tail 与 head重合,需要扩容了
            doubleCapacity();
    }

private void doubleCapacity() {
		//断言, head != tail 就会抛出异常。也可体现出,ArrayDequeue 当 head == tail 时,进行扩容。 
        assert head == tail;
        //记录头结点
        int p = head;
        //当前数组容量
        int n = elements.length;
        //head 到 elements.length - 1 ,共有多少元素 (p,elements.length - 1]
        int r = n - p; // number of elements to the right of p
        //将数组的 容量扩容两倍
        int newCapacity = n << 1;
        //越界爬出异常
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
       	//使用扩容后的容量创建新的数组    
        Object[] a = new Object[newCapacity];
        //相当于将 老数组 head 到 数组末尾的数据,复制到新数组中,复制到新数组下标0开始位置,长度就是r
        //你可以理解为head位起始位置,从head一直复制到tail,依次复制给新数组,从0 开始的位置。
        System.arraycopy(elements, p, a, 0, r);
        //将老数组o 位置开始以此复制给新数组。
        //这两部给个过程就明白了
        //				
        // 1. element[0, 1, 2, 3, 4, 5]   => newElements[3,4,5,0,0,0,0,0,0,0];
        //                 head
        // 2. element[0, 1, 2, 3, 4, 5]   => newElements[3,4,5,0,1,2,0,0,0,0];
        //                 head
        System.arraycopy(elements, 0, a, r, p);
        //elements 指向新的数组
        elements = a;
        //head 指向索引 0
        head = 0;
        //tail 指向最后一个位置的下一位,即下一个添加进ArrayDequeue元素的位置。
        tail = n;
    }

方法、方法分析

  • 添加元素方法。

//添加元素到尾部,即tail位置
public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        //呼应我前面介绍tail的,tail 为下一个添加元素的位置,所以直接在该位置添加。 
        elements[tail] = e;
        //判断队列是否已满了,其实效果就是 tail + 1 后是否和 head 重合,即 tail + 1 == head ???
        //如何理解这段代码呢,就要结合我上面说的 为什么 elements 的数组长度设计为 2的幂次方有关了!!
        //正因为 elements.length 是2的幂次方,那么它 - 1 后二进制的最高位(该位置记为@)由 1 -> 0,而所有的低位变为1
        //在和tail 做 & 运算时,就会将tail二进制 @ 位及更高位全部转化为0,而@后面的全部转化为1,
        //相当于 tail % elements 做取余运算。
        //所以将elements.length设为2的幂次方一个好处就是:在判断tail 与 head 的关系是,
        //可用二进制运算取余代替 运算符取余运算,提高效率。
        //name 有一个疑问了,为什么不直接用 tail + 1 == head ? 不就行了吗??算都不用算不是更快吗??
        //因为要考虑越界的情况!!elements 是一个可循环使用的环形数组,比如 tail = elements.lenth - 1,tail + 1后越界了
        //所以要做 & 运算!!
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        	//tail 与 head重合,需要扩容了
            doubleCapacity();
    }

//向 head - 1 处添加元素
public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
            //取余运算,同时防止了越界。将head - 1 处复制e
        elements[head = (head - 1) & (elements.length - 1)] = e;
       //判断是否扩容
        if (head == tail)
        	//扩容,扩容方法看构造方法里介绍了
            doubleCapacity();
    }
//向 tail 位置添加元素
public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
            //直接给tail 位置赋值
        elements[tail] = e;
        //取余元素,判断是否扩容
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        	//扩容
            doubleCapacity();
    }
    
//向 tail 处添加元素    
public boolean offerLast(E e) {
        addLast(e);
        return true;
    }
    
//向 head - 1 处添加元素
public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }

//向tail 处添加元素
public boolean add(E e) {
        addLast(e);
        return true;
    }
//向tail 处添加元素
public boolean offer(E e) {
        return offerLast(e);
    }
//向 head - 1处添加元素
 public void push(E e) {
        addFirst(e);
    }
  • 获取元素方法。
//弹出 head 位置元素
public E pollFirst() {
        int h = head;
        @SuppressWarnings("unchecked")
        E result = (E) elements[h];
        // Element is null if deque empty
        if (result == null)
            return null;
        //弹出后将head位置设为null
        elements[h] = null;     // Must null out slot
        //获取head的下一个位置,取余运算防止越界。
        head = (h + 1) & (elements.length - 1);
        //返回结果
        return result;
    }

//弹出 tail - 1 处元素
public E pollLast() {
		//获取元素位置,取余防止越界
        int t = (tail - 1) & (elements.length - 1);
        @SuppressWarnings("unchecked")
        E result = (E) elements[t];
        if (result == null)
            return null;
        //弹出后,该位置设为null    
        elements[t] = null;
        //新的tail
        tail = t;
        return result;
    }

//获取head位置元素,但不弹出
public E getFirst() {
        @SuppressWarnings("unchecked")
        E result = (E) elements[head];
        if (result == null)
            throw new NoSuchElementException();
        return result;
    }

//获取tail-1位置处元素,但不弹出
public E getLast() {
        @SuppressWarnings("unchecked")
        E result = (E) elements[(tail - 1) & (elements.length - 1)];
        if (result == null)
            throw new NoSuchElementException();
        return result;
    }

//获取head位置元素,但不弹出
public E peekFirst() {
        // elements[head] is null if deque empty
        return (E) elements[head];
    }

//获取tail-1位置处元素,但不弹出
public E peekLast() {
        return (E) elements[(tail - 1) & (elements.length - 1)];
    }

//弹出 head 位置元素
public E poll() {
        return pollFirst();
    }


//获取head位置元素,但不弹出
public E element() {
        return getFirst();
    }


//获取head位置元素,但不弹出
public E peek() {
        return peekFirst();
    }
    
//弹出 head 位置元素
public E pop() {
        return removeFirst();
    }
  • 移除元素方法:
//删除 head 处元素
public E removeFirst() {
		//删除 head 处元素
        E x = pollFirst();
        if (x == null)
            throw new NoSuchElementException();
         //返回被删除的元素   
        return x;
    }
    
//删除 tail - 1 处的元素
public E removeLast() {
		删除 tail - 1 处的元素
        E x = pollLast();
        if (x == null)
            throw new NoSuchElementException();
         //返回被删除的元素    
        return x;
    }

//删除 head 处元素
public E pollFirst() {
		//记录head位置
        int h = head;
        @SuppressWarnings("unchecked")
        //记录head处元素,用于返回
        E result = (E) elements[h];
        // Element is null if deque empty
        if (result == null)
            return null;
        //将head处元素设为空    
        elements[h] = null;     // Must null out slot
        //计算新的head,取余元素防止越界
        head = (h + 1) & (elements.length - 1);
        return result;
    }

//删除 tail - 1 处的元素
public E pollLast() {
		//获取 tail -1 位置下标,取余运算,防止越界
        int t = (tail - 1) & (elements.length - 1);
        @SuppressWarnings("unchecked")
        //获取删除元素位置
        E result = (E) elements[t];
        if (result == null)
            return null;
        //tail - 1位置被置空    
        elements[t] = null;
        //更新tail位置
        tail = t;
        return result;
    }

//删除head处元素
public E remove() {
        return removeFirst();
    }
    
//删除head出元素
public E poll() {
        return pollFirst();
    }

//删除head处元素
public E pop() {
        return removeFirst();
    }
  • 扩容方法:在介绍构造函数时,一并介绍了。

  • 迭代器:

private class DeqIterator implements Iterator<E> {
        /**
         * Index of element to be returned by subsequent call to next.
         */
        //用于记录迭代得到下一个元素的下标位置。初始值当时是head了。
        private int cursor = head;

        /**
         * Tail recorded at construction (also in remove), to stop
         * iterator and also to check for comodification.
         */
         //用于记录迭代的中止位置
        private int fence = tail;

        /**
         * Index of element returned by most recent call to next.
         * Reset to -1 if element is deleted by a call to remove.
         */
         //用于记录最后一次迭代得到的元素位置,迭代还未开始,当然给-1了。
        private int lastRet = -1;
		
		//判断是否还有元素可以迭代
        public boolean hasNext() {
            return cursor != fence;
        }
		
		//迭代获取下一个元素
        public E next() {
        	//无元素了,报错
            if (cursor == fence)
                throw new NoSuchElementException();
            @SuppressWarnings("unchecked")
            //获取cursor处的位置元素
            E result = (E) elements[cursor];
            // This check doesn't catch all possible comodifications,
            // but does catch the ones that corrupt traversal
            //判断迭代过程中集合是否被修改过。和 modCount作用相同
            if (tail != fence || result == null)
                throw new ConcurrentModificationException();
            //记录最后一次迭代得到元素的位置    
            lastRet = cursor;
            //获取下一次迭代元素位置下标,位运算,防止越界
            cursor = (cursor + 1) & (elements.length - 1);
            return result;
        }
		
		//迭代删除,删除的是最后以此迭代得到的元素
        public void remove() {
        	//以此还被迭代呢,删除失败
            if (lastRet < 0)
                throw new IllegalStateException();
                //删除元素
            if (delete(lastRet)) { // if left-shifted, undo increment in next()
                cursor = (cursor - 1) & (elements.length - 1);
                fence = tail;
            }
            //上一次迭代的元素被删除了,所以即为-1。所以迭代过程中不能连续remove。
            lastRet = -1;
        }
    }

四、总结

  • 使用的是可循环使用的双指针数组来存储结构。可以有效的减少扩容次数,并且提高资源利用率。不支持存储null元素。
  • 是线程不安全的。
  • 可当做链表,栈,队列使用。在头部和尾部插入或者删除元素,时间复杂度为 O(1),但是在扩容的时候需要批量移动元素,其时间复杂度为 O(n)。
  • 扩容的时候,将数组长度扩容为原来的 2 倍,即 n << 1。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1504919.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入探讨AI团队的角色分工

目录 前言1. 软件工程师&#xff1a;构建系统基石的关键执行者2. 机器学习工程师&#xff1a;数据与模型的塑造专家3. 机器学习研究员&#xff1a;引领算法创新的智囊4. 机器学习应用科学家&#xff1a;理论与实践的巧妙连接5. 数据分析师&#xff1a;洞察数据&#xff0c;智慧…

如何在Linux上为PyCharm创建和配置Desktop Entry

在Linux操作系统中&#xff0c;.desktop 文件是一种桌面条目文件&#xff0c;用于在图形用户界面中添加程序快捷方式。本文将指导您如何为PyCharm IDE创建和配置一个 .desktop 文件&#xff0c;从而能够通过应用程序菜单或桌面图标快速启动PyCharm。 步骤 1: 确定PyCharm安装路…

力扣中档题:删除排序链表中的 重复元素

此题可以选择暴力解决&#xff0c;首先将链表中的元素放到数组中&#xff0c;然后将数组中的重复元素放到另一个数组中&#xff0c;最后再判断并将目标值放到第三个数组中排序再改链表&#xff0c;注意链表nextNULL的操作 struct ListNode* deleteDuplicates(struct ListNode*…

【Nestjs实操】环境变量和全局配置

一、环境变量 1、使用dotenv 安装pnpm add dotenv。 根目录下创建.env文件&#xff0c;内容如下&#xff1a; NODE_ENVdevelopment使用 import {config} from "dotenv"; const path require(path); config({path:path.join(__dirname,../.env)}); console.log(…

简介:基于 OpenTiny 组件库的 rendereless 无渲染组件架构

在 HAE 自研阶段&#xff0c;我们实现的数据双向绑定、面向对象的 JS 库、配置式开发的注册表等特性&#xff0c;随着前端技术的高速发展现在已经失去存在的意义&#xff0c;但是在 AUI 阶段探索的新思路新架构&#xff0c;经过大量的业务落地验证&#xff0c;再次推动前端领域…

万用表数据导出变化曲线图——pycharm实现视频数据导出变化曲线图

万用表数据导出变化曲线图——pycharm实现视频数据导出变化曲线图 一、效果展示二、环境配置三、代码构思四、代码展示五、代码、python环境包链接 一、效果展示 图1.1 效果展示 &#xff08;左图&#xff1a;万用表视频截图&#xff1b;右图&#xff1a;表中数据变化曲线图&am…

宽度优先搜索算法(BFS)

宽度优先搜索算法&#xff08;BFS&#xff09;是什么&#xff1f; 宽度优先搜索算法&#xff08;BFS&#xff09;&#xff08;也称为广度优先搜索&#xff09;主要运用于树、图和矩阵&#xff08;这三种可以都归类在图中&#xff09;&#xff0c;用于在图中从起始顶点开始逐层…

字节跳动的 SDXL-LIGHTNING : 体验飞一般的文生图

TikTok 的母公司字节跳动推出了最新的文本到图像生成人工智能模型&#xff0c;名为SDXL-Lightning。顾名思义&#xff0c;这个新模型只需很轻量的推理步骤&#xff08;1&#xff0c;4 或 8 步&#xff09;即可实现极其快速且高质量的文本到图像生成功能。与原始 SDXL 模型相比&…

嵌入式 Linux 学习

在学习嵌入式 Linux 之前&#xff0c;我们先来了解一下嵌入式 Linux 有哪些东西。 1. 嵌入式 Linux 的组成 嵌入式 Linux 系统&#xff0c;就相当于一套完整的 PC 软件系统。 无论你是 Linux 电脑还是 windows 电脑&#xff0c;它们在软件方面的组成都是类似的。 我们一开电…

.NET高级面试指南专题十六【 装饰器模式介绍,包装对象来包裹原始对象】

装饰器模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式&#xff0c;用于动态地给对象添加额外的职责&#xff0c;而不改变其原始类的结构。它允许向对象添加行为&#xff0c;而无需生成子类。 实现原理&#xff1a; 装饰器模式通过创建一个包装对象来包裹原…

【数据可视化】动手用matplotlib绘制关联规则网络图

下载文中数据、代码、绘图结果 文章目录 关于数据绘图函数完整可运行的代码运行结果 关于数据 如果想知道本文的关联规则数据是怎么来的&#xff0c;请阅读这篇文章 绘图函数 Python中似乎没有很方便的绘制网络图的函数。 下面是本人自行实现的绘图函数&#xff0c;如果想…

【深度学习笔记】6_9 深度循环神经网络deep-rnn

注&#xff1a;本文为《动手学深度学习》开源内容&#xff0c;部分标注了个人理解&#xff0c;仅为个人学习记录&#xff0c;无抄袭搬运意图 6.9 深度循环神经网络 本章到目前为止介绍的循环神经网络只有一个单向的隐藏层&#xff0c;在深度学习应用里&#xff0c;我们通常会用…

three.js如何实现简易3D机房?(四)点击事件+呼吸灯效果

接上一篇&#xff1a; three.js如何实现简易3D机房&#xff1f;&#xff08;三&#xff09;显示信息弹框/标签&#xff1a;http://t.csdnimg.cn/5W2wA 目录 八、点击事件 1.实现效果 2.获取相交点 3.呼吸灯效果 4.添加点击事件 5.问题解决 八、点击事件 1.实现效果 2.…

ChatGPT发不出消息?GPT发不出消息怎么办?

前言 今天发现&#xff0c;很多人的ChatGPT无法发送信息&#xff0c;我就登陆看一下自己的GPT的情况&#xff0c;结果还真的无法发送消息&#xff0c;ChatGPT 无法发送消息&#xff0c;但是能查看历史的对话&#xff0c;不过通过下面的方法解决了。 第一时间先打开官方的网站&a…

Mint_21.3 drawing-area和goocanvas的FB笔记(七)

FreeBASIC gfx 基本 graphics 绘图 8、ScreenControl与屏幕窗口位置设置 FreeBASIC通过自建屏幕窗口摆脱了原来的屏幕模式限制&#xff0c;既然是窗口&#xff0c;在屏幕坐标中就有它的位置。ScreenControl GET_WINDOW_POS x, y 获取窗口左上角的x, y位置&#xff1b;ScreenC…

【REST2SQL】11 基于jwt-go生成token与验证

【REST2SQL】01RDB关系型数据库REST初设计 【REST2SQL】02 GO连接Oracle数据库 【REST2SQL】03 GO读取JSON文件 【REST2SQL】04 REST2SQL第一版Oracle版实现 【REST2SQL】05 GO 操作 达梦 数据库 【REST2SQL】06 GO 跨包接口重构代码 【REST2SQL】07 GO 操作 Mysql 数据库 【RE…

设计模式学习系列 -- 随记

文章目录 前言 一、设计模式是什么&#xff1f; 二、设计模式的历史 三、为什么以及如何学习设计模式&#xff1f; 四、关于模式的争议 一种针对不完善编程语言的蹩脚解决方案 低效的解决方案 不当使用 五、设计模式分类 总结 前言 最近可能工作生活上的稳定慢慢感觉自己丢失…

掌握 Vue3、Vite 和 SCSS 实现一键换肤的魔法步骤

前言 一个网站的换肤效果算是一个比较常见的功能&#xff0c;尤其是在后台管理系统中&#xff0c;我们几乎都能看到他的身影&#xff0c;这里给大家提供一个实现思路。 搭建项目 vitevue3搭建项目这里就不演示了&#xff0c;vite官网里面讲得很清楚。 注&#xff1a;这里使…

浅析开源内存数据库Fastdb

介绍&#xff1a; Fastdb是免费开源内存数据库&#xff0c;其优秀的性能&#xff0c;和简洁的C代码&#xff0c;让我学习使用过程中收益颇多&#xff0c;但是国内中文相关研究的文章相当稀少&#xff0c;外文我查询相当不便。有兴趣的朋友可以通过以下网站访问&#xff1a;Mai…

java-ssm-jsp基于ssm的冰淇淋在线购买网站

java-ssm-jsp基于ssm的冰淇淋在线购买网站 获取源码——》公主号&#xff1a;计算机专业毕设大全