集合概述
集合类:提供一种存储空间可变的存储模型
List系列集合特点
元素有序、可重复、有索引。
- 有序:存和取的顺序是一致的;
- 可重复:集合中的元素可重复;
- 有索引:可以通过索引获取集合中的元素。
Set系列集合特点
元素无序、不可重复、无索引。
- 无序:存和取的顺序有可能不一致;
- 不可重复:元素不可重复;
- 无索引:不能通过索引获取元素。
一、单列集合
单列集合特点
- 单列集合一次只能存一个元素。
单列集合继承体系
红色:接口
蓝色:实现类
单列集合的选择
条件 | 选择 |
---|---|
默认 | ArrayList |
存储元素不重复 | HashSet |
元素不重复且存取一致 | LinkedHashSet |
元素不重复且可指定排序规则 | TreeSet |
1 Collection
Collection
是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。
- JDK不提供此接口的任何直接实现,它提供更具体的子接口(如Set、List)实现
导包:java.util.Collection
Collection方法及说明
方法 | 说明 |
---|---|
boolean add(E e) | 添加元素,并返回是否添加成功 |
boolean remove(E e) | 从集合中移除指定的元素 |
boolean contains(Object o) | 判断集合中是否存在指定的元素 |
boolean isEmpty() | 判断集合是否为空(长度是否为0) |
int size() | 计算集合长度,即元素个数 |
void clear() | 清空集合中的元素(慎用) |
注意:
- 方法
add(E e)
- 在List系列中,返回值永远是true,因为List系列允许数据重复。
- 在Set系列,如果添加不重复元素返回true,反之返回false。
- 方法
remove(Object o)
- Collection里面定义的是共性的方法,所以只能提供元素名删除。
- 方法
contains(Object o)
- 该方法的底层是依赖
equals
方法判断的。
要想拿来判断自定义类型,必须在自定义类型的JavaBean类中重写equals
方法。
Collection遍历方式
场景 | 使用方式 |
---|---|
仅遍历 | 增强for、Lambda |
遍历的同时需要删除某元素 | 迭代器 |
1 迭代器
2 增强for
其底层就是迭代器。目的是为了简化代码。
所有的单列集合、数组才能用增强for遍历。
格式:
//快速生成:集合名.for
for (E e : 数组或集合) {
...
}
import java.util.ArrayList;
import java.util.Collection;
public class 增强forDemo {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("Hello");
coll.add("World");
for (String s : coll) {
System.out.println(s);
}
}
}
3 Lambda表达式
JDK8以后,更简单更方便的遍历方式。
方法:forEach(Consumer<E> c)
其底层是使用for遍历集合,并将值传给accept
方法。
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Consumer;
public class LambdaDemo {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("Hello");
coll.add("World");
//匿名内部类
coll.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
//Lambda
coll.forEach(s -> System.out.println(s));
}
}
2 List
List是一个接口。
导包:java.util.List
List集合特点
-
有序集合
存储和取出的元素顺序一致。
-
可重复
与
Set
集合不同,List
集合允许重复元素 -
有索引
可以精确的控制列表中每个元素的插入位置,可以通过整数索引访问元素,搜索列表中的元素.
List常用方法
来自父类Collection
Collection方法 | 说明 |
---|---|
boolean add(E e) | 添加元素,并返回是否添加成功 |
boolean remove(E e) | 从集合中移除指定的元素 |
boolean contains(Object o) | 判断集合中是否存在指定的元素 |
boolean isEmpty() | 判断集合是否为空(长度是否为0) |
int size() | 计算集合长度,即元素个数 |
void clear() | 清空集合中的元素(慎用) |
在List系列中,
add(E e)
方法返回值永远是true,因为List系列允许数据重复。
List特有方法
方法 | 说明 |
---|---|
void add(索引,元素) | 在指定位置插入指定元素 |
E remove(索引) | 删除指定位置的元素,并返回该元素 |
E set(索引,元素) | 修改指定位置元素,返回被修改的元素 |
E get(索引) | 返回指定索引处的元素 |
注意:
对于list系列集合remove()
方法有两种:remove(对象), remove(索引)
public class Demo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.remove(1); //这里删除的是 索引1 上的元素
/*
如果方法有重载,优先调用实参与形参类型一致的那个方法。
实参 1 是 int 类型,传递索引的remove方法的形参就是int类型。所以会优先调用它。
*/
//如果想删除 元素1 ,可以删除 索引0 , 也可以手动装箱。
Integer i = Integer.valueOf(1);
list.remove(i);
}
}
List遍历方式
除了可以使用Collection集合中的三种遍历方式,还有以下两种独有遍历方法。
场景 | 使用方式 |
---|---|
仅遍历 | 增强for、Lambda |
遍历的同时需要删除元素 | 迭代器 |
遍历的同时需要添加元素 | 列表迭代器 |
需要对索引操作 | 普通for |
普通for
for (int i=0; i<list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
列表迭代器
ListIterator
:列表迭代器。
列表迭代器对象可以通过List集合的ListIterator()
方法得到,是List集合特有的迭代器。
- 该迭代器可以从反方向遍历List系列集合,但是要先把指针移至最后。
常用方法
方法 | 说明 |
---|---|
boolean hasNext() | 如果迭代还有元素,则返回true |
E next() | 获取当前迭代元素,并后移指针 |
boolean hasPrevious() | 如果迭代器在反方向遍历列表时有更多元素,返回true |
E previous() | 获取当前迭代元素,并前移指针 |
void add(E e) | 将指定的元素插入列表 |
void remove() | 删除迭代器所指向的元素 |
特有操作: 如果在该集合中有某元素,就插入另一元素
ListIterator<String> list = new list.ListIterator();
while (list.hasNext()) {
String s = list.next();
if (s.equals("world")) {
list.add("hello");
}
}
3 ArrayList
ArrayList
是一种大小可变的数组。
-
<E>
是一种特殊的数据类型:泛型 -
每个
ArrayList
对象都有一个容量。该容量是指用来存储列表元素的数组的大小,它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。ArrayList底层实现
导包:java.util.ArrayList
ArrayList
构造方法
构造方法 | 说明 |
---|---|
public ArrayList() ; | 创建一个初始容量为10的空集合对象 |
import java.util.ArrayList
public class Demo {
public static void main(String[] args) {
ArrayList<String> array = new ArrayList<String>();
//JDK7以后右侧尖括号内容可不写
ArrayList<String> array = new ArrayList<>();
sout: array; // 输出 []
/*
ArrayList是java已经写好的类,这个类在底层做了处理,使得:
1.直接打印对象打印的不是地址值,而是集合中存储的元素
2.打印的元素会用[]包裹起来
*/
}
}
ArrayList
成员方法
ArrayList继承Collection和List的所有方法。
方法 | 说明 |
---|---|
public boolean add(E e) ; | 将指定元素追加到此集合的末尾 |
public void add(int index, E e) ; | 在指定位置(索引)插入指定的元素 |
public boolean remove(E e) ; | 删除指定元素e,返回是否删除成功 |
public E remove(int index); | 根据索引删除元素,返回被删除元素 |
public E set(int index, E e); | 修改索引指向的元素,返回被修改元素 |
public E get(int index); | 返回索引处的元素 |
public int size(); | 返回集合长度 |
import java.util.ArrayList
public class Demo {
public static void main(String[] args) {
ArrayList<String> array = new ArrayList<String>();
//将指定元素追加到此集合的末尾
array.add("hello");
array.add("java");
Sout: array; // 输出:[hello, java]
//在指定位置(索引)插入指定的元素
array.add(1, "world");
Sout: array; // 输出:[hello , world , java]
//删除指定元素e,返回是否删除成功
Sout: array.remove("world"); // 输出:true
Sout: array; // 输出 [hello , java]
//根据索引删除元素,返回被删除元素
Sout: array.remove( 1 ); // 输出 world
Sout: array; // 输出 [hello , java]
//修改索引指向的元素,返回被修改元素
Sout: array.set(1 , "javase"); // 输出 world
Sout: array; // 输出 [hello , javase , java]
//返回索引处的元素
Sout: array.get( 1 ); // 输出 world
//返回集合长度
Sout: array.size(); // 输出 3
}
}
4 LinkedList
LinkedList的底层数据结构是双向链表。查询慢,增删快。
- 提供了操作首尾元素的特有API,所以操作首尾元素,速度也快。
导包:java.util.LinkedList
LinkedList特有方法
LinkedList除了继承Collection和List的所有方法外,还有自己的特有方法。
- 并不常用。Collection和List里的方法完全可以实现这些功能。
特有方法 | 说明 |
---|---|
void addFirst(E e) | 在链表的开头插入元素 |
void addLast(E e) | 在链表尾追加元素(和默认add方法一样) |
E getFirst() | 返回首元素 |
E getLast() | 返回尾元素 |
E removeFirst() | 删除首元素 |
E removeLast() | 删除尾元素 |
5 Set
Set是一个接口。
导包:java.util.Set
Set集合特点
-
无序集合
存和取的顺序有可能不一致。
-
不可重复
与
List
集合不同,Set集合不允许重复元素。 -
无索引
没有带索引的方法,不能使用普通for循环遍历。
Set常用方法
Set基本没有特有的方法,直接使用Collection方法即可。
Collection方法 | 说明 |
---|---|
boolean add(E e) | 添加元素,并返回是否添加成功 |
boolean remove(E e) | 从集合中移除指定的元素 |
boolean contains(Object o) | 判断集合中是否存在指定的元素 |
boolean isEmpty() | 判断集合是否为空(长度是否为0) |
int size() | 计算集合长度,即元素个数 |
void clear() | 清空集合中的元素(慎用) |
在Set系列,
add(E e)
方法如果添加不重复元素返回true,反之返回false。
6 HashSet
导包:java.util.HashSet
HashSet集合特点
-
底层数据结构是哈希表
-
无序集合
没有带索引的方法,不能使用普通for循环遍历。
存和取的顺序有可能不一致。
-
元素不可重复
如果HashSet集合存入的是自定义对象,必须重写hashCode和equals方法。
重写:
自动生成:按下alt + insert -> 选择equals() and hashCode()
-> 模板选择7+
-> 全默认
HashSet基本没有特有的方法,直接使用Collection(或Set)方法即可。
哈希值
哈希值:是JDK根据对象的地址
、字符串
、数字
算出来的int类型的数值
获取哈希值
定义在Object类中的 hashCode()
方法可以返回使用对象的地址值计算出的哈希值。
对象.hashCode()
一般选择重写 hashCode()
方法,使其能根据对象的属性值计算哈希值。
哈希值的特点
-
同一个对象多次调用hashCode()方法哈希值是相同的
-
没有重写hashCode()方法,不同对象的哈希值不同。
重写hashCode()方法,让有相同属性的不同对象的哈希值相同。
-
哈希碰撞:不同属性值或不同地址值计算出的哈希值有可能会一样。
HashSet底层原理
根据哈希表在Java里的实现方式的不同,底层原理也不同:
- JDK7以前:采用
数组+链表
实现,是一个元素为链表的数组 - JDK8以后:
数组+链表+红黑树
。
-
创建一个默认长度16,默认加载(负载)因子0.75的数组table。
当集合长度到达
16 * 加载因子
时,数组会扩容。
-
根据元素的哈希值与数组的长度,计算出该元素应存入的位置。
计算公式:
int index = (数组长度 - 1) & 哈希值
-
判断计算出的索引位置是否为null
-
如果是null,直接添加。
-
如果不是null,调用equals方法比较属性值。
-
属性值一样:不存。(元素不可重复)
-
属性值不一样:在该索引形成链表(或红黑树),将元素存入。
- JDK7以前:将新元素存入数组,老元素挂在新元素下面。
- JDK8以后:将新元素挂在老元素下面。
JDK7以前
JDK8以后
当链表长度大于8,且数组长度大于等于64时,该链表会转换为红黑树(JDK8以后)。
案例
使用集合存储学生对象,存储的对象不能重复。如果属性值一致则认为是同一个对象。
import java.util.Objects;
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
//省略get set方法
//重写equals hashCode方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
import java.util.HashSet;
public class HashSetDemo {
public static void main(String[] args) {
HashSet<Student> set = new HashSet<>();
Student s1 = new Student("zhangsan", 18);
Student s2 = new Student("lisi", 19);
Student s3 = new Student("zhangsan", 18);
System.out.println(set.add(s1)); //true
System.out.println(set.add(s2)); //true
System.out.println(set.add(s3)); //false,重复元素,不添加
//遍历
set.forEach(s -> System.out.println(s.getName() + ", " + s.getAge()));
}
}
7 LinkedHashSet
LinkedHashSet是哈希表和双向链表实现的Set接口。继承了HashSet.
-
链表保证元素有序,元素的存储和取出
顺序一致
-
哈希表保证元素唯一,元素
不重复
导包:java.util.LinkedHashSet
LinkedHashSet集合特点
-
有序集合
存储和取出的元素顺序一致。
-
元素不可重复
-
无索引
LinkedHashSet基本没有特有的方法,直接使用Collection(或Set或HashSet)方法即可。
LinkedHashSet底层原理
在HashSet基础上多了一条记录顺序的双向链表,
在遍历时会根据该链表记录的顺序遍历,从而保证了存储和取出的元素顺序一致。
8 TreeSet
底层是红黑树。
导包:java.util.TreeSet
TreeSet集合特点
-
可排序
:元素按照一定的规则排序,默认升序,可指定。 -
元素不可重复
-
无索引
没有带索引的方法,不能使用普通for循环遍历。
TreeSet基本没有特有的方法,直接使用Collection(或Set)方法即可。
TreeSet排序规则
类型 | 排序规则 |
---|---|
数值类型:Integer,Double等 | 默认按照从小到大的顺序排序。 |
字符类型 | 默认按照字符在ASCLL码表中对应的值从小到大排序。 |
字符串类型 | 默认排序规则与字符串长度无关,是从首个字符开始往后按字符ASCLL码表中对应的值比。 |
自定义数据类型(或要指定规则) | 需要手动指定排序规则。方法1:自然排序/默认排序;方法2:比较器排序 |
优先选择方法1,当方法1不能满足要求时,再用方法2
当方法1和方法2同时存在时,以方法2为准。
自然排序
步骤:
-
让元素所属的JavaBean类实现泛型接口
Comparable
。导包:java.lang.Comparable
-
重写
compareTo(T o1, T o2)
方法指定排序规则。
重写方法时,要注意排序规则必须按照要求的
主次条件
来写
例:存储的学生按照年龄从小到大排序,当年龄相同时,按照姓名首字母排序
public class TreeSetDemo {
public static void main(String[] args) {
TreeSet<Student> ts = new TreeSet<>();
Student s1 = new Student(...) ;
Student s2 = new Student(...) ;
Student s3 = new Student(...) ;
ts.add(s1) ;
ts.add(s2) ;
ts.add(s3) ;
//遍历
ts.forEach(s -> System.out.println(s.getName() + ", " + s.getAge()));
}
}
//学生类
class Student implements Comparable<Student> { //1.实现泛型接口Comparable<E>
//省略JavaBean
//2.重写compareTo() 方法
compareTo(Student s) {
//按照年龄从小到大排序
int result = this.age - s.age;
//年龄相同时,按照姓名首字母排序
result = result == 0? this.name.compareTo(s.name) : result;
return result;
}
}
底层原理:
首先要清楚其底层是存储在红黑树里。
第25行:把 this
理解为当前要存储的元素, s
理解为 ts集合
里已经存储的元素(比如存储 s2 时, s2 就是接下来要存储的元素, s1 相对来说就是已经存储的元素)。
该方法的返回值:
-
如果是正数,说明将要存储的元素
s2
比已经存储的元素s1
大,就把s2
放在已经存储的元素s1
的左边; -
如果是负数,说明比已经存储的元素
s1
小,就把s2
放在已经存储的元素s1
的右边。 -
如果是0,说明要存储的元素已经存在,不存。
同理s3、s4…按此规则排好就是正序。
相反,如果规则是 s.age - this.age
排出来的结果就是倒序。
比较器排序
带参构造方法
使用的是比较器排序
步骤:
- 让集合构造方法接收比较器接口
Comparator
的实现类对象,
该接口是函数式接口,可用Lambda表达式简写。
- 重写
compare(T o1, T o2)
指定排序规则。
// 测试类
//采用匿名内部类
TreeSet<Student> ts = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
// s1 相当于自然排序中的 this,s2 相当于自然排序中的 s
int result = s1.getAge() - s2.getAge();
result = result == 0? s1.getName().compareTo(s2.getName()) : result;
return result;
}
});
//使用Lambda表达式改写
TreeSet<Student> ts = new TreeSet<>((s1, s2) -> {
int result = s1.getAge() - s2.getAge();
result = result == 0? s1.getName().compareTo(s2.getName()) : result;
return result;
}
});
底层原理: 与自然排序一样。