Java面向对象的三大特征


之前介绍了面向对象的一些内容,今天来看下面向对象最重要的三个概念:封装、继承和多态。

封装

用洗衣机时,只需要按开关和调节模式就行,对于洗衣机的内部构造没有必要知道。这就是封装的体现。

概述

所谓的封装,就是把客观事物封装成抽象概念的类,并且类可以把自己的数据和方法只向可信的类或对象开放,向没必要开放的类或对象隐藏信息。

通俗的将,把该隐藏的隐藏起来,该暴露的暴露出来,这就是封装性的设计思想。

随着系统越来越复杂,类会越来越多,那么类之间的访问便捷必须要把握好,面向对象的开发原则要遵循高内聚、低耦合。高内聚是指类的内部数据操作细节自己完成,不允许外部干涉。低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。

封装的使用

实现封装就是控制类或成员的可见性范围,这就需要依赖访问控制修饰符,也称为权限修饰符来控制。

成员变量/属性私有化

私有化类的成员变量,提供公共的get和set方法,对外暴露获取和修改属性的功能。

public class Person {
    private String name; //使用private修饰成员变量
    private int age;
    private boolean marry;

    //提供getXxx方法/setXxx方法,可以访问成员变量
    public void setName(String n) {
	name = n;
    }
    public String getName() {
        return name;
    }
    public void setAge(int a) {
        age = a;
    }
    public int getAge() {
        return age;
    }
    public void setMarry(boolean m){
        marry = m;
    }
    public boolean isMarry(){
        return marry;
    }
}

public class PersonTest {
    public static void main(String[] args) {
        Person p = new Person();
        //实例变量私有化,跨类无法直接使用
        //p.name = "张三";
        //通过get/set方法使用
        p.setName("张三");
        System.out.println("p.name = " + p.getName());
    }
}

成员变量封装的好处:

  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法内加入控制逻辑,限制对成员变量的不合理访问,还可以进行数据检查,从而有利于保证对象信息的完整性
  • 便于修改,提高了代码的可维护性,主要说的是隐藏的部分,在内部修改了如果其对外可以访问的方式不变的话,外部是感觉不到它的修改的

私有化方法

示例:

//自定义操作数组的工具类
public class ArrayUtil {
    //数组的排序
    public void sort(int[] arr,String desc) {
        if("ascend".equals(desc)){
            for (int i = 0; i < arr.length - 1; i++) {
                for (int j = 0; j < arr.length - 1 - i; j++) {
                    if (arr[j] > arr[j + 1]) {
                        swap(arr,j,j+1); //调用私有方法
                    }
                }
            }
        } else if ("descend".equals(desc)){
            for (int i = 0; i < arr.length - 1; i++) {
                for (int j = 0; j < arr.length - 1 - i; j++) {
                    if (arr[j] < arr[j + 1]) {
                        swap(arr,j,j+1);
                    }
                }
            }
        } else{
            System.out.println("您输入的排序方式有误!");
        }
    }
	
    private void swap(int[] arr,int i,int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

注:开发中一般成员实例变量都习惯使用private修饰,再提供相应的public权限的get/set方法访问。对于final的实例变量,不提供set方法。对于static final的成员变量,习惯上使用public修饰。

继承

对于java中的继承,当多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,则多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成继承关系即可。

概述

继承的出现减少了代码的冗余,提高了代码的复用性。继承的出现更有利于功能的扩展。继承让类与类之间产生了is-a的关系,为多态的使用提供了前提。继承描述事物之间的所属关系,这种关系就是is-a的关系,表示父类更通用更一般,而子类更具体。

注意不要仅仅为了获取其他类中的某个功能而去继承。

继承的使用

语法格式:通过extends关键字可以声明一个类继承另一个类。

[修饰符] classB extendsA {
	...
}

其中类B称为子类、派生类,类A称为父类、超类、基类。

示例:

//定义动物类Animal,做为父类
package xyz.robofly.inherited.grammar;

public class Animal {
    String name; //定义name属性
    int age; //定义age属性
    //定义动物吃东西的方法
    public void eat() {
        System.out.println(age + "岁的" + name + "在吃东西");
    }
}

//定义猫类Cat作为子类继承动物类Animal
package xyz.robofly.inherited.grammar;

public class Cat extends Animal {
    int count; //记录每只猫抓的老鼠数量
    //定义猫抓老鼠的方法catchMouse
    public void catchMouse() {
        count++;
        System.out.println("抓老鼠,已经抓了" + count + "只老鼠");
    }
}

//测试类
package xyz.robofly.inherited.grammar;

public class TestCat {
    public static void main(String[] args) {
        Cat cat = new Cat(); //创建一个猫类对象
        cat.name = "Tom"; //为该猫类对象的name属性进行赋值
        cat.age = 2; //为该猫类对象的age属性进行赋值
        cat.eat(); //调用该猫继承来的eat()方法
        cat.catchMouse(); //调用该猫的catchMouse()方法
    }
}
  1. 子类会继承父类所有的实例变量和实例方法。
    从定义看类是一类具有相同特性的事物的抽象描述,父类是所有子类共同特征的抽象概述,而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。
    • 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存
    • 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没有找到会看它的父类甚至父类的父类是否声明了这个方法,遵循从下往上找的顺序,找到了就停止,一直到根父类都没有就会报编译错误

所以继承意味着子类的对象除了看子类的类模板外还要看父类的类模板。

  1. 子类不能直接访问父类中私有的成员变量和方法。
    子类虽然会继承父类私有的成员变量,但子类不能对继承的私有成员变量直接进行访问,可通过继承到的get/set方法进行访问。

  2. 在java中继承的关键字用的是extends,即子类不是父类的子集,而是对父类的扩展。
    子类在继承父类后,还可以定义自己特有的方法,这就可以看做是对父类功能上的扩展。

  3. java支持多层继承。
    比如类A是父类,类B继承类A,类C继承类B,这就是个多层继承,子类和父类是一种相对的概念。Object类是顶层父类,所有的类默认继承Object作为父类。

  4. 一个父类可以同时拥有多个子类。
    比如类A可以被类B类C类D等同时继承。

  5. java只支持单继承,不支持多重继承。
    一个类只能有一个父类,不可以有多个直接父类。

多态

一千个读者眼中有一千个哈姆雷特。

概述

多态性是面向对象中的重要概念,在java中的体现为:父类的引用指向子类的对象

java引用变量有两个类型,编译时类型和运行时类型,编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定,简称编译时看左边运行时看右边。

若编译时类型和运行时类型不一致,就出现了对象的多态性。多态情况下,看左边看的是父类的引用(父类中不具备子类特有的方法),看右边看的是子类的对象(实际运行的是子类重写父类的方法)。

多态使用的前提,类的继承关系和方法的重写。

为什么需要多态:开发中有时在设计一个数组或一个成员变量或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型,这种就需要用到多态。

多态的好处:变量引用的子类对象不同,执行的方法就不同,实现了动态绑定,使得代码编写更灵活功能更强大,可维护性和扩展性更好。

多态的弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法(编译时为父类类型,没有子类扩展的属性和方法,用多态后再访问扩展的就会报错)。

开发中使用父类做方法的形参,是多态使用最多的场合,即使增加了新子类方法也不用改变,提高了扩展性,符合开闭原则(开闭原则:对扩展开放对修改关闭,通俗就是软件系统中的各种组件,如模块、类以及功能等,应该在不修改现有代码的基础上来引入新功能)。

多态的使用

语法格式:父类类型指子类继承的父类类型,或者实现的接口类型。

父类类型 变量名 = 子类对象;

示例:

Person p = new Student();
Object o = new Person(); //Object类型的变量o,指向Person类型的对象
o = new Student(); //Object类型的变量o,指向Student类型的对象

对象的多态在java中,子类的对象可以替代父类的对象使用,所以一个引用类型变量可能指向(引用)多种不同类型的对象。

多态示例:

//父类
package xyz.robofly.polymorphism.grammar;

public class Pet {
    private String nickname;
    public String getNickname() {
        return nickname;
    }
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    public void eat(){
        System.out.println(nickname + "吃东西");
    }
}
//子类
package xyz.robofly.polymorphism.grammar;

public class Cat extends Pet {
    //子类重写父类的方法
    @Override
    public void eat() {
        System.out.println("猫咪" + getNickname() + "吃鱼仔");
    }
    //子类扩展的方法
    public void catchMouse() {
        System.out.println("抓老鼠");
    }
}
//子类
package xyz.robofly.polymorphism.grammar;

public class Dog extends Pet {
    //子类重写父类的方法
    @Override
    public void eat() {
        System.out.println("狗子" + getNickname() + "啃骨头");
    }
    //子类扩展的方法
    public void watchHouse() {
        System.out.println("看家");
    }
}
  1. 方法内局部变量的赋值体现多态。
package xyz.robofly.polymorphism.grammar;

public class TestPet {
    public static void main(String[] args) {
        //多态引用
        Pet pet = new Dog();
        pet.setNickname("小白");
        //多态的表现形式
        //编译时看父类,只能调用父类声明的方法,不能调用子类扩展的方法
        //运行时看子类,如果子类重写了方法,一定是执行子类重写的方法体
        pet.eat(); //运行时执行子类Dog重写的方法
        //pet.watchHouse(); //不能调用Dog子类扩展的方法
        pet = new Cat();
        pet.setNickname("雪球");
        pet.eat(); //运行时执行子类Cat重写的方法
    }
}
  1. 方法的形参声明体现多态。
package xyz.robofly.polymorphism.grammar;

public class Person{
    private Pet pet;
    public void adopt(Pet pet) { //形参是父类类型,实参是子类对象
        this.pet = pet;
    }
    public void feed(){
        pet.eat(); //pet实际引用的对象类型不同,执行的eat方法也不同
    }
}

package xyz.robofly.polymorphism.grammar;

public class TestPerson {
    public static void main(String[] args) {
        Person person = new Person();
        Dog dog = new Dog();
        dog.setNickname("小白");
        person.adopt(dog); //实参是dog子类对象,形参是父类Pet类型
        person.feed();
        Cat cat = new Cat();
        cat.setNickname("雪球");
        person.adopt(cat); //实参是cat子类对象,形参是父类Pet类型
        person.feed();
    }
}
  1. 方法的返回值类型体现多态。
package xyz.robofly.polymorphism.grammar;

public class PetShop {
    //返回值类型是父类类型,实际返回的是子类对象
    public Pet sale(String type){
        switch (type){
            case "Dog":
                return new Dog();
            case "Cat":
                return new Cat();
        }
        return null;
    }
}

package xyz.robofly.polymorphism.grammar;

public class TestPetShop {
    public static void main(String[] args) {
        PetShop shop = new PetShop();
        Pet dog = shop.sale("Dog");
        dog.setNickname("小白");
        dog.eat();
        Pet cat = shop.sale("Cat");
        cat.setNickname("雪球");
        cat.eat();
    }
}

虚方法的调用

在java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。

如下:

Person e = new Student();
e.getInfo(); //调用Student类的getInfo()方法

子类中定义了与父类同名同参的方法,在多态下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法,这样的方法调用在编译期是无法确定的。

扩展:

  • 静态链接(早期绑定):当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接,那么调用这样的方法就称为非虚方法调用。比如调用静态方法、私有方法、final方法、父类构造器、本类重载构造器等
  • 动态链接(晚期绑定):如果被调用的方法在编译期无法被确定下来,也就是只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接,调用这样的方法就称为虚方法调用。比如调用重写的方法(针对父类)、实现的方法(针对接口)

成员变量没有多态性

若子类重写父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量。

向上转型和向下转型

首先一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变,即这个对象的运行时类型,本质的类型永远不会变。但是把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。

用多态就一定会有把子类对象赋值给父类变量的时候,这个时候在编译期间就会出现类型转换的现象。但是使用父类变量接收了子类对象之后,就不能调用子类拥有而父类没有的方法了。这也是多态带来的一点小麻烦。所以想要调用子类特有的方法,就必须做类型转换,使得编译通过。

  1. 向上转型:当左边的变量的类型(父类)>右边对象/变量的类型(子类),就称为向上转型。
    • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法
    • 但是运行时仍然是对象本身的类型,所以执行的方法是子类重写的方法体
    • 此时一定是安全的,而且也是自动完成的
  2. 向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),就称为向下转型。
    • 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法
    • 但是运行时仍然是对象本身的类型
    • 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全可以通过isInstanceof关键字进行判断

示例:向上转型自动完成,向下转型(子类类型)父类变量。

package xyz.robofly.polymorphism.grammar;

public class ClassCastTest {
    public static void main(String[] args) {
        //没有类型转换
        Dog dog = new Dog(); //dog的编译时类型和运行时类型都是Dog
        //向上转型
        Pet pet = new Dog(); //pet的编译时类型是Pet,运行时类型是Dog
        pet.setNickname("小白");
        pet.eat(); //可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
        //pet.watchHouse(); //不能调用父类没有的方法watchHouse
        Dog d = (Dog) pet;
        System.out.println("d.nickname = " + d.getNickname());
        d.eat(); //可以调用eat方法
        d.watchHouse(); //可以调用子类扩展的方法watchHouse
        Cat c = (Cat) pet; //编译通过,从语法检查来说pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
    }
}

为了避免ClassCastException的发生,java提供了instanceof关键字,给引用变量做类型的校验。

语法格式:

//检验对象a是否是数据类型A的对象,返回值为boolean
对象a instanceof 数据类型A 
  • 只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常
  • 如果对象a属于类A的子类B,a ClassCastException A的值也为true
  • 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误

示例:

package xyz.robofly.polymorphism.grammar;

public class TestInstanceof {
    public static void main(String[] args) {
        Pet[] pets = new Pet[2];
        pets[0] = new Dog(); //多态引用
        pets[0].setNickname("小白");
        pets[1] = new Cat(); //多态引用
        pets[1].setNickname("雪球");
        for (int i = 0; i < pets.length; i++) {
            pets[i].eat();
            if(pets[i] instanceof Dog){
                Dog dog = (Dog) pets[i];
                dog.watchHouse();
            }else if(pets[i] instanceof Cat){
                Cat cat = (Cat) pets[i];
                cat.catchMouse();
            }
        }
    }
}

最后,今天的内容就到这里,Java教程持续更新中,喜欢的话点个关注吧,下篇见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

码农高飞

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

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

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

打赏作者

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

抵扣说明:

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

余额充值