深入理解HashMap

news2024/11/16 7:38:22

HashMap集合

1. HashMap集合简介

  • HashMap基于哈希表的Map接口实现,是以key-value存储形式存在,即主要用来存放键值对。hashMap的实现不是同步的,这就意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
    JDK1.8之前的HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突(两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同)而存在的。
    JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为8)并且当前数组的长度大于64时,此时索引位置上的所有数据改为使用红黑树存储。
    补充:将链表转换成红黑树前会判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表变成红黑树。而是选择进行数组扩容。这样做的目的时因为数组比较小,尽量避开红黑树结构,这种情况下变成红黑树结构,反而会降低效率,因为红黑树需要进行左旋、右旋,变色这些操作来保持平衡。同时数组长度小于64,搜索时间相对要快一些。所以为了提高性能和减少搜索时间,底层在阈值大于8并且数组大于64时,链表才会转为红黑树。具体可以参考treeifyBin方法。
    虽然增加了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于8并且数组长度大于64时,链表转换为红黑树时,效率也变得更加高效。

2.HashMap集合底层的数据结构

存储过程:

HashMap<String,Integer> hashMap=new HashMap();
  1. 当创建HashMap集合对象的时候,在jdk8前,构造方法中创建了一个默认长度是16的Entry[ ]table 用来存储键值对数据的。在jdk8以后不是在HashMap的构造方法底层创建数组了,是在第一次调用put方法时创建数组的,Node [ ]table用来存储键值对的数据。

  2. 假设想=向哈希表中存储“张三-18”的数据,根据张三调用String类中重写之后的hashCode()方法计算出哈希值,然后结合数组长度采用某种算法计算出Node数组中存储数据空间的索引值。如果计算出的索引空间没有数据,则直接将“张三-18”存储到数组中。

  3. 向哈希表中存储数据“李四-20”数据,假设李四计算出的hashCode方法结合数长度计算出的索引值与张三一致,那么此时数组空间不是null,此时底层会比较张三和李四的hash值是否一直,如果不一致,则在此空间上画出一个节点来存储键值对数据"李四-20"。

  4. 假设向哈希表中存储数据“张三-20”,那么首先根据张三调用的hashCode方法结合数组长度计算出索引肯定与张三一致此时比较存储的数据颤三和已经存储的数据的hash值是否相等,如果相等,此时发生哈希冲突。那么底层就会调用张三所属类String中的equals方法比较两个内容是否相等?若相等,则将后添加的数据覆盖之前的value。若不相等,那么继续向下和其他的数据的key进行比较,如果都不相等,在画出一个节点存储数据。

哈希表底层采用何种算法计算hash值?还有哪些算法可以计算出hash值?
底层采用的Key的hashCode方法的值结合数组长度进行无符号右移(>>>)、按位异或(^)、按位与(&)计算出索引,还可以采用:平方取中法、取余数、伪随机数法(这些计算方式效率比较低,而位运算效率要高)

3. HashMap继承关系

继承关系图
说明

  • Cloneable 克隆接口,表示可以克隆。创建并返回HashMap对象的一个副本。
  • Serializable 序列化接口。属于标记性接口。HashMap对象可以被序列化和反序列化。
  • AbstractMap 父类提供了Map实现接口。以最大限度地减少实现此接口所需的工作。
补充:通过上述继承关系我们发现了一个很奇怪的现象,就是HashMap已经继承了AbstractMap 而AbstractMap类实现了Map接口,那么为什么HashMap还要在实现Map接口呢?同样在在ArrayList中LinkedList中都是这种结构。
  • 据Java集合框架的创始人Josh Bloch描述,这样的写法是一个失误。 在Java集合框架中,类似这样的写法很多,最开始写Java集合框架的时候,认为这样写,在某些地方是有价值的,后来他意识到错了。显然的,JDK的维护者,不认为这个小小的失误值得去修改,所以就这样保存下来了。

4.HashMap成员变量

  1. 序列化版本号
private static final long serialVersionUID = 362498820763181265L;
  1. 集合的初始化容量(必须是2的n次幂
//默认的初始容量是16相    1<<4相当于1*2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
问题:为什么必须是2的n次幂呢,如果不是2的n次幂会怎么样?
  • HashMap的构造方法还可以指定集合的初始化容量大小;
HashMap(int initialCapacity) 构造一个带指定容量和默认加载因子(0.75)的空HashMap.
  • 当向HashMap中添加一个元素的时候,要根据key的hash值,去确定其在数组中的具体位置。HashMap为了存取高效,要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存在哪个链表的算法。这个算法实际上就是去模,hash%length,计算机中直接求余效率不如位移运算,所以源码中做了优化,使用hash&(length-1),而实际上hash%length等于hash&(length-1)的前提就是length是2的n次幂。
为什么这样能均匀减少碰撞呢?2的n次方实际上就是1后面呢跟n个0,2的n次方-1实际上就是n个1;
例如长度是8的时候,3&(8-1)=3 2&(8-1)=2,不同位置,不冲突
长度为8的时候,8是2的三次方,二进制是1000;
	1000
 -     1
 -----------------
     111

数组长度为8的时候
数组长度为9的时候

如果创建HashMap对象时,输入的数组长度是10,不是2的次幂,HashMap会通过位运算和或运算得到2的幂次数,并且是大于且最接近的那个数。
//创建HashMap集合的时候,数组长度设置为10(不是2的幂)
HashMap hashmap=new HashMap(10);

public HashMap(int initialCapacity) {//initialCapacity=10
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

 public HashMap(int initialCapacity, float loadFactor) {//initialCapacity
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }


 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

由此可见,当在实例化HashMap实例时,如果给定了initialCapacity(假设是10),由于HAshMap的Capacity必须是2的幂,因此这个方法用于找到大于等于initialCapacity的最小2的幂。(如果initialCapacity就是2的幂,则不变)
下面分析这个算法

  • 为什么要对cap做减1操作。(int n =cap-1;)
    这是为了防止,cap已经是2的幂,如果cap已经是2的幂,有没有执行这个减一操作,则执行后面几条无符号右移的操作之后,返回的Capacity僵尸这个cap的2倍。
  • 如果n这个时候为0(经过了cap-1之后),则经过后面的几次无符号右移仍然是0,最后返回的Capacity是1(最后有个n+1);
  • 注意:|(按位或运算):运算规则:相同的二进制位上,都是0的时候结果为0,否则为1;
第一次右移

第一次右移

第二次右移

第二次右移

第三次右移

第三次右移
这次把已经有的高位中的连续的4个1,右移4位,在做或操作,这样n的二进制表示的高位中正常会有8个连续的1。如0000 1111 1111 xxxx。以此类推。注意,容量最大也就是32bit的整数,因此最后n|=n>>>16;最多也就是32个1(但这已经是负数了。在执行tableSizeFor之前,对initialCapacity做了判断,如果大于MAXIMUM_CAPACITY(230),则取MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY,会执行移位操作,所以这里面的移位操作之后,最大30个1,不会大于等于MAXIMUM_CAPACITY。30个1加1之后得到230次方)

  1. 默认的加载因子,默认值是0.75
static final float DEFAULT_FACTOR = 0.75f;
  1. 集合最大容量
//集合最大容量的上限是:2的30次方
static final float MAXIMUM_CAPACITY = 1 << 30;
  1. 当链表的值超过8则会转为红黑树(JDK1.8新增)
//当桶(bucket)上的结点个数大于这个值时会转成红黑树
static final float TREEIFY_THRESHOLD = 8;
  1. 当链表的值小于6则会从红黑树转回链表
//当桶(bucket)上的结点小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
  1. 当Map里面的数量超过这个值时,表中的桶才能进行树形化,否则桶内元素太多会扩容,而不是树形化,为了避免惊醒扩容、树形化选择的冲突,这个值不能小于4*TREEIFY_THRESHOLD (8)
//桶中结构转化为红黑树对应数组长度的最小值
static final int MIN_TREEIFY_CAPACITY= 64;
  1. ** table用来初始化(必须是2的n次幂)**
//存储元素的数组
transient Node<K,V>[] table;

table在JDK1. 8中我们了解到HashMap是由数组加链表加红黑树来组成的结构其中table就是HashMap中的数组,jdk8之前数组类型是Entry<K,V>类型。从jdk1 .8之后是Node<K,V>类型。只是换了个名字,都实现了一样的接口: Map.Entry<K,V> 。负责存储键值对数据的。

  1. 用来存放缓存
//存放具体元素的集合
transient Set<Map.Entry<K,V>> entrSet;
  1. HashMap中存放元素的个数
//存放元素的个数,注意这个不等于数组的长度
transient int size;
//size为HashMap中K-V的实时数量,不是数组table的长度。
  1. 用来记录HashMap的修改次数
//每次扩容和更改map结构的计数器
transient int modCount;
  1. 用来调整大小下一个容量的值计算方式为(容量*负载因子)
// 临界值 当实际大小(容量*负载因子)超过临界值时,会进行扩容
int threshold;
  1. 哈希表的加载因子
//加载因子
final float loadFactor;
//loadFactor加载因子,时用来衡量HashMap满的程度,表示HashMap的疏密程度,影响hash操作到同一个数组位置的概率。计算HashMap的实时加载因子的方法为:size/capacity 而不是占用桶的数量去除以capacity。(capacity时桶的个数,也就是table的长度length。)
//loadFactor 太大会导致查找元素效率低,太小岛主数组的利用率低,存放数据会很分散。loadFactor的默认值为0.75,是官方给出的一个比较好的临界值。
//当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太急了,需要扩容,而扩容这个过程涉及到rehash、复制数据等操作,非常消耗性能,所以在开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。
//同时在HashMap的构造器中可以定制loadFactor。
//构造方法
HashMapint initialCapacity,float loadFactor)
//构造一个代指定容量和加载因子的空HashMap
构造方法

HashMap中重要的构造方法,它们分别是

  1. 构造一个空的HashMap,默认初始容量为(16),默认负载因子(0.75).
public HashMap(){
 this.loadFactor = DEFAULT_LOAD_FACTOR; // 将默认的加载因子0.75赋值给loadFactor,并没有创建数组
 }
  1. 构造一个具有指定的初始容量和迷人负载因子(0.75)HashMap。
// 指定“容量大小”的构造函数
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

  1. 构造一个具有初始容量和负载因子的HashMap.
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
  1. 包含另一个“Map”的构造函数
//构造一个映射关系与指定map相同的新HashMap。
 public HashMap(Map<? extends K, ? extends V> m) {
 //负载因子loadFactor变成默认的负载因子0.75;
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

最后调用了putMapEntries

    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

注意: float ft = (float)s / loadFactor) + 1.0F;这一行代码中为什么要加1.0F ?
s/loadFactor的结果是小数,加1.0F与(int)ft相当于 是对小数做一个向, 上取整以尽可能的保证更大容量,更大的容量能够减少resize的调用次数。所以+ 1.0F是为了获取更大的容量。
例如:原来集合的元素个数是6个,那么6/0.75是8, 是2的n次幕,那么新的数组大小就是8了。然后原来数组的数据就会存储到长度是8的新的数组中了,这样会导致在存储元素的时候,容量不够,还得继续扩容,那么性能降低了,而如果+1呢,数组长度直接变为16了,这样可以减少数组的扩容。

成员方法
  1. 增加方法

put方法是比较复杂的,实现步骤大致如下:

1)先通过hash值计算出key映射到哪个桶;
2)如果桶上没有碰撞冲突,则直接插入;
3)如果出现碰撞冲突了,则需要处理冲突:
  a:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据; .
  b:否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
4)如果桶中存在重复的键,则为该键替换新值value;
5)如果size大于阈值threshold, 则进行扩容;

具体的方法如下:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
` ``

```java
   static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

2.putVal()方法

现在看putVal()方法,看看它到底做了什么。
主要参数:
●hash key的hash值
●key 原始Key
●value要存放的值
●onlylfAbsent如果true代表不更改现有的值
●evict 如果为false表示table为创建状态

putVal()方法源代码如下所示: .

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)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 >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && 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 > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
扩容方法resize

●1.什么时候才需要扩容
●2.HashMap的扩容是什么
1.什么时候才需要扩容
当HashMap中的元素个数超过数组大(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值(DEFAULT_ LOAD_ FACTOR)是0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中的元素个数超过16x0.75=12(这个值就是阈值或者边界值threshold值)的时候,就把数组的大小扩展为2x16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预知元素的个数能够有效的提高HashMap的性能。
补充:
当HashMap中的其中一个链表的对象个数如果达到了8个, 此时如果数组长度没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链表会变成红黑树,节点类型由Node变成TreeNode类型。当然,如果映射关系被移除后,下次执行resize方法时判断树的节点个数低于6,也会再把树转换为链表。

HashMap的扩容是什么?

  • 进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的,在编写程序中要尽量避免resize。
  • HashMap在进行扩容时,使用的rehash方式非常巧妙,因为每次扩容都是翻倍,与原来的(n-1)&hash的结果相比,只是多了一个bit位,所以节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。

resize示意图
源码

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 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 < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & 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;
    }

为什么要引入红黑树?

JDK1.8以前的HashMap的是现实数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当HashMap中有大量元素都存放在同一个桶中时,这个桶下有一条长长的链表,这个时候HashMap就相当于一个单链表,假如单链表有n个元素,便利的时间复杂度就是o(n),完全失去了它的优势。针对这种情况,JDk1.8中引入了红黑树(查找时间复杂度为o(logn))来优化这个问题。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断边长,肯定会对查询性能有一定的影响,所以才需要转成树。

为什么阈值是8?

8这个阈值定义在HashMap中,针对这个成员变量,在源码的注释中只说明了8是bin(bin就是bucket(桶))从;i按表转成树的阈值,但是并未说明为什么是8但是在HashMap中有一段注释说明:我们继续往下看:

 Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:

因为树节点的大小大约是普通节点的两倍,所以我们只在箱子包含足够的节点时才使用树节点。当他们变得太小(由于删除或调整大小)时,就会被转换回普通的桶。在使用分布良好的用户hashcode时,很少使用树箱。理想情况下箱子中节点的频率服从泊松分布总之在权衡空间和时间的复杂度之下,确定阈值为8。

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

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

相关文章

短视频上热门技巧总结,这样做你也可以快速上热门。

最近开始做短视频&#xff0c;找了很多短视频运营创作技巧&#xff0c;但能上热门的只有那么几个&#xff0c;经过近一周的分析&#xff0c;结合了我赢上短视频运营创作技巧&#xff0c;得到了以下几个经典技巧合集&#xff1a;学会一个就值了。 首先说一下&#xff1a;什么样的…

团簇生长过程-Ovito渲染

文章目录一、选择出团簇原子和非团簇原子1. 选择团簇原子2. 删除非团簇原子二、选择出团簇原子和非团簇原子1. 团簇分析2. 团簇具体信息三、渲染团簇1、 对团簇进行选择2、 获得团簇渲染后的结果四、渲染结果五、 案例dump下载博文《根据近邻列表法识别团簇—冷凝成核 MatlabOv…

java06-面向对象1

一&#xff1a;面向对象学习内容&#xff1a; 1.java 类及成员&#xff1a;属性、方法、构造器、代码块、内部类 2.面向对象三大特征&#xff1a;封装、继承、多态 3.其他关键字&#xff1a;this、super static、final、abstact、interface 、package、import 二&#xff…

iPhone/iPad上值得推荐的5个免费PDF转Word

PDF 文件是在不同平台上传输数据的最便捷方式&#xff0c;可确保保持高端信息质量。处理将不同文件格式转换为 PDF 的任务通常很麻烦&#xff0c;尤其是在 iOS 设备上。为了解决这个问题&#xff0c;这里讨论了您可以轻松依赖的前 5 个iPhone PDF 转换器工具。 适用于 iPhone 和…

多传感器融合定位六-惯性导航原理及误差分析

多传感器融合定位六-惯性导航原理及误差分析1. 惯性技术简介1.1 惯性技术发展历史1.2 惯性器件1.2.1 机械陀螺(几乎没人用了)1.2.2 激光陀螺1.2.3 光纤陀螺1.2.4 MEMS陀螺(常用)1.2.5 加速度计2. 惯性器件误差分析2.1 信号误差组成2.2 Allan方差分析3. 惯性器件内参标定3.1 惯性…

十六、状态管理——Vuex(1)

本章概要 简单的状态管理安装 Vuex基本用法 Vuex 是一个专门为 Vue.js 应用程序开发的状态管理模式。他采用集中式存储来管理应用程序中所有组件的状态&#xff0c;并以相应的规则保证状态以一种可预测的方式发生变化。 Vuex 也被集成到了 Vue 官方调试工具 vue-devtools 中…

跨年夜,想请你看一场烟花秀!

代码分享地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1Cu_lKYfAlMBDttSzhVXPuQ 提取码&#xff1a;2ocd 代码效果展示&#xff1a; 源代码分享如下&#xff1a; <!--* Author: Xiao Wang* Date: 2022-12-30 14:26* Description: --> <!DOCTYPE html …

剑指 Offer 20. 表示数值的字符串

题目 请实现一个函数用来判断字符串是否表示数值&#xff08;包括整数和小数&#xff09;。 数值&#xff08;按顺序&#xff09;可以分成以下几个部分&#xff1a; 若干空格 一个 小数 或者 整数 &#xff08;可选&#xff09;一个 ‘e’ 或 ‘E’ &#xff0c;后面跟着一个…

canopen11-sdo-2b写入命令

源码 1、SDO介绍 就对象而言,主机要访问节点词典的数据,因此主机是client客户端,节点是server服务器。上传与下载是对服务器来说的(这点和常识有点不太一样)。因此,上传指的是服务器发送数据给客户端,下载是客户端给服务器数据。 我们这里要用主机访问节点服务器2000位…

Go 语言从入门到实战

《Go 语言从入门到实战》 的学习笔记&#xff0c;欢迎阅读斧正。感觉该专栏整体来说对有些后端编程经验的来说比无后端编程经验的人更友好。 数据类型 运算符 算数运算符 比较运算符 用 比较数组 相同维数切含有相同个数元素的数组才可以比较&#xff0c;每个元素都相同的才…

四旋翼无人机学习第18节--cadence的bom表、网表导出,PCB板创建,层叠设置

文章目录1 bom表导出2 网表导出3 PCB板创建4 PCB板文件重要设置5 层叠设置1 bom表导出 1、选择DSN文件&#xff0c;之后依次点击Tools&#xff0c;Bill of Materials。 2、当然&#xff0c;也可以点击图标进行bom导出。 3、下图标出的地方可以自行修改&#xff0c;最后点击O…

设计师必须知道的 5个设计灵感网站

设计没灵感&#xff0c;一定要上这5个网站&#xff0c;设计师每天必逛&#xff0c;建议收藏&#xff01; 设计素材免费下载&#xff1a; https://www.sucai999.com/?vNTYwNDUx 国内灵感网 1、设计之窗 http://www.333cn.com/ 设计之窗是一个设计师分享作品及备案的网站&…

【Ctfer训练计划】——命令执行的解题技巧(持续更新中)

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座右…

什么是软考?软考有什么作用?

一、软考是什么&#xff1f; 1.软考介绍 计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试是由国家人力资源和社会保障部、工业和信息化部领导的国家级考试&#xff0c;其目的是&#xff0c;科学、公正地对全国计算机与软件专业技术人员进行职业资格、专业技术…

保姆教程系列三、Redis高可用(Cluster集群模式)

系列文章目录 &#xff01;&#xff01;&#xff01;是的没错&#xff0c;胖友们&#xff0c;保姆教程系列又更新了&#xff01;&#xff01;&#xff01; 保姆教程系列一、Redis部署 so easy 保姆教程系列二、Redis高可用&#xff08;主从同步哨兵模式&#xff09; 保姆教程系…

java开发的医院体检预约系统

简介 体检项目预约网站&#xff0c;普通用户注册登录可以网上预约体检项目&#xff0c;经过后台人员审核后可以去体检。用户还可以记录自己的身体指标下载体检报个&#xff0c;查看医嘱等。医院后台可以进行权限管理&#xff0c;实现多角色管理后台的其他业务等&#xff0c;实…

前端—化繁为简

化繁为简 HTML5要的就是简单、避免不必要的复杂性。HTML5的口号是“简单至上&#xff0c;尽可能简化”。因此&#xff0c;HTML5做了以下改进&#xff1a; 以浏览器原生能力替代复杂的JavaScript代码。 新的简化的DOCTYPE。 新的简化的字符集声明。 简单而强大的HTML5API。 我…

【 shell 编程 】第2篇 判断

判断 文章目录判断一、条件测试二、流程控制1.单分支结构2.双分支结构3.多分支结构4.嵌套结构三、匹配模式一、条件测试 格式&#xff1a; 格式1&#xff1a;test 条件表达式 格式2&#xff1a;[ 条件表达式 ] 格式3&#xff1a;[[ 条件表达式 ]] 注意&#xff1a;[] 中的条件…

电气器件系列三十五:开关电源选型实例

开关模式电源&#xff08;Switch Mode Power Supply&#xff0c;简称SMPS&#xff09;&#xff0c;又称交换式电源、开关变换器&#xff0c;是一种高频化电能转换装置&#xff0c;是电源供应器的一种。其功能是将一个位准的电压&#xff0c;透过不同形式的架构转换为用户端所需…

c++11 标准模板(STL)(std::deque)(三)

定义于头文件 <deque> std::deque 赋值给容器 std::deque<T,Allocator>::operator deque& operator( const deque& other ); (1) deque& operator( deque&& other ); (2)(C11 起) (C17 前) deque& operator( deque&& other ) no…