Java基础知识总结(下)

final关键字

在Java中声明类、变量和方法时,可使用关键字final来修饰,表示“最终的”

  • final标记的类不能被继承。提高安全性,提高程序的可读性。

    例如:String类、System类、StringBuffer类

  • final标记的方法不能被子类重写。

    例如:Object类中的getClass()。

  • final标记的成员变量必须在声明时或在每个构造器中或代码块中显式赋值,然后才能使用。

  • final修饰的引用数据类型其地址值不能变化,但是其内部属性可以改变。

final修饰类

1
2
3
4
5
6
final class A{

}
class B extends A{ //错误,不能被继承。

}

中国古代,什么人不能有后代,就可以被final声明,称为“太监类”

final修饰方法

1
2
3
4
5
6
7
8
9
10
11
class A {
public final void print() {
System.out.println("A");
}
}
class B extends A {
public void print() { // 错误,不能被重写。
System.out.println("B");
}
}

final修饰变量—常量

1
2
3
4
5
6
7
class A {
private final String INFO = "test"; //声明常量
public void print() {
//The final field A.INFO cannot be assigned
INFO = "尚硅谷";//错误,final修饰的变量不能被改变
}
}

final举例应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class Test {
public static int totalNumber = 5;
public final int ID;
public Test() {
ID = ++totalNumber; // 可在构造器中给final修饰的“变量”赋值
}
public static void main(String[] args) {
Test t = new Test();
System.out.println(t.ID);
t.ID = 1;//非法
final int x = 10;
x = 20;//非法
final int y;
y = 30; //合法
}
}

static关键字

当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上 的对象,只有通过new关键字才会产生出对象,这时系统才会分配内存空间给对象, 其方法才可以供外部调用。我们有时候希望无论是否产生了对象或无论产生了多少 对象的情况下,某些特定的数据在内存空间里只有一份,例如所有的中国人都有个 国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中 都单独分配一个用于代表国家名称的变量。

1
2
3
4
5
6
7
8
9
10
11
12
class Circle{
private double radius = 1.0;
public Circle(double radius){
this.radius=radius;
}
public double findArea(){
return Math.PI*radius*radius;
}
}
//创建两个Circle对象
Circle c1=new Circle(2.0); //c1.radius=2.0
Circle c2=new Circle(3.0); //c2.radius=3.0

上面的Circle类中的变量radius是一个**实例变量(instance variable)**,它属于类的每一个对象,不能被同一个类的不同对象所共享。上例中c1的radius独立于c2的radius,存储在不同的空间。c1中的radius 变化不会影响c2的radius,反之亦然。

  • static关键字的使用范围:

    • 在Java类中,可用static修饰属性、方法、代码块、内部类
  • 被修饰后的成员具备以下特点:

    • 随着类的加载而加载
    • 优先于对象存在
    • 修饰的成员,被所有对象所共享
    • 访问权限允许时,可以不创建对象,直接被类调用

静态变量(类变量)

类属性作为该类各个对象之间共享的变量。在设计类时,分析哪些属性不因对象的不同而改变,将这些属性设置为类属性。相应的方法设置为类方法。

拿上面的煮个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Circle{
private static double radius = 1.0;
public Circle(){

}
public double findArea(){
return Math.PI*radius*radius;
}
}
//创建两个Circle对象
Circle c1=new Circle(); //c1.radius=1.0
Circle c2=new Circle(); //c2.radius=1.0
//这两个对象是共用这个radius,radius在内存空间中指向的是同一个地址

再煮个栗子:

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
31
32
public class Person {
private int id;
public static int total = 0;
public Person() {
total++;
id = total;
}
}

class Person {
private int id;
public static int total = 0;
public Person() {//每次new对象的时候id会自动加1
total++;
id = total;
}
public static void main(String args[]){
Person Tom = new Person();
Tom.id=0;
total=100; // 不用创建对象就可以访问静态成员
}
}

public class StaticDemo {
public static void main(String args[]) {
Person.total = 100; // 不用创建对象就可以访问静态成员
//访问方式:类名.类属性,类名.类方法
System.out.println(Person.total);
Person c = new Person();
System.out.println(c.total); //输出101
}
}
image-20200506143718368.png

静态方法(类方法)

如果方法与调用者无关,则这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用。

  • 因为不需要实例就可以访问static方法,因此static方法内部不能有this。==(也不能有super )==

  • ==static修饰的方法不能被重写==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
private int id;
private static int total = 0;
public static void setTotalPerson(int total){
this.total=total; //非法,在static方法中不能有this,也不能有super
}
public Person() {
total++;
id = total;
}
}
public class PersonTest {
public static void main(String[] args) {
Person.setTotalPerson(3);
}
}

单例(Singleton)设计模式

设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、 以及解决问题的思考方式。设计模免去我们自己再思考和摸索。就像是经典的棋谱,不同的棋局,我们用不同的棋谱。

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private,这样,就不能用new操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象, 静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的。

饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
// 1.私有化构造器
private Singleton() {

}
// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single = new Singleton();
// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
return single;
}
}

懒汉式

这种写法暂时还存在线程安全的问题,后面多线程的时候再修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
// 1.私有化构造器
private Singleton() {

}
// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single;
// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
if(single == null) {
single = new Singleton();
}
return single;
}
}

单例模式的优点

由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的 产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

应用场景

  • 网站的计数器,一般是单例模式实现,否则难以同步
  • 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
  • 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,都生成一个对象去读取。
  • Windows的Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

main方法的理解

由于Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是 public,又因为Java虚拟机在执行main()方法时不必创建对象,所以该方法必须 是static的,该方法接收一个String类型的数组参数,该数组中保存执行Java命令 时传递给所运行的类的参数。

又因为main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创 建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种情 况,我们在之前的例子中多次碰到。

命令行参数用法举例

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CommandPara {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "] = " + args[i]);
}
}
}
//运行程序CommandPara.java
//java CommandPara “Tom" “Jerry" “John"
//输出结果
//args[0] = Tom
//args[1] = Jerry
//args[2] = John

代码块

作用

对Java类或对象进行初始化

静态代码块

格式

1
2
3
4
5
6
7
8
9
class Test{
//属性......
//代码块
static{

}
//构造器......
//方法......
}

注意:

  1. 不可以调用非静态的属性和方法。

  2. 静态代码块的执行要先于非静态代码块。

  3. 静态代码块随着类的加载而加载,并且只执行一次。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
public static int total;
static {
total = 100;
System.out.println("in static block!");
}
}
public class PersonTest {
public static void main(String[] args) {
System.out.println("total = " + Person.total);//100
System.out.println("total = " + Person.total);//100
}
}

非静态代码块

格式:

1
2
3
4
5
6
7
8
9
class Test{
//属性.......
//代码块
{

}
//构造器.......
//方法......
}

注意:

  1. 可以调用静态的代码结构,也可以调用非静态的结构。
  2. 每次创建对象的时候都会执行一次,且先于构造器的执行。

总结:程序中成员变量赋值的执行顺序

graph TB
a(默认初始化) --> b(显示初始化)
a(默认初始化) --> c(代码块)
b --> d(构造器)
c --> d
d --> e(通过 对象.属性 或 对象.方法 给属性赋值)

内部类

当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么整个内部的完整结构最好使用内部类。

内部类一般用在定义它的类或语句块之内,在外部引用它时必须给出完 整的名称。

成员内部类

  • 内部类作为类的成员的角色:

    • 和外部类不同,内部类还可以声明为private或protected
    • 可以调用外部类的结构
    • 内部类可以声明为static的,但此时就不能再使用外层类的非static的成员变量
  • 内部类作为类的角色:

    • 可以在内部定义属性、方法、构造器等结构
    • 可以声明为abstract类 ,因此可以被其它的内部类继承
    • 可以声明为final的

注意:

  1. 非static的成员内部类中的成员不能声明为static的,只有在外部类或static的成员内部类中才可声明static成员。
  2. 外部类访问成员内部类的成员,需要“内部类.成员”或“内部类对象.成员”的方式
  3. 成员内部类可以直接使用外部类的所有成员,包括私有的数据
  4. 当想要在外部类的静态成员部分使用内部类时,可以考虑内部类声明为静态的

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Outer {
private int s = 111;
public class Inner {
private int s = 222;
public void mb(int s) {
System.out.println(s); // 局部变量s
System.out.println(this.s); // 内部类对象的属性s
System.out.println(Outer.this.s); // 外部类对象属性s
}
}
public static void main(String args[]) {
Outer a = new Outer();
Outer.Inner b = a.new Inner();
b.mb(333);
}
}

局部内部类

1
2
3
4
5
6
7
8
9
10
class 外部类{
方法(){
class 局部内部类{
}
}
{
class 局部内部类{
}
}
}

注意:

  1. 只能在声明它的方法或代码块中使用,而且是先声明后使用。除此之外的任何地方都不能使用该类
  2. 是它的对象可以通过外部方法的返回值返回使用,返回值类型只能是局部内部类的父类或父接口类型

局部内部类的特点

  • 内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号,以及数字编号。
  • 只能在声明它的方法或代码块中使用,而且是先声明后使用。除此之外的任何地方都不能使用该类。
  • 局部内部类可以使用外部类的成员,包括私有的。
  • 局部内部类可以使用外部方法的局部变量,但是必须是final的。由局部内部类和局部变量的声明周期不同所致。
  • 局部内部类和局部变量地位类似,不能使用public,protected,缺省,private
  • 局部内部类不能使用static修饰,因此也不能包含静态成员

匿名内部类

匿名内部类不能定义任何静态成员、方法和类,只能创建匿名内部类的一个实例。一个匿名内部类一定是在new的后面,用其隐含实现一个接口或 实现一个类。

特点:

  • 匿名内部类必须继承父类或实现接口
  • 匿名内部类只能有一个对象
  • 匿名内部类对象只能使用多态形式引用

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface A{
public abstract void fun1();
}
public class Outer{
public static void main(String[] args) {
new Outer().callInner(new A(){
//接口是不能new但此处比较特殊是子类对象实现接口,只不过没有为对象取名
public void fun1() {
System.out.println(“implement for fun1");
}
});// 两步写成一步了
}
public void callInner(A a) {
a.fun1();
}
}

思考题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public Test() {
Inner s1 = new Inner();
s1.a = 10;
Inner s2 = new Inner();
s2.a = 20;
Test.Inner s3 = new Test.Inner();
System.out.println(s3.a);
}
class Inner {
public int a = 5;
}
public static void main(String[] args) {
Test t = new Test();
Inner r = t.new Inner();
System.out.println(r.a);
}
}

输出结果是两个5

没想到吧,其实仔细看一下也不能理解

抽象类和抽象方法

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一 般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父 类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。

先煮个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class A {  //抽象类
abstract void m1(); //抽象方法
public void m2() {
System.out.println("A类中定义的m2方法");
}
}
class B extends A {
void m1() {//抽象方法的具体实现
System.out.println("B类中定义的m1方法");
}
}
public class Test {
public static void main(String args[]) {
A a = new B();
a.m1();//虚拟方法调用
a.m2();
}
}

抽象类的一些规定

  • 用abstract关键字来修饰一个类,这个类叫做抽象类。
  • 用abstract来修饰一个方法,该方法叫做抽象方法。
    • 抽象方法:只有方法的声明,没有方法的实现,以分号结束: 比如:public abstract void talk();
  • 含有抽象方法的类必须被声明为抽象类。
  • 抽象类==不能被实例化==。抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写==全部==的抽象方法,则该类仍为抽象类。
  • 抽象类可以有构造器,但是不能直接用构造器实例化对象,但实例化子类的时候,就会初始化父类,不管父类是不是抽象类都会调用父类的构造方法,初始化一个类,都要先先初始化父类。

注意:

  1. 不能用abstract修饰变量、代码块、构造器;
  2. 不能用abstract修饰私有方法、静态方法、final的方法、final的类。

具体应用

在航运公司系统中,Vehicle类需要定义两个方法分别计算运 输工具的燃料效率和行驶距离。

问题:卡车(Truck)和驳船(RiverBarge)的燃料效率和行驶距离的计算方法完全不 同。Vehicle类不能提供计算方法,但子类可以。

具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class Vehicle{
public abstract double calcFuelEfficiency(); //计算燃料效率的抽象方法
public abstract double calcTripDistance(); //计算行驶距离的抽象方法
}
public class Truck extends Vehicle{
public double calcFuelEfficiency() {
//写出计算卡车的燃料效率的具体方法
}
public double calcTripDistance() {
//写出计算卡车行驶距离的具体方法
}
}
public class RiverBarge extends Vehicle{
public double calcFuelEfficiency() {
//写出计算驳船的燃料效率的具体方法
}
public double calcTripDistance() {
//写出计算驳船行驶距离的具体方法
}
}

注意:抽象类不能实例化 new Vihicle()是非法的!

模板方法设计模式

抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。

应用场景:

当功能内部一部分实现是确定的,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用, 这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class Template {
public final void getTime() {
long start = System.currentTimeMillis();
code();
long end = System.currentTimeMillis();
System.out.println("执行时间是:" + (end - start));
}
public abstract void code();
}
class SubTemplate extends Template {
public void code() {
for (int i = 0; i < 10000; i++) {
System.out.println(i);
}
}
}

接口(interface)

概述

  1. 一方面,有时必须从几个类中派生出一个子类,继承它们所有的属性和方 法。但是,Java不支持多重继承。有了接口,就可以得到多重继承的效果。
  2. 另一方面,有时必须从几个类中抽取出一些共同的行为特征,而它们之间又 没有is-a的关系,仅仅是具有相同的行为特征而已。例如:鼠标、键盘、打 印机、扫描仪、摄像头、充电器、MP3机、手机、数码相机、移动硬盘等都 支持USB连接。
  3. 接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要…则 必须能…”的思想。继承是一个”是不是”的关系,而接口实现则是 “能不能” 的关系。
  4. 接口的本质是契约,标准,规范,就像我们的法律一样。制定好后大家都 要遵守。
image-20200506191958389

示例:

1
2
3
4
5
6
public interface Runner {
int ID = 1;
void start();
public void run();
void stop();
}

接口的特点

  • 接口中所有成员变量都默认是public static final修饰的
  • 接口中所有抽象方法都是public abstract修饰的
  • 接口中没有构造器
  • 接口可以被多实现

上面示例的代码实际上是:

1
2
3
4
5
6
public interface Runner {
public static final int ID = 1;
public abstract void start();
public abstract void run();
public abstract void stop();
}

接口的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Runner {
public void start();
public void run();
public void stop();
}
class Person implements Runner {
public void start() {
// 准备工作:弯腰、蹬腿、咬牙、瞪眼
// 开跑
}
public void run() {
// 摆动手臂
// 维持直线方向
}
public void stop() {
// 减速直至停止、喝水。
}
}

注意:

  • 一个类可以实现多个接口,接口也可以继承其它接口。
  • 实现接口的类中必须提供接口中所有方法的具体实现,方可实例化。否则,仍为抽象类,不可实例化。
  • 与继承关系类似,接口与实现类之间存在多态性
  • 接口和类是并列关系,或者可以理解为一种特殊的类。从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义(JDK7.0及之前),而没有变量和方法的实现。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Runner { 
public void run();
}
interface Swimmer {
public double swim();
}
class Creator{
public int eat(){

}
}
class Man extends Creator implements Runner, Swimmer{//一个类可以实现多个接口
public void run() {

}
public double swim() {

}
public int eat() {

}
}

实现类必须给出接口以及父接口中所有方法的实现。否则,实现类仍需声明为abstract的。

1
2
3
4
5
6
7
8
9
10
11
interface MyInterface{
String s=“MyInterface”;
public void absM1();
}
interface SubInterface extends MyInterface{
public void absM2();
}
public class SubAdapter implements SubInterface{
public void absM1(){System.out.println(“absM1”);}
public void absM2(){System.out.println(“absM2”);}
}

Java 8中关于接口的改进

Java 8中,你可以为接口添加静态方法和默认方法。从技术角度来说,这是完 全合法的,只是它看起来违反了接口作为一个抽象定义的理念。

静态方法:

使用 static 关键字修饰。可以通过接口直接调用静态方法,并执行 其方法体。我们经常在相互一起使用的类中使用静态方法。你可以在标准库中 找到像Collection/Collections或者Path/Paths这样成对的接口和类。

默认方法:

默认方法使用 default 关键字修饰。可以通过实现类对象来调用。 我们在已有的接口中提供新方法的同时,还保持了与旧版本代码的兼容性。 比如:java 8 API中对Collection、List、Comparator等接口提供了丰富的默认 方法。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
public interface AA {
double PI = 3.14;
public default void method() {//默认方法
System.out.println("北京");
}
default String method1() {
return "上海";
}
public static void method2() {//静态方法
System.out.println(“hello lambda!");
}
}

注意:

  • 若一个接口中定义了一个默认方法,而另外一个接口中也定义了一个同名同参数的方法(不管此方法是否是默认方法),在实现类同时实现了这两个接口时,会出现:接口冲突。

  • 若一个接口中定义了一个默认方法,而父类中也定义了一个同名同参数的非抽象方法,则不会出现冲突问题。因为此时遵守:类优先原则。接口中具有相同名称和参数的默认方法会被忽略。

煮个有趣的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Filial {// 孝顺的
default void help() {
System.out.println("老妈,我来救你了");
}
}
interface Spoony {// 痴情的
default void help() {
System.out.println("媳妇,别怕,我来了");
}
}
class Man implements Filial, Spoony {
@Override
public void help() {
System.out.println("我该怎么办呢?");
Filial.super.help();
Spoony.super.help();
}
}

代理模式

概述:

代理模式是Java开发中使用较多的一种设计模式。代理设计就是为其他对象提供一种代理以控制对这个对象的访问。

举例:

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
interface Network {
public void browse();
}
// 被代理类
class RealServer implements Network {
@Override
public void browse() {
System.out.println("真实服务器上网浏览信息");
}
}
// 代理类
class ProxyServer implements Network {
private Network network;
public ProxyServer(Network network) {
this.network = network;
}
public void check() {
System.out.println("检查网络连接等操作");
}
public void browse() {
check();
network.browse();
}
}
public class ProxyDemo {
public static void main(String[] args) {
Network net = new ProxyServer(new RealServer());
net.browse();
}
}

应用场景

  1. 安全代理:屏蔽对真实角色的直接访问。
  2. 远程代理:通过代理类处理远程方法调用(RMI)
  3. 延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象 比如你要开发一个大文档查看软件,大文档中有大的图片,有可能一个图片有 100MB,在打开文件时,不可能将所有的图片都显示出来,这样就可以使用代理 模式,当需要查看图片时,用proxy来进行大图片的打开。

分类

  • 静态代理(静态定义代理类)
  • 动态代理(动态生成代理类)

工厂模式

略……..

接口和抽象类之间的对比

区别点 抽象类 接口
定义 包含抽象方法的类 主要是抽象方法和全局常量的集合
组成 构造器、抽象方法、普通方法、常量、变量 常量、抽象方法、(jdk8.0之后:默认方法、静态方法)
使用 子类继承抽象类(extends) 子类实现接口(implements)
关系 抽象类可以实现多个接口 接口不能继承抽象类,但接口可以继承接口
局限 只能单继承 可以多实现
实际 作为一个模板 作为一种标准或是表示一种能力
常见设计模式 模板方法 工厂设计模式、代理模式

异常处理

在使用计算机语言进行项目开发的过程中,即使程序员把代码写得尽善尽美, 在系统的运行过程中仍然会遇到一些问题,因为很多问题不是靠代码能够避 免的,比如:客户输入数据的格式,读取文件是否存在,网络是否始终保持 通畅等等。

Java程序在执行过程中所发生的异常事件可分为两大类:

  • Error:Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowError。一般不会编写针对性的代码进行处理。
  • Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,可以使 用针对性的代码进行处理。例如:
    • 空指针异常
    • 读取的文件不存在
    • 网络连接中断
    • 数据角标越界

对于这些错误,一般有两种解决方法:一是遇到错误就终止程序的运行。另一种方法是由程序员在编写程序时,就考虑到错误的检测、错误消息的提示,以及错误的处理。

捕获错误最理想的是在编译期间,但有的错误只有在运行时才会发生。比如:除数为0,数组下标越界等。

这里又可以将异常分为两类:分别是编译时异常运行时异常

image-20200512121258745

运行时异常

是指编译器不要求强制处置的异常。一般是指编程时的逻辑错误,是程序员应该积极避免其出现的异常。java.lang.RuntimeException类及它的子类都是运行时异常。

对于这类异常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。

编译时异常

是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。编译器要求Java程序必须捕获或声明所有编译时异常。

对于这类异常,如果程序不处理,可能会带来意想不到的结果。

常见的异常

java.lang.RuntimeException

  1. ClassCastException

    1
    2
    3
    4
    5
    6
    7
    8
    public class Order {
    public static void main(String[] args) {
    Object obj = new Date();
    Order order;
    order = (Order) obj;
    System.out.println(order);
    }
    }
  2. ArrayIndexOutOfBoundsException

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class IndexOutExp {
    public static void main(String[] args) {
    String friends[] = { "lisa", "bily", "kessy" };
    for (int i = 0; i < 5; i++) {
    System.out.println(friends[i]); // friends[4]?
    }
    System.out.println("/nthis is the end");
    }
    }
  3. NullPointerException

    1
    2
    3
    4
    5
    6
    7
    8
    public class NullRef {
    int i = 1;
    public static void main(String[] args) {
    NullRef t = new NullRef();
    t = null;
    System.out.println(t.i);
    }
    }
  4. ArithmeticException

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class DivideZero {
    int x;
    public static void main(String[] args) {
    int y;
    DivideZero c=new DivideZero();
    y=3/c.x;
    System.out.println("program ends ok!");
    }
    }
  5. NumberFormatException

  6. InputMismatchException

  7. ……

java.io.IOExeption

  1. FileNotFoundException
  2. EOFException

java.lang.ClassNotFoundException

java.lang.InterruptedException

java.io.FileNotFoundException

java.sql.SQLException

异常的处理

在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行x/y运算时,要检测分母为0,数据为空,输入的不是数据而是字符等。过多的if-else分支会导致程序的代码加长、臃肿,可读性差。因此采用异常处理机制。

机制一:try-catch-finally

格式:

1
2
3
4
5
6
7
8
9
10
11
12
try{
...... //可能产生异常的代码
}
catch( ExceptionName1 e ){
...... //当产生ExceptionName1型异常时的处置措施
}
catch( ExceptionName2 e ){
...... //当产生ExceptionName2型异常时的处置措施
}
finally{
...... //无论是否发生异常,都无条件执行的语句
}
  • try

    捕获异常的第一步是用try{…}语句块选定捕获异常的范围,将可能出现异常的代码放在try语句块中。

  • catch (Exceptiontype e)

    在catch语句块中是对异常对象进行处理的代码。每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。

  • finally

    捕获异常的最后一步是通过finally语句为异常处理提供一个统一的出口,使得在控制流转到程序的其它部分以前,能够对程序的状态作统一的管理。不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句中是否有return,finally块中的语句都会被执行。

如果明确知道产生的是何种异常,可以用该异常类作为catch的参数;也可以用其父类作为catch的参数。

比 如 : 可以用ArithmeticException 类作为参数的地方,就可以用RuntimeException类作为参数,或者用所有异常的父类Exception类作为参数。但不能是与ArithmeticException类无关的异常,比如:NullPointerException(catch 中的语句将不会执行)。

捕获异常的有关信息:与其它对象一样,可以访问一个异常对象的成员变量或调用它的方法。

  • getMessage() 获取异常信息,返回字符串

  • printStackTrace() 获取异常类名和异常信息,以及异常出现在程序中的位置。返回值void。

image-20200512201503354

常见异常捕获的举例

数据角标越界
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class IndexOutExp {
public static void main(String[] args) {
String friends[] = { "lisa", "bily", "kessy" };
try {
for (int i = 0; i < 5; i++) {
System.out.println(friends[i]);
}
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("index err");
}
System.out.println("/nthis is the end");
}
}
//输出结果
//lisa
//bily
//kessy
//index err
//this is the end
除零异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DivideZero1 {
int x;
public static void main(String[] args) {
int y;
DivideZero1 c = new DivideZero1();
try {
y = 3 / c.x;
} catch (ArithmeticException e) {
System.out.println("divide by zero error!");
}
System.out.println("program ends ok!");
}
}
//divide by zero error!
//program ends ok!

不捕获异常时的情况

前面使用的异常都是RuntimeException类或是它的子类,这些类的异常的特点是:即使没有使用try和catch捕获,Java自己也能捕获,并且编译通过( 但运行时会发生异常使得程序运行终止 )。

如果抛出的异常是IOException等类型的非运行时异常,则必须捕获,否则编译错误。也就是说,我们必须处理编译时异常,将异常进行捕捉,转化为运行时异常。

机制二:throws

如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这 种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理, 而由该方法的调用者负责处理。

在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。

声明抛出异常举例:
1
2
3
4
5
6
public void readFile(String file) throws FileNotFoundException {
……
// 读文件的操作可能产生FileNotFoundException类型的异常
FileInputStream fis = new FileInputStream(file);
..……
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThrowsTest {
public static void main(String[] args) {
ThrowsTest t = new ThrowsTest();
try {
t.readFile();
} catch (IOException e) {
e.printStackTrace();
}
}
public void readFile() throws IOException {
FileInputStream in = new FileInputStream("atguigushk.txt");
int b;
b = in.read();
while (b != -1) {
System.out.print((char) b);
b = in.read();
}
in.close();
}
}
image-20200512203407744
重写方法声明抛出异常的原则

重写方法不能抛出比被重写方法范围更大的异常类型。在多态的情况下,对methodA()方法的调用-异常的捕获按父类声明的异常处理。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class A {
public void methodA() throws IOException {
……
} }
public class B1 extends A {
public void methodA() throws FileNotFoundException {
……
} }
public class B2 extends A {
public void methodA() throws Exception { //报错
……
} }

手动抛出异常

Java异常类对象除在程序执行过程中出现异常时由系统自动生成并抛出,也可根据需要使用人工创建并抛出。

首先要生成异常类对象,然后通过throw语句实现抛出操作(提交给Java运行环境)。

1
2
IOException e = new IOException();
throw e;

可以抛出的异常必须是Throwable或其子类的实例。下面的语句在编译时将 会产生语法错误:

==throw new String(“want to throw”);==

用户自定义异常

  • 一般地,用户自定义异常类都是RuntimeException的子类。

  • 自定义异常类通常需要编写几个重载的构造器。

  • 自定义异常需要提供serialVersionUID

  • 自定义的异常通过throw抛出。

  • 自定义异常最重要的是异常类的名字,当异常出现时,可以根据名字判断异常类型。

用户自定义异常类MyException,用于描述数据取值范围错误信息。用户自己的异常类必须继承现有的异常类。

1
2
3
4
5
6
7
8
9
10
11
class MyException extends Exception {
static final long serialVersionUID = 13465653435L;
private int idnumber;
public MyException(String message, int id) {
super(message);
this.idnumber = id;
}
public int getId() {
return idnumber;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyExpTest {
public void regist(int num) throws MyException {
if (num < 0)
throw new MyException("人数为负值,不合理", 3);
else
System.out.println("登记人数" + num);
}
public void manager() {
try {
regist(100);
} catch (MyException e) {
System.out.print("登记失败,出错种类" + e.getId());
}
System.out.print("本次登记操作结束");
}
public static void main(String args[]) {
MyExpTest t = new MyExpTest();
t.manager();
}
}
image-20200512204401486