day05【泛型、数据结构、List接口、Set接口】
typora-copy-images-to: imgs
day05【泛型、数据结构、List接口、Set接口】
主要内容
-
泛型
-
数据结构
-
List接口
-
Set接口
第一章 泛型
1 泛型引入
集合是一个容器,可以保存对象。集合中是可以保存任意类型的对象。
List list = new ArrayList();
list.add(“abc”); 保存的是字符串对象
list.add(123); 保存的是Integer对象
list.add(new Person()); 保存的是自定义Person对象
这些对象一旦保存到集合中之后,都会被提升成Object类型。当我们取出这些数据的时候,取出来的时候一定也是以Object类型给我们,所以取出的数据发生多态了。发生多态了,当我们要使用保存的对象的特有方法或者属性时,需要向下转型。而向下转型有风险,我们还得使用 instanceof关键字进行判断,如果是想要的数据类型才能够转换,不是不能强制类型转换,使用起来相对来说比较麻烦。
举例:
现在要使用String类的特有方法,需要把取出的obj向下转成String类型。
String s = (String)obj;
代码如下:
需求:查看集合中字符串数据的长度。
分析和步骤:
1)创建一个ArrayList的集合对象list;
2)使用list集合调用add()函数向集合中添加几个字符串数据和整数数据;
3)迭代集合分别取出集合中的数据,并查看集合中的字符串数据的长度;
代码如下:
package cn.itcast.sh.generic.demo;
import java.util.ArrayList;
import java.util.Iterator;
/*
* 泛型引入
*/
public class GenericDemo1 {
public static void main(String[] args) {
// 创建集合对象
ArrayList list = new ArrayList();
// 向集合中添加数据
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(true);
list.add(123);
//迭代集合
for (Iterator it = list.iterator(); it.hasNext();) {
Object obj = it.next();
/*
* 需求:想使用String类型的特有的函数查看字符串的长度
* Object obj = it.next();上述代码发生多态了,想使用String类中特有的函数必须得强制类型转换
* 可是由于集合可以存储任意类型的对象,而这里只是适合String类型的强制类型转换,其他数据类型会报classCastException类转换
* 异常,如果为了不报异常只能在转换前需要判断,这样做比较麻烦
* 由于这里的需求只是遍历集合后对于取出来的数据是String类型,查看他的长度,其他数据类型不管
* 我们能否想办法不让运行时报错呢,在存储的时候就告诉我,只要是String类型的可以存储,其他数据类型不让存储,这样做起来效率会高一些
*/
String s=(String)obj;
System.out.println(s+"长度是"+s.length());
}
}
}
上述的情况会发生ClassCastException异常。发生这个异常的原因是由于集合中保存了多种不同类型的对象,而在取出的时候没有进行类型的判断,直接使用了强转。
换句话也就是说我们存储的时候,任何类型都让我们存储。
然后我们取的时候,却报错,抛异常。非常不靠谱。你应该在我存的时候就告诉我:锁哥,我只能存字符串,其他引用数据类型不能存储,那么这样我在取出数据的时候就不会犯错了。
假设我们在使用集合的时候,如果不让给集合中保存类型不同的对象,那么在取出的时候即使有向下转型,也不会发生异常。
我们回顾下以前学习的数组容器:
在前面学习数组的时候,我们知道数组这类容器在定义好之后,类型就已经确定,如果保存的数据类型不一致,编译直接报错。
代码举例如下所示:
数组是个容器,集合也是容器,数组可以在编译的时候就能检测数保存的数据类型有问题,如果我们在定义集合的时候,也让集合中的数据类型进行限定,然后在编译的时候,如果类型不匹配就不让编译通过, 那么运行的时候也就不会发生ClassCastException。
要做到在向集合中存储数据的时候限定集合中的数据类型,也就是说编译的时候会检测错误。java中从JDK1.5后提供了一个新的技术,可以解决这个问题:泛型技术。
2 泛型概述
泛型的格式:
<具体的数据类型>
使用格式:
ArrayList<限定集合中的数据类型> list = new ArrayList<限定集合中的数据类型>();
说明:给集合加泛型,就是让集合中只能保存具体的某一种数据类型。
使用泛型改进以上程序中存在的问题:
说明:由于创建ArrayList集合的时候就已经限定集合中只能保存String类型的数据,所以编译的时候保存其他的数据类型就会报错,这样就达到了我们上述保存数据的目的了。
3 使用泛型的好处
上一节只是讲解了泛型的引入,那么泛型带来了哪些好处呢?
-
将运行时期的ClassCastException,转移到了编译时期变成了编译失败。
-
避免了类型强转的麻烦。
通过我们如下代码体验一下:
public class GenericDemo2 {
public static void main(String[] args) {
Collection<String> list = new ArrayList<String>();
list.add("abc");
list.add("itcast");
// list.add(5);//当集合明确类型后,存放类型不一致就会编译报错
// 集合已经明确具体存放的元素类型,那么在使用迭代器的时候,迭代器也同样会知道具体遍历元素类型
Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
//当使用Iterator<String>控制元素类型后,就不需要强转了。获取到的元素直接就是String类型
System.out.println(str.length());
}
}
}
tips:泛型是数据类型的一部分,我们将类名与泛型合并一起看做数据类型。
4 泛型的注意事项
1)泛型只支持引用数据类型(类类型或接口类型等),泛型不支持基本数据类型:
ArrayList<int> list = new ArrayList<int>();//错误的
2)泛型不支持数据类型以继承的形式存在,要求前后泛型的数据类型必须一致:
ArrayList<Object> list = new ArrayList<String>();//错误的
3)在jdk1.7之后,泛型也可以支持如下写法:
ArrayList<String> list = new ArrayList<>();//正确的
注意:现在的开发中,泛型已经成为编写代码的规范。
5 自定义泛型
在集合中,不管是接口还是类,它们在定义的时候类或接口名的后面都使用<标识符>,当我们在使用的时候,可以指定其中的类型。如果当前的类或接口中并没有<标识符>,我们在使用的时候也不能去指定类型。
举例:比如我们之前所学习的集合ArrayList类:
new ArrayList<E>
说明:在ArrayList类上有一个泛型的参数:E
设计API的时候,设计者并不知道我们要用ArrayList存储哪种数据类型,所以他定义了一个泛型。
然后当我们使用ArrayList的时候,我们知道要存储的类型,比如要存储String类型:
ArrayList<String> list = new ArrayList<String>();
当我们new对象的时候,就把泛型E替换成了String,于是JVM就知道我们要存储的其实是String。
泛型:如果我们不确定数据的类型,可以用一个标识符来代替。
如果我们在使用的时候已经确定要使用的数据类型了,我们在创建对象的时候可以指定使用的数据类型。
泛型自定义格式:
<标识符>
这里的标识符可以是任意的字母、数字、下划线和 $ 。但是这里一般规范使用单个大写字母。
注意:自定义泛型也属于标识符,满足标识符的命名规则。1)数字不能开始;2)关键字不能作为标识符;
根据以上分析我们可以思考一个问题:既然我们学习过的集合类可以定义泛型,那么我们自己在描述类或接口的时候,是否也可以在自己的类或接口上定义泛型,然后别人使用的时候也可以指定这个类型呢?
答案当然是可以的。
自定义泛型类(掌握)
泛型类:
在定义类的时候,在类名的后面书写泛型。
格式:
class 类名<泛型参数>
{
}
泛型参数其实就是标识符。
分析和步骤:
1)创建测试类GenericDemo1 ,在这个类中定义一个main函数;
2)定义一个泛型类Demo<ABC>;
3)在这个类中定义一个成员变量name,类型是泛型ABC类型;
4)在定义一个非静态成员函数show,接收参数给name属性赋值,局部变量name的类型是ABC类型;
5)在main函数中创建Demo类的对象,并指定泛型分别是String 和Integer类型;
package cn.itcast.sh.generic.demo1;
/*
* 自定义泛型类演示
* 在类上定义的泛型,当外界创建这个对象的时候,由创建者指定泛型的类型
* 在类上定义的泛型,类中的成员变量和函数都可以使用
*/
class Demo<ABC>
{
ABC name;
public void show(ABC name)
{
this.name=name;
}
}
public class GenericDemo1 {
public static void main(String[] args) {
//创建Demo类的对象
/*
* 注意如果这里创建Demo类的对象时没有指定泛型类的类型时,这里ABC默认是Object类型
* 如下代码所示创建Demo类的对象的时候,指定的泛型类类型是String类型,
* 那么Demo类中的泛型类ABC就是String类
* 同理,如果指定的数据类型是Integer,那么ABC就是Integer类
*/
// Demo<String> d=new Demo<String>();
Demo<Integer> d=new Demo<Integer>();
// d.show("哈哈");
d.show(123);
System.out.println(d.name);
}
}
说明:
1)在类上定义的泛型,当外界在创建这个类的对象的时候,需要创建者自己来明确当前泛型的具体类型;
2)在类上定义的泛型,在类中的方法上和成员变量是可以使用的;
3)上述代码中如果创建Demo类的对象时没有指定泛型类的类型时,那么ABC默认是Object类型;
4)上述代码中如果创建Demo类的对象时指定的泛型类类型是String类型,那么ABC默认是String类型;
注意:对于自定义泛型类只有在创建这个类的对象的时候才可以指定泛型的类型。
在方法上使用泛型(掌握)
我们不仅可以在自己定义的类或接口上使用泛型,还可以在自定义的函数上使用泛型。
虽然可以在类上定义泛型,但是有时类中的方法需要接收的数据类型和类上外界指定的类型不一致。也就是说对于某个函数而言参数的数据类型和所属类的泛型类型不一致了,这时我们可以在这个类中的这个方法上单独给这个方法设定泛型。
在函数上使用泛型的格式:
函数修饰符 <泛型名> 函数返回值类型 方法名( 泛型名 变量名 )
{
函数体;
}
说明:函数返回值类型前面的<泛型名>相当于定义了方法参数列表中泛型的类型。
代码演示如下图所示:
说明:
1)类上的泛型是在创建这个类的对象的时候明确具体是什么类型;
2)方法上的泛型是在真正调用这个方法时,传递的是什么类型,泛型就会自动变成什么类型;
举例:上述代码中当调用者传递的是2,那么这个Q就代表Integer类型。
如果调用者传递的是new Student(“班长”,19),那么这个Q类型就是Student类型。
3)上述method函数中<Q>表示定义的泛型,而参数列表中的Q q是在使用泛型类型,而这里的Q类型具体由调用者来指定;
泛型接口和泛型传递(掌握)
通过查阅API得知,类支持泛型,那么接口也可以支持泛型,比如集合中的接口.
那么既然API中的接口支持泛型,自己定义的接口同样也可以使用泛型。
泛型接口的格式:
修饰符 interface 接口名<泛型>{}
问题:泛型类的泛型,在创建类的对象时确定。
那么接口又无法创建对象,什么时候确定泛型的类型呢?有两种方式可以实现。
方式1:类实现接口的时候,直接明确泛型类型。
方式2:类实现接口的时候,还不确定数据类型,这时可以在实现类上随便先定义个泛型,当这个类被创建对象的时候,
就明确了类上的泛型,于是接口上的泛型也明确了。
我们管方式2这种方式叫做泛型的传递。
代码实现如下:
举例,API中集合的泛型使用情况解释如下所示:
比如:
interface Collection<E>{
}
interface List<E> extends Collection<E>{
}
class ArrayList<E> implements List<E>{
}
ArrayList<String> list = new ArrayList<String>();
结论:通过以上操作,上述集合接口中的泛型类型是String类型。
6泛型通配符
-
格式:
1)<?> :可以表示任何类型
2)<? extends XXX> :表示可以接受XXX和XXX的子类类型(泛型的上限限定)
举例:<? extends Person> :?代表的是一种类型,当前这个类型可以是Person本身,也可以是Person的子类。
3)<? super XXX> :表示可以接受XXX和XXX的父类类型(泛型的下限限定)
举例: <? super Student> :?代表当前的类型可以是Student类型,也可以是Student的父类类型。 -
示例代码:
public class Demo03 {
public static void main(String[] args) {
//创建对象
ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
//调用方法
method(list1);
method(list2);
}
//定义方法
//<?>可以接受任何类型
public static void method(ArrayList<?> l){
}
}public class Demo04 {
public static void main(String[] args) {
//创建集合
ArrayList<Person> list1 = new ArrayList<>();
//创建集合
ArrayList<Student> list2 = new ArrayList<>();
//调用方法
method(list1);
method(list2);
}
//实际接受的是Person或者是Person的子类(泛型的上限)
public static void method(ArrayList<? extends Person> list){
}
}public class Demo05 {
public static void main(String[] args) {
//创建集合
ArrayList<Person> list1 = new ArrayList<>();
//创建集合
ArrayList<Student> list2 = new ArrayList<>();
//调用方法
method(list1);
method(list2);
}
//实际接受的是Student以及Student的父类类型(泛型的下限)
public static void method(ArrayList<? super Student> list){
}
}
7泛型在开发中的使用
类上、方法上、接口上定义泛型,我们今天只是学习写法。在开发中也不需要自己定义泛型。刚入行我们做的只是使用泛型,而不是定义泛型。
第二章 数据结构
数据结构学习网站:
https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
1 数据结构介绍
数据结构 : 数据用什么样的方式组合在一起。就是数据的存储方式。
2 常见数据结构
数据存储的常用结构有:栈、队列、数组、链表和红黑树。我们分别来了解一下:
1栈和队列
栈
-
栈:stack,又称堆栈,它是运算受限的线性表,其限制是仅允许在标的一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。
简单的说:采用该结构的集合,对元素的存取有如下的特点
-
先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。例如,子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先弹出上面的子弹,然后才能弹出下面的子弹。
-
栈的入口、出口的都是栈的顶端位置。
这里两个名词需要注意:
-
压栈:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。
-
弹栈:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。
队列
-
队列:queue,简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行取出并删除。
简单的说,采用该结构的集合,对元素的存取有如下的特点:
-
先进先出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。
-
队列的入口、出口各占一侧。例如,下图中的左侧为入口,右侧为出口。
-
2数组和链表
数组
是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素.就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。
简单的说,采用该结构的集合,对元素的存取有如下的特点:
-
特点:查找快,增删慢
-
查找元素快:通过索引,可以快速访问指定位置的元素
-
增删元素慢
-
指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置。如下图
-
-
指定索引位置删除元素:需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置元素不复制到新数组中。如下图
-
链表
-
链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。我们常说的链表结构有单向链表与双向链表,那么这里给大家介绍的是单向链表。后面讲双向链表。
简单的说,采用该结构的集合,对元素的存取有如下的特点:
-
多个结点之间,通过地址进行连接。例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。
-
-
查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素。
-
增删元素快:
说明:
查找慢:因为每个元素在内存中位置不同,所以查找慢。
增删快:增删时只需要改变前后两个元素的指针指向,对其他元素没有任何影响。
-
3 树基本结构介绍
树结构
计算机中的树结构就是生活中倒立的树。
树具有的特点:
-
每一个节点有零个或者多个子节点
-
没有父节点的节点称之为根节点,一个树最多有一个根节点。类似于生活中大树的树根。
-
每一个非根节点有且只有一个父节点
名词 | 含义 |
---|---|
节点 | 指树中的一个元素(数据) |
节点的度 | 节点拥有的子树(儿子节点)的个数,二叉树的度不大于2,例如:下面二叉树A节点的度是2,E节点的度是1,F节点的度是0 |
叶子节点 | 度为0的节点,也称之为终端结点,就是没有儿子的节点。 |
高度 | 叶子结点的高度为1,叶子结点的父节点高度为2,以此类推,根节点的高度最高。例如下面二叉树ACF的高度是3,ACEJ的高度是4,ABDHI的高度是5. |
层 | 根节点在第一层,以此类推 |
父节点 | 若一个节点含有子节点,则这个节点称之为其子节点的父节点 |
子节点 | 子节点是父节点的下一层节点 |
兄弟节点 | 拥有共同父节点的节点互称为兄弟节点 |
二叉树
如果树中的每个节点的子节点的个数不超过2,那么该树就是一个二叉树。
二叉查找树
上面都是关于树结构的一些概念,那么下面的二叉查找树就是和java有关系的了,那么接下来我们就开始学习下什么是二叉查找树。
二叉查找树的特点:
1. 【左子树】上所有的节点的值均【小于】他的【根节点】的值
2. 【右子树】上所有的节点值均【大于】他的【根节点】的值
3. 每一个子节点最多有两个子树
4. 二叉查找树中没有相同的元素
说明:
1.左子树:根节点左边的部分称为左子树.
2.右子树: 根节点右边的部分称为右子树.
案例演示(20,18,23,22,17,24,19)数据的存储过程;
遍历二叉树有几种遍历方式:
1)前序(根)遍历:根-----左子树-----右子树
2)中序(根)遍历:左子树-----根-----右子树
3)后序(根)遍历:左子树-----右子树-----根
4)按层遍历:从上往下,从左向右
遍历获取元素的时候可以按照"左中右"(中序(根)遍历)的顺序进行遍历来实现数据的从小到大排序;
注意:二叉查找树存在的问题:会出现"瘸子"的现象,影响查询效率
平衡二叉树
概述
为了避免出现"瘸子"的现象,减少树的高度,提高我们的搜素效率,又存在一种树的结构:"平衡二叉树"
规则:它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
如下图所示:
1.如下图所示,左图是一棵平衡二叉树.
举例:
1)根节点10,左右两子树的高度差的绝对值是1。10的左子树有3个子节点(743),10的右子树有2个子节点.所以高度差是1.
2)15的左子树没有节点,右子树有一个子节点(17),两个子树的高度差的绝对值是1.
2.而右图不是一个平衡二叉树,虽然根节点10左右两子树高度差是0(左右子树都是3个子节点),但是右子树15的左右子树高度差为2,不符合定义。15的左子树的子节点为0,右子树的子节点为2.差的绝对值是2。所以右图不是一棵平衡二叉树。
说明:为什么需要平衡二叉树?如下图:
左图是一个平衡二叉树,如果查到左子树的叶子节点需要执行3次。而右图不是一个平衡二叉树,那么查到最下面的叶子节点需要执行5次,相对来说平衡二叉树查找效率更高。
旋转
在构建一棵平衡二叉树的过程中,当有新的节点要插入时,检查是否因插入后而破坏了树的平衡,如果是,则需要做旋转去改变树的结构,变为平衡的二叉树。
各种情况如何旋转:
左左:只需要做一次右旋就变成了平衡二叉树。
右右:只需要做一次左旋就变成了平衡二叉树。
左右:先做一次分支的左旋,再做一次树的右旋,才能变成平衡二叉树。
右左:先做一次分支的右旋,再做一次数的左旋,才能变成平衡二叉树。
课上只讲解“左左”的情况,其余情况都作为扩展去学习,这里只是让你知道怎么旋转即可。
左左
左左即为在原来平衡的二叉树上,在节点的左子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"18"节点的左子树"16",的左子树"13",插入了节点"10"导致失衡。
需求:二叉树已经存在的数据:18 16 20 13 17
后添加的数据是:10
说明:
1.左左:只需要做一次右旋就变成了平衡二叉树。
2.右旋:将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点
扩展(自己去学习)
如果搞不懂可以问老师
左旋:
左旋就是将节点的右支往左拉,右子节点变成父节点,并把晋升之后多余的左子节点出让给降级节点的右子节点;
右旋:
将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点
举个例子,像上图是否平衡二叉树的图里面,左图在没插入前"19"节点前,该树还是平衡二叉树,但是在插入"19"后,导致了"15"的左右子树失去了"平衡",
所以此时可以将"15"节点进行左旋,让"15"自身把节点出让给"17"作为"17"的左树,使得"17"节点左右子树平衡,而"15"节点没有子树,左右也平衡了。如下图,
由于在构建平衡二叉树的时候,当有新节点插入时,都会判断插入后时候平衡,这说明了插入新节点前,都是平衡的,也即高度差绝对值不会超过1。当新节点插入后,
有可能会有导致树不平衡,这时候就需要进行调整,而可能出现的情况就有4种,分别称作左左,左右,右左,右右。
左左
左左即为在原来平衡的二叉树上,在节点的左子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"10"节点的左子树"7",的左子树"4",插入了节点"5"或"3"导致失衡。
左左调整其实比较简单,只需要对节点进行右旋即可,如下图,对节点"10"进行右旋,
左右
左右即为在原来平衡的二叉树上,在节点的左子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2,如上即为"11"节点的左子树"7",的右子树"9",
插入了节点"10"或"8"导致失衡。
左右的调整就不能像左左一样,进行一次旋转就完成调整。我们不妨先试着让左右像左左一样对"11"节点进行右旋,结果图如下,右图的二叉树依然不平衡,而右图就是接下来要
讲的右左,即左右跟右左互为镜像,左左跟右右也互为镜像。
左右这种情况,进行一次旋转是不能满足我们的条件的,正确的调整方式是,将左右进行第一次旋转,将左右先调整成左左,然后再对左左进行调整,从而使得二叉树平衡。
即先对上图的节点"7"进行左旋,使得二叉树变成了左左,之后再对"11"节点进行右旋,此时二叉树就调整完成,如下图:
右左
右左即为在原来平衡的二叉树上,在节点的右子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如上即为"11"节点的右子树"15",的左子树"13",
插入了节点"12"或"14"导致失衡。
前面也说了,右左跟左右其实互为镜像,所以调整过程就反过来,先对节点"15"进行右旋,使得二叉树变成右右,之后再对"11"节点进行左旋,此时二叉树就调整完成,如下图:
右右
右右即为在原来平衡的二叉树上,在节点的右子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"11"节点的右子树"13",的左子树"15",插入了节点
"14"或"19"导致失衡。
右右只需对节点进行一次左旋即可调整平衡,如下图,对"11"节点进行左旋。
红黑树
概述
红黑树是一种自平衡的二叉查找树,是计算机科学中用到的一种数据结构,它是在1972年由Rudolf Bayer发明的,当时被称之为平衡二叉B树,后来,在1978年被Leoj.Guibas和Robert Sedgewick修改为如今的"红黑树"。它是一种特殊的二叉查找树,红黑树的每一个节点上都有存储位表示节点的颜色,可以是红或者黑;
红黑树不是高度平衡的,它的平衡是通过"红黑树的特性"进行实现的;
红黑树的特性:
1. 每一个节点或是红色的,或者是黑色的。
2. 根节点必须是黑色
3. 每个叶节点(Nil)是黑色的;(如果一个节点没有子节点,则该节点相应的指针属性值为Nil,这些 Nil视为叶节点)
4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
5. 对每一个节点,从该节点到其所有后代叶节点的路径上,均包含相同数目的黑色节点
如下图所示就是一个红黑树
在进行元素插入的时候,和之前一样; 每一次插入完毕以后,使用黑色规则进行校验,如果不满足红黑规则,就需要通过变色,左旋和右旋来调整树,使其满足红黑规则;
第三章 List接口
我们掌握了Collection接口的使用后,再来看看Collection接口中的子类,他们都具备那些特性呢?
接下来,我们一起学习Collection中的常用几个子类(java.util.List
集合、java.util.Set
集合)。
1 List接口介绍
java.util.List
接口继承自Collection
接口,是单列集合的一个重要分支,习惯性地会将实现了List
接口的对象称为List集合。在List集合中允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素。另外,List集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。
看完API,我们总结一下:
List接口特点:
-
它是一个元素存取有序的集合。例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
-
它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
-
集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。
tips:我们之前已经学习过List接口的子类java.util.ArrayList类,该类中的方法都是来自List中定义。
2 List接口中常用方法
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:
-
public void add(int index, E element)
: 将指定的元素,添加到该集合中的指定位置上。 -
public E get(int index)
:返回集合中指定位置的元素。 -
public E remove(int index)
: 移除列表中指定位置的元素, 返回的是被移除的元素。 -
public E set(int index, E element)
:用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
List集合特有的方法都是跟索引相关。
代码演示:
public class Demo_List {
public static void main(String[] args) {
//创建对象
//多态写法(之能调用父类中定义的共性方法,不能调用子类中的特有方法)
//Collection<String> c = new ArrayList<>();
//创建List
List<String> list = new ArrayList<>();
//增加
list.add("柳岩");
list.add("美美");
list.add(1,"石原里美");
//删除(返回的是被删除的元素)
list.remove(2); //删掉的是“美美”
//修改(返回的是被替换的元素)
list.set(0, "新垣结衣");//把“柳岩”修改成了“新垣结衣”
//查询
String s = list.get(1);
System.out.println(s); //石原里美
System.out.println(list); //[新垣结衣, 石原里美]
}
}
tips:我们之前学习Colletion体系的时候,发现List集合下有很多集合,它们的存储结构不同,这样就导致了这些集合它们有各自的特点,供我们在不同的环境下使用,那么常见的数据结构有哪些呢?在下一章我们来介绍:
3 ArrayList集合
java.util.ArrayList
集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList
是最常用的集合。
许多程序员开发时非常随意地使用ArrayList完成任何需求,并不严谨,这种用法是不提倡的。
4 LinkedList集合
java.util.LinkedList
集合数据存储的结构是链表结构。方便元素添加、删除的集合。
LinkedList是一个双向链表,那么双向链表是什么样子的呢,我们用个图了解下
说明:
1.LinkedList集合底层是由双向链表组成的
2.双向链表的节点由三部分组成,一部分是数据域存储数据的,一部分是指针域分别存储前一个和后一个节点的地址
3.链表有头和尾组成,我们可以针对链表的头和尾进行操作,可以从链表头或者链表尾开始操作。
实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而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。
LinkedList是List的子类,List中的方法LinkedList都是可以使用,这里就不做详细介绍,我们只需要了解LinkedList的特有方法即可。在开发时,LinkedList集合也可以作为堆栈,队列的结构使用。
代码演示:
public class Demo03_LinkedList {
public static void main(String[] args) {
//创建LinkedList对象
LinkedList<String> list = new LinkedList<>();
list.add("石原");
list.add("里美");
list.add("柳岩");
//void addFirst(E e)
//往开头添加元素
list.addFirst("老王"); //[老王, 石原, 里美, 柳岩]
//void addLast(E e)
//往末尾添加元素
list.addLast("小王"); //[老王, 石原, 里美, 柳岩, 小王]
//E getFirst()
//获取开头的元素
String s = list.getFirst(); //老王
//E getLast()
//获取末尾的元素
String s2 = list.getLast(); //小王
//E removeFirst()
//删除开头的元素
list.removeFirst();
//System.out.println(list); //[石原, 里美, 柳岩, 小王]
//E removeLast()
//删除末尾的元素
list.removeLast();
list.removeLast();
System.out.println(list); //[石原, 里美]
//E pop()
//模拟栈的结构,弹出第一个元素
String pop = list.pop();
System.out.println(pop); //石原
System.out.println(list); //[里美]
//void push(E e)
//模拟栈的结构,推入一个元素
list.push("老王");
System.out.println(list); //[老王, 里美]
}
}
说明:
1.这么多方法感觉有很多是相似的。。其实就是同样的原理换了个名字而已
例如:
1)pop() 方法表示弹出栈结构的第一个元素,而removeFirst()方法也表示删除第一个元素。查看pop方法源码
public E pop() {
return removeFirst();
}
2)add()方法表示向集合最后添加,addLast()也是向集合最后添加。
add()方法源码:
public boolean add(E e) {
//调用linkLast方法添加数据
linkLast(e);
return true;
}
addLast()方法源码:
public void addLast(E e) {
//调用linkLast方法添加数据
linkLast(e);
}
这些方法能看明白作用就可以了。不需要去记忆和研究。
5 LinkedList源码分析
代码演示:
public class Test02 {
public static void main(String[] args) {
//创建LinkedList对象
LinkedList<String> list = new LinkedList<>();
list.add("小王");
list.add("石原");
list.add("里美");
list.add("柳岩");
System.out.println("list = " + list);
}
}
-
LinkedList的源码分析:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
transient int size = 0;
/**
*存储第一个节点的引用
*/
transient Node<E> first;
/**
* 存储最后一个节点的引用
*/
transient Node<E> last;
//......
//LinkedList的内部类Node类源码分析
private static class Node<E> {
E item;//被存储的对象
Node<E> next;//下一个节点地址值
Node<E> prev;//前一个节点地址值
//构造方法
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
//......
//LinkedList的add()方法源码分析
public boolean add(E e) {
linkLast(e);//调用linkLast()方法
return true;//永远返回true
}
void linkLast(E e) {
final Node<E> l = last;//一个临时变量,存储最后一个节点的地址值
/*
这里调用Node类的构造方法:
·1.Node<E> prev = l;将上个节点的地址值赋值给新的节点前面的指针域
2.E element = e;将元素e存储到新的节点的数据域中
3.Node<E> next = null 新的节点作为链表中最后一个节点的下一个指针域为null
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
*/
final Node<E> newNode = new Node<>(l, e, null);//创建一个Node对象
last = newNode;//将新Node对象地址值存储到last
if (l == null)//如果没有最后一个元素,说明当前是第一个节点。l等于null说明集合是空的,还没添 加数据
first = newNode;//将新节点存为第一个节点
else
l.next = newNode;//说明不是第一个节点,将新的节点地址值赋值到上个节点的的next成员
size++;//总数量 + 1
modCount++;//修改一次集合,该变量就会+1
}
}
【扩展,有兴趣自行学习】
-
LinkedList的get()方法:
public E get(int index) {
checkElementIndex(index);//检查索引的合法性(必须在0-size之间),如果不合法,此方法抛出异常
return node(index).item;
}
Node<E> node(int index) {//此方法接收一个索引,返回一个Node
// assert isElementIndex(index);
if (index < (size >> 1)) {//判断要查找的index是否小于size / 2,二分法查找
Node<E> x = first;// x = 第一个节点——从前往后找
for (int i = 0; i < index; i++)//从0开始,条件:i < index,此循环只控制次数
x = x.next;//每次 x = 当前节点.next;
return x;//循环完毕,x就是index索引的节点。
} else {
Node<E> x = last;// x = 最后一个节点——从后往前找
for (int i = size - 1; i > index; i--)//从最后位置开始,条件:i > index
x = x.prev;//每次 x = 当前节点.prev;
return x;//循环完毕,x就是index索引的节点
}
}
第四章 Set接口
java.util.Set
接口和java.util.List
接口一样,同样继承自Collection
接口,它与Collection
接口中的方法基本一致,并没有对Collection
接口进行功能上的扩充,只是比Collection
接口更加严格了。与List
接口不同的是,Set
接口都会以某种规则保证存入的元素不出现重复。
Set
集合有多个子类,这里我们介绍其中的java.util.HashSet
、java.util.LinkedHashSet
、java.util.TreeSet
这两个集合。
tips:Set集合取出元素的方式可以采用:迭代器、增强for。
1 HashSet集合介绍
java.util.HashSet
是Set
接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不能保证不一致)。java.util.HashSet
底层的实现其实是一个java.util.HashMap
支持,由于我们暂时还未学习,先做了解。
HashSet
是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存储和查找性能。保证元素唯一性的方式依赖于:hashCode
与equals
方法。
我们先来使用一下Set集合存储,看下现象,再进行原理的讲解:
public class HashSetDemo {
public static void main(String[] args) {
//创建 Set集合
HashSet<String> set = new HashSet<String>();
//添加元素
set.add(new String("cba"));
set.add("abc");
set.add("bac");
set.add("cba");
//遍历
for (String name : set) {
System.out.println(name);
}
}
}
输出结果如下,说明集合中不能存储重复元素:
cba
abc
bac
tips:根据结果我们发现字符串"cba"只存储了一个,也就是说重复的元素set集合不存储。
2 HashSet集合存储数据的结构(哈希表)
什么是哈希表呢?
在JDK1.8之前,哈希表底层采用数组+链表实现,即使用数组处理冲突,同一hash值的链表都存储在一个数组里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。
看到这张图就有人要问了,这个是怎么存储的呢?
为了方便大家的理解我们结合一个存储流程图来说明一下:
总而言之,JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。
3 HashSet存储自定义类型元素
给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一.
创建自定义Student类:
public class Student {
private String name;
private int age;
//get/set
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
创建测试类:
public class HashSetDemo2 {
public static void main(String[] args) {
//创建集合对象 该集合中存储 Student类型对象
HashSet<Student> stuSet = new HashSet<Student>();
//存储
Student stu = new Student("于谦", 43);
stuSet.add(stu);
stuSet.add(new Student("郭德纲", 44));
stuSet.add(new Student("于谦", 43));
stuSet.add(new Student("郭麒麟", 23));
stuSet.add(stu);
for (Student stu2 : stuSet) {
System.out.println(stu2);
}
}
}
执行结果:
Student [name=郭德纲, age=44]
Student [name=于谦, age=43]
Student [name=郭麒麟, age=23]
4 HashSet的源码分析
4.1 HashSet的成员属性及构造方法
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable{
//内部一个HashMap——HashSet内部实际上是用HashMap实现的
private transient HashMap<E,Object> map;
// 用于做map的值
private static final Object PRESENT = new Object();
/**
* 构造一个新的HashSet,
* 内部实际上是构造了一个HashMap
*/
public HashSet() {
map = new HashMap<>();
}
}
-
通过构造方法可以看出,HashSet构造时,实际上是构造一个HashMap
4.2 HashSet的add方法源码解析
public class HashSet{
//......
public boolean add(E e) {
return map.put(e, PRESENT)==null;//内部实际上添加到map中,键:要添加的对象,值:Object对象
}
//......
}
4.3 HashMap的put方法源码解析
public class HashMap{
//......
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//......
static final int hash(Object key) {//根据参数,产生一个哈希值
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//......
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; //临时变量,存储"哈希表"——由此可见,哈希表是一个Node[]数组
Node<K,V> p;//临时变量,用于存储从"哈希表"中获取的Node
int n, i;//n存储哈希表长度;i存储哈希表索引
if ((tab = table) == null || (n = tab.length) == 0)//判断当前是否还没有生成哈希表
n = (tab = resize()).length;//resize()方法用于生成一个哈希表,默认长度:16,赋给n
if ((p = tab[i = (n - 1) & hash]) == null)//(n-1)&hash等效于hash % n,转换为数组索引
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))))//判断哈希值和equals
e = p;//将哈希表中的元素存储为e
else if (p instanceof TreeNode)//判断是否为"树"结构
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//排除以上两种情况,将其存为新的Node节点
for (int binCount = 0; ; ++binCount) {//遍历链表
if ((e = p.next) == null) {//找到最后一个节点
p.next = newNode(hash, key, value, null);//产生一个新节点,赋值到链表
if (binCount >= TREEIFY_THRESHOLD - 1) //判断链表长度是否大于了8
treeifyBin(tab, hash);//树形化
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//跟当前变量的元素比较,如果hashCode相同,equals也相同
break;//结束循环
p = e;//将p设为当前遍历的Node节点
}
}
if (e != null) { // 如果存在此键
V oldValue = e.value;//取出value
if (!onlyIfAbsent || oldValue == null)
e.value = value;//设置为新value
afterNodeAccess(e);//空方法,什么都不做
return oldValue;//返回旧值
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
}
5 LinkedHashSet
我们知道HashSet保证元素唯一,可是元素存放进去是没有顺序的,那么我们要保证有序,怎么办呢?
在HashSet下面有一个子类java.util.LinkedHashSet
,它是链表和哈希表组合的一个数据存储结构。
演示代码如下:
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<String>();
set.add("bbb");
set.add("aaa");
set.add("abc");
set.add("bbc");
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
结果:
bbb
aaa
abc
bbc