写在前面
笔者是有一年C++基础的大二学生,计划走Java后端开发方向,目前在更新Java学习记录,已经更新完基础语法、面向对象知识,现在开始学习集合部分。本篇博客主要包括Java的几种集合类型(数组、链表、队列、栈、哈希表)、泛型、迭代器
其实本篇博客里的各种集合类型类似于C++中的STL库,都是封装好的某些数据结构。由于笔者是初学者,所以博客里只会记录自己的理解、用法、各类集合的浅显对比,对源码这种底层的东西会略过
List
Java的List代表有序、可重复的集合,可以使用下标访问元素,包括ArrayList、LinkedList、Vector、Stack。
ArrayList
这个类类似于C++的vector类,底层是由Java的数组实现的,本质上也就是一个方便的数组。
创建:
java
//完整写法
ArrayList<数据类型> 集合名 = new ArrayList<数据类型>();
//简化写法
List<数据类型> 集合名 = new ArrayList<>();
增加元素:
java
//在末尾添加元素
数组名.add(要添加的内容(必须符合数据类型))
//指定位置添加元素
数组名.add(指定位置,要添加的内容)
删除元素:
java
//删除指定下标
数组名.remove(下标)
//删除指定元素(如果有多个则只能删除第一个)
数组名.remove(元素)
修改元素:
java
数组名.set(要修改的下标,修改后的新元素)
查找元素
java
//正序查找,返回出现的第一个下标
数组名.indexOf(要查找的内容)
//倒序查找,返回出现的最后一个下标
数组名.lastIndexOf(要查找的内容)
二分查找法:
java
int index = Collections.binarySearch(数组名,要查找的内容);
此方法要求必须是有序的
总结:这就是一个动态数组,可以随意增删改查和访问元素,但是由于底层实现,操作的时间复杂度会有不同
LinkedList
这个类类似于C++的list,底层实现是链表,也是一种有序的数据结构
初始化:
java
LinkedList<类型名> 链表名 = new LinkedList();
增加元素:
java
//最简单的增加
链表名.add(要添加的内容)
//添加到最前边
链表名.addFirst(要添加的内容)
//添加到最后边
链表名.addLast(要添加的内容)
add和addLast在功能上是一样的,但是add更多的体现链表,而addLast和addFirst更多的体现双端队列
删除元素:
java
//删除第一个节点
链表名.remove()
链表名.removeFirst()
//删除指定位置的节点
链表名.remove(位置)
//删除指定元素的节点
链表名.remove(元素名)
//删除最后一个节点
链表名.removeLast()
修改元素:
java
链表名.set(位置,修改后的内容)
查找元素:
java
//查找某个元素所在的位置
链表名.indexOf(元素名)
//查找某个位置上的元素
链表名.get(位置)
ArrayList和LinkedList对比
从表面上来看,这两个线性集合的各种操作调用的方法名都一样,而且都能实现某些功能,但是两者由于底层实现容器不同,各种操作的效率也不同
首先,由于ArrayList是由数组实现的,所以其实ArrayList在内存上是有固定长度的,不过由于其动态扩容机制,才使得长度是动态变化的;而LinkedList是由链表实现的,只有添加上才会往后加一个节点,所以理论上内存无限大的话,链表长度无上限
其次,ArrayList是支持下标快速随机访问的,而链表想要访问某一个位置的元素,必须一个一个遍历过去
关于ArrayList和LinkedList的增删改查效率对比,一句话说清楚它们两个的机制:ArrayList支持O(1)访问,但是增加删除元素要把所有被影响到的元素全部移动一个位置,LinkedList访问元素是从两边向中间查找,但是增加删除元素不影响其他元素。
因此,需要频繁查找时使用ArrayList,需要频繁插入删除时使用LinkedList
Vector
事实上Java也是有Vector这个类的,与C++同名,但是在Java中这个类已经逐步被淘汰了
因为这个类太过古老,而古早的类往往使用老旧的线程安全方式,即全部加锁,这样大大降低了效率,所以被更快的容器取代,也是成为时代的眼泪
Stack
Stack这个类是继承Vector的,所以他也是古早的线程安全的类,现在同样成为时代的眼泪,在Java应用中很少见。
栈的特征是先进后出,后进先出,这里不具体解释数据结构的内容了,不过这个功能同样被效率更高的双端队列ArrayDeque实现了,所以现在使用更多的是双端队列
Queue
Queue是队列的意思,队列也是一种数据结构,特点是先进先出,类似于排队,这里也不细讲数据结构的内容了
Java中实现队列功能的有双端队列ArrayDeque、链表LinkedList、优先队列PriorityQueue
ArrayDeque
ArrayDeque是基于数组写的双端队列,底层实现是一个循环数组
初始化:
java
Deque<数据类型> 队列名 = new ArrayDeque<>();
由于双端队列只能操作两端,所以增删改查的语法都是针对两端的
增加元素:
java
//抛出异常
//在队头添加元素
队列名.addFirst(要添加的内容)
//在队尾添加元素
队列名.addLast(要添加的内容) / add(要添加的内容)
//不抛出异常
//在队头添加元素
队列名.offerFirst(要添加的内容)
//在队尾添加元素
队列名.offerLast(要添加的内容) / offer(要添加的内容)
删除元素:
java
//抛出异常
//删除并返回队首元素
队列名.removeFirst()
//删除并返回队尾元素
队列名.removeLast()
//非抛出异常
//删除并返回队首元素
队列名.pollFirst()
//删除并返回队尾元素
队列名.pollLast()
修改元素:
双端队列不支持任意修改元素,如果想要修改可以先查找到再修改
查找元素:
双端队列只能依靠迭代器遍历的方法访问指定位置,不能通过下标直接访问
但是可以直接访问队首和队尾
java
//抛出异常
//访问队首元素
队列名.getFirst()
//访问队尾元素
队列名.getLast()
//不抛出异常
//访问队首元素
队列名.peekFirst()
//访问队尾元素
队列名.peekLast()
而前文也提到了双端队列可以用作栈,所以还有栈的语法
java
//添加元素
队列名.push(要添加的内容)
//删除元素
队列名.pop()
//访问栈顶元素
队列名.peek()
LinkedList
前文已经讲到了,知道它可以作为队列就行
PriorityQueue
该队列叫做优先队列,就是一个始终按照指定优先级弹出元素的数据结构。
举个例子,将1,2,3,4,5,6插入队列中,如果是按照从大到小排序,那么队头始终是最大值,如果按照从小到大排序,那么队头始终是最小值。
优先队列的底层实现是一种叫堆的数据结构,这种数据结构是支持排序的二叉树,在数据非常多的时候可以大大提升效率,具体原理不在这篇博客讲了,后续可能会更新堆的博客
现在来记录优先队列的语法规则
指定排序规则的初始化方式:
java
PriorityQueue<数据类型> pq3 = new PriorityQueue<>(new Comparator<数据类型>() {
@Override
public int compare(数据类型 o1, 数据类型 o2) {
// 降序排列(最大堆):o2 - o1
// 升序排列(最小堆):o1 - o2(默认)
return o2 - o1;
}
});
自定义排序简化版:
java
PriorityQueue<Integer> pq4 = new PriorityQueue<>((o1, o2) -> o2 - o1);
最简单的初始化:
java
//默认为最小堆
PriorityQueue<Integer> pq1 = new PriorityQueue<>();
增加元素:
java
队列名.add(要添加的内容) / offer(要添加的内容)
删除元素:
java
//移除并返回队头元素
队列名.poll()
//移除指定元素
队列名.remove(元素)
查找元素:
java
//获取队列头元素
队列名.peek()
Map
Map是映射的意思,即一个键对应一个值,不能重复,Map主要包括HashMap、LinkedHashMap、TreeMap
先说明几个术语:哈希函数、哈希冲突
哈希函数是哈希表的实现方式,具体就是使用各种数学定理推出的最佳的计算后取模的方法
至于推理过程可以搜索一些更加专业的文章
哈希冲突是指在一定范围内,算出的哈希值重复了,导致值不能放在键上的情况
处理哈希冲突可以使用拉链法、开放寻址法,具体实现不在这里赘述了,可以搜索专门的文章
HashMap
HashMap的特点:
- HashMap 中的键和值都可以为 null。如果键为 null,则将该键映射到哈希表的第一个位置。
- 可以使用迭代器或者 forEach 方法遍历 HashMap 中的键值对。
- HashMap 有一个初始容量和一个负载因子。初始容量是指哈希表的初始大小,负载因子是指哈希表在扩容之前可以存储的键值对数量与哈希表大小的比率。默认的初始容量是 16,负载因子是 0.75。
它具体实现是依靠哈希函数,将键映射到某一个位置的
哈希方法大致思路就是通过这个元素的地址、大小等等元素获取一个独一无二的hashcode,然后调用哈希函数算出最终的哈希值
哈希方法不止可以用于map,还可以用在许多地方,例如加密算法
就比如忘记密码时App不会提供原来的密码,而是让用户重新修改密码,其实原因是App也不知道密码是什么,它只能验证用户输入的对不对,这就是使用了哈希方法
现在来说HashMap的各种方法
增加元素
java
容器名.put(键,值)
删除元素
java
容器名.remove(键)
修改元素
java
容器名.put(要修改的键,修改后的内容)
因为HashMap是不重复的,所以如果有重复内容的话,会直接覆盖
查找元素
java
容器名.get(键)
LinkedHashMap
LinkedHashMap就是HashMap实现了一个双向链表,然后就实现了按照插入时的顺序来排序元素
使用的语法差不多
TreeMap
TreeMap的底层实现是红黑树,可以实现将map的元素按照自然顺序排序,可以方便地找到最大或最小的元素,语法与HashMap类似
需要注意的是,想要实现排序,元素必须实现 Comparable 接口,或创建 TreeMap 时指定 Comparator 比较器,这个接口在优先队列里讲到了
三种map如何选择?
- 是否需要按照键的自然顺序或者自定义顺序进行排序。如果需要按照键排序,则可以使用 TreeMap;如果不需要排序,则可以使用 HashMap 或 LinkedHashMap。
- 是否需要保持插入顺序。如果需要保持插入顺序,则可以使用 LinkedHashMap;如果不需要保持插入顺序,则可以使用 TreeMap 或 HashMap。
- 是否需要高效的查找。如果需要高效的查找,则可以使用 LinkedHashMap 或 HashMap,因为它们的查找操作的时间复杂度为 O(1),而是 TreeMap 是 O(log n)。
Set
set在C++中才是正统集合的意思,当然他也确实实现的是集合的功能,即存储不重复且无序的元素
Set由Map实现,包括HashSet、LinkedHashSet、TreeSet,自然分别由HashMap、LinkedHashMap、TreeMap实现
HashSet
HashSet在实际编程中其实很尴尬,因为List和Map都可以实现它的功能,它主要可以用于去重,由于底层实现可以自动去重,所以在去重时有用
LinkedHashSet
LinkedHashSet其实就是有序集合,即实现了去重且有序,但是它的有序是插入有序,顺序是插入时的顺序
底层实现了一个双向链表,所以不会像HashSet那样插入就不管了
TreeSet
TreeSet 是一种基于红黑树实现的有序集合,可以自动对插入的元素排序,也是有序集合,但是是排序有序,元素会按照自然顺序排列
Java泛型
Java泛型从功能理解的角度来说可以理解为C++的模板,即不指定类型,仅提供一个可以放进任何类的模板方法,作用是使用类型参数解决元素的不确定问题
举一个具体的例子代码:
java
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Java的泛型中还有两个特性:类型擦除和通配符
类型擦除:
Java的泛型只在编译时起作用,运行时不会保留泛型类型信息
泛型在编译时会将泛型类型擦除,将泛型类型替换成 Object 类型。这是为了向后兼容,避免对原有的 Java 代码造成影响
通配符:
通配符用于表示某种未知的类型,例如 List<?> 表示一个可以存储任何类型对象的 List,但是不能对其中的元素进行添加操作
通配符可以用来解决类型不确定的情况,例如在方法参数或返回值中使用
迭代器
讲到Java的迭代器,必须区分一下Iterator和Iterable,一句话说清楚:Iterable负责标记一个类可以使用迭代器,Iterator是迭代的具体实现方法
继承了Iterable接口的类就可以使用迭代器,而Iterator就是具体的那个遍历的迭代器
语法形式;
java
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.print(it.next() + ",");
}
类似于这种的迭代器遍历,iterator有很多方法可以调用,功能差不多都是访问,递增,递减一类
篇末总结
Java和C++的这些数据结构其实有异曲同工之妙,包括底层实现、继承关系、各种方法都差不多,果然高级程序员优秀的思想总是一致的
有C++的基础,我初步理解底层原理和语法规则都比较容易
其实我并不是很关注语法规则,因为AI时代即便忘记了什么方法,只要问AI就可以了,重要的是知道它有没有这个功能,知道有没有这个功能就可以用了
插一句写这个博客时的纠结:我参考的教学中讲ArrayDeque支持快速随机访问,但是我问过AI之后,不同AI有不同的答案,有的AI说可以使用get方法访问,有的AI说下标和get都不可以,我自己测试之后,发现确实两者都不可以。虽然ArrayDeque的底层实现是数组,但是很可惜,Java更强调它的队列功能所以并没有提供快速随机访问的接口
后续更新并发编程、JVM的内容