面向对象 3

本章重点针对面向对象编程的三大特征:继承、封装、多态进行详细的讲解。另外还包 括抽象类、接口、内部类等概念。很多概念对于初学者来说,更多的是先进行语法性质的了 解。不要期望,通过本章学习就“搞透面向对象编程”。本章只是面向对象编程的起点,后 面所有的章节说白了都是对面向对象这一章的应用。

1 继承

继承是面向对象编程的三大特征之一,它让我们更加容易实现对于已有类的扩展、更加 容易实现对于现实世界的建模。 继承有两个主要作用:
1.代码复用,更加容易实现类的扩展 。
2.方便建模。

1.1 继承的实现

继承让我们更加容易实现类的扩展。 比如,我们定义了人类,再定义 Boy 类就只需要 扩展人类即可。实现了代码的重用,不用再重新发明轮子(don’t reinvent wheels)。 从英文字面意思理解,extends 的意思是“扩展”。子类是父类的扩展。现实世界中的 继承无处不在。比如:在这里插入图片描述
上图中,哺乳动物继承了动物。意味着,动物的特性,哺乳动物都有;在我们编程中,如果 新定义一个 Student 类,发现已经有 Person类包含了我们需要的属性和方法,那么 Student 类只需要继承 Person 类即可拥有 Person 类的属性和方法。
使用 extends 实现继承的代码如下:

package com.txw.test;

public class Test {
    public static void main(String[] args) {
        Student student =new Student("Adair",160,"java");
        student.rest();
        student.study();
    }
}
class Person{
    String name;
    int height;
    public void rest(){
        System.out.println("休息一会!");
    }
}
class Student extends Person{
    String major; // 专业
    public void study(){
        System.out.println("在CSDN学习Java!");
    }

    public Student( String name, int height,String major) {
        // 天然拥有父类的属性
        this.name = name;
        this.height =height;
        this.major = major;
    }
}

1.2 instanceof 运算符

instanceof 是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对 象时,返回 true;否则,返回 false。
使用instanceof运算符进行类型判断的代码如下:

package com.txw.test;

public class Test {
    public static void main(String[] args) {
        Student student =new Student("Adair",160,"java");
        System.out.println(student instanceof  Person);
        System.out.println(student instanceof  Student);
    }
}
class Person{
    String name;
    int height;
    public void rest(){
        System.out.println("休息一会!");
    }
}
class Student extends Person{
    String major; // 专业
    public void study(){
        System.out.println("在CSDN学习Java!");
    }

    public Student( String name, int height,String major) {
        // 天然拥有父类的属性
        this.name = name;
        this.height =height;
        this.major = major;
    }
}

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

1.3 继承使用要点

1.父类也称作超类、基类。子类:派生类等。
2.Java 中只有单继承,没有像 C++那样的多继承。多继承会引起混乱,使得继承链 过于复杂,系统难于维护。
3.Java 中类没有多继承,接口有多继承。
4.子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。
5.如果定义一个类时,没有调用 extends,则它的父类是:java.lang.Object。

1.4 方法的重写 override

子类通过重写父类的方法,可以用自身的行为替换父类的行为。方法的重写是实现多态 的必要条件。
方法的重写需要符合下面的三个要点:
1.“= =”: 方法名、形参列表相同。
2.“≤”:返回值类型和声明异常类型,子类小于等于父类。
3.“≥”: 访问权限,子类大于等于父类。
方法重写的代码如下:

package com.txw.test;

public class TestOverride {
    public static void main(String[] args) {
        Vehicle v1 = new Vehicle();
        Vehicle v2 = new Horse();
        Vehicle v3 = new Plane();
        v1.run();
        v2.run();
        v3.run();
        v2.stop();
        v3.stop();
    }
}
// 交通工具类
class Vehicle{
    public void run(){
        System.out.println("跑...");
    }
    
    public void stop() {
        System.out.println("停止不动");
    }
}
// 马也是交通工具
class Horse extends Vehicle{
    // 重写父类的方法
    public void run() {
        System.out.println("四蹄翻飞,嘚嘚嘚...");
    }
}
class Plane extends Vehicle {
    // 重写父类方法
    public void run() { 
        System.out.println("天上飞!"); 
    }
    
    public void stop() { 
        System.out.println("空中不能停,坠毁了!");
    }
}

1.5 final 关键字

final 关键字的作用:
1.修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。

final int MAX_SPEED = 120; 

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

 final void shout(){} 

如图所示:
在这里插入图片描述

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

  final class Dog {}

如图所示:在这里插入图片描述

1.6 继承和组合

我们可以通过继承方便的复用已经定义类的代码。还有一种方式,也可以方便的实现“代 码复用”,那就是:“组合”。
“组合”不同于继承,更加灵活。 “组合”的核心就是“将父类对象作为子类的属性”,然后,“子类通过调用这个属性 来获得父类的属性和方法”。
我们把继承第一节课中的代码,使用组合来实现一遍。代码如下:

package com.txw.test;

public class Test{
   public static void main(String[] args) {
      Student s = new Student("Adair",160,"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 void study(){
      System.out.println("在CSDN学习Java!");
   }

   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”关系,使用组合更好。

2 Object 类详解

我们学习的所有类、我们以后定义的所有类都是 Object 类的子类,也都具备 Object 类的所有特性。因此,我们非常有必要掌握 Object 类的用法。

2.1 Object 类基本特性

Object 类是所有 Java 类的根基类,也就意味着所有的 Java 对象都拥有 Object 类的属 性和方法。如果在类的声明中未使用 extends 关键字指明其父类,则默认继承 Object 类。
Object 类的代码如下:

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

补:IDEA 部分快捷键
IDEA 快捷键和相关操作:
1.类的结构视图:alt+7
2.看类的源码:ctrl+左键
3. 查看类的关系:鼠标放到类名上,右键–>Diagram–>show Diagram
4.自动生成构造器、get、set 方法、equals 等:alt+insert
5. 鼠标悬停显示提示(重新设置:File–>Setting–>Editor–>general):
6.快捷输出常见字符串:

a) main public static void main(String[] args){}
b) sout System.out.println(); 
c) soutm System.out.println(“描述:所在类中的,所在方法”);

2.2 toString 方法

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

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

根据如上源码得知,默认会返回“类名+@+16 进制的 hashcode”。在打印输出或者 用字符串连接对象时,会自动调用该对象的 toString()方法。
重写 toString()方法的代码如下:

package com.txw.test;

class  Person{
    String name;
    int age;

    @Override
    public String toString() {
        return name + ",年龄:" + age;
    }
}
public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        person.age = 25;
        person.name = "Adair";
        System.out.println("info: " + person);
        Test test = new Test();
        System.out.println(test);
    }
}

执行结果如图所示:在这里插入图片描述

2.3 ==和 equals 方法

“==”代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表 示地址相等即是同一个对象。
Object 类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容 相等”的逻辑。比如,我们在公安系统中认为 id 相同的人就是同一个人、学籍系统中认为 学号相同的人就是同一个人。
Object 的 equals 方法默认就是比较两个对象的 hashcode,是同一个对象的引用时 返回 true 否则返回 false。但是,我们可以根据我们自己的要求重写 equals 方法。
自定义类重写 equals()方法的代码如下:

package com.txw.test;

import java.util.Objects;
class  Person{
    int id;
    String name;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id && Objects.equals(name, person.name);
    }
}
public class TestEquals {
    public static void main(String[] args) {
        Person p1 = new Person(123,"Adair");
        Person p2 = new Person(123,"Adair6");
        System.out.println(p1==p2); // false,不是同一个对象
        System.out.println(p1.equals(p2)); // false,不是同一个对象
        String s1 = new String("学无止路");
        String s2 = new String("学无止路");
        System.out.println(s1==s2);		// false, 两个字符串不是同一个对象
        System.out.println(s1.equals(s2)); // true, 两个字符串内容相同
    }
}

JDK 提供的一些类,如 String、Date、包装类等,重写了 Object 的 equals 方法,调 用这些类的 equals 方法, x.equals (y) ,当 x 和 y 所引用的对象是同一类对象且属性内容 相等时(并不一定是相同对象),返回 true 否则返回 false。

2.4 super 关键字

super“可以看做”是直接父类对象的引用。可以通过 super 来访问父类中被子类覆盖 的方法或属性。
使用 super 调用普通方法,语句没有位置限制,可以在子类中随便调用。
在一个类中,若是构造方法的第一行代码没有显式的调用 super(…)或者 this(…);那么 Java 默认都会调用 super(),含义是调用父类的无参数构造方法。这里的 super()可以省略。
super 关键字的使用代码如下:

package com.txw.test;

class FatherClass{
    public int value;
    public void f(){
        value = 100;
        System.out.println("FatherClass.value" + value);
    }
}
class ChildClass extends 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 class TestSuper01 {
    public static void main(String[] args) {
        new ChildClass().f();
    }
}

执行结果为如图所示:在这里插入图片描述

2.6 继承树追溯

属性/方法查找顺序:(比如:查找变量 h)
1.查找当前类中有没有属性 h
2.依次上溯每个父类,查看每个父类中是否有 h,直到 Object
3.如果没找到,则出现编译错误。
4.上面步骤,只要找到 h 变量,则这个过程终止。
·构造方法调用顺序
构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上 追溯到 Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。
注:静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。
继承条件下构造方法的执行过程的代码如下:

package com.txw.test;

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

执行为如图所示:在这里插入图片描述

3 封装(encapsulation)

封装是面向对象三大特征之一。对于程序合理的封装让外部调用更加方便,更加利于写 作。同时,对于实现者来说也更加容易修正和改版代码。

3.1 封装的作用和含义

我要看电视,只需要按一下开关和换台就可以了。有必要了解电视机内部的结构吗?有 必要碰碰显像管吗?制造厂家为了方便我们使用电视,把复杂的内部细节全部封装起来,只 给我们暴露简单的接口,比如:电源开关。具体内部是怎么实现的,我们不需要操心。
需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。说的 专业一点,封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部 实现细节。
我们程序设计要追求“高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成, 不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。
编程中封装的具体优点:
1.提高代码的安全性。
2.提高代码的复用性。
3. “高内聚”:封装细节,便于修改内部代码,提高可维护性。
4.“低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。
未进行封装的代码演示:

package com.txw.test;

class Person{
    String name;
    int age;
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}
public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        person.name ="Adair";
        person.age = -12;  // 年龄可以通过这种方式随意赋值,没有任何限制
        System.out.println(person);

    }
}

我们都知道,年龄不可能是负数,也不可能超过130岁,但是如果没有使用封装的话,便可以给年龄赋值成任意的整数,这显然不符合我们的正常逻辑思维。执行结果如图 所示:在这里插入图片描述
再比如说,如果哪天我们需要将 Person 类中的 age 属性修改为 String 类型的,你会 怎么办?你只有一处使用了这个类的话那还比较幸运,但如果你有几十处甚至上百处都用到 了,那你岂不是要改到崩溃。而封装恰恰能解决这样的问题。如果使用封装,我们只需要稍 微修改下 Person 类的 setAge()方法即可,而无需修改使用了该类的客户代码。

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

Java 是使用“访问控制符”来控制哪些细节需要封装,哪些细节需要暴露的。 Java 中 4 种“访问控制符”分别为 private、default、protected、public,它们说明了面向对 象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。
下面详细讲述它们的访问权限问题。其访问权限范围如表所示。在这里插入图片描述
1.private 表示私有,只有自己类能访问。
2.default 表示没有修饰符修饰,只有同一个包的类能访问。
3.protected 表示可以被同一个包的类以及其他包中的子类访问。
4.public 表示可以被该项目的所有包中的所有类访问。
【注】关于 protected 的两个细节:
1.若父类和子类在同一个包中, 子类可访问父类的 protected 成员,也可访问父类对象 的 protected 成员。
2.若子类和父类不在同一个包中,子类可访问父类的 protected 成员,不能访问父类对象 的 protected 成员。

3.3 封装的使用细节

开发中封装的简单规则:
1.属性一般使用 private 访问权限。
1.1属性私有后, 提供相应的 get/set 方法来访问相关属性,这些方法通常是 public 修饰的,以提供对属性的赋值与读取操作(注意:boolean 变量的 get 方法是 is 开头!)。
2.方法:一些只用于本类的辅助性方法可以用 private 修饰,希望其他类调用的方法 用 public 修饰。
JavaBean 的封装演示:

package com.txw.test;

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;
    }
}

封装的使用代码如下:

package com.txw.test;

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 String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
       // 在赋值之前先判断年龄是否合法
        if (age > 130 || age < 0) {
            this.age = 18;      // 不合法赋默认值18
        } else {
            this.age = age;     // 合法才能赋值给属性age
        }
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class Test2 {
    public static void main(String[] args) {
        Person person = new Person();
        // person.name = "小红"; // 编译错误
        // person.age = -45; // 编译错误
        person.setName("Adair");
        person.setAge(-12);
        System.out.println(person);
        Person person1 = new Person("adair",300);
        System.out.println(person1);
    }
}

执行结果为如图所示:在这里插入图片描述

4 多态(polymorphism)

4.1 多态概念和实现

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个 方法,具体实现会完全不同。 比如:同样是调用人的“休息”方法,张三是睡觉,李四是 旅游,高淇老师是敲代码,数学教授是做数学题; 同样是调用人“吃饭”的方法,中国人 用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。
多态的要点:
1.多态是方法的多态,不是属性的多态(多态与属性无关)。
2.多态的存在要有 3 个必要条件:继承,方法重写,父类引用指向子类对象。
3.父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。
多态和类型转换的代码如下:

package com.txw.test;

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();
          }*/
}

执行为如图所示:
在这里插入图片描述
给大家展示了多态最为多见的一种用法,即父类引用做方法的形参,实参可 以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。
由此,我们可以看出多态的主要优势是提高了代码的可扩展性,符合开闭原则。但是多 态也有弊端,就是无法调用子类特有的功能,比如,我不能使用父类的引用变量调用 Dog 类特有的 seeDoor()方法。
那如果我们就想使用子类特有的功能行不行呢?行!这就是我们下一章节所讲的内容: 对象的转型。

4.2 对象的转型(casting)

父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。
向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方 法。这时,我们就需要进行类型的强制转换,我们称之为向下转型!
对象的转型代码如下:

package com.txw.test;

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.他们俩运行时是同一个对象
    }
}

执行结果为如图所示:在这里插入图片描述
在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类 型转换异常 ClassCastException。
类型转换异常的代码如下:

package com.txw.test;

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));
    }
}

执行结果为如图所示:在这里插入图片描述
为了避免出现这种异常,我们可以使用所学的instanceof运算符进行判断。
向下转型中使用 instanceof的代码如下:

package com.txw.test;

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));
        }
    }
}

执行结果为如图所示:在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

学无止路

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

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

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

打赏作者

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

抵扣说明:

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

余额充值