面向对象三大特征

发布于:2022-11-28 ⋅ 阅读:(156) ⋅ 点赞:(0)

  一、继承

5ea52b8478744bcbb80222f15c1fd3d4.png

 继承是面向对象编程的三大特征之一。继承让我们更加容易实现类的扩展。实现代码的重用,不用再重新发明轮子(don't  reinvent wheels)。

        继承有两个主要的作用:

① 代码重复,更加容易实现类的扩展

② 方便建模

继承的实现

从英文字面意思理解,extends的意思是"扩展"。子类是父类的扩展。现实世界中的继承无处不在。比如:

612c7e9a5200400091fea49573120486.png

上图中,哺乳动物继承了动物,意味着,动物的特性,哺乳动物都有;在编程中,如果新定义一个Student类,发现已经有Person类包含了我们需要的属性和方法,那么Student类只需要继承Person类即可拥有Person类的属性和方法。

【eg】使用extends实现继承

package com.wang.oop;

/**
 *  测试继承
 */
public class TestExtends{
    public static void main(String[] args){
        Student1 s1 = new Student1("张三","183","C++");
    System.out.println(s1 instanceof Student1);
    System.out.println(s1 instanceof  Person1);
    }
}

class Person1 {
    String name;
    int height;
    public void rest(){System.out.println("休息!");}
}

calss Student1 extends Person1 {
    String major;  //专业
    public void study(){System.out.println("在家里,学习C++");}
    public Student1(String name,int height,String major){
    //天然拥有父类的属性
    this.name = name;
    this.height = height;
    this.major = major;
    }
}

 执行结果:

c3a11ad772d34d06b363e63f7fb16285.png

 instanceof 运算符

instanceof 是二元运算符,左边是对象,右边是类;当对象是右边类或子类所创建对象时,返回true;否则,返回false.比如:

【eg】使用 instanceof 运算符类进行类型判断

public class Test{
    public static void main(String[] args){
        Student s = new Student("张三","187","java");

        System.out.println(s instanceof Person);
        System.out.println(s instanceof Student);
    }
}

两条语句的输出结果都是 true

继承使用要点

① 父类也称作超类、基类。子类:派生类等

② Java 中只有单继承,没有像C++那样的多继承。多继承会引起混乱,使得继承链过于复杂,系统难于维护。

③ Java 中类没有多继承,接口有多继承。

④ 子类继承父类,可以得到父类的全部属性和方法(除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。

 

  方法重写 (override)

91d5b8d65fff47a38250952d35ae32c5.png

子类重写父类的方法,可以用自身行为替换父类行为。重写是实现多态的必要条件。

方法重写需要符合下面的三个要点: 

①  ==  : 方法名、形参列表相同

②  ≤  :返回值类型和声明异常类型,子类小于等于父类

③  ≥  :访问权限,子类大于等于父类

【eg】方法重写

package com.wang.oop;

/**
 * 测试方法的重写
 */
public class TestOverride{
    public static void main(String[] args){
        Horse h = new Horse();
        Plane p = new Plane();
        h.run();
        h.getVehicle();
        p.run();
    }
}


class Vehicle{  //交通工具类
    public void run(){
        System.out.println("跑....")
}

public Vehicle getVehicle(){
    System.out.println("给你一个交通工具!");
    return null;
    }
}

class Horse extends Vehicle {  //马也是交通工具

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

    @Override
public Horse getVehicle(){
    return new Horse();
    }
}

class Plane extends Vehicle{
    @Override
    public void run(){
        System.out.println("天上飞....")
    }
}

final 关键字

final关键字的作用

 ● 修饰变量: 被他修饰的变量不可改变,一旦赋了初值,就不能被重新赋值

        final int MAX_SPEED  = 120;

 ●  修饰方法:该方法不可被子类重写。但是可以被重载!

        final void study(){}

 ●  修饰类:修饰的类不能被继承。比如:Math、String等

        final class A {}

17f0ef8e61004ebfa67d86fcb1917428.png

 

final 修饰类如图所示

a350c82c6b7148349ac638d6db1fa18d.png

 继承和组合

16abab201167451f87490022fbe63a72.png

结婚就是一种组合。两人组合后,可以复用对方的属性和方法!

除了继承," 组合 " 也能实现代码的复用!" 组合 " 核心是" 将父类对象作为子类的属性 "。

【eg】继承的代码用组合重新实现

public class Test{
    public static void main(String[] args){
        Student s = new Student("张三",183,"Java");
        s.person.rest();  //s.rest();
        s.study();
    }
}


class Person{
    String name;
    int height;
    public void rest(){
    System.out.println(" 休息一会!");
    }
}

class Student /*extends Person*/{
    Person person = new Person();
    String major;  //专业

  public Student(String name,int height,String major){
    //拥有父类的对象,通过这个对象间接拥有它的属性和方法
    this.person.name = name; //this.name = name;
    this.person.height = height; //this.height = height;
    this.person.rest();


    this.major = major;

    }
 
}

 组合比较灵活。继承只能有一个父类,但是组合可以有多个属性。所以,有人声称"组合优于继承,开发中可以不用继承",但是,不建议走极端。

对于 is - a 关系建议使用继承,has - a 关系建议使用组合。

比如:上面的例子,Student is a Person 这个逻辑没问题,但是:Student has a Person 就有问题了。这时候,显然继承关系比较合适

再比如:笔记本和芯片的关系显然是 has - a 关系,使用组合更好。 

Object类详解

81641a7b0c8a4dc0bffae1b137418464.png

所有类都是Object类的子类,也都具备Object类的所有特性

Object类的基本特性 

①   Object类是所有类的父类,所有Java对象都拥有Object类的属性和方法

②   如果在类的声明中未使用extends,则默认继承Objece类

936f70a0095946eba69b1204df0af0f4.png

【eg】Object类

public class Pesrson{
    ...
  }

//等价于:
public class Person extends Object{
    ...
  }  

 toString方法

Object类中定义有public String toString()方法,其返回值是String 类型。Object类中toString 方法的源码为:

public String toString(){
    return getClass().getName() + "@" + Integer.
toHexString(hashCode());
    }

根据如上源码得知,默认会返回"类名 + @ +16 进制的hashcode ",在打印输出或者用字符串连接对象时,会自动调节该对象的 toString方法

【eg】重写toString()方法

class Person{
    String name;
    int age;
    @Override
    public String toString(){
        return name + ",年龄: " + age;
    }
}
public class Test{
    public static void main(String[] args){
        Person p = new Person();
        p.age = 20;
        p.name = "李东";
        System.out.println("info: " + p);


        Test t = new Test();
        System.out.println(t);
    }
}

执行结果如图所示:

e63fbf51fc0e4bf2a09e4c009e86c6ec.png

 

== 和 equals 方法

eafc73a8c5db4854a2f781d9c13bcd5a.png

== 代表比较双方是否相同,如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。 

equals()提供定义" 对象内容相等 " 的逻辑。比如,我们在公安系统中认为id相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。

equals()默认是比较两个对象的hashcode,但,可以根据自己的要求重写equals方法

【eg】自定义类重写equals()方法

public class TestEquals{
    public static void main(String[] args){
        Person p1 = new Person(123,"张三");
        Person p2 = new Person(123,"李四");
        System.out.println(p1 == p2);  //false,不是同一个对象
        System.out.println(p1.equals(p2));//true,id相同则认为两个对象内容相同
            String s1 = new String("北京");
            String s2 = new String("北京");
        System.out.println(s1 == s2);  //fals,两个字符串不是同一个对象
        System.out.println(s1.equals(s2)); //true,两个字符串内容相同
    }
}


class Person {
    int id;
    String name;
    public Person(int id,String name){
        this.id = id;
        this,name = name;
    }


    public boolean equals(Object obj){
        if(obj == null){
            return false;
        }else{
            if(obj instance Person){
                Person c = (Person)obj;
                if(c.id == this.id){
                    return true;
            }
        }
    }
            return false;
    }
}      

super关键字

de588eeb75424a488ca8a6404eed0209.png

① super  " 可以看做 " 是直接父类对象的引用。可通过 super 来访问父类中被子类覆盖的方法或属性。

② 使用 super 调用普通方法,语句没有位置限制,可以在子类中随便调用。

③ 在一个类中,若是构造方法的第一行没有调用 super (...)或者 this (..);那么Java默认都会调用super(),含义是调用父类的无参数构造方法

【eg】super 关键字的使用

public class TetsSuper01{
    public static void main(String[] args){
        new ChildClass().f();
        }
    }
class FatherClass{
    public int value;
    public void f(){
        super.f(); //调用父类的普通方法
        value = 200;
        System.out.println("ChildClass.value = " + value);
        System.out.println(value);
        System.out.println(super.value); //调用父类的成员变量
    }

public void f2(){
        System.out.println(age); 
        }
    }

 执行结果如图所示:

fa2c3d457141482a9ce0ec93be9db115.png

继承树追溯

eb38f0b39b7d43be957a050f37add697.png

属性 / 方法查找顺序:(比如:查找变量h) 

● 查找当前类中有没有属性 h

● 依次上溯每个父类,查看每个父类中是否有 h,知道 Object

● 如果没找到,则出现编译错误

● 上面步骤,只要找到 h 变量,则这个过程终止

 

构造方法调用顺序:

构造方法第一句总是:super(...)来调用父类对应的构造方法,所以,流程就是:先向上追溯到Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。

注:静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。

【eg】继承条件下构造方法是执行过程

public class TestSuper02{
    public static void main(String[] args){
        System.out.println("开始创建一个ChildClass对象......")
        new ChildClass();
        }
    }

calss FatherClass{
    public FatherClass(){
        System.out.println("创建FatherClass");
        }
    }

class ChildClass extends FatherClass{
    public ChildClass(){
        System.out.println("创建ChildClass");
        }
    }

 执行结果如图所示:

08d6f75a38cd4afe8b4706ef07a466ce.png

 

 

二、封装(encapsulation)

c16e9bcfe28f4ad4acf5ef2f9bf99d69.png

 

封装的作用和含义

        我要看电视,只需要按一下开关和换台就可以了。有必要了解电视剧内部的结构吗?有必要碰碰显像管吗?制造厂家为了方便我们使用电视,把复杂的内部细节全部封装起来,只给我们暴露简单的接口。

        我们程序设计要追求"高内聚,低耦合"。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。

 

编程中封装的具体优点:

提高代码的安全性

提高代码的复用性

"高内聚":封装细节,便于修改内部代码,提高可维护性。

"低耦合":简化外部调用,便于调用者使用,便于扩展和协作。

 

封装的实现————使用访问控制符

Java 是使用访问控制符来控制哪些细节需要封装,哪些细节需要暴露的。

Java 中4种访问控制符分别为private、default、protected、public

访问权限修饰符

修饰符

 

同一个类

      

同一个包中 子类 所有类
private      
default    
protected  
public

 

8330b2c5be3e4e7ea6d3e6f6ccc22baa.png

 

【Note】关于protected的两个细节

① 若父类和子类在同一个包中,子类可访问父类的protected成员,也可访问父类对象的protected成员。

② 若子类和父类不在同一个包中,子类可访问父类的protected成员,不能访问父类对象的protected成员。

封装的使用细节

cf4a51af9bce4d62ba87969618c594f9.png

 

开发中封装的简单规则:

● 属性一般使用private访问权限

属性私有后,提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)

方法:一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰

【eg】JavaBean的封装演示

public class Person{
    //属性一般使用private修饰
    private String name;
    private int age;
    private boolean flag;
    //为属性提供public修饰的set/get方法
    public String getName(){
        return name;
    }
    
public void setName(String name){
    this.name = name;
}
public int getAge(){
    return age;
}
public void SetAge(int age){
    this.age = age;
}
public boolean isFlag(){//注意:boolean类型的属性get方法是is开头的
    return flag;
}
public void setFlag(boolean flag){
    this.flag = flag;
    }
}

【eg】封装的使用

class Person {
    private String name;
    private int age;
    public Person() {


    }
    public Person(String name, int age) {
        this.name = name;
        // this.age = age;//构造方法中不能直接赋值,应该调用setAge方法
        setAge(age);
    }
    
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setAge(int age) {
        //在赋值之前先判断年龄是否合法
        if (age > 130 || age < 0) {
            this.age = 18;//不合法赋默认值18
        } else {
            this.age = age;//合法才能赋值给属性age
        }
    }
    public int getAge() {
        return age;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}


public class Test2 {
    public static void main(String[ ] args) {
        Person p1 = new Person();
        //p1.name = "小红"; //编译错误
        //p1.age = -45;  //编译错误
        p1.setName("小红");
        p1.setAge(-45);
        System.out.println(p1);
        
        Person p2 = new Person("小白", 300);
        System.out.println(p2);
    }
}

执行结果:

319fe875127a44cd82a2e390a2c9c831.png

 

 

三、多态(polymorphism)

3551598f7f72426abd349e293964346c.png

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。 现实生活中,同一个方法,具体实现会完全不同。比如:同样是调用人"吃饭"的方法,中国人用筷子吃饭,英国人用叉吃饭,印度人用手吃饭。

多态的要点:

① 多态是方法的多态,不是属性的多态(多态与属性无关)

② 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象

③ 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了

【eg】多态和类型转换

class Animal {
    public void shout() {
        System.out.println("叫了一声!");
    }
}
class Dog extends Animal {
    public void shout() {
        System.out.println("旺旺旺!");
    }
    public void seeDoor() {
        System.out.println("看门中....");
    }
}
class Cat extends Animal {
    public void shout() {
        System.out.println("喵喵喵喵!");
    }
}
public class TestPolym {
    public static void main(String[ ] args) {
        Animal a1 = new Cat(); // 向上可以自动转型
        //传的具体是哪一个类就调用哪一个类的方法。大大提高了程序的可扩展性。
        animalCry(a1);
        Animal a2 = new Dog();
        animalCry(a2);//a2为编译类型,Dog对象才是运行时类型。
        
        /*编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
         * 否则通不过编译器的检查。*/
        Dog dog = (Dog)a2;//向下需要强制类型转换
        dog.seeDoor();
    }


    // 有了多态,只需要让增加的这个类继承Animal类就可以了。
    static void animalCry(Animal a) {
        a.shout();
    }


    /* 如果没有多态,我们这里需要写很多重载的方法。
     * 每增加一种动物,就需要重载一种动物的喊叫方法。非常麻烦。
    static void animalCry(Dog d) {
        d.shout();
    }
    static void animalCry(Cat c) {
        c.shout();
    }*/
}


执行结果:

018e69e2f1514a76a9ab73d57e353a16.png

如上eg,这是多态最为多见的一种方法,即父类引用做方法的形参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。

由此,我们可以看出多态的主要优势是提高了代码的可扩展性。但是多态也有弊端,就是无法调用子类特有的功能,比如,我不能使用父类的引用变量调用Dog类特有的seeDoor() 方法。

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。 比如:同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。

多态的要点:

① 多态是方法的多态,不是属性的多态(多态与属性无关)。

② 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。

③ 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。

对象的转型

a8e6cbcff42c468b85edb8cbfaae8254.png

① 父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。

② 向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行类型的强制转换,我们称之为向下转型。

 【eg】对象的转型

public class TestCasting {
    public static void main(String[ ] args) {
        Object obj = new String("北京"); // 向上可以自动转型
        // obj.charAt(0) 无法调用。编译器认为obj是Object类型而不是String类型
        /* 编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
         * 不然通不过编译器的检查。 */
        String str = (String) obj; // 向下转型
        System.out.println(str.charAt(0)); // 位于0索引位置的字符
        System.out.println(obj == str); // true.他们俩运行时是同一个对象
    }
}

执行结果:

bb1dcda6817044f4836365ae1147e880.png

在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常ClassCastException。如下所示。

【eg】类型转换异常

public class TestCasting2 {
    public static void main(String[ ] args) {
        Object obj = new String("北京");
        //真实的子类类型是String,但是此处向下转型为StringBuffer
        StringBuffer str = (StringBuffer) obj;
        System.out.println(str.charAt(0));
    }
}

执行结果:

edb0d4c9a766ddb0545a95756fc14dd9.png

为了避免出现这种异常,我们可以使用instanceof运算符进行判断。

【eg】向下转型中使用instanceof

public class TestCasting3 {
    public static void main(String[ ] args) {
        Object obj = new String("北京");
        if(obj instanceof String){
            String str = (String)obj;
            System.out.println(str.charAt(0));
        }else if(obj instanceof StringBuffer){
            StringBuffer str = (StringBuffer) obj;
            System.out.println(str.charAt(0));
        }
    }
}

 

 


网站公告

今日签到

点亮在社区的每一天
去签到