# 第十一章 集合和泛型
# 集合进阶
# 集合体系结构
集合体系一般分为两类:
单列集合:Collection
双列集合:Map
list系列集合: 添加的元素是有序的、可重复、有索引
Set系列集合:添加的元素是无序、不重复、无索引
# Collection
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() | 返回集合中元素的个数/集合的长度 |
# collection集合遍历方式
# 迭代器遍历
迭代器不依赖索引
迭代器在到java中的类是Iterator,迭代器是集合专用的遍历方式。
方法名称 | 说明 |
---|---|
Iterator | 返回迭代器对象,默认指向当前集合的0索引 |
# Iterator中的常用方法
方法名称 | 说明 |
---|---|
boolean hasNext() | 判断当前位置是否有元素,有元素返回true,没有元素返回false |
E next() | 获取当前位置的元素,并将迭代器对象移向下一个位置 |
Iterator<String> it = list.iterator();//创建指针
while(it.hasNext()){//判断是否有元素
String str = it.next();//获取元素然后移动指针
System.out.println(str);
}
2
3
4
5
细节:
如果当前位置没有元素,还要强行获取,会报NoSuchElementException
迭代器遍历完毕,指针不会复位
循环中只能用一次next方法
迭代器遍历时,不能用集合的方法进行增加或者删除
# 总结
1.迭代器在遍历集合的时候是不依赖索引的
2.迭代器需要掌握三个方法
3.迭代器的四个细节:
如果当前位置没有元素,还要强行获取,会报NoSuchElementException
迭代器遍历完毕,指针不会复位
循环中只能用一次next方法
迭代器遍历时,不能用集合的方法进行增加或者删除
# 增强for遍历
增强for的底层就是迭代器,为了简化迭代器的代码书写的。
它是JDK5之后出现的,其内部原理就是一个Iterator迭代器
所有的单列集合和数组才能用增强for来遍历
格式:
for(元素的数据类型 变量名 : 数组或者集合名){
}
2
3
# 增强for的细节
修改增强for中的变量,不会改变集合中原本的数据。
for(String s : list){
s = "lmx";
}
2
3
# Lambda表达式遍历
得益于JDK8开始的新技术Lambda表达式,提供了一种更简单、更直接的遍历集合的方式。
方法名称 | 说明 |
---|---|
default void forEach(Consumer<? super T> action): | 结合Lambda遍历 |
集合名.forEach((String s)->{
System.out.println(s);
});
2
3
集合名.forEach(s -> System.out.println(s));
# 总结
1.Collection是单列集合的顶层接口,所有方法被List和Set系列集合共享
2.常见成员方法:add、clear、remove、contains、isEmpty、size
3.三种通用的遍历方式:
迭代器:在遍历的过程中需要删除元素,请使用迭代器。(最好少用吧)
增强for、Lambda:(多用)
仅仅想遍历,那么使用增强for或Lambda表达式。
# List系列集合
# List集合的特有方法
- Collection的方法List都继承了
- List集合因为有索引,所以多了很多索引操作的方法
利用list集合存储数据,并使用多种方式遍历
1.迭代器
2.增强for
3.Lambda表达式
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
2
3
4
1.迭代器
Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
System.out.println(str);
}
2
3
4
5
2.增强for
for(String s : list){
System.out.println(s);
}
2
3
3.Lambda表达式
list.forEach(s->System.out.println(s));
# Set系列集合
无序:存取顺序不一致(存了之后顺序就固定了)
不重复:可以去除重复
无索引:没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素
# Set集合的实现类
HashSet:无序、不重复、无索引
LinkedHashSet:有序、不重复、无索引
TreeSet:可排序、不重复、无索引
Set接口中的方法上基本与Collection的API一致
# 练习:存储字符串并遍历
利用Set系列的集合,添加字符串,并使用多种方式遍历
1.迭代器
2.增强for
3.Lambda表达式
//添加一个Set集合的对象
Set<String> s = new HashSet<>();
//2.添加元素
boolean r1 = s.add("张三");
boolean r2 = s.add("张三");
System.out.println(r1);//true
System.out.println(r2);//false
System.out.println(s);//[张三]
2
3
4
5
6
7
8
9
打印集合
Set<String> s = new HashSet<>();
s.add("张三");
s.add("李四");
s.add("王麻子");
s.add("六筒");
//打印集合
System.out.println(s);//无序的
2
3
4
5
6
7
迭代器遍历
Set<String> s = new HashSet<>();
s.add("张三");
s.add("李四");
s.add("王麻子");
s.add("六筒");
//迭代器
Iterator<String> iterable = s.iterator();
while (iterable.hasNext()){
String next = iterable.next();
System.out.println(next);
}
2
3
4
5
6
7
8
9
10
11
增强for
Set<String> s = new HashSet<>();
s.add("张三");
s.add("李四");
s.add("王麻子");
s.add("六筒");
//增强for
for (String str:s) {
System.out.println(str);
}
2
3
4
5
6
7
8
9
Lambda表达式
Set<String> s = new HashSet<>();
s.add("张三");
s.add("李四");
s.add("王麻子");
s.add("六筒");
s.forEach(s->System.out.println(s));
2
3
4
5
6
# 总结
# Set系列集合的特点
- 无序、不重复、无索引
- Set集合的方法上基本与Collection的API一致
# Set集合的实现类特点
HashSet:无序、不重复、无索引
LinkedHashSet:有序、不重复、无索引
TreeSet:可排序、不重复、无索引
# HashSet
# HashSet底层原理
- HashSet集合底层采取
哈希表
存储数据 - 哈希表是一种对于增删改查数据性能都能较好的结构
# 哈希表的组成
- JDK8之前:数组+链表
- JDK8开始:数组+链表+红黑树
# 哈希值(重要)
是JDK根据对象的地址,按照某种规则算出来的int类型的数值。
Object类的API
- public int hashCode() : 返回对象的哈希值
# 对象的哈希值特点
- 同一个对象多次调用hashCode()方法返回的哈希值是相同的
- 默认情况下,不同对象的哈希值是不同的
# HashSet底层原理
1.创建一个默认长度16,默认加载为0.75的数组,数组名table
2.根据元素的哈希值跟数组的长度计算出应存入的位置
3.判断当前位置是否为null,如果是null直接存入
4.如果位置不为null,表示有元素,则调用equals.方法比较属性值
5.一样:不存,不一样:存入数组,形成链表
JDK8以前:新元素存入数组,老元素挂在新元素下面
JDK8以后:新元素直接挂在老元素下面
JDK8以后,当链表长度超过8
,而且数组长度大于等于64
时,自动转换为红黑树
如果集合中存储的是自定义对象,必须要重写hashcode
和equals
方法
JDK8开始后,哈希表对于红黑树的引入近一步提高了操作数据的性能
# HashSet的三个问题
问题1:HashSet为什么存和取的顺序不一样?
无序是跟hashset算法有关系的,
问题2:HashSet为什么没有索引?
底层是数组+链表+红黑树
问题3:Hashset是利用什么机制保证数据去重的?
三步:
判断当前位置是否为null,如果是null直接存入
如果位置不为null,表示有元素,则调用equals.方法比较属性值
一样:不存,不一样:存入数组,形成链表
# 总结:
1.HashSet集合的底层数据结构是什么样的?(JDK1.8的底层数据结构)
数组+链表+红黑树
2.HashSet添加元素的过程?
哈希值 % 16(数组的长度:创建HashSet) = 值对应数组的索引
3.HashSet为什么存和取的顺序不一样?
无序是跟hashset算法有关系的
4.HashSet为什么没有索引?
底层是数组+链表+红黑树
5.Hashset是利用什么机制保证数据去重的?
三步:
判断当前位置是否为null,如果是null直接存入
如果位置不为null,表示有元素,则调用equals.方法比较属性值
一样:不存,不一样:存入数组,形成链表
需要去重写equals和hashCode方法
# 案例:利用HashSet集合去除重复元素
需求:创建一个存储学生对象的集合,存储多个学生对象
使用程序实现在控制台遍历该集合
要求:学生对象的成员变量相同,我们就认为是同一个对象
分析:
- 定义学生类,创建HashSet集合对象,创建学生对象
- 把学生添加到集合
- 在学生类中重写两个方法,hashCode()和equals(),自动生成即可
# Map集合体系
# Map集合的概述
Map集合是一种双列集合 ,每个元素包含两个数据(我们要把这两个数据看成一个元素)
Map集合的每个元素的格式:key=value(键值对元素)。
Map集合也被称为“键值对集合”
# Map集合整体格式
Collection集合的格式:[元素1,元素2,元素3....]
Map集合的完整格式:{key1=value1,key2=value2,key3=value3,…}
# Map集合体系特点
说明
- 使用最多的Map集合是HashMap
- 重点掌握HashMap其他的后续理解
特点:
Map集合的特点都是由键(key)决定的
Map集合的键是无序,不重复的,无索引,值(value)不做要求(可以重复)
Map集合后面重复的键对应的值会覆盖前面重复键的值(后面如果存相同的key,后面的key会覆盖前面的key)
Map集合的键值对都可以为null。(null:null)
Map集合实现类的特点:
HashMap:元素按照键是无序,不重复,无索引,值不做要求。(与Map体系一致)
# Map集合常用API
Map是双列集合的祖宗接口,它的功能是全部双列集合都可以继承使用的。
方法名称 | 说明 |
---|---|
V put(K key , V value) | 添加元素 |
V remove(Object key) | 根据键删除键值对元素 |
void clear() | 移除所有的键值对元素 |
boolean containsKey(Object key) | 判断集合是否包含指定的键 |
boolean containsValue(Object value) | 判断集合是否包含指定的值 |
boolean isEmpty() | 判断集合是否为空 |
int size() | 集合的长度,也就是集合中键值对的个数 |
Set | 获取全部键的集合 |
Collection | 获取全部值的集合 |
# Map集合的遍历方式一:键找值
先获取Map集合的全部键的Set集合。
遍历键的Set集合,然后通过键提取对应值。
键找值涉及的API:
方法名称 | 说明 |
---|---|
Set | 获取所有键的集合 |
V get(Object key) | 根据键获取值 |
Set<String> strings = map.keySet();
for (String s: strings) {
Integer integer = map.get(s);
System.out.println(s+":"+integer);
}
2
3
4
5
# Map集合的遍历方式二:键值对
先把Map集合转换成Set集合,Set集合中每个元素都是键值对实体类型了。
遍历Set集合,然后提取键以及提取值。
键值对涉及到的API:
方法名称 | 说明 |
---|---|
Set<Map.Entry<K,V>> entrySet() | 获取所有键值对对象的集合 |
K getKey() | 获得键 |
V getValue() | 获取值 |
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> m: entries) {
String key = m.getKey();
Integer value = m.getValue();
System.out.println(key+":"+value);
}
2
3
4
5
6
# Map集合的遍历方式三:lambda表达式
得益于JDK8开始的新技术Lambda表达式,提供了一种更简单、更直接的遍历集合的方式。
Map结合Lambda遍历的API
方法名称 | 说明 |
---|---|
default void forEach(BiConsumer<? super K , ? super V> action) | 结合Lambda遍历Map集合 |
map.forEach(new BiConsumer<String,Integer>(){
@Override
public void accept(String key, Integer value) {
System.out.println(key+":"+value);
}
});
2
3
4
5
6
map.forEach((k,v)->{
System.out.println(k+":"+v);
});
2
3
# 案例:Map集合案例-统计投票人数
需求:某个班级80名学生,现在需要组成秋游活动,班长提供了四个景点依次是(A、B、C、D),每个学生只能选择一个景点,请统计出最终哪个景点想去的人数最多。
# 案例:存储学生对象并遍历
需求:
创建个HashMap:集合,键是学生对象(Student),值是籍贯(String)
存储三个键值对元素,并遍历
要求:同姓名,同年龄认为是同一个学生
# 泛型
看一个需求
请编写程序,在ArrayList中,添加3个Dog对象
Dog对象含有name和age,并输出name和age(要求使用getXXX())
我们先使用传统方式来解决
class Dog {
private String name;
private int age;
public Dog() {
}
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
使用传统方法的问题分析
传统方法的问题分析
1.不能对加入到集合ArrayList中的数据类型进行约束(不安全)
2.遍历的时候,需要进行类型转换,如果集合中的数据量过大,对效率有影响
使用泛型的好处
1.编译时,可以添加元素的类型,提高了安全性
2.减少了类型转换的次数,提高效率
# 泛型介绍
理解:泛型 => Integer,String,Dog
泛型是一种表示数据类型的数据类型
泛型又称为参数化类型,解决了数据类型的安全性问题,在类声明或者实例化时,只需要指定要需要的具体的类型即可
Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常,同时代码更加简洁,健壮
泛型的作用:可以在类声明时通过一个标识类中某个属性的类型,或者是某个方法的返回值的类型,或者是参数类型.
# 泛型的细节
泛型中不能写基本数据类型
指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型
如果不写泛型,类型默认是Object
# 泛型类
使用场景:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类
此处E可以理解为变量,但是不是用来记录数据的,而是记录数据的类型,可以写成T、E、K、V等
# 练习:泛型方法的练习
案例1:
创建3个学生对象
放入到HashSet中
放入到HashMap中,要求 key 是String name,value 是学生对象
使用两种方式遍历
案例2:
# 泛型接口
重点:如何使用一个带泛型的接口
方式1:实现类给出具体类型
方式2:实现类延续泛型,创建对象时再确定
# 自定义泛型类
基本语法
class 类名<T,R....>{
成员
}
2
3
注意细节:
1.普通成员可以使用泛型
2.使用泛型的数组,不能初始化
3.静态方法中不能使用类的泛型
4.泛型类的类型,是在创建对象时确定的(因为创建对象的时候,需要确定类型)
5.如果在创建对象的时候没有指定类型,默认为object
# 自定义泛型接口
基本语法
interface 接口名<T,R.....>{
}
2
3
注意细节:
1.接口中,静态成员也不能使用泛型(这个和泛型类规定一样)
2.泛型接口的类型,在继承接口或实现接口确定
3.没有指定类型,默认为Object