一. 输入输出
- 输入的作用,就是由使用者告诉程序要操作的数据
- 例如,我要通过饿了么订餐,你得告诉程序你要吃什么,送货地址是什么吧
- 输出的作用,就是由程序向使用者展现执行的结果
- 还是订餐的例子,程序向你展示骑手接单没有,送到哪里了
- 将来输入输出来源会有多种,比如 app,网页,终端程序等等
1. System.out
之前已经介绍过【标准输出】:System.out
打开 jshell 用一下,回忆下对象和方法使用格式
对象.方法(参数);
套用一下,对象是 System.out,方法是 println,参数是 “你好”
jshell> System.out.println("你好");
你好
小技巧
- jshell 中用
Tab
键可以提示对象有哪些方法 - jshell 中省略
;
也不会报错
2. System.in
再来看看输入,对象是 System.in,方法叫 read,没有参数
jshell> System.in.read();
运行后,可以看到光标一闪一闪,表示正在等待用户的输入,这时输入小 a
jshell> System.in.read();
a
$1 ==> 97
-
会显示 97,称之为返回值,代表 read() 读入的结果
- 因为计算机中所有符号,都是用数字表示,参看下表
-
前面的 $1 是一个【变量】,将来它就代表 97,也就是刚才输入的小 a
-
【变量】可以反复被使用
\ | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0000 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 |
0016 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 | 无 |
0032 | ! | " | # | $ | % | & | ’ | ( | ) | * | + | , | - | . | / | |
0048 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | : | ; | < | = | > | ? |
0064 | @ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O |
0080 | P | Q | R | S | T | U | V | W | X | Y | Z | [ | \ | ] | ^ | _ |
0096 | ` | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o |
0112 | p | q | r | s | t | u | v | w | x | y | z | { | | | } | ~ | 无 |
System.in 的缺点
- 字符被转成的数字,不便人类阅读
- 只能输入一个字符
3. Scanner
用 Scanner 改进,System.in 是 java 为我们提供好的对象,而 Scanner 需要我们自己创建,语法是
jshell> new Scanner(System.in);
$2 ==> java.util.Scanner...
将来这个 $2 就代表刚才的 Scanner 对象,我们称之为【变量】
Scanner 对象里面最常用的方法是 nextLine,用法如下
jshell> $2.nextLine();
你好啊
$3 ==> "你好啊"
4. 变量名
$2
,$3
这样作为变量名虽然也可以,但如果用更有意义的名称来表示,更方便人类阅读、记忆。例如
jshell> var scanner = new Scanner(System.in)
scanner ==> java.util.Scanner[delimiters=\p{javaWhitespace}+] ... \E][infinity string=\Q∞\E]
jshell> var line = scanner.nextLine()
hello
line ==> "hello"
- scanner 就代指输入对象
- line 就代指用 nextLine() 读取到的字符串值
- var 是关键字,代表某种类型,具体有哪些类型后面再展开
5. 关键字
变量取名时要注意两个规则,不能以数字开头,不能是关键字
什么是关键字呢?关键字就是 java 中有特殊意义的单词,例如见过的有 class,var,new 等等,如果用 idea 中可以通过特殊颜色强调哪些单词是关键字,可以看到这些蓝色的单词都属于关键字
至java 17 为止,共有 67 个关键字,参看这两份表格,这些关键字,都会在今后的课程中陆续学到
二. 类型、变量、运算符
1. 字符与字符串
字符值与字符串值
像这样用双引号引起来的值,在 Java 里称为字符串,字符串顾名思义,由多个字符组成,单个字符用单引号表示,例如
jshell> 'a'
$4 ==> 'a'
jshell> "abc"
$5 ==> "abc"
- 单引号里必须由一个字符
- 双引号里可以有零个、一个、多个字符
转义字符
比如我需要输出一个单引号字符值,'''
这样写行不行?本意是想表示中间的单引号,但遗憾的是java把前两个单引号当成了一对,把它当作了那个空字符了
怎么办呢
为了把真正的单引号跟语法的单引号区分开,需要给它加一个反斜杠标记,告诉java,我想表示真正的单引号,而不是语法中的单引号。试一下。
jshell> System.out.println("\'")
'
这种结合了反斜杠的具有特殊含义的字符,称之为转义字符(Escape Character)
常见的有七个:\' \" \\ \n \t \b \r
刚才已经讲过单引号转义了
继续来看几个例子
jshell> System.out.println("\"") // 双引号转义
"
jshell> System.out.println("\\") // 反斜杠本身转义
\
jshell> System.out.println("1\n2") // 换行
1
2
jshell> System.out.println("123\t4") // 缩进
123 4
jshell> System.out.println("123\b4") // 退格
124
jshell> System.out.println("123\r4") // 回车,退格是光标退一格,回车是退到头
423
文本块
最后再再来看看文本块,如果有一段文字内,其中需要有很多的转义字符,那么可读性会变得很差,例如
jshell> System.out.println("床前\"明月\"光,\n疑是地上霜。")
因此在 java 14 这个版本引入了文本块来进行改善。
jshell> System.out.println("""
床前"明月"光,
疑是地上霜。""")
文本块本质上还是属于字符串值,由一对 三个双引号作为起始和结束标记,中间如果想表示双引号、换行这两个特殊字符,无需再转义表示
- 一个注意事项是 “”" 后需要换个行,不要紧接着写字符。
2. 类型
何为类型
现在让用户输入两个数,求得相加结果
jshell> scanner.nextLine()
1
$22 ==> "1"
jshell> scanner.nextLine()
2
$23 ==> "2"
jshell> $22 + $23
$24 ==> "12"
显然,这并不是我们想要的结果,它是输入的值当作了字符串,+ 号执行的是字符串连接操作,解决办法如下
jshell> scanner.nextInt()
1
$25 ==> 1
jshell> scanner.nextInt()
2
$26 ==> 2
jshell> $25 + $26
$27 ==> 3
nextLine() 和 nextInt() 返回的类型是不同的
- 前者返回的是字符串,类型为 String,+ 表示两个字符串连接
- 后者返回的是整数,类型为 int,+ 表示两个整数相加
数字类型
类型名 | 说明 | 数字范围 | 类型后缀 |
---|---|---|---|
byte | 整数类型,用1个字节表示 | [ − 2 7 , 2 7 ) [-2^7,2^7) [−27,27) 即 [ − 128 , 128 ) [-128,128) [−128,128) | |
short | 整数类型,用2个字节表示 | [ − 2 15 , 2 15 ) [-2^{15},2^{15}) [−215,215) | |
int | 整数类型,用4个字节表示 | [ − 2 31 , 2 31 ) [-2^{31},2^{31}) [−231,231) | |
long | 整数类型,用8个字节表示 | [ − 2 63 , 2 63 ) [-2^{63},2^{63}) [−263,263) | L |
float | 浮点小数,用4个字节表示 | [ − 1.9999999 ∗ 2 127 , 1.9999999 ∗ 2 127 ] [-1.9999999 * 2^{127},1.9999999 * 2^{127}] [−1.9999999∗2127,1.9999999∗2127] | F |
double | 浮点小数,用8个字节表示 | [ − 1.9999999 ∗ 2 1023 , 1.9999999 ∗ 2 1023 ] [-1.9999999 * 2^{1023},1.9999999 * 2^{1023}] [−1.9999999∗21023,1.9999999∗21023] | D |
-
[]
包含等于,()
不包含等于 -
类型后缀
- 不区分大小写,但建议用大写,因为小写的 L 与 1 容易混淆
- 尾符号 D 可以省略
-
float 和 double 精度不同,即小数点后的位数
- float 的精度二进制是 23,换算成十进制是 6~7
- double 的精度二进制是 52,换算成十进制是 15~16
字符类型
类型名 | 说明 | 范围 |
---|---|---|
char | 字符类型,配合单引号 | [ 0 , 2 16 ) [0,2^{16}) [0,216) 即 [ 0 , 65536 ) [0, 65536) [0,65536) |
String | 字符串类型,配合双引号或文本块 | - |
3. 变量与运算符
变量
变量的定义格式为
类型 变量名 = 值;
- 从语法可以看到,变量由类型和名称组成,类型决定了变量能存储的数据大小与数据格式,名字用来代表后面的值
- 这个语法其实咱们前面见过类似的,var scanner = new Scanner(System.in),这里 var 是类型,因为之前我们还没有学习具体有哪些类型,因此使用了 var 来代表某种类型,scanner是变量名,后面这一串就是值,也是 Scanner 对象
= 称之为赋值运算符,可以用来更新变量代表的值。
例如:
int a = 10
这行代码的意思是,定义了整型变量a,更新它的初始值为10
再来一句:
a = 20
这时候为啥不用写前面的类型了呢,因为变量定义只需一次,定义好之后变量就可以反复使用了,这行代码的意思是,将 a 所代表的值更新为 20
变量可以用来保存运算的结果,它自身也能参与运算
运算符
int a = 5 + 3
结果为 8
int a = 5 - 3
结果为 2
int a = 5 * 3
结果为 15
int a = 5 / 3
结果为 1,整数除法有两个注意点
- 整数除法,只会保留商,而小数部分会被舍弃,并不考虑四舍五入
- 除0是不合法的,会出错
int a = 5 % 3
结果为 2
小数加减乘除与整数类似,只是小数除法可以保留小数点后的数字,而且可以除零,例如
jshell> 5.0 / 3.0
$40 ==> 1.6666666666666667
jshell> 5.0 / 0.0
$41 ==> Infinity
增强赋值运算符
int a = 20;
a = a + 10;
意思是,获取 a 的原有值 20,执行加 10 运算,将新的结果重新赋值给 a,a 代表的值被更新成了 30。可以用 += 增强赋值运算符达到类似的效果
a += 10;
先不要看 = 号,获取 a 的原有值 30,执行加 10 运算,运算的结果 40 赋值给 a
a -= 10;
先拿 a 的原有值 40 与后面的 10 做减法,再将结果 30 赋值给 a
自增自减运算符
两个特殊运算符 ++ 与 –
++ 是让变量自增1,-- 是让变量自减1,举个例子
int a = 10;
a++;
结果为 11,++ 和 – 既可以写在变量之前,也可以写在变量之后,这两种写法的区别,我们到高级篇再讲,目前暂不用去了解
4. 练习 - 房贷计算器
【等额本息还款】法计算房贷
术语
等额本息是指一种贷款的还款方式,是在还款期内,每月偿还同等数额的贷款(包括本金和利息)
每月偿还的贷款可以通过下述公式计算
p ∗ r ∗ ( 1 + r ) m / ( ( 1 + r ) m − 1 ) p * r * (1 + r)^m / ((1 + r)^m - 1) p∗r∗(1+r)m/((1+r)m−1)
- p 为贷款本金 principal
- r 为月利率 monthlyInterestRate
- m 为还款月数 months
公式中这些都是什么意思呢
例如:贷款 200 万元 ,对应公式中的 p,200 万就是贷款本金,年利率 6%,月利率 mr 就是 6% / 12 = 0.5%,假设 10 年还清,这时还款月数就是 360 个月。套入公式计算即可得到每月还多少钱
要完成这个计算,有一点没学过的是求这里的 1+ mr 的 m 次方,计算它需要用一个求幂方法,这个方法是 jdk 核心类库中 Math 这个类提供的,Math的字面意思是数学,Math 类中提供了很多与数学计算相关的方法,如果你以后有这方面需求,就找它。
Math.pow()
pow 是 static 方法,语法为 类名.方法名(参数值)
,它需要两个参数,参数1是底数,参数2是指数
jshell> Math.pow(2.0, 1)
$42 ==> 2.0
jshell> Math.pow(2.0, 2)
$43 ==> 4.0
jshell> Math.pow(2.0, 3)
$44 ==> 8.0
解答
打开 idea,编写 Calculator 类
public class Calculator {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入贷款本金 p");
double p = scanner.nextDouble();
System.out.println("请输入年利率 r%");
double yr = scanner.nextDouble();
double mr = yr / 100.0 / 12.0;
System.out.println("请输入贷款月数 m");
int m = scanner.nextInt();
System.out.println(p * mr * Math.pow((1 + mr), m) / (Math.pow((1 + mr), m) - 1));
}
}
数字格式化
对结果的数字进行格式化,让它以货币的格式来显示
需要借助核心类库中一个 NumberFormat 对象,字面意思是数字格式化,使用它的 getCurrencyInstance 方法来获取一个货币格式化对象,再使用它的 format 方法把 double 小数格式化为货币格式,格式化时也会保留两位小数
例子
System.out.println(NumberFormat.getCurrencyInstance(Locale.CHINA).format(1000000.00)); System.out.println(NumberFormat.getCurrencyInstance(Locale.US).format(1000000.00));
System.out.println(NumberFormat.getCurrencyInstance(Locale.GERMANY).format(1000000.00));
System.out.println(NumberFormat.getCurrencyInstance(Locale.KOREA).format(1000000.00));
输出
¥1,000,000.00
$1,000,000.00
1.000.000,00 €
₩1,000,000
如果 Locale 省略不写,默认为中国
房贷计算器可以改写为
double payment = p * mr * Math.pow((1 + mr), m) / (Math.pow((1 + mr), m) - 1); System.out.println(NumberFormat.getCurrencyInstance().format(payment));
查阅 Javadoc
javadoc 就是 java documentation 的缩写,我们下载的 jdk 中已经自带了,无需额外再下载。那怎么查阅 javadoc 呢,如果大家用的是 idea,那么可以通过一些快捷键来查阅java文档
-
比如想看看类的文档,这时先按 Ctrl + N 查找类,假设我想看 Math 类的文档,输入要查阅的类名 Math,回车,可以跳转到这个类
-
接下来我想看看方法的文档怎么办呢,按一下 ctrl + f12,列出当前类的所有方法,绿色表示可以使用方法,橙色带锁的,表示是该类一种特殊的私有方法,不能直接使用。找感兴趣的方法时,如果你懂一些英文单词,那么会有一定优势,例如你想找一个平方根方法,它对应的英文是 sqrt,这时敲入这几个字母,就会定位到方法,同样可以用翻译查看该方法的功能
-
可以查到它的作用:返回一个数的平方根,这是方法名,查看后面括号内可以得知,需要一个参数,代表要求平方根的那个数字,是一个double 小数,方法名称前还有个 double 表示它的结果类型也是一个 double 小数
-
Math 中的方法大部分都是 static 方法,也就是配合类名使用的方法,之前也说过,用法为 类名.方法名(参数)
在平时写代码时,如果忘记了某个方法的作用,可以光标定位到该方法,按 Ctrl + Q 进行查阅,效果是类似的
三. 条件语句
编程时有一种重要的语句叫做条件语句,之前我们学过的都属于顺序语句,也就是从上至下,依次要执行每一行代码。
但是有的情况下,我们并不希望所有代码都执行,而是希望满足条件的代码才执行
例如:要对用户输入数据的合法性进行检查:
- 贷款本金必须大于0
- 贷款月数范围在 1 ~ 360 之间
- 年利率范围在 1% ~ 36% 之间
如果你输入的值连这些条件都不满足,有必要去计算每月还款金额吗?
这种情况下,就要用到条件语句了,它的语法格式为
if(条件) {
// 条件为 true 执行这里
} else {
// 条件为 false 执行这里
}
什么意思呢,if 本意是如果,如果条件成立,执行代码1,else 本意是否则,即条件不成立,执行代码2,其中 else { }
语句块不是必须的,可以省略
那么条件这部分怎么写呢?对于数字类型可以借助比较运算符的运算结果来充当条件,参考下面的表格,这种表格列出了所有比较运算符
比较运算符 | 含义 | |
---|---|---|
a == b | 判断 a 与 b 是否相等 | |
a > b | 判断 a 是否 > b | |
a >= b | 判断 a 是否 >= b | |
a < b | 判断 a 是否 < b | |
a <= b | 判断 a 是否 <= b | |
a != b | 判断 a 与 b 是否不相等 |
1. boolean 类型
判断的结果是布尔类型,可以充当条件,它的取值非真即假,真用 true 表示,假用 false 表示
2. 单条件
jshell> int a = 1000;
a ==> 1000
jshell> if(a > 0){
...> System.out.println("ok");
...> } else {
...> System.out.println("必须>0");
...> }
ok
jshell> a = -1000;
a ==> -1000
jshell> if(a > 0){
...> System.out.println("ok");
...> } else {
...> System.out.println("必须>0");
...> }
必须>0
房贷计算器改写如下
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入贷款本金 p");
double p = scanner.nextDouble();
if(p > 0) {
System.out.println("请输入年利率 r%");
double yr = scanner.nextDouble();
double mr = yr / 100.0 / 12.0;
System.out.println("请输入贷款月数 m");
int m = scanner.nextInt();
double payment = p * mr * Math.pow((1 + mr), m) / (Math.pow((1 + mr), m) - 1);
System.out.println(NumberFormat.getCurrencyInstance().format(payment));
} else {
System.out.println("贷款金额必须大于0");
}
}
3. 多条件
刚才我们在判断贷款本金的时候,只需要有一个大于 0 的条件就可以了,但是接下来我们要去检查年利率的时候,他是在一个范围之间,这就必须有两个条件,一个条件呢是要让年利率大于等于 1%,第二个条件呢,是让上年利率必须小于等于 36%,而且呢这两个条件你必须同时成立
多个条件可以用逻辑运算符连接
逻辑运算符 | 含义 | |
---|---|---|
条件1 && 条件2 | && 意思是并且,两个条件必须同时成立,结果为 true | |
条件1 || 条件2 | || 意思是或者,两个条件有一个成立,结果为 true | |
! 条件 | ! 意思是取反 |
举例
jshell> int b = 120;
b ==> 120
jshell> if(b >= 1 && b <= 360) {
...> System.out.println("ok");
...> } else {
...> System.out.println("必须在1~360之间");
...> }
ok
jshell> b = 0
b ==> 0
jshell> if(b >= 1 && b <= 360) {
...> System.out.println("ok");
...> } else {
...> System.out.println("必须在1~360之间");
...> }
必须在1~360之间
jshell> b = 361
b ==> 361
jshell> if(b >= 1 && b <= 360) {
...> System.out.println("ok");
...> } else {
...> System.out.println("必须在1~360之间");
...> }
必须在1~360之间
房贷计算器改写如下
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入贷款本金 p");
double p = scanner.nextDouble();
if(p > 0.0) {
System.out.println("请输入年利率 r%");
double yr = scanner.nextDouble();
if (yr >= 1.0 && yr <= 36.0) {
double mr = yr / 100.0 / 12.0;
System.out.println("请输入贷款月数 m");
int m = scanner.nextInt();
if (m >= 1 && m <= 360) {
double payment =
p * mr * Math.pow((1 + mr), m) / (Math.pow((1 + mr), m) - 1);
System.out.println(NumberFormat.getCurrencyInstance()
.format(payment));
} else {
System.out.println("贷款月数范围在 1 ~ 360 之间");
}
} else {
System.out.println("年利率范围在 1% ~ 36% 之间");
}
} else {
System.out.println("贷款金额必须大于0");
}
}
4. 相反条件
我们这段代码,逻辑上没错,但你会发现不容易阅读
多层 if 嵌套导致代码的可读性变得很差,一旦大家写代码时出现了两层以上的 if 语句,就要小心了。如何改进呢?
这里给同学们介绍一种方法:可以去除 else 提高代码可读性
比如,现在有两个分支,c 是一个条件,要么走分支1,要么走分支2,用下面的 if else 可以表示
if(c) {
// 分支1
} else {
// 分支2
}
等价于
if(!c) {
// 分支2
} else {
// 分支1
}
能不能不写 else 呢?假设进入了分支2,分支2的代码执行后,程序还会继续向下执行,导致分支1也被执行
if(!c) {
// 分支2
}
// 分支1
可以改写为下面的形式,这样就避免了 else 出现
if(!c) {
// 分支2
return;
}
// 分支1
总结一下,以上代码的等价转换,有一句口诀:条件取反,if else 倒置,return 一加, else 可去
变换有一定的规律:
原条件 | 相反条件1 | 相反条件2 |
---|---|---|
p > 0.0 | !(p > 0.0) | p <= 0.0 |
yr >= 1.0 && yr <= 36.0 | !(yr >= 1.0 && yr <= 36.0) | yr < 1.0 || yr > 36.0 |
m < 1 || m > 360 | !(m < 1 || m > 360) | m >= 1 && m <= 360 |
-
用逻辑与连接的两个条件,可以两个条件分别取反,然后&&变||
- 年利率>=1.0 并且<=36.0 的相反条件是:年利率<1.0 或者 年利率>36.0
-
用逻辑或连接的两个条件,可以两个条件分别取反,然后||变&&
这种逻辑变换规律称之为反演规则,公式记不住没关系,简单的反演我们自己就能想出来,复杂的变换才用公式保证正确性
房贷计算器改写如下
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入贷款本金 p");
double p = scanner.nextDouble();
if (p <= 0.0) {
System.out.println("贷款金额必须大于0");
return;
}
System.out.println("请输入年利率 r%");
double yr = scanner.nextDouble();
if (yr < 1.0 || yr > 36.0) {
System.out.println("年利率范围在 1% ~ 36% 之间");
return;
}
double mr = yr / 100.0 / 12.0;
System.out.println("请输入贷款月数 m");
int m = scanner.nextInt();
if (m < 1 || m > 360) {
System.out.println("贷款月数范围在 1 ~ 360 之间");
return;
}
double payment = p * mr * Math.pow((1 + mr), m) / (Math.pow((1 + mr), m) - 1);
System.out.println(NumberFormat.getCurrencyInstance().format(payment));
}
四. 循环语句
1. 循环语句语法
for(初始化变量; 终止条件; 更新变量) {
// 循环体代码
}
- 初始化部分仅执行一次
- 执行终止条件,如果为 true 继续循环,如果为 false 退出循环
- 执行循环体代码
- 执行迭代部分,从 2 开始执行
例如
for(int i = 0; i < 3; i++) {
System.out.println(i);
}
执行流程如下
- 声明一个变量 i,初始值为 0,用来控制循环次数
- 判断 i < 3,此时 i 取值是 0,条件为 true,继续循环
- System.out.println(0)
- i++,自增为 1
- 判断 i < 3,此时 i 取值是 1,条件为 true,继续循环
- System.out.println(1)
- i++,自增为 2
- 判断 i < 3,此时 i 取值是 2,条件为 true,继续循环
- System.out.println(2)
- i++,自增为 3
- 判断 i < 3,此时 i 取值是 3,条件为 false,退出循环
2. 变量的作用范围
上例中 i 的作用范围,仅在循环语句的 {} 内有效,现在要求求 1~100 的整数和,则需要把 sum 这个变量定义在 {} 外层
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
System.out.println(sum);
这里能否这样写
for (int i = 1; i <= 100; i++) {
int sum = 0;
sum += i;
}
System.out.println(sum);
不行,变量有它们各自的作用范围,从变量的定义开始,找到包围它的,右 } 括号为止。
- 在它上面用它
- 在右括号外用它
3. 练习 - 贷款计算详情
现在需要计算每月偿还的利息、偿还的本金、剩余的本金
例如,借款 1000 元,利息 100%,两月还清,根据公式计算出来每月还款 1333.33
月份 | 本月还款 | 偿还本金 | 偿还利息 | 剩余本金 |
---|---|---|---|---|
1 | 1333.33 | 333.33 | 1000.00 | 666.67 |
2 | 1333.33 | 666.67 | 666.67 | 0 |
总额 | 2666.67 | 1000.00 | 1666.67 |
可以看到
- 偿还利息 = 剩余本金 ∗ 月利率 偿还利息 = 剩余本金 * 月利率 偿还利息=剩余本金∗月利率
- 偿还本金 = 每月还款 − 偿还利息 偿还本金 = 每月还款 - 偿还利息 偿还本金=每月还款−偿还利息
房贷计算器改写如下
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入贷款本金 p");
double p = scanner.nextDouble();
if (p <= 0.0) {
System.out.println("贷款金额必须大于0");
return;
}
System.out.println("请输入年利率 r%");
double yr = scanner.nextDouble();
if (yr < 1.0 || yr > 36.0) {
System.out.println("年利率范围在 1% ~ 36% 之间");
return;
}
double mr = yr / 100.0 / 12.0;
System.out.println("请输入贷款月数 m");
int m = scanner.nextInt();
if (m < 1 || m > 360) {
System.out.println("贷款月数范围在 1 ~ 360 之间");
return;
}
double payment = p * mr * Math.pow((1 + mr), m) / (Math.pow((1 + mr), m) - 1);
System.out.println(NumberFormat.getCurrencyInstance().format(payment));
double totalInterest = 0.0; // 总利息
for (int i = 1; i <= m; i++) {
double interest = p * mr; // 每月偿还利息
double principal = payment - interest; // 每月偿还本金
p -= principal; // 剩余本金
totalInterest += interest;
System.out.print(i);
System.out.print("\t本月还款:" +
NumberFormat.getCurrencyInstance().format(payment));
System.out.print("\t偿还本金:" +
NumberFormat.getCurrencyInstance().format(principal));
System.out.print("\t偿还利息:" +
NumberFormat.getCurrencyInstance().format(interest));
System.out.println("\t剩余本金:" + NumberFormat.getCurrencyInstance().format(p));
}
System.out.print("总还款额:" +
NumberFormat.getCurrencyInstance().format(payment * m));
System.out.println("\t总利息:" +
NumberFormat.getCurrencyInstance().format(totalInterest));
}
五. 方法
将来代码多了,全部写在 main 方法里,会显得非常凌乱,难于阅读。这节课的目标是使用方法来改写前面的代码。
1. 定义方法
先来了解一下定义方法的语法
[访问修饰符] [static] 返回结果类型 方法名([参数类型1 参数名1, 参数类型2 参数名2, ...]) {
// 方法体
return 返回结果
}
- 其中用 [] 的([] 不是语法的一部分)是可选部分
- 访问修饰符先省略不写,后面再讲
- static 这里先写上,这个前面提过,用 static 修饰的方法不属于对象
- 方法执行完毕后可能会有返回结果
- 如果没有返回结果,返回值类型填写 void
- 如果有返回结果,填写实际的类型,例如
- 返回整数,填写 int
- 返回小数,填写 double
- 方法名随便起,但应当做到望文生义
- 参数是方法执行时需要传入的数据,可以有零到多个,格式为【参数类型 参数名】,多个参数之间用逗号分隔
- {} 内写方法的具体代码
- 最后 return 用来结束方法的运行,如果方法有返回结果,也需要配合 return 把结果传递给调用者
例如,想计算一下两个整数的和
class Test {
static int add(int a, int b) {
return a + b;
}
}
2. 调用方法
回忆一下 Math.pow(2.0, 2) 就是一个由 java 提供好的 static 方法,它怎么用呢,Math 是类名,pow 是方法名,括号内是参数,对于我们自己写的 static 方法,用法是类似的:
类名.方法名([参数值1, 参数值2, ...])
即
Test.add(100,200);
怎么拿到返回结果呢?
int c = Test.add(100,200);
如果是调用本类 static 方法,可以省略前面的类名
3. 方法的意义
学完了方法的定义、调用流程,再来看看方法的意义
方法的一个意义在于隐藏实现细节:
例如,对于前面例子中的【等额本息】方式计算房贷,如果没有方法,那就要求编程者必须非常清楚计算公式
double payment = p * mr * Math.pow((1 + mr), m) / (Math.pow((1 + mr), m) - 1);
假设有一位资深程序员(例如你)提供了计算房贷方法,那么编程者就只需要知道:
计算等额本金还款,需要一个名字叫calculate的方法
它需要三个参数,… ,至于具体的计算过程,被隐藏在了方法内部
double payment = calculate(p, mr, m);
- p 贷款本金
- mr 月利率
- m 贷款月数
对于使用它的小白程序员来讲,无需了解它的实现细节,直接拿来用就可以了。小白程序员是站在你的肩膀上编程
方法的另一个意义在于减少重复代码、提高代码的可维护性:
对比以下代码,第一段是没用方法,如果有一处写错了,所有重复的地方都得修改
double p1 = p * mr * Math.pow((1 + mr), m) / (Math.pow((1 + mr), m) - 1);
double p2 = p * mr * Math.pow((1 + mr), m) / (Math.pow((1 + mr), m) - 1);
double p3 = p * mr * Math.pow((1 + mr), m) / (Math.pow((1 + mr), m) - 1);
用了方法,万一写错,只需要一个地方的代码需要修改
static double calculate(double p, double mr, int m) {
return p * mr * Math.pow((1 + mr), m) / (Math.pow((1 + mr), m) - 1);
}
4. 练习 - 贷款计算 - 方法改写
public class Calculator3 {
public static void main(String[] args) {
double p = inputAndCheckP();
double mr = inputAndCheckMr();
int m = inputAndCheckM();
double payment = Calculator3.calculate(p, mr, m);
printDetails(p, mr, m, payment);
}
static double inputAndCheckP() {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入本金");
double p = scanner.nextDouble();
if(p <= 0) {
// System.out.println("贷款金额必须 > 0");
// throw new 异常类型("提示信息")
throw new IllegalArgumentException("贷款金额必须 > 0");
}
return p;
}
static double inputAndCheckMr() {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入年利率");
double yr = scanner.nextDouble();
if(yr < 1.0 || yr > 36.0) {
throw new IllegalArgumentException("年利率必须是 1 ~ 36");
}
return yr / 12.0 / 100;
}
static int inputAndCheckM() {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入还款月数");
int m = scanner.nextInt();
if(m < 1 || m > 360) {
throw new IllegalArgumentException("贷款月数必须是 1 ~ 360");
}
return m;
}
static void printDetails(double p, double mr, int m, double payment) {
for (int i = 0; i < m; i++) {
double payInterest = p * mr; // 偿还利息
double payPrincipal = payment - payInterest; // 偿还本金
p -= payPrincipal; // 剩余本金
System.out.print ("月份:" + (i + 1));
System.out.print("\t本月还款:" + NumberFormat.getCurrencyInstance().format(payment));
System.out.print("\t偿还本金:" + NumberFormat.getCurrencyInstance().format(payPrincipal));
System.out.print("\t偿还利息:" + NumberFormat.getCurrencyInstance().format(payInterest));
System.out.println("\t剩余本金:" + NumberFormat.getCurrencyInstance().format(p));
}
System.out.println("总还款额:" + NumberFormat.getCurrencyInstance().format(payment * m));
}
/**
* 以等额本息方式计算每月还款金额
* @param p 本金
* @param mr 月利率
* @param m 还款月数
* @return 每月还款金额
*/
static double calculate(double p, double mr, int m) {
double pow = Math.pow(1 + mr, m);
return p * mr * pow / (pow - 1);
}
}
说明1
大家抽取方法时有一个原则,就是把一组完整功能,所对应的多行代码抽取为一个方法,这里我们把计算还款总额和计算还款详情,分别抽取了两个方法
- calculate
- printDetails
抽取时,要点如下
- 方法名要见文知义
- 返回值不着急写,看看方法的外部需不需要用到这个方法内的变量
- 需要,返回值定义为结果类型
- 不需要,返回值定义为 void
- 参数也不着急写,看方法内缺哪些变量定义,以它们为方法参数
说明2
对于 calculate 这种比较重要的方法定义,最好给它加一个文档,你得告诉将来这个方法的使用者,怎么用这个方法,每个参数是什么意思。
先写斜杠两个星号的开始,不用着急写它的结束,直接一回车。idea 就会自动生成一段 javadoc 文档,你可以在这里介绍方法的作用
- 在 @param 这里对每个参数进行说明
- 在 @return 这里对返回值进行说明。
说明3
如果在某些验证不通过,想让剩余代码不要运行,可以利用 throw 语法
- return 这种语法叫正常返回,也就是当方法调用后,代码还会继续运行
- 还有就是这种throw 语法,它称为异常返回,如果没有额外处理,代码从方法调用后就中断运行
六. package
1. package
随着我们写的类越来越多,把他们都放在一块儿来管理,感觉比较的不规范,因此,我们要引入一个新的package语法,对源文件进行一个更好的管理。
其实这个package说白了就是Java中一种目录结构
|- 包1
|- 从属于包1 的类
|- 包2
|- 从属于包2 的类
语法:
package 包名; // 告诉下面的类从属于此包
class 类 {
}
包的命名一般都是域名的倒置,如
- baidu.com 域名的倒置就是 com.baidu
- bilibilicom 域名的倒置就是 com.bilibili
2. import
与 package 关系非常密切的一个语法:import,如果你的类想使用另外一个类,而两个类不同包,这时就必须用 import,把另一个类导入进来才能使用
package com.itheima.a;
import java.util.Scanner;
class Calulator {
public static void main(String[] args) {
// 要用到 Scanner, 这时就用用到上面的 import 语句
}
}
- 有一种特殊情况不需要 import 导入,即 java.lang 包下的类使用时,都不需要 import