文章目录
一、 继承
1. 为什么要继承
假设一种场景,你是一个富二代,你继承了家里的遗产,这样你就获得了父辈的遗产
而你作为下一代,就继承了上一代的财产
又比如你是一个大学生,首先最基础的你是人,其次你是学生,再其次你才是大学生
那当我是学生这一层身份的时,我继承了人的属性,那我还需要再定义一遍人的属性吗,不用
那进一步当我是大学生这一层身份的时候,我继承了学生的属性,我还需要把学生和人的属性再定义一遍吗,不用
-
这就是继承的意义:避免冗余代码,把共同属性组织起来,实现代码复用
-
父类(基类,超类):继承的上一代,子类(派生类):继承的下一代
对于子类间共有的属性,我们可以抽象出来,组成一个共同的父类,而子类则可以在父类原有的基础上进行扩展延伸
就比如学生类,我是在人这个父类进行拓展延伸的,定义了一些学生才有的属性,比如年级,学校等等
- 既然是继承,那是不是可以调用父类的方法和属性呢,答案是可以的
- 而你继承后不能完全跟父类一样吧,因此需要添加独属于子类特有的属性和方法
2. 如何继承
格式:定义类的时候,通过
extends (你要继承的类名)
关键字
举个例子,我定义了一个“人”类,同时定义了一个“学生”类,我让“学生”类继承“人”类
然后在学生类中输出学生类的成员变量和父类中的成员变量//“人”类文件 public class Person { public int age = 18; public double high = 180.5; public char sex = '男'; } //“学生”类文件 public class Student extends Person{ public String colleague = "清华大学";//学校... public String subject = "Java编程语言";//主修学科... public int stage = 1;//大一、大二... @Override public String toString() { return "Student{" + "colleague='" + colleague + '\'' + ", subject='" + subject + '\'' + ", stage=" + stage + ", sex=" + sex + '}'; } } //测试类文件 public class Try { public static void main(String[] args) { Student stu = new Student(); System.out.println(stu); } }
我们可以看到当我们去输出“学生”类的时候,因为继承了“人”类
我在输出类成员方法的时候是可以输出“人”类中的成员变量(性别是男)
- 但是倘若成员变量或者方法命名不规范,会出现一些问题
3. 情况一:子父类成员变量重名
比如我在刚刚代码基础上在“学生”这个类中添加与“人”类相同的成员变量
char sex = '女'
当我输出类的成员变量的时候,我输出的是“人”类中的性别还是“学生”类的性别
很显然是子类(“学生”类的变量),因此我们得出一条结论:当父子类成员变量重名,优先访问子类
但是你就是倔,就想要访问父类的怎么办,Java提供了super关键字,顾名思义就是超级访问权限
格式:super.父类的成员变量
举个例子,我们将“学生”类中输出成员变量的部分稍微做了下改变,对于性别加了个super关键字@Override public String toString() { return "Student{" + "colleague='" + colleague + '\'' + ", subject='" + subject + '\'' + ", stage=" + stage + ", sex=" + super.sex + '}'; }
我们再看打印结果,这就符合我们的预期了
还有一种情况,就是如果子类中没有你想要找的成员变量,那么就会在父类中寻找,如果父类没有就会报错
就拿我们之前的代码举例,我们在测试类中尝试打印以下成员变量public class Try { public static void main(String[] args) { Student stu = new Student(); System.out.println(stu.colleague); System.out.println(stu.high); System.out.println(stu.money); } }
发现我们找不到money
这个成员变量,我们把其注释掉再找执行一次代码
发现不管是子类还是父类中的成员变量都访问到了,因此寻找的顺序是:
子类优先 --> 子类不存在 --> 父类其次 —> 父类不存在 --> 报错
如果你使用this关键字访问当前对象的成员变量,默认访问的还是子类中的
以下是图解
this
关键字和super
关键字
4. 情况二:子父类成员方法重名
- 假如你虽然重名,但是参数列表不同,构成了重载,在实例化对象的时候会自动根据传的参数来匹配子类或者是父类的成员方法
//“人”类中定义了一个方法
public void eat (){
System.out.println("人正在吃饭");
}
//“学生”类中定义了一个方法
public void eat(String colleague){
System.out.println(colleague+"的大学生正在吃饭");
}
//测试类中定义了一个方法
public static void main(String[] args) {
Student stu = new Student();
stu.eat("北京大学");
stu.eat();
}
我们可以看到虽然都是同一个“学生”类,但是我访问成员方法的时候的参数列表不同,访问的结果也不一样
- 那假如子父类成员方法重名且参数列表相同,则优先访问子类中的成员方法
//在“人”类中定义了一个成员方法
public void sleep(){
System.out.println("人在睡觉");
}
//在“学生”类中定义了一个相同的成员方法
public void sleep(){
System.out.println("大学生
}
//在测试类中再调用
stu.sleep();
我们可以看到优先访问的是子类,若果想访问父类,则利用super关键字
:stu.super.sleep
注意:只能在非静态方法中使用,切记切记!!!
public void sleep(){
super.sleep();
System.out.println("大学生在睡觉");
}
5. 子父类构造方法问题
若在父类中定义了构造方法,子类继承了父类,在子类完成构造前,先要帮父类进行构造,不然报错
//“人”类中定义构造方法
public class Person {
public int age;
public double high;
public char sex;
Person(int age,double high,char sex){
this.age = age;
this.high = high;
this.sex = sex;
}
}
//“学生”类中定义构造方法
public class Student extends Person{
public String colleague;//学校...
public String subject;//主修学科...
public int stage;//大一、大二...
public char sex;
Student(String colleague,String subject,int stage,char sex){
this.colleague = colleague;
this.subject = subject;
this.stage = stage;
this.sex = sex;
}
}
//测试方法中实例化对象
Student stu = new Student("北京大学","Java数据结构",2,'男');
直接报错,提示没有给父类应给的类型,因此我们要在子类构造方法中初始化父类中的成员变量
//“人”类构造方法
public Person(int age,double high,char sex){
this.age = age;
this.high = high;
this.sex = sex;
}
//“学生”类构造方法,注意super关键字传的内容
public Student(String colleague,String subject,int stage,char sex){
super(18,180.5,'男');
this.colleague = colleague;
this.subject = subject;
this.stage = stage;
this.sex = sex;
}
//测试类方法中实例化对象
Student stu = new Student("北京大学","Java数据结构",2,'男');
- 若父类和子类都没写构造方法,就像我们只看演示一样,都没写构造方法,为什么不会报错呢
- 因为Java会默认添加无参构造方法,可以自己都写上调试下,这里就不过多演示了
6. 继承中代码块调用顺序
我们先给出示例代码,我实例化类两个子类的对象
//Person类
public class Person {
public int age;
public double high;
public char sex;
static{
System.out.println("Person类的静态代码块被调用");
}
{
System.out.println("Person类的构造代码块被调用");
}
public Person(int age,double high,char sex){
this.age = age;
this.high = high;
this.sex = sex;
System.out.println("Person类构造方法被调用");
}
}
//Student类
public class Student extends Person{
public String colleague;//学校...
public String subject;//主修学科...
public int stage;//大一、大二...
public char sex;
static{
System.out.println("Student类的静态代码块被调用");
}
{
System.out.println("Student类的构造代码块被调用");
}
public Student(String colleague,String subject,int stage,char sex){
super(18,180.5,'男');
this.colleague = colleague;
this.subject = subject;
this.stage = stage;
this.sex = sex;
System.out.println("Student类构造方法被调用");
}
}
//测试类,注意我实例化类两个对象
public class Try {
public static void main(String[] args) {
Student stu = new Student("北京大学","Java数据结构",2,'男');
System.out.println("============================");
Student stus = new Student("浙江大学","Java微服务",3,'男');
}
}
- 通过观察我们看到静态代码块最先被调用,并且是先父类再子类
- 其次是父类的构造代码块和构造方法,其次才是子类的构造代码块和构造方法
- 而且静态代码块才执行一次(这个上次讲过,不过多解释)
- 为什么,我认为:肯定静态代码块优先,重点在于构造代码块,既然子类是继承父类的
- 那实例化子类对象的时候,在子类中先进入来自父类的区域,之后在进入子类都有的区域
7. protected关键字
解释:你肯定发现在定义成员变量的时候我都是加的public关键字,指的是哪里都可以访问,不管是不同类还是不同包
- 但是,这样是不是权限太大了,不安全,如果是private,只能在当前类中访问,权限又太小了
- 因此,我们定义了protected关键字,来表明这不大不小的权限
- protected规则:同个包甭管子类还是非子类都可访问,不同包继承得来的子类可以访问,非子类不可访问
- 给出示例代码
//“Person”类
protected int hands = 2;
//“Student”类
protected int money = 200;
//测试类
System.out.println(stu.money);
System.out.println(stu.hands);
-
但是如果是不同的包呢?由于代码过多,我采用截图的方法给大家演示
-
这里插一嘴:除了private,protected,public外,还有个default(默认),它只能在同个包中访问
7. 继承方式
- 单继承(继承一个)
- 多层继承(传宗接代)
- 不同类继承自同一个类(几个孩子有个共同父亲)
它们各有的性质我相信你都应该大致了解类,在Java中没有菱形继承(一个子类继承两个父类)
8. final关键字
你可以把它理解成C语言的const,如果加在变量
如果加载类前面,表示不可被继承
而且Srting类也是一个final类
9. 继承和组合
- 继承:B继承与A,那B就是A,换句话来说继承后拥有了被继承方全部内容
- 组合:把继承拆开,把各个部分拆成一个一个模块,到时候再组合起来
- 为什么推崇组合?
- 我们把每一个成员方法都写成一个类,再在其他的一个大的类中将这些小的类组合起来,相比于继承每个类之间都联系紧密,组合让结构更加松散,这样不管哪个类修改,都不会影响到其他类,更灵活
- 假设一个场景,你定义了个动物的类,类中有个
run()
方法,你派生出三个类:狗、猫、鱼,此时狗和猫继承了动物这个类,拥有run()
这个方法,但是你的鱼也有了run()
这个方法,合理吗?不合理- 如果采用组合。我们就把各个方法定义成一个个小的类,我们在派生子类的时候根据子类的情况把各个小的类适当选择然后组合,这样就可以避免比如上面的鱼具有
run()
这个方法的尴尬- 组合最大的好处:避免了牵一发而动全身的痛苦
二、多态
1. 多态代码示例:
- 我们分别创建四个类,现定义一个基类
Person
类,里面有一个成员方法sleep()
,内容是姓名+人在睡觉- 再定义两个其派生类
Teacher
类和Student
类,分别再在里面定义一个sleep()
成员方法。内容更加精确了,一个是哪个部门老师在睡觉,一个是哪个班的学生在睡觉- 我们再定义一个测试类用于调用类中成员方法,然后我们通过创建
父类引用去引用子类对象
这种形式来创建多态//Person类 public class Person { String name; public Person(String name) { this.name = name; } public void sleep() { System.out.println(name+"人正在睡觉"); } } //Student类 public class Student extends Person{ String classes; int age; public Student(String name, String classes, int age) { super(name); this.classes = classes; this.age = age; } public void sleep() { System.out.println(name+classes+"学生在睡觉"); } } //Teacher类 public class Teacher extends Person{ String department; int age; public Teacher(String name,String department, int age) { super(name); this.department = department; this.age = age; } public void sleep() { System.out.println(name+department+"老师在睡觉"); } } //Test测试类 public class Test { public static void main(String[] args) { Person pr1 = new Student("张三","计科2班",19); Person pr2 = new Teacher("李四","教务处",35); pr1.sleep(); pr2.sleep(); } }
Tips:如何区分重写和重载
要好好区分哦 重写 重载 返回值 相同 无规定 方法名 相同 相同 参数列表 相同且顺序一致 不同
2. 向上转型
- 如果你观察我刚刚提供的代码,你会发现有一个这种写法
Person pr = new Student("张三","计科2班",19);
和Person pr2 = new Teacher("李四","教务处",35);
- 诶,我之前不是说过类实例化对象要指向自己呀,怎么你前面能引用一个父类的对象呢?
- 这就是向上转型,即父类的引用去引用子类的对象,这样便可以调用子类中重写父类的同名的方法
- 这种调用方式称作为运行时绑定(动态绑定),即在代码执行时才知道调用哪个类中方法
格式:向上转型+子类重写父类方法+通过父类引用调用子类中重写父类的方法
- 而我们常说的重载方法即编译时绑定(静态绑定),即在代码编译的时候就知道要调用哪个类的方法
3. 向上转型的三种方式
- 除了刚刚说的那种一行代码直接赋值
Person pr1 = new Student("张三","计科2班",19);
以外,还有两种方法 - 通过参数去传递
- 在Test类中,我们写一个静态方法去调用子类中的
sleep()
方法,然后参数写父类的引用Person person
,其他类中内容保持不变
public class Test {
public static void func(Person person){
person.sleep();
}
public static void main(String[] args) {
Student pr1 = new Student("张三","计科2班",19);
Teacher pr2 = new Teacher("李四","教务处",35);
func(pr1);
func(pr2);
}
}
可以看到结果依然是一样的
在func方法中,参数部分的person并不关心会指向哪个子类,它只修要做好父类引用这个指责就好,到底要调用哪个子类中
sleep()
方法就看子类调用的是哪个类中对象就好了
总结:虽然func方法中是同个父类引用,但是这个引用所引用的对象不一样的时候(引用子类),调用的子类方法也就不一样了
3. 通过返回值去传递
- 在Test类中,我们可以通过输入一个值让类引用作为返回值去调用
sleep()
方法,然后再在main方法中通过Person per
引用去引用子类对象
public static Person funcs(int num){
if(num == 10){
return new Teacher("李四","教务处",35);
}else{
return new Student("张三", "计科2班", 19);
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int input = sc.nextInt();
Person person = funcs(input);
person.sleep();
}
结果就如图所示了,因此可以通过返回值不同达到向上转型的效果
4. 重写的特殊情况
- 我们可以把父类和子类中重写的方法的返回值类型改为类本身,然后我们目前先暂时不返回值
//Person类
public Person sleep()
{
System.out.println(name+"人正在睡觉");
return null;
}
//Student类
public Student sleep()
{
System.out.println(name+classes+"学生在睡觉");
return null;
}
//Teacher类
public Teacher sleep()
{
System.out.println(name+department+"老师在睡觉");
return null;
}
实现的效果和之前的效果是一样的
- 协变类型⬇️
- 这里多插一嘴,以上三个类中的的返回类型无论是Person、Student还是Teacher之间
- 必须互相是父子类关系,当然返回类型也不一定要写与当前类相同名字
- 可以写其他类,但是其他类之间也需要互相是父子类关系才行,不然编译错误
public A sleep()
{
System.out.println(name+"人正在睡觉");
return null;
}
//Student类
public B sleep()
{
System.out.println(name+classes+"学生在睡觉");
return null;
}
//Teacher类
public C sleep()
{
System.out.println(name+department+"老师在睡觉");
return null;
}
这里A类是B类和C类的共同父类,它们之间有父子类关系,否则会编译失败!!
注意事项:
- 访问修饰限定符要注意在子类中被重写的方法的权限一定要 > = >= >=父类的权限
- 重写的方法上,有一个
@override
,用来检查是否重写@Override public Student sleep() { System.out.println(name+classes+"学生在睡觉"); return null; }
一旦发现没有重写方法,会直接报错
- 静态方法、构造方法、被final修饰的方法(变成常类了)不可被重写,一些分别是静态、构造、final修饰报错
- 如果类已经投入使用,我们想拓展子类功能,无需修改当前投入使用的子类,我们直接重写父类的方法就好了
- 举个例子:假如以前十元钱只能拿来买苹果,但是现在还可以买梨子,难道我要把父类中的买苹果这个方法给修改吗
- 不需要,我直接在子类中重写就好了,这样既可以调用父类实现买苹果,又可以调用子类来既买苹果又买梨子
5. 向下转型
我们前文提到向上转型是子类调用父类中被重写的方法,那假设父类中没有A方法
但是子类中有,此时我们就无法通过向上转型去调用子类中特有的方法A了
- 上述情况就需要用到向下转型,即通过父类调用子类的特有方法
- 但是为什么我们不推荐向下转型,因为这会导致父类指向子类特有的方法时,会导致另一个子类也拥有当前子类方法,比如如下例子:
//在Test测试类中我们通过强转形式调用子类特有的方法
public static void main(String[] args) {
Person person = new Student("张三","计科2班",19);
Student student = (Student) person;
student.homework();
}
//我们分别在Student类和Teacher类中新增其特有方法即写作业和教书
public Student homework()
{
System.out.println(name+classes+"在写作业");
return null;
}
public Teacher teach()
{
System.out.println(name+department+"在教书");
return null;
}
我们看到虽然是我们想要的结果,但是隐形的此时Teacher类中已经具备类Student类中的做作业方法
6. 多态优点的示例代码
- 我们创建一个新的包,再创建一个Eat父类,派生出Vegetable子类和Meat子类,再添加个测试类
//Eat类
public class Eat {
void eat()
{
System.out.println("吃饭");
}
}
//Vegetable类
public class Vegetable extends Eat {
void eat()
{
System.out.println("吃蔬菜");
}
}
//Meat类
public class Meat extends Eat{
void eat()
{
System.out.println("吃肉肉");
}
}
//Teat测试类
public class Test {
public static void main(String[] args) {
Eat eat = new Meat();
eat.eat();
Eat eats = new Vegetable();
eats.eat();
}
}
- 若此时我们还想添加一个海鲜子类,我们无需再父类中再改
eat()
方法,只需要再定义一个子类去重写父类中的eat
方法即可,灰常方便
//添加一个Fish类
public class Fish extends Eat{
void eat()
{
System.out.println("吃海鲜");
}
}
//在Test测试类中直接调用重写的方法即可
public class Test {
public static void main(String[] args) {
Eat eat = new Meat();
eat.eat();
Eat eats = new Vegetable();
eats.eat();
Eat eatss = new Fish();
eatss.eat();
}
}
- 若子类未从写父类中方法,则调用父类的
- 通过这个例子我们不难看出,多态的拓展性很强,降低了代码复杂度,即圈复杂度
- 即描述代码的分支循环语句,理解起来复杂,一般不能超过10层,以下有反例代码
public static void main(String[] args) {
//我们想实现的是遍历字符数组,如果是不同的情况我们就打印不同结果
Vegetable vegetable = new Vegetable();
Meat meat = new Meat();
String[] eats = {"Ve", "Me", "Ve", "Me"};
for (int i = 0; i < eats.length; i++) {
String eat = eats[i];
if (eat.equals("Ve")) {
vegetable.eat();
} else {
meat.eat();
}
}
}
- 虽然我们看到的结果如我所愿,但是如果字符数组的内容增多呢
- 比如像之前一样添加个Fish类或者是Cow类呢,那就要写很多判断语句,代码复杂了,可读性大大降低
- 如果有使用多态,代码就可以大大简化
Vegetable vegetable = new Vegetable();
Meat meat = new Meat();
Eat [] eats = {vegetable,meat,vegetable,meat};
Eat [] eatss = {new Vegetable(),new Meat(),new Vegetable(),new Meat()};//前提Eat是父类
- 最后两行代码上下有啥差别吗,有的,上面的代码是使用匿名对象的,它适用于余只使用对象一次的情况
- 你看虽然eats数组中有两个“Ve”对象,但是实质上只实例化类一个,后面一个说白了就是引用前一个对象
- 下面一行代码中,明显new了两个同类对象,就有两个“Ve”对象
- 接下来我们再用强遍历,通过父类引用引用子类对象调用eat方法
for(Eat eat:eats){
eat.eat();
}
System.out.println("=======================");
for(Eat eat:eatss){
eat.eat();
}
也能达到之前效果,代码就大大简化了,想要添加内容直接添加就好了,无需再写复杂循环去判断
总结:上述代码所用知识点向上转型+重写+动态绑定+多态,神奇吧嘿嘿嘿!!!
三、重新认识toString
- 提醒我们所有的类实际上继承了Object类
- 我们之前提到过,要想打印类中的成员变量,如果直接打印,会打印其默认的toString,是一串编码
//EatPlus类
public class EatPlus {
public String foodType;
public String biolFire;
public EatPlus(String foodType, String biolFire) {
this.foodType = foodType;
this.biolFire = biolFire;
}
@Override
public String toString() {
return "EatPlus{" +
"foodType='" + foodType + '\'' +
", biolFire='" + biolFire + '\'' +
'}';
}
}
//Test测试类
public class TestPlus {
public static void main(String[] args) {
EatPlus eat1 = new EatPlus("鱼","中等火");
System.out.println(eat1);
}
}
- 我们在EatPlus类中重写类toString方法,我们点开看看
- 我们可以看到原本要调用Object中的toString方法,但是你重写类toString方法
- 于是就调用了你子类EatPlus(相对于Object类而言)中的toString方法
- 我们再看看println方法
- 再点开valueOf方法
- 可以看到实际上调用的就是Object类中的toString方法,但是Object作为父类
- 本应调用自己的toString方法,但是在EatPlus类中被重写了(因为EatPlus默认继承Object类)
- 因此在刚刚那张图中通过父类引用调用了子类重写的toString方法,这就发生了动态绑定
四、避免在构造方法中调用重写的方法
我们创建三个类,一个父类One,派生出一个子类Two,再添加一个Test测试类
Tips:以下是反例,千万不要这么写!
//one类
public class One {
public One(){
func();
}
public void func(){
System.out.println("One中的func方法");
}
}
//Two类
public class Two extends One{
private int n = 1;
@Override
public void func(){
System.out.println("Two中的func方法");
}
}
//Test测试类
public class Test {
public static void main(String[] args) {
Two two = new Two();
}
}
我们可以看到调用的是子类Two中的func方法且n的值是0,那就奇怪了不是
原因分析:
- 因继承关系,且父类中存在默认构造函数,那在子类中也存在默认构造函数,且默认构造函数中存在super关键字去构造父类
public D(){ super(); }
- 但是又因为子类中重写了父类的func方法,所以默认调用子类重写的方法,发生动态绑定
- 但是对于变量n,因为Two子类中此时你的构造方法还没执行完,在调用func方法时b并未初始化完成,因此默认值就是0
总结:在构造方法内部免使用类实例化的方法,除非方法被final和private修饰
建议:以后不要在构造方法中调用方法,以免触发动态绑定且发生极难发现的隐藏性极高的错误,因为这样会导致半初始化状态(跟刚刚演示的代码一样)