12-异常,线程

涉及代码见github

第一章 异常

1.1 异常概念

· 异常:指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。

Java等面向对象的语言中,异常本身是一个类。产生异常就算创造一个异常对象,Java处理异常就是中断处理

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行。

1.2 异常体系

所有错误或者异常的超类是 java.lang.Throwable ,其下有两个子类:java.lang.Errorjava.lang.Exception ,平常所说的异常java.lang.Exception
· Exception:编译期异常,进行编译(写代码)Kava程序出现的问题
- RuntimeException:运行期异常
注:把异常处理掉,程序就可以继续执行。

· Error:错误。
注:错误必须修改源代码,程序才能继续执行。

例一 :编译期异常
SimpleDateFormat的parse()方法本身有编译期异常。处理方法:
alt+回车

  1. 把异常抛出,交给虚拟机来处理。方式:中断处理,中止程序把异常打印在控制台。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static void main(String[]args)throwsParseException{
    //Exception:编译期异常
    //格式化日期
    // 1. throwsParseException处理异常
    SimpleDateFormatsdf=newSimpleDateFormat("yyyy-MM-dd");
    //把字符串格式的日期解析为Date格式的日期
    /*
    正常情况
    Datedate=sdf.parse("1999-8-16");
    System.out.println(date);//MonAug1600:00:00CST1999
    */

    /*
    不正常情况
    */
    Datedate=sdf.parse("19998-16");
    System.out.println(date);//ParseException解析异常
    }

    缺点:正常执行,没有问题。如果模式或者字符串有问题有问题,会报解析异常

  2. 选择try-catch处理异常。
    优点:处理后,程序可以正常执行。如果异常,控制台会输出ParseException,不过也会打印后续代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //2.try-catch处理异常
    Datedate=null;
    try{
    date=sdf.parse("19998-16");
    }catch(ParseExceptione){
    e.printStackTrace();
    }
    System.out.println(date);
    System.out.println("后续代码");

例二:运行期异常

1
2
3
4
5
6
7
8
9
10
11
int[]arr={1,2,3};

//System.out.println(arr[3]);//ArrayIndexOutOfBoundsException(数组索引越界异常)
try{
//可能会出现异常的代码
System.out.println(arr[3]);
}catch(Exceptione){
//异常的处理逻辑
System.out.println(e);
}
System.out.println("后续代码");

输出:
java.lang.ArrayIndexOutOfBoundsException: 3
后续代码

例三:Error
必须修改代码。

1
2
//Error
int[]arr=newint[1024*1024*1024];//OutOfMemoryError内存溢出

1.4 异常产生过程解析

在这里插入图片描述

第二章 异常的处理

2.1 抛出异常throw

可以使用throw关键字在指定方法中抛出指定的异常
使用格式:throw new xxxException(“异常产生的原因”);
注意:

  1. throw必须写在方法内部

  2. throw后面的new的对象是Exception或者是Exception的子类对象

    1
    2
    3
    if(arr == null) {
    throw new NullPointerException("传递的数组的值为null");
    }

*空指针异常是一个运行期异常,我们不用处理,默认交给JVM处理。

  1. throw抛出指定的异常对象,我们就必须处理这个异常对象

-如果throw后创建的是RuntimeException或者是RuntimeException的子类对象,我们可以不处理,默认交给JVM处理(打印异常对象,中断程序)
-如果throw后创建的是编译异常,我们就必须处理这个异常,要么throws,要么try-catch

  1. 以后我们首先必须对方法传递过来的参数进行合法性校验。如果参数不合法,就必须使用抛出异常的方法,告知方法的调用者,传递的参数有问题。
    1
    2
    3
    If(index < 0 || index > arr.length - 1) {
    throw new ArrayOutOfBoundException("传递的数组超出了数组的范围");
    }

*ArrayOutOfBoundException同样是一个运行期异常

2.2 Objects非空判断

Objects类中的静态方法:(简化代码)
public static <T> T requireNonNull(T obj) :查看指定引用对象是不是null。
源码:

1
2
3
4
5
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}

· 在方法中直接使用Objects.requireNonNull(obj)来代替类似于该方法源码那样自己写throw来抛出参数为null的异常。
· 也可以Objects.requireNonNull(obj, "传递的对象的值是null"),自行添加message。

2.3 声明异常throws

throws关键字:异常处理的第一种方式,交给别人处理
作用:当方法内部抛出异常对象的时候,我们就必须处理这个异常的对象。可以使用throws来处理,会把异常对象声明抛出给方法的调用者处理,最终交给JVM处理(中断处理)
使用格式:方法声明时使用

1
2
3
4
5
修饰符 返回值类型 方法名(参数) throws  AAAException, BBBException…{ 
throw new AAAException("产生原因");
throw new BBBException("产生原因");

}

注意:

  1. throws必须写在方法声明处
  2. throws后面声明的异常必须是Exception或者是Exception的子类
  3. 方法内部如果抛出了多个异常对象 ,那么throws后必须也声明多个异常。
    【如果抛出的多个异常有子父类关系,只需要声明父类异常即可】
  4. 调用一个声明抛出异常的方法,我们就必须处理声明的异常。
  • 要么继续使用throws声明抛出,交给方法的调用者处理,最终交给JVM–>中断处理。
  • 要么try-catch,自己处理异常。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public static void main(String[] args) throws FileNotFoundException,IOException {  //交给JVM处理
    readFile("c:\\a.tx");
    System.out.println("后续代码");
    }

    /*
    定义一个方法,对传递的文件路径进行合法性判断
    */
    public static void readFile(String fileName) throws FileNotFoundException,IOException{
    if(!fileName.equals("c:\\a.txt")){
    throw new FileNotFoundException("传递的文件路径不是c:\\a.txt");
    }

    /*
    如果传递的路径,不是.txt结尾
    那么我们就抛出IO异常对象,告知方法的调用者,文件的后缀名不对

    */
    if(!fileName.endsWith(".txt")){
    throw new IOException("文件的后缀名不对");
    }

    System.out.println("路径没有问题,读取文件");
    }
    }

*FileNotFoundException是编译异常,抛出了编译异常,就必须处理这个异常
*可以使用throws继续声明抛出FileNotFoundException这个异常对象,让方法调用者处理

  • FileNotFoundException extends IOException extends Excepiton
    所以public static void readFile(String fileName) throws FileNotFoundException,IOException还可以用public static void main(String[] args) throws IOException或者public static void main(String[] args) throws Exception代替。

缺陷:如果有后续代码,无法执行。

2.4 捕获异常try-catch

try-catch:异常处理第二种方式,自己处理异常
格式:

try{
    **可能产生**异常的代码
}catch(定义一个异常的**变量**,用来**接收**try中抛出的异常对象){
    异常的处理逻辑,接收异常对象之后,怎么处理异常对象
    【一般在工作中,会把异常的信息记录到一个日志中】
}
...
catch(异常类名 变量名){

}

注意:
1. try中可能会抛出多个异常对象,那么就可以使用多个catch来处理这些异常对象
2. 如果try中产生了异常,那么就会执行catch中的异常处理逻辑, 执行完毕catch中的处理逻辑, 继续执行try…catch之后的代码;
如果try中没有产生异常,那么就不会执行catch中异常的处理逻辑,执行完try中的代码,继续执行try…catch之后的代码
调用方法输出异常信息

方法名 说明
void printStackTrace() 输出异常的堆栈信息
String getMesssage() 返回异常信息描述字符串,是printStackTrace()输出信息的一部分

常见的异常类型
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
publicstaticvoidmain(String[]args){
try {
//可能产生异常的代码
readFile("d:\\a.tx");
System.out.println("资源释放");
} catch(IOExceptione) { //try中抛出什么异常对象,catch就定义什么异常变量,用来接收这个异常对象
//异常的处理逻辑,接收异常对象之后,怎么处理异常对象
System.out.println("catch-传递的文件后缀不是.txt");
}
System.out.println("后续代码");
}
publicstaticvoidreadFile(StringfileName)throwsIOException{
if(!fileName.endsWith(".txt")){
throw newIOException("文件的后缀名不对");
}
System.out.println("路径没有问题,读取文件");
}

2.5 Throwable类中3个异常处理的方法

- `String getMessage()`返回此 throwable 的简短描述。
- `String toString()` 返回此 throwable 的详细消息字符串。
- `void printStackTrace()`  JVM打印异常对象,默认此方法, 打印的异常信息是最全面的

在上一个例子的catch中的异常处理逻辑使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
StringgetMessage()返回此throwable的简短描述。
StringtoString()返回此throwable的详细消息字符串。
voidprintStackTrace()JVM打印异常对象,默认此方法,打印的异常信息是最全面的
*/
//System.out.println(e.getMessage());//文件的后缀名不对
//System.out.println(e.toString());//重写Object类的toStringjava.io.IOException:文件的后缀名不对
//System.out.println(e);//java.io.IOException:文件的后缀名不对

//异常产生的位置、原因、内容
/*
java.io.IOException:文件的后缀名不对
atcom.itheima.demo02.Exception.Demo01TryCatch.readFile(Demo01TryCatch.java:55)
atcom.itheima.demo02.Exception.Demo01TryCatch.main(Demo01TryCatch.java:27)
*/
e.printStackTrace();

2.4 finally 代码块

格式:
try{
} catch() {

}

catch() {

} finally {
    无论是否出现异常都会执行
}

注意:
1. finally不能单独使用,必须和try一起使用
2. finally一般用于资源释放(资源回收),无论程序是否出现异常,最后都要资源释放(IO)

2.5 异常注意事项

多个异常使用捕获又该如何处理?

  1. 多个异常分别处理。(多个try-catch)
  2. 多个异常一次捕获,多次处理。(一个try,多个catch)
    注意事项:
  • catch里边定义的异常变量,如果有子父类关系,那么子类的异常变量必须写在上边,否则就会报错

“多态”:try中的子类异常也可以被父类异常变量catch到进行处理,所以下面的子类异常变量无法被使用,就会报错。

  1. 多个异常一次捕获一次处理。(catch的异常变量直接写为Exception e

*运行时异常被抛出可以不处理,即不捕获也不声明抛出。
默认给虚拟机处理,终止程序,什么时候不抛出运行时异常了,再来继续执行程序

*尽量不要在finally中写return,否则返回的总是finally中的return。

*子父类的异常:
子类重写父类方法时,

  1. 抛出和父类相同的异常
  2. 或者抛出父类异常的子类
  3. 或者不抛出异常
  4. 如果父类方法没有抛出异常,子类也不可抛出异常。
    此时子类产生该异常,只能捕获处理,不能声明抛出

第三章 自定义异常

3.1 概述

自定义异常类:
java提供的异常类, 不够我们使用, 需要自己定义一些异常类
格式:
public class XXXExcepiton extends Exception | RuntimeException{
添加一个空参数的构造方法
添加一个带异常信息的构造方法
}
** 注意**:

  1. 自定义异常类一般都是以Exception结尾,说明该类是一个异常类
  2. 自定义异常类,必须的继承Exception或者RuntimeException

-继承Exception:那么自定义的异常类就是一个编译期异常,如果方法内部抛出了编译期异常,就必须处理这个异常,要么throws,要么try…catch
-继承RuntimeException:那么自定义的异常类就是一个运行期异常,无需处理,交给虚拟机处理(中断处理)

3.2 自定义异常练习

Demo06MyException

第四章 多线程

4.1 并发与并行

并发:交替执行(单CPU系统)
并行:同时执行(多CPU系统)

4.2 进程与线程

· 进程:进入到内存中的应用程序。
进程也是程序的一次执行过程,是系统运行程序的基本单位

在这里插入图片描述

· 线程:进程的一个执行单元。
在这里插入图片描述

· 线程调度:
- 分时调度:所有线程轮流进行,均分时间
- 抢占式调度:优先让优先级高的线程使用 CPU,如果优先级相同,就随机。
注:
java使用的是抢占式调度;
win10设置优先级:任务管理器–>详细信息–>右键某应用程序–>选择设置优先级

对于CPU的一个核而言,某个时刻,只能执行一个线程。多线程程序并不能提高程序运行速度,但能提高程序运行效率

在这里插入图片描述

· 主线程:执行主方法(main())的线程

· 单线程程序:java程序中只有一个线程

  • 执行从main方法开始,从上到下依次执行。
    所以中间一旦出现异常,中止程序,后面代码不能执行。
    在这里插入图片描述

4.3 创建线程类(一)

1)创建多线程程序的第一种方式: 创建Thread类的子类
java.lang.Thread类: 是描述线程的类,我们想要实现多线程程序,就必须继承Thread类

· 实现步骤:

  1. 创建一个Thread类的子类
  2. 在Thread类的子类中重写Thread类中的run方法, 设置线程任务(开启线程要做什么)
  3. 创建Thread类的子类对象
  4. 调用Thread类中的方法start方法,开启新的线程,执行run方法

    void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
    1)结果是两个线程并发地运行;当前线程(main线程)和另一个线程(创建的新线程, 执行其 run 方法)。
    2)多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
    3)java程序属于抢占式调度,那个线程的优先级高,那个线程优先执行;同一个优先级,随机选择一个执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static void main(String[] args) {
    // 3.创建Thread类的子类对象
    MyTread mt = new MyTread();
    // 4.调用Thread类中的方法start方法,开启新的线程,执行run方法
    mt.start();

    for (int i = 0; i < 20; i++) {
    System.out.println("main"+i);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 1.创建一个Thread类的子类
    public class MyTread extends Thread{
    // 2.在Thread类的子类中重写Thread类中的run方法,设置线程任务
    @Override
    public void run() {
    for (int i = 0; i < 20; i++) {
    System.out.println("run"+i);
    }
    }
    }
    案例“随机打印结果”见GIthub:Demo07Thread

4.4 多线程原理

多线程程序_随机打印结果原理:
在这里插入图片描述

多线程程序_随机打印内存图解:
在这里插入图片描述

4.5 Thread类

4.5.1 常用方法

· 获取线程的名称两种方法:
1. 使用Thread类中的方法getName()
String getName() 返回该线程的名称。
2. 可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
static Thread currentThread() 返回对当前正在执行的线程对象的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在run方法中获取的是新线程的名称
// 方法一
String name = getName();
System.out.println(name); // Thread-0,……

//方法二
Thread t = Thread.currentThread();
System.out.println(t); // Thread[Thread-0,5,main]
String name = t.getName();
System.out.println(name); // Thread-1
//链式编程
System.out.println(Thread.currentThread().getName());
// 如果想获取主线程名称,就在主线程中编程。但不能使用方法一。

· 设置线程名称两种方法(了解):
1. 使用Thread类中的方法setName(名字)
void setName(String name) 改变线程名称,使之与参数 name 相同。
2. 创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给子线程起一个名字
Thread(String name)分配新的 Thread 对象。
e.g 在main方法中,开启多线程

1
2
3
4
5
6
//方法一
MyThread mt = new MyThread();
mt.setName("小强");
mt.start();
//方法二
new MyThread("旺财").start();
-  `public static void sleep(long millis)`: 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。<font color = #F9000>1000ms = 1s</font>
毫秒数结束之后,线程继续执行
方法有异常,可以用try-catch处理异常

4.6 Runnable

4.6.1 创建线程类(二)

2)创建多线程程序的第二种方式: 实现 java.lang.Runnable
接口
- Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法

- java.lang.Thread类的构造方法
    `Thread(Runnable target)` 分配新的 Thread 对象。
    `Thread(Runnable target, String name)` 分配新的 Thread 对象。

实现步骤:
1. 创建一个Runnable接口的实现类
2. 在实现类中重写Runnable接口的run方法,设置线程任务
3. 创建一个Runnable接口的实现类对象
4. 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
5. 调用Thread类中的start方法,开启新的线程执行run方法

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
// 3.创建一个Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
// 4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t = new Thread(run);
// 5.调用Thread类中的start方法,开启新的线程执行run方法
t.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
1
2
3
4
5
6
7
8
9
public class RunnableImpl implements Runnable{
// 2.在实现类中重写Runnable接口的run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}

上述案例见GitHub:Demo08Runnable

4.6.2 Tread和Runnable区别

实现Runnable接口创建多线程程序的好处:
1. 避免了单继承的局限性
一个类只能继承一个类,类继承了Thread类就不能继承其他的类
实现了Runnable接口,还可以继承其他的类,实现其他的接口
2. 增强了程序的扩展性,降低了程序的耦合性(解耦)
实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
· 实现类中,重写了run方法:用来设置线程任务
· 创建Thread类对象,调用start方法:用来开启新线程

4.7.3 匿名内部类方式实现线程创建

匿名内部类作用: 简化代码
- 把子类继承父类重写父类的方法创建子类对象合为一步完成
- 把实现类实现类接口重写接口中的方法创建实现类对象合成一步完成
匿名内部类的最终产物: 子类/实现类对象, 而这个类没有名字

格式:

1
2
3
new 父类/接口() {
重复父类/接口中的方法
};

e.g

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 线程的父类是Thread
// new MyThread().start();
new Thread(){
//重写run方法,设置线程任务
@Override
public void run() {
···
}
}.start();

//线程的接口Runnable
//Runnable r = new RunnableImpl(); // 多态
Runnable r = new Runnable(){
//重写run方法,设置线程任务
@Override
public void run() {
···
}
};
new Thread(r).start();

//简化接口的方式
new Thread(new Runnable(){
//重写run方法,设置线程任务
@Override
public void run() {
···
}
}).start();
}