我的编程空间,编程开发者的网络收藏夹
学习永远不晚

如何理解Java容器中Map的源码分析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

如何理解Java容器中Map的源码分析

本篇文章为大家展示了如何理解Java容器中Map的源码分析,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

如果没有特别说明,以下源码分析基于 JDK 1.8。

一、HashMap

为了便于理解,以下源码分析以 JDK 1.7 为主。

1. 存储结构

内部包含了一个 Entry 类型的数组 table。

transient Entry[] table;

Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。 即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突, 同一个链表中存放哈希值相同的 Entry。

如何理解Java容器中Map的源码分析

static class Entry<K,V> implements Map.Entry<K,V> {    //包含了四个字段    final K key;    V value;    //next指向下一个节点,说明是链表结构    Entry<K,V> next;    int hash;    Entry(int h, K k, V v, Entry<K,V> n) {        value = v;        next = n;        key = k;        hash = h;    }    public final K getKey() {        return key;    }    public final V getValue() {        return value;    }    public final V setValue(V newValue) {        V oldValue = value;        value = newValue;        return oldValue;    }    public final Boolean equals(Object o) {        if (!(o instanceof Map.Entry))                    return false;        Map.Entry e = (Map.Entry)o;        Object k1 = getKey();        Object k2 = e.getKey();        // k1==k2 比较的是 hashcode 值,        // k1.equals(k2)比较的是k1和k2的内容 equals 未重写,则等价于 k1 == k2        if (k1 == k2 || (k1 != null && k1.equals(k2))) {            Object v1 = getValue();            Object v2 = e.getValue();            if (v1 == v2 || (v1 != null && v1.equals(v2)))                            return true;        }        return false;    }    public final int hashCode() {        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());    }    public final String toString() {        return getKey() + "=" + getValue();    }}

2. 拉链法的工作原理

HashMap<String, String> map = new HashMap<>();map.put("K1", "V1");map.put("K2", "V2");map.put("K3", "V3");
  • 新建一个 HashMap,默认大小为 16;

  • 插入

    键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
  • 插入

    键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
  • 插入

    键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在前面。

应该注意到链表的插入是以头插法方式进行的,例如上面的不是插在后面,而是插入在链表头部。

查找需要分成两步进行:

  • 计算键值对所在的桶;

  • 在链表上顺序查找,时间复杂度显然和链表的长度成正比。

3. put 操作

public V put(K key, V value) {    if (table == EMPTY_TABLE) {        inflateTable(threshold);    }    // 键为 null 单独处理    if (key == null)            return putForNullKey(value);    int hash = hash(key);    // 确定桶下标    int i = indexFor(hash, table.length);    // 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value    // 时间复杂度显然和链表的长度成正比。    for (Entry<K,V> e = table[i]; e != null; e = e.next) {        Object k;        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }    modCount++;    // 插入新键值对    addEntry(hash, key, value, i);    return null;}

HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。

private V putForNullKey(V value) {    //HashMap 使用第 0 个桶 table[0] 存放键为 null 的键值对。    for (Entry<K,V> e = table[0]; e != null; e = e.next) {        if (e.key == null) {            V oldValue = e.value;            e.value = value;            // 更新值            e.recordAccess(this);            return oldValue;            // 返回旧值        }    }    modCount++;    //void addEntry(int hash, K key, V value, int bucketIndex)    addEntry(0, null, value, 0);    return null;}

使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。

//TODO:使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。void addEntry(int hash, K key, V value, int bucketIndex) {    if ((size >= threshold) && (null != table[bucketIndex])) {        resize(2 * table.length);        hash = (null != key) ? hash(key) : 0;        bucketIndex = indexFor(hash, table.length);    }    createEntry(hash, key, value, bucketIndex);}void createEntry(int hash, K key, V value, int bucketIndex) {    Entry<K,V> e = table[bucketIndex];    // 头插法,链表头部指向新的键值对    table[bucketIndex] = new Entry<>(hash, key, value, e);    size++;}
Entry(int h, K k, V v, Entry<K,V> n) {    value = v;    next = n;    key = k;    hash = h;}

4. 确定桶下标

很多操作都需要先确定一个键值对所在的桶下标。

int hash = hash(key);int i = indexFor(hash, table.length);

①. 计算 hash 值

final int hash(Object k) {    int h = hashSeed;    if (0 != h && k instanceof String) {        return sun.misc.Hashing.stringHash42((String) k);    }    h ^= k.hashCode();    // This function ensures that hashCodes that differ only by    // constant multiples at each bit position have a bounded    // number of collisions (approximately 8 at default load factor).    h ^= (h >>> 20) ^ (h >>> 12);    return h ^ (h >>> 7) ^ (h >>> 4);}
public final int hashCode() {    return Objects.hashCode(key) ^ Objects.hashCode(value);}

②. 取模

令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:

x   : 00010000x-1 : 00001111

令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:

y       : 10110010x-1     : 00001111y&(x-1) : 00000010

这个性质和 y 对 x 取模效果是一样的:

y   : 10110010x   : 00010000y%x : 00000010

我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能。

确定桶下标的最后一步是将 key 的 hash 值对桶个数取模: hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算。

static int indexFor(int h, int length) {    return h & (length-1);}

就等价于

static int indexFor(int h, int length) {    return h % length;}

但是效率会更高。

5. 扩容-基本原理

设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。

为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。 HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。

和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。

如何理解Java容器中Map的源码分析

static final int DEFAULT_INITIAL_CAPACITY = 16;//保证 capacity 为 2 的 n 次方,那么就可以将indexFor方法中操作转换为位运算static final int MAXIMUM_CAPACITY = 1 << 30;//保证 capacity 为 2 的 n 次方,那么就可以将 indexFor 方法中操作转换为位运算static final float DEFAULT_LOAD_FACTOR = 0.75f;transient Entry[] table;transient int size;int threshold;final float loadFactor;transient int modCount;

从下面的添加元素代码中可以看出,当需要扩容时,令 capacity 为原来的两倍。

void addEntry(int hash, K key, V value, int bucketIndex) {    Entry<K,V> e = table[bucketIndex];    table[bucketIndex] = new Entry<>(hash, key, value, e);    if (size++ >= threshold)            resize(2 * table.length);    //令 capacity 为原来的两倍}

扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。

void resize(int newCapacity) {    Entry[] oldTable = table;    int oldCapacity = oldTable.length;    if (oldCapacity == MAXIMUM_CAPACITY) {        threshold = Integer.MAX_VALUE;        return;    }    Entry[] newTable = new Entry[newCapacity];    transfer(newTable);    table = newTable;    threshold = (int)(newCapacity * loadFactor);}void transfer(Entry[] newTable) {    Entry[] class="lazy" data-src = table;    int newCapacity = newTable.length;    for (int j = 0; j < class="lazy" data-src.length; j++) {        Entry<K,V> e = class="lazy" data-src[j];        if (e != null) {            class="lazy" data-src[j] = null;            do {                Entry<K,V> next = e.next;                int i = indexFor(e.hash, newCapacity);                e.next = newTable[i];                newTable[i] = e;                e = next;            }            while (e != null);        }    }}

6. 扩容-重新计算桶下标

在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以提升重新计算桶下标的效率。

假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32:

capacity     : 00010000new capacity : 00100000

对于一个 Key,

  • 它的哈希值如果在第 5 位上为 0,那么取模得到的结果和之前一样;

  • 如果为 1,那么得到的结果为原来的结果 +16。

7. 计算数组容量

HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。

先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到:

mask |= mask >> 1    11011000mask |= mask >> 2    11111110mask |= mask >> 4    11111111

mask+1 是大于原始数字的最小的 2 的 n 次方。

num     10010000mask+1  100000000

以下是 HashMap 中计算数组容量的代码:

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;    //得到n的掩码    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

8. 链表转红黑树

从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。

9. 与 HashTable 的比较

  • HashMap 是非线程安全的,HashTable 使用 synchronized 来进行同步,是线程安全的。

  • HashMap 要比 HashTable 效率高一点。Hashtable 基本被淘汰,不要在代码中使用它。

  • HashMap 可以插入键为 null 的 Entry;HashTable 中插入的键只要有一个为 null,直接抛出 NullPointerException。

  • HashMap 的迭代器是 fail-fast 迭代器。

  • HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。

  • HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间;Hashtable 没有这样的机制。

  • HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍;Hashtable 容量默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。 在初始化时如果给定了容量初始值,HashMap 会将其扩充为2的幂次方大小;Hashtable 会直接使用初始值。

10. 与 HashSet 的比较

HashSet 底层就是基于HashMap实现的。 (HashSet 的源码非常非常少,因为除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不实现之外, 其他方法都是直接调用 HashMap 中的方法。)

如何理解Java容器中Map的源码分析

二、LinkedHashMap

1.存储结构

继承自 HashMap,因此具有和 HashMap 一样的快速查找特性。

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。

transient LinkedHashMap.Entry<K,V> head;transient LinkedHashMap.Entry<K,V> tail;

accessOrder 决定了顺序,默认为 false,此时维护的是插入顺序。

final boolean accessOrder;

LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。

void afterNodeAccess(Node<K,V> p) { }void afterNodeInsertion(boolean evict) { }

2.afterNodeAccess()

当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。

void afterNodeAccess(Node<K,V> e) {    // move node to last    LinkedHashMap.Entry<K,V> last;    if (accessOrder && (last = tail) != e) {        LinkedHashMap.Entry<K,V> p =                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;        p.after = null;        if (b == null)                    head = a; else                    b.after = a;        if (a != null)                    a.before = b; else                    last = b;        if (last == null)                    head = p; else {            p.before = last;            last.after = p;        }        tail = p;        ++modCount;    }}

3.afterNodeInsertion()

在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。

evict 只有在构建 Map 的时候才为 false,在这里为 true。

void afterNodeInsertion(Boolean evict) {    // possibly remove eldest    LinkedHashMap.Entry<K,V> first;    if (evict && (first = head) != null && removeEldestEntry(first)) {        K key = first.key;        removeNode(hash(key), key, null, false, true);    }}

removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {    return false;}

4.LRU 缓存

以下是使用 LinkedHashMap 实现的一个 LRU 缓存:

  • 设定最大缓存空间 MAX_ENTRIES 为 3;

  • 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;

  • 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。

public class LRUCache<K,V> extends LinkedHashMap<K,V>{    private static final int MAX_ENTRIES = 3;    LRUCache(){        super(MAX_ENTRIES,0.75f,true);    }        @Override        protected Boolean removeEldestEntry(Map.Entry eldest) {        return size() > MAX_ENTRIES;    }    public static void main(String[] args) {        LRUCache<Integer,String> cache=new LRUCache<>();        cache.put(1, "a");        cache.put(2, "b");        cache.put(3, "c");        cache.get(1);        //LRU  键值1被访问过了,则最近最久未访问的就是2        cache.put(4, "d");        System.out.println(cache.keySet());    }}
[3, 1, 4]

三、WeakHashMap

1.存储结构

WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。

WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>

2.ConcurrentCache

Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。

ConcurrentCache 采取的是分代缓存:

  • 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收;

  • 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。

  • 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。

  • 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。

public final class ConcurrentCache<K, V> {    private final int size;    private final Map<K, V> eden;    private final Map<K, V> longterm;    public ConcurrentCache(int size) {        this.size = size;        this.eden = new ConcurrentHashMap<>(size);        this.longterm = new WeakHashMap<>(size);    }    public V get(K k) {        V v = this.eden.get(k);        if (v == null) {            v = this.longterm.get(k);            if (v != null)                            this.eden.put(k, v);        }        return v;    }    public void put(K k, V v) {        if (this.eden.size() >= size) {            this.longterm.putAll(this.eden);            this.eden.clear();        }        this.eden.put(k, v);    }}

上述内容就是如何理解Java容器中Map的源码分析,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注编程网行业资讯频道。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

如何理解Java容器中Map的源码分析

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

如何理解Java容器中Map的源码分析

本篇文章为大家展示了如何理解Java容器中Map的源码分析,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。如果没有特别说明,以下源码分析基于 JDK 1.8。一、HashMap为了便于理解,以下源码分
2023-06-05

如何理解Java容器中ArrayList的源码分析

这篇文章给大家介绍如何理解Java容器中List的源码分析,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。如果没有特别说明,以下源码分析基于 JDK 1.8。一、ArrayList1. 概览实现了 RandomAcces
2023-06-05

如何理解Java 容器中并发容器的源码分析

这期内容当中小编将会给大家带来有关如何理解Java 容器中并发容器的源码分析,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。如果没有特别说明,以下源码分析基于 JDK 1.8。CopyOnWriteArra
2023-06-05

源码分析Java中ThreadPoolExecutor的底层原理

这篇文章主要带大家从源码分析一下Java中ThreadPoolExecutor的底层原理,文中的示例代码讲解详细,具有一定的学习价值,需要的可以参考一下
2023-05-19

从源码解析Android中View的容器ViewGroup

这回我们是深入到ViewGroup内部\,了解ViewGroup的工作,同时会阐述更多有关于View的相关知识。以便为以后能灵活的使用自定义空间打更近一步的基础。希望有志同道合的朋友一起来探讨,深入Android内部,深入理解Androi
2022-06-06

Java异步编程中如何进行FutureTask源码分析

本篇文章给大家分享的是有关Java异步编程中如何进行FutureTask源码分析,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。Java的异步编程是一项非常常用的多线程技术。但之
2023-06-19

如何理解Java容器中的设计模式

如何理解Java容器中的设计模式,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。一、迭代器模式Collection 继承了 Iterable 接口,其中的 iterator(
2023-06-05

如何解析Python源码分析的相关操作步骤

今天就跟大家聊聊有关如何解析Python源码分析的相关操作步骤,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。Python是一种动态的脚本语言。具体的我就不多介绍了,源代码链接在这里:
2023-06-17

如何分析Kubernetes中的容器网络

这篇文章将为大家详细讲解有关如何分析Kubernetes中的容器网络,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。| 前言随着云计算的兴起,各大平台之争也落下了帷幕,Kubernetes作为
2023-06-04

Java中的InputStreamReader和OutputStreamWriter源码分析_动力节点Java学院整理

InputStreamReader和OutputStreamWriter源码分析1. InputStreamReader 源码(基于jdk1.7.40)package java.io; import java.nio.charset.Cha
2023-05-31

如何用JVM源码分析Java对象的创建过程

这篇文章将为大家详细讲解有关如何用JVM源码分析Java对象的创建过程,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。基于HotSpot实现对Java对象的创建过程进行深入分析。定义两个简单的
2023-06-17

如何分析Java创建线程中的代码

如何分析Java创建线程中的代码,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。Java创建线程经常在我们的编码中出现,当我们在使用的时候会有不少的问题困扰着我们
2023-06-17

如何理解Java图形界面开发中的Swing容器

今天就跟大家聊聊有关如何理解Java图形界面开发中的Swing容器,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。11.5 JViewport类JViewport很少在JScrollP
2023-06-17

深入理解Python虚拟机中调试器实现原理与源码分析

本文主要给大家介绍python中调试器的实现原理,通过了解一个语言的调试器的实现原理我们可以更加深入的理解整个语言的运行机制,可以帮助我们更好的理解程序的执行,感兴趣的可以了解一下
2023-05-17

如何理解java 知识点的集合分析

今天就跟大家聊聊有关如何理解java 知识点的集合分析,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。1 、对象的初始化  (1 )非静态对象的初始化  在创建对象时,对象所在类的所有
2023-06-02

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录