九、多态(1)

news2024/12/26 7:30:09

本章概要

  • 向上转型回顾
    • 忘掉对象类型
  • 转机
    • 方法调用绑定
    • 产生正确的行为
    • 可扩展性
    • 陷阱:“重写”私有方法
    • 陷阱:属性与静态方法

多态是面向对象编程语言中,继数据抽象和继承之外的第三个重要特性。

多态提供了另一个维度的接口与实现分离,以解耦做什么和怎么做。多态不仅能改善代码的组织,提高代码的可读性,而且能创建有扩展性的程序——无论在最初创建项目时还是在添加新特性时都可以“生长”的程序。

封装通过合并特征和行为来创建新的数据类型。隐藏实现通过将细节私有化把接口与实现分离。这种类型的组织机制对于有面向过程编程背景的人来说,更容易理解。而多态是消除类型之间的耦合。在上一章中,继承允许把一个对象视为它本身的类型或它的基类类型。这样就能把很多派生自一个基类的类型当作同一类型处理,因而一段代码就可以无差别地运行在所有不同的类型上了。多态方法调用允许一种类型表现出与相似类型的区别,只要这些类型派生自一个基类。这种区别是当你通过基类调用时,由方法的不同行为表现出来的。

在本章中,通过一些基本、简单的例子(这些例子中只保留程序中与多态有关的行为),你将逐步学习多态(也称为_动态绑定_或_后期绑定_或_运行时绑定_)。

向上转型回顾

在上一章中,你看到了如何把一个对象视作它的自身类型或它的基类类型。这种把一个对象引用当作它的基类引用的做法称为向上转型,因为继承图中基类一般都位于最上方。

同样你也在下面的音乐乐器例子中发现了问题。即然几个例子都要演奏乐符(Note),首先我们先在包中单独创建一个 Note 枚举类:

// polymorphism/music/Note.java
// Notes to play on musical instruments
package polymorphism.music;

public enum Note {
    MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}

枚举已经在”第 6 章初始化和清理“一章中介绍过了。

这里,Wind 是一种 Instrument;因此,Wind 继承 Instrument

// polymorphism/music/Instrument.java
package polymorphism.music;

class Instrument {
    public void play(Note n) {
        System.out.println("Instrument.play()");
    }
}

// polymorphism/music/Wind.java
package polymorphism.music;
// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
    // Redefine interface method:
    @Override
    public void play(Note n) {
        System.out.println("Wind.play() " + n);
    }
}

Music 的方法 tune() 接受一个 Instrument 引用,同时也接受任何派生自 Instrument 的类引用:

// polymorphism/music/Music.java
// Inheritance & upcasting
// {java polymorphism.music.Music}
package polymorphism.music;

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); // Upcasting
    }
}

输出:

Wind.play() MIDDLE_C

main() 中你看到了 tune() 方法传入了一个 Wind 引用,而没有做类型转换。这样做是允许的—— Instrument 的接口一定存在于 Wind 中,因此 Wind 继承了 Instrument。从 Wind 向上转型为 Instrument 可能“缩小”接口,但不会比 Instrument 的全部接口更少。

忘掉对象类型

Music.java 看起来似乎有点奇怪。为什么所有人都故意忘记掉对象类型呢?当向上转型时,就会发生这种情况,而且看起来如果 tune() 接受的参数是一个 Wind 引用会更为直观。这会带来一个重要问题:如果你那么做,就要为系统内 Instrument 的每种类型都编写一个新的 tune() 方法。假设按照这种推理,再增加 StringedBrass 这两种 Instrument :

// polymorphism/music/Music2.java
// Overloading instead of upcasting
// {java polymorphism.music.Music2}
package polymorphism.music;

class Stringed extends Instrument {
    @Override
    public void play(Note n) {
        System.out.println("Stringed.play() " + n);
    }
}

class Brass extends Instrument {
    @Override
    public void play(Note n) {
        System.out.println("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);
    }
}

输出:

Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C

这样行得通,但是有一个主要缺点:必须为添加的每个新 Instrument 类编写特定的方法。这意味着开始时就需要更多的编程,而且以后如果添加类似 tune() 的新方法或 Instrument 的新类型时,还有大量的工作要做。考虑到如果你忘记重载某个方法,编译器也不会提示你,这会造成类型的整个处理过程变得难以管理。

如果只写一个方法以基类作为参数,而不用管是哪个具体派生类,这样会变得更好吗?也就是说,如果忘掉派生类,编写的代码只与基类打交道,会不会更好呢?

这正是多态所允许的。但是大部分拥有面向过程编程背景的程序员会对多态的运作方式感到一些困惑。

转机

运行程序后会看到 Music.java 的难点。Wind.play() 的输出结果正是我们期望的,然而它看起来似乎不应该得出这样的结果。观察 tune() 方法:

public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
}

它接受一个 Instrument 引用。那么编译器是如何知道这里的 Instrument 引用指向的是 Wind,而不是 BrassStringed 呢?编译器无法得知。为了深入理解这个问题,有必要研究一下_绑定_这个主题。

方法调用绑定

将一个方法调用和一个方法主体关联起来称作_绑定_。若绑定发生在程序运行前(如果有的话,由编译器和链接器实现),叫做_前期绑定_。你可能从来没有听说这个术语,因为它是面向过程语言不需选择默认的绑定方式,例如在 C 语言中就只有_前期绑定_这一种方法调用。

上述程序让人困惑的地方就在于前期绑定,因为编译器只知道一个 Instrument 引用,它无法得知究竟会调用哪个方法。

解决方法就是_后期绑定_,意味着在运行时根据对象的类型进行绑定。后期绑定也称为_动态绑定_或_运行时绑定_。当一种语言实现了后期绑定,就必须具有某种机制在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器仍然不知道对象的类型,但是方法调用机制能找到正确的方法体并调用。每种语言的后期绑定机制都不同,但是可以想到,对象中一定存在某种类型信息。

Java 中除了 staticfinal 方法(private 方法也是隐式的 final)外,其他所有方法都是后期绑定。这意味着通常情况下,我们不需要判断后期绑定是否会发生——它自动发生。

为什么将一个对象指明为 final ?正如前一章所述,它可以防止方法被重写。但更重要的一点可能是,它有效地”关闭了“动态绑定,或者说告诉编译器不需要对其进行动态绑定。这可以让编译器为 final 方法生成更高效的代码。然而,大部分情况下这样做不会对程序的整体性能带来什么改变,因此最好是为了设计使用 final,而不是为了提升性能而使用。

产生正确的行为

一旦当你知道 Java 中所有方法都是通过后期绑定来实现多态时,就可以编写只与基类打交道的代码,而且代码对于派生类来说都能正常地工作。或者换种说法,你向对象发送一条消息,让对象自己做正确的事。

面向对象编程中的经典例子是形状 Shape。这个例子很直观,但不幸的是,它可能让初学者困惑,认为面向对象编程只适合图形化程序设计,实际上不是这样。

形状的例子中,有一个基类称为 Shape ,多个不同的派生类型分别是:CircleSquareTriangle 等等。这个例子之所以好用,是因为我们可以直接说“圆(Circle)是一种形状(Shape)”,这很容易理解。继承图展示了它们之间的关系:

在这里插入图片描述

向上转型就像下面这么简单:

Shape s = new Circle();

这会创建一个 Circle 对象,引用被赋值给 Shape 类型的变量 s,这看似错误(将一种类型赋值给另一种类型),然而是没问题的,因此从继承上可认为圆(Circle)就是一个形状(Shape)。因此编译器认可了赋值语句,没有报错。

假设你调用了一个基类方法(在各个派生类中都被重写):

s.draw()

你可能再次认为 Shapedraw() 方法被调用,因为 s 是一个 Shape 引用——编译器怎么可能知道要做其他的事呢?然而,由于后期绑定(多态)被调用的是 Circledraw() 方法,这是正确的。

下面的例子稍微有些不同。首先让我们创建一个可复用的 Shape 类库,基类 Shape 为它的所有子类建立了公共接口——所有的形状都可以被绘画和擦除:

// polymorphism/shape/Shape.java
package polymorphism.shape;

public class Shape {
    public void draw() {}
    public void erase() {}
}

派生类通过重写这些方法为每个具体的形状提供独一无二的方法行为:

// polymorphism/shape/Circle.java
package polymorphism.shape;

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

// polymorphism/shape/Square.java
package polymorphism.shape;

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

// polymorphism/shape/Triangle.java
package polymorphism.shape;

public class Triangle extends Shape {
    @Override
    public void draw() {
        System.out.println("Triangle.draw()");
    }
    @Override
    public void erase() {
        System.out.println("Triangle.erase()");
    }
}

RandomShapes 是一种工厂,每当我们调用 get() 方法时,就会产生一个指向随机创建的 Shape 对象的引用。注意,向上转型发生在 return 语句中,每条 return 语句取得一个指向某个 CircleSquareTriangle 的引用, 并将其以 Shape 类型从 get() 方法发送出去。因此无论何时调用 get() 方法,你都无法知道具体的类型是什么,因为你总是得到一个简单的 Shape 引用:

// polymorphism/shape/RandomShapes.java
// A "factory" that randomly creates shapes
package polymorphism.shape;
import java.util.*;

public class RandomShapes {
    private Random rand = new Random(47);
    
    public Shape get() {
        switch(rand.nextInt(3)) {
            default:
            case 0: return new Circle();
            case 1: return new Square();
            case 2: return new Triangle();
        }
    }
    
    public Shape[] array(int sz) {
        Shape[] shapes = new Shape[sz];
        // Fill up the array with shapes:
        for (int i = 0; i < shapes.length; i++) {
            shapes[i] = get();
        }
        return shapes;
    }
}

array() 方法分配并填充了 Shape 数组,这里使用了 for-in 表达式:

// polymorphism/Shapes.java
// Polymorphism in Java
import polymorphism.shape.*;

public class Shapes {
    public static void main(String[] args) {
        RandomShapes gen = new RandomShapes();
        // Make polymorphic method calls:
        for (Shape shape: gen.array(9)) {
            shape.draw();
        }
    }
}

输出:

Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()

main() 方法中包含了一个 Shape 引用组成的数组,其中每个元素通过调用 RandomShapes 类的 get() 方法生成。现在你只知道拥有一些形状,但除此之外一无所知(编译器也是如此)。然而当遍历这个数组为每个元素调用 draw() 方法时,从运行程序的结果中可以看到,与类型有关的特定行为奇迹般地发生了。

随机生成形状是为了让大家理解:在编译时,编译器不需要知道任何具体信息以进行正确的调用。所有对方法 draw() 的调用都是通过动态绑定进行的。

可扩展性

现在让我们回头看音乐乐器的例子。由于多态机制,你可以向系统中添加任意多的新类型,而不需要修改 tune() 方法。在一个设计良好的面向对象程序中,许多方法将会遵循 tune() 的模型,只与基类接口通信。这样的程序是可扩展的,因为可以从通用的基类派生出新的数据类型,从而添加新的功能。那些操纵基类接口的方法不需要改动就可以应用于新类。

考虑一下乐器的例子,如果在基类中添加更多的方法,并加入一些新类,将会发生什么呢:

在这里插入图片描述

所有的新类都可以和原有类正常运行,不需要改动 tune() 方法。即使 tune() 方法单独存放在某个文件中,而且向 Instrument 接口中添加了新的方法,tune() 方法也无需再编译就能正确运行。下面是类图的实现:

// polymorphism/music3/Music3.java
// An extensible program
// {java polymorphism.music3.Music3}
package polymorphism.music3;
import polymorphism.music.Note;

class Instrument {
    void play(Note n) {
        System.out.println("Instrument.play() " + n);
    }
    
    String what() {
        return "Instrument";
    }
    
    void adjust() {
        System.out.println("Adjusting Instrument");
    }
}

class Wind extends Instrument {
    @Override
    void play(Note n) {
        System.out.println("Wind.play() " + n);
    }
    @Override
    String what() {
        return "Wind";
    }
    @Override
    void adjust() {
        System.out.println("Adjusting Wind");
    }
}

class Percussion extends Instrument {
    @Override
    void play(Note n) {
        System.out.println("Percussion.play() " + n);
    }
    @Override
    String what() {
        return "Percussion";
    }
    @Override
    void adjust() {
        System.out.println("Adjusting Percussion");
    }
}

class Stringed extends Instrument {
    @Override
    void play(Note n) {
        System.out.println("Stringed.play() " + n);
    } 
    @Override
    String what() {
        return "Stringed";
    }
    @Override
    void adjust() {
        System.out.println("Adjusting Stringed");
    }
}

class Brass extends Wind {
    @Override
    void play(Note n) {
        System.out.println("Brass.play() " + n);
    }
    @Override
    void adjust() {
        System.out.println("Adjusting Brass");
    }
}

class Woodwind extends Wind {
    @Override
    void play(Note n) {
        System.out.println("Woodwind.play() " + n);
    }
    @Override
    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);
    }
}

输出:

Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C

新方法 what() 返回一个带有类描述的 String 引用,adjust() 提供一些乐器调音的方法。

main() 方法中,当向 orchestra 数组添加元素时,元素会自动向上转型为 Instrument

tune() 方法可以忽略周围所有代码发生的变化,仍然可以正常运行。这正是我们期待多态能提供的特性。代码中的修改不会破坏程序中其他不应受到影响的部分。换句话说,多态是一项“将改变的事物与不变的事物分离”的重要技术。

陷阱:“重写”私有方法

你可能天真地试图像下面这样做:

// polymorphism/PrivateOverride.java
// Trying to override a private method
// {java polymorphism.PrivateOverride}
package polymorphism;

public class PrivateOverride {
    private void f() {
        System.out.println("private f()");
    }
    
    public static void main(String[] args) {
        PrivateOverride po = new Derived();
        po.f();
    }
}

class Derived extends PrivateOverride {
    public void f() {
        System.out.println("public f()");
    }
}

输出:

private f()

你可能期望输出是 public f(),然而 private 方法可以当作是 final 的,对于派生类来说是隐蔽的。因此,这里 Derivedf() 是一个全新的方法;因为基类版本的 f() 屏蔽了 Derived ,因此它都不算是重写方法。

结论是只有非 private 方法才能被重写,但是得小心重写 private 方法的现象,编译器不报错,但不会按我们所预期的执行。为了清晰起见,派生类中的方法名采用与基类中 private 方法名不同的命名。

如果使用了 @Override 注解,就能检测出问题:

// polymorphism/PrivateOverride2.java
// Detecting a mistaken override using @Override
// {WillNotCompile}
package polymorphism;

public class PrivateOverride2 {
    private void f() {
        System.out.println("private f()");
    }
    
    public static void main(String[] args) {
        PrivateOverride2 po = new Derived2();
        po.f();
    }
}

class Derived2 extends PrivateOverride2 {
    @Override
    public void f() {
        System.out.println("public f()");
    }
}

编译器报错信息是:

error: method does not override or
implement a method from a supertype

陷阱:属性与静态方法

一旦学会了多态,就可以以多态的思维方式考虑每件事。然而,只有普通的方法调用可以是多态的。例如,如果你直接访问一个属性,该访问会在编译时解析:

// polymorphism/FieldAccess.java
// Direct field access is determined at compile time
class Super {
    public int field = 0;
    
    public int getField() {
        return field;
    }
}

class Sub extends Super {
    public int field = 1;
    
    @Override
    public int getField() {
        return field;
    }
    
    public int getSuperField() {
        return super.field;
    }
}

public class FieldAccess {
    public static void main(String[] args) {
        Super sup = new Sub(); // Upcast
        System.out.println("sup.field = " + sup.field + 
                          ", sup.getField() = " + sup.getField());
        Sub sub = new Sub();
        System.out.println("sub.field = " + sub.field + 
                          ", sub.getField() = " + sub.getField()
                          + ", sub.getSuperField() = " + sub.getSuperField())
    }
}

输出:

sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0

Sub 对象向上转型为 Super 引用时,任何属性访问都被编译器解析,因此不是多态的。在这个例子中,Super.fieldSub.field 被分配了不同的存储空间,因此,Sub 实际上包含了两个称为 field 的属性:它自己的和来自 Super 的。然而,在引用 Subfield 时,默认的 field 属性并不是 Super 版本的 field 属性。为了获取 Superfield 属性,需要显式地指明 super.field

尽管这看起来是个令人困惑的问题,实际上基本不会发生。首先,通常会将所有的属性都指明为 private,因此不能直接访问它们,只能通过方法来访问。此外,你可能也不会给基类属性和派生类属性起相同的名字,这样做会令人困惑。

如果一个方法是静态(static)的,它的行为就不具有多态性:

// polymorphism/StaticPolymorphism.java
// static methods are not polymorphic
class StaticSuper {
    public static String staticGet() {
        return "Base staticGet()";
    }
    
    public String dynamicGet() {
        return "Base dynamicGet()";
    }
}

class StaticSub extends StaticSuper {
    public static String staticGet() {
        return "Derived staticGet()";
    }
    @Override
    public String dynamicGet() {
        return "Derived dynamicGet()";
    }
}

public class StaticPolymorphism {
    public static void main(String[] args) {
        StaticSuper sup = new StaticSub(); // Upcast
        System.out.println(StaticSuper.staticGet());
        System.out.println(sup.dynamicGet());
    }
}

输出:

Base staticGet()
Derived dynamicGet()

静态的方法只与类关联,与单个的对象无关。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/868561.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Qt5开发视频播放器

一、播放器界面UI设计 控件对象名位置&#xff08;坐标点&#xff09;对象名称组件名称备注Widget(0, 0, 809, 572)WidgetQWidgetlabellabelQLabel播放窗口label_2label_2QLabelvoice_controlvoice_controlQSlider音量滑动条btn_openbtn_openQPushButton打开文件按钮label_4la…

在阿里云服务器上安装Microsoft SharePoint 2016流程

本教程阿里云百科分享如何在阿里云ECS上搭建Microsoft SharePoint 2016。Microsoft SharePoint是Microsoft SharePoint Portal Server的简称。SharePoint Portal Server是一个门户站点&#xff0c;使得企业能够开发出智能的门户站点。 目录 背景信息 步骤一&#xff1a;添加…

Spring循环依赖-实践三级缓存的再次理解

目录 Spring循环依赖流程图场景&#xff1a;**A 依赖B**; **B依赖A、C**; **C依赖A**A&#xff0c;B, C三个类的定义容器类测试输入如下 总结 Spring循环依赖流程图 很早之前阅读源码写过总结&#xff1a; https://github.com/doctording/spring-framework-5.1.3.RELEASE/blob…

8/12 题解

解题思路 贪心&#xff0c;小的搭配大的&#xff0c;和会最小 AC代码 #include <iostream> using namespace std;int main() {int n;cin >> n;int l 1;int r n;while(l < r){ cout << l << ;l;cout << r << ;--r;if(l r){cout …

java 9的新特性解读(3)

目录 语法改进&#xff1a;try语句 String存储结构变更 Motivation Description 那StringBuffer 和 StringBuilder 是否仍无动于衷呢&#xff1f; 集合工厂方法&#xff1a;快速创建只读集合 语法改进&#xff1a;try语句 Java 8 中&#xff0c;可以实现资源的自动…

JavaScript【瀑布流-页面布局、动态设置内容居中、动态设置图片位置、页面触底、上拉加载、页面布局、动态切换、页面布局】(十五)

目录 DOM实操-瀑布流-页面布局 瀑布流特点 DOM实操-瀑布流-动态设置内容居中 DOM实操-瀑布流-动态设置图片位置 DOM实操-瀑布流-页面触底 DOM实操-瀑布流-上拉加载 DOM实操-轮播图-页面布局 轮播图 轮播图特点 DOM实操-轮播图-动态切换 DOM实操-放大镜-页面布局 放大…

Leetcode-每日一题【剑指 Offer 25. 合并两个排序的链表】

题目 输入两个递增排序的链表&#xff0c;合并这两个链表并使新链表中的节点仍然是递增排序的。 示例1&#xff1a; 输入&#xff1a;1->2->4, 1->3->4输出&#xff1a;1->1->2->3->4->4 限制&#xff1a; 0 < 链表长度 < 1000 解题思路 1…

Java对象内存结构、对象在内存是什么样的

我们知道Java对象分配在堆内存中&#xff0c;一个对象在堆内存中的存储布局可以分为三部分&#xff1a; 对象头Header实例数据对齐填充 1. 对象头Header 对象头部分又包含两部分&#xff1a; 第一部分是用于存储对象自身运行时数据&#xff0c;例如哈希码、GC分代年龄等第二…

时序预测 | MATLAB基于扩散因子搜索的GRNN广义回归神经网络时间序列预测(多指标,多图)

时序预测 | MATLAB基于扩散因子搜索的GRNN广义回归神经网络时间序列预测(多指标,多图) 目录 时序预测 | MATLAB基于扩散因子搜索的GRNN广义回归神经网络时间序列预测(多指标,多图)效果一览基本介绍程序设计学习小结参考资料效果一览

Spring5 AOP 默认使用 JDK

这是博主在使用dubbo实现远程过程调用的时候遇到的问题&#xff1a; 我们如果在服务提供者类上加入Transactional事务控制注解后&#xff0c;服务就发布不成功了。原因是事务控制的底层原理是为服务提供者类创建代理对象&#xff0c;而默认情况下Spring是基于JDK动态代理方式创…

LeetCode面向运气之Javascript—第121题-买卖股票的最佳时机-97.77%

LeetCode第121题-买卖股票的最佳时机 题目要求 给定一个数组prices &#xff0c;它的第i个元素prices[i]表示一支给定股票第i天的价格。 你只能选择某一天买入这只股票&#xff0c;并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回…

Martin_DHCP_V3.0 (DHCP自动化泛洪攻击GUI)

Github>https://github.com/MartinxMax/Martin_DHCP_V3.0 首页 Martin_DHCP_V3.0 自动化DHCP洪泛攻击 Martin_DHCP_V3.0 使用方法 安装三方库 #python3 1.RunMe_Install_Packet.py 攻击路由器 #python3 Martin_DHCP_Attack.py 填写网卡 填写攻击次数 开始运行

语音芯片的型号有哪些?为什么强烈推荐使用flash型可擦写的

一、语音芯片的简介 语音芯片的型号有哪些&#xff1f;为什么强烈推荐使用flash型可擦写的芯片。这里我们简单描述一下如下常见类容&#xff1a; 1、他们都有什么特点&#xff1f;以及发展的历程简介 2、常见的语音芯片有哪些&#xff1f; 3、为什么推荐使用flash型可以重复…

Scractch3.0_Arduino_ESP32_学习随记_显示网络天气(二)

这里写目录标题 目的器材程序联系我们 目的 通过C02获取网络天气。并在屏上显示 器材 硬件: 齐护机器人C02 购买地址 软件: scratch3.0 下载地址:官网下载 程序 使用的是公开免费的API&#xff0c;对请求间隔和次数有限制&#xff0c;如果连续获取可能会被封IP&#xff…

机器学习讲解!(多种算法示例 全网最详细!)

机器学习 机器学习是人工智能的一个分支&#xff0c;它研究计算机如何通过自身的学习和经验来提高其性能&#xff0c;而不需要明确的被编程。机器学习算法可以从大量的数据中学习&#xff0c;并能根据这些数据做出预测或分类。机器学习目前已经被广泛应用于许多领域&#xff0…

轻量级 Spring Task 任务调度可视化管理

Spring Task/Spring Scheduler 傻傻分不清 首先做一下“名词解释”&#xff0c;分清楚这两者的区别&#xff1a; Spring Task Spring Task 是 Spring 框架自带的一个任务调度模块&#xff0c;提供了基本的任务调度功能。它是通过 Java 的 Timer 和 TimerTask 类来实现的&…

Qt扫盲-QWidget理论使用总结

QWidget理论使用总结 一、概述二、顶层 控件 和子 控件三、复合控件四、自定义控件和绘制五、大小提示和大小策略六、事件七、一组函数和属性八、QWidget样式表九、透明度和双缓冲十、创建半透明窗口 一、概述 widget 是用户界面的最小单位&#xff1a;它从window系统接收鼠标…

STM32F429IGT6使用CubeMX配置串口通信

1、硬件电路 2、设置RCC&#xff0c;选择高速外部时钟HSE,时钟设置为180MHz 3、配置USART1引脚 4、生成工程配置 5、部分代码 //重定向printf函数 int fputc(int ch, FILE *f) {HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);return ch; } /* USER CODE BE…

初学vue3时应该注意的几个问题

初学vue3时应该注意的几个问题 声明响应式 响应式数据的声明在vue2的时候很简单&#xff0c;在data中声明就行了。但现在可以使用多个方式。 reactive用于声明Object, Array, Map, Set; ref用于声明String, Number, Boolean 使用reactive来声明基础数据类型&#xff08;Str…

华为OD机试真题 Java 实现【寻找相同子串】【2023 B卷 100分】,附详细解题思路

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#…