目录
1.package、import
1.1 package(包)
package,称为包,用于指明定义的类、接口等结构所在的包
1.1.1 语法格式
package 顶层包名.子包名
假设 Person.java 的存放路径是 \pack1\pack2 ,那么 Person.java 中的顶部第一条语句就是 package pack1.pack2,如下所示
1.1.2 注意事项
- 一个源文件( .java 文件)只能有一个声明包的 package 语句,且 package 语句作为第一条语句出现。若缺少该语句,则指定为无名包
- 包名:属于标识符,满足标识符命名的规则和规范即可
- 同一个包下可以声明多个类、接口,但不能定义同名的类、接口。不同的包下可以定义同名的类、接口
1.1.3 包的作用
包的作用
1.2 import(导入)
为了使用定义在其它包中的 Java 类,需用 import 语句来显式引入指定包下所需要的类。相当于 import 语句告诉编译器到哪里去寻找这个类
1.2.1 语法格式
import 包名.类名
1.2.2 应用举例
在 pack1\pack2 下创建 Person.java,在 demo 目录下创建 Cat.java,然后在 Person.java 中创建一个 Cat 对象
Cat.java
package demo;
public class Cat {
public void eat(){
System.out.println("cat eat");
}
}
Person.java
package pack1.pack2;
import demo.Cat;
public class Person {
public static void main(String[] args) {
Cat cat = new Cat();
cat.eat();
}
}
1.2.3 注意事项
(1) import 语句,声明在包的声明(package xx.xx)和类的声明之间
(2)如果使用 .* ,即导入包下的所有结构。举例:可以使用 java.util.* 的方式,会一次性导入util包下所有的类或接口
(3)建议使用到哪个类,就导入哪个类即可,不建议使用 .* 导入所有结构
(4)如果导入的类或接口是在当前包下的,则可以省略 import 语句
2.访问修饰符(背)
2.1 基本介绍
Java 提供 4 种访问控制修饰符,用于控制方法、属性(成员变量)的访问权限(访问范围)
4 种访问控制修饰符:public 、 protected 、 缺省 、 private
(1)public:公开级别,对外公开
(2)protected:对子类和同一个包中的类公开
(3)缺省:没有修饰符号,向同一个包的类公开
(4)private:只有类本身可以访问,子类也不能访问,不对外公开。是最严格的级别
本类内部 | 本包内 | 其他包的子类 | 其他包非子类 | |
---|---|---|---|---|
public | ✔ | ✔ | ✔ | ✔ |
protected | ✔ | ✔ | ✔ | ❌ |
缺省 | ✔ | ✔ | ❌ | ❌ |
private | ✔ | ❌ | ❌ | ❌ |
2.2 注意事项
外部类、接口(interface)只能用 public、缺省 访问控制修饰符
成员变量、成员方法、构造器、成员内部类 4 种访问控制修饰符都可以使用
举例
下面例子中,Person 即外部类, Dog 即成员内部类
public class Person{
public String name;
public Dog dog;
}
3.面向对象特征之一:封装(encapsulation)
面向对象的三大特征:封装、继承、多态
3.1 什么是封装
封装就是把客观事物封装成抽象概念的类,并且类可以把自己的数据和方法只向可信的类或者对象开放,向没必要开放的类或者对象隐藏信息
通俗的讲就是把该隐藏的隐藏起来,该暴露的暴露出来,这就是封装的设计思想
3.2 为什么需要封装
-
我要用洗衣机,只需要按一下开关和洗涤模式即可。有必要了解洗衣机内部的结构吗?
-
我要开车,需要懂离合、油门、制动等原理吗?
-
客观世界里每一个事物的内部信息都隐藏在其内部,外界无法直接操作和修改,只能通过指定的方式进行访问和修改
随着系统越来越复杂,类会越来越多,那么类之间的访问边界必须把握好,面向对象的开发原则要遵循 "高内聚、低耦合"
内聚:指一个模块内各个元素彼此结合的紧密程度
耦合:指一个软件结构内不同模块之间互连程度的度量
内聚意味着重用和独立,耦合意味着多米诺效应牵一发动全身
"高内聚,低耦合" 在 Java 中的体现之一:
高内聚 :类的内部数据操作细节自己完成,不允许外部干涉
低耦合 :仅暴露少量的方法给外部使用,尽量方便外部调用
3.3 封装的实现步骤
3.3.1 成员变量/属性私有化
概述:私有化类的成员变量,提供公共的 get 和 set 方法,对外暴露获取和修改属性的功能。在Java 的开发中,一般都会这样做
步骤
(1)将属性作 Private 私有化,这样就不能在其他类直接修改了
(2)提供一个公共的(Public) set 方法,用于对属性合法判断并赋值
public void setXxx(类型 参数名){Xxx表示某个属性
//加入数据验证的业务逻辑
属性 = 参数名;
}
(3)提供一个公共的(Public) get 方法,用于获取属性的值
public 数据类型 getXxx(类型 参数名){Xxx表示某个属性
return Xxx;
}
案例
创建 Person 类
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
//增加判断逻辑
if (name.length()>=2 && name.length()<=6){
this.name = name;
return;
}
System.out.println("Invalid name!");
}
public int getAge() {
return age;
}
public void setAge(int age) {
//增加判断逻辑,用户有可能将age赋值为负值
if (age <= 0){
System.out.println("Invalid age!");
return;
}
this.age = age;
}
}
验证
public class PersonTest {
public static void main(String[] args) {
Person p = new Person();
/*实例变量用private修饰,跨类是无法直接调用的
p.name = "小马"; X,错误
p.age = 20; X,错误
*/
p.setName("小马");
p.setAge(20);
System.out.println("name:"+p.getName()+" age:"+p.getAge());
}
}
3.3.2 成员变量/属性私有化的好处
-
让使用者只能通过事先预定的方法访问数据,不能直接访问(即直接对象名.成员变量名),从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问和赋值
-
便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改
3.3.3 封装与构造器的结合使用
在构造器中结合 SetXxx 方法即可,这样即使用构造器方法初始化对象时也可以判断属性赋值的合法性
案例
创建 Person 类
public class Person {
private String name;
private int age;
public Person(String name, int age) {
setName(name);
setAge(age);
}
public String getName() {
return name;
}
public void setName(String name) {
//增加判断逻辑
if (name.length()>=2 && name.length()<=6){
this.name = name;
return;
}
System.out.println("Invalid name!");
}
public int getAge() {
return age;
}
public void setAge(int age) {
//增加判断逻辑,用户有可能将age赋值为负值
if (age <= 0){
System.out.println("Invalid age!");
return;
}
this.age = age;
}
}
创建测试类 PersonTest 类
public class PersonTest {
public static void main(String[] args) {
Person p = new Person("小马", 20);
System.out.println("name:"+p.getName()+" age:"+p.getAge());
}
}
4. 面向对象特征之二:继承
4.1 生活中的继承
财产继承
绿化:前人栽树,后人乘凉
样貌继承
样貌继承之外,还可以样貌进化
补充:继承有延续(下一代延续上一代的基因、财富)、扩展(下一代和上一代又有所不同)的意思
4.2 Java中的继承
4.2.1 角度:从上而下
通过继承,可以简化 Student 类的定义:
说明:Student 类继承了父类 Person 的所有属性和方法,并自己增加了一个属性 school . Person 中的属性和方法,Student 都可以使用
4.2.2 角度:自下而上
多个类中存在相同属性和行为时,可以将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成继承关系。如下图所示
4.3 继承的好处
-
继承的出现减少了代码冗余,提高了代码的复用性,有利于更能的扩展
-
继承的出现让类与类之间产生了
is-a
的关系,为多态的使用提供了前提 - 父类更通用、更一般,子类更具体
4.4 继承的语法
通过 extends
关键字,可以声明一个类B继承另外一个类A,定义格式如下:
[修饰符] class 类A {
...
}
[修饰符] class 类B extends 类A {
...
}
类B:称为子类、派生类(derived class)、SubClass
类A:称为父类、超类、基类(base class)、SuperClass
代码示例
创建父类 Animal
//定义动物类,作为父类
public class Animal {
String name;
int age;
//定义动物的吃东西方法
public void eat() {
System.out.println(age + "岁的"
+ name + "在吃东西");
}
}
创建子类 Cat
//定义猫类Cat 继承 动物类Animal
public class Cat extends Animal {
int count;//记录每只猫抓的老鼠数量
// 定义一个猫抓老鼠的方法catchMouse
public void catchMouse() {
count++;
System.out.println("抓老鼠,已经抓了"
+ count + "只老鼠");
}
}
创建测试类 TestCat
public class TestCat {
public static void main(String[] args) {
Cat cat = new Cat();
//对猫从父类继续来的属性赋值
cat.name = "Tom";
cat.age = 2;
//调用该猫继承来的eat()方法
cat.eat();
//调用该猫的catchMouse()方法
cat.catchMouse();
cat.catchMouse();
cat.catchMouse();
}
}
/*输出结果
2岁的Tom在吃东西
抓老鼠,已经抓了1只老鼠
抓老鼠,已经抓了2只老鼠
抓老鼠,已经抓了3只老鼠
*/
4.5 继承的深入讨论/细节问题
(1)子类会继续父类的所有属性和方法,但是如何访问具体还得看这个属性和方法的修饰符是什么。即需要遵守前面 2.访问修饰符 中那个表
验证,debug 查看子类是否继承了父类的全部属性
(2)创建子类对象时,子类必须调用父类的构造器,完成父类的初始化(必须要做的)
-
父类有无参构造器:当创建子类对象时,不管使用子类的哪一个构造器,默认情况下会调用父类的无参构造器。即子类的任何一个构造器第一行会默认有一个
super()
来调用父类的无参构造器,除非你显示定义使用其他的父类构造器,即super(实参列表)
验证
-
父类没有无参构造器:必须在子类的构造器中用
super()
去指定使用父类的哪个构造器完成对父类的初始化工作,否则,编译不会通过-
父类没有无参构造器的情况:类中定义了参构造器,没有显示定义无参构造器,所以系统也不会自动生成无参构造器
-
验证
(3)super 在使用时,必须放在构造器第一行,且 super 只能在构造器中使用
(4)super() 、 this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器,构造器中要调用其他构造器要么用 super 要么用 this ,两者必须存一
(5)Java 中所有类都是 Object 类的子类, 即 Object 是所有类的父类
用前面的 Base 和 Sub 做一下 debug 验证
(6) 父类构造器的调用不限于直接父类,会一直往上追溯直到 Object 类 (顶级父类),如下图所示
(7)Java 只支持单继承:一个父类可以有多个子类,但一个子类只能有一个父类
思考:如何让 A "继承" B类和C类?
解决方案:【A继承B,B继承C】
(8)继承不能滥用,子类和父类最好满足 is-a 的逻辑关系
Person类 Music类
Person is a Music?
Music extends Person//X,不合理
Animal类 Cat类
Cat is a Animal?
Cat extends Animal//√,合理
(9)子类不是父类的子集,而是对父类的 "扩展"
4.6 子类对象实例化过程
示例代码
public class Test {
public static void main(String[] args) {
Son son = new Son();
}
}
class GrandPa{
String name = "爷爷";
String hobby = "旅游";
}
class Father extends GrandPa{
String name = "爸爸";
int age = 39;
}
class Son extends Father{
String name = "儿子";
}
内存原理图
在调用 son.name 时,要按照查找关系来返回对应的值
(1)首先看子类是否有该属性
(2)如果子类有这个属性,并且可以访问(看访问控制符),则返回信息
(3)如果子类没有这个属性,就看父类有没有这个属性(如果父类有该属性,并且可以访问,则返回信息)
(4)如果父类没有就按照(3)的规则,继续找父类的父类,直到找到 Object 类
(5)如果经过(1)-(4)都没有找到属性,则报错
4.7 练习题
练习题1
public class test{
public static void main(String[] args){
B b = new B();
}
}
class A{
A(){
System.out.println("a");
}
A(String name){
System.out.println("a name");
}
}
class B extends A{
B(){
this("abc");
System.out.println("b");
}
B(String name){
System.out.println("b name");
}
}
/*输出结果
a
b name
b
*/
练习题2
public class test {
public static void main(String[] args) {
C c = new C();
}
}
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("hahah");
System.out.println("我是C类的有参构造");
}
}
/*输出结果
我是A类
hahah我是B类的有参构造
我是C类的有参构造
我是C类的无参构造
*/
5.super关键字
5.1 基本介绍
super 代表父类的引用,用于访问父类的属性、方法、构造器
5.2 基本语法
(1)访问父类属性:super.属性名
同样要看到属性的修饰符是什么;例如父类中的 Private 属性在子类中用 super.属性名 仍然不能访问
(2)访问父类方法:super.方法名(参数列表)
同样要看方法的修饰符是什么;例如父类中的 Private 方法在子类中用 super.方法名 仍然不能访问
(3)访问父类构造器:super(),super(参数)
super(),super(参数) 只能出现在构造器的首行
5.3 注意事项
-
super 的追溯不限于直接父类
-
如果多个基类(上级类)中都有同名的成员,使用 super 访问遵循''就近原则''
-
当子类中有和父类中的属性或方法重名时,为了访问父类的属性或方法,必须通过 super 。如果没有重名的,使用 super、this、直接访问 三种方式都是一样的效果
super、this、直接访问三种方式示例
public class Test {
public static void main(String[] args) {
Son son = new Son();
son.test();
}
}
class Father {
public Father() {
}
public void father_method() {
System.out.println("father method");
}
}
class Son extends Father {
public Son() {
}
public void test(){
//当无重名方法或属性时,三种调用都可行
this.father_method();
super.father_method();
father_method();
}
}
/*输出结果
father method
father method
father method
*/
6.super、this的对比(非常重要)
this | super | |
---|---|---|
访问属性 | 访问本类中的属性,如果本类没有该属性则从父类中查找 | 从父类开始查找属性 |
调用方法 | 访问本类中的方法,如果本类没有该方法则从父类中查找 | 从父类开始查找方法 |
调用构造器 | 调用本类构造器,必须放在构造器首行 | 调用父类构造器,必须放在子构造器的首行 |
特殊 | 表示当前对象 | 子类中访问父类对象 |
super:追溯不限于直接父类
this:调用构造器时不会追溯到其父类,只会调用本类的构造器;调用属性/方法时 this 的追溯同样不限于直接父类
7.方法重写/方法覆盖(Override)
场景问题:父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类的实现不适合于自己当前的类,该怎么办呢?
解决方案:子类可以对从父类中继承来的方法进行改造,我们称为方法的重写(Overwrite)。也称为方法的覆盖(Override)
7.1 快速入门案例
public class Test {
public static void main(String[] args) {
Son son = new Son();
son.cry("小马");
son.test();
}
}
class GrandPa{
public GrandPa(){}
public void test(){
System.out.println("GrandPa的test方法");
}
}
class Father extends GrandPa{
public Father() {}
public void cry(String name){
System.out.println(name+"在哭");
}
}
class Son extends Father {
public Son() {}
//重写父类的cry方法
public void cry(String name){
System.out.println(name+"在狗叫");
}
//重写爷爷类的test方法
public void test(){
System.out.println("重写GrandPa的test方法");
}
}
/*输出结果
小马在狗叫
重写GrandPa的test方法
*/
7.2 方法重写的细节
(1)子类方法的形参列表、方法名称,要和父类方法的形参列表、方法名称完全一样
(2)返回类型:和父类方法一致或者是父类方法返回类型的子类型
比如父类返回类型是 Object ,子类方法返回类型是String
public class Test {
public static void main(String[] args) {
Son son = new Son();
String result = son.sayHello("小马");
System.out.println(result);
}
}
class Father{
public Object sayHello(String name){
return name+"调用父类的sayHello";
}
}
class Son extends Father{
//重写父类的sayHello方法
public String sayHello(String name){
return name+"调用子类的sayHello";
}
}
/*
小马调用子类的sayHello
*/
(3)方法访问修饰符:不能缩小父类方法的访问权限
访问权限从宽松到严格的顺序是:Public、Protected、缺省、Private
class Father{
protected Object sayHello(String name){
return name+"调用父类的sayHello";
}
}
class Son extends Father{
//X,缩小了父类方法的访问权限
private String sayHello(String name){
return name+"调用子类的sayHello";
}
//√,重写了父类的sayHello方法
public String sayHello(String name){
return name+"调用子类的sayHello";
}
}
8.重写与重载的对比
发生范围 | 方法名 | 形参列表 | 返回类型 | 修饰符 | |
---|---|---|---|---|---|
重载(Overload) | 本类 | 必须一致 | 类型,个数或者顺序至少有一个不同 | 无要求 | 无要求 |
重写(Override) | 父子类 | 必须一致 | 必须完全一致 | 子类方法返回的类型与父类返回的类型一致,或者是其子类型 | 子类不能缩小父类方法的访问范围 |
重载的练习题
判断下方函数是否与void show(int a,char b,double c){}构成重载
void show(int x,char y,double z){}//不是
int show(int a,double c,char b){}//是,参数顺序不同
void show(int a,double c,char b){}//是,参数顺序不同
boolean show(int c,char b){}//是,参数个数不同
void show(){}//是,参数个数不同
double show(int x,char y,double z){}//不是,虽然返回类型不同,但重载对返回类型无要求。参数个数、类型、顺序全都没有改变,不是重载
9.面向对象特征之三:多态
多态的使用前提:① 类的继承关系 ② 方法的重写
9.1 快速入门案例
public class Test {
public static void main(String[] args) {
//animal 编译类型是Animal,运行类型是Dog
Animal animal = new Dog();
animal.cry();//执行到此行时,animal运行类型是Dog,所以cry就是Dog的cry
//animal编译类型仍然是Animal,运行类型编程Cat
animal = new Cat();
animal.cry();//执行到此行时,animal运行类型是Cat,所以cry就是Cat的cry
}
}
class Animal {
public void cry() {
System.out.println("Animal cry");
}
}
class Dog extends Animal {
public void cry() {
System.out.println("Dog cry");
}
}
class Cat extends Animal {
public void cry() {
System.out.println("Cat cry");
}
}
/*输出结果
Dog cry
Cat cry
*/
9.2 为什么需要多态?
案例
引入多态前后 Master 主人类的编写对比
引入多态前后的对比
优势:引入多态后无论有多少种动物,只需要定义一个喂食方法即可,可以"一劳永逸",代码复用性高
引入多态前后测试类 Test 的编写对比:
9.3 对象的多态
对象的多态性:父类的引用指向子类的对象
比如 Animal animal = new Dog();Dog 是子类,Animal 是父类
多态机制可以使得一个对象的编译类型和运行类型不一致
(1)编译类型、运行类型: = 号左边为编译类型, 右边为运行类型
(2)编译:使用 javac 指令,.java -> .class
(3)运行:使用 java 指令, .class ->执行程序
(4)编译类型可以简单理解为编译器编译时看到的类型,运行类型是真正执行 Java 程序时该数据的类型
(5)编译类型在定义对象时就确定了,上述中 animal 的编译类型即 Animal,运行类型为 Dog
9.4 方法形参的多态性
方法形参的多态性:形参是父类类型,实参是子类对象
public class Test {
public static void main(String[] args) {
Person person = new Person();
Cat cat = new Cat();
cat.setNickname("雪球");
person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
person.feed();
}
}
//人类
class Person{
private Pet pet;
public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
this.pet = pet;
}
public void feed(){
pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
}
}
//宠物类
class Pet {
private String nickname; //昵称
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public void eat(){
System.out.println(nickname + "吃东西");
}
}
class Cat extends Pet {
//子类重写父类的方法
public void eat() {
System.out.println("猫咪" + getNickname() + "吃鱼仔");
}
//子类扩展的方法(特有的方法)
public void catchMouse() {
System.out.println("抓老鼠");
}
}
9.5 方法返回类型的多态性
方法返回类型的多态性:返回值类型是父类类型,实际返回的是子类对象
public class Test {
public static void main(String[] args) {
PetShop shop = new PetShop();
Pet dog = shop.salPet("Bird");
dog.setNickname("小白");
dog.eat();
Pet cat = shop.salPet("Cat");
cat.setNickname("雪球");
cat.eat();
}
}
//宠物商店类
class PetShop {
//返回值类型是父类类型,实际返回的是子类对象
public Pet salPet(String type){
switch (type){
case "Bird":
return new Bird();
case "Cat":
return new Cat();
}
return null;
}
}
//宠物类
class Pet {
private String nickname; //昵称
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public void eat(){
System.out.println(nickname + "吃东西");
}
}
//猫类
class Cat extends Pet {
//子类重写父类的方法
public void eat() {
System.out.println("猫咪" + getNickname() + "吃鱼仔");
}
//子类扩展的方法(特有的方法)
public void catchMouse() {
System.out.println("抓老鼠");
}
}
//鸟类
class Bird extends Pet {
//子类重写父类的方法
public void eat() {
System.out.println("鸟" + getNickname() + "吃虫子");
}
//子类扩展的方法(特有的方法)
public void catchAnt() {
System.out.println("抓蚂蚁");
}
}
9.6 数组的多态性
数组的多态性:多态数组,即数组的定义类型为父类类型,保存的元素类型可以为子类
public class Test {
public static void main(String[] args) {
Person persons[] = new Person[2];
persons[0] = new Student();//向上转型,编译类型为Person,运行类型为Student
persons[1] = new Doctor();//向上转型,编译类型为Person,运行类型为Doctor
}
}
class Person{}
class Student extends Person{}
class Doctor extends Person{}
9.7 多态的向上转型
向上转型的本质:父类的引用指向了子类的对象
9.7.1 语法
父类类型 引用名 = new 子类类型()
Animal animal = new Dog()
9.7.2 注意事项
Animal animal = new Dog()
(1)编译类型看左边,运行类型看右边
(2)可以调用父类中的所有成员;成员= 属性 + 方法,不过需要遵守修饰符访问权限
(3)调用方法的规则:当使用 animal.方法名(实参) 时,优先于从子类 Dog 中找方法,找不到再到父类(会一直往上追溯,不限于直接父类)中查找
public class Test1 {
public static void main(String[] args) {
Father f = new Son();
f.say();
f.run();
}
}
class Father{
public void say(){
System.out.println("father say");
}
public void run(){
System.out.println("father run");
}
}
class Son extends Father{
public void say(){
System.out.println("son say");
}
}
/*输出结果
son say
father run
*/
(4)调用属性的规则:当使用 animal.属性名 时,即使子类 Dog 也有该属性并且赋予了其他值,也只会从父类 Animal 类中查找该属性。下面是一个例子:
public class Test1 {
public static void main(String[] args) {
Father f = new Son();
System.out.println(f.name);
}
}
class Father{
String name = "爸爸";
}
class Son extends Father{
String name = "儿子";
}
/*输出结果
爸爸
*/
(5)可以调用子类中的方法,但需要注意的是不能调用子类中有但父类中没有的方法
Animal animal = new Dog()
原因:编译阶段时,编译器看到的 animal 是 Animal 类型,而如果此时调用的方法在父类 Animal 中没有,则编译不通过
案例
原因:在编译阶段,编译器看到的 f 就是 Father 类型,而 Father 类中并没有定义 test() 方法,所以编译不通过
(6)程序运行阶段时,利用 animal.方法名(参数) 调用方法是先从子类开始查找方法,查找不到就往其父类(会一直往上追溯,不限于直接父类)查找
案例
public class Test {
public static void main(String[] args) {
Father f = new Son();
f.test();//追溯到其父类的父类,即GrandPa的test方法
}
}
class GrandPa{
public void test(){
System.out.println("GrandPa test");
}
}
class Father extends GrandPa{}
class Son extends Father{}
9.8 instanceof
在讲多态的向下转型之前,讲一下 instanceof 的使用,这会在后面的向下转型使用到
instanceof的作用:用于判断对象的运行类型是否为XX类型或XX类型的子类型
public class Test1{
public static void main(String[] args) {
//编译类型是Base,运行类型是Sub
Base base = new Sub();
//运行类型是Sub,是Base的子类型,输出true
System.out.println(base instanceof Base);
//运行类型是Sub,输出true
System.out.println(base instanceof Sub);
Object object = new Object();
//运行类型是Object,不是Base类型也不是Base的子类型,输出false
System.out.println(object instanceof Base);
}
}
class Base{}//父类
class Sub extends Base{}//子类
9.9 多态的向下转型
向下转型(Downcasting):将一个父类类型的引用变量转换为其子类类型的引用变量
在某些情况下,当一个对象被向上转型后,它的具体类型信息会丢失,只保留了父类类型的信息。如果需要访问子类中特有的成员,就需要使用向下转型
9.9.1 语法
父类类型 引用名 = new 子类类型();//向上转型
子类类型 引用名 = (子类类型) 父类引用//向下转型
//示例
Person person = new Student();//向上转型
Student student = (Student) person//向下转型
(1)向下转型后,可以调用子类中所有的成员
(2)需要注意的是,在进行向下转型之前,一定要确保对象实际上是子类的实例,否则会导致 ClassCastException
异常。因此,在进行向下转型之前,应该使用 instanceof
运算符进行类型检查,以避免出现异常情况。在 9.9.2示例代码 中会演示如何使用 instanceof
9.9.2 示例代码
class Animal {
public void eat() {
System.out.println("Animal is eating.");
}
}
class Dog extends Animal {
//子类中重写的方法
@Override
public void eat() {
System.out.println("Dog is eating.");
}
//子类中特有的方法
public void bark() {
System.out.println("Dog is barking.");
}
}
public class Test1 {
public static void main(String[] args) {
Animal animal = new Dog(); //向上转型,animal是父类的引用,指向了子类对象
//animal.bark();X,无法访问Dog类中特有的bark()方法
// 使用向下转型之前,最好先检查animal是否实际上是子类的实例
if (animal instanceof Dog) {
Dog dog = (Dog) animal; //向下转型
dog.bark(); // 调用Dog类中的 bark() 方法
} else {
System.out.println("animal is not an instance of Dog");
}
}
}
在上述示例中,首先创建了一个 Dog
类的对象,并将其赋值给一个 Animal
类型的引用变量 animal
,这就是向上转型的过程。然后,通过使用 instanceof
检查 animal
是否是 Dog
类的实例,以确保向下转型时的类型安全
如果 animal
是 Dog
类的实例,那么可以将其转型为 Dog
类型,并使用 dog
引用变量调用 Dog
类中特有的方法 bark()
。如果 animal
不是 Dog
类的实例,则可以根据实际需求进行相应的处理
9.10 成员变量没有多态性
在前面的 9.7.2注意事项 已经提到,由于比较重要,所以这里再重复一下
在多态机制下,即使子类里定义了与父类完全相同的成员变量,这个成员变量依然不可能覆盖父类中定义的实例变量,调用的仍然是父类中的成员变量
案例
public class Test {
public static void main(String[] args) {
Base b = new Sub();//向上转型
System.out.println(b.a);//1,Base中的a
System.out.println(((Sub)b).a);//2,向下转型,调用的是Sub中的a
Sub s = new Sub();
System.out.println(s.a);//2,Sub中的a
System.out.println(((Base)s).a);//1,向上转型,调用的是Base中的a
}
}
class Base{
int a = 1;
}
class Sub extends Base{
int a = 2;
}
/*输出结果
1
2
2
1
*/
9.11 动态绑定机制
(1)调用对象方法时:该方法会和该对象的运行类型绑定。先去运行类型中找此方法,如果查找不到,则发挥继承机制,去其父类中(一直往上追溯,不限于直接父类)查找方法
(2)调用对象属性时:没有动态绑定机制,哪里声明,哪里使用
案例
public class Test {
public static void main(String[] args) {
//编译类型是A,运行类型是B
A a = new B();
//输出20+10=30,调用方法遵循动态绑定机制,调用的是B中的getI()
System.out.println(a.sum());
//输出20+10=30,调用方法遵循动态绑定机制,调用的是B类中的sum1(),而属性没有动态绑定机制,B.sum1()中的i即是B中的i
System.out.println(a.sum1());
}
}
class A{
public int i = 10;
public int sum(){
return getI()+10;
}
public int sum1(){
return i + 10;
}
public int getI() {
return i;
}
}
class B extends A{
public int i = 20;
public int getI(){
return i;
}
public int sum1() {
return i + 10;
}
}
9.12 虚方法调用
Java 虚方法是指:在编译阶段不能确定该方法的调用入口地址,只有在运行阶段才能确定,那么此方法就称为虚方法,即可能被重写的方法
在多态情况下,如果其子类中重写了父类的方法,则此时父类的方法称为虚方法,父类会根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译阶段编译器是无法确定的,只有在运行阶段才能确定
案例
Person 类中定义了 welcome() 方法,各个子类重写了 welcome() ,Person 中的 welcome() 方法即为虚方法
public class Test1 {
public static void main(String[] args) {
Chinese chinese = new Chinese();
Korean korean = new Korean();
Reception reception = new Reception();
reception.service(chinese);
reception.service(korean);
}
}
//门口接待员
class Reception{
public void service(Person person) {//形参为父类对象,实参为子类对象
person.welcome();//根据传入的子类动态调用各国的欢迎语
}
}
//人类
class Person{
public void welcome(){}
}
//中国人
class Chinese extends Person{
public void welcome(){
System.out.println("欢迎光临");
}
}
//韩国人
class Korean extends Person{
public void welcome(){
System.out.println("어서 오세요");
}
}
//泰国人
class Thai extends Person{
public void welcome(){
System.out.println("ยินดีต้อนรั");
}
}
//美国人
class American extends Person{
public void welcome(){
System.out.println("welcome");
}
}
9.13 多态练习题
练习题1
public class Test1{
public static void main(String[] args) {
double d =13.4;//√
long l = (long)d;//√
System.out.println(l);//13
int in = 5;//√
boolean b =(boolean)in;//X,boolean-> int
Object obj = "Hello";//√,向上转型,编译类型是Object,运行类型是String
String objStr = (String)obj;//√,向下转型
System.out.println(objStr);//Hello
Object objPri = new Integer(5);//√,向上转型,编译类型是Object,运行类型是Integer
String str = (String)objPri;//X,ClassCastException,objPri原本指向的是Integer,不能向下转型为String
Integer str1 =(Integer)objPri; //√,向下转型
}
}
练习题2
public class Test1{
public static void main(String[] args) {
Sub sub = new Sub();
System.out.println(sub.count);//20
sub.display();//20
Base base = sub;//向上转型,编译类型是Base,运行类型是Sub(父类的引用指向子类对象)
System.out.println(base == sub);//true,base和sub指向的都是同一个引用
System.out.println(base.count);//10,成员变量没有多态性,调用的是Base类中的count
base.display();//20
}
}
class Base{
int count = 10;
public void display(){
System.out.println(this.count);
}
}
class Sub extends Base{
int count = 20;
public void display(){
System.out.println(this.count);
}
}
/*输出结果
20
20
true
10
20
*/
练习题3
public class Test1 {
public static void main(String[] args) {
Base base = new Sub();//向上转型,编译类型为Base,运行类型为Sub
base.add(1, 2, 3);//调用Sub中的add(int a,int[] arr),输出sub_1
Sub s = (Sub)base;//向下转型
s.add(1,2,3);//调用Sub中的add(int a,int b,int c),输出sub_2
}
}
class Base {
public void add(int a, int... arr) {
System.out.println("base");
}
}
class Sub extends Base {
//因为可变参数的本质就是数组,所以Sub中的这个add方法重写了Base中的add方法
public void add(int a, int[] arr) {
System.out.println("sub_1");
}
//与Sub上面这个add方法构造重载
public void add(int a, int b, int c) {
System.out.println("sub_2");
}
}
练习题4
多态是运行时行为,证明如下:
import java.util.Random;
//证明如下:
class Animal {
protected void eat() {
System.out.println("animal eat food");
}
}
class Cat extends Animal {
protected void eat() {
System.out.println("cat eat fish");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("Dog eat bone");
}
}
class Sheep extends Animal {
public void eat() {
System.out.println("Sheep eat grass");
}
}
public class Test1 {
//根据生成的随机数来返回对应的子类
public static Animal getInstance(int key) {
switch (key) {
case 0:
return new Cat();
case 1:
return new Dog();
default:
return new Sheep();
}
}
public static void main(String[] args) {
int key = new Random().nextInt(3);
System.out.println(key);
//多态
Animal animal = getInstance(key);//向上转型
animal.eat();
}
}