2014年3月23日 星期日

Java 複習筆記 : 資料型態與語法

此處整理 Java 資料型態與語法方面一些較容易搞混與忽略的觀念.

http://docs.oracle.com/javase/7/docs/api/

一. include 與 import 的差異 :

Java 程式頭常會用 import 指令匯入所需之類別或類別庫 (用 .*), 這似乎與 C/C++ 的 imclude 很像, 功能上都是匯入 library, 但其實兩者不同. C/C++ 的 include 會在編譯時把指定的函式全部匯入程式裡, 但 Java 的 import 並非將類別匯入程式中, 只是告訴編譯器程式中會用到的類別的路徑而已. 當使用 * 匯入函式庫時, 程式中用不到的類別並不會造成程式膨脹或執行效能下降, 因為 JVM 是在執行時期才會把實際使用到的類別內容匯入.

要注意的是, 位於相同目錄下的其他類別不需用 import 指令匯入, 直接使用即可, 因為編譯器會自動在目前目錄底下搜尋使用到的其他類別, 搜尋不到時才到 import 指令區去找該類別的套件位置.

二.  變數與資料型態 :

Java 是物件導向語言, 主要的資料型態當然是物件, 字串與陣列都是物件, 但為了增進執行效率也提供了 8 種原始資料型態, 這使得 Java 不能像 Small Talk 語言那樣稱為 100% 的物件導向語言. Java 的資料類型如下圖所示 :




Java 的資料型別可以說只有兩類, 一個是原始資料型態, 一是參考型態. 原始型態又可分為數值 與布林兩大類 (字元可以視為數值型態). 而參考類型則包括字串, 陣列, 與物件.

這裡首先要特別注意的是字元, 整數, 與實數的常數表示法 :

1. 字元與字串常數表示法 :

字串常數要用雙引號括起來, 字元常值要用單引號 :

char ch='K';             //以字面值表示字元
char ch='\101';         //以 8 進位表示字元
char ch='\u0041';     //以 unicode 表示字元
String str="string";   //字串要用雙引號

字元除了用字面值 (Literal)  'A' 表示以外, 也可以使用 8 進位或 16 進位 unicode 字元編碼表示, 因為 Java 的字元本身就是用 Unicode 儲存的. 用 16 位元 unicode 字元編碼表示時, 其值為 '\u0000' 至 '\uffff' (0 至 65535), 只要將字元宣告為 int 類型, 就可顯示其整數編碼值了 :

char ch1='A';
char ch2='台';
char ch3='\u0041';
char ch4='\101';
int i1=ch1;  //自動轉型為 int
int i2=ch2;  //自動轉型為 int
int i3=ch3;  //自動轉型為 int
int i4=ch4;  //自動轉型為 int
System.out.println(ch1);   //輸出 A
System.out.println(ch2);   //輸出 台
System.out.println(ch3);   //輸出 A
System.out.println(ch4);   //輸出 A
System.out.println(i1);      //輸出 65
System.out.println(i2);      //輸出 21488
System.out.println(i3);      //輸出 65
System.out.println(i4);      //輸出 65

2. 整數常數表示法 :

Java 的整數有 byte, short, int, 與 long 四種, 整數常數預設型態是 int, 如果數值較大, 可以在最後面加上 l 或 L 表示用長整數儲存, 例如 123L.

整數常數原本只有三種表示法, 即 8, 10, 16 進位表示法, Java 7 新增了 2 進位表示法. 除了十進位表示法以外, 其他三種表示法都需要前置字元, 八進位的前置字元為 0, 十六進位為 0x 或 0X, 二進位則為 0b 或 0B :

int i1=15;
int i2=017;
int i3=0xf;
int i4=0b1111;         //二進制表示法 : Java 7 才有支援
int i5=9_000_210;   //以底線代表千位數分段 (Java 7 才有支援)
System.out.println(i1);   //輸出 15
System.out.println(i2);   //輸出 15
System.out.println(i3);   //輸出 15
System.out.println(i4);   //輸出 15
System.out.println(i5);   //輸出 9000210

注意, 這裡的 ox/0X 與 0b/0B 是 Java 兩個不分大小寫的地方之一. 其次, 不論是 2/8/10/16 哪一種進位表示法, 整數常數預設型態是 int (例如上面的例子最後面都沒有 l 或 L, 所以預設都以 int 存放), 但也可以在數值最後面加上 l 或 L 表示要用 long 型態儲存 (8 個 bytes), 例如 :

long i1=15L;
long i2=15l;

這是 Java 兩個不分大小寫的地方之二. 這樣就會出現一個問題, 當為一個變數賦值時, 就須考慮變數與常數值長度的匹配, 例如 :

int i1=15L;

常數 15 是用 long 儲存, 但變數 i1 卻宣告為 int, 這在編譯時就會出現 "error: possible loss of precision" 的錯誤, 表示這樣的賦值可能會造成數值失去精確度. 這必須使用強制轉型才能通過編譯 :

int i1=(int)15L;

強制轉型是告訴編譯器, 我們接受可能之精確度損失, 這樣就能通過編譯了.

另外, 在 Java 7 以後開始支援商用的千位分段符號, 但用底線取代逗號.

3. 實數常值表示法 :

Java 的實數有兩個 : float 與 double, 實數常數有浮點數與科學表示法兩種表示法, 在數值末尾可以加 f/F 或 d/D 指定儲存型態, 若未指定, 則預設型態為 double :

float f1=123.45f;
float f2=1.2345e4F;
double d1=123.45;  //預設為 double
double d2=123.45d;
double d3=1.2345e4D;

上面的 d1 值末尾未指定型態, 故預設為 double, 變數 d1 也須宣告為 double, 若上例中的 d1 變數宣告為 float, 則編譯時將出現 "loss of precision" 錯誤, 必須宣告為 double, 或用 float 強制轉型才能通過編譯.

其次 Java 的實數還有位數限制, 須注意這會影響數值精確度, 小數位數部分, float 最多是 7 位數, double 最多 17 位數, 位數超過時會被截掉,  float 在剪掉尾巴時會四捨五入, 但 double 則直接捨去, 如下所示 :

public class test {
  public static void main(String[] args) {  
    float f1=0.123456789F;
    float f2=0.1234567890e2F;
    float f3=123456789F;
    double d1=0.1234567890123456D;
    double d2=0.1234567890123456789e2D;
    double d3=1234567890123456789D;
    System.out.println(f1);    //輸出 0.12345679    (8 位數不含 0)
    System.out.println(f2);    //輸出 12.345679      (8 位數)
    System.out.println(f3);    //輸出 1.23456792E8 (9 位數)
    System.out.println(d1);   //輸出 0.1234567890123456  (17 位數不含 0)
    System.out.println(d2);   //輸出 12.345678901234567  (17 位數)
    System.out.println(d3);   //輸出 1.23456789012345677E18 (18 位數)
    }
  }

另外, 實數都會以科學表示法儲存, 因此上例中 f1 會先轉成 1.23456789E-1, 因為 float 只能儲存 7 位小數, 所以會四捨五入為 1.2345679E-1, 最後顯示為 0.12345679. 同樣地, f2 先轉成 1.234567890E1, 小數部分四捨五入成 7 位數 1.2345679E1, 最後轉回 12.345679 :

f2 : 0.1234567890E2 -> 1.234567890E1 -> 1.2345679E-1 -> 12.345679
f3 : 123456789 -> 1.23456789E8 -> 1.2345679E8 -> 1.23456792E8

f3 會先轉成 1.23456789E8, 但因為 float 只能儲存 7 位小數, 因此第 8 位的 9 會四捨五入變成 1.2345679E8, 但因為小數第 8 位會有誤差, 最後得到之值為 1.23456792E8. 而變數 d3 會先轉成 1.234567890123456789E18, 因為 double 只能存 17 位小數, 所以會直接刪掉最後面的 9 變成 1.23456789012345678E18, 同樣因為第 18 位小數的誤差, 最後得到 1.23456789012345677E18.

關於浮點數精確度問題, 參考 : java中的float和double的精度问题

4. 成員變數的預設值 :

類別的成員若為原始型態變數, 即使宣告時未初始化, JDK 編譯器仍會自動賦予預設值如下, 故可通過編譯 :



public class test {
  static byte by;
  static short s;
  static int i;
  static long l;
  static float f;
  static double d;
  static char c;
  static boolean bo;
  public static void main(String[] args) {
    System.out.println(by);  //輸出 0
    System.out.println(s);    //輸出 0
    System.out.println(i);     //輸出 0
    System.out.println(l);     //輸出 0
    System.out.println(f);     //輸出 0.0
    System.out.println(d);    //輸出 0.0
    System.out.println(c);    //輸出
    System.out.println(bo);  //輸出 false
    }
  }

但區域變數就不會自動給予預設值了, 方法中的區域變數一定要初始化才能使用, 否則無法通過編譯, 出現 "variable xxx might not have been initialized" 錯誤.

至於 String 與陣列均屬於物件, 若類別的成員變數為物件, 宣告後未初始化, 其預設值為 null.

5. 原始資料類型的包裹類別 :  

雖然原始資料型態運算較快速, 但只能做單純的運算, 對於較複雜之計算必須呼叫方法, 而只有類別才有方法, 因此 Java 在 java.lang 類別庫中為 8 種基本類型定義了包裹類別 (wrapper class), 可以將原始型態包裝成類別 :


由於八個原始型態中有六個為數值型態, 因此包裹類別繼承了 java.lang.Number 為父類別. 呼叫其建構子即可建立包裹物件, 可傳入對應之原始資料型態數值字串 :

Byte b=new Byte((byte)127);
Byte b=new Byte("127");

注意, 因為整數常值預設為 int 型態, 因此必須強制轉型為 byte, 否則編譯時會出現 "no suitable constructor found for Byte(int)" 錯誤. 其他包裹類別也是如此. 包裹類別提供了幾個有用的方法.

首先是 xxxValue() 方法, 此方法可將包裹物件的值轉成指定之數值類原始型態值傳回, 這個方法不支援 char 與 boolean, 只支援下列六種原始數值型態 : byte, short, int, long, float, 與 double :

Double -> byte, short, int, long, float
Float ->  byte, short, int, long, double
Long -> byte, short, int, float, double
Integer -> byte, short, long, float, double
Short -> byte, int, long, float, double

例如 :

Byte b=new Byte((byte)127);             //必須強制轉型
Short s=new Short("32767");
Integer i=new Integer(12345);             //不須強制轉型
Float f=new Float(1234.56789);
Double d=new Double("23.59123e10");
System.out.println(b.doubleValue());    //輸出 127.0
System.out.println(s.floatValue());        //輸出 32767.0
System.out.println(f.intValue());            //輸出 1234 (有誤差)
System.out.println(d.floatValue());        //輸出 2.35912298e11 (有誤差)

注意傳入原始型態常數時可能需要強制轉型, 而且浮點數轉型時可能會有誤差出現.

第二個常用方法是 parseXxx() 方法, 此方法會剖析傳入之字串, 傳回指定之原始資料型態值, 支援除了 char 以外的 7 種原始資料型態 :

byte parseByte(String str)
short parseShort(String str)
int parseInt(String str)
long parseLong(String str)
boolean parseBoolean(String str)
float parseFloat(String str)
double parseDouble(String str)

例如 :

byte b=Byte.parseByte("127");
int i=Integer.parseInt("123");
float f=Float.parseFloat("123.456789");
boolean b1=Boolean.parseBoolean("true");
boolean b2=Boolean.parseBoolean("True");
boolean b3=Boolean.parseBoolean("OK");
System.out.println(b);     //輸出 127
System.out.println(i);      //輸出 123
System.out.println(f);      //輸出 123.456789
System.out.println(b1);   //輸出 true
System.out.println(b2);   //輸出 true
System.out.println(b3);   //輸出 false

注意, 數值字串之值不可超過值域, 例如上例中的 byte 若傳入 "128", 雖可通過編譯, 但執行時會出現 "java.lang.NumberFormatException: Value out of range." 錯誤.

第三個常用方法是 equals() 與 compareTo(), 這跟 String 類別一樣, 用來比較兩個包裹物件之值. 兩者的差別只是傳回值不同, equals() 傳回 true/false, 而 compareTo() 則傳回數值, 相等傳回 0, 本物件較大傳回正數, 較小傳回負數, 例如 :

Integer i1=new Integer(127);
Integer i2=new Integer(126);
System.out.println(i1.equals(i2));              //輸出 false
System.out.println(i1.compareTo(i2));      //輸出 1

另外, Float 與 Double 提供了 isNaN() 與 isInfinite() 用來判斷是否為數值, 以及是否為無限大 (即除以 0 情況), 此為類別方法, 可以直接呼叫, 傳入計算式 :

System.out.println(Double.isInfinite(1.0/0.0));     //輸出 true (除以 0 為無限大)
System.out.println(Double.isNaN(1.0/0.0));       //輸出 false (無限大仍為數值)
System.out.println(Double.isNaN(0.0/0.0));       //輸出 true (0/0 為非數值)

也可以用包裹物件方法呼叫 :

Double d=new Double(1.0/0.0);
System.out.println(d.isInfinite());    //輸出 true
System.out.println(d.isNaN());       //輸出 false

但是 Float 只能使用物件方法呼叫, 不能呼叫類別方法 (雖然 API 中說可以) :

Float f=new Float(1.0/0.0);
System.out.println(f.isInfinite());     //輸出 true
System.out.println(f.isNaN());       //輸出 false

下列呼叫無法編譯成功, 出現 "no suitable method found for isInfinite(double)" 錯誤訊息 :

//System.out.println(Float.isInfinite(1.0/0.0));  
//System.out.println(Float.isNaN(1.0/0.0));    

除了 Character 外, 其他 7 個包裹類別都定義了兩個屬性 MIN_VALUE 與 MAX_VALUE 是用來紀錄各資料類型的值域 :

System.out.println(java.lang.Byte.MIN_VALUE);      //輸出 -128
System.out.println(java.lang.Byte.MAX_VALUE);     //輸出 127
System.out.println(java.lang.Short.MIN_VALUE);     //輸出 -32768
System.out.println(java.lang.Short.MAX_VALUE);    //輸出 32767
System.out.println(java.lang.Integer.MIN_VALUE);   //輸出 -2147483648
System.out.println(java.lang.Integer.MAX_VALUE);  //輸出 2147483647
System.out.println(java.lang.Long.MIN_VALUE);      //輸出 -9223372036854775808
System.out.println(java.lang.Long.MAX_VALUE);     //輸出 922337203685477580
System.out.println((int)java.lang.Character.MIN_VALUE);    //輸出 0
System.out.println((int)java.lang.Character.MAX_VALUE);    //輸出 65535
System.out.println(java.lang.Float.MIN_VALUE);      //輸出 1.4E-45
System.out.println(java.lang.Float.MAX_VALUE);     //輸出 3.4028235E38
System.out.println(java.lang.Double.MIN_VALUE);   //輸出 4.9E-324
System.out.println(java.lang.Double.MAX_VALUE);  //輸出 1.7976931348623157E308

注意, 因為字元是以 unicode 儲存, 因此要強制轉型為 int 才會轉成字元編碼, 否則會顯示空字串與問號.

6. 原始資料型態的轉型問題 :

當使用 "=" 運算子為變數賦值時, 等號兩邊的資料型態必須相同, 亦即, 等號左邊變數宣告的型態, 必須與等號右邊常數的型態相同, 否則可能出現 "type imcompatible" 或 "loss of precision" 的錯誤. 例如 :

byte b=true;

就會出現如下錯誤訊息 :

test.java:3: error: incompatible types
byte b=true;
        ^
  required: byte
  found:    boolean
1 error

因為變數 b 是 byte 整數, 而 true 是布林值, 兩者不匹配. 再者, 例如 :

int i=20.1;

會出現如下編譯錯誤 :

test.java:3: error: possible loss of precision
int i=20.1;
      ^
  required: int
  found:    double
1 error

因為變數 i 是整數, 而 20.1 預設是 double, 所以會失去精確度. 精確性錯誤可以透過強制轉型解決, 這是告訴編譯器, 我們知道並容許失去精確性, 這樣就能編譯成功了 :

int i=(int)20.1;  //將 double 強制轉型為 int

但是型態不匹配問題卻不是強制轉型能解決的 (無解), 因為那是無法轉換的, 例如 :

byte b=(byte) true;  //錯誤的強制轉換

這會出現下列錯誤訊息 :

test.java:3: error: inconvertible types
byte b=(byte)true;
             ^
  required: byte
  found:    boolean
1 error

可見強制轉型也是有限制的, boolean 型態無法跟任何數值型態互轉, 只有數值 (整數, 實數, 字元) 型態之間才能互轉, 而且只有在精確度大變小 (narrowing) 時才需要強制轉型, 而精確度小變大 (widening) 時不需要強制轉型, 編譯器會進行自動轉型 (auto-casting).下列是大變小必須強制轉型的例子 :

byte b=(byte)65;
short s=(short)123;
int i=(int)123L;
long l=(long)1.23F;
float f=(float)1.23D;
char c1=(char)65L;
char c2=(char)s;
char c3=(char)b;

byte b1=(byte)c3;
System.out.println(b);   //輸出 65
System.out.println(s);   //輸出 123
System.out.println(i);    //輸出 123
System.out.println(l);    //輸出 1
System.out.println(f);    //輸出 1.23
System.out.println(c1);  //輸出 A
System.out.println(c2);  //輸出 {
System.out.println(c3);  //輸出 A
System.out.println(b1);  //輸出 65

注意, 上例中的 float 轉 long, 雖然 long 是 8 bytes, 而 float 僅 4 bytes, 但是 float 精確度比 long 大, 因此一定要強制轉換才能通過編譯. 其次, char 佔 2 bytes (與 short 一樣), 雖然比 byte 大, 但是 char 沒有負數, 因此 short 與 byte 轉成 char 也是必須強制轉換, 事實上, 所有數值類型轉成 char 都必須強制轉換. 但是 char 常數轉成其他數值卻都會自動轉型 (因為 char 沒有負數) :

char c='A';
byte b='A';   //用 b=c 不行
short s='A';  //用 b=c 不行
int i=c;
long l=c;
float f=c;
double d=c
System.out.println(b);  //輸出 65
System.out.println(s);  //輸出 65
System.out.println(c);  //輸出 A
System.out.println(i);   //輸出 65
System.out.println(l);   //輸出 65
System.out.println(f);   //輸出 65.0
System.out.println(d);  //輸出 65.0

可見常數字元皆會自動轉型為其他任何數值型態, 但轉成 byte 與 short 時不可使用字元變數, 必須強制轉換, 用字元常數才會自動轉換. 除了轉成 byte 與 short 外, char 轉成其他類型不論常數或變數均可.

以下是會自動轉型的兩種情況 :
  1. byte -> short -> int -> long -> float -> double
  2. int -> byte, short, char (僅在值域內時會自動轉型)
第一項的自動轉型範例 :

byte b=127;  //int 轉 byte
short s=b;     //byte 轉 short
int i=s;          //short 轉 int
long l=i;        //int 轉 long
float f=l;       //long 轉 float
double d=f;  //float 轉 double
System.out.println(b);   //輸出 127
System.out.println(s);   //輸出 127
System.out.println(i);    //輸出 127
System.out.println(l);    //輸出 127
System.out.println(f);    //輸出 127.0
System.out.println(d);   //輸出 127.0

上面第二項 int 轉成 byte, short, char 是精確度由大變小, 照理是不會自動轉型, 但因為 int 是整數常數之預設型態, 因此只要常數是在值域之內, 會自動轉型, 不需強制轉型, 例如 :

byte b1=127;                //值域內自動轉型
byte b2=(byte)128;        //值域外需強制轉型
short s1=32767;            //值域內自動轉型
short s2=(short)32768;  //值域外需強制轉型
System.out.println(b1);   //輸出 127
System.out.println(b2);   //輸出 -128
System.out.println(s1);   //輸出 32767
System.out.println(s2);   //輸出 -32768

但是實數就不是這樣了, 都必須強制轉型. 實數常數預設型態為 double, 即使其值在值域之內, 仍然不會自動轉型 :

float f1=(float)1234.5678;        //必須強制轉型
float f2=(float)1.2345678e3;    //必須強制轉型
System.out.println(f1);              //輸出 1234.5678
System.out.println(f2);              //輸出 1234.5678

7. 宣告適當資料型態 :

宣告原始型態變數時要選擇適當資料型態, 否則會浪費記憶體空間, 甚至影響計算結果, 導致不正確的輸出, 例如數值運算有精確度問題 :

int i=1/2;
float f=10/3;
double d=10.0/3;
System.out.println(i);    //輸出 0
System.out.println(f);    //輸出 3.0
System.out.println(d);   //輸出 3.3333333333333335

可見兩個整數相除, 結果也是只取整數, 所以 1/2 得到 0, 而不是 0.5.


沒有留言 :