iT邦幫忙

0

【左京淳的JAVA學習筆記】第五章 class定義與物件生成

如果把程式當成是魔法,前面幾章都是基本的咒文。
到這章開始需要用到想像力了。

class(類)

class像是沒有生命的模型,裡面記載了一些關於物件的敘述與特徵。
透過new()方法,可以從模型中複製一或多個一模一樣的物件出來,並且具有生命力,可以執行各種動作。想像一下你的玩具車或鋼彈模型被附加了想像力,真正動起來的樣子吧!

來看看以下範例:

class Employee {
  int id;
  void setId(int i){
    id = i;
  }
  int getId(){
    return id;
  }
}
class Sample5_1 {
  public static void main(String[] args) {
    //實例化社員A
    Employee a = new Employee();
    //設定A的社員編號
    a.setId(100);
    //實例化社員B
    Employee b = new Employee();
    //設定B的社員編號
    b.setId(200);
    
    //請電腦告訴我們社員A和社員B的編號
    System.out.println("社員A的編號:" + a.getId());
    System.out.println("社員B的編號:" + b.getId());
  }
}

執行結果

社員A的編號:100
社員B的編號:200

JAVA的物件像是黑盒子,如果要把資料存進去或拿出來,得要透過特定的方法。
通常會命名為set和get方法,利用這兩個方法來從物件裡存取資料。如範例中的setId()和getId()。
set資料的時候要準備引數,用來放進物件裡。

void setId(int i) //指定引數為單個int型數據,void表示不返回數值。

get資料的時候不用引數,但要指定拿出資料時,資料的型態。

int getId() //沒有引數,返回int數值。

在範例5_1裡使用new()實例化了社員A和社員B。這兩個物件複製了class Employee的所有資料及方法。在實例化之後,A和B各自保有自己的資料。

變數的使用範圍(scope)

在方法內定義的變數又稱為區域變數,只適用於該方法內(也就是{}的裡面)。看看以下範例:

class Test {
  int x;
  public static void main(String[] args) {
    boolean y = true;
    if (y){
      String z = "JAVA";
    }
    System.out.println(z);
  }
}

變數x的使用範圍在class Test內
變數y的使用範圍在main method內
變數z的使用範圍在if內,所以System.out.println(z);這行因無法找到變數z,而導致編譯錯誤。

建構式(constructor)

class除了變數及方法外,還可以定義建構式。
建構式是物件實例化的時候,會執行一次的式子(相當於將物件初期化)。
建構式長得跟一般方法有點像,透過以下特徵來區隔兩者:

  1. 建構式的名字與class一樣
  2. 沒有返回值
  3. 可以建立複數個名稱一樣,但接收不同引數的建構式。依照引數的格式,會執行相應的建構式。

如果沒有寫建構式的話,JAVA預設會給一個空的建構式,長得像這樣:Class名(){}
這就是為什麼前面的案例沒有給任何引數也能執行的原因。
不過一旦自己寫了建構式,JAVA就不會再幫忙寫空的建構式了。
看看以下範例:

class Employee {
  int id;
  String name;
  //建構式定義
  Employee(int i){
    id = i;
  }
  Employee(int i,String s){
    id = i;
    name = s;
  }
}

class Sample5_2 {
  public static void main(String[] args) {
    //實例化員工
    Employee a = new Employee();  //會因為找不到相應的建構式而報錯
    Employee b = new Employee(100);
    Employee c = new Employee(100,"Tom");
  }
}

從此範例可以發現,當要實例化一個員工時
若沒有提供任何引數,則會因為找不到相應的建構式而報錯
至少需提供int數值(id編號),此時Employee(int i)建構式會被呼叫。
若提供了int數值和String資料,則Employee(int i,String s)建構式會被呼叫。
這個特性稱為overload,確保了資料不足的時候,程式仍具有通用性。

overload的特性不只適用於建構式,也適用於一般方法。如下例:

class Test {
  void myPirnt(){
    //處理1
  }
  void myPirnt(int i){
    //處理2
  }
}

static修飾子

static是靜態的意思,static修飾子可以放在變數或方法的前面,表示其屬於class的變數或方法,而非屬於實例化物件(instance)。
JVM在執行時,會先載入靜態資料和class,然後執行main方法,然後進入動態的世界(實例化物件、建立非靜態的變數等..)

注意:
static變數或方法於class建構時被建立。
非static變數或方法於實例化物件時被建立。
這表示非static變數或方法(後被建立者)可以訪問static變數或方法(先被建立者)
相對的,在static方法中要訪問非static變數或方法時,需要先實例化一個物件之後才能使用。
請看下例:

class Sample5_3 {
  int instanceVal;
  static int staticVal;
  
  int methodA(){return instanceVal;}             //OK
  int methodB(){return staticVal;}               //OK
  static int methodC(){return instanceVal;}      //NG static方法不能直接訪問instance變數
  static int methodD(){return staticVal;}        //OK
  static int methodE(){                          //OK 實例化一個物件後,static方法可以訪問此物件的instance變數
    Sample5_3 obj = new Sample5_3();
    return obj.instanceVal;
  }
}

就如同各個物件擁有自己的數據,class也可以保存自己的數據(即static變數)。
由同一個class實例化出來的各個物件,可以共用和修改這些static變數。
※要使用class變數的時候,別忘記加上static喔!只有靜態變數和方法,會在class建立時一同被建立。非靜態的則是物件實例化時會被建立。

Static Initializer (類的建構式)

剛剛提到物件被實例化時,會執行建構式。而Static Initializer相當於類的建構式。
當類別初次被使用時,Static Initializer會被執行。
看以下範例:

class Foo {
  static {
    System.out.println("Foo class: Static Initializer");
  }
  Foo() {
    System.out.println("Foo class: Constructor");
  }
}

class Sample5_4 {
  static {
    System.out.println("Sample5_4 class: Static Initializer");
  }
  public static void main(String[] args) {
    System.out.println("Sample5_4 class: Main Method");
    Foo f = new Foo();
  }
}

執行結果

Sample5_4 class: Static Initializer
Sample5_4 class: Main Method
Foo class: Static Initializer
Foo class: Constructor

以上的結果翻譯成中文就是:

  1. JVM(JAVA虛擬機)先載入了Sample5_4類,執行了他的Static Initializer。
  2. 然後執行main方法。
  3. 由於在main方法裡new了一個 Foo物件,所以Foo類先被載入。
  4. 然後Foo物件被初始化,Constructor被執行。

Access Modifier(存取修飾子)

還記得main方法前面的public嗎?這就是存取修飾子,可以放在類、變數及方法前面,決定這些東西的公開範圍(存取權限)。
例如JAVA的實例化物件裡面,通常會把變數設為private,而get,set等方法設定為public。以確保物件裡面的值不會直接被外部看見或修改,而是只能透過固定的方法來訪問。

class Employee {
  private int id;

  public Employee(int i) {id = i;}
  public int getId() {return id;}

}

class Sample5_5 {
  public static void main(String[] args) {
    Employee emp = new Employee(100);
    System.out.println(emp.id);
    System.out.println(emp.getId());
  }
}

嘗試編譯以上的檔案會發現,System.out.println(emp.id);這行會因權限不足而報錯。必須使用getId()方法才能訪問id的值。

除了public和private之外,還有protected和預設值(未指定)兩種。
要解釋修飾子,首先得先認識JAVA的package(包)的概念。包就像是資料夾,裡面存放不同的class文件。
文法如下:

package 包名;
class X {…}

如果沒指定包名的類,則會被JAVA歸類到沒有名字的包內。

關於包的詳細介紹待會兒會再說明,先回到修飾子的說明。
public表示完全開放,任何別的包裡的類都可使用此類、變數或方法。
protected只開放給同一個包使用,或是不同包但繼承了此類的子類也可使用。
不指定,即採用預設值,只開放給同一個包使用。
private,僅該類自己可以使用。

值及物件的複製

在JAVA裡面,如果把一個基本數值的資料,當作引數丟給一個方法進行處理。則丟進去的會是一個複製的新值,而非原本的數值。例如:

int num = 10;
obj.method1(num);
System.out.println(num);  //方法外的num保持為10

void method1(int num) {
  num += 10;
  System.out.println(num);  //方法內的num為20
}

寫成以下的樣子可能比較好理解

void method1(int n) {
  n += 10;
  System.out.println(n); //n為20
}

也就是方法內的num其實跟外部的num沒有任何關係。只是複製了其值而已。而且num作為引數被傳入方法之後,其變數名也可以任意取名。

不過如果引數是物件的話,則物件本身會被傳入方法內。

int[] array = {10};
obj.method2(array);
System.out.println(array[0]);  //方法外的值也變為20

void method2(int[] array) {
  array[0] += 10;
  System.out.println(array[0]);  //方法內的值為20
}

package(包)的建構

請看以下範例:

package chap05.com.se;

class Foo {
  void print() {
    System.out.println("package sample");
  }
}

class Sample5_6 {
  public static void main(String[] args) {
    Foo f = new Foo();
    f.print();
  }
}

由於在java文件中指定了包的路徑,因此編譯和執行時也都要符合設定的路徑。
首先在當前路徑下,建立以下資料夾chap05/com/se,然後把Sample5_6.java檔放進去。
進行編譯

javac chap05/com/se/Sample5_6.java

執行

java chap05.com.se.Sample5_6

由於手動新增資料夾很麻煩,所以也可使用-d指令自動建立路徑(d後面的"."表示指定當前路徑為起點)

javac -d . Sample5_6.java

package的引用(import)

如果需要用到的類,在不同包裡面時,就使用import指令來引用。
建立以下的範例檔案:
Foo.java

package chap05.com.se.ren;
public class Foo {  //要給不同包的程式使用,必須加上public修飾子
  public void print() {  //要給不同包的程式使用,必須加上public修飾子
    System.out.println("package sample");
  }
}

Sample5_7.java

package chap05.com.se;
import chap05.com.se.ren.Foo;
//import chap05.com.se.ren.*;  //也可以使用"*"來引用ren包下面所有類。

class Sample5_7 {
  public static void main(String[] args) {
    Foo f = new Foo();
    f.print();
  }
}

先編譯Foo.java,再編譯Sample5_7.java(由於Sample5_7裡面import了Foo,所以如果沒先建立Foo的話會報錯)

javac -d . Foo.java
javac -d . Sample5_7.java

※因中文導致編譯失敗請指定以utf-8編碼進行編譯,如下例:

javac -d . -encoding utf-8 Foo.java

編譯完成後執行

java chap05.com.se.Sample5_7

以上是第五章 class定義及物件的學習心得,接下來第六章會介紹繼承與多型。

參考教材: JAVAプログラマSilver SE8 - 山本 道子


尚未有邦友留言

立即登入留言