服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - Java编程 多态

Java编程 多态

2022-01-26 12:50pursuingdreams Java教程

这篇文章主要介绍了关于Java编程的多态,多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。构建可扩展的程序,需要的朋友可以参考下

前言:

封装,是合并属性和行为创建一种新的数据类型,继承是建立数据类型之间的某种关系(is-a),而多态就是这种关系在实际场景的运用。

多态就是把做什么和怎么做分开了;其中,做什么是指调用的哪个方法[play 乐器],怎么做是指实现方案[使用A乐器 使用B乐器],''分开了''指两件事不在同一时间确定。

一、向上转型

对象既可以作为它本身的类型使用,也可以作为它的基类型使用,而这种把对某个对象的引用视为对其基类型的引用的做法就是向上转型。

example:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public enum Note {
 // 演奏乐符
    MIDDLE_C, C_SHARP, B_FLAT;
 
}
public class Instrument {
 
 // 乐器基类
    public void play(Note n) {
        print("Instrument.play()");
    }
 
}
public class Wind extends Instrument{
 
 // Wind是一个具体的乐器
    // Redefine interface method:
    public void play(Note n) {
        System.out.println("Wind.play() " + n);
    }
 
}
public class Music {
 
 // 乐器进行演奏
    public static void tune(Instrument i) {
        i.play(Note.MIDDLE_C);
    }
 
    public static void main(String[] args) {
        Wind flute = new Wind();
        tune(flute); // 向上转型
    }
}

好处:

在上面例子中,如果让tune方法接受一个Wind引用作为自己的参数,似乎看起来更为直观,但是会引发一个问题:这个时候你就需要为系统中Instrument的每种类型都编写一个新的tune方法。所以我们只写一个简单的方法,仅仅接收基类作为参数,而不是特殊的导出类,这么做情况不是变得更好吗。

example:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Stringed extends Instrument {
    public void play(Note n) {
        print("Stringed.play() " + n);
    }
}
 
 
class Brass extends Instrument {
    public void play(Note n) {
        print("Brass.play() " + n);
    }
}
 
 
public class Music2 {
    public static void tune(Wind i) {
        i.play(Note.MIDDLE_C);
    }
 
    public static void tune(Stringed i) {
        i.play(Note.MIDDLE_C);
    }
 
 
    public static void tune(Brass i) {
        i.play(Note.MIDDLE_C);
 
    }
    public static void main(String[] args) {
        Wind flute = new Wind();
        Stringed violin = new Stringed();
        Brass frenchHorn = new Brass();
        tune(flute); // No upcasting
        tune(violin);
        tune(frenchHorn);
    }
}

二、转机

?
1
2
3
4
5
public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
 
}

 

在上面这个方法中,它接收一个Instrument引用,那么在这种情况下,编译器怎么样才能知道这个instrument引用指向的是Wind对象呢? ——通过后期绑定

1、绑定

将一个方法调用同一个方法主体关联起来称为绑定。

在程序执行前进行绑定,就是前期绑定,比如C语言就只有一种方法调用,就是前期绑定。

在运行时根据对象的类型进行绑定就是后期绑定,也叫做动态绑定或者运行时绑定。

Java中除了static方法和final方法之外,其它所有方法都是后期绑定,这意味着通常情况下,我们不必判定是否应该进行后期绑定——它会自动发生。

2、扩展性

由于有多态机制,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改 true()方法。在一个设计良好的 OOP 程序中,我们的大多数或者所有方法都会遵从 tune()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不需要改变,
对于乐器例子,假设我们在基础类里加入更多的方法[what/adjust],以及一系列新类[Woodwind/Brass],

例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class Instrument {
    void play(Note n) { print("Instrument.play() " + n); }
    String what() { return "Instrument"; }
    void adjust() { print("Adjusting Instrument"); }
}
 
 
class Wind extends Instrument {
    void play(Note n) { print("Wind.play() " + n); }
    String what() { return "Wind"; }
    void adjust() { print("Adjusting Wind"); }
}
 
 
class Percussion extends Instrument {
    void play(Note n) { print("Percussion.play() " + n); }
    String what() { return "Percussion"; }
    void adjust() { print("Adjusting Percussion"); }
}
 
class Stringed extends Instrument {
    void play(Note n) { print("Stringed.play() " + n); }
    String what() { return "Stringed"; }
    void adjust() { print("Adjusting Stringed"); }
}
 
 
class Brass extends Wind {
    void play(Note n) { print("Brass.play() " + n); }
    void adjust() { print("Adjusting Brass"); }
}
 
 
class Woodwind extends Wind {
    void play(Note n) { print("Woodwind.play() " + n); }
    String what() { return "Woodwind"; }
}
 
 
public class Music3 {
    // Doesn't care about type, so new types
    // added to the system still work right:
    public static void tune(Instrument i) {
        // ...
        i.play(Note.MIDDLE_C);
    }
 
    public static void tuneAll(Instrument[] e) {
        for(Instrument i : e)
            tune(i);
    }
    public static void main(String[] args) {
        // Upcasting during addition to the array:
        Instrument[] orchestra = {
            new Wind(),
            new Percussion(),
            new Stringed(),
            new Brass(),
            new Woodwind()
        };
        tuneAll(orchestra);
    }
}

​ 为乐器系统添加更多的类型,而不用改动tune方法。tune方法完全可以忽略它周围代码所发生的全部变化,依旧正常运行。

3、缺陷

私有方法

private方法被自动修饰为final,而且对导出类是屏蔽的,所以在子类Derived类中的f方法是一个全新的方法。既然基类中的f方法在在子类Derived中不可见,那么也不能被重载。

域与静态方法

任何域(field)的访问操作都是由编译器解析的,因此不是多态的。

如果某个方法是静态的,那么它就不具有多态性

三、构造器与多态

通常,构造器不同于其它方法,涉及到多态时也是如此。构造器是不具有多态性的

1、构造器的调用顺序

基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接。使得每个基类的构造器都能得到调用。

2、构造器内部的多态方法的行为

构造器调用的层次结构带来一个问题:如果在一个构造器内部调用正在构造的对象的某个动态绑定方法,会发生什么?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Glyph {
 
    void draw() { print("Glyph.draw()"); }
    Glyph() {
        print("Glyph() before draw()");
        draw(); // 调用正在构造的对象的某个动态绑定方法,对象的字段radius被初始化为0
        print("Glyph() after draw()");
    }
 
}
 
 
public class RoundGlyph extends Glyph{
 
    private int radius = 1;
 
    RoundGlyph(int r) {
        radius = r;
        print("RoundGlyph.RoundGlyph(), radius = " + radius);
    }
    void draw() {
        print("RoundGlyph.draw(), radius = " + radius);
    }
 
 
}
 
 
public class PolyConstructors {
    
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
    
}
 
/* Output:
    Glyph() before draw()
    RoundGlyph.draw(), radius = 0
    Glyph() after draw()
    RoundGlyph.RoundGlyph(), radius = 5
*///:~

Glyph的构造器中,我们调用了draw方法,因为这个是动态绑定方法的缘故,我们就会调用导出类RoundGlyph中的draw方法,但是这个方法操纵的成员radius还没初始化,所以就体现出问题了,结果中第一次输出radius为0。

所以初始化的实际过程是:

  • 1 在其他任何事物之前,将分配给对象的存储空间初始化成二进制的零
  • 2 如前所述调用基类构造器
  • 3 按照声明的顺序调用成员的初始化方法
  • 4 调用导出类的构造器主体

四、协变返回类型

在面向对象程序设计中,协变返回类型指的是子类中的成员函数的返回值类型不必严格等同于父类中被重写的成员函数的返回值类型,而可以是更 "狭窄" 的类型。

​ Java 5.0添加了对协变返回类型的支持,即子类覆盖(即重写)基类方法时,返回的类型可以是基类方法返回类型的子类。协变返回类型允许返回更为具体的类型。

例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.ByteArrayInputStream;
import java.io.InputStream;
 
class Base
{
    //子类Derive将重写此方法,将返回类型设置为InputStream的子类
   public InputStream getInput()
   {
      return System.in;
   }
}
public  class Derive extends Base
{
 
    @Override
    public ByteArrayInputStream getInput()
    {
 
        return new ByteArrayInputStream(new byte[1024]);
    }
    public static void main(String[] args)
    {
        Derive d=new Derive();
        System.out.println(d.getInput().getClass());
    }
}
/*程序输出:
class java.io.ByteArrayInputStream
*/

五、继承进行设计

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Actor {
 public void act() {
 }
}
 
class HappyActor extends Actor {
 public void act() {
  System.out.println("HappyActor");
 }
}
 
class SadActor extends Actor {
 public void act() {
  System.out.println("SadActor");
 }
}
 
class Stage {
 private Actor actor = new HappyActor();
 
 public void change() {
  actor = new SadActor();
 }
 
 public void performPlay() {
  actor.act();
 }
}
 
public class Transmogrify {
 public static void main(String[] args) {
  Stage stage = new Stage();
  stage.performPlay();
  stage.change();
  stage.performPlay();
 }
 
}

输出:

  HappyActor
    SadActor

一条通用的准则是:“用继承表达行为间的差异,并用字段表达状态上的变化”。在上述例子中,两者都用到了:通过继承得到了两个不同的类,用于表达 act()方法的差异:而 Stage通过运用组合使自己的状态发生了变化。在这种情况下,这种状态的改变也就产生了行为的改变。

总结:

多态意味着 不同的形式。在面向对象的设计中,我们持有从基类继承而来的相同接口,以及使用该接口的不同形式不同版本的多态绑定方法。 运用数据的抽象和继承,能更好的类型和创造多态的例子。

到此这篇关于Java编程 多态的文章就介绍到这了,更多相关Java多态内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://www.cnblogs.com/pursuingdreams/p/15371179.html

延伸 · 阅读

精彩推荐