1. 程式人生 > >符號引用和直接引用,解析和分派

符號引用和直接引用,解析和分派

1. 符號引用

考慮這樣一個Java類:

public class X {
  public void foo() {
    bar();
  }

  public void bar() { }
}

它編譯出來的Class檔案的文字表現形式如下:

Classfile /private/tmp/X.class
  Last modified Jun 13, 2015; size 372 bytes
  MD5 checksum 8abb9cbb66266e8bc3f5eeb35c3cc4dd
  Compiled from "X.java"
public class X
  SourceFile: "X.java"
minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#16 // java/lang/Object."<init>":()V #2 = Methodref #3.#17 // X.bar:()V #3 = Class #18 // X #4 = Class #19 // java/lang/Object
#5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 LocalVariableTable #10 = Utf8 this #11 = Utf8 LX; #12 = Utf8 foo #13 = Utf8 bar #14
= Utf8 SourceFile #15 = Utf8 X.java #16 = NameAndType #5:#6 // "<init>":()V #17 = NameAndType #13:#6 // bar:()V #18 = Utf8 X #19 = Utf8 java/lang/Object { public X(); flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this LX; public void foo(); flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokevirtual #2 // Method bar:()V 4: return LineNumberTable: line 3: 0 line 4: 4 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this LX; public void bar(); flags: ACC_PUBLIC Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this LX; }

可以看到Class檔案裡有一段叫做“常量池(Constant pool)”,裡面儲存的該Class檔案裡的大部分常量的內容。

來考察foo()方法裡的一條位元組碼指令:

1: invokevirtual #2  // Method bar:()V

這在Class檔案中的實際編碼為:

[B6] [00 02]

其中0xB6是invokevirtual指令的操作碼(opcode),後面的0x0002是該指令的運算元(operand),用於指定要呼叫的目標方法。
這個引數是Class檔案裡的常量池的下標。那麼去找下標為2的常量池項,是:

#2 = Methodref          #3.#17         //  X.bar:()V

這在Class檔案中的實際編碼為(以十六進位制表示,Class檔案裡使用高位在前位元組序(big-endian)):

[0A] [00 03] [00 11]

其中0x0A是CONSTANT_Methodref_info的tag,後面的0x0003和0x0011是該常量池項的兩個部分:class_index和name_and_type_index。這兩部分分別都是常量池下標,引用著另外兩個常量池項。
順著這條線索把能傳遞引用到的常量池項都找出來,會看到(按深度優先順序排列):

   #2 = Methodref          #3.#17         //  X.bar:()V
   #3 = Class              #18            //  X
  #18 = Utf8               X
  #17 = NameAndType        #13:#6         //  bar:()V
  #13 = Utf8               bar
   #6 = Utf8               ()V

把引用關係畫成一棵樹的話:

     #2 Methodref X.bar:()V
     /                     \
#3 Class X       #17 NameAndType bar:()V
    |                /             \
#18 Utf8 X    #13 Utf8 bar     #6 Utf8 ()V

標記為Utf8的常量池項在Class檔案中實際為CONSTANT_Utf8_info,是以略微修改過的UTF-8編碼的字串文字。

這樣就清楚了對不對?
由此可以看出,Class檔案中的invokevirtual指令的運算元經過幾層間接之後,最後都是由字串來表示的。這就是Class檔案裡的“符號引用”的實態:帶有型別(tag) / 結構(符號間引用層次)的字串。

2. 直接引用

這裡就不拿HotSpot VM來舉例了,因為它的實現略複雜。讓我們看個更簡單的實現,Sun的元祖JVM——Sun JDK 1.0.2的32位x86上的做法。
請先參考另一個回答裡講到Sun Classic VM的部分:為什麼bs虛擬函式表的地址(int*)(&bs)與虛擬函式地址(int*)(int)(&bs) 不是同一個? - RednaxelaFX 的回答

Sun Classic VM:(以32位Sun JDK 1.0.2在x86上為例)

         HObject             ClassObject
                       -4 [ hdr            ]
--> +0 [ obj     ] --> +0 [ ... fields ... ]
    +4 [ methods ] \
                    \         methodtable            ClassClass
                     > +0  [ classdescriptor ] --> +0 [ ... ]
                       +4  [ vtable[0]       ]      methodblock
                       +8  [ vtable[1]       ] --> +0 [ ... ]
                       ... [ vtable...       ]

(請留心閱讀上面連結裡關於虛方法表與JVM的部分。Sun的元祖JVM也是用虛方法表的喔。)

元祖JVM在做類載入的時候會把Class檔案的各個部分分別解析(parse)為JVM的內部資料結構。例如說類的元資料記錄在ClassClass結構體裡,每個方法的元資料記錄在各自的methodblock結構體裡,等等。

在剛載入好一個類的時候,Class檔案裡的常量池和每個方法的位元組碼(Code屬性)會被基本原樣的拷貝到記憶體裡先放著,也就是說仍然處於使用“符號引用”的狀態;直到真的要被使用到的時候才會被解析(resolve)為直接引用。

假定我們要第一次執行到foo()方法裡呼叫bar()方法的那條invokevirtual指令了。
此時JVM會發現該指令尚未被解析(resolve),所以會先去解析一下。
通過其運算元所記錄的常量池下標0x0002,找到常量池項#2,發現該常量池項也尚未被解析(resolve),於是進一步去解析一下。
通過Methodref所記錄的class_index找到類名,進一步找到被呼叫方法的類的ClassClass結構體;然後通過name_and_type_index找到方法名和方法描述符,到ClassClass結構體上記錄的方法列表裡找到匹配的那個methodblock;最終把找到的methodblock的指標寫回到常量池項#2裡。

也就是說,原本常量池項#2在類載入後的執行時常量池裡的內容跟Class檔案裡的一致,是:

[00 03] [00 11]

(tag被放到了別的地方;小細節:剛載入進來的時候資料仍然是按高位在前位元組序儲存的)
而在解析後,假設找到的methodblock*是0x45762300,那麼常量池項#2的內容會變為:

[00 23 76 45]

(解析後位元組序使用x86原生使用的低位在前位元組序(little-endian),為了後續使用方便)
這樣,以後再查詢到常量池項#2時,裡面就不再是一個符號引用,而是一個能直接找到Java方法元資料的methodblock* 了。這裡的methodblock* 就是一個“直接引用”

解析好常量池項#2之後回到invokevirtual指令的解析。
回顧一下,在解析前那條指令的內容是:

[B6] [00 02]

而在解析後,這塊程式碼被改寫為:

[D6] [06] [01]

其中opcode部分從invokevirtual改寫為invokevirtual_quick,以表示該指令已經解析完畢。
原本儲存運算元的2位元組空間現在分別存了2個1位元組資訊,第一個是虛方法表的下標(vtable index),第二個是方法的引數個數。這兩項資訊都由前面解析常量池項#2得到的methodblock* 讀取而來。
也就是:

invokevirtual_quick vtable_index=6, args_size=1

這裡例子裡,類X對應在JVM裡的虛方法表會是這個樣子的:

[0]: java.lang.Object.hashCode:()I
[1]: java.lang.Object.equals:(Ljava/lang/Object;)Z
[2]: java.lang.Object.clone:()Ljava/lang/Object;
[3]: java.lang.Object.toString:()Ljava/lang/String;
[4]: java.lang.Object.finalize:()V
[5]: X.foo:()V
[6]: X.bar:()V

所以JVM在執行invokevirtual_quick要呼叫X.bar()時,只要順著物件引用查詢到虛方法表,然後從中取出第6項的methodblock*,就可以找到實際應該呼叫的目標然後呼叫過去了。
假如類X還有子類Y,並且Y覆寫了bar()方法,那麼類Y的虛方法表就會像這樣:

[0]: java.lang.Object.hashCode:()I
[1]: java.lang.Object.equals:(Ljava/lang/Object;)Z
[2]: java.lang.Object.clone:()Ljava/lang/Object;
[3]: java.lang.Object.toString:()Ljava/lang/String;
[4]: java.lang.Object.finalize:()V
[5]: X.foo:()V
[6]: Y.bar:()V

於是通過vtable_index=6就可以找到類Y所實現的bar()方法。
所以說在解析/改寫後的invokevirtual_quick指令裡,虛方法表下標(vtable index)也是一個“直接引用”的表現。

在現在的HotSpot VM裡,圍繞常量池、invokevirtual的解析(再次強調是resolve)的具體實現方式跟元祖JVM不一樣,但是大體的思路還是相通的。

HotSpot VM的執行時常量池有ConstantPool和ConstantPoolCache兩部分,有些型別的常量池項會直接在ConstantPool裡解析,另一些會把解析的結果放到ConstantPoolCache裡。以前發過一帖有簡易的圖解例子,可以參考:請問,jvm實現讀取class檔案常量池資訊是怎樣呢?

==================================================

由此可見,符號引用通常是設計字串的——用文字形式來表示引用關係。

而直接引用是JVM(或其它執行時環境)所能直接使用的形式。它既可以表現為直接指標(如上面常量池項#2解析為methodblock*),也可能是其它形式(例如invokevirtual_quick指令裡的vtable index)。
關鍵點不在於形式是否為“直接指標”,而是在於JVM是否能“直接使用”這種形式的資料。

3. 解析

就如上所說,解析(resolve)就是將一個符號引用轉換成直接引用的一個過程。

當一個java檔案編譯成class之後,方法都是以符號引用的方式儲存。而在載入類時,部分符合條件的符號引用會被轉換成“直接引用”,這個過程我們稱之為“解析(Resolution)”

而符合這以條件的方法有:靜態方法、私有方法、構造方法、父類方法。它們在類載入的的解析階段就會將符號引用解析為該方法的直接引用。

jvm中提供了5條方法呼叫位元組碼指令:

  • invokestatic: 呼叫靜態方法
  • invokespecail: 呼叫構造方法、私有方法和父類方法。
  • invokevirtual: 呼叫所有的虛方法。
  • invokeinterface: 呼叫介面方法,會在執行時再確定一個實現此介面的物件。
  • invokedynamic: 現在執行時動態解析出呼叫點限定符所引用的方法,然後再執行該方法。在此之前的4條呼叫指令,分派邏輯是固化在jvm內部的,而invokedynamic指令的分派邏輯是由使用者所設定的引導方法決定的。(沒懂)

只要能被invokestatic和invokespecail指令呼叫的方法,都可以在解析(Resolution)階段中確定方法的呼叫版本,符合這個條件的有靜態方法、私有方法、構造方法、父類方法。它們在類載入時就會把符號引用解析(resolve)為該方法的直接引用,這些方法可以稱為非虛方法,與之相反稱為虛方法(除final方法)。而虛方法則會在程式執行期間才解析成直接引用。

4. 分派

以下內容來自 深入理解Java虛擬機器_JVM高階特性與最佳實踐 第2版

分派指的是確定方法的呼叫版本,例如有兩個過載的方法,具體呼叫哪個,可以通過引數型別和引數個數來判斷,這個過程就是分派。

而分派又分為靜態分派和動態分派,編譯期間即可確定的需要執行的方法版本稱為靜態分派,而動態分派則是需要在實際執行時才能確定需要執行的方法版本。它們的應用分別對應“過載”和“重寫”。

4.1 靜態分派

靜態分派在編譯期間即可確定方法的執行版本,經典的例子就是“過載”。(AItsuki:實際上靜態分派和“靜態代理”一樣,是相對於動態分派和動態代理而出現的,實際上英文文件中並沒有靜態分派和靜態代理的說法)

public class StaticDispatch {  

    static abstract class Human{  
    }  

    static class Man extends Human{  
    }  

    static class Woman extends Human{  
    }  

    public void sayHello(Human guy){  
        System.out.println("hello,guy!");  
    }

    public void sayHello(Man guy){  
        System.out.println("hello,gentlemen!");  
    }

    public void sayHello(Woman guy){  
        System.out.println("hello,lady!");  
    }  

    public static void main(String[] args) {  
        Human man = new Man();  
        Human woman = new Woman();  
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);  
        sr.sayHello(woman);  
    }  
}  

執行結果:

hello,guy!
hello,guy!

執行結果很容易得出,但是這裡要說一個重要的概念。

Human man = new Man();  

在這行程式碼中,Human被稱為變數的“靜態型別”,或者“外觀型別”,後面的Man則被稱為“實際型別”

靜態型別和實際型別在程式中都可以發生變化,但是靜態型別的變化僅僅發生在使用時,變數本身的靜態型別不會改變,最終的靜態型別在編譯期是可知的;
而實際型別變化的結果在執行期才可以確定,編譯時並不知道一個物件的實際型別是什麼。例如:

// 實際型別變化
Human man = new Man();
man = new Woman();

// 靜態型別變化
sr.sayHello((Man) man);
sr.sayHello((Woman) man);

在上面StaticDispatch的案例程式碼中,main()方法的兩次呼叫sayHello(),在方法接收者已經確定是物件sr的前提下,使用哪個過載版本,就完全取決於傳入引數的數量和資料型別。程式碼中刻意的定義了兩個靜態型別相同但實際型別不同的變數,但編譯器在過載時時通過引數的靜態型別而不是實際型別作為判定依據的。並且靜態型別時編譯期可知的,因此,在編譯階段,javac編譯器會根據引數的靜態型別決定使用哪個過載版本,所以選擇了sayHello(Human)作為呼叫目標。
所有依賴靜態型別來定位方法執行版本的分派動作稱為靜態分派,靜態分派的典型應用是方法過載,發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機器來執行的。

另外,靜態分派會選擇最合適的過載版本。

public class Main {

    public static void main(String[] args) {
        sayHello('a');
    }

    public static void sayHello(char a) {
        System.out.println("say char");
    }

    public static void sayHello(int a) {
        System.out.println("say int");
    }

    public static void sayHello(long a) {
        System.out.println("say long");
    }

    public static void sayHello(Character a) {
        System.out.println("say Character");
    }

    public static void sayHello(Serializable a) {
        System.out.println("say Serializable");
    }
}

例如上面的程式碼會輸出

say char

而註釋掉sayHello(char)後則會呼叫sayHello(int),這裡發生了一次自動型別轉換,a被轉換成97。

繼續註釋掉sayHello(int)後則會呼叫sayHello(long),這裡發生了兩次自動型別轉換,a先轉換成97之後,進一步轉型為97L。
程式碼中沒有float、double等過載,不過實際上自動轉型還能繼續發生多次,按照char->int->long->float->double的順序轉型進行匹配。但不會匹配到byte和short型別,因為這個轉型是不安全的。

我們繼續註釋掉sayHello(long),那麼會呼叫 sayHello(Character),這裡發生了一次自動裝箱。

繼續註釋掉sayHello(Character),最後則會呼叫sayHello(Serializable),因為Character實現了Serializable介面,會自動轉型為介面型別。

4.2 動態分派

public class DynamicDispatch {

    static abstract class Human { 
        protected abstract void sayHello();
    }

    static class Man extends Human {
        @Override
        protected void sayHello() {
            System.out.println("man say hello");
        } 
    }

    static class Woman extends Human {
        @Override
        protected void sayHello() {
            System.out.println("woman say hello");
        } 
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}

執行結果為

man say hello
woman say hello
woman say hello

結果不出預料,習慣了面向物件思維的java程式設計師會覺得這是理所當然的,但是虛擬機器是如何知道要呼叫哪個方法的?

顯然這裡不是根據靜態型別來決定,看看這兩段程式碼的位元組碼:

public class com.aitsuki.proxytest.DynamicDispatch {
  public com.aitsuki.proxytest.DynamicDispatch();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/aitsuki/proxytest/DynamicDispatch$Man
       3: dup
       4: invokespecial #3                  // Method com/aitsuki/proxytest/DynamicDispatch$Man."<init>":()V
       7: astore_1
       8: new           #4                  // class com/aitsuki/proxytest/DynamicDispatch$Woman
      11: dup
      12: invokespecial #5                  // Method com/aitsuki/proxytest/DynamicDispatch$Woman."<init>":()V
      15: astore_2
      16: aload_1
      17: invokevirtual #6                  // Method com/aitsuki/proxytest/DynamicDispatch$Human.sayHello:()V
      20: aload_2
      21: invokevirtual #6                  // Method com/aitsuki/proxytest/DynamicDispatch$Human.sayHello:()V
      24: new           #4                  // class com/aitsuki/proxytest/DynamicDispatch$Woman
      27: dup
      28: invokespecial #5                  // Method com/aitsuki/proxytest/DynamicDispatch$Woman."<init>":()V
      31: astore_1
      32: aload_1
      33: invokevirtual #6                  // Method com/aitsuki/proxytest/DynamicDispatch$Human.sayHello:()V
      36: return
}

0~15行是位元組碼的準備動作,建立man和woman的記憶體空間,呼叫Man和Woman的構造器,將這兩個例項的引用存放在第1、2個區域性變量表Slot中,這個動作也對應了程式碼中的這兩句:

Human man = new Man();
Human woman = new Woman();

16、20行分別把剛剛建立的兩個物件的引用壓到棧頂,這兩個物件是將要執行的sayHello()方法的所有者,稱為接收者(Receiver);17和21行是方法呼叫指令,可以看到它們對應的常量池地址都是#6,代表它們的符號引用是完全一樣的。但是這兩句指令最終執行的目標方法並不相同,原因就要從invokevirtual指令的多型查詢過程說起,invokevirtual指令的執行時解析過程大致分為以下幾個步驟:
1. 找到運算元棧頂的第一個元素所指向的物件的實際型別,記作C。
2. 如果在型別C中找到與長兩種描述符和簡單名稱都相同的方法,則進行訪問許可權校驗,如果通過則返回這個方法的直接引用,查詢過程結束;如果不通過,則返回java.lang.IllegalAccessError異常。
3. 否則,按照繼承關係從下往上依次對C的個個父類進行第二步的搜尋和驗證過程。
4. 如果始終沒有找到合適的方法,則丟擲java.lang.AbstractMethodError異常。

由於invokevirtual指令執行的第一步就是在執行期確定接收者的實際型別,所以兩次呼叫中的invokevirtual指令把常量池中的類方法符號引用解析到了不同的直接引用上,這個過程就是java語言中方法重寫的本質。我們把這種在執行期根據實際型別確定方法執行版本的分派過程稱為動態分派。

5. 總結

5.1 符號引用

java程式碼被編譯成位元組碼時,方法是以符號引用表示。

5.2 直接引用

符號引用在某個時刻(可能類載入時,也可能執行時)會被解析成直接引用,直接引用即JVM能直接使用的形式,可能表現為“直接指標” methodblock* 也可能是其它形式。

5.3 解析

解析(Resolve)是將部分符合條件的符號引用轉換成直接引用的過程。
or
解析(Resolution)是類載入的其中一個過程,該過程該過程其中一個作用是將部分符合條件的符號引用轉換成直接引用。(RednaxelaFX大神好像將這個過程稱為parse)

5.4 分派

分派指的是確定方法呼叫版本,又分為靜態分派和動態分派。
靜態分派是在編譯期即根據變數的靜態型別即可確定方法呼叫版本。
動態分派是需要在執行時根據實際型別才能確定方法呼叫版本。

來個急轉彎給一些思維短路的朋友(其實是我通宵寫文章腦子沒轉過來):
靜態分派是在編譯期即根據變數的靜態型別即可確定方法呼叫版本,那麼動態分派不也是在編譯期間可以確定了麼,因為不是靜態分派就是動態分派啊?

上面的問題是偷換概念了,概念不要放在靜態分派和動態分派這兩個術語上,而是要放在確定方法呼叫版本這句話上。
在編譯期即可確定靜態分派或動態分派這句話沒錯,但是在編譯期即可“確定方法呼叫版本”就不對了。分派指的是一個方式,確定方法呼叫版本的方式。

相關推薦

符號引用直接引用解析分派

1. 符號引用 考慮這樣一個Java類: public class X { public void foo() { bar(); } public void bar() { } } 它編譯出來的Class檔案的文

JVM 解析階段 符號引用直接引用的區別

1.符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。例如,在Class檔案中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref

java -- JVM的符號引用直接引用

不同 class文件 rep 類加載 repl 符號 ava 內存 內存地址 在JVM中類加載過程中,在解析階段,Java虛擬機會把類的二級制數據中的符號引用替換為直接引用。 1.符號引用(Symbolic References):   符號引用以一組符號來描述所引用的目標

個人理解 java虛擬機器中的符號引用直接引用

      在java中,一個java類將會編譯成一個class檔案。在編譯時,java類並不知道引用類的實際記憶體地址,因此只能使用符號引用來代替。比如org.simple.People類引用org

符號引用直接引用

先看Class檔案裡的“符號引用”。 考慮這樣一個Java類: public class X { public void foo() { bar(); } public void bar() { } } 它編譯出來的Class檔案的文字表現形式如下: Classfile /priva

java虛擬機器中的符號引用直接引用

在java中,一個java類將會編譯成一個class檔案。在編譯時,java類並不知道引用類的實際記憶體地址,因此只能使用符號引用來代替。比如org.simple.People類引用org.simple.Tool類,在編譯時People類並不知道Tool類的實際記憶體地址,因此只能使用符號org.simple

淺析 JVM 中的符號引用直接引用

前言 在 JVM 的學習過程中,一直會遇到符號引用和直接引用這兩個概念。最近我也查閱了一些資料,有了一些初步的認識,記錄在此與大家分享。文中的內容,主要參考自 JVM裡的符號引用如何儲存? 與 自己動手寫Java虛擬機器。 關於符號引用與直接引用,我們還是用一個例項來分析

Java符號引用直接引用

Java 符號引用與直接引用 1. 定義(深入理解JVM第182頁) 符號引用        符號引用以一組符號來描述所引用的目標,符號可以是任意形式的字面量,只要使用時能無歧義的定位到目標即可。符號與虛擬機器實現的記憶體佈局無關,引用目標並不一定載入到記

java_符號引用直接引用

1.符號引用(Symbolic References):   符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。例如,在Class檔案中它以CONSTANT_Class_info、CONSTANT_Fieldref_in

JavaScript運算符:遞增遞減(++i--i i++i-- 的區別)

nbsp key mic comment 包含 -- 效應 1+n com 遞增和遞減操作符直接借鑒自C,而且各有兩個版本:前置型 (遞增 ++i ,遞減 --i )和 後置型 (遞增 i++ ,遞減 i-- )。書本上對兩者的定義是:前置型應該位於要操作的變量之前,而後置

js中decodeURI()encodeURI()區別decodeURIComponentencodeURIComponent區別

nbsp sch www 問題 encode 替換 副本 字符替換 序列 decodeURI()定義和用法:decodeURI()函數可對encodeURI()函數編碼過的URI進行解碼.語法:decodeURI(URIstring)參數描述:URIstring必需,一個字

for-infor-offorEachMap

循環對象 共同點 school 遍歷 fine 不能 name pre 包括 for-in和for-of   1.  for-in循環實際是為循環”enumerable“對象而設計的,是用來循環帶有字符串key的對象的。    使用for in會遍歷數組所有的可枚舉屬性,包

python23搭建pip2pip3的安裝

pip python 環境搭建 虛擬環境 virtualenv linux自帶python2,手動安裝python3,sudo apt-get install python3.5下載pip,安裝到python2使用easy_install 安裝pip;sudo python3 -m eas

潭州課堂25班:Ph201805201 第十一課 繼承多繼承魔術方法屬性方法 (課堂筆記)

筆記 父類 當前 TE -s __del__ color true ont 繼承: class p : cls_name = ‘p‘ def __init__(self): print(‘正在實例化‘) def __del__(se

is “==” 的區別編碼解碼

數字 們的 密文 進制 -- + - 列表 的區別 == is 是指比較兩者的內存地址是否相等 “==" 是指比較兩者的值是否相等。 小數據池 數字小數據池的範圍 -5---256 字符串:字符串*20內內存地址一樣,單個*21以上,內存地址就不一樣 字符串中如果

讓HTML標籤、DIV、SPAN擁有focus事件blur事件聚焦失焦

DIV和其他普通標籤是不具有onfocus和onblur事件的。INPUT和A標籤為什麼擁有?而DIV和SPAN等普通標籤卻沒有?有時候我們習慣性用鍵盤的TAB來移動游標,仔細看你會發現,游標只在INPUT和A上跳轉。因為INPUT和A標籤具備TAB屬性。我們只需要給DIV或者SPAN等普通標籤建立TAB,這

談IO中的阻塞非阻塞同步非同步及三種IO模型

什麼是同步和非同步?        燒水,我們都是通過熱水壺來燒水的。在很久之前,科技還沒有這麼發達的時候,如果我們要燒水,需要把水壺放到火爐上,我們通過觀察水壺內的水的沸騰程度來判斷水有沒有燒開。隨著科技的發展,現在市面上的水壺都有了提醒功能,當我們把水壺插電

談IO中的阻塞非阻塞同步異步及三種IO模型

狀態 阻塞io 舉例 最大的 data- str 被調用 當我 返回 什麽是同步和異步? 燒水,我們都是通過熱水壺來燒水的。在很久之前,科技還沒有這麽發達的時候,如果我們要燒水,需要把水壺放到火爐上,我們通過觀察水壺內的水的沸騰程度來判斷水有沒有燒開。隨著科

Queue介面分析:addoffer區別removepoll方法到底啥區別

往佇列中新增元素有兩個方法,分佈是add和offer方法,兩者的入參和返回值都一樣,或許很多同學都奇怪,為什麼,要有這兩個功能類似(都是新增元素),但方法名不一樣的方法,看看原始碼的註釋 以下為Queue介面中方法定義: ``` public interface Queue<E

vue 陣列遍歷方法forEachmap的原理解析實際應用

一、前言 forEach和map是陣列的兩個方法,作用都是遍歷陣列。在vue專案的處理資料中經常會用到,這裡介紹一下兩者的區別和具體用法示例。 二、程式碼 1. 相同點 都是陣列的方法 都用來遍歷陣列 兩個函式都有4個引數:匿名函式中可傳3個引數item(當前項), index(當前項的