Java 零基础入门学习(小白也能看懂!)一https://developer.aliyun.com/article/1494847
3. 运算符
3.1 什么是运算符
计算机的最基本的用途之一就是执行数学运算,比如:
int a = 10; int b = 20; a + b; a < b;
上述 + 和 < 等就是运算符,即:对操作数进行操作时的符号,不同运算符操作的含义不同。
作为一门计算机语言,Java也提供了一套丰富的运算符来操纵变量。Java中运算符可分为以下:算术运算符(+ - * /)、关系运算符(< > ==)、逻辑运算符、位运算符、移位运算符以及条件运算符等
3.2 算术运算符
- 基本四则运算:加减乘除求模(
+
,-
,*
,/
,%
)
int a = 20; int b = 10; System.out.println(a + b); // 30 System.out.println(a - b); // 10 System.out.println(a * b); // 200 System.out.println(a / b); // 2 System.out.println(a % b); // 0 --->模运算相当于数学中除法的余数
【注意】:
- 都是二元运算符,使用时必须要有左右两个操作数
int
/int
结果还是int类型,而且会向下取整
int a = 3; int b = 2; // 在数学中应该是1.5 但是在Java中输出结果为1 会向下取整,即小数点之后全部舍弃掉了 System.out.println(a / b); // 1 // 如果要得到数学中的结果,可以使用如下方式 double d = a * 1.0 / b; System.out.println(d);// 1.5
- 做除法和取模时,右操作数不能为0
- % 不仅可以对整型取模,也可以对double类型取模,但是没有意义,一般都是对整型取模的
System.out.println(11.5 % 2.0);// 1.5
两侧操作数类型不一致时,向类型大的提升
System.out.println(1 + 0.2); // +的左侧是int,右侧是double,在加之前int被提升为double // 1.2
结合赋值和运算符(+=
,-=
,*=
,/=
,%=
)
该种类型运算符操作完成后,会将操纵的结果赋值给左操作数.
int a = 1; a += 2; // 相当于 a = a + 2 System.out.println(a); // 输出3 a -= 1;// 相当于 a = a - 1 System.out.println(a); // 输出2 a *= 3;// 相当于 a = a * 3 System.out.println(a); // 输出6 a /= 3;// 相当于 a = a / 3 System.out.println(a); // 输出2 a %= 3; // 相当于 a = a % 2 System.out.println(a); // 输出2
- 【注意】:只有变量才能使用该运算符,常量不能使用。
- 自增/自减运算符(
++
,--
)++是一种自增的操作符,又分为前置++和后置++,–是一种自增的操作符,又分为前置–-和后置–-。
- 前置
++
public class Test1 { public static void main(String[] args) { int a = 10; int b = ++a;// ++的操作数是a,是放在a的前面的,就是前置++ System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 11 , b = 11 } }
计算口诀:先 +1,后使用
a原来是10,先 +1,后a变成了11,再使用赋值给b,b得到的也是11,所以计算后,a和b都是11,等价于这样的代码:
public class Test1 { public static void main(String[] args) { int a = 10; a += 1; int b = a; System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 11 , b = 10 } }
- 后置
++
public class Test1 { public static void main(String[] args) { int a = 10; int b = a++;// ++的操作数是a,是放在a的后面的,就是后置++ System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 11 , b = 10 } }
计算口诀:先使用,后 +1
a原来是10,先使用,把a赋值给b,b变成了10,后a+1变成了10,所以计算后,a=11,b=10,等价于这样的代码:
public class Test1 { public static void main(String[] args) { int a = 10; int b = a; a += 1; System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 11 , b = 10 } }
- 前置
--
和前置++同理,只是换成了-1
计算口诀:先 -1,后使用
public class Test1 { public static void main(String[] args) { int a = 10; int b = --a;// --的操作数是a,是放在a的前面的,就是前置-- System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 9 , b = 9 } }
- 后置
--
和后置++同理,只是换成了-1
计算口诀:先使用,后-1
public class Test1 { public static void main(String[] args) { int a = 10; int b = a--;// --的操作数是a,是放在a的后面的,就是后置-- System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 9 , b = 10 } }
【注意】:
- 如果混合使用,【前置++】先+1,然后使用变量+1之后的值,【后置++】先使用变量原来的值,表达式 结束时给变量+1
- 只有变量才能使用自增/自减运算符,常量不能使用,因为常量不允许被修改
3.3 关系运算符
关系远算符有6个:(==
,!=
,>
,<
,>=
,<=
),其结果是true
和flase
int a = 10; int b = 20; // 注意:在Java中 = 表示赋值,要与数学中的含义区分 //在Java中 == 表示相等 System.out.println(a == b); // false System.out.println(a != b); // true System.out.println(a < b); // true System.out.println(a > b); // false System.out.println(a <= b); // true System.out.println(a >= b); // false
【注意】:当需要多次判断时,不能连着写,比如:3 < a < 5,Java程序与数学中是有区别的
3.4 逻辑运算符(重点)
逻辑远算符主要由3个:(&&
,||
,!
),运算结果都是 boolean
类型。
逻辑与 &&
语法规则:表达式1 && 表达式2,左右表达式必须是boolean类型的结果。两个表达式都为真,结果才是真,只要有一个是假,结果就是假。
表达式1 | 表达式2 | 结果 |
真 | 真 | 真 |
真 | 假 | 假 |
假 | 真 | 假 |
假 | 假 | 假 |
int a = 1; int b = 2; System.out.println(a == 1 && b == 2); // 左为真 且 右为真 则结果为真 System.out.println(a == 1 && b > 100); // 左为真 但 右为假 则结果为假 System.out.println(a > 100 && b == 2); // 左为假 但 右为真 则结果为假 System.out.println(a > 100 && b > 100); // 左为假 且 右为假 则结果为假
逻辑或 ||
语法规则:表达式1
|| 表达式2
,左右表达式必须是boolean
类型的结果。
两个表达式都为假,结果才是假,只要由一个是真,结果就是真。
表达式1 | 表达式2 | 结果 |
真 | 真 | 真 |
int a = 1; int b = 2; System.out.println(a == 1 || b == 2); // 左为真 且 右为真 则结果为真 System.out.println(a == 1 || b > 100); // 左为真 但 右为假 则结果也为真 System.out.println(a > 100 || b == 2); // 左为假 但 右为真 则结果也为真 System.out.println(a > 100 || b > 100); // 左为假 且 右为假 则结果为假
逻辑非 !
语法规则:! 表达式
真变假,假变真。
表达式 | 结果 |
真 | 假 |
假 | 真 |
int a = 1; System.out.println(!(a == 1)); // a == 1 为true,取个非就是false System.out.println(!(a != 1)); // a != 1 为false,取个非就是true
短路求值
&& 和 || 遵守短路求值的规则.
System.out.println(10 > 20 && 10 / 0 == 0); // 打印 false System.out.println(10 < 20 || 10 / 0 == 0); // 打印 true
【注意】:
对于 && , 如果左侧表达式值为 false, 则表达式结果一定是 false, 无需计算右侧表达式
对于 ||, 如果左侧表达式值为 true, 则表达式结果一定是 true, 无需计算右侧表达式.
& 和 | 如果表达式结果为 boolean 时, 也表示逻辑运算. 但与 && || 相比, 它们不支持短路求值.
记忆口诀:
&&:全真为真,有假必假,遇假则停
||:全假为假,有真必真,遇真则停
!:真变假,假变真
3.5 位运算符
Java 中数据存储的最小单位是字节,而数据操作的最小单位是比特位. 字节是最小的存储单位,每个字节是由8个二进制比特位组成的,多个字节组合在一起可以表示各种不同的数据。
位运算符主要有四个:&
,|
, ~
, ^
,除~
是一元运算符外,其余都是二元运算符。
位操作表示 按二进制位运算. 计算机中都是使用二进制来表示数据的(01构成的序列), 按位运算就是在按照二进制位 的每一位依次进行计算。
按位与 &: 如果两个二进制位都是 1, 则结果为 1, 否则结果为 0
int a = 10; int b = 20; System.out.println(a & b); // 0000 1010 10的二进制 // 0001 0100 20的二进制 // 0000 0000
按位或 |: 如果两个二进制位都是 0, 则结果为 0, 否则结果为 1
int a = 10; int b = 20; System.out.println(a | b); // 0000 1010 // 0001 0100 // 0001 1110
.按位取反 ~: 如果该位为 0 则转为 1, 如果该位为 1 则转为 0
int a = 0xf; System.out.printf("%x\n", ~a);
【注意】:
- 0x 前缀的数字为 十六进制 数字. 十六进制可以看成是二进制的简化表示方式. 一个十六进制数字对应 4 个二进制位.
- 0xf 表示 10 进制的 15, 也就是二进制的 1111
- printf 能够格式化输出内容, %x 表示按照十六进制输出.
- \n 表示换行符
按位异或 ^: 如果两个数字的二进制位相同, 则结果为 0, 相异则结果为 1
int a = 0x1; int b = 0x2; System.out.printf("%x\n", a ^ b);
- 【注意】:如果两个数相同,则异或的结果为0
3.6 移位运算符(了解)
移位运算符有三个:<<
,>>
,>>>
,都是二元运算符,且都是按照二进制比特位来运算的。
- 左移 <<: 最左侧位不要了, 最右侧补 0
int a = 0x10; System.out.printf("%x\n", a << 1); // 运行结果(注意, 是按十六进制打印的) //20
- 【注意】:向左移位时,丢弃的是符号位,因此正数左移可能会编程负数
- 右移 >>: 最右侧位不要了, 最左侧补符号位(正数补0, 负数补1)
int a = 0x10; System.out.printf("%x\n", a >> 1); // 运行结果(注意, 是按十六进制打印的) //8 int b = 0xffff0000; System.out.printf("%x\n", b >> 1); // 运行结果(注意, 是按十六进制打印的) //ffff8000
无符号右移 >>>: 最右侧位不要了, 最左侧补 0.
int a = 0xffffffff; System.out.printf("%x\n", a >>> 1); // 运行结果(注意, 是按十六进制打印的) //7ffffff
【注意】:
左移 1 位, 相当于原数字 * 2. 左移 N 位, 相当于原数字 * 2 的N次方.
右移 1 位, 相当于原数字 / 2. 右移 N 位, 相当于原数字 / 2 的N次方.
由于计算机计算移位效率高于计算乘除, 当某个代码正好乘除 2 的N次方的时候可以用移位运算代替.
移动负数位或者移位位数过大都没有意义.
3.7 条件运算符
条件运算符只有一个: 表达式1 ? 表达式2 : 表达式3
当 表达式1 的值为true
时, 整个表达式的值为 表达式2
的值;
当 表达式1 的值为 fals
e 时, 整个表达式的值为 表达式3
的值.
也是 Java 中唯一的一个 三目运算符, 是条件判断语句的简化写法.
// 求两个整数的最大值 int a = 10; int b = 20; int max = a > b ? a : b;
【注意】:
- 表达式2和表达式3的结果要是同类型的,除非能发生类型隐式类型转换
int a = 10; int b = 20; int c = a > b? 1 : 2.0;
- 表达式不能单独存在,其产生的结果必须要被使用。
3.8 运算符的优先级
在一条表达式中,各个运算符可以混合起来进行运算,但是运算符的优先级不同,比如:*
和 /
的优先级要高于 +
和-
,有些情况下稍不注意,可能就会造成很大的麻烦。
// 求a和b的平均值 int a = 10; int b = 20; int c = a + (b - a) >> 1; System.out.println(c);
上述表达式中,由于 +
的优先级要高于 >>
, 因此a
先和b-a
的结果做加法,整体为20,最后再进行右移,因此结果 为10。
【注意】:运算符之间是有优先级的. 具体的规则我们不必记忆. 在可能存在歧义的代码中加上括号即可.
4. 程序逻辑控制
4.1 顺序结构
顺序结构比较简单,按照代码书写的顺序一行一行执行。
public class Test { public static void main(String[] args) { System.out.println("aaa"); System.out.println("bbb"); System.out.println("ccc"); /* 运行结果 aaa bbb ccc */ } }
如果调整代码的书写顺序, 则执行顺序也发生变化。
public class Test { public static void main(String[] args) { System.out.println("aaa"); System.out.println("ccc"); System.out.println("bbb"); /* 运行结果 aaa ccc bbb */ } }
4.2 分支结构(选择结构)
它的作用是根据判断的条件是否成立(真或假),来决定后续代码执行顺序。
举例:
如同在岔路口做选择。不同的选择会带来不同的路径及结果。
4.2.1 if 语句
- 语法格式1
if (布尔表达式) { // 语句 }
如果布尔表达式结果为true
,执行if
中的语句,否则不执行。
执行流程:
举例:小明,如果这次考试考60分或以上就不挂科。
public class Test { public static void main(String[] args) { int score = 90; if (score >= 60) { System.out.println("不挂科"); } } }
语法格式2
if (布尔表达式) { // 语句 } else { // 语句 }
如果布尔表达式结果为true
,执行if
中的语句,执行else
中的语句。
执行流程:
举例:小明,如果这次考试考60分或以上就不挂科,没有考到60分就挂科。
public class Test { public static void main(String[] args) { int score = 90; if (score >= 60) { System.out.println("不挂科"); } else { System.out.println("挂科"); } } }
语法格式3
if(布尔表达式1){ // 语句1 }else if(布尔表达式2){ // 语句2 }else{ // 语句2 }
表达式1成立,执行语句1,否则表达式2成立,执行语句2,否则执行语句3
比如:考虑到学生自尊,不公开分数排名,因此:
分数在 [90, 100] 之间的,为优秀
分数在 [80, 90) 之前的,为良好 分数在 [70, 80) 之间的,为中等
分数在 [60, 70) 之间的,为及格 分数在 [ 0, 60) 之间的,为不及格
错误数据按照上述办法通知学生成绩。
public class Test { public static void main(String[] args) { int score = 90; if(score >= 90){ System.out.println("优秀"); }else if(score >= 80 && score < 90){ System.out.println("良好"); }else if(score >= 70 && score < 80){ System.out.println("中等"); }else if(score >= 60 && score < 70){ System.out.println("及格"); }else if(score >= 0 && score < 60){ System.out.println("不及格"); }else{ System.out.println("错误数据"); } } }
【注意事项】:
- 代码风格
// 风格1-----> 推荐 int x = 10; if (x == 10) { // 语句1 } else { // 语句2 } // 风格2 int x = 10; if (x == 10) { // 语句1 } else { // 语句2 }
- 虽然两种方式都是合法的, 但是 Java 中更推荐使用风格1,代码跟紧凑。
- 分号问题
int x = 20; if (x == 10); { System.out.println("hehe"); } // 运行结果 hehe
- 此处多写了一个 分号, 导致分号成为了
if
语句的语句体, 而 { } 中的代码已经成为了和一个 if 无关的代码块,所以运行结果是haha
,而不是空白。 - 悬垂
else
问题
int x = 10; int y = 10; if (x == 10) if (y == 10) System.out.println("aaa"); else System.out.println("bbb");
if else
语句中可以不加大括号 . 但是也可以写语句(只能写一条语句). 此时else
是和最接近的if
匹配. 但是实际开发中我们不建议这么写. 最好加上大括号.
4.2.2 switch 语句
基本语句
switch(表达式){ case 常量值1:{ 语句1; [break;] } case 常量值2:{ 语句2; [break;] } ... default:{ 内容都不满足时执行语句; [break;] } }
执行流程:
- 先计算表达式的值
- 和
case
依次比较,一旦有响应的匹配就执行该项下的语句,直到遇到break
时结束 - 当表达式的值没有与所列项匹配时,执行
default
代码示例:
public class Test { public static void main(String[] args) { String week = "周四"; switch (week) { case "周一": System.out.println("埋头苦干,写程序"); break; case "周二": System.out.println("请求学长帮忙解决bug"); break; case "周三": System.out.println("今晚烧烤、小龙虾"); break; case "周四": System.out.println("帮助学妹解决bug"); break; case "周五": System.out.println("今晚吃鸡"); break; case "周六": System.out.println("上GitHub交友"); break; case "周日": System.out.println("郁郁寡欢、准备上课"); default: System.out.println("输入错误"); } } }
【注意事项】:
- 多个
case
后的常量值不可以重复 switch
的括号内只能是以下类型的表达式:
- 基本类型:byte、char、short、int,注意不能是long类型
- 引用类型:String常量串、枚举类型
break
不要遗漏, 否则会失去 “多分支选择” 的效果
int day = 1; switch(day) { case 1: System.out.println("星期一"); // break; case 2: System.out.println("星期二"); break; } // 运行结果 星期一 星期二
switch
不能表达复杂的条件
// 例如: 如果 num 的值在 10 到 20 之间, 就打印 hehe // 这样的代码使用 if 很容易表达, 但是使用 switch 就无法表示. if (num > 10 && num < 20) { System.out.println("hehe"); }
switch
虽然支持嵌套, 但是很丑,一般不推荐
public class Test { public static void main(String[] args) { int x = 1; int y = 1; switch(x) { case 1: switch(y) { case 1: System.out.println("hehe"); break; } break; case 2: System.out.println("haha"); break; } } }
综上, 我们发现, switch
的使用局限性是比较大的
4.3 循环结构
循环结构它是在满足条件的情况下,反复执行某一段代码的计算过程。
举例:
我们要围着操场跑 5 圈,跑圈这个行为就重复了 5 次,也就是循环了 5 次。
4.3.1 while 语句
基本格式:
while (循环条件) { // 语句 }
执行流程:
首先执行判断表达式,表达式的值为false
,循环直接结束;表达式的值为true
,则执行循环语句,语句执行完后再继续判断,是否进行下一次判断。
代码示例:打印1~10的值
public class Test { public static void main(String[] args) { int i = 1; while (i <= 10) { System.out.println(i); i++; } } }
4.3.2 for 语句
基本格式:
for (表达式1;表达式2;表达式3) { // 语句 }
表达式1:用于循环变量的初始化
表达式2:用于循环结束条件的判断
表达式3:用于循环变量的调整
执行流程:
首先执行表达式1初始化循环变量,接下来就是执行表达式2的判断部分,表达式2的结果如果为false,则循环结束;如果表达式2的结果为true,则执行循环语句,循环语句执行完后,再去执行表达式3,调整循环变量,然后再去表达式2的地方执行判断,表达式2的结果是否为false,决定循环是否继续。
整个循环的过程中,表达式1初始化部分只被执行1次,剩下的就是表达式2、循环语句、表达式3在循环
代码示例:打印1~10的值
public class Test { public static void main(String[] args) { for (int i = 1; i <= 10; i++) { System.out.println(i); } } }
基本格式:
do{ 语句 }while(表达式);
while 和 for 这两种循环都是先判断,条件如果满足就进入循环,执行循环语句,如果不满足就跳出循环;
而do...while 循环则是先直接进入循环体,执行循环内部,然后在执行 while 后的判断表达式,表达式为真,就会进行下一次,表达式为false,则不再继续循环。
执行流程:
在do...while 循环中先执行图上的“语句”,执行完语句,在去执行“判断表达式”,在判断表达式的结果是true,则继续循环,执行循环;判断表达式的结果false,则循环结束。
所以在do...while 语句中循环体是至少执行一次的,这是do...while 循环比较特殊的地方。
代码示例:打印1~10的值
public class Test { public static void main(String[] args) { int i = 1; do { System.out.println(i); i++; } while (i <= 10); } }
4.3.4 while语句 和 for语句的对比
4.4 break和continue
在循环执行的过程中,如果某些状况发生的时候,需要提前终止循环。
break
的作用是用于永久的终止循环,只要break
被执行,直接就会跳出循环,继续往后执行。
代码示例:找到100~200之间的第一个3的倍数
public class Test { public static void main(String[] args) { for (int num = 100; num <= 200; num++) { if (num % 3 == 0) { System.out.println(num); break; } } } } // 运行结果 102
continue
的作用是跳出本次循环 continue
后边的代码
代码示例:找到100~200中的所有3的倍数
public class Test { public static void main(String[] args) { for (int num = 100; num <= 200; num++) { if (num % 3 == 0){ System.out.println(num); continue; } } } }
5. 方法
5.0 前言
在编程的过程中,经常会出现一部分代码多次使用的情况,比如计算多边形面积,输出固定格式的文字等。
今天我们就来学习方法。
5.1 方法的概念和使用
5.1.1 什么是方法
方法就是一个代码片段,类似于C语言的“函数”。
方法存在的意义:
- 是能够模块化的组织代码(当代码规模比较复杂的时候)
- 做到代码被重复使用, 一份代码可以在多个位置使用.
- 让代码更好理解更简单.
- 直接调用现有方法开发, 不必重复造轮子
5.1.2 方法的定义
方法的语法格式:
修饰符 返回值类型 方法名称(形参列表){ // 方法体 return 返回值; }
代码示例:实现一个两个整数相加的方法
public static int add(int a, int b) { int c = a + b; return c; }
【注意事项】:
修饰符:现阶段直接使用public static 固定搭配
返回值类型:如果方法有返回值,返回值类型必须要与返回的实体类型一致,如果没有返回值,必须写成
void
方法名字:采用小驼峰命名
参数列表:如果方法没有参数,()中什么都不写,如果有参数,需指定参数类型,多个参数之间使用逗号隔开
方法体:方法内部要执行的语句
在 Java当中,方法必须写在类当中
在 Java当中,方法不能嵌套定义
在 Java当中,没有方法声明一说
5.1.3 方法调用的过程
方法调用过程:
调用方法—>传递参数—>找到方法地址—>执行被调方法的方法体—>被调方法结束返回—>回到主调方法继续往下执行
【注意事项】
- 定义方法的时候, 不会执行方法的代码. 只有调用的时候才会执行.
- 一个方法可以被多次调用
代码示例:计算两个整数相加
public class Main { public static void main(String[] args) { int x = 10; int y = 20; int ret = add(x, y); System.out.println(ret); } public static int add(int a, int b) { int c = a + b; return c; } }
方法调用过程:
DeBug
- 内存图
方法是放在方法区中的,被调用的时候,需要进入到栈内存中运行
- 一旦程序遇到
return
或者方法执行结束,就会把当前方法栈帧就从栈上进行销毁(回收)
5.1.4 形参和实参(重要)
方法的形参相当于数学函数中的自变量,比如:1 + 2 + 3 + … + n的公式为sum(n) =(1 + n) * n / 2
Java中方法的形参就相当于sum函数中的自变量n,用来接收sum函数在调用时传递的值的。形参的名字可以随意取,对方法都没有任何影响,形参只是方法在定义时需要借助的一个变量,用来保存方法在调用时传递过来的值。
public class Main { public static void main(String[] args) { getSum(10); // 10是实参,在方法调用时,形参n用来保存10 getSum(100); // 100是实参,在方法调用时,形参n用来保存100 } private static int getSum(int n) { // n 是形参 return (1 + n) * n / 2; } }
再比如:
public class Main { public static void main(String[] args) { add(2,3); // 2 和 3 是实参,在调用时传给形参a 和 b } public static int add(int a, int b) { return a + b; } }
5.1.5 没有返回值的方法
方法的返回值是可选的. 有些时候可以没有的,没有时返回值类型必须写成void
代码示例:
public class Main { public static void main(String[] args) { int a = 10; int b = 20; print(a,b); } private static void print(int x, int y) { System.out.println("x = " + x + ",y = " + y); } }
return
问题
- 如果没有返回值的方法要写
return
,则return
后面不能加任何返回值。
return
后面的语句不会被执行,return
表示方法的结束
5.2 方法的重载
5.2.1 为什么需要方法的重载
由于参数类型不匹配, 所以不能直接使用现有的 add
方法.
一种比较简单粗暴的解决方法如下:
public class Main { public static void main(String[] args) { int x = 10; int y = 20; System.out.println(addInt(x,y)); double a = 1.1; double b = 1.2; System.out.println(addDouble(a,b)); } public static int addInt(int a, int b) { return a + b; } public static double addDouble(double a, double b) { return a + b; } }
上述代码确实可以解决问题,但不友好的地方是:需要提供许多不同的方法名,而取名字本来就是让人头疼的事情。那能否将所有的名字都给成 add
呢?
5.2.2 方法重载的概念
在Java中,如果多个方法的名字相同,参数列表不同,则称该几种方法被重载了。
public class Main { public static void main(String[] args) { int x = 10; int y = 20; System.out.println(add(x,y));// 调用add(int, int) double a = 1.1; double b = 1.2; System.out.println(add(a,b));// 调用add(double, double) double c = 1.3; System.out.println(add(a,b,c));// 调用add(double, double,double) } public static int add(int a, int b) { return a + b; } public static double add(double a, double b) { return a + b; } public static double add(double a, double b, double c) { return a + c; } }
注意:
- 方法名必须相同
- 参数列表必须不同(参数的个数不同、参数的类型不同、类型的次序必须不同)
- 与返回值类型是否相同无关
5.2.3 方法签名
在同一个作用域中不能定义两个相同名称的标识符。比如:方法中不能定义两个名字一样的变量,那为什么类中就可以定义方法名相同的方法呢?
方法签名即:经过编译器编译修改过之后方法最终的名字。具体方式:方法全路径名+参数列表+返回值类型,构成方法完整的名字。
5.3 递归
5.3.1 生活中的例子
从前有坐山,山上有座庙,庙里有个老和尚给小和尚将故事,讲的就是:
"从前有座山,山上有座庙,庙里有个老和尚给小和尚讲故事,讲的就是:
“从前有座山,山上有座庙…”
“从前…”
上面的两个例子有个共同的特征:自身中又包含了自己,该种思想在数学和编程中非常有用,因为有些时候,我们遇到的问题直接并不好解决,但是发现将原问题拆分成其子问题之后,子问题与原问题有相同的解法,等子问题解决之后,原问题就迎刃而解了。
5.3.2 递归的概念
一个方法在执行过程中调用自身, 就称为 “递归”.
递归相当于数学上的 “数学归纳法”, 有一个起始条件, 然后有一个递推公式.
例如, 我们求 N!
起始条件: N = 1 的时候, N! 为 1. 这个起始条件相当于递归的结束条件.
递归公式: 求 N! , 直接不好求, 可以把问题转换成 N! => N * (N-1)!
递归的必要条件:
将原问题划分成其子问题,注意:子问题必须要与原问题的解法相同
递归出口代码示例
public class Main { public static void main(String[] args) { fun(); } public static void fun() { fun(); } }
上述代码就是一个最简单的递归。
但是存在错误:出来栈溢出错误的时候,就说明结束条件不对或者没有结束条件
代码示例:递归求 N 的阶乘
public class Main { public static void main(String[] args) { System.out.println(factor(5)); // 120 } public static int factor(int n) { if (n == 1) { return 1; } return factor(n - 1) * n; // factor()方法调用自己 } }
5.3.3 递归执行过程分析
递归的程序的执行过程不太容易理解, 要想理解清楚递归, 必须先理解清楚 “方法的执行过程”, 尤其是 "方法执行结束之后, 回到调用位置继续往下执行.
代码示例:递归求 N 的阶乘
public class Main { public static void main(String[] args) { System.out.println(factor(5)); } public static int factor(int n) { System.out.println("函数开始, n = " + n); if (n == 1) { System.out.println("函数结束, n = 1 ret = 1"); return 1; } int ret = n * factor(n - 1); System.out.println("函数结束, n = " + n + " ret = " + ret); return ret; } } /* 函数开始, n = 5 函数开始, n = 4 函数开始, n = 3 函数开始, n = 2 函数开始, n = 1 函数结束, n = 1 ret = 1 函数结束, n = 2 ret = 2 函数结束, n = 3 ret = 6 函数结束, n = 4 ret = 24 函数结束, n = 5 ret = 120 120 */
执行图:
6. 数组
6.1 数组的基本概念
6.1.1 为什么使用数组?
假设现在要存储5个学生的年龄,按照之前掌握的知识点,我们会写出如下代码:声明5个变量存储学生变量
pubpublic class Test { public static void main(String[] args) { int age1; int age2; int age3; int age4; int age5; } }
如果我们有10个学生呢?我们就要声明20个变量,似乎没有什么问题。那如果有100,1000个学生呢,我们就要声明100,1000个变量,这样就有点离谱了,使用数组我们就可以解决一个问题。
6.1.2 什么是数组
数组,是指一组类型相同的数据的集合,数组中每个数据称为元素。数组可以存放任意类型的元素,但同一个数组里存放的元素类型必须一致。数组分为一维数组和多维数组。
数组在内存中是一段连续的空间,比如现实中的车库:
在 Java中,包含6个整形类型元素的数组,就相当于上图中连在一起的6个车位,从上图中可以看到:
- 数组中存放的元素其类型相同
- 数组的空间是连在一起的
- 每个空间有自己的编号,起始位置的编号为0,即数组的下标。
6.1.3 数组的创建和初始化
6.1.3.1 数组的创建
基本语法格式:
T[] 数组名 = new T[N];
T
:表示数组中存放元素的类型T[]
:表示数组类型N
:表示数组的长度
代码示例:存储10个人的年龄
int[] ages = new int[10];
6.1.3.2 数组的初始化
Java 数组初始化主要分为静态初始化以及动态初始化
- 动态初始化:在创建数组时,直接指定数组中元素的个数
int[] ages = new int[10];
动态初始化:在创建数组是不直接指定数据元素个数,而直接讲具体的数据内容进行指定
语法格式:
T[] 数组名 = {data1,data2,....data};
int[] ages = new {1,2,3,4,5};
【注意事项】
- 静态初始化虽然没有指定数组的长度,编译器在编译时会根据{}中元素个数来确定数组的长度。
- 静态初始化时, {}中数据类型必须与[]前数据类型一致。
- 静态初始化可以简写,省去后面的new T[]。
int[] arr = {1,3,2,5,4}; // 注意:虽然省去了new T[], 但是编译器编译代码时还是会还原
数组也可以按照如下C语言个数创建,不推荐
int arr[] = {1, 2, 3}; /* 该种定义方式不太友好,容易造成数组的类型就是int的误解 []如果在类型之后,就表示数组类型,因此int[]结合在一块写意思更清晰 */
静态和动态初始化也可以分为两步,但是省略格式不可以。
public class Main { public static void main(String[] args) { int[] array1; array1 = new int[10]; int[] array2; array2 = new int[]{10, 20, 30}; // 注意省略格式不可以拆分, 否则编译失败 //int[] array3; //array3 = {1, 2, 3}; } }
-
- 如果没有对数组进行初始化,数组中元素有其默认值
- 如果数组中存储元素类型为基类类型,默认值为基类类型对应的默认值,比如:
类型 | 默认值 |
byte | 0 |
short | 0 |
int | 0 |
long | 0 |
float | 0.0f |
double | 0.0 |
char | /u0000 |
boolean | false |
- 如果数组中存储元素类型为引用类型,默认值为
null
6.1.4 数组的使用
6.1.4.1 数组中元素访问
数组在内存中是一段连续的空间,空间的编号都是从0开始的,依次递增,该编号称为数组的下标,数组可以通过下标访问其任意位置的元素。比如:
public class Main { public static void main(String[] args) { int[] arr = new int[]{1,2,3,4,5}; System.out.println(arr[0]); System.out.println(arr[1]); System.out.println(arr[2]); System.out.println(arr[3]); System.out.println(arr[4]); } }
【注意事项】:
- 数组是一段连续的内存空间,因此支持随机访问,即通过下标快速访问数组中任意位置的元素
- 下标从0开始,介于[0,N) 之间不包含N,N为元素个数,不能越界,否则会报出下标越界异常。
6.1.4.2 遍历数组
所谓 “遍历” 是指将数组中的所有元素都访问一遍, 访问是指对数组中的元素进行某种操作,比如:打印。
public class Main { public static void main(String[] args) { int[] arr = new int[]{1,2,3,4,5}; System.out.println(arr[0]); System.out.println(arr[1]); System.out.println(arr[2]); System.out.println(arr[3]); System.out.println(arr[4]); } }
上述代码可以起到对数组中元素遍历的目的,但问题是:
如果数组中增加了一个元素,就需要增加一条打印语句
如果输入中有100个元素,就需要写100个打印语句
如果现在要把打印修改为给数组中每个元素加1,修改起来非常麻烦。
通过观察代码可以发现,对数组中每个元素的操作都是相同的,则可以使用循环来进行打印。
1. 循环遍历数组
public class Main { public static void main(String[] args) { int[] arr = new int[]{1,2,3,4,5}; for (int i = 0; i < 5; i++) { System.out.println(arr[i]); } } }
改成循环之后,上述三个缺陷可以全部2和3问题可以全部解决,但是无法解决问题1。那能否获取到数组的长度呢?
【注意】:在数组中可以通过 数组对象.length
来获取数组的长度
public class Main { public static void main(String[] args) { int[] arr = new int[]{1,2,3,4,5}; for (int i = 0; i < arr.length; i++) { System.out.println(arr[i]); } } }
2. 使用 for-each
遍历数组
语法格式:
public class Main { public static void main(String[] args) { int[] arr = new int[]{1,2,3,4,5}; for (int x : arr) { System.out.println(x); } } }
for-each
是 for
循环的另外一种使用方式. 能够更方便的完成对数组的遍历. 可以避免循环条件和更新语句写错.
for-each
循环语句的循环变量将会遍历数组中的每个元素,而不是下标值。
3. 数组转字符串输出
import java.util.Arrays; public class Main { public static void main(String[] args) { int[] arr = new int[]{1,2,3,4,5}; String ret = Arrays.toString(arr); System.out.println(ret); } }
代码分析:
6.2 数组是引用类型
6.2.1 JVM 内存分布
内存是一段连续的存储空间,主要是用来存储程序运行时数据的。比如:
程序运行时代码需要加载到内存
程序运行产生的中间数据要存放在内存
程序中的常量也要保存
有些数据可能需要长时间存储,而有些数据当方法运行结束后就要被销毁。
如果对内存中存储的数据不加区分的随意存储,那对内存管理起来将会非常麻烦。比如:
因此 JVM 也对所使用的内存按照功能的不同进行了划分:
程序计数器:只是一个很小的空间,保存下一条执行的指令的地址
虚拟机栈:与方法调用相关的一些信息,每个方法在执行时,都会先创建栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。比如:局部变量。当方法运行结束后吧,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。
本地方法栈:本地方法栈于虚拟机栈的作用类似,只不过保存的内容是方法的局部变量。在有些版本的 JVM 实现中,本地方法栈和虚拟机栈是一起的
堆:JVM 所管理的最大内存区域,使用**new创建的对象都是在堆上保存,堆是随着程序开始运行时而创建,随着程序的结束而销毁,堆中的数据只要还有在使用,就不会被销毁**
方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法编译出的字节码就是保存在这个区域。
6.2.2 基本类型的变量与引用类型变量的区别
基本数据类型的变量,称为基本变量,该变量空间中直接存放的是其所对应的值;
而引用数据类型创建的变量,一般称为对象的引用,其空间中存储的是对象所在空间的地址
public class Main { public static void main(String[] args) { int a = 10; int[] arr = new int[]{1,2,3}; } }
在上述代码中,a
、arr
,都是函数内部的变量,因此其空间都在main
方法对应的栈帧中分配。
a
是内置类型的变量,因此其空间中保存的就是给该变量初始化的值。
arr
是数组类型的引用变量,其内部保存的内容可以简单理解成是数组在堆空间中的首地址。
上图可以看出,引用变量并不直接存储对象本生,可以简单理解成存储的是对象在堆中空间的起始地址。通过该地址,引用变量便可以去操作对象。有点类似C语言中的指针,但是 Java 中引用要比指针的操作更简单。
Java 零基础入门学习(小白也能看懂!)三https://developer.aliyun.com/article/1494897