第一章 基本语法
1. 基本介绍
1.1 Java是什么
应用领域:
- Java SE: 是本课程的核心,也是java语言的核心,可用来开发桌面软件(现在很少用这个功能)
- Jave ME: 开发功能机小游戏,已淘汰
- Jave EE: 用来开发服务端程序,奠定了Java的地位(最广泛的应用领域)
- Android: 开发智能机程序
- 大数据: 首选Java开发,升华普通数据
运行机制:
前提: 程序员编写的是源代码,计算机最终运行的是机器码。
如何生成机器码:
- 编译型: 程序运行前,由编译器将源代码完全翻译成机器码。
- 解释型: 程序运行前,由解释器将源代码逐行翻译成机器码,并运行。
Java采用的方式: 混合型,先由编译器完全翻译成字节码,再由解释器逐行翻译成机器码,并运行。
为什么: 在90年代,受制于硬件的限制,编译型语言的速度快于解释型语言,但是编译型语言对于跨平台(不同的架构)要花额外的精力去修改,因此Java既有编译型语言运行快的优点,也有解释型语言“一次编写,到处运行”的优势。
从图上可以看出,不同架构的机器有自己的解释器,因此对程序员更友好,不用考虑太多跨平台的问题。
1.2 Java Kit开发工具
- JDK(Java Development Kit): 开发工具包,编写Java程序
- JRE(Java Runtime Environment): Java运行环境,用户基本运行程序的必要
1.3 第一个Java程式
- 编写源文件: 后缀.java
- 编译Java源代码:
javac First.java
得到字节码文件First.class
- 运行Java字节码:
java First
程序运行 - Java虚拟机负责解释运行class文件
2. 变量
2.1 什么是变量
数据类型用于指导计算机,该为这块内存分配多大的空间。
使用变量时:
- 声明变量,
int a;
- 初始化变量,
a = 10;
- 使用变量,
a = 200
需要注意;
- 未声明的变量不能使用
- 为初始化的变量不能使用
- 存入的数据必须符合声明的类型
2.2 基本数据类型
基本数据类型和它们的范围
类型 | bit位 | 范围 |
---|---|---|
byte | 8位 | -2ˆ7~2ˆ7-1 |
short | 2字节 | -2ˆ15~2ˆ15-1, 大概3万多 |
int | 4字节 | -2ˆ31~2ˆ31-1,大概21亿 |
long | 8字节 | -2ˆ63~2ˆ63-1 |
整型变量:
1.直接写出的值叫直接量
2.int直接量可直接赋值给long类型,long类型的直接量需要以l/L结尾
// 1.直接写出的值叫直接量 System.out.println(9); System.out.println(3.14); // 2.int // 直接写出的整数是int类型的直接量 int i = 123; // 赋值超出范围,编译报错 // i = 3000000000; // 运算超出范围,结果错误 i = 2147483647; // 0111... System.out.println(i + 1); // 1000... 输出: -2147483648 // 3.byte, short, long // int直接量可直接赋值给long类型 long l = 2; // long类型的直接量需要以l/L结尾 l = 3000000000L; // int直接量可直接赋值给byte,short,但不能超出其范围 byte b = 4; // b = 128; // b = i; short s = 2; // s = 32768; // s = i;
浮点型变量:
先将小数转为科学计数法的形式,再将一串二进制的整数分为三段,分别记录小数的符号,尾数,指数。
float:1位符号,8位指数,23位尾数 -3.4*10ˆ38 ~ 3.4*10ˆ38
double:1位符号,11位指数,52位尾数 -1.8*10ˆ308 ~ 1.8*10ˆ308
浮点数的进度不是无限的,是有误差的,如果业务需求是高精度计算,则需要用java.math.BigDecimal
这个包
// 4.float,double // 直接写出的小数是double类型的直接量 double d = 0.3; // float类型的直接量需要以f/F结尾 float f = 0.8F; // 浮点数不精确 System.out.println(300000.02f); System.out.println(300000.03f); System.out.println(3.3f + 0.1f); 输出: 300000.03; 300000.03; 3.3999999
字符型变量:
ASCII 表示256个字符
Unicode 可以表示 65535个字符,含括所有的东亚字符,java采用的是这种字符集
// 5.char char c1 = 'A'; char c2 = 0b01000001; // 65 char c3 = '\u0041'; //unicode(16进制) // 转义字符 char c4 = '\''; char c5 = '\t'; char c6 = '\\'; char c7 = '\n'; // 字符串 String str = "Hello World."; System.out.println(str);
布尔类型:
真假: true/false
2.3 基本类型的关系
数据的相互转换:
前提是: 除了布尔类型外,其余7种均为数字
关键是:与数据类型的范围有关
方式:
- 自动类型 (小变大,能装下)
- 强制转换类型 (大变小,可能会溢出)
// 1.自动类型转换 char c = 'A'; // 自动转换,因为int的范围比char大 int i = c; System.out.println(i); long l = 100L; double d = l; System.out.println(d); // 2.强制类型转换 int ii = 65; char cc = (char) ii; System.out.println(cc); double dd = 3.14; long ll = (long) dd; System.out.println(ll); // 3.运算时的自动类型转换 // 1)先将byte,short,char转为int // 2)再将int转换为更大的类型 char ccc = 'A'; int iii = 100; double ddd = 3.14; System.out.println(ccc + iii + ddd);
要注意的坑⚠️⚠️⚠️
// 注意如下的坑 byte b = 8; //(b-3)是表达式 // 如果不加(byte) 会报错: error: incompatible types: possible lossy conversion from int to byte b = (byte) (b - 3); // 下列情况是默认规则,不是类型转换 byte k = 5; short m = 6; char n = 7;
3. 运算符
3.1 运算符的分类
运算符的分类
- 算术运算符; 2. 关系运算符; 3. 逻辑运算符;
- 赋值运算符; 5. 三元运算符; 6. 字符串运算符; 7. 位运算符;
1.算术运算符
先看算术运算符
// ++a 先自增,后运算 int a = 100; System.out.println(++a + 100); System.out.println(a); 输出: 201, 101 // b++ 先运算,后自增 int b = 100; System.out.println(b++ + 100); System.out.println(b); 输出: 200, 101
关系运算符: >= == <=
等
3.逻辑运算符 和 短路现象
逻辑运算符: 与&&
或||
非!
, 都作用布尔类型数据上&& 或者 ||
存在短路现象⚠️ 当某个逻辑值可以直接推算出结果时,后续的表达式不会被执行
int age = 32; double salary = 40000.00; // &&, || 存在短路现象 // salary > 50000.00 为false 因此直接返回false System.out.println(salary > 50000.00 && ++age < 30); System.out.println(age); 输出: false 32 // salary > 30000.00 为true 因此直接返回true System.out.println(salary > 30000.00 || ++age < 30); System.out.println(age); 输出: true 32
4.赋值运算符
赋值运算符: = += -= *= /= %=
y=x=1
等价于 x =1; y=x;
5.三元运算符
一定是一个赋值表达式,要有返回值
// 判断是否成年 boolean isAdault = age < 18 ? "未成年" : "成年"; // 判断阿里巴巴工资,谁高于5万,就统一砍为5万,否则维持不变 double salary = salary > 50000.00 ? 50000.00 : salary;
6.字符串运算符
字符串 + 任意类型数据 --> 字符串
// 6.字符串运算符 String s1 = "她的年龄:" + age; System.out.println(s1); 返回: 她的年龄:32 String s2 = "她的工资:" + salary; System.out.println(s2); 返回: 她的工资:50000.0 System.out.println("" + 100 + 200); System.out.println(100 + 200 + ""); 返回: 100200 300
7.位运算符和补码的原理
在计算机内部,负数是以补码形式存储的
期望采用加法器电路,来实现减法运算
- 原码
最高位存储符号(0 - 正,1 - 负),其他位存数据的绝对值。 如:[-2]原 = 1000 0010
- 反码
[正数]反 = [正数]原,[负数]反 = [负数]原 除符号位外按位取反。 如:[-2]反 = 1111 1101
- 补码
[正数]补 = [正数]原,[负数]补 = [负数]反 + 1。 如:[-2]补 = 1111 1110
单例推演:
1 – 1 = 1 + (-1) = 0 原码:0000 0001 + 1000 0001 = 1000 0010 = -2 反码:0000 0001 + 1111 1110 = 1111 1111 = -0 补码:0000 0001 + 1111 1111 = 0000 0000 = 0
推理:
在时钟系统中,从六点到4点可以有两种拨法: 后退两格到4,或者前进10格到4
4 = 6 + (-2) = (6 + -2 + 12) mod 12 =(6 + 10) mod 12 12是这个系统的模,-2和10同余
因此在计算机系统中,比如在计算8位数据时, 0(0000 0000) ~ 255(1111 1111)
当从最大值加一后,就变成了0,因此256就是这个8位系统的模。
255 + (-3) = 255 + (256-3) mod 256 = (255 + 253) mod 256 = 508 mod 256 = 252 -3 的同余数就是 256+(-3) 其中-3 := 253(1111 1101)
在实际上,计算机里面都统一是正数,上述的补码系统是人为的一种简化计算体系,为了方便计算同余数,实际还是加法器的计算
注意⚠️其中的,有符号右移位操作,是符号位在最高位补齐!
移位运算的最小单位是 int,如果是byte移位也会自动转换成int型
java自带的十进制数转二进制
// 如何将十进制转换为二进制 System.out.println(Integer.toBinaryString(5));
位运算的规则和特殊技巧,参考我的博客
4. 输入和流程控制
4.1 Scanner接收数据
// -->引入 import java.util.Scanner // 创建对象 Scanner scan = new Scanner(System.in); // 接收数据 scan.nextInt(), scan.nextDouble(), scan.nextLine(), ... // 停止输入 scan.close()
举个例子
// 创建Scanner对象 Scanner scan = new Scanner(System.in); // 开始输入 System.out.println("请输入你的名字!"); String name = scan.nextLine(); System.out.println("请输入你的年龄!"); int age = scan.nextInt(); System.out.println("请输入你的学号!"); double salary = scan.nextDouble(); // 关闭Scanner scan.close();
4.2 if语句
分支语句
if (score >= 90) { System.out.println("A"); } else if (score >= 80) { System.out.println("B"); } else if (score >= 70) { System.out.println("C"); } else if (score >= 60) { System.out.println("D"); } else { System.out.println("E"); }
4.3 switch 语句
switch的语法还是有点生疏
switch (表达式) { case 值1: { ... break; } case 值2: { ... break; } default: { ... } }
注意⚠️switch里面的值是: byte, short, int, char, String, Enum.
,但不能是逻辑表达式
若不写break,只要有一个case命中,它就会顺序向下执行
switch (month) { case 1: case 2: case 3: System.out.println("第一季度"); break; case 4: case 5: case 6: System.out.println("第二季度"); break; case 7: case 8: case 9: System.out.println("第三季度"); break; case 10: case 11: case 12: System.out.println("第四季度"); break; default: System.out.println("错误的月份!"); }
4.4 if与switch对比
三元运算符 比较 if else:
三元运算符简洁但功能单一,if else里面可以放很多行,逻辑更多
if vs switch:
switch只能放表达式,只有数值匹配上才能执行分支,没有 if else 功能强大
5. 循环
- 循环条件
逻辑表达式,决定了是否执行循环体。 - 循环体
循环的主体,如果循环条件允许,该代码块将被重复执行。 - 迭代语句
改变循环条件中的变量,使得循环条件趋近于假,并最终为假,从而导致循环结束。
5.1 while 循环
求一个十进制数有多少位
int num = 7; // 记录整数的位数 int len = 0; // 循环到除尽为止 while (num != 0) { len++; num /= 10; System.out.println("len=" + len + ", num=" + num); } // 不可能小于一位,考虑到num=0的情况! len = len == 0 ? 1 : len;
5.2 do while 循环
先执行,再判断:
do{ something; } while(判断逻辑);
和while的区别就是,一定会先执行一次,再判断
// 循环到除尽为止, 不存在num=0的小bug do{ len++; num /= 10; System.out.println("len=" + len + ", num=" + num); } while (num != 0);
5.3 for循环
for (初始化语句; 循环条件; 迭代语句){ ... }
例子一: 求一个数的阶乘
注意⚠️零的阶乘是1,输入的数必须是自然数
if (n < 0) { System.out.println("请输入一个自然数!"); } else if (n == 0) { System.out.println("0! = 1"); } else { // 记录累乘结果 long s = 1; // i ∈ [1, n] for (int i = 1; i <= n; i++) { s *= i; } System.out.println(n + "! = " + s); }
例子二: 两个变量一起在for里面
for (int m = 0, n = 9; m < 10 && n > -1; m++, n--) { System.out.println("m = " + m + ", n = " + n); }
5.4 选择哪种循环
根据业务的需求,选择循环的方式,这里给出一个例子对比 do while 和 while循环
// 输入任意多个整数(负数代表输入结束), 求它们的平均数. Scanner scan = new Scanner(System.in); int n = 0; // 输入 int sum = 0; // 合计 int amount = 0; // 个数 do { n = scan.nextInt(); if (n >= 0) { sum += n; amount++; } } while (n >= 0); // 或者 n = scan.nextInt(); while (n >= 0) { sum += n; amount++; n = scan.nextInt(); } if (amount > 0) { double avg = (double) sum / amount; System.out.println("平均数: " + avg); } scan.close();
5.5 break 和 continue 关键字
break
的用法:
结束循环,强制跳出循环体。一旦遇到 break,系统将完全结束该循环,开始执循环之后的代码。
// 判断一个数,是否是质数 // 质数是,只能被自己或者1整除的数 if (n <= 1) { System.out.println("请输入一个大于1的正整数!"); } else { // 是否为质数 boolean b = true; // i ∈ [2, n-1] for (int i = 2; i < n; i++) { if (n % i == 0) { // 可以整除, 不是质数 b = false; // 得出结果, 跳出循环 break; } } System.out.println(n + (b ? "是质数" : "不是质数")); }
continue
的用法:
忽略本次循环剩下的语句,接着开始下一次循环,并不会终止循环。
// 求0-n的所有的奇数的和 if (n <= 1) { System.out.println("请输入一个大于1的正整数!"); } else { // 合计值 int sum = 0; // i ∈ [1, n] for (int i = 1; i <= n; i++) { if (i % 2 == 0) { continue; } sum += i; } System.out.println("合计值: " + sum); } }
5.6 嵌套循环
内层循环作为外层循环的循环体,
每次执行外层循环,内层循环都要完整的执行一遍。假设外层循环执行 m 次,内层循环执行 n 次,
则程序总的循环次数为二者的乘积关系,即 m*n 次。
// 1-100的所有的质数,请打印 for (int i = 2; i <= 100; i++) { // 判断i是不是质数 boolean b = true; // j ∈ [2, i-1] for (int j = 2; j < i; j++) { if (i % j == 0) { b = false; break; } } // 打印判断结果 if (b) { System.out.print(i + "\t"); }
5.7 死循环
1. 程序的漏洞
- 忘记写迭代语句;
- 写了迭代语句,但是迭代语句向着趋近于终止的相反方向发展;
double d = 1; while (d < 10) { System.out.println(d); // d 会越来越小,永远跳不出循环 d *= 0.1; } // 可以构造的死循环 while (true) { System.out.println("Hello."); } // 可以构造的死循环 for (; ; ) { System.out.println("Hello."); }
2. 刻意的营造
- 构造死循环,用以处理不确定次数的循环场景;
- 在循环体内增加判断,当条件达成时利用 break 关键字强制结束循环;
// 求平均数,用户输入数字,知道负数结束 Scanner scan = new Scanner(System.in); int n = 0; // 输入 int sum = 0; // 合计 int amount = 0; // 个数 while (true) { // scan.nextInt() 保证每次可以有输入 n = scan.nextInt(); if (n < 0) { break; } sum += n; amount++; } if (amount > 0) { double avg = (double) sum / amount; System.out.println("平均数: " + avg); } scan.close();
5.8 变量作用域
循环语句中变量的有效范围:
- 在代码块内部声明的变量,只能在代码块内部使用;
- 在代码块前面声明的变量,可以在代码块内部使用,也可在代码块后面使用;
- for 循环上声明的变量,只能在循环体内部使用;
- do while 循环体内部声明的变量,可以在循环体内部使用,不能在循环条件中使用;
int m = 0; while (m < 10) { int n = 3; System.out.print(m * n + "\t"); m++; } // 试图打印n,但是编译器无法解析,n的生存周期已经结束了 System.out.println(n);
6. 数组
6.1 数组和其遍历
数组的静态初始化,指明其存放的内容
type[] arrayName = {element1, element2, ...}; // 声明时初始化 int[] arr = {1,2,3}; //或者 int[] arr1 = new int[] {1,2,3};
数组的动态初始化,默认存放的是 整数类型默认值为 0, 浮点类型默认值为 0.0, 字符类型默认值为 ’\u0000’, 布尔类型默认值为 false, 引用类型默认值为 null。
// 默认存放都是0 int[] arr = new int[3];
数组的长度,可以通过它的属性, arraryName.length
访问得到
遍历数组的两种方式, for loop 和 foreach loop
// for loop for (int i = 0; i < array.length; i++) { System.out.println(array[i]); } // for each loop, 针对数组和集合 for (type variableName : array | collection) { System.out.println(variableName); }
6.2 数据的工具类
Arrays.copyOf
是属于浅拷贝,若原数组内存放的是基本类型,则是值传递,若内部存放的是对象,则是引用传递,修改一个对象会引起原数组对象的修改。
6.3 内存中的数组
在java中,数组是引用类型,是指向数组连续内存的首地址
int[] arr1 = {10, 20, 30, 40, 50}; // 引用类型赋值 int[] arr2 = arr1;
6.4 多维数组
初始化:
// 静态初始化 int[][] arr1 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; // 动态初始化 int[][] arr2 = new int[5][2];
打印多纬数组:
Arrays.deepToString(arr);
本质是一维数组,可以不是绝对的矩形:
// 本质是一维 int[][] arr3 = new int[3][]; arr3[0] = new int[]{1, 2}; arr3[1] = new int[]{3, 4, 5}; arr3[2] = new int[]{6, 7, 8, 9};
6.5 数组反转
不调用类方法,去底层实现数组反转
对称的交换可以减少一半的常数时间,相比于反序遍历
// 倒转数组 for (int i = 0; i < arr.length / 2; i++) { // arr[0] ~ arr[length-1-0] // arr[1] ~ arr[length-1-1] // arr[2] ~ arr[length-1-2] // arr[i] ~ arr[length-1-i] // tips: t = a; a = b; b = t; int temp = arr[i]; arr[i] = arr[arr.length - 1 - i]; arr[arr.length - 1 - i] = temp; }
6.6 数组求平均值
需求:
- 输入任意多个整数,直到输入负数为止;
- 计算这些整数的平均值,并打印出结果;
// 初始数组长度设为5 int[] arr = new int[5]; // 当前输入数字下标 int index = 0; // 循环输入,直到遇到-1停止 int sum = 0; int len = 0; double average = 0; Scanner sc = new Scanner(System.in); while (true){ int cur = sc.nextInt(); if(cur == -1){ sc.close(); average = len==0 ? 0 : (double)sum/len; break; } arr[index++] = cur; sum += cur; len++; // 数组扩容二倍 if(len == 5){ arr = Arrays.copyOf(arr, arr.length * 2); } }
6.7 数组冒泡排序
最基本的排序思想:
- 每一轮比较出一个最大(小)值,将其移动到数组最右(左)端;
- 对于长度为 N 的数组,需历经 N-1 轮才能完成所有的比较。
// 轮次: length -1 for (int i = 0; i < arr.length - 1; i++) { // 比较的次数 // 0: 0 ~ length - 1 // 1: 0 ~ length - 1 - 1 // 2: 0 ~ length - 1 - 2 // i: 0 ~ length - 1 - i // 循环的边界 // j = arr.length - 1 - i - 1 // j + 1 = arr.length - 1 - i for (int j = 0; j < arr.length - 1 - i; j++) { // 从大到小: 将小的数从左向右转移. if (arr[j] < arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } }
7. 方法
7.1 参数
函数的参数分为,基本类型参数 和 引用类型参数.
在main函数中,程序执行是由栈帧控制的,swapNum
传入的是基本类型,在各自的栈帧内会为a和b分配内存空间,一个函数的操作不会影响另一个。
在函数swapArray
中传入的是引用类型的参数,实际传入的是引用,指向相同的内存空间,因此会相互影响修改。
public static void main(String[] args) { int a = 1; int b = 2; swapNum(a, b); System.out.println("a = " + a + ", b = " + b); int[] arr = {1, 2, 3, 4, 5}; swapArray(arr); System.out.println("arr = " + Arrays.toString(arr)); } // 交换两个数 public static void swapNum(int m, int n) { int t = m; m = n; n = t; System.out.println("m = " + m + ", n = " + n); } // 交换数组首尾的两个数 public static void swapArray(int[] array) { if (array == null || array.length < 2) { return; } int t = array[0]; array[0] = array[array.length - 1]; array[array.length - 1] = t; }
7.2 可变参数
什么是可变参数
- 在定义方法时,可以声明数量不确定的参数,这样的参数叫可变的参数;
- 一个方法最多声明一个可变参数,并且该参数必须位于参数列表的末尾;
- 可变参数的本质是一个数组,调用时可以分开传入多个值,也可以直接传入一个数组。
◼ 如何声明可变参数
修饰符 返回值类型 方法名(类型 参数1, 类型 参数2, ..., 类型... 参数N) { }
public static void main(String[] args) { System.out.println(sum()); System.out.println(sum(1)); System.out.println(sum(1, 2)); System.out.println(sum(1, 2, 3)); System.out.println(sum(new int[]{1, 2, 3, 4, 5})); } public static int sum(int... nums) { int s = 0; for (int num : nums) { s += num; } return s; }
7.3 方法重载
◼ 什么是方法重载
在同一个类里,定义多个名称相同、参数列表不同的方法,叫做方法重载。
若只是参数名字不一样不是方法重载,参数名字和类型一样但返回值类型不一样不是方法重载
◼ 方法重载的作用
对于调用者而言,多个重载的方法就像是一个方法,便于记忆、便于调用。
总结:
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
参考资料
如: System.out.println(), Arrays.toString(), Math.abs() 都是方法重载
// java中Arrays.toString()的源码,针对不同类型的参数,重载了toString这个方法 public static String toString(int[] a) { if (a == null) return "null"; int iMax = a.length - 1; if (iMax == -1) return "[]"; StringBuilder b = new StringBuilder(); b.append('['); for (int i = 0; ; i++) { b.append(a[i]); if (i == iMax) return b.append(']').toString(); b.append(", "); } } public static String toString(float[] a) { if (a == null) return "null"; int iMax = a.length - 1; if (iMax == -1) return "[]"; StringBuilder b = new StringBuilder(); b.append('['); for (int i = 0; ; i++) { b.append(a[i]); if (i == iMax) return b.append(']').toString(); b.append(", "); } }
7.4 调试程序
idea里面设置断点,调试程序,常用的快捷方式
执行下一步,调入方法内部,执行到下个断点