异常的定义、声明和处理以及相关的关键字,线程(Thread类,Runnable接口)的声明和API(十五)

229 阅读18分钟

异常

Java中的错误情况:

​ 语法错误:编译肯定不通过,必须照着笔记查看是违反什么语法,修正之后才能编译通过。

​ 逻辑错误:编译可以通过,但是运行得不到正确的结果。

​ 异常:有的编译可以通过。但是程序运行期间,发生异常,导致程序中断/崩溃;有的是编译不通过,会提醒我们可能会发生xx异常,我们必须做出处理,如果不处理,编译不通过;如果处理了,编译通过,如果处理得当,程序发生异常后,还是可以正常运行;否则的话程序发生异常后,还会崩溃。

1.什么是异常

程序执行过程中,发生的非正常情况。Java的语法错误和逻辑错误不在异常讨论范围内。

2.Java中如何表现异常和处理异常?

(1)Java中一切皆对象,在这里也有体现。

Java会给每一种“异常”情况,用一种类型来描述,即一种异常对应一个类。

例如:空指针异常==》NullPointerException

​ 数组下标越界==》ArrayIndexOutOfBoundsException

​ 类型转换异常==》ClassCastException

​ 算数异常==》ArithmeticException

​ ...

(2)当某句代码发生异常时,JVM会根据当前的情况,判定是属于哪种异常,并且会给我们new一个异常的对象,“抛”出来。

(3)如果在发生异常的代码的外围有try..catch这中结构,那么JVM会继续判断try..catch结构是否可以捕获,如果可以捕获,程序就不会挂掉,否则程序依然会挂掉。

​ 如果在发生异常的代码外,没有try..catch,那么程序肯定会挂掉。

异常体系

1.如果仅从类的角度来说,根父类仍然是Object,如果从异常的角度来说,根异常类型是java.lang.Throwable类。

(1)只有作为此类(或其一个子类)的实例的对象由Java虚拟机抛出,或者可以由Java throw语句抛出。

(2)只有此类或其子类之一才可以是catch子句中的参数类型

2.两个子类的实例,Error和Exception,通常用于指示发生了异常情况。即Java的异常可以分为两大类:

(1)java.lang.Error:用于表示合理的应用程序不应该试图捕获的严重问题。

​ 例如:Virtual MachineError(虚拟机错误),其中一个子类StackOverflowError(栈内存溢出):在使用递归时可能会发生。另一个子类OutOfMemoryError(简称OOM,堆内存溢出),因为GC回收对象的内存基本上都是在堆中。

(2)Exception:指出了合理的程序应用程序想要捕获的条件。

3.Exception又可以划分为:

(1)编译时异常(又称为受检异常):除了运行时异常,剩下都是编译时异常。

​ 在编译期间,编译器会提示程序员某段代码可能发生异常,但是不表示一定会发生,需要程序员必须提前写好处理异常的代码,否则编译不通过。

(2)运行时异常(又称为非受检异常):RuntimeException及其子类。

​ 空指针异常==》Null Pointer Exception

​ 数组下标越界==》ArrayIndexOutOfBoundsException

​ 类型转换异常==》ClassCastException

​ 算数异常==》ArithmeticException

这些异常都是RuntimeException异常的子类。在编译期间,完全感受不到,在程序运行时可能发生。运行时异常,大多数都需要靠程序员“自觉”通过代码去避免,

例如:类型转换之前,通过instanceof判断后再向下转型;在两个整数相除之前,要考虑除数是不是为0.

注意:

无论是编译时异常还是运行时异常,一旦发生,只要没有合理处理,都会导致程序中断/崩溃。

异常处理

1.try...catch

(1)语句结构

try{
	可能发生异常的代码;
}catch(异常类型1 e){
	处理异常的代码;
}catch(异常类型2 e){
	处理异常的代码;
}...

2.说明

(1)catch中的{}中处理异常的代码:

①仅仅是打印,做简单程序时是在控制台,做项目时,记录到日志。

②必须编写对应的逻辑处理。

(2)catch可能有多个

①如果有多个,判断的顺序是从上往下

②如果上面的某个catch()中的类型与try中发生异常的类型“匹配”,那么下面的catch就不看了,即多个catch只会执行其中一个。

③如果有多个异常,多个异常的类型之间如果有包含关系,必须子类在上,父类在下。

④虽然编写了多个的catch,但是仍然存在一个catch都匹配不上的情况。

(3)当某些代码需要加try..catch进行异常处理时,可以选中这些代码,然后按快捷键Ctrl+Alt+T,然后选择try..catch。

3.异常信息打印

(1)标准的异常打印语句(JSE期间推荐)

e.printStackTrace():打印异常对象的堆栈跟踪记录,就是这个异常对象经过哪些调用。

(2)普通的打印语句

System.out.println(xx);当成普通对象打印

(3)普通的错误打印

System.error.println(xx);当成错误对象打印,内容跟(2)相同,颜色不同。

finally块

1.finally块它必须和try或try..catch一起使用,不能单独使用

2.语法格式

(1)格式一

try{

}finally{

}
//说明:try中发生的话,它不处理异常,把异常抛给使用者

(2)格式二

try{
	可能发生异常的代码;
}catch(异常类型1 e){
	处理异常的代码;
}catch(异常类型2 e){
	处理异常的代码;
}...finally{

}

3.finally块有什么用?

(1)无论try中是否发生异常

(2)无论catch是否存在,或者说,存在的话,是否可以捕获异常

(3)无论try...catch中是否有return语句,finally块都要执行。

4.放什么进去?

通常是资源代码释放,例如:xxx.close()。

5.在什么情况下finally不执行?

在try或catch中执行了System.exit(0),直接退出虚拟机,就不会执行finally中的代码块了。

6.finally和return

要点:

(1)无论try或catch中是否有return,finally都要执行。(finally的return会让try和catch中的return失效,开发时千万不要出现这种情况)

throws声明异常

1. throws用在哪里?

​ 在使用方法声明时

//非抽象方法
【修饰符】 返回值类型 方法名(【形参列表】)【throws 异常列表】{
//...
}


//抽象方法或native方法
【修饰符】 返回值类型 方法名(【形参列表】)【throws 异常列表】;

2.为什么使用?

通常情况下,我们会在可能发生异常的代码“外围”用try...catch进行异常的检测和处理。但是有的时候,在该代码位置不适合try...catch处理异常,而是应该由上级(调用者)来处理,那么这个时候,就需要吧可能发生的异常类型告诉调用者这个方法可能发生xx异常需要注意并处理。如果调用者也不处理,那么就会导致程序崩溃。

3.throws后面可以写几个异常?

(1)throws后面可以写多个异常,并且多个异常的顺序无所谓

(2)如果throws后面又多个异常,并且多个异常之间有包含关系的话,可以并列存在,也没有顺序要求,通常这种情况我们保留父类就可以了。

4.throws异常列表在重写时,有什么要求?

方法重写的要求:

(1)方法名:必须相同

(2)形参列表:必须相同

(3)返回值类型

​ 基本数据类型和void都必须相同

​ 引用数据类型:<=被重写的方法

(4)权限修饰符:>=被重写的方法的权限修饰符

​ 注意:被重写的方法不能private,跨包重写不能时缺省

(5)其他修饰符:被重写方法不能使static和final

(6)以下:

​ ①如果被重写方法没有throws编译时异常,那么重写方法时,就不能throws编译时异常

​ ②如果被重写方法throws编译时异常,那么重写方法时,只能throws该异常类型或该异常类型的子类。<=

​ ③如果被重写方法throws运行时异常,子类重写时,可以一样,可以不写。(无关

​ ④如果被重写方法没有throws运行时异常,子类重写时,可以一样,可以throws自己的运行时异常。(无关

​ 结论:重写时throws后只看编译时类型。

两同两小一大四不能(补充笔记(十))

(1)方法的重写(override)

​ 当子类继承了父类的成员方法后,如果父类的方法体实现不适合子类,那么子类可以选择重写。

(2)方法的重写有要求

①方法名必须相同

②形参列表必须相同

③返回值类型:

​ Ⅰ:父类方法的返回值类型是void和基本数据类型,那么子类方法必须与父类相同。

​ Ⅱ:父类方法的返回值类型是引用数据类型,那么子类方法的返回值类型可以是这个类,或者这个类的子类。

④权限修饰符:

​ Ⅰ:如果父类的方法的权限修饰符是private,那么子类是无法进行重写的,

如果父类是其他包的,并且方法的修饰符是缺省,那么子类也是无法进行重写的。

​ Ⅱ:其他情况下,重写方法的权限修饰符 可以大于等于被重写方法的权限修饰符

​ 本包中,父类的方法的修饰符是缺省的,子类的重写方法的修饰符可以是缺省的,protected,public ​ 其他包中,父类的方法的修饰符是protected,子类的重写方法的修饰符可以是protected,public ​ 其他包中,父类的方法的修饰符是public,子类的重写方法的修饰符可以是public

⑤其他修饰符

父类的static,final方法是不能重写的

(3)抛出异常时的要求

①如果被重写方法没有throws编译时异常,那么重写方法时,就不能throws编译时异常

​ ②如果被重写方法throws编译时异常,那么重写方法时,只能throws该异常类型或该异常类型的子类。<=

​ ③如果被重写方法throws运行时异常,子类重写时,可以一样,可以不写。(无关

​ ④如果被重写方法没有throws运行时异常,子类重写时,可以一样,可以throws自己的运行时异常。(无关

//throws声明异常
public class TestThrows {
    public static void main(String[] args) {
        try {
            copy("d:/1.txt","e:/1.txt");
            //调用者在调用copy方法时,就发现copy方法可能发生FileNotFoundException,需要处理
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println("其他代码");

        System.out.println("-----------------------------");
        Father f = new Son();//多态引用
        f.method();  //编译时看Father,运行时看Son
        try {
            f.fun();//编译时看Father,运行时看Son
        } catch (IOException e) {  //父类的类型是可以匹配子类的异常对象
            e.printStackTrace();
        }

        System.out.println("-----------------------------");
        Father father = new Son();
        Object value = f.getValue();//编译时看Father,运行时看Son
    }

    /**
     * 希望编写一个方法,实现复制一个文件的功能
     * @param srcFileName String 源文件的路径和文件名
     * @param destFileName String 目标文件的路径和文件名
     */
    public static void copy(String srcFileName, String destFileName)throws FileNotFoundException {
        // FileInputStream 这个类学过,从名字中猜测一下,用于读文件
        //new FileInputStream(srcFileName);可能抛出一个异常,这个异常是“编译时异常”FileNotFoundException
   //编译器告诉我们必须编写处理代码
// 通常的处理代码是try...catch,但是,这里用try...catch不合适
//因为当前方法只负责复制文件,关于源文件不存在这种情况,当前方法是处理不了的,必须交给调用者处理。
   //如果不管它呢,编译不通过,所以我必须告诉调用者,你要处理FileNotFoundException异常
 //我们只能通过在方法签名后面加throws 语法告诉调用者xx异常
        FileInputStream fis  = new FileInputStream(srcFileName);
//  fis.read();
    }
}

class Father{
    public void method(){//父类被重写方法,并没有throws 编译时异常,例如:FileNotFoundException
        //..
    }

    public void fun()throws IOException { //父类被重写方法,throws 编译时异常,例如:IOException
        //...
    }

    public void test()throws NullPointerException{

    }

    protected Object getValue(){
        return new Object();
    }
}
class Son extends Father{
/*    @Override
    public void method()throws FileNotFoundException {
    //父类的这个方法没有抛出编译时异常,那么重写方法时,就不能throws编译时异常
        super.method();
    }*/

    @Override
    public void fun() throws FileNotFoundException {//重写方法时,可以抛出比被重写方法 “小”的异常
        //...
    }

    @Override
    public void test()  {
        super.test();
    }

    public String getValue(){
        return "";
    }
}

class Son2 extends Father{
    @Override
    public void fun() throws IOException {//重写方法时,可以抛出和被重写方法 “一样”的异常
        super.fun();
    }
}

异常throw

和异常有关的关键字:try,catch,finally,throws,throw。

1.throw:在Java中是抛异常对象

2.异常的对象在Java中有两个角色可以抛:

(1)JVM

通常都是抛出系统预定义的异常,JVM会根据当前代码的情况,判断是那种异常情况,找到对应的类型,创建对象并抛出

(2)throw语句

①可以抛出系统预定义异常,需要我们自己new对象,自己抛。

②自定义异常类型,自己new对象,自己抛。

3.注意

(1)无论是JVM抛的,还是我们自己通过throw语句抛的,最终都要通过try...catch处理,否则都会导致程序崩溃。

(2)当然在哪里try...catch,根据实际情况来决定,可以在当前方法中直接try..catch,也可以先throws,然后由调用者try...catch

4.如何throw?

//throw格式一
异常类型 对象 = new 异常类型(【实参列表】);
throw 对象;
//格式二
throw new 异常类型(【实参列表】)

5.throw语句也是跳转语句

​ 当方法执行了throw语句,且在throw外面没有加try..catch。它后面的语句就无法执行了,会导致当前方法结束。

​ 当方法执行了throw语句,但在throw外面加了try..catch。try后面的就无法执行了,但是try...catch下面的代码可以继续执行。

​ 通常,可以使用throw语句代替return语句,用于异常情况下结束当前方法。

自定义异常

1.为什么要自定义异常?

​ 系统已经预定义了很多的异常类型,但是有时候还是难以描述我们当前的代码问题,比如:在编写一个某个银行的管理系统,我们想要描述客户取钱的时候,金额输入太多,余额不足的情况,如果想要通过抛异常的方式来表达的话,在现有的系统预定类型中找不到。

2.如何自定义异常?

要求:

(1)必须继承Throwable类或者它的子类

​ 通常我们是机场Exception或者RuntimeException。

​ 如果是继承RuntimeException或者它的子类的话,那么这个自定义的异常也是运行时异常,否则是编译时异常。

(2)建议自定义异常,保留无参构造

(3)建议自定义异常,定义一个这样的构造器

【修饰符】 构造器名称(String message){
//这个message是关于异常对象的描述信息。	
}

(4)建议自定义异常实现一个接口java.io.Serializable接口(后面IO流章节学习,它是关于对象序列化的)。

Serializable和Cloneable这两个接口中都没有抽象方法,这种接口属于标识性接口,只是为了区别类型用的,例如:我们把类分为支持克隆和不支持克隆。实现了Cloneable几口就支持,不实现这个接口就不支持,Cloneable接口的抽象方法在Object类中,clone()方法。因为它希望无论对象呈现为什么类型(即编译时类型是任何类型)都可以调用clone()方法。如果任意类型的对象都想要拥有的方法,只能放在Object类中。

Serializable接口也是用于区分两种对象,一种支持序列化,一种不支持序列化,序列化的工作,实际上是由虚拟机帮我们完成的。

3.语法格式

【修饰符】 class 自定义异常类 extends 父异常类{	}

4.自定义异常的抛出,必须由程序员自己创建new,并且自己throw抛出。

多线程

1.并行与并发

并行(parallel):无论从宏观还是微观的角度,都是多个任务同时运行,需要多个处理器(cpu)的支持。

并发(concurrency):指的是两个或者多个事件在同一个时间段内发生。指在同一时刻只能有一条指令执行,但多个进程(线程)的指令被快速轮换执行,使得在宏观上具有多个进程(线程)同时执行的效果。

2.进程与线程

程序:为了完成某个任务和功能,选择一种编程语言编写的一组指令的集合。

进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

==》进程是操作系统调度和分配资源的最小单位。

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

Java程序本身就是多线程的。

除了main线程,后台还有GC线程,异常监测线程....

​ 操作系统支持多线程系统(多任务操作系统),然后操作系统支持多线程。早期时,计算机都是只有一个CPU,并且单核。同一时刻只能运行一个线程==》单任务单进程,顺序进行。

​ 中期时,计算机都是只有一个CPU,但是我们希望充分利用CPU,可以让CPU在多个任务之间快速切换,让用户感觉是多个任务同时进行==》并发。

​ 后来开始支持多核CPU,可以在同一时刻运行多个任务==》并行。

当然,在任务数量大于CPU个数时,还是会有并行和并发同时存在的。

另行创建和启动线程

1.在Java中如何支持多线程?

(1)java.lang包:Thread类,Runnable接口。(SE阶段)

(2)java.util.concurrent:callable接口等(高级部分),俗称JUC。

2.如何开启main线程以外的线程

JavaSE阶段有两种方式:

(1)继承Thread类

(2)实现Runnable接口

Thread类

1.步骤

(1)声明一个线程类继承Thread类

(2)必须重写父类的一个方法:run();

这个方法的方法体,就是这个线程的任务代码,如果不进行重写,那么这个线程毫无意义。

例如:在这个线程中,打印1-10之间的偶数。

(3)创建线程对象

(4)必须启动线程,调用start()方法。

示例:

public class TestThread {    public static void main(String[] args) {        //(3)创建线程对象        MyThread my = new MyThread();//        my.run();//语法上允许这么调,但是,这么写和多线程一点关系都没有。        my.start();//这个start()它是从Thread类继承的        //在main线程中,我打印1-10之间的奇数        for(int i=1; i<=1000; i+=2){            System.out.println("main,i = " + i);        }    }}class MyThread extends Thread{    //重写:ctrl + o    @Override    public void run() {        for(int i=2; i<=1000; i+=2){            System.out.println("自定义线程,i = " + i);        }    }}

start方法:

他是从Thread继承来的方法

run方法:

不是由我们直接调用的,笼统的说,是JVM中某个程序帮我们调用的,线程调度程序。

Runnable接口

1.步骤

(1)声明一个线程类实现Runnable接口。

(2)重写Runnable接口的抽象方法:run()

(3)创建自定义线程对象

(4)创建Thread类对象(因为实现类,父接口,父类中都没有start方法),将线程对象传入Thread构造器,调用有参构造创建对象。

(5)启动线程,调用start方法。

示例:

public class TestRunnable {    public static void main(String[] args) {        MyRunnable my = new MyRunnable();//        my.start(); //错误的,因为MyRunnable这个类中没有,父类和父接口也没有        //创建Thread类对象        Thread thread = new Thread(my);//my给Thread类的target赋值        thread.start();        //在main线程中,我打印1-10之间的奇数        for(int i=1; i<=1000; i+=2){            System.out.println("main,i = " + i);        }    }}class MyRunnable implements Runnable{    @Override    public void run() {        for(int i=2; i<=1000; i+=2){            System.out.println("自定义线程,i = " + i);        }    }}

如何通过Thread类的对象,间接调用run方法的?

看源码:java.lang.Thread类的public void run()

public void run(){	if(target!=null){		target.run();	}}

快捷键:查看某个类的源码 Ctrl+N

​ 查看某个类的成员列表Ctrl+F12,有的可能要加上Fn

​ 其实JVM的线程调度器是调用了Thread类对象的run方法。

使用匿名内部类对象来实现线程的创建和启动

方法一:继承Thread类

new Thread("新的线程!"){
	@Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+":正在执行!"+i);
        }
	}
    }.start();

方法二:实现Runnable

new Thread(new Runnable(){
			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					System.out.println(Thread.currentThread().getName()+":" + i);
				}
			}
		}).start();

线程的API

  1. public void run() :此线程要执行的任务在此处定义代码。

  2. public void setName(String name):设置线程名称。

  3. public String getName() :获取当前线程名称。每一个线程都有名字,main线程的名称就是main,其他线程默认是thread-编号,编号从零开始。

  4. public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

  5. public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。

  6. public final int getPriority() :返回线程优先级

  7. public final void setPriority(int newPriority) :改变线程的优先级,如果优先级不在MIN _PRIORITY 和MAX_PRIORITY之间(常量),会抛出非法参数异常。

每个线程都有一定的优先级,优先级高的线程抢到CPU的概率越高,但是不代表优先级低的一点机会都没有。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread类提供了setPriority(int newPriority)和getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:

MAX_PRIORITY(10):最高优先级

MIN _PRIORITY (1):最低优先级

NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。

  1. public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

  2. public static void yield():yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。

  3. void join() :等待该线程终止。

    void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。

    void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。