目录
[(1) add 方法](#(1) add 方法)
[(2) clear方法](#(2) clear方法)
[(4)contains 方法](#(4)contains 方法)
[(2)增强 for 遍历](#(2)增强 for 遍历)
[(1)普通 for 循环遍历](#(1)普通 for 循环遍历)
[① hasPrevious 和 previous](#① hasPrevious 和 previous)
[② add](#② add)
(4)containsKey和containsValue方法
[7. 双列集合的使用场景](#7. 双列集合的使用场景)
[(1)不可变的 List 集合](#(1)不可变的 List 集合)
[(2)不可变的 Set 集合](#(2)不可变的 Set 集合)
[(3)不可变的 Map 集合](#(3)不可变的 Map 集合)
一、体系结构
1.集合体系结构
在java中,集合分为两种:单列集合 和 多列集合
单列集合:每次只能添加一个数据
双列集合:每次添加的是一对数据
2.单列集合体系结构
单列集合分为两种:List系列 和 Set系列
List系列集合: 添加的元素是有序,可重复,有索引的
Set系列集合: 添加的元素是无序,不可重复,无索引的
二、单列集合
1.Collection集合的使用
Collection是单列集合的顶层接口,它的功能是全部单列集合都可以使用的(共性的)。
由于Collection是一个接口, 我们不能直接创建他的对象。
所以,调用方法时,只能创建他的实现类对象。
(1) add 方法
public class Demo {
public static void main(String[] args) {
//实现类:ArrayList
Collection<String> coll = new ArrayList<>();
//1.添加元素
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
System.out.println(coll);//[aaa, bbb, ccc]
}
}
细节:
① 添加元素时,如果是往 List 系列集合中添加元素,那么返回值永远为true,因为List系列是允许重复的。
② 添加元素时,如果是往 Set 系列集合中添加元素:
如果要添加的元素不存在,返回值为 true;
如果要添加的元素已经存在,返回值为false。
因为 Set 系列集合中的元素是不可重复的。
(2) clear方法
public class Demo {
public static void main(String[] args) {
//实现类:ArrayList
Collection<String> coll = new ArrayList<>();
//1.添加元素
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
System.out.println(coll);//[aaa, bbb, ccc]
//2.清空元素
coll.clear();
System.out.println(coll);//[]
}
}
(3)remove方法
public class Demo {
public static void main(String[] args) {
//实现类:ArrayList
Collection<String> coll = new ArrayList<>();
//1.添加元素
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
System.out.println(coll);//[aaa, bbb, ccc]
//3.删除
System.out.println(coll.remove("aaa"));//true
System.out.println(coll);//[bbb, ccc]
}
}
细节:
① 删除元素时,由于 Collection 是顶层接口,只能定义共性的方法。所以删除方法只能通过元素的对象删除,不能通过索引删除,因为Set 集合是无索引的。
虽然 ArrayList 中也有remove的重载方法,可以根据索引删除。
但是该实现类对象是由接口多态创建的,遵循"编译看左边,运行看右边",所以不能调用实现类的独有方法。
② 删除元素时,如果要删除的元素存在,则删除成功,返回true;
如果要删除的元素不存在,则删除失败返回false。
(4)contains 方法
public class Demo {
public static void main(String[] args) {
//实现类:ArrayList
Collection<String> coll = new ArrayList<>();
//1.添加元素
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
System.out.println(coll);//[aaa, bbb, ccc]
//4.判断元素是否包含
System.out.println(coll.contains("aaa"));//true
System.out.println(coll.contains("ddd"));//false
}
}
细节:
底层是通过for循环依次遍历,依赖 equals 方法进行判断是否存在的。
所以,如果集合中存储的对象是自定义类,想要通过 contains 方法判断是否包含,那么必须在该自定义类中,重写 equals 方法,使其判断的不是地址值,而是内部的属性值。
这里可以正常判断的原因是,String 类本身就已经重写好了 equals 方法,使其比较的是字符串的值,而不是地址值。
Student类:
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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 class Demo2 {
public static void main(String[] args) {
Collection<Student> coll = new ArrayList<>();
Student s1=new Student("zhangsan",23);
Student s2=new Student("lisi",24);
coll.add(s1);
coll.add(s2);
//创建一个一模一样的对象,判断是否存在集合中
Student s3=new Student("zhangsan",23);
System.out.println(coll.contains(s3));//false
}
}
由于Student类中并没有重写equals方法,所以默认使用父类Object 类中的 equals 方法。
而Object 类中的 equals 方法比较的是地址值,由于s1和s3的地址值不相等,所以结果为false。
**************************************************************************************************************
重写后的Student类:
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@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);
}
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;
}
}
这时,由于重写了Student类中equals方法,改为比较内部属性值,结果才为true。
(5)isEmpty方法
public class Demo {
public static void main(String[] args) {
//实现类:ArrayList
Collection<String> coll = new ArrayList<>();
//1.添加元素
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
//5.判断集合是否为空
boolean result=coll.isEmpty();
System.out.println(result);//false;
}
}
细节:
isEmpty方法的底层事实上就是判断集合的长度是否为0。
(6)size方法
public class Demo {
public static void main(String[] args) {
//实现类:ArrayList
Collection<String> coll = new ArrayList<>();
//1.添加元素
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
//6.获取集合的长度
int size = coll.size();
System.out.println(size);//3
}
}
底层直接返回 ArrayList 类的成员变量 size即可
2.Collection集合的通用遍历方式
由于Collection是单列集合的顶级接口,所以遍历得兼容 List系列和 Set 系列。
而 Set集合是不含索引的,所以不能直接用 for 循环进行遍历。
需要采用一种通用的方式,使得 List 和 Set 都能给够进行遍历。
(1)迭代器遍历
迭代器在Java当中的接口是 Iterator,迭代器是集合专门的遍历方式,是不依赖索引的。
步骤:
① 创建迭代器对象后,迭代器对象默认指向集合的 0 索引。
② 然后调用 hasNext 方法判断当前位置是否有元素。
③ 最后调用 next 方法获取当前的元素,迭代器对象向后移动,指向下一个位置。
public class IteratorDemo {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
//1.获取迭代器对象(类似指针,默认指向0索引)
Iterator<String> it = coll.iterator();
//2.利用循环不断地获取集合中的每一个元素
while (it.hasNext()) {
//3.next方法做了两件事情:获取元素 + 移动指针
String str = it.next();
System.out.println(str);
}
}
}
注意点:
① 如果 hasNext 方法已经返回 false,则不能再调用 next 方法,否则报错 NoSuchElementException
② 迭代器遍历完毕后,指针仍指向结束位置,不会复位。
如果想要二次遍历集合,只能重新创建新的迭代器对象。
public class IteratorDemo {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
Iterator<String> it1 = coll.iterator();
while (it1.hasNext()) {
String str = it1.next();
System.out.println(str);
}
//当上述代码执行完毕后,迭代器的指针已经指向结束位置,不存在任何元素
System.out.println(it1.hasNext());//false
//二次遍历
Iterator<String> it2 = coll.iterator();
while (it2.hasNext()) {
String str = it2.next();
System.out.println(str);
}
}
}
③ hasNext 方法要和 next 方法配套使用,即循环中只能使用一次 next 方法。
public class IteratorDemo2 {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
Iterator<String> it = coll.iterator();
while (it.hasNext()) {
System.out.println(it.next());
System.out.println(it.next());
}
}
}
集合中只有三个元素,每次循环却调用两次 next 。
在第二次循环的第一次 next 后,指针就已经移到最终位置了。
这时再 next 就会报错 NoSuchElementException。
**结论:**hasNext 方法要和 next 方法配套使用,即一次 hasNext,后面跟着一次 next。
**************************************************************************************************************
Tips:因为迭代器遍历时是不依赖索引的,如果想要反复使用某一元素,需要使用变量保存下来。
因为 next 获取元素之后,指针就后移了,已经不会指向该元素了。
public class IteratorDemo2 {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
Iterator<String> it = coll.iterator();
while (it.hasNext()) {
String str = it.next();
System.out.println(str);//aaa bbb ccc
System.out.println(str);//aaa bbb ccc
System.out.println(str);//aaa bbb ccc
}
}
}
④ 迭代器遍历时,不能用集合的方法进行增加或者删除,会导致并发修改异常。遍历结束时,可以正常修改集合。
解决办法:
对于删除,可以使用迭代器 Iterator 接口提供的 remove 方法进行删除。
对于添加,"暂时" 没有办法。
public class IteratorDemo2 {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
Iterator<String> it = coll.iterator();
while (it.hasNext()) {
String str = it.next();
if ("bbb".equals(str)) {
it.remove();
}
}
System.out.println(coll);//[aaa, ccc, ddd]
}
}
(2)增强 for 遍历
增强 for 的底层实质上就是一个迭代器,是为了简化迭代器的代码而书写的。
JDK5以后诞生,其内部原理就是一个 Iterator 迭代器。
所有的单列集合和数组才能用增强 for 进行遍历。
public class ForDemo {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
for (String s : coll) {
System.out.println(s);
}
}
}
细节:
增强 for 中的变量 s,只是一个临时变量,表示集合中的每一个数据,相当于 String s = it.next();
所以,修改增强 for 中的变量,是不会对集合本身的数据造成影响的。
public class ForDemo {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
for (String s : coll) {
s = "qqq";
}
System.out.println(coll);//[aaa, bbb, ccc, ddd]
}
}
(3)Lambda表达式遍历
JDK8开始后,诞生了Lambda表达式,从而提供了一种更简单,更直接的遍历集合的方式。
该种遍历方式需要依赖 forEach 方法。
通过源码可以发现,forEach 底层是通过 for 循环进行遍历的。
然后遍历集合,根据索引调用 element(es,i) 方法得到的集合中的每一个元素,交给 accept 方法。
forEach 方法的形参是一个 Consumer 类型。
我们发现,Consumer 是一个接口,而且是一个函数式接口,表明它可以使用 Lambda 表达式 。
public class ForEachDemo {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
//1.使用匿名内部类的形式
coll.forEach(new Consumer<String>() {
@Override
//s 表示集合中遍历得到的每一个元素
public void accept(String s) {
System.out.println(s);
}
});
//2.使用Lambda表达式的形式
coll.forEach(s -> System.out.println(s));
}
}
可以发现,使用匿名内部类时,重写的正是accept 方法。
而 accept 方法的形参 s,正是 for 循环中通过 element(es,i) 方法得到的,表示集合中的每一个元素。
**question1:**可能有人会奇怪,不是说 Collection 中定义的都是共性的方法吗?怎么 forEach 方法底层使用的是普通 for 。Set集合不是没有索引,不能使用普通 for 吗?
回答: 请注意,由于我们是使用接口多态的方式创建的对象,所以查看源码要看实现类。
多态的运行规则遵循 "编译看左边,运行看右边"。
所以上面我们实现类是 ArrayList,查看的也是 ArrayList 类中 forEach 方法。
对于 ArrayList 的它是有索引的,所以可以采用普通 for 进行遍历。
**question2:**那如果这样的话,Set 集合又没有索引,怎么使用 forEach 进行遍历呢?而且对于 Set 集合的实现类,例如 HashSet 中,也没有重写这个 forEach 方法。
回答:
刚刚也说了,编译看左边,运行看右边。
在已知右边可能并不存在 forEach 的重写方法的情况下,相信应该也能猜到。
左边定义的 forEach 方法可能并不是一个抽象方法,而是一个有方法体的方法。
当右边(实现类)没有重写左边(接口)中的某个方法,那么在实现类对象调用这个方法时,就会执行接口中原来已经实现的那个方法。
可能很快又有人发现,左边 Collection 接口中,也没有 forEach 方法呀,这是为什么?
不要忘了,接口与接口之间,是有继承关系的,可以单继承,也可以多继承。
而 Collection 接口继承于 Iterable 接口。
在 Iterable 接口中,定义了一个默认方法 forEach,不强制实现类进行重写。
这个 forEach 采用增强 for 进行遍历,同样也将遍历到的每个元素 t ,交给 accpet 方法进行处理。
这样对于没有索引的Set 集合,也可以进行遍历了。
所以,具体的继承和实现关系,如下图所示。
结论: 以后在看源码时,如果是利用多态创建的对象,要去看其实现类的源码。
如果实现类中没有,再顺着实现/继承关系继续往上找。
3.List集合的使用
List集合的特点:
① 有序:存和取的元素顺序一致
② 有索引:可以通过索引操作元素
③ 可重复:存储的元素可以重复
由于 Collection 是单列集合的顶层接口,所以 Collection 中的方法,List都继承了。
而 List 集合是有索引的,所以多了很多索引操作的方法。
由于 List 本身也是一个接口,所以不能直接创建对象,需要创建其实现类对象。
(1)add方法
public class Demo {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
//1.add 在指定位置插入元素
list.add(1,"ddd");
System.out.println(list);//[aaa, ddd, bbb, ccc]
}
}
细节:
add 方法如果不加索引的话,调用的是继承自 Collection 重写的 add 方法,默认将元素加在末尾。
add 方法形参中有索引的话,调用的是重载的add方法。
(2)remove方法
public class Demo {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
//2.remove 删除指定索引处的元素,并返回被删除的元素
String s=list.remove(0);
System.out.println(s);//aaa
}
}
同理,如果 remove 中参数是一个对象,则调用的是继承自 Collection 重写的 remove 方法。
如果 remove 中参数是一个索引,则调用的是重载的 remove 方法。
**************************************************************************************************************
**Test:**如果集合中的元素是 Integer整数类型,那么调用 remove 方法,即下列结果如何?是删除了 1 这个元素,还是删除了索引为 1 的元素。
public class RemoveDemo {
public static void main(String[] args) {
List<Integer> list=new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.remove(1);
System.out.println(list);
}
}
运行结果:
原因:
在方法调用的时候,如果方法出现了重载现象,那么会优先调用实参和形参一致的那个方法。
remove(1)中的实参 1 是 int 类型,所以会调用重载的 remove 方法,删除索引为1的元素。
而继承自 Collection 重写的 remove 方法的形参是一个 Integer 类型,所以不会优先调用。
**************************************************************************************************************
**question:**那如果我就想删除元素1呢?如何实现?
方法一:list.remove(0);
方法二:手动装箱,把基本数据类型的 1 ,变成 Integer 类型.
public class RemoveDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Integer i = Integer.valueOf(1);
list.remove(i);
System.out.println(list);//[2, 3]
}
}
(3)set方法
public class Demo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
//1.add 在指定位置插入元素
list.add(1, "ddd");
System.out.println(list);//[aaa, ddd, bbb, ccc]
//3.set 修改指定索引处的元素,返回被修改的元素
String resullt = list.set(0, "qqq");
System.out.println(list);//[qqq, ddd, bbb, ccc]
}
}
(4)get方法
public class Demo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
//1.add 在指定位置插入元素
list.add(1, "ddd");
System.out.println(list);//[aaa, ddd, bbb, ccc]
//4.get 返回指定索引处的值
String s = list.get(0);
System.out.println(s);//aaa
}
}
4.List集合的遍历方式
List 集合继承自 Collection 集合,所以之前 Collection 的三种通用遍历方式均可使用。
除此之外,List 集合还可以使用 列表迭代器遍历 和 普通 for 循环遍历 。
(1)普通 for 循环遍历
public class Demo2 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
//1.普通for循环遍历
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
}
}
(2)列表迭代器遍历
列表迭代器在Java当中的接口是 ListIterator,继承自迭代器接口 Iterator,所以其方法也能够使用。
此外,ListIterator 还新增了几个方法。
① hasPrevious 和 previous
该两个方法和之前的 hasNext 和 next 正好相反,前面是向下一个移动,这个是向前一个移动。
但是,一开始也是默认指向 0 位置,这时不能直接调用 previous,否则报错。
② add
之前提到,迭代器遍历时,不能用集合的方法进行增加或者删除,会导致并发修改异常。
对于删除,可以使用迭代器 Iterator 接口提供的 remove 方法进行删除,这里 ListIterator 同样也继承了该方法。
对于添加,之前是不行的。但现在 ListIterator 新增了 add 方法,是可以做到添加元素的。
public class Demo2 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
//2.列表迭代器
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
String s = it.next();
if ("bbb".equals(s)) {
it.add("qqq");
}
}
System.out.println(list);//[aaa, bbb, qqq, ccc]
}
}
(3)五种遍历方式对比
5.ArrayList的底层源码分析
① 最初情况下,空参创建了一个集合,在底层创建了一个默认长度为 0的数组。
② 添加第一个元素时,底层会创建一个长度为 10 的新数组,默认初始化为 null。
注意:size不光表示数组中元素的个数,也表示下次元素要存入的位置。
③ 存满时,会扩容 1.5 倍。
④ 如果一次添加多个元素,会调用 addAll 方法,1.5 倍还放不下,则新创建的数组长度以实际为准。
注意:
如果只说 ArrayList 底层的扩容机制是 自动扩容为1.5倍,这样是不严谨的。
在插入多个数据的时候,1.5倍可能不够,此时会以实际长度为准。
ArrayList 进行数组拷贝的时候调用的是 Arrays 类的 copyof 方法,但该方法底层实质上调用的是System 类的 arraycopy 方法。
6.LinkedList集合的底层源码分析
LinkedList底层数据结构是双链表,查询慢,增删快。
双链表可以直接对表头和表尾进行操作,因此 LinkedList本身多了很多直接操作首尾元素特有的API。
这些方法并不常用,因为我们也可以使用从 List 和 Collection 接口继承下来的方法。
**************************************************************************************************************
首先,LinkedList 类中定义了一个静态内部类 Node,表示链表中的节点 。
节点有三个属性值,分别是 前驱指针,当前元素,后继指针。
LinkedList 同时也定义了一个头指针和尾指针,指向第一个节点和最后一个节点。
起始时,first 和 last 都定义为(指向) null,插入第一个节点后,两个指针都指向了该节点。
插入第二个节点时,头指针不发生改变,只改变尾指针。
后续插入的原理类似,实质上就是节点的指针会发生变化而已。
7.Iterator迭代器的底层源码分分析
首先,Iterator 是一个接口,是不能直接创建对象的。
这个接口定义了一些方法,如 hasNext 和 next。
在每个集合类中,都定义了一个内部类 Itr 用于实现 Iterator 接口。
同时定义了一个 iterator 方法,用于获取迭代器对象。
在内部类 Itr 中,有两个成员变量:
cursor :游标,也就是迭代器中的指针。由于没有赋值,所以默认初始化为 0,即默认指向 0 索引
lastRet:表示上一次(刚刚)操作的元素的索引。
初始情况下,cursor 为 0 。
调用 hasNext 方法会对游标进行校验,判断 cursor 是否不等于集合长度。
调用 next 方法 ,cursor会进行二次校验,不合法会抛出异常。然后 cursor 和 lastRet 会向后移动,将元素插入到底层数组中。
当已经遍历到最后一个元素时,cursor 会等于 size,此时 hasNext 会返回 false,next 也会抛出异常。
我们注意到内部类 Itr 的成员变量中,还有一个变量 modCount ,它代表集合变化的次数。
对集合每 add 或 remove 一次,modCount 都会 ++。
我们在创建迭代器对象时,会将这个次数告诉迭代器。
迭代器在遍历的过程中,会调用 checkForComodification 方法校验次数是否正确,即该次数是否和一开始记录的次数 expectedModCount 相同。
如果不同,代表遍历过程中,使用了集合的方法添加或删除元素,此时就会导致并发修改异常。
如果相同,证明当前集合没有发生改变,可以正常遍历。
8.Set集合的使用
特点:
① 无序:存取顺序不一致
② 不重复:每个元素都是唯一的(可以利用该性质进行去重)
③ 无索引:没有带索引的方法,所以不能使用普通 for 进行遍历,也不能通过索引来获取元素。
实现类:
HashSet:无序,不重复,无索引
LinkedHashSet:有序,不重复,无索引
TreeSet:可排序,不重复,无索引
Set 接口继承自 Collection 接口,因此 Collection 中的方法 Set 都可以使用。
Set 中没有什么额外的方法需要学习,直接使用 Collection 中的方法即可。
Set 集合也是一个接口,所以我们不能直接创建其对象,需要创建其实现类对象。
public class Demo {
public static void main(String[] args) {
//1.创建一个Set集合的对象
Set<String> set = new HashSet<>();
//2.添加元素
boolean res1 = set.add("aaa");
boolean res2 = set.add("aaa");
set.add("bbb");
set.add("ccc");
//不重复
System.out.println(res1);//true
System.out.println(res2);//false
//无序
System.out.println(set);//[aaa, ccc, bbb]
//3.遍历集合(无序)
for (String s : set) {
System.out.println(s);//aaa ccc bbb
}
}
}
细节:
① 由于 Set 集合的不可重复性,所以调用 add 方法:
如果要添加的元素不存在,返回值为 true;
如果要添加的元素已经存在,返回值为false。
② 由于 Set 集合的无序性,所以无论直接打印 Set,还是通过遍历,都可能和存入的顺序不一致。
9.HashSet集合的底层原理
(1)哈希值
① 哈希值是根据 hashCode 方法所计算出来的 int 类型的整数。
② 该方法定义在 Object类中,所有对象都可以调用,默认使用地址值进行计算。
由于使用地址值进行计算,所以不同对象计算的哈希值是不一样的。
③ 我们希望不同的对象属性值相同,就代表这个对象已经重复,而并不希望根据地址值来判断。
所以一般情况下,会重写 hashCode 方法,改为利用对象内部的属性值来计算哈希值。
Student类:
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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 class HashDemo {
public static void main(String[] args) {
Student s1=new Student("zhangsan",23);
Student s2=new Student("zhangsan",23);
//1.如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
}
}
运行结果:
此时由于 s1 和 s2 的地址值并不相同,所以计算得到的哈希值也是不一样。
我们想要改为根据属性值进行计算哈希值,就必须重写 hashCode 方法。
运行结果:
④在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值,是有可能相同的,这个情况被称为 "哈希碰撞"。
由于 int 取值范围一共就 42 个亿多,这时如果创建 50 亿个对象,int 的范围必然支撑不住,所以可能会出现哈希碰撞的情况,但这种可能性是极小的。
对于String类,可以发现内部已经重写了 hashCode 方法,使其可以根据字符串来计算哈希值。
但对于不同的字符串 "abc" 和 "acD" ,计算出的哈希值却是一样的,发生了哈希碰撞。
(2)底层原理
HashSet 集合底层采取哈希表存储元素,哈希表设计一种对于增删改查数据性能都很好的结构。
哈希表组成:
JDK8 之前:数组 + 链表
元素存入的位置会根据数组长度和哈希值进行计算,公式如下:
如果存入位置已经有元素,会调用 equals 方法,和链表中的所有元素进行 一 一比较 。
当 数组中的元素为 16 (数组长度 ) x 0.75 (加载因子) = 12 时,数组则会触发扩容,长度变为原来的两倍,即 16 x 2 = 32。
JDK8 开始:数组 + 链表 + 红黑树
和JDK8之前的区别:
① 元素存入数组中的链表,由头插法变为尾插法。
② 当数组中,有某个链表的长度 > 8 而且 数组长度 >= 64 时,当前的链表会自动转变成红黑树。
所以说 数组,链表,红黑树三种结构可同时存在。
注:
如果集合中存储的是自定义对象 ,必须要重写 hashCode 和 equals 方法。
hashCode :用于根据对象的属性值计算哈希值,从而进一步计算出该元素存入数组中的位置。
equals:用于当数组中存入的位置已经有元素了,和该位置的链表中的每个元素根据属性值进行 一 一比较。
(3)三个问题
问题一:HashSet 为什么存和取的顺序不一样?
HashSet 在遍历时,数组会按照从左到右,从上到下的顺序进行遍历,如上图。
因为在存入元素时,未必就一定按照这个顺序进行存,所以取的时候结果就有可能不一致了。
问题二:HashSet 为什么没有索引?
虽然说数组的确是有索引的,但是数组中的每个位置都存放着一个链表或红黑树,包含多个元素。
在这三个数据结构的组合下, 根本没有办法去定义元素的索引,所以只能取消索引了。
问题三:HashSet 是利用什么机制去保证数据去重的呢?
① 利用 hashCode 方法计算哈希值(自定义类需要重写,不重写根据对象地址值计算),从而确定当前元素在数组中存放的位置。
② 再利用 equals 方法去比较对象内部中的属性值是否相同(自定义类需要重写,不重写比较对象地址值)。
10.LinkedHashSet的底层原理
特点:有序,不重复,无索引
**question1:**LinkedHashSet 是如何实现有序(存取顺序一致)的呢?
原理: 底层数据结构依然是哈希表,只是每个元素又额外多了一个双链表 的机制用于记录存储的顺序。
所以在打印或者遍历时,都是根据 头指针 head 来进行一个一个遍历的,从而实现了有序。
**question2:**以后如果要数据去重,我们使用哪个?
一般默认使用 HashSet,如果要求去重且存取有序,才使用 LinkedHashSet 。
因为 LinkedHashSet 虽然实现了有序,但额外定义了一个双链表,所以底层相比于 HashSet,效率要低一点。
所以优先选择 HashSet,虽然无序,但效率高。
11.TreeSet集合的底层原理
特点:可排序(按照元素的默认规则进行排序),不重复,无索引
注: TreeSet 集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好。
排序的默认规则:
① 对于数值类型:Integer,Double,默认按照从小到大的顺序进行排序。
public class TreeSetDemo {
public static void main(String[] args) {
//1.创建TreeSet集合对象
TreeSet<Integer> ts = new TreeSet<>();
//2.添加元素
ts.add(4);
ts.add(5);
ts.add(2);
ts.add(3);
ts.add(1);
//3.打印集合
System.out.println(ts);//[1, 2, 3, 4, 5]
//4.遍历集合
Iterator<Integer> it = ts.iterator();
while (it.hasNext()) {
int i = it.next();
System.out.println(i);//1 2 3 4 5
}
}
}
② 对于字符、字符串类型:按照字符在 ASCII 码表中的数字升序进行排序。
③ 对于自定义类,需要指定比较规则。
方式一:(默认排序/自然排序) JavaBean类实现 Compareable 接口指定比较规则**。**
Student类:
//实现Comparable接口,由于定义的是Student对象间的排序规则,所以泛型直接限定类型
public class Student implements Comparable<Student> {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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 "Student{name = " + name + ", age = " + age + "}";
}
//重写compareTo方法,指定比较规则
@Override
public int compareTo(Student o) {
//按照年龄升序排序
return this.getAge() - o.getAge();
}
}
细节:
① 实现Comparable接口时,由于定义的是Student对象间的排序规则,所以泛型直接限定类型。
② 由于TreeSet底层数据结构是红黑树,不是哈希表,所以不用重写 hashCode 和 equals 方法。
③ 对于 compareTo 方法:
I. this:表示当前要添加的元素
II. o :表示已经在红黑树中存在的元素
返回值:
I. 负数:表示当前要插入的元素 < 已排好的元素,插在该元素的左子树
II. 正数:表示当前要插入的元素 > 已排好的元素,插在该元素的右子树
III. 0 : 表示当前要插入的元素 = 已排好的元素,即该元素已存在,舍弃
fang'sh
public class TreeSetSort {
public static void main(String[] args) {
//1.创建四个学生对象
Student s1 = new Student("wangwu", 25);
Student s2 = new Student("lisi", 24);
Student s3 = new Student("zhangsan", 23);
Student s4 = new Student("zhaoliu", 26);
//2.创建集合对象
TreeSet<Student> ts=new TreeSet<>();
//3.添加元素
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
//4.打印集合
System.out.println(ts);
}
}
运行结果:
底层红黑树变化(省略叶子节点):
为了方便理解,这里附上红黑树的规则 和 添加节点时红黑树的调整规则:
方式二: (比较器排序) 创建TreeSet 对象时,传递比较器 Comparator 对象指定比较规则。
**使用原则:**默认使用第一种,如果第一种不能满足当前需求,就使用第二种。
**Test:**向集合中存入四个字符串,"c","ab","df","qwer",要求按照长度排序,长度一样则按照字符排序。
在 String 类中,java的开发者已经根据方式一,实现了 Compareable 接口并指定了比较规则,默认根据字符在ASCII 码表中的顺序进行排序。
此时,我们如果想重新定义比较规则,那就必须得修改 String 类源码中的比较规则,显然这时不可能的。
所以,采用方式二,在创建集合对象的时候, 直接传递比较器对象,重新指定比较规则。
public class TreeSetSort {
public static void main(String[] args) {
//1.创建集合
TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
//按照长度排序
int i = o1.length() - o2.length();
//如果长度相同,则按照字符排序(调用默认排序规则)
i = (i == 0 ? o1.compareTo(o2) : i);
return i;
}
});
//2.添加元素
ts.add("c");
ts.add("ab");
ts.add("df");
ts.add("qwer");
//3.打印集合
System.out.println(ts);//[c, ab, df, qwer]
}
}
细节:
① Comparator 是一个函数是接口,可以使用 Lambda 表达式。
② String 类中已经制定了比较规则,而现在通过构造方法的方式,又重新制定了一个新的比较规则。这表明,在方式一和方式二都存在的情况下,方式二的优先级比方式一更高。
③ compare 方法的形参:
I. o1:表示当前要添加的元素
II. o2:表示已经在红黑树中存在的元素
返回值:
I. 负数:表示当前要插入的元素 < 已排好的元素,插在该元素的左子树
II. 正数:表示当前要插入的元素 > 已排好的元素,插在该元素的右子树
III. 0 : 表示当前要插入的元素 = 已排好的元素,即该元素已存在,舍弃
④ 在 Arrays 类中,其 sort 方法也传递了一个 Comparator 比较器对象,可以参考一起学习。
Java常用API(三)https://blog.csdn.net/xpy2428507302/article/details/139356503?spm=1001.2014.3001.5501
12.单列集合的使用场景
三、双列集合
特点:
① 双列集合需要一次存一对数据,分别为键和值。
② 键不能重复,但值是可以重复的。
③ 键和值是 一 一对应的,每个键只能找到自己所对应的值。
④ "键 + 值" 这个整体被称为 "键值对" 或者 "键值对对象",在Java中叫做 "Entry对象"。
1.Map集合的使用
由于 Map 是一个接口, 我们不能直接创建他的对象。
所以,调用方法时,只能创建他的实现类对象。
其次,由于 Map 中存的是键值对,是一对元素,所以泛型也要有两个(K:key,V:value)。
Map是双列集合的顶层接口,它的功能是全部双列集合都可以继承使用的。
(1)put方法
public class Demo {
public static void main(String[] args) {
//创建Map集合的实现类对象(接口多态)
Map<String, Integer> map = new HashMap<>();
//1.put 添加元素
Integer result1 = map.put("语文", 90);
System.out.println(result1);//null
map.put("数学", 95);
map.put("英语", 92);
System.out.println(map);//{数学=95, 语文=90, 英语=92}
Integer result2 = map.put("语文", 80);
System.out.println(result2);//90
System.out.println(map);//{数学=95, 语文=80, 英语=92}
}
}
细节:
① put 方法不光会添加元素,还有覆盖功能:
I. 在添加元素时,如果键不存在,会直接将键值对对象添加到 Map 集合中,方法返回 null;
II. 在添加元素时,如果键已经存在,那么会将原有的键值对对象的值覆盖,并把被覆盖的值返回。
② 由于返回的是被覆盖的值,所以 put 方法的返回值类型是 V,即值的数据类型。
(2)remove方法
public class Demo {
public static void main(String[] args) {
//创建Map集合的实现类对象(接口多态)
Map<String, Integer> map = new HashMap<>();
//1.put 添加元素
map.put("语文", 90);
map.put("数学", 95);
map.put("英语", 92);
//2.remove 根据键删除键值对元素
Integer result=map.remove("英语");
System.out.println(result);//92
System.out.println(map);//{数学=95, 语文=90}
}
}
细节:
删除后会将被删除的键值对对象的值进行返回,所以remove方法的返回值类型是 V,即值的数据类型。
(3)clear方法
public class Demo {
public static void main(String[] args) {
//创建Map集合的实现类对象(接口多态)
Map<String, Integer> map = new HashMap<>();
//1.put 添加元素
map.put("语文", 90);
map.put("数学", 95);
map.put("英语", 92);
//3.clear 清空集合
map.clear();
System.out.println(map);//{}
}
}
(4)containsKey和containsValue方法
public class Demo {
public static void main(String[] args) {
//创建Map集合的实现类对象(接口多态)
Map<String, Integer> map = new HashMap<>();
//1.put 添加元素
map.put("语文", 90);
map.put("数学", 95);
map.put("英语", 92);
//4.containsXxx 判断键/值是否包含
boolean KeyResult= map.containsKey("英语");
System.out.println(KeyResult);//true
boolean ValueResult= map.containsValue(100);
System.out.println(ValueResult);//false
}
}
(5)isEmpty方法
public class Demo {
public static void main(String[] args) {
//创建Map集合的实现类对象(接口多态)
Map<String, Integer> map = new HashMap<>();
//1.put 添加元素
map.put("语文", 90);
map.put("数学", 95);
map.put("英语", 92);
//5.isEmpty 判断集合是否为空
boolean result = map.isEmpty();
System.out.println(result);//false
}
}
(6)size方法
public class Demo {
public static void main(String[] args) {
//创建Map集合的实现类对象(接口多态)
Map<String, Integer> map = new HashMap<>();
//1.put 添加元素
map.put("语文", 90);
map.put("数学", 95);
map.put("英语", 92);
//6.size 获取集合长度
int size = map.size();
System.out.println(size);//3
}
}
2.Map集合的遍历方式
(1)键找值
将所有的键存到一个单列集合中,遍历再在使用 get 方法获取每个键所对应的值。
public class IterateDemo1{
public static void main(String[] args) {
//创建Map集合对象
Map<String, String> map = new HashMap<>();
//添加元素
map.put("工藤新一", "毛利兰");
map.put("服部平次", "远山和叶");
map.put("黑羽快斗", "中森青子");
map.put("京极真", "铃木园子");
//1.通过键找值
//获取所有的键,将这些键放到一个单列集合当中
Set<String> keys = map.keySet();
for (String key : keys) {
//遍历单列集合,得到每一个键
String value = map.get(key);
System.out.println(key + "=" + value);
}
}
}
细节:
① keySet 方法可以自动获取所有的键,并创建一个单列集合将这些键存放到其中。
② get方法可以根据键返回所对应的值。
(2)键值对
将所有的键值对对象存放到一个单列集合中,遍历再使用 getXxx 分别获取每个对象中的键和值。
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class IterateDemo2 {
public static void main(String[] args) {
//创建Map集合对象
Map<String, String> map = new HashMap<>();
//添加元素
map.put("工藤新一", "毛利兰");
map.put("服部平次", "远山和叶");
map.put("黑羽快斗", "中森青子");
map.put("京极真", "铃木园子");
//2.通过键值对对象进行遍历
//获取所有的键值对对象
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
//遍历集合,使用getXxx方法获取键和值
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + "=" + value);
}
}
}
细节:
① entrySet 方法可以自动获取所有的键值对对象,并创建一个 Set 集合将这些对象存放到其中。
② 这个 Set 集合确实是一个单列集合,因为集合中的每个元素是一个键值对对象,不是键和值两个元素。
注意这里使用了泛型嵌套,Set 的泛型是键值对类型,而键值对对象中有两个泛型:键和值。
③ Entry 事实上是 Map 接口中的内部接口,所以在表示 Entry 类型时,
需要使用:外部接口名.内部接口名
Tips:只有在已经导包的情况下:import java.util.Map.Entry;
才可以直接使用 Entry,即 Set<Entry<String,string>>
④ 键值对对象通过 getKey 和 getValue 方法可以获取键和值。
(3)Lambda表达式
public class IterateDemo3 {
public static void main(String[] args) {
//创建Map集合对象
Map<String, String> map = new HashMap<>();
//添加元素
map.put("工藤新一", "毛利兰");
map.put("服部平次", "远山和叶");
map.put("黑羽快斗", "中森青子");
map.put("京极真", "铃木园子");
//3.使用Lambda表达式进行遍历
//内部类书写方式
/*
map.forEach(new BiConsumer<String, String>() {
@Override
public void accept(String key, String value) {
System.out.println(key + "=" + value);
}
});*/
//Lambda表达式书写方式
map.forEach((key, value) -> System.out.println(key + "=" + value));
}
}
细节:
① BiConsumer 是一个函数式接口,所以可以使用 Lambda表达式。
② 由于这里实现类对象是 HashMap,所以查看 HashMap 中的 forEach 方法源码。
底层事实上用到也是一个增强 for 进行遍历,然后将 key 和 value 交给 accept 方法进行处理。
3.HashMap集合的使用
特点:
① HashMap 是 Map 接口中的一个实现类。
② 没有额外需要学习的方法,直接使用 Map 中的方法就可以了。
③ 特点都是由键决定的:无序,不重复,无索引 --> 指的都是键
④ HashMap 和 HashSet 的底层原理是基本一模一样 的,都是哈希表结构。
HashMap 和 HashSet 底层的区别:
① HashMap 在计算哈希值时,只根据键进行计算,与值无关。
② HashMap 在利用 equals 方法进行比较时,只比较键的属性值,与值无关。
③ 如果键比较的结果一样,那么会将新的键值对对象进行覆盖,而不是和 HashSet 一样进行舍弃
注:
由于都是哈希表结构,所以 HashMap 也依赖 hashCode 和 equals 来保证键的唯一。
所以:如果键存储的是自定义对象,需要重写 hashCode 和 equals 方法。
但如果值存储的是自定义对象,则不需要重写 hashCode 和 equals 方法。
4.linkedHashMap集合的使用
特点 (由键决定): 有序,不重复,无索引
原理: 和 LinkedHashSet 一样,底层数据结构依然是哈希表,只是每个键值对元素又额外多了一个双链表 的机制用于记录存储的顺序。
public class LinkHashMapDemo {
public static void main(String[] args) {
//创建集合对象
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
//添加元素
map.put("语文", 90);
map.put("数学", 95);
map.put("英语", 92);
System.out.println(map);//{语文=90, 数学=95, 英语=92}
}
}
5.TreeMap集合的使用
原理: TreeMap 和 TreeSet 底层原理一样,都是红黑树结构的。
特点 (由键决定):可排序(对键进行排序),不重复,无索引
注: 排序规则默认按照键的从小到大进行排序,也可以自己指定键的排序规则。
public class SortDemo1 {
public static void main(String[] args) {
//创建集合对象
TreeMap<Integer,String> map = new TreeMap<>();
//添加元素
map.put(2,"矿泉水");
map.put(4,"方便面");
map.put(3,"可口可乐");
map.put(5,"奥利奥");
map.put(1,"蛋黄派");
//打印集合
System.out.println(map);
//{1=蛋黄派, 2=矿泉水, 3=可口可乐, 4=方便面, 5=奥利奥}
}
}
细节:
其实所谓默认,实质上是 Java 已经按照自定义排序规则的方式一指定了规则,不需要我们手动指定了而已。
这里键是 Integer 类型,所以查看 Integer 类的源码。
可以发现,该类已经实现了 Compareable 接口,并重写了 compareTo 方法。
自定义排序规则:
方式一: 实现 Compareable 接口指定比较规则
student类:
//实现Comparable接口,由于定义的是Student对象(键)间的排序规则,所以泛型直接限定类型
public class Student implements Comparable<Student> {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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 "Student{name = " + name + ", age = " + age + "}";
}
@Override
public int compareTo(Student o) {
//this:表示当前要添加的元素
// o :表示红黑树中存在的元素
int i = this.getAge() - o.getAge();
return i == 0 ? this.getName().compareTo(o.name) : i;
}
}
测试类:
public class SortDemo3 {
public static void main(String[] args) {
//1.创建四个学生对象
Student s1 = new Student("wangwu", 25);
Student s2 = new Student("lisi", 24);
Student s3 = new Student("zhangsan", 23);
Student s4 = new Student("zhaoliu", 26);
//2.创建集合对象
TreeMap<Student,String> map=new TreeMap();
//3.添加元素
map.put(s1,"上海");
map.put(s2,"北京");
map.put(s3,"南京");
map.put(s4,"深圳");
map.put(s2,"合肥");//会进行覆盖
//4.打印集合
System.out.println(map);
/* {Student{name = zhangsan, age = 23}=南京,
Student{name = lisi, age = 24}=合肥,
Student{name = wangwu, age = 25}=上海,
Student{name = zhaoliu, age = 26}=深圳}*/
}
}
注意:
这两种方式的排序规则中的形参和 TreeSet 中一模一样,但返回值为 0 时的意义不一样:
I. 负数:表示当前要插入的元素 < 已排好的元素,插在该元素的左子树
II. 正数:表示当前要插入的元素 > 已排好的元素,插在该元素的右子树
III. 0 : 表示当前要插入的元素 = 已排好的元素,即该元素已存在,进行覆盖( 覆盖键值对对象的值) 。
**方式二:**创建集合式传递比较器 Comparator 对象指定比较规则(优先级更高)。
比如:想要 Integer 类型的键按照降序排列,此时我们无法修改 java 已经写好的源代码,可以采用方式二进行指定比较规则。
public class SortDemo2 {
public static void main(String[] args) {
//创建集合对象
TreeMap<Integer, String> map = new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//o1:当前要添加的元素
//o2:已经在红黑树中存在的元素
return o2 - o1;
}
});
//添加元素
map.put(2, "矿泉水");
map.put(4, "方便面");
map.put(3, "可口可乐");
map.put(5, "奥利奥");
map.put(1, "蛋黄派");
//打印集合
System.out.println(map);
//{5=奥利奥, 4=方便面, 3=可口可乐, 2=矿泉水, 1=蛋黄派}
}
}
7. 双列集合的使用场景
① 默认情况下:使用 HashMap --> 效率最高
② 如果要保证存取有序:使用 LinkedHashMap
③ 如果要进行排序:使用 TreeMap
四、可变参数
1.引言
在对于计算 2个,3个,4个这种确定数量的数据时,我们可以在方法中定义多个形参。
但如果是 n个数据呢?也就是形参个数不确定时,该如何解决这个问题?
我们通常会定义一个数组,将这 n 个数据存入数组中,再将数组作为方法的形参。
但这种方法过于麻烦,有没有不需要定义数组的方法,就可以解决这个问题呢?
2.定义
**可变参数:**方法的形参是可以发生改变的(JDK5开始)。
**格式:**数据类型... 变量名(如:int... args)
**底层原理:**可变参数事实上底层就是一个数组,但这个数组无需手动创建,java会自动创建好。
3.细节
① 在方法的形参中最多只能写一个可变参数,否则报错。
因为可变参数表示参数不确定,即可以接收多个数据。
假设有 10 个数据,在这种情况下,无法确定这 10个中,多少个属于 args1,多少个属于 args2。
② 在方法中,如果还有其他的形参,可变参数需要在最后面。
只有前面参数的个数确定了,后面的所有参数才能都交给可变参数。
假设有 10 个数据,在这种情况下,第一个数据是 a,剩下 9 个属于可变参数 agrs。
如果可变参数不写在最后,那么可变参数 agrs 会代表全部的 10个数据,后续的 a 将没有数据。
五、Collections集合工具类
1.定义
**作用:**Collectons 不是一个集合,而是集合工具类,提供了一系列操作集合的方法。
2.方法
(1)addAll和shuffle方法
public class Demo {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
//1.addAll 批量添加元素
Collections.addAll(list,"abc","bcd","qwer","df","asdf","zxcv","1234","qwer");
System.out.println(list);
//2.shuffle 打乱
Collections.shuffle(list);
System.out.println(list);
}
}
运行结果:
注意点:
① addAll 方法的形参中的泛型只有一个,所以只能传递单列集合,不能传递双列集合
② shuffle 方法的形参是一个 List类型的集合,所以只能传递 List 系列的集合,不能传递 Set 系列
(2)其他方法
java
public class Demo2 {
public static void main(String[] args) {
System.out.println("-------------sort默认规则--------------------------");
//默认规则,需要重写Comparable接口compareTo方法。
//Integer已经实现,按照从小打大的顺序排列
//如果是自定义对象,需要自己指定规则
ArrayList<Integer> list1 = new ArrayList<>();
Collections.addAll(list1, 10, 1, 2, 4, 8, 5, 9, 6, 7, 3);
Collections.sort(list1);
System.out.println(list1);
System.out.println("-------------sort自己指定规则规则--------------------------");
Collections.sort(list1, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
System.out.println(list1);
Collections.sort(list1, (o1, o2) -> o2 - o1);
System.out.println(list1);
System.out.println("-------------binarySearch--------------------------");
//需要元素有序
ArrayList<Integer> list2 = new ArrayList<>();
Collections.addAll(list2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
System.out.println(Collections.binarySearch(list2, 9));
System.out.println(Collections.binarySearch(list2, 1));
System.out.println(Collections.binarySearch(list2, 20));
System.out.println("-------------copy--------------------------");
//把list3中的元素拷贝到list4中
//会覆盖原来的元素
//注意点:如果list3的长度 > list4的长度,方法会报错
ArrayList<Integer> list3 = new ArrayList<>();
ArrayList<Integer> list4 = new ArrayList<>();
Collections.addAll(list3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Collections.addAll(list4, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
Collections.copy(list4, list3);
System.out.println(list3);
System.out.println(list4);
System.out.println("-------------fill--------------------------");
//把集合中现有的所有数据,都修改为指定数据
ArrayList<Integer> list5 = new ArrayList<>();
Collections.addAll(list5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Collections.fill(list5, 100);
System.out.println(list5);
System.out.println("-------------max/min--------------------------");
//求最大值或者最小值
ArrayList<Integer> list6 = new ArrayList<>();
Collections.addAll(list6, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
System.out.println(Collections.max(list6));
System.out.println(Collections.min(list6));
System.out.println("-------------max/min指定规则--------------------------");
// String中默认是按照字母的abcdefg顺序进行排列的
// 现在我要求最长的字符串
// 默认的规则无法满足,可以自己指定规则
// 求指定规则的最大值或者最小值
ArrayList<String> list7 = new ArrayList<>();
Collections.addAll(list7, "a","aa","aaa","aaaa");
System.out.println(Collections.max(list7, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}));
System.out.println("-------------swap--------------------------");
ArrayList<Integer> list8 = new ArrayList<>();
Collections.addAll(list8, 1, 2, 3);
Collections.swap(list8,0,2);
System.out.println(list8);
}
}
六、不可变集合
1.定义
**不可变集合:**不可以被修改的集合(添加、删除、修改元素均不可以)。
应用场景:
① 如果某个数据不能被修改,把它防御性的拷贝到不可变集合中是个很好的实践。
② 当集合对象被不可信的库调用时,不可变形式是安全的。
2.书写格式
在 List,Set,Map接口中,都存在静态的of 方法,可以获得一个不可变的集合。
(1)不可变的 List 集合
注:
① 一旦创建完毕后,是无法进行修改的,只能进行查询操作。
② of 方法的形参是一个可变参数,所以创建集合时,可以传递很多数据。
(2)不可变的 Set 集合
细节:
① 由于 Set 集合是不可重复的,所以当创建一个不可变的 Set 集合时,of 方法中的参数一定要保证唯一性。
如果 of 方法中的参数重复了,则会报错。
② 和 List 中的of 方法一样,形参是一个可变参数,所以创建集合时,可以传递很多数据。
(3)不可变的 Map 集合
细节:
① 可以看出,of 方法把奇数位参数当作键,偶数位参数当作值。
② 由于 Map 集合中键是是不能重复的,所以 of 方法中的键也不能重复。
③ of 方法最多只能容纳 10 个键值对,因为该方法的参数并不是可变参数,而是固定数量的形参。
of 方法提供 11 个重载方法,参数中键值对的数量由 0 到 10。
但并没有形参为可变参数的重载方法,所以参数中键值对最多只能为 10个。
question1:为什么不能定义一个可变参数的 of 的重载方法呢?
由于键值对是两个变量,类型可能不一样,如果要用可变参数,形式应该为:of(K... k, V... v)
但这样写就错了,前面说过,**在方法的形参中最多只能写一个可变参数,**所以没有办法定义。
question2:那如果我想创建一个键值对超过 10 个的不可变的Map集合,该如何实现呢?
应该有人能想到,那我将键值对这个整体作为一个对象,不就只需要一个可变参数了吗?
没错,Map 也是这么实现的,它提供了一个 ofEntries 方法,形参是一个 Entry 类型的可变参数。
java
public class MapDemo2 {
public static void main(String[] args) {
//创建一个普通的Map集合
HashMap<String, String> hm = new HashMap<>();
hm.put("aaa", "111");
hm.put("bbb", "222");
hm.put("ccc", "333");
hm.put("ddd", "444");
hm.put("eee", "555");
hm.put("fff", "666");
hm.put("ggg", "777");
hm.put("hhh", "888");
hm.put("iii", "999");
hm.put("jjj", "101010");
hm.put("kkk", "111111");
//利用上面的数据来创建一个不可变的Map集合
//1.获取到所有的键值对对象(Entry对象)
Set<Map.Entry<String, String>> entries = hm.entrySet();
//2.把entries变成一个数组
Map.Entry[] temp = new Map.Entry[0];
Map.Entry[] arr = entries.toArray(temp);
//3.创建不可变的Map集合
Map map = Map.ofEntries(arr);
System.out.println(map);
}
}
细节:
前面说过,可变参数本质上就是一个数组。
所以需要通过 toArray 方法将 entries 集合变成一个数组,再传给 ofEntries 方法。
toArray 方法有两个重载方法。
无参的 toArray 方法默认返回一个Object 类型的数组,并不常用。
有参的 toArray 方法需要传递一个数组作为参数,返回值的类型则是这个数组的类型。
如果集合的长度(Entries) > 数组的长度(temp):数据在数组中放不下,此时会根据实际数据的个数重新创建数组。
如果集合的长度(Entries) <= 数组的长度(temp):数据在数组中放的下,此时不会创建新数组,而是直接用 temp。
所以,实质上可以简写为:
java
Map<String,String> map = Map.ofEntries(hm.entrySet().toArray(new Map.Entry[0]));
但是这种写法过于麻烦,也不容易记,为此 Map 又定义了一个 copyOf 方法进行了封装。
在底层,会首先判断传入的 集合是否是可变集合。
如果是,则直接返回该集合;
如果不是,则根据该集合创建一个不可变集合。
java
public class MapDemo2 {
public static void main(String[] args) {
//创建一个普通的Map集合
HashMap<String, String> hm = new HashMap<>();
hm.put("aaa", "111");
hm.put("bbb", "222");
hm.put("ccc", "333");
hm.put("ddd", "444");
hm.put("eee", "555");
hm.put("fff", "666");
hm.put("ggg", "777");
hm.put("hhh", "888");
hm.put("iii", "999");
hm.put("jjj", "101010");
hm.put("kkk", "111111");
//利用上面的数据来创建一个不可变的Map集合
Map<String,String> map=Map.copyOf(hm);
System.out.println(map);
}
}