Java基础—面向对象(二)

面向对象(二)

一、继承

1.继承概述
将多个类的共性抽取出来,形成一个父类。让所有具备这些共性属性的类,继承这个父类,成为其子类。这样做的好处在于:
1)这样可以避免重复代码,提高代码复用性;
2)继承是面向对象另一特点多态的基础之一,有了继承,才有多态;
3)同时可以使子类的功能更加强大灵活,便于扩展

注意:
1)不要仅为了获取其他类的功能,而使用继承;必须在类与类之间有所属关系的情况下,才使用继承;

2)Java中只支持单继承。因为多继承容易带来安全隐患。如果支持多继承,多个父类当中有相同名字的不同功能的方法,子类在调用同名方法的时候,虚拟机不能确定要运行哪个;

3)Java中保留了多继承的机制,用多实现的方式来体现(implements);

4)Java支持多层继承,C继承B,B继承A,以此类推,形成一个继承体系。继承体系相对复杂,想要良好运用该体系,先查阅体系中父类的描述,通过了解父类中的共性功能,就能了解该继承体系的基本功能。在具体调用时,应创建最小子类的对象。由于:
1> 父类不能创建对象,如抽象类;
2> 子类对象有更多的功能,包括父类和自由的功能

2.继承的表现形式
类与类之间不止有A属于B这一种关系,还有聚集的关系。聚集关系分为:
1)聚合关系:A由B组成,B是A中的一个,但是缺少一个B,不影响A的整体功能;例如:球队由球员组成,一个球员是球队的一员;
2)组合关系:A由B组成,B是A的一部分,缺少B,A的功能将受到极大影响;例如:心脏是人的一部分

3.继承的应用
1)super关键字
1> super关键字和this关键字的用法基本一致;
2> this代表本类对象的引用,super代表父类对象的引用

2)这一小节探讨继承关系出现后,类中成员的变化
1> 变量的变化

示例代码:

class Parent {
    int num = 4;
}

class Sub extends Parent {
    int num = 5;
    void show() {
        System.out.println(super.num);
    }
}

class Test {
    public static void main(String[] args) {
        Sub s = new Sub();
        s.show();
    }
}

继承状态下,成员变量内存加载图示:

这里写图片描述

继承状态下,成员变量的变化如下:
如果子类和父类中出现非私有同名变量,子类要访问本类中的同名变量,用this关键字;子类要访问父类中的同名变量,用super关键字。

小扩展:
继承的时候,子类继承父类的成员变量,同时继承该成员变量的值。

示例代码:

class Parent {
    private int num = 4;
}

class Sub extends Parent {
    void show() {
        System.out.println(num);
        System.out.println(this.num);
    }
}

class Test {
    public static void main(String[] args) {
        Sub s = new Sub();
        s.show();
    }
}

2> 方法的变化

示例代码:

class Parent {
    void show() {
        System.out.println("Parent show");
    }
}

class Sub extends Parent {
    void show() {
        System.out.println("Sub show");
    }
}

class Test {
    public static void main(String[] args) {
        Sub s = new Sub();
        s.show();
    }
}

继承状态下,成员方法的变化如下:
当子类出现和父类一模一样的方法时,子类对象调用同名方法,会运行子类方法的内容。由于子类继承了父类的方法,但是子类在具体实现的时候需要有不同的功能,这个时候,只需要用到方法的另一个特性:复写。在子类中定义一个和父类完全相同的方法,在方法体中实现子类的特有功能即可。如果要在子类的复写方法中,调用父类的方法,只需要加上super.方法名即可。

小扩展:
super关键字的使用

示例代码:

class Parent {
    void show() {
        System.out.println("Parent show");
    }
}

class Sub extends Parent {
    void show() {
        super.show();
        System.out.println("Sub show");
    }
}

子类对象调用show()方法时,上述代码既输出"Parent show",也输出"Sub show"

复写注意事项:
a. 复写成功必须保证子类方法权限大于等于父类方法权限;
b. 静态方法只能复写静态方法;
c. 复写和重载的区别在与,复写要求子类父类方法一模一样,重载要求同名函数的参数列表不一样

3> 构造方法的变化

示例代码:

class Parent {
    Parent() {
        System.out.println("Parent construct...");
    }
}

class Sub extends Parent {
    Sub() {
        //super();
        System.out.println("Sub construct...");
    }

    Sub(int x) {
        //super();
        System.out.println("4");
    }
}

class Test {
    public static void main(String[] args) {
        Sub s = new Sub();
    }
}

子类对象进行初始化时,父类的构造方法也会执行。因为子类的构造方法第一行默认写有隐式语句super();super()会访问父类中空参数的构造方法。super();存在于所有子类的所有构造方法的第一行。如果访问父类中指定的构造方法,只需通过手动定义super语句。

子类一定要访问父类构造方法的原因:
由于子类继承了父类的成员,子类对象在被创建时,应该先知道父类是如何对其成员进行初始化的,以避免代码功能的重复。

注意:
构造函数中,this和super只能存在一个,因为两者都必须写在构造方法第一行。

二、final关键字

1.final的定义及特点
final作为一个修饰符:
1)可以修饰类,变量以及方法;
2)被final修饰的类不能被继承;
3)被final修饰的方法不能被复写;
4)被final修饰的变量是常量,值不能被改变;既可以修饰成员变量,也可以修饰局部变量;
5)内部类定义在类中的局部位置上时,只能访问该局部被final修饰的局部变量

示例代码:

//TestFinal被final关键字修饰,不能被继承
final class TestFinal {
    //PI被final修饰,为常量,值不能修改
    private final int PI = 3.14; 
}

class TestFinal {
    //method1被final关键字修饰,不能被复写
    final void method1() {

    }

    void method2() {

    }
}

三、抽象类

1.抽象的定义
抽象就是将多个事物的共性的、本质的内容进行抽取和概括。例如:老虎,猫,狮子都是猫科动物,猫科就是一个抽象的概念。

2.抽象类
抽象类是包含抽象方法的类。抽象方法只有方法的定义,没有方法体。抽象方法的具体实现,有子类复写完成。抽象方法只负责抽象出不同对象的共性,但是不涉及具体实现的细节。例如:猫和狗都会叫,但是叫的内容不同。抽象方法定义一个叫的功能,具体叫的内容则有子类对象来实现。

3.抽象类的特点
1)抽象方法一定定义在抽象类中;
2)抽象方法和抽象类都被abstract关键字修饰;
3)抽象类不可以实例化;
4)抽象类中的抽象方法要被使用,必须由子类复写其所有抽象方法,由子类对象进行调用;如果子类中只覆盖了部分抽象方法,那么该子类还是一个抽象类;
5)抽象类中可以不定义抽象方法,仅为了让该类不被实例化

代码示例:

abstract class Employee {
    abstract void work();
}

class Programmer extends Employee {
    void work() {
        System.out.println("Program...");
    }
}

class Janitor extends Employee {
    void work() {
        System.out.println("Clean...");
    }
}

4.抽象类练习
1)练习1:
开发一个系统需要对员工进行建模,员工包含3个属性:姓名,工号以及工资;经理也是员工,除了含有员工的属性外,另外还有一个奖金属性。请使用继承的思想设计出员工类和经理类。

示例代码:

package test;

public abstract class Employee {

    /**
     * 声明员工的三个属性
     */
    private String name;
    private String id;
    private double pay;

    /**
     * 声明员工的构造方法,对三个属性进行初始化
     * @param name 员工姓名
     * @param id 员工工号
     * @param pay 员工工资
     */
    Employee(String name, String id, double pay) {
        this.name = name;
        this.id = id;
        this.pay = pay;
    }

    /**
     * 声明员工的抽象方法;由于每个员工都需要工作,而具体的工作内容不明确,所以声明为抽象方法
     */
    public abstract void work();
}

class Programmer extends Employee {

    /**
     * 声明程序员的构造方法,调用父类的构造方法对程序员属性进行初始化
     * @param name 程序员姓名
     * @param id 程序员工号
     * @param pay 程序员工资
     */
    Programmer(String name, String id, double pay) {
        super(name, id, pay);
    }

    /**
     * 复写父类的work方法,具体实现程序员的工作
     */
    public void work()   {
        System.out.println("Programming...");
    }
}

class Manager extends Employee {

    /**
     * 声明经理独有的属性
     */
    private int bonus;

    /**
     * 声明经理的构造方法,调用父类的构造方法对经理属性进行初始化
     * @param name 经理姓名
     * @param id 经理工号
     * @param pay 经理工资
     * @param bonus 经理奖金
     */
    Manager(String name, String id, double pay, int bonus) {
        super(name, id, pay);
        this.bonus = bonus;
    }

    /**
     * 复写父类的work方法,具体实现经理的工作
     */
    public void work() {
        System.out.println("Managing...");
    }
}

2)练习2:模版方法模式
使用System类中的currentTimeMillis()方法获取一段程序运行的时间。

示例代码:

package test;

class GetTime {

    public static void getTime() {
        /**
         * 获取程序开始时间
         */
        long start = System.currentTimeMillis();

        /**
         * 程序执行
         */
        for(int x = 0; x < 1000; x ++) {
            System.out.print(x);
        }

        /**
         * 获取程序结束时间
         */
        long end = System.currentTimeMillis();

        /**
         * 打印程序运行时间
         */
        System.out.println("Time elapse: " + (end = start));
    }
}

class UpTime {
    public static void main(String[] args) {
        GetTime.getTime();
    }
}

如果GetTime类要被其他程序使用,来获取程序运行时间,那么该类中所要运行的程序代码是不确定的。因此,首先将要运行的代码提取出来,封装在一个方法中。

示例代码:

package test;

class GetTime {

    public static void getTime() {
        /**
         * 获取程序开始时间
         */
        long start = System.currentTimeMillis();

        /**
         * 程序执行
         */
        runCode();

        /**
         * 获取程序结束时间
         */
        long end = System.currentTimeMillis();

        /**
         * 打印程序运行时间
         */
        System.out.println("Time elapse: " + (end = start));
    }

    /**
    * 封装要运行的代码块
    */
    public void runCode() {
        for(int x = 0; x < 1000; x ++) {
            System.out.print(x);
        }
    }
}

class UpTime {
    public static void main(String[] args) {
        GetTime.getTime();
    }
}

现在想运行另外一段代码,直接修改源代码的方式操作性极差。那么可以运用一个类,继承GetTime类,然后以复写runCode()方法的方式更改要运行的代码,并在主函数中创建子类的对象,调用getTime()方法即可。

示例代码:

package test;

class GetTime {

    public void getTime() {
        /**
         * 获取程序开始时间
         */
        long start = System.currentTimeMillis();

        /**
         * 程序执行
         */
        runCode();

        /**
         * 获取程序结束时间
         */
        long end = System.currentTimeMillis();

        /**
         * 打印程序运行时间
         */
        System.out.println("Time elapse: " + (end = start));
    }

    /**
    * 封装要运行的代码块
    */
    public void runCode() {
        for(int x = 0; x < 1000; x ++) {
            System.out.print(x);
        }
    }
}

/**
* 声明Sub类,继承GetTime类,复写runCode()方法
*/
class Sub extends GetTime {
    public void runCode() {
        for(int x = 0; x < 2000; x ++) {
            System.out.print(x);
        }
    }
}

class UpTime {
    public static void main(String[] args) {
        Sub s = new Sub();
        s.getTime();
    }
}

进一步,在GetTime类中定义一段代码,并没有意义。GetTime类的作用是计算时间,至于要运行的代码,需要使用GetTime类的程序来确定。同时,GetTime类是用来计算时间的,计算时间的方法不需要其他程序来改变,否则该类将失去意义。因此,最好的方式是,将GetTime类声明为抽象类,将runCode()方法声明为抽象方法,将getTime()方法声明为final方法。子类继承GetTime类,复写runCode()抽象方法,写入要运行的代码,再用子类对象调用getTime()方法计算运行时间。

示例代码:

package test;

abstract class GetTime {

    public final void getTime() {
        /**
         * 获取程序开始时间
         */
        long start = System.currentTimeMillis();

        /**
         * 程序执行
         */
        runCode();

        /**
         * 获取程序结束时间
         */
        long end = System.currentTimeMillis();

        /**
         * 打印程序运行时间
         */
        System.out.println("Time elapse: " + (end = start));
    }

    /**
    * 声明抽象方法
    */
    public abstract void runCode();
}

/**
* 声明Sub类,继承GetTime类,复写runCode()方法
*/
class Sub extends GetTime {
    public void runCode() {
        for(int x = 0; x < 2000; x ++) {
            System.out.print(x);
        }
    }
}

class UpTime {
    public static void main(String[] args) {
        Sub s = new Sub();
        s.getTime();
    }
}

这是模版方法设计模式。在定义功能时,功能的一部分是确定的,有一部分不确定;而确定的部分在使用不确定的部分。这时,就将不确定的部分暴露出去,由该类的子类来实现不确定的部分。

四、接口

1.定义
在Java中,用interface关键字定义接口。格式为:

interface {

}

其他类可以通过implements关键字实现接口,并复写其功能。接口可以实现"多继承"的机制,即一个类,可以实现多个接口。

2.接口的特点
1)接口中通常定义常量和抽象方法
2)接口中的成员有固定的修饰符
常量:public static final
方法:public abstract

示例代码:

interface Inter {
    public static final int x = 1;
    public abstract void run();
}

3)接口中的成员都是public权限
4)接口不能实例化
5)子类必须复写接口中的所有抽象方法才能实例化
6)一个类可以实现多个接口

示例代码:

interface A {

}

interface B {

}

class C implements A, B {

}

7)一个类可以在继承之后继续实现一个或多个接口

示例代码:

interface A {

}

interface B {

}

class C {

}

class D extends C implements A, B {

}

8)接口和接口之间可以继承

示例代码:

interface A {

}

interface B extends A {

}

9)接口之间可以多继承 原因是抽象方法没有方法体,具体方法,具体实现即可

示例代码:

interface A {

}

interface B {

}

interface C extends A, B {

}

3.接口的应用
在描述问题的时候,如果是类的基本功能,就可以提取到抽象类类当中,将功能定义为方法或抽象方法。如果是类可以具备,但是不是必要的扩展功能,可以定义在接口中,作为类的功能扩展。

示例代码:

package test;

public class TestInterface {

}

class ITWorker {

    public static void program() {
        System.out.println("Programming...");
    }

    public static void sleep() {
        System.out.println("Sleeping...");
    }
}

interface FixComputer {
    abstract void fix();
}

class Programmer extends ITWorker implements FixComputer {

    @Override
    public void fix() {
        System.out.println("Reinstall OS...");
    }
}

示例代码中,编程和睡觉,可以作为IT男的基本功能被提取到ITWorker类当中;而修电脑,则作为扩展功能,定义成接口,让有修电脑能力的程序员,实现这个接口即可,而没有修电脑功能的程序员,只用继承IT男的基本功能。