Java面向对象编程(四)多态

四、多态

4.1 多态(重中之重)

概念

  • 多态主要指的是同一事物表现出的多种形态
  • 饮料:可乐、雪碧、红牛、脉动、…
  • 人:学生、教师、工人、…

语法格式

父类类型 引用变量 = new 子类类型();
  • 如:
    Shape sr = new Rect();
    sr.show();
  • 总之一句话:父类类型的引用指向子类类型的对象,形成了多态

特点

  • 当父类类型的引用指向子类类型的对象时,父类类型的引用可以直接调用父类独有方法
  • 当父类类型的引用指向子类类型的对象时,父类类型的引用不可以直接调用子类独有方法
    • 不能直接,但是可以间接(即强转)
  • 对于父子类都有的非静态方法来说,编译阶段调用父类版本运行阶段调用子类重写的版本(动态绑定
  • 对于父子类都有的静态方法来说,编译运行都调用父类版本

4.2 案例:Shape类和Rect类的实现

  • 要点:
    • 在多态中,当子类中重写某个非静态方法后,调用该方法时:在编译阶段调用父类的方法,在运行阶段调用的是子类的方法
      (记忆方法:披着羊皮的狼,远看(编译)是羊(父类),近看(执行)是狼(子类))
/*
 *	父类方法Shape	
 */
public class Shape {
	
    private int x; // 横坐标
    private int y; // 纵坐标

    public Shape() {
    }

    public Shape(int x, int y) {
        setX(x);
        setY(y);
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public void show() {
        System.out.println("横坐标是:" + getX() + ", 纵坐标是:" + getY());
    }

}
public class Rect extends Shape {
	
	// 子类Rect独有的成员变量
    private int length; // 长度
    private int width;  // 宽度

    public Rect() {
    	// super(); // 编译器会默认加入此语句,因此可以省略不写
    }

    public Rect(int x, int y, int length, int width) {
        super(x, y); // 继承父类的成员变量
        setLength(length);
        setWidth(width);
    }

    // 长度
    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        if(length > 0) {
            this.length = length;
        } else {
            System.out.println("长度不合理哦!");
        }
    }

    // 宽度
    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        if(width > 0) {
            this.width = width;
        } else {
            System.out.println("宽度不合理哦!");
        }
    }

    // 重写show方法
    @Override
    public void show() {
        super.show(); // 默认调用子类方法时,不用父类方法,所以父类方法只在编译时调用
        			  // 运行阶段如果也要执行父类方法,那就手动加入super.show()

        System.out.println("长度是:" + getLength() + ", 宽度是:" + getWidth());
    }

}
public class ShapeRectTest {

    public static void main(String[] args) {

        // 1.声明Shape类型的引用指向Shape类型的对象并打印特征
        Shape s = new Shape(10,20);
        s.show();

        System.out.println("-----------------------------");
        // 2.声明Rect类型的引用指向Rect类型的对象并打印特征
        Rect r = new Rect(1,2,1000,2000);
        r.show();

        System.out.println("-----------------------------");
        // 3.声明Shape类型的引用指向Rect类型的对象并打印特征
        Shape sr = new Rect(1111, 2222, 3333, 4444); // 形成了多态
        // 当Rect类中重写show方法后,下面的代码在编译阶段调用Shape类的方法,在运行阶段调用的是Rect类的方法
        sr.show();
    }
}

4.3 引用数据类型

引用数据类型之间的转换方法(多态)

  • 当父类类型的引用指向子类类型的对象时,父类类型的引用不可以直接调用子类独有方法,但是可以间接调用,采用的是引用数据类型的转换
  • 引用数据类型的转换方式分为两种:
    • 自动类型转换:小类型 -> 大类型,即子类 -> 父类,也称向上转型
      如 Shape sr = new Rect(1, 2, 3, 4); // 即正常的 父类类型的引用指向子类类型的对象
    • 强制类型转换:大类型 -> 小类型,即父类 -> 子类,也称向下转型或显式类型转型
      如 int ib = ((Rect) sr).getLength(); // 父类转成子类,此时可以调用子类的独有方法

引用数据类型转换的注意事项

  • 引用数据类型之间的转换 必须发生在 父子类之间 ,否则编译报错
  • 若强转的目标类型并不是该引用真正指向的数据类型时则编译通过,运行阶段发生类型转换异常
    • 父类是Shape,再新建一个子类Circle

public class Circle extends Shape { }

如果进行强转:Circle c1 = (Circle)sr;
编译OK,运行时抛出异常:ClassCastException 类型转换异常
- 现在一共出现四种异常:
(1) ArithmeticException
(2) ArraysOutOfBoundsException
(3) NullPointerException
(4) ClassCastException
- 引用类型转换在运行时出现异常的原因:
编译阶段sc调用的是 父类 版本Shape的方法,而Shape和Circle是父子关系,因此编译不报错
运行阶段sc调用的是 子类 版本Rect的方法,但是Rect和Circle不是父子关系,因此运行时报错

public class Circle extends Shape {
    int ir;  // 半径

    public Circle() {
    }

    public Circle(int x, int y, int ir) {
        super(x, y);
        setIr(ir);
    }

    public int getIr() {
        return ir;
    }

    public void setIr(int ir) {
        if(ir > 0) {
            this.ir = ir;
        } else {
            System.out.println("半径不正确哦!");
        }
    }

    @Override
    public void show() {
        super.show();	// 默认调用子类方法时,不用父类方法,所以父类方法只在编译时调用
        				// 运行阶段如果也要执行父类方法,那就手动加入super.show()
        System.out.println("圆的半径是:" + getIr());
    }

}

instanceof

  • 为了避免强转时发生错误,应该在强转之前进行判断:
if(引用变量 instanceof 数据类型)  // 判断引用变量指向的对象是否是后面的数据类型
例如:
if(sr instanceof Circle) {
	System.out.println("可以放心地转换!");
	Circle c1 = (Circle)sr;
} else {
	System.out.println("强转有风险,操作需谨慎!");
}

4.4 案例:Rect对象特征打印(方法的调用作为参数)

public class ShapeTest {
	
	// 自定义成员方法实现将参数指定矩形对象特征打印出来的行为
	// static最大的意义在于:调用时直接用 类名.
	public static void draw(Rect r) {  // 为什么Rect r能当形参?类比 String s 
		r.show();
	}

	public static void main(String[] args) {

		// Rect r = new Rect(1, 2, 30, 40);
		ShapeTest.draw(new Rect(1, 2, 30, 40));  // 注意传实参的方式
	}
}

4.5 多态的实际意义(重点)

  • 多态的实际意义在于屏蔽不同子类的差异性,实现通用的编程带来不同的效果
  • 通过以下例子来说明:自定义成员方法实现分别打印矩形、圆形对象的特征,对象由参数传入,然后自定义成员方法来既能打印矩形也能打印圆形对象的特征,对象有参数传入
  • 不采用多态的特性时:(很繁琐)
public class ShapeTest {

    // 自定义成员方法实现将参数指定矩形对象特征打印出来的行为,也就是绘制图形的行为
    public static void draw(Rect r) {
        r.show();
    }

    // 自定义成员方法实现将参数指定圆形对象特征打印出来的行为
    public static void draw(Circle c) { // 重载
        c.show();
    }
    
    // 自定义成员方法实现既能打印矩形对象又能打印圆形对象的特征,对象由参数传入
    public static void draw(Circle c, Rect r) {
        c.show();
        r.show();
    }

    public static void main(String[] args) {

        // Rect r = new Rect(1, 2, 30, 40);
        ShapeTest.draw(new Rect(1, 2, 30, 40));
        
        System.out.println("-----------------------");
        ShapeTest.draw(new Circle(5, 6, 700));
        
        System.out.println("-----------------------");
        ShapeTest.draw(new Circle(111,222,333), new Rect(444,555,66,777));
    }
}
  • 采用多态的特性时:(简单
public class ShapeTest {

	/*
    // 自定义成员方法实现将参数指定矩形对象特征打印出来的行为,也就是绘制图形的行为
    public static void draw(Rect r) {
        r.show();
    }

    // 自定义成员方法实现将参数指定圆形对象特征打印出来的行为
    public static void draw(Circle c) { // 重载
        c.show();
    }
    
    // 自定义成员方法实现既能打印矩形对象又能打印圆形对象的特征,对象由参数传入
    public static void draw(Circle c, Rect r) {
        c.show();
        r.show();
    }*/
    
    // 自定义成员方法实现既能打印矩形对象又能打印圆形对象的特征,对象由参数传入
    // Shape s = new Rect(1,2,3,4);	父亲类型的引用子类类型的对象,形成了多态
    // Shape s = new Circle(1,2,3);	父亲类型的引用子类类型的对象,形成了多态


    // 多态的使用场合之一:通过参数传递形成了多态
    public static void draw(Shape s) {
    	// 编译阶段调用父类的版本,运行阶段调用子类重写以后的版本
		s.show();
	}

    public static void main(String[] args) {

        // Rect r = new Rect(1, 2, 30, 40);
        ShapeTest.draw(new Rect(1, 2, 30, 40));
        
        System.out.println("-----------------------");
        ShapeTest.draw(new Circle(5, 6, 700));
        
        System.out.println("-----------------------");
        //ShapeTest.draw(new Circle(111, 222, 333), new Rect(444, 555, 666, 777));
        ShapeTest.draw(new Circle(111, 222, 333)); 		// 打印圆形
        ShapeTest.draw(new Rect(444, 555, 666, 777));	// 打印矩形
    }
}

4.6 抽象方法和抽象类(重点)

抽象方法的概念

  • 抽象方法主要指不能具体实现的方法并且使用 abstract 关键字修饰,即没有方法体(抽象方法不能被调用)
  • 具体格式:
访问权限 abstract 返回值类型 方法名();
如: 
	public abstract void cry();	// 注意与 public abstract void cry(){} 的区别

抽象类的概念

  • 抽象类主要指不能具体实例化的类并且使用abstract关键字修饰,即不能创建对象
    (其实真正意义上的抽象类应该是:具有抽象方法并且使用abstract关键字修饰的类)
  • 抽象类的类名命名规范:以Abstract开头,如public abstract class AbstractTest { }

抽象方法和抽象类的关系

  • 抽象类中可以有成员变量、成员方法、构造方法;
    • 其中抽象类的构造方法存在意义:子类可以用super()的方式来调用父类的构造方法
  • 抽象类中可以没有抽象方法,也可以有抽象方法;也可以有静态方法
  • 拥有抽象方法的类必须是抽象类,因此真正意义上的抽象类是具有抽象方法并且使用abstract关键字修饰的类
  • 为什么抽象类不能实例化(不能使用new创建对象):
    • 只要是抽象类,类体里就有可能存在抽象方法,而抽象方法是不能调用的,因此为了防止程序员不小心调用到抽象方法引起错误,Java干脆设置抽象类不能使用new来创建对象

抽象类的实际意义

  • 抽象类的实际意义在于被继承多态),而不是在于创建对象
  • 当一个类继承抽象类后必须重写抽象方法,否则该类也变成抽象类,即:抽象类对子类具有强制性和规范性,因此也叫做 模板设计模式
/*
	创建抽象类AbstractTest
 */
public abstract class AbstractTest {
    private int cnt;

    public AbstractTest() {
    }

    public AbstractTest(int cnt) {
        setCnt(cnt);
    }

    public int getCnt() {
        return cnt;
    }

    public void setCnt(int cnt) {
        this.cnt = cnt;
    }

    // 自定义抽象方法
    public abstract void show();

    public static void main(String[] args) {

        // 声明该类类型的引用指向该类类型的对象
        //AbstractTest at = new AbstractTest();
        //System.out.println("at.cnt = " + at.cnt); // 0
    }
}
/*
	创建抽象类AbstractTest的子类SubAbstractTest
 */
public class SubAbstractTest extends AbstractTest {

	@Override
	public void show() { // 去掉了abstract关键字
		System.out.println("其实我这个方法是被迫重写的,因为继承了抽象类");
	}

	public static void main(String[] args) {

		// 1.声明本类类型的引用指向本类类型的对象
		SubAbstractTest sat = new SubAbstractTest();
		sat.show();

		// 2.声明AbstractTest类型的引用指向子类的对象,形成了多态
		// 多态的使用场合之二:直接在方法体中使用抽象类(父类)的引用指向子类类型的对象
		AbstractTest at = new SubAbstractTest();
		// 编译阶段调用父类版本,运行阶段调用子类版本
		at.show();
		// 此 at.show() 与上面的sat.show() 打印结果一样
		// 区别在于前者没有使用多态,而后者使用了多态
		// 推荐使用多态,理由:只需改变子类(如上面的SubStractTest),修改为其他子类,
		// 就可以改变结果,而不需要动其他代码,很方便
	}
}

开发经验分享(多态优缺点)

  • 由上面的代码引出了以下的开发经验分享
  • 优点:在以后的开发中推荐使用多态的格式,此时父类类型直接调用的所有方法一定是父类所拥有的方法,若以后更换子类,只需要将new关键字后面的子类类型修改而其他地方无需改变即可立即生效,从而提高代码的可维护性可扩展性
  • 缺点:父类引用 不能直接调用 子类独有 的方法,若调用需要强制类型转换

案例:抽象类的应用

  • 银行有 定期账户 和 活期账户,继承自账户类。
  • 账户类中:
public class Account {

	private double money;
	public double getLixi() { };	// 对于不同的账户,利息计算不一样
									// 因此不能写具体方法的功能,而写抽象方法
}
  • 创建抽象类Account
public abstract class Account {

    private int money;

    public Account() {
    }

    public Account(int money) {
        setMoney(money);
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        if(money > 0) {
            this.money = money;
        } else {
            System.out.println("金额不合理哦!");
        }
    }

    // 自定义抽象方法实现计算利息并返回的功能描述
    public abstract double getLixi();

}
  • 创建抽象类Account的子类FixedAccount
/*
	活期账户
 */
public class FixedAccount extends Account {

	// 本类没有独有的成员变量

    public FixedAccount() {
    	// super(); // 调用父类的无参构造方法,不写则编译器默认已写上
    }

    public FixedAccount(int i) {
        super(i); // 调用父类的有参构造方式
        //this.setMoney(i); // 也可以用这个方法
    }

    @Override
    public double getLixi() {
        // 利息 = 本金 * 利率 * 时间
        return getMoney() * 0.03 * 1;
    }

    public static void main(String[] args) {

        // 1.声明一个Account类型的引用指向子类类型的对象,形成多态
        // Account acc = new FixedAccount(1000); // 有参构造
        Account acc = new FixedAccount();        // 无参构造
        acc.setMoney(1000);
        double res = acc.getLixi();
        System.out.println("计算的利息是:" + res);
    }

}

笔试考点

  • privateabstract 关键字不能共同修饰一个方法
    • 因为父类的private私有方法不能被子类继承,因此对于(abstract修饰的)抽象方法来说,私有方法是没有意义的
  • finalabstract 关键字不能共同修饰一个方法
    • 因为final方法不能被重写(可以被继承),而abstract方法就是用来继承与重写的,两者冲突
  • finalabstract 关键字不能共同修饰一个
    • 因为final类不能被继承,而abstract类需要被继承,两者冲突
  • staticabstract 不能共同修饰一个方法
    • 因为本来抽象类里的abstract抽象方法是为了防止程序员直接用new从抽象类里调用方法,而static会将该抽象方法从对象层级提升为类层级,可以用 类名. 调用, 两者冲突

4.7 接口(重点)

接口的基本概念

  • 接口就是一种比抽象类还抽象的所有方法都为抽象方法(里面没有构造方法)
  • 定义接口的关键字:interface (定义类的关键字是class)
public interface InterfaceTest {

    public static final int CNT = 1; // 里面只能有常量, 不写static的话,会默认有static
    private void show(){}            // 从jdk9开始允许接口出现私有方法
    public abstract void show(); // 里面只有抽象方法(新特性除外),public abstract关键字可以省略,但建议写上,为了与新特性区分开
    //void show(); // public abstract关键字省略了
}

接口的实际意义

  • 接口弥补了Java中不支持多继承的不足
    在这里插入图片描述
public interface Metal {

    // 自定义抽象方法来描述发光的行为
    public abstract void shine();
}
public interface Money {

    // 自定义抽象方法描述购物的行为
    public abstract void buy();
}
// 使用 implements关键字 表达实现的关系,支持多实现(implements是实现的意思)
public class Gold implements Metal, Money{
    @Override
    public void shine() {
        System.out.println("金黄色光芒");
    }

    @Override
    public void buy() {
        System.out.println("购物");
    }

    public static void main(String[] args) {

        // 1.声明接口类型的引用指向实现类的对象,形成了多态
        Metal mt = new Gold();
        mt.shine();

        Money mn = new Gold();
        mn.buy();
    }
}

案例:类与接口之间的关系

题目:

  • 编程实现Runner接口,提供一个描述奔跑行为的抽象方法
  • 编程实现Hunter接口继承Runner接口,并提供一个描述捕猎行为的抽象方法
  • 编程实现Man类实现Hunter接口并重写抽象方法,在main方法中使用多态方式测试
public interface Runner {

    // 自定义抽象方法来描述奔跑行为
    public abstract void run();
}
// 接口只能继承接口,不能继承类
public interface Hunter extends Runner {

    // 自定义抽象方法来描述捕猎的行为
    public abstract void hunt();
}
public class Man implements Hunter {

    @Override
    public void hunt() {
        System.out.println("捕猎!");
    }

    @Override
    public void run() {
        System.out.println("跑!");
    }

    public static void main(String[] args) {

        // 1.声明接口类型的引用指向实现类的对象,形成了多态
        Hunter hunter = new Man();
        hunter.hunt();
        hunter.run();

        System.out.println("--------------------");
        Runner runner = new Man();
        runner.run();
    }
}

在这里插入图片描述

抽象类和接口的主要区别(笔试题)

  • 定义抽象类的关键字:abstract class定义接口的关键字:interface
  • 继承抽象类的关键字:extends实现接口的关键字:implements
  • 继承抽象类支持单继承,实现接口支持多实现
  • 抽象类中可以有构造方法,接口中不能有构造方法
  • 抽象类中可以有成员变量,接口中只可以有常量
  • 抽象类中可以有成员方法,接口中只可以有抽象方法
  • 抽象类中增加方法时可以不重写,接口中增加方法时实现类需要重写(Java8以前的版本)
  • Java8开始增加新特性,接口中允许出现非抽象方法静态方法,但非抽象方法需要使用default关键字修饰
  • Java9开始增加新特性,接口中允许出现私有方法

总结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值