Java 集合

Java容器

原文路径

集合容器概述

什么是集合

  • 集合就是一个放数据的容器,准确的说是放数据对象引用的容器
  • 集合类存放的都是对应的引用,而不是对象的本身
  • 集合类型主要有3种:set(集)、list(列表)和map(映射)

集合的特点

  • 集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理
  • 和数组对比,对象的大小不确定,因为集合是可变长度,数组需要提前定义大小

集合和数组的区别

  • 数组是固定长度,集合可变长度
  • 数组可以存储基本数据类型,也可以存储引用数据类型,集合只能存储引用数据类型
  • 数组存储的元素必须是同一数据类型,集合存储的对象可以是不同数据类型

使用集合框架的好处

  • 容量自增长
  • 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量
  • 可以方便的扩展或改写集合,提高代码复用性和可操作性性
  • 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本

常用的集合类

Map接口和Collection接口是所有集合框架的父接口

  • Collection接口的子接口:Set和List
  • Map接口的实现类:HashMap、TreeMap、HashTable、ConcurrentHashMap以及Properties等
  • Set接口的实现类:HashSet、TreeSet、LinkedHashSet
  • List接口的实现类:ArrayList、LinkedList、Stack以及Vector

List,Set,Map三者区别

在这里插入图片描述

集合框架底层数据结构

  • Collection
    • List
      • ArrayList:Object数组
      • Vector:Object数组
      • LinkedList:双向循环链表
    • Set
      • HashSet(无序,唯一):基于HashMap实现的,底层采用HashMap来保存元素
      • LinkedHashSet:继承于HashSet,内部是通过LinkedHashMap来实现
      • TreeSet(有序,唯一):红黑树(自平衡的排序二叉树)
    • Map
      • HashMap:JDK 8之前由数组+链表组成,数组是主体,链表是为了解决哈希冲突(拉链法)
      • LinkedHashMap:继承自HashMap,在HashMap的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时是通过链表进行相应的操作,实现了访问顺序相关逻辑
      • HashTable:数组+链表组成
      • TreeMap:红黑树(自平衡的排序二叉树)

哪些集合类是线程安全

  • Vector:比ArrayList多synchronized
  • HashTable:比HashMap多synchronized
  • ConcurrentHashMap:由Segment数组结构和HashEntry数组结构组成,每个Segment里面包含一个HashEntry数组,Segment数组扮演锁的角色

Java集合的快速失败机制 fail-fast

  • 是Java集合的一种错误检测知己,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast机制

例如:存在两个线程(线程1,线程2,),线程1通过Iterator遍历集合A,线程2修改了集合A的结构(insert,delete),则会抛出ConcurrentModificationException异常,从而产生fail-fast机制

原因:迭代器在遍历时直接访问集合中的内容,并在遍历过程中使用一个modCOunt遍历,集合在被遍历期间如果发生变化,就会改变modCount的值。每当迭代器使用hasNext()/next()遍历下一个元素之前,都会检测modCOunt变量是否为expectModCount值,是的话就返回遍历,否则抛出异常,终止遍历

  • 解决办法:
    • 在遍历过程中,所有设计到改变modCount值得地址全部加上synchronized
    • 使用CopyOnWriteArrayList替换ArrayList

怎么确保一个集合不能被修改?

  • 可以使用Collections.unmodifiableCollection(Collection c)方法创建一个只读集合,任何改变集合的操作都会抛出异常
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());

Collection接口

List接口

迭代器Iterator是什么?

  • Iterator接口提供便利任何Collection的接口。可以从Collection中使用迭代器方法来获取迭代器实例,迭代器取代了Java集合框架的Enumeration,迭代器运行调用者在迭代过程中移除元素
  • Collection继承了Iterator接口

Iterator使用和特点

示例:

List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
  String obj = it. next();
  System. out. println(obj);
}
  • 特点:只能单向便利,更加安全,在遍历时被修改会抛出异常

如何遍历时移除Collection中元素?

使用Iteerator.remote()

Iterator<Integer> it = list.iterator();
while(it.hasNext()){
   *// do something*
   it.remove();
}

Iterator和ListIterator区别

  • Iterator可以遍历Set和List,ListIterator只能遍历List
  • Iterator单向遍历,ListIterator双向遍历
  • ListIterator实现Iterator接口,添加了一些额外功能,如添加一个元素、替换一个元素、获取前面或者后面的索引位置等

遍历List的方式?每种方法实现原理?最佳实践是什么?

方式:

  • for循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每个位置的元素,当读取到最后一个元素后停止
  • 迭代器遍历,Iterator。Iterator是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,同一遍历集合的接口
  • foreach循环遍历。foreach内部也是才用了Iterator的方式实现,使用时不需要显式声明Iterator或计数器。优点是代码简介,不易出错。缺点是:只能做简单的遍历,不能再遍历过程中操作数据集合,如删除、替换

最佳实践:

Java Collections框架中提供了一个RandomAccess接口,用来标记List实现是否支持Random Access

  • 如果一个数据集合实现了该接口,就意味着支持Random Access,按位置读取元素的平均实践复杂度为O(1),如ArrayList
  • 如果没有实现该接口,表示不支持Random Access,如LinkedList
  • 推荐:支持Random Access的列表用for循环遍历,否则使用Iterator或foreach

ArrayList优缺点:

ArrayList比较适合顺序添加、随机访问的场景

优点

  • ArrayList底层以数组实现,是一种随机访问模式。ArrayList实现了RandomAccess接口
  • ArrayList在顺序添加一个元素时很方便

缺点

  • 插入、删除操作性能差,涉及到很多元素移动

如何实现数组和List直接转换?

  • 数组 -> List:Arrays.asList(array)
  • List -> 数组:list.toArray()

ArrayList和LinkedList区别

  • 数据结构实现:ArrayList是动态数组的数据结构,而LinkedList是双向链表
  • 随机访问效率:ArrayList比LinkedList在随机访问效率高,因为LinkedList是线性存储方式,需要指针移动
  • 插入和删除效率:LinkedList效率高,指针移动即可,ArrayList需要移动大量元素
  • 内存空间占用:LinkedList比ArrayList更占内存,LinkedList需要存储指针
  • 线程安全:都不安全
  • 综合:频繁读取使用ArrayList,插入和删除操作较多使用LinkedList

ArrayList和Vector区别

线程安全&性能&扩容容量

多线程环境下使用ArrayList

List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++) {
    System.out.println(synchronizedList.get(i));
}

为什么ArrayList的elementData加上transient修饰?

数组定义:

private transient Object[] elementData;

ArrayList定义:

public class ArrayList<E> extends AbstractList<E>
     implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • ArrayList实现了Serializable接口,意味着ArrayList支持序列化。transient作用是不希望elementData数组被序列化,重写了writeObject实现:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    *// Write out element count, and any hidden stuff*
        int expectedModCount = modCount;
    s.defaultWriteObject();
    *// Write out array length*
        s.writeInt(elementData.length);
    *// Write out all elements in the proper order.*
        for (int i=0; i<size; i++)
            s.writeObject(elementData[i]);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
}

  • 每次序列化,先调用defaultWriteObject()方法序列化ArrayList的非transient元素,然后遍历elementData,值序列化已存入的元素,这样既加快了序列化的速度,又见笑了序列化后文件大小

List和Set区别

  • 都继承自Collection
  • List:有序,元素可以重复,可以插入多个null元素,常用实现类有ArrayList、LinkedList和Vector
  • Set:无序,不可重复,只能存入一个null元素,常用实现类是HashSet、LinkedHashSet以及TreeSet
  • List支持索引查询,Set只能通过迭代器
  • 对比:Set检索元素效率低下,插入和删除效率高,插入和删除不会引起元素位置改变。List类似数组,List可以动态增长,查找元素效率高,插入删除元素效率低

Set接口

HashSet实现原理

  • HashSet是基于HashMap实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此HashSet的实现比较简单,相关HashSet的操作基本上都是直接调用HashMap的方法

HashSet如何坚持重复?HashSet如何保证数据不可重复?

  • add时,判断元素是否存在,通过hash值和equals方法比较
  • add方法会使用HashMap的put方法
  • HashMap的key是唯一的,所以不会重复

Map接口

什么是Hash算法

  • 把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制叫哈希值

什么是链表

  • 链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能
  • 链表:单链表和双向链表
    • 单链表:每个结点包含两个部分(数据和指向下一节点的指针)在这里插入图片描述
    • 除了单链表的部分,还增加了pre指向上一个节点的指针在这里插入图片描述
    • 优点:
      • 插入删除速度快
      • 内存利用率高
      • 大小不固定,拓展灵活
    • 缺点
      • 不能随机查找,必须从第一个开始遍历,查找效率低

HashMap实现原理

  • HashMap概述:HashMap是基于哈希表的Mao接口的非同步实现,无序,key-value容器
  • 数据结构:数组+链表
  • 基于Hash算法
    • 往HashMap中put元素怒时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
    • 存储时,出现hash值相同的key
      • 如果key相同,则覆盖原始值
      • 如果key不同,则将key-value放入链表中
    • 获取时,直接找到hash值对应的下标,再进一步判断key是否相同,从而找到对应值
  • JDK 8对HashMap的实现做了优化,当链表节点数据超过8个并且同容量大于64之后,链表会转为红黑树来提高查询效率,从O(n) -> O(logn)

HashMap在JDK 7和JDK 8中有哪些不同?HashMap的底层实现

  • 数据结构中,有两种简单常见的数据结构:链表和数组。数组的特点是寻址容易,插入和删除困难。链表的特点是寻址困难,插入和删除容易。将数组和链表结构,使用拉链法的方式解决哈希冲突

HashMap JDK 8之前采用的是拉链法,即数组+链表

在这里插入图片描述

HashMap在 JDK 8之后

  • 相比于之前的版本,JDK 8在解决哈希冲突时有了较大的变化,引入红黑树,减少搜索时间

在这里插入图片描述

JDK 7和JDK 8 HashMap比较

  • resize扩容优化
  • 引入红黑树
  • 解决多线程死循环问题,仍非线程安全,多线程时可能会造成数据丢失

什么是红黑树

什么是二叉树

  • 每个节点上可以关联两个子节点

红黑树

  • 红黑树是一种特殊的二叉查找树。红黑树的节点可以用颜色来表示类型

img

  • 红黑树的每个结点是黑色或者红色,其中:根结点和叶子结点是黑色(NIL或NULL)
  • 红结点的子结点一定是黑色
  • 每个节点到叶子结点NIL经过的黑色结点数一样

HashMap的put方法具体流程

在这里插入图片描述

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

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

//实现Map.put和相关方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 步骤①:tab为空则创建 
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 步骤②:计算index,并对null做处理  
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 步骤③:节点key存在,直接覆盖value 
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // 步骤④:判断该链为红黑树 
        // hash值不相等,即key不相等;为红黑树结点
        // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 步骤⑤:该链为链表 
        // 为链表结点
        else {
            // 在链表最末插入结点
            for (int binCount = 0; ; ++binCount) {
                // 到达链表的尾部
                
                //判断该链表尾部指针是不是空的
                if ((e = p.next) == null) {
                    // 在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                    //判断链表的长度是否达到转化红黑树的临界值,临界值为8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //链表结构转树形结构
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 步骤⑥:超过最大容量就扩容 
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

  1. 哦按的键值对数组table[i]是否为空或为null,否则执行resize()进行扩容
  2. 根据键值key计算hash值得到插入的数组索引,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③
  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④(hashCode和equals)
  4. 判断table[i]是否是treeNode(红黑树),如果是红黑树,则直接在树种插入键值对,否则转向⑤
  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作,遍历过程中若发现key以及存在直接覆盖集合
  6. 插入成功后,判断实际的键值对数量size是否超过了最大容量threshold,如果超过,进行扩容

HashMap的扩容机制

  • JDK 8中,resize方法时在HashMap中的键值对数量大于阈值时或者初始化时,就调用resize方法进行扩容
  • 每次扩容都是扩展2倍
  • 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;//oldTab指向hash桶数组
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
        if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值
            threshold = Integer.MAX_VALUE;
            return oldTab;//返回
        }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold
    }
    // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂
    // 直接将该值赋给新的容量
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 新的threshold = 新的cap * 0.75
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        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];//新建hash桶数组
    table = newTab;//将新数组的值复制给旧的hash桶数组
    // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散
    if (oldTab != null) {
        // 遍历新数组的所有桶下标
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
                oldTab[j] = null;
                // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树
                if (e.next == null)
                    // 用同样的hash映射算法把该元素加入新的数组
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // e是链表的头并且e.next!=null,那么处理链表中元素重排
                else { // preserve order
                    // loHead,loTail 代表扩容后不用变换下标,见注1
                    Node<K,V> loHead = null, loTail = null;
                    // hiHead,hiTail 代表扩容后变换下标,见注1
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍历链表
                    do {             
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
                                // 代表下标保持不变的链表的头元素
                                loHead = e;
                            else                                
                                // loTail.next指向当前e
                                loTail.next = e;
                            // loTail指向当前的元素e
                            // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,
                            // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
                            // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。
                            loTail = e;                           
                        }
                        else {
                            if (hiTail == null)
                                // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

HashMap如何解决哈希冲突?

  • 链表发:将相同的Hash值得对象组织成一个链表放在hash值对应的槽位
  • 开放地址法是通过一个探测算法,当某个操作已经被占据的情况下,可任意查找下一个可以使用的槽位

什么是TreeMap

  • TreeMap是一个有序的key-value集合,通过红黑树实现
  • TreeMap基于红黑树,该映射根据其键的自然顺序进行排序,根据创建映射器时提供的Comparator进行排序,具体取决于使用的构造方法
  • TreepMap是非线程安全

HashMap or TreeMap

  • 对于Map中插入、删除和定为元素使用HashMap,有序集合则使用TreeMap

HashMap和ConcurrentHashMap

  • ConcurrentHashMap对整个桶进行了分隔分段(Segment),每个分段上都用了lock锁机芯保护,相对于HashTable的synchronized粒度更精细,并发性能更好,而HashMap没有锁机制,不是线程安全
  • HashMap的键值对允许null,但是ConcurrentHashMap都不允许

ConcurrentHashMap和HashTable区别

  • 底层数据结构:JDK 7点的ConcurrentHashMap底层采用 分段数组+链表 实现,JDK 8采用的数据结构根据HashMap结构一样,数组+链表/红黑树
  • 实现线程安全方式
    • JDK 7时,ConcurrentHashMap对整个桶数组进行了分割分段(Segment),每一把锁只锁容器中一部分数据,默认16个Segment,到JDK 8是一件摒弃了Segment的概念,直接使用Node数组+链表+红黑树的实现,并发控制使用synchronized和CAS来操作

HashTable

在这里插入图片描述

JDK 7的ConcurrentHashMap

在这里插入图片描述

JDK 8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点)

在这里插入图片描述

ConcurrentHashMap底层具体实现?实现原理?

JDK 7

  • 将数据分为很多段存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问
  • 采用Segment + HashEntry实现

在这里插入图片描述

  • 该类包含两个静态内部类HashEntry和Segment,前者用来封装映射表的键值对,后者用来充当锁的角色
  • Segment是一种可重入的锁ReentrantLock,每个Segment守护一个HashEntry数组里得到袁术,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment锁

JDK 8

  • 放弃Segment臃肿的设计,取而代之的是采用Node+CAS+synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突就不会产生并发问题,效率提升N倍

辅助工具类

Array和ArrayList区别

  • Array可以存储基本数据类型和对象,ArrayList只能存储对象
  • Array是指定固定大小,ArrayList大小是自动扩展的
  • Array内置方法没有ArrayList多

Array和List直接转换

  • Array到List:Arrays.toList(arrsy)
  • List到Array:list.toArray()

Comparable和Comparator区别

  • comparable接口出自java.lang包,需要重写compareTo(Object obj)方法来排序。相当于内部比较器,耦合性要强一些,因为是写在类上的,如果要修改比较算法,要修改Comparable接口的实现类

    public class Person implements Comparable<Person>{
        private String name;
        private int age;
       ... getter/setter方法
       //按照年龄升序排序
       @Override
       public int compareTo(Person o){
           if(this.age>o.getAge()){
               return 1;
           }
           if(this.age<o.getAge()){
               return -1;
           }
           return 0;
       }
    }
    
  • comparator接口出自java.lang包,需要重写compare(Object obj1,Object obj2)方法来排序,相当于外部比较器,是外部进行比较的。不需要对实现类进行修改

    Collections.sort(arrayList,new Comparator<Integer>(){
        @Override
        public int compare(Integer o1,Integer o2){
            return o2.compareTo(o1);//降序
        }
    });
    

Collection和Collections区别

  • java.util.Collection是集合类的一个顶级接口。提供了对集合对象机芯基本操作的通用接口方法
  • Collections是集合类的工具类,提供了一些列静态方法,用于对集合中元素进行排序、搜索以及现场安全等各种操作

TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?

  • TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo() 方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序

  • Collections工具类的sort()方法有两种重载形式

    • 第一种要求传入的对象必须实现Comparable接口,重写compareTo(Object obj)
    • 第二种需要单独传入实现了Comparator接口的实现类
# java 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×