集合
为什么要学习集合,因为集合是Java中成熟的数据结构的实现,为什么要用这些数据结构,因为我们想要更合理的存储数据,数组有缺陷。集合关注的是怎么操作一个集合,把数据存进去取出来,删除,判断有与否,对数据的操作。
文章目录
类集概述
集合是常用类库里面很重要的一部分,与数组的概念相似,是数据的容器。普通对象数组最大的问题在于数组中的元素的个数是固定的,我们使用的动态扩容数组也是使用创建新数组来实现的。通过我们的学习,我们可以通过二叉树、链表等等的方法来完成可以无限制存储数据还不用扩容的这种机制。
在 Java 中为了方便用户操作各个数据结构,所以引入了类集的概念,有时候就可以把类集称为 java 对数据结构的实现。类集包含了很多的常见的数据结构,每种数据结构都有它擅长的点。
在整个类集中的,这个概念是从 JDK 1.2(Java 2)之后才正式引入的,最早也提供了很多的操作类,但是并没有完整的提出类集的完整概念。
类集中最大的几个操作接口:Collection、Map、Iterator(迭代器),这三个接口为以后要使用的最重点的接口。所有的类集操作的接口或类都在 java.util 包中。
Collection接口
Collection接口是整个Java类集结构中保存单值的最大操作接口,里面每次存储只能存一个对象的数据,此接口定义在java.util包中。此接口定义如下:
public interface Collection<E> extends Iterable<E>
它提供的方法有很多,一共有15个方法:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public boolean add(E e) | 普通 | 向集合中插入一个元素 |
2 | public boolean addAll(Collection<? extends E> c) | 普通 | 向集合中插入一组元素 |
3 | public void clear() | 普通 | 清空集合中的元素 |
4 | public boolean contains(Object o) | 普通 | 查找一个元素是否存在 |
5 | public boolean containsAll(Collection<?> c) | 普通 | 查找一组元素是存在 |
6 | public boolean isEmpty() | 普通 | 判断集合是否为空 |
7 | public Iterator iterator() | 普通 | 为Iterator接口实例化 |
8 | public boolean remove(Object o) | 普通 | 从集合中删除一个对象 |
9 | boolean removeAll(Collction<?> c) | 普通 | 从集合中删除一组对象 |
10 | boolean retainAll(Collection<?> c) | 普通 | 判断是否没有指定的集合 |
11 | public int size() | 普通 | 求出集合中元素的个数 |
12 | public Object[] toArray() | 普通 | 以对象数组形式返回集合中的全部内容 |
13 | T[] toArray(T[] a) | 普通 | 指定操作的泛型类型,并把内容返回 |
14 | public boolean equals(Object o) | 普通 | 从Object类中覆写而来 |
15 | public int hashCode() | 普通 | 从Object类中覆写而来 |
因为Collection都是是接口,这些方法都是抽象的方法,此接口的全部子类或子接口就将全部继承以上接口中的方法。在开发中不会直接使用 Collection 接口。而使用其操作的子接口:List、Set。其中List里面存储的数据是允许重复的,Set里面存储的数据是不允许重复的。
List接口
在整个集合中 List 是 Collection 的子接口,里面的所有内容都是允许重复的。
List 子接口的定义:
public interface List<E> extends Collection<E>
此接口上任然使用了使用了泛型技术。此接口对于Collection接口来讲有如下的扩充方法:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public void add(int index,E element) | 普通 | 在指定位置添加元素 |
2 | boolean addAll(int index,Collection<? extends E> c) | 普通 | 在指定位置处增加一组元素 |
3 | public E get(int index) | 普通 | 根据索引位置取出每一个元素 |
4 | public int indexOf(Object o) | 普通 | 据对象查找指定位置,找不到返回-1 |
5 | public int lastIndexOf(Object o) | 普通 | 从后面向前查找位置,找不到返回-1 |
6 | public ListIterator listIerator() | 普通 | 返回ListIterator接口的实例 |
7 | public ListiTerator listIterator(int index) | 普通 | 返回指定位置ListIterator接口实例 |
8 | public E set(int index,E element) | 普通 | 修改指定位置的内容 |
9 | public E set(int index,E element) | 普通 | 修改指定位置的内容 |
10 | List subList(int fromIndex,int toIndex) | 普通 | 返回自集合 |
在 List 接口中有以上 10 个方法是对已有的 Collection 接口进行的扩充。所以,证明List 接口拥有比 Collection 接口更多的操作方法。
了解了 List 接口之后,那么该如何使用该接口呢?需要找到此接口的实现类,常用的实现类有如下几个:
· ArrayList(95%)、Vector(4%)、LinkedList(1%)。
其中Vector是ArrayList的早期实现,它们的区别是一个线程安全一个线程不安全;而LinkdedList是通过链表结构进行存储。它们都支持List的方法。
ArrayList
ArrayList 是 List 接口的子类,此类的定义如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>,RandomAccess,Cloneable,Serializable
此类继承了AbstractList 类。AbstractList 是 List 接口的子类。AbstractList 是个抽象类,适配器设计模式。
示例:
package com.test.fourThree;
import java.util.ArrayList;
import java.util.Arrays;
/**
* 1. Arraylist采用无参构造器构造的集合在开始时长度默认10
*/
public class Demo1 {
public static void main(String[] args) {
//ArrayList: list集合下的一个实现类,使用的是数组结构,对于增删操作慢,查找快.
//必须传的是包装类,int不是具体的引用数据类型,Integer可以.单值存储
ArrayList<Integer> date = new ArrayList<>();//0
// 增加内容,此方法从Collection接口继承而来
date.add(100);
date.add(233);
// 增加内容,此方法是List接口单独定义的
date.add(1, Integer.valueOf("2"));
// 打印data对象调用toString()方法
System.out.println(date.get(2));
// 删除指定位置的元素
data.remove(1);
for(int x=0;x<data.size();x++){//size()方法从Collection接口继承而来
System.out.print(data.get(x)+",")
}
}
}
Java中的包装类
基本类型 | 对应安装包 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
其中的ArrayList构造方法通过API进行查看:
构造器 | 描述 |
---|---|
ArrayList() | 构造一个初始容量为10的空列表 |
ArrayList(int initialCapacity) | 构造具有指定初始容量的空列表 |
ArrayList(Collection<? extends E> c) | 按照集合的迭代器返回的顺序构造一个包含指定集合元素的列表 |
当不传参数时它会自动构造一个初始容量为10的空列表,在我们存储一个数据较多的内容的时候,建议使用一参构造方法,因为使用无参构造方法的时候它会一种在进行扩容影响了程序的效率。
其中add方法当中增加一个数据,无论存储成功与否放回的布尔类型数据都是true,点开看一下源码
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
Vector
与 ArrayList 一样,Vector 本身也属于 List 接口的子类,此类的定义如下:
public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable
此类与 ArrayList 类一样,都是 AbstractList 的子类。所以,此时的操作只要是 List 接口的子类就都按照 List 进行操作。与新的集合实现不同,Vector是同步的(排着队走),如果不需要线程安全实现,建议使用ArrayList代替Vector。
因为Vector与ArrayList使用方法基本一致,就不在进行代码演示,我们看一看它的构造方法:
构造器 | 描述 |
---|---|
Vector() | 构造一个空向量,使其内部数据数组的大小为10,其标准容量增量为零 |
Vector(int initialCapacity) | 构造一个具有指定初始容量且容量增量等于零的空向量 |
Vector(int initialCapacity,int capacityIncrement) | 构造具有指定初始容量和容量增量的空向量 |
Vector(Collection<? extends E> c) | 按照集合的迭代器返回的顺序构造一个包含指定集合元素的向量 |
Vector提供了四个构造方法,其中无参构造方法与ArrayList有一些出入,Vecrot每次扩容可以指定增加的量,而ArrayList是每次扩容1.5倍。再看一看第三个Vector(a, b),其中a表示初始容量,b表示每次扩容增加的量,当b=0的时候,扩容的是长度就为a。
ArrayList类与Vector类的区别:
LinkedList
此类的使用几率是非常低的,但是此类的定义如下:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable
此类继承了 AbstractList,所以是 List 的子类。但是此类也是 Queue 接口的子类,Queue 接口定义了如下的方法:
方法名称 | 类型 | 描述 | |
---|---|---|---|
1 | public boolean addd(E e) | 普通 | 增加元素,如果容量限制,并且以满,则抛出异常 |
2 | public E element() | 普通 | 获取元素,取出后不删除元素,如果队列为空则抛出异常 |
3 | boolean offer(E e) | 普通 | 添加元素,若有容量限制且以存满,就无法添加但不抛出异常,返回false |
4 | E peek() | 普通 | 取得头元素,取出后不删除,如果队列为空,则返回null |
5 | E poll() | 普通 | 取得头元素,取出后删除,如果队列为空,则返回null |
6 | E remove() | 普通 | 删除当前元素,如果队列为空则抛出异常 |
示例:
package com.test.fourThree;
import java.util.LinkedList;
public class Demo2 {
public static void main(String[] args) {
//LinkedList : 使用的是双向链表结构,对于增加删除快,查找慢.
//add,remove,get()
LinkedList<Integer> date = new LinkedList<>();
//首部添加
data.addFrinst(88);
data.addFrinst(99);
//打印并删除99
System.out.println(data.removeFrinst());
//模拟栈结构
//压栈,先进后出,后进先出
date.push(100);
date.push(300);
//弹栈
Integer i = date.pop();
System.out.println(i);
}
}
LinkedList跟ArrayList是互补的状态,ArrayList它增删操作慢,查找快,而LinkedList它增加删除快,查找慢。我们就可以根据自己的业务需求选择不同的类进行数据操作。
而因为LinkedList使用的是双向链表结构,就可以使用它模拟栈、队列来使用。
Iterator与ListIterator(迭代器)
迭代器用于遍历集合。
Iterator此接口定义如下:
public interface Iterator<E>
LisrIterator此接口定义如下:
public interface ListIterator<E>
extends Iterator<E>
之前我们遍历集合是这样的:
package com.test.fourThree;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
public class Demo3 {
public static void main(String[] args) {
//创建集合对象
ArrayList<Integer> date = new ArrayList<>();
date.add(100);
date.addFirst(200);
date.addLast(304);
date.add(2);
for(int i=0;i<data.size();i++){
System.out.println(data.get(i));
}
}
}
当然这样遍历是没有任何的问题,但是当变成LinkedList以后,这不就不是一个最优的实现,我们就可以通过迭代器进行遍历集合,迭代器分为Iterator和ListIterator。Iterator用来迭代Collection下的所有集合,比如List和Set;而ListIterator只能用来迭代List下面的集合。因为Iterator也能迭代List下的集合,就使用Iterator来演示迭代的操作:
package com.test.fourThree;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
/**
* Iterator:迭代器
* 遍历集合,用于从集合中取数据
*
* ListIterator:只能迭代List下面的集合
*/
public class Demo3 {
public static void main(String[] args) {
//创建集合对象
LinkedList<Integer> date = new LinkedList<>();
date.add(100);
date.addFirst(200);
date.addLast(304);
date.add(2);
//创建对象,通过集合对象
Iterator<Integer> iterator = date.iterator();
//快捷批量注释:Ctrl+Shift+/
//遍历数据:
/* while(iterator.hasNext()){
//获取该数组数据的同时向下移一位
Integer i = iterator.next();
System.out.println(i);
}*/
/**
* //remove直接这样使用是不可行的
* date.remove();
*/
//iterator类的remove方法必须获取数据后才能删除,因为只有指针指到了非空才能删除
//移动指针获取数据
iterator.next();
//进行删除
date.remove();
System.out.println(date.size());
}
}
Iterator里面有一个hasNext()方法,查询是否拥有下一个数据节点,但是它不能使得指针往下走,这就得配合iterator.next()使得指针往下一个走,完成遍历(注意遍历的时候为了能打印所有数据,一定得让指针在顶部,第一个数据上的位置,再开始遍历!)。
下面是Iterator的方法:
变量和类型 | 方法 | 描述 |
---|---|---|
default void | forEachRemaining(Consumer<? super E>action) | 对每个剩余元素执行给定操作,直到处理完所有元素或操作引发异常 |
boolean | hasNext() | 下一个位置有元素,返回true |
E | next() | 返回迭代中的下一个元素 |
default void | remove() | 从底层集合中移除此迭代器返回的最后一个元素(可选操作) |
下面我们看一看ListIterator的方法:
通过Iterator我们知道,想要remove数据得往下移动,LisIterator当中的previous想要执行也得指针下移后才能操作。我们来看一看:
package com.test.fourThree;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
public class Demo3 {
public static void main(String[] args) {
//创建集合对象
LinkedList<Integer> date = new LinkedList<>();
date.add(100);
date.addFirst(200);
date.addLast(304);
date.add(2);
ListIterator<Integer> iterator = date.listIterator();
//不调用next()的话previous不能执行,因为指针没有下移是不能上移的。通俗的说就是
System.out.println(iterator.next());
System.out.println(iterator.previous());
}
}
当然在ListIterator里面还有一些添加方法,我们下来都可以试一试。
forEach循环
forEach最早出现在c#语言当中,在java用于迭代数组或集合结构,在使用forEach时,内部会使用最优的迭代方法,for循环的增强版,它优于之前我们使用的while(Iterator.hasNext())遍历的方法。
注意:forEach只能用于迭代数组或集合,并且这个集合只能是Collection下的集合
格式:
for(数据类型 变量名:集合或数组名){}
比如之前我们使用for迭代数组:
package com.test.fourThree;
public class Demo4 {
public static void main(String[] args) {
int[] arr = {5,4,3,1};
for(int i=0;i<arr.length;i++){
System.out.println(arr[i]);
}
}
}
这样迭代显得不是那么友好,java就提供了forEach用于迭代我们需要迭代的数组或集合:
package com.test.fourThree;
import java.util.ArrayList;
/**
* foEach : 增强for循环,最早出现在C#语言中.
* 1. java中用于迭代数组 或 集合(Collection下的),不能迭代其他的
* 2. 语法:for(数据类型 变量名:集合或数组名){}
* 3. 小tip:Shift+Enter:光标下移一行
*/
public class Demo4 {
public static void main(String[] args) {
//测试迭代数组
int[] arr = {5,4,3,1};
//forEach循环
for(int data:arr){
System.out.println(data);
}
System.out.println("-----------这是一条华丽的分割线------------");
//测试迭代Collection下的集合
//创建ArrayList对象
ArrayList<String> data = new ArrayList<>();
data.add("锄禾日当午");
data.add("汗滴禾下土");
data.add("谁知盘中餐");
data.add("粒粒皆辛苦");
//这里把String换成Objdect也是可以的,因为String类型属于Object类型
for (String s:data) {
System.out.println(s);
}
}
}
使用forEach迭代数组或集合(Collection下)显得便捷很多,当然与Iterator的效率是一样的。
Set接口
Set 接口也是 Collection 的子接口,与 List 接口最大的不同在于,Set 接口里面的内容是不允许重复的。Set 接口并没有对 Collection 接口进行扩充,基本上还是与 Collection 接口保持一致。因为此接口没有 List 接口中定义
的 get(int index)方法,所以无法使用循环进行输出。那么在此接口中有两个常用的子类:HashSet、TreeSet。
注:
- 我们查看Set类的方法发现我们无法直接获取数据,要从Set里面取数据使用Iterator方法得到迭代器,或者通过toArray把它变成一个数组再去找里面的数据
- 如果将可变对象用作Set元素,则必须非常小心。因为Set的一些子类(如HashSet)存储是无序的,有时候因为属性的更改可能会导致一些问题发生,后续提到这些子类的时候会进行一些解释。
散列存放:HashSet
既然 Set 接口并没有扩充任何的 Collection 接口中的内容,所以使用的方法全部都是 Collection 接口定义而来的。既然 Set 接口并没有扩充任何的 Collection 接口中的内容,所以使用的方法全部都是 Collection 接口定义而来的。同样,需要获取HashSet里面的数据,也得转成数组进行遍历寻找或使用Iterator对其进行迭代。
HashSet既然属于Collection集合,所有属于HashSet单值存储结构,系统之前设置了一个HashMap进行的是双值存储,这里的HashSet就重复利用了这个双值存储的哈希表,它的利用方式就是往双值存储的哈希表里面put了两个数据,其中一个值就是我们需要存储的数据,另一个就是系统固定好的常量,我们来看一看HashSet当中的add方法的源码:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
一个是我们需要存如的数据e,是PRESRNT,我们再对PRESENT查看源码:
private static final Object PRESENT = new Object();
PRESENT是一个固定好的常量。那具体的哈希表后续再进行描述。
HashSet方法:
注意看add方法是由一个boolea类型的返回值的,下面示例程序会用到这一点:
package com.test.fourThree;
import java.util.HashSet;
import java.util.Iterator;
/**
* HashSet:
* 1. 散列存放(哈希表在学习HashMap集合时讲解)
* 2. Map集合是双值存储的,HashSet是单值存储
* 3. HashSet是基于HashMap的一种存储方式,有一个值定义为常量就成了单值存储
* 4. 不允许重复存储
* 5. 散列是不能保证存储顺序的(因为是由HashMap存储方式)
*/
public class Demo5 {
public static void main(String[] args) {
//泛型
HashSet<String > set = new HashSet<>();
boolean flag1 = set.add("锄禾日当午");
set.add("汗滴禾下土");
set.add("谁知盘中餐");
set.add("粒粒皆辛苦");
//重复存储看是否能存入,添加方法是由返回值的,我们给一个命名
boolean flag2 = set.add("锄禾日当午");
//对flag1、flag2进行输入查看谁成功了.很明显flag1=true,flag2=false
System.out.println("flag1="+flag1+",flage2="+flag2);
//创建Iterator对象,使用forEach迭代输出
Iterator<String> iterator = set.iterator();
for (String s:set
) {
System.out.println(s);
}
}
}
程序输入的顺序不能保证,因为HashSet是由HashMap的散列存储方式,后续的HashMap会对其原理进行解释。
排序子类:TreeSet
TreeSet与HashSt很像,都是Set集合的实现类。与 HashSet 不同的是,TreeSet 本身属于排序的子类,即使用有序(自然顺序)的二叉树存储的,二叉树大家都已经有所了解不进行过多的赘述。TreeSet是基于TreeMap集合来实现的,后续会对TressMap进行分析。此类的定义如下:
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, Serializable
很多集合里面都有这么一句话:Iterator方法返回的迭代器是快速失败和安全失败:
- 快速失败:在遍历这个集合的时候,这个迭代器遍历的是集合本身,它有一个记录自己遍历到哪一个数据的变量,假设有10个数据,迭代器正在遍历的时候如果有另外一条执行路径对集合内容进行了改变(删除、添加等),与之前的数据链不一样,此时迭代器就是懵的,就会抛出异常(ConcurrentModificationException异常)
- 安全失败:这个失败不会出错,在遍历的时候把集合复制了一份,不管集合是被操作与否,对于迭代器来说都是无所谓的,因为它迭代的是复制的那一份。
是什么失败可以根据API里面的描述去查看,但一般不进行特殊的描述的都是安全失败,遍历的时候哪怕改变需要遍历的集合也不会出现问题。
我们回到TreeSet的使用,它是进行有序的存储,这个有序不是指存储的有序,而是根据数据的顺序进行排序(根据ascii码),这个排序类似于微信里面的好友列表,对名称进行排序。如下代码所示:
package com.test.fourThree;
import java.util.TreeSet;
/**
* TreeSet
*/
public class Demo9 {
public static void main(String[] args) {
//TreeSet为泛型别忘了
TreeSet<String> data = new TreeSet<>();
//打乱顺序进行存储
data.add("C");
data.add("D");
data.add("E");
data.add("A");
data.add("B");
data.add("F");
//遍历输出
for (String s:data
) {
System.out.println(s);
}
}
}
存储系统所提供的数据,数据进行排序是没有问题的,但是当我们存储自定义类型的数据,又会有怎么样的问题?看下面的代码:
package com.test.fourThree;
import java.util.Objects;
import java.util.TreeSet;
/**
* TreeSet
*/
public class Demo9 {
public static void main(String[] args) {
TreeSet<Preson> temp = new TreeSet<>();
//测试自定义类型的数据
Preson p1 = new Preson("张三",18);
Preson p2 = new Preson("李四",19);
temp.add(p1);
temp.add(p2);
//系统会怎么进行排序呢?
for (Preson t:temp
) {
System.out.println(t);
}
}
//自定义一个类
static class Preson{
private String name;
private int age;
//生成构造方法
public Preson() {
}
public Preson(String name, int age) {
this.name = name;
this.age = age;
}
//生成getter/setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
//生成toString方法
@Override
public String toString() {
return "Preson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
//生成equals和hashcode方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Preson preson = (Preson) o;
return age == preson.age &&
Objects.equals(name, preson.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
}
结果是注意的:
Exception in thread "main" java.lang.ClassCastException: class com.test.fourThree.Demo9$Preson cannot be cast to class java.lang.Comparable (com.test.fourThree.Demo9$Preson is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
at java.base/java.util.TreeMap.compare(TreeMap.java:1291)
at java.base/java.util.TreeMap.put(TreeMap.java:536)
at java.base/java.util.TreeSet.add(TreeSet.java:255)
at com.kaikeba.fourThree.Demo9.main(Demo9.java:29)
程序员不指定谁在前系统也不知道谁在前,必然会导致错误。再来分析错误提醒:com.test.fourThree.Demo9$Preson cannot be cast to class java.lang.Comparable 表示Preson类转Comparable类失败,而Comparable是一个接口,它有一个抽象的方法,如果想要系统对数据进行排序就得实现这个Comparable接口,调用Comparable里面的一个比较的方法。下面来进行实现一下:
package com.test.fourThree;
import java.util.Objects;
import java.util.TreeSet;
/**
* TreeSet
*/
public class Demo9 {
public static void main(String[] args) {
TreeSet<Preson> temp = new TreeSet<>();
//测试自定义类型的数据
Preson p1 = new Preson("张三",21);
Preson p2 = new Preson("李四",19);
Preson p3 = new Preson("王二",22);
temp.add(p1);
temp.add(p2);
temp.add(p3);
for (Preson t:temp) {
System.out.println(t);
}
}
//创建Comparable接口
static class Preson implements Comparable<Preson>{
//要比较的是Preson与Preson类
private String name;
private int age;
/**
* this 与 o 进行比较
* 返回的数据是:
* 1. 负数:this小
* 2. 零:相同
* 3. 正数:this大
* @param o
* @return
*/
@Override
public int compareTo(Preson o) {
//比较年龄,进行排序
//规定按照年龄升序
if(this.age > age){
return 1;
}else if(this.age == o.age){
//如果发现两个一样大,不存储第二个数据
return 0;
}
return -1;
}
//生成构造方法
public Preson() {
}
public Preson(String name, int age) {
this.name = name;
this.age = age;
}
//生成getter/setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
//生成toString方法
@Override
public String toString() {
return "Preson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
//生成equals和hashcode方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Preson preson = (Preson) o;
return age == preson.age &&
Objects.equals(name, preson.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
}
那么需要多加练习,才能熟练掌握、使用。
哈希表概述
Java中哈希表实现方式:采用对象数组加链表,在链表长度达到一定程度以后链表转换成二叉树,这样的一种数据结构。下面进行一些简单的分析:
说到哈希表就不得不提到前面的Object.hashCode,我们重写equals方法的时候让我们重写的都是hashCode、equals。先看一下hashCode的API:
此方法支持散列表,而散列表就是哈希表。对象数组在默认的时候它的长度的16,下标是0-15。
具体存储的原理:Object类可以存储所有类型,所以每一个数据都会有一个哈希值(哈希值均匀分布),而哈希表在存储数据的时候会调用数据的hashCode值,然后去跟数组的长度(初始为16)进行取余(%)运算,得到0-15的数,这个结果就是该数据存储具体的下标。比如一个数据的哈希值是18,18%16 = 2,所以该数据存储在哈希表的2下标,而当数据取余得到的下标重复的时候,该下标数据会以链表进行存储(因为每一个下标都进行链式存储,固每一个下标都称为哈希桶)。该存储的优点就是查找数据比较简单,直接获取哈希值进行取余,找到下标。
JDK1.8对哈希表进行了优化,当某哈希桶中的数据量存储方式为链表时,该哈希桶数据量增加到8时,存储方式从链表转化为红黑树进行存储,查找变为更快捷;当该某希桶中的数据存储方式为红黑树时,该哈希桶数据量减少到6时,该哈希桶的存储方式从红黑树二叉树转换为链表存储。这就提到了一个面试点:已知数据是7个,现在要减少一个数据,问该哈希桶一定为由红黑树存储转化为链表存储吗?不是的,并没有说已经转换为红黑树,如果它本身就是链表,没有到过8,是不会有转换存储方式的这个操作。
这一块是哈希表的基础结构,但是还有一些比较特殊的点:
- 它会有一个初始的桶的数量16(float类型)
- 散列因子0.75(也称负载因子)
- 当已有桶已经由75%的桶存上数据了,就对桶进行扩容为原长度的2倍
- 这就是为什么上面哈希值要均匀分布的原因
- 官方测试最合适的数值(兼顾内存与效率)
另外,影响HashMap的性能的参数就是初始容量和负载因子(0.75),一定要给合理的初始参数,避免频繁的散列影响性能。负载因子越大内存空间利用率越高,使用效率越低;负载因子越小内存空间利用率越低,使用效率越高。
Map接口
首先,Map(Mapping)和Collection是一个等级的,要避免List(允许重复)、Set(不允许重复)与Collection之间关系弄混淆。
以上的 Collection 中,每次操作的都是一个对象,如果现在假设要操作一对对象,则就必须使用 Map 了。那么保存以上信息的时候使用 Collection 就不那么方便,所以要使用 Map 接口。里面的所有内容都按照 key -> value(键值对)的形式保存,也称为二元偶对象。通俗一点就是:键是钥匙,数据是锁,我们需要存储钥匙和锁,找到钥匙才能打开锁获取数据。
当然Map集合里的 键(key) 是不允许重复的,一个键最多可以映射一个值。而Set使用Map集合的键(key)进行存储,这也就是为什么Set集合下的HashSet和TreeSet存储的数据不能重复的原因。
此接口定义如下:
public interface Map<K,V>//两个泛型,key的类型和value的类型
此接口与 Collection 接口没有任何的关系,是第二大的集合操作接口。此接口常用方法如下:
类型 | 方法 | 描述 |
---|---|---|
void | clear() | 清空Map集合中的内容 |
boolean | containsKey(Object key) | 判断集合中是否存在指定的key |
boolean | containsValue(Object value) | 判断集合中是否存在指定的value |
Set<Map.Entry<K,V>>entrySet() | 将Map接口变为Set集合 | |
V | get(Object key) | 根据key找到其对应的value |
boolean | isEmpty() | 判断是否为空 |
SetkeySet() | 将全部的key变为Set集合 | |
Collectionvalues() | 将全部的value变为Collection集合 | |
V | put(K key,V value) | 向集合中增加内容 |
void | putAll(Map<? extends K,? extends V>m) | 增加一组集合 |
V | remove(Object key) | 根据key删除内容 |
Map本身就是一个接口,所以一般会使用HashMap、TreeMap、Hashtable三个子类。
HashMap此类的定义如下:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap与Hashtable区别
Map集合使用案例:
package com.test.fourThree;
import java.util.Collection;
import java.util.HashMap;
import java.util.Set;
/**
* HashMap集合
* 1. HashMap里面的数据存储无序
*/
public class Dmeo6 {
/**
* 1. Map
* 2. HashMap/HashTable/ConcurrentHashMap--
* 1. 容器
* 2. 区别在多线程,线程安全与否
* 3. HashMap 线程不安全,效率高
* 4. HashTable 线程安全,效率低
* 5. ConcurrentHasMap 采用分段锁机制,保证线程安全,效率又比较高
* 3. TreeMap--自动进行排序
* 4. LinkedHashMap
*
*/
public static void main(String[] args) {
/**
* 一下几种集合的实现方式都是一样的,只要把包导入即可
* 1. Map
* 2. HashMap/HashTable/ConcurrentHashMap
* 3. TreeMap--自动进行排序
* 4. LinkedHashMap
*/
//创建 值 和 key 都是String类型的,对象名为data
HashMap<String,String> data = new HashMap<>();
//调用put存储数据,注意:输入key和数据
data.put("key1","锄禾日当午");
data.put("key2","汗滴禾下土");
data.put("key3","谁知盘中餐");
//调用get获取key,后续才能使用key获取得到数据
String value = data.get("key2");
System.out.println(value);
value = data.get("key3");
System.out.println(value);
System.out.println("--------这是一条华丽的分割线---------");
//另一种遍历方式,获取key集合
Set<String> set = data.keySet();
for (String key:set) {
//再次通过key获取值
System.out.println(key+"->"+data.get(key));
}
System.out.println("--------这是一条华丽的分割线---------");
//获取值的集合,
Collection<String> values = data.values();
for (String v: values) {
System.out.println(v);
}
}
}
Map集合下HashMap/HashTable/ConcurrentHashMap的区别:根本区别在于多线程(程序的多条执行路径)问题,线程安全与否问题,基于存、取、删。
- HashMap:多线程,线程不安全(效率高)。
- HashTable:单线程,线程安全(效率低),进行排队,前一个完成后一个才能进行执行。拥有一个队伍排队。
- ConcurrentHashMap:采用分段锁机制,保证线程安全,效率较高。分段锁机制简而言之就是每一个哈希桶只能由进行操作,可以同时操作多个不同的哈希桶,拥有多个排队的队伍。
值得注意的是:使用Map集合,存储自定义类型的时候,特别是HashMap这种哈希表结构存储数据时,一定要支持equals和hashcode。且千万不能乱改 键 当中的数据,很严重,会导致之前存储的数据找不到的情况,当数据数量达到一定程度使用散列表存储的时候,带来的问题会越来越多。如果非要在存储数据以后进行数据的key或value的更改,就不能使用HashMap这种哈希表结构存储数据。
package com.test.fourThree;
import java.util.HashMap;
import java.util.Objects;
/**
* 存储自定义对象
*/
public class Demo7 {
public static void main(String[] args) {
HashMap<Book,String> data = new HashMap<>();
Book book1 = new Book("金苹果","讲述了一个金色的苹果");
//key+数据 进行存储
data.put(book1,"人生中第一本书");
Book book2 = new Book("银苹果","讲述了一个银色的苹果");
data.put(book2,"人生中第二本书");
Book book3 = new Book("铜苹果","讲述了一个铜色的苹果");
data.put(book3,"人生中第三本书");
System.out.println(data.get(book1));
//存一个与boo1一样的key
Book book4 = new Book("金苹果","讲述了一个金色的苹果");
//book4并没有作为key存进去,系统通过计算哈希值,得到与book1相同的哈希值则可以进行输出
System.out.println(data.get(book4));
//重置book1的一个数据
book1.setName("x苹果");
//下面打印的是null,因为data.get能通过book1的hash值找到book1的位置,但是equals不能通过
//千万不能乱改 键 当中的数据,很严重,
System.out.println(data.get(book4));
}
//main中调用使用静态
static class Book{
private String name;
private String info;
//快捷键生成构造方法
public Book() {
}
public Book(String name, String info) {
this.name = name;
this.info = info;
}
//快捷键生成getter/setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", info='" + info + '\'' +
'}';
}
//快捷键生成equals和hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(name, book.name) &&
Objects.equals(info, book.info);
}
//哈希码是由传入的数据计算出来的
@Override
public int hashCode() {
return Objects.hash(name, info);
}
}
}
JDK9集合的新特性
创建固定长度的集合。存在于3个接口当中,分别是List、Set、Map,它们的子类没有。
Set:
List:
Map:
示例:
package com.test.fourThree;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* JDK9的新2特性
* 提高储存固定的长度的集合的一些便捷方法
*/
public class Demo8 {
public static void main(String[] args) {
//Map集合
Map<String,String> map = Map.of("h1","锄禾日当午","h2","汗滴禾下土");
//Map结合:先通过keySet获取它的key,再遍历
Set<String> keySet = map.keySet();
for (String key:keySet) {
//通过get键找到对应的值,进行输出
System.out.println(key+"->"+map.get(key));
}
//List集合
List<String> list = List.of("床前明月光","疑是地上霜");
//固定存储,不能修改,扩容
//list.add("举头望明月");//error
for (String l:list) {
System.out.println(l);
}
//不保证顺序的Set集合
//快捷键创建:Alt+Enter-
Set<String> set = Set.of("飞流直下三千尺", "疑是银河落九天");
for (String s:set) {
System.out.println(s);
}
}
}
t:
[外链图片转存中…(img-iRukod25-1599902658986)]
Map:
[外链图片转存中…(img-cGbacPGv-1599902658986)]
示例:
package com.test.fourThree;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* JDK9的新2特性
* 提高储存固定的长度的集合的一些便捷方法
*/
public class Demo8 {
public static void main(String[] args) {
//Map集合
Map<String,String> map = Map.of("h1","锄禾日当午","h2","汗滴禾下土");
//Map结合:先通过keySet获取它的key,再遍历
Set<String> keySet = map.keySet();
for (String key:keySet) {
//通过get键找到对应的值,进行输出
System.out.println(key+"->"+map.get(key));
}
//List集合
List<String> list = List.of("床前明月光","疑是地上霜");
//固定存储,不能修改,扩容
//list.add("举头望明月");//error
for (String l:list) {
System.out.println(l);
}
//不保证顺序的Set集合
//快捷键创建:Alt+Enter-
Set<String> set = Set.of("飞流直下三千尺", "疑是银河落九天");
for (String s:set) {
System.out.println(s);
}
}
}
多多学习,集百家之长,不闭门造车。