Java集合详解

集合解析

---->集合是什么?

通常,我们的Java程序需要根据程序运行时才知道创建了多少个对象。但若非程序运行,程序开发阶段,我们根本不知道到底需要多少个数量的对象,甚至不知道它的准确类型。为了满足这些常规的编程需要,我们要求能在任何时候,任何地点创建任意数量的对象,而这些对象用什么来容纳呢?我们首先想到了数组,但是!数组只能存放同一类型的数据,而且其长度是固定的,那怎么办了?集合便应运而生了。集合实际上就是一个容器,可以来容纳其他类型的数据。

 Java集合类存放在java.util包中,是一个用来存放对象的容器。

  注意:

  1.集合只能存放对象。比如你存入一个int型数据66放入集合中,其实它是自动转换成Integer类后存入的,Java中每一种基本数据类型都有对应的引用类型。

  2.集合存放的都是对象的引用,而非对象本身。所以我们称集合中的对象就是集合中对象的引用(对象的内存地址)。对象本身还是放在堆内存中。

  3.集合可以存放不同类型,不限数量的数据类型。集合也是一个对象,可以内嵌。集合套集合。

 

---->Java集合的框架关系图

  • Collection集合

 

 

 

  • Map集合

  •  

     

---->Java集合各接口和实现类常用方法

Collection接口

  1. 没有使用泛型时,可以存储Object的所有子类型,使用泛型后,只能存储某个具体的类型。不能直接存储基本数据类型,也不能存Java对象,只是存放Java对象的内存地址。存放基本数据类型会自动装箱。

  2. Collection接口的常用方法

    •  1 **boolean add(Object o)添加对象到集合**
       2 **boolean remove(Object o)删除指定的对象**
       3 **int size()返回当前集合中元素的数量**
       4 **boolean contains(Object o)查找集合中是否有指定的对象**
       5  6 **boolean isEmpty()判断集合是否为空**
       7 **Iterator iterator()返回一个迭代器**
       8 **boolean containsAll(Collection c)查找集合中是否有集合c中的元素**
       9 **boolean addAll(Collection c)将集合c中所有的元素添加给该集合**
      10 **void clear()删除集合中所有元素**
      11 **void removeAll(Collection c)从集合中删除c集合中也有的元素**
      12 **void retainAll(Collection c)从集合中删除集合c中不包含的元素**

    contains(Object o)判断查找集合中是否有指定的对象时,需要了解对象类型是否重写了euqals方法,若没有重写,比较的是内存地址,若重写则查看重写方法里面的内容比较是否相同,存放在集合里的类型一定要重写equals方法。remove()方法底层也调用了equals()方法

迭代器Iterator

  • 迭代器(Iterator)是一种设计模式、提供了一种方法,来对集合、容器进行遍历的方式,不需要关注底层数据结构和数据类型,来达到底层和上层遍历解耦的目的

  • Iterable 迭代器接口,这是Collection类的父接口实现这个Iterable接口的对象允许使用foreach进行遍历,也就是说,所有的Collection集合对象都有"foreach可遍历性"。这个Iterable接口只有一个方法: iterator()。它返回一个代表当前集合对象的泛型< T >迭代器,用于之后的遍历操作

  •  

    Iterator its = map.keySet().iterator();

  • Iterator its = list.iterator();

  • Iterator its = set.iterator();

Iterator里面有三个方法:

  • boolean hasNext() :判断集合是否还有元素; true表示还存在元素 ,false表示不存在元素

  • E next():返回当前数据,迭代器前进一位

  • void remove():删除元素,不能调用集合的remove()方法,迭代器获取集合状态后,相当于有了一个快照,若调用集合的remove()方法,集合中元素改变,但是迭代器获取的还是之前集合的元素,不知道集合已经变化了,就会出现异常。用迭代器的remove()方法相当于是录像,录像里的状态发生改变,录像会一起改变。自动更新迭代器,并且更新集合

  • 注意*集合结构只要发生改变,迭代器必须重新获取。若还是之前的迭代器,会出现异常。获取迭代器类似于快照状态,获取迭代器对象后,当前集合状态是一个快照,迭代器会按照这个快照进行迭代,若发生了数据改变,改变后的集合状态跟之前是不一样的,会抛出异常。

public static void main(String[] args) {
        Collection list=new ArrayList();
        list.add(1);
        list.add("hhh");
        list.add(true);
        System.out.println(list1.size());
        //方式一:迭代器遍历
        Iterator it=list.iterator();
        System.out.println("*************方式一*************");
        while(it.hasNext()) {  //若为while(true)//会抛出异常
            Object object=it.next();//取出来都是Object,类型跟之前一样
            System.out.println(object);//调用了toString();
        }
        //方式二:foreach遍历
        System.out.println("*************方式二*************");
        for(Object o:list) {
            System.out.println(o);
        }
    }
}

List接口

  • List接口是Collection接口的子接口,List中的元素:有序可重复,所以被称为是序列。

  • 实现类

    • ArrayList:数组实现,查询快,增删慢,轻量级;(线程不安全)

    • LinkedList:双向链表实现,增删快,查询慢 (线程不安全)

    • Vector:数组实现,重量级 (线程安全、使用少)

  • List接口的常用方法

int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean addAll(int index, Collection<? extends E> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
//比较常用
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index)
int indexOf(Object o);
int lastIndexOf(Object o);
  1. ArrayList

    • 底层是Object数组,所以ArrayList具有数组的查询速度快的优点以及增删速度慢的缺点,但是向数组末尾添加元素效率蛮高,因为程序中查找元素操作较多,所以用的较多,。

    • 不是线程安全的默认初始化容量为10(底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量为10),可以自己指定初始容量。

    • 扩容:int newCapacity = oldCapacity + (oldCapacity >> 1),其中oldCapacity是原来的容量大小,oldCapacity >> 1 为位运算的右移操作,右移一位相当于除以2,所以这句代码就等于int newCapacity = oldCapacity + oldCapacity / 2;即容量扩大为原来的1.5倍(注意我这里使用的是jdk1.8)

    • ArrayList扩容优化策略:建议给定一个预估计的初始化容量,减少数组的扩容次数

    • Collections是集合框架中的一个工具类。该类中的方法都是静态的。提供的方法中有可以对list集合进行排序,二分查找等方法。通常常用的集合都是线程不安全的。因为要提高效率。如果多线程操作这些集合时,可以通过该工具类中的同步方法,将线程不安全的集合,转换成安全的。collections.synchronizedList(集合)

  2. LinkedList

    • LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。

    • LinkedList 实现 List 接口,能对它进行队列操作。

    • LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。

    • LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。

    • LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。

    • 没有初始化容量,最初链表中无任何元素,first和last都为null。

    • LinkedList 是非同步的。不是线程安全的

    • 优点:随机增删元素效率较高(增删元素不涉及到大量元素位移)

    • 缺点:查询效率较低,每一次查找元素都要从头节点开始往下遍历。

  3. Vector

    • 向量(Vector)是一个封装了动态大小数组的顺序容器(Sequence Container)。跟任意其它类型容器一样,它能够存放各种类型的对象。可以简单的认为,向量是一个能够存放任意类型的动态数组

    • 初始化容量为10,扩容为原本容量的2倍。

    • 线程同步的

泛型

  • 所有集合类都位于java.util包中,集合中只能保存对象的引用。集合类把它所含有的元素看成是Object的实例,这样方便但是也有隐患,即多个类型不同的元素被放入一个集合中,会增加集合访问时类型转换的困难,甚至会产生错误。泛型的引入改善了这种情况,使用泛型来限制集合里元素的类型,并让集合记住元素的类型。可以允许编译器检查加入集合的元素类型,避免值类型不一致的错误。

  • 优点:集合中存储的元素类型统一了,从集合中取出的元素是泛型指定的类型,不需要进行大量的“向下转型”

  • 缺点:导致集合中存储的元素缺乏多样性。

  • 只在程序编译阶段起作用,给编译器参考,运行阶段泛型不参与

  • 自动类型推断机制(钻石表达式)

    List<String> list =  new ArrayList<>()//后面的<>中的类型会根据前面的自动推断 
  • 可以自定义泛型,自定义泛型<>中是一个标识符,可以随便写。在迭代器类型中指定泛型,可以在迭代中直接获取参数类型,若获取的是对象,则要转化。

Set接口

  • set接口是继承自Collection的子接口,特点是元素不重复,存储无序。 在set接口的实现类中添加重复元素是不会成功的,判断两个元素是否重复根据元素类重写的hashCode()和equals()方法。

  1. HashSet

    • 无序不可重复,实际为一个HashMap的实例,允许null.

    //构造方法
    public HashSet(){
        map = new HashMap<>();
    }
    //add方法
    public boolean add(E e){
        return map.put(e,PRESENT)==null;//其底层调用的是HashMap的添加元素方法
    }
    • 底层是哈希表数据结构,放到HashSet集合中的元素实际上是放到HashMap集合的key部分了。HashSet保证元素唯一哈希表依赖于哈希值存储

  2. TreeSet

    • TreeSet基于 TreeMap 的 NavigableSet 实现使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。排序方式完全取决于使用的构造方法。使用元素的自然顺序对元素进行排序(自然排序)根据创建 set 时提供的 Comparator 进行排序(比较器排序)TreeSet保证元素的排序和唯一性的 底层数据结构是红黑树(自平衡二叉树)

Map接口

  • Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。

  • 它也没有继承Collection

  • 在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。key值有HashSet的特性,无序不可重复。key是主导,value是附属

  • key和value都是引用数据类型,存储的都是对象的内存地址。

  • 常用方法

    /*Map接口中定义的一些常用方法:
    * public V put (K key,V value):把键与值添加到Map集合中
    * public V get(Object key):根据指定的键,获取对应的值
    * public V clear() :清空Map集合
    * boolean containKey(Object key):判断是否包含指定的key
    * boolean containValue(Object value):判断是否包含指定的value
    * boolean isEmpty() :判断Map集合中元素个数是否为空
    * public V remove (Object key):删除key对应的值
    * int size() :获取集合中键值对的个数
    * Collection<V> values() :获取Map集合猴子那个所有的value,返回一个Collection
    * Set<K> keySet:获取Map集合中所有的Key,存储到set集合中
    * Set<Map.entry<K,v>> entrySet() :当Map类集合创建的时候,会生成一个Entry对象
    * Map.entry<K,v>是一个接口
    * 他是用来存储key和value的,每一组一个对象。这个方法可以把这些所有的Entry对象放到Set集合中。Set集合中元素类型是Map.Entry<K,V>
    * Entry对象中有getKey()和getValue()两种方法可以提取出key和value的值*/
  • Map集合遍历

  • 使用Set集合迭代器方法

    1、先使用keySet()方法获取关键字集合的Set集合

  • 2、再根据Set集合的iterator方法得到相应的迭代器

  • 3、根据Set的迭代器进行遍历

    • 方式一:keySet()迭代器while循环

    • System.out.println("==========keySet()迭代器while循环==========");
      
              Set<可以添加泛型> keySet = map.keySet();
              Iterator<> it = keySet.iterator();//这里添加泛型后,就可以在while循环里获取这种泛型类型的对象,不在只能是Object对象
              while(it.hasNext()){
                  Object key = it.next();
                  System.out.println(key+"---"+map.get(key));
              }

       

    • 方式二:foreach循环

    • System.out.println("==========keySet()foreach循环==========");
              Set<可以添加泛型> keySet = map.keySet();
              for (Object(可以为泛型类型) key1 : keySet) {
                  System.out.println(key1+"---"+map.get(key1));
              }

       

  • 使用entrySet迭代器方法

    • 1、先使用Map接口的entrySet( )方法得到一个Set集合,Set集合元素类型是Map.Entry

      2、根据得到的Set集合得到新的iterator迭代器

      3、根据迭代器进行遍历,每次取出一个Node

      4、从Node中获取key和value

    • 方式三:entrySet()迭代器while循环
    • System.out.println("=========entrySet()迭代器while循环==========");
              Set entrySet = map.entrySet();
              Iterator iterator = entrySet.iterator();
              while(iterator.hasNext()){
                  Map.Entry node = (Map.Entry)iterator2.next();
                  System.out.println(node.getKey()+"---"+node.getValue());
              }

       

    • 方式四:entrySet()foreach循环//效率较高,直接从Node对象中获取属性值,适合大容量数据

    • System.out.println("==========entrySet()foreach循环==========");
              for (Object object : entrySet) {
                  Map.Entry node2 = (Map.Entry)object;
                  System.out.println(node2.getKey()+"---"+node2.getValue());
              }
  • 完整代码(使用泛型)

    public class MapForeach {
        public static void main(String[] args) {
            //HashMap<Object, Object> map1 = new HashMap<>();
            Map<Integer,String> map = new HashMap<>();//
            map.put(001,"一号");
            map.put(002,"二号");
            map.put(003,"三号");
            map.put(004,"四号");
            map.put(005,"五号");
            System.out.println("=========keySet()迭代器while循环==========");
            Set<Integer> keys = map.keySet();
            Iterator<Integer> it = keys.iterator();
            while(it.hasNext()){
                Integer key = it.next();
                System.out.println(key+"-->"+map.get(key));
            }
            System.out.println("=========keySet()foreach循环==========");
            for (Integer key1 : keys) {
                System.out.println(key1+"---"+map.get(key1));
            }
            System.out.println("=========entrySet()迭代器while循环==========");
            Set<Map.Entry<Integer, String>> entries = map.entrySet();
            Iterator<Map.Entry<Integer, String>> iterator = entries.iterator();
            while(iterator.hasNext()){
                Map.Entry<Integer, String> node = iterator.next();
                System.out.println(node.getKey()+"-->"+node.getValue());
            }
            System.out.println("=========entrySet()foreach循环==========");
            for(Map.Entry<Integer,String> node2 :entries){
                System.out.println(node2.getKey()+"---"+node2.getValue());
            }
        }
    }

     

1、HashMap

  • 集合底层是哈希表(散列表)的数据结构

  • 哈希表:是一个数组和单向链表的结合体哈希表的主干就是数组,链表则是主要为了解决哈希冲突而存在的比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。 这个函数可以简单描述为:存储位置 = f(关键字) ,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。

  • 插入过程

     

     

  • 哈希冲突

    • 如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

  • 部分成员变量如下:
    //初始值,为16,必须为2的次幂
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;    
    //当容量被占满0.75时就需要reSize扩容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //链表长度到8,就转为红黑树
    static final int TREEIFY_THRESHOLD = 8;
    // 树大小为6,就转回链表
    static final int UNTREEIFY_THRESHOLD = 6;
  • 底层分析

//HashMap集合底层源代码
public class HashMap{
    //HashMap底层实际上就是一个数组,数组中每一个元素是一个单向链表
   transient Node<K,V>[] table;
      //静态内部类HashMap.Node
     static class Node<K,V>{
         final int hash;//哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希算法,可以转换成数组下标)
         final K key;//存储到Map集合中的Key
         V value;//存储到Map集合中的value
         Node<K,V> next;//下一个节点的内存地址
     }
}
  • HashMap总体结构:横向的为数组,纵向的为链表,一个Entry就是一个Node

 

 

  • 在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操作的时候才真正构建table数组

  • put(K,V)操作实现原理

    1. 先将K,V封装到Node对象中

    2. 底层调用K的hashCode()方法得出hash值,通过哈希算法/函数,将hash值转换成数组下标,下标位置处如果没有任何元素,把Node添加到这个位置上。

    3. 如果已经有元素,就把K和链表上的节点进行equals比较,如果返回false,节点添加到链表末尾,如果返回true,把这个节点的value值覆盖

  • get(K,V)操作实现原理

    1. 先调用K的hashCode()方法得出哈希值,通过哈希算法转化成数组下表,通过数组下标快速定位到某个位置,如果这个位置什么也没有,返回null。

    2. 如果这个位置有Node,参数K和每个Node中的K进行equals比较,如果返回false,那么返回null,没有找到。如果,返回true,返回这个Node中的value值

  • 可以看出无论是put(K,V)还是get(K,V),都会先调用K的hashCode方法获取hash值,再调用equals()方法进行元素值比较,放在HashMap集合Key部分其实就是放在HashSet集合中了。所以hashCode()与equals()方法都需要重写(HashMap集合key部分和HashSet)。

  • 为什么要重写hashCode()与equals()方法

    • 在object类中,hashcode()方法是本地方法,返回的是对象的地址值,而object类中的equals()方法比较的也是两个对象的地址值,如果equals()相等,说明两个对象地址值也相等,当然hashcode()也就相等了;在String类中,equals()返回的是两个对象内容的比较,当两个对象内容相等时,Hashcode()方法根据String类的重写代码的分析,也可知道hashcode()返回结果也会相等。以此类推,可以知道Integer、Double等封装类中经过重写的equals()和hashcode()方法也同样适合于这个原则。当然没有经过重写的类,在继承了object类的equals()和hashcode()方法后,也会遵守这个原则。

    • 在定义类时,我们经常会希望两个不同对象的某些属性值相同时就认为他们相同,所以我们要重写equals()方法,按照原则,我们重写了equals()方法,也要重写hashCode()方法,

    • 如果两个对象相同,那么它们的hashCode值一定要相同;如果两个对象的hashCode相同,它们并不一定相同(这里说的对象相同指的是用eqauls方法比较)。所以java中的很多类都重写了这两个方法,例如String类,包装类,在java应用程序运行时,无论何时多次调用同一个对象时的hashCode()方法,这个对象的hashCode()方法的返回值必须是相同的一个int值

    • equals()方法不相等的两个对象,hashcode()有可能相等。反过来,hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。

  • 同一个单向链表上所有节点的hash基本相同(由于hash算法的不同,可能不同的hash可以计算出相同的数组下标),同一个链表上的K和K的equals方法返回值为false,不可重复。

  • [参考链接](https://blog.csdn.net/woshimaxiao1/article/details/83661464)

2、HashTable

  • (1)线程安全:HashMap是线程不安全的类,多线程下会造成并发冲突,但单线程下运行效率较高;HashTable是线程安全的类,很多方法都是用synchronized修饰,但同时因为加锁导致并发效率低下,单线程环境效率也十分低;

  • (2)插入null:HashMap允许有一个键为null,允许多个值为null;但HashTable不允许键或值为null;

  • (3)容量:HashMap底层数组长度必须为2的幂,这样做是为了hash准备,默认为16;而HashTable底层数组长度可以为任意值,这就造成了hash算法散射不均匀,容易造成hash冲突,默认为11;

  • (4)Hash映射:HashMap的hash算法通过非常规设计,将底层table长度设计为2的幂,使用位与运算代替取模运算,减少运算消耗;而HashTable的hash算法首先使得hash值小于整型数最大值,再通过取模进行散射运算;

  • (5)扩容机制:HashMap创建一个为原先2倍的数组,然后对原数组进行遍历以及rehash;HashTable扩容将创建一个原长度2倍的数组,再使用头插法将链表进行反序;

  • (6)结构区别:HashMap是由数组+链表形成,在JDK1.8之后链表长度大于8时转化为红黑树;而HashTable一直都是数组+链表;

  • (7)继承关系:HashTable继承自Dictionary类;而HashMap继承自AbstractMap类;

  • (8)迭代器:HashMap是fail-fast;而HashTable不是

  • properties

    • properties是一个Map集合,key和value都是String类型,被称为属性类对象,是线程安全的

    • setProperties(key,value);
      getProperties(Key);

3、TreeMap

  • TreeSet集合底层实际上是一个TreeMap

  • TreeMap底层是一个二叉树

  • 放到TreeSet集合中的元素,等同于放到TreeMap集合Key部分了

  • key无序不可重复,但可以按照元素大小顺序自动排序。不允许出现重复的key;可以插入null键,null值;

  •  

posted @ 2021-08-12 17:32  长安街ぷ  阅读(163)  评论(0)    收藏  举报