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”关系。继承可以简化代码,但切记滥用。
- 除非所有继承的方法都有意义,否则不要使用继承。
- 在覆盖方法时,不要改变预期的行为。置换原则不仅应用于语法,也可以应用于行为。在覆盖一个方法时,不应该毫无缘由的改变行为的内涵。
- 使用多态,而非类型信息。
- 不要过多的使用反射。
反射机制使得人们可以通过在运行时查看域和方法,让人们编写除更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适用于编写应用程序。
反射是很脆弱的,编译器很难帮助人们发现程序中的错误,只有在运行时才发现错误并导致异常。