【JavaSE 第十五天】
一、 异常
异常的知识点不好理解,主要目的记住使用格式。
1. try…catch 异常处理
try catch的异常处理的格式写法:
try{
被检测的代码
可能发生异常的代码
}catch(异常类的类名 变量名){
异常的处理方式:写什么都可以
定义变量,创建对象,调用方法,循环,判断...
只要写了catch,异常就被处理掉了
}
测试:
public static void main(String[] args) {
int[] arr = {1};
// try catch异常处理,将可能发生异常的代码写入 try catch 之中
try {
int i = getNum(arr);
System.out.println("i = " + i);
}catch (Exception ex){
System.out.println("异常被处理掉");
}
// 加上 try...catch 之后其中的异常就会被处理,使得之后的语句可以继续执行
System.out.println("异常之后的的代码");
}
public static int getNum(int[] arr){
return arr[1] + 10;
}
try…catch 异常处理,处理机制:
① arr[1] 索引不存在,不能计算,JVM 检测到索引的问题,JVM 创建了一个对象
② new ArrayIndexOutOfBoundsException(1) 将索引 1 传入
③ try 检测到有异常对象,异常对象继续抛出,抛到 catch 中
④ catch 捕获异常,相当于进行了Exception ex = new ArrayIndexOutOfBoundsException(1)
(Exception 是父类,ArrayIndexOutOfBoundsException 是子类,体现了多态性)
⑤ 异常被处理掉(异常是由 catch 处理的,没有再次返回到 JVM 中),继续向下执行代码
Throwable 类的,异常信息的处理方法的测试:
public static void main(String[] args) {
int[] arr = {1};
// try catch异常处理,将可能发生异常的代码写入 try catch 之中
try {
int i = getNum(arr);
System.out.println("i = " + i);
}catch (Exception ex){
// Throwable 类的,异常信息的处理方法
String message=ex.getMessage();
System.out.println("message = " + message); // message = 1 这里的 1 就是越界异常的下标
String str=ex.toString();
System.out.println("str = " + str); // str = java.lang.ArrayIndexOutOfBoundsException: 1
/**
* 控制台输出:出现问题的异常类的类,错误的原因,发生问题的的代码行数
* java.lang.ArrayIndexOutOfBoundsException: 1
* at com.xxxxxxx.object.ExceptionTest.getNum(ExceptionTest.java:23)
* at com.xxxxxxx.object.ExceptionTest.main(ExceptionTest.java:8)
*/
ex.printStackTrace();
}
// 加上 try...catch 之后其中的异常就会被处理,使得之后的语句可以继续执行
System.out.println("异常之后的的代码");
}
public static int getNum(int[] arr){
return arr[1] + 10;
}
2. 多 catch 并行处理
异常处理的代码中:try 可以跟随多个 catch
好处:不同的异常,可以区别对待,分开处理
public static void main(String[] args) {
/**
* myException 中出现2个异常
* 写2个 catch 分别捕获异常
*/
try {
myException(0); // 传递一是 处理空指针异常 传递零是 处理越界异常
}catch (NullPointerException ex){
System.out.println("处理空指针异常");
}catch (ArrayIndexOutOfBoundsException ex){
System.out.println("处理越界异常");
}
}
/**
* 多 catch 并行处理
* Exception 顶级父类
* Exception的子类: ①NullPointerException
* ②IndexOutOfBoundsException
* ②的子类: ArrayIndexOutOfBoundsException
*/
public static void myException(int i){
/**
* 定义方法,目的是引发异常
* 传递参数:对参数进行判断
*/
if(i==0){
// 引发空指针异常
String s=null;
int len=s.length();
}else{
// 引发越界异常
int[] arr={};
int a=arr[0];
}
}
多个catch处理异常的时候,写法特别注意:如果 catch 中的异常类没有继承关系,先写后写没有区别。 catch 中的异常类有继承关系时,父类写在最下面(如果父类写在上面,由于多态性,子类就无法捕获异常,成为多余的代码)
3. throw 和 throws 关键字的使用
- throw 关键字:只能写在方法内部,关键字的后面跟随对象的创建
- throws 关键字:只能写在方法的定义上,关键字后面跟随异常类名
(内部有问题就用 throw,但是外界不知道该问题,对外声明暴露问题就需要 throws)
/**
* throw 和 throws 关键字的使用
*/
public static void main(String[] args) throws Exception {
/**
* getArea() 调用方法,方法上有异常
* 只能处理,否则就会编译失败
* 两种处理方式:
* 1.在 main 方法上加 throws 异常没有处理,交给了 JVM 处理
* 2.try...catch 处理
*/ // 2.try...catch 处理,可以这样写
int area=getArea(-10); // try {
// 但是当传递 -10 时,仍是可以计算但是违反了实际,所以要在方法中加入校验 // int area = getArea(-10);
System.out.println("area = " + area); // System.out.println("area = " + area);
} // } catch (Exception e) {
/** // e.printStackTrace();
* 功能:计算正方形的面积,需要参数:边长 // }
* 语法:方法内部如果出现了异常,必须在方法定义上暴露 // 推荐使用 try...catch 处理
*/
public static int getArea(int length) throws Exception {
if(length<=0){ // 数据错误,导致后面的计算无法进行
// 内部出现问题
throw new Exception("边长不存在");
}
return length*length;
}
4. finally 代码块
finally 代码块跟随 try … catch 使用,也可以跟随 try 使用
finally 代码块里面的程序,无论是否出现异常,都会执行,必须执行(但是结束 JVM 后,finally 就不会执行)
finally 代码块后期主要用于释放资源
测试:
public static void main(String[] args) {
try {
int[] arr = {1};
System.out.println(arr[1]);
}catch (Exception ex){ // 捕获异常
ex.printStackTrace();
}finally {
// 后期用于资源的释放
System.out.println("这里的代码,必须执行");
}
}
找茬问题①:
public static void main(String[] args) {
int x=getNum();
System.out.println("x = " + x);
}
public static int getNum(){
try{
return 1;
}catch (Exception ex){
return 2;
}finally {
return 3;
}
// 方法一旦遇到 return 就会结束,但是 finally 一定要执行,所以 return 3; 就会覆盖 return 1;
}
找茬问题②:
public static void main(String[] args) {
int x=getNum();
System.out.println("x = " + x);
}
public static int getNum(){
int a=1;
try{
return a; // 程序运行起来 a 就不存在了,就不能再进行运算,所以在这里 JVM 已经决定返回 "1" 了,所以不会再更改
}catch (Exception ex){
return a;
}finally {
++a; // a 在这里变为 2,但是在前面已经决定返回 "1" 了
}
}
// 最终结果就是 "1"
5. RuntimeException 异常
异常的父类是 Exception,Exception 类的子类有 RuntimeException,凡是 RuntimeException 和他的所有子类,都称为运行异常,RuntimeException 和的它的子类之外的则都称为编译异常
- 编译异常:方法出现编译异常,调用者必须处理,否则编译失败。处理方式可以是 try…catch 或者是 throws
- 运行异常:方法出现运行异常,在方法的定义上,不需要 throws 声明,调用者也不需要处理这个异常
建议不要处理运行异常:程序一旦发生运行异常,请程序人员修改源码
- 常见的运行异常:
NullPointerException
空指针IndexOutOfBoundsException
越界异常ClassCastException
类型强制转换IllegalArgumentException
无效的参数异常
6.自定义异常
Java 官方已经定义了大量的异常类,但是依然不够,以后做项目的时候,会出现的异常,在 JDK 中没有定义的,需要我们自己定义异常
- 自定义异常,需要继承 Exception 或者 RuntimeException
- 只有 Exception 和他的子类,才具有可抛出性
- 在自定义的类中,构造方法,super 调用父类构造方法,传递异常信息
自定义的异常类:
/**
* 自定义的 异常类
* 成绩负数的异常
* 选择继承的父类
* 需要自定义信息:继承的父类 RuntimeException 带有 String 类型的构造方法 (String 异常信息)
*/
public class ScoreException extends RuntimeException{
public ScoreException(String s){
super(s); // 自定义的异常不要写死错误,用构造器传递参数
}
}
测试类:
public class ScoreTest {
public static void main(String[] args) {
int avg=getAvg(100,-1);
System.out.println("avg = " + avg);
}
/**
* 计算成绩的平均分
*/
public static int getAvg(int math,int chinese){
// 判断成绩是否合法
if(math<0||chinese<0){
// 手动抛出自定义的异常
throw new ScoreException("成绩不合法!");
}
return (math+chinese)/2;
}
}
二、集合框架
1.集合框架由来
JDK 1.2 版本后,出现集合框架,到 JDK1.5 后,大幅度优化。
①集合本质上是存储对象的容器
②数组也能存储对象,数组的弊端就是定长(限制长度)
③为了解决数组的问题,开发出来集合框架,集合框架无需考虑长度
- 集合和数组的区别与共同点:
- 集合,数组都是容器,都可以存储数据
- 集合只存储引用数据类型,不存储基本数据类型
- 数组可以存储基本类型,也可以存储引用类型
- 数组定长,集合容器可以变长
牢记:数据多了存数组,对象多了存集合
- 集合学习的关键点:
- 怎么存储数据
- 怎么取出数据
- 选择哪种容器
2.集合框架的继承体系
(1)第一个派系:
Collection (集合)
顶级接口 单列集合(一次只能存一个对象)List (列表)
接口ArrayList (数组列表)
实现类LinkedList (链表)
实现类Vector (数组列表)
实现类,已过时
Set (集合)
接口HashSet (哈希表)
实现类LinkedHashSet (链表哈希表)
实现类,继承自HashSet
TreeSet (红黑树)
实现类
(2)第二个派系:
Map (映射键值对)
顶级接口 双列集合(一次可以存两个对象)HashMap (哈希表)
实现类LinkedHashMap (链表哈希表)
实现类,继承自HashMap
TreeMap (红黑树)
实现类Hashtable (哈希表)
实现类,已过时Properties (哈希表)
实现类,继承自Hashtable
ConCurrentHashMap (哈希表)
线程相关
(3)存在一个独立的接口
Iterator
迭代器接口(用来做遍历)
(4)泛型
- 泛型
Generic
- 写法
- 泛型类 ,泛型方法,泛型接口,泛型限定,泛型通配符
(5)增强型循环
for(:)
循环
3.Collection 接口
是所有单列集合的顶级接口,任何单列集合都是他的子接口或者是实现类,该接口中定义的方法,是所有单列集合的共性方法。
使用接口 Collection 的实现类 ArrayList
,创建对象。
Collection<E>
尖括号就是泛型,(这是 API 文档中的写法)自己编程时 E 处要写,集合存储的数据类型
(1) Collection 接口的常用方法
方法的定义 | 方法作用 |
---|---|
boolean add(E) | 元素添加到集合 |
void clear() | 清空集合容器中的元素 |
boolean contains(E) | 判断元素是否在集合中 |
boolean isEmpty() | 判断集合的长度是不是0,如果是0返回 true |
int size() | 返回集合的长度,集合中元素的个数 |
boolean remove(E) | 移除集合中指定的元素,移除成功返回 true |
T[ ] toArray(T[ ] a) | 集合转成数组 |
Collection 接口的常用方法,使用ArrayList
实现类创建对象:
①boolean add(E)
/**
* boolean add(E) 元素添加到集合中
* 返回值,目前都是true
*/
public static void collectionAdd(){
// 建议使用接口多态创建集合容器对象,存储的数据类型是字符串
Collection<String> coll = new ArrayList<>(); // 后面尖括号中的数据类型可以不写,但是如果要写前后的数据类型必须一致
// 注意:集合不能存储基本数据类型
// 集合对象的方法 add 添加元素
coll.add("hello");
coll.add("world");
coll.add("java");
coll.add("apple");
coll.add("banana");
/**
* 输出语句中,输出集合对象,调用的是方法 toString()
* 看到的内容是一个完整的字符串,这种操作不叫遍历
*/
System.out.println(coll);
}
②void clear(), int size(), boolean isEmpty()
/**
* void clear() 清空集合中的所有元素
* int size() 集合的长度
*/
public static void collectionClear(){
Collection<Integer> coll = new ArrayList<>(); // 可以写包装类,但是不能写 int
coll.add(1);
coll.add(2);
coll.add(3); // 具备自动装箱的操作
System.out.println(coll);
System.out.println("集合的长度::"+ coll.size()); //长度
coll.clear();
System.out.println(coll); // 里面的元素被清空了,但是集合本身仍然存在
System.out.println("集合的长度::"+ coll.size());
System.out.println("集合是空吗?" + coll.isEmpty()); //长度=0,isEmpty() 返回 true
}
③boolean contains(), boolean remove()
/**
* boolean contains(E) 判断是否包含
* boolean remove(E) 移除元素
*/
public static void collectionContains(){
// 接口多态创建集合容器对象,存储的数据类型是字符串
Collection<String> coll = new ArrayList<>();
// 集合对象的方法 add 添加元素
coll.add("hello");
coll.add("wife");
coll.add("world");
coll.add("java");
coll.add("apple");
coll.add("banana");
// 判断集合中是否包含某个元素
boolean b = coll.contains("world");
System.out.println("b = " + b);
// 移除集合中的元素
// 删除成功返回 true,如果有多个相同的对象,删除最先遇到的那个
boolean b1 = coll.remove("banana"); // 写一个本来就没有的元素,删不掉就是 false
System.out.println("b1 = " + b1);
System.out.println(coll);
}
4. Iterator 接口
迭代器接口 Iterator ,为集合进行遍历的,迭代器技术是所有 Collection 集合的通用遍历形式。
(1)Iterator 接口的抽象方法
boolean hasNext()
判断集合中是否有下一个可以遍历的元素,如果有,就返回 trueE next()
获取集合中下一个元素(集合存的是什么,“E” 就是什么)void remove()
移除遍历到的元素
(2)获取迭代器接口实现类
迭代器就是为了遍历集合而产生,集合的顶层接口 Collection 中定义了方法:方法的名字就是
iterator()
,返回值是 Iterator 接口类型,准确来说返回的是 Iterator 接口实现类的对象
Collection 接口中的方法摘要:
public Iterator iterator();
返回迭代器接口实现类的对象
使用的对象 ArrayList ,它会实现接口 Collection,重写方法 iterator();
(new 对象 new 的是 ArrayList, iterator()
是写在 ArrayList 的接口里面的,所以要进行方法重写)
public static void main(String[] args) {
// 迭代器遍历集合
Collection<String> coll = new ArrayList<>();
coll.add("hello");
coll.add("world");
coll.add("java");
coll.add("apple");
coll.add("banana");
// ①遍历 集合对象,调用方法 iterator() 获取迭代器接口的实现类对象
Iterator<String> it = coll.iterator(); // 集合存什么,遍历时的数据类型就是什么,迭代器的泛型跟随集合
// ②迭代器对象的方法,判断集合是否有下一个元素
boolean b = it.hasNext();
System.out.println(b); // 如果有就会返回 true
// ③迭代器对象的方法,取出元素
String str = it.next();
System.out.println(str);
// ② 与 ③ 的过程应该是反复执行的所以,使用循环:第②步与第③步可以缩减
// 条件,集合中有下一个元素就可以
while ( it.hasNext() ){
String str = it.next();
System.out.println(str);
}
// 这种方式适用于所有的单列集合
}
迭代器在每一次运行中只可以使用一次
解释:
迭代器的运行可以看作是指针,开始指向索引 -1 位置,运行循环过程时,指针一直向后移动,最后停留在最后一个元素上,迭代器停止运行,程序结束。迭代器使用结束,如果需要再次使用迭代器,必须再次重新调用方法产生一个迭代器。
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("hello");
coll.add("world");
coll.add("java");
coll.add("apple");
coll.add("banana");
Iterator<String> it = coll.iterator();
while ( it.hasNext() ){
String str = it.next();
System.out.println(str);
}
// 可以使用 for 循环调用方法产生新的迭代器重新遍历
for(Iterator<String> its= coll.iterator(); its.hasNext(); System.out.println(its.next()));
// 这种 for 循环的写法可以节约内存,变量写在 for 之中,for 循环一旦结束,对象就会消失。
}
(3)迭代器的实现原理
每个集合容器,内部结构不同,但是迭代器都可以进行统一的遍历实现
结论:迭代器是隐藏在集合的内部的, 提供公共的访问方式: Iterator 接口
伪代码:
interface Iterator{
// 定义一个接口
boolean hasNext();
E next();
void remove();
}
public class ArrayList {
public Iterator iterator(){ // 权限是公共的,返回值是 Iterator 接口类型
return new Itr(); // 拿到内部类的对象,内部类实现了该接口,再使用内部类调用三个方法
}
private class Itr implements Iterator{
// 私有的成员,外部要调用必须要有 get/set 方法
// 要实现接口,必须重写方法
boolean hasNext(); //重写,要加入权限和主体
E next(); //重写,要加入权限和主体
void remove(); //重写,要加入权限和主体
}
}
(4)并发修改异常(属于运行异常)
异常的产生原因:在迭代器遍历集合的过程中,使用了集合的功能,改变了集合的长度造成的并发修改异常。
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("hello");
coll.add("world");
coll.add("java");
coll.add("apple");
coll.add("banana");
// 迭代器遍历集合,首先获得接口的实现类
Iterator<String> it = coll.iterator();
while ( it.hasNext() ){
String str = it.next(); // 取出元素
// 判断,遍历到的集合元素是不是 java
if (str.equals("java")){
// 故意添加元素 出现并发修改异常
coll.add("add");
// 用集合的方法 add 改变了集合的长度,一旦改变长度就会出现并发修改异常
// 但是有一个特殊情况,可以删除倒数第二个元素,不会报并发修改异常
// 原因:遍历到倒数第二个元素时,将它已经删除导致循环无法进入,循环停止,it.next(); 就不会执行,就不会报错
// 所以建议做遍历时就不要动集合中的元素
}
System.out.println(str);
}
}
(5)集合存储自定义对象并迭代
public static void main(String[] args) {
// 创建集合,存储自定义的对象
Collection<Person> coll = new ArrayList<>();
// 集合的方法 add 存储 Person 对象
coll.add( new Person("张三",21) ); // 匿名对象存储
coll.add( new Person("李四",22) );
coll.add( new Person("王五",23) );
// 迭代器遍历集合
Iterator<Person> iterator = coll.iterator();
while (iterator.hasNext()){
Person person = iterator.next();
System.out.println(person); // 打印对象需要调用 toString() 方法,否则返回哈希值地址
System.out.println(person.getName()); // 单独打印姓名,调用方法即可
}
}
建议重新创建一个包,并把 Person 类放入其中(简单的 Java 对象放在一起):
/**
* ①定义私有成员
* ② get set方法
* ③无参数构造方法(一定要有)
*
* 满足以上的三个条件,这个类,就可以换一个名字,叫作 JavaBean
*/
public class Person {
private String name;
private int age;
// 无参构造器
public Person(){}
// 有参构造器
public Person(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 "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- ①定义私有成员
- ②拥有 get set方法
- ③无参数构造方法(一定要有)
满足以上的三个条件,这个类就可以换一个名字,叫作 JavaBean
5. Collection 下的 List 接口
List 接口,继承 Collection 接口,是单列集合
(1)List 接口的特点
- 这个接口的集合都具有索引
- 这个接口中的元素允许重复
- 这个接口中的元素是有序的
- 元素不会排序,有序指的是:元素存储和取出的顺序是一致的
List 接口的所有实现类,都具有以上三个特征
(与 List 接口并列存在的 Set 接口,基本上特点是相反的,Set 接口的集合没有索引,元素不允许重复,接口中的元素根据实际有些有序有些无序。)
(2)List 接口自己(独有)的方法 (带有索引)
①add(int index ,E e)
/**
* List接口的方法 add(int index, E e)
* 指定的索引位置,添加元素
*
* IndexOutOfBoundsException (继承自RuntimeException 异常) 集合越界异常 集合的长度是size()
* 子类:①StringIndexOutOfBoundsException 字符串越界异常 字符串的长度是 length()
* ②ArrayIndexOutOfBoundsException 数组越界异常 数组的长度是 length
*/
public static void listAdd(){
List<String> list = new ArrayList<>();
list.add("a") ; // 这样是在集合的尾部添加
list.add("b");
list.add("c");
list.add("d");
list.add("e");
System.out.println(list); // 按照输入的顺序输出
// 指定的索引上,添加元素,3 索引上添加元素
list.add(3,"QQ"); // 如果超出本来的索引数就会出现异常
System.out.println(list); // 添加后按照索引,依次输出
}
②get(int index)
/**
* List 接口的方法 E get(int index)
* 返回指定索引上的元素
* List 集合可以使用 for 循环像数组一样的方式遍历
*/
public static void listGet(){
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
// List 接口方法 get 取出元素
// String s = list.get(3);
// System.out.println(s);
for(int i = 0 ; i < list.size() ; i++){
System.out.println(list.get(i));
// 已经有了索引后,就和数组的遍历一样
}
}
③set(int index,E e),remove(int index)
/**
* List接口方法
* E set (int index , E e) 修改指定索引上的元素,返回被修改之前的元素
* E remove(int index) 移除指定索引上的元素,返回被移除之前的元素
*/
public static void listSetRemove(){
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
System.out.println(list);
// 修改指定索引上的元素,修改 3 索引
String str = list.set(3,"https://www.baidu.com");
System.out.println(list);
System.out.println(str);
// 删除指定索引上的元素,删除 3 索引
str = list.remove(3);
System.out.println(list);
System.out.println(str);
}
(3)List 集合的特有迭代器
List 接口中的方法 listIterator()
返回迭代器,迭代器的接口是 ListIterator ,集合的专用迭代器
- ListIterator 迭代器接口的方法:
boolean hasNext()
E next()
boolean hasPrevious()
判断集合中是否有上一个元素,反向遍历E previous()
取出集合的上一个元素
/**
* List 接口的方法:
* listIterator() List 集合的特有迭代器
* 反向遍历
*/
public static void iterator(){
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
// 获取特有迭代器接口实现类对象
ListIterator<String> lit = list.listIterator();
// 由于集合遍历中的指针只有一个,需要让指针到最后位置,所以先要正向遍历
while (lit.hasNext()){
String s = lit.next();
System.out.println(s);
}
// 反向遍历
// 判断上一个元素
while (lit.hasPrevious()){
//取出元素
String s = lit.previous();
System.out.println(s);
}
// 反向遍历并不实用
}
6. List 接口的实现类 ArrayList
ArrayList 集合,其内部定义了一个数组,数组的名字叫做: elementDate ,默认初始化长度为 10,一旦超过长度 10,长度就会翻1.5倍,拷贝原数组中的值进入新数组,从而扔掉原数组,但是不能减容量,如果要删除中间的某个元素,就会把之后的元素复制粘贴前进到新的位置,但是它的数组长度不是元素存储的长度(10),而是其中所包含的的元素个数 ArrayList()
一旦被创建(new),就会构造一个初始容量为十的空列表,其中有一个计数器,一旦通过该类调用 size
计数器方法就会返回输入元素的个数
7. List 接口的实现类的数据结构
链表结构
(存储数据时,都会放入三个节点中间的地方,起初的节点数据为 null ,要在之后再次输入数据就会在后一个节点中输入下一个元素的地址,再次在新地址中重复操作,存储到最后的节点数据也为 null,删除其中的一个数据,大体结构不会变化,就让要删除元素的上一个节点存储要删除元素的下一个的对象地址。)
数组与链表的对比:
- 数组:
- 有索引,数组中元素的地址是连续,查询速度快
- 数组的长度为固定的,实现扩容需要新创建数组,要通过数组元素的复制,增加或者删除堆内存中的数据导致效率慢
- 链表:
- 链表没有索引,采用对象之间内存地址记录的方式存储
- 查询元素,必须通过第一个节点依次查询,查询性能慢
- 增删元素,不会改变原有链表的结构,速度比较快