九、多態(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.pswp.cn/news/34799.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/34799.shtml
英文地址,請注明出處:http://en.pswp.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…

ubuntu中redis+mysql安裝使用

pip -V 回車&#xff08;大寫V&#xff09;&#xff1a;python包庫安裝路徑 python -m site: python查找路徑 1、redis ubuntu安裝redis System has not been booted with systemd as init system (PID 1). Cant operate&#xff1b;該問題是systemctl start redis報錯&#…

ZLMediaKit(webrtc)在linux上(CentOS7)部署與啟動

一.ZLMediaKit(webrtc)在CentOS7部署與啟動 # 1. 卸載舊版本 yum remove git # 2. 安裝 yum 源的 Git 版本 yum install -y git # 3. 查看版本 git version # 輸出 git version 1.8.3.1配置全局環境變量 # 1. 編輯配置文件 vim /etc/profile # 2. 在 /etc/profile 文件中末尾…

用 Rufus 制作 Ubuntu 系統啟動盤時,選擇分區類型為MBR還是GPT?

當使用 Rufus 制作 Ubuntu 系統啟動盤時&#xff0c;您可以根據您的需求選擇分區類型&#xff0c;MBR&#xff08;Master Boot Record&#xff09;還是 GPT&#xff08;GUID Partition Table&#xff09;。 MBR 是傳統的分區表格式&#xff0c;適用于大多數舊版本的操作系統和舊…

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

JMM JAVA Memory Model java內存模型 作用&#xff1a;緩存一致性協議&#xff0c;用于定義數據讀寫的規則&#xff08;遵守&#xff0c;找到這個規則&#xff09; JMM定義了線程2工作內存和主內存之間的抽象關系&#xff1a;線程之間的共享變量存儲在主內存&#xff08;main …

TLS協議

目錄 什么是TLS協議&#xff1f; TLS的基本流程&#xff1f; 兩種密鑰交換算法&#xff1f; 基于ECDHE密鑰交換算法的TLS握手過程&#xff1f; 基于RSA密鑰交換算法的TLS握手過程&#xff1f; 基于RSA的握手和基于ECDHE的握手有什么區別&#xff1f; 什么是前向保密&…

tp6 v3微信退款

/*** Notes:退款* param $out_trade_no 支付時候訂單號&#xff08;order表 original_bn&#xff09;兩個參數選一個這個要選對* param $out_refund_no 退款訂單號* param $total 訂單金額* param $refund 退款金額* Time: 2023-08-10*/public function refundMoney($out_trade…

oracle的異常處理

oracle提供了預定義例外、非預定義例外和自定義例外三種類型。其中&#xff1a; l預定義例外用于處理常見的oracle錯誤&#xff1b; l非預定義例外用于處理預定義所不能處理的oracle錯誤&#xff1b; l自定義例外處理與oracle錯誤無關的其他情況。 Oracle代碼編寫過程中&am…

nginx反向代理與負載均衡

負載均衡依靠反向代理實現。nginx的代理分為七層代理與四層代理&#xff1a; 七層代理&#xff1a;七層代理的就是http請求和響應。七層代理是最常用的反向代理方式&#xff0c;只能配置在nginx配置文件的http模塊。而且配置方法名稱&#xff1a;upstream模塊&#xff0c;不能寫…

提升效率!Go語言開發者不可錯過的必備工具集合!

&#x1f337;&#x1f341; 博主貓頭虎 帶您 Go to Golang Language.??&#x1f341; &#x1f984; 博客首頁——貓頭虎的博客&#x1f390; &#x1f433;《面試題大全專欄》 文章圖文并茂&#x1f995;生動形象&#x1f996;簡單易學&#xff01;歡迎大家來踩踩~&#x1…

UDP數據報網絡編程(實現簡單的回顯服務器,客戶端)

回顯服務器表示客戶端發的是啥&#xff0c;服務器就返回啥&#xff0c;主要是為了熟悉UDP數據報網絡編程的基本步驟 對于程序的所有分析都寫到了代碼上 UDP回顯服務器代碼 package UdpEcho;import java.io.IOException; import java.net.DatagramPacket; import java.net.Dat…

ClickHouse查看執行計劃(EXPLAIN語法)

1.EXPLAIN 語法示例 EXPLAIN [AST | SYNTAX | QUERY TREE | PLAN | PIPELINE | ESTIMATE | TABLE OVERRIDE] [setting value, ...] [ SELECT ... | tableFunction(...) [COLUMNS (...)] [ORDER BY ...] [PARTITION BY ...] [PRIMARY KEY] [SAMPLE BY ...] [T…

途樂證券-最準確的KDJ改良指標?

KDJ目標是技術剖析的一種重要目標之一&#xff0c;它是利用隨機目標&#xff08;%R&#xff09;發展而來的&#xff0c;是一種反映商場超買和超賣狀況的買賣目標。KDJ目標由快線&#xff08;K線&#xff09;、慢線&#xff08;D線&#xff09;和隨機值&#xff08;J線&#xff…

RestTemplate HTTPS請求忽略SSL證書

問題描述 使用RestTemplate發送HTTPS請求的時候&#xff0c;出現了這樣的一個問題&#xff1a; sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification …

MySQL多表查詢

1.創建student和score表 創建score表 2.為student表和score表增加記錄 向student表插入記錄的INSERT語句如下&#xff1a; 向score表插入記錄的INSERT語句如下&#xff1a; 1.查詢student表的所有記錄 2.查詢student表的第2條到4條記錄 3.從student表查詢所有學生的學號&#…

2023最新影視泛目錄系統,蘋果cms最新下載地址

影視泛目錄系統是指一種集成了電影、電視劇、綜藝節目、動漫等多種影視資源資源的系統&#xff0c;其目的是為用戶提供一個方便快捷的影視資源檢索、瀏覽、分享、評論的平臺。該系統通過對影視資源進行分類、標簽化、評分等方式&#xff0c;為用戶提供更加精準的檢索和推薦服務…

C#多線程學習 生產者和消費者

前面說過&#xff0c;每個線程都有自己的資源&#xff0c;但是代碼區是共享的&#xff0c;即每個線程都可以執行相同的函數。這可能帶來的問題就是幾個線程同時執行一個函數&#xff0c;導致數據的混亂&#xff0c;產生不可預料的結果&#xff0c;因此我們必須避免這種情況的發…

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…