java集合

集合类存放于 java.util 包中,主要有 Collection 和 Map

1. Collection

Collcetion 是集合 List、Set、Queue 的最基本的接口

1.1 List

学习重点:ArrayListVectorLinkList

ArrayList

ArrayList 是非线程安全的集合

ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动,代价比较高。因此,它适合随机查找和遍历,不适合插入和删除

Vector

Vector 与 ArrayList 一样,也是通过数组实现的 ,区别为它是线程安全的,支持线程的同步,由于同步需要很高的花费,所以访问它比 ArrayList

LinkList

LinkList 是非线程安全的集合

LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,它还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用

1.2 Set

学习重点:HashSetTreeSetLinkHashSet

Set 注重独一无二的性质,该体系集合用于存储无序元素,所以存入和取出的顺序不一定相同,并且值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的) 判断的, 如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法

HashSet

  1. HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。
  2. HashSet 允许有 null 值;HashSet 是无序的,即不会记录插入的顺序。
  3. HashSet 不是线程安全的。

TreeSet

  1. TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。
  2. TreeSet是基于 TreeMap 实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。
  3. TreeSet 有基本操作(add、remove 和 contains)。
  4. 对于java类库中定义的类,TreeSet 可以直接对其进行存储,只有String,Integer,因为这些类已经实现了 Comparable 接口,其他类型的话,需要自己自定义;如果自定义对象的话,则必须自己实现 Comparable 接口,来让TreeSet知道要如何排序。

排序原理

//查看put的源码
private final Comparator<? super K> comparator;     //成员变量

//TreeSet的底层是TreeMap。如果TreeSet使用的是无参构造,那么TreeMap肯定也是无参构造,此时comparator比较器对象就是null。
public TreeMap() {                            
	comparator = null;
}

//如果TreeSet使用的是带参构造,那么TreeMap肯定也是带参构造,此时comparator比较器对象就不是null了。
public TreeMap(Comparator<? super K> comparator) {         
    this.comparator = comparator;
}

 public V put(K key, V value) {                     //我们只需要看这个K 即可(因为我们看add方法里面调用put的时候就知道了)
   Entry<K,V> t = root;               
    if (t == null) {                                //创造一个树的根
        compare(key, key);           

        root = new Entry<>(key, value, null);    
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
   
   //如果是TreeSet是无参构造,comparator就是null,如果是带构造器的带参构造comparator就有值不是null
   Comparator<? super K> cpr = comparator;
    
   if (cpr != null) {                                             //如果有构造器(比较器方式)
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {                                                         //如果没有构造器(自然排序)
        if (key == null)
            throw new NullPointerException();
        Comparable<? super K> k = (Comparable<? super K>) key;     //把Integer类型的key转换成了k
        do {
            parent = t;
            cmp = k.compareTo(t.key);                              //进行比较,根据结果进行排序
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

通过源码我们可以发现,存入元素的时候,它创建了一个树,第一个元素就是树的根节点,后面的元素依次从树的根节点开始向后比较(创建比较器,利用comparator()方法进行比较),小的就往左边放,大的就往右边放,而相同的就不放进去(实现了唯一性)。取出元素的时候,它采用前序遍历的方法(根节点 左子树 右子树)遍历整个树,达到有序。

LinkHashSet

LinkedHashSet 集合的特点:Java.util.LinkedHashSet集合extends HashSet集合,底层是一个哈希表(数组+链表、红黑树)+链表。多了一条链表(记录元素的存储顺序),保证元素有序

其实 LinkedHashSet 和 HashSet 的用法差不多,只是多了一条链表来记录它的存储顺序。

1.3 Queue

未完待续…

2. Map

学习重点:HashMapHashTableTreeMapConcurrentHashMapLinkHashMap

2.1 HashMap

HashMap 底层数据结构为数组+链表+红黑树

HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的

HashMap 最多只允许一条记录的 key 为 null,允许多条记录的 value 为 null,HashMap 为非线程安全的,如果需要线程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。
在这里插入图片描述
在这里插入图片描述

2.1.1 put
  1. 首先将 K,V 封装到Node对象当中
  2. 然后它的底层会调用 K 的hashCode()方法得出hash值
  3. 通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着 k 和链表上每个节点的 k 进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
2.1.2 get
  1. 先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标
  2. 通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

注意:java7中,HashMap 中的查找根据 hash 值能快速定位到数组的具体下标,但是之后,需要顺着链表一个个比较下去才能找到需要的值,时间复杂度取决于链表的长度,为O(n),java8中,**当链表的元素超过8个并且桶的容量大于 64 时之后,**会将链表转换为红黑树,时间复杂度为 O(logN)

重点属性的含义

table:table就是 HashMap 中的数组(数组+链表+红黑树),数组其实就是 Node 数组

size:为 HashMap 中K-V的实时数量

loadFactor:加载因子,是用来衡量 HashMap 满的程度,计算HashMap的实时加载因子的方法为:size/capacity

capacity: 是桶的数量,也就是 table 的length,即数组的长度

threshold:计算公式:capacity * loadFactor。这个值是当前已占用数组长度的最大值。超过这个数就重新resize(扩容),扩容后的 HashMap 容量是之前容量的两倍

HashMap默认初始容量为16,默认加载因子为0.75

加载因子为什么是默认为 0.75?

这其实是出于容量和性能之间平衡的结果:

  • 当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生 Hash 冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;
  • 而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,此时元素的存储就比较稀疏,发生哈希冲突的可能性就比较小,因此操作性能会比较高。

2.2 HashTable

HashTable 是遗留类,是线程安全的,现在已经不推荐使用,被遗弃

2.3 TreeMap

TreeMap是一个内部元素排序版的HashMap,一般需要排序的时候会使用

下面展示 TreeSet 的两种用法:

  • 内部自动排序,默认从小到大的规则,也可使用降序方法排序
public static void main(String[] args) {
    Set<String> strSet = new TreeSet<>();
    strSet.add("abc");
    strSet.add("afc");
    strSet.add("ade");
    strSet.add("oge");
    strSet.add("bfg");
    strSet = ((TreeSet<String>) strSet).descendingSet();//降序排序
    Iterator it = strSet.iterator();
    while (it.hasNext()){
        System.out.println(it.next());
    }
}
//输出结果
oge
bfg
afc
ade
abc
  • 实现 Comparable 自定义排序规则
//学生类实现Comparable
public class Student implements Comparable{

    private int age;

    private String name;

    public Student(){}

    public Student(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int compareTo(Object o) {
        Student student = (Student) o;
        if (this.age > student.getAge()){
            return 1;
        }else if (this.age == student.getAge()){ //年龄相等时按照名字排序
            return this.name.compareTo(student.getName());
        }else {
            return -1;
        }
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

public class Chap7Main {

    public static void main(String[] args) {
        Set<Student> studentSet = new TreeSet<>();
        Student student1 = new Student(10,"cheng");
        Student student2 = new Student(10,"aheng");
        Student student3 = new Student(11,"zhang");
        Student student4 = new Student(13,"gheng");

        studentSet.add(student1);
        studentSet.add(student2);
        studentSet.add(student3);
        studentSet.add(student4);

        Iterator iterator = studentSet.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
//输出结果,先按照年龄排序,如果年龄相等则按照名字排序,升序
Student{age=10, name='aheng'}
Student{age=10, name='cheng'}
Student{age=11, name='zhang'}
Student{age=13, name='gheng'}

studentSet = ((TreeSet<Student>) studentSet).descendingSet();//降序

2.4 ConcurrentHashMap

java7下的ConcurrentHashMap
在这里插入图片描述
从图中我们可以看出,在 ConcurrentHashMap 内部进行了 Segment 分段,Segment 继承了 ReentrantLock,可以理解为一把锁,各个 Segment 之间都是相互独立上锁的,互不影响。

每个 Segment 的底层数据结构与 HashMap 类似,仍然是数组和链表组成的拉链法结构。默认有 0~15 共 16 个 Segment,所以最多可以同时支持 16 个线程并发操作(操作分别分布在不同的 Segment 上)。16 这个默认值可以在初始化的时候设置为其他值,但是一旦确认初始化以后,是不可以扩容的。

put加锁

通过分段加锁segment,一个hashmap里有若干个segment,每个segment里有若干个桶,桶里存放K-V形式的链表,put数据时通过key哈希得到该元素要添加到的segment,然后对segment进行加锁,然后在哈希,计算得到给元素要添加到的桶,然后遍历桶中的链表,替换或新增节点到桶中

java8下的ConcurrentHashMap
在这里插入图片描述
可以看出,java8下的ConcurrentHashMap的结构为数组+链表+红黑树

put源码分析

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) {
        throw new NullPointerException();
    }
    //计算 hash 值
    int hash = spread(key.hashCode());

    int binCount = 0;

    for (Node<K, V>[] tab = table; ; ) {
        Node<K, V> f;
        int n, i, fh;
        //如果数组是空的,就进行初始化
        if (tab == null || (n = tab.length) == 0) {
            tab = initTable();
        }
        // 找该 hash 值对应的数组下标
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //如果该位置是空的,就用 CAS 的方式放入新值
            if (casTabAt(tab, i, null,
                    new Node<K, V>(hash, key, value, null))) {
                break;
            }
        }
        //hash值等于 MOVED 代表在扩容
        else if ((fh = f.hash) == MOVED) {
            tab = helpTransfer(tab, f);
        }
        //槽点上是有值的情况
        else {
            V oldVal = null;
            //用 synchronized 锁住当前槽点,保证并发安全
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //如果是链表的形式
                    if (fh >= 0) {
                        binCount = 1;
                        //遍历链表
                        for (Node<K, V> e = f; ; ++binCount) {
                            K ek;
                            //如果发现该 key 已存在,就判断是否需要进行覆盖,然后返回
                            if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                            (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent) {
                                    e.val = value;
                                }
                                break;
                            }
                            Node<K, V> pred = e;
                            //到了链表的尾部也没有发现该 key,说明之前不存在,就把新值添加到链表的最后
                            if ((e = e.next) == null) {
                                pred.next = new Node<K, V>(hash, key,
                                        value, null);
                                break;
                            }
                        }
                    }
                    //如果是红黑树的形式
                    else if (f instanceof TreeBin) {
                        Node<K, V> p;
                        binCount = 2;
                        //调用 putTreeVal 方法往红黑树里增加数据
                        if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key,
                                value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent) {
                                p.val = value;
                            }
                        }
                    }
                }
            }
            if (binCount != 0) {
                //检查是否满足条件并把链表转换为红黑树的形式,默认的 TREEIFY_THRESHOLD 阈值				是 8
                if (binCount >= TREEIFY_THRESHOLD) {
                    treeifyBin(tab, i);
                }
                //putVal 的返回是添加前的旧值,所以返回 oldVal
                if (oldVal != null) {
                    return oldVal;
                }
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

get源码分析

public V get(Object key) {

    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;

    //计算 hash 值
    int h = spread(key.hashCode());

    //如果整个数组是空的,或者当前槽点的数据是空的,说明 key 对应的 value 不存在,直接返回 null
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
        //判断头结点是否就是我们需要的节点,如果是则直接返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //如果头结点 hash 值小于 0,说明是红黑树或者正在扩容,就用对应的 find 方法来查找
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        //遍历链表来查找
        while ((e = e.next) != null) {
            if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

总结一下 get 的过程:

  1. 计算 Hash 值,并由此值找到对应的槽点;
  2. 如果数组是空的或者该位置为 null,那么直接返回 null 就可以了;
  3. 如果该位置处的节点刚好就是我们需要的,直接返回该节点的值;
  4. 如果该位置节点是红黑树或者正在扩容,就用 find 方法继续查找;
  5. 否则那就是链表,就进行遍历链表查找。

put安全
就算有多个线程同时进行put操作,在初始化数组时使用了乐观锁CAS操作来决定到底是哪个线程有资格进行初始化,其他线程均只能等待。

用到的并发技巧

volatile变量(sizeCtl):它是一个标记位,用来告诉其他线程这个坑位有没有人在,其线程间的可见性由volatile保证。
CAS操作:CAS操作保证了设置sizeCtl标记位的原子性,保证了只有一个线程能设置成功

并发度

Java 7 中,每个 Segment 独立加锁,最大并发个数就是 Segment 的个数,默认是 16。

但是到了 Java 8 中,锁粒度更细,理想情况下 table 数组元素的个数(也就是数组长度)就是其支持并发的最大个数,并发度比之前有提高,所以java8中 concurrentHashMap的并发度默认是16,最大能支持到 table 数组元素个数的并发度。

ConcurrentHashMap默认的并发度为16,但用户也可以在构造函数中设置并发度。当用户设置并发度时,ConcurrentHashMap会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)。

如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。(根据你并发的线程数量决定,太多会导性能降低)

保证并发安全的原理

Java 7 采用 Segment 分段锁来保证安全,而 Segment 是继承自 ReentrantLock。

Java 8 中放弃了 Segment 的设计,采用 Node + CAS + synchronized 保证线程安全。

遇到 Hash 碰撞

Java 7 在 Hash 冲突时,会使用拉链法,也就是链表的形式。

Java 8 先使用拉链法,在链表长度超过一定阈值时,将链表转换为红黑树,来提高查找效率。

查询时间复杂度

Java 7 遍历链表的时间复杂度是 O(n),n 为链表长度。

Java 8 如果变成遍历红黑树,那么时间复杂度降低为 O(log(n)),n 为树的节点个数。

2.5 LinkHashMap

基本不怎么使用,先不做学习…

3. Set是如何保证集合内的元素不可重复?

Set中插入数据时,先比较HashCode

  1. 如果hashCode相同才会比较equals,equals相同,则两个对象相同,不能插入,equals不同,可以插入
  2. 如果hashCode不同,就直接插入了,两个对象hashCode不相等,他们equals一定是false

4. HashMap如何保证key值不可重复

HashMap是基于Hash算法实现的,我们通过put(key,value)存储,get(key)来获取。当传入key时,HashMap会根据key.hashCode()计算出hash值,根据hash值将value保存在bucket里。
当计算出的hash值相同时,我们称之为hash冲突,HashMap的做法是用链表和红黑树存储相同hash值的value。当hash冲突的个数比较少时,使用链表否则使用红黑树。

key值唯一的判断如下

  1. 如果hashCode相同才会比较equals,equals相同,则两个对象相同,不能插入,equals不同,可以插入。
  2. 如果hashCode不同,就直接插入了,两个对象hashCode不相等,他们equals一定是false。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值