java基礎不牢固容易踩的坑
經過一年java后端代碼以及對jdk源碼閱讀之后的總結,對java中一些基礎中的容易忽略的東西寫下來,給偏愛技術熱愛開源的Coder們分享一下,避免在寫代碼中誤入雷區。
(注:如無特殊說明,均以jdk8為基礎,本文所有例子均已通過編譯器通過,且對輸出進行了驗證)。
1.關於基本類型的包裝類的。
基本類型boolean、char、byte、short、int、long、float、double。是java的特殊類型,特殊性在於區別於對象的存儲,對象存儲的是引用,引用指向在jvm堆中分配的值,基本類型直接存儲的就是值,能提高效率。
同時java遵循面向對象思想為每個基本類型都提供了封裝類:Boolean、Character、Byte、Short、Integer、Float、Double。
坑1:變量賦值與類型轉換。
變量賦值其實並不算是個坑,因為編譯器會自動檢查,例如long var = 2;編譯器會報錯。
類型轉換分為自動轉換和強制轉換,這里不在贅述,具體轉換規則自行查詢。
賦值的時候 = 和+=的區別,+=會自動轉換類型。
short num;
num = num + 1; //error
num += 1; //ok
坑2:計算
整數相除默認只保留整數,即使賦值給浮點類型也不行。
double d = 5 / 2;
System.out.println(d); //2
byte相加超出長度后數值會變得很怪異。
byte num = 127;
num += 1;
System.out.println(num);//-128
兩個float相加結果會存在一定的誤差等等。
float a1 = 1.001f;
float a2= 1.819f;
float a3 = a1 + a2;
System.out.println(a3);//jdk8: 2.8200002
System.out.println(12.0 - 11.9 == 0.1) //false
坑3:裝箱與拆箱
int a =100;
Integer b = 100;
Integer c= 100;
System.out.println(a==b); //true
System.out.println(b==a); //true
System.out.println(b==c); //true
基本類型和包裝類型運算時會自動拆箱,所以ab相等;
當把100換成200 b==c會返回false。因為==比較的是引用;
b==c為true的原因是Integer采用了緩存,對-128到127之間的數據不再自動生成,而是直接引用(請看Integer中的IntegerCache內部類),類似於String;
2. null值
1.null關鍵字,大小寫敏感
2.null是引用類型的默認值
4.null既不是對象也不是類型,可以強制轉換成任何引用類型。
String s = (String) null; //ok
int a = (int) null; //error
4.null值的引用類型變量,instance會返回false,如下:
Integer iAmNull = null;
System.out.println(iAmNull instanceof Integer); //false
5.null值的引用變量調用非靜態方法,會拋npe,調用靜態方法是可以的。
3 void,Void
void在邏輯上是一種數據類型,但不是基本類型,也不是引用類型。我們暫且不管它到底是什么類型,因為很多人都說不清。
void提供了包裝類Void,看源碼我們會發現它被定義成final,而且構造方法是private,也就是說不能實例化。
Void類型只能賦值為null,而void不能賦值,僅僅用來作為方法返回值輸出。
Void能作為方法輸入參數當做占位符,只能傳值為null。
4.多態
1.父類引用能指向子類對象,調用的方法具體取決於引用的對象,而不是取決於引用。
public class A {
public String show(D obj){
return ("A and D");
}
public String show(A obj){
return ("A and A");
}
}
class B extends A{
public String show(B obj)...{
return ("B and B");
}
public String show(A obj){
return ("B and A");
}
}
class C extends B{}
class D extends B{}
A a1 = new A(); A a2 = new B();
B b = new B(); C c = new C(); D d = new D(); System.out.println(a1.show(b)); ① System.out.println(a1.show(c)); ② System.out.println(a1.show(d)); ③ System.out.println(a2.show(b)); ④ System.out.println(a2.show(c)); ⑤ System.out.println(a2.show(d)); ⑥ System.out.println(b.show(b)); ⑦ System.out.println(b.show(c)); ⑧ System.out.println(b.show(d)); ⑨
結果
① A and A ② A and A ③ A and D ④ B and A ⑤ B and A ⑥ A and D ⑦ B and B ⑧ B and B ⑨ A and D
2.子類對父類方法不可見的情況下是不會覆蓋的,而是重新定義了一個方法。
3.繼承關系只有方法會覆蓋,成員變量不會被覆蓋。
public class A {
protected int i = 1;
public void show(){
System.out.println(i);
}
}
public class B extends A{
private int i = 10;
public void show(){
System.out.println(i);
}
}
A a = new A();
A a1 = new B();
B b = new B();
a.show(); //1
a1.show(); //10
b.show(); //10
5. super
super並沒有代表超類的一個引用的能力,只是代表調用父類的方法而已。
public class Test extends Number{
public static void main(String[] args) {
new Test().test();
}
private void test(){
System.out.println(super.getClass().getName()); //獲取父類方法名getClass().getSuperClass().getName();
}
}
這里應結合多態的override來理解上面的輸出。
6.字符串
老生常談的問題了,字符串采用常量池緩存,不宜創建太多字符串,subString、new、+、等操作慎用,會創建很多字符串常量無法回收,當運行久了之后會占用越多越多的內存。
字符串做參數,並不會改變改變實參的值。
7.多線程
線程安全的問題建議單獨去看。充分考慮到線程安全問題,不會出現死鎖問題。
Object提供的wait、notify、notify不建議對多線程了解不深入的人去用。
建議使用可重入鎖替代synchronized。
多線程知識較多,這里不做詳細說明。
8.異常處理
1.異常處理塊中可以繼續拋異常。
2.try塊可以不需要catch或finally,但二者必須至少有一個
3.finially塊中return 語句會覆蓋try塊中的return,finally塊在try塊代碼執行完后,return語句之前執行。
public class MyClass {
public static void main(String[] args) {
System.out.print(new MyClass().getNum()); //4
}
int getNum(){
try {
System.out.println("try block");
return 3;
} finally {
System.out.println("finally block");
return 4;
}
}
}
輸出結果是:
try block
finally block
4.碰到事務方法,異常處理要特別注意。
5.finially不一定必執行,當在try塊中有system.exit(1);
try {
System.out.println("try block begin");
System.exit(1);
System.out.println("try block end");
} catch (Exception e){
System.out.println("catch block begin");
System.exit(1);
System.out.println("catch block end");
} finally {
System.out.println("finally block");
}
以上代碼輸出try block begin
6.異常不建議往上拋,特殊情況除外。
9.正則表達式
replace、split等所有以正則表達式作為參數的方法
一定要注意正則表達式的含義,例如如下輸入
String a = "acb..";
System.out.println(a.replaceAll(".","b")); //bbbbb
轉義字符串,尤其是路徑問題
10.靜態相關
static可以修飾類(包含內部類)、成員方法、成員變量、類中代碼塊。
static只能修飾類變量,不能修飾局部變量,編譯器會報錯。
類啟動加載順序。靜態>非靜態,成員變量>代碼塊>構造方法,父類>子類。
靜態方法和靜態變量會隨類的加載而加載,靜態內部類只有在使用時才會加載。
static 不能和abstract同時使用,可以和final同時使用。
11 循環刪除
在list中刪除a,看起來一切正常。如下所示。
List<String> list = new LinkedList<>();
list.add("a");
list.add("b");
list.add("c");
for (String str : list){
if ("b".equals(str)){
list.remove(str);
}
}
System.out.println(list); //[a, c]
假如刪除a呢?ConcurrentModificationException,自己模擬下流程思考下原因。
12. 特殊關鍵字
1.volatile:一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
2)禁止進行指令重排序。
2.transient:修飾成員變量。當對象被序列化時(寫入字節序列到目標文件)時,transient阻止實例中那些用此關鍵字聲明的變量持久化。
3.strictfp:一旦使用了關鍵字strictfp來聲明某個類、接口或者方法時,那么在這個關鍵字所聲明的范圍內所有浮點運算都是精確的,符合IEEE-754規范的。
例如一個類被聲明為strictfp,那么該類中所有的方法都是strictfp的。
4.native:原生態方法,可以調用其他語言。這里不做詳細說明。
13 枚舉
1.枚舉為每個枚舉對象創建一個實例,在首次使用時初始化。
public static void main(String[] args) {
weekday mon = weekday.mon;
}
public enum weekday {
mon, tue, wes, thus, fri;
private weekday() {
System.out.println("hello"); //輸出hello五次
}
}
2.構造方法可以傳值。
public static void main(String[] args) {
weekday mon = weekday.mon;
}
public enum weekday {
mon, tue(1), wes(2), thus, fri;
private weekday() {
System.out.print("hello ");
}
private weekday(int a) {
System.out.print("ok ");
}
}
輸出:hello ok ok hello hello
14 泛型
1. 泛型類、泛型方法,用<>表示,<>內的內容只要符合變量命名規范即可,不要求是T、K、E、V
2. 泛型可以有多個變量,例如public Class MyClass<T1,T2,T3,T4>,一般1到2個。
3. Set<Integer> 不是Set<Number>的子類,邏輯上不具備任何繼承關系,二者都屬於Set類。Set<Integer>賦值給Set<Number>會報錯。
4. 上面一行的解決方式是泛型通配符。
5. 泛型的類型參數只能是引用類型,如Set<int>編譯報錯。
6. 不能對確切的泛型類型使用instance操作,
7. <T extends Number>作用於方法或者類上,而 <? extends Number> 則不可以。
8. 泛型運行期即被擦除,所以不能通過Type type = new TypeToken<TestGeneric<String>>(){}.getType(); 這種方式在運行期動態獲取泛型類型。
9. 泛型類型在邏輯上看以看成是多個不同的類型,實際上都是相同的基本類型。