第四章 面向对象(下)
4.1 类的继承
4.1.1 继承的概念
- 在程序中,继承描述的是事物之间的所属关系。
- 在Java中,类的继承是指在一个现有类的基础上去构建一个新的类,构建出来的新类被称作子类,现有类被称作父类或基类。子类会自动拥有父类所有可继承的属性和方法。
- 子类与父类之间要使用extends关键字实现继承关系。
基本语法格式:
[修饰符] class 子类名 extends 父类名 {
//程序核心代码
}
- 在Java中,类只支持单继承,不允许多重继承,也就是说一个类只能有一个直接父类。
- 多个类可以继承同一个父类。
- 在Java中,多层继承是允许的,即一个类的父类可以再去继承另外的父类。
- 在Java中,子类和父类是一种相对的概念,即一个类是某个类父类的同时,也可以是另一个类的子类。
4.1.2 重写父类方法
- 继承关系中,子类会自动继承父类中公共的方法。
- 子类中重写的方法需要和父类被重写的方法具有相同的方法名、参数列表以及返回值类型。
- 子类重写父类方法时,不能使用比父类中被重写的方法更严格的访问权限。如父类中的方法访问权限是public,子类重写父类该方法的访问权限就不能是private。
4.1.3 super关键字
- 使用super关键字调用父类的成员变量和成员方法
- 使用super关键字调用父类的构造方法
- 通过super调用父类构造方法的代码必须位于子类构造方法的第一行,并且只能出现一次,否则程序在编译期间就会报错
- 在子类的构造方法中一定会调用父类的某个构造方法
- 在定义一个类时,如果没有特殊需求,当定义了有参构造方法后,尽量在类中再显式地定义一个无参构造方法,这样可以避免该类被继承时出现错误
4.1.4 Object类
是所有类的父类,即每个类都直接或间接继承自该类,因此Object类通常被称为超类、基类或根类。
方法声明 | 功能描述 |
boolean equals(Object obj) | 判断某个对象与此对象是否相等 |
final Class<?> getClass() | 返回此Object的运行时类 |
int hashCode() | 返回该对象的哈希码值 |
String toString() | 返回该对象的字符串表示 |
void finalize() | 垃圾回收器调用此方法来清理没有被任何引用变量所引用对象的资源 |
4.2 final关键字
final关键字可用于修饰类、变量和方法
- final修饰的类不能被继承
- final修饰的方法不能被子类重写
- final修饰的变量(成员变量和局部变量)是常量,只能赋值一次
4.2.1 final关键字修饰类
被final关键字修饰的类为最终类,不能被其他类继承,不能够派生子类。
4.2.2 final关键字修饰方法
被final关键字修饰的方法为最终方法,子类不能对该方法进行重写。
4.2.3 final关键字修饰变量
- 被final修饰的变量为常量,它只能赋值一次,其值不可改变。
- 当局部变量使用final关键字进行修饰时,可以在声明变量的同时对变量进行赋值,也可以先声明变量然后再进行有且只有一次的赋值。而当成员变量被final修饰时,在声明变量的同时必须进行初始化赋值,否则编译报错。
4.3 抽象类和接口
4.3.1 抽象类
抽象方法必须使用abstract关键字来修饰,并且在定义方法时不需要实现方法体。当一个类中包含了抽象方法,那么该类也必须使用abstract关键字来修饰,这种使用abstract关键字修饰的类就是抽象类。
抽象类及抽象方法定义的基本语法格式如下:
//定义抽象类
[修饰符] abstract class 类名 {
//定义抽象方法
[修饰符] abstract 方法返回值类型 方法名 ([参数列表]);
//其他方法或属性
}
包含抽象方法的类必须定义为抽象类,但抽象类中可以不包含任何抽象方法。另外,抽象类是不可以被实例化的,因为抽象类中有可能包含抽象方法,抽象方法是没有方法体的,不可以被调用。如果想调用抽象类中定义的抽象方法,需要创建一个子类,在子类中实现抽象类中的抽象方法。
4.3.2 接口
- 接口是一种特殊的抽象类,它不能包含普通方法,其内部的所有方法都是抽象方法,它将抽象进行得更为彻底。
- JDK8中,接口中除了抽象方法外,还可以有默认方法和静态方法(类方法),都允许有方法体。
- 定义接口时,使用interface关键字来声明。
[修饰符] interface 接口名 [extends 父接口 1,父接口 2,…] {
[public][static][final] 常量类型 常量名 = 常量值 ;
[public][abstract] 方法返回值类型 方法名([参数列表]);
[public] default 方法返回值类型 方法名 ([参数列表]) {
//默认方法的方法体
}
[public] static 方法返回值类型 方法名 ([参数列表]) {
//静态方法的方法体
}
}
- 定义一个接口时,可以同时继承多个父接口,解决了类的单继承限制。在接口内部可以定义多个常量和抽象方法,定义常量时必须进行初始化赋值。
- 接口中可以包含三类方法:抽象方法、默认方法和静态方法,其中静态方法可以通过“接口名.方法名”的形式来调用,而抽象方法和默认方法只能通过接口实现类的实例对象来调用。
- 定义一个接口的实现类需要使用implements关键字实现当前接口,并实现接口中的所有抽象方法。
- 一个类可以在继承另一个类的同时实现多个接口,并且多个接口之间需要使用英文逗号(,)分隔。
[修饰符] class 类名 [extends 父类名] [implements 接口1,接口2,…] {
…
}
接口与接口之间也可以是继承关系(extends)
- 在JDK8之前,接口中的方法都必须是抽象的,并且方法不能包含方法体。在调用抽象方法时,必须通过接口的实现类的对象才能调用实现方法。JDK8后,接口中的方法除了包含抽象方法外,还包含默认方法和静态方法(这两者可以有方法体),并且静态方法可以通过“接口.方法名”来调用。
- 当一个类实现接口时,如果这个类是抽象类,只需实现接口中的部分抽象方法即可,否则需要实现接口中的所有抽象方法。
- 一个类可以通过implements关键字同时实现多个接口,被实现的多个接口之间要用英文逗号(,)隔开。
- 接口之间可以通过extends关键字实现继承,并且一个接口可以同时继承多个接口,接口之间用英文逗号隔开。
- 一个类在继承一个类的同时还可以实现接口,此时,extends关键字必须位于implements关键字之前。
4.4 多态
4.4.1 多态概述
- 在Java中,多态是指不同类的对象在调用同一个方法时所呈现的多种不同行为。
- 在一个类中定义的属性和方法被其他类继承或重写后,当把子类对象直接赋值给父类引用变量时,相同引用变量类型的变量调用同一个方法将呈现多种不同形态。
- 通过多态,消除了类之间的耦合关系,大大提高了程序的可扩展性和可维护性。
- Java的多态性是由类的继承、方法重写以及父类引用指向子类对象体现的。
4.4.2 对象的类型转换
- 将子类对象当作父类类型使用,即“向上转型”.
如: Animal an = new Cat(); //将Cat类对象当做Animal类型来使用
将子类对象当作父类使用时不需要任何显式声明,此时不能通过父类变量去调用子类特有的方法.
- 将本质为子类类型的对象由父类类型强转为子类类型,即“向下转型”.
- instanceof关键字可以判断一个对象是否为某个类(或接口)的实例或者子类实例.
4.5 内部类
在Java中,允许在一个类的内部定义类,这样的类称作内部类,这个类所在的类称作外部类。实际情况中,根据内部类的位置、修饰符和定义方式的不同,内部类可以分为四种形式:成员内部类、局部内部类、静态内部类和匿名内部类。
4.5.1 成员内部类
- 在类中定义的一个类被称为成员内部类。
- 成员内部类可以访问外部类所有成员,同时外部类也可以访问成员内部类的所有成员。
- 要操作内部类中的成员,需要通过外部类对象创建的内部类对象。
创建内部类对象的语法格式:
外部类名.内部类名 变量名 =new 外部类名().new 内部类名();
4.5.2 局部内部类
局部内部类是定义在方法中的类。
局部内部类可以访问外部类的所有成员变量和方法,而局部内部类中的变量和方法却只能在创建该局部内部类的方法中进行访问。
4.5.3 静态内部类
静态内部类,就是使用static关键字修饰的成员内部类。它只能访问外部类的静态成员,同时通过外部类访问静态内部类成员时,可以跳过外部类从而直接通过内部类访问静态内部类成员。
创建静态内部类对象的基本语法格式:
外部类名.静态内部类名 变量名 = new 外部类名.静态内部类名();
4.5.4 匿名内部类
匿名内部类就是没有名称的内部类,在调用包含接口类型参数的方法时,可以通过匿名内部类的形式传入一个接口类型参数,在匿名内部类中直接完成方法的实现。
JDK8前,局部变量前必须加final修饰符,JDK8开始,允许在局部内部类、匿名内部类中访问非final修饰的局部变量。
4.6 JDK8的Lambda表达式
4.6.1 Lambda 表达式入门
- Lambda表达式只针对有一个抽象方法的接口实现。
- 一个Lambda表达式由三个部分组成,分别为参数列表、“->”和表达式主体,其语法格式如下:
([数据类型 参数名,数据类型 参数名,…]) -> {表达式主体}
([数据类型 参数名,数据类型 参数名,…]):如果只有一个参数时,可以省略参数列表的括号();
->:表示Lambda表达式箭牌,用来指定参数列表数据指向,不能省略,且必须用英文横线和大于号书写;
{表达式主体}:由单个表达式语句或语句块组成的主体,本质就是接口中抽象方法的具体实现,如果表达式主体只有一条语句,那么可以省略包含主体的大括号。其中允许有返回值,当只有一条return语句时,也可以省略该关键字。
4.6.2 函数式接口
函数式接口即有且只有一个抽象方法的接口。
JDK8中,专门为函数式接口引入了一个@FunctionalInterface注解,该注解只是显示地标注了接口是一个函数式接口,并强制编辑器进行更严格的检查,确保该接口是函数式接口,否则会编译报错,对程序运行并无实质影响。
4.6.3 方法引用与构造器引用
Lambda表达式对普通方法和构造方法的引用形式
种类 | Lambda表达式示例 | 对应的引用示例 |
类名引用普通方法 | (x,y,…)->对象名x.类普通方法名(y,…) | 类名::类普通方法名 |
类名引用静态方法 | (x,y,…)->类名.类静态方法名(y,…) | 类名::类静态方法名 |
对象名引用方法 | (x,y,…)->对象名.实例方法名(y,…) | 对象名::实例方法名 |
构造器引用 | (x,y,…)->new 类名(x,y,…) | 类名::new |
1.类名引用静态方法
/**
*通过一个求绝对值的案例来演示类名引用静态方法的使用。
*/
//定义一个函数式接口
@FunctionalInterface
interface Calcable {
int calc(int num);
}
//定义一个类,并在类中定义一个静态方法
class Math {
//定义一个求绝对值的方法
public static int abs(int num) {
if(num < 0) {
return -num;
}else {
return num;
}
}
}
//定义测试类
public class Example{
private static void printAbs(int num,Calcable calcable) {
System.out.println(calcable.calc(num));
}
public static void main(String[] args) {
//使用Lambda表达式方式
printAbs(-10,n->Math.abs(n));
//使用方法引用的方式
printAbs(-10,Math::abs);
}
}
2.对象名引用方法
/**
*通过一个返回字符串所有字母大写的案例来演示对象名引用方法的使用。
*/
//定义一个函数式接口
@FunctionalInterface
interface Printable {
void print(String str);
}
class StringUtils {
public void printUpperCase(String str) {
System.out.println(str.toUpperCase());
}
}
//定义测试类
public class Example{
private static void printUpper(String text,Printable pt) {
pt.print(text);
}
public static void main(String[] args) {
StringUtils stu = new StringUtils();
//使用Lambda表达式方式
printUpper("Hello",t->stu.printUpperCase(t));
//使用方法引用的方式
printUpper("Hello",stu::printUpperCase);
}
}
3.构造器引用方法
/**
*通过一个构造方法获取属性的案例来演示构造器引用方法的使用。
*/
//定义一个函数式接口
@FunctionalInterface
interface PensonBuiler {
Person buildPerson(String name);
}
//定义一个Person类,并添加有参构造方法
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
//定义测试类
public class Example{
private static void printName(String name,PersonBuilder builder) {
System.out.println(builder.buildPerson(name).getName());
}
public static void main(String[] args) {
//使用Lambda表达式方式
printName("kk爱闹",name->new Person(name));
//使用方法引用的方式
printName("kk爱闹",Person::new);
}
}
4.类名引用普通方法
/**
*通过一个返回字符串所有字母大写的案例来演示类名引用普通方法的使用。
*/
//定义一个函数式接口
@FunctionalInterface
interface Printable {
void print(StringUtils su,String str);
}
class StringUtils {
public void printUpperCase(String str) {
System.out.println(str.toUpperCase());
}
}
//定义测试类
public class Example{
private static void printUpper(StringUtils su,String text,Printable pt) {
pt.print(su,text);+
}
public static void main(String[] args) {
//使用Lambda表达式方式
printUpper(new StringUtils(),"Hello",(object,t)->object.printUpperCase(t));
//使用方法引用的方式
printUpper(new StringUtils(),"Hello",StringUtils::printUpperCase);
}
}
4.7 异常
4.7.1 什么是异常
针对非正常情况,Java语言中引入了异常,以异常类的形式对这些非正常情况进行封装,并通过异常处理机制对程序运行时发生的各种问题进行处理。
Throwable有两个直接子类Error和Exception。
Error类称为错误类,它表示Java运行时产生的系统内部错误或资源耗尽的错误,是比较严重的,仅靠修改程序本身是不能恢复执行的,如系统崩溃,虚拟机错误等。
Exception类称为异常类,它表示程序本身可以处理的错误。它有一个特殊的RuntimeException类,该类及其子类用于表示运行时异常。除了此类,Exception类下所有其他的子类都用于表示编译时异常。
Throwable常用方法
方法声明 | 功能描述 |
String getMessage() | 返回此throwable的详细消息字符串 |
void printStackTrace() | 将此throwable及其追踪输出至标准错误流 |
void printStackTrace(PrintStream s) | 将此throwable及其追踪输出到指定的输出流 |
4.7.2 异常的类型
- 编译时异常(checked异常)
其特点是在程序编写过程中,Java编译器就会对编写的代码进行检查,如果出现比较明显的异常就必须对异常进行处理,否则程序无法通过编译。
处理编译时异常有两种方式,具体如下:
- 使用try…catch 语句对异常进行捕获处理。
- 使用throws关键字声明抛出异常,让调用者对其处理。
- 运行时异常(unchecked异常)
运行时异常是在程序运行时由Java虚拟机自动进行捕获处理的,即使没有使用try…catch语句捕获或使用throws关键字声明抛出,程序也能编译通过,只是在运行过程中可能报错。
常见运行时异常
异常类名称 | 异常类说明 |
ArithmeticException | 算术异常 |
IndexOutOfBoundsException | 角标越界异常 |
ClassCastException | 类型转换异常 |
NullPointerException | 空指针异常 |
NumberFormatException | 数字格式化异常 |
运行时异常一般是由于程序中的逻辑错误引起的,在程序运行时无法恢复。
4.7.3 try…catch 和 finally
异常捕获能够保证程序在出现异常时能够继续向下执行。
try…catch 的语法格式:
try {
//可能发生异常的语句
}catch(Exception 类或其子类 e) {
//对捕获的异常进行相应处理
}
在try{}代码块中,发生异常语句后面的代码是不会被执行的。
在程序中,有时会希望有些语句无论程序是否发生异常都要执行,这时候可以在try…catch语句后面再加一个finally{}代码块。例如释放系统资源、关闭线程池等。
如果在try…catch语句中执行了System.exit(0)语句,finally语句就不会执行。
System.exit(0)表示退出当前的Java虚拟机,Java虚拟机停止了,任何代码都不能再执行了。
4.7.4 throws 关键字
在Java中,将异常抛出需要使用throws关键字来实现,该关键字用在会抛出异常的方法名称后,同时支持一次性抛出多种类型的异常,基本语法格式如下:
[修饰符] 返回值类型 方法名 ([参数类型 参数名 1…]) throws 异常类1,异常类2,… {
//方法体…
}
当调用者在调用有抛出异常的方法时,除了可以在调用的程序中直接进行try…catch异常处理外,也可以根据提示使用throws关键字继续抛出异常,这样程序也能编译通过。但是,程序发生了异常,终究是需要进行处理的,如果没有被处理,程序就会非正常终止。
4.7.5 throw关键字
throw与throws
- throw用于方法体内,并且抛出的是一个异常类对象,而throws关键字用在方法声明中,用来指明方法可能抛出的多个异常。
- 通过throw关键字抛出异常后,还需要使用throws关键字或try…catch对异常进行处理。如果抛出的是Error、RuntimeException或它们的子类异常对象,则无须使用throws关键字或try…catch对异常进行处理。
[修饰符] 返回值类型 方法名 ([参数类型 参数名,…]) throws 抛出的异常类 {
//方法体…
throw new Exception 类或其子类构造方法;
}
throw关键字除了可以抛出代码的逻辑性异常外,也可以抛出Java能够自动识别的异常。
4.7.6 自定义异常
通常开发过程中会遇到特有的异常情况,我们可以自定义异常,但必须继承自Exception或其子类。
4.8 垃圾回收
垃圾回收机制(Java GC)
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以分为可用状态、可恢复状态和不可用状态三种。
对象的状态转换
除了等待Java虚拟机进行自动垃圾回收外,还可以通过如下两种方式强制系统进行垃圾回收。
- 调用System类的gc()静态方法:System.gc()
- 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()
当一个对象在内存中被释放时,它的finalize()方法会被自动调用,finalize()方法是定义在Object类中的实例方法,其方法原型是:
protected void finalize() throws Throwable { }