🌸个人主页: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.1 为什么需要继承](#1.1.1 为什么需要继承)
- [1.1.2 继承的概念](#1.1.2 继承的概念)
- [1.1.3 继承的语法格式](#1.1.3 继承的语法格式)
- [1.1.4 父类成员访问](#1.1.4 父类成员访问)
-
- 1.1.4.1子类中访问父类的成员变量
- [1.1.4.2 子类中访问父类的成员方法](#1.1.4.2 子类中访问父类的成员方法)
- [1.1.5 super关键字](#1.1.5 super关键字)
- [1.1.6 子类构造方法](#1.1.6 子类构造方法)
- [1.1.7 super和this](#1.1.7 super和this)
- [1.1.8 继承体系下的代码块](#1.1.8 继承体系下的代码块)
- [1.1.9 再谈访问限定符](#1.1.9 再谈访问限定符)
- [1.1.10 继承方式](#1.1.10 继承方式)
- [1.1.11 final关键字](#1.1.11 final关键字)
- [1.1.12 继承与组合](#1.1.12 继承与组合)
- [1.2 多态](#1.2 多态)
-
- [1.2.1 多态的概念](#1.2.1 多态的概念)
- [1.2.2 多态实现的条件](#1.2.2 多态实现的条件)
- [1.2.3 重写](#1.2.3 重写)
- [1.2.4 向上转型和向下转型](#1.2.4 向上转型和向下转型)
- [1.2.5 避免在构造方法中使用重写的方法](#1.2.5 避免在构造方法中使用重写的方法)
1. 继承与多态
1.1 继承
1.1.1 为什么需要继承
Java中使用类对现实中的事物进行描述的时候,由于世间事物错综复杂,事物之间难免会存在一些特定的关联 ,这就是程序设计时候所需要考虑的问题。
比如:猫和狗都是动物
我们使用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关键字,具体格式如下:
java
修饰符 class 子类 extends 父类{
//
}
现在,我们使用继承语法对猫类和狗类进行重新设计:
java
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子类中访问父类的成员变量
- 子类和父类不存在同名的情况
java
public class Base {
int a;
int b;
}
public class Derived extends Base{
int c;
public void method(){
a = 10; // 访问从父类中继承下来的a
b = 20; // 访问从父类中继承下来的b
c = 30; // 访问子类自己的c
}
}
不同名的情况下,可直接访问父类成员
- 子类和父类同名的情况
java
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 子类中访问父类的成员方法
- 成员方法不同名
java
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()
}
}
- 成员方法同名
java
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关键字 ,这个关键字的作用是:在子类方法中访问父类成员。
java
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 子类构造方法
父子父子,先有父再有子,即:子类对象构造时,需要先调用父类的构造方法,然后再执行子类的构造方法
java
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 继承体系下的代码块
还记得我们之前讲的代码块吗,我们简单回顾,主要有两种代码块 :实例代码块和静态代码块 ,在没有继承关系下的执行顺序是:静态代码块>实例代码块
java
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);
}
}
- 静态代码块先执行,并且只执行一次,在类加载阶段执行
- 有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后执行构造方法
(详细内容请参考前面的文章)
要是加入了继承关系呢?
java
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 | 不同包中的非子类 | × | × | × | √ |
这里记忆的时候,分为同一包和不同包,同一类和不同类,子类和非子类.
那么父类中不同访问权限的成员,在子类中的可见性怎么样呢?
java
// 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不仅可以修饰类,还可以用来修饰变量,成员方法
- 修饰变量或者字段,表示常量(即不可以被修改)
java
final int a=10;
//a=20 error
- 修饰类,表示一个类不可以被继承,称为密封类
java
final public class Animal{
//
}
//public class Cat extends Animal{
//
//} error
- 修饰方法:表示该方法不可以被重写
1.1.12 继承与组合
和继承类似,组合也是一种表达类之间关系的方式,也可以实现代码的复用。组合并没有用到什么关键字,也没有固定的语法格式,仅仅是将一个类的实例作为另外一个类的字段
继承表示的是一种is a 的关系,比如猫是动物
而组合表示的是一种has a 的关系,比如人有心脏
java
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中想要实现多态,必须要满足以下几个条件,缺一不可:
- 必须在继承体系下
- 子类必须要对父类中的方法进行重写
- 通过父类引用调用重写的方法
多态实现:在代码运行时,当传递不同类对象时,会调用对应类中的方法
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);
}
@Override
public void eat(){
System.out.println(name+"吃鱼~~~");
}
}
public class Dog extends Animal {
public Dog(String name, int age){
super(name, age);
}
@Override
public 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 子类类型()
java
Animal animal=new Cat("miaomiao",2);
我们就拿上面的代码来说明:
- 使用场景
-
直接赋值
-
方法传参
-
方法返回
java
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();
}
}
通过上述的代码,我们不难发现向上转型的有限,他可以使得代码更加简单灵活 ,但是缺点也很明显,不可以调用子类特有的方法,怎么办呢,我们这里便引出了向下转型
-
向下转型
将一个子类对象经过向上转型之后当做了父类的对象使用,无法再调用到子类的方法,但是我们有时候会想要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型
- 语法格式:子类类型 对象名=(子类类名)经过向上转型的父类对象
从上述语法格式来看,其实就是强制类型转换 - 安全性问题
向下转型存在不安全的问题,不同于向上转型,是安全的,比如原来一个对象是狗类,向上转为了动物类,再向下转型的时候必须转回狗类,不可以转为猫类
javapublic 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实际指向的是狗 // 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException cat = (Cat)animal; cat.mew(); // animal本来指向的就是狗,因此将animal还原为狗也是安全的 dog = (Dog)animal; dog.bark(); } }
- 解决方案
使用instanceof判断一个对象所属于的类是否属于一个类的父类
javapublic 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 避免在构造方法中使用重写的方法
下面展示一段有坑的代码
java
class B {
public B() {
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public 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.
所以我们在构造方法中调用重写方法时一定要注意,在自己编程是尽量避免这种行为