第八章 集合
集合
在Java中,集合(Collection)是一种或用于存储和操作一组对象的数据结构。它提供了一组接口和类,用于处理和操作对象的集合。
集合框架是Java中用于表示操作集合的一组类和接口,它位于java.util包中,并提供了一系列的接口和类,包含集合接口Collection,列表接口List,集合类Set,映射接口Map等。
集合框架的主要目标是提供一种通用的方式来存储和操作对象的集合,无论集合的具体实现方法如何,用户都可以使用统一的接口和方法操作集合。
集合理解:
- 数组的长度是固定的,集合的长度是可变的
- 数组中存储的是同一类型的元素,集合存储的数据可以是不同类型的
- 数组可以存放基本类型数据或者引用类型变量,集合中只能存放引用类型的变量
- 集合框架由许多接口和实现类组成,定义并实现了很多方法,功能强大
集合分类
- 单列集合:每个集合元素只包含一个单独的对象,它是集合框架中最简单的形式
- 多列集合:每个集合由多个列组成,可以同时存储和操作多个相关的值
Collection
Collection接口是单列集合类的父接口,这种接口可以将数据一个一个的存放到集合中。有两个子接口,List和Set
添加元素add()
清空所有元素clear()
判断当前集合是否包含指定对象contains(Object o)
判断当前集合是否为空isEmpty()
把给定的对象,在当前集合中删除remove(Object o)
返回集合中元素的个数size()
把集合中的元素存储到数组中toArray()
Collection集合的遍历方式
迭代器是集合框架提供的一种遍历集合元素的方式。通过调用集合的iterator() 方法可以获取一个迭代器对象,然后使用迭代器的 hasNext() 方法判断是否还有下一个元素,使用 next() 方法获取下一个元素。
迭代器实现原理:
- 获取迭代器对象:集合类实现了 Iterable 接口,该接口定义了一个iterator() 方法,用于获取迭代器对象。迭代器对象是实现了 Iterator接口的具体类的实例。
- 迭代器位置初始化:在创建迭代器对象时,迭代器的位置通常初始化为集合的起始位置。不同的集合实现可能对位置初始化进行不同的处理。
- 遍历集合元素:通过调用迭代器对象的 hasNext() 方法,可以判断集合中是否还有下一个元素。如果有下一个元素,可以通过 next() 方法获取下一个元素,并将迭代器的位置后移。
- 迭代器状态管理:迭代器对象会记录当前迭代的状态,包括当前位置、遍历过程中的操作等。这些状态可以帮助迭代器在遍历过程中正确地访问和操作集合的元素。
- 结束迭代:当集合中没有更多元素时,迭代器的 hasNext() 方法将返回false ,表示遍历结束。
注意事项:
- 迭代器的有点事提供了一种统一的遍历方式,对单列集合,都可以使用迭代器按照顺序访问元素
- 迭代器隐藏了集合的具体实现细节,提供了一种抽象的访问接口,使代码变得更加灵活和可复用。
- 迭代器是单向的,只能从前往后遍历,无法倒序或在遍历过程中修改集合
Collection coll = new ArrayList<>();
coll.add("hello");
coll.add("wsx");
coll.add("123qwe");
coll.add(10001);
//1.集合转数组遍历.....把集合中的元素复制一份保存到一个数组中返回
Object[] array = coll.toArray();
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
System.out.println("----------------------------");
//2.增强for循环,foreach
//for( 数据类型 变量名:集合){}
for (Object obj : coll) {
System.out.println(obj);
}
System.out.println("----------------------------");
{
//foreach循环遍历数组
int[] arr = {1, 2, 3};
for (int i : arr) {
System.out.println(i);
}
System.out.println("----------------------------");
}
//3.使用迭代器对象遍历
//(1)获取迭代器对象
Iterator iterator = coll.iterator();
// System.out.println(iterator.next());
// System.out.println(iterator.next());
// System.out.println(iterator.next());
// System.out.println(iterator.next());
// System.out.println("----------------------------");
//(2)
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
1.每个迭代器对象都是一次性的
一个迭代器对象从头到尾进行的一次迭代是吧不可逆的
如果已经使用迭代器对集合进行了遍历
那么就需要新建一个迭代器进行重新遍历
2.迭代过程内外数据一致性问题
在迭代器对集合进行遍历的过程中
不能调用集合中的方法对元素进行增删操作
否则会在下一次next()方法调用时会抛出一个异常 ConcurrentModificationException 并发修改异常
3.如果确实有需求,在循环过程中需要删除某个元素
1)先记录要删除的对象的引用,遍历结束之后再删除
2)直接调用迭代器的删除功能
List接口
List接口继承了Collection接口
特点
- List是一种有序集合,按照顺序存储。
- List是一种带索引的集合,可以通过元素下标索引,精确查找对应的元素数据
- List集合可以存放重复元素,可以把相同的数据,在List结合中多次保存
//返回集合中指定位置的元素。
E get(int index);
//用指定元素替换集合中指定位置的元素,并返回被替代的旧元素。
E set(int index, E element);
//将指定的元素,添加到该集合中的指定位置上。
void add(int index, E element);
//从指定位置开始,把另一个集合的所有元素添加进来
boolean addAll(int index, Collection<? extends E> c);
//移除列表中指定位置的元素, 并返回被移除的元素。
E remove(int index);
//查收指定元素在集合中的所有,从前往后查到的第一个元素(List集合可以重
复存放数据)
int indexOf(Object o);
//查收指定元素在集合中的所有,从后往前查到的第一个元素(List集合可以重
复存放数据)
int lastIndexOf(Object o);
//根据指定开始和结束位置,截取出集合中的一部分数据
List subList(int fromIndex, int toIndex);
ArrayList
ArrayList类底层使用数组来实现数据的存储,特点是:增删慢,查找快
//1.创建List集合对象
List<String> list = new ArrayList<>();
//2.添加元素,默认尾部添加
list.add("hello1");
list.add("hello2");
list.add("hello3");
list.add("hello1");
System.out.println(list);
//3.指定位置添加元素
list.add(1, "world");
System.out.println(list);
//3.删除索引位置为2的元素
//boolean f = list.remove(2);
//4.修改指定位置元素
list.set(0, "briup");
System.out.println(list);
LinkedList
底层采用双向链表,特点是:增删快,查找慢
和ArrayList刚好相反,需要对集合中的元素做大量增删操作的时候,可以选择使用LinkedList
在 LinkedList 中,定义了一些操作头节点和尾节点的方法:
//将指定元素插入此列表的开头
void addFirst(E e)
//将指定元素添加到此列表的结尾
void addLast(E e)
//返回此列表的第一个元素
E getFirst()
//返回此列表的最后一个元素
E getLast()
//移除并返回此列表的第一个元素
E removeFirst()
//移除并返回此列表的最后一个元素
E removeLast()
//从此列表所表示的堆栈处弹出一个元素
E pop()
//将元素推入此列表所表示的堆栈
void push(E e)
Vector
是基于动态数据实现,线程安全,Vector在功能和使用方式上和ArrayList非常相似,在多线程环境中安全。
Set集合
srt接口继承了Collection接口,是常用的一种集合类型。
- Set是一种无序的集合
- Set是一种不带下标索引的集合
- Set是一种不能存放重复数据的集合
//实例化Set集合,指向HashSet实现类对象
Set set = new HashSet<>();
HashSet
HashSet是Set接口的实现类,使用哈希表作为底层数据结构来存储数据
HashSe的特点:
- 无序性:HashSet中的元素的存储顺序与插入顺序无关
- 唯一性:HashSet中不允许重复的元素,即每个元素都是唯一的
- 允许null元素:但是能存储一个null元素
- 高效性:HashSet的插入、删除和查找操作的时间复杂度都是O(1)
元素插入过程:
- 当向HashSet中插入元素时,先获取元素的hashCode()值,再检查HashSet中是否存在相同哈希值的元素,如果元素哈希值唯一,则直接插入元素
- 如果存在相同哈希值的元素,会调用元素的equals()方法来进一步判断元素是否是相同。如果相同,则不会插入重复元素;如果不同,则插入
hashCode()方法:
- hashCode() 方法是 Object类 中的一个方法,它返回一个 int 类型的哈希码值
- 通常情况下, hashCode() 方法会根据对象的属性值计算一个哈希码值(重写自定义类中 hashCode方法 )
- 如果两个对象的hashCode值不同,则两个对象的属性值肯定不同
- 如果两个对象的hashCode值相同,则两个对象的属性值可能相同,也可能不同
结论:如果要往HashSet集合存储自定义类对象,那么一定要重写自定义类中的hashCode方法和equals方法。
实体类(类似于Student类型)equals()和hashCode()方法重写规范
1.要么两个方法同时重写,要么同时不重写
2.重写两个方法要保证他们的结果具有一致性
如果两个对象进行equals()比较的结果为true,那么它们的hashCode()值应该是相同的。
原因:
1,规范性问题
2,保障对象在集合中存储时规则的正确性
例如:重写了equals,没有重写hashCode,两个属性一模一样的Student对象,通过重写后的equals比较结果为true,
但是没有重写hashCode的话,会得到两个根据内训地址进行哈希运算得到不同的结果,如果把这样两个对象,插入到一个HashSet集合中,就会判定为不同的对象导致插入不成功。
3,程序效率的提升
如果能够通过hashCode()直接区分出不同的对象,就尽量不要让它们
生成相同的hashCode()结果,然后通过equals()方法才能比较出他们不同。
这样的话会增加很多不必要的比较步骤执行,降低效率。
【equals()的比较成本是比hashCode()要高的】
1)hashCode()方法对于每个对象来说最多调用一次(在这个元素插入到集合的时候)
调用的结果会保存在对象所在的元素节点中一个名为hash的属性中,
后续需要和其他对象进行比较的时候,直接读取这个属性即可,无需重新调用hashCode()方法生成哈希值。
存在结果复用。
2)hash值的比较只是两个int值的==等值比较,效率很高。
而equals()的比较过程一般会比较复杂,步骤较多,例如:
- 地址是否一致
- 类型是否一致
- 强转类型
- 依次判断每个属性值是否一致
TreeSet
TreeSet是SortedSet的实现类,是基于红黑树数据结构实现
特点:
有序性:插入的元素会自动排序,每次插入,删除后都会自动排序
唯一性:TreeSet中不允许重复的元素,即集合中的元素是唯一的,如果尝试插入一个已有的元素,该操作会被忽略
动态性:TreeSet是一个动态的集合,可以根据需要随时添加,删除和修改元素
高效性:具有快速查找和插入性能。对于有序集合的操作,TreeSet通常比HashSet更高效。
1.TreeSet:自然排序
如果一个类实现了Comparable接口,并重写了compareTo方法,那么这个类的对选哪个就是可以比较大小的。
元素插入过程:
当向TreeSet插入元素时,TreeSet会使用元素的 compareTo() 方法来比较元素之间的大小关系。根据比较结果,TreeSet会将元素插入到合适的位置,以保持有序性。如果两个元素相等( compareTo() 方法返回0),TreeSet会认为这是重复的元素,只会保留一个。
this指已有的值,传入的参数表示新插入的值
注意事项:compareTo方法的放回结果,只关心正数、负数、零,不关心具体的值是多少
2.比较器排序/第三方排序/客户端排序/定制排序
比较器排序步骤:
- 创建一个实现了Comparator接口的比较器类,并重写其中的 compare() 方法
该方法定义了元素之间的比较规则。在 compare() 方法中,我们可以根据元素的属性进行比较,并返回负整数、零或正整数,来表示元素之间的大小关系。 - 创建TreeSet对象时,将比较器对象作为参数传递给构造函数,这样,TreeSet会根据比较器来进行排序。
也可以使用匿名内部类的形式
Comparator<Object> c = new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return 0;
}
};
compare方法说明:
int result = compare(o1, o2);
- result的值大于0,表示升序
- result的值小于0,表示降序
- result的值等于0,表示元素相等,不能插入
注意,这里和自然排序的规则是一样的,只关心正数、负数、零,不关心具体的值是多少
注意:如果同时使用自然排序和比较器排序,比较器排序将覆盖自然排序。
自然排序:
- 使用元素类实现Comparable接口,并重写其中的 compareTo() 方法
- 元素会按照其自身的比较规则进行排序
- 自然排序是默认的排序方式,可以直接使用TreeSet或Collections.sort()方法进行排序
- 只能有一种自然排序方式
比较器排序: - 使用Comparator对象来定义元素之间的比较规则
- 可以自定义排序规则,不依赖于元素类的实现Comparable接口
- 需要创建一个实现Comparator接口的比较器类,并重写其中的 compare()方法
- 可以有多个比较器,根据需要选择不同的比较器进行排序
- 可以通过传入Comparator对象给TreeSet或Collections.sort()方法来进行比较器排序
自然排序和比较器排序都有自己的应用场景。自然排序适用于元素类已经实现了Comparable接口,并且希望按照元素自身的比较规则进行排序的情况。比较器排序适用于需要自定义排序规则,或者元素类没有实现Comparable接口的情况。
LinkedHashSet
是HashSet的一个子类,具有HashSet的高效性和唯一特性,并且保持了元素的插入的顺序,其底层基于哈希表和链表实现
Map集合
Map集合是一种用于存储键值对(key-value)映射关系的集合类。它提供了一种快速查找和访问数据的方式,其中每个键都是唯一的,而值可以重复。
Map接口为双列集合的根接口。
Map集合的特点:
- 存储元素时,必须以key - value键值对的方式进行
- 键唯一性:Map集合中的键是唯一的,每个键只能对应一个值
- 可重复性:Map集合中的值可以重复,不同的键可以关联相同的值
- 高效的查找和访问:通过给定key键,可以快速获取与之对应的value值
Map集合内部使用哈希表或红黑树等数据结构来实现高效的查找和访问
- HashMap,底层借助哈希表实现,元素存取顺序不能保证一致。由于要保证键的唯一,不重复,粗腰重写键所属类的hashCode()方法,equals方法
- Hashtable :和之前List集合中的 Vector 的功能类似,可以在多线程环境中,保证集合中的数据的操作安全(线程安全)
- LinkedHashMap :该类是 HashMap 的子类,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致(存入顺序就是取出顺序)
- TreeMap :该类是 Map 接口的子接口 SortedMap 下面的实现类,和TreeSet 类似,它可以对key值进行排序,同时构造器也可以接收一个比较器对象作为参数。支持key值的自然排序和比较器排序俩种方式(支持key排序)
Map的遍历
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "hello");
map.put(3, "world");
map.put(4, "hi");
map.put(2, "welcome");
System.out.println("map = " + map);
//1.键列视图遍历
//keySet()
Set<Integer> keys = map.keySet();
for (Integer key : keys) {
System.out.println("key = " + key + " value = " + map.get(key));
}
System.out.println("===================================");
//2.值列视图遍历 无法根据value反向获取key
Collection<String> values = map.values();
for (String value : values) {
System.out.println("value = " + value);
}
System.out.println("===================================");
//3.键值对视图遍历 如果需求是同时操作键和值,性能最高
Set<Map.Entry<Integer, String>> entries = map.entrySet();
for (Map.Entry<Integer, String> entry : entries) {
System.out.println("key = " + entry.getKey() + " value = " + entry.getValue());
}
System.out.println("===================================");
//*4.借助forEach()语法
map.forEach((key, value) -> System.out.println("key = " + key + " value = " + value));
HashMap
HashMap底层借助哈希表实现,元素的存取顺序不能保证一致。
HashMap存取的键值对时,如果键类型为自定义类,那么一般需要重写所属类的hashCode方法和equals方法
HashMap的特点:
- 键唯一
- 值可以重复
- 无序性
- 线程不安全
- 键和值都允许为null(键只能有一个null,键唯一性)
适用Map的场景:
1、Java中需要保存一些数据,数据本身具备键值对结构
例如HTTP请求参数、某些配置文件
2、数据本身是单值特征,但是为了方便操作这些数据,
故意设计为Map结构,用对象的唯一标识特征作为Key
Hashtable
Hashtable特点:
- JDK1.0提供,接口方法较为复杂,后期实现了Map接口
- 线程安全:Hashtable是线程安全的,相对于HashMap性能稍低
- **键和值都不能为null,**如果尝试使用null作为键和值,将会抛出NullPointerException(因为源代码会检验value的值是否为null,并且抛出异常,key则会调用hashCode方法,如果key为null,则会抛出异常)
- 哈希表实现:和HashMap一样,内部使用哈希表数据结构来存储键值对
单线程环境下建议使用HashMap,性能更好
多线程环境下,建议使用HashMap+Collection
TreeMap
TreeMap是有序映射实现类,它实现了SortedMap接口,基于红黑树数据结构来存储键值对
TreeMap特点:
- 键的排序:TreeMap中的键是按照自然顺序或自定义比较器进行排序的
- 红黑树实现:TreeMap内部使用红黑树这种自平衡二叉搜索树数据结构来存储键值对
- 键唯一,值可以重复
- 线程不安全,如果在多线程环境下使用TreeMap,应该使用Collection工具类处理
- 初始容量:TreeMap没有初十容量的概念,它会根据插入的键值对动态的调整红黑树的大小
可以重写自然排序的方法,也可以采用比较器排序
TreeMap底层借助红黑树实现,它提供了高效的有序映射功能,可以用于范围查找、排序和遍历等操作。但红黑树的平衡操作会带来额外的开销,相比于HashMap等实现类,TreeMap在插入和删除操作上可能稍慢。
LinkedHashMap
是HashMap的一个子类,底层在哈希表基础上,通过维护一个双向链表来保持键值对的有序性,可以保证存次序一致
泛型
泛型(Generics)的概念是在JDK1.5中引入的,它的主要目的是为了解决类型安全性和代码复用的问题。
泛型是一种强大的特性,它允许我们在定义类、接口和方法时使用参数化类型 。
//T是数据类型,但是不是确定的数据类型
//程序员在使用该类的时候,传入什么具体的类型给T,T就代表什么类型
public class MyClass<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
MyClass 是一个泛型类,使用类型参数 T 。我们可以在创建对象时指定具体的类型,例如 MyClass 或 MyClass 。
泛型能够使我们编写出来通用的代码,提高代码的可读性和重用性。通过使用泛型,我们可以在类、接口和方法中使用类型参数,使得代码可以处理不同类型的数据,同时保持类型安全。
自定义泛型
泛型类
static class Container<T> {
private T value;
public Container() {
}
public Container(T value){
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
泛型接口
public interface Action<T> {...}
public static void main(String[] args) {
//创建匿名内部类
Action<String> a = new Action<>() {
//...
};
}
泛型方法
注意:泛型方法调用时不需要额外指定泛型类型,系统自动识别泛型类型。
public <F> void disp(F f) {
System.out.println("in 泛型方法disp, f: " + f);
}
注意事项
ArrayList list = new ArrayList();
注意,= 号俩边的所指定的泛型类型,必须是要一样的
通配符 ?
public void test1(Collection<Integer> c) {
}
public void test2(Collection<String> c) {
}
public void test3(Collection<Double> c) {
}
/*
在这种情况下,就可以使用通配符(?)来表示泛型的父类型:
注意,这时候test方法中的参数类型,使用了泛型,并且使用问号来表示这
个泛型的类型,这个问号就是通配符,可以匹配所有的泛型类型
test方法可以接收 泛型是任意引用类型的 Collection集合对象
*/
public void test(Collection<?> c) {
}
通配符所带来的问题
Collection<?> c;
c = new ArrayList<String>();
//编译报错
//因为变量c所声明的类型是Collection,同时泛型类型是通配符(?)
//那么编译器也不知道这个?将来会是什么类型,因为这个?只是一个通配符
//所以,编译器不允许使用变量c来向集合中添加新数据。
c.add("hello");
//编译通过
//但是有一个值是可以添加到集合中的,null
//集合中一定存的是引用类型,null是所有引用类型共同的一个值,所以一定可
以添加进去。
c.add(null);
泛型边界
泛型的上限:
格式: 类型名<? extends 类型 > 对象名称
意义: 只能接收该类型及其子类
例如: List<? extends Number> list
将来引用list就可以接收泛型是 Number 或者 Number 子类型的List集合对象
public static void main(String[] args) {
List<? extends Number> list;
//list可以指向泛型是Number或者Number【子】类型的集合对象
list = new ArrayList<Number>();
list = new ArrayList<Integer>();
list = new ArrayList<Double>();
//编译报错,因为String不是Number类型,也不是Number的子类型
//list = new ArrayList<String>();
}
泛型的下限:
格式: 类型名<? super 类型 > 对象名称
意义: 只能接收该类型及其父类型
例如: List<? super Number> list
将来引用list就可以接收泛型是 Number 或者 Number 父类型的List集合对象
public static void main(String[] args) {
List<? super Number> list;
//list可以指向泛型是Number或者Number【父】类型的集合对象
list = new ArrayList<Number>();
list = new ArrayList<Serializable>();
list = new ArrayList<Object>();
//编译报错,因为String不是Number类型,也不是Number的父类型
//list = new ArrayList<String>();
//编译报错,因为Integer不是Number类型,也不是Number的父类型
//list = new ArrayList<Integer>();
泛型中 extends 和 super 对比:
- 使用extends可以定义泛型的【上限】,这个就表示将来泛型所接收的类型【最大】是什么类型。可以是这个最大类型或者它的【子类型】。
- 使用super可以定义泛型的【下限】,这个就表示将来泛型所接收的类型【最小】是什么类型。可以是这个【最小类型】或者它的【父类型】。
泛型擦除
泛型类型只存在于编译期间,编译后的字节码和运行时不包含泛型信息,所有的泛型类型映射到同一份字节码
由于泛型是JDK1.5才加入到Java语言特性的,Java让编译器擦除掉关于泛型类型的信息,这样使得Java可以向后兼容之前没有使用泛型的类库和代码,因为在字节码(class)层面是没有泛型概念的。
Java的泛型只存在于编译期间,泛型使得编译器可以在编译期间对类型进行检查以提高类型安全,减少运行时由于对象类型不匹配引发的异常
编译成功后,所有泛型信息都会被擦除,变为原始的Object