JavaSE复习:集合

        一方面, 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用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第一对映射关系时,系统会创建一个长度为initialCapacityNode数组,这个长度在哈希表 中被称为容量(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.7Java 1.8HashMapHashMap中的put()get()方法在实现上差异很大:

put()方法源码分析

JDK1.7:

put()方法实际上是

  1. keynull时,直接调用putForNullKey()方法。否则进入下一步
  2. 调用hash()方法获取keyhash值,进入下一步
  3. 调用indexFor()计算命中的散列表table的索引
  4. 遍历链表,如果链表不存在或链表不存在keyhash值相同的节点,则创建新的链表或尾部添加节点,否则替换对应节点的value

putForNullKey()、indexFor()方法、hash()方法、addEntry()方法、createEntry()方法。

JDK1.8:

put()方法实际上是

  1. 调用hash()方法获取到keyhash
  2. 调用putVal()方法存储key-value

核心方法是putVal()方法,还有hash()方法、putVal()方法

get()方法源码分析

JDK1.7:

get()方法的关键点如下:

  1. keynull,则调用getForNullKey()方法获取value,否则进入下一步
  2. 调用getEntry()方法获取对应的Entry对象
  3. 对应的Entry对象为null时返回null,否则调用getValue()返回其value

getForNullKey()、getEntry()

JDK1.8:

get()方法实际上是

  1. 调用hash()方法获取到keyhash
  2. 调用getNode()方法通过keyhash获取对应的value。不存在则返回null

核心方法是getNode()方法,下面我会先分析一下getNode()方法。

总结:put()get()方法是HashMap的常用方法,通过学习其源码了解到HashMap是如何使用拉链法解决哈希冲突。

比较

既然分析了Java 1.7Java 1.8HashMapput()get()
方法,当然少不了对二者的比较:

  1. Java 1.7HashMap中存在很多重复的代码。例如putForNullKey()put()方法中重复的链表遍历,大量重复的hash值计算逻辑等等。而在Java 1.8中则对这部分的代码进行了重构。例如将putForNullKey()put()方法重复的代码整合成putVal()方法,hash()方法处理keynull时的情况。
  2. Java 1.8中的put()方法会在链表超过树化阈值的时候,将链表转化为红黑树。而Java 1.7中则只有链表
  3. Java 1.7的链表节点插入为头插法(不需要判断链表是否存在),而Java 1.8的链表节点插入则为尾插法。
  4. 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则是集合类的一个工具类/帮助类,其中提供了一系列的静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值