一、介绍
1、集合的概念
数组就是集合,集合实际上就是一个容器,可以容纳其他类型的数据。
集合是一个容器,是一个载体,可以一次容纳多个对象。
2、集合的储存
集合中不能直接存储基本数据类型,集合也不能直接存储Java对象,集合当中存储的都是Java对象的内存地址。(集合中存储的是引用);
集合中任何时候存储的都是引用;
当然,集合也有内存地址,集合也可以装集合。
3、集合与数据结构
在java中每一个不同的集合,底层会对应不同的数据结构。往不同的集合中存储元素,等于将数据放到了不同的数据结构当中。
常见的数据结构有:二叉树、数组、链表、哈希表…
使用了不同的集合等同于使用了不同的数据结构。
Java中已经有了这些常用集合类,只需掌握那些情况的用法。
new ArrayLIst(); 创建一个集合,底层是数组。
new LinkedList(); 创建一个集合,底层是链表。
new TreeSet() ; 创建一个集合对象,底层是二叉树。
二、集合的分类
Collection 接口的接口 对象的集合(单列集合)
Map 接口 键值对的集合 (双列集合)
在集合中集合分为两大类:
- 单个方式存储元素:
- 父接口:java.util.Collection
- 键值对的方式存储元素
- 父接口:java.util.Map
总结
List 有序、可重复 :
- ArrayList优点: 底层数据结构是数组,查询快,增删慢。缺点: 线程不安全,效率高
- Vector优点: 底层数据结构是数组,查询快,增删慢。缺点: 线程安全,效率低
- LinkedList优点: 底层数据结构是链表,查询慢,增删快。缺点: 线程不安全,效率高
Set 无序、唯一 :
- HashSet底层数据结构是哈希表。(无序、唯一)
- 1.如何来保证元素唯一性? 依赖两个方法:hashCode()和equals()
- LinkedHashSet底层数据结构是链表和哈希表。(FIFO插入有序、唯一)
- 1.由链表保证元素有序
- 2.由哈希表保证元素唯一
- TreeSet底层数据结构是红黑树。(排序有序、唯一)
- 1.如何保证元素排序的呢? 自然排序、比较器排序
- 2.如何保证元素唯一性的呢? 根据比较的返回值是否是0来决定
三、Collection
其中:
- 外框为虚线的表示接口,边框为实线的表示类;
- 箭头为虚线的表示实现了接口,箭头为实线的表示继承了类。
从图中可以看出,集合的根节点是 Collection,而 Collection 下又提供了两大常用集合,分别是:
- List:使用最多的有序集合,提供方便的新增、修改、删除的操作;
- Set:集合不允许有重复的元素,在许多需要保证元素唯一性的场景中使用。
1、Vector
Vector 是 Java 早期提供的线程安全的有序集合,如果不需要线程安全,不建议使用此集合,毕竟同步是有线程开销的。
Vector vector = new Vector();
vector.add("dog");
vector.add("cat");
vector.remove("cat");
System.out.println(vector); // [dog]
2、ArrayList
ArrayList 是最常见的非线程安全的有序集合,因为内部是数组存储的,所以随机访问效率很高,但非尾部的插入和删除性能较低,如果在中间插入元素,之后的所有元素都要后移。ArrayList 的使用与 Vector 类似。
3、LinkedList
LinkedList 是使用双向链表数据结构实现的,因此增加和删除效率比较高,而随机访问效率较差。
LinkedList 除了包含以上两个类的操作方法之外,还新增了几个操作方法,如 offer() 、peek() 等,请参考以下代码:
LinkedList linkedList = new LinkedList();
// 添加元素
linkedList.offer("bird");
linkedList.push("cat");
linkedList.push("dog");
// 获取第一个元素
System.out.println(linkedList.peek()); // dog
// 获取第一个元素,并删除此元素
System.out.println(linkedList.poll()); // dog
System.out.println(linkedList); // [cat,bird]
4、HashSet
HashSet 是一个没有重复元素的集合。虽然它是 Set 集合的子类,实际却为 HashMap 的实例,相关源码如下:
public HashSet() {
map = new HashMap<>();
}
HashSet 默认容量为 16,每次扩充 0.75 倍,相关源码如下:
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
5、TreeSet
TreeSet 集合实现了自动排序,也就是说 TreeSet 会把你插入数据进行自动排序。
示例代码如下:
TreeSet treeSet = new TreeSet();
treeSet.add("dog");
treeSet.add("camel");
treeSet.add("cat");
treeSet.add("ant");
System.out.println(treeSet); // [ant, camel, cat, dog]
6、LinkedHashSet
LinkedHashSet 是按照元素的 hashCode 值来决定元素的存储位置,但同时又使用链表来维护元素的次序,这样使得它看起来像是按照插入顺序保存的。
四、集合的排序
在 Java 语言中排序提供了两种方式:Comparable 和 Comparator,它们的区别也是常见的面试题之一。
1、Comparable
Comparable 位于 java.lang 包下,是一个排序接口,也就是说如果一个类实现了 Comparable 接口,就意味着该类有了排序功能。
package java.lang;
import java.util.*;
public interface Comparable {
public int compareTo(T o);
}
使用示例:
class ComparableTest {
public static void main(String[] args) {
Dog[] dogs = new Dog[]{
new Dog("老旺财", 10),
new Dog("小旺财", 3),
new Dog("二旺财", 5),
};
// Comparable 排序
Arrays.sort(dogs);
for (Dog d : dogs) {
System.out.println(d.getName() + ":" + d.getAge());
}
}
}
class Dog implements Comparable<Dog> {
private String name;
private int age;
@Override
public int compareTo(Dog o) {
return age - o.age;
}
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
//输出:
//小旺财:3
//二旺财:5
//老旺财:10
compareTo() 返回值有三种:
- e1.compareTo(e2) > 0 即 e1 > e2;
- e1.compareTo(e2) = 0 即 e1 = e2;
- e1.compareTo(e2) < 0 即 e1 < e2。
2、Comparator
Comparator 是一个外部比较器,位于 java.util 包下,
之所以说 Comparator 是一个外部比较器,是因为它无需在比较类中实现 Comparator 接口,而是要新创建一个比较器类来进行比较和排序。
public interface Comparator<T> {
int compare(T o1, T o2);
}
使用示例:
class ComparatorTest {
public static void main(String[] args) {
Dog[] dogs = new Dog[]{
new Dog("老旺财", 10),
new Dog("小旺财", 3),
new Dog("二旺财", 5),
};
// Comparator 排序
Arrays.sort(dogs,new DogComparator());
for (Dog d : dogs) {
System.out.println(d.getName() + ":" + d.getAge());
}
}
}
class DogComparator implements Comparator<Dog> {
@Override
public int compare(Dog o1, Dog o2) {
return o1.getAge() - o2.getAge();
}
}
class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
// 输出:
//小旺财:3
//二旺财:5
//老旺财:10
五、Map
1、Map 简介
Map 常用的实现类如下:
- Hashtable:Java 早期提供的一个哈希表实现,它是线程安全的,不支持 null 键和值,因为它的性能不如 ConcurrentHashMap,所以很少被推荐使用。
- HashMap:最常用的哈希表实现,如果程序中没有多线程的需求,HashMap 是一个很好的选择,支持 null 键和值,如果在多线程中可用 ConcurrentHashMap 替代。
- TreeMap:基于红黑树的一种提供顺序访问的 Map,自身实现了 key 的自然排序,也可以指定 Comparator 来自定义排序。
- LinkedHashMap:HashMap 的一个子类,保存了记录的插入顺序,可在遍历时保持与插入一样的顺序。
2、Map 常用方法
常用方法包括:put、remove、get、size 等,所有方法如下图:
使用示例:
Map hashMap = new HashMap();
// 增加元素
hashMap.put("name", "老王");
hashMap.put("age", "30");
hashMap.put("sex", "你猜");
// 删除元素
hashMap.remove("age");
// 查找单个元素
System.out.println(hashMap.get("age"));
// 循环所有的 key
for (Object k : hashMap.keySet()) {
System.out.println(k);
}
// 循环所有的值
for (Object v : hashMap.values()) {
System.out.println(v);
}
3、HashMap 数据结构
HashMap 底层的数据是数组被成为哈希桶,每个桶存放的是链表,链表中的每个节点,就是 HashMap 中的每个元素。
在 JDK 8 当链表长度大于等于 8 时,就会转成红黑树的数据结构,以提升查询和插入的效率。
4、HashMap 两个重要方法
(1)添加方法
执行流程如下:
- 对 key 进行 hash 操作,计算存储 index;
- 判断是否有哈希碰撞,如果没碰撞直接放到哈希桶里,如果有碰撞则以链表的形式存储;
- 判断已有元素的类型,决定是追加树还是追加链表,当链表大于等于 8 时,把链表转换成红黑树;
- 如果节点已经存在就替换旧值;
- 判断是否超过阀值,如果超过就要扩容。
方法源码:
public V put(K key, V value) {
// 对 key 进行 hash()
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// 对 key 进行 hash() 的具体实现
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// tab为空则创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算 index,并对 null 做处理
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;
// 超过load factor*current capacity,resize
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
(2)获取方法:get(Object key)
执行流程如下:
- 首先比对首节点,如果首节点的 hash 值和 key 的 hash 值相同,并且首节点的键对象和 key 相同(地址相同或 equals 相等),则返回该节点;
- 如果首节点比对不相同、那么看看是否存在下一个节点,如果存在的话,可以继续比对,如果不存在就意味着 key 没有匹配的键值对。
方法源码:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**@param hash key 的 hash 值,根据 hash 值在节点数组中寻址,该 hash 值是通过 hash(key) 得到的
* @param key key 对象,当存在 hash 碰撞时,要逐个比对是否相等
* @return 查找到则返回键值对节点对象,否则返回 null
*/
* 该方法是 Map.get 方法的具体实现
* 接收两个参数
*
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 声明节点数组对象、链表的第一个节点对象、循环遍历时的当前节点对象、数组长度、节点的键对象
// 节点数组赋值、数组长度赋值、通过位运算得到求模结果确定链表的首节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // 首先比对首节点,如果首节点的 hash 值和 key 的 hash 值相同,并且首节点的键对象和 key 相同(地址相同或 equals 相等),则返回该节点
((k = first.key) == key || (key != null && key.equals(k))))
return first; // 返回首节点
// 如果首节点比对不相同、那么看看是否存在下一个节点,如果存在的话,可以继续比对,如果不存在就意味着 key 没有匹配的键值对
if ((e = first.next) != null) {
// 如果存在下一个节点 e,那么先看看这个首节点是否是个树节点
if (first instanceof TreeNode)
// 如果是首节点是树节点,那么遍历树来查找
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 如果首节点不是树节点,就说明还是个普通的链表,那么逐个遍历比对即可
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) // 比对时还是先看 hash 值是否相同、再看地址或 equals
return e; // 如果当前节点e的键对象和key相同,那么返回 e
} while ((e = e.next) != null); // 看看是否还有下一个节点,如果有,继续下一轮比对,否则跳出循环
}
}
return null; // 在比对完了应该比对的树节点 或者全部的链表节点 都没能匹配到 key,那么就返回 null
5、HashMap 的遍历方式
HashMap 的遍历分为以下四种方式:
- 方式一:entrySet 遍历
- 方式二:iterator 遍历
- 方式三:遍历所有的 key 和 value
- 方式四:通过 key 值遍历
Map<String, String> hashMap = new HashMap();
hashMap.put("name", "老王");
hashMap.put("sex", "你猜");
// 方式一:entrySet 遍历
for (Map.Entry item : hashMap.entrySet()) {
System.out.println(item.getKey() + ":" + item.getValue());
}
// 方式二:iterator 遍历
Iterator<Map.Entry<String, String>> iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
System.out.println(entry.getKey() + ":" + entry.getValue());
}
// 方式三:遍历所有的 key 和 value
for (Object k : hashMap.keySet()) {
// 循环所有的 key
System.out.println(k);
}
for (Object v : hashMap.values()) {
// 循环所有的值
System.out.println(v);
}
// 方式四:通过 key 值遍历
for (Object k : hashMap.keySet()) {
System.out.println(k + ":" + hashMap.get(k));
}