集合
分类
集合包括Collection单列集合,和Map的双列集合
方法
-
add 吧给定的对象添加到当前集合中,返回值是boolean
-
clear 清空集合中所有的元素
-
remove 把给定的对象在当前集合中删除(参数是要删除的值)
-
contains 判断当前集合中是否包含给定的对象。底层是使用equals方法判断对象是否一致,如果存的是自定义对象,没有重写equals方法,那么默认使用Object类中的equals方法进行判断,而Object类中equals方法,依赖地址值进行判断
collection包含的公共方法
isEmpty 判断当前集合是否为空 size 返回集合中元素的个数/集合的长度
集合的分类:List添加的元素是有序的、可重复、有索引 Set:无序、不重复、无索引
List Set ArrayList LinkedList Vector HashSet TreeSet LinedHashSet
集合遍历的三种方式
1.迭代器遍历:迭代其在遍历集合的时候是不依赖索引的
迭代器:Iterator hasNext() 判断当前位置是否有元素 next()获取当前位置的元素,并将迭代器对象移向下一个位置
public static void main(String[] args) { Collection<String> coll = new ArrayList<>(); coll.add("aaa"); System.out.println(coll); coll.clear(); System.out.println(coll); // 迭代器 // 获取一个迭代器对象,默认指向集合下标为0的位置 Iterator<String> it = coll.iterator(); // 判断是否有下一个对象 boolean flag = it.hasNext(); // 获取当前的值,并将指针移向下一个数据的位置 String str = it.next(); System.out.println(str); // 正常迭代器循环 // 1. 创建迭代器对象,并将指针指向集合内第一个值的位置 Iterator<String> it2 = coll.iterator(); // 2. 循环判断 条件是判断当前指针后是否还有下一个值 while (it2.hasNext()){ // 获取当前指针下的值,并将指针往后移动一个位置 String s = it2.next(); } }
细节:
-
迭代器遍历完毕,指针不会复位
-
循环中只能用一次next方法
-
迭代器遍历时,不能用集合的方法进行增加或者删除
只能使用迭代器的方法删除,不能使用集合的remove方法删除,不能进行添加
-
如果要第二次遍历集合,只能再次获取一个新的迭代器对象
增强for循环遍历
1.增强for的底层就是迭代器,为了简化迭代器的代码书写
2.原理内部就是一个Iterator迭代器
3.所有的单列集合和数组才能用增强for进行遍历
// 增强for循环 // 2. 利用增强for循环进行遍历 // s是第三方变量,再循环的时候一次表示集合中的每一个数据 for (String s : coll){ // 在循环体期间修改s的值,炳辉改变集合中原本的数据 // 如果在循环体内将s的值进行修改,底层的迭代器会将s进行第二次赋值 }
Lambda表达式遍历
// lambda演变 // 1. 匿名内部类 // forEach底层是一个for循环,把得到的每个元素传递给accept方法 coll.forEach(new Consumer<String>() { @Override // public void accept(String s) { System.out.println(s); } }); // lambda表达式 coll.forEach(s -> System.out.println(s));
Collection 是单列集合的顶层集合,所有方法被List和Set系列集合共享
常见的成员方法:add,clear,remove,contains,inEmpty,size
三种通用的遍历方式:迭代器:遍历的过程需要删除元素的时候可以选择,增强for循环,lambda表达式
List系列集合:有序(存和取的顺序一致)、有索引、可重复
增:add(索引,数据):再次集合的指定位置插入指定的元素,原来的元素一次后移
删:remove(索引):直接删除元素/通过索引删除元素 如果调用方法的时候,方法出现了重载的现象,会优先调用与形参与实参相同的方法,可以考虑手动将传入的参数向上转型()封装
改:set(索引,数据) :返回被修改的元素
查:get(索引):
List集合的遍历方式
1.迭代器遍历
2.列表带迭起遍历 previous:返回列表中的前一个元素
在遍历的过程中可以添加元素:使用迭代器本身的添加的方法进行添加元素
3.增强for遍历
4.lambda表达式遍历
5.普通for循环遍历
Set系列集合:
无序:存取顺序不一致
不可重复:可以去除重复
无索引:没有带索引的方法,所以不能使用普通For循环遍历,也不能通过索引来获取元素
实现类
hashSet:无序、不重复、无索引
LinkedHashSet:有序、不重复、无索引
TreeSet:可排序、不重复、无索引
方法:
add(E e):把给定的对象添加到当前集合
clear():清空集合中所有的元素
remove(E e):把给定的对象在当前集合中删除
只能根据对象删除,不能根据索引删除
contains(Object obj) 判断当前集合是否为空
isEmpty():判断当前集合是否为空
size():返回集合中元素的个数/集合的长度
遍历
1.迭代器
2.增强for
3.lambda表达式
HashSet
无序、不重复、无索引
直接使用Collection中的API
LinkedHashSet是HashSet的子类
直接使用Collection中的API
有序、不重复、无索引
TreeSet
遍历的方式:迭代器,增强for,lambda表达式
直接使用Collection中的API
不可重复,无索引,可排序
默认按照从小到大顺序排序
底层是基于红黑树的数据结构实现排序的,增删改查性能都较好
默认遍历就是排序(从小到大)
对于字符或字符串类型,是按照其对应的ASCII码表中的数字升序进行排序
当添加自定义对象类型的数据时,需要手动添加默认的比较大小的规则
默认排序规则 让自定义类去实现Comparable,重写里面的抽象方法,在指定比较规则
public int comparaTo(自定义对象 o){ // 指定排序规则 // 假设以自定义对象的Age属性进行排序 return this.getAge() - o.getAge(); } // this表示当前要参加的元素 //o 表示已经在红黑树存在的元素 // 返回值: // 负数:认为添加的元素是小的,存左边 // 正数:认为要添加的元素是大的,存右边 // 0 :认为当前要添加的元素已经存在,舍弃
比较器排序,创建TreeSet对象的时候,传递比较器Compartor指定规则,默认第一种规则,如果第一种满足不了就第二种
// 实现Comparator接口 重写compare(String o1 , String o2)方法 // 自定义排序规则
如果方式一和方式二同时存在的情况,默认以方式二进行比较
使用场景
如果想要集合中的元素可重复(ArrayList)
如果增删操作多于查询操作(LinkerList)
如果对集合中元素去重(HashSet)
如果想去重和保证存储顺序(LinkedHashSet)效率低于HashSet
堆积各种的元素进行排序(TreeSet)
增强forEach底层最右侧的参数是this 也就是遍历方法的调用者本身
数据结构:计算机存储、组织数据的方式
栈、队列、数组、链表、二叉树、二叉查找树、平衡二叉树、红黑树
栈:后进先出,先进后出
后进先出,先进后出 // 一端开口 / 栈顶元素,一端封闭 栈底元素
数据进入栈模型的过程称为:压/进栈
数据离开栈模型的过程称为:弹/出栈
队列:先进先出,后进后出
两端开口 先进先出,后进后出
数据从后端进入队列模型的过程称为:入队列
数据从前端出队列模型的过程称为:出队列
数组:查询快,增删慢
查询速度快: 查询数据时通过地址值和索引定位,查询任意位置的数据耗时相同,元素在内存中是连续存储的
删除效率低:要将原始数据删除,同时后面的每个数据前移
添加效率低:添加位置后的每个数据后移,再添加元素
链表:查询慢,增删比较快
头节点会记录自身的地址,同时记录下一个数据的地址,每个节点内存储的是一个自身的数据和下一个数据的地址,^表示空地址
链表中的每一个节点是独立的对象,在内存中是不连续的,每个节点包含数据值和下一个节点的地址
单向链表
双向链表:既记录上一个节点的地址,也记录后一个节点的地址
查询第N个元素时,会先判断N距离头部近还是底部近,随后再决定从前往后找还是从后往前找
树
一个节点存储父节点地址,左子结点地址,右子节点地址,没有子节点或者没有父节点则记为null
度:每个节点的字节数量
二叉树中:任意节点的度<=2
树高:树的总层数
根节点:最顶层的节点
左/右子节点:左/右下方的节点
根节点的左/右子树:以根节点的左/右子结点开始,其下面所有的子节点
二叉查找树
每个子节点最多只能有两个子节点
任意节点左子树上的值都小于当前节点
任意节点右子树上的值都大于当前节点
存储规则:
小的存左边
大的存右边
一样的不存
二叉树的遍历方法
1.前序遍历
从根节点开始,按照当前节点,左子节点,右子节点的顺序遍历
2.中序遍历(最终结果是从小到大的顺序)
从根节点开始,按照左子结点,当前节点,右子节点顺序遍历
3.后序遍历
从根节点开始,按照左子结点,有子节点,当前节点顺序遍历
4.层序遍历
从根节点开始,一层一层遍历
二叉查找树弊端:旋转次数多,造成添加时资源的浪费
可能会出现单向链表的情况
平衡二叉树:任意节点左右子树高度差不超过1
通过旋转机制来保证任意左右子树高度差不超过1
确定支点:从添加的节点开始,不断往父节点找不平衡的节点
左旋:
1.以不平衡的点作为支点
2.把支点左旋降级,变成左子结点
当层级比较复杂时:将跟节点的右侧往左拉
原先的右子节点变成新的父节点,并把多余的左子节点让出来,给已经降级的根节点当右子节点
3.晋升原来的右子节点
右旋:
1.以不平衡的点作为支点
2.把支点右旋降级,变成右子结点
当层级比较复杂时:将跟节点的右侧往右拉
原先的左子节点变成新的父节点,并把多余的右子节点让出来,给已经降级的根节点当左子节点
3.晋升原来的右子节点
触发时机:当添加一个节点之后,该树不在是一个平衡二叉树
触发左/右旋的时机
1.左左:当根节点左子树的左子树有节点插入,导致二叉树不平衡 , 一次右旋
2.左右:当根节点左子树的右子树有节点插入,导致二叉树不平衡,先局部左旋,再整体右旋
3.右右:当根节点右子树的右子树有节点插入,导致二叉树不平衡,一次左旋
4.右左:当根节点的右子树的左子树有节点插入,导致二叉树不平衡,先局部右旋,再整体左旋
小结:
二叉树,无规律,查询效率低-->二叉搜索树,容易出现单向链表的情况--->平衡二叉树(本质是一个二叉查找树)
红黑树:增删改查性能都很好
每个节点的组成:父节点地址/值/左右子节点地址/颜色
1.红黑树是一种自平衡的二叉查找树,是计算机科学中用到的一种数据结构、
2.特殊的二叉查找树,红黑树的每一个节点上都有存储位表示节点的颜色
3.每一个节点可以使红或者黑,红黑树不是高度平衡的,他的平衡式通过“红黑规则”进行实现的
是一个二叉查找树
但是不是高度平衡的
条件:特有的红黑规则
红黑规则
1.每一个节点或是红色的,或者是黑色的
2.根节点必须是黑色的
3.如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点是黑色的
4.如果某一个节点是红色的,那么他的子节点必须是黑色的(不能出现两个红色节点相连的情况)
5.对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
后代叶节点:其节点后代中所有的nil的叶节点
简单路径:一路往前
添加节点(默认红色的,效率高)
源码
ArrayList底层原理
1.利用空参创建的集合,在底层创建一个默认长度为0的数组,数组名为elementData,有一个参数为size
2.添加第一个元素时,底层才会创建一个新的长度为10的数组,size既表示元素的个数也表示下次存入数据时的起始位置
3.存满时,会扩容1.5倍,然后将原数组的数据copyOf至新数组
4.如果一次添加多个元素,1.5倍放不下,则新创建的数组的长度以实际为准
Array底层的add方法:
调用add方法需要传入三个参数:参数一E e 表示当前要添加的元素
参数二: Object[] elementData :表示集合底层的数组的名字
参数三:int s:集合的长度/当前元素应存的位置
第一次添加元素
先判断s是否与数组的长度相同,如果相同,则调用grow方法
随后gorw会调用一个有参构造方法grow(size +1)
记录原来的老容量--> 第一次添加元素-->比较老容量与默认长度相比,随后创建一个默认长度为10的数组
第n次添加元素(需扩容的情况)
grow()方法扩容,无参构造方法内会将数组长度+1,表示当前可存入数据后的最小长度
记录原来的老容量-->调用ArraysSupport.newLength(oldCapacity(老容量),minCapacity - oldCapacity(理论上至少要新增的容量) , oldCapacity >> 1(等于Old除于2,默认增加一半));
newLength()方法内部
先将第二个参数和第三个参数进行比较:在集合中,不仅仅是一次添加一个元素,还可以一次添加多个元素
int prefLength = oldLength + Math.max(minGrowth , prefGrowth)
第一种 情况:如果一次添加一个元素,那么第二个参数一定是1,表示此时数组只要扩容一个单位就可以了
第二种情况:如果一次添加多个元素,假设100,那么第二个参数是100,表示此时数组需要扩容100个单位才可以
prefLength:表示数组真正的长度
Arrays.copyOf(elementData,newCapacity)
1.会根据第二个参数创建新的数组
2.把第一个参数中的所有数据,全部拷贝到新数组中
随后返回,再将需要添加的元素添加至新数组中,size+1
LinkedList集合
底层数据结构是双向链表,查询慢,增删快
add默认添加到列表末尾
addFirst 在列表开头插入指定的元素
addLast 将指定的元素追加到此列表尾部
getFirst 返回此列表中的第一个元素
getLast 返回此列表中的最后一个元素
removeFirst 从此列表中删除并返回第一个元素
removeLast 从此列表中删除并返回最后一个元素
底层实现
Node类
E item 表示当前要存储的数据
Node<E> next 表示下一个要存储的地址值
Node<E> prev 表示上一个值的地址值
成员变量
size 表示集合的长度/也表示节点的总个数
first 头节点
last 尾节点
LineLast
第一次调用
先将Last拿出来,赋给一个局部变量L
创建一个节点对象(l为null , e表示当前要添加的元素,) 表示在内存内创建了一个前后节点为空,中间为要添加的元素的值的一个节点对象
将新创建的节点对象的地址值赋给last
如果l == null 则将新节点的地址值赋给first
第二次添加节点
如果l!=null, 那么将l.next重新赋值为新节点的地址值
迭代器底层
默认属性:cursor:光标/指针/ 默认指向0索引位置
lastRet:刚刚操作索引位置/表示上一次操作的索引
int expectedModCount = modCount
checkForComodification
校验现在集合变化的次数和集合创建时变化的次数
ModCoun表示集合变化的次数,调用add/remove次变量都是自增
当我们创建迭代器对象的时候,就会把这个次数高速迭代器
当调用next方法时,会校验这个数和迭代器创建的时候的数是否相同,如果不同则抛出异常
modCount:集合变化的次数
hasNext 判断当前元素后是否还有元素
cursor != size 表示当指针位置与集合长度相同,即为没有下一个元素了
Object[] elementData = ArrayList.this.elementData;
获取集合顶部的数组(地址值)赋值给局部数组
cursor = i+1;移动指针
return (E)elementData[lastRet = i] // 返回数组中的该索引上的元素
HashSet底层原理
1.HashSet集合底层采取哈希表存储数据
哈希表是对于增删改查数据性能都比较好的结构
JDK8之前:数组+链表
JDK8开始:数组+链表+红黑树
Hash值:对象的整数表现形式
1.根据hashCode方法算出来的int类型的整数
2.该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算
3.一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值
对象的HashCode特点
1.如果没有重写hashCode方法,不同对象计算处的哈希值是不同的(因为父类Object是根据地址值来计算hash值的)
2.如果已经重写hashCode方法,不同对象只要属性值相同,计算出来的哈希值就是一样的
3.在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样(hash碰撞)
HashSet底层
1.创建一个默认长度为16,默认加载因子0.75(当数组内拥有的数据数量占数组大小的0.75时开始扩容,扩容至原来数组大小的两倍)的数组,数组名table
2.根据元素的哈希值跟数组的长度计算出应存入的位置(int index = (数组长度 - 1) & 哈希值)
3.判断当前位置是否为null,如果是null直接存入,如果不为null,表示有元素,则调用equals方法比较属性值,如果属性值一样,则会舍弃当前要存入的值,如果不一样,存入数组,形成链表
JDK8之前:新元素存入数组,老元素挂在新元素下面
JDK8以后:新元素直接挂在老元素下面
当链表长度大于8且数组长度大于等于64时,自动将链表转为红黑树
如果集合中存储的是自定义对象,必须要重写hashCode和equals方法
1.HashSet为什么存和取的顺序不一样
因为HashSet是通过数组加链表的形式存储的,所以存取无顺序
2.HashSet为什么没有索引
因为HashSet是通过数组加链表组成的,无法定义索引
3.HashSet是利用什么机制保证数据去重的
hashCode方法和equals方法
LinkedHashSet底层原理
效率比HashSet低
有序
保证存储和取出的元素顺序一致
原理:底层数据结构依然是哈希表,只是每个元素有额外多了一个双链表的机制记录存储的顺序
遍历的时候是先遍历双向链表
底层是基于哈希表,使用双向链表记录添加顺序
泛型
泛型只能支持引用数据类型
不能写基本数据类型,无法转换为Object
在指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型
如果不写泛型,类型默认为Object
如果没有给集合指定类型,默认所有数据类型都是Object类型,此时可以往集合中添加任意类型的数据,获取数据的时候,无法使用它的特有行为
java中的泛型是伪泛型,当集合真正添加的时候还是将限定的泛型转变为Object类型,在取的时候还是会再将Object类型转换为泛型限定的类型
泛型的擦除
把运行时期的问题提前到编译期间,避免了强制类型转换可能出现的异常
泛型的定义:类后面,方法后面,接口后面 , 返回值位置
同一个类中的泛型使用的替代泛型的字母需要全部统一
修饰符 class 类名<类型(泛型)>
返回值类型为泛型的 修饰符 泛型 方法名 方法体
形参为泛型的 修饰符 返回值 方法名(泛型 形参)方法体
泛型方法:在方法声明定义自己的泛型
此时他可以接收任意数据类型的数据
修饰符 <泛型> 返回值类型 方法名(形参)方法体
泛型接口
修饰符 interface 接口名 <泛型>
-
实现类给出具体的类型
-
泛型延续,创建对象时在确定具体的类型
泛型的继承和通配符
如果类型不确定,只能传递某个继承体系中的,可以使用通配符
泛型不具备继承性,但是数据具备继承性
调用泛型的方法,必须和其定义的泛型类型保持一致,即使存在继承关系也不行
? 表示不确定的类型
?extends E 可以传毒E或者E所有的子类类型
? super E 可以传递E或者E所有父类类型
-
细节:
1.泛型中不能写基本数据类型
2.指定泛型的具体类型后,传递数据时,可以传入该类型和他的子类类型
3.如果不写泛型,类型默认是Object