集合
概述
集合实际上就是一个容器 , 可以动态的容纳不同类型的对象。数组也是一个集合但它长度一旦确定就不能更改
- 集合是一个载体 , 可将数据库中查询到的10条记录封装成10个java对象放到某一个集合当中并传到前端,然后前端遍历这个集合做数据展示
- 集合不能直接存储基本数据类型和 java 对象 ,集合中存储的都是堆中某个java对象的内存地址 , list.add(100)是发生了自动装箱
所有的集合类和集合接口都在java.util包下, 每一个不同的集合底层会对应不同的数据结构。我们需要选择合适的集合 , 将数据放到合适的数据结构当中
- new ArrayList(); 创建一个底层是数组的集合,检索效率极高(末尾添加元素效率也很高)
- new LinkedList(); 创建一个底层是链表的集合,把随机增删发挥到极致 , 检索效率较低(每次都是从头开始遍历)
- new TreeSet(); 创建一个底层是二叉树的集合
- new HashMap(); 创建一个底层是哈希表的集合
java中集合分为两大类
- 单列集合: 单个方式存储元素,这一类集合中超级父接口:java.util.Collection
- 双列集合: 以键值对的方式存储元素,这一类集合中超级父接口:java.util.Map
集合中的有序 , 无序 , 排序 , 可重复 , 不可重复
- 有序: 集合中的元素存进去和取出来的顺序是一致的
- 无序: 集合中的元素存进去和取出来的顺序不同
- 排序: 对集合中的元素按照从大到小的顺序排序 , 一旦这样做集合中的元素存进去和取出来的顺序就会发生变化
- 不可重复: 无论是单列集合还是双列集合说到的不可重复 , 都是说的key不可重复
- 可重复: 存进去 一个 1 , 也可以在存储一个 1
单列集合超级父接口Collection
单列集合分为两大类
- List类型的集合: 有序可重复 , 这种类型的集合的元素都有下标
- Set类型的集合: 无序不可重复 , 这种类型的集合的元素都没有下标
- Queue类型的集合: 先进先出(FIFO) , 只能一端进并且在另一端出的队列
Collection中能存放的元素: 没有使用“泛型”之前Collection中可以存储Object的所有子类型。使用了“泛型”之后Collection中只能存储某个指定的类型
- 存放在一个集合中的元素,一定要重写equals方法。因为调用 contains 和 remove 方法时需要调用equals方法判断对象是否相等
单列集合继承结构图
java.util.Collection常用方法
向集合中添加删除元素的方法
方法名 | 功能 |
---|---|
boolean add(Object e) | 向集合中添加元素(默认都是向集合末尾添加元素) , 没有使用“泛型”之前,Collection中可以存储Object的所有子类型 |
boolean addAll(Collection c) | 向集合中添加多个元素(添加一个集合) |
boolean remove(Object o) | 指定删除集合中的某个元素,也可以通过下标指定删除 |
boolean removeAll(Collection c) | 删除集合中的多个元素(删除一个集合) |
Object[] toArray() | 调用这个方法可以把集合转换成数组 , 同时Arrays中也有方法把数组转化成集合 |
对集合中元素的判断
方法名 | 功能 |
---|---|
int size() | 获取集合中元素的个数 |
void clear() | 清空集合的元素 |
boolean contains(Object o) | 判断当前集合中是否包含某个元素,包含返回true, 底层调用该元素的equals方法与集合中的元素一个一个进行比对 |
boolean containsAll(Collection c) | 判断集合中是否包含多个元素 |
boolean isEmpty() | 判断该集合中元素的个数是否为0 |
public class CollectionTest01 {
public static void main(String[] args) {
//利用多态创建一个集合对象。接口是抽象的,无法实例化
Collection c = new ArrayList();
// 测试add方法 ,向集合中添加元素
// 自动装箱(java5的新特性),实际上是放进去了一个对象的内存地址
c.add(1200);
c.add(3.14);
c.add(new Object());
c.add(new Student());
c.add(true);
// 测试size方法,获取集合中元素的个数
System.out.println("集合中元素个数是:" + c.size()); // 5
// 测试clear方法 ,清空集合中的元素
c.clear();
System.out.println("集合中元素个数是:" + c.size()); // 0
// 再向集合中添加元素
c.add("绿巨人");
// 测试contains方法 ,判断集合中是否包含"绿巨人"
boolean flag = c.contains("绿巨人");
System.out.println(flag); // true
boolean flag2 = c.contains("蓝巨人");
System.out.println(flag2); // false
// 测试remove方法,删除集合中某个元素
c.remove("绿巨人");
System.out.println("集合中元素个数是:" + c.size()); // 0
// 判断集合是否为空(集合中是否存在元素)
System.out.println(c.isEmpty()); // true(true表示集合中没有元素了)
// 再向集合中添加元素
// "helloworld!"对象的内存地址放到了集合当中
c.add("helloworld!");
c.add(new Student());
// 测试toArray()方法,将集合转换成数组
Object[] objs = c.toArray();
for(int i = 0; i < objs.length; i++){
// 遍历数组输出集合中的元素
Object o = objs[i];
System.out.println(o);
}
}
}
class Student{
}
深入 contains 和 remove 方法
存放在集合中的元素一定要重写 equals 方法。因为调用 contains 和 remove 方法时需要调用 equals 方法判断对象是否相等
contains 方法是用来判断集合中是否包含某个元素的方法,底层是拿着该元素调用它的 equals 方法与集合中的元素一个一个进行比对
public class CollectionTest04 {
public static void main(String[] args) {
// 创建集合对象
Collection c = new ArrayList();
// 向集合中存储元素
String s1 = new String("abc"); // s1 = 0x99
c.add(s1);
String s2 = new String("def"); // s2 = 0x77
c.add(s2);
// 新建的对象String
String x = new String("abc"); // x = 0x94
// 虽然c集合不包含x对象的内存地址 ,但是调用equals方法判断它和s1相等,所以集合中包含x
System.out.println(c.contains(x)); //true
}
}
remove 方法用来删除对应的集合元素 , 底层也是拿着该元素调用它的 equals 方法与集合中的元素一个一个进行比对
public class CollectionTest05 {
public static void main(String[] args) {
// 创建集合对象
Collection c = new ArrayList();
// 创建用户对象
User u1 = new User("jack");
// 将u1对象加入集合
c.add(u1);
// 创建u2对象
User u2 = new User("jack");
// 删除集合中的u2对象
// 没有重写equals之前,不会删除集合中的u1, 重写equals之后,java认为u1和u2是一样的所以会删除u1
c.remove(u2);
// 元素个数0个
System.out.println(c.size());
}
}
class User{
private String name;
public User(){}
public User(String name){
this.name = name;
}
// 重写equals方法, 这个equals方法的比较原理是:只要姓名一样就表示同一个用户
public boolean equals(Object o) {
if(o == null || !(o instanceof User)) return false;
if(o == this) return true;
User u = (User)o;
return u.name.equals(this.name);
}
}
利用迭代器对象遍历集合
先获取集合对象的迭代器对象Iterator , 通过获取的迭代器对象开始迭代/遍历集合,是所有 Collection 类型的实现类集合通用的一种方式
- 使用foreach的方式对集合中的元素遍历 , 底层也是利用的迭代器原理
Iterable接口中的方法
方法名 | 功能 |
---|---|
Iterator iterator() | 获取 Iterator 类型的迭代器对象 ,对集合中的元素进行迭代 , 当集合的结构发生改变时,迭代器必须重新获取,如果还是用以前老的迭代器会出现异常 |
迭代器对象的方法
方法名 | 功能 |
---|---|
boolean hasNext() | 最初迭代器没有指向任何元素, 判断是否还有元素可以迭代有则返回 true |
Object next() | 返回迭代器指向一个元素 , **在没有指定泛型之前 , 不管你存进去什么的类型元素,取出来元素的编译类型统一都是 Object 类型的, 但是元素的运行类型没有改变 **取元素之前一定要先判断, 如果不调用且下一条记录无效时则会出现异常:java.util.NoSuchElementException |
迭代器对象.remove(o) | 通过迭代器删除迭代器指向的当前元素,会自动更新迭代器和更新集合(删除集合中的元素), 直接通过集合去删除元素,没有通知迭代器(导致迭代器的快照和原集合状态不同) |
当集合的结构发生改变时,迭代器必须重新获取,如果还是用以前老的迭代器会出现异常:java.util.ConcurrentModificationException
- 当我们获取迭代器对象的时候 , 迭代器会对当前集合的状态拍一个快照 , 然后参照这个快照对集合进行迭代 , 并且每次迭代的时候会与集合中的元素状态进行比较 , 如果不一致会出现异常:java.util.ConcurrentModificationException
- 使用集合的 remove 方法删除元素 , 只会删除集合中的元素,但是没有通知迭代器(迭代器不知道集合变化了), 应该重新去获取迭代器
- 使用迭代器的 remove 方法删除元素 , 迭代器知道集合变化了 , 并删除集合中的元素
public class CollectionTest06 {
public static void main(String[] args) {
Collection c = new ArrayList();
// 创建集合
Collection c = new ArrayList();
// 此时获取的迭代器,指向的是集合中没有元素状态下的迭代器
Iterator it = c.iterator();
// 向集合添加中元素 , 集合的状态发生了改变
c.add("abc");
c.add("def");
c.add("xyz");
// 重新获取迭代器,此时指向的是集合中有3个元素状态下的迭代器
Iterator it2 = c2.iterator();
while(it2.hasNext()){
Object o = it2.next();
// 迭代器的remove方法删除迭代器指向的当前元素
it2.remove();
System.out.println(o);
}
System.out.println(c2.size()); //0
}
}
List集合的实现类
List类型的集合存储元素特点:有序可重复
- 有序:List集合中的元素有下标, 从0开始,以1递增。因为有下标,所以List集合有自己比较特殊的遍历方式
- 可重复:存储一个1,还可以再存储1
LIst接口的常用方法
List是Collection接口的子接口,也有自己“特色”的方法
- 对于ArrayList集合添加和删除指定下标位置的元素时涉及到数组中元素的位移导致效率比较低 , 所以方法使用不多
方法名 | 功能 |
---|---|
void add(int index, Object element) | 在集合中的指定下标位置插入指定元素 , 默认都是向集合末尾添加元素 |
void addAll(int index, Collection c) | 在集合中的指定下标位置插入一个集合的元素 , 默认都是向集合末尾添加 |
Object remove(int index) | 删除集合中指定下标位置的元素 |
Object set(int index, Object element) | 修改集合中指定下标位置的元素为另一个对象 , 相当于替换 |
Object get(int index) | 根据下标获取集合中的元素 ,可以通过下标遍历集合【List集合特有的方式,Set没有】 |
int indexOf(Object o) | 获取指定对象在集合中第一次出现处的索引 |
int lastIndexOf(Object o) | 获取指定对象在集合中最后一次出现处的索引 |
List subList(int fromIndex , int toIndex) | 返回从 fromIndex 到 toIndex 位置的一个新集合 , 前闭后开 |
public class ListTest01 {
public static void main(String[] args) {
// 创建任意一种List类型的集合
//List myList = new LinkedList();
//List myList = new Vector();
List myList = new ArrayList();
// 默认都是向集合末尾添加元素 , 效率较高
myList.add("A");
myList.add("B");
myList.add("C");
// 在集合中的指定下标位置插入指定元素,对于ArrayList集合来说效率比较低
myList.add(1, "C");
// 根据下标获取集合中的元素
Object firstObj = myList.get(0);
System.out.println(firstObj);// A
// 通过下标遍历集合【List集合特有的方式,Set没有】
// A C B C
for(int i = 0; i < myList.size(); i++){
Object obj = myList.get(i);
System.out.println(obj);
}
// 获取指定对象在集合中第一次出现处的索引
System.out.println(myList.indexOf("C")); // 1
// 获取指定对象在集合中最后一次出现处的索引
System.out.println(myList.lastIndexOf("C")); // 3
// 删除集合中指定下标位置的元素,对于ArrayList集合来说效率比较低
myList.remove(0);
System.out.println(myList.size()); // 3
// 修改集合中指定下标位置的元素
myList.set(2, "D");
// 通过下标遍历集合
// C B D
for(int i = 0; i < myList.size(); i++){
Object obj = myList.get(i);
System.out.println(obj);
}
}
}
ArrayList集合底层数组原理
ArrayList集合在没有指定泛型前底层是一个Object[]数组
- ArrayList集合的扩容: 增长到原容量的1.5倍 , 因为数组扩容效率比较低,建议在使用ArrayList集合的时候预估计元素的个数,给定一个初始化容量
- ArrayList检索效率最高: 数组的每个元素占用空间大小相同且内存地址是连续的,通过首元素内存地址和指定位置的下标,=可以快速定位元素的内存地址
- ArrayList集合随机增删元素效率比较低: 向数组末尾添加元素效率很高, 无法存储大数据量的数据(很难找到一块非常巨大的连续的内存空间)
- ArrayList集合是非线程安全的
- ArrayList集合可以加入多个null , 并且是多个
构造方法 | 功能 |
---|---|
ArrayList() | 默认数组的初始化容量为10(底层先创建了一个长度为0的数组,当添加第一个元素的时候才会初始化数组的容量) |
ArrayList(int) | 指定数组的初始化容量 |
ArrayList(Collection) | 通过这个构造方法就可以将Collection类型的集合转换成ArrayList类型的集合 |
public class ArrayListTest02 {
public static void main(String[] args) {
// 默认数组的初始化容量为10
List myList1 = new ArrayList();
// 指定数组的初始化容量为100
List myList2 = new ArrayList(100);
// 创建一个HashSet集合
Collection c = new HashSet();
// 添加元素到Set集合
c.add(100);
c.add(200);
c.add(900);
c.add(50);
// 通过这个构造方法就可以将HashSet集合转换成List集合
List myList3 = new ArrayList(c);
for(int i = 0; i < myList3.size(); i++){
System.out.println(myList3.get(i));
}
}
}
LinkedList底层双向链表原理
LinkedList集合底层是双向链表
- LinkedList集合最初没有初始化容量。最初这个链表中没有任何元素。first和last引用都是null
- 链表的优点:由于链表上的元素在空间存储上内存地址不连续。所以随机增删元素的时候不会有大量元素位移,因此随机增删效率较高
- 链表的缺点:LinkedList 集合虽然也有下标,但是检索/查找某个元素的时候效率比较低,因为只能从头节点开始一个一个遍历 ,直到找到为止
public class LinkedListTest01 {
public static void main(String[] args) {
// LinkedList集合底层也是有下标的
List list = new LinkedList();
list.add("a");
list.add("b");
list.add("c");
for(int i = 0; i <list.size(); i++){
Object obj = list.get(i);
System.out.println(obj);
}
}
}
LinkedList底层双向链表添加元素的源码分析
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
Vector集合底层是数组且线程安全
Vector集合底层是一个数组但是效率较低所以使用较少 ,因为我们可以使用集合工具类java.util.Collection将一个线程不安全的ArrayList集合转换成线程安全的
- Vector集合的底层数组每次扩容之后是原容量的2倍
- Vector中所有的方法都是线程同步的,都带有synchronized关键字,是线程安全的 , 但由于其效率比较低所以使用较少了
public class VectorTest {
public static void main(String[] args) {
// 创建一个Vector集合
List vector = new Vector();
//Vector vector = new Vector();
// 添加元素 , 默认初始化容量10个。满了之后扩容(扩容之后的容量是20)
vector.add(1);
Iterator it = vector.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.println(obj);
}
// 将一个线程不安全的ArrayList集合转换成线程安全的
List myList = new ArrayList();
// 变成线程安全的
Collections.synchronizedList(myList);
// myList集合就是线程安全的了。
myList.add("111");
}
}
Set集合的实现类
Set 集合就是一个特殊的 Map 集合,往 Set 集合中放数据时 , 都是放到了 Map 集合的 key 部分
- 无序:Set集合中的元素没有下标, 存进去和取出的顺序不一定相同
- 不可重复:同一个元素添加第二次时虽然没有报错 ,但不会再次存储到集合中
HashSet集合底层是 HashMap
放到 HashSet 集合中的元素等同于放到 HashMap 集合的 Key 部分,该集合存储元素的特点无序不可重复
HashSet集合中的元素需要同时重写hashCode()+equals()方法
public class HashSetTest01 {
public static void main(String[] args) {
// HashSet集合特点: 无序不可重复
Set<String> strs = new HashSet<>();
// 添加元素
strs.add("hello3");
strs.add("hello1");
// 存不进去
strs.add("hello3");
//迭代器遍历
Iterator strs = strs.iterator();
while(strs.hasNext()){
System.out.println(strs.next());
}
// 增强for遍历
for(String s : strs){
System.out.println(s);
}
}
}
TreeSet可排序集合底层是TreeMap
由于TreeSet实现了SortedSet接口 , 集合的 key 可以自动按照大小顺序排序 , 元素要想做到排序包括两种方式
- 第一: 将 key 强制转化为Comparable类型, 调用key实现的compareTo方法(这个方法的返回值决定了元素的排序规则) ,compareTo和equals方法可实现一个
- 第二: 构造TreeMap\TreeSet集合时传入一个比较器Comparator类型的对象, 实现compare方法 , 对集合中的元素进行挨个比较
public class TreeSetTest02 {
public static void main(String[] args) {
// 创建一个TreeSet集合
TreeSet<String> ts = new TreeSet<>();
// 添加String
ts.add("zhangsan");
ts.add("lisi");
// 遍历
for(String s : ts){
// 按照字典顺序,升序
System.out.println(s);
}
// 创建一个TreeSet集合
TreeSet<Integer> ts2 = new TreeSet<>();
// 添加Integer ,底层自动类型转换
ts2.add(100);
ts2.add(200);
ts2.add(10);
for(Integer elt : ts2){
// 升序
System.out.println(elt);
}
}
}
Queue集合的实现类
stack
Vector 很多方法都用了synchronized修饰,线程安全,但效率太低,已被弃用 , Stack 继承Vector,暴露了set/get方法,可以进行随机位置的访问,与Stack 的设计理念相冲突 , Java官方推荐使用Deque替代Stack使用
Stack类表示对象的后进先出(LIFO)堆栈 , 它用五个操作扩展了Vector类,允许将一个向量作为堆栈处理
方法 | 作用 |
---|---|
push(E) | 把元素压栈 |
pop(E) | 把栈顶的元素“弹出” |
peek(E) | 获取栈顶元素但不弹出 |
boolean empty() | 判断栈是否为空 |
int search(Object o) | 查找某个元素在栈中的位置(距离栈顶有多远) |
Stack s = new Stack();
s.push(1);
s.pop();
System.out.println(s);
Queue
Queue是Collection接口的子接口 , 它的特点是先进先出(FIFO) , 只能一端进并且在另一端出的队列
- 除了基本的集合操作之外 , 队列还提供额外的插入,提取和检查的这些方法 , 方法以两种形式存在(抛出异常或者返回特殊值)
抛出异常的方法 | 返回特殊值的方法 | 功能 |
---|---|---|
add(e) | offer(e) , 插入元素失败返回false | 插入 |
remove(e) | poll() , 取出失败返回null | 取出 |
element(e) | peek() , 该元素不存在返回null | 检查 |
LinkedList实现Queue接口 , 内部是通过链表来实现的
Queue queue = new LinkedList();
Deuqe(double ended queue)
Deuqe继承自Queue接口是Java中的双端队列集合类型 , 在队列的两端都能插入和删除元素, 具备普通队列FIFO的功能,也具备了Stack的LIFO功能
- Deque的实现类是LinkedList、ArrayDeque、LinkedBlockingDeque,其中LinkedList是最常用的
双端队列模拟栈和队列
- 模拟队列——FIFO(先进先出)行为: 元素添加和删除的位置是双端队列的不同位置
- 模拟堆栈——LIFO(后进先出): 元素添加和删除的位置是双端队列的相同位置
//普通队列(一端进另一端出)
Queue queue = new LinkedList()或Deque deque = new LinkedList();
//双端队列(两端都可进出)
Deque deque = new LinkedList();
//堆栈
Deque deque = new LinkedList();
Deuqe自己的扩展方法,同样也继承Queue的方法
抛出异常的方法 | 返回特殊值的方法 | 功能 |
---|---|---|
addFirst(e) | offerFirst(e) , 插入元素失败返回false | 将指定的元素插入此双端队列的前面 |
removeFirst(e) | pollFirst() , 取出失败返回null | 取出第一个元素 |
getFirst(e) | peekFirst() , 该元素不存在返回null | 检查第一个元素 |
抛出异常的方法 | 返回特殊值的方法 | 功能 |
---|---|---|
addLast(e) | offerLast(e) , 插入元素失败返回false | 将指定的元素插入此双端队列的后面 |
removeLast(e) | pollLast() , 取出失败返回null | 取出最后一个元素 |
getLast(e) | peekLast() , 该元素不存在返回null | 检查最后一个元素 |
堆栈方法
- 用Deque接口来模拟一个Stack接口时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,那样会破坏栈的本质
方法 | 功能 |
---|---|
push(e) | 把元素压栈 , 从头不添加 |
pop(e) | 把栈顶的元素弹出,从头部删除 |
peek(e) | 取栈顶元素但不弹出, 从头部取出 |
public class Main {
public static void main(String[] args) {
// 默认构造器
Deque<String> deque = new LinkedList<>();
// 从头部开始添加元素,先添加的先到尾部
//此时deque中元素的位置(从头部开始):Java, Like , I
deque.addFirst("I");
deque.addFirst("Like");
deque.addFirst("Java");
System.out.println("---------get-------");
//获取头
System.out.println(deque.getFirst()); // Java
//获取尾
System.out.println(deque.getLast()); // I
System.out.println("--------poll-------");
//弹出栈顶元素
System.out.println(deque.poll());
System.out.println("--------push-------");
//从栈顶开始添加元素
deque.push("LanQiao");
System.out.println(deque.pollFirst());// LanQiao
System.out.println(deque.pollLast()); // I
deque.addFirst("LZP");
System.out.println("--------peek--------");
等价于peekFirst(),取出队头元素,但是队列中还是存在该元素
System.out.println(deque.peek());
System.out.println(deque.peekFirst());
System.out.println(deque.peekLast());
System.out.println(deque.size()); // 2
System.out.println("=======real remove=========");
// poll才是真正的remove
System.out.println(deque.poll());
System.out.println(deque.size());// 1
}
}
实现类结构
ArrayDeque: 数组结构
- 没有容量限制,可根据需求自动进行扩容。无法确定数据量时,后期扩容会影响效率
- 作为栈来使用,效率要高于 Stack,作为队列来使用,效率相较于基于双向链表的LinkedList也要更好一些
- 不支持为null的元素
LinkedList: 链表结构
- 没有容量限制,可根据需求自动进行扩容。无法确定数据量时,有更好表现(在插入和删除时优于ArrayList,随机访问ArrayList更优)
- 作为栈、队列或双端队列
- 插入元素能为null
PriorityQueue类
PriorityQueue实现了Queue接口,队列中的元素默认按元素的比较器方法自然排序 , 或者通过队列构造时提供的Comparator(比较器)在队列实例化的时排序
- 使用数组来存储数据,按照每层从左到右的顺序存放,因此它不允许存入null值 , 而且不支持不可比较的对象,比如用户自定义的类
- 优先队列的头是基于自然排序或者Comparator排序的最小元素
- 如果有多个对象拥有同样的排序,那么就可能随机地取其中任意一个 , 当我们获取队列时,返回队列的头对象
- 优先队列的大小是不受限制的,但在创建时可以指定初始大小。当我们向优先队列增加元素的时候,队列大小会自动增加
- PriorityQueue是非线程安全的,所以Java提供了PriorityBlockingQueue(实现BlockingQueue接口)用于Java多线程环境
构造函数 | 描述 |
---|---|
PriorityQueue() | 使用默认的容量(11)创建一个优先队列 , 元素的顺序规则采用的是自然顺序 (看元素是否实现比较接口) |
PriorityQueue(int 容量) | 使用指定的容量创建一个优先队列 , 元素的顺序规则采用的是自然顺序 |
PriorityQueue(比较器) | 使用默认的容量创建一个优先队列 , 元素的顺序规则采用的是比较器 |
PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
双列集合超级父接口Map
Map集合以key和value这种键值对的方式存储数据: key和value都是存储对象的内存地址 , 其中key起到主导的地位,value是key的一个附属品
- HashMap集合的key部分特点:无序不可重复 , 如果key重复了value会覆盖
双列集合继承结构图
java.util.Map常用方法
操作集合中元素的方法
方法名 | 功能 |
---|---|
V put(K key, V value) | 向Map集合中添加键值对 |
V get(Object key) | 通过key获取value,key不存在返回,返回为nul |
getOrDefault(key ,默认值) | 通过key获取value, 如果key不存在,则返回指定的默认值 |
V remove(Object key) | 通过key删除键值对 ,底层调用key 的equals方法与集合中的其他key一个一个进行比较,相同则删除该键值对 , **放入集合中的自定义类型的元素需要重写equals方法 ** |
void clear() | 清空Map集合 |
Collection< V > values() | 获取Map集合中所有的value,返回一个Collection类型的集合 |
Set< K > keySet() | 获取Map集合所有的key(所有的键是一个Set集合) |
Set<Map.Entry<K,V>>entrySet() | 将Map集合转换成Set集合 |
entrySet() 方法: Map集合转换成的Set集合中的元素的类型是 Map.Entry<K,V> 类型
#Map集合对象
key value
----------------------------
1 zhangsan
2 lisi
3 wangwu
4 zhaoliu
#Set集合对象 ,集合中元素的类型是 Map.Entry 类型
1=zhangsan
2=lisi
3=wangwu
4=zhaoliu
Map.Entry和String一样都是一种类型的名字,只不过Map.Entry是Map中静态内部类,并且这个类声明的时候指定了泛型
public class MyMap {
// 声明一个静态内部类并且指定了泛型, 类名叫做:MyMap.MyEntry<K,V>
public static class MyEntry<K,V> {
// 内部类中的静态方法
public static void m1(){
System.out.println("静态内部类的m1方法执行");
}
// 内部类中的实例方法
public void m2(){
System.out.println("静态内部类中的实例方法执行!");
}
}
public static void main(String[] args) {
// 执行内部类中的静态方法
MyMap.MyEntry.m1();
// 创建静态内部类对象,执行内部类中的实例方法
MyMap.MyEntry mm = new MyMap.MyEntry()
mm.m2();
// 声明一个Set集合,存储的对象类型是String类型
Set<String> set2 = new HashSet<>();
// 声明一个Set集合,存储的对象是MyMap.MyEntry类型,没有指定泛型默认是Object类型
Set<MyMap.MyEntry> set = new HashSet<>();
// 声明一个Set集合,存储的对象是MyMap.MyEntry<Integer, String>类型
Set<MyMap.MyEntry<Integer, String>> set3 = new HashSet<>();
}
}
对集合中的元素判断的方法
方法名 | 功能 |
---|---|
boolean containsKey(Object key) | 判断Map中是否包含某个key |
boolean containsValue(Object value) | 判断Map中是否包含某个value , 底层调用的value的equals方法与集合中其他value一个一个进行比较,有则返回true, **放入集合中的自定义类型的元素需要重写equals方法 ** |
boolean isEmpty() | 判断Map集合中元素个数是否为0 |
int size() | 获取Map集合中键值对的个数 |
public class MapTest01 {
public static void main(String[] args) {
// 创建Map集合对象
Map<Integer, String> map = new HashMap<>();
// 测试put方法,向Map集合中添加键值对
map.put(1, "zhangsan"); // 1在这里进行了自动装箱
map.put(2, "lisi");
map.put(3, "wangwu");
map.put(4, "zhaoliu");
// 测试get方法,通过key获取value
String value = map.get(2);
System.out.println(value);
// 测试size方法,获取键值对的数量
System.out.println("键值对的数量:" + map.size());
// 测试remove方法,通过key删除key-value
map.remove(2);
System.out.println("键值对的数量:" + map.size());
// 测试containsKey方法,判断是否包含某个key
System.out.println(map.containsKey(new Integer(4))); // true
// 判断containsValue方法,判断是否包含某个value
System.out.println(map.containsValue(new String("wangwu"))); // true
// 测试values方法,获取所有的value
Collection<String> values = map.values();
// foreach
for(String s : values){
System.out.println(s);
}
// 测试clear方法,清空map集合
map.clear();
System.out.println("键值对的数量:" + map.size());
// 测试isEmpty()方法,判断集合是否为空
System.out.println(map.isEmpty()); // true
}
}
在JDK8之后,对map新增了getOrDefault()方法
public class Demo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("张三", 23);
map.put("赵四", 24);
map.put("王五", 25);
String age= map.getOrDefault("赵四", 30);
System.out.println(age);// 24,map中存在"赵四",返回其对应值24
String age = map.getOrDefault("刘能", 30);
System.out.println(age);// 30,map中不存在"刘能",返回默认值30
}
}
Map集合的遍历
调用集合的 keySet() 方法获取所有的 key(Set集合),使用迭代器或者foreach的方式遍历Set集合中的每一个key , 通过key获取value
// 向Map集合中添加元素
Map<Integer, String> map = new HashMap<>();
map.put(1, "zhangsan");
map.put(2, "lisi");
// 获取所有的key,所有的key存在一个Set集合
Set<Integer> keys = map.keySet();
// 使用迭代器的方式遍历keys,通过key获取value
Iterator<Integer> it = keys.iterator();
while(it.hasNext()){
// 取出其中一个key
Integer key = it.next();
// 通过key获取value
String value = map.get(key);
System.out.println(key + "=" + value);
}
// 使用foreach的方式遍历key,通过key获取value
for(Integer key : keys){
System.out.println(key + "=" + map.get(key));
}
调用集合的 entrySet() 方法直接把Map集合直接全部转换成Set集合 , 遍历Set集合中的每一个节点对象 , 通过节点对象的方法获取key和value
- 这种方式效率比较高,因为获取key和value都是直接从node对象中获取的属性值,适合于大数据量
// 向Map集合中添加元素
Map<Integer, String> map = new HashMap<>();
map.put(1, "zhangsan");
map.put(2, "lisi");
// 把Map集合转换成Set集合 ,Set集合中元素的类型是:Map.Entry
Set<Map.Entry<Integer,String>> set = map.entrySet();
// 使用迭代器的方式遍历Set集合,每一次取出一个Node(有Key和Value属性)
Iterator<Map.Entry<Integer,String>> it2 = set.iterator();
while(it2.hasNext()){
Map.Entry<Integer,String> node = it2.next();
Integer key = node.getKey();
String value = node.getValue();
System.out.println(key + "=" + value);
}
// 使用foreach的方式遍历Set集合,每一次取出一个Node(有Key和Value属性)
for(Map.Entry<Integer,String> node : set){
System.out.println(node.getKey() + "--->" + node.getValue());
}
Map 集合的实现类
HashMap 集合底层是哈希表
HashMap集合底层是哈希表/散列表的数据结构(数组和单向链表的结合体)
- 数组在查询方面效率很高,单向链表在随机增删方面效率较高,哈希表将以上的两种数据结构融合在一起,充分发挥它们各自的优点
- HashMap底层实际上就是个一维数组 , 这个数组中每一个元素是一个单向链表
- 放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法
HashMap集合的key部分特点:无序,不可重复
- 无序: 因为添加的元素不一定挂到哪个单向链表上
- 不可重复: 调用key的equals方法来保证HashMap集合的key不可重复。如果key重复了value会覆盖
HashMap集合的默认初始化容量是16,默认加载因子是0.75(当HashMap集合底层数组的容量达到75%的时候,数组开始扩容)
- HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,可以达到散列均匀,提高HashMap集合的存取效率
public class HashMapTest01 {
public static void main(String[] args) {
// Integer是key,它的hashCode和equals都重写了。
Map<Integer,String> map = new HashMap<>();
map.put(1111, "zhangsan");
map.put(2222, "zhaoliu");
map.put(2222, "king"); //key重复的时候value会自动覆盖。
System.out.println(map.size()); // 2
// 遍历Map集合
Set<Map.Entry<Integer,String>> set = map.entrySet();
for(Map.Entry<Integer,String> entry : set){
// 验证结果:HashMap集合key部分元素:无序不可重复。
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
}
Map集合中存取元素的源码分析
Map集合存取元素时都是先调用key的hashCode方法确定数组下标,然后再调用key的equals方法一个一个比较(equals方法有可能调用,也有可能不调用)
- put(k,v) 和 get(k) : k.hashCode()方法返回哈希值,哈希值经过哈希算法转换成数组下标。数组下标位置上如果是null,equals不需要执行
- o1和o2的hash值相同,一定在同一个单向链表上。如果o1和o2的hash值不同,但哈希算法执行结束之后转换的数组下标可能相同,发生“哈希碰撞’’
hashCode和equals方法的返回值
- 假设将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了纯单向链表。这种情况我们成为:散列分布不均匀
- 假设将所有的hashCode()方法返回值都设定为不一样的值,会导致底层哈希表就成为一维数组了,没有链表的概念了
- equals方法返回如果是true,hashCode()方法返回的值必须一样(相同的对象必须在同一个单向链表上比较 , 只有这样才能保证其他链表上没有该对象)
静态的内部类HashMap.Node
public class HashMap{
// HashMap底层实际上就是一个数组(一维数组)
Node<K,V>[] table;
static class Node<K,V> {
// 哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函数/算法,可以转换存储成数组的下标)
final int hash;
// 存储到Map集合中的那个key
final K key;
// 存储到Map集合中的那个value
V value;
// 下一个节点的内存地址
Node<K,V> next;
}
}
散列分布均匀需要你重写hashCode()方法时有一定的技巧( 使用 IDEA 一起生成 equals + hashCode 方法 )
- 假设有100个元素,10个单向链表,那么每个单向链表上有10个节点,这是最好的,是散列分布均匀的
- equals方法返回如果是true,hashCode()方法返回的值必须一样(相同的对象必须在同一个单向链表上比较 , 只有这样才能保证其他链表上没有该对象)
public class HashSetTest {
public static void main(String[] args) {
//创建对象
Student s1 = new Student("zhangsan");
Student s2 = new Student("zhangsan");
//重写equals方法之后是true,认为s1和s2相等
System.out.println(s1.equals(s2));
//同一个对象的hashCode()值相同, 不同对象的hashCode()值一般不相同
System.out.println("s1的hashCode=" + s1.hashCode()); //284720968 (重写hashCode之后-1432604525)
System.out.println("s2的hashCode=" + s2.hashCode()); //122883338 (重写hashCode之后-1432604525)
//s1.equals(s2)结果是true表示s1和s2是一样的,那么往HashSet集合中放的话,按说只能放进去1个
Set<Student> students = new HashSet<>();
students.add(s1);
students.add(s2);
//这个结果是2.不符合HashSet集合元素无序不可重复的特点
System.out.println(students.size());
//equals方法返回如果是true,hashCode()方法返回的值必须一样。
}
}
class Student {
private String name;
public Student(String name) {
this.name = name;
}
//构造 + set 和 get 方法....
// equals(如果学生名字一样,表示同一个学生)
// equals方法返回如果是true,hashCode()方法返回的值必须一样 , 这样才能放到同一个单向链表进行比较
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
//保证如果两个对象经过equals比较相同时,那么它们hashCode()方法返回的值一定一样
return Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
HashMap集合 key 部分 和 value 部分允许null , 但是 HashMap 集合的key null值只能有一个
public class HashMapTest03 {
public static void main(String[] args) {
Map map = new HashMap();
// HashMap集合允许key为null
map.put(null, null);
System.out.println(map.size()); // 1
// key重复的话value是覆盖!
map.put(null, 100);
System.out.println(map.size()); //1
// 通过key获取value
System.out.println(map.get(null)); // 100
}
}
Hashtable 集合: 底层是哈希表
Hashtable和HashMap一样,底层都是哈希表数据结构
- Hashtable的初始化容量是11,默认加载因子是:0.75f, Hashtable的扩容是:原容量 * 2 + 1
- Hashtable的key和value都是不能为null的 , 因为其首先就会调用 key 的 hashCode() 方法 ,以及value的 equals 方法
- Hashtable方法都带有synchronized:线程安全的。但是现在线程安全有其它的方案,这个Hashtable对线程的处理导致效率较低,使用较少了。
public class HashtableTest01 {
public static void main(String[] args) {
Map map = new Hashtable();
//map.put(null, "123");
map.put(100, null);
}
}
Properties 集合
Properties 是一个Map集合, 直接继承 Hashtable 底层是哈希表
- key 和 value 只能存储 String 类型的字符串
- Properties 被称为属性类对象。 该集合是线程是安全的 , 使用较多
常用方法
方法名 | 功能 |
---|---|
Properties() | 创建一个 Properties() 集合 |
setProperty | 调用map.put()方法,向集合中存元素 |
getProperty | 调用map.get()方法, 通过key获取value |
public class PropertiesTest01 {
public static void main(String[] args) {
// 创建一个Properties对象
Properties pro = new Properties();
// Properties的存元素方法
pro.setProperty("url", "jdbc:mysql://localhost:3306/bjpowernode");
pro.setProperty("driver","com.mysql.jdbc.Driver");
pro.setProperty("username", "root");
pro.setProperty("password", "123");
// 通过key获取value
String url = pro.getProperty("url");
String driver = pro.getProperty("driver");
String username = pro.getProperty("username");
String password = pro.getProperty("password");
System.out.println(url);
System.out.println(driver);
System.out.println(username);
System.out.println(password);
}
}
TreeMap集合:底层是自平衡二叉树
TreeMap集合的key可以自动按照大小顺序排序 , 元素要想做到排序包括两种方式
- 第一: 将 key 强制转化为 Comparable 类型, 并调用 key 实现的 compareTo 方法。如果元素实现了 compareTo 方法 ,equals方法可以不用重写。
- 第二: 构造 TreeMap\TreeSet 集合时传入一个比较器 Comparator 类型的对象,实现 compare 方法 , 对集合中的元素进行挨个比较
实现 Comparable 接口和 Comparator 接口的选择
- 当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。Comparable是java.lang包下的。
- 如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。Comparator是java.util包下的。
注意: String 和 Integer 以及 Date 类底层都实现了 Comparable 接口
方法名 | 功能 |
---|---|
TreeMap(Comparator c) | 在构造TreeSet或者TreeMap集合的时候给它传一个比较器对象 |
自动排序的比较规则
自定义类型实现Comparable接口后对集合中的元素进行排序
- k.compareTo(t.key) 方法 ,返回0表示元素的 key 部分相同,此时 key 的 value 会覆盖 , 大于或小于0表示按照从大大小或者从小到大的顺序排序
public class TreeSetTest05 {
public static void main(String[] args) {
TreeMap<Vip> vips = new TreeMap<>();
vips.add(new Vip("zhangsi", 20));
vips.add(new Vip("zhangsan", 20));
vips.add(new Vip("king", 18));
vips.add(new Vip("soft", 17));
for(Vip vip : vips){
System.out.println(vip);
}
}
}
//集合中的元素实现 Comparable 接口
class Vip implements Comparable<Vip>{
String name;
int age;
public Vip(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Vip{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
// 需要在这个方法中编写比较的规则 ,方法的返回值决定了排序的顺序
@Override
public int compareTo(Vip v) {
// 年龄相同时按照名字排序。
if(this.age == v.age){
// 姓名是String类型,可以直接比。调用compareTo来完成比较。
return this.name.compareTo(v.name);
} else {
// 年龄不一样
return this.age - v.age;
}
}
}
自定义类型实现Comparator接口后对集合中的元素进行排序
public class TreeSetTest06 {
public static void main(String[] args) {
// 使用匿名内部类的方式创建比较器对象, 这里指定了泛型
TreeSet<WuGui> wuGuis = new TreeSet<>(new Comparator<WuGui>() {
@Override
public int compare(WuGui o1, WuGui o2) {
return o1.age - o2.age;
}
});
wuGuis.add(new WuGui(1000));
wuGuis.add(new WuGui(800));
wuGuis.add(new WuGui(810));
for(WuGui wuGui : wuGuis){
System.out.println(wuGui);
}
}
}
// 乌龟
class WuGui{
int age;
public WuGui(int age){
this.age = age;
}
@Override
public String toString() {
return "小乌龟[" +
"age=" + age +
']';
}
}
// 单独在这里编写一个类实现Comparator接口
class WuGuiComparator implements Comparator<WuGui> {
@Override
public int compare(WuGui o1, WuGui o2) {
// 按照年龄排序
return o1.age - o2.age;
}
}
java.util.Collections
常用静态方法
Collections工具类里面包含了一系列的静态方法 , 用于管理或操作集合(比如排序)
方法名 | 功能 |
---|---|
synchronizedList | 把一个非线程安全的集合转化为一个线程安全的集合 |
sort | 因为 List 集合不会对元素进行排序 , 可以使用sort方法对其排序 , 但是也需要保证List集合中的元素实现了Comparable接口 , 或者排序时传入一个比较器对象 , 对HashSet集合排序时可以先将其转化为List集合 |
public class CollectionsTest {
public static void main(String[] args) {
// ArrayList集合不是线程安全的。
List<String> list = new ArrayList<>();
// 变成线程安全的
Collections.synchronizedList(list);
// 排序
list.add("abf");
list.add("abx");
list.add("abc");
list.add("abe");
Collections.sort(list);
for(String s : list){
System.out.println(s);
}
List<WuGui2> wuGuis = new ArrayList<>();
wuGuis.add(new WuGui2(1000));
wuGuis.add(new WuGui2(8000));
wuGuis.add(new WuGui2(500));
// 注意:对List集合中元素排序,需要保证List集合中的元素实现了:Comparable接口。
Collections.sort(wuGuis);
for(WuGui2 wg : wuGuis){
System.out.println(wg);
}
// 对Set集合怎么排序呢?
Set<String> set = new HashSet<>();
set.add("king");
set.add("kingsoft");
set.add("king2");
set.add("king1");
// 将Set集合转换成List集合
List<String> myList = new ArrayList<>(set);
Collections.sort(myList);
for(String s : myList) {
System.out.println(s);
}
// 这种方式也可以排序。
//Collections.sort(list集合, 比较器对象);
}
}
class WuGui2 implements Comparable<WuGui2>{
int age;
public WuGui2(int age){
this.age = age;
}
@Override
public int compareTo(WuGui2 o) {
return this.age - o.age;
}
@Override
public String toString() {
return "WuGui2{" +
"age=" + age +
'}';
}
}