首页 > 其他分享 >第五章 子类与继承

第五章 子类与继承

时间:2024-10-13 15:18:04浏览次数:3  
标签:重写 继承 子类 对象 第五章 父类 方法

第五章 子类与继承

5.1 子类与父类

当我们准备编写一个类,发现某个类有我们所需要的成员变量和方法,如果我们要服用这个类的成员对象和方法,那我们可以把我们编写的类作为某个类的子类,这也就是我们常说的继承

继承,可以先定义一个共有属性的一般类,根据该一般类再定义具有特殊属性的子类,子类继承一般类的属性和行为,并要添加自己的新的属性和行为,子类会继承父类的属性(成员变量),包括它们的默认值。如果父类中定义了成员变量的初始值,那么子类的实例在创建时也会拥有这些初始值,除非在子类中被显式地修改。(声明对象后,都是独立的一份拷贝,子类和父类对象内存也是独立的一份)

继承得到的类叫做子类,被继承的类叫做父类(超类)
特别特别需要注意的是,java不能多重继承(这是区别于c++很大的部分),一个子类只能有一个父类----is-a关系,但是一个父亲可以有很多儿子,所以说也是树状结构

5.1.1 子类

格式:class 子类名 extends 父类名{}

5.1.2类的树形结构

  • 如果C是B的子类,B是A的子类,习惯上称C是A的子孙类
  • java的类按照继承关系形成树形结构,根节点为Object类(java.lang包中),即Object类是所有类的祖先类,任何类都是Object的子孙类,每个类(除了Object类,无父类)有且仅有一个父类,一个类可以用一个或多个子类。
  • 如果一个类没有extends关键字,默认为Object的子类,即class A的效果和Class A extends Object一样

5.2 子类的继承性

继承之后,父亲的成员变量就也在子类里面声明了,可以被子类中自己定义的任何实例方法操作,而子类继承父类的方法,就体现在这个方法可以被自己定义的任何实例方法调用

记住这一点:子类和父类不管同不同包都可以继承,只不过继承的权限不一样

5.2.1 子类和父类在同一包中的继承性

很自然的,如果子类和父类在同一包中,那么就继承了子类中不是private的成员(包括方法和变量)作为自己的成员,继承过来的成员变量和方法的访问权限不变

5.2.2 子类与父类不在同一包中的继承性

当子类和父类不在同一个包中的时候,子类只继承父类中public和protected(不包括友好类)的成员,这就是我上一章说的protected和友好类的区别就在这里了)------>这里可以这么想,protected就是保护,也就是保护了跨包后的父子关系哈哈

5.2.3 继承关系的UML图

从子类引一条实线指向父类
继承的UML图

5.2.4 protected的进一步说明(关于子类对象的调用问题)

会有点绕,我先简单复习一下前面对protected的阐述
刚刚说到,子类和父类即使不在同一个包中,父类中protected的成员也是可以被继承下来的,所以无论如何,父类中的protected成员永远可以被子类继承

好,那我们还要补充什么只是呢。

  1. 如果在D类在D本身创建了一个对象,那么该对象总是可以通过"."去调用protected(包括其他所有)的方法与变量,这是很自然的,我们上一章有讲过)
  2. 但是如果在另一个类中创建里这个对象(再简单复习一下:上一章讲过,只有public的成员才能被相应对象跨包调用(先简单理解一下这句话),好好理解这句话和上面那句父类中protected成员是可以被继承下来的的区别,这两句初学可能有点绕)

Other类中用子类D建了一个对象object:
  A对于子类自己声明的protected成员,需要Other类和子类D在同一个包中,那么object就可以访问这些protected成员
  B对于子类从父类继承的protected成员,需要追溯到该protected成员所在的祖先类(也就是最早声明这个成员的地方),如果Other类和该祖先类在同一个包,那object就可以访问这些protected成员

你这时候可能脑子有点乱吧?多看几遍,然后看看我下面的总结。
(下面用目前类Now表示那个调用类D创建对象d的类)
  首先,刚才有复习过,只有public的成员才可以被相应对象跨包调用,所以protected的成员理论上是不可以被相应对象跨包调用的。
  理解了上面,下面就顺理成章了。接下来会有两种情况,
第一种:protected的成员就是类D本身声明的,那就不能被d跨包调用,所以必须确保该对象所在类D与Now类在同一个包中;
第二种:protected的成员是从某祖先类A继承下来的,那是不是这时候就要看这个protected成员的源头在哪个包,只有在A和Now同包才可以被对象d调用
  因为子类和祖先类不一定在同一个包中(前面讲过,不管同不同包都可以继承),所以有时候可能会有一种子类的对象跨包调用其protected成员的错觉,但是其实本质上是因为最先声明这个成员是其祖先,且祖先与Now类在同一个包,才可以这样调用的。所以本质上,都是看最先声明该成员的类的包与Now是否在同个包中。

辛苦辛苦,接下来看张图
protected详情
如果能看懂这张图,你就出师了!

5.3 子类与对象

5.3.1 子类对象的特点

  创建一个对象的时候,不仅子类中的全部实例变量被分配了内存,父亲的全部实例变量(包括没继承的)也都分配了内存,但是只将子类继承的那部分实例变量分配给子类对象(同包—>public,protected,友好,不同包---->public,protected)
  那岂不是浪费了?这一部分既不是子类对象的变量,当然也不是父类对象的变量(注意:即使子类继承了父类,但是他们并非共享了这个变量哦,每个对象都有自己的一份拷贝)。其实并不浪费,因为从父类继承过来的方法是可以操作这部分未继承的变量

以下这段我是借鉴了知乎的,大家可以跳转也可以看我的,基本一样的
作者:小猴子1024
链接:子类能否继承父类的私有变量
来源:知乎
  这里其实有一个复杂的问题,就是子类能否继承父类的私有变量?其实是不行的(官方说了),但是不能继承不代表不能使用,倘若父类中提供了public或者protected修饰的方法来访问该属性,比如set,get方法,这样在子类中是可以通过方法来使用该private属性的。需要注意的是这里不能被子类继承并非指的是不能被子类使用,父类private属性是会存在于子类对象中的,下文有验证。


//Person作为父类,有一个private修饰的属性且提供了public修饰的get和set方法
class Person {
    private int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}
//子类student继承父类Person
class Student extends Person {
    private String name;//set、get略
}

//测试类
public class Test {
    public static void main(String[] args) {
        Student stu = new Student();
        //利用set方法给父类中的id赋值
        stu.setId(1001);
        //利用get方法获取父类中的id
        System.out.println(stu.getId());
    }
}

在创建子类对象的时候,父类中的private属性在哪?
  通过上面的代码示例可以看出,子类对象可以通过父类提供的public修饰的方法来访问私有属性,那么该私有属性在哪里呢?答案是在该子类对象的堆内存中,需要注意的是这里并不会产生Person对象。也就是说子类可以通过父类的方法操纵这一块privated,这一块其实也是独属于该子类的,只不过子类只能通过父类的方法去搞它。

----->总结:根据oracle官方文档的描述,子类是不能继承父类中private修饰的属性,只不过从内存的角度看的话,父类private属性是会存在于子类对象中的。

5.3.2 关于instanceof运算符

这是独属于java的双目运算符

左边的操作元是对象,右边的操作元是类。
当左边的对象是右边类或其子类的对象的时候,结果是true,否则是false
eg,zhang instanceof Student

5.4 成员变量的隐藏和方法重写

5.4.1 成员变量的隐藏

在声明变量的时候,所声明的成员变量的名字与从父亲继承来的成员变量名字相同(与类型无关),在这种情况下就会隐藏所继承的成员变量。(我们上一章也有讲到隐藏,那里的隐藏指的是局部变量会隐藏同名的成员变量,这时候的隐藏是可以用this去调用的)

特点如下:

  1. 子类对象以及子类自己的方法操作这个与父类同名的成员变量是指子类重新声明的这个成员变量
  2. 子类继承的方法所操作的成员变量一定是被子类继承或隐藏的成员变量

总结:

  • 子类继承的方法只能操作子类继承和隐藏的成员变量,无法操作新声明的成员变量(这个其实很好理解,子类继承的方法也就是父亲的方法,怎么会知道子类会有什么新特点呢)
  • 子类新定义的方法可以操作子类继承和子类新声明的成员变量,但是无法操作被隐藏的成员变量(需使用super关键字才可以,后面5.5会讲)

5.4.2 方法重写(方法覆盖)

子类通过重写可以隐藏已经继承的方法

1)重写的语法规则

有权继承就有权重写。子类中定义一个方法,该方法的名字、参数个数、参数的类型和父类的方法完全相同,这个方法的类型和父亲的方法类型一致或者是父亲的方法的类型的子类型,那就是子类重写

2)重写的目的

子类通过方法的重写可以把父类的状态和行为改变为自身的状态和行为。

  • 重写方法既可以操作继承的成员变量.调用继承的方法,还可以操作新声明的成员,但是不能操作被隐藏的成员(除非用super)//这个和上面那个新定义方法的操作范围基本是一样的(毕竟重写,也就相当于新定义了)
  • 注意区分重载和重写

我来举个例子
例子1:

//若父类中有一个
float computer(float x,float y){
	return x+y;
}
//而子类中有一个
double computer(float x,float y){
	return x*y;
}

这是不被允许的,会编译错误,double和float不一样,没有办法达成重写覆盖,则子类出现除了类型其余都相同的两个方法,这是不被允许的(因为这样也够不成重载,也够不成重写)

例子2:

//若子类是下面这样
float computer(float x,float y,double z){
	return x-y;
}

这样是可以的,虽然没有构成重写,但是构成了方法重载。

3)重写的注意事项

  • 重写父类的方法时候,不允许降低方法的访问权限,但是可以提高访问权限(也就是放得更开)----->想:孩子都比较放得开,大人才注重隐私hhhh

5.5 super 关键字

5.5.1 用super操作被隐藏的成员变量和方法

子类一旦隐藏了继承成员(包括变量和函数),那么子类创建的对象就不再拥有该成员,那么该成员就归super关键字所有,就只能用super关键字来调用,super调用的成员变量是隐藏的或继承的(这个不用解释了吧,毕竟是父亲的才可能被隐藏)

5.5.2 用super调用父类的构造方法

当用子类的构造方法时,子类的构造方法总会先调用父类的某构造函数。子类不继承父类的构造方法,所以要用super(),而且super必须是子类构造方法的头一条语句

那具体是哪一个构造函数呢?

  • 如果我们没有显示地调用super(),或者我们写super(),那么就调用父亲的没带参数的构造函数(所以说,最好我们在写类的构造函数的时候,一定要有一个没带参数的构造函数,防止子类忘记显示调用super()
  • 如果我们显示地调用了super(xx,xx),那就调用父类中的A(xx,xx)那个构造函数

5.6 final关键字

final可以用来修饰类,成员变量,和方法中的局部变量

5.6.1 final类

可以使用final将类声明为final类final class A{},final类不能被继承,既不能有子类(见名知义了final也就是最后一个)

经常起到一个保护的作用,比如String类,不能被拓展,因为其涉及编译器和解释器的正常运行

5.6.2 final方法

如果用final修饰父类的一个方法,那么该方法不允许子类重写,也就隐藏不了。

5.6.3 常量

成员变量和局部变量被修饰为final,那他就是常量,运行期间不能再发生变化,所以声明的时候一定要给他赋值。

5.7 对象的上转型对象

知识回顾:
我们以前学过基本数据类型之间的类型转换:
  第一种:小容量转化成大容量,叫做自动类型转换
  第二种:大容量转换成小容量,叫做强制类型转换
其实,除了基本数据类型之间的转换之外,对于引用数据类型来说,也有类型转换,只不过我们不叫其做自动类型转换或者强制类型转换,而是叫它们做向上转型和向下转型。

注意:这些转型的前提都是有继承关系

  1. 向上转型(upcasting):子------->父(类比于自动类型转换)
  • 子类型的对象赋值给一个父类型的引用father A =new son()
  • 就像我们常说的,美国人是人。

  1. 向下转型(downcasting):父-------->子(类比于强制类型转换)
  • 父类型的引用可以转换为子类型的引用,但是需要加强制类型转换符
  • 我们不能说人是美国人吧。

注意:这里不是指可以把father的实体转成孩子类型的实体!!下面会详细讲,现在留个悬念。

上转型的实体是子类负责创建的,但是上转型会失去原对象的一些属性和功能
上转型对象示意图
如图所示,上转型可以调用隐藏的变量和继承的变量,也可以调用继承和重写的方法(这个对后面多态的实现很有用)

  • 可以将对象的上转型对象再强制转换到一个子类对象,这时候,子类对象又具备了子类所有的属性和功能(注意:这里一定得是上转型对象才可以进行向下转型
  • 什么意思呢?先知道强制转换可能会导致运行时异常,因为在转换的过程中,如果对象的实际类型与转换的目标类型不兼容,就会抛出ClassCastException异常。

  • 子类转父类:
Son A = new Son()
Father B = A //这是正确的
  • 父类转子类:
1. 真实父类对象转子类对象,报ClassCastException异常
Father f = new Father();
Son s = (Son)f;//出错 ClassCastException
2. “假”父类对象转子类对象,可以(假父亲也就是上转型对象---->本质上父亲的内在还是个宝宝)
Father f = new Son();
Son s = (Son)f;//可以

还有一个点,就是如果是实例方法被重写,那么会被覆盖(所以可以实现多态),如果是类方法(静态方法)被重写,那么不会被覆盖,只是会被隐藏,仍可以通过类名调用。
如果子类重写了父类的静态方法,那么子类对象的上转型对象不能调用子类重写的静态方法,只能调用父类的静态方法。

上面的内容是不是都还比较抽象,那么java到底底层是怎么实现这样的呢

Animal a2 = new Cat()
a2.move()

看上面这个程序,我们来分析一下:
第一阶段:编译阶段

  1. 在编译的时候,编译器只知道a2的类型是animal类型,因此在编译的时候就会去Animal类中找move()方法,找到之后,绑定上去,此时发生静态绑定,能够绑定成功,所以编译通过
    第二阶段:运行阶段
  2. 在运行的时候,堆内存中真实的java对象是Cat类型,所以move()的行为一定是Cat对象发生的,因此运行的时候会自动调用Cat对象的move()方法,这种绑定叫做运行期绑定/动态绑定。

  以上也就是我们常说的”编译看左边,运行看右边“,因为编译阶段是一种形态,编译之后又是另外一种形态,因此得名:多态。

Cat c2 = (Cat)a2;
c2.catchMouse(); //这是子类中特有的方法,必须向下转型后才可以调用
Animal x = new Cat();
Bird y = (Bird)x;
1.以上的情况编译是可以通过的。因为x是Animal类型,Animal
与Bird之间存在继承关系,语法没问题,所以继承通过了。
2.以上的情况运行是通过不了的,会出现ClassCastException
(类型转换异常)?因为运行时堆中真实对象是Cat对象,Cat无法
转化为Bird,则出现类型转换异常。

其实前面讲的instanceof就可以用来避免类型转换异常的问题

if(a instanceof Cat)
Cat c = Cat(a)

5.8 继承与多态

其实我们上节课讲的很多知识都是为这节课的知识服务的(前面其实也讲了多态的概念了)------>当一个类有很多个子类,并且子类都重写了父类的某个方法,那么把该子类创建的对象的引用放到一个父类对象中,也就是上转型对象,那么这个上转型对象在调用这个方法时就可能有多种形态,这就是多态!

子类重写父类的某一实例函数,然后通过上转型对象调用,就可以调用子类独特的函数。------>有同学会想知道为什么要这样?因为这样之后,我们可以提高程序的复用性以及让程序更加简洁,降低程序的耦合度。不懂也没关系,后面会讲的。迫切想知道的,看看这个博主多态

5.9 abstract类和abstract方法

用关键字abstract修饰的类叫做abstract类(抽象类)
abstract class A{}
用abstract方法修饰的方法称为abstract方法(抽象方法)

对于abstract方法: 只允许声明,不允许实现,且不能用final和static修饰。(前面有讲到上转型对象不能调用abstract方法)

那么abtract类呢?

  1. 区别于其他类,abtract类中可以有abstract方法,也可以有非abstract方法。(当然abstract类也可以没有abstract方法)
  2. abstract类不能用new运算符创建对象。如果一个非抽象类继承了某个抽象类,那它必须重写父类的抽象方法,给出方法体,这就是为什么不允许使用final和abstract类修饰同一个方法或类的原因。
  3. abstract类的子类:A若该子类是非abstract类,它必须重写(第2条讲过了,此处略);B若该子类是abstract类,它可以选择重写abstact方法,也可以选择继承anstact方法。
  4. 第二条讲过不能用abstract类new对象,但是可以用其声明对象,则该对象可以成为其子类对象的上转型对象,那么该对象就可以调用子类重写的方法。
  5. 抽象类的意义:

这个更为重要:
1)抽象出重要的行为标准,该行为标准用抽象方法表示,即抽象类封装了子类必须要有的行为准则;
2)抽象类声明的对象作为上转型对象,可以调用子类的重写的方法,即体现子类的具体行为。

5.10 面向抽象编程

这一节主要讲解的就是为什么要用多态,也就是我前几节课留下来的悬念。
所谓面向抽象编程,是指当设计某种重要的类的时候,不让该类面向具体的类,而是面向抽象类,即所设计类的重要数据是抽象类声明的对象。(高亮这句话将贯穿我们下文)

现在我们来看个例子。

public class Circle{
double r;
Circle(double r){
	this.r =r;
}
public double getArea(){
	return(3.14*r*r);
}
}

public class Pillar{
Circle bottom;
double height;
Pillar(Circle bottom,double height){
	this.bottom = bottom;
	this.height = height;
}
public double getVolume(){
	return bottom.getArea()*height;
}
}

上面这段程序实现了柱体求体积,乍一看没什么问题,但是问题来了,现在客户想要底面为矩形咋办?是不是只能改Pillar类?(软件设计面对的最大问题就是用户需求的变化)。但是我们发现,不管是求圆为底,还是求矩形为底,他们其实都是要求图形有求面积的功能!!我们是不是就抽象出了一个共同点,那我们就可以用这个共同点去搞一个抽象类。

public abstract class Gemetry(){   //Gemetry可以理解为可以算面积的底面。
	public abstract double getArea();
}
public class Pillar{
	Gemetry bottom;
	double height;
	Pillar(Geometry bottom,double height){  //这个函数是精髓所在!!
		this.bottom = bottom;
		this.height = height;
	}
	public double getVolume(){ //改进后的函数可以算出由任何可算面积底面构成的图形的体积。
		if(bottom == null){
			System.out.println("没有底,不能计算体积");
			return -1;
		}
		else{
			return bottom.getArea()*height;
		}
	}
}
public class Rectangle/.... extends Geometry{
	double a,b;
	Rectangle/..(double a,double b){
		this.a = a;
		this.b = b;
	}
	public double getArea(){
		return a*b/....;
	}
}

是不是很妙?将抽象类作为形参,这时候只要定义任何可以算面积的底面去继承它,用户都可以算出相应的体积。我们来看看用户实现的界面是怎么样的。

public class Application{
	public static void main(String args[]){
		Pillar pillar;
		Geometry bottom = new Rectangle(12,22);
		pillar = new Pillar(bottom,58);
		System.out.println("体积"+pillar.getVolume());
		bottom = new Circle(10);
		pillar = new Pillar(bottom,58);
		System.out.println("体积"+pillar.getVolume());
	}
}

现在不管要添加什么底面都很简单!用好上转型对象!

总结一下:面向抽象编程的目的是为了应对用户需求的变化,将某个类中因需求变化而需要修改的代码从该类中分离出去(尽量不要修改类,否则后患无穷)。

5.11 开-闭原则

所谓开闭原则,就是对扩展开放,对修改关闭。这句话的本质是讲当系统中增加新的模块的时候,不需要修改现有的模块。其实就是我们上节课讲得内的内容,这是一种系统设计的思想。符合开闭原则的系统是以维护的。一般我们无法让每一部分都遵守开闭原则,我们要把主要经历用在思考核心部分的开闭原则。

哇!恭喜,坚持到这里的都很不容易!java其实真的很有魅力,因为它真的很有逻辑,很接近人类思考的方式。

每位优秀的程序员都曾是初学者,从小步开始,走向伟大,共勉!!

标签:重写,继承,子类,对象,第五章,父类,方法
From: https://blog.csdn.net/2301_79939181/article/details/142725368

相关文章

  • 第五章 CSS盒模型
    盒模型是CSS定位布局的核心内容,页面中所有的元素都是放进一个容器内的,这个容器可以看成是一个盒子。可以通过CSS来控制这些盒子的各种显示属性,把这些盒子进行定位,完成整个页面的布局。在掌握了盒子模型以及其中每个元素的用法后,才能拥有较完善的布局观。5.1盒模型的定义web......
  • 第五章 作业
    1.用盒模型技术,制作一个“走进内心的文学”页面。<!DOCTYPEhtml><html> <head> <metacharset="utf-8"/> <title>走进内心的文字</title> <style> .all{ width:700px; height:600px; } .father{ background-image:......
  • tour cpp: std::variant 实现无继承层次的访问者模式
    std::variant是基于模板而实现的一种包括了一个标志位的高级union对象;可以完全替代如下场景:structst{inttype;unionun{inti;floatf;};};#include<iostream>#include<variant>template<class...base>structoverloaded:bas......
  • C++(继承)
    1.继承1.1基础使用继承就是在一个已经存在的类的基础上新建立一个类,新创建的类拥有之前类的特性。继承是面向对象的三大特性之一,体现了代码复用的思想。已经存在的类被称为“基类BaseClass”或“父类”新创建的类被称为“派生类”或“子类SubClass”下面是一个最简......
  • 第五章 CSS盒模型
    5.1盒模型的定义Web页面上大部分的元素(特别是块状元素)都可以看作是一个盒子,W3C组织建议把所有网页上的对象都放在一个盒子中,设计者可以通过创建定义来控制这个盒子的各种属性,这些对象包括段落、列表、标题、图片以及层。盒子的结构可以看作一个矩形框,包括边框、外边距、内边......
  • 继承--C++
    文章目录一、继承的概念及定义1、继承的概念二、继承定义1、定义格式2、继承基类成员访问方式的变化3、继承类模板三、基类和派生类间的转换1、继承中的作用域2、隐藏规则:四、派生类的默认成员函数1、4个常见默认成员函数2、实现⼀个不能被继承的类五、继承与友元六......
  • 08 JAVA 继承多态抽象类接口
    1函数题1,super.a来明确访问父类的字段。super(a);表示调用父类的构造函数,并传递参数a2,抽象类继承需要写新的构造器,重写抽象方法classCircleextendsshape{privatedoubleradius;publicCircle(doubleradius){this.radius=radius;}//构......
  • 第五章CSS盒模型
    5.1盒模型的定义盒模型示意图:5.2CSS元素的高度和宽度5.2.1盒模型的宽度width5.2.2盒模型的高度height<!DOCTYPEhtml><html> <head> <metacharset="utf-8"> <title></title> <style> *{ margin:0px; padding:0px; ......
  • 第五章作业
    1.用盒模型技术,制作一个“走进内心的文学”页面。<!DOCTYPEhtml><html> <head> <metacharset="utf-8"> <title>走进内心的文字</title> <styletype="text/css"> .all{ width:700px; height:850px; } .top{ ......
  • 第五章 CSS盒模型
    5.1盒模型的定义盒模型是在CSS中用来描述和控制一个元素在页面中所占空间的一种模型。在盒模型中,每个元素被看作一个矩形的盒子,其大小由四个边界确定:上边界(top)、下边界(bottom)、左边界(left)和右边界(right)。这些边界围成一个矩形,决定了元素的尺寸和位置。盒模型由以下几个部分组......