单列集合
为了存储不同类型的多个对象,Java提供了一些特殊系列的类,这些类可以存储任意类型的对象,并且存储的长度可变,这些类统称为集合。可以简单的理解为一个长度可变,可以存储不同数据类型的动态数组。集合都位于java.util包中,使用集合时必须导入java.util包。在学习具体集合之前,先了解一下整个集合的核心继承体系:
单列集合:
双列集合
以上两张图中,红线框里的是接口类型,蓝线框里的是具体的实现类。集合里的核心接口如下表:
接口 | 描述 |
---|---|
Collection | 集合中最基本的接口,一般不直接使用该接口 |
List | Collection的子接口,一般用于存储有序,可重复,有索引的对象,是集合中最常用的接口之一 |
Set | Collection的子接口,一般用于存储无序,不可重复,无索引的对象 |
Map | 双列集合的根接口,用于存储一组键值对象,提供键到值的映射 |
一,Collection接口
Collection接口是单列集合的顶层接口(List,Set使它的子接口),既然是接口就不能直接使用,需要通过实现类!
它最常见的实现类是ArrayList,LinkedList,HashSet和TreeSet,它定义了各种具体实现类的共性,其他单列集合(实现类)直接或间接的继承该接口。
方法声明 | 功能描述 |
---|---|
boolean add(Object o) | 相当前集合中添加一个元素 |
boolean addAll(Collection c) | 将指定集合c中的所有元素添加到当前集合中 |
void clear() | 清空当前集合中的所有元素 |
boolean remove(Object o) | 删除当前集合中的指定元素 |
boolean removeAll(Object c) | 删除当前集合中包含的集合c的所有元素 |
boolean isEmpty() | 判断当前集合是否为空 |
boolean contains(Object o) | 判断当前集合是否包含某个元素 |
boolean containsAll(Collection c) | 判断当前集合是否包含指定集合c的所有元素 |
Iterator iterator() | 返回当前集合的迭代器。迭代器用于遍历集合中的所有元素 |
int size() | 获取当前集合的元素个数(集合长度) |
1,Collection集合的遍历
Collection这个系列集合的通用的3种遍历方式:
方法一:Iterator迭代器:
方法:new出来的集合,调用iterator()方法,创建一个迭代器对象,用来遍历集合。(不同之处是,遍历过程可删除元素)
public static void main(String[] args) {
//迭代器循环
//1,创建一个ArrayList集合
Collection<String> list=new ArrayList<>();
//向集合中添加元素
list.add("肘子");
list.add("大王");
list.add("晚上好呀");
//2,new出来的集合,调用iterator方法,创建一个迭代器对象,用来遍历集合
//迭代器就好比是一个箭头,默认指向集合的0索引处
Iterator<String> iterator = list.iterator();
//遍历
while (iterator.hasNext()){
//hasNext():判断当前集合中的指针位置是否存有元素(对象),有返回true,没有返回false
String s = iterator.next();//next():获取元素,并后移指针
//删除元素
/*迭代器遍历时,不能用集合中的方法,对元素进行增加或删除
如果实在要删除,可以用迭代器提供的remove方法进行删除
如果需要添加,暂时没有方法*/
if(s.equals("大王")){
iterator.remove();
}
System.out.println(s);
}
System.out.println(list);
//迭代器使用完毕,指针不会复位
}
结果:
📝注意:📝📝📝📝📝📝📝📝📝📝📝📝📝📝📝📝📝📝📝
1,迭代器在遍历集合的时候是不依赖索引的,他是通过创建指针并移动指针的方式,来获取集合中的元素
2,迭代器需要掌握的3个方法:
- 通过集合的iterator()方法,获取一个迭代器对象(迭代器就好比是一个箭头,默认指向集合的0 索引处);
- 通过迭代器对象的hasNext()方法,判断当前集合中的指针位置是否存有元素(对象),有返回true,没有返回false;
- 通过迭代器对象的next()方法获取元素,并后移指针,每一个next()方法都代表一次,获取元素,后移指针,所以一次循环里写一个next()方法。
3,迭代器的四个细节:
- 如果当前位置没有元素,还要强行获取,会报NoSuchElementException
- 迭代器遍历完毕,指针不会复位,所以再次遍历,需要重新创建一个迭代器对象
- 遍历的循环中只能用一次next()方法,因为运行一次指针就后移一位并获取当前指向的对象
- 迭代器遍历时,不能用集合里的方法进行增加或者删除,只能用迭代器里的方法。
方法二:增强for:
public static void main(String[] args) {
//增强for循环
//创建一个集合
Collection<String> list=new ArrayList<>();
//向集合中添加元素
list.add("肘子");
list.add("大王");
list.add("晚上好呀");
//遍历
for (String s : list) {
//s="bbb";//s属于第三方变量,s改变了,但是集合里面的元素不会改变
System.out.println(s);
System.out.println(list);
}
}
结果:
方法三:Lambda表达式:
public static void main(String[] args) {
//创建集合
Collection<String> list=new ArrayList<>();
//向集合中添加元素
list.add("肘子");
list.add("大王");
list.add("晚上好呀");
//遍历
//底层原理:
//其实也会自己遍历,依次得到每一个元素
//得到的元素,传递给下面的accept()方法
//s表示集合中的每一个元素
list.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
System.out.println("------------------");
//lambda表达式:()->{}
//():与方法的形参想匹配,数据类型可省略,形参只有一个,小括号可省略
//{}:与方法体相匹配
list.forEach(s-> System.out.println(s));//上面forEach()的简化版
}
结果 :
2,List接口
List接口继承Collection接口,允许存储重复的元素,所有的元素以线性方式存储。在程序中可以通过索引所有访问List接口实例中存储的元素,且存储的元素是有序的。
List集合作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了根据元素索引操作集合的特有方法。
特点:有序,可重复,有索引
在java.util包可以找到List接口, 它是一个ListIterator接口工厂。通过ListIterator,我们可以向前和向后迭代列表。List接口的实现类是ArrayList、LinkedList、Stack和Vector。ArrayList和LinkedList在Java编程中被广泛使用,Vector类自Java 5以来已弃用。
2.1接口的声明:
public interface List<E> extends Collection<E> ;
让我们详细说明如何在List类中创建对象或实例:
- 因为List是一个接口,所以不能按List类型创建对象。我们总是需要一个实现这个List的类来创建对象(如ArrayList)
- 在Java 1.5中引入泛型之后,可以限制可以存储在List中的对象的类型, 就像其他几个由用户定义的“类”实现的用户定义的“接口”一样
- List是一个“接口”,由ArrayList等类实现实现,在java.util 包下
2.2语法:
List<E> list = new ArrayList<E> ();
- E: 泛型数据类型,用于设置 objectName 的数据类型,只能为引用数据类型。添加的元素为基本类型的话,就要使用它的包装类,如下表:
- list: 创建的集合名
2.3操作:
现在让我们使用List 接口执行各种操作,以便更好地理解这些操作。我们将讨论下面列出的操作以及稍后通过干净的java代码实现的操作。
由于List是一个接口,它只能与实现该接口的类一起使用。现在,让我们看看如何对List执行一些常用操作。
- 使用add()方法向List类添加元素
- 使用set()方法更新List类中的元素
- 使用get()方法返回元素
- 使用remove()方法删除元素
现在,让我们分别讨论操作,并在代码中实现相同的操作,以便更好地掌握它。
1.添加操作
为了向列表中添加元素,可以使用add()方法。此方法被重载以基于不同参数执行多个操作。
参数:接受2个参数,分别为:
- add(Object):该方法用于在List的末尾添加一个元素。
- add(int index, Object):该方法用于在List的特定索引处添加元素
2. 更新操作
- 添加元素之后,如果我们希望更改元素,可以使用
set(int index,E element)方法
来完成。 - 因为List有索引的,所以我们想要更改的元素会被该元素的索引所引用。
- 因此,此方法接受一个索引和需要插入到该索引处的更新元素。
3. 返回操作
- 添加元素之后,如果我们需要得到某个元素,可以使用get(int index)
方法
来完成。
4. 删除操作
为了从列表中删除一个元素,我们可以使用remove()方法。此方法被重载以基于不同参数执行多个操作
Parameters:
- remove(Object):该方法用于简单地从List中删除一个对象。如果有多个这样的对象,则删除第一个出现的对象。
- remove(int index):由于List被建立了索引,所以这个方法接受一个整数值,它只是简单地删除List中特定索引处的元素。删除元素后,所有元素都向左移动以填充空间,并更新对象的索引。
实例:
public static void main(String[] args) {
//创建集合E
List<String> list=new ArrayList<>();
//添加元素 void add(int index,E element)
list.add("肘子");
list.add("大王");
list.add("晚上好呀");
list.add("上头的85批");
System.out.println("原本集合:"+list);
//删除指定元素 E remove(int index)
delete(list);
//修改指定索引元素 E set(int index,E element)
alter(list);
//返回指定索引元素 E get(int index)
obtainE(list);
}
private static void delete(List<String> list) {
System.out.println("删除指定元素---------------");
//表示删除索引为0的元素
list.remove(1);
System.out.println("表示删除索引为0的元素:"+ list);
//删除第一个元素(手动装箱,手动把基本数据类型肘子,转换成String类型,变成一个对象)
String i=String.valueOf("大王");
list.remove(i);
//为什么相同的remove()方法,运行的效果不同:
// 在调用方法的时候,如果方法出现了重载,
// 优先调用型参与实参数据类型相同的那一个
System.out.println("删除‘大王’元素:"+list);
}
private static void alter(List<String> list) {
System.out.println("替换指定元素--------------------只能替换存在的元素");
String s1 = list.set(0, "替换索引0");
System.out.println("返回的是被替换的元素:"+s1); //返回的是被替换的元素
System.out.println("list集合:"+list);
}
private static void obtainE(List<String> list) {
System.out.println("返回指定索引[0]元素-----------");
String s = list.get(0);
System.out.println(s);
System.out.println("集合list:"+list);
}
结果:
2.4 ArrayList
1,概念 🧩
ArrayList 类是一个实现了List接口的单列集合实现类,可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。ArrayList 继承了 AbstractList ,并实现了 List 接口。
2,ArrayList用于🧩
- 频繁访问列表中的某一个元素。
- 只需要在列表末尾进行添加和删除元素操作
- 不适合做大量的增删操作,而适合元素的查找
2.5 LinkedList
1,概念
java.util.LinkedList是一个实现了List接口的单列集合实现类,底层是一个双向链表,链表中的每一个元素都使用引用的方式记录它的前一个元素和后一个元素,从而将所有的元素连接起来。同时实现了实现了List
和Deque
接口
优点:🧸🧸🧸
- 由于LinkedList底层实现是双向链表,所以在进行插入和删除操作方面具有高效性。
- 由于LinkedList是动态的数据结构,因此在使用时不需要预先分配空间
- 由于LinkedList实现了Deque接口 在集合的队首和队尾可以进行高效的插入和删除操作
缺点:🧸🧸🧸
- 由于LinkedList底层实现是双链表,所以从集合中访问某个元素时需要从头开始遍历整个链表,所以其查询操作慢
- 由于LinkedList在进行元素对象存储时,需要将指向下一个元素和上一个元素的指针同时进行存储,其占用的内存空间相比ArrayList数组要更多。
- 由于LinkedList是基于链表实现的,因此不支持随机访问,只能通过遍历整个链表来访问元素
📝📝📝所以, LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。
2, LinkedList常用于
- 你需要通过循环迭代来访问列表中的某些元素。
- 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。
3,Set接口
List接口和Set接口都是Collection的子接口,两者不同的是:Set系列集合添加的元素是无序、不重复、无索引的。
Set系列集合:
- 无序:存取顺序不一致
- 不重复:可以去除重复的元素
- 无索引:没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素
Set集合的实现类:
- HashSet:无序,不重复,五索引
- LinkedHashSet:有序,不重复,无索引
- TreeSet:可排序,不重复,无索引
Set接口中的方法基本与Collection的API一致。没有额外方法去学习,直接使用Collection中常见方法就行。
利用Set系列集合存储字符串并遍历:
package day0606SetDemo;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.function.Consumer;
public class SetDemo1 {
public static void main(String[] args) {
//1,创建一个Set集合的对象
Set<String> s=new HashSet<>();
boolean b = s.add("肘子");
boolean b2 = s.add("西施");
boolean b3 = s.add("花木兰r");
//2,添加元素
//add()方法的返回值是Boolean类型,对于List来说没有意义,因为它允许元素重复,不管添加什么都返回true,而Set不允许元素重复
boolean b1 = s.add("肘子");
System.out.println(b);//true
System.out.println(b1);//false
//3,打印集合
//无序
System.out.println(s);//[肘子, 花木兰r, 西施]
//3,1迭代器遍历
Iterator<String> iterator = s.iterator();
while (iterator.hasNext()){//判断当前指向的位置是否有元素
String next = iterator.next();//获取当前元素并指针后移一位
System.out.println(next);
}
System.out.println("-------------------------");
//3,2增强for
for (String s1 : s) {
System.out.println(s1);
}
System.out.println("-------------------------");
//3,3Lambda表达式
s.forEach( str->System.out.println(str));//str表示集合里的每一个元素
}
}
注意:
结果:
1,HashSet
HashSet底层原理
- HashSet底层采取哈希表(核心)存储数据
- 哈希表是一种对于增删改查数据性能都较好的结构
哈希表组成
- JDK8之前:数组+链表
- JDK8开始:数组+链表+红黑树
哈希值
哈希表在底层它是有数组存在的,添加数据,它不是由0索引开始挨个往后添加的,而是根据数组长度和哈希值(公式:)计算出元素应该存入数组中哪个位置
- 对象的整数表现形式
- 根据hashCode方法算出来的int类型的整数
- 该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算(意义不是很大)
- 一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值
对象的哈希值特点
- 如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
- 如果已经重写hashCode方法,不同对象只有属性值相同,计算出的哈希值就是一样的
- 小部分情况下,不同的属性值或者不同的地址值计算出的哈希值也有可能一样(哈希碰撞)
重写hashCode方法
右键-->generate-->equals and hashCode-->next-->next-->next-->finish
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Demo2Stu demo2Stu = (Demo2Stu) o;
return age == demo2Stu.age && Objects.equals(name, demo2Stu.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
🎨总结🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨🎨
HashSet集合的底层数据结构是什么样的?
- JDK8之前:数组+链表
- JDK8开始:数组+链表+红黑树
底层添加数据原理是什么?
- 创建一个默认长度为16,默认加载因子为0.75的数组,数组名为table
- 加载因子作用:扩容。当数组存储长度达到16*0.75=12时,数组扩增1倍,数组长度变为32
- 如果集合中存储的是自定义对象,则必须要重写hashCode()和equals()方法
- 根据公式:计算出元素在数组当中应该存入的位置
- 判断该位置是否为null,如果是null直接存入
- 如果该位置不为null,表是有元素,则调用equals()方法比较属性值
- 一样:不存
- 不一样:存入数组。JDK8之前:新元素放到数组当中,原本的元素连接到新元素的下面,形成链表。JDK8开始:新元素连接到原本元素的后面,形成链表;
- 当链表的长度大于8而且数组长度大于等于64时,链表转成红黑树(JDK8以后)
HashSet为什么存和取的顺序不一样?
因为它是从0索引开始,一条链表一条链表开始遍历的。(数组有索引,集合没有)
HashSet为什么没有索引?
- 因为它是由数组、链表、红黑树3个组合而成的,无法定义索引
HashSet利用什么机制保证数据去重的?
HashCode方法和equals方法
- HashCode方法:根据哈希值确定数据添加到哪个位置
- equals方法:判断要添加的数据与原本该位置上的数据是否重复
2,LinkedHashSet
LinkedHashSet集合的特点和原理是什么?
- 有序、不重复、无索引
- 底层基于哈希表,但多了一个双向链表记录添加顺序
这个双向链表的目的是存取有序,而不是为了提高增删效率
以后如果要数据去重,我们使用哪个?
- 默认使用HashSet
- 如果要求去重且存取有序,才使用LinkedHashSet