面向对象-下

每天学习新知识,每天进步一点点。

1. 类的继承

现实生活中,说到继承,多会想到子女继承父辈的财产。在程序中,继承指的是事物之间的所属关系,通过继承可以使多种事物之间形成一种关联体系。例如:猫和狗都属于动物。程序中便可以描述为猫和狗继承自动物。

1.1 继承的概念

在Java中,类的继承是指在一个现有类的基础上去构建一个新的类,构建出来的新类被称作子类,现有类可被称作父类或者基类,子类会自动拥有父类所有可继承的属性和方法

在程序中,如果想声明一个类继承另一个类,需要使用关键字extends。其基本语法格式如下:

[修饰符]class 子类 extends 父类 {
    // 子类独有属性和方法
}

接下来通过一个案例来学习子类示如何继承父类的,如例1-1所示:
例1-1:Example1.java

//定义animal类
class Animal {
    String name;        //声明name属性

    //定义动物叫的方法
    void shout(){
        System.out.println("动物叫");
    }
}

//定义dog类,继承animal类
class Dog extends Animal {

    //定义一个打印名字的方法
    void printName(){
        System.out.println("小狗的名字是:" + name);
    }
}

//创建测试类
public class Example1 {
    public static void main(String[] args) {
        //创建Dog对象
        Dog dog = new Dog();
        //为dog对象设置name属性
        dog.name = "旺财";
        //调用dog对象的printName方法
        dog.printName();
        //调用dog对象的shout方法
        dog.shout();
    }
}

输出

小狗的名字是:旺财
动物叫

例1-1中,Dog类通过extends关键字继承Animal类,这样Dog类便是Animal类的子类。并且子类没有声明name属性和shout()方法,但是却能访问这些属性和方法,这就说明,子类在继承父类的时候,会自动拥有父类的所有公共的属性和方法。

tip:在java中,类只支持单继承,不允许多重继承,也就是说一个类只能有一个直接父类。但是可以多个类继承同一个父类,也可以一个类的父类再去继承另外的父类。

1.2 重写父类方法

在继承关系中,子类会自动继承父类中公共的方法。但有时候,子类可能需要重新定义父类的方法,即对父类方法进行重写。需要注意的是,子类中重写的方法需要和父类被重写的方法具有相同的方法名、参数列表和返回值类型。

例1-1中,Dog类从Animal类继承了shout()方法,该方法在被调用时会打印"动物叫",这显然不能描述动物的具体叫声,Dog类表示犬类,发出的叫声应该是"汪汪"。为了解决这个问题,我们可以在Dog类中重写父类的shout()方法,如下所示:

例1-2:Example2.java

//定义animal类
class Animal {
    //定义动物叫的方法
    void shout(){
        System.out.println("动物叫");
    }
}

//定义dog类,继承animal类
class Dog extends Animal {
    //定义狗叫的方法
    void shout(){
        System.out.println("汪汪");
    }
}

//创建测试类
public class Example1 {
    public static void main(String[] args) {
        //创建Dog对象
        Dog dog = new Dog();
        //调用dog对象的shout方法
        dog.shout();
    }
}

输出

汪汪

例1-2中,Dog类定义了一个shout()方法,该方法重写了父类的shout()方法。从运行结果可以看出,在调用Dog类对象的shout()方法时,只会调用子类重写的方法,而不是父类的方法。

tip:子类重写父类方法时,不能用比父类中被重写的方法更严格的访问权限。比如,父类的方法是public,子类可以重写为protected,但不能重写为private

1.3 super关键字

例1-2的运行结果可以看出,当子类重写父类的方法后,子类对象将无法直接访问父类被重写的方法。为了解决这个问题,Java专门g提供了super关键字,它可以调用父类的成员。

接下来通过一个案例来学习super关键字的使用,如例1-3所示:

例1-3:Example3.java

class Animal {
    String name = "动物";        //声明name属性
    //定义动物叫的方法
    void shout(){
        System.out.println("动物叫");
    }
    //定义父类的有参构造方法
	public Animal(String name){
		System.out.println("我是一只:" + name);
	}
}

//定义dog类,继承animal类
class Dog extends Animal{
	
	String name = "犬类";
	void shout(){
		//调用父类被重写的方法
		super.shout();
	}
	
	//定义打印name的方法
	void printName(){
		System.out.println("调用父类的成员名字:" + super.name);
	}
	public Dog(){
		//调用父类的有参构造方法
		super("癞皮狗");
	}
}

//定义测试类
public class Example3 {
	public static void main(String[] args){
		//实例化Dog类对象
		Dog dog = new Dog();
		dog.shout();
		dog.printName();
	}
}

输出

我是一只:癞皮狗
动物叫
调用父类的成员名字:动物

例1-3中,演示了如何使用super关键字调用父类的成员和构造方法。首先定义了Animal类,它有name属性和shout()方法,还有一个有参构造方法。然后定义了Dog类,它继承了Animal类,并重写了父类的shout()方法,并定义了自己的name属性和printName()方法。在Dog类的构造方法中,调用了父类的有参构造方法,并传入了"癞皮狗"作为参数。这个时候我们在测试类中实例化Dog类对象,首先就会通过Dog类无参构造方法,调用父类Animal类有参构造方法,打印出"我是一只癞皮狗"。然后dog对象调用了重写的shuot()方法,打印出父类的shout()中的"动物叫",并调用自己定义的printName()方法,打印出父类当前的name属性。

当我们将super("癞皮狗");这一行注释掉以后在执行程序,就会发现程序会报错,因为Dog类没有定义Animal类的无参构造方法,所以编译器会报错。所以要进行修改,如例:

class Animal {
    String name = "动物";        //声明name属性
    //定义动物叫的方法
    void shout(){
        System.out.println("动物叫");
    }
    //定义父类的无参构造方法
    public Animal(){
		System.out.println("调用父类的无参构造方法,name=" + name);
	}
    //定义父类的有参构造方法
	public Animal(String name){
		System.out.println("我是一只:" + name);
	}
}

//定义dog类,继承animal类
class Dog extends Animal{
	
	String name = "犬类";
	void shout(){
		//调用父类被重写的方法
		super.shout();
	}
	
	//定义打印name的方法
	void printName(){
		System.out.println("调用父类的成员名字:" + super.name);
	}
	public Dog(){
		//调用父类的有参构造方法
		//super("癞皮狗");
	}
}

//定义测试类
public class Example3 {
	public static void main(String[] args){
		//实例化Dog类对象
		Dog dog = new Dog();
		dog.shout();
		dog.printName();
	}
}

输出

调用父类的无参构造方法,name=动物
动物叫
调用父类的成员名字:动物

tipsuper关键字只能在子类的成员方法或构造方法中使用,不能在静态方法中使用。调用super关键字调用父类的构造方法时,必须位于子类构造方法的第一行,并且只能出现一次。

1.4 Object类

java中提供了一个Object类,它是所有类的父类,所有的类都继承自Object类。因此Object类通常被称为超类基类或者根类。当定义一个类时,如果没有使用extends关键字指定父类,则默认继承自Object类。

Object类中定义了一些常用的方法,其中常用的方法如表所表1-4所示:

表1-4:Object类常用方法

方法声明功能描述
String toString()返回该对象的字符串表示形式
boolean equals(Object obj)判断两个对象是否相等
int hashCode()返回该对象的哈希码值
void finalize()当垃圾回收器确定该对象不再被使用时,调用该方法
final Class<?> getClass()返回该对象的运行时类

了解了Object类的这些常用方法,我们用案例来演示类中方法的使用,如例例1-4所示:

例1-4:Example4.java

class Animal {
    //定义动物叫的方法
    void shout(){
        System.out.println("动物叫");
    }
}

public class Example4 {
    public static void main(String[] args) {
        Animal animal = new Animal();
        //调用Animal类的toString()方法
        System.out.println(animal.toString());
        //调用Animal类的equals()方法
        Animal animal2 = new Animal();
        System.out.println(animal.equals(animal2));
        //调用Animal类的hashCode()方法
        System.out.println(animal.hashCode());
        //调用Animal类的getClass()方法
        System.out.println(animal.getClass());
    }
}

输出

Animal@5caf905d
false
1555009629
class Animal

在上面案例中,我们调用了Animal对象toString()方法,虽然Animal类没有定义toString()方法,是因为Object类中定义了该方法,Animal类默认继承了Object类

Object类toString()方法中输出信息具体格式如下:

getClass().getName() + "@" + Integer.toHexString(hashCode())

为了方便理解,接下来分别对其中的方法进行解释,具体如下:

  1. getClass().getName():返回该对象的运行时类名,即包名+类名的全限定名称。例如:Animal.getClass().getName()返回"Animal",Animal.getClass().返回"class Animal",。
  2. hasCode():返回该对象的哈希码值。是Object类中定义的一个方法,将对象的内存地址进行哈希计算,返回一个int类型的哈希值。
  3. equals():判断两个对象是否相等。是Object类中定义的一个方法,默认比较两个对象的内存地址是否相同,如果相同,则返回true,否则返回false
  4. Integer.toHexString(hashCode()):代表将对象的哈希码值转换为16进制字符串。

在实际开发中,通常希望toString()方法返回的不仅仅是对象的基本信息,而是一些有用的信息,因此可以通过重写Object类toString()方法来实现。如例例1-5所示:

例1-5:Example5.java

class Animal {
    //重写toString()方法
    public String toString(){
        return "我是一只动物";
    }
}

public class Example5 {
    public static void main(String[] args) {
        Animal animal = new Animal();
        //调用Animal类的toString()方法
        System.out.println(animal.toString());
    }
}

输出

我是一只动物

2. final关键字

在Java中,final关键字用来修饰类、方法和变量,用来表示它们的值不能被修改。

final修饰的类、变量和方法将具有以下特性:

  1. final修饰的类不能被继承
  2. final修饰的方法不能被子类重写
  3. final修饰的变量是常量,只能被赋值一次,不能被修改

2.1 final关键字修饰类

java中的类可以被final关键字修饰,表示该类不能被继承,也就是不能够派生子类。

接下来通过一个案例进行验证,如例2-1所示:

例2-1:Example6.java

final class Animal {
    //定义动物叫的方法
    void shout(){
        System.out.println("动物叫");
    }
}

class Dog extends Animal {

}

public class Example6 {
    public static void main(String[] args) {
        Animal animal = new Animal();
        //调用Animal类的shout()方法
        animal.shout();
        //尝试实例化Dog类对象
        Dog dog = new Dog();
    }
}

输出

Example6.java:8: 错误: 无法从最终Animal进行继承
class Dog extends Animal {
                  ^
1 个错误

可以从例子的编译结果来看,由于Animal类被final关键字修饰,当Dog类继承Animal类时,我们使用javac命令编译,出现了The type subclass the final class Animal(无法从最终Animal进行继承)的错误。由此可见,被final修饰的类为最终类,不能被其他类继承。

2.2 final关键字修饰方法

java中的方法可以被final关键字修饰,表示该方法不能被重写,也就是说,子类不能覆盖该方法。

接下来通过一个案例进行验证,如例2-2所示:

例2-2:Example7.java

class Animal {
    //定义动物叫的方法
    final void shout(){
        System.out.println("动物叫");
    }
}

class Dog extends Animal {
	//定义狗叫的方法
	void shout(){
		System.out.println("汪汪");
	}
}

public class Example7 {
    public static void main(String[] args) {
        Animal animal = new Animal();
        //调用Animal类的shout()方法
        animal.shout();
        //尝试实例化Dog类对象
        Dog dog = new Dog();
        //调用Dog类的shout()方法
        dog.shout();
    }
}

输出

Example7.java:10: 错误: Dog中的shout()无法覆盖Animal中的shout()
        final void shout(){
                   ^
  被覆盖的方法为final
1 个错误

可以从例子的运行结果来看,由于Animal类中的shout()方法被final关键字修饰,当我们尝试在Dog类中定义一个shout()方法时,编译器会报错,提示Cannot override the final method from Animal(无法重写父类final方法)。由此可见,被final修饰的方法为最终方法,不能被子类覆盖。正是由于final关键字的存在,当父类中定义某个方法时,如果不希望被子类重写,就可以使用final关键字修饰该方法。

2.3 final关键字修饰变量

java中的变量可以被final关键字修饰,被称为常量,它只能被赋值一次,也就是说一旦被赋值,就不能再被修改。如果再进行赋值,则编译器会报错。

接下来通过一个案例进行验证,如例2-3所示:

例2-3:Example8.java

public class Example8 {
    public static void main(String[] args) {
        final int a = 10;
        //尝试修改a的值
        a = 20;
        System.out.println("当前a的值:" + a);
    }
}

输出

Example8.java:5: 错误: 无法为最终变量a分配值
        a = 20;
        ^
1 个错误

可以从例子的编译结果来看,由于a变量被final关键字修饰,当我们尝试修改a的值时,编译器会报错,提示Cannot assign a value to a final variable(不能为final变量赋值)。由此可见,被final修饰的变量为常量,只能被赋值一次,不能被修改。

tip例2-3演示的是局部变量被final修饰的情况,当局部变量使用final进行修饰时,可以在声明变量的同时对变量进行赋值,也可以先声明在进行有且只有一次的赋值。而当成员变量使用final进行修饰时,在声明变量的同时必须进行初始化赋值,否则程序编译报错。

如例例2-4所示:

例2-4:Example9.java

public class Example9{
	final int a; //被修饰的成员变量,必须在声明的同时进行赋值,否则编译报错
	public static void main(String[]args){
		final int b; //被修饰的局部变量,可以先声明  再进行一次赋值
		b = 20;
	}
}

输出

Example9.java:2: 错误: 变量 a 未在默认构造器中初始化
        final int a; //被修饰的成员变量,必须在声明的同时进行赋值,否则编译报错
                  ^
1 个错误

可以从例子的编译结果来看,由于a变量被final关键字修饰,在Example9类中声明时,没有进行初始化赋值,编译器会报错,提示Variable 'a' might not have been initialized(变量'a'可能没有初始化)。由此可见,被final修饰的成员变量必须在声明的同时进行初始化赋值,否则编译报错。而被final修饰的局部变量可以先声明,再进行一次赋值,也可以在声明的同时进行赋值。

3. 抽象类和接口

3.1 抽象类

当定义一个类时,常常需要定义一些方法来描述该类的行为特征,但有时这些方法的实现并不确定。例如,在前面定义Animal类时,shout()方法用于表示动物的叫声,但是不同的动物,叫声也是不同的。因此shout()方法无法准确描述动物的叫声。这时,我们就可以使用抽象方法来满足这种需求。抽象方法必须使用abstract关键字进行修饰,并且在定义方法时不需要实现方法体。当一个类中包含了抽象方法,那么该类也必须使用abstract关键字进行修饰,表示该类是一个抽象类

抽象类及抽象方法定义的基本语法格式:

[修饰符] abstract class 类名{
    //定义抽象方法
    [修饰符] abstract 返回值类型 方法名([参数列表]);
    //其他方法或者属性
}

需要注意的是,包含抽象方法的类必须定义为抽象类,但是抽象类中可以不包含任何抽象方法。另外,抽象类也是不可以被实例化的,因为抽象类中可能包含抽象方法,而抽象方法是没有方法体的,不能被调用。如果想要调用抽象类中定义的抽象方法,则必须定义一个子类,并重写父类的抽象方法

tip:定义抽象方法只需要在普通方法上增加abstract关键字,并把普通方法的方法体全部去掉,然后再方法名称后面加上分号。例如:"public abstract void shout();"

接下来,通过一个案例来学习如何实现抽象类中的方法,如例3-1所示:

例3-1:Example10.java

//定义抽象类Animal
abstract class Animal {
    //定义动物叫的方法
    abstract void shout();
}

//定义狗类Dog,继承自Animal类
class Dog extends Animal {
    //实现抽象方法shout()方法,编写方法体
    void shout(){
        System.out.println("汪汪");
    }
}

public class Example10 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        //调用Dog类的shout()方法
        dog.shout();
    }
}

输出

汪汪

由例子可以看出,子类实现了父类的抽象方法后,可以正常进行实例化操作,通过实例化的对象即可调用实现的方法

3.2 接口

如果一个抽象类中的所有方法都是抽象的,则可以将这个类定义为接口。接口是抽象类的一种特例,它不能实例化,只能被其他类实现,并且不能有方法体。在JDK 8中,对接口进行了重新定义,接口中还可以有默认方法静态方法(也叫类方法)默认方法使用default关键字进行修饰,静态方法使用static关键字进行修饰。并且这两种方法都允许有方法体

与定义类不同的是,在定义接口时,不再使用class关键字,而是使用interface关键字。

接口定义的基本语法格式如下:

[修饰符] interface 接口名 [extends 父接口列表(父接口1,父接口2,...)]{
    [public] [static] [final] 常量类型 常量名 = 常量值;
    [public] [abstract] 方法返回值类型 方法名([参数列表]);
    [public] default 返回值类型 方法名([参数列表]) {
        //方法体;
    }

    [public] static 返回值类型 方法名([参数列表]) {
        //方法体;
    }
}

在上述语法格式中,修饰符可以使用public或直接省略。extends关键字用于指定接口的父接口,可以有多个父接口,解决了普通类的单继承的限制。常量定义格式与类中的常量定义格式相同,且必须声明时就进行初始化赋值。方法定义格式与类中的方法定义格式相同,但是方法体是不需要的。默认方法静态方法定义格式与类中的方法定义格式相同。

tip:在接口中定义常量时,可以省略public static final修饰符,因为接口中默认会为变量添加public static final修饰符。与此类似,定义抽象方法时,可以省略public abstract修饰符,因为接口中默认会为方法添加public abstract修饰符。定义默认方法静态方法时,可以省略public修饰符,因为接口中默认会为方法添加public修饰符。

从接口定义的语法格式可以看出,接口中可以包含三类方法抽象方法默认方法静态方法。其中静态方法可以通过"接口名.方法名"的形式来调用,而默认方法抽象方法只能通过接口实现类的实例对象来调用。因此需要定义一个接口的实现类,该类通过implements关键字来实现接口,并重写接口中定义的抽象方法和默认方法。需要注意的是,一个接口可以在继承另一个类同时实现多个接口,接口之间使用英文逗号分隔。

定义接口的实现类语法格式如下:

[修饰符] class 类名 [extends 父类] [implements 接口1,接口2,接口3]{
    //实现接口中定义的抽象方法和默认方法
}

了解了接口及其方法的定义方式后,通过一个案例来学习接口的实现与调用。如例3-2所示:

例3-2:Example11.java

//定义接口Animal
interface Animal {
    //定义常量
    String name = "动物";
    //定义动物叫的方法
    void shout();

    //定义一个默认方法
    default void getType(String type){
        System.out.println("该动物属于" + type);
    }

    //定义一个静态方法
    static String getName(){
        return Animal.name;
    }
}

//定义狗类Dog,实现接口Animal
class Dog implements Animal {
    //实现接口中定义的抽象方法shout()方法
    public void shout(){
        System.out.println("汪汪");
    }
}

public class Example11 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        //调用Dog类的shout()方法
        dog.shout();
        //调用Dog类的getType()方法
        dog.getType("哺乳动物");
        //使用接口名调用Dog类的getName()方法
        System.out.println(Animal.getName());
        //使用实例化对象调用常量
        System.out.println(dog.name);
    }
}

输出

汪汪
该动物属于哺乳动物
动物
动物

由例子可以看出,Dog类实现了Animal接口,并重写了Animal接口中定义的抽象方法默认方法。通过实例化的对象即可调用实现的方法,也可以通过接口名调用静态方法。另外,Dog类还定义了一个常量,通过实例化的对象也可以访问到。

上面演示的是类与接口之间的实现关系,其实接口与接口之间还可以实继承关系,接口中的继承同样使用extends关键字。接下来对例3-2进行修改,演示接口之间的继承关系,如例3-3所示:

例3-3:Example12.java

//定义接口Animal
interface Animal {
    //定义常量
    String name = "动物";
    //定义动物叫的方法
    void shout();

    //定义一个默认方法
    default void getType(String type){
        System.out.println("该动物属于" + type);
    }

    //定义一个静态方法
    static String getName(){
        return Animal.name;
    }
}
//定义接口Mammal,继承自Animal接口
interface Mamal extends Animal {
    //定义哺乳动物的叫声
    void breathe();
}

//定义狗类Dog,实现接口Mamal
class Dog implements Mamal {
    //实现接口中定义的抽象方法shout()方法
    public void shout(){
        System.out.println("汪汪");
    }

    //实现接口中定义的默认方法breathe()方法
    public void breathe(){
        System.out.println("狗在呼吸");
    }
}

public class Example12 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        //调用Dog类的shout()方法
        dog.shout();
        //调用Dog类的breathe()方法
        dog.breathe();
        //调用Dog类的getType()方法
        dog.getType("哺乳动物");
        //使用接口名调用Dog类的getName()方法
        System.out.println(Animal.getName());
        //使用实例化对象调用常量
        System.out.println(dog.name);
    }
}

输出

汪汪
狗在呼吸
该动物属于哺乳动物
动物
动物

由例子可以看出,Dog类实现了Mamal接口,并继承了Animal接口,因此Dog类同时实现了Animal接口和Mamal接口。通过实例化的对象即可调用实现的方法,也可以通过接口名调用静态方法。另外,Dog类还定义了一个常量,通过实例化的对象也可以访问到。

为了加深对接口的认识,对接口的特点进行归纳,具体如下:

  1. JKD 8 之前,接口中的方法都必须是抽象的,并且方法不能包含方法体。在调用抽象方法时,必须通过接口的实现类的对象才能调用。从JDK 8 开始,接口可以包含默认方法静态方法,并且可以有方法体。并且静态方法可以直接通过"接口名.方法名"的形式调用。
  2. 当一个类实现接口时,如果这个类是抽象类,只需要实现接口中的部分抽象方法即可。否则需要实现接口中的所有抽象方法。
  3. 一个类可以通过implements关键字来实现多个接口,接口之间使用英文逗号分隔。
  4. 接口之间可以通过extents关键字来继承,接口的继承关系同样支持多继承。
  5. 一个类在继承一个类的同时,还可以实现接口。但是extends关键字必须出现在implements关键字之前。

4. 多态

在java中,多态是指不同类的对象在调用同一个方法是所呈现的多种不同行为,通常来说,在一个类中定义的属性和方法被其它类继承或重写后,当把子类对象直接赋值给父类引用变量时,相同引用类型的变量调用同一个方法将呈现多种不同形态。通过多态,消除了类直接的耦合关系,大大提高了程序的可扩展性和可维护性。

4.1 多态的概述

Java的多态性是由类的继承方法重写以及父类引用指向子类对象体现的,由于一个类可以有多个子类,多个子类都可以重写父类方法,并且多个不同的子类对象也可以指向同一个父类,这样在程序运行时才能知道具体代表的是哪个子类对象,体现了多态性。

在了解了多态性的概念后,通过案例来演示说明多态的概念。如例4-1所示:

例4-1:Example13.java

//定义抽象类Animal
abstract class Animal {
    //定义动物叫的方法
    abstract void shout();
}

//定义狗类Dog,继承自Animal类
class Dog extends Animal {
    //实现抽象方法shout()方法,编写方法体
    void shout(){
        System.out.println("汪汪");
    }
}

//定义猫类Cat,继承自Animal类
class Cat extends Animal {
    //实现抽象方法shout()方法,编写方法体
    void shout(){
        System.out.println("喵喵");
    }
}

public class Example13 {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new cat();
        //调用Dog类的shout()方法
        dog.shout();
        //调用Cat类的shout()方法
        cat.shout();
    }
}

输出

汪汪
喵喵

例4-1中,首先定义了Animal抽象类抽象方法,接着定义了DogCat两个子类,DogCat都继承自Animal类,并且都实现了shout()方法。在main()方法中,通过Animal类的引用变量dogcat分别指向DogCat类的对象,然后调用shout()方法,由于DogCat都实现了shout()方法,因此调用的结果是不同的。这就是多态的概念,不同类的对象在调用同一个方法呈现的不同行为。

4.2 对象的类型转换

在多态的学习中,设计子类对象当作父类类型使用的情况,这种情况在java中称为向上转型。在向上转型时,编译器会自动将子类对象转换为父类类型,这样就可以调用父类中定义的属性和方法。

例如:

Animal animal = new Dog();   //将Dog类对象当作Animal类对象使用
Animal animal2 = new Cat();  //将Cat类对象当作Animal类对象使用

tip:将子类对象当作父类使用时不需要任何显式声明,需要注意的是,此时不能通过父类变量去调用子类特有的方法。
如例:
例4-2:Example14.java

//定义接口 Animal
interface Animal {
    //定义动物叫的方法
    void shout();
}

//定义cat类实现Animal接口
class Cat implements Animal {
    //实现接口中定义的抽象方法shout()方法
    public void shout() {
        System.out.println("喵喵");
    }
    //定义Cat类特有的抓老鼠方法
    public void catchMouse() {
        System.out.println("抓老鼠");
    }
}
//定义测试类
public class Example14 {
    public static void main(String[] args) {
        Animal animal = new Cat();
        //调用Animal接口的shout()方法
        animal.shout();
        //调用Cat类特有的抓老鼠方法
        animal.catchMouse();
    }
}

输出

Example14.java:25: 错误: 找不到符号
        animal.catchMouse();
              ^
  符号:   方法 catchMouse()
  位置: 类型为Animal的变量 animal
1 个错误

程序编译出现了"The method catchMouse() is undefined for the type Animal(在父类Animal中未定义catchMouse()方法)"的错误。原因在于,创建Cat类对象指向了Animal父类类型,这样新创建的Cat对象会自动向上转型为Animal类型。此时通过父类对象调用了Cat类特有的catchMouse()方法,但是由于Animal接口中没有catchMouse()方法,因此编译器无法调用。

由于通过new Cat() 创建的对象本质上就是Cat类型,所以通过Cat类型对象调用catchMouse()方法是可以的。所以为了解决上面的问题,可以将父类类型的animal对象强制转换为Cat类型,然后调用子类特有的catchMouse()方法。

接下来对上面的例子进行修改,演示向上转型的用法。如例4-3所示:

例4-3:Example15.java

//定义接口 Animal
interface Animal {
    //定义动物叫的方法
    void shout();
}

//定义cat类实现Animal接口
class Cat implements Animal {
    //实现接口中定义的抽象方法shout()方法
    public void shout() {
        System.out.println("喵喵");
    }
    //定义Cat类特有的抓老鼠方法
    public void catchMouse() {
        System.out.println("抓老鼠");
    }
}
//定义测试类
public class Example15 {
    public static void main(String[] args) {
        Animal animal = new Cat();
        //调用Animal接口的shout()方法
        animal.shout();
        //调用Cat类特有的抓老鼠方法
        ((Cat)animal).catchMouse();
    }
}

输出

喵喵
抓老鼠

例4-3中,通过new Cat()创建的对象animalCat类型,因此可以直接调用catchMouse()方法。但是由于animal是父类类型,因此不能调用子类特有的catchMouse()方法,因此需要将animal强制转换为Cat类型,然后调用子类特有的catchMouse()方法。

需要注意的是,在进行对象向下转型(强制类型转换)时,必须转换为本质类型,否则转换会出现ClassCastException异常。例如:

Animal animal = new Dog();
//这样做就是错误的,虽然Dog类和Cat类都继承自Animal类,但是animal对象的本质类型是Dog类,是无法强制转换为Cat类的
Cat cat = (Cat)animal; 

为了避免上面这种异常情况的发生,java提供了一个关键字instanceof来判断对象是否是某个类的实例,语法格式如下:

对象(或者对象引用变量) instanceof 类(或接口)

接下来通过案例来演示instanceof关键字的用法。如例:

例4-4:Example16.java

//定义接口 Animal
interface Animal {
    //定义动物叫的方法
    void shout();
}
//定义Dog类实现Animal接口
class Dog implements Animal {
    //实现接口中定义的抽象方法shout()方法
    public void shout() {
        System.out.println("汪汪");
    }
}

//定义cat类实现Animal接口
class Cat implements Animal {
    //实现接口中定义的抽象方法shout()方法
    public void shout() {
        System.out.println("喵喵");
    }
    //定义Cat类特有的抓老鼠方法
    public void catchMouse() {
        System.out.println("抓老鼠");
    }
}
//定义测试类
public class Example16 {
    public static void main(String[] args) {
        Animal animal = new Dog();
        if(animal instanceof Cat){  //判断animal对象是否是Cat类的实例
            Cat cat = (Cat)animal;  //向下转型为Cat类型
            cat.catchMouse();      //调用Cat类特有的抓老鼠方法
        }else{
            //调用Animal接口的shout()方法
            animal.shout();
            System.out.println("该动物不是猫");
        }
    }
}

输出

汪汪
该动物不是猫

例4-4中,通过new Dog()创建的对象animalDog类型,因此判断animal instanceof Catfalse,因此执行else语句,调用Animal接口的shout()方法,并输出"该动物不是猫"。如果animal instanceof Cattrue,则执行if语句,通过animal向下转型为Cat类型,然后调用Cat类特有的catchMouse()方法。

5. 内部类

在java中,允许在一个类的内部定义类,这样的类被称作内部类。这个内部类所在的类称为外部类。在实际情况中,根据内部类的位置修饰符定义方式的不同,内部类可以分为成员内部类局部内部类匿名内部类静态内部类四种。

5.1 成员内部类

成员内部类中,可以访问外部类所有成员,包括成员变量和成员方法外部类中同样可以访问成员内部类的变量和方法

通过案例学习成员内部类的定义何使用。如例5-1所示:

例5-1:Example17.java

//定义外部类
class Outer{
	int m = 10;
	void test1(){
		System.out.println("测试外部类成员方法");
	}
	//定义内部类
	class Inner{
		int n = 20;
		//定义内部类成员方法
		void show1(){
			System.out.println("测试内部类成员方法");
		}
		//定义内部类成员方法,访问外部类成员变量
		void show2(){
			System.out.println("测试访问外部类成员变量m=" + m);
			test1();
		}
	}
	//定义外部类方法,访问内部类成员变量
	void test2(){
		Inner inner = new Inner();
		System.out.println("访问内部类成员变量n=" + inner.n);
		inner.show1();
	}
}

public class Example17{
	public static void main(String[]args){
		Outer outer = new Outer();
		outer.test2();
		Outer.Inner inner = outer.new Inner();
		inner.show2();
	}
}

输出

访问内部类成员变量n=20
测试内部类成员方法
测试访问外部类成员变量m=10
测试外部类成员方法

例5-1中,定义了一个外部类Outer,内部类Inner定义在Outer类中,Inner可以访问外部类的成员变量m和成员方法test1()Outer类可以访问Inner类的成员变量n和成员方法show1()Outer类还可以访问Inner类的成员方法show2(),并且Inner类可以访问Outer类的成员方法test1()

tip:需要通过外部类对象创建内部类对象,才可以访问内部类中的成员,创建内部类对象的具体语法格式如下:
外部类名.内部类名 变量名 = new 外部类名().new 内部类名();

5.2 局部内部类

局部成员类也叫做方法内部类,就是定义在某个局部范围中的类,和局部变量一样,都是在方法中定义,有效范围只限于方法内部。在局部内部类中,可以访问外部类的所有成员方法和变量,而局部内部类中的变量和方法只能在创建该局部内部类的方法中进行访问。

通过案例学习局部内部类的定义何使用。如例5-2所示:

例5-2:Example18.java

class Outer{
    int m =10;
    void test1(){
        System.out.println("测试外部类成员方法");
    }
    void test2(){
        //定义内部类
        class Inner{
            int n  = 20;
            void show(){
                test1();
            }
        }
        Inner inner = new Inner();
        System.out.println("访问内部类成员变量n=" + inner.n);
        inner.show();
    }  
}

public class Example18{
    public static void main(String[]args){
        Outer outer = new Outer();
        outer.test2();
    }
}

输出

访问内部类成员变量n=20
测试外部类成员方法

例5-2中,定义了一个外部类Outer,内部类Inner定义在test2()方法中,Inner可以访问外部类的成员变量m和成员方法test1(),并且Inner类可以访问Outer类的成员方法test1()

5.3 静态内部类

静态内部类是指内部类中定义的类,就是用static关键字修饰的成员内部类。它的对象与其他对象之间没有任何关系,也就是说,它没有自己的this指针,也不能访问外部类的非静态成员变量和方法。

创建静态内部类对象的基本语法格式:

外部类名.静态内部类名 变量名 = new 外部类名.静态内部类名();

通过案例学习静态内部类的定义何使用。如例5-3所示:

例5-3:Example19.java

class Outer{
    static int m = 10;
    //定义静态内部类
    static class Inner{
        void show(){
            System.out.println("访问外部静态变量m=" + m);
        }

    }
}
public class Example19{
    public static void main(String[]args){
        Outer.Inner inner = new Outer.Inner();
        inner.show();
    }
}

输出

访问外部静态变量m=10

例5-3中,定义了一个外部类Outer,内部类Inner定义在Outer类中,Inner可以访问外部类的静态变量m,并且Inner类可以访问Outer类的静态成员方法show()

5.4 匿名内部类

Java调用某个方法时,如果该方法的参数是一个接口类型,除了可以传入一个参数接口实现类,还可以使用匿名内部类实现接口来作为该方法的参数。

匿名内部类就是指没有名字的内部类,它的语法格式如下:

new 父接口() {
    //匿名内部类实现部分
};

接下来通过一个案例学习匿名内部类的定义和使用。如例5-4所示:

例5-4:Example20.java

//定义动物类接口
interface Animal{
	void shout();
}

//创建测试类
public class Example20{
    //定义静态方法,接收接口类型参数
	static void animalShout(Animal an){
		an.shout(); //调用传入对象an的shout()方法
	}

	public static void main(String[]args){
		//定义变量
        String name = "小花";
		//调用静态方法,传入匿名内部类对象
        animalShout(new Animal(){
			public void shout(){
                //从JDK 8 开始,局部内部类、匿名内部类可以访问外部类非final的局部变量
				System.out.println(name+"在喵喵叫");
			}
		});
	}
}

输出

小花在喵喵叫

看上去匿名内部类的写法可能难以理解,接下来分为两部来介绍。具体如下:

  1. 在调用animalShout()方法时,在方法的参数位置上写上new Animal(){},这相当于创建了一个实例对象,并将对象作为参数传递给了animalShout()方法。在new Animal()后面,跟了一对大括号,表示创建的对象为Animal子类实例,并且该子类为匿名的。如下:
animalShout(new Animal(){
    //匿名内部类实现部分
});
  1. 在大括号中编写匿名子类的实现代码即可。如下:
animalShout(new Animal(){
    //因为是Animal类的子类,因此需要重写shout()方法
    public void shout(){
        //从JDK 8 开始,局部内部类、匿名内部类可以访问外部类非final的局部变量
        System.out.println(name+"在喵喵叫");
    }
});

tip:匿名内部类中访问了局部变量name,而name并没有用final修饰,只是JDK 8 开始有的特性。在JDK 8 之前,想要访问局部变量前必须加final修饰符,否则程序编译报错。

6. JDK 8 的Lambda表达式

JDK 8 引入了Lambda表达式,可以简化代码,提高代码的可读性和可维护性。Lambda表达式是一种匿名函数,可以用来代替匿名内部类,可以用来创建函数式接口的实例。

6.1 Lambda表达式入门

一个Lambda表达式三个部分组成:分别为参数列表箭头(->)函数体。其语法格式如下:

([数据类型 参数名,数据类型 参数名,......]) -> {表达式主体};

从语法格式来看,Lambda表达式的书写非常简单。下面针对组成部分进行简单介绍:

  1. ([数据类型 参数名,数据类型 参数名,......]):用来向表达式主体传递接口方法需要的参数多个参数中间必须用英文逗号分隔。在编写时,可以省略参数的参数类型,后面表达式主体会自动进行校对和匹配。如果只有一个参数,可以省略括号()
  2. ->:箭头,用来指定参数数据指向不能省略,且必须用英文横线大于号书写。
  3. {表达式主体}:由单个表达式或语句块组成的主体,本质就是接口中抽象方法的实现。如果表达式主体只有一条语句,可以省略大括号{}。另外,Lamdba表达式主体中允许有返回值,当只有一条return语句时,可以省略return关键字。

学习了Lambda表达式的语法后,接下来通过案例来学习Lambda表达式的用法。如例6-1所示:

例6-1:Example21.java

interface Animal{
	void shout();
}


public class Example21{
	static void animalShout(Animal an){
		an.shout();
	}

	public static void main(String[] args){
		String name  = "咪咪";
		//使用匿名内部类
		animalShout(new Animal(){
			public void shout(){
				System.out.println(name + "正在喵喵叫");
			}
		});
		
		//使用Lambda表达式
		animalShout(()-> System.out.println("正在喵喵叫的是:" + name));
	}
}

输出

咪咪正在喵喵叫
正在喵喵叫的是:咪咪

例6-1中,定义了一个Animal接口。然后,测试类中定义了一个animalShout()方法,该方法接收一个Animal接口的实例作为参数。在main()方法中,创建了一个匿名内部类对象,并将其作为参数传递给animalShout()方法。另外,还创建了一个Lambda表达式,并将其作为参数传递给animalShout()方法。可以看出,Lambda表达式的书写更加简洁,更加易于理解。

6.2 函数式接口

虽然Lambda表达式可以简化代码,提高代码的可读性和可维护性,但是,Lambda表达式只能用于函数式接口,也就是只有一个抽象方法的接口。在JDK8中,专门为函数式接口定义了注解@FunctionalInterface,用来检查是否是一个函数式接口。使用该注解显式的标注了接口是一个函数式接口,并强制编辑器进行更严格的检查。如果不是函数式接口,编译器就会报错。

接下来通过一个案例来学习函数式接口的定义和使用。如例6-2所示:

例6-2:Example22.java

@FunctionalInterface
interface Animal{
	void shout();
}

interface Calculate{
    int sum(int a, int b);
}

public class Example22{
	static void animalShout(Animal an){
		an.shout();
	}

    static void showSum(int x,int y,Calculate c){
        System.out.println(x + "+"+y+"="+c.sum(x,y));
    }

    public static void main(String[] args){
		String name  = "小猫";	
		//使用Lambda表达式
		animalShout(()-> System.out.println("正在叫的是:" + name));
        showSum(10,20,(x,y)->x+y); 
	}
}

输出

正在叫的是:小猫
10+20=30

例6-2中,先定义了两个函数式接口,分别是AnimalCalculate。然后在测试类中分别编写了两个静态方法用于将函数式接口作为参数的形式传入,最后在main()方法中调用了两个方法。可以看出,Lambda表达式可以作为参数传入,并且可以作为方法的返回值。

tipshowSum(10,20,(x,y)->x+y); ,调用了静态的showSum()方法,10和20作为参数,(x,y)则作为了Calculate接口的sum()方法的两个参数,(x,y)->x+y则是Lambda表达式,实际上就是(x,y)->{return x+y;}。将两个参数相加后返回。

6.3 方法引用与构造器引用

Lambda表达式的主体只有一条语句时,程序不仅可以省略包含主体的大括号,还可以通过英文双冒号"::"的语法格式来引用方法和构造方法。这两种形式可以进一步简化Lambda表达式的书写。其本质都是对Lambda表达式的主体部分已存在的方法进行直接引用,主要区别就是对普通方法与构造方法的引用而已。

在JDK 8中,Lambda表达式支持的引用类型主要有以下几种:

种类Lambda表达式对应的引用示例
类名引用普通方法(x,y,...) -> 对象名 x.类普通方法名(y,...)类名::类普通方法名
类名引用静态方法(x,y,...) -> 类名.类静态方法名(x,y,...)类名::类静态方法名
对象名引用方法(x,y,...) -> 对象名.实例方法名(x,y,...)对象名::实例方法名
构造方法引用(x,y,...) -> new 类名(x,y,...)类名::new

6.3.1 类名引用静态方法

类名引用静态方法也是通过类名对静态方法的引用,该类可以是Java自带的特殊类,也可以是自定义的普通类。

接下来通过一个求绝对值的案例来演示(Math 特殊类)引用静态方法的使用。如例6-3所示:

例6-3:Example23.java

@FunctionalInterface
interface Calculate{
    int calc(int num);
}

class Math{
    public static int abs(int num){
        if(num < 0){
            return -num;
        }else{
            return num;
        }
    }
}

public class Example23{
    static void showAbs(int num,Calculate c){
        System.out.println(c.calc(num));
    }

    public static void main(String[] args){
        showAbs(-10, n->Math.abs(n));    //使用Lambda表达式
        showAbs(-10,Math::abs);          //方法引用的方式
    }
}

输出

10
10

例6-3中,先定义了一个函数式接口Calculate,以及一个包含静态方法的Math类,然后再测试类中编写了一个静态方法showAbs()和一个main()方法。最后在main()方法中分别使用Lambda表达式方法引用的方式作为静态方法showAbs()参数进行调用。

6.3.2 对象名引用方法

对象名引用方法指的是通过实例化对象的名称对其方法进行的引用。

接下来通过一个返回字符串所有字母大写的案例来演示对象名引用方法的使用。如例6-4所示:
例6-4:Example24.java

@FunctionalInterface
interface Printable{
    void print(String str);
}

class StringUtil{
    public void toUpperCase(String str){
        System.out.println(str.toUpperCase());
    }
}

public class Example24{
	static void printUpperCase(String str,Printable p){
		p.print(str);
	}
    
    public static void main(String[] args){
        StringUtil su = new StringUtil();
        //使用Lambda表达式
        printUpperCase("asdsasadwdsa", str -> su.toUpperCase(str));
		//使用对象名引用方法
		printUpperCase("asdsasadwdsa",su::toUpperCase);
    }
}

输出

ASDSAASADWDSA
ASDSAASADWDSA

例6-4中,先定义了一个函数式接口Printable,以及一个包含实例方法的StringUtil类,然后再测试类中编写了一个静态方法printUpperCase()和一个main()方法。最后在main()方法中分别使用Lambda表达式方法引用的方式作为静态方法printUpperCase()参数进行调用。

6.3.3 构造方法引用

构造方法引用是指通过构造方法创建对象,并对其进行引用。

接下来通过一个创建Person对象并调用其方法的案例来演示构造方法引用的使用。如例6-5所示:
例6-5:Example25.java

@FunctionalInterface
interface PersonBuild{
    //注意这里的返回值是Person类型
	Person buildPerson(String name);
}

class Person{
	private String name;
	
	public Person(String name){
		this.name = name;
	}
	
	public String getName(){
		return name;
	}
}

public class Example25{
	static void buildPersonName(String name,PersonBuild p){
		System.out.println(p.buildPerson(name).getName());
	}
	
	public static void main(String[]args){
        //使用Lambda表达式
		buildPersonName("大海1号", name->new Person(name));
        //使用构造方法引用的方式
		buildPersonName("大海2号",Person::new);
	}
}

输出

大海1号
大海2号

例6-5中,先定义了一个函数式接口PersonBuild,以及一个包含构造方法的Person类,然后再测试类中编写了一个静态方法buildPersonName()和一个main()方法。最后在main()方法中分别使用Lambda表达式构造方法引用的方式作为静态方法buildPersonName()参数进行调用。

6.3.4 类名引用普通方法

类名引用普通方法是指通过类名对普通方法的引用。

接下来仍然通过一个返回字符串所有字母大写的案例来演示类名引用普通方法的使用。如例6-6所示:

例6-6:Example26.java

// 定义一个函数式接口 Printable,该接口只有一个抽象方法 printUpperCase
@FunctionalInterface
interface Printable {
    void printUpperCase(StringUtil su, String str);
}

// 定义一个工具类 StringUtil,包含一个方法 printCase 用于打印字符串的大写形式
class StringUtil {
    // 将输入字符串转换为大写并打印
    public void printCase(String str) {
        System.out.println(str.toUpperCase());
    }
}

// 主类 Example26
public class Example26 {

    // 定义静态方法 printUpper,接收一个字符串、一个 StringUtil 对象和一个 Printable 函数式接口的实现
    static void printUpper(String str, StringUtil su, Printable p) {
        // 调用 Printable 接口的 printUpperCase 方法
        p.printUpperCase(su, str);
    }

    public static void main(String[] args) {
        // 调用 printUpper 方法,使用 Lambda 表达式作为 Printable 的实现
        printUpper("sdadsa", new StringUtil(), (object, str) -> object.printCase(str));

        //lambda表达式,实际上就是这段匿名内部类的简写
       /*
        printUpper("sdadsa", new StringUtil(), new Printable() {
            @Override
            public void printUpperCase(StringUtil object, String str) {
                // 在这里调用 StringUtil 的 printCase 方法
                object.printCase(str);
            }
        });
        */

        // 调用 printUpper 方法,使用方法引用来作为 Printable 的实现
        printUpper("sdadsa", new StringUtil(), StringUtil::printCase);
    }
}

输出

SDADSA
SDADSA

例6-6中,先定义了一个函数式接口Printable,以及一个包含普通方法的StringUtil类,然后再测试类中编写了一个静态方法printUpper()和一个main()方法。最后在main()方法中分别使用Lambda表达式方法引用的方式作为静态方法printUpper()参数进行调用。

7. 异常

7.1 什么是异常

在程序运行过程中,经常会发现一些非正常情况。比如说:程序运行时磁盘空间不足、网络连接中断、被加载的类不存在等,针对这些非正常情况,Java语言引入了异常(Exception),以异常类的形式对这些非正常情况进行封装,并通过异常处理机制对程序运行时发生的各种问题进行处理。

接下来通过一个案例来认识什么是异常,如例7-1所示:
例7-1:Example27.java

public class Example27 {
    //定义一个方法,用于计算两个数的商
    static int divide(int x,int y){
        int result = x/y;   //定义一个变量result记录商
        return result;  //返回结果
    }

    public static void main(String[] args){
        int result = divide(4,0);   //调用divide()方法,计算4/0,抛出ArithmeticException异常
        System.out.println(result);
    }
}

输出

Exception in thread "main" java.lang.ArithmeticException: / by zero
        at Example27.divide(Example27.java:4)
        at Example27.main(Example27.java:9)

例7-1中,定义了一个方法divide(),用于计算两个数的商。然后在main()方法中调用divide()方法,计算4/0。查看结果,可以看出,程序抛出了java.lang.ArithmeticException: / by zero(被0除的算术运算异常)的错误信息。由于程序中的第9行调用divide()方法时,传入的参数y=0,因此程序抛出了ArithmeticException异常。在这个异常发生以后,程序会立即结束,无法继续向下执行。

例7-1中,产生了一个ArithmeticException异常,这个异常只是Java异常类中的一种,Java中提供了大量的异常类,这些类都继承自java.lang.Throwable类。

接下来使用一张图来展示Throwable类的继承体系,如图:
Exception类的层次.jpg

从上图可以看出,Thorwable中有两个直接子类ErrorExceptionError类表示程序中产生的错误,Exception代表程序中产生的异常。

接下来对两个直接子类进行解释说明:

  • Error类:称为错误类,表示Java运行时产生的系统内部错误或资源耗尽的错误,是比较严重的错误,仅靠修改程序本身是不能恢复执行的,如:系统崩溃,虚拟机错误等。
  • Exception类:称为异常类,表示程序本身可以处理的异常。在Java中进行的异常处理,都是针对Exception类及其子类的异常进行处理。在Exception类及其子类中,有一个特殊的RuntimeException类,该类及其子类用于表示运行异常。除了此类,Exception类下所有的其他子类都用于表示编译时异常

已经了解了Throwable类及其子类的基本概念,为了方便后面的学习,现在罗列出Throwable类中的常用方法,如表所示:

方法声明功能描述
String getMessage()返回异常的详细信息,用于描述异常的原因。
void printStackTrace()打印异常的堆栈跟踪信息,用于定位异常发生的位置。
void printStackTrace(PrintStream s)打印异常的堆栈跟踪信息,输出到指定输出流。

7.2 异常的类型

在实际开发中,经常会在程序编译时期产生一些异常,而这些异常必须要进行处理,这种异常被称为编译时期异常,称为checked异常。另一种异常是程序运行时期产生的,这种异常即使不编写异常处理代码,依然可以通过编译,称为unchecked异常。

  1. checked异常(编译时异常)编译时异常的特点是在程序编写过程中,java编译器就会对编写的代码进行检查,如果出现比较明显的异常就必须对异常进行处理,否则程序无法通过编译。

处理编译时异常有两种,具体如下:

  • 使用try-catch语句捕获异常,并进行处理。
  • 使用throws关键字声明异常,并由调用者进行处理。
  1. unchecked异常(运行时异常)运行时异常的特点是程序运行时由java虚拟机自动进行捕获处理的,程序也能编译通过,只是在运行过程中可能报错。

在Java中,常见的运行时异常有多种,如表所示:

异常类名称异常类说明
NullPointerException空指针异常,表示程序试图访问空指针对象。
IndexOutOfBoundsException数组索引越界异常,表示数组访问越界。
ArithmeticException算术异常,表示程序中出现除0等运算错误。
ClassCastException类型转换异常,表示程序试图将对象转换为错误的类型。
NumberFormatException数字格式异常,表示程序中字符串转换为数字时出现错误。

运行时异常一般是由程序中的逻辑错误引起的,程序运行时无法恢复。例如通过数组的下标访问数组的元素时,如果超过了数组的最大下标,就会发生运行时异常,示例如下:

int[] arr = {1, 2, 3};
int num = arr[3]; // 数组下标越界,抛出运行时异常
//由于数组arr的长度为3,最大下标应该为3-1 = 2,因此访问下标3的元素是非法的,程序会抛出数组越界(IndexOutOfBoundsException)的异常。

7.3 try...catch和finally

当程序发生异常时,会立即终止,无法继续向下执行,为了保证程序能够有效的执行,Java中提供了一种对异常进行处理的方式--异常捕获

异常捕获通常使用try...catch语句,具体格式如下:

try{
    //可能产生异常的代码
}catch(异常类名 异常变量名){
    //异常处理代码
}

上述格式中,try{}代码块中包含的是可能发生异常的语句catch{}代码块中编写针对捕获的异常中进行处理的代码。当try{}代码块中的程序发生了异常,系统就会将这个异常的信息封装成一个异常对象,并将这个对象传递给catch{}代码块。catch{}代码块需要一个参数指明它所能够接收的异常类型,这个参数的类型必须是Exception或其子类。

接下来使用try...catch例7-1中出现的异常进行处理,如例7-2所示:

例7-2:Example28.java

public class Example28 {
    //定义一个方法,用于计算两个数的商
    static int divide(int x,int y){
        try{
            int result = x/y;   //定义一个变量result记录商
            return result;  //返回结果
        }catch(Exception e){
            System.out.println("捕获的异常信息为:" + e.getMessage());
        }

        return -1;  //如果出现异常,则返回-1
    }

    public static void main(String[] args){
        int result = divide(4,0);   //调用divide()方法,计算4/0,抛出ArithmeticException异常
        if(result == -1){
            System.out.println("程序出现异常");
        }else{
            System.out.println(result);
        }
    }
}

输出

捕获的异常信息为:/ by zero
程序出现异常

例7-2中,在定义的整数除法运算方法divide()中对可能发生异常的代码用try...catch语句进行了捕获处理。在try{}代码块中发生被0除异常,程序会转而执行catch{}代码块,通过调用Exception类getMessage()方法,将异常信息打印出来。当对异常处理完毕后,程序仍会向下执行,而不会因为异常而终止执行

tip:在try{}代码块中,发生异常语句后的代码是不会被执行的,上述例子中return result; //返回结果就没有执行

有时候希望有些语句无论程序是否发生异常都会执行,这就需要在try...catch语句后,加一个finally{}代码块。finally{}代码块中的代码无论是否发生异常都会执行,一般用于释放资源、关闭流等。

接下来对例7-2进行修改,演示finally{}代码块的使用,如例7-3所示:

例7-3:Example29.java

public class Example29 {
    //定义一个方法,用于计算两个数的商
    static int divide(int x,int y){
        int result = 0;
        try{
            result = x/y;   //定义一个变量result记录商
            return result;  //返回结果
        }catch(Exception e){
            System.out.println("捕获的异常信息为:" + e.getMessage());
        }finally{
            System.out.println("finally块中的代码无论是否发生异常都会执行");
        }    
        return -1;  //如果出现异常,则返回-1
    }

    public static void main(String[] args){
        int result = divide(4,0);   //调用divide()方法,计算4/0,抛出ArithmeticException异常
        if(result == -1){
            System.out.println("程序出现异常");
        }else{
            System.out.println(result);
        }
    }
}

输出

捕获的异常信息为:/ by zero
finally块中的代码无论是否发生异常都会执行
程序出现异常

例7-3中,divide()方法中增加了一个finally{}代码块,用于处理无论程序是否发生异常都要执行的语句,该代码块并不受return语句影响,即使程序发生异常,finally{}代码块中的代码也会执行。在程序设计时,经常会在try...catch语句后面加上finally{}代码块来完成必须做的事情,例如用于释放资源、关闭流、关闭线程池等。

tipfinally{}代码块中的代码无论是否发生异常都会执行,但是在一种情况下是不会执行的,那就是在try...catch中执行了System.exit(0)语句。System.exit(0)语句表示退出当前的Java虚拟机,Java虚拟机停止了,任何代码都不能再执行

7.4 throws关键字和throw关键字

在上面的例子中,定义除法运算时,通常会意识到可能出现的异常,可以直接通过try...catch语句进行异常捕获,但有些时候,方法中代码是否会出现异常,我们并不明确或者并不着急处理,这时就可以使用throws关键字声明该方法可能抛出的异常,并由调用者进行处理。

7.4.1 throws关键字

在Java中,将异常抛出需要使用throws关键字,throws关键字后面可以跟多个异常类,表示该方法可能抛出的异常类型。调用该方法的地方,需要对可能发生的异常进行捕获处理。

基本语法格式如下:

[修饰符] 返回值类型 方法名([参数列表]) throws 异常类1, 异常类2, 异常类3,....{
    //方法体
}

从上面的语法格式中可以看出,throws关键字需要写在方法声明的后面,throws后面需要声明方法中发生异常的类型,通常将这种做法称为为方法声明抛出一个异常。

接下来对例7-1进行修改,在divide()方法上声明抛出异常,演示throws关键字的使用,如例7-4所示:

例7-4:Example30.java

public class Example30 {
    //定义一个方法,用于计算两个数的商
    static int divide(int x,int y) throws Exception{
        int result = x/y;   //定义一个变量result记录商
        return result;  //返回结果
    }

    public static void main(String[] args){
        int result = divide(4,0);   //调用divide()方法
        System.out.println(result);
    }
}

输出

Example30.java:9: 错误: 未报告的异常错误Exception; 必须对其进行捕获或声明以便抛出
        int result = divide(4,0);   //调用divide()方法
                           ^
1 个错误

例7-4中,在调用divide()方法时候,由于该方法声明时抛出了异常,因此在调用divide()方法时,必须对Exception异常进行捕获处理,否则编译器会报错。从输出结果来看,程序编译时发生了Unhandled exception type Exception,表示程序中未处理的异常。这时,有两种快速解决的方案,分别是:

  1. Add throws declaration表示在方法上继续使用throws关键字抛出异常,接下来对例7-4进行修改,如例7-5所示:

例7-5:Example31.java

public class Example31 {
    //定义一个方法,用于计算两个数的商
    static int divide(int x,int y) throws Exception{
        int result = x/y;   //定义一个变量result记录商
        return result;  //返回结果
    }

    public static void main(String[] args) throws Exception{
        int result = divide(4,0);   //调用divide()方法
        System.out.println(result);
    }
}

输出

Exception in thread "main" java.lang.ArithmeticException: / by zero
        at Example31.divide(Example31.java:4)
        at Example31.main(Example31.java:9)

例7-5中,在调用divide()方法时,继续使用了throws关键字将异常抛出,从输出结果可以看出,虽然编译没有问题,但是运行时由于没有对/by zero异常进行处理,最终导致程序终止运行。

  1. Surround with try/catch表示在出现异常的代码位置使用try/catch语句进行捕获处理,接下来对例7-4进行修改,如例7-6所示:

例7-6:Example32.java

public class Example32 {
    //定义一个方法,用于计算两个数的商
    static int divide(int x,int y) throws Exception{
        int result = x/y;   //定义一个变量result记录商
        return result;  //返回结果
    }

    public static void main(String[] args){
        try{
            int result = divide(4,0);   //调用divide()方法
                        System.out.println(result);
        }catch(Exception e){
            System.out.println("捕获的异常信息为:" + e.getMessage());
        }
    }
}

输出

捕获的异常信息为:/ by zero

例7-6中,在main()方法中,使用try...catch语句对divide()方法的调用进行捕获处理,并打印出异常信息。从输出结果可以看出,程序正常运行,并打印出了异常信息。

7.4.2 throw关键字

除了可以通过throws关键字声明方法可能抛出的异常,还可以通过throw关键字抛出一个异常。throw关键字用于方法体内,并且抛出的是一个异常类对象。而throws关键字用于方法声明,用于声明方法可能抛出的异常类型

通过throw关键字抛出一个异常,还需要使用throws关键字或try...catch语句进行捕获处理。需要注意的是,如果throw抛出的是ErrorRuntimeException或它们的子类异常对象,则无需使用throws关键字或try...catch语句进行捕获处理,因为这类异常是不可查的,程序运行时会直接终止。

使用throw关键字抛出异常的语法格式如下:

[修饰符] 返回值类型 方法名([参数列表]) throws 异常类1, 异常类2, 异常类3,....{
    //方法体
    throw new 异常类对象([参数列表]);
}

接下来通过一个案例来演示throw关键字的使用,如例7-7所示:

例7-7:Example33.java

public class Example33 {
    //定义一个方法,输出年龄
    static void printAge(int age) throws Exception{
        if(age < 0){
            throw new Exception("年龄不能为负数,必须为正整数");
        }else{
            System.out.println("年龄为:" + age);
        }
    }

    public static void main(String[] args){
        try{
            printAge(-10);   //调用printAge()方法
        }catch(Exception e){
            System.out.println("捕获的异常信息为:" + e.getMessage());
        }
    }
}

输出

捕获的异常信息为:年龄不能为负数,必须为正整数

例7-7中,定义了一个printAge()方法,用于输出年龄,并在方法中对输入的内容进行判断,当数值小于0时,使用throw抛出异常,并指定异常提示信息,同时在方法后继续用throws关键字处理抛出的异常。

7.5 自定义异常

在实际开发中,我们经常会遇到一些特殊的异常,比如输入参数不合法、网络连接失败等,这些异常并不是由系统抛出的,而是需要我们自己定义并抛出。需要注意的是,自定义的异常类必须继承自Exception或其子类

接下来通过一个案例来学习如何自定义异常,如例7-8所示:

例7-8:Example34.java

public class Example34 {
    //定义一个自定义异常类
    static class MyException extends Exception{
        public MyException(String message){
            super(message); //调用Exception有参的构造方法
        }
    }

    //定义一个方法,输出年龄
    static void printAge(int age) throws MyException{
        if(age < 0){
            throw new MyException("年龄不能为负数,必须为正整数");
        }else{
            System.out.println("年龄为:" + age);
        }
    }

    public static void main(String[] args){
        try{
            printAge(-10);   //调用printAge()方法
        }catch(MyException e){
            System.out.println("捕获的异常信息为:" + e.getMessage());
        }
    }
}

输出

捕获的异常信息为:年龄不能为负数,必须为正整数

例7-8中,定义了一个MyException类,继承自Exception类,并重写了Exception类的构造方法,用于接收异常信息。在printAge()方法中,使用throw关键字抛出了一个MyException对象,并指定异常提示信息。在main()方法中,使用try...catch语句对printAge()方法的调用进行捕获处理,并打印出异常信息。

8. 垃圾回收

在Java中,当一个对象称为垃圾后仍会占用内存空间,时间一长,就会导致内存空间不足。针对这种情况,Java引入了垃圾回收机制(Java GC)。有了这种机制,就不需要再过多关心对象回收的问题,Java虚拟机会自动回收垃圾对象所占用的内存空间。在Java中,垃圾回收机制是自动管理内存的一种机制,它可以自动释放不再使用的内存,以便为程序提供更大的内存空间。

当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以分为三种:

  1. 可用状态:当一个对象被创建后,如果有一个以上的引用变量引用它,则它处于可用状态
  2. 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,则它处于可恢复状态。这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象finalize()方法进行资源清理。如果系统在调用finalize()方法前重新使用引用变量引用该对象,则该对象会被重新标记为可用状态,否则进入不可用状态
  3. 不可用状态:当对象失去了所有引用变量的关联,且系统已经调用所有对象的finalize()方法后,这个对象将永久地失去引用,变成不可用状态。只有当一个对象处于不可用状态时,系统才会回收它所占用的内存空间。

一个对象在彻底失去引用成为垃圾后会暂时的保留在内存中,当这样的垃圾堆积的一定程度时,Java虚拟机就会启动垃圾回收器将这些垃圾对象从内存中释放。虽然程序可以控制一个对象何时不再被任何引用变量所引用,但是却无法精准地控制Java垃圾回收的机制。除了等待Java虚拟机进行自动垃圾回收外,还可以通过两种方法强制系统进行垃圾回收

  1. 调用System类的gc()静态方法System.gc()方法可以调用Java虚拟机的垃圾回收器进行垃圾回收。但是,调用该方法并不一定会立即释放内存空间,因为垃圾回收器的执行过程是由系统自动进行的。
  2. 调用Runtime对象的gc()实例方法Runtime.getRuntime().gc()方法可以调用Java虚拟机的垃圾回收器进行垃圾回收。该方法立即释放内存空间,但它也不保证一定会立即释放所有的垃圾对象,因为垃圾回收器的执行过程是由系统自动进行的。

当一个对象在内存中被释放时,它的finalize()方法会被自动调用,finalize()方法是定义在Object类中的实例方法,其方法原型如下:

protected void finalize() throws Throwable {}

接下来通过一个案例来演示Java虚拟机进行垃圾回收的过程,如例8-1所示:

例8-1:Example35.java

class Person{
    public void finalize(){
        System.out.println("Person对象被作为垃圾回收了");
    }
}


public class Example35 {
    //1.演示一个不通知强制垃圾回收的方法
    public static void recyclegWaste1(){
        Person p1 = new Person();
        p1 = null;
        int i = 1;
        while(i++<10){
            System.out.println("方法1循环中...");
        }
    }

    //2.演示一个通知强制垃圾回收的方法
    public static void recyclegWaste2(){
        Person p2 = new Person();
        p2 = null;
        //通知垃圾回收器进行强制垃圾回收
        System.gc();
        //Runtinme.getRuntime().gc();
        int i = 1;
        while(i++<10){
            System.out.println("方法2循环中...");
        }
    }
    public static void main(String[] args) {
        //分别调用两个模拟演示垃圾回收的方法
        recyclegWaste1();
        System.out.println("===========");
        recyclegWaste2();
    }
}

输出

方法1循环中...
方法1循环中...
方法1循环中...
方法1循环中...
方法1循环中...
方法1循环中...
方法1循环中...
方法1循环中...
方法1循环中...
===========
方法2循环中...
Person对象被作为垃圾回收了
Person对象被作为垃圾回收了
方法2循环中...
方法2循环中...
方法2循环中...
方法2循环中...
方法2循环中...
方法2循环中...
方法2循环中...
方法2循环中...

例8-1中,定义了一个Person类,并重写了finalize()方法,用于在对象被回收时打印出提示信息。

recyclegWaste1()方法中,创建了一个Person对象,并将其引用变量设置为null,然后在一个while循环中打印提示信息,模拟程序运行过程。

recyclegWaste2()方法中,创建了一个Person对象,并将其引用变量设置为null,然后调用System.gc()方法通知Java虚拟机进行垃圾回收,并在一个while循环中打印提示信息,模拟程序运行过程。

main()方法中,分别调用recyclegWaste1()recyclegWaste2()方法,模拟程序运行过程。

从输出结果可以看出,recyclegWaste1()方法中,程序运行正常,Person对象被回收后,提示信息打印正常。

recyclegWaste2()方法中,程序运行正常,Person对象被回收后,提示信息打印正常。

虽然recyclegWaste2()方法中调用了System.gc()方法通知Java虚拟机进行垃圾回收,但是并不保证一定会立即释放Person对象所占用的内存空间,因为Java虚拟机的垃圾回收机制是自动进行的。

tip:Java垃圾回收的finalize()已经过时。finalize() 方法在 Java 中被用于执行在对象被垃圾收集器收集之前需要完成的清理工作。然而,自从 Java 9 起,finalize() 方法已经被标记为 @Deprecated(不推荐使用),并且在未来的版本中可能会被移除。

原因:

  1. finalize() 方法的调用不确定,即不确定何时对象会被垃圾收集器收集,也不确定收集时会以何种顺序进行。
  2. 它通常不适合用于清理资源,因为它的执行代价高昂,可能会导致性能问题。
  3. 在 finalize() 方法中抛出的任何异常都会被忽略,这可能会导致资源泄露。
  4. 使用 finalize() 进行资源清理不是线程安全的,可能会引起竞态条件。

解决方法:

  1. 使用 try-with-resources 或者自定义清理方法实现 AutoCloseable 接口,在 try 块结束后自动清理资源。

  2. 使用 java.lang.ref.Cleaner 类来注册清理函数,这是一个较为低级别的工具,可以在对象被垃圾收集器准备回收时执行清理动作。

  3. 如果必须使用 finalize(),应该尽量简化其实现,并确保它不会阻塞垃圾收集器的工作,避免影响系统性能。

如例:

public class ExampleResource implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();
    private static final ReferenceQueue<ExampleResource> q = new ReferenceQueue<>();
 
    @Override
    public void close() {
        // 清理资源的逻辑
    }
 
    public static void useResource() {
        ExampleResource resource = new ExampleResource();
        try {
            // 使用资源
        } finally {
            resource.close(); // 显式关闭资源
        }
    }
}

在这个示例中,我们使用了 AutoCloseable 接口和 try-with-resources 语句来管理资源,避免了使用 finalize() 方法。这是一个更加推荐的做法,因为它更加显式、可控且有助于避免潜在的资源泄露问题。