导言:
在Java中,继承和多态是面向对象编程的两个重要概念,它们允许我们创建更加灵活和可扩展的代码。本文主要对继承和多态的语法和一些细节做一个介绍和解释。
目录
正文:
一.继承
继承是面向对象编程中的一种机制,允许一个类(称为子类)继承另一个类(称为父类)的属性和方法。通过继承,子类可以获得父类的所有属性和方法,同时可以在其基础上添加新的属性和方法,或者修改已有的属性和方法。通俗来讲,继承主要就是用来对类和类之间进行共性抽取,从而实现代码复用。
1. 基本语法
在Java中,继承通过使用关键字extends
来实现。子类可以继承父类的非私有属性和方法。基本的继承语法如下:
class ParentClass {
// 父类的属性和方法
}
class ChildClass extends ParentClass {
// 子类的属性和方法
}
需要 注意的是:子类会将父类中的成员变量或者成员方法继承到子类中。 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
2. 继承的特点
要知道子类并不能将父类的所有东西都继承下来,需要遵循一定的规则。
对于构造方法而言:
子类无法继承父类的构造方法,但是子类可以可以调用父类的构造方法来帮助父类进行初始化。
对于成员变量而言:
子类可以继承父类的所有成员变量。
对于成员方法而言:
子类可以继承非私有的成员方法,但不能继承私有的成员方法。
下面是一些关于Java中继承的重要特点:
- 子类可以继承父类的非私有属性和方法,这意味着子类可以直接访问父类的属性和方法。
- 子类可以重写父类的方法,从而实现对父类方法的覆盖。
- Java中不支持多继承,一个类只能有一个直接父类。
- 子类可以调用父类的构造方法来初始化父类的属性。
- 子类可以在继承父类的基础上添加新的属性和方法,从而实现代码的扩展。
3.子类中访问父类的成员变量
- 当子类和父类不存在同名变量时:
class ParentClass {
// 父类的属性和方法
int a;
int b;
}
class ChildClass extends ParentClass {
// 子类的属性和方法
int c;
public void method() {
a = 10; // 访问从父类中继承下来的a
b = 20; // 访问从父类中继承下来的b
c = 30; // 访问子类自己的c
}
}
在Java中,如果子类和父类不存在同名变量,子类可以直接使用父类的成员变量。这是因为子类会继承父类的变量,即使没有同名的变量也可以直接使用父类的变量。
- 当子类和父类存在成员变量同名时:
public class Base {
int a;
int b;
int c;
}
//----------------------------------------------------------------
public class Derived extends Base{
int a; // 与父类中成员a同名,且类型相同
char b; // 与父类中成员b同名,但类型不同
public void method(){
a = 100; // 访问的成员变量与父类中成员变量同名,优先访问自己的。
b = 101; // 访问的成员变量子类中没有,则访问父类继承下来的
c = 102; // 子类没有c,访问的肯定是从父类继承下来的c
// d = 103; // 编译失败,因为父类和子类都没有定义成员变量b
}
}
通过子类对象访问成员时遵循以下几点:
- 如果访问的成员变量子类中有,优先访问自己的成员变量。
- 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
- 如果访问的成员变量与父类中成员变量同名,则优先访问自己的。
- 成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。
4. 子类中访问父类的成员方法
- 当子类和父类成员方法名字不同时:
class Parent {
public void display() {
System.out.println("This is the display method in the Parent class");
}
}
class Child extends Parent {
public void callParentMethod() {
display(); // 子类直接调用父类的成员方法
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.callParentMethod(); // 输出:This is the display method in the Parent class
}
}
和成员变量一样,如果子类和父类不存在同名方法,子类可以直接使用父类的成员方法。在子类方法中或者通过子类对象访问方法时,优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错。
- 当子类和父类成员方法名字相同时:
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(),基类的无法访问到
}
}
通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错。
通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错;
可见成员方法和变量一致,优先访问子类的,但是我们可以指定其优先访问父类,我们可以通过super关键字来实现。
5.super关键字
super
关键字在Java中用于引用对象的直接父类,可以用于调用父类的构造方法,访问父类的成员变量和成员方法,以及在子类中调用父类的构造方法和成员方法。我们可以通过super.的形式访问到父类的成员方法和变量。
public class Base {
int a;
public void methodA(){
System.out.println("Base中的methodA()");
}
public void methodB(){
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base{
int a;
public void methodB(){
System.out.println("Derived中的methodB()方法");
}
public void methodC(){
a=10;//直接访问,访问到的都是子类中的变量
super.a=10;//super调用,访问到了父类的成员变量
methodA(); // 子类没有,访问父类的方法
methodB(); // 直接访问,访问到的都是子类中的methodB(),基类的无法访问到
super.methodB();//super调用,访问到了父类的成员方法
}
}
在子类中,如果想要明确访问父类中成员时,借助super关键字即可。
同时我们还可以利用super来调用父类的构造方法:
调用父类的构造方法:在子类的构造方法中使用super()
来调用父类的构造方法。可以使用super()
来调用父类的无参构造方法,也可以使用super(参数列表)
来调用父类的有参构造方法。
public class Child extends Parent {
public Child() {
super(); // 调用父类的无参构造方法
}
}
在构造方法中也存在着一些细节:
1. 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法
2. 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。
3. 在子类构造方法中,super(...)调用父类构造时,必须是子类构造函数中第一条语句。
4. super(...)只能在子类构造方法中出现一次,并且不能和this同时出现需要注意的是,super与this一样, 只能在非静态方法中使用。
6.代码的执行顺序
在Java中,当涉及到继承体系时,代码的执行顺序遵循以下规则:
- 静态代码块和静态变量初始化:首先,会按照继承层次从上到下的顺序依次执行每个类中的静态代码块和静态变量初始化。这意味着父类的静态代码块和静态变量会先被初始化,然后是子类的静态代码块和静态变量。
- 父类构造方法:父子父子,先有父再有子,接着,会调用父类的构造方法。如果父类还有父类,那么会依次调用父类的构造方法,直到最顶层的父类。
- 父类的实例代码块
- 子类成员变量和实例代码块初始化:接下来,会初始化子类的成员变量和实例代码块,按照声明的顺序执行。
- 子类构造方法:最后,会调用子类的构造方法。
代码验证:
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:实例代码块执行");
}
static {
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:实例代码块执行");
}
static {
System.out.println("Student:静态代码块执行");
}
}
public class test {
public static void main(String[] args) {
Student student1 = new Student("张三", 19);
System.out.println("===========================");
Student student2 = new Student("gaobo", 20);
}
}
结果如下:
通过分析执行结果,得出以下结论:
1、父类静态代码块优先于子类静态代码块执行,且是最早执行
2、父类实例代码块和父类构造方法紧接着执行
3、子类的实例代码块和子类构造方法紧接着再执行
4、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
二.多态
1.基本概念:
多态,是指一个引用通过不同的对象的调用,呈现出不同的状态。通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。多态的一种体现,就是在代码运行时,当传递不同类对象时,会调用对应类中的方法。
多态的一种表现形式是:
父类类型 对象名称 = 子类对象;
例如:
class Person {
}
class Student extends Person {
}
public class test {
public static void main(String[] args) {
Person p = new Student();
}
}
在java中要实现多态,必须要满足如下几个条件,缺一不可:
1. 必须在继承体系下
2. 子类必须要对父类中方法进行重写
3. 通过父类的引用调用重写的方法
下面是一个多态的实例:
// 定义一个动物类
class Animal {
public void makeSound() {
System.out.println("动物发出叫声");
}
}
// 定义一个狗类,继承自动物类
class Dog extends Animal {
public void makeSound() {
System.out.println("狗发出汪汪的叫声");
}
}
// 定义一个猫类,继承自动物类
class Cat extends Animal {
public void makeSound() {
System.out.println("猫发出喵喵的叫声");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
Animal animal1 = new Dog(); // 使用父类引用指向子类对象
Animal animal2 = new Cat(); // 同样使用父类引用指向另一个子类对象
animal1.makeSound(); // 输出:狗发出汪汪的叫声
animal2.makeSound(); // 输出:猫发出喵喵的叫声
}
}
当类的调用者在编写 makeSound 这个方法的时候, 参数类型为 Animal (父类), 此时在该方法内部并不知道, 也不关注当前的引用指向的是哪个类型(哪个子类)的实例. 此时 这个引用调用 makeSound方法可能会有多种不同的表现(和 a 引用的实例相关), 这种行为就称为 多态.
2.重写
Java中,方法重写(Override)是指子类重新定义父类中已有的方法,以实现特定的功能。方法重写是实现多态性的一种方式,它允许子类提供对继承的方法的自定义实现。当子类重写父类的方法时,子类对象调用该方法时将执行子类中的实现,而不是父类中的实现。
以下是方法重写的一些重要规则和注意事项:
- 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
- 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
- 父类被static、private修饰的方法、构造方法都不能被重写。
- 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
下面是一个重写的案例:
// 父类
class Animal {
public void makeSound() {
System.out.println("动物发出叫声");
}
}
// 子类
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("狗发出汪汪的叫声");
}
}
public class OverrideExample {
public static void main(String[] args) {
Animal animal = new Dog(); // 使用父类引用指向子类对象
animal.makeSound(); // 输出:狗发出汪汪的叫声
}
}
重写和重载有很多类似的地方,为了防止混淆,这里做一个总结。
重写和重载的区别:
- 重载方法是在同一个类中定义多个方法,而重写方法是子类重新定义父类中已有的方法。
- 重载方法的方法名相同,但参数列表不同,而重写方法的方法名、参数列表和必须与父类中被重写的方法完全一致。
- 重载方法的调用是根据参数列表的类型和数量来确定的,而重写方法的调用是根据对象的实际类型来确定的。
- 重载可以修改访问限定符,但重写的访问限定符子类必须大于父类。
3.向上转型
向上转型是指将子类的实例赋给父类引用的过程。在向上转型中,子类的实例可以赋给父类的引用变量,这样就可以通过父类的引用变量来访问子类对象的方法和属性。向上转型是安全的,因为子类对象具有父类对象的所有属性和方法,但不能访问子类特有的方法和属性。
实例代码:
class Animal {
public void makeSound() {
System.out.println("动物发出叫声");
}
}
class Dog extends Animal {
public void makeSound() {
System.out.println("狗发出汪汪的叫声");
}
public void wagTail() {
System.out.println("狗摇尾巴");
}
}
public class UpcastingExample {
public static void main(String[] args) {
Animal animal = new Dog(); // 向上转型
animal.makeSound(); // 输出:狗发出汪汪的叫声
// animal.wagTail(); // 编译错误,无法访问子类特有的方法
}
}
在这个例子中,子类Dog的实例被赋给了父类Animal的引用变量,这就是向上转型。通过父类引用变量animal可以调用子类重写的方法makeSound,但无法访问子类特有的方法wagTail。
4.向下转型
向下转型是指将父类引用转换为子类引用的过程。在向下转型中,需要使用强制类型转换将父类引用转换为子类引用,以便访问子类特有的属性和方法。向下转型是不安全的,因为父类引用指向的对象可能不是子类的实例,如果强制类型转换失败会抛出ClassCastException异常。
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了instanceof
,instanceof
的作用是判断 object
是否是 class
的实例或者子类的实例,如果是则返回 true
,否则返回 false
。如果 object
为 null
,则返回 false
。的作用是判断 object
是否是 class
的实例或者子类的实例,如果是则返回 true
,否则返回 false
。如果 object
为 null
,则返回 false
。如果该表达式为true
,则可以安全转换。
实例代码:
public class DowncastingExample {
public static void main(String[] args) {
Animal animal = new Dog(); // 向上转型
if (animal instanceof Dog) {
Dog dog = (Dog) animal; // 向下转型
dog.makeSound(); // 输出:狗发出汪汪的叫声
dog.wagTail(); // 输出:狗摇尾巴
}
}
}
在这个例子中,animal instanceof Animal
返回 true
,因为 animal
是 Animal
类的实例;animal instanceof Dog
也返回 true
,因为 animal
是 Dog
类的实例。
5.多态的优缺点
优点:
-
灵活性:多态使得代码更加灵活,能够适应不同的对象和场景。通过多态,可以使用父类类型的引用变量来引用子类对象,从而实现统一的接口访问不同的子类对象,简化了代码的设计和实现。
-
可扩展性:多态使得代码更易于扩展。当需要添加新的子类时,无需修改现有的代码,只需要创建新的子类并实现父类的抽象方法,就可以直接在现有代码中使用新的子类对象。
-
维护性:多态提高了代码的可维护性。通过多态,可以将代码模块化,降低了代码之间的耦合度,便于单独修改和维护各个模块。
-
能够降低代码的 "圈复杂度", 避免使用大量的 if - else
缺点:
-
性能损失:多态会带来一定的性能损失。由于多态需要在运行时动态确定对象的类型,因此需要额外的运行时开销,可能会导致一定的性能损失。
-
可读性降低:过度使用多态可能会降低代码的可读性。如果在代码中过度使用多态,可能会导致代码难以理解和维护,降低代码的可读性。
-
不适合所有场景:多态并不适合所有的场景。有些情况下,需要明确地知道对象的具体类型,而不是使用多态。过度使用多态可能会导致代码的复杂性增加,不利于理解和维护。
总结:
继承:一个类可以继承另一个类的属性和方法,这样可以避免重复编写相似的代码,并且可以实现多层次的类组织管理。
多态:允许不同的对象对同一消息作出不同的响应,通过动态绑定的方式,可以根据对象的实际类型来确定调用的方法,从而实现代码的灵活性和可扩展性。