• Java基础-继承


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

    类、超类和子类

    定义子类


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

    覆盖方法(override)


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

      1 public class Employee {
      2     private String name;
      3     private double salary;
      4 
      5     public Employee(String n, double s){
      6         name = n;
      7         salary = s;
      8     }
      9 
     10     public String getName(){
     11         return name;
     12     }
     13 
     14     public double getSalary(){
     15         return salary;
     16     }
     17 }
     18 


    创建一个子类:

      1 public class Manager extends Employee {
      2     private String name;
      3     private double salary;
      4     private double bonus;
      5 
      6     public Employee(String n, double s){
      7         name = n;
      8         salary = s;
      9     }
     10 
     11     public String getName(){
     12         return name;
     13     }
     14 
     15     public double getSalary(){
     16         double sumSalary = super.getSalary();
     17         return sumSalary + bonus;
     18     }
     19 
     20     public void setBonus(double bonus){
     21         this.bonus = bonus;
     22     }
     23 }
     24 


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

    子类构造器

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


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

    继承层次


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

    多态


    一个对象变量可以指示多种实例类型的现象被称为多态。
    例如:一个超类类型的变量既可以指示超类的实例,也可以指示子类的实例类型。当程序运行时可以自动的选择使用超类的方法,也可以调用子类的方法。
    这种在运行时能够自动地选择调用哪个方法的现象称为动态绑定
    动态绑定的一个非常重要的**特性**:无需对现存代码进行修改,就可以对程序进行扩展。(假设增加一个新的子类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类。格式如下:

      1 public final class 类名 {}


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

    抽象类


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

      1 public abstract class Person {
      2     public abstract String getDescription();
      3 }


    注意:

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

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

      1 public abstract class Person {
      2     private String name;
      3     public Person(String name) {
      4         this.name = name;
      5     }
      6     public abstract String getDescription();
      7     public String getName() {
      8         return name;
      9     }
     10 }


    抽象方法充当着占位的角色,具体实现在子类中。扩展抽象类可以有两种选择:
    第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方法代码如下:

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


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

      1 public boolean equals(Object anObject) {
      2     if (this == anObject) {
      3         return true;
      4     }
      5     if (anObject instanceof String) {
      6         String anotherString = (String)anObject;
      7         int n = value.length;
      8         if (n == anotherString.value.length) {
      9             char v1[] = value;
     10             char v2[] = anotherString.value;
     11             int i = 0;
     12             while (n-- != 0) {
     13                 if (v1[i] != v2[i])
     14                     return false;
     15                 i++;
     16             }
     17             return true;
     18         }
     19     }
     20     return false;
     21 }


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

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

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

    相等测试与继承


    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方法,以便获得这个对象的字符串描述。
    注意:
    在调用x.toString()的地方可以用""+x代替。这条语句将一个空串域x的字符串相连接,这里的x就是x.toString()。
    即使x使基本类型,这条语句依然有效。

    泛型数组列表


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

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


    但是尖括号中的类型必须是引用数据类型,不能是基本数据类型。
    基本数据类型|对应的引用数据类型的表现形式:

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


    创建示例如下:

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


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

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


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

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


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

    对象包装器与自动装箱


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

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


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

      1 list.add(3);


    将自动的变换成:

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


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

      1 int n = list.get(i);


    翻译成:

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


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

      1 Integer n = 1;
      2 n++;


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

    参数数量可变的方法


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

      1 public class PrintStream {
      2     public PrintStream printf(String fmt, Object... args) {
      3         return format(fmt, args);
      4     }
      5 }


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

    枚举类


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

      1 public enum  SeasonEnum {
      2     SPRING,SUMMER,FALL,WINTER
      3 }


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

      1 public enum  SeasonEnum {
      2     SPRING,SUMMER,FALL,WINTER;
      3     private  int  other;
      4 }


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

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


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

      1 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。
    注意:这个名字很容易混淆而使人不理解,重点注意区分.class文件(字节码文件)和Class类的对象。
    反射的过程:
    加载.class文件到内存中-->系统在内存中自动生成.class文件的Class类的对象-->由这些Class类的对象可以反向获取类的信息(域、构造器、方法等)-->进而使用这些类的信息。

    类的加载


    当程序要使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对类进行初始化。
    加载就是指将编译后的.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 //1. 对象获取
      2 Employee e = new Employee();
      3 //调用其超类Object类中的getClass()方法,返回一个Class类型的实例。
      4 Class c1 = e.getClass();
      5 System.out.println(c1);
      6 
      7 //2. Class类的静态方法forName(保存在字符串中的类全名,即:包.类名)获取
      8 String className = "com.test.Employee"
      9 Class c2 = Class.forName(className);
     10  System.out.println(c2);
     11 
     12 //3. 类名获取
     13 Class c3 = Employee.class 14 System.out.println(c3);
     15 


    注意:
    Class类的静态方法forName()获取对象时,参数必须是类名或者接口名才能够执行。
    否则该方法会抛出一个checked exception,所以无论何时使用这个方法都应该提供一个异常处理器(throws ClassNotFoundException)。

    虚拟机为每个类型管理一个Class对象。因此可以使用“==”运算符(当然也可以使用equals方法)实现两个类对象的比较操作。

    利用反射操作对象


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

    获取默认无参数构造器并运行:

      1 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
      2 
      3     try {
      4         Class c = Class.forName("Employee");
      5         Constructor constructor = c.getConstructor();
      6         Object object = constructor.newInstance();
      7         System.out.println(object);
      8     } catch (ClassNotFoundException e) {
      9         System.out.println("类名错误!");
     10     }
     11     System.out.println("获取无参数构造器程序执行完成!");
     12 
     13 }
     14 



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

    快捷获取默认无参数构造器并运行:

      1 try {
      2     Class c = Class.forName("Employee");
      3     Object object = c.newInstance();
      4     System.out.println(object);
      5 } catch (ClassNotFoundException e) {
      6     System.out.println("类名错误!");
      7 }
      8 System.out.println("使用Class类的newInstance快速获取无参数构造器程序执行完成!");



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

    获取有参数构造器并运行:

      1 try {
      2     Class c = Class.forName("Employee");
      3     Constructor constructor = c.getConstructor (String.class,double.class,int.class,int.class,int.class);
      4     Object object = constructor.newInstance("Dcl_Snow",10000,2019,1,1);
      5     System.out.println(object);
      6 } catch (ClassNotFoundException e) {
      7     System.out.println("类名错误!");
      8 }
      9 System.out.println("获取全参数构造器程序执行完成!");

    获取私有构造器并运行:
    先在Employee类中添加一个如下的私有构造器:

      1 private Employee( double salary, String name, int year, int month, int day) {
      2     this.name = name;
      3     this.salary = salary;
      4     hireDay = LocalDate.of(year, month, day);
      5 }



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

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



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

      1 public String sex;



    获取域并设置域值:

      1 public static void main(String[] args) throws IllegalAccessException, InstantiationException {
      2 
      3     try {
      4         Class c = Class.forName("Employee");
      5         Object object = c.newInstance();
      6         Field field = c.getField("sex");
      7         field.set(object, "man");
      8         System.out.println(object);
      9     } catch (ClassNotFoundException | NoSuchFieldException e) {
     10         System.out.println("类名错误!");
     11     }
     12     System.out.println("获取域并设置域值执行完成!");
     13 }
     14 



    获取方法并运行:


    获取空参数的方法并运行:
    先在Employee类中添加一个如下的空参数的方法:

      1 public void work(){
      2     System.out.println("大家在工作!");
      3 }



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

      1 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
      2 
      3     try {
      4         Class c = Class.forName("Employee");
      5         Object object = c.newInstance();
      6         Method method = c.getMethod("work");
      7         method.invoke(object);
      8     } catch (ClassNotFoundException e) {
      9         System.out.println("类名错误!");
     10     }
     11     System.out.println("获取无参数方法并运行程序执行完成!");
     12 }
     13 



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

    获取有参数的方法并运行:
    先改造Employee类中的raiseSalary方法:

      1 public void raiseSalary(double byPercent) {
      2     double raise = this.salary * byPercent / 100;
      3     this.salary += raise;
      4     System.out.println(salary);
      5 }



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

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



    反射泛型的擦除


    .class文件是没有泛型的:

      1 public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
      2 
      3     ArrayList<String> arrayList = new ArrayList<>();
      4      arrayList.add("first");
      5 
      6     Class c = arrayList.getClass();
      7     Method method = c.getMethod("add",Object.class);
      8     method.invoke(arrayList,100);
      9     method.invoke(arrayList,101.01);
     10     method.invoke(arrayList,true);
     11     System.out.println(arrayList);
     12 }
    



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

    继承的设计技巧

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

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

  • 相关阅读:
    Linux_23 DNS 正向解析区域、反向解析区域;主/从;子域;基本安全控制
    Linux_22 加密和解密及OpenSSL
    Linux_21 日志系统、ssh服务、系统安装及系统故障排除
    Linux_20 子网划分
    Akavache简明使用指南
    Oracle存储过程解析XML内容
    docker部署微服务不支持中文字体的解决方案
    Three.js
    Three.js
    [Linux] vim状态栏配置
  • 原文地址:https://www.cnblogs.com/Dcl-Snow/p/10668834.html
Copyright © 2020-2023  润新知