目录
1.集合进阶
Java中有众多集合,ArrayList
只是其中一种。
1.1.集合体系结构
集合可以分成两类,单列集合(Collection)、双列集合(Map)。单列一次添加一个数据,双列集合一次添加一对数据。
1.1.1.单列集合体系结构
Collection:单列集合的顶层接口,它的功能全部单列集合都可以使用
Collection的两个系列:
-
List系列:添加元素时有序、可重复、有索引。
-
有序:指存放时的顺序和取出来的顺序要相同,而非大小关系顺序
-
可重复:字面含义
-
有索引:字面含义
-
-
Set系列:无序、不重复、无索引
-
无序:存和取的顺序不一定相同
-
不重复:当中的每个元素是唯一的->应用场景:去重
-
无索引:不能通过索引来获取集合中的元素
-
1.1.2.单列集合应用场景
-
如果想要集合中的元素可重复,用ArrayList集合最多,基于数组;
-
如果想要集合中的元素去重,用HashSet最多,基于哈希表;
-
如果想要集合中的元素可重复,且增删操作明显大于查询,用LinkedList,基于链表;
-
如果想要集合中的元素去重,且保证存取的顺序,用LinkedHashSet,基于哈希表和链表。
-
如果想要集合中的元素排序,用TreeSet,基于红黑树。
1.1.2.1.Collection的常用成员方法
public boolean add(E e) //添加
public void clear() //清空
public boolean remove(E e) //删除
public boolean contains(Object obj) //判断是否包含
public boolean isEmpty() //判断是否为空
public int size() //集合长度
//举例
public class Demo {
public static void main(String[] args) {
//注意:Collection是一个接口,我们不能直接创建他的对象。所以,现在我们学习他的方法时,只能创建他实现类的对象。实现类:ArrayList。
//接口多态
Collection<String> coll = new ArrayList<>();
//1.添加元素
//细节1:如果我们要往List系列集合中添加数据,那么方法永远返回true,因为List系列的是允许元素重复的。
//细节2:如果我们要往Set系列集合中添加数据,如果当前要添加元素不存在,方法返回true,表示添加成功。如果当前要添加的元素已经存在,方法返回false,表示添加失败,因为Set系列的集合不允许重复。
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
System.out.println(coll);
//2.清空,没有返回值
coll.clear();
//3.删除
//细节1:因为Collection里面定义的是共性的方法,所以此时不能通过索引进行删除。只能通过元素的对象进行删除。
//细节2:方法会有一个布尔类型的返回值,删除成功返回true,删除失败返回false
//如果要删除的元素不存在,就会删除失败。
System.out.println(coll.remove("aaa"));
System.out.println(coll);//true
//4.判断元素是否包含
//细节:ArrayList底层是依赖equals方法进行判断是否存在的。(ps:如果想要查看equals的底层源码,不能到Collection接口中查看,因为接口中的抽象方法没有方法体。因此需要右键点击contains方法->go to->选择ArrayList类查看,或者直接Ctrl点进ArrayList中查看equals方法)
//所以,如果集合中存储的是其他的自定义对象,如Student等,也想调用contains方法来判断是否包含,那么在javabean类中,一定要自己重写equals方法。
boolean result1 = coll.contains("bbb");
System.out.println(result1);//true
//5.判断集合是否为空
boolean result2 = coll.isEmpty();
System.out.println(result2);//false
//6.获取集合的长度
coll.add("ddd");
int size = coll.size();
System.out.println(size);//3
}
}
1.1.2.2.Collection系列集合的遍历
for循环的弊端:只能遍历有索引的容器,对于Set集合无法遍历,可以通过使用迭代器接口,是集合专用的遍历方式。
Collection系列集合三种通用的遍历方式:
-
迭代器遍历
-
增强for遍历
-
lambda表达式遍历
如果在遍历的时候需要删除元素,那么需要使用迭代器;如果仅仅只是想遍历,那么使用增强for和Lambda表达式。
1.1.2.2.1.迭代器遍历(含源码)
迭代器遍历的本质:
-
迭代器不依赖索引就可以遍历。本质是通过创建指针、移动指针来实现遍历。
迭代器遍历的流程:
-
Iterator<E> it = new iterator();
:获取一个迭代器对象,指针默认指向集合的0位置处 -
boolean hasNext()
:判断当前指针指向的位置是否有元素,有则返回true,没有返回false。配合while循环使用 -
E next()
:每调用一次方法,就完成两件事情:①获取当前指针指向的元素 ②移动指针到下一个位置
迭代器遍历举例:
public class Demo {
public static void main(String[] args) {
//1.创建集合并添加元素
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
//2.获取迭代器对象
Iterator<String> it = coll.iterator();
//3.利用循环不断的去获取集合中的每一个元素
while(it.hasNext()){
//4.next方法的两件事情:获取元素并移动指针到下一个位置
String str = it.next();
System.out.println(str);
}
}
}
注意事项:
-
如果迭代器已经指到末尾,继续调用next方法,那么会报错
NoSuchElementException(没有元素异常)
-
迭代器遍历完毕,指针不会复位,如果想要再次遍历,需要再创建一个新的迭代器对象
-
每次循环中只能用一次next方法,如果一个循环中调用多次,有可能会报错
NoSuchElementException(没有元素异常)
-
迭代器遍历时,不能用集合的方法进行增加或者删除,会报错(并发编改异常);需要使用迭代器自身的remove方法进行删除,也就是
迭代器对象.remove()
迭代器的底层源码:
//获取迭代器对象的底层
Iterator<String> it = list.iterator();
//创建一个集合对象list,然后调用父类接口Collection当中的iterator()方法
//iterator()方法本质就是创建了ArrayList类中的内部类Itr的对象
//因此迭代器对象实际上这个内部类Itr的对象
//通过这个内部类Itr的对象,我们就可以调用其中的方法进行遍历
1.1.2.2.2.增强for遍历
JDK5以后出现的,底层就是迭代器
所有单列集合和数组才能用增强for遍历
for(元素数据类型 变量名:数组或者单列集合){
...
}
举例:
for(String s:list){
print(s);
}
//结果是会遍历list中的每一个元素并进行打印
注意: s本质是一个第三方变量,和集合没有直接关系。作用是在循环中依次记录集合中的每一个数据。如果修改for中的变量,例如在上面的for中给s赋值,不会改变集合中原本的数据,因为s本质上是一个第三方变量
for(String s:list){
s = "a";
}
print(list);
//打印结果仍然是集合中的数据不变。如果把print语句放在for循环中,打印list呢?
for(String s:list){
s = "a";
print(list);
}
//注意:如果把print语句放在for循环中,此时打印list仍然不会改变,这原因是s是一个第三方变量。如果打印s,此时结果就会发生改变了。
快捷键:集合变量名字.for,可以直接生成增强for循环
1.1.2.2.3.Lambda表达式遍历
foreach方法
forEach
在底层是一个ArraList
类中被重写了的方法,采用的原理就是普通的for循环
//采用匿名内部类的形式,创建接口Consumer的一个实现类的对象
list.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
);
//采用Lambda表达式,简单记为(形参)->{方法体},数据类型可以省略;当形参只有一个的时候,小括号可以省略;当方法体只有一行代码的时候,可以省略大括号、分号、return
list.forEach(s->System.out.println(s));
1.1.2.3.List系列
特点:
-
有序
-
有索引
-
可重复
1.1.2.3.1.List系列的成员方法
List系列也是一个接口,顶级接口Collection的子接口,因此Collection中的所有方法其都可以使用。这里介绍其特有的方法,都是跟索引相关的。
//List系列集合独有的方法:
void add(int index,E element) //在此集合中的指定位置插入指定的元素,原来索引上的元素会向后移
E remove(int index) //删除指定索引处的元素,返回被删除的元素
E set(int index,E element) //修改指定索引处的元素,返回被修改的元素
E get(int index) //返回指定索引处的元素
//1.创建一个集合
List<String> list = new ArrayList<>();
//2.添加元素
list.add("aaa");
list.add("bbb");//1
list.add("ccc");
void add(int index,E element) //在此集合中的指定位置插入指定的元素
//细节:在一个指定索引上面添加元素,原来索引上的元素会依次往后移
list.add(1,"QQQ");
E remove(int index) //删除指定索引处的元素,返回被删除的元素
String remove = list.remove(0);
System.out.println(remove);//aaa
E set(int index,E element) //修改指定索引处的元素,返回被修改的元素
String result = list.set(0, "QQQ");
System.out.println(result);
E get(int index) //返回指定索引处的元素
String s = list.get(0);
System.out.println(s);
//3.打印集合
System.out.println(list);
}
}
1.1.2.3.2.List系列的遍历
Collection中的三种遍历List均可以使用。
//首先创建集合并添加元素
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
- 迭代器遍历
应用场景:想要在遍历的过程中删除元素的时候使用。
Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
System.out.println(str);
}
- 增强for
应用场景:代码简单,仅仅想要遍历的时候使用。
for (String s : list) {
System.out.println(s);
}
- 普通for循环
size方法跟get方法还有for循环结合的方式,利用索引获取到集合中的每一个元素。应用场景:想要在遍历的过程中操作索引的时候使用。
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
- Lambda表达式
list.forEach(s->System.out.println(s) );
-
列表迭代器
应用场景:想要在遍历的过程中增加元素的时候使用。
ListIterator<String> it = list.listIterator();
while(it.hasNext()){
String str = it.next();
if("bbb".equals(str)){
//qqq
it.add("qqq");
}
}
System.out.println(list);
五种遍历方式的对比:
- - 遍历过程中需要删除元素,使用迭代器遍历;
- - 遍历过程中需要增加元素,使用列表迭代器;
- - 仅仅想遍历,使用增强for和Lambda表达式遍历;
- - 遍历的时候想要操作索引,采用普通for遍历。
1.1.2.3.3.ArrayList
底层源码见Java基础上。底层数据结构是数组。
1.1.2.3.4.LinkedList
底层数据结构是双向链表,查询慢(但是比单向链表快),增删快,但是如果操作的是首尾元素,速度也是极快的。因此多了很多首尾操作的特有API。LinkedList自身方法用的很少,基本上使用Collection和List中的方法就足以。
LinkedList
底层源码
LinkedList添加元素的底层:尾插法
final Node<E> l = last //表示用l这个Node类型的变量来临时存储上一个结点的地址值last(如果是第一次添加那么last为null)。
final Node<E> newNode = new Node<>(l,e,null);//创建新结点newNode,上一个结点地址值为l,数据为变量e,下一个结点地址值为null
last = newNode;//把新结点的地址值赋值给last
if else语句;//如果l中是null(表示第一次添加),那么就用first这个变量记录新结点的地址值,表示是头结点;如果不是null,说明新结点之前已经有一个结点,那么把新结点newNode的地址值赋值给l(l记录上一个结点的地址值,这里表示上一个结点)的成员变量next(记录新结点的地址值)。
1.1.2.4.Set系列
特点:
-
无序:存和取的顺序有可能不一致。
-
不重复:可以去重
-
无索引:没有带索引的方法
Set三个实现类的特点:
-
HashSet:无序、不重复、无索引
-
LinkedSet:有序、不重复、无索引
-
TreeSet:可排序、不重复、无索引
1.1.2.4.1.Set系列的成员方法和遍历
Set系列是一个接口。其方法基本与Collection中一致。
//成员方法和遍历方式的演示
public static void main(String[] args) {
//创建一个Set集合的对象
Set<String> s = new HashSet<>();
//添加元素
//如果当前元素是第一次添加,那么可以添加成功,返回true
//如果当前元素是第二次添加,那么添加失败,返回false
s.add("张三");//true
s.add("张三");//false
s.add("李四");//true
s.add("王五");//true
//打印集合,发现和存的顺序不一致
//System.out.println(s);//[李四, 张三, 王五]
//迭代器遍历
Iterator<String> it = s.iterator();
while (it.hasNext()){
String str = it.next();
System.out.println(str);
}
//增强for遍历
for (String str : s) {
System.out.println(str);
}
// Lambda表达式遍历
s.forEach(str->System.out.println(str));
}
1.1.2.4.2.HashSet
应用背景:需要去重的时候使用HashSet。
底层结构:采用哈希表数据结构。增删改查数据性能都较好。
JDK8之前,数组+链表。JDK8以后,数组+链表+红黑树。
HashSet三个特点的底层原理:
-
无序:存的时候是按照哈希值来存的,而取的时候是从头按链表-链表来遍历的
-
不重复:通过hashCode方法计算哈希值,从而确定了唯一存储的位置;如果哈希值相同,再通过equals方法进一步比较对象是否相同,如果相同那么就不会存,确定了唯一性。
-
注意:如果集合中存储的是自定义对象,必须要重写hashCode和equals方法,不然会默认采用地址值去计算哈希值,就会导致不同对象也能存储。String和Integer底层已经重写好了。
-
-
无索引:因为是由数组+链表+红黑树复合而成的,没有索引机制。
哈希值:对象的整数表现形式。是根据hashCode方法算出来的int类型的整数。
-
hashCode方法:这个方法定义在Object类中,所有对象都可以调用,默认用地址值计算(但意义不大),因此一般情况下会重写hashCode方法,用对象内部属性值计算哈希值
对象的哈希值的特点:
-
如果没有重写,用地址值计算,因此不同对象计算出来的哈希值不同
-
如果重写,不同对象属性值相同,那么计算出来的哈希值相同
-
哈希碰撞:小概率情况下,地址值不同或者不同属性值,计算出来的哈希值却相同。(底层是因为对象数量大于整数的范围)
public static void main(String[] args) {
//创建对象
Student s1 = new Student("zhangsan",23);
Student s2 = new Student("zhangsan",23);
//如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
//如果已经重写hashcode方法,不同的对象只要属性值相同,计算出的散列值就是一样的
System.out.println(s1.hashCode());//-1461067292
System.out.println(s2.hashCode());//-1461067292
//在小部分情况下,不同的属性值或者不同的地址值计算出来的散列值也有可能一样。
//哈希碰撞
System.out.println("abc".hashCode());//96354
System.out.println("acD".hashCode());//96354
}
HashSet添加元素的底层原理
-
创建一个默认长度16,默认加载因子为0.75的数组,数组名table。
-
根据元素的哈希值跟数组的长度计算出应存入的位置。
-
判断当前位置是否为null,如果是null直接存入;
如果位置不为null,表示有元素,则调用equals方法比较。如果是链表,会依次和链表中所有元素比较
-
属性值一样:不存
不一样:存入数组,形成链表
JDK8以前:新元素存入数组,老元素挂在新元素下面;JDK8以后:新元素直接挂在老元素下面
JDK8以后:当链表长度超过8,并且数组长度≥64,会自动将链表转换为红黑树
练习:利用HashSet去重
public class Demo {
public static void main(String[] args) {
/* 需求:创建一个存储学生对象的集合,存储多个学生对象。
使用程序实现在控制台遍历该集合。
要求:学生对象的成员变量值相同,我们就认为是同一个对象
注意要重写hashCode和equals方法!
*/
//1.创建三个学生对象
Student s1 = new Student("zhangsan",23);
Student s2 = new Student("lisi",24);
Student s3 = new Student("wangwu",25);
Student s4 = new Student("zhangsan",23);
//2.创建集合用来添加学生
HashSet<Student> hs = new HashSet<>();
//3.添加元素
System.out.println(hs.add(s1));
System.out.println(hs.add(s2));
System.out.println(hs.add(s3));
System.out.println(hs.add(s4));
//4.打印集合
System.out.println(hs);
}
}
1.1.2.4.3.LinkedHashSet
应用背景:需要去重的同时保证有序。否则默认使用HashSet(效率更高)。
特点:有序、不重复、无索引
-
有序的底层原理:底层结构也是哈希表,但是每个元素额外多了双链表机制,元素会互相记录地址值,相当于记录了存储的先后顺序。存是如何存,取就是如何取。
public static void main(String[] args) {
//创建4个学生对象
Student s1 = new Student("zhangsan",23);
Student s2 = new Student("lisi",24);
Student s3 = new Student("wangwu",25);
Student s4 = new Student("zhangsan",23);
//创建集合的对象
LinkedHashSet<Student> lhs = new LinkedHashSet<>();
//添加元素
System.out.println(lhs.add(s1));//true
System.out.println(lhs.add(s2));//true
System.out.println(lhs.add(s3));//true
System.out.println(lhs.add(s4));//false
//打印集合
System.out.println(lhs);//最终打印出来的顺序和添加时候的顺序相同[s1,s2,s3]
}
}
1.1.2.4.4.TreeSet
TreeSet底层数据结构是红黑树。
特点:不重复、无索引、可排序。
- 可排序:默认按照元素大小顺序进行排列。
//TreeSet的可排序和遍历方式演示
public static void main(String[] args) {
//创建TreeSet集合对象,同时指定数据类型Integer
TreeSet<Integer> ts = new TreeSet<>();
//添加元素
ts.add(4);
ts.add(5);
ts.add(1);
ts.add(3);
ts.add(2);
//打印集合
System.out.println(ts);//打印得[1,2,3,4,5]
//遍历集合(三种遍历)
//迭代器
Iterator<Integer> it = ts.iterator();
while(it.hasNext()){
int i = it.next();
System.out.println(i);
}
//增强for
for (int t : ts) {
System.out.println(t);
}
//lambda
ts.forEach( i-> System.out.println(i));
}
TreeSet集合添加时的比较规则(Comparable 和 Comparator 的区别)
-
对于数值类型,默认按照升序排列;
-
底层:对于Integer这类包装类,已经默认实现了Comparable接口,并重写了其中的方法。
-
-
对于字符、字符串类型,按照字符在ASCII码表中数字的升序进行排列
-
底层:对于String,已经默认实现了Comparable接口,并重写了其中的方法。
-
比较字符串时,先比第一个字母,如果能比出结果,则直接排序;如果比不出结果,再比第二个字母,以此类推;如果比到某一位没有字母,而另一个字符串有字母,那么认为有字母的比较大。
-
-
对于自定义对象类型,如果没有指定比较规则,那么在给TreeSet添加自定义对象的时候就会出现报错。
-
对象类型比较规则:默认使用第一种,如果不能满足需求则使用第二种
-
默认比较:JavaBean类实现Comparable接口,重写里面的抽象方法,再指定比较规则
-
-
-
比较器排序:创建TreeSet对象的时候采用TreeSet当中的有参构造方法,传递Comparator比较器,也就是传递一个Comparator接口的实现类对象,在重写的方法中指定比较规则
//练习1:默认排序
/* 按照总分从高到低输出到控制台
如果总分一样,按照语文成绩排
如果语文一样,按照数学成绩排
如果数学成绩一样,按照英语成绩排
如果英文成绩一样,按照年龄排
如果年龄一样,按照姓名的字母顺序排
如果都一样,认为是同一个学生,不存。*/
//采用默认比较的规则,JavaBean类实现Comparable接口,重写里面的抽象方法compareTo(),再指定比较规则
@Override
public int compareTo(Student2 o) {
int sum1 = this.getChinese() + this.getMath() + this.getEnglish();
int sum2 = o.getChinese() + o.getMath() + o.getEnglish();
//比较两者的总分
int i = sum1 - sum2;
//如果总分一样,就按照语文成绩排序
i = i == 0 ? this.getChinese() - o.getChinese() : i;
//如果语文成绩一样,就按照数学成绩排序
i = i == 0 ? this.getMath() - o.getMath() : i;
//如果数学成绩一样,按照英语成绩排序(可以省略不写)
i = i == 0 ? this.getEnglish() - o.getEnglish() : i;
//如果英文成绩一样,按照年龄排序
i = i == 0 ? this.getAge() - o.getAge() : i;
//如果年龄一样,按照姓名的字母顺序排序
i = i == 0 ? this.getName().compareTo(o.getName()) : i;
return i;
}
//在测试类中添加对象
//创建学生对象
Student2 s1 = new Student2("zhangsan",23,90,99,50);
Student2 s2 = new Student2("lisi",24,90,98,50);
Student2 s3 = new Student2("wangwu",25,95,100,30);
Student2 s4 = new Student2("zhaoliu",26,60,99,70);
Student2 s5 = new Student2("qianqi",26,70,80,70);
//创建集合
TreeSet<Student2> ts = new TreeSet<>();
//添加元素
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
//练习2:比较器排序
//要求:存入四个字符串, “c”, “ab”, “df”, “qwer”
//按照长度排序,如果一样长则按照首字母排序
//创建对象的同时传递一个Comparator接口的实现类的对象
//o1:表示当前要添加的元素
//o2:表示已经在红黑树存在的元素
TreeSet<String> ts = new TreeSet<>((o1, o2)->{
// 按照长度排序
int i = o1.length() - o2.length();
//如果一样长则按照首字母排序。o1.compareTo(o2),o1调用Comparable接口中的compareTo方法来进行默认比较
i = i == 0 ? o1.compareTo(o2) : i;
return i;
});