Java學習筆記整理java
本文檔是我我的整理的,首先是想經過完成本文檔更加紮實本身的基礎增強對java語言的理解,而後就是想給入了門的同志們作下貢獻。程序員
固然,本文檔主要是對java語言基礎(固然還有不少基礎沒有涉及到)的進行較全面的理解已經整理,只要充分掌握了基礎知識,學習高級部分的知識才會事半功倍猶如輕車熟路通常容易上手。算法
正文:數組
CLASSPATH的設置:咱們知道運行java必需要設置CLASSPATH環境變量,可是sun java 1.4以後改進了設計,JRE會自動搜索當前路徑下的類文件,並且使用java的編譯和運行工具時,系統會自動加載dt.jar和tools.jar文件中的類,所以再也不須要設置該環境變量。緩存
固然,若是你設置了CLASSPATH環境變量就必須設置全,好比包含當前路徑,不然將會出現找不到類的問題。安全
Java程序的源文件必須與public類的類(接口)名相同,若是源文件全部類都沒有使用public修飾,則文件名能夠是任意合法的文件名,所以,一個Java源文件裏最多自能定義一個public類(接口)。閉包
運算符的結合性和優先級ide
只有 單目運算符,賦值運算符和三目運算符是從右向左結合的,其他全是左結合的。函數
運算符優先級工具
運算符說明 java運算符
分隔符 . [] () {} , ;
單目運算符 ++ -- ~ !
強制類型轉換運算符 (type)
四則運算符和求餘運算
移位運算符 << >> >>>
關係運算符 < <= > >= instanceof
等價運算符 == !=
按位與 &
按位異或 ^
按位或 |
條件與 &&
條件或 ||
三目運算符 ?:
賦值運算符 = += -= /= *= &= |= ^\ <<= >>= >>>= %=
break continue 後跟標籤,能夠直接結束其外層循環。 return 用於結束整個方法。
數組
定義數組時不能指定數組的長度。
foreach
當使用foreach 來迭代訪問數組元素時,foreach中的循環變量至關於一個臨時變量,整個臨時變量並非數組元素,它只是保存了數組元素的值(深複製),所以若是但願改變數組元素的值,則不能使用這種foreach循環。
棧內存和堆內存之分
當一個方法執行是,每一個方法都會創建本身的內存棧,在這個方法內定義的變量將會逐個放入這塊棧內存裏,隨着方法的執行結束,這個方法的內存棧也將被銷燬,所以,全部在方法定義的變量都是放在棧內存中的。
當咱們在程序中建立一個對象時,這個對象將被保存到運行時數據區中,這個運行時數據區就是堆內存。只有當一個對象沒有任何引用變量引用它時,纔會在合適的時機回收它。
面向對象
this能夠表明任何對象,當this出如今某個方法體中時,它所表明的對象是不肯定的,但它的類型是肯定的,它所表明對象只能是當前類;只有當這個方法被調用時,他所表明的對象才能被肯定下來。
static 修飾的方法中不能使用this引用,因此static修飾的方法中不能訪問沒有使用static修飾的普通成員。static 用來修飾方法和屬性等成員。局部成員的上已經程序單元是方法不是類,使用static修飾它們是沒有意義的,因此局部成員都不能使用static修飾,
在構造器中使用this引用時,this老是引用該構造器正在初始化的對象。
方法自己是指令的操做碼部分,保存在stack中,方法內部變量做爲指令的操做數部分,跟着指令的操做碼以後,保存在stack中(實際是簡單類型保存在stack中,引用類型在stack中保存地址,在heap中保存值)。
對象實例以及非靜態屬性保存在heap中的,而heap必須經過stack中的地址指針纔可以被指令(類的方法)訪問到。靜態屬性保存在stack中(這是網上找到的說,筆者認爲應該是靜態屬性的地址保存在stack中)。
非靜態方法有一個隱含的傳入參數,該參數是JVM給的,和咱們的代碼無關,這個隱含參數就是對象實例在stack中的地址指針(this)。所以非靜態方法(在stack中的指令代碼)老是能夠找到本身專用數據(在heap中的對象屬性值)。固然非靜態方法也必須得到該隱含參數,所以在調用非靜態方法以前,必須先new一個對象實例,得到stack中地址的指針,不然JVM將沒法將隱含參數傳給非靜態方法。而靜態方法無需此隱含參數,所以不須要new對象,只要class文件被ClassLoader load進入JVM的stack,該方法便可被調用,固然此時靜態方法存取不到heap中的對象屬性的。
造成長度可變的方法
public void test(int a, String… books){}
能夠傳入多個字符串參數做爲參數值,實際上是能夠看着參數數組。但一個方法只能有一個長度可變的造成且位於形參列表的最後。
方法重載
方法的重載要求:兩同(同一個類中的方法名相同),一不一樣(參數列表不一樣)(不建議使用長度可變的形參重載方法),跟方法的其餘部分(返回值類型(有時咱們調用方法時不須要返回值,就不能根據返回值類型來肯定究竟是調用哪一個方法),修飾符等)沒有任何關係。
系統在第一次使用類加載類,並初始化類。類屬性從這個類準備階段起開始存在。系統不會爲局部變量執行初始化,局部變量在訪問以前必定要肯定是已經初始化。
模塊設計追求高內聚(儘量把模塊的內部數據,功能實現細節隱藏在模塊內部獨立完成,不容許外部直接干預),低耦合(僅暴露少來的方法給外部使用)
構造器
若是咱們提供了自定義的構造器,系統再也不提供默認的構造器,一般咱們都保留無參數的默認構造器。
多態性
當編譯時類型和運行時類型不一致,就會出現所謂的多態。
當把子類對象賦給父類引用變量時,被稱爲向上轉型,這個老是能夠成功的,但把一個父類對象賦給一個子類引用變量時,就須要強制類型轉換,並且還可能在運行時產生ClassCastException異常(當這個父類對象編譯時類型爲父類類型,運行時類型是子類類型纔是正確的),使用instanceof運算符可讓強制類型轉換更安全。
Instanceof 運算符
a instanceof B a必須是具體的實例,B是一種類(或接口),能夠是數組
若是A是編譯時可以肯定具體的類型,那麼Instanceof 運算符前面操做數的編譯時類型要麼與後面類(接口,抽象類)相同,要麼是後面類的父類,(便可以經過類型轉換(B)a到B的,不能經過(B)a轉換到B的能夠,轉換到它們共有的父類如(Object)a,但仍返回false),不然會引發編譯錯誤。若是要運行時才肯定類型(如何調用函數getObject()返回對象等,但new String()這個在編譯時就會肯定下類型,由於String Integer等類被final修飾,因此編譯時就肯定類型),沒有這樣的限定。
B不能是肯定的泛型參數的泛型,能夠是 List或者 List<?>
Instanceof 運算符前面的對象是不是後面的類,或者其子類。實現類的實例。若是是,就返回true,不然返回false,若a是null則返回false。
通過instanceof 運算符判斷一個對象是否能夠強制類型轉換,而後在使用(type)運算符進行強制轉換,從而保證程序不會出現錯誤。
初始化塊,構造器質性順序
當Java建立一個對象時,系統先爲該對象的全部實例屬性分配內存(前提是該類已經被加載過了),接着程序開始對這些實例屬性執行初始化,其初始化順序是:先執行初始化塊或聲明屬性是指定的初始化值,而後執行構造器裏指定的初始值。
類初始化階段:(類初始化只執行依次,而後會在虛擬機一直存在)
先執行最頂層父類的靜態初始化塊,依次向下,最後執行當前類的靜態初始化塊
對象初始化階段:(每次建立實例對象是都要進行)
先執行最最頂層父類的初始化塊,構造器,依次向下,最後執行當前類初始化塊。
構造器。
初始化塊和聲明指定初始化值,他們的執行順序與源代碼中的排列順序相同。
基本類型之間的轉換
Xxx parseXxx(String s) 將字符串類型轉換成基本類型(除了Character以外)
String 中的valueOf() 將基本類型轉換成字符串。
==和equals
== 判斷兩個變量是否相等是,若是2個變量是基本類型的變量,且都是數值型(不必定要求數據類型嚴格形同),則只要兩個變量值相等,就返回true;但當判斷兩個引用類型的變量,必須它們指向同一個對象是,== 判斷纔會返回true。
equals 在比較字符串是隻要兩個字符串引用變量的指向內容相等就返回true(String類已經重寫了Object的equals方法),自定義的類要想達到這樣的效果,必須重寫Object的equals方法(自定義標準),不然用這個方法判斷兩個對象相等的標準與 == 符號是沒有區別的。
單例類(Singleton)
一個類只能建立一個實例
eg:
class Singleton
{
private static Singleton instance;
//隱藏構造器
private Singleton(){}
//提供一個靜態方法,用於返回Singleton實例
//該方法能夠加入自定義的控制,保證只產生一個Singleton對象
public static Singleton getInstance()
{
if(instance==null)
{
Instance=new Singleton();
}
return instance;
}
}
這樣用 getInstance返回的對象始終都是同一個。
final 修飾符
當final修飾引用類型變量時,final只保證這個引用所引用的地址不會改變即一直引用同一個對象,但這個對象能夠發生改變。
若是final修飾的變量是基本數據類型,且在編譯時就能夠肯定該變量的值,因而能夠把該變量當成常量處理,若是是修飾引用數據類型,就沒法在編譯時就得到值,而必須在運行時才能獲得值。
成員變量是隨着類初始化或對象初始化而初始化的。當類初始化時,系統會爲該類的類屬性分配內存,並分配默認值;當建立對象時,系統會爲該對象的和私立屬性分配內存,並分配默認值。
類屬性:可在靜態初始化塊中,聲明該屬性時指定初始值
實例屬性:可在非靜態初始化塊中,聲明該屬性、構造器中指定初始值
並且上面初始化操做要其只能被初始化一次。(必須由程序員顯示初始化)
若是在構造器,初始化塊中對final成員變量進行初始化,則不要在初始化以前訪問final成員變量的值,不然會出現「可能未初始化變量」的錯誤。(系統不會進行隱式初始化)。
final 修飾的方法只是不能被重載,並非不能被重寫。
抽象類
抽象方法不能有方法體 abstract void String getName();
只要含有抽象方法(直接定義了一個抽象方法;繼承了一個抽象父類,但沒有徹底實現父類包含的抽象方法;以及實現一個接口,但沒有徹底實現接口包含的抽象方法)的類就是抽象類
抽象類能夠包含普通類的成分,可是必須有抽象方法。
abstract 不能用於修飾屬性和構造器,private 和 abstract 不能同時使用。
更完全的抽象:接口
修飾符能夠是public或者省略,省略了public 訪問控制符,則只有在相同包結構下才能夠訪問該接口。
一個接口能夠有多個直接父接口,但不能繼承類。
一個接口不能包含構造器和初始化塊定義,能夠包含屬性(只能是常量),方法(只能是抽象方法),內部類,內部接口,和枚舉類定義。
定義接口成員是,能夠省略訪問控制修飾符,若是指定訪問修飾符,只能使用public修飾符。在接口定義屬性時,無論是否使用 public static final修飾符,接口裏的屬性老是使用者三個修飾符來修飾。一樣接口老是使用public abstract 來修飾。接口不可以使用static修飾接口裏定義的方法。同理接口老是使用public static對內部類(靜態內部類),內部接口和枚舉類進行修飾。
內部類
內部類至關外部類定義的一個普通方法,能夠訪問外部類的私有數據和方法。當內部類和外部類的成員或者方法出現重名時,可使用this和外部類名.this來區分。外部類就至關一個命名空間。
非靜態內部類
非靜態內部類對象必須寄存在外部類對象裏,故要建立一個非靜態內部類必定要有經過外部了對象。在靜態方法中不能直接經過new 內部類了建立內部類對象(沒有外部類對象)(相似靜態方法不能調用非靜態方法以及訪問非靜態屬性)。
非靜態內部類裏不能有靜態方法,靜態屬性,靜態初始化塊,能夠有普通初始化塊。
靜態內部類
靜態內部類至關於一個外部類的一個靜態方法。
內部類的使用
Out.In in=new Out().new In();
當繼承內部類是必定要有外部類對象來調用內部類的構造器。
局部內部類
局部內部類無需使用訪問控制閥和static修飾符修飾
匿名內部類
匿名內部了不能是抽象類,不能定義構造器,由於匿名內部類沒有類名,因此沒法定義構造器,但能夠定義實例初始化塊。
枚舉類(省略)
泛型
當使用泛型接口、父類時不能再包含類型形參,如繼承帶泛型的父類、接口要傳入世界參數類型。 如 public class A extends B<String>。
ArrayList<String>並無生產新的類,系統不會生產真正的泛型類,如應用 instance instanceof List<String>編譯時引發錯誤:instanceof 運算符後不能使用泛型類。
List<String>對象不能被當成List<Object>對象使用,也就是說List<String>類並非List<Object>類的子類。如 ArrayList<Number> nList=new ArrayList<Integer>(); 會引發編譯錯誤。
這點與數組不一樣,若是Apple 是 Fruit 的子類,則 Apple[] 依然是 Fruit[]的子類
類型通配符
爲了表示各類List的父類,使用類型通配符「?」,可是並不能把元素加入到其中(不然會引發編譯錯誤),由於咱們並不知道元素的類型,惟一例外的是null,它是全部引用類的實例。其次,咱們使用get()方法返回List<?>集合指定索引的元素,其返回值是一個未知類型,當能夠確定的是,它是一個Object類型。前面咱們使用List<E> 使用add 的參數必須是E類的對象或者是子類的對象。
設定通配符的上限
List<? Extends Fruit> 一樣此處咱們使用add參數類型是 ?extends Fruit 它表示 Fruit未知的子類,咱們沒法準確知道這個類型是什麼,因此咱們沒法將任何對象添加到這種集合中。
設定類型形參的上限
class A <T extends Fruit>
更極端 class A<T extends Number &java.io.Serializable>
泛型方法
public <T >void add (T a, List<T> list)
{
t.add(a);
}
若是無需向泛型集合添加元素,或者修改泛型集合的元素時,可使用類型通配符,無需使用泛型方法。
設定通配符的下限
List<? super Type> 它表示它必須是Type自己或者Type的父類。
泛型的擦除和轉換
擦除,當把一個具備泛型形象的對象賦給另外一個沒有泛型信息的變量時,則全部在尖括號直接的類型信息都被扔掉,如List<String> 類型被轉換成List,則該List對幾乎元素的類型檢查變成了類型變量的上限(即Object),當再次 String s =list.get(0);將會引發編譯錯誤(要強制轉換)。
轉換,
List<Integer> li=new ArrayList<Integer>();
li.add(6);
//編譯運行正常,只是引發「未經檢查的轉換」的警告
List list=li; //泛型擦除
List<String> ls=list;
System.out.println(ls.get(0)); //將引發運行時異常
//一樣會引發運行時異常,不能進行類型轉換
System.out.println((String)li.get(0));
List<?> lis=li;
System.out.println(li.get(0)); //OK
Lis.add(new Integer(6)); //error
泛型數組
數組元素的類型不能包含類型變量或類型形參,除非是無上限的類型通配符。
List<String>[] la=new List<String>[10]; //這是不容許的
但能夠
List<String> la=new ArrayList[10]; //編譯時有「未經檢查的轉換」警告
使用時候注意不要 引發「ClassCastException」異常。
異常處理
進行異常捕獲的時候必定要先捕獲小的異常,而後在捕獲大的異常。
在finally塊中使用了return或throw的語句,將會致使try塊、catch塊中的return,throw 語句失效。
當java程序執行try塊、catch塊時遇到return throw語句(沒有System.exit(1);語句),這兩個語句或致使該方法當即結束,因此係統並不會當即執行這個兩個語句,而是去尋找該異常處理流程中是否包含finally塊,若是有finally塊,只有當finally塊執行完畢,系統纔會再次跳回來執行try塊、catch塊裏的return或throw語句。
數組與內存控制
數組靜態初始化:
String[] books=new String[]{「wo shi 「,」zhongshan」,」daxue」};
String[] books={「wo shi 「,」zhongshan」,」daxue」};
數組動態初始化:
String[] books=new String[5];
執行動態初始化時系統將爲數組元素分配默認初始值。
不要同時使用靜態初始化和動態初始化。
經過其餘數組賦值初始化:
String[] books1=books;
數組變量是一種引用類型的變量(存儲的是數組的引用),數組變量自己存儲在棧區,指向的內容是指向堆區。在Java中對象的引用自己不須要進行初始化,而對象自己才須要進行初始化。
全部局部變量都是放在各自的方法棧內存裏保存的,但引用類型變量所引用的對象則老是存儲在堆內存中的。只要程序經過引用類型變量訪問屬性或者調用方法,則該引用類型將被其所引用的對象代替,即要進行初始化。
int[] sArra=null;
System.out.println(sArra) //這是沒有問題的,輸出的是null
對象與內存控制
實例變量和類變量
static不能修飾外部類,局部變量,局部內部類
//下面代碼將提示:非法向前引用
int num1=num2+1;
int num2=3;
上面代碼變量同時用 static 修飾一樣會出現上述問題。
但
//下面代碼正常
int num1=num2+1;
static int num2=3;
這說明類變量的初始化時間老是比實例變量以前。
使用javap –c 類名 能夠獲得該類初始化的過程,當咱們隊一個類初始化過程不是很清楚的時候不妨嘗試下該命令。
實例變量的初始化時機
每次建立Java對象的時都會爲實例變量分配內存空間,並對實例變量執行初始化
實例變量初始化有三種方式
定義實例變量時指定初始值;
非靜態初始化塊中對實例變量指定初始值;
構造器中對實例變量指定初始值
其實前兩種初始化執行順序和它們在源代碼中位置排列相同,也就擁有想多等級初始化時機,但都比第三種初始化方式更早。
實際中實例變量初始化 指定初始值和初始化塊 當通過編譯器處理後,它們都會被提取到構造器中。
例如:
double a=24;
實際上會被分紅以下2步執行:
(1)double a; 建立Java對象時系統根據該語句爲該對象分配內存
(2)a=24; 這條語句是被提取到Java類的構造器中執行
public class JavapToolTest
{
//初始化快中爲count實例變量指定初始值
count = 12;
}
int count = 20;
//定義構造器
public JavapToolTest()
{
System.out.println(count);
}
使用javap –c 命令能夠獲得以下代碼:結果就一目瞭然了
C:\Users\D.S.Qiu\Desktop\codes\codes\02\2.1>javap -c JavapToolTest
Compiled from "JavapToolTest.java"
public class JavapToolTest {
int count;
public JavapToolTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: aload_0
5: bipush 12
7: putfield #2 // Field count:I
10: aload_0
11: bipush 20
13: putfield #2 // Field count:I
16: getstatic #3 // Field java/lang/System.out:Ljava/
io/PrintStream;
19: aload_0
20: getfield #2 // Field count:I
23: invokevirtual #4 // Method java/io/PrintStream.printl
n:(I)V
26: return
類變量初始化時機
類變量初始化的方法
定義類變量時指定初始值;
靜態初始化塊中對類變量指定初始值
初始化時機與實力變量初始化時機相似。
對沒有給出初始化的變量系統總分配默認值。
父類構造器
當建立Java對象時,程序總會先依次調用每一個父類的非靜態初始化塊,父類構造器執行初始化,最後才調用本類的非靜態初始塊,構造器執行初始化。
隱式調用和顯示調用
若是子類構造器中沒有使用super調用父類構造器,將會隱式調用父類無參數的構造器。
super 和this調用構造器最多隻能使用其中一者,並且必須做爲構造器代碼的第一行。
訪問子類對象的實例變量
下面看一個極端的例子
class Base
{
//定義了一個名爲i的實例變量
private int i = 2;
public Base()
{
this.display();
}
public void display()
{
System.out.println(i);
}
}
//繼承Base的Derived子類
class Derived extends Base
{
//定義了一個名爲i的實例變量
private int i = 22;
//構造器,將實例變量i初始化爲222
public Derived()
{
i = 222;
}
public void display()
{
System.out.println(i);
}
}
public class Test
{
public static void main(String[] args)
{
//建立Derived的構造器建立實例
new Derived();
}
}
最終輸出的竟是 0 。實際上,構造器只是負責對Java對象實例變量執行初始化(賦初始值),但在執行構造器以前,該對象所佔的內存已經被分配下來了,只是默認初始值(空值)。
程序在執行Derived類構造器以前首先會執行父類Base構造器(固然Object類在此以前是必定會被執行),此時Base中的 i 已經指定初始值2,而後執行構造器中的 this.display(); 關鍵是此處this 表明誰,是Base仍是Derived
爲此咱們能夠在Base類構造器中添加以下代碼:
System.out.println(this.i);
System.out.println(i);
其實結果都是輸出 2,但在Derived的display()函數中添加上述代碼,輸出結果都是0,這代表訪問成員變量只有所處的類有關,但爲何調用this.display();卻輸出 0 。
其實咱們知道:this在構造器中表明正在初始化的Java對象,而咱們易知此時正在初始化對象時Derived對象——是Derived()構造器隱式調用了Base構造器的代碼,故得出this表明Derived對象,而不是Base對象。咱們能夠用System.out.println(this.class)來驗證咱們的結論。
但因爲上述this位於Base的構造器中的,因此該this雖然表明的是Derived對象可是它的編譯類型仍然是Base,只是實際引用了一個Derived對象(運行時類型)。
咱們知道當變量編譯時類型和運行時類型不一致時,經過該變量訪問其引用對象的實例屬性(成員變量)時,該實例屬性的值由訪問該屬性的所處的類決定(不管是否使用this),但調用實例方法時,該方法行爲是由它的實際引用的對象類型(運行時類型)決定。
此外,若是咱們在Derived類中定義了一個getI()方法 而後在Base類的構造器中調用this.getI()方法時會報錯,由於this的編譯類型是Base類型不能調用getI()方法而沒法經過編譯。
調用子類重寫的方法
參考下面程序代碼
class Animal
{
//desc實例變量保存對象toString方法的返回值
private String desc;
public Animal()
{
//調用getDesc()方法初始化desc實例變量
this.desc = getDesc(); //(3)
}
public String getDesc()
{
return "Animal";
}
public String toString()
{
return desc;
}
}
public class Wolf extends Animal
{
//定義name、weight兩個實例變量
private String name;
private double weight;
public Wolf(String name , double weight)
{
//爲name、weight兩個實例變量賦值
this.name = name; //(3)
this.weight = weight;
}
//重寫父類的getDesc()方法
@Override
public String getDesc()
{
return "Wolf[name=" + name + " , weight="
+ weight + "]";
}
public static void main(String[] args)
{
System.out.println(new Wolf("灰太郎" , 32.3)); //(1)
}
}
固然是先初始化Wolf對象,在執行(2)以前先調用父類的無參數構造函數,this.desc=getDesc(); 執行getDesc()函數根據前面,咱們知道調用的是子類Wolf的方法,但此時Wolf的name和weight尚未執行初始化(只是默認空值),故會輸出 Wolf[name=null,weight=0];
爲了不這種問題的發生,咱們能夠這樣定義Animal的toString()方法
class Animal
{
public String getDesc()
{
return 「Animal」;
}
public String toString()
{
return getDesc();
}
}
這樣就子類Wolf調用父類toString()的方法時,只要重寫getDesc()方法就能夠獲得想要的結果。
父子實例的內存控制
繼承成員變量和繼承方法的區別參照以下代碼
class Base
{
int count = 2;
public void display()
{
System.out.println(this.count);
}
}
class Derived extends Base
{
int count = 20;
@Override
public void display()
{
System.out.println(this.count);
}
}
public class FieldAndMethodTest
{
public static void main(String[] args)
{
Derived d=new Derived();
//聲明一個Base變量,並將Derived對象賦給該變量
Base bd = new Derived();
//直接訪問count實例變量和經過display訪問count實例變量
System.out.println(bd.count);
bd.display();
//讓d2b變量指向原d變量所指向的Dervied對象
Base d2b = d;
//訪問d2b所指對象的count實例變量
System.out.println(d2b.count);
System.out.println(((Derived)d2b).count);
d2b.display();
}
}
輸出結果是 2 20 2 20 20
其實我知道 d ,db, d2b實際指向的是一個Derived對象,而這個對象分別存儲了父類和本身的count的值。
此外 若是咱們敲下:Derived dd=(Derived)new Base();運行時會出現類型轉換錯誤。(編譯能夠經過,運行時會報錯)。
另外,咱們經過javap –c 命令能夠知道 編譯器在處理(繼承下來的)方法和變量時的區別,對於父類的變量子類不會有保留,這使得子類能夠擁有和父類同名的實例變量(經過對象名直接訪問屬性時是由編譯類型決定,在函數內訪問與屬性所處的類名有關),但若是子類重寫了父類的方法,就意味着子類定義的方法完全覆蓋了父類的同名方法了(調用函數是由運行時類型決定)。
內存中子類的實例
上面程序中 Derived d=new Derived();
Base d2b=d;
其實就只建立了一個Derived對象,可是這個Derived對象不只保存了在Derived類中定義的全部實例變量,還保存了它全部父類所定義的所有實例變量。
對於this,我想總結一個更爲深入的結論就是,this到底表明什麼對象跟運行時哪一個對象執行this所在的方法,而與this所在的方法位置沒有直接的關係。Java能夠經過return this 返回調用當前方法的Java對象(子類調用父類的方法(若方法返回值是return this,則返回的是子類對象),但不容許直接return super 來返回父類對象。Super關鍵字自己沒有引用任何對象,不容許將super直接當作變量使用如 super==a 是會引發編譯錯誤,在子類使用super只是做爲調用父類方法的限定(當子類重寫了父類的相應方法)。
父、子類的類變量
因爲類變量屬於類自己,所以不會像實例變量那樣複雜。
final修飾符(必定要顯式指定初始值,不能默認空值)
final 修飾的實例變量必須是顯式指定初始值,只能在3個位置指定初始值
定義final實例變量時指定初始值
在非靜態初始化塊中爲final實例變量指定初始值
在構造器中爲final實例變量指定初始值
本質上final實例 變量只能在構造器中初始值(用javap –c 能夠查看響應的原理)
對於final 修飾的類變量只能在 定義時和靜態初始化塊指定初始值,其實 通過編譯器的處理,這2種方式都會被抽取到靜態初始化塊去賦予初始值。
final修飾局部變量必定要在定義時指定初始值。
final 修飾引用類型變量時,引用變量自己不能改變,但其引用的對象能夠改變。
執行「宏替換」的變量
final變量在指定初始值以後本質是已經再也不是一個變量,而是至關一個直接量。
public class Test
{
public static void main(String[] args)
{
//定義一個普通局部變量
int a = 5;
System.out.println(a);
}
}
使用javap -c Test 編譯
C:\Users\D.S.Qiu\Desktop\codes\codes\02\2.4>javap -c Test
Compiled from "FinalLocalTest.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_5
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/
io/PrintStream;
5: iload_1
6: invokevirtual #3 // Method java/io/PrintStream.printl
n:(I)V
9: return
}
若是使用final修飾變量a,使用java –c分析新產生的class文件發現缺乏了 istore_1 和iload_1兩行代碼,這說明用final修飾的變量a以後,變量a就徹底消失了。當定義final變量時指定了初始值,而這初始值能夠在編譯時就肯定下來了,那這個final變量本質上就是一個「宏變量」,編譯器會把程序中全部用到該變量的地方直接替換成該變量的值。
Java會緩存全部曾經用過的字符串直接量
String s1=」瘋狂」;
String s2=」Java」;
String s3=」瘋狂」+」Java」;
String s4=s1+s2;
System.out.println(s1==s2); //輸出的是false
由於編譯器沒法在編譯時肯定s2的值,不會讓s3指向字符串池中的緩存中的「瘋狂Java」,故s3==s4輸出false。
若是咱們將s1,s2用final去修飾,s1,s2就變成「宏變量」這樣在編譯時就能夠肯定s4的值,就能夠得到true的輸出。
值得注意的是,final變量只有在定義該變量指定初始值,纔會出現「宏替換」的效果。
對於類變量的final修飾相似。
Java要求因此內部類訪問的局部變量都要使用final修飾那是有緣由的:對於局部變量而言,做用域就只停留在該方法內,當方法執行結束,該局部變量也要隨着消失,但若是內部類若是產生隱式的「閉包」,閉包將使得局部變量脫離它的所在方法的繼續存在(以下面的程序)。是故若是不可避免要使用該變量能夠將該變量聲明成全局變量。
public class ClosureTest
{
public static void main(String[] args)
{
//定義一個局部變量
final String str = "Java";
//在內部類裏訪問局部變量str
new Thread(new Runnable()
{
public void run()
{
for (int i = 0; i < 100 ; i++ )
{
//此處將一直能夠訪問到str局部變量
System.out.println(str + " " + i);
//暫停0.1秒
try
{
Thread.sleep(100);
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
}).start();
//執行到此處,main方法結束
}
}
常見Java集合的實現細節
Set和Map 參看源代碼
HashMap和HashSet
對於HashSet而言,系統採用Hash算法決定集合元素的存儲位置,而HashMap 系統將value當成key的附屬物。
HashMap類有一個重要的內部接口Map.Entry,每一個Map.Entry其實就是一個key-value對。當兩個Entry對象的key的hashCode()返回值相同時,將有key經過eqauls()比較值決定是採起覆蓋行爲(返回true)仍是產生Entry鏈。
當建立hashMap是將initialCapacity參數值指定爲2的n次方,這樣能夠減小系統的計算開銷。
對於HashSet而言,它是基於HashMap實現的。HashSet底層採用HashMap來保存全部的元素,而其的value則存儲了一個PRESENT,它是一個靜態的Object對象。重寫equals()和hashCode()方法必須保持返回值的一致性,全部計算hashCode返回值的關鍵屬性,都應該用於做爲equals比較的標準。
TreeMap和TreeSet
TreeSet底層採用的一個 NavigableMap來保存TreeSet集合的元素。但實際上,因爲NavigableMap只是一個接口,所以底層依然是使用TreeMap來包含set集合中的全部元素。
Map和List
Map的values()方法
HashMap的values()方法代表上返回一個Values集合對象,但這個對象不能添加元素,主要用於遍歷HashMap裏的全部value。
ArrayList 和LinkedList
ArrayList和ArrayQeque(是Deque集合的實現類)底層都是基於Java數組實現的,Vector(還有一個Stack子類)其實就是ArrayList的線程安全版本,絕大部分實現方法都是相同的,只是在方法上增長了synchronized修飾。
LinkedList是一種鏈式存儲的線性表,本質就是雙向鏈表,不只實現了List接口,還實現了Deque(雙端隊列)接口。
Java的內存回收
都說Java不會內存泄露,可是在必定狀況仍是會發生的。例如咱們對一個Stack結構按以下方式定義一個pop方法
修改Stack的size屬性,也就是記錄棧內元素減1
返回elementData數組中索引爲size-1的元素
這樣是會致使內存泄露的(沒有作相應賦null處理)。
內存管理小技巧
儘可能使用直接量
使用StringBuilder和StringBuffer進行字符串鏈接
儘早釋放無用對象的引用
儘可能少用靜態變量
避免在常常調用的方法、循環體重建立java對象(每次建立都要分配內存空間,執行初始化操做,而後就是回收操做,影響程序的性能)。
緩存常用的對象
儘可能不要使用finalize方法(將資源清理放在finalize方法中完成是很是拙劣的選擇,垃圾回收機制工做量比較大)。
表達式中的陷阱
Java程序建立對象的常規方式有
經過new調用構造器建立Java對象
經過Class對象的newInstance()方法調用構造器來創造Java對象
經過Java反序列化機制從IO流中回覆Java對象
經過Java對象提供的clone方法複製一個新的對象
String s=」瘋狂」+」Java」+10;
String s1=」瘋狂Java10」;
s==s1; //true
上述表達式中若是運算數都是字符串直接量,整數直接量,沒有變量參與(有「宏替換」例外),沒有方法調用,JVM在編譯時就肯定了該字符串鏈接的表達式的值。
String s=」瘋狂」+」Java」+」DSQiu」;
上述字符串只建立了一個對象,由於str的值能夠在編譯時肯定下來,JVM會在編譯時就計算出s的值爲」瘋狂JavaDSQiu」,而後將該字符串直接量放入字符串池中,並不存在所謂的」瘋狂」等字符串對象。
表達式類型的陷阱
表達式的類型自動提高
當表達式中有多個基本類型的知識,整個算術表達式的數據類型自動提高與表達式中最高級別操做數一樣
char
int long float double 從左向右提高類型
byte short
short sVa=5;
sVa=sVa-2; //沒法經過編譯表達式右邊提高成 int類型
但若改成 sVa-=2;則沒有問題,由於複合賦值運算符包含了隱式類型轉換。但隱式轉換類型可能會出現「截斷」偏差(數據溢出) 如 short st=5; st+=90000; st卻獲得24479.
用 +=鏈接字符串是 +=左邊的變量只能是String類型,不能是String的父類型。
int a=23;
int b=a/2; //除不盡,可是沒有問題,因爲a是int類型的,a/2表達式也依然保持int類型。
輸入法的陷阱
當咱們輸入空格時,使用的是中文輸入法的空格,編譯的時候會報出「非法字符」的錯誤。
註釋字符必須合法
如 G:\codes\uinits5\Hello.java 該句註釋會報出「非法轉義字符」的錯誤,Java容許直接使用\uxxxx的形式表示字符,必須後是0~F的字符。
轉義字符的陷阱
如 System.out.println(「abc\u000a」.length());
\u000a是一個換行符,而字符串中是不容許直接使用換行符(要轉移)或者說字符串不能分紅兩行來書寫故編譯時報出錯誤。
一樣//\u000a也會引發錯誤
然而 http://www.sysu.edu.cn 這個直接出如今代碼中是沒有錯誤的,由於能夠當作http:(標籤 goto)和//www.sysu.edu.cn(註釋)
泛型可能引發的錯誤
看代碼
List list=new ArrayList();
list.add(「sb」);
List<Integer> inList=list; //編譯時只會有warning
System.out.println(inList.get(0)); //沒有問題能正常輸出字符串,訪問集合元素是不關心集合實際類型
Integer in=inList.get(0); //編譯時正常,運行時訪問集合元素當實際類型與集合所帶的泛型信息不匹配時拋出ClassCastException
String s=inList.get(0); //編譯不能經過,不能兼容類型
此外,當把一個帶泛型信息的Java對象付給不帶泛型信息的變量是,Java程序不只會擦除傳入的類型參數的泛型信息,還會擦除其餘泛型信息(如 public List<String> getValues()中返回值及括號的信息。
Class Apple<t extends Number> 擦除信息後編譯器只能知道上限是Number但不知道具體是哪一個子類。
不能建立泛型數組 如 List<String>[] list=new List<String>[10]; //不被容許的
面向對象的陷阱
Instanceof運算符的陷阱
Instanceof運算符前面操做數的編譯時類型必須是一下3中狀況
與後面的類相同
是後面的類父類
是後面的類子類
若是它實際引用的對象是第二個操做數的實例,或者是第二個操做數的子類,實現類(接口等)的實例,運算結果返回true。
在編譯階段,強制轉換要求被轉換變量的編譯時類型必須是如下3中狀況
與目標的類相同
是目標的類父類 返回false
是目標的類子類(自動向上轉型) 返回true
運行階段,被轉型變量所引用對象的實際類型必須是目標類型的實例,或者是目標類型的子類,實現類的實例,不管是否強制轉換或者編譯時類型是什麼:
Base base=new Derived();
Derived d=new Derived();
Base base1=new Base();
//Base是Derived的父類
System.out.println(base1 instanceof Derived); //false
System.out.println(d instanceof Base); //ture
System.out.println((Base)base instanceof Derived); //true
System.out.println((Derived)base instanceof Base); //true
null能夠做爲因此引用類型的變量的值,因此在使用instanceof以前要排除null的影響。
構造器的陷阱
反序列化機制恢復的對象雖然和原來對象具備徹底相同的實例變量值,可是系統中會將會禪師兩個對象。使用反序列化機制對單例類機制的破壞,能夠爲單例類提供readResolve方法(直接返回靜態實例變量)就能夠解決。
經過重寫clone 方法實現對Java對象的複製也須要調用構造器。
無限遞歸的構造器
防止構造器的遞歸調用
儘可能不要在定義實例變量是指定實例變量的值爲當前類的實例
儘可能不要初始化塊中建立當前類的實例
儘可能不要在構造器內調用本構造器建立Java對象
調用哪一個重載方法
一個方法被屢次重載後,傳入參數可能被向上轉型,使之符合調用方法的須要;JVM會選擇一個最精確匹配的一個方法。
看代碼
public void info(Object obj,int count)
public void info(Object[] obj, double count)
當調用 info(null,5)會引發「引用不明確」的異常,null與Object[]更匹配,5與int更匹配。
非靜態內部類的陷阱
系統在編譯階段總會爲非靜態內部類的構造器增長一個參數(第一個參數老是外部類)(能夠經過javap –c分析得到),所以調用非靜態內部類的構造器是必須傳入一個外部類對象做爲參數,不然程序將引起運行時異常(不能經過反射調用newInstance()來建立內部類)。
非靜態內部類(自己至關於一個非靜態上下文)固然不能擁有靜態成員。
this.super() 不能正常使用能夠用 類名.this.super()來解決。該類名不是當前類的類名
異常捕捉的陷阱
對於Check異常,要麼聲明拋出,要麼使用try…catch進行捕捉。
finally回收資源安全模式
finally
{
If(os!=null)
{
try{
os.close();
}catch(Excepiton e){}
}
}
學過Java基礎的人都能很容易理解上面的代碼和多態的原理,可是仍有一些關鍵的地方須要注意的,算是本身對多態的一個小結:
1.Java中除了static和final方法外,其餘全部的方法都是運行時綁定的。在我另一篇文章中說到private方法都被隱式指定爲final的,所以final的方法不會在運行時綁定。當在派生類中重寫基類中static、final、或private方法時,實質上是建立了一個新的方法。 2.在派生類中,對於基類中的private方法,最好採用不一樣的名字。 3.包含抽象方法的類叫作抽象類。注意定義裏面包含這樣的意思,只要類中包含一個抽象方法,該類就是抽象類。抽象類在派生中就是做爲基類的角色,爲不一樣的子類提供通用的接口。 4.對象清理的順序和建立的順序相反,固然前提是本身想手動清理對象,由於你們都知道Java垃圾回收器。 5.在基類的構造方法中當心調用基類中被重寫的方法,這裏涉及到對象初始化順序。 6.構造方法是被隱式聲明爲static方法。 7.用繼承表達行爲間的差別,用字段表達狀態上的變化。
我一直有個這樣的想法:若是咱們每一個人都樂於分享本身學習經驗,那麼咱們在自學的時候可以少走不少彎路,順利找到捷徑。所以我但願可以有一套這樣的分享機制:每一個人均可以幫本身的學習經驗整理出來,供你們學習交流,並能作進一步補充,不斷完善,最終趨於完美。