什么是对象?之前我们讲过,对象就是计算机中的虚拟物体。例如 System.out,System.in 等等。然而,要开发自己的应用程序,只有这些现成的对象还远远不够。需要我们自己来创建新的对象。
例如,我想开发一个电商应用,在网上卖手机,打算使用对象来代表这些手机。怎么做呢?首先要对现实世界的手机进行抽象,抽取它属性、抽取它的行为
抽取时要抓取本质属性,在真实物体上做简化,并不是所有的属性都要抽象
例如对于手机来说,分析最终的展示效果可以得知,需要品牌、内存、大小、颜色、价格,其它页面展示用不上的属性,就不必抽取了
这些属性信息在 java 里称为对象的字段,根据它们,我们就能确定这个对象将来长什么样,相当于给对象定义了模板,模板一旦确定,再创建的对象就不能跳出模板的范围。
模板有了,就可以根据它创建各种各样的手机对象,比如
手机1,品牌苹果、内存128G、大小6.1英寸、颜色星光色、价格5799
手机2,品牌红米、内存4G、大小6.53英寸、颜色金色、价格1249
手机3,品牌华为、内存4G、大小6.3英寸、颜色幻夜黑、价格999
这个模板,以后在 java 里称之为类,英文 class,这些对象呢,英文也要知道:object
代码里怎么表示类呢,看一下类的语法
class 类 {
字段;
构造方法(参数) {
}
返回值类型 方法名(参数) {
代码
}
}
字段定义与之前学过的局部变量、方法参数的定义类似,它们本质都是变量
构造方法也是一种方法,在将来创建对象时被使用,作用是为对象字段赋初始值
从属于对象的方法不用 static 修饰,它就代表对象的行为,这里先放一下
就以刚才的手机为例,按照语法写一下
public class Phone {
// 类型 名字
String brand; // 品牌
String memory; // 内存
String size; // 大小
String color; // 颜色
double price; // 价格
public Phone(String b, String m, String s, String c, double p) {
brand = b;
memory = m;
size = s;
color = c;
price = p;
}
}
有了类,才能创建对象,创建对象的语法
new 类构造方法(参数)
例如
public class TestPhone {
public static void main(String[] args) {
Phone p1 = new Phone("苹果", "128G", "6.1寸", "星光色", 5799.0);
Phone p2 = new Phone("红米", "4G", "6.53寸", "金色", 1249.0);
Phone p3 = new Phone("华为", "4G", "6.3寸", "幻夜黑", 999.0);
System.out.println(p1.color); // 获取p1的颜色
System.out.println(p1.price); // 获取p1的价格
p1.price = 3000.0; // 修改p1的价格
System.out.println(p1.price); // 获取p1的价格
}
}
总结一下:
如果字段没有通过构造方法赋值,那么字段也会有个默认值
类型 | 默认值 | 说明 |
---|---|---|
byte short int long char | 0 | |
float double | 0.0 | |
boolean | false | |
其它 | null |
比如说,想加一个字段 available 表示手机上架还是下架,这时就可以使用默认值统一设置
public class Phone {
// 类型 名字
String brand; // 品牌
String memory; // 内存
String size; // 大小
String color; // 颜色
double price; // 价格
boolean available; // 是否上架
public Phone(String b, String m, String s, String c, double p) {
brand = b;
memory = m;
size = s;
color = c;
price = p;
}
}
对于变量的命名,包括方法参数的命名,最好做到见文知义,也就是起名的时候起的更有意义,最好能一眼看出来这个变量它代表什么,你来看手机构造方法这些参数的名字起的好不好?不好吧,这样改行不行(先只改一个 brand):
public class Phone {
// 类型 名字
String brand; // 品牌
String memory; // 内存
String size; // 大小
String color; // 颜色
double price; // 价格
boolean available; // 是否上架
public Phone(String brand, String m, String s, String c, double p) {
brand = brand;
memory = m;
size = s;
color = c;
price = p;
}
}
当方法参数与字段重名时,需要用在字段前面加 this 来区分
public class Phone {
// 类型 名字
String brand; // 品牌
String memory; // 内存
String size; // 大小
String color; // 颜色
double price; // 价格
boolean available; // 是否上架
public Phone(String brand, String memory, String size, String color, double price) {
this.brand = brand;
this.memory = memory;
this.size = size;
this.color = color;
this.price = price;
this.available = true;
}
}
提示
- 如果觉得自己写 this 比较麻烦,可以使用 IDEA 的快捷键 ALT + Insert 来生成构造方法
- 用 IDEA 生成的构造方法字段和方法参数都是用 this 区分好的
带参数的构造并不是必须的,也可以使用无参构造,例如
public class Student {
String name; // 姓名 null
int age; // 年龄 0
Student() {
}
}
使用无参构造创建对象示例如下
public class TestStudent {
public static void main(String[] args) {
Student s1 = new Student();
s1.name = "张三";
s1.age = 18;
System.out.println(s1.name);
System.out.println(s1.age);
}
}
无参构造有个特性:
前面我们讲了,面向对象编程,就是抽象现实世界的物体把它表示为计算机中的对象,手机的例子中,我们定义了类,抽取了字段来描述对象长什么样,这节课我们继续来抽取方法描述这个对象的行为,方法决定了这个对象能干什么
手机的例子不需要方法,计算机中的手机是假的,没法打电话,下面举一个有方法的例子
这里使用了 javascript 语言来举这个例子,虽然大家没有学过这门语言,但它的语法与 java 非常类似,相信我解释一下大家就能理解,另外,我待会讲解时,咱们把注意力集中的类和方法这部分代码上,其它一些 javascript 语言的细节也不用去关注
class Car {
constructor(color, speed, x, y) {
this.color = color; // 颜色
this.speed = speed; // 速度
this.stopped = true; // 是否停止
this.x = x;
this.y = y;
}
run() {
this.stopped = false;
}
// 更新坐标
update() {
}
// 省略其它无需关注的代码
}
创建汽车对象的语法与 java 几乎是一样的:
new Car("red", 5); // 这是创建一个红色的,速度为 5 的汽车
new Car("blue", 4); // 这是创建一个蓝色的,速度为 4 的汽车
执行完上两行创建汽车对象,页面效果如下所示
网页上的汽车不能动啊,方法要登场了!方法就是用来控制对象的行为
主要来看这个 update 方法,update 方法的作用,被我设计为控制汽车的坐标,你只需要把坐标如何变化通过代码在update里写出来,就能看到效果
update() {
if(this.stopped) {
return;
}
this.y -= this.speed;
if (this.y <= 20) {
this.y = 20;
}
}
假设只能向上跑,每次调用方法时让 this.y 减少,减少多少呢?
固定成 3,但这样大家速度都一样了
改为根据 this.speed 减少
假设顶端是终点,this.y 不能超过终点,因此加一个 if 判断,如果小于 0 则固定为 0
因为汽车的 y 坐标是以矩形底部开始算的,0 会导致汽车跑出了画面,所以汽车跑到终点时 y 坐标应改为20,也就是汽车长度
汽车没有听我命令就一开始就跑了,this.stopped 控制这个汽车是停还是移动
通过这个例子,我们应当知道,方法的作用是控制对象的行为
更专业的说
例如
我的需求是,对比相同本金,不同利率和贷款月份,计算总还款额,看哪个更划算一些
不用面向对象方式,写出来的代码长这样
public class TestCal {
public static void main(String[] args) {
// 对比相同本金,不同利率和贷款月份,计算总还款额(等额本息),看哪个更划算一些
double p1 = 100000.0;
int m1 = 24;
double yr1 = 4.5;
double r1 = cal(p1, m1, yr1);
System.out.println("4.5% 利率借 2 年:" + r1);
double p2 = 100000.0;
int m2 = 12; // 1 年
double yr2 = 6.0;
double r2 = cal(p2, m2, yr2);
System.out.println("6.0% 利率借 1 年:" + r2);
}
static double cal(double p, int m, double yr) {
double mr = yr / 100.0 / 12;
double pow = Math.pow(1 + mr, m);
return m * p * mr * pow / (pow - 1);
}
}
那么将来对象从何而来呢?很简单,找关系!把一组相关的数据作为一个整体,就形成了对象。
我们现有的数据中,哪些是相关的?
代码变成了下面的样子
public class Cal {
double p;
int m;
double yr;
public Cal(double p, int m, double yr) {
this.p = p;
this.m = m;
this.yr = yr;
}
}
public class TestCal {
public static void main(String[] args) {
// 对比相同本金,不同利率和贷款月份,计算总还款额(等额本息),看哪个更划算一些
Cal c1 = new Cal(100000.0, 24, 4.5);
double r1 = cal(c1);
System.out.println("4.5% 利率借 2 年:" + r1);
Cal c2 = new Cal(100000.0, 12, 6.0);
double r2 = cal(c2);
System.out.println("6.0% 利率借 1 年:" + r2);
}
static double cal(Cal c) {
double mr = c.yr / 100.0 / 12;
double pow = Math.pow(1 + mr, c.m);
return c.m * c.p * mr * pow / (pow - 1);
}
}
总结:把相关的数据作为一个整体,就形成了对象,对象的字段演化完毕
方法执行总得需要一些数据,以前我们学习的主要是这种 static 方法,它的数据全部来自于方法参数。
今天开始,要学习对象方法,顾名思义,它是从属于对象的方法,语法上要去掉 static,变成这个样子
public class Cal {
double p;
int m;
double yr;
public Cal(double p, int m, double yr) {
this.p = p;
this.m = m;
this.yr = yr;
}
double cal() {
double mr = yr / 100.0 / 12;
double pow = Math.pow(1 + mr, m);
return m * p * mr * pow / (pow - 1);
}
}
看看改动成对象方法后,都有哪些代码发生了变化?为啥不需要参数了呢?
这种对象方法执行需要的数据:
既然我们讲的这种对象方法都从属于 Calculator 对象了,那么方法参数这里是不是就没必要再加一个 Calculator 对象了啊
方法体内这些本金、月份、利率,都来自于方法所从属的对象的字段。不用写前面的 c. 了
最后,方法调用时,为了表达与对象的这种从属关系,格式也应变化为:对象.方法()
public class TestCal {
public static void main(String[] args) {
// 对比相同本金,不同利率和贷款月份,计算总还款额(等额本息),看哪个更划算一些
Cal c1 = new Cal(100000.0, 24, 4.5);
double r1 = c1.cal();
System.out.println("4.5% 利率借 2 年:" + r1);
Cal c2 = new Cal(100000.0, 12, 6.0);
double r2 = c2.cal();
System.out.println("6.0% 利率借 1 年:" + r2);
}
}
例如:
对象的方法演化完毕
静态方法 vs 对象方法
- 而 static 方法需要的数据,全都来自于方法参数,它没有关联对象,没有对象的那一部分数据
- 对象方法执行的数据,一部分数据从方法参数转移至对象内部
用面向对象思想设计等额本息和等额本金两个类
class Calculator0 {
Calculator0(double p, int m, double yr) {
this.p = p;
this.m = m;
this.yr = yr;
}
double p;
int m;
double yr;
String[] cal0() {
double mr = yr / 12 / 100.0;
double pow = Math.pow(1 + mr, m);
double payment = p * mr * pow / (pow - 1);
return new String[]{
NumberFormat.getCurrencyInstance().format(payment * m),
NumberFormat.getCurrencyInstance().format(payment * m - p)
};
}
String[][] details0() {
String[][] a2 = new String[m][];
double mr = yr / 12 / 100.0;
double pow = Math.pow(1 + mr, m);
double payment = p * mr * pow / (pow - 1); // 月供
for (int i = 0; i < m; i++) {
double payInterest = p * mr; // 偿还利息
double payPrincipal = payment - payInterest; // 偿还本金
p -= payPrincipal; // 剩余本金
String[] row = new String[]{ // 一行的数据
(i + 1) + "",
NumberFormat.getCurrencyInstance().format(payment),
NumberFormat.getCurrencyInstance().format(payPrincipal),
NumberFormat.getCurrencyInstance().format(payInterest),
NumberFormat.getCurrencyInstance().format(p)
};
a2[i] = row;
}
return a2;
}
}
class Calculator1 {
Calculator1(double p, int m, double yr) {
this.p = p;
this.m = m;
this.yr = yr;
}
double p;
int m;
double yr;
String[] cal1() {
double payPrincipal = p / m; // 偿还本金
double backup = p; // 备份本金
double mr = yr / 12 / 100.0;
double payInterestTotal = 0.0; // 总利息
for (int i = 0; i < m; i++) {
double payInterest = p * mr; // 偿还利息
p -= payPrincipal; // 剩余本金
payInterestTotal += payInterest;
}
// [0]还款总额 [1]总利息
return new String[]{
NumberFormat.getCurrencyInstance().format(backup + payInterestTotal),
NumberFormat.getCurrencyInstance().format(payInterestTotal)
};
}
String[][] details1() {
double payPrincipal = p / m; // 偿还本金
double mr = yr / 12 / 100.0;
String[][] a2 = new String[m][];
for (int i = 0; i < m; i++) {
double payInterest = p * mr; // 偿还利息
p -= payPrincipal; // 剩余本金
double payment = payPrincipal + payInterest; // 月供
String[] row = new String[]{
(i + 1) + "",
NumberFormat.getCurrencyInstance().format(payment),
NumberFormat.getCurrencyInstance().format(payPrincipal),
NumberFormat.getCurrencyInstance().format(payInterest),
NumberFormat.getCurrencyInstance().format(p)
};
a2[i] = row;
}
return a2;
}
}
控制器代码则变为,比以前看着也简洁了不少
@Controller
public class CalController {
@RequestMapping("/cal")
@ResponseBody
String[] cal(double p, int m, double yr, int type) {
if (type == 0) { // 等额本息
return new Calculator0(p, m, yr).cal0();
} else { // 等额本金
return new Calculator1(p, m, yr).cal1();
}
}
@RequestMapping("/details")
@ResponseBody
String[][] details(double p, int m, double yr, int type) {
if (type == 0) {
return new Calculator0(p, m, yr).details0();
} else {
return new Calculator1(p, m, yr).details1();
}
}
}
下面是对圆、计算圆面积进行了面向对象设计
public class Circle {
double r; // 半径
double pi = 3.14;
public Circle(double r) {
this.r = r;
}
double area() {
return pi * r * r;
}
}
测试代码如下
public class TestCircle {
public static void main(String[] args) {
Circle c1 = new Circle(1.0);
c1.pi = 3.14;
Circle c2 = new Circle(1.0);
c2.pi = 3;
System.out.println(c1.area()); // 圆的面积计算结果为 3.14
System.out.println(c2.area()); // 圆的面积计算结果为 3
}
}
这显然不合理,不能一个圆计算时 π \pi π 值是 3.14,换成另一个圆计算时 π \pi π 值就变成了 3,对于 π \pi π 值来讲,应该是所有圆共享一个值
改进如下
public class Circle {
double r; // 半径
static double pi = 3.14; // 静态变量, 所有圆对象共享它
public Circle(double r) {
this.r = r;
}
double area() {
return pi * r * r;
}
}
回到测试代码
public class TestCircle {
public static void main(String[] args) {
Circle c1 = new Circle(1.0);
c1.pi = 3.14;
Circle c2 = new Circle(1.0);
c2.pi = 3;
System.out.println(c1.area()); // 圆的面积计算结果为 3
System.out.println(c2.area()); // 圆的面积计算结果为 3
}
}
两次计算结果相同,因为 c1.pi 和 c2.pi 都是修改的同一个变量。注意几点
Circle.pi = 3
static final pi = 3.14
Math.PI
,上面我们自己写的 pi 只是为了举例需要至今为止,一共学习了四种变量,下面就对它们做一个简单对比
public class TestVariable {
public static void main(String[] args) {
m(10);
if (true) {
C c1 = new C(30); // 出了if语句块,c1 对象就无法使用了,随带的它内部的对象变量也失效
}
}
static void m(int a) { // 1. 参数变量, 作用范围是从方法调用开始,直到方法调用结束
for (int i = 0; i < 10; i++) {
int b = 20; // 2. 局部变量, 作用范围从定义开始,到包围它的 } 为止, 必须赋初值才能使用
System.out.println(b);
}
}
}
class C {
int c; // 3. 对象变量(成员变量) 从对象创建开始, 到对象不能使用为止
public C(int c) {
this.c = c;
}
static int d = 40; // 4. 静态变量, 从类加载开始, 到类卸载为止
}
阅读代码发现,这两个类中有一些相同的对象变量和方法代码,能否减少这两个类的重复代码,答案是继承
继承的语法
class 父类 {
字段;
方法() {}
}
class 子类 extends 父类 {
}
可以用父子类继承的方式减少重复声明,例如 A 是父类,B,C 类是子类,那么
class A {
String name;
void test() {}
}
class B extends A {
}
class C extends A {
}
但注意,构造方法不能继承
class A {
String name;
A(String name) {
this.name = name;
}
void test() {}
}
class B extends A {
B(String name) {
super(name);
}
}
class C extends A {
C(String name) {
super(name);
}
}
回到我们的例子
第一步,减少重复的字段声明,定义一个父类型,里面放 p、m、yr 这三个字段
public class Calculator {
double p;
int m;
double yr;
Calculator(double p, int m, double yr) {
this.p = p;
this.m = m;
this.yr = yr;
}
}
然后子类中不必再写这三个字段
class Calculator0 extends Calculator{
Calculator0(double p, int m, double yr) {
super(p, m, yr);
}
// ...
}
class Calculator1 extends Calculator{
Calculator1(double p, int m, double yr) {
super(p, m, yr);
}
// ...
}
第二步,分析哪些代码重复:
可以看到详情中生成一行数据的代码重复了,抽取为父类的方法,然后子类可以重用
public class Calculator {
// ...
String[] createRow(double payment, int i, double payInterest, double payPrincipal) {
return new String[]{ // 一行的数据
(i + 1) + "",
NumberFormat.getCurrencyInstance().format(payment),
NumberFormat.getCurrencyInstance().format(payPrincipal),
NumberFormat.getCurrencyInstance().format(payInterest),
NumberFormat.getCurrencyInstance().format(p)
};
}
}
例如
public class Calculator0 extends Calculator {
// ...
@Override
String[][] details() {
String[][] a2 = new String[m][];
double mr = yr / 12 / 100.0;
double pow = Math.pow(1 + mr, m);
double payment = p * mr * pow / (pow - 1); // 月供
for (int i = 0; i < m; i++) {
double payInterest = p * mr; // 偿还利息
double payPrincipal = payment - payInterest; // 偿还本金
p -= payPrincipal; // 剩余本金
// 这里重用了从父类继承的方法
a2[i] = createRow(i, payment, payPrincipal, payInterest);
}
return a2;
}
}
继承能够减少字段定义和方法定义的重复代码
java 中的类型分成了两大类
基本类型 primitive type
引用类型 reference type
包装类型 | 基本类型 | 备注 |
---|---|---|
Byte | byte | |
Short | short | |
Integer | int | |
Long | long | |
Float | float | |
Double | double | |
Character | char | |
Boolean | boolean |
隐式转换
byte a = 10;
int b = a; // 自动从 byte 转换为 int
强制转换
int c = 20;
byte d = (byte) c; // 在圆括号内加上要转换的目标类型
强制转换可能损失精度
int c = 1000;
byte d = (byte) c; // byte 的数字范围就是在 -128 ~ 127,存不下 1000,最后结果是 -24
int a = 20;
Integer b = a;
Integer c = new Integer(30);
int d = c;
顺箭头方向(向上)隐式转换,即子类可以用它的更高层的类型代表,表达一种是一个的关系
例如一个猫对象,可以隐式转换为动物
Animal a = new Cat(); // 用父类型的变量 a 代表了一只猫对象
Object b = new Cat(); // 用祖先类型的变量 b 代表了一只猫对象
逆箭头方向(向下)首先要符合是一个规则,然后用显式强制转换
如果一个动物变量,它代表的是一个猫对象,可以通过强制转换还原成猫
Animal a = new Cat();
Cat c = (Cat) a;
如果一个动物变量,它代表的是一个猫对象,即使强制转换,也不能变成狗,编译不报错,但运行就会出现 ClassCastException
Animal a = new Cat();
Dog d = (Dog) a;
为什么需要向上转型?主要是为了使用父类统一处理子类型
例1:
static void test(Animal a) {
}
这时,此方法既可以处理猫对象,也可以处理狗对象
test(new Cat());
test(new Dog());
例2:用父类型的数组,可以既装猫对象,也装狗对象
Animal[] as = new Animal[]{ new Cat(), new Dog() };
Animal a = new Cat();
如果想知道变量 a 代表对象的实际类型,可以使用
System.out.println(a.getClass()); // 输出结果 class com.itheima.Cat
如果想检查某个对象和类型之间是否符合【是一个】的关系,可以使用
Animals a = new Cat();
Object b = new Cat();
System.out.println(a instanceof Cat); // true
System.out.println(a instanceof Dog); // false
System.out.println(b instanceof Animal);// true
经常用在向下转型之前,符合是一个的关系,再做强制类型转换
除了以上转换规则,在赋值、方法调用时,一旦发现类型不一致,都会提示编译错误,需要使用一些转换方法才行
例如:两个字符串对象要转成整数做加法
String a = "1";
String b = "2";
System.out.println(a + b); // 这样不行,字符串相加结果会拼接为 12
System.out.println(Integer.parseInt(a) + Integer.parseInt(b)); // 转换后再相加,结果是 3
例如,我这里有多个汽车对象,调用这些汽车对象的 update 方法修改坐标,表面调用的方法都一样,但实际的效果是,它们各自的运动轨迹不同
function draw() {
for (let i = 0; i < cars.length; i++) {
let c = cars[i];
c.update(); // update 方法用来修改坐标
c.display();
}
}
再比如,我有一个 getAnimals() 方法,会传递过来 Animal 动物数组,但遍历执行 Animal 的方法 say,行为不同
public class TestAnimal {
public static void main(String[] args) {
Animal[] animals = getAnimals();
for (int i = 0; i < animals.length; i++) {
Animal a = animals[i];
a.say();
}
}
}
会输出
喵~
汪汪汪
哼哧哼哧
如果像上两个例子中体现的:同一个方法在执行时,表现出了不同的行为,称这个方法有多态的特性。
不是所有方法都有多态,像之前写过的静态方法,不管怎么调用,表现出的行为都是一样的。那么要成为这种多态方法要满足哪些条件呢?先来看看多态这个词是怎么来的
多态,英文 polymorphism 本意是多种形态,是指执行一个相同的方法,最终效果不同。为什么会有这种效果?
方法虽然都是同一个,但调用它们的对象相同吗?看起来都是 Animal 啊,其实不是
方法具备多态性的两个条件:
用父类型代表子类对象,有了父类型才能代表多种子类型,只有子类型自己,那将来有多种可能吗,不行吧?
第二个条件,是子类和父类得有一个相同的 say 方法。如果子类存在与父类相同的方法,称发生了方法重写。重写方法要满足:
只有重写了,才能表现出多种形态,如果没有重写,调用的都是父类方法,最终的效果是相同的,没有多态了
表面上调用的是 Animal 父类的 say 方法,实际内部会执行下面的逻辑判断
总结一下,具有这种多态特性的方法,调用内部就会走这样很多的判断逻辑,当然这些判断是 JVM 虚拟机帮我们判断的,不需要我们自己判断。简单的说,多态方法调用时,得先看变量所代表的对象真正类型是什么。是狗走狗的逻辑,是猫走猫的逻辑。然后呢,优先执行真正类型中的重写方法。如果没有重写方法呢?才执行父类中的方法。这就是这种多态方法执行的流程。
伪代码如下:
Animal a = ...
Class c = a.getClass() // 获得对象实际类型
if(c == Animal.class) {
执行 Animal.say()
} else if(c == Dog.class && Dog 重写了 say 方法) {
执行 Dog.say()
} else if(c == Cat.class && Cat 重写了 say 方法) {
执行 Cat.say()
} else if(c == Pig.class && Pig 重写了 say 方法) {
执行 Pig.say()
} else {
执行 Animal.say()
}
在控制器代码中,需要用 if else 来判断使用哪个 Calculator 对象完成计算,Calculator0 还是 Calculator1,将来如果贷款类型越来越多,就要写好多 if else,如何避免呢?利用多态的原理,让 jvm 帮我们做判断
原来的
显然不行
改写如下:
class Calculator {
// ...
String[] cal() {
return null;
}
String[][] details() {
return null;
}
}
class Calculator0 extends Calculator {
Calculator0(double p, int m, double yr) {
super(p, m, yr);
}
@Override
String[] cal() {
// ...
}
@Override
String[][] details() {
// ...
}
}
class Calculator1 extends Calculator {
Calculator1(double p, int m, double yr) {
super(p, m, yr);
}
@Override
String[] cal() {
// ...
}
@Override
String[][] details() {
// ...
}
}
根据类型创建不同 Calculator 对象有点小技巧(避免了创建对象时的 if else),如下:
Calculator[] getCalculator(double p, int m, double yr) {
return new Calculator[] {
new Calculator0(p, m, yr),
new Calculator1(p, m, yr)
};
}
最后通过父类型来执行,表面上是调用 Calculator 父类的 cal() 和 details() 方法,但实际执行的是某个子类的 cal() 和 details() 方法,通过多态,避免了方法调用时的 if else 判断
@Controller
public class CalController {
Calculator[] getCalculator(double p, int m, double yr) {
return new Calculator[] {
new Calculator0(p, m, yr),
new Calculator1(p, m, yr)
};
}
@RequestMapping("/cal")
@ResponseBody
String[] cal(double p, int m, double yr, int type) {
Calculator[] cs = getCalculator(p, m, yr);
return cs[type].cal();
}
@RequestMapping("/details")
@ResponseBody
String[][] details(double p, int m, double yr, int type) {
Calculator[] cs = getCalculator(p, m, yr);
return cs[type].details();
}
}
cs[type] 是根据类型找到对应的子类对象,例如
多态有什么好处呢?
新增一个 Calculator 子类
public class Calculator2 extends Calculator{
Calculator2(double p, int m, double yr) {
super(p, m, yr);
}
@Override
String[] cal() {
return new String[]{
NumberFormat.getCurrencyInstance().format(p),
NumberFormat.getCurrencyInstance().format(0)
};
}
@Override
String[][] details() {
String[][] a2 = new String[m][];
double payment = p / m;
for (int i = 0; i < m; i++) {
p -= payment;
a2[i] = createRow(payment, i, 0, payment);
}
return a2;
}
}
原有代码只需很少改动(扩展性高了)
对于原有的这两个方法来讲,它需要关心你到底是哪个子类对象吗?它不需要关心,因为对于它来讲,它都是统一按照父类型来处理的,通过父类型多态调用方法,具体该调哪个子类方法,多态内部就处理好了
@Controller
public class CalController {
Calculator[] getCalculator(double p, int m, double yr) {
return new Calculator[] {
new Calculator0(p, m, yr),
new Calculator1(p, m, yr),
new Calculator2(p, m, yr)
};
}
@RequestMapping("/cal")
@ResponseBody
String[] cal(double p, int m, double yr, int type) {
Calculator[] cs = getCalculator(p, m, yr);
return cs[type].cal();
}
@RequestMapping("/details")
@ResponseBody
String[][] details(double p, int m, double yr, int type) {
Calculator[] cs = getCalculator(p, m, yr);
return cs[type].cal();
}
}
可以尝试用原来 if else 的办法自己实现一遍,对比一下代码量。
关于多态的应用的例子讲完了,总结一下
前提
效果
什么时候使用多态
封装的例子我们前面已经见过一些了:
class Car {
constructor(color, speed, x, y) {
this.color = color; // 颜色
this.speed = speed; // 速度
this.stopped = true; // 是否停止
this.x = x;
this.y = y;
}
run() {
this.stopped = false;
}
update() {
if(this.stopped) {
return;
}
this.y -= this.speed;
if( this.y <= 20) {
this.y = 20;
}
}
display() {
fill(this.color);
rect(this.x, this.y, 10, -20);
}
}
其根本问题在于,使用者直接使用了字段,绕过了 update 方法对 y 值的处理,解决方法也很简单,就是将字段的访问权限设置为私有,字段定义时前面加 # 即可,其它用 x,y 的地方也替换为 #x 和 #y
class Car {
#x; // 设置为私有
#y; // 设置为私有
constructor(color, speed, x, y) {
this.color = color; // 颜色
this.speed = speed; // 速度
this.stopped = true; // 是否停止
this.#x = x;
this.#y = y;
}
update() {
if(this.stopped) {
return;
}
this.#y -= this.speed;
if( this.#y <= 20) {
this.#y = 20;
}
}
// ...
}
这回再执行 cars[0].#y = 20 就会告诉你私有字段不能访问了,这样字段只能在类的内部可以访问,出了类的范围就不能使用了,将来 Java 中会有类似的控制手段。
Java 中可以用访问修饰符来对字段或方法进行访问权限控制,一共有四种
名称 | 访问权限 | 说明 |
---|---|---|
public | 标识的【字段】及【方法】及【类】,谁都能使用 | |
protected | 标识的【字段】及【方法】,只有同包类、或是子类内才能使用 | |
标识的【字段】及【方法】及【类】,只有同包类才能使用 | 默认访问修饰符 | |
private | 标识的【字段】及【方法】只有本类才能使用(或内部类) |
package com.itheima.encapsulation;
public class Car {
private int y; // 私有的
private void test() { } // 私有的
void update() {
// 本类内可以使用
System.out.println(this.y);
this.test();
}
}
package com.itheima.encapsulation; // 同包测试类
public class Test1 {
public static void main(String[] args) {
Car car = new Car();
System.out.println(car.y); // 错误,不能访问 private 字段
car.test(); // 错误,不能访问 private 方法
}
}
package com.itheima.encapsulation;
public class Car {
int y; // 默认的
void test() {} // 默认的
void update() {
// 本类内可以使用
System.out.println(this.y);
this.test();
}
}
package com.itheima.encapsulation; // 同包测试类
public class Test2 {
public static void main(String[] args) {
Car car = new Car();
System.out.println(car.y); // 同包可以使用
car.test(); // 同包可以使用
}
}
package com.itheima; // 不同包测试类
import com.itheima.encapsulation.Car;
public class Test3 {
public static void main(String[] args) {
Car car = new Car();
System.out.println(car.y); // 错误,不同包不能访问 默认 字段
car.test(); // 错误,不同包不能访问 默认 方法
}
}
package com.itheima.encapsulation;
public class Car {
protected int y; // 受保护的
protected void test() {} // 受保护的
// 本类内可以使用
void update() {
System.out.println(this.y);
this.test();
}
}
package com.itheima; // 不同包子类
import com.itheima.encapsulation.Car;
public class SubCar extends Car {
void display() {
System.out.println(this.y); // 不同包子类内可以使用
this.test(); // 不同包子类内可以使用
}
}
尽可能让访问范围更小
JavaBean 规范
例子
class Teacher implements Serializable {
private String name; // 小写
private boolean married; // 已婚
private int age;
public boolean isMarried() { // 对 boolean 类型,用这种 isXXX
return this.married;
}
public void setMarried(boolean married) {
this.married = married;
}
// get 方法 用来获取私有字段值
public String getName() { // get 后面单词首字母要大写
return this.name;
}
// set 方法 用来修改私有字段值
public void setName(String name) {
this.name = name;
}
public Teacher(String name, boolean married) {
this.name = name;
this.married = married;
}
public Teacher() {
}
}
测试类
public class TestJavaBean {
public static void main(String[] args) {
Teacher t = new Teacher("张老师", false);
// 全部改用公共方法来间接读写字段值
System.out.println(t.getName());
System.out.println(t.isMarried());
t.setMarried(true);
System.out.println(t.isMarried());
}
}
Java Bean 主要用来封装数据,不会提供哪些包含业务逻辑的方法
最后要区分两个名词:字段和属性
这节课来学习单继承的问题
咱们都知道,java 中只支持单继承,也就是对于子类来讲,只能继承一个父类,但这样会出现代码重用方面的问题。看这个例子
上面的问题,究其本质,是因为 Java 只支持单继承,若想补足这方面的短板,需要用到接口,看这张图:
这些继承关系不变,但把重复的代码放在接口当中, swimmable 里放游泳方法,flyable 里放飞翔方法,然后要重用方法的类实现它们。
一个类只能继承一个父类,但一个类可以实现多个接口,使用接口就解决了刚才的问题
接口里主要提供给的都是方法,代表的是具备某方面的能力,能游泳,能飞翔,因此命名上常用 able
它的语法如下
interface A {
public default void a() {}
}
interface B {
public default void b() {}
}
// C 从 A, B 两个接口重用方法 a() 和 b()
class C implements A, B {
}
解决之前的问题
public class TestInterface1 {
public static void main(String[] args) {
Duck d = new Duck();
d.swim();
d.fly();
}
}
interface Swimmable {
default void swim() {
System.out.println("游泳");
}
}
interface Flyable {
default void fly() {
System.out.println("飞翔");
}
}
class Duck implements Swimmable, Flyable {
}
刚才我们学习了接口的第一个特性,解决单继承的问题,接下来看看接口的第二个特性,接口方法也支持多态。
方法多态的两个条件需要进一步完善
看这张图,上面这是接口E,下面这俩类 F、G 实现了接口,他俩以后可以叫做实现类,看一下这种上下级关系就可以知道,它们之间符合向上转型,F,G能够沿箭头向上转换为接口类型,因此能用接口类型代表实现类对象
先来看第一条,接口类型可以代表实现类对象
public class TestInterface2 {
public static void main(String[] args) {
E[] array = new E[] {
new F(),
new G()
};
}
}
interface E {
}
class F implements E {
}
class G implements E {
}
再看第二条,方法重写
public class TestInterface2 {
public static void main(String[] args) {
E[] array = new E[] {
new F(),
new G()
};
for (int i = 0; i < array.length; i++) {
E e = array[i];
e.e(); // 多态
}
}
}
interface E {
default void e() {
System.out.println("e");
}
}
class F implements E {
@Override
public void e() {
System.out.println("f");
}
}
class G implements E {
@Override
public void e() {
System.out.println("g");
}
}
其实要使用接口多态,更多地是使用一种抽象方法,而非默认方法,所谓抽象方法仅有方法声明,没有方法体代码。
你看我用抽象方法代替掉这里的默认方法,它包含 abstract 关键字,而且也只能是 public 的,平时这俩关键字都可以省略不写
public class TestInterface2 {
public static void main(String[] args) {
E[] array = new E[] {
new F(),
new G()
};
for (int i = 0; i < array.length; i++) {
E e = array[i];
e.e(); // 多态
}
}
}
interface E {
void e(); // 抽象方法,没有方法体,只能是 public 的,省略了 public abstract
}
class F implements E {
@Override
public void e() { // 默认
System.out.println("f");
}
}
class G implements E {
@Override
public void e() {
System.out.println("g");
}
}
为啥抽象方法设计为不需要方法体呢?因为你看:
另外,抽象方法有个好处:它强制了实现类要实施方法重写,如果实现类没有重写,语法上会报错
接口封装的更为彻底
public class TestInterface3 {
public static void main(String[] args) {
M m = new N(); // 用接口类型代表了实现类对象
m.m(); // 只能调用接口中定义的方法
}
}
interface M {
void m(); // public abstract
}
class N implements M {
public String name;
@Override
public void m() {
System.out.println("m");
}
public void n() {
System.out.println("n");
}
}
封装的关键在于,对外隐藏实现细节,接口完美地做到了这一点
经验
- 在声明方法的参数、返回值,定义变量时,能用接口类型,就用接口类型,有更好的扩展性
之前我们讲面向对象设计,都是把数据和逻辑放在一起,这是理想情况。
现实情况是,把对象分为两类,一类专门存数据,一类专门执行逻辑,如下
混在一起有什么缺点呢
- 数据是方法调用时才能确定的,只有请求来了,才知道数据(p,m, yr)是什么,才能根据它们创建这个既包含数据,也包含逻辑的对象,而对象的逻辑部分是一样的,重复创建感觉有点浪费
- 如果把数据和逻辑分离开
- 数据对象才需要每次请求来了创建
- 逻辑对象只需一开始创建一次就足够了
因此把数据和逻辑分成 java bean 和 service 能让你的代码更灵活,这也是这么做的目的
存数据的就是一个 Java Bean
public class Calculator {
private double p;
private int m;
private double yr;
public Calculator(double p, int m, double yr) {
this.p = p;
this.m = m;
this.yr = yr;
}
// 省略 get set 方法
}
存逻辑的叫做 XxxService,例如
class CalculatorService0 {
public String[] cal(Calculator c) {
double p = c.getP();
int m = c.getM();
double mr = c.getYr() / 12 / 100.0;
double pow = Math.pow(1 + mr, m);
double payment = p * mr * pow / (pow - 1);
return new String[]{
NumberFormat.getCurrencyInstance().format(payment * m),
NumberFormat.getCurrencyInstance().format(payment * m - p)
};
}
public String[][] details(Calculator c) {
double p = c.getP();
int m = c.getM();
String[][] a2 = new String[m][];
double mr = c.getYr() / 12 / 100.0;
double pow = Math.pow(1 + mr, m);
double payment = p * mr * pow / (pow - 1); // 月供
for (int i = 0; i < m; i++) {
double payInterest = c.getP() * mr; // 偿还利息
double payPrincipal = payment - payInterest; // 偿还本金
p -= payPrincipal; // 剩余本金
a2[i] = createRow(payment, i, payInterest, payPrincipal, p);
}
return a2;
}
private String[] createRow(double payment, int i, double payInterest, double payPrincipal, double p) {
return new String[]{ // 一行的数据
(i + 1) + "",
NumberFormat.getCurrencyInstance().format(payment),
NumberFormat.getCurrencyInstance().format(payPrincipal),
NumberFormat.getCurrencyInstance().format(payInterest),
NumberFormat.getCurrencyInstance().format(p)
};
}
}
显然,Service 根据计算方式不同,有多个
public interface Cal {
String[] cal(Calculator c);
String[][] details(Calculator c);
default String[] createRow(double payment, int i, double payInterest, double payPrincipal, double p) {
return new String[]{ // 一行的数据
(i + 1) + "",
NumberFormat.getCurrencyInstance().format(payment),
NumberFormat.getCurrencyInstance().format(payPrincipal),
NumberFormat.getCurrencyInstance().format(payInterest),
NumberFormat.getCurrencyInstance().format(p)
};
}
}
class CalculatorService0 implements Cal {
public String[] cal(Calculator c) {
// ...
}
public String[][] details(Calculator c) {
// ...
}
}
class CalculatorService1 implements Cal {
public String[] cal(Calculator c) {
// ...
}
public String[][] details(Calculator c) {
// ...
}
}
class CalculatorService2 implements Cal {
public String[] cal(Calculator c) {
// ...
}
public String[][] details(Calculator c) {
// ...
}
}
Controller 的代码变成了
@Controller
public class CalController {
// ...
private Cal[] calArray = new Cal[]{
new CalculatorService0(),
new CalculatorService1(),
new CalculatorService2()
};
@RequestMapping("/cal")
@ResponseBody
String[] cal(double p, int m, double yr, int type) {
System.out.println(calArray[type]);
return calArray[type].cal(new Calculator(p, m, yr));
}
@RequestMapping("/details")
@ResponseBody
String[][] details(double p, int m, double yr, int type) {
return calArray[type].details(new Calculator(p, m, yr));
}
}
一直以来,都是我们自己用 new 关键字配合构造方法来创建对象,但我们现在用的是 Spring 框架,可以把一些创建对象的活交给 Spring 框架去做。
那么 Spring 框架怎么创建对象呢?它主要是配合一些注解来完成对象的创建,例如,我们一直在用的 @Controller 注解,当 Spring 程序运行时,它会检查这些类上有没有加一些特殊注解,例如它发现这个类上加了 @Controller 注解,框架就知道,该由框架来创建这个 CalculatorController 对象,默认只会创建一个。
这样的注解还有不少,我们现在需要掌握的有 @Controller 算一个,还有一个是 @Service,试试把这些 Service 类的创建交给 Spring 吧:
@Service
class CalculatorService0 implements Cal {
public String[] cal(Calculator c) {
}
public String[][] details(Calculator c) {
}
}
@Service
class CalculatorService1 implements Cal {
public String[] cal(Calculator c) {
}
public String[][] details(Calculator c) {
}
}
@Service
class CalculatorService2 implements Cal {
public String[] cal(Calculator c) {
}
public String[][] details(Calculator c) {
}
}
把对象的创建权交给 Spring 来完成,对象的创建权被交出去,这称之为控制反转
那么我们的代码里怎么拿到 Spring 创建的对象呢?
@Controller
public class CalController {
// ...
@Autowired
private Cal[] calArray;
// ...
}
这儿又要引入一个相关的名词:依赖注入
比如说,这里的 控制器 需要 service 才能工作,就可以说控制器对象依赖于 service 对象,缺了这些依赖对象行吗?不行吧。怎么找到这些依赖对象呢?如果是框架帮你找到这些依赖对象,按一定规则提供给你,就称之为依赖注入
怎么让框架帮你找到这些依赖对象呢?答案是 @Autowired
在 Cal[] 数组上添加 @Autowired 即可,它是根据类型去问 Spring 要对象,Spring 中有很多对象,具体要哪个对象呢?答案是根据类型要
Spring 还可以根据请求中的多个查询参数,帮我们创建 JavaBean 数据对象
@Controller
public class CalController {
// ...
@Autowired
private Cal[] calArray;
@RequestMapping("/cal")
@ResponseBody
String[] cal(Calculator c, int type) {
return calArray[type].cal(c);
}
@RequestMapping("/details")
@ResponseBody
String[][] details(Calculator c, int type) {
return calArray[type].details(c);
}
}
注意
- 不是所有 JavaBean 对象都应该交给 Spring 创建,一般只有请求中的数据,才会这么做
这节课讲讲包结构约定,之前我们也讲过当类的数目比较多时,要根据它们的功能,进一步划分 package,以便更好的管理。 目前可以划分 3 个包
最后要注意一下入口类的位置,必须放在 service, controller 这几个包的上一层,为什么呢?
这个入口类,它还肩负了一个职责,查找 @Service, @Controller 等注解的类,然后创建对象。它查找的范围是在这个类的当前 package 之内,因此如果 service,controller 等包如果不在这个 package 内,那么会查找不到
接下来需要讲解的是 ArrayList,它常常被用来替代数组
数组的缺点:不能自动扩容,比如已经创建了大小为 5 的数组,再想放入一个元素,就放不下了,需要创建更大的数组,还得把旧数组的元素迁移过去。
自己来做比较麻烦
public class TestArray {
public static void main(String[] args) {
String[] arr0 = new String[]{"a", "b", "c", "d", "e"};
String[] arr1 = new String[6];
for (int i = 0; i < arr0.length; i++) {
arr1[i] = arr0[i];
}
arr1[5] = "f";
System.out.println(Arrays.toString(arr0));
System.out.println(Arrays.toString(arr1));
}
}
这时可以使用 ArrayList 来替代 String[],它的内部仍然是数组,只是封装了更多实用的逻辑
ArrayList list = new ArrayList(5); // 指定初始容量为 5, 如果不指定默认是 10
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
// 不用担心容量不够, 它内部封装了扩容的逻辑, 每次按原来容量的 1.5 扩容, 向下取整, 如 5->7->10
list.add("f");
System.out.println(list);
前面说了,ArrayList 封装了扩容逻辑,这对使用者当然是一件好事,就像我们平时使用家用电器似的,不需要知道这些电器内部如何工作,只需要会按它们对外的几个开关、按钮就足够了。像这个 ArrayList,我们只需要会创建对象,调用它的add方法就够用了,不需要看它内部结构
不过呢,有利必然有弊,比如你想观察验证它的扩容规则是不是真的是如我所说的 1.5 倍,封装就会带来障碍。这里交给大家一招深入对象内部,探究它组成结构的好办法,debug 调试。
debug 的第一步要添加断点,所谓断点就是让代码运行到断点处先停下来,不要向下走了,这样就能方便我们观察对象状态。在 18 行加一个断点,然后记得用调试模式运行代码
要查看 list 的详情,先按照下图进行选择查看方式
这样就可以观察 list 的内部结构了
以上介绍了一些基本的调试方法,更多的调试方法请关注笑傲篇的高级内容
List 的遍历有多种办法,这里只介绍最简单的一种
for (Object e : list) {
System.out.println(e);
}
// 与之对比, 数组也能类似方式进行遍历
for (String s : arr0) {
System.out.println(s);
}
上例中的 list 是把元素当作 Object 加入到它的内部,再取出来也会当作 Object,有时候,就会不方便
可以用泛型来限制元素的存取类型,例如
ArrayList
专门存取字符串类型元素ArrayList
专门存取整数类型元素
例如
ArrayList<Integer> list = new ArrayList<Integer>(5);
list.add(1);
list.add(2);
list.add(3);
list.add(4); // 按 Integer 存
int sum = 0;
for (Integer i : list) { // 按 Integer 取
sum += i;
}
System.out.println(sum);
其中等式右边的
可以简化为 <>
List<Integer> list2 = List.of(1, 2, 3, 4);
System.out.println(list2.getClass()); // class java.util.ImmutableCollections$ListN
List<Integer> list3 = List.of(1, 2);
System.out.println(list3.getClass()); // class java.util.ImmutableCollections$List12
of 方法隐藏了内部实现细节,对于使用者不需要关心对象的实际类型。
要是真想知道这个对象的类型是什么可以不?可以用继承自 Object 的 getClass 方法获得对象的真正类型
String[]
和 ArrayList
都有一个缺点:查找其中某个元素不高效,例如:
public static String find1(String value) {
String[] array = new String[]{"小明", "小白", "小黑"};
for (String s : array) {
if (s.equals(value)) {
return s;
}
}
return null;
}
public static String find2(String value) {
List<String> list = List.of("小明", "小白", "小黑");
for (String s : list) {
if (s.equals(value)) {
return s;
}
}
return null;
}
可以想象,如果集合大小为 n,而要查找的元素是最后一个,需要比较 n 次才能找到元素
解决思路,人为建立一种【映射】关系,比如:
public static String find1(int key) {
String[] array = new String[]{"小明", "小白", "小黑"};
if (key < 0 || key >= array.length) {
return null;
}
return array[key];
}
public static String find2(int key) {
List<String> list = List.of("小明", "小白", "小黑");
if (key < 0 || key >= list.size()) {
return null;
}
return list.get(key);
}
但 0、1、2 意思不是很直白,如果元素数量较多,容易弄混,如果能起个更有意义的名字是不是更好?这就是下面要讲的 Map
什么是 Map 呢,很简单,它就是一组映射关系。
你看刚才的例子中,是建立了数组索引和数据元素之间的映射关系,根据索引快速找到元素。现在 map 也是一组映射关系,只是把索引变得更有意义而已。用 bright 映射小明,white 映射小白,black 映射小黑,前面的 bright、white、black 称之为 key ,key 需要唯一 , 后面这些称之为 value
代码如下:
public static String find3(String key) {
Map<String, String> map = Map.of(
"bright", "小明",
"white", "小白",
"black", "小黑"
);
return map.get(key);
}
Map 需要掌握的点:
for (Map.Entry<String, String> e : map.entrySet()) {
// e.getKey() 获取 key
// e.getValue() 获取 value
}
回忆之前我们对异常的使用,我们用异常改变了方法执行流程
public class TestTry {
public static void main(String[] args) {
System.out.println(1);
test(0.0);
System.out.println(3);
}
public static void test(double p) {
if(p <= 0.0) {
// 异常也是一个对象, 包含的是错误描述
throw new IllegalArgumentException("本金必须大于 0"); // 1 处
}
System.out.println(2);
}
}
输出
1
Exception in thread "main" java.lang.IllegalArgumentException: 本金必须大于 0
at com.itheima.module3.TestTry.test(TestTry.java:13)
at com.itheima.module3.TestTry.main(TestTry.java:7)
这个例子中,执行到 1 处出现了异常,后续的输出 2、3 的代码都不会执行了
但如果希望,一个方法出现异常后,不要影响其它方法继续运行,可以用下面的语法来处理
public class TestTry {
public static void main(String[] args) {
System.out.println(1);
try {
test(0.0);
} catch (IllegalArgumentException e) {
System.out.println(e);
}
System.out.println(3);
}
public static void test(double p) {
if (p <= 0.0) {
throw new IllegalArgumentException("本金必须大于 0");
}
System.out.println(2);
}
}
输出
1
java.lang.IllegalArgumentException: 本金必须大于 0
3
执行流程为
如果把 catch 的异常类型改为 NullPointerException
如果不加 try - catch 块,异常对象也会继续从 main 方法抛给 jvm,jvm 收到异常终止程序执行
如果把 catch 的异常类型改为 Exception
问题:为何之前我们控制器中出现的异常不用 try - catch 处理?
我们当然也能自己 catch 异常,但可悲的是,你就算 catch 住异常又能干什么呢?还得考虑自己如何把异常信息转换为响应,还不如不 catch,交给 Spring 去处理
异常按语法可以分成两类
分别举一个例子:throw 一个运行时异常,没有额外语法,此异常抛给上一层方法来处理
public static void test(double p) {
if (p <= 0.0) {
throw new IllegalArgumentException("本金必须大于 0");
}
System.out.println(2);
}
如果 throw 一个编译异常
public static void test(double p) {
if (p <= 0.0) {
throw new Exception("本金必须大于 0"); // 语法报错了!
}
System.out.println(2);
}
public static void test(double p) throws Exception {
if (p <= 0.0) {
throw new Exception("本金必须大于 0");
}
System.out.println(2);
}
但编译时异常的烦人之处在于,当编译时异常抛给上一层方法后,上一层方法也被迫做出类似的选择
如果无论是否出现异常,都一定要执行的代码,可以用 finally 语法
try {
} catch (Exception e) {
} finally {
}
其中 catch 不是必须的,可以 try 与 finally 一起用
那这个 finally 的使用场景是什么呢?
以后我们的代码常常需要与一些外部资源打交道,外部资源有文件、数据库等等。这些外部资源使用时都有个注意事项,就是用完后得把资源及时释放关闭,资源都是有限的,如果用完不关,最终会导致资源耗尽,程序也无法继续运行了。将来这边代表资源的对象一般都会提供一个名为 close 的方法,用来释放资源。显然在 finally 中调用资源的 close 方法最为科学
public class TestFinally {
public static void main(String[] args) {
Resource r = new Resource();
try {
System.out.println("使用资源");
int i = 1 / 0;
} catch (Exception e) {
System.out.println(e);
} finally {
r.close();
}
}
}
class Resource implements Closeable {
@Override
public void close() {
System.out.println("释放资源");
}
}
如果资源实现了 Closeable 接口,那么可以用 try-with-resource 语法来省略 finally
public class TestFinally {
public static void main(String[] args) {
// try - with - resource
try (Resource r = new Resource()) {
System.out.println("使用资源");
int i = 1 / 0;
} catch (Exception e) {
System.out.println(e);
}
}
}
class Resource implements Closeable {
@Override
public void close() {
System.out.println("释放资源");
}
}
eException <|-- ArithmeticException
RuntimeException <|-- NullPointerException
* Throwable 是异常中最顶层的父类
* getMessage() 提供获取异常信息的功能
* printStackTrace() 会在【标准错误】输出方法的调用链,用于定位错误位置
* Error 代表无药可救的异常,通常这种异常就算 catch 也救不了
* Exception 代表还可以救一救的异常,catch 后可以让程序恢复运行
* 我们见过的异常有
* IllegalArgumentException 非法参数异常
* ArrayIndexOutOfBoundsException 数组越界异常
* ArithmeticException 算术异常
* NullPointerException 空指针异常
## 3. Spring 处理异常
问题:为何之前我们控制器中出现的异常不用 try - catch 处理?
* 控制器方法是由 Spring 的方法来调用的,因此控制器方法中出现异常,会抛给 Spring 方法
* Spring 的方法内部用了 try - catch 来捕捉异常,并在 catch 块中会把异常信息作为响应返回
我们当然也能自己 catch 异常,但可悲的是,你就算 catch 住异常又能干什么呢?还得考虑自己如何把异常信息转换为响应,还不如不 catch,交给 Spring 去处理
## 4. 编译异常与运行时异常
异常按语法可以分成两类
* 运行时异常(也称未检查异常)
* Error 以及它的子类
* RuntimeException 以及它的子类
* 编译异常(也称检查异常)
* 除掉运行时以外的所有异常,都属于编译异常
分别举一个例子:throw 一个运行时异常,没有额外语法,此异常抛给上一层方法来处理
```java
public static void test(double p) {
if (p <= 0.0) {
throw new IllegalArgumentException("本金必须大于 0");
}
System.out.println(2);
}
如果 throw 一个编译异常
public static void test(double p) {
if (p <= 0.0) {
throw new Exception("本金必须大于 0"); // 语法报错了!
}
System.out.println(2);
}
public static void test(double p) throws Exception {
if (p <= 0.0) {
throw new Exception("本金必须大于 0");
}
System.out.println(2);
}
但编译时异常的烦人之处在于,当编译时异常抛给上一层方法后,上一层方法也被迫做出类似的选择
如果无论是否出现异常,都一定要执行的代码,可以用 finally 语法
try {
} catch (Exception e) {
} finally {
}
其中 catch 不是必须的,可以 try 与 finally 一起用
那这个 finally 的使用场景是什么呢?
以后我们的代码常常需要与一些外部资源打交道,外部资源有文件、数据库等等。这些外部资源使用时都有个注意事项,就是用完后得把资源及时释放关闭,资源都是有限的,如果用完不关,最终会导致资源耗尽,程序也无法继续运行了。将来这边代表资源的对象一般都会提供一个名为 close 的方法,用来释放资源。显然在 finally 中调用资源的 close 方法最为科学
public class TestFinally {
public static void main(String[] args) {
Resource r = new Resource();
try {
System.out.println("使用资源");
int i = 1 / 0;
} catch (Exception e) {
System.out.println(e);
} finally {
r.close();
}
}
}
class Resource implements Closeable {
@Override
public void close() {
System.out.println("释放资源");
}
}
如果资源实现了 Closeable 接口,那么可以用 try-with-resource 语法来省略 finally
public class TestFinally {
public static void main(String[] args) {
// try - with - resource
try (Resource r = new Resource()) {
System.out.println("使用资源");
int i = 1 / 0;
} catch (Exception e) {
System.out.println(e);
}
}
}
class Resource implements Closeable {
@Override
public void close() {
System.out.println("释放资源");
}
}