文章目录
单列集合—Collection
1)List : 存取有序, 有索引, 可以存储重复的
①ArrayList : 底层数组结构 -> 查询快, 修改快;增删慢
- 优点:查询效率高,因为数组中的元素在内存中是连续的,可以快速的根据下标获取
集合中的元素 - 缺点:增删效率低,因为在对数组中元素进行增删操作的时候,涉及移位
②LinkedList : 底层是双向链表结构 -> 增删快;查询慢, 修改慢
- 优点:增删效率高,因为对链表上的元素进行增删操作的时候,不需要移位,只需
要改变链表中节点的指向即可 - 缺点:查询效率低,查询集合中的元素的时候,需要进行全链表的扫描
③Vector:底层数组结构 -> 只不过是线程安全的
2)Set : 存取无序, 无索引, 不可以存储重复的
①HashSet:底层是HashMap
②SortedSet:让集合中的元素具备了排序的能力
- TreeSet:底层是TreeMap
3)集合与数组对比
集合特点:
- 只能存储引用数据类型(基础数据类型——包装类)
- 长度可变
- 提供很多操作集合的方法
注意:
-
add()添加基本数据类型时,可以自动装箱:why? how?
- JVM会自动调用对应包装类的valueOf()方法:返回一个包装类
-
可以存储null值:how(联想到StringBuild添加null值)?
- null也是一个Object类
1.Collection[接口]具体方法
1)Collection的“toString()方法”
首先:因为Collection是最顶层的父接口, 内部具备的功能, 所有子类都有一份.
细节1: 如果一个类没有找到我们使用的方法, 可以去父类中查询
问题:Collection 并没有toString()方法,如何实现打印输出的呢?
代码:
public class Demo1_Collection {
public static void main(String[] args) {
// 1. 创建集合对象
// 在jdk1.7版本之后, 右侧的尖括号中可以不写类型, 菱形泛型
// <> : 泛型 -> 用于限制集合中存储的数据类型
Collection<String> c = new ArrayList<>();
// 2. 向集合中添加元素
c.add("abc");
c.add("bbb");
// 3. 展示集合中的元素
System.out.println(c.toString()); //谁调用的toString()
}
}
那么问题:Collection接口的toString方法从哪来的呢???
原因:接口虽然没有继承Object类,但是底层会存在Object类所有方法的签名(声明),
这样设计的原因在于:避免调用方法时,出现编译错误
2)Collection的成员方法
1)增
add(Object obj); 添加数据
addAll(Collection coll); 将参数中的所有数据添加到指定集合中
2)删
remove(Object obj); 删除数据
removeAll(Collection coll); 将参数集合中的数据从指定集合中移除
3)改:没有提供修改的方法
4)查
contains(12) 是否包含
containsAll(Collection coll) 返回指定集合中是否包含参数集合的全部内容
size() 获取数据的个数
isEmpty() 判断是否为空
clear() 清空
5)集合转数组
-
Collection --> Array
toArray();
-
Array —> Collection
Arrays.asList(arrays); 基本数据类型数组中数据不能转为集合
2.迭代器:Iterator 接口
Iterator iterator() ; 返回在此 collection 的元素上进行迭代的迭代器。
1.集合遍历:采用Iterator迭代器
-
Iterator iter = coll.iterator(); ——将一个指针指向集合的最上方
-
iter.next(); ——将指针指向下一个数据并返回该数据
- next() : [每一次调用], 内部的指针都会向后移动一位.**
- 在循环体中,推荐只调用一次next()方法;
-
iter.hasNext();//判断下一个是否有值
-
iter.remove();//移除迭代器此刻指向的对象
2.问题:Iterator 是个接口,它的实现类的实例化对象从哪来的?
//ArrayList中的源码:
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // index of next element to return 游标
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount; //复制当前集合的版本号,发生修改异常的原因就是版本号的判断
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
//迭代器自己的移除方法:移除迭代器此刻指向的对象
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
……
}
iterator()方法返回一个实现Iterator接口的成员内部类对象,
注意:
- 1)如果集合中存储的类型,是自定义类型,没有重写toString()的话,打印的是自定义类对象的地址值
- 2)contains()方法底层依赖于对应参数对象的equals()方法
- 3)remove()方法底层依赖于对应参数对象的equals()方法
- 4)remove()方法参数不会自动装箱,如果想要根据元素值来删除元素(如Integer类型数据),需要手动装箱
- 因为:Collection的remove()方法 在 其实现类如ArrayList中会被重写[remove(int) 和 remove(Object)]
- 使用多态形式调用 Collection 的 remove(int i) 调用的是 对应实现类的方法,并不会自动装箱
- 5)使用迭代器迭代的同时,不能使用集合自身的remove()方法移除元素,一般会抛出ConcurrentModificationException()异常
建议:
- 1)今后编写自定实体类(Student……),重写equals和toString方法
- 2)使用remove方法时判断是否需要手动装箱
- 3)使用迭代器迭代时删除元素:必须使用 迭代器对象自身的remove()方法;
示例代码:
public static void main(String[] args) {
// 1. 创建集合对象, 容器中存储的是Person对象
Collection<Person> c = new ArrayList<>();
// 2. 向集合中添加Person对象.
c.add(new Person("张三", 23));
c.add(new Person("李四", 24));
c.add(new Person("王五", 25));
// 注意: 如果添加的元素是自定义对象的话, 那么没有重写toString, 将打印地址值
System.out.println(c);
// 3. 调用集合的删除方法, 删除张三
// 注意 : remove方法底层依赖于equals方法.
c.remove(new Person("张三", 23));
System.out.println(c);
// 4. 调用集合中判断是否包含的方法
// 注意 : contains方法底层依赖于equals方法.
System.out.println(c.contains(new Person("李四", 24)));
// 5. 调用集合的清空方法
c.clear();
// 6. 调用集合判断是否为空的方法
System.out.println(c.isEmpty());
// 7. 打印集合的长度
System.out.println(c.size());
}
3.List接口
List 元素存取有序(添加顺序),有索引, 可以存储重复元素
List接口方法:
1)增
add(Object obj);
addAll(Collection coll);
//增加索引后
add(int index,Object obj);在指定索引位置添加数据(数据的后移)
addAll(int index,Collection coll);
2)删
remove(Object obj);
removeAll(Collection coll);
remove(int index);移除指定索引位置的值
clear();
3)改
list.set(int index, Object obj); 修改
4)查
list.size()获得数据个数
list.contains(56.7)
list.containsAll(null)
list.isEmpty()
get(int index)根据索引值取值
List接口的遍历方式
- a. Iterator
- b. 增强for循环
- c. 普通for循环
ArrayList
ArrayList是一个动态数组,实现了List接口以及list相关的所有方法,它允许所有元素的插入,包括null。另外,ArrayList和Vector除了线程不同步之外,大致相等。
1)构造方法
默认情况下,elementData是一个大小为0的空数组,当我们指定了初始大小的时候,elementData的初始大小就变成了我们所指定的初始大小了。
2)add()方法
在插入元素之前,它会先检查是否需要扩容,然后再把元素添加到数组中最后一个元素的后面。
检查扩容:ensureCapacityInternal()
在ensureCapacityInternal方法中,我们可以看见:
- 1)当elementData为空数组时,它会使用默认的大小去扩容。所以说,通过无参构造方法来创建ArrayList时,它的大小其实是为0的,只有在使用到的时候,才会通过grow方法去创建一个大小为10的数组。
- 2)当elementData为指定长度数组时[构造方法初始化时指定],需要扩容时,会通过 grow 方法去创建一个大小为原数组长度 1.5 被的新数组(注意:初始长度为1时,其0.5倍为0,但也会增到2)
3)grow()方法
grow方法是在数组进行扩容的时候用到的:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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);
}
从源码中,ArrayList每次扩容都是扩1.5倍(oldCapacity >> 1),然后调用Arrays类的copyOf方法,把元素重新拷贝到一个新的数组中去。
Vector
Vector很多方法都跟ArrayList一样,只是多加了个synchronized来保证线程安全,所以只把Vector与ArrayList的不同点提一下:
1)Vector比ArrayList多了一个属性:
protected int capacityIncrement;
这个属性是在扩容的时候用到的,它表示每次扩容只扩capacityIncrement个空间就足够了。该属性可以通过构造方法给它赋值。
2)构造方法初始化
从构造方法中可以看出Vector的默认大小也是10,而且它在初始化的时候就已经创建了数组了,这点跟ArrayList不一样。
3)grow方法:
从grow方法中我们可以发现,newCapacity[新容量]默认情况下是两倍的oldCapacity[旧容量],而当指定了capacityIncrement[Vector特有属性]的值之后,newCapacity变成了oldCapacity+capacityIncrement。
总结:
- ArrayList创建时的大小为0;当加入第一个元素时,进行第一次扩容时,默认容量大小为10。
- ArrayList每次扩容都以当前数组大小的1.5倍去扩容。
- Vector创建时的默认大小为10。
- Vector每次扩容都以当前数组大小的2倍去扩容。当指定了capacityIncrement之后,每次扩容仅在原先基础上增加capacityIncrement个单位空间。
- ArrayList和Vector的add、get、size方法的复杂度都为O(1),remove方法的复杂度为O(n)。
- ArrayList是非线程安全的,Vector是线程安全的。
- | 版本 | 底层 | 线程安全 | 效率 |
---|---|---|---|---|
ArrayList | JDK1.2 | 数组 | 不安全 | 较高 |
Vector | JDK1.0 | 数组 | 安全 | 较低 |
LinkedList
LinkedList 是通过一个双向链表来实现的,它允许插入所有元素,包括null,同时,它是线程不同步的。
1.基础知识:双向链表
双向链表每个结点除了数据域之外,还有一个前指针和后指针,分别指向前驱结点和后继结点(如果有前驱/后继的话)。
另外,双向链表还有一个first指针,指向头节点,和last指针,指向尾节点。
2.内部成员
1)属性:
链表的节点个数
transient int size = 0;
指向头节点的指针
transient Node<E> first;
指向尾节点的指针
transient Node<E> last;
2)结点结构:Node
Node 是在 LinkedList 里定义的一个静态内部类;
Node 表示链表每个节点的结构,包括一个数据域item,一个后置指针next,一个前置指针prev。
- E(Object) item;//真正存储当前节点的数据的
- Node next;//存储下一个节点对象
- Node prev;//存储上一个节点对象
3)常用方法
4)源码追踪
-
在实例化LinkedList对象时,构造器没有做任何事情(为LinkedList属性初始化)
-
在第一次添加时,创建一个节点对象(item=值,prev=null,next=null) 赋值给first和last
-
在第二次以及后面的每一次添加:
先将原来的最后一个节点备份
final Node l = last;创建一个新节点,设置新节点的前一个节点为之前的最后一个节点
final Node newNode = new Node<>(l, e, null);将新节点设置为当前的最后一个节点
last = newNode;将原来的最后一个节点的下一个节点设置为当前节点
l.next = newNode;
5)总结
-
LinkedList 的底层结构是一个带头/尾指针的双向链表,可以快速的对头/尾节点进行操作。
-
相比基于数组的ArrayList,链表结构的LinkedList的特点就是:在指定位置插入和删除元素的效率较高,但是查找指定元素的效率相对较慢
-
ArrayList和LinkedList的优缺点对比
底层 | 增删效率 | 改查效率 | |
---|---|---|---|
ArrayList | 数组 | 较低 | 较高★ |
LinkedList | 双向链表 | 较高 | 较低 |
4.Set接口 - 集合
1.Set集合的特点
- 无序(安插顺序):why
- 不可重复:why?how?
HashSet
源码分析:
HashSet底层其实是维护了一个HashMap,HashMap底层维护了一个Node类型的数组(Node是一个单向的节点)
添加的原理:
- 会根据添加的数据计算出当前数据对应的hash值 —— hashCode()
- 根据当前数据的hash值和数组(Node[])的长度计算出一个索引值,并作为当前数组下标
//1)Node hash值的计算: hashCode()
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//2)Node对应的哈希数组索引值的计算:hash值 和 数组(Node[])的长度
tab[i = (n - 1) & hash])
-
判断当前下标是否有数据,如果没有,则直接添加;
-
如果当前下标有数据:
- 判断这两个Node的hash值是否一致,如果hash值不一致,再判断当前结构是否是树结构
如果是树结构就按照树结构的方式添加新数据
如果不是树结构就按照单向链表的方式去添加(将新数据放在现有数据的next中 或者 进行覆盖) - 如果hash值是一样的,则判断内容是否一样((k = p.key) == key || (key != null && key.equals(k))),如果内容也一样则进行覆盖
- 判断这两个Node的hash值是否一致,如果hash值不一致,再判断当前结构是否是树结构
//判断条件
p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
HashSet的不重复原理:由 HashCode() 和 equals() 来决定的
HashSet的无序的原理:因为添加数据时是通过数据的hash值计算出来的数组索引值,去存储到指定单向链表中;而取值时,是从头开始,
导致HashSet集合是无序的
2.Set接口并没有提供额外的方法(Collection)
3.遍历方式
- a. Iterator
- b. foreach
4.实现类
- HashSet:如何实现的不重复?
- TreeSet(排列大小有序):如何实现的不重复、如何实现的排序?
- LinkedHashSet(存取有序) :如何实现的不重复、如何实现的存取有序?
思考:
往HashSet中添加若干个Person对象,我认为name和age是一样的两个人,不能同时添加到HashSet中
Person per=new Person("张三", 18);
Person per1=new Person("张三", 18);
TreeSet
1.特性:
- TreeSet 会有排序 不可重复
- 底层是一个红黑树
- 源码分析
TreeSet底层维护了一个TreeMap, 其排序在于 使用 compareTo() 来比较大小
- 正数this大,负数参数大,如果是0则说明相同
2.问题:HashSet和TreeSet实现不重复的方式是一样的吗?各是什么?
- HashSet:hashCode() + equals() = 哈希数组 + 链表/红黑树
- TreeSet : 使用 [元素对象本身的compareTo()方法 或 自己声明的排序器 Comparator ] 进行大小判断 = 红黑树 [数据结构与相应算法]
3.注意事项:
-
TreeSet不能存 null 值:因为比较大小时会抛出空指针异常
-
自定义的类型,要是没有指定排序方式,将会出现ClassCastException异常,
- 因为当前的类型不具备可比性。
1)自然排序[内部比较器] --> 元素实现了 Comparable 接口
元素对应的类实现了 Comparable 接口 --> 重写compareTo() 方法
- 1.添加的数据所属类型实现接口 implements Comparable
- 2.实现接口中的抽象方法 int compareTo(Object obj)
Comparable接口API描述:
Comparable<T>
类型参数:
T - 可以与此对象进行比较的那些对象的类型
方法摘要:
int compareTo(T o)
比较此对象与指定对象的顺序。
2)比较器排序[定制排序] --> 传入 Comparator接口的实现类的实例化对象
自定义接口实现类 或 使用接口的匿名内部类(JDK8会有优化)–> 采用TreeSet的有参构造方法
- 在构造器中传入Comparator类型对象
- 由于Comparator是一个接口,采用创建实现类或者匿名内部类去实现抽象方法int compare(Object o1,Object o2)
Comparator接口API描述:
接口 Comparator<T>
类型参数:
T - 此 Comparator 可以比较的对象类型
int compare(T o1, T o2)
比较用来排序的两个参数。
boolean equals(Object obj)
指示某个其他对象是否“等于”此 Comparator。
3)TreeMap源码
private final Comparator<? super K> comparator;
public TreeMap() {
comparator = null;
}
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
外部排序案例:
TreeSet b = new TreeSet( new Comparator(){
@Override
public int compare(Object o1, Object o2) {
System.out.println("匿名的外部排序执行啦");
return ((Person)o1).getAge() - ((Person)o2).getAge();
}
});
b.add(new Person("张三", 18));
思考:往TreeSet中放入若干个Person对象,实现年龄的排序,如果都存在,谁优先级高?
4)Collecions工具类的两个 sore() 排序方法
1.sort(List list)
public static <T extends Comparable<? super T>> void sort(List<T> list)
根据元素的 自然顺序 对指定列表按升序进行排序。
列表中的所有元素都必须实现 Comparable 接口。
此外,列表中的所有元素都必须是可相互比较的。
- (也就是说,对于列表中的任何 e1 和 e2 元素,e1.compareTo(e2) 不得抛出 ClassCastException)。
2.sort(List list, Comparator<? super T> c)
public static <T> void sort(List<T> list, Comparator<? super T> c)
根据指定比较器产生的顺序对指定列表进行排序。
此列表内的所有元素都必须可使用指定比较器相互比较
- 也就是说,对于列表中的任意 e1 和 e2 元素,c.compare(e1, e2) 不得抛出 ClassCastException
LinkedHashSet
底层数据结构是?
- 哈希表 + 单向链表 + 双向链表
怎么实现不重复? --> extends HashSet new 对象时使用的是LinkedHashMap(其底层依旧是 HashMap)
LinkedHashMap: extends HashMap<K,V>
//内部类 Entry 继承自 HashMap.Node : 为双向链表结构多了一个头,一个尾
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);
}
}
//指向双向链表的尾部
transient LinkedHashMap.Entry<K,V> head;
//指向双向链表的头部
transient LinkedHashMap.Entry<K,V> tail;
如何实现存取有序? 底层结构实现了 双向链表
5.Collections工具类
Arrays 操作数组的工具类
Collections 操作集合(Collection)
reverse(List):反转 List 中元素的顺序[直接操作原List]
shuffle(List):对 List 集合元素进行随机排序
sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object):返回指定集合中指定元素的出现次数
void copy(List dest,List src):将src[source]中的内容复制到dest[destination 目的地]中(**注意条件**:目标列表的size >= 源列表的size )
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
6.问题
ArrayList 如何进行扩容操作的?以及与Vector的区别?
LinkedList 的底层存储结构,以及如何进行增删操作?
HashSet如何实现的不重复(HashMap)?
TreeSet如何实现的不重复、如何实现的排序?
HashMap源码解析:各成员变量的作用,与执行原理,以及如何保证key的唯一性,为何要转为红黑树? 如何转为红黑树? hash值的计算? 如何扩容?扩容之后如何保证hash值计算正确?