Java筆記(二)類
類
一、類的基礎
1.類---一種自定義資料型別。
2.與方法內建立區域性變數不同,在建立物件的時候,所有的例項變數都會分配
一個預設值,這與建立陣列的時候是類似的。
3.在{}對例項變數內賦值:
int x; int y; { x = 1; y = 2; }
在新建立一個物件的時候會先呼叫這個初始化,然後再執行建構函式。
靜態變數可使用static{}初始化:
private static int STATE_ONE; private static int STATE_TOW; static { STATE_ONE = 1; STATE_TOW= 2; }
靜態初始化程式碼塊在類載入的時候執行,這是任何物件建立之前,且只執行一次。
4.物件和陣列一樣有兩塊記憶體,儲存地址的部分分配在棧中,而儲存實際內容的部分
分配在堆中。
5.jar包:
打包的是編譯後文件,將多個編譯後文件打包,方便其他程式呼叫。
到編譯後的class檔案根目錄,執行:
jar -cvf <包名>.jar <最上層包名>
如何使用jar包:將其加入classpath中即可。
二、類的繼承
1.使用繼承的好處:一是可以複用程式碼,二是不同的子類物件可以方便地統一管理。
2.向上轉型:轉換為父類的型別。
3.多型:一種型別變數可以引用多種實際型別物件。
4.動態繫結:在呼叫時實際呼叫的是子類的方法。
5.多型和動態繫結是計算機程式的一種重要的思維方式,使得操作物件的程式
不需要關心物件的實際型別。
6.過載:指方法名稱相同但引數簽名(引數的個數、型別或者順序)不同。
7.重寫:子類重寫父類引數簽名相同的方法。
8.注意:當有多個重名函式時,在決定呼叫哪個函式的過程中,首先是按照引數
型別進行匹配,換句話說,先尋找所有過載中最匹配的,然後再看變數的動態型別
進行動態型別繫結。
9.一個父類的變數是否能轉換為子類的變數,取決於這個父類的
動態型別(即引用的物件型別)是不是這個子類或者該子類的子類。
10.重寫時子類的方法不能降低父類方法的可見性。
11.用final關鍵子修飾的類不可被繼承。
繼承實現的基本原理:
舉個例子:
基類:
public class Base { public static int s; private int a; static { System.out.println("基類靜態程式碼塊, s = " + s); s = 1; } { System.out.println("基類例項程式碼塊, a = " + a); a = 1; } public Base() { System.out.println("基類構造方法, a = " + a); a = 2; } protected void step() { System.out.println("base s = " + s + " a = " + a); } public void action() { System.out.println("start:"); step(); System.out.println("end:"); } }
子類:
public class Child extends Base{ public static int s; private int a; static { System.out.println("子類靜態程式碼塊, s = " + s); s = 10; } { System.out.println("子類例項程式碼塊, a = " + a); a = 10; } public Child() { System.out.println("子類構造方法, a = " + a); a = 20; } protected void step() { System.out.println("child s = " + s + " a = " + a); } }
呼叫:
public class Use { public static void main(String[] args) { System.out.println("-----new Child()"); Child c = new Child(); System.out.println("\n-----c.action()"); c.action(); Base b = c; System.out.println("\n------b.action()"); b.action(); System.out.println("\n------c.s: " + c.s); System.out.println("\n------b.s: " + b.s); } } /*-----new Child() 基類靜態程式碼塊, s = 0 子類靜態程式碼塊, s = 0 基類例項程式碼塊, a = 0 基類構造方法, a = 1 子類例項程式碼塊, a = 0 子類構造方法, a = 10 -----c.action() start: child s = 10 a = 20 end: ------b.action() start: child s = 10 a = 20 end: ------c.s: 10 ------b.s: 1*/
過程詳解:
1)類載入過程:
在Java中所謂的類載入是指將類的相關資訊載入到記憶體中。在Java中,類是動態
載入的,當第一次使用這個類的時候才會載入,載入一個類時會檢視其父類是否
載入,如果沒有則會載入其父類。
存放類資訊的記憶體區域在Java中被稱為方法區(不同於堆和棧)。載入後方法區
就有了類的資訊:
例項初始化程式碼包括了例項初始化程式碼塊和構造方法。
本例中,類的載入大概在記憶體形成了類似上面的佈局,然後分別執行了Base和Child的初始化程式碼。
2)物件的建立過程
類載入過後,new Child()就是建立Child例項。建立過程包括
1.分配記憶體
2.對所有例項變數賦予預設值
3.執行例項初始化程式碼
其中,分配的記憶體包括本類和所有父類的例項變數,但不包括任何類變數。
例項初始化程式碼從父類開始,再執行子類。
每個物件除了儲存有例項變數外,還儲存有類資訊的引用。
3)方法呼叫過程
尋找要執行的例項方法的時候,先從物件的例項開始查詢,找不到再查詢父類。(這也是動態繫結的實現原理)
4)變數的訪問過程
變數的訪問是靜態訪問的,無論是類變數還是實際變數。比如例子中,如果變數a不是私有的
b.a訪問的是Base中的變數,c.a訪問的Child例項中的變數。
繼承是一把雙刃劍:
繼承會破壞封裝,而封裝可以說是程式設計的基本原則,另外繼承可能沒有反應出is-a關係。
1.繼承會破壞封裝
繼承可能破壞封裝是因為子類和父類可能存在著實現細節的依賴。
子類在繼承父類的時候,往往不得不關注父類的實現細節,而父類
如果在修改其內部實現的時候,如果不考慮子類,也往往會影響到子類。
舉個例子:
public class Base { private static final int MAX_NUM = 1000; private int[] arr = new int[MAX_NUM]; private int count; public void add(int number) { if (count < MAX_NUM) { arr[count++] = number; } } public void addAll(int[] numbers) { for (int num: numbers) { add(num); } } }
public class Child extends Base{ private long sum; @Override public void add(int number) { super.add(number); sum += number; } @Override public void addAll(int[] numbers) { super.addAll(numbers); for (int i = 0; i < numbers.length; i++) { sum += numbers[i]; } } public long getSum() { return sum; } }
public class Use { public static void main(String[] args) { Child child = new Child(); child.addAll(new int[]{1, 2, 3}); System.out.println("The sum is " + child.getSum());//The sum is 12 } }
說明:如果子類不知道父類的實現細節就不能正確地擴充套件。子類和父類之間是細節依賴的。
因此,父類不能隨意增加公開的方法,因為給父類增加等於給子類增加,而子類可能需要
重寫該方法才能保證其正確性。
2.繼承沒有反應is-a關係
繼承關係是用來反應is-a關係的,子類是父類物件的一種,
父類的屬性和行為也適用於子類。在is-a關係中,重寫方法
時,子類不應該改變父類的預期行為,但這在Java的繼承中
是無法約束的。
3.如何應對繼承的雙面性
方法一:避免使用繼承
1)使用final避免繼承:
final方法不能被重寫,final類不能被繼承。
給方法新增final,父類就保留了隨意修改該方法內部實現的自由。類新增final同理。
2)優先使用組合而不是繼承:
使用組合可以避免父類變化對子類的影響,從而保護子類。
public class Child extends Base{ private Base base; private long sum; public Child(){ base = new Base(); } public void add(int number) { base.add(number); sum+=number; } public void addAll(int[] numbers) { base.addAll(numbers); for(int i=0;i<numbers.length;i++){ sum+=numbers[i]; } } public long getSum() { return sum; } }
3)使用介面
方法二:正確使用繼承
三大場景
1)父類是別人寫的,我們寫子類
我們需要注意:
重寫方法不要改變預期行為
閱讀文件說明,理解可重寫方法的實現機制,尤其是方法間的依賴關係
基類如果修改,閱讀其修改說明
2)我們寫基類,別人寫子類
使用繼承反應真正的is-a關係,只將正則公共的部分放入基類。
對不希望被重寫的公開方法新增final修飾符
寫文件,對子類如何實現提供指導
寫修改說明
三、內部類
一個類可以放在另一個類的內部,該類稱為內部類,包含它的類稱為外部類。
一般而言,內部類與包含它的外部類有密切的關係,而與其他類關係不大,使用內部類,
可以對外部完全隱藏,因此可以有更好的封裝性,程式碼實現上也更為簡潔。
不過,內部類知識Java編譯器的概念,對於Java虛擬機器而言是不知道有內部類這回事的,
每個內部類都被編譯為一個獨立的類,生成一個獨立的位元組碼檔案。
1.靜態內部類
public class Outer { private static int shared = 100; //靜態內部類只能訪問外部類的靜態變數和方法,例項變數和方法不能訪問。 public static class StaticInner { public void innerMethod() { System.out.println("I got the outer shared=" + shared); } } public void test() { //在外部類內部,可以直接使用靜態內部類 StaticInner staticInner = new StaticInner(); staticInner.innerMethod(); } }
//public靜態內部類可以被外部使用 Outer.StaticInner inner = new Outer.StaticInner(); inner.innerMethod(); //I got the outer shared=100
靜態內部類的實現:
程式碼實際上會生成兩個類,一個是Outer,一個是Outer$StaticInner
public class Outer { private static int shared = 100; public void test() { Outer$StaticInner staticInner = new Outer$StaticInner(); staticInner.innerMethod(); }
static int access$0(){
return shared;
} } public static class Outer$StaticInner { public void innerMethod() { //內部類訪問了外部類的靜態私有變數,類的私有變數是不能被外部訪問到的 //Java的解決辦法:自動為Outer生成一個非私有訪問方法access$0,它返回shared變數。 System.out.println("I got the outer shared=" + Outer.access$0()); } }
靜態內部類使用場景:與外部類關係密切,且不需要使用外部類例項變數。
2.成員內部類 成員內部類沒有static修飾
public class Outer { private static int a = 100; public class Inner { public static final int A = 1; // ok //public static int b = 2; 報錯 //除了外部類的靜態變數和方法,成員內部類可以訪問例項變數和方法 public void innerMethod() { System.out.println("I got the outer a = " + a); action(); //如果內部類有方法與外部類重名可以使用 //Outer.this.action(); } } private void action() { System.out.println("action"); } public void test() { Inner inner = new Inner(); inner.innerMethod(); } }
Outer outer = new Outer(); //與靜態內部類不同,成員內部類總是與一個外部類的例項關聯 Outer.Inner inner = outer.new Inner(); //Outer.Inner inner1 = new Outer.Inner(); 編譯器報錯
與靜態內部類不同,成員內部類裡面不能定義靜態變數和方法,final修飾的除外。(想一想,WHY????)
成員內部類的實現,也會生成兩個類:
public class Outer { private static int a = 100; public void action() { System.out.println("action"); } public void test() { Outer$Inner inner = new Outer$Inner(); inner.innerMethod(); } static int access$0(Outer outer) { return outer.a; } static void access$1(Outer outer) { outer.action(); } } public static class Outer$Inner { final Outer outer; public Outer$Inner(Outer outer) { this.outer = outer; } public void innerMethod() { System.out.println("outer a " + Outer.access$0(outer)); Outer.access$1(outer); } }
成員內部類應用場景:內部類與外部類關係密切,需要訪問外部類的例項變數或者方法。
另外,外部類的一些方法的返回值可能是某些介面,為了返回這個介面,外部類方法可能使用
內部實現這些介面,這個內部類可以設定為private,完全對外隱藏。(不需要其他類使用的介面)
3.方法內部類
public class Outer { private int a =100; public void test(final int param) { final String str = "hello"; //方法內部類只能在定義它的方法內使用 //如果該方法是例項方法,則除了靜態變數和方法外 //內部類還能直接訪問外部類的例項變數和方法 //如果該方法是靜態方法,則只能外部類的訪問靜態變數和方法 class Inner { public void innerMethod() { System.out.println("outer a " + a); System.out.println("param " + param); System.out.println("local str " + str); } } Inner inner = new Inner(); inner.innerMethod(); } public static void main(String[] args) { Outer outer = new Outer(); outer.test(88); } }
方法內部類的實現:
public class Outer { private int a =100; public void test(final int param) { final String str = "hello"; Inner inner = new Inner(this, param); inner.innerMethod(); } static int access$0(Outer outer) { return outer.a; } public class Inner { Outer outer; int param; public Inner(Outer outer, int param) { this.outer = outer; this.param = param; } public void innerMethod() { System.out.println("outer a " + Outer.access$0(this.outer)); System.out.println("param " + param); //String str 並沒有作為引數傳遞,這是因為它被定義為了常量,可直接使用 System.out.println("local str " + "hello"); } } public static void main(String[] args) { Outer outer = new Outer(); outer.test(88); } }
以上程式碼也解釋了為什麼方法內部類訪問外部方法的引數和區域性變數必須定義為final:
因為內部方法類操作的實際是自己的例項變數,並不是外部變數,只是這些變數和外部
變數有一樣的值。所以對這些變數賦值,並不會改變外部方法變數的值,
為了避免混淆乾脆將外部方法的區域性變數和引數宣告為final。
如果確實需要修改外部方法的區域性變數和引數,可以把他們宣告為陣列:
public class Outer { private int a =100; public void test(final int param) { final String[] str = new String[] {"hello"}; class Inner { public void innerMethod() { str[0] = "hello world"; } } } }
4.匿名內部類
建立物件的時候定義的類:
四、列舉
列舉可以定義在一個單獨的檔案中,也可以定義在類的內部。
public enum Size { SMALL,MEDIUM,LARGE } Size size = Size.LARGE; System.out.println(size); //LARGE
列舉變數可以使用equals和==進行比較。列舉型別都有一個方法
ordinal(),表示列舉時在宣告時的順序,從0開始。另外,列舉型別都實現了Comparable。
在switch內部列舉值不能帶列舉型別字首。
列舉類的實現:列舉型別會被Java編譯器轉換為一個對應的類,該類繼承了java.lang.Enum類
public final class Size extends Enum<Size> { public static final Size SMALL = new Size("SMALL", 0); public static final Size MEDIUM = new Size("MEDIUM", 1); public static final Size LARGE = new Size("LARGE", 2); private static Size[] VALUES = new Size[] { SMALL, MEDIUM, LARGE }; private Size(String name, int ordinal) { super(name, ordinal); } public static Size[] values() { Size[] values = new Size[VALUES.length]; System.arraycopy(VALUES, 0, values, 0, VALUES.length); return values; } public static Size valueOf(String name) { return Enum.valueOf(Size.class, name); } }
應用例項:
public enum Size { //列舉值的定義必須放在最上面 //列舉值寫完後必須以分號結尾 //列舉值定義完成後才能寫其他程式碼 SMALL("S", "小號"), MEDIUM("M", "中號"), LARGE("L", "大號"); private String abbr; private String title; private Size(String abbr, String title) { this.abbr = abbr; this.title = title; } public String getAbbr() { return abbr; } public void setAbbr(String abbr) { this.abbr = abbr; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public static Size fromAbbr(String abbr) { for (Size size : Size.values()) { if (size.getAbbr().equals(abbr)) { return size; } } return null; } }
自定義列舉id(思考為什麼使用自定義id而不是使用ordinal):
public enum Size { XSMALL(10), SMALL(20), MEDIUM(30), LARGE(40); private int id; private Size(int id) { this.id = id; } public int getId() { return id; } }