初识Java 7-1 多态

目录

向上转型

难点

方法调用绑定

产生正确的行为

可扩展性

陷阱:“重写”private方法

陷阱:字段与静态方法

构造器和多态

构造器的调用顺序

继承和清理

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

协变返回类型

使用继承的设计

替换和扩展

向下转型和反射


本笔记参考自: 《On Java 中文版》


        多态,是面向对象编程语言的一个基本特性,也被称为动态绑定后期绑定运行时绑定。这一特性分离了做什么(接口)和怎么做(实现)。到目前为止,已经可以总结:

  • 封装,通过组合特征和行为来创建新的数据类型;
  • 隐藏实现,通过把实现细节设为private来分离接口和实现。

        而多态则是根据类型来进行解耦的。多态方法调用允许一种类型表现出和另一种相似类型之间的区别,而只要求它们都继承相同的基类。

向上转型

        获取对象引用并把其当作基类型的引用称为向上转型,这是因为继承层次结构是以基类在顶部的方式进行绘制的。

        以乐器为例,先创建一个枚举:

package music;public enum Note {MIDDLE_C, C_SHARP, B_FLAT;
}

        已知,管乐器(Wind)是一种乐器(Instrument):

package music;public class Instrument {public void play(Note n) {System.out.println("这是方法Instrument.play");}
}

        那么,Wind就可以继承Instrument

package music;public class Wind extends Instrument { // Wind方法是一种Instrument,它们有相同的接口@Overridepublic void play(Note n) {System.getProperty("这是方法Wind.play() " + n);}
}

        现在就可以使用这些子类和基类了:

package 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); // 向上转型}
}

        虽然Music.tune()方法接收的是一个Instrument的引用,但是也可以接收任何继承了Instrument的类。在上述程序中,Music.tune()就接收了一个Wind类。程序执行的结果是:

        在上述程序中,将Wind引用传递给tune()方法不需要任何强制类型转换。因为Instrument中的接口必定存在于Wind中,Wind向上转型是缩小了自己的接口。

忘记对象类型

        在向上转型的过程中,会出现如上这种忘记了对象类型的情况。

        如果反过来,向上转型无法发生的话,我们就得为系统内每种类型的乐器(Instrument)编写一个tune()方法,这就意味着更多的编程工作,并且在进行重载的管理时,会遇到不少的困难。

package 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(Stringed i) {i.play(Note.MIDDLE_C);}public static void tune(Brass i) {i.play(Note.MIDDLE_C);}public static void main(String[] args) {Stringed violin = new Stringed();Brass frenchHorn = new Brass();tune(violin);tune(frenchHorn);}
}

        如果能够通过编写一个以基类为参数的方式,而不必在意任何的子类,或者说忘记子类的存在,那么整个程序就会变得更加直观和简单。这就是由多态进行实现的工作了。

难点

        在上面的例子中,Music.tune()方法接收了一个Wind类型的参数,但这里存在着一个问题:tune()只有一个Instrument类型的参数,这个方法是怎么知道其接收的是一个Wind()类型的参数,而不会是一个Stringed或者是Brass

public static void tune(Instrument i) { // tune()方法的形式

        解答这个问题的关键,就在于绑定

方法调用绑定

        绑定,就是将一个方法调用和一个方法体关联在一起。如果在程序运行之前执行绑定(若存在编译器和链接器,由它们完成),则称之为前期绑定

        与前期绑定相对的,后期绑定意味着绑定发生在运行时,并且基于对象的类型。这种绑定往往会通过某种机制确定对象的类型,并调用恰当的方法(后期绑定的实现会因为语言的不同产生差异,但可以认为,这些机制都需要将某种类型信息放入对象中)

    Java中的所有方法都是后期绑定,除非方法是staticfinal的(private是隐式的final)。例如,如果把Instrument.play()方法设为final的,那么在编译Music.java时就会报错。


产生正确的行为

        利用多态,就可以编写直接与基类互动的代码了。并且所有子类都可以通过这个相同的代码进行正确工作。

        在面向对象中,有一个经典的示例:“形状”。这个示例包括基类Shape及其的各种子类:Circle(圆形)、Square(正方形)、Triangle(三角形)等。它们的关系如图所示:

        向上转型的实现十分简单:

Shape s = new Circle() // 将Circle向上转型为Shape

        这条语句创建了一个Circle对象,并且把这个对象赋给了一个Shape引用。通过继承,Circle被认为是一种Shape。编译器认可这种语句。

        现在,假设存在一个基类方法draw(),这一方法在子类中已经进行了重写:

s.draw();

这条语句将不会调用Shapedraw(),由于后期绑定(即多态),Circle.draw()会被正确地调用。

    实际上,编译器不需要任何可以让其在编译时进行正确调用的特殊信息。这些都是动态绑定的工作。


可扩展性

        多态允许我们向系统内添加任意数量的新类型,而不需要修改基类的方法。在一个设计良好的OOP程序中,许多方法会遵循基类方法的模型,即只与基类接口通信。这样,程序就有了可扩展性。

        以之前的乐器(Instrument)为例,可以向其中添加更多的方法和类:

        这些后来的新方法可以和旧方法和谐相处。比如原本的tune()方法,它并不需要了解周围的代码变更,而可以正常工作。可以说,多态是程序员“将变化的事物和不变的事物分离”的一项重要技术。


陷阱:“重写”private方法

        若在无意之中,我们一个private的方法进行了“重写”,如:

public class PrivateOverride {private void f() {System.out.println("隐藏的f()方法");}public static void main(String[] args) {PrivateOverride po = new Derives();po.f();}
}class Derives extends PrivateOverride {public void f() { // 尝试性的“重写”System.out.println("公开的f()方法");}
}

        若没有注意到被重写的方法是private的,我们可能会认为输出的是“公开的f()方法”。但实际上的输出结果是:

        这是因为private方法也是final的,这种方法对子类隐藏。所以,在Derived中的f()是一个全新的方法,这个方法没有重载,因为f()的基类版本对Derived而言,是不可见的。所以,只有private的方法才能被重写。为此,最好在子类中使用与基类的private方法不同的名称。

        若使用@Override,就可以发现异常:

    @Override public void f() {System.out.println("公开的f()方法");}

        尝试编译,会发生报错:


陷阱:字段与静态方法

        与方法调用不同,字段并不存在多态。在直接访问一个字段时,该访问会在编译时解析:

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(); // 向上转型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());}
}

        程序执行的结果是:

        在上述程序中,Sub对象向上转型为Super引用时,其字段访问都会被编译器解析(得到的field字段是属于Super对象的)。因此,这不是多态。

        注意Super.fieldSub.field被分配了不同的存储空间。

        因此,Sub实际上包含了两个名称是field的字段:Sub自己的和Super的。而上述例子可以表明,当直接使用Sub.field时,不会获得基类的字段。要使用Superfield,就需要明确使用super.field

    为了防止混淆,一般不会让子类字段和基类字段使用相同的名称。

        除了字段,静态方法的行为也不是多态的:

class StaticSuper {public static String staticGet() {return "属于基类的staticGet()方法";}public String dynamicGet() {return "属于基类的dynamicGet()方法";}
}class StaticSub extends StaticSuper {public static String staticGet() { // 静态方法直接与类关联return "派生的staticGet()方法";}@Overridepublic String dynamicGet() {return "派生的dynamicGet()方法";}
}public class StaticPolymorphism {public static void main(String[] args) {StaticSuper sup = new StaticSub();System.out.println(StaticSuper.staticGet());System.out.println(sup.dynamicGet());}
}

        程序执行的结果如下:

        静态方法直接和类关联,不会与单个的对象关联。

构造器和多态

        构造器不同于其他方法,这点在涉及多态时也是如此。构造器是隐式的static方法,理解其在复杂层次结构和多态中的工作方式也很重要。

构造器的调用顺序

        基类的构造器总是在子类的构造过程中被调用。这是因为构造器需要保证对象的正确调用。由于字段通常是private的,因此一般必须假设子类只能访问自己的成员,而不能访问基类的成员。通过一个例子展示组合、继承及多态对构造顺序的影响:

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

        程序执行的结果是:

        根据上述的输出结果,可以得出一个复杂对象的构造器调用顺序:

  1. 基类的构造器被调用:
    1. 重复调用基类构造器,直到到达根基类。
    2. 根基类构造完毕,构造根基类的子类。
    3. 以此类推,直到最底层的子类构造完毕。
  2. 然后,按声明的顺序初始化成员。
  3. 最后,执行子类构造器的方法体。

        构造器的调用顺序是十分重要的。如果能够理清上述的顺序,就可以假定在子类中,基类的所有成员都是有效的

    为了使得所有成员在构造器中都是有效的,应该在类的定义处(如上述的bcl)来初始化所有的成员对象。


继承和清理

        大多时候,Java的清理可以交给垃圾收集器来处理。但若确有清理的必要,就需要为自己创建的新类创建一个清理方法(方法名可以自拟,本篇章中统一使用dispose()方法表示)。

        在继承时,若有特殊清理必须作为垃圾收集的一部分,那么也应该在子类中重写dispose()方法来执行该操作。并且,记住要调用基类的dispose()

class Characteristic {private String s;Characteristic(String s) {this.s = s;System.out.println("特征创建:" + s);}protected void dispose() {System.out.println("特征清理:" + s);}
}class Description {private String s;Description(String s) {this.s = s;System.out.println("特征创建:" + s);}protected void dispose() {System.out.println("特征清理:" + s);}
}class LivingCreature {private Characteristic p = new Characteristic("有活力的");private Description t = new Description("是一个活着的生物");LivingCreature() {System.out.println("构造器LivingCreature()");}protected void dispose() {System.out.println("清理LivingCreature");t.dispose();p.dispose();}
}class Animal extends LivingCreature {private Characteristic p = new Characteristic("有一颗心脏");private Description t = new Description("是动物而不是植物");Animal() {System.out.println("构造器Animal()");}@Overrideprotected void dispose() {t.dispose();p.dispose();super.dispose();}
}class Amphibian extends Animal {private Characteristic p = new Characteristic("能在水中生存");private Description t = new Description("水陆两栖");Amphibian() {System.out.println("构造器Amphibian()");}@Overrideprotected void dispose() {System.out.println("清理Amphibian");t.dispose();p.dispose();super.dispose();}
}public class Frog extends Amphibian {private Characteristic p = new Characteristic("呱呱叫");private Description t = new Description("吃虫子");public Frog() {System.out.println("构造器Frog()");}@Overrideprotected void dispose() {t.dispose();p.dispose();super.dispose();}public static void main(String[] args) {Frog frog = new Frog();System.out.println("结束");System.out.println();frog.dispose();}
}

        上述程序执行的结果是:

        上述程序中,清理的顺序刚好和初始化顺序相反。对于字段而言,这意味着与声明顺序相反(字段是按顺序初始化的)。对于基类,首先进行子类的清理,然后再进行基类的清理。

        Frog对象拥有其余的成员对象,并且能够控制对这些成员的清理。但是,如果其中的某个成员被其他成员共享,情况就会变得更加复杂,此时不能简单地调用dispose()。一个方法是使用引用计数的方式。例如:

class Shared {private int refcount = 0;private static long counter = 0;private final long id = counter++;Shared() {System.out.println("创建:" + this);}public void addRef() {refcount++;}protected void dispose() {if (--refcount == 0)System.out.println("清理:" + this);}@Overridepublic String toString() {return "Shared " + id;}
}class Compsoing {private Shared shared;private static long counter = 0;private final long id = counter++;Compsoing(Shared shared) {System.out.println("创建:" + this);this.shared = shared;this.shared.addRef();}protected void dispose() {System.out.println("清理:" + this);shared.dispose();}@Overridepublic String toString() {return "Composing " + id;}
}public class ReferenCounting {public static void main(String[] args) {Shared shared = new Shared();Compsoing[] compsoings = {new Compsoing(shared),new Compsoing(shared),new Compsoing(shared),new Compsoing(shared),new Compsoing(shared)};System.out.println();for (Compsoing c : compsoings) {c.dispose();}}
}

        程序执行的结果如下:

        对于这个程序而言,如果想要在类中使用共享对象,就需要调用addRef()。通过这种方式进行引用计数的跟踪,以此来判断是否进行清理。


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

        对一个普通的方法而言,动态绑定调用是在运行时解析的。这是为了确定被调用的方法到底属于子类还是基类。

        若在一个构造器内部调用动态绑定方法,就会得到该方法被重写后的定义。由于此时对象还没有被构造完毕,这个被重写的方法可能会带来一些难以被发现的错误

        构造器用于对象的创建工作,因此在构造器中,对象往往处于部分形成的状态,只有基类对象是已知被初始化的。若正在构造一个子类对象,那么当其基类构造器被调用时,这一子类对象还没有被全部初始化。但是,动态绑定可以跳出这一层次,直接调用子类(还未被初始化完毕的)中的方法。

        这就是一个有问题的例子:

class Glyph {void draw() {System.out.println("方法Glyph.draw()");}Glyph() {System.out.println("构造器Glyph:在调用draw()之前");draw();System.out.println("构造器Glyph:在调用draw()之后");}
}class RoundGlyph extends Glyph {private int radius = 1;RoundGlyph(int r) {radius = r;System.out.println("调用构造器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.draw()是为了重写而设计的方法,重写发生在RoundGlyph中。但Glyph()调用了该方法,实际上被调用的是RoundGlyph.draw()。或许有些人确实想要这个效果,但除此之外,红框所指的部分中,radius的值很明显是不对的。这就是初始化不完整导致的。

        补充并复习一下初始化的顺序:

  1. 在所有动作发生之前,为对象分配的储存空间会被初始化为二进制零。
  2. 基类构造器按层次被调用。此时被重写的draw()方法会被调用,而由于第1步的关系,radius是0
  3. 按声明顺序初始化成员。
  4. 执行子类构造器的主体代码。

        这就是为什么上述程序会出现问题。

    在编写构造器时的一个准则:使用尽可能少的操作使对象进入正常状态,并尽可能避免调用此类中的任何其他方法。

        注意:只有基类中的final方法(及隐式的finalprivate方法)可以在构造器中被安全调用。

协变返回类型

        Java 5加入的协变返回类型,使得子类中重写方法的返回值可以是基类方法返回值的子类型

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

        程序执行的结果是:

        协变返回类型允许process()的重写版本返回Wheat引用。但是在Java 5之前,process()会被强制要求返回Grain。也就是说,协变返回类型允许更具体的Wheat返回类型。

使用继承的设计

        事实上,在创建新类时,更好的选择是使用组合。因为组合不会强制要求程序设计使用继承层次结构,它更加灵活,可以动态选择类型(和随后的行动),而继承在编译时就需要知道确定的类型。例如:

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

        程序执行的结果是:

        上述的Stage.performPlay()会根据引用的不同而产生不同的行为,因为引用可以在运行时绑定到不同的对象上。这就在运行中获得了动态灵活性(状态模式)。相反,不能在运行时决定使用不同的方式进行继承。

    通用的原则:使用继承表达行为上的差异,使用字段表达状态的变化。

替换和扩展

        在继承中,最简洁的关系是“is-a”关系,即只有来自基类的方法会在子类中被重写:

        在这种方法中,子类的接口不会比基类的多。这时,使用子类对象不会需要额外的信息。完全相同的接口使得基类可以接收任何发送给子类的信息。

        但是,在一些时候我们会需要通过扩展接口来解决特定问题。这种关系被称为“is-like-a”,也就是说,子类像基类——子类拥有和基类相同的基本接口,同时也有用于实现特性的额外方法。

        这种扩展的部分在基类中是不可用的。因此,一旦发生向上转型,就无法调用这些扩展方法了:


向下转型和反射

        在进行向上转型时会丢失特定类型的信息,此时就可以通过向下转型来重新获取类型信息,即在继承层次结构中向下移动。

        尽管向上转型是安全的,因为基类只有那些通用的接口。但是向下转型却不一样,这是有危险的。

    打个比方,我们实际上无法知道一个形状是不是一个圆形。因为这个形状也可以是正方形、三角形或是其他类型。

        为此,就必须要有某种方法来保证向下转型的安全性。在Java中,每次的转型都会被检查。即使只是一次最普通的强制类型转换,都会在运行时被检查。这种运行时检查类型的行为是Java反射的一部分。

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 Reflect {public static void main(String[] args) {Useful[] x = {new Useful(),new MoreUseful()};x[0].f();x[1].g();// 下方这行语句触发编译时错误:无法在Useful中找到对应方法// x[1].u();((MoreUseful) x[1]).u(); // 向下转型,触发反射((MoreUseful) x[0]).u(); // 该条语句会抛出运行时异常}
}

        编译正常通过,但是若试图运行该程序,会发生异常:

        在尝试向下转型时,若类型正确就会直接通过,反之会得到一个异常。另外,反射并不仅仅包括简单的转型,但笔者尚未学到,此处就不做涉及。

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

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

相关文章

Unity中Shader的变体shader_feature

文章目录 前言一、变体的类型1、multi_compile —— 无论如何都会被编译的变体2、shader_feature —— 通过材质的使用情况来决定是否编译的变体 二、使用 shader_feature 来控制 shader 效果的变化1、首先在属性面板暴露一个开关属性,用于配合shader_feature来控制…

Java(四)数组与类和对象

Java(四)数组与类和对象 六、数组(非常重要)1.定义2.遍历2.1遍历方法2.2Arrays方法 3.二维数组数组小总结 七、类和对象1. 定义(重要)1.1 类1.2 对象 2. this关键字(重要)2.1 特点 3…

lv4 嵌入式开发-4 标准IO的读写(二进制方式)

目录 1 标准I/O – 按对象读写 2 标准I/O – 小结 3 标准I/O – 思考和练习 文本文件和二进制的区别: 存储的格式不同:文本文件只能存储文本。除了文本都是二进制文件。 补充计算机内码概念:文本符号在计算机内部的编码(计算…

肖sir__设计测试用例方法之正交表08_(黑盒测试)

设计测试用例方法之正交 一、正交表定义 正交试验设计法,是从大量的试验点中挑选出适量的、有代表性的点,应用依据迦罗瓦理论导出的“正交表”,合理的安排试验的一种科学的试验设计方法。 二、 正交常用的术语 指标:通常把判断试验…

OpenCV 12(图像直方图)

一、图像直方图 直方图可以让你了解总体的图像像素强度分布,其X轴为像素值(一般范围为0~255),在Y轴上为图像中具有该像素值像素数。 - 横坐标: 图像中各个像素点的灰度级. - 纵坐标: 具有该灰度级的像素个数. 画出上图的直方图: …

【实践篇】Redis最强Java客户端(三)之Redisson 7种分布式锁使用指南

文章目录 0. 前言1. Redisson 7种分布式锁使用指南1.1 简单锁:1.2 公平锁:1.3 可重入锁:1.4 红锁:1.5 读写锁:1.6 信号量:1.7 闭锁: 2. Spring boot 集成Redisson 验证分布式锁3. 参考资料4. 源…

IntelliJ IDEA远程调试:使用IDEA Remote Debug进行高效调试的指南

引言 在开发分布式系统时,调试是一个重要但复杂的环节。开发者通常需要跨越多个服务、模块和线程来追踪和解决问题。在没有远程调试的情况下,许多开发者会在代码中添加各种日志语句,然后重新部署和上线来调试。这种方法不仅费时,…

Hive_Hive统计指令analyze table和 describe table

之前在公司内部经常会看到表的元信息的一些统计信息,当时非常好奇是如何做实现的。 现在发现这些信息主要是基于 analyze table 去做统计的,分享给大家 实现的效果某一个表中每个列的空值数量,重复值数量等,平均长度 具体的指令…

华为数据管理——《华为数据之道》

数据分析与开发 元数据是描述数据的数据,用于打破业务和IT之间的语言障碍,帮助业务更好地理解数据。 元数据是数据中台的重要的基础设施,元数据治理贯彻数据产生、加工、消费的全过程,沉淀了数据资产,搭建了技术和业务…

【C++模拟实现】手撕AVL树

【C模拟实现】手撕AVL树 目录 【C模拟实现】手撕AVL树AVL树的介绍(百度百科)AVL树insert函数的实现代码验证是否为AVL树AVL树模拟实现的要点易忘点AVL树的旋转思路 作者:爱写代码的刚子 时间:2023.9.10 前言:本篇博客将…

python28种极坐标绘图函数总结

文章目录 基础图误差线等高线polar场图polar统计图非结构坐标图 📊python35种绘图函数总结,3D、统计、流场,实用性拉满 matplotlib中的画图函数,大部分情况下只要声明坐标映射是polar,就都可以画出对应的极坐标图。但…

9、补充视频

改进后的dijkstra算法 利用小根堆 将小根堆特定位置更改,再改成小根堆 nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);//改进后的dijkstra算法 //从head出发,所有head能到达的节点,生成到达每个节点的最小路径记录并返回 public static HashMap<No…

c语言练习44:深入理解strstr

深入理解strstr strstr作用展示&#xff1a; #include <stdio.h> #include <string.h> int main() {char str[] "This is a simple string";char* pch;pch strstr(str, "simple");/*strncpy(pch, "sample", 6);*/printf("%s…

Nginx详解 第五部分:Ngnix反向代理(负载均衡 动静分离 缓存 透传 )

Part 5 一、正向代理与反向代理1.1 正向代理简介1.2 反向代理简介 二、配置反向代理2.1 反向代理配置参数2.1.1 proxy_pass2.1.2 其余参数 2.2 配置实例:反向代理单台web服务器2.3 代理转发 三、反向代理实现动静分离四、缓存功能五、反向代理客户端的IP透传5.1 原理概述5.2 一…

基于语雀编辑器的在线文档编辑与查看

概述 语雀是一个非常优秀的文档和知识库工具&#xff0c;其编辑器更是非常好用&#xff0c;虽无开源版本&#xff0c;但有编译好的可以使用。本文基于语雀编辑器实现在线文档的编辑与文章的预览。 实现效果 实现 参考语雀编辑器官方文档&#xff0c;其实现需要引入以下文件&…

Pandas 掉包侠刷题实战--条件筛选

本博文内容为力扣刷题过程的记录&#xff0c;所有题目来源于力扣。 题目链接&#xff1a;https://leetcode.cn/studyplan/30-days-of-pandas/ 文章目录 准备工作1. isin(values) 和 ~2. df.drop_duplicates()3. df.sort_values()4. df.rename()5. pd.merge() 题目-条件筛选1. 大…

入门人工智能 —— 使用 Python 进行文件读写,并完成日志记录功能(4)

入门人工智能 —— 使用 Python 进行文件读写&#xff08;4&#xff09; 入门人工智能 —— 使用 Python 进行文件读写打开文件读取文件内容读取整个文件逐行读取文件内容读取所有行并存储为列表 写入文件内容关闭文件 日志记录功能核心代码&#xff1a;完整代码&#xff1a;运…

RabbitMQ从入门到精通之安装、通讯方式详解

文章目录 RabbitMQ一、RabbitMQ介绍1.1 现存问题 一、RabbitMQ介绍二、RabbitMQ安装三、RabbitMQ架构四、RabbitMQ通信方式4.1 RabbitMQ提供的通讯方式4.2 Helloworld 方式4.2Work queues4.3 Publish/Subscribe4.4 Routing4.5 Topics4.6 RPC (了解) 五、Springboot 操作RabbitM…

【结合AOP与ReflectUtil对返回数据进行个性化填充展示】

结合AOP与ReflectUtil对返回数据进行个性化填充展示 背景 对于接口列表返回的数据&#xff0c;我们通常有时候会对某些特殊的字段进行转化&#xff0c;或者根据某逻辑进行重新赋值&#xff0c;举个例子&#xff0c; 比如返回的列表数据中有性别sex&#xff0c;我们通常会同时…

微信小程序实现连续签到七天

断签之后会从第一天重新开始 <template><view class"content" style"height: 100vh;background: white;"><view class"back"><view style"position: absolute;bottom: 200rpx;left: 40rpx;width: 90%;"><i…