集合上
学习目标
1.掌握集合框架体系
2.Collection接口公共方法
3.Iterator迭代器的使用
4.list接口基本使用
5.Set接口基本使用
一、集合框架概述
为什么要有集合?
之前我们学习过数组,通过数组可以存储一批数据。但是数组存取数据限定了容器的大小和类型,存放并不方便。所以java
引入了集合的概念。集合可以随着使用者存取数据的多少而自动扩充容器的大小。
Java 集合可分为 Collection 和 Map 两种体系
- Collection接口:单列数据,定义了存取一组对象的方法的集合
- List:元素有序、可重复的集合
- Set:元素无序、不可重复的集合
- Map接口:双列数据,保存具有映射关系“key-value对”的集合
Collection体系架构如下:
Map体系架构如下:
二、Collection 接口
2.1 Collection接口介绍
Collection 接口是 List、Set 父接口,该接口里定义的方法 既可用于操作 Set 集合,也可用于操作 List 集合。
JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现。
在 Java5
之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5
增加了泛型以后,Java 集合可以记住容器中对象的数据类型
2.2 Collection常用方法
Collection接口常用方法如下:
-
添加
- add(Object obj) 添加一个元素
- addAll(Collection coll) 添加一个集合内的所有元素
-
查询
- int size() :获取有效元素的个数
- boolean isEmpty() :是否是空集合
- boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
- boolean containsAll(Collection c):也是调用元素的equals方法来比 较的。拿两个集合元素挨个比较。
-
删除
-
void clear() 清空集合元素
-
boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
-
boolean removeAll(Collection coll):取当前集合的差集
-
-
遍历
- Object[] toArray() :转成对象数组
- iterator():返回迭代器对象,用于集合遍历
-
取两个集合的交集 boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c
-
集合是否相等 :boolean equals(Object obj)
注意:向Collection容器中添加数据obj时,要求obj所在类要重写equals()
示例1:Collection添加方法演示
//添加方法演示
@Test
public void test1(){
//创建一个Collection集合
Collection<Integer> collection = new ArrayList();
//添加元素
collection.add(1);
collection.add(2);
Collection<Integer> collection2 = new ArrayList();
collection2.add(3);
collection2.add(4);
//添加一个集合中的所有元素
collection.addAll(collection2);
System.out.println(collection);
}
示例2:Collection查询方法演示
@Test
public void test2(){
//创建一个Collection集合
Collection<User> collection = new ArrayList();
//向集合添加数据
User user = new User(1,"张三",18);
User user2 = new User(2,"李四",19);
User user3 = new User(3,"王五",20);
collection.add(user);
collection.add(user2);
collection.add(user3);
//查看集合的长度
System.out.println(collection.size());
//查看集合是否为空
System.out.println(collection.isEmpty());
//查看是否包含用户张三,需要重写equals方法
System.out.println(collection.contains(new User(1,"张三",18)));
//创建一个collection是否包含collection2的所有元素
Collection<User> collection2 = new ArrayList();
collection.add(new User(2,"李四",19));
collection.add(new User(3,"王五",20));
System.out.println(collection.containsAll(collection2));
}
User类的内容如下:
class User{
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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;
}
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public User() {
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return id == user.id && age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name, age);
}
}
示例3:删除方法演示
//删除方法演示
@Test
public void test3(){
//创建一个Collection集合
Collection<User> collection = new ArrayList();
//向集合添加数据
User user = new User(1,"张三",18);
User user2 = new User(2,"李四",19);
User user3 = new User(3,"王五",20);
collection.add(user);
collection.add(user2);
collection.add(user3);
//删除张三用户
/* System.out.println(collection.remove(new User(1,"张三",18)));
System.out.println(collection);*/
//删除一个集合内的所有元素
Collection<User> collection2 = new ArrayList();
collection2.add(new User(1,"张三",18));
collection2.add(new User(2,"李四",19));
collection.removeAll(collection2);
System.out.println(collection);
//清空集合元素
collection.clear();
System.out.println(collection);
}
示例4:遍历集合
//遍历集合
@Test
public void test4(){
//创建一个Collection集合
Collection<User> collection = new ArrayList();
//向集合添加数据
User user = new User(1,"张三",18);
User user2 = new User(2,"李四",19);
User user3 = new User(3,"王五",20);
collection.add(user);
collection.add(user2);
collection.add(user3);
//集合转数组
Object[] objects = collection.toArray();
for(Object object: objects){
System.out.println(object);
}
}
示例5:其他方法
//集合的其他方法
@Test
public void test5(){
//创建一个Collection集合
Collection<Integer> collection = new ArrayList();
collection.add(1);
collection.add(2);
collection.add(3);
Collection<Integer> collection2 = new ArrayList();
collection2.add(1);
collection2.add(2);
//求collection与collection2集合的交集
System.out.println(collection.retainAll(collection2));
System.out.println(collection);
//判断collection与collection2集合是否相等
System.out.println(collection.equals(collection2));
}
三、Iterator迭代器
3.1迭代器的一些相关概念
Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
迭代器模式的定义:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。类似于“公交车上的售票员”、“火车上的乘务员”、“空姐”。
Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
3.2迭代器的使用
迭代器的常用方法如下:
迭代器应用过程如下:
示例1:使用迭代器遍历集合
@Test
public void test1() {
Collection c1 = new ArrayList();
c1.add(1);
c1.add(2);
c1.add(3);
//1.得到迭代器对象
Iterator iterator = c1.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
}
3.3迭代器的执行原理
迭代器的底层原码如下:
3.4迭代器的常见错误
易错1 连续调用两次next()方法
@Test//迭代器使用易错1
public void test1() {
Collection c1 = new ArrayList();// 0 1 2
c1.add(1);
c1.add(2);
c1.add(3);
//1.得到迭代器对象
Iterator iterator = c1.iterator();
while(iterator.next()!=null) {
System.out.println(iterator.next());
}
}
易错2 一次遍历使用两个迭代器
@Test//迭代器使用易错2
public void test2() {
Collection c1 = new ArrayList();// 0 1 2
c1.add(1);
c1.add(2);
c1.add(3);
while(c1.iterator().hasNext()) {
System.out.println(c1.iterator().next());
}
}
易错3
- Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法。
- 如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法, 再调用remove都会报IllegalStateException。
@Test//迭代器使用易错3
public void test3() {
Collection c1 = new ArrayList();// 0 1 2
c1.add(1);
c1.add(2);
c1.add(3);
Iterator iterator = c1.iterator();
//Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,
// 不是集合对象的remove方法。
// 报错
/*while(iterator.hasNext()) {
System.out.println(iterator.next());
c1.remove(1);
}*/
while(iterator.hasNext()){
/*报错
iterator.remove();*/
System.out.println(iterator.next());
//正确
iterator.remove();
}
System.out.println(c1);
}
3.5 小结
- 迭代器原理
- 迭代器使用
- 迭代器常见错误
四、List集合体系
4.1 List接口概述
鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组 。
List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
List接口的实现类常用的有:ArrayList
、LinkedList
和Vector
。
4.2 List接口常用方法
List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。具体方法如下
- void add(int index, Object ele):在index位置插入ele元素
- boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
- Object get(int index):获取指定index位置的元素
- int indexOf(Object obj):返回obj在集合中首次出现的位置
- int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
- Object remove(int index):移除指定index位置的元素,并返回此元素
- Object set(int index, Object ele):设置指定index位置的元素为ele
- List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex 位置的子集合
示例1:list接口的常用方法
public class ListDemo {
@Test//添加
public void test1() {
//1.创建list集合
List list = new ArrayList();
//2.向集合中指定索引处添加元素
list.add(0, 1);
list.add(1,"哈哈");
//报索引越界异常,即便集合可以自动扩充容器大小,也必须按照顺序使用下标
//list.add(4,"嘻嘻");
list.add(2,"嘻嘻");
System.out.println(list);
List list2 = new ArrayList();
list.add(0, "a");
list.add(1, "b");
//3.向集合中指定索引处添加集合元素
list.addAll(0, list2);
System.out.println(list);
}
@Test //删除
public void test2() {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
//1.根据指定下标删除元素并返回
Object obj = list.remove(0);
System.out.println(obj);
System.out.println(list);
}
@Test //查询
public void test3() {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
//1.根据下标获得指定位置元素
Object object = list.get(0);
System.out.println(object);
//2.查看是否为空
System.out.println(list.isEmpty());
//3.查看集合大小
System.out.println(list.size());
}
@Test //修改
public void test4() {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
//1.根据指定下标修改元素内容
list.set(0, 11);
System.out.println(list);
}
@Test //遍历
public void test5() {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
//1.遍历1 迭代器
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
//2.foreach
for(Object obj : list) {
System.out.println(obj);
}
//3.foreach循环
for(int i=0;i<list.size();i++) {
System.out.println(list.get(i));
}
}
@Test //其他方法了解
public void test6() {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
list.add(1);
//1.返回obj在集合中首次出现的位置
System.out.println(list.indexOf(1));
//2返回obj在当前集合中末次出现的位置
System.out.println(list.lastIndexOf(1));
//3.返回从fromIndex到toIndex 位置的子集合
System.out.println(list.subList(1, 3));
}
}
4.3 ArrayList
ArrayList 是 List 接口的典型实现类、主要实现类 ,本质上,ArrayList是对象引用的一个”变长”数组 。
ArrayList的JDK8之前与之后的实现区别?
- JDK7:ArrayList像饿汉式,直接创建一个初始容量为10的数组
- JDK8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为10的数组
Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合 。
jdk7ArrayLIst底层源码如下:
jdk8底层源码
jdk8底层源码大致和jdk7相同,不同的是jdk8是在第一次添加数据的时候进行容器初始化
4.4 LinkedList
4.4.1 LinkedList 基本使用
对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高 ;LinkedList实现类特有的方法如下:
- void addFirst(Object obj)
- void addLast(Object obj)
- Object getFirst()
- Object getLast()
- Object removeFirst()
- Object removeLast()
4.4.2 LinkedList 底层原理
LinkedList
:双向链表,内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:
- prev变量记录前一个元素的位置
- next变量记录下一个元素的位置
LinkedList的底层源码如下:
总结: LinkedList底层运用Node对象存储数据,而每个Node对象都保存了它上一个对象和下一个对象的内存地址形成链表结构。
4.5 Vector
4.5.1基本使用
-
Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
-
在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时, 使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。
4.5.2 底层解析
底层都创建了长度为10的数组。在扩容方面,默认扩容为原来的数组长度的2倍。其余部分的源码和ArrayList大致相同
4.6 小结
面试题
请问ArrayList/LinkedList/Vector
的异同?并简述底层原理
参考答案
- ArrayList底层是动态数组结构,容器大小为10,每次扩容1.5倍;线程是不安全的;执行查询、修改操作效率较高。
- LinkedList底层是双向链表结构,用Node对象存储数据,Node对象保存了上一个数据和下一个数据的内存地址;LinkedList是线程不安全的;执行添加、删除操作效率较高。
- Vector底层是动态数组结构,底层原理和ArrayList基本相同;线程是安全的;增删改查效率明显低于ArrayList。
五、Set集合体系
Set接口是Collection的子接口,set接口没有提供额外的方法
Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。
Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
@Test
public void test1() {
Set set = new HashSet();
//1.添加方法
set.add(1);
set.add(2);
set.add(3);
//2.删除
set.remove(2);
//3.没有修改方法
//4.查询方法
System.out.println(set.isEmpty());
System.out.println(set.contains(1));
System.out.println(set.size());
//5.遍历
for(Object obj : set) {
System.out.println(obj);
}
//6.迭代器
Iterator iterator = set.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
}
5.1 HashSet
5.1.1 HashSet特点概述
HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
HashSet 具有以下特点:
- 不能保证元素的排列顺序
- HashSet 不是线程安全的
- 集合元素可以是 null
HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相 等,并且两个对象的 equals() 方法返回值也相等。
对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
5.1.2 HashSet 底层实现过程
jdk7种HashSet的底层原理
- 向
HashSet
中添加元素的过程:- 当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法 来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布, 该散列函数设计的越好)
- 如果两个元素的hashCode()值相等,会再继续调用equals方法,如果equals方法结果 为true,添加失败;如果为false,那么会保存该元素,但是该数组的位置已经有元素了, 那么会通过链表的方式继续链接。
- 如果两个元素的 equals() 方法返回 true,但它们的
hashCode()
返回值不相等,hashSet
将会把它们存储在不同的位置,但依然可以添加成功。(开发时注意hashCode
值要和equals() 方法保持一致)
相对于jdk7来说,jdk8主要在以下几方面进行调整:
-
new HashSet():底层没有创建一个长度为16的数组
-
jdk 8底层的数组是:Node[],而非Entry[]
-
首次调用add()方法时,底层创建长度为16的数组
-
jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
-
形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
-
当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
5.1.3 总结HashCode()与equals()方法重写原则
equals方法只能比较引用数据类型,默认比较的是内存地址是否相等,可以重写Object类的equals方法,重写为比较对象的内容是否相等。
hashCode方法显示的是对象的哈希值;支持此方法是为了方面使用HashMap所提供的散列表。
对象的哈希码值默认是根据对象的内存地址数值经过哈希算法得出的哈希值。所以哈希值不是内存地址,但和内存地址有关系。因此hashCode要满足如下约束:
- 如果两个对象的equals(obj)方法是相等的,那么对应的hashCode值也是一样的。
- 如果两个对象的equals(obj)方法是不相等的,那么对应的hashCode值尽量不一样,如果一样会影响HashMap容器的性能。
结论:**重写equals方法的同时要重写hashCode()方法;**因为equals方法默认比较的是内存地址,那么对应的hashCode也是默认设计为对象地址相同,hashCode一定相同;对象地址不同,hashCode值尽量不一样(一样也没事);
重写后的equals方法比较的是对象的内容是否相等,那么对应的hashCode也要重写为相同内容的对象hashCode值一定相等,对象内容不同的hashCode值尽量不等(不一样也没事)。
5.1.4 IDEA工具里hashCode()
的重写
- 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的 “冲突”就越少,查找起来效率也会提高。(减少冲突)
- 并且31只占用5bits,相乘造成数据溢出的概率较小。
- 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
- 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)
5.2 LinkedHashSet
LinkedHashSet 是 HashSet 的子类
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置, 但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
5.3 TreeSet 讲解
5.3.1TreeSet特点
TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
TreeSet 底层使用红黑树结构存储数据
TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
特点:有序(按照升序排序),查询速度比List快
5.3.2 自然排序
自然排序实现如下:
/**
* 自然排序:按照用户年龄排序如果,年龄相同按照姓名排序
*/
public class TreeSetDemo1 {
public static void main(String[] args) {
//创建TreeSet集合
Set<User> set = new TreeSet();
User user1 = new User(1,"zhangsan",18);
User user2 = new User(2,"lisi",19);
User user3 = new User(3,"wangwu",18);
set.add(user1);
set.add(user2);
set.add(user3);
System.out.println(set);
}
}
class User implements Comparable<User>{
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public User() {
}
@Override
public int compareTo(User user) {
if(user==null){
throw new NullPointerException("排序的对象不能为null");
}
int result = this.age-user.getAge();
return result==0?this.name.compareTo(user.getName()):result;
}
}
5.3.3 定制排序
定制排序实现如下:
/**
* 定制排序:按照用户年龄排序如果,年龄相同按照姓名排序
*/
public class TreeSetDemo2 {
public static void main(String[] args) {
//创建TreeSet集合
Set<User> set = new TreeSet(new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
if(u1==null || u2==null){
throw new NullPointerException("排序的对象不能为null");
}
int result = u1.getAge()-u2.getAge();
return result==0?u1.getName().compareTo(u2.getName()):result;
}
});
User user1 = new User(1,"zhangsan",18);
User user2 = new User(2,"lisi",19);
User user3 = new User(3,"wangwu",18);
set.add(user1);
set.add(user2);
set.add(user3);
System.out.println(set);
}
}
class User{
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public User() {
}
}
5.4 面试题
Set 集合实现类 HashSet LinkedHashSet TreeSet 区别
HashSet 底层采用 数组+哈希算法+单向链接结构+红黑树的方式存取数据 ,添加、删除、修改、查询都很快;HashSet可以存储null值,HashSet是线程不安全的;HashSet底层采用哈希算法计算元素在数组下标中的位置,如果该位置没有数据直接存储,有数据比较哈希值,不同以链表的方式存储,相同比较equals方法,不同继续以链表的方式存储,相同添加失败。
LinkHashSet 是HashSet 的子类 ,在HashSet 基础上 添加双向链表结构。在添加、删除的时候效率降低,但是遍历集合元素时效率增加;可以存储null ,看起来有序,底层无序;LinkedHashSet线程是不安全的。
TreeSet 底层采用红黑二叉树结构实现,查询速度极快,比list集合还要快;TreeSet不可以存储null,TreeSet是线程不安全的。
六、练习题
第一题
练习:在List内去除重复数字值,要求尽量简单
public static List duplicateList(List list) {
HashSet set = new HashSet();
set.addAll(list);
return new ArrayList(set);
}
第二题
王子向左走5步,王子向上走1步,王子向下走3步,王子向左走3步。
- 求王子走了几个方向
public static Set getStep(String str) {
Set set = new HashSet();
for(String s : str.split(",")) {
set.add(s.substring(s.indexOf("向")+1, s.indexOf("走")));
}
return set;
}