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”关系。继承可以简化代码,但切记滥用。
  • 除非所有继承的方法都有意义,否则不要使用继承。
  • 在覆盖方法时,不要改变预期的行为。置换原则不仅应用于语法,也可以应用于行为。在覆盖一个方法时,不应该毫无缘由的改变行为的内涵。
  • 使用多态,而非类型信息。
  • 不要过多的使用反射。

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

posted @ 2019-04-08 10:09  Dcl_Snow  阅读(382)  评论(0编辑  收藏  举报