面向对象特征二:继承
通过 extends
关键字,可以声明一个类B继承另外一个类A,定义格式如下:
[修饰符] class 类A {
...
}
[修饰符] class 类B extends 类A {
...
}
继承中的基本概念
类B,称为子类、派生类(derived class)、SubClass
类A,称为父类、超类、基类(base class)、SuperClass
2.3 代码举例
1、父类
package com.atguigu.inherited.grammar;
/*
* 定义动物类Animal,做为父类
*/
public class Animal {
// 定义name属性
String name;
// 定义age属性
int age;
// 定义动物的吃东西方法
public void eat() {
System.out.println(age + "岁的"
+ name + "在吃东西");
}
}
2、子类
package com.atguigu.inherited.grammar;
/*
* 定义猫类Cat 继承 动物类Animal
*/
public class Cat extends Animal {
int count;//记录每只猫抓的老鼠数量
// 定义一个猫抓老鼠的方法catchMouse
public void catchMouse() {
count++;
System.out.println("抓老鼠,已经抓了"
+ count + "只老鼠");
}
}
3、测试类
package com.atguigu.inherited.grammar;
public class TestCat {
public static void main(String[] args) {
// 创建一个猫类对象
Cat cat = new Cat();
// 为该猫类对象的name属性进行赋值
cat.name = "Tom";
// 为该猫类对象的age属性进行赋值
cat.age = 2;
// 调用该猫继承来的eat()方法
cat.eat();
// 调用该猫的catchMouse()方法
cat.catchMouse();
cat.catchMouse();
cat.catchMouse();
}
}
2.4 继承性的细节说明
1、子类会继承父类所有的实例变量和实例方法
从类的定义来看,类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。
- 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存。
- 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,会看它的父类甚至父类的父类是否声明了这个方法,遵循
从下往上
找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误。
所以继承意味着子类的对象除了看子类的类模板还要看父类的类模板。
2、子类不能直接访问父类中私有的(private)的成员变量和方法
子类虽会继承父类私有(private)的成员变量,但子类不能对继承的私有成员变量直接进行访问,可通过继承的get/set方法进行访问。如图所示:
3、在Java 中,继承的关键字用的是“extends”,即子类不是父类的子集,而是对父类的“扩展”
子类在继承父类以后,还可以定义自己特有的方法,这就可以看做是对父类功能上的扩展。
4、Java只支持单继承,不支持多重继承
public class A{}
class B extends A{}
//一个类只能有一个父类,不可以有多个直接父类。
class C extends B{} //ok
class C extends A,B... //error
class A{}
class B extends A{}
class C extends B{}
说明:
子类和父类是一种相对的概念
顶层父类是Object类。所有的类默认继承Object,作为父类。
一个父类可以同时拥有多个子类**
class A{}
class B extends A{}
class D extends A{}
class E extends A{}
方法的重写
父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于自己当前的类,该怎么办呢?子类可以对从父类中继承来的方法进行改造,我们称为方法的重写 (override、overwrite)
。也称为方法的重置
、覆盖
。
比如新的手机增加来电显示头像的功能,代码如下:
package com.atguigu.inherited.method;
public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
package com.atguigu.inherited.method;
//SmartPhone:智能手机
public class SmartPhone extends Phone{
//重写父类的来电显示功能的方法
@Override
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
}
//重写父类的通话功能的方法
@Override
public void call() {
System.out.println("语音通话 或 视频通话");
}
}
package com.atguigu.inherited.method;
public class TestOverride {
public static void main(String[] args) {
// 创建子类对象
SmartPhone sp = new SmartPhone();
// 调用父类继承而来的方法
sp.call();
// 调用子类重写的方法
sp.showNum();
}
}
@Override使用说明:
写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰的知道这是一个重写的方法。
3.2 方法重写的要求
-
子类重写的方法
必须
和父类被重写的方法具有相同的方法名称
、参数列表
。 -
子类重写的方法的返回值类型
不能大于
父类被重写的方法的返回值类型。(例如:Student < Person)。
注意:如果返回值类型是基本数据类型和void,那么必须是相同
- 子类重写的方法使用的访问权限
不能小于
父类被重写的方法的访问权限。(public > protected > 缺省 > private)
注意:① 父类私有方法不能重写 ② 跨包的父类缺省的方法也不能重写
- 子类方法抛出的异常不能大于父类被重写方法的异常
此外,子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。
小结:方法的重载与重写
方法的重载:方法名相同,形参列表不同。不看返回值类型。
方法的重写:见上面。
(1)同一个类中
package com.atguigu.inherited.method;
public class TestOverload {
public int max(int a, int b){
return a > b ? a : b;
}
public double max(double a, double b){
return a > b ? a : b;
}
public int max(int a, int b,int c){
return max(max(a,b),c);
}
}
(2)父子类中
package com.atguigu.inherited.method;
public class TestOverloadOverride {
public static void main(String[] args) {
Son s = new Son();
s.method(1);//只有一个形式的method方法
Daughter d = new Daughter();
d.method(1);
d.method(1,2);//有两个形式的method方法
}
}
class Father{
public void method(int i){
System.out.println("Father.method");
}
}
class Son extends Father{
public void method(int i){//重写
System.out.println("Son.method");
}
}
class Daughter extends Father{
public void method(int i,int j){//重载
System.out.println("Daughter.method");
}
}
关键字:super
super的理解
在Java类中使用super来调用父类中的指定操作:
- super可用于访问父类中定义的属性
- super可用于调用父类中定义的成员方法
- super可用于在子类构造器中调用父类的构造器
注意:
- 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
- super的追溯不仅限于直接父类
- super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识
class Father{
int a = 10;
int b = 11;
}
class Son extends Father{
int a = 20;
public void test(){
//子类与父类的属性同名,子类对象中就有两个a
System.out.println("子类的a:" + a);//20 先找局部变量找,没有再从本类成员变量找
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
//子类与父类的属性不同名,是同一个b
System.out.println("b = " + b);//11 先找局部变量找,没有再从本类成员变量找,没有再从父类找
System.out.println("b = " + this.b);//11 先从本类成员变量找,没有再从父类找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
public void method(int a, int b){
//子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a
System.out.println("局部变量的a:" + a);//30 先找局部变量
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
System.out.println("b = " + b);//13 先找局部变量
System.out.println("b = " + this.b);//11 先从本类成员变量找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
}
class Test{
public static void main(String[] args){
Son son = new Son();
son.test();
son.method(30,13);
}
}
总结:起点不同(就近原则)
-
变量前面没有super.和this.
- 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的
局部变量
, - 如果不是局部变量,先从当前执行代码的
本类去找成员变量
- 如果从当前执行代码的本类中没有找到,会往上找
父类声明的成员变量
(权限修饰符允许在子类中访问的)
- 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的
-
变量前面有this.
- 通过this找成员变量时,先从当前执行代码的本类去找成员变量
- 如果从当前执行代码的本类中没有找到,会往上找父类声明的成员变量(权限修饰符允许在子类中访问的)
-
变量前面super.
- 通过super找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
- 如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)
子类构造器中调用父类构造器
- 子类继承父类时,不会继承父类的构造器。只能通过super()或super(实参列表)的方式调用父类的构造器。
- super();:子类构造器中一定会调用父类的构造器,默认调用父类的无参构造,super();可以省略。
- 当父类中没有空参数的构造器时,子类的构造器必须通过this(参数列表)或者super(参数列表)语句指定调用本类或者父类中相应的构造器。同时,只能”二选一”
- super()和super(实参列表)都只能出现在子类构造器的首行
- 如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有无参的构造器,则编译出错
public class Person {
private String name;
private int age;
private Date birthDate;
public Person(String name, int age, Date d) {
this.name = name;
this.age = age;
this.birthDate = d;
}
public Person(String name, int age) {
this(name, age, null);
}
public Person(String name, Date d) {
this(name, 30, d);
}
public Person(String name) {
this(name, 30);
}
}
public class Student extends Person {
private String school;
public Student(String name, int age, String s) {
super(name, age);
school = s;
}
public Student(String name, String s) {
super(name);
school = s;
}
// 编译出错: no super(),系统将调用父类无参数的构造器。
public Student(String s) {
school = s;
}
}
package com.atguigu.constructor;
public class TestEmployee {
public static void main(String[] args) {
Manager m1 = new Manager();
System.out.println(m1.getInfo());
Manager m2 = new Manager("张三",23,20000,0.1);
System.out.println(m2.getInfo());
}
}
形式一:
class A{
}
class B extends A{
}
class Test{
public static void main(String[] args){
B b = new B();
//A类和B类都是默认有一个无参构造,B类的默认无参构造中还会默认调用A类的默认无参构造
//但是因为都是默认的,没有打印语句,看不出来
}
}
形式二:
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类默认有一个无参构造,
//B类的默认无参构造中会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"
}
}
形式三:
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类显示声明一个无参构造,
//B类的无参构造中虽然没有写super(),但是仍然会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"和"B类无参构造器")
}
}
形式四:
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类显示声明一个无参构造,
//B类的无参构造中明确写了super(),表示调用A类的无参构造
//可以看到会输出“A类无参构造器"和"B类无参构造器")
}
}
形式五:
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test05{
public static void main(String[] args){
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造没有写super(...),表示默认调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}
形式六:
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(int a){
super(a);
System.out.println("B类有参构造器");
}
}
class Test07{
public static void main(String[] args){
B b = new B(10);
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个有参构造,
//B类的有参构造明确写super(a),表示调用A类的有参构造
//会打印“A类有参构造器"和"B类有参构造器"
}
}
形式七:
class A{
A(){
System.out.println("A类无参构造器");
}
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
super();//可以省略,调用父类的无参构造
System.out.println("B类无参构造器");
}
B(int a){
super(a);//调用父类有参构造
System.out.println("B类有参构造器");
}
}
class Test8{
public static void main(String[] args){
B b1 = new B();
B b2 = new B(10);
}
}
小结:this与super
1、this和super的意义
this:当前对象
- 在构造器和非静态代码块中,表示正在new的对象
- 在实例方法中,表示调用当前方法的对象
super:引用父类声明的成员
2、this和super的使用格式
- this
- this.成员变量:表示当前对象的某个成员变量,而不是局部变量
- this.成员方法:表示当前对象的某个成员方法,完全可以省略this.
- this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
- super
- super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
- super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
- super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错
面向对象特征三:多态性
7.1 为什么需要多态性(polymorphism)?
开发中,有时我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。
案例:
(1)声明一个Dog类,包含public void eat()方法,输出“狗啃骨头”
(2)声明一个Cat类,包含public void eat()方法,输出“猫吃鱼仔”
(3)声明一个Person类,功能如下:
- 包含宠物属性
- 包含领养宠物方法 public void adopt(宠物类型Pet)
- 包含喂宠物吃东西的方法 public void feed(),实现为调用宠物对象.eat()方法
package com.atguigu.polymorphism.problem;
public class Dog {
public void eat(){
System.out.println("狗啃骨头");
}
}
package com.atguigu.polymorphism.problem;
public class Cat {
public void eat(){
System.out.println("猫吃鱼仔");
}
}
package com.atguigu.polymorphism.problem;
public class Person {
private Dog dog;
//adopt:领养
public void adopt(Dog dog){
this.dog = dog;
}
//feed:喂食
public void feed(){
if(dog != null){
dog.eat();
}
}
/*
问题:
1、从养狗切换到养猫怎么办?
修改代码把Dog修改为养猫?
2、或者有的人养狗,有的人养猫怎么办?
3、要是同时养多个狗,或猫怎么办?
4、要是还有更多其他宠物类型怎么办?
如果Java不支持多态,那么上面的问题将会非常麻烦,代码维护起来很难,扩展性很差。
*/
}
多态的形式和体现
对象的多态性
多态性,是面向对象中最重要的概念,在Java中的体现:对象的多态性:父类的引用指向子类的对象
格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)
父类类型 变量名 = 子类对象;
举例:
Person p = new Student();
Object o = new Person();//Object类型的变量o,指向Person类型的对象
o = new Student(); //Object类型的变量o,指向Student类型的对象
对象的多态:在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象
多态的理解
Java引用变量有两个类型:编译时类型
和运行时类型
。编译时类型由声明
该变量时使用的类型决定,运行时类型由实际赋给该变量的对象
决定。简称:编译时,看左边;运行时,看右边。
- 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
- 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法)
“看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
多态的使用前提:① 类的继承关系 ② 方法的重写
package com.atguigu.polymorphism.grammar;
public 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 + "吃东西");
}
}
package com.atguigu.polymorphism.grammar;
public class Cat extends Pet {
//子类重写父类的方法
@Override
public void eat() {
System.out.println("猫咪" + getNickname() + "吃鱼仔");
}
//子类扩展的方法
public void catchMouse() {
System.out.println("抓老鼠");
}
}
package com.atguigu.polymorphism.grammar;
public class Dog extends Pet {
//子类重写父类的方法
@Override
public void eat() {
System.out.println("狗子" + getNickname() + "啃骨头");
}
//子类扩展的方法
public void watchHouse() {
System.out.println("看家");
}
}
1、方法内局部变量的赋值体现多态
package com.atguigu.polymorphism.grammar;
public class TestPet {
public static void main(String[] args) {
//多态引用
Pet pet = new Dog();
pet.setNickname("小白");
//多态的表现形式
/*
编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
*/
pet.eat();//运行时执行子类Dog重写的方法
// pet.watchHouse();//不能调用Dog子类扩展的方法
pet = new Cat();
pet.setNickname("雪球");
pet.eat();//运行时执行子类Cat重写的方法
}
}
2、方法的形参声明体现多态
package com.atguigu.polymorphism.grammar;
public class Person{
private Pet pet;
public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
this.pet = pet;
}
public void feed(){
pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
}
}
package com.atguigu.polymorphism.grammar;
public class TestPerson {
public static void main(String[] args) {
Person person = new Person();
Dog dog = new Dog();
dog.setNickname("小白");
person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
person.feed();
Cat cat = new Cat();
cat.setNickname("雪球");
person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
person.feed();
}
}
3、方法返回值类型体现多态
package com.atguigu.polymorphism.grammar;
public class PetShop {
//返回值类型是父类类型,实际返回的是子类对象
public Pet sale(String type){
switch (type){
case "Dog":
return new Dog();
case "Cat":
return new Cat();
}
return null;
}
}
package com.atguigu.polymorphism.grammar;
public class TestPetShop {
public static void main(String[] args) {
PetShop shop = new PetShop();
Pet dog = shop.sale("Dog");
dog.setNickname("小白");
dog.eat();
Pet cat = shop.sale("Cat");
cat.setNickname("雪球");
cat.eat();
}
}
多态的好处和弊端
好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。
Student m = new Student();
m.school = "pku"; //合法,Student类有school成员变量
Person e = new Student();
e.school = "pku"; //非法,Person类没有school成员变量
// 属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。
使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,符合开闭原则。
父类引用做方法的形参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。另外,即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。
补充:开闭原则OCP
- 对扩展开放,对修改关闭
- 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能
向上转型和向下转型
为什么要类型转换呢?
因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间
,就会出现类型转换的现象。
但是,使用父类变量接收了子类对象之后,我们就不能调用
子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过
。
-
向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
- 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
- 此时,一定是安全的,而且也是自动完成的
-
向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断
如何向上转型与向下转型
向上转型:自动完成
向下转型:(子类类型)父类变量
package com.atguigu.polymorphism.grammar;
public class ClassCastTest {
public static void main(String[] args) {
//没有类型转换
Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog
//向上转型
Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
pet.setNickname("小白");
pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
// pet.watchHouse();//不能调用父类没有的方法watchHouse
Dog d = (Dog) pet;
System.out.println("d.nickname = " + d.getNickname());
d.eat();//可以调用eat方法
d.watchHouse();//可以调用子类扩展的方法watchHouse
Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
//这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
}
}
7.4.3 instanceof关键字
为了避免ClassCastException的发生,Java提供了 instanceof
关键字,给引用变量做类型的校验。如下代码格式:
//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A
- 说明:
- 只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
- 如果对象a属于类A的子类B,a instanceof A值也为true。
- 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。
示例代码:
package com.atguigu.polymorphism.grammar;
public class TestInstanceof {
public static void main(String[] args) {
Pet[] pets = new Pet[2];
pets[0] = new Dog();//多态引用
pets[0].setNickname("小白");
pets[1] = new Cat();//多态引用
pets[1].setNickname("雪球");
for (int i = 0; i < pets.length; i++) {
pets[i].eat();
if(pets[i] instanceof Dog){
Dog dog = (Dog) pets[i];
dog.watchHouse();
}else if(pets[i] instanceof Cat){
Cat cat = (Cat) pets[i];
cat.catchMouse();
}
}
}
}
标签:java,子类,void,public,面向对象,println,父类,class
From: https://www.cnblogs.com/lanzhi666/p/18671834