1. 继承
为什么要继承?
从生物学角度来说,继承就是把父辈的基因遗传给子代,然后子代再在细胞分裂的途中产生变异,生成比父辈更加适应环境的物种.其中很重要的就是继承给子代的基因(父类的方法和属性)和子代在父辈的基础上产生的变异(方法的重写). 比如猫和狗都是哺乳动物,是在哺乳动物的基础上发生进化和变异产生的独立的物种.它们都能吃饭和睡觉,但是他们的叫声不同.
由该图可看出,猫和狗的类存在大量的重复,因此我们提出了继承的思想,专门用来进行共性的抽取,实现代码的复用.
1.1 继承的概念
继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特 性的基础上进行扩展,增加新功能,这样产生新的类,称派生类(子类)。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。
例如:狗和猫都是动物,那么我们就可以将共性的内容进行抽取,然后采用继承的思想来达到共用
上述图示中,Dog和Cat都继承了Animal类,其中:Animal类称为父类/基类或超类,Dog和Cat可以称为Animal的子类/派生类,继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可。
从继承概念中可以看出继承最大的作用就是:实现代码复用,还有就是来实现多态(后序讲)
1.2 继承的语法
我们使用extends关键字来实现继承
修饰符 class 子类 extends 父类 {
//具体内容}
我们拿刚刚的猫和狗继承动物类的例子来说
//这个是父类
class Animal {
//下面是俩个类共同的特点,我们把类的共性进行抽取,从而实现代码的复用效果
//继承是 is a 的关系
public String name;
public int age;
public void eat(){
System.out.println(this.name+"正在吃饭!");
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("animal的构造方法");
}
static {
System.out.println("animal的静态代码块");
}
{
System.out.println("animal的实例代码块");
}
}
//以下是动物类的子类
public class Dog extends Animal{
public Dog(String name, int age) {
super(name, age);
System.out.println("Dog调用父类的构造方法,初始化成员");
}
public void bark(){
System.out.println(this.name+"正在狗叫");
}
}
class Cat extends Animal{
public Cat(String name, int age) {
super(name, age);//这个是调用父类的有俩个参数的构造方法
System.out.println("Cat调用父类的构造方法,初始化成员");
}
public void miao(){
System.out.println(this.name+"正在猫叫");
}
}
1.3 访问父类和自己的成员变量和方法
访问变量:
如果父子类的变量名字不同,我们直接创建对象之后直接调用即可.
如果父子类变量名字相同,那么我们优先调用子类的变量
访问方法:
成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错。
我们来看一段代码:
class Animal {
//下面是俩个类共同的特点,我们把类的共性进行抽取,从而实现代码的复用效果
//继承是 is a 的关系
public String name = "小黑";
public int age;
public void eat(){
System.out.println(this.name+"正在吃饭!");
}
public Animal() {
//无参构造方法
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("animal的构造方法");
}
}
}
public class Dog extends Animal{
public int id = 123;
public String name = "小白";
public Dog(String name, int age) {
super(name, age);
System.out.println("Dog调用父类的构造方法,初始化成员");
}
public Dog(String name, int age, int id) {
super(name, age);
this.id = id;
}
public Dog() {
}
public void bark(){
System.out.println(this.name+"正在狗叫");
}
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println(dog.name);//直接访问继承下来的name
System.out.println(dog.id);//直接访问dog自身的属性id
dog.eat();//使用父类继承下来的eat方法
dog.bark();//使用dog自身的bark方法
}
}
//执行结果:
小白
123
小黑正在吃饭!
小白正在狗叫
1.4 super关键字
然后提出一个问题,我们该怎怎么在子类方法中访问父类的成员?
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()
}
}
【注意事项】
1. 只能在非静态方法中使用
2. 在子类方法中,访问父类的成员变量和方法。
然后我们就可以根据super来构造子类的构造方法
package Class_Object.继承;
class Base {
public int a = 3;
public int c;
public Base(int a, int c) {
this.a = a;
this.c = c;
}
public void method(){
System.out.println("Base:method");
}
}
class Derived extends Base {
public int a = 10;
public int b;
public Derived(int a, int c) {
super(a, c);
}
public void method(){
System.out.println("Derived::method");
}
public void test() {
super.method();//调用的是父类的方法
this.method();//调用的是子类的方法
System.out.println(this.a);
System.out.println(a);
System.out.println(super.a);
}
}
public class Test {
public static void main(String[] args) {
Derived derived = new Derived(1,2);
//super不能在静态方法使用
//当子类和父类有同名的成员变量的时候,先看子类自己有没有,再看父类有没有
derived.test();
}
}
//Base:method
Derived::method
10
10
1
在子类构造方法中,并没有写任何关于基类构造的代码,但是在构造子类对象时,先执行基类的构造方法,然后执行子类的构造方法,因为:子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分 。父子父子肯定是先有父再有子,所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整
1.5 super和this的异同
super和this都可以在成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句,那他们之间有什么区别呢?
相同点:
1. 都是Java中的关键字
2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点:
1. this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成
2. 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
3. 在构造方法中:this(...)用于调用本类构造方法,super(...)用于调用父类构造方法,两种调用不能同时在构造 方法中出现4. 构造方法中一定会存在super(...)的调用,用户没有写编译器也会增加,但是this(...)用户不写则没有员的引用
1.6 再谈初始化
在java类和对象(上)篇里面我们介绍了几个代码块,我们现在再把继承的思想融入进去,看运行结果有什么不同
package Class_Object.继承;
public class Dog extends Animal{
public int id = 123;
public String name = "小白";
public Dog(String name, int age) {
super(name, age);
System.out.println("Dog调用父类的构造方法,初始化成员");
}
public Dog(String name, int age, int id) {
super(name, age);
this.id = id;
}
public Dog() {
}
public void bark(){
System.out.println(this.name+"正在狗叫");
}
static {
System.out.println("Dog的静态代码块");
}
{
System.out.println("Dog的实例代码块");
}
public static void main(String[] args) {
// Dog dog = new Dog();
// System.out.println(dog.name);//直接访问继承下来的name
// System.out.println(dog.id);//直接访问dog自身的属性id
// dog.eat();//使用父类继承下来的eat方法
// dog.bark();//使用dog自身的bark方法
Dog dog = new Dog("小白",12);
System.out.println("=============");
Dog dog1 = new Dog("小黑",12);
}
}
class Cat extends Animal{
public Cat(String name, int age) {
super(name, age);//这个是调用父类的有俩个参数的构造方法
System.out.println("Cat调用父类的构造方法,初始化成员");
}
public void miao(){
System.out.println(this.name+"正在猫叫");
}
static {
}
}
class Animal {
//下面是俩个类共同的特点,我们把类的共性进行抽取,从而实现代码的复用效果
//继承是 is a 的关系
public String name = "小黑";
public int age;
public void eat(){
System.out.println(this.name+"正在吃饭!");
}
public Animal() {
//无参构造方法
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("animal的构造方法");
}
static {
System.out.println("animal的静态代码块");
}
{
System.out.println("animal的实例代码块");
}
}
//animal的静态代码块
Dog的静态代码块
animal的实例代码块
animal的构造方法
Dog的实例代码块
Dog调用父类的构造方法,初始化成员
=============
animal的实例代码块
animal的构造方法
Dog的实例代码块
Dog调用父类的构造方法,初始化成员
通过分析执行结果,得出以下结论:
1、父类静态代码块优先于子类静态代码块执行,且是最早执行
2、父类实例代码块和父类构造方法紧接着执行
3、子类的实例代码块和子类构造方法紧接着再执行
4、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
1.7 protected 关键字
我们来填一下前面的坑
protected里面如果一个父类里面一个变量被protected修饰,那么我们可以在另一个包里面创建一个类,继承这个父类,然后创建对象,就能够访问父类的变量
package Class_Object.继承;
public class Test1 {
protected int a = 1999;//被protected修饰的变量
final public void method(){
System.out.println("这个是final不能被重写");
}
}
package Class_Object.Protected;
import Class_Object.继承.Test1;
public class Test extends Test1 {//不同包的子类
public void func() {
System.out.println(super.a);//使用父类被protected修饰的变量
//protected不同包中的子类
}
public static void main(String[] args) {
Test test = new Test();
test.func();
}
}
}
1.8 final关键字
在引入final之前我们先聊一下继承的方式,在java中,继承只能是单继承,不支持多继承,而且一般我们不希望出现超过三层的继承关系,这时,我们就需要final关键字了.我们可以把final放在不希望被继承的类的访问修饰限定符前面如:final public class 类名{}
final关键可以用来修饰变量、成员方法以及类。
1. 修饰变量或字段,表示常量(即不能修改)
final int a = 10; a = 20; // 编译出错
2. 修饰类:表示此类不能被继承
final public class Animal {
...
}
public class Bird extends Animal {
...
} /
/ 编译出错
Error:(3, 27) java: 无法从最终com.bit.Animal进行继
package Class_Object.Protected;
import Class_Object.继承.Test1;
public class Test extends Test1 {
public void func() {
System.out.println(super.a);
//protected不同包中的子类
}
public static void main(String[] args) {
Test test = new Test();
test.func();
}
//
// @Override
// public void method() {
// super.method();
// }TODO 无法重写,因为final修饰父类的该方法,该方法变成了密封方法不能被重写
}
//TODO final的几种用法
final class TestFinall {//TODO 1.它这个类加了final表示当前类不能被继承了
final int a = 10;//TODO 2.此时a就叫做常量,a只能初始化一次
// a = 20;//
//final表示不可变
}
//}
//class f extends TestFinall{
//
//}
1.9 继承和组合的关系
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。
继承表示对象之间是is-a的关系,比如:狗是动物
猫是动物组合表示对象之间是has-a的关系,比如:汽车
汽车和其轮胎、发动机、方向盘、车载系统等的关系就应该是组合,因为汽车是有这些部件组成的。
// 轮胎类
class Tire{
// ...
} /
/ 发动机类
class Engine{
// ...
} /
/ 车载系统类
class VehicleSystem{
// ...
}
class Car{
private Tire tire; // 可以复用轮胎中的属性和方法
private Engine engine; // 可以复用发动机中的属性和方法
private VehicleSystem vs; // 可以复用车载系统中的属性和方法
// ...
} /
/ 奔驰是汽车
class Benz extend Car{
// 将汽车中包含的:轮胎、发送机、车载系统全部继承下来
}
再比如学校,学校里面由学生和老师组成的:
package Class_Object.组合;
public class School {
//组合是has a的关系
public Student[] students;
public Teacher[] teachers;
int a;
public School() {
this.students = new Student[10];
this.teachers = new Teacher[10];
this.a = a;
}
}
class Student{
}
class Teacher{
}
差不多是这个意思: 把各个部件设置成一个类,然后把整体再设置一个类(框架),再在整体里面创建各个部件的类来填充这个类.
2.多态
2.1 多态的概念
简单来说就是多种形态,即:不同对象完成某个行为的时候会产生不同的状态.
比如:一个好看的小姐姐来和你搭话,你是一个好的态度,一个大老爷们来找你的时候你是另一个态度.
总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果
2.2 多态的实现条件
1. 要有继承关系(向上转型)
2. 子类和父类有同名的 重写(覆盖) 方法
3. 通过父类的引用 去调用这个重写方法
看个例子:
package Class_Object.多态;
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name+"正在吃东西");
}
public Animal test(){
return null;
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
public void bark() {
System.out.println(this.name + "狗叫");
}
@Override //这个只是个注解 ctrl+o
public void eat() {
//在继承关系上,满足方法返回值,方法名字,方法的参数列表一致,那么就是方法的重写
System.out.println(this.name+"正在吃狗粮");
}
@Override
public Dog test(){
return null;//这个返回值父类是父类类型,子类为子类类型
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(this.name + "正在吃猫粮");
}
@Override
public Cat test() {
return null;
}
public void miao() {
System.out.println(this.name+"正在喵喵叫");
}
}
public class Test {
//向上转型
//子类对象给父类
public static void fuc(Animal animal) {
//TODO 传参进行向上转型
}
public static Dog fuc2(){
Dog dog = new Dog("12",2);
return dog;//TODO 返回值进行向上转型
}
public static void eatFunc(Animal animal) {
animal.eat();//TODO 多态: 当父类引用 引用的子类对象不一样的时候,调用这个重写的方法,表现出来的行为是不一样的
}
public static void main(String[] args) {
Dog dog = new Dog("小白",9);
Cat cat = new Cat("大白",89);
eatFunc(dog);
eatFunc(cat);
}
public static void main1(String[] args) {
Dog dog = new Dog("小白",9);
dog.eat();
dog.bark();
Animal animal1 = dog;//animal1这个引用指向了dog所指向的对象
System.out.println("===========");
Animal animal = new Dog("小黑",8);//TODO 直接引用
animal.eat();
fuc(dog);//这个也是向上转型
Animal animal2 = fuc2();//这个也是向上转型
animal.eat();//这个就是动态绑定,调用的时子类的eat(eat时父子类的重写方法)
animal.test();//此时也是动态绑定(协变)
System.out.println(dog);
Animal animal3 = new Cat("大白",89);
}
}
Animal是父类,Dog是子类,我们用父类的引用指向子类的对象,也就是:
父类 对象名 = new 子类(),然后我们再通过对象名来调用子类重写的父类的方法,这个过程就叫多态
2.3 重写
重写(override): 也称为覆盖.重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
【方法重写的规则】
子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致.
被重写的方法返回值类型可以不同,但是必须是具有父子关系
访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
父类被static、private修饰的方法、构造方法都不能被重写。
重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
重写和重载的区别:
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现
反正记住,重写只能改内容,其他不能改,构造方法一般不进行重写.
【重写的设计原则】
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的类,可能还在有用户使用,正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求了。
然后我们聊聊静态绑定和动态绑定:
静态绑定(前期绑定/早绑定): 比如你重载了一个方法再编译的时候,就会根据实参类中具体调用哪个方法
动态绑定(后期绑定/晚绑定): 在编译的时候不能确定方法的行为(先调用父类的),需要等到程序运行时,才能确定具体调用哪个类的方法(再确定调用子类的)
上次我们讲的toSting来打印对象就用到了方法的重载和动态绑定,
这个Object的toString(),注意Object是所有类的父类
因此我们没有重写toString方法来直接调用object的toString方法:
就会出现这个类似地址的东西
但是如果我们重写了toString方法
结果是:
然后我们就能够更好的理解重写了:重写就是为了扩充这个方法内部的实现功能.
2.4 向上转型和向下转型:
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型()
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
向下转型: 父类引用再还原为子类对象
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实际指向的是狗
// 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException
cat = (Cat)animal;
cat.mew();
// animal本来指向的就是狗,因此将animal还原为狗也是安全的
dog = (Dog)animal;
dog.bark();
}
}
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换。
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();
}
}
}
2.5 多态的实际用例:
我们学习到这里应该已经了解了继承和多态的使用,现在写一个例子
package Class_Object.多态的应用;
abstract public class Shape {//这是个抽象类,后面下一节会讲,现在单纯理解为一个父类
abstract public void draw();
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("矩形");//重写draw方法
}
}
class Triangle extends Shape{
@Override
public void draw() {
System.out.println("三角形");//重写draw方法
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("圆形");//重写draw方法
}
}
public static void main(String[] args) {//这个是法2
Shape[] shapes = {new Cycle(),new Triangle(),new Rect()};//造一个shape数组,然后直接在里面new 它的子类对象
for (Shape shape:shapes) {
shape.draw();//直接for循环,循环到哪个就调用哪个draw方法
}
}
public static void main1(String[] args) {//这个是法1
Cycle cycle = new Cycle();
Rect rect = new Rect();
Triangle triangle = new Triangle();
String[] strings = {"cycle","rect","triangle"};
for (String x:strings){
if(x.equals("cycle")) {
cycle.draw();
} else if (x.equals("rect")) {
rect.draw();
} else if (x.equals("triangle")) {
triangle.draw();
}//但是if else的大量使用会造成圈复杂度太高
}
}
}
看到法1我们发现圈复杂度太高(使用大量的 if - else)不利于开发
并且法二有利于对扩展
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}