1.继承
1.1Java中为什么有继承的概念
Java用继承将现实世界中多种多样的实体进行共性抽取,然后让实体继承共性,从而简化代码.比如,我们将猫和狗抽取出他们的共性—动物.
java中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实中的实体,但是现实世界错综复杂,事物之间可能会存在一些关联,那在设计程序是就需要考虑。
我们用下面的代码描述狗和猫.
从上述代码可以看出,狗类和猫类存在一段重复的代码,这就是他们的共性,我们对这些重复的代码抽取共性,然后定义一个新的动物类.如下图:
对描述狗和猫的代码使用继承方式重新设计:
在Java中如果要表示类之间的继承关系,需要借助extends关键字
注意:子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
以上过程便是面向对象思想中提出的继承的概念,专门用来进行共性抽取,实现代码复用.
1.2 继承的概念
继承(inheritance)机制:面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类的特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。
例如:狗和猫都是动物,那么我们就可以将共性的内容进行抽取,然后采用继承的思想来达到共用。
上述图示中,Dog和Cat都继承了Animal类,其中:Animal类称为父类/基类/超类,Dog和Cat可以称为Animal的子类/派生类,继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可。从继承概念中可以看出继承最大的作用就是:实现代码复用,还有就是来实现多态。
1.3 继承的语法
在Java中如果要表示类之间的继承关系,需要借助extends关键字,具体如下:
修饰符 class 子类 extends 父类 {
…
}
注意:
- 子类会将父类中的成员变量或者成员方法继承到子类中了
- 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
1.4 父类成员访问
子类将父类中的方法和变量继承下来了,子类中能直接访问父类中继承下来的成员.
1.4.1 子类中访问父类的成员变量
自我优先原则:
- 访问的成员变量子类中有,优先访问自己的成员变量。
- 访问的成员变量子类中无,访问父类继承下来的,如果父类也没有定义,编译报错。
- 访问的成员变量与父类中成员变量同(不考虑数据类型),优先访问自己的
子类和父类不存在同名成员变量
子类和父类存在同名成员变量.
同名时,非要访问父类成员变量,用super,可强制访问父类成员变量.
1.4.2 子类中访问父类的成员方法
成员方法名字不同:成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错。
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()
// methodD(); // 编译失败,在整个继承体系中没有发现方法methodD()
}
}
123456789101112131415
成员方法名字相同:子类对象访问父类与子类同名方法时,如果父类和子类同名方法但参数列表不同(重载),根据调用方法视传递的参数选择合适的方法访问,如果没有则报错
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(),父类的无法访问
12345678910111213141516171819
由此可见,方法重载可以不在同一个类中.
//此处应有代码示范的图片(super构造方法)
1.5 super关键字
由于设计不好,或者因场景需要,子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,直接访问是无法做到的,Java提供了super关键字,该关键字主要作用是:在子类方法中访问父类的成员.
有些书上说: super代表父类对象的引用这句话是错误的! !super其实就是一个非常简单的关键字,增加了代码的可读性!
super使用举例如下:
public class Father {
int a;
int b;
public void methodA(){
System.out.println("Father中的methodA()");
}
public void methodB(){
System.out.println("Father中的methodB()");
}
}
public class Father extends Son{
int a; // 与父类中成员变量同名且类型相同
char b; // 与父类中成员变量同名但类型不同
// 与父类中methodA()构成重载
public void methodA(int a) {
System.out.println("Son中的method()方法");
} // 与父类中methodB()构成重写(即原型一致,重写后序详细介绍)
public void methodB(){
System.out.println("Son中的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(); // 直接访问,则永远访问到的都是子类中的methodB(),父类的无法访问到
super.methodB(); // 访问父类的methodB()
}
}
public class Father {
public Father(){
System.out.println("Father()");
}
1234567891011121314151617181920212223242526272829303132333435363738394041
super有三种使用场景:
- super.成员变量
- super.方法名()
- super()调用父类构造方方法
使用super时注意:
- super只能在非静态方法中使用
- super只能在子类方法中访问父类的成员变量和方法
- super() 调用父类构造方法必须在子类中使用才能调用父类的方法和属性
- super() 帮助子类调用父类构造方法初始化成员变量
- super() 当父类有构造方法时,必须先帮父类完成构造方法, 子类才能完成构造方法,否则编译报错。
- this () 和 super () 不能在同一个方法中,因为这两条语句都必须放在代码的第一行
面试问题:this和super的区别是什么?
1.6 super和this的区别
相同点:
- super和this都可以在成员方法中用来访问:成员变量和调用其他的成员函数
- 都是Java中的关键字
- 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
- 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点
- this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用
- 在非静态成员方法中:this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
- 在构造方法中:this(…)用于调用本类构造方法,super(…)用于调用父类构造方法,两种调用不能同时在构造方法中出现
- 构造方法中一定会存在super(…)的调用,用户没有写编译器也会增加,但是this(…)用户不写则没有
1.7 静态代码块和实例代码块在有继承关系时的执行顺序
我们先用一段代码演示一下在没有继承关系时的执行顺序:
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("实例代码块执行");
} s
tatic {
System.out.println("静态代码块执行");
}
}
public class TestDemo {
public static void main(String[] args) {
Person person1 = new Person("bit",10);
System.out.println("============================");
Person person2 = new Person("gaobo",20);
}
}
//执行结果:
//静态代码块执行
//实例代码块执行
//构造方法执行
//============================
//实例代码块执行
//构造方法执行
1234567891011121314151617181920212223242526272829
我们可以得出以下结论:
- 静态代码块先执行,并且只执行一次,在类加载阶段执行
- 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行
我们在来看在继承关系上的执行
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:实例代码块执行");
} s
tatic {
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:实例代码块执行");
} s
tatic {
System.out.println("Student:静态代码块执行");
}
}
public class TestDemo4 {
public static void main(String[] args) {
Student student1 = new Student("张三",19);
System.out.println("===========================");
Student student2 = new Student("gaobo",20);
}
public static void main1(String[] args) {
Person person1 = new Person("bit",10);
System.out.println("============================");
Person person2 = new Person("gaobo",20);
}
}
//
执行结果:
Person:静态代码块执行
Student:静态代码块执行
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行
===========================
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
得出以下结论:
- 父类静态代码块优先于子类静态代码块执行,且是最早执行
- 父类实例代码块和父类构造方法紧接着执行
- 子类的实例代码块和子类构造方法紧接着再执行
- 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
1.8 protect
为了实现封装特性,Java中引入了访问限定符,主要限定:类或者类中成员能否在类外或者其他包中被访问。在程序设计时,类要尽量做到 “封装”,即隐藏内部实现细节, 只暴露出必要的信息给类的调用者.因此我们在使用的时候应该尽可能的使用比较严格的访问权限. 例如如果一个方法能用private, 就尽量不要用 public. 还有一种 简单粗暴 的做法: 将所有的字段设为private, 将所有的方法设为public. 不过这种方式属于是对访问权限的滥用,不建议这样使用.
默认就是不加任何访问修饰限定符,也可以叫做default.
这里我们重点讲protected在不同位置中的使用.
同一个类:
同一个包中的类:
- 同一个包中的子类
- 同一个包中的非子类
不同包的子类:
注意:不同包中的子类要通过super访问
不同包且不是子类:
那我们什么时候用private什么时候用public呢?
在考虑用哪一个限定符时,我们应该考虑该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用).
1.9 继承方式
在现实生活中事物的继承关系可能非常复杂且灵活,那么我们就需要对不同的继承关系分类.
我们举例写的类是现实事物的抽象. 而我们真正开发所遇到的项目往往业务比较复杂, 可能会涉及到一系列复杂的概念, 都需要我们使用代码来表示, 所以真实项目中所写的类也会有很多. 类之间的关系也会更加复杂.即使如此, 我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了.
如果想从语法上进行限制继承, 就可以使用 final 关键字
JAVA中的继承方式有以下几种:
注意:Java中不支持下面这种多继承方式,C++中支持.
1.10 final 关键字
上面我们提到可以用final关键字限制继承,下面我们来详细了解final.
- 修饰变量或字段,表示常量(即不能修改)
final int a = 10;
a = 20; // 编译出错
12
定义的同时可以不初始化,但是只能初始化一次.
final int a;
a = 100;
System.out.println(a);
123
- 修饰类:表示此类不能被继承
final public class Animal {
...
}
public class Bird extends Animal {
...
} /
/ 编译出错
Error:(3, 27) java: 无法从最终com.bit.Animal进行继
12345678
我们查看String字符串类可以看到,String字符串类是用final修饰的,不能被继承.
- 修饰方法,表示该方法不能被重写
2. 组合
2.1 组合的概念
Java是一个面向对象的语言,每一个学习过Java的人都知道,封装、继承、多态是面向对象的三个特征。但组合并不是一种特性,它只是一种实现手段.组合也是一种表达类之间关系的方式, 也是能够达到代码复用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。
组合(Composition)体现的是整体与部分、拥有的关系,即has-a的关系.我们可以用xx是xx的一部分这样的语句描述组合中体现的关系.
继承(Inheritance)是一种联结类与类的层次模型,指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系,是一种is-a关系。
例如,我们用组合写一个汽车的代码表现这种关系,如下:
// 轮胎类
class Tire{
// ...
} /
/ 发动机类
class Engine{
// ...
} /
/ 车载系统类
class VehicleSystem{
// ...
}
class Car{
private Tire tire; // 可以复用轮胎中的属性和方法
private Engine engine; // 可以复用发动机中的属性和方法
private VehicleSystem vs; // 可以复用车载系统中的属性和方法
// ...
} /
/ 奔驰是汽车
class Benz extend Car{
// 将汽车中包含的:轮胎、发送机、车载系统全部继承下来
}
12345678910111213141516171819202122
2.2 组合和继承的区别
下面我们从以下几个方面区别组合和继承:
- 表示关系上:
继承表示对象之间是is-a的关系(可以用xx是xx描述),比如:狗是动物,猫是动物
组合表示对象之间是has-a的关系(可以用xx是xx的一部分描述),比如:零件是汽车的一部分
- 代码复用上:
继承:父类的内部细节对于子类是可见的,所以我们通常也可以说通过继承的代码复用是一种白盒式代码复用。(如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性;)
组合:通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是黑盒式代码复用。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)
- 类的关系上:
继承:在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。)
组合:在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。
建议在同样可行的情况下,优先使用组合而不是继承。因为组合更安全,更简单,更灵活,更高效,长期大量的使用继承会给代码带来很高的维护成本。