Java Virtual Machine

C語言的開發模式, 是編寫.c的Source Code, 再經由Compiler編譯成Object Code。所謂Object Code指的是和硬體相關的機器指令, 也就是說當我們想要把C程式移植到不同的硬體時, 必須要重新Compile,以產生新的執行檔。除了需要重新編譯外,新系統是否具備應用程式所需的程式庫,include的檔案是否相容, 也是程式能否在新機器上順利編譯和執行的條件之一。

在實務上,為了讓C程式能在不同的UNIX版本上都能順利編譯,原作者往往必須使用前置處理器的#ifdef指令,判斷不同環境的適當寫法。如果想把在UNIX上開發的C程式移植到Windows上,則有用到專屬程式庫的部分(如UNIX的使用者介面可能用到X Window的API,Windows就沒有支援,必須一台一台灌程式庫才行,很可能還要花錢買),就必須重寫才行。

解決此類問題的方法之一,是定義一種Virtual Machine(虛擬機器),讓程式語言編譯時不要翻成實體機器的指令,而是翻成Virtual Machine的目的碼。Virtual Machine一般是以軟體來模擬的,只要新的平台有Virtual Machine,則原始程式不用Compile,執行舊機器上已有的Virtual Machine目的碼,就可以了。當然要達到完全不用重新Compile就能執行的理想,還要配合標準的程式庫才行。

Java語言基於上述理念,定義了Java Virtual Machine,它所用的指令稱為byte code。使用Virtual Machine的缺點之一,是執行的速度較慢,代價是開發的速度變快了。以現在的硬體來說,大部分應用程式的執行速度已經沒有那麼重要,反倒是軟體的開發速度和品質越來越值得重視。

此外JVM的技術不斷進步, 諸如Just In Time(JIT) Compiler, 或HotSpot等技術都可以讓Java程式以非常接近原生碼(Native Code)的速度執行。因此不要因為某些偏頗的報告或直覺, 就不使用Java了。

開發Java應用程式的工具中,最常見的是由Java的原創公司Sun Micro所出版的JDK(Java Development Kit)。JDK可以免費下載。以Text Editor寫好的Hello.java原始檔:

public class Hello {
    public static int gvar;
    public static void say(String s) {
        int x = 10;
        System.out.print(s+x);
    }
    public static void main(String[] argv) {
        float y = 0;
        say("Hello, world\n");
    }
}
這程式的C版本如下
#include <stdio.h>
int gvar;
void say(char[] s) {
    int x = 10;
    printf("%s%d", s, x);
}
int main(int argc, char** argv) {
    float y = 0;
    say("Hello, world\n");
}

經過:

javac Hello.java

編譯完成後會產生byte code格式的Hello.class,然後

java Hello

就可以利用Java Virtual Machine(此處是java這個執行檔)來執行了。

上述過程中幾個比較會發生的問題是

Java是物件導向(Object-Oriented)程式語言

Java是由C++簡化來的。由於C++要和C完全相容,又很注重效能問題,因此C++算是很複雜的程式語言。Java在設計之初,考量的重點之一就是簡單,因此和C++比起來,不僅更為物件導向,而且比C++容易學習。

Java許多運算符號和敘述語法都是來自C語言,假設各位已經對C語言有所了解,本章後面的部分只將Java和C在運算符號和敘述語法上的差異點出來,相同的部分請參見C語言的課程內容。

資料型別

Java語言所定義的基本資料型別有

型別名稱位元長度範圍
boolean1true或false
byte8-128 ~ 127
short16-32768 ~ 32767
char16Unicode characters
int32-2147483648 ~ 2147483647
long64-9223372036854775808 ~ 9223372036854775807
float32+-3.4028237*10+38 ~ +-1.30239846*10-45
double64+-1.76769313486231570*10+308 ~ 4.94065645841246544*10-324

Java的資料型態裡沒有unsigned。

Java對數值型態的轉換比C稍微嚴格一點,下列左邊的部分都可以指定(assignment)給右邊的型別:

byte --> short --> int --> long --> float --> double

除上述外,其他型別間的轉換都必須下達型別轉換(Type Casting)命令來處理,其形式為圓括弧裡寫上型別名稱,如(double)

由於Java在char的型態部分採用Unicode,因此字元常數的表示法,除因循C的規則外,也可以直接指定16bits Unicode編碼給char型別的變數。例如由Windows "字元對應表" 程式中可查到象棋中的紅車的unicode編碼為4FE5, Java可用 '\u4fe5' 來表達。Java的變數也可以用Unicode來命名,換句話說,你可以用中文取變數名稱。

除了這些基本資料型別外,Java還有一個稱為Reference(參考)的型別。Reference用來存取Object(物件),其功能和C語言的pointer用來存取記憶體有點像,但沒有pointer的&+-等運算符號,而且Reference只能存取型態相符合的類別。宣告Reference的語法是ClassName varName,例如

String s;

宣告s是一個型態為reference的變數,這表示我們可透過s來存取屬於String類別的物件(s is a reference to String object)。

要特別強調的是, s並不是物件, 而是用來指向String物件的reference。打個比方,

public class 動物 {
    動物 手指頭; // java 因字元編碼使用unicode, 所以可用中文當變數名稱
    public static void main(String[] arg) {
        動物 手指頭2;
        手指頭2 = new 動物();
    }
}

變數 "手指頭" 宣告為reference, 可指向屬於 class "動物" 的物件, 手指頭不是動物, 而是用手指頭指向某隻動物。

java.lang.Float f;
java.lang.Double d;
java.lang.Integer i;

以上變數的型態都是reference

運算符號(Operator)

Java語言在運算式的部分,和C語言極為類似, 除了沒有sizeof, pointer和struct相關的運算符號外, 另外新增了>>>向右無號shift, 以及用來判斷物件型態的instanceof。Java的常數的表示法也和C相同,而Java裡的新資料型態boolean的合法值為true和false兩個常數。

算術(Arithmetic)運算符號

運算符號功能敘述
+
*
-
/
%餘數
++加一
--減一

邏輯(logic)運算符號

運算符號功能敘述
>大於
<小於
>=大於等於
<=小於等於
==等於
!=不等於
&&logic AND
||logic OR
!logic NOT
instanceofreference instanceof ClassName
判斷reference所指到的物件其型態是否和ClassName相容

Java語言和C語言有關邏輯運算最大的不同,在於Java以boolean資料型態(只有true和false兩種值)判斷條件是否成立,而C語言只能使用0或非0。

位元(Bit)運算符號

運算符號功能敘述
&bit AND
<<left bit shift
|bit OR
>>right bit shift with sign
^bit XOR
~1補數
>>>同>>但左邊一律補零

其他運算符號

運算元功能敘述
=將右邊的值複製到左邊的變數
(type)將右邊的數值或reference轉換成type型別
+=將右邊的數值加上左邊的數值然後指定給左邊的變數
?:若?左邊成立則做:左邊否則做:右邊
,合併兩個運算視為一個敘述
(運算式)表示()內優先運算
.Reference.ObjectMember或ClassName.ClassName
存取物件或類別成員
new產生物件

優先權

種類運算符號結合順序
group(op)left to right
postfix[] . (params) op++ op--right to left
prefix++op --op +op -op ~ !right to left
creation or castingnew (type)opright to left
multiplicative* / %left to right
additive+ -left to right
shift<< >> >>>left to right
relational< > <= >= instanceof ==left to right
equality== !=left to right
bitwise and&left to right
bitwise exclusive or^left to right
bitwise inclusive or|left to right
logical and&&left to right
logical or||left to right
conditional? :right to left
assignment= += -= *= /= %= &= ^= |= <<= >>= >>>=right to left
seperator,left to right

流程控制敘述

Java的流程控制敘述和C語言極為類似,不同處在於break和continue兩個指令。Java的break和continue指令後面可以加上標籤,以指示要跳出或繼續的範圍。

public class BreakContinueExample {
    public static void main(String[] argv) {
        int i, j;
        outerLoop:
        for (i = 0; i < 100; i++) {
            innerLoop:
            for (j = 0; j < 100; j++) {
                if (j == 50 && i == 50) {
                    break outerLoop;
                }
            }
        }
        System.out.println("Loop have been terminated.");
    }
}

在上面的例子中,當j==50且i==50時,break指令會跳出最外面的迴圈,直接印出迴圈終止訊息。如果break後面沒有outerLoop的話, 只會跳出裡面的迴圈,然後i從51繼續做下去。

字串

C語言定義以0結尾的字元陣列就是字串。但對Java來說, 字串是由String類別來表達, 也就是說String是物件而不是陣列。由於我們經常使用字串, 為了寫作程式方便起見, Java Compiler碰到+符號某一邊的型態是String時, 就會把+翻譯成StringBuffer類別裡相對應的append Method。例如:


public class StringTest {
    public static void main(String[] argv) {
        int x = 5;
        float y = 1.5;
        System.out.println("x = " + x + ", y = " + y);
    }
}

會翻譯成:

public class StringTest {
    public static void main(String[] argv) {
        int x = 5;
        float y = 1.5;
        System.out.println((new StringBuffer("x = ")).append(x).append(", y = ").append(y).toString());
    }
}

如果你會C++, 看到Java字串+符號的語法, 千萬不要以為Java支援operator overloading。Java只是透過Compiler來做特別的轉換, 稱這種技術為Compiler Sugar比較適合。

Java語言的寫作風格

寫作Java程式時,請注意下列幾種風格
/**
 * 第一行的兩個**用來告訴javadoc此部份註解要變成HTML文件的一部份
 * 這段註解裡的所有文字都會變成此類別一開頭的說明
 */
public class Hello { // Class Name首字大寫
    /**
     * 此段註解會變成描述main方法的一部分
     * @param argv 使用@param註記會產生參數(parameter)argv的相關說明
     * @return 傳回值的意義說明
     */
     public static void main(String[] argv) { // Method Name首字小寫
        // argv: array of references to String object
        int myVariable; // 變數宣告
        int i, sum;
        for (i = 1, sum = 0; i <= 100; i++) {
            sum += i;
        }
        System.out.println("summation from 1 to 100 is "+sum);
    }
}

運算符號範例

攝氏溫度轉華氏溫度

public class Example {
    public static void main(String[] argv) {
        float degree = 100.0;
        System.out.println("100C=" + (degree * 9.0 / 5.0 + 32.0));
    }
}

華氏溫度轉攝氏溫度

怎麼寫呢?

1 + 2 + ... + n的總合

public class Example {
    public static void main(String[] argv) {
        int n = 100;
        System.out.println("1+2+...+"+n+"  = " + ( n * (n + 1) / 2));
    }
}

特別注意上述的運算式裡/2要放到最後面,如果寫成n/2*(n+1),從數學式子的角度看好像沒問題,但別忘了,binary operator的兩邊必須是同樣型別的資料,而且計算的結果也是同樣的型別。因此n/2*(n+1)會先計算n/2,如果n不能被2整除的話,那麼為了符合計算結果必須是整數的限制,則小數點的部份就會無條件捨去,使得計算的結果錯誤。下面的範例一樣要注意相同的問題。

12 + 22 + ... + n2的總合

怎麼寫?

把浮點數四捨五入為整數

Java語言規定浮點數轉整數時,小數點部分無條件捨去。如果要達到浮點數四捨五入為整數的效果,可以使用下面的小技巧

public class Example {
    public static void main(String[] argv) {
        double x = 20.6;
        System.out.println(x + " 四捨五入成為 " + (int)(x+0.5));
        System.out.println(x + " 四捨五入成為 " + round(x));
    }
    static int round(double y) {
        return (int)(y + 0.5);
    }
}

迴圈範例

寫一程式輸入5個整數數字,計算其總合和平均。解析:

  1. 需要1個變數紀錄到目前為止所有inputNum的總和,稱此變數為sum,其初始值為0
  2. 以迴圈執行5次,每次輸入數字加總到sum,迴圈執行的次數以變數i來代表
  3. 平均數為sum/5
  4. 如何讀入資料?
import java.util.Scanner;
public class Example {
    public static void main(String[] argv) {
        int sum = 0, i = 0;
        Scanner in = new Scanner(System.in);
        while (i < 5 && in.hasNextInt()) {
            sum = sum + in.nextInt();
            i++;
        }
        System.out.println("sum is "+sum", average is "+(sum/5.0));
    }
}

寫一函數輸入參數int n,傳回1 + 2 + 3 ... + n的總合。解析:

  1. 要想辦法拜訪1,2,3...n的每一個數字一次
  2. 可用for(i=1; i <= n; i++)的形式達成上述目標
  3. 拜訪到這些數字時,就把它們加起來
public class Example {
    public static int sum(int n) {
        int total = 0; // 紀錄到目前為止的總和
        for (int i = 1; i <= n; i++) {
            total += i;
        }
        return total;
    }
    public static void main(String[] argv) {
        System.out.println(sum(100));
    }
}

寫一函數輸入參數int n,傳回1 + 3 + 5 ... + n的總合。解析:

  1. 要想辦法拜訪1,3,5...n的每一個數字一次,也就是從1開始每次加2
  2. 可用for(i = 1; i <= n; i += 2)的形式達成上述目標
  3. 拜訪到這些數字時,就把它們加起來

怎麼寫?

寫一函數於螢幕上畫出九九乘法表。解析:

  1. 總共有i = 1..9 列, j = 1..9 行, 對第i列第j行元素來說, 其數值為i*j
public class Example {
    public static void main(String[] argv) {
        for (int i = 1; i <= 9; i++) {
            for (int j = 1; j <= 9; j++) {
                System.out.print(" " + (i * j));
            }
            System.out.println();
        }
    }
}

輸入參數int size,並在螢幕上印出正方形,size=3的樣子如下

***
***
***

解析

  1. 螢幕上的游標只能由上而下,由左而右,無法回頭。
  2. 此圖形共有1..size列,每列有size個*,因此可用兩層迴圈來做。
  3. 要讓一個敘述執行size次,可用for(i = 1; i <= size; i++)的形式來達成
public class Example {
    public static void print(int size) {
        int i, j; // 第i列,第j行
        for (i = 1; i <= size; i++) { // 印出第i列
            for (j = 1; j <= size; j++) { // 第i列有size個*
                System.out.print("*");
            }
            System.out.println();
        }
    }
    public static void main(String[] argv) throws Exception {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        print(Integer.parseInt(in.readLine()));
    }
}

輸入參數int size,並在螢幕上印出直角三角形,size=3的樣子如下

*
**
***

解析

  1. 螢幕上的游標只能由上而下,由左而右,無法回頭。
  2. 此圖形共有1..size列,第i列有個*,因此可用兩層迴圈來做。

怎麼寫?

撰寫一函數輸入int size,並在螢幕上印出等腰的三角形,size=3的樣子如下

  *
 ***
*****

解析

  1. 總共有1..size列,對第i列而言,有size-i個空格,以及(2 * i- 1)個*

怎麼寫?

寫一函數求兩個整數的最大公因數,解析:

  1. 此函數需要兩個參數x,y
  2. 當y不能整除x時,將x設成為y,y設為x%y, 重複此步驟直到x%y為0
  3. 此時y就是這兩個數的最大公因數
public class Example {
    public static void main(String[] argv) {
        System.out.println(gcd(12,18));
    }
    public static int gcd(int x, int y) {
        int tmp;
        // 如果x < y 則下面的迴圈執行第一次時就會交換x,y了
        while (x % y != 0) {
            tmp = y;
            y = x % y;
            x = tmp;
        }
        return y;
    }
}      

寫一函數求兩個整數的最小公倍數

怎麼寫?

寫一函數求費氏數,解析:

  1. F(n)=n, if n<=1;
  2. F(n)=F(n-1)+F(n-2), otherwise
  3. 可定義兩變數fn_1,fn_2表示最近兩個找出的費氏數
  4. 下一個費氏數依定義為fn_1 + fn_2
  5. 找到最新的費氏數後,最近的兩個費氏數就變成了fn_1+fn_2以及fn_1
  6. 以變數i紀錄目前要求的是哪一個費氏數
  7. 以變數tmp作為更新最新兩個費氏數所需的記憶體空間
public class Example {
    public static void main(String[] argv) {
        System.out.println(fab(5));
    }
    public static int fab(int n) {
        int fn_1 = 1, fn_2 = 0; // 紀錄最近找到的兩個費氏數
        int i, tmp; // i表示目前要找F(i)
        if (n <= 1) return n;
        for (i = 2; i <= n; i++) {
            tmp = fn_1;   // 先把fn_1紀錄在tmp
            fn_1 += fn_2; // 最新的費氏數是前面兩個相加
            fn_2 = tmp;   // 第二新的就是原先的fn_1
        }
        return fn_1;
    }
}

遞迴(recursion)範例

求1+2+3+...+n

解析

public class Example {
    public static void main(String[] argv) {
        System.out.println(sum(100));
    }
    public static int sum(int n) {
        if (n == 1) {
            return 1;
        }
        return n + sum(n - 1);
    }
}

以遞迴計算1*2+2*3+3*4+…+(n-1)*n之和

怎麼寫?

利用遞迴求得A的B次方

public class Example {
    public static void main(String[] argv) {
        System.out.println(power(2, 6));
    }
    public static int power(int a, int b) {
        switch(b) {
        case 0: return 1;
        case 1: return a;
        default: return (a * power(a, b - 1));
    }
}

以遞迴求兩個整數m,n的最大公因數

解析

怎麼寫?

費式數列

解析

public class Example {
    public static void main(String[] argv) {
        System.out.println(fab(5));
    }
    public static int fab(int num) {
        if (num <= 1) {
            return num;
        }
        return fab(num - 1) + fab(num - 2);
    }
}

Ackerman函數

A(m, n)定義為

  1. n + 1, if m = 0
  2. A(m - 1, 1), if n = 0
  3. A(m - 1, A(m, n - 1)), otherwise

怎麼寫?