笛子的Java系列总结——集合篇
1 Java集合框架概述
1.1 Collection 和 Map.
Java集合可以分为Collection 和 Map 两种体系
- Collection接口:单列数据,定义了存取一组对象的方法的集合
- List:元素有序、可重复的集合
- Set:元素无序、不可重叠的集合
(实际上Collection的接口不止这两个,只是这两个最常用,下图中红框中的都是Collection的继承接口)
- Map接口:双列数据,保持具有映射关系“key-value对”的集合
不同的key可以对应同一个value;
但是一个key不能对应多个value。
1.2 Collection接口继承树
1.3 Map接口继承树
2 Collection接口方法
可以看之前写的笛子的Java系列总结——集合Collection提供的接口方法
3 Iterator迭代器接口
3.1 迭代器概述
- Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素
- Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
定义一个迭代器的代码
Iterator iterator = coll.iterator() // coll 是一个集合类对象
Iterator仅用于遍历集合
,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。集合对象每次调用iterator()方法都得到一个全新的迭代器对象
,默认游标都在集合的第一个元素之前。
3.2 Iterator接口的方法
- hasNext() 用于检测iteration中是否还有待遍历的元素
- next() 获取到iteration中的下一个元素
- remove() 从底层集合中删除此迭代器返回的最后一个元素(可选操作)。
需要注意的地方
- 在调用it.next()方法前必须要调用it.hasdNext() 进行检测,若不调用,且下一条记录无效,会抛出NosuchElementException异常。
- Iterator接口的remove()方法
(这部分还是敲代码运行体会一下)
Iterator iter = coll.iterator();//回到起点
while(iter.hasNext()){
Object obj = iter.next();
if(obj.equals("Tom")){
iter.remove();
}
}
(1)Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法。
(2) 如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。
3.3 使用foreach循环遍历集合元素
Java5.0提供foreach循环迭代访问Collection和数组,这种遍历操作不需要获取集合或数组长度,无需使用索引访问元素,底层调用Iterator完成操作
代码示例:
String[] str = new String[5];
for (String s : str){
// 操作语句
}
4 Collection子接口之一:List接口
4.1 List接口方法
List除了从Collection集合继承的方法外,List集合里添加了一些根据索引来控制集合元素的方法
测试代码
@Test
// 用来测试List中根据索引操作集合的方法
public void test1(){
List ls = new ArrayList();
ls.add(1);
ls.add(2);
ls.add(3);
System.out.println(ls); // 输出结果为:[1, 2, 3]
ls.add(1,false);
ls.add(2,3);
System.out.println(ls); // 输出结果为:[1, false, 3, 2, 3]
System.out.println(ls.get(1)); // 输出结果为:false
System.out.println(ls.indexOf(3)); // 输出结果为:2
System.out.println(ls.lastIndexOf(3)); // 输出结果为:4
ls.set(3,"笛子");
System.out.println(ls); // 输出结果为:[1, false, 3, 笛子, 3]
ls.remove(1);
System.out.println(ls); // 输出结果为:[1, 3, 笛子, 3]
}
有一点需要说明的是,remove方法有remove (int index) 和 remove(Object obj)两种,第一种按照索引删除,第二种按照数据删除(直接删除值等于obj的数据)
4.2 List具体实现类
4.2.1 ArrayList(源码分析)
源于JDK 2,List接口也出现于JDK 2
JDK 7 和 JDK 8有所不同,暂时以JDK 7为例
jdk 7 情况下
ArrayList list = new ArrayList();
这条代码底层创建了长度是10的Object[]数组elementData
list.add(1);
底层执行逻辑——elementData[0] = new Integer(3)
…
list.add(11);
如果此次的添加导致底层elementData数组容量不够,则扩容。默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的元素复制到新的数组中。
扩容源码如下(当然注释是自己加的)
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 正常情况扩容为原来的1.5倍(0.5倍通过右移位实现)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 扩容完如果还是不够的情况
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 扩容到MAX_ARRAY_SIZE之后会取整型的最大值作为容量了
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);
}
JDK 8中ArrayList的变化
ArrayList list = new ArrayList();
底层Object[] elementData初始化为{ },并没有创建长度为10的数组
list.add(1);
第一次调用add()时,底层才创建了长度为10的数组,并将数据1添加到elementData中
…
后续的添加和扩容操作与JDK 7无异
小结
jdk 7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk 8中的ArrayList对象创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
4.2.2 LinkedList(源码分析)
源于JDK 2,List接口也出现于JDK 2
在JDK 7 和 8 中没什么区别
LinkedList list = new LinkedList();
内部声明了Node类型的first和last属性,默认值为null
list.add(1);
将123封装到Node中,创建了Node对象,其中Node定义如下,证明了LinkedList是双向链表
双向链表中的节点定义
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
其他操作:其他操作,比如增删元素等就和算法题里做过的链表操作差不多,暂时不一一看源码了,其实ArrayList也是一样,其他没有介绍的操作和对数组的操作也都差不多
4.2.3 Vector(源码分析)
- 源于JDK 1.0,作为List接口的古老实现类,线程安全的,效率低,底层使用Object[]。
- 无参构造器 JDK7 和 JDK8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组
public Vector() {this(10);}
- 在扩容方面,默认扩容为原来的数组长度的2倍。
4.2.4 ※三者的异同点
- 三者相同点
三个类都是实现了list接口的具体实现类,存储数据的特点相同:存储有序的、可重复的数据 - 不同点
(1)ArrayList和LinkedList的异同
二者都线程不安全,相对线程安全的Vector,执行效率高。
此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
(2)ArrayList和Vector的区别
Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。
5 Collection子接口之二:Set接口
5.1 Set特点
5.1.1 概述
- Set接口是Collection的子接口,Set接口没有提供额外的方法;
- Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个
Set 集合中,则添加操作失败; - Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
5.1.2 对Set无序、不可重复的理解
- 无序性
不等于随机性,每次遍历的结果其实是一样的。另外虽然LInkedHashSet遍历顺序与添加顺序一样,但是也不代表其是有序的。
这里的无序性的意思是针对存储顺序说的,存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定。 - 不可重复性
保证添加的元素按照equals()判断时,不能返回true,即相同的元素只能添加一个
5.2 Set具体实现类
5.2.2 HashSet
底层是使用HashMap实现的
添加元素的过程:以HashSet为例
向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出HashSet底层数组中的存放位置(即为:索引位置),判断此位置上是否已经有其他元素:
情况1:如果此位置上没有其他元素,则元素a添加成功。
如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a和元素b的hash值:情况2:如果hash值不相同,则元素a添加成功。
如果hash值相同,进而需要调用元素a所在类的equals()方法:情况3:equals()返回true,元素a添加失败
情况4:equals()返回false,则元素a添加成功
注意的是:哈希值不直接等同于底层数组中的存放位置,中间还经过了某些运算(散列函数)
对于添加成功的的情况2和4而言,元素a与已经存在指定索引位置上数据以链表的方式存储。
jdk 7 :元素a放到数组中,指向原来的元素
jdk 8 :原来的元素在数组中,指向元素a(七上八下)
hashCode() 和 equals()方法的重写
- 向Set中添加数据,其所在的类一定要重写equals()和hashCode(),且重写的equals()和hashCode()尽可能保持一致性,即相等的对象比较具有相等的散列码
- 重写 hashCode() 方法的基本原则
(1)在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值
(2)当两个对象的 equals() 方法比较返回 true 时,这两个对象hashCode()
方法的返回值也应相等。
(3)对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值
注:field(成员变量)是指类的数据成员 - 重写 equals() 方法的基本原则
复写equals方法的时候一般都需要同时复写hashCode方法。 通
常参与计算hashCode 的对象的属性也应该参与到equals() 中进行计算。(和2中的第三条其实是一个意思,就是把原来判断逻辑相等的成员数据在equals和hashcode重写中要同时考虑) - 为什么不直接equals比较值是否相同,而先要计算hashCode?
直接用equals比较确实可以完成比较任务,这么做的目的是为了效率。hash算法是二进制算法(位运算),计算式本质是二进制,所以hash算法速度很快。如若hashCode不同则可直接存储不用equlas比较。所以先计算hashCode大大加快了存储速率。 实际开发中一般不需要自己去设计两个重写方法,可以在自定义类中调用工具自动重写equals和hashCode
举例:
class User {
private String name;
private int age;
private String email;
public User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
// IDEA自动重写的equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
if (age != user.age) return false;
if (name != null ? !name.equals(user.name) : user.name != null) return false;
return email != null ? email.equals(user.email) : user.email == null;
}
// IDEA自动重写的hashCode方法
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
result = 31 * result + (email != null ? email.hashCode() : 0);
return result;
}
}
5.2.3 LinkedHashSet
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以
插入 顺序保存
的。 LinkedHashSet插入性能略低于 HashSet
,但在迭代访问 Set 里的全
部元素时有很好的性能。
5.2.4 TreeSet(可以按照添加的对象的指定属性进行排序)
- 向TreeSet中添加的数据,要求是相同的类
- TreeSet底层使用红黑树结构存储数据,所以要求不能有重复的数据
- 两种排序方法:自然排序 和 定制排序(之前介绍过的Java两种比较器)
(1)自然排序中,不同于HashSet,对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareT o(Object obj) 方法比较返回值
,即compareT o()放回0,不再是equals()。所以:
要求1 :当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareT o(Object obj) 方法有一致的结果:如果两个对象通过equals() 方法比较返回 true,则通过 compareT o(Object obj) 方法比较应返回 0。否则,让人难以理解
要求2:如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。不重写会抛出异常
(2)定制排序中,通过通过Comparator接口来实现。需要重写compare(T o1,T o2)方法
① 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
② 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
③ 仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异
常。
④ 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0
6 Map接口
6.1 概述
- Map与Collection并列存在。用于保存具有 映射关系的数据:key-value
- Map 中的 key 和 value 都可以是任何引用类型的数据
- Map 中的 key 用Set来存放, 不允许重复,即同一个 Map 对象所对应
的类,须重写hashCode()和equals()方法 - 常用String类作为Map的“键”
- key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到
唯一的、确定的 value - Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和
Properties。其中,HashMap是 Map 接口使用频率最高的实现类
6.2 Map实现类1:HashMap
6.2.1 底层实现原理
-
HashMap的底层实现原理(以jdk 7为例)和HashSet除了情况4不同,其他过程基本相同
(1) HashMap map = new HashMap()
在实例化以后,底层创建了长度是16的一维数组Entry[] table。
(2) map.put(key1, value1)
① 首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
② 如果此位置上数据为空,此时的key1-value1添加成功。 ----情况1
③ 如果此位置上的数据不为空(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:如果key1的哈希值和已经存在的数据的哈希值都不相同,此时key1-value1添加成功 ---- 情况2
如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)
如果equals()返回false:此时key1-value1添加成功 ---- 情况3
如果equals()返回false:使用value1替换value2 ----情况4④补充:关于情况2和情况3:,此时key1-value1和原来的数据以链表的方式存储,在不同的添加过程中,会涉及到扩容问题,默认的扩容方式:当超出临界值(且要存放的位置非空时),扩容为原来的2倍,并将原有的数据复制过来。
-
jdk8 相较于jdk7在底层实现方面的不同:
(1)new HashMap():底层没有创建一个长度为16的数组
(2)jdk 8底层的数组是:Node[],而非Entry[]
(3)首次调用put()方法,底层创建长度为16的数组
(4)jdk7底层结构只有:数组 + 链表。jdk8中底层结构:数组 + 链表 + 红黑树。当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8,且当前数组的长度 > 64时,此时此索引位置上的所有数据改为红黑树存储。
6.2.2 HashMap源码中的重要常量
DEFAULT_INITIAL_CAPACITY
:HashMap的默认容量,16
MAXIMUM_CAPACITY
:HashMap的最大支持容量,2^30
DEFAULT_LOAD_FACTOR
:HashMap的默认加载因子,0.75
TREEIFY_THRESHOLD(桶的树化阙值)
:Bucket中链表长度大于该默认值,转化为红黑树,8
注解:桶(Bucket),对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),不过 Entry 对象可以包含一个引用变量
UNTREEIFY_THRESHOLD(桶的链表还原阙值)
:Bucket中红黑树存储的Node小于默认值,转化为链表,6
MIN_TREEIFY_CAPACITY(最小树形化容量阙值)
:Bucket中的Node被树化时的hash表容量,(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作。这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4
倍。)默认值64
table
:存储元素的数组,总是2的n次幂
entrySet
:存储具体元素的集
size
:HashMap中存储的键值对的数量
modCount
:HashMap扩容和结构改变的次数
threshold
:扩容的临界值,= 容量*填充因子
loadFactor
:填充因子(默认的是DEFAULT_LOAD_FACTOR)
6.3 Map实现类2:LinkedHashMap
6.3.1 说明
- LinkedHashMap 是 HashMap 的子类
- 在HashMap存储结构的基础上,使用了一对双向链表来记录添加
元素的顺序 - 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代
顺序:迭代顺序与 Key-Value 对的插入顺序一致
6.3.2 源码中的存储结构
- HashMap中的内部类:Node
// 只截取了源码中的属性部分
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
- LinkedHashMap中的内部类:Entry
// 重写了HashMap中的newNode方法 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
// LinkedHashMap中的Entry
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
6.4 Map实现类3:TreeMap
类似于TreeSet
-
TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。
TreeMap 可以保证所有的 Key-Value 对处于 有序状态。 -
TreeSet底层使用 红黑树结构存储数据
-
TreeMap 的 Key 的排序:
自然排序
:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException定制排序
:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现
Comparable 接口 -
TreeMap判断 两个key 相等的标准:两个key通过compareT o()方法或
者compare()方法返回0。
6.5 Map实现类4:HashTable
- 像Vector一样,是个古老的Map实现类,JDK 1就提供了,不同于HsahMap,Hashtable是线程安全
- Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
- 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
- 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
- Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
6.6 Map实现类5:Properties
- Properties 类是 Hashtable 的子类,该对象用于处理属性文件
- 由于属性文件里的 key、value 都是字符串类型,所以Properties 里的 key 和 value 都是字符串类型
6.7 Map类的常用方法
- 增
① Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
② void putAll(Map m):将m中的所有key-value对存放到当前map中 - 删
Object remove(Object key):移除指定key的key-value对,并返回value - 改
① Object put(Object key,Object value):将指定key-value修改当前map对象中(当加入key值相同的键值对时会对value进行更新)
② void clear():清空当前map中的所有数据 - 查
① Object get(Object key):获取指定key对应的value
② boolean containsKey(Object key):是否包含指定的key
③ boolean containsValue(Object value):是否包含指定的value
④ int size():返回map中key-value对的个数
⑤ boolean isEmpty():判断当前map是否为空
⑥ boolean equals(Object obj):判断当前map和参数对象obj是否相等 - 遍历
① Set keySet():返回所有key构成的Set集合
② Collection values():返回所有value构成的Collection集合;
③ Set entrySet():返回所有key-value对构成的Set集合
Map map = new HashMap();
//map.put(..,..)省略
System.out.println("map的所有key:");
Set keys = map.keySet();// HashSet
for (Object key : keys) {
System.out.println(key + "->" + map.get(key));
}
System.out.println("map的所有的value:");
Collection values = map.values();
Iterator iter = values.iterator();
while (iter.hasNext()) {
System.out.println(iter.next());
}
System.out.println("map所有的映射关系:");
// 映射关系的类型是Map.Entry类型,它是Map接口的内部接口
Set mappings = map.entrySet();
for (Object mapping : mappings) {
Map.Entry entry = (Map.Entry) mapping;
System.out.println("key是:" + entry.getKey() + ",value是:" + entry.getValue());
}
7 Collections工具类
-
Collections是一个操作Set、List和Map等集合的工具类
-
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
-
排序操作 (均为static方法,针对List)
-
查找、替换
-
同步控制
Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集
合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全
问题,使用的时候只要调用下对应的方法即可,比如
List list1 = Collections.synchronizedList(List);