1 基礎文法
Java不僅是一種語言,Java是一個完整的平台,有一個龐大的庫,其中包含了很多可重用的代碼和一個提供諸如安全性、跨作業系統的可移植性以及自動垃圾收集等服務的執行環境。
面向對象:是一種程式設計技術。它将重點放在資料(即對象)和對象的接口上。
1.1 封裝
在面向對象那個設計方法中,封裝(Encapsulation)是一種将抽象函數式接口的實作細節包裝、隐藏起來的方法。
封裝就是隐藏實作細節,提供簡化接口。使用者隻需要關注怎麼用,而不需要關注内部是怎麼實作的。實作細節可以随時修改,而不影響使用者。函數是封裝,類也是封裝。通過封裝,才能在更高的層次上考慮和解決問題。
可以說,封裝是程式設計的第一原則,沒有封裝,代碼之間會存在着實作細節的依賴,則建構和維護複雜的程式是難以想象的。
- 可以被認為是一種保護屏障,防止該類的代碼和資料被外部類定義的代碼随機通路。
- 要通路該列的代碼和資料,必須通過嚴格的接口控制。
- 封裝最主要的功能在于我們修改自己的實作代碼,而不用修改那些調用我們代碼的程式片段。
- 适當的封裝可以讓程式更容易了解與維護,也加強了程式的安全性。
典型示例:
私有化成員變量,公有化通路方法。
public class Encaptest {
private String name;
private String idNum;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getIdNum() {
return idNum;
}
public void setIdNum(String idNum) {
this.idNum = idNum;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
對類進行測試:
public class RunEncap {
public static void main(String[] args) {
Encaptest encap = new Encaptest();
encap.setName("wonjie");
encap.setIdNum("008008008");
encap.setAge(18);
System.out.println("Name : " + encap.getName() + "\n" +"Age : " + encap.getAge());
}
}
輸出結果:
Name : wonjie
Age : 18
1.2 繼承
就是子類繼承父類的特征和行為,使得子類對象(執行個體) 具有父類的執行個體域和方法,或者子類從父類繼承方法,使得子類具有父類相同的行為。
與封裝的關系:由于子類與父類之間可能存在着實作細節的依賴,繼承可能會破壞封裝。
class Parent {
public void run() {
System.out.println("I just run.");
}
}
public class Son extends Parent {
public static void main(String[] args) {
Son son = new Son();
son.run();
}
}
輸出:
I just run.
可以看出,Son類并沒有定義方法和屬性,但是通過繼承Parent類,就可以具有run()方法。
注:Java支援支援單繼承,不支援多繼承,但支援多重複繼承。

特性:
- 子類可以擁有父類的非private的屬性、方法
- 子類可以擁有自己的屬性和方法,即子類可以對父類進行擴充
- 子類可以用自己的方法實作父類的方法
super 和 this 的差別:
- this引用一個對象,是實實在在的,可以作為函數參數,可以作為傳回值
- super是一個關鍵字,不能作為參數和傳回值,它隻是用于告訴編譯器通路父類的相關變量和方法
使用繼承的一個好處是可以統一處理不同子類型的對象。
關于繼承需要注意的點:
- 如果父類沒有預設的構造方法,它的任何子類都必須在構造方法中通過super調用Base的帶參數構造方法。否則Java會提示編譯錯誤。
class Base {
private String member;
public Base(String member) {
this.member = member;
}
}
public class Child extends Base {
private int a = 123;
public Child(String member) {
super(member);
}
}
- 如果在父類構造方法中調用了可被重寫的方法,則可能出現意想不到的結果。
class Base {
public Base() {
test();
}
public void test() {
}
}
public class Child extends Base {
private int a = 123;
public Child() {
}
public void test() {
System.out.println(a);
}
}
測試:
public class Test {
public static void main(String[] args) {
Child c = new Child();
c.test();
}
}
輸出:
123
第一次輸出是在new過程中輸出的,在new過程中,首先初始化父類,父類構造函數調用test()方法,test()方法被子類重寫了,就會調用子類的test()方法,子類方法通路子類執行個體變量a,而這個時候子類的執行個體變量的指派語句和構造方法還有沒執行,是以輸出的是其預設值0。
1.2.1 重名和靜态綁定
基類代碼:
public class Base {
public static String s = "static_base";
public String m = "base";
public static void staticTest() {
System.out.println("base static:" + s);
}
}
子類代碼:
public class Child extends Base {
public static String s = "child_base";
public String m = "child";
public static void staticTest() {
System.out.println("child static:" + s);
}
}
子類定義了和父類重名的變量和方法。對于一個子類對象,它就有了兩份變量和方法,在子類内部通路的時候,通路的是子類的,或者說,子類變量和方法隐藏了父類對應的變量和方法。
測試:
建立了一個子類對象,然後将對象分别指派給子類引用變量c和父類引用變量b,然後通過b和c分别引用變量和方法。
public class Test {
public static void main(String[] args) {
Child c = new Child();
Base b = c;
System.out.println(b.s);
System.out.println(b.m);
b.staticTest();
System.out.println(c.s);
System.out.println(c.m);
c.staticTest();
}
}
static_base
base
base static:static_base
child_base
child
child static:child_base
通過b通路的是Base的變量和方法,當通過c通路時,通路的是Child的變量和方法,這稱之為靜态綁定,即通路綁定到變量的靜态類型。靜态綁定是程式編譯階段即可決定,而動态綁定則到等到程式運作時。
執行個體變量、靜态變量、靜态方法、private方法,都是靜态綁定的。
1.2.2 重載和重寫
重載:方法名稱相同但參數簽名不同(參數個數、類型或順序不同)
重寫:子類重寫與父類相同參數簽名的方法
基類代碼:
public class Base {
public int sum(int a, int b) {
System.out.println("base_int_int");
return a+b;
}
}
子類代碼:
public class Child extends Base {
public long sum(long a, long b) {
System.out.println("child_long_long");
return a+b;
}
}
測試代碼:
public class Test {
public static void main(String[] args) {
Child c = new Child();
int a = 2;
int b = 3;
c.sum(a,b);
}
}
Child和Base都定義了sum方法,子類的sum方法雖然不完全比對但是是相容的,父類的sum方法參數類型是完全比對的。輸出:
base_int_int
如果将父類改為:
public class Base {
public long sum(int a, long b) {
System.out.println("base_int_long");
return a+b;
}
}
輸出:
base_int_long
調用依然是父類的方法,父類和子類的兩個方法的類型都不完全比對,由于父類的更比對一點,是以仍然調用父類方法。再修改子類代碼:
public class Child extends Base {
public long sum(int a, long b) {
System.out.println("child_int_long");
return a+b;
}
}
目前子類與父類方法相同,同樣不比對,此時,輸出:
child_int_long
是以可以看出:當多個重名函數的時候,在決定要調用那個函數的過程中,首先是按照參數類型進行比對,換句話說,尋找在所有重載版本中最比對的,然後才看變量的動态類型,進行動态綁定。
PS:當子類和父類方法都是完全比對時,調用子類方法。
1.2.3 父子類型轉換
向上轉型:子類型的對象可以指派給父類型的引用變量
Base b = new Child();
向下轉型:父類型的對象指派給子類型的引用變量(不一定轉換成功)
Child c = (Child)b;
Child c = (Child)b;就是将變量b的類型強制轉換為Child并指派為c,這是沒有問題的,因為b的動态類型就是Child,但是下面的轉化方式就不可以。
Base b = new Base();
Child c = (Child)b;
文法上不會報錯,但運作時會抛出錯誤,錯誤為類型轉換異常。
Exception in thread "main" java.lang.ClassCastException: beforeJob.Base cannot be cast to beforeJob.Child
一個父類變量能不能轉換為一個子類的變量,取決于這個父類變量的動态類型(即引用的對象類型) 是不是這個子類或這個子類的子類。
1.2.4 繼承通路權限protected
public:外部可以通路
private:隻能内部通路
protected:可以被子類通路,還可以被同一包中的其他類通路,不管其他類是不是該類的子類。
基類代碼:
public class Base {
protected int currentStep;
protected void step1() {
}
protected void step2() {
}
public void action() {
this.currentStep = 1;
step1();
this.currentStep = 2;
step2();
}
}
action表示對外提供的行為, 内部有兩個步驟step1()和step2(),使用currentStep變量表示目前進行到了那個步驟,step1()、step2()和currentStep是protected的,子類一般不重寫action,而隻是重寫step1和step2,同時,子類可以直接通路currentStep檢視進行到哪一步。子類代碼:
public class Child extends Base {
public void step1() {
System.out.println("child step " + this.currentStep);
}
protected void step2() {
System.out.println("child step " + this.currentStep);
}
}
測試:
public class Test {
public static void main(String[] args) {
Child c = new Child();
c.action();
}
}
輸出:
child step 1
child step 2
基類定義了表示對外行為的方法action,并定義了可以被子類重寫的兩個步驟step1()和step2() ,以及被子類檢視的變量currentStep,子類通過重寫protected方法step1()和step2()來修改對外的行為。
這種思路和方法是一種設計模式,稱之為模闆方法。action方法就是一個模闆方法,它定義了實作的模闆,而具體實作則由子類提供。模闆方法在很多架構中有廣泛的應用,這是使用protected的一種常見場景。
1.2.5 其他
可見性重寫:重寫時,子類方法不能降低父類方法的可見性。
final修飾類-防止繼承;修飾方法-防止重寫。
1.2.6 繼承實作的基本原理
基類:
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 Test {
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---b.s:" + b.s);
System.out.println("\n---c.s:" + c.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
---b.s:1
---c.s:10
1.3 多态
一種類型的變量,可以引用多種實際類型對象。
例子:
public class ShapeManager {
private static final int MAX_NUM = 100;
private Shape[] shapes = new Shape[MAX_NUM];
private int shapeNum = 0;
public void addShape(Shape shape) {
if (shapeNum < MAX_NUM) {
shapes[shapeNum++] = shape;
}
}
public void draw() {
for (int i = 0; i < shapeNum; i++) {
shapes[i].draw();
}
}
}
public class ManageTest {
public static void main(String[] args) {
ShapeManager manager = new ShapeManager();
manager.addShape(new Circle(new Point(4,4),3));
manager.addShape(new Line(new Point(2,3),new Point(3,4),"green"));
manager.addShape(new ArrowLine(new Point(1,2),new Point(5,5),"black",false,true));
manager.draw();
}
}
對于變量shape,有兩種類型:
- 類型Shape,稱為shape的靜态類型
- 類型Circle/Line/ArrorLine,稱之為shape的動态類型。在ShapeManager的draw方法中,shape[i].draw()調用的是其對應動态類型的draw方法,這種稱之為方法的動态綁定。
為什麼要有多态和動态綁定?
建立對象的代碼和操作對象的代碼,經常不在一起,操作對象的代碼往往隻知道對象是某種父類型,也往往隻需要知道它是某種父類型就可以了。
多态和動态綁定是計算機程式中的一種重要思維方式,使得操作對象的程式不需要關注對象的實際類型,進而可以統一處理不同對象,但又能實作每個對象的特有行為。
子類對象可以指派給父類引用變量,這叫多态;實際執行調用的是子類實作,這叫動态綁定。
2 容器
3 異常
3.1 異常分類
Throwable是所有異常的基類,它有兩個子類:Error和Exception。
Error表示系統錯誤或資源耗盡,由Java系統自己使用,應用程式不抛出和處理。如上圖虛拟機錯誤(VirtualMachineError)及其子類記憶體溢出錯誤和棧溢出錯誤。
Exception表示應用程式錯誤,它有很多子類,應用程式也可以通過繼承Exception或其子類建立自定義異常。
如此多不同的異常類其實并沒有比Throwable這個基類多多少屬性和方法,大部分類在繼承父類之後隻是定義了幾個構造方法,這些構造方法也隻是調用了父類的構造方法,并沒有額外的操作。
定義這麼多不同的類,是因為名字不同。異常類名字本身就代表異常的關鍵資訊,無論是抛出還是捕獲異常,使用合适的名字都有助于代碼的可讀性和可維護性。
自定義異常:
public class AppException extends Exception {
public AppException() {
super();
}
public AppException(String message, Throwable cause) {
super(message, cause);
}
public AppException(String message) {
super(message);
}
public AppException(Throwable cause) {
super(cause);
}
}
3.2 異常處理
3.2.1 catch比對
try {
// 可能觸發異常的代碼
} catch (NumberFormatException e) {
System.out.println("not valid number");
} catch (RuntimeException e) {
System.out.println("runtime exception " + e.getMessage());
} catch (Exception e) {
e.printStackTrace();
}
3.2.2 重新抛出異常
對catch塊内處理完後,可以重新抛出異常。這個異常可以是原來的,也可以是建立的。
try {
// 可能觸發異常的代碼
} catch (NumberFormatException e) {
System.out.println("not valid number");
throw new AppException("輸入格式不正确", e);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
3.2.3 finally
catch後面可以跟finally語句,文法如下:
try {
// 可能抛出異常
} catch () {
// 捕獲異常
} finally {
// 不管有無異常都執行
}
3.2.4 try-with-resources
對于一些使用資源的場景,比如檔案和資料庫連接配接,典型的使用流程是首先打開資源,最後在finally語句中調用資源的關閉方法。針對這種場景,Java7開始支援一種新的文法,稱為try-with-resources,這種文法針對實作了java.lang.AutoCloseable接口的對象,該對象定義為:
public interface AutoCloseable {
void close() throws Exception;
}
沒有try-with-resources時,使用形式如下:
public static void useResource() throws Exception {
AutoCloseable r = new FileInputStream("hello"); //建立資源
try {
// 使用資源
} finally {
r.close();
}
}
使用try-with-resources文法,形式如下:
public static void useResource() throws Exception {
try(AutoCloseable r = new FileInputStream("hello")) { //建立資源
// 使用資源
}
}
3.2.5 throws
異常機制中,還要一個和throw很像的關鍵字throws,用于聲明一個方法可能抛出的異常,文法如下所示:
public void test() throws AppException, SQLException, NumberFormatException {
// 主體代碼
}