1.1 什么是继承
在我们日常写代码的时候,我们发现我们会遇到两个类里有相同的成员或者方法;那如此以往,会造成写代码效率的下降,并且代码冗余过多。由此,我们引出了我们今天的主角——继承。
比如说:
class Dog {
public String name;
public int age;
public void eat() {
System.out.println(this.name+"在吃饭!");
}
public void bark() {
System.out.println(this.name+"在叫!");
}
}
class Bird {
public String wing;
public String name;
public int age;
public void eat() {
System.out.println(this.name+"在吃饭!");
}
public void bark() {
System.out.println(this.name+"在叫!");
}
public void fly() {
System.out.println(name+"在飞!");
}
}
我们会发现,狗和鸟这两个不同的类,但是他们里面有好多重复的内容。那么我们是不是能否将这些共性抽取呢?面相对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。
1.2 继承概念
继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特 性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。
继承主要解决的问题是:共性的抽取,实现代码复用。
Dog和Bird都继承了Animal类,其中:Animal类称为父类/基类或超类,Dog和Birdt可以称为Animal的子类/派生类,继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可。
1.3 继承的语法
在Java中如果要表示类之间的继承关系,需要借助extends关键字。
修饰符 class 子类 extends 父类 {
// ...
}
我们对开局的代码重新书写--->
class Animal {
public String name;
public int age;
public void eat() {
System.out.println(this.name+"在吃饭!");
}
public void bark() {
System.out.println(this.name+"在叫!");
}
}
class Dog extends Animal{
public void swin(){
System.out.println(this.name+"正在狗刨!");
}
}
class Bird extends Animal{
public String wing;
public void fly() {
System.out.println(name+"在飞!");
}
}
我们写个main函数来验证是否可行;
public class TextDemo {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "大黄";
dog.eat();
dog.bark();
System.out.println("-------------");
Bird bird = new Bird();
bird.name = "鹦鹉";
bird.eat();
bird.fly();
}
}
成功。
注意:
1. 子类会将父类中的成员变量或者成员方法继承到子类中了
2. 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
1.4 父类成员访问
1.4.1 子类中访问父类的成员变量
1. 子类和父类不存在同名成员变量
public class Base {
int a;
int b;
}
public class Derived extends Base{
int c;
public void method(){
a = 10; // 访问从父类中继承下来的a
b = 20; // 访问从父类中继承下来的b
c = 30; // 访问子类自己的c
}
}
2. 子类和父类成员变量同名
class Base {
public int a;
public int b;
}
public class Derived extends Base {
public int a;
public boolean b;
public int c;
public int d;
public void test() {
//首先,在访问时优先访问子类的成员,在访问父类的成员。
a = 10;
//b = 20;//编程报错;因为优先访问子类的成员,错误!
b = true;//访问的是子类的成员,boolean类型,正确!
//但是如果想访问父类的成员;怎么办?
//有一个关键字super
super.b = 20;//利用super关键字,我们可以直接去访问父类成员。
c = 30;
d = 40;
//e = 1;//报错;因为父类子类都没有变量e。
System.out.println(a+" "+b+" "+c+" "+d);
}
public static void main(String[] args) {
Derived derived = new Derived();
derived.test();
}
}
总结:
在子类方法中 或者 通过子类对象访问成员时:
---如果访问的成员变量子类中有,优先访问自己的成员变量。
---如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
---如果访问的成员变量与父类中成员变量同名,则优先访问自己的,即:子类将父类同名成员隐藏了。
成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。
所以,在这里,我们又引出了一个重要的关键字 —— super
【super】
super.date 访问父类的成员变量;
super.func() 访问父类的成员方法;
super(); 调用父类的构造方法。
注意:
public static void testD() {
super.a = 10;//报错。Derived.super' cannot be referenced from a static context
}
Derived.super' cannot be referenced from a static context
【注意事项】
1. 只能在非静态方法中使用
2. 在子类方法中,访问父类的成员变量和方法。
1.4.2 子类中访问父类的成员方法
class Base {
public int a;
public int b;
public void testA() {
System.out.println("Base::testA()!");
}
}
public class Derived extends Base {
public int a;
public boolean b;
public int c;
public int d;
public void testB() {
System.out.println("Drived::testB()!");
}
public void testC() {
testB();//访问自己的test
testA();//访问Base的test
}
public static void main(String[] args) {
Derived derived = new Derived();
derived.testC();
}
}
总结:
成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错。
2. 成员方法名字相同
class Base {
public int a;
public int b;
public void testA() {
System.out.println("Base::testA()!");
}
public void testB() {
System.out.println("Base::testB()!");
}
}
public class Derived extends Base {
public int a;
public boolean b;
public int c;
public int d;
public void testA(int a) {
System.out.println("Derived::testA()!");
}
public void testB() {
System.out.println("Drived::testB()!");
}
public void testC() {
testA(10);//传参了,所以访问的是子类的方法。
testB(); //父类子类都是没有参数的方法,所以优先访问子类的方法。
testA(); //没传参,所以访问的是父类的方法。
}
public static void main(String[] args) {
Derived derived = new Derived();
derived.testC();
}
}
【说明】
1. 通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错。
2. 通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用
3. 方法适传递的参数选择合适的方法访问,如果没有则报错;如果父类和子类同名方法的原型一致(重写-后面讲),则只能访问到子类的,父类的无法通过派生类对象直接访问到。
1.5 子类构造方法
引文:父子父子,先有父再有子,即:子类对象构造时,需要先调用基类构造方法,然后执行子类的构造方法。
class Animal {
public String name;
public int age;
//当有没带参数的构造方法存在时,程序不会报错。
/*public Animal() {
System.out.println("Animal的不带参数的构造方法!");
}*/
//当没带参数的构造方法被提供的构造方法代替后,编译器不会再默认生成不带参数的方法。
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal的带参数的构造方法!");
}
}
class Dog extends Animal{
public Dog() {
super("11",2);
}
}
class Bird extends Animal{
public String wing;
public Bird(String name, int age) {
super(name, age);
}
}
当子类继承了父类之后,要帮助父类进行构造
如何帮助父类进行构造呢? 显示的调用父类的构造方法,帮助父类进行 构造 ,此时 构造 的意思就是,初始化继承过来的父类的成员。
在子类构造方法中,并没有写任何关于基类构造的代码,但是在构造子类对象时,先执行基类的构造方法,然后执行子类的构造方法,因为:在构造子类对象时,先要将从父类继承下来的成员初始化完整,然后再初始化子类自己新增加的成员。
注意:
1. 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法;
2. 如果父类构造方法是带有参数的,此时编译器不会再给子类生成默认的构造方法,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败;
3. 在子类构造方法中,super(...)调用父类构造时,必须是子类构造函数中第一条语句;
4. super(...)只能在子类构造方法中出现一次,并且不能和this同时出现。
1.6 super和this
super和this都可以在成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句。
那他们之间有什么区别呢?
【相同点】
1. 都是Java中的关键字;
2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段;
3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在。
【不同点】
1. this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成的引用;
2. 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性;
3. this是非静态成员方法的一个隐藏参数,super不是隐藏的参数;
4. 成员方法中直接访问本类成员时,编译之后会将this还原,即本类非静态成员都是通过this来访问的;在子类中如果通过super访问父类成员,编译之后在字节码层面super实际是不存在的(通过字节码文件可以验证)员;
5. 在构造方法中:this(...)用于调用本类构造方法,super(...)用于调用父类构造方法,两种调用不能同时在构造方法中出现;
6. 构造方法中一定会存在super(...)的调用,用户没有写编译器也会增加,但是this(...)用户不写则没有。
1.7 再谈初始化
我们可以通过自己书写代码,运行自己进行感受。
class Animal {
public String name;
public int age;
/*public Animal() {
System.out.println("Animal的不带参数的构造方法!");
}*/
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal的带两个参数的构造方法!");
}
}
class Dog extends Animal{
public Dog() {
super("11",2);
System.out.println("Dog不带参数的构造方法!");
}
public void swin(){
System.out.println(this.name+"正在狗刨!"+"年龄:"+age);
}
}
class Bird extends Animal{
public String wing;
public Bird(String name, int age) {
super(name, age);
System.out.println("Bird的带两个参数的构造方法!");
}
}
public class TextDemo {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println("=========");
Bird bird = new Bird("七七",23);
}
class Animal {
public String name;
public int age;
static {
System.out.println("Animal的静态代码块");
}
{
System.out.println("Aniaml的实例代码块");
}
/*public Animal() {
System.out.println("Animal的不带参数的构造方法!");
}*/
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal的带两个参数的构造方法!");
}
}
class Dog extends Animal{
public Dog() {
super("11",2);
System.out.println("Dog不带参数的构造方法!");
}
public void swin(){
System.out.println(this.name+"正在狗刨!"+"年龄:"+age);
}
static {
System.out.println("Dog的静态代码块");
}
{
System.out.println("Dog的实例代码块");
}
}
public class TextDemo {
public static void main(String[] args) {
Dog dog = new Dog();
}
}
class Animal {
public String name;
public int age;
static {
System.out.println("Animal的静态代码块");
}
{
System.out.println("Aniaml的实例代码块");
}
/*public Animal() {
System.out.println("Animal的不带参数的构造方法!");
}*/
public Animal(String name,int age) {
this.name = name;
this.age = age;
System.out.println("Animal的带两个参数的构造方法!");
}
}
class Dog extends Animal{
public Dog() {
super("11",2);
System.out.println("Dog不带参数的构造方法!");
}
public void swin(){
System.out.println(this.name+"正在狗刨!"+"年龄:"+age);
}
static {
System.out.println("Dog的静态代码块");
}
{
System.out.println("Dog的实例代码块");
}
}
public class TextDemo {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println("========");
Dog dog1 = new Dog();
}
}
1. 静态代码块先执行,并且只执行一次,在类加载阶段执行
2. 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行
通过分析执行结果,得出以下结论:
1、父类静态代码块优先于子类静态代码块执行,且是最早执行
2、父类实例代码块和父类构造方法紧接着执行
3、子类的实例代码块和子类构造方法紧接着再执行
4、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
1.8 protected 关键字
在类和对象章节中,为了实现封装特性,Java中引入了访问限定符,主要限定:类或者类中成员能否在类外或者其他包中被访问。
证明:
//同一个包的同一个类
package Demo1;
public class Test1 {
public int a = 10;
protected int b =20;
public static void main(String[] args) {
Test1 test1 =new Test1();
System.out.println("同一个包的同一个类"+test1.b);
}
}
//同一个包的不同类
package Demo1;
public class Test2 {
public static void main(String[] args) {
Test1 test1 =new Test1();
System.out.println("同一个包的不同类"+test1.b);
}
}
//不同包中子类 和 不同一个包的非子类
package Demo2;
import Demo1.Test1;
public class Test3 extends Test1{
public void func() {
System.out.println("不同包中子类"+super.b);
}
public static void main(String[] args) {
Test3 test3 =new Test3();
test3.func();
//System.out.println("不同一个包的不同类"+test1.b);//报错
}
}
总结:
什么时候用哪个访问修饰限定符,没有明确规定。一般粗暴点:字段都是私有的,方法都是公开的。但是,一定要根据情况而定。
1.9 继承方式
Java中只支持以下几种继承方式:
注意:Java中不支持多继承。
=>时刻牢记, 我们写的类是现实事物的抽象. 而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到一系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多. 类之间的关系也会更加复杂.
=>但是即使如此, 我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了.
=>如果想从语法上进行限制继承, 就可以使用 final 关键字
1.10 final 关键字
final关键可以用来修饰变量、成员方法以及类。
1. 修饰变量或字段,表示常量(即不能修改)
final int a = 10;
a = 20; // 编译出错
2. 修饰类:表示此类不能被继承
final public class Animal {
...
}
public class Bird extends Animal {
...
}
// 编译出错
Error:(3, 27) java: 无法从最终com.bit.Animal进行继
我们平时是用的 String 字符串类, 就是用 final 修饰的, 不能被继承.
3. 修饰方法:表示该方法不能被重写(后序介绍)
1.11 继承与组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。
继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物
组合表示对象之间是has-a的关系,比如:汽车
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合。
end