数据结构就是计算机存储、组织数据的方式。
在计算机科学中,算法的时间复杂度是一个函数,它定性描述了该算法的运行时间,常用O符号来表述。
时间复杂度是同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法
我们对数组的CRUD操作进行性能分析
添加操作
如果保存在数组的最后一个位置,至少需要一次操作
如果保存的位置在数组的第一个位置,那么如果存在N个元素,那么此时后面的元素需要整体后移,此时需要操作N次
那么平均就是(N+1)/2次,如果需要扩容,那么性能会更低
删除操作
如果删除的是最后一个元素,那么需要操作一次
如果操作的是第一个元素,那么其他元素需要整体前移,需要操作N次
平均就是(N+1)/2次
修改操作
给定索引时,仅仅只是操作一次
查询操作
根据索引操作1次,如果根据内存查询的话需要操作N次
总结·
基于数组的数据结构做查询和修改事宜非常快的(性能很高),如果做删除和增加就比较慢了,那如果想保证保存和删除操作的性能,此时就得提链表这种数据结构了
链表(类似火车和火车车厢)是由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时i动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
我们常说的链表结构有单向链表与双向链表分为两种:
单向链表:只能从头到尾(从尾到头)遍历
双向链表:既可以从头到尾又可以从尾到头遍历
对链表操作的性能分析
增加操作
仅仅只是操作1次,断掉链和新增链
删除操作
仅仅只是操作1次
修改操作
如果修改的是第一个元素,那么需要操作1次,如果需要修改的是最后一个元素,那么需要操作N次,所以平均(N+1)/2
查询操作
如果查询的是第一个元素,那么需要操作1次,如果需要查询的是最后一个元素,那么需要操作N次,所以平均(N+1)/2
结论
链表的查询和修改性能比较低,而增加和删除性能高
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,队列是一种操作受限制的线性表。
进行插入操作的端称为队尾,进行删除操作的端称为队头,单向队列是先进先出的,只能从队尾插入元素,从对头删除元素
单项队列
双向队列
栈(stack)又名堆栈,它是一种运算受限的线性表,后进先出(LIFO),和水瓶类似,先装进去的水最后才可以喝到。
栈结构仅允许在表的一端进行插入和删除运算,这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈中插入新元素又称作入栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素。从一个栈中删除元素又称作出栈,表示把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
数组中的元素在数组中的索引位置是随机的,元素的取值和元素的位置之间没有确定的关系,因此在数组中查找特定的值时,需要将特定的值和整个数组元素进行一个个比较。
此时查询的效率依赖于比较的次数,如果比较的次数比较多,那么此时查询的效率还是不高。
如果此时元素的值(value)和在数组中的索引位置(index)有一个确定的对应关系,我们将这种关系称之为哈希(hash),则元素值和索引之间对应的公式为:index = hash(value),也就是说给定元素值,只要调用了hash(value)方法,就能找到数组中取值value的元素的位置
比方说图中的hash的算法公式为:index = value/10-1,在哈希表中存储对象时,该hash算法就是对象的hashCode方法(真正的hash算法并不是这样,只是打个比方,真实的hash算法我们大可不必去关心)
在JDK1.8之前,哈希表底层采用数组+链表实现,即使用数组处理冲突,同一hash值的链表都存储在一个数组里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。
他的存储原理如下图,JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。
计算机中的树,是根据生活中的树抽象而来的,表示N个有父子关系的节点的集合。
N为0的时候,该节点集合为空,这棵树就是空树
任何非空树中,有且只有一个根节点(root)
N>1时,一颗树由根和若干棵子树组成,每棵子树由更小的若干子树组成
树中的节点根据有没有子节点,分成两种:
名词 | 含义 |
---|---|
节点 | 指树中的一个元素 |
节点的度 | 节点拥有的子树的个数,二叉树的度不大于2 |
叶子节点 | 度为0的节点,也称之为终端结点 |
高度 | 叶子结点的高度为1,叶子结点的父节点高度为2,以此类推,根节点的高度最高 |
层 | 根节点在第一层,以此类推 |
父节点 | 若一个节点含有子节点,则这个节点称之为其子节点的父节点 |
子节点 | 子节点是父节点的下一层节点 |
兄弟节点 | 拥有共同父节点的节点互称为兄弟节点 |
树的结构因为存在多种子节点情况,真的太复杂了,如果我们对普通的树加上一些约束,比如让每一棵树的节点最多只能包含两个子节点,而且严格区分左子节点和右子节点(左右位置不能交换),此时就形成了二叉树。
排序二叉树是一颗有顺序的数,满足以下三个条件:
增删改查的性能都很高!!!遍历获取元素的时候可以按照"左中右"的顺序进行遍历;
注意:二叉查找树存在的问题:会出现"瘸子"的现象,影响查询效率。因此此时相当于链表,比如12345678排成一棵树就变成了链表
为了避免出现"瘸子"的现象,减少树的高度,提高我们的搜素效率,又存在一种树的结构:"平衡二叉树"
规则:它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
左图是一棵平衡二叉树,根节点10,左右两子树的高度差是1,而右图,虽然根节点左右两子树高度差是0,但是右子树15的左右子树高度差为2,不符合定义,所以右图不是一棵平衡二叉树。
变成平衡二叉树的办法就是那边高就往他的反方向旋转。
左旋就是将节点的右支往左拉,右子节点变成父节点,并把晋升之后多余的左子节点出让给降级节点的右子节点;
将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点
举个例子,像上图是否平衡二叉树的图里面,左图在没插入前"19"节点前,该树还是平衡二叉树,但是在插入"19"后,导致了"15"的左右子树失去了"平衡",
所以此时可以将"15"节点进行左旋,让"15"自身把节点出让给"17"作为"17"的左树,使得"17"节点左右子树平衡,而"15"节点没有子树,左右也平衡了。如下图:
由于在构建平衡二叉树的时候,当有新节点插入时,都会判断插入后时候平衡,这说明了插入新节点前,都是平衡的,也即高度差绝对值不会超过1。
当新节点插入后,有可能会有导致树不平衡,这时候就需要进行调整,而可能出现的情况就有4种,分别称作左左,左右,右左,右右。
左左即为在原来平衡的二叉树上,在节点的左子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"10"节点的左子树"7",的左子树"4",插入了节点"5"或"3"导致失衡。
左右即为在原来平衡的二叉树上,在节点的左子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2,如上即为"11"节点的左子树"7",的右子树"9",插入了节点"10"或"8"导致失衡。
右左即为在原来平衡的二叉树上,在节点的右子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如上即为"11"节点的右子树"15",的左子树"13",插入了节点"12"或"14"导致失衡。
右右即为在原来平衡的二叉树上,在节点的右子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"11"节点的右子树"13",的左子树"15",插入了节点"14"或"19"导致失衡。
右右只需对节点进行一次左旋即可调整平衡,如下图,对"11"节点进行左旋。
红黑树本质上是一颗具有更高查询效率的排序二叉树。
排序二叉树可以快速查找,但是如果只有左节点或者左右右节点的时候,此时二叉树就变成了普通
的链表结构,查询效率比较低。为此一种更高效的二叉树出现了——红黑树,满足以下几个条件:
集合是Java中提供的一种容器,可以用来存储多个数据
数组相比于集合来说缺点很明显:
集合是Java中提供的一种容器,可以用来存储多个数据,根据不同存储方式形成的体系结构,就叫做集合框架体系。集合也时常被称为容器,且集合中存储的数据叫做元素,而元素只可以是对象
根据容器的存储特点的不同,可以分成三种情况:
集合 | 描述 |
---|---|
List(列表) | 允许记录添加顺序,允许元素重复(有序可重复) |
Set(集合) | 不记录元素添加的顺序,不允许元素重复(无序且唯一) |
Map(映射) | 容器中每一个元素都包含一对key和value,key不允许重复,value可以重复 |
我们查看源码可以看到集合的继承关系:List和Set继承与Collection接口,而Map不继承Collection接口, 容器接口或类都处于java.util包中
接口 | 描述 |
---|---|
Collection接口 | 泛指广义上的集合,主要表示List和Set两种存储方式 |
List接口 | 表示列表,有序可重复 |
Set接口 | 表示狭义上的集合,无序不可重复 |
Map接口 | 表示映射关系 |
List接口是Collection接口子接口,List接口定义了一种规范,要求该容器允许记录元素的添加顺序,也允许元素重复。那么List接口的实现类都会遵循这一种规范。
List集合存储的特点:
该接口常用的实现类有:
Object set(int index, Object ele):修改列表中指定索引位置的元素,返回被替换的旧元素
ArrayList类,基于数组算法的列表,通过查看源代码会发现底层其实就是一个Object数组
ArrayList是一个List接口的实现类,实现了可变数组,当添加一个元素时,如果容量足够,直接添加,如果容量不够,按照newCapacity = oldCapacity + oldCapacity/2
原则拓容
由于ArrayList底层数据结构是数组,决定了该实现类在查询的场景下比较适用,添加和删除的场景效率稍低
public class ArrayListDemo1 { /** ArrayList的常用API **/ public static void main(String[] args) { //创建一个默认长度的列表对象 List list = new ArrayList(); //打印集合中元素的个数 System.out.println("元素数量:"+list.size());//0 //添加操作:向列表中添加4个元素 list.add("Will"); list.add(100); list.add(true); list.add("Lucy"); //查询操作: System.out.println("列表中所有元素:"+list);//输出:[Will, 100, true, Lucy] System.out.println("元素数量:"+list.size());//4 System.out.println("第一个元素:"+list.get(0));//Will //修改操作:把索引为2的元素,替换为wolfcode list.set(2, "wolfcode"); System.out.println("修改后:"+list);//输出:[Will, 100, wolfcode, Lucy] //删除操作:删除索引为1的元素 list.remove(1); System.out.println("删除后:"+list);//输出:[Will, wolfcode, Lucy] } }
我们画出ArrayList的内存图可以发现集合类中存储的对象,都存储的是对象的引用,而不是对象本身
ArrayList实现了三个接口:
类的序列化由实现java.io.Serializable
接口的类启用。 不实现此接口的类将不会使任何状态序列化或反序列化。 可序列化类的所有子类型都是可序列化的。 序列化接口没有方法或字段,仅用于标识可串行化的语义。
序列化:将对象的数据写入到文件(写对象)。
反序列化:将文件中对象的数据读取出来(读对象)。
public class Student implements Serializable{ private static final long serialVersionUID = 1014100089306623762L; //姓名 private String name; //年龄 private Integer age; public Student() { } public Student(String name, Integer age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { //优化toString方法 StringBuilder sb = new StringBuilder(); sb.append("[").append("name = ").append(this.name).append(", ").append("age = ").append(this.age).append("]"); return sb.toString(); } }
public class Test01 { public static void main(String[] args) throws Exception { Student s = new Student(); System.out.println(s); //创建对象操作流 --> 序列化 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MyTest\\obj.txt")); //创建集合,且添加学生对象 ArrayList<Student> list = new ArrayList<Student>(); list.add(new Student("悔创阿里杰克马",51)); list.add(new Student("会点一点长几颗",26)); list.add(new Student("容颜老去蒋青青",32)); list.add(new Student("将里最丑刘一飞",27)); //将集合写入到文件 oos.writeObject(list); //创建对象输入流 --> 反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("MyTest\\obj.txt")); //读取数据 Object o = ois.readObject(); //向下转型 ArrayList<Student> al = (ArrayList<Student>) o; //遍历集合 for (int i = 0; i < al.size(); i++) { //根据索引取出集合的每一个元素 Student stu = al.get(i); System.out.println(stu); } } }
一个类实现Cloneable接口
就会拥有Object.clone() 方法
,该方法对于该类的实例进行字段的复制是合法的。在不实现 Cloneable 接口
的实例上调用对象的克隆方法会导致异常CloneNotSupportedException
克隆就是依据已经有的数据,创造一份新的完全一样的数据拷贝。
克隆有两个前提条件:
public class ArrayList_Clone { public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("人生就是旅途"); list.add("也许终点和起点会重合"); list.add("但是一开始就站在起点等待终点"); list.add("那么其中就没有美丽的沿途风景和令人难忘的过往"); //调用方法进行克隆 Object o = list.clone(); System.out.println(o == list); System.out.println(o); System.out.println(list); } }
public class ArrayList<E> { public Object clone() { try { ArrayList<?> v = (ArrayList<?>) super.clone(); v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { throw new InternalError(e); } } }
实现了RandomAccess
接口表明它们支持快速(通常为恒定时间)随机访问。主要目的是允许通用算法更改其行为,以便在应用于随机访问列表或顺序访问列表时提供良好的性能。通过代码,我们可以看到是随机访问的速度大于顺序访问。
public class Test01 { public static void main(String[] args) { //创建ArrayList集合 List<String> list = new ArrayList<>(); //添加10W条数据 for (int i = 0; i < 100000; i++) { list.add(i+"a"); } System.out.println("----通过索引(随机访问:)----"); long startTime = System.currentTimeMillis(); for (int i = 0; i < list.size(); i++) { //仅仅为了演示取出数据的时间,因此不对取出的数据进行打印 list.get(i); } long endTime = System.currentTimeMillis(); System.out.println("随机访问: "+(endTime-startTime)); System.out.println("----通过迭代器(顺序访问:)----"); startTime = System.currentTimeMillis(); Iterator<String> it = list.iterator(); while (it.hasNext()){ //仅仅为了演示取出数据的时间,因此不对取出的数据进行打印 it.next(); } endTime = System.currentTimeMillis(); System.out.println("顺序访问: "+(endTime-startTime)); } }
LinkedList由于底层数据结构是链表,在添加元素和删除元素时效率很高,查询元素效率低。
LinkedList类,底层采用链表算法,实现了链表,队列,栈的数据结构。无论是链表还是队列主要操作的都是头和尾的元素,因此在LinkedList类中除了List接口的方法,还有很多操作头尾的方法。常用的方法如下:
//将指定元素插入此列表的开头。 void addFirst(Object e); //将指定元素添加到此列表的结尾。 void addLast(Object e); //返回此列表的第一个元素。 Object getFirst(); //返回此列表的最后一个元素。 Object getLast(); //移除并返回此列表的第一个元素。 Object removeFirst(); //移除并返回此列表的最后一个元素。 Object removeLast(); //在此列表的开头插入指定的元素。 boolean offerFirst(Object e); //在此列表末尾插入指定的元素。 boolean offerLast(Object e); ////获取但不移除此列表的第一个元素;如果此列表为空,则返回 null。 Object peekFirst(); ////获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null。 Object peekLast(); ////获取并移除此列表的第一个元素;如果此列表为空,则返回 null。 Object pollFirst(); //获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。 Object pollLast(); //将元素推入此列表所表示的栈。 void push(Object e); //从此列表所表示的栈处弹出一个元素。 Object pop(); //获取但不移除此列表的头(第一个元素)。 Object peek()
import java.util.LinkedList; public class LinkedListDemo { public static void main(String[] args) { LinkedList list = new LinkedList(); //添加元素 list.addFirst("A"); list.addFirst("B"); System.out.println(list); list.addFirst("C"); System.out.println(list); list.addLast("D"); System.out.println(list); //获取元素 System.out.println("获取第一个元素:" + list.getFirst());//C System.out.println("获取最后一个元素:" + list.getLast());//D //删除元素 list.removeFirst(); System.out.println("删除第一个元素后:" + list);//[B, A, D] list.removeLast(); System.out.println("删除最后一个元素后:" + list);//[B, A] } }
我们可以利用代码来比较一下是随机访问的速度快还是顺序访问。我们可以看到LinkedList随机访问比顺序访问要慢很多。
public class Test02 { public static void main(String[] args) { //创建LinkedList集合 List<String> list = new LinkedList<>(); //添加10W条数据 for (int i = 0; i < 100000; i++) { list.add(i+"a"); } System.out.println("----通过索引(随机访问:)----"); long startTime = System.currentTimeMillis(); for (int i = 0; i < list.size(); i++) { //仅仅为了演示取出数据的时间,因此不对取出的数据进行打印 list.get(i); } long endTime = System.currentTimeMillis(); System.out.println("随机访问: "+(endTime-startTime)); System.out.println("----通过迭代器(顺序访问:)----"); startTime = System.currentTimeMillis(); Iterator<String> it = list.iterator(); while (it.hasNext()){ //仅仅为了演示取出数据的时间,因此不对取出的数据进行打印 it.next(); } endTime = System.currentTimeMillis(); System.out.println("顺序访问: "+(endTime-startTime)); } }
Vector类:基于数组算法实现的列表,其实就是ArrayList类的前身。和ArrayList的区别在于方法使用synchronized修饰,所以相对于ArrayList来说,线程安全,但是效率就低了点。
Stack类:表示栈,是Vector类的子类,具有后进先出(LIFO)的特点,拥有push(入栈),pop(出栈)方法。
List<String> list = new ArrayList<>(); list.add("西施"); list.add("王昭君"); list.add("貂蝉"); list.add("杨玉环");
for (int index = 0; index < list.size(); index++) { String ele = list.get(index); System.out.println(ele); }
在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator
。
想要遍历Collection集合,那么就要获取该集合迭代器完成迭代操作,下面介绍一下获取迭代器的方法:
public Iterator iterator()
: 获取集合对应的迭代器,用来遍历集合中的元素的。Iterator表示迭代器对象,迭代器中拥有一个指针,默认指向第一个元素之前,有如下几个方法:
public E next()
:判断指针后是否存在下一个元素public boolean hasNext()
:获取指针位置下一个元素,获取后指针向后移动一位,如果仍有元素可以迭代,则返回 trueIterator iterator = list.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); }
注意:
java.util.NoSuchElementException
没有集合元素异常。ConcurrentModificationException
并发修改异常。语法:
for(数据类型 迭代变量: 实例对象){ //TODO }
for (String ele : list) { System.out.println(ele); }
list.forEach(object -> System.out.println(object));
在迭代集合时删除集合元素,比如删除王昭君
List<String> list = new ArrayList<>(); list.add("西施"); list.add("王昭君"); list.add("貂蝉"); list.add("杨玉环"); for (String ele : list){ if ("王昭君".equals(ele)){ list.remove(ele); } System.out.println(ele); }
执行完会发现报错了:
![image-20201215213112816](D:\学习笔记\Java SE\Java集合\Java集合.assets\image-20201215213112816.png)
造成该错误的原因是,不允许在迭代过程中改变集合的长度(不能删除和增加)。
如果要在迭代过程中删除元素,就不能使用集合的remove方法,只能使用迭代器的remove方法,此时只能使用迭代器来操作,不能使用foreach。
List<String> list = new ArrayList<>(); list.add("西施"); list.add("王昭君"); list.add("貂蝉"); list.add("杨玉环"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()){ if ("王昭君".equals(iterator.next())){ iterator.remove(); } } System.out.println(list);
Set是Collection的子接口,Set接口定义了一种规范,也就是说明该容器不记录元素的添加顺序,也不允许元素重复。
不允许元素重复
不会记录元素添加的先后顺序
set只包含从Collection继承的方法,不过Set无法记住添加的顺序,也不允许元素重复,当将两个相同的元素添加进Set集合的时候,添加操作失败,添加方法(add()
)返回false
HashSet类:底层采用哈希表实现
TreeSet类:底层采用红黑书实现,可以对集合中的元素排序
HashSet底层采用哈希表实现,元素对象的HashCode值决定了在哈希表中存储的位置,当往HashSet中添加新元素的时候,先会判断该位置是否有元素:
如果没有,直接把该新的对象存储到hashCode指定的位置
如果有,再继续判断新对象和集合对象的equals作比较:
每一个存储到哈希表中的对象,都得重写hashCode和equals方法来判断是否是同一个对象,在一般情况下,equals为true的时候,hashCode应该也相等,Idea可以自动生成这些方法
package day14_ArrayList2.classing; import java.util.HashSet; import java.util.Iterator; import java.util.Set; /** * @author Xiao_Lin * @version 1.0 * @date 2020/12/17 19:38 */ public class SetApi { public static void main(String[] args) { Set<String> set = new HashSet<>(); //添加操作,向列表中添加元素 set.add("will"); set.add("wolf"); set.add("code"); set.add("lucy"); //查询操作 System.out.println("集合中的所有元素为:"+set); System.out.println("元素数量:"+set.size()); System.out.println("是否存在某个元素:"+set.contains("111")); System.out.println("是否存在某个元素:"+set.contains("code")); //删除元素 set.remove("code"); System.out.println("删除后的集合为:"+set); //增强for循环遍历 for (String ele : set){ System.out.println(ele); } //迭代器遍历 Iterator<String> it = set.iterator(); while (it.hasNext()){ System.out.println( it.next()); } } }
package day14_ArrayList2.classing; /** 重写equals和hashCode方法 * @author Xiao_Lin * @version 1.0 * @date 2020/12/17 11:34 */ public class Student { private String name; private Integer sn; private String classname; @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Student student = (Student) o; if (name != null ? !name.equals(student.name) : student.name != null) { return false; } if (sn != null ? !sn.equals(student.sn) : student.sn != null) { return false; } return classname != null ? classname.equals(student.classname) : student.classname == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (sn != null ? sn.hashCode() : 0); result = 31 * result + (classname != null ? classname.hashCode() : 0); return result; } public Student(String name, Integer sn, String classname) { this.name = name; this.sn = sn; this.classname = classname; } public Student(){ } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getSn() { return sn; } public void setSn(Integer sn) { this.sn = sn; } public String getClassname() { return classname; } public void setClassname(String classname) { this.classname = classname; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", sn=" + sn + ", classname='" + classname + '\'' + '}'; } }
TreeSet底层采用了红黑树算法,会对存储的元素对象默认使用自然排序(升序)
数据类型 | 排序规则 |
---|---|
BigDecimal、BigInteger、Byte、Double、Float、Integer、Long、Short | 按照数字大小排序 |
Character | 按照字符的Unicode值的数字大小排序 |
String | 按照字符串中字符的Unicode值排序 |
必须保证TreeSet集合中的元素对象是相同的数据类型,否则报错
import java.util.Set; import java.util.TreeSet; public class TreeSetDemo{ public static void main(String[] args) { Set<String> set = new TreeSet<>(); set.add("wolf"); set.add("will"); set.add("sfef"); set.add("allen"); System.out.println(set);//[allen, sfef, will, wolf] } }
第一步
插入第一个节点,无需比较,直接作为根节点
第二步
插入第二个节点,和wolf根节点比较,如果小于根节点就作为左子树,如果大于根节点就放在右边,作为右子树
第三步
插入第三个节点,先和wolf根节点比较,较小,左移动作为左子树,因为左边已经有了一颗左子树,所以再和will节点进行比较,较小,继续放在左边作为左子树
第四步
由于TreeSet是平衡二叉树,如果树不平衡(左右子树差值大于1),会对节点进行调整
第五步
插入第四个节点,先和根节点will进行比较,较小,左移,再和sfef节点作比较,依然较小,左移
在缺省的情况下,TreeSet中的元素会采用自然排序(从小到大),此时要求元素对象必须实现java.util.Comparable
接口,大多数的JDK自带的类都实现了该接口,比如说最常见的八大包装类和String
TreeSet会调用元素的comparaTo()方法来比较元素的大小关系,然后将集合元素按照升序排列
public interface Comparable<T> { public int compareTo(T o); }
比较规则
如果我们自定义一个类,且需要存储到TreeSet中,此时我们需要让该类实现Comparable接口,并且覆盖compareTo()方法,在该方法中编写比较规则
package day14_ArrayList2.classing; /** * @author Xiao_Lin * @version 1.0 * @date 2020/12/17 11:34 */ public class Student implements Comparable<Student>{ private String name; private Integer sn; private String classname; //比较规则 @Override public int compareTo(Student student) { //当前对象大于传进来的对象 if (this.sn > student.sn){ return 1; }else if (this.sn < student.sn){ return -1; } return 0; //可以简化为; //return this.sn - student.sn; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Student student = (Student) o; if (name != null ? !name.equals(student.name) : student.name != null) { return false; } if (sn != null ? !sn.equals(student.sn) : student.sn != null) { return false; } return classname != null ? classname.equals(student.classname) : student.classname == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (sn != null ? sn.hashCode() : 0); result = 31 * result + (classname != null ? classname.hashCode() : 0); return result; } public Student(String name, Integer sn, String classname) { this.name = name; this.sn = sn; this.classname = classname; } }
TreeSet除了支持默认的自然排序外,还支持自定义排序,此时需要在构建TreeSet对象时传递java.util.Comparator
接口的实现类对象,Comparator表示比较器,里面封装了比较规则。
此时compare方法返回0,则认为两个对象是同一个对象,返回正数排前面,返回负数排后面。
public interface Comparator<T> { int compare(T o1, T o2); }
示范
class User { private String name; import java.util.TreeSet; private int age; public User(String name, int age) { this.name = name; this.age = age; } public int getAge() { return age; } public String getName() { return name; } public String toString() { return "User [name=" + name + ", age=" + age + "]"; } } public class App { public static void main(String[] args) { Set<User> set = new TreeSet<>(new NameLengthComparator()); set.add(new User("James", 30)); set.add(new User("Bryant", 22)); set.add(new User("Allen", 28)); set.add(new User("Will", 17)); System.out.println(set);// } } //封装了比较规则 class NameLengthComparator implements java.util.Comparator<User> { public int compare(User o1, User o2) { int ret = o1.getName().length() - o2.getName().length(); if (ret > 0) { return 1; } else if (ret < 0) { return -1; } else { if (o1.getAge() > o2.getAge()) { return 1; } else if (o1.getAge() < o2.getAge()) { return -1; } else { return 0; } } } }
Map翻译为映射
从结构图上看,Map并不是集合,而是类似两个集合的映射关系,所以Map中没有实现Collection接口
在Map中,要求A集合的每一个元素(key)都可以在B集合中找到唯一的值(value)与之对应,意味着A集合中的元素是不可以重复的而B集合中的元素却可以重复,所以A集合应该是一个Set集合,而B集合是一个List集合
其实一个Map中就由很多的key-value(kv键值对)组成的,每一个键值对我们用Entry表示
其实我们也可以把Map看成是多个Entry的集合
总的来说,我们还是习惯把Map称为集合,不过和List、Set不同,List、Set是单元素集合,Map是双元素集合
单元素集合:每一次只能存储一个元素,比如List、Set
双元素集合:每次需要存储两个元素(一个key一个value),比如Map
注意:
Object remove(Object key):从Map中删除指定key的键值对,并返回被删除key对应的value
无专门的方法,可以调用put方法,存储相同key,不同value的键值对,可以覆盖原来的。
map.forEach((k,v)->{ System.out.println(k+" "+v); });
HashMap底层是基于哈希表算法,Map中存储的key对象和HashCode值决定了在哈希表中存储的位置,因为Map中的key是Set,所以不能保证添加的先后顺序,也不允许重复
import java.util.HashMap; import java.util.Map; public class HashMapDemo1{ public static void main(String[] args) { Map<String, String> map = new HashMap<>(); map.put("girl1", "西施"); map.put("girl2", "王昭君"); map.put("girl3", "貂蝉"); map.put("girl4", "杨玉环"); System.out.println("map中有多少键值对:"+map.size()); System.out.println(map); System.out.println("是否包含key为girl1:"+map.containsKey("girl1")); System.out.println("是否包含value为貂蝉:"+map.containsValue("貂蝉")); //替换key为girl3的value值 map.put("girl3", "小乔"); System.out.println(map); //删除key为girl3的键值对 map.remove("girl3"); System.out.println(map); } }
//获取Map中所有的key Set<String> keys = map.keySet(); System.out.println("Map中所有key:"+keys); //获取Map中所有的value Collection<String> values = map.values(); System.out.println("Map中所有value:"+values); //获取Map中所有的key-value(键值对) Set<Entry<String, String>> entrys = map.entrySet(); for (Entry<String, String> entry : entrys) { String key = entry.getKey(); String value = entry.getValue(); System.out.println(key+"->"+value); }
统计一个字符串中每隔字符出现次数
package day14_ArrayList2.classing; import java.util.HashMap; import java.util.Scanner; /** * @author Xiao_Lin * @version 1.0 * @date 2020/12/17 16:54 */ public class Test1 { //需求:统计一个字符串中每一个字符出现的次数 public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String str = scanner.nextLine(); HashMap<Character, Integer> map = new HashMap();//新建一个map,map的key存储字符,value存储出现的次数 //进行map的遍历 for (int i = 0 ;i<str.length();i++){ char c = str.charAt(i);//将str字符串遍历并且强转为字符 if (map.containsKey(c)){//map是否包含遍历的字符 Integer value = map.get(c);//如果包含就将原来的字符对应的value(次数)值取出来并且自增 value+=1;//次数自增 map.put(c,value);//将修改后的value放入map集合中 }else { map.put(c,1);//否则将字符放进集合中,并且将次数置为1 } } System.out.println(map); } }
TreeMap底层的key是基于红黑树,因为Map中的key也是一个Set集合,所以不能保证添加的先后顺序,且也不允许重复,因为Map中存储的key会默认使用自然排序(从小到大),和TreeSet一样,除了可以使用自然排序也可以自定义自己的排序
import java.util.Map; import java.util.TreeMap; public class App { public static void main(String[] args) { Map<String, String> map = new TreeMap<>(); map.put("girl4", "杨玉环"); map.put("girl2", "王昭君"); map.put("key1", "西施"); map.put("key3", "貂蝉"); System.out.println(map); //------------------------------------------- map = new TreeMap<>(map); System.out.println(map); } }
package day14_ArrayList2.classing; import java.util.Comparator; import java.util.TreeSet; /** * @author Xiao_Lin * @version 1.0 * @date 2020/12/17 15:55 */ public class TreeSet1 { public static void main(String[] args) { //使用自定义的比较器,需要传入一个Comparator接口的实现类,这里使用匿名内部类 TreeSet<String> set = new TreeSet<String>(new Comparator<String>() { @Override public int compare(String s1, String s2) { if (s1.length()==s2.length()){ return s1.compareTo(s2); }else { return s1.length()-s2.length(); } } }); set.add("s12"); set.add("see"); set.add("s1234"); System.out.println(set); } }
异常是指在程序的运行过程中所发生的不正常现象,它会中断正在运行的程序,异常不是错误,更不是逻辑的错误
异常会导致程序中断进行
Java异常处理机制
Java编程语言使用异常处理机制为程序提供了异常处理的能力,异常处理机制可以保证程序出现异常后,继续向正确的方向运行
异常处理的分类
异常处理包含两种代码块:
异常对象是出现异常时的那条语句自动产生的一个对象,由JVM自动创建,异常在Java类中通过Exception
或者其他具体的子类创建,命名规则是:异常类型+Exception,Exception是所有异常的父类,他有如下方法
方法 | 作用 |
---|---|
toString() | 返回异常类型和异常信息 |
getMessage() | 返回异常信息 |
printStackTrace | 打印堆栈信息(红色) |
堆栈的信息大致如下:
![image-20201219191345443](D:\学习笔记\Java SE\Java集合\Java集合.assets\image-20201219191345443.png)
第一句:由异常类型和异常的Message构成
最后一句:异常发生的具体位置
把可能产生异常的代码放到try中,如果代码产生了异常由catch捕获异常,然后交由catch里面的代码请求处理
try{ //可能产生异常的代码块 }catch(异常类型 e){//异常由catch捕获 //异常处理 }
如果没有发现异常,则程序正常执行
程序出现了异常,且异常被catch捕获,也就是说异常的类型符合catch中的类型,此时进行异常处理,处理完成后程序正常执行
package day15_exception.classing.exception; import java.util.Scanner; /** * @author Xiao_Lin * @version 1.0 * @date 2020/12/19 11:31 */ public class TestException { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { System.out.println("请输入被除数:"); int num1 = scanner.nextInt(); System.out.println("请输入除数:"); int num2 = scanner.nextInt(); System.out.println(num1/num2); }catch (Exception e){ System.out.println("出现异常"); System.out.println(e.getMessage()); } } }
![image-20201219190758864](D:\学习笔记\Java SE\Java集合\Java集合.assets\image-20201219190758864.png)
总结
异常发生后,从异常发生的那句代码开始,程序不继续向下运行,立即转入异常处理
异常不匹配时,程序中断
package day15_exception.classing.exception; import java.util.Scanner; /** * @author Xiao_Lin * @version 1.0 * @date 2020/12/19 11:31 */ public class TestException { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { System.out.println("请输入被除数:"); int num1 = scanner.nextInt(); System.out.println("请输入除数:"); int num2 = scanner.nextInt(); System.out.println(num1/num2); }catch (ArithmeticException e){//这里我们捕获的是除数异常,但是我们模拟的是InputMismatchException(输入匹配异常),所以程序2到这里并没有匹配到合适的异常中断程序 System.out.println("出现异常"); e.printStackTrace(); } } }
我们可以为 try 代码书写多个 catch 用于捕获多个具体的异常,但是要注意在书写的时候,子类在上,父类在下,因为Java的代码从上往下执行,没有合适的异常就用最大的异常来进行捕获。
package day15_exception.classing.exception; import java.util.InputMismatchException; import java.util.Scanner; /** * @author Xiao_Lin * @version 1.0 * @date 2020/12/19 11:31 */ public class TestException { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { int num1 = scanner.nextInt(); System.out.println("请输入除数:"); int num2 = scanner.nextInt(); int r = num1 / num2; System.out.println(r); }catch (InputMismatchException e) { System.out.println("输入数据有误"); }catch(ArithmeticException e) { System.out.println("除数不能为 0"); }catch(Exception e) { System.out.println("未知异常"); } System.out.println("程序正常结束!"); } }
try...catch 和之前一样用于捕获并处理异常,finally 代码块用于处理异常后的收尾工作。
不管是否发生异常,finally 总是执行,但是finally的工作仅仅只是包含释放内存、关闭文件、关闭网络连接、关闭数据库连接等收尾工作。
在finally中不建议书写代码和修改变量的值,因为finally无法修改临时堆栈的值
package day15_exception.classing.exception; import java.util.InputMismatchException; import java.util.Scanner; /** * @author Xiao_Lin * @version 1.0 * @date 2020/12/19 11:31 */ public class TestException { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { System.out.println("请输入被除数"); int num1 = scanner.nextInt(); System.out.println("请输入除数:"); int num2 = scanner.nextInt(); int r = num1 / num2; System.out.println(r); }catch (InputMismatchException e) { System.out.println("输入数据有误"); }catch(ArithmeticException e) { System.out.println("除数不能为 0"); }catch(Exception e) { System.out.println("未知异常"); }finally { System.out.println("我是finally"); } System.out.println("程序正常结束!"); } }
finally唯一不执行的情况是:JVM正常退出
package day15_exception.classing.exception; import java.util.InputMismatchException; import java.util.Scanner; /** * @author Xiao_Lin * @version 1.0 * @date 2020/12/19 11:31 */ public class TestException { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { System.out.println("请输入被除数"); int num1 = scanner.nextInt(); System.out.println("请输入除数:"); int num2 = scanner.nextInt(); int r = num1 / num2; System.out.println(r); } catch (Exception e) { System.out.println(e.toString()); // jvm 正常退出 System.exit(0); } finally { System.out.println("我是 finally"); } System.out.println("程序正常结束!"); } }
package day15_exception.classing.exception; import java.util.InputMismatchException; import java.util.Scanner; /** * @author Xiao_Lin * @version 1.0 * @date 2020/12/19 11:31 */ public class TestException { public static int div(int a, int b) { int r = 0; try { r = a / b; return r; } catch (Exception e) { System.out.println(e.toString()); } finally { System.out.println("我是 finally"); } return r; } public static void main(String[] args) { System.out.println(div(10, 0)); System.out.println("程序正常结束!"); } }
结论
快捷键
快速打出try...catch的快捷键
异常的继承体系,Throwable 类是 Java 语言中所有错误(Error)或异常的父类。
Error,表示代码运行时 JVM(Java 虚拟机)出现的问题。如系统崩溃或内存溢出等,不需要处理 Error,我们唯一能做的就是等待,在开发中极少见到,常见的有以下几个
Error类型 | 描述 |
---|---|
StackOverflowError | 当应用程序递归太深而发生堆栈溢出时,抛出该错误。比如死循环或者没有出口的递归调用 |
OutOfMemoryError | 因为内存溢出或没有可用的内存提供给垃圾回收器时,Java 虚拟机无法分配一个对象,这时抛出该错误。比如 new 了非常庞大数量的对象而没释放。 |
Exception
表示程序在运行时出现的一些不正常情况,一般大多数表示轻度到中度的问题,属于可预测、可恢复问题。如除数为 0,数组索引越界等,这种情况下,程序员通过合理的异常处理,确保程序的正常运行直到结束。
异常的构造方法
//构造一个异常信息为null的新异常 Exception(); //构造一个异常信息为 message的新异常 Exception(String message)
异常的分类
RuntimeException(运行时异常)是指在程序运行过程中出现的异常时可处理、可不处理的异常,他的父类是RuntimeException
运行时异常 | 描述 |
---|---|
InputMismatchException | 输入不匹配异常 |
ArithmeticException | 数学计算异常(比如除数为0) |
IndexOutOfBoundsException | 下标/索引越界异常 |
ArrayIndexOutOfBoundsException | 数组下标越界异常 |
StringIndexOutOfBoundsException | 字符串下标越界 |
NullPointerException | 空指针异常 |
IllegalArgumentException | 方法接收到非法参数 |
ClassCastException | 强制类型转换异常 |
NumberFormatException | 数字格式转换异常,如把"abc"转换成数字 |
检查时异常(Checked Exception):也称编译时异常,指在编译期间检查程序可能存在不正常的情况,在程序运行过程中必须处理,否则编译不通过。在 Java 中没有特定的父类,一般用
Exception 表示检查时异常。
检查时异常 | 描述 |
---|---|
ParseException | 格式(日期时间等)解析异常 |
ClassNotFoundException | class没找到异常 |
FileNotFoundException | 文件未找到异常 |
IOException | IO异常 |
SQLException | SQL异常 |
UnsupportedEncodingException | 不支持字符串编码异常 |
我们可以通过throws来声明某个方法可能出现的异常。
当方法的定义者在定义方法的时候不知道调用者在调用该方法的时候会出现异常,但是定义者又不知道如何处理时,此时可以选择使用throws关键字来声明异常,可以声明多个异常,用逗号分隔
[修饰符] 返回值类型 方法名(形参列表) throws 异常 1, 异常 2, 、、、{ }
声明异常时要根据异常的分类来确定是否外界(调用处)是否必须处理该方法产生的异常。如果需要外界必须处理,需要声明检查时异常,否则声明运行时异常。
package day15_exception.classing.exception; import java.util.InputMismatchException; import java.util.Scanner; /** * @author Xiao_Lin * @version 1.0 * @date 2020/12/19 11:31 */ public class TestException { public static int div(int a, int b) throws Exception{//抛出了检查时异常 int r = 0; r = a / b; return r; } public static void main(String[] args) { System.out.println(div(10, 0));//这里并没有解决异常,会报错,编译无法通过 System.out.println("程序正常结束!"); } }
public static void main(String[] args) {//改为这样才可以 try { System.out.println(div(10, 0)); } catch (Exception e) { e.printStackTrace(); } System.out.println("程序正常结束!"); }
无关系
如果父类声明运行时异常,子类可以声明运行时异常或者不声明;如果父类声明检查时异常,子类可以声明检查时异常或者不声明或者运行时异常。
如果父类没有声明任何异常,子类要么不声明任意异常,要么可以声明运行时异常,但不能声明检查时异常。
总结:子类方法的异常 <= 父类方法的异常
在实际开发过程中,如果调用 A 类 方法存在异常,调用者也不知如何处理这个异常时,可以继续向上声明异常,我们把这个过程称为异常的上抛。
在实际中遇到的异常能内部处理的优先处理,无法处理再上抛
public class Sub extends Parent{ @Override public void showInfo() throws Exception{ } }
public class Test01 { public static void main(String[] args) throws Exception{ Sub sub = new Sub(); sub.showInfo(); System.out.println("程序正常结束!"); } }
在实际开发过程中,开发者也可以根据程序的需要,手动抛出异常,通过 throw 关键字,程序员可以主动抛出某种特定类型的异常。
package com.kal02.exception07; public class Student { private String name; private String gender; public void setGender(String gender) throws RuntimeException{ if(gender.equals(Student.MAN)||gender.equals(Student.WOMAN)){ this.gender = gender; }else { // 手动抛出异常 throw new RuntimeException("性别只能是男或者女"); } } }
当 JDK 中的异常类型不能满足程序的需要时(也即需要定义具体的业务异常时),可以自定义异常。
自定义异常的步骤:
Student类
package day15_exception.classing.customize; /** * @author Xiao_Lin * @date 2020/12/19 17:25 */ public class Student { private String name; private String Gender; public Student(String name, String gender) { this.name = name; Gender = gender; } public Student() { } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getGender() { return Gender; } public void setGender(String gender) throws GenderException{ if (gender.equals("男") || gender.equals("女") || gender.equals("保密")){ Gender = gender; }else { throw new GenderException("性别不合法");//s } } }
自定义异常类
package day15_exception.classing.customize; /** * @author Xiao_Lin * @date 2020/12/19 17:27 */ public class GenderException extends Exception{ public GenderException() { } public GenderException(String message) { super(message); } }
测试类
package day15_exception.classing.customize; /** * @author Xiao_Lin * @date 2020/12/19 17:28 */ public class CustomizeException { public static void main(String[] args) { Student student = new Student("宿舍","钕"); try { student.setGender("宿舍"); } catch (GenderException e) { e.printStackTrace(); } } }
Java异常中throw和throws的区别
throws:
用来声明一个方法可能产生的所有异常,不做任何处理而是将异常往上传,谁调用我我就传给谁(甩锅)。
用在方法声明后面跟的是异常类名。
可以声明多个异常,用,隔开。
throws表示的是一种可能性,仅仅只是说可能会出现这种异常,并不是百分之百会出现这些异常。
throw: