9 JAVA编程思想 多形性

               

欢迎转载,转载请标明出处:http://blog.csdn.net/notbaron/article/details/51040241

“对于面向对象的程序设计语言,多型性是第三种最基本的特征(前两种是数据抽象和继承。”

“多形性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成长”。

通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实施细节的隐藏,可将接口与实施细节分离,使所有细节成为“private”(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。

多形性却涉及对“类型”的分解。通过上面的学习,已知道通过继承可将一个对象当作它自己的类型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(从相同的基础类型中衍生出来)可被当作同一种类型对待。而且只需一段代码,即可对所有不同的类型进行同样的处理。利用具有多形性的方法调用,一种类型可将自己与另一种相似的类型区分开,只要它们都是从相同的基础类型中衍生出来的。这种区分是通过各种方法在行为上的差异实现的,可通过基础类实现对那些方法的调用。

由浅入深地学习有关多形性的问题(也叫作动态绑定、推迟绑定或者运行期绑定)。

 

1      上溯造型

可将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得

一个对象句柄,并将其作为基础类型句柄使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位于最上方。

但这样做也会遇到一个问题,如下例所示:

package com.toad7;

 

class Note {

    privateintvalue;

 

    private Note(intval) {

        value =val;

    }

 

    publicstaticfinal Note middleC =new Note(0),cSharp =new Note(1),

            cFlat =new Note(2);

} // Etc.

 

class Instrument {

    publicvoid play(Noten) {

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

    }

}

 

// Wind objects are instruments

// because they have the sameinterface:

class Windextends Instrument {

    // Redefine interfacemethod:

    publicvoid play(Noten) {

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

    }

}

 

publicclass Music {

    publicstaticvoid tune(Instrument i) {

        // ...

        i.play(Note.middleC);

    }

 

    publicstaticvoid main(String[] args) {

        Windflute =new Wind();

        tune(flute);//Upcasting

    }

} // /:~

 

输出:

Wind.play()

 

方法Music.tune()接收一个Instrument句柄,同时也接收从Instrument衍生出来的所有东西。当一个Wind句柄传递给 tune()的时候,就会出现这种情况。此时没有造型的必要。这样做是可以接受的;

Instrument里的接口必须存在于Wind 中,因为Wind是从Instrument 里继承得到的。从 Wind向Instrument的上溯造型可能“缩小”那个接口,但不可能把它变得比 Instrument的完整接口还要小。

 

 

1.1      为什么要上溯造型

为什么所有人都应该有意忘记一个对象的类型呢?进行上溯造型时,就可能产生这方面的疑惑。而且如果让tune()简单地取得一个Wind 句柄,将其作为自己的自变量使用,似乎会更加简单、直观得多。但要注意:假如那样做,就需为系统内Instrument 的每种类型写一个全新的tune()。

示例如下:

package com.toad7;

class Note {

    privateintvalue;

    private Note(intval) {

        value =val;

    }

    publicstaticfinal Note middleC =new Note(0),cSharp =new Note(1),

            cFlat =new Note(2);

} // Etc.

class Instrument {

    publicvoid play(Noten) {

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

    }

}

// Wind objects are instruments

// because they have the same interface:

class Windextends Instrument {

    // Redefine interfacemethod:

//  publicvoid play(Note n) {

//      System.out.println("Wind.play()");

//  }

}

publicclass Music {

    publicstaticvoid tune(Instrument i) {

        // ...

        i.play(Note.middleC);

    }

    publicstaticvoid main(String[] args) {

        Windflute =new Wind();

        tune(flute);//Upcasting

    }

} // /:~

 

这样做行得通,但却存在一个极大的弊端:必须为每种新增的Instrument2类编写与类紧密相关的方法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个象tune()那样的新方法或者为Instrument添加一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行过载设置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。

但假如只写一个方法,将基础类作为自变量或参数使用,而不是使用那些特定的衍生类,岂不是会简单得多?也就是说,如果我们能不顾衍生类,只让自己的代码与基础类打交道,那么省下的工作量将是难以估计的。

这正是“多形性”大显身手的地方。

 

2      深入理解

2.1      方法调用的绑定

将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何程序化语言里都是不可能的。C 编译器只有一种方法调用,那就是“早期绑定”。

上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个Instrument句柄的前提下,编译器不知道具体该调用哪个方法。

解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。

Java 中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。

把一个方法声明成final能防止其他人覆盖那个方法。但也许更重要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可为final 方法调用生成效率更高的代码。

 

2.2      产生正确的行为

Java 里绑定的所有方法都通过后期绑定具有多形性以后,就可以相应地编写自己的代码,令其与基础类沟通。此时,所有的衍生类都保证能用相同的代码正常地工作。或者换用另一种方法,我们可以“将一条消息发给一个对象,让对象自行判断要做什么事情。”

在面向对象的程序设计中,有一个经典的“形状”例子。由于它很容易用可视化的形式表现出来,所以经常都用它说明问题。但很不幸的是,它可能误导初学者认为 OOP只是为图形化编程设计的,这种认识当然是错误的。

形状例子有一个基础类,名为Shape;另外还有大量衍生类型:Circle(圆形),Square(方形),Triangle(三角形)等等。大家之所以喜欢这个例子,因为很容易理解“圆属于形状的一种类型”等概念。

下面这幅继承图向我们展示了它们的关系:

上溯造型可用下面这个语句简单地表现出来:

 Shape s = new Circle();

我们创建了Circle 对象,并将结果句柄立即赋给一个Shape。这表面看起来似乎属于错误操作(将一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,Circle属于Shape 的一种。因此编译器认可上述语句,不会向我们提示一条出错消息。

当我们调用其中一个基础类方法时(已在衍生类里覆盖):

s.draw();

同样地,大家也许认为会调用Shape的draw(),因为这毕竟是一个Shape句柄。那么编译器怎样才能知道该做其他任何事情呢?但此时实际调用的是Circle.draw(),因为后期绑定已经介入(多形性)。

示例如下:

package com.toad7;

 

class Shape {

    void draw() {

    }

 

    void erase() {

    }

}

 

class Circleextends Shape {

    void draw() {

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

    }

 

    void erase() {

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

    }

}

 

class Squareextends Shape {

    void draw() {

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

    }

 

    void erase() {

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

    }

}

 

class Triangleextends Shape {

    void draw() {

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

    }

 

    void erase() {

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

    }

}

 

publicclass Shapes {

    publicstatic Shape randShape() {

        switch ((int) (Math.random() * 3)) {

        default:// To quiet the compiler

        case 0:

            returnnew Circle();

        case 1:

            returnnew Square();

        case 2:

            returnnew Triangle();

        }

    }

 

    publicstaticvoid main(String[] args) {

        Shape[]s =new Shape[9];

        // Fill up the arraywith shapes:

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

            s[i] =randShape();

        // Makepolymorphicmethod calls:

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

            s[i].draw();

    }

} // /:~

 

输出:

Circle.draw()

Triangle.draw()

Circle.draw()

Circle.draw()

Circle.draw()

Square.draw()

Triangle.draw()

Square.draw()

Square.draw()

 

针对从Shape 衍生出来的所有东西,Shape 建立了一个通用接口——也就是说,所有(几何)形状都可以描绘和删除。衍生类覆盖了这些定义,为每种特殊类型的几何形状都提供了独一无二的行为。

在主类Shapes 里,包含了一个static 方法,名为 randShape()。它的作用是在每次调用它时为某个随机选择的Shape 对象生成一个句柄。请注意上溯造型是在每个return 语句里发生的。这个语句取得指向一个Circle,Square 或者Triangle 的句柄,并将其作为返回类型 Shape发给方法。所以无论什么时候调用这个方法,就绝对没机会了解它的具体类型到底是什么,因为肯定会获得一个单纯的Shape 句柄。

main()包含了 Shape 句柄的一个数组,其中的数据通过对randShape()的调用填入。在这个时候,我们知道自己拥有Shape,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进,

并为每个元素调用draw()的时候,与各类型有关的正确行为会魔术般地发生。

由于几何形状是每次随机选择的,所以每次运行都可能有不同的结果。之所以要突出形状的随机选择,是为了体会一点:为了在编译的时候发出正确的调用,编译器毋需获得任何特殊的情报。对draw()的所有调用都是通过动态绑定进行的。

 

2.3      扩展性

让我们仍然返回乐器(Instrument)示例。由于存在多形性,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改true()方法。在一个设计良好的OOP程序中,我们的大多数或者所有方法都会遵从tune()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不需要改变, 对于乐器例子,假设我们在基础类里加入更多的方法,以及一系列新类,那么会出现什么情况呢?下面是示意图:

所有这些新类都能与老类——tune()默契地工作,毋需对tune()作任何调整。即使 tune()位于一个独立的文件里,而将新方法添加到Instrument 的接口,tune()也能正确地工作,不需要重新编译。下面这个程序是对上述示意图的具体实现:

package com.toad7;

importjava.util.*;

class Instrument3 {

    publicvoid play() {

        System.out.println("Instrument3.play()");

    }

    public String what() {

        return"Instrument3";

    }

    publicvoid adjust() {

    }

}

class Wind3extends Instrument3 {

    publicvoid play() {

        System.out.println("Wind3.play()");

    }

    public String what() {

        return"Wind3";

    }

    publicvoid adjust() {

    }

}

class Percussion3extends Instrument3 {

    publicvoid play() {

        System.out.println("Percussion3.play()");

    }

    public String what() {

        return"Percussion3";

    }

    publicvoid adjust() {

    }

}

class Stringed3extends Instrument3 {

    publicvoid play() {

        System.out.println("Stringed3.play()");

    }

    public String what() {

        return"Stringed3";

    }

    publicvoid adjust() {

    }

}

class Brass3extends Wind3 {

    publicvoid play() {

        System.out.println("Brass3.play()");

    }

    publicvoid adjust() {

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

    }

}

class Woodwind3extends Wind3 {

    publicvoid play() {

        System.out.println("Woodwind3.play()");

    }

    public String what() {

        return"Woodwind3";

    }

}

publicclass Music3 {

    // Doesn't care abouttype, so new types

    // added to thesystem still work right:

    staticvoid tune(Instrument3i) {

        // ...

        i.play();

    }

    staticvoid tuneAll(Instrument3[]e) {

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

            tune(e[i]);

    }

    publicstaticvoid main(String[] args) {

        Instrument3[]orchestra =new Instrument3[5];

        inti = 0;

        //Upcastingduring addition to the array:

        orchestra[i++] = new Wind3();

        orchestra[i++] = new Percussion3();

        orchestra[i++] = new Stringed3();

        orchestra[i++] = new Brass3();

        orchestra[i++] = new Woodwind3();

        tuneAll(orchestra);

    }

} // /:~

新方法是what()和adjust()。前者返回一个String句柄,同时返回对那个类的说明;后者使我们能对每种乐器进行调整。

在main()中,当我们将某样东西置入Instrument3数组时,就会自动上溯造型到 Instrument3。

可以看到,在围绕tune()方法的其他所有代码都发生变化的同时,tune()方法却丝毫不受它们的影响,依然故我地正常工作。这正是利用多形性希望达到的目标。我们对代码进行修改后,不会对程序中不应受到影响的部分造成影响。此外,我们认为多形性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有发生改变的东西区分开”。

 

3      覆盖与过载

在下面这个程序中,方法play()的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“过载”。编译器允许我们对方法进行过载处理,使其不报告出错。但这种行为可能并不是我们所希望的。

下面是个例子:

package com.toad7;

class NoteX {

 publicstaticfinalint

   MIDDLE_C = 0,C_SHARP = 1,C_FLAT = 2;

}

class InstrumentX {

 publicvoid play(intNoteX) {

   System.out.println("InstrumentX.play()");

 }

}

class WindXextends InstrumentX {

 //OOPS! Changes the method interface:

 publicvoid play(NoteXn) {

   System.out.println("WindX.play(NoteXn)");

 }

publicclass WindError {

 publicstaticvoid tune(InstrumentX i) {

   // ...

   i.play(NoteX.MIDDLE_C);

 }

 publicstaticvoid main(String[] args) {

   WindX flute =new WindX();

   tune(flute);// Not the desiredbehavior!

 }

} ///:~

输出是:

InstrumentX.play()

 

在InstrumentX 中,play()方法采用了一个int(整数)数值,它的标识符是NoteX。也就是说,即使 NoteX 是一个类名,也可以把它作为一个标识符使用,编译器不会报告出错。但在WindX中,play()采用一个NoteX 句柄,它有一个标识符 n。即便我们使用“play(NoteX

NoteX)”,编译器也不会报告错误。这样一来,看起来就象是程序员有意覆盖play()的功能,但对方法的类型定义却稍微有些不确切。然而,编译器此时假定的是程序员有意进行“过载”,而非“覆盖”。请仔细体会这两个术语的区别。“过载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。

请注意如果遵守标准的Java 命名规范,自变量标识符就应该是noteX,这样可把它与类名区分开。 在tune 中,“InstrumentXi”会发出play()消息,同时将某个 NoteX 成员作为自变量使用(MIDDLE_C)。由于NoteX 包含了int 定义,过载的play()方法的int 版本会得到调用。同时由于它尚未被“覆盖”,所以会使用基础类版本。

 

4      抽象类和方法

在所有乐器(Instrument)例子中,基础类 Instrument 内的方法都肯定是“伪”方法。若去调用这些方法,就会出现错误。那是由于Instrument的意图是为从它衍生出去的所有类都创建一个通用接口。

之所以要建立这个通用接口,唯一的原因就是它能为不同的子类型作出不同的表示。它为我们建立了一种基本形式,使我们能定义在所有衍生类里“通用”的一些东西。为阐述这个观念,另一个方法是把 Instrument称为“抽象基础类”(简称“抽象类”)。若想通过该通用接口处理一系列类,就需要创建一个抽象类。

对所有与基础类声明的签名相符的衍生类方法,都可以通过动态绑定机制进行调用(然而,正如之前描述的那样,如果方法名与基础类相同,但自变量或参数不同,就会出现过载现象,那或许并非我们所愿意的)。

如果有一个象Instrument 那样的抽象类,那个类的对象几乎肯定没有什么意义。换言之,Instrument的作用仅仅是表达接口,而不是表达一些具体的实施细节。所以创建一个Instrument对象是没有意义的,而且我们通常都应禁止用户那样做。为达到这个目的,可令Instrument 内的所有方法都显示出错消息。但这样做会延迟信息到运行期,并要求在用户那一面进行彻底、可靠的测试。无论如何,最好的方法都是在编译期间捕捉到问题。

针对这个问题,Java 专门提供了一种机制,名为“抽象方法”。它属于一种不完整的方法,只含有一个声明,没有方法主体。下面是抽象方法声明时采用的语法:

abstract void X();

包含了抽象方法的一个类叫作“抽象类”。如果一个类里包含了一个或多个抽象方法,类就必须指定成abstract(抽象)。否则,编译器会向我们报告一条出错消息。若一个抽象类是不完整的,那么一旦有人试图生成那个类的一个对象,编译器又会采取什么行动呢?由于不

能安全地为一个抽象类创建属于它的对象,所以会从编译器那里获得一条出错提示。通过这种方法,编译器可保证抽象类的“纯洁性”,我们不必担心会误用它。

如果从一个抽象类继承,而且想生成新类型的一个对象,就必须为基础类中的所有抽象方法提供方法定义。如果不这样做(完全可以选择不做),则衍生类也会是抽象的,而且编译器会强迫我们用abstract关键字标志那个类的“抽象”本质。

即使不包括任何abstract 方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而且我们想禁止那个类的所有实例,这种能力就会显得非常有用。

Instrument类可很轻松地转换成一个抽象类。只有其中一部分方法会变成抽象方法,因为使一个类抽象以后,并不会强迫我们将它的所有方法都同时变成抽象。

下面是它看起来的样子:

代码如下:

package com.toad7;

 

importjava.util.*;

 

abstractclass Instrument4 {

    inti;// storage allocated for each

 

    publicabstractvoid play();

 

    public String what() {

        return"Instrument4";

    }

 

    publicabstractvoid adjust();

}

 

class Wind4extends Instrument4 {

    publicvoid play() {

        System.out.println("Wind4.play()");

    }

 

    public String what() {

        return"Wind4";

    }

 

    publicvoid adjust() {

    }

}

 

class Percussion4extends Instrument4 {

    publicvoid play() {

        System.out.println("Percussion4.play()");

    }

 

    public String what() {

        return"Percussion4";

    }

 

    publicvoid adjust() {

    }

}

 

class Stringed4extends Instrument4 {

    publicvoid play() {

        System.out.println("Stringed4.play()");

    }

 

    public String what() {

        return"Stringed4";

    }

 

    publicvoid adjust() {

    }

}

 

class Brass4extends Wind4 {

    publicvoid play() {

        System.out.println("Brass4.play()");

    }

 

    publicvoid adjust() {

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

    }

}

 

class Woodwind4extends Wind4 {

    publicvoid play() {

        System.out.println("Woodwind4.play()");

    }

 

    public String what() {

        return"Woodwind4";

    }

}

 

publicclass Music4 {

    // Doesn't care abouttype, so new types

    // added to thesystem still work right:

    staticvoid tune(Instrument4i) {

        // ...

        i.play();

    }

 

    staticvoid tuneAll(Instrument4[]e) {

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

            tune(e[i]);

    }

 

    publicstaticvoid main(String[] args) {

        Instrument4[]orchestra =new Instrument4[5];

        inti = 0;

        //Upcastingduring addition to the array:

        orchestra[i++] = new Wind4();

        orchestra[i++] = new Percussion4();

        orchestra[i++] = new Stringed4();

        orchestra[i++] = new Brass4();

        orchestra[i++] = new Woodwind4();

        tuneAll(orchestra);

    }

} // /:~

输出如下:

Wind4.play()

Percussion4.play()

Stringed4.play()

Brass4.play()

Woodwind4.play()

 

除基础类以外,实际并没有进行什么改变。创建抽象类和方法有时对我们非常有用,因为它们使一个类的抽象变成明显的事实,可明确告诉用户和编译器自己打算如何用它

 

5      接口

“interface”(接口)关键字使抽象的概念更深入了一层。我们可将其想象为一个“纯”抽象类。它允许创建者规定一个类的基本形式:方法名、自变量列表以及返回类型,但不规定方法主体。接口也包含了基本数据类型的数据成员,但它们都默认为static 和final。接口只提供一种形式,并不提供实施的细节。

接口这样描述自己:“对于实现我的所有类,看起来都应该象我现在这个样子”。因此,采用了一个特定接口的所有代码都知道对于那个接口可能会调用什么方法。这便是接口的全部含义。所以我们常把接口用于建立类和类之间的一个“协议”。有些面向对象的程序设计语言采用了一个名为“protocol”(协议)的关键字,它做的便是与接口相同的事情。

为创建一个接口,请使用interface关键字,而不要用 class。与类相似,我们可在 interface关键字的前面增加一个 public关键字(但只有接口定义于同名的一个文件内);或者将其省略,营造一种“友好的”状态。

为了生成与一个特定的接口(或一组接口)相符的类,要使用implements(实现)关键字。我们要表达的意思是“接口看起来就象那个样子,这儿是它具体的工作细节”。除这些之外,我们其他的工作都与继承极为相似。下面是乐器例子的示意图:

具体实现了一个接口以后,就获得了一个普通的类,可用标准方式对其进行扩展。

可决定将一个接口中的方法声明明确定义为“public”。但即便不明确定义,它们也会默认为 public。所以在实现一个接口的时候,来自接口的方法必须定义成public。否则的话,它们会默认为“友好的”,而且会限制我们在继承过程中对一个方法的访问——Java 编译器不允许我们那样做。

在Instrument 例子的修改版本中,大家可明确地看出这一点。注意接口中的每个方法都严格地是一个声明,它是编译器唯一允许的。除此以外,Instrument5 中没有一个方法被声明为public,但它们都会自动获得public属性。

 示例:

package com.toad7;

 

importjava.util.*;

 

interface Instrument5 {

    // Compile-timeconstant:

    inti = 5;// static & final

 

    // Cannot have methoddefinitions:

    void play();// Automaticallypublic

 

    Stringwhat();

 

    void adjust();

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值