9. 面向对象-继承

内容导视:

包机制封装继承多态顶级父类 Object9.1 包机制

内容导视:

包的介绍包的命名规则导入类有可能遇见的错误打 jar 包

求职:软件测试实习生等其它岗位,愿意从 0 开始,你好世界。

9.1.1 包的介绍

那么多类,如果我创建的类刚好与导入的类的名字一样怎么办?如我经常创建 Test 类测试 demo 代码(因为方便,主要也想不起来什么别的好名字了),那么多的 Test 类如何区分?

为了解决上述问题,Java 引入了包(package)机制,提供了类的多层命名空间,用于解决类的命名冲突、类文件管理等问题。

包允许将类组合成较小的单元(类似文件夹),它基本上隐藏了类,并避免了名称上的冲突。包允许在更广泛的范围内保护类、数据和方法。你可以在包内定义类,而在包外的代码不能访问该类。这使你的类相互之间有隐私,但不被其他世界所知。

有时,开发人员还可能需要将处理同一方面的问题的类放在同一个包下,以便于管理。

比如关于我创建的有序集合类都放在 com.cqh.list 下,本质是把类放在了模块的绝对路径/src/com/cqh/list 下。

包的作用如下:

区分相同名称的类。能够较好地管理大量的类。控制访问范围。9.1.2 包的命名规则

命令规则

只能包含数字、字母、下划线、小圆点。不能以数字开头,不能是关键字,反例如下:

com.12abc,错,不能以数字开头 com.class.abc,错,class是关键字

命名规范

因为域名是唯一的,所以以域名的倒序作为包名可以确保唯一性。

小写字母+小圆点

com.公司名.项目名.业务模块名.功能名

如 com.baidu.egov.system.utils 是 egov 项目的系统模块的工具类所在包名。

java 中常用的包

java.lang.*,lang 包是基本包,默认导入,不需要 import,如 Math、System、String...

java.util.*,util 包下的类主要是常用的工具类,如 Scanner、Arrays...

java.net.*,网络包,用于网络开发

java.awt.*, 图形用户界面开发,简称 GUI

java.swing.*,对 awt 的改良

实际使用

创建好目录,比如 com/cqh,在 cqh 目录下创建 .java 源文件,在首行加入

package com.cqh;

package 是一个关键字,声明本类所在包;package 语句只能写在 java 源代码的 import 和类定义的上面,最多只能写一句;不写是默认包。

package com.cqh; class Test { public static void main(String[] args) { out.println("Hello World!"); } }9.1.3 导入类

如 Scanner 在 java.util 下,那么 Scanner 的完整类名(全类名,也称全限定名)为 java.util.Scanner,即所在包+类名,那么 Scanner 是简单类名。

导入单一类

首行加上 import 此类的完整类名;

import java.util.Scanner; class Test { Scanner scanner; }

导入某包下的所有类

import 包名.*;

import java.util.*; class Test { Scanner scanner; Random random; Arrays arrays; }

import java.util.*,导入 util 下的所有类,但不包括 util 的子目录下的所有类

导入某类下的静态变量

import static 此类的完整类名.静态变量名

import static java.lang.System.out; class Test { public static void main(String[] args) { out.println("Hello World!"); } }

细节

只能导入被 public 修饰的类同一个包下的类直接使用,不需要导入import 放在 package 下,类定义上,可以有多句,没有顺序要求不可导入多个简单类名相同的类,其余简单类名相同的类,只能写完整类名

package com.cqh; // 导入了 junit 下的 Test import org.junit.Test; class Hello { public static void main(String[] args) { // 想要使用其它包下的 Test 类,就只能写完整类名 com.cqh.arr.Test test = new com.cqh.arr.Test(); // 如果使用简单类名,会被当作是 org.junit.Test System.out.println(Test.class.getName());// org.junit.Test System.out.println(com.cqh.arr.Test.class.getName());// com.cqh.arr.Test } }9.1.4 可能遇见的错误

错误:找不到或无法加载主类

通过类路径+完整类名可以定位类的位置,如 E:\cqh_demo01-javaSE\package\src\com\cqh\Scanner.class,这个 class 类的完整类名如果是 com.cqh.Scanner,则 E:\cqh_demo\01-javaSE\package\src 就是它的类路径。

运行时,要指定完整类名,如 java com.cqh.Scanner,类路径默认是当前路径。他会在当前路径\com\cqh 下找 Scanner.class。

而通常会 cd 进入E:\cqh_demo\01-javaSE\package\src\com\cqh 也就是 class 文件所在目录,再使用这个运行命令,相当于在 E:\cqh_demo\01-javaSE\package\src\com\cqh\com\cqh 找 Scanner 类,一定找不到,除非你编译时,使用 -d 参数指定把生成的 class 文件放在当前目录。

几种解决方法

跳转至类路径,输入命令 java 完整类名加 -classpath 或 -cp 参数,指定类路径

java -cp E:\cqh_demo\01-javaSE\package\src com.cqh.Scanner配置环境变量 classpath,重启 DOS 窗口,输入 java 完整类名. 代表当前路径,如果省去了,就不会在当前路径下寻找。

找不到符号,程序包 xxx.xxx 不存在

编译比较简单,可以跳转至 java 文件所在目录,直接 javac 源文件名.java 就可以在当前目录下生成 class 文件;或者运行时为了免去切换目录的麻烦,直接在类路径下输入 javac com/cqh/Scanner.java,注意编译时路径以斜杆分隔,而不是 . 号。

或者指定类路径 javac -cp E:\cqh_demo\01-javaSE\package\src Scanner.java

此外源文件,要保证包名与实际目录位置一致,如 M 的确在 com/cqh/util 目录下;

package com.cqh.util; public class M {}

编写的 Test 类导入 M 类时

package com.cqh; import com.cqh.util.M; public class Test { public static void main(String[]args) {} public void use(){ new M(); } }

编译 Test 类时,会在当前路径的 com/cqh/util 寻找 M,如果你是跳转到 Test.java 所在目录 E:\cqh_demo\01-javaSE\package\src\com\cqh 进行编译,那肯定找不到,要跳转至类路径下,javac com/cqh/Test.java。

9.1.5 打 jar 包

目录层次结构,如 out、src 同级别,在同一个目录下。com 比 src 低一个级别,是 src 的子目录。

out src com cqh Test.java

jar 包是一种压缩格式,主要把生成的有目录结构的 class 文件合并到一个文件里,生成 .jar 的后缀。

先要得到不含源文件干净的目录。

默认使用 javac 命令生成的 class 文件与源文件是同级目录,可以加 -d 参数指定 class 文件生成的位置。

比如放在上级目录的 out 目录下,类路径下输入 javac -d ..\out com\cqh\Test.java

测试入口类依赖的 class 文件是否都编译进来了:进入 out 目录下,使用命令 java com.cqh.Test,如果找不到类,肯定某个字节码文件没有在 out 目录正确的位置下,自己手动移下。

在 src 下创建 META-INF 文件夹,新建文件 MANIFEST.MF,注意要在末尾添加一行空行。

Manifest-Version: 1.0 Created-By: cqh Main-Class: com.cqh.Test

Main-Class 是入口类的完整类名。

到 out 目录下,输入命令 jar cvfm cqh.jar ..\src\META-INF\MANIFEST.MF .

out 目录下就生成了 cqh.jar 包,java -jar cqh.jar 就可以运行。

如果在其它路径下,可以通过 classpath 变量或 -cp 参数指定类路径:

java -cp E:\cqh_demo\01-javaSE\package\out\cqh.jar com.cqh.Test

双击运行 jar 包,创建 runJar.bat 文件,内容如下:

@echo offjava -jar %1pause

jar 文件右键,打开方式/选择其它应用/在这台电脑上查找其它应用,选择使用 runJar.bat 打开;以后双击 jar 包就行。

IDEA 中打 jar 包

打开 Project Structure/Artifacts,按下 + 号,JAR/From modules with dependencies...

Module:选择将哪个模块下的类打成 jar 包

Main Class:选择入口类

OK 后指定 jar 包的生成位置,Output directory,去掉 Output Layout 中多余的目录,只保留依赖的 jar 包和当前模块的 complie output

OK 后点击菜单栏的 Build Artifact/Build,在指定位置就生成了 jar 文件。

9.2 封装

内容导视:

访问权限修饰符封装实现步骤JavaBean 与 Property

面向对象编程三大特征:封装、继承、多态。

9.2.1 访问权限修饰符

一共有四种访问权限修饰符,用来控制类、方法、成员变量的访问范围。

分别是 public、protected、default、private。

public:公开的,所有地方的类都可以访问它。

protected:受保护的,只对同一个包下的类或子类公开,但子类中不能通过父类引用直接访问。例,如果父类与 子类不在同一个包下,则在子类中只能通过子类引用访问父类中的被 protected 修饰的方法。

default:默认级别,不用写修饰符,只对同一个包下的类公开。

private:私有,只有本类才能访问。比如私有级别的字段,同包下访问不了(即使是子类实例访问父类的实例相 关的)

其它

类访问权限只有默认和 public 级别。默认级别的类只能在同包下的类中使用,其它包下的类导入不了此类。(因为默认级别只对本类和同包下的类的开放)被 private 修饰的构造方法,其它类无法访问此构造方法,也就无法通过 new 创建此类的对象;同理,创建此类的子类时,由于子类的构造器会自动调用父类构造器,但又无法访问到,所以会报错。

原因

编写类实现功能的过程中,有部分方法只供开发者使用,由于外来者不可能详细地去了解每一个方面,这样也比较浪费精力;但如果随便公开,供他们调用,又很容易出错,所以将这些方法的权限设为 private,不对外公开。

如堆排序的 adjust 方法,第一次构建大顶堆时要从最后一个分支结点开始往前比较,第一次调用方法时传入的 i 应等于 n / 2 - 1;创建好后就可以从第 1 个结点开始与自己的子结点比较,此时调用 adjust 方法传入的 i 应等于 0。

但是对不了解堆排序的人说,是不是太过于勉强,文档注释也不太可能描述的那么详细,清清楚楚告诉你什么时候传 n / 2 - 1,什么时候再传 0;是的,你当然可以学习排序,弄清楚该传什么,但是调一个方法就要求必须掌握底层原理后才能使用,否则很容易出错,未免显得太过于苛刻。

private static void adjust(int[] arr, int i, int n) {

那么我们可以将 adjust 方法设为私有,隐藏它,禁止外来者调用,然后再提供一个公开的方法,隐藏内部的繁琐的实现细节,文档注释写好,提供 API 文档供人翻阅。

也就是说使用者可以不用关心 adjust 方法,了解内部原理;他们只需要知道调用 sort 方法能够对数组进行排序就可以了。

由于使用者无法调用 adjust,即使删掉此方法,重新换个实现也是 OK 的。

/** * 对 arr 数组内的元素进行排序(顺序) */ public static void sort(int[] arr) { for (int i = n / 2 - 1; i >= 0; i--) { adjust(arr, i, n); } for (; n > 1; n--) { swap(arr, 0, n - 1); adjust(arr, 0, n - 1); } }

例子:电视机里的关键部分被包裹在壳中,只提供外面的按钮供人开关、调台,不需要你关心内部的细节也可以使用。

9.2.2 封装实现步骤

封装(encapsulation),将字段私有化,然后对外提供访问入口;也就说只能通过提供的公开方法,才能够对数据进行操作,有效的保护了数据。

class Person { int age;}class Test { public static void main(String[] args) { Person p = new Person(); p.age = -100; }}

如上例,年龄可以为负值吗?

实现步骤

将字段私有化,外部无法直接修改和访问字段提供公开的 set 方法(set + 字段首字母大写),可以判断数据是否合理,对字段赋值提供公开的 get 方法(get + 字段首字母大写),用于外部类获取字段值

class Person { // 将字段私有化 private int age; public Person(int age) { // 注意,这里不能用 this.age = age,否则就绕过了 set 方法的验证环节 setAge(age); } // 提供公开的 get 方法,供其他类读取 age 字段值 public int getAge() { return this.age; } // 提供公开的 set 方法,对字段赋值 public void setAge(int age) { if (age < 0 || age > 130) { System.out.println("年龄应为 0 ~ 130 之间,已设默认值 20"); this.age = 20; } else { this.age = age; } }}

现在想要修改 Person 类的实例,只能通过 set 方法,避免不合理的数据;类的内部数据操作细节自己控制,不允许外部干涉,仅对外暴露少量的方法使用。

好处

隐藏实现细节可以对数据进行验证,保证数据安全合理

其它

布尔类型的字段生成的 get 方法名是 is + 字段首字母大写,如果共同遵守规范,那么仅凭方法名就可以知晓是 boolean 类型。

private boolean deleted;public boolean isDeleted() { return deleted;}9.2.3 JavaBean 与 Property

JavaBean

JavaBean 是符合一定规范的 Java 类,是可重用组件。它的方法命名、构造以及行为必须符合特定的要求:

类有 public 修饰所有字段为 private 修饰这个类必须具有一个公共的(public)无参构造函数字段必须提供 public 的 get 和 set 方法供外界访问这个类是可序列化的,要实现 Serializable 接口

bean 通常作为 DTO(数据传输对象),用来封装值对象,在各层之间传递数据。

Property

property 就是 JavaBean 中定义的 set 和 get 方法名去掉 set 或 get 得到的字符串首字母小写。

如 getAge()方法,属性是 age。

大多数情况,属性名和实例变量名保持一致,这样不会造成困惑。

图中的字母含义:

c:class 类 p:property 属性 m:method 方法 f:field 字段

9.3 继承

内容导视:

继承语法super方法重写9.3.1 继承语法

重复的代码片段可以封装成方法供其它地方的类调用;那么如果是多个类中的代码重复呢?

class Cat { int age; String name; public void play() { System.out.println("飞天遁地无所不能!"); } }

class Dog { int age; String name; public void play() { System.out.println("看门守家我最行!"); } }

此时用继承可以解决代码复用,当多个类存在相同的字段和方法时,可以从这些类抽象出父类,在父类定义这些相同的字段和方法,然后子类使用关键字 extends 继承父类,就自动拥有了父类的字段和方法,只需要定义自己特有的字段和方法。

父类又称超类、基类;子类又称派生类、扩展类。

class Animal { int age; String name; public void play() { System.out.println("..."); }}class Cat extends Animal {}class Dog extends Animal {}class Test { public static void main(String[] args) { Cat cat = new Cat(); cat.age = 3; }}

继承的好处

减少代码冗余,提高了代码的复用性是方法重写与多态的前提提高了代码的扩展性和维护性(给父类新增一个字段,全部子类就有了这个字段)上面一条即是优点也是缺点,耦合度高,父类修改了,子类也会受牵连

细节

子类继承了父类的所有字段与方法(没有继承父类的构造器)子类虽然不能直接访问父类中的私有字段和私有方法,但可以通过父类提供的非私有的方法访问(提供公开的 get 方法供子类访问)

所有类的顶级父类是 Object,(不写 extends,默认继承 Object)比如 B extends A,那么 B 的父类是 A,A 的父类是 Object;A 拥有了 Object 的方法,如 hashCode、toString、equals 方法,B 继承 A 的同时也就拥有了 Object 的方法。

class A {}class B extends A {}class Test { public static void main(String[] args) { A a = new A(); B b = new B(); int hashCode = a.hashCode(); String str = b.toString(); System.out.println(hashCode); System.out.println(str); }}

子类最多直接继承一个父类(单继承),不支持 extends A, B 这种写法,那么如何让 A 继承 B 和 C 的字段和方法呢?答:让 A 继承 B,B 继承 C。

多继承会有些麻烦,如果直接继承的父类们有相同的字段,子类使用 super.字段名会引用不明确。

不能滥用继承,子类和父类之间必须满足 is a 的逻辑关系,比如猫是一个动物,猫才能继承动物,Cat extends Animal;可以说子类是对父类的一种扩展。

初始化父类字段

子类的构造器默认在第一行调用父类的无参构造器(不可见),初始化从父类继承的字段。(父类字段由父类初始化,子类字段由子类初始化,因为父类字段有可能是 private,子类访问不了)

class A {public A() { System.out.println("A 类构造器执行"); }}class B extends A {}class Test { public static void main(String[] args) { new B(); }}

所以父类的无参构造器必须得有,同时能够被子类访问到,否则就会报错;如果不想有子类,就可以把构造器设为 private。

好,接下来再看看如何指定调用父类的其它构造器。

9.3.2 super

super(实际参数列表); 只能出现在构造器的第一句,用来指定调用父类的对应构造器。

class A { int age; public A(int age) { this.age = age; } } class B extends A { public B(int age) { super(age); } } class Test { public static void main(String[] args) { B b = new B(5); System.out.println(b.age);// 5 } }

this() 与 super() 都只能放在构造器的第一句,不能共存,但最后还是会调用父类的构造器。

class A { int age; } class B extends A { public B() { this(1); } public B(int i) {} }

如果调用 B 的无参构造,就会跳转至本类的另一个构造器,而此构造器如果没写 this(),第一句默认还是 super() 就会调用父类的无参构造,除非子类的构造器都有 this() 且形成了一个循环,但显然不可能,因为构造器不允许递归调用。

由于在执行子类的构造器(可见部分)前会执行父类的构造器,那么往上追溯,直到 Object 类的无参构造器。

super 除了可以访问父类构造器外,还可以访问父类的字段、方法

super.字段名、super.方法名(实际参数列表)

特别是父类与子类的方法、字段重名时:

class A { int age = 6;}class B extends A { int age = 2; public void some() { System.out.println(this.age);// 2 System.out.println(super.age);// 6 }}

注意,这里的父类并不仅限于直接父类。

过程

this 先看本类是否有此字段,如果没有就往上(父类)找,在父类中...

如果有,但不能访问(private),报错如果有,可以访问,直接返回值如果没有,继续上找;如果直到顶级父类 Object 类,还未能找到此字段,就报错

而 super 直接从父类开始找,如果就往父类的上面找。

9.3.3 方法重写

方法重写也称方法覆盖,子类中有一个方法与父类的某个方法的名称、返回类型、形式参数列表一样,就说子类的这个方法覆盖父类的此方法。

当父类的方法不能满足子类的要求,就需要在子类中重写它。创建子类对象,通过对象引用访问父类的实例方法时,就会调用子类重写后的方法。

继承开头的例子,父类 Animal 的 play 方法很明显不能显示出子类的个性,需要重写,建议直接拷贝此方法的定义到子类处。

class Animal { int age; String name; public void play() { System.out.println("..."); }}class Cat extends Animal { public void play() { System.out.println("飞天遁地无所不能!"); }}class Dog extends Animal { public void play() { System.out.println("看门守家我最行!"); }}

class Test { public static void main(String[] args) { new Cat().play();// 飞天遁地无所不能! new Dog().play();// 看门守家我最行! }}

如何重写

两个类要有继承关系子类重写后的方法的形式参数列表、方法名称(方法签名)要与父类的完全一样,建议直接复制父类方法的定义子类重写后的方法的返回值类型要么与父类方法的返回值类型一样,要么是父类方法返回值类型的子类(因为可以自动转成父类的返回值类型)子类重写后的方法的访问权限不能比父类的更低(当通过父类引用调用此方法时,结果运行时子类对象因访问权限不足无法调用此方法,这不是很可笑吗?)子类重写后的方法能够抛出的异常类型要么与父类抛出的异常一致,要么为父类异常的子类型(可以抛出多个)

注意

1)父类中无法被访问到的方法无法被子类重写,如 private 修饰的方法;正常情况下,重写错误会给出提示:

但是当父类方法无法被直接访问到,如把这个方法的访问控制权限设为 private,就不会报错。

为了确定是否真的重写,可以在方法之上加一个注解 @Override,既可以验证是否重写,也是一个标记,标记这个方法是重写父类后的方法。如:

2)不能重写静态方法,因为静态方法使用类名.方法名访问,与实例无关。

3)不能重写 final (最终的,不可修改的)修饰的方法

4)构造方法不能被继承,自然也不能重写

5)方法重写与字段无关

9.4 多态

内容导视:

多态语法例子向上转型与向下转型过程9.4.1 多态语法

方法重写是配合多态使用的,否则完全可以定义另一个方法。

父类型的引用允许指向子类型的对象,引用的编译类型可以与实例的类型不一致,即

允许 Animal a = new Cat(); 。

Animal 称为编译类型,Cat 称为运行类型或实际类型。

// 动物代码在方法重写中 class dynamic { public static void some(Animal a) { // 不确定调用的是哪个实例的 play 方法 a.play(); } }

如果调用 some 方法传入的是 new Cat(),Animal a = new Cat()。

some 方法内部调用了 a 的 play 方法,大家都知道调用实例方法,得先有一个实例,然后调用此实例所在类的实例方法,a 指向的是 Cat 类的实例,所以调用的是 Cat 类中的 play 方法(本类没有就往父类找),所以输出 飞天遁地无所不能!;

如果传入的是 new Dog(),同理调用的是 Dog 类的 play 方法,输出 看门守家我最行!。

...

引用究竟指向哪个实例对象,编译期不确定,只有运行时才能确定调用的是哪个子类对象重写后的方法。这样仅改动一点代码就可以把引用绑定到不同的类实例上,让程序拥有了多个运行状态,这就是多态。

class Test { public static void main(String[] args) { play(new Cat()); play(new Dog()); } }

多态和方法重写配合使用,在定义方法时,形参类型定义为父类。使用时,无需修改父类的代码,如 Animal 的 play 方法,只需要编写更多的子类重写 play 方法就实现功能扩展。

由于这个方法的形参可以接收所有子类型的对象,运行时真正执行的方法是传入引用的实际类型的方法(即重写后的方法)。可以降低程序的耦合度,提高程序的扩展力。

多态是指:针对某个实例方法的调用,根据运行时引用的实际类型不同,展现的状态也不同。

之前说过,静态方法没必要重写,这是因为静态方法使用类名调用,与实例无关,传什么都是一样的状态,就算传 null,也不会报空指针异常。

class A { public static void some() { System.out.println("A"); }}class B extends A { public static void some() { System.out.println("B"); }}

class Test { public static void main(String[] args) { other(new B());// A other(null);// A } public static void other(A a) { // 调用静态方法时,会自动转成 A.some(); 与实例无关 a.some(); }}

对比下:

class A { public void some() { System.out.println("A"); }}class B extends A { @Override public void some() { System.out.println("B"); }}

class Test { public static void main(String[] args) { other(new A());// A other(new B());// B other(null);// NullPointerException } public static void other(A a) { a.some(); } }9.4.2 例子

需求:收养不同的动物喂不同的食物。

食物与它的子类:

class Food { String name; } class Fish extends Food { public Fish(String name) { this.name = name; } } class Bone extends Food { public Bone(String name) { this.name = name; } } class Rice extends Food { public Rice(String name) { this.name = name; } }

动物与它的子类:

class Animal { String name; public void eat() {} } class Cat extends Animal { public Cat(String name) { this.name = name; } @Override public void eat() { System.out.println(name + "喵喵叫,在你臂上划了三条痕"); } } class Dog extends Animal { public Dog(String name) { this.name = name; } @Override public void eat() { System.out.println(name + "兴奋地摇起了尾巴,大舌头舔湿了你的脸~"); } } class Pig extends Animal { public Pig(String name) { this.name = name; } @Override public void eat() { System.out.println(name + "吭哧吭哧~"); } }

人类:

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

在人类中加入给动物喂食的方法:

// 给狗喂骨头public void feed(Dog dog, Bone bone) { System.out.println(name + "给" + dog.name + "喂食" + bone.name); dog.eat();}// 给猫喂鱼public void feed(Cat cat, Fish fish) { System.out.println(name + "给" + cat.name + "喂食" + fish.name); cat.eat();}// 给...喂...

测试类:

class Test { public static void main(String[]args) { Person zs = new Person("张三"); Dog wc = new Dog("旺财"); Bone bone = new Bone("小骨头"); zs.feed(wc, bone);// 张三给旺财喂食小骨头 }}

如果针对某一子类具体写代码,那么每来一种动物,都需要在 Person 类新增一个喂食方法。代码复用性不高,不利于代码维护。

但是如果把它当作父类型处理,那么就能囊括所有子类型。(面向父类、抽象、接口编程,而不对具体编程)

将 feed 方法的形参改为父类型:使用多态(Polymorphic)

public void feed(Animal a, Food f) { System.out.println(name + "给" + a.name + "喂食" + f.name); // 实例方法的调用根据运行类型,而不是编译类型。 a.eat();}

正是由于多态机制,父类型的引用可以容纳子类型的对象,更加通用了。

这样来再多的动物,不用新增 feed 方法,也可以对它喂食。

class Test{ public static void main(String[]args){ Person ls = new Person("李四"); Cat tom = new Cat("汤姆"); Bone bone = new Bone("鱼骨头"); ls.feed(tom, bone); }}

要想达到多态的效果,必须要运行时才能确定调用哪个方法,编译时不知道哪一段代码会执行。

有人也许会说,不对啊,我明眼人一看,在 main 方法内调用 feed 方法时传入了 Cat 类型的实例,编译器怎么可能不知道是调用 Cat 类的 eat 方法,非要得到运行时才能确定?

Person ww = new Person("王五"); Random random = new Random(); // 随机生成 [0, 99] 内的整数 int i = random.nextInt(100); if (i < 35) { Dog dog = new Dog("小黄"); Bone bone = new Bone("猪骨头"); ww.feed(dog, bone); } else if (i < 65) { Pig pig = new Pig("佩奇"); Rice rice = new Rice("大米"); ww.feed(pig, rice); } else { Cat tom = new Cat("汤姆"); Bone bone = new Bone("鱼骨头"); ww.feed(tom, bone); }

这样,你还能在编译时就能确定调用的是哪个动物类的 eat 方法吗?

前期(静态)绑定:在编译期时已确定目标方法,解析阶段时,目标方法与所需类型绑定。

晚期(动态)绑定:在编译器无法确定目标方法,运行时,目标方法才能与引用的实际类型绑定。

9.4.3 向上转型与向下转型

向上转型

本质:父类的引用指向子类的对象

语法:父类类型 引用名 = new 子类类型();

特点:编译类型看左边,运行类型看右边。调用实例方法的最终运行效果看子类的具体实现。

缺点:能访问哪些方法和字段主要看编译类型,不能访问子类中特有的字段和方法。

使用父类作为形参已然确定了作为实参的子类对象能够调用哪些方法。

向下转型

语法:子类类型 引用名 = (子类类型)父类引用;

强转的是引用的数据类型,而不是对象的类型。并且只有当父类的引用指向的是子类类型的对象,才能强转成该子类类型,否则报 java.lang.ClassCastException 异常。

可以通过 instanceof 关键字,判断引用指向的对象类型是否为 xxx 类型或 xxx 类型的子类,然后再转成 xxx。

向下转型后,可以调用子类中的所有方法和字段(遵循访问控制权限)。

Object o = new String("str"); boolean b = o instanceof String; System.out.println(b);// true // 如果 o 的实际类型是 String 类型,就将其强转为 String 类型 if (b) { String s = (String)o; // 强转后,就可以访问 String 类独有的方法 System.out.println(s.length()); }9.4.4 过程

引用能够访问哪些方法与字段,看编译类型。

class A { int age = 55; } class B extends A { String name; int age = 4; }

class Test { public static void main(String[] args) { some(new B()); A a = new B(); String s = a.name;// 找不到符号 } public static void some(A a) { int i = a.age; String s = a.name;// 找不到符号 } }

使用引用调用实例方法:

编译时,看编译类型是否有此方法,找不到就往上找,直到 Object;如果找到了但无访问权限,或没有找到,报错。运行时,从引用的实际类型所在类开始找,找到了直接调用,没找到继续上找。

使用引用访问字段:

看编译类型是否有此字段,找不到就往上找,直到 Object;如果找到了但无访问权限,或没有找到,报错。字段没有动态绑定机制,与实际类型无关,只从编译类型开始往上找。

class Test { public static void main(String[] args) { A a = new B(); // 与 B 无关 int i = a.age;// 55 } public static void some(A a) { int i = a.age;// 55 } }9.5 Object

Object 类是所有类的顶级父类、超类,常用的方法如下:

内容导视:

getClassequalshashCodetoStringfinalizeclone9.5.1 getClass

获取引用的运行时类型

Object obj = new String("你好"); String str = obj.getClass().toString(); System.out.println(str);//class java.lang.String9.5.2 equals

equals 方法用于判断两个对象是否相等,默认使用双等号比较,参与比较的两个引用,如果指向同一个对象,则返回 true,否则返回 false。

public boolean equals(Object obj) { return (this == obj);}

可以根据自己的需要,重写此方法更改比较规则。

公认的:内容相等且是同一类型的两个对象,就认为它们相等,比如:

class Time { int second; int minute; int hour; @Override public boolean equals(Object o) { // 如果两个对象的内存地址相等,则返回 true if (this == o) { return true; } /* 如果 o 为空,或者它们的运行类型不一致,就返回 false 如果需求是子类对象也可以参与比较,条件可以改为 !(o instanceof Time) */ if (o == null || getClass() != o.getClass()) { return false; } // 往下转型,为了访问子类的特有字段 Time time = (Time) o; // 如果两个对象的时分秒都相同,则返回 true return second == time.second && minute == time.minute && hour == time.hour; } // 有参构造略}

Time t1 = new Time(35, 45, 12);Time t2 = new Time(35, 45, 12);System.out.println(t1 == t2);// falseSystem.out.println(t1.equals(t2));// true

在内容相同就认为相等的前提下,引用数据类型的实例变量不能使用双等号比较,而是使用 equals 方法。(确保 equals 方法重写过了)

class Person { String name; Time time; @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Person p = (Person) o; // String、Time 类的 equals 方法都已经重写过了,是内容相等就返回 true return name.equals(p.name) && time.equals(p.time); }}

我们总是习惯将肯定不为 null 的引用放在前面,以免出现空指针异常:

public void some(String s) { boolean b = "abc".equals(s);// 不会出现空指针异常 boolean b1 = s.equals("abc");// 当传入的 s 为 null 时,就会出现空指针异常}

用 instanceof 还是 getClass

现在有子类 Date 继承了 Time,如果子类有自己的比较规则,重写了 equals 方法,使用了 instanceof;

对于有着相同数据的 Date date 和 Time time 对象,调用 time.equals(date)时,date instanceof Time 为 true,date 与 time 相等。

date.equals(time),time instanceof Date 为 false,因为 time 的类型不是 Date,date 与 time 不相等。

出现了矛盾,所以必须采用 getClass == o.getClass,父类与子类比较,直接返回 false。

如果由父类决定相等的概念(不允许子类重写 equals),允许在不同类型的子类中比较,就可以使用 instanceof。

如果子类有自己的比较规则,使用 getClass。

9.5.3 hashCode

返回对象的哈希码值。支持此方法是为了便于使用 java.util.HashMap 提供的哈希表。

hashCode 方法重写的一般约定是: 在 Java 应用程序执行期间,只要在同一个对象上多次调用它,hashCode 方法必须始终返回相同的整数,前提是没有修改对象上 equals 比较中使用的信息。该整数不需要从应用程序的一次执行到同一应用程序的另一次执行保持一致。

同一对象调用 hashCode 必须返回同一整数

如果两个对象根据 equals 方法比较相等,则对两个对象中的每一个,调用 hashCode 方法必须产生相同的整数结果。没有要求不等的两个对象调用 hashCode 方法时必须产生不同的整数结果。

经 equals 比较相等的两个对象的 hashCode 必须一致,不相等的两个对象的 hashCode 不要求一定不一致

但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。在合理的范围内,类 Object 定义的 hashCode 方法确实为不同的对象返回不同的整数。 (这通常通过将对象的内部地址转换为整数来实现,但 Java 编程语言不需要这种实现技术。

所以当 equals 重写后,hashCode 方法也应该重写,相同的内容返回的哈希码值应一致。

在 equals 这节的例子当作,认为只要时分秒相等,则 equals 比较返回 true,那么重写后的 hashCode 方法也是一样,只要时分秒相等,就返回相同的整数。

@Overridepublic int hashCode() { return Objects.hash(second, minute, hour);}

如果没有兴趣了解底层细节,可跳过。

Objects 类中:

// Objects.hash 方法实际调用的是 Arrays 类的 hashCode 方法public static int hash(Object... values) { return Arrays.hashCode(values);}

这里不得不说,当实参为基本数据类型时,如 4,会自动调用对应包装类型的 valueOf 方法获得实例,把数据封装到实例的 value,所以传入的实际是引用类型。

Arrays 类中:

public static int hashCode(Object a[]) {// 当什么都没传入,返回 0 if (a == null) return 0; int result = 1; // 遍历一维数组,得到每一个元素 for (Object element : a) { // 结果与元素的 hashCode 方法相关 result = 31 * result + (element == null ? 0 : element.hashCode()); } return result;}

如 Boolean 的 hashCode 方法:当 value 为 false 时,返回 1237;为 true 时,返回 1231。

Integer 的 hashCode 方法返回 value。

String 的 hashCode 方法:

public int hashCode() { int h = hash;// hash 默认为 0 if (h == 0 && value.length > 0) {// 如果 h 不等于 0,直接返回 h char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h;}

求 hashCode:

class A { int i = 5; boolean b = false; String s = "abc"; @Override public int hashCode() { return Objects.hash(i, b, s); } // 略}class Test { public static void main(String[] args) { int hashCode = new A().hashCode(); System.out.println(hashCode); }}

实际是求 hashCode(5, false, "abc");

new Integer(5)的 hashCode 为 5;

new Boolean(false)的 hashCode 为 1237;

"abc".hashCode 为 96354。

h = 0h = 31 * 0 + 'a' = 97h = 31 * 97 + 'b' = 3105h = 31 * 3105 + 'c' = 96354

所以 new A().hashCode 为 169297

result = 1;result = 31 * 1 + 5 = 36;result = 31 * 36 + 1237 = 2353result = 31 * 2353 + 96354 = 169297

对于int 类型的一维数组的实例变量,由于它们没有重写 hashCode 方法,所以不可以直接传入 Objects.hash 方法,否则即使内容相同,返回的 hashCode 也不一致:

int[] arr = {5, 6, 9};int[] arr1 = {5, 6, 9};System.out.println(arr.hashCode());// 460141958System.out.println(arr1.hashCode());// 1163157884

可以借助 Arrays 的 hashCode 方法,遍历 int 数组得到每个元素 5,6,9,由此计算出 hashCode。

如果直接将 arr 或 arr1 传入 Objects 的 hash 方法,由于它是将一维数组当成了数组中的元素 Object[] o = {arr}; 那么得到的 hashCode 就与一维数组的 hashCode 相关。

对于 int 类型的二维或更高维的数组,则需要遍历得到每个一维数组,再分别传入 Arrays 的 hashCode 方法累加。

@Overridepublic int hashCode() { int result = Objects.hash(重写过 hashCode 方法的引用类型的元素 , 基本数据类型的元素...); // 一维数组单独拿出来计算 hashCode result = 31 * result + Arrays.hashCode(arr);; return result;}9.5.4 toString

直接输出引用时,会自动调用 toString 方法:

public String toString() { // 完整类名@十六进制的 hashCode return getClass().getName() + "@" + Integer.toHexString(hashCode());}

package com.cqh;class A { public static void main(String[] args) { System.out.println(new A());// com.cqh.A@1b6d3586 }}

重写 toString 方法,一般用于显示该对象的字段等信息:

class Dog { int age = 5; String name = "大黄"; @Override public String toString() { return "Dog {年龄:" + age + ",姓名:" + name + "}"; }}class Test { public static void main(String[] args) { System.out.println(new Dog());// Dog {年龄:5,姓名:大黄} }}9.5.5 finalize

当某个对象没有任何引用时,JVM 就认为这个对象是一个垃圾,使用垃圾回收器销毁该对象,销毁对象前,调用 finalize 方法。

垃圾回收机制的调用由自己的 GC 算法决定(我的是大概超过 20 万个垃圾才会清理),也可以尝试调用 System.gc 方法建议 JVM 启动垃圾回收机制。

System.gc():运行垃圾收集器。调用 gc 方法表明 Java 虚拟机花费精力回收未使用的对象,以使它们当前占用的内存可用于快速重用。从方法调用到返回时,Java 虚拟机已尽最大努力从所有丢弃的对象中回收空间。调用 System.gc() 实际上等效于调用: Runtime.getRuntime().gc()

从 JDK 9 开始,finalize 方法被标记为 deprecated,意味着废弃。由于我们没办法控制 GC 发生的时间,JVM 也不一定会调用它,依赖此方法释放资源是有很大的不确定性。

class Test { String name; public Test(String name) { this.name = name; } public static void main(String[] args) { for (int i = 0; i < 1000; i++) { Test t = new Test("实例" + i); // 断掉引用与对象的连接 t = null; System.gc(); } } @Override protected void finalize() throws Throwable { System.out.println("启动垃圾回收机制" + name); }}

System.gc 方法是异步的:不用等待 gc 方法执行完毕,就可以执行 gc 后面的代码。

9.5.6 clone

创建并返回对象的副本,是浅拷贝,只拷贝字段保存的值(包括地址),并没有拷贝实例中的引用类型的变量指向的对象。

造成的后果是如果原有变量修改此实例指向的变量的内容,也会影响被克隆的实例。

class Clone { int[] a;int b; public Clone(int[] a, int b) { this.a = a; this.b = b; }}

下面是不同的例子:

赋值:没有创建新的对象。

class Test { public static void main(String[] args) { int[] arr1 = {5, 6, 9}; int i1 = 3; Clone c1 = new Clone(arr1, i1);Clone c2 = c1; // 修改 c1 的实例的字段,会影响 c2 的字段 c1.b = 5; System.out.println(c2.b);// 5 System.out.println(c1 == c2);// true }}

浅拷贝:没有让引用类型的实例变量指向新的对象。

class Test { public static void main(String[] args) { int[] arr1 = {5, 6, 9}; int i1 = 3; Clone c1 = new Clone(arr1, i1);int[] arr2 = c1.a; int i2 = c1.b; Clone c2 = new Clone(arr2, i2); // 修改 c1 实例的 a 字段指向的数组的元素,会影响 c2 的 a 字段 c1.a[0] = 2; System.out.println(c2.a[0]);// 2 }}

深拷贝:

class Test { public static void main(String[] args) { int[] arr1 = {5, 6, 9}; int i1 = 3; Clone c1 = new Clone(arr1, i1);int[] arr2 = new int[c1.a.length]; for (int i = 0; i < c1.a.length; i++) { arr2[i] = c1.a[i]; } int i2 = c1.b; Clone c2 = new Clone(arr2, i2); // 修改 c1 实例的 a 字段指向的数组的元素,不会影响 c2 的 a 字段 c1.a[0] = 2; System.out.println(c2.a[0]);// 5 }}

重写 clone 方法,深拷贝:

@Overrideprotected Object clone() throws CloneNotSupportedException { // 得到浅拷贝的实例 Clone clone = (Clone) super.clone(); // 将实例变量 a 也拷贝一份赋给 clone 的 a 变量 clone.a = a.clone(); return clone;}

此方法是 protected 修饰,所以只能在同包或本类中访问;同时被克隆的类要实现 Cloneable 接口,否则会报 CloneNotSupportedException 不支持克隆异常。

Cloneable 接口不包含任何方法,只是作为标记,代表子类允许克隆。

if (子类对象 instanceof Cloneable) { 开始浅拷贝...} else { 抛出异常}

例:

class Clone implements Cloneable { ...

class Test { public static void main(String[] args) throws CloneNotSupportedException { int[] arr = {5, 6, 6}; int i = 4; Clone c1 = new Clone(arr, i); Clone c2 = (Clone) c1.clone(); System.out.println(c1 == c2);// false c1.a[0] = 3; System.out.println(c2.a[0]);// 5 }}9.x 总结回顾

使用包机制用于解决类的命名冲突、类文件管理等问题,包名由小写字母组成。

访问控制修饰符

本类

同包

子类

任意位置

public

protected

×


×

×

private

×

×

×

字段或方法不想被其它类访问到,使用 private 修饰符。

封装隐藏内部实现细节,保证数据安全。

继承解决多个类中代码重复的问题。子类会继承父类的字段与方法,但父类修改也会影响子类。

构造器第一句默认调用父类构造器,用于初始化子类的父类型特征。

super 与 this 的比较

当子类和父类有相同的字段和方法时,访问父类使用 super.xxx。

访问字段或方法时,this 是从本类开始寻找,如果没有才往上找,super 是从父类开始寻找。

this 代表当前对象,使用 this.xxx 更多时候是为了区分同名局部变量。

都只能出现在实例方法和构造器中。

重载与重写的区别

重载为了减轻记名和取名的麻烦,让功能相似的方法的名一致;重写是父类的方法无法满足要求。

重载是在同一个类中,要求:方法名相同,形参列表不同(顺序、个数、类型至少有一个不同),对于返回类型与修饰符无要求。

重写在父子类中,要求:方法名相同,形参列表相同,子类返回类型与父类返回类型一致或是其子类,子类的访问控制权限不能比父类更低,子类抛出的编译时异常范围不能比父类的更大。

重写与字段、静态方法无关。

多态

父类的引用指向子类型的对象,运行时才能根据实际类型决定调用哪个实例方法。

Object 的方法

基本数据类型比较相等使用 “==”,引用类型使用 equals 方法。

== 与 equals 的对比

== 可以判断两个基本类型的变量保存的值是否相等,也可以判断引用类型保存的内存地址是否相等,即是否指向的是同一个对象。

equals 默认实现是判断两个引用是否指向同一个对象,需要重写为判断两个对象的内容是否相等。

hashCode

使用 equals 比较返回 true 的两个引用,调用 hashCode 返回的整数应一致。

finalize

使用 finalize 完成资源释放不太可靠,已被废弃。

9.y 脑海练习

9.1 控制台上输出什么?

1)

class A { public A() { System.out.println("我是 A"); }}class B extends A { public B() { System.out.println("我是 B 的无参构造器"); } public B(String name) { System.out.println(name + "我是 B 的有参构造器"); }}class C extends B { public C() { this("hello"); System.out.println("我是 C 的无参构造器"); } public C(String name) { super("呵呵"); System.out.println(name + "我是 C 的有参构造器"); }}class Test { public static void main(String[] args) { new C(); }}

2)

class Base { int count = 10; public void display() { System.out.println(this.count); } public void some() { System.out.println(this.count); }}class Sub extends Base { int count = 20; @Override public void display() { System.out.println(this.count); }}class Test { public static void main(String[] args) { Sub s = new Sub(); System.out.println(s.count); s.display(); Base b = s; System.out.println(b == s); System.out.println(b.count); b.display(); b.some(); }}

3)

class A { public void doOther() { some(); } private void some() { System.out.println("A类的some方法被调用"); }}class B extends A { public void some(){ System.out.println("B类的some方法被调用"); }}class Test { public static void main(String[]args) { new B().doOther(); }}

4)

class A { int i = 10; public int sum() { return getI() + this.i; } public int sum1() { return i + 10; } public int getI() { return i; }}class B extends A { int i = 20; public int getI() { return i; } public int sum1() { return i + 10; }}class Test { public static void main(String[] args) { A a = new B(); System.out.println(a.i); System.out.println(a.sum()); }}

9.2 哪里有误?

class Test { public static void main(String[] args) { double d = 13.4; long l = (long) d; System.out.println(l); int i = 5; boolean b = (boolean) i; Object obj = "Hello"; String str = (String) obj; System.out.println(str); Object objI = new Integer(5); String str2 = (String) objI; Integer objI1 = (Integer) objI; }}9.z 习题答案

9.1 控制台上输出什么?

1)

class A { public A() { System.out.println("我是 A"); }}class B extends A { public B() { System.out.println("我是 B 的无参构造器"); } public B(String name) { // 先调用父类的无参构造 System.out.println(name + "我是 B 的有参构造器"); }}class C extends B { public C() { // 调用另一个构造器 this("hello"); System.out.println("我是 C 的无参构造器"); } public C(String name) { // 调用父类有参构造 super("呵呵"); System.out.println(name + "我是 C 的有参构造器"); }}class Test { public static void main(String[] args) { new C(); }}

调用 C 类的无参构造器 ①

调用 C("hello")②

调用 C 的父类的构造器 B("呵呵")③

调用 B 的父类的构造器 A(),输出 我是 A

回到 ③ 输出 呵呵我是 B 的有参构造器

回到 ② 输出 hello我是 C 的有参构造器

回到 ① 输出 我是 C 的无参构造器

2)

class Base { int count = 10; public void display() { System.out.println(this.count); } public void some() { System.out.println(this.count); }}class Sub extends Base { int count = 20; @Override public void display() { System.out.println(this.count); }}class Test { public static void main(String[] args) { Sub s = new Sub(); System.out.println(s.count); s.display(); Base b = s; System.out.println(b == s); System.out.println(b.count); b.display(); b.some(); }}

s.count = 20,输出 20

s.display 调用的是 Sub 类的 display 方法,输出 20

b 与 s 指向同一个对象,输出 true

b.count = 10,重写与字段无关,从编译类型 Base 开始找,输出 10

b 实际是 Sub 类型,b.display 调用的是 Sub 的 display 方法,输出 20

Sub 类没有 some 方法,往上找,b.some 调用的是 Base 的 some 方法,输出本类的 count 即 10

3)

class A { public void doOther() { some(); } private void some() { System.out.println("A类的some方法被调用"); }}class B extends A { public void some(){ System.out.println("B类的some方法被调用"); }}class Test { public static void main(String[]args) { new B().doOther(); }}

B 类没有 doOther 方法,往上找,调用 A 类的 doOther 方法,调用 some 方法,输出 A类的some方法被调用。

私有方法不能被重写,可以加 @Override 验证

4)

class A { int i = 10; public int sum() { return getI() + this.i; } public int sum1() { return i + 10; } public int getI() { return i; }}class B extends A { int i = 20; public int getI() { return i; } public int sum1() { return i + 10; }}class Test { public static void main(String[] args) { A a = new B(); System.out.println(a.i); System.out.println(a.sum()); }}

字段没有重写机制,在 A 类中 a.i = 10,输出 10;

a 的实际类型是 B,B 类没有 sum 方法,调用 A 类的 sum 方法,返回 getI()+ 10 ①

getI()被 B 重写,返回 20;

回到 ①,返回 30,输出 30

9.2 哪里有误?

class Test { public static void main(String[] args) { double d = 13.4; long l = (long) d; System.out.println(l); int i = 5; // boolean b = (boolean) i; Object obj = "Hello"; String str = (String) obj; System.out.println(str); Object objI = new Integer(5); // String str2 = (String) objI; Integer objI1 = (Integer) objI; }}

int 类型不能强转成 boolean。

objI 的实际类型为 Integer 类型,不能强转成 String,运行时会报 ClassCastException。