目录标题
1.为什么需要继承?
我们说Java语言对象与对象之间的交互更符合真实的世界,Java使用类来对现实世界中的实体进行描述,但是我们发现,很多事物之间都是有共同点的,比如猫猫和狗狗都有名字,年龄,他们都属于动物类,而猫猫会抓老鼠,狗狗会拆家,这就是他们有区别的地方。
class Cat {
String name;
int age;
public void mew() {
System.out.println("喵喵!");
}
public void eat() {
System.out.println(this.name + "吃猫粮!");
}
}
class Dog {
String name;
int age;
public void bark() {
System.out.println("狗叫!");
}
public void eat() {
System.out.println(this.name + "吃狗粮!");
}
}
我们发现,这里Cat类和Dog类中都有String name,int age, 还有eat()方法,如果还有其他动物类的话,这些代码就会重复定义显得冗杂。那该怎么办呢?
2.extends关键字
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。
在Java中如果要表示类之间的继承关系,需要借助extends
关键字,单看extends字面意思,是扩展延伸的意思,跟我们所说的扩展增加新功能相符合,具体用法如下:
修饰符 class 子类 extends 父类 {
// ...
}
我们来对上面的代码进行改造,抽取共性,写一个Animal类。
class Animal {
String name;
int age;
public void eat() {
System.out.println(this.name + "呲饭饭!");
}
}
Cat类和Dog类都是动物,继承Animal的属性,来重新写一下这两个类。
class Cat extends Animal {
String hair;
public void mew() {
System.out.println("喵喵!");
}
}
class Dog extends Animal {
String breed;
public void bark() {
System.out.println("汪汪!");
}
}
来试试实例化对象。
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
//Dog类和Cat类并没有name变量,这是继承自父类的
dog.name = "七七";
cat.name = "九九";
System.out.println(dog.name);
System.out.println(cat.name);
//自己特有的成员变量
dog.breed = "柴犬";
cat.hair = "灰白色";
System.out.println(dog.breed);
System.out.println(cat.hair);
//调用父类的方法
dog.eat();
cat.eat();
//调用自己特有的方法
dog.bark();
cat.mew();
}
}
运行结果:
注意
:
1.子类会将父类中的成员变量或者成员方法继承到子类中了。
2.被继承的类我们叫做父类/超类/基类,继承者我们称为子类/派生类。
3. 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了。
3.父类成员访问
既然子类继承了父类的字段和方法,那子类能否访问父类的成员呢?
3.1 子类访问父类成员变量
3.1.1成员变量名不同
我们还是用上面Animal和Cat举例子,为了便于大家理解,我们简写一下,来看下面的代码:
class Animal {
String name = "七七";
int age = 2;
}
class Cat extends Animal {
String hair = "黄色";
public void method() {
//访问父类继承的
System.out.println(name);
System.out.println(age);
//访问自己的
System.out.println(hair);
}
}
调用一下method方法看看结果!
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
cat.method();
}
}
3.1.2成员变量名相同时
如果我们在子类中定义了与父类相同的成员变量,当我们在子类中访问时会访问谁的成员呢?
我们马上来测试一下!
class Animal {
String name = "七七";
int age = 2;
}
class Cat extends Animal {
String name = "九九";
String hair = "黄色";
public void method() {
System.out.println(name);
//调用谁的name?
System.out.println(age);
//子类中没有age,会报错吗?
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
cat.method();
}
}
我们看到父类和子类中都有name,但在子类中访问时,访问了子类自己的成员,子类将父类的成员隐藏了,子类没有age,子类访问时访问了父类的age。于是我们得到以下结论:
成员变量访问遵循就近原则
,自己有优先自己的,如果没有则向父类中找。
很容易理解,咱有咱就不麻烦家里人了,没有了再厚着脸皮找家里要吧!
3.2 子类访问父类成员方法
知道了子类是如何访问成员变量的,我们再来试试成员方法的访问,我猜啊,可能差不多嗷!
3.2.1成员方法名不同
说别的没用,上精简代码!
class Animal {
String name;
int age;
public void methodA() {
System.out.println("父类的methodA方法");
}
}
class Cat extends Animal {
String name;
int age;
String hair;
public void methodB() {
System.out.println("子类的methodB的方法");
}
public void method() {
methodA();
methodB();
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
cat.method();
}
}
运行结果如下:
这个调用很简单,没什么好说的,既可以调用自己的,也能调用父类的(private修饰的私有方法当然不行)。
这样想,即使是父类的,虽然咱没有,但咱继承下来了对吧,咱就当在同一个类中用就完事了,老子的东西也是“老子”的!
3.2.2成员方法名相同
成员方法名相同可就有意思了,因为成员方法名相同时,有重载和重写两种。
重载
什么叫方法重载呢?
就是方法名相同,但参数列表不同(包括但不限于类型,顺序,个数),方法返回值不做要求。
方法签名
编译器是怎么区分这些方法的呢? 其实编译器是通过方法签名来判断的,我们用之前说过的javap工具来看看方法签名。点击查看javap工具介绍及其使用
我们在Animal类中写两个重载方法,用javap -s 文件名
来看看。
class Animal {
String name;
int age;
public void methodA() {
System.out.println("父类无参的methodA方法");
}
public void methodA(int a, int b) {
System.out.println("父类带两个int参数的methodA方法");
}
}
methodA()这个方法的方法签名就是 methodA()V
, “V”表示返回值为void
methodA(int a, int b) 方法的签名是 methodA(II)V
, 括号中II表示有两个参数,第一个是int
,第二个是int
,“V” 表示返回值为void
。其他的同理哦。
重载时子类访问父类
上测试代码!
class Animal {
String name;
int age;
public void methodA() {
System.out.println("父类无参的methodA方法");
}
public void methodA(int a, int b) {
System.out.println("父类带两个int参数的methodA方法");
}
}
class Cat extends Animal {
String name;
int age;
String hair;
public void methodA(int a) {
System.out.println("子类的带一个int参数的methodA的方法");
}
public void method() {
methodA();
methodA(1);
methodA(1, 2);
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
cat.method();
}
}
运行结果:
通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法传递的参数选择合适的方法访问,如果没有则报错。
也就是说,编译器根据你所传参数的不同,去匹配调用相应的的方法。
重写
那还有一种情况,子类和父类的方法名,参数列表,返回类型一模一样,只是内部实现不同而已,这就是所谓的重写,总结为一句话就是,外壳相同,核心不同
。后面多态部分再详细介绍,我们先来测试代码。
class Animal {
String name;
int age;
public void eat() {
System.out.println(this.name + "呲饭饭!");
}
}
class Cat extends Animal {
String hair;
public void eat() {
System.out.println(this.name + "吃猫粮!");
}
}
class Dog extends Animal {
String breed;
public void eat() {
System.out.println(this.name + "吃狗粮!");
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
Dog dog = new Dog();
cat.eat();
dog.eat();
}
}
这里我们的eat方法在子类中完成了重写,那么通过子类对象调用eat方法,会执行谁的呢?
看运行结果:
看!他们各自调用了自己的eat方法,并没有执行父类的方法。
总结一下:
父类和子类同名方法的原型一致(重写),则只能访问到子类的,父类的无法通过子类对象直接访问到.
如果我们就是想在子类中访问父类的同名的方法怎么办?
3.3 super关键字
在开发中,难免有父类子类存在相同成员的情况,那我们非要访问父类的成员的时侯该怎么办呢?直接访问肯定做不到,Java提供了super
关键字来访问父类的成员。
class Animal {
String name ="七七";
int age = 2;
public void eat() {
System.out.println(this.name + "呲饭饭!");
}
public void sleep() {
System.out.println(this.name + "睡觉觉!");
}
}
class Cat extends Animal {
String name = "九九";
int age = 4;
String hair;
public void eat() {
System.out.println(this.name + "吃猫粮!");
}
public void method() {
System.out.println(super.name);
//调用父类继承的name,输出七七
System.out.println(this.name);
//调用自己的name,输出九九
System.out.println(super.age);
//调用父类继承的的age,输出2
System.out.println(this.age);
//调用自己的age,输出4
this.eat();
//调用自己的eat 输出九九吃猫粮!
super.eat();
//调用父类的eat 输出七七呲饭饭!
}
}
//运行下method方法
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
cat.method();
}
}
注意,super关键字只能在非静态方法中访问。
详细用法后面介绍
3.4 子类构造方法
在刚刚的代码中,我们并没有为父类和子类写构造方法,所以编译器在编译时已经帮我们添加了无参构造方法。
我们知道,子类继承了父类的成员,子类成员包含父类的和自己的,那么在实例化子类对象时,父类的成员也是需要创建的,而成员的初始化,是通过调用构造方法的。点击查看调用构造方法如何初始化成员?
两个类构成父子关系,那肯定先有父再有子,所以在构造子类对象时候 ,先要调用父类的构造方法,将从父类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整 。
我们来自己写构造方法试试。
class Animal {
String name;
int age;
Animal() {
System.out.println("这是父类构造方法");
}
public void eat() {
System.out.println(this.name + "呲饭饭!");
}
public void sleep() {
System.out.println(this.name + "睡觉觉!");
}
}
class Cat extends Animal {
String hair;
Cat() {
System.out.println("这是子类构造方法 ");
}
public void eat() {
System.out.println(this.name + "吃猫粮!");
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
}
}
看看运行结果:
我们发现,实例化子类对象时,先执行父类构造方法,再执行子类的构造方法。但是,它是怎么调用父类的构造方法的呢?
实际上,如果父类构造方法是默认构造方法(即我们没有自己实现构造方法)或者我们自己写了无参的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用父类构造方法。
实际代码段是这样的:
class Base {
//成员
Base() {
//内容
}
}
class Derived extends Base {
//成员
Derived() {
super();
//内容
}
}
如果我们定义的父类构造方法是含有参数的,那我们必须自己来写出super()并为父类构造方法传参来调用父类构造方法。
如果不写,编译器报错。
因为子类并没有调用父类的构造方法,所以我们必须在子类构造方法中调用父类相应的构造方法。 如下图:
这样写就没有问题了,注意:
super(…)语句必须写在子类构造方法的第一行,且只能出现一次,不能和this同时出现。 否则都会报错的,大家可以自己试一下。
4.super和this的异同点
相同点
- 都是Java中的关键字
- 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
- 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点
- this是当前对象的引用,当前对象即调用实例方法的对象,谁调用this就指向谁,super相当于是子类对象中从父类继承下来部分成员的引用.
- 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
- this是非静态成员方法的一个隐藏参数,
super不是隐藏的参数
- 成员方法中直接访问本类成员时,编译之后会将this还原,即本类非静态成员都是通过this来访问的;在子类中如果通过super访问父类成员,编译之后在字节码层面
super实际是不存在的
(通过字节码文件可以验证)。
- 构造方法中一定会存在super(…)的调用,用户没有写编译器也会增加(带有参数的需要自己写),但是this(…)用户不写则没有。
- 在构造方法中:this(…)用于调用本类构造方法,super(…)用于调用父类构造方法,两种调用不能同时在构造方法中出现。
5.代码块在继承中的执行
没有继承关系时,构造方法和代码块执行顺序是:
静态代码块 -> 实例代码块 -> 构造方法
在继承关系中,执行顺序是怎么样的呢? 我们来写代码测试!万年不变的Animal和Cat
class Animal {
String name;
int age;
{
System.out.println("这是父类中的实例代码块!");
}
static {
System.out.println("这是父类中的静态代码块!");
}
Animal() {
System.out.println("这是父类构造方法");
}
public void eat() {
System.out.println(this.name + "呲饭饭!");
}
public void sleep() {
System.out.println(this.name + "睡觉觉!");
}
}
class Cat extends Animal {
String hair;
{
System.out.println("这是子类中的实例代码块!");
}
static {
System.out.println("这是子类中的静态代码块!");
}
Cat() {
super();
System.out.println("这是子类构造方法 ");
}
public void eat() {
System.out.println(this.name + "吃猫粮!");
}
}
public class Test {
public static void main(String[] args) {
//这里我们实例化两个对象,观察静态代码块的执行情况
Cat cat1 = new Cat();
System.out.println("=====我是分割线=======");
Cat cat2 = new Cat();
}
}
运行结果:
这个代码很清晰的表示出了代码块和构造方法的执行顺序:
总结:
1.静态代码块只执行一次,即在类被加载时加载一次,之后不再执行
2.实例代码块因为被编译器加在了构造方法的前面,所以在构造方法之前执行
3.super()比实例代码块更早执行。
6.protected关键字
再来回顾一下前面说过的这张图
这张图展现了Java中访问修饰限定符的权限从小到大的排序,可以看到,protected权限仅次于public,被protected修饰的类或成员,除了在其他包中的其他类不能访问
外,其他地方都是能访问的。
至于什么时候用哪一种呢?没有硬性要求,我们根据需求尽量隐藏内部细节,只提供必要的信息给类的调用者就可以了。要分清楚我们写代码是站在类的创建者角度还是类的调用者角度,类自己使用还是其他类或者子类使用。
7.继承方式
真实项目中所写的类也会有很多,类之间的关系也会更加复杂。但是即使如此, 我们并不希望类之间的继承层次太复杂,一般我们不希望出现超过三层的继承关系。
如果想从语法上进行限制,就需要用到final关键字。
8.final关键字
final关键可以用来修饰变量、成员方法以及类。
修饰变量
修饰变量或字段,表示常量,不能被修改。
final int a = 1;
a = 2; // 编译出错
修饰类
表示该类不可被继承。
final public class Animal {
...
}
public class Cat extends Animal {
...
}
// 编译出错
例如,我们用的字符串类型,是不能被继承的,按住Ctrl来看下源码。
修饰方法
修饰方法:表示该方法不能被重写。
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{
// 将汽车中包含的:轮胎、发送机、车载系统全部继承下来
}
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般情况下我们建议能使用组合尽量使用组合。
=========================================================
码字不易,点个赞再走吧!收藏不迷路! 持续更新中…