循环结构,了解Java爱的魔力转圈圈

循环结构

你好,欢迎回来,我是BoBo!HAKUNA MATATA!!!

紧接着上次课的选择结构,今天学习流程控制的另外一种情况:循环结构。“循环”就是事物周而复始的运动或变化的现象,生活中这样的现象数不胜数。比如海陆空物流系统、自然界的水循环系统,还有废弃物回收利用的环保系统等等:

循环结构,了解Java爱的魔力转圈圈_第1张图片

Java 语言与现实生活是紧密联系的,因此,在 Java 语言中也有让代码重复执行的循环结构。

代码不会无缘无故地重复执行,想要让代码重复执行,必须要满足一定的条件;此外,代码重复执行多少次呢?如果无限制的重复执行下去,其它的代码将永远也得不到执行的机会,这肯定不是我们想要看到的,还得想办法控制代码执行的次数。因此,想要弄明白循环结构,我们必须得想清楚两件事:

  • 代码重复执行的条件
  • 如何控制代码重复执行的次数

Java 语言为我们提供了三种基本的循环结构:forwhiledo...while,绝大部分时候三种循环都能实现相同的效果,也就是说,它们是可以相互替换的,但因为彼此格式上细微的差异,决定了不同的循环更适合于不同的场景。本次课程将为你介绍这三种循环的格式和用法,希望你重点关注它们的差异,以便把它们用在最合适的场景。

通过本次课程的学习,你将会有以下收获:

  1. 使用 Java 语言中三种基本循环结构(forwhiledo...while)实现代码的重复执行

    1. 在循环结构中根据一定条件控制循环是否继续或终止
    2. 在复杂的循环结构中进行跳转

本次课程的内容如下:

  • ​ 循环:爱的魔力转圈圈

    ​ for循环

    ​ while循环

    ​ do…while循环

  • ​ 循环终止:不带走一片云彩
  • ​ 标号:循环跳转的骚走位

第一关 循环:爱的魔力转圈圈

1.1 for循环

开发中用的最多的是 for 循环,并非它有多特殊,习惯而已。看一个需求:把“爱的魔力转圈圈”输出5遍。你当然可以写5次输出语句,但是太low,你好意思写,我都不好意思看。使用 for 循环该怎么做呢?上代码:

public class Test{
    public static void main(String[] args) {
        for (int i = 1; i <= 5; i++) {
            System.out.println("爱的魔力转圈圈");
        } 
    }
}

输出:

爱的魔力转圈圈
爱的魔力转圈圈
爱的魔力转圈圈
爱的魔力转圈圈
爱的魔力转圈圈

这段代码看起来简单又复杂:简单指的是输出语句只有一句,复杂指的是,for 循环的声明看起来内容很多,而且跟以前每句代码独占一行的写法不同。没错,我们把 for 循环的声明提取出基本格式:

public static void main(String[] args) {
    for (初始化语句; 判断条件; 控制条件) {
        // 循环体
    }
}

从整体来看,for 循环的声明与 if 语句的声明,结构上类似:关键字 + 小括号 + 大括号,只不过 if 语句中的小括号里只有关系表达式,而 for 循环小括号里用两个分号(;)将三条语句隔开:初始化语句、判断条件、控制条件,这里的“判断条件”就是一个关系表达式,返回结果也是 boolean 类型,与 if 语句里的一致;后面紧跟着的一对大括号,里面是要重复执行的代码,叫做“循环体”,这就是 for 循环声明的固定格式。

初始化语句:for 循环执行需要的初始数据,一般都是变量的定义。

判断条件:代码重复执行的条件,意思是,条件成立就会重复执行,不成立就终止循环。

控制条件: 控制整个循环能否继续执行下去的条件,一般是对“判断条件”的数据的修改。

循环体:要重复执行的代码。

那这个 for 循环是如何执行的呢?来看看它的执行流程:

循环结构,了解Java爱的魔力转圈圈_第2张图片

对照上图和代码。

for循环开始,会首先执行初始化语句,完成所需数据的定义和初始化;
紧接着执行判断条件,此时,判断条件有可能成立,也有可能不成立:
    如果条件不成立(判断条件返回 false):循环立即结束;
    反之,如果条件成立(判断条件返回 true):执行循环体,这时,会把循环体中所有代码执行一遍,
        然后,执行控制条件,
        到此为止,第一次循环执行结束,打印了一行信息:爱的魔力转圈圈。

很明显,for 循环并没有终止执行,接下来,它继续执行判断条件,检查循环继续执行的条件是否成立,同样的:
    如果条件不成立(判断条件返回 false):循环立即结束;
    反之,如果条件成立(判断条件返回 true):执行循环体,这时,会把循环体中所有代码再执行一遍,
        然后,再执行控制条件,
        到此为止,第二次循环执行结束,再一次打印了一行信息:爱的魔力转圈圈。

就这样一直重复下去,直到判断条件不成立,循环结束。

从上面的执行流程可以看出,判断条件的成立与否,是决定循环体是否执行的关键,初始化语句给判断条件提供了初始数据,而控制条件通过修改判断条件使用的数据,从而控制判断条件是否成立,进而影响循环体的重复执行。

那么请问,假设for 循环最大可执行N次,for 循环中的初始化语句、判断条件、控制条件和循环体在整个循环执行的过程中,分别会执行多少次?

  • 初始化语句:有且仅有 1 次。
  • 判断条件:至少 1 次,即1-N次。当第一次判断不成立的时候,循环结束,所以只执行一次;如果成立,则在下次循环开始还会进行判断。
  • 控制条件:0-N次。0次,是指第一次判断条件就不成立。
  • 循环体:0-N次。0次,同上。

做个练习试试手。

需求:使用 for 循环在控制台输出1-5

分析:for 循环对你来说还是个新知识,要使用它,首先搞明白几件事:

  1. 我们要做的事是什么,即循环体是什么:打印从1到5的数字。

    1. 我们要做的事有什么规律,即循环体要执行几次每次执行之间的联系:执行5次,打印数字每次递增1。
    2. 如何决定循环体是否执行,即判断条件成立的根据是什么:判断执行的次数,最大不能超过5次。
    3. 如何控制判断条件成立与否,即控制条件如何修改判断条件的数据:控制次数,逐次递增1.

好了,根据上面的分析和 for 循环的基本格式可以得出实现这个需求的步骤:

  1. 定义整型变量 number,表示要打印的数字,初始值是1,最大值是5,每打印一次之后都需要加1:

    int number= 1;

  2. 定义整型变量 time,表示循环体执行的次数,作为 for 循环的初始化语句,初始值是1:

    int time = 1;

  3. 循环最多执行5次,所以变量 time 的最大值是5,即 for 循环的判断条件:

    time <= 5;

  4. 每打印一次数字,次数都需要加1,所以,for 循环的控制条件:

    time++

  5. 在循环体中打印数字,然后让数字加1:

    System.out.println(number);

    number++;

完整代码如下:

public class Test{
    public static void main(String[] args) {
        // 1.要打印的数字,初始值是1,最大值是5,每打印一次之后都需要加1
        int number = 1; 
        /*
           2.定义整型变量 time,表示循环体执行的次数,作为 for 循环的初始化语句,初始值是1
           3.循环最多执行5次,所以变量 time 的最大值是5,即 for 循环的判断条件:time <= 5
           4.每打印一次数字,次数都需要加1,所以,for 循环的控制条件:time++
        */
        for (int time = 1; time <= 5; time++) {
            // 5.在循环体中打印数字,
            System.out.println(number);
            number++; // 然后让数字加1
        }
    }
}

输出结果:

1
2
3
4
5

数字已经正确输出,完全符合需求。

聪明的你可能已经发现,变量 timenumber 的初始化、变化规律完全一样!没错。那能不能优化一下代码,只使用一个变量呢?答案是肯定的:

public class Test{    
    public static void main(String[] args) {
        for (int number = 1; number <= 5; number++) {
            System.out.println(number);
        }
    }
}

这个时候,number 变量既代表打印的次数,又代表每次打印的数字,一举两得。学习的过程不仅要弄明白知识的来龙去脉,还要有自己的思考。代码看起来很简单,每个组成部分的来源和意义才是关键。当你能够用自己的方式把知识之间的关系联结起来,你才算是学会了。

考一考:for 循环的使用

1.需求:使用 for 循环输出1-5之和

分析:

  1. 定义一个变量 sum,代表1到5的和,初始化值是0
  2. for 循环获取1-5的数据
  3. 把每一次获取到的数据累加到变量 sum

    sum = sum + x; 或者 sum += x;

  4. 循环结束,输出变量 sum 的值

循环结构,了解Java爱的魔力转圈圈_第3张图片

答案:

public class Test{    
    public static void main(String[] args) {
        // 1. 定义求和变量sum.
        int sum = 0;
        // 2. 通过for循环获取1~5之间的数据.
        for (int i = 1; i <=5; i++) { // i记录的就是: 1~5之间的数字
            // 3. 把获取到的数据依次累加给变量sum
            sum += i; // sum = sum + i;
        }
        // 4. 打印结果
        System.out.println("sum = " + sum);
    }
}

2.需求:求出1-100之间偶数和

分析:

  1. 定义一个求和变量 sum,初始化值是0
  2. 获取1-100之间的数,用 for 循环实现
  3. 判断每一个数是否为偶数,是就累加,否则不做操作

    对2取余等于0,则为偶数: x % 2 == 0

  4. for 循环结束,输出求和变量 sum 的值

循环结构,了解Java爱的魔力转圈圈_第4张图片

答案:

public class Test{
    public static void main(String[] args) {
        // 1. 定义一个求和变量sum
        int sum = 0;
        // 2. 获取1~100之间所有的数据
        for (int i = 1; i <= 100; i++) { // i的值其实就是1~100之间的数字, 只要判断i是否是偶数即可
            // 3. 判断当前获取到的数据是否是偶数, 是就累加
            if(i % 2 == 0) {
                // 能走到这里, 说明i是偶数, 累加即可
                sum += i;
            }
        }
        // 4. 打印结果
        System.out.println("sum: " + sum);
    }
}

3.需求:在控制台输出所有的”水仙花数”

分析:

​ 水仙花数:所谓的水仙花数是指一个三位数,其各位数字的立方和等于该数本身
​ 举例:153是一个水仙花数:1 1 1 + 5 5 5 + 3 3 3 = 1 + 125 + 27 = 153

步骤:

  1. 获取所有的三位数,即100-1000之间的数
  2. 获取每一个三位数的个位,十位,百位
    ​ 个位:153 % 10 = 3
    ​ 十位:153/10%10 = 5
    ​ 百位:153/10/10%10 = 1
  3. 拿个位,十位,百位的立方和与该数本身进行比较,如果相等,则在控制台打印该数

循环结构,了解Java爱的魔力转圈圈_第5张图片

答案:

public class Test{
    public static void main(String[] args) {
        // 1. 通过for循环, 获取所有的三位数.
        for (int i = 100; i < 1000; i++) { // i表示的就是所有的三位数
            // 2. 获取该数据的个位, 十位, 百位数字.
            int ge = i % 10;
            int shi = i / 10 % 10;
            int bai = i / 10 / 10 % 10;

            // 3. 判断该数字是否是水仙花数, 如果是, 直接打印即可
            if (ge * ge * ge + shi * shi * shi + bai * bai * bai == i) {
                // 能走到这里, 说明i是水仙花数
                System.out.println(i);
            } // 不是水仙花数,不做任何操作
        }
    }
}
public class Test{
    public static void main(String[] args) {
        // 1. 通过for循环, 获取所有的三位数.
        
            // 2. 获取该数据的个位, 十位, 百位数字        

            // 3. 判断该数字是否是水仙花数, 如果是, 直接打印即可
            
    }
}

4.需求:统计所有的”水仙花数”的个数

分析:

  1. 定义计数器变量 count,初始化值为0
  2. 使用 for 循环获取所有的三位数,即100-1000之间的数
  3. 判断每一个三位数是否为水仙花数,是则 count 自增1
    count ++;
  4. 循环结束,输出计数器 count 的值

答案:

public class Test{
    public static void main(String[] args) {
        // 1. 定义一个计数器, 用来记录水仙花数的个数
        int count = 0;
        // 2. 获取到所有的三位数
        for (int i = 100; i < 1000; i++) { // i记录的就是所有的三位数
            // 3. 获取到该数字的个位, 十位, 百位数字.
            int ge = i % 10;
            int shi = i / 10 % 10;
            int bai = i / 10 / 10 % 10;

            // 4. 判断该数字是否是水仙花数, 如果是, 计数器自增1.
            if (ge * ge * ge + shi * shi * shi + bai * bai * bai == i) {
                // 能走到这里, 说明i是一个水仙花数
                ++count;
            }
        }
        // 5. 打印计数器的结果即可
        System.out.println("水仙花数的个数是: " + count);
    }
}
public class Test{
    public static void main(String[] args) {
        // 1. 定义一个计数器, 用来记录水仙花数的个数
        
        // 2. 获取到所有的三位数
        
            // 3. 获取到该数字的个位, 十位, 百位数字.
            
            // 4. 判断该数字是否是水仙花数, 如果是, 计数器自增1.
            
        // 5. 打印计数器的结果即可
        
    }
}

1.2 while循环

从语法的角度上讲,所有的 for 循环都可以用 while 循环改写。只不过两种循环的格式不同,适用场景有所差别。来看下 while 循环的格式:

    初始化语句;
    while (判断条件) {
         循环体;
         控制条件;
    }

while 循环的初始化语句控制条件不像 for 循环那样在小括号里面,这是二者最大的不同。按照这种格式,可以很容易的把 for 循环的代码改写成 while格式(打印1-5的数字):

public class Test{
    public static void main(String[] args) {
        int number = 1;    // 初始化语句
        while (number <= 5) { // 判断条件
            System.out.println(number); // 循环体
            number++; // 控制条件
        }
    }
}

输出结果没什么不同:

1
2
3
4
5

while 循环的执行流程和 for 循环几乎完全一样,只不过由于初始化语句的位置不同,while 循环的初始化语句优先执行之后,才真正进入 while 循环的地界,后续的执行流程与 for 循环完全一致,贴个图了事,这里就不赘述了:

循环结构,了解Java爱的魔力转圈圈_第6张图片

你可能会说,既然两种循环结构任何时候都可以互换,而且执行流程也一样,为啥还弄俩,一个不就行了吗?虽然看起来是这样,但是你有没有注意到,while 循环的初始化语句在整个循环结构的外面,由此看来,它并非是 while 循环必不可少的一部分;同样的,控制条件在循环体的内部,换句话说,它也可以认为是循环体的一部分啊!照这么说,while 循环的初始化语句和控制条件语句都是可以省略的。换句话说,while 循环相当于 for 循环的简洁形式。

“哦,原来如此。但是彭彭,有个Bug。”

“说来听听。”

“你不是说 for 循环和 while 循环任何时候都可以互换吗?”

“是的。”

“省略了初始化语句和控制条件的 while 循环,怎么转成 for 循环?”

“聪明。小伙子反应还挺快。”

其实在 for 循环声明格式里,小括号里面的三条语句都可以省略——但是两个分号(;)不能省。如果省略了三条语句,整个 for 循环看起来光秃秃的,像没穿衣服的小屁孩(是不是想到了小时候的自己):

     for ( ; ; ) { // 死循环
        // 循环体
    }

这种格式的 for 循环是一种最简单的"死循环"——本节最后介绍。省略了初始化语句和控制条件的 while 循环,只需要把判断条件放到 for 循环对应的位置,就改写成了 for 循环:

     for ( ; 判断条件; ) {
        // 循环体
    }

对比 for 循环,while 循环相当于简写形式,也就是说,当我们不需要“初始化条件”和/或“控制条件”的时候,就可以使用 while 循环,因为它的格式更加简洁。

好了,现在交给你个任务,把前面介绍的所有 for 循环的代码改成 while 循环的格式。

1.3 do…while循环

老夫子教导我们:学而时习之。学完一个知识,要多练习。刻意练习才能够熟能生巧,如果练一遍不行,那就练两遍、三遍,但是,至少要练一遍,直到学会为止。否则,知识仅仅在脑海中浮光掠影般过了一遍,没有留下深刻的印象,就不算完成了学习。多次练习其实就是重复执行,我们可以用循环来实现,但是如何确保“至少练一次”呢?

Java 语言提供了一种特殊的循环结构:do...while 循环,这个结构的特殊之处在于,它会让循环体先执行(do)一遍,然后去判断条件是否满足,再根据实际需要决定是否继续循环。这种格式的循环完美适用上面的需求。

来看看神奇的 do…while 循环语句的格式:

    初始化语句;
    do {
         循环体;
         控制条件;
    } while (判断条件); 

注意 while 小括号最后的分号不可省略。与 while 循环相同的地方是,初始化语句在整个循环结构的上面,控制条件依然在循环体的最后;不同的地方在于,循环体和判断条件的位置发生了调换,这也就意味着,先执行循环体和控制条件,再执行判断条件。

它的执行流程是这样的:

循环结构,了解Java爱的魔力转圈圈_第7张图片

  1. 先执行初始化语句。这一步与 while 循环完全一样。
  2. 执行循环体。
  3. 执行控制条件。
  4. 执行判断条件:

    条件成立,则再一次执行循环体、控制条件等内容

    条件不成立,循环终止。

由于 do...while 循环的判断条件放在最后,所以它的循环体部分必然能执行,或者说,do...while 循环的循环体至少执行一次,这是它与前两个循环最大的不同。

回到前面提到的需求:如何用 do…while 循环实现“学完一个知识,至少练习1次”?

分析:假设练习三次之后一定能学会,那么,do...while 循环的各个部分如下:

​ 1. 初始化条件:定义 int 型变量 count,代表练习的次数,初始化值为 1

​ 2. 判断条件:定义 boolean 型变量 isOK,作为一个标记,代表是否学会,默认值为 false

​ 3. 循环体:

​ 判断当练习次数小于等于3时,打印正在练习的次数

​ 每练习一次,次数加1:count ++

  1. 控制条件:当次数大于3,表示已学会:给标记重新赋值:isOK = true

示例代码:

public class Test{
    public static void main(String[] args) {
        // 1. 定义一个变量, 记录练习次数
        int count = 1;
        // 2. 定义一个变量, 用来标记是否学会这个知识点. true: 学会了, false: 没学会
        boolean isOK = false;
        do {
            if (count <= 3) { // 3. 判断当练习次数小于等于3时,
                System.out.println("正在进行第" + count + "次练习"); //  打印正在练习的次数
                count++; // 每练习一次, 次数要+1
            } else { // 4. 当次数大于3,表示已学会
                isOK = true; // 将boolean类型变量的值改为: true
            }
        } while (!isOK); // 判断标记不为 false,即没学会的时候,循环执行
    }
}

输出结果:

正在进行第1次练习
正在进行第2次练习
正在进行第3次练习

这就是 do...while 循环的基本使用。

对比这三种循环:从格式上看,while 循环相当于 for 循环的简化格式,要知道,这两种循环任何时候都可以相互替换。之所以会出现两个不同格式,是因为有些场景的循环操作并不需要初始化条件,对控制条件的需要也没那么强烈,简化版循环 while 就显得结构更清晰,可读性更强。对于 do...while 循环来说,它在格式上类似 while 循环,只不过二者循环体和判断条件的位置正好相反,也是这个原因,使得 do...while 循环的循环体部分先于判断条件执行,即至少执行一次。

综上所述,一般情况下,我们尽可能使用 for 循环,如果不关注循环的初始化条件,可以使用简化版的 while 循环,当你的需求必须要执行一次循环体的时候,使用 do...while 循环。

到此为止,这三种循环的基本使用介绍完了,希望你多练习案例代码,尝试相互转换三种循环的代码。

第二关 循环终止:不带走一片云彩

2.1 break

如果对一组数据的每一条都要进行处理,那么遍历所有的数据是必要的;但如果是为了查找一组数据中的某一个,就不总是循环所有的次数,因为可能在任何一次循环的中间找到需要的数据。这个时候,对后续数据的遍历就不再必要了。为了提高性能,同时也为了节省时间,找到需要的数据之后就终止循环才是最合适的做法。

那么,如何在循环执行的过程中去终止它呢?

答案是:使用 break; 语句。

你应该对 break; 语句不陌生了吧,因为在《选择结构》课程中介绍的 switch 结构中,就是通过 break; 语句来结束的,同样的,在循环结构中,也可以使用它来结束。用法非常简单,在循环体中的任何一个位置,只要你认为可以结束循环,那么就可以把这个语句放到这里。

举个栗子演示一下。

比如现在有这样一个需求:班级中有15位同学,请查找编号为3的同学。

分析:查找的过程是一个重复操作,使用 for 循环来实现。我们假设班级中同学们的编号是1-15,那么遍历每个同学,查看他们的编号,如果为3,就终止循环。如果不是,就继续查找,直到找到3号同学为止。由此,循环的各个组成部分:

​ 1.初始化语句:定义同学的编号,从1开始,最大15

int number = 1;

​ 2.判断条件:遍历每个同学的编号,最大到15结束,所以编号的范围是1-15

number <= 15;

​ 3.控制条件:每遍历一位同学,编号就加1

number++

​ 4.循环体:

​ 判断同学编号是否为3
​ 若该同学编号为3,则打印该同学编号,结束循环
​ 若该同学编号不为3,不做任何操作

示例代码:

public class Test{
    public static void main(String[] args) {
        // 遍历编号为1-15的所有同学
        for (int number = 1; number <= 15; number++) {
            if (number == 3) { // 如果某位同学的编号为3
                System.out.println("找到了编号为3的同学"); // 打印信息
                break; // 结束循环
            } // 否则,不作任何操作,让循环继续,进行下一次查找
        }
    }
}

代码非常简单。我们在循环体里加入一个 if 判断语句:当编号为3时,打印同学信息,然后使用 break; 语句结束循环,否则,不做任何操作,让循环继续,进行下一次查找,直到找到为止。变量 number 的值从1开始,那么肯定是到第三次循环的时候找到了对应的同学,然后整个循环被 break; 语句终止了。

是不是很简单!

2.2 continue

另一种情况,在遍历一组数据的时候,并不是要取出那些特殊的数据,而是跳过它们,因为它们可能是非法的、或者是因为测试而填入的数据。那么,怎么在循环执行的过程中跳过这个数据呢?

答案是:使用 continue; 语句。

再举个栗子。

需求:一起来玩逢7必过小游戏。游戏规则:多人围坐在一起,依次快速说出从1-100的数字,所有含7、7的倍数的数不能说,否则就失败受到惩罚。

分析:

    1.同样,使用for循环遍历1-100的数,

​ 2.然后在循环体中,判断当前遍历的数中是否含7、或是否为7的倍数

​ 是否含7,包括个位是7和十位是7两种情况:

​ 个位是7:对10取模,余数为7:number % 10 == 7

​ 十位是7:除以10,商为7:number / 10 == 7

​ 是否为7的倍数:对7取模,余数为0:number % 7 == 0

​ 3.跳过所有符合上述要求的数:continue;

    4.打印其它数。打印效果:

循环结构,了解Java爱的魔力转圈圈_第8张图片

示例代码:

public class Test{
    public static void main(String[] args) {
        // 1. 通过for循环获取到1~100之间所有的数据
        for (int number = 1; number <= 100; number++) {
            // 2. 包含7或者是7的倍数, 这些数据都要跳过
            if (number % 10 == 7 || number / 10 == 7 || number % 7 == 0) {
                // 3. 符合要求,直接跳过当前数(跳过本次循环)
                continue;
            }
            // 4. 如果数据合法, 直接打印即可
            System.out.println(number);
        }
    }
}

程序运行过程中,如果通过了 if 语句的判断,就会执行 continue; 语句,它的作用是:跳过本次循环,进行下次循环。

break; 语句和 continue; 语句都有“结束”循环的意思,但它们有明显的区别:

break:结束所在循环的遍历操作。它终止了整个循环,不再进行循环遍历操作。

continue:跳过本次循环,继续下次循环。它终止了整个循环操作的某一次,然后继续下一次循环操作。

2.3 死循环

探索人生的道路上,我们不知道失败了多少次,而且,在不远的未来,我们可能还会再一次遭遇失败。但是——前面都是废话,无论失败多少次,我们依然坚持尝试,不管需要坚持多久,我们都不会放弃,直到找到成功出路的那天。

说的我自己都快信了。

虽然我们的人生方向并不迷茫,但我们并不明确的知道获得成功的具体日期,所以,我们不得不一次又一次的尝试,不知道还需多少次,何时才能破局而出,升职加薪、走上人生癫疯。其实,我是想说,有些时候,重复的次数是无法预知的,而前面介绍的循环,都明确知道循环的最大次数。在不知道应该重复执行多少次的时候,如何用循环来实现、又如何终止循环呢?

答案:用死循环来实现,然后在满足某种特殊条件时,使用 break; 语句进行终止。

“死循环”的意思,是指循环本身不会自动终止,而是需要依靠其它的条件才能结束。前面介绍的循环,最终都会因为判断条件不成立而终止,可死循环的判断条件永远都是成立的。三种简单的死循环格式如下:

循环结构,了解Java爱的魔力转圈圈_第9张图片

通过上图可以看出,for 循环的死循环格式,要求判断条件为空,或者恒成立,初始化语句和控制条件并没有作任何要求;while 循环和 do...while 循环的判断条件都是 boolean 类型的常量:true,这样也能保证判断条件的恒成立。这就是三种循环的简单死循环格式。

我们并不会容忍死循环永远的执行下去,只不过由于不知道这种循环具体的执行次数,才不得不使用这种格式。所以,一般情况下,我们都会在循环体内部设置某些条件,当条件满足就让循环终止,而不是任由它浪费CPU的资源。

举个栗子。

需求:假设纸张厚度为 0.001米,喜马拉雅山高度为8848米。请问:需要将纸张对折多少次,才能达到喜马拉雅山的高度?

分析:由于不知道循环多少次,用死循环。就使用格式简洁的 while 循环,你也可以改成另两种格式。每次循环让厚度加一倍,即厚度乘以2,这里的厚度是在上一次折叠之后的基础上,所以是累乘的效果。

  1. 定义 int 变量 count 作为折叠计数器,代表需要折叠的次数,初始值为0,每折叠一次加1
  2. 定义 double 变量 thicknessheight,分别代表纸张厚度和喜马拉雅山高度,初始值分别是0.001和8848
  3. 循环体:

    判断当前纸张厚度是否达到山的高度:thickness >= height

    如果已达到,即判断条件返回 true,终止循环:break;

    否则,

    ​ 进行纸张的折叠:thickness *= 2;

    ​ 每折叠一次,让计数器加1:count++;

  4. 循环结束,打印折叠计数器的值

示例代码:

public class Test{
    public static void main(String[] args) {
        // 1. 定义折叠次数,每次加1
        int count = 0; 
        // 2. 定义纸张厚度和喜马拉雅山高度
        double thickness = 0.001; // 纸张厚度
        double height = 8848; // 喜马拉雅山高度

        while (true) { // 不知道循环多少次,用死循环
            // 3. 判断条件,当前纸张厚度是否达到山的高度
            if (thickness >= height) {
                break; // 如果已达到,则终止循环
            }
            // 否则。如果 if 条件成立,必然跳出循环,if 语句体里的代码不会与这里的代码同时执行。
            thickness *= 2; // 进行纸张的折叠
            count++; // 计数器加1
        }
        // 4. 循环结束,打印折叠计数器的值
        System.out.println(count);
    }
}

输出结果:

24

需要折叠24次,不信你可以试试。

如果死循环使用不当,比如你设置的结束条件始终无法达成,那么这个循环体代码可能会一直执行下去,从而导致CPU资源耗尽,所以,使用死循环一定要慎重,确保使它跳出的条件一定能够执行到,否则,等待你的将是......,你自己品。

期待你破局而出的日子。

第三关 标号:循环跳转的骚走位

3.1 循环嵌套

遍历一组数据对你来说没有什么挑战性,现在,增加一点难度:遍历多组数据。

需求:假设现在有3个班级,每个班级15名同学,如何获取所有班级中的同学呢?

这个需求里有两组数据:一、3个班级;二、每个班级15名同学。如果要找到所有的同学,可以一个班一个班地找,每找一个班,再逐个把该班级所有同学找出来就行了。看起来逻辑并不复杂,该怎么实现?

分析:通过观察可知,班级和同学这两组数据之间有一定的关系:班级包含同学。遍历每个班级需要用到循环,同样,遍历每个同学也需要用到循环,莫非循环之间也可以有包含关系吗?是的,我们可以在一个循环体语句中包含另一个循环语句,这种情况称为循环嵌套。班级循环称为外层循环,被包含的同学循环称为内层循环。我们先来尝试把外层循环实现出来:

  1. 初始化条件:定义 int 型变量 classNumber,初始值为1,最大值为3;

    1. 判断条件:classNumber <= 3;
    2. 循环体:打印班级编号
    3. 控制条件:classNumber++;

示例代码:

public class Test{
    public static void main(String[] args) {
        for (int classNumber = 1; classNumber <= 3; classNumber++) { // 外层循环,遍历每个班级
            System.out.println(classNumber); // 打印班级编号
        }
    }
}

输出:

1
2
3

现在,我们再把内存循环实现出来,也就是把一个班级的同学编号打印出来:

 1. 初始化条件:定义 `int` 型变量 `studentNumber`,初始值为1,最大值为15;
 2. 判断条件:`studentNumber <= 3;`
 3. 循环体:打印同学编号
 4. 控制条件:`studentNumber++;`

示例代码:

public class Test{
    public static void main(String[] args) {
        for (int studentNumber = 1; studentNumber <= 15; studentNumber++) { // 内层循环,遍历每个同学
            System.out.println(studentNumber); // 打印同学编号
        }
    }
}

运行输出:

1
2
3
...
14
15

好了,两层循环都完成了,怎么把它们包含起来呢?

其实非常简单,我们的要求是:在遍历每个班级的时候去查找该班级的所有同学,所以,内存循环——即遍历同学的循环,其实是外层循环的一部分,即循环体:

public class Test{
    public static void main(String[] args) {
        for (int classNumber = 1; classNumber <= 3; classNumber++) { // 外层循环,遍历每个班级
            for (int studentNumber = 1; studentNumber <= 15; studentNumber++) { // 内层循环,遍历每个同学
                System.out.println("正在获取的第" + classNumber + "个班级的第" + studentNumber + "位同学"); // 打印同学编号
            }
        }
    }
}

输出:

正在获取的第1个班级的第1位同学
正在获取的第1个班级的第2位同学
正在获取的第1个班级的第3位同学
...
正在获取的第2个班级的第1位同学
正在获取的第2个班级的第2位同学
...
正在获取的第2个班级的第14位同学
正在获取的第2个班级的第15位同学
正在获取的第3个班级的第1位同学
正在获取的第3个班级的第2位同学
正在获取的第3个班级的第3位同学
...
正在获取的第3个班级的第13位同学
正在获取的第3个班级的第14位同学
正在获取的第3个班级的第15位同学

就这样,我们通过循环嵌套完成了3个班级共45名同学的查找。

循环嵌套并没有想像中的那么复杂,在设计双层循环结构的时候,首先要想清楚每层循环所代表的含义,然后分别专注于每层循环代码的实现就可以了。你只需要把内层循环作为外层循环的循环体,在编写内层循环代码的时候,就把它当作普通的循环,与外层循环无关,剥离外层循环的干扰,专注于内层循环的实现。还要注意一个细节,两层循环使用的变量名不要重复,要不然很容易混乱,各自独立是最好的。

3.2 标号

稍微改一下需求:A公司邀请程旭元同学加入,旭元同学还在班级里埋头狂敲代码,现按班级查找程旭元同学。有3个班级,每班15个同学,假设第3个班级的第10位同学名叫程旭元,找到该同学后则停止查找。怎么实现?

肯定还是要使用双层嵌套循环,外层循环和内层循环的含义没变,问题是,查找同学的操作在内层循环,找到程旭元同学后怎么让两层循环同时终止?我们知道结束单层循环的方式:break; 语句,却没有办法结束双层循环,况且是外层循环。

这的确是个难题。

但是难不倒聪明的彭彭。 break; 语句默认情况下的作用是结束当前循环,如果给它一种能力,让它可以结束外层循环不就行了嘛!

怎么做呢?可以给外层循环加一个名字,同时,在执行 break; 语句的时候把这个名字带上,意思是“结束指定名字的循环”。这种名字叫“标号”,即循环的名称。我们给循环定义一个标号,就可以根据需要结束或跳转到指定循环,它的定义格式是这样的:

    标号: for () {}    // 标号的定义,直接在循环前写标识符。while 和 do…while 举例略

或者这样,让标号独占一行:

    标号:
    for () {}         // 标号的定义,直接在循环前写标识符。while 和 do…while 举例略

使用的时候,只需要在 break 关键字后面跟上标号就可以了,就像这样:

    break 标号;        // 结束名称为指定标号的循环

或者这样,使用 continue 关键字加标号,意思是跳转到指定标号的循环继续执行:

    continue 标号;    // 跳转到名称为指定标号的循环继续执行

标号是一种标识符,定义的时候必须符合标识符的命名规则。但它不是变量,不需要前面加数据类型。标号只能用于多层嵌套循环中,不能在同级别的循环之间使用哦,那样的话,会让代码的执行顺序乱套的!

现在,我们就可以开始查找程旭元同学了。

分析:

​ 1. 先使用 for 循环遍历每一个班级,定义标号:

label_class: for () { }

​ 2. 在班级循环体中,再使用 for 循环遍历每个同学

​ 3. 判断:如果班级编号为3,同学编号为10,则停止查找:break label_class;

示例代码:

public class Test{
    public static void main(String[] args) {
        label_class: // 外层循环标号
        //     1. 先使用 for 循环遍历每一个班级
        for (int classNumber = 1; classNumber <= 3; classNumber++) {
            // 2. 在班级循环体中,再使用 for 循环遍历每个同学
            for (int studentNumber = 1; studentNumber <= 15; studentNumber++) {
                // 打印正在查找的同学编号
                System.out.println("正在查找的第" + classNumber + "个班级的第" + studentNumber + "位同学"); 
                // 3. 判断:如果班级编号为3,同学编号为10,则停止查找
                if (classNumber == 3 && studentNumber == 10) {
                    System.out.println("哈哈, 找到程旭元同学了, 整个循环结束");
                    break label_class; // 停止查找:结束指定标号的循环
                }
            }
        }
    }
}

输出:

正在查找的第1个班级的第1位同学
正在查找的第1个班级的第2位同学
正在查找的第1个班级的第3位同学
...
正在查找的第1个班级的第15位同学
正在查找的第2个班级的第1位同学
正在查找的第2个班级的第2位同学
...
正在查找的第2个班级的第14位同学
正在查找的第2个班级的第15位同学
正在查找的第3个班级的第1位同学
...
正在查找的第3个班级的第9位同学
正在查找的第3个班级的第10位同学
哈哈, 找到程旭元同学了, 整个循环结束

使用 break 标号; 语句结束指定名称的循环非常方便。此时,标号为 label_class 的外层循环被强制结束,那内层循环呢,还执行吗?很明显,内层循环也结束了,因为内层循环的代码不可能脱离外层循环独立执行啊。

再看另一种情况,什么样的场景下会使用 continue 标号; 语句。

需求:按批次检测商品的次品量。现有3个批次,每个批次有10件商品,如果某批次商品中包含任意一个次品,则该批次商品不合格,跳过该批次剩余商品的检测并记录,继续下个批次。假设查找到第2个批次的第5件商品为次品。

分析:

  1. 先使用 for 循环遍历每一个批次,定义标号:
    label_batch: for () { }

    1. 在批次循环体中,再使用 for 循环遍历每个商品
    2. 判断如果批次编号为2,商品编号为5,则结束当前批次的检测,继续下个批次:
      continue label_batch;

示例代码:

public class Test{
    public static void main(String[] args) {
        label_batch: // 外层循环标号
        // 1. 先使用 for 循环遍历每一个批次
        for (int batchNumber = 1; batchNumber <= 3; batchNumber++) {
            // 2. 在批次循环体中,再使用 for 循环遍历每个商品
            for (int goodsNumber = 1; goodsNumber <= 10; goodsNumber++) {
                // 打印正在检测的商品编号
                System.out.println("正在检测第" + batchNumber + "个批次的第" + goodsNumber + "件商品");
                // 3. 判断:如果批次编号为2,商品编号为5,则停止查找
                if (batchNumber == 2 && goodsNumber == 5) {
                    System.err.println("记录:第" + batchNumber + "个批次的第" + goodsNumber + "件商品是次品");
                    continue label_batch; // 停止检测当前批次,继续下个批次:继续指定标号的循环
                }
            }
        }
    }
}

输出结果:

正在检测第1个批次的第1件商品
正在检测第1个批次的第2件商品
...
正在检测第2个批次的第4件商品
正在检测第2个批次的第5件商品
记录:第2个批次的第5件商品是次品
正在检测第3个批次的第1件商品
正在检测第3个批次的第2件商品
...
正在检测第3个批次的第9件商品
正在检测第3个批次的第10件商品

一定要注意的是,break 标号;continue 标号; 两个语句含义的不同:break 代表“结束全部”,而 continue 代表“结束部分,然后继续”。仔细区分它们的含义,才能正确地使用它们。

3.3 循环案例:1024程序员节,小黑带你发橙子

3.3.1 需求:

    *1024程序员节*,是传智播客发起的中国程序员共同的节日。每到10月24日,小黑都会按班级给每位同学发橙子。假设有3个班级,每个班级有35个同学,现在要将100个橙子分别发放给每位同学,每人只能拿一个。
   
      条件:如果该同学已经有了橙子,则不再发给该同学;如果橙子发完了,则发放活动终止。

3.3.2 分析:

    A:模拟发橙子的过程:循环每一个班级,然后遍历班级的每个同学,所以需要双层循环

​ B:假设编号为5的倍数的同学都已经有了橙子,则发放到该同学时,使用 continue 语句结束该次循环

​ C:橙子的数量为0时,使用 break + 标号; 语句结束外层循环,发放活动终止。

3.3.3 步骤:

​ A:实现给每位同学发橙子的功能

​ B:添加判断条件:跳过编号为5的倍数的同学

​ C:添加判断条件:橙子数目为0,则终止发放

3.3.4 技术点:

​ 循环嵌套
​ break
​ continue
​ 标号

循环结构,了解Java爱的魔力转圈圈_第10张图片

参考代码:

public class Test{
    public static void main(String[] args) {
        int orangeCount = 100; // 橙子总数100个
        label_class: // 外层循环标号:班级循环
        //     1. 使用 for 循环遍历每一个班级
        for (int classNumber = 1; classNumber <= 3; classNumber++) {
            // 2. 在班级循环体中,再使用 for 循环遍历每个同学
            for (int studentNumber = 1; studentNumber <= 35; studentNumber++) {
                if (orangeCount <= 0) { // 检查橙子数量是否足够,如果橙子数量不够了
                    break label_class; // 结束发橙子的活动:结束外层循环
                }
                if (studentNumber % 5 == 0) { // 检查当前同学编号:编号为5的倍数,该同学已经有了橙子
                    continue; // 跳过当前同学
                }
                // 打印得到橙子的同学编号
                System.out.println("正在给第" + classNumber + "个班级的第" + studentNumber + "位同学发橙子"); 
                orangeCount--; // 橙子数量减1
            }
        }
        // 活动结束,打印发出的橙子数量
        System.out.println("总共发出了" + (100 - orangeCount) + "个橙子");
    }
}

课程总结

  1. 三种循环的基本格式和执行流程(参考上文中图):

         for (初始化语句; 判断条件; 控制条件) {
            循环体
        }
        初始化语句;
        while (判断条件) {
            循环体;
            控制条件;
        }
        初始化语句;
        do {
            循环体;
            控制条件;
        } while (判断条件); 
  2. 循环的终止:

    break:结束当前循环

    continue:结束本次循环,继续下次循环

    循环嵌套:多组数据之间有包含关系时,使用循环嵌套格式:每层循环所代表的含义;变量名重复问题;

    标号:循环的名字,配合 breakcontinue 语句使用。

你可能感兴趣的:(java程序员后端前端)