2021年Java进阶篇(三)List、Set、数据结构和Collections
进阶篇第三篇文章奉上
文章目录
前言
话不多说直接上干货~~~~~
提示:以下是本篇文章正文内容,下面案例可供参考
一、数据结构
1.1 数据结构是什么?
举个通俗的例子
当我们在使用Java里面的容器类的时候感觉非常的方便,但是我们并没有想过,为什么ArrayList就像一个无限扩充的数组,也好像是链表之类的。用着方便么?当然方便,然而这就是数据结构,让我们再不知不觉中就使用了。
本篇文章数据结构部分只是作为简单的了解,如果大家想要深入的学习了解数据结构的话,建议系统化的学习一下~~~
1.2 常见的数据结构
数据存储的常用结构:栈、队列、数组、链表和红黑树
1.2.1 栈
栈:stack,又称为堆栈,它是运算受限制的线性表,其限制是仅允许在标的一端进行插入和删除操作,不允许在其他的任何位置进行添加、查找和删除等操作。
栈的特点
先进后出
(存进去的元素,要在比该元素晚放进去的元素都取出来后,才能取出该元素)。
例如:子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,开枪的时候只有先把上面的子弹打出去了,才能把下面的子弹打出去
栈的入口、出口都是在栈的顶端位置
入栈:就是存元素,把元素存储到栈的顶端位置,栈中已有的元素依次向栈底的方向移动一个位置
出栈:就是取出来元素,把栈的顶端元素取出,栈中已有的元素依次向栈顶方向移动一个位置
1.2.2 队列
队列:queue,简称为队,它和堆栈一样也是一种运算受限制的线性表,但是相当于栈的只能在一个口进行插入删除来说已经好很多了,其限制是仅允许在表的一端进行插入,在表的另一端进行删除
队列的特点
先进先出
(存进去的元素,要在它前面的元素依次取出后才能取出该元素)。例如:火车进山洞,车头先进去车尾后进去,而出山洞的时候同样是车头先出来车尾后出来
队列的入口
和出口
各占一侧
1.2.3 数组
数组:Array,是有序的元素序列,数组就是在内存中开辟一段连续空间,并在此空间存放元素。例如:一排出租屋有20个房间,从01到20每一个房间都有固定编号,可以通过编号快速找到租房子的人。
数组的物理地址和逻辑地址都必须是连续的
数组的特点
查找元素快
:通过索引,可以快速访问指定位置的元素
增删元素慢
:指定索引位置增加元素,需要创建一个新的数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置
指定索引位置删除元素
:需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置元素不复制到新数组中
1.2.4 链表
链表:Linked list ,由一系列的结点node(链表中的每一个元素被称为结点)组成,结点可以在运行的时候动态生成。每个结点包括两个部分:义格式存储数据元素的数据域,另外一个是存储下一个结点地址的指针域。我们常说的链表结构有单向链表
和双向链表
,我们这里进行介绍的是单向链表
链表的特点
多个结点之间,通过地址进行连接。例如:多个人手拉手,每个人使用右手拉住下一个人的左手,依次类推,这样多个人之间就连在一起了
查找元素慢
:想查找某个元素,需要通过连接的结点,依次向后查找指定元素
增删元素快
:只需要修改连接下一个元素的地址即可
1.2.5 红黑树
红黑树:binary tree,是每个结点都不超过2的有序树
简单来说就是一种类似于我们生活中树的结构,只不过每个结点上都是最多只能有两个子节点。二叉树是每个节点最多有两个子树的树结构。顶上的叫做根节点,两边被称作"左子树"和"右子树"
而我们要说的二叉树是一种比较有意思的叫做红黑树,红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着树的键值仍然是有效的
红黑树的约束
- 结点可以是红色或者黑色的
- 根节点是黑色的
- 叶子节点(特指空节点)是黑色的
- 每一个红色节点的子节点都是黑色的
- 任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同
红黑树的特点
速度特别快,趋近于平衡二叉树,查找叶子元素最少和最多次数不多于二倍
二、List集合
2.1 List接口介绍
java.util.List
接口继承自Collection
接口,是单列集合的一个重要分支,习惯性的会将实现了List接口的对象称为List集合。在List集合中允许出现重复元素,所有的元素都是以一种线性方式进行存储,在程序中可以通过索引来访问集合中指定的元素。另外List集合还有一个特点就是元素有序,及元素的存入顺序和取出顺序是一致的
List接口特点
- 它是一个元素存储有序的集合。例如存入元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的
- 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与元素的索引是一个道理)
- 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素
2.2 List接口中的常用方法
List作为Collection集合的子接口,不但继承了Collection接口的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法
public void add(int index, E element)
: 将指定的元素,添加到该集合中的指定位置上。public E get(int index)
:返回集合中指定位置的元素。public E remove(int index)
: 移除列表中指定位置的元素, 返回的是被移除的元素。public E set(int index, E element)
:用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
public class ListDemo {
public static void main(String[] args) {
// 创建List集合对象
List<String> list = new ArrayList<String>();
// 往 尾部添加 指定元素
list.add("图图");
list.add("小美");
list.add("不高兴");
System.out.println(list);
// add(int index,String s) 往指定位置添加
list.add(1,"没头脑");
System.out.println(list);
// String remove(int index) 删除指定位置元素 返回被删除元素
// 删除索引位置为2的元素
System.out.println("删除索引位置为2的元素");
System.out.println(list.remove(2));
System.out.println(list);
// String set(int index,String s)
// 在指定位置 进行 元素替代(改)
// 修改指定位置元素
list.set(0, "三毛");
System.out.println(list);
// String get(int index) 获取指定位置元素
// 跟size() 方法一起用 来 遍历的
for(int i = 0;i<list.size();i++){
System.out.println(list.get(i));
}
//还可以使用增强for
for (String string : list) {
System.out.println(string);
}
}
}
三、List的子类
3.1 ArrayList集合
java.util.ArrayList
集合数据存储的结构是数组结构。元素的增删慢,查询快,由于日常开发中使用最多的功能是查询数据和遍历数据,所以ArrayList集合是最常用的集合。
3.2 LinkedList集合
java.util.LinkedList
集合数据存储的结构是链表结构,方便元素的添加、删除的集合
LinkedList集合是一个双向链表
LinkedList集合提供了大量首尾操作的方法
- public void addFirst(E e)`:将指定元素插入此列表的开头。
public void addLast(E e)
:将指定元素添加到此列表的结尾。public E getFirst()
:返回此列表的第一个元素。public E getLast()
:返回此列表的最后一个元素。public E removeFirst()
:移除并返回此列表的第一个元素。public E removeLast()
:移除并返回此列表的最后一个元素。public E pop()
:从此列表所表示的堆栈处弹出一个元素。public void push(E e)
:将元素推入此列表所表示的堆栈。public boolean isEmpty()
:如果列表不包含元素,则返回true。
LinkedList是List的子类,List中的方法LinkedList都是可以使用的。LinkedList集合也可以作为堆栈,队列的结构使用
public class LinkedListDemo {
public static void main(String[] args) {
LinkedList<String> link = new LinkedList<String>();
//添加元素
link.addFirst("abc1");
link.addFirst("abc2");
link.addFirst("abc3");
System.out.println(link);
// 获取元素
System.out.println(link.getFirst());
System.out.println(link.getLast());
// 删除元素
System.out.println(link.removeFirst());
System.out.println(link.removeLast());
while (!link.isEmpty()) { //判断集合是否为空
System.out.println(link.pop()); //弹出集合中的栈顶元素
}
System.out.println(link);
}
}
四、Set接口
java.util.Set
接口和java.util.List
接口一样,同样继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。与List接口不同的是,Set接口中元素无序,并且都会以某种规则保证存入的数据不出现重复。
Set集合有多个子类,这里我们主要是介绍java.util.HashSet
,java.util.LinkedHashSet
这两个集合
4.1 HashSet集合的介绍
java.util.HashSet是
Set接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。
java.util.HashSet底层的实现其实是一个
java.util.HashMap`支持,由于我们暂时还未学习,先做了解。
HashSet
是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashCode
与equals
方法。
public class HashSetDemo {
public static void main(String[] args) {
//创建 Set集合
HashSet<String> set = new HashSet<String>();
//添加元素
set.add(new String("cba"));
set.add("abc");
set.add("bac");
set.add("cba");
//遍历
for (String name : set) {
System.out.println(name);
}
}
}
输出结果如下,说明集合中不能存储重复元素:
cba
abc
bac
tips:根据结果我们发现字符串"cba"只存储了一个,也就是说重复的元素set集合不存储。
4.2 HashSet集合存储数据的结构(哈希表)
什么是哈希表?
在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。
HashSet存储原理图
总而言之,JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式
4.3 HashSet存储自定义类型元素
给HashSet中存放自定义类型元素时,需要重写对象的hashCode方法和equals方法,建立自定义的比较方法,这样才能够保证HashSet集合中的对象是唯一的
// 创建自定义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;
}
@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);
}
}
// 创建测试类
public class HashSetDemo2 {
public static void main(String[] args) {
//创建集合对象 该集合中存储 Student类型对象
HashSet<Student> stuSet = new HashSet<Student>();
//存储
Student stu = new Student("于谦", 43);
stuSet.add(stu);
stuSet.add(new Student("郭德纲", 44));
stuSet.add(new Student("于谦", 43));
stuSet.add(new Student("郭麒麟", 23));
stuSet.add(stu);
for (Student stu2 : stuSet) {
System.out.println(stu2);
}
}
}
执行结果:
Student [name=郭德纲, age=44]
Student [name=于谦, age=43]
Student [name=郭麒麟, age=23]
4.4 LinkedHashSet
我们都知道HashSet保证元素唯一,但是元素存放是没有顺序的,所以我们怎么要保证元素有序呢?
在HashSet下面有一个子类java.util.LinkedHashSet
,它是链表和哈希表组合的一个数据存储结构
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<String>();
set.add("bbb");
set.add("aaa");
set.add("abc");
set.add("bbc");
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
结果:
bbb
aaa
abc
bbc
五、Collections
5.1 常用功能
java.utils.Collections
是集合工具类,用来对集合进行操作。部分方法如下:
public static <T> boolean addAll(Collection<T> c, T... elements)
:往集合中添加一些元素。public static void shuffle(List<?> list) 打乱顺序
:打乱集合顺序。public static <T> void sort(List<T> list)
:将集合中元素按照默认规则排序。public static <T> void sort(List<T> list,Comparator<? super T> )
:将集合中元素按照指定规则排序。
public class CollectionsDemo {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
//原来写法
//list.add(12);
//list.add(14);
//list.add(15);
//list.add(1000);
//采用工具类 完成 往集合中添加元素
Collections.addAll(list, 5, 222, 1,2);
System.out.println(list);
//排序方法
Collections.sort(list);
System.out.println(list);
}
}
结果:
[5, 222, 1, 2]
[1, 2, 5, 222]
代码演示之后 ,发现我们的集合按照顺序进行了排列,可是这样的顺序是采用默认的顺序,如果想要指定顺序那该怎么办呢?
我们发现还有个方法没有讲,public static <T> void sort(List<T> list,Comparator<? super T> )
:将集合中元素按照指定规则排序。接下来讲解一下指定规则的排列。
5.2 Comparator比较器
我们还是先研究这个方法
public static <T> void sort(List<T> list)
:将集合中元素按照默认规则排序。
不过这次存储的是字符串类型。
public class CollectionsDemo2 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("cba");
list.add("aba");
list.add("sba");
list.add("nba");
//排序方法
Collections.sort(list);
System.out.println(list);
}
}
结果:
[aba, cba, nba, sba]
我们使用的是默认的规则完成字符串的排序,那么默认规则是怎么定义出来的呢?
说到排序了,简单的说就是两个对象之间比较大小,那么在JAVA中提供了两种比较实现的方式,一种是比较死板的采用java.lang.Comparable
接口去实现,一种是灵活的当我需要做排序的时候在去选择的java.util.Comparator
接口完成。
那么我们采用的public static <T> void sort(List<T> list)
这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口完成比较的功能,在String类型上如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
String类实现了这个接口,并完成了比较规则的定义,但是这样就把这种规则写死了,那比如我想要字符串按照第一个字符降序排列,那么这样就要修改String的源代码,这是不可能的了,那么这个时候我们可以使用
public static <T> void sort(List<T> list,Comparator<? super T> )
方法灵活的完成,这个里面就涉及到了Comparator这个接口,位于位于java.util包下,排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是:
-
public int compare(String o1, String o2)
:比较其两个参数的顺序。两个对象比较的结果有三种:大于,等于,小于。
如果要按照升序排序,
则o1 小于o2,返回(负数),相等返回0,01大于02返回(正数)
如果要按照降序排序
则o1 小于o2,返回(正数),相等返回0,01大于02返回(负数)
操作如下:
public class CollectionsDemo3 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("cba");
list.add("aba");
list.add("sba");
list.add("nba");
//排序方法 按照第一个单词的降序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.charAt(0) - o1.charAt(0);
}
});
System.out.println(list);
}
}
结果如下
[sba, nba, cba, aba]
5.3 简述Comparable和Comparator两个接口的区别
Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
Comparator强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。
补充
可变参数
在JDK1.5之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化成如下格式:
修饰符 返回值类型 方法名(参数类型... 形参名){ }
其实这个书写完全等价与
修饰符 返回值类型 方法名(参数类型[] 形参名){ }
只是后面这种定义,在调用时必须传递数组,而前者可以直接传递数据即可。
JDK1.5以后。出现了简化操作。… 用在参数上,称之为可变参数。
同样是代表数组,但是在调用这个带有可变参数的方法时,不用创建数组(这就是简单之处),直接将数组中的元素作为实际参数进行传递,其实编译成的class文件,将这些元素先封装到一个数组中,在进行传递。这些动作都在编译.class文件时,自动完成了。
public class ChangeArgs {
public static void main(String[] args) {
int[] arr = { 1, 4, 62, 431, 2 };
int sum = getSum(arr);
System.out.println(sum);
// 6 7 2 12 2121
// 求 这几个元素和 6 7 2 12 2121
int sum2 = getSum(6, 7, 2, 12, 2121);
System.out.println(sum2);
}
/*
* 完成数组 所有元素的求和 原始写法
public static int getSum(int[] arr){
int sum = 0;
for(int a : arr){
sum += a;
}
return sum;
}
*/
//可变参数写法
public static int getSum(int... arr) {
int sum = 0;
for (int a : arr) {
sum += a;
}
return sum;
}
}
tips: 上述add方法在同一个类中,只能存在一个。因为会发生调用的不确定性
注意:如果在方法书写时,这个方法拥有多参数,参数中包含可变参数,可变参数一定要写在参数列表的末尾位置。