java集合框架
集合可以看作是一种容器,用来存储对象信息。所有集合类都位于java.util包下,但支持多线程的集合类位于java.util.concurrent包下。
![img](https://img-blog.csdnimg.cn/20190426170026120.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzU3MzgyNA==,size_16,color_FFFFFF,t_70)
Collection接口
- 所有已知实现常用类:
- List接口
- ArrayList
- LinkedList
- Vextor
- Set接口
- HashSet
- TreeSet
- Queue接口
- ArrayBlockingQueue
- LinkedBlockingQueue
- List接口
常见方法
返回值 | 方法 |
---|---|
boolean | add(E e) 确保此集合包含指定的元素(可选操作)。 |
boolean | addAll(Collection<? extends E> c) 将指定集合中的所有元素添加到此集合(可选操作)。 |
void | clear() 从此集合中删除所有元素(可选操作)。 |
boolean | contains(Object o) 如果此集合包含指定的元素,则返回 true 。 |
boolean | containsAll(Collection<?> c) 如果此集合包含指定 集合 中的所有元素,则返回true。 |
boolean | equals(Object o) 将指定的对象与此集合进行比较以获得相等性。 |
int | hashCode() 返回此集合的哈希码值。 |
boolean | isEmpty() 如果此集合不包含元素,则返回 true 。 |
Iterator<E> | iterator() 返回此集合中的元素的迭代器。 |
default Stream<E> | parallelStream() 返回可能并行的 Stream 与此集合作为其来源。 |
boolean | remove(Object o) 从该集合中删除指定元素的单个实例(如果存在)(可选操作)。 |
boolean | removeAll(Collection<?> c) 删除指定集合中包含的所有此集合的元素(可选操作)。 |
default boolean | removeIf(Predicate<? super E> filter) 删除满足给定谓词的此集合的所有元素。 |
boolean | retainAll(Collection<?> c) 仅保留此集合中包含在指定集合中的元素(可选操作)。 |
int | size() 返回此集合中的元素数。 |
default Spliterator<E> | spliterator() 创建一个Spliterator 在这个集合中的元素。 |
default Stream<E> | stream() 返回以此集合作为源的顺序 Stream 。 |
Object[] | toArray() 返回一个包含此集合中所有元素的数组。 |
<T> T[] | toArray(T[] a) 返回包含此集合中所有元素的数组; 返回的数组的运行时类型是指定数组的运行时类型。 |
方法详细信息及其使用
public class CollectionDemo01 {
public static void main(String[] args) {
List list = new ArrayList();
//添加字符串
list.add("字符串1");
list.add("字符串2");
list.add("字符串3");
System.out.println(list);
List list1 = new ArrayList();
list1.add("字符串4");
list1.add("字符串5");
list1.add("字符串6");
list.addAll(list1); //将addAll将集合list1添加到list集合中
System.out.println(list);
System.out.println(list.size());
//返回list集合的大小 6
System.out.println(list.contains("字符串2"));
//集合list是否包含"字符串2" 返回true
System.out.println(list.containsAll(list1));
//集合list是否包含list1中的元素, 返回true
System.out.println(list.remove("字符串6"));
//移除指定的元素"字符串6" 返回true
System.out.println(list.get(1));
//获取指定下标的元素
System.out.println(list.set(2,"字符串7"));
//将下标为2的元素替换为"字符串7"元素
list.clear(); // 清空list集合中所有元素
System.out.println(list);
// list.add(new Pet("宠物猫")); 报错,实际参数列表和形式参数列表长度不同,同一个ArrayList只能存贮同一种对象
List list2 = new ArrayList();
list2.add(new Pet("宠物猫",15));
list2.add(new Pet("宠物狗",10));
list2.add(new Pet("仓鼠",5));
System.out.println(list2);
list2.hashCode(); //获取list2的哈希值
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Pet{
String name;
int age;
}
迭代器Iterator
public class CollectionDemo02 {
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Book("西游记",50));
list.add(new Book("水浒传",60));
list.add(new Book("红楼梦",64));
Iterator iterator = list.iterator();
//获取迭代器,快捷代码itit
while(iterator.hasNext()){
// iterator.hasNext() 判断是否存在迭代元素
// iterator.next() 获取迭代元素,指针下移
System.out.println(iterator.next());
}
iterator.next();
//报错:Exception in thread "main" java.util.NoSuchElementException
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
String name;
int price;
}
注意:想再次迭代操作,需要重新定义迭代器,因为上一次的迭代指针在最低端,不刷新迭代器则会报NoSuchElementException异常
for循环与增强for循环遍历
public class CollectionDemo03 {
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Book01("西游记",50));
list.add(new Book01("水浒传",60));
list.add(new Book01("红楼梦",64));
//增强for循环,底层实现也是iterator迭代器,可用debug跟踪检测
for (Object o : list) {
System.out.println(o);
}
//普通for循环实现遍历
for (int i = 0; i < list.size()-1; i++) {
System.out.println(list.get(i));
}
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Book01{
String name;
int price;
}
手写集合的冒泡排序
public class CollectionDemo04 {
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Book02("西游记",80));
list.add(new Book02("水浒传",60));
list.add(new Book02("红楼梦",64));
sort(list);
for (Object o : list) {
System.out.println(o);
}
}
public static void sort(List list){
for (int i = 0; i <= list.size()-1; i++) {
for (int j = 0; j <= list.size()-1-1; j++) {
Book02 book1 = (Book02) (list.get(j));
Book02 book2 = (Book02) (list.get(j+1));
if(book1.getPrice() > book2.getPrice()){
//大于 使用set更新覆盖
list.set(j,book2);
list.set(j+1,book1);
}
}
}
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Book02{
String name;
int price;
}
List接口
ArrayList
ArrayList介绍
- 可调整大小的数组的实现
List
接口。 实现所有可选列表操作,并允许所有元素,包括null 。 除了实现List接口
之外,该类还提供了一些方法来操纵内部使用的存储列表的数组的大小。 (这个类是大致相当于Vector,
不同之处在于它是不同步的)。 - 该
size,isEmpty,get,set,iterator
和listIterator
操作在固定时间内运行。add
操作以摊余常数运行 ,即添加n个元素需要O(n)个时间。 所有其他操作都以线性时间运行(粗略地说)。 与LinkedList
实施相比,常数因子较低。 - 每个
ArrayList
实例都有一个容量 。 容量是用于存储列表中的元素的数组的大小。 它总是至少与列表大小一样大。 当元素添加到ArrayList时,其容量会自动增长。 没有规定增长政策的细节,除了添加元素具有不变的摊销时间成本。 - 应用程序可以添加大量使用
ensureCapacity
操作元件的前增大ArrayList
实例的容量。 这可能会减少增量重新分配的数量。
请注意,此实现不同步 。如果多个线程同时访问884457282749
实例,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素的任何操作,或明确调整后台数组的大小;仅设置元素的值不是结构修改。)这通常是通过在一些自然地封装了列表。 如果没有这样的对象存在,列表应该使用Collections.synchronizedList方法“包装”。 这最好在创建时完成,以防止意外的不同步访问列表:
List list = Collections.synchronizedList(new ArrayList(...));
构造方法
-
ArrayList()
构造一个初始容量为十的空列表。 -
ArrayList(Collection<? extends E> c)
构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。 -
ArrayList(int initialCapacity)
构造具有指定初始容量的空列表。
扩容机制
ArrayList中维护了一个Object类型的数组elementData
transient Object[] elementData; //transient表示该属性不会被序列化
-
初始化ArrayList实例时,没有指定大小,默认为0,但添加数据时,会进行第一次扩容,扩容大小为10,但添加数据量大于10时,进行第二次扩容,扩容大小是原来的1.5倍,即为15,后面的扩容也遵循1.5的扩容机制
-
初始化ArrayList实例并指定大小时,例如new ArrayList(8) ,添加的数据量大于8时进行扩容,大小是原来的1.5倍,即12
源码分析
ArrayList.java中扩容机制涉及源码如下
//注:DEFAULT_CAPACITY常量为10 size=0 elementData为一个Object数组
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
//定义一些变量,先不用理解
//没有指定Arraylist大小
public ArrayList() {
// 先创建一个空的elementData数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//指定ArrayList大小
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果指定了数值大小大于0,直接创建一个该指定大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果指定的数值大小为0,则创建一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
//如果传入的参数不合法(传入负数或则不是数值类型),报IllegalArgumentException不合法参数异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//添加元素操作
public boolean add(E e) {
//添加一个泛型e(即任意类型的变量)
//ensureCapacityInternal: 保证容量充足,是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//将元素添加到elementData数组中
elementData[size++] = e;
//添加成功返回true
return true;
}
public void trimToSize() {
modCount++; //计数器,统计添加元素的次数
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
//
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//如果elementData不为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,将minExpand设置为0
//也就是当不走无参构造时,将minExpand设置为0,即不需要第一次扩容为10的这个操作 minExpan:最小扩容
//否则,将minExpand设置为10 ,也就是无参构造 第一次扩容,大小为10 DEFAULT_CAPACITY=10
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
//如果最小的容量小于最小扩容,也就是指定的大小小于此时的minExpand 执行ensureExplicitCapacity方法
ensureExplicitCapacity(minCapacity);
}
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//如果elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,相当于走有参构造,即已经指定大小
//则将指定的大小和10比较大小,选出最大值作为新的minCapacity
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//执行ensureExplicitCapacity并传入新的minCapacity
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; //计数,记录添加元素的个数
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//如果minCapacity大于elementData数组的长度,即执行grow方法,实现正真扩容
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//正真实现扩容
private void grow(int minCapacity) {
// overflow-conscious code
//Capacity :容量
int oldCapacity = elementData.length;
//将elementData的长度赋值oldCapacity
int newCapacity = oldCapacity + (oldCapacity >> 1);
//新的容量等于原来容量的1.5倍 oldCapacity+oldCapacity/2 右移一位相当于除2
if (newCapacity - minCapacity < 0)
//如果新的容量小于最小容量,执行将最小容量赋值给新的容量
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
//如果新的容量大于最大的数组大小,执行hugeCapacity(minCapacity);
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
//将elementData扩容newCapacity大小
/*
Arrays.copyOf(T[] original, newLength);
Arrays中的copyOf具有扩充数组的功能,其中original为待扩充的原始数组,newLength为需要扩充的容量的大小,方法不会直接在原数组中直接修改,而是返回新的一个数组,所以copyOf具有返回值。
*/
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
Vector
Vector
类实现了可扩展的对象数组,可变数组。像数组一样,它包含可以使用整数索引访问的组件。但是Vector的大小可以根据需要增长或缩小(扩容机制),以适应在创建Vector
之后添加和删除项目。
Vector与ArrayList比较
底层结构 | 版本 | 线程安全(同步)效率 | 扩容机制 | |
---|---|---|---|---|
ArrayList | 可变数组 | jdk1.2 | 不安全 效率高 | 有参构造每次1.5倍扩容 无参:默认为0 1.第一次10 2.第二次开始每次1.5倍扩容 |
Vector | 可变数组 | jdk1.0 | 安全(同步)效率不高 | 有参:每次2倍扩容 无参:直接默认10 满后每次按2倍扩容 |
常用操作
boolean | add(E e) 将指定的元素追加到此Vector的末尾。 |
---|---|
void | add(int index, E element) 在此Vector中的指定位置插入指定的元素。 |
boolean | addAll(Collection<? extends E> c) 将指定集合中的所有元素追加到该向量的末尾,按照它们由指定集合的迭代器返回的顺序。 |
boolean | addAll(int index, Collection<? extends E> c) 将指定集合中的所有元素插入到此向量中的指定位置。 |
void | addElement(E obj) 将指定的组件添加到此向量的末尾,将其大小增加1。 |
int | capacity() 返回此向量的当前容量。 |
void | clear() 从此Vector中删除所有元素。 |
Object | clone() 返回此向量的克隆。 |
boolean | contains(Object o) 如果此向量包含指定的元素,则返回 true 。 |
boolean | containsAll(Collection<?> c) 如果此向量包含指定集合中的所有元素,则返回true。 |
void | copyInto(Object[] anArray) 将此向量的组件复制到指定的数组中。 |
E | elementAt(int index) 返回指定索引处的组件。 |
Enumeration<E> | elements() 返回此向量的组件的枚举。 |
void | ensureCapacity(int minCapacity) 如果需要,增加此向量的容量,以确保它可以至少保存最小容量参数指定的组件数。 |
boolean | equals(Object o) 将指定的对象与此向量进行比较以获得相等性。 |
E | firstElement() 返回此向量的第一个组件(索引号为 0 的项目)。 |
void | forEach(Consumer<? super E> action) 对 Iterable 的每个元素执行给定的操作,直到所有元素都被处理或动作引发异常。 |
E | get(int index) 返回此向量中指定位置的元素。 |
int | hashCode() 返回此Vector的哈希码值。 |
int | indexOf(Object o) 返回此向量中指定元素的第一次出现的索引,如果此向量不包含元素,则返回-1。 |
int | indexOf(Object o, int index) 返回此向量中指定元素的第一次出现的索引,从 index 向前 index ,如果未找到该元素,则返回-1。 |
void | insertElementAt(E obj, int index) 在指定的index插入指定对象作为该向量中的一个 index 。 |
boolean | isEmpty() 测试此矢量是否没有组件。 |
Iterator<E> | iterator() 以正确的顺序返回该列表中的元素的迭代器。 |
E | lastElement() 返回向量的最后一个组件。 |
int | lastIndexOf(Object o) 返回此向量中指定元素的最后一次出现的索引,如果此向量不包含元素,则返回-1。 |
int | lastIndexOf(Object o, int index) 返回此向量中指定元素的最后一次出现的索引,从 index ,如果未找到元素,则返回-1。 |
ListIterator<E> | listIterator() 返回列表中的列表迭代器(按适当的顺序)。 |
ListIterator<E> | listIterator(int index) 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。 |
E | remove(int index) 删除此向量中指定位置的元素。 |
boolean | remove(Object o) 删除此向量中指定元素的第一个出现如果Vector不包含元素,则它不会更改。 |
boolean | removeAll(Collection<?> c) 从此Vector中删除指定集合中包含的所有元素。 |
void | removeAllElements() 从该向量中删除所有组件,并将其大小设置为零。 |
boolean | removeElement(Object obj) 从此向量中删除参数的第一个(最低索引)出现次数。 |
void | removeElementAt(int index) 删除指定索引处的组件。 |
boolean | removeIf(Predicate<? super E> filter) 删除满足给定谓词的此集合的所有元素。 |
protected void | removeRange(int fromIndex, int toIndex) 从此列表中删除所有索引为 fromIndex (含)和 toIndex 之间的元素。 |
void | replaceAll(UnaryOperator<E> operator) 将该列表的每个元素替换为将该运算符应用于该元素的结果。 |
boolean | retainAll(Collection<?> c) 仅保留此向量中包含在指定集合中的元素。 |
E | set(int index, E element) 用指定的元素替换此Vector中指定位置的元素。 |
void | setElementAt(E obj, int index) 设置在指定的组件 index 此向量的要指定的对象。 |
void | setSize(int newSize) 设置此向量的大小。 |
int | size() 返回此向量中的组件数。 |
void | sort(Comparator<? super E> c) 使用提供的 Comparator 对此列表进行排序以比较元素。 |
Spliterator<E> | spliterator() 在此列表中的元素上创建*late-binding和故障切换* Spliterator 。 |
List<E> | subList(int fromIndex, int toIndex) 返回此列表之间的fromIndex(包括)和toIndex之间的独占视图。 |
Object[] | toArray() 以正确的顺序返回一个包含此Vector中所有元素的数组。 |
<T> T[] | toArray(T[] a) 以正确的顺序返回一个包含此Vector中所有元素的数组; 返回的数组的运行时类型是指定数组的运行时类型。 |
String | toString() 返回此Vector的字符串表示形式,其中包含每个元素的String表示形式。 |
void | trimToSize() 修改该向量的容量成为向量的当前大小。 |
注意:addElement(E obj) 将指定的组件添加到此向量的末尾,将其大小增加1。该方法是Vector新增的方法
Vector扩容机制核心源码
思想与ArrayList差不多,这里指出重要部分
//无参直接默认10的大小
public Vector() {
this(10);
}
//2倍扩容机制
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
//capacityIncrement大于0,相当于int newCapacity = oldCapacity + oldCapacity
一般在业务中常常选择ArrayList,因为其效率高,Vector因为每一个方法都有synchronized修饰,每次都需要校验锁,效率低
LinkedList
双链表实现了List和Deque接口。实现所有可选列表操作,并允许所有元素(包括null
)。
所有的操作都能像双向列表一样预期。 索引到列表中的操作将从开始或结束遍历列表,以更接近指定的索引为准。
请注意,此实现不同步。 如果多个线程同时访问链接列表,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素的任何操作;仅设置元素的值不是结构修改。)这通常通过在自然封装列表的对象上进行同步来实现。 如果没有这样的对象存在,列表应该使用Collections.synchronizedList 方法“包装”。 这最好在创建时完成,以防止意外的不同步访问列表:
List list = Collections.synchronizedList(new LinkedList(...));
这个类的iterator
和listIterator
方法返回的迭代器是故障快速的 :如果列表在迭代器创建之后的任何时间被结构化地修改,除了通过迭代器自己的remove
或add
方法之外,迭代器将会抛出一个ConcurrentModificationException 。 因此,面对并发修改,迭代器将快速而干净地失败,而不是在未来未确定的时间冒着任意的非确定性行为。
请注意,迭代器的故障快速行为无法保证,因为一般来说,在不同步并发修改的情况下,无法做出任何硬性保证。 失败快速迭代器尽力投入ConcurrentModificationException
。 因此,编写依赖于此异常的程序的正确性将是错误的:迭代器的故障快速行为应仅用于检测错误。
源码分析
效果体验:
LinkedList linkedList = new LinkedList();
linkedList.add("数据1");
linkedList.add("数据2");
System.out.println(linkedList); //[数据1, 数据2] 尾部插入数据2
linkedList.addFirst("数据3");
System.out.println(linkedList); //[数据3, 数据1, 数据2] 尾部插入数据3
linkedList.remove();
System.out.println(linkedList); //[数据1, 数据2] 头部移除数据3
transient Node<E> last;
// 三种插入元素方式
//addFirst调用头插法
public void addFirst(E e) {
linkFirst(e);
}
//addLast调用尾插法addLast
public void addLast(E e) {
linkLast(e);
}
//add调用尾插方法linkLast
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as first element.
*/
//该方法实现头插法
private void linkFirst(E e) {
//当添加第一个元素时,fist为空=>f为空,即Node<>(null, e, null) 将first,last都指向这个本身节点 即只有这个节点
//当添加第二个元素时,first不为空,指向的是上一个节点,此时该节点为 Node<>(null, e, f) 将first指向新添加的元素节点,last指向本身
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
//判断是否只有一个元素,成立则将last指向本身
last = newNode;
else
//当元素不只有一个元素时,成立则f的前驱指向新添加的元素节点
f.prev = newNode;
size++;
modCount++;
}
/**
* Links e as last element.
*/
//该方法实现尾插法
void linkLast(E e) {
//当添加第一个元素时,last为空=>l为空,即Node<>(null, e, null) 将first,last都指向这个本身节点 即只有这个节点
//当添加第二个元素时,last不为空,l等于last,新增的元素Node<>(l, e, null),将last指向新添加的节点,first指向本身
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
//判断是否只有一个元素,成立则将first指向新添加元素节点的本身
first = newNode;
else
//当元素不只有一个元素时,成立则将原节点的前驱指向新添加的元素
l.next = newNode;
size++;
modCount++;
}
/**
* Inserts element e before non-null Node succ.
*/
//插入指定节点前的元素
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//原理: 将指定节点的前驱赋值给新添加的节点的前驱 ,将指定节点的前驱指向当前新添加的节点 ,完成添加操作
//指定节点的前驱赋值给pred ,新添加的元素为 Node<>(pred, e, succ)
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
//将指定节点的前驱指向新添加的节点元素
succ.prev = newNode;
if (pred == null)
//pred=null,即添加的节点位置位于最头节点
first = newNode;
else
//新添加的节点不为头节点时,将前一个节点的next指向新添加的节点元素
pred.next = newNode;
size++;
modCount++;
}
/**
* Unlinks non-null first node f.
*/
//移除头部元素
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
//原理:将指向给节点的前后节点
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
/**
* Unlinks non-null last node l.
*/
//移除尾部元素
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
/**
* Unlinks non-null node x.
*/
//移除指定元素
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
LinkList踩坑
1.remove()方法踩坑
remove(index)移除指定下标元素
使用for循环移除元素时
Java List在进行remove()方法是通常容易踩坑,主要有一下几点
循环时:问题在于,删除某个元素后,因为删除元素后,后面的元素都往前移动了一位,而你的索引+1,所以实际访问的元素相对于删除的元素中间间隔了一位。
几种常见方法
使用for循环不进行额外处理时(错误)
for(int i=0;i<list.size();i++) {
if(list.get(i)%2==0) {
list.remove(i);
}
}
2.使用foreach循环(错误)
for(Integer i:list) {
if(i%2==0) {
list.remove(i);
}
}
抛出异常:java.util.ConcurrentModificationException 该异常是基于java集合中的快速失败(fail-fast)机制产生的,在使用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了增删改,就会抛出该异常。
foreach的本质是使用迭代器实现,每次进入for (Integer i:list) 时,会调用ListItr.next()方法;
继而调用checkForComodification()方法, checkForComodification()方法对操作集合的次数进行了判断,如果当前对集合的操作次数与生成迭代器时不同,抛出异常
public E next() {
checkForComodification();
if (!hasNext()) {
throw new NoSuchElementException();
}
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
// checkForComodification()方法对集合遍历前被修改的次数与现在被修改的次数做出对比
final void checkForComodification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
使用for循环,并且同时改变索引;(正确)
//正确
for(int i=0;i<list.size();i++) {
if(list.get(i)%2==0) {
list.remove(i);
i--;//在元素被移除掉后,进行索引后移
}
}
使用for循环,倒序进行;(正确)
//正确
for(int i=list.size()-1;i>=0;i--) {
if(list.get(i)%2==0) {
list.remove(i);
}
}
使用while循环,删除了元素,索引便不+1,在没删除元素时索引+1(正确)
//正确
int i=0;
while(i<list.size()) {
if(list.get(i)%2==0) {
list.remove(i);
}else {
i++;
}
}
4.使用迭代器方法(正确,推荐)
只能使用迭代器的remove()方法,使用列表的remove()方法是错误的
//正确,并且推荐的方法
Iterator<Integer> itr = list.iterator();
while(itr.hasNext()) {
if(itr.next()%2 ==0)
itr.remove();
}
性能分析
下面来谈谈当数据量过大时候,需要删除的元素较多时,如何用迭代器进行性能的优化,对于ArrayList这几乎是致命的,从一个ArrayList中删除批量元素都是昂贵的时间复杂度为O(n²),那么接下来看看LinkeedList是否可行。LinkedList暴露了两个问题,一个:是每次的Get请求效率不高,而且,对于remove的调用同样低效,因为达到位置I的代价是昂贵的。
是每次的Get请求效率不高
需要先get元素,然后过滤元素。比较元素是否满足删除条件。
remove的调用同样低效
LinkedList的remove(index),方法是需要先遍历链表,先找到该index下的节点,再处理节点的前驱后继。
以上两个问题当遇到批量级别需要处理时时间复杂度直接上升到O(n²)
使用迭代器的方法删除元素
对于LinkedList,对该迭代器的remove()方法的调用只花费常数时间,因为在循环时该迭代器位于需要被删除的节点,因此是常数操作。对于一个ArrayList,即使该迭代器位于需要被删除的节点,其remove()方法依然是昂贵的,因为数组项必须移动。下面贴出示例代码以及运行结果
public class RemoveByIterator {
public static void main(String[] args) {
List<Integer> arrList1 = new ArrayList<>();
for(int i=0;i<100000;i++) {
arrList1.add(i);
}
List<Integer> linList1 = new LinkedList<>();
for(int i=0;i<100000;i++) {
linList1.add(i);
}
List<Integer> arrList2 = new ArrayList<>();
for(int i=0;i<100000;i++) {
arrList2.add(i);
}
List<Integer> linList2 = new LinkedList<>();
for(int i=0;i<100000;i++) {
linList2.add(i);
}
removeEvens(arrList1,"ArrayList");
removeEvens(linList1,"LinkedList");
removeEvensByIterator(arrList2,"ArrayList");
removeEvensByIterator(linList2,"LinkedList");
}
public static void removeEvensByIterator(List<Integer> lst ,String name) {//利用迭代器remove偶数
long sTime = new Date().getTime();
Iterator<Integer> itr = lst.iterator();
while(itr.hasNext()) {
if(itr.next()%2 ==0)
itr.remove();
}
System.out.println(name+"使用迭代器时间:"+(new Date().getTime()-sTime)+"毫秒");
}
public static void removeEvens(List<Integer> list , String name) {//不使用迭代器remove偶数
long sTime = new Date().getTime();
int i=0;
while(i<list.size()) {
if(list.get(i)%2==0) {
list.remove(i);
}else {
i++;
}
}
System.out.println(name+"不使用迭代器的时间"+(new Date().getTime()-sTime)+"毫秒");
}
}
/*
*运行结果
ArrayList不使用迭代器的时间538毫秒
LinkedList不使用迭代器的时间4696毫秒
ArrayList使用迭代器时间:486毫秒
LinkedList使用迭代器时间:8毫秒
*/
原理 重点看一下LinkedList的迭代器
另一篇博客 Iterator简介 LinkedList使用迭代器优化移除批量元素原理
调用方法:list.iterator();
重点看下remove方法
private class ListItr implements ListIterator<E> {
//返回的节点
private Node<E> lastReturned;
//下一个节点
private Node<E> next;
//下一个节点索引
private int nextIndex;
//修改次数
private int expectedModCount = modCount;
ListItr(int index) {
//根据传进来的数字设置next等属性,默认传0
next = (index == size) ? null : node(index);
nextIndex = index;
}
//直接调用节点的后继指针
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
//返回节点的前驱
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
/**
* 最重要的方法,在LinkedList中按一定规则移除大量元素时用这个方法
* 为什么会比list.remove效率高呢;
*/
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
}
LinkedList 源码的remove(int index)的过程是
先逐一移动指针,再找到要移除的Node,最后再修改这个Node前驱后继等移除Node。如果有批量元素要按规则移除的话这么做时间复杂度O(n²)。但是使用迭代器是O(n)。
先看看list.remove(idnex)是怎么处理的
LinkedList是双向链表,这里示意图简单画个单链表
比如要移除链表中偶数元素,先循环调用get方法,指针逐渐后移获得元素,比如获得index = 1;指针后移两次才能获得元素。
当发现元素值为偶数是。使用idnex移除元素,如list.remove(1);链表先Node node(int index)返回该index下的元素,与get方法一样。然后再做前驱后继的修改。所以在remove之前相当于做了两次get请求。导致时间复杂度是O(n)。
继续移除下一个元素需要重新再走一遍链表(步骤忽略当index大于半数,链表倒序查找)
以上如果移除偶数指针做了6次移动。
删除2节点
get请求移动1次,remove(1)移动1次。
删除4节点
get请求移动2次,remove(2)移动2次。
迭代器的处理
迭代器的next指针执行一次一直向后移动的操作。一共只需要移动4次。当元素越多时这个差距会越明显。整体上移除批量元素是O(n),而使用list.remove(index)移除批量元素是O(n²)
Set集合
不包含重复元素的集合。更正式地,集合不包含一对元素e1
和e2
,使得e1.equals(e2)
,并且最多一个空元素。正如其名称所暗示的那样,这个接口模拟了数学集抽象。
Set
接口除了继承自Collection
接口的所有构造函数以及add,equals
和hashCode
方法的外 ,还增加了其他规定。 其他继承方法的声明也包括在这里以方便。 (伴随这些声明的规范已经量身定做Set
接口,但它们不包含任何附加的规定。)
构造函数的额外规定并不奇怪,所有构造函数都必须创建一个不包含重复元素的集合(如上所定义)。
注意:如果可变对象用作设置元素,则必须非常小心。 如果对象的值以影响equals
比较的方式更改,而对象是集合中的元素, 则不
指定集合的行为。 这种禁止的一个特殊情况是,一个集合不允许将其本身作为一个元素。
一些集合实现对它们可能包含的元素有限制。 例如,一些实现禁止空元素,有些实现对元素的类型有限制。 尝试添加不合格元素会引发未经检查的异常,通常为NullPointerException
或ClassCastException
。 尝试查询不合格元素的存在可能会引发异常,或者可能只是返回false; 一些实现将展现出前者的行为,一些实现将展现出后者。 更一般来说,尝试对不符合条件的元素的操作,其完成不会导致不合格元素插入到集合中,可能会导致异常,或者可能会成功执行该选项。 此异常在此接口的规范中标记为“可选”。
HashSet
底层实际上是一个HashMap,元素不重复且无序,只能存在一个null
public HashSet() {
map = new HashMap<>();
}
- 可以存放null 但是只能存放一个null
- HashSet不保证元素是有序,取决于索引的结果
- 不能有重复元素/对象(这里指的是同一个对象
public class HashSeto1 {
public static void main(String[] args){
HashSet hashSet = new HashSet();
System.out.println(hashSet.add("数据1")); //ture
System.out.println(hashSet.add("数据2")); //ture
System.out.println(hashSet.add("数据2")); //false
System.out.println(hashSet.add("数据3")); //true
System.out.println("=======================");
People people = new People("李四");
System.out.println(hashSet.add(people)); //ture
System.out.println(hashSet.add(people)); //false
System.out.println("=======================");
//System.out.println(new People("张三") == new People("张三"));
System.out.println(hashSet.add(new People("张三"))); //ture
System.out.println(hashSet.add(new People("张三"))); //ture
System.out.println("=======================");
System.out.println(hashSet.add(new String("hashset"))); //ture
System.out.println(hashSet.add(new String("hashset"))); //false
System.out.println(hashSet);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class People{
String name;
}
发现一个有趣现象:
System.out.println(hashSet.add(new People(“张三”)));
System.out.println(hashSet.add(new People(“张三”)));
People对象使用lombok插件重写toString和自己手动重写toString结果不一致
手动重写toString结果:true true
lombok重写toString结果:true false
解释,因为lomok重写了hashcode方法和equals方法,而HashSet是根据这两个方法判断是否相同
源码如下:
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
因此,当我们需要实现参数一样的两个对象只能添加一个这种情况时,重写hashcode和equals方法即可!
分析HashSet
HashSet底层是一个HashMap,而HashMap底层是一个数组+链表+红黑树 (存储效率高)
简单理解 数组+链表+红黑树 :数组中每一个位置都可以存放一条链表,链表长度大于等于8时,链表树化成红黑树,小于6时退化为链表
- HashSet底层是 HashMap
- 添加一个元素时,先得到hash值-会转成->索引值
- 找到存储数据表table,看这个索引位置是否已经存放的有元素
- 如果没有。直接加入
- 如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后
- 在Java8中,如果一条链表的元素个数达到TREEIFY_THRESHOLD(默认是8),并且table的大小>=
MIN_TREEIFY_CAPACITY(默认64)(如果链表长度到达了8,但table没有到达64,则table会进行扩容),就会进行树化(红黑树)
简单模拟数组+链表
public class HashSet02 {
public static void main(String[] args) {
Node[] table = new Node[16];
Node node1 = new Node("数据1",null);
table[1] = node1;
Node node2 = new Node("数据2",null);
node1.next = node2;
Node node3 = new Node("数据3",null);
node2.next = node3;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Node{
Object item; //该变量存储数据
Node next; //指向下一个节点
}
打断点观察数据的变化!
注意点:在源码中,死循环一般不用while(true),而是用的是for(循环开始条件; ;循环结束条件)实现,原因for编译出来的指令比while少,相对来说不会占用寄存器空间
聊一聊for循环执行流程
for(循环开始条件;循环判定条件;循环结束条件){
语句体
}
//1.当不存在循环判定条件,为for死循环
//2.执行流程为 循环开始条件--》循环判定条件--》语句体--》循环结束条件
//3.for死循环可以用break跳出
底层实现添加元素 的基本步骤
- 先获取元素的哈希值(hashCode方法)
- 对哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号
- 如果该位置上没有其他元素,则直接存放
- 如果该位置上已经有其他元素,则需要进行equals判断,如果相等,则不再添加,如果不相等,则以链表的方式添加
LinkedHashSet
- LinkedHashSet是HashSet的一个子类,继承了HashSet实现了Set接口
- LinkedHashSet底层是一个LinkedHashMap(是HashMap的子类),底层维护了一个 数组+双向链表(HashSet是单链表)
- LinkedHashSet根据元素的hashCode值来确定元素的存储位置,同时使用链表维护的次序(图),这使得元素看起来是以插入顺序保存的,是有序的
- LinkedHashSet不允许添加重复元素
说明
-
在LinkedHashSet中维护了一个hash表和双向链表(LinkedHashSet有head和tail)
-
每一个节点有pre和next属性,这样可以形成双向链表
-
在添加一个元素时,先求hash值,在求索引,确定该元素在hashtable的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样)
tail.next = newElement ; newElement.pre = tail; tail = newElement;
-
这样,我们遍历LinkedHashSet也能确定插入顺序一致
package org.example;
import java.util.HashSet;
import java.util.LinkedHashSet;
public class LinkedHashSet01 {
public static void main(String[] args) {
//HashSet和LinkedHashSet对比
HashSet hashSet = new HashSet();
hashSet.add("数据1");
hashSet.add("数据2");
hashSet.add("数据3");
hashSet.add("数据4");
hashSet.add("数据5");
hashSet.add("数据6");
System.out.println(hashSet);
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add("数据1");
linkedHashSet.add("数据2");
linkedHashSet.add("数据3");
linkedHashSet.add("数据4");
linkedHashSet.add("数据5");
linkedHashSet.add("数据6");
System.out.println(linkedHashSet);
}
}
//运行结果
//[数据6, 数据1, 数据2, 数据3, 数据4, 数据5]
//[数据1, 数据2, 数据3, 数据4, 数据5, 数据6]
//由此可见,HashSet是无序的,LinkedHashSet是有序的,即所有节点都由双链表连接
相比于hashset的优点是使得查询效率变高了,单链表只能找位于同一数组下标下的,不能找其他数组下标的
源码分析
//先从构造器入手
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
//三个构造方法
//1.无参构造:默认容量大小initialCapacity=16 集合负载量75%,即容量大于75%进行扩容,loadFactor负载
//2.有参构造分别为指定容量大小使用默认负载量75%,指定容量大小和指定负载量
//三个构造方法分别调用父类(HashSet)的构造方法super
//进入无参构造(其他两个类比)
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
//创建一个LinkedHashMap,由此可见,LinkedHashSet底层由LinkedHashMap实现的
//进入LinkedHashMap
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
//accessOrder:存取顺序
//进入LinkedHashMap调用父类,由此可见LinkedHashSet与HashMap机制一样,只不过指定了默认的大小和负载量
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;
this.threshold = tableSizeFor(initialCapacity);
}
//添加元素操作,使用debug调试
//LinkedHashSet 加入顺序和取出元素/数据的顺序一致
//LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
//LinkedHashSet 底层结构(数组table + 双向链表)
//添加第一次时,直接将 数组table 扩容到 16,存放的节点类型是LinkedHashMap$Entry table数组类型是HashMap$Node[](打断点可知)而HashMap存放的节点则是HashMap$Node table数组类型也是Hash$Node[]
//由此抛出问题,table和存放的数组不一致是怎么存进去的?显然此时用了多态数组,即Entry是Node的子类或则实现了Node类
//列出当前类的idea的快捷键为Alt+7
//找出了Entry接口
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);
}
}
//该接口继承了HashMap.Node<K,V>,证明之前猜测对的,而且Node类是由HaspMap以类名形式调用,可以猜测Node类是HashMap的一个静态内部类,可追源码验证
//由此可见,它有两个属性before, after,分别代表前后节点的指向
//插入一条数据 debug调试
/*
...
10 = null
11 = {LinedHashMap$Entry@549} "数据1=java.lang.Object@3abfe8836"
before = null
after = null
hash = 25743995
key = "数据1"
value = {cahr[3]@556[数,剧,1]}
hash = 25744371
value = {Object@547}
Class has no fields(类没有字段)
next = null
12 = null
...
16 = null
*/
//插入三条条数据,分别为数据1、数据2、数据3 debug调试
/*
...
10 = null
11 = {LinedHashMap$Entry@549} "数据1=java.lang.Object@3abfe836"
before = null
after = null
hash = 25743995
key = "数据1"
value = {cahr[3]@556[数,剧,1]}
hash = 25744371
value = {Object@547}
Class has no fields(类没有字段)
next = null
12 = {LinedHashMap$Entry@561} "数据2=java.lang.Object@3abfe836"
before = {LinkedHashMap$Entry@549}"数据1"
after = {LinkedHashMap$Entry@561} 数据3
hash = 25743996
key = "数据2"
value = {Object@547}
next = null
13 = {LinedHashMap$Entry@574} "数据2=java.lang.Object@3abfe836"
...
*/
//注意:这里的next要和after区分开,next是数组同一个位置中链表的下一个元素,而after是双向链表的后一个元素
//添加元素的源码底层同Hasp一致
//双向链表源码分析
transient LinkedHashMap.Entry<K,V> tail;
// link at the end of list 元素添加到链表的末尾
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
//初始tail为空,则last为null
tail = p;
//将添加的该节点赋值给tail
if (last == null)
//如果last为空,则证明该集合中只有一个元素,则将head赋值为空
head = p;
else {
//如果不为空,则说明并不是只有一个元素,将该节点的before指向上一个元素,last指向本身,即没有后续节点
p.before = last;
last.after = p;
}
}
TreeSet
Queue
Map
在前面所说的HaspSet中我们知道其底层就是使用了HaspMap,而Map是一个K-V结构,而HaspSet只用了K类存储数据,V用了一个常量present填充
Map接口实现类的特点,在实际应用中极为广泛,
注意:这里讲的是JDK8的Map接口特点 Map_.java
- Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value
- Map 中的key 和 value可以是任何引用类型的数据,会封装到HashMap$Node
对象中 - Map 中的key 不允许重复,原因和HashSet一样,前面分析过源码
- Map 中的value可以重复
- Map 的key可以为null, value也可以为null,注意key 为null, 只能有一个,
value为null ,可以多个. - 常用String类作为Map的key
- key 和 value之间存在单向一对一关系,即通过指定的key 总能找到对应的value
常见的实现子类常有HashMap(子类有LinkedHashMap) TableMap(子类有Properties) TreeMap
Collection的添加操作是add,而Map是put
Map遍历底层原理
-
k-v为了方便遍历,会创建一个EntrySet集合,该集合存放的元素的类型是Entry,而一个Entry对象就有K和V (EntrySet<Entry<K,V>) 源码:transient Set<Map.Entry<K,V>> entrySet
-
EntrySet 中,定义的类型是Map.Entry,但是实际中存放的还是 HashMap$Node
源码:static class Node<K,V> implements Map.Entry<K,V>
-
当把HashMap$Node对象 存放到 entrySet 就方便我们的遍历,因为Map.Entry 提供了两个重要方法 K getKey() 和 V getValue()
-
同理,为了只单独遍历K或则单独遍历Value,也都创建了一个对应的集合,K对应Set集合,value对应Collection集合
package org.example;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class HashMap01 {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
System.out.println(hashMap.put("name1", "数据1"));
System.out.println(hashMap.put("name2", "2"));
System.out.println(hashMap.put("name3", 5));
System.out.println(hashMap.put("name4", new People()));
System.out.println(hashMap.put("name2", "替换value"));
System.out.println(hashMap.put("name1", "数据1"));
System.out.println(hashMap);
//get是通过k返回对应的value
System.out.println(hashMap.get("name1"));
System.out.println("==============================");
Set set = hashMap.entrySet();
System.out.println(set.getClass());
for(Object obj : set){
System.out.println(obj.getClass()); //HashMap$Node
//为了从HashMap$Node取出 k-v 先向下转型
Map.Entry entry = (Map.Entry)obj;
System.out.println(entry.getKey()+"----"+entry.getValue());
}
System.out.println("==============================");
Set set1 = hashMap.keySet();
System.out.println(set1.getClass());
for (Object o : set1) {
System.out.println(o);
}
System.out.println("==============================");
Collection values = hashMap.values();
System.out.println(values.getClass());
for (Object value : values) {
System.out.println(value);
}
}
}
小结:为了方便遍历,会创建一个对应的集合,该集合中存储了table数组中Node节点的地址,可debug观察
Map的六大遍历方式
主要涉及到的方法
- containsKey :查找键是否存在
- keySet:获取所有的键
- entrySet:获取所有的关系k-v
- values:获取所有的值
package org.example;
import java.util.*;
public class MapDemo01 {
public static void main(String[] args) {
Map map = new HashMap();
map.put("key1","value1");
map.put("key2","value2");
map.put("key3","value3");
map.put("key4","value4");
map.put("key5","value5");
System.out.println("========方式一========");
//方式一:先取出所有的Key 通过Key 取出对应的Value
Set set = map.keySet();
//set遍历又有两种,迭代器和增强for
//迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()){
Object key = iterator.next();
Object value = map.get(key);
System.out.println("key:" +key +"===="+"value:"+value);
}
System.out.println("--------------------");
//增强for
for (Object key : set) {
Object value = map.get(key);
System.out.println("key:" +key +"===="+"value:"+value);
}
System.out.println("========方式二========");
//方式二:把所有的values取出
Collection values = map.values();
//Collection可以使用三种遍历方法 迭代器 增强for
//迭代器
Iterator iterator1 = values.iterator();
while (iterator1.hasNext()) {
Object next = iterator1.next();
System.out.println("values:"+next);
}
System.out.println("--------------------");
//增强for
for (Object value : values) {
System.out.println("values:"+value);
}
System.out.println("========方式三========");
//方式三,通过EntrySet 获取K-V
Set set1 = map.entrySet();
//增强for
for (Object entry : set1) {
//将entry转化为Map.Entry,因为Map.Entry中提供了getKey()和getValue()
Map.Entry m = (Map.Entry)entry;
Object key = m.getKey();
Object value = m.getValue();
System.out.println("key:" +key +"===="+"value:"+value);
}
System.out.println("--------------------");
//迭代器
Iterator iterator2 = set1.iterator();
while (iterator2.hasNext()) {
Object entry = iterator2.next();
Map.Entry m = (Map.Entry)entry;
Object key = m.getKey();
Object value = m.getValue();
System.out.println("key:" +key +"===="+"value:"+value);
}
}
}
常用方法
Modifier and Type | Method and Description |
---|---|
void | clear() 从该地图中删除所有的映射(可选操作)。 |
boolean | containsKey(Object key) 如果此映射包含指定键的映射,则返回 true 。 |
boolean | containsValue(Object value) 如果此地图将一个或多个键映射到指定的值,则返回 true 。 |
Set<Map.Entry<K,V>> | entrySet() 返回此地图中包含的映射的Set视图。 |
boolean | equals(Object o) 将指定的对象与此映射进行比较以获得相等性。 |
default void | forEach(BiConsumer<? super K,? super V> action) 对此映射中的每个条目执行给定的操作,直到所有条目都被处理或操作引发异常。 |
V | get(Object key) 返回到指定键所映射的值,或 null 如果此映射包含该键的映射。 |
default V | getOrDefault(Object key, V defaultValue) 返回到指定键所映射的值,或 defaultValue 如果此映射包含该键的映射。 |
int | hashCode() 返回此地图的哈希码值。 |
boolean | isEmpty() 如果此地图不包含键值映射,则返回 true 。 |
Set<K> | keySet() 返回此地图中包含的键的Set 视图。 |
default V | merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction) 如果指定的键尚未与值相关联或与null相关联,则将其与给定的非空值相关联。 |
V | put(K key, V value) 将指定的值与该映射中的指定键相关联(可选操作)。 |
void | putAll(Map<? extends K,? extends V> m) 将指定地图的所有映射复制到此映射(可选操作)。 |
default V | putIfAbsent(K key, V value) 如果指定的键尚未与某个值相关联(或映射到 null )将其与给定值相关联并返回 null ,否则返回当前值。 |
V | remove(Object key) 如果存在(从可选的操作),从该地图中删除一个键的映射。 |
default boolean | remove(Object key, Object value) 仅当指定的密钥当前映射到指定的值时删除该条目。 |
default V | replace(K key, V value) 只有当目标映射到某个值时,才能替换指定键的条目。 |
default boolean | replace(K key, V oldValue, V newValue) 仅当当前映射到指定的值时,才能替换指定键的条目。 |
default void | replaceAll(BiFunction<? super K,? super V,? extends V> function) 将每个条目的值替换为对该条目调用给定函数的结果,直到所有条目都被处理或该函数抛出异常。 |
int | size() 返回此地图中键值映射的数量。 |
Collection<V> | values() 返回此地图中包含的值的Collection视图。 |
HashMap
- 数据存放形式是键值对
- 当添加的元素的k相同时,会替换原有的value值
- 存取无序,value可以重复
- key可以为null,value也可以为null,但是key为null只能有一个,value可以为多个
- 常用String作为Map的key,但是k可以是任意Object
HaspMap结构
jdk7中 底层实现 数组+链表
jdk8中 底层实现 数组+链表+红黑树
扩容机制
-
HashMap底层维护的是一个Node类型table数组(默认大小是null,默认负载因子0.75),数组中存放着链表的节点,是HashMap$Node 该node节点是实现了Map.Entry<K,V>
-
当创建HashMap对象时,将加载因子(loadfactor)初始化为0.75
-
当添加一个k-v时 ,如果第一次添加,则需要将table数组容量设为16,临界值(treshold)12
-
添加一个元素并计算Key其hash值,通过一个算法得到对应的下标,如果该下标存在元素(哈希碰撞),就会进行equals对比确定是否是同一个对象,是,直接替换Val,否,判断是树结构还是链表结构,做出相应处理,如是链表结构,添加到该元素的链表的后端,这也就是解决哈希碰撞的一种方法,拉链法
-
以后再次扩容时,则table扩容大小是原来的两倍,临界值也为原来的两倍(其实底层就是乘负载因子)即24,以此类推
-
在jdk8中,当链表长度大于等于TREEIFY_THRESHOLD(默认是8)时,并且table的大小>= MIN_TREEIFY_CAPACITY(默认是16)时,链表树化为红黑树,因为树的查询效率是很高效的
小于等于临界值6时,退化为链表
注:当链表的长度到达8时且数组长度到达64时,才会进行转换为红黑树,如果当链表的长度到达8时,但是数组的长度小于64时,不会转化为红黑树,因为数组的长度较小,应该尽量避开红黑树,因为红黑树需要进行左旋右旋变色操作来保持平衡,所以当数组长度小于64时,使用数组加链表比使用红黑树查询效率要更快,效率要更高