今日内容
- Collection集合
- 迭代器
- 泛型
- 数据结构
教学目标
- 能够说出集合与数组的区别
- 能够使用Collection集合的常用功能
- 能够使用迭代器对集合进行取元素
- 能够使用增强for循环遍历集合和数组
- 能够理解泛型上下限
- 能够阐述泛型通配符的作用
- 能够说出常见的数据结构
- 能够说出数组结构特点
- 能够说出栈结构特点
- 能够说出队列结构特点
- 能够说出单向链表结构特点
第一章 Collection集合
1.1 集合概述
在前面我们已经学习过并使用过集合ArrayList ,那么集合到底是什么呢?
- 集合:集合是java中提供的一种容器,可以用来存储多个数据。
集合和数组既然都是容器,它们有什么区别呢?
- 数组的长度是固定的。集合的长度是可变的。
- 数组中存储的是同一类型的元素,可以存储任意类型数据。集合存储的都是引用数据类型。如果想存储基本类型数据需要存储对应的包装类型。
存储整数:int[] 存储字符串:String[]
存储整数:ArrayList<Integer> 存储字符串:ArrayList<String>
1.2 集合常用类的继承体系
Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是java.util.List
和java.util.Set
。其中,List
的特点是元素存取有序、元素可重复 ; Set
的特点是元素存取无序,元素不可重复。List
接口的主要实现类有java.util.ArrayList
和java.util.LinkedList
,Set
接口的主要实现类有java.util.HashSet
和java.util.LinkedHashSet
。
从上面的描述可以看出JDK中提供了丰富的集合类库,为了便于初学者进行系统地学习,接下来通过一张图来描述集合常用类的继承体系
注意:这张图只是我们常用的集合有这些,不是说就只有这些集合。
集合本身是一个工具,它存放在java.util包中。在Collection
接口定义着单列集合框架中最最共性的内容。
1.3 Collection 常用功能
Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:
public boolean add(E e)
: 把给定的对象添加到当前集合中 。public void clear()
:清空集合中所有的元素。public boolean remove(E e)
: 把给定的对象在当前集合中删除。public boolean contains(Object obj)
: 判断当前集合中是否包含给定的对象。public boolean isEmpty()
: 判断当前集合是否为空。public int size()
: 返回集合中元素的个数。public Object[] toArray()
: 把集合中的元素,存储到数组中
tips: 有关Collection中的方法可不止上面这些,其他方法可以自行查看API学习。
public class Demo02ListMethod {
public static void main(String[] args) {
//多态写法
Collection<String> c = new ArrayList<>();
//boolean add(E e)
//添加方法(添加成功会返回true,添加失败会返回false.但是ArrayList只会成功不会失败所以返回永远是true,不接受这个返回值)
c.add("石原里美");
c.add("新垣结衣");
c.add("石原里美");
//void clear()
//清空集合中的元素
//c.clear(); //调用后集合被清空
//boolean remove(Object e)
//删除集合中的某个元素(返回的是true或false代表删除成功或失败,只会删除第一个匹配的元素)
c.remove("柳岩"); //没有柳岩,删除没作用
c.remove("石原里美"); //集合中有两个“石原里美”,会删除第一个
//boolean contains(Object obj)
//判断集合是否包含某个元素
boolean b = c.contains("石原里美");
System.out.println(b); //集合中包含有“石原里美”这个元素所以结果是true
//boolean isEmpty()
//判断集合是否为空
boolean b2 = c.isEmpty();
System.out.println(b2); //集合里面有元素,不为空 结果是false
//int size()
//获取集合的长度
int size = c.size();
System.out.println(size); //2
//Object[] toArray()
//把集合转成Object[]类型
Object[] arr = c.toArray();
//打印数组
System.out.println(Arrays.toString(arr));
//如果想要转成具体的类型,用到了一个高级写法,我现在可以给演示一下,不讲原理
String[] strs = c.toArray(new String[c.size()]);
System.out.println(Arrays.toString(strs));
}
}
第二章 Iterator迭代器
2.1 Iterator接口
在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator
。
想要遍历Collection集合,那么就要获取该集合迭代器完成迭代操作,下面介绍一下获取迭代器的方法:
方法 | 说明 |
---|---|
public Iterator iterator() | 获取集合对应的迭代器,用来遍历集合中的元素的。 |
下面介绍一下迭代的概念:
-
迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续再判断,如果还有就再取出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
注意:调用next()方法获取数据之前一定先调用hasNext()方法判断有没有有数据,否则会报异常。
Iterator接口的常用方法如下:
方法 | 说明 |
---|---|
E next() | 获取集合中的元素 |
boolean hasNext() | 判断集合中有没有下一个元素,如果仍有元素可以迭代,则返回 true。 |
void remove() | 删除当前元素 |
接下来我们通过案例学习如何使用Iterator迭代集合中元素:
代码如下:
public class Demo02 {
public static void main(String[] args) {
// 使用多态方式 创建对象
Collection<String> coll = new ArrayList<String>();
// 添加元素到集合
coll.add("aaaa");
coll.add("bbbb");
coll.add("cccc");
//根据当前集合对象生成迭代器对象
Iterator<String> it = coll.iterator();
//获取数据
System.out.println(it.next());
System.out.println(it.next());
System.out.println(it.next());
System.out.println(it.next());
}
}
运行结果:
aaaa
bbbb
cccc
Exception in thread "main" java.util.NoSuchElementException
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:896)
at com.itheima.sh.demo_03.Demo02.main(Demo02.java:22)
分析异常的原因:
说明:当迭代器对象指向集合时,可以获取集合中的元素,如果迭代器的光标移动集合的外边时,此时迭代器对象不再指向集合中的任何元素,会报NoSuchElementException没有这个元素异常。
解决方案:在使用next()函数获取集合中元素前,使用hasNext()判断集合中是否还有元素。
上述代码一条语句重复执行多次,我们可以考虑使用循环来控制执行的次数,
循环条件是 迭代器对象.hasNext() 为false时表示集合中没有元素可以获取了,循环条件迭代器对象.hasNext() 为true的时候说明还可以获取元素。
代码演示如下:
package cn.itcast.sh.iterator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/*
* 演示迭代器的使用
*/
public class IteratorDemo {
public static void main(String[] args) {
//创建集合对象
Collection coll=new ArrayList();
//向集合中添加数据
coll.add("aaaa");
coll.add("bbbb");
coll.add("cccc");
//根据当前集合获取迭代器对象
Iterator it = coll.iterator();
//取出数据
/*System.out.println(it.next());//it.next()表示获取迭代器对象指向集合中的数据
System.out.println(it.next());
System.out.println(it.next());
System.out.println(it.next());*/
//使用while循环遍历集合
while(it.hasNext())//it.hasNext()表示循环条件,如果为true,说明集合中还有元素可以获取,否则没有元素
{
//获取元素并输出
System.out.println(it.next());
}
}
}
tips:
1.使用while循环迭代集合的快捷键:itit
2.在进行集合元素获取时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会抛出java.util.NoSuchElementException没有集合元素异常。
2.2 迭代器的实现原理
我们在之前案例已经完成了Iterator遍历集合的整个过程。当遍历集合时,首先通过调用集合的iterator()方法获得迭代器对象,然后使用hashNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。
Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素。在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素,当第一次调用迭代器的next方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用next方法时,迭代器的索引会指向第二个元素并将该元素返回,依此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。
1)ctrl + N 搜索 ArrayList这个类
2)在ArrayList里面按 alt + 7 左边点击Itr
3)Itr是ArrayList里面的一个内部类。(这些东西不要求研究)
- 源码
public class ArrayList<E>{
//Itr属于ArrayList集合的成员内部类,实现了迭代器Iterator接口
private class Itr implements Iterator<E> {
//定义游标,默认值是0
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
/*
1.第一个 modCount 集合结构变动次数,如:一开始add调用了4次,那么这个变量就是4
2.第二个 expectedModCount表示期望的更改次数 在调用iterator()方法时,初始化值等于modCount ,初始化时也是4,这里 int expectedModCount = modCount;
*/
int expectedModCount = modCount;
//判断集合是否有数据的方法,如果有数据返回true,没有返回false
//size表示集合长度,假设添加3个数据,那么size的值就是3
public boolean hasNext() {
/*
判断游标变量cursor是否等于集合长度size,假设向集合中存储三个数据那么size等于3
1.第一次cursor的默认值是0 size是3 --》cursor != size --》0!=3-->返回true
*/
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
//主要作用是判断it迭代器数据是否和list集合一致 我们下面会讲解
checkForComodification();
//每次调用一次next方法都会将游标cursor的值赋值给变量i
int i = cursor;
//判断i是否大于等于集合长度size,如果大于等于则报NoSuchElementException没有这个元素异常
if (i >= size)
throw new NoSuchElementException();
//获取ArrayList集合的数组
Object[] elementData = ArrayList.this.elementData;
//判断i的值是否大于等于数组长度,如果为true则报并发修改异常
if (i >= elementData.length)
throw new ConcurrentModificationException();
/*
将i的值加1赋值给游标cursor.就是将游标向下移动一个索引,可以理解指针向下移动一次
*/
cursor = i + 1;
/*
elementData就是集合的底层数组,获取索引i位置的元素返回给调用者
*/
return (E) elementData[lastRet = i];
}
}
}
- 讲解图
2.3迭代器的问题:并发修改异常
-
异常:
ConcurrentModificationException(并发修改异常)
-
产生原因:
- 在迭代器遍历的同时如果使用集合对象修改集合长度(增或删)就会出现并发修改异常。
-
代码演示:
package com.itheima.sh.demo_04; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class Test01 { public static void main(String[] args) { Collection<String> list = new ArrayList<>(); list.add("aaa"); list.add("ddd"); list.add("bbb"); list.add("ccc"); Iterator<String> it = list.iterator(); while(it.hasNext()){ String s = it.next(); if("ddd".equals(s)){ //使用集合调用方法删除 list.remove(s); } } System.out.println(list); } }
-
源码
public class ArrayList<E>{
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
/*
1.第一个 modCount 集合结构变动次数,如:一开始add调用了4次,那么这个变量就是4
2.第二个 expectedModCount表示期望的更改次数 在调用iterator()方法时,初始化值等于 modCount ,初始化时也是4,这里 int expectedModCount = modCount;
*/
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
}
}
1.为什么会报并发修改异常?
1)在Itr内部类中的成员位置有这样一行代码,
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
}
由于是成员变量,所以在执行list.iterator()创建It对象时就已经有值了。
上述两个变量含义:
第一个 modCount 集合结构变动次数,如:一开始add调用了4次,那么这个变量就是4,
第二个 expectedModCount 在调用iterator()方法时,初始化值等于modCount ,初始化时也是4,这里 int expectedModCount = modCount;
2)it.next(): 看源代码可以发现每次在next()调用后,都会先调用checkForComodification()这个方法;
public E next() {
// checkForComodification(): 主要作用是判断it迭代器数据是否和list集合一致,
checkForComodification();
}
3)在checkForComodification()方法体中有如下代码:
final void checkForComodification() {
// 这个方法判断当 modCount != expectedModCount 时,
//抛出异常ConcurrentModificationException:
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
4)
//如果我们调用的是集合list的remove方法,那么modCount 就会+1 而expectedModCount 不变,这就会造成 modCount != expectedModCount;
public boolean remove(Object o) {
if (o == null) {
-------------
} else {
-------------删除方法
fastRemove(index);
----------------
}
return false;
}
fastRemove(index);方法:
private void fastRemove(int index) {
//这里加1了
modCount++;
。。。。。。。。。。。。。
}
-
解决办法:
- 使用集合增加或删除都会出现并发修改异常。
- 在Iterator迭代器中,有删除方法可以避免异常。但是他没有增加的方法。
public class Test01 { public static void main(String[] args) { Collection<String> list = new ArrayList<>(); list.add("aaa"); list.add("ddd"); list.add("bbb"); list.add("ccc"); Iterator<String> it = list.iterator(); while(it.hasNext()){ String s = it.next(); if("ddd".equals(s)){ //使用集合调用方法删除 // list.remove(s); //使用迭代器的删除方法 it.remove(); } } System.out.println(list); } }
-
为什么使用迭代器Iterator中的删除方法就不会报并发修改异常了
5)如果我们调用迭代器的remove方法,expectedModCount 会重新赋值, 迭代器中的remove方法: public void remove() { 。。。。。。。。。 expectedModCount = modCount; 。。。。。。。。。。。。。。。。。 }
-
在迭代器中有特殊的子类可以做元素的增加,这个类是ArrayList里面的内部类,叫ListItr.
因为以后也不用,所以现在不讲了,但是简单扩展一下告诉你有这个东西,以后万一有兴趣大家自己学一下。
2.4 增强for
增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
格式:
for(元素的数据类型 变量 : Collection集合or数组){
//写操作代码
}
它用于遍历Collection和数组。通常只进行遍历元素,不要在遍历的过程中对集合元素进行增删操作。
代码演示
public class NBForDemo1 {
public static void main(String[] args) {
int[] arr = {3,5,6,87};
//使用增强for遍历数组
for(int a : arr){//a代表数组中的每个元素
System.out.println(a);
}
Collection<String> coll = new ArrayList<String>();
coll.add("小河神");
coll.add("老河神");
coll.add("神婆");
for(String s :coll){
System.out.println(s);
}
}
}
tips:
1.快捷键:
1)数组/集合.for
2)iter2.增强for循环必须有被遍历的目标,目标只能是Collection或者是数组;
3.增强for(迭代器)仅仅作为遍历操作出现,不能对集合进行增删元素操作,否则抛出ConcurrentModificationException并发修改异常
4.如果需要使用索引,也不能使用增强for
小结:Collection是所有单列集合的根接口,如果要对单列集合进行遍历,通用的遍历方式是迭代器遍历或增强for遍历。
第三章 泛型
3.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后提供了一个新的技术,可以解决这个问题:泛型技术。
3.2 泛型概述
泛型的格式:
<具体的数据类型>
使用格式:
ArrayList<限定集合中的数据类型> list = new ArrayList<限定集合中的数据类型>();
说明:给集合加泛型,就是让集合中只能保存具体的某一种数据类型。
使用泛型改进以上程序中存在的问题:
说明:由于创建ArrayList集合的时候就已经限定集合中只能保存String类型的数据,所以编译的时候保存其他的数据类型就会报错,这样就达到了我们上述保存数据的目的了。
3.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:泛型是数据类型的一部分,我们将类名与泛型合并一起看做数据类型。
3.4 泛型的注意事项
1)泛型只支持引用数据类型(类类型或接口类型等),泛型不支持基本数据类型:
ArrayList<int> list = new ArrayList<int>();//错误的
2)泛型不支持数据类型以继承的形式存在,要求前后泛型的数据类型必须一致:
ArrayList<Object> list = new ArrayList<String>();//错误的
3)在jdk1.7之后,泛型也可以支持如下写法:
ArrayList<String> list = new ArrayList<>();//正确的
注意:现在的开发中,泛型已经成为编写代码的规范。
3.5 自定义泛型
在集合中,不管是接口还是类,它们在定义的时候类或接口名的后面都使用<标识符>,当我们在使用的时候,可以指定其中的类型。如果当前的类或接口中并没有<标识符>,我们在使用的时候也不能去指定类型。
举例:比如我们之前所学习的集合ArrayList类:
new ArrayList
说明:在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;
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类型具体由调用者来指定;
**泛型接口和泛型传递(**掌握)
通过查阅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类型。
3.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){ } }
3.6泛型在开发中的使用
类上、方法上、接口上定义泛型,我们今天只是学习写法。在开发中也不需要自己定义泛型。刚入行我们做的只是使用泛型,而不是定义泛型。
第四章 数据结构
4.1 数据结构介绍
数据结构 : 数据用什么样的方式组合在一起。就是数据的存储方式。
4.2 常见数据结构
数据存储的常用结构有:栈、队列、数组、链表和红黑树。我们分别来了解一下:
4.2.1栈和队列
栈
- 栈:stack,又称堆栈,它是运算受限的线性表,其限制是仅允许一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。
简单的说:采用该结构的集合,对元素的存取有如下的特点
- 先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。例如,子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先弹出上面的子弹,然后才能弹出下面的子弹。
- 栈的入口、出口的都是栈的顶端位置。
- 需求:演示向栈中存储数据ABC,然后再取出数据的过程。
这里两个名词需要注意:
- 压栈:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。
- 弹栈:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。
队列
-
队列:queue,简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在一端进行插入,而在另一端进行取出并删除。
简单的说,采用该结构的集合,对元素的存取有如下的特点:
- 先进先出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。排队买票等。
- 队列的入口、出口各占一侧。例如,下图中的左侧为入口,右侧为出口。
- 需求:演示向队列中存储数据ABC,然后再取出数据的过程。
4.2.2数组和链表
数组
是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素.就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。
简单的说,采用该结构的集合,对元素的存取有如下的特点:
- 特点:查找快,增删慢
-
查找元素快:通过索引,可以快速访问指定位置的元素
-
增删元素慢
-
指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置。如下图
-
**指定索引位置删除元素:**需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置元素不复制到新数组中。如下图
链表
- 链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括多个部分:一部分是存储数据元素的数据域,另一部分是存储前后一个结点地址的指针域。我们常说的链表结构有单向链表与双向链表,那么这里给大家介绍的是单向链表。后面讲双向链表。
简单的说,采用该结构的集合,对元素的存取有如下的特点:
-
多个结点之间,通过地址进行连接。例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。
-
查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素。
- 增删元素快:
说明:
查找慢:因为每个元素在内存中位置不同,所以查找慢。
增删快:增删时只需要改变前后两个元素的指针指向,对其他元素没有任何影响。
- 增删元素快:
4.2.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)按层遍历:从上往下,从左向右
遍历获取元素的时候可以按照"左中右"(中序(根)遍历)的顺序进行遍历来实现数据的从小到大排序;
注意:二叉查找树存在的问题:会出现"瘸子"的现象,影响查询效率
(17,24,19,18,20,23)数据的存储过程;
平衡二叉树
概述
为了避免出现"瘸子"的现象,减少树的高度,提高我们的搜素效率,又存在一种树的结构:“平衡二叉树”
规则:它的左右两个子树的高度差的绝对值不超过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. 对每一个节点,从该节点到其所有后代叶节点的路径上,均包含相同数目的黑色节点
如下图所示就是一个红黑树
在进行元素插入的时候,和之前一样; 每一次插入完毕以后,使用黑色规则进行校验,如果不满足红黑规则,就需要通过变色,左旋和右旋来调整树,使其满足红黑规则;