HashMap

public class HashMap<K, V> extends AbstractMap<K, V>
implements Map<K, V>, Cloneable, Serializable {
}

1. 一些重要参数

1.1 serialVersionUID属性

// 序列化版本号
private static final long serialVersionUID = 362498820763181265L;

serialVersionUID适用于java序列化机制。简单来说,JAVA序列化的机制是通过 判断类的serialVersionUID来验证的版本一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID于本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。

具体序列化的过程是这样的:序列化操作时会把系统当前类的serialVersionUID写入到序列化文件中。当反序列化时系统会自动检测文件中的serialVersionUID,判断它是否与当前类中的serialVersionUID一致。如果一致说明序列化文件的版本与当前类的版本是一样的,可以反序列化成功,否则就失败;

1.2 DEFAULT_INITIAL_CAPACITY属性

/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

DEFAULT_INITIAL_CAPACITYHashMap的默认初始容量,大小为16

1.3 DEFAULT_LOAD_FACTOR属性

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

DEFAULT_LOAD_FACTORHashMap的默认负载因子大小,大小为0.75。

元素数量 ≥ 容量*负载因子​,那么HashMap需要扩容。

1.4 MAXIMUM_CAPACITY属性

/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

MAXIMUM_CAPACITY属性是HashMap的最大容量。

1.5 TREEIFY_THRESHOLD属性

/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;

TREEIFY_THRESHOLD属性是HashMap转变为红黑树实现的阈值。当元素数量大于这个值,就会转变为红黑树。

1.6 UNTREEIFY_THRESHOLD属性

UNTREEIFY_THRESHOLDHashMap由红黑树转变为链表的阈值。当元素数量大于这个值,就会转变为链表。

1.7 MIN_TREEIFY_CAPACITY属性

MIN_TREEIFY_CAPACITY是可以将链表转变为红黑树的最小数组容量。如果没有这个限制,假如节点过多,节点数组会频繁地resize

2. 一些重要属性

2.1 table属性

/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K, V>[] table;

table属性用来存节点。

2.2 entrySet属性

/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K, V>> entrySet;

entrySet属性用来缓存键值对集合。

2.3 size属性

/**
* The number of key-value mappings contained in this map.
*/
transient int size;

size属性用来存map的键值对数目。

2.4 modCount属性

/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;

modCount属性记录了map结构被修改的次数,用于在迭代器中的快速失败。

2.5 threshold属性

/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;

threshold属性记录了下一个需要resize的size大小

2.6 loadFactor属性

/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;

loadFactor属性记录了当前map的负载因子

2.7 Node内部类

static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next; Node(int hash, K key, V value, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
} public final K getKey() {
return key;
} public final V getValue() {
return value;
} public final String toString() {
return key + "=" + value;
} public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
} public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
} public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

Node内部类实则表达了一个键值对, 并包含了哈希值和next指针。

2.8 EntrySet内部类

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (Node<K,V> e : tab) {
for (; e != null; e = e.next)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}

EntrySet内部类用来表示当前HashMap的键集合。

3. 一些工具方法

3.1 hash方法

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

这个hash()方法的(h = key.hashCode()) ^ (h >>> 16)过程值得研究。

首先,h >>> 16是将哈希值向右无符号移动16位,也就相当于取了原值的高16位。

0000 0100 1011 0011 1101 1111 1110 0001 >>> 16

得到:

0000 0000 0000 0000 0000 0100 1011 0011

然后,我们再来看一个JDK1.7中的indexFor()方法。在JDK8以后,源码中也大量出现tab[(n - 1) & hash]的形式,实际上也是一样的。

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

indexFor()方法的返回值就是table数组中我们想要找到的数组下标。但由于绝大多数情况下length一般都小于2^16即小于65536。所以return h & (length-1)结果始终是h的低16位与(length-1)进行&运算。

所以很显然的是,我们每次在计算下标的时候,都几乎只能用到低位。如果我们想一个办法,把高位也利用起来,那么就可以增加散列的程度。所以hash()方法中选择了与自身的高十六位(h >>> 16)进行异或,来利用到高位。

3.2 tableSizeFor方法

/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

tableSizeFor方法用于计算参数cap对应的resize阈值。往往出现为下面的语句。

threshold = tableSizeFor(size);

3.3 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) {// 如果旧容量不为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;
} else if (oldThr > 0) // 如果旧容量为0,且旧扩容阈值大于0
// 那么就让新容量变为旧扩容阈值
newCap = oldThr;
else { // 旧容量和旧扩容阈值都为0
// 令新容量为默认初始容量,并计算扩容阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
} if (newThr == 0) {// 如果新扩容阈值为0
// 计算当前的容量*负载因子
float ft = (float) newCap * loadFactor;
// 设置新扩容阈值为ft或Integer.MAX_VALUE
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
// 更新扩容阈值
threshold = newThr;
// 创建一个新的table,大小为先前计算出来的新容量
@SuppressWarnings({"rawtypes", "unchecked"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
// 更新table指针
table = newTab; if (oldTab != null) {// 如果旧数组不为null
for (int j = 0; j < oldCap; ++j) {// 遍历之
Node<K, V> e;
if ((e = oldTab[j]) != null) {// 如果j处的旧数组值不为null,存进e
// 将旧数组里的值设为null
oldTab[j] = null;
if (e.next == null)// 如果e.next为null
// 在新数组中找到并赋值
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)// 如果e这里是一棵树,调用split分割树,并带到新数组中
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
else { // e这里是链表,且e.next不为null
// 下面要进行一个链表分割操作,将原链表分为lo串和hi串两串。
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
do {
// 保存next
next = e.next;
/* 通过e.hash & oldCap是0还是1,可以判断e应该在原串上还是在新串上。
hashMap扩容前后容量大小都是2的幂,假如原容量size二进制形式是1000,新容量二进制形式是10000
又由于数组下标位置的计算公式是e.hash & (size-1),那么原来是hash & 0111,后来是hash & 01111。
也就实际上是多用了hash的高一位用来确定位置。那么hash&size就可以知道hash值中高的这一位是0还是1
如果高的一位是0,那么新位置和原位置一样,不变。如果高的一位是1,那么就要移动位置。
*/
if ((e.hash & oldCap) == 0) {// 如果位置不用改变
// 将e加入到lo串中
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else {// 如果位置需要改变
// 将e加入到hi串中
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);// 如果e赋为next后不为null
// 将lo串放到下标为j的位置处,即不变的位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 将hi串放到下标为j+oldCap处,因为新位置的hash中高一位是1,那下标就要加一个oldCap
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

先尝试用数学方法确定新容量和新扩容阈值。

随后遍历数组,并深入遍历其中的链表/红黑树。如果是链表,要将他分裂为lo链表和hi链表。

3.4 treeifyBin方法

final void treeifyBin(Node<K, V>[] tab, int hash) {
int n, index;
Node<K, V> e;
// 如果数组很空或很小,那么就resize()
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {// hash对应的位置在数组中不为null,就可以进入这个循环
TreeNode<K, V> hd = null, tl = null;
do {
// 将节点e转变为一个树节点存到变量p
TreeNode<K, V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
// 转变为红黑树
hd.treeify(tab);
}
}

treeifyBin方法将符合要求的链表转变为一个红黑树。

4. 一些业务方法

4.1 get方法

public V get(Object key) {
Node<K, V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
} final Node<K, V> getNode(int hash, Object key) {
Node<K, V>[] tab;
Node<K, V> first, e;
int n;
K k;
if ((tab = table) != null // table不为空
&& (n = tab.length) > 0 // table长度不为0
&& (first = tab[(n - 1) & hash]) != null) {// 在算出来的位置拿到的Node不为null,这是链表头
if (first.hash == hash // 检查first的哈希值,假如正确且key也符合
&& ((k = first.key) == key || (key != null && key.equals(k))))
return first;// 直接返回first
if ((e = first.next) != null) {// 如果first链表长度不止为1
if (first instanceof TreeNode) {// 如果table当前位置延伸出的是一个红黑树
return ((TreeNode<K, V>) first).getTreeNode(hash, key);// 调用getTreeNode方法
}
// 不是红黑树,是链表,遍历查找即可
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

详见上面的注释。用哈希值计算出数组中的位置,再沿着链表或红黑树去找。

4.2 put方法

public V put(K key, V value) {
// 调用putVal
return putVal(hash(key), key, value, false, true);
} 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)
// 如果table为null或者为空,先扩容,再把长度赋值为n
n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null)
// 如果找到的指定位置为空,那么在这里用新键值对调用newNode
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))))// 并且key也吻合
// 将当前节点p赋给e
e = p;
else if (p instanceof TreeNode)// 如果当前p是一个树节点,调用putTreeVal方法
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {// 如果当前是一个链表节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {// 令e=p.next,如果它为null
// 令p.next为用新键值对创建的newNode,就是将新节点插在了链表尾
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,for循环一开始的时候e=p.next,所以相当于p后移了
p = e;
}
}
if (e != null) { // 如果key已经存在,更新它并返回oldValue
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 检查是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

详见上面的注释。用哈希值计算出数组中的位置,再在链表或红黑树中搜索,做更新操作或newNode操作。

4.3 remove方法

public V remove(Object key) {
Node<K, V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
} final Node<K, V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K, V>[] tab;
Node<K, V> p;
int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {// 检查对应数组位置是否存在链表/树
Node<K, V> node = null, e;
K k;
V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果链表头就是,赋值给node
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
// 如果是树,就调用对应的方法找到,赋值给node
node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
else {
// 是链表,就遍历查找
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
// 找到了,将当前节点赋值给node
node = e;
// 退出循环,这导致了当找到后,下面的p=e没有执行
break;
}
// 将当前节点赋值给p
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
// 如果是树,用对应方法删除
((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
else if (node == p)
// 如果要移除的是链表头,那么根据上面do-while块中的逻辑,node==p,那么我们直接让链表从node.next开始
tab[index] = node.next;
else
// 如果要移除的不是链表头,那么根据上面do-while块中的逻辑node!=p,之前是p->node,删除node
p.next = node.next;
// 处理modCount和size属性
++modCount;
--size;
afterNodeRemoval(node);
// 返回remove掉的节点node
return node;
}
}
return null;
}

找->删,非常清晰的逻辑

4.4 clone方法

@Override
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize();
result.putMapEntries(this, false);
return result;
}

clone()方法对HashMap做了一个浅拷贝。

5. 一些来自JDK8的方法

下面的方法来自于重写JDK8中的Map接口中的方法。

一些函数式的方法在此不做解析,如compute()merge()等。

5.1 getOrDefault / putIfAbsent

@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
} @Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}

很好理解,获得实际值或默认值/不存在时插入。

5.2 remove / replace

@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
} @Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
} @Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}

当对应键值对存在时,进行删除\替换操作。

JDK源码阅读(4):HashMap类阅读笔记的更多相关文章

  1. JDK源码分析之hashmap就这么简单理解

    一.HashMap概述 HashMap是基于哈希表的Map接口实现,此实现提供所有可选的映射操作,并允许使用null值和null键.HashMap与HashTable的作用大致相同,但是它不是线程安全 ...

  2. JDK源码分析(三)——HashMap 下(基于JDK8)

    目录 概述 内部字段及构造方法 哈希值与索引计算 存储元素 扩容 删除元素 查找元素 总结 概述   在上文我们基于JDK7分析了HashMap的实现源码,介绍了HashMap的加载因子loadFac ...

  3. JDK源码分析(三)——HashMap 上(基于JDK7)

    目录 HashMap概述 内部字段及构造方法 存储元素 扩容 取出元素 删除元素 判断 总结 HashMap概述   前面我们分析了基于数组实现的ArrayList和基于双向链表实现的LinkedLi ...

  4. 【jdk源码学习】HashMap

    package com.emsn.crazyjdk.java.util; /** * “人”类,重写了equals和hashcode方法...,以id来区分不同的人,你懂的... * * @autho ...

  5. 【jdk源码3】HashMap源码学习

    可以毫不夸张的说,HashMap是容器类中用的最频繁的一个,而Java也对它进行优化,在jdk1.7及以前,当将相同Hash值的对象以key的身份放到HashMap中,HashMap的性能将由O(1) ...

  6. jdk源码阅读笔记-HashSet

    通过阅读源码发现,HashSet底层的实现源码其实就是调用HashMap的方法实现的,所以如果你阅读过HashMap或对HashMap比较熟悉的话,那么阅读HashSet就很轻松,也很容易理解了.我之 ...

  7. 【并发编程】【JDK源码】JDK的(J.U.C)java.util.concurrent包结构

    本文从JDK源码包中截取出concurrent包的所有类,对该包整体结构进行一个概述. 在JDK1.5之前,Java中要进行并发编程时,通常需要由程序员独立完成代码实现.当然也有一些开源的框架提供了这 ...

  8. JDK源码那些事儿之浅析Thread上篇

    JAVA中多线程的操作对于初学者而言是比较难理解的,其实联想到底层操作系统时我们可能会稍微明白些,对于程序而言最终都是硬件上运行二进制指令,然而,这些又太过底层,今天来看一下JAVA中的线程,浅析JD ...

  9. 如何阅读jdk源码?

    简介 这篇文章主要讲述jdk本身的源码该如何阅读,关于各种框架的源码阅读我们后面再一起探讨. 笔者认为阅读源码主要包括下面几个步骤. 设定目标 凡事皆有目的,阅读源码也是一样. 从大的方面来说,我们阅 ...

  10. JDK源码阅读(一):Object源码分析

    最近经过某大佬的建议准备阅读一下JDK的源码来提升一下自己 所以开始写JDK源码分析的文章 阅读JDK版本为1.8 目录 Object结构图 构造器 equals 方法 getClass 方法 has ...

随机推荐

  1. MySql 文件导入导出

    1.将表输出到文件 select * FROM zhilianzhaopin4 INTO OUTFILE 'G:/test.csv'      --------输出位置 fields terminat ...

  2. java基本数据类型转换字符串

    1.基本数据类型转换为字符串 int t1 = 2; String t2 = Integer.toString(t1); 2.字符串转换为基本数据类型 int t3 = Integer.parseIn ...

  3. MZY项目笔记:session歧路

    from my typora MZY项目笔记:session歧路 文章目录 MZY项目笔记:session歧路 那该怎么办? 1. 手动加上cookie的header. 2.自己模拟一个Session ...

  4. Windows-MacOSX-Ubuntu·不同平台文件互传文件共享

    时间:2018-11-23 整理:byzqy 标题:Mac下的virtual box 安装的Ubuntu虚拟机互传文件问题 地址:https://blog.csdn.net/qq_20044689/a ...

  5. MySQL-存储引擎-MERGE

    MERGE存储引擎是一组Myisam表的组合,这些Myisam表必须结构完全相同,MERGE表本身并没有数据,对MERGE类型的表可以进行查询.更新.删除操作,这些操作实际上是对内部的Myisam表进 ...

  6. Cookie及通过Cookie常见应用

    会话的概念 会话可以简单理解为:用户打开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一个会话. 会话需要解决的问题 每个用户与服务器进行交互的过程中,各自会有一 ...

  7. Linux系统的高级网络配置(bond、team、网桥)

    1.bond接口 Red Hat Enterprise Linux 允许管理员使用 bonding 内核模块和称为通道绑定接口的特殊网络接口将多个网络接口绑定 到一个通道.根据选择的绑定模式 , 通道 ...

  8. finally方法体

    1.资源释放 java7可以在try(创建资源对象,方法体结束之后自动释放) 2.finally中有返回

  9. 网站URL如何SEO优化

    前言 本文讲解网站的URL如何进行SEO优化,并在自己的WordPress博客echeverra中优化URL. 起因 对于SEO我了解的并不多,只知道SEO做的好,那么各大搜索网站搜索你网站相关内容时 ...

  10. 动态规划精讲(一)LC 最长递增子序列的个数

    最长递增子序列的个数 给定一个未排序的整数数组,找到最长递增子序列的个数. 示例 1: 输入: [1,3,5,4,7]输出: 2解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, ...