美文网首页
四、继承

四、继承

作者: Dcl_Snow | 来源:发表于2019-04-08 10:04 被阅读0次

继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求。

类、超类和子类

定义子类

关键字“extends”表示继承。已存在的类称为超类、基类或父类。新类称为子类、派生类或孩子类。
在通过扩展超类定义子类的时候,仅需指出子类域超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中。

覆盖方法(override)

超类中有些方法对子类并不一定适用。
创建一个超类:

public class Employee {
    private String name;
    private double salary;

    public Employee(String n, double s){
        name = n;
        salary = s;
    }

    public String getName(){
        return name;
    }

    public double getSalary(){
        return salary;
    }
}

创建一个子类:

public class Manager extends Employee {
    private String name;
    private double salary;
    private double bonus;

    public Employee(String n, double s){
        name = n;
        salary = s;
    }

    public String getName(){
        return name;
    }

    public double getSalary(){
        double sumSalary = super.getSalary();
        return sumSalary + bonus;
    }

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

这里Employee超类中的getSalary()方法就不适用Manager子类了,所以子类中提供了一个新的方法来覆盖超类中的这个方法。

子类构造器

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

这里的super是“调用超类Employee中含有name、salary参数的构造器”的简写形式。
由于子类构造器不能访问超类的私有域,所以必须使用超类的构造器(super)对这部分私有域进行初始化。
使用super调用构造器的语句必须是子类构造器的第一条语句。
若子类的构造器没有显式的调用超类的构造器,则将自动调用超类默认(无参数)的构造器。
若超类没有不带参数的构造器,并且子类中的构造器又没有显式的调用超类的其他构造器,Java编译器将会报错。

继承层次

继承并不仅限于一个层次:


继承层次.png

由一个公共超类派生出来的所有类的集合被称为继承层次。
在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链。

多态

一个对象变量可以指示多种实例类型的现象被称为多态。
例如:一个超类类型的变量既可以指示超类的实例,也可以指示子类的实例类型。当程序运行时可以自动的选择使用超类的方法,也可以调用子类的方法。
这种在运行时能够自动地选择调用哪个方法的现象称为动态绑定
动态绑定的一个非常重要的特性:无需对现存代码进行修改,就可以对程序进行扩展。(假设增加一个新的子类N,并且变量e有可能引用N的对象,则不需要对包含调用e.xxx的方法进行重新编译。)
有一个用来判断是否应该设计为继承关系的简单规则,就是“is - a”规则,表明每个子类的对象也是超类的对象。
“is - a”规则的另一种表达时置换法则,表明程序中出现超类对象的任何地方都可以用子类对象置换。即可以将子类的引用赋给超类变量,但是不能将超类的引用赋给子类变量。

理解方法调用

假设要调用x.f(args)方法,隐式参数x声明为类C的一个对象。

  1. 编译器查看对象的声明类型和方法名。假设调用x.f(param),但是C类的对象可能存在多个名字为f,但是参数类型不同的方法(f(int)或者f(String)),编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法。
    此时编译器已获得所有可能被调用的候选方法。
  2. 然后编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析。由于允许类型转换,所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,则会报错。
    此时编译器已获得需要调用的方法名字和参数类型。
  3. 如果是private方法,static方法、final方法或者构造器,那么编译器将可以准确的知道应该调用哪个方法,这种调用方式称为静态绑定
  4. 当程序运行时,并且采用动态绑定调用方法时,虚拟机一定要调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型时D,D类时C类的子类,现在调用x.f(String),如果D类定义了方法f(String),就直接调用它;否则将在C类中寻找f(String)方法,以此类推。
    每次调用方法都进行搜索,时间开销很大。所以虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。这样在调用方法的时候,虚拟机仅仅需要查找这个表就可以了。

阻止继承:final类和final方法

有时候可能不希望将某个类作为超类来定义子类,这种不允许扩展的类被称为final类。格式如下:
public final class 类名 {}
此外类中的特定方法也可以被声明为final方法,此时就不能覆盖这个方法,final类中的所有方法自动的成为final方法。
强制类型转换
有时候可能需要将某个类的对象引用转换成另一个类的对象引用,就像前面有时候需要将浮点类型转换成整型数值一样,转换的语法类似。
进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。
一个良好的设计习惯:在进行类型转换之前,先查看一下是否可以成功进行转换,使用instenceof操作符就可以实现。

抽象类

如果自下而上在类的继承层次机构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度上看,祖先类更加通用,实际使用时只将它作为派生其他类的基类,而不作为想使用的特定的实例类。
抽象类和抽象方法使用abstract关键字修饰,格式如下:

public abstract class Person {
    public abstract String getDescription();
}

\color{red}{注意:}

  • \color{red}{抽象类不能被实例化}
  • \color{red}{包含一个或多个抽象方法的类本身必须被声明为抽象类。}
  • \color{red}{类即使不包含抽象方法,也可以将该类声明为抽象类。}

抽象类中可以包含抽象方法,也可以包含具体数据和具体方法:

public abstract class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public abstract String getDescription();
    public String getName() {
        return name;
    }
}

抽象方法充当着占位的角色,具体实现在子类中。扩展抽象类可以有两种选择:
第1种是在抽象类中定义部分抽象方法或不定义抽象类方法,这样就必须将子类也标记为抽象类。
第2种是定义全部的抽象方法,这样子类就不是抽象的了。

受保护的访问

Java用户控制可见性的4个修饰符:

  • private 仅对本类可见。
  • protected 对本包和所有子类可见。
  • public 对所有类可见。
  • 默认(无修饰符) 对本包可见。
    在实际应用种,要谨慎使用protected属性。假设需要将设计的类提供给其他程序员使用,而在这个类种设置了一些受保护域,由于其他程序员可以由这个类再派生出新类,并访问其中的受保护域,因此如果需要对这个类的实现进行修改,就必须通知所有使用这个类的程序员,这违背了OOP提倡的数据封装原则。

Object:所有类的超类

Object类是Java中所有类的始祖,再Java中的每个类都是由它扩展而来的。
在Java中只有基本类型不是对象,例如数值、字符和布尔类型的值都不是对象。所有的数组类型,不论是对象数组还是基本类型的数组都扩展了Object类。
Object类有很多重要的方法。

equals方法

equals方法用于检测一个对象是否等于另外一个对象。在Object类中,这个方法将判断两个对象是否具有相同的引用。
Object类可以在JDK的安装路径下找到,打开JDK的安装目录,里面有一个名为src.zip的压缩包,将这个包解压缩,在java目录下的lang目录下可以找到Object类,可以使用notepad++打开Object.java文件,可以看到equals方法代码如下:

public boolean equals(Object obj) {
    return (this == obj);
}

如果两个对象具有相同的引用,它们一定是相等的。对于多数类来说,这种判断并没有什么意义,例如采用这种方式比较两个PrintStream对象是否相等就完全没有意义,所以很多时候会重写equals方法,例如String的equals方法,就对Object的该方法进行了重写:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

再来看一下java.util.Objects类中的equals方法:

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

\color{red}{注意:}
\color{red}{在子类中定义equals方法时,首先调用超类的equals方法。}
\color{red}{如果检测失败,对象就不可能相等。如果超类中的域都相等,再比较子类中的实例域是否相等。}

相等测试与继承

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

  • 自反性:对于任意非空引用x,x.equals(x)应该返回true。
  • 对称性:对于任意非空引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true。
  • 传递性:对于任意非空引用x,y和z,如果x.equals(y)返回true,y.equals(z)也返回true,则x.equals(z)也应该返回true。
  • 一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
  • 对于任意非空引用x,x.equals(null)应该返回false。
    如果隐式和显式的参数不属于用一个类,这种情况该怎么处理呢。
    前面使用instanceof进行检测过,此时不能在超类中重写的equals方法中使用instanceof进行检测,因为这样做无法解决显式参数是子类的情况,因为使用instanceof检测会返回true。
    例如e是超类的一个对象,m是子类的一个对象,两个对象的域相同,如果e.equals(m)中使用了instanceof进行检测,则返回true,意味着m.equals(e)也应该返回true,因为对称性不允许这个方法调用返回false,或者抛出异常。
    因此使子类抽到了限制,子类的equals方法必须能够用自己与任何一个超类对象进行比较,而不考虑子类拥有的那部分特有的域信息,所以使用instanceof进行检测并不完美。
    可以分成两种情况看待这个问题:
  • 如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测。
  • 如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同的子类的对象之间进行相等的比较。
    如果在子类中重新定义equals方法,就要在其中包含调用super.equals()方法。

hashCode

hash code(散列码)是由对象导出的一个整型值。散列码是没有规律的。
两个不同对象的hashCode()基本上不会相同。如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。
hashCode方法应该返回一个整型数值(也可以是负数),并且合理的组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。
Equals域hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须域y.hashCode()具有相同的值。

toString方法

Object类还有一个重要的方法,就是toString方法,它用于返回表示对象值的字符串。
toString方法是随处可见的方法,因为只要对象于一个字符串通过操作符“+”连接,Java编译就会自动的调用toString方法,以便获得这个对象的字符串描述。
\color{red}{注意:}
\color{red}{在调用x.toString()的地方可以用""+x代替。这条语句将一个空串域x的字符串相连接,这里的x就是x.toString()。}
\color{red}{即使x使基本类型,这条语句依然有效。}

泛型数组列表

前面学习了数组,数组可以保存多个元素,但在某些情况下无法确定到底要保存多少个元素,此时数组将不再适用,因为一旦确定了数组的大小,就不能改变。
在Java中解决这个问题最简单的方法使使用另一个类:ArrayList。该类使用起来有点像数组,但是在增加或者删除元素时,具有自动调节数组容量的功能。
ArrayList是一个采用类型参数泛型类。为了指定数组列表保存的元素对象类型,需要使用一对尖括号将类名括起来加在后面,例如:ArrayList<String>。
创建一个ArrayList的对象格式如下:

ArrayList<数据类型> 变量名 = new ArrayList<数据类型>();

但是尖括号中的类型必须是引用数据类型,不能是基本数据类型。

基本数据类型 对应的引用数据类型的表现形式:
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

创建示例如下:

ArrayList<String> list = new ArrayList<String>();
ArrayList<Integer> list = new ArrayList<String>();
ArrayList<Person> list = new ArrayList<String>();

使用add方法可以将元素添加到数组列表中,默认新增元素是追加到集合的末尾,但是也可以给add方法传递一个位置参数,用来在数组列表中间插入元素,位于指定位置之后的所有元素都要向后移动一个位置;可以用remove方法删除数据列表中的元素;set方法实现改变元素的操作;get方法可以返回集合中指定位置上的元素;size方法返回集合中元素的个数;clear方法用于清空数组列表中的所有元素。

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<String>();
    list.add("stu1");
    list.add("stu2");
    list.set(0,"stu3");
    System.out.println("集合的长度:" + list.size());
    System.out.println("第1个元素是:" + list.get(0));
    System.out.println("第2个元素是:" + list.get(1));
}

既然ArrayList相当于是一个长度可变的数组,所以访问集合中的元素也与数组元素的访问一样,采用索引方式访问。
数组列表管理着对象引用的一个内部数组。最终数组的全部空间有可能被用尽。这就显现出数组列表的操作魅力:如果调用add方法且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
如果使用时已知初始容量(例如为10),可以将该值传递给ArrayList构造器:

ArrayList<String> list = new ArrayList<>(10);

遍历数组列表与遍历数组的方式相同,都可以使用for循环和for each循环进行遍历。

对象包装器与自动装箱

有时需要将int这样的基本类型转换为对象,前面的table中已经列出了所有基本类型对应的类,Integer类对应基本类型int,Integer等这些类就称为包装器。
对象包装器类是不可变的,一旦构造了包装器,就不允许更改包装在其中的值,同时对象包装器还是final,不能定义它们的子类。
想要声明一个整型的数组列表,不能写成ArrayList<int>,而应该写成如下形式:

ArrayList<Integer> list = new ArrayList<>();

一个很有用的特性,可以更方便与添加int类型的元素到ArrayList<Integer>中:

list.add(3);

将自动的变换成:

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

这种变换就称为自动装箱
相反当将一个Integer对象赋给一个int值时,将会自动地拆箱。编译器会将如下语句:

int n = list.get(i);

翻译成:

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

在算术表达式中也能够自动地装箱和拆箱,例如自增操作符应用于一个包装器引用:

Integer n = 1;
n++;

此时编译器将自动的插入一条对象拆箱的指令,然后进行自增计算,最后再将结果装箱。
\color{red}{注意:}
由于包装器类引用是可以为null的,所以自动装箱有可能会抛出一个NullPointerException异常。
如果一个条件表达式中混合使用Integer和Double类型,Integer值就会拆箱,提升为double,再装箱为Double。
装箱和拆箱时编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机知识执行这些字节码。

参数数量可变的方法

现在的Java提供了可以用可变的参数数量调用的方法(“变参”方法)。
printf方法是这样定义的:

public class PrintStream {
    public PrintStream printf(String fmt, Object... args) {
        return format(fmt, args);
    }
}

这里的...是Java代码的一部分,表明这个方法除了fmt参数之外,还可以接收任意数量的对象。实际上printf方法接收两个参数,一个是格式字符串,一个是Object[]数组,其中保存着所有的参数(如果调用者提供的是整型数组或者其他基本类型的值,自动装箱功能将把它们转换成对象)。现在将扫描fmt字符串,并将第i个格式说明符与args[i]的值相匹配。

枚举类

前面已经定学习过如何定义枚举类型。

public enum  SeasonEnum {
    SPRING,SUMMER,FALL,WINTER
}

实际上这个声明定义的是一个类,类中有4个实例,在此尽量不要构造新对象。
因此在比较两个枚举类型的值时,永远不需要使用equals方法,直接使用“==”即可。
因为示例代码中只写了内容队列,所以后面不用加分号“;”。
枚举类型中可以添加构造器、方法和域。构造器只是在构造枚举常量的时候被调用。当添加了构造器、方法和域时,内容对列后面就需要加分号“;”。

public enum  SeasonEnum {
    SPRING,SUMMER,FALL,WINTER;
    private  int  other;
}

所有的枚举类型都是Enum类的子类,所以其超类不是Object类。所以继承了Enum类的很多方法,其中toString()方法能够返回枚举常量名,例如:SeanEnum.SPRING.toString()将返回字符串“SPRING”。toString的逆方法是valueOf()。

SeanEnum s = Enum.valueOf(SeanEnum.class, "SPRING");

将s设置成SeanEnum.SPRING。
每个枚举类型都有一个静态的values方法,返回一个包含全部枚举值的数组。

SeanEnum[] v = SeanEnum.values;

返回包含元素SeanEnum.SPRING、SeanEnum.SUMMER、SeanEnum.FALL、SeanEnum.WINTER的数组v。
ordinal方法返回enum声明中枚举常量的位置,位置从0开始计数。例如:SeanEnum.SPRING.ordinal()返回0。

反射

反射

Java的反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有域和方法;对于任意一个对象,都能够调用它的任意一个域和方法。这种动态获取信息以及动态调用对象的方法的功能称为Java语言的反射机制。
反射机制具体功能包括:

  • 运行时分析类的能力。
  • 在运行时查看对象。
  • 实现通用的数组操作代码。
  • 利用Method对象。

在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识,这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。
在Java中有专门的类访问这些信息,保存这些信息的类被称为Class。
\color{red}{注意:这个名字很容易混淆而使人不理解,重点注意区分.class文件(字节码文件)和Class类的对象。}
\color{red}{反射的过程:}
\color{red}{加载.class文件到内存中-->系统在内存中自动生成.class文件的Class类的对象}
\color{red}{-->由这些Class类的对象可以反向获取类的信息(域、构造器、方法等)}
\color{red}{-->进而使用这些类的信息。}

类的加载

当程序要使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对类进行初始化。
加载就是指将编译后的.class文件读入内存,并为之创建一个.class文件(字节码文件)的Class对象。任何类被使用时,系统都会为它建立一个Class类的对象(该对象只能由系统自动创建,终生唯一,不能由使用者自定义创建)
连接首先是验证类是或否有正确的内部结构;然后进行准备,为类的静态成员分配内存,并设置默认初始化值;第三是解析,将类的二进制数据中的符号引用替换为直接引用,以节省计算机资源。
初始化就是Java类中的正常初始化。

类初始化的时机

  • 创建类的实例。
  • 调用类的静态变量,或者为静态变量赋值。
  • 类的静态方法。
  • 初始化某个类的子类,其超类先加载到内存中。
  • 直接用java命令运行的类。
  • 使用反射方式去创建某个类或者接口对应的对象时。

类加载器

负责将编译后的.class文件加载到内存中,并且为它生成对应的Class对象。
三种类加载器

  • Bootstrap ClassLoader:根类加载器,也被称为引导类加载器,负责Java核心类(String、System等)的加载,在JDK目录下的JRE目录下的lib目录下的rt.jar文件中。
  • Extension ClassLoader:扩展类加载器,负责JRE的扩展目录中jar包的加载,在JDK目录下的JRE目录下的lib目录下的ext目录中。
  • System ClassLoader:系统类加载器,负责在JVM虚拟机启动时,加载来自java命令的class文件,以及CLASSPATH环境变量所指定的jar包和类路径。

获取.class文件对象的三种方式:

  1. 对象获取
  2. Class类的静态方法获取
  3. 类名获取
//1. 对象获取
Employee e = new Employee();
//调用其超类Object类中的getClass()方法,返回一个Class类型的实例。
Class c1 = e.getClass();
System.out.println(c1);

//2. Class类的静态方法forName(保存在字符串中的类全名,即:包.类名)获取
String className = "com.test.Employee"
Class c2 = Class.forName(className);
System.out.println(c2);

//3. 类名获取
Class c3 = Employee.class;
System.out.println(c3);

\color{red}{注意:}
\color{red}{Class类的静态方法forName()获取对象时,参数必须是类名或者接口名才能够执行。}
\color{red}{否则该方法会抛出一个checked exception,所以无论何时使用这个方法都应该提供一个异常处理器(throws ClassNotFoundException)。}
虚拟机为每个类型管理一个Class对象。因此可以使用“==”运算符(当然也可以使用equals方法)实现两个类对象的比较操作。

利用反射操作对象

\color{red}{过程是:获取.class文件对象-->从获取的.class文件对象中,获取需要的成员。}
java.lang.reflect包中的Constructor用于描述类的构造器。
获取构造器并运行:

  • 获取默认无参数构造器并运行:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

    try {
        Class c = Class.forName("Employee");
        Constructor constructor = c.getConstructor();
        Object object = constructor.newInstance();
        System.out.println(object);
    } catch (ClassNotFoundException e) {
        System.out.println("类名错误!");
    }
    System.out.println("获取无参数构造器程序执行完成!");
        
}

Constructor类的newInstance()方法,可以动态的创建一个Employee类的实例,调用默认构造器初始化新创建的对象。

  • 快捷获取默认无参数构造器并运行:
try {
    Class c = Class.forName("Employee");
    Object object = c.newInstance();
    System.out.println(object);
} catch (ClassNotFoundException e) {
    System.out.println("类名错误!");
}
System.out.println("使用Class类的newInstance快速获取无参数构造器程序执行完成!");

Class类的newInstance()方法,可以动态的创建一个Employee类的实例,调用默认构造器初始化新创建的对象。
\color{red}{注意:}
\color{red}{Class类的newInstance()方法只能调用默认的无参数构造器,如果这个类没有默认的构造器,会抛出一个异常。}
\color{red}{需要调用传递参数的构造器,则必须使用Constructor类的newInstance()方法。}

  • 获取有参数构造器并运行:
try {
    Class c = Class.forName("Employee");
    Constructor constructor = c.getConstructor(String.class,double.class,int.class,int.class,int.class);
    Object object = constructor.newInstance("Dcl_Snow",10000,2019,1,1);
    System.out.println(object);
} catch (ClassNotFoundException e) {
    System.out.println("类名错误!");
}
System.out.println("获取全参数构造器程序执行完成!");
  • 获取私有构造器并运行:
    先在Employee类中添加一个如下的私有构造器:
private Employee( double salary, String name, int year, int month, int day) {
    this.name = name;
    this.salary = salary;
    hireDay = LocalDate.of(year, month, day);
}

不推荐获取私有构造器,因为破坏了封装性):

try {
    Class c = Class.forName("Employee");
    Constructor constructor = c.getDeclaredConstructor(double.class, String.class, int.class, int.class,int.class);
    constructor.setAccessible(true);
    Object object = constructor.newInstance(10000, "Dcl_Snow", 2019, 1, 1);
    System.out.println(object);
} catch (ClassNotFoundException e) {
    System.out.println("类名错误!");
}
System.out.println("获取私有构造器程序执行完成!");

setAccessible()是Constructor的超类AccessibleObject 的方法:
将此对象的accessible标志设置为指示的布尔值。 true的值表示反射对象应该在使用时抑制Java语言访问检查。 false的值表示反映的对象应该强制执行Java语言访问检查。
java.lang.reflect包中的Constructor用于描述类的域。
获取域并更改域值:
先在Employee类中添加一个域:

public String sex;

获取域并设置域值:

public static void main(String[] args) throws IllegalAccessException, InstantiationException {

    try {
        Class c = Class.forName("Employee");
        Object object = c.newInstance();
        Field field = c.getField("sex");
        field.set(object, "man");
        System.out.println(object);
    } catch (ClassNotFoundException | NoSuchFieldException e) {
        System.out.println("类名错误!");
    }
    System.out.println("获取域并设置域值执行完成!");
}

获取方法并运行:

  • 获取空参数的方法并运行:
    先在Employee类中添加一个如下的空参数的方法:
public void work(){
    System.out.println("大家在工作!");
}

获取空参数的方法并运行:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

    try {
        Class c = Class.forName("Employee");
        Object object = c.newInstance();
        Method method = c.getMethod("work");
        method.invoke(object);
    } catch (ClassNotFoundException e) {
        System.out.println("类名错误!");
    }
    System.out.println("获取无参数方法并运行程序执行完成!");
}

使用getMethod()方法获取方法,然后使用invoke()方法执行该方法。
invoke()方法:在具有指定参数的方法对象上调用此方法对象表示的底层方法(即获取哪个成员方法就调用哪个成员方法)。

  • 获取有参数的方法并运行:
    先改造Employee类中的raiseSalary方法:
public void raiseSalary(double byPercent) {
    double raise = this.salary * byPercent / 100;
    this.salary += raise;
    System.out.println(salary);
}

获取有参数的方法并运行:

try {
    Class c = Class.forName("Employee");
    Object object = c.newInstance();
    Method method = c.getMethod("raiseSalary",double.class);
    method.invoke(object,5);
} catch (ClassNotFoundException e) {
    System.out.println("类名错误!");
}
System.out.println("获取有参数方法并运行程序执行完成!");

反射泛型的擦除

.class文件是没有泛型的:

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

    ArrayList<String> arrayList = new ArrayList<>();
    arrayList.add("first");

    Class c = arrayList.getClass();
    Method method = c.getMethod("add",Object.class);
    method.invoke(arrayList,100);
    method.invoke(arrayList,101.01);
    method.invoke(arrayList,true);
    System.out.println(arrayList);
}

该程序执行结果打印一个数组列表:[first, 100, 101.01, true],元素类型分别为String、int、double、boolean。可以看到利用反射将程序开始定义的泛型类型为String给擦除了,使得arrayList存储了包括String在内共四种类型的元素。
\color{red}{注意:}
\color{red}{实际情况下arrayList这种数组列表没有任何意义,这里只是加强对反射的理解。}

继承的设计技巧

  • 将公共操作和域放在超类。
  • 不要使用受保护的域。
    protected机制不能够带来更好的保护由两点原因:第一,子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性。第二,在Java程序设计语言中,在同一个包中的所有类都可以访问protected域,而不论它是否是这个类的子类。
  • 使用继承实现“is - a”关系。
    继承可以简化代码,但切记滥用。
  • 除非所有继承的方法都有意义,否则不要使用继承。
  • 在覆盖方法时,不要改变预期的行为。
    置换原则不仅应用于语法,也可以应用于行为。在覆盖一个方法时,不应该毫无缘由的改变行为的内涵。
  • 使用多态,而非类型信息。
  • 不要过多的使用反射。
    反射机制使得人们可以通过在运行时查看域和方法,让人们编写除更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适用于编写应用程序。
    反射是很脆弱的,编译器很难帮助人们发现程序中的错误,只有在运行时才发现错误并导致异常。

相关文章

  • 四 继承

    1.每个HTML元素根据继承属性都有父parent元素。举个例子,h3 元素的父元素是 , 的父元素是 body...

  • (四)继承

    1.原型链 javascript中没有类的概念,需要利用原型链来模拟。我们知道,构造函数、原型对象、实例之间有如下...

  • 四、继承

    继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求。 类、超类...

  • JavaScript 继承(四)原型式继承

    借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。 在 object() 函数内部,先创建了一个...

  • 第十三章 类继承(4)c++的三种继承方式

    (四)c++的三种继承方式 c++有三种继承方式,分别是公有继承,私有继承和保护继承。 (1)公有继承 这是最常用...

  • JAVA语言第二课

    JAVA面向对象——四大特征 继承篇——extendsJava 继承继承的概念继承是java面向对象编程技术的...

  • javascript继承之原型式继承(四)

    借助原型可以基于已有的对象创建新的对象,同时还不必因此创建自定义类型 创建一个对象 通过object方法原型式继承...

  • 08. 纯虚函数、抽象类、多继承、菱形继承、虚

    一.虚函数 二.纯虚函数 三,虚析构函数 四.纯虚函数 五,多继承 六.多继承-虚函数 七.菱形继承 八. 虚继承...

  • 面向对象(七)组合优于继承?

    组合优于继承,多用组合少用继承。 1、为什么不推荐使用继承? 继承是面向对象的四大特性之一,用来表示类之间的 is...

  • Python-面向对象(二)

    四、继承方法 1、单继承 子类在继承的时候,在定义类时,小括号()中为父类的名字父类的属性、方法,会被继承给子类 ...

网友评论

      本文标题:四、继承

      本文链接:https://www.haomeiwen.com/subject/lahrmqtx.html