一.继承
1.1 为什么需要继承
Java中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实中的实体,但是 现实世界错综复杂,事物之间可能会存在一些关联,那在设计程序是就需要考虑
比如,狗和狼,它们都是动物
如果使用Java语言来描述
class Dog {
//成员属性
String name;
String color;
int age;
//成员方法
public void eat() {
System. out.println("汪汪汪~"+name+"在吃狗粮!");
}
public void play() {
System. out.println(name+"在玩耍!");
}
}
class Wolf {
成员属性
String name;
String color;
int age;
//成员方法
public void eat() {
System. out.println("嗷呜~"+name+"在吃肉!");
}
public void play() {
System. out.println(name+"在玩耍!");
}
}
可以很清晰的看到,狗和狼都有color,name,age属性,都有play行为,能不能只定义一份成员,让狗和狼共同来使用呢?
1.2 继承的概念
继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行 扩展,增加新功能,这样产生新的类,称 派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。
继承主要解决的问题是: 共性的抽取,实现代码复用
例如,狗和狼都是动物,就可以定义一个动物类,让狗和狼去继承它
从上图中可以知道,狗和狼都继承了动物的成员,我们把被继承的Animal类称为父类/超类/基类,把继承的Dog和Wolf称为子类/派生类,继承之后,子类可以使用父类的成员变量或方法,还可以增添自己的成员
那么怎么让编译器知道狗和狼是动物的继承呢?
1.3 继承的语法
Java中表示继承关系需要用到extends关键字
修饰符 class 子类 extends 父类 {
// ...
}
现在重新设计一下狗类和狼类
public class Animal {
String name;
String color;
int age;
public void play() {
System. out.println(name+"在玩耍!");
}
}
class Dog extends Animal{
public void eat() {
System. out.println("汪汪汪~"+name+"在吃狗粮!");
}
}
class Wolf extends Animal{
public void eat() {
System. out.println("嗷呜~"+name+"在吃肉!");
}
}
通过继承,子类就可以拥有父类包含的成员
class test {
public static void main(String[] args) {
Dog dog=new Dog();
dog.name="旺财";//给从父类继承下来的成员变量赋值
dog.play();//调用从父类继承下来的方法
Wolf wolf=new Wolf();
wolf.name="灰太狼";
wolf.eat();//调用子类自己的方法
}
}
1.4 子类成员的访问
分为两种情况:
1.父类成员和子类不重名
用到哪个成员的名字就访问哪个成员
class test {
public static void main(String[] args) {
Dog dog=new Dog();
dog.name="旺财";
dog.play();//play是父类的成员方法
dog.eat();//eat是子类自己的方法
}
}
//输出结果
旺财在玩耍!
汪汪汪~旺财在吃狗粮!
2.父类成员方法和子类重名
这种情况稍微复杂一点,又分为方法重载和重写
1.重载:
在同一个类中,有参数顺序/类型/个数不同的同名方法
在子类中,有和父类参数顺序/类型/个数不同的同名方法
public class Animal {
String name;
String color;
int age;
public void play() {
System. out.println(name+"在玩耍!");
}
}
class Dog extends Animal{
public void eat() {
System. out.println("汪汪汪~"+name+"在吃狗粮!");
}
//和Animal类中的play方法构成重载
public void play(String player) {
System. out.println("汪汪汪~"+"在和"+player+"玩耍");
}
}
构成重载的情况下,编译器根据参数列表的不同来决定调用哪个方法
class test {
public static void main(String[] args) {
Dog dog=new Dog();
dog.name="旺财";
dog.play();//调用无参的play
dog.play("阿福");//调用带一个参数的play
}
}
2.重写
与重载不同,重写要求子类方法和父类的参数列表完全一致,返回类型一致或构成继承关系(后文多态会再详述)
public class Animal {
String name;
String color;
int age;
public void play() {
System. out.println(name + "在玩耍!");
}
}
class Dog extends Animal{
public void eat() {
System. out.println("汪汪汪~"+name+"在吃狗粮!");
}
//和父类的play构成重写
public void play() {
System. out.println("汪汪汪~"+"玩皮球!");
}
}
子类对象在调用时会优先调用子类自己的方法
class test {
public static void main(String[] args) {
Dog dog=new Dog();
dog.name="旺财";
dog.play();
}
}
//输出
汪汪汪~玩皮球!
3.父类成员变量和子类重名
这种情况下子类会优先访问自己的成员变量
class A {
int a;
int b=10;//给A的成员就地初始化
}
class B extends A {
int b=108;//给B的成员就地初始化
int c;
public static void main(String[] args) {
B b=new B();
System. out.println(b.b);
}
}
上面的代码用图解就是这样的
根据子类优先原则,b.b的输出结果是108
总结:
1.如果访问的成员变量子类中有,优先访问自己的成员变量。
2.如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
3.如果访问的成员变量与父类中成员变量同名,则优先访问自己的
1.5 父类成员的访问
我们已经知道,无论是成员方法,还是成员变量,子类都会优先访问自己的,自己没有才会去找父类,那么该怎么指定访问父类的成员呢?
1.5.1 super关键字
super关键字主要作用:在子类方法中访问父类的成员
public class Animal {
String name;
String color;
int age;
public void play() {
System. out.println(name + "在玩耍!");
}
}
class Dog extends Animal{
public void eat() {
System. out.println("汪汪汪~"+name+"在吃狗粮!");
}
public void play() {
System. out.println("汪汪汪~"+"玩皮球!");
}
public void func() {
this.name="旺财";
super.play();//调用父类的play
this.play();//调用子类的play
}
}
//测试类,用于实现main方法
class test {
public static void main(String[] args) {
Dog dog=new Dog();
dog.func();
}
}
//输出结果
旺财在玩耍!
汪汪汪~玩皮球!
super的使用有限制条件:
1.只能在非静态方法中使用
2.只能在子类当中使用,用来访问父类的成员
class A {
int a;
int b=10;
}
class B extends A {
int b=108;
int c;
void func() {
System. out.println(super.b);
System. out.println(super.c);//报错,父类中没有c这个成员变量
}
}
class test {
public static void main(String[] args) {
B b=new B();
b.func();//输出10
}
}
注意:
super只能代表子类对象中父类的那一部分,而并非父类对象的引用(因为根本就没有创建父类对象),使用super只是为了代码便于读懂
1.5.2 父类的构造
来看下面一段代码有没有问题
public class Animal {
String name;
String color;
int age;
//定义带三个参数的构造函数
Animal(String name,String color,int age) {
this.name=name;
this.color=color;
this.age=age;
}
}
class Wolf extends Animal{
public void eat() {
System. out.println("嗷呜~"+name+"在吃肉!");
}
}
看起来没毛病对不对?
实际上这段代码是不能通过编译的。设想一下,如果定义了一个Wolf对象,这个对象继承了Animal的name,color,age成员,那么Wolf对象必须要调用父类的构造方法对父类的成员初始化。在这种情况下该怎么调用父类的构造方法呢?
实际上,super关键字不仅可以用来访问父类的成员,还可以用来构造父类的成员变量
class Wolf extends Animal{
Wolf(String name,String color,int age) {
super(name,color,age);//在子类的构造方法中调用父类的构造
}
public void eat() {
System. out.println("嗷呜~"+name+"在吃肉!");
}
}
注意:
1.构造子类对象必须先完成父类成员的构造
2.super(参数列表)必须放在子类构造方法的第一行
看到这里你可能会注意到一个问题
public class Animal {
String name;
String color;
int age;
public void play() {
System. out.println(name + "在玩耍!");
}
}
class Wolf extends Animal{
public void eat() {
System. out.println("嗷呜~"+name+"在吃肉!");
}
}
对于Animal类,编译器会默认生成一份不带参数的构造方法,对于Wolf类,编译器也会生成一份不带参数的构造方法,这两份构造方法实际上啥也没干,但是构造Wolf对象时必须要用super()构造Animal类的成员,所以上面这段代码会报错吗?
答案是不会
如果父类显示定义无参的构造方法或者编译器默认生成,编译器会把super()添到子类的构造方法中
上面那段代码在编译器处理后是这样的
public class Animal {
String name;
String color;
int age;
//定义默认构造
public Animal() {
}
}
class Wolf extends Animal{
//定义子类默认构造
public Wolf() {
super();//会默认调用父类的无参构造
}
}
1.5.3 对比:super和this
super | this | |
概念 | 用来指代子类对象中属于父类的那一部分 | 当前对象的引用 |
使用范围 | 子类的非静态方法 | 非静态方法 |
使用格式 | super.父类成员变量 super.父类成员方法 super(参数列表)调用父类的构造 | this.成员变量 this.成员方法 this() 调用其他构造方法 |
访问范围 | 只能用来访问父类成员 | 访问成员 |
特殊限制 | super()只能写在子类构造方法的第一行 | this()只能写在构造方法的第一行 |
注意:
因为super()和this()语句都只能出现在构造方法的第一行,所以在同一个构造方法中它们不能同时出现
子类的构造方法一定会有super()的调用
1.6再谈初始化
来看下面一段代码
public class Animal {
String name;
String color;
int age;
static {
System.out.println("Animal类静态代码块被执行");
}
{
System.out.println("Animal类实例代码块被执行");
}
public Animal() {
System.out.println("Animal类构造方法被执行");
}
}
class Dog extends Animal{
static {
System.out.println("Dog类静态代码块被执行");
}
{
System.out.println("Dog类实例代码块被执行");
}
public Dog() {
System.out.println("Dog类构造方法被执行");
}
}
//定义测试类
class test {
public static void main(String[] args) {
Dog dog=new Dog();
}
}
来看看执行结果
由此可以总结出代码块的执行顺序:
静态父类代码块--》静态子类代码块--》父类实例代码块--》父类构造方法--》子类实例代码块--》子类构造方法
也就是说,父类成员的构造肯定要先于子类成员的构造
如果再定义一个Wolf派生类并创建Wolf对象
class Wolf extends Animal{
static {
System.out.println("Wolf类静态代码块被执行");
}
{
System.out.println("Wolf类实例代码块被执行");
}
public Wolf() {
System.out.println("Wolf类构造方法被执行");
}
}
class test {
public static void main(String[] args) {
Dog dog=new Dog();
System.out.println("-------------------------");
new Wolf();
}
}
来看执行结果
可以看到,静态代码块只会执行一次,Animal静态代码块并没有被再次执行
1.7 protected关键字
在类和对象那篇博文中已经讲解了其他三种访问权限,现在来看看与继承有关的protected
protected限定的成员访问权限是
同一个包中的同一类,同一个包中的不同类,不同包中的子类
前两种由于和默认访问权限一样,此处不再讨论,详情请看http://t.csdn.cn/nw0gu
来看看第三种权限的使用方式
package base;//Base类定义在base包下
public class Base {
protected int number1=1;
protected String number2="Missing";
protected static String number4="Fighting";
}
package derived;//Derived类和Test类定义在derived包下
import base.Base;
class Derived extends Base {
double number3;
public static void main(String[] args) {
Base bs=new Base();
System.out.println(bs.number2);//报错,不能在子类中直接通过父类访问protected非静态成员
System.out.println("通过父类对象访问"+bs.number4);//输出Fighting
System.out.println("通过父类访问"+Base.number4);//输出Fighting
System.out.println("通过子类访问"+Derived.number4);//输出Fighting
}
}
输出结果
如果不在一个包内,在子类中访问父类的protected非静态成员时,需要借助子类对象来访问
如果在子类中访问protected的静态成员,通过父类或子类(对象)都可以访问
正确访问方式
class Derived extends Base {
double number3;
public static void main(String[] args) {
Derived dv=new Derived();//创建子类对象
System. out.println(dv.number2);//通过子类对象访问
}
}
1.8 继承方式
1.8.1 继承方式划分
在现实生活中,事物之间的关系是非常复杂,灵活多样,比如
但Java中只存在以下几种继承方式:
单继承 |
| class A {...} class B extends A {...} |
多层继承 |
| class A { ... } class B extends A { ... } class C extends B { ... } |
不同类继承同一个类 |
| class A { ... } class B extends A { ... } class C extends A { ... } |
实际上还有一种继承方式:一个类继承多个类
有了这种继承方式,就会出现菱形继承的情况(C++存在),但Java不允许这么做
注意:对于多层继承,一般不超过三层的继承关系
1.8.2 final关键字
在某些情况下,我们并不希望自己的类被别人继承,可以从语法层面上限制,这就要用到final关键字
final修饰变量或字段
被final修饰的变量或字段只能赋值一次,不能进行更改
final修饰局部变量
class test {
public static void main(String[] args) {
final int a=10;
a=100;//error
}
}
final修饰字段时,必须对该字段进行构造初始化(就地初始化,实例代码块或构造方法均可)
class test {
final int b=10;
public static void main(String[] args) {
test t=new test();
System. out.println(t.b);
}
}
实际上,final相当于C语言中的const,但是有一点不同
public static void main(String[] args) {
final int a;
a=10;//final可以先定义再赋值
}
int main
{
const int a=10;//const必须定义时初始化
}
final修饰类
被final修饰的类称为密封类,不能被继承
inal class A {
}
class B extends A {
}
会报错
final修饰成员方法
被final修饰的方法称为密封方法,表示该方法不能被重写(下文会详述)
1.9 is和has
is和has的意思不必赘述,但是在Java语法中,它们分别代表了继承和组合这两派之争
什么是is
毫无疑问,三张图片里的都是房子,这就需要用继承来抽取它们的共性,至于它们的不同...可以给自己类体内增添细节
什么是has
一个房子里有水晶灯,沙发,电视...这就要用到组合,把这些成员放到类体里面
总结:
is和has都可以实现代码的复用,至于如何使用是根据对象之间的关系决定的
但是尽量使用组合,这样不破坏封装
二.多态
2.1 多态的概念
多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
比如不同的人处理同一张照片😆
在了解多态前,需要先知道下面几个条件
2.2 重写
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。 即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
重写的规则:
子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
被重写的方法返回值类型可以不同,但是必须是具有父子关系的
访问权限不能比父类中被重写的方法的访问权限更小
父类被static、private、final修饰的方法、构造方法都不能被重写。
重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验.
public class Animal {
String name;
String color;
int age;
public void eat() {
System.out.println(name+"在吃饭");
}
}
class Dog extends Animal{
//eat方法构成重写
@Override
public void eat() {
System.out.println("汪汪汪~"+name+"在吃狗粮!");
}
}
子类重写方法的返回值类型必须和父类一样或者与父类返回类型构成继承关系
class Base2 {
}
class Derived2 extends Base2 {
}
class Base1 {
public Base2 func1() {
return null;
}
}
class Derived1 extends Base1 {
public Derived2 func1() {
return null;
}
}
idea也提供了函数重写的快捷键
使用@override表示告诉编译器这个方法要进行重写,它可以帮助我们检查方法名,参数列表,返回值是否符合重写规则
public class Animal {
String name;
String color;
int age;
public void eat() {
System.out.println(name+"在吃饭");
}
}
class Wolf extends Animal{
@Override
public Wolf eat() { //会报错,返回值类型不符合重写规则
System.out.println("嗷呜~"+name+"在吃肉!");
return null;
}
}
2.3 向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用
向上转型有三种使用方式
1.直接赋值
直接将子类对象传给父类引用
语法格式
父类类型 对象名 = new 子类类型()
或者
父类类型 对象名;
对象名=new 子类类型();
来看下面一个例子
public class Animal {
String name;
String color;
int age=10;
public void eat() {
System. out.println(name+"在吃饭");
}
}
class Dog extends Animal{
int age=18;
@Override
public void eat() {
System. out.println("汪汪汪~"+name+"在吃狗粮!");
}
public void play() {
System. out.println("汪汪汪~"+"玩皮球!");
}
}
Dog类继承了Animal类,并且定义了自己的方法play,Animal类和Dog类有一个同名变量age
class test {
public static void main(String[] args) {
Animal animal=new Dog();
animal.name="灰灰";
System. out.println(animal.age);
//animal.play();//error,父类引用不能访问子类方法
animal.eat();
}
}
输出结果
上面的代码中可以看到,当把一个子类对象传给父类引用时,这个父类引用不能调用子类的成员方法,也不能访问子类的成员变量,也就是说,父类引用只能看到属于父类的成员
但是,如果父类引用调用了被重写的方法,使用哪个方法由对象的类型决定
再来一个Wolf类重写eat方法
class Wolf extends Animal{
@Override
public void eat() {
System. out.println("嗷呜~"+name+"在吃肉!");
}
}
class test {
public static void main(String[] args) {
//把dog类对象赋给animal引用变量
Animal animal=new Dog();
animal.name="灰灰";
//把wolf对象赋给animal2引用变量
Animal animal2=new Wolf();
animal2.name="红太狼";
animal.eat();
animal2.eat();
}
}
可以得到如下输出
2.传参数
比如定义一个方法,参数类型是父类
void Func(Animal animal) {
animal.eat();
}
如果传入的是Dog对象,会调用Dog的eat方法,如果传入的是Wolf对象,会调用Wolf的eat方法
从这里也可以理解对象打印那里重写toString的语法了
实际上,Object类可以理解为是所有类的祖先,因此,如果我们自定义了一个类,并且在这个类中完成了toString的重写,当调用println方法时传入的对象是我们自己定义的类型,也就会调用我们自己重写的方法
3.传返回值
Animal func() {
...
return new Dog();
}
如果使用Animal引用变量接收func的返回值,也会发生向上转型
2.4 多态的实现
实现多态的条件
1.父类引用指向子类对象
2.父类引用调用子类重写的方法
来看代码示例
static void Func(Animal animal) {
animal.eat();
}
public static void main(String[] args) {
Dog dog=new Dog();
dog.name="旺财";
Func(dog);
Wolf wolf=new Wolf();
wolf.name="灰太狼";
Func(wolf);
}
传入的对象不同,虽然Func方法接收的是同一类型的参数,但产生的结果不同,这就是多态
动态绑定和静态绑定
动态绑定:也称为后期绑定(晚绑定,运行时绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
来看看Func经过编译后的结果
Func在编译的时候并不知道会传给自己什么类型的对象,直到程序运行的时候才确定传入的是“旺财”还是“灰太狼”。
动态绑定也是多态的体现
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
例如
class test {
public static int add(int a,int b) {
return a+b;
}
public static double add(double a,double b) {
return a+b;
}
public static void main(String[] args) {
add(1,2);
add(2.0,5.0);
}
}
add方法构成重载,但是在编译的时候编译器已经确定了要调用哪个方法
2.5 向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可
编译器认为这是从大范围到小范围(Animal-->Dog)的转换,是不安全的,因此需要强制类型转换
public class Animal {
String name;
String color;
int age;
}
class Dog extends Animal{
public void play() {
System.out.println("汪汪汪~"+"玩皮球!");
}
}
class test {
public static void main(String[] args) {
//向上转型
Animal animal=new Dog();
//向下转型
Dog dog=(Dog)animal;
//可以调用子类方法
dog.play();
}
}
//输出结果
汪汪汪~玩皮球!
如果把Animal引用转换成Wolf类呢?能不能直接调用Wolf的方法呢?
class Wolf extends Animal{
public void hunt() {
System.out.println("嗷呜~"+"在训练");
}
}
class test {
public static void main(String[] args) {
Animal animal=new Dog();
Wolf wolf=(Wolf)animal;
Wolf.hunt();
}
}
是不可以的
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。
Java中为了提高向下转型的安全性,引入了 instanceof ,表示如果前面的引用变量是后面类型的实例,就可以安全转换
class test {
public static void main(String[] args) {
Animal animal=new Dog();
if(animal instanceof Wolf) {
Wolf wolf=(Wolf)animal;
wolf.hunt();
}
if(animal instanceof Dog) {
Dog dog=(Dog)animal;
dog.play();
}
}
}
2.6 避免在构造方法中使用被重写的方法
如果在父类的构造方法里调用了被重写的方法,此时子类成员还没有构造出来,可能会发生意想不到的结果
class B {
public B() {
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
上面代码的执行过程如下图
当B类的构造方法调用FunC的时候,num还没有被初始化成1,而是默认值0,因此输出结果是D.funC() 0