------- android培训、java培训、期待与您交流! ----------
在前面提到,面向对象的三个特征:封装、继承、多态。从Day07开始,我们就正式开始学习继承的相关知识。
举几个例子:
我们描述现实生活中的两种动物:老虎和猫。(以下对老虎和猫的描述请不要深究,不是学生物的)
猫是一种动物,小时候靠吃母乳维持生命,猫会爬树,叫声喵喵的,猫吃肉...等等;老虎是一种动物,小时候靠吃母乳维持生命,老虎不会爬树,叫声嗷嗷的,老虎吃肉...等等。在这些描述(相当与Java中的类)中,他们有很多共性的地方:小时候都吃母乳,都吃肉类...等等。这样我们就考虑,是否能将他们共性的地方全都提取出来,独立封装成一个新的描述(Java中的类),并给这个描述起一个名字。这样以后再进行描述的时候,直接引用这个新的描述的名字来代替之前很长的一段描述。如:将都吃肉类 和 小时候都吃母乳 提取为“肉食类哺乳动物”。这样只要说有一种肉食类哺乳动物会爬树,叫声喵喵的就能想到描述的是猫。
再比如,描述工人和学生。工人有姓名,有年龄,有工号等属性,还有工作的方法;学生有姓名,有年龄,有学号等属性,还有学习的方法。他们的共性内容姓名和年龄恰恰是人类所共有的,我们可以提取出来,单独封装;他们各自的特有属性和特有方法保留即可。
上面提到的猫和老虎都属于肉食类哺乳动物,学生和工人都属于人,这些就是Java中的继承。即,将很多类的共性内容抽取并封装成一个独立的类,让其他类与这个类产生关系,这个关系就是继承。
继承(Extends)
继承提高了代码的复用性,让类与类之间产生了关系。有了这个关系,才有了多态的特性。
注意:千万不要为了获取其他类的功能、简化代码而继承,必须是类与类之间有所属关系才可以继承。
如何判断是否有继承关系?先去“继承”一下,如果父类中的内容子类都具备,那么他们有继承关系;如果“父类”中的某些功能“子类”不具备,那么他们就不能够形成继承关系。
Java语言中,Java只支持单继承,不支持多继承。因为多继承容易带来安全隐患:当多个父类中定义了相同功能,但是功能的具体内容不同时,子类对象不确定执行哪一个。但是Java保留了这种机制,并使用另一种体现形式来完成表示,称为多实现。
Java支持多层继承,也就是一个继承体系。
class A{}
class B extends A{}//B继承A,B是子类,A是父类
class C extends B{}//C继承B,C是子类,B是父类
如何使用一个复杂继承体系中的功能呢?
想要使用体系,先查阅体系中父类的描述,因为父类中定义的是体系中最共性的功能。
通过了解共性的功能,就可以知道该体系的基本功能。那么这个体系已经可以基本使用了。
在具体调用时,创建最子类的对象:
1.父类有可能不能创建对象;
2.创建子类对象可以使用更多的功能,包括基本的也包括特有的。
简单一句话就叫,查阅父类功能,创建子类对象使用功能。
除了继承,类与类之间的关系还有聚合,聚集,组合...等。
类中的成员包括变量、函数、构造函数三部分。
子父类出现后,类成员的特点:
1.变量:
子类出现非私有的同名变量时,子类访问本类中的变量,用this;子类访问父类中的变量,用super。super的使用和this的使用几乎一致。
this代表本类对象的引用,super代表父类对象的引用。
2.子父类中的函数:
当子类中定义了和父类一模一样的函数时,如果子类对象调用了该函数,会运行子类函数的内容,就如同父类的函数被覆盖了一样。
这种情况是函数的另一个特性——重写(或者称为覆盖)。父类的方法其实是还在内存当中的,只不过没有被使用而已。
子类继承父类,沿袭了父类的功能到子类中。然而子类虽然具备该功能,但是有可能功能的内容和父类不一致。这时没有必要定义新的功能,而是使用函数覆盖的特性,保留父类的功能定义,重写该功能的内容。
class Fu
{
void run()
{
System.out.println("Fu run");
}
}
class Zi extends Fu//子类继承父类
{
void run()//与父类中的run()方法一模一样,重写父类方法
{
System.out.println("Zi run");
}
}
class ExtendsDemo0
{
public static void main(String[] args)
{
Zi z = new Zi();//创建子类对象
z.run();//调用run()方法
}
}
执行结果为:
使用覆盖需要注意:
①.子类覆盖父类,必须保证子类权限大于等于父类权限,才可以覆盖,否则编译失败;
②.静态只能覆盖静态。
这里注意,当父类中的方法被private修饰后,子类中的相同方法不是覆盖。因为被private修饰后,方法不对外提供,子类“不知道”父类有这个方法,所以不叫覆盖。
class Fu
{
private void show(){}//私有,不对外提供/显示此方法
}
class Zi extends Fu
{
void show(){}//不属于覆盖,但是可以编译通过
}
关于函数的两个“相似”特性——重载和重写的区别:重载只看同名函数的参数列表,而重写要求子父类方法一模一样,包括返回值的类型也要一模一样。
3.子父类中的构造函数:
在对子类对象进行初始化时,父类的构造函数也会运行,因为子类的构造函数默认第一行有一条隐式的语句 super(); ,该语句会访问父类中空参数的构造函数。而且子类中所有的构造函数第一行默认都是 super();
class Fu
{
Fu()//父类空参数构造函数
{
System.out.println("Fu run");
}
Fu(int x)//父类构造函数
{
System.out.println("Fu int run");
}
}
class Zi extends Fu//继承父类
{
Zi()
{
//隐式语句super();
System.out.println("Zi run");
}
Zi(int x)
{
//隐式语句super();
System.out.println("Zi int run");
}
}
class ExtendsGouZao
{
public static void main(String[] args)
{
Zi z = new Zi();//创建两个不同的子类对象
Zi z1 = new Zi(4);
}
}
运行结果为:
说明子类构造函数的第一行确实默认有一条隐式语句 super();
为什么子类一定要访问父类中的构造函数呢?
由于父类中的数据子类可以直接获取,所以子类在建立对象时,需要先查看父类是如何对这些数据进行初始化的。所以子类在对象初始化时,要先访问一下父类中的构造函数。如果要访问父类中指定的构造函数,可以通过手动定义super语句的方式来指定。
注意:super语句一定定义在子类构造函数的第一行。子类构造函数绝不可能覆盖父类构造函数。
子类的实例化过程:子类的所有的构造函数,默认都会访问父类中空参数的构造函数。因为子类每一个构造函数内的第一行都有一局隐式的super()。当父类中没有空参数的构造函数时,子类必须手动通过super语句形式,指定要访问的父类中的构造函数。当然,子类的构造函数第一行也可以手动指定this语句来i访问本类中的构造函数。子类中至少会有一个构造函数会访问到父类中的构造函数。
所有类的父类是Object类。
为什么this 和 super语句不能同时存在于构造函数中?
因为根据this()和super()的性质,他们都必须在构造函数的第一行,初始化动作要先进行。
继承的出现有利有弊,继承打破了封装性。
final关键字
final,“最终”的意思。是一个修饰符。
final的特点:
1.可以修饰类、函数、变量;
2.被final修饰的类不可以被继承;
3.被final修饰的方法不可以被复写;
4.被final修饰的变量是一个常量,只能赋值一次,既可以修饰成员变量,也可以修饰局部变量。当描述事物时,一些数据的值是固定的,那么为了增强阅读性,我们给这些数据起一个名字,方便于阅读。而这个值不需要改变,所以加上final修饰。作为常量,常量的书写规范是所有的字母都大写,如果由多个单词组成,单词间通过"_"连接。
5.内部类定义在类中的局部位置上时,只能访问该局部被final修饰的局部变量。
抽象类abstract
当多个类中出现相同功能,但是功能的主体不同时,可以进行向上抽取。这时只抽取功能定义,而不抽取功能主体,这些功能定义需要被abstract修饰。
将这些功能定义封装成一个抽象类,当子类继承这个抽象类后,自己定义方法主体。
abstract可以修饰类和方法。被修饰的类就叫抽象类,被修饰的方法成为抽象方法。
抽象类的特点:
1.抽象方法一定在抽象类中;
2.抽象方法和抽象类都必须被abstract关键字修饰;
3.抽象类不可以用new创建对象。因为调用抽象方法没有意义。
4.抽象类中的抽象方法要被使用,必须由子类复写其所有的抽象方法后,建立子类对象调用。如果子类只覆盖了部分抽象方法,那么该子类还是一个抽象类。
记住,抽象类与之前定义的类没有太大的区别,它仅仅是对相同功能定义向上抽取后得到的产物。
对于事物,该怎么描述就怎么描述,只不过事物中有了一些“看不懂”的东西。这些不确定的部分也是该事物的功能,所以需要明确提出,但是无法定义其主体,就用抽象方法来表示。这些需要继承它的子类去完成。
抽象类比一般类多了抽象方法,抽象类不可以实例化。
抽象的存在可以强制子类定义一些方法。
抽象类中可以定义非抽象方法。特别的,抽象类中可以不定义抽象方法,这样做仅仅是不让该类建立对象。
综合前面所学,编写了一段用于打印图形的代码,如下:
abstract class Shape{//定义图形类
double area;//定义面积变量
String name;//定义名称变量
abstract public void draw();//定义画图的抽象方法
public final double area(){//定义获取面积的方法
return area;
}
public final void show()//展示图形属性
{
System.out.println("我的形状是"+name+"我的面积是"+area);
}
}
class Triangle extends Shape{//定义Triangle类继承shape类
double d,h;//定义成员变量(底和高)
Triangle(String name,double d,double h){//定义变量为图形名称、底、高的Triangle的构造函数
this.d = d;
this.h = h;
super.area = d*h/2;//三角形面积公式
super.name = name;
}
public void draw(){//定义方法,展示图形名称及其面积
System.out.println("形状是"+name+"******面积是"+area);
}
}
class Rectangle extends Shape{//定义Rectangle类继承Shape类
double l,w;//定义成员变量(长和宽)
Rectangle(String name,double l,double w){//定义变量为图形名称、长、宽的Rectangle的构造函数
this.l = l;
this.w = w;
super.area = l*w;//长方形面积公式
super.name = name;
}
public void draw(){//定义方法,展示图形名称及其面积
System.out.println("形状是"+name+"++++++面积是"+area);
}
}
class PrintShape{
public static void main(String[] args){
Triangle t = new Triangle("三角形",20,10);//将Triangle类实例化
Rectangle r = new Rectangle("长方形",8,9);//将Rectanglel类实例化
t.draw(); //调用三角形的draw()方法
t.show();
r.draw();//调用长方形的draw()方法
r.show();
}
}
运行结果为:
模版方法模式
当定义功能时,功能的一部分是确定的,但是有一部分是不确定的,并且确定的部分在调用不确定的部分。这时,就将不确定的部分暴露出去,由该类的子类去完成。
模版方法模式的出现,提高了代码的扩展性和复用性。
实战练习:
获取一段程序执行所用的时间。
abstract class GetTime//定义抽象工具类,用来获取时间
{
public final void getTime()//定义获取时间的通用模版
{
long start = System.currentTimeMillis();
this.runcode();//调用同类中不确定的抽象方法。成员间调用通过对象完成,所以这里实际上省略了"this."
long end = System.currentTimeMillis();
System.out.println("毫秒:"+(end-start));
}
abstract public void runcode();//由于具体执行什么代码是不确定的,所以将方法抽象,交给子类重写
}
class SubTime extends GetTime//继承GetTime抽象工具类
{
public void runcode()//复写父类不确定的方法
{
for (int x=0;x<4000 ;x++ )
{
System.out.print(x);
}
}
}
class TemplateDemo
{
public static void main(String[] args)
{
SubTime gt = new SubTime();//创建SubTime实例对象
gt.getTime();//对象调用getTime()方法
}
}
执行结果为:
注意:模版方法模式是一种设计思想,思想的提出是由于类中方法相互调用时,可确定的方法调用了不能确定的方法。对应的解决办法是将不确定的部分对外暴露,由子类去完成。并不一定要将该方法抽象。
接口(Interface)
接口的初期理解可以认为是一个特殊的抽象类,该抽象类中的所有方法都是抽象方法。类与接口之间的关系用实现(implement)来表示。
定义类的关键字是class,定义接口的关键字是interface。
接口定义时的格式特点:
1.接口中常见定义:常量和抽象方法;
2.接口中的成员都有固定的修饰符进行修饰。修饰常量的是public static final,修饰方法的是public abstract
注意:接口中的成员都是public的
interface Inter
{
public abstract void show();
public static final int NUM = 3;
}
接口中的成员(成员变量和成员函数)前的修饰符是固定的,如果书写的时候漏写了,Java会自动的将其补上。但是为了提高代码的阅读行方便他人理解,最好是自己手动的将这些修饰符写出。
接口是不可以创建对象的,因为有抽象方法。需要被子类实现,子类对接口中的抽象方法全都覆盖后,子类才可以实例化;否则,子类是一个抽象类。
注意,由于接口中所有的成员都是public的,而函数的覆盖要求权限必须不低于原方法的权限。所以复写的方法必须也要用public修饰。
interface Inter//定义接口
{
public abstract void show();//抽象方法
public static final int NUM = 3;//全局常量
}
class Test implements Inter//创建Test类实现接口
{
public void show(){};//复写方法,注意权限
}
class InterfaceDemo
{
public static void main(String[] args)
{
Test t = new Test();//将对象实例化
System.out.println(t.NUM);//对象调用
System.out.println(Test.NUM);//NUM被static修饰,可直接使用类名调用
System.out.println(Inter.NUM);//执行main函数后生成三个class文件,其中包括Inter.class,也可使用类名调用(可将interface看作是一个特殊的类)
}
}
类与类之间是继承(extends)关系,因为父类中有非抽象的内容可供子类直接使用;
类与接口之间的关系称为实现(implements),因为接口中的方法都是抽象的,变量都是final修饰的,没有可供"继承"的内容,我们称之为实现;
接口与接口之间是继承的关系,并且接口之间可以多继承。
一个类在继承了另一个类的同时,还可以进行多实现。
接口可以被类多实现,即,一个类可以同时实现多个接口。Java不支持多继承,但是支持多实现。可以看作是Java对多继承不支持的一种转换形式。
注意:由于接口中的方法都是抽象方法,之后方法名,没有方法主题,所以即便在多实现过程中出现了多个同名的抽象方法,也不会相互冲突,所以可以多实现,不可以多继承。
接口的特点:
①接口是对外暴露的规则;
②接口是程序的功能扩展;
③接口的出现降低耦合性;
④接口可以用来多实现;
⑤类与接口之间是实现关系,而且类可以继承一个类的同时实现多个接口;
⑥接口与接口之间可以有继承关系,并且可以多继承。
接口与抽象类的区别:
接口与抽象都是通过对事物的共性内容不断向上抽取出来的抽象概念。他们的区别有:
①抽象类体现的是继承关系,只能被单继承;接口体现的是实现关系,可以被多实现。
②抽象类中定义的是体系中的共性功能,这些功能在子类继承时必须被复写(实现);而接口中定义的是体系中的扩展功能。
③抽象类体现的是“A is a B”的概念;接口体现的是“A like B”的概念。
④抽象类中的成员特点:成员变量可以是变量也可以是常量(final修饰);成员方法可以是抽象方法(abstract修饰,无方法体)也可以是非抽象方法,甚至可以没有抽象方法;抽象类中有构造方法。
接口中的成员特点:成员变量默认被public static final修饰,是一个常量;成员方法默认被public abstract修饰,全都是抽象方法;接口由于不可以被继承,所以没有构造函数
视频外获得的知识:
根据abstract的特点,可以推出:抽象方法一定不可能同时被private修饰