🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
目录
- 1. 继承与多态
- 1.1 继承
- 1.1.1 为什么需要继承
- 1.1.2 继承的概念
- 1.1.3 继承的语法格式
- 1.1.4 父类成员访问
- 1.1.4.1子类中访问父类的成员变量
- 1.1.4.2 子类中访问父类的成员方法
- 1.1.5 super关键字
- 1.1.6 子类构造方法
- 1.1.7 super和this
- 1.1.8 继承体系下的代码块
- 1.1.9 再谈访问限定符
- 1.1.10 继承方式
- 1.1.11 final关键字
- 1.1.12 继承与组合
- 1.2 多态
- 1.2.1 多态的概念
- 1.2.2 多态实现的条件
- 1.2.3 重写
- 1.2.4 向上转型和向下转型
- 1.2.5 避免在构造方法中使用重写的方法
1. 继承与多态
1.1 继承
1.1.1 为什么需要继承
Java中使用类对现实中的事物进行描述的时候,由于世间事物错综复杂,事物之间难免会存在一些特定的关联,这就是程序设计时候所需要考虑的问题。
比如:猫和狗都是动物
我们使用Java语言就会有如下描述
public class Dog{string name;int age;float weight;public void eat(){System.out.println(name + "正在吃饭");}
public void sleep(){System.out.println(name + "正在睡觉");}
void Bark(){System.out.println(name + "汪汪汪~~~");}
}
public class Cat{string name;int age;float weight;public void eat(){System.out.println(name + "正在吃饭");}public void sleep(){System.out.println(name + "正在睡觉");}void mew(){System.out.println(name + "喵喵喵~~~");}
}
通过上述代码发现,猫类和狗类中有大量重复的成员,它们都有自己的名字,年龄,体重,它们都会吃,会睡觉,只有叫声是不一样的。那么我们能否对这写共性进行抽取呢?面向对象的程序设计思想中提出了继承的概念,专门用来进行共性抽取,以实现代码的复用。
1.1.2 继承的概念
继承的机制:这是面向对象程序设计中使代码可以复用的最重要的手段,他允许程序员在保持一个类原有特征的同时进行扩展,增加功能,这样产生的类,称为派生类。很好地体现了面向对象程序设计的清晰的层次结构。总之,继承所解决的问题就是:共性的抽取,实现代码的复用。
如上图所示,Dog和Cat类都继承了Animal类,其中Animal类称为父类,基类,或者超类,Dog和Cat类称为子类或者派生类,继承之后,子类就可以复用父类中的成员,子类在实现时只关心自己的成员即可。
1.1.3 继承的语法格式
在Java中入如果要表示继承关系,需要使用extends关键字,具体格式如下:
修饰符 class 子类 extends 父类{
//
}
现在,我们使用继承语法对猫类和狗类进行重新设计:
public class Animal{String name;int age;public void eat(){System.out.println(name + "正在吃饭");}public void sleep(){System.out.println(name + "正在睡觉");}
}
public class Dog extends Animal{void bark(){System.out.println(name + "汪汪汪~~~");}
}
public class Cat extends Animal{void mew(){System.out.println(name + "喵喵喵~~~");}
}
注意事项:
- 子类会将父类的成员变量和成员方法继承到子类中
- 子类继承父类之后,必须要新添加自己特有的成员,否则就没必要继承了
- static修饰的成员不可以继承,因为他们不属于对象,他们属于类(这时候可以返回上一篇文章看看删掉的那一行了)
1.1.4 父类成员访问
在继承体系中,既然子类把父类的方法和字段都继承下来了,那子类怎样去访问父类中的成员呢?
1.1.4.1子类中访问父类的成员变量
- 子类和父类不存在同名的情况
public class Base {int a;int b;
}
public class Derived extends Base{int c;public void method(){a = 10; // 访问从父类中继承下来的ab = 20; // 访问从父类中继承下来的bc = 30; // 访问子类自己的c}
}
不同名的情况下,可直接访问父类成员
- 子类和父类同名的情况
public class Base {int a;int b;int c;
}
public class Derived extends Base{int a; // 与父类中成员a同名,且类型相同char b; // 与父类中成员b同名,但类型不同public void method(){a = 100; // 访问父类继承的a,还是子类自己新增的a?自己的b = 101; // 访问父类继承的b,还是子类自己新增的b? 自己的c = 102; // 子类没有c,访问的肯定是从父类继承下来的c}
}
子类中的成员访问遵循以下原则:
- 如果访问的成员变量中,子类里有,优先访问自己的成员变量
- 如果访问的成员变量中,子类中没有,但父类中有,则访问父类继承下来的
- 如果访问的成员变量子类和父类重名,优先访问自己的
成员变量的访问遵循就近访问原则,自己有优先用自己的,如果没有再从父类中去找
1.1.4.2 子类中访问父类的成员方法
- 成员方法不同名
public class Base {public void methodA(){System.out.println("Base中的methodA()");}
}
public class Derived extends Base{public void methodB(){System.out.println("Derived中的methodB()方法");}public void methodC(){methodB(); // 访问子类自己的methodB()methodA(); // 访问父类继承的methodA()}
}
- 成员方法同名
public class Base {public void methodA(){System.out.println("Base中的methodA()");}public void methodB(){System.out.println("Base中的methodB()");}
}
public class Derived extends Base{public void methodA(int a) {System.out.println("Derived中的method(int)方法");//重载}public void methodB(){System.out.println("Derived中的methodB()方法");}public void methodC(){methodA(); // 没有传参,访问父类中的methodA()methodA(20); // 传递int参数,访问子类中的methodA(int)methodB(); // 直接访问,则永远访问到的都是子类中的methodB(),基类的无法访问到}
}
总结
- 通过子类对象访问父类与子类不同名的方法时,优先在子类中找,否则在父类中找
- 通过子类对象访问父类与子类同名的方法时,若两种方法构成重载,根据调用参数的不同调用相应的方法,
至于参数列表相同的情况,我们在后面解释到方法的重写的时候再详细解释
那么问题来了,如果在子类中,我偏要访问父类的成员呢?这时我们就需要引入下一个关键字。
1.1.5 super关键字
既然想要在子类中访问父类的成员,直接访问是无法做到的,这时,Java就为我们提供了super关键字,这个关键字的作用是:在子类方法中访问父类成员。
public class Base {int a;int b;public void methodA(){System.out.println("Base中的methodA()");}public void methodB(){System.out.println("Base中的methodB()");}
}
public class Derived extends Base{int a; // 与父类中成员变量同名且类型相同char b; // 与父类中成员变量同名但类型不同// 与父类中methodA()构成重载public void methodA(int a) {System.out.println("Derived中的method()方法");}// 与基类中methodB()构成重写(即原型一致,重写后序详细介绍)public void methodB(){System.out.println("Derived中的methodB()方法");}public void methodC(){// 对于同名的成员变量,直接访问时,访问的都是子类的a = 100; // 等价于: this.a = 100;b = 101; // 等价于: this.b = 101;// 注意:this是当前对象的引用// 访问父类的成员变量时,需要借助super关键字// super是获取到子类对象中从基类继承下来的部分super.a = 200;super.b = 201;// 父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法methodA(); // 没有传参,访问父类中的methodA()methodA(20); // 传递int参数,访问子类中的methodA(int)// 如果在子类中要访问重写的基类方法,则需要借助super关键字methodB(); // 直接访问,则永远访问到的都是子类中的methodA(),基类的无法访问到super.methodB(); // 访问基类的methodB()}}
注意事项
- 只能在非静态方法中使用
- 在子类方法中,访问父类的成员变量和方法会用到
- super还有其他用法,后续介绍
1.1.6 子类构造方法
父子父子,先有父再有子,即:子类对象构造时,需要先调用父类的构造方法,然后再执行子类的构造方法
public class Base {public Base(){System.out.println("Base()");}
}
public class Derived extends Base{public Derived(){// super(); // 注意子类构造方法中默认会调用基类的无参构造方法:super(),// 用户没有写时,编译器会自动添加,而且super()必须是子类构造方法中第一条语句,// 并且只能出现一次System.out.println("Derived()");}
}
public class Test {public static void main(String[] args) {Derived d = new Derived();}
}
在子类的构造方法中,并没有写任何关于父类的构造代码,但是在构造子类对象时,先执行了父类的构造方法,然后执行子类的构造方法,因为:子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分 。先有父再有子,所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整 。
注意:
- 如果父类,显式定义无参或者默认构造方法时,在子类构造方法的第一行有默认的super()调用,即调用父类的构造方法
- 如果父类的构造方法是带有参数的,此时需要在子类构造方法中主动调用父类构造方法
- 在子类构造方法中,super调用父类构造方法的语句必须放在子类构造方法的第一行
- super只能在子类构造方法中出现一次,且不可以与this同时出现
1.1.7 super和this
【相同点】
- 都是Java语言中的关键字
- 只能在非静态方法中使用,用来访问非静态成员方法和字段
- 在构造方法中调用时,必须是构造方法中的第一条语句,而且不能同时存在
【不同点】
- this是指当前对象的引用,super相当于是子类对象从父类继承下来部分成员的引用
- 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
- 在构造方法中,==this调用本类的构造方法,super调用父类的构造方法,==但是两种方法不可以同时出现
- 构造方法中一定会存在super调用,用户不写编译器会默认增加,但是this不写则没有
1.1.8 继承体系下的代码块
还记得我们之前讲的代码块吗,我们简单回顾,主要有两种代码块:实例代码块和静态代码块,在没有继承关系下的执行顺序是:静态代码块>实例代码块
class Person {public String name;public int age;public Person(String name, int age) {this.name = name;this.age = age;System.out.println("构造方法执行");}{System.out.println("实例代码块执行");}static {System.out.println("静态代码块执行");}
}
public class TestDemo {public static void main(String[] args) {Person person1 = new Person("zhangsan",10);System.out.println("============================");Person person2 = new Person("lisi",20);}
}
- 静态代码块先执行,并且只执行一次,在类加载阶段执行
- 有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后执行构造方法
(详细内容请参考前面的文章)
要是加入了继承关系呢?
class Person {public String name;public int age;public Person(String name, int age) {this.name = name;this.age = age;System.out.println("Person:构造方法执行");}{System.out.println("Person:实例代码块执行");}static {System.out.println("Person:静态代码块执行");}
}
class Student extends Person{public Student(String name,int age) {super(name,age);System.out.println("Student:构造方法执行");}{System.out.println("Student:实例代码块执行");}static {System.out.println("Student:静态代码块执行");}
}
public class TestDemo4 {public static void main(String[] args) {Student student1 = new Student("zhangsan",19);System.out.println("===========================");Student student2 = new Student("lisi",20);}
}
执行结果:
Person:静态代码块执行
Student:静态代码块执行
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行
===========================
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行
经过上述分析,我们可以得出以下结论:
- 父类静态>子类静态>父类实例>父类构造>子类实例>子类构造,原理就是,在java文件被写好之后,类就已经存在了,所以不管是父类还是子类,静态代码块都会被优先执行,且父类优先于子类,在之后实例化子类对象的时候,由于子类继承于父类,所以父类的实例代码块和构造方法优先被加载,创建一个对象的时候,先为对象分配内存空间,于是实例代码块被执行,之后调用构造方法,构造方法被执行,之后就是子类的实例代码块和构造方法
- 第二次实例化的时候,父类和子类的静态代码块都不会再被执行,他们只执行一次,这是因为类只加载一次.
1.1.9 再谈访问限定符
再封装的章节,我们引入了访问限定符,主要限定:类或者类中成员能否再类外或者其他包中被访问
NO | 范围 | private | default | protected | public |
---|---|---|---|---|---|
1 | 同一包中的同一类 | √ | √ | √ | √ |
2 | 同一包中的不同类 | × | √ | √ | √ |
3 | 不同包中的子类 | × | × | √ | √ |
4 | 不同包中的非子类 | × | × | × | √ |
这里记忆的时候,分为同一包和不同包,同一类和不同类,子类和非子类.
那么父类中不同访问权限的成员,在子类中的可见性怎么样呢?
// extend01包中
public class B {private int a;protected int b;public int c;int d;
}
// extend01包中
// 同一个包中的子类
public class D extends B{public void method(){// super.a = 10; // 编译报错,父类private成员在相同包子类中不可见super.b = 20; // 父类中protected成员在相同包子类中可以直接访问super.c = 30; // 父类中public成员在相同包子类中可以直接访问super.d = 40; // 父类中默认访问权限修饰的成员在相同包子类中可以直接访问}
}
// extend02包中
// 不同包中的子类
public class C extends B {public void method(){// super.a = 10; // 编译报错,父类中private成员在不同包子类中不可见super.b = 20; // 父类中protected修饰的成员在不同包子类中可以直接访问super.c = 30; // 父类中public修饰的成员在不同包子类中可以直接访问//super.d = 40; // 父类中默认访问权限修饰的成员在不同包子类中不能直接访问}
}
// extend02包中
// 不同包中的类
public class TestC {public static void main(String[] args) {C c = new C();c.method();// System.out.println(c.a); // 编译报错,父类中private成员在不同包其他类中不可见// System.out.println(c.b); // 父类中protected成员在不同包其他类中不能直接访问System.out.println(c.c); // 父类中public成员在不同包其他类中可以直接访问// System.out.println(c.d); // 父类中默认访问权限修饰的成员在不同包其他类中不能直接访问}
}
- 如上面的代码所示,想要在子类中访问到父类中的成员,在允许访问到的前提下,我们可以通过super访问到
- 父类中的private成员虽然在子类中不可以访问到,但是也继承到了子类中
- 注意在使用访问限定符时,要认证思考,不可以滥用访问限定符,考虑好类中的字段和方法提供给“谁”用,从而加上合适的访问限定符
1.1.10 继承方式
在Java中只支持以下几种继承方式
注意:Java中不支持多继承
就像一个孩子不可以有多个父亲一样,但是一个父亲可以有多个孩子,就像上面的第三种
1.1.11 final关键字
我们有时不希望一个类被继承,我们希望在继承上进行限制,这时我们就需要用到final关键字
,而且final不仅可以修饰类,还可以用来修饰变量,成员方法
- 修饰变量或者字段,表示常量(即不可以被修改)
final int a=10;
//a=20 error
- 修饰类,表示一个类不可以被继承,称为密封类
final public class Animal{
//
}
//public class Cat extends Animal{
//
//} error
- 修饰方法:表示该方法不可以被重写
1.1.12 继承与组合
和继承类似,组合也是一种表达类之间关系的方式,也可以实现代码的复用。组合并没有用到什么关键字,也没有固定的语法格式,仅仅是将一个类的实例作为另外一个类的字段
继承表示的是一种is a的关系,比如猫是动物
而组合表示的是一种has a的关系,比如人有心脏
class Heart{
//
}
class Brain{
//
}
class Lungs{
//
}
class Person{private Heart heart;private Brain brain;private Lungs lungs;
}
组合和继承都可以实现代码的复用,该继承还是组合,一般看实际的应用场景
1.2 多态
1.2.1 多态的概念
通俗来说就是:多种形态,具体一点就是去完成某个行为,当不同的对象去完成的时候会产生出不同的效果
总的来说:同一件事情发生在不同对象的身上,就会产生不同的结果
1.2.2 多态实现的条件
Java中想要实现多态,必须要满足以下几个条件,缺一不可:
- 必须在继承体系下
- 子类必须要对父类中的方法进行重写
- 通过父类引用调用重写的方法
多态实现:在代码运行时,当传递不同类对象时,会调用对应类中的方法
public class Animal {String name;int age;public Animal(String name, int age){this.name = name;this.age = age;}public void eat(){System.out.println(name + "吃饭");}
}
public class Cat extends Animal{public Cat(String name, int age){super(name, age);}
@Overridepublic void eat(){System.out.println(name+"吃鱼~~~");}
}
public class Dog extends Animal {public Dog(String name, int age){super(name, age);}@Overridepublic void eat(){System.out.println(name+"吃骨头~~~");}
}
public class TestAnimal {// 编译器在编译代码时,并不知道要调用Dog 还是 Cat 中eat的方法// 等程序运行起来后,形参a引用的具体对象确定后,才知道调用那个方法// 注意:此处的形参类型必须时父类类型才可以public static void eat(Animal a){a.eat();}public static void main(String[] args) {Cat cat = new Cat("元宝",2);Dog dog = new Dog("小七", 1);eat(cat);eat(dog);}
}
当类的调用者在编写eat这个方法的时候,参数类型为Animal,此时在方法内部并不关注当前的a指向的是那个类型的实例,此时a这个引用调用eat方法可能会有多种不同的表现,这种行为称为多态
1.2.3 重写
- 概念
重写:也称为覆盖。重写就是对非静态方法,非private修饰的方法,非final修饰的方法,非构造方法的实现过程进行重新编写,返回值和形参都不能改变,即外壳不变,实现核心重写,好处就是子类可以在父类的基础上实现自己的方法,定义特定于自己的行为。 - 规则
- 子类在重写父类的方法时,一般情况下返回值和形参都不能改变
- 被重写的方法返回值类型有时可以不同,但是他们必须具有父子关系
- 访问权限不可以比父类中被重写的方法更低,例如:父类方法被public修饰,则子类方法中重写该方法就不能声明为private
- 父类方法不可以被static,final,private修饰,也不可以是构造方法,否者不可以被重写
- 重写的方法,编译器会用“@override”来注解,可以检查合法性
- 重写和重载的区别
区别点 | 重写 | 重载 |
---|---|---|
参数列表 | 一定不可以修改 | 必须修改 |
返回类型 | 一定不可以修改(除非有父子关系) | 可以修改 |
访问限定符 | 一定不可以降低权限 | 可以修改 |
- 静态绑定与动态绑定
- 静态绑定:也称为前期绑定,在编译时根据用户传递的参数类型就可以知道调用哪个方法,典型代表为方法的重载
- 动态绑定:也称为后期绑定,在编译时,不可以确定方法的行为,无法知道调用哪个方法,需要等程序运行时才可以确定。
1.2.4 向上转型和向下转型
- 向上转型
- 概念:创建一个子类对象,把他当做父类对象来使用,就是范围从小到大的转换
- 语法格式:==父类类型 对象名=new 子类类型()
Animal animal=new Cat("miaomiao",2);
我们就拿上面的代码来说明:
3. 使用场景
- 直接赋值
- 方法传参
- 方法返回
public class TestAnimal {
// 2. 方法传参:形参为父类型引用,可以接收任意子类的对象public static void eatFood(Animal a){a.eat();}
// 3. 作返回值:返回任意子类对象public static Animal buyAnimal(String var){if("狗".equals(var) ){return new Dog("狗狗",1);}else if("猫" .equals(var)){return new Cat("猫猫", 1);}else{return null;}
}
public static void main(String[] args) {Animal cat = new Cat("元宝",2); // 1. 直接赋值:子类对象赋值给父类对象Dog dog = new Dog("小七", 1);eatFood(cat);eatFood(dog);Animal animal = buyAnimal("狗");animal.eat();animal = buyAnimal("猫");animal.eat();}
}
通过上述的代码,我们不难发现向上转型的有限,他可以使得代码更加简单灵活,但是缺点也很明显,不可以调用子类特有的方法,怎么办呢,我们这里便引出了向下转型
-
向下转型
将一个子类对象经过向上转型之后当做了父类的对象使用,无法再调用到子类的方法,但是我们有时候会想要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型- 语法格式:子类类型 对象名=(子类类名)经过向上转型的父类对象
从上述语法格式来看,其实就是强制类型转换 - 安全性问题
向下转型存在不安全的问题,不同于向上转型,是安全的,比如原来一个对象是狗类,向上转为了动物类,再向下转型的时候必须转回狗类,不可以转为猫类
public class TestAnimal {public static void main(String[] args) {Cat cat = new Cat("元宝",2);Dog dog = new Dog("小七", 1);// 向上转型Animal animal = cat;animal.eat();animal = dog;animal.eat();// 编译失败,编译时编译器将animal当成Animal对象处理// 而Animal类中没有bark方法,因此编译失败// animal.bark();// 向上转型// 程序可以通过编程,但运行时抛出异常---因为:animal实际指向的是狗// 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastExceptioncat = (Cat)animal;cat.mew();// animal本来指向的就是狗,因此将animal还原为狗也是安全的dog = (Dog)animal;dog.bark();} }
- 解决方案
使用instanceof判断一个对象所属于的类是否属于一个类的父类
public class TestAnimal {public static void main(String[] args) {Cat cat = new Cat("元宝",2);Dog dog = new Dog("小七", 1);// 向上转型Animal animal = cat;animal.eat();animal = dog;animal.eat();if(animal instanceof Cat){cat = (Cat)animal;cat.mew();}if(animal instanceof Dog){dog = (Dog)animal;dog.bark();} } }
- 语法格式:子类类型 对象名=(子类类名)经过向上转型的父类对象
1.2.5 避免在构造方法中使用重写的方法
下面展示一段有坑的代码
class B {public B() {func();}public void func() {System.out.println("B.func()");}
}
class D extends B {private int num = 1;@Overridepublic void func() {System.out.println("D.func() " + num);}
}
public class Test {public static void main(String[] args) {D d = new D();}
}
运行结果:D.func() 0
难道结果不应该是1吗,我们下面解释为什么不是1
在构造D对象的同时,会调用B的构造方法,在子类构造的时候,先构造父类。B的构造方法调用了func方法,此时会触发动态绑定,会调用D中的func,此时D还没有触发构造方法,D自身还没有构造,此时num处于未初始化状态,值为0,所以输出为0.
所以我们在构造方法中调用重写方法时一定要注意,在自己编程是尽量避免这种行为