【Java】《On Java》第12章 集合 读书笔记

结合JavaGuide和《On Java》的集合笔记。

不要使用时代的眼泪VectorHashTableStack

文章目录

  • 1 泛型与类型安全的集合
  • 2 基本概念
  • 3 添加一组元素★
  • 4 集合的打印
  • 5 List
      • toArray的使用 ★
      • Collection.sort的使用
  • 6 ArrayList
    • 6.1 ArrayList成员变量
    • 6.2 ArrayList构造函数
    • 6.3 ArrayList的插入与删除
    • 6.4 ArrayList的动态扩容:grow ★
  • 7 LinkedList
  • 8 Stack
  • 9 Set
      • 9.1 == 和 equals() 的区别
      • 9.2 HashSet如何检查重复?hashCode有什么用?★
      • 9.3 HashSet的插入
  • 10 Map(看了进阶版以后来填坑)
  • 11 Queue
      • ArrayDeque 与 LinkedList 的区别

1 泛型与类型安全的集合

在Java5之前,编译器允许向集合中插入不正确的类型。

比如,考虑一个Apple对象的集合,它由这条语句创建:

ArrayList apples = new ArrayList();

由于这个示例没有泛型,正常情况下编译器会给出警告:

Raw use of parameterized class 'ArrayList'

但如果我们向这个apples集合中插入一个Orange对象,代码并不会报错。

apples.add(new Orange())

而当我们取出对象时,会出现问题,提示我们Orange cannot be cast to Apple, 得到一个Orange是在我们意料之外的行为。

Apple apple = (Apple) apples.get(0);

所以java提供了泛型,当我们希望指明一个集合中包含的是Apple类型时,只需要定义类型参数AppleArrayList

在Java7之前,我们需要像第二行代码一样,在两侧重复写出类型声明。

程序员观察到,所有的信息都可以在左侧得到,因此没有理由任由编译器强迫我们在右侧再写一遍,因此变成了钻石语法<>的空括号。(解开了我多年的疑惑

ArrayList<Apple> apples = new ArrayList<>();
ArrayList<Apple> apples = new ArrayList<Apple>();

当指定了某个类型为泛型参数时,并不仅限于只能将确切类型的对象放入集合中。向上转型也可以像作用于其他类型一样作用于泛型,其子类对象也可加入集合。

2 基本概念

【Java】《On Java》第12章 集合 读书笔记_第1张图片

Java集合类库从设计上讲,可以分为两个不同的概念,表示为库的两个基本接口:

  • 集合Collection :一个独立元素的序列

  • 映射Map : 一组成对的“键值对”对象

尽管并非总是如此,但在理想情况下,我们编写的大部分代码在与这些接口打交道,只有在创建时才需要指明所使用的确切类型,因此可以这样创建一个List

List<Apple> apples = new ArrayList<>();

当希望修改实现时,只需要更改apples的实例化,而无需更改其他代码。

List<Apple> apples = new LinkedList<>();

当然,因为有些类具有额外的功能,我们有时可能需要使用这些功能,而希望使用更详细的数据类型。

3 添加一组元素★

方法 描述 说明
Arrays.asList 将数组 / 多个元素变为List对象
Collection() 构造方法,可接受使用另一个Colletion作为参数 浅拷贝(深拷贝需要使用clone)
Collection.addAll() Collection中加入另一个Collection的元素 浅拷贝
Collections.addAll() 接受数组、Collection、多个元素作为参数 浅拷贝
  1. Arrays.asList()方法可以接受一个数组,或者一个用逗号分隔的元素列表(使用可变参数),并将其转换为一个List对象。但是它的底层实现是一个数组,大小无法调整,不能进行add或者remove操作。
// 逗号分隔的元素列表
Arrays.asList(1, 2, 3, 4, 5)
// 接受一个数组
Integer[] moreInts = {6, 7, 8, 9, 10};
Arrays.asList(moreInts)
// 如果想改变数组大小,要像这样再构造一个新对象
List<Integer> arrayList = new ArrayList<>(Arrays.asList(moreInts));
  1. Collection类的构造器可以接受使用另一个Colletion作为参数:
 Collection<Integer> collection = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
  1. Collection类的addAll()函数只能接受使用另一个Colletion作为参数,不如Arrays.asList或者Collections.addAll()方法灵活,二者可以接受可变参数列表。
collection.addAll(Arrays.asList(moreInts));
  1. Collections.addAll()方法接受一个Collection对象、一个数组、或者一个用逗号分隔的列表,将其中所有的元素都加入这个Collection中。Collections.addAll()方法速度要快得多,也更加灵活,因此更推荐这种方式
Collections.addAll(collection, 11, 12, 13, 14, 15);
Collections.addAll(collection, moreInts);
Collections.addAll(collection, Arrays.asList(moreInts));

4 集合的打印

ArrayList LinkedList 都是 List 的类型,从输出中可以看出,它们都按插入顺序保存元素。两者之间的区别不仅在于执行某些类型的操作时的性能,而且 LinkedList 包含的操作多于 ArrayList

HashSet TreeSetLinkedHashSet是 Set 的类型。从输出中可以看到, Set 仅保存每个相同项中的一个,并且不同的 Set 实现存储元素的方式也不同。

  • 如果存储顺序很重要,则可以使用TreeSet,它将按比较结果的升序保存对象。
  • 或使用 LinkedHashSet ,它按照被添加的先后顺序保存对象。

键和值保存在 HashMap 中的顺序不是插入顺序,因为 HashMap 实现使用了非常快速的算法来控制顺序。

  • TreeMap 升序保存键
  • LinkedHashMap 在保持 HashMap 查找速度的同时按键的插入顺序保存键。

5 List

【Java】《On Java》第12章 集合 读书笔记_第2张图片

有两种类型的 List

  • 基本的 ArrayList ,底层实现为Object[]数组。擅长随机访问元素,但在List中间插入和删除元素时速度较慢。

  • LinkedList ,底层实现为双向链表。它通过代价较低的在 List 中间进行的插入和删除操作,提供了优化的顺序访问。LinkedList对于随机访问来说相对较慢,但它具有比 ArrayList 更多的功能。

方法 说明 备注
contains 判断某个对象是否在列表中
add 加入元素 / 按照指定下标添加元素
remove 按照下标删除某个元素 / 删除某个指定元素
indexOf 从列表中获取对象的对应下标,如果没查到,返回 -1
subList 截取列表, [ s t a r t , e n d ) [start,end) [start,end) subList生成的列表,底层是原始列表。
containsAll 是否包含某个集合
Collections.sort(list); 对集合进行排序,可以使用自定义的new Comparator重写compareTo方法
A.retainAll(B) 求交集,保留A在B中的所有元素。
removeAll 删除参数包含的所有对象
toArray 转换为数组

toArray的使用 ★

toArray的无参版本会返回一个Object数组;如果向重载的toArray版本传递一个目标类型的数组,它就会生成指定类型的数组。

如果参数数组太小,以至于无法将所有对象保存在这个List中,toArray方法会自动创建一个大小适当的新数组。

// toArray() 将列表转为数组
Object[] o = pets.toArray();

// toArray() 还有一个重载方法,传入一个形参,可以直接返回对应类型的数组,
// 这里的 new Integer[0] 就是起一个模板的作用,指定了返回数组的类型,
// 0是为了节省空间
Integer[] pa = pets.toArray(new Integer[pets.length()]);

ArrayList toArray(T[] a)方法为例:

	@SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // 新建一个运行时类型的数组,但是ArrayList数组的内容
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
            //调用System提供的arraycopy()方法实现数组之间的复制
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

Collection.sort的使用

Comparable 接口和 Comparator 接口都是 Java 中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用:

  • Comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序
  • Comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序

一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法。

// Comparator定制排序的用法
Collections.sort(arrayList, new Comparator<int[]>() {
    @Override
    public int compare(int[] o1, int[] o2) {
        return o1[0] - o2[0];
    }
});
// 使用Comparabale接口进行排序
public class Person implements Comparable<Person> {
 	...
 	@Override
    public int compareTo(Person o) {
        if (this.age > o.getAge()) {
            return 1;
        }
        if (this.age < o.getAge()) {
            return -1;
        }
        return 0;
    }
}

TreeMap<Person, String> pdata = new TreeMap<Person, String>();

6 ArrayList

ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。

【Java】《On Java》第12章 集合 读书笔记_第3张图片

List : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。

RandomAccess :这是一个标志接口,表明实现这个接口的 List 集合是支持 快速随机访问 的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。

Cloneable :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。

Serializable : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输。

6.1 ArrayList成员变量

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    // 默认初始容量大小
    private static final int DEFAULT_CAPACITY = 10;

    // 空数组(用于空实例)。
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 用于默认大小空实例的共享空数组实例。
    // 我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 保存ArrayList数据的数组
    transient Object[] elementData; // non-private to simplify nested class access

    // ArrayList 所包含的元素个数
    private int size;
}

6.2 ArrayList构造函数

1、带初始容量的构造函数,将创建initialCapacity大小的数组。

    // 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //如果传入的参数大于0,创建initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //如果传入的参数等于0,创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //其他情况,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        }
    }

2、默认无参构造函数,elementData为空数组,第一次加入元素时容量变为默认大小10。

    // 默认无参构造函数,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,
    // 也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

3、从一个Collection构造数组Object[],大小与传入的Collection参数相同。

	// 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。集合类型中元素为E及其子类。
    public ArrayList(Collection<? extends E> c) {
        //将指定集合转换为数组
        elementData = c.toArray();
        //如果elementData数组的长度不为0
        if ((size = elementData.length) != 0) {
            // 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组)
            if (elementData.getClass() != Object[].class)
                //将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 其他情况,用空数组代替
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

6.3 ArrayList的插入与删除

  • 插入:

    • 头部插入:由于需要将所有元素都依次向后移动一个位置,因此时间复杂度是 O ( n ) O(n) O(n)

    • 尾部插入:当 ArrayList 的容量未达到极限时,往列表末尾插入元素的时间复杂度是 O ( 1 ) O(1) O(1),因为它只需要在数组末尾添加一个元素即可;当容量已达到极限并且需要扩容时,则需要执行一次 O ( n ) O(n) O(n)的操作将原数组复制到新的更大的数组中,然后再执行 O ( 1 ) O(1) O(1) 的操作添加元素。

      	public boolean add(E e) {
              modCount++;
              add(e, elementData, size);
              return true;
          }	
      
      	private void add(E e, Object[] elementData, int s) {
              //  如果空间足够,不需要进行扩容;如果空间不够,就进行grow
              if (s == elementData.length)
                  elementData = grow();
              elementData[s] = e;
              size = s + 1;
          }
      
    • 指定位置插入:需要将目标位置之后的所有元素都向后移动一个位置arraycopy,然后再把新元素放入指定位置。这个过程需要移动平均 n / 2 n/2 n/2 个元素,因此时间复杂度为 O ( n ) O(n) O(n)

      	public void add(int index, E element) {
              rangeCheckForAdd(index);
              modCount++;
              final int s;
              Object[] elementData;
              if ((s = size) == (elementData = this.elementData).length)
                  elementData = grow();
              System.arraycopy(elementData, index, elementData, index + 1, s - index);
              elementData[index] = element;
              size = s + 1;
          }
      
  • 对于删除:

    • 头部删除:由于需要将所有元素依次向前移动一个位置,因此时间复杂度是 O ( n ) O(n) O(n)
    • 尾部删除:当删除的元素位于列表末尾时,时间复杂度为 O ( 1 ) O(1) O(1)
    • 指定位置删除:需要将目标元素之后的所有元素向前移动一个位置以填补被删除的空白位置,因此需要移动平均 n / 2 n/2 n/2 个元素,时间复杂度为 O ( n ) O(n) O(n)

6.4 ArrayList的动态扩容:grow ★

核心函数为grow

如果旧容量为0,创建一个新数组即可。如果旧容量不等于0,则需要判断 m i n C a p a c i t y minCapacity minCapacity 1.5 × o l d C a p a c i t y 1.5 \times oldCapacity 1.5×oldCapacity的大小,将数组容量扩充为更大的那个值,并用ArrayCopy方法复制旧数组。

	private Object[] grow() { return grow(size + 1); }
    
    private Object[] grow(int minCapacity) {
        // 旧容量大小
        int oldCapacity = elementData.length;
        // 如果旧容量不为0
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 扩容后所需的长度(minCapacity)是否大于当前数组长度的1.5倍
            // 若是则将新的数组容量(newCapacity)设为minCapacity
            // 若不是则将新的数组容量设为当前数组长度的1.5倍(oldCapacity >> 1,位右移运算,表示当前数组长度的0.5倍)
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } 
        // 旧容量为0,建立一个新数组即可
        else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }

此外,在大量加入元素之前,可以通过ensureCapacity手动进行扩容,以减少不断调用grow方法的复制次数。

    public void ensureCapacity(int minCapacity) {
        if (minCapacity > elementData.length
            && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
                 && minCapacity <= DEFAULT_CAPACITY)) {
            modCount++;
            grow(minCapacity);
        }
    }

7 LinkedList

与ArrayList相比,LinkedList执行插入和删除的效率更高 ,不过随机访问操作的表现要差一些。

  • 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。

  • 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。

  • 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。

更重要的是,LinkedList中加入了一些可以使其用作栈、队列、双端队列的方法。

功能 为空抛出异常 为空返回null
返回头部 getFirst / element peek / peekFirst
移除并返回头部 removeFirst / remove / pop poll / pollFirst
头部插入元素 addFirst / push
返回尾部 peekLast
尾部插入元素 addLast / add / offer
移除并返回尾部 removeLast pollLast

【Java】《On Java》第12章 集合 读书笔记_第4张图片

8 Stack

Java1.0就提供了Stack类,但是这个类的设计十分糟糕。它是一个类而不是接口,继承了设计糟糕的Vector类,Vector类可以看成方法名又臭又长但线程安全的ArrayListStack继承了Vector所有的方法与行为。而Java由于向后兼容,没有办法摆脱这种糟糕的设计错误。

Java6加入ArrayDeque,提供了直接实现栈功能的方法pushpoppeek

请不要使用Stack类!

9 Set

【Java】《On Java》第12章 集合 读书笔记_第5张图片

Set 不保存重复的元素。查找通常是 Set 最重要的操作,因此通常会选择HashSet实现,该实现针对快速查找进行了优化。Set 具有与 Collection 相同的接口,因此没有任何额外的功能。实际上, Set 就是一个 Collection ,只是行为不同。

  • HashSet(无序): 使用哈希函数,基于 HashMap 实现,底层采用 HashMap 来保存元素。
  • LinkedHashSet: LinkedHashSetHashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。使用链表和哈希表,按照插入顺序维护元素。
  • TreeSet: 将元素排序存储在红黑树数据结构中。

9.1 == 和 equals() 的区别

== 对于基本类型和引用类型的作用效果是不同的:

  • 对于基本数据类型来说,== 比较的是值。
  • 对于引用数据类型来说,== 比较的是对象的内存地址。

equals()不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals() 方法存在两种使用情况:

  • 类没有重写 equals()方法:通过equals()比较该类的两个对象时,使用的默认是 Objectequals()方法,等价于直接比较this == obj
  • 类重写了 equals()方法:一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

String 中的 equals 方法是被重写过的,因为 Objectequals 方法是比较的对象的内存地址,而 Stringequals 方法比较的是对象的值。当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

9.2 HashSet如何检查重复?hashCode有什么用?★

⭐当你把对象加入 HashSet 时:

  • HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCodeHashSet 会假设对象没有重复出现。
  • 如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同:
    • 如果两者相同,HashSet 就不会让其加入操作成功。
    • 如果不同的话,就会重新散列到其他位置。

hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。

  • 两个对象的hashCode 值相等并不代表两个对象就相等,但是有时可以大大减少equals 的次数,相应提高执行速度。

  • 重写equals()时必须重写 hashCode() 方法,因为两个相等对象的hashCode值必须相等。否则可能导致在进行HashSet插入的类似操作时出现错误。

总结就是:

  1. 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。

  2. 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。

  3. 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。

9.3 HashSet的插入

底层为HashTable实现,只是简单的调用了HashMapput()方法,并且判断了一下返回值以确保是否有重复元素。

// Returns: true if this set did not already contain the specified element
// 返回值:当 set 中没有包含 add 的元素时返回真
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}

10 Map(看了进阶版以后来填坑)

方法 说明 示例
get() 根据键查找值
getOrDefault() 根据键查找值,查找不到时使用默认值代替
put() 放入键值对
containsKey() 是否含某个键
containsValue() 是否含某个值
keySet() 键的集合
values() 值的集合

11 Queue

【Java】《On Java》第12章 集合 读书笔记_第6张图片

Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。

Queue 扩展了 Collection 的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。

Queue 接口 抛出异常 返回特殊值
插入队尾 add(E e) offer(E e)
删除队首 remove() poll()
查询队首元素 element() peek()

Deque 是双端队列,在队列的两端均可以插入或删除元素。

Deque 扩展了 Queue 的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:

Deque 接口 抛出异常 返回特殊值
插入队首 addFirst(E e) offerFirst(E e)
插入队尾 addLast(E e) offerLast(E e)
删除队首 removeFirst() pollFirst()
删除队尾 removeLast() pollLast()
查询队首元素 getFirst() peekFirst()
查询队尾元素 getLast() peekLast()

事实上,Deque 还提供有 push()pop() 等其他方法,可用于模拟栈。

【Java】《On Java》第12章 集合 读书笔记_第7张图片

ArrayDeque 与 LinkedList 的区别

ArrayDequeLinkedList 都实现了 Deque 接口,两者都具有队列的功能,但两者有什么区别呢?

  • ArrayDeque 是基于可变长的数组和双指针来实现,而 LinkedList 则通过链表来实现。
  • ArrayDeque 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 LinkedList 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。

从性能的角度上,选用 ArrayDeque 来实现队列要比 LinkedList 更好。

你可能感兴趣的:(Java,java,开发语言)