继承和多态
面向对象编程允许从已经存在的类中定义新类,这称为继承。继承是面向对象编程的一个重要特征。假设在QQ宠物游戏中要定义一个类,对狗狗,企鹅还有猪猪建模。这些类有很多共同的特性。我们可以使用继承来编码冗余并使系统更易于理解和易于维护。
父类和子类
使用类来对同一类型的对象建模。不同的类也可能会有一些共同的特征和行为,这些共同的特征和行为都统一放在一个类中,它是可以被其他类所共享的。可以定义特定的类继承自通用类。这些特定的类继承通用类中的特征和方法。考虑一下QQ宠物对象。假设要设计类建模像Dog和Penguin这样的宠物对象。宠物对象有许多共同的行为和属性。这样一个通用类Pet可以用来建模所有的宠物对象。
package edu.uestc.avatar.inherit.pet; /** * 宠物这一类事务共同的属性和行为抽取在一个通用的公共类-----Pet * @author Tiger * */ public class Pet { //共同的特征---名字 private String name; //共同的特征---健康值 private int health; //共同的特征---爱心值 private int love; public Pet(String name, int health, int love) { System.out.println("pet---构造方法"); this.name = name; this.health = health; this.love = love; } /** * 自白 */ public void sayHello() { System.out.print("我的名字叫" + this.name + ",我的健康值是" + health + ",我和主人的亲密度是" + love); } /** * 吃东西 */ public void eat() { if(this.health <= 95) { this.health += 3; this.love -= 5; } } /** * 陪主人玩 */ public void play() { if(this.health > 60) { this.health -= 5; this.love += 3; } } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getHealth() { return health; } public void setHealth(int health) { this.health = health; } public int getLove() { return love; } public void setLove(int love) { this.love = love; } }
Dog类继承了Pet类所有可以访问的数据域和方法。除此之外,它还有个新的数据域strain以及与strain相关的setter和getter方法。
package edu.uestc.avatar.inherit.pet; /** * Dog扩展自通用的宠物类Pet(继承) * */ public class Dog extends Pet{ private String strain; //在子类中通过super()调用父类的构造方法 public Dog(String name,int health,int love,String strain) { super(name,health,love);//调用父类的构造方法必须在第一句 this.strain = strain; System.out.println("dog---构造方法"); } /** * super关键字 * 1、子类调用父类中的方法 * 2、在子类构造方法中调用父类的构造方法 */ public void sayHello() { super.sayHello();//子类调用父类中的方法 System.out.println(",我的类型是" + strain); } public String getStrain() { return strain; } public void setStrain(String strain) { this.strain = strain; } }
Penguin类继承了Pet类所有可以访问的数据域和方法。除此之外,它还有个新的数据域gender以及与gender相关的setter和getter方法
package edu.uestc.avatar.inherit.pet; /** * 子类是父类的特殊化,每个子类的实例都是父类的实例。 * */ public class Penguin extends Pet{ private String gender; public Penguin(String name,String gender) { super(name,100,100); this.gender = gender; System.out.println("penguin---构造方法"); } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } /** * 为满足子类的需要,子类需要覆盖父类中定义的方法实现----------方法重写 * 1、访问修饰符:子类重写方法访问权限不能小于父类的访问修饰权限 * 2、返回值类型:一般具有相同的返回值类型,子类重写的方法返回值类型不能大于父类的返回值类型 * 3、异常类型:子类重写的方法不能抛出比父类更大类型的异常 * 4、仅当实例方法可访问时,才能进行方法重写 * 5、静态方法也能被继承,静态方法不能被覆盖。 * @Override 编译器可帮我们检查是否是符合方法重写要求的 */ @Override public void sayHello() { System.out.println("我的名字叫" + this.getName() + ",我的健康值是" + getHealth() + ",我和主人的亲密度是" + getLove() + ",我的性别是:" + gender); }
//企鹅所特有的方法:撒娇
public void 撒娇(){
} }
在java术语中,如果Dog类继承自Pet类,那么就将Dog类称为次类,将Pet称为超类。超类也称为父类或基类;次类也称为子类,扩展类或者派生类
关于继承应该注意的几个关键点:
- 和传统的理解不同,子类并不是父类的一个子集。实际上,一个子类比它的父类包含更多的信息和方法。
- 父类中的私有属性在该类之外是不可访问的。因此,不能在子类中直接使用。但是,如果父类中定义了公共的setter/getter,那么可以通过这些公共的setter/getter来修改和访问它们
- 继承是用来为is-a关系建模的。不要仅仅为了重用方法而盲目扩展一个类。例如:尽管Person类和Tree类可以共享类似高度和重量这样的通用特性,但是从Person类扩展出Tree类是毫无意义的。一个父类和它的子类之间必须存在“是一种”(is-a)关系
- 不是所有的is-a关系都该用继承来建模。例如:正方形是一种矩形,但是不应该定义一个Square类来扩展Rectangle类,因为width和height属性并不适合于正方形。
- java是不允许多重继承的。一个java类只能直接继承自一个父类。多重继承可以通过接口来实现。
使用super关键字
关键字super指代父类,可以用于调用父类中的普通方法和构造方法,前面介绍了关键字this的作用,它是对调用对象的引用。关键字super是指这个super关键字所在的类的父类。关键字super可以用于两种途径:
1)调用父类构造方法
构造方法用于构建一个类的实例。不同于属性和普通方法,父类的构造方法不会被子类继承。它们只能使用super从子类的构造方法中调用。语法:super或者super(parameters)
public Penguin(String name,String gender) { //显式的调用父类的构造方法 super(name);//必须是构造方法的第一题语句 this.gender = gender; System.out.println("我是Penguin的构造方法"); }
构造方法链
在任何情况下,构造一个类的实例时,将会调用沿着继承链的所有父类的构造方法。当构造一个子类对象时,子类构造方法会在完成自己任务之前,首先调用它的父类构造方法。如果父类继承自其他类,那么父类构造方法又会在完成自己任务前调用它自己的父类的构造方法。这个过程持续到沿着这个继承体系结构的最后一个构造方法被调用为止。如果没有被显式的调用,编译器将会自动添加super()作为构造方法的第一条语句。
2)调用父类方法
方法重写
出现的前提:有子类继承父类
出现的场景:当子类继承了父类的方法后发现父类的方法不适用于子类,则子类可以通过重写父类的方法。
在调用时,调用的是子类重写后的方法,而不是来自于父类的方法。
重写的规则:
1)要求子类与父类的方法:返回值类型、方法名、形参列表必须完全相同。
2)子类的修饰符权限不能小于父类的修饰符权限
3)若父类方法抛异常,那么子类方法抛的异常类型不能大于父类方法抛的异常类型
4)子类可以重写父类的静态方法,但是必须是同为静态的(对父类静态方法来说是隐藏,可以使用父类.静态方法的形式访问)
Object类及其toString()方法
java中的所有类都继承自java.lang.Object类。toString()方法:
1)当我们打印一个引用数据类型的对象时,默认调用的是这个对象的toString();//Loan@15037e5
2)当我们重写了toString()后再打印该对象,会调用重写后的toString();
3)像String、File、Date等类已经重写了toString方法。
多态
多态意味着父类的变量可以指向子类对象。理解成一种事物的多种表现形态.
首先,定义两个有用的术语:子类型和父类型。一个类实际上定义了一种类型。子类定义的类型称为子类型(subtype),父类定义的类型称为父类型(supertype)。因此,可以说Dog是Pet的子类型,而Pet是Dog的父类型。
继承关系使一个子类继承父类的特征,并且附加一些新特征。子类是它的父类的特殊化,每个子类的实例都是其父类的实例,但是反过来就不成立。例如:每个狗狗都是一个宠物对象,但并非每个宠物对象都是狗狗。因此,总可以将子类的实例传给需要父类型的参数。
/* * 多态 * 动态绑定:编译看左边(父类型),运行看右边 * Pet[]:声明类型 * dog,penguin:实际类型 */ Pet[] pets = {new Dog("毛毛",100,100,"斑点狗"),new Penguin("pedro", "male"),new Dog("天天",100,100,"二哈")}; for(Pet pet : pets) pet.sayHello(); }
动态绑定
在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法。
声明类型和实际类型
//一个变量必须被声明为某种类型 Pet pet声明类型 Pet pet = new Dog("毛毛",100,100,"斑点狗");//new Dog("毛毛",100,100,"斑点狗");实际类型
pet调用哪个play()方法由pet的实际类型决定。这称为动态绑定。
动态绑定和静态绑定
编译器在每次调用方法时都要进行搜索,时间开销相当大。因此虚拟机会预先为每个类创建一个方发表(method table),其中列出了所有方法的签名和实际调用的方法。
对象转换和instanceof运算符
将一个类型强制转换为另外一个类型的过程称为类型转换,语法格式为:
Pet pet = new Penguin(); Penguin penguin = (Penguin)penguin
在java中,每个对象变量都属于一个类型。将一个值存入变量时,编译器将检查是否允许该操作。将一个子类的引用赋给超类类型(向上转型),编译器是允许的。但将一个超类的引用赋给一个子类变量(向下转型),必须进行类型转换。
如果试图在继承链上进行向下的类型转换,并且谎报有关对象包含的内容,会发生什么情况呢?
Pet pet = new Penguin();
Dog dog = (Dog)pet;
java运行时系统将报告这个错误,产生一个ClassCastException异常。如果没有捕获这个异常,程序就会终止。因此,应该养成一个良好的程序设计习惯,在进行类型转换之前,先查看一下是否能够成功地转换。使用instanceof运算符就可以实现
if(pet instanceof Penguin){ Penguin penguin = (Penguin)pet; ... } //jdk 14的模式匹配写法 if(pet instanceof Penguin penguin){ ... }
实际上,通过类型转换调整对象的类型并不是一个好的做法。在列举的示例中,大多数情况并不需要将Pet对象转成Penguin对象。两个类的对象都能够调用play方法,这是因为实现多态性的动态绑定机制能够自动的找到相应的方法。只有在使用Penguin中特有的方法时才需要进行类型转换。
Object类的equals方法
1)equals不适用于基本数据类型,equals只适用于引用数据类型
2)如果不重写equals方法,则比较的是两个对象的地址值,等效于“==”.因此,应该根据需要在自己的客户类中重写equals方法。
3)像String、File、Date等类重写了Object的equals方法,比较的是两个String对象的具体的值,而不是地址值。
protected数据和方法
1、基类的protected成员是包内可见的,并且对子类可见;
2、若子类与基类不在同一包中,那么在子类中,子类实例可以访问其从基类继承而来的protected方法,而不能访问基类实例的protected数据域
防止扩展和重写
final关键字:最终的,可以用来修饰类、属性、方法
1.final修饰类:这个类不能被继承
2.final修饰方法:这个方法不能被重写,当一个方法所要体现的功能已经被确定,则用final修饰。
3.final修饰属性:表示常量,则该常量必须有初始化值。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南