10.Java集合汇总

1. Java集合概述

集合就是可以动态存放多个对象的容器

数组在存储数据方面的弊端:

  • 数组长度不可变,不便于扩展

  • 集合提供了多种方法操作集合,这是数组不具备的

  • 数组存储数据类型单一

集合的使用场景

1.1 Java集合框架概述

1.2 Collection接口继承树

1.3 Map接口继承树

2. Collection接口

  • Collection 接口是 List、Set 和 Queue 接口的父接口
  • 在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。

2.1 Collection接口方法

注意:向Collection接口的实现类的对象中添加obj,建议要重写该obj类的equals方法!!(否则会遇到大麻烦)toString方法也要重写

  1. 添加

    • add(Object obj)
    • addAll(Collection coll)
  2. 获取有效元素的个数:int size()

  3. 清空集合:void clear()

  4. 是否是空集合:boolean isEmpty()

  5. 是否包含某个元素:

    • boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
    • boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
  6. 删除:

    • boolean remove(Object obj):通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
    • boolean removeAll(Collection coll):通过调用equals方法,取当前集合的差集(删除交集)
  7. 取两个集合的交集:boolean retainAll(Collection c):把交集的结果存在当前集合

  8. 集合是否相等:boolean equals(Object obj),object若不是Collection,一定返回false

  9. 转成对象数组:Object[] toArray()

  10. 获取集合对象的哈希值:hashCode()

  11. 遍历:iterator():返回迭代器对象,用于集合遍历

  12. asList方法

    //拓展:数组 --->集合:调用Arrays类的静态方法asList()
    List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
    System.out.println(list);
    
    List arr1 = Arrays.asList(new int[]{123, 456});
    System.out.println(arr1.size());//1,认为只有一个元素--arr1的元素类型为数组,解决办法看下面:
    
    List arr2 = Arrays.asList(new Integer[]{123, 456});
    System.out.println(arr2.size());//2
    

说说下面输出结果:

@Test
public void test1(){
    //用ArrayList来展示Collection接口的方法
    Collection coll = new ArrayList();
    
    //add()的用法
    coll.add(123);
    coll.add(456);
    coll.add(new Person("Jerry",20));
    coll.add(new String("Tom"));
    coll.add(false);
    
    //contains(Object obj):判断当前集合中是否包含obj
    //我们在判断时会调用obj对象所在类的equals()。
    System.out.println(coll.contains(123));//此处调用Integer重写的equals()方法
    
    System.out.println(coll.contains(new String("Tom")));//此处调用String重写的equals()方法

    System.out.println(coll.contains(new Person("Jerry",20)));//调用Person中的equals()方法,没有重写就相当于比较地址值,此处重写的equals再第一行加入System.out.println("Person equals()....");


    //containsAll(Collection coll1):判断形参coll1中的所有元素是否都存在于当前集合中。
    Collection coll1 = Arrays.asList(123,4567);
    System.out.println(coll.containsAll(coll1));
}

输出结果:

image-20230125235752689

思考为什么输出了三个Person equals()…(提示信息:Person类中重写equals()方法中,只要调用一次equals方法,就输出一次Person equals()…)

而输出三次Person equals()…是由于分别和123,456和new Person(“Jerry”,20)比对三次导致的

3 Iterator迭代器接口

Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素

  • 迭代器模式:访问容器(container)中各元素,而不需暴露容器内部细节。迭代器模式,就是为容器而生。
  • Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象
  • Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
  • 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前

3.1 Iterator接口的方法

在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常。

  • 便利集合常见的方式 hasNext()+Next()
//hasNext():Returns true if the iteration has more elements.
while(iterator.hasNext()){
    //next():Returns the next element in the iteration
    System.out.println(iterator.next());
}
  • Iterator接口中remove()方法
//测试Iterator中的remove()
//如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,
//再调用remove都会报IllegalStateException。
@Test
public void test3(){
    Collection coll = new ArrayList();
    coll.add(123);
    coll.add(456);
    coll.add(new Person("Jerry",20));
    coll.add(new String("Tom"));
    coll.add(false);

    //删除集合中"Tom"
    Iterator iterator = coll.iterator();
    while (iterator.hasNext()){
        Object obj = iterator.next();
        if("Tom".equals(obj)){
            iterator.remove();
        }
    }
    
    //遍历集合
    iterator = coll.iterator();
    while (iterator.hasNext()){
        System.out.println(iterator.next());
    }
}
  • Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法。

  • 如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。

3.2 foreach循环

foreach也叫增强for循环,用于遍历集合或数组

  • foreach底层调用Iterator完成操作
  • foreach还可以用来遍历数组

练习:判断输出结果:

public class ForTest {
    public static void main(String[] args) {
        String[] str = new String[5];
        for (String myStr : str) {
            myStr = "atguigu";
            System.out.println(myStr);
        }
        System.out.println(Arrays.toString(str));
    }
}

4 List接口

  • 鉴于Java中数组的局限性,通常用List替代数组
  • List集合类中元素有序可重复,List中的每个元素都有对应的顺序索引,支持根据索引直接获取元素(可以直接理解为可扩展的动态数组)
  • List接口常用的实现类有:ArrayList、LinkedList 和 Vector

三种List的实现类分析:

  1. ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储
  2. LinkedList:对于频繁的插入、删除操作,使用此类效率高;底层使用双向链表存储。线程不安全
  3. Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储

4.1 List接口方法

List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。

  1. void add(int index, Object ele):在index位置插入ele元素

  2. boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来

  3. Object get(int index):获取指定index位置的元素

    上面Iterator中讲述了两种遍历Collection的方法,对于List可以结合size()方法和get()方法直接用for循环进行遍历

  4. int indexOf(Object obj):返回obj在集合中首次出现的位置

  5. int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置

  6. Object remove(int index):移除指定index位置的元素,并返回此元素(重载Collection接口的remove,Collection中的根据对象删除任然能使用)

  7. Object set(int index, Object ele):设置指定index位置的元素为ele

  8. List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合

简单来记忆就是增,删,改,查,插,长度,遍历

4.1 ArrayList

ArrayList是动态数组

ArrayList的JDK1.8之前与之后的实现区别?

  • JDK1.7:初始状态创建长度为10的数组,空间不足则扩容为原来的1.5倍并将数据迁移

    • ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
    • list.add(123);//elementData[0] = new Integer(123);
    • list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。

    默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。

    结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)

  • JDK1.8:首次创建长度为0的数组,首次添加元素,创建长度为10的数组,后续扩容也是1.5倍并数据迁移

    • ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没有创建长度为10的数组、
    • list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]------后续的添加和扩容操作与jdk 7 无异。

小结:jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。

Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是Vector 实例。Arrays.asList(…) 返回值是一个固定长度的 List 集合

4.2 LinkedList

频繁插入删除操作适合使用LinkedList。底层是双向链表存储

相较于与ArrayList新增方法:头插,尾插,头节点,尾节点,头删,尾删

  • void addFirst(Object obj)
  • void addLast(Object obj)
  • Object getFirst()
  • Object getLast()
  • Object removeFirst()
  • Object removeLast()

LinkedList:底层为双向链表,定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:

  • prev变量记录前一个元素的位置
  • next变量记录下一个元素的位置
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.3 Vector

Vector大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。

ArrayList是用的最多的。但LinkedList插入删除效率高。Vector使用的很少

新增方法:

  • void addElement(Object obj)
  • void insertElementAt(Object obj,int index)
  • void setElementAt(Object obj,int index)
  • void removeElement(Object obj)
  • void removeAllElements()

4.4 面试题

请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?

  • ArrayList和LinkedList的异同二者都线程不安全,相对线程安全的Vector,执行效率高。此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
  • ArrayList和Vector的区别Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制(使用工具类,将ArrayList变为线程安全的)。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。

程序题:remove调用的是Collection中的方法还是List中的方法:

@Test
public void testListRemove() {
    List list = new ArrayList();
    list.add(1);
    list.add(2);
    list.add(3);
    updateList(list);
    System.out.println(list);//
}
private static void updateList(List list) {
    list.remove(2);
}

本题考查的是到底条用的remove是Collection中的方法,还是List中的方法。Collection中的remove是按equals方法匹配,List中的remove则是按照下标匹配

调用方法从当前类匹配,没有匹配方法才会到父类中找。所以List中的remove匹配上就调用了呗,就不会去父类中找了

若想要调用Collection的方法,那么就直接传进Integer的参数即可

5 Set接口

  • Set接口是Collection的子接口,set接口没有提供额外的方法

  • List接口有序可重复,而Set接口无序不重复。Set完全相当于高中数学中集合的概念。

    1. 无序性:不等于随机性。所谓无序性指的是存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的

    2. 不可重复性:保证添加的元素按照equals()判断时,不能返回true.即:相同的元素只能添加一个。

5.1 HashSet

HashSet是Set接口的实现类,用的很多。HashSet 不是线程安全的,集合元素可以是 null

HashSet 集合判断两个元素相等的标准: hashCode() 方法和equals() 方法返回值均相等才判断两个对象相等。

对于存放在Set容器中的对象,对应的类一定要重写equals()hashCode(Object obj)方法,以实现对象相等规则。

5.1.1 HashSet添加元素底层

当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好)

  • 如果此位置上没有其他元素,则元素a添加成功。 —>情况1
  • 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
    • 如果hash值不相同,则元素a添加成功。—>情况2
    • 如果hash值相同,进而需要调用元素a所在类的equals()方法:
      • equals()返回false,则元素a添加成功。—>情况3
      • equals()返回true,元素a添加失败

对于添加成功的情况2和情况3而言:元素a与已经存在指定索引位置上数据以链表的方式存储。七上八下

  1. jdk 7:新元素放到数组中,指向原来的元素。
  2. jdk 8:原来的元素在数组中,指向新元素

底层也是数组,初始容量为16,当如果使用率超过0.75,(16*0.75=12)就会扩大容量为原来的2倍。(16扩容为32,依次为64,128…等)

5.1.2 HashSet重写相关细节

其实多数情况下就自动生成,没有猪b会自己写

重写hashCode()方法的基本原则

  • 同一对象多次调用 hashCode()方法应该返回相同的值。
  • 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
  • 对象中用作 equals() 方法比较的字段,都应该用来计算 hashCode 值。

重写equals()方法的基本原则

重写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

当自定义的类有自己特定的”逻辑相等“的概念,就需要重写equals方法。由于相等的对象必须要有相等的散列码(哈希别称)。如果没有重写hashCode(),就调用Object中的hashCode()。根据Object.hashCode()方法,它们仅仅是两个对象。显然违反了“相等的对象必须具有相等的散列码”。所以equals和hashCode往往是一起重写的

Eclipse/IDEA工具里hashCode()的重写

以Eclipse/IDEA为例,在自定义类中可以调用工具自动重写equals和hashCode。

问题:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?

  • 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
  • 并且31只占用5bits,相乘造成数据溢出的概率较小。
  • 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
  • 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)

5.2 LinkedHashSet

LinkedHashSet 是 HashSet 的子类

  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。(就是再原来HashSet的基础上加了双向链表)
  • LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
  • LinkedHashSet 不允许集合元素重复(Set的特性)

5.3 TreeSet

  • TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态(所以TreeSet中元素必须是相同类型)。

  • TreeSet底层使用红黑树结构存储数据

  • 新增的方法如下: (了解)

    1. Comparator comparator()
    2. Object first()
    3. Object last()
    4. Object lower(Object e)
    5. Object higher(Object e)
    6. SortedSet subSet(fromElement, toElement)
    7. SortedSet headSet(toElement)
    8. SortedSet tailSet(fromElement)
  • TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序

TreeSet和后面要讲的TreeMap采用红黑树的存储结构

5.3.1 自然排序

自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列

  • 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。(回忆之前HashSet,若将对象添加到HashSet中必须重写HashCode和equals)

  • 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。

  • Comparable 的典型实现:(回忆常用类)

    • BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较
    • Character:按字符的 unicode值来进行比较
    • Boolean:true 对应的包装类实例大于 false 对应的包装类实例
    • String:按字符串中字符的 unicode 值进行比较
    • Date、Time:后边的时间、日期比前面的时间、日期大(因为时间对应的时到1970年1月1日0时0分0秒的毫秒数,数字大自然就大)
  • 向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。

  • 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。

  • 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较返回值。不同于Set的其他接口使用equals比较是否相等

  • 当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。否则,让人难以理解。(一般不会出问题的)

可以发现List和Set都建议重写equals方法,List的contain方法会调用,set本身不重复也会调用。相应的toString方法也最好重写。特殊的HashSet需要重写HashCode方法用于比较是否相等

5.3.2 定制排序

TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。

定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。

  • 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
  • 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
  • 此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。
  • 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。

更具体的可以参考常用类中的比较器的讲解

6 Map接口

  • Map与Collection并列存在。存储key-value键值对
  • Map 中的 key 和 value 都可以是任何引用类型的数
  • Map 中的 key 用Set(无序不重复)来存放,不许重复,即同一个 Map 对象所对应的类,须重写hashCode()equals()方法(Set使用hashCode和equals来保证无序不重复)
  • 常用String类作为Map的键
  • Map接口的常用实现类:HashMapTreeMapLinkedHashMapProperties。其中,HashMap是 Map 接口使用频率最高的实现类
Map的实现类的结构:
|----Map:双列数据,存储key-value对的数据   ---类似于高中的函数:y = f(x)
     |----HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value
          |----LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
                  原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
                  对于频繁的遍历操作,此类执行效率高于HashMap。
     |----TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序
                  底层使用红黑树
     |----Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
          |----Properties:常用来处理配置文件。key和value都是String类型

6.1 Map接口方法

  • Object put(Object key,Object value):将指定key-value添加到(或若重复则修改)当前map对象中(Collection中是add)

  • void putAll(Map m):将m中的所有key-value对存放到当前map中

  • Object remove(Object key):移除指定key的key-value对,并返回value

    如果不存在该键值对,返回null

  • void clear():清空当前map中的所有数据(内部数组元素置null)

  • Object get(Object key):获取指定key对应的value

  • boolean containsKey(Object key):是否包含指定的key

  • boolean containsValue(Object value):是否包含指定的value

  • int size():返回map中key-value对的个数(键值对个数)

  • boolean isEmpty():判断当前map是否为空(内部用isEmpty来判断是否为空)

  • boolean equals(Object obj):判断当前map和参数对象obj是否相等

  • Set keySet():返回所有key构成的Set集合

  • Collection values():返回所有value构成的Collection集合

    values和keySet的遍历顺序是一一对应的

    案例:map集合中的valueList<T>类型,如何将mapvalue放入List<T>类型变量中

    //返回 map 中存放的所有 T 对象
    public List<T> list(){
      //错误的:
      //Collection<T> values = map.values();
      //return (List<T>) values;
      //正确的:
      ArrayList<T> list = new ArrayList<>();
      Collection<T> values = map.values();
      for(T t : values){
          list.add(t);
      }
      return list;
    }
    

    回顾类型转换,用父类引用指向子类实例,这个是多态。父类类型转为子类类型,这个是强制类型转换,但是强制类型转换要求instanceof为true,可以说这个强制类型转换只是还原其本身的面目,而非基本数据类型理解的那种强制类型转换

    这里value本身就是Collection,不能强制类型转换为List

  • Set entrySet():返回所有key-value对构成的Set集合(集合原色为kv键值对)

    集合为Emtry类型,在Emtry内部有相应的方法获得相应的key和value

遍历Map

@Test
public void test5(){
    Map map = new HashMap();
    map.put("AA",123);
    map.put(45,1234);
    map.put("BB",56);

    //遍历所有的key集:keySet()
    Set set = map.keySet();
    Iterator iterator = set.iterator();
    while(iterator.hasNext()){
        System.out.println(iterator.next());
    }
    System.out.println();
    
    //遍历所有的value集:values()
    Collection values = map.values();
    for(Object obj : values){
        System.out.println(obj);
    }
    System.out.println();
    
    //遍历所有的key-value
    //方式一:entrySet()
    Set<Map.Emtry<String,Integer>> entrySet = map.entrySet();
    Iterator<Map.Emtry<String,Integer>> iterator1 = entrySet.iterator();
    while (iterator1.hasNext()){
        Object obj = iterator1.next();
        //entrySet集合中的元素都是entry
        Map.Entry entry = (Map.Entry) obj;//Emtry是接口!!
        System.out.println(entry.getKey() + "---->" + entry.getValue());

    }
    System.out.println();
    //方式二:
    Set keySet = map.keySet();
    Iterator iterator2 = keySet.iterator();
    while(iterator2.hasNext()){
        Object key = iterator2.next();
        Object value = map.get(key);
        System.out.println(key + "=====" + value);

    }

}

总结:增删改查长度遍历,插?插p无序插der

/*
 * 添加:put(Object key,Object value)
 * 删除:remove(Object key)
 * 修改:put(Object key,Object value)
 * 查询:get(Object key)
 * 长度:size()
 * 遍历:keySet() / values() / entrySet()
 */

6.2 HashMap

  • HashMap是 Map 接口使用频率最高的实现类。
  • 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
  • map类型中key采用set集合存储,所以key无序不重复。并且,key所在的类要重写:equals()和hashCode()
  • map类型中value构成的集合是Collection,所以value无序可重复。所以,value所在的类要重写:equals()
  • kv键值对构成entry,entry内部包含获取key和value的方法
  • HashMap 判断两个 key 相等的标准是:equals()和hashCode()均返回true(equals()相等则HashCode一定相等,所以往往先比较HashCode)。
  • HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。

6.2.1 HashMap的存储结构

类似HashSet,HashSet将存储的值映射到底层数组。HashMap将键值对的键映射到底层数组,存储键值对,过程类似HashSet。

  • JDK 7及以前版本:HashMap是数组+链表结构
  • JDK 8版本发布以后:HashMap是数组+链表+红黑树实现

JDK7及以前—数组加链表

JDK8-数组加链表加红黑树(开始存储链表,超过一定长度转换为红黑树提高查询效率,小于一定长度转回链表)

image-20230227160106944

6.2.2 HashMap源码常量

  • DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16

  • MAXIMUM_CAPACITY : HashMap的最大支持容量 230

  • DEFAULT_LOAD_FACTOR:HashMap的默认加载因子

  • TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树

  • UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表

  • MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作,这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)

  • table:存储元素的数组,总是2的n次幂

  • entrySet:存储具体元素的集

  • size:HashMap中存储的键值对的数量

  • modCount:HashMap扩容和结构改变的次数。

  • threshold:扩容的临界值,=容量*填充因子

  • loadFactor:填充因子

6.2.3 JDK8前HashMap

6.2.3.1 HashMap构造器

HashMap的内部存储结构其实是 数组和链表的结合 ** 。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry类型的数组table**,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。(数组加链表)

而且新添加的元素作为链表的head。(七上八下)

//默认构造器,底层调用双参数的构造器
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);
}

//设置初始容量的Hashmap,底层调用双参数的构造器,值得注意容量只会是2的整数倍。指定容量为15,那么容量就会是16
public HashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//双参数构造器,第一个参数指代容量,第二个参数指代加载因子
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }
6.2.3.2 MashMap添加元素

这里以put为例讲解

向HashMap中添加entry(key1,value1),需要首先计算key1的哈希值(根据key1所在类的hashCode()计算得到,此哈希值经过处理以后(散列映射),得到在底层Entry[]数组中要存储的位置i。

  • 如果位置i上为空,则entry直接添加成功
  • 如果位置i上不为空(意味着此位置上存在一个或多个数据(以链表形式存在)),则需要通过循环的方法,依次比较entry中key和其他的entry的key的哈希值
    • 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
    • 如果key1的哈希值和已经存在的某一个数据(key2-value2)的key哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:
      •          **如果equals()返回false:此时key1-value1添加成功。----情况3**
        
      •          **如果equals()返回true:使用value1替换value2。**
        

不同于HashSet中的key值相等则添加失败,HashMap若key相等则替换

//put源码
public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);//将null添加到Hashmap
    
    int hash = hash(key);//计算哈希值
    int i = indexFor(hash, table.length);//计算Hashmap存放位置
    
    //
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //如果相等,就将value值取代
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);//内部调用resize存在扩容,后一部分有讲
    return null;
}

值得关注为什么put有一个引用类型变量,这是因为在hashset中的add方法调用的就是hashmap的put方法,判断put方法的返回值是否等于空,来保证hashset的值不重复。

6.2.3.3 HashMap扩容

当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高(桶bucket存放的链表变长),因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算(扩容后,数据正确存放位置发生变化,取模)

其在新数组中的位置,并放进去,这就是resize

那么HashMap什么时候进行扩容呢?

当HashMap中的元素个数超过 length*loadFactor 时(数组总大小length,不是数组中有效数据的个数size) , 就会进行数组 扩容,loadFactor 的默认值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍(源码是移位运算),然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能

6.2.4 JDK8的HashMap

HashMap的内部存储结构其实是 数组+链表+树的结合 。当实例化一个HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组(Entry另有用途了用到LinkedHashMap中去了),这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。

也可能是一个一个TreeNode对象(红黑树存储),每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。

那么HashMap什么时候进行扩容和树形化呢?

当HashMap中的元素个数超过 length*loadFactor 时 (数组总大小length,不是数组中个数size), 就会进行数组扩容 , loadFactor 的默认值 (DEFAULT_LOAD_FACTOR)为0.75.

这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16* 0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。

总结:JDK1.8相较于之前的变化:

  1. HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
  2. 当首次调用map.put()时,再创建长度为16的数组
  3. 数组为Node类型,在jdk7中称为Entry类型
  4. 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
  5. 当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。

面试题:负载因子值的大小,对HashMap有什么影响

  1. 负载因子的大小决定了HashMap的数据密度。
  2. 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  3. 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
  4. 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。

6.2.5 怎样修改 HashMap 的 Key

首先,让我们看看 HashMap 是如何存储键值对的。HashMap 内部使用 Node 类型来维护键值对:

static class Node<K,V> implements Map.Entry<K,V> {
     final int hash;
     final K key;
     V value;
     ...
}

如我们所见,键声明有 final 关键字。因此,我们不能直接修改key值

但是我们可以使用remove()然后put(),以达到修改键的目的。

假设我们在 HashMap 中有键值对 K1 -> V ,我们希望修改为K2 -> V。不能直接将K1修改为K2。但是我们也可以删除 K1 -> V ,并添加 K2 -> V,达到一样的效果。操作如下:

Map<String, Integer> playerMap = new HashMap<>();
playerMap.put("Kai", 42);
playerMap.put("Amanda", 88);
playerMap.put("Tom", 200);
// 用Eric替换Kai
playerMap.put("Eric", playerMap.remove("Kai"));
assertFalse(playerMap.containsKey("Kai"));
assertTrue(playerMap.containsKey("Eric"));
assertEquals((Integer)42, (Integer)playerMap.get("Eric"));

尽管我们的问题已经解决了,但还有一个潜在的问题。我们知道 HashMap 的键是一个 final 变量。所以,我们不能重新分配变量。但是我们可以修改一个 final对象的值。在我们的 playerMap 示例中,键是 String。我们不能改变它的值,因为字符串是不可变的。但是如果它是一个可变对象,我们可以通过修改键来解决问题吗?

首先,我们不应该在 Java 的 HashMap 中使用一个可变对象作为键,因为这可能导致潜在的问题和意外的行为。

这是因为 HashMap 中的键对象用于计算一个哈希码,该哈希码决定了相应的值将被存储在哪个桶中。如果键是可变的并且在被用作 HashMap 中的键之后被更改,哈希码也可以更改。结果,我们将无法正确检索与键关联的值,因为它将位于错误的桶中。

接下来,让我们通过一个例子来理解它。

首先,创建一个只有一个属性的Player类:

class Player {
    private String name;
    
    public Player(String name) {
        this.name = name;
    }

    // 省略了getter和setter方法
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Player)) {
            return false;
        }
        Player player = (Player) o;
        return name.equals(player.name);
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }
}

如我们所见,Player 类在 name 属性上有一个 setter ,所以它是可变的。此外,hashCode() 方法使用 name 属性来计算哈希码。这意味着更改 Player 对象的名字可以使它具有不同的哈希码。

接下来,让我们创建一个 map,并在其中放入一些条目,使用 Player对象作为键:

Map<Player, Integer> myMap = new HashMap<>();
Player kai = new Player("Kai");
Player tom = new Player("Tom");
Player amanda = new Player("Amanda");
myMap.put(kai, 42);
myMap.put(amanda, 88);
myMap.put(tom, 200);
assertTrue(myMap.containsKey(kai));

接下来,让我们将玩家 kai 的名字从 “Kai” 更改为 “Eric”,然后验证我们是否可以得到预期的结果:

// 将Kai的名字更改为Eric
kai.setName("Eric");
assertEquals("Eric", kai.getName());
Player eric = new Player("Eric");
assertEquals(eric, kai);
// 现在,map中既不包含Kai也不包含Eric:
assertFalse(myMap.containsKey(kai));
assertFalse(myMap.containsKey(eric));

如上面的测试所示,更改 kai 的名字为 “Eric” 后,我们无法再使用 kai 或 eric 检索 “Eric” -> 42 的 Entry。然而,对象 Player(“Eric”) 存在于 map 中作为一个键(因为它不在它该在的位置mother fucker):

// 虽然Player("Eric")存在:
long ericCount = myMap.keySet()
 .stream()
 .filter(player -> player.getName()
 .equals("Eric"))
 .count();
assertEquals(1, ericCount);

要理解为什么会这样,我们首先需要了解 HashMap 是如何工作的。

HashMap 维护一个内部哈希表来存储添加到 map 中的键的哈希码。一个哈希码引用一个 map 条目。当我们检索一个条目时,例如通过使用 get(key)方法,HashMap 计算给定键对象的哈希码,并在哈希表中查找哈希码。

在上面的例子中,我们将 kai(“Kai”) 放入 map 中。所以,哈希码是基于字符串“Kai”计算的。HashMap 存储了结果,让我们说 “hash-kai”,在哈希表中。后来,我们将 kai(“Kai”) 更改为 kai(“Eric”)。当我们试图通过 kai(“Eric”) 检索条目时,HashMap计算“hash-eric”作为哈希码。然后,它在哈希表中查找它。当然,它找不到它。

不难想象,如果我们在一个真正的应用程序中这样做,这种意外行为的根本原因将很难找到。

因此,我们不应该在 HashMap 中使用可变对象作为键。更进一步,我们永远不应该修改键

画图加深理解:Kai、Tom、Amanda 分别 put 之后,效果大致如下:

在这里插入图片描述

在这里插入图片描述

其中 Eric 未真实 put,紫色这里只是模拟如果 put 之后,一个虚拟的位置(即 如果有 Eric 这个 Key put 之后应该在哪里)。

因为 Player 是可变的,那么直接 name 设置为 Eric 会导致 Map 中原本为 Kai 的对象的 name 改为了 Eric (注意此时该对象位置未发生变化)。

在这里插入图片描述

总结:在该部分中,我们学习了remove() 然后 put()方法来替换 HashMap 中的一个键。此外,我们通过一个例子讨论了为什么我们应该避免在 HashMap 中使用可变对象作为键,以及为什么我们永远不应该修改 HashMap 中的键。(只有出现可变对象的时候才会有修改键的说法,因为final)

6.3 LinkedHashMap

  • LinkedHashMap是HashMap的子类
  • 再HashMap存储结构的基础上使用了一对双向链表来记录添加元素的顺序
  • 与LinkedHashSet类似,LinkedHashMap可以维护Map的迭代顺序(迭代顺序与Key-Value对的插入顺序一致)

HashMap中的内部类:Node

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

LinkedHashMap中的内部类:Entry

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
    	super(hash, key, value, next);
	}
}

6.4 TreeMap

  • TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。

  • TreeSet底层使用红黑树结构存储数据

  • TreeMap 的 Key 的排序:

    • 自然排序:TreeMap 的 Key类 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
    • 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接
  • TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。

6.5 Hashtable

  • Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的。
  • Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
  • 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
  • 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
  • Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。

6.6 Properties

  • Properties 类是 Hashtable 的子类,该对象用于处理属性文件
  • 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
  • 存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties"));
String user = pros.getProperty("user");
System.out.println(user);

7 Collections常用方法

  • Collections 是一个操作 Set、List 和 Map 等集合的工具类
  • Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
  1. reverse(List):反转 List 中元素的顺序
  2. shuffle(List):对 List 集合元素进行随机排序
  3. sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
  4. sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
  5. swap(List,int,int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
  6. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  7. Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
  8. Object min(Collection)
  9. Object min(Collection,Comparator)
  10. int frequecy(Collection,Object):返回指定集合中指定元素的出现次数
  11. void copy(List dest,List src):将src中的内容复制到dest中
  12. boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值

这里着重看copy的写法,copy的list双方需要确保size()的大小一致

@Test
public void test2(){
    List list = new ArrayList();
    list.add(123);
    list.add(43);
    list.add(765);
    list.add(-97);
    list.add(0);

    //报异常:IndexOutOfBoundsException("Source does not fit in dest")
    //List dest = new ArrayList();
    //Collections.copy(dest,list);
    
    //正确的:
    List dest = Arrays.asList(new Object[list.size()]);
    System.out.println(dest.size());//list.size();
    Collections.copy(dest,list);

    System.out.println(dest);

}

Collections同步控制

Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

8 面试题

8.1 HashMap和Hashtable的区别

  1. HashMap与Hashtable都实现了Map接口。由于HashMap的非线程安全性,效率上可能高于Hashtable。Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。
  2. HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。
  3. HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。
  4. Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。
  5. Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。

线程安全(效率)+是否添加null+提供方法的变化

8.2 程序输出

public static void main(String[] args) {
    Integer[] datas = {1,2,3,4,5};
    List<Integer> list = Arrays.asList(datas);
    list.add(5);
    System.out.println(list.size());
}

运行异常,不允许添加元素(具体看下)

9 刨析Arrays.asList()

首先Arrays.asList()可不简单,得少用,是这道题让我深入认识这个函数,来看看输出结果?

public static void main(String[] args) {
    Integer[] datas = {1,2,3,4,5};
    List<Integer> list = Arrays.asList(datas);
    list.add(5);
    System.out.println(list.size());
}

结果:运行异常,不允许修改集合。要是答错了,下面的知识可得看看了!!程序员的素养课来啦!


Arrays.asList() 作用是将数组转化为集合,但它返回的集合并非如你所愿。先来看看《阿里巴巴Java开发手册》的讲述:

使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear 等方法会抛出 UnsupportedOperationException 异常。(调用的add/remove其实是AbstractList的方法,该方法就是抛出异常。后面会讲)

说明:asList 的返回对象是一个Arrays 内部类java.util.Arrays.ArrayList(我们常用的式java.util.ArrayList),该类并没有实现集合的修改方法。Arrays.asList() 体现的是适配器模式,只是转换接口,后台的数据仍是数组。

在解决以上问题前,先看看Arrays.asList()的使用吧

9.1 基本数据类型数组转化为List

int[] data = new int[]{1,2,3,4,5};
List list = Arrays.asList(data);
System.out.println(list.size());

输出结果是1,也就是说数组转化为List,是将整个数组作为一个元素!看看 Arrays.asList() 的函数如何定义就能理解了:

public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

可以 asList() 看到使用了可变长泛型来定义,泛型的实例化必须是类,而基本数据类型不支持泛型化。案例中没有规定泛型,于是 asList 将数组作为泛型的实例,解决办法就是使用包装类,如下:

@Test
public void test(){
    Integer[] data = new Integer[]{1,2,3,4,5};
    List<Integer> list = Arrays.asList(data);
    System.out.println(list.size());
}

输出结果:5

9.2 数组转换为集合后进行增删操作

public static void main(String[] args) {
    Integer[] datas = {1,2,3,4,5};
    List<Integer> list = Arrays.asList(datas);
    list.add(5);
    System.out.println(list.size());
}//运行结果:抛出异常!

原因: Arrays.asList() 返回的并非是 java.util.Arrays.ArrayList 。不同于 java.util.ArrayListjava.util.Arrays.ArrayList 类内部的put,add等增删改查方法方法体都是直接抛出异常。下面详细说明:

通过调试发现, Arrays.asList() 返回的并非是 java.util.ArrayList 而是java.util.Arrays.ArrayList!(看红框框处)

java.util.Arrays.ArrayList是工具类Arrays内部的静态类:

public class Arrays {
    //省略其他方法
    
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

    //here
    private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable{

        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }

        @Override
        public int size() {
            return a.length;
        }
    }
}

Arrays.ArrayList 是工具类 Arrays 的一个内部静态类,该静态类继承了 AbstractList,对应类java.util.AbstractList,内部的大部分方法都是抛出异常(只要是修改集合的都是抛出异常)–

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
	//略
	public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }
    //略
}

但是java.util.Arrays.ArrayList重写了父类的getset方法,所以由工具类Arrays.asList()把数组转换成的集合,除了set操作,不允许其他修改集合的操作

而 ArrayList直接实现了List 接口,实现了List所有方法,真正实现了增删等操作。

总结:使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法(可以set),它的add/remove/clear 方法会抛出 UnsupportedOperationException 异常。

9.3 数组转化为集合后的数据异常

看看此题输出?

 @Test
public void test() {
    String[] arr = {"欢迎","关注","Java"};
    List list = Arrays.asList(arr);

    arr[1] = "爱上";
    list.set(2,"我");

    System.out.println(Arrays.toString(arr));
    System.out.println(list.toString());
}

结果:

啊?都一样,震惊一百年啊哥们?

回到刚才的asList的源码:

//Arrays中asList
public class Arrays {
    private final E[] a;
    //省略其他方法
    
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

    //ArrayList<>(a);
    ArrayList(E[] array) {
         a = Objects.requireNonNull(array);//这里的a时内部定义的泛型数组
    }

    //requireNonNull()
    public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }
}

可以看到这是直接将数组引用返回了啊,所以数组和转化后的集合实际上公用一个数组,本质依旧数组,改了其中一个,数据都会变化。你要是不知道,数据异常的事情你是抠破脑袋也想不明白的呀!

总结asList的坑:

  1. 基本数据类型数组转化集合只有一个元素(基本数据类型不支持泛型化)
  2. 数组转化为集合,该集合不能修改,除了set方法(抛出异常)
  3. 数据转化为集合,本质依旧是数组,公用一块空间(内部直接引用赋值)

9.4 数组如何转化为java.util.ArrayList集合

方法一:遍历添加(较low)

Integer intArray[] = {1, 2, 3};
ArrayList<Integer> aList = new ArrayList<>();
for (Integer i: intArray){
    aList.add(i);
}

方法二:用Collections工具类中方法(addAll后面的参数为可变泛型,也就是不支持基本数据类型)

List<String> list = new ArrayList();
Collections.addAll(list, "welcome", "to", "china");
System.out.println(list);

此法效率高?too young too simple!
addAll() 方法的实现就是用的上面遍历的方式。

方法三:将Arrays.asList返回的集合作为ArrayList的构造参数

ArrayList arrayList = new ArrayList<>(Arrays.asList("welcome", "to", "china"));

没有方法可以直接让基本数据类型的数组转化为集合,只能一个个添加,或者使用包装类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值