1. 总揽全局
java集合包括 Collection接口 和 Map接口,其中Collection接口包括 LIst接口 和 Set接口 ,最后还有也别重要的 Iterator迭代器接口 和 Collections工具类
1.1 Java集合的引出
面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象进行操作,就要对对象进行存储(这里的存储,主要指的是内存层面的存储,不涉及持久化存储)。其中,使用Array(数组)也可以存储对象,但是具有一些弊端 :
(1)数组在内存存储方面的特点
- 数组初始化以后,长度就确定了
- 数组声明的类型,就决定了进行元素初始化时的类型(优点)
(2)数组在存储数据方面的弊端
- 数组初始化以后,长度就不可变了,不便于扩展
- 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。同时无法直接获取存储的实际元素的个数(也就是无属性、方法可用于获取存储的实际元素的个数)。
- 数组存储的数据是有序的、可以重复的。对于无序、不可重复的需求无法满足。
而Java集合可以动态的把多个对象的引用放如容器中,可以用于存储数量不等的多个对象,还可以用于保存具有映射关系的关联数组。 解决了数组存储数据方面的弊端
1.2 框图
- Collection
- Map
2. Collection接口
2.1 简介
Collection接口是List、Set 、Queue 等接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List 和 Queue 集合。
【注】:JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:List、Set)实现;从JDK 5.0 增加了泛型以后,Java集合可以记住容器中对象的数据类型。
2.2 常用方法
3. iterator 迭代器接口
3.1 简介
3.2 iterator接口常用方法
3.2.1 hasNext() 和 next() 方法
在调用 it.next() 方法之前需要调用 it.hasNext() 进行检测。若不调用,且下一条记录无效,则直接调用 it.next() 会抛出 NoSuchElementException 异常。
3.2.2 remove() 方法
- iterator 可以删除集合的元素,但是遍历过程之中通过迭代器对象的 remove 方法,不是集合对象的 remove 方法。
- 如果还未调用next() 或在上一次调用next() 方法之后已经调用了 remove 方法,再调用 remove 都会报 IllegalStateException 。
3.3 使用foreach循环遍历集合元素
增强for循环:
遍历集合的底层调用iterator完成操作
【注】:foreach不只可以遍历集合,还可以遍历数组。
来道面试小练习!!!!
结果:
st
st
st
st
st
null
null
null
null
null
4. List接口
4.1 概述
鉴于java中数组用来存储数据的局限性,我们通常使用List代替数组。List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引,可以根据索引存取容器中的元素。又因其可以根据元素多少,自动扩容,可以称其为“动态数组”。List接口的实现类常用的有:ArrayList、LinkedList、Vector(Vector的子类为Stack)。
4.2 List接口方法
4.3 LIst实现类
4.3.1 ArrayList
ArrayList 是 LIst 接口的典型实现类、主要实现类,本质上是对象引用的一个“变长”数组(动态数组)。它是线程不安全的,所以效率高;底层使用Object[ ] elementData 存储。
ArrayList的JDK1.8之前与之后的实现区别?
- JDK1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组;
list.add(123); 相当于 elementData[0] = new Integer(123);
若容量不够,默认扩容为原来的1.5倍,同时需要将原有数组中的数据复制到新的数组中。 - JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个初始容量为10的数组;
后续的添加和扩容操作与JDK1.7无异。
【注】:
- 在工具类Arrays中有Arrays.asList()方法返回的是一个固定长度List集合,不能再添加,否则报java.lang.UnsupportedOperationException;
- 建议开发中使用带参的构造器,直接输入容量,可以防止因扩容带来的时间复杂度增大;
- 调用 Collections.sychronizedList(list) 方法可以让ArrayList转变成线程安全的。
4.3.2 LinkedList
对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储。
新增方法:
4.3.3 Vector
作为List接口的古老实现类,它是线程安全的,所以效率低;底层使用Object[ ] elementData 存储。
在各种 list 中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。
新增方法:
4.4 来一个面试题
结果:
[1,2]
//因为当添加时,add方法只有一个,所以参数会自动装箱,但是当调用remove时,remove方法有两个(Collection、List),一般能不装箱就不装箱,所以这里的2是索引。
5. Set 接口
5.1 概述
Set 接口是Collection接口的子接口,它没有额外的方法,且不允许包含相同的元素。Set集合判断两个对象是否相同不是使用 == 运算符,而是根据equals()方法。
5.2 Set的实现类
5.2.1 HsahSet
HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的 存取、查找、删除性能 。
特点:
- 不保证元素的排列顺序
- HashSet不是线程安全的
- 集合元素可以为null
HashSet 集合判断两个元素相等的标准:
两个对象通过 hashCode () 方法比较相等,并且两个对象的 equals() 方法返回值也相等 。 因此对于存放在 Set 容器中的对象,对应的类一定要重写 equals 和 hashCode(Object obj) 方法,以实现对象相等规则 。即:“相等的对象必须具有相等的散列码 。
向HashSet中添加元素的过程:
- 当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置 。( 这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布该散列函数设计的越好)
- 如果两个元素的 hashCode() 值相等,会再继续调用 equals 方法,如果equals 方法结果为 true,添加失败;如果 为 false,那么会保存该元素,但是该数组的位置已经有元素了,那么会通过链表的方式继续链接 。
【注】:如果 两个元素的 equals() 方法返回 true ,但它们的 hashCode () 返回值不相等, hashSet 将会把它们存储在不同的位置,但依然可以添加成功 。
重写equals()方法的基本原则:
向Set中添加的数据,其所在类一要重写hashCode() 和 equals() 方法,重写 equals 方法的时候一般都需要同时复写 hashCode 方法 。 通常参与计算 hashCode 的对象的属性也应该参与到 equals() 中进行计算。当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。
5.2.2 LinkedHashSet
LinkedHashSet 是 HashSet 的子类,它是根据元素的 hashCode 值来决定元素的存储位置,但同时使用双向链表维护元素的次序。因此,LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
5.2.3 TreeSet
TreeSet 是 SortedSet 接口的实现类,可以确保集合元素处于排序状态,其底层使用红黑树结构存储数据。
特点:
- 向TreeSet中添加的数据,要求是相同类的对象。
- 可以按照添加对象的指定属性进行排序。
- 有序,查询速度比List快
TreeSet 两种排序方式:自然排序(实现Comparable)、定制排序(实现Comparator)(默认情况下采用自然排序)。
自然排序:
定制排序:
【注】:对于 TreeSet 集合而言,它 判断两个对象是否相等的唯一标准是:两个对象通过 compareTo (Object obj ) 方法比较返回值。当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo (Object obj ) 方法有一致的结果:如果两个对象通过equals() 方法比较返回 true ,则通过 compareTo (Object obj ) 方法比较应 返回 0 。否则,让人难以理解。
5.3面试题
结果:
6. Map接口
6.1 概述
Map接口 与 Collection接口 并列存在,用于保存具有 映射关系 的数据 :key value。key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value。key 和 value可以是任何引用类型的数据,其中key 用 Set 来存放(无序的、不可重复的),具体使用什么Set要看是什么Map,即 key 对象所对应的类须重写 hashCode 和 equals 方法(以HashMap为例);value用Collection存放(无序的、可重复的),即 value 所在类要重写 equals();一 个 key、value 对构成一 个 entry 对象,所有的 entry 构成的集合是 Set(无序的、不可重复的)。
6.2 常用方法
6.3 Map实现类
6.3.1 HashMap
1.简介
HashMap作为Map的主要实现类,java1.2版本出现,其线程是不安全的,但是效率高。允许使用 null 键和 null 值,与 HashSet 一样,不保证映射的顺序。
2.存储结构
-
JDK 7及以前版本: HashMap 是数组+链表结构 (即为链地址法)
-
JDK 8版本发布以后: HashMap 是数组+链表+红黑树实现。
JDK 7及以前版本:
实例化: 当实例化一个 HashMap 时,系统会创建一个长度为 Capacity 的 Entry 数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为 “桶”(bucket),每个bucket 都有自己的索引,系统可以根据索引快速的查找 bucket 中的元素 。每个 bucket 中存储一个元素,即一个 Entry 对象。但每一个 Entry 对象可以带一个引用变量,用于指向下一个元素。因此,在一个桶中就有可能生成一个 Entry 链 。而且新添加的元素作为链表的 head 。
添加元素: 向HashMap 中添加 entry1 (key,value) 需要首先计算 entry1 中 key 的哈希值 (根据 key 所在类的 hashCode() 计算得到),此哈希值经过处理以后,得到在底层 Entry[] 数组中要存储的位置 i 。如果位置 i 上没有元素,则 entry 1 直接添加成功 。如果位置 i 上已经存在 entry2(或还有链表存在的 entry3,entry4),则需要通过循环的方法,依次比较 entry1 中 key 和 其他 entry 的。如果彼此 hash 值不同,则直接添加成功 。 如果 hash 值不同,继续比较二者是否 equals 。如果返回值为 true,则使用 entry1 的 value 去替换 equals 为 true 的 entry 的 value 。如果遍历一遍以后,发现所有的 equals 返回都为 false, 则 entry1 仍可添加成功 。 entry1 指向原有的 entry 元素 。
HashMap扩容: 当 HashMap 中的元素越来越多的时候,hash 冲突的几率也就越来越高, 因为数组的长度是固定的 。 所以为了提高查询的效率,就要对 HashMap 的数组进行扩容,而在 HashMap 数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是 resize 。
什么时候进行扩容呢? : 当 HashMap 中的元素个数超过 数组大小×loadFactor 时,就会进行数组扩容,loadFactor 的默认值为0.75,这是一个折中的取值 。默认情况下数组大小为 16 那么当 HashMap 中元素个数超过 16×0.75=12(这个值就是代码中的 threshold 值),且要存放的位置非空时,就把数组的大小扩展为 2×16=32 ,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能 。JDK 8版本:
实例化: 当实例化一个 HashMap 时,会初始化 initialCapacity 和 loadFactor,在 put 第一对映射关系时,系统会创建一个长度为initialCapacity 的 Node 数组,这个长度在哈希表中被称为容量 Capacity,在这个数组中可以存放元素的位置我们称之为 “桶”(bucket),每个 bucket 都有自己的索引,系统可以根据索引快速的查找 bucket 中的元素 。
添加元素: 每个 bucket 中存储一个元素,即一个 Node 对象,但每一个 Node 对象可以带一个引用变量 next,用于指向下一个元素。因此,在一个桶中,就有可能生成一个 Node 链 ,也可能是一个 TreeNode 对象。每一个 TreeNode 对象可以有两个叶子结点 left 和 right,因此在一个桶中,就有可能生成一个 TreeNode 树 。而新添加的元素作为链表的 last 或 树的叶子结点 。
HashMap 什么时候进行扩容呢?: 当HashMap 中的元素个数超过数组大小×loadFactor 时,当 HashMap 中元素个数超过 16×0.75=12(这个值就是代码中的 threshold 值),且要存放的位置非空时,就把数组的大小扩展为 2×16=32 ,即扩大一倍,然后重新计算每个元素在数组中的位置。
HashMap 什么时候进行树形化呢?: 当 HashMap 中的其中一个链的对象个数如果达到了 8 个,此时如果 capacity 没有达到 64 ,那么 HashMap 会先扩容解决,如果已经达到了 64 ,那么这个链会变成树,结点类型由 Node 变成 TreeNode 类型。当然,如果当映射关系被移除后,下次 resize 方法时判断树的结点个数低于 6 个,也会把树再转为链表。
关于映射关系的 key 是否可以修改 answer :不要修改:映射关系存储到 HashMap 中会存储 key 的 hash 值,这样就不用在每次查找时重新计算每一个 Entry 或 Node (TreeNode )的 hash 值了,因此如果已经 put 到 Map 中的映射关系,再修改 key 的属性,而这个属性又参与 hashcode 值的计算,那么会导致匹配不上。
6.3.2 LinkedHashMap
LinkedHashMap 是 HashMap 的子类,java1.4版本出现,在遍历map元素时,可以按照添加的顺序实现遍历,其顺序遍历是因为双向链表结构实现,所以对于频繁的遍历操作,此类执行效率高于HashMap。
6.3.3 TreeMap
java1.2版本出现,按照添加的键值对进行排序,实现排序遍历,此时考虑key的自然排序或定制排序。底层使用的是红黑树。
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出ClasssCastException !
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口 !
TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
6.3.4 Hashtable
作为古老的实现类,java1.0版本出现,其线程是安全的,所以效率低,不能存储值为 null 的 key 和 value。Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。Hashtable判断两个key相等、两个value相等的标准也与HashMap一致。
6.3.5 Properties
Properties 是 Hashtable 的子类,常用来处理配置文件。key 和 value 都是 String 类型。存取数据时,建议使用setProperty(String key,String value)方法和 getProperty(String key)方法
6.4 面试题
1.谈谈你对HashMap中put/get方法的认识?如果了解再谈谈HashMap的扩容机制?默认大小是多少?什么是负载因子(或填充比)?什么是吞吐临界值(或阈值、threshold)?
答:该问题上面都有介绍,这里补充一点,吞吐临界值(或阈值、threshold) = 数组大小×loadFactor !
7 Collections工具类
Collections 是一个操作 Set、List 和 Map 等集合的工具类,它提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。
7.1 常用方法
7.2 同步控制
Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题