一.集合框架
1.为什么会有集合?
集合和数组都是java中提供的可以用来存储多个数据的一种容器.由于数组类型特点是存储同一类型的元素且长度固定,可以存储基本数据类型值.为了满足现实需求, Java中提供可以存储不同引用数据类型的对象(即不同数据结构的数据)且长度可变的数据结构,这样的结构类型就是“集合类型” .在开发中一般当数据对象多的时候,使用集合进行存储.
2.数组和集合的区别:
a、数组的长度固定,集合的长度可自动扩容
b、数组存储的数据是有序且可以重复的,数据类型固定,集合可以存储任意类型 ,集合支持泛型,还可用于保 存具有映射关系的关联数组.
c、数组没有方法,而集合提供大量的方法
d、Java中提供了动态数组集合类型以及其他结构集合类型.
3.集合的框架图
集合按照其存储结构可以分为两大类,分别是单列集合 java.util.Collection 和双列集合 java.util.Map ,现在主要学习Collection 集合,后面再讲解 Map 集合.集合本身是一个工具,它存放在java.util包中.在 Collection 接口中定义着单列集合框架中最最共性的内容.
Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是 java.util.List 和 java.util.Set .其中, List 的特点是元素有序且元素可重复.Set 的特点是元素无序且元素不可重复. List 接口的主要实现类有 java.util.ArrayList 和 java.util.LinkedList , Set 接口的主要实现类有 java.util.HashSet 和 java.util.TreeSet .
接下来通过一张图来描述 整个集合类的继承体系.其中,橙色框里填写的都是接口类型,而蓝色框里填写的都是具体的实现类.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4W4CUky4-1604894287233)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/1591347967566.png)]
使用Iterator遍历器或者foreach循环遍历集合对元素操作时不可同时对集合元素执行添加或者删除操作.
4.集合
4.1Collection集合接口
1.Collection:
是单列集合类的上层接口。本身是一个Interface,里面包含了一些集合的基本操作. Collection接口是Set接口和List接口的父接口
2.Collections
Collections是一个集合框架的帮助类(工具类),里面包含一些对集合的排序,搜索以及序列化的操作.最根本的是Collections是一个类,Collections 是一个包装类,Collection 表示一组对象,这些对象也称为 collection 的元素。一些 collection 允许有重复的元素, 而另一些则不允许,一些 collection 是有序的,而另一些则是无序的
1.常用方法
Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可 用于操作所有的单列集合.
public void add(E):将给定的元素添加到集合中
public void clear():清空集合中的所有元素
public boolean remove(E):删除集合中指定的元素,删除成功返回true
public boolean contains(E):判断该元素是否存在集合中
public boolean isEmpty():判断是否为空集合对象,是则返回true.null会报异常
public int size():获取集合的元素个数
public Object toArray():将集合转成对象数组
ArrayList集合即动态数组实现扩容的过程:
ensureCapacityInternal(size + 1); //可视为改变数组的实际容量即扩容
elementData[size++] = e; //在数组中正确的位置上放上元素e,并且size++
1.获取elementData数组的实际数据个数size,因为要添加一个元素,所以实际元素个数应变为minCapacity=size+1.
2.判断elementData数组是否为空,若为空,则数组的长度为0,minCapacity=1>0存不下,所以将minCapacity变成10,即初始化为默认大小,但是此时还没有真正的初始化elementData数组的大小(长度).(准备工作)
3.判断minCapacity与数组的长度elementData.length的大小关系,若minCapacity>elementData.length,说明elementData数组放不下了,要扩容.
4.将扩容长度newCapacity的大小扩为原长度elementData.length的1.5倍,同时判断newCapacity - minCapacity < 0是否成立,若成立,说明elementData数组是空数组,则直接将minCapacity(=10)赋值给newCapacity,若不成立,则说明原数组不为空即newCapacity=1.5*elementData.length.
5.判断扩容长度newCapacity的值是否超过了最大的容量限制,如果newCapacity超过了最大的容量限制,就调用hugeCapacity将能给的最大值赋值给newCapacity.
6.经过以上步骤,新的容量(动态数组长度)已经确定好了,接着就copy原数组.
7.得到了扩容后的数组elementData后,将指定元素插入到指定位置size(刚好原数组数据个数size就是要插入的位置,同时size++就是扩容后的元素个数).
源码分析参考:https://www.cnblogs.com/zhangyinhua/p/7687377.html#_lab2_0_1
方法调用示例:
public static void main(String[] args) {
//通过接口创建实现类 , 可指定存储的泛型
Collection<String> collection = new ArrayList<String>();
// 集合中指定了元素的类型 String
collection.add("hello"); // 默认添加到末尾
collection.add("hi");
collection.add("哈哈");
System.out.println("元素大小:"+ collection.size());
// 删除集合元素 (后面的原始往前 移动)
collection.remove("hi");
System.out.println("元素大小:"+collection.size());
System.out.println("第一个元素:"+((ArrayList<String>) collection).get(0));
System.out.println("第二个元素:"+((ArrayList<String>) collection).get(1));
// 判断元素是否存在
System.out.println("是否存在哈哈:"+collection.contains("哈哈"));
// 转成数组对象
Object [] objs = collection.toArray();
//遍历元素
for(Object obj : objs){
System.out.println("数组的元素:"+obj);
}
//清空元素 clear
collection.clear();
// 大小
System.out.println("清空后元素的大小(对象依然存在,只能内容为空)"
+collection.size());
// 判断对象中是否是空集合
System.out.println(collection.isEmpty());
}
2.迭代器
Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素.在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素,当第一次调用迭代器的 next方法时,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用next方法时,迭代器的 索引会指向第二个元素并将该元素返回,依此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对 元素的遍历.
// Iterator 集合遍历接口
// 直接对集合元素遍历 泛型只能是包装类
Collection<Integer> scores = new ArrayList<>();
scores.add(90);
scores.add(88);
scores.add(92);
scores.add(91);
//遍历集合 使用 ,
// 在遍历集合时 不能一边遍历集合一边删除集合元素,这样会改变它的遍历的模式
Iterator<Integer> is = scores.iterator();//创建迭代器集合,迭代输出元素
//判断是否有下一个元素 ,如果true ,则可以通过next()获取值
while(is.hasNext()){//判断是否还有下一个元素
Integer score = is.next();next():1.指针下移 2.将下移以后集合位置上的元素返回
System.out.println(score);
}
迭代器遍历时,默认下标都在集合的第一个元素之前
get()方法只有ArrayList有:获取指定位置的元素
设置集合的最低容量,集合的实际容量length()以及集合中的实际数据的个数(有效元素个数)size()是不同的概念(比如:集合容量是5,但是只存了3个元素)
3.for each循环
增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的,无需使用索引访问元素.它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作,但是通过迭代器实现对集合中的元素进行增删操作,Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法而不是集合对象的remove方法.
格式:
for(元素的数据类型 变量 : Collection集合or数组){
//写操作代码
}
注意:新式for循环的目标只能是Collection集合或者是数组.新式for仅仅作为遍历操作出现.
4.2.List集合
习惯性地将实现了 List 接口的对象称为List集合.在List集合中允许出现重复的元素,且元素有序即元素的存入顺序和取出顺序一 致,所有的元素是以一种线性方式进行存储的.
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法(List集合特有的方法都是跟索引相关),如下:
public void add(int index, E element) : 将指定的元素,添加到该集合中的指定位置上。
public E get(int index) :返回集合中指定位置的元素。
public E remove(int index) : 移除列表中指定位置的元素, 返回的是被移除的元素。
public E set(int index, E element) :用指定元素替换指定位置上的元素
1.数组结构集合
ArrayList类
java.util.ArrayList是一个数组结构的集合,它实现了动态数组的功能,扩展了所有List接口的方法.线程不安全.
数组结构的本质: 线性的顺序存储结构**(有序的:元素的存取顺序一样)**,ArrayList中使用连续的内存空间存储数据(即线性存储结构), 访问时通过下标(即元素所在的位置)访问元素,即ArrayList是基于数组实现的List实现类.
ArrayList集合对元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以 ArrayList 是最常用的集合.
ArrayList类的数据结构
分析一个类的时候,数据结构往往是它的灵魂所在,理解底层的数据结构其实就理解了该类的实现思路,具体的实现细节再具体分析。
ArrayList的数据结构是:
说明:底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据.我们对ArrayList类的实例的所有的操作底层都是基于数组的.
源码分析参考:https://www.cnblogs.com/zhangyinhua/p/7687377.html#_lab2_0_1
public static void main(String[] args) {
// 通过ArrayList 创建集合对象
// 还可以存储自定义对象 默认容量是 10
ArrayList<String> list = new ArrayList<String>();
// 存储有序集合
list.add("aaa");
list.add("bbb");
list.add("ccc");
// 将元素插入到指定位置
list.add(1,"ddd");
//直接 遍历元素 get(Index) 通过下标访问元素
for(int i = 0 ;i<list.size();i++){
System.out.println("集合元素:"+ list.get(i));
}
// 设置集合的最小容量
list.ensureCapacity(20);
}
2.链表结构集合
比如LinkedList类,它是一个链表结构的集合.参考链接:https://blog.csdn.net/weixin_36605200/article/details/88804537.
1.单链表结构
1.单链表结构(SingleLinkedList类)
一种链式存取的数据结构,单链表中的数据是以结点的形式存在,每一个结点是由数据元素和下一个结点的存储位置组成.单链表与数组相比的最大差别是:单链表的数据元素存放的地址在内存空间中是不连续的,而数组的数据元素存放的地址在内存空间中是连续的,这也是为什么根据索引无法像数组那样直接就能查询到数据元素的原因,即链表结构不能像数组结构一样快速查找.
单链表结构示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n5EUFcqO-1604894287238)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/屏幕快照 2020-10-26 14.49.44.png)]
前驱:该结点的上一个结点元素的地址
后继:存储的是当前结点的下一个结点的地址
单链表除了最后一个结点外,每一个结点都有一个后继(单向性),最后一个结点(尾结点)的后继为null.
header:头结点,就是链表的第一个结点.
2.单链表的基本操作(单链表的实现)
基本操作:增删查改,以及判断是否为空集合
链表结构操作特点:添加,删除元素的效率高,查询效率低.
数组结构操作特点:添加,删除元素的效率低,查询效率高.
1.添加操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EJGXUZ4L-1604894287240)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/屏幕快照 2020-10-26 18.32.32.png)]
/**
* 添加到最后元素
* @param obj
*/
public void addLast(Object obj){
//将节点添加到最后
//add(obj , this.size);
// 创建节点
// Node node = new Node(obj);
// // 找到最后一个元素的地址
// Node lastNode = this.header;
// for(int i = 0;i<this.size-1 ;i++){
// lastNode = lastNode.next;
// }
//
// lastNode.next=node;
// 找最后一个结点 (由于最后一个结点的next是null)
Node node = new Node(obj);
Node lastNode = this.header;
while(lastNode.next!=null){
lastNode = lastNode.next;
}
lastNode.next = node;
this.size++;
}
2.查询操作
3.删除操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OadqY9CC-1604894287242)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/删除.png)]
/**
* 删除第一个节点
* @param index
* @return
*/
public void removeFirst(){
//删除第一个节点
if(this.size==0){
throw new IllegalArgumentException("没有需要删除的元素");
}
// 获取当前头结点的“后继”
Node node = this.header.next;
// 并让后继作为头
this.header = node;
this.size--;
}
/**
* 删除最后节点
*/
public void removeLast(){
//删除是否存在数据
if(this.size==0){
throw new IllegalArgumentException("没有需要删除的元素");
}
// 找最后一个元素的前一个 地址 ,并将该地址的next 改为null
Node cur = this.header;
Node pre = this.header;
while(cur.next!=null){
pre = cur;
// 下一个变为当前
cur = cur.next;
}
//cur:尾结点,pre:尾结点的前一个结点
// 设置pre为尾结点
pre.next = null;
size--;
}
2.双链表结构
LinkedList类
java.util.LinkedList集合是java.util.List的实现类,实现List接口的所有方法(添加,删除,查找,判断是否空等) ,它添加,删除元素较快,查询相对慢,但是查询头尾元素较快.线程不安全.
在开发时,LinkedList集合也可以作为堆栈,队列的结构使用.
LinkedList集合实现双向链表接口,实现从头元素到尾元素的链表和从尾到头元素的链表,目的是为了增加元素的检索效率 ,即 数据存储结构是双向链表结构.数据结构如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fvRt29ve-1604894287243)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/双向.png)]
实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,关于LinkedList类实现大量操作头元素和尾元素的方法,必须通过LinkedList的引用创建该对象 :
public void addFirst(E e) :将指定元素插入此列表的开头。
public void addLast(E e) :将指定元素添加到此列表的结尾。
public E getFirst() :返回此列表的第一个元素。
public E getLast() :返回此列表的后一个元素。
public E removeFirst() :移除并返回此列表的第一个元素。
public E removeLast() :移除并返回此列表的后一个元素。
public E pop() :从此列表所表示的堆栈处弹出一个元素。
public void push(E e) :将元素推入此列表所表示的堆栈。
public boolean isEmpty() :如果列表不包含元素,则返回true
折半查找
3.Vector实现类
Vector 是一个古老的集合,是LinkedList的子类,JDK1.0就有了,大多数操作与ArrayList 相同,区别之处在于Vector是线程安全的.
在各种list中,最好把ArrayList作为缺省选择,当插入、删除频繁时, 使用LinkedList.Vector总是比ArrayList慢,所以尽量避免使用.
面试题:Vector和ArrayList的最大区别?
Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于 强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用 ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大 小的2倍空间,而ArrayList是1.5倍.Vector还有一个子类Stack.
4.3.Set集合
java.util.Set 接口 继承自Collection接口,实现了对元素的基本操作,它与 Collection 接口中 的方法基本一致,并没有对 Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了 .与List 接口不同的是,Set 接口中元素无序(即存取顺 序不一致),并且都会以某种规则保证存入的元素不出现重复.
HashSet和TreeSet集合底层实际上分别是以HashMap和TreeMap实现的.
Set集合有多个子类,主要介绍实现类 :HashSet 、 LinekedHashSet 、TreeSet .
Set集合取出元素的方式可以采用:迭代器、增强for.
Set 判断两个对象是否相同不是使用 == 运算符,对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”.
1.HashSet
java.util.HashSet 是 Set 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即元素的存取顺序不一致). java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持.不是线程安全的.
父类中的hashcode()方法返回值即哈希码值与有效属性有关.
HashSet集合依据元素的哈希值确定其在内存中的存储位置,所谓Hash值就是内存中哈希表的唯一标志,按Hash 算法得到的,通过哈希值可快速检索到元素在哈希表中的位置 ,因此具有良好的存取,删除和查找性能.保证元素唯一 性的方式依赖于: hashCode 与 equals 方法.与HashSet类似结构的包括HashMap 等.
HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相 等,并且两个对象的 equals() 方法返回值也相等.
HashSet集合存储数据的结构:哈希表
什么是哈希表呢
在JDK1.8之前,哈希表底层采用数组+单向链表实现,即 使用链表处理“Hash冲突”(多个对象生成的哈希值一样的情形),也即同一hash值的对象都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过索引依次查找的效率较低.为了解决由于哈希冲突导致的数据查询效率低下,JDK1.8以后,哈希表存储采用数组+单向链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间.
哈希表结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D06DLL0j-1604894287245)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/12.png)]
哈希表存储流程图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z4ly6NXR-1604894287247)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/屏幕快照 2020-10-28 17.18.30.png)]
向HashSet中添加元素的过程:
1.当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法 来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象 在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在 数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布, 该散列函数设计的越好)
2.如果两个元素的hashCode()值相等,会再继续调用equals方法,如果equals方法结果 为true,添加失败;如果为false,那么会保存该元素,但是该数组的位置已经有元素了, 那么会通过链表的方式继续链接。
3.如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相 等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功。
底层也是数组,初始容量为16,当如果使用率超过0.75,(16*0.75=12) 就会扩大容量为原来的2倍。(16扩容为32,依次为64,128…等).
总而言之,JDK1.8引入红黑树大程度优化了HashMap的性能.根据底层实现可知,那么对于我们来讲保证HashSet集合元素的唯一, 其实就是根据对象的hashCode和equals方法来决定的.如果我们往集合中存放自定义的对象,那么为了保证其唯一, 就必须复写hashCode和equals方法建立属于当前对象的比较方式.
(1)重写 hashCode() 方法的基本原则
1.在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值.
2.当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()
方法的返回值也应相等.
3.对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值
(2)重写 equals() 方法的基本原则
何时需要重写equals()呢?
1.当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),因为只根据一个类的equals方法(改写后),两个截然不 同的实例有可能在逻辑上是相等的,但是根据Object.hashCode()方法,它们仍是两个对象,这就违反了“相等的对象必须具有相等的散列码”的原则.
2.结论:复写equals方法的时候一般都需要同时复写hashCode方法.通 常参与计算hashCode的对象的属性也应该参与到equals()中进行计算.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nn2fXuk8-1604894287248)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/屏幕快照 2020-10-28 19.39.09.png)]
HashSet存储自定义对象类型
给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一
HashSet对于对象是否相同的依据:判断对象的hashCode值和equals是否相等,如果它们相等则判断元素一致,不能重复添加 .
public class People {
private int pid;
private String pname;
private int age;
public People(int pid, String pname, int age) {
this.pid = pid;
this.pname = pname;
this.age = age;
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int hashCode() {//以id作为hashcode值
return this.pid;
}
@Override
public boolean equals(Object obj) {//判断两个对象是否相同
if(this == obj){//内存地址是否相等
return true;
}
if(obj instanceof People){//指定内容即属性值pid和Pname是否相等
People p = (People) obj;
if(p.pid == this.pid && p.getPname().equals(p.getPname())){
return true;
}
}
return false;
}
}
//存储 对象类型
Set<People> sets = new HashSet<>();
People p = new People(1001,"关羽",100);
People p2 = new People(1002,"张飞",100);
People p3 = p2;
System.out.println("p的hashcode:"+p.hashCode());
sets.add(p);
// 检查是否为同一个地址
sets.add(p);
sets.add(p2);
sets.add(p3);
// 插入一个重新new的张飞对象 HashSet以 equals和hashcode的结果作为是否重复对象的依据
People p4 = new People(1002,"张飞",90);
sets.add(p4); // 会当做是重复的对象 ,不能添加成功.
System.out.println("sets的长度:"+sets.size());
for(People obj : sets){
System.out.println(obj.getPid()+"---"+obj.getPname()+"---"+obj.getAge());
}
2.LinkedHashSet
在HashSet中存储的数据是唯一且无序的,如何保证数据的有序性呢?可通过扩展HashSet的子类完成.非线程安全的.
java.util.LinkedHashSet是HashSet的子类 ,它实现有序的Hash结构, 它的底层实现使用双向链表+哈希结构(即数据存储结构), 创建LinkedHashSet时,就是创建一个LinkedHashMap结构.存储元素特点:有序且不可重复.
元素有序的原因:LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置, 但它同时**使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的.**主要由LinkedHashSet.class中重写超类的两个addEntry和createEntry 实现双向链表的结构,保证数据以我们录入的顺序遍历输出.
LinkedHashSet插入性能略低于 HashSet,因为每个节点都需要保存当前节点的next和prev两个属性,这样才能保证优点,所以需要更多的内存开销,并且删除和添加也会比较费时间.但在迭代访问 Set 里的全部元素时有很好的性能.
结构示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EiV59mjl-1604894287250)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/屏幕快照 2020-10-28 19.51.17.png)]
accessOrder = false; 按照插入的顺序存储 accessOrder = true: 按照访问的顺序存储.
// 创建LinkedHashSet对象
LinkedHashSet<String> set = new LinkedHashSet();
set.add("aaa");
set.add("bbb");
set.add("ccc");
set.add("ddd");
//遍历元素
for(String s : set){
System.out.println(s);
}
3.TreeSet
TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态,也包含基础的Set集合功能.存放在TreeSet中的元素是有序的(指按照比较规则得到的大小顺序排序),默认自然升序(默认情况下按照字典中的大小顺序排序),也可以自定义排序规则,称为可排序的集合.
TreeSet底层使用红黑树结构存储数据
TreeSet 的两种自定义排序方法:自然排序和定制排序,默认情况下,TreeSet 采用自然升序.
1.自然排序Comparable
Comparable是排序接口。强行对实现它的每个类的对象进行整体排序,这种排序被称为类的自然排序,类的compareTo方法 被称为它的自然比较方法.若一个类实现了Comparable接口,就意味着该类支持排序。实现了Comparable接口的类的对象的对象列表或数组可以通过Collections.sort或Arrays.sort进行自动排序.此外,实现此接口的对象可以用作有序映射中的键或有序集合中的集合,无需指定比较器.只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序.
实现方法:int compareTo(T o)
比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数.
参数:
o - 要比较的对象。
返回:
负整数、零或正整数,根据此对象是小于、等于还是大于指定对象。
抛出:
ClassCastException - 如果指定对象的类型不允许它与此对象进行比较。
如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口,实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小
向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添 加的所有元素都会调用compareTo()方法进行比较
因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同 一个类的对象
对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通 过 compareTo(Object obj) 方法比较返回值。
当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保 证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过 equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0,否则让人难以理解。
2.定制排序comparator
强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或 有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序.如果在使用的时候,想要独立的定义规则去使用 可以采用Collections.sort(List list,Comparetor c)方式,自己定义规则. Collections.sort(List list) .
TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没 有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照 其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来 实现。需要重写compare(T o1,T o2)方法。
利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表 示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构 造器。
此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异 常。
使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。
特点:有序,查询速度比List快
结构示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-prMA4GBL-1604894287251)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/屏幕快照 2020-10-28 20.19.31.png)]
对红黑树的讲解可以参看:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html
两种方式实现排序规则
1.自然排序:对元素(自定义类)实现 java.lang.Comparable 接口,重写 compareTo方法
public class Fruit implements Comparable<Fruit>{
private int id;
private String name;
public int compareTo(Fruit o) {
// return this.id-o.id; 升序
return o.id - this.id;//this指当前对象(相当于compare方法中的第一个参数o1),o指前一个对象(相当于compare方法中的第二个参数o2)
// 正数: 前一个大于后一个
// 负数: 前一个小于后一个
}
}
// 实现自定义排序规则的方式一 : 对象实现Comparable接口 (java.lang)
// 重写compareTo 方法。
TreeSet<Fruit> fruitSet = new TreeSet<>();
Fruit f1 = new Fruit(100,"苹果");
Fruit f2 = new Fruit(101,"香蕉");
fruitSet.add(f1);
fruitSet.add(f2);
System.out.println(fruitSet.size());
for(Fruit f : fruitSet){
System.out.println(f.getId()+"---"+f.getName());
}
2、通过匿名内部类的方式 在创建TreeSet时,创建定制排序规则 ,new Comparator的接口
// 自定义排序规则的方式二: 对treeSet实现匿名内部类 new Comparator(java.util)
TreeSet<Integer> scores = new TreeSet (new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1; //降序
}
});
// 添加元素
scores.add(80);
scores.add(87);
scores.add(90);
scores.add(78);
for(Integer score : scores){
System.out.println(score);
}
// 按照对象的某一属性降序
TreeSet<People> peopleSet = new TreeSet<>(new Comparator<People>() {
@Override
public int compare(People o1, People o2) {
return o2.getAge()- o1.getAge(); // 根据age降序排列
}
});
peopleSet.add(new People(1001,"张飞",100));
peopleSet.add(new People(1002,"刘备",102));
peopleSet.add(new People(1003,"关羽",108));
for(People p : peopleSet){
System.out.println(p.getPid()+"--"+p.getAge());
}
面试题:comparable和comparator接口的异同:
相同点:它们都可以实现对集合元素的自定义排序
不同点: 1.Comparable接口 属于java.lang包 ,而 Comparator接口属于java.util包
2.它们的方法不同:Comparable定义compareTo(obj) ,Comparator定义compare(obj1,obj2).
3.comparable
4.4Map集合
java.util.Map集合以key-valu键值对的方式存储元素.所有Map集合的key特点:无序且不可重复的(和Set集合存储元素特点相同),一个键对应一个值,其中键在集合中是唯一的, Value可以重复,例如 学号与学生的关系,省份编号对应省份信息.对于Map集合的常用实现类包括 HashMap 、LinkedHashMap、HashTable 、TreeMap 等 .线程不安全.
(1)Map与Collection并列存在,用于保存具有映射关系的数据:key-value
(2)Map 中的 key 和 value 都可以是任何引用类型的数据
(3)Map 中的 key 用Set来存放,不允许重复,即同一个 Map 对象所对应 的类,须重写hashCode()和equals()方法,常用String类作为Map的“键”
(4)key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
(5)Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和 Properties.其中,HashMap是 Map 接口使用频率最高的实现类.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4keAOofW-1604894287254)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/屏幕快照 2020-10-29 08.58.22.png)]
常用方法:
添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据
元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等
元视图操作的方法:
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合
Map map = new HashMap(); //map.put(..,..)省略 System.out.println("map的所有key:"); Set keys = map.keySet();// HashSet for (Object key : keys) {
System.out.println(key + "->" + map.get(key)); }
System.out.println("map的所有的value:"); Collection values = map.values(); Iterator iter = values.iterator(); while (iter.hasNext()) {
System.out.println(iter.next()); }
System.out.println("map所有的映射关系:");
// 映射关系的类型是Map.Entry类型,它是Map接口的内部接口
Set mappings = map.entrySet();
for (Object mapping : mappings) {
Map.Entry entry = (Map.Entry) mapping;
System.out.println("key是:" + entry.getKey() + ",value是:" + entry.getValue()); }
1.HashMap(重点)
java.util.HashMap 存储无序的,键值对数据,HashMap的实现原理在JDK1.8以前使用链表+数组结构,1.8以后使用链表+数组+红黑树结构,使用Hash表的存储方式其检索效率高.JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法) .JDK 8版本发布以后:HashMap是数组+链表+红黑树实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HWXiGb43-1604894287255)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/屏幕快照 2020-10-29 11.37.14.png)]
(1)HashMap是 Map 接口使用频率最高的实现类
(2)允许使用null键和null值,与HashSet一样,不保证映射的顺序
(3)所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写: equals()和hashCode()
(4)所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类 要重写:equals()
(5)一个key-value构成一个entry(目)
(6)所有的entry构成的集合是Set:无序的、不可重复的
(7)HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true, hashCode 值也相等。
(8)HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true
(9)对于相同key 元素,它的value会覆盖原始value
HashMap的常用方法
HashMap()
构造一个空的 HashMap ,默认初始容量(16)和默认负载系数(0.75)。
HashMap(int initialCapacity)
构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)。
HashMap(int initialCapacity, float loadFactor)
构造一个空的 HashMap具有指定的初始容量和负载因子。
HashMap(Map<? extends K,? extends V> m)
构造一个新的 HashMap与指定的相同的映射 Map 。
a、put(K key,V value) : 存储key-value 到容器中
b、V get(K key): 根据key 获取对应的value
c、Set keySet(): 返回所有的key,Set集合
d、boolean containsKey(K key): 判断key是否存在
e、clear():清空容器的元素
f、boolean containsValue(V value):判断value是否存在
g、Collection values() : 返回所有的value集合
h、isEmpty(): 判断是否为空集合
i、remove(Object key) : 根据key删除这个key-value
j、size():返回有效元素个数
k、Set<Map.Entry<Key,Value>> entrySet(): 返回容器的key-value的实体类的集合,方便遍历元素
HashMap的源码分析:
/**
file:///Users/yanjiamin/Desktop/HashMap实现原理及源码分析之JDK8%20-%20娜娜娜娜小姐姐%20-%20博客园.html
**/
// 创建HashMap对象
Map<String , Integer> cards = new HashMap();
//存储
cards.put("红桃",3);
cards.put("黑桃",3);
cards.put("方片",2);
cards.put("梅花",8);
cards.put("红桃",2); // 会覆盖原始的value
//获取指定key的value元素
System.out.println(cards.get("红桃"));
// 获取所有的key
Set<String> keys= cards.keySet();
//通过遍历所有的key 访问对应的value
for(String k : keys){
System.out.println(k+"-----"+cards.get(k));
}
// 判断key 是否存在, 判断value是否存在
System.out.println("是否有红桃:"+cards.containsKey("红桃"));
System.out.println("判断是否有value:"+cards.containsValue(2));
// 获取所有的value
Collection<Integer> values = cards.values();
Iterator its= values.iterator();
while(its.hasNext()){
System.out.println("values ----"+its.next());
}
//获取所有的元素
System.out.println(cards.size());
// 遍历map集合元素 entrySet
Set<Map.Entry<String, Integer>> entrys = cards.entrySet();
for(Map.Entry<String, Integer> en : entrys ){
System.out.println("entry遍历方式:"+en.getKey()+"-----"+en.getValue());
}
// remove
System.out.println("删除元素:"+cards.remove("红桃"));
//清空元素
cards.clear();
System.out.println("元素的大小:"+cards.size());
}
HashMap的原理及源码分析
HashMap基于哈希表存储键值对,以下讲解JDK8的源码.
数据结构:数组+链表+红黑树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OYuMv1Tm-1604894287257)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/屏幕快照 2020-10-29 10.42.25.png)]
HashMap实现步骤:
1.通常,我们把数组中的每个节点(Node<K,V>)称为桶,每次往桶里添加key-value键值对时,首先计算键值对中 key的hash值(采用哈希函数生成),根据hash值确定键值对插入到数组的位置,如果数组里面有数据,就会发生hash冲突,此时按照尾插入法(JDK7及以前是头插入法),添加key-value键值对到同一hash值的元素的后面,链表就这样形成了.
2.当链表长度超过8(TREEIFY_THRESHOLD)时,为了提高查询效率,链表就转换为红黑树结构(树形结构检索效率高).我们通常将桶连接的链表/红黑树中的每个元素称为bin.
HashMap中的一些关键属性和方法
/**
* 扩容的临界值,通过capacity * load factor可以计算出来。超过这个值HashMap将进行扩容
* @serial
*/
int threshold;
/**
* 存储键值对的数组,一般是2的幂
*/
transient Node<K,V>[] table;
/**
* 键值对的实际个数
*/
transient int size;
/**
* 记录HashMap被修改结构的次数。
* 修改包括改变键值对的个数或者修改内部结构,比如rehash
* 这个域被用作HashMap的迭代器的fail-fast机制中(参考ConcurrentModificationException)
*/
transient int modCount;
/**
* HashMap的节点类型。既是HashMap底层数组的组成元素,又是每个单向链表的组成元素
*/
static class Node<K,V> implements Map.Entry<K,V> {
//key的哈希值
final int hash;
final K key;
V value;
//指向下个节点的引用
Node<K,V> next;
}
//增长因子 0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
几个常用方法分析:
1、get(Object)
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
返回目标Node
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 整个数组长度不为空, 且第一个Node不为空 说明已找到对一个的hash位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 表示当前“桶”的第一个元素 的hash值相同,且 key也相同,说明value 是目标查找对象
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 说明一个“桶”中有多个元素, 继续找
if ((e = first.next) != null) {
// 多个元素中 需要先判断是否为 “树”结构,因为超过8个长度就转成了数
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 一定是单链表结构 ,依次从头找到尾,看有没有对应的 key
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
2、put(K,V)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 获取元素的总长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 判断如果当前集合中没有对应的 “桶”,说明没有出现 “Hash碰撞”
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果碰撞了,且桶中的第一个节点就匹配了
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果桶中的第一个节点没有匹配上,且桶内为红黑树结构,则调用红黑树对应的方法插入键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//不是红黑树结构,那么就肯定是链式结构
for (int binCount = 0; ; ++binCount) {
//如果到了链表的尾部 ,插入到后面
if ((e = p.next) == null) {
// 创建一个新节点 创建最后节点 next中
p.next = newNode(hash, key, value, null);
// 如果长度大于 8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 将tab 的桶的所有元素 转成 树结构
treeifyBin(tab, hash);
break;
}
// 如果还没有到达尾部 就找到元素了, 直接返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果找到该元素 ,需要替换它的 value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 修改次数增加
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put插入的流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a4mY0jNe-1604894287259)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/put.png)]
步骤一: 先判断键值对数组是否为空或者长度是否为0,否则执行resize()进行扩容
步骤二: 根据键值key 计算hash值得到插入的数组索引即根据hash值找到对应的数组中的位置,如果该位置为空,说明没有hash冲突,直接将键值对插入哈希表,并长度+1
步骤三:如果该位置的内容不为空,说明产生hash冲突 ,判断该位置上的首个元素的key是否和key相同,如果相同则直接覆盖value(这里的相同指的是hashcode以及equals)
步骤四: 如果不相同,则此时可能出现链表或红黑树, 如果是红黑树采用树结构的插入法插入键值对(省略分析过程).否则一定是链表结构
步骤五: 如果是链表,先判断链表长度是否大于8,若大于8的话则把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作:遍历过程中若发现key已经存在则直接覆盖value即可
步骤六:插入成功之后,判断整个容器的元素个数size是否超出 扩容的临界值(threshold = capacity* 增长因子)(最大容量),若超过了则进行扩容.
关于JDK7.0和JDK8.0的HashMap的区别:
1.数据结构不同:
JDK7.0采用数组+链表实现,JDK8.0采用数组+链表+红黑树实现
2.hash值的计算方式不同:
JDK7.0:在创建HashMap时创建长度为16的数组table
JDK8.0:HashMap首次调用put方法时创建长度为16的数组table
3.发生hash冲突时插入方式不同
JDK7采用头插法,JDK8采用尾插法
4.resize操作方式不同
JDK7重写计算index 的值,JDK8通过判断相应的位是0还是1,要么依旧是原index,要么是oldCap + 原index
2.LinkedHashMap
由于HashMap存储的key是无序的,如果需要存储有序的key可使用LinkedHashMap,它依然满足HashMap的所有特点 ,是有序的HashMap结构.存储 key是有序的,不可重复的键值对数据.
3.TreeMap
TreeMap实现一个可排序的Map集合 ,默认对key升序排列,也可以自定义排序规则. 如果添加元素的key为自定义类,需要实现Comparable接口或Comparator接口。 TreeMap的底层实现是二叉树结构 (有关二叉树的特点 ),实现有序的key的分布.
用于存储可排序的key-value集合.其中key必须是实现了排序规则的对象类型(包装类,String类,自定义类(必须实现comparable或者comparator接口)).默认对key是升序排序.
// TreeMap 用于存储可排序的Key -Value集合 ,
// 其中key必须实现了排序规则的对象 (包装类,String,自定类)
public static void main(String[] args) {
// 存储学号和分数 默认对key 进行升序
// TreeMap<Integer ,Integer> maps = new TreeMap<>();
TreeMap<Integer,Integer> maps = new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1; // 降序
}
});
maps.put(1003,88);
maps.put(1002,90);
maps.put(1004,80);
maps.put(1001,85);
//输出
Set<Integer> keys = maps.keySet();
for(Integer k : keys){
System.out.println(k+"----"+maps.get(k));
}
// 自定义规则
// 注意 ,如果key不是包装类而是自定义,必须要求该类实现Comparable或Comparator接口
TreeMap<Student ,Integer> stuMap = new TreeMap<>();
stuMap.put(new Student(1001,"张飞"),90);
stuMap.put(new Student(1003,"刘备"),87);
stuMap.put(new Student(1002,"关羽"),96);
// 这里降序排列
for(Map.Entry<Student,Integer> en : stuMap.entrySet()){
System.out.println(en.getKey().getSid() + "---"+ en.getValue());
}
}
class Student implements Comparable<Student>{ private int sid; private String sname; public Student(int sid, String sname){ this.sid = sid; this.sname = sname; } public int getSid() { return sid; } public void setSid(int sid) { this.sid = sid; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } @Override public int compareTo(Student o) { return o.sid-this.sid; // this表示前一个对象, o表示后一个对象 }}
4.HashTable
HashTable是实现hash结构的key-value集合,是线程安全的,且不允许存储空的键和值,其它与HashMap相似. HashTable 是线程安全的(它的很多方法是同步操作).
扩展自 Dictionary类 和 实现Map接口
常用方法 :
put ()
get()
clear()
containsKey()
containsValue()
它 有一个子类 是 Properties类,用于存储属性文件的 key- value
public static void main(String[] args) {
//创建HashTable 无序
Hashtable<String,String> tables = new Hashtable<>();
// 存储
tables.put("王宝强","马蓉");
tables.put("贾乃亮","李小璐");
tables.put("文章","马伊琍");
//获取 使用所有key遍历 返回枚举类型
Enumeration<String> keys = tables.keys();
while(keys.hasMoreElements()){
String s = keys.nextElement();
System.out.println(s + "---"+ tables.get(s));
}
// 有一个HashTable的子类 Properties
Properties prop = new Properties();
prop.setProperty("username","张三");
prop.setProperty("password","123456");
//获取对应属性名的值
System.out.println("根据属性名获取值:"+prop.getProperty("username"));
System.out.println("根据属性名获取值:"+prop.getProperty("password"));
}
4.4.队列结构集合
参考链接:https://www.cnblogs.com/soren-wanglu/articles/12035663.html
1.单向队列结构(Queue)
在基于链表结构的基础上 ,实现的一种**“先进先出”**的数据结构, 常用操作 :入队(put),出队(pop)等.设置队列的头结点 和 尾结点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-923Ir4Qq-1604894287261)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/队列结构.png)]
2.单向队列的实现
基本操作:增删查改,以及判断是否为空集合
public class MyQueue<T> {
// 头结点
private Node front;
// 尾结点
private Node tail;
// 队列中的元素个数
private int size;
public MyQueue(){
// 头,尾为空
this.front= this.tail=null;
}
class Node{
private T obj;
private Node next;
public Node(T obj){
this.obj = obj;
}
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
/**
* 入队 : 将元素添加到队列的尾部
*/
public void put(T obj){
// 创建节点
Node node = new Node(obj);
// 如果元素为空 则头就是尾,尾就是头
if(isEmpty()){
this.front = this.tail = node;
return ;
}
// 将新元素的地址 作为尾的next
this.tail.next=node;
//将新元素的结点 作为尾节点
this.tail = node;
this.size++;
}
/**
* 出队: 将元素从队列的头部移除 (保持与队列脱离关系)
* @return
*/
public T pop(){
if(isEmpty()){
throw new IllegalArgumentException("没有弹出的原始");
}
// 移除头部元素
Node popNode = this.front;
// 设置现在的头元素是下一个
this.front = popNode.next;
// 将弹出的元素next 设置null,与队列脱离关系
popNode.next=null;
this.size--;
// 如果没有元素了 则需要 设置头尾都是null
if(this.size<0){
this.front=this.tail=null;
}
return popNode.getObj();
}
/**
* 判断元素是否为空
* @return
*/
public boolean isEmpty(){
if(this.front==null && this.tail==null){
return true;
}
return false;
}
}
4.5.栈结构集合
参考链接:https://blog.csdn.net/weixin_43533825/article/details/96708590
1.栈结构(Stack)
也是常用数据结构之一,它具有 “后进先出” 的特点.具有push()
、pop()
(返回栈顶元素并出栈)、peek()
(返回栈顶元素不出栈)、isEmpty()
这些基本的方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kU1XoDL6-1604894287262)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/栈.png)]
2.栈的实现
基本操作:增删查改,以及判断是否为空集合
(1)采用数组实现栈
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AV46wnL5-1604894287264)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/屏幕快照 2020-10-27 14.59.44.png)]
public class MyStack<T> {
// 定义一个数组 ,用于存储元素
private Object[] obj;
private int size;
public MyStack(){//初始化
obj = new Object[10];//容器容量
size=0;//元素个数
}
/**
* 入栈: 压入栈顶元素
* @param t
*/
public void push(T t){
expandCapacity(size+1);//判断是否需要扩容,需要则扩,不需要则不扩
//以下两步等价于: obj[size++]=t;
obj[size]=t;//不管有没有扩容,t刚好是放在size的位置
size++;//元素个数+1
}
/**
* 返回栈顶元素:peek
*/
public T peek(){
if(size>0) {
return (T) obj[size - 1];
}
return null;
}
/**
* 出栈: 返回栈顶的元素,并删除该元素
* @return
*/
public T pop(){
T t = peek();
if(size>0) {
// 将最后一个元素 删除
obj[size - 1] = null;
size--;
}
return t;
}
/**
* 是否为空元素
* @return
*/
public boolean isEmpty(){
return size==0;
}
/**
* 扩容数组大小 : 扩容1.5倍
*/
public void expandCapacity(int size){
if(obj.length< size){当前容器容量:obj.length,实际所需容量:size
// 需要扩容
int length = size*3/2 + 1;//更新(扩容)当前容器容量是实际所需容量的1.5倍
this.obj = Arrays.copyOf(this.obj,length);//copy到大容器中
}
}
}
(2)采用链表实现栈
head:栈顶结点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rEWwDZzr-1604894287265)(/Users/yanjiamin/Desktop/2020:9软帝/笔记/assets/屏幕快照 2020-10-27 15.28.04.png)]
(3)采用LinkedList实现栈
即单链表结构实现栈
- push-----addFirst()
- pop-------removeFirst()
- peek-----getFirst()
- isEmpty-isEmpty()
5.集合常见面试题
1.Collection 和 Collections 的区别? Array 和Arrays的区别?
Collection是集合的顶级接口 ,Collections是一个集合工具类 ,它提供大量的操作集合方法,例如排序, 打乱顺序,添加元素等.
Array 表示一个数组对象 , Arrays是一个数组工具类 ,提供大量的数组操作方法.
2.List 和 Set 的区别? ArrayList 和 LinkedList的区别
相同点:List、Set 都继承Collection接口
不同点: List 存储不唯一,有序集合元素 , Set 存储唯一,无序的集合元素
ArrayList 实现数组结构集合,查询比较快,添加,删除效率低
LinkedList实现双向链表结构集合,添加,删除效率高, 查询效率低
3、HashSet 和 TreeSet的区别?
它们都存储唯一集合,都实现Set接口
HashSet无序,底层实现Hash结构的存储, TreeSet有序,可实现自定义排序,实现树形结构
4.HashMap 和 HashSet的区别?
它们都属于Hash结构的集合,存储效率较高 , HashSet是存储单个元素 ,HashMap存储键值对元素
HashMap实现Map接口,HashSet实现Set接口 ,
5.HashMap 和HashTable的区别
它们都来自Map 的实现类, HashTable 还继承一个父类 Dictionary
-
HashMap 的key和value 可以为空,HashTable 的key value 不能为空;
-
HashTable 的子类包含key value 的方法
-
HashMap线程不安全效率高 ,HashTable线程安全效率低
6、HashMap 和 TreeMap的区别
它们都实现Map接口 ,TreeMap 有序,HashMap无序, HashMap实现哈希结构 集合,TreeMap实现二叉树集合.
二.JDK8新特性
参看链接:https://www.jianshu.com/p/5b800057f2d8
在JDK8中新增一些特殊功能,方便开发时使用,其中主要的功能如下:
1、接口的默认方法
public interface MyInterface {
public default void defaultMethods(){
System.out.println("这是一个接口的默认方法。");
// 静态方法可以在default方法中调用
staticMethods();
}
public void service();
public static void staticMethods(){
System.out.println("这是接口的静态方法");
}
}
public static void main(String[] args) {
// 创建匿名内部类的方式
MyInterface my = new MyInterface() {
@Override
public void service() {
System.out.println("service方法.....");
}
};
my.defaultMethods();
//通过接口名 调用静态方法
MyInterface.staticMethods();
}
2、Lambda表达式
JDK8中支持一种对方法调用的 简写方式 ,也是一种特殊写法
语法: ([形参列表]) ->{ 方法体}
要求:这个接口中有且只有一个方法即函数式接口,并对方法实现
原始代码
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
// 降序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.compareTo(o1); //降序
}
})
由于JDK能识别sort的参数二是Comparator , 并且Comparator是函数式接口(一个接口中有且只有一个方法。),实现的一定是唯一的方法名, 所有这里方法名和返回值也省略, 它的简写方式
// 使用lambda表达式 ([形参])->{方法体}
Collections.sort(list ,(String o1,String o2)->{
return o2.compareTo(o1);
});
由于形参的数据类与集合的元素类型一致,这里的形参类型也省略 、return也省略
// 最精简的Lambda
Collections.sort(list ,(o1,o2)->o2.compareTo(o1));
3、函数式接口
函数式接口主要用于满足前面Lambda表达式的语法的使用.在一个接口中 有且只有一个方法的接口称为 “函数式接口” ,
如何将一个接口定义为函数式接口呢? 在接口上增加注解 “@FunctionalInterface”
package com.j2008.functionalFun;
@FunctionalInterface // 该注解的意义在于 约束接口只能有一个方法
public interface ConverterInterface<T> {
public String convertStr(T t);
}
//传统写法
ConverterInterface<Integer> con = new ConverterInterface<Integer>() {
@Override
public String convertStr(Integer o) {
return o.toString();
}
};
String ss = con.convertStr(100);
System.out.println(ss);
//使用 lambda表达式写法
ConverterInterface<Date> cons = (o)->o.toLocaleString();
String s = cons.convertStr(new Date());
System.out.println(s);
4、方法和构造器的引用
4.1 方法的引用
在Lambda表达式中,convert方法的实现在java.lang包中是存在相同功能的方法的,所以 convert的实现可以直接引用已有静态方法 Integer.valueOf 或者 Integer.parseInt
以上代码的实现可以改成这样 :
语法: 类名 :: 静态方法名
@FunctionalInterface
public interface Convert<F,T> {
//将F转成T
public T convertType (F f);
}
public static void main(String[] args) {
// 原始使用lambda表达式 可以这样写
// 把字符串转成 Integer
Convert<String ,Integer> con = (f)->{
return Integer.parseInt(f);
};
Integer n = con.convertType("888");
// 在lambda基础上,如果实现的方法 已存在,则可直接调用 类名::方法名
Convert<String ,Integer> con2 = Integer::valueOf;
Convert<String ,Integer> con3= Integer::parseInt;
//调用方法实现
Integer n2 = con2.convertType("123");
int n3 = con3.convertType("555");
}
4.2 构造器的引用
当方法的实现 是使用构造器时,可直接引用构造器
语法: 类名::new
@FunctionalInterface
public interface StudentFactory<T> {
// 参数是创建对象时 的 属性
public T create(int id ,String name);
}
public class Student {
private int sid;
private String sname;
public Student(int sid, String sname) {
this.sid = sid;
this.sname = sname;
}
}
public static void main(String[] args) {
//使用Lambda实现 函数接口的方法
StudentFactory<Student> factory = (id,name)->{
return new Student(id ,name);
};
Student stu1 = factory.create(1001,"张三丰");
System.out.println(stu1);
// 以上写法可以直接换成 引用构造器方式
StudentFactory<Student> factory1 = Student::new;
//创建
Student stu2 = factory1.create(1002,"张无忌");
System.out.println(stu2);
}