6-Java 面向对象高级1

6.1 继承

6.1.1 继承的实现

继承让我们更加容易实现类的扩展。 比如,我们定义了人类,再定义Boy类就只需要扩展人类即可。实现了代码的重用,不用再重新发明轮子(don't reinvent wheels)。

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

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

小技巧: Eclipse 中可以选中类名,Ctrl+T 查看类的继承层次结构

/**
 * 测试继承
 * @author IZJ
 *
 */
public class TestExtends {
    public static void main(String[] args) {
        Student stu = new Student();
        stu.name="ZJax";
        stu.height = 172;
        stu.rest();

        Student stu2 = new Student("IZJ", 172, "CS");
        stu2.study();

        System.out.println(stu2 instanceof Student); // true
        System.out.println(stu2 instanceof Person); // true
    }
}

class Person /*extends Object*/ {
    String name;
    int height;

    public void rest() {
        System.out.println("Resting");
    }
}

class Student extends Person {
    String major;

    public Student() {}

    public Student(String name, int height, String major) {
        this.name = name;
        this.height = height;
        this.major = major;
    }

    public void study() {
        System.out.println("Studying");
    }
}

instanceof 运算符

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

继承使用要点

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

继承的好处和弊端

  • 继承好处

    • 提高了代码的复用性(多个类相同的成员可以放到同一个类中)
    • 提高了代码的维护性(如果方法的代码需要修改,修改一处即可)
  • 继承弊端

    继承让类与类之间产生了关系,类的耦合性增强了,当父类发生变化时子类实现也不得不跟着变化,削弱了子类的独立性

6.1.2 方法的重写 override

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

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

  1. “==”: 方法名、形参列表相同。
  2. “≤”:返回值类型和声明异常类型,子类小于等于父类。
  3. “≥”: 访问权限,子类大于等于父类。

6.1.3 Object 类基本特性

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

Object 源码:

注意:如果遇到方法中有native修饰词,则该方法是由本地计算机来实现的,与 Java 无关。

6.1.4 toString() 方法

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

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

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

示例:

/**
 * 测试 Object
 * @author IZJ
 */
public class TestObject {
    public static void main(String[] args) {
        TestObject to = new TestObject();
        System.out.println(to.toString());
        System.out.println(to.getClass().getName() + "@" + Integer.toHexString(to.hashCode()));
    }

    @Override
    public String toString() {
        return "Hello World!";
    }
}

6.1.5 == 和 equals 方法

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

Object类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为 id 相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。

Object 的 equals 方法默认就是比较两个对象的 hashcode,是同一个对象的引用时返回 true 否则返回 false。但是,我们可以根据我们自己的要求重写equals 方法。

/**
 * 测试 equals
 * @author IZJ
 */
public class TestEquals extends Object {
    public static void main(String[] args) {
        Person2 p1 = new Person2(1, "ZJax", "ZJax");
        Person2 p2 = new Person2(1, "IZJ", "IZJ");
        System.out.println(p1 == p2); // false
        System.out.println(p1.equals(p2)); // true

        String str1 = new String("zj");
        String str2 = new String("zj");
        System.out.println(str1 == str2); // false
        System.out.println(str1.equals(str2)); // true
    }
}


class Person2 {
    int id;
    String name;
    String pwd;

    public Person2(int id, String name, String pwd) {
        super();
        this.id = id;
        this.name = name;
        this.pwd = pwd;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + id;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person2 other = (Person2) obj;
        if (id != other.id)
            return false;
        return true;
    }
}

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

6.1.6 super 关键字

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

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

若是构造方法的第一行代码没有显式的调用super(...)或者this(...);那么 Java 默认都会调用 super();,含义是调用父类的无参数构造方法。这里的super(); 可以省略。

/**
 * 测试 super
 * @author IZJ
 */
public class TestSuper {
    public static void main(String[] args) {
        new ChildClass().f();
    }
}

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() {
        System.out.println("调用父类方法前:ChildClass.value=" + value);
        super.f(); // 调用父类对象的普通方法
        System.out.println("调用父类方法后:ChildClass.value=" + value);
        System.out.println("==========");
        value = 200;
        System.out.println("ChildClass.value=" + value);
        System.out.println("FatherClass.value=" + super.value); // 调用父类对象的成员变量
    }
}

img

6.1.7 继承树追溯

属性/方法查找顺序(以查找属性 h 为例):

  1. 查找当前类中有没有属性 h
  2. 依次上溯每个父类,查看每个父类中是否有 h,直到 Object
  3. 如果没找到,则出现编译错误。
  4. 上面步骤,只要找到 h 变量,则这个过程终止。

构造方法调用顺序:

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

注:静态初始化块调用顺序,与构造方法调用顺序一样,一起记即可。

示例:

/**
 * 测试继承树
 * @author IZJ
 */
public class TestSuper02 { 
    public static void main(String[] args) {
        System.out.println("开始创建一个ChildClass2对象......");
        new ChildClass2();
    }
}
class FatherClass2 {
    public FatherClass2() {
        System.out.println("创建FatherClass");
    }
}
class ChildClass2 extends FatherClass2 {
    public ChildClass2() {
        // super(); // super(); 如果不手动加,编译器会自动加
        System.out.println("创建ChildClass");
    }
}

img

6.2 封装

6.2.1 封装的作用和含义

当我们看电视的时候,不需要了解它内部是怎么实现的,只需要知道遥控器的使用即可。诸如此类,很多东西我们都不必去了解它的内部结构。

需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。说的专业一点,封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。

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

编程中封装的具体优点:

  1. 提高代码的安全性。
  2. 提高代码的复用性。
  3. “高内聚”:封装细节,便于修改内部代码,提高可维护性。
  4. “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。

没有封装的代码会出现一些问题

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 p = new Person();
        p.name = "小红";
        p.age = -45; // 年龄可以通过这种方式随意赋值,没有任何限制
        System.out.println(p);
    }
}

我们都知道,年龄不可能是负数,也不可能超过 130 岁,但是如果没有使用封装的话,便可以给年龄赋值成任意的整数,这显然不符合我们的正常逻辑思维。

再比如说,如果哪天我们需要将 Person 类中的 age 属性修改为 String 类型的,你会怎么办?你只有一处使用了这个类的话那还比较幸运,但如果你有几十处甚至上百处都用到了,那你岂不是要改到崩溃。而封装恰恰能解决这样的问题。如果使用封装,我们只需要稍微修改下 Person 类的 setAge() 方法即可,而无需修改使用了该类的客户代码。

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

Java 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。

  • private: 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
  • default:(即默认,什么也不写)在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
  • protected: 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
  • public: 对所有类可见。使用对象:类、接口、变量、方法
修饰符同一个类中同一个包中子类、无关类不同包的子类不同包的无关类
private
默认
protected
public

6.2.3 封装的使用细节

类的属性的处理:

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

这样编写的类,我们称之为 JavaBean

/**
 * 编写 JavaBean
 * @author IZJ
 */
public class Person {
    private int id;
    private String name;
    private int age;
    private boolean gender;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    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 isGender() {
        return gender;
    }
    public void setGender(boolean gender) {
        this.gender = gender;
    }
    @Override
    public String toString() {
        return "Person [id=" + id + ", name=" + name + ", age=" + age + ", gender=" + gender + "]";
    }
}

6.3 多态

多态(polymorphism)指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。 比如:同样是调用人的“休息”方法,张三是睡觉,李四是旅游,高淇老师是敲代码,数学教授是做数学题; 同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。简单来说:多态指的是同一个对象,在不同时刻表现出来的不同形态

多态的要点:

  1. 多态是方法的多态,不是属性的多态(多态与属性无关)。
  2. 多态的存在要有 3 个必要条件:继承,方法重写,父类引用指向子类对象。
  3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。

对象的转型

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

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

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

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

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

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

public class TestCasting3 {
    public static void main(String[] args) {
        Object obj = new String("ZJax");
        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));
        }
    }
}