本文主要介紹Java中的創建對象的內存分析,參數傳遞的過程(值傳遞)以及Java中三大特性之一的多態特性。行文中會穿插Java基礎中其他的一些知識點,而這部分知識點也是比較容易疏忽的知識點。
面向對象引入
面向過程和麪向對象的區別
-
面向過程:主要的關注點是,實現的具體過程,因果關係
優點:對於業務邏輯比較簡單的程序,可以達到快速開發,前期投入成本較低
缺點:1)難以解決非常複雜的業務邏輯;2)元素之間“耦合度”高;3)沒有獨立體的概念,無法實現組件複用 -
面向對象:主要的關注點是,對象(獨立體)能夠完成哪些功能
優點:1)容易解決更復雜的業務邏輯;2)耦合度低,擴展力強;3)組件複用性強
缺點:前期投入成本高,需要進行獨立體的抽取,大量的系統分析與設計
C語言:純面向過程,C++:半面向對象,Java:純面向對象
面向對象的生命週期階段
- 面向對象的分析:OOA
- 面向對象的設計:OOD
- 面向對象的編程:OOP
對象創建的內存分析
JVM中三塊主要的內存空間
- 方法區內存:類加載的時候,classs字節碼代碼片段被加載到該內存空間
- 棧內存:方法代碼片段執行的時候,給該方法的局部變量分配內存空間,在棧內存中壓棧
- 堆內存:new出來的對象在堆內存中存儲
對象和引用的區別
- 對象:new 運算符在
堆內存
中開闢的內存空間稱爲對象 - 引用:引用是一個變量,只不過這個變量中保存了另一個Java對象的內存地址
在Java語言當中,程序員不能直接操作堆內存,java中沒有指針,不像C語言,程序員只能通過“引用”去訪問堆內存當中對象內部的實例變量
訪問實例變量的語法格式
- 讀取數據:引用.變量名
- 修改數據:引用.變量名 = 值
構造方法的作用
- 創建對象
- 創建對象的同時,初始化實例變量的內存空間
【案例】分析以下代碼執行過程中的內存分配情況,特別注意 s變量的內存情況
分析:i變量和s變量在main方法中都是局部變量,局部變量在棧內存中開闢內存空間;用關鍵字new出來的對象在堆內存中創建內存空間,並且在創建Student對象的時候會創建Student類中的五個實例變量,並初始化(初始化時一切向0看起,no爲0,name爲null,age爲0,sex爲false,addr爲null);s變量既是局部變量也是引用,它保存堆內存中的對象地址。上述的代碼片段的內存圖如下所示:
暫且可以這麼表示,實際上String類型屬於引用數據類型,需要開闢獨立的堆內存空間
【案例】分析以下代碼片段的內存分配情況。其中User類中的成員變量包括用戶身份id標識no,name和addr,Address中的成員變量包括城市名city,街道street和郵編zipcode
public static void main(String[] args) {
User u= new User();
Address a = new Address();
u.addr = a;
System.out.println(u.addr.city);//null
a.city = "天津";
System.out.println(u.addr.city);//天津
}
因爲a變量賦值給了u.addr,所以a和u.addr中保存的都是Address對象的內存地址,兩者的值相同
JVM相關重要知識點
- 靜態變量存儲在方法區內存當中
- 三塊內存當中變化最頻繁的是棧內存,最先有數據的是方法區內存,垃圾回收器主要針對的是堆內存
- 垃圾回收器(自動垃圾回收機制、GC機制)什麼時候考慮將某個java對象的內存回收?
堆內存中的java對象成爲垃圾數據(沒有更多的引用指向這個對象)的時候,會被垃圾回收器回收。這個對象無法被訪問,因爲訪問對象只能通過引用的方式。
空指針異常
public static void main(String[] args) {
Customer c = new Customer();
System.out.println(c.id);
c = null;
System.out.println(c.id);
}
編譯通過,運行報錯。
上述的代碼能夠編譯通過,因爲符合語法;但是運行時出現了空指針異常錯誤(java.lang.NullPointerException)。空引用訪問“實例”相關的數據一定會出現空指針異常。在執行完c = null;後,創建出的Customer對象沒有更多的引用指向它,則該對象被GC回收。
Eclipse相關操作
文件夾
- matadata文件夾
在eclipse的工作區當中有一個文件夾:.metadata
該文件夾當中存儲了當前eclipse的工作狀態(即我們在eclipse中打開了哪些文件,設置了怎麼的佈局,下次再次打開eclipse的時候仍然進入的是同樣的界面)。將 .matadata文件夾刪除之後,下一次再次進入這個工作區的時候是一個全新的開始,但是會發現這個IDE中所有的項目丟失了,需要重新導入,磁盤上的項目沒有丟。
佈局
- Eclipse佈局
在Eclipse中我們可以自定義佈局,但是一旦我們的佈局被我們搞亂了,我們可以恢復佈局
【Window】->【Perspective】->【Reset Perspective】
快捷鍵
- 快捷鍵
Ctrl+Shift+T :查找某個類文件(Open Type)
Ctrl+Shift+R :查找資源
Ctrl+O :查找類中的屬性或方法
Ctrl+1 :代碼糾錯 - 調試快捷鍵
F5:Step into(進入方法內部)
F6:Step over (執行下一行)
F7:Step return (返回方法)
F8:Resume(執行到下一個斷點)
程序
- 大家所學的類庫一般包括三個部分:
源碼【可以看源碼來理解程序】
字節碼【程序開發的過程中使用】
幫助文檔【對開發提供幫助】 - 包名的命名規範
公司域名倒序 + 項目名 + 模塊名 + 功能名;
採用這種方式重名的機率較低,因爲公司域名具有全球唯一性。
擴展
擴展:在一個類中使用了package機制之後,應該如何編譯,如何運行?
如A.java類使用了package機制(包名爲com.syc.activiti)之後,類名不再是A了,而是com.syc.activiti.A
第一種方式編譯運行:
編譯:javac 源文件路徑
運行:需要先手動方式創建路徑(com\syc\activiti),然後將字節碼文件剪切到該路徑中,然後在com所在的路徑運行java com.syc.activiti.A
第二種方式編譯運行:
編譯:javac -d 編譯之後的存放路徑 java源文件路徑
運行:JVM的類加載器ClassLoader默認從當前路徑下加載,需要保證DOS命令窗口的路徑先切換到com所在的路徑,然後執行java com.syc.activiti.A
面向對象特性
封裝
封裝步驟
- 所有屬性私有化,使用private修飾,數據只能在本類中訪問;
- 對外提供簡單的操作入口,get方法和set方法,外部程序必須通過這些簡單的入口進行訪問;
封裝的好處
- 對外提供訪問方式,隱藏了具體的實現細節。(這樣就看不到這個事務的複雜的那一面,只能看到事務簡單的那一面)
- 提高了代碼的複用性。(封裝後的程序可以重複使用,並且適應性較強,在任何場合都可以使用)
- 提高了安全性。(別人不能通過變量名.屬性值的方式修改某個私有的成員變量)
Java值傳遞
Java語言當中方法調用的時候涉及到參數傳遞的問題,參數傳遞實際上傳遞的是變量中保存的具體值,並且一切參數的傳遞都是值傳遞。
對比下面的兩個程序。
【程序1】
public class MyTest {
public static void main(String[] args) {
int i = 10;
method(i);
System.out.println("main -->" + i); // i=10
}
public static void method(int i) {
i++;
System.out.println("method -->" + i);// i=11
}
}
【程序2】
public class MyTest {
public static void main(String[] args) {
User u = new User(20);
add(u);
System.out.println("main-->" + u.age); //21
}
public static void add(User u) {
u.age++;
System.out.println("add-->" + u.age); //21
}
}
class User{
int age;
public User(int i){
age = i;
}
}
分析:在這兩個程序中主要講的是java中的值傳遞問題。在下面畫出了兩個程序的內存圖。接下來主要講解第二個程序。第一步,執行程序入口main方法,創建User對象,開闢堆內存空間,創建成員變量age的空間,並賦初始值20,該對象賦值給User u之後,在棧內存中開闢局部變量u的內存空間,並且其中保存了User對象的內存地址。第二步,調用add方法,將該方法入棧,因爲該方法中定義了一個User變量u,因此開闢u的內存空間,因爲在調用方法的時候同時把實參u傳遞給了形參u,相當於將u中保存的內存地址值
給了這個add方法中的u,因此兩個u指向的是同一個對象,第三步,u.age++,將u的實例變量age+1,此時,u的age已經是21了,因爲add方法中的u和main方法中的u指向的都是同一個對象,因此最終打印出的age值是相同的。
【最終結論】
方法調用的時候,涉及到參數傳遞的問題,傳遞的時候,java只遵循一種語法機制,就是將變量中保存的“值”傳遞過去了,只不過有的時候這個值是一個字面值,有的時候這個值是一個java對象的內存地址。
this
Java語言中的this關鍵字
- this是一個關鍵字,譯爲“這個”
- this是一個引用,this是一個變量,this變量中保存了內存地址指向了自身,this存儲在JVM堆內存java對象內部
- 每個對象都有this,創建100個java對象,也就說有100個不同的this
- this可以出現在“實例方法”中,this指向當前正在執行這個動作的對象。(this代表當前對象)
- this在多數情況下都是可以省略不寫的
- this不能使用在帶有static的方法中
this可以出現的位置
- this 可以使用在實例方法中,代表當前對象
- 可以使用在構造方法中,表示通過當前的構造方法調用其他的構造方法【語法格式:this(實參);】
this(實參); 這種語法只能出現在構造函數的第一行
【錯誤案例】
public class MyTest {
String name;
public static void main(String[] args) {
}
public static void doSome(){
//以下代碼報錯
System.out.println(name);//name編譯錯誤
System.out.println(this);//this編譯錯誤
}
}
說明:1)name變量屬於成員變量之實例變量,要訪問該變量需要先有“當前對象”,static修飾的方法是通過類名的方式訪問的,也就是說在這個執行過程中沒有“當前對象”;2)this表示的是當前對象,在static修飾的方法中沒有當前對象,不能使用this
【結論】
在帶有static的方法中不能“直接”訪問實例變量和實例方法,static修飾的方法中是沒有“當前對象”的,自然無法訪問當前對象的實例變量和實例方法。
static
靜態方法的調用
- 帶有static的方法,其實既可以採用類名的方式訪問,也可以採用引用的方式訪問
- 但是即使採用引用的方式去訪問,實際上執行的時候和引用指向的對象無關!
- 使用eclipse開發工具的時候,使用引用的方式訪問帶有static的方法,程序會出現警告,但會不報錯,所以帶有static的方法還是建議使用“類名.”的方式訪問
public class Test {
public static void main(String[] args) {
Test.doSome();
doSome();
Test t = new Test();
t.doSome(); //eclipse出現黃色應該,IDEA中 t.的方式點不出來靜態方法
t = null;
t.doSome(); //這裏不會出現空指針異常
}
public static void doSome(){
System.out.println("do some!");
}
}
上述 t=null; 執行之後仍然能夠調用doSome方法,因爲靜態方法的調用已經和引用指向的對象無關了。
實例變量 OR 靜態變量
- 什麼時候成員變量聲明爲實例變量?
所有對象都有這個屬性,對象的屬性值會隨着對象的變化而變化(不同對象的屬性值不同) - 什麼時候成員變量聲明爲靜態變量?
所有對象都有這個屬性,並且所有對象的這個屬性的值是一樣的,建議定義成靜態變量,節省內存開銷
靜態變量在類加載的時候初始化,內存在方法區中開闢。訪問的時候不需要創建對象,直接使用“類名.靜態變量名”的方式訪問。
可以使用static關鍵字來定義“靜態代碼塊”
- 語法格式:
static {
Java語句;
} - 靜態代碼塊在類加載時執行,並且只執行一次
- 靜態代碼塊在一個類中可以編寫多個,並且遵循自上而下的順序依次執行
- 靜態代碼塊的作用是什麼,什麼時候用?
靜態代碼塊是Java爲程序員準備的一個特殊的時刻,這個特殊的時刻被稱爲類加載時刻
。若希望在此刻執行一段特殊的程序,這段程序可以直接放到靜態代碼塊當中,例如要在類加載的時候執行代碼完成日誌的記錄,那麼這段日誌代碼可以寫到靜態代碼塊中,完成日誌記錄。 - 通常在靜態代碼塊當中完成預備工作,先完成數據的準備工作,例如初始化連接池,解析XML配置文件…
實例代碼塊【瞭解,使用的非常少】
- 實例代碼塊在一個類中可以編寫多個,也是遵循自上而下的順序依次執行
- 實例代碼塊在構造方法執行之前執行,構造方法執行一次,實例代碼塊對應執行一次
- 實例代碼塊也是Java語言爲程序員準備的一個特殊時刻,這個特殊的時刻被稱爲
對象初始化時刻
public class Test {
public Test() {
System.out.println("默認構造器");
}
public Test(int a){
System.out.println(a);
}
//實例代碼塊
{
System.out.println(1);
}
//實例代碼塊
{
System.out.println(2);
}
public static void main(String[] args) {
Test t = new Test();
Test t2 = new Test(5);
}
}
靜態方法
方法什麼時候定義爲靜態的?
大多數方法都定義爲實例方法,一般一個行爲或者一個動作在發生的時候,都需要對象的參與。但是也有例外的,例如:大多數“工具類”中的方法都是靜態方法,編寫工具類是爲了方便編程,爲了方便方法的調用,自然不需要new對象是最好的。
繼承
繼承的重要基礎知識
- 繼承“基本”的作用是:
代碼複用
。但是繼承最“重要”的作用是:有了繼承纔有了以後“方法的覆蓋”
和“多態機制”
。 - 繼承中的術語:
B類繼承A類,其中:
A類被稱爲:父類、基類、超類、superclass
B類被稱爲:子類、派生類、subclass - 子類繼承父類的哪些數據?
私有的不支持繼承
構造方法不支持繼承
其他數據都可以繼承 - Java語言只支持單繼承,但是一個類也可以間接繼承其他類
以下程序輸出爲:this is B (C的基類爲B類)
public class MyTest {
public static void main(String[] args) {
C c = new C();
c.doSome();
}
}
class A{
public void doSome(){
System.out.println("this is A");
}
}
class B{
public void doSome(){
System.out.println("this is B");
}
}
class C extends B{
}
方法覆蓋/重寫
回顧方法重載
- 方法重載爲Overload
- 方法重載什麼時候使用?
在同一個類中,方法完成的功能是相似的,建議方法名相同,這樣方便程序員的編程,就像在調用一個方法似的,代碼美觀。 - 什麼條件滿足之後構成方法重載?
- 在同一個類當中
- 方法名相同
- 參數列表不同:類型、順序、個數
- 方法重載和什麼無關?
- 和方法的返回值類型無關
- 和方法的修飾列表無關
方法覆蓋
- 方法覆蓋又被稱爲方法重寫,英文爲override/overwrite
- 什麼時候使用方法重寫?
父類中的方法已經無法滿足當前子類的業務需求,子類有必要將父類中繼承過來的方法進行重新編寫,這個重新編寫的過程稱爲方法重寫/方法覆蓋 - 什麼條件滿足之後發生重寫?
方法重寫發生在具有繼承關係的父子類之間 - 方法重寫的時候儘量複製粘貼,不要編寫,容易出錯,導致沒有產生覆蓋
- 注意:私有方法不能繼承,所以不能覆蓋;構造方法不能繼承,所以不能覆蓋;靜態方法不存在覆蓋;覆蓋只針對方法,不談屬性。
多態
多態涉及的概念
- 向上轉型(upcasting):子類型–>父類型,又被稱爲自動類型轉換
- 向下轉型(downcasting):父類型->子類型,又被稱爲強制類型轉換
- 無論是向上轉型還是向下轉型,兩種類型當中必須要有繼承關係。沒有繼承關係,程序是無法編譯通過的。
使用多態
public class MyTest {
public static void main(String[] args) {
Animal a = new Cat();
a.move();
}
}
class Animal{
public void move(){
System.out.println("動物在走路");
}
}
class Cat extends Animal{
public void move(){
System.out.println("貓在走路");
}
public void eatFish(){
System.out.println("貓喫魚");
}
}
【向上轉型】
在上面的代碼中我們看到Animal a = new Cat(); Java中是允許這種語法的,即父類引用指向子類對象,也即向上轉型。但是我們卻不能寫a.eatFish(); 因爲編譯器知道a爲Animal類型,但是調用a.move();的時候打印的是“貓在走路”。說明如下:
- java程序永遠分爲編譯階段和運行階段
- 編譯階段編譯器檢查a這個引用的數據類型爲Animal,由於Animal.class字節碼當中有move()方法,所以編譯通過了。這個過程稱爲
靜態綁定
,編譯階段綁定,只有靜態綁定成功之後纔有後續的運行。(對於eatFish方法,因爲在字節碼文件中沒有找到該方法,導致靜態綁定失敗,沒有綁定成功也就是編譯失敗) - 在運行階段,JVM堆內存當中真實創建的對象是Cat對象,那麼上述程序在運行階段一定調用Cat對象的move方法,此時發生了程序的
動態綁定
,運行階段綁定。 - 父類引用指向子類對象這種機制導致程序在編譯階段綁定和運行階段綁定兩種不同的形態/狀態,這種機制稱爲一種多態語法機制。
【向下轉型】
需求:要想讓上述的程序執行eatFish方法,該怎麼辦?
a的類型是Animal(父類),轉換成Cat(子類),被稱爲向下轉型/downcasting/強制類型轉換。當調用的方法是子類型中持有,在父類型中不存在,必須進行向下轉型。當然,向下轉型也需要兩種類型之間必須有繼承關係,不然編譯報錯。強制類型轉換需要加強制類型轉換符。
什麼時候進行向下轉型?
答:需要訪問子類對象當中特有的方法的時候
Cat c = (Cat) a;
c.eatFish();
強制類型轉換錯誤
若接着上面的程序,我們也一個鳥類(Bird繼承了Animal類),並且創建了一個鳥類對象,但是在下轉型的時候卻轉爲了Cat型,此時就發生了強制類型轉換錯誤。
Animal b = new Bird();
b.move();
Cat c2 = (Cat) b;
Exception in thread “main” java.lang.ClassCastException: Bird cannot be cast to Cat
at MyTest.main(MyTest.java:23)
分析:(1)編輯階段,編譯器檢查b的類型是Animal,Animal和Cat之間存在繼承關係,Animal是父類,Cat是子類,可以進行向下轉型,語法合格。(2)運行階段,JVM內存當中真實存在的對象是Bird類型,Bird對象無法轉換成Cat對象,因爲兩種類型之間不存在任何繼承關係,此時便出現了著名的強制類型轉換錯誤。
如何避免強制類型轉換錯誤
使用 instanceof 進行類型判斷。Java規範中要求:在進行強制類型轉換之前,建議採用instanceof運算符進行判斷,避免ClassCastException異常的發生。這是一種編程的好習慣。
Animal b = new Bird();
b.move();
if(b instanceof Bird){
Bird b2 = (Bird) b;
b2.move();
b2.fly();
}
else if(b instanceof Cat){
Cat c2 = (Cat) b;
c2.move();
c2.eatFish();
}
多態的作用
- 降低程序的耦合度,提高程序的擴展力
- 核心是:面向抽象編程,儘量不要使用面向具體編程
【以下案例說明多態的作用】
場景是:有一個主人要給寵物餵食,寵物分貓和狗。抽象爲主人有餵養動作,寵物有喫食物動作。主人給貓餵食時顯示貓喫魚,給狗餵食時顯示狗啃骨頭。
未使用多態:
public class MyTest {
public static void main(String[] args) {
Master zhangsan = new Master();
Cat cat = new Cat();
zhangsan.feed(cat);
Dog dog = new Dog();
zhangsan.feed(dog);
}
}
class Master{
public void feed(Cat cat){
cat.eat();
}
public void feed(Dog dog){
dog.eat();
}
}
class Cat{
public void eat(){
System.out.println("貓喫魚");
}
}
class Dog{
public void eat(){
System.out.println("狗啃骨頭");
}
}
分析一波:若主人又添加了一個寵物,如鸚鵡,當給鸚鵡餵食時,需要在Master類中重載feed方法,這樣程序擴展極其不便,程序改動大。下面使用多態來實現。
使用多態:
public class MyTest {
public static void main(String[] args) {
Master zhangsan = new Master();
Cat cat= new Cat();
zhangsan.feed(cat);
Dog dog = new Dog();
zhangsan.feed(dog);
}
}
class Master{
public void feed(Pet pet){ //父類引用指向子類對象
pet.eat(); //運行態調用的是實例對象的方法
}
}
class Cat extends Pet{
public void eat(){
System.out.println("貓喫魚");
}
}
class Dog extends Pet{
public void eat(){
System.out.println("狗啃骨頭");
}
}
class Pet{
public void eat(){
}
}
在上述方法中,我們只要定義一個寵物類,然後讓其他的具體的寵物都類繼承這個類,在餵食動作中,我們放進去的形參也只是寵物,這樣在用給具體的寵物餵食時就用到了自動類型轉換(靜態綁定),而在pet.eat();時卻是具體的寵物的喫食動作(動態綁定)。
final
- final是一個關鍵字,表示最終的,不可變的
- final修飾的類無法被繼承
- final修飾的方法無法被覆蓋
- final修飾的變量一旦賦值之後,不可重新賦值
- final修飾的實例變量必須手動賦值,不能採用系統默認值
- final修飾的引用一旦指向某個對象之後,不能再指向其他對象,並且被指向的對象無法被垃圾回收器回收(必須等到程序運行結束);final修飾的引用雖然指向某個對象之後不能指向其他對象,但是所指向的對象內部的值是可以被修改的
- final修飾的實例變量是不可變的,這種變量一般和static聯合使用,被稱爲常量。
常量定義的語法格式:public static final 類型 常量名 = 值;
如public static final double PI = 3.1415926;(Java規範中要求所有常量的名字全部大寫,每個單詞之間使用下劃線連接)
對第5點說明:實例變量有默認值+final修飾的變量一旦賦值不能重新賦值。綜合考慮,Java語言最終規定實例變量使用final修飾之後必須手動賦初值,不能採用系統默認值。
public class MyTest {
// final int age;//編譯錯誤
//第一種解決方案
final int age = 10;
//第二種解決方案
final int num;
public MyTest(){
num = 10;
}
public static void main(String[] args) {
final int a;
a = 10;
//不可二次賦值
// a = 20;
}
}
在構造方法中爲final修飾的實例變量賦值的效果與在定義時賦值的效果是等效的,因爲這兩者的賦值時間相同,都是在構造方法執行過程中給實例變量賦值。