Android版数据结构与算法(四):基于哈希表实现HashMap核心源码彻底分析
版权声明:本文出自汪磊的博客,未经作者允许禁止转载。
存储键值对我们首先想到HashMap,它的底层基于哈希表,采用数组存储数据,使用链表来解决哈希碰撞,它是线程不安全的,并且存储的key只能有一个为null,在安卓中如果数据量比较小(小于一千),建议使用SparseArray和ArrayMap,内存,查找性能方面会有提升,如果数据量比较大,几万,甚至几十万以上还是使用HashMap吧。本篇只详细分析HashMap的源码,SparseArray和ArrayMap不在本篇讨论范围内,后续会单独分析。
HashMap的理解,最最核心就是扩容那二十几行代码,可以说是HashMap的核心所在了,然而网上绝大部分博客只是一带而过,大体说了一下结论,让人十分失望,本篇将会彻底分析扩容机制,源码分析基于android-23。
好了,直入主题吧。
一、HashMap中成员变量
1 private static final int MINIMUM_CAPACITY = 4;//约定hashmap中最小容量,也可以是0,如果不为0,那么最小容量限制为4
2 private static final int MAXIMUM_CAPACITY = 1 << 30;约定hashmap中最大容量,为2的30次方
static final float DEFAULT_LOAD_FACTOR = .75F;//扩容因子:主要用于扩容时机,后续会细讲
4
5 transient HashMapEntry<K, V>[] table;//盛放数据的table,数据每一项key不为null,每一项都是一个HashMapEntry对象
6
7 transient HashMapEntry<K, V> entryForNullKey;盛放key为null的数据项
8
9 transient int size;//hashmap中已经盛放数据的大小 private transient int threshold;//用来判断是否需要扩容,其值为DEFAULT_LOAD_FACTOR * hashmap的容量,当盛放数据达到hashmap的四分之三时,急需要考虑扩容了
主要成员变量已经有所标注,后续分析的时候会再次提及,此处不做过多解释。
二、HashMap中数据项HashMapEntry
HashMap中每个数据项都是HashMapEntry对象,HashMapEntry是HashMap的内部类,我们先来看看其结构:
static class HashMapEntry<K, V> implements Entry<K, V> {
final K key;
V value;
final int hash;
HashMapEntry<K, V> next; HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
} public final K getKey() {
return key;
} public final V getValue() {
return value;
} public final V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
} @Override public final boolean equals(Object o) {
if (!(o instanceof Entry)) {
return false;
}
Entry<?, ?> e = (Entry<?, ?>) o;
return Objects.equal(e.getKey(), key)
&& Objects.equal(e.getValue(), value);
} @Override public final int hashCode() {
return (key == null ? 0 : key.hashCode()) ^
(value == null ? 0 : value.hashCode());
} @Override public final String toString() {
return key + "=" + value;
}
}
主要信息就是每一个数据项都包含了我们存储的key,value以及根据key算出来的hash值,next用于发生哈希碰撞的时候指向其下一个数据项。
三、HashMap构造方法
HashMap构造方法有如下:
public HashMap()
public HashMap(int capacity)
public HashMap(int capacity, float loadFactor)
public HashMap(Map<? extends K, ? extends V> map)
构造方法有如上4种方式,我们平时最常用的就是第一种方式,直接初始化然后不停往里面仍数据就可以了,第二种初始化的时候可以指定容量大小,第三,四中估计大部分人没用过,第三种除了指定容量大小我们还可以指定扩容因子,不过我们还是不要动扩容因子为好,指定为0.75是时间和空间的权衡,平时我们使用就用默认的0.75就可以了。
我们看一下HashMap()这种构造方式:
public HashMap() {
table = (HashMapEntry<K, V>[]) EMPTY_TABLE;
threshold = -1; // Forces first put invocation to replace EMPTY_TABLE
}
太简单了,就是初始化table为空的数组EMPTY_TABLE,这个EMPTY_TABLE的初始化容量可不为0,源码如下:
private static final Entry[] EMPTY_TABLE
= new HashMapEntry[MINIMUM_CAPACITY >>> 1];
看到了吧,初始化容量为MINIMUM_CAPACITY的一半,也就是2。
此外threshold初始化的时候置为-1。
接下来我们在看下HashMap(int capacity) 这种构造方式:
public HashMap(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("Capacity: " + capacity);
} if (capacity == 0) {
@SuppressWarnings("unchecked")
HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
table = tab;
threshold = -1; // Forces first put() to replace EMPTY_TABLE
return;
} if (capacity < MINIMUM_CAPACITY) {
capacity = MINIMUM_CAPACITY;
} else if (capacity > MAXIMUM_CAPACITY) {
capacity = MAXIMUM_CAPACITY;
} else {
capacity = Collections.roundUpToPowerOfTwo(capacity);
}
makeTable(capacity);
} private HashMapEntry<K, V>[] makeTable(int newCapacity) {
@SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable
= (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
table = newTable;
threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
return newTable;
}
6-12行逻辑大体就是我们调用hashmap()空参数的构造函数初始化一样。
14-17行,就是对我们设置的容量capacity进行检查了,如果小于MINIMUM_CAPACITY那么就重置为MINIMUM_CAPACITY,如果大于MAXIMUM_CAPACITY则重置为MAXIMUM_CAPACITY。
19行,Collections.roundUpToPowerOfTwo这个方法就是找出一个2^n的数,使其不小于给出的数字,并且最近接给出的数字。
比如:
Collections.roundUpToPowerOfTwo(3)返回4,22.
Collections.roundUpToPowerOfTwo(4)返回4,22.
Collections.roundUpToPowerOfTwo(100)返回128,27.
明白了吧?也就是说返回的数肯定是2的几次方,也就是说hashmap的容量肯定是2的几次方形式,这里很重要,一定要记住,后续分析的时候还会用到。
接下来就是调用makeTable了。
26,27就是根据给定的容量创建newTable数组。
28行,成员变量table指向新创建的newTable数组。
29行,计算threshold的值,也就是我们指定的容量的四分之三了。
好了,以上就是构造方法逻辑,其余两种方法可自行查看一下,比较核心的就是19行代码,对capacity数据的转换,约束hashmap容量大小肯定为2的n次方。
四、HashMap中put方法分析
接下来就是HashMap核心所在了,我们一点点分析,先看下put方法源码:
@Override
public V put(K key, V value) {
if (key == null) {
return putValueForNullKey(value);
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// No entry for (non-null) key is present; create one
modCount++;
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
}
3-5行,如果我们放入的数据key为null,那么执行4行代码逻辑并且直接返回,putValueForNullKey源码如下:
private V putValueForNullKey(V value) {
HashMapEntry<K, V> entry = entryForNullKey;
if (entry == null) {
addNewEntryForNullKey(value);
size++;
modCount++;
return null;
} else {
preModify(entry);
V oldValue = entry.value;
entry.value = value;
return oldValue;
}
}
大体逻辑很简单,就是对成员变量entryForNullKey操作,其就是HashMapEntry对象实例,3-8行如果entry为null,则代表之前没有放入过key为null的数据,则只需要创建即可。8-12行表示之前放入锅key为null的数据,那么只需要将value替换为新的value即可,这里说明HashMap只会有一个数据的key为null,重复放入只会将value替换为最新value.好了,这里就只是简单分析一下。
回到put方法,如果我们放入的key不为null,则继续向下执行:
6行,根据key计算二次哈希值,源码如下:
public static int secondaryHash(Object key) {
return secondaryHash(key.hashCode());
} private static int secondaryHash(int h) {
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
就是将key的hashCode方法返回的值传入secondaryHash(int h) 再次计算一次返回一个值,这里最重要的一点就是我们传入的key必须有hashCode()方法并且每次返回的值一样,如果HashMap Key的哈希值在存储键值对后发生改变,Map可能再也查找不到这个Entry了,所以HashMap中key我们需要使用不可变对象,比如经常使用的String,Integer对象,其HashCode()方法分别如下:
@Override
public int hashCode() {//String中HashCode()方法
int hash = hashCode;
if (hash == 0) {
if (count == 0) {
return 0;
}
for (int i = 0; i < count; ++i) {
hash = 31 * hash + charAt(i);
}
hashCode = hash;
}
return hash;
} @Override
public int hashCode() {//Integer中HashCode()方法
return value;
}
回到put方法,则继续向下执行:
7行定义局部变量tab指向全局变量table数组。
8行,计算放入的数据在tab中的位置,计算方式为key的hash值按位与tab的长度减1,这样确保了计算出的index不会超出数组角标,比如:
key的hash值为11111111111111111111111111111111,tab容量为8,则tab.length-1为7,其数组角标范围为0~7。
hash & (tab.length - 1)按位与计算如下:
也就是经过上述计算最大值为7,不会超过数组角标。
为什么要进行二次哈希值得计算呢?
比如我们放入三个数据,key的HashCode值分别为:31,63,95。tab容量为8
如果不进行二次哈希值计算索引index,也就是key.hashcode() & (tab.length - 1),计算如下:
31=00011111 & 00000111 = 0111 = 7
63=00111111 & 00000111 = 0111 = 7
95=01011111 & 00000111 = 0111 = 7
进行二次哈希值后再计算索引index,也就是源码中secondaryHash(key.hashCode())& (tab.length - 1),计算如下:
31=00011111 =>secondaryHash=> 00011110 & 00000111= 0110 = 6
63=00111111 ==secondaryHash=> 00111100 & 00000111= 0100 = 4
95=01011111 ==secondaryHash=> 01011010 & 00000111= 0010 = 2
如上不经过二次哈希值计算最终计算出的index值均为7,也就是我们放入数组中都处于同一位置。而经过二次哈希值计算之后再计算index值分别为6,4,2也就是在数组中处于了三个不同的位置,这样就达到了更加散列的效果。但是即使经过二次哈希值计算也不能保证计算出的index值都不相同,这里只是尽可能的散列化,不能保证避免哈希碰撞。
回到put方法,继续向下分析:
我们知道HashMap存储数据结构如下:
简单说就是我们放入一个数据的时候会先根据数据项的key计算出其在table数组中的索引,如果索引位置已经有元素了,那么则与已经放入的数据形成链表的关系,相信稍有经验的都明白,这里只是稍微提一下。
9-16行的for循环逻辑就是挨个遍历所在数组行链表中每一个数据项,然后将每个数据项的hash值和key与将要放入的key及其hash值比较,如果二者均相等则表明HashMap中已经存在此数据。
12-14行就是将对应数据项的value值替换为新的value值,并将之前value返回。
如果整个for循环都没有找到则表明HashMap中没有将要存储的数据项,继续向下执行。
19行,判断是否需要扩容,threshold上面说过值为table容量的四分之三,size记录我们HashMap中存入数据的大小,我们放入数据时如果超过容量的四分之三那么就需要扩容了。
20行,如果需要扩容那么调用doubleCapacity()方法进行扩容(后续会仔细分析扩容机制),扩容完此方法会返回扩容后的数组。
21行,由于数组已经扩容,容量发生了变化,所以这里需要重新计算一下将要放入数据的index索引。
23行调用addNewEntry方法将数据放入数组中。addNewEntry源码如下:
void addNewEntry(K key, V value, int hash, int index) {
table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}
这里就是根据我们传入的key,value,hash值新建HashMapEntry数据节点,此数据节点的next指向原table[index],最后将新数据节点赋值给table[index],这里说的有点蒙圈,用图来解释一下,又要展示我强大的画图能力了:
这里通过阅读源码可以发现新添加的数据项是放在链表头部的,而不是直接放在尾部。
好了,以上就是put方法主要逻辑了,不再做其余分析,下面我们着重看一下HashMap的扩容机制。
五、HashMap中扩容机制分析
好了,如果你看到这里那么清理一下大脑吧,下面的有点烧脑了。
废话少说,直接看扩容方法源码;
private HashMapEntry<K, V>[] doubleCapacity() {
HashMapEntry<K, V>[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
return oldTable;
}
int newCapacity = oldCapacity * 2;
HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
if (size == 0) {
return newTable;
}
for (int j = 0; j < oldCapacity; j++) {
/*
* Rehash the bucket using the minimum number of field writes.
* This is the most subtle and delicate code in the class.
*/
HashMapEntry<K, V> e = oldTable[j];
if (e == null) {
continue;
}
int highBit = e.hash & oldCapacity;
HashMapEntry<K, V> broken = null;
newTable[j | highBit] = e;
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
int nextHighBit = n.hash & oldCapacity;
if (nextHighBit != highBit) {
if (broken == null)
newTable[j | nextHighBit] = n;
else
broken.next = n;
broken = e;
highBit = nextHighBit;
}
}
if (broken != null)
broken.next = null;
}
return newTable;
}
2-6行oldTable,oldCapacity记录原来数组,数组长度以及检查原数组长度是否已经达到MAXIMUM_CAPACITY,如果已经达到最大长度,那么不好意思了,直接返回原数组了,老子无法给你扩容了,都那么长了,还扩什么容,自己继续在原数组玩吧,管你哈希碰撞导致链表多长我都不管了。
7行,定义newCapacity也就是新数组长度为原数组长度的2倍。
8行,就是执行makeTable()逻辑,创建新的数组newTable了,至于makeTable方法上面说过,就不再分析了。
9-11行,检查size是否为0,如果为0那么表明HashMap中没有存储过数据,不用执行下面的数据拷贝逻辑了,直接返回newTable就可以了。
12-37行,这可就是HashMap整个类的精华所在了,这几行代码看不懂这个类你就没有真正理解,看懂了其余扫一下就明白了。
假设原HashMap如图:
12行很简单就是遍历原数组中每个位置的数据,也可以说每个链表的头数据。
13-16行,风骚的注释:直白翻译就是下面这几行代码是这个类中最风骚的几行代码。
17-20行代码,就是检查取出的数组中每个数据项是否为null,为null则表明此行没有数据,继续循环就可以了。
在继续向下讲请大家思考一个问题:HashMap中同一个链表中每一个数据项的哈希值有什么相同点?比如原数组大小是8,那么同一链表中每一个数据项的哈希值有什么相同点?
思考。。。。。
这里直接说了:能在同一链表说明计算出来的index值相同,在看计算公式为int index = hash & (tab.length - 1),这里在扩容之前tab.length-1的值是相同的,比如数组长度为8,那么tab.length - 1的二进制表示为00000111,不同hash值计算出的index又相同,那么这里同一链表中每一个数据项的hash值得最后三位一定相同,只有这样计算出的index值才相同,如下:
如上图 两个hash值不同的数据项,经过运算后得出index均为2,原因就是虽然整体的hash值不同,但是最后三位均为010,所以计算出index值是相同的(此处假设数组长度为8)。
进而得出结论:如果HashMap中数组长度为2的n次方,那么同一链表中不同数据项的hash值的最后n位一定相同。
好了,到这里第一个难点通过,我们继续分析doubleCapacity()方法。
21行,int highBit = e.hash & oldCapacity计算出highBit位,翻译过来就是高位,这他妈又是什么玩意?仔细看计算方式与的oldCapacity,而不是oldCapacity-1,所以这里取得是数据项hash值得第n+1位(hashmap数组长度为2的n次方)是0还是1,这里一定要知道HashMap数组长度一定为2的n次方,二进制形式就是第n+1位为1其余为均为0。这里先记住这个highBit是哪一位,后面会用到。
22行,定义一个broken,知道有这么个玩意,后面也会用到。
23行,newTable[j | highBit] = e,将我们从原数组取出的数据项放入新数组中,也就是数据的拷贝了,注意这里e是每一个链表的头部,也就是处于数组中的数据。链表其余数据是通过24行for循环挨个遍历再放入新数组中的。但是这里有个疑问原数组放入数据是按照hash & (tab.length - 1)计算其在数组中位置的,这里怎么成了j | highBit这样计算了呢?这里真是卡住我了,一开始我是怎么想怎么想不通,但是我觉得二者之间一定有什么关系,不可能用两个完全不相关的算法来计算同一数据项在数组中的位置,绝不可能,一定有内在联系,我查啊查,算啊算,在经过如下计算我终于想明白了:hash & (tab.length - 1)与j | highBit这二者逻辑是完全相同的,TMD,逻辑竟然是相同的。
接下来咱们推导分析一下:
j | highBit
= j | (e.hash & oldCapacity) 第一步
= (e.hash & (oldCapacity-1)) | (e.hash & oldCapacity) 第二步
= e.hash & ( (oldCapacity-1) | oldCapacity) 第三步
= e.hash & (newCapacity- 1) 第四步
从开始到第一步很简单了,highBit的计算方式就是e.hash & oldCapacity这里只是替换回来。
第一步到第二步,j怎么就成了e.hash & (oldCapacity-1)呢?还记得index的计算方式吗?就是e.hash & (oldCapacity-1),那就是说j就是index了,在看看j是什么?j就是从0开始到oldCapacity的值,这里我们想一下啊,e就是通过oldTable[j]获取的,我们想想put方法怎么放入的呢,不就是oldTable[e.hash & (oldCapacity-1)] = e吗,想到了什么?想到了什么?对,通过j获取的元素e,这个j就是e.hash & (oldCapacity-1),所以这里可以替换的。
第二步到第三步就是数学方面的,记住就可以了。
第三步到第四步怎么来的呢?也就是(oldCapacity-1) | oldCapacity与newCapacity- 1相等,还记得上面说的HashMap数组容量一定是2的n次方吗?并且newCapacity = oldCapacity * 2 。
oldCapacity为2的n次方,也就是n+1位为1,其余都为0,oldCapacity-1也就是0到n位为1其余都为0,二者或运算后0到n+1为1其余位0。
newCapacity= oldCapacity * 2 也就是n+2位为1其余位0,newCapacity- 1也就是0到n+1位为1其余为0.
所以(oldCapacity-1) | oldCapacity与newCapacity- 1相等。
到此,我们就证明了j | highBit = e.hash & (newCapacity- 1) 其计算数据项在新数组中位置与原数组的计算逻辑是一样的,只不过十分巧妙的运用了位运算,好了,想明白这里恭喜你通过了第二个难点,我们继续向下分析。
24-34行就是遍历链表中数据项了,把他们挨个放入新数组中,这里思考一个问题?在原数组中同一链表的数据项在新数组中还处于同一链表吗?如果不是那么是什么决定它们不在同一链表了?
在上面分析的时候我们得出一个结论:如果HashMap中数组长度为2的n次方,那么同一链表中不同数据项的hash值的最后n位一定相同。
扩容后数组容量为原来的2倍了,根据index的计算方式e.hash & (newCapacity- 1)每个数据项的hash值是不变的,但是长度变了,所以同一链表中不同数据项在新数组中不一定还处于同一链表,那么具体是什么决定在新数组中二者在不在同一链表呢?
原数组长度为2的n次方,新数组长度扩容后为原数组2倍也就是2的n+1次方,原数组中同一链表中不同数据项的hash值的最后n位一定相同,所以新数组同一链表中不同数据项的hash值的最后n+1位一定相同。如果上面讲的你真的理解了,这里就不难理解,不过多解释。
在原数组中同一链表的数据项已经确保了hash值最后n位相同,按照计算方式新数组中处于同一链表的数据项需要确保hash值最后n+1位相同即可,既然原链表中的数据项最后n位已经相同了,在新数组中是否处于同一链表那么只需要比较同链表数据项hash值的第n+1位即可,如果相同则表明在新数组中依然处于同一链表,如果不同那么就处于不同链表了,上面的高位highBit就是取的是每一数据项的第n+1位,后面比较也只是比较每个数据项的highBit是否相同。
好了,这里我认为解释的已经很清楚了,这里你要是明白了,恭喜你,HashMap中最难理解的部分你已经完全掌握了。
至于24-34行具体逻辑我就不一一分析了,静下心来,自己试着分析,难度不大。
六、总结
好了,到这里本篇就要结束了,咦?这不就分析了一个put方法外加扩容机制吗?这就完了?是的,我想说的就这些,这部分是最难理解的,至于其余自己看看都能理解的差不多了。
最关键是一定要理解扩容机制,那几行最难理解的代码设计的真是巧妙,大神的思想真是无法企及啊!!!
本篇到此结束,希望对你有用。
声明:文章将会陆续搬迁到个人公众号,以后文章也会第一时间发布到个人公众号,及时获取文章内容请关注公众号
Android版数据结构与算法(四):基于哈希表实现HashMap核心源码彻底分析的更多相关文章
- Android版数据结构与算法(三):基于链表的实现LinkedList源码彻底分析
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. LinkedList 是一个双向链表.它可以被当作堆栈.队列或双端队列进行操作.LinkedList相对于ArrayList来说,添加,删除元素效 ...
- Android版数据结构与算法(二):基于数组的实现ArrayList源码彻底分析
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 本片我们分析基础数组的实现--ArrayList,不会分析整个集合的继承体系,这不是本系列文章重点. 源码分析都是基于"安卓版" ...
- Android版数据结构与算法(五):LinkedHashMap核心源码彻底分析
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 上一篇基于哈希表实现HashMap核心源码彻底分析 分析了HashMap的源码,主要分析了扩容机制,如果感兴趣的可以去看看,扩容机制那几行最难懂的 ...
- java数据结构和算法09(哈希表)
树的结构说得差不多了,现在我们来说说一种数据结构叫做哈希表(hash table),哈希表有是干什么用的呢?我们知道树的操作的时间复杂度通常为O(logN),那有没有更快的数据结构?当然有,那就是哈希 ...
- Android版数据结构与算法(七):赫夫曼树
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 近期忙着新版本的开发,此外正在回顾C语言,大部分时间没放在数据结构与算法的整理上,所以更新有点慢了,不过既然写了就肯定尽力将这部分完全整理好分享出 ...
- Android版数据结构与算法(一):基础简介
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 一.前言 项目进入收尾阶段,忙忙碌碌将近一个多月吧,还好,不算太难,就是麻烦点. 数据结构与算法这个系列早就想写了,一是梳理总结,顺便逼迫自己把一 ...
- Android版数据结构与算法(八):二叉排序树
本文目录 前两篇文章我们学习了一些树的基本概念以及常用操作,本篇我们了解一下二叉树的一种特殊形式:二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree) ...
- Android版数据结构与算法(六):树与二叉树
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 之前的篇章主要讲解了数据结构中的线性结构,所谓线性结构就是数据与数据之间是一对一的关系,接下来我们就要进入非线性结构的世界了,主要是树与图,好了接 ...
- JavaScript 版数据结构与算法(二)队列
今天,我们要讲的是数据结构与算法中的队列. 队列简介 队列是什么?队列是一种先进先出(FIFO)的数据结构.队列有什么用呢?队列通常用来描述算法或生活中的一些先进先出的场景,比如: 在图的广度优先遍历 ...
随机推荐
- 魔咒,90%未学满三个月Python编程的朋友都会出错!
Python语言虽然优美,简洁和强大,但是也有很多坑,一不小心就会掉进去.我学Python的时候也遇到过,今天总结一下,希望对大家能有收获! Python的默认参数就被创建 一次,而不是每次调用函数的 ...
- oracle 触发器,当一个表更新或插入时将数据同步至另个库中的某个表中
有两个表分别是 A用户下的 T_SRC_WEATHER_TSPG字段如图, B用户下的t_src_weather 表,如图: 要求,当A用户下的T_SRC_WEATHER_TSPG表有插入或者更新数据 ...
- 团队项目第二阶段个人进展——Day7
一.昨天工作总结 冲刺第七天,动手完成了一个demo来实现数据的上传与下载 二.遇到的问题 代码逻辑没看太懂 三.今日工作规划 对发布页面的数据进行处理,实现能够请求和响应,并学习如何实现图片的上传与 ...
- Python_sniffer(网络嗅探器)
import socket import threading import time activeDegree=dict() flag=1 def main(): global activeDegre ...
- OC和Swift中的UITabBar和UINaviGationBar的适配 [UITabbar在IPad中的适配]
作者 sundays http://www.cnblogs.com/sundaysgarden/ OC中UITabbar的适配[iphoneX和Ipad适配] 自定可以UITabar 自定义UITab ...
- Http Header信息
REMOTE_ADDR – 访问客户端的 IP 地址 HTTP_VIA – 如果有该条信息, 就证明您使用了代理服务器,代理服务器的地址就是后面的数值. HTTP_X_FORWARDED_FOR – ...
- 深入浅出 TCP/IP 协议
TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入因特网,以及数据如何在它们之间进行传输.TCP/IP 协议采用4层结构,分别是应用层.传输层.网络层和链路 ...
- 接口调用(发送http请求)
// 向对应的url地址发送http请求, 并获取响应的json字符串 public String getHttpResponse(String url) { // result用 ...
- PAT1106:Lowest Price in Supply Chain
1106. Lowest Price in Supply Chain (25) 时间限制 200 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CH ...
- mysql中enum类型理解
ENUM是枚举类型,它虽然只能保存一个值,却能够处理多达65535个预定义的值.下面是我写的一个mysql语句 CREATE TABLE student( id INT(11) PRIMARY key ...