集合类总结
集合类
可分为单列集合和双列集合,即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
- 特点:效率较高,但是线程不安全
- 扩容机制:无参构造方法会先创建初始10容量的对象数组elementData[],之后扩容会变成原来的1.5倍
2.Vector类
Since v1.0,底层是数组。
- 特点:效率低,但是线程安全(方法里都写了synchronized)
- 扩容机制:底层也是elementData的对象数组,初始容量10,之后每次扩容会变成原来两倍(个人理解为多线程时才需要synchronized,所以往往会需要比较大的容量,如果扩容小了会频繁的进行扩容);
LinkedList类
since v1.2,底层是双向链表
- 特点:线程不安全,方便增删,不方便改查
Set接口
特点:
- 无序(这里的无序指和你添加的顺序不一致,实际上会按照某个算法进行排序,比如hash),
- 不可重复,
- 可null,但只能有一个
- 无索引,所以遍历方法只能用Iterator或者增强for,也不包含set/get等index方法
1.HashSet类
底层是hashMap,只是value值用常量Object对象PRESENT占位了,实际存储的数据就是key。
2.LinkedHashSet类
是HashSet的子类,底层LinkedHashMap,是数组+双向链表的数据结构
特点:
- 有序(使用双向链表可以进行按照存入顺序取出)
- 数组是HashMap[]Node类型的,但是存放的元素是LinkedHashMap Entry类型的,Entry类是Node类的子类。这是多态现象。
3.TreeSet类
底层为TreeMap。
特点:
- 可以进行排序
- 添加的元素必须实现Comparable接口(String类已经实现)。如果没有实现该接口,则每次使用必须传入一个comparator
- 根据comparator的比较规则,追溯到底层,如果o1-o2==0的情况,那么会进入setvalue方法,只是修改value值而不会添加新元素。例子:把comparator方法写成str.length的比较,则相同长度的字符串是无法重复添加的。
Map接口
hashMap类
底层:数组+链表+红黑树,默认容量16
特点:
- key、value都可以有一个null
- key不可重复,value可以
扩容机制
- 当某条链上节点超过8个(此时bigcount=7,node=9个,bigcount>=DEFAULT(8)-1)时会进行树化判断,此时如果数组大小没到64会进行扩容,也就是说node=9时,resize1次,capacity=32,node=10时,capacity=64,node=11时,进行树化。
- 如果链上的节点都没有超过8个,那么会判断是否到达临界值,由加载因子决定临界值:threshold=0.75*当前容量。第一次的时候threshold=0.75 *16=12,当添加了第13个节点后进行扩容,16 *2 =32;
常见问题
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。

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后面成链表。

浙公网安备 33010602011771号