九、多态(1)

本章概要

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

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

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

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

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

向上转型回顾

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

同样你也在下面的音乐乐器例子中发现了问题。即然几个例子都要演奏乐符(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:@Overridepublic 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 {@Overridepublic void play(Note n) {System.out.println("Stringed.play() " + n);}
}class Brass extends Instrument {@Overridepublic 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 upcastingtune(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 {@Overridepublic void draw() {System.out.println("Circle.draw()");}@Overridepublic void erase() {System.out.println("Circle.erase()");}
}// polymorphism/shape/Square.java
package polymorphism.shape;public class Square extends Shape {@Overridepublic void draw() {System.out.println("Square.draw()");}@Overridepublic void erase() {System.out.println("Square.erase()");}}// polymorphism/shape/Triangle.java
package polymorphism.shape;public class Triangle extends Shape {@Overridepublic void draw() {System.out.println("Triangle.draw()");}@Overridepublic 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 {@Overridevoid play(Note n) {System.out.println("Wind.play() " + n);}@OverrideString what() {return "Wind";}@Overridevoid adjust() {System.out.println("Adjusting Wind");}
}class Percussion extends Instrument {@Overridevoid play(Note n) {System.out.println("Percussion.play() " + n);}@OverrideString what() {return "Percussion";}@Overridevoid adjust() {System.out.println("Adjusting Percussion");}
}class Stringed extends Instrument {@Overridevoid play(Note n) {System.out.println("Stringed.play() " + n);} @OverrideString what() {return "Stringed";}@Overridevoid adjust() {System.out.println("Adjusting Stringed");}
}class Brass extends Wind {@Overridevoid play(Note n) {System.out.println("Brass.play() " + n);}@Overridevoid adjust() {System.out.println("Adjusting Brass");}
}class Woodwind extends Wind {@Overridevoid play(Note n) {System.out.println("Woodwind.play() " + n);}@OverrideString 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 {@Overridepublic 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;@Overridepublic int getField() {return field;}public int getSuperField() {return super.field;}
}public class FieldAccess {public static void main(String[] args) {Super sup = new Sub(); // UpcastSystem.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()";}@Overridepublic String dynamicGet() {return "Derived dynamicGet()";}
}public class StaticPolymorphism {public static void main(String[] args) {StaticSuper sup = new StaticSub(); // UpcastSystem.out.println(StaticSuper.staticGet());System.out.println(sup.dynamicGet());}
}

输出:

Base staticGet()
Derived dynamicGet()

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

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

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

相关文章

C++_模板初阶

在面向对象中&#xff0c;我们可以使用重载来实现多态。 但是问题在于&#xff0c;重载的函数仅仅是类型不同&#xff0c;代码复用率比较低&#xff0c;只要有新的类型出现时&#xff0c;就要增加对应的函数&#xff1b;另一方面它的代码可维护性比较低&#xff0c;一个出错可…

java实现文件的下载

系统日志的获取不可能每次都登录服务器&#xff0c;所以在页面上能够下载系统运行的日志是必须的 如何来实现日志的下载&#xff0c;这样的一个功能 前端我们用到的是window.open(...)这样可以发送一个get请求到后台 后台接收到get请求之后&#xff0c;如何实现对文件的下载 R…

2023/08/13_____JMM JAVA Memory Model JAVA内存模型

JMM JAVA Memory Model java内存模型 作用&#xff1a;缓存一致性协议&#xff0c;用于定义数据读写的规则&#xff08;遵守&#xff0c;找到这个规则&#xff09; JMM定义了线程2工作内存和主内存之间的抽象关系&#xff1a;线程之间的共享变量存储在主内存&#xff08;main …

提升效率!Go语言开发者不可错过的必备工具集合!

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to Golang Language.✨✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1…

途乐证券-最准确的KDJ改良指标?

KDJ目标是技术剖析的一种重要目标之一&#xff0c;它是利用随机目标&#xff08;%R&#xff09;发展而来的&#xff0c;是一种反映商场超买和超卖状况的买卖目标。KDJ目标由快线&#xff08;K线&#xff09;、慢线&#xff08;D线&#xff09;和随机值&#xff08;J线&#xff…

MySQL多表查询

1.创建student和score表 创建score表 2.为student表和score表增加记录 向student表插入记录的INSERT语句如下&#xff1a; 向score表插入记录的INSERT语句如下&#xff1a; 1.查询student表的所有记录 2.查询student表的第2条到4条记录 3.从student表查询所有学生的学号&#…

mousedown拖拽功能(vue3+ts)

因为项目有rem适配&#xff0c;使用第三方插件无法处理适配问题&#xff0c;所有只能自己写拖拽功能了 拖拽一般都会想到按下&#xff0c;移动&#xff0c;放开&#xff0c;但是本人亲测&#xff0c;就在div绑定一个按下事件就行了&#xff08;在事件里面写另外两个事件&#x…

爬虫ip池越大越好吗?

作为一名资深的程序员&#xff0c;今天我要给大家分享一些关于爬虫ip池的知识。关于ip代理池的问题&#xff0c;答案是肯定的&#xff0c;池子越大越好。下面跟我一起来盘点一下ip池大的好处吧&#xff01; 1、提高稳定性 爬虫ip池越大&#xff0c;意味着拥有更多可用的爬虫ip…

「C/C++」C/C++搭建程序框架

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「C/C」C/C程序设计&#x1f4da;全部专栏「UG/NX」NX二次开发「UG/NX」BlockUI集合「VS」Visual Studio「QT」QT5程序设计「C/C」C/C程序设计「Win」Windows程序设计「DSA」数据结构与算法「File」数据文件格式 目录 1. 分离职…

Flume原理剖析

一、介绍 Flume是一个高可用、高可靠&#xff0c;分布式的海量日志采集、聚合和传输的系统。Flume支持在日志系统中定制各类数据发送方&#xff0c;用于收集数据&#xff1b;同时&#xff0c;Flume提供对数据进行简单处理&#xff0c;并写到各种数据接受方&#xff08;可定制&…

使用阿里云服务器搭建Discuz论坛网站教程基于CentOS系统

阿里云百科分享使用阿里云服务器建站教程&#xff0c;本文是搭建Discuz论坛&#xff0c;Discuz!是一款通用的社区论坛软件系统&#xff0c;它采用PHP和MySQL组合的基础架构&#xff0c;为您提供高效的论坛解决方案。本文介绍如何在CentOS 7操作系统的ECS实例上搭建Discuz! X3.4…

Nginx 安装与部署

文章和代码已经归档至【Github仓库&#xff1a;https://github.com/timerring/front-end-tutorial 】或者公众号【AIShareLab】回复 nginx 也可获取。 文章目录 虚拟机安装CentOS7.4Linux配置配置上网配置静态ip Nginx的安装版本区别备份克隆 安装编译安装报错解决 启动Nginx防…

分布式 - 消息队列Kafka:Kafka生产者发送消息的方式

文章目录 1. Kafka 生产者2. kafaka 命令行操作3. kafka 生产者发送消息流程4. Kafka 生产者的创建5. Kafka 生产者发送消息1. 发送即忘记2. 同步发送3. 异步发送 6. Kafka 消息对象 ProducerRecord 1. Kafka 生产者 不管是把Kafka作为消息队列、消息总线还是数据存储平台&…

wpf控件上移下移,调整子集控件显示顺序

页面代码: <!-- 导出A2,自定义导出设置列,添加时间:2023-8-9 14:14:18,作者:whl; --><Window x:Class="WpfSnqkGasAnalysis.WindowGasExportA2"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http:/…

git远程仓库的创建及使用

1.仓库的概念&#xff1a; 1.1 本地仓库&#xff1a; 了解远程仓库前我们先了解一下本地仓库&#xff0c;本地仓库开发人员在完成部分代码的编写之后&#xff0c;可以将这一部分的代码做一个提交。这个提交完全就是一个新的版本提交&#xff0c;当然这个提交动作是在开发者的电…

B100-技能提升-线程池分布式锁

目录 线程池什么是线程池&#xff1f;为什么用线程池?线程池原理常见四种线程池和自定义线程池 线程池 什么是线程池&#xff1f; 池化技术 为什么用线程池? 1 由于设置最大线程数&#xff0c;防止线程过多而导致系统崩溃。 2 线程复用&#xff0c;不需要频繁创建或销毁…

Linux安装Zookeeper

1、Zookeeper简介 ZooKeeper是一个分布式的&#xff0c;开放源码的分布式应用程序协调服务&#xff0c;是Google的Chubby一个开源的实现&#xff0c;是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件&#xff0c;提供的功能包括&#xff1a;配置维护、域…

基于灰狼优化(GWO)、帝国竞争算法(ICA)和粒子群优化(PSO)对梯度下降法训练的神经网络的权值进行了改进(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

环保行业如何开发废品回收微信小程序

废品回收是近年来受到越来越多人关注的环保行动。为了推动废品回收的普及和方便&#xff0c;我们可以利用微信小程序进行制作&#xff0c;方便人们随时随地参与废品回收。 首先&#xff0c;我们需要注册并登录乔拓云账号&#xff0c;并进入后台。乔拓云是一个提供微信小程序制作…

数据结构(一):顺序表详解

在正式介绍顺序表之前&#xff0c;我们有必要先了解一个名词&#xff1a;线性表。 线性表&#xff1a; 线性表是&#xff0c;具有n个相同特性的数据元素的有限序列。常见的线性表&#xff1a;顺序表、链表、栈、队列、数组、字符串... 线性表在逻辑上是线性结构&#xff0c;但…