初识Java 7-1 多态

news2025/1/18 11:57:34

目录

向上转型

难点

方法调用绑定

产生正确的行为

可扩展性

陷阱:“重写”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,它们有相同的接口
    @Override
    public 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 {
    @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(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;

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

    @Override
    public 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()");
    }

    @Override
    protected 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()");
    }

    @Override
    protected 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()");
    }

    @Override
    protected 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);
    }

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

    @Override
    public 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);
    }

    @Override
    void 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 {
    @Override
    public String toString() {
        return "Grain";
    }
}

class Wheat extends Grain {
    @Override
    public String toString() {
        return "Wheat";
    }
}

class Mill {
    Grain process() {
        return new Grain();
    }
}

class WheatMill extends Mill {
    @Override
    Wheat 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 {
    @Override
    public void act() {
        System.out.println("HappyActor");
    }
}

class SadActor extends Actor {
    @Override
    public 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 {
    @Override
    public void f() {
    }

    @Override
    public 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.coloradmin.cn/o/997724.html

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

相关文章

Java开发之Mysql【面试篇 完结版】

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、知识体系二、Mysql-优化1. 优化-如何定位慢查询① 问题引入② 解决方案③ 问题总结④ 实战面试 2. 优化-sql执行很慢,如何解决① 问题引入② 解…

AI项目五:结印动作识别

若该文为原创文章,转载请注明原文出处。 感谢恩培大佬对项目进行了完整的实现,并将代码进行开源,供大家交流学习。 恩培大佬开源地址,有兴趣的可以去复现一下。GitHub - enpeizhao/CVprojects: computer vision projects | 计算机…

计算机网路学习-time_wait过多

四次挥手 调试命令 netstat -an|awk ‘/tcp/ {print $6}’|sort|uniq -c netstat -an 列出系统中所有处于活动状态的网络连接信息,包括 IP 地址、端口号、协议等。 其中,第六列是tcp的状态。 Proto Recv-Q Send-Q Local Address Foreign Addr…

Aidlux工业视觉缺陷检测

Aidlux工业视觉缺陷检测 1. AidLux简介 AidLux是成都阿加犀智能科技有限公司自主研发的融合架构平台,提供Android/鸿蒙+Linux融合系统, 双系统既能独立使用又能相互通信。 阿加犀致力于人工智能核心技术持续创新, 独…

CSS元素浮动

概述 浮动简介 在最初,浮动是用来实现文字环绕图片效果的,现在浮动是主流的页面布局方式之一。 元素浮动后的特点 脱离文档流。不管浮动前是什么元素,浮动后,默认宽与高都是被内容撑开的(尽可能小)&am…

AKF拆分原则

在分布式软件环境下,为了保障分布式架构的可靠性、可扩展、高性能,通常会通过集群、扩容、数据分治等思想来实现,比如很多中间件的使用Redis、ZK、Kafka等,都可以通过这种设计思想来提高系统架构吞吐量。AKF是一个系统化的拓展思想…

Vue框架+Element组件库学习笔记

一、Vue框架 vue:是一款前端框架,免除原生JavaScript中的DOM操作(如document.getElementById("文本输入框名").value),简化书写。基于MVVM(Model-View-ViewModel)思想,实…

YApi 新版如何查看 http 请求数据

YApi 新版如何查看 http 请求数据 因chrome 安全策略限制,在 cross-request 升级到 3.0 后, 不再支持文件上传功能,并且需要通过以下方法查看 network:1.首先在chrome 输入 > chrome://extensions打开扩展页2.开启开发者模式3.点击 cross…

震惊:QGroupBox在linux下居然不显示边框

1、linux系统下GroupBox默认无边框,如需添加,需要通过style来修改 下面介绍几个例子: QGroupBox {background: transparent;border: 1px solid rgb(130, 130, 130);border-radius: 6px;}QGroupBox::title {subcontrol-origin: margin;subcont…

RegShot – 注册表比较工具

RegShot 是一种注册表比较工具,它通过两次抓取注册表而快速地比较出答案。它还可以将您的注册表以纯文本方式记录下来,便于浏览;还可以监察 Win.ini,System.ini 中的键值;还可以监察您Windows目录和 System 目录中文件…

比较聚合模型实战文本匹配

引言 本文我们采用比较聚合模型来实现文本匹配任务。 数据准备 数据准备包括 构建词表(Vocabulary)构建数据集(Dataset) 本次用的是LCQMC通用领域问题匹配数据集,它已经分好了训练、验证和测试集。 我们通过pandas来加载一下。 import pandas as pdtrain_df …

diskqueue第五篇 - 追尾检测,错误处理,如何正常关闭

diskqueue是nsq消息持久化的核心,内容较多,故分为多篇 1. diskqueue第一篇 - 是什么,为什么需要它,整体架构图,对外接口 2. diskqueue第二篇 - 元数据文件,数据文件,启动入口,元数…

熟悉Redis6

NoSQL数据库简介 技术发展 技术的分类 1、解决功能性的问题:Java、Jsp、RDBMS、Tomcat、HTML、Linux、JDBC、SVN 2、解决扩展性的问题:Struts、Spring、SpringMVC、Hibernate、Mybatis 3、解决性能的问题:NoSQL、Java线程、Hadoop、Nginx…

嵌入式-Linux基本操作 pwd cd ls touch clear mkdir rm cp mv

目录 一.Linux文件系统 二.Linux目录结构 三.Linux基本命令 3.1shell脚本 3.2pwd命令 3.3cd命令 3.4ls命令 3.5touch命令 3.6clear命令 3.7mkdir命令 3.8rm命令 3.9cp命令 3.10mv命令 一.Linux文件系统 Linux文件系统是Linux操作系统中用于组织和管理文件和目录的…

【算法训练-链表 六】【查找】:链表中倒数第k个节点

废话不多说,喊一句号子鼓励自己:程序员永不失业,程序员走向架构!本篇Blog的主题是【查找链表】,使用【链表】这个基本的数据结构来实现,这个高频题的站点是:CodeTop,筛选条件为&…

C盘清理教程

C盘清理教程 首先使用space Sniffer 扫一下c盘,然后看一下到底是哪个文件这么大 第二步,创建软链接。 首先将我们需要移动的文件的当前路径拷贝下来:C:\Users\Tom\Desktop\test-link\abc\ghi.txt 然后假设剪切到D盘下:D:\ghi.…

MOOC软件系统外包开发

MOOC(大规模开放在线课程)系统是用于创建、管理和交付在线教育课程的软件平台。这些系统通常具有多种功能,旨在支持大规模的在线学习。以下是MOOC系统主要实现的功能以及一些常见的开源系统,希望对大家有所帮助。北京木奇移动技术…

vite项目框架搭建

vite项目框架搭建 1. 使用vite初始化项目 开始 | Vite 官方中文文档 (vitejs.dev) pnpm create vite # 依次设置项目名称、选择框架【vue】、选择语言【typescript】 √ Project name: ... vite-project √ Select a framework: Vue √ Select a variant: TypeScript2. ele…

【strtok函数和strerror函数的介绍和使用以及扩展】

strtok函数和strerror函数的介绍和使用以及扩展 一.strtok函数 1.strtok函数介绍 资源来源于cplusplus网站 它的作用: 对此函数的一系列调用将 str 拆分为标记,这些标记是由分隔符中的任何字符分隔的连续字符序列。 在第一次调用时,该函数…

vue学习之Javascript 表达式内容渲染和属性绑定

Javascript 表达式内容渲染和属性绑定 创建 demo4.html,内容如下 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0">…