面向对象进阶3(多态、内部类、常用API)
# 面向对象进阶3(多态、内部类、常用 API)
# 1. 多态:面向对象三大特征之三
# 1.1 多态的概述、形式
- 啥是多态
- 同一父类型的对象,执行同一个行为时,会表现出不同的行为特征。
- 多态的常见形式
父类类型 对象名称 = new 子类构造器;
(父类类型指向一个子类的对象)接口 对象名称 = new 实现类构造器;
(接口相当于一个干爹,实现类相当于儿子)- 具体的行为方法在子类中重写
- 多态中成员访问特点-
- 方法调用:编译看左边,运行看右边。
- 变量调用:编译看左边,运行也看左边。(多态侧重行为多态,变量没有多态的概念)
- 即对于变量调用,还是会使用父类中的变量,而方法是会使用子类中重写的方法的。
- 多态的前提
- 有继承/实现关系
- 有父类引用指向子类对象
- 有方法重写
# 1.2 多态的优势
- 在多态形式下,右边对象可以实现解耦合,便于扩展和维护。
Animal a = new Dog();
a.run(); // 后续业务行为随对象而变,后续代码无需修改
2
上述代码中,若后续业务需要将 Dog 子类改为Tortoise 子类,则直接改“=”右侧的子类类型即可,后续代码中的行为随着对象的改变而直接变化了,无需修改很多后续的代码。
- 定义方法的时候,使用父类型作为参数,该方法就可以接收这父类的一切子类对象,体现出多态的扩展性与便利
多态下会产生的一个问题
多态下不能使用子类的独有功能,因为编译时会使用左侧的父类中的行为。
solve:使用强制类型转换,将父转为子类型。
# 1.3 多态下引用数据类型的类型转换
在多态下会有两种类型转换:
- 自动类型转换(从子到父)
- 强制类型转换(从父到子)
关于强制类型转换:
子类类型 对象变量 = (子类)父类类型的变量
代码示例:
// 自动类型转换 Animal a = new Dog(); a.run(); // 强制类型转换:可以实现调用子类独有功能的 Dog d = (Dog) a; d.lookDoor();
1
2
3
4
5
6作用:可以解决多态下的劣势,可以实现调用子类独有的功能
注意: 如果转型后的类型和对象真实类型不是同一种类型,那么在转换的时候就会报错 ClassCastException(类型转换异常)
强转转换的一点建议
规定:有继承或者实现关系的2个类型就可以强制类型转换,故编译时可能没问题,但运行时发现强制转换后的类型不是对象真实类型而报错。
Java 建议强转转换前使用 instanceof 判断当前对象的真实类型,再进行强制转换。
关于 instanceof 关键字的使用:
变量名 instanceof 真实类型
判断关键字左边的变量指向的对象的真实类型,是否是右边的类型或者是其子类类型,是则返回true,反之false。
代码示例:
public static void go(Animal a){ // 参数类型为父类类型,保证各种动物都可以使用go方法
System.out.println("预备~~~");
a.run();
// 独有功能
if(a instanceof Tortoise){
Tortoise t = (Tortoise) a;
t.layEggs();
}else if(a instanceof Dog){
Dog d1 = (Dog) a;
d1.lookDoor();
}
System.out.println("结束~~~~");
}
2
3
4
5
6
7
8
9
10
11
12
13
# 1.4 多态的案例
需求:设计一个电脑对象,可以安装2个USB设备。鼠标:被安装时可以完成接入、调用点击功能、拔出功能;键盘:被安装时可以完成接入、调用打字功能、拔出功能。
分析:
①定义一个USB的接口(申明USB设备的规范必须是:可以接入和拔出)。
②提供2个USB实现类代表鼠标和键盘,让其实现USB接口,并分别定义独有功能。
③创建电脑对象,创建2个USB实现类对象,分别安装到电脑中并触发功能的执行。
# 2. 内部类
# 2.1 内部类的概述
内部类就是定义在一个类里面的类,里面的类可以理解成(寄生),外部类可以理解成(宿主)。
如,心脏和人的关系:
public class People{
// 内部类
public class Heart{
}
}
2
3
4
5
# 2.2 内部类的使用场景、作用
- 当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么整个内部的完整结构可以选择使用内部类来设计。
- 内部类通常可以方便访问外部类的成员,包括私有的成员。
- 内部类提供了更好的封装性,内部类本身就可以用 private、protected 等修饰,封装性可以做更多控制。(外部类只能用 public 修饰)
# 2.3 内部类的分类
# 2.3.1 静态内部类
- 有static修饰,属于外部类本身
- 特点、使用与普通类是一样的,类有的成分它都有,只是位置在别人里面而已
- 可以直接访问外部类的静态成员,不能直接访问外部类的实例成员(可以创建一个外部类对象,再访问其实例成员)
- 注意:开发中实际上用的还是比较少
- 定义示例:
public class Outer{
// 静态成员内部类
public static class Inner{
}
}
2
3
4
5
- 创建对象的格式:
Outer.Inner in = new Outer.Inner();
# 2.3.2 成员内部类
- 无 static 修饰,属于外部类的对象
- JDK16 之前,成员内部类中不能定义静态成员,JDK 16 开始也可以定义静态成员了
- 特点:可以直接访问外部类的静态成员,实例方法中可以直接访问外部类的实例成员(因为必须先有外部类对象,才能有成员内部类对象,所以可以直接访问外部类对象的实例成员)
- 定义示例:
public class Outer {
// 成员内部类
public class Inner {
}
}
2
3
4
5
- 创建对象格式:
Outer.Inner in = new Outer().new Inner();
(注意外部类对象后面要加() ) - 相关面试题:
- 在成员内部类中访问所在外部类对象,格式:
外部类名.this.value
,如:System.out.println(People.this.heartbeat);
- 访问当前对象的成员变量,要用
this.value
- 在成员内部类中访问所在外部类对象,格式:
# 2.3.3 局部内部类
- 鸡肋语法,了解即可
- 局部内部类放在方法、代码块、构造器等执行体中
- 局部内部类编译后也会有对应的类文件,其类文件名为: 外部类$N内部类.class
# 2.3.4 匿名内部类
本质上是一个没有名字的局部内部类,定义在方法中、代码块中、等
作用:方便创建子类对象,最终目的为了简化代码编写
特点
- 匿名内部类写出来就会产生一个匿名内部类的对象
- 匿名内部类的对象类型相当于是当前 new 的那个的类型的子类类型(可以视为是 Animal 的子类 Tiger 类)
格式
new 类|抽象类名|或者接口名() {
重写方法;
};
2
3
Animal a = new Animal() {
public void run() {
}
};
a. run();
2
3
4
5
⚠️ 直接 new abstract_class()
是不可以的,故后面加上 {} 进行方法的重写,实现一个匿名内部类。
简单示例:
line3 等号后 ~ line8 的部分相当于 Tiger 类了。
public class Test {
public static void main(String[] args) {
Animal a = new Animal(){
@Override
public void run() {
System.out.println("老虎跑的块~~~");
}
};
a.run();
}
}
//class Tiger extends Animal{
// @Override
// public void run() {
// System.out.println("老虎跑的块~~~");
// }
//}
abstract class Animal{
public abstract void run();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2.3.5 匿名内部类常见使用形式
匿名内部类通常作为方法的实际参数进行传输。
示例代码:
public class Test2 {
public static void main(String[] args) {
go(new Swimming() {
@Override
public void swim() {
System.out.println("运动员泳🏊的贼快~~~~~");
}
});
/**
学生 老师 运动员可以一起参加游泳比赛
*/
public static void go(Swimming s){
System.out.println("开始。。。");
s.swim();
System.out.println("结束。。。");
}
}
interface Swimming{
void swim();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
对象回调:
提示
开发中不是我们主动去定义匿名内部类的,而是别人需要我们写或者我们可以写的时候才会使用。
匿名内部类的代码可以实现代码进一步的简化。
# 3. 常用 API
# 3.1 Object 类
Object 类的方法是一切子类对象都可以直接使用的,所以我们要学习 Object 类的方法。
一个类要么默认继承了 Object 类,要么间接继承了 Object 类,Object 类是 Java 中的祖宗类
Object类的常用方法
方法名 | 说明 |
---|---|
public String toString() | 默认是返回当前对象在堆内存中的地址信息:类的全限名@内存地址 |
public boolean equals(Object o) | 默认是比较当前对象与另一个对象的地址是否相同,相同返回true,不同返回false |
- 一般直接在 IDEA 中自动重写对应类的这两个方法即可。
toString 方法:
因为 toString 默认输出对象的地址在开发中毫无意义,更多是希望看到对象的内容数据)
toString 方法存在的意义就是为了被子类重写,以便返回对象的内容信息,而不是地址信息
equals 方法:
直接比较两个对象的地址是否相同完全可以用 “==” 替代 equals
父类 equals 方法存在的意义就是为了被子类重写,以便子类自己来定制比较规则
# 3.2 Objects 类
Objects 是一个工具类,提供了一些方法去完成一些功能
Objects 类与 Object 类也是继承关系。
Objects 类是从 JDK1.7 之后才有的。
官方在进行字符串比较时(自动重写的 equals 方法),没有用字符串对象的的 equals 方法,而是选择了 Objects 的 equals 方法来比较。
原因:使用Objects 的 equals 方法在进行对象的内容比较时会更安全。
使用 Objects 的 equals 方法在进行对象的内容比较时更安全的原因
Objects 的 equals 方法在比较时,底层会先进行非空判断,从而可以避免空指针异常。再进行 equals 比较
源码分析:
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
2
3
若直接使用 equals 方法比较时,可能会出现空指针异常,例如:
public class Test {
public static void main(String[] args) {
String s1 = null;
String s2 = new String("itheima");
// System.out.println(s1.equals(s2)); // 留下了隐患,可能出现空指针异常。
}
}
2
3
4
5
6
7
8
String 的 equals 方法 VS 类的 equals 方法
字符串可以直接用 equals 比较内容,因为 String 类已经重写过该方法了;但自定义的类要重写 equals 方法。
⚠️ String 不能用 “==” 比较内容是否一样,因为 “==” 会视为比较的二者为不同的对象
# 3.3 StringBuilder 类
StringBuilder 是一个可变的字符串类,我们可以把它看成是一个对象容器。(String 是不可变字符串类)
作用:提高字符串的操作效率,如拼接、修改等。
StringBuilder 常用方法
方法名称 | 说明 |
---|---|
public StringBuilder append(任意类型) | 添加数据并返回StringBuilder对象本身 |
public StringBuilder reverse() | 将对象的内容反转,会直接将对象本身修改 |
public int length() | 返回对象内容长度 |
public String toString() | 通过toString()就可以实现把StringBuilder转换为String |
- StringBuilder 支持链式编程,e.g.
sb1.append("a").append("b").append("c").append("我爱你中国");
⚠️ 注意:StringBuilder 只是拼接字符串的手段,效率好,使用时最终还是要转成 String。 用 toString 恢复 String。故 定义字符串用 String,拼接、修改等操作字符串使用 StringBuilder。
示例代码:
StringBuilder sb2 = new StringBuilder();
sb2.append("123").append("456");
// 恢复成String类型
String rs = sb2.toString();
check(rs); // 使用字符串
public static void check(String data){
System.out.println(data);
}
2
3
4
5
6
7
8
9
内存原理
String 类拼接字符串时,一个加号会导致堆内存中创建两个对象,一个是拼接时的 StringBuilder 对象,一个是拼好后转换过去的 String 对象。
StringBuilder 类拼接字符串时,只会在堆内存中创建一个 StringBuilder 对象,所有拼接操作都在这个对象中完成。效率变高。
# 3.4 Math 类
包含执行基本数字运算的方法,Math类没有提供公开的构造器
**如何使用类中的成员呢?**看类的成员是否都是静态的,如果是,通过类名就可以直接调用
Math 类的常用方法
方法名 | 说明 |
---|---|
public static int abs(int a) | 获取参数绝对值 |
public static double ceil(double a) | 向上取整 |
public static double floor(double a) | 向下取整 |
public static int round(float a) | 四舍五入 |
public static int max(int a,int b) | 获取两个int值中的较大值 |
public static double pow(double a,double b) | 返回a的b次幂的值 |
public static double random() | 返回值为double的随机值,范围[0.0,1.0) |
# 3.5 System 类
System 也是一个工具类,代表了当前系统,提供了一些与系统相关的方法
System 类的常用方法
方法名 | 说明 |
---|---|
public static void exit(int status) | 终止当前运行的 Java 虚拟机,非零表示异常终止 |
public static long currentTimeMillis() | 返回当前系统的时间毫秒值形式 |
public static void arraycopy(数据源数组, 起始索引, 目的地数组, 起始索引, 拷贝个数) | 数组拷贝 (了解) |
注意:System.exit() 相当于删库跑路,可不能瞎用!!!
关于时间毫秒值:
计算机认为时间是有起点的,起始时间: 1970年1月1日 00:00:00
时间毫秒值:指的是从1970年1月1日 00:00:00走到此刻的总的毫秒数,应该是很大的。 1s = 1000ms。
System.currentTimeMillis()
可以用来进行时间的计算:性能分析。
# 3.6 BigDecimal 类
(大数据类型)用于解决浮点型运算精度失真的问题
浮点型运算的时候直接+ * / 可能会出现数据失真(精度问题)。
BigDecimal 的使用:
创建对象 BigDecimal 封装浮点型数据(最好的方式是调用方法)
⚠️ 阿里开发规范:禁止使用构造方法 BigDecimal(double)的方式把 double 值转化为 BigDerimal 对象。
原因:BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。如:BigDecimalg = new BigDecimal(0.1F);
实际的存储值为:0.10000000149
正例:优先推荐入参为String 的构造方法,或使用 BigDecimal 的 valueof 方法,此方法内部其实执行了Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
⚠️ 注意:BigDecimal 只是手段,效率好,使用时最终还是要转成 对应的 double 数据类型。 使用 doubleValue() 转为 double。
示例代码:
// 包装浮点型数据成为大数据对象 BigDeciaml
BigDecimal a1 = BigDecimal.valueOf(a);
BigDecimal b1 = BigDecimal.valueOf(b);
BigDecimal c1 = a1.add(b1);
// BigDecimal c1 = a1.subtract(b1);
// BigDecimal c1 = a1.multiply(b1);
// BigDecimal c1 = a1.divide(b1);
System.out.println(c1);
// 目的:double
double rs = c1.doubleValue();
System.out.println(rs);
2
3
4
5
6
7
8
9
10
11
12
⚠️ BigDecimal 是一定要精度运算的
如,10/3 就无法精确运算,故 BigDecimal 会抛出异常。
solve: public BigDecimal divide (另一个BigDecimal对象,精确几位,舍入模式)