- 本篇不适合初学者,及使用方法,适合
面试者
观看借鉴; - 本人在尚硅谷看的,整理的笔记。
- 不正确的地方,多多指教。
Java 集合源码详解
集合和数组:
- 数组声明了它容纳的元素的类型,而集合不声明
存储Object类型
可以通过泛型进行规范! - 数组是静态的,一个数组实例具有固定的大小,一旦创建了就无法改变容量了。
集合是可以动态扩展容量,可以根据需要动态改变大小,集合提供更多的成员方法,能满足更多的需求。 - 集合: 和数组一样Java中用来存储数据的作用,
弥补了数组长度固定的缺点更灵活
List接口
概述:
-
鉴于Java中数组用来存储数据长度固定,我们通常使用
List替代数组
动态数组 -
List集合类中元素
有序、且可重复
集合中的每个元素都有其对应的顺序索引。
List除了从Collection集合继承的方法外,List 集合里添加了一些 根据索引来操作集合元素的方法。
void add(int index, Object ele):在index位置插入ele元素
boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
Object get(int index):获取指定index位置的元素
… -
List接口的实现类常用的有:
ArrayList
、LinkedList
和Vector
ArrayList 源码分析
- ArrayList 是 List 接口的典型实现类、主要实现类
用的最多
- 本质上,ArrayList是对象引用的一个”变长”数组
因为是数组,所以非常适合与进行遍历!
ArrayList的JDK1.8之前与之后的实现区别?
- JDK1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组
- JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为10的数组
建议可以自己深入底层查看效果更佳!
List list = new ArrayList();
按住Ctrl+鼠标右键 进入源码, 注意更改JDK版本!
JDK1.7
- 对于 ArrayList 而言,它实现 List 接口、底层使用数组保存所有元素。
- Ctrl+F 快速查找方法…
ArrayList.Java
底层使用数组实现
private transient Object[] elementData;
构造器
ArrayList 提供了三种方式的构造器
- 可以构造一个默认初始容量为 10 的空列表
- 构造一个指定初始容量的空列表
- 构造一个指定初始容量的空列表
- 对应着三个不同的构造方法…
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
public ArrayList() {
this(10);
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
add
// 将指定的元素添加到此列表的尾部。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
add(int index, E element)
// 将指定的元素插入此列表中的指定位置
// 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加 1)。
public void add(int index, E element) {
rangeCheckForAdd(index);
// 如果数组长度不足,将进行扩容。
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将 elementData 中从 Index 位置开始、长度为 size-index 的元素
// 拷贝到从下标为 index+1 位置开始的新的 elementData 数组中。
// 即将当前位于该位置的元素以及所有后续元素右移一个位置。
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
扩容机制:
- 从上面介绍的向 ArrayList 中存储元素的代码中,我们看到,每当向数组中添加元素时
都要去检查添加后元素的个数是否会超出当前数组的长度
- 如果超出,数组将会进行扩容,以满足添加数据的需求。
- 数组扩容通过一个公开的方法
ensureCapacity(int minCapacity)
来实现。
public void ensureCapacity(int minCapacity) {
//判断扩展值大于0
if (minCapacity > 0)
ensureCapacityInternal(minCapacity);
}
//开始扩容
private void ensureCapacityInternal(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)
int newCapacity = oldCapacity + (oldCapacity >> 1); //新长度 = 旧 + 旧右移1位!(二级制右移)
//<< 左移 3<<2 : 3左移两位 结果就是 3*2*2=12; 左移几位就是 *几次2; 注意数值移动太多数值会出问题;(二级制~ 最后会出现负数) 左移在一定范围内 相当于*2
//>> 右移 3>>1 : 3右移一位 结果就是 3/2=1; 右移几位就是 /几次2; 注意数值移动太多数值会出问题; 右移在一定范围内 相对于/2
// x = 2+(2>>1)=3 及一点五倍!
//如果还是小于扩容的长度,直接等于改长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新的数组容量newCapacity大于数组能容纳的最大元素个数 MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//把旧数组放进新的扩容后的数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
//那么再判断传入的参数minCapacity是否大于MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) {
//传入的参数必须大于0,否者报错
if (minCapacity < 0)
throw new OutOfMemoryError();
//如果minCapacity大于MAX_ARRAY_SIZE
//那么//newCapacity等于Integer.MAX_VALUE,否者newCapacity等于MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
总结:
- 当重新计算的容量(x1.5那个计算)小于传入要求容量参数,则新容量以传入的比较大的容量参数为准。
- 当传入容量参数太大,大到超过了数组的容量限定值2^{31}-1-8却又小于整数限定值 2^{31}-1
- 那么新的数组容量以整数限定值 2^{31}-1为准
- 但是当传入的容量参数不大于数组的容量限定值时,以容量限定值2^{31}-1-8为准。
JDK1.8
- 1.8 和 1.7 没有太大变化只是由原来的,饿汉更改为了懒汉
//存放元素的数组,从这可以发现 ArrayList 的底层实现就是一个 Object数组
transient Object[] elementData;
//数组中包含的元素个数
private int size;
//数组的最大上限
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacit
y: "+initialCapacity);
- elementData 是一个大小为 0 的空数组
- 当我们指定了初始大小的时候,elementData 的初始大小就变成了我们所指定的初始大小了。
add 添加方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCou
nt!!
elementData[size++] = e;
return true;
}
-
ArrayList 的 add 方法也很好理解,在插入元素之前,它会先检查是否需要扩容
-
然后再把元素添加到数组中最后一个元素的后面。
-
在 ensureCapacityInternal 方法中,
我们可以看见,如果当 elementData 为空数组时,它会使用默认的大小去扩容。 -
通过无参构造方法来创建 ArrayList 时,它的大小其实是为 0 的,
只有在使用到的时候,才会通过 grow 方法去创建一个大小为 10 的数组。
LinkedList 源码分析
- LinkedList 是通过一个双向链表来实现的,
- 它允许插入所有元素,包括 null,同时,它是线程不同步的。
- 双向链表每个结点除了数据域之外,还有一个
前指针next
和后指针prev
- 分别指向前驱结点和后继结点(如果有前驱/后继的话)。
- 双向链表还有一个 first 指针,指向头节点,和 last 指针,指向尾节点。
即当前,LinkedList 最后一个节点, 和第一个节点!
LinkedList 中的属性:
//链表的节点个数
transient int size = 0;
//指向头节点的指针
//整个List集合中的第一个元素!
transient Node<E> first;
//指向尾节点的指针
//整个List集合中最后一个元素!
transient Node<E> last;
- 当然如果集合中就一个元素,
它即是first 也是 last
结点结构
- Node 是在 LinkedList 里定义的一个静态内部类
该类只在LinkedList中使用到!
- 它表示链表每个节点的结构,包括一个数据域 item,一个后置指针 next,一个前置指针 prev。
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;
}
}
- item 实际的元素
- 因为LinkedList是一个双向链表,next prev 分布表示 item 的下一个元素 和 上一个元素!
add
//对外保留的添加方法!
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
//获取当前最后一个节点
final Node<E> l = last;
//创建e要添加的节点,因为新增是增在最后的,也不需要指定 next下一个元素位置...
final Node<E> newNode = new Node<>(l, e, null);
//并把这个新增的设置为 最后一个节点!
last = newNode;
//判断最后一个是否为null
//如果为null 就表示这个集合还没有一个元素!没有最后一个元素!
//那我就是第一个元素了,并把值赋值给first节点!
//else
//已经存在元素,把最后一个元素的下一个节点指向e. 因为现在e是才是最后一个!
if (l == null)
first = newNode;
else
l.next = newNode;
//长度++
size++;
modCount++;
}
linkFirst 第一个位置添加元素
private void linkFirst(E e) {
//获取当前集合中第一个元素
final Node<E> f = first;
//创建e 为第一个元素,因为是第一所有没有prev
final Node<E> newNode = new Node<>(null, e, f);
//e成为第一个元素
first = newNode;
//判断第一个元素是否为null,表示当前集合啥也没有! e即是第一也是最后!
//else f不在是第一,并指向e
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
linkLast 最后一个位置添加元素
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
- 将e 设置成为 last 并将上一个 last指向e…
ok.现在回头看发现,双向链表果然时候新增!
- 在任何位置新增,只需要将 前后元素进行链接即可!
Vector 源码分析
Vector 是一个古老的集合,JDK1.0就有了。 已经淘汰很少有人使用了!官方已经停止更新了!
- 大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
- 7/8没变化, 底层默认数组长度10,每次扩容2倍!
ArrayList
LinkedList
Vector
异同
同
- 三者都实现了List 接口
存储数据特点相同:
有序 可重复数据!
异
ArrayList和LinkedList的异同
-
二者都线程不安全,相对线程安全的Vector,执行效率高。
-
ArrayList是实现了基于
动态数组
的数据结构
LinkedList基于双向链表
的数据结构
对于随机访问get和set,ArrayList觉得优于LinkedList
因为LinkedList要移动指针
对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势
因为ArrayList要移动数据。
ArrayList和Vector的区别
- Vector和ArrayList几乎是完全相同的 底层都是
动态数据
结构 - 唯一的区别在于Vector是同步类(synchronized) 因此开销就比ArrayList要大,访问要慢。
大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。 - Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍
- Vector还有一个子类Stack
Set接口
Set 概述:
Set接口是Collection的子接口 set接口没有提供额外的方法
存储无序, 唯一的数据
-
无序:
无序,不表示随机!只是, 存入数据在 底层数据上的位置
有自己的一套规则
-
唯一:
通过, hashCode和 equals 方法实现,存储数据唯一! -
实现类:
HashSet: set接口的主要实现类,线程不安全,可以存储null值;
LinkedHashSet: 作为HashSet在子类
,遍历内部数据时,可以按照添加时的顺序进行展示!
但不代表,它是有序!
TreeSet
set 接口的实现类,无序,唯一! 存储的数据都是统一类型的!
可以对新增的元素 , 内部指定一个排序规则:自然排序
定制排序
(Java类比较强)
HashSet
实例:
User.Java
自定义对象类型
public class User {
private int id=0;
private String name;
//get/set/无参有参构造...
}
CSDemo
非常正常的一个 HashSet使用
public class CSDemo {
public static void main(String[] args) {
//创建新增元素!
HashSet hset = new HashSet();
hset.add(123);
hset.add(123);
hset.add(456);
hset.add("ABC");
hset.add("DEF");
hset.add(new User(1, "张三"));
hset.add(new User(1, "张三"));
//遍历输出
Iterator it = hset.iterator();
while (it.hasNext()){
System.out.println(it.next());
}
}
}
- 可以看到, hashset 确实是
无序,唯一
的 - 两次 123 都,直接输出不了了
并且输入顺序 和输出的顺序并不一样
… - 但. 两个User对象…
确并不是结果唯一
这是为什么呢? set不是值唯一吗? 接下来让我们来深入源码!
HashSet 实现分析:
-
进行
深入 HashSet() 构造!
-
我们发现,HashSet的构造其实就是
HashMap
HashSet的值存放于HashMap的key上HashMap的value统一为PRESENT
那么, 这里就不深入了解了…后面对HashMap进行深入! -
而我们通过上面的代码,发现 两个一模一样的对象,
为什么出现了两次, 不是唯一值吗?
HashSet 存储原理:
-
首先, 我们已经知道, Hashset 底层就是HashMap
而, 一般要进行存储唯一的元素, 难免要对元素进行, 比较是否一致, 一致则不添加! 不一致添加HaseSet集合
Java 中进行比较的方法我们也都知道是equals()
而, equals其实本质上就是 == 比较地址, 上面两个对象地址不同当然不同,所以是唯一的! -
没错, HashSet 也确实是这么干的, 通过比较equals 对象是否true
一致
则不新增! -
如果, 我们想对新增的对象,类型的值,进行比较唯一,
对equals重写..即可!
不同地址对象, 值相同添加失败! 实现唯一的效果!
-
但
如果,只是重写 equals理论上确实可以实现效果... 实际却并不是这样!
改变上面 User重写equals
执行!
实际情况,直接使用工具重写即可!
-
执行之后, 发现效果并不变,
还是两个 id=1 name=张三
总结: ❗
- HashSet 本质上就是一个:
数组+链表
初始容量为16,当如果使用率超过0.75负载因子
(16*0.75=12)
就会扩大容量为原来的2倍。32 64...
HashSet 集合判断两个元素相等的标准 唯一 / 无序
- 直接通过equals 其实就可以,实现
唯一
的特点,但因为这样会极大影响程序性能!
一个个eq比较,如果集合有 10个 100个 1000个难道每次新增都要比较...?
太垃圾了!😥
通过 数组 + hashcode 和 equals 实现!
-
首先, 创建出一个 数组长度16的数组…
-
当 HashSet 新增一个元素,
-
首先调用 haseCode() 方法, 方法经过重写, 返回一个哈希值 **
内部通过某种算法...获得具体存放数组的位置!
比如取模 16直接就获得在数组中的位置…当然不会在这么简单的算法..
判断 ,该位置上是否存在元素 ,如果没有则说明元素不存在, 新增成功!
这里就表现出,无序的原因!
新增时候根据, 哈希值获得在数组上的位置!
-
存在: 则比较链表上元素的 哈希值
是否一样
, 不一样,新增成功!并以链表的形式, 排列在集合数组中..
-
hase值一致:
则直接比较 equale是否返回 true
true一致添加失败!
flase不一致添加成功!
继续排列链表!
haseCode() 和 equals()
重写 hashCode() 方法 原则
-
同一个对象多次调用 hashCode() 方法应该返回相同的值。
-
当两个对象的 equals() 方法比较返回 true 时 ,
这两个对象的 hashCode() 方法的返回值也应相等。 -
对象中用作 equals() 方法比较的属性,都应该用来计算 hashCode 值。
重写 equals() 方法
- 当一个类有自己特有的“逻辑相等”概念
改写equals()的时候,
总是要改写hashCode(),根据一个类的equals方法(改写后)
两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法,它们仅仅是两个对象
- 违反了:
相等的对象必须具有相等的散列码
- 复写equals方法的时候一般都需要同时复写hashCode方法。
通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。
IDEA工具的重写
User 的重写!
//比较同一个类型对象, 值是否一致:
//一致返回 true
//不一致返回 false
@Override
public boolean equals(Object o) { //返回true 表示对象一致,新增失败!
//比较对象地址是否一致
if (this == o) return true;
//判断当前对象是否为 null 或 类型一致...
if (o == null || getClass() != o.getClass()) return false;
//获取对象, 比较id name 属性是否一致....
User user = (User) o;
if (id != user.id) return false;
return name != null ? name.equals(user.name) : user.name == null;
}
//同一个类型, 相同值,调用该方法返回相同的hash值!
@Override
public int hashCode() { //idea hashCode生成!
int result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
为啥 *31
- 31 = (2的5次幂 -1)
- 通常程序,为了方便技术将一个数值进行 , 加倍变大…计算… 当然太大也不好!超过最大值了!
- 计算机是二进制进行计算的… 2的次幂会提高性能. 所以就找了一个不大不小
又是素数的 31
来提高程序计算效率!
JDK7 和 JDK8
JDK7
- 数组的实现的
饿汉式
创建时候就指定了长度! - 链表插入是头插法…
JDK8
- 数组的实现的
懒汉式
第一次使用时,才指定长度!
为什么7头插 8尾插
-
头插法是操作速度最快的,找到数组位置就直接找到插入位置了
但 , 因为hashmap是不安全的, 多线程情况下,
AB 执行添加, 在同一个数组位置, B先头插了… A本来要插在C 前面的… 造成死循环… -
jdk8开始hashmap链表在节点长度达到8之后会变成红黑树
-
这样一来在数组后节点长度不断增加时,遍历一次的次数就会少很多很多(否则每次要遍历所有)
-
尾插法比头插法而言,尾插法操作额外的遍历消耗已经小很多了,也可以避免之前的循环列表问题。 尾插法同样是线程不安全的。
LinkedHashSet
总结:
-
LinkedHashSet 是 HashSet 的子类
-
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,
但它同时使用双向链表维护元素的次序,这使得元素看起来以插入顺序保存的。
每新增一个元素时候, 会像 LinkedList 一样 每个元素指定了下一个元素, 和上一个元素的地址! -
LinkedHashSet插入性能略低于 HashSet
,但在迭代访问 Set 里的全部元素时有很好的性能。 -
LinkedHashSet 不允许集合元素重复。
TreeSet
-
TreeSet 是 SortedSet 接口的实现类
-
TreeSet 可以确保集合元素处于排序状态。
TreeSet 可以确保集合元素处于排序状态。
-
Integer和String对象都可以进行默认的TreeSet排序
而自定义类的对象是不可以的,
自己定义的类必须实现Comparable接口,并且覆写相应的compareTo()函数,才可以正常使用。 -
而且, TreeSet存储的是一组,相同类型的数据…
-
不像之前,
123
"ABC"
new User()
它一次只能存储一组类型… 因为, TreeSet会对存储的值进行排序…类型不同如何排序~
TreeSet 两种排序方法:自然排序
和定制排序
。默认情况下,TreeSet 采用自然排序。
-
默认 Integer 和 String类型可以存储
它俩默认实现了自然排序!
存储一组相同类型数据!
Java比较器
- Java中的对象, 正常情况下, 只能进行比较,==(同地址) 或 !=(地址不同)
不能使用 > < 的方式比较值..
- 但是在开发场景中, 我门需要对多个对象进行, 排序, 言外之意就是比较对象的大小;
- Java通过两个接口实现: Comparable( 中: 比较 读: 看牌啊爆 ) 或 Comparator( 中: 比较器 读: 看牌啊特 );
- 分为两种:
自然排序: java.util.Comparable
定制排序: java.util.Comparator
自然排序:
- 类实现,Comparable接口
重写 compareTo( obj );
- 可以对类对象进行,某种方式的排序; 称为:自然排序
- 实现接口的类,对象数组/集合。也可以通过, Collections.sort(); 或 Arrays.sort
其实内部就是Collection.sort();
给数组/集合进行排序;
- 这是我之前的代码截图…
- 总结:
要进行排序的对象类, 继承Comparable接口, 重写 compareTo(); 方法;
返回 1 当然对象 大于> 比较对象
返回 -1 当然对象 小于< 比较对象
返回 0 无法比较, 一般就像相等…但是在, TreeSet中如果比较值相等,表示
对象相等 Set 无序唯一原则:相等的对象,不存在, 添加失败!
定制排序:
因为Java是继承的…
- 当元素没有实现 Comparable接口, 而不方便修改代码;
- 或者实现了 Comparable 接口, 但其排序的操作,并不符合当前操作
String 默认从大小排,而我想要从小到大排序……
可以考虑 Comparator 类型对象来排序
实现:
User.Java
public class User {
private int id=0;
private String name;
//静态内部类..
public static class UserCompare implements Comparator {
public int compare(Object ob1, Object ob2){
User u1 = (User)ob1; //当前对象
User u2 = (User)ob2; //比较对象
//当前id 与 比较id 对比: 大于1 小于-1 等于0
int result = u1.id > u2.id ? 1 :(u1.id == u2.id ? 0 : -1);
//如果id一致在比较name... String默认存在compareTo排序...
if(result == 0){
result = u1.name.compareTo(u2.name);
}
return result;
}
}
//省略其它...
}
main 运行:
public class CSDemo {
public static void main(String[] args) {
//创建新增元素!
TreeSet hset = new TreeSet(new User.comp());
hset.add(new User(2, "张三"));
hset.add(new User(3, "张三"));
hset.add(new User(1, "张三"));
hset.add(new User(5, "张三"));
hset.add(new User(5, "张三"));
//遍历输出
Iterator it = hset.iterator();
while (it.hasNext()){
System.out.println(it.next());
}
}
}
两接口的重写的方法, 用于比较两个对象的大小
内部操作细节可自定义, 返回值 int , Java的 Arrays类会调用方法使用, 根据返回值给 数组元素重新排位置, 1 往后排 -1小往前 )
总结:
- TreeSet 和常规,的Set不同, 在新增元素的时候, 会根据元素的
自然排序
给元素排好序! - 因此, 对于自定义的类型要处理号排序…
自然排序
定制排序
而且, 存储的都是一组相同类型的数据!
底层在存储数据 也有一定变化!
- 新增一个元素时: ,
- 会调用对象类实现的 Comparable 接口的 compareTo() 方法和集合中的对象比较,根据方法返回的结果有序存储
- 如果比较结果为 0 则该元素 添加失败!