集合分为两大类:Collection和Map。数组也属于集合,有一点非常重要:对于所有集合,里面存储的是对象的引用。
- 集合继承结构图_Collection部分
- 集合继承结构图_Map部分
文章目录
一、List接口
List接口主要有两个实现类:ArrayList和LinkedList。ArrayList和LinkedList它们都是属于有序可重复的。
- 有序就是存入集合时顺序是什么样,取出来还是什么样。还不理解?比如我创建一个ArrayList集合,依次存入1、2、3三个数据,那么我遍历这个集合取出来的数据依次是:1、2、3,也就是基于线性存储的
- 可重复就就是存入的数据可以相同。
实现类还有一个Vector,但是不常用,Vector是线程安全,对Vector集合的操作基本上和ArrayList一样。
List集合都有下标,所以对于List集合来说可以通过下标来遍历,也可以通过foreach来遍历。
1.1、ArrayList
ArrayList允许插入null在内的所有元素。ArrayList默认初始化容量为10(Vector也是),集合底层是一个object[]数组,由于底层是一个数组,元素在空间存储上内存地址是连续的,所以查询效率高,但是在除末尾元素增删操作外,效率都很低。ArrayList是非线程安全。
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
transient Object[] elementData;
ArrayList扩容:扩容后的capacity时先前capacity的1.5倍。
Vector扩容:扩容后的capacity时先前capacity的2倍。
int newCapacity = oldCapacity + (oldCapacity >> 1);
1.2、LinkedList
LinkedList底层是一个双向链表,所以没有capacity的概念。由于链表上的元素在空间存储上内存地址不连续,所以进行增删操作时,不会涉及到大量元素位移,效率较高。但同时由于地址不连续,所以无法通过计算地址直接查询元素,只能从头遍历,所以查询效率较低。(LinkedList集合底层也是有下标的,这个有啥意义吗?)
二、Set接口
Set接口主要有两个常用实现类:HashSet和TreeSet。它们存储元素都是无序不可重复的。对于Set集合没有下标的概念
- 无序就是存入和取出的顺序不一样
- 不可重复就是不能存入相同的数据
2.1、HashSet
HashSet底层是一个HashMap,也就是在创建一个HashSet集合时,底层会创建一个HashMap。放到HashSet集合中的元素实际上是放到HashMap集合的key部分了。
public HashSet() {
this.map = new HashMap();
}
在API文档中有这些描述:
- HashMap 的实例有两个参数影响其性能:initialCapacity 和loadFactor。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量(16)。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度(0.75)。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
- 通常,默认loadFactor(0.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
在重写了hashCode和equals方法后,向HashSet集合中加入相同的元素,后加入的元素不会覆盖先加入的元素,使用add方法时就会返回一个false。
怎么实现的呢?在《Head First Java 》中提到了引用相等性和对象相等性。
- 引用相等性:引用堆上的同一个对象的两个引用时相等的。如果对两个引用调用hashCode(),会得到相同的结果,并且equals()也相等。
- 对象相等性(堆上的两个不同对象在意义上是相同的):如果你想要把两个不同的对象视为相等,就需要重写从Object继承下来的hashCode()和equals()方法。
当equals()方法判断两个对象时相等时,那么两个对象的hashCode也一定相等;但是当两个对象的hashCode相等时,两个对象也不一定相等。看到这,不禁想问为啥重写equals方法时,也必须要重写hashCode方法。同样《Head First Java》给出了解释。当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
hashCode()和equals()的相关规定
- 如果两个对象相等,那么他们的hashCode也一定相等,并且对两个对象的任意一个调用equals()必定返回true
- 如果两个对象有一样的hashCode值,他们也不一定相同,哈希碰撞
- 重写equals方法时,一定要重写hashCode方法
- hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
- equals() 的默认行为是执行==比较,也就是说回去测试两个引用是否对得上heap上同一个对象。如果没有重写 equals(),两个对象永远不会被视为相同的,因为不同的对象有不同的字节组合。
- Object类中的equals()方法比较的是两个对象的内存地址,只有自身与自身比较时才相等。
public boolean equals(Object obj) {
return this == obj;
}
举个例子,只重写equals()会发生什么
Person类
class Person01 {
public int id;
public int age;
public String name;
public Person01(int id, int age, String name) {
this.id = id;
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person01)) return false;
Person01 person01 = (Person01) o;
return id == person01.id &&
age == person01.age &&
Objects.equals(name, person01.name);
}
/*@Override
public int hashCode() {
return Objects.hash(id, age, name);
}*/
}
测试类
public class HashMapTest02 {
public static void main(String[] args) {
Person01 p1 = new Person01(1,18,"zhangsan");
Person01 p2 = new Person01(1,18,"zhangsan");
System.out.println("p1 == p2?" + p1.equals(p2));//p1 == p2?true
HashSet<Person01> hashSet = new HashSet<>();
System.out.println("p1地址:" + p1);//p1地址:com.bjpowernode.javase.collection.Person01@2d98a335
System.out.println("p2地址:" + p2);//p2地址:com.bjpowernode.javase.collection.Person01@16b98e56
System.out.println("p1是否加入成功?" + hashSet.add(p1));//p1是否加入成功?true
System.out.println("p2是否加入成功?" + hashSet.add(p2));//p2是否加入成功?true
}
}
p1 == p2?true
p1地址:com.bjpowernode.javase.collection.Person01@2d98a335
p2地址:com.bjpowernode.javase.collection.Person01@16b98e56
p1是否加入成功?true
p2是否加入成功?true
我们在测试类中的结果可以看出,只重写equals()时,虽然p1 == p2,但他们的地址不同(也即hashCode不同),而且都能加入到HashSet集合中。而HashSet集合规定不能加入重复数据,所以也就验证了第四条规定,两个对象不相等。
我们对equals()和hashCode()进行重写
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person01)) return false;
Person01 person01 = (Person01) o;
return id == person01.id &&
age == person01.age &&
Objects.equals(name, person01.name);
}
@Override
public int hashCode() {
return Objects.hash(id, age, name);
}
p1 == p2?true
p1地址:com.bjpowernode.javase.collection.Person01@aa9caac2
p2地址:com.bjpowernode.javase.collection.Person01@aa9caac2
p1是否加入成功?true
p2是否加入成功?false
这时发现p1和p2的hashCode相同,两个对象在意义上也是相同,在加入HashSet集合时,p1加入成功,而p2加入失败。
更多equals和hashCode的介绍可以看equals与hashcode的区别与联系
个人认为两个对象相等只是在实际需求意义上的相等,并不是他们就是完全一个东西,他们在heap上依然开辟两个内存空间。就像你有两枚一模一样的硬币,他们在面值意义上是一样的,但并不就是同一枚。
2.2、TreeSet
TreeSet集合底层时二叉树,可对插入对象按照某种比较规则进行排序,实现方法有两种。
- 对于自定义的数据类型,实现Comparable接口
class Teacher implements Comparable<Teacher>{
public int age;
public String name;
public Teacher(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public int compareTo(Teacher teacher) {
return this.age - teacher.age;
}
}
- 创建一个比较类,实现Comparator接口,将算法与数据分离,这种方法需要在创建TreeSet集合时,new一个比较器对象传入TreeSet参数列表中,更方便的的可以使用匿名内部类创建比较器。
class TeacherComparator implements Comparator<Teacher> {
@Override
public int compare(Teacher t1, Teacher t2) {
return t1.age - t2.age;
}
}
TeacherComparator teacherComparator = new TeacherComparator();
TreeSet<Teacher> treeSet = new TreeSet<>(teacherComparator);
TreeSet<Teacher> treeSet = new TreeSet<>(new Comparator<Teacher>() {
@Override
public int compare(Teacher t1, Teacher t2) {
return t1.age - t2.age;
}
});
三、Map接口
向Map集合中存,以及从Map集合中取,都是先调用key的hashCode方法,然后再调用equals方法!equals方法有可能调用,也有可能不调用。
- 拿put(k,v)举例,什么时候equals不会调用?
k.hashCode()方法返回哈希值,
哈希值经过哈希算法转换成数组下标。
数组下标位置上如果是null,equals不需要执行。 - 拿get(k)举例,什么时候equals不会调用?
k.hashCode()方法返回哈希值,
哈希值经过哈希算法转换成数组下标。
数组下标位置上如果是null,equals不需要执行。
Map集合遍历的方法
keySet()
返回的是所含键的Set
视图
Set<K> keys = map.keySet();
for(K key : keys){
System.out.println(key + "=" + map.get(key));
}
entrySet()
返回的是映射关系的Set
视图
Set<Map.Entry<K,V>> set = map.entrySet();
for(Map.Entry<K,V> node : set){
System.out.println(node.getKey() + "--->" + node.getValue());
}
Map集合的Key和Value允许为null,但只能Key为null只能有一个
3.1、HashMap
HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。HashMap在碰到相同键时,Key不会被覆盖,但是Value会被覆盖。
put方法的返回值
public static void main(String[] args) {
Person01 p1 = new Person01(1,18,"zhangsan");
Person01 p2 = new Person01(1,18,"zhangsan");
System.out.println("p1 == p2?" + p1.equals(p2));
HashMap<Person01, Integer> hashMap = new HashMap<>();
System.out.println(hashMap.put(p1,1));//null
System.out.println( hashMap.put(p2,2));//1
System.out.println(hashMap.get(p2));//2
}
结果为
p1 == p2?true
null
1
2
如果当前键没有重复,put
方法返回null,否者返回当前键对应的值,并且旧值会被新值覆盖掉。(其实这里就可以解释道为啥对Set集合插入重复值会返回false,看源码)
public boolean add(E e) {
return this.m.put(e, PRESENT) == null;
}
HashMap在JDK1.8中的新特性
在JDK1.7之前,HashMap采用的时数组加链表这种结构,如果遇到哈希冲突,就将冲突的值加入到链表中
在JDK1.8之后,当链表的长度大于8 && 数组长度大于64后,链表会转为红黑树,以减少搜索时间。
3.2、TreeMap
没啥讲的,和TreeSet一样,结合着前面的看看就行。
Java集合的快速失败机制 “fail-fast”?
一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
解决办法:
- 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
- 使用CopyOnWriteArrayList来替换ArrayList
Collection接口的遍历
Iterator迭代器
Iterator
迭代器是Collection
接口通用的遍历手段。
Iterator it = c1.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.println(obj);
}
如何边遍历边删除元素remove()
从迭代器指向的 collection 中移除迭代器返回的最后一个元素(可选操作)。每次调用 next 只能调用一次此方法。如果进行迭代时用调用此方法之外的其他方式修改了该迭代器所指向的 collection,则迭代器的行为是不确定的。
抛出:IllegalStateException
- 如果尚未调用 next 方法,或者在上一次调用 next 方法之后已经调用了 remove 方法。
Iterator it = c1.iterator();
while(it.hasNext()){
it.remove();
}
Exception in thread "main" java.lang.IllegalStateException
所以要先调用next()
才能进行remove
操作
Iterator it = c1.iterator();
while(it.hasNext()){
Object obj = it.next();
it.remove();
}
总结
这是在学习到Java集合部分自己的一些理解,肯定有不足的地方以及表述错误的,这也是自己的第一篇博客,希望看官多多指正。