继承概述
-
类中为什么需要继承?
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。子类就可以访问到父类的实例变量和实例方法。 -
继承格式
使用关键字extends
描述子类与父类之间的关系 -
继承的利弊
优点:
继承提高了代码的复用性;
提高了代码的维护性;如果想要更改整个动物的习性,只需要修改一个Animal类,如果没有继承,需要修改每一个动物类;
让类与类之间产生了关系,是多态的前提
缺点:
软件开发的迪米特原则就是高内聚,低耦合,内聚指的是自己完成某件事的能力,耦合指的是类与类的关系,这样程序才能达到最大的复用。但是继承的出现使得类与类之间产生关系耦合性增强; -
Java中类的继承特点
- Java只支持单继承,一个子类只能继承一个父类;
- 支持多层继承(子继承父,父继承爷,子拥有父与爷的成员);
- 子类只能继承父类所有非私有成员(成员变量和成员方法),子类无法访问被关键字private修饰的成员,注意父类里面不能存在私有构造(因为子类的构造方法第一行会默认的调用父类的构造方法);
- 子类不能继承父类的构造方法,但是可以通过super关键字去访问父类构造方法;
- 不要为了部分功能去继承(继承的缺点导致)因为继承会增加耦合性;继承就是抽取多个子类的共性部分组成父类;
- java继承体系中的顶层父类:object,所有类都会直接或间接继承它,不写继承的类默认继承object
public class MyTest {
public static void main(String[] args) {
Son son = new Son();
System.out.println(son.num);//200
System.out.println(son.f);//20000
//2.Java中子类只能继承父类非私有的成员,私有成员子类无法继承。
//System.out.println(son.a);//报错
//3.构造方法不参与继承。
//son.Fu();//报错
}
}
//1.Java中只支持单继承,一个子类只能有一个父类,但是支持多层继承。
class Fu {
public Fu() {
}
int num=200;
private int a=500;
}
class Father extends Fu{
int f=20000;
}
class Son extends Father{
}
- 那么什么时候使用继承呢?
如果有两个类A,B。只有他们符合A是B的一种,或者B是A的一种,就可以考虑使用继承。
this和super的区别和应用
为什么需要super?---------子类局部范围访问父类成员
this——代表的是本类对象的引用;
super——代表的是父类存储空间的标识(可以理解成父类的引用,可以操作父类的成员);
this和super的使用
a:调用成员变量
this.成员变量 调用本类的成员变量
super.成员变量 调用父类的成员变量
b:调用构造方法
this(…) 调用本类的构造方法
super(…) 调用父类的构造方法
c:调用成员方法
this.成员方法 调用本类的成员方法
super.成员方法 调用父类的成员方法
小练习1 理解this与super:
子类调用方法:
编译器会先去——>查找子类有没有这个方法,如果有就使用,如果没有就去——>父类里面查找,最终没有的话就会——>报错;
public class MyTest {
public static void main(String[] args) {
Son son = new Son();
son.show();
son.test();
}
}
class Father{
int num = 20;
public void hehe(){
System.out.println("父类的方法");
}
}
class Son extends Father{
int a=2000;
int num=2;
public void show(){
System.out.println(a);//2000
System.out.println(this.num); //2
System.out.println(super.num); //20
System.out.println(new Father().num);//20
}
public void test(){
//子类调用方法: 编译器会先去——>查找子类有没有这个方法,如果有就使用,如果没有就去——>父类里面查找,最终没有的话就会——>报错;
//用子类对象调用的子类继承的父类的方法
this.hehe();
//用的是父类空间标识,来调用的父类方法。
super.hehe();
}
public void heihei(){
this.show(); //调用本类的方法
}
}
小练习2:求程序运行结果
class Fu {
static {
System.out.println("静态代码块Fu"); //1
}
{
System.out.println("构造代码块Fu"); //3
}
public Fu() {
System.out.println("构造方法Fu"); //4
}
}
class Zi extends Fu {
static {
System.out.println("静态代码块Zi");//2
}
{
System.out.println("构造代码块Zi"); //5
}
public Zi() {
//super(); 执行默认的父类空参构造方法
System.out.println("构造方法Zi"); //6
}
}
class Test {
public static void main(String[] args) {
Zi z = new Zi(); //请执行结果。
}
}
执行结果:
静态代码块Fu
静态代码块Zi
构造代码块Fu
构造方法Fu
构造代码块Zi
构造方法Zi
上面程序分析:
1、类字节码文件进入方法区(没有初始化数据),先找到父类,加载父类字节码文件;
2、执行父类的时候会先执行父类的静态代码块,它随类一起进入方法区执行;
3、方法区内然后加载子类,这时子类的静态代码块也执行了;
4、接着方法区内找到main方法,进入栈区读取代码,因为要new对象,因此会执行构造方法在堆区分配内存空间;
5、但是在执行构造方法之前必须先要执行构造代码块;
6、子类的构造方法执行之前先执行父类的构造方法;
7、因此父类的构造代码块先执行,然后执行父类的构造方法;
8、再依次执行子类的构造代码块、构造方法。
继承中构造方法的关系
我们在创建子类对象时,为什么会先去调用父类的构造方法呢?
解释:
- 因为子类要继承父类的数据,甚至要使用父类的数据,所以,在初始化子类的时候,想要调用父类的构造方法,来完成父类数据的初始化,这样子类才能够继承父类的数据和使用父类的数据。
- 在每个类的构造方法中的第一行,有一行默认语句是super() 会去调用父类的空参构造,来完成父类数据的初始化,然后才执行子类构造方法的内容。
父类没有空参构造,怎么办?
方法一: 在父类中添加一个无参的构造方法
方法二: 子类通过super去显示调用父类有参的构造方法:
如果父类定义了有参构造方法,子类在完成父类数据初始化时要调用父类有参构造方法:子类构造方法中第一行写super(参数)
方法三: 子类通过
this
去调用本类的其他构造方法
本类其他构造方法也必须首先访问了父类构造,再执行当前构造方法的其他语句
注意事项:
super(…)用来调用父类构造方法、this()通过调用本类构造来间接调用父类构造方法,super(…) 或者 this(….)都必须出现在构造方法第一条语句上(为了保证父类的构造函数先被执行),因此this和super不能同时存在构造方法中
public class MyTest extends Object {
public static void main(String[] args) {
// 方法二: 子类通过super去显示调用父类有参的构造方法:
//先找到子类对应的有参构造方法,先执行父类的构造方法不指定父类构造方法就默认执行父类空参构造,再执行子类构造方法的内容。
Zi zi1 = new Zi("aaa", 100);
//方法三: 子类通过this去调用本类的其他构造方法
Zi zi = new Zi("aaa");
}
}
//Java继承体系中的顶层父类是Object类,所有类都是直接或间接继承自他
class Fu extends Object {
int num = 100;
public Fu() {
super();
System.out.println("父类的空参构造调用了");
}
public Fu(String name) {
super();
System.out.println("父类的有参构造调用了");
}
public Fu(String name, int age) {
super();
System.out.println("父类的有参构造调用了" + name + "==" + age);
}
}
class Zi extends Fu {
public Zi() {
super("abc"); //调用父类有参构造
System.out.println("子类的空参构造调用了");
}
public Zi(String str) {
//this();和super(); 不能同时存在在构造方法里面
this();//调用本类空参构造
//this("abc",23); //调用本类有参参构造
System.out.println("子类的空参构造调用了");
}
public Zi(String str, int age) {
// this();和super(); 不能同时存在在构造方法里面
super(str, age);
System.out.println("子类的空参构造调用了");
}
}
执行结果:
父类的有参构造调用了aaa==100
子类的空参构造调用了
父类的有参构造调用了
子类的空参构造调用了
子类的空参构造调用了
- 继承中的成员变量及成员方法之间的关系
对于成员变量:
-
- 子类中的成员变量和父类中的成员变量名称一样,在子类中访问一个变量的查找顺序(“就近原则”),首先在子类的方法的局部范围中查找,有就使用,没有的话会去——>子类的成员变量中找,如果还找不到,——>就去父类的成员范围内找,如果找不到就——>报错;
-
- 子类没有父类中的成员变量名称时,子类继承父类的成员变量值
public class Person{
public static void main(String[] args) {
Teacher teacher1 = new Teacher();
//子类teacher没有父类中的成员变量age时,调用父类的成员变量值
System.out.println(teacher1.age);//200
Student student1 = new Student();
//自己调用自己的成员变量,与父类无关
System.out.println(student1.age);//100
//子类中的成员变量和父类中的成员变量名称一样时,
// a: 在子类的方法的局部范围找,有就使用
// b: 在子类的成员范围找,有就使用
// c: 在父类的成员范围找,有就使用
// d:如果还找不到,就报错
student1.show(20);
//子类继承父类的sleep方法
teacher1.sleep();
}
}
class Personn {
int age=200;
String sex;
public static void sleep(){
System.out.println("sleep");
}
}
class Teacher extends Personn {
int age=10;
public static void job(){
System.out.println("teaching");
}
}
class Student extends Personn{
int age=100;
public static void task(){
System.out.println("studying");
}
public void show(int age){
//成员变量的就近原则:当成员变量和局部变量重名时,变量访问原则遵循就近原则,先在方法内和形参找,找不到就去该类的成员范围找,如果找不到就去父类的成员范围找
System.out.println(age);//20
System.out.println(this.age);//100
System.out.println(super.age);//200
}
}
对于成员方法:
通过子类调用方法:
编译器会先去——>查找子类有没有这个方法,如果有就使用,如果没有就去——>父类里面查找,最终没有的话就会——>报错;
方法重写概述及其应用
方法重写概念:
子类中出现了和父类中一模一样的方法声明(方法名,参数列表,返回值类型),也被称为方法覆盖,方法复写。
方法重写的应用:
当子类需要父类的功能,而功能主体子类有自己特有内容时,可以重写父类中的方法。
这样,即沿袭了父类的功能,又定义了子类特有的内容。
方法重写的注意事项:
-
只有父类实例方法参与重写;
-
父类中私有方法不能被重写;
因为父类私有方法子类无法访问,就无法继承,更不能重写 -
父类构造方法不参与重写;
因为子类不继承父类构造方法,只是在创建子类的时候访问执行父类构造方法 -
父类静态方法不参与重写;
静态方法属于类,不参与重写,子类对于父类的静态方法重写就是自定义一个子类的静态方法; -
父类final修饰的方法子类不能重写;
-
子类重写父类方法时,访问权限不能更低
最好就一致 -
子类重写父类方法的时候,最好声明一模一样。
小练习:定义手机的父类与子类
public class Phone {
public void call(){
System.out.println("打电话");
}
protected void sendMsg(){
System.out.println("发短信");
}
public static void fuShow(){
System.out.println("父类静态方法");
}
}
public class IPhone extends Phone {
@Override
public void call() {
//super沿袭父类的功能
super.call();
//扩展你自己的功能
System.out.println("自己扩展了视频通话");
}
//public> protected >缺省>private
@Override
public void sendMsg() {
System.out.println("发彩信");
}
// @Override可以检测该方法是不是重写父类,报错,说明此方法不是重写父类
public static void fuShow() {
System.out.println("子类自定义静态方法,不是对父类静态方法的重写");
}
}
测试:
public class MyTest {
public static void main(String[] args) {
IPhone iPhone = new IPhone();
iPhone.call();
//重写时注意的事项。@Override 他可以检测该方法是不是重写父类 Ctrl+O 重写父类方法。
iPhone.fuShow();//对象调用静态方法
IPhone.fuShow();//子类调用自己的静态方法
Phone.fuShow();//父类调用自己的静态方法
}
}
执行结果:
打电话
自己扩展了视频通话
子类静态方法
子类静态方法
父类静态方法
final关键字
为什么会有final
由于继承中有一个方法重写的现象,而有时候我们不想让子类去重写父类的方法.这对这种情况java就给我们提供了一个关键字: final
final概述
final关键字是最终的意思,可以修饰类,变量,成员方法。
final关键字修饰类,方法以及变量的特点
final修饰特点
- 修饰类: 被修饰类不能被继承
- 修饰方法: 被修饰的方法不能被重写,可以继承。因此final与public常搭配
- 修饰变量: 被修饰的变量不能被重新赋值,因为这个量其实是一个常量
对于基本类型,是值不能被改变
对于引用类型(对象),是地址值不能被改变
public class MyTest {
//公共的静态常量
public static final int B = 2000;
public static void main(String[] args) {
//此静态常量不可以被重新赋值
//报错: MyTest.B=100;
System.out.println(MyTest.B);//2000
//final修饰基本类型,指的是,这个值不能被再次改变。
//final修饰引用类型,指的是,这个地址值不能再次被改变。
final Fu fu = new Fu();
System.out.println(fu);//org.westos.demo94.Fu@4554617c
}
}
//定义一个final类,此类不能被继承
final class Fu{
public final void show(){
System.out.println("fu show");
}
}
//Fu不能被继承,以下代码报错
/* class Zi extends Fu{
}*/
多态概述
-
多态的概述
指的是一种事物,在不同时刻,所表现出的不同状态。
比如说,动物这个类可以表现为猫、狗、鸟。。等等 -
多态的前提
1、要有继承关系;
2、要有方法重写,其实没有也是可以的,但是如果没有多态就没有意义;
3、要有父类引用接收各种子类对象:Animal one =new Cat();Animal two =new Dog();
注意:多态one、two是父类的引用,它调用静态方法仍然是父类的静态方法。
多态的成员访问特点
1、成员变量
编译看左边(父类有此成员变量才不报错),运行看左边(多态形式访问成员变量访问的是父类的成员变量);
2、构造方法
创建子类对象的时候,会访问父类的构造方法,首先对父类的数据进行初始化;
3、成员方法
编译看左边(看父类是否有此方法(有,语法不报错)),运行看右边(以子类重写过后的方法为准,如果子类没有重写就运行父类的方法);
4、静态方法
编译看左边,运行看左边;(静态属于类,只能继承不参与重写,所以只看当前引用变量所属的类中的静态方法。尽管子类有和父类相同的静态方法,也只是子类自定义一个与父类相同的静态方法,与父类无关,所以多态中对于静态方法的访问,依旧是多态所属的父类的静态方法的调用)
public class MyTest {
public static void main(String[] args) {
//多态中,子类创建对象先执行父类构造方法,再执行子类构造方法
Fu fu = new Zi();
//多态的形式来访问成员变量,编译看左边,运行也看左边。多态的形式访问成员变量,访问的都是父类的变量。
int n=fu.num;
System.out.println(n); //100
//编译看左边,运行看右边,编译期,看父类中有没有这个方法,如果有语法就报错,实际调用时,会以子类重写过后的为准。
//当然,子类确实没有重写,那就以父类方法为准。
fu.show();
//多态访问静态方法,是对多态所属的父类的静态方法的调用。
fu.jing();
Fu.jing();//父类调用自己的静态方法
Zi.jing();//子类继承父类的静态方法,对父类静态方法的调用
}
}
class Fu{
public Fu() {
System.out.println("父类 构造调用了");
}
int num=100;
public void show(){
System.out.println("fu show");
}
public static void jing(){
System.out.println("fu jing");
}
}
class Zi extends Fu{
int num=10;
public Zi() {
System.out.println("子类构造执行了");
}
@Override
public void show() {
System.out.println("zi show");
}
}
执行结果:
父类 构造调用了
子类构造执行了
100
zi show
fu jing
fu jing
fu jing
成员方法重写的深度理解:
class A {
public void show() {
show2();
}
public void show2() {
System.out.println("我");
}
}
class B extends A {
public void show2() {
System.out.println("爱");
}
}
class C extends B {
public void show() {
super.show();
}
public void show2() {
System.out.println("你");
}
}
public class DuoTaiTest4 {
public static void main(String[] args) {
A a = new B();
a.show(); // 爱
B b = new C();
b.show(); // 你
}
}
【分析】:
1、上面的程序,发现首先创建了一个B类的对象,但是使用A类的引用来接收,父类的引用指向子类的对象,这就是多态的形式;
2、因为使用该引用去调用方法,因此这里应该遵循编译看左,执行看右;也就是编译器回首先查看A类里面有没有show(),然后再去执行B类里面的show();
3、显然,A里面存在show(),但是B类里面没有重写A里面的show(),因此a继承A的show()方法,由于A的show()方法调用了show2()方法,因此a.show()就是在运行a.show2(),这里依旧遵循编译看左,执行看右;也就是编译器首先查看A类里面有没有show2(),然后再去执行B类里面的show2();因此a.show()执行结果为“爱”;
4、b.show();中,这次是使得B类的引用指向C类的对象,因为B本身继承于A,虽然B里面没有定义方法show(),但是他继承了A里面的方法,因此编译看左不会报错;执行看右边,由3可知b.show();就是在执行b.show2(),这里依旧遵循编译看左,执行看右;也就是编译器首先查看B类里面有没有show2(),然后再去执行C类里面的show2();因此b.show()执行结果为“你”;
多态的好处和弊端
好处:
1、提高了代码的维护性(继承保证);维护只是维护父类即可。
2、提高了代码的扩展性(由多态保证) ;设计一个类,方法形参类型使用父类引用,传入时为各种子类对象,对重写的子类方法调用。这个方法可以接收它所有的子类。
对于多态好处的理解:首先定义多个子类继承父类
public class Animal {
public void eat(){
System.out.println("吃饭");
}
}
class Dog extends Animal {
public void eat(){
System.out.println("狗吃骨头");
}
public void lockdoor(){
System.out.println("狗看门");
}
}
class Rabbit extends Animal{
public void eat(){
System.out.println("兔子吃草");
}
public void beauty(){
System.out.println("兔子可爱");
}
}
其次设计一个TestUtils类,它的方法形参类型使用父引用,指向各种子类对象,对重写的子类方法调用。这个方法就可以接收它各种各样的子类
public class TestUtils {
//让此类构造方法私有化,不想让这个类被实例化
private TestUtils(){}
//设计一个类的方法形参类型使用父引用,指向各种子类对象,对重写的子类方法调用。这个方法就可以接收它各种各样的子类
public static void testEat(Animal animal){
//形参类型为父类,给形参传入子类,父类引用指向子类对象,eat()方法的调用是子类对父类重写的eat方法,使得多种子类实现各自的自定义方法
//Rabbit rabbit=new Rabbit();
// Animal animal=rabbit;
animal.eat();
}
}
测试:使得多种子类(狗、兔子)实现各自的自定义方法
public class Test {
public static void main(String[] args) {
Dog dog=new Dog();
TestUtils.testEat(dog);
Rabbit rabbit=new Rabbit();
TestUtils.testEat(rabbit);
}
执行结果:
狗吃骨头
兔子吃草
弊端:
不能直接调用子类自定义(非重写父类)的方法,也就是不能使用子类特有的功能。
解决方法:可以采用向下转型来调用子类特有方法,而多态就是向上转型向下转型:把父类的引用强制转换为子类的引用。
小练习:理解向下转型使得多态可以调用子类自定义功能
public class Test {
public static void main(String[] args) {
Fu fu=new Zi();
//可以采用向下转型来调用子类特有方法;
Zi one= (Zi) fu;
one.hehe();//调用子类特有方法
System.out.println(one.num);//100
class Fu{
int num=10;
public void show(){
System.out.println("父类show方法");
}
}
class Zi extends Fu{
int num=100;
@Override
public void show() {
System.out.println("子类重写父类show方法 ");
}
public void hehe(){
System.out.println("子类自定义的非父类重写的方法");
}
}
向下转型的注意事项:父类引用指向第一种子类向(第一种子类)下转型,不能继续向下转型为第二种子类,要先将父类引用指向第二种子类,再向下转型。
小练习:理解向下转型的注意事项,首先定义父类与子类
public class Animal {
public void eat(){
System.out.println("吃饭");
}
}
public class Dog extends Animal {
public void eat(){
System.out.println("狗吃骨头");
}
//子类特有功能,多态无法调用
public void lockdoor(){
System.out.println("狗看门");
}
}
public class Rabbit extends Animal{
public void eat(){
System.out.println("兔子吃草");
}
//子类特有功能,多态无法调用
public void beauty(){
System.out.println("兔子可爱");
}
}
测试:
public class Test {
public static void main(String[] args) {
//父类与一种子类转型可以互相转换
Animal an=new Dog();//父类引用指向子类狗 狗这个子类向上转型为父类an
an.eat();//使用子类狗的重写方法
//父类an向下转型为狗
Dog dog= (Dog) an;
dog.lockdoor();//可以调用子类狗的自定义方法
//父类引用想要向下转型为第二中子类兔子,需要先将引用指向第二种子类兔子,再向下转型
an=new Rabbit();//将父类引用指向第二种子类兔子
an.eat();//父类an执行子类的重写eat方法
Rabbit rabbit= (Rabbit) an;//父类引用向下转型为第二中子类兔子
rabbit.beauty();
}
}
执行结果:
狗吃骨头
狗看门
兔子吃草
兔子可爱
多态的内存图解
子类会在创建出对象的空间为父类开辟一段内存空间用来存放父类的变量;
抽象类
抽象类:抽取所有子类的共性功能,但是不给出共性功能的具体实现,而是交由子类根据自身的特性做以具体的实现。
抽象类特点
a:抽象类和抽象方法必须用abstract关键字修饰
抽象类格式: abstract class 类名 {} 抽象方法格式: public abstract void eat();
b:抽象类不一定有抽象方法,有抽象方法的类一定是抽象类
c:抽象类中可以有构造方法,抽象类不能进行实例化(无法创建具体事物),那么要构造方法有什么作用呢?
用于子类访问父类数据时的初始化
d:抽象类不能直接实例化那么,抽象类如何实例化呢?
按照多态的方式,由具体的子类实例化。其实这也是多态的一种,抽象类多态。
举例如下:
public class MyTest2 {
public static void main(String[] args) {
//抽象类,不能直接实例化,我们可以通过多态的形式,间接实例化父类
Fu fu= new Zi();
}
}
abstract class Fu{
int num=20;
final int A=200;
//抽象类有构造方法,作用:用于子类访问父类数据时的初始化
public Fu(){
System.out.println("父类的构造执行了");
}
}
class Zi extends Fu{
public Zi() {
System.out.println("子类的构造执行了");
}
}
e:抽象类的子类
要么是抽象类
要么重写抽象类中的所有抽象方法
抽象类的成员特点
成员变量:既可以是变量,也可以是常量。
构造方法:有,用于子类访问父类数据的初始化。
成员方法:既可以是抽象的,也可以是非抽象的。
抽象方法:强制要求子类做的事情。,抽象类中所有抽象方法强制子类必须重写;
非抽象方法:子类继承的事情,提高代码复用性。
小练习:理解抽象类
假如我们在开发一个系统时需要对员工(Employee) 类进行设计,员工包含3个属性:姓名、工号以及工资(salary)。 经理(Manager) 也是员工,除了含有员工的属性外,另为还有一个奖金(bonus) 属性。然后定义工作的方法. 请使用继承的思想设计出员工类和经理类
首先定义一个抽象员工类与经理类,经理类继承员工类
public abstract class Employee {
String name;
int number;
double salary;
public Employee(String name, int number, double salary) {
this.name = name;
this.number = number;
this.salary = salary;
}
public Employee() {
}
public abstract void working();
}
class Manager extends Employee{
double bonus;
public Manager(String name, int number, double salary) {
//创建对象首先执行父类的有参构造方法,把参数传递过去
super(name, number, salary);
}
//写出子类空参构造方法,子类有参构造方法的存在会让字类空参构造方法不存在
//如果想要使用空参构造实例化对象,父类也要有空参构造,因为子类空参构造默认第一行是调用父类的空参构造
public Manager() {
}
@Override
public void working() {
System.out.println(
"little work");
}
public void businesstrip(){
System.out.println("经理出差");
}
}
测试: