java继承

前言

为何要对基础进行再次学习,是希望能更加深入的了解java中的基础知识。可能在日常开发中已经能正常的使用一些基础java知识进行开发,但是总觉得差了一点什么。本博客意在由浅入深的去探索这些java基础知识。本文内容参考java核心技术卷 I (第十版)以及jdk1.8帮助文档。请务必跟随我的节奏,按照下方的步骤,去理解。过程中代码可能报错,请不要过于在意紧跟后续步骤,连贯起来之后就不会报错了。

一、类、父类(超类)、子类(派生类)

对于类的概念在这里不再展开细说了。

    =》创建两个类Employee(打工人类)和Manager(管理人类),稍后Manager类会继承Employee类。可以简单理解管理人是有打工人的属性和方法的。 其中Employee类内容如下《=

public class Employee {
    
    private String name;//姓名
    private double salary;//薪资
    private LocalDate hireDay;//雇佣日期


    public Employee(String name, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        hireDay = LocalDate.of(year, month, day);
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public LocalDate getHireDay() {
        return hireDay;
    }

    public void raiseSalary(double byPercent){
        double raise = salary * byPercent / 100;
        salary += raise;
    }
}

1.定义子类

1.1关键字extends。

关键字extends 表明正在构造的新类派生于一个已存在的类。

public class Manager extends Employee{

}

尽管Employee类是一个父类,但并不是因为它优于子类或者拥有比子类更多的功能。实际上恰恰相反,子类比父类拥有的功能更加丰富。子类可以封装了更多的数据, 拥有更多的功能。

1.2在子类(Manager)增加一个属性bonus表示奖金

时刻关注当前内容,忽略编译器报错。

public class Manager extends Employee{

    private double bonus;//奖金

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }
}

    这里定义的方法和属性并没有什么特别之处。如果有一个Manager 对象, 就可以使用setBonus方法。由于setBonus 方法不是在Employee 类中定义的, 所以属于Employee 类的对象不能使用它。然而, 尽管在Manager 类中没有显式地定义getName 和getHireDay 等方法, 但属于Manager 类的对象却可以使用它们, 这是因为Manager 类自动地继承了父类Employee 中的这些方法。

    同样,从父类中还继承了name、salary 和hireDay 这3 个属性。这样一来, 每个Manager 类对象就包含了4个属性:name、salary 、hireDay 和bonus。

在通过扩展父类定义子类的时候, 仅需要指出子类与父类的不同之处。因此在设计类的时候,应该将通用的方法放在父类中, 而将具有特殊用途的方法放在子类中,这种将通用的功能放到父类的做法,在面向对象程序设计中十分普遍。

2.覆盖方法

    =》父类中的有些方法对子类Manager 并不一定适用。比如:Manager 类中的getSalary方法应该返回薪水和奖金的总和。因此,需要提供一个新的方法来覆盖(override)父类中的这个方法。《=

根据上述思路,我们来简单实现

public class Manager extends Employee{

    private double bonus;//奖金

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }
    
    public double getSalary() {
        return salary + bonus;// 薪水和奖金的总和
    }

}

这样写,看似好像没啥问题。还挺简单的,但是这个方法不能运行。IDE工具也报错了。

    这是因为Manager 类的getSalary 方法不能够直接地访问父类的私有属性。也就是说,尽管每个Manager 对象都拥有一个名为salary 的属性, 但在Manager 类的getSalary 方法中并不能够直接地访问salary属性。只有Employee 类的方法才能够访问私有部分。如果Manager 类的方法一定要访问父类的私有属性,就必须借助于公有的接口, Employee 类中的公有方法getSalary 正是这样一个接口。

    =》再次对该方法进行修改,将对salary属实的访问替换成调用getSalary 方法。《=

public class Manager extends Employee {

    private double bonus;//奖金

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }

    public double getSalary() {
        double baseSalary = getSalary();// 薪水和奖金的总和
        return baseSalary + bonus;
    }
}

这个时候又出现的新的问题,那就是double baseSalary= getSalary(); 这行代码执行时调用的是Manager类的
public double getSalary() 方法而不是Employee类的方法。这样就会导致该方法会无限的递归下去。

    =》我们希望调用父类Employee的getSalary方法而不是当前类(Manager)的方法,这时就可以使用特定的关键字super 解决这个问题《=

public class Manager extends Employee {

    private double bonus;//奖金

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }


    public double getSalary() {
        double baseSalary = super.getSalary();// 薪水和奖金的总和
        return baseSalary + bonus;
    }
}

这里需要注意:有些人认为super 与this 引用是类似的概念, 实际上, 这样比较并不太恰当。这是 因为super 不是一个对象的引用,不能将super赋给另一个对象变量, 它只是一个指示编译器调用父类方法的特殊关键字。
再说一下thissuper的各自用途:
    关键字this有两个用途: 一是引用隐式参数, 二是调用该类其他的构造器。
同样,super 关键字也有两个用途:一是调用父类的方法,二是调用父类的构造器。在调用构造器的时候, 这两个关键字的使用方式很相似。调用构造器的语句只能作为另一个构造器的第一条语句出现。构造参数既可以传递给本类( this ) 的其他构造器, 也可以传递给父类(super ) 的构造器。

    正像前面所看到的那样, 在子类中可以增加属性、增加方法或覆盖父类的方法, 然而绝对不能删除继承的任何属性和方法。

3.子类构造器

其实代码到这里,IDE工具目前还是在报错。

    =》我们在子类中编写一个构造器《=

public class Manager extends Employee {

    private double bonus;//奖金

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }


    public double getSalary() {
        double baseSalary = super.getSalary();// 薪水和奖金的总和
        return baseSalary + bonus;
    }

    public Manager(String name, double salary, int year, int month, int day) {
        super(name, salary, year, month, day);
        bonus = 0;
    }
}

其中 super(name, salary, year, month, day);表示调用父类调Employee 中含有name、salary、year month 和day 参数的构造器。由于Manager 类的构造器不能访问Employee 类的私有属性, 所以必须利用Employee 类的构造器对这部分私有属性进行初始化, 我们可以通过super 实现对父类构造器的调用。使用super 调用构造器的语句必须是子类构造器的第一条语句。

    如果子类的构造器没有显式地调用父类的构造器, 则将自动地调用父类默认(没有参数)的构造器。如果父类没有不带参数的构造器, 并且在子类的构造器中又没有显式地调用父类的其他构造器则Java 编译器将报告错误。

    =》这里再举一个例子,之前没在子类加 public Manager(String name, double salary, int year, int month, int day)这个构造方法时,编译器是一直报错的。那我们这次稍微改动一下这两个类《=

子类删除掉有参构造改为,此时编译器会报错。
public class Manager extends Employee {

    private double bonus;//奖金

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }


    public double getSalary() {
        double baseSalary = super.getSalary();// 薪水和奖金的总和
        return baseSalary + bonus;
    }
    
}

父类改为增加一个无参构造

public class Employee {

    private String name;//姓名
    private double salary;//薪资
    private LocalDate hireDay;//雇佣日期

    public Employee(String name, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        hireDay = LocalDate.of(year, month, day);
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public LocalDate getHireDay() {
        return hireDay;
    }

    public void raiseSalary(double byPercent) {
        double raise = salary * byPercent / 100;
        salary += raise;
    }

    public Employee() {
    }
}

此时编译报错消失,也验证了上方所说如果子类的构造器没有显式地调用父类的构造器, 则将自动地调用父类默认(没有参数)的构造器。

4.初识多态与动态绑定

    =》这里再举一个例子,重新定义Manager 对象的getSalary 方法之后, 奖金就会自动地添加到管理的薪水中。《=

public class TestExtends {
    public static void main(String[] args) {
        Manager boss = new Manager("王经理", 80000,1987, 12 , 15);
        boss.setBonus(5000);
        System.out.println(boss.getSalary());
    }
}

结果
在这里插入图片描述

    =》定义一个包含3 个打工人的数组《=

public class TestExtends {
    public static void main(String[] args) {
        Manager boss = new Manager("王经理", 80000,1987, 12 , 15);
        boss.setBonus(5000);
        Employee[] staff = new Employee[3];
        staff [0] = boss;
        staff [1] = new Employee("程序猿", 50000, 1989, 10, 1) ;
        staff [2] = new Employee("程序媛", 50000, 1990, 3, 15);
        for (Employee e : staff){
            System.out.println(e.getName() + " " + e.getSalary());
        }
    }
}

结果
在这里插入图片描述
    这里的staff[1]和staffs[2]仅输出了基本薪水, 这是因为它们对应的是Employee 对象,而staff[0] 对应的是Manager 对象, 它的getSalary 方法将奖金与基本薪水加在了一起。这里从创建的类就可以看出。

    这里额外注意:e.getSalary()这行代码它是能够确定应该执行哪个getSalary 方法。尽管这里将e 声明为Employee 类型(Employee e : staff),但实际上e 既可以引用Employee 类型的对象,也可以引用Manager 类型的对象。当e引用Employee对象时, e.getSalary( ) 调用的是Employee 类中的getSalary 方法;当e引用Manager 对象时,e.getSalary()调用的是Manager类中的getSalary 方法。

    虚拟机1知道e实际引用的对象类型,因此能够正确地调用相应的方法。一个对象变量(例如, 变量e ) 可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。 下文中将详细地讨论这两个概念。

5.继承层次

继承并不仅限于一个层次(这里展开解释一下,请结合下方的图理解)
在这里插入图片描述
由一个公共父类衍生出来的所有类的集合被称为继承层次(inheritance hierarchy),上图中公共父类为员工。在继承层次中, 从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain) 。通常,一个祖先类可以拥有多个子孙继承链.

6.多态

判断是否应该设计为继承关系的简单规则 "is-a” 规则。它表明子类的每个对象也是父类的对象。

例如:每个经理都是企业的员工,因此, 将Manager 类设计为Employee 类的子类是显而易见的,反之不然, 并不是每一名员工都是经理。“is-a” 规则的另一种表述法是置换法则。它表明程序中出现父类对象的任何地方都可以用子类对象置换。

    =》这里用代码再举几个例子,将一个子类的对象赋给父类变量。《=

public class TestExtends {
    public static void main(String[] args) {
        Employee e;
        e = new Employee();
        e = new Manager();
    }
}

在Java 程序设计语言中, 对象变量是多态的。一个Employee 变量既可以引用一个Employee 类对象, 也可以引用一个Employee 类的任何一个子类的对象(例如, Manager、Executive、Secretary 等)。

    =》这里用代码再举个例子,创建一个类型为Employee三个长度的数组,与之前的相似《=

public class TestExtends {
    public static void main(String[] args) {
        Manager boss = new Manager("王经理", 80000,1987, 12 , 15);
        Employee[] staff = new Employee[3];
        staff [0] = boss;
    }
}

在这个例子中,变量staff[0]与boss引用同一个对象。但编译器将staff[0] 看成Employee 对象。这意味着, 可以这样调用boss.setBonus(5000); 但不能这样调用staff[0].setBonus(5000);
这是因为staff[0]声明的类型是Employee, 而setBonus不是Employee 类的方法。然而,不能将一个父类的引用赋给子类变量。
例如,下面的赋值是非法的

Manager m = staff[i];

原因: 不是所有的员工都是经理。如果赋值成功,m 有可能引用了一个不是经理的Employee 对象, 当在后面调用m.setBonus()方法 时就有可能发生运行时错误。

注意!!!
在Java 中, 子类数组的引用可以转换成父类数组的引用, 而不需要采用强制类型转换。

Manager[] managers = new Manager[10];
Employee[] staff = managers;
上方代码是完全合法的,也不会报错。如果manager[i] 是一个Manager, 也一定是一个Employee。

然而, 实际上, 将会发生一些令人惊讶的事情。要切记managers 和staff 引用的是同一个数组。现在看一下这条语句

public class TestExtends {
    public static void main(String[] args) {
        Manager[] managers = new Manager[10];
        Employee[] staff = managers;
        staff[0] = new Employee("王经理", 80000,1987, 12 , 15) ;
    }
}

编译器接纳了这个赋值操作,IDE工具也没有报错。但是实际执行时。
在这里插入图片描述
使用new managers[10] 创建的数组是一个经理数组。如果试图存储一个Employee 类型的引用就会引发ArrayStoreException 异常。即上方演示代码。
在这里, staff[0] 与manager[0] 引用的是同一个对象, 似乎我们把一个普通员工擅自归入经理行列中了。这是一种很忌伟发生的情形,会搅乱相邻存储空间的内容。在执行时也会抛出异常ArrayStoreException

为了确保不发生这类错误, 所有数组都要牢记创建它们的元素类型, 并负责监督仅将类型兼容的引用存储到数组中。
例如

public class TestExtends {
    public static void main(String[] args) {
        Employee[] staff = new Manager[10];
        staff[0] = new Manager("王经理", 80000,1987, 12 , 15) ;
    }
}

7.理解方法调用

弄清楚如何在对象上应用方法调用。
举例:
下面假设要调用c.method(args),
隐式参数2c声明为类C 的一个对象。下面是调用过程的详细描述:

    一、首先,编译器査看对象的声明类型和方法名。

    假设调用c.method(param),且隐式参数c 声明为C 类的对象。需要注意的是: 有可能存在多个名字为method, 但参数类型不一样的方法。例如,可能存在方法method(int) 和方法method(String)。 编译器将会一 一列举所有C 类中名为method的方法和其父类中访问属性为public 且名为method的方法(父类的私有方法不可访问)。至此编译器已获得所有可能被调用的候选方法。

    二、接下来,编译器将査看调用方法时提供的参数类型。如果在所有名为method的方法中存在一个与提供的参数类型完全匹配, 就选择这个方法。这个过程被称为重载解析(overloading resolution)。

    例如,对于调用c.method("Hello")来说, 编译器将会挑选method(String),而不是method(int)。 由于允许类型转换( int 可以转换成double, Manager 可以转换成Employee, 等等),所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法, 或者发现经过类型转换后有多个方法与之匹配, 就会报告一个错误。至此, 编译器已获得需要调用的方法名字和参数类型。

    方法的名字和参数列表称为方法的签名。

    例如:method(int) 和method(String) 是两个具有相同名字, 不同签名的方法。如果在子类中定义了一个与父类签名相同的方法, 那么子类中的这个方法就覆盖了父类中的这个相同签名的方法。不过, 返回类型不是签名的一部分, 因此,在覆盖方法时, 一定要保证返回类型的兼容性。允许子类将覆盖方法的返回类型定义为原返回类型的子类型。
例如:Employee 类(打工人)有一个方法获取相同职级的员工public Employee getBuddy(){},那么Manager类是继承了Employee 类的,那自己如果不重写此方法那么返回的职级应该低于管理人(经理)的职级的,这显然不是我们想要的。按照如下方式覆盖方法
public Manager getBuddy(){}那么这两个getBuddy 方法就是具有可协变的返回类型

    三、如果是private 方法、static 方法、final 方法(有关final 修饰符的含义将在后续讲解)或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为静态绑定(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型, 并且在运行时实现动态绑定。

    例如:method(int) 和method(String)的例子中,编译器采用动态绑定的方式生成一条调用method(String) 的指令。

    四、当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与c所引用对象的实际类型最合适的那个类的方法。假设c的实际类型是D,并且D是C 类的子类。如果D 类定义了方法method(String),就直接调用它;否则, 将在D 类的父类中寻找f(String),也就是去C类中寻找该方法,以此类推。

    关于动态绑定,每次程序都需要寻找究竟调用哪个方法,时间开销相当大。因此, 虚拟机预先为每个类创建了一个方法表(method table), 其中列出了所有方法的签名和实际调用的方法。这样一来,在真正 调用方法的时候, 虚拟机仅查找这个表就行了。在上面的例子中, 虚拟机搜索D类的方法表,以便寻找与调用method(String) 相匹配的方法。这个方法既有可能是D.method(String) , 也有可能是C.method(String) , 这里的C是D的父类。这里需要提醒一点, 如果调用super.method(param), 编译器将对隐式参数父类的方法表进行搜索。

    =》回顾之前的类,现在把子类和父类相关方法定义好。并新增一个测试类《=

父类

public class Employee {

    private String name;//姓名
    private double salary;//薪资
    private LocalDate hireDay;//雇佣日期

    public Employee(String name, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        hireDay = LocalDate.of(year, month, day);
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public LocalDate getHireDay() {
        return hireDay;
    }

    public void raiseSalary(double byPercent) {
        double raise = salary * byPercent / 100;
        salary += raise;
    }
    
}

子类

public class Manager extends Employee {

    private double bonus;//奖金

    public Manager(String name, double salary, int year, int month, int day) {
        super(name, salary, year, month, day);
        bonus = 0;
    }

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }


    public double getSalary() {
        double baseSalary = super.getSalary();// 薪水和奖金的总和
        return baseSalary + bonus;
    }
}

新建一个测试类

public class ManagerTest {
    public static void main(String[] args) {
        Manager boss = new Manager("经理人", 80000, 1987, 12, 15);
        boss.setBonus(5000) ;
        Employee [] staff = new Employee[3];
        staff[0] = boss;
        staff[1] = new Employee("程序员1号", 50000, 1989, 10, 1);
        staff[2] = new Employee("程序员2号", 40000, 1990, 3, 15) ;
        for (Employee e : staff) {
            System.out.println("name=" + e.getName() + ",salary=" + e.getSalary()) ;
        }
    }
}

结合上方的例子,我们来看下e.getSalary() 的详细过程。

1.首先,e 声明为Employee类型。Employee 类只有一个名叫getSalary 的方法, 这个方法没有参数因此, 在这里不必担心重载解析的问题。


2.其次,由于getSalary 不是private 方法、static 方法或final 方法, 所以将采用动态绑定。

虚拟机为Employee 和Manager 两个类生成方法表。

在Employee的方法表中, 列出了这个类定义的所有方法(简单举例,实际上下方列出的方法并不完整, Employee类还有一个父为Object类,后边会讲到。这里省略了Object类的相关列举):

在这里插入图片描述

Manager的方法表:

在这里插入图片描述

3.在调用e.getSalary()的解析过程为:

①首先,虚拟机提取e 的实际类型的方法表。既可能是Employee、Manager 的方法表,也可能是Employee 类的其他子类的方法表。 ②接下来, 虚拟机搜索定义getSalary签名的类。此时, 虚拟机已经知道应该调用哪个方法。 ③最后, 虚拟机调用方法。     动态绑定有一个非常重要的特性: 无需对现存的代码进行修改,就可以对程序进行扩展。 假设增加一个新类Executive, 并且变量e 有可能引用这个类的对象, 我们不需要对包含调用e.getSalary() 的代码进行重新编译。如果e 恰好引用一个Executive 类的对象, 就会自动地调用Executive.getSalary()方法。

注意! 在覆盖一个方法的时候,子类方法不能低于父类方法的可见性。特别是, 如果父类方法是public, 子类方法一定要声明为public。经常会发生这类错误:在声明子类方法的时候, 遗漏了public修饰符。此时,编译器将会把它解释为试图提供更严格的访问权限。

8.阻止继承:final 类和方法

    有些场景,不希望利用某个类定义子类。不允许扩展的类被称为final 类。如果在定义类的时候使用了final 修饰符就表明这个类是final类。

    =》不允许继承Manager类《=

public final class  Manager{ }
    类中的特定方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法。final类中的所有方法自动地成为final 方法。

    =》Employee类中不允许子类覆盖getName( )方法《=

Employee类修改为
public final String getName() {
    return name;
}

在子类覆盖此方法时
在这里插入图片描述

注意! 属性也可以被声明为final。对于final属性来说, 构造对象之后就不允许改变它们的值了。不过, 如果将一个类声明为final, 只有其中的方法自动地成为final,而不包括属性。
例如:String 类也是final 类,这意味着不允许任何人定义String 的子类。换言之, 如果有一个String 的引用, 它引用的一定是一个String 对象, 而不可能是其他类的对象。

9.强制类型转换

    将一个类型强制转换成另外一个类型的过程被称为类型转换。

Java 程 序设计语言提供了一种专门用于进行类型转换的表示法。 例如:

double x = 3.405;
int nx = (int) x;

将表达式x 的值转换成整数类型, 舍弃了小数部分。

    正像有时候需要将浮点型数值转换成整型数值一样, 有时候也可能需要将某个类的对象引用转换成另外一个类的对象引用。对象引用的转换语法与数值表达式的类型转换类似, 仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。

例如

Employee[] staff = new Employee[3];
staff [0] = new Manager("王经理", 80000,1987, 12 , 15);
Manager staff1 = (Manager)staff[0];

进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后, 使用对象的全部功能。


    =》回顾一下之前的测试类TestExtends《=

public class TestExtends {
    public static void main(String[] args) {
        Manager boss = new Manager("王经理", 80000,1987, 12 , 15);
        boss.setBonus(5000);
        Employee[] staff = new Employee[3];
        staff [0] = boss;
        staff [1] = new Employee("程序猿", 50000, 1989, 10, 1) ;
        staff [2] = new Employee("程序媛", 50000, 1990, 3, 15);
        Manager staff1 = (Manager)staff[0];
        System.out.println(staff1.getSalary());
    }
}
    由于某些项是普通雇员, 所以staff 数组必须是Employee 对象的数组。我们需要将数组中引用管理人(经理)的元素复原成Manager 类, 以便能够访问新增加的所有变量(需要注意, 在前面的示例代码中, 为了避免类型转换, 我们做了一些特别的处理, 即将boss 变量存入数组之前,先用Manager 对象对它进行初始化。而为了设置经理的奖金, 必须使用正确的类型)。     大家知道, 在Java 中, 每个对象变量都属于一个类型。类型描述了这个变量所引用的以 及能够引用的对象类型。例如,staff[i] 引用一个Employee 对象(因此它还可以引用Manager 对象)。      将一个值存入变量时, 编译器将检查是否允许该操作。将一个子类的引用赋给一个父类变量, 编译器是允许的。但将一个父类的引用赋给一个子类变量, 必须进行类型转换, 这样才能够通过运行时的检査。      如果试图在继承链上进行向下的类型转换,并且“ 谎报” 有关对象包含的内容, 会发生异常,看下图
public class TestExtends {
    public static void main(String[] args) {
        Manager boss = new Manager("王经理", 80000,1987, 12 , 15);
        boss.setBonus(5000);
        Employee[] staff = new Employee[3];
        staff [0] = boss;
        staff [1] = new Employee("程序猿", 50000, 1989, 10, 1) ;
        staff [2] = new Employee("程序媛", 50000, 1990, 3, 15);
        Manager staff2 = (Manager)staff[1];
    }
}

结果

在这里插入图片描述

    因此,应该养成这样一个良好的程序设计习惯: 在进行类型转换之前, 先查看一下是否能够成功地转换。这个过程简单地使用 instanceof 操作符就可以实现。例如:
public class TestExtends {
    public static void main(String[] args) {
        Manager boss = new Manager("王经理", 80000,1987, 12 , 15);
        boss.setBonus(5000);
        Employee[] staff = new Employee[3];
        staff [0] = boss;
        staff [1] = new Employee("程序猿", 50000, 1989, 10, 1) ;
        staff [2] = new Employee("程序媛", 50000, 1990, 3, 15);
        //主要看这里!!!
        if (staff[1] instanceof Manager){
            //可以转换为Manager
            System.out.println("能转换为Manager类型");
            System.out.println(staff[1].getClass());
        }else {
            System.out.println("不能转换为Manager类型");
            System.out.println(staff[1].getClass());
        }
    }
}

结果:
在这里插入图片描述

    最后, 如果这个类型转换不可能成功, 编译器就不会进行这个转换。 例如,下面这个类型转换: String c = (String) staff[1] ; 将会产生编译错误, 这是因为String 不是Employee的子类。

在这里插入图片描述

总结:1.只能在继承层次内进行类型转换。2.建议在将父类转换成子类之前,应该使用instanceof 进行检查。

注意!x instanceof C。其中x为null,不会产生异常, 只是返回false。之所以这样处理是因为null 没有引用任何对象, 当然也不会引用C类型的对象。

    再说一点,通过类型转换调整对象的类型并不是一种好的做法。在我们列举的示例中, 大多数情况并不需要将Employee 对象转换成Manager 对象, 两个类的对象都能够正确地调用getSalary 方法,这是因为实现多态性的动态绑定机制能够自动地找到相应的方法。     只有在使用Manager 中特有的方法时才需要进行类型转换, 例如, setBonus 方法。如果鉴于某种原因, 发现需要通过Employee 对象调用setBonus 方法, 那么就应该检查一下父类的设计是否合理。重新设计一下父类,并添加setBonus法才是正确的选择。请记住, 只要没有捕获ClassCastException 异常, 程序就会终止执行。在一般情况下,应该尽量少用类型转换和instanceof 运算符。

10.抽象类

    如果自下而上在类的继承层次结构中上移, 位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看, 祖先类更加通用, 人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。

例如, 考虑一下对Employee 类层次的扩展。打工人是一个人, 学生也是一个人。下面将类Person (人类)和类Student 添加到类的层次结构中。

在这里插入图片描述

    为什么要花费精力进行这样高层次的抽象呢? 每个人都有一些诸如姓名这样的属性。学生与打工人都有姓名属性, 因此可以将getName 方法放置在位于继承关系较高层次的通用父类中。
现在, 再增加一个getDescription 方法,它可以返回对一个人的简短文本描述。

     在Employee 类和Student 类中实现这个方法很容易。但是在Person 类中应该提供什么内容呢? 除了姓名之外,Person 类一无所知。当然, 可以让Person. getDescription( )返回一个空字符串。然而, 还有一个更好的方法, 就是使用abstract 关键字,这样就完全不需要实现这个方法了。为了提高程序的清晰度, 包含一个或多个抽象方法的类本身必须被声明为抽象的。
public abstract class Person {
    public abstract String getDescription();
}
     除了抽象方法之外, 抽象类还可以包含具体数据和具体方法。例如, Person 类还保存着姓名和一个返回姓名的具体方法。
public abstract class Person {

    private String name;

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

    public String getName() {
        return name;
    }
    
    public abstract String getDescription();
}

参考核心卷建议:建议尽量将通用的属性和方法(不管是否是抽象的)放在父类(不管是否是抽象类)中。

    抽象方法充当着占位的角色, 它们的具体实现在子类中。扩展抽象类可以有两种选择。一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。(这些知识在大学课课程中应该经常听到)

类即使不含抽象方法,也可以将类声明为抽象类。抽象类不能被实例化。也就是说,如果将一个类声明为abstract , 就不能创建这个类的对象。但可以创建一个具体子类的对象。需要注意,可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。



    =》代码举例,三个类如下《=

打工人类

public class Employee extends Person{

    private double salary;//薪资
    private LocalDate hireDay;//雇佣日期

    public Employee(String name, double salary, int year, int month, int day) {
        super(name);
        this.salary = salary;
        hireDay = LocalDate.of(year, month, day);
    }

    public String getDescription() {

        return "打工人薪资为:"+this.salary;
    }

    public double getSalary() {
        return salary;
    }

    public LocalDate getHireDay() {
        return hireDay;
    }

    public void raiseSalary(double byPercent) {
        double raise = salary * byPercent / 100;
        salary += raise;
    }

}

学生类

public class Student extends Person{

    private String major;

    public String getMajor() {
        return major;
    }

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

    @Override
    public String getDescription() {
        return "这个童鞋专业为:"+this.major;
    }
}

测试类

public class PersonTest {
    public static void main(String[] args) {
        Person[] people = new Person[2];
        people[0] = new Employee("阿明" , 50000, 1989, 10, 1);
        people[1] = new Student("小明", "软件工程");
        for (Person p : people) {
            System.out.println(p.getName() + ", " + p.getDescription());
        }
    }
}

结果
在这里插入图片描述

由于不能构造抽象类Person 的对象, 所以变量p 永远不会引用Person 对象, 而是引用诸如Employee 或Student 这样的具体子类对象, 而这些对象中都定义了getDescription 方法。

是否可以省略Person 父类中的抽象方法, 而仅在Employee 和Student 子类中定义getDescription 方法呢? 如果这样的话, 就不能通过变量p 调用getDescription 方法了。编译器只允许调用在类中声明的方法。
在Java 程序设计语言中, 抽象方法是一个重要的概念。在接口( interface) 中将会看到更多的抽象方法。有关接口的详细介绍请关注后边的博客

11.受保护访问

大家都知道, 最好将类中的属性标记为private, 而方法标记为public。任何声明为private的内容对其他类都是不可见的。如果我希望子类能访问父类的某个属性或者方法怎么办?
将父类的属性或者方法声明为proteced。

    在实际应用中,要谨慎使用protected 属性。假设需要将设计的类提供给其他程序员使用, 而在这个类中设置了一些受保护属性, 由于其他程序员可以由这个类再派生出新类,并访问其中的受保护属性。在这种情况下, 如果需要对这个类的实现进行修改, 就必须通知所有使用这个类的程序员。

这违背了 OOP3提倡的数据封装原则。

protected 这种方法的一个最好的示例就是Object 类中的clone 方法,有关它的详细内容请关注后续博客内容。

  • private:仅对本类可见
  • public:对所有类可
  • protected:对本包和所有子类可
  • 默认(无修饰符):对本包可见

二、Object: 所有类的父类

Object 类是Java 中所有类的始祖, 在Java 中每个类都是由它扩展而来的。

但是并不需要这样写:public class Employee extends Object。
如果没有明确地指出父类,Object 就被认为是这个类的父类。由于在Java 中,每个类都是由Object 类扩展而来的, 所以,熟悉这个类提供的所有服务十分重要。(目前我这里没有详细的去写Object相关方法,感兴趣的小伙伴可自行去了解)

在Java中, 只有基本类型(primitive types)不是对象。所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。

1.equals 方法

    Object 类中的equals 方法用于检测一个对象是否等于另外一个对象。在Object 类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用, 它们一定是相等的。从这点上看,将其作为默认操作也是合乎情理的      然而,对于多数类来说, 这种判断并没有什么意义。例如, 采用这种方式比较两个PrintStream(打印流)对象是否相等就完全没有意义。

2.相等测试与继承

    如果隐式和显式的参数不属于同一个类, equals 方法将如何处理呢?
    这是一个很有争议的问题。在前面的例子中, 如果发现类不匹配, equals 方法就返冋false: 但是, 许多程序员却喜欢使用instanceof 进行检测:这样做不但没有解决otherObject 是子类的情况,并且还有可能会招致一些麻烦。这就是建议不要使用这种处理方式的原因所在。

    Java 语言规范要求equals 方法具有下面的特性:

  1. 自反性: 对于任何非空引用x, x.equals(x) 应该返回true
  2. 对称性: 对于任何引用x 和y, 当且仅当y.equals(x) 返回true, x.equals(y) 也应该返回true。
  3. 传递性: 对于任何引用x、y 和z, 如果x.equals(y) 返回 true, y.equals(z) 返回true,x.equals(z) 也应该返回true。
  4. 一致性: 如果x 和y 引用的对象没有发生变化, 反复调用x.equals(y) 应该返回同样的结果。
  5. 对于任意非空引用x, x.equals(null) 应该返回false。
     这些规则十分合乎情理, 从而避免了类库实现者在数据结构中定位一个元素时还要考虑 调用x.equals(y), 还是调用y.equals(x) 的问题。然而, 就对称性来说, 当参数不属于同一个类的时候需要仔细地思考一下。请看下面这个调用:
e.equals(m)
     这里的e 是一个Employee 对象,m 是一个Manager 对象, 并且两个对象具有相同的姓名、 薪水和雇佣日期。如果在Employee.equals 中用instanceof 进行检测, 则返回true,. 然而这意 味着反过来调用:
m.equals(e)
     也需要返回true、对称性不允许这个方法调用返回false, 或者抛出异常。 这就使得Manager 类受到了束缚。这个类的equals 方法必须能够用自己与任何一 个Employee 对象进行比较, 而不考虑管理人(经理)拥有的那部分特有信息! 猛然间会让人感觉 instanceof 测试并不是完美无瑕。      某些书的作者认为不应该利用getClass 检测, 因为这样不符合置换原则。有一个应用AbstractSet 类的equals 方法的典型例子, 它将检测两个集合是否有相同的元素。AbstractSet类有两个具体子类: TreeSet 和HashSet, 它们分别使用不同的算法实现查找集合元素的操作。无论集合采用何种方式实现, 都需要拥有对任意两个集合进行比较的功能。      然而, 集合是相当特殊的一个例子, 应该将AbstractSet.equals 声明为final , 这是因为没有任何一个子类需要重定义集合是否相等的语义(事实上, 这个方法并没有被声明为final。 这样做, 可以让子类选择更加有效的算法对集合进行是否相等的检测)

下面可以从两个截然不同的情况看一下这个问题:

  1. 如果子类能够拥有自己的相等概念, 则对称性需求将强制采用getClass 进行检测
  2. 如果由父类决定相等的概念,那么就可以使用instanceof进行检测, 这样可以在不同子类的对象之间进行相等的比较。
     在打工人和管理人的例子中, 只要对应的属性相等, 就认为两个对象相等。如果两个Manager 对象所对应的姓名、薪水和雇佣日期均相等, 而奖金不相等, 就认为它们是不相同的, 因此, 可以使用getClass 检测。 但是, 假设使用打工人的ID作为相等的检测标准, 并且这个相等的概念适用于所有的子类, 就可以使用instanceof进行检测, 并应该将Employee.equals 声明为final。

核心卷中给出编写equals方法的的建议:
  1. 显式参数命名为otherObject, 稍后需要将它转换成另一个叫做other 的变量。
  2. 检测this与otherObject 是否引用同一个对象:if (this = otherObject) return true;(这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的属性所付出的代价小得多。)
  3. 检测otherObject是否为null , 如果为null , 返回false。这项检测是很必要的。if (otherObject == null ) return false;
  4. 比较this 与otherObject 是否属于同一个类。如果equals 的语义在每个子类中有所改变,就使用getClass 检测:if (getClass( ) != otherObject.getCIass( )) return false;
    如果所有的子类都拥有统一的语义,就使用instanceof 检测:if (!(otherObject instanceof ClassName)) return false;
  5. 将otherObject 转换为相应的类类型变量:ClassName other = (ClassName) otherObject
  6. 现在开始对所有需要比较的属性进行比较了。使用== 比较基本类型属性,使用equals 比较对象属性。如果所有的属性都匹配, 就返回true; 否则返回false。(如果在子类中重新定义equals, 就要在其中包含调用super.equals(other)。)对于数组类型的属性, 可以使用静态的Arrays.equals 方法检测相应的数组元素是否相等。

3.hashCode 方法

散列码( hash code ) 是由对象导出的一个整型值。散列码是没有规律的。如果x 和y 是两个不同的对象, x.hashCode( ) 与y.hashCode( ) 基本上不会相同。

    由于hashCode 方法定义在Object 类中, 因此每个对象都有一个默认的散列码,其值为对象的存储地址。



    =》代码举例《=

public class HashCodeMain {
    public static void main(String[] args) {
        String s = "Ok";
        StringBuilder sb = new StringBuilder (s) ;
        System.out .println(s.hashCode() + " | - | " + sb.hashCode());
        String t = new String("Ok") ;
        StringBuilder tb = new StringBuilder(t);
        System.out.println(t .hashCode()+ " | - | "  + tb.hashCode());
    }
}

结果:
在这里插入图片描述

    请注意, 字符串s 与t 拥有相同的散列码, 这是因为字符串的散列码是由内容导出的。而字符串缓冲sb 与tb 却有着不同的散列码, 这是因为在StringBuffer 类中没有定义hashCode 方法,它的散列码是由Object 类的默认hashCode 方法导出的对象存储地址。

如果重新定义equals 方法, 就必须重新定义hashCode 方法, 以便用户可以将对象插入到散列表中(有关散列表的内容将在后边章节讲解)。如果存在数组类型的属性, 那么可以使用静态的Arrays.hashCode 方法计算一个散列码,这个散列码由数组元素的散列码组成。

equals 与hashCode 的定义必须一致:如果x.equals(y) 返回true, 那么x.hashCode( ) 就必须与y.hashCode( ) 具有相同的值。

4.toString 方法

在Object 中还有一个重要的方法, 就是toString 方法, 它用于返回表示对象值的字符串。

在调用x.toString( ) 的地方可以用""+x 替代。这条语句将一个空串与x 的字符串表示相连接。这里的x 就是x.toString( )。 与toString 不同的是, 如果x 是基本类型, 这条语句照样能够执行。

如果x 是任意一个对象, 并调用println 方法就会直接地调用x.toString(),井打印输出得到的字符串。
这里看下println方法代码,它里边是有调用这个对象的toString方法的。

public void println(Object var1) {
    String var2 = String.valueOf(var1);
    synchronized(this) {
        this.print(var2);
        this.newLine();
    }
}
public static String valueOf(Object var0) {
    return var0 == null ? "null" : var0.toString();
}

Object 类定义了toString 方法, 用来打印输出对象所属的类名和散列码。
我们往往会进行如下操作

System.out.println(对象实例);

得到的结果,不是它里边的属性内容,而是上方说的类名和散列码内容。这是因为你这个对象类没有覆盖父类的toString方法。更烦的是, 数组继承了object 类的toString 方法, 数组类型将按照旧的格式打印。可以改为Arrays.toString方式输出包含里边元素字符串。例如String s = Arrays.toString(arr对象);要想打印多维数组(即, 数组的数组)则需要调用Arrays.deepToString 方法。


三、泛型数组列表

可以先简单看下这一小节,后续会详细讲解ArryList及源码,毕竟面试高频考点(当然这也可能和面试对应的职级有一丢丢关系)。

1.ArrayList

     一旦确定了数组的大小, 改变它就不太容易了。在Java 中, 解决这个问题最简单的方法是使用Java 中另外一个被称为ArrayList 的类。它使用起来有点像数组,但在添加或删除元素时, 具有自动调节数组容量的功能,而不需要为此编写任何代码。      ArrayList 是一个采用类型参数( type parameter ) 的泛型类( generic class )。为了指定数 组列表保存的元素对象类型, 需要用一对尖括号将类名括起来加在后面。

例如

ArrayList<Employee> staff = new ArrayList<Employee>();

Java SE 7 中, 可以省去右边的类型参数:

ArrayList<Employee> staff = new ArrayList<>();

这被称为“ 菱形” 语法, 因为空尖括号<>就像是一个菱形。可以结合new 操作符使用菱形语法。编译器会检查新值是什么。如果赋值给一个变量, 或传递到某个方法, 或者从某个方法返回, 编译器会检査这个变量、参数或方法的泛型类型, 然后将这个类型放在<>中。在这个例子中,new ArrayList<>() 将赋至一个类型为ArrayList 的变量, 所以泛型类型为Employee。

2.类型化与原始数组列表的兼容性

在你自己的代码中, 你可能更愿意使用类型参数来增加安全性。你会了解如何与没有使用类型参数的遗留代码交互操作。



    =》代码举例《=

    public void update(ArrayList list) {
        //省略update具体内容
    }

    public ArrayList find(String query) {
        //省略find具体内容
    }

可以将一个类型化的数组列表传递给update 方法, 而并不需要进行任何类型转换。也可以将staff 对象传递给update 方法。

ArrayList<Employee> staff = new ArrayList<>();
employee.update(staff);

相反地,将一个原始ArrayList 赋给一个类型化ArrayList 会得到一个警告。

ArrayList<Employee> result = employee.find("查询条件");

为了能够看到警告性错误的文字信息,要将编译选项置为-Xlint:unchecked。
在这里插入图片描述
并且,使用类型转换并不能避免出现警告。

ArrayList<Employee> result = (ArrayList<Employee>) employee.find("") ;

在这里插入图片描述

这就是Java 中不尽如人意的参数化类型的限制所带来的结果。鉴于兼容性的考虑, 编译器在对类型转换进行检査之后, 如果没有发现违反规则的现象, 就将所有的类型化数组列表转换成原始ArrayList 对象。在程序运行时, 所有的数组列表都是一样的, 即没有虚拟机中的类型参数。因此, 类型转换( ArrayList ) 和( ArrayList ) 将执行相同的运行时检查。
在这种情形下, 不必做什么。 只要在与遗留的代码进行交叉操作时,研究一下编泽器的警告性提示,并确保这些警告不会造成太严重的后果就行了。一旦能确保不会造成严重的后果, 可以用@SuppressWarnings(“unchecked”)标注来标记这个变量能够接受类型转换。

        @SuppressWarnings("unchecked")
        ArrayList<Employee> result = employee.find("");

四、对象包装器与自动装箱

     有时, 需要将int 这样的基本类型转换为对象。所有的基本类型都冇一个与之对应的类。例如,Integer 类对应基本类型int。通常, 这些类称为包装器( wrapper ) 这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character 、Void和Boolean ( 前6 个类派生于公共的父类Number)。对象包装器类是不可变的, 即一旦构造了包装器, 就不 允许更改包装在其中的值。同时, 对象包装器类还是final , 因此不能定义它们的子类。      假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型, 也就是说,不允许写成ArrayList。这里就用到了Integer 对象包装器类。我们可以声明一个Integer对象的数组列表。

由于每个值分别包装在对象中, 所以ArrayList<Integer> 的效率远远低于int[]数组。因此, 应该用它构造小型集合, 其原因是此时程序员操作的方便性要比执行效率更加重要。

有一个很有用的特性, 从而更加便于添加int 类型的元素到ArrayList< Integer>中。

ArrayList<Integer> list = new ArrayList<>();
list.add(3);

会自动变换成

list.add(Integer.value0f(3)) ;

这种变换被称为自动装箱(autoboxing)

相反地, 当将一个Integer 对象赋给一个int 值时, 将会自动地拆箱。也就是说, 编译器将下列语句:

int n = list.get(i);

翻译成

int n = list.get(i).intValue();

大多数情况下, 容易有一种假象, 即基本类型与它们的对象包装器是一样的, 只是它们的相等性不同。大家知道, == 运算符也可以应用于对象包装器对象, 只不过检测的是对象是否指向同一个存储区域, 因此,下面的比较通常不会成立:

		Integer a = 1000;
        Integer b = 1000;
        System.out.println(a==b);

    =》代码举例《=

public class TestInteger {
    public static void main(String[] args) {
        Integer a = 1000;
        Integer b = 1000;
        System.out.println(a==b);
        System.out.println(a.equals(b));
        String c = "";
        System.out.println("变量a的内存地址:"+System.identityHashCode(a));
        System.out.println("变量b的内存地址:"+System.identityHashCode(b));
    }
}

结果
在这里插入图片描述

    这里需要注意亿点点细节:首先a和b是两个对象实例,这个首先要达成共识,然后是用==比较时,比较的是内存地址,因为是对象嘛,不是基本数据类型。到这里也没问题。然后再次强调一下,Object对象有equals方法,而且a和b也是Integer类的实例对象,这时你会发现Integer类重写了equals方法。再次说明这里不要觉得equals方法逻辑都是一样的,这么理解就大错特错了,子类重写时需要看子类具体实现逻辑。然后这些都达成共识之后,结合下方源码再次看下上方的结果,是不是就恍然大悟了,多尝试看下一下源码,习惯之后源码没有那么深不可测,也没有那么遥不可及。

在这里插入图片描述
在这里插入图片描述

自动装箱规范要求boolean、byte、char <=127, 介于-128 ~ 127 之间的short 和int 被包装到固定的对象中。例如, 如果在前面的例子中将a 和b 初始化为100, 对它们进行比较的结果一定成立。

上代码

public class TestInteger {
    public static void main(String[] args) {
        Integer a = 100;
        Integer b = 100;
        System.out.println(a==b);
        System.out.println(a.equals(b));
        System.out.println("变量a的内存地址:"+System.identityHashCode(a));
        System.out.println("变量b的内存地址:"+System.identityHashCode(b));
    }
}

结果
在这里插入图片描述
如果在一个条件表达式中混合使用Integer 和Double 类型, Integer 值就会拆箱,提升为double, 再装箱为Double。最后强调一下, 装箱和拆箱是编译器认可的, 而不是虚拟机。编译器在生成类的字节码时, 插入必要的方法调用。虚拟机只是执行这些字节码。

使用数值对象包装器还有另外一个好处。Java 设计者发现,可以将某些基本方法放置在包装器中, 例如, 将一个数字字符串转换成数值。int x = Integer.parselnt(字符串) ;
这里与Integer 对象没有任何关系, parselnt 是一个静态方法。但Integer 类是放置这个方法的一个好地方。

     这里可能会有人想到,Integer是一个类,那么他的实例对象通过参数传递给某个方法,在方法中对该对象的值进行修改,那么就实现了对这个值得修改。但是很遗憾,并不会生效。因为该类是final 的。

在这里插入图片描述

    针对修改数值内容,除了自定义一个类,然后实例化这个类的对象,实现在方法中修改其值的方法,有没有 其他的方法呢?

如果想编写一个修改数值参数值的方法, 就需要使用在org.omg.CORBA 包中定义的持有者( holder ) 类型, 包括IntHolder、ooleanHolder 等。每个持有者类型都包含’一个公有属性值, 通过它可以访问存储在其中的值。



    =》这里代码举例,不深入去讲解了,我在平时没有用到过这种方式,也可能是我现在还是比较菜吧《=

public class IntHolderTest {
    public static void main(String[] args) {
        IntHolder intHolder = new IntHolder();
        intHolder.value=10;
        //此方法实现intHolder变量值乘2
        test1(intHolder);
        System.out.println(intHolder.value);
    }
    public static void test1(IntHolder a){
        a.value = a.value<<1;
    }
}

五、参数数量可变的方法

在Java SE 5.0 以前的版本中, 每个Java 方法都有固定数量的参数。然而,现在的版本(这里使用的jdk版本为jdk1.8)提供了可以用可变的参数数量调用的方法(有时称为“ 变参” 方法)。

经典例子为printf方法

	//第一个方法
    public PrintStream printf(String var1, Object... var2) {
        return this.format(var1, var2);
    }
	//第二个方法
    public PrintStream printf(Locale var1, String var2, Object... var3) {
        return this.format(var1, var2, var3);
    }

拿第一个方法举例,这里的省略号. . . 是Java 代码的一部分,它表明这个方法可以接收任意数量的对象(除var1参数之外)。

public class PrintfTest {
    public static void main(String[] args) {
        System.out.printf("%d  %s",1,"字符串");
    }
}

实际上,printf 方法接收两个参数, 一个是格式字符串, 另一个是Object [ ] 数组, 其中保存着所有的参数(如果调用者提供的是整型数组或者其他基本类型的值, 自动装箱功能将把它们转换成对象)。现在将扫描var1字符串, 并将第i 个格式说明符与var2[i] 的值匹配起来。换句话说,对于printf 的实现者来说,Object… 参数类型与Object[ ] 完全一样。编译器需要对printf 的每次调用进行转换, 以便将参数绑定到数组上, 并在必要的时候进行自动装箱:

public class PrintfTest {
    public static void main(String[] args) {
        System.out.printf("%d  %s",1,"字符串");
        System.out.println();//换行作用
        System.out.printf("%d  %s",new Object[]{ new Integer(1),"字符串" });
    }
}

六、枚举类

所有的枚举类型都是Enum 类的子类。它们继承了这个类的许多方法。其中最有用的一个是toString, 这个方法能够返回枚举常量名。
toString 的逆方法是静态方法valueOf。(这里没有太多要补充的细节,可以自行去了解学习,工作中还是经常能用到的,但是不复杂。)

七、反射



    =》这里代码举例,不深入去讲解了,我在平时没有用到过这种方式,也可能是我现在还是比较菜吧《=

    反射库( reflection library ) 提供了一个非常丰富且精心设计的工具集, 以便编写能够动态操纵Java 代码的程序。这项功能被大量地应用于JavaBeans 中, 它是Java 组件的体系结构( 有关JavaBeans 的详细内容在和核心卷II 中有,这里只是学习和参考核心卷I)。使用反射, Java 可以支持Visual Basic 用户习惯使用的工具。特别是在设计或运行中添加新类时, 能够快速地应用开发工具动态地查询新添加类的能力。

    能够分析类能力的程序称为反射(reflective)。反射机制的功能极其强大,在下面可以看到, 反射机制可以用来做什么:

  • 在运行时分析类的能力。
  • 在运行时查看对象, 例如, 编写一个toString 方法供所有类使用。
  • 实现通用的数组操作代码。
  • 利用Method 对象, 这个对象很像C++中的函数指针。

反射是一种功能强大且复杂的机制。使用它的主要人员是工具构造者, 而不是应用程序员P 如果仅对设计应用程序感兴趣, 而对构造工具不感兴趣, 可以跳过下边的剩余部分, 稍后再返回来学习。

1.Class 类

在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。然而, 可以通过专门的Java 类访问这些信息。保存这些信息的类被称为Class, 这个名字很容易让人混淆。Object 类中的getClass( ) 方法将会返回一个Class 类型的实例。

    =》这里还用之前的代码举例《=

public class EmployeeTest {
    public static void main(String[] args) {
        Employee e = new Employee("程序员1号", 50000, 1989, 10, 1);
        Class cl = e.getClass();
        System.out.println(cl);
    }
}

结果
在这里插入图片描述
如同用一个Employee 对象表示一个特定的打工人属性一样, 一个Class 对象将表示一个特定类的属性。最常用的Class 方法是getName。这个方法将返回类的名字。
例如,下面这条语句:

public class EmployeeTest {
    public static void main(String[] args) {
        Employee e = new Employee("程序员1号", 50000, 1989, 10, 1);
        Class cl = e.getClass();
        System.out.println(cl);
        System.out.println(e.getClass().getName()+"-----"+e.getName());
    }
}

结果(一个是这个类的名字,一个是这个类中我们定义name属性的值,需要自己绕一下。)
在这里插入图片描述
如果类在一个包里,包的名字也作为类名的一部分:

    public static void main(String[] args) {
        Random generator = new Random();
        Class cl = generator.getClass() ;
        String name = cl .getName();
        System.out.println(name);
    }

结果
在这里插入图片描述

还可以调用静态方法forName 获得类名对应的Class 对象。

    public static void main(String[] args) throws ClassNotFoundException {
        String dassName = "java.util.Random";
        Class cl = Class.forName(dassName) ;
        System.out.println(cl);
    }

这里会有一个异常ClassNotFoundException,这里捕获了一下。

    public static void main(String[] args) {
        try {
            String dassName = "java.util.Random";
            Class cl = Class.forName(dassName);
            System.out.println(cl);
        } catch (ClassNotFoundException e) {
            System.out.println("没找到这个类");
        }

    }

如果类名保存在字符串中, 并可在运行中改变, 就可以使用这个方法。当然, 这个方法只有在className 是类名或接口名时才能够执行。否则, forName 方法将抛出一个checkedexception ( 已检查异常)。无论何时使用这个方法, 都应该提供一个异常处理器( exception
handler ) 。

在启动时, 包含main 方法的类被加载。它会加载所有需要的类。这些被加栽的类又要加载它们需要的类, 以此类推。对于一个大型的应用程序来说, 这将会消耗很多时间, 用户会因此感到不耐烦。可以使用下面这个技巧给用户一种启动速度比较快的幻觉。不过,要确保包含main 方法的类没有显式地引用其他的类。首先, 显示一个启动画面;然后, 通过调用Class.forName 手工地加载其他的类。

获得Class 类对象的第三种方法非常简单。如果T 是任意的Java 类型(或void 关键字),T.class 将代表匹配的类对象。
请注意, 一个Class 对象实际上表示的是一个类型, 而这个类型未必一定是一种类。例如,int 不是类, 但int.class 是一个Class 类型的对象。

Class 类实际上是一个泛型类。例如, Employee.class 的类型是Class< Employee >。没有说明这个问题的原因是: 它将已经抽象的概念更加复杂化了。在大多数实际问题中, 可以忽略类型参数, 而使用原始的Class 类。

还有一个很有用的方法newlnstance( ), 可以用来动态地创建一个类的实例。

    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        Employee employee = new Employee("程序员1号", 50000, 1989, 10, 1);
        Employee employee1 = employee.getClass().newInstance();

    }

创建了一个与e 具有相同类类型的实例。newlnstance 方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认的构造器, 就会抛出一个异常。将forName 与newlnstance 配合起来使用, 可以根据存储在字符串中的类名创建一个对象。

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        String s = "java.util.Random";
        Object m = Class.forName (s).newInstance() ;
    }

2.利用反射分析类的能力

下面简要地介绍一下反射机制最重要的内容—检查类的结构

    在java.lang.reflect 包中有三个类Field、Method 和Constructor 分别用于描述类的属性、方法和构造器。这三个类都有一个叫做getName 的方法, 用来返回项目的名称。Field类有一个getType 方法, 用来返回描述属性所属类型的Class 对象。Method 和Constructor 类有能够报告参数类型的方法, Method 类还有一个可以报告返回类型的方法。这三个类还有一个叫做getModifiers的方法, 它将返回一个整型数值, 用不同的位开关描述public 和static 这样的修饰符使用状况。

    另外, 还可以利用java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的整型数值。例如, 可以使用Modifier 类中的isPublic、isPrivate 或isFinal判断方法或构造器是否是public、private 或final。我们需要做的全部工作就是调用Modifier类的相应方法, 并对返回的整型数值进行分析, 另外, 还可以利用Modifier.toString 方法将修饰符打印出来。      Class 类中的getFields、getMethods 和getConstructors 方法将分别返回类提供的public属性、方法和构造器数组, 其中包括父类的公有成员。Class 类的getDeclareFields、getDeclareMethods 和getDeclaredConstructors 方法将分别返回类中声明的全部属性、方法和构造器, 其中包括私有和受保护成员,但不包括父类的成员。

如果大家使用过aop的话这里的部分方法应该不陌生(感兴趣可以了解一下aop是什么)。

3.在运行时使用反射分析对象

本节将进一步查看数据属性的实际内容。当然, 在编写程序时, 如果知道想要査看的属性名和类型, 查看指定的属性是一件很容易的事情。而利用反射机制可以查看在编译时还不清楚的对象属性。

     查看对象属性的关键方法是Field 类中的get 方法。如果f 是一个Field 类型的对象(例如,通过getDeclaredFields 得到的对象), obj 是某个包含f 属性的类的对象,f.get(obj) 将返回一个对象,其值为obj 属性的当前值。
public class EmployeeTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Employee employee = new Employee("程序员1号", 50000, 1989, 10, 1);
        Class cl = employee.getClass();
        Field f = cl .getDeclaredField("name");
        Object v = f.get(employee) ;
        System.out.println(v);
    }
}

实际上,这段代码存在一个问题。由于name 是一个私有属性, 所以get 方法将会抛出一个IllegalAccessException异常。只有利用get 方法才能得到可访问属性的值。除非拥有访问权限,否则Java 安全机制只允许査看任意对象有哪些属性, 而不允许读取它们的值。
在这里插入图片描述

     反射机制的默认行为受限于Java 的访问控制。然而, 如果一个Java 程序没有受到安全管理器的控制, 就可以覆盖访问控制。为了达到这个目的, 需要调用Field、Method 或Constructor 对象的setAccessible方法。
f.setAccessible(true);

全部代码

public class EmployeeTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Employee employee = new Employee("程序员1号", 50000, 1989, 10, 1);
        Class cl = employee.getClass();
        Field f = cl .getDeclaredField("name");
        f.setAccessible(true);
        Object v = f.get(employee) ;
        System.out.println(v);
    }
}

结果
在这里插入图片描述
setAccessible方法是AccessibleObject 类中的一个方法, 它是Field、Method 和Constructor类的公共父类。这个特性是为调试、持久存储和相似机制提供的。稍后将利用它编写一个通用的toString 方法。

     get 方法还有一个需要解决的问题。name 属性是一个String, 因此把它作为Object 返回没有什么问题。但是, 假定我们想要查看salary属性。它属于double 类型, 而Java 中数值类型不是对象。要想解决这个问题, 可以使用Field 类中的getDouble 方法, 也可以调用get方法, 此时, 反射机制将会自动地将这个属性值打包到相应的对象包装器中, 这里将打包成Double。 当然,可以获得就可以设置。调用f.set(obj,value) 可以将obj 对象的f 属性设置成新值。

    =》编写一个可供任意类使用的通用toString 方法。其中使用getDeclaredFileds 获得所有的数据属性, 然后使用setAccessible 将所有的属性设置为可访问的。对于每个属性,获得了名字和值。《=

泛型toString 方法需要解释几个复杂的问题。循环引用将有可能导致无限递归。因此,ObjectAnalyzer 将记录已经被访问过的对象。另外, 为了能够査看数组内部, 需要采用一种不同的方式。有关这种方式的具体内容将在后续博客中详细描写。
public class ObjectAnalyzer {
    private ArrayList<Object> visited = new ArrayList<>();

    public String toString(Object obj) {
        if (obj == null) {
            return "null";
        }
        if (visited.contains(obj)) {
            return "重复";
        }
        visited.add(obj);
        Class<?> cl = obj.getClass();
        if (cl == String.class) {
            return (String) obj;
        }
        if (cl.isArray()) {
            String r = cl.getComponentType() + "[]"+"\n"+"{"+"\n";
            for (int i = 0; i < Array.getLength(obj); i++) {
                if (i > 0) {
                    r += "," + "\n";
                }
                Object val = Array.get(obj, i);
                if (cl.getComponentType().isPrimitive()) {
                    r += val;
                } else {
                    r += toString(val);
                }
            }
            return r +"\n"+ "}";
        }

        String r = cl.getName();

        do {
            r += "[";
            Field[] fields = cl.getDeclaredFields();
            AccessibleObject.setAccessible(fields, true);
            for (Field f : fields) {
                if (!Modifier.isStatic(f.getModifiers())) {
                    if (!r.endsWith("[")) {
                        r += ",";
                    }
                    r += f.getName() + "=";
                    try {
                        Class t = f.getType();
                        Object val = f.get(obj);
                        if (t.isPrimitive()) {
                            r += val;
                        } else {
                            r += toString(val);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            r += "]";
            cl = cl.getSuperclass();
        } while (cl != null);
        return r;
    }
}

测试类

    public static void main(String[] args) {
        ArrayList<Integer> squares = new ArrayList<>();
        for (int i = 1; i < 6; i++) {
            squares.add(i * i);
        }
        System.out.println(new ObjectAnalyzer().toString(squares));
    }

结果
在这里插入图片描述

4.使用反射编写泛型数组代码

java.lang.reflect 包中的Array 类允许动态地创建数组。例如, 将这个特性应用到Array类中的copyOf方法实现中, 这个方法可以用于扩展已经填满的数组。

public class EmployeeTest {
    public static void main(String[] args) {
        Employee[] a = new Employee[100];
        a = Arrays.copyOf(a, 2 * a.length);
        System.out.println(a.length);
    }
}

结果
在这里插入图片描述

如何编写这样一个通用的方法呢? 正好能够将Employee[ ] 数组转换为Object[ ] 数组。

    =》第一次尝试改造《=

    public static Object[] badCopyOf(Object[] a, int newLength){
        Object[] newArray = new Object[newLength];
        System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength)) ;
        return newArray;
    }

然而, 在实际使用结果数组时会遇到一个问题。这段代码返回的数组类型是对象数组(Object[ ]) 类型,
这是由于使用下面这行代码创建的数组:

new Object[newLength]
     一个对象数组不能转换成打工人数组( Employee[ ] )。如果这样做, 则在运行时Java 将会产生ClassCastException 异常。前面已经看到,Java 数组会记住每个元素的类型, 即创建数组时new 表达式中使用的元素类型。      将一个Employee[ ] 临时地转换成Object[ ] 数组, 然后再把它转换回来是可以的,但一从开始就是Object[ ] 的数组却永远不能转换成Employe[ ]数组。      为了编写这类通用的数组代码, 需要能够创建与原数组类型相同的新数组。为此, 需要java.lang.reflect 包中Array 类的一些方法。其中最关键的是Array 类中的静态方法newlnstance,它能够构造新数组。在调用它时必须提供两个参数,一个是数组的元素类型,一个是数组的 长度。
Object newArray = Array.newInstance(componentType, newLength);

为了能够实际地运行,需要获得新数组的长度和元素类型。
可以通过调用Array.getLength(a) 获得数组的长度, 也可以通过Array 类的静态getLength方法的返回值得到任意数组的长度。而要获得新数组元素类型,就需要进行以下工作:

  1. 首先获得a 数组的类对象。
  2. 确认它是一个数组。
  3. 使用Class 类(只能定义表示数组的类对象)的getComponentType 方法确定数组对应的类型。
    public static Object goodCopyOf(Object a, int newLength) {
        Class cl = a.getClass();
        if (!cl.isArray()) {
            return null;
        }
        Class componentType = cl.getComponentType();
        int length = Array.getLength(a);
        Object newArray = Array.newInstance(componentType, newLength);
        System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
        return newArray;
    }

这个CopyOf 方法可以用来扩展任意类型的数组, 而不仅是对象数组。

intn a = { 12, 3, 4, 5 };
a = (int[]) goodCopyOf (a, 10) ;

为了能够实现上述操作,应该将goodCopyOf 的参数声明为Object 类型,.而不要声明为对象型数组(Object[ ])。整型数组类型 int[ ]可以被转换成Object 但不能转换成对象数组。

测试类

public class CopyOfTest {
    public static void main(String[] args) {
        int[] a = {1, 2, 3};
        a = (int[]) goodCopyOf(a, 10);
        System.out.println(Arrays.toString(a));
        String[] b = {"张三", "李四", "王五" };
        b = (String[]) goodCopyOf(b, 10);
        System.out.println(Arrays.toString(b));
//        b = (String[]) badCopyOf(b, 10) ;
//        System.out.println(Arrays.toString(b));
        
    }

    public static Object[] badCopyOf(Object[] a, int newLength) {
        Object[] newArray = new Object[newLength];
        System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
        return newArray;
    }

    public static Object goodCopyOf(Object a, int newLength) {
        Class cl = a.getClass();
        if (!cl.isArray()) {
            return null;
        }
        Class componentType = cl.getComponentType();
        int length = Array.getLength(a);
        Object newArray = Array.newInstance(componentType, newLength);
        System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
        return newArray;
    }
}

结果
在这里插入图片描述

5.调用任意方法

在C 和C++ 中, 可以从函数指针执行任意函数。从表面上看, Java 没有提供方法指针,即将一个方法的存储地址传给另外一个方法, 以便第二个方法能够随后调用它。事实上,Java 的设计者曾说过: 方法指针是很危险的, 并且常常会带来隐患。他们认为Java 提供的接口(interface ) ( 将在后续讲解·)是一种更好的解决方案。然而, 反射机制允许你调用任意方法。

为了能够看到方法指针的工作过程, 先回忆一下利用Field 类的get 方法查看对象属性的过程。与之类似, 在Method 类中有一个invoke 方法, 它允许调用包装在当前Method 对象中的方法。invoke 方法的:

    @CallerSensitive
    public Object invoke(Object var1, Object... var2) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        if (!this.override && !Reflection.quickCheckMemberAccess(this.clazz, this.modifiers)) {
            Class var3 = Reflection.getCallerClass();
            this.checkAccess(var3, this.clazz, var1, this.modifiers);
        }

        MethodAccessor var4 = this.methodAccessor;
        if (var4 == null) {
            var4 = this.acquireMethodAccessor();
        }

        return var4.invoke(var1, var2);
    }

这里不展开讲解了,还是建议使用接口的方式。

可以使用method 对象实现C ( 或C# 中的委派)语言中函数指针的所有操作。同C 一样,这种程序设计风格并不太简便,出错的可能性也比较大。如果在调用方法的时候提供了一个错误的参数,那么invoke 方法将会抛出一个异常另外, invoke 的参数和返回值必须是Object 类型的。这就意味着必须进行多次的类型转换。这样做将会使编译器错过检查代码的机会。因此, 等到测试阶段才会发现这些错误, 找到并改正它们将会更加困难。不仅如此, 使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些。

建议仅在必要的时候才使用Method 对象,而最好使用接口以及Java SE 8 中的lambda 表达式(第6 章中介绍)。特别要重申: 建议Java 开发者不要使用Method 对象的回调功能。使用接口进行回调会使得代码的执行速度更快, 更易于维护。

八、继承的设计技巧

最后,给出一些对设计继承关系很有帮助的建议。

  1. 将公共操作和属性放在父类
    这就是为什么将姓名属性放在Person 类中,而没有将它放在Employee 和Student 类中的原因。

  2. 不要使用受保护的属性
    有些程序员认为,将大多数的实例属性定义为protected 是一个不错的主意, 只有这样, 子类才能够在需要的时候直接访问它们。然而, protected 机制并不能够带来更好的保护, 其原因主要有两点。第一,子类集合是无限制的, 任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected 的实例属性, 从而破坏了封装性。第二, 在Java 程序设计语言中,在同一个包中的所有类都可以访问proteced属性,而不管它是否为这个类的子类。不过,protected 方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。

  3. 使用继承实现“ is-a” 关系
    使用继承很容易达到节省代码的目的,但有时候也被人们滥用了。例如, 假设需要定义一个钟点工类。钟点工的信息包含姓名和雇佣日期, 但是没有薪水。他们按小时计薪,并且不会因为拖延时间而获得加薪,这似乎在诱导人们由Employee 派生出子类Contractor, 然后再增加一个hourlyWage属性。这并不是一个好主意。因为这样一来, 每个钟点;对象中都包含了薪水和计时工资这两个属性。在实现打印支票或税单方法的时候, 会带来无尽麻烦, 并且与不采用继承, 会多写很多代码。钟点工与程序员(打工人)之间不属于“ is-a” 关系。钟点工不是特殊的程序员。

  4. 除非所有继承的方法都有意义, 否则不要使用继承
    假设想编写一个Holiday类。毫无疑问, 每个假日也是一日,并且一日可以用GregorianCalendar 类的实例表示,因此可以使用继承。很遗憾, 在继承的操作中, 假日集不是封闭的。在GregorianCalendar 中有一个公有方法add, 可以将假日转换成非假日:因此,继承对于这个例子来说并不太适宜。需要指出, 如果扩展LocalDate 就不会出现这个问题。由于这个类是不可变的, 所以没有任何方法会把假日变成非假日。

  5. 在覆盖方法时, 不要改变预期的行为
    置换原则不仅应用于语法, 而且也可以应用于行为,这似乎更加重要。在覆盖一个方法的时候, 不应该毫无原由地改变行为的内涵。就这一点而言,编译器不会提供任何帮助, 即编译器不会检查重新定义的方法是否有意义。例如,可以重定义Holiday 类中add 方法“ 修正” 原方法的问题,或什么也不做, 或抛出一个异常, 或继续到下一个假日。然而这些都违反了置换原则。不管x 属于GregorianCalendar 类, 还是属于Holiday 类, 执行上述语句后都应该得到预期的行为。

    int d1 = x.get(Calendar.DAY_OF_MONTH) ;
    x.add(Calendar.DAY_OF_MONTH , 1) ;
    int d2 = x.get(Calendar.DAY_OF_MONTH) ;
    System.out.println(d2 - d1) ;
    

    当然, 这样可能会引起某些争议。人们可能就预期行为的含义争论不休。例如, 有些人争论说, 置换原则要求Manager.equals 不处理bonus 属性, 因为Employee.equals 没有它。实际上, 凭空讨论这些问题毫无意义。关键在于, 在覆盖子类中的方法时,不要偏离最初的设计想法。

  6. 使用多态, 而非类型信息
    无论什么时候,对于下面这种形式的代码

        if (x is of type1){
           action1(x);
        } else if (x is of type2){
            action2(x);
        }
    

    都应该考虑使用多态性。action1与action2表示的是相同的概念吗? 如果是相同的概念, 就应该为这个概念定义一
    个方法, 并将其放置在两个类的父类或接口中,然后, 就可以调用x.action方法。以便使用多态性提供的动态分派机制执行相应的动作。使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。

  7. 不要过多地使用反射
    反射机制使得人们可以通过在运行时查看属性和方法, 让人们编写出更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误, 因此只有在运行时才发现错误并导致异常。

    继承END~


  1. Java 虚拟机 (Java Virtual Machine, JVM) 是一个基于栈的计算机器。它是 Java 语言的核心和关键,负责将编译后的 Java 代码翻译成计算机能够理解和执行的机器码,从而实现 Java 的跨平台特性。
    JVM 有以下几个重要的组成部分:
    1.类加载器 (Class Loader):负责将编译后的 Java 代码加载到内存中,并构造出相应的 Java 类。
    2.运行时数据区 (Runtime Data Area):JVM 把它看做是一个公共存储区域,所有的线程都可以访问它,包括堆、栈、方法区等。
    3.执行引擎 (Execution Engine):执行 Java 代码的核心部分。通过解释器实现代码解释和执行,也通过即时编译器提高代码执行效率。
    4.Java Native Interface (JNI):提供了 Java 与本地代码交互的接口。
    5.本地方法库 (Native Method Library):存放本机代码的库。
    JVM 的主要作用是,为开发人员提供一个统一标准的运行环境,在不同平台上运行编写好的 Java 代码,从而实现跨平台性。同时,JVM 还具有自动内存管理功能、垃圾回收功能、安全性等重要特性。 ↩︎

  2. 隐式参数是指通过方法调用所在的对象来访问的参数。在Java中,每个非静态方法都有一个隐式参数,即该方法所属的对象,也称为方法的接收者。这个隐式参数使用关键字this表示。当方法被调用时,调用该方法的对象会作为隐式参数传递给方法。
    例如:
    在这里插入图片描述
    在上面的例子中,printName()方法中的隐式参数this表示了调用该方法的对象。
    显式参数是在方法调用时明确提供的参数。它们通常由方法的定义者和调用者共同约定,在方法调用时必须提供相应的参数值。显式参数使用方法调用语法中的实际参数列表表示。
    例如:
    在这里插入图片描述
    在上面的例子中,add()方法的定义中有两个显式参数a和b,在方法调用时通过提供实际参数的方式传递给方法。
    总结来说,隐式参数是通过方法调用所在的对象来访问的参数,而显式参数是在方法调用时明确提供的参数。它们一起构成了方法的参数列表,用于传递数据给方法并参与方法的执行 ↩︎

  3. OOP三大特征是封装、继承和多态。
    1.封装:封装是指将数据和对数据的操作封装在一起,形成一个完整的对象。通过封装,可以将对象的内部实现隐藏起来,只对外暴露必要的接口,提高了代码的安全性和可维护性。封装可以通过访问修饰符(如public、private、protected)来实现对成员变量和方法的访问控制。
    2.继承:继承是指通过已有的类创建新的类,并且新类拥有原有类的属性和方法。通过继承,可以实现代码的重用,减少重复编写相似功能的代码。在继承关系中,父类也称为基类或超类,子类也称为派生类。子类可以继承父类的公共成员,同时还可以添加自己特有的成员。
    3.多态:多态是指同一种类型的对象,在不同的情况下表现出不同的行为。多态性可以通过继承和接口实现。通过继承,子类可以重写父类的方法,从而改变方法的行为。通过接口,不同的类可以实现相同的接口,但具体的实现方式可以不同。多态性提高了代码的灵活性和扩展性,使得程序更容易适应变化。
    五大设计原则是SOLID原则,包括单一职责原则(Single Responsibility Principle, SRP)、开放封闭原则(Open-Closed Principle, OCP)、里氏替换原则(Liskov Substitution Principle, LSP)、接口隔离原则(Interface Segregation Principle, ISP)和依赖倒置原则(Dependency Inversion Principle, DIP)。
    1.单一职责原则:一个类应该只负责一项功能,即一个类只有一个引起它变化的原因。这样可以提高类的内聚性,降低类的耦合度,使得代码更加清晰、可维护和可扩展。
    2.开放封闭原则:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。意味着当需求发生变化时,应通过增加新的代码来扩展功能,而不是修改已有的代码。通过遵循该原则,可以保持系统的稳定性和灵活性。
    3.里氏替换原则:父类的对象可以被子类的对象替代,而不影响程序的正确性。子类在继承父类时,不能改变父类原有的行为。该原则保证了继承关系的正确性,同时提高了代码的可扩展性和复用性。
    4.接口隔离原则:客户端不应该依赖它不需要的接口。一个类对其他类的依赖应该建立在最小的接口上,尽量避免对不相关的接口产生依赖。通过接口隔离原则,可以减少类之间的耦合度,提高系统的灵活性和可维护性。
    5.依赖倒置原则:高层模块不应该依赖低层模块,二者都应该依赖于抽象接口。抽象不应该依赖具体实现,而具体实现应该依赖于抽象。通过依赖倒置原则,可以降低模块之间的耦合度,提高代码的可扩展性和复用性。 ↩︎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

叫我柒月

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

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

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

打赏作者

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

抵扣说明:

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

余额充值