• JDK源码阅读—基本集合类(java.util)


    JDK源码阅读—基本集合类

    My Github
    很久以前就看过集合类,但是没有记笔记,本文就当是补的笔记吧,其中涉及java.util包中的集合类型,没有包括java.util.concurrent包。

    惯例的类图


    Vector
    Vector 实现可增长的对象数组。与数组一样,它包含可以使用整数索引进行访问的组件。Vector 的大小可以根据需要增大或缩小,以适应创建 Vector 后进行添加或移除项的操作。
    每个向量会试图通过维护 capacity 和 capacityIncrement 来优化存储管理。capacity始终至少应与向量的大小相等;这个值通常比后者大些,因为随着将组件添加到向量中,其存储将按 capacityIncrement的大小增加存储。应用程序可以在插入大量组件前增加向量的容量;这样就减少了增加的重分配的量。 它是线程安全的。
    下面是它的扩容相关的方法。

     1 protected Object[] elementData;
     2 protected int capacityIncrement; // 如果不设置,在扩容时翻倍
     3 public synchronized void ensureCapacity(int minCapacity) {
     4     if (minCapacity > 0) {
     5         modCount++;
     6         ensureCapacityHelper(minCapacity);
     7     }
     8 }
     9 
    10 private void ensureCapacityHelper(int minCapacity) {
    11     // overflow-conscious code
    12     if (minCapacity - elementData.length > 0)
    13         grow(minCapacity);
    14 }
    15 
    16 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    17 private void grow(int minCapacity) {
    18     // overflow-conscious code
    19     int oldCapacity = elementData.length;
    20     int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
    21     if (newCapacity - minCapacity < 0)
    22         newCapacity = minCapacity;
    23     if (newCapacity - MAX_ARRAY_SIZE > 0)
    24         newCapacity = hugeCapacity(minCapacity);
    25     elementData = Arrays.copyOf(elementData, newCapacity);
    26 }
    27 
    28 private static int hugeCapacity(int minCapacity) {
    29     if (minCapacity < 0) // overflow
    30         throw new OutOfMemoryError();
    31     return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE :
    32         MAX_ARRAY_SIZE;
    33 }

    那个求2的幂的算法在我的博客中也有提到过,详情见 concurrentHashMap中的2的n次幂上舍入方法


    ArrayList
    ArrayList是List的数组实现,使用数组作为元素存储的数据结构,使用的是一个Object[]。下面是ArrayList数组扩容的方法。

     1 private void grow(int minCapacity) {
     2   // 下面代码考虑了int的溢出
     3   int oldCapacity = elementData.length;
     4   int newCapacity = oldCapacity + (oldCapacity >> 1);
     5   if (newCapacity - minCapacity < 0)
     6   newCapacity = minCapacity;
     7   if (newCapacity - MAX_ARRAY_SIZE > 0)
     8     newCapacity = hugeCapacity(minCapacity);
     9   elementData = Arrays.copyOf(elementData, newCapacity);
    10 }
    11 
    12 private static int hugeCapacity(int minCapacity) {
    13   if (minCapacity < 0) // overflow
    14     throw new OutOfMemoryError();
    15   return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
    16 }


    另外,subList()方法提供的是ArrayList的一个视图,列表的修改冲突使用一个modCount计数器作为判断依据。Iterator中有一个modCount的快照,在修改数组的时候如果快照与modCount不相等说明列表被同时修改了,这时候操会抛出异常。
    ArrayList不是线程安全的。

    LinkedList

    LinkedList是列表的双向链表实现,在LinkedList中有first,last两个Node的引用,分别是链表的头和尾。
    Node的数据结构如下。

     1 private static class Node<E> {
     2     E item;
     3     Node<E> next;
     4     Node<E> prev;
     5     Node(Node<E> prev, E element, Node<E> next) {
     6         this.item = element;
     7         this.next = next;
     8         this.prev = prev;
     9     }
    10 }


    LinkedList因为使用的是链表的实现,所以不存在数组扩容的问题,其他的实现与ArrayList类似,只是换成了链表的相关操作。
    下面是获取对应index的节点的操作,还是做了一些优化的:

     1 /**
     2  * Returns the (non-null) Node at the specified element index.
     3  */
     4 Node<E> node(int index) {
     5     // assert isElementIndex(index);
     6     // 如果index小于size的二分之一则从头遍历,否则从链尾遍历
     7     if (index < (size >> 1)) {
     8         Node<E> x = first;
     9         for (int i = 0; i < index; i++)
    10             x = x.next;
    11         return x;
    12     } else {
    13         Node<E> x = last;
    14         for (int i = size - 1; i > index; i--)
    15             x = x.prev;
    16         return x;
    17     }
    18 }


    LinkedList也不是线程安全的。

    HashMap

    HashMap使用一个Node数组作为桶的数据结构。在有元素冲突发生的时候,使用链表和红黑树解决冲突。当一个桶中的元素小于TREEIFY_THRESHHOLD的时候,使用链表处理冲突,否则用将链表转换成红黑树。当桶中的元素个数小于UNTREEIFY_THRESHOLD的时候,将红黑树转换成链表。
    由于红黑树是一种部分平衡的二叉搜索树,这使得在一个桶中元素较多的时候HashMap避免遍历链表,还能有较好的查询性能。

      1 /**
      2  * 对于传入的size,计算能够容纳该size的2的最小次幂,这个神奇的算法在《算法心得》中有提到。
      3  */
      4 static final int tableSizeFor(int cap) {
      5     int n = cap - 1;
      6     n |= n >>> 1;
      7     n |= n >>> 2;
      8     n |= n >>> 4;
      9     n |= n >>> 8;
     10     n |= n >>> 16;
     11     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
     12 }
     13 /**
     14  * 元素插入
     15  */
     16 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
     17                    boolean evict) {
     18     Node<K,V>[] tab; Node<K,V> p; int n, i;
     19     if ((tab = table) == null || (n = tab.length) == 0)
     20         n = (tab = resize()).length;
     21     if ((p = tab[i = (n - 1) & hash]) == null)
     22         tab[i] = newNode(hash, key, value, null);
     23     else {
     24         Node<K,V> e; K k;
     25         if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
     26             e = p;
     27         else if (p instanceof TreeNode) // 如果是红黑树节点
     28             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
     29         else {
     30             for (int binCount = 0; ; ++binCount) {
     31                 if ((e = p.next) == null) { // 没有已存在的相等节点
     32                     p.next = newNode(hash, key, value, null);
     33                     // 是否超过转化成红黑树的阀值
     34                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
     35                         treeifyBin(tab, hash);
     36                     break;
     37                 }
     38                 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 找到想等节点
     39                     break;
     40                 p = e;
     41             }
     42         }
     43         if (e != null) { // existing mapping for key
     44             V oldValue = e.value;
     45             if (!onlyIfAbsent || oldValue == null)
     46                 e.value = value;
     47             afterNodeAccess(e);
     48             return oldValue;
     49         }
     50     }
     51     ++modCount;
     52     if (++size > threshold)
     53         resize();
     54     afterNodeInsertion(evict);
     55     return null;
     56   }
     57 
     58 /**
     59  * 扩容Map
     60  */
     61 final Node<K,V>[] resize() {
     62     Node<K,V>[] oldTab = table;
     63     int oldCap = (oldTab == null) ? 0 : oldTab.length;
     64     int oldThr = threshold;
     65     int newCap, newThr = 0;
     66     if (oldCap > 0) {
     67         if (oldCap >= MAXIMUM_CAPACITY) {
     68             threshold = Integer.MAX_VALUE;
     69             return oldTab;
     70         } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
     71                   oldCap >= DEFAULT_INITIAL_CAPACITY)
     72             newThr = oldThr << 1; // 原来的两倍
     73     } else if (oldThr > 0) // initial capacity was placed in threshold
     74         newCap = oldThr;
     75     else {               // zero initial threshold signifies using defaults
     76         newCap = DEFAULT_INITIAL_CAPACITY;
     77         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
     78     }
     79     if (newThr == 0) {
     80         float ft = (float)newCap * loadFactor;
     81         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
     82                  (int)ft : Integer.MAX_VALUE);
     83     }
     84     threshold = newThr;
     85     @SuppressWarnings({"rawtypes","unchecked"})
     86     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
     87     table = newTab;
     88     if (oldTab != null) {
     89         // rehash
     90         for (int j = 0; j < oldCap; ++j) {
     91             Node<K,V> e;
     92             if ((e = oldTab[j]) != null) {
     93                 oldTab[j] = null;
     94                 if (e.next == null) // 如果桶中只有一个元素
     95                     newTab[e.hash & (newCap - 1)] = e;
     96                 else if (e instanceof TreeNode) // 处理红黑树
     97                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
     98                 else { // preserve order
     99                     Node<K,V> loHead = null, loTail = null;
    100                     Node<K,V> hiHead = null, hiTail = null;
    101                     Node<K,V> next;
    102                     // 这一段处理比较巧妙,用e.hash & oldCap根据oldCap的为1的那一位是否是1来判断该元素是在新的桶数组的前一半还是后一半
    103                     do {
    104                         next = e.next;
    105                         if ((e.hash & oldCap) == 0) {
    106                             if (loTail == null)
    107                                 loHead = e;
    108                             else
    109                                 loTail.next = e;
    110                                 loTail = e;
    111                             }
    112                         else {
    113                             if (hiTail == null)
    114                                 hiHead = e;
    115                             else
    116                                 hiTail.next = e;
    117                                 hiTail = e;
    118                             }
    119                     } while ((e = next) != null);
    120                     // lowHalf 在新桶数组的前一半
    121                     if (loTail != null) {
    122                         loTail.next = null;
    123                         newTab[j] = loHead;
    124                     }
    125                     // highHalf 在新桶数组的后一半
    126                     if (hiTail != null) {
    127                         hiTail.next = null;
    128                         newTab[j + oldCap] = hiHead;
    129                     }
    130                 }
    131             }
    132         }
    133     }
    134     return newTab;
    135 }


    另外,在HashMap中的KeySet, Values, EntrySet都只是一个view,遍历通过相应的Iterator进行。HashMap不是线程安全的

    HashTable

    HashTable是线程安全的Map实现,使用方法级的synchronized保证线程安全。
    默认的初始化大小是11,size并不是2的幂,与HashMap 的不一样,它也是使用链表法处理冲突。下面是几个关键的操作方法,足以让我们了解HashTable的数据结构操作。

     1 private transient Entry<?,?>[] table;
     2 
     3 protected void rehash() {
     4     int oldCapacity = table.length;
     5     Entry<?,?>[] oldMap = table;
     6 
     7     // 考虑了溢出的情况
     8     // 新的size是两倍+1,跟HashMap的不一样
     9     int newCapacity = (oldCapacity << 1) + 1;
    10     if (newCapacity - MAX_ARRAY_SIZE > 0) {
    11         if (oldCapacity == MAX_ARRAY_SIZE)
    12             // Keep running with MAX_ARRAY_SIZE buckets
    13             return;
    14         newCapacity = MAX_ARRAY_SIZE;
    15     }
    16     Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
    17 
    18     modCount++;
    19     threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    20     table = newMap;
    21 
    22     for (int i = oldCapacity ; i-- > 0 ;) {
    23         for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
    24             Entry<K,V> e = old;
    25             old = old.next;
    26             int index = (e.hash & 0x7FFFFFFF) % newCapacity;
    27             e.next = (Entry<K,V>)newMap[index];
    28             newMap[index] = e;
    29         }
    30     }
    31 }
    32 
    33 private void addEntry(int hash, K key, V value, int index) {
    34     modCount++;
    35     Entry<?,?> tab[] = table;
    36     if (count >= threshold) {
    37         // Rehash the table if the threshold is exceeded
    38         rehash();
    39         tab = table;
    40         hash = key.hashCode();
    41         // 这里用了我们常见的取模操作
    42         index = (hash & 0x7FFFFFFF) % tab.length;
    43     }
    44     // Creates the new entry.
    45     @SuppressWarnings("unchecked")
    46     Entry<K,V> e = (Entry<K,V>) tab[index];
    47     tab[index] = new Entry<>(hash, key, value, e);
    48     count++;
    49 }
    50 
    51 @Override
    52 public synchronized boolean remove(Object key, Object value) {
    53     Objects.requireNonNull(value);
    54     Entry<?,?> tab[] = table;
    55     int hash = key.hashCode();
    56     int index = (hash & 0x7FFFFFFF) % tab.length;
    57     @SuppressWarnings("unchecked")
    58     Entry<K,V> e = (Entry<K,V>)tab[index];
    59     for (Entry<K,V> prev = null; e != null; prev = e, e = e.next) {
    60         if ((e.hash == hash) && e.key.equals(key) && e.value.equals(value)) {
    61             modCount++;
    62             if (prev != null) {
    63                 prev.next = e.next;
    64             } else {
    65                 tab[index] = e.next;
    66             }
    67             count--;
    68             e.value = null;
    69             return true;
    70         }
    71     }
    72     return false;
    73 }


    ArrayDeque

    双端队列Deque接口的数组实现,非线程安全,使用数组作为存储数据结构,可以在使用的时候自动扩容。

     1 transient Object[] elements; // non-private to simplify nested class access
     2 // 队列头索引
     3 transient int head;
     4 // 队列尾索引
     5 transient int tail;
     6 /**
     7   * 又是这个神奇的算法
     8   * Allocates empty array to hold the given number of elements.
     9   */
    10 private void allocateElements(int numElements) {
    11     int initialCapacity = MIN_INITIAL_CAPACITY;
    12     // Find the best power of two to hold elements.
    13     // Tests "<=" because arrays aren't kept full.
    14     if (numElements >= initialCapacity) {
    15         initialCapacity = numElements;
    16         initialCapacity |= (initialCapacity >>>  1);
    17         initialCapacity |= (initialCapacity >>>  2);
    18         initialCapacity |= (initialCapacity >>>  4);
    19         initialCapacity |= (initialCapacity >>>  8);
    20         initialCapacity |= (initialCapacity >>> 16);
    21         initialCapacity++;
    22         if (initialCapacity < 0)   // Too many elements, must back off
    23             initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    24     }
    25     elements = new Object[initialCapacity];
    26 }
    27 
    28 private void doubleCapacity() {
    29     assert head == tail;
    30     int p = head;
    31     int n = elements.length;
    32     int r = n - p; // number of elements to the right of p
    33     int newCapacity = n << 1;
    34     if (newCapacity < 0)
    35         throw new IllegalStateException("Sorry, deque too big");
    36     Object[] a = new Object[newCapacity];
    37     System.arraycopy(elements, p, a, 0, r);
    38     System.arraycopy(elements, 0, a, r, p);
    39     elements = a;
    40     head = 0;
    41     tail = n;
    42 }


    对于ArrayDeque的操作,就要看内部类DeqIterator的实现了,以下是部分代码。

     1 private class DeqIterator implements Iterator<E> {
     2     // 头尾索引
     3     private int cursor = head;
     4     private int fence = tail;
     5 
     6     // next方法返回的位置, 如果有元素被删除,那么重置为-1
     7     private int lastRet = -1;
     8 
     9     public boolean hasNext() {
    10         return cursor != fence;
    11     }
    12 
    13     public E next() {
    14         if (cursor == fence)
    15             throw new NoSuchElementException();
    16         @SuppressWarnings("unchecked")
    17         E result = (E) elements[cursor];
    18         if (tail != fence || result == null)
    19             throw new ConcurrentModificationException();
    20         lastRet = cursor;
    21         cursor = (cursor + 1) & (elements.length - 1); // 相当于取模
    22         return result;
    23     }
    24 
    25     public void remove() {
    26         if (lastRet < 0)
    27             throw new IllegalStateException();
    28         if (delete(lastRet)) { // if left-shifted, undo increment in next()
    29             cursor = (cursor - 1) & (elements.length - 1);
    30             fence = tail;
    31         }
    32         lastRet = -1;
    33     }
    34     /**
    35      * 这个方法中注意为尽量少移动元素而进行的优化
    36      */
    37     private boolean delete(int i) {
    38         checkInvariants();
    39         final Object[] elements = this.elements;
    40         final int mask = elements.length - 1;
    41         final int h = head;
    42         final int t = tail;
    43         final int front = (i - h) & mask; // i到head的距离
    44         final int back  = (t - i) & mask; // i到tail的距离
    45         // Invariant: head <= i < tail mod circularity
    46         if (front >= ((t - h) & mask))
    47             throw new ConcurrentModificationException();
    48         // 为尽量少移动元素优化
    49         if (front < back) {
    50             // 离head比较近
    51             if (h <= i) {
    52                 // 正常情况
    53                 System.arraycopy(elements, h, elements, h + 1, front);
    54             } else { // Wrap around
    55                 // oioootail****heado这种情况,o表示有元素*表示没有元素
    56                 System.arraycopy(elements, 0, elements, 1, i);
    57                 elements[0] = elements[mask];
    58                 System.arraycopy(elements, h, elements, h + 1, mask - h);
    59             }
    60             elements[h] = null;
    61             head = (h + 1) & mask;
    62             return false;
    63         } else {
    64             // 离head比较远
    65             if (i < t) { // Copy the null tail as well
    66                 System.arraycopy(elements, i + 1, elements, i, back);
    67                 tail = t - 1;
    68             } else { // Wrap around
    69                 // otail****headooio这种情况,o表示有元素*表示没有元素
    70                 System.arraycopy(elements, i + 1, elements, i, mask - i);
    71                 elements[mask] = elements[0];
    72                 System.arraycopy(elements, 1, elements, 0, t);
    73                 tail = (t - 1) & mask;
    74             }
    75             return true;
    76         }
    77     }


    LinkedHashMap

    在Map的实现中,HashMap是无序的。而LinkedHashMap则是有序Map的一种,这个顺序可以是访问顺序或者插入顺序,这个根据构造函数的参数而定,默认是插入顺序。
    LinkedHashMap维护了一个Entry的双向链表,通过重写父类HashMap中的操作后处理方法和TreeNode操作方法来维护链表。

     1 Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
     2     // ...
     3     transferLinks(q, t);
     4     return t;
     5 }
     6 
     7 TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
     8     // ...
     9     linkNodeLast(p);
    10     return p;
    11 }
    12 
    13 TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    14     // ...
    15     transferLinks(q, t);
    16     return t;
    17 }
    18 
    19 void afterNodeRemoval(Node<K,V> e) { // unlink
    20     LinkedHashMap.Entry<K,V> p =
    21         (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    22     p.before = p.after = null;
    23     if (b == null)
    24         head = a;
    25     else
    26         b.after = a;
    27     if (a == null)
    28         tail = b;
    29     else
    30         a.before = b;
    31 }
    32 
    33 void afterNodeInsertion(boolean evict) { // possibly remove eldest
    34     LinkedHashMap.Entry<K,V> first;
    35     if (evict && (first = head) != null && removeEldestEntry(first)) {
    36         K key = first.key;
    37         removeNode(hash(key), key, null, false, true);
    38     }
    39 }
    40 
    41 void afterNodeAccess(Node<K,V> e) { // move node to last
    42     LinkedHashMap.Entry<K,V> last;
    43     if (accessOrder && (last = tail) != e) {
    44         LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    45         p.after = null;
    46         if (b == null)
    47             head = a;
    48         else
    49             b.after = a;
    50         if (a != null)
    51             a.before = b;
    52         else
    53             last = b;
    54         if (last == null)
    55             head = p;
    56         else {
    57             p.before = last;
    58             last.after = p;
    59         }
    60         tail = p;
    61         ++modCount;
    62     }
    63 }


    TreeMap

    说到有序的Map就不能不提TreeMap了。它是基于红黑树(Red-Black tree)的 NavigableMap 实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。此实现为 containsKey、get、put 和 remove 操作提供受保证的 log(n) 时间开销。这些算法是 Cormen、Leiserson 和 Rivest 的 Introduction to Algorithms 中的算法的改编。
    TreeMap的操作更多是跟红黑树的实现相关,在这里我就不仔细说了(其实我也说不清楚哈哈),详情可以参考红黑树的wiki百科Red Black Tree

    WeakHashMap

    以弱键实现的基于哈希表的Map。在WeakHashMap中,当某个键不再正常使用时,将自动移除其条目。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除,因此,该类的行为与其他的Map实现有所不同。
    它将Key关联到一个弱引用,而元素的KV对象Entry继承自WeakReference,并绑定了一个队列,弱引用不影响GC对于Key的回收,当Key被回收以后,Entry会被添加到ReferenceQueue中。
    WakHashMap适合内存敏感的应用场景。

     1 /**
     2   * Reference queue for cleared WeakEntries
     3   */
     4 private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
     5 
     6 private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
     7     V value;
     8     final int hash;
     9     Entry<K,V> next;
    10 
    11     /**
    12      * Creates new entry.
    13      */
    14     Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) {
    15         super(key, queue);  // 调用WeakReference构造函数
    16         this.value = value;
    17         this.hash  = hash;
    18         this.next  = next;
    19     }
    20 }
    21 
    22 /**
    23  * 这个方法将队列中的Node清除
    24  */
    25 private void expungeStaleEntries() {
    26     for (Object x; (x = queue.poll()) != null; ) {
    27         synchronized (queue) {
    28             @SuppressWarnings("unchecked")
    29                 Entry<K,V> e = (Entry<K,V>) x;
    30             int i = indexFor(e.hash, table.length);
    31 
    32             Entry<K,V> prev = table[i];
    33             Entry<K,V> p = prev;
    34             while (p != null) {
    35                 Entry<K,V> next = p.next;
    36                 if (p == e) {
    37                     if (prev == e)
    38                         table[i] = next;
    39                     else
    40                         prev.next = next;
    41                     // Must not null out e.next;
    42                     // stale entries may be in use by a HashIterator
    43                     e.value = null; // Help GC
    44                     size--;
    45                     break;
    46                 }
    47                 prev = p;
    48                 p = next;
    49             }
    50         }
    51     }
    52 }

    以上,接下来应该就到Concurrent包里面的集合类了吧。

  • 相关阅读:
    Java语法总结 线程
    Java多线程编程总结
    eclipse插件开发
    Java私塾的一些基础练习题(一)
    反射练习
    内部类实现动态链表(增,删,查,打印)
    oracle 存储过程第四天
    java 面向对象个人理解
    jsp的flash小例子
    oralcle 存储过程批处理
  • 原文地址:https://www.cnblogs.com/katsura/p/6464102.html
Copyright © 2020-2023  润新知