Polymorphism【1】

本文深入探讨面向对象编程中的多态性概念,解释其在代码组织、扩展性和抽象性中的重要作用。通过具体示例展示了多态如何使程序更具灵活性,允许在运行时决定对象的行为。

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

Polymorphism is the third essential feature of an object-oriented programming language, after data abstraction and inheritance.

It provides another dimension of separation of interface from implementation, to decouple what from how. Polymorphism allows improved code organization and readability as well as the creation of extensible programs that can be “grown” not only during the original creation of the project, but also when new features are desired.

多态性是面向对象语言中继封装,继承之后第三个基本特征。它在另一个层面实现了接口和实现的分离,也就是说分开了做什么和怎么做,多态性不但可以提高代码的组织结构和易读性,这样你就可以创建出可扩展的程序,不仅仅是在创建程序的时候可以扩展,而且还可以在增加新的特性的时候扩展。

Encapsulation creates new data types by combining characteristics and behaviors. Implementation hiding separates the interface from the implementation by making the details private. This sort of mechanical organization makes ready sense to someone with a procedural programming background. But polymorphism deals with decoupling in terms of types. In the last chapter, you saw how inheritance allows the treatment of an object as its own type or its base type. This ability is critical because it allows many types (derived from the same base type) to be treated as if they were one type, and a single piece of code to work on all those different types equally. The polymorphic method call allows one type to express its distinction from another, similar type, as long as they’re both derived from the same base type. This distinction is expressed through differences in behavior of the methods that you can call through the base class.

封装是通过绑定有特征的数据和行为,创建一个新的数据类型。隐藏实现通过设置明细为private将实现和接口部分分离开来,这种机械呆板的讲解方式主要是为了那些习惯于过程式开发的程序员。但是多态是站在类的角度来处理这种逻辑上的分离的,在上一章节中,我们知道了如何让一个对象作为自己或者以自己的基类的方式来处理。这种能力是很关键的,它可以让许多类(继承自同一个类)来作为一个类来处理。这样一段代码就可以作用于不同的类了,多态性的方法的调用能看出两个相似的类之间的差别,甚至于两个类继承自同一个基类。这种区别你通过调用基类的一个方法得到不同的行为而证实。

In this chapter, you’ll learn about polymorphism (also called dynamic binding or late binding or run-time binding) starting from the basics, with simple examples that strip away everything but the polymorphic behavior of the program.

在本章,你将通过一些只有多态方法的小例子学习到多态性(动态绑定,后绑定或者运行时绑定)。

Upcasting revisited

In Chapter 6 you saw how an object can be used as its own type or as an object of its base type. Taking an object reference and treating it as a reference to its base type is called upcasting because of the way inheritance trees are drawn with the base class at the top.

You also saw a problem arise, which is embodied in the following example about musical instruments. Since several examples play Notes, we should create the Note class separately, in a package:

在第六章你知道了如何将一个对象转化为自己的类型或者它的基类的对象。将一个对象的reference转化为它的基类的reference来处理,叫做“上传”。因为在继承关系图中基类总是在最顶部。你会看到问题也出来了,已经在下面关于乐器的例子中体现出来了,因为几个例子都要使用音符Note,所以我们建立了一个Note类。

package c07.music;

import com.bruceeckel.simpletest.*;

 

public class Note {

  private String noteName;

  private Note(String noteName) {

    this.noteName = noteName;

  }

  public String toString() { return noteName; }

  public static final Note

    MIDDLE_C = new Note("Middle C"),

    C_SHARP  = new Note("C Sharp"),

    B_FLAT   = new Note("B Flat");

}

This is an “enumeration” class, which has a fixed number of constant objects to choose from. You can’t make additional objects because the constructor is private.

In the following example, Wind is a type of Instrument, therefore Wind is inherited from Instrument:

这是一个枚举类,它创建了几个固定的对象顾你选择,你不能创建其它的对象,因为这个类的构造方法是私有的。下面的例子中,你可以看到Wind是一种乐器,因为它继承了Instrument类。

package c07.music;

import com.bruceeckel.simpletest.*;

 

public class Music {

  private static Test monitor = new Test();

  public static void tune(Instrument i) {

    i.play(Note.MIDDLE_C);

  }

  public static void main(String[] args) {

    Wind flute = new Wind();

tune(flute);   

monitor.expect(new String[] {

      "Wind.play() Middle C"

    });

  }

}

 

package c07.music;

 

public class Wind extends Instrument {

  public void play(Note n) {

    System.out.println("Wind.play() " + n);

  }

}

 

The method Music.tune( ) accepts an Instrument reference, but also anything derived from Instrument. In main( ), you can see this happening as a Wind reference is passed to tune( ), with no cast necessary. This is acceptable—the interface in Instrument must exist in Wind, because Wind is inherited from Instrument. Upcasting from Wind to Instrument may “narrow” that interface, but it cannot make it anything less than the full interface to Instrument.

这个 Music.tune( )只接受Instrument类的对象,但是也可以接受任何Instrument的派生类的对象。在主方法中,Wind对象在没有进行上传转化的情况下也调用了tune()方法。这是可行的,因为Wind继承了Instrument类并且实现了它的接口,将Wind上传转化为Instrument将接口变窄了,但是再窄也不比它的基类Instrument少。

Forgetting the object type

Music.java might seem strange to you. Why should anyone intentionally forget the type of an object? This is what happens when you upcast, and it seems like it could be much more straightforward if tune( ) simply takes a Wind reference as its argument. This brings up an essential point: If you did that, you’d need to write a new tune( ) for every type of Instrument in your system. Suppose we follow this reasoning and add Stringed and Brass instruments:

 Music.java这个写法对你来说可能是陌生的,为什么需要每个对象忽略自己的类别呢,上面的例子就是再做这个事情,但是看起来让tune()方法接受Wind的对象好像更简单一些,这就带来了一个很本质的观点:如果那样的话你就需要再为每个类写一个新的方法。假设顺着这个思路我们再增加一个Stringed类和Brsaa类呢?

package c07.music;

import com.bruceeckel.simpletest.*;

 

class Stringed extends Instrument {

  public void play(Note n) {

    System.out.println("Stringed.play() " + n);

  }

}

 

class Brass extends Instrument {

  public void play(Note n) {

    System.out.println("Brass.play() " + n);

  }

}

 

public class Music2 {

  private static Test monitor = new Test();

  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);

    monitor.expect(new String[] {

      "Wind.play() Middle C",

      "Stringed.play() Middle C",

      "Brass.play() Middle C"

    });

  }

}

This works, but there’s a major drawback: you must write type-specific methods for each new Instrument class you add. This means more programming in the first place, but it also means that if you want to add a new method like tune( ) or a new type of Instrument, you’ve got a lot of work to do. Add the fact that the compiler won’t give you any error messages if you forget to overload one of your methods and the whole process of working with types becomes unmanageable.

这样可以实现,但是有很多的弊端:你必须为你增加的每个乐器增加该类别特有的方法。这样首先会有很多的代码,也同样意味着你如果增加一个tune()方法或者增加Instrument的派生类的时候你就有许多的代码要去做,如果你忘记了重载某个方法的话编译器是不会报错的,但是这个程序的整体处理流程可能就乱套了。

Wouldn’t it be much nicer if you could just write a single method that takes the base class as its argument, and not any of the specific derived classes? That is, wouldn’t it be nice if you could forget that there are derived classes, and write your code to talk only to the base class?

That’s exactly what polymorphism allows you to do. However, most programmers who come from a procedural programming background have a bit of trouble with the way polymorphism works.

如果你只在基类当中写一个以基类作为参数的方法不是更好吗?并不是为每一个派生类都写一个方法,如果你忽略所有派生类的类型并且仅仅对基类做处理不是更好吗?这就让我们很明白多态允许我们作些什么了,但是大部分以过程式开发的程序员们会对使用多态有些抵触。

The twist

The difficulty with Music.java can be seen by running the program. The output is Wind.play( ). This is clearly the desired output, but it doesn’t seem to make sense that it would work that way. Look at the tune( ) method:

当运行程序的时候Music.java还是比较让人费解,输出的结果是Wind.play().这是我们所期望的,但是程序看起来并不是那样执行的,让我们来看tune()这个方法:

  public static void tune(Instrument i) {

    i.play(Note.MIDDLE_C);

  }

 

It receives an Instrument reference. So how can the compiler possibly know that this Instrument reference points to a Wind in this case and not a Brass or Stringed? The compiler can’t. To get a deeper understanding of the issue, it’s helpful to examine the subject of binding.

 这个方法接受一个Instrument对象。但是编译器如何知道的调用这个方法的是Wind对象而不是Brass或者Stringed呢?编译器不知道,如果想深入的理解这个问题,学习绑定这项知识很有帮助。

Method-call binding

Connecting a method call to a method body is called binding. When binding is performed before the program is run (by the compiler and linker, if there is one), it’s called early binding. You might not have heard the term before because it has never been an option with procedural languages. C compilers have only one kind of method call, and that’s early binding.

The confusing part of the preceding program revolves around early binding, because the compiler cannot know the correct method to call when it has only an Instrument reference.

将一个方法的调用连接到一个方法本身这叫做“绑定”。如果绑定是在程序运行的时候(编译或者Linker的时候),叫做“事前绑定”。你可能没有听说过这个说法,因为在过程式语言中这不是一个选项。C编译器也只有一种绑定方式就是事前绑定。在上面的问题中就是因为事前绑定,因为当一个Instrument的对象调用的时候,编译器也不知道什么类型的对象在调用。

The solution is called late binding, which means that the binding occurs at run time, based on the type of object. Late binding is also called dynamic binding or run-time binding. When a language implements late binding, there must be some mechanism to determine the type of the object at run time and to call the appropriate method. That is, the compiler still doesn’t know the object type, but the method-call mechanism finds out and calls the correct method body. The late-binding mechanism varies from language to language, but you can imagine that some sort of type information must be installed in the objects.

还有一种叫做事后绑定,即程序在运行的时候根据对象的类型才绑定,事后绑定又叫做动态绑定或者运行时绑定,如果一个语言的实现部分是后绑定,那么那一定有某些机制能够在程序运行的时候判断对象的类别,然后取调用相关的方法。后绑定机制在不同的语言之间也不同,但是我们可以想想得出对象中一定存着某些所属类型的信息。

All method binding in Java uses late binding unless the method is static or final (private methods are implicitly final). This means that ordinarily you don’t need to make any decisions about whether late binding will occur—it happens automatically.

Java中所有的绑定都是属于事后绑定,除非是static或者finalprivate暗含了private)。所以一般来讲你没有必要进行特别的说明,程序默认就是事后绑定。

Why would you declare a method final? As noted in the last chapter, it prevents anyone from overriding that method. Perhaps more important, it effectively “turns off” dynamic binding, or rather it tells the compiler that dynamic binding isn’t necessary. This allows the compiler to generate slightly more efficient code for final method calls. However, in most cases it won’t make any overall performance difference in your program, so it’s best to only use final as a design decision, and not as an attempt to improve performance.

那为什么要声明一个final的方法呢?我们已经在上一章中进行了介绍,它禁止别人覆写那个方法,可能更重要的是它有效的关闭了动态绑定,或者它告诉了编译器,这里不需要动态绑定,这样编译器就能为final的方法的调用产生更为高效的代码,然后在大多情况下,它不会对你的程序性能有多大的提高,所以final最好还是作为一种设计概念,不要对它提高性能寄予希望。

Producing the right behavior

Once you know that all method binding in Java happens polymorphically via late binding, you can write your code to talk to the base class and know that all the derived-class cases will work correctly using the same code. Or to put it another way, you “send a message to an object and let the object figure out the right thing to do.”

The classic example in OOP is the “shape” example. This is commonly used because it is easy to visualize, but unfortunately it can confuse novice programmers into thinking that OOP is just for graphics programming, which is of course not the case.

一旦你知道了在Java中的绑定都是事后绑定,你就可以放心的以基类对象作为参数写代码,并且这样派生类的对象也可以使用同样的代码被处理的正确,或者说,你给对象发送一个消息,让它自己去做正确的事情。在学习面向对象的时候“Shape”是一个经典的例子,这个经常的被使用是因为它比较形象,但是不幸的是这个例子很容易搞晕新手,他们误以为面向对象只是为了形状编程,当然,不仅仅是这样的。

The shape example has a base class called Shape and various derived types: Circle, Square, Triangle, etc. The reason the example works so well is that it’s easy to say “a circle is a type of shape” and be understood. The inheritance diagram shows the relationships:

形状的这个例子有一个Shape的基类,并且许多的继承类,CircleSquareTriangle等等。这个例子可以使用的很好是因为很容易的就可以理解“圆是一种形状。下面的继承图很好的展现了这种关系:

The upcast could occur in a statement as simple as:

Shape s = new Circle();

Here, a Circle object is created, and the resulting reference is immediately assigned to a Shape, which would seem to be an error (assigning one type to another); and yet it’s fine because a Circle is a Shape by inheritance. So the compiler agrees with the statement and doesn’t issue an error message.

Suppose you call one of the base-class methods (that have been overridden in the derived classes):

在这里,创建了一个Circle的对象,但是很快被转化成了并赋值给了Shape的对象,这样看起来好像是一个错误;但是在这里是允许的,因为Circle是继承自Shape。所以编译器允许这种语法并且不会提示错误信心。假设你调用一个基类的方法(这个方法在派生类中已经被覆写了)

s.draw();

Again, you might expect that Shape’s draw( ) is called because this is, after all, a Shape reference—so how could the compiler know to do anything else? And yet the proper Circle.draw( ) is called because of late binding (polymorphism).

这次,你可能以为调用的是Shapedraw()方法,因为它就是一个Shape的对象,但是编译器如何确定调用哪个呢?实际上因为动态绑定的机制调用的是Circledraw()方法。

The following example puts it a slightly different way:

import com.bruceeckel.simpletest.*;

import java.util.*;

 

class Shape {

  void draw() {}

  void erase() {}

}

 

class Circle extends Shape {

  void draw() {

    System.out.println("Circle.draw()");

  }

  void erase() {

    System.out.println("Circle.erase()");

  }

}

 

class Square extends Shape {

  void draw() {

    System.out.println("Square.draw()");

  }

  void erase() {

    System.out.println("Square.erase()");

  }

}

 

class Triangle extends Shape {

  void draw() {

    System.out.println("Triangle.draw()");

  }

  void erase() {

    System.out.println("Triangle.erase()");

  }

}

 

class RandomShapeGenerator {

  private Random rand = new Random();

  public Shape next() {

    switch(rand.nextInt(3)) {

      default:

      case 0: return new Circle();

      case 1: return new Square();

      case 2: return new Triangle();

    }

  }

}

 

public class Shapes {

  private static Test monitor = new Test();

  private static RandomShapeGenerator gen =

    new RandomShapeGenerator();

  public static void main(String[] args) {

    Shape[] s = new Shape[9];

    for(int i = 0; i < s.length; i++)

      s[i] = gen.next();

    for(int i = 0; i < s.length; i++)

      s[i].draw();

    monitor.expect(new Object[] {

      new TestExpression("%% (Circle|Square|Triangle)"

        + "//.draw//(//)", s.length)

    });

  }

}

 

The base class Shape establishes the common interface to anything inherited from Shape—that is, all shapes can be drawn and erased. The derived classes override these definitions to provide unique behavior for each specific type of shape.

RandomShapeGenerator is a kind of “factory” that produces a reference to a randomly-selected Shape object each time you call its next( ) method. Note that the upcasting happens in the return statements, each of which takes a reference to a Circle, Square, or Triangle and sends it out of next( ) as the return type, Shape. So whenever you call next( ), you never get a chance to see what specific type it is, since you always get back a plain Shape reference.

基类“Shape”为每个继承“Shape”类的对象提供了通用的接口,所有的“Shape”的对象以及子类的对象都可以调用drawerase方法,派生类中覆写了所有提供的接口来为每个派生类的对象提供唯一的方法。RandomShapeGenerator当你调用next()方法的时候来产生随即的Shape的对象,所有派生类的对象都被进行了上传转换动作,你永远也不可能看到这些对象的实际类型,因为他们执行后就被强转为了基类的对象。

main( ) contains an array of Shape references filled through calls to RandomShapeGenerator.next( ). At this point you know you have Shapes, but you don’t know anything more specific than that (and neither does the compiler). However, when you step through this array and call draw( ) for each one, the correct type-specific behavior magically occurs, as you can see from the output when you run the program.

Main()通过调用RandomShapeGenerator.next( )方法获得多了很多的Shape对象,在这时候你只是知道自己获得了Shape对象,但是其它的信息一点也不知道,包括编译器。但是当你分别使用这些Shape的对象调用draw()方法的时候,运行程序的时候你会看到程序调用正确的行为。

The point of choosing the shapes randomly is to drive home the understanding that the compiler can have no special knowledge that allows it to make the correct calls at compile time. All the calls to draw( ) must be made through dynamic binding.

之所以要选择随即的形状就是要展示在编译的时候编译器也不知道它应该调用哪个方法是正确的,所有关于draw()方法的调用都是动态绑定的。

Extensibility

Now let’s return to the musical instrument example. Because of polymorphism, you can add as many new types as you want to the system without changing the tune( ) method. In a well-designed OOP program, most or all of your methods will follow the model of tune( ) and communicate only with the base-class interface. Such a program is extensible because you can add new functionality by inheriting new data types from the common base class. The methods that manipulate the base-class interface will not need to be changed at all to accommodate the new classes.

现在让我们重新回到乐器的那个例子,因为多态,你可以在不修改tune()方法的前提下随意的增加新的类型,在一个良好的面向对象设计中,大部分的方法应该和例子中的tune()方法一样,通讯的话只需要调用基类的接口就可以,这样的程序就是一个可扩展的,因为他你可以将新类型里面的继承下来的方法进行更全面的扩展。这样那些与基类打交道的对象不需要做任何修改就可以与新继承下来的对象打交道。

Consider what happens if you take the instrument example and add more methods in the base class and a number of new classes. Here’s the diagram:

可以设想如果继续那个instrument的例子,你可用为基类增加很多的派生类和方法,那么会发生什么,下面就是这个关系图:

All these new classes work correctly with the old, unchanged tune( ) method. Even if tune( ) is in a separate file and new methods are added to the interface of Instrument, tune( ) will still work correctly, even without recompiling it. Here is the implementation of the diagram:

在不修改tune()的方法的情况下,这些新增的类可以很好的运转,即使将tune()方法保存到一个独立的文件中,给Instrument类增加一些新的接口,tune()仍然可以正常的工作,甚至不需要编译,下面就是对类关系图的一个实现。

package c07.music3;

import com.bruceeckel.simpletest.*;

import c07.music.Note;

 

class Instrument {

  void play(Note n) {

    System.out.println("Instrument.play() " + n);

  }

  String what() { return "Instrument"; }

  void adjust() {}

}

 

class Wind extends Instrument {

  void play(Note n) {

    System.out.println("Wind.play() " + n);

  }

  String what() { return "Wind"; }

  void adjust() {}

}

 

class Percussion extends Instrument {

  void play(Note n) {

    System.out.println("Percussion.play() " + n);

  }

  String what() { return "Percussion"; }

  void adjust() {}

}

 

class Stringed extends Instrument {

  void play(Note n) {

    System.out.println("Stringed.play() " + n);

  }

  String what() { return "Stringed"; }

  void adjust() {}

}

 

class Brass extends Wind {

  void play(Note n) {

    System.out.println("Brass.play() " + n);

  }

  void adjust() {

    System.out.println("Brass.adjust()");

  }

}

 

class Woodwind extends Wind {

  void play(Note n) {

    System.out.println("Woodwind.play() " + n);

  }

  String what() { return "Woodwind"; }

}

 

public class Music3 {

  private static Test monitor = new Test();

  public static void tune(Instrument i) {

    i.play(Note.MIDDLE_C);

  }

  public static void tuneAll(Instrument[] e) {

    for(int i = 0; i < e.length; i++)

      tune(e[i]);

  }

  public static void main(String[] args) {

    Instrument[] orchestra = {

      new Wind(),

      new Percussion(),

      new Stringed(),

      new Brass(),

      new Woodwind()

    };

    tuneAll(orchestra);

    monitor.expect(new String[] {

      "Wind.play() Middle C",

      "Percussion.play() Middle C",

      "Stringed.play() Middle C",

      "Brass.play() Middle C",

      "Woodwind.play() Middle C"

    });

  }

}

The new methods are what( ), which returns a String reference with a description of the class, and adjust( ), which provides some way to adjust each instrument.

In main( ), when you place something inside the orchestra array, you automatically upcast to Instrument.

What()新方法可以返回一个对象的类型的描述,而adjust()方法则提供了不同的方式去调整没见Instrument。在主方法中,当你在orchestra这个容器中放入对象的时候,会自动的将这些对象上传转化为Instrument类的对象。

You can see that the tune( ) method is blissfully ignorant of all the code changes that have happened around it, and yet it works correctly. This is exactly what polymorphism is supposed to provide. Changes in your code don’t cause damage to parts of the program that should not be affected. Put another way, polymorphism is an important technique for the programmer to “separate the things that change from the things that stay the same.”

你可以看到tune()方法对周围代码的改动不需要任何的调整,它仍然可以正常的运转。这恰恰就是多态机制所做到的,当你修改代码的时候不会对这部分代码造成影响。或者换一种说话,多态是一种将变化的和不变的东西分割开来的一种很重要的技术。

Pitfall: “overriding” private methods

Here’s something you might innocently try to do:

import com.bruceeckel.simpletest.*;

 

public class PrivateOverride {

  private static Test monitor = new Test();

  private void f() {

    System.out.println("private f()");

  }

  public static void main(String[] args) {

    PrivateOverride po = new Derived();

    po.f();

    monitor.expect(new String[] {

      "private f()"

    });

  }

}

 

class Derived extends PrivateOverride {

  public void f() {

    System.out.println("public f()");

  }

}

 

You might reasonably expect the output to be “public f( )”, but a private method is automatically final, and is also hidden from the derived class. So Derived’s f( ) in this case is a brand new method; it’s not even overloaded, since the base-class version of f( ) isn’t visible in Derived.

你可能很期待的等待输出的结果是“public f()”,但是private方法即为final,当然在派生类看来是隐藏的。所以在派生类里面这种情况会被视为一个新的方法,因为基类的private方法在派生类是看不到的。

The result of this is that only non-private methods may be overridden, but you should watch out for the appearance of overriding private methods, which generates no compiler warnings, but doesn’t do what you might expect. To be clear, you should use a different name from a private base-class method in your derived class.

这样的结果说明了只有非private方法才可以被覆写,但是你需要留意那些看上去很像覆写private的方法,这些不会产生编译错误,但是不会执行你所希望的,为了代码更加清晰,在派生类当中你应该命名一个和基类不同的方法名称。

Abstract classes and methods

In all the instrument examples, the methods in the base class Instrument were always “dummy” methods. If these methods are ever called, you’ve done something wrong. That’s because the intent of Instrument is to create a common interface for all the classes derived from it.

在所有关于Instrument的例子中,Instrument类中的方法都不是真正的方法,如果这些方法被调用的话将会出现错误信息。因为Instrument类的目的只是为了所有的派生类建立一个公共的接口。

The only reason to establish this common interface is so it can be expressed differently for each different subtype. It establishes a basic form, so you can say what’s in common with all the derived classes. Another way of saying this is to call Instrument an abstract base class (or simply an abstract class). You create an abstract class when you want to manipulate a set of classes through this common interface. All derived-class methods that match the signature of the base-class declaration will be called using the dynamic binding mechanism. (However, as seen in the last section, if the method’s name is the same as the base class but the arguments are different, you’ve got overloading, which probably isn’t what you want.)

创建公共接口的唯一目的是因为接口是因为同样的接口可以为不同的派生类对象提供不同的行为。它只是建立一个基本的框架,你可以说这是所有的派生类都有的,换一种说法你可以叫Instrument为抽象的基类或者简化为抽象类。你创建了一个公共接口,你可以通过这个接口操作很多这个接口的派生类。在派生类中所有与基类的参数声明相同的方法在调用的时候都会启用动态绑定机制。(然而你会在最后一章中看到,如果方法的名字和基类相同,但是参数不同的话这被称之为重载,这可能不是你所期望的)

If you have an abstract class like Instrument, objects of that class almost always have no meaning. That is, Instrument is meant to express only the interface, and not a particular implementation, so creating an Instrument object makes no sense, and you’ll probably want to prevent the user from doing it. This can be accomplished by making all the methods in Instrument print error messages, but that delays the information until run time and requires reliable exhaustive testing on the user’s part. It’s better to catch problems at compile time.

如果你有一个像Instrument的类,这种类的对象是没有任何意义的,Instrument仅仅是为了表示一个接口,并没有详细的实现,所以创建Instrument的对象没有任何意义,你可能像禁止用户这么做,你可以通过在方法中打印错误信息来完成,但是这样比较晚了,因为这样需要程序运行的时候进行非常详尽的测试才行。而最好的是应该在编译期间捕获这些错误。

Java provides a mechanism for doing this called the abstract method. This is a method that is incomplete; it has only a declaration and no method body. Here is the syntax for an abstract method declaration:

Java提供了做这种事的机制被称为abstract方法,这样的方法不是完整的;它只有声明部分没有方法实现部分。下面给出一个关于abstract方法声明的例子:

abstract void f();

A class containing abstract methods is called an abstract class. If a class contains one or more abstract methods, the class itself must be qualified as abstract. (Otherwise, the compiler gives you an error message.)

If an abstract class is incomplete, what is the compiler supposed to do when someone tries to make an object of that class? It cannot safely create an object of an abstract class, so you get an error message from the compiler. This way, the compiler ensures the purity of the abstract class, and you don’t need to worry about misusing it.

如果一个类内部有abstract的方法那么这个类就被称为“虚拟类”。如果一个类中创建了一个或者多个虚拟的方法,那么这个类必须被标识为虚拟类。否则编译器会提示错误信息。虚拟类是不完整的,但是如果有人试图创建这个类的对象的时候编译器会怎么做呢?去创建虚拟类的对象是不安全的,所以编译器会提示错误信息,这样编译器就可以确保这是一个纯粹的虚拟类,你就不必担心误用这个虚拟类了。

If you inherit from an abstract class and you want to make objects of the new type, you must provide method definitions for all the abstract methods in the base class. If you don’t (and you may choose not to), then the derived class is also abstract, and the compiler will force you to qualify that class with the abstract keyword.

It’s possible to create a class as abstract without including any abstract methods. This is useful when you’ve got a class in which it doesn’t make sense to have any abstract methods, and yet you want to prevent any instances of that class.

如果你继承了这个虚拟类然后去创建这个新类的对象,那么你必须在这个新类中实现所有虚拟类中定义的方法,如果没有全部去实现这些方法,那么派生类也是虚拟类,并且编译器也会强制你将这个类标识为abstract关键词。创建一个不包含虚拟方法的虚拟类也是允许的。如果你希望创建一个不希望被实例话的类,那么这就可以使用这种方式实现。

The Instrument class can easily be turned into an abstract class. Only some of the methods will be abstract, since making a class abstract doesn’t force you to make all the methods abstract. Here’s what it looks like:

Here’s the orchestra example modified to use abstract classes and methods:

这里关于orchestra例子被修改为了使用abstract关键词的类和方法:

package c07.music4;

import com.bruceeckel.simpletest.*;

import java.util.*;

import c07.music.Note;

 

abstract class Instrument {

  private int i;

  public abstract void play(Note n);

  public String what() {

    return "Instrument";

  }

  public abstract void adjust();

}

 

class Wind extends Instrument {

  public void play(Note n) {

    System.out.println("Wind.play() " + n);

  }

  public String what() { return "Wind"; }

  public void adjust() {}

}

 

class Percussion extends Instrument {

  public void play(Note n) {

    System.out.println("Percussion.play() " + n);

  }

  public String what() { return "Percussion"; }

  public void adjust() {}

}

 

class Stringed extends Instrument {

  public void play(Note n) {

    System.out.println("Stringed.play() " + n);

  }

  public String what() { return "Stringed"; }

  public void adjust() {}

}

 

class Brass extends Wind {

  public void play(Note n) {

    System.out.println("Brass.play() " + n);

  }

  public void adjust() {

    System.out.println("Brass.adjust()");

  }

}

 

class Woodwind extends Wind {

  public void play(Note n) {

    System.out.println("Woodwind.play() " + n);

  }

  public String what() { return "Woodwind"; }

}

 

public class Music4 {

  private static Test monitor = new Test();

  static void tune(Instrument i) {

    i.play(Note.MIDDLE_C);

  }

  static void tuneAll(Instrument[] e) {

    for(int i = 0; i < e.length; i++)

      tune(e[i]);

  }

  public static void main(String[] args) {

    Instrument[] orchestra = {

      new Wind(),

      new Percussion(),

      new Stringed(),

      new Brass(),

      new Woodwind()

    };

    tuneAll(orchestra);

    monitor.expect(new String[] {

      "Wind.play() Middle C",

      "Percussion.play() Middle C",

      "Stringed.play() Middle C",

      "Brass.play() Middle C",

      "Woodwind.play() Middle C"

    });

  }

}

You can see that there’s really no change except in the base class.

It’s helpful to create abstract classes and methods because they make the abstractness of a class explicit, and tell both the user and the compiler how it was intended to be used.

你可以看到除了在基类当中并没有什么实际的变动。由此可见创建abstract的类和方法是很有用处的,因为它明确了类的抽象性,并且告诉使用者和编译器应该如何使用。
python+opencv简谱识别音频生成系统源码含GUI界面+详细运行教程+数据 一、项目简介 提取简谱中的音乐信息,依据识别到的信息生成midi文件。 Extract music information from musical scores and generate a midi file according to it. 二、项目运行环境 python=3.11.1 第三方库依赖 opencv-python=4.7.0.68 numpy=1.24.1 可以使用命令 pip install -r requirements.txt 来安装所需的第三方库。 三、项目运行步骤 3.1 命令行运行 运行main.py。 输入简谱路径:支持图片或文件夹,相对路径或绝对路径都可以。 输入简谱主音:它通常在第一页的左上角“1=”之后。 输入简谱速度:即每分钟拍数,同在左上角。 选择是否输出程序中间提示信息:请输入Y或N(不区分大小写,下同)。 选择匹配精度:请输入L或M或H,对应低/中/高精度,一般而言输入L即可。 选择使用的线程数:一般与CPU核数相同即可。虽然python的线程不是真正的多线程,但仍能起到加速作用。 估算字符上下间距:这与简谱中符号的密集程度有关,一般来说纵向符号越稀疏,这个值需要设置得越大,范围通常在1.0-2.5。 二值化算法:使用全局阈值则跳过该选项即可,或者也可输入OTSU、采用大津二值化算法。 设置全局阈值:如果上面选择全局阈值则需要手动设置全局阈值,对于.\test.txt中所提样例,使用全局阈值并在后面设置为160即可。 手动调整中间结果:若输入Y/y,则在识别简谱后会暂停代码,并生成一份txt文件,在其中展示识别结果,此时用户可以通过修改这份txt文件来更正识别结果。 如果选择文件夹的话,还可以选择所选文件夹中不需要识别的文件以排除干扰
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值