十一、面向对象-进阶
1. 面向对象三大特征(一)-封装
1.1 概念
随着代码量的提升,类会越来越多,面向对象的开发原则我们需要遵循 高内聚,低耦合
高内聚:一个模块内部功能单一化,"自己的事情自己做"
低耦合:各个模块之间关系简单,"少依赖别人"
- 定义:所谓 封装,就是把客观事物封装成抽象概念的类,并且类把自己的数据和方法只向可信的类或者对象开放,向没必要开放的类或者对象隐藏;通俗来讲,也就是将数据和方法整合为类,然后把该隐藏的隐藏起来,该暴露的暴露出来,这就是封装性的设计思想
- 作用:保护数据安全性,避免外部随意修改,同时降低代码耦合度。
- 实现:通过访问修饰符(public、protect、缺省、private)控制访问权限。
1.2 如何实现封装
实现封装就是控制类或成员的可见性范围。这就需要依赖权限修饰符(public、protect、缺省、private)
| 修饰符 | 本类内部 | 本包内 | 其他包的子类 | 其他包非子类 |
|---|---|---|---|---|
| private | √ | × | × | × |
| 缺省 | √ | √ | × | × |
| protected | √ | √ | √ | × |
| public | √ | √ | √ | √ |
1.3 封装的体现
在实际开发中,我们通常会 私有化类的成员变量,也就是 成员变量使用private修饰,再为其提供 公共的get和set方法,对外暴露获取和修改属性的功能,而这正是封装思想的体现
这样设计的好处在于:
- 只能通过预设的方法来访问数据,从而可以在方法中添加其他限制逻辑,例如年龄字段赋值限制不能小于0等
- 便于维护,访问方式不变的情况下,可以实现无感知维护。例如 java8->java9 将 String 从 char[] 改为了 byte[],而对外的方法不变,我们使用者根本感觉不到它内部的修改
代码示例:
public class Person {
// 私有化成员变量
private String name;
private int age;
// 通过公共方法实现 赋值和获取
public void setName(String n) {
name = n;
}
public String getName() {
return name;
}
public void setAge(int a) {
age = a;
}
public int getAge() {
return age;
}
}
2. 面向对象三大特征(二)-继承
2.1 概念
简单来说,就是子类继承父类的属性和方法,同时子类还可以拓展自己的功能
- 父类: 被继承的类,拥有公共的属性和方法
- 子类: 继承父类的类,可以复用父类内容,也能继续拓展
- 关键字:
extends,Java 只支持单继承,即一个子类只能继承一个父类 - 作用:减少重复代码,建立类之间的关系,支持多态
用大白话来举例就是,学生和老师都是人,那么可以创建一个人的类,让学生类和老师类都继承人类,学生类和老师类都有姓名和性别,那么人类中就可以声明姓名和性别的成员变量,而学生类和老师类继承人类后,在学生类和老师类中就不再需要手动声明姓名和性别了,同时,学生可以继续拓展学号属性,老师可以继续拓展教师资格证编号等属性或方法
2.2 如何实现继承
继承的关键字是 extends,通过子类继承父类的方式来实现继承
代码格式:
[修饰符] class 子类名称 extends 父类名称 {
}
代码示例:
// 人类(父类)
public class Person() {
String name;
int age;
public void walk() {
System.out.println("人正在行走...");
}
}
// 学生类(子类)
public class Student() {
String studentId;
public void readBook() {
System.out.println("学生正在读书...");
}
}
// 测试类
public class Test {
public static void main(String[] args) {
// 创建一个学生类对象
Student s1 = new Student();
s1.name = "LiuXT";
s1.age = 25;
s1.studentId = "A00000001";
// 调用继承父类的walk方法,打印人正在行走...
s1.walk();
// 调用自身的readBook方法,打印学生正在读书...
s1.readBook();
}
}
2.3 继承的细节说明
- 子类继承的是父类中的
所有属性和方法 - 父类中
private私有化的属性或方法,实际上是被继承的,只是没有访问权限(无法直接在子类中访问),可以通过继承父类中的get/set方法来实现访问 - 子类本质上是父类基础上的拓展,可以理解为
父类 Plus - Java中支持多层继承,比如A继承B,B再继承C...
- Java中只能单继承,即一个子类只能继承一个父类,比如A继承B,则同时不可再继承其他类
- Java中所有的类
默认都继承 Object 类
2.4 继承进阶-重写
概念: 子类继承父类的方法后,支持重新定义方法的内容
- 子类重写的方法必须和父类被重写的方法具有相同的 方法名称、参数列表
- 子类重写的方法返回值类型 不能大于 父类被重写的方法的返回值类型,例如 Student < Person
- 当返回值类型是基本数据类型和 void 时,必须相同!
- 子类重写的方法的访问权限 不能小于 父类被重写的方法的访问权限
- 父类私有方法、跨包的缺省方法也不能重写
- 子类方法抛出的异常 不能大于 父类被重写方法的异常
- static 修饰的方法不属于重写范畴,因为 static 方法是属于类的,子类无法覆盖父类的方法
代码示例:
// 人类(父类)
public class Person() {
String name;
int age;
public void walk() {
System.out.println("人正在行走...");
}
}
// 学生类(子类)
public class Student() {
String studentId;
public void readBook() {
System.out.println("学生正在读书...");
}
// 此处重写了walk方法!!!!!
@Override
public void walk() {
System.out.println("学生正在行走...");
}
}
// 测试类
public class Test {
public static void main(String[] args) {
// 创建一个学生类对象
Student s1 = new Student();
s1.name = "LiuXT";
s1.age = 25;
s1.studentId = "A00000001";
// 调用继承父类的walk方法,打印学生正在行走...!!!!!
s1.walk();
// 调用自身的readBook方法,打印学生正在读书...
s1.readBook();
}
}
2.5 继承进阶-this/super
在Java中,this 关键字用于调用本类中的成员变量、方法和构造器
- 在方法(准确的说是实例方法或非static的方法)内部使用,表示调用该方法的对象
- 在构造器内部使用,表示该构造器正在初始化的对象
- 如果本类中 this 调用的内容不存在,会自动去父类中查找
this 的使用场景:
- 实例方法或构造器中使用当前对象的成员变量 this.name
- 同一个类中构造器互相调用,但不可递归调用,且必须声明在首行 this()中调用this(a,b,c)
在Java中,super 关键字用于调用父类中的成员变量、方法和构造器
- 尤其当子父类出现同名成员时,可以用 super 表明调用的是父类中的成员
- super 的追溯不仅限于直接父类
super 的使用场景:
- 子类中调用父类被重写的方法
- 子类中调用父类中同名的成员变量(实际开发中,子类和父类中禁止声明同名变量)
- 子类构造器中调用父类构造器
总结就是:起点不同,就近原则!
2.6 继承进阶-子类实例化过程
- 当创建子类的对象时,优先调用的是父类的构造器,依次递进完成子类的构造
- 当创建子类的对象时,内存中只会有一个子类的实例对象
3. 面向对象三大特征(三)-多态
3.1 概念
简单来说,多态就是 一个事物的多种形态
在 Java 中,主要通过对象的多态性来体现,即 父类的引用指向子类的对象
其特征是,编译时看左,运行时看右,具体表现为:在编译时是当做父类,此时不能调用子类独有的属性和方法,在运行时是执行子类的方法,实际上执行时执行的是子类重写后的方法,实际开发中,提高了代码的的可拓展性
因此,多态性体现的前提是:1.类的继承关系 且 2.方法的重写,多态性的体现只能应用于类的方法,成员变量不存在多态性 的体现,即使子类和父类声明了完全相同的成员变量,子类的成员变量也无法覆盖父类的成员变量
代码格式:
父类类型 变量名 = new 子类类型();
代码示例:
Person p1 = new Student();// p1是Person类型
p1.walk();// 学生在行走...
3.2 类型转换
因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。
但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点”小麻烦”。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过。
-
向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
- 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
- 此时,一定是安全的,而且也是
自动完成的
-
向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过
instanceof关键字进行判断
代码示例:
// 向上转型(自动)
Person p1 = new Student();
// 向下转型(强转)
Student s1 = (Person) p1;
3.3 instanceof 关键字
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验。如下代码格式:
//检验对象a是否是数据类型A的对象,返回值为boolean型
对象名 instanceof 数据类型
-
说明:
- 只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
- 如果对象a属于类A的子类B,a instanceof A值也为true。
- 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。
代码示例:
public class TestInstanceof {
public static void main(String[] args) {
Pet[] pets = new Pet[2];
pets[0] = new Dog();//多态引用
pets[0].setNickname("小白");
pets[1] = new Cat();//多态引用
pets[1].setNickname("雪球");
for (int i = 0; i < pets.length; i++) {
pets[i].eat();
if(pets[i] instanceof Dog){
Dog dog = (Dog) pets[i];
dog.watchHouse();
}else if(pets[i] instanceof Cat){
Cat cat = (Cat) pets[i];
cat.catchMouse();
}
}
}
}
4. Object类
4.1 概念
在Java中,所有类都默认继承父类: Object,可以理解为 Java 中定义的一个 根父类
- Object类型的变量与除Object以外的任意引用数据类型的对象都存在多态引用
- 所有对象(包括数组)都实现这个类的方法
- 如果一个类没有特别指定父类,那么默认则继承自Object类
4.2 Object类的核心方法
1. equals 方法
基本数据类型进行比较时,使用==符号,当变量值相等时,返回true引用数据类型进行比较时,使用==符号,当对象地址值相同时(指向同一个对象),返回true
Object 类中的 equals 方法默认也是使用 == 进行比较,但是,我们可以通过重写 equals 方法来实现对比对象的内容是否相同
== 和 equals 的区别
- == 对于基本数据类型就是比较值,对于引用数据类型就是比较内存地址
- equals 属于 Object 类里面的方法,如果该方法没有被重写过,默认也是 ==
- 通常情况下,重写equals方法时,会实现
比较类中的相应属性是否都相等
2. toString 方法
- 默认情况下,toString返回的是对象的地址值(格式:数据类型@hashCode值)
- 可以通过重写 toString 方法来实现返回对象的属性等信息的字符串(String类)
3. clone 方法
创建并返回当前对象的一个副本(克隆体)
- 要使用 clone 方法,类必须实现 Cloneable 接口
- 默认为浅拷贝,只复制基本类型 + 引用地址,不复制引用对象
- 深拷贝需要手动重写,将对象也复制(副本为完全克隆的全新独立对象)
4. finalize 方法
-
当对象被回收时,系统自动调用该对象的 finalize() 方法。(不是垃圾回收器调用的,是本类对象调用的)
- 永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用。
-
什么时候被回收:当某个对象没有任何引用时,JVM就认为这个对象是垃圾对象,就会在之后不确定的时间使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize()方法。
-
子类可以重写该方法,目的是在对象被清理之前执行必要的清理操作。比如,在方法内断开相关连接资源。
- 如果重写该方法,让一个新的引用变量重新引用该对象,则会重新激活对象。
-
在JDK 9中此方法已经被
标记为过时的。
5. getClass 方法
public final Class<?> getClass():获取对象的运行时类型
因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法
6. hashCode 方法
public int hashCode():返回每个对象的hash值(格式:数据类型@hashCode值)
7. native关键字
使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++等非Java语言实现的,并且被编译成了DLL,由Java去调用。
- 本地方法是有方法体的,用c语言编写。由于本地方法的方法体源码没有对我们开源,所以我们看不到方法体
- 在Java中定义一个native方法时,并不提供实现体。
为什么要用native方法?
Java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,例如:Java需要与一些底层操作系统或某些硬件交换信息时的情况。native方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。
native声明的方法,对于调用者,可以当做和其他Java方法一样使用嘛?
native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM将控制调用本地方法的所有细节。