Java面向对象(继承、多态、抽象类、接口)

面向对象三大特性:封装、继承、多态。

继承

  • 现实生活中事物和事物之间可能存在或多或少的联系,例如:

Imgur

  • 兔子、山羊、奶牛属于食草动物,狮子、老虎、豹子属于食肉动物。
  • 食草动物和食肉动物都属于动物。
  • 因此继承需要符合is-a的关系,子类是父类的具体体现。

为什么要使用继承

  • 若我们没有继承,那定义如下两个类:DogCat。会存在大量重复的代码,两个类都有属性nameage,都有eat()sleep()行为。

Imgur

  • 我们可以将这些共同的属性和行为整合起来,定义一个动物类父类,让狗和猫继承动物这个父类。

Imgur

继承的语法

  • 子类使用extends关键字来继承父类。
class A extends B {}

由此,上面的设计代码实现:

class Animal { // 动物类 父类
    public String name;
    public int age;
    public void eat() {
        System.out.println(name + "在吃饭");
    }
    public void sleep() {
        System.out.println(name + "在睡觉");
    }
}
class Dog extends Animal{ // 子类继承父类
    public void bark() {
        System.out.println(name + "在汪汪叫");
    }
}
class Cat extends Animal {
    public void mew() {
        System.out.println(name + "在喵喵叫");
    }
}

继承的类型

Imgur

Imgur

Imgur

  • 还有一种Java不支持的继承类型。

Imgur

访问父类成员

子类访问父类成员变量

访问不同名成员变量
class Base {
    int a;
    int b;
}
class Sub extends Base {
    int c;
    public void method() {
        a = 10;
        b = 20;
        c = 30;
    }
}
public class Test {
    public static void main(String[] args) {
        Sub s = new Sub();
        s.method();
        System.out.println(s.a);
        System.out.println(s.b);
        System.out.println(s.c);
    }
}
// 输出结果:
// 10
// 20
// 30
  • 只要父类的成员变量不是private修饰的,且子类、父类在一个包下,子类可以直接访问父类的成员变量。
访问同名成员变量
class Base {
    int a;
    int b;
    int c = 99;
}
class Sub extends Base {
    int c;
}
public class Test {
    public static void main(String[] args) {
        Sub s = new Sub();
        System.out.println(s.c);
    }
}
// 输出结果:
// 0
  • 若子类的成员变量和父类成员变量重名,则访问子类的成员变量。若想访问父类同名成员变量,需使用super引用来访问。
class Base {
    int a;
    int b;
    int c = 99;
}
class Sub extends Base {
    int c;
    public void method() {
        System.out.println(super.c);
    }
}
public class Test {
    public static void main(String[] args) {
        Sub s = new Sub();
        s.method();
    }
}
// 输出结果:
// 99

成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。

子类访问父类成员方法

访问不同名成员方法
class Base {
    public void method1() {
        System.out.println("Base中的method1方法");
    }
}
class Sub extends Base {
    public void method2() {
        System.out.println("Sub中的method2方法");
    }
    public void method3() {
        method1();
        method2();
    }
}
public class Test {
    public static void main(String[] args) {
        Sub s = new Sub();
        s.method3();
    }
}
// 输出结果:
// Base中的method1方法
// Sub中的method2方法
  • 只要父类的成员方法不是private修饰,子类就可以直接访问。
访问同名成员方法
class Base {
    public void method1() {
        System.out.println("Base中的method1方法");
    }
}
class Sub extends Base {
    public void method1() {
        System.out.println("Sub中的method2方法");
    }
    public void method3() {
        method1();
    }
}
public class Test {
    public static void main(String[] args) {
        Sub s = new Sub();
        s.method3();
    }
}
// 输出结果:
// Sub中的method2方法
  • 若子类的成员方法和父类成员方法重名,则访问子类的成员方法。若想访问父类同名成员方法,需使用super引用来访问。
class Base {
    public void method1() {
        System.out.println("Base中的method1方法");
    }
}
class Sub extends Base {
    public void method1() {
        System.out.println("Sub中的method2方法");
    }
    public void method3() {
        method1();
        super.method1();
    }
}
public class Test {
    public static void main(String[] args) {
        Sub s = new Sub();
        s.method3();
    }
}
// 输出结果:
// Sub中的method2方法
// Base中的method1方法
  • 通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错。

  • 通过子类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错。

构造方法

  • 当我们写这样一段代码时,是会报错的。
class Animal {
    private String name;
    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
class Dog extends Animal{

}

Imgur

  • 子类是不能继承父类的构造方法的,子类只是调用父类隐式或显式的构造方法。因此,才进行子类的构造前要对父类完成构造。
  • 如上代码,父类创建了构造方法public Animal(String name, int age),不会自动生成隐式的构造方法public Animal()。因此,若子类不提供任何构造方法,父类无法完成构造,就会报错。
  • 如上代码,子类必须提供public Dog(String name, int age)构造方法,使用super()调用父类构造方法。
class Animal {
    private String name;
    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
class Dog extends Animal{
    public Dog(String name, int age) {
        super(name, age);
    }
}

Imgur

若父类未自行提供构造方法,系统提供隐式构造方法。

class Animal {
    private String name;
    private int age;
  	
  	// 以下为隐式构造方法
  	public Animal() {}
}

子类可以不写构造方法,系统会默认提供隐式构造方法public Dog()帮助完成父类构造,如:

class Dog extends Animal{
  	// 以下为隐式构造方法
    public Dog() {
        super();
    }
}

构造方法调用验证

class Animal {
    private String name;
    private int age;

    public Animal() {
        System.out.println("Animal类无参构造方法");
    }

    public Animal(String name) {
        System.out.println("Animal类name参数构造方法");
    }

    public Animal(String name, int age) {
        System.out.println("Animal类两个参数构造方法");
    }
}
class Dog extends Animal{
    public Dog() {
        System.out.println("Dog类无参构造方法");
    }

    public Dog(String name) {
        super(name);
        System.out.println("Dog类name参数构造方法");
    }

    public Dog(String name, int age) {
        super(name, age);
        System.out.println("Dog类两个参数构造方法");
    }
}

public class AnimalTest {
    public static void main(String[] args) {
        Dog d1 = new Dog();
        Dog d2 = new Dog("旺财");
        Dog d3 = new Dog("旺财", 3);
    }
}
输出结果:
Animal类无参构造方法
Dog类无参构造方法
Animal类name参数构造方法
Dog类name参数构造方法
Animal类两个参数构造方法
Dog类两个参数构造方法
  • 以上代码可以看出创建子类对象前会调用父类适配的构造方法。

代码调试演示:

继承中代码块的执行顺序

在普通类当中,静态代码块在类加载时执行,因此最先执行。构造代码块在创建对象时执行。

因此我们可以猜测在有继承关系中,代码块的执行顺序是:

  1. 父类的静态代码块。
  2. 子类的静态代码块。
  3. 父类的构造代码块。
  4. 子类的构造代码块。

代码验证:

class Animal {
    private String name;
    private int age;

    static {
        System.out.println("父类的静态代码块");
    }
    {
        System.out.println("父类的构造代码块");
    }

    public Animal() {
        System.out.println("Animal类无参构造方法");
    }

    public Animal(String name) {
        System.out.println("Animal类name参数构造方法");
    }

    public Animal(String name, int age) {
        System.out.println("Animal类两个参数构造方法");
    }
}
class Dog extends Animal{
    static {
        System.out.println("子类的静态代码块");
    }

    {
        System.out.println("子类的构造代码块");
    }
    public Dog() {
        System.out.println("Dog类无参构造方法");
    }

    public Dog(String name) {
        System.out.println("Dog类name参数构造方法");
    }

    public Dog(String name, int age) {
        System.out.println("Dog类两个参数构造方法");
    }
}

public class AnimalTest {
    public static void main(String[] args) {
        Dog d1 = new Dog();
    }
}
输出结果:
父类的静态代码块
子类的静态代码块
父类的构造代码块
Animal类无参构造方法
子类的构造代码块
Dog类无参构造方法

代码调试演示:

执行顺序结论:

父类静态代码块->子类静态代码块->父类构造代码块->父类构造方法->子类构造代码块->子类构造方法

关键字

this

  • this关键字依旧指代本类对象,this()调用本类构造方法。

注:this()必须放在构造方法中的第一行。

class Animal {
    private String name;
    private int age;

    public Animal() {
        age = 0;
        this("旺财"); // error
    }
    public Animal(String name) {
        this.name = name;
    }
}

Imgur

super

  • super关键字指代父类,用法和this类似。
class Animal {
    private String name;
    private int age;

    public void func() {
        System.out.println("父类的func方法");
    }
}
class Dog extends Animal{
    public void func() {
        System.out.println("子类的func方法");
    }

    public void func1() {
        func();
        super.func();
    }
}

public class AnimalTest {
    public static void main(String[] args) {
        Dog d1 = new Dog();
        d1.func1();
    }
}
输出结果:
子类的func方法
父类的func方法

注:和this()一样,子类构造方法要想调用父类的构造方法,使用super()也需要放在构造方法的第一行。

class Animal {
    public String name;
    public int age;

    public Animal() {
        
    }
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
class Dog extends Animal{
    public Dog() {
        name = "旺财";
        age = 0;
        super(); // error
    }
}

Imgur

final

final关键字修饰变量
  • final关键字修饰变量,不能再改变变量中储存的值。
public class FinalTest {
    public static void main(String[] args) {
        final int a = 10;
        a = 20; // error
    }
}

Imgur

  • 因此我们常称被final修饰的变量就是常量。

注:final修饰的引用数据类型,引用不能再指向其他对象,但是对象内的属性值是可以改变的。

public class FinalTest {
    public static void main(String[] args) {
        final int[] arr = {1,2,3,4,5};
        arr[0] = 100;
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }
}
// 输出结果:100 2 3 4 5

但若代码时这样:

public class FinalTest {
    public static void main(String[] args) {
        final int[] arr = {1,2,3,4,5};
        arr[0] = 100;
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
        
        arr = new int[10]; // error
    }
}

就会报错。

Imgur

final关键字修饰类
  • final关键字修饰类,该类不能被继承。
  • 一个典型的例子就是String类,当我们打开原码,就会看到:

Imgur

  • String这个类是无法被继承的。
final class Animal {
    public String name;
    public int age;
    
}
class Dog extends Animal{}

Imgur

final关键字修饰方法
  • final关键字修饰方法,该方法不能被子类重写。
class Animal {
    public String name;
    public int age;

    public final void func() {
        System.out.println("final方法");
    }

}
class Dog extends Animal{
    public void func() {
        System.out.println("子类final方法");
    }
}
// error: java: com.max.classes.Dog中的func()无法覆盖com.max.classes.Animal中的func()被覆盖的方法为final

Imgur

protected

  • private修饰的,可以在同一个包中同一个类中访问。
class Base {
    private int a;
    public void baseFun() {
        System.out.println(a); // 可以访问
    }
}
  • default修饰的,可以在同一个包中不同类访问。
class Base {
    int a;
    public void baseFun() {
        System.out.println(a);
    }
}
public class BaseTest {
    public static void main(String[] args) {
        Base b = new Base();
        System.out.println(b.a); // 可以访问
    }
}
  • public范围很广,在任意位置都可以访问,不同包的非子类也可以访问。
package com.max.demo1;
public class Base {
    public int a;
}
package com.max.demo2;
import com.max.demo1.Base;

public class BaseTest {
    public static void main(String[] args) {
        Base b = new Base();
        System.out.println(b.a); // 可以访问
    }
}
  • protected修饰的,可以在不同包中的子类访问。
package com.max.demo1;
public class Base {
    public int a;
}
package com.max.demo2;
import com.max.demo1.Base;

class Sub extends Base {
    public void func() {
        System.out.println(super.a); // 可以访问
    }
}

多态

多态就是同一个行为的不同表现形式。

Imgur

多态存在必要条件

  • 继承。
  • 方法重写。
  • 父类引用指向子类对象。

方法重写

重写是子类对父类可以访问的方法进行重新实现,重写的方法名、形参列表、返回值都不能改变。

class Animal {
    public void run() {
        System.out.println("动物在跑");
    }
}
class Dog extends Animal{
    public void run() {
        System.out.println("狗在跑");
    }
}
public class AnimalTest {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.run();
    }
}
// 输出结果:狗在跑

虽然引用a的类型是Animal,但它指向的是一个Dog对象,若Animal成员方法被重写,则编译器会动态绑定Dog中重写的Animal成员方法。

重写的规则

  • 参数列表必须相同。
  • 返回值类型可以不同,但必须是父类返回值的子类。
  • 重写的方法访问权限不能比父类成员方法的访问权限更低。
class Animal {
    public void run() {
        System.out.println("动物在跑");
    }
}
class Dog extends Animal{
    protected void run() { // error
        System.out.println("狗在跑");
    }
}
  • 父类的成员方法只能被它的子类重写。
  • final修饰的方法不能被重写。
  • static修饰的方法不能被重写,但是能够被再次声明。
  • 构造方法不能被重写。

方法重载

重载是在一个类中,方法名相同,参数列表不同(顺序不同、类型不同、个数不同)。

同一个类中多个合法的构造方法就构成方法重载。

重载和重写的区别

区别点重载重写
参数列表必须不同必须相同
返回类型可以不同必须相同
异常可以不同可以减少或删除,但不能抛出更多异常
访问可以不同可以放大,但一定不能缩小

向上转型

当我们有多个不同动物类:

class Animal {
    public void run() {
        System.out.println("动物在跑");
    }
}
class Dog extends Animal{
    public void run() {
        System.out.println("狗在跑");
    }
}
class Cat extends Animal {
    public void run() {
        System.out.println("猫在跑");
    }
}
class Bird extends Animal {
    public void run() {
        System.out.println("鸟在飞");
    }
}

向要调用一个方法,传入对象,调用该对象的run()方法。在没有多态的情况下,我们需要重载多个方法,比如:

public class AnimalTest {
    public static void func(Dog d) {
        d.run();
    }
    public static void func(Cat c) {
        c.run();
    }
    public static void func(Bird b) {
        b.run();
    }
    public static void func(Animal a) {
        a.run();
    }
    public static void main(String[] args) {}
}

这样未免太过麻烦。

在我们使用向上转型,就只需要写一个方法就可以了。

向上转型:创建一个子类对象,将其当成父类对象来使用。

public class AnimalTest {
    public static void func(Animal a) {
        a.run();
    }
    public static void main(String[] args) {
        Dog d = new Dog();
        Cat c = new Cat();
        Bird b = new Bird();
        func(d);
        func(c);
        func(b);
    }
}
// 输出结果:
// 狗在跑
// 猫在跑
// 鸟在飞

向下转型

  • 子类对象通过向上转型当成父类后,是无法调用子类特有的方法的。若想调用自己特有的方法,需要强制类型转换成子类对象,即向下转型。

向下转型是非常危险的,比如:狗类和猫类都继承于动物类。

Imgur

Dog类对象向上转型为Animal类,再向下转型为Cat类想调用mew()方法是无法办到的。

class Animal {
    public void run() {
        System.out.println("动物在跑");
    }
}
class Dog extends Animal{
    public void bark() {
        System.out.println("狗在叫");
    }
}
class Cat extends Animal {
    public void mew() {
        System.out.println("猫在叫");
    }
}

public class AnimalTest {
    public static void func(Animal a) {
        Cat c = (Cat)a;
        c.mew();
    }
    public static void main(String[] args) {
        func(new Dog());
    }
}
// Exception in thread "main" java.lang.ClassCastException: com.max.classes.Dog cannot be cast to com.max.classes.Cat
// 造成类型转换异常

因此我们需要使用关键字instanceof来判断是否属于该子类。

class Animal {
    public void run() {
        System.out.println("动物在跑");
    }
}
class Dog extends Animal{
    public void bark() {
        System.out.println("狗在叫");
    }
}
class Cat extends Animal {
    public void mew() {
        System.out.println("猫在叫");
    }
}

public class AnimalTest {
    public static void func(Animal a) {
        if(a instanceof Dog) {
            ((Dog) a).bark();
        } else if(a instanceof Cat) {
            ((Cat) a).mew();
        }
    }
    public static void main(String[] args) {
        func(new Dog());
    }
}
// 输出结果:狗在叫

多态的优缺点

  • 优点:
    • 消除类型之间的耦合关系。
    • 扩展能力强。
  • 缺点:
    • 代码运行效率降低。

抽象类

当我们定义一个动物类,编写一个吃饭的成员方法,但因为子类都有自己的吃饭动作,实现动物类的吃饭动作显得没有必要。

因此可以把动物类定义为一个抽象类,吃饭成员方法定义为抽象方法,等待被子类重写。

  • 使用abstract关键字来修饰抽象类或抽象方法。

注:抽象方法必须在抽象类当中,抽象类中可以有非抽象的成员方法。

abstract class Animal {
    public abstract void eat();
}
class Dog extends Animal{
    public void bark() {
        System.out.println("狗在叫");
    }

    @Override
    public void eat() {
        System.out.println("狗在啃骨头");
    }
}
class Cat extends Animal {
    public void mew() {
        System.out.println("猫在叫");
    }

    @Override
    public void eat() {
        System.out.println("猫在吃小鱼干");
    }
}

抽象类的特性

  • 抽象类不能实例化对象。
abstract class Animal {
    public abstract void eat();
}
public class AnimalTest {
    public static void main(String[] args) {
        Animal a = new Animal();
    }
}
// java: com.max.classes.Animal是抽象的; 无法实例化
  • 抽象方法不能用private修饰。
  • 抽象方法不能被finalstatic修饰。
    • final修饰的成员方法不能被重写,违背了抽象方法的特点。
    • static修饰的方法是类方法,为满足给类调用,类方法必须有方法体,这样就不是抽象方法了。
  • 抽象类必须被继承,且子类必须重写抽象方法。

接口

接口可类比于提供规范的载体。只在于提供规范,不在乎如何实现。

语法

public interface 接口名 {
  	规范;
}
public interface Animal {
    public abstract void eat();
    public abstract void sleep();
}

上述代码就是一个Animal接口。

当我们使用IDEA编写接口方法是,public abstract会变成浅色。

Imgur

这表明接口中的方法,默认是public abstract修饰的,因此可以不写。

接口的实现

当类实现接口时,需要重写接口中的所有方法。否则类必须声明为抽象类。

使用implements实现接口。

public interface Animal {
    void eat();
    void sleep();
}
class Dog implements Animal {

    @Override
    public void eat() {
        System.out.println("狗啃骨头");
    }

    @Override
    public void sleep() {
        System.out.println("狗睡觉");
    }
}

接口的特性

  • 不能实例化接口对象。
  • 接口中的方法是不能实现的,只能在实现类中实现。
  • 重写接口方法时,不能使用默认访问权限,只能是public修饰的,因为接口方法是public abstract
  • 接口中可以有变量,默认是public static final修饰的。

Imgur

  • 接口中不能有静态/构造代码块和构造方法。
  • jdk8中,接口还可以包含default方法。
public interface Animal {

    void eat();
    void sleep();
    default void sound() {
        System.out.println("动物发出声音");
    }
}

实现多个接口和接口的继承

Java中不支持多继承,但是可以实现多个接口。

interface A {
    void a();
}
interface B {
    void b();
}

class C implements A, B {

    @Override
    public void a() {
        System.out.println("a");
    }

    @Override
    public void b() {
        System.out.println("b");
    }
}
  • 实现多个接口的类,必须重写实现的多个接口的所有方法。否则就必须定义为抽象类。

  • 接口之间也可以使用extends继承,以达到复用的效果。

interface A {
    void a();
}
interface B extends A{
    void b();
}

抽象类和接口的区别

  • 抽象类中的方法可以实现,接口中的方法不能实现(jdk8引入的default方法除外)。
  • 抽象类中的成员变量权限可以是各种类型的,接口中的变量只能是public static final修饰的常量。
  • 接口中不能有静态/构造代码块,也不能有静态方法;抽象类中都可以有。
  • 一个类只能继承一个抽象类,但可以实现多个接口。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烛九_阴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值