常见的集合框架

集合框架

Collection

|–Collection[一个一个的]
|–List[有序可重复的]
|–ArrayList[使用数组实现的列表]
|–LinkedList[使用双向链表实现的列表]
|–Vector[使用数组实现的线程安全的列表]
|–Set[无序不重复的]
|–HashSet[使用Hash算法实现的集合]
|–LinkedHashSet[使用Hash算法实现的集合,并且使用链表维护插入顺序]
|–TreeSet[使用树结构实现的集合]
|–Treeset判断元素是否重复是按照比较结果判断的,因此如果想把元素插入到TreeSet中,那么元素需要具备比较器。

Collection遍历

  1. 1. public class Test02 {
    2.//这是main方法,程序的入口
    3.public static void main(String[] args) {
    4.Collection col = new ArrayList();
    5. ​        col.add(18);
    6. ​        col.add(12);
    7. ​        col.add(11);
    8. ​        col.add(17);
    9. ​        col.add("abc");
    10. ​       col.add(9.8);
    11. 
    12.//对集合遍历(对集合中元素进行查看)
    13.//方式1:普通for循环
    14./*for(int i= 0;i<col.size();i++){
    15. ​            col.get(i);
    16. ​        }*/
    17. 
    18.//方式2:增强for循环
    19.for(Object o:col){
    20.System.out.println(o);
    21.}
    22.System.out.println("------------------------");
    23.//方式3:iterator()迭代器
    24.Iterator it = col.iterator();
    25.while(it.hasNext()){
    26.System.out.println(it.next());
    27.}
    28.}
    29. }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z2NOwBL5-1659423480789)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504144628216.png)]

Collection接口常用方法

public class Re {
    public static void main(String[] args) throws Exception {    
       /*
        Collection接口的常用方法:
        增加:add(E e) addAll(Collection<? extends E> c)
        删除:clear() remove(Object o)
        修改:
        查看:iterator() size()
        判断:contains(Object o)  equals(Object o) isEmpty()
         */
        //创建对象:接口不能创建对象,利用实现类创建对象:
        Collection col = new ArrayList();
        //调用方法:
        //集合有一个特点:只能存放引用数据类型的数据,不能是基本数据类型
        //基本数据类型自动装箱,对应包装类。int--->Integer
        col.add(18);
        col.add(12);
        col.add(11);
        col.add(17);
        System.out.println(col/*.toString()*/);
        List list = Arrays.asList(new Integer[]{11, 15, 3, 7, 1});
        col.addAll(list);//将另一个集合添加入col中
        System.out.println(col);
        //col.clear();清空集合
        System.out.println(col);
        System.out.println("集合中元素的数量为:"+col.size());
        System.out.println("集合是否为空:"+col.isEmpty());
        boolean isRemove = col.remove(15);
        System.out.println(col);
        System.out.println("集合中数据是否被删除:"+isRemove);
        Collection col2 = new ArrayList();
        col2.add(18);
        col2.add(12);
        col2.add(11);
        col2.add(17);
        Collection col3 = new ArrayList();
        col3.add(18);
        col3.add(12);
        col3.add(11);
        col3.add(17);
        System.out.println(col2.equals(col3));
        System.out.println(col2==col3);//地址一定不相等  false
        System.out.println("是否包含元素:"+col3.contains(117));

    }
}

list和set区别

list可重复,有序,set不重复,具有无序性,

list取出元素可以根据索引,通过get方法获取,set获取元素只能逐一遍历

list中可以存在多个null值,set因为不重复的原因最多只能存在一个null值

list

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ek2NHmMf-1659423480801)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220715094033712.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MwiRTkKO-1659423480805)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504161806341.png)]

|–Collection(一个一个的值)
|–List(有序可重复)
|–ArrayList(数组列表)
|–LinkedList(链表列表)

​ |–Vector(数组列表)【已经被淘汰】

ArrayList和LinkedList的取舍

|–效率
|–在中间的某个位置插入值的时候,LinkedList效率更高。
|–ArrayList向某个中间的位置插入值的时候,需要将之后的元素后移(消耗效率),和扩容(消耗效率)。
|–以上ArrayList主要消耗效率的两点,LinkedList都不存在。

​ |–往列表最后的位置插入值的时候,两者效率基本上差不多(如果次数多,LinkedList占有优势)。

ArrayList和LinkedList区别
  • ArrayList:必须开辟连续空间,查询快,增删慢。

  • LinkedList:无需开辟连续空间,查询慢,增删快。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qzChVzJo-1659423480806)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220505135119559.png)]

ArrayList

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AabCpq6J-1659423480807)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220505134754556.png)]

ArrayList源码分析
  • 默认容量大小:private static final int DEFAULT_CAPACITY = 10;

  • 存放元素的数组:transient Object[] elementData;

  • 实际元素个数:private int size;

  • 创建对象时调用的无参构造函数:

    COPY//这是一个空的数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    

    这段源码说明当你没有向集合中添加任何元素时,集合容量为0。那么默认的10个容量怎么来的呢?

    这就得看看add方法的源码了:

    COPYpublic boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    

    假设你new了一个数组,当前容量为0,size当然也为0。这时调用add方法进入到ensureCapacityInternal(size + 1);该方法源码如下:

    COPYprivate void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    

    该方法中的参数minCapacity传入的值为size+1也就是 1,接着我们再进入到calculateCapacity(elementData, minCapacity)里面:

    COPYprivate static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    

    上文说过,elementData就是存放元素的数组,当前容量为0,if条件成立,返回默认容量DEFAULT_CAPACITY也就是10。这个值作为参数又传入ensureExplicitCapacity()方法中,进入该方法查看源码:

     COPYprivate void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    

    我们先不要管modCount这个变量。

    因为elementData数组长度为0,所以if条件成立,调用grow方法,重要的部分来了,我们再次进入到grow方法的源码中:

    COPYprivate void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

    这个方法先声明了一个oldCapacity变量将数组长度赋给它,其值为0;又声明了一个newCapacity变量其值为oldCapacity+一个增量,可以发现这个增量是和原数组长度有关的量,当然在这里也为0。第一个if条件满足,newCapacity的值为10(这就是默认的容量,不理解的话再看看前面)。第二个if条件不成立,也可以不用注意,因为MAX_ARRAY_SIZE的定义如下:

    COPYprivate static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    

    这个值太大了以至于第二个if条件没有了解的必要。

    最后一句话就是为elementData数组赋予了新的长度,Arrays.copyOf()方法返回的数组是新的数组对象,原数组对象不会改变,该拷贝不会影响原来的数组。copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。

    这时候再回到add的方法中,接着就向下执行elementData[size++] = e;到这里为止关于ArrayList就讲解得差不多了,当数组长度为10的时候你们可以试着过一下源码,查一下每次的增量是多少(答案是每次扩容为原来的1.5倍)。

扩容:

ArrayList的初始容量为10

当数组中的10个位置都满了的时候就开始进行数组的扩容,扩容长度为 原数组的1.5倍:

扩容的本质将老数组的内容复制到新数组中,然后返回新数组

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z60NoeMU-1659423480808)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504150515206.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4HMcehWe-1659423480809)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504150524496.png)]

LinkedList

LinkedList的底层数据结构是基于双链表,查询慢,首尾操作的速度是极快的,所以有很多首尾操作的api

底层原理图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yN9dnOQG-1659423480809)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504151537224.png)]

源码分析
public class LinkedList<E> {//E是一个泛型,具体的类型要在实例化的时候才会最终确定
    transient int size = 0;//集合中元素的数量

    //Node的内部类
    private static class Node<E> {
        E item;//当前元素
        Node<E> next;//指向下一个元素地址
        Node<E> prev;//上一个元素地址

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

    transient Node<E> first;//链表的首节点
    transient Node<E> last;//链表的尾节点

    //空构造器:
    public LinkedList() {
    }

    //添加元素操作:
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    void linkLast(E e) {//添加的元素e
        final Node<E> l = last;//将链表中的last节点给l 如果是第一个元素的话 l为null
        //将元素封装为一个Node具体的对象:
        final Node<E> newNode = new Node<>(l, e, null);
        //将链表的last节点指向新的创建的对象:
        last = newNode;

        if (l == null)//如果添加的是第一个节点
            first = newNode;//将链表的first节点指向为新节点
        else//如果添加的不是第一个节点 
            l.next = newNode;//将l的下一个指向为新的节点
        size++;//集合中元素数量加1操作
        modCount++;
    }

    //获取集合中元素数量
    public int size() {
        return size;
    }

    //通过索引得到元素:
    public E get(int index) {
        checkElementIndex(index);//健壮性考虑
        return node(index).item;
    }

    Node<E> node(int index) {
        //如果index在链表的前半段,那么从前往后找
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {//如果index在链表的后半段,那么从后往前找
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
}
listIterator迭代器
package com.msb.test06;
        import java.util.ArrayList;
        import java.util.Iterator;
        import java.util.List;
/**
 * @author : msb-zhaoss
 */
public class Test2 {
    //这是main方法,程序的入口
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");
        list.add("ee");
        //在"cc"之后添加一个字符串"kk"
        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            if("cc".equals(it.next())){
                list.add("kk");
            }
        }
    }
}

这种情况会出错,list和迭代器同时对集合进行了操作

解决办法:事情让一个“人”做 --》引入新的迭代器:ListIterator

迭代和添加操作都是靠ListIterator来完成的:


package com.msb.test06;
        import java.util.ArrayList;
        import java.util.Iterator;
        import java.util.List;
        import java.util.ListIterator;
/**
 * @author : msb-zhaoss
 */
public class Test2 {
    //这是main方法,程序的入口
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");
        list.add("ee");
        //在"cc"之后添加一个字符串"kk"
        ListIterator<String> it = list.listIterator();
        while(it.hasNext()){
            if("cc".equals(it.next())){
                it.add("kk");
            }
        }
        System.out.println(it.hasNext());
        System.out.println(it.hasPrevious());
        //逆向遍历:
        while(it.hasPrevious()){
            System.out.println(it.previous());
        }
        System.out.println(it.hasNext());
        System.out.println(it.hasPrevious());
        System.out.println(list);
    }
}

set

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CtXAvhSE-1659423480810)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504161649127.png)]

​ |–Set[无序不重复的]
​ |–HashSet[使用Hash算法实现的集合]
​ |–LinkedHashSet[使用Hash算法实现的集合,并且使用链表维护插入顺序]
​ |–TreeSet[使用树结构实现的集合]
​ |–Treeset判断元素是否重复是按照比较结果判断的,因此如果想把元素插入到TreeSet中,那么元素需要具备比较器。

HashSet底层原理哈希表

HashSet集合底层采取哈希表存储的数据

哈希表是一个对于增删改查数据性能都较好的结构

哈希值:根据对象地址根据某种规则算出的int类型的值

jdk8之前由数组+链表构成jdk8之后由数组+链表+红黑树构成,链表节点超过8个之后会生成红黑树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gPqdpVQf-1659423480811)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504170828227.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sJeKf3wX-1659423480811)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504170920587.png)]

HashSet去重原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jMwmepBI-1659423480812)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504170757985.png)]

去重原因:

先判断哈希值,在判断equals

问题

如果希望set集合认为两个内容相同的对象是重复的怎么办

重写对象的hashcode和equals方法

例子
public class Re {
    public static void main(String[] args) throws Exception {
        Student s1 = new Student("jyg", 20, "男");
        Student s2 = new Student("jyg", 20, "男");
        Student s3 = new Student("jyg", 20, "女");

        Set<Student> sets=new HashSet<>();
        sets.add(s1);
        sets.add(s2);
        sets.add(s3);
        System.out.println(sets);
    }
}
//输出结果
//[Student{name='jyg', age=20, sex='女'}, Student{name='jyg', age=20, sex='男'}, Student{name='jyg', age=20, sex='男'}]


虽然s1和s2内容一样,但是sets集合中没有去重,原因是二者是不同的对象哈希值不同,如果想将二者视为一个对象则必须要重写hashcode和equals
    
    

重写hashcode和equals后结果

package com;

import java.lang.reflect.Method;
import java.util.*;
class Student {
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && name.equals(student.name) && sex.equals(student.sex);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, age, sex);
    }
}
输出结果
    [Student{name='jyg', age=20, sex='男'}, Student{name='jyg', age=20, sex='女'}]
LinkedHashSet(有序,不重复)

原理:底层数据结构依然是哈希表,只是每个元素又额外的多了一个双链表机制记录存储的顺序(有序)

TressSet

不重复,无索引,可排序

集合底层基于红黑树,增删改查性能都很好,TreeSet是必须要排序的,按照制定规则进行排序

TreeSet集合自定义排序规则:

两种

类实现Comparable接口,重写比较规则

集合自定义Comparator比较器对象,重写比较规则

比较器
内部比较器

public class Re {
    public static void main(String[] args) throws Exception {
        TreeSet<Student> ts = new TreeSet<>();
        ts.add(new Student("elili",10,"男"));
        ts.add(new Student("blili",20,"男"));
        ts.add(new Student("alili",5,"男"));
        ts.add(new Student("elili",6,"男"));
        ts.add(new Student("flili",9,"男"));
        ts.add(new Student("dlili",3,"男"));
        System.out.println(ts.size());
        System.out.println(ts);
    }
}

class Student implements Comparable<Student>{
    public String name;
    public int age;
    public String sex;

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

    
    @Override
    public int compareTo(Student o ) {
        return this.getAge()-o.getAge();
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }
}
外部比较器
public class Re {
    public static void main(String[] args) throws Exception {
        Comparator<Student> com = new BiJiao();
        TreeSet<Student> ts = new TreeSet<>(com);
        ts.add(new Student("elili", 10, "男"));

        ts.add(new Student("alili", 5, "男"));
        ts.add(new Student("elili", 6, "男"));
        ts.add(new Student("dlili", 3, "男"));
        ts.add(new Student("flili", 9, "男"));
        ts.add(new Student("blili", 20, "男"));
        System.out.println(ts.size());
        System.out.println(ts);

        
        /*  TreeSet<Student> ts2 = new TreeSet<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });*/

        TreeSet<Student> ts2 = new TreeSet<>((o1, o2) -> o1.getName().compareTo(o2.getName()));
        ts2.add(new Student("fyz2", 10, "男"));

        ts2.add(new Student("fyz3", 5, "男"));
        ts2.add(new Student("fyz1", 6, "男"));
        ts2.add(new Student("fyz6", 3, "男"));
        ts2.add(new Student("fyz5", 9, "男"));
        ts2.add(new Student("fyz0", 20, "男"));
        System.out.println(ts2.size());
        System.out.println(ts2);
    }
}

class Student {
    private String name;
    private int age;
    private String sex;

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && name.equals(student.name) && sex.equals(student.sex);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, sex);
    }


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

class BiJiao implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getName().compareTo(o2.getName());
    }
}

class BiJiao02 implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        //比较姓名:
        return o1.getName().compareTo(o2.getName());
    }
}

class BiJiao03 implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        //在年龄相同的情况下 比较身高  年龄不同比较年龄
        if((o1.getAge()-o2.getAge())==0){
            return ((Double)(o1.getHeight())).compareTo((Double)(o2.getHeight()));
        }else{//年龄不一样
            return o1.getAge()-o2.getAge();
        }
    }
}

外部比较器和内部比较器 谁好呀?

答案:外部比较器,多态,扩展性好

Map

|–Map
|–java.util.HashMap
|–java.util.LinkedHashMap
|–java.util.TreeMap
|–java.util.HashTable
|–java.util.Properties

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qaor1oRE-1659423480813)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504221959272.png)]

Map常用方法

Map<String,Integer> map = new HashMap<>();
System.out.println(map.put("lili", 10101010));
map.put("nana",12345234);
map.put("feifei",34563465);
System.out.println(map.put("lili", 34565677));
map.put("mingming",12323);
/*map.clear();清空*/
/*map.remove("feifei");移除*/
System.out.println(map.size());
System.out.println(map);
System.out.println(map.containsKey("lili"));
System.out.println(map.containsValue(12323));
Map<String,Integer> map2 = new HashMap<>();
System.out.println(map2.put("lili", 10101010));
map2.put("nana",12345234);
map2.put("feifei",34563465);
System.out.println(map2.put("lili", 34565677));
map2.put("mingming2",12323);
System.out.println(map==map2);
System.out.println(map.equals(map2));//equals进行了重写,比较的是集合中的值是否一致
System.out.println("判断是否为空:"+map.isEmpty());
System.out.println(map.get("nana"));

Map遍历方法

//keySet()对集合中的key进行遍历查看:
Set<String> set = map.keySet();
for(String s:set){
    System.out.println(s);
}
System.out.println("-----------------------------------");
//values()对集合中的value进行遍历查看:
Collection<Integer> values = map.values();
for(Integer i:values){
    System.out.println(i);
}
System.out.println("-----------------------------------");
//get(Object key) keySet()
Set<String> set2 = map.keySet();
for(String s:set2){
    System.out.println(map.get(s));
}
System.out.println("-----------------------------------");
//entrySet()遍历键和值
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for(Map.Entry<String, Integer> e:entries){
    System.out.println(e.getKey()+"----"+e.getValue());

HashMap

源码分析
  • 默认初始化容量:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    • 数组最大容量:static final int MAXIMUM_CAPACITY = 1 << 30;
  • 默认加载因子:static final float DEFAULT_LOAD_FACTOR = 0.75f;

  • 链表调整为红黑树的链表长度阈值(JDK1.8):static final int TREEIFY_THRESHOLD = 8;

  • 红黑树调整为链表的链表长度阈值(JDK1.8):static final int UNTREEIFY_THRESHOLD = 6;

  • 链表调整为红黑树的数组最小阈值(JDK1.8):static final int MIN_TREEIFY_CAPACITY = 64;

  • HashMap存储的数组:transient Node<K,V>[] table;

  • HashMap存储的元素个数:transient int size;

    • 默认加载因子是什么?
      • 就是判断数组是否扩容的一个因子。假如数组容量为100,如果HashMap的存储元素个数超过了100*0.75=75,那么就会进行扩容。
    • 链表调整为红黑树的链表长度阈值是什么?
      • 假设在数组中下标为3的位置已经存储了数据,当新增数据时通过哈希码得到的存储位置又是3,那么就会在该位置形成一个链表,当链表过长时就会转换成红黑树以提高执行效率,这个阈值就是链表转换成红黑树的最短链表长度;
    • 红黑树调整为链表的链表长度阈值是什么?
      • 当红黑树的元素个数小于该阈值时就会转换成链表。
    • 链表调整为红黑树的数组最小阈值是什么?
      • 并不是只要链表长度大于8就可以转换成红黑树,在前者条件成立的情况下,数组的容量必须大于等于64才会进行转换。

    HashMap的数组table存储的就是一个个的Node<K,V>类型,很清晰地看到有一对键值,还有一个指向next的指针(以下只截取了部分源码):

    COPYstatic class Node<K,V> implements Map.Entry<K,V> {
          final K key;
          V value;
          Node<K,V> next;
      }
    

    之前的代码中在new对象时调用的是HashMap的无参构造方法,进入到该构造方法的源码查看一下:

    COPYpublic HashMap() {
          this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
      }
    

    发现没什么内容,只是赋值了一个默认加载因子;而在上文我们观察到源码中table和size都没有赋予初始值,说明刚创建的HashMap对象没有分配容量,并不拥有默认的16个空间大小,这样做的目的是为了节约空间,此时table为null,size为0。

    当我们往对象里添加元素时调用put方法:

    COPYpublic V put(K key, V value) {
          return putVal(hash(key), key, value, false, true);
      }
    

    put方法把key和value传给了putVal,同时还传入了一个hash(Key)所返回的值,这是一个产生哈希值的方法,再进入到putVal方法(部分源码):

    COPYfinal V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                        boolean evict) {
          Node<K,V>[] tab; Node<K,V> p; int n, i;
          if ((tab = table) == null || (n = tab.length) == 0)
              n = (tab = resize()).length;
          if ((p = tab[i = (n - 1) & hash]) == null)
              tab[i] = newNode(hash, key, value, null);
          else{
              //略
          }
      }
    

    这里面创建了一个tab数组和一个Node变量p,第一个if实际是判断table是否为空,而我们现在只关注刚创建HashMap对象时的状态,此时tab和table都为空,满足条件,执行内部代码,这条代码其实就是把resize()所返回的结果赋给tab,n就是tab的长度,resize顾名思义就是重新调整大小。查看resize()源码(部分):

    COPYfinal Node<K,V>[] resize() {
          Node<K,V>[] oldTab = table;
          int oldCap = (oldTab == null) ? 0 : oldTab.length;
          int oldThr = threshold;
          if (oldCap > 0);
          else if (oldThr > 0);
          else {               // zero initial threshold signifies using defaults
              newCap = DEFAULT_INITIAL_CAPACITY;
              newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
          } 
          @SuppressWarnings({"rawtypes","unchecked"})
          Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
          table = newTab;
          return newTab;
      }
    

    该方法首先把table及其长度赋值给oldTab和oldCap;threshold是阈值的意思,此时为0,所以前两个if先不管,最后else里newCap的值为默认初始化容量16;往下创建了一个newCap大小的数组并将其赋给了table,刚创建的HashMap对象就在这里获得了初始容量。然后我们再回到putVal方法,第二个if就是根据哈希码得到的tab中的一个位置是否为空,为空便直接添加元素,此时数组中无元素所以直接添加。至此HashMap对象就完成了第一个元素的添加。当添加的元素超过16*0.75=12时,就会进行扩容:

    COPYfinal V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict){
          if (++size > threshold)
              resize();
      }
    

    扩容的代码如下(部分):

    COPYfinal Node<K,V>[] resize() {
          int oldCap = (oldTab == null) ? 0 : oldTab.length;
          int newCap;
          if (oldCap > 0) {
              if (oldCap >= MAXIMUM_CAPACITY) {//略}
              else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                       oldCap >= DEFAULT_INITIAL_CAPACITY)
          }
      }
    

    核心部分是else if里的移位操作,也就是说每次扩容都是原来大小的两倍

  • *:额外说明的一点是在JDK1.8以前链表是头插入,JDK1.8以后链表是尾插入。

put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
 
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // table表该索引位置不为空,则进行查找
        Node<K,V> e; K k;
        // 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
            for (int binCount = 0; ; ++binCount) {
                // 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
                    // 减一是因为循环是从p节点的下一个节点开始的
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;  // 将p指向下一个节点
            }
        }
        // 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 用于LinkedHashMap
            return oldValue;
        }
    }
    ++modCount;
    // 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);  // 用于LinkedHashMap
    return null;
}

HashSet的底层就是利用HashMap来完成的

public class HashSet<E>{S
    //重要属性:
    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();
    //构造器:
    public HashSet() {
        map = new HashMap<>();//HashSet底层就是利用HashMap来完成的
    }

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
}

HashMap的特点和底层原理

由键决定:无序,不重复,无索引.底层是哈希表结构

依赖hashCode和equals保证键的唯一(键相同会覆盖)

如果要存储自定义对象,要重写hashCode和equals

因为基于哈希表,所以增删改查性能都很好

扩容机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qt8t5CAH-1659423480814)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220715095145051.png)]

LinkedHashMap

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PpwHQBGX-1659423480814)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220505134710071.png)]

TreeMap

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c1gJrUZr-1659423480814)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220505134631654.png)]

外部比较器

public class Re {
    public static void main(String[] args) throws Exception {
        Map<Student,Integer> map =
                new TreeMap<>((o1, o2) -> ((o1.getName())).compareTo((o2.getName())));
        map.put(new Student(19,"elili",170.5),1001);
        map.put(new Student(18,"blili",150.5),1003);
        map.put(new Student(19,"alili",180.5),1023);
        map.put(new Student(17,"clili",140.5),1671);
        map.put(new Student(10,"dlili",160.5),1891);
        System.out.println(map);
        System.out.println(map.size());

    }
}

class Student {
    private int age;
    private String name;
    private double height;
    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;
    }
    public double getHeight() {
        return height;
    }
    public void setHeight(double height) {
        this.height = height;
    }
    public Student(int age, String name, double height) {
        this.age = age;
        this.name = name;
        this.height = height;
    }
    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", height=" + height +
                '}';
    }

}

ConcurrentHashMap

1.7

数据结构:ReentrantLock+SegMent+HashEntry数组,每个HashEntry又是一个链表结构

元素查询:二次hash,定位到Segment,在定位到元素所在链表头部

并发级别就是segment的个数

初始化时的HaehEntry数组大小,根据并发级别,和实际需要进行计算,即initualCapacity和ConcurrentLevel

扩容:不影响segment,扩容阔的是hashEntry数组

锁:ReentrantLock是个分段锁,锁的粒度小一点,锁定操作的segment,其他的segment不受影响

get方法无需加锁,volatile

1.8

数据结构synchronized+Cas+Node+红黑树,node的val和next用volatile来修饰,保证可见性

查找,替换,赋值操作都是用cas

锁:锁的是链表的head头节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容是,阻塞所有的读写操作,并发扩容

读操作无所:

node的val和next用volatile修饰,读写线程对该变量互相可见

  this.name = name;
}
public double getHeight() {
    return height;
}
public void setHeight(double height) {
    this.height = height;
}
public Student(int age, String name, double height) {
    this.age = age;
    this.name = name;
    this.height = height;
}
@Override
public String toString() {
    return "Student{" +
            "age=" + age +
            ", name='" + name + '\'' +
            ", height=" + height +
            '}';
}

}


### ConcurrentHashMap

1.7

 数据结构:ReentrantLock+SegMent+HashEntry数组,每个HashEntry又是一个链表结构

元素查询:二次hash,定位到Segment,在定位到元素所在链表头部

并发级别就是segment的个数

初始化时的HaehEntry数组大小,根据并发级别,和实际需要进行计算,即initualCapacity和ConcurrentLevel

扩容:不影响segment,扩容阔的是hashEntry数组

锁:ReentrantLock是个分段锁,锁的粒度小一点,锁定操作的segment,其他的segment不受影响

get方法无需加锁,volatile



1.8

数据结构synchronized+Cas+Node+红黑树,node的val和next用volatile来修饰,保证可见性

查找,替换,赋值操作都是用cas

锁:锁的是链表的head头节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容是,阻塞所有的读写操作,并发扩容

读操作无所:

node的val和next用volatile修饰,读写线程对该变量互相可见

数组用volatile修饰,保证扩容时被其他线程感知
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值