java基础之集合

1.简介

集合分三大接口:Collection、Map、Iterator,集合框架的接口和类在java.util包中
在这里插入图片描述

2.Collection

Collection主要用于存储单个对象,Collection的两大接口List和Set。

2.1 List

List接口: ArrayList、Vector、LinkedList

2.1.1 ArrayList

ArrayList的用法

// 可以储存多个不同类型的对象,我们可以限定List只能存储什么类型的元素
List<String> list = new ArrayList<String>();
list.add("array1");
list.add("array2");
list.add("array3");
list.add(null); // 可以存储null
list.add(null);

int size = list.size(); // 把size()拿出来存储变量保存在栈内要比在循环里面调用方法的效率高
for(int i = 0; i < size; i++) {
	System.out.print(list.get(i) + " "); // array1 array2 array3 null null
}

// 是否存在array2
System.out.println(list.contains("array2")); // true
// 移除对象
list.remove("array3");
size = list.size();
for(int i = 0; i < size; i++) {
	System.out.println(list.get(i) + " "); // array1 array2 null null
}

ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。这里的数组是一个Object数组,以便能够容纳任何类型的对象。
自动扩容:每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。
数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。

2.1.2 Vector

ArrayList不是线程安全的,而Vector则是线程安全的,因为它的方法用了synchronized来操作。Vector也是用动态数组实现的,默认容量为10,也是Object类型。
在这里插入图片描述
在这里插入图片描述
当其增量为零是扩充大小是原来的两倍,否则是一倍加增量。
在这里插入图片描述

2.1.3 LinkedList

和ArrayList、Vector不同,LinkedList采用双向链表的结构来实现。因为链表的缘故所以增加删除速度比较快,查找的话相比于数组就慢些。
在这里插入图片描述
在这里插入图片描述

2.2 Set

List是有序的而Set不是并且不支持存储重复元素。Set接口主要有TreeSet、HashSet、LinkedHashSet

2.2.1 TreeSet

基于**TreeMap(二叉树)**实现,支持有序性操作,例如根据一个范围查找元素的操作。
在这里插入图片描述

存储自定义对象需要实现Comparable接口(有序的基础),否则会报错。
在这里插入图片描述
实现Comparable接口的compareTo方法
在这里插入图片描述
在这里插入图片描述
但是结果只存了两个对象,因为person2和person3的年龄相同,认为是同一个对象。
在这里插入图片描述
所以:TreeSet可以实现两个功能:去掉重复元素排序

2.2.2 HashSet

基于哈希表(HashMap)实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
在这里插入图片描述
添加元素存储在HashMap的键。而value使用固定的对象。
在这里插入图片描述
在这里插入图片描述
HashSet保证元素唯一性的源码分析

首先进入HashSet的add方法,底层是调用了HashMap的put方法
在这里插入图片描述
点击进入put方法
在这里插入图片描述
再进入hash方法,可以看到该方法是计算key的hash值,然后再作为参数传递到putVal方法
在这里插入图片描述
点击进入putVal方法
在这里插入图片描述
第一个if判断哈希表是否为空,为空则初始化哈希表;如果不为空,则判断该哈希值在哈希表中对应数组位置是否为空,为空的话直接插入;如果不为空,则判断key值是否相同,相同的替换掉旧节点,不同则判断是否树化,树化就按树的方式进行存储,没有树化就按链表的方式进行存储,保证了元素的唯一性。
可以看出,HashSet的唯一性则是通过重写hashCode()方法和equal()方法实现的。所以如果自定义类要实现唯一性,必须重写Object公共父类的hashCode()方法和equal()方法
String重写了这些方法:
在这里插入图片描述
在这里插入图片描述

2.2.2 LinkedHashSet

具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
在这里插入图片描述

  1. LinkedHashSet的底层使用LinkedHashMap存储元素。
  2. LinkedHashSet是有序的,它是按照插入的顺序排序的。

3. Map

Map是键值对映射的数据结构,一个映射不能包含重复的键并且存取的顺序不能保证,允许存null键和null值。
在这里插入图片描述

3.1 HashMap

HashMap基于哈希表实现。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
基本用法:

Map<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");

System.out.println(map.get("key1")); // value1
System.out.println(map.get("key3")); // null

// 遍历1
Set<Map.Entry<String, String>> set = map.entrySet();
Iterator<Map.Entry<String, String>> iterator = set.iterator();
while (iterator.hasNext()) {
    Map.Entry<String, String> entry = iterator.next();
    String key = entry.getKey();
    String value = entry.getValue();
    System.out.println("key = " + key + "  value = " + value);
}

//遍历2
Iterator<String> iter = map.keySet().iterator();
while (iter.hasNext()) {
    String key = iter.next();
    String value = map.get(key);
    System.out.println("key = " + key + "  value = " + value);
}

HashMap存储结构:JDK1.7和 JDK1.8之后的 HashMap 存储结构。在JDK1.7及之前,是用数组加链表的方式存储的。当链表的长度特别长的时候,查询效率将直线下降,查询的时间复杂度为 O(n),因此,JDK1.8 把它设计为达到一个特定的阈值之后,就将链表转化为红黑树。
红黑树的特点:

  1. 每个节点只有两种颜色:红色或者黑色
  2. 根节点必须是黑色
  3. 每个叶子节点(NIL)都是黑色的空节点
  4. 从根节点到叶子节点,不能出现两个连续的红色节点
  5. 从任一节点出发,到它下边的子节点的路径包含的黑色节点数目都相同

HashMap 结构示意图:
在这里插入图片描述
常量:

//默认的初始化容量为16,必须是2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量为 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认的加载因子0.75,乘以数组容量得到的值,用来表示元素个数达到多少时,需要扩容。
//为什么设置 0.75 这个值呢,简单来说就是时间和空间的权衡。
//若小于0.75如0.5,则数组长度达到一半大小就需要扩容,空间使用率大大降低,
//若大于0.75如0.8,则会增大hash冲突的概率,影响查询效率。
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//刚才提到了当链表长度过长时,会有一个阈值,超过这个阈值8就会转化为红黑树
static final int TREEIFY_THRESHOLD = 8;

//当红黑树上的元素个数,减少到6个时,就退化为链表
static final int UNTREEIFY_THRESHOLD = 6;

//链表转化为红黑树,除了有阈值的限制,还有另外一个限制,需要数组容量至少达到64,才会树化。
//这是为了避免,数组扩容和树化阈值之间的冲突。
static final int MIN_TREEIFY_CAPACITY = 64;

//存放所有Node节点的数组
transient Node<K,V>[] table;

//存放所有的键值对
transient Set<Map.Entry<K,V>> entrySet;

//map中的实际键值对个数,即数组中元素个数
transient int size;

//每次结构改变时,都会自增,fail-fast机制,这是一种错误检测机制。
//当迭代集合的时候,如果结构发生改变,则会发生 fail-fast,抛出异常。
transient int modCount;

//数组扩容阈值
int threshold;

//加载因子
final float loadFactor;					

//普通单向链表节点类
static class Node<K,V> implements Map.Entry<K,V> {
	//key的hash值,put和get的时候都需要用到它来确定元素在数组中的位置
	final int hash;
	final K key;
	V value;
	//指向单链表的下一个节点
	Node<K,V> next;

	Node(int hash, K key, V value, Node<K,V> next) {
		this.hash = hash;
		this.key = key;
		this.value = value;
		this.next = next;
	}
}

//转化为红黑树的节点类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
	//当前节点的父节点
	TreeNode<K,V> parent;  
	//左孩子节点
	TreeNode<K,V> left;
	//右孩子节点
	TreeNode<K,V> right;
	//指向前一个节点
	TreeNode<K,V> prev;    // needed to unlink next upon deletion
	//当前节点是红色或者黑色的标识
	boolean red;
	TreeNode(int hash, K key, V val, Node<K,V> next) {
		super(hash, key, val, next);
	}
}	

构造函数:

//默认无参构造,指定一个默认的加载因子
public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

//可指定容量的有参构造,但是需要注意当前我们指定的容量并不一定就是实际的容量,下面会说
public HashMap(int initialCapacity) {
	//同样使用默认加载因子
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//可指定容量和加载因子,但是笔者不建议自己手动指定非0.75的加载因子
public HashMap(int initialCapacity, float loadFactor) {
	if (initialCapacity < 0)
		throw new IllegalArgumentException("Illegal initial capacity: " +
										   initialCapacity);
	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity = MAXIMUM_CAPACITY;
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
		throw new IllegalArgumentException("Illegal load factor: " +
										   loadFactor);
	this.loadFactor = loadFactor;
	//这里就是把我们指定的容量改为一个大于它的的最小的2次幂值,如传过来的容量是14,则返回16
	//注意这里,按理说返回的值应该赋值给 capacity,即保证数组容量总是2的n次幂,为什么这里赋值给了 threshold 呢?
	//先卖个关子,等到 resize 的时候再说
	this.threshold = tableSizeFor(initialCapacity);
}

//可传入一个已有的map
public HashMap(Map<? extends K, ? extends V> m) {
	this.loadFactor = DEFAULT_LOAD_FACTOR;
	putMapEntries(m, false);
}

//把传入的map里边的元素都加载到当前map
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
	int s = m.size();
	if (s > 0) {
		if (table == null) { // pre-size
			float ft = ((float)s / loadFactor) + 1.0F;
			int t = ((ft < (float)MAXIMUM_CAPACITY) ?
					 (int)ft : MAXIMUM_CAPACITY);
			if (t > threshold)
				threshold = tableSizeFor(t);
		}
		else if (s > threshold)
			resize();
		for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
			K key = e.getKey();
			V value = e.getValue();
			//put方法的具体实现,后边讲
			putVal(hash(key), key, value, false, evict);
		}
	}
}

resize() 扩容机制
当HashMap中的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中的元素个数超过16×0.75=12(这个值就是阈值或者边界值threshold值)的时候,就把数组的大小扩展为2×16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预知元素的个数能够有效的提高HashMap的性能。

3.2 LinkedHashMap

LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的。HashMap是无序的,当我们希望有顺序地去存储key-value时,就需要使用LinkedHashMap了。TreeSet就是用到了LinkedHashMap而HashSet用到了HashMap。

3.2 TreeMap

TreeMap中默认的排序为升序,如果要改变其排序可以自己写一个Comparator。
下面代码按照key降序。
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值