《Java JDK8學習筆記》讀書筆記(4)

第4章 認識對象
學習目標
 區分基本類型與對象類型
 瞭解對象與引用的關係
 從打包器認識對象
 以對象觀點看待數組
 認識字符串的特性
4.1 類與對象
第3章講的都是基本類型,這一章開始講對象類型。在Java中一切皆對象(Object),要產生對象必須先寫類(Class),類是對象的模版,對象是類的實例(Instance)
4.1.1 定義類
一個類就是一個模版,它包括了所有的屬性和方法,就象設計師手上的設計圖紙一樣,如果我們需要設計一個大黃鴨,那麼我們首先會繪製一個大黃鴨的設計圖,上面定義了大黃鴨的大小、顏色等,我們會根據設計圖製作出實際的大黃鴨,每個大黃鴨都是同一個款式,但會擁有自己的大小,顏色。
大黃鴨
圖4.1 大黃鴨設計、製作
如果我們要設計的就是一個關於大黃鴨的設計的軟件,那如何使用Java來編寫呢?首先就要在程序中定義類,這相當於圖4.1中大黃鴨的設計圖:

class BigDuke{
    String color;
    int size;
}

類定義時使用class關鍵字,名稱使用BigDuck,相當於爲大黃鴨的設計圖取名叫BigDuck,大黃鴨的顏色用字符串表示,也就是color變量,可存儲”深黃”,”淺黃”,”大黃”等。
大黃鴨的尺寸會是’大’,’中’,’小’,所以使用了char類型。如果要在程序中,利用BigDuck在程序內建立實例,就要使用new關鍵字。例如:

new BigDuck();

在對象術語中,這叫做實例化一個對象。如果要有個標籤,和這個對象綁定,可以這樣聲明:
BigDuck bd;
是不是很象前面我們聲明基本變量的形式?不過Java專門爲這種指向對象的“變量”取了個術語,叫“引用”(Reference Name)。如果要將bd綁到新建的對象上,可以使用“=”賦值,在Java術語中,叫“將bd引用至實例化對象”。如:

BigDuck bd=new BigDuck();

這裏寫圖片描述
圖4.2 class、new、=等語法對應
提示:對象(Object)與實例(Instance)在Java中幾乎是等義的名詞,本書中就視爲相同意義。
在BigDuck類中,定義了color與size兩個變量,以Java的術語來說,叫作定義兩個屬性(Field)成員,或叫定義兩個成員變量,這表示每個新建的BigDuck實例中都能擁有各自不同的color值與size值。(默然說話:其實簡單說,就是類是一個模版,它定義的類別應該具有的屬性,但沒有值,而實例對象則會具備相同的屬性,且每個屬性可以有不同的值。比如人類可以定義膚色與身高,然後每個人類的實例可以擁有不同的膚色值與身高值。)

class BigDuck{
    String color;
    char size;
}
/**
 * 說明類和對象關係的代碼示例
 * 文件:ObjectTest.java
 * @author mouyo
 */
public class ObjectTest {
    public static void main(String[] args) {
        //實例化大黃鴨對象
        BigDuck foodDuck=new BigDuck();
        BigDuck lakeDuck=new BigDuck();

        //爲每個大黃鴨對象的屬性賦值
        foodDuck.color="淺黃";
        foodDuck.size='小';
        lakeDuck.color="深黃";
        lakeDuck.size='大';

        //顯示每個大黃鴨對象的各自屬性值
        System.out.println("黃鴨飯:"+foodDuck.color+","+foodDuck.size);
        System.out.println("湖中大黃鴨:"+lakeDuck.color+","+lakeDuck.size);
    }
}

在這個ObjectTest.java中,定義了兩個類,一個是公開(public)的ObjectTest類,Java規定,公開(public)的類名必須與文件一致。另一個是非公開的BigDuck。

提示:只要有類定義,編譯程序就會產生一個.class文件。上例中會產生ObjectTest.class和BigDuck.class兩個文件

程序中實例化了兩個BigDuck對象,並分別聲明瞭foodDuck與lakeDuck兩個引用變量,接着指定了foodDuck綁定的對象所擁有的color和size分別賦值爲”淺黃”和’小’,而lakeDuck綁定的對象所擁有的color和size分別賦值爲”深黃”和’大’。最後分別顯示foodDuck與lakeDuck各自擁有的屬性值:

run:
黃鴨飯:淺黃,小
湖中大黃鴨:深黃,大
成功構建 (總時間: 0 秒)

執行結果如上,可以看出來,每個對象都擁有自己的數據,它們是相互獨立,互不影響的。(默然說話:這也就在提醒我們,當我們進行屬性賦值的時候,除了要關心屬性的值之外,還要關心被賦值的屬性是哪個對象,如果搞錯了對象,那值也就是不對的了。)
觀察爲每個大黃鴨對象的屬性賦值的那段代碼,你會發現它們雖然是給不同的對象賦值,但代碼的書寫基本是一樣的。只要是一樣的代碼,我們就可以進行模版化,讓這些代碼只寫一遍。這裏我們可以使用構造方法。構造方法是與類名同名的方法,它沒有返回值。

public class BigDuck2 {
   String color;
   char size;
   BigDuck2(String color,char size){//構造方法
       this.color=color;//將參數color指定給color屬性
       this.size=size;
   }

    public static void main(String[] args) {
        BigDuck2 foodDuck=new BigDuck2("淺黃",'小');//使用指定構造方法建立對象
        BigDuck2 lakeDuck=new BigDuck2("深黃",'大');

        //顯示每個大黃鴨對象的各自屬性值
        System.out.println("黃鴨飯:"+foodDuck.color+","+foodDuck.size);
        System.out.println("湖中大黃鴨:"+lakeDuck.color+","+lakeDuck.size);
    }
}

在這個例子中,定義新建對象時,必須傳入兩個參數給String類型的color與char類型size參數,而構造方法中,由於color參數與color屬性名相同,你不可以直接寫color=color,而要寫成this.color=color。
在實際使用new創建對象時,就可以直接傳入字符串與字符,分別代表BigDuck實例的color與size值,執行結果與上個例子相同。
4.1.2 使用標準類
第一章已經講過,Java Se提供了標準API,這些API就是由許多類組成,你可以直接取用這些標準類,省去自己打造基礎的需求。下面舉兩個基本的標準類:java.util.Scanner和java.math.BigDecimal。
1.使用java.util.Scanner
目前爲止的程序例子都很無聊,變量值都是寫死的,沒有辦法接受用戶的輸入。如果要在命令行模式下取得用戶輸入,我們就可以使用java.util.Scanner。

package cn.com.speakermore.ch04;

import java.util.Random;
import java.util.Scanner;//導入標準API中的Scanner類

/**
 * 猜數
 * @author mouyo
 */
public class Guess {
    public static void main(String[] args) {
        Scanner input=new Scanner(System.in);//創建Scanner對象
        int number=new Random().nextInt(10);
        int guess;

        do{
            System.out.print("猜數(0~9)");
            guess=input.nextInt();//獲得用戶輸入的整型數
        }while(guess!=number);
        System.out.println("恭喜!猜對了!");
    }
}

由於我們不想每次都輸入java.util.Scanner,所以一開始就使用import告訴編譯器:“我需要使用java.util.Scanner,請幫我導入。”在實例化(new)Scanner對象時,必須傳入System.in對象,它是一個java.io.InputStream流對象(默然說話:關於這些內容,第10章會詳細介紹,你現在就當做烏龜的屁股——規定——來照着寫就可以了)。
接下來就可以很簡單的通過Scanner的對象(名字叫input)來獲得鍵盤的輸入了。大家完全不用管它是怎麼獲得鍵盤輸入的,反正就是能獲得。(默然說話:這就是面向對象裏面的一個概念:封裝。舉個生活中的例子:你只要懂得打開水龍頭,就可以得到水,而完全不用去關心水怎麼是從水龍頭裏流出來的。忽略細節,可以讓我們更專心的做自己想要去完成的事情,而不用啥都操心,這就是封裝。後面的相關章節還會詳細闡述。)
Scanner的nextInt()方法會查看鍵盤輸入的是不是數字,如果是數字就會接收。Scanner對每個基本類型,都會有個對應的nextXxx()方法,如nextByte()、nextShort()、nextLong()、nextFloat()、nextDouble()、nextBoolean()等,如果要取字符串,則使用next(),如果是取一行字符串,則使用nextLine()(以換行分隔)。
提示:包是java管理類的一種方式,後面相關內容會介紹。包名以java開頭的類,都是標準API提供的類。
2.使用java.math.BigDecimal
第2章介紹基本類型的時候有個問題:1.0-0.8的結果是多少?答案肯定不是0.2,而是0.19999999999999996。爲什麼?java的bug?肯定不是,因爲你用其他的語言來算這個算式也會得到同樣的結果。
簡單來說,java遵守IEEE754浮點數運算規範,使用分數與指數來表示浮點數。例如0.5是使用1/2來表示,0.75是使用1/2+1/4來表示,而0.1會使用1/16+1/32+1/256+…無限循環下去,無法精確表示。因而造成運算上的誤差。
再來舉個例子,你覺得下面的程序片段會顯示什麼結果?

        double a=0.1;
        double b=0.1;
        double c=0.1;

        if((a+b+c)==0.3){
            System.out.println("等於0.3");
        }else{
            System.out.println("不等於0.3");
        }

由於浮點數誤差的關係,結果是顯示“不等於0.3”。類似的例子還有很多,結論就是,如果要求精確度,那就要小心地使用浮點數,而且別用==直接比較浮點數運算結果。
那麼要怎麼辦能得到更好的精確度?可以使用java.math.BigDecimal類。

import java.math.BigDecimal;
/**
 *DecimalDemo.java
 * @author mouyo
 */

public class DecimalDemo {
    public static void main(String[] args) {
        BigDecimal a=new BigDecimal("1.0");
        BigDecimal b=new BigDecimal("0.8");
        BigDecimal result=a.subtract(b);
        System.out.println(result);
        System.out.println(1.0-0.8);
    }
}

創建BigDecimal的方法之一是使用字符串,BigDecimal在創建時會分析字符串,以默認精度進行接下來的運算。BigDecimal提供有plus()、substract()、multiply()、divide()等方法,可以進行加、減、乘、除等運算,這些方法都會返回代表運算結果的BigDecimal。
上面的例子可以看到0.2的結果,再來看利用BigDecimal比較相等的例子。

import java.math.BigDecimal;

/**
 *DecimalDemo2.java
 * @author mouyo
 */
public class DecimalDemo2 {
    public static void main(String[] args) {
        BigDecimal a=new BigDecimal("0.1");
        BigDecimal b=new BigDecimal("0.1");
        BigDecimal c=new BigDecimal("0.1");
        BigDecimal result=new BigDecimal("0.3");

        if(a.add(b).add(c).equals(result)){
            System.out.println("等於0.3");
        }else{
            System.out.println("不等於0.3");
        }
    }
}

由於BigDecimal的add()方法都會返回代表運算結果的BigDecimal,所以就直接利用返回再次add()。最後再調用equals()方法比較相等。最後的結果是等於0.3。
4.1.3 對象指定與相等性
在上一例中,比較兩個BigDecimal是否相等,是使用equals()方法而非使用==運算符,爲什麼?前面說過,在Java中有兩大類型系統,基本類型和引用類型,基本類型把值直接放到變量中,而引用類型是把對象的內存地址放入變量中。
當使用=(默然說話:記住!這個不是等號,是賦值號,用來把一個值放到某個變量中!)給基本變量賦值時,就是把值直接放入了變量中,而引用類型變量賦值時,放入的是一個對象的所在位置的地址。正因爲如此,當使用==進行比較時,基本變量就是比較了值是否相等(默然說話:因爲基本變量就保存了一個值呀!)而引用類型變量比較的卻是內存位置是否相等(因爲引用類型變量保存的是地址呀!)。
這是基本類型變量的代碼與示例圖,記住,基本類型保存的是值!

        int a=10;
        int b=10;
        int c=a;
        System.out.println(a==b);
        System.out.println(a==c);

這裏寫圖片描述這裏寫圖片描述
圖4.3 基本變量的賦值與比較相等的結果 圖4.4 基本變量的賦值與比較相等的原理
這是引用類型變量的代碼與示例圖,記住,引用類型保存的是地址!

      BigDecimal a=new BigDecimal("0.1");
        BigDecimal b=new BigDecimal("0.1");
        BigDecimal c=a;
        System.out.println(a==b);
        System.out.println(a==c);

這裏寫圖片描述 這裏寫圖片描述
圖4.5 引用類型變量的賦值與比較相等 圖4.6 引用類型變量的比較相等結果
使用==比較的就是變量裏所保存的值是否相等,由於引用類型保存的值是對象的內存地址,所以比較兩個引用型變量相等,就是比較兩個引用變量所引用的對象是不是同一個(默然說話:內存地址相同,就是同一個變量,因爲不可能在同一個內存地址中放兩個不同的對象。),如果我們想要的結果是兩個對象的內容是不是一樣,我們需要使用equals()方法。(默然說話:上面的程序中可以看到兩個對象都裝着“0.1”這個數,所以如果我們是要兩個對象裝的內容是不是一樣,是不能使用==來比較的,因爲它比較的是地址)
提示:其實從內存的實際運作來看,=與==對於基本類型變量與引用類型變量的作用並沒有不同,只是因爲它們所保存的值的意義不一樣,才造成了這一區別。
4.2 基本類型包裝類
基本類型long、int、double、float、boolean等,在J2SE5.0之前必須手動使用Long、Integer、Double、Float、Boolean等打包爲對象,才能當作對象來操作。5.0之後開始自動打包了。
4.2.1 包裝基本類型
Java的基本類型在於效率,但更多的時候,會使用類建立實例,因爲對象本身可以提供更多信息,更多方便的操作。可以使用Long、Integer、Double、Float、Boolean、Byte等類來包裝(Wrap)基本類型。
Long、Integer、Double等類就是所謂的包裝類(Wrapper),正如此名稱所示,這些類主要目的就是提供對象實例作爲“殼”,將基本類型包裝在對象中,就像是將基本類型當作對象操作。

/**
 *  IntegerDemo.java
 * @author mouyo
 */
public class IntegerDemo {
    public static void main(String[] args) {
        //基本類型
        int data1=10;
        int data2=10;
        //包裝類型
        Integer wrapper1=new Integer(data1);
        Integer wrapper2=new Integer(data2);
        //基本類型除法,只能得到整數部分,小數被捨棄
        System.out.println(data1/3);
        //使用包裝類的方法把整數轉爲小數,得到了有小數的結果
        System.out.println(wrapper1.doubleValue()/3);
        //使用包裝類提供的方法進行比較,得到更多的信息
        System.out.println(wrapper2.compareTo(wrapper2));
    }
}

包裝類都放在java.lang包中,這個包是java編譯器默認會導入的包,所以不需要import語句來顯示導入。包裝一個基本類型就是new出包裝類,然後將基本類型傳給包裝類就可以了。
基本數據類型如果都是整型,那最後的計算結果也會是整形。
如果我們期望得到小數的結果,那麼可以使用包裝類的轉換方法來得到一個小數,然後計算機在運算時會自動類型轉換爲小數後再做計算,最後的結果就也是小數(默然說話:其實我們平時寫程序時,是直接把整型數寫成小數。比如10/3.0,這樣的結果就會是小數了。)。
Integer提供compareTo()方法,可與另一個Integer對象比較,如果相同就返回0,如果小於傳入的對象就返回-1。否則就是1。而==和!=只能得到很少的信息。
4.2.2 自動裝箱、拆箱
除了使用new來包裝之外,從J2SE5.0之後提供了自動裝箱功能。可以這樣包裝基本類型:
Integer wrapper=10;
編譯程序會自動判斷是否能進行自動裝箱,在上例如你的wrapper會被賦值爲一個Integer對象。其他基本類型也是一樣的。改寫下上面的代碼:

Integer data1=10;
Integer data2=20;
System.out.println(data1.doubleValue() / 3);
System.out.println(data1.compareTo(data2));

程序看上去簡潔很多,data1和data2在運行時會自動裝箱Integer對象。自動裝箱還可以這樣用:

int i=0;
Integer wrapper=i;

也可以使用更一般化的Number來自動裝箱:

Number num=3.11f;

3.11f會先被自動裝箱爲Float,然後引用給num。
Java SE 5.0開始,還可以進行自動拆箱,也就是把包裝類自動取值賦給基本類型變量。如:

Integer wrapper=10;//自動裝箱
int a=wrapper;//自動拆箱

在運算時,也可以進行自動裝箱與拆箱,如:

Integer a=10;
System.out.println(i+10);
System.out.println(i++);

4.2.3 自動裝箱、拆箱的內幕
所謂自動裝箱與拆箱其實只是編譯器幫我們做了本來是我們需要自己做的事情。編譯器在編譯時會根據我們所寫的代碼來決定是否進行裝箱或拆箱。例如下面的代碼:

Integer number=100;

在Oracle的JDKh ,編譯程序會自動將代碼展開爲:

Integer localInteger=Integer.valueOf(100);

但自動裝拆箱其實是有問題的,例如:

Integer a=null;
int j=i;

這段代碼在編譯的時候是不會報錯的,因爲null是一個特殊對象,它可以指定給任何聲明的對象,表示沒有引用到任何的內存地址,但是一旦執行編譯出的代碼,就會報錯,因爲編譯後的代碼是這樣的:

Object localObject=null;
int i=localObject.intValue();

這裏由於localObject對象被賦值爲空,而代表沒有任何的內存位置,所以也就不可能執行第二句調用intValue()方法,此時計算機就會報錯NullPointerException(空指針異常,有的教科書或老師又把它叫做空點異常)。表示你想調用一段根本不在內存裏存在的代碼。
除了有這方面的bug之外,還有一些稀奇古怪的現象。比如下面的代碼:

/**
 *  Boxing.java
 * @author mouyo
 */
public class Boxing {
    public static void main(String[] args) {
        Integer i1=100;
        Integer i2=100;
        if(i1==i2){
            System.out.println("相等");
        }else{
            System.out.println("不相等");
        }
    }
}

這個代碼沒問題,結果是相等。
這裏寫圖片描述
圖4.7 結果相等
可是,我們只要改一下,象下面這樣:

package cn.com.speakermore.ch04;

/**
 *  Boxing.java
 * @author mouyo
 */
public class Boxing {
    public static void main(String[] args) {
        Integer i1=200;
        Integer i2=200;
        if(i1==i2){
            System.out.println("相等");
        }else{
            System.out.println("不相等");
        }
    }
}

能看出來改了哪裏麼?對,只是把100換成了200,運行之後的結果卻變成了不相等。
這裏寫圖片描述
圖4.8 200之後的結果是不相等。
爲何是這樣的結果,這是因爲在Integer的自動裝箱過程中,它使用了valueOf()方法,而valueOf()方法會建立一個緩存,緩存那些小於128的整數。這樣,當我們的值小於128時,值相同,對象就是相同的,但是一旦大於等於128,由於沒有緩存,雖然值是相同的,但對象就會不一樣。用==比較時,自然就得到了不相等的結果。
所以,結論還是前面已經討論過的,別使用==或!=來比較兩個對象的值是否相同(因爲引用型數據類型都是比較的地址),而要使用equals()方法。
4.3 數組對象
數組在Java中就是對象,所以前面介紹過的對象基本性質,在操作數組時也都要注意,如引用名的聲明、=指定的作用、==與!=的比較等。
4.3.1 數組基礎
數組基本上是用來收集數據,具有下標(index)的數據結構,在Java中要聲明數組並初始值,可以如下:
int[] score={88,87,99,67,77,81,86,55,46,78};
這段代碼創建一個數組,因爲使用int[]聲明,所以會在內存中分配長度爲10的int連續空間,每個空間依次存儲了大括號中的整數,每個整數都用一個下標來標識,下標從0開始,所以10個長度的數組,下標最大就只到9。如果你使用了10或以上的數字作下標,就會拋出ArrayIndexOutOfBoundException(數組下標越界異常)。
數組使用下標來獲得每一個數據,重點就是可以很方便的和循環結合,用很少的代碼對大量數據進行批量處理:

/**
 * Score.java
 * @author mouyo
 */
public class Score {
    public static void main(String[] args) {
        int[] score={88,87,99,67,77,81,86,55,46,78};
        for (int i = 0; i < score.length; i++) {
            System.out.println("學生分數:"+score[i]);
        }
    }
}

在聲明的數組名稱旁加上[]並指定下標,就可以取得對應值,上例從i爲0-9,逐一取出值並顯示出來。
這裏寫圖片描述
圖4.9 10個學生分數的顯示
在Java中數組是對象,而不是單純的數據集合,數組的length屬性可以取得數組長度。也就是數組的元素個數。
其實上面這個程序可以使用更簡單的方式來編寫,因爲是順序取到數組中每一個值,所以我們可以使用增強式for循環,這是從JDK5開始出現的更方便的for循環,我基本是強烈推薦,在需要遍歷一個數組的時候都使用增強式for循環。

for (int score:scores ) {
   System.out.println("學生分數:"+score);
}

這個程序片段會取得scores數組第一個元素,指定給score變量後執行循環體,接着取得scores中第二個元素,指定給score變量後執行循環體。依此類推,直到scores數組中所有元素都訪問完爲止。將這段for循環片段取代Score類中的for循環,執行結果相同。實際上,增強式for循環也是Java提供的方便功能。
如果要給數組的某個元素賦值,也是要通過下標。例如:

scores[3]=86;
System.out.println(scores[3]);

上面這個程序片段將數組中第4個元素(因爲下標從0開始,下標3就是第4個元素)指定86,所以會顯示86,所以會顯示86的結果。
一維數組使用一個下標存取數組元素,你也可以聲明二維數組,二維數組使用兩個下標存取數組元素。例如,聲明數組來儲存XY座標位置要放的值。

/**
 * XY.java
 * @author mouyo
 */
public class XY {
    public static void main(String[] args) {
        int[][] cords={
            {1,2,3},
            {4,5,6}
        };//聲明二維數組並賦值初始值
        for(int x=0;x<cords.length;x++){//獲得有幾行
            for(int y=0;y<cords[x].length;y++){//獲得每行有幾個元素
                System.out.print(cords[x][y]+"  ");
            }
            System.out.println("");
        }
    }
}

要聲明二維數組,就是在類型關鍵詞旁加上[][]。初學者暫時將二維數組看作是一個表格會比較容易理解,由於有兩個維度,所以首先得通過cords.length得到有幾行,然後再使用cords[x].length獲得每行有幾個元素,之後再一個一個進行輸出。
二維數組有兩個下標,所以也要使用二重嵌套循環來完成取值遍歷的代碼,同樣由於這裏對下標並沒有特別需要,所以也可以使用增強for循環來完成

       for(int[] row:cords){//獲得有幾行
            for(int value:row){//獲得每行幾個元素
                System.out.print(value+"  ");
            }
            System.out.println("");
        }

最後執行結果相同。
這裏寫圖片描述
圖4.10 二維數組執行結果
提示:如果是三維數組,就是在類型關鍵字後使用三個[],如果是四維就是四個[]。依此類推。不過基本上是用不到如此複雜的維度,現代編程通常一維就足夠了,二維都基本上不用的。
4.3.2 操作數組對象
前面都是知道元素值來建立數組的例子,如果事先不知道元素值,只知道元素個數,那可以使用new關鍵字指定長度的方式創建數組。例如:

int[] scores=new int[5];

在Java中只要看到new,就一定是創建對象(默然說話:計算機的實際操作是劃分一塊內存空間供對象使用),這個語法其實已經說明數組就是一個對象。使用new創建數組之後,數組中每個元素都會被初始化,如表4.1所示:
表4.1 數組元素初始值

數據類型 初始值
byte 0
short 0
int 0
long 0L
float 0.0F
double 0.0
char \u0000(即’’)
boolean false
null

如果默認值不符合你的需求,可以使用java.util.Arrays的fill()方法來設定新建數組的元素值。例如,每個學生的成績默認60分:

import java.util.Arrays;

/**
 * Score2.java
 * @author mouyo
 */
public class Score2 {
    public static void main(String[] args) {
        int[] scores=new int[10];
        System.out.println("默認初始值");
        for(int score:scores){
            System.out.print(score+"  ");
        }
        System.out.println("\n初始填充後");
        Arrays.fill(scores, 60);
         for(int score:scores){
            System.out.print(score+"  ");
        }
    }
}

執行結果如下:
這裏寫圖片描述
圖4.11 學生分數數組的初始化值
數組既然是對象,再加上我們講過,對象是根據類而建立的實例,那麼代表數組的類定義在哪裏呢?答案是由JVM動態產生。你可以將int[]看作是類名稱,這樣,根據int[]聲明的變量就是一個引用變量,那麼下面這段代碼會是什麼結果?

      int[] scores1={88,66,48,99,12,45,55,76};
         int[] scores2=scores1;

         scores2[0]=99;
         System.out.println(scores1[0]);

因爲數組是對象,而scores1與scores2是引用名稱,所以將scores1賦值給scores2的意思就是將scores1引用的對象內存地址賦值給scores2,所以兩個變量指向了同一塊內存,那麼,當你通過scores2[0]對這個數組的第1個元素進行修改之後,scores1也能看到變化(

默然說話:記住,因爲它們的指向是同一個地址,再想想前面給大家的引用變量聲明的動畫

)。
所以最後的輸出scores1[0]的值也是99。
再來看前面提過的二維數組:
int[][] cords=new int[2][3];
這個語法其實是建立了一個int[][]類型的對象,裏面有2個int[]類型的對象。這2個一維的數組對象的長度都是3。初始值爲0。
二維數組的對象引用示意
圖4.12 二維數組的對象引用示意。
對,從上圖中我們可以得到一個結論,其實Java的二維數組根本不是一個表格,所以Java完全可以建立一個不規則的數組。例如:

/**
 * IrregularArray.java
 * @author mouyo
 */
public class IrregularArray {
    public static void main(String[] args) {
        int[][] arr=new int[2][];//聲明arr對象爲二維數組
        arr[0]=new int[]{1,2,3,4,5};//arr[0]是一個長度爲5的一維數組
        arr[1]=new int[]{1,2,3};//arr[1]是一個長度爲3的一維數組
        //從輸出可以看出來,第一個輸出了5個數,第二個輸出了3個數,並非一個表格
        for(int[] row:arr){
            for(int value:row){
                System.out.print(value+"  ");
            }
            System.out.println();
        }

    }
}

這個例子我們可以看到,在new一個二維數組時可以只寫最高維,而不用寫最後一個維度的數量。然後再給每個數組下標實例化不同的低維度數組。而具體的數值就放在最低維度指向的空間裏。(默然說話:我驚訝的發現,當寫了二維聲明之後,不能再使用直接初始化的語法了,也就是說,不能寫成arr[0]={1,2,3,4,5},必須寫成arr[0]=new int[]{1,2,3,4,5},可能是因爲一開始聲明時沒有給出低維度的長度,所以必須重新對低維度劃分空間罷)。
當然,其實這個初始化的代碼也可以寫爲:

      int[][] arr={
            {1,2,3,4,5},
            {1,2,3}
        };

以上都是基本類型創建的數組,那麼引用類型創建的數組會是什麼樣的情況呢?首先看看如何用new關鍵字建立Integer數組:

Integer[] scores=new Integer[3];

看上去不過就是把int換成了類Integer而已。那想想這個數組創建了幾個Integer對象呢?錯誤!不是3個,而是一個Integer對象都沒有創建。回頭看錶4.1,對象類型的默認值是null。這裏只創建出一個Integer[]對象,它可以容納3個Integer對象,但是因爲對象型的默認初始化值爲null,所以現在這三個對象都還沒有產生。我們必須在給它們的每個下標都賦值時,纔會有Integer對象產生。

/**
 * IntegerArray.java
 * @author mouyo
 */
public class IntegerArray {
    public static void main(String[] args) {
        Integer[] scores=new Integer[3];
        System.out.println("僅實例化了數組之後,數組中的初始化值:");
        for(Integer score:scores){
            System.out.println(score);
        }
        scores[0]=new Integer(98);
        scores[1]=new Integer(99);
        scores[2]=new Integer(87);

        System.out.println("實例化每個元素之後,數組中的元素值:");
         for(Integer score:scores){
            System.out.println(score);
        }

    }
}

代碼的執行結果如下:
對象類型的數組初始化
4.13 對象類型的數組初始化
所以每次講對象類型數組,我總是說,對象類型數組需要new兩次,一次是new出對象數組,另一次是new出一系列放在數組中的對象。
上面的代碼之所以這樣寫,主要是爲了突出對象的特點,其實JDK5.0之後有了自動裝箱和拆箱,所以數組元素賦值可以寫成這樣:

       scores[0]=98;
        scores[1]=99;
        scores[2]=87;

對象類型其實也可以使用大括號進行初始化,只是寫得要長些:

Integer[] scores={new Integer(98),new Integer(99),new Integer(87)};

同樣,上面的寫法也是爲了強調對象型需要先new,JDK5.0之後有了自動裝箱和拆箱,所以也可以寫成這樣:

Integer[] scores={99,98,87};

4.3.3 數組複製
在知道了數組其實是一種對象,你就要知道,下面的代碼不是複製:

int[] scores1={88,66,48,99,12,45,55,76};
int[] scores2=scores1;

複製應該是出現兩個內容相同的對象,但這只是讓兩個變量指向了同一個對象,也就是對象只有一個,並沒有兩個。所以真正的複製應該是這樣的。

int[] scores1={88,66,48,99,12,45,55,76};
int[] scores2=new int[scores1.length];
for(int i=0;i<scores1.length;i++){
    scores2[i]=scores1[i];
}

這是自己使用循環來完成每一個值的複製,其實可以使用System.arraycopy()方法來直接完成這個過程,比我們用循環來得快:

int[] scores1={88,66,48,99,12,45,55,76};
int[] scores2=new int[scores1.length];
System.arraycopy(scores1,0,scores2,0,scores1.length);

System.arraycopy()有五個參數,分別是源數組,源數組開始下標,目標數組,目標數組開始下標,複製長度。如果使用JDK6以上,還有個更方便的Arrays.copyOf()方法,你不用另行建立數組,Arrays.copyOf()會幫你創建:

import java.util.Arrays;

/**
 * CopyArray.java
 * @author mouyo
 */
public class CopyArray {
    public static void main(String[] args) {
        int[] scores1={88,81,75,68,79,95,93};
        int[] scores2=Arrays.copyOf(scores1, scores1.length);
        System.out.println("scores2數組的元素:");
        for(int score:scores2){
            System.out.print(score+"  ");
        }
        System.out.println("");
        scores2[0]=99;
        System.out.println("修改了scores2第1個元素之後,scores1的元素仍然不變");
        for(int score:scores1){
            System.out.print(score+"  ");
        }
    }
}

執行結果如下所示:
Arrays.copyOf()進行數組複製無需自行創建數組
圖10.14 Arrays.copyOf()進行數組複製無需自行創建數組
Java中,數組一旦創建,長度就是固定的,如果長度不夠,就只能創建新的數組,將原來的內容複製到新數組中。如:

int[] scores1={88,81,75,68,79,95,93};
int[] scores2=Arrays.copyOf(scores1, scores1.length*2);
System.out.println("scores2數組的元素:");
for(int score:scores2){
    System.out.print(score+"  ");
}

Arrays.copyOf()的第二個參數就是指定新數組的長度。上面程序讓新創建的數組長度是原來數組長度的2倍,輸出時會看到增加部分元素的值均爲默認值0。
無論是System.arraycopy()還是Arrays.copyOf(),用在類類型聲明的數值時,都是執行淺層複製(默然說話:也就是說,如果你的數組元素是對象,那麼複製一個新的數組時,新數組還在引用原數組的對象,並不會把原數組的對象進行復制!)。如果真的要連同對象一同複製,你得自行操作,因爲基本上只有自己才知道,每個對象複製時,有哪些屬性必須複製。
4.4 字符串對象
在Java中,字符串本質是打包字符數組的對象。它同樣也有屬性和方法。不過在Java中基於效率的考慮,給予字符串某些特別且必須注意的性質。
4.4.1 字符串基礎
由字符組成的文字符號稱爲字符串。例如,“Hello”字符串就是由’H’、’e’、’l’、’l’、’o’、五個字符組成,在某些程序語言中,字符串是以字符數組的方式存在,然而在Java中,字符串是java.lang.String實例,用來打包字符數組。所有用””包括的一串字符都是字符串。

       String name="sophie";//創建String對象
        System.out.println(name);//輸出sophie
        System.out.println(name.length());//顯示長度爲6
        System.out.println(name.charAt(0));//顯示第1個字符s
        System.out.println(name.toUpperCase());//顯示全大寫SOPHIE

因爲字符串在Java中是對象,所以它有很多的方法,上面的示例給大家看到了幾個很常用的字符串方法,length()可以獲得字符串長度;charAt()可以獲得指定下標的字符,toUpperCase()可以把所有小寫字母轉成大寫字母。
如果已有一個char[]數組,也可以使用new來創建String對象。如:

       char[] ch={'H','e','l','l','o'};
        String str=new String(ch);

也可以使用toCharArray()方法,將字符串以char[]的形式返回。

char[] ch2=str.toCharArray()

Java中可以使用+運算來連接字符串。前面我們一直在用。
爲了認識數組與字符串,可以看看程序入口main()方法的String[] args參數,在啓動JVM並指定執行類時,可以一併指定命令行參數。

/**
 * Avarage.java
 * @author mouyo
 */
public class Avarage {
    public static void main(String[] args) {
        long sum=0;
        for(String arg:args){
            sum+=Long.parseLong(arg);//把字符串轉化爲長整型
        }
        System.out.println("平均:"+(float)sum/args.length);
    }
}

在NetBeans中如果要提供命令行參數,可以這樣進行操作。
(1)在項目上右擊,在彈出的快捷菜單中選擇“屬性”,打開“項目屬性”對話框,在左邊的“類別”列表中選擇“運行”。
配置命令行參數
圖4.15 配置命令行參數
(2)單擊右側上方“配置”列表框旁邊的“新建…”按鈕,打開“創建新的配置”對話框,在“類別”文本框中填入配置的名稱,我填的是“命令行參數”。
創建新的配置
圖4.16 創建新的配置
(3)單擊“主類”文本框右側的“瀏覽…”按鈕,從中選擇你寫的示例。
(4)在“參數”文本框中手工輸入參數1 2 3 4,注意,每個數字之間要用空格分開,不然JVM會認爲你只輸入了一個參數。
這樣設定之後,點擊工具欄上的“運行”按鈕,就會按你所設置的配置調用程序了。
4.4.2 字符串特性
各程序語言會有細微、重要且不容忽視的特性。在Java的字符串來說,就有一些必須注意的特性:
 字符串常量和字符串池。
 “不可變”(Immutable)的字符串。
1.字符串常量與字符串池
來看下面的片段:

String name1=new String("Hello");
String name2=new String("Hello");
System.out.println(name1==name2);

希望現在的你會很自信的回答:結果是false。是的,結果的確是false。因爲name1和name2引用了不同的對象,儘管它們的內容一樣,但內存地址不同,所以==的比較結果就是false。
片段執行結果
圖4.17 片段執行結果

再看下面的片段:

String name1="Hello";
String name2="Hello";
System.out.println(name1==name2);

這個代碼和上面似乎沒啥區別嘛,只不過沒有顯式new而已。但是如果你運行一下,你就會驚訝的發現:結果是true!!!
片段執行結果2
圖4.18 片段執行結果
難道這裏使用了不同的規則?答案是:沒有,和前面的顯式new的規則是一致的!所以正確的推論是:name1和name2都指向了同一個對象!
在Java中爲了效率的考慮,以””包括的字符串,只要內容相同(序列、大小寫相同),無論在程序中出現幾次,JVM都只會建立一個String對象,並在字符串池中維護。在上面這個程序片段的第一行,JVM會建立一個String實例放在字符串池中,並把地址賦值給name1,而第二行則是讓name2直接引用了字符串池中的同一個String對象。
用””寫下的字符串稱爲字符串常量(String Literal),既然你用”Hello”寫死了字符串內容,基於節省內存的考慮,自然就不用爲這些字符串常量分別建立String實例。
前面一直強調,如果想比較對象的內容是否相同,不要使用==,要使用equals()。這個同樣適用String。
2.不可變動字符串
在Java中,字符串對象一旦建立,就無法更改對象中任何內容,對象沒有提供任何方法可以更改字符串內容。那麼+連字符串是怎麼做到的呢?

String name1=”Java”;
String name2=name1+”World”;
System.out.println(name2);

上面這個程序片段會顯示JavaWorld,由於無法更改字符串對象內容,所以絕不是在name1引用的字符串對象之後附加World。而創建了java.lang.StringBuilder對象,使用其append()方法來進行+左右兩邊字符串附加,最後再轉換爲toString()返回。
簡單地說,使用+連接字符串會產生新的String對象,這並不是告訴你,不要使用+連接字符串,畢竟這種做法非常方便。但是不要將+用在重複性的連接場合,比如循環或遞歸時。這會因爲頻繁產生新對象,造成效能上的負擔。
比如,如果我們想輸出一個從1加到100的算式(注意,不是1到100相加的結果,而是顯示描述1加到100的這個算式:1+2+3+4+5……+100),你會怎麼寫?
也許是這樣寫?

for(int i=1;i<100;i++){
    System.out.print(i+"+");
}
System.out.println(100);

這樣寫代碼很簡潔,但是一個一個循環輸出效能是很低的。有沒有更好的辦法?

String str="";
for(int i=1;i<100;i++){
    str+=i+"+";
}
System.out.println(str+100);

只有一個輸出語句,效能大大提高!但是使用了連接字符串,會造成頻繁產生新對象的問題。象上面這樣的情況下,強烈建議使用StringBuilder!

/**
 * OneTo100.java
 * @author mouyo
 */
public class OneTo100 {
    public static void main(String[] args) {
        StringBuilder builder=new StringBuilder();
        for(int i=1;i<100;i++){
            builder.append(i).append("+");
        }
        System.out.println(builder.append(100).toString());
    }
}

StringBuilder每次append()調用之後,都會返回原StringBuilder對象,所以我們可以連接調用append()方法。這個程序只產生一個StringBuilder對象,只進行一次輸出,無論是從計算效能還是內存使用效能上來說,都是非常棒的!

提示:java.lang.StringBuilder是JDK5之後新增的類,在該版本之前,我們有java.lang.StringBuffer,StringBuilder和StringBuffer其實是一樣的。但是StringBuilder的效率更高,因爲他不考慮多線程情況下的同步問題。所以如果你的程序會涉及到多線程的情況,可以直接改用StringBuffer

4.4.3 字符串編碼
你我都是中國人(默然說話:對的!下面要談論的問題與中文有關,如果你就不使用中國字,那可以略過),在Java中你必然要處理中文,所以你要了解Java如何處理中文。
要知道,java源代碼文檔在NetBeans中是UTF-8編碼。在windows簡體中文版下默認是GB2312。在Eclipse中情況又會不同,Eclipse會自動讓java源代碼文件的編碼是所在操作系統的默認編碼。所以,如果你的Eclipse在Windows簡體中文下就是GB2312,如果是在Mac下,就會是UTF-8。
如果你使用的是GB2312編碼,在編譯器編譯你的代碼時,會自動把所有的中文都轉爲Unicode編碼。比如”哈囉”就會變成”\u54C8\u56C9”。這個”\uxxxx”就是Unicode的編碼形式。
那麼編譯器是如何知道要將中國字轉成哪種Unicode編碼呢?當你使用javac指令沒有指定-encodding參數時,會使用操作系統默認編碼,如果你的源代碼文件採用了不同的編碼形式,則必須指定-encoding參數。
提示:在Windows下不要使用純文本編輯器轉存UTF-8文件來編寫源代碼,因爲記事本會在文檔頭加BOM,這樣會造成Java編譯器無法編譯。建議使用NotePad++。
IDE也是允許你自定義編碼的,如果是NetBeans,可以在項目上右擊,在彈出的快捷菜單中選擇“屬性”,打開“項目屬性”對話框,在左邊“類別”列表中選擇“源”。然後在右側最下方的“編碼”選擇框中選擇代碼的編碼。
4.5 查詢Java API文件
書上提到的各種Java類是如何知道應該調用什麼方法呢?是通過查詢Java API文檔知道的。
http://docs.oracle.com/javase/8/docs/api/
這個鏈接就是Java API文檔,裏面包括了所有Java類的方法屬性及使用介紹、特點介紹。
不過在平時如果知道一個Java類,想知道怎麼用,我更習慣用下面這個鏈接。
http://www.baidu.com/
以前喜歡用谷歌,現在用不了,只好將就着用了。
4.6 重點複習
要產生對象必須先定義類,類是對象的模板,對象是類的實例。定義類使用class關鍵字,實例化對象使用new關鍵字。
想在建立對象時,一併進行某個初始流程,可以定義構造函數,構造函數是與類名同名的方法,它不能寫返回值。參數根據需要指定,也可以沒有參數。
Java遵守IEEE 754浮點運算規範,使用分數與指數來表示浮點數。如果要求精確度,那就要小心使用浮點數。不要使用==直接比較浮點數運算結果。
要讓基本類型象對象一樣的操作,可以使用Long、Integer、Double、Float等類來打包基本類型。JDK5之後提供了自動裝箱和拆箱,媽媽再也不擔心我不會裝箱和拆箱了。
數組在Java中就是對象,下標從0開始,下標超出範圍會拋異常。
無論是System.arraycopy()還是Arrays.copyOf(),用在類的數組時,都是執行淺層複製。
字符串也是對象。
字符串對象一旦建立,無法更改對象內容。不要將+用在重複性的連接場合。
使用javac指令沒有指定-encoding參數時,會使用操作系統默認編碼。
4.7 課後練習
4.7.1 選擇題
1. 如果有以下的程序代碼:
int x=100;
int y=100;
Integer wx=x;
Integer wy=y;
System.out.println(x==y);
System.out.println(wx==wy);
在JDK 5以上的環境編譯與執行,則顯示結果是()
A.true、true B.true、false
C.false、true D.編譯失敗

  1. 如果有以下的程序代碼:
    int x=200;
    int y=200;
    Integer wx=x;
    Integer wy=y;
    System.out.println(x==y);
    System.out.println(wx==wy);
    在JDK 5以上的環境編譯與執行,則顯示結果是()
    A.true、true B.true、false
    C.false、true D.編譯失敗

  2. 如果有以下的程序代碼:
    int x=300;
    int y=300;
    Integer wx=x;
    Integer wy=y;
    System.out.println(wx.equals(x));
    System.out.println(wy.equals(y));
    在JDK 5以上的環境編譯與執行,則顯示結果是()
    A.true、true B.true、false
    C.false、true D.編譯失敗

  3. 如果有以下的程序代碼:
    int[] arr1={1,2,3};
    int[] arr2=arr1;
    arr2[1]=20;
    System.out.println(arr1[1]);
    在JDK 5以上的環境編譯與執行,則顯示結果是()
    A.執行時顯示2 B.執行時顯示20
    C.執行時出現ArrayIndexOfBuondException D.編譯失敗

  4. 如果有以下的程序代碼:
    int[] arr1={1,2,3};
    int[] arr2=new int[arr1.length]
    arr2=arr1;
    for(int value:arr2){
    System.out.print(value);
    }
    在JDK 5以上的環境編譯與執行,則顯示結果是()
    A.執行時顯示123 B.執行時顯示12300
    C.執行時出現ArrayIndexOfBuondException D.編譯失敗

  5. 如果有以下的程序代碼:
    String[] strs=new String[5]
    以下描述正確的是()
    A.產生5個String對象 B.產生1個String對象
    C.產生0個String對象 D.編譯失敗

  6. 如果有以下的程序代碼:
    String[] strs={“java”, “java”, “java”, “java”, “java”}
    以下描述正確的是()
    A.產生5個String對象 B.產生1個String對象
    C.產生0個String對象 D.編譯失敗

  7. 如果有以下的程序代碼:
    String[][] strs=new String[2][5]
    以下描述正確的是()
    A.產生10個String對象 B.產生2個String對象
    C.產生0個String對象 D.編譯失敗

  8. 如果有以下的程序代碼:
    String[][] strs={
    {“java”, “java”, “java”},
    {“java”, “java”, “java”, “java”}
    };
    System.out.println(strs.length);
    System.out.println(strs[0].length);
    System.out.println(strs[1].length);
    以下描述正確的是()
    A.顯示2、3、4 B.顯示2、0、1
    C.顯示1、2、3 D.編譯失敗

  9. 如果有以下的程序代碼:
    String[][] strs={
    {“java”, “java”, “java”},
    {“java”, “java”, “java”, “java”}
    };
    for( row:strs){
    for( value:row){

    }
    }
    空白處應該分別填上()
    A.String、String B.String、String[]
    C.String[]、String D.String[]、String[]

4.7.2 操作題
1.Fibonacci爲13 歐洲數學家,在他的著作中提過,若一隻兔子每月生一隻小兔子,一個月後小兔子也開始生產。起初只有一隻小兔子,一個月後有兩隻兔子,兩個月後有三隻兔子,三個月後有五隻 …,也就是每個月兔子總數會是1、1、2、3、5、8、13、21、34、89,這就是費氏數列,可用公式定義如下:
fn=fn-1+fn-2 if n>1
fn=n if n=0,1
編寫程序,可以讓用戶輸入想計算的費氏數列的個數,由程序全部顯示出來。
2.請編寫一個簡單的洗 程序,可以在文本模式下顯示洗牌結果。
3.下面是一個數組,請使用程序使其中元素排序爲由小到大:
int[] number={70,80,31,37,10,1,48,60,33,80};
4.下面是一個排序後的數組,請編寫程序可讓用戶在數組中尋找指定數字,找到就顯示索引值,找不到就顯示-1:
int number={1,10,31,33,37,48,60,70,80};

發佈了293 篇原創文章 · 獲贊 56 · 訪問量 72萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章