Java集合

  • 集合
  • ————单值存储————
  • Collection
  • List
  • 相对于 Collection,添加的方法
  • ArrayList
  • 底层实现是数组:
  • 为什么默认是长度为 10?
  • 为什么扩容 1.5 倍?
  • Vector
  • LinkedList
  • Iterator 和 ListIterator
  • Iterator
  • Iteraotr 实现删除
  • ListIterator
  • Set
  • Set集合该怎么获取元素?
  • HashSet
  • TreeSet
  • TreeSet 放置自定义实例
  • ————键值对存储————
  • 哈希表概念
  • 碰撞处理
  • 散列因子
  • HashMap
  • HashMap 的存储流程
  • HashMap HashTable ConcurrentHashMap 的区别
  • TreeMap
  • LinkedHashMap
  • Map的==注意点==
  • 使用自定义 Key 时,要重写 equals 方法和 hashCode 方法
  • 自定义 key 存入后,==不能修改==其属性


集合

我这次不讲解用法,要知道怎么用,去翻阅 API 文档就好了,我这里,就稍微品尝一下集合的源码

这里,我先放上集合的所有继承关系图:

java源码分析 java源代码分析_ci

————单值存储————

Collection

下面就是 Collection 的源码,所有集合,都要实现 Collection接口

在早期,都是用 Collection 去指向集合实现的,但是因为无法区分 List 和 Set (主要是有取消重复元素的需要),所以,Sun 在开源项目 PetShop 中,开始推荐使用 List 和 Set 来指向集合的实现

public interface Collection<E> extends Iterable<E> {
   //返回大小
    int size();
  //是否为空
    boolean isEmpty();
	//是否包含
    boolean contains(Object o);
	//迭代器
    Iterator<E> iterator();
	//将所包含的元素,
    Object[] toArray();

    <T> T[] toArray(T[] a);

    // Modification Operations

	  //增加元素
    boolean add(E e);

    //删除指定元素
    boolean remove(Object o);


    // Bulk Operations

   //判断当前集合是否包含传入集合的所有元素
    boolean containsAll(Collection<?> c);

   //将传入集合的所有元素,放入当前集合
    boolean addAll(Collection<? extends E> c);

   
    boolean removeAll(Collection<?> c);

    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }

   //和传入的集合,取交集
    boolean retainAll(Collection<?> c);

   
    void clear();


    // Comparison and hashing

   
    boolean equals(Object o);

    int hashCode();

    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
    }

  //java8新增的 stream 流操作 
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

源码的注释中,还给出了 Collection 的所有子类:

* @see     Set
 * @see     List
 * @see     Map
 * @see     SortedSet
 * @see     SortedMap
 * @see     HashSet
 * @see     TreeSet
 * @see     ArrayList
 * @see     LinkedList
 * @see     Vector
 * @see     Collections
 * @see     Arrays
 * @see     AbstractCollection

List

继承 Collection 接口,也添加了自己特殊的接口方法

public interface List<E> extends Collection<E> {
  //...
}

相对于 Collection,添加的方法

java源码分析 java源代码分析_集合_02

ArrayList

这里,我们就小尝一下 ArrayList 的源码

ArrayList 在未被指定大小的时候,会默认申请一个长度为 10 的空间

如果在 add 的时候,长度不够,会扩容 1.5 倍

ArrayList,是线程不安全的,所以效率比 Vector 高(Vector 线程安全)

底层实现是数组:

我相信,很多人在一开始写代码的时候,都尝试过写自己的工具类吧(我在知道集合之前时,也因为数组的种种问题,尝试过去写自己的工具类)

/**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

为什么默认是长度为 10?

下面是其无参构造:

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

DEFAULTCAPACITY_EMPTY_ELEMENTDATA默认是一个空数组:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

那为什么还说默认是 10 的空间呢?

那是在 add 中实现的:

add 源码如下:

这里要引伸一个话题,add 的返回值只会是 true

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true; //只可能返回 true
    }

add 前,会先去确保大小:

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

这里,就会去计算,应该分给数组多少空间:

如果长度为默认长度,就分一个 DEFAULT_CAPACITY

private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity); //如果长度为默认长度,就分一个 DEFAULT_CAPACITY
        }
        return minCapacity;
    }

DEFAULT_CAPACITY,就是 10:

/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

为什么扩容 1.5 倍?

这里,我们从 add 的确保空间的函数开始:

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

这里,如果传入的最小空间,大于原来的数组长度,则进行扩容:

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);//扩容
    }

扩容:

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
 
			  //右移一位,就是除以 2,也就是说,最后扩容得到的大小,是原来的 1.5 倍
        int newCapacity = oldCapacity + (oldCapacity >> 1); 
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        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);
    }

Vector

Vector 线程安全,所以效率低于 ArrayList

因为底层实现和 ArrayList 很像,所以,就不再重复讲解了

LinkedList

使用双向链表,增删很快,但是查找较慢

因为其实现的是双向链表,所以,可以把它当栈和队列去使用

Iterator 和 ListIterator

Iterator

Itetrator,在设计模式的迭代器模式中,我已经充分的讲解过了,这里就不再过多赘述

简单来说,Iterator 可以屏蔽遍历时的细节

而且,相比于 ForEach 来说,iterator 可以在遍历的过程中,删除元素

其对应的方法如下:

java源码分析 java源代码分析_集合_03

这里,我来演示一下,Iterator 该怎么实现删除:

Iteraotr 实现删除

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
  Integer ele = iterator.next(); //这里,一定要在 iterator 获取以后,才能删除
  if (ele.equals(3)) {
    iterator.remove();
  }
}

System.out.println(list.toString());

java源码分析 java源代码分析_集合_04

ListIterator

ListIterator 只能在 List 集合中使用

相较于 Iterator,ListIterator 还实现了向集合中插入元素的操作

因为是在是不怎么常用,我就不演示了

这里,还是建议各位去看看迭代器模式,这样,就能对 Iterator 有一个更深刻的认识

Set

Set 是一个集合,既然是集合,那么,其中存储的元素,就是无序的,且不能出现重复的元素

Set 在存储可变的元素的时候,一定要十分小心,因为,元素在 Set 中的位置,是根据元素中的属性计算好的,如果元素属性发生变化,但是其位置有没法根据属性变化后的值动态修改,那么,该元素就出现在了它不该出现的地方

Set集合该怎么获取元素?

我们知道,Set 是没有get()方法的(这也是必然,元素是无序的,没办法通过下标获取)

所以如果我们要获取 Set 中的元素,只能:

  • 使用 toArray()方法,将所有元素放到数组中,然后遍历
  • 使用迭代器
  • 使用 forEach 遍历

HashSet

HashSet,使用的是散列存储

其中的元素顺序,依赖的是 Hash 算法

HashSet,其实就是把 HashMap 给封装了一下:

public HashSet() {
        map = new HashMap<>();
    }

只使用 HashSet 的 key,把 value 封装了:

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

TreeSet

TreeSet 中的元素,是使用二叉树存储的,是按照自然顺序,有序排列的,在说的直白一点,TreeSet 可以帮助我们,给传入的数据进行排序

public class TreeSetDemo {
    public static void main(String[] args) {
        Set<String> set = new TreeSet<>();
        set.add("B");
        set.add("C");
        set.add("A");
        set.add("D");

        Iterator<String> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

    }
}

TreeSet 把 ABCD 的顺序排好了:

java源码分析 java源代码分析_集合_05

TreeSet 放置自定义实例

我们在 TreeSet 中,放置自定义的 Person 对象:

public class TreeSetDemo {
    public static void main(String[] args) {
        Set<Person> set = new TreeSet<>();
        Person p1 = new Person("张三", 20);
        Person p2 = new Person("李四", 19);

        set.add(p1);
        set.add(p2);

    }
    
}

class Person {
    private String name;
    private Integer age;

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

发现报错,显示无法转换为 Comparable 接口:

java源码分析 java源代码分析_ci_06

这是因为,TreeSet 在放置对象的时候,是依照既定规则,去放置的(这也是为什么我们放入的元素,最后是有序的),所以,我们要让自定义类实现 Comparable 接口

class Person implements Comparable<Person> {
    private String name;
    private Integer age;

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

  //实现 compareTo 方法
    @Override
    public int compareTo(Person o) {
        return this.age-o.getAge();
    }
}

这里我们以 age 为比照对象,当 compareTo 返回的是:

  • 大于 0:放后
  • 等于 0:不放
  • 小于 0:放前

这里我们再传入一个 age 为 19,但 name 不一样的对象:

Person p1 = new Person("张三", 20);
Person p2 = new Person("李四", 19);
Person p3 = new Person("麻子", 19);

set.add(p1);
set.add(p2);
set.add(p3);

可以发现,麻子没有被传进去,说明compareTo 起作用了:

java源码分析 java源代码分析_java源码分析_07

————键值对存储————

哈希表概念

java源码分析 java源代码分析_java_08

碰撞处理

java8中,当碰撞,则将数据以链表的形式,存放在碰撞的元素后面

当这个链表长度大于等于 8 的时候,由链表转换成红黑树

当红黑树元素个数少到 6 的时候,又转换为链表

桶,就是对象数组中,一个个存链表或者红黑树的位置,在上图中标出来了

HashMap 的初始容量是 16,即HashMap 初始有 16 个桶

散列因子

如果将一个对象数组都存满了,而且元素很多,那么,Hash 表的效率,就会很低,为了避免这种情况发生,我们就要在适当的时机扩容

但是什么时候该扩容呢?这就需要借助散列因子

HashMap 中,散列因子是 0.75,表示一旦对象数组中,有 75%的桶中存放了内容,就对对象数组进行扩容,这个 0.75,是官方根据大量测试,最后获得的结论

散列因子也可以指定

散列因子越大,空间浪费越少,但是查找效率会降低

散列因子越小,空间浪费越多,但是查找效率会提升

HashMap

碰撞,使用链表+红黑树解决

初始大小是 16(有 16 个桶)

**负载因子(散列因子)**是 0.75

初始容量散列因子,能影响 HashMap的效率

HashMap 的存储流程

源码分析搞死人了,这里先放张图

java源码分析 java源代码分析_hashmap_09

这玩意儿的源码,我记得自己在学 c++的时候,写过,那时候可是花费了大力气,红黑树的转换真的是搞死人了

源码分析,等我有心思了,再去搞吧

HashMap HashTable ConcurrentHashMap 的区别

**HashMap:**线程不安全,效率高

**HashTable:**线程安全,效率低

ConcurrentHashMap:采用分段锁机制,保证线程安全,但是效率也比较高

TreeMap

了解 TreeSet,就知道了 TreeMap

TreeMap 中的 key,是按照自然顺序排序的

LinkedHashMap

记录我们的存储顺序

实现是即使用 HasnMap

又通过一个双向链表,实现存储顺序的保存

Map的注意点

首先我们要明确,Map 的 key 值的比较,是依据 HashCode 和 equals 来判断的(equals 要重写,使得其判断标准是属性值)
只有 HashCode 和 equals判断都是相等的,这个 key 才是相等的

使用自定义 Key 时,要重写 equals 方法和 hashCode 方法

我们知道,Object 的equals()方法,默认是获取对象的内存位置的,我们的自定义 key 对象,需要重写 equals(),使其比对的方式,是类中的部分属性

我们自定义一个 Book 类:

这里重写了 equals()方法,使其比较的是属性内容

重写了hashCode()方法,使得计算 hash 的数据,是来自属性值,而不是内存位置,这样,就可以拿属性内容一样,但是是另外 new 出来的对象,作为 key 值了

class Book {
    private String name;
    private String desc;

    // constructor...

    // getter and setter...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Objects.equals(name, book.name) &&
                Objects.equals(desc, book.desc);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, desc);
    }
}

我们来写一个测试,将 Book 作为 key 值:

Map<Book, String> map1 = new HashMap<>();
Map<Book, String> map2 = new HashMap<>();

Book book1 = new Book("金苹果", "讲述了种植苹果的心酸历程");
Book book2 = new Book("银苹果", "讲述了种植苹果的心酸历程");

map1.put(book1,"我的第一本书");
map1.put(book2,"我的第二本书");

//如果不重写 hashCode 方法,那是不能拿新 new 出来的对象,作为 key 值的
Book book3 = new Book("金苹果", "讲述了种植苹果的心酸历程");
System.out.println(map1.get(book3));

java源码分析 java源代码分析_java_10

如果我们删除 Book 类中,重写的hashCode()方法,看看发生了什么:

class Book {
    private String name;
    private String desc;

    // constructor...

    // getter and setter...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Objects.equals(name, book.name) &&
                Objects.equals(desc, book.desc);
    }

		// @Override
    // public int hashCode() {
    //     return Objects.hash(name, desc);
    // }
}

这时候的测试结果,就是找不到:

因为默认的 hashCode() 计算方法,就是按照内存位置计算的,所以新 new 出来的对象,就算属性值一模一样,但是 hashCode 是永远不可能相等的。

java源码分析 java源代码分析_java源码分析_11

自定义 key 存入后,不能修改其属性

如果修改其属性值,那么,其 hashCode 就会发生变化,那就很难被再次找到

这里,我们承接之前的测试,但是,在存入金苹果后,把金苹果的 name 修改:

Map<Book, String> map1 = new HashMap<>();
Map<Book, String> map2 = new HashMap<>();

Book book1 = new Book("金苹果", "讲述了种植苹果的心酸历程");
Book book2 = new Book("银苹果", "讲述了种植苹果的心酸历程");

map1.put(book1,"我的第一本书");
map1.put(book2,"我的第二本书");

/**
         * 修改 key 值
         *
         * 这和时候,属性值变成了:  name:铜苹果  desc:讲述了种植苹果的心酸历程
         * 但是,hash 值还是按照   name:金苹果  desc:讲述了种植苹果的心酸历程   算出来的
         */
book1.setName("铜苹果");

Book book3 = new Book("金苹果", "讲述了种植苹果的心酸历程");
Book book4 = new Book("铜苹果", "讲述了种植苹果的心酸历程");
System.out.println(map1.get(book3));  //equals 对不上
System.out.println(map1.get(book4));  //hashCode 对不上

测试结果如下,都取不到:

java源码分析 java源代码分析_ci_12

这是因为,修改 key 值,这个时候,属性值变成了: name:铜苹果 desc:讲述了种植苹果的心酸历程
但是,hash 值还是按照 name:金苹果 desc:讲述了种植苹果的心酸历程 算出来的。所以,无论如何,都对应不上。