Java集合学习总结

Java集合

​ 在Java中,有很多种方式可以用来表示集合,一般常用的就是Collection接口(编程时,一般使用的他的子类,List接口和Set接口)、Map接口和数组。

在这里插入图片描述

Collection

List

  1. ArrayList:底层数据结构是数组(动态数组),查询快(因为底层是数组,可以根据下标快速查找),增删慢,线程不安全,效率高,可以存储重复元素

    • 什么是动态数组?我们知道,在Java中数组一旦初始化,那么长度就固定了,而动态数组不存在这个问题。

    • 动态数组的实现原理:自己封装一个类用于实现,其实现动态数组的核心就是扩容方法,详情可查看另一位大神的文章(非本人创作)动态数组

      1. 先申明一个类

        public class Array<E>
        
      2. 成员变量

        private int size; //数组中元素的个数
        private E[] data; //数组声明
        
      3. 构造方法

        // 有参构造
        public Array (int capacity) {
        data = (E[])new Object[capacity];
        size = 0;
        }
        // 无参构造
        public Array () {
        this(10); //调用有参构造方法,并默认初始容量为10
        }
        
      4. 其他基本方法

        //获得数组元素个数
        public int getSize () {
        return size;
        }
        //获得数组长度
        public int getCapacity () {
        return data.length;
        }
        //获得数组是否为空
        public boolean isEmpty () {
        return size == 0;
        }
        
      5. add方法:本质就是,从后往前到指定索引位置,每个元素向后移一个格,给新来的腾出个地方。因为需要一个一个向后移动,通过循环给后面的每一个元素重新赋值,所以插入方法比链表慢。

        //向数组指定位置添加元素,index为指定索引位置,e为添加的值
        public void add (int index, E e) {
        //索引位置不能让它瞎插,索引为负数,或者跳格子插,不可以。
        if (index < 0 || index > size) {
        throw new IllegalArgumentException("add is fail, require index < 0 || index > size");
        }
        //当数组容量满了的时候,调用扩容方法,此处给它扩当前数组长度的两倍。
        if (data.length == size) {
        this.resize(data.length * 2);
        }for (int i = size - 1; i >= index; i--) {
        data[i+1] = data[i];
        }
        //新来的进坑
        data[index] = e;
        //维护size
        size ++;
        }
        
      6. 删除方法(一种根据索引删除,一种根据值删除):和添加相反,从要删除的索引位置的下一位开始,到最后一位元素索引位置结束,依次向前占一个,方向是从前向后,与add相反。

        //根据索引删除某个元素 返回删除的元素
        public E remove (int index) {
        if (index < 0 || index >= size) {
        throw new IllegalArgumentException("remove is fail,require index < 0 || index >= size");
        }
        //先把要删除的元素存起来,不然等会就给覆盖了。
        E value = data[index];
        for (int i = index + 1; i < size; i++) {
        data[i-1] = data[i];
        }
        //维护size
        size --;
        //此处为什么设置为null呢,因为泛型的原因,传进来的都是类对象,数组中存的是引用地址,引用不断开的话,垃圾回收器没办法回收。
        data[size] = null;
        //此处缩容,当数组元素个数等于数组长度四分之一时,进行缩容if (size == data.length/4 && data.length / 2 != 0) {
        //缩容为数组长度的二分之一
        this.resize(data.length /2);
        }
        return value;
        }
        
        
        //根据值删除某个元素
        public void removeByValue (E e) {
        //复用根据值查找元素的方法,返回索引(此方法在下面)
        int index = this.getByElement(e);
        if (index != -1) {
        //复用根据索引删除的方法
        this.remove(index);
        }
        }
        
      7. 查找方法**(分为两种,一种根据索引,一种根据值**)

        //根据索引查找数组某个元素,返回值
        public E getByIndex (int index) {
        
        if (index < 0 || index >= size) {
        throw new IllegalArgumentException("get is fail, require index < 0 || index >= size");
        }
        return data[index];// 根据索引查找速度就非常的快了
        }
        
        //根据值查找数组某个元素,返回索引
        public int getByElement (E e) {
        //本质:遍历数组进行比对
        for (int i = 0; i < size; i++) {
        if (data[i].equals(e) ) {
        return i;
        }
        }
        return -1;
        }
        
        //是否包含该元素
        public boolean contains (E e) {
        //本质:遍历数组进行比对
        for (int i = 0; i < size; i++) {
        if (data[i].equals(e)) {
        return true;
        }
        }
        return false;
        }
        
      8. 修改方法

        //修改数组某个元素
        public void set (int index, E e) {
        
        if (index < 0 || index >= size) {
        throw new IllegalArgumentException("set is fail, require index < 0 || index >= size");
        }
        
        data[index] = e;
        }
        
      9. 扩容方法:这个个人认为就是动态数组的核心,当动态数组的容量达到最大后,调用扩容方法,将一个长度更大的数组初始化,重新引用给原本的数组,同时将原本存储的数据也都给赋值过去。

        private void resize (int newCatacity) {
        E[] newData = (E[])new Object[newCatacity];
        for (int i = 0; i < size; i++) {
        newData[i] = data[i];
        }
        //给成员变量data重新赋值新引用(后面有内存图介绍)
        data = newData;
        }
        
  2. LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素

    • 链表不像数组那样存储了下标,链表只存了next,item,prev,所以查询时比较麻烦,因为没有下标,但是插入就很方便。当我们有新元素插入时,只需要修改所要插入位置的前一个元素的next值和后一个元素的prev值即可,如下图:
      在这里插入图片描述

    • 链表的底层是怎么实现的呢?

      1. 定义链表类

        public class LinkedList<E>
        
      2. 定义成员变量

        transient int size = 0;// 记录链表长度
        transient Node<E> first;// 第一个链表元素
        transient Node<E> last;// 最后一个链表元素
        
      3. Node静态内部类:这个就是核心,一个静态内部类,定义在LinkedList类中,类中的属性就是next,item,prev

        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;
            }
        }
        
      4. get方法:因为只存了next,item,prev3个属性,查询时只能通过循环一个一个的查找,所以链表查询较慢。

        public E get(int index) {
            checkElementIndex(index);// 判断是否超出了链表的长度,超出了就报错
            return node(index).item;
        }
        
        /**
         * 返回一个指定索引的非空节点.
         */
        Node<E> node(int index) {
            // assert isElementIndex(index);
            if (index < (size >> 1)) {
                Node<E> x = first;
                for (int i = 0; i < index; i++)
                    x = x.next;
                return x;
            } else {
                Node<E> x = last;
                for (int i = size - 1; i > index; i--)
                    x = x.prev;
                return x;
            }
        }
        
      5. add方法:插入时,只需要修改所要插入位置的前一个元素的next值和后一个元素的prev值即可,所以快。

        public boolean add(E e) {
            linkLast(e);
            return true;
        }
        
        /**
         * 设置元素e为最后一个元素
        */
        void linkLast(E e) {
            final Node<E> l = last;
            final Node<E> newNode = new Node<>(l, e, null);
            last = newNode;
            if (l == null)
                first = newNode;
            else
                l.next = newNode;
            size++;
            modCount++;
        }
        
        
        //在指定位置添加一个元素
        public void add(int index, E element) {
            checkPositionIndex(index);
            if (index == size)
                linkLast(element);
            else
                linkBefore(element, node(index));
        }
        
        /**
         * 在一个非空节点前插入一个元素
         */
        void linkBefore(E e, Node<E> succ) {
            // assert succ != null;
            final Node<E> pred = succ.prev;
            final Node<E> newNode = new Node<>(pred, e, succ);
            succ.prev = newNode;
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            size++;
            modCount++;
        }
        
      6. remove方法

        //删除某个对象
        public boolean remove(Object o) {
            if (o == null) {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item == null) {
                        unlink(x);
                        return true;
                    }
                }
            } else {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (o.equals(x.item)) {
                        unlink(x);
                        return true;
                    }
                }
            }
            return false;
        }
        //删除某个位置的元素
        public E remove(int index) {
            checkElementIndex(index);
            return unlink(node(index));
        }
        //删除某节点,并将该节点的上一个节点(如果有)和下一个节点(如果有)关联起来
        E unlink(Node<E> x) {
            final E element = x.item;
            final Node<E> next = x.next;
            final Node<E> prev = x.prev;
        
            if (prev == null) {
                first = next;
            } else {
                prev.next = next;
                x.prev = null;
            }
        
            if (next == null) {
                last = prev;
            } else {
                next.prev = prev;
                x.next = null;
            }
        
            x.item = null;
            size--;
            modCount++;
            return element;
        }
        

Set

  1. HashSet:底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
  2. LinkedHashSet:底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
  3. TreeSet:底层数据结构采用二叉树来实现,元素唯一且已经排好序。

Map

HashMap

HashMap的数据结构为 数组+(链表或红黑树)

为什么采用这种结构来存储元素呢?

数组的特点:查询效率高,插入,删除效率低

链表的特点:查询效率低,插入删除效率高

在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。

那么存储的时候是怎么存储的呢?

  • 第一步:计算出键的hashCode,该值用来定位要将这个元素存放到数组中的什么位置。
  • 第二步:根据它的hashCode找到数组中的位置,判断是否为空,如果这个位置为空,就直接扔进去,如果不为空,那么判断两者是否相等,相等则直接***覆盖***,如果不等则在原元素下面使用链表的结构存储该元素
  • 第三步:因为链表中元素太多的时候会影响查找效率,所以当链表的元素个数达到**8的时候使用链表存储就转变成了使用红黑树存储,原因就是红黑树是平衡二叉树,在查找性能方面比链表要高

在这里插入图片描述

其节点存储,Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用。所以这就是hashMap是数组+链表结构的原因。

static class Node<K,V> implements Map.Entry<K,V> {
   final int hash;
   final K key;
   V value;
   Node<K,V> next;
  1. 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) {
    // 判断数组是否为空,长度是否为0,是则进行扩容数组初始化
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
       n = (tab = resize()).length;
    // 通过hash算法找到数组下标得到数组元素,为空则新建
    if ((p = tab[i = (n - 1) & hash]) == null)
       tab[i] = newNode(hash, key, value, null);
    else {
       Node<K,V> e; K k;
            // 找到数组元素,hash相等同时key相等,则直接覆盖
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 该数组元素在链表长度>8后形成红黑树结构的对象,p为树结构已存在的对象
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 该数组元素hash相等,key不等,同时链表长度<8.进行遍历寻找元素,有就覆盖无则新建
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        // 新建链表中数据元素,尾插法
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 链表长度>=8 结构转为 红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 新值覆盖旧值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // onlyIfAbsent默认false
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
    // 判断是否需要扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  1. get()方法

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 永远检查第一个node
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

TreeMap

​ TreeMap 是一个有序的key-value集合,继承于AbstractMap,它是通过红黑树实现的。

HashTable

​ Hashtable:线程安全的,不允许null的键或值。

数组

***数组的定义:***数组是相同类型数据的有序集合。数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中,每一个数据称作一个元素,每个元素可以通过一个索引(下标)来访问它们。

定义方式1:元素类型[ ] 数组名 = new 元素类型 [元素个数或数组长度]; // int [] arr = new int [3];

定义方式2:元素类型[ ] 数组名 = new 元素类型 []{元素1,元素2 …}; // int [] arr = new int []{1,2,3};

定义方式3:元素类型[ ] 数组名 = {元素1,元素2 …}; // int [] arr = {1,2,3};

在Java中,数组也是属于集合,只是它和其他的集合还是有些区别。

  1. 数组可以储存基本数据类型和对象,而集合中只能储存对象(可以以包装类形式存储基本数据类型)。
  2. 数组的长度是固定的(初始化后长度就固定了),集合长度是可以改变的。
  3. 定义数组时必须指定数组元素类型,集合默认其中所有元素都是Object。
  4. 无法直接获取数组实际存储的元素个数,length用来获取数组的长度,但集合可以通过size()直接获取集合实际存储的元素个数。
  5. 集合有多种实现方式和不同的适用场合,而不像数组仅采用分配连续的空间方式。
  6. 集合以接口和类的形式存在,具有封装,继承和多态等类的特性,通过简单的方法和属性调用即可实现各种复杂的操作,大大提高软件的开发效率。

JDK提供的java.util.Arrays类,包含了常用的数组操作,Arrays类包含了:排序(Arrays.sort)、查找(Arrays.binarySearch,二分法查找 )、填充(Arrays.fill)、打印内容(Arrays.toString)等常见的操作。

扩展

数组转LIst效率最高的方法

Integer[] intrArray = {1,2,3,4};
ArrayList< Integer> arrayList = new ArrayList<>(strArray.length);// 先定义好长度,可避免多消耗
Collections.addAll(arrayList, strArray);// 这样做和asList方法比起来效率更高

Object转LIst的方法

public static List castList(Object obj, Class clazz) { 
    List result = new ArrayList(); 
    if (obj instanceof List) { 
        for (Object o : (List) obj) 
        { 
            result.add(clazz.cast(o)); 
        } 
        return result; 
    } 
    return null;
}

本文只详细总结了一些常用的,像ArrayListLinkedListHashMap,而有时候为了线程安全甚至都可能不用他们,而一些不常用的只是一笔带过。学无止境,奥利给就完事了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

佛祖保佑永不宕机

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值