《Java 编程的逻辑》笔记——第1章 编程基础(一)

声明:

本博客是本人在学习《Java 编程的逻辑》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

开头语

计算机就是个机器,这个机器主要由 CPU、内存、硬盘和输入输出设备组成。计算机上跑着操作系统,如 Windows 或 Linux,操作系统上运行着各种应用程序,如 Word, QQ 等。

操作系统将时间分成很多细小的时间片,一个时间片给一个程序用,另一个时间片给另一个程序用,并频繁地在程序间切换。不过,在应用程序看来,整个机器资源好像都归他使,操作系统给他提供了这种假象。 对程序员而言,我们写程序,基本不用考虑其他应用程序,我们想好怎么做自己的事就可以了。

应用程序看上去能做很多事情,能读写文档,能播放音乐,能聊天,能玩游戏,能下围棋 … 但本质上,计算机只会执行预先写好的指令而已,这些指令也只是操作数据或者设备。所谓程序,基本上就是告诉计算机要操作的数据和执行的指令序列, 即对什么数据做什么操作

比如说:

  • 读文档,就是将数据从磁盘加载到内存,然后输出到显示器上
  • 写文档,就是将数据从内存写回磁盘。
  • 播放音乐,就是将音乐的数据加载到内存,然后写到声卡上。
  • 聊天,就是从键盘接收聊天数据,放到内存,然后传给网卡,通过网络传给另一个人的网卡,再从网卡传到内存,显示在显示器上。

基本上,所有数据都需要放到内存进行处理,程序的很大一部分工作就是操作在内存中的数据

1.1 数据类型和变量

1.1.1 数据类型

数据类型用于对数据归类,方便理解和操作。对 Java 语言而言,有如下基本数据类型

扫描二维码关注公众号,回复: 11938900 查看本文章
  • 整数类型:有四种整型 byte/short/int/long,分别有不同的取值范围
  • 小数类型 :有两种类型 float/double,有不同的取值范围和精度
  • 字符类型:char,表示单个字符
  • 真假类型:boolean,表示真假

基本数据类型都有对应的数组类型,数组表示固定长度的同种数据类型的多条记录,这些数据在内存中挨在一起存放。 比如说,一个自然数可以用一个整数类型数据表示,100 个连续的自然数可以用一个长度为 100 的整数数组表示。一个字符用一个 char 表示,一段文字可以用一个 char 数组表示。

Java 是一个面向对象的语言,除了基本数据类型,其他都是对象类型。对象到底是什么呢?简单的说,对象是由基本数据类型、数组和其他对象组合而成的一个东西,以方便对其整体进行操作。

比如说,一个学生对象,可以由如下信息组成:

  • 姓名:一个字符数组
  • 年龄:一个整数
  • 性别:一个字符
  • 入学分数:一个小数

日期在 Java 中也是一个对象,内部表示为整形 long。

1.1.2 变量

为了操作数据,需要把数据存放到内存中,所谓内存在程序看来就是一块有地址编号的连续的空间。数据放到内存中的某个位置后,为了方便地找到和操作这个数据,需要给这个位置起一个名字。编程语言通过变量这个概念来表示这个过程。

声明一个变量,比如 int a 其实就是在内存中分配了一个空间,这个空间存放 int 数据类型,a 指向这个内存空间所在的位置。通过对 a 操作即可操作 a 指向的内存空间,比如 a=5 这个操作即可将 a 指向的内存空间的值改为 5。

之所以叫 “变” 量,是因为它表示的是内存中的位置,这个位置存放的值是可以变化的。

虽然变量的值是可以变化的,但名字是不变的,这个名字应该代表程序员心目中这块内存位置的意义,这个意义应该是不变的。比如说这个变量 int second 表示时钟秒数,在不同时间可以被赋予不同的值,但它表示的就是时钟秒数。之所以说应该是因为这不是必须的,如果你非要起一个变量名叫 age 但赋予它身高的值,计算机也拿你没办法。

1.2 赋值

声明变量之后,就在内存分配了一块位置,但这个位置的内容是未知的,赋值就是把这块位置的内容设为一个确定的值。Java 中基本类型、数组、对象的赋值有明显不同,本文介绍基本类型和数组的赋值,关于对象后续文章会详述。

1.2.1 基本类型

1.2.1.1 整数类型

整数类型有 byte, short, int 和 long,分别占用 1/2/4/8 个字节,取值范围如表 1-1 所示。

在这里插入图片描述

赋值形式很简单,直接把熟悉的数字常量形式赋值给变量即可,对应的内存空间的值就从未知变成了确定的常量。但常量不能超过对应类型的表示范围。例如:

byte b = 23;
short s = 3333;
int i = 9999;
long l = 32323;

但是,在给 long 类型赋值时,需要在常量后面加大写或小写的 L,例如:

long a = 3232343433L;

1.2.1.2 小数类型

小数类型有 float 和 double,占用的内存空间分别是 4 和 8 个字节,有不同的取值范围和精度,double 表示的范围更大,精度更高,具体如表 1-2 所示。

在这里插入图片描述

E 表示以 10 为底的指数,E 后面的 + 号和 - 号代表正指数和负指数,例如:1.4E-45 表示 1.4 乘以 10 的 -45 次方。后续文章会进一步分析小数的二进制表示。

对于 double,直接把熟悉的小数表示赋值给变量即可,例如:

double d = 333.33;

但对于 float,需要在数字后面加大写 F 或小写 f,例如:

float f = 333.33f;

除了小数,也可以把整数直接赋值给 float 或 double,例如:

float f = 33;
double d = 3333333333333L;

1.2.1.3 真假类型

这个很简单,直接使用 true 或 false 赋值,分别表示真和假,例如:

boolean b = true;
b = false;

1.2.1.4 字符类型

字符类型 char 用于表示一个字符,这个字符可以是中文字符,也可以是英文字符。

char 占用的内存空间是两个字节

赋值时把常量字符用单引号括起来,不要使用双引号,例如:

char c = 'A';
char z = '中';

1.2.1.5 一些说明

上面介绍的赋值都是直接给变量设置一个常量值。但也可以把变量赋给变量,例如:

int a = 100;
int b = a;

变量可以进行各种运算(后续文章讲解),也可以将变量的运算结果赋给变量,例如:

int a = 1;
int b = 2;
int c = 2*a+b; //2乘以a的值再加上b的值赋给c

上面介绍的赋值都是在声明变量的时候就进行了赋值,但这不是必须的,可以先声明变量,随后再进行赋值。

1.2.2 数组类型

1.2.2.1 赋值语法

基本类型的数组有 3 种赋值形式,如下所示:

1. int[] arr = {1,2,3};

2. int[] arr = new int[]{1,2,3};

3. int[] arr = new int[3];
   arr[0]=1; arr[1]=2; arr[2]=3;

第 1 种和第 2 种都是预先知道数组的内容,而第 3 种是先分配长度,然后再给每个元素赋值。

第 3 种形式中,即使没有给每个元素赋值,每个元素也都有一个默认值,这个默认值跟数组类型有关。数值类型的值为 0,boolean 为 false, char 为空字符。

数组长度可以动态确定,如下所示:

int length = ... ;//根据一些条件动态计算
int arr = new int[length];

虽然可以动态确定,但定了之后就不可以变。数组有一个 length 属性,但只能读,不能改。

一个小细节,不能在给定初始值的同时还给定长度,即如下格式是不允许的:

int[] arr = new int[3]{
    
    1,2,3}

这是可以理解的,因为初始值已经决定了长度,再给个长度,如果还不一致,计算机将无所适从。

1.2.2.2 数组和基本类型的区别

一个基本类型变量,内存中只会有一块对应的内存空间。但数组有两块,一块用于存储数组内容本身,另一块用于存储内容的位置

用一个例子来说明,有一个 int 变量 a,和一个 int 数组变量 arr,其代码,变量对应的内存地址和内存内容如表 1-3 所示:

在这里插入图片描述

基本类型 a 的内存地址是 1000,这个位置存储的就是它的值 100。

数组类型 arr 的内存地址是 2000,这个位置存储的值是一个位置 3000,3000 开始的位置存储的才是实际的数据 1,2,3。

1.2.2.3 为什么数组要用两块空间

不能只用一块空间吗?我们来看下面这个代码:

int[] arrA = {
    
    1,2,3};

int[] arrB = {
    
    4,5,6,7};
arrA = arrB;

这个代码中,arrA 初始的长度是 3,arrB 的长度是 4,后来将 arrB 的值赋给了 arrA。如果 arrA 对应的内存空间是直接存储的数组内容,那么它将没有足够的空间去容纳 arrB 的所有元素。

用两块空间存储,这个就简单的多,arrA 存储的值就变成了和 arrB 的一样,存储的都是数组内容 {4,5,6,7} 的地址,此后访问 arrA 就和 arrB 是一样的了,而 arrA {1,2,3} 的内存空间由于无人引用会被垃圾回收,如下所示:

arrA        {1,2,3} 

      \

        \

arrB  ->  {4,5,6,7}

由上,也可以看出,给数组变量赋值和给数组中元素赋值是两回事。给数组中元素赋值是改变数组内容,而给数组变量赋值则会让变量指向一个不同的位置。

上面我们说数组的长度是不可以变的,不可变指的是数组的内容空间,一经分配,长度就不能再变了,但是可以改变数组变量的值,让它指向一个长度不同的空间,就像上例中 arrA 后来指向了 arrB 一样。

1.3 基本运算

有了初始值之后,可以对数据进行运算。运算有不同的类型,不同的数据类型支持的运算也不一样,本文介绍 Java 中基本类型数据的主要运算。

  • 算术运算:主要是日常的加减乘除
  • 比较运算:主要是日常的大小比较
  • 逻辑运算:针对布尔值进行运算

1.3.1 算术运算

算术运算符有加减乘除,符号分别是 + - * /,另外还有取模运算符 %,以及自增(++)和自减(–)运算符。取模运算适用于整数和字符类型,其他算术运算适用于所有数值类型和字符类型,其他都符合常识,但字符类型看上去比较奇怪,后续文章解释。

减号(-)通常用于两个数相减, 但也可以放在一个数前面,例如 -a, 这表示改变 a 的符号,原来的正数会变为负数,原来的负数会变为正数,这也是符合我们常识的。

取模(%)就是数学中的求余数,例如,5%3 是 2,10%5 是 0。

自增(++)和自减(–),是一种快捷方式,是对自己进行加一或减一操作。

1.3.1.1 加减乘除注意事项

运算时要注意结果的范围,使用恰当的数据类型。两个正数都可以用 int 表示,但相乘的结果可能就会超,超出后结果会令人困惑,例如:

int a = 2147483647*2; //2147483647是int能表示的最大值

a 的结果是 -2。为什么是 -2 我们暂不解释,要避免这种情况,我们的结果类型应使用 long,但只改为 long 也是不够的,因为运算还是默认按照 int 类型进行,需要将至少一个数据表示为 long 形式,即在后面加 L 或 l,下面这样才会出现期望的结果:

long a = 2147483647*2L;

另外,需要注意的是,整数相除不是四舍五入,而是直接舍去小数位,例如:

double d = 10/4;

结果是 2 而不是 2.5,如果要按小数进行运算,需要将至少一个数表示为小数形式,或者使用强制类型转化,即在数字前面加 (double),表示将数字看做 double 类型,如下所示任意一种形式都可以:

double d = 10/4.0;
double d = 10/(double)4;

1.3.1.2 小数计算结果不精确

无论是使用 float 还是 double,进行运算时都会出现一些非常令人困惑的现象,比如:

float f = 0.1f*0.1f;
System.out.println(f);

这个结果看上去,不言而喻,应该是 0.01,但实际上,屏幕输出却是 0.010000001,后面多了个 1。换用 double看看:

double d = 0.1*0.1;
System.out.println(d);

屏幕输出 0.010000000000000002,一连串的 0 之后多了个 2,结果也不精确。

这是怎么回事?看上去这么简单的运算,计算机怎么能计算不精确呢?但事实就是这样,究其原因,我们需要理解 float 和 double 的二进制表示,后续文章进行分析。

1.3.1.3 自增(++)/自减(–)

自增/自减是对自己做加一和减一操作,但每个都有两种形式,一种是放在变量后,例如 a++, a–,另一种是放在变量前,例如 ++a, --a。

如果只是对自己操作,这两种形式也没什么差别,区别在于还有其他操作的时候。放在变量后(a++),是先用原来的值进行其他操作,然后再对自己做修改,而放在变量前(++a),是先对自己做修改,再用修改后的值进行其他操作。例如,快捷运算和其等同的运算如表 1-4 所示:

在这里插入图片描述

1.3.2 比较运算符

比较运算就是计算两个值之间的关系,结果是一个布尔类型(boolean)的值。比较运算适用于所有数值类型和字符类型。数值类型容易理解,但字符怎么比呢?后续文章解释。

比较操作符有:大于(>),大于等于(>=),小于(<),小于等于(<=),等于(==),不等于(!=)。

大部分也都是比较直观的,需要注意的是等于。

首先,它使用两个等号 ==,而不是一个等号(=),为什么不用一个等号呢?因为一个等号(=)已经被占了,表示赋值操作。

另外,对于数组,== 判断的是两个数组是不是同一个数组,而不是两个数组的元素内容是否一样。即使两个数组的内容是一样的,但如果是两个不同的数组,== 依然会返回 false,如下所示:

int[] a = new int[] {
    
    1,2,3};
int[] b = new int[] {
    
    1,2,3};

// a==b的结果是false

如果需要比较数组的内容是否一样,需要逐个比较里面存储的每个元素。

1.3.3 逻辑运算

逻辑运算根据数据的逻辑关系,生成一个布尔值 true 或者 false。逻辑运算只可应用于 boolean 类型的数据,但比较运算的结果是布尔值,所以其他类型数据的比较结果可进行逻辑运算。

逻辑运算符具体有:

  • 与(&):两个都为 true 才是 true,只要有一个是 false 就是 false
  • 或(|):只要有一个为 true 就是 true,都是 false 才是 false
  • 非(!):针对一个变量,true 会变成 false, false 会变成 true
  • 异或(^):两个相同为 false, 两个不相同为 true
  • 短路与(&&): 和 & 类似,不同之处马上解释
  • 短路或 (||):与 | 类似,不同之处马上解释

逻辑运算的大部分都是比较直观的,需要注意的是 & 和 &&,以及 | 和 || 的区别。如果只是进行逻辑运算,它们也都是相同的,区别在于同时有其他操作的情况下,例如:

boolean a = true;
int b = 0;
boolean flag = a | b++>0; 

因为 a 为 true,所以 flag 也为 true,但 b 的结果为 1,因为 | 后面的式子也会进行运算,即使只看 a 已经知道 flag 的结果,还是会进行后面的运算。而 || 则不同,如果最后一句的代码是:

boolean flag = a || b++>0; 

则 b 的值还是 0,因为 || 会"短路",即在看到 || 前面部分就可以判定结果的情况下,忽略 || 后面的运算。

1.3.4 运算符优先级

一个稍微复杂的运算可能会涉及多个变量,和多种运算,那哪个先算,哪个后算呢?程序语言规定了不同运算符的优先级,有的会先算,有的会后算,大部分情况下,这个优先级与我们的常识理解是相符的。

但在一些复杂情况下,我们可能会搞不明白其运算顺序。但这个我们不用太操心,可以使用括号 () 来表达我们想要的顺序,括号里的会先进行运算。简单的说,不确定顺序的时候,就使用括号

1.4 条件执行

为了编写有更多实用功能的程序,只进行基本操作是远远不够的,我们至少需要对操作的过程进行流程控制。流程控制主要有两种:一种是条件执行,另一种是循环执行,接下来的两节对它们进行详细的介绍。

流程控制中最基本的就是条件执行,也就是说,某些操作只能在某些条件满足的情况下才执行,在一些条件下执行某种操作,在另外一些条件下执行另外某种操作。这与交通控制中的红灯停、绿灯行条件执行是类似的。

Java 中表达这种流程控制的基本语法是 if 语句

1.4.1 语法和陷阱

1.4.1.1 if

if 的语法为:

if(条件语句){
    
    
  代码块
}

if(条件语句) 代码; 

它表达的含义也非常简单,只在条件语句为真的情况下,才执行后面的代码,为假就不做了。具体来说,条件语句必须为布尔值,可以是一个直接的布尔变量,也可以是变量运算后的结果。我们在 1.3 节介绍过,比较运算和逻辑运算的结果都是布尔值,所以可作为条件语句。

比如,只在变量为偶数的情况下输出:

int a=10;
if(a%2==0){
    
    
   System.out.println("偶数");
}

int a=10;
if(a%2==0) System.out.println("偶数");

if 的陷阱

初学者有时会忘记在 if 后面的代码块中加括号,有时希望执行多条语句而没有加括号,结果只会执行第一条语句。建议所有 if 后面都跟括号

1.4.1.2 if/else

if 实现的是条件满足的时候做什么操作,如果需要根据条件做分支,即满足的时候执行某种逻辑,而不满足的时候执行另一种逻辑,则可以用 if/else。

if/else 的语法是:

if(判断条件){
    
    
   代码块1
}else{
    
    
   代码块2
}

if/else 也非常简单,判断条件是一个布尔值,为 true 的时候执行代码块 1,为假的时候执行代码块 2

1.4.1.3 三元运算符

我们之前介绍了各种基本运算,这里介绍一个条件运算,和 if/else 很像,叫三元运算符,语法为:

判断条件 ? 表达式 1 : 表达式2

三元运算符会得到一个结果,判断条件为真的时候就返回表达式 1 的值,否则就返回表达式 2 的值。三元运算符经常用于对某个变量赋值,例如求两个数的最大值:

int max = x > y ? x : y;

三元运算符完全可以用 if/else 代替,但在某些场景下书写更简洁。

1.4.1.4 if/else if/else

如果有多个判断条件,而且需要根据这些判断条件的组合执行某些操作,则可以使用 if/else if/else。

if/else if/else 语法是:

if(条件1){
    
    
  代码块1
}else if(条件2){
    
    
  代码块2
}...
else if(条件n){
    
    
   代码块n
}else{
    
    
   代码块n+1
} 

if/else if/else 也比较简单,但可以表达复杂的条件执行逻辑,它逐个检查条件,条件 1 满足则执行代码块 1,不满足则检查条件 2,…,最后如果没有条件满足,且有 else 语句,则执行 else 里面的代码。最后的 else 语句不是必须的,没有就什么都不执行。

if/else if/else 的陷阱

需要注意的是,在 if/else if/else 中,判断的顺序是很重要的,后面的判断只有在前面的条件为 false 的时候才会执行。初学者有时会搞错这个顺序,如下面的代码:

if(score>60){
    
    
  return "及格";
}else if(score>80){
    
    
  return "良好";
}else{
    
    
  return "优秀"
}

看出问题了吧?如果 score 是 90,可能期望返回"优秀",但实际只会返回"及格"。

1.4.1.5 switch

在 if/else if/else 中,如果判断的条件基于的是同一个变量,只是根据变量值的不同而有不同的分支,如果值比较多,比如根据星期几进行判断,有 7 种可能性,或者根据英文字母进行判断,有26种可能性,使用 if/else if/else 显的比较啰嗦,这种情况可以使用 switch,switch 的语法是

switch(表达式){
    
    
	case1:
           代码1; break;
    case2:
           代码2; break;
           ...
    case 值n:
          代码n; break;
    default: 代码n+1
}

switch 也比较简单,根据表达式的值执行不同的分支,具体来说,根据表达式的值找匹配的 case,找到后,执行后面的代码,碰到 break 时结束,如果没有找到匹配的值则执行 default 中的语句

表达式值的数据类型只能是 byte, short, int, char, 枚举, 和 String (Java 1.7以后)。枚举和 String 我们在后续文章介绍。

switch 会简化一些代码的编写,但 break 和 case 语法会对初学者造成一些困惑。

容易忽略的break

break 是指跳出 switch 语句,执行 switch 后面的语句。每条 case 语句后面都应该跟 break 语句,否则的话它会继续执行后面 case 中的代码直到碰到 break 语句或 switch 结束。比如,下面的代码会输出所有数字而不只是 1。

int a = 1;
switch(a){
    
    
    case 1:
        System.out.println("1");
    case 2:
        System.out.println("2");
    default:
        System.out.println("3");
}

case 堆叠

case 语句后面可以没有要执行的代码,如下所示:

char c = 'A';//某字符
switch(c){
    
    
   case 'A':
   case 'B':
   case 'C':
        System.out.println("A-Z");break;
   case 'D':
       ....
}

case ‘A’/‘B’ 后都没有紧跟要执行的代码,他们实际会执行第一块碰到的代码,即 case ‘C’ 匹配的代码。

1.4.1.6 小结

条件执行总体上是比较简单的,单一条件满足时执行某操作使用 if,根据一个条件是否满足执行不同分支使用 if/else,表达复杂的条件使用 if/else if/elese,条件赋值使用三元运算符,根据某一个表达式的值不同执行不同的分支使用 switch。

从逻辑上讲,if/else, if/else if/else,三元运算符,switch 都可以只用 if 代替,但使用不同的语法表达更简洁,在条件比较多的时候,switch 从性能上也更高(马上解释为什么)。

1.4.2 实现原理

条件执行具体是怎么实现的呢

程序最终都是一条条的指令,CPU 有一个指令指示器,指向下一条要执行的指令,CPU 根据指示器的指示加载指令并且执行。指令大部分是具体的操作和运算,在执行这些操作时,执行完一个操作后,指令指示器会自动指向挨着的下一个指令。

但有一些特殊的指令,称为跳转指令。这些指令会修改指令指示器的值,让 CPU 跳到一个指定的地方执行。跳转有两种,一种是条件跳转,另一种是无条件跳转。条件跳转检查某个条件,满足则进行跳转,无条件跳转则是直接进行跳转。

if, else 实际上会转换为这些跳转指令,比如说下面的代码:

int a=10;
if(a%2==0)
{
    
    
	System.out.println("偶数");
}
//其他代码

转换到的转移指令可能是:

int a=10;
条件跳转: 如果a%2==0,跳转到第4行
无条件跳转:跳转到第7{
    
    
	System.out.println("偶数");
}
//其他代码

你可能会奇怪其中的无条件跳转指令,没有它不行吗?不行,没有这条指令,不管什么条件,括号中的代码都会执行。

不过,对应的跳转指令也可能是:

int a=10;
条件跳转: 如果a%2!=0,跳转到第6{
    
    
	System.out.println("偶数");
}
//其他代码

这个就没有无条件跳转指令,具体怎么对应和编译器实现有关。在单一 if 的情况下可能不用无条件跳转指令,但稍微复杂一些的情况都需要。if, if/else, if/else if/else, 三元运算符都会转换为条件跳转和无条件跳转。但 switch 不太一样。

switch 的转换和具体系统实现有关,如果分支比较少,可能会转换为跳转指令。但如果分支比较多,使用条件跳转会进行很多次的比较运算,效率比较低,可能会使用一种更为高效的方式,叫跳转表。跳转表是一个映射表,存储了可能的值以及要跳转到的地址,如表 1-5 所示:

在这里插入图片描述

跳转表为什么会更为高效呢?因为,其中的值必须为整数,且按大小顺序排序。按大小排序的整数可以使用高效的二分查找。即先与中间的值比,如果小于中间的值则在开始和中间值之间找,否则在中间值和末尾值之间找,每找一次缩小一倍查找范围。如果值是连续的,则跳转表还会进行特殊优化,优化为一个数组,连找都不用找了,值就是数组的下标索引,直接根据值就可以找到跳转的地址。即使值不是连续的,但数字比较密集,差的不多,编译器也可能会优化为一个数组型的跳转表,没有的值指向 default 分支。

程序源代码中的 case 值排列不要求是排序的,编译器会自动排序。之前说 switch 值的类型可以是 byte, short, int, char, 枚举和 String。其中 byte/short/int 本来就是整数,在上节我们也说过,char 本质上也是整数,而枚举类型也有对应的整数,String 用于 switch 时也会转换为整数(通过 hashCode 方法,后文介绍)。为什么不可以使用 long 呢?跳转表值的存储空间一般为 32 位,容纳不下 long

简单总结一下,条件执行的本质依赖于条件跳转、无条件跳转和跳转表。

条件执行中的跳转只会跳转到跳转语句以后的指令,能不能跳转到之前的指令呢?可以,那样就会形成循环。

1.5 循环

本节我们介绍流程控制中的循环,所谓循环就是多次重复执行某些类似的操作,这个操作一般不是完全一样的操作,而是类似的操作

计算机程序运行时大概只能顺序执行、条件执行和循环执行,顺序和条件其实没什么特别,而循环大概才是程序强大的地方。凭借循环,计算机能够非常高效的完成人很难或无法完成的事情,比如说,在大量文件中查找包含某个搜索词的文档,对几十万条销售数据进行统计汇总等。

1.5.1 循环的 4 种形式

在 Java 中,循环有四种形式,分别是 while, do/while, for, foreach,下面我们分别来看一下。

1.5.1.1 while

while 的语法为:

while(条件语句){
    
    
    代码块
}

while(条件语句) 代码; 

while 表达的含义也非常简单,只要条件语句为真,就一直执行后面的代码,为假就停止不做了。例如:

Scanner reader = new Scanner(System.in);
System.out.println("please input password");
int num = reader.nextInt();
int password = 6789;
while(num!=password){
    
    
    System.out.println("please input password");
    num = reader.nextInt();
}
System.out.println("correct");
reader.close();

以上代码中,我们使用类型为 Scanner 的 reader 变量从屏幕控制台接收数字,reader.nextInt() 从屏幕接收一个数字,如果数字不是 6789,就一直提示输入,否则才跳出循环。(以上代码 Scanner 我们还没有介绍过,可以忽略其细节,另外代码只用于解释语法,不应看做是实际良好代码)

while 循环中,代码块中会有代码影响循环条件,但也经常不知道什么时候循环会退出。如上例所示,匹配的时候会退出但什么时候能匹配取决于用户的输入。

1.5.1.2 do/while

如果不管条件语句是什么,代码块都会至少执行一次,则可以使用 do/while 循环。do/while 的语法是:

do{
    
    
   代码块;
}while(条件语句)

这个也很容易理解,先执行代码块,然后再判断条件语句,如果成立,则继续循环,否则退出循环。也就是,不管条件语句是什么,代码块都会至少执行一次。用上面的例子,其 do/while 循环是:

Scanner reader = new Scanner(System.in);
int password = 6789;
int num = 0;
do{
    
    
    System.out.println("please input password");    
    num = reader.nextInt();
}while(num!=password);
System.out.println("correct");
reader.close();

1.5.1.3 for

实际中应用最为广泛的循环语法可能是 for 了,尤其是在循环次数已知的情况下。for 的语法是:

for(初始化语句; 循环条件; 步进操作){
    
    
   循环体
}

for 后面的括号中有两个分号,分隔了三条语句,除了循环条件必须返回一个 boolean 类型外,其他语句没有什么要求,但通常情况下第一条语句用于初始化, 尤其是循环的索引变量,第三条语句修改循环变量,一般是步进,即递增或递减索引变量,循环体是在循环中执行的语句。

for 循环简化了书写,但执行过程对初学者而言不是那么明显,实际上,它执行的流程是这样的

  1. 执行初始化指令
  2. 检查循环条件是否为 true,如果为 false,跳转到第 6 步
  3. 循环条件为真,执行循环体
  4. 执行步进操作
  5. 步进操作执行完后,跳转到第 2 步,即继续检查循环条件。
  6. for 循环后面的语句

下面是一个简单的 for 循环:

int[] arr = {
    
    1,2,3,4};
for(int i=0;i<arr.length;i++){
    
    
    System.out.println(arr[i]);
}

顺序打印数组中的每个元素,初始化语句初始化索引 i 为 0,循环条件为索引小于数组长度,步进操作为递增索引 i,循环体打印数组元素。

在 for 中,每个语句都是可以为空的,也就是说:

for(;;){
    
    }

是有效的,这是个死循环,一直在空转,和 while(true){} 的效果是一样的。可以省略某些语句,但分号不能省。如:

int[] arr = {
    
    1,2,3,4};
int i=0;
for(;i<arr.length;i++){
    
    
    System.out.println(arr[i]);
}

索引变量在外面初始化了,所以初始化语句可以为空。

1.5.1.4 foreach

foreach 的语法如下代码所示:

int[] arr = {
    
    1,2,3,4};
for(int element : arr){
    
    
    System.out.println(element);
}

foreach 使用冒号,冒号前面是循环中的每个元素,包括数据类型和变量名称,冒号后面是要遍历的数组或集合,每次循环 element 都会自动更新。对于不需要使用索引变量,只是简单遍历的情况,foreach 语法上更为简洁

1.5.2 循环控制

在循环的时候,会以循环条件作为是否结束的依据,但有时候可能会根据别的条件提前结束循环或跳过一些代码,这时可以使用 break 或 continue 关键字对循环进行控制。

1.5.2.1 break

break 用于提前结束循环。比如说,在一个数组中查找某个元素的时候,循环条件可能是到数组结束,但如果找到了元素,可能就会想提前结束循环,这时候可以使用 break。

我们在介绍 switch 的时候提到过 break,它用于跳转到 switch 外面。在循环的循环体中也可以使用 break,它的含义和 switch 中类似,用于跳出循环,开始执行循环后面的语句。以在数组中查找元素作为例子,代码可能是:

int[] arr = ... ; //在该数组中查找元素
int toSearch = 100; //要查找的元素
int i = 0;
for(;i<arr.length;i++){
    
    
    if(arr[i]==toSearch){
    
    
        break;
    }
}
if(i!=arr.length){
    
    
    System.out.println("found");
}else{
    
    
    System.out.println("not found");
}

如果找到了,会调用 break, break 执行后会跳转到循环外面,不会再执行 i++ 语句,所以即使是最后一个元素匹配,i 也小于 arr.length,而如果没有找到,i 最后会变为 arr.length,所以可根据 i 是否等于 arr.length 来判断是否找到了。

以上代码中,也可以将判断是否找到的检查放到循环条件中,但通常情况下,使用 break 可能会使代码更清楚一些。

1.5.2.2 continue

在循环的过程中,有的代码可能不需要每次循环都执行,这时候,可以使用 continue 语句。continue 语句会跳过循环体中剩下的代码,然后执行步进操作。我们看个例子,以下代码统计一个数组中某个元素的个数:

int[] arr = ... //在该数组中查找元素
int toSearch = 2; //要查找的元素
int count = 0;
for(int i=0;i<arr.length;i++){
    
    
    if(arr[i]!=toSearch){
    
    
        continue;
    }
    count++;
}
System.out.println("found count "+count);

上面代码统计数组中值等于 toSearch 的元素个数,如果值不等于 toSearch,则跳过剩下的循环代码,执行 i++。以上代码也可以不用 continue,使用相反的 if 判断也可以得到相同的结果,这只是个人偏好的问题,如果类似要跳过的情况比较多,使用 continue 可能会更简洁。

1.5.3 循环嵌套

和 if 类似,循环也可以嵌套,在一个循环体中开启另一个循环。在嵌套循环中,break 语句只会跳出本层循环,continue 也一样

1.5.4 实现原理

和 if 一样,循环内部也是靠条件跳转和无条件跳转指令实现的。比如说下面的代码:

int[] arr = {
    
    1,2,3,4};
for(int i=0;i<arr.length;i++){
    
    
    System.out.println(arr[i]);
}

其对应的跳转过程可能为:

int[] arr = {
    
    1,2,3,4};
int i=0;
条件跳转:如果i>=arr.length,跳转到第7行
System.out.println(arr[i]);    
i++
无条件跳转,跳转到第3行
其他代码

在 if 中,跳转只会往后面跳,而 for 会往前面跳,第 6 行就是无条件跳转指令,跳转到了前面的第 3 行。break/continue 语句也都会转换为跳转指令。

1.5.5 小结

循环的语法总体上也是比较简单的,初学者需要注意的是 for 的执行过程,以及 break 和 continue 的含义。

虽然循环看起来只是重复执行一些类似的操作而已,但它其实是计算机程序解决问题的一种基本思维方式。凭借循环(当然还有别的),计算机程序可以发挥出强大的能力,比如说批量转换数据,查找过滤数据,统计汇总等。

使用基本数据类型、数组、基本运算、加上条件和循环,其实已经可以写很多程序了,但使用基本类型和将代码都放在一起,程序难以理解,尤其是程序逻辑比较复杂的时候。

解决复杂问题的基本策略是分而治之,将复杂问题分解为若干不那么复杂的子问题,然后子问题再分解为更小的子问题……程序由数据和指令组成,大程序可以分解为小程序,小程序接着分解为更小的程序。那如何表示子程序,以及子程序之间如何协调呢?我们下一小节介绍。

猜你喜欢

转载自blog.csdn.net/bm1998/article/details/107586919