集合容器笔记(持续更新)

Collection单列集合

List接口

迭代器iterator

  • 因为Collection继承了Iterator迭代器,所以iterator接口提供了遍历Collection的接口。可以从一个Collection中使用迭代器方法来获取迭代器实例
  • 使用方法
    Iterator的接口定义:
public interface Iterator{
    boolean hasNext();
    Object next();
    void remove();
}

其中:

  • hasNext(): 判断容器内是否还有可供访问的元素
  • next(): 返回迭代器刚用过的元素引用,返回Object,需要强转为具体的类型 remove(): 删除迭代器刚越过的元素

使用方法代码如下:

List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String s = iterator.next();
    if ("HelloWorld".equals(s)) {
        iterator.remove();    
    }
}
  • 因为Iterator是单项遍历的,所以更加安全,它可以确保在当前遍历的集合元素被更改的时候抛出ConcurrentModificationException异常

Iterator和ListIterator有什么区别

  • Iterator可以遍历Set和List集合,而ListIterator只能遍历List。
  • Iterator只能单项遍历,而ListIterator可以双i想遍历
  • ListIterator实现了Iterator接口,添加了额外的功能,比如添加元素,替换元素,获取前面或者后面元素的索引

一个List的几种遍历方式

  • for循环遍历:在集合外部维护一个计数器,依次读取每一个位置的元素,读到最后一个元素位置
  • 迭代器Iterator遍历:Iterator为OOP的一个设计模式,目的是为了屏蔽不同集合的特点,统一遍历集合的接口
  • foreach遍历:foreach内部也是采用了Iterator的方式实现,使用时不用显示声明Iterator或者计数器,优点为代码简洁,缺点是功能简单,不能再遍历时操作数据集合,如,增加、删除等操作。

ArrayList的优缺点

优点

  • ArrayList底层为数组,采取随机访问模式,因此查询速度很快,并且顺序添加很方便

缺点

  • 删除,插入操作采用元素复制操作,如果元素非常多,性能将十分低下

使用场景

  • 适合随机访问,顺序添加删除的场景

List和数组之间的转换

  • 数组转换为List:Arrays.asList(array)
  • List转换为Array:使用List自带的toList()方法

ArrayList和LinkedList区别

ArrayList底层是动态数组,LinkedList底层实双向链表

  • 随机访问效率:ArrayList比LinkedList在随机访问时的效率要高,因为LinkedList是线性存储,在查找时需要移动指针从前往后依次进行
  • 增叫删除效率:在非首尾的增删操作上,LinkedList比AraayList效率要高,因为ArrayList采用移动复制元素的操作,会影响到其他数组内元素的下标
  • 内存空间占用差别:LinkedList比ArrayList更占空间,LinkedList的节点不仅要存储数据,还要存储两个引用,一个指向直接前驱元素,一个指向直接后驱元素
  • 线程安全:两个都是不同步的,即两个都是线程不安全的
    总结:ArrayList适合读取比较多的场景,LinkedList适合插入删除操作比较多的场景

ArrayList和Vector的区别

  • 两个类都实现了List接口(List继承了Collection接口),都是有序集合
    线程安全:Vector使用了synchronized来实现线程同步,是线程安全的,ArrayList是线程不安全的
  • 性能:因为Vector是线程安全的,所以性能低于ArrayList
    扩容:ArrayList和Vector都能根据实际情况动态扩容,不过Vector每次增加一倍,ArrayList每次增加50%

总结:在不需要线程安全时,使用ArrayLsit以保证效率

多线程下使用ArrayList

  • ArrayList线程不安全,如果要在多线程场景下使用,可用通过Coolections的synchronizedList方法将其转换为线程安全的容器再使用:如
ArrayList<String > list = new ArrayList<>();
list.add("Hello");
list.add("World");
List<String> synArrayList = Collections.synchronizedList(list);
for (String s : synArrayList) {
    System.out.println(s);
}

List和Set区别

  • List和Set都是继承于Collection接口
  • List有序(元素存入顺序和取出顺序一致),元素可重复,可以插入多个null元素,元素有索引,常用实现类有ArrayList、LinkedList和Vector;
  • Set无序,元素不可重复,只允许存入一个null元素,必须保证元素唯一性,常用实现类为HshSet、LinkedHashSet和TreeSet
  • List支持for循环,即通过下标遍历,也支持迭代器遍历;而Set只能使用迭代器遍历,因为无序,无法使用下标来遍历

效率:

  • Set:检索效率低,增删效率高,增删不会影响元素位置改变
  • List:长度动态增长,检索效率高,增删效率低,因为增删会影响元素位置改变

Set接口

HashSet的实现原理
先看一下部分源码

private transient HashMap<E,Object> map;

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

public HashSet() {
    map = new HashMap<>();
}
public boolean add(E e) {
    //实际上调用的HsahMap的put方法,present实际上为一个一直相同的虚值
    return map.put(e, PRESENT)==null;
}
  • HashSet是基于HashMap实现的,HashSet的值存放在HashMap的Key上,HashMap的value统一为present,因此HashSet的实现比较简单,相关HashSet的操作基本上都是直接调用底层的HashMap的相关方法来完成,并且HshSet不允许重复的值。

HashSet如何检查重复?HashSet是如何保证数据不重复的?

  • 在HashSet中add元素时,判断元素是否存在的依据,不仅要比较Hash值,还要结合equals()方法比较
  • HashMap比较key是否相等,是先比较hashCode,再比较equals
  • HashSet的add()实际上时HashMap的put()方法。HashMap中的key是唯一的,由源码可以看出HashSet中add()的value实际上是HashMap中put的key,在HashMap中如果K,V相同,则新V会覆盖旧V,并返回旧V
    ,所以不会重复

hashCode()和equals的相关说明

  • hashcode:为jdk根据对象的地址或字符串或数字算出来的int类型的数值
    如果两个对象相等,则hashcode一定是相等的;但如果两个对象hashcode值相等,对象不一定相等
  • 如果两个对象相等,则equals返回为true
  • equals方法被覆盖,hashCode也必须被覆盖
  • hashCode()的默认行为是对在堆上的对象产生独特值,如果没有重写hashCode(),则该class的两个对象无论如何都不会相等

HashMap与HashSet的区别

HashMapHashSet
实现Map接口实现Set接口
存储键值对仅存储对象
实现Map接口实现Set接口
使用put()添加元素使用add()添加元素
HashMap使用Key计算hashcode值HashSet使用成员对象来计算hashcode值 ;如果两个对象hashcode值相同,则使用equals()来判断相等性
HashMap使用唯一key来获取value,因此速度相较于HashSet更快相较于HashMap,HashSet更慢

Map

HashMap的实现原理

  • HashMap是基于哈希表的Map接口的非同步实现,此实现提供所有可选的映射操作,并且允许null值和null键。HashMap不保证映射的顺序,特别是不保证顺序恒久不变;

  • HashMap的底层数据结构是链表散列,即链表加数组,主要是数组,链表是为了解决hash冲突;

  • HashMap基于Hash算法实现:
    a) 在往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象元组在数组中的下标;
    b) 存储时如果存在hashCode相同的key,有两种情况:

     i) 如果key相同,则覆盖原值;
     ii) 如果key不同,则将当前的key-value放入链表中(解决hash冲突);
    

获取HashMap值时,通过hash值找到对应下标,再进一步判断key是否相同,从而找到对应值;

  • HashMap解决hash冲突的关键就在于使用数组加链表的方式存储数据。主要使用数组存储数据,然后将冲突的key的对象放入链表中做进一步对比;

附:jdk1.8之后对HashMap的实现进行优化,当链表的容量超过8个之后,链表会转换成红黑树来提高查询效率,后面会详细说明。

HashMap在jdk1.7和1.8的底层实现及如何解决hash冲突

在JDK1.8前采用拉链法解决哈希冲突

  • hash冲突:不同的key可能对应着相同的hash值,这就是hash冲突
    拉链法:即将数组和链表结合起来,发挥各自的优势(数组的查询速度快,链表的增删速度快)。方法为创建一个数组链表,数组的每一个元素就是一个链表,如果发生哈希冲突,将冲突的值加进链表

在JDK1.8后的方法

  • 当链表长度大于阈值(默认8)时,将链表转换成红黑树,以减小搜索时间
    HashMap在JDK1.8与JDK1.7比较
    JDK优化了以下地方
    1. resize扩容优化
    2. 引入红黑树,避免单条链表过长而影响效率
    3. 解决多线程死循环问题,但仍然是线程不安全的
JDK1.7JDK1.7
数据结构数组+链表数组+链表+红黑树
初始化方式单独函数:inflateTable()直接集成到扩容函数resize()
hash计算方式扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
数据存储规则无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突&链表长度<8,存放单链表;冲突&链表长度<8,树化存放在红黑树
数据插入方式头插法尾插法
扩容后存储位置的计算方式按原来方式计算按扩容规则计算
posted @ 2021-07-25 22:12  TellMeYourStory  阅读(40)  评论(0)    收藏  举报