编程基础👨‍💻

计算机是个机器,由CPU、输入/输出设备、硬盘、内存等组成。计算机上面运行着操作系统(Windows、Linux、Mac OS),操作系统上运行着程序,如Word、QQ等。

操作系统将时间分成很多细小的时间片,一个时间片给一个程序使用,另一个时间片给另一个程序用。并在它们之间反复切换。

应用程序看起来能做很多事,能读写文档、听歌、聊天、玩游戏……但本质上,计算机只会执行预先编写好的指令,这些指令也只是操作数据或者设备。所谓程序,基本上就是操作数据或者设备的指令的集合,即对数据做什么操作。

  • 读文档:就是把硬盘中的数据加载到内存,再由内存输出到显示器上面。
  • 写文档:就是将内存中的数据写到磁盘。
  • 听音乐:就是从磁盘中将数据加载到内存,由内存写到声卡。
  • 聊天:就是从键盘获取输入的数据,放到内存,由内存传给网卡,通过网络发送到另一个人网卡,再从网卡放到内存,显示到显示器上面。

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

什么是数据?

数据在计算机中都是二进制表示,不方便操作,因此高级语言引入了数据类型变量的概念。

数据类型

Java语言提供了8种基本数据类型。

  • 整数型:byte/short/int/long
  • 浮点型:float/double
  • 字符型:char
  • 布尔型:boolean | 数据类型 | 位 | 长度 | | :—-: | :—-: | :—-: | | byte | 1 | -2~2-1 | | short | 2 | -2~2-1 | | int | 4 | -2~2-1 | | long | 8 | -2~2-1 | | float | 32 | 1.4E-45 ~ 3.4E+38-3.4E+38 ~-1.4E-45 | | double | 64 | 4.9E-324 ~1.7E+308-1.7E+308 ~ -4.9E-324 | | char | 16 | |

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

变量

数据存放在内存中,为了方便找到和操作数据,编程语言引入了变量的概念。

  1. int age = 18;
  2. char firstName = '肖';
  3. float height = 175F;

声明一个变量其实就是在内存中分配了一块空间,比如int age = 18;这个空间能够存放int类型的数据。通过对变量名的操作即可操作变量名指向的空间。

命名规则

  • 见名知意
  • 骆驼峰风格
  • boolean类型字段尽量不要以is开头

赋值

通过声明变量,给定一个数据类型和一个有意义的变量名,我们就告诉了计算机我们要操作的数据。声明变量相当于在内存中分配了一块空间,但这块空间的内容是未知的,赋值就是给这个空间一个确切的值。

基本数据类型

整数型

整数常量默认为int,如果赋值long类型的值超过了int能表示值的范围,需要在值后面加上小写字母l或者大写字母L。推荐使用大写字母L因为小写字母l与数字l比较相似。

  1. byte b = 1;
  2. short s = 2;
  3. int age = 12;
  4. long l = 333;
  5. long l1 = 3232343433L;

浮点型

之所以叫浮点型是因为运算的结果会上下浮动(不准确),浮点型常量默认类型是double,声明float类型需要在值后面加上F或者f

  1. float f = 122.2F;
  2. float height = 172;

字符型

字符型只能表示单个字符可以是中文字符也可以是英文字符,字符需要使用``括起来。

  1. char c = 'c'

布尔型

布尔型只有两个值,分别是truefalse分别代表真假。

  1. boolean b = true;

数组类型

赋值语法
  1. int[] arr1 = new int[]{1, 2, 3};
  2. int[] arr2 = {1, 2, 3};
  3. int[] arr3 = new int[3];
  4. arr3[0] = 1;
  5. arr3[1] = 2;
  6. arr3[2] = 3;

第一种和第二种方式都是预先知道数组的内容,而第三种方式则是先分配长度,再给每个元素赋值。

第三种方式中,即使没有给每个元素赋值,每个元素都有默认值,这个默认值跟数组类型有关。数值类型类型是0,boolean是false,char类型是空字符串。

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

  1. int length = 10;
  2. int[] arr = new int[10];

还有一个小细节,不能给数组赋初始值的时候同时给定长度,以下格式是不允许的。

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

初始值已经决定长度,如果再给个长度将无从适应。

数组和基本类型的区别

基本类型变量只有一块对应的内存空间,但是数据由两块,一块存储数组内容的位置,一块存储数组的内容。

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

对应内存空间如下:

代码 内存地址 内存数据
int a = 10; 1001 10
int[] arr = new int[]{1, 2, 3}; 2001 3000
3000 1
3002 2
3004 3

基本数据类型a内存地址是1001对应的内存数据是10,而数组类型arr的内存地址是20012001指向的3000开始的位置才是数据存放的数组{1, 2, 3}

为什么数组需要两块空间

为什么数组需要两块内存空间?,我们来看下下面这段代码👇。

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

如果数据在内存只有一块空间,那么arr将无法容纳arr1中的元素,如果数据在内存由两块空间,那么就是把arr1数据值的引用传给了arr。虽然说数组的长度一经确定就无法修改,但是可以修改数组值的引用,让它指向一个新的数组。


运算符

单目运算符

++、—
  1. int a = 10;
  2. int result = (a++) + (++a) + a * 10;
  3. // 142
  4. System.out.println(result);

双目运算符

算术运算符

使用算数运算符的时候需要注意结果的范围,从而选择合适数据类型,如果使用的数据类型不准确会造成不必要的结果。

  1. // 2147483647 是int类型能表示的最大值
  2. int a = 2147483647 * 2;
  3. System.out.println(a);

对于取余,如果左边的数小于右边的数,直接返回左边的数。

  1. System.out.println(1 / 3); // 1

需要注意的是整数相除的结果还是整数。

  1. int a = 5 / 2;
  2. System.out.println(a); // 2

两个不同类型的值进行运算结果自动转换为运算数中类型范围较大的一方。

  1. double d = 2.0;
  2. int a = 5;
  3. System.out.println(a / d); // 2.5
  • 大类型转小类型属于默认转换
  • 小类型转大类型需要显示转换,否则会抛出异常
  1. short s = 1;
  2. s = s + 1;

浮点数运算不准确

  1. System.out.println(0.1f * 0.1f); // 0.010000001
  2. System.out.println(0.1 * 0.1); // 0.010000000000000002

比较运算符

比较运算符返回的结果一定是boolean类型,对于基本数据类型=比较的是值是否相等,对于引用类型=比较的引用。

  1. int a = 10;
  2. int b = 10;
  3. System.out.println(a == b); // true
  4. int[] arr = new int[]{1, 2, 3};
  5. int[] arr1 = new int[]{1, 2, 3};
  6. System.out.println(arr == arr1); // false

逻辑运算符

表达式一 逻辑运算符 表达式二

逻辑运算符左右两边只能是boolean类型,运算结果是boolean类型。

  1. & 与 两边为true则为true
  2. | 或 只要有一个为true则为true
  3. && 短路与,表达式一如果为false将不会执行表达式二
  4. || 短路或,表达式一如果为true将不会执行表达式二
  5. ! 取反
  6. ^ 两个相同为false,不同为true

三目运算符

表达式一 ? 表达式二 : 表达式三

如果表达式一成立将执行表达式二否则执行表达式三。

  1. int a = 10 > 5 ? 10 : 5;

为什么浮点型运算不准确

进制

二进制

十进制

十六进制

补码 原码 反码

计算机如何进行运算


编码

unicode编码

非unicode编码

为什么会乱码

如何从乱码中恢复


表达式

关系表达式

比较运算符构成

  1. boolean b = 5 > 6;

选择结构

if语句

区间判断可以使用if语句

  1. if(条件语句){
  2. 代码块
  3. }
  4. if(条件语句){
  5. 代码块
  6. }else{
  7. 代码块
  8. }
  9. if(条件语句){
  10. 代码块
  11. }else if(条件语句){
  12. 代码块
  13. }else {
  14. 代码块
  15. }

switch语句

等值判断且分支不是特别多的情况下可以使用switch,根据表达式的值,去进行匹配case的值,如果找到了就执行case对应的代码块,遇到break结束,如果没有对应匹配的case则执行default语句。

switch语句只支持byteshortintchar(ASCII码)、String(JDK1.7之后,本质上是hashCode())、枚举

  1. switch(表达式){
  2. case 值:
  3. break;
  4. case 值:
  5. break;
  6. default:
  7. break;
  8. }

条件执行本质

程序最终都是一条条指令,CPU有一个指令指示器,指向下一条要执行的指令,CPU根据指令指示器的指示加载并执行指令,执行完指令之后,指示器会自动指向下一条指令。

跳转指令

跳转指令会修改指示器的值,让CPU跳转到指定的指令去执行。跳转指令分为条件跳转无条件跳转

if-else实际会转换为这些指令。

  1. int a = 10;
  2. if(a % 2 = 0) {
  3. System.out.println("偶数");
  4. }
  5. // 代码块

转换后可能为:

  1. int a = 10;
  2. 条件跳转:a % 2 = 0 // { 那一行代码
  3. 无条件跳转:代码块哪一行代码
  4. {
  5. System.out.println("偶数");
  6. }
  7. // 代码块

也有可能是下面这种情况:

  1. int a = 10;
  2. 条件跳转: a % 2 != 0 // { 代码块一行代码
  3. {
  4. System.out.println("偶数");
  5. }
  6. // 代码块

switch的转换跟具体的情况有关,如果是分支比较少的情况下,可能会转换成跳转指令。但如果分支比较多,使用跳转跳转会进行很多次比较运算,效率比较低。这时候可能会使用一种更高效的方式,称之为跳跃表。跳跃表是一个映射表存储了可能的值以及要跳转的地址。

值一 跳转地址一
值二 跳转地址二
值三 跳转地址三

为什么跳跃表更高效?因为其中的值必须是整数且按大小排序。按大小排序可以使用高效的二分查找。如果值是连续的则跳跃表还可以进行优化,优化成一个数组,直接通过数组索引获取地址。

switch值的类型可以是byte/short/int/char/枚举/String,其中byte/short/int本来就是整数,char本质上也是整数(ASCII码),枚举也有对应的整数,String用于switch时也会转换为整数(hashCode方法)。为什么不可以使用long?跳跃表值的存储空间一般是32位,容不下long。

循环结构

while

while循环很简单,如果条件语句为true则执行代码块。

  1. while(条件语句){
  2. 代码块
  3. }

do-while

do-whilewhile如果条件语句为true则执行代码块,与while语句不同的是,do-while语句会先执行代码块在判断条件语句,根据条件语句的值决定是否继续执行循环。

  1. do{
  2. 代码块
  3. }while(条件语句)

for

  1. for(初始化语句; 条件语句; 增量语句){
  2. 代码块
  3. }
  1. 初始化语句
  2. 判断条件语句
  3. 如果为true执行代码块,如果为false结束循环
  4. 执行增量语句,返回第二步
  1. int i = 0;
  2. for(;i<10;i++){
  3. }
  4. int j = 0;
  5. for(;j<10;){
  6. j++;
  7. }
  8. for(;;){}

for-each

  1. for(类型 元素 : 集合){
  2. }
  3. int[] arr = new int[]{1, 2, 3};
  4. for(int elment : arr){
  5. System.out.println(element);
  6. }

循环嵌套

  1. int line = 4;
  2. for (int i = 0; i < line; i++) {
  3. for (int j = 0; j <= i; j++) {
  4. System.out.print("*");
  5. }
  6. System.out.print("\n");
  7. }

循环控制-break

在循环中,条件语句是决定结束循环的关键,但有时候我们会根据某些条件提前结束循环。比如在数组中查找某个元素,循环条件可能是到数组结束,如果找到了元素,可能想提前结束循环这时候可以使用break

  1. int[] array = new int[]{1, 3, 5, 7, 9};
  2. int target = 9;
  3. int i;
  4. for(i = 0; i < array.length; i++){
  5. if(array[i] == target){
  6. break;
  7. }
  8. }
  9. if(i != array.length){
  10. System.out.println("found");
  11. }else {
  12. System.out.println("not found");
  13. }

如果内层循环想要中断外层循环,可以在外层循环中添加标量在内存循环中break 标量;即可。

  1. label:
  2. for(;;;){
  3. for(;;;){
  4. break label;
  5. }
  6. }
  1. A:
  2. for(int i = 0; i < 5; i++){
  3. for(int j = 0; j <= i; j++){
  4. System.out.println(i,j);
  5. break A;
  6. }
  7. }

循环控制-countinue

continue作用是跳出本次循环,然后执行判断

  1. int count = 0;
  2. for(int i = 1; i < 10; i++){
  3. if(i % 2 == 0){
  4. continue;
  5. }
  6. count++;
  7. }

实现原理

if一样,循环也是用了条件跳转或者无条件跳转指令来实现循环,比如下面代码:

  1. int[] score = new int[]{60, 70, 80, 90, 100};
  2. for(int i = 0; i < score.length; i++){
  3. System.out.println(score[i]);
  4. }

其指令可能为:

  1. 初始化变量int i = 0;
  2. 条件跳转i >= score.length,跳转到循环体外
  3. 执行循环体
  4. 执行增量语句
  5. 无条件跳转

小结

  • 对于知道循环次数确定的情况下推荐使用for循环
  • do-while循环至少执行一次
  • break用于跳出当前循环,如果需要跳出外层循环可以使用标量
  • continue用于跳出本次循环

常见练习题

  1. 99乘法口诀表
  2. 打印菱形
  3. 杨辉三角
  1. for (int i = 1; i <= 9; i++) {
  2. for (int j = 1; j <= i; j++) {
  3. System.out.print("\t");
  4. }
  5. System.out.print("\n");
  6. }

二分查找

  1. /**
  2. @param array 查找的数组
  3. @param searchElement 查找的元素
  4. @return 查找元素所在的索引,如果没有找到则返回-1
  5. */
  6. public int binarySearch(long[] array,long searchElement){
  7. // low high 用于跟踪要查找的范围
  8. long low = 0;
  9. long high = array.length - 1;
  10. while(low <= high){// 只要范围区间没有缩小到只包含一个元素
  11. long mid = (high + low) / 2;
  12. long guess = array[mid];
  13. if(guess > searchElement){ // 排除当前元素
  14. high = mid - 1;
  15. }else if(guess < searchElement){ // 排除当前元素
  16. low = mid + 1;
  17. }else{
  18. return mid;
  19. }
  20. }
  21. return -1;
  22. }

函数

如果经常要做某些操作,则相关的代码要写很多遍。比如在数组中查询某个数、对数组进行排序、对字符串进行非空判断……另外,有一些操作很复杂,可能分为多个步骤,如果都放在一起会造成代码的臃肿以及后期维护困难。

计算机使用函数这个概念来解决这类问题,即使用函数来减少重复代码和分解操作

基本概念

函数声明
  1. 访问修饰符 [static] 返回值类型 方法名(参数列表){
  2. // 代码块
  3. [return [返回值]];
  4. }
  1. 方法名,表示函数的功能
  2. 参数列表,表示执行操作需要的参数0到多个
  3. 代码块,具体的操作
  4. return,表示结束函数,return可以接上参数,参数的数据类型与返回值相同,如果不带参数需要将返回值类型声明为void
  1. public static void sort(int[] array){
  2. }
  3. public static void isEmpty(){
  4. }
  5. public static void main(String[] args){
  6. sort(new int[]{1, 2, 3, 6, 5, 7});
  7. }

进一步理解函数

可变长度参数

有时候函数接收的参数长度可能是不确定的,这时候可以是使用可变长度参数,可变长度本质上是一个数组。具体定义为数据类型...参数名

  1. public static int sum(int... numbers) {
  2. int sum = 0;
  3. for (int element : numbers) {
  4. sum += element;
  5. }
  6. return sum;
  7. }
  8. public static int sum(int param, int... numbers) {
  9. return sum(numbers);
  10. }
  11. public static void main(String[] args){
  12. int sum = sum(new int[]{1, 2, 3, 4, 5});
  13. sum = sum(1, new int[]{1, 2, 3, 4, 5});
  14. }
  • 可变参数必须是参数的最后一个
  • 一个函数只能有一个可变参数
  • 如果发生参数无从适应可以显示new array作为参数传递

函数重载
  • 在同一个类中方法名相同
  • 参数数据类型不同
  • 参数个数不同
  • 与返回值无关
  1. public static long max(long a, long b);
  2. public static int max(int a, int b);
  3. public static double max(double a, double b);
  4. public static float max(float a,float b);

参数传递实际上就是给参数赋值,传递的数据与函数声明的数据类型需要一致,但不要求完全一样,Java编译器会自动进行类型转换,匹配合适的函数。

  1. char charA = 'a';
  2. char charB = 'b';
  3. System.out.println(Math.max(charA, charB));

参数类型是char类型,但Math类并没有针对char类型的max函数,这是因为char类型本质上是一个整数,Java会将其自动转换为int类型,然后调用Math.max(int a, int b)

如果没有针对int类型的Math.max(int a, int b)呢?调用也会转换为Math.max(long a, long b)。如果long也没有有呢?会调用Math.max(flaot a,float b)。如果float也没有呢?会调用Math.max(double a,double b)

在只有一个函数情况下,即没有重载,只要可以进行类型转换,就会调用该函数,再有函数重载的情况下,会调用最佳匹配的函数。

递归函数

函数大部分都是被其他函数调用,但也可以调用本身,调用本身的函数就叫递归函数

  1. public static void factorail(int n){
  2. if(n == 1){
  3. return 1;
  4. }
  5. return n * factorail(n - 1);
  6. }

但递归其实是有开销的,而且使用不当,可能会出现意外的结果。

  1. System.out.println(factorial(1000000));

递归函数经常可以转换成非递归函数,通过循环实现。

  1. public static void factorail(int n){
  2. long result = 1;
  3. for(int i = 1; i <= n; i++){
  4. result *= i;
  5. }
  6. return result;
  7. }

函数调用基本原理

栈的概念

我们之前提到过CPU有一个指令指示器,指向下一个要执行的指令并通知CPU,指令要么顺序执行,要么进行跳转(条件跳转或无条件跳转)。

基本上这是成立的,程序从main函数开始顺序执行,函数调用可以看作是无条件跳转,跳转到对应的函数开始执行,碰到return语句或者函数执行结束的时候,再执行一次无条件跳转,跳转回调用方,执行调用函数后的下一条指令。

是一块内存,特点是先进先出。类似一个桶。往栈里面放入数据称为入栈,最下面是的称为栈底,最上面的称为栈顶,从栈顶拿数据称为出栈。栈一般是从高位地址向低位地址扩展。换句话说栈底的内存位置最高,栈顶的内存位置最低。

函数执行的基本原理

数据和对象的内存分配

递归调用的原理

小结