一、集合概述
- 为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java 提供了集合类。集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。Java 所有的集合类都位于 java.util 包下,提供了一个表示和操作对象集合的统一构架,包含大量集合接口,以及这些接口的实现类和操作它们的算法。
- Java 集合类型分为 Collection 和 Map,它们是 Java 集合的根接口,这两个接口又包含了一些子接口或实现类。
1.1集合与数组的区别
- 数组可以保存基本类型的数据,也可以是引用类型的数据;而集合只能保存对象,比如你存入一个int型数据放入集合中,其实它是自动转换成Integer类后存入的,Java中每一种基本数据类型都有对应的引用类型。
- 数组只能存储同一种数据类型的数据,集合可以存储不同类型的数据。(但是一般也只保存同一种数据类型的数据)。
- 数组的长度一旦初始化就被定义好了,不可变;集合的长度是可变的.
1.2集合的大致接口实现
|----Collection接口:单列集合,用来存储一个一个的对象
|----List接口:存储有序的、可重复的数据
|----ArrayList:作为List接口的主要实现类,线程不安全的,效率高;底层采用Object[] elementData数组存储
|----LinkedList:对于频繁的插入删除操作,使用此类效率比ArrayList效率高底层采用双向链表存储
|----Vector:作为List的古老实现类,线程安全的,效率低;底层采用Object[]数组存储
|----Set接口:存储无序的、不可重复的数据
|----HashSet:作为Set接口主要实现类;线程不安全;可以存null值
|----LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加顺序遍历;对于频繁的遍历操作,LinkedHashSet效率高于HashSet.
|----TreeSet:可以按照添加对象的指定属性,进行排序。
|----Map:双列数据,存储key-value对的数据
|----HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value
|----LinkedHashMap:保证在遍历map元素时,可以照添加的顺序实现遍历,底层双向链表实现。
原因:在原的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap。
|----TreeMap:保证照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序,底层使用红黑树
|----Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
|----Properties:常用来处理配置文件。key和value都是String类型
二、Collection接口
- Collection 接口是 List、Set 和 Queue 接口的父接口,通常情况下不被直接使用。
- Collection 接口定义了一些通用的方法,通过这些方法可以实现对集合的基本操作。定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
2.1 Collection接口常用的方法
方法名称 | 说明 |
---|---|
boolean add(E e) | 向集合中添加一个元素,如果集合对象被添加操作改变了,则返回 true。E 是元素的数据类型 |
boolean addAll(Collection c) | 向集合中添加集合 c 中的所有元素,如果集合对象被添加操作改变了,则返回 true。 |
void clear() | 清除集合中的所有元素,将集合长度变为 0。 |
boolean contains(Object o) | 判断集合中是否存在指定元素 |
boolean containsAll(Collection c) | 判断集合中是否包含集合 c 中的所有元素 |
boolean isEmpty() | 判断集合是否为空 |
Iterator<E>iterator() | 返回一个 Iterator 对象,用于遍历集合中的元素 |
boolean remove(Object o) | 从集合中删除一个指定元素,当集合中包含了一个或多个元素 o 时,该方法只删除第一个符合条件的元素,该方法将返回 true。 |
boolean removeAll(Collection c) | 从集合中删除所有在集合 c 中出现的元素(相当于把调用该方法的集合减去集合 c)。如果该操作改变了调用该方法的集合,则该方法返回 true。 |
boolean retainAll(Collection c) | 从集合中删除集合 c 里不包含的元素(相当于把调用该方法的集合变成该集合和集合 c 的交集),如果该操作改变了调用该方法的集合,则该方法返回 true。 |
int size() | 返回集合中元素的个数 |
Object[] toArray() | 把集合转换为一个数组,所有的集合元素变成对应的数组元素。 |
三、Collection接口子接口:List
1.概述
- List 是一个有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List 集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。List 集合默认按元素的添加顺序设置元素的索引,第一个添加到 List 集合中的元素的索引为 0,第二个为 1,依此类推。
- List 实现了 Collection 接口,它主要有两个常用的实现类:ArrayList 类和 LinkedList 类。
2.List接口常用方法
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:
方法 | 描述 | |
---|---|---|
void add(int index, Object o) | 在index位置插入ele元素 | |
boolean addAll(int index, Collection c) | 从index位置开始将c中的所有元素添加进来 | |
Object get(int index) | 获取指定index位置的元素 | |
Object remove(int index) | 移除指定index位置(0是第一个元素)的元素,并返回此元素 | |
Object set(int index, Object o) | 设置指定index位置的元素为o | |
List subList(int fromIndex, int toIndex) | 返回从fromIndex到toIndex位置的子集合 |
3.List实现类(1)ArrayList
ArrayList 类实现了可变数组的大小,存储在内的数据称为元素。它还提供了快速基于索引访问元素的方式,对尾部成员的增加和删除支持较好。使用 ArrayList 创建的集合,允许对集合中的元素进行快速的随机访问,不过,向 ArrayList 中插入与删除元素的速度相对较慢。
ArrayList实现List接口后,未新增方法,我们就简单实现一下List新增方法。
创建一个汽车类,在该类中定义 2 个属性和 toString() 方法,分别实现 setter/getter 方法。代码的实现如下:
class Car {
private String name;
private Integer price;
public Car(String name, Integer price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
接下来就是主函数:
public static void main(String[] args) {
Car car = new Car("宝马", 400000);
Car car1 = new Car("宾利", 5000000);
ArrayList<Car> cars = new ArrayList<>();
cars.add(car);
cars.add(car1);
// 刚开始遍历
for (Car car2 : cars) {
System.out.println(car2);
}
System.out.println("========");
Car bens = new Car("bens", 1000000);
// void add(int index, Object o)
cars.add(0, bens);
System.out.println("加入一个对象后集合的size: " + cars.size());
// Object remove(int index)
System.out.println("被移除的对象是: " + cars.remove(2));
System.out.println("移除一个对象后集合的size: " + cars.size());
// Object get(int index)
System.out.println("根据索引0取到的对象: " + cars.get(0));
// Object set(int index, Object o)
Car car3 = new Car("三轮", 100);
System.out.println("======== ========");
// 最后遍历一下全部
for (Car car2 : cars) {
System.out.println(car2);
}
}
输出结果如下:
Car{name='宝马', price=400000}
Car{name='宾利', price=5000000}
========
加入一个对象后集合的size: 3
被移除的对象是: Car{name='宾利', price=5000000}
移除一个对象后集合的size: 2
根据索引0取到的对象: Car{name='bens', price=1000000}
======== ========
Car{name='bens', price=1000000}
Car{name='宝马', price=400000}
4.List实现类(2)LinkedList
LinkedList 类采用链表结构保存对象,这种结构的优点是便于向集合中插入或者删除元素。需要频繁向集合中插入和删除元素时,使用 LinkedList 类比 ArrayList 类效果高,但是 LinkedList 类随机访问元素的速度则相对较慢。这里的随机访问是指检索集合中特定索引位置的元素。
LinkedList 类除了包含 Collection 接口和 List 接口中的所有方法之外,还特别新增了以下表中所示的方法。
LinkList类中的方法
方法名称 说明 void addFirst(E e) 将指定元素添加到此集合的开头 void addLast(E e) 将指定元素添加到此集合的末尾 E getFirst() 返回此集合的第一个元素 E getLast() 返回此集合的最后一个元素 E removeFirst() 删除此集合中的第一个元素 E removeLast() 删除此集合中的最后一个素
用一个简答的例子来实现一下LinkedList新增的方法.代码的实现如下:
public static void main(String[] args) {
LinkedList<String> fruit = new LinkedList<>();
fruit.add("苹果");
fruit.add("香蕉");
fruit.add("菠萝");
fruit.add("山竹");
fruit.add("草莓");
System.out.println(" ******** 目前水果店中的类型有 ********");
for (String s : fruit) {
System.out.printf(s + "\t");
}
System.out.println();
String s1 = new String("橘子");
String s2 = new String("甘蔗");
fruit.addFirst(s1);
fruit.addLast(s2);
System.out.println("**** 在集合头部添加了\'橘子\'和尾部添加了\'甘蔗\'后水果店类型
****");
for (String s : fruit) {
System.out.printf(s + "\t");
}
System.out.println();
String s = fruit.removeLast();
String last = fruit.getLast();
System.out.println("删除的最后一个对象为: " + s);
System.out.println("集合现最后一个对象为: " + last);
}
输出结果如下:
******** 目前水果店中的类型有 ********
苹果 香蕉 菠萝 山竹 草莓
**** 在集合头部添加了'橘子'和尾部添加了'甘蔗'后水果店类型 ****
橘子 苹果 香蕉 菠萝 山竹 草莓 甘蔗
删除的最后一个对象为: 甘蔗
集合现最后一个对象为: 草莓
5.List实现类(3)Vector
Vector的内部实现类似于ArrayList,Vector也是基于一个容量能够动态增长的数组来实现的,该类是JDK1.0版本添加的类,它的很多实现方法都加入了同步语句,因此是线程安全的。
但因为他的线程安全,因此效率很慢,所有不推荐使用,现在有更好的实现线程安全的替代方法,比如CopyOnWriteArrayList。
由于Vector是JDK1.0版本的类,所以方法名可能有所不同,但实际实现的效果都差不多。下面列举了几个方法:
public void addE1ement (E obj) | 相当于add() |
public E elementAt(int index) | 相当于get() |
public Enumeration<E> elements() | iterator() |
6.ArrayList,LinkedList,Vector的区别
ArrayList | Vector | LinkedList | |
底层实现方式 | Object数组 | Object数组 | 双向链表 |
读写机制 | 插入:超过当前数组预定义的最大值时,数组需要扩容,扩容为原数组大小的1.5倍,扩容过程需要调用底层System.arraycopy()方法进行数组复制操作 删:删除元素时并不会减少数组的容量,如需缩小容量,可调用trimToSize()方法 | 读写机制与ArrayList基本一致,区别是Vector在扩容时,容量会提高一倍 | 在查找元素时,需遍历链表 在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元素删除 |
读写效率 | ArrayList对元素的增加和删除都会引起数组的内存分配空间动态发生变化,对其进行插入和删除速度较慢,但检索速度快 | 效率低 | LinkedList由于基于链表方式存放数据,增加和删除元素的速度较快,但检索速度较慢 |
线程安全性 | 非线程安全 | 线程安全 | 非线程安全 |
四、Collection接口子接口:Set
1.概述
- Set 集合类似于一个罐子,程序可以依次把多个对象“丢进”Set 集合,而 Set 集合通常不能记住元素的添加顺序。也就是说 Set 集合中的对象不按特定的方式排序,只是简单地把对象加入集合。Set 集合中不能包含重复的对象,并且最多只允许包含一个 null 元素。
- 它与 Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,但是比 Collection 接口更加严格。
- Set 实现了 Collection 接口,它主要有两个常用的实现类:HashSet 类和 TreeSet类。
2.Set实现类(1)HashSet
- hashSet底层其实是通过hashMap实现的。它将要存储的对象直接做为map的key,并且创建一个空的object对象作为map的值。
- 因为HashMap的Key不可重复,而我们只关心Key,所以HashSet元素不可重复,元素无序。
- 要想真正了解HashSet的实现,那你就得了解HashMap的具体实现。
3.Set实现类(2)LinkedHashSet
LinkedHashSet继承了HashSet。与HashMap有所不同的是它底层使用LinkedHashMap来实现,即在HashMap的基础上新增加了一个双向链表来确保迭代有序(迭代的元素顺序和插入的元素顺序相同)。因此性能略低于HashSet,但是迭代访问元素时将具有很好的性能,因为它的内部是以链表来实现。
4.Set实现类(3)TreeSet
- 它存储唯一的元素
- 它不保留元素的插入顺序
- 它按升序对元素进行排序
- 它不是线程安全的
与 HashSet 完全类似的是,TreeSet 里绝大部分方法都是直接调用 TreeMap 的方法来实现的。
对象根据其自然顺序以升序排序和存储。该TreeSet中使用红黑树。
五、Map
1.概述
- Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键(key)对象和一个值(value)对象。用于保存具有映射关系的数据。
- Map 集合里保存着两组值,一组值用于保存 Map 里的 key,另外一组值用于保存 Map 里的 value,key 和 value 都可以是任何引用类型的数据。Map 的 key 不允许重复,value 可以重复,即同一个 Map 对象的任何两个 key 通过 equals 方法比较总是返回 false。
2.Map常用方法
方法名称 | 说明 |
---|---|
void clear() | 删除该 Map 对象中的所有 key-value 对。 |
boolean containsKey(Object key) | 查询 Map 中是否包含指定的 key,如果包含则返回 true。 |
boolean containsValue(Object value) | 查询 Map 中是否包含一个或多个 value,如果包含则返回 true。 |
V get(Object key) | 返回 Map 集合中指定键对象所对应的值。V 表示值的数据类型 |
V put(K key, V value) | 向 Map 集合中添加键-值对,如果当前 Map 中已有一个与该 key 相等的 key-value 对,则新的 key-value 对会覆盖原来的 key-value 对。 |
void putAll(Map m) | 将指定 Map 中的 key-value 对复制到本 Map 中。 |
V remove(Object key) | 从 Map 集合中删除 key 对应的键-值对,返回 key 对应的 value,如果该 key 不存在,则返回 null |
boolean remove(Object key, Object value) | 这是 Java 8 新增的方法,删除指定 key、value 所对应的 key-value 对。如果从该 Map 中成功地删除该 key-value 对,该方法返回 true,否则返回 false。 |
Set entrySet() | 返回 Map 集合中所有键-值对的 Set 集合,此 Set 集合中元素的数据类型为 Map.Entry |
Set keySet() | 返回 Map 集合中所有键对象的 Set 集合 |
boolean isEmpty() | 查询该 Map 是否为空(即不包含任何 key-value 对),如果为空则返回 true。 |
int size() | 返回该 Map 里 key-value 对的个数 |
Collection values() | 返回该 Map 里所有 value 组成的 Collection |
3.实现类HashMap
- HashMap是Java语言中用的最频繁的一种数据结构。
- HashMap 在jdk1.7的底层数据结构是数组加链表。
- HashMap 在jdk1.8的底层数据结构是数组+链表+红黑树。
我们以1.8版本为例:
put方法源码解析
put方法其实是调用的putVal方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
(⊙o⊙)… 没错,它就是长这么恶心,那我们就分块分别来解析吧。
(1)Node<K,V>[] tab中tab表示的就是数组。Node<K,V> p中p表示的就是当前插入的节点
(2)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
如果数组为空,就调用resize 函数初始化数组;
(3)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
如果要插入的位置p为空,也就是没有节点,那么直接插入到该位置
(4)
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
如果hash冲突,要插入的位置的节点的键与传入的节点的键相同。那么直接替换掉原来位置上的节点。如果被插入的位置是红黑树,那么转化为树节点插入到树中。
(5)
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
遍历链表,如果遍历到链表最后,没有找到和要插入的节点的key相同的节点,就将要插入的节点追加到链表的最后,并且此时如果链表的长度大于8了,就应该将链表转化为红黑树,退出循环;如果在链表中找到了和要插入的节点的key相等的节点,直接退出循环。
后续操作,如果e不为null,说明找到了和要插入的节点的key相等的节点,就将要插入的节点的value替换给老节点e的value,并对e节点进行afterNodeAccess(e)操作。
(6)
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
插入成功之后,还要判断一下实际存在的键值对数量size是否大于阈值threshold。如果大于那就开始扩容了。
4.实现类TreeMap
TreeMap 类的使用方法与 HashMap 类相同,唯一不同的是 TreeMap 类可以对键对象进行排序,这里不再赘述。