Demo12-集合(Collection、数据结构、List、泛型深入)
1.集合体系
1.集合有两种:
- Collection单列集合:每个元素(数据)只包含一个值
- List集合:添加的元素有序,可重复,有索引
- ArrayList集合:有序,可重复,有索引
- LinkedList集合:有序,可重复,有索引
- 但其实ArrayList和LinkedList底层数据结构不同,以后做增删改查时性能不同
- Set集合:添加的元素无序,不重复,无索引
- HashSet集合:无序,不重复,无索引
- LinkedHashSet:有序,不重复,无索引
- TreeSet集合:**按照大小默认升序排序,**不重复,无索引
- HashSet集合:无序,不重复,无索引
- List集合:添加的元素有序,可重复,有索引
- Map双列集合:每个集合包含两个值(键值对)
2.集合都是支持泛型的,可以在编译阶段约束集合只能操作某种数据类型
3.集合和泛型都只能支持引用数据类型,不支持基本数据类型,所以集合中存储的元素都认为是对象(基本数据类型用对应的包装类)
Collection<Double> list = new ArrayList<>();
list1.add(23.3);//将23.3自动装箱为Double类型后作为参数传给add方法
list1.add(23.0);//整数默认是int,是不能自动装箱为Double类型的,两种解决办法:23d或23.0
list1.add(23d);
2.Collection集合
2.1Collection常用API
Collection是单列集合的祖宗接口,它的功能是全部单列结合都可以继承使用的
方法名称 | 说明 |
---|---|
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() | 把集合中的元素存储到数组中 |
public boolean addAll(Collection<? extend E>) | 向集合中加入另一个集合的所有元素 |
关于public boolean addAll(Collection<? extend E>),public boolean remove(E e)和public Object[] toArray():
-
addAll方法有一种用法是向集合中加入另一个集合的所有元素:
Collection<String> c1 = new ArrayList<>(); c1.add("java1"); Collection<String> c2 = new ArrayList<>(); c2.add("赵敏"); c2.add("殷素素"); c1.addAll(c2);
- 注意:加入之后c2的集合内容不变
-
为什么remove方法不支持索引删除呢:
Collection<String> c1 = new ArrayList<>(); c1.add("java1"); c1.remove("yyds")
- Collection c1 = new ArrayList<>();是多态的形式,其中c1是Collection类型的变量,我们知道那么调用c1的方法时必须是Collection集合中有的方法
- Collection集合有这个根据索引删除元素的方法吗
- 因为Collection集合中有的系列有索引,有的系列无索引,所以就理所当然的没有定义根据索引删除元素的方法(祖宗集合定义的方法是需要子集合必须用的,那他的子集合如HashSet没有索引,那人家怎么使用根据索引删除元素这个方法呢)
- 所以Collection集合没有根据索引删除元素的方法,所以remove方法的参数不能是索引
-
public Object[] toArray()为什么返回值类型是Object[]呢:
Collection<String> c1 = new ArrayList<>();
- 以后我们可以通过其他技术绕过编译(也就是写代码的阶段)强行向c1中加入String类型以外的对象,toArray方法为了兼容一切类型,索性就让返回值类型是Object[]
2.2Collection集合的遍历方式
2.2.1迭代器
1.迭代器遍历概述
- 迭代器就是一个一个的把容器中的元素访问一遍
- 迭代器在java中的代表是Iterator,迭代器是集合的专用遍历模式
2.怎么使用迭代器遍历:
首先需要Collection集合获取迭代器:
方法名称 | 说明 |
---|---|
Iterator iterator() | 返回集合中的迭代器对象,该迭代器对象默认指向当前集合的-1的位置 |
Iterator中的常用方法:
方法名称 | 说明 |
---|---|
boolean hasNext() | 询问当前位置的下一个位置是否有元素存在,存在返回true,反之false |
E next() | 获取当前位置的下一个位置的元素,并同时将迭代器对象移向下一个位置,注意防止取出越界 |
public class CollectionDemo01 {
public static void main(String[] args) {
ArrayList<String> lists = new ArrayList<>();
lists.add("赵敏");
lists.add("小昭");
Iterator<String> it = lists.iterator();
while (it.hasNext()){
String ele = it.next();
System.out.println(ele);
}
}
}
2.2.2增强for循环
-
既可以遍历集合也可以遍历数组
-
它是JDK5之后出现的,其内部原理是一个Iterator迭代器,遍历集合相当于是迭代器的简化写法
-
实现Iterator接口的类才可以使用迭代器和增强for,Collection接口已经继承了Iterator接口
public interface Collection<E> extends Iterable<E>
-
格式:
for(元素数据类型 变量名 : 数组或者Collection集合) {
//在此处使用变量即可,该变量就是元素
}
public class CollectionDemo02 {
public static void main(String[] args) {
Collection<String> lists = new ArrayList<>();
lists.add("赵敏");
lists.add("小昭");
System.out.println(lists);
for (String ele : lists) {
System.out.println(ele);
}
double[] scores = {100, 99.5 , 59.5};
for (double score : scores) {
System.out.println(score);
// if(score == 59.5){
// score = 100.0; // 修改无意义,不会影响数组的元素值。
// }
}
}
}
注意:使用迭代器或者增强for进行遍历修改值无效,因为这就相当于将实参copy一份交给形参,形参修改的值当然不会影响到实参
2.2.3Lambda表达式
-
得益于JDK8开始的新技术Lambda表达式,提供了一种更简单,更直接的遍历集合的方式
-
Collection结合Lambda遍历的API:forEach方法
- Collection继承Iterator接口,Iterator接口中定义了该方法
方法名称 说明 default void forEach(Consumer<? super T> action) 结合lambda遍历集合
public class CollectionDemo03 {
public static void main(String[] args) {
Collection<String> lists = new ArrayList<>();
lists.add("赵敏");
lists.add("小昭");
lists.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
System.out.println("=======Lambda简化版========");
lists.forEach(s ->System.out.println(s));
}
}
现在分析一下这段代码:
lists.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
看一下forEach源码:
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
- 1.先把匿名内部类对象的地址赋值给action
- 2.forEach内部有一个增强for,其中this表示调用该方法的集合
- 也就是说在forEach内部实现了遍历集合
- 3.在增强for中调用action的accept方法,并将遍历集合得到的元素传给该方法
- 4.因为我们重写了accept方法,根据多态特性,会调用我们重写后的accept方法,也就是输出遍历得到的元素
3.常见数据结构
3.1栈
栈数据结构的执行特点:后进先出,先进后出
3.2队列
队列数据结构的执行特点:先进先出,后进后出
3.3数组
- 查询速度快:查询数据通过地址值和索引定位,查询任意数据耗时相同(元素在内存中是连续存储的)
- 删除效率低:要将原始数据删除,同时后面每个数据前移
- 添加效率低:添加位置后的每个数据后移,再添加元素
3.4链表
- 链表中的元素是在内存中不连续存储的,每个元素节点包含数据值和下一个元素的地址
- 链表查询慢(对比数组),无论查询哪个数据都要从头开始找
- 链表增删相对快(对比数组)
- 两种链表
- 单向链表
- 双向链表
- 数组根据索引查询元素是所有数据结构中最快的,链表增删首尾元素是所有数据结构中最快的
3.5二叉树
- 二叉树成分:
- 父节点地址
- 左子节点地址
- 右子节点地址
- 数据值
- 二叉树只能有一个根节点,每个节点最多支持2个直接子节点
- 节点的度:节点拥有的子树的个数,二叉树的度不大于2,叶子节点是度为0的节点,也称之为终端节点
- 高度:叶子结点的高度为1,叶子结点的父节点高度为2,依次类推,根节点的高度最高
- 二叉树分为普通的二叉树和二叉查找树(又称为二叉排序树或二叉搜索树),二叉查找树的元素值都是数据值小的作为左子树,数据值大的作为右子树,这样的目的是提高检索数据的性能
3.6平衡二叉树
- 二叉查找树的缺陷:如果待存储的数据本就是有序的,则会出现瘸子现象,导致查询的性能与单链表一样,查询速度变慢
- 平衡二叉树是在满足查找二叉树的大小规则,让树尽可能矮小,以此提高数据的性能
- 平衡二叉树的要求:任意节点的左右两个子树的高度差不超过1,任意节点的左右两个子树都是一颗平衡二叉树
- 平衡二叉树在添加元素后可能导致不平衡,需要做一些操作让其平衡,具体操作建议看数据结构
3.7红黑树
- 红黑树是一种自平衡的二叉查找树,是计算机科学中用到的一种数据结构
- 1972年出现,当时被称之为平衡二叉B树,1978年被修改为如今的"红黑树"
- 红黑树的每一个结点可以是红或黑;红黑树不是通过高度平衡的,它的平衡是通过"红黑规则"进行实现的
- 红黑规则:
- 每一个节点或是红色,或是黑色,根节点必须是黑色
- 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,叶节点是黑色的
- 如果某一个节点是红色,那么它的子节点必须是黑色,不能出现两个红色节点相连的情况
- 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
- 红黑树成分:
- 父节点地址
- 左子节点地址
- 右子节点地址
- 数据值
- 颜色
- 添加节点建议去看数据结构
红黑树增删改查的性能都很好
4.List集合
4.1List集合特有方法及遍历方式
List集合因为支持索引,所以多了很多索引操作的独特API,其他Collection的功能List也继承了
方法名称 | 说明 |
---|---|
void add(int index, E element) | 在此集合中的指定位置插入指定的元素 |
E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
E set(int index, E element) | 修改指定索引处的元素,返回被修改的元素 |
E get(int index) | 返回指定索引处的元素 |
遍历方式:
- 迭代器
- 增强for
- Lambda表达式
- for循环(因为List集合存在索引)
4.2ArrayList集合底层原理
- ArrayList底层是基于数组实现的:根据索引定位元素快,增删需要做元素的移位操作
- 第一次创建集合并添加第一个元素的时候,在底层创建一个默认长度为10的数组
为什么默认长度是10呢:
1.先分析add方法源码:
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
根据第3行elementData[size++] = e;可知只有向elementData数组添加元素时size值才会增加1,所以此时size为默认值0,也就是说,第一次向集合添加元素时会先调用ensureCapacityInternal(1);注意这里的实参为size + 1 = 0 + 1 = 0
2.看一下ensureCapacityInternal方法的源码:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
源码中在定义ArrayList类时已经定义了两个空数组和一个常量:
-
transient Object[] elementData;
- 也就是定义了一个空数组
-
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
- 也就是定义了一个空数组
-
private static final int DEFAULT_CAPACITY = 10;
- 也就是定义了一个常量10
那么我们回到ensureCapacityInternal方法看elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA发现判断结果为真,进入分支语句,然后分支语句中的Math.max(DEFAULT_CAPACITY, minCapacity);第一个参数是10,第二个参数是调用ensureCapacityInternal方法时传入的参数1,显然,这里将10赋值给minCapacity变量.接着调用ensureExplicitCapacity方法并传入参数10
3.看一下ensureExplicitCapacity方法的源码:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
minCapacity我们已经知道是10,elementData在前面也已经说过了是一个空数组,所以其数组长度是0,那么if条件判断为真,调用grow方法进行扩容,并将容量扩容为10.这一系列都做完后回到add方法:
elementData[size++] = e;
将元素赋值给数组中索引为0的位置.至此,解释完了为什么第一次向集合中添加元素时默认数组长度为10
我在分析这个源码的时候发现了ArrayList集合有一个构造器可以手动设置第一次向集合中添加元素时数组长度,现在把这个流程再过一边:
1.看一下这个构造器的源码:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
假如我传入的是15,那么此时会执行this.elementData = new Object[initialCapacity];创建一个长度为15的数组并将其地址赋值给elementData变量(上面我们说过这个变量默认是一个空数组,而此时它将是一个长度为15的数组)
2.add方法逻辑不变,调用ensureCapacityInternal方法并传参1
3.看ensureCapacityInternal方法的源码
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
可知执行minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);时会把较大的15赋值给minCapacity,然后调用ensureExplicitCapacity并传参15
4.看ensureExplicitCapacity方法的源码:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
此时minCapacity和elementData都为15,if条件判断为假,不执行grow方法扩容
如果这个数组长度为15且准备向集合中添加第16个元素,底层怎么执行扩容的呢:
1.看add方法源码:
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
调用ensureCapacityInternal方法,此时参数为15 + 1 = 16
2.看ensureCapacityInternal方法的源码:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
此时if条件判断为假,所以直接调用ensureExplicitCapacity方法并传参16
3.看ensureExplicitCapacity方法的源码:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
先把minCapacity - elementData.length > 0转换为minCapacity > elementData.length这样比较好理解.
minCapacity是容纳所有元素(包括我此时准备向集合中添加的这一个元素)所需的最少数组长度,elementData.length是实际数组长度
minCapacity为16,elementData.length为15,很显眼,此时容纳所有元素所需的最少数组长度>实际数组长度,判断成立,执行grow方法进行扩容
方法调来调去的看着好麻烦,我简单总结了一下这几个方法之间的关系(我自己看的,所以写的很简略,看不懂的话我建议自己也总结一个类似小流程,真的好用!!!超级便于记忆)
- 当第一次向集合添加元素时:
- 1.add方法把集合中有几个元素进行+1操作后传下去(传下去的实际上就是容纳所有元素所需的最小数组长度,后面我都称为最小容量)注意这里不能是把数组元素个数进行+1返回,因为数组长度可能不等于集合中元素个数
- 2.进行判断,如果最小容量<10则将其改为10并传下去
- 3.把最小容量与数组长度比较,如果前者>后者,就把最小容量传下去进行扩容
- 非第一次向集合添加元素时:
- 1.add方法把集合中有几个元素进行+1操作后传下去
- 2.直接传下去
- 把最小容量与数组长度比较,如果前者>后者,就把最小容量传下去进行扩容
4.3LinkedList集合
- 底层数据结构是双链表,查询慢,首尾操作的速度是极快的,所以多了很多首尾操作的特有API
方法名称 | 说明 |
---|---|
public void addFirst(E e) | 在该列表开头插入指定的元素 |
public void addLast(E e) | 将指定的元素追加到此列表的末尾 |
public E getFirst() | 返回此列表中的第一个元素 |
public E getLast() | 返回此列表中的最后一个元素 |
public E removeFirst() | 从此列表中删除并返回第一个元素 |
public E removeLast() | 从此列表中删除并返回最后一个元素 |
public class ListDemo03 {
public static void main(String[] args) {
// LinkedList可以完成队列结构,和栈结构 (双链表)
// 1、做一个队列:
LinkedList<String> queue = new LinkedList<>();
// 入队
queue.addLast("1号");
queue.addLast("2号");
System.out.println(queue);
// 出队
System.out.println(queue.removeFirst());
// 2、做一个栈
LinkedList<String> stack = new LinkedList<>();
// 入栈 压栈 (push)
stack.addFirst("第1颗子弹");
stack.push("第2颗子弹");//点进push方法发现其实就是addFirst方法
System.out.println(stack);
// 出栈 弹栈 pop
System.out.println(stack.removeFirst());
System.out.println(stack.pop());//点进pop方法发现其实就是removeFirst方法
}
}
5.集合的并发修改异常问题
当我们从集合中找出某个元素并删除的时候可能会出现一种并发修改异常问题,比如:
- 1.迭代器遍历集合且直接用集合删除元素(list.remove(ele))的时候出现:报ConcurrentModificationException异常,解决办法是用迭代器自己的删除方法操作即可解决
- 2.增强for循环遍历集合且直接用集合删除元素(list.remove())的时候可能会出现:报ConcurrentModificationException异常,无法解决
- 3.Lambda表达式循环遍历集合且直接用集合删除元素(list.remove())的时候可能会出现(无法解决)
- 4.使用for循环遍历并删除元素不会存在这个问题,运行不报错,但是可能会漏删(这个我理解了为啥,因为ArrayList底层基于数组实现的,数组删除元素后,后面的所有元素就会前移,接着for循环会i++,这样造成有一个元素没有被判断,这样就可能会造成程序没报错,但漏删了)
- 要明白为啥和预期不符为啥要这样解决需要研究源码,这个给以后留的作业吧,目前先记着怎么解决就可以了
1.解决迭代器遍历集合并删除元素:用迭代器自己的删除方法操作即可解决
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("情话");
list.add("Java");
list.add("Java");
list.add("赵敏");
//迭代器遍历删除
Iterator<String> it = list.iterator();
while (it.hasNext()){
String ele = it.next();
if("Java".equals(ele)){
// list.remove(ele); //错误示范
it.remove(); //用迭代器自己的删除方法
}
}
System.out.println(list);
}
}
2.for循环遍历集合并删除元素错误示范:
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("情话");
list.add("Java");
list.add("Java");
list.add("赵敏");
for (int i = 0; i < list.size(); i++) {
String ele = list.get(i);
if("Java".equals(ele)){
list.remove(ele);
}
}
System.out.println(list);
}
}
解决方法一:在if中让i自减
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("情话");
list.add("Java");
list.add("Java");
list.add("赵敏");
for (int i = 0; i < list.size(); i++) {
String ele = list.get(i);
if("Java".equals(ele)){
list.remove(ele);
i--;//将i进行自减操作
}
}
System.out.println(list);
}
}
解决方法二:遍历集合时倒着遍历
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("情话");
list.add("Java");
list.add("Java");
list.add("赵敏");
for (int i = list.size() - 1; i >= 0 ; i--) {
String ele = list.get(i);
if("Java".equals(ele)){
list.remove(ele);
}
}
System.out.println(list);
}
}
6.泛型深入
6.1泛型概述及优势
1.泛型概述:
- 泛型是JDK5中引入的特性,可以在编译阶段约束操作的数据类型并进行检查
- 泛型格式:<数据类型>;注意:泛型只能支持引用数据类型
- 集合体系的全部接口和实现类都是支持泛型的使用的
2.泛型的好处:
-
统一数据类型
-
把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为编译阶段类型就能确定下来了
List<Object> list1 = new ArrayList(); list1.add("Java"); list1.add(23.3); list1.add(false); for (Object o : list1) { String ele = (String) o; System.out.println(ele); }
上述代码会报ClassCastException异常
因为在这个案例中for循环中变量o指向的真实对象是String,Double,Boolean类型,而Double和Boolean类型的对象不能强制转换为String类型
3.泛型可以在很多地方进行定义:
- 类后面是泛型类
- 接口后面是泛型接口
- 方法申明后面是泛型方法
6.2自定义泛型
6.2.1自定义泛型类
-
定义类的同时定义了泛型的类就是泛型类
-
泛型类的格式:修饰符 class 类名<泛型变量> {}
-
范例:public class MyArrayList {}
-
此处泛型变量可以随机写为任意标识,常见的如E,T,K,V等
-
-
泛型类的作用:编译阶段可以指定数据类型,类似于集合的使用
-
泛型方法的原理:把出现泛型变量的地方全部替换成传输的真实数据类型
案例实现:模拟ArrayList集合自定义一个集合MyArrayList集合,完成添加和删除功能的泛型设计即可
public class Test {
public static void main(String[] args) {
// 需求:模拟ArrayList定义一个MyArrayList ,关注泛型设计
MyArrayList<String> list = new MyArrayList<>();
list.add("Java");
list.add("MySQL");
System.out.println(list);
}
}
class MyArrayList<E> {
//换壳思想:我不会你这个操作,那我让表面上是我的,实际实现还是用你的操作实现
private ArrayList lists = new ArrayList();
public void add(E e){
lists.add(e);
}
public void remove(E e){
lists.remove(e);
}
//不真实的换壳
// @Override
// public String toString() {
// return "MyArrayList{" +
// "lists=" + lists +
// '}';
// }
//真实的换壳
@Override
public String toString() {
return lists.toString();//注意这里重写的toString方法,只有这样重写才能换壳真实
}
}
6.2.2自定义泛型方法
- 定义方法同时定义了泛型的方法就是泛型方法
- 泛型的格式:修饰符 方法返回值 方法名称(形参列表){}
- 范例:public void show(T t){}
- 作用:方法中可以使用泛型接收一切实际类型的参数,方法更具备通用性
- 泛型方法的原理:把出现泛型变量的地方全部替换成传输的真实数据类型
案例实现:给你任何一个类型的数组,都能返回它的内容,也就是实现Arrays.toString(数组)的功能
public class GenericDemo {
public static void main(String[] args) {
String[] names = {"小璐", "蓉容", "小何"};
printArray(names);
Integer[] ages = {10, 20, 30};
printArray(ages);
Integer[] ages2 = getArr(ages);//再理解一下泛型方法的核心思想:ages是Integer
//类型的数组,进入getArr方法后把出现泛型变量的地方全部替换成传输的真实数据类型,
//所以方法返回的也是Integer类型的数组,将该数组赋值给ages2时不需要进行强转
String[] names2 = getArr(names);//同上
}
public static <T> T[] getArr(T[] arr){
return arr;
}
public static <T> void printArray(T[] arr){
if(arr != null){
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
sb.append(arr[i]).append(i == arr.length - 1 ? "" : ", ");
}
sb.append("]");
System.out.println(sb);
}else {
System.out.println(arr);
}
}
}
6.2.3自定义泛型接口
- 使用了泛型定义的接口就是泛型类接口
- 泛型类接口的格式:修饰符 interface 接口名称<泛型变量>{}
- 范例:public interface Data{}
- 作用:泛型接口可以让实现类选择当前功能需要操作的数据类型
- 泛型接口的原理:实现类可以在实现接口的时候传入自己操作的数据类型,这样重写的方法都将是针对该类型的操作
案例实现:教务系统,提供一个接口可约束一定要完成数据(学生,老师)的增删改查操作
public interface Data<E> {
void add(E e);
void delete(int id);
E queryById(int id);
}
public class StudentData implements Data<Student>{
@Override
public void add(Student student) {
}
@Override
public void delete(int id) {
}
@Override
public Student queryById(int id) {
return null;
}
}
public class TeacherData implements Data<Teacher>{
@Override
public void add(Teacher teacher) {
}
@Override
public void delete(int id) {
}
@Override
public Teacher queryById(int id) {
return null;
}
}
6.3泛型通配符,上下限
通配符:?可以在使用的时候代表一切类型,注意和ETKV的区别:后者是在定义泛型的时候使用的
泛型的上下限:
- ? extends Car:
- ?必须是Car或者其子类,是泛型上限
- ? super Car:
- ?必须是Car或者其父类,是泛型下限
案例实现:开发一个极品飞车的游戏,所有的汽车都能一起参与比赛
public class GenericDemo {
public static void main(String[] args) {
ArrayList<BMW> bmws = new ArrayList<>();
bmws.add(new BMW());
bmws.add(new BMW());
bmws.add(new BMW());
go(bmws);
ArrayList<BENZ> benzs = new ArrayList<>();
benzs.add(new BENZ());
benzs.add(new BENZ());
benzs.add(new BENZ());
go(benzs);
ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
// go(dogs);
}
public static void go(ArrayList<? extends Car> cars){
}
}
class Dog{
}
class Car{
}
class BENZ extends Car{
}
class BMW extends Car{
}
现在解释一下ArrayList<? extends Car> cars怎么来的:
- 1.最开始我用ArrayList,但是这样只有bmws能传进来而benzs无法传进来
- 2.BMW和BENZ都继承Car,那我能不能写成ArrayList呢:
- 不能,会发现一旦这样写bmws和benzs都无法传进来了
- 会发现虽然BMW和BENZ都继承Car,但是ArrayList和ArrayList与ArrayList是没有关系的,并不是ArrayList继承ArrayList,这两个集合之间无任何关系
- 3.java为此提供了通配符?,我们用ArrayList<?>表示任意数据类型的集合都可以传进来,至此,解决了传入bmws和benzs的问题
- 4.但是我们会发现如果我定义了一个集合存放Dog类型,这个集合竟然也能作为实参传给go(ArrayList<?> cars)方法,这是我们不想看到的
- 为此java提出了上下限的概念
- 此时已经演变为了最终形式go(ArrayList<? extends Car> cars),至此演变完成