笛子的Java系列总结——集合篇(含源码分析)

1 Java集合框架概述

1.1 Collection 和 Map.

Java集合可以分为Collection 和 Map 两种体系

  1. Collection接口:单列数据,定义了存取一组对象的方法的集合
  • List:元素有序、可重复的集合
  • Set:元素无序、不可重叠的集合
    (实际上Collection的接口不止这两个,只是这两个最常用,下图中红框中的都是Collection的继承接口)
    在这里插入图片描述
  1. Map接口:双列数据,保持具有映射关系“key-value对”的集合
    不同的key可以对应同一个value;
    但是一个key不能对应多个value。

1.2 Collection接口继承树

在这里插入图片描述

1.3 Map接口继承树

在这里插入图片描述

2 Collection接口方法

可以看之前写的笛子的Java系列总结——集合Collection提供的接口方法

3 Iterator迭代器接口

3.1 迭代器概述

  1. Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素
  2. Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
    定义一个迭代器的代码
Iterator iterator = coll.iterator() // coll 是一个集合类对象
  1. Iterator仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
  2. 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。

3.2 Iterator接口的方法

  1. hasNext() 用于检测iteration中是否还有待遍历的元素
  2. next() 获取到iteration中的下一个元素
  3. remove() 从底层集合中删除此迭代器返回的最后一个元素(可选操作)。

需要注意的地方

  1. 在调用it.next()方法前必须要调用it.hasdNext() 进行检测,若不调用,且下一条记录无效,会抛出NosuchElementException异常。
  2. Iterator接口的remove()方法
    (这部分还是敲代码运行体会一下)
Iterator iter = coll.iterator();//回到起点
while(iter.hasNext()){
    Object obj = iter.next();
    if(obj.equals("Tom")){
        iter.remove();
    }
}

(1)Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法。
(2) 如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。

3.3 使用foreach循环遍历集合元素

Java5.0提供foreach循环迭代访问Collection和数组,这种遍历操作不需要获取集合或数组长度,无需使用索引访问元素,底层调用Iterator完成操作

代码示例:

String[] str = new String[5];
for (String s : str){
    //  操作语句
}

4 Collection子接口之一:List接口

4.1 List接口方法

List除了从Collection集合继承的方法外,List集合里添加了一些根据索引来控制集合元素的方法
在这里插入图片描述

测试代码

    @Test
    // 用来测试List中根据索引操作集合的方法
    public void test1(){
        List ls = new ArrayList();
        ls.add(1);
        ls.add(2);
        ls.add(3);
        System.out.println(ls); // 输出结果为:[1, 2, 3]
        ls.add(1,false);
        ls.add(2,3);
        System.out.println(ls); // 输出结果为:[1, false, 3, 2, 3]
        System.out.println(ls.get(1)); // 输出结果为:false
        System.out.println(ls.indexOf(3)); // 输出结果为:2
        System.out.println(ls.lastIndexOf(3)); // 输出结果为:4
        ls.set(3,"笛子");
        System.out.println(ls); // 输出结果为:[1, false, 3, 笛子, 3]
        ls.remove(1);
        System.out.println(ls); // 输出结果为:[1, 3, 笛子, 3]
    }

有一点需要说明的是,remove方法有remove (int index) 和 remove(Object obj)两种,第一种按照索引删除,第二种按照数据删除(直接删除值等于obj的数据)

4.2 List具体实现类

4.2.1 ArrayList(源码分析)

源于JDK 2,List接口也出现于JDK 2
JDK 7 和 JDK 8有所不同,暂时以JDK 7为例

jdk 7 情况下

ArrayList list = new ArrayList();
这条代码底层创建了长度是10的Object[]数组elementData

list.add(1);
底层执行逻辑——elementData[0] = new Integer(3)

list.add(11);
如果此次的添加导致底层elementData数组容量不够,则扩容。默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的元素复制到新的数组中。

扩容源码如下(当然注释是自己加的)

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 正常情况扩容为原来的1.5倍(0.5倍通过右移位实现)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 扩容完如果还是不够的情况
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 扩容到MAX_ARRAY_SIZE之后会取整型的最大值作为容量了
        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);
    }
JDK 8中ArrayList的变化

ArrayList list = new ArrayList();
底层Object[] elementData初始化为{ },并没有创建长度为10的数组

list.add(1);
第一次调用add()时,底层才创建了长度为10的数组,并将数据1添加到elementData中

后续的添加和扩容操作与JDK 7无异

小结

jdk 7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk 8中的ArrayList对象创建类似于单例的懒汉式,延迟了数组的创建,节省内存。

4.2.2 LinkedList(源码分析)

源于JDK 2,List接口也出现于JDK 2
在JDK 7 和 8 中没什么区别

LinkedList list = new LinkedList();
内部声明了Node类型的first和last属性,默认值为null

list.add(1);
将123封装到Node中,创建了Node对象,其中Node定义如下,证明了LinkedList是双向链表

双向链表中的节点定义

    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;
        }
    }

其他操作:其他操作,比如增删元素等就和算法题里做过的链表操作差不多,暂时不一一看源码了,其实ArrayList也是一样,其他没有介绍的操作和对数组的操作也都差不多

4.2.3 Vector(源码分析)

  1. 源于JDK 1.0,作为List接口的古老实现类,线程安全的,效率低,底层使用Object[]。
  2. 无参构造器 JDK7 和 JDK8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组
public Vector() {this(10);}
  1. 在扩容方面,默认扩容为原来的数组长度的2倍。

4.2.4 ※三者的异同点

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

5 Collection子接口之二:Set接口

5.1 Set特点

5.1.1 概述

  1. Set接口是Collection的子接口,Set接口没有提供额外的方法;
  2. Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个
    Set 集合中,则添加操作失败;
  3. Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法

5.1.2 对Set无序、不可重复的理解

  1. 无序性
    不等于随机性,每次遍历的结果其实是一样的。另外虽然LInkedHashSet遍历顺序与添加顺序一样,但是也不代表其是有序的。
    这里的无序性的意思是针对存储顺序说的,存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定。
  2. 不可重复性
    保证添加的元素按照equals()判断时,不能返回true,即相同的元素只能添加一个

5.2 Set具体实现类

5.2.2 HashSet

底层是使用HashMap实现的

添加元素的过程:以HashSet为例

向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出HashSet底层数组中的存放位置(即为:索引位置),判断此位置上是否已经有其他元素:

情况1:如果此位置上没有其他元素,则元素a添加成功。
如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a和元素b的hash值:

情况2:如果hash值不相同,则元素a添加成功。
如果hash值相同,进而需要调用元素a所在类的equals()方法:

情况3:equals()返回true,元素a添加失败
情况4:equals()返回false,则元素a添加成功

注意的是:哈希值不直接等同于底层数组中的存放位置,中间还经过了某些运算(散列函数)
对于添加成功的的情况2和4而言,元素a与已经存在指定索引位置上数据以链表的方式存储。
jdk 7 :元素a放到数组中,指向原来的元素
jdk 8 :原来的元素在数组中,指向元素a(七上八下)

hashCode() 和 equals()方法的重写
  1. 向Set中添加数据,其所在的类一定要重写equals()和hashCode(),且重写的equals()和hashCode()尽可能保持一致性,即相等的对象比较具有相等的散列码
  2. 重写 hashCode() 方法的基本原则
    (1)在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值
    (2)当两个对象的 equals() 方法比较返回 true 时,这两个对象hashCode()
    方法的返回值也应相等。
    (3)对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值
    注:field(成员变量)是指类的数据成员
  3. 重写 equals() 方法的基本原则
    复写equals方法的时候一般都需要同时复写hashCode方法。 通
    常参与计算hashCode 的对象的属性也应该参与到equals() 中进行计算。(和2中的第三条其实是一个意思,就是把原来判断逻辑相等的成员数据在equals和hashcode重写中要同时考虑)
  4. 为什么不直接equals比较值是否相同,而先要计算hashCode?
    直接用equals比较确实可以完成比较任务,这么做的目的是为了效率。hash算法是二进制算法(位运算),计算式本质是二进制,所以hash算法速度很快。如若hashCode不同则可直接存储不用equlas比较。所以先计算hashCode大大加快了存储速率。
  5. 实际开发中一般不需要自己去设计两个重写方法,可以在自定义类中调用工具自动重写equals和hashCode
    举例:

class User {
   private String name;
   private int age;
   private String email;

   public User(String name, int age, String email) {
       this.name = name;
       this.age = age;
       this.email = email;
   }
   //  IDEA自动重写的equals方法
   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;

       User user = (User) o;

       if (age != user.age) return false;
       if (name != null ? !name.equals(user.name) : user.name != null) return false;
       return email != null ? email.equals(user.email) : user.email == null;
   }
   //  IDEA自动重写的hashCode方法
   @Override
   public int hashCode() {
       int result = name != null ? name.hashCode() : 0;
       result = 31 * result + age;
       result = 31 * result + (email != null ? email.hashCode() : 0);
       return result;
   }
}

5.2.3 LinkedHashSet

  1. LinkedHashSet 是 HashSet 的子类
  2. LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入 顺序保存的。
  3. LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全
    部元素时有很好的性能。

5.2.4 TreeSet(可以按照添加的对象的指定属性进行排序)

  1. 向TreeSet中添加的数据,要求是相同的类
  2. TreeSet底层使用红黑树结构存储数据,所以要求不能有重复的数据
  3. 两种排序方法:自然排序 和 定制排序(之前介绍过的Java两种比较器)

(1)自然排序中,不同于HashSet,对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareT o(Object obj) 方法比较返回值,即compareT o()放回0,不再是equals()。所以:

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

要求2:如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。不重写会抛出异常
在这里插入图片描述

(2)定制排序中,通过通过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接口

6.1 概述

  • Map与Collection并列存在。用于保存具有 映射关系的数据:key-value
  • Map 中的 key 和 value 都可以是任何引用类型的数据
  • Map 中的 key 用Set来存放, 不允许重复,即同一个 Map 对象所对应
    的类,须重写hashCode()和equals()方法
  • 常用String类作为Map的“键”
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到
    唯一的、确定的 value
  • Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和
    Properties。其中,HashMap是 Map 接口使用频率最高的实现类

6.2 Map实现类1:HashMap

6.2.1 底层实现原理

  1. HashMap的底层实现原理(以jdk 7为例)和HashSet除了情况4不同,其他过程基本相同
    (1) HashMap map = new HashMap()
    在实例化以后,底层创建了长度是16的一维数组Entry[] table。
    (2) map.put(key1, value1)
    ① 首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
    ② 如果此位置上数据为空,此时的key1-value1添加成功。 ----情况1
    ③ 如果此位置上的数据不为空(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:

    如果key1的哈希值和已经存在的数据的哈希值都不相同,此时key1-value1添加成功 ---- 情况2

    如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)

    如果equals()返回false:此时key1-value1添加成功 ---- 情况3
    如果equals()返回false:使用value1替换value2 ----情况4

    ④补充:关于情况2和情况3:,此时key1-value1和原来的数据以链表的方式存储,在不同的添加过程中,会涉及到扩容问题,默认的扩容方式:当超出临界值(且要存放的位置非空时),扩容为原来的2倍,并将原有的数据复制过来。

  2. jdk8 相较于jdk7在底层实现方面的不同:
    (1)new HashMap():底层没有创建一个长度为16的数组
    (2)jdk 8底层的数组是:Node[],而非Entry[]
    (3)首次调用put()方法,底层创建长度为16的数组
    (4)jdk7底层结构只有:数组 + 链表。jdk8中底层结构:数组 + 链表 + 红黑树。当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8,且当前数组的长度 > 64时,此时此索引位置上的所有数据改为红黑树存储。

6.2.2 HashMap源码中的重要常量

DEFAULT_INITIAL_CAPACITY:HashMap的默认容量,16
MAXIMUM_CAPACITY:HashMap的最大支持容量,2^30
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子,0.75
TREEIFY_THRESHOLD(桶的树化阙值):Bucket中链表长度大于该默认值,转化为红黑树,8

注解:桶(Bucket),对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),不过 Entry 对象可以包含一个引用变量

UNTREEIFY_THRESHOLD(桶的链表还原阙值):Bucket中红黑树存储的Node小于默认值,转化为链表,6
MIN_TREEIFY_CAPACITY(最小树形化容量阙值):Bucket中的Node被树化时的hash表容量,(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作。这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4
倍。)默认值64


table :存储元素的数组,总是2的n次幂
entrySet :存储具体元素的集
size :HashMap中存储的键值对的数量
modCount :HashMap扩容和结构改变的次数
threshold :扩容的临界值,= 容量*填充因子
loadFactor :填充因子(默认的是DEFAULT_LOAD_FACTOR)

6.3 Map实现类2:LinkedHashMap

6.3.1 说明

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

6.3.2 源码中的存储结构

  1. HashMap中的内部类:Node
// 只截取了源码中的属性部分
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        }
  1. LinkedHashMap中的内部类:Entry
// 重写了HashMap中的newNode方法  Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

// 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 Map实现类3:TreeMap

类似于TreeSet

  • 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通过compareT o()方法或
    者compare()方法返回0。

6.5 Map实现类4:HashTable

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

6.6 Map实现类5:Properties

  • Properties 类是 Hashtable 的子类,该对象用于处理属性文件
  • 由于属性文件里的 key、value 都是字符串类型,所以Properties 里的 key 和 value 都是字符串类型

6.7 Map类的常用方法


  • ① Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
    ② void putAll(Map m):将m中的所有key-value对存放到当前map中

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

  • ① Object put(Object key,Object value):将指定key-value修改当前map对象中(当加入key值相同的键值对时会对value进行更新)
    ② void clear():清空当前map中的所有数据

  • ① Object get(Object key):获取指定key对应的value
    ② boolean containsKey(Object key):是否包含指定的key
    ③ boolean containsValue(Object value):是否包含指定的value
    ④ int size():返回map中key-value对的个数
    ⑤ boolean isEmpty():判断当前map是否为空
    ⑥ boolean equals(Object obj):判断当前map和参数对象obj是否相等
  • 遍历
    ① Set keySet():返回所有key构成的Set集合
    ② Collection values():返回所有value构成的Collection集合;
    ③ Set entrySet():返回所有key-value对构成的Set集合
Map map = new HashMap();
//map.put(..,..)省略
System.out.println("map的所有key:");
Set keys = map.keySet();// HashSet
for (Object key : keys) {
System.out.println(key + "->" + map.get(key));
}
System.out.println("map的所有的value:");
Collection values = map.values();
Iterator iter = values.iterator();
while (iter.hasNext()) {
System.out.println(iter.next());
}
System.out.println("map所有的映射关系:");
// 映射关系的类型是Map.Entry类型,它是Map接口的内部接口
Set mappings = map.entrySet();
for (Object mapping : mappings) {
Map.Entry entry = (Map.Entry) mapping;
System.out.println("key是:" + entry.getKey() + ",value是:" + entry.getValue());
}

7 Collections工具类

  • Collections是一个操作Set、List和Map等集合的工具类

  • Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

  • 排序操作 (均为static方法,针对List)
    在这里插入图片描述

  • 查找、替换
    在这里插入图片描述

  • 同步控制
    Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集
    合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全
    问题,使用的时候只要调用下对应的方法即可,比如

List list1 = Collections.synchronizedList(List);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值