java集合对象实现原理
1.集合包
集合包是java中最常用的包,它主要包括Collection和Map两类接口的实现。
对于Collection的实现类需要重点掌握以下几点:
1)Collection用什么数据结构实现?
2)Collection的创建、添加对象、删除对象、获取对象、遍历、判断是否存在、排序等操作的原理,及优缺点。
1.1.Collection
Collection存放的是多个单对象。Collection又分为两类接口,List和Set。
1.1.1.List
List支持放入重复的对象。List的实现类主要包括:ArrayList、LinkedList、Vector、Stack。
ArrayList
1)从ArrayList的构造方法可以看出,他使用数组作为底层数据结构。
public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } |
默认的initialCapacity为10。
2)添加对象
由于是定容量的数组存储对象,总有数组满的时候,此时需要我们进行扩容。
public void ensureCapacity(int minCapacity) {//minCapacity为目前需要的最小容量 modCount++; int oldCapacity = elementData.length;//当前list的最大存储容量 if (minCapacity > oldCapacity) { Object oldData[] = elementData; int newCapacity = (oldCapacity * 3)/2 + 1;//扩展容量1.5倍 if (newCapacity < minCapacity) newCapacity = minCapacity; // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity);//复制、扩容 } } |
其中Arrays.copyOf可能会比较陌生,他的具体实现如下:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength);//创建一个新数组,该数组的类型和之前ArrayList中元素的类型一致。 System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));//System arrayCopy return copy; } |
添加对象还有一种方法,add(int index, E element),在指定位置插入一个对象。
public void add(int index, E element) { if (index > size || index < 0) throw new IndexOutOfBoundsException( "Index: "+index+", Size: "+size); ensureCapacity(size+1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } |
指定的位置必须是存在的(0到size之间),由于ArrayList是数组实现,因此需要将插入位置之后的元素进行后移一个位置,腾出空间给新元素。因此这个方法多了一次数组复制的工作。
于此同时还有一个修改对象的方法,set(int index, E element),将制定位置的对象替换掉。
public E set(int index, E element) { RangeCheck(index); E oldValue = (E) elementData[index]; elementData[index] = element; return oldValue; } |
好奇他的范围检索只对“上界”检查,不对“下界”检查。
private void RangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException( "Index: "+index+", Size: "+size); } |
3)删除对象
删除指定位置的对象remove(int index)。
public E remove(int index) { RangeCheck(index);//检查范围 modCount++; E oldValue = (E) elementData[index]; int numMoved = size - index - 1;//计算移动几个元素 if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // gc回收 return oldValue; } |
remove(int index)和add(int index , E element)类似,需要通过数组的复制覆盖或腾出空间。
删除指定对象remove(E element)
public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } |
删除指定的对象,需要对删除对象是否为null区别对待。如果为null,则遍历数组中的元素,并比较是否为null(==null),如果为null则调用fastRemove删除。如果不为null,则遍历数组中的元素,并用equals比较是否相等,相等则调用fastRemove删除。
private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // Let gc do its work } |
fastRemove是简化的remove(int index),不需要进行范围检查。
还有removeRange(int fromIndex, int toIndex)意思差不多,不予赘述!
4)获取单个对象
get(int index),传入的参数的为数组元素的位置。
public E get(int index) { RangeCheck(index); return (E) elementData[index]; } |
获取指定对象的位置,indexOf(Object o)。
public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; } |
其实remove(Object o)可以改成:
public boolean remove(Object o) { int index = indexOf(o); if(index>=0){ fastRemove(index); return true; }else return false; } |
有了indexOf,contains(Object E)就简单了。
public boolean contains(Object o) { return indexOf(o) >= 0; } |
5)遍历
iterator有ArrayList的父类AbstractList实现,调用iterator会创建一个内部类Itr的实例(class Itr implements Iterator<E>)。主要关注hasNext、next方法。
public boolean hasNext() { return cursor != size(); } |
比较当前指向数组的位置是否和数组中已有元素的个数相等。
public E next() { checkForComodification(); try { E next = get(cursor); lastRet = cursor++; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } |
checkForComodification
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } |
调用next的时候要比较当前的modCount和创建iterator时的modCount是否相等。如果不相等,则说明对集合大小产生了影响,此时抛出ConcurrentModificationException。
相等则调用get方法,此时有可能抛出IndexOutOfBoundsException,在捕获IndexOutOfBoundException后,检查modCount(checkForComodification),如果modCount不相等,抛出ConcurrentModificationException,
如果相等则抛出NoSuchElementException。
LinkedList
LinkedList是基于双向链表机制,在LinkedList中,Entry类来代表集合中的元素。
private static class Entry<E> { E element; Entry<E> next; Entry<E> previous; Entry(E element, Entry<E> next, Entry<E> previous) { this.element = element; this.next = next; this.previous = previous; } } |
元素的值赋给element,previous指向前一个元素,next指向后一个元素,通过previous、next将多个独立的Entry串起来形成链表,因为它有两个方向的关联,所以称为双向链表。
1)创建LinkedList
private transient Entry<E> header = new Entry<E>(null, null, null); private transient int size = 0; /** * Constructs an empty list. */ public LinkedList() { header.next = header.previous = header; } |
创建一个Entry对象,将其previous、nest全部指向自己(header),形成一个闭环。
2)添加元素
add(E e)实际调用了addBefore。(addBefore(e, header);)
private Entry<E> addBefore(E e, Entry<E> entry) { Entry<E> newEntry = new Entry<E>(e, entry, entry.previous); newEntry.previous.next = newEntry; newEntry.next.previous = newEntry; size++; modCount++; return newEntry; } |
这个地方稍微有点绕,新建一个Entry对象,并将next指向header,previous指向header.previous,实际header.previous都是指向最后一个元素(为添加之前最后一个元素)。
将前一元素的next指向自己,前一元素为header.previous,即为添加前最后一个元素。
将自己的next元素,即header元素的previous指向自己,这样也始终保持了header.previous都是指向最后一个元素。
3)删除元素
remove(Object o)
public boolean remove(Object o) { if (o == null) { for (Entry<E> e = header.next; e != header; e = e.next) { if (e.element == null) { remove(e); return true; } } } else { for (Entry<E> e = header.next; e != header; e = e.next) { if (o.equals(e.element)) { remove(e); return true; } } } return false; } |
先遍历找到对应的Entry,然后在调用remove(Entry e)。
private E remove(Entry<E> e) { if (e == header) throw new NoSuchElementException(); E result = e.element; e.previous.next = e.next; e.next.previous = e.previous; e.next = e.previous = null; e.element = null; size--; modCount++; return result; } |
要删除指定的Entry e比较简单,让e的前一个元素的next指向e的next(e.previous.next = e.next),让e的后一个元素的previous指向e的previous(e.next.previous = e.previous)。
然后将e的element、next和previous置为null,此时gc应该有机会将删除的e消灭掉。
4)获取指定位置的元素
get(int index)有entry(int index)实现。
private Entry<E> entry(int index) { if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); Entry<E> e = header; if (index < (size >> 1)) { for (int i = 0; i <= index; i++) e = e.next; } else { for (int i = size; i > index; i--) e = e.previous; } return e; } |
这里有个小小的优化,如果index<size/2,则从前往后遍历,否则从后往前遍历链表。
5)遍历
iterator会创建一个AbstractList的内部类ListItr。
这里的类结构有必要说明一下。Iterator接口就定义了三个方法:hasNext、next、remove。
ListIterator接口,继承Iterator接口,又定义了:add、hasPrevious、previous、set、previousIndex、nextIndex等方法。
Ite作为AbstractList的内部类,实现了Iterator接口,主要用于ArrayList的遍历。
ListIte作为AbstractList的内部类,实现了ListIterator接口、同时继承了Ite类,主要用于LinkedList的遍历。
看看ListIte的previous方法:
public E previous() { checkForComodification(); try { int i = cursor - 1; E previous = get(i); lastRet = cursor = i; return previous; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } |
因此LinkedList可以向前、向后遍历。
6)其他一些方法
offer、peek、poll、pop、push。
offer和add类似(offerFirst、offerLast)
peek和get类似(peekFirst、peekLast)
poll和remove类似(pollFirst、pollLast)
pop等价removeFirst
push等价addFirst
Vector
Vector和ArrayList一样,也是基于数组的方式来实现的。
Vector是基于synchronized实现的线程安全的ArrayList,因此很多方法都和ArrayList的类似,只是添加了synchronized关键字。
除此之外,还有扩容方面稍有差别。
private void ensureCapacityHelper(int minCapacity) { int oldCapacity = elementData.length; if (minCapacity > oldCapacity) { Object[] oldData = elementData; int newCapacity = (capacityIncrement > 0) ? (oldCapacity + capacityIncrement) : (oldCapacity * 2); if (newCapacity < minCapacity) { newCapacity = minCapacity; } elementData = Arrays.copyOf(elementData, newCapacity); } } |
他这里直接就是翻倍,而ArrayList是1.5倍。为什么还搞个这样的区别呢?
Stack
Stack是继承Vector,实现了LIFO的栈操作。主要由push、pop、peek方法。
public E push(E item) { addElement(item); return item; } public synchronized E pop() { E obj; int len = size(); obj = peek(); removeElementAt(len - 1); return obj; } public synchronized E peek() { int len = size(); if (len == 0) throw new EmptyStackException(); return elementAt(len - 1); } |
ArrayList是基于数组的,get很快,但添加、删除操作需要移动元素,效率较低。且不是线程安全的。
LinkedList是基于双向链表的,添加、删除不需要移动元素,仅仅只要改变元素的previous、next,效率较高。get需要从前或从后开始遍历,效率较低。同样不是线程安全的。
Vector是线程安全的ArrayList实现,Stack在继承Vector的基础上实现了栈的操作。
通常我们会在外部对线程安全进行控制而选用ArrayList而非Vector。
1.1.2.Set
Set不支持放入重复的对象。Set的实现类主要包括:HashSet、TreeSet。
(先看Map部分)
HashSet
HashSet是基于HashMap来实现的。
TreeSet
TreeSet是基于TreeMap来实现的。
1.2.Map
Map存放Key-Value形式的键值对。Map的实现类主要包括:HashMap、TreeMap。
HashMap
1)创建
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); } |
默认loadFactor为0.75,threshold为12,创建一个大小为16的Entry对象数组。(大小为2的4次方)
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); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); } |
指定initialCapacity、loadFactor,capacity为大于initialCapacity的最小的2的n次方。capacity为Entry数组的大小。
2)添加
put(Object key , Object value)
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); 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);//key不存在 return null; } |
indexFor是根据capacity做取余操作。
static int indexFor(int h, int length) { return h & (length-1); } |
当key不存在的时候,也许会冲突,这个交由addEntry处理。
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); } |
addEntry不对冲突进行特殊处理,都会将新加的k-v作为一个Entry加入到每个列表的头部。
当size大于等于threshod时,需要进行扩容。扩容是一个比较繁琐的过程,需要对当前Entry对象数组中的元素重新hash,并填充数组,最后重新设置threshold值。
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[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity);//重新hash e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } } |
这块内容也比较复杂,有兴趣的同学最好跟下代码。
如果我们预知需要存入很多k-v,还调用默认无参构造map,那么就会面临很多次不必要的扩容操作。因此最好选用public HashMap(int initialCapacity)构造方法。(或者:public HashMap(int initialCapacity, float loadFactor) )
3)获取
get(Object key)
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; } |
对于key为null的情况,直接获取数组中的第一个Entry对象,并基于next属性进行遍历,寻找key为null的Entry,如果找到了则返回该Entry的value,没有找到返回null。
如果key不为null,则对key进行hash,然后取余获取其的存储位置。然后获取该位置上的Entry,并基于next属性进行遍历,寻找key为null的Entry,如果找到了则返回该Entry的value,没有找到返回null。
4)删除
remove(Object key)
具体的删除过程如下:
final Entry<K,V> removeEntryForKey(Object key) { int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; } |
remove和get类似,也是先找到key对应的存储位置,然后遍历找到key删除entry。这里使用单向链表解决冲突。
5)包含
containKey(Object key)
public boolean containsKey(Object key) { return getEntry(key) != null; } /** * Returns the entry associated with the specified key in the * HashMap. Returns null if the HashMap contains no mapping * for the key. */ final Entry<K,V> getEntry(Object key) { int hash = (key == null) ? 0 : hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; } |
getEntry和get方法类似,getEntry返回Entry对象,get返回Entry的value。
HashMap参考:
http://www.cnblogs.com/huangfox/archive/2012/07/06/2579614.html
TreeMap
TreeMap是支持排序的Map实现,可以自己指定Comparator参数。
1)创建
public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; } |
2)添加
put(Object key , Object value)
public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { // TBD: // 5045147: (coll) Adding null to an empty TreeSet should // throw NullPointerException // // compare(key, key); // type check root = new Entry<K,V>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths Comparator<? super K> cpr = comparator; if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } else { if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key; do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } Entry<K,V> e = new Entry<K,V>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e); size++; modCount++; return null; } |
先判断root是否为null,如果是则新建一个Entry对象,并赋值给root。
如果root不为null,首先判断是否指定了Comparator,如果已经传入,则基于红黑树的方式遍历,通过比较结果选择左树或者右树(左小右大)。
如果找到相等的key则直接替换vlaue,并返回结束put操作。
如果遍历结束都没有找到相等的key,则根据最后一次比较结果在遍历的最后一个节点添加一个左结点或右结点,依据依然是左小右大。
如果没有指定Comparator(Comparator == null),则需要根据key来创建一个比较器(Comparable<? super K> k = (Comparable<? super K>) key;),操作过程和上面相似。
2)获取
get(Object key)
final Entry<K,V> getEntry(Object key) { // Offload comparator-based version for sake of performance if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key; Entry<K,V> p = root; while (p != null) { int cmp = k.compareTo(p.key); if (cmp < 0) p = p.left; else if (cmp > 0) p = p.right; else return p; } return null; } |
3)删除
remove(Object key)
public V remove(Object key) { Entry<K,V> p = getEntry(key); if (p == null) return null; V oldValue = p.value; deleteEntry(p); return oldValue; } |
首先通过getEntry获取entry对象,如果不为null将此entry从红黑树上删除,并重新调整树的相关节点。
这个过程比较复杂,可以参考红黑树的相关知识。
java集合对象实现原理的更多相关文章
- 转载---Java集合对象的深度复制与普通复制
原博文:http://blog.csdn.net/qq_29329775/article/details/49516247 最近在做算法作业时出现了错误,原因是没有弄清楚java集合的深度复制和浅度复 ...
- Java集合 HashSet的原理及常用方法
目录 一. HashSet概述 二. HashSet构造 三. add方法 四. remove方法 五. 遍历 六. 合计合计 先看一下LinkedHashSet 在看一下TreeSet 七. 总结 ...
- 1.Java集合-HashMap实现原理及源码分析
哈希表(Hash Table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常 ...
- java集合对象区别二
集合包是Java中最常用的包,它最常用的有Collection和Map两个接口的实现类,Collection用于存放多个单对象,Map用于存放Key-Value形式的键值对. Collection中常 ...
- Java集合:ConcurrentHashMap原理分析
集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap).这篇文章主 ...
- Java集合 LinkedList的原理及使用
Java集合 LinkedList的原理及使用 LinkedList和ArrayList一样是集合List的实现类,虽然较之ArrayList,其使用场景并不多,但同样有用到的时候,那么接下来,我们来 ...
- java集合框架使用原理分析
集合是我们日常编程中可能用的很多的技术之一 使用频率极高 可能平时就会知道怎么去用 但是集合之间的关系与不同之处都不是很清楚 对它们的底层原理更甚 所以写词文章 让自己有一个更深的认识 集合是一个庞大 ...
- 6.Java集合-LinkedList实现原理及源码分析
Java中LinkedList的部分源码(本文针对1.7的源码) LinkedList的基本结构 jdk1.7之后,node节点取代了 entry ,带来的变化是,将1.6中的环形结构优化为了直线型链 ...
- 3.Java集合-HashSet实现原理及源码分析
一.HashSet概述: HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持,它不保证set的迭代顺序很久不变.此类允许使用null元素 二.HashSet的实现: 对于Ha ...
随机推荐
- GNS3 模拟免费ARP
R2 : conf t int f0/0 no shutdown ip add 192.168.1.254 255.255.255.0 end R1 : conf t int f0/0 no shut ...
- MySQL设置各类字符集
一.查看字符集编码: 登录mysql show variables like '%character%'; 二.修改编码: 编辑/etc/my.cnf ,设置后的配置文件如下: [root@node0 ...
- 洛谷 P2031 脑力达人之分割字串
题目传送门 解题思路: f[i]表示到第i位可获得的最大分割次数,对于每个f[i]都可由其符合条件的前缀转移过来,条件就是当前串除了前缀的剩余字符里有所给单词,然后一看,这不是在剩余字符里找有没有所给 ...
- web页面性能优化
web前端页面性能优化 网站的划分一般为二:前端和后台.我们可以理解成后台是用来实现网站的功能的,比如:实现用户注册,用户能够为文章发表评论等等.而前端呢? 其实应该是属于功能的表现.并且影响用户访问 ...
- 003.Oracle数据库 , 查询日期格式格式化
/*日期格式转换*/ SELECT TO_CHAR( OCCUR_DATE, 'yyyy/mm/dd hh24:mi:ss' ) FROM LM_FAULT WHERE ( ( OCCUR_DATE ...
- 3-安装RabbitMQ
1. 安装erlang 1.1 下载erlang http://www.erlang.org/downloads/20.1 yum -y install make ncurses-devel gcc ...
- IDEA maven 项目报警告解决(自己的maven配置记录)
IDEA maven 项目报警告解决 应该是JDK版本太低 虽然你装的高但是默认使用maven 默认的 这里要配一下JDK版本 理解不深入只为 自己记录使用 1 配置 仓库为阿里云 配置本地储存j ...
- 《机学一》特征工程1 ——文本处理:sklearn抽取、jieba中文分词、TF和IDF抽取
零.机器学习整个实现过程: 一.机器学习数据组成 特征值: 目标值: 二.特征工程和文本特征提取 1.概要: 1.特征工程是什么 2.特征工程的意义:直接影响预测结果 3.scikit-learn库 ...
- python中excel表格的读写
#!usr/bin/env python #-*- coding:utf-8 -*- import xlrd import xlwt from xlutils.copy import copy imp ...
- Windows按键消息—虚拟键码(转)
源地址:http://c.biancheng.net/cpp/html/1253.html 虚拟键码保存在WM_KEYDOWN.WM_KEYUP.WM_SYSKEYDOWN和WM_SYSKEYUP消息 ...