一方面, 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用Array存储对象方面具有一些弊端,而Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中。
集合与数组的比较:
首先说下数组,
Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。
所以,数组是固定长度的,集合是可变长度的;
数组可以存储基本数据类型和引用数据类型,集合只能存储引用数据类型;
数组存储的元素必须为同一个类型,在声明时就已经决定了类型,而集合存储的类型可以是不一样的;
还有一个方面,数据结构,也就是容器中存储数据的方式。
对于集合容器,有很多种。因为每一个容器的自身特点不同,其原理在于每个容器的内部数据结构不同。
而集合容器在不断向上抽取的过程中,出现了集合体系。在使用一个体系的原则:参阅顶层内容,建立顶层对象。
Iterator迭代器接口
其实Iterator的特点是只能单向遍历,,但更加安全,因为其可以确保,在当前遍历的集合元素被更改的时候,就会抛ConcurrentModificationException异常。
所以,遍历一个list的时候有多少种方法:
1.for循环遍历:基于计数器,在集合外部去维护一个计数器,然后依次读取每一个位置的元素,当读取最后一个元素后停止。
2.迭代器遍历:Iterator,是一个面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java在Collections中支持iterator模式。
3.foreach循环遍历:foreach其实内部也是iterator的实现方式,使用时无需显式声明iterator或者计数器,其优点就是代码整洁,不易出错;缺点是局限性大,只能做简单的遍历,而不能在遍历过程中去操作数据集合,例如删除、替换。
最佳实践:Java Collections框架中提供了一个RandomAccess接口,用来标记List是否支持RandomAccess。这时就需要用instanceof来判断List集合子类是否实现RandomAccess接口。
ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快,所以说,当我们在做项目时,应该考虑到List集合的不同子类采用不同的遍历方式,能够提高性能!
推荐的做法就是,支持RandomAccess的列表可用for循环,否则用Iterator或foreach遍历。
ArrayList集合实现RandomAccess接口有何作用?为何LinkedList集合却没实现这接口?_DriveMan的博客-CSDN博客_arraylist实现了randomaccess
Collection子接口之一: List接口
1.List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
2.List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据 序号存取容器中的元素。
3.JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector。
如何实现数组和List之间的转换:
数组转list:使用Arrays.asList(array)进行转换;
list转数组:使用List自带的toArray()方法。
List实现类之一:ArrayList
ArrayList 是 List 接口的典型实现类、主要实现类
本质上,ArrayList是对象引用的一个”变长”数组
ArrayList的JDK1.8之前与之后的实现区别?
JDK1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组
JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个初始容量为10的数组
说一下ArrayList的优缺点:
优:
1.ArrayList底层以“变长”数组实现,是一种随机访问模式。
2.ArrayList实现了RandomAccess接口,因此查找速度是很快的,而且在遍历时直接用for遍历效果比用迭代器要好;
3.ArrayList在顺序添加一个元素时很方便。
劣:
4.删除元素时,需要做一次元素复制的操作,如果要复制的元素很多,会比较耗费性能;而插入元素也是需要做一次元素复制的操作。
5.所以,ArrayList适合顺序添加,随机访问的场景(没什么特定需求也是用ArrayList用的多)。
List实现类之二:LinkedList
1.对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高
2.LinkedList:双向链表,内部没有声明数组,而是定义了Node类型的first和last, 用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:
prev变量记录前一个元素的位置
next变量记录下一个元素的位置
双向链表:
也叫双链表,是链表的一种,它的每一个数据结点都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表的任意一个结点开始,都可以很方便地访问它的前驱节点和后继节点。
ArrarList和LinkedList的区别:(下面六个角度,可以补充一些代码例子,看当场状态了)
1.数据结构:ArrayList是动态数组的数据结构实现,而LinkedList是双向链表的数据结构,其内部并没有声明数组;
2.随机访问效率:ArrayList比LinkedList在随机访问效率时要好,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找;
3.增加和删除效率:在非首尾的增加和删除操作,LinkedList要比ArrayList效率要高,因为ArrayList增删操作时会影响数组内的其他数据的下标。
4.内存空间占用:LinkedList要比ArrayList占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,另一个指向后一个元素;
5.线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程的安全;
6.使用场景:在需要频繁地读取集合中的元素时,更推荐使用ArrayList,而在插入和删除操作较多时,更推荐用LinkedList。
List 实现类之三:Vector
(回答该题时,想清楚角度分点回答,再概括:线程安全、性能、扩容、综合)
Collection子接口之二: Set接口(无序不可重复)
1.Set接口是Collection的子接口,set接口没有提供额外的方法
2.Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
3.Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
Set实现类之一:HashSet
(这里的难点就是HashSet如何检查重复的?)
底层也是数组,初始容量为16,当如果使用率超过0.75,(16*0.75=12) 就会扩大容量为原来的2倍。(16扩容为32,依次为64,128....等)
Set实现类之二:LinkedHashSet
在添加数据时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。
对于频繁地遍历操作,LinkedHashSet效率高于HashSet
Set实现类之三:TreeSet
1.TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
2.TreeSet底层使用 红黑树 结构存储数据
3.TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
排 序—自然排序
自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列
如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。
实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。
向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较
因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。
对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较返回值。
当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过 equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。 否则,让人难以理解。
排 序—定制排序
定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。
Map接口
Map实现类之一:HashMap(面试的高频,已被问一次)
1.HashMap是 Map 接口使用频率最高的实现类。
2.允许使用null键和null值,与HashSet一样,不保证映射的顺序。
3.所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写: equals()和hashCode()
4.所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
5.一个key-value构成一个entry,所有的entry构成的集合是Set:无序的、不可重复的
6.HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true, hashCode 值也相等。
7.HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
HashMap的存储结构
JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法)
JDK 8版本发布以后:HashMap是数组+链表+红黑树实现。
HashMap的存储结构:JDK 1.8之前
1.HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时, 系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量 (Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个 bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
2.每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。 而且新添加的元素作为链表的head。
3.添加元素的过程:
向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据 key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置i。
如果位置i上没有元素,则entry1直接添加成功。
如果位置i上 已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次 比较entry1中key和其他的entry。
如果彼此hash值不同,则直接添加成功。
如果 hash值相同,继续比较二者是否equals。
如果返回值为true,则使用entry1的value 去替换equals为true的entry的value。
如果遍历一遍以后,发现所有的equals返回都 为false,则entry1仍可添加成功。entry1指向原有的entry元素。
4.HashMap的扩容
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在 HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算 其在新数组中的位置,并放进去,这就是resize。
那么HashMap什么时候进行扩容呢?
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数 size)*loadFactor 时,就会进行数组扩容 ,loadFactor 的默认值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数, 那么预设元素的个数 能够有效的提高HashMap的性能。
HashMap的存储结构:JDK 1.8
1.HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个 HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表 中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为 “桶”(bucket),,每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
2.每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带 一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能 生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象 可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个 TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。
那么HashMap什么时候进行扩容和树形化呢?
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数 size)*loadFactor 时 ,就会进行数组扩容 ,loadFactor 的默认值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数, 那么预设元素的个数能够有效的提高HashMap的性能。
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后, 下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
关于映射关系的key是否可以修改?answer:不要修改
映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算 每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。
1.Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
Object get(Object key):获取指定key对应的value
Java 1.7
和Java 1.8
的HashMap
的HashMap
中的put()
和get()
方法在实现上差异很大:
put()方法源码分析
JDK1.7:
put()
方法实际上是
- 若
key
为null
时,直接调用putForNullKey()
方法。否则进入下一步 - 调用
hash()
方法获取key
的hash
值,进入下一步 - 调用
indexFor()
计算命中的散列表table
的索引 - 遍历链表,如果链表不存在或链表不存在
key
和hash
值相同的节点,则创建新的链表或尾部添加节点,否则替换对应节点的value
putForNullKey()、indexFor()方法、hash()方法、addEntry()方法、createEntry()方法。
JDK1.8:
put()
方法实际上是
- 调用
hash()
方法获取到key
的hash
值 - 调用
putVal()
方法存储key-value
核心方法是putVal()
方法,还有hash()方法、putVal()方法
get()方法源码分析
JDK1.7:
get()方法
的关键点如下:
- 若
key
为null
,则调用getForNullKey()
方法获取value
,否则进入下一步 - 调用
getEntry()
方法获取对应的Entry
对象 - 对应的
Entry
对象为null
时返回null,否则调用getValue()
返回其value
getForNullKey()、getEntry()
JDK1.8:
get()
方法实际上是
- 调用
hash()
方法获取到key
的hash
值 - 调用
getNode()
方法通过key
和hash
获取对应的value
。不存在则返回null
核心方法是getNode()
方法,下面我会先分析一下getNode()
方法。
总结:put()
和get()
方法是HashMap
的常用方法,通过学习其源码了解到HashMap
是如何使用拉链法解决哈希冲突。
比较
既然分析了Java 1.7
和Java 1.8
中HashMap
的put()
和get()
方法,当然少不了对二者的比较:
Java 1.7
的HashMap
中存在很多重复的代码。例如putForNullKey()
和put()
方法中重复的链表遍历,大量重复的hash
值计算逻辑等等。而在Java 1.8
中则对这部分的代码进行了重构。例如将putForNullKey()
和put()
方法重复的代码整合成putVal()
方法,hash()
方法处理key
为null
时的情况。Java 1.8
中的put()
方法会在链表超过树化阈值的时候,将链表转化为红黑树。而Java 1.7
中则只有链表Java 1.7
的链表节点插入为头插法(不需要判断链表是否存在),而Java 1.8
的链表节点插入则为尾插法。Java 1.8
增加了对putIfAbsent()
方法(存在才进行更新)的支持,详情可以看putVal()
中关于onlyIfAbsent
参数的处理逻辑。
上述get()与put()的讨论来自于以下文章:
HashMap剖析之put()和get()方法_zzm_的博客-CSDN博客
2.HashMap的扩容机制:
JDK 1.8之前:
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在 HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算 其在新数组中的位置,并放进去,这就是resize。
那么HashMap什么时候进行扩容呢?
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数 size)*loadFactor 时,就会进行数组扩容 ,loadFactor 的默认值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数, 那么预设元素的个数 能够有效的提高HashMap的性能。
JDK 1.8:
那么HashMap什么时候进行扩容和树形化呢?
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数 size)*loadFactor 时 ,就会进行数组扩容 ,loadFactor 的默认值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数, 那么预设元素的个数能够有效的提高HashMap的性能。
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后, 下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
3.HashMap负载因子,与扩容机制有关;即若当前容器的容量,达到设定最大值,就需要要执行扩容操作
Map实现类之二:LinkedHashMap
Map实现类之三:TreeMap
如何决定使用HashMap还是TreeMap?
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。
然而,假如你需要对一个有序的key集合进行遍历, TreeMap是更好的选择。
Map实现类之四:Hashtable
Map实现类之五:Properties
Collections工具类
1.Collections 是一个操作 Set、List 和 Map 等集合的工具类
2.Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作, 还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
Collection和Collections的区别:
java.util.Collection是一个集合接口(集合类的一个顶级接口)。它提供了对 集合对象 进行基本操作的通用接口方法。Collection接口在Java类库中有很多的具体实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承有List和Set。
Collection则是集合类的一个工具类/帮助类,其中提供了一系列的静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。