Java學習之路06—邏輯結構陷阱


架構圖

懸掛

邏輯判斷語句若是只有連接一條語句時,這一條語句會與離它最近的邏輯判斷語句相連,例如以下例子的System.out.println("none-zero!");會接在if結構下,這種語法結構稱為懸吊

while(iter < 10)
    if(iter != 0)
        System.out.println("none-zero!");

錯誤的懸吊常常發生在縮排想表達的邏輯與實際程式執行邏輯不匹配

例如下方程式,當a不大於1時,開發者想把b賦值成20,但else語句的懸吊特性,它其實是連接離它最近的邏輯判斷語句if(a > 1),所以只有當a大於5時才會將b賦值成20,縮排對程式的執行沒有影響

int a = -1;
int b = 0;

if(a > 1)
    if(a > 5)
        b = 1;
else
    b = 10;

System.out.println("b = " + b);

大括號的重要性

其實當邏輯判斷式只有一條語句時是可以省略大括號。若該語句也是邏輯判斷語句,則該原則可以繼續套用下去

例如下面的程式。但是為了易讀性著想請避免嵌套太多非必要邏輯結構

int a = 5;

if(a > 0)
    if(a > 1)
        if(a > 2)
            if(a > 3)
                if(a > 4)
                    System.out.println("hi");

那假如我們在每個邏輯判斷式下加上else語句,並更改a值,那麼它實際上是懸掛在誰的下方呢?

因為邏輯判斷式下的語句都可以被解讀成if(...){},所以我們看到程式範例中嵌套的if語句其實可以看成只有一條語句if(...){if(...){...}}

而else語句的判斷還是依照懸吊的特性分配給最近的邏輯判斷式,若該邏輯判斷式已被分配else語句則像上一層分配

以下這種程式類型俗稱"波動拳"(看看它的形狀),實際編寫程式時應該要思考,如何減少嵌套的層次

int a = 11;

if(a > 0)
    if(a > 10)
        if(a > 20)
            if(a > 30)
                if(a > 40)
                    System.out.println("hi");
                else
                    System.out.println("40");
            else
                System.out.println("0");
        else
            System.out.println("20");
    else
        System.out.println("10");
else
    System.out.println("0");

我們把將上述程式轉換成偽代碼來看看,其實下面的else語句可以想像成列隊等待分配給上面的if語句的排隊人潮

/*視為單條語句*/
if(...) // A
    if(...) // B 
        if(...) // C
            if(...) // D
                if(...) // E
                    // do something
else // 分配給E
    System.out.println("...");
else // 分配給D
    System.out.println("...");
else // 分配給C
    System.out.println("...");
else // 分配給B
    System.out.println("...");
else // 分配給A
    System.out.println("...");

所以實際輸出結果為:

20

假如說開發者在設計之初有進行縮排等格式化的處理,程式的邏輯判斷還好處理,不然的話真得慘不忍睹…但無論如何,在編寫程式時盡量加上大括號,不論是否只有一條語句

加上括號能讓後續開發者更容易讀懂程式邏輯,也可以避免程式上的錯誤,例如以下程式其實就存在兩個問題

  1. if(a > 5)只能懸掛一條語句,也就是說Sytem.out.println("a大於5");無論如何都會被執行
  2. else語句必須懸掛在if語句後,但Sytem.out.println("a大於5");不屬於if語句,所以發生錯誤
int a = 4;
if(a > 5)
    Sytem.out.println("a大於0");
    Sytem.out.println("a大於5");
else
    Sytem.out.println("a不大於5");

更安全的做法 → 不會花您多少寶貴的時間(珍視後續開發人員debug的時間🙏)

int a = 4;
if(a > 5){
    Sytem.out.println("a大於0");
    Sytem.out.println("a大於5");
}
else{
    Sytem.out.println("a不大於5");
}

編譯器會將大括號裡的內容視為一條語句,等同於邏輯判斷式下懸掛一條語句,所以就讓我們再看看另一個例子,下面這兩種程式結構其實是相同的

for (int i=0; i<5; i++){
    for (int j=0; j<5; j++){
        for (int k=0; k<5; k++){
            // do something
                }
      }
}
for(int i=0; i<5; i++)
    for(int j=0; j<5; j++)
        for(int k=0; k<5; k++){
            // do something
        }

null statement

當邏輯判斷語句結尾直接加上分號結尾時,這時就產生所謂的null statement語句,也就是說你編寫的邏輯判斷語句不做任何事,它與後方加上空的大括號其實作用是一樣的

while語句若遇到null statement,則會遇到無窮迴圈,同理若是for迴圈沒有良好的退出機制,也容易卡進無窮迴圈

例如下面幾個語句其實就發生了null statement,只有if語句可以安全退出

if(i == 3);

while(iter != myobj.gain());
    System.out.println("I'm here!");;

for(i=0;;i++);

整數與布林

C/C++因為沒有所謂真的布林值,所以使用非0和0代替,所以我們常看到邏輯判斷語句中使用整數當作true或false的依據

但在java中,布林型態無法和其他型態做運算。因此諸如if, while, do-while, for等語句需要避免整數與布林混用判斷

例如下面的錯誤程式代碼

public static void main(String[] args){
    int a=10, b=15, c=20, d=25;

    if(a>b && b>c){
        System.out.println("1. 執行成功");
    }
    else if((++d >= a--) == 1){ // boolean不能跟整數比較
        System.out.println("2. 執行成功");
    }
    else{
        System.out.println(3. 執行成功);
    }
}

使用浮點做判斷

實際編寫程式時盡量不要使用浮點數判斷,下列程式雖然能夠順利執行,但實際打印值卻與判斷值不相等,因為浮點數的運算與賦值上或多或少會存在誤差,盡可能的使用整數或其他變數類型當判斷式依據

public class MathDemo {

    public static void main(String[] args) {
        float c=123456789.123456789f;
        if(c > 123456789.123456789f){
            System.out.println("c > 123456789.123456789");
        }
        else if(c == 123456789.123456789f){// 危險
            System.out.println("c == 123456789.123456789");
        }
        else{
            System.out.println("c < 123456789.123456789");
        }

        System.out.println(c);
    }
}

數據類型判斷原則

可用來比較的數據類型

  1. int
  2. long
  3. double
  4. float
  5. char
  6. String

原則上只要能透過自動類型轉換進行匹配的變數類型都可以進行互相比較。不過一般情況下都是相同數據類型之間進行比較

int         i = 6;
long         l = 18l;
double         d = 6.0d;
float         f = 6.0f;
char         ch = 'a';
String         s = "a";
boolean     b = true;

/*整數之間比較*/
System.out.println(i > l); // false

/*浮點數之間比較*/
System.out.println(f == d); // true

/*整數與浮點數之間比較*/
System.out.println(i == d); // true
System.out.println(l > f); // true

/*整數與字元之間*/
System.out.println(i > ch); // false
System.out.println(l > ch); // false

/*整數、浮點數、字元都不能與字串比較*/
System.out.println(i > s);
System.out.println(d > s);
System.out.println(ch == l);

/*布林類型不能與任何類型比較*/
System.out.println(i > b);
System.out.println(l > b);
System.out.println(f > b);
System.out.println(d > b);
System.out.println(ch > b);
System.out.println(s > b);

case順下問題

若在switch語句的case中沒有填上break語句,則執行權會自動向下執行另一個case直到遇上break

例如下面的switch案例,把所有的break註解掉後,會把當前case以下的所有表達式全部執行

int a = 100;

switch(a/10){
    case 10:
    case 9:
        System.out.println("A");
        // break;
    case 8:
    case 7:
        System.out.println("B");
        // break;
    case 6:
        System.out.println("C");
        // break;

    default:
        System.out.println("不及格");
        // break;
}

輸出結果:

A
B
C
不及格

程式發生多個不同狀況同時執行輸出時,不妨查看stwich語句的case是否正確填入break語句

switch的輸出類型

JDK7.0以後表達式的值可以是基本數據類型byte, short, int, char,以及String類型,但switch判斷式內的數據類型必須要跟case後的變數類型一致,否則程式執行時會拋出異常

switch後表達式的變數類型不可為long, float, double, boolean。還記得浮點數的精準度問題嗎,所以它不適合放在switch語句中做判斷。而布林就更好理解了,能用if解決的問題,不需要動用switch結構

至於long這個比較特別,看後續java更新會不會加入long類型,目前還是不支援的

Scanner s = new Scanner(System.in);
System.out.print("輸入字串str: ");

String str = s.next();

switch(str){
    case "apple":
        System.out.println("蘋果");
        break;
    case "banana":
        System.out.println("香蕉");
        break;
    case "watermelon":
        System.out.println("西瓜");
        break;
    case "papaya":
        System.out.println("木瓜");
        break;
    default:
        System.out.println("沒有這種水果");
        break;
}

for的表達式

我們先來看看for語句執行的順序為何,以下面的程式碼為例

首先int n=1為初始化語句,目的是對局部變數n(或已宣告的其他變數)進行賦值,該語句只會執行一次

接下來是判斷變數是否符合條件n<5,若成立則進入迴圈

完成迴圈內的語句後會對局部變數n進行自增處理n++,完成後再判斷是否符合n<5,為真則再次進入迴圈內,否則退出for迴圈。後續的迴圈處理都是循環這個部分

for(int n=1; n<5; n++){
    // do something
}

其實我們可以將for迴圈的表達邏輯以while的方式呈現,相信這樣會更好理解。一樣看看下面這段偽代碼,兩個邏輯判斷式的功能一模一樣

for(int i=100; (i%5 == 0)&(i > 8); i=i-8){
    System.out.println("i = " + i); 
}
int i = 100;
while((i%5 == 0)&(i > 8)){
    System.out.println("i = " + i); 
    i -= 8;
}

輸出結果均為:

i = 100

省略表達式

for迴圈的三個判斷式均能夠被省略,其中第二個判斷式被省略後,預設為true

舉例來說若省略第一個表達式,則相當於把初始化功能去除,若希望程式能夠正常執行,需要另外對變數進行初始化

int i = 0;
for(; i<5; i++){
    System.out.println("hi");
}

省略第二個判斷式,相當於把離開for loop的條件省略,使得第一個分號後恆為true,這會讓程式無法跳出迴圈

若不希望程式變成無窮迴圈,可以在迴圈內編寫執行跳出的條件語句

int a = 0;
for(int i=0;;i++){
    if(i>10)
        break;
    a++;
}

System.out.println("a = " + a);

省略第三個表達式相當於去除迴圈後續會進行的變數運算,這個運算式將運算結果交給第二個判斷式仲裁是否再次進入迴圈

省略該項很有可能也造成無窮迴圈,因為判斷變數從初始化以後就沒有變過,所以必須在迴圈本體中加入運算式才能正確執行

for(int i=0; i<50;){
    if(i%2 == 0){
        System.out.println("even!");
        i += 7;
    }
}

省略三個表達式,其表達為一個無窮迴圈,相當於while(1)迴圈

for(;;){}

while(1){}

for中填入多個判斷式

除了第二個判斷式以外,第一以及第三個判斷式可以進行擴充

例如以下程式,同時對i和j初始化,並對這兩個變數做運算處理,不過退出迴圈條件只會有j < q一個,不能進行擴充

int i, j;
int p = 0, q = 8;
for(i = 0, j = 0; j < q; --i, j++){
    System.out.println("i = " + i + ", j = " + j);
}

輸出結果:

i = 0, j = 0
i = -1, j = 1
i = -2, j = 2
i = -3, j = 3
i = -4, j = 4
i = -5, j = 5
i = -6, j = 6
i = -7, j = 7

有興趣的朋友可以利用for loop的擴充功能編寫一些程式碼看看,例如下面這段程式碼就是指使用一個for loop循環打印一個九九乘法表

但是記得實際編寫程式時不要這麼做嘿😎

for(int i=1, j=1; j<10; i=(i==9 ? (++j/j) : i+1)){
    System.out.print(i + " * " + j + " = " + i*j + (i==9 ? '\n' : ", "));
}

輸出結果:

1 * 1 = 1, 2 * 1 = 2, 3 * 1 = 3, 4 * 1 = 4, 5 * 1 = 5, 6 * 1 = 6, 7 * 1 = 7, 8 * 1 = 8, 9 * 1 = 9
1 * 2 = 2, 2 * 2 = 4, 3 * 2 = 6, 4 * 2 = 8, 5 * 2 = 10, 6 * 2 = 12, 7 * 2 = 14, 8 * 2 = 16, 9 * 2 = 18
1 * 3 = 3, 2 * 3 = 6, 3 * 3 = 9, 4 * 3 = 12, 5 * 3 = 15, 6 * 3 = 18, 7 * 3 = 21, 8 * 3 = 24, 9 * 3 = 27
1 * 4 = 4, 2 * 4 = 8, 3 * 4 = 12, 4 * 4 = 16, 5 * 4 = 20, 6 * 4 = 24, 7 * 4 = 28, 8 * 4 = 32, 9 * 4 = 36
1 * 5 = 5, 2 * 5 = 10, 3 * 5 = 15, 4 * 5 = 20, 5 * 5 = 25, 6 * 5 = 30, 7 * 5 = 35, 8 * 5 = 40, 9 * 5 = 45
1 * 6 = 6, 2 * 6 = 12, 3 * 6 = 18, 4 * 6 = 24, 5 * 6 = 30, 6 * 6 = 36, 7 * 6 = 42, 8 * 6 = 48, 9 * 6 = 54
1 * 7 = 7, 2 * 7 = 14, 3 * 7 = 21, 4 * 7 = 28, 5 * 7 = 35, 6 * 7 = 42, 7 * 7 = 49, 8 * 7 = 56, 9 * 7 = 63
1 * 8 = 8, 2 * 8 = 16, 3 * 8 = 24, 4 * 8 = 32, 5 * 8 = 40, 6 * 8 = 48, 7 * 8 = 56, 8 * 8 = 64, 9 * 8 = 72
1 * 9 = 9, 2 * 9 = 18, 3 * 9 = 27, 4 * 9 = 36, 5 * 9 = 45, 6 * 9 = 54, 7 * 9 = 63, 8 * 9 = 72, 9 * 9 = 81

while(i == i+1)的難題

請思考這樣一個問題,要將i賦值成多少才能使下面的while迴圈變成無窮迴圈

while(i == i + 1){}

這個問題勢必牽涉到在java中,甚麼變數經過加1運算後還會跟自己相等,這個問題官方文件給出了很明確的答案

浮點數的(正負)無限值或者是非常大的一個數值,本身經過與有限數值或與自身正負號相等的無限數值加減運算後,其結果依然與自身相等

因此我們將浮點數設成正負無限兩種,發現都可以將while()判斷式置為無窮迴圈

float i = Float.POSITIVE_INFINITY; // 正無限
// float i = Float.NEGATIVE_INFINITY; // 負無限
// double i = Double.POSITIVE_INFINITY; // double類型也可以

while(i == i + 1){}

另外因為浮點數的精準度有一個最小極限,也就是說當浮點變數被賦值一個非常大的數值時,進行加減運算其實是不會影響原有數值的

例如將float的最小值加1其實還是等於自己本身

float i = -Float.MAX_VALUE;

while(i == i + 1){}

或者手動賦值一個非常大的正負常數,也可以得到相同效果

float i = 9999999999999f;

while(i == i + 1){}

關於浮點數最小值問題可以參考之前的文章

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

%d 位部落客按了讚: