JAVA-理解多态

本文探讨了JAVA中的多态性,包括它的定义、优点、必要条件和形式。介绍了向上转型的概念,解释了它如何消除类型耦合并提高代码的可扩展性。此外,还详细讨论了动态绑定的内部机制以及方法表的作用。文章强调了多态对代码可扩展性的益处,并提醒读者注意向下转型时可能遇到的问题及其解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

  面向对象有三大特征:封装、继承、多态。
封装隐藏了类的内部实现机制,可以在不影响使用者的前提下改变类的内部结构,继承是为了重用父类代码,而多态呢?今天我就谈谈自己对多态的理解。

多态

  多态是指同一消息可以根据发送对象的不同而采用多种不同的行为方
式。多态具有以下几个优点:
1. 消除类型之间的耦合关系
2. 可替换性
3. 可扩充性
4. 接口性
5. 灵活性
6. 简化性

多态存在的三个必要条件:继承、重写、父类引用指向子类对象

多态的形式:

Parent p = new Child();

向上转型

  要理解多态,首先需要了解向上转型。例如我定义了一个Shape类,子类Circle继承自Shape类,实例化一个Circle对象,可以这样表示

Shape s = new Circle();

简单来说,就是:父类引用指向子类对象。
那么向上转型有啥好处呢?首先我们来看看如果没有向上转型:

public class Shape {
    public void draw(){
      System.out.println("draw shape");
    }
}

public class Circle extends Shape{
    public void draw(){
        System.out.println("draw circle");
    }
}

public class Square extends Shape{
  public void draw(){
      System.out.println("draw square");
  }
}

public class Painter{
  public static void main(String[] args){
    Painter painter = new Painter();
    Circle c = new Circle();
    painter.draw(c);
  }

  public void draw(Circle c){
    c.draw();
  }

  public void draw(Square s){
    s.draw();
  }
}

最后将打印

draw circle

  这么做是可以的,但是有个主要缺点,若我们需要添加一个新的Shape子类,则必须要在Painter中添加一个新的draw()方法,若遇到需要大量Shape子类工作的情况呢,这个将变为很糟糕,因此,多态就很好地帮我们解决了这个问题。

若使用多态,Painter类只需要这样设计。

public class Painter{
  public static void main(String[] args){
    Painter painter = new Painter();
    Circle c = new Circle();
    painter.draw(c);
  }

  public void draw(Shape s){
    s.draw();
  }
}

  当Circle实例传给draw()时,draw()会将Circle实例当做Shape对象,因此对Shape所做的任何操作都将被Circle所接收到。当然,这也是有前提的,Shape的子类必须重写Shape的方法。若子类没有重写父类的方法,则最终会调用的是父类中的方法,因此最好将抽象的部分设为抽象方法,这样子类在继承的时候若没有重写,编译器将会报错。

绑定

  我们只需要子类重写父类方法,在需要的时候将子类实例传给父类引用,便可完成向上转型。那么编译器是如何区分传给父类引用的是哪个子类实例呢,其实编译器是一直不知道对象的类型,但JAVA提供了一种解决办法,后期绑定,也就是在运行时根据对象的类型进行绑定。因此后期绑定也叫动态绑定或运行时绑定。
  《JAVA编程思想》中提到,Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是动态绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定。若将方法设为final类型,不仅可以防止其他人重写该方法,也可以有效地”关闭”动态绑定。

动态绑定内部机制

为了提高动态分派时方法查找的效率,JVM 会在链接类的过程中,给类分配相应的方法表内存空间。每个类对应一个方法表。
一个类的方法表包含类的所有方法入口地址,从父类继承的方法放在前面,接下来是接口方法和自定义的方法。当我们调用某个方法时,JVM会从方法表中查找相应的方法,其过程如下:

  1. 首先编译器确定对象的声明类型和方法名。然后找当前类中方法名字匹配的所有方法(由于重载,可能存在多个),然后在其父类中也找类似的属性为public的方法;
  2. 编译器查看调用方法的参数类型,先在本类中找,然后在超类中找,这一过程称为重载解析(overloading resolution)。若没找到,或在同一个类中找到多个,均报错。
  3. 若为private、static或者final修饰的方法,为静态绑定,可直接知道调用的是哪个方法,此情况下就省去了剩下的步骤;
  4. 在程序运行时,JVM会根据对象的实际类型从方法表中调用最合适的方法。

可扩展性

由于引入了多态机制,我们在对现有的代码进行扩展时,而不需要修改现有的方法。还是以Shape为例,向其添加一个size()方法,并在子类中实现该方法,即使如此,我们也不必修改Painter中draw()方法,原代码依然可以稳健运行。具体实现如下:

public class Shape {
    public void draw(){
      System.out.println("draw shape");
    }
    public void size(){
      //TODO
    }
}

public class Circle extends Shape{
    public void draw(){
        System.out.println("draw circle");
    }

    public void size(){
      //TODO
    }
}

public class Square extends Shape{
  public void draw(){
      System.out.println("draw square");
  }

  public void size(){
    //TODO
  }
}


public class Painter{
  public static void main(String[] args){
    Painter painter = new Painter();
    Circle c = new Circle();
    painter.draw(c);
  }

  public void draw(Shape s){
    s.draw();
  }
}

这个例子很好地体现了多态的特性,我们对代码所做的修改,不会对程序中其他不应受到影响的部分产生破坏。

向下转型类型判断

由于向上转型会丢失具体的类型信息,比如Shape的子类Circle中有额外的color()方法,将Circle实例转为Shape类型,这样做是安全的,因为父类不会具有大于子类的接口,因此通过父类调用的方法都是可行的。
而对于向下转型,我们无法知道一个父类会转为哪个子类类型,因此也无法确保被调用的方法是那个类中所含有的。如下所示:

public class Shape {
}
public class Circle extends Shape{
    public void color(){
        System.out.println("paint yellow");
    }
}
public class Square extends Shape{
  public void size(){
      System.out.println("40 x 40");
  }
}
public class Painter{
  public static void main(String[] args){
    Shape shape = new Circle();
    Square square = (Square)shape;
    square.size(); // ClassCastException
  }
}

将Shape实例强转为Square类型,编译器是不会报错的,因为Square是Shape的子类。当用强转后的Square实例调用Circle中的color()方法,编译器就会报一个ClassCastException错误。
为解决上述问题,我们可以使用 ’instanceof关键字‘ 来确保不会出现ClassCastException错误。
将Painter改为:

public class Painter{
  public static void main(String[] args){
    Shape shape = new Circle();
    if(shape instanceof Square){
      Square square = (Square)shape;
      square.size();
    }
  }
}

写在最后

本菜鸟也跟随潮流,开通了基于HEXO的个人博客。欢迎各位关注:https://pomelojiang.github.io/

参考

  1. 《JAVA编程思想》
  2. 深入理解JVM方法调用的内部机制
### Java多态的概念 Java中的多态性意味着同一个实体可以表示多种形式。这种特性通过继承和接口实现,允许程序在运行时决定调用哪个具体方法[^2]。 #### 继承与重写 当存在一个类层次结构时,子类可以从父类那里继承属性和行为,并能够覆盖(即重写)这些行为来提供特定的功能。例如,在给定的例子中`Person`作为基类而`Student`是其派生出来的子类之一。如果两者都定义了一个名为`number()`的方法,则创建`Student`类型的实例并将其赋值给`Person`类型的引用后,执行此引用上的`number()`操作实际上会触发来自更具体的`Student`版本的行为[^1]。 ```java class Person { public void number() { System.out.println("This is from Person class"); } } class Student extends Person { @Override public void number() { System.out.println("Overridden method in Student class"); } } ``` ### 多态的应用场景 为了利用多态带来的灵活性,通常会在声明对象时采用较宽泛的数据类型——通常是某个共同祖先或实现了相同接口的不同类别之间的公共超类型。这样做不仅简化了代码编写过程,还增强了系统的可扩展性和维护便利度。比如下面这段展示如何运用ArrayList存储多种动物的动作描述: ```java List<Animal> animals = new ArrayList<>(); animals.add(new Snake()); animals.add(new Tiger()); for (Animal animal : animals) { animal.eat(); } // 输出结果可能是:“蛇吞象!” “虎吃鸡!” 这取决于各自的具体实现方式。 ``` 这里的关键在于即使列表被指定为保存`Animal`类型的元素,实际加入其中的对象却是属于不同的子类(`Snake`, `Tiger`)。每当遍历该集合并对每个成员调用eat()函数时,JVM都会自动查找最适合当前项的那个版本来进行处理[^4]。 ### 实现机制:动态绑定/后期绑定 这一切之所以能顺利运作是因为Java采用了所谓的“动态绑定”,也称为“后期绑定”。这意味着编译器不会立即确定要执行的确切方法体;相反,它等待直到程序真正运行起来之后才做出最终的选择依据于当时可用的信息。因此,只要遵循面向对象设计原则合理安排好类间关系以及适当标记那些打算让后代重新诠释的操作符们(@Override),就能轻松享受到由多态所带来的诸多好处了[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值