Java基础内容集合
这部分是Java中的基础内容,集合,也叫做Java容器,用在很多的地方。
集合是用来存储数据的,简称为容器,其中这里的存储指内存层面的存储,不是持久化存储。
1. 数组的特点:
- 指定长度后,长度不可以更改
- 声明了类型后,数组只能存放这个类型的数据。
- 数组的查询效率高,删除、增加元素的效率低
- 数组中实际元素的数量无法获取,没有提供具体的方法来获取
- 数组存储数据是有序可重复的,这里的有序不是指实际数据排序,指是线性排列的意思
综上所述,数组的灵活性低,引入了集合,有Set、List、Map等等,不同集合底层数据结构不一样,所以每个集合都有各自的特点,适用于不同的场景。
2. 集合体系结构图

3. 集合的应用场合:
集合是用来存储数据的,在整个开发过程中从始到终一直在用到,根据各个集合的特点选用对应合适的集合使用。其中集合只能存放引用数据类型的数据,不能是基本数据类型,基本数据类型自动装箱。当使用泛型时候,为了方便统一管理,一般只会存入一种数据类型。
- 大致总结一下如:
- 如果需要频繁访问元素,使用ArrayList
- 如果需要频繁插入和删除元素,使用LinkedList
- 如果不允许重复元素且无序,使用HashSet
- 如果需要自动排序,使用TreeSet
- 如果需要快速查找和存储键值对,使用HashMap
- 如果需要按键自动排序,使用TreeMap
区别:
List接口特点:不唯一,有序的
Set接口特点:唯一,无序的(无序不等于随机,是相对于List接口部分来说的)
4. ArrayList
ArrayList是一个可变大小的数组实现,适用于需要频繁访问元素的场景,查询快,增删慢,是多线程不安全的。
在提前知道list长度的时候,可以提前指定集合的容量,这样可以减少扩容的次数,提高性能(数据量越大,效果越明显)
ArrayList源码类似StringBuilder,其中jdk1.7 中和jdk1.8版本中ArrayList实现有所不同(以下使用jdk1.8版本做说明)。

新建一个ArrayList对象,未指定大小时候,使用的数组为空。调用add后,才会确定数组长度,存实际数据


新增到空间不够了,再进行扩容

ArrayList底层的数据结构分为物理结构和逻辑结构,物理结构对应紧密结构,在内存中是连续着的,形成的逻辑结构是线性表中的数组。
public static void main(String[] args) {
List<Object> arrayList = new ArrayList<>();
arrayList.add(10);
arrayList.add(8);
arrayList.add(13);
System.out.println("arrayList中数据:" + arrayList);
arrayList.add(1, 24);
System.out.println("arrayList中数据2:" + arrayList);
arrayList.set(3, 12);
System.out.println("arrayList中数据3:" + arrayList);
arrayList.add(2);
System.out.println("arrayList中数据4:" + arrayList);
arrayList.remove(2);
System.out.println("arrayList中数据5:" + arrayList);
}
输出
arrayList中数据:[10, 8, 13]
arrayList中数据2:[10, 24, 8, 13]
arrayList中数据3:[10, 24, 8, 12]
arrayList中数据4:[10, 24, 8, 12, 2]
arrayList中数据5:[10, 24, 12, 2]
5. LinkedList
底层使用的双向链表,使用链表查找元素,所以查询慢,增删只需要替换掉指针的指向即可,所以增删快,元素按照插入的顺序排列,元素可以重复。

新增数据时候,创建新Node节点信息,指向上一个元素的地址

Node对象:存前一个元素的地址,当前存入的元素,下一个元素的地址,整个对象是链表中的一个节点。

遍历LinkList的方式:
for (Object node : linkedList) {
System.out.println(node);
}
Iterator<Object> iterator = linkedList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
// 这种方式,it在for循环这个作用域内有效,for循环结束时候,it这个对象的生命周期结束,就自动消失了
for (Iterator<Object> it = linkedList.iterator(); it.hasNext(); ) {
System.out.println(it.next());
}
- LinkedList的数据结构:
- 物理结构:内存中是跳转结构,不连续的
- 逻辑结构:是线性表中的链表
6. HashSet
Set接口里没有跟索引相关的方法,意味着不能用普通for循环遍历
- 遍历方式:
- 迭代器
- 增强for循环(底层还是迭代器来的)
存放基本数据类型的数据时候,都满足唯一,无序的特点。
存放自定义类型的数据时候,没有满足唯一的特点。
HashSet集合存入的数据(以Integer类型为例):
调用对应的hashCode方法计算哈希值,哈希值是int类型的数据,再通过哈希值和一个表达式计算在数组中存放的位置。如位置冲突,则维护一个链表来存储数据,如果存放的数据重复了,不会再次放入(使用equals方法比较)
HashSet底层原理:数组+链表,即哈希表
所以,放入HashSet中的数据,一定要重写两个方法:hashCode和equals
其中底层用到了HashMap的结构

public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
// 所以说HashSet底层是用HashMap来完成的
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// value都是一样的,用来占位
return map.put(e, PRESENT)==null;
}
}
示例:
public static void hashSetTest() {
HashSet<Object> set = new HashSet<>();
set.add(12);
set.add(8);
set.add(9);
set.add(42);
set.add(14);
System.out.println(set);
}
输出:
[8, 9, 42, 12, 14]
7. LinkedHashSet
HashSet是无序的,所以出现LinkedHashSet,是唯一,有序的(按照输入顺序进行输出)
底层原理:其中就是再HashSet的基础上,多了一个总的链表,这个总链表将放入的元素串在一起,方便有序的遍历
public static void hashSetTest() {
HashSet<Object> set = new LinkedHashSet<>();
set.add(12);
set.add(8);
set.add(9);
set.add(42);
set.add(14);
System.out.println(set);
}
输出:
[12, 8, 9, 42, 14]
8. TreeSet
首先看一下比较器的内容先:
内部比较器java.lang.Comparable#compareTo,返回int类型,判断>0 =0 <0 来确定数据的大小

String类型也可以比较大小,因为实现了Comparable方法,重写了其中的compareTo方法
外部比较器:实现Comparator接口,重写compare方法
外部比较器比内部比较器好用些,因为用了多态,好扩展
然后是TreeSet的内容:
特点:唯一的,有序:可以按照升序进行遍历,没有按照输入顺序进行输出
底层是用的二叉树(逻辑结构)
其中二叉树有三种遍历方式,中序遍历、先序遍历、后序遍历
当前的TreeSet底层的二叉树使用的中序遍历,所以输出是升序的。
例如:Integer 实现了Comparable接口,所以可以比较(基本数据类型)
public static void test() {
TreeSet<Integer> set = new TreeSet<>();
set.add(6);
set.add(4);
set.add(34);
set.add(12);
System.out.println(set);
}
输出:
[4, 6, 12, 34]
如果放入自定义类型时候,自定义类型必须实现比较器,否则使用TreeSet添加数据会失败,可以使用内部比较器或者外部比较器。
public static void test() {
Comparator<User> comparator = new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
// return o1.getName().compareTo(o2.getName());
return o1.getAge() - o2.getAge();
}
};
TreeSet<User> set = new TreeSet<>(comparator);
set.add(new User(15, "娜娜"));
set.add(new User(12, "苗苗"));
set.add(new User(13, "文文"));
set.add(new User(15, "红红"));
System.out.println(set);
System.out.println(set.size());
}
输出:
[User{age=12, name='苗苗'}, User{age=13, name='文文'}, User{age=15, name='娜娜'}]
3
底层原理和TreeMap类似,通过TreeMap的key来控制TreeSet的元素
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object();
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
//底层创建了一个TreeMap
public TreeSet() {
this(new TreeMap<E,Object>());
}
// 通过add增加数据,实际上是通过TreeMap的put方法放数据
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
}
9. HashMap
首先Map是一个泛型接口,存储是key-value形式,称为键值对,键用key表示,值用value表示,是一对信息一起存储
HashMap的特点(在key的角度):无序,唯一的(是按照key进行总结的,因为底层key是遵循哈希表的)
哈希表的原理:比如放入集合的数据的那个类,必须重写hashCode和equals方法
一般HashMap的存放的key都是String类型的,String这个类默认重写了hashCode和equals方法
HashMap是无序的,不能按照输入顺序进行输出
首先HashMap底层维护了一个哈希表(数组+链表),使用key的hashCode方法拿到哈希码,通过哈希码和表达式,计算出元素在数组中的位置,就可以放如对应元素。在数组中存入元素的类型是entry类型。
如果存入元素发生哈希冲突了,底层就形成了一个链表,jdk7使用链表的头插法,jdk8 使用链表的后插法新增Node节点(即7上8下)
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
//16,底层数组的长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大的容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 定义了一个负载因子,又叫加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//底层主数组
transient Node<K,V>[] table;
//集合中的元素数量
transient int size;
//默认为0,用来表示数组扩容的边界值/门槛值
int threshold;
// 用来接收负载因子
final float loadFactor;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 存储数据的方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 底层主数组为空时候,初始化处理一下
// n是数组的长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// i 是元素在数组中的下标位置,通过 (n - 1) & hash 来计算
if ((p = tab[i = (n - 1) & hash]) == null)
// 当前位置没有发生哈希冲突时候,直接新建Node,放进数组中,null指的是没有下一个Node节点
tab[i] = newNode(hash, key, value, null);
else {
// p 不等于null,说明当前位置上有Node了
Node<K,V> e; K k;
//发生哈希碰撞时,先比较哈希值,再比较key是否是一个对象,如果key是一个对象,不使用equals比较
//如果不是同一个对象,使用equals方法比较是否一样
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循环,直到找到链表的最后一个节点,使用后插法
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;
}
}
// key存在处理,使用新的value替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 会返回oldValue
return oldValue;
}
}
++modCount;
// 如果下一次新增元素可能会大于临界值,需要扩容
// 数组长度扩2倍,然后将老数组的元素放到新数组里,以后使用新数组
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
// 二次hash,没有直接用hashCode的值,为了解决哈希冲突
// 右移,使用扰动函数,用来增加值的不确定性,让最终得到的数组尽量不一样(让元素所在位置不一样)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
面试题(题外话):
- 负载因子为什么是0.75?
如果负载因子增大(设置为1):空间利用率得到了很多的满足(数组的16个位置都用到了),很容易碰撞,就会产生链表,导致查询效率低
如果负载因子减小(设置为0.5):碰撞的概率低,直接进行扩容,产生链表的几率低,导致查询效率高,但是空间利用率太低
所以取中间值,做折中。 - 主数组的长度为什么必须为2^n?
原因1:(n - 1) & hash 等效于 hash对n取余操作,等效的前提就是:n必须是2的整数倍(n是主数组的长度,hash是key的哈希值)
原因2:防止哈希冲突,元素在主数组上的位置冲突
10. TreeMap
这里可以和TreeSet对比着来学习
特点:唯一,有序(可以按照升序或者降序来输出)
底层原理:二叉树,这里key遵循二叉树的特点
所以放入集合中key的数据所对应的类型内部一定要实现比较器(内部比较器或者外部比较器都可以)
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
//外部比较器
private final Comparator<? super K> comparator;
//树的根节点,节点类型是Entry
private transient Entry<K,V> root;
// 集合中元素的数量
private transient int size = 0;
// 如果使用空构造器,底层就不使用外部比较器
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;
// 如果放第一个元素,root为null
if (t == null) {
compare(key, key); // type (and possibly null) check
// 根节点为当前元素,确定root
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
// cpr 为外部比较器,创建对象时候传的
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
// 将元素的key值做比较
cmp = cpr.compare(key, t.key);
if (cmp < 0)
// <0 ,key比t.key小,放在左子树
t = t.left;
else if (cmp > 0)
t = t.right;
else
两个元素的key相等,替换value
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;
// 将元素的key值做比较
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);
}
//将key 和value封装成对象
Entry<K,V> e = new Entry<>(key, value, parent);
//没有等于0的情况,如果等于0 ,已经被替换过了
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
}
}
11. LinkedHashMap
底层在哈希表的基础上多维护了一个链表,所以可以按照输入顺序进行输出
特点:唯一(在key的角度),有序(按照输入顺序进行输出)
12. Hashtable
HashTable用起来和HashMap是一样的,当然两个也是有区别的。
区别:
HashTable是jdk1.0开始的,效率低,但是他是线程安全的,key不可以存空值。
HashMap 是jdk1.2开始的,效率高,是线程不安全的,这里key可以存空值,并且key的null值也遵循唯一的特点。
13. Vector
实际上已经很少用了,底层是Object数组,底层数组长度默认为10(当未指定长度时),底层扩容数组长度为2倍,是线程安全的

Vector和ArrayList的区别:
ArrayList底层扩容长度为原数组的1.5倍,Vector底层扩容长度为原数组的2倍。
ArrayList线程不安全,Vector是线程安全的(因为用了synchronized)

浙公网安备 33010602011771号