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 文档就好了,我这里,就稍微品尝一下集合的源码

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

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

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 hashingboolean equals(Object o);int hashCode();@Overridedefault 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,添加的方法

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 codeif (minCapacity - elementData.length > 0)grow(minCapacity);//扩容}

扩容:

private void grow(int minCapacity) {// overflow-conscious codeint 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 可以在遍历的过程中,删除元素

其对应的方法如下:

这里,我来演示一下,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());

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 的顺序排好了:

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;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}
}

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

这是因为,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;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}//实现 compareTo 方法@Overridepublic 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 起作用了:

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

哈希表概念

碰撞处理

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

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

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

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

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

散列因子

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

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

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

散列因子也可以指定

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

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

HashMap

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

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

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

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

HashMap 的存储流程

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

这玩意儿的源码,我记得自己在学 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...@Overridepublic 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);}@Overridepublic 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));

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

class Book {private String name;private String desc;// constructor...// getter and setter...@Overridepublic 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 是永远不可能相等的。

自定义 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 对不上

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

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

Java 集合 源码分析+重点解析 超详细学习资料相关推荐

  1. Java集合源码分析(二)ArrayList

    ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...

  2. java集合源码分析之HashMap

    UML类图: 基本简介: 底层的数据结构是数组,数组的元素类型是链表或者红黑树. 元素的添加可能会触发数组的扩容,会使元素重新哈希放入桶中,效率比较低. 元素在不扩容的情况下添加效率高,查找.删除.修 ...

  3. java集合源码分析

    List,Set,Map都是接口,前两个继承Collection接口,Map为独立接口 Set的实现由HashSet,LinkedHashSet,TreeSet List下有ArrayList,Vec ...

  4. java web开源项目源码_超赞!推荐一个专注于Java后端源码分析的Github项目!

    大家好,最近有小伙伴们建议我把源码分析文章及源码分析项目(带注释版)放到github上,这样小伙伴们就可以把带中文注释的源码项目下载到自己本地电脑,结合源码分析文章自己本地调试,总之对于学习开源项目源 ...

  5. Java学习集合源码分析

    集合源码分析 1.集合存在的原因 可以用数组来表示集合,那为什么还需要集合? 1)数组的缺陷 ​ 在创建数组时,必须指定长度,一旦指定便不能改变 数组保存必须是同一个类型的数据 数组的增加和删除不方便 ...

  6. MyBatis 源码分析 - 配置文件解析过程

    文章目录 * 本文速览 1.简介 2.配置文件解析过程分析 2.1 配置文件解析入口 2.2 解析 properties 配置 2.3 解析 settings 配置 2.3.1 settings 节点 ...

  7. Python实现飞机大战-第二部分(附源码、素材、超详细教程)

    飞机大战第二部分 1.前言 2.飞机射击 2.1.添加子弹的相关设置 2.2.创建文件bullet.py 2.3.修改game_functions.py 2.4.修改mian.py 2.5.射击效果 ...

  8. java hashset 源码_Java集合源码分析-HashSet和LinkedHashSet

    前两篇文章分别分析了Java的ArrayList和LinkedList实现原理,这篇文章分析下HashSet和LinkedHashSet的源码.重点讲解HashSet,因为LinkedHashSet是 ...

  9. java地图源码_Java集合源码分析(四)HashMap

    一.HashMap简介 1.1.HashMap概述 HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对映射.此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作 ...

  10. Java集合源码浅析(一) : ArrayList

    (尊重劳动成果,转载请注明出处:https://yangwenqiang.blog.csdn.net/article/details/105418475冷血之心的博客) 背景 一直都有这么一个打算,那 ...

最新文章

  1. 一起谈.NET技术,.Net Discovery系列之-深入理解平台机制与性能影响 (中)
  2. 达摩院2022十大科技趋势发布:人工智能将催生科研新范
  3. 报错,贴图整理(1)
  4. DFT实际应用-User-Defined Test Points Example
  5. C#网络编程(基本概念和操作) - Part.1
  6. 合肥工业大学机器人技术期末_机器人技术第三次作业(HFUT)
  7. html 改变文本框字体颜色,CSS更改文本框的字体颜色
  8. 软件工程网络15个人阅读作业1 (201521123107)
  9. 资源放送丨《MySQL在某航空业公司的架构选型演进之路》PPT视频
  10. 5G 是否有过度承诺之嫌?
  11. CentOS7安装KVM、KVM安装CentOS7
  12. NET 技术FAQ(六)-----属性
  13. linux磁盘健康监控,MegaCli监控RAID磁盘健康信息
  14. FreeRTOS使用教程(配合CubeMX)
  15. gif转为帧(gif分解)加分解图片批量打包下载
  16. CRC_8循环冗余校验码verilog实现
  17. 微信小程序添加服务器域名
  18. 检验新买内存条的真假
  19. 用Python分析《令人心动的offer2》的13万条弹幕,网友们都在吐槽什么?
  20. CAD制图怎么绘制对称的图形?

热门文章

  1. 保密安全风险自评估单机版检查工具V1.5
  2. java实习生面试题
  3. 51单片机编程软件keil4的安装过程
  4. springboot+微信小程序点餐系统的设计与实现毕业设计源码221541
  5. Vue项目实例(一)------背景
  6. 一款用来下载pdf word zip img各种文件的js插件
  7. C4D建模宝典R20笔记
  8. 研究支付业务中,银行、支付机构与银联、网联的关系逻辑
  9. 斐讯K2刷华硕固件教程
  10. html css 银行卡,CSS