编程基础与二进制

第1章 编程基础

  • 所谓程序,基本上就是告诉计算机要操作的数据执行的指令序列,即对什么数据做什么操作。

1.1 数据类型和变量

  • 数据类型用于对数据归类,以便于理解和操作。
  • 对 Java 语言而言,有如下基本数据类型
  • 整数类型:有 4 种整型 byte/short/int/long,分别有不同的取值范围;
  • 小数类型:有两种类型 foat/double,有不同的取值范围和精度;
  • 字符类型:char,表示单个字符
  • 真假类型:boolean,表示真假。
  • 基本数据类型都有对应的数组类型,数组表示固定长度的同种数据类型的多条记录,这些数据在内存中连续存放
  • Java 是面向对象的语言,除了基本数据类型,其他都是对象类型。
  • 简单地说,对象是由基本数据类型、数组和其他对象组合而成的一个东西,以方便对其整体进行操作。
  • 日期在 Java 中也是一个对象,内部表示为整型 long。
  • 所谓内存在程序看来就是一块有地址编号的连续的空间,数据放到内存中的某个位置后,为了方便地找到和操作这个数据,需要给这个位置起一个名字。编程语言通过变量这个概念来表示这个过程。
  • 之所以叫“变”量,是因为它表示的是内存中的位置,这个位置存放的值是可以变化的。
  • 虽然变量的值是可以变化的,但变量的名字是不变的,这个名字应该代表程序员心目中这块内存空间的意义,这个意义应该是不变的。
  • 通过声明变量,每个变量赋予一个数据类型和一个有意义的名字,我们就告诉了计算机要操作的数据。
  • 变量就是给数据起名字,方便找不同的数据,它的值可以变,但含义不应变。

1.2 赋值

  • 声明变量之后,就在内存分配了一块位置,但这个位置的内容是未知的,赋值就是把这块位置的内容设为一个确定的值。
1.2.1 基本类型
  • 整数类型
  • 整数类型有 byte、short、int 和 long,分别占 1、2、4、8 个字节。
  • 赋值形式很简单,直接把熟悉的数字常量形式赋值给变量即可,对应的内存空间的值就从未知变成了确定的常量。
  • 常量不能超过对应类型的表示范围。
  • 在给 long 类型赋值时,如果常量超过了 int 的表示范围,需要在常量后面加大写或小写字母 L,即 L 或 l,之所以需要加 L 或 l,是因为数字常量默认为是 int 类型。
  • 小数类型
  • 小数类型有 float 和 double,占用的内存空间分别是 4 和 8 字节,有不同的取值范围和精度,double 表示的范围更大,精度更高。
  • 对于 double,直接把熟悉的小数表示赋值给变量即可。
  • 但对于 float,需要在数字后面加大写字母 F 或小写字母 f,这是由于小数常量默认是 double 类型。
  • 除了小数,也可以把整数直接赋值给 float 或 double。
  • 真假类型
  • 真假(boolean)类型很简单,直接使用 true 或 false 赋值,分别表示真和假。
  • 字符类型
  • 字符类型 char 用于表示一个字符,这个字符可以是中文字符,也可以是英文字符,char 占用的内存空间是两个字节。
  • 赋值时把常量字符用单引号括起来,不要使用双引号。
  • 变量可以进行各种运算,也可以把变量赋给变量,也可以将变量的运算结果赋给变量。
1.2.2 数组类型
  • 基本类型的数组有 3 种赋值形式:
1. int[] arr = {1, 2, 3};
2. int[] arr2 = new int[]{1, 2, 3};
3. int[] arr3 = new int[3];
   for (int i = 0; i < arr3.length; i++) {
       arr3[i] = i;
   }
  • 第 1 种和第 2 种都是预先知道数组的内容,而第 3 种是先分配长度,然后再给每个元素赋值。
  • 第 3 种形式中,即使没有给每个元素赋值,每个元素也都有一个默认值,这个默认值跟数组类型有关数值类型的值为 0, boolean 为 false, char 为空字符
  • 数组有一个 length 属性,但只能读,不能改。还有一个小细节,不能在给定初始值的同时给定长度。
  • 数组类型和基本类型是有明显不同的,一个基本类型变量,内存中只会有一块对应的内存空间。但数组有两块:一块用于存储数组内容本身,另一块用于存储内容的位置。
  • 给数组变量赋值和给数组中元素赋值是两回事,给数组中元素赋值是改变数组内容,而给数组变量赋值则会让变量指向一个不同的位置。
  • 我们说数组的长度是不可以变的,不可变指的是数组的内容空间,一经分配,长度就不能再变了,但可以改变数组变量的值,让它指向一个长度不同的空间。
  • 给变量赋值就是将变量对应的内存空间设置为一个明确的值,有了值之后,变量可以被加载到 CPU, CPU 可以对这些值进行各种运算,运算后的结果又可以被赋值给变量,保存到内存中。

1.3 基本运算

  • 有了初始值之后,可以对数据进行运算。
  • 算术运算:主要是日常的加减乘除。
  • 比较运算:主要是日常的大小比较。
  • 逻辑运算:针对布尔值进行运算。
1.3.1 算术运算
  • 算术运算符有加、减、乘、除,符号分别是 +、-、*、/,另外还有取模运算符 %,以及自增(++)和自减(–)运算符。
  • 取模运算适用于整数和字符类型,其他算术运算适用于所有数值类型和字符类型。
  • 加、减、乘、除注意事项
  • 运算时要注意结果的范围,使用恰当的数据类型。
  • 整数相除不是四舍五入,而是直接舍去小数位。
  • 如果要按小数进行运算,需要将至少一个数表示为小数形式,或者使用强制类型转化。
  • 小数计算结果不精确。
  • 自增自减放在变量后(a++)是先用原来的值进行其他操作,然后再对自己做修改,而放在变量前(++a)是先对自己做修改,再用修改后的值进行其他操作。
1.3.2 比较运算
  • 比较运算就是计算两个值之间的关系,结果是一个布尔类型(boolean)的值。比较运算适用于所有数值类型和字符类型。
  • 比较操作符有大于(>)、大于等于(>=)、小于(<)、小于等于(<=)、等于(==)、不等于(! =)。
  • 对于数组,判断的是两个变量指向的是不是同一个数组,而不是两个数组的元素内容是否一样,即使两个数组的内容是一样的,但如果是两个不同的数组,依然会返回 false。如果需要比较数组的内容是否一样,需要逐个比较里面存储的每个元素。
1.3.3 逻辑运算
  • **逻辑运算根据数据的逻辑关系,生成一个布尔值 true 或者 false。**逻辑运算只可应用于 boolean 类型的数据,但比较运算的结果是布尔值,所以其他类型数据的比较结果可进行逻辑运算。
  • 逻辑运算符具体有以下这些。
  • 与(&):两个都为 true 才是 true,只要有一个是 false 就是 false;
  • 或(|):只要有一个为 true 就是 true,都是 false 才是 false;
  • 非(!):针对一个变量,true 会变成 false,false 会变成 true;
  • 异或(^):两个相同为 false,两个不相同为 true;
  • 短路与(&&):和 & 类似;
  • 短路或(||):与 | 类似。

1.4 条件执行

  • 流程控制中最基本的就是条件执行,也就是说,一些操作只能在某些条件满足的情况下才执行,在一些条件下执行某种操作,在另外一些条件下执行另外的操作。
1.4.1 语法和陷阱
  • Java 中表达条件执行的基本语法是 if 语句,它的语法是:
if (条件语句) {
	代码块
}
  • 表达的含义也非常简单,只在条件语句为真的情况下,才执行后面的代码,为假就不执行了。
  • 具体来说,条件语句必须为布尔值,可以是一个直接的布尔变量,也可以是变量运算后的结果

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

  • if 实现的是条件满足的时候做什么操作,如果需要根据条件做分支,即满足的时候执行某种逻辑,而不满足的时候执行另一种逻辑,则可以用 if/else,语法是:
if(判断条件) {
	代码块 1
}else {
	代码块 2
}
  • if/else 也非常简单,判断条件是一个布尔值,为 true 的时候执行代码块 1,为假的时候执行代码块 2。
  • 三元运算符语法为:判断条件 ? 表达式 1 : 表达式 2
  • 三元运算符会得到一个结果,判断条件为真的时候就返回表达式 1 的值,否则就返回表达式 2 的值。
  • 在 if/else if/else 中,如果判断的条件基于的是同一个变量,只是根据变量值的不同而有不同的分支,如果值比较多,比如根据星期几进行判断,有 7 种可能性,或者根据英文字母进行判断,有 26 种可能性,使用 if/else if/else 比较烦琐,这种情况可以使用 switch,语法是:
switch(表达式) {
	case 值1:
		代码 1;
		break;
	case 值2:
		代码 2;
		break;
	case 值3:
		代码 1;
		break;
	......
	case 值n:
		代码 n;
		break;
	default: 代码 n+1;
}
  • switch 也比较简单,根据表达式的值执行不同的分支。
  • 具体来说,根据表达式的值找匹配的 case,找到后执行后面的代码,碰到 break 时结束,如果没有找到匹配的值则执行 default 后的语句。
  • 表达式值的数据类型只能是 byte、short、int、char、枚举和 String。
  • break 是指跳出 switch 语句,执行 switch 后面的语句。每条 case 语句后面都应该跟 break 语句,否则会继续执行后面 case 中的代码直到碰到 break 语句或 switch 结束。
  • case 语句后面可以没有要执行的代码。
1.4.2 实现原理
  • 程序最终都是一条条的指令,CPU 有一个指令指示器,指向下一条要执行的指令。
  • CPU 根据指示器的指示加载指令并且执行。
  • 指令大部分是具体的操作和运算,在执行这些操作时,执行完一个操作后,指令指示器会自动指向挨着的下一条指令。
  • 有一些特殊的指令,称为跳转指令,这些指令会修改指令指示器的值,让 CPU 跳到一个指定的地方执行。
  • 跳转有两种:一种是条件跳转;另一种是无条件跳转。
  • 条件跳转检查某个条件,满足则进行跳转,无条件跳转则是直接进行跳转。
  • if、if/else、if/elseif/else、三元运算符都会转换为条件跳转和无条件跳转,但 switch 不太一样。
  • switch 的转换和具体系统实现有关。如果分支比较少,可能会转换为跳转指令。如果分支比较多,使用条件跳转会进行很多次的比较运算,效率比较低,可能会使用一种更为高效的方式,叫跳转表。跳转表是一个映射表,存储了可能的值以及要跳转到的地址。

1.5 循环

  • 所谓循环,就是多次重复执行某些类似的操作,这个操作一般不是完全一样的操作,而是类似的操作。
1.5.1 循环的 4 种形式
  • 在 Java 中,循环有 4 种形式,分别是 while、do/while、for 和 foreach。
  • while
  • while 的语法为:
while(条件语句) {
	代码块
}
  • while 和 if 的语法很像,只是把 if 换成了 while,它表达的含义也非常简单,只要条件语句为真,就一直执行后面的代码,为假就停止不做了。
  • while 循环中,代码块中会有影响循环中断或退出的条件,但经常不知道什么时候循环会中断或退出。
  • do/while
  • 如果不管条件语句是什么,代码块都会至少执行一次,则可以使用 do/while 循环,其语法为:
do {
	代码块;
	}while(条件语句)
  • 先执行代码块,然后再判断条件语句,如果成立,则继续循环,否则退出循环。
  • 也就是说,不管条件语句是什么,代码块都会至少执行一次。
  • for
  • 实际中应用最为广泛的循环语法可能是 for 了,尤其是在循环次数已知的情况,其语法为:
for (初始化语句; 循环条件; 步进操作) {
	循环体
}
  • for 后面的括号中有两个分号;,分隔了三条语句。
  • 除了循环条件必须返回一个 boolean 类型外,其他语句没有什么要求,但通常情况下第一条语句用于初始化,尤其是循环的索引变量,第三条语句修改循环变量,一般是步进,即递增或递减索引变量,循环体是在循环中执行的语句。
  • foreach
  • foreach 不是一个关键字,它使用冒号:,冒号前面是循环中的每个元素包括数据类型和变量名称冒号后面是要遍历的数组或集合,每次循环 element 都会自动更新。
  • 对于不需要使用索引变量,只是简单遍历的情况,foreach 语法上更为简洁。
1.5.2 循环控制
  • 在循环的时候,会以循环条件作为是否结束的依据,但有时可能会需要根据别的条件提前结束循环或跳过一些代码,这时可以使用 break 或 continue 关键字对循环进行控制。
  • break
  • break 用于提前结束循环。
  • 在循环的循环体中也可以使用 break,它的含义和 switch 中的类似,用于跳出循环,开始执行循环后面的语句。
  • continue
  • 在循环的过程中,有的代码可能不需要每次循环都执行,这时候,可以使用 continue 语句,continue 语句会跳过循环体中剩下的代码,然后执行步进操作

1.6 函数的用法

1.6.1 基本概念
  • 计算机程序使用函数来减少重复代码和分解复杂操作。
  • 函数的基本语法结构:
修饰符 返回值类型 函数名(参数类型 参数名, ...) {
	操作
	return 返回值; 
}
  • 函数的主要组成部分有以下几种。
  • 1)函数名:名字是不可或缺的,表示函数的功能。
  • 2)参数:参数有0个到多个,每个参数由参数的数据类型和参数名组成。
  • 3)操作:函数的具体操作代码。
  • 4)返回值:函数可以没有返回值,如果没有返回值则类型写成 void,如果有则在函数代码中必须使用 return 语句返回一个值,这个值的类型需要和声明的返回值类型一致。
  • 5)修饰符:Java 中函数有很多修饰符,分别表示不同的目的。
  • 定义函数就是定义了一段有着明确功能的子程序,但定义函数本身不会执行任何代码,函数要被执行,需要被调用。
  • Java 中,任何函数都需要放在一个类中。
  • 一个类里面可以定义多个函数,类里面可以定义一个叫做 main 的函数,形式如:
public static void main(String[] args) {
}
  • 这个函数有特殊的含义,表示程序的入口,String[] args 表示从控制台接收到的参数,我们暂时可以忽略它。
  • Java 中运行一个程序的时候,需要指定一个定义了 main 函数的类,Java 会寻找 main 函数,并从 main 函数开始执行。
  • 不管 main 函数定义在哪里,Java 函数都会先找到它,然后从它的第一行开始执行。
  • main 函数中除了可以定义变量,操作数据,还可以调用其他函数。调用函数需要传递参数并处理返回值。
  • 调用函数如果没有参数要传递,也要加括号 ()。
  • 传递的参数不一定是个变量,可以是常量,也可以是某个运算表达式,可以是某个函数的返回结果。
  • 定义函数时声明参数,实际上就是定义变量,只是这些变量的值是未知的,调用函数时传递参数,实际上就是给函数中的变量赋值。
  • 对于需要重复执行的代码,可以定义函数,然后在需要的地方调用,这样可以减少重复代码。对于复杂的操作,可以将操作分为多个函数,会使得代码更加易读。
  • 在 Java 中,函数在程序代码中的位置和实际执行的顺序是没有关系的。
1.6.2 进一步理解函数
  • 参数传递
  • 有两类特殊类型的参数:数组和可变长度的参数。
  • 数组
  • 数组作为参数与基本类型是不一样的,基本类型不会对调用者中的变量造成任何影响,但数组不是,在函数内修改数组中的元素会修改调用者中的数组内容
public static void reset(int[] arr) {
	for (int i = 0; i < arr.length; i++) {
		arr[i] = i;
	}
}

public static void main(String[] args) {
    int[] arr = {10, 20, 30, 40};
    reset(arr);
    for (int i = 0; i < arr.length; i++) {
        System.out.println(arr[i]);
    }
}
  • 在 reset 函数内给参数数组元素赋值,在 main 函数中数组 arr 的值也会变。
  • 一个数组变量有两块空间,一块用于存储数组内容本身,另一块用于存储内容的位置,给数组变量赋值不会影响原有的数组内容本身,而只会让数组变量指向一个不同的数组内容空间。
  • 在上例中,函数参数中的数组变量 arr 和 main 函数中的数组变量 arr 存储的都是相同的位置,而数组内容本身只有一份数据,所以,在 reset 中修改数组元素内容和在 main 中修改是完全一样的。
  • 可变长度的参数
  • 可变长度参数的语法是在数据类型后面加三个点“… ”,在函数内,可变长度参数可以看作是数组
  • 可变长度参数必须是参数列表中的最后一个,一个函数也只能有一个可变长度的参数
  • 可变长度参数实际上会转换为数组参数,也就是说,函数声明 max(int min, int… a) 实际上会转换为 max(int min, int[] a),在 main 函数调用 max(0,2,4,5) 的时候,实际上会转换为调用 max(0, new int[]{2,4,5}),使用可变长度参数主要是简化了代码书写。
  • 理解返回
  • 函数返回值类型为 void 时,return 不是必需的,在没有 return 的情况下,会执行到函数结尾自动返回。return 用于显式结束函数执行,返回调用方。
  • return 可以用于函数内的任意地方,可以在函数结尾,也可以在中间,可以在 if 语句内,可以在 for 循环内,用于提前结束函数执行,返回调用方。函数返回值类型为 void 也可以使用 return,即 “return; ”,不用带值,含义是返回调用方,只是没有返回值而已。
  • 重复的命名
  • 每个函数都有一个名字,这个名字表示这个函数的意义,名字可以重复吗?
  • 在不同的类里,答案是肯定的,在同一个类里,要看情况。
  • 同一个类里,函数可以重名,但是参数不能完全一样,即要么参数个数不同,要么参数个数相同但至少有一个参数类型不一样。
  • 同一个类中函数名相同但参数不同的现象,一般称为函数重载。
  • 为什么需要函数重载呢?一般是因为函数想表达的意义是一样的,但参数个数或类型不一样。
  • 调用的匹配过程
  • 参数传递实际上是给参数赋值,调用者传递的数据需要与函数声明的参数类型是匹配的,但不要求完全一样。
  • 在只有一个函数的情况下,即没有重载,只要可以进行类型转换,就会调用该函数,在有函数重载的情况下,会调用最匹配的函数。
  • 递归函数
  • 函数大部分情况下都是被别的函数调用的,但其实函数也可以调用它自己,调用自己的函数就叫递归函数。
  • 那递归不可行的情况下怎么办呢?递归函数经常可以转换为非递归的形式,通过循环实现。

1.7 函数调用的基本原理

1.7.1 栈的概念
  • CPU 有一个指令指示器,指向下一条要执行的指令,要么顺序执行,要么进行跳转(条件跳转或无条件跳转)。
  • 计算机系统主要使用栈来存放函数调用过程中需要的数据,包括参数、返回地址,以及函数内定义的局部变量。
  • 计算机系统就如何在栈中存放这些数据,调用者和函数如何协作做了约定。
  • 返回值不太一样,它可能放在栈中,但它使用的栈和局部变量不完全一样,有的系统使用 CPU 内的一个存储器存储返回值,我们可以简单认为存在一个专门的返回值存储器。
  • main 函数的相关数据放在栈的最下面,每调用一次函数,都会将相关函数的数据入栈,调用结束会出栈。
1.7.2 函数执行的基本原理
  • 通过一个例子来具体说明函数执行的过程:
public class Sum {
    public static int sum(int a, int b) {
        int c = a + b;
        return c;
    }

    public static void main(String[] args) {
        int d = Sum.sum(1, 2);
        System.out.println(d);
    }
}
  • 当程序在main函数调用Sum.sum之前, 栈中主要存放了两个变量 args 和 d。栈的情况大概如下图所示:
  • 在程序执行到 Sum.sum 的函数内部,准备返回之前,栈的情况大概如下图所示:
  • 在 main 函数调用 Sum.sum 时,首先将参数 1 和 2 入栈,然后将返回地址(也就是调用函数结束后要执行的指令地址)入栈,接着跳转到 sum 函数,在 sum 函数内部,需要为局部变量 c 分配一个空间,而参数变量 a 和 b 则直接对应于入栈的数据 1 和 2,在返回之前,返回值保存到了专门的返回值存储器中。
  • 在调用 return 后,程序会跳转到栈中保存的返回地址,即 main 的下一条指令地址,而 sum 函数相关的数据会出栈。
  • main 的下一条指令是根据函数返回值给变量 d 赋值,返回值从专门的返回值存储器中获得。
  • 函数中的参数和函数内定义的变量,都分配在栈中,这些变量只有在函数被调用的时候才分配,而且在调用结束后就被释放了,这个说法主要针对基本数据类型。
1.7.3 数组和对象的内存分配
  • 对于数组和对象类型,它们都有两块内存,一块存放实际的内容,一块存放实际内容的地址,实际的内容空间一般不是分配在栈上的,而是分配在堆中,但存放地址的空间是分配在栈上的。
public class ArrayMax {
    public static int max(int min, int[] arr) {
        int max = min;
        for (int a : arr) {
            if (a > max) {
                max = a;
            }
        }
        return max;
    }

    public static void main(String[] args) {
        int[] arr = new int[]{2, 3, 4};
        int ret = max(0, arr);
        System.out.println(ret);
    }
}
  • main 函数新建了一个数组,然后调用函数 max 计算 0 和数组中元素的最大值,在程序执行到 max 函数的 return 语句之前的时候,内存中栈和堆的情况如下图所示:
  • 对于数组 arr,在栈中存放的是实际内容的地址 0x1000,存放地址的栈空间会随着入栈分配,出栈释放,但存放实际内容的堆空间不受影响。
  • 但说堆空间完全不受影响是不正确的,当 main 函数执行结束,栈空间没有变量指向它的时候,Java 系统会自动进行垃圾回收,从而释放这块空间。