集合概述
生活中的集合:人或事物聚集在一起。
数学中的集合:具有某种特性的事物的整体。
Java中的集合:是工具类,可以存储任意数量的具有共同属性的对象
Java集合中只能存放对象的引用,不能存放基本类型数据
集合与数组
数组的缺陷:长度必须确定且不能改变,类型固定,算法需自己实现
而集合的长度是可以动态改变的,集合允许存储不同类型的对象,集合对外提供了很多算法实现。
用数组去满足存储固定长度的数据场景,比如存储20名学生的信息;而集合更适用于动态变化的场景,比如存储购物车的商品信息。
应用场景
- 无法预测存储数量的数据
- 同时存储具有一对一关系的数据(键值对)
- 需要进行数据的增删
- 数据重复问题
- ……
集合框架
为了表示和操作集合而规定的一种统一的标准体系结构。
分类
- 单列集合Collection
— List:元素有序、可包含重复元素
— Set:元素无序、不包含重复元素 - 双列集合Map:键值映射关系
- 迭代器Iterator
- 比较接口Comparable与Comparator
主要组成
- 对外的接口
表示集合的抽象数据类型,提供了让我们对集合中所表示的内容进行单独操作的可能 - 接口的实现
实际上就是那些可复用的数据结构 - 对集合运算的算法
这些算法通常是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现
使用集合框架的好处
- 减少了程序设计的辛劳
- 提高了程序速度和质量
- 减少学习和使用新的API的辛劳
- 减少了设计新API的努力
- 集合框架鼓励软件的复用
集合框架的体系结构
Collection接口
用于表示任何对象或元素组,集合框架的顶层接口,想要尽可能以常规方式处理一组元素时,就使用该接口
常用方法
Iterator接口
用于对集合容器进行向前的单方向遍历,通常称为迭代器。该接口的实例也是容器。在返回的迭代器这个容器中,已经将集合中的内容复制了一份。
常用方法
遍历集合
//创建集合
List list = new ArrayList();
//添加元素
list.add(1);list.add(2);list.add(3);
//获取迭代器对象进行遍历
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
List接口
List是元素有序并且可以重复的集合,称为序列,顺序性是List最重要的特性。
List可以精确的控制每个元素的插入位置,或删除某个位置的元素。
List的两个主要实现类是ArrayList
和LinkedList
特有方法
ListIterator接口
list特有的迭代器,内含避免并发修改异常的算法。
并发修改异常
并发修改异常,java.util.ConcurrentModificationException
如果在迭代集合的过程中,使用集合本身添加或删除元素,则会产生该异常。
//创建集合
List list = new ArrayList();
//添加元素
list.add(1);list.add(2);list.add(3);
//遍历
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
//在迭代的过程中使用集合本身添加/删除元素,抛出异常ConcurrentModificationException
//list.remove(0);
list.add(4);
}
特有方法
除了迭代器的hasNext()
、next()
、remove()
方法,还包含以下方法
使用列表迭代器解决并发修改问题
//创建集合
List list = new ArrayList();
//添加元素
list.add(1);list.add(2);list.add(3);
//使用列表迭代器
ListIterator iterator = list.listIterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
//使用迭代器的添加方法进行添加元素
iterator.add(4);
}
System.out.println(list);
//[1, 4, 2, 4, 3, 4]
ArrayList类
- ArrayList底层是由数组实现的,实现了动态增长,以满足应用程序的需求
- 在列表尾部插入或删除数据非常高效,更适合查找和更新元素,不适合中间插入和删除
- ArrayList中的元素可以为null
特殊方法:public void trimToSize()
因为remove()方法删除后集合实际内存空间不变,使用该方法将此 ArrayList 实例的容量调整为列表的当前大小。应用程序可以使用此操作来最小化 ArrayList 实例的存储量
使用示例
public static void main(String[] args) {
//创建对象
List list = new ArrayList();
//添加元素
list.add("maven");
list.add("redis");
list.add("git");
list.add("docker");
list.add(3,"shiro");
//集合元素的数量:5
System.out.println(list.size());
//获取第二个元素:redis
System.out.println(list.get(1));
//删除docker元素
list.remove(4);
//修改元素:将maven替换为gradle
list.set(0, "gradle");
//遍历ArrayList
for (int i = 0; i < list.size(); i++) {
//gradle redis git shiro
System.out.print(list.get(i) + " ");
}
System.out.println();
//查找git元素的位置:2
System.out.println(list.indexOf("git"));
//判断集合是否包含redis元素
System.out.println(list.contains("redis")); //true
//清空集合
list.clear();
System.out.println(list.isEmpty()); //true
}
LinkedList类
- LinkedList底层是由双向循环列表实现的,该类添加了一些处理列表两段元素的方法,使用这些方法可以轻松把LinkedList当作一个堆栈、队列或双端队列的数据结构
- 查询效率慢,更适合快速的插入和删除元素
特殊方法
Set接口
- Set是元素无序并且不可以重复的集合,被称为集。
- Set就是一个不包含重复元素的Collection,没有引入新的方法。
HashSet类
- HashSet称为散列表或者哈希集,底层实现实际上是HashMap
- 允许存储一个null元素,具有良好的存取和查找性能
- 默认负载因子是0.75,默认容量是16
- 按照哈希算法计算出哈希码作为存取和比较的依据
HashSet判断元素唯一性
- HashSet存储对象时,先调用hashCode方法生成哈希码
- 如果生成的哈希码与已有的某个对象哈希码相同,则继续调用equals方法比较两个对象
- 如果equals方法也返回true,则认为是重复元素,不进行存储操作
- 如果哈希码不相等或者哈希码相等但是equals返回false,则根据哈希码在集合中找合适位置存储对象
关于哈希表结构
使用ArrayList存储元素如100个,如果希望查找某个元素,需要遍历整个容器,效率低。
假设我们在内存区域开辟三块空间来存储这100个元素,可以看成是三个桶,编号分别为0、1、2。什么数据存放在哪个桶里我们需要定义一个规则,这个规则其实就是hashCode,hashCode需要根据需求编写相应的算法
比如规则为n%3,则元素值为1存入1号桶,元素值为2存入2号桶,元素值为3存入0号桶...
然后查找元素时先判断数据在哪个桶里(hashCode),然后遍历桶里的元素(equals)即可。
重写hashCode方法
考虑到效率,添加到HashSet的对象需要采用恰当分配哈希码的方式实现HashCode()方法,一般的规则如下
- 对象的属性如果是类似int类型的基本数据类型,直接返回该值本身就是hashCode值
- 如果是引用数据类型,则调用该属性值的hashCode方法返回hashCode值
- 将所有属性的hashCode值相加,就是该对象根据内容返回的hashCode值
private int age;
private String name;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
Set的并发修改异常
List有提供解决并发修改异常的迭代器ListIterator,那么Set集合如何解决呢?
思路就是对于修改单个元素,修改后使用break;退出。修改多个元素,则暂存在新的Set集合中,然后调用removeAll或者addAll方法
HashSet set = new HashSet<>();
set.add("小明");
set.add("小李");
set.add("小红");
set.add("苗苗");
set.add("笑笑");
//只删除一个元素
for(String elem : set){
if("小明".equals(elem)){
set.remove(elem);
break;
}
}
//删除多个元素
HashSet set1 = new HashSet<>();
for(String elem : set){
if(elem.indexOf("小") != -1){
set1.add(elem);
}
}
set.removeAll(set1);
System.out.println(set.size());//2
Comparable接口
- 实现该接口表示:这个类的实例可以比较大小,可以进行自然排序
- 该接口定义了默认的比较规则
- 其实现类需实现
compareTo(T o)
方法 - 返回值:如果调用方法的对象小于指定对象,则返回负整数,大于则返回正整数,等于则返回0
Comparator接口
- 比较工具接口,用于定义临时比较规则,而不是默认比较规则
- 其实现类需要实现
compare(T o1, T o2)
方法,返回值同Comparable
TreeSet类
- 基于TreeMap的NavigableSet实现,线程不安全的
- TreeSet集合是用来对象元素进行排序的,同样他也可以保证元素的唯一
- 添加到TreeSet的元素必须是可排序的,即具备比较能力,如果不具备,则运行时抛出ClassCastException异常
构造方法
public TreeSet(Comparator comparator)
传入指定的比较器进行排序
public TreeSet()
根据元素的自然顺序排序(必须实现Comparable接口)
排序说明
- 通过Comparable让集合内元素具备比较性
- 通过Comparator让集合具备比较某种类型元素的能力
- 当Comparator与Comparable冲突时,优先使用Comparator
- TreeSet判断元素唯一性的方法:就是元素大小顺序的比较方法,方法返回0时表示元素相等
- TreeSet之所以可以存储字符串,Integer等类型,因为它们实现了Comparable接口
TreeSet存储自定义类型
1.自定义类实现Comparable接口,重写compareTo方法
public class Person implements Comparable {
private String name;
private Integer age;
@Override
public int compareTo(Person p) {
// this: 新添加的元素,p: 集合中已经存在的老元素
// 先比较年龄
int result = this.age - p.age;
// 如果年龄相同,再比较姓名
return result == 0 ? this.name.compareTo(p.name) : result;
}
//测试
public static void main(String[] args) {
//使用自然排序规则
TreeSet treeSet = new TreeSet<>();
treeSet.add(new Person("小红",22));
treeSet.add(new Person("小明",18));
treeSet.add(new Person("张三",22));
for(Person person : treeSet){
//小明 小红 张三(多次遍历顺序是固定的)
System.out.print(person.getName() + " ");
}
}
}
2.定义比较器类,实现Comparator接口,使用比较器对象创建集合(也可以直接使用匿名内部类)
public static void main(String[] args) {
//使用匿名内部类方式调用TreeSet带比较器对象的构造方法
TreeSet treeSet = new TreeSet<>(new Comparator(){
@Override
public int compare(Person p1, Person p2) {
// 先比较年龄
int result = p1.getAge() - p2.getAge();
// 如果年龄相同,再比较姓名
return result == 0 ? p1.getName().compareTo(p2.getName()) : result;
}}
);
treeSet.add(new Person("小红",22));
treeSet.add(new Person("小明",18));
treeSet.add(new Person("张三",22));
for(Person person : treeSet){
//小明 小红 张三(多次遍历顺序是固定的)
System.out.print(person.getName() + " ");
}
}
Collections类
是java集合框架中,用来操作集合对象的工具类,常用方法如下
我们对之前实现排序规则的自定义类使用List存储并进行排序
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(new Person("小红",22));
list.add(new Person("小明",18));
list.add(new Person("张三",22));
//排序
Collections.sort(list);
for(int i = 0; i < list.size(); i++){
//小明 小红 张三
System.out.print(list.get(i).getName() + " ");
}
}
List与数组间的转换
List集合转换为数组:调用List的toArray(T[] a)
方法
数组转换为List集合:调用Arrays的asList(T... a)
方法
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(4);
list.add(2);
list.add(1);
list.add(3);
//排序
Collections.sort(list);
//1.List集合转换为数组
Integer[] array = new Integer[list.size()];
list.toArray(array);
//打印数组:[1, 2, 3, 4]
System.out.println(Arrays.toString(array));
//2.数组转换为List
List transfList = Arrays.asList(array);
//反转顺序
Collections.reverse(transfList);
//[4, 3, 2, 1]
System.out.println(transfList);
}
Map简介
- 双列集合,Map中的数据是以键值对( key-value)的形式存储的
- key-value以Entry类型的对象实例存在
- 可以通过key值快速地查找value
- 一个映射不能包含重复的键,每个键最多只能映射到一个值
常用方法
Entry接口:(Map.Entry)
- 映射项(键-值对),是Map
接口的一个内部接口,常用方法: -
K getKey()
获取键 -
V getValue()
获取值 -
V setValue(V value)
修改值
HashMap类
- 基于哈希表的Map接口的实现,对键进行无序唯一约束,是最常用的Map集合
- 允许使用null值和null键,线程不安全
- 适合插入、删除和定位元素
简单示例
public static void main(String[] args) {
//创建Map集合对象
Map map = new HashMap<>();
//向集合对象中添加元素
map.put("white", "红色");
map.put("white", "白色");
map.put("blue", "蓝色");
map.put("yellow", "黄色");
map.put("black", "黑色");
//根据key获取值
String color = map.get("white");
System.out.println(color); //白色
//根据key删除元素
map.remove("yellow");
//判断key是否存在
boolean yellowFlag = map.containsKey("yellow");
System.out.println(yellowFlag); //false
//遍历map元素
for (Entry entry : map.entrySet()) {
System.out.println("key:" + entry.getKey() + " ,value:" + entry.getValue());
//key:white ,value:白色
//key:blue ,value:蓝色
//key:black ,value:黑色
}
}
对键的唯一约束
HashMap对键的唯一性判断与HashSet相同,先判断键的hashCode()方法,如果相同则继续调用equals方法。更确切的说HashSet判断元素唯一性其实引用的是HashMap的判断机制
TreeMap类
- 能够对键进行排序的双列集合,要求存储的键必须是可比较的。
- 其排序原理与TreeSet相同。更确切的说TreeSet的排序其实是通过TreeMap完成的
构造方法
public TreeMap(Comparator comparator)
传入指定的比较器进行排序
public TreeMap()
根据元素的自然顺序排序(必须实现Comparable接口)
示例
对于给定的字符串"aabcbdeeeeedbddcc",要求获取字符串中每一个字母出现的次数。
输出结果格式:a(2)b(3)c(3)d(4)e(5)
public static void main(String[] args) {
String str = "aabcbdeeeeedbddcc";
//由于字母与次数存在一一对应关系,并且输出的字母是有序的,所以可以使用TreeMap
TreeMap map = new TreeMap();
//1,将字符串变成字符数组,因为要操作字符串中的每一个字母
char[] chars = str.toCharArray();
//2,遍历数组,将每一个字母都作为键去map集合中获取值
for (char key : chars) {
Integer value = map.get(key);
// 3,如果map中没有该字母,就加入该字母1次
if (value == null) {
map.put(key, 1);
} else {
//4, 如果有该字母,就将之前的次数+1
map.put(key, ++value);
}
}
// 5,将集合中的数据变成字符串打印
StringBuilder sb = new StringBuilder();
for (Entry entry : map.entrySet()) {
sb.append(entry.getKey()).append("(").append(entry.getValue()).append(")");
}
System.out.println(sb);
//a(2)b(3)c(3)d(4)e(5)
}
Map遍历的四种方式
- 通过entrySet方法遍历Entry对象,大多数情况下推荐使用这种方式
Map map = new HashMap<>();
for (Entry entry : map.entrySet()) {
System.out.println("key:" + entry.getKey() + " ,value:" + entry.getValue());
}
- 在for-each循环中单独遍历keys或values
Map map = new HashMap<>();
//遍历键
for (String key : map.keySet()) {
System.out.println("Key: " + key);
}
//遍历值
for (String value : map.values()) {
System.out.println("Value: " + value);
}
- 通过Iterator对Entry对象进行遍历
Map map = new HashMap<>();
Iterator> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = it.next();
System.out.println("key: " + entry.getKey() + " ,value: " + entry.getValue());
}
- 对键集合遍历,通过get方法获取值(耗时),效率低,不推荐使用
Map map = new HashMap<>();
for (String key : map.keySet()) {
String value = map.get(key);
System.out.println("Key: " + key + " ,Value: " + value);
}