写在前面:博主是一只经过实战开发历练后投身培训事业的“小山猪”,昵称取自动画片《狮子王》中的“彭彭”,总是以乐观、积极的心态对待周边的事物。本人的技术路线从Java全栈工程师一路奔向大数据开发、数据挖掘领域,如今终有小成,愿将昔日所获与大家交流一二,希望对学习路上的你有所助益。同时,博主也想通过此次尝试打造一个完善的技术图书馆,任何与文章技术点有关的异常、错误、注意事项均会在末尾列出,欢迎大家通过各种方式提供素材。
本文关键字:Java、面向对象、三大特征、继承
从类的概念出发,我们可以通过定义class去描述一类事物,具有相同的属性和行为。但在很多时候我们希望对类的定义能够进一步细化,这就相当于是一个大的分类下面有很多的子分类,如文具下面可以分为:写字笔、便签、文件管理等等。
如果品类更加的复杂,可以先分为:学生文具、办公文具、财会用品,然后在每个品类下面再根据具体的作用去划分。这些被划分出来的子类别都一定具有父类别的某些共同特征或用途,并且有可能存在多级的分类关系,那么如果我们使用面向对象的语言去描述出这样一种关系就可以使用继承。
下面我们将例子与面向对象中的概念进行对应:
如果我们将学生类进一步细化为:初中生、高中生、大学生。显然,细化之后的类与类之间一定是存在某些差异的,但是也一定会存在共同点。如果我们使用代码进行表示,三个类中会有很多相同的属性或方法,也会存在一些差异:
// 定义类:初中生
public class JuniorStudent{
// 相同的属性
public String name;
public int age;
public String school;
public String grade;
// 其他方法
}
// 定义类:高中生
public class SeniorStudent{
// 相同的属性
public String name;
public int age;
public String school;
public String grade;
// 不同的属性
public String subject;// 科目:文理科
// 其他方法
}
// 定义类:大学生
public class UniversityStudent{
// 相同的属性
public String name;
public int age;
public String school;
public String grade;
// 不同的属性
public String college;// 学院
public String major;// 专业
// 其他方法
}
上面只是列举了部分的属性,可以发现有很多属性是完全重合的,方法也有可能存在相同的现象。这个时候我们就们就可以将其中相同的属性和方法抽取出来,定义一个Student学生类,从而对每个类进行简化。
// 定义类:学生
public class Student{
// 提取公共的属性
public String name;
public int age;
public String school;
public String grade;
// 提取公共的方法
}
// 简化后的初中生
public class JuniorStudent extends Student{
// 其他方法
}
// 简化后的高中生
public class SeniorStudent extends Student{
// 不同的属性
public String subject;// 科目:文理科
// 其他方法
}
// 简化后的大学生
public class UniversityStudent extends Student{
// 不同的属性
public String college;// 学院
public String major;// 专业
// 其他方法
}
// 定义测试类
public class Test{
public static void main(String[] args){
JuniorStudent juniorStudent = new JuniorStudent();
juniorStudent.name = "小明";// 正常使用,来自父类
SeniorStudent seniorStudent = new SeniorStudent();
seniorStudent.name = "小李";// 正常使用,来自父类
seniorStudent.subject = "文科";// 自有属性,来自子类
UniversityStudent universityStudent = new UniversityStudent();
universityStudent.name = "小陈";// 正常使用,来自父类
universityStudent.college = "XX大学";// 自有属性,来自子类
universityStudent.major = "XX专业";// 自有属性,来自子类
}
}
从上面的例子可以看出,子父类之间可以通过extends关键字建立继承关系。子类可以直接使用父类中定义的属性和方法,也可以覆盖父类中的方法,表现出子类自己的特点。使用继承有以下几个好处:
在Java中,继承的使用存在一些限制,我们需要先明确使用规则才能更好的去设计子父类。一言以蔽之:Java不支持多继承,但支持多重继承(多级继承),从一个子类出发,只存在一条路找到最终的父类。
class A{
...
}
class B extends A{
...
}
class A{
...
}
class B extends A{
...
}
class C extends B{
...
}
class A{
...
}
class B extends A{
...
}
class C extends A{
...
}
当我们需要通过程序去描述某一个场景或实现某一个应用系统时,就需要构建很多相关的类,合理的使用继承可以使代码更加高效也更加利于维护。那么子父类的构建就可以从类本身所代表的意义出发,如果含义相似或相近,并且类与类之间没有较大的冲突,那么我们就可以把他们归为一类。另外一种情况就是原有构建的类不能满足新功能的需要,需要据此改进,那么我们就可以将原有类作为父类,扩充出他的子类,使整体的功能更加强大,同时又不会对已有的代码产生较大的影响。
可以从管理员(AdminUser)、普通用户(NormalUser)、VIP用户(VIPUser)中提取相同的特征,得到父类:用户(User),同样具有用户名,密码,昵称等信息,同样存在登录方法,只不过各自的实现会有所不同。我们也可以混合使用多种继承方式,得到如下的类关系:
对于一个简单的电商场景,产品类的设计会比较简单,只需要标识基本信息和价格即可。如果此时需要举行一个秒杀活动,要在购买页面中标识出原价、特价、活动时间、活动介绍等等信息,这就使得我们要对产品类做出升级,如果直接去修改产品类,会导致出现一些可能不会经常使用的属性和方法,因为这些属性和方法纯粹是为特价商品而设计的。比较好的做法是将原有的商品类(Product)作为父类,然后扩充出它的子类:特价商品类(SpecialProduct),在特价商品类中存放新出现的信息。
理解了相关的概念后,我们回到Java的语法中来,子父类间通过extends来建立继承关系,结构如下:
// 定义父类
public class Father{
...
}
// 定义子类,继承父类
public class Son extends Father{
...
}
在之前的文章中,已经介绍了权限修饰符的用法,不清楚的同学可以进传送门:Java面向对象编程三大特征 - 封装。当两个类建立了继承关系时,虽然父类当中的所有内容均会被子类继承,但是由于存在权限修饰符,无访问权限的属性或方法会被隐藏,无法被调用和访问(实例化子类对象时,父类对象也会一同被实例化,详细过程会在后面的文章中单独说明)。
在子类中可以直接调用父类中被public和protected声明的属性和方法,如果是在测试类中,在进行属性调用时依然会受到权限修饰符的限制,看下面一个例子:
src
└──edu
└──sandtower
└──bean
│ Father.java
│ Son.java
└──test
│ Test.java
以上为实体类与测试类所在的目录结构
package edu.sandtower.bean;
public class Father{
// 父类中的私有属性
private double ownMoney = 2000;// 私房钱
// 父类中的受保护属性
protected double money = 5000;
// 父类中的公开属性
public String name = "老李";
}
package edu.sandtower.bean;
public class Son extends Father{
// 子类中的独有属性
...
// 测试方法
public void test(){
Son son = new Son();
System.out.println(son.ownMoney);// 编译失败,无法访问私有属性,查看私房钱
System.out.println(son.money);// 编译通过,在子类中可以访问protected属性
System.out.println(son.name);// 编译通过,可以访问public属性
}
}
package edu.sandtower.test;
import edu.sandtower.bean.Son;
public class Test{
public static void main(String[] args){
// 在test包中的Test类中创建Son实例
Son son = new Son();
son.name = "小李";// 编译通过,可以访问自父类继承的公开属性
// 编译失败,在测试类中无法访问protected属性,因为Test类与Father类并无子父类关系
son.money -= 500.0;
// 对于Son的其他属性和Father的使用可以自行进行测试,不再赘述
}
}
从上面的例子可以看到,权限修饰符所起的作用还是很大的。测试类对于子父类来说是一个处在不同包中的完全无关的类,在调用时会被权限修饰符所限制,所以这里也再度明确一下:权限修饰符是根据类的所在路径与类之间的结构关系进行限定的,不是说在任意一个地方使用子类实例都能调用出父类中的属性和方法。
明确了权限修饰符的作用规则后就带来了一个问题,既然在其他类中不能够访问某些属性,那应该如何赋值和使用呢?这时就可以使用封装的办法,同时结合this和super的使用来解决。
在使用子类实例时,如果我们想要使用某些父类的属性或方法,可以借助构造器和封装方法。将代码修改后,得到如下结果:
package edu.sandtower.bean;
public class Father{
// 父类中的私有属性
private double ownMoney = 2000;// 私房钱
// 父类中的受保护属性
protected double money = 5000;
// 父类中的公开属性
public String name = "老李";
}
package edu.sandtower.bean;
public class Son extends Father{
// 子类中的独有属性
...
// 使用构造器为属性赋值
public Son(String name,double money){
super.name = name;
super.money = money;
}
// 使用封装方法操作父类中的属性
public void setMoney(double money){
super.money = money;
}
public double getMoney(){
return super.money;
}
}
package edu.sandtower.test;
import edu.sandtower.bean.Son;
public class Test{
public static void main(String[] args){
// 在test包中的Test类中创建Son实例
Son son = new Son("小李",3000);// 成功为父类继承而来的属性赋值
// 以下代码编译通过
double money = son.getMoney();
System.out.println(money);
son.setMoney(money - 500);
}
}
final修饰符可以用来修饰属性和方法,也可以用来修饰类。
当修饰属性时,如果是基本数据类型,则可看做是定义了一个常量,值一旦被指定则不可变。如果是引用类型,则引用无法发生变化,即:可以修改数组或实例中的属性值,但是引用的指向不能再发生变化,无法再指向其他的实例和数组。
由final修饰的方法被子类继承后不能被重写,有关于继承中子父类方法的关系将在论述多态的文章中详细讨论。
由final修饰的class不能被继承,如果我们把继承关系想象成一棵大树,父类为根,子类为枝的话,那么final class就一定只能做树叶了,因为确认不会有它的子类存在了。
在刚接触面向对象时,我们可能就听说过一个类,那就是:Object,好像它无所不能装,大饼夹一切的存在。我们所有定义的class都会隐式的继承Object,即:如果我们的类没有使用extends关键字显示的指定父类,那么会自动认为Object是父类,这一过程是在JVM运行时完成的,所以我们不能通过反编译来进行验证。
Object中提供了特别通用的方法,如:toString,hashCode,equals等等。那么为什么使用Object能装下一切呢?首先就是因为Object类一定是最终父类的存在,换句话说就是树根本根!因为如果一个类显示的指定了另外一个类作为父类,那么他的父类或者父类的父类,一定会在某一级隐式的继承Object。
如果想进一步了解为什么任意类型的对象实例都能使用Object类型的引用接收可以查看另外一篇文章:Java面向对象编程三大特征 - 多态。