自学Java核心技术(三)之继承,反射

    xiaoxiao2025-03-28  17

    这一章来说,主要是来讲解 继承这个特性,利用继承,人们可以基于已经存在的类构造一个新类,继承已存在的类就是复用这些类的方法和域,在此基础上,还可以添加一些新的方法和域,以满足需求,另外这一张还会提及反射的概念,反射是在程序运行期间发现更多的类和属性的能力

    一.类,超类,子类

    在这边,我们之前有着Employee这个类,然后在公司中,我们都知道有着经理这个职位,从理论上说,Manager和Employee之间存在明现的“is-a”关系,这种关系是继承的一个明显特征

    1.定义子类

    关键字extends表示继承

    public class Manager extends Employee{

     

    }

    已经存在的类是一个超类,新类叫为子类,子类比超类拥有更多的数据和功能

    可以在子类中添加子类需要的域和方法,添加后,子类对象可以使用,但是父类对象不能使用,因此在设计类的时候,应该将通用的方法放在超类中,而将特殊用途的方法放在子类中

    2.覆盖方法

    在超类中的一些方法不一定对子类适合,那么我们就需要提供一个新的方法来覆盖

    如父类

    public double getSalary(){

    return salsry;

    }

    在子类中

    public double getSalary(){

    return salsry + bonus;

    }

    但是此时这个方法并不能运行,因为子类的getSalary()不能够直接访问超类私有域,也就是说尽管每个Manager拥有一个名为salary的域,但是在Manager的getSalary并不能直接地访问这个域,只有Employee的方法才可以去访问私有的变量,如果Manager类的方法一定要访问私有类,那么就必须借助公有的接口

    再试一下

    public double getSalary(){

    double baseSalary = getSalary()

    return baseSalsry + bonus;

    }

    但是此时也会报错,因为Manager它自己也有一个getSalary(),所以这段代码会无限地调用自己,知道程序崩溃,所以我们这边需要指出我们想要调用的是父类的方法,所以使用关键字super就可以来解决这个问题了

    super.getSalary()

    下面是最正确的

    public double getSalary(){

    double baseSalary = super.getSalary()

    return baseSalsry + bonus;

    }

    super并不是一个对象的引用,不能将super赋给另一个对象,它只是一个指示编译器调用超类方法的特殊关键字,我们可以增加域和方法但是不能删除继承的任何域和方法

    3.子类构造器

    下面是一个子类的构造器

    public Manager(String name,double salary,int year,int month,int day) {

    super(name,salary,year,month,day);//调用父类的构造器,这是由于子类的构造器无法访问父类的私有域,所以必须利用super来调用超类的构造器,super语句必须是子类构造器的第一条语句

    bonus = 0;

    }

    如果子类没有显示地调用超类的构造器,那么就将自动地调用超类默认(没有参数)的构造器,如果超类没有不带参数的构造器,并在子类没有显示地调用超类的其他构造器,那么Java编译器就会报错

    多态:一个对象变量可以指示多种实际类型的现象,在运行时能自动选择调用哪一个方法的现象称为动态绑定,如下面的代码

    Employee[] staff = new Employee[3];

    Manager boss = new Manager("sho",8000,2,1,2)

    staff[0] = boss;

    staff[1] = new Employee("Harry ",5990.3090.20.2);

    staff[2] = new Employee("Harry dda",5990.3090.20.2);

    然后调用for(Employee e : staff) {

    System.out.println(e.getSalary());//它能够确定执行哪一个getSalary方法,尽管这边声明是Employee,但是e既可以引用Employee类型,也可以引用Manager类型

    }

    4.继承的层次

    Java不支持多继承,但是上面的Manager也可以派生出Executive类,由一个公共超类继承出来的所有类的集合称为继承层次,从特定的类到其祖先的路径称为该类的继承链,通常一个祖先类可以有拥有多条继承链

    5.多态

    有一个判断是不是应该设计为继承关系的简单规则就是“is-a”规则,它表明每个子类的对象也是超类的对象,它的另一种表达法就是置换法则,它表明出现超类对象的任何地方都可以用子类对象来替换

    例如,可以将一个子类的对象赋给超类的变量,但是不能将一个超类的引用赋给子类的变量

    在Java中,对象变量是多态的,一个超类的变量可以引用父类的对象也可以引用子类的对象

    警告:所有的数组都要牢记创建他们的元素类型,并负责仅将类型相容的引用存储到数组中,如使用一个new Managers[10],创建的数组是一个经理的数组,如果想要存储一个Employee类型的引用,那么就会引发ArrayStoreException异常

    6.理解方法的调用

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

    1)编译器会先查看对象声明的类型和方法名,注意:可能存在多个名字为f,但是参数类型不一样的方法,如可能存在发f(int)和f(String),编译器会一一列出所有C类中名为f的方法和超类中访问属性为public和名字为f的方法(超类的private方法不能访问)

    2)接下来就是查看哪一个参数适合就调用那一个,这个过程叫做重载解析,如果没有比配或是有多个比配,那么就会报告一个错误,至此编译器已经获得需要调用的方法名字和参数类型

     

    方法的名字和参数列表称为方法的签名,如果在子类中定义了一个与超类签名相同的方法,那么子类这个方法就覆盖了超类中的这个相同签名的方法

    返回类型不是签名的一部分,因此在覆盖方法时一定要注意保证返回类型的兼容性,允许子类的返回类型定义为子类的类型

    3)如果是private方法和static方法final方法或者构造器,那么编译器就可以准确地知道应该调用哪一个方法,我们将这个调用方式叫为静态绑定

    4)当程序运行时,并采用动态绑定时,虚拟机一定知道与x所引用的对象的实际类型最合适的那个类的方法,每次调用都需要进行搜索,时间开销很大,因此,虚拟机预先为每一个类创建了一个方法表,其中列出了所有的方法的签名和实际调用的方法,这样在调用方法时查找一个方法就可以

     

    警告:在覆盖一个类的方法时,子类方法不能低于超类的可见性,特别是当超类为public时,子类一定要声明为public

    在运行时,调用e.getSalary()的解析过程

    1)首先虚拟机提取e的实际类型的方法表

    2)接下来,虚拟机搜索定义getSalary签名的类,此时虚拟机以及知道应该调用哪一个方法了

    3)最后,虚拟机调用方法

    动态绑定还有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展,假设有一个新类,变量e可能引用这个类的对象,我们不需要去重新编译,如果e刚好引用这个对象,就自动调用了

    7.阻止继承:final和方法

    有时候,可能希望阻止人们利用某个类来定义子类,不允许扩展的类叫做final类(不允许子类处理某些问题)

    public final class Executive {

     

    }

    类中的特定方法也可以声明为final,这样子类就不能覆盖这个方法(final类中的所有方法都自动成为final方法。但是域不会)

    域也可以被声明为final,对于final域来说,构造对象之后就无法改变他们的值了

    早期的Java中,有些程序员为了避免动态绑定带来的系统开销而使用final

    内联:如果一个方法没有被覆盖而且很短,编译器能够对它进行优化处理,如果e.getName()将被替换为e.name域,然而如果这个方法在另一个类中被覆盖了,那么编译器就无法知道会做什么操作,就不会进行内联

    8.强制类型转换

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

    double x = 3.234;

    int nx = (int)x;//将表达式x的值转换为整数类型,舍弃了小数部分

    我们有时候也需要将某个类的对象引用转换为另一个类的对象引用

    Manager boss = (Manager)staff[0];

    每个对象都是一种类型,类型描述了这个变量所引用的以及能够引用的对象类型

    将一个值存入变量,编译器会检查是不是允许该操作,将一个子类的引用赋给一个超类的变量,编译器是允许的,但是将一个超类的引用赋给一个子类变量,那么就需要进行类型转换,这样才能通过运行时的检查

    在试图在继承链上进行上向下的类型转换,并谎报了有关对象包含的内容,那么运行这个程序时,系统将会报错,并产生一个ClassCastException异常,如果没有捉到这个异常,那么程序就会停止,所以应该:

    在进行类型转换之前先查看一下是不是能够成功地转换,这个过程可以简单使用instanceof操作符就可以实现

    if(staff[0] instanceof Manager) {

    boss = (Manager)staff[0];

    }

    最后如果这个类型不可能转换成功,那么编译器就不会进行这个转换

    总结:

    只能在继承层次上进行类型转换,在将超类转换为子类之前,应该私用instanceof操作符

    注释:

    如果x是null,进行

    x instanceof C时,不会产生异常,只会返回false,这是因为null没有引用任何对象,当然就不会引用C类型的对象

    (其实如果我们实现多态性的动态绑定机制的时候,父类自然就可以找到相对应的方法,之所以要进行类型转换,那是因为子类有着父类没有的方法,那么我们有时可以在父类添加某些方法来解决这个问题)

     

    9.抽象类

    如果我们使用abstract关键字,那么就完全不需要实现这个方法

    public abstract String getDescription();

    为了提高程序的清晰度,包含一个或是多个抽象方法的类本身必须声明为抽象的

    public abstract class Person {

    public abstract String getDescription();

    }

    除了抽象方法外,抽象类还可以包含具体的数据和具体的方法

    抽象方法充当着占位的角色,他们的具体实现在子类中,扩展抽象类有下面的两种方法:

    1)在抽象类中定义抽象方法或不定义抽象方法,这样就必须将子类标记为抽象类

    2)定义全部的抽象方法,这样子类就不是抽象的

    注意:抽象类不能实例化,但是可以创建一个具体的子类的对象,需要注意的是可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象,如:People p = new Student("ss","ss");

    对于下面代码的理解:由于不能构造抽象类的对象,所以变量p永远不会引用Person对象,而是引用它的具体子类对象,他们的子类对象都定义了这个方法

    for(Person p : people) {

    System.out.println(p.getName()+"dd"+p.getDescription());

    }

    10.受保护访问

    有些时候我们可能希望超类中的某些方法允许子类访问,或是允许子类的方法访问超类的某个域,那么就可以将这些方法或是域的声明设置为protected,如将超类的hireDay设置为protected,而不是私有的,那么子类的方法就可以直接访问它了

    注意:要谨慎地使用protected属性,假设需要将设计的类提供给其他程序员使用,而在这个类中设置了一些受保护的域,那么由于程序员可以由这个类来派生出新类,并访问其中的受保护域,这种情况,如果需要修改这个类,那么就需要通知所有使用这个类的人了,着违背了OOP提倡的数据封装原则

    但是需要是为了限制某个方法的使用,那么就可以将它声明为protected,这表明子类得到信任,可以正确使用这个方法,而其他的类就不行(如Object的clone())

    下面是四个修饰符和他们的范围:

    private:仅仅对本类可见

    public:对所有类可见

    protected:对本包和子类可见

    不需要修饰符:对本包可见

    二.Object:所有类的超类

    Object是Java中所有类的祖先,Java中的每个类都是由它扩展来的,如果没有准备指出超类,那么Object就默认是这个类的超类,

    可以使用Object的变量来引用任何类型的对象:Object obj = new Employee("hayy","35000");

    不过Object类型的变量只能作为各种值的通用持有者,要对内容进行具体的操作,那么还需要清楚对象原始类型,并进行相对应的类型转换

    Employee e = (Employee)obj;

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

    obj = staff;

    obj = new int[10];

    1.equal方法

    Object类中的equals方法用于检测一个对象是不是等于另一个对象,这个方法是通过判断两个对象是不是有相同的引用,如果是,那么就是相等的

    但是对大多数类来说,这个没有什么意义,我们经常需要检测两个对象的相等性,如果两个对象状态相等,那么就可以说他们是相等的

    在下面是一个例子来检测两个雇员对象的姓名,薪水,雇佣日期是不是一样

    public boolean equals(Object otherObject) {

    if(this == otherObject) return true;

    if(otherObject == null) return false;

    if(getClass() !=otherObject.getClass()) return false;

    Employee other = (Employee)otherObject;

    return Object.equals(name,other.name)&&salary == other.salary&&Object.equals(hireDay,other.hireDay);//这是为了防备name或hireDay可能为null的情况,使用Objects.equals(),如果两个都为null,那么Objects.equals(a,b)将返回true,其中一个为null返回false,两个参数都不为null,调用a.equals(b);

    }

    在子类中定义equals方法,首先需要调用超类的equals,如果失败,对象就不可能相等,如果超类的域都相等,那么就需要比较子类中的实例域

    public boolean equals(Object otherObject) {

    if(!super.equals(otherObject)) return false;

    Manager other = (Manager)otherObject;

    return bonus == other.bonus;

    }

    2.相等的测试和继承

    注意:使用instanceof来检测相等时具有一定的限制的,这是得当由父类决定相等得概念时,才可以使用这个来检测,这样就可以在不同的子类对象之间进行相等的比较,不然有时可能会违背对称性的特性

    如果子类拥有自己相等的概念,那么对称性就需要将强制采用getClass进行检测

    如:在雇员和经理的例子中,只要对应的域相等,那么就可以认为两个对象相等,如果两个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.getClass()) return false;

    如果所有的子类都拥有统一的语义,那么就使用instanceof检测

    if(!(otherObject instanceof ClassName)) return false;

    5)将otherObject转换为相对应的类类型变量

    ClassName other = (ClasName)otherObject

    6)现在就开始对所有需要比较的域进行比较就可以了,使用==比较基本类型域,使用equals比较对象域,如果所有的域都相等,那么就返回true,不然就需要返回false

    如果是在子类中定义equals,那么就需要包含调用super.equals(other);

    注意:对于数组的域,可以使用静态的Arrays.equals方法来检测相对应的数组元素是不是相等,还有就是需要注意好显式参数是什么,应该是(Object other)的

    3.hashCode方法

    散列码(hash code)是由对象导出来的一个整数值,散列码是没有规律的,如果是两个不同的对象,那么它们的散列码基本不一样

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

    注意:下面的例子

    String s = "ok";

    StringBuilder sb = new StringBuilder(s);

    System.out.println(s.hashCode() + sb.hashCode());

    String t = new String("ok");

    StringBUilder tb = new StirngBuilder(t);

    System.out.println(t.hashCode() + "" tb.hashCode());

    这里s和t拥有相同的散列码,这是因为字符串的散列码是由内容导出来的,而字符串缓存sb和tb却有着不同的散列码,这是由于StringBuilder类中没有定义hashCode方法,它的散列码来自Object的方法,所以导出来的是对象的存储地址

     

    如果重新定义了equals方法,那么就需要重新定义hashCode方法,以便用户可以将对象插入到散列表中

    hashCode方法应该返回一个整形数值,并合理组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀

    下面是最好的方法

    如果需要组合多个散列值时,就可以调用Objects.hash并提供多个参数,这个方法会对各个参数调用Objects.hashCode并组合这些散列值,这样当比较名字,薪水,雇的日期时就可以下面的代码

    public int hashCode(){

    return Object.hash(name,salary,hireDay);

    }

    Equals和hashCode的定义必须一致,这边如果用定义的equals比较雇员的ID时,那么hashCode方法就需要散列ID,而不是雇员的姓名等

    如果存在数组的域,那么就可以使用静态的Arrays.hashCode方法来计算一个散列码

    4.toString方法

    这个方法用于返回对象值的字符串,下面时Point类的toString方法将返回下面的字符串:

    java.awt.Point[x=10,y=10]

    绝大数类的toString方法都遵守这样的格式:类的名字,随后是一对方括号括起来的域值,下面是Employee类的toString方法的实现

    public String toString(){

    return getClass().getName() + "[name=" + name + " ,salary=" + salary + ",hireDay=" + hireDay + "]";

    }

    设计子类的程序员也可以定义自己的toStirng方法,并将子类域的描述添加进去,如果超类使用了getClass().getName(),那么子类只要调用super.toString()就可以,如下面的是Manager的toString()

    public String toString(){

    return super.toString() + "[bonus=" + bonus + "]";

    }

    只要一个对象与一个字符串通过操作符“+”连接起来,那么Java编译器就会自动调用toString(),以便获得这个对象的字符串描述

    提示:在调用x.toString()的地方可以用 "" + x来代替,这条语句将一个空串与x的字符串表示相连接

    Object类中定义了toString方法,用来打印出对象所属于的类名和散列码,如果打印出像下面的代码

    java.io.PrintStream@37392

    那么就说明这个类没有去覆盖toString类

    警告:数组继承了object的toString方法,所以数组会按旧的格式来打印,生成类似下面的代码:

    【I1a37de90 //[I代表是一个整形数组

    修正的方法是调用静态方法Arrays.toString(数组名)

    想要打印多维数组的话:Arrays.deepToString()

    建议都为自己的类加上toString()

    三.泛型数组列表

    有一个类ArrayList是一个采用类型参数的泛型类,为了指定数组列表指定的元素对象类型,需要将一对尖括号将类名扩起来加在后面,如ArrayList<Employee>,它使用起来像数组但是在添加或是删除与元素的时候,具有自动调节数组容量的功能

    在Java7后可以省去右边的<>里面的类型参数

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

    1)使用一个add()方法可以把元素添加到数组的列表

    staff.add(new Employee("ss",23,24));

    数组列表管理着对象引用的一个内部数组,最终,数组的全部空间用尽的时候,数组列表会自动创建一个更大的数组,并把所有的对象从较小的数组中拷贝到较大的数组中去

    2)如果已经清楚知道数组中可能存储的元素数量,那么就可以在填充数组之前调用ensureCapacity(100);

    也可以把初始容量传递给ArrayList的构造器,ArrayList<Employee> staff = new ArrayList<>();

    数组列表的容量与数组的大小有一个非常大的区别,如果数组分配100个元素的存储空间,那么数组就有100个空位置可以使用,而数组列表的话只是拥有100个元素的潜力(实际分配的话会超过100),但最初分配的话是不含有任何元素的

    3)size()方法将返回数组列表中包含的实际元素数组

    staff.size()//将返回staff数组列表的当前元素的数量

    4)trimToSize(),一旦确保数组列表的大小不再发生变化,就可以调用trimToSize方法,这个方法将把存储区域的大小调整为所需要的存储空间的数组,垃圾回收器将回收多余的存储空间

    1.访问数组列表元素

    数组列表其实并不是Java的一部分,它只是一个由某些人设计后放在标准库中的一个实用类,它是使用get和set方法来访问或是改变数组元素的操作,不是使用[]语法

    staff.set(i,harry);//设置第i个元素

    注意:只有i小于或是等于数组列表的大小时,才能够调用list.set(i,x);使用add()来为数组添加新元素而不是set,set只能用来替换

    Employee e = staff.get(i);//获得数组列表的元素

    小技巧:可以灵活地扩展数组,又可以方便访问数组元素,首先创建一个数组并添加所有的元素

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

    while(..){

    x = ...;

    list.add(x);

    }

    执行完上面的操作后就使用toArray方法将数组元素拷贝到另一个数组中

    X[] a = new X[list.size()];

    list.toArray(a);

    除了可以在数组列表后面追加元素之外,还可以在数组列表的中间插入元素,使用带索引的add(),

    int n =  staff.size()/2;

    staff.add(n,e);

    Employee e = staff.remove(n);

    对于数组来说,它对数组实施插入和删除元素的操作比较低,如果经常需要在中级位置上插入和删除元素,那么就应该使用链表了吧

    可以使用foreach来循环遍历数组列表

    for(Employee e : staff) {

     

    }

    等于下面的代码

    for(int i=0;i<staff.size();i++) {

    Employee e = staff.get(i);

    dosometing with e

    }

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

    在自己的代码中,我们都喜欢使用类型参数来增加安全性,但有时我们需要了解如何与没有使用类型参数的遗留代码交互操作

    假如有下面这样一个类

    public class EmployeeB {

    public void update(ArrayList list) {

    }

    public ArrayList find(String query) {}

    }

    此时我们可以将一个类型化的数组列表传递给update(),而不用进行任何类型转换

    ArrayList<Employee> staff = ...;

    employeeB.update(staff);

    上面尽管没有给出任何错误信息或是警告,但是这样调用不安全,因为在update中,添加到数组列表中的元素可能不是Employee类型,在对这些类型进行搜索时就会出现异常,

    相反的,如果将一个原始ArrayList赋给一个类型化ArrayList会得到一个警告

    ArrayList<Employee> result = employeeDB.find(query);

    使用类型转换不能避免这个问题,而是会得到另一个警告,指出类型转换有误

    .ArrayList<Employee> result =(ArrayList<Employee>) employeeDB.find(query);//在这种情况下,研究一下编译器的警告性提示,确保这些警告不会造成太严重的后果时就可以使用@SuppressWarning("unchecked")标注来标记这个变量可以接受类型转换

    4.对象包装器和自动装箱

    有时候我们需要将int这样的基本类型转换为对象,所有的基本类型都有一个与之对应的类,如Integer对应int,这类就是包装类(wrapper)

    有Integer Long Float Double Short Byte Character,Void,Boolean(前6个类派生于Number类)

    包装类时不可以变的,一旦构造了包装器,就不能修改里面的值,对象包装类还是final,因此不能定义他们的子类

     

    如我们想要定义一个整形数组列表,而尖括号里面的类型参数不允许是基本类型,所以这边就用到了Integer包装类

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

    但是由于每个值包装在对象中,所以ArrayList<Integer>的效率远远低于int[]数组,因此应该使用它构造小型的集合,之所以这么用是因为方便有时比效率重要

    现在有自动装箱:list.add(3);等于list.add(Integer.valueOf(3));

    自动拆箱:int n = list.get(i);等于int n = list.get(i).inValue();

    甚至可以在算术表达式中自动装箱和拆箱,可以将一个自增操作符应用于一个包装器引用:

    Integer n = 3;

    n++;

    编译器会自动地插入一个对象拆箱的指令,然后进行自增计算,最后将结果封装

     == 也可以使用于对象包装器,只不过检测的是对象是不是指向同一个存储区域,但是在Java中可能会使得下面的代码为真

    Integer a = 100;

    Integer b = 100;

    if(a==b)

    所以两个包装器对象比较时调用equals方法

    注意:

    由于包装器类引用可以为null,所以自动装箱时可能会抛出NullPointerException异常

    Integer n = null;

    System.out.println(2*n);

    如果在一个条件表达式中混合使用Integer和Double类型,那么Integer值就会自动拆箱,提升为double,然后再装箱为Double

    装箱和拆箱是编译器认可的,而不是虚拟机,编译器在生成字节码的时候插入必要的方法调用,而虚拟机只是执行这些字节码而已

    使用数值对象包装器也有一个好处,就是可以把某些基本方法放在包装器中,例如将一个数字字符串转换成数值

    int x = Integer.parseInt(s);这里与Integer对象没有任何方法,但是parseInt是一个静态方法,Integer类是放在这个地方的一个好的地方

    但是包装类不能来进行实现修改数值参数的方法,这是因为Java方法是值传递的,不能编写下面的代码来实现变大三倍

    public static void trip(int x) {

    x = 3*x;

    }

    这里就算把int换成Integer也是不能的,因为Integer对象是不可以改变的,包含在包装器里面的内容也就不会改变,所以不能使用上面的方法来改变数值

    如果真想改变数值,那么就可以利用 org.pmg.CORBA包中定义的持有者类型(holder),每个持有者类型都拥有一个公有的域值,通过它可以访问存储在其中的值

    public static void triple(IntHolder x) {

    x.value = 3*x.value;

    }

    5.参数数量可变的方法

    在Java5之前,每个Java方法都有着固定数量的参数,然而,现在的版本提供了可以用可变参数数量的方法(“变参”方法)

    如:public PrintStream printf(String fmt,Object...args) {

    }

    这里的省略号是Java代码的一部分,它表明这个方法可以接收任意数量的对象,事实上这个方法接受两个参数,一个格式字符串,一个是Object[]数组,其中保存着所有的参数,现在扫描fmt字符串,第i个格式对应着args[i]的值相比配

    其中对于我们来说,Object...参数和Object[]完全一样,每次需要调用的时候就将参数绑定到数组上面去,所以这边我们也可以允许将一个数组传给可变参数方法的最后一个参数,如下面:

    System.out.printf("%d%s",new Object[]{  new Integer(1),"widgets"});

    6.枚举类

    之前我们已经知道了如何去定制枚举类,如下面

    public enum Size {SMALL,MEDIUM,LARGE,EXTRE};

    实际上这声明一个类,它刚好有四个实例,再次尽量不要再构造新对象,在比较两个枚举类的时候,永远不要调用equals,而是直接使用 “==”

    如果需要也可以在枚举类型中添加一些构造器,方法和域,构造器只是在构造枚举常量的时候使用

    public enum Size {

    SMALL("s"),MEDIUM("m"),LARGE("l"),EXTRE("e");

    private String ab;

    private Size(String ab){

    this.ab = ab;

    }

    public String getAb(){

    return this.ab

    }

    };

    所以的枚举类型都是Enum类的子类,他们继承了这个类的许多方法

    toString():返回枚举常量名,如Size.SMALL.toString()将返回字符串"SMALL"

    valueOf():Size s = Enum.value(Size.class,"SMALL");将s设置为Size.SMALL

    values()返回一个包含全部枚举值的数组

    ordinal():返回enum声明中枚举常量的位置,为位置从0开始计算

    七.反射:

    反射库中提供了一个好的库,以便能够动态操作Java代码的程序,能够分析类能力的程序称为反射,反射的机制可以用来

    1)在运行时分析类的能力

    2)在允许时查看对象

    1.Class类

    在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标志,这个信息跟踪着每一个对象所属的类,虚拟机利用运行时类型信息选择相应的方法执行

    (1)可以通过Class类来访问这些信息,一个Class对象表示一个特定的类的属性

    Class cl  = generator.getClass();

    String name = cl.getName();;//name is "java.util.Random";

    (2)通过类名获得对应的Class对象

    String className = "java.util.Rondom";

    Class cl = Class.forName(className);

    如果类名保存在字符串中,并可在运行时改变,就可以使用这个方法,这得需要这个字符串是是雷鸣得时候才拥有,不然会抛出一个受检得异常,所以无论在哪里使用这个方法,都应该提供一个异常处理器

    (3)如果T是任意得Java类型,那么T.class将代表比配的类的类对象

    Class cl1 = Romdom.class;

    Class cl1 = int.class;

    一个Class对象表示的是一种类型,这个类型不一定是一种类,如int不是一个类,但int.class是一个Class的对象

    注解:Class对象实际上是一个泛型类,如Employee.class的类型是Class<Employee>

    警告:由于历史原因,getName方法应用于数组类型上的时候会返回一个很奇怪的名字

    Double[].class.getName() 返回“[Ljava.lang.Double;”

    int[].class.getName() 返回“[I”

    虚拟机为每个类型管理一个Class对象,因此可以利用 == 运算符来实现两个类对象比较的操作

    if(e.getClass() == Employee.class);

    newInstance()这个方法可以用来动态创建一个类的实例

    e.getClass().newInstance();//这就创建了一个与e具有相同类型的实例,newInstance方法调用默认的构造器(没有参数的构造器)初始化新创建的对象,如果这个类没有默认的构造器,那么就会抛出异常

    可以将forName与newInstance配合起来使用,可以根据存储在字符串中的类名创建一个对象

    String s = "java.util.Rondom";

    Object m = Class.forName(s).newInstance();

    2.捕获异常

    当程序发生错误的时候,就会抛出异常,抛出异常比终止程序要灵活得多了,这是因为我们可以提供一个“捕捉”异常的处理器

    如果没有提供处理器,那么程序就会终止

    异常有下面两种:

    1)未检查异常,也有许多异常,像null引用就是

    2)已检查异常,对它,编译器会检查是不是已经提供了处理器

    对于上面的Class.forName方法就是一个抛出已受检异常的例子

    将可能抛出已检查异常的一个或是多个方法调用代码放在try块中,然后在catch子句中提供处理器代码

    try{

    String name = ...;

    Class cl = Class.forName(name);

    }catch (Exception e) {

    e.printStackTrace();//打印出栈的轨迹

    }

    此时如果类名不存在,那么就会进入catch子句,如果没有异常,那么就会跳过catch语句

    如果调用了一个抛出异常的方法,而又没有提供处理器,那么编译器就会给出错误

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

    反射包里面有三个类Field,Method和Constructor分别用于描述类的域,方法和构造器

    这三个类里面都有一个叫做getName的方法,用来返回项目的名字

    Field里面有一个getType方法,用来描述域所属类型的Class对象

    Class类中的getFields,getMethods和getConstructor方法将返回类提供的public域,方法,和构造器数组,其中包含超类的公有成员

    Class类中的getDeclareFields,getDeclareMethods,getDeclareConstructors方法将返回类中声明的全部域,方法和构造器,其中包含私有和受保护成员,但不包含超类的成员,其他的需要用到是详细看方法API

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

    从上面我们已经知道了如何去获得对应的Class对象和通过Class对象调用getDeclaredFields来获得数据域类型

    可以在编译时查看对象域,在查看对象域的关键方法就是Field类的get方法

    如果f是一个Field类型的对象(例如通过getDeclaredFields得到的对象),obj是某个包含f域的类的对象,那么

    f.get(obj)将会返回一个对象,其值为obj域的当前的值,如下面的例子

    Employee harry = new Employee("sdsd",2839,29,1,1998);

    Class cl = harry.getClass();//得到Employee.class

    Field f = cl.getDeclaredField("name");//得到name对应的那个对象

    Object v = f.get(harry);//得到sdsd

    但是现在上面的代码会出错,因为nama是一个私有的域,所以get方法会抛出一个IllegalAccessException,此时除非有访问的权限,不然Java的安全机制只允许查看任意对象有哪一些域,而不能去读取它的值

    反射机制是受限于Java的访问控制,如果一个Java程序没有受到安全管理器的控制,那么就可以覆盖访问控制,为了达到这个目标,就需要调用Field,Method或是Constructor对象的setAccessible方法

    如上面的代码就需要添加

    f.setAccessible(true);//此时才会真正得到sdsd

    此时因为name是一个Strng域,所以直接可以把它当作Object返回,但是如果查看double类型的话,由于Java中数值类型不是对象,所以我们需要使用Field类的getDouble方法

    然后有了获取,当然也就可以来设置,调用f.set(obj,value);这样就可以将obj对象的f域设置为新值

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

    反射包的Array类允许动态地创建数组,下面来编写一个通用的方法用来将Employee[]数组转换为Object[]数组

    我们都知道Java数组会记住每个元素的类型,也就是创建new表达式中使用的元素类型,将一个Employee[]临时转成Object[]数组,然后将它转换回来时可以的,但是如果一开始就是Object[]的就永远不能转成Employee[]数组

    此时需要调用Array类的newInstance,它能构造新数组,调用它需要数组的元素类型和数组的长度

    Object newArray = Array.newInstance(componentType,newLength);

    想要新数组元素类型,就需要进行下面的工作

    1)首先获得a数组的类对象

    2)确认它是一个数组

    3)使用Class的getComponentType方法来确定数组对应的类型

    public static Object goodCopy(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;

    }

    上面的方法可以用来扩展任意类型的数组,不仅仅是对象数组,这是因为声明是 Object类型,不是Object[]类型,整形数组类型可以转换为Object,但不能转换为对象数组

    7.调用任意方法

    反射机制允许我们调用任意方法

    在Method类中有一个invoke方法,它允许调用当前Method对象中的方法

    Object invoke(Object obj,Object...args)

    第一个是隐式参数,其余对象都提供了一个显式参数,对于静态方法,第一个参数可以被忽视,也就是可以把它设置为null

    假如用m1代表Employee类的getName方法,下面的语句就是显示了如何调用这个方法

    String s = (String) m1.invoke(harry);

    如果返回类型是基础类型,那么invoke方法会返回其包装器类型,假如m2是Employee类的getSalary方法,那么返回的实际就是一个Double,必须相应地完成类型转换

    double s = (double)m2.invoke(harry);

    下面是来获得Method对象,我们可以用下面的两种方法

     

    1)getDeclareMethod方法,然后对返回的Method对象数组进行查找,

    2)调用Class类的getMethod方法得到想要的方法,如下面,有时我们需要提供参数类型给他们

    Method getMethod(String name,Class...parameterTypes)

    Method m1 = Employee.class.getMethod("getName");

    Method m2 = Employee.class.getMethod("raiseSalary",double.class);

     

    对于这个invoke方法,如果在调用方法时提供了一个错误的参数,那么这个方法就会抛出一个异常

    另外,invoke的参数和返回值必须是Object的,这就说明需要进行多次的类型转换,这会导致编译器错过检查代码的机会,等到测试时才发现这些错误,所以建议在必要时才去使用Method对象,最好使用接口和使用接口回调会使得代码的执行速度更好

    8.继承的设计技巧

    1)将公共的操作和域放在超类

    2)不要使用受保护的域,因为子类集合时无限的,任何人都可以由一个类派生出一个子类,并编写代码来访问protected的实例域,破坏了封装性,还有就是Java中,同一个包中的所有类都可以访问protected域

    不过protected方法对于那些不提供一般用途而在子类中重新定义的方法都很有用

    3)不是“is-a”就不使用使用继承

    4)除非所有继承的方法都有意义,不然就不使用继承

    5)在覆盖方法时,不要改变预期的行为(也就是不要改变太多的设计想法)

    6)使用多态而不是非类型信息

    如果是下面的这种形式的代码

    if(x is of type1) action1(x)

    else if (x is of type2) action2(x);

    此时看看action1和action2是不是相同的概念,如果是,那么就应该为这个类定义一个方法,然后将其放在两个类的超类或是接口中去,然后就可以调用x.action();

    这样以后比较容易来维护和扩展

    7)不要过多使用反射

    反射是适用于编写系统程序的,但是不适合编写应用程序

     

    最新回复(0)