文章目录
一、继承
1、为什么要继承?
举个例子:
class Dog {
public String name;
private int age;
public void eat() {
System.out.println(this.name + "正在吃饭");
}
public void bark() {
System.out.println(this.name + "正在汪汪");
}
}
class Cat {
public String name;
private int age;
public void eat() {
System.out.println(this.name + "正在吃饭");
}
public void meow() {
System.out.println(this.name + "正在喵喵");
}
}
class wolf {//...}
可以看到,猫和狗都有一个共同的特点:
那么可以把这个共同的特性抽象出来,放到另外一个Animal类中:
class Animal {
public String name;
private int age;
public void eat() {
System.out.println(this.name + "正在吃饭");
}
}
class Dog extends Animal{
public void bark() {
System.out.println(this.name + "正在汪汪");
}
}
class Cat extends Animal{
public void meow() {
System.out.println(this.name + "正在喵喵");
}
}
这样一来,每个类中的代码量明显减少,还避免了代码的重复。
可以看出,“is-a”关系是继承的一个明显特征:Cat “is-a” Anmal;Dog “is-a” Animal。
关键字extends表明正在构造的新类派生于一个已存在的类。这个已存在的类成为超类(superclass)、基类(bass class)或父类(parent class);新类成为子类(subclass)、派生类(derived class)或孩子类(child class)。
例如:class Dog extends Animal
中,Dog
被成为子类(派生类);Animal
被成为父类(基类、超类);extends
则是关键字。
总结:继承是一种思想,对共性进行抽取,从而达到代码的复用效果。
注:
-
子类会将父类中的成员方法和成员变量继承到子类中。
比如:Animal类中的
private int age
也会被继承到子类中,只是子类不能直接访问。说明:public和private只是访问修饰符,并不影响
-
子类继承父类后,必须添加自己特有的成员,体现与基类的不同,否则就没有必要继承了。
2、继承的概念
继承(inheritance)机制:是面向对象程序设计使代码复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展(extends)、增加新功能,这样产生的新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码的复用。
例如:猫和狗都是动物,我们将其共性内容eat()
进行抽取,然后采用继承的思想达到共用。
3、继承的语法
使用extends关键字表示类之间的继承关系
class 子类 extends 父类 {
//。。。
}
4、覆盖方法
很多时候父类的有些方法对子类不一定适用,此时,我们需要一个新的方法来**重写/覆盖(Override)**父类的这个方法。
具体来说:
class Animal {
private String name;
private int age;
public void eat() {
System.out.println(this.name + "eating");
}
}
class bird extends Animal{
@Override
public void eat() {
System.out.println(name + "is eating");
}
}
这看起来似乎很简单,但问题是父类中name是通过private修饰的,子类虽然能继承到但是不能直接访问。这是我们就可以使用Animal中的公共方法getName()
;
class Animal {
private String name;
private int age;
public void eat() {
System.out.println(this.name + "eating");
}
public String getName() {
return name;
}
}
class bird extends Animal{
@Override
public void eat() {
System.out.println(getName() + "is eating");
}
}
但是如果子类中也有一个getName()
方法呢?
class Animal {
//。。。
}
class bird extends Animal {
private String name;
@Override
public void eat() {
System.out.println(getName() + "is eating");
}
public String getName() {
return name;
}
}
这时我们就会发现getName()
调用的是子类自己的方法而非父类的方法。那如果我们想访问父类的getName()
方法呢?此时,我们可以使用特殊关键字super
来解决这个问题。
class Animal {
//。。。
}
class bird extends Animal {
private String name;
@Override
public void eat() {
System.out.println(super.getName() + "is eating");
}
public String getName() {
return name;
}
}
这样,我们调用的就是父类的方法而非子类重写的方法了。
在子类方法中 或者 通过子类对象访问成员时:
- 如果访问的成员变量子类有,优先访问自己的成员变量。
- 如果访问的成员变量子类中无,则访问父类继承下来的;如果父类也没有定义,则编译报错。
- 如果访问的成员变量与父类中成员变量同名,则优先访问自己的。
成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。
注释:有些人认为super与this引用时类似的概念,实际上,这样比较并不太恰当。这是因为super不是一个对象的引用。例如,不能将值super赋给另一个对象变量,它只是一个指示编辑器调用超类方法的特殊关键字。
继承可以增加字段、增加方法或重写超类的方法,不过,继承绝对不会删除任何字段或方法。
5、super关键字
子类和超类中可能会存在相同名称的成员,如果要在子类方法中访问超类同名成员时,该如何操作?无法直接访问,但可以使用Java提供的super关键字。该关键字主要作用:在子类方法中访问父类的成员。
public class Base {
int a;
int b;
public void methodA() {
System.out.println("methodA");
}
public void meethodB() {
System.out.println("methodB");
}
}
class Derived extends Base {
int a;//与父类成员变量同名同类型
char b;//与父类成员变量同名不同类型
//与超类methodA方法重载
@Overload
public void methodA(int a) {
System.out.println("Overload methodA");
}
//与超类methodB方法重写
@Override
public void methodB() {
System.out.println("Override methodB");
}
public void methodC() {
//对于同名的成员变量,直接访问时,访问的都是子类的。
a = 100;//等价于this,a = 100;
//this时当前对象的引用
//访问父类的成员变量时,需要借助super关键字
//super是获取到子类对象中从父类继承下来的部分
super.a = 200;
super.b = 201;
//父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法
methodA();//没有传参,访问父类中的methodA();
methodA(20);//传递int参数,访问子类中methodA(int a);
// 如果在子类中要访问重写的基类方法,则需要借助super关键字
methodB();//访问子类methodB方法
super.methodB();访问父类methodB方法
6、super和this
super和this都可以再成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句,那它们之间有什么区别呢?
【相同点】
- 都是Java中的关键字。
- 只能在类的非静态方法中使用,用来访问非静态成员方法和字段。
- 在构造方法中调用时,必须时构造方法中的第一条语句,并且不能同时存在。
【不同点】
- this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用。
- 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性。
- 在构造方法中,this(…)用于调用本类的构造方法;super(…)用于调用父类的构造方法。两种调用不能同时在构造方法中出现。
- 构造方法中一定会存在super(…)的调用,用户没有写编译器也会增加;但是this(…)用户不写就没有.
6、子类的构造方法
当子类继承超类时:
class Animal {
private String name;
private int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name + "eat food.");
}
}
class bird extends Animal{
private boolean whetherFly;
public bird(String name, int age, boolean whetherFly) {
super(name, age);
this.whetherFly = whetherFly;
}
public void fly() {
whetherFly = true;
System.out.println("Can birds fky?" + this.whetherFly);
}
}
这里的关键字super具有不同的涵义。语句super(name,age);是“调用超类Animal中带有name、age参数的构造方法”的简写形式。
由于bird的构造方法不能访问超类Animal中的私有字段,所以必须通过一个构造方法来初始化这些私有字段。可以利用特殊的super语法调用这个构造方法。使用super调用构造方法的语句必须是子类构造方法的第一条语句。
如果子类的构造方法没有显示地调用超类的构造方法,将自动地调用超类的无参构造方法。如果超类没有无参数的构造方法,并且在子类的构造方法中又没有显示地调用超类的其他构造方法,Java编译器就会报告一个错误。
关键字this有两个含义:一是指示隐式参数的引用,二是调用该类的其他构造方法。类似地,super关键字也有两个含义:一是调用超类的方法,二是调用超类的构造方法。在调用构造方法的时候,this和super这两个关键字紧密相关。调用构造方法的语句只能作为另一个构造方法的第一条语句出现。构造方法参数可以传递给当前类(this)的另一个构造方法也可以传递给超类(super)的构造方法。
7、实例代码块和静态代码块
实例代码块和静态代码块在无继承关系是执行结果如下:
public class ExtendsTest {
public static void main(String[] args) {
Person1 person1 = new Person1("warframe",10);
System.out.println("=============");
Person1 person2 = new Person1("Mesa Prime",100);
}
}
class Person1 {
private String name;
private int age;
public Person1(String name, int age) {
this.name = name;
this.age = age;
System.out.println("构造方法执行");
}
{
System.out.println("实例代码块执行");
}
static {
System.out.println("静态代码块执行");
}
}
执行结果:
- 静态代码块先执行,并且只执行一次,在类加载阶段执行
- 当对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行。
【继承关系上的执行顺序】
public class ExtendsTest {
public static void main(String[] args) {
Student student1 = new Student("张三",19);
System.out.println("=================");
Student student2 = new Student("李四",20);
}
}
class Person1 {
private String name;
private int age;
public Person1(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Peson:构造方法执行");
}
{
System.out.println("Peson:实例代码块执行");
}
static {
System.out.println("Peson:静态代码块执行");
}
}
class Student extends Person1 {
public Student(String name, int age) {
super(name, age);
System.out.println("Student:构造方法执行。");
}
{
System.out.println("Student:实例代码块执行");
}
static {
System.out.println("Student:静态代码块执行");
}
}
执行结果:
- 父类静态代码块优先于子类静态代码块执行,且是最早执行。
- 父类实例代码块和父类构造方法紧接着执行。
- 子类的实例代码块和子类构造方法紧接着再执行。
- 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行。
8、protected关键字
为了实现封装性,Java中引入了访问修饰符。主要作用:类或者类中成员能否再类外或者其他包中访问。
NO | 范围 | private | default | protected | public |
---|---|---|---|---|---|
1 | 同一包中的同一类 | ✔ | ✔ | ✔ | ✔ |
2 | 同一包中的不同类 | ✔ | ✔ | ✔ | |
3 | 不同包中的子类 | ✔ | ✔ | ||
4 | 不同包中的非子类 | ✔ |
注意:虽然父类中private修饰的成员变量在子类中不能直接访问,但也继承到子类中了。
什么时候用哪一种呢?
我们希望类要尽量做到”封装“,即隐藏内部实现细节,只暴露出必要的信息给类的调用者。
因此我们在使用的时候应该尽可能的使用比较严格的访问权限。例如:如果一个方法能用private,就尽量不要用public。
另外,还有一种简单粗暴的做法:将所有字段设为private,将所有的方法设为public,不过这种方式属于是对访问权限的滥用,写代码时应该认真思考,该类提供的字段方法到底给“谁”使用(是类内部自己用,还是类的调用者使用,还是子类使用)。
这张表清楚的展现了访问情况,这里我们重点说一下protected再不同包的子类中的访问情况:
//包ProtectedTest
package ProtectedTest;
public class B {
private int a = 1;
protected int b = 100;
public int c = 200;
int d = 300;
}
//包ProtectedTest1
package ProtectedTest1;
import ProtectedTest.B;
public class D extends B {
public static void main(String[] args) {
B test = new B();
System.out.println(test.b);
}
}
此时将会产生错误:
因为b
不能通过对象的引用访问,而是应该通过super
。但是当我们用super调用会发现:
public static void main(String[] args) {
B test = new B();
System.out.println(super.b);
}
还是会出现错误提示:
这是因为main方法是通过static修饰的,static方法不依赖于对象,不能用super。那么应该如何调用呢?
public class D extends B {
public void func() {
System.out.println(super.b);
}
public static void main(String[] args) {
D test = new D();
test.func();
}
}
结果为:
显然这样访问成功了。
注意:类的修饰不能用protected和private。
9、继承层次
显示中,事物之间的关系错综复杂,例如:
像这样,由一个公共超类派生出来的所有类合称为继承层次。
在继承层次中,从某个特定的类到其祖先的路径称为该类的继承链(inheritance chain)
在Java中只支持一下几种继承方式:
注意:Java中不支持多继承.。
我们写的类是现实事物的抽象,在实际应用过程中,可能会涉及到一系列复杂的概念,都需要我们使用代码来表示。所以我们实际开发中写的类会有很多,类之间的关系也会更加复杂。
但即使如此,我们并不希望类之间的继承层次太复杂,**一般我们不希望出现超过三层的继承关系。**如果继承层次太多,就需要考虑对代码进行重构了。
Java虽然不支持多继承,但提供了一些类似多重继承的功能,有关内容请看本人写的抽象类与接口:http://t.csdn.cn/uGmPx
10、final关键字
final可以用来修饰变量、成员方法和类。
-
修饰变量或者字段:表示常量(即不可更改)
final int a = 10; a = 20;//编译出错
-
修饰类:表示此类不能被继承
final public class Base { //... } class Derived extends Base { //... } //编译出错,无法继承
经典例子:String类:
-
修饰方法:表示该方法不能被重写
11、继承和组合
和继承类似,组合也是一种表达类之间关系的方式,也是能够达到代码重用的效果。组合没有涉及特殊的语法(诸如extends这样的关键字),仅仅是将一个类的实例作为另一个类的字段。
继承表示对象之间是 is-a的关系:比如猫是动物,狗是动物
组合表示对象之间是has-a的关系:比如汽车和其发动机、方向盘、刹车系统等等之间的关系就是组合,因为契合是由这些部件组成的。
//轮胎类
class Tire {
//...
}
//发动机类
class Engine {
//...
}
...
//汽车类
class Car {
private Tire tire;//可以复用轮胎类中的属性和方法
private Engine engine;//可以复用发动机类中的属性和方法
//...
}
//玛莎拉蒂属于汽车
class Maserati {
//将汽车中包含的:轮胎、发动机等等全部继承下来
}
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合。
二、多态
1、多态的概念
具体来说就是去完成某个行为,当不同的对象去完成时会产生不同的状态。
同样是吃,富人就吃美食,穷人就吃糟糠,这就是同一件事情在不同对象身上,就会产生不同的结果。
就如同下面将会讲到的例子
public class test {
public static void eatFunc(Animal animal) {
animal.eat();
}
public static void main(String[] args) {
Dog dog = new Dog("旺财",7);
eatFunc(dog);
Cat cat = new Cat("喵呜",7);
eatFunc(cat);
}
}
class Dog extends Animal{
//...
@Override
public void eat() {
//...
}
}
class Cat extends Animal{
//...
@Override
public void eat();
}
class Animal {
//...
public void eat() {
}
}
在方法main中,传递给eatFunc()的子类对象不同,调用eat这个重写方法所表现出来的行为就是不一样,传猫就调用猫的重写方法,传狗就调用狗的重写方法,这种思想就叫多态
2、多态实现的条件
在Java中实现多态,必须满足一下条件,缺一不可:
- 必须在继承体系下。
- 子类必须要对父类中的方法进行重写。
- 通过父类的引用调用重写的方法。
当完成以上三部分,就会发生动态绑定。动态绑定时多态的基础。
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
3、动态绑定和静态绑定
动态绑定:也成为前期绑定(早绑定、运行时绑定),即在编译时,根据用户所传递实参类型就确定了具体调用哪个方法。典型代表:函数重载。
静态绑定:也成为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法。
3.1、重写
重写(Override):也称为覆盖。重写是子类对父类非静态、非private修饰、非final修饰、非构造方法等实现过程进行重新编写,**返回值和型材不能改变,即外壳不变,核心重写。**重写的好处在于子类可以根据需求,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。
【重写的规则】:
- 子类在重写父类的方法时,必须与父类方法原型一致:返回值类型、方法名、参数列表要完全一致。
- 被重写的方法返回值可以不同,但是必须时具有父子关系的。
- **访问权限不能比父类中被重写的方法的访问权限更低。**例如:如果父类方法public修饰,则子类中重写该方法就不能声明为protected。
- 父类被static、private修饰的方法、构造方法都不能被重写。
- 重写的方法,可以使用
Override
注解来显示指定,有了这个注解能帮我们进行一些合法性校验,例如不小心将方法名字拼写错了(例如eat写成了aet),那么此时编译器就会发现父类中没有aet方法,就会编译报错,提示无法构成重写。
【重写与重载的区别】
区别点 | 重写(Override) | 重载(Overload) |
---|---|---|
参数列表 | 一定不能修改 | 必须修改 |
返回类型 | 一定不能修改(除非能构成父子类关系) | 可以修改 |
访问限定符 | 一定不能做更严格的限制(可以降低限制) | 可以修改 |
【重写的设计原则】
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
4、向上转型和向下转型
4.1向上转型
当我们使用实例化子类对象调用父类和子类方法时:
public class Transition {
public static void main(String[] args) {
Dog dog = new Dog("旺财",7);
dog.bark();
dog.eat();
}
}
class Animal {
private String name;
private int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name+"吃");
}
public String getName() {
return name;
}
}
class Dog extends Animal{
public Dog(String name, int age) {
super(name, age);
}
public void bark() {
System.out.println(super.getName()+"正在狗叫");
}
}
结果为:
旺财正在狗叫
旺财吃
但如果我们实例化父类对象试图调用子类成员方法时:
public class Transition {
public static void main(String[] args) {
System.out.println("===================");
Animal animal = new Animal("乐迪",8);
animal.eat();
animal.bark();//报错
}
}
class Animal {
private String name;
private int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name+"吃");
}
public String getName() {
return name;
}
}
class Dog extends Animal{
public Dog(String name, int age) {
super(name, age);
}
public void bark() {
System.out.println(super.getName()+"正在狗叫");
}
}
此时会出现错误:
无法解析 ‘Animal’ 中的方法 ‘bark’
得出结论:通过父类引用只能调用父类自己特有的成员方法或者成员变量。
而向上转型实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型();
Animal animal = new Dog("旺财",7);//代表了animal这个引用指向了
等同于
Dog dog = new Dog("旺财",7);
Animal animal = dog;
//一般来说等号两边要赋值需要两边的类型一样,但因为两者是继承关系,所以可以进行赋值操作
Animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围转换。
除了以上的直接赋值的向上转型方式,另外两种向上转型:
public static void func(Animal animal){
//...
}
public static Animal func2() {
Dog dog = new Dog("旺财",7);
return dog;
}
public static void main(String[] args) {
Animal animal = func2();
}
总结:常见的可以发生向上转型的3个时机
- 直接赋值
- 方法的参数,传参的时候进行向上转型
- 返回值向上转型
向上转型的优点:让代码实现更简单灵活。
向上转型的缺点:不能调用子类特有的方法。
4.2、向下转型
将子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型。
Animal animal = new Dog("旺财",7);
//若此时我们调用bark()方法
animal.bark();
//会编译失败,因为animal中没有bark()方法。
//向下转型
dog = (Dog)animal;//因为animal本来指向的就是Dog,因此animal能被还原为狗。
dog.bark();
//但是如果我们还原为猫
cat = (Cat)animal;
cat.mew();
//程序能通过编译但会抛出ClassCastException的异常
//因为animal实际指向的是Dog
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了instanceof
,如果该表达式为true,则可以安全转换。
//向上转型
Dog dog = new Dog("旺财",7);
Cat cat = new Cat("喵呜",7);
Animal animal = dog
animal.eat();
animal = cat;
animal.eat();
if(animal instanceof Dog) {
dog = (Dog)animal;
dog.bark();
}
if (animal instanceof Cat) {
cat = (Cat)animal;
cat.mew();
}
总结:常见的可以发生向上转型的3个时机:
- 直接赋值
- 方法的参数,传参的时候进行向上转型
- 返回值向上转型
向上转型的有点:让代码实现更简单灵活。
向上转型的缺点:不能调用子类特有的方法。
4.2、向下转型
将子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型。
Animal animal = new Dog("旺财",7);
//若此时我们调用bark()方法
animal.bark();
//会编译失败,因为animal中没有bark()方法。
//向下转型
dog = (Dog)animal;//因为animal本来指向的就是Dog,因此animal能被还原为狗。
dog.bark();
//但是如果我们还原为猫
cat = (Cat)animal;
cat.mew();
//程序能通过编译但会抛出ClassCastException的异常
//因为animal实际指向的是Dog
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了instanceof
,如果该表达式为true,则可以安全转换。
//向上转型
Dog dog = new Dog("旺财",7);
Cat cat = new Cat("喵呜",7);
Animal animal = dog
animal.eat();
animal = cat;
animal.eat();
if(animal instanceof Dog) {
dog = (Dog)animal;
dog.bark();
}
if (animal instanceof Cat) {
cat = (Cat)animal;
cat.mew();
}