Java集合(List-Set、Map)

Java集合概述

一、java集合类型分为两种:Collection、Map,它们是 Java 集合的根接口,这两个接口又包含了一些子接口或实现类
其中List、Set的顶级父类接口是java.util.Collection

Colleciton是一个泛行接口 public interface Collection<E> extends Iterable<E>,它是java提供的一种容器。集合的长度是可变的,集合存储的都是对象,而且对象的类型可以不一致。

如下图所示,为Colletion集合、Map集合整体关系图:
在这里插入图片描述在这里插入图片描述

在这里插入图片描述

.
.---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
线程安全系列集合:如下图所示:
在这里插入图片描述
在这里插入图片描述

特点总结

List系列集合:添加的元素是有序、可重复、有索引。

ArrayList、LinekdList :有序、可重复、有索引。

Set系列集合:添加的元素是无序、不重复、无索引。

HashSet: 无序、不重复、无索引;LinkedHashSet: 有序、不重复、无索引。

TreeSet:按照大小默认升序排序、不重复、无索引。
————————————————

Colletion集合

三种遍历方式

方式一:迭代器

迭代器在 Java 中的代表是 Iterator ,迭代器是集合的专用遍历方式。

迭代器遍历集合。
    -- 方法:
         public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素的
         boolean hasNext():判断是否有下一个元素,有返回true ,反之。
         E next():获取下一个元素值!
    --流程:
        1.先获取当前集合的迭代器
            Iterator<String> it = lists.iterator();
        2.定义一个while循环,判断一次取一次。
          通过it.hasNext()询问是否有下一个元素,有就通过
          it.next()取出下一个元素
 注意:之所以是【取下一个元素】,是因为iterator的起始位置是-1、值为null,而不是起始位置为0下标的值

方式二:foreach(增强for循环):既可以遍历集合也可以遍历数组

foreach遍历集合实际上是迭代器遍历集合的简化写法。
for(元素类型 变量名 : 集合或数组){}

方式三:Lambda表达式遍历集合
Lambda只不过是简化操作,用的是foreach(new Consumer<E>)
例如:

public class IteratorDemo {
    public static void main(String[] args) {
        ArrayList<String> arr=new ArrayList();
        arr.add("xxx");
        arr.add("ppp");
        arr.add("kkk");
        System.out.println(arr);
        //Lanmda表达式简化后的写法
        arr.forEach((String s) -> System::println);
    }
}

共性方法

        //多态的写法
        Collection<Integer> st =new ArrayList<>();
        for(int i =0; i<6; i++)
            st.add(i);//往集合中添加元素
        System.out.println(st);
        //删除给定的对象元素
        System.out.println(st.remove(3));
        //判断时候否为空
        System.out.println(st.isEmpty());
        //判断时候包含指定的某个元素
        System.out.println(st.contains(2));
        //集合的大小
        System.out.println(st.size());
        //将集合转换成数组
        Object [ ]arry =st.toArray();
       for(int i =0;i< arry.length;i++){
           System.out.print(arry[i]+" ");
       }
       System.out.println();
       //清空集合元素
        st.clear();
        System.out.println(st.isEmpty());

结果如下:
在这里插入图片描述

List集合

特点

List集合是Collection集合的子接口,特点如下:

ArrayList、LinekdList :有序,可重复,有索引。
有序:存储和取出的元素顺序一致
有索引:可以通过索引操作元素
可重复:存储的元素可以重复

为何ArrayList查询快、增删元素相对较慢

因为ArrayList底层是数组实现的,根据下标查询不需要比较, 查询方式: 首地址+(元素长度*下标),基于这个位置读取相应的字节数就可以了,所以非常快;
.
增删会带来元素的移动,增加数据会向后移动,删除数据会向前移动,所以影响效率。
相反,在添加或删除数据的时候,LinkedList只需改变节点之间的引用关系,这就是LinkedList在添加和删除数据的时候通常比ArrayList要快的原因

并发修改异常问题

问题存在的场景:

迭代器遍历集合且直接用集合删除元素的时候可能出现。
增强 for 循环遍历集合且直接用集合删除元素的时候可能出现。

1.迭代器删除集合元素:删除的时候不能用集合的remove方法,只能用迭代器自带的remove方法!

  // 需求:删除全部的Java信息。
        // a、迭代器遍历删除
        Iterator<String> it = list.iterator();
        while (it.hasNext()){
            String ele = it.next();
            if("Java".equals(ele)){
                // list.remove(ele); // 集合删除会出毛病,抛异常
                it.remove(); // 删除迭代器所在位置的元素值(没毛病)
            }

2.foreach迭代删除:foreach不能边遍历边删除,会出bug

// b、foreach遍历删除 (会出现问题,这种无法解决的)
       for (String s : list) {
           if("Java".equals(s)){
               list.remove(s);
            }
       }

3.Lambda表达式删除:同上所述

ArrayList扩容机制

ArrayList扩容的本质就是计算出新的扩容数组的size后通过Arrays.copyOf()实例化,并将原有数组内容复制到新数组中去。
扩容大小是原来1.5倍进行扩容
ArrayList扩容底层调用了Arrays.copyOf(),再往深看源码,最底层调用了System.arraycopy()方法

安全问题

ArrayList和LinkedList都是线程不安全的,不安全的原因在于在add()方法的时候,源码如下所示:
在这里插入图片描述
黄色框里的并不是一个原子操作,它由两步操作构成:

elementData[size] = e;
size = size + 1;

举个例子:

假设 size = 0,我们要往这个数组的末尾添加元素
线程 A 开始添加一个元素,值为 A。此时它执行第一条操作,将 A 放在了数组 elementData 下标为 0 的位置上
接着线程 B 刚好也要开始添加一个值为 B 的元素,且走到了第一步操作。此时线程 B 获取到的 size 值依然为 0,于是它将 B 也放在了elementData 下标为 0 的位置上
线程 A 开始增加 size 的值,size = 1
线程 B 开始增加 size 的值,size = 2

ArrayList 的线程安全版本是 Vector,它的实现很简单,就是把所有的方法统统加上 synchronized:
在这里插入图片描述
既然它需要额外的开销来维持同步锁,所以理论上来说它要比 ArrayList 要慢

为什么线程不安全还要用它呢?

因为在大多数场景中,查询的情况居多,不会涉及太频繁的增删。那如果真的涉及频繁的增删,可以使用LinkedList,底层链表实现,为增删而生。而如果你非得保证线程安全那就使用 Vector。当然实际开发中使用最多的还是 ArrayList,虽然线程不安全、增删效率低,但是查询效率高啊。

线程安全CopyOnWriteArrayList

ArrayList的一个线程安全的变体,与ArrayList的使用方式一样
写有锁,读无锁,读写之间不阻塞,优于读写锁
写入时,先copy一个容器副本、再添加新元素,最后替换引用

public class TestCopyOnWriteArrayList {
	public static void main(String[] args) {
		CopyOnWriteArrayList<String> alist = new CopyOnWriteArrayList<String>();
		//写操作有锁
		alist.add("A");//将底层数组做一次复制,写的是新数组,完成赋值后将新替换为旧
		alist.add("B");//每调用一次,底层方法扩容一次
		//读操作无锁
		System.out.println(alist.get(1));//读是写操作完成之前的旧数组写完后才能读到值
	}
}

Set集合

public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable

特点

1. Set 系列集合的特点。

无序、不重复、无索引。

2. Set 集合的实现类特点。

HashSet 无序、不重复、无索引。
LinkedHashSet 有序 、不重复、无索引。
TreeSet 可排序 、不重复、无索引。
Set 集合的功能上基本上与 Collection 的 API 一致。

HashSet

HashSet是Set接口的实现类

底层原理

HashSet 集合底层采取 哈希表 存储的数据。
哈希表是一种对于增删改查数据性能都较好的结构。
JDK8 之前的,底层使用 数组 + 链表 组成
JDK8 开始后,底层采用 数组 + 链表 + 红黑树 组成。
结论:哈希表是一种对于增删改查数据性能都较好的结构
当挂在元素下面的数据过多时,查询性能降低,从 JDK8 开始后,当链表节点超 8 、数组元素超64的时候,链表会自动转换为红黑树。

哈希表实现流程

创建一个默认长度16,默认加载因子0.75的table数组
根据元素地址的高低16位进行异或运算 ^
根据地址计算得到的值再与数组长度-1 进行 & 运算,计算出元素在数组上的索引下标
判断当前位置是否为 null ,如果是 null 直接存入,
如果位置不为 null ,表示有元素, 则调用 equals 方法比较属性值,如为ture,则不存,如为false,则存入数组。
当数组存满到 16*0.75=12 时,就自动扩容,每次扩容原先的两倍

LinkedHashSet

特点

有序 、不重复、无索引。
这里的有序指的是保证存储和取出的元素顺序一致
原理 :底层数据结构是依然哈希表,只是每个元素又额外的多了一个双链表的机制记录存储的顺序

TreeSet

特点

不重复、无索引、可排序
可排序:按照元素的大小默认升序(有小到大)排序。
TreeSet 集合底层是基于 红黑树的数据结构实现排序的,增删改查性能都较好。
注意: TreeSet 集合是一定要排序的,可以将元素按照指定的规则进行排序。

默认排序

对于数值类型: Integer , Double ,官方默认按照大小进行升序排序。(对于Double类型,Double.compare(x1,x2))
对于字符串类型:默认按照首字符的编号升序排序。(用String中的实例方法string.compareT(string))
对于自定义类型如 Student 对象, TreeSet 无法直接排序。
结论:想要使用TreeSet存储自定义类型,需要制定排序规则

自定义排序

TreeSet 集合存储对象的的时候有 2 种方式可以设计自定义比较规则

方式一: 让自定义的类(如学生类)实现Comparable接口重写里面的compareTo方法来定制比较规则。
方式二: TreeSet集合有参数构造器,可以设置Comparator接口对应的比较器对象,来定制比较规则。

Set<Apple> apples = new TreeSet<>(new Comparator<Apple>() {
            @Override
            public int compare(Apple o1, Apple o2) {
                // return o1.getWeight() - o2.getWeight(); // 升序
                // return o2.getWeight() - o1.getWeight(); // 降序
                // 注意:浮点型建议直接使用Double.compare进行比较
                // return Double.compare(o1.getPrice() , o2.getPrice()); // 升序
                return Double.compare(o2.getPrice() , o1.getPrice()); // 降序
            }
        });

线程安全CopyOnWriteSet

线程安全的Set,底层使用CopyOnWriteList实现
不同set的是add底层使用addIfAbsent()添加元素,会遍历数组
如果存在元素,则不添加

public class TestCopyOnWriteArraySet {
	public static void main(String[] args) {	
		//无序、无下标、不允许重复
		CopyOnWriteArraySet<String> aset = new CopyOnWriteArraySet<String>();
		//写操作时,add方法底层是用的CopyOnWriteArrayList的addIfAbsent()来判断要插入的新值是否存在
		aset.add("A"); 
		aset.add("B");
		aset.add("C");
		for(String s : aset) {
			System.out.println(s);
		}
	}
}

Map集合

特点

HashMap:元素按照键是无序,不重复,无索引,值不做要求。 ( 与 Map 体系一致 )
LinkedHashMap:元素按照键是 有序,不重复,无索引,值不做要求。
TreeMap :元素按照建是 排序 ,不重复,无索引的, 值不做要求。

遍历方式

方式一 : 键找值的方式遍历:先获取Map集合全部的键,再根据遍历键找值。
方式二: 键值对的方式遍历 ,把“键值对“看成一个整体, 难度较大。
方式三:Lambda表达式

HashMap

HashMap是Map集合的实现类,底层数据结构实现(JDK1.7)是一个哈希表(数组+链表)
在JDK1.8中,HashMap底层数据结构实现为哈希表+红黑树,目的:提高性能
在jdk1.8中,当链表长度超8,并且数组元素超64,则链表就会转为红黑树,提高查询性能

JDK1.7-1.8版本比较

结构不同
HashMap在1.7中是以数组+链表的形式存在的, 如图所示
在这里插入图片描述

而HashMap在1.8中则是以数组+链表+红黑树构成的, 当一个节点的链表长度超过8并且数组长度超过64时会将链表转换为红黑树, 如图:
在这里插入图片描述

扩容时间不同

在1.7中, hashmap会先检测是否需要扩容之后再往里面放数据, 而在1.8中hashmap会先把数据放进去在检测是否需要扩容

put插入方式不同

在1.7中, hashmap调用put()方法插入时时采用的是头插法, 并且因为hashmap不是线程安全的, 所以当并发插入并触发扩容时可能会把数组内部的链表变成循环链表, 造成死循环的问题
.
在1.8中, hashmap的put()方法改为了尾插法插入, 因此解决了1.7中并发扩容造成的循环链表问题,但是实际上在往红黑树内部并发插入时也有可能会造成两个父节点相互引用而导致的死循环问题

HashMap的Put⽅法的⼤体流程:

1. 根据Key通过哈希算法与与运算得出数组下标

2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是
Node对象)并放⼊该位置

3. 如果数组下标位置元素不为空,则要分情况讨论
	a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对
		象,并使⽤头插法添加到当前位置的链表中
	
	b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
		i. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过
		   程中会判断红⿊树中是否存在当前key,如果存在则更新value
		
		ii. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插
		   ⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否
		   存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊
		   到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转成红⿊树
		
		iii. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就扩容,
			如果不需要就结束PUT⽅法

扩容条件不同:

jdk1.7中,HashMap是否进行扩容,满足两个条件之一即可进行: 判断当前个数是否大于等于阈值 、当前存放是否发生哈希碰撞
.
在Jdk1.8中,HashMap扩容条件也为两个之一即可:判断当前个数是否大于等于阈值,链表节点个数超8,如果链表长度超8,且数组元素个数小于64时,则会进行扩容,当数组元素超64,则会将链表转为红黑树

扩容后元素下标计算方式不同

jdk1.7中原数组中的数据必须重新计算其在新数组中的位置,并放进去,这个操作是极其消耗性能的
而在jdk1.8中数组元素下标计算无非两情况,在新数组中保持原来位置,或者原位置+原数组长度,效率快,减少碰撞

ConcurrentHashMap

ConcurrentHashMap实线程安全的Map,在多线程下使用HashMap的不安全的情况下,推荐使用 ConcurrentHashMap

ConcurrentHashMap 1.7与1.8的锁区别,以及数据结构区别

JDK1.7版本的ReentrantLock+Segment+HashEntry,
JDK1.8版本中synchronized+CAS+HashEntry+红黑树

jdk1.7 put()

Segment实现了ReentrantLock,也就带有锁的功能,

1,当执行put,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值

2,然后进行第二次hash,找到相应的HashEntry位置,将数据插入指定的HashEntry位置时,先ReentrantLock的tryLock()方法尝试去获取锁

3,如果获取成功就直接插入相应的位置

4,如果已有线程获取该Segment的锁,那当前线程会以自旋的方式去继续tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒

jdk1.8 put()

1,如果没有初始化就先调用initTable()方法来进行初始化过程
2,如果没有hash冲突就直接CAS插入
3,如果还在进行扩容操作就先进行扩容
4,如果存在hash冲突,就加锁synchronized来保证线程安全
5, 如果Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时会将链表结构转换为红黑树的结构,break再一次进入循环
6,如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

synchronized和ReentrantLock的锁区别?

SynchronizedJVM 层面的锁,是Java关键字,ReentrantLockAPI 层面的锁。
Synchronized 是隐式锁,自动释放锁,ReentrantLock 是显示锁,则手动释放锁
Synchronized 是不可中断类型的锁,ReentrantLock可中断(可设置超时时间)
Synchronized 为非公平锁,ReentrantLock 则即可选公平锁也可选非公平锁,默认非公平
Synchronzied 锁的是对象,ReentrantLock 锁的是线程
Synchronzied 不能过去锁状态,ReentrantLock 可以获取锁状态

Synchronized 适合于并发竞争低的情况,Synchronized 的锁升级如升级为重量级锁,在使用过程中是无法消除的,
意味着每次都要和 cpu 去请求锁资源,而ReentrantLock 提供了阻塞的能力,通过在高并发下线程挂起,来减少竞争,
提高并发能力

synchronized有锁升级那么有锁降级吗

HotSpot 虚拟机中是有锁降级的, 但是仅仅只发生在 STW 的时候 ,只有垃圾回收线程能够观测到它,也就是说,
在我们正常使用的过程中是不会发生锁降级的,只有在 GC 的时候才会降级。

动态高并发时为什么推荐 ReentrantLock 而不是 Synchronized?

Synchronized 升级为重量级锁后无法在正常情况下完成降级,而 ReentrantLock 是通过阻塞来提高性能的,在设计模式上就体现出了对多线程情况的支持

线程安全ConcurrentHashMap

初始容量默认为16段(Segment),jdk1.7使用分段锁设计,用法与HashMap一样
不对整个Map加锁,而是为每个Segment加锁
多个对象存入同一个Segment时,才需要互斥
最理想状态为16个对象分别存入16个Segment,并行数量16

public class TestConcurrentHashMap {
	public static void main(String[] args) {
		HashMap<String,String> maps = new HashMap<String,String>();
		ConcurrentHashMap<String, String> ch = new ConcurrentHashMap<String, String>();
		ch.put("A","check噢");
		ch.keySet();
	}
}

集合总尾

/**
         * ArrayList底层扩容是通过Arrays.copyOf扩容,这个底层是通过System.arraycopy()扩容
         * 有序
         */
        ArrayList<Object> arrayLisyt = new ArrayList<>();

        /**
         * linkedList底层是一个双向链表,有前驱后继节点
         * 有序
         */
        LinkedList<Object> linkList = new LinkedList<>();

        /**
         * 底层是一个hashmap,利用hash()和equal()去重
         * 但是Integer类型在小于容器得长度情况下,通过hash算法是排序
         * 无序
         */
        HashSet<Object> hashSet = new HashSet<>();

        /**
         * 底层是一个LinkedHashmap,底层维护一个数组+双向链表(head、tail)
         * 有序
         */
        LinkedHashSet<Object> linkedHashSet = new LinkedHashSet<>();

        /**
         * 底层是一个treemap,可以对元素进行排序
         * 去重方式是基于比较器中 == 0去重
         * 排序
         */
        TreeSet<Object> treeSet = new TreeSet<>();

        /**
         * 底层是一个哈希表(数组+单链表+红黑树)jdk1.8
         * 无序
         */
        HashMap<Object, Object> hashMap = new HashMap<>();

        /**
         * 底层是一个哈希表(数组+双相链表+红黑树),维护节点,accessorder=false时候有序
         * 默认有序
         */
        LinkedHashMap<Object, Object> linkedHashMap = new LinkedHashMap<>();

        /**
         * 底层是一个红黑树
         * 去重方式是基于比较器中 == 0去重
         * key是按照字典得顺序排序得
         * 排序
         */
        TreeMap<Object, Object> treeMap = new TreeMap<>();      
        
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值