1. 程式人生 > >Java程式設計思想--第二章 一切都是物件

Java程式設計思想--第二章 一切都是物件

2.1用引用操縱物件
一切都看作物件,但操作的識別符號實際是物件的一個引用(reference);

例如遙控器(引用)來操縱電視機(物件),實際操控的遙控器(引用),再由遙控器來調控電視機(物件)。如果在房間四處走動,同時又想調控電視機,那麼只需攜帶遙控器(引用),而不是電視機(物件)。

沒有電視機,遙控器可獨立存在,也就是說,你擁有一個引用,並不一定要有一個物件與它關聯,例如建立一個String的引用:

String s;

這裡建立的只是引用,並不是物件,如果此時向s傳送一個訊息,就會返回一個執行時錯誤,此時s沒有與任何物件關聯,因此,比較安全的做法是:建立一個引用的同時便進行初始化。

String s="hello123";

此處java語言的一個特性,字串可以用帶引號的文字初始化,但是還是建議對物件採用更通用的初始化方法。

2.2 必須由你建立所有物件
當你建立一個引用時,就希望它能與一個新的物件想關聯,通常我們是通過new操作符來實現的,2.1裡的例子可以寫成:

String s=new String("hello123");

2.2.1儲存到什麼地方

程式執行時,物件怎麼進行放置安排的?記憶體怎麼分配的?有5個不同的地方可以儲存資料:
1) 暫存器,最快的儲存區,因為它位於處理器內部,不同於其他儲存區的地方。但是暫存器的數量有限,因此暫存器需要根據需求進行分配,而不能人為控制;

2) 堆疊,位於通用RAM(隨機訪問儲存器),也即記憶體中,但通過堆疊指標可以從處理器那裡獲得直接支援。堆疊指標若向下移動,則分配新的記憶體;若向上移動,則釋放那些記憶體。這是一種快速有效的分配儲存方法,僅次於暫存器。建立程式時,java系統必須知道儲存在堆疊內所有項的確切生命週期,以便上下移動堆疊指標,這一約束限制了程式的靈活性,所以雖然某些java資料儲存在堆疊中—-特別是物件引用,但是java物件並不儲存與其中;

3) 堆 , 一種通用的記憶體池(位於RAM區),用於存放所有的java 物件,堆不同於堆疊的好處是:編譯器不需要知道儲存的資料在堆裡存活多長時間。因此,在堆裡分配儲存有很大的靈活性。當需要一個物件時,只需用new寫一行簡單的程式碼,當執行這行程式碼時,會自動在堆裡進行記憶體分配,當然,為這種靈活性必須付出相應的代價,用堆進行儲存分配和清理可能比用堆疊進行儲存需要更多的時間;

4)常量儲存 , 常量值通常直接存放在程式程式碼內部,這樣做是安全的,因為他們永遠不會被改變。有時,在嵌入式系統中,常量本身會和其他部分分隔離開,所以在這種情況下,可以選擇將其存放在ROM(只讀儲存器)中;

5)非RAM儲存 , 如果資料完全存活於程式之外,那麼它可以不受程式的任何控制,在程式沒有執行時也可以存在。器中兩個基本的例子是“流物件”和“持久化物件”。在“流物件”中,物件轉化成位元組流,通常被髮送給另一臺機器。在“持久化物件”中,物件被存放於磁碟上,因此,即使程式終止,它們仍可以保持自己的狀態。這種儲存方式的技巧在於:把物件轉化成可以存放在其他媒介上的事物,在需要時,可恢復成常規的,基於RAM的物件。

2.2 特例—-基本型別
在程式設計中經常用到一系列型別,它們需要特殊對待。你可以把它們想象成“基本”型別。之所以特殊對待,是因為new將物件存在“堆”裡,故用new建立一個物件——特別是小的、簡單的變數,往往不是很有效。因此,對於這些型別,Java採取與C和C++相同的方法。也就是說,不用new來建立變數,而是建立一個並非是“引用”的“自動”變數。這個變數擁有它的“值”,並置於堆疊中,因此更加高效。
Java確定每種基本型別所佔儲存空間的大小,不會隨著硬體架構而改變,儲存空間大小的不變形是java程式更具可移植性
這裡寫圖片描述
所有數值型別都有正負號,所以不要去尋找無符號的數值型別。
Boolean型別所佔的空間大小沒有明確指出,僅定義為能夠取字面值的true和false;

基本型別具有包裝器類,繼承Object類,可以在堆中建立一個非基本物件,用來表示對應的基本型別,例如:

int a=10;
Integer object=new Integer(10);

注:這裡涉及到自動拆箱和自動裝箱,之後會提到

—–高精度數字

java 提供了兩個用於高精度計算的類:BigInteger和BigDecimal(java.math.*)。雖然它們大體上屬於”包裝器類”的範疇,但二者都沒有對應的基本型別。這兩個類包含的方法,提供的操作與對基本型別所能執行的操作相似,只不過必須以方法呼叫方式取代運算子方式來實現,關於呼叫這兩個類的構造器和方法的詳細資訊,請查閱JDK文件
或者參考一下這篇文章處理大數字BigInteger與BigDecimal

——java中的陣列
C和C++的陣列就是記憶體塊,如果一個程式訪問其自身記憶體塊之外的陣列,或在陣列初始化前使用記憶體,會產生難以預料的後果。

java的主要目標之一是安全性,java確保陣列會被初始化,而且不能在它的範圍之外被訪問,這種範圍檢查,是以每個陣列上少量的記憶體開銷及執行時的下標檢查為代價的,但由此換來的是安全性和效率的提高,因此付出的代價是值得的。

當建立一個數組物件時,實際上就是建立了一個引用陣列,並且每個引用都會自動被初始化為一個特定值,該值擁有自己的關鍵字null,一旦java 看到null,就知道這個引用還沒有指向某個物件,在使用任何引用前
必須為其指定一個物件,如果試圖使用一個還是null的引用,在執行時將會報錯,因此,常犯的陣列錯誤在java中就可以避免,還可以建立用來存放基本資料型別的陣列,同樣,編譯器也能確保這種陣列的初始化,因為它會將這種陣列所佔的記憶體全部置零。

2.3 永遠不需要銷燬物件
2.3.1 作用域
作用域決定了在其內定義的變數名的可見性和生命週期,在C,C++和Java中,作用域由花括號的位置決定,
例如:

 {
        int a=1;
        //此處只有a可以用
        {
          int b=2;
          //此處a和b都可以用
        }
        //此處只有a可以用,b已經超過作用域了
    }

在作用域裡定義的變數只可用於作用域結束之前

在C和C++中以下程式碼是合法的,但在java中是錯的,

 {
      int a=2;
      {
        int a=3;  //不合法Illegal
      }
  }
編譯器會報告變數a已經定義過,所以,在C和C++中將一個較大作用域的變數“隱藏”起來的做法,在java中是不允許的,因為java設計者認為這樣會導致程式混亂。

2.3.2 物件的作用域
java 物件不具備和基本型別一樣的生命週期,當用new建立一個java物件時,它可以存活於作用域之外,
所以假如這樣寫:
   {
        String s=new String("hello123");
    }
引用s在作用域終點就消失了,然而,s指向的String物件仍繼續佔據記憶體空間,在這一段程式碼中,我們無法在這個作用域之後訪問這個物件,因為對它唯一的引用已超出了作用域的範圍;
事實證明:
由new建立的物件,只要你需要,就會一直保留下去,這樣,許多C++的程式設計問題在java中就完全消失了,在C++中,你不禁必須要確保物件的保留時間與你需要這些物件的時間一樣長,而且還必須在你使用完它們之後,將其銷燬。

大家想一下,如果java讓物件繼續存在,那麼靠什麼才能防止這些物件填滿記憶體空間,進而阻塞你的程式呢?
這正是C++裡面可能發生的問題,這也是java神奇之所在,java 有一個垃圾回收器,用來監視用new建立的所有物件,並辨別哪些不會再被引用的物件,隨後釋放這些物件的記憶體空間,以便供其他新的物件使用,也就是說,你根本不必擔心記憶體回收的問題,你只需要建立物件,一旦不再需要,它們就會自行消失,這樣做就消除了這類程式設計問題(即“記憶體洩漏”),這是由於程式設計師忘記釋放記憶體而產生的問題。

2.4 建立新的資料型別:類
class 關鍵字 之後緊跟著的是新型別的名稱,例如
class ClassA{
/*
Class Body
*/
}
這樣就建立了一種新的型別,你可以用new 建立這個ClassA這個型別的物件了;
ClassA a=new ClassA();
但是,在定義它的所有方法之前,還沒有辦法能讓它去做更多的事情。

2.4.1 欄位和方法
一旦定義了一個類(在java中你所做的全部工作就是定義類,產生哪些類的物件,以及傳送訊息給這些物件),就可以在
類中設定兩種型別的元素:欄位(或稱資料成員)和方法(或稱成員函式),欄位可以是任何型別的物件,可以通過其
引用與其進行通訊,也可以是基本型別中的一種,如果欄位是對某個物件的引用,那麼必須初始化該引用,以便使其
與一個實際的物件相關聯。
每個物件都有用來儲存其欄位的空間,普通欄位不能在物件間共享,下面是一個具有某些欄位的類:

    class DataOnly{
      int i;
      double d;
      boolean b;
    }

儘管這個類除了儲存資料之外什麼也不能做,但是仍舊可以像下面這樣建立它的一個物件:

    DataOnly data=new DataOnly();

可以給欄位賦值,但首先必須要知道如何引用一個物件的成員,具體的實現為:在物件引用的名稱之後緊接著一個句點,然後再接著是物件內部的成員名稱:

    objectReference.member(物件引用的成員)
    例如:
    data.i=2;
    data.d=1.1;
    data.b=true;
想修改的資料也有可能位於物件所包含的其他物件中,在這種情況下,只需要再使用連線句點即可,例如:
    myPlane.leftTank.capacity=100;

DataOnly類除了儲存資料外沒別的用處,因為它沒有任何成員方法,如果想了解成員方法的執行機制,就得先了解引數和返回值的概念,稍後將對此作簡略描述。

——基本成員預設值
若類的某個成員是基本資料型別,即使沒有初始化,java也會確保它獲得一個預設值,以確保哪些是基本型別的成員變數得到初始化(C++沒有此功能),防止產生程式錯誤,但是,這些初始化對你的程式來說,可能是不正確的,甚至是不合法的,所以最好明確地對變數進行初始化。
然而上述確保初始化的方法並不適用於”區域性“變數(即並非某個類的欄位)。因此,如果在某個方法定義中有 int x;那麼變數x得到的可能是任意值(與C和C++中一樣),而不會被自動初始化為零,所以在使用x前,應當對其賦一個適當的值,如果忘記了這麼做,java會在編譯時返回一個錯誤,告訴你此變數沒有初始化,這正是java優於C++的地方,(許多C++編譯器會對未初始化變數給予警告,而java則視為錯誤)

2.5 方法、引數和返回值

許多程式設計語言(像C和C++)用函式這個術語來描述命名子程式,而在java裡卻常用方法這個術語來表示”做某些事情 的方式”。
java的方法決定了一個物件能夠接收什麼樣的訊息。方法的基本組成部分包括:名稱、引數、返回值和方法體。
下面是它最基本的形式:

ReturnType methodName(/* argument list*/){
/* Method Body*/
}

返回型別描述的是在呼叫方法之後從方法返回的值,引數列表給出了要傳給方法的資訊的型別和名稱,方法名 和引數列表(合起來稱為“方法簽名”)唯一的標識出某個方法。
java中的方法只能作為類的一部分來建立,方法只能通過物件呼叫,且這個物件必須能執行這個方法呼叫。
如果試圖在某個物件上呼叫它並不具備的方法,那麼在編譯時就會得到一條錯誤資訊,通過物件呼叫方法時,需要先列出物件名,緊接著是句點,然後是方法名和引數列表。如:

 objectName.methodName(arg1,arg2,arg3);

例如 物件a,可以呼叫方法f(),返回值為int
int x=a.f();
這種呼叫方法的行為通常稱為傳送訊息給物件,訊息為f(),物件為a。面向物件的
程式設計通常稱為“向物件傳送訊息”。
2.5.1 引數列表
方法的引數列表也是採用物件形式,在引數列表必須指定每個所傳遞物件的型別和名字,java中任何傳遞物件的場合,實際上傳遞的也是引用,並且引用的型別必須正確,如果引數被設為String型別,則必須傳遞一個String物件,否則比那一起將丟擲錯誤。
例如:

          int storage(){
              return s.length() +1;
          }

這個方法必須置於某個類的定義內才能被正確編譯。

將s傳遞給此方法,就可以把他當作其他物件一樣進行處理(可以給它傳遞訊息),在這裡,s的length()方法被呼叫,它是String類提供的方法之一,會返回字串包含的字元數。
return關鍵字包括兩方面:
1、代表“已經做完,離開此方法”;
2,如果方法返回值,這個值要放在return語句後面,這個例子中返回值是計算s.length()+1這個表示式得到的。

返回值可以返回任意型別,如果不想返回值,可以指示此方法返回void(空),例子如下:

      boolean flag() {return true;}
      double aaa(){return 2.73;}
      void nothing{return;}
      void nothing2{}
  若返回型別是void,return關鍵字的作用只是退出方法,因此,沒有必要到方法結束時才離開,可在任何地方返回,但如果返回型別不是void,那麼無論在何處返回,編譯器都會強制返回一個正確型別的返回值。

2.6 構建一個java程式
2.6.1 名字可見性
如果在程式的某個模組使用了一個名字,在程式的另一模組也使用了相同的名字,為了避免名字重複引發的問題,java採用了全新的方法避免問題,為了給一個類庫生成不會與其他名字混淆的名字,java設計者希望程式設計師反過來使用自己的域名,因為這樣可以保證他們肯定是獨一無二的,我的域名是www.lyt.com,我的各種類庫都被命名為
com.lyt.XXX(XXX可以是支付,也可以是購買),反轉後,句點代表子目錄的劃分。這種機制意味著所有的檔案都能夠自動存活於自己的名字空間內,而且同一個檔案內的每個類都有唯一的識別符號。
2.6.2 運用其他構件
如果想在自己的程式中使用預先定義好的類,那麼編譯器就必須知道怎麼定位它們,當然,這個類可能就在發出呼叫的那個原始檔中,在這種情況下, 就可以直接使用這個類-即使這個類在檔案的後面才會被定義(java 消除了所謂的“向前引用”問題)
如果那個類位於其他檔案中,可以使用關鍵字import來準確的告訴編譯器想要的類是什麼。import指示編輯器匯入一個包,也就是一個類庫。
大多時候,我們使用與編譯器附在一起的java標準庫裡的構件。例如:
import java.util.ArrayList;
這行程式碼告訴編譯器,你想使用java的ArrayList類,但是util包含了數量眾多的類,有時你想使用其中的幾個,同時又不想明確地逐一宣告:那麼你很容易使用萬用字元“*”來達到這個目的:
import java.util.*;
2.6.3 static 關鍵字
當建立類時,實際就是描述類的物件的外觀和行為。執行new來建立物件時,資料儲存空間才被分配,其方法才能被外界呼叫。
有兩種用上述方法無法解決:
1、只想為某特定域分配單一儲存空間,而不考慮建立物件的個數;
2、希望某個方法不與包含它的類的任何物件關聯在一起,也就是說,即使沒有建立物件,也能夠呼叫這個方法。
通過static關鍵字可以滿足這兩方面的需要,當宣告一個事物是static時,就意味著這個域或方法不會與包含它的類的任何物件例項關聯在一起,所以,即使從未建立過某個類的物件,也可以呼叫其static方法或訪問其static域,通常,你必須建立一個物件,並用它來訪問資料或方法。因為非static域和方法必須知道他們一起運作的特定物件。
例如:

       class User(){
          static int age=17;
        }

生成了一個static欄位,並進行了初始化;現在建立兩個StaticTest物件,User.age也只有一份儲存空間, 這兩個物件共享一個age,
User user=new User();
User user1=new User();
user.age和user1.age指向同一儲存空間,因此他們具有相同的值17。
引用static變數有兩種方法:
1、可以通過物件定位它,user.age;
2、也可以通過其類名直接引用,User.age,而對於非靜態成員則不行。
使用類名引用static變數是首選,靜態方法和靜態物件的操作類似,引用可以用ClassName.method()形式,定義靜態方法:

        class User(){
            static void sing(){
                //唱歌
            }
        }

當static作用於某個欄位時,會改變資料建立的方式(因為一個static欄位對每個類來說都只有一份儲存空間,而非static欄位則是對每個物件有一個儲存空間),但是如果static作用於某個方法,差別卻沒那麼大, static方法的一個重要用法就是在不建立任何物件的前提下就可以呼叫它,這一點對於定義main()方法很重要,這個方法是執行一個應用的入口點。

        import java.util.*;
        public class Util {

          public static void main(String[] args) {

              System.out.println(new Date());   // java標準庫裡的Date類
            }
          }

程式開頭,必須宣告import語句,引入在檔案程式碼中需要用到的額外類,之所以說它們“額外”,是因為有一個特定類會自動被匯入到每一個java檔案中:java.lang。java.lang是預設匯入到每個java檔案中的,所以它的所有類都可以被直接使用。system是它裡面的類。
類的名字必須和檔名相同,如果建立一個獨立執行的程式,檔案中必須存在某個類和該檔案同名,且那個類必須包含一個名為main()的方法,

         public static void main(String[] args){

         }

其中 public 關鍵字是指這是一個可由外部呼叫的方法,main()方法的引數是一個String物件的陣列,程式中為用到args,但是java編譯器要求必須這麼做,因為args要用來儲存命令列引數。
列印完後,Date物件就不再被使用,而垃圾回收器會發現這一情況,並在任意時刻回收。
2.7.1 編譯和執行
要編譯、執行java程式,首先要有一個java開發環境,安裝好jdk,配置好路徑,確保計算機能找到javac和java這兩個檔案,開啟到java檔案所在目錄,輸入

       javac HelloWorld.java   //編譯
       //正常不會有任何反應,如果有錯會返回錯誤,
       接著執行java HelloWord   //執行

2.8 註釋和嵌入式文件

java中有兩種註釋風格

         /**
       *第一種
       *
        */

        /**
          第一種            
         */// 第二種 單行註釋

2.8.1 註釋文件
javadoc用於提取註釋的工具,是jdk安裝的一部分,提取註釋標籤,輸出一個HTML檔案,用於Web瀏覽器檢視。
2.8.2 語法
三種類型的註釋文件,對應註釋後面的三種元素:類、域、方法。

         /**
          A Class comment
         */
        public class Documentationl{
            /**
             A field comment
             */
            public int i;
            /**
            A method comment
             */
             public void f(){}
        }