集合类总结

集合类

可分为单列集合和双列集合,即Collection接口(单列)和map接口(双列,即key-value集合)

集合体现了多态的思想。

Collection接口

List接口

  • 特点:有序(先进先出)
  • 可重复
  • 可null
  • 有索引,可用index,所以可以有三种遍历方式(包括Iterator或者增强for和最基本的for循环--通过set/get获取元素)

主要方法:

  • add():底层是对象数组,所以添加什么都行。基本类型会进行封装。
  • remove(index/object):如果是object为参数,遇到重复元素只会是删除遇到的第一个元素。如果要删除int的对象的话,为避免与index冲突,需要强制转换成Integer类型才能直接删除该对象。
  • set(index,object)

1.ArrayList类

Since v1.2,底层是数组,默认容量10

  1. 特点:效率较高,但是线程不安全
  2. 扩容机制:无参构造方法会先创建初始10容量的对象数组elementData[],之后扩容会变成原来的1.5倍

2.Vector类

Since v1.0,底层是数组。

  1. 特点:效率低,但是线程安全(方法里都写了synchronized)
  2. 扩容机制:底层也是elementData的对象数组,初始容量10,之后每次扩容会变成原来两倍(个人理解为多线程时才需要synchronized,所以往往会需要比较大的容量,如果扩容小了会频繁的进行扩容);

LinkedList类

since v1.2,底层是双向链表

  1. 特点:线程不安全,方便增删,不方便改查

Set接口

特点:

  • 无序(这里的无序指和你添加的顺序不一致,实际上会按照某个算法进行排序,比如hash),
  • 不可重复,
  • 可null,但只能有一个
  • 无索引,所以遍历方法只能用Iterator或者增强for,也不包含set/get等index方法

1.HashSet类

底层是hashMap,只是value值用常量Object对象PRESENT占位了,实际存储的数据就是key。

2.LinkedHashSet类

是HashSet的子类,底层LinkedHashMap,是数组+双向链表的数据结构

特点:

  1. 有序(使用双向链表可以进行按照存入顺序取出)
  2. 数组是HashMap[]Node类型的,但是存放的元素是LinkedHashMap Entry类型的,Entry类是Node类的子类。这是多态现象。

3.TreeSet类

底层为TreeMap。

特点:

  1. 可以进行排序
  2. 添加的元素必须实现Comparable接口(String类已经实现)。如果没有实现该接口,则每次使用必须传入一个comparator
  3. 根据comparator的比较规则,追溯到底层,如果o1-o2==0的情况,那么会进入setvalue方法,只是修改value值而不会添加新元素。例子:把comparator方法写成str.length的比较,则相同长度的字符串是无法重复添加的。

Map接口

hashMap类

底层:数组+链表+红黑树,默认容量16

特点:

  1. key、value都可以有一个null
  2. key不可重复,value可以

扩容机制

  1. 当某条链上节点超过8个(此时bigcount=7,node=9个,bigcount>=DEFAULT(8)-1)时会进行树化判断,此时如果数组大小没到64会进行扩容,也就是说node=9时,resize1次,capacity=32,node=10时,capacity=64,node=11时,进行树化。
  2. 如果链上的节点都没有超过8个,那么会判断是否到达临界值,由加载因子决定临界值:threshold=0.75*当前容量。第一次的时候threshold=0.75 *16=12,当添加了第13个节点后进行扩容,16 *2 =32;

hash底层的元素存入

常见问题

1.hashMap的hash()方法不是得到hashcode值,而是得到hashcode优化后的值:(h = key.hashCode()) ^ (h >>> 16)

它会先用这个Key值求出hashcode值并赋给h,然后用这个hashcode值与无符号右移16位后的hashcode进行异或操作(相同的为0,不同的为1,0异或任何都等于它本身,1异或任何都等于它求反),这样做可以减少碰撞率。

2.hashCode()方法和内存地址的关系?
hashCode并不是内存地址,但是算法与内存地址有关。openJDK的源码里有5种hashCode的算法:

static inline intptr_t get_next_hash(Thread * Self, oop obj) {
    intptr_t value = 0 ;
    if (hashCode == 0) {
        // This form uses an unguarded global Park-Miller RNG,
        // so it's possible for two threads to race and generate the same RNG.
        // On MP system we'll have lots of RW access to a global, so the
        // mechanism induces lots of coherency traffic.
        value = os::random() ;//随机
    } else
    if (hashCode == 1) {
        // This variation has the property of being stable (idempotent)
        // between STW operations.  This can be useful in some of the 1-0
        // synchronization schemes.
        intptr_t addrBits = intptr_t(obj) >> 3 ;//地址基础上hack
        value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
    } else
    if (hashCode == 2) {//固定返回1,不会用这个东西,可能是测试用的
        value = 1 ;            // for sensitivity testing
    } else
    if (hashCode == 3) {
        value = ++GVars.hcSequence ;
    } else
    if (hashCode == 4) {//这个是直接用内存地址的
        value = intptr_t(obj) ;
    } else {//通过当前状态值进行异或(XOR)运算得到的一个 hash 值,相比前面的自增算法和随机算法来说效率更高,但重复率应该也会相对增高
        // Marsaglia's xor-shift scheme with thread-specific state
        // This is probably the best overall implementation -- we'll
        // likely make this the default in future releases.
        unsigned t = Self->_hashStateX ;
        t ^= (t << 11) ;
        Self->_hashStateX = Self->_hashStateY ;
        Self->_hashStateY = Self->_hashStateZ ;
        Self->_hashStateZ = Self->_hashStateW ;
        unsigned v = Self->_hashStateW ;
        v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
        Self->_hashStateW = v ;
        value = v ;
    }

    value &= markOopDesc::hash_mask;
    if (value == 0) value = 0xBAD ;
    assert (value != markOopDesc::no_hash, "invariant") ;
    TEVENT (hashCode: GENERATE) ;
    return value;
}

由上述代码可以知道,当全局变量hashcode==4的时候,hashcode()的计算结果才是内存地址。

3.HashSet类添加重复的元素,是被覆盖还是不处理?

hashSet的底层是hashMap,实际上它虽然是hashMap的结构,但是set集合是单列集合,即它只存储key值,value值是一个不可更改的static final Object对象:PRESENT(只是占坑用的,实际没什么作用)。当添加重复的元素的时候,key值是不会覆盖的,虽然说hashMap的value值可以覆盖,但这里实际上替换的还是同一个静态对象PRESENT。

img

4.HashMap的EntrySet 、keySet、values之间的区别?

这三者都可以通过map的方法去获得,如 Set entrySet = map.entrySet();

区别1:values是collection类型的,而keyset、entryset是set类型的。keyset存放的只是key,而entrySet实际存放的key是Map.Entry<K, V>,它的实现类就是Node。所以它这个key实际上包含了键和值的映射

区别2:遍历速度,values是最快的,但没有意义,主要比较keyset和entryset。可能会觉得keyset少了values会遍历得更快,实际相反,entryset可以直接用方法getkey()、getvalue()获取key-value值,并且他获取entry的值也是通过先遍历数组(hash可以筛选大部分的元素)然后再遍历少量的链表元素即可。但是keyset方法只是获取key值,然后再根据map.get(key)去求value值,并且set集合是单链表,查找是比较麻烦的。

注意:这些集合只是存放前往key-value值的地址,实际存放这些数据的还是在hashmap里面。

5.为什么重写了equals方法后还要重写HashCode()方法?

重写HashCode方法主要用在HashMap、hashset这类集合里,如果不涉及hashcode的话其实没必要重写。重写hashcode是由hashmap的底层机制决定的,他会先根据添加的key,通过hashcode后计算得到索引位置(p = tab[i = (n - 1) & hash]),如果索引位置冲突了,才会进行equals比较。如果不重写hashcode方法,在判断数组索引这一步,如果hash值不一致,很有可能索引位置不会发生冲突,找个空的位置就直接加进去了,这不符合set和map的key值唯一的规则。

Hashtable类

Since 1.0,

1.和hashmap差别不是很大,主要是线程安不安全,以及是否可null 之类的

2.默认容量11,threshold=8;之后扩容按2n+1扩。

习题
//Person类已按照id、name重写了hashcode和equals方法,以下几个语句的输出是?
HashSet set = new HashSet();
Person p1 = new Person(1001,"AA");
Person p2= new Person(1002,"BB");
set.add(p1);
set.add(p2);
p1.name="CC";
set.remove(p1);
System.out.println(set);
set.add(new Person(1001,"CC"));
System.out.println(set);
set.add(new Person(1001,"AA"));
System.out.println(set);

1.remove方法因为重写了hashcode 和equals方法,hash索引变更,无法根据新的hash值找到存放的位置,删除失败;所以第一个输出仍为2;

2.1001,"CC"这个新对象虽然和修改后的p1一样,但是p1仍存放在原来1001,“AA”索引的位置,所以1001,"CC"可以存放在空的索引位置,输出3个了;

3.此时新对象1001,“AA”索引和p1一样,但是因为equal方法重写了,所以会挂在p1后面成链表。

posted @ 2022-01-20 00:13  BerserkD  阅读(67)  评论(0)    收藏  举报