弄不清楚的JAVA基礎知識
JAVA基礎知識
使用“+”可以連線兩個字串(String物件),那麼,是怎樣進行連線的?
``` public class StringTest { public static void main(String[] args) { final String s = "abc"; String x = "abc" + "def"; String y = s + "def"; String z = x + "abc"; String z1 = s + "def" + "abc";
String s1 = "black";
String s2 = "board";
String s3 = s1 + s2;
String s4 = "black" + s2;
System.out.println("s3==s4 " + (s3 == s4));
System.out.println(s4.intern() == s3.intern());
}
}
```
反編譯檢視:
```
0 ldc #2
```
結論:
當字串拼接的前後可以確定為常量(final修飾或者直接字串)則不使用stringbuilder,直接確定結果
當字串拼接前後有一個不為常量則使用stringbuilder.append拼接
隱式轉換+浮點型別
``` public static void main(String[] args) {
final short s =30;
byte b = s;
System.out.println(b);
char a = (char)b;
int i = a;
System.out.println(i);
float f1 = 3000000;
BigDecimal f1b = new BigDecimal(f1);
float f2 =f1+1;
BigDecimal f2b = new BigDecimal(f2);
System.out.println(f1==f2);
System.out.println(f1b);
System.out.println(f2b);
}
```
執行結果
30
30
false
3000000
3000001
稍微修改程式碼:
``` public static void main(String[] args) { //改為負數 final short s =-30; byte b = s; System.out.println(b);
char a = (char)b;
int i = a;
System.out.println(i);
//增加一個0
float f1 = 30000000;
BigDecimal f1b = new BigDecimal(f1);
float f2 =f1+1;
BigDecimal f2b = new BigDecimal(f2);
System.out.println(f1==f2);
System.out.println(f1b);
System.out.println(f2b);
}
```
執行結果:
-30
65506
true
30000000
30000000
byte 1位元組
short 2位元組
int 4位元組
long 8位元組
char 2位元組(C語言中是1位元組)可以儲存一個漢字
float 4位元組
double 8位元組
boolean false/true(理論上佔用1bit,1/8位元組,實際處理按1byte處理)
char型別是無符號型別(0~65535),因此char與byte(−128~127),char與short(−32768~32767)型別不存在子集關係,也就是說,char與其他兩種型別之間的轉換總是需要型別轉換。
整型資料(byte、short、char、int、long 5種類型)間的擴充套件轉換,如果運算元是有符號的,擴充套件時就進行有符號擴充套件,擴充套件位為符號位。如果運算元是無符號的,則擴充套件時進行無符號擴充套件,擴充套件位為0。整型資料間的收縮轉換,只是進行簡單的截斷,保留目標型別的有效位(即丟棄所有高位)。
float型別在Java中佔用4位元組,long型別在Java中佔用8位元組,為什麼float型別的取值範圍比long型別的取值範圍還大?
在Java中,浮點型別的結構與運算符合IEEE754標準。浮點型別使用符號位、指數與有效位數(尾數)來表示。其中,符號位用來表示浮點值的正負,指數位用來儲存指數值,有效位數用來儲存小數值。在Java中,浮點型別float與double的結構如表:
其中,符號位為0,浮點值為正,符號位為1,浮點值為負。浮點型別的指數與有效位數都是無符號的,指數採用了偏移量方式來儲存指數值,偏移量為2x −1(比實際指數大2x −1),其中x為指數域的位數,float型別為8位,double型別為11位。例如,浮點值float型別值8.1f的指數為3,在指數位中實際儲存的值為127 +3,即130。任意一個非0並且非無窮大的浮點數v都可以表示成v = s × m × 2e的形式。s為1或−1,m為有效位數(小數),e為指數。
在計算機中,所能儲存的兩個臨近小數之間的差值,就是浮點數值的間隙,我們可以使用Math類的ulp方法來取得這個間隙值,浮點之間的間隙是隨著浮點值的絕對值增大而增大的,當浮點數的絕對值很大時,間隙也會很大,對浮點數進行一個較小的增量,無法使浮點值改變。
當一個值A不能夠準確地由浮點型別(float或double)表示時,就會使用最接近的,並且可以使用浮點型別表示的值來代替值A。代替的標準採用最近舍入模式。
如果值A位於可用浮點型別表示的兩個相鄰值B與C之間
檢視間隙:
``` public static void main(String[] args) { //改為負數 final short s =-30; byte b = s; System.out.println(b);
char a = (char)b;
int i = a;
System.out.println(i);
//增加一個0
float f1 = 30000000;
/*列印f1的間隙, 在間隙除以2的閉區間內的數不會增大原始資料,即會近似取最近的資料表示。
*/
System.out.println("間隙:"+Math.ulp(f1));
BigDecimal f1b = new BigDecimal(f1);
float f2 =f1+1;
BigDecimal f2b = new BigDecimal(f2);
System.out.println(f1==f2);
System.out.println(f1b);
System.out.println(f2b);
}
```
執行結果:
-30
65506
間隙:2.0
間隙:2.0
true
30000000
30000000
i++與++i到底有什麼不同?僅僅是先加與後加的區別嗎?
``` public static void main(String[] args) { int spi=16; int spi2 =++spi; System.out.println(spi);
int sd =16;
sd=sd++;
System.out.println(sd);
}
```
執行結果:
17
16
使用javap -c 檢視:
```
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
public static void main(java.lang.String[]);
Code:
0: bipush 16 //將16壓入運算元棧
2: istore_1 //彈出運算元棧的首位並儲存在區域性變數的1位置(spi=)
3: iinc 1, 1 //區域性變數1位置進行+1操作
6: iload_1 //將區域性變數1壓入運算元棧
7: istore_2 //彈出運算元棧的首位並儲存在區域性變數的2位置(spi2=)
8: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_1
12: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
15: bipush 16 //將16壓入運算元棧
17: istore_3 //彈出運算元棧的首位並儲存在區域性變數的3位置(sd=)
18: iload_3 //將區域性變數3壓入運算元棧
19: iinc 3, 1 //將區域性變數3進行+1操作
22: istore_3 //彈出運算元棧的首位並儲存在區域性變數的3位置(sd=)
23: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
26: iload_3
27: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
30: return
}
```
從0--> 6 和 15-->19 可以得到
++i 操作是先區域性變數+1再壓棧操作,
i++ 是直接壓棧後區域性變數自+1
i++的操作直接導致操作棧數沒有進行+1操作;可以認為是個臨時變數。
所以得出: [content_hide] i++和++i並沒有賦值先後順序的說法,不管 s=i++
還是s=++i
,都是先進行自增++運算,再進行賦值;只是兩次賦值使用的值是不一樣的,一個是使用自增後的值,一個是使用臨時變數的值. [/content_hide]
例子:
```
int[] str = {0,0,0,0,0};
int index=1;
str[++index]=index++;
System.out.println(Arrays.toString(str));
```
執行結果:
[0, 0, 2, 0, 0]
雖然賦值運算子是從右向左結合的,但是運算元的確定是從左向右的,也就是在賦值操作發生前.
運算前會先將左側的運算元儲存起來,左側的運算元不會受到其右側表示式的影響而造成改變.
例子:
```
public static void main(String[] args) {
int[] str = {0,0,0,0,0};
int index=1;
str[++index]=index++;
System.out.println(Arrays.toString(str));
test1(index,++index,index=2);
test1(index=5,index++,index);
}
public static void test1(int a,int b ,int c){
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
```
執行結果
[0, 0, 2, 0, 0]342556
變數交換
一個變數x異或另一個變數y兩次,結果的值為x。
如下:
``` /* * 中間變數交換 * @param v / public void swap1(Value v) { int temp = v.x; v.x = v.y; v.y = temp; }
/**
* 加法交換,即便溢位最終結果也是正確的。
* @param v
*/
public void swap2(Value v) {
v.x = v.x+v.y;
v.y = v.x-v.y;
v.x=v.x-v.y;
}
/**
* v^y^y = x
* @param v
*/
public void swap3(Value v) {
v.x = v.x^v.y;
v.y = v.x^v.y;
v.x=v.x^v.y;
}
/**
* 減法交換
* @param v
*/
public void swap4(Value v) {
v.x = v.x-v.y;
v.y = v.x+v.y;
v.x=v.y-v.x;
}
class Value { public int x; public int y;
} ```
開關選擇表示式switch的型別內幕
- switch表示式可以是byte、short、char、int、Byte、Short、Character、Integer、String或列舉型別。
- case表示式必須是常量表達式或列舉常量名,並且其型別可以賦值給switch表示式型別
- switch表示式的型別為基本資料型別的包裝型別時,將包裝型別拆箱為基本資料型別。
- 當switch型別為列舉型別時,會建立一個匿名類來輔助完成。
- 當switch型別為String型別時,將switch語句拆分為兩個switch語句,分別對String物件的雜湊碼及臨時變數來輔助完成。
例子:
``` public static void main(String[] args) { String swstr="test";
switch (swstr){
case "test":
System.out.println("test");
break;
case "test1":
System.out.println("test1");
break;
default:
System.out.println("null");
}
}
```
使用javap -c 檢視反編譯程式碼:
```
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
public static void main(java.lang.String[]); Code: 0: ldc #7 // String test 2: astore_1 3: aload_1 4: astore_2 5: iconst_m1 6: istore_3 7: aload_2 8: invokevirtual #9 // Method java/lang/String.hashCode:()I 11: lookupswitch { // 2 3556498: 36 110251487: 50 default: 61 } 36: aload_2 37: ldc #7 // String test 39: invokevirtual #15 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 42: ifeq 61 45: iconst_0 46: istore_3 47: goto 61 50: aload_2 51: ldc #19 // String test1 53: invokevirtual #15 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 56: ifeq 61 59: iconst_1 60: istore_3 61: iload_3 62: lookupswitch { // 2 0: 88 1: 99 default: 110 } 88: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream; 91: ldc #7 // String test 93: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 96: goto 118 99: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream; 102: ldc #19 // String test1 104: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 107: goto 118 110: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream; 113: ldc #33 // String null 115: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 118: return } ```
大致意思如下:
public static void main(String[] args) {
String swstr = "test";
String swstr1 = s;
byte byte0 = -1;
switch (swstr1.hashCode()) {
case 3556498:
if (swstr1.equals("test")) {
byte0 = 0;
}
break;
case 110251487:
if (swstr1.equals("test1")) {
byte0 = 1;
}
break;
}
switch (byte0) {
case 0:
System.out.println("test");
break;
case 1:
System.out.println("test1");
break;
default:
System.out.println("null");
}
}
string 極限值解析說明
對於這個問題,就要說到Java原始檔編譯生成的class檔案。在class檔案中,使用CONSTANT_Utf8_info表來存放各種常量字串,包括String字面常量,類或介面的全限定名,方法及變數的名稱、描述符等。CONSTANT_Utf8_info表的結構如表
CONSTANT_Utf8_info表使用2位元組來表示字串的長度,因此,bytes陣列的最大長度為216−1,即65535位元組。
可是,為什麼4個字元(“A”、“á”、“字”與“㊣”)的執行結果各不相同呢?原因在於,在CONSTANT_Utf8_info表中,從“\u0001”~“\u007f”,bytes使用1位元組來表示,空字元(null,即“\u0000”)和從“\u0080”~“\u07ff”,使用2位元組來表示,從“\u0800”~“\uffff”,使用3位元組來表示,而對於增補字元,即程式碼點範圍在“U+10000”~“U+10FFFF”之間的字元,使用6位元組來表示。也可以這樣認為,增補字元是使用一個代理對來表示的,而代理對的取值範圍為“\ud800”~“\udfff”,這些字元都在“\u0800”~“\uffff”之間,每個代理字元使用3位元組表示,共6位元組。
上述的儲存是在class檔案中的實現,不要與Java程式中的字元相混淆,對於Java程式來說,“A”、“á”、“字”都使用一個char型別變量表示,即2位元組,而“[插圖]”(增補字元)使用兩個char型別變量表示,即4位元組。
String字面常量的最大長度與String在記憶體中的最大長度是不一樣的,後者的最大長度為int型別的最大值,即2147483647,而前者根據字元(字元Unicode值)的不同,最大長度也不同,最大長度為65534(可手動修改class檔案,令輸出結果為65535)。
String字面常量的最大長度是由CONSTANT_Utf8_info表來決定的,該長度在編譯時確定,如果超過了CONSTANT_Utf8_info表bytes陣列所能表示的上限,就會產生編譯錯誤。
==與equals
-
從Object類繼承的equals方法與“==”運算子的比較方式是相同的。如果繼承的equals方法對我們自定義的類不適用,則可以重寫equals方法。
-
重寫equals方法的時候,需要遵守5點規定,否則該類與其他類(例如實現了Collection介面或其子介面的類)互動時,很可能產生不確定的執行結果。
- 自反性。對於任何非null的引用值x,x.equals(x)應返回true。
- 對稱性。對於任何非null的引用值x與y,當且僅當:y.equals(x)返回true時,x.equals(y)才應返回true。
- 傳遞性。對於任何非null的引用值x、y與z,如果x.equals(y)返回true,並且y.equals(z)返回true,那麼x.equals(z)也應返回true
- 一致性。對於任何非空引用值x與y,假設物件上equals比較中的資訊沒有被修改,則多次呼叫x.equals(y)始終返回true或始終返回false。
- 對於任何非空引用值x,x.equals(null)應返回false。
-
在重寫equals方法的同時,也必須要重寫hashCode方法。否則該類與其他類(例如實現了Map介面或其子介面的類)互動時,很可能產生不確定的執行結果。
-
重寫hashCode方法時也要遵守3點規定,其中第3點規定是建議性的。
- 在Java應用程式執行期間,如果在物件equals方法比較中所用的資訊沒有被修改,那麼在同一物件上多次呼叫hashCode方法時,必須一致地返回相同的整數。但如果多次執行同一個應用時,不要求該整數必須相同。
- 如果兩個物件通過呼叫equals方法是相等的,那麼這兩個物件呼叫hashCode方法必須返回相同的整數。
- 如果兩個物件通過呼叫equals方法是不相等的,不要求這兩個物件呼叫hashCode方法必須返回不同的整數。但是,程式設計師應該意識到對不同的物件產生不同的雜湊碼值可以提高雜湊表的效能。
字面常量到String常量池
- String類維護一塊特殊的區域,稱為常量池。因為String物件是不可改變的,因此沒有必要建立兩個相同的String物件。只需將String物件加入常量池,在需要的時候取出,這樣即可實現String物件的共享
- 在程式中出現String編譯時常量(String字面常量與String常量表達式)時,會自動呼叫intern方法,如果常量池中含有相等的String物件,則直接返回常量池中的物件,否則將物件加入常量池中並返回該物件。
- 對於執行時建立的String物件(非String編譯時常量),會分配到堆中,系統不會自動呼叫intern方法拘留該物件,不過我們依然可以自行呼叫該物件的intern方法對該物件進行拘留。
過載
-
當兩個或多個方法的名稱相同,而引數列表不同時,這幾個方法就構成了過載。過載方法可以根據引數列表對應的型別與引數的個數來區分,但是,引數的名稱、方法的返回型別、方法的異常列表與型別引數不能作為區分過載方法的條件。
-
究竟選擇哪個方法呼叫,順序是這樣的
- 在第1階段,自動裝箱(拆箱)與可變引數不予考慮,搜尋對應形參型別可以匹配實參型別並且形參個數與實參個數相同的方法。
- 如果在步驟1中不存在符合條件的方法,在第2階段,自動裝箱與拆箱將會執行。
- 如果在步驟2中不存在符合條件的方法,在第3階段,可變引數的方法將會考慮。
- 如果3個階段都沒有搜尋到符合條件的方法,將會產生編譯錯誤。如果符合條件的方法多於一個,將會選擇最明確的方法。最明確的方法定義為:如果A方法的形參列表型別對應的都可以賦值給B方法的形參列表型別,則A方法比B方法明確。如果無法選出最明確的方法,則會產生編譯錯誤
-
當方法的引數型別是型別變數時,可以首先將型別變數進行擦除,然後與普通型別的呼叫規則相同。
-
法過載不同於方法重寫。呼叫哪個過載方法是根據實參的靜態型別(編譯時型別)決定的,與執行時實參的具體型別無關。
重寫
- 方法重寫不同於方法過載,方法過載是根據實參的靜態型別來決定呼叫哪個方法,而重寫是根據執行時引用所指向物件的實際型別來決定呼叫哪個方法。
- 在方法是靜態還是例項方面,方法重寫要求父類與子類的方法都是例項方法,如果其中有一個方法是靜態方法,則會產生編譯錯誤,如果兩個方法都是靜態方法,沒有編譯錯誤,但這種情況是方法隱藏,不是方法重寫。
- 在方法簽名方面,方法重寫要求子類方法簽名是父類方法簽名的子簽名。
- 在方法的返回型別方面,方法重寫要求子類方法返回型別是父類方法返回型別的可替換型別。
- 在方法的返回型別方面,方法重寫要求子類方法返回型別是父類方法返回型別的可替換型別。
- 在方法的異常列表方面,方法重寫要求子類方法不能比父類方法丟擲更多的受檢異常(但可以丟擲更多的非受檢異常),否則就會在呼叫方法的位置無法成功捕獲。
- 在方法的繼承方面,方法重寫要求子類繼承了父類的方法,即父類的方法在子類中必須是可訪問的。如果子類沒有繼承父類的方法,則父類的方法在子類中不可訪問,自然也就不可能重寫父類的方法。
方法與成員變數的隱藏
- 靜態方法不能重寫,只可以隱藏。
- 成員變數也不能重寫,只可以隱藏。相對於方法的隱藏,成員變數的隱藏只要求父類與子類的成員變數名稱相同,並且父類的成員變數在子類中可見即可。與成員變數的訪問許可權、型別、例項變數還是靜態變數無關。
- 重寫與隱藏的本質區別是:重寫是動態繫結的,根據執行時引用所指向物件的實際型別來決定呼叫相關類的成員。而隱藏是靜態繫結的,根據編譯時引用的靜態型別來決定呼叫相關類的成員。換句話說,如果子類重寫了父類的方法,當父類的引用指向子類物件時,通過父類的引用呼叫的是子類的方法。如果子類隱藏了父類的方法(成員變數),通過父類的引用呼叫的仍然是父類的方法(成員變數)。
構造方法
構造器,也稱構造方法,用來初始化類的例項成員變數,在使用new關鍵字建立物件的時候,由系統自動呼叫。構造器必須與類名相同,並且沒有返回值,在外觀上與類中宣告的方法相似,例如,也可以具備形式引數、型別變數、異常列表等。然而,構造器不是方法,也不是類的成員。
- 構造器不是方法,也不是類的成員。因此,子類不能繼承父類的構造器
- · 構造器是遞迴呼叫的,子類的構造器會呼叫父類的構造器,直到呼叫到Object類的構造器為止。
- 構造器沒有建立物件,構造器是使用new建立物件時由系統自動呼叫的,用來初始化類的例項成員。從順序上來說,是先建立物件,然後才呼叫構造器的。
- 當類中沒有顯式地宣告構造器時,編譯器會自動新增一個無參的構造器,該構造器的訪問許可權與類的訪問許可權相同。預設的構造器體並不為空,該構造器會呼叫父類的無參構造器,並可能執行例項成員變數的初始化。
- protected構造器與包訪問許可權構造器是不同的,前者可以在子類的構造器中使用super來呼叫,而後者不能。
- 在構造器或是例項方法呼叫的時候,會將其所關聯的物件作為第1個引數隱式傳遞,這個物件就是我們在構造器或例項方法中使用的當前物件this,靜態方法沒有關聯物件,因此也不會隱式傳遞物件。
``` public static void main(String[] args) { Object o = new Object(); NullCall nullCall = null; nullCall.m(); }
class NullCall{ public static void m(){ System.out.println("m()"); } }
```
執行結果:
``` m()
```
成員變數不同的初始化方式
- 成員變數在建立時,系統會為其分配一個預設值,布林型別為false,字元型別為‘\u0000’,整數型別為0,浮點型別為0.0,引用型別(包括陣列型別)為null。區域性變數不管是什麼型別的,都無預設值,在使用區域性變數的值時一定要先對區域性變數進行初始化。
- 例項變數可以在宣告處初始化,也可以在例項初始化塊或構造器中初始化。靜態變數可以在宣告處或靜態初始化塊中初始化
- 當子類繼承父類的例項變數x,如果子類沒有隱藏變數x,則對於同一個物件,只存在一個變數x,即通過this.x與super.x訪問的是同一個變數。如果子類隱藏變數x,則通過this.x與super.x訪問的將不再是同一個變數。
- 當子類繼承父類的靜態變數x,如果子類沒有隱藏變數x,則x由父類以及所有子類所共享,無論是通過類名(父類或子類)還是物件名(父類物件或子類物件)訪問的x,都是同一個變數。如果子類隱藏變數x,則通過父類(父類名或父類物件)訪問的x與通過子類(子類名或子類物件)訪問的x將不再是同一個變數。
初始化順序和向前引用
- 初始化的順序可以簡單總結為先靜態,後例項,先父類,後子類。對於靜態初始化,按照靜態變數宣告處初始化與靜態初始化塊在類中出現的順序執行。對於例項初始化,按照例項變數宣告處初始化與例項初始化塊在類中出現的順序執行,然後執行構造器。
- 當心潛在的向前引用,如果使用一個尚未初始化的變數值,就可能得到錯誤的結果。
- 在構造器中不要呼叫可由子類重寫的方法,呼叫private與final的方法才是安全的。
- 對於值為編譯時常量的final變數,可以認為這樣的變數會最先得到初始化,我們在程式中無法觀察到其預設值,即使向前引用這種型別的變數也是如此。
向前引用例子:
``` public static void main(String[] args) { ParentX parentX = new SubToY(); }
class ParentX{ public String kind ="parent";
@Override
public String toString(){
return kind;
}
public ParentX(){
System.out.println(toString());
}
}
class SubToY extends ParentX{ public String color ="sub"; public String kind ="sub"; @Override public String toString(){
return "super.kind = "+super.toString()+" ,this.color="+color+" ,this.kind="+kind;
}
}
```
執行結果:
``` super.kind = parent ,this.color=null ,this.kind=null
```
載入,連結,初始化
- 可以呼叫ClassLoader類的loadClass方法載入一個類(介面),類(介面)在載入後不會初始化。
- 類的載入使用的是雙親委派模型,即當類載入器載入某個類時,首先委派雙親載入器載入該類。
- Java類庫中的類是由啟動類載入器所載入的,而我們自定義的類通常是由系統類載入器所載入的。
- 類中的例項變數宣告處初始化與例項初始化塊可以認為被複制到構造器最上方執行,編譯器會為類中的每個構造器生成一個<init>方法,也會為靜態初始化(包括靜態變數宣告處初始化與靜態初始化塊)生成一個<clinit>方法(如果存在靜態初始化語句)。
- 理解類與介面初始化的時刻,在什麼情況下初始化,在什麼情況下不會初始化。
- 分清主動使用與被動使用,被動使用的時候,不會初始化被動使用關聯的類(介面)。
作者:gschaos
連結:http://juejin.cn/post/7210958340712841271
來源:稀土掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。