九、多态(2)

本章概要

  • 构造器和多态
    • 构造器调用顺序
    • 继承和清理
    • 构造器内部多态方法的行为
  • 协变返回类型
  • 使用继承设计
    • 替代 vs 扩展
    • 向下转型与运行时类型信息

构造器和多态

通常,构造器不同于其他类型的方法。在涉及多态时也是如此。尽管构造器不具有多态性(事实上人们会把它看作是隐式声明的静态方法),但是理解构造器在复杂层次结构中运作多态还是非常重要的。理解这个可以帮助你避免一些不愉快的困扰。

构造器调用顺序

在“初始化和清理”和“复用”两章中已经简单地介绍过构造器的调用顺序,但那时还没有介绍多态。

在派生类的构造过程中总会调用基类的构造器。初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。这么做是有意义的,因为构造器有着特殊的任务:检查对象是否被正确地构造。由于属性通常声明为 private,你必须假定派生类只能访问自己的成员而不能访问基类的成员。只有基类的构造器拥有恰当的知识和权限来初始化自身的元素。因此,必须得调用所有构造器;否则就不能构造完整的对象。这就是为什么编译器会强制调用每个派生类中的构造器的原因。如果在派生类的构造器主体中没有显式地调用基类构造器,编译器就会默默地调用无参构造器。如果没有无参构造器,编译器就会报错(当类中不含构造器时,编译器会自动合成一个无参构造器)。

下面的例子展示了组合、继承和多态在构建顺序上的作用:

// polymorphism/Sandwich.java
// Order of constructor calls
// {java polymorphism.Sandwich}
package polymorphism;class Meal {Meal() {System.out.println("Meal()");}
}class Bread {Bread() {System.out.println("Bread()");}
}class Cheese {Cheese() {System.out.println("Cheese()");}
}class Lettuce {Lettuce() {System.out.println("Lettuce()");}
}class Lunch extends Meal {Lunch() {System.out.println("Lunch()");}
}class PortableLunch extends Lunch {PortableLunch() {System.out.println("PortableLunch()");}
}public class Sandwich extends PortableLunch {private Bread b = new Bread();private Cheese c = new Cheese();private Lettuce l = new Lettuce();public Sandwich() {System.out.println("Sandwich()");}public static void main(String[] args) {new Sandwich();}
}

输出:

Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()

这个例子用其他类创建了一个复杂的类。每个类都在构造器中声明自己。重要的类是 Sandwich,它反映了三层继承(如果算上 Object 的话,就是四层),包含了三个成员对象。

从创建 Sandwich 对象的输出中可以看出对象的构造器调用顺序如下:

  1. 基类构造器被调用。这个步骤被递归地重复,这样一来类层次的顶级父类会被最先构造,然后是它的派生类,以此类推,直到最底层的派生类。
  2. 按声明顺序初始化成员。
  3. 调用派生类构造器的方法体。

构造器的调用顺序很重要。当使用继承时,就已经知道了基类的一切,并可以访问基类中任意 publicprotected 的成员。这意味着在派生类中可以假定所有的基类成员都是有效的。在一个标准方法中,构造动作已经发生过,对象其他部分的所有成员都已经创建好。

在构造器中必须确保所有的成员都已经构建完。唯一能保证这点的方法就是首先调用基类的构造器。接着,在派生类的构造器中,所有你可以访问的基类成员都已经初始化。另一个在构造器中能知道所有成员都是有效的理由是:无论何时有可能的话,你应该在所有成员对象(通过组合将对象置于类中)定义处初始化它们(例如,例子中的 bcl)。如果遵循这条实践,就可以帮助确保所有的基类成员和当前对象的成员对象都已经初始化。

不幸的是,这不能处理所有情况,在下一节会看到。

继承和清理

在使用组合和继承创建新类时,大部分时候你无需关心清理。子对象通常会留给垃圾收集器处理。如果你存在清理问题,那么必须用心地为新类创建一个 dispose() 方法(这里用的是我选择的名称,你可以使用更好的名称)。由于继承,如果有其他特殊的清理工作的话,就必须在派生类中重写 dispose() 方法。当重写 dispose() 方法时,记得调用基类的 dispose() 方法,否则基类的清理工作不会发生:

// polymorphism/Frog.java
// Cleanup and inheritance
// {java polymorphism.Frog}
package polymorphism;class Characteristic {private String s;Characteristic(String s) {this.s = s;System.out.println("Creating Characteristic " + s);}protected void dispose() {System.out.println("disposing Characteristic " + s);}
}class Description {private String s;Description(String s) {this.s = s;System.out.println("Creating Description " + s);}protected void dispose() {System.out.println("disposing Description " + s);}
}class LivingCreature {private Characteristic p = new Characteristic("is alive");private Description t = new Description("Basic Living Creature");LivingCreature() {System.out.println("LivingCreature()");}protected void dispose() {System.out.println("LivingCreature dispose");t.dispose();p.dispose();}
}class Animal extends LivingCreature {private Characteristic p = new Characteristic("has heart");private Description t = new Description("Animal not Vegetable");Animal() {System.out.println("Animal()");}@Overrideprotected void dispose() {System.out.println("Animal dispose");t.dispose();p.dispose();super.dispose();}
}class Amphibian extends Animal {private Characteristic p = new Characteristic("can live in water");private Description t = new Description("Both water and land");Amphibian() {System.out.println("Amphibian()");}@Overrideprotected void dispose() {System.out.println("Amphibian dispose");t.dispose();p.dispose();super.dispose();}
}public class Frog extends Amphibian {private Characteristic p = new Characteristic("Croaks");private Description t = new Description("Eats Bugs");public Frog() {System.out.println("Frog()");}@Overrideprotected void dispose() {System.out.println("Frog dispose");t.dispose();p.dispose();super.dispose();}public static void main(String[] args) {Frog frog = new Frog();System.out.println("Bye!");frog.dispose();}
}

输出:

在这里插入图片描述

层级结构中的每个类都有 CharacteristicDescription 两个类型的成员对象,它们必须得被销毁。销毁的顺序应该与初始化的顺序相反,以防一个对象依赖另一个对象。对于属性来说,就意味着与声明的顺序相反(因为属性是按照声明顺序初始化的)。对于基类(遵循 C++ 析构函数的形式),首先进行派生类的清理工作,然后才是基类的清理。这是因为派生类的清理可能调用基类的一些方法,所以基类组件这时得存活,不能过早地被销毁。输出显示了,Frog 对象的所有部分都是按照创建的逆序销毁的。

尽管通常不必进行清理工作,但万一需要时,就得谨慎小心地执行。

Frog 对象拥有自己的成员对象,它创建了这些成员对象,并且知道它们能存活多久,所以它知道何时调用 dispose() 方法。然而,一旦某个成员对象被其它一个或多个对象共享时,问题就变得复杂了,不能只是简单地调用 dispose()。这里,也许就必须使用_引用计数_来跟踪仍然访问着共享对象的对象数量,如下:

// polymorphism/ReferenceCounting.java
// Cleaning up shared member objects
class Shared {private int refcount = 0;private static long counter = 0;private final long id = counter++;Shared() {System.out.println("Creating " + this);}public void addRef() {refcount++;}protected void dispose() {if (--refcount == 0) {System.out.println("Disposing " + this);}}@Overridepublic String toString() {return "Shared " + id;}
}class Composing {private Shared shared;private static long counter = 0;private final long id = counter++;Composing(Shared shared) {System.out.println("Creating " + this);this.shared = shared;this.shared.addRef();}protected void dispose() {System.out.println("disposing " + this);shared.dispose();}@Overridepublic String toString() {return "Composing " + id;}
}public class ReferenceCounting {public static void main(String[] args) {Shared shared = new Shared();Composing[] composing = {new Composing(shared),new Composing(shared),new Composing(shared),new Composing(shared),new Composing(shared),};for (Composing c: composing) {c.dispose();}}
}

输出:

在这里插入图片描述

static long counter 跟踪所创建的 Shared 实例数量,还提供了 id 的值。counter 的类型是 long 而不是 int,以防溢出(这只是个良好实践,对于本书的所有示例,counter 不会溢出)。idfinal 的,因为它的值在初始化时确定后不应该变化。

在将一个 shared 对象附着在类上时,必须记住调用 addRef(),而 dispose() 方法会跟踪引用数,以确定在何时真正地执行清理工作。使用这种技巧需要加倍细心,但是如果需要清理正在共享的对象,你没有太多选择。

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

构造器调用的层次结构带来了一个困境。如果在构造器中调用了正在构造的对象的动态绑定方法,会发生什么呢?

在普通的方法中,动态绑定的调用是在运行时解析的,因为对象不知道它属于方法所在的类还是类的派生类。

如果在构造器中调用了动态绑定方法,就会用到那个方法的重写定义。然而,调用的结果难以预料因为被重写的方法在对象被完全构造出来之前已经被调用,这使得一些 bug 很隐蔽,难以发现。

从概念上讲,构造器的工作就是创建对象(这并非是平常的工作)。在构造器内部,整个对象可能只是部分形成——只知道基类对象已经初始化。如果构造器只是构造对象过程中的一个步骤,且构造的对象所属的类是从构造器所属的类派生出的,那么派生部分在当前构造器被调用时还没有初始化。然而,一个动态绑定的方法调用向外深入到继承层次结构中,它可以调用派生类的方法。如果你在构造器中这么做,就可能调用一个方法,该方法操纵的成员可能还没有初始化——这肯定会带来灾难。

下面例子展示了这个问题:

// polymorphism/PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect
class Glyph {void draw() {System.out.println("Glyph.draw()");}Glyph() {System.out.println("Glyph() before draw()");draw();System.out.println("Glyph() after draw()");}
}class RoundGlyph extends Glyph {private int radius = 1;RoundGlyph(int r) {radius = r;System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);}@Overridevoid draw() {System.out.println("RoundGlyph.draw(), radius = " + radius);}
}public class PolyConstructors {public static void main(String[] args) {new RoundGlyph(5);}
}

输出:

Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5

Glyphdraw() 被设计为可重写,在 RoundGlyph 这个方法被重写。但是 Glyph 的构造器里调用了这个方法,结果调用了 RoundGlyphdraw() 方法,这看起来正是我们的目的。输出结果表明,当 Glyph 构造器调用了 draw() 时,radius 的值不是默认初始值 1 而是 0。这可能会导致在屏幕上只画了一个点或干脆什么都不画,于是我们只能干瞪眼,试图找到程序不工作的原因。

前一小节描述的初始化顺序并不十分完整,而这正是解决谜团的关键所在。初始化的实际过程是:

  1. 在所有事发生前,分配给对象的存储空间会被初始化为二进制 0。
  2. 如前所述调用基类构造器。此时调用重写后的 draw() 方法(是的,在调用 RoundGraph 构造器之前调用),由步骤 1 可知,radius 的值为 0。
  3. 按声明顺序初始化成员。
  4. 最终调用派生类的构造器。

这么做有个优点:所有事物至少初始化为 0(或某些特殊数据类型与 0 等价的值),而不是仅仅留作垃圾。这包括了通过组合嵌入类中的对象引用,被赋予 null。如果忘记初始化该引用,就会在运行时出现异常。观察输出结果,就会发现所有事物都是 0。

另一方面,应该震惊于输出结果。逻辑方面我们已经做得非常完美,然而行为仍不可思议的错了,编译器也没有报错(C++ 在这种情况下会产生更加合理的行为)。像这样的 bug 很容易被忽略,需要花很长时间才能发现。

因此,编写构造器有一条良好规范:做尽量少的事让对象进入良好状态。如果有可能的话,尽量不要调用类中的任何方法。在基类的构造器中能安全调用的只有基类的 final 方法(这也适用于可被看作是 finalprivate 方法)。这些方法不能被重写,因此不会产生意想不到的结果。你可能无法永远遵循这条规范,但应该朝着它努力。

协变返回类型

Java 5 中引入了协变返回类型,这表示派生类的被重写方法可以返回基类方法返回类型的派生类型:

// polymorphism/CovariantReturn.java
class Grain {@Overridepublic String toString() {return "Grain";}
}class Wheat extends Grain {@Overridepublic String toString() {return "Wheat";}
}class Mill {Grain process() {return new Grain();}
}class WheatMill extends Mill {@OverrideWheat process() {return new Wheat();}
}public class CovariantReturn {public static void main(String[] args) {Mill m = new Mill();Grain g = m.process();System.out.println(g);m = new WheatMill();g = m.process();System.out.println(g);}
}

输出:

Grain
Wheat

关键区别在于 Java 5 之前的版本强制要求被重写的 process() 方法必须返回 Grain 而不是 Wheat,即使 Wheat 派生自 Grain,因而也应该是一种合法的返回类型。协变返回类型允许返回更具体的 Wheat 类型。

使用继承设计

学习过多态之后,一切看似都可以被继承,因为多态是如此巧妙的工具。这会给设计带来负担。事实上,如果利用已有类创建新类首先选择继承的话,事情会变得莫名的复杂。

更好的方法是首先选择组合,特别是不知道该使用哪种方法时。组合不会强制设计是继承层次结构,而且组合更加灵活,因为可以动态地选择类型(因而选择相应的行为),而继承要求必须在编译时知道确切类型。下面例子说明了这点:

// polymorphism/Transmogrify.java
// Dynamically changing the behavior of an object
// via composition (the "State" design pattern)
class Actor {public void act() {}
}class HappyActor extends Actor {@Overridepublic void act() {System.out.println("HappyActor");}
}class SadActor extends Actor {@Overridepublic 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

Stage 对象中包含了 Actor 引用,该引用被初始化为指向一个 HappyActor 对象,这意味着 performPlay() 会产生一个特殊行为。但是既然引用可以在运行时与其他不同的对象绑定,那么它就可以被替换成对 SadActor 的引用,performPlay() 的行为随之改变。这样你就获得了运行时的动态灵活性(这被称为状态模式)。与之相反,我们无法在运行时才决定继承不同的对象;那在编译时就完全决定好了。

有一条通用准则:使用继承表达行为的差异,使用属性表达状态的变化。在上个例子中,两者都用到了。通过继承得到的两个不同类在 act() 方法中表达了不同的行为,Stage 通过组合使自己的状态发生变化。这里状态的改变产生了行为的改变。

替代 vs 扩展

采用“纯粹”的方式创建继承层次结构看上去是最清晰的方法。即只有基类的方法才能在派生类中被重写,就像下图这样:

在这里插入图片描述

这被称作纯粹的“is - a"关系,因为类的接口已经确定了它是什么。继承可以确保任何派生类都拥有基类的接口,绝对不会少。如果按图上这么做,派生类将只拥有基类的接口。

纯粹的替代意味着派生类可以完美地替代基类,当使用它们时,完全不需要知道这些子类的信息。也就是说,基类可以接收任意发送给派生类的消息,因为它们具有完全相同的接口。只需将派生类向上转型,不要关注对象的具体类型。所有一切都可以通过多态处理。

在这里插入图片描述

按这种方式思考,似乎只有纯粹的“is - a”关系才是唯一明智的做法,其他任何设计只会导致混乱且注定失败。这其实也是个陷阱。一旦按这种方式开始思考,就会转而发现继承扩展接口(遗憾的是,extends 关键字似乎怂恿我们这么做)才是解决特定问题的完美方案。这可以称为“is - like - a” 关系,因为派生类就像是基类——它有着相同的基本接口,但还具有需要额外方法实现的其他特性:

在这里插入图片描述

虽然这是一种有用且明智的方法(依赖具体情况),但是也存在缺点。派生类中接口的扩展部分在基类中不存在(不能通过基类访问到这些扩展接口),因此一旦向上转型,就不能通过基类调用这些新方法:

在这里插入图片描述

如果不向上转型,就不会遇到这个问题。但是通常情况下,我们需要重新查明对象的确切类型,从而能够访问该类型中的扩展方法。下一节说明如何做到这点。

向下转型与运行时类型信息

由于向上转型(在继承层次中向上移动)会丢失具体的类型信息,那么为了重新获取类型信息,就需要在继承层次中向下移动,使用_向下转型_。

向上转型永远是安全的,因为基类不会具有比派生类更多的接口。因此,每条发送给基类接口的消息都能被接收。但是对于向下转型,你无法知道一个形状是圆,它有可能是三角形、正方形或其他一些类型。

为了解决这个问题,必须得有某种方法确保向下转型是正确的,防止意外转型到一个错误类型,进而发送对象无法接收的消息。这么做是不安全的。

在某些语言中(如 C++),必须执行一个特殊的操作来获得安全的向下转型,但是在 Java 中,每次转型都会被检查!所以即使只是进行一次普通的加括号形式的类型转换,在运行时这个转换仍会被检查,以确保它的确是希望的那种类型。如果不是,就会得到 ClassCastException (类转型异常)。这种在运行时检查类型的行为称作运行时类型信息。下面例子展示了 RTTI 的行为:

// polymorphism/RTTI.java
// Downcasting & Runtime type information (RTTI)
// {ThrowsException}
class Useful {public void f() {}public void g() {}
}class MoreUseful extends Useful {@Overridepublic void f() {}@Overridepublic void g() {}public void u() {}public void v() {}public void w() {}
}public class RTTI {public static void main(String[] args) {Useful[] x = {new Useful(),new MoreUseful()};x[0].f();x[1].g();// Compile time: method not found in Useful://- x[1].u();((MoreUseful) x[1]).u(); // Downcast/RTTI((MoreUseful) x[0]).u(); // Exception thrown}
}

输出:

Exception in thread "main"
java.lang.ClassCastException: Useful cannot be cast to
MoreUseful
at RTTI.main

正如前面类图所示,MoreUseful 扩展了 Useful 的接口。而 MoreUseful 也继承了 Useful,所以它可以向上转型为 Useful。在 main() 方法中可以看到这种情况的发生。因为两个对象都是 Useful 类型,所以对它们都可以调用 f()g() 方法。如果试图调用 u() 方法(只存在于 MoreUseful 中),就会得到编译时错误信息。

为了访问 MoreUseful 对象的扩展接口,就得尝试向下转型。如果转型为正确的类型,就转型成功。否则,就会得到 ClassCastException 异常。你不必为这个异常编写任何特殊代码,因为它指出了程序员在程序的任何地方都可能犯的错误。{ThrowsException} 注释标签告知本书的构建系统:在运行程序时,预期抛出一个异常。

RTTI 不仅仅包括简单的转型。例如,它还提供了一种方法,使你可以在试图向下转型前检查所要处理的类型。“类型信息”一章中会详细阐述运行时类型信息的方方面面。

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

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

相关文章

【JavaScript】new 的原理以及实现

网道 - new 命令的原理 使用new命令时,它后面的函数依次执行下面的步骤。 创建一个空对象,作为将要返回的对象实例。将这个空对象的原型,指向构造函数的prototype属性。将这个空对象赋值给函数内部的this关键字。如果构造函数返回了一个对象…

在Visual Studio上,使用OpenCV实现人脸识别

1. 环境与说明 本文介绍了如何在Visual Studio上,使用OpenCV来实现人脸识别的功能 环境说明 : 操作系统 : windows 10 64位Visual Studio版本 : Visual Studio Community 2022 (社区版)OpenCV版本 : OpenCV-4.8.0 (2023年7月最新版) 实现效果如图所示&#xff0…

Linux命令200例:adduser用于创建新用户

🏆作者简介,黑夜开发者,全栈领域新星创作者✌。CSDN专家博主,阿里云社区专家博主,2023年6月csdn上海赛道top4。 🏆数年电商行业从业经验,历任核心研发工程师,项目技术负责人。 &…

代理模式【Proxy Pattern】

什么是代理模式呢?我很忙,忙的没空理你,那你要找我呢就先找我的代理人吧,那代理人总要知道 被代理人能做哪些事情不能做哪些事情吧,那就是两个人具备同一个接口,代理人虽然不能干活,但是被 代…

解决 Mac 上使用 Electron Updater 更新 App 不成功的问题!!!

文章目录 1. 现象2. 分析并如何解决3. 后续 1. 现象 在Mac电脑上,使用Electron Updater对程序进行更新,但是一直不成功,也不报错。具体表现是这样的:当前我的程序版本是3.11版本,点击更新之后,也下载了&am…

微PE工具箱实现U盘重装Windows系统

教程来源 U盘重装Windows系统(微PE工具箱)_哔哩哔哩_bilibili 加上自己的一丢丢理解,如果你觉得长视频看了犯困,不如看看我的理解文章说不定能够帮助到你 准备工作 到这个网站使用迅雷下载免费无插件的官方镜像MSDN, 我告诉你…

JVM笔记 —— 出现内存溢出错误时时如何排查

一、出现内存溢出的几种情况 内存溢出错误分为StackOverflowError和OutOfMemoryError,前者是栈中出现溢出,后者一般是堆或方法区出现溢出,简称OOM 1. 栈溢出 StackOverflowError 栈溢出一般都是因为没有正确的结束递归导致的,无…

python安装第三方包时报错:...\lib\site-packages\pip\_vendor\urllib3\response.py...

安装redis第三方包: pip install redis报错现象: 解决方法:使用以下命令可成功安装 pip install redis -i http://pypi.douban.com/simple --trusted-host pypi.douban.com

关于网络入侵检测领域使用Spark/Flink等计算框架做分布式

关于网络入侵检测领域使用Spark/Flink等计算框架做分布式 0、引言1 基于LightGBM的网络入侵检测研究2 基于互信息法的智能化运维系统入侵检测Spark实现3 基于Spark的车联网分布式组合深度学习入侵检测方法4 基于Flink的分布式在线集成学习框架研究5 基于Flink的分布式并行逻辑回…

【Spring】-Spring的IoC和DI

作者:学Java的冬瓜 博客主页:☀冬瓜的主页🌙 专栏:【Framework】 主要内容:什么是spring?IoC容器是什么?如何使代码解耦合?IoC的核心原理,IoC的优点。依赖注入/对象装配/…

Linux下在qtcreator中创建qt程序

目录 1、新建项目 2、单工程项目创建 3、多工程项目创建 4、添加子工程(基于多工程目录结构) 5、 .pro文件 1、新建项目 切换到“编辑”界面,点击菜单栏中的“文件”-“新建文件或项目” 2、单工程项目创建 只有一个工程的项目&#…

Axure RP移动端高保真CRM办公客户管理系统原型模板及元件库

Axure RP移动端高保真CRM办公客户管理系统原型模板及元件库,一套典型的移动端办公工具型APP Axure RP原型模板,可根据实际的产品需求进行扩展,也可以作为移动端原型设计的参考案例。为提升本作品参考价值,在模板设计过程中尽量追求…

chatGPT应用于房地产行业

作为 2023 年的房地产专业人士,您无疑认识到技术对行业的重大影响。近年来,一项技术进步席卷了世界——人工智能。人工智能彻底改变了房地产业务的各个方面,从简化管理任务到增强客户互动。 在本文中,我们将探讨几种巧妙的人工智…

zabbix自动注册服务器以及部署代理服务器

文章目录 Zabbix自动注册服务器及部署代理服务器一.zabbix自动注册1.什么是自动注册2.环境准备3.zabbix客户端配置4.在 Web 页面配置自动注册5.验证自动注册 二.部署 zabbix 代理服务器1.分布式监控的作用:2.环境部署3.代理服务器配置4.客户端配置5.web页面配置5.1 …

分类预测 | MATLAB实现GWO-BiLSTM-Attention多输入分类预测

分类预测 | MATLAB实现GWO-BiLSTM-Attention多输入分类预测 目录 分类预测 | MATLAB实现GWO-BiLSTM-Attention多输入分类预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.GWO-BiLSTM-Attention 数据分类预测程序 2.代码说明:基于灰狼优化算法&#xff08…

微信小程序(原生)搜索功能实现

一、效果图 二、代码 wxml <van-searchvalue"{{ keyword }}"shape"round"background"#000"placeholder"请输入关键词"use-action-slotbind:change"onChange"bind:search"onSearch"bind:clear"onClear&q…

实践-CNN卷积层

实践-CNN卷积层 1 卷积层构造2 整体流程3 BatchNormalization效果4 参数对比5 测试效果 1 卷积层构造 2 整体流程 根据网络结构来写就可以了。 池化 拉平 训练一个网络需要2-3天的时间。用经典网络来&#xff0c;一些细节没有必要去扣。 损失函数&#xff1a; fit模型&…

运维监控学习笔记1

1、监控对象&#xff1a; 1、监控对象的理解&#xff1b;CPU是怎么工作的&#xff1b; 2、监控对象的指标&#xff1a;CPU使用率&#xff1b;上下文切换&#xff1b; 3、确定性能基准线&#xff1a;CPU负载多少才算高&#xff1b; 2、监控范围&#xff1a; 1、硬件监控&#x…

线性扫描寄存器分配算法介绍

线性扫描寄存器分配 文章目录 线性扫描寄存器分配1. 算法介绍2. 相关概念3. 算法的实现3.1 伪代码3.2 图示 参考文献 论文地址&#xff1a; Linear Scan Register Allocation ​ 我们描述了一种称为线性扫描的快速全局寄存器分配的新算法。该算法不基于图形着色&#xff0c;而…

echarts3d柱状图

//画立方体三个面 const CubeLeft echarts.graphic.extendShape({shape: {x: 0,y: 0,width: 9.5, //柱状图宽zWidth: 4, //阴影折角宽zHeight: 3, //阴影折角高},buildPath: function (ctx, shape) {const api shape.api;const xAxisPoint api.coord([shape.xValue, 0]);con…