JAVA基础复习(二)

    本着重新学习(看到什么复习什么)的原则,这一篇讲的是JAVA的集合类。看了诸位大神的解释后详细的查了一些东西,记录下来,也感谢各位在网络上的分享!!!

    针对集合类,其实平时接触最多的应该就是根据不同的应用场景使用的各种各样功能强大的List或者Map,今天来总结一下,深入调查一下其中的区别和关联。这张图是很详尽的一张关系网络,可以先从这张图入手。

    我首先从左上角的Iterator接口开始。

    1.Iterator接口和Iterable接口:

    说到Iterator就不得不提到Iterable接口,从名称上就知道这两者有着什么猫腻,able表示能力,那么实现Iterable接口就拥有了某种能力,但是能力是什么实质上就在于Iterable接口内返回了一个Iterator接口的实例。就相当于是一个赋予能力,一个规范能力能做的事情。我们再回溯一下,那为什么一定要有两个不同的接口去做所谓的两件事呢?为什么不是在赋予能力的同时规范能力,只继承或实现一个接口一举两得呢?实质上是与地址相关。(此时我突然发现了一个问题,明天查清楚。。。mark一下。。。)举例说明,数组是顺序的存储结构,我们可以用下标的方式访问数组中的任意元素,但是在增加删除操作的时候,需要移动大量的元素,而对于链表而言,链表是链式的存储结构,由于元素在链表中不是顺序存储,而是通过指向该元素的指针将元素关联起来,那么在进行增加删除操作的时候只需要修改对应元素的指针就好,但是如果我们需要找到链表中的某一个元素,便只能从第一个元素开始。而Iterator接口中的三个方法(hasNext(),next(),remove())正是基于当前位置的,当我们稍后看一下当前位置的具体概念会发现当前位置都是在Iterator接口的实现中定义的,而如果直接继承Iterator接口,必然会带着位置信息进行每一步操作,单线程考虑好像没什么问题,该访问访问,从第一个元素开始就成了,但是如果两个或多个线程同时访问,并且此时线程间相差着几个位置,查询的时候应该怎么办呢?所以结论是不能直接继承Iterator接口,所以才有了一步又一步的继承与实现,在真正处理数据结构关系的时候处理位置的问题。关于当前位置的理解我是这样认为的,现在只看一条之路(Iterator<——Collection<——List<——AbstractList<——ArrayList),跳过中间的所有实现路径,我们看一下ArrayList是怎么做到当前位置的指定的。

     /**
     * An optimized version of AbstractList.Itr
     */
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        //我们暂且省去实现的其他方法
        ......
    }

    我们可以看到在ArrayList中定义了一个名为Itr的内部类实现了Iterator接口,并且包含三个变量,cursor(代表了返回的下一个元素的下标),lastRet(代表了返回最后一个元素的下标,如果没有最后一个元素,则返回-1)和expectModCount(期望的ArrayList修改的次数),而每一个接口方法的实现都是围绕着几个带着位置信息的变量进行的更多的操作。同理,LinkedList中有名为ListItr的内部类实现ListIterator接口,而ListIterator接口继承自Iterator接口。

    2.Collection接口:

    Collection接口拥有着诸多子接口的,并为诸多子接口提供常用的接口方法,当然,也包括取得Iterator接口对象用于集合的输出。常用的方法(size(),isEmpty(),contains(),add(),remove(),toArray()等)都一直被继承其子接口的类提供着实现。具体的区别等到描述到该集合类再进行总结。

    3.Queue接口:

    提到Queue接口,先来复习一下数据结构中的队列。队列是严谨的线性表结构,仅允许先进先出(FIFO),相当于是只有一个入口就是队尾,出口也只有一个便是队首。可以分为两种,链式队列和顺序队列,顺序队列会先申请好空间,而后进行操作,使用期间不会释放,并且是固定长度,所以会有空间浪费的问题,而链式队列会在操作中申请和释放新的结点,并且不会固定长度,所以不会有空间浪费的问题,但会有一些操作时间上的消耗。这就是数据结构中的队列一些概念。而在Queue接口的定义中并不总是FIFO的方式排序元素,即方法的定义是相同的但具体实现在各种不同的队列排序策略中是不同的。如优先级队列或者LIFO队列,虽然实现不同,但是必须指定元素顺序的定义。

    进去看过源码会发现,感觉Queue接口的定义中是两套东西啊,(add(),remove(),element())和(offer(),poll(),peek())两组,我摘抄一点源码和注释出来比对一下就会知道原因了。

    /**
     ......
     * @return {@code true} (as specified by {@link Collection#add})
     * @throws IllegalStateException if the element cannot be added at this
     *         time due to capacity restrictions
     ......
     */
    boolean add(E e);

    /**
     ......
     * @return {@code true} if the element was added to this queue, else
     *         {@code false}
     ......
     */
    boolean offer(E e);

    /**
     ......
     * @return the head of this queue
     * @throws NoSuchElementException if this queue is empty
     */
    E remove();

    /**
     ......
     * @return the head of this queue, or {@code null} if this queue is empty
     */
    E poll();

    /**
     ......
     * @return the head of this queue
     * @throws NoSuchElementException if this queue is empty
     */
    E element();

    /**
     ......
     * @return the head of this queue, or {@code null} if this queue is empty
     */
    E peek();

    可以看到每一组相对的方法,实质上做的是一样的事情(咳咳,我没摘抄具体的方法定义原因,想了解的可以直接看源码),但是返回值上都有区别,前者会抛出异常等待处理,后者会返回false或者null。

    从源码正上方的@see注解可以看到跳转,发现会有分别继承AbstractQueue类和实现BlockingQueue接口的。

    AbstractQueue是一个抽象类,继承了AbstractCollection抽象类和实现了Queue接口,在其中提供了基于offer,poll和peek方法的add,remove和element方法,并且提供了clear和addAll方法,从注释说明看来“拓展该类的队列必须最少实现一个不允许插入null值的offer()方法,如果不能满足条件,请考虑为AbstractCollection创建子类”,故这个类中的实现适用于不允许包含null值的Queue。

    BlockingQueue接口是继承了Queue接口的,从注释说明看来“BlockingQueue接口主要用于生产者-消费者队列,但还支持Collection接口,其是线程安全的。”也可以得知,BlockingQueue的方法有四种形式,不同的方式处理结果也不同,从方法的定义和处理方法上也可以明确BlockingQueue是一种带有阻塞机制的队列。

操作抛出异常特殊值阻塞超时

插入

add(e)offer(e)put(e)offer(e, time, unit)
删除remove()poll()take()poll(time, unit)
检查element()peek()not applicablenot applicable

    前两列的区别与Queue接口内的对应方法返回定义相同,后两种的则说明了当队列没有空间时,队列会一直阻塞线程直到拿到数据或者中断,也就是阻塞列;当队列没有空间时,队列会等待一定时间,如果超出一定的时间会中断的超时列。

    /**
     * Inserts the specified element into this queue, waiting if necessary
     * for space to become available.
     ......
     */
    void put(E e) throws InterruptedException;

    /**
     * Inserts the specified element into this queue, waiting up to the
     * specified wait time if necessary for space to become available.
     ......
     */
    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    图中的Deque接口是继承了Queue接口,根据doc可知它实现的是“双端队列”,是一种具有队列和栈两种性质的数据结构。它还可以将输入或输出端口进行限制,从而形成一端输出(输入)受限的双端队列(一个端点允许插入和删除,另一个端点只允许插入(输出)的双端队列)。不过据度娘说“尽管双端队列看起来似乎比栈和队列更灵活,但实际上在应用程序中远不及栈和队列有用。”。

    关于Queue其中更具体的分类如是否有界,单双端,是否阻塞,是否存在优先级等又分别形成了更多的实现类,根据其依据的不同的数据结构,达成不同的功能实现,详尽的分类日后详细学习。(mark*2。。。)

    4.List接口:

    List接口继承了Collection接口,并且在List接口中存在(ListIterator<E> listIterator();正如上文中所述,ListIterator接口是继承Iterator接口的)返回一个List的迭代器。它是一个有序的集合,可以准确的通过位置索引去控制每一个元素在列表中的位置,并且可以达成列表内元素搜索。与Collection不同的是,List是允许重复值的,并且在一些实现类中也同样允许空元素(null)。List集合的三个实现类包括ArrayList,LinkedList和Vector,三个子类还分别实现了RandomAccess接口(ArrayList和Vector支持随机访问)和Deque接口(LinkedList),这表示他们在某些地方可以进行分类。

类别底层数据结构查询速度增删速度线程安全效率
ArrayList数组
Vector数组
LinkedList链表

    数组的查询由于有索引的原因,比只能从首元素逐个读取下一个元素地址的链表查询速度快一些。但是由于链表的每个元素中都存有下一个元素的地址,故元素的增加删除又比之数组快一些(数组则需要在所选元素索引位置上将他的下一个元素进行左移或右移完成删除和增加的操作)。

    在ArrayList接口的源码中可以发现,实质上我们平时使用的是不定长的ArrayList(基于动态数组,但是如果已知ArrayList能达到的容量,在初始化时指定长度应该会节省开销)。因为它定义了一个DEFAULT_CAPACITY,即如果我们不给定长度,它会给定一个默认长度10,如果我们给定长度,那么它将返回一个长度为给定值的对象。但是实际使用中我们通常不会只给定10个元素,这就是ArrayList的扩容。通过在ArrayList中使用add方法添加超过10个元素可以发现,它会先使用ensureCapacityInternal方法判定数组此时是否是空数组,如果是便会赋予一个最小容量DEFAULTCAPACITY_EMPTY_ELEMENTDATA,否则便会赋予一个DEFAULT_CAPACITY和(size+1)返回的最大值作为最小所需容量(minCapacity)。此后ensureCapacityInternal方法内调用ensureExplicitCapacity方法来确认扩容大小,如果最小扩容所需容量(minCapacity)大于数组当前的长度,那么就需要扩容。扩容调用grow方法通过右移一位的方式进行扩容1.5倍(int newCapacity = oldCapacity + (oldCapacity >> 1); >>是带符号右移位运算符,>>1是右移一位,也就相当于是除以2,便相当于新的容量会在原来容量的基础上加一半的容量,也就是1.5倍)。相关方法源码如下:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

    相比较而言,同样基于动态数组的Vector在扩容时直接增加两倍。所以在节约空间方面使用ArrayList更加有利。

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    相比较于ArrayList的风光无限,Vector看起来比较没落。线程安全的特性反而让Vector降低了效率,并且由于双倍扩容,所以在数据量相当大的时候会消耗更多的内存空间和资源。Vector接口在线程方面通过synchronized关键字进行同步,使得在Vector接口内是十分安全的,但是在某些场景下还是会出现同样的问题。所以综合考虑使用ArrayList接口更佳。

    LinkedList接口维护的是一个双向链表(在查阅过程中发现,在jdk1.7开始,便从双向循环链表改为双向链表,这个需要注意一下),即它的每一个节点都有两个指针分别称为前驱和后驱。而双向循环链表和双向链表的区别就在于头结点的前驱指向尾结点,同理尾结点的后驱也指向头结点,形成一个环状。举两个例子说明下。

    LinkedList接口的头尾:在双向链表中会有头结点和尾结点的区别,而对于双向循环链表只需要一个Header,并且在初始化时将Header的前驱和后驱均指向Header本身,形成环。

    LinkedList接口的插入:双向链表第一次插入时需要判断第一个元素是否是null,也就是说该列表是否为空。双向循环链表只需要分配内存,而后节点指针分别指向对应的前驱和后驱就好,如果为空,则指向自己,即保证为环。

    LinkedList接口的这个变化是要注意的,感觉在面试里被考到的几率还挺大的。

    5.Set接口:

    Set接口一直以其不支持重复值而闻名,所以很多时候在一些数组去重的场景都会出现Set的身影。根据类内说明可知,Set接口是数据抽象建模,接口特性是不允许出现重复元素,也就代表着最多只会有一个值为null的元素。但是我没有从中发现另一个常见概念,即“List是有序的,Set是无序的”。实际上这里所谓的有序和无序指是否按照添加元素的顺序存储,如果有序,那么输出的时候也会是按照添加元素的顺序输出,而不是我们意义上的有“顺序”。但是Set接口的每一个实现类内部可能都有自己的排序方式,表现上是没有按照添加元素顺序输出,即无序,但是按照某些实现类内的排序方式来说,可能Set都是有序的(如TreeSet),我简单测试了一些数据,每次HashSet接口的输出都是相同的,所以这个点我还有一些疑问。

package com.day_2.excercise_1;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;

public class TrySet {
	public static void main(String[] args) {
		Set<String> set = new HashSet<String>();
		set.add("test1");
		set.add("test2");
		...
		set.add("test24");
		set.add("test25");
		set.forEach(e-> System.out.print(e+"->"));
		System.out.println();
		
		Set<String> set2 = new LinkedHashSet<String>();
		set2.add("test1");
		set2.add("test2");
		...
		set2.add("test24");
		set2.add("test25");
		set2.forEach(e-> System.out.print(e+"->"));
		System.out.println();
		
		Set<String> set3 = new TreeSet<String>();
		set3.add("test1");
		set3.add("test2");
		...
		set3.add("test24");
		set3.add("test25");
		set3.forEach(e-> System.out.print(e+"->"));
		
	}
}

    根据HashSet源码可见,初始化了一个用transient修饰的HashMap对象,并且HashMap的所有构造方法都是在构造HashMap,所以HashSet是基于HashMap实现的。而HashMap是key,value形式,但是我们在使用HashSet内方法时并没有添加过另一个值的原因是在HashSet中定义了一个虚拟值PRESENT作为value。看到这里我们再回头看HashSet的唯一,无序等特点可以发现均来自HashMap的key值唯一和无序存储特性(由于调用add方法也是调用HashMap的put方法,所以可见HashSet是按元素的哈希值存储,所以是无序的)。

    // HashSet add()
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

    // HashMap put()
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

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

    关于transient修饰符的作用,明明已经实现了Serializable接口,又为何要使用transient修饰符,并且在类内声明了两个私有方法writeObject和readObject呢?通过打断点测试可以发现JDK在进行对象序列化(ObjectOutputStream)和反序列化(ObjectInputStream)会判定是否自己声明了同名方法并使用同名方法。这里mark一篇文章(https://my.oschina.net/demons99/blog/1929309),文章中的举例还是很清晰的,我这里百度翻译了一下,如下:

    For example, consider the case of a hash table. The physical representation is a sequence of hash buckets containing key-value entries. The bucket that an entry resides in is a function of the hash code of its key, which is not, in general, guaranteed to be the same from JVM implementation to JVM implementation. In fact, it isn’t even guaranteed to be the same from run to run. Therefore, accepting the default serialized form for a hash table would constitute a serious bug. Serializing and deserializing the hash table could yield an object whose invariants were seriously corrupt.

    “例如,考虑哈希表的情况。物理表示是包含键值项的哈希桶序列。条目所在的bucket是其键的散列代码的函数,一般来说,不能保证从JVM实现到JVM实现是相同的。事实上,从一次运行到另一次运行,它甚至不能保证是相同的。因此,接受哈希表的默认序列化格式将构成严重的错误。序列化和反序列化哈希表可能会产生不变量严重损坏的对象。”

    大致了解了一些,因为无法保证JVM的实现相同,所以不能保证每一次或通过序列化后在反序列化时能得到原有的结果。故不使用原有的接口方法而是自己定义方法。

    TreeSet是一个有序集合类,同HashSet相同TreeSet也是基于Map,不过TreeSet基于的是TreeMap。由于TreeMap底层数据结构是红黑树,所以基于TreeMap的TreeSet也相当于是基于红黑树,而红黑树是一个自平衡二叉查找树,这保证了TreeSet的有序性。此时需要注意,TreeSet不能传入null,否则会抛出NullPointerException 异常。(但是我也看到了一些文章对此有别的见解,mark一下。。。)

    /**
     * Adds the specified element to this set if it is not already present.
     * More formally, adds the specified element {@code e} to this set if
     * the set contains no element {@code e2} such that
     * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
     * If this set already contains the element, the call leaves the set
     * unchanged and returns {@code false}.
     *
     * @param e element to be added to this set
     * @return {@code true} if this set did not already contain the specified
     *         element
     * @throws ClassCastException if the specified object cannot be compared
     *         with the elements currently in this set
     * @throws NullPointerException if the specified element is null
     *         and this set uses natural ordering, or its comparator
     *         does not permit null elements
     */
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }

    相比较而言,HashSet由于基于哈希表,所以速度快,而TreeSet有序,各有特长。

    6.Map接口:

    Map是替代了Dictionary的一个接口,存储基于<key,value>形式的映射数据。常用实现类有HashMap,TreeMap,Hashtable,SortedMap。

    HashMap是一个根据键的哈希值存储数据的Map,其每一个键值对也叫做Entry。底层实现是一种“数组+链表”的数据结构(也可称为哈希桶(又被称为开链法,开散列法。哈希桶的数组中存的是指针,并且数组中每一个位置都存着一段链表,当插入数据时不用处理哈希冲突,直接将数据链接在链表即可。参考链接:https://blog.youkuaiyun.com/qq9116136/article/details/80327841)),并且当链表的长度大于8时会转换为红黑树。HashMap集合了数组(查询速率快,插入删除慢)和链表(查询速率慢,插入删除快)的特性,使得操作速度均很快。同之前所讲相同,HashMap也有默认大小,并且对于数据结构转换也有对应的默认值定义。有默认值就会有扩容(resize())。在resize方法中定义了扩容大小为原来的两倍。(想要具体了解resize方法可以查看参考链接:https://www.cnblogs.com/winterfells/p/8876888.html

    //默认大小
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    //最大容量(如果传入容量过大将被替换为该值)
    static final int MAXIMUM_CAPACITY = 1 << 30;

    //链表转换为红黑树阈值
    static final int TREEIFY_THRESHOLD = 8;

    //树结构还原为链表阈值
    static final int UNTREEIFY_THRESHOLD = 6;

    //部分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; // double threshold
        }
        ...
        return newTab;
    }

    此外HashMap是非线程安全的。并且是可以接受null值的(key和value都可以为null)。

    与HashMap相对比,HashTable是线程安全的,内部方法大部分均存在synchronized修饰符。并且HashTable不允许有null值的存在(key和value都不能为null)。所以由于线程安全的问题,HashMap效率会比HashTable高一些。

    与TreeSet介绍时相同,TreeMap是基于红黑树的,故可以按照key的大小顺序进行排序,而HashMap和HashTable不能保证数据有序。

    LinkedHashMap则可以保证数据保持插入顺序。

    我测试了一些数据,代码如下:

package com.day_2.excercise_1;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;

public class TryMap {
	public static void main(String[] args) {
		
		System.out.println("============HashMap============");
		
        Map<Object,Object> hashmap = new HashMap<Object,Object>();    
        hashmap.put(null, "这是key为null的情况");
        hashmap.put("ValueNull",null);
//        hashmap.put(null,null);// 同样可以key和value均为null,key不可重复故暂且注释
        
        Iterator<Map.Entry<Object, Object>> hashit = hashmap.entrySet().iterator();
        while (hashit.hasNext()) {
        Map.Entry<Object, Object> entry = hashit.next();
        System.out.println("key: " + entry.getKey() + " \r\nvalue: " + entry.getValue());
        }
        
        System.out.println("============Hashtable============");
        
        try{
        	Map<Object,Object> tablemap = new Hashtable<Object,Object>();
            tablemap.put(null, "这是key为null的情况");
            tablemap.put("ValueNull",null);
        }catch(NullPointerException e){
        	System.out.println("捕捉了NullPointerException异常");
        }
        
        System.out.println("============LinkedHashMap============");
        
        Map<String,String> linkedmap = new LinkedHashMap<String,String>();
        
        linkedmap.put("1", "String1");
        linkedmap.put("4", "String4");
        linkedmap.put("2", "String2");
        linkedmap.put("3", "String3");
        linkedmap.put("5", "String5");
        linkedmap.put("7", "String7");
        linkedmap.put("6", "String6");
        
        Iterator<Map.Entry<String, String>> linkedit = linkedmap.entrySet().iterator();
        while (linkedit.hasNext()) {
        Map.Entry<String, String> entry = linkedit.next();
        System.out.println("key: " + entry.getKey() + " \r\nvalue: " + entry.getValue());
        }
        
        System.out.println("============TreeMap============");
        
        Map<String,String> treemap = new TreeMap<String,String>();
        //往map集合添加 key  和 value 
        treemap.put("1", "String1");
        treemap.put("4", "String4");
        treemap.put("2", "String2");
        treemap.put("3", "String3");
        treemap.put("5", "String5");
        treemap.put("7", "String7");
        treemap.put("6", "String6");
        
        Iterator<Map.Entry<String, String>> treeit = treemap.entrySet().iterator();
        while (treeit.hasNext()) {
        Map.Entry<String, String> entry = treeit.next();
        System.out.println("key: " + entry.getKey() + " \r\nvalue: " + entry.getValue());
        }
        
    }
}

    这一篇拖拖拉拉写了大概一周,查阅了很多大佬的文章,都非常优秀,才能让我在自己瞎琢磨源码的时候有了一定的理解,看源码果然是一个很枯燥的过程,但追根溯源之后学到的和记下的也着实很多。mark的点比较多,以后有时间一一解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无语梦醒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值