1.HashMap的底层实现图示

如上图所示:

HashMap底层是由  数组+(链表)=(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value之外,还包括hash值(key.hashCode()) ^ (h >>> 16)) 以及执行下一个节点的指针next。

2.HashMap源码分析

2.1 重要常量

public class HashMap extends AbstractMap    implements Map, Cloneable, Serializable {    private static final long serialVersionUID = 362498820763181265L;    static final int DEFAULT_INITIAL_CAPACITY = 1 <

static final int MAXIMUM_CAPACITY = 1 <[] table;

transient Set> entrySet;

transient int size;

transient int modCount;

int threshold;

final float loadFactor;

DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16

MAXIMUM_CAPACITY : HashMap的最大支持容量,2^30

DEFAULT_LOAD_FACTOR:HashMap的默认加载因子

TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树

UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表

MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)

table:存储元素的数组,总是2的n次幂

entrySet:存储具体元素的集

size:HashMap中存储的键值对的数量

modCount:HashMap扩容和结构改变的次数。

threshold:扩容的临界值,=容量*填充因子

loadFactor:填充因子

2.2 重要方法

1.获取hash值  hashstatic final int hash(Object key) {        int h;        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

hash方法用传入的key的hashCode和hashCode无符号右移16位的结果,做异或运算后作为hash值返回。

注:之所以获取hashCode后,还需要和右移16位的hashCode做异或运算,原因是:在根据hash值获取键值对在bucket数组中的下标时,采用的算法是:index=h & (length-1),当数组的length较小时,只有低位能够参与到“与”运算中,但是将hashCode右移16位再与本身做异或获取到的hash,可以使高低位均能够参与到后面的与运算中。

下面图说明:

2.根据键值对数量获取HashMap容量方法   tableSizeFor

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 = MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

tabSizeFor方法,主要根据传入的键值对容量,来返回大于容量的最小的二次幂数值。

算法如下:

将传入的容量-1:至于这里为什么需要减1,是为了防止cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍,各位可自行验证:

假设原始n:    0001  xxxx xxxx xxxx第一次右移1位+或运算:二进制序列出现至少两个连续的1,如 0001 1xxx xxxx xxxx;第二次右移2位+或运算:二进制序列出现至少四个连续的1,如 0001 111x xxxx xxxx;

第三次右移4位+或运算:二进制序列出现至少八个连续的1, 如 0001 1111 1111 xxxx;

第四次右移8位+或运算:二进制序列至少出现16个连续的1,如 0001 1111 1111 1111;第五次右移16位+或运算:二进制序列至少出现32个连续的1,如 0001 1111 1111 1111;上述运算中,若出现右移后为0,则或运算得到的结果和原始值一致,则后续推导过程可以忽略。

此时可以保证,原始序列从包含1的最高位,到最低位,全部都变成了1.

最后+1,返回的结果就是大于原值的最小二次幂数。

3.插入方法   putVal

1  final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 2                    boolean evict) { 3                     4         Node[] tab;    //存储Node节点的数组tab 5         Node p;         //单个Node节点p 6         int n, i;            //HashMap的容量n 7         //初始化数组桶table 8         if ((tab = table) == null || (n = tab.length) == 0) 9             n = (tab = resize()).length;10         //如果数组桶中不包含要插入的元素,将新键值对作为新Node存入数组,11         if ((p = tab[i = (n - 1) & hash]) == null)    //此处p初始化,p和需要存储的键值对下标相同且P是链表的第一个元素12             tab[i] = newNode(hash, key, value, null);13             14         //桶中包含要插入的元素15         else {16             Node e; K k;17             //如果key和链表第一个元素p的key相等18             if (p.hash == hash &&19                 ((k = p.key) == key || (key != null && key.equals(k))))20             //则将e指向该键值对21                 e = p;22             //若p是TreeNode类型,则使用红黑树的方法插入到树中23             else if (p instanceof TreeNode)24                 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);25             //else:键值对的引用不在链表的第一个节点,此时需要遍历链表    26             else {27                 for (int binCount = 0; ; ++binCount) {28                     //将e指向p的下一个元素,直到其next为null时,将键值对作为新Node放到p的尾部或树中。29                     if ((e = p.next) == null) {30                         p.next = newNode(hash, key, value, null);31                         //如果遍历链表的长度大于等于8,则变成树32                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st33                             treeifyBin(tab, hash);34                         break;   //新元素已追加到链表尾部或树中,退出遍历35                     }36                     //在冲突链表中找到另一个具有相同key值的节点,退出遍历37                     if (e.hash == hash &&38                         ((k = e.key) == key || (key != null && key.equals(k))))39                         break;40                         41                     //将e指向p,便于下次遍历e = p.next42                     p = e;43                 }44                 //上述for循环执行完毕后,e要么指向了存储的新节点,要么是原来已有的元素,具有和新节点一样key值45             }46             //当e非空时,说明e是原来HashMap中的元素,具有和新节点一样的key值47             if (e != null) { // existing mapping for key48                 V oldValue = e.value;49                 if (!onlyIfAbsent || oldValue == null)    //onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值50                     e.value = value;51                 //空实现,LinkedHashMap用52                 afterNodeAccess(e);53                 return oldValue;54             }55         }56         //HashMap结构更改,modCount+157         ++modCount;58         //判断是否需要扩容59         if (++size > threshold)60             resize();61         //空实现,LinkedHashMap用62         afterNodeInsertion(evict);63         return null;64     }

HashMap中进行存储的入口方法是:put(K,V),但是核心方法是putVal方法,该方法一共有以下步骤:初始化数组桶

判断数组桶中对应下标是否无元素存在,是,就直接存入

若数组桶中对应下标有元素存在,则开始遍历,根据长度将元素存入链表尾部或树中。

判断是否需要扩容

4.扩容方法   resize

1 final Node[] resize() {  2         Node[] oldTab = table;  3         int oldCap = (oldTab == null) ? 0 : oldTab.length;    //原HashMap的容量  4         int oldThr = threshold;                                //原HashMap的扩容临界值                  5         int newCap, newThr = 0;  6         //case1:odlCap>0,说明桶数组已经初始化过  7         if (oldCap > 0) {  8             //原HashMap的越界检查  9             if (oldCap >= MAXIMUM_CAPACITY) { 10                 threshold = Integer.MAX_VALUE; 11                 return oldTab; 12             } 13             //容量扩大一倍后的越界检查 14             else if ((newCap = oldCap <= DEFAULT_INITIAL_CAPACITY) 16                 newThr = oldThr <0,桶数组尚未初始化,当调用带初始化容量的构造函数时会发生该情况 19         else if (oldThr > 0) // initial capacity was placed in threshold 20             //在前面HashMap的初始化中,将Initial capcity暂存在threshold中 21             newCap = oldThr; //未初始化,则用Initial capcity作为新的容量 22              23             //若oldThr = threshold = 0,则说明未传入初始化容量参数 24              25         //case3:oldCap=0 && oldThr = 0,当调用无参构造函数时会发生该情况,此时使用默认容量初始化 26         else {               // zero initial threshold signifies using defaults 27             newCap = DEFAULT_INITIAL_CAPACITY;    //默认容量 28             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);    //默认扩容临界值 29         } 30          31         // 当newThr 为 0 时,阈值按照计算公式进行计算 32         if (newThr == 0) { 33             float ft = (float)newCap * loadFactor; 34             newThr = (newCap [] newTab = (Node[])new Node[newCap]; 46         table = newTab; 47         //若oldTab非空,则需要将原来桶数组的元素取出来放到新的桶数组中 48         if (oldTab != null) { 49             for (int j = 0; j  e; 51                 if ((e = oldTab[j]) != null) { 52                     oldTab[j] = null;    //将原桶数组的元素占用的空间释放,便于GC 53                     if (e.next == null)

54                         //若桶中元素的next为空,获取index后直接将其放入新桶数组中 55                         newTab[e.hash & (newCap - 1)] = e; 56                         //若桶中元素的next是树节点 57                     else if (e instanceof TreeNode) 58                         //采用树的方式插入 59                         ((TreeNode)e).split(this, newTab, j, oldCap); 60                         //若桶中元素的next是链表节点 61                     else { // preserve order 62                         Node loHead = null, loTail = null; 63                         Node hiHead = null, hiTail = null; 64                         Node next; 65                          66                         /*遍历原链表,按照原来的顺序进行分组 67                         */ 68                          69                          70                         /*原始链表中的元素,在resize之后,其下标有两种可能,一种是在原来下标处,另一种是原来下标+oldCap处 71                         *举例说明:  若原来的容量 -1后 只有n位,低位有n个1,去下标公式为:i = (oldCap - 1) & hash,若hash值只有低n为有值,则与运算后获得的值和 72                         *扩容前是一样的,若hash不止第n位有值,那采用与运算后,结果比原来刚好大oldCap。 下面有图片示例) 73                         */ 74

75                          76                         do { 77                             next = e.next 78                             //若e.e.hash & oldCap 结果为0,则下标在新桶数组中不用改变,此时,将元素存放在loHead为首的链表中 79                             if ((e.hash & oldCap) == 0) { 80                                 if (loTail == null) 81                                     loHead = e; 82                                 else 83                                     loTail.next = e; 84                                 loTail = e; 85                             } 86                              87                             //若e.e.hash & oldCap 结果不为0,则下标在新桶数组等于原下标+oldCap,此时,将元素存放在hiHead为首的链表中 88                             else { 89                                 if (hiTail == null) 90                                     hiHead = e; 91                                 else 92                                     hiTail.next = e; 93                                 hiTail = e; 94                             } 95                         } while ((e = next) != null); 96                          97                          98                          99                         if (loTail != null) {        //loHead为首的链表中,下标不改变100                             loTail.next = null;101                             newTab[j] = loHead;102                         }103                         if (hiTail != null) {        //hiHead为首的链表中,下标=原下标+oldCap104                             hiTail.next = null;105                             newTab[j + oldCap] = hiHead;106                         }107                     }108                 }109             }110         }111         return newTab;

112     }

上述代码分析较长,总结如下:

1.获取不同情况下的 新的容量 和 新的扩容临界值

2.根据新容量创建新的桶数组tab。

3.根据节点类型,树节点和链表节点分别采用对应方法放入新的桶数组

5.移除元素  remove

1  final Node removeNode(int hash, Object key, Object value, 2                                boolean matchValue, boolean movable) { 3         Node[] tab; Node p; int n, index; 4          5         //通过hash值获取下标,下标对应的节点p不为空 6         if ((tab = table) != null && (n = tab.length) > 0 && 7             (p = tab[index = (n - 1) & hash]) != null) { 8             Node node = null, e; K k; V v; 9             //若节点p的key和待移除的节点key相等10             if (p.hash == hash &&11                 ((k = p.key) == key || (key != null && key.equals(k))))12                 //则p就是待移除节点13                 node = p;    //将p指向待移除节点14             //p的key和待移除的节点key不相等,遍历p作为头的链表或者树15             else if ((e = p.next) != null) {16                 //若p是树节点17                 if (p instanceof TreeNode)18                     //采用树节点方式获得要移除的节点19                     node = ((TreeNode)p).getTreeNode(hash, key);20                 else {//p是链表的头节点21                     do {22                         //采用循环,当p.next不为空,比对它和传入的key,直到找到相等的key23                         if (e.hash == hash &&24                             ((k = e.key) == key ||25                              (key != null && key.equals(k)))) {26                             //找到后,将节点指向node27                             node = e;    //将e指向待移除节点,此时相当于p.next就是待移除的节点node,可自行验证循环便知28                             break;29                         }30                         p = e;

31                     } while ((e = e.next) != null);32                 }33             }34             //若node非空,传入的matchValue参数为flase或 node的value等于传入value35            if (node != null && (!matchValue || (v = node.value) == value ||36                                  (value != null && value.equals(v)))) {37                 //若node是树节点38                 if (node instanceof TreeNode)39                     //采用树节点的方式移除40                     ((TreeNode)node).removeTreeNode(this, tab, movable);41                     //若待移除节点是链表头,将其指向待移除元素的next,移除对node的引用42                 else if (node == p)43                     tab[index] = node.next;

44                 else//待移除元素是链表中的元素,此时其等于p.next45                     //将p.next指向node.next,移除了对node的引用46                     p.next = node.next;47                 //增加结构修改计数器48                 ++modCount;49                 //size-150                 --size;51                 //空实现,LinkedHashMap用52                 afterNodeRemoval(node);53                 54                 //返回移除的节点node55                 return node;56             }57         }58         return null;59     }

移除节点的入口方法是: public V remove(Object key)  ,其核心方法是removeNode,主要做了以下几个工作:通过用key获取的hash,来获取下标。

若下标对应处无元素,返回null。

若下标对应处有元素,判断是树或者链表,采用对应方法移除。

6.查找元素方法 get

1     final Node getNode(int hash, Object key) { 2         Node[] tab; Node first, e; int n; K k; 3         //根据hash值,获取对应下标的第一个元素first 4         if ((tab = table) != null && (n = tab.length) > 0 && 5             (first = tab[(n - 1) & hash]) != null) { 6             //如果first的key和待查询的key相等,返回first 7             if (first.hash == hash && // always check first node 8                 ((k = first.key) == key || (key != null && key.equals(k)))) 9                 return first;10             //若first不是待查询的元素11             if ((e = first.next) != null) {12                 //若first是树节点,采用树节点的方式获取13                 if (first instanceof TreeNode)14                     return ((TreeNode)first).getTreeNode(hash, key);15                 //first是链表节点头,使用循环获取16                 do {17                     if (e.hash == hash &&18                         ((k = e.key) == key || (key != null && key.equals(k))))19                         return e;20                 } while ((e = e.next) != null);21             }22         }23         return null;24     }

查询元素的入口方法是:public V get(Object key),返回值是node的value,核心方法是getNode(int hash, Object key)。

2.3 构造方法

1.无参构造函数public HashMap() {        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted

}

使用所有默认参数来构造一个HashMap,我们常用的就是这种。

2.给出容量的构造函数public HashMap(int initialCapacity) {        this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

此处调用了下面这个构造函数,将给出的容量传入和默认负载因子。

3.给出容量和负载因子的构造函数

public HashMap(int initialCapacity, float loadFactor) {

//容量越界检查        if (initialCapacity

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);  //此处将初始化的容量暂存到threshold中

}

4.用一个map来初始化的构造函数public HashMap(Map extends K, ? extends V> m) {        this.loadFactor = DEFAULT_LOAD_FACTOR;

putMapEntries(m, false);

}

此处将map中所有元素放入HashMap进行初始化。

3.常见问题解答

3.1 HashMap的容量为什么必须是2的n次幂?

答:当容量是2的n次幂时,不同的key获取在桶数组中的下标相同的概率减小,发生Hash碰撞几率减少,元素分布更加均匀,见下图。

结论:

1.由上述实例可以看出,当HashMap容量为2的n次幂的时候,length-1,可以使得在计算index的"&"运算过程中,hash值的对应位都能参与到计算;若HashMap容量不等于2的n次幂,leng-1后必然有一些位是等于0的,那么在计算index的"&"运算过程中,对应位的数字无论是0或者1,都未能参与到运算中。导致Hash冲突概率增大。

2.初次之外,若HashMap容量不为2的n次幂,无论Hash值如何变化,始终有一些下标值无法取得,因为"&"运算过程中,必然有一些位置结果始终为0,如实例所示,其最低位始终为0,因此下标 1(二进制0000 0001)、3(二进制0000 0011)、5(二进制0000 0101)、7(二进制0000 0111)等下标、永远都获取不了。造成了容量的浪费

3.2 HashMap的时间复杂度?

答:O(1)或者O(log(n))或O(n),分析如下:

根据第一节的内容可知,根据HashMap的数据结构,可能有以下三种情况:

1.无链表和红黑树,理想状态。每个桶只有一个或0个元素,不存在Hash冲突,此时时间复杂度为O(1);但此时耗费的内存空间较大。

2.只有链表,此时因为需要循环链表来获取元素,时间复杂度为O(n)

3.只有红黑树,此时时间复杂度为红黑树查找的时间复杂度,O(log(n)).

4.链表和红黑树均有,此时时间复杂度依据红黑树的层数和链表长度而定。为O(n)或者O(log(n)).

3.3 负载因子LoadFactor为何默认值为0.75。

当负载因子过大时,Hash冲突概率增加、读写的时间复杂度也大大增加,当负载因子过小时,Hash冲突概率较小,时间复杂度较低,但占用内存空间较大。

至于为什么默认值是0.75,这是一个基于时间和空间复杂度的综合考虑的结果,可以参考这篇文章:HashMap的loadFactor为什么是0.75?

3.4 作为HasHMap的key有什么条件?

使用HashMap,若用int/String等作为key值,因为Integer类和String类都以及重写了equals()和hashCode()方法.

但是如果key是自定义的类,就必须重写hashcode()和equals()。理由如下:

//在插入元素中,根据hash值后,与length-1做&运算获取下标       //获取hash

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

}       //获取下标

p = tab[i = (n - 1) & hash]    //用equals方法比对key值和节点的key值,来确认是否遍历到所需元素

(key != null && key.equals(k)))

/*对比hash值,如果不重写hashCode方法,那么采用Object类的默认的hash方法是获取内存地址,此时即使两个key对象相等,但其内存地址不可能相等,所以必须重写hashCode方法。*//*equals方法若不重写,采用的Object的equals方法,比对的是内存地址,如果不重写,会造成两个一样的key值都插入,存在重复元素*/

//同理,在查找过程中,在第二节putVal方法中有分析,会用到hash值,以及用到key.equals方法,因此如果不重写equals()和hashCode(),会造成虽然元素存在,但是因内存地址不一致,差找不到对应元素。

3.5 HashMap key允许为空吗?,最多几个?

答:允许但只允许一个key值为空。当key值为空时,其hash值为0,因此在桶数组中位置是0,即第一个元素。

那么为什么不能有两个key值为null呢,原因是两个key为null,是一样的,后面put进去的(null,value2)会覆盖先put进去的(null,value1)。验证如下:

3.6 HashMap value允许为空吗?最多几个?

答:允许,可以有多个value为null,查看源码可知,在putVal中没有对value进行限制,验证如下:

写在最后:

1.本文中设计到数操作的都没有详细介绍,因为红黑树本身概念较为抽象复杂,打算下一篇文章中再来详细分析一下,还有其他一些类似于“map.clear()、map.ContainsKey()”等等逻辑较为简单的方法也未作赘述。

2.不得不感叹一些设计Java集合类的大牛是真的牛,看似一个简单的HashMap中、对于位运算、链表。红黑树的应用可谓是炉火纯青,看起来都不能一目了然,设计时那更是天马行空,匠心独运。

查询已有链表的hashmap_源码分析系列1:HashMap源码分析(基于JDK1.8)相关推荐

  1. 查询已有链表的hashmap_面试官再问你 HashMap 底层原理,就把这篇文章甩给他看...

    前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希望对你有所帮助~ ...

  2. 查询已有链表的hashmap_原创 | 面试不再慌,看完这篇保证让你写HashMap跟玩一样...

    点击上方蓝色小字,关注"码农小黑屋" 重磅干货,第一时间送达 今天这篇文章给大家讲讲hashmap,这个号称是所有Java工程师都会的数据结构.为什么说是所有Java工程师都会呢, ...

  3. C语言已排序链表插入新节点保持排序状态(附完整源码)

    C语言已排序链表插入新节点保持排序状态 C语言已排序链表插入新节点保持排序状态完整源码(定义,实现,main函数测试) C语言已排序链表插入新节点保持排序状态完整源码(定义,实现,main函数测试) ...

  4. dubbo源码分析系列——dubbo-cluster模块源码分析

    2019独角兽企业重金招聘Python工程师标准>>> 模块功能介绍 该模块的使用介绍请参考dubbo官方用户手册如下章节内容. 集群容错 负载均衡 路由规则 配置规则 注册中心参考 ...

  5. 【java集合框架源码剖析系列】java源码剖析之ArrayList

    注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 本博客将从源码角度带领大家学习关于ArrayList的知识. 一ArrayList类的定义: public class Arr ...

  6. JAVA系列:HashMap源码分析

    目录 HashMap的Node HashMap的存储结构 确定哈希桶数组索引位置 HashMap的put方法 扩容机制 线程安全性 HashMap的Node HashMap的存储结构 HashMap的 ...

  7. 【java集合框架源码剖析系列】java源码剖析之java集合中的折半插入排序算法

    注:关于排序算法,博主写过[数据结构排序算法系列]数据结构八大排序算法,基本上把所有的排序算法都详细的讲解过,而之所以单独将java集合中的排序算法拿出来讲解,是因为在阿里巴巴内推面试的时候面试官问过 ...

  8. hashmap remove 没释放内存_java从零开始手写 redis(13)HashMap 源码原理详解

    为什么学习 HashMap 源码? 作为一名 java 开发,基本上最常用的数据结构就是 HashMap 和 List,jdk 的 HashMap 设计还是非常值得深入学习的. 无论是在面试还是工作中 ...

  9. Dubbo 实现原理与源码解析系列 —— 精品合集

    摘要: 原创出处 http://www.iocoder.cn/Dubbo/good-collection/ 「芋道源码」欢迎转载,保留摘要,谢谢! 1.[芋艿]精尽 Dubbo 原理与源码专栏 2.[ ...

最新文章

  1. ​卷积层和分类层,哪个更重要?
  2. linux常用命令--diff
  3. P3846-[TJOI2007]可爱的质数【BSGS,数论】
  4. 单E1光端机分类及技术指标详解
  5. 【LeetCode笔记】剑指 Offer 20. 表示数值的字符串(Java、字符串)
  6. 新冠患者样本单细胞测序文献汇总
  7. 演练:调试 Windows 窗体
  8. Python趣味编程3则:李白买酒、猴子吃桃、宝塔上的琉璃灯
  9. 可用子网数要不要减2_详解IP地址、子网掩码、网络号、主机号、网络地址、主机地址...
  10. EF6 CodeFirst+Repository+Ninject+MVC4+EasyUI实践(九)
  11. du命令和df命令的区别
  12. 【python自动化第十篇:】
  13. 中文对比英文自然语言处理NLP的区别综述
  14. sqlmap使用教程(超详细)
  15. https://blog.csdn.net/zxp_cpinfo/article/details/53692922
  16. win 10 好吗?对比与ubuntu,对比于Mac呢?
  17. linux实用技巧:ubuntu18.04安装samba服务器实现局域网文件共享
  18. Python 让多图排版更加美观
  19. 使用IMU与轮速计进行单线激光雷达的运动畸变校正
  20. SUSE 搭建 虚拟专用网络 (pptpd )_linux

热门文章

  1. oracle 11.2.0.4 mos,【翻译自mos文章】在RHEL7 or OEL7上安装oracle 11.2.0.4 db时的
  2. php如何判断多文件上传,php多文件上传
  3. mysql视图_mysql之视图详解
  4. Python中必知的知识点:文本转义及编码的常用方法
  5. Python高阶函数和函数嵌套
  6. Python中:re的match和search区别?
  7. socket通信流程图
  8. Intel Realsense D435 官方推荐有源USB线(有源电缆 cable)
  9. python dict()函数(用于创建一个字典)
  10. Visual Studio 2013或2015工程属性中包含目录和库目录的添加方法,附加依赖项,相对路径