3 继承知识点


title: 继承
tag: 标签名
categories: 分类
comment: 是否允许评论(true or false)
description: 描述
top_img: https://z3.ax1x.com/2021/10/06/4xq2s1.png
cover: https://z3.ax1x.com/2021/10/06/4xq2s1.png

类、超类和子类

我们在公司的很多地方,员工和经理之间存在着相同的地方,但是又有不同的地方。普通员工在完成本职工作之后仅领取薪水,而经理在完成任务之后还会领取到奖金。这种形式就需要使用继承。

定义子类

下面有继承Employee类来定义Manager类的格式,关键字extends表示继承。

public class Manager extends Employee{
    添加方法和域
}

关键字extends表明正在构造的新类派生于一个已存在的类。已存在的类成为超类、基类或父类;新类称为子类、派生类或孩子类。

尽管Employee类是一个超类,但并不是因为它优于子类或者比子类拥有更多的功能,相反,子类比它拥有更多的功能。

在Manager类中,增加了一个用于存储奖金信息的域,以及一个用于设置这个域的新方法:

public class Manager extends Employee{
    private double bonus;
    ...
    public void setBouns(double bonus){
        this.bonus = bonus;
    }
}

如果创建一个Manager对象,那么这个对象就可以使用setBouns方法。

Manager boss = ...;
boss.setBonus(5000);

但是由于setBouns方法并不是在Employee中定义的,所以属于Employee类的对象不能使用它。

尽管Manager类中没有显式地定义getName和getHireDay方法,但是这些Manager类的对象去可以使用它们,这是因为Manager类自动继承了超类Employee中的这些方法。

在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中,这种将通用的功能放到超类的做法,在面向对象程序设计中十分普遍。

覆盖方法

超类中的有些方法对子类Manager并不一定适用。具体来说,Manager类中的getSalary方法应该返回薪水和奖金得到总和。为此,需要提供一个新的方法来覆盖超类的方法

public class Manager extends Employee{
    ...
    public double getSalary(){
        ...
    }
    ...
}

如果我们希望调用超类Employee中的getSalary方法,而不是当前类的这个方法。为此,可以使用特定的关键字super解决这个问题:

super.getSalary()

上语句中调用的是Employee类中的getSalary方法。下面则是Manager类中getSalary方法的正确书写格式:

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

有些人认为super和this是一个类似的概念,但是实际上这样比较是不恰当的,这是因为super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

在子类中可以增加域、增加方法或覆盖超类中的方法,然而绝对不能删除继承的任何域和方法。

子类构造器

我们来提供一个构造器

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

这里关键字super具有不同的含义。语句

super(name, salary, year, month, day);

是调用超类Employee中含有n,s,year,month和day参数的构造器

由于Manager类的构造器不能访问Employee类的私有域,所以必须利用Employee类的构造器对这部分私有域进行初始化,我们可以通过super实现对超类构造器的调用,使用super调用构造器的语句必须是子类构造器的第一条语句。

如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类中又没有显式地调用超类的其他构造器,则Java编译器将报告错误。

注意:关键字this有两个用途:一是引用隐式参数,二是调用该类其他的构造器
          super也有两个用途:一是调用超类的方法,而是调用超类的构造器
 在调用构造器时,这两个关键字的使用方式很相似,调用构造器的语句只能作为另一个构造器的第一条语句出现。

一个对象变量可以指示多种实际类型的现象别称为多态;在运行时能够自动第选择调用哪个方法的现象称为动态绑定*。

继承层次

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

3 继承知识点

一个祖先类可以拥有多个子孙继承链。java中不支持多继承。

多态

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

Employee e;
e = new Employee(...);
e = new Manager(...);

在Java程序设计语言中,对象变量是多态的。一个Employee变量既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类的对象。

Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;

在上述例子中,变量staff[0]与boss引用同一个对象。但编译器将staff[0]看成Employee对象。着说明

boss.setBonus(5000); //ok
但是不能这样用:
staff[0].setBonus(5000); //Error
这是因为staff[0]声明的类型是Employee,而setBouns不是Employee类的方法。
不能将一个超类的引用赋给子类变量。例如:下面的赋值就是非法的
Manager m = staff[i]; //Error
原因:不是所有的雇员都是经理。如果赋值成功,m有可能引用了一个不是经理的Employee对象,当在后面调用m.setBonus(...)时就有可能发生运行时错误。

理解方法调用

1 编译器查看对象的声明类型和方法名。假设调用x.f(prarm),且隐式参数x声明为C类的对象。需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法。如:可能存在方法f(int)和方法f(String).编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法。

2 接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析。对于调用x.f(“Hello”)来说,编译器将会挑选f(String),而不是f(int).由于允许类型转换,所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。

3 如果是private方法、static方法、final方法或者构造器,那么编译器将准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定。

4 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型时D,它时C类的子类。如果D类定义了方法f(String),就直接调用它;否则,将在D类的超类中寻找f(String),以此类推。

每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。

阻止继承:final类和方法

有时候,可能希望阻止人们利用某个定义子类。不允许扩展的类被称为fianl类。如果在定义类的时候使用了final修饰符就表明这个类时final类。

public final class Exective extends Manager{
    ...
}

类中的特定方法也可以被声明为final.如果这样做,子类就不能覆盖这个方法。

public class Employee{
    ...
    public final String getName(){
        return name;
    }
    ...
}

将方法或类声明为final主要目的:确保它们不会再子类中改变语义。String类是final类,这意味着不允许任何人定义String的子类。换言之,如果有一个String的引用,它引用的一定是一个String对象,而不可能是其他类的对象。

强制类型转换

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

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

有时候也可能需要将某个类的对象引用转换成另外一个类的对象引用。对象引用的转换语法与数值表达式的类型转换类型,仅需要用一对圆括号将目标类名括起来,并放置再需要转换的对象引用之前就可以了。

Manager boss = (Manager)staff[0];

上面这个程序在运行的时候会报错,因此我们在进行类型转换之前,先查看一下能否成功转换。这个过程简单地使用instanceof操作符就可以。

if(staff[1] instanceof Manager){
    boss = (Manager)staff[1];
    ...
}

因此在类型转换时:

只能在继承层次内进行类型转换

在将超类转换成子类之前,应该使用instanceof进行检查。

在一般情况下,应该尽量少用类型转换和instanceof运算符。

抽象类

如果自下而上在类的继承层次结构中上移,位于上层的类更加具有通用性,甚至可能更加抽象。

下面将Person和类Student添加到类的层次结构中。

3 继承知识点

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

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

除了抽象方法之外,抽象类还可以包含具体数据和具体方法,如Person类还保存着姓名和一个返回姓名的具体方法。

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

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

类即使不含抽象方法,也可以将类声明为抽象类。

抽象类不能被实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。

需要注意的是,可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。例如

Person p = new Student("Vince Vu","Economics");
// 这里的p是一个抽象类Person的变量,Person引用了一个非抽象子类Student的实例。

下面定义一个扩展抽象类Person的具体子类Student:

public class Student extends Person{
    private String major;
    public Student(String name,String major){
        super(name);
        this.major = major;
    }
    public String getDescription(){
        return "a student majoring in"+ major;
    }
}

在Student类中定义了getDescription方法。因此,在Student类中的全部方法都是非抽象的,这个类不再是抽象类。

受保护访问

大家都知道,最好将类中的域标记为private,而方法标记为public.任何声明为private的内容对其他类都是不可见的。

有时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域、为此,需要将这些方法或域声明为protected.如果将超类Employee中的hireDay声明为protected,而不是私有的,Manager中的方法就可以直接地访问它。

private 仅对本类可见
public 对所有类可见
protected 对本包和所有子类可见
默认 不需要修饰符

Ojbect:所有类的超类

Object类是Java中所有类的始祖,在Java中每个类都是由它扩展而来的。

如果没有明确指出超类,Object就被认为是这个类的超类。由于在Java中,每个类都是由Object类扩展而来的,所以,熟悉这个类提供的所有服务十分重要。

可以使用Object类型的变量引用任何类型的对象:

Object obj = new Employee("Harry Hacker",35000)

当然,Object类型的变量只能用于作为各种值的通用持有者,要想对其中的内容进行具体的操作,还需要清除对象的原始类型,并进行相应的转换:

Employee e = (Employee)obj;

在java中,只有基本数据类型不是对象,如数值、字符和布尔类型的值都不是对象。

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

equals方法

Object类中的equals方法用于检测一个对象是否等于另外一个对象。在Object类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。但是对于多数类来说,这种判断并没有什么意义。如,采用这种方式比较两个PrintStrem对象是否相等就完全没有意义。然而,经常需要检测两个对象状态的相等性,如果两个对象的状态相等,就认为这两个对象是相等的。

下面演示equals方法的实现机制

public class Employee{
    ...
    public boolean equals(Object otherOject){
        if(this == otherObject) return true;
        
        if(otherObject == null)return false;
        
        if(getClass() != otherObject.getClass())return false;
        
        Employee other = (Employee)otherObject;
        
        return name.equals(other.name)
            && salary == other.salary
            && hireDay.equals(other.hireDay);
    }
}
// getClass方法将返回一个对象所属的类,在检测中,只有在两个对象属于同一类时,才有可能相等。

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

相等测试与继承

如果隐式和显式的参数不属于同一个类,在前面的例子中,如果发现类不匹配,equals方法就返回false。

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

  • 自反性:对于任何非空的引用x,x.equals(x)应该返回true。
  • 对称性:对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true。
  • 传递性:对于任何引用x、y和z,如果x.euqals(y)返回true.y.equals(z)返回true,x.equals(z)也应该返回true.
  • 一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
  • 对于任意非空引用x,x.equals(null)应该返回false.

下面给出编写一个完美的equals方法的建议:

  • 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量

  • 检测this与otherObject是否引用同一个对象:

    if(this == otherObject)return true;
    
  • 检测otherObject是否为null,如果为null,返回false.这项检测是很必要的

  • if(otherObject == null)retrun false;
    
  • 比较this与otherObjct是否属于同一个类。如果equals的语义在每个子类中有所改变,就使用getClass检测:

  • if(getClass() != otherObject.getClass())return false;
    
  • 将otherObject转换为 相应的类类型变量:

    ClassName other = (ClassName)otherObject
    
  • 现在开始对所有需要比较的域进行比较了。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配。就返回true,否则返回false.

hashCode方法

散列码是由对象导出的一个整型值。散列码是没有规律的。如果x和y是两个不同的对象,x.hashCode()和y.hashCode()基本上不会相同。

Equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。

toString方法

在Object中还有一个重要的方法,就是toString方法,它用于返回表示对象值的字符串。

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

public String toString(){
    return "Employee[name=" + name
        +",slary=" + salary
        +",hireDay="+ hireDay
        + "]";
}

随处可见的toString方法的主要原因是:只有对象与一个字符串通过操作符“+”连接起来,Java编译就会自动地调用toString方法,以便获得这个对象的字符串描述。如:

String message = "The current position is"+p;

java.lang.Object API

class getClass()
// 返回包含对象信息的类对象。
boolean equals(Object otherObject)
// 比较两个对象是否相等,如果两个对象指向同一块存储区域,方法返回true;否则方法返回false.在自定义的类中应该覆盖这个方法。
String toString()
// 返回描述该对象值的字符串。

泛型数组列表

在java中,解决这个问题最简单的方法是使用Java中另外一个被称为ArrayList的类。它使用起来有点像数组,但是在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。

ArrayList是采用一个类型参数的泛型类。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面。

下面声明构造一个保存Employee对象的数组列表:

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

后面的Java SE7后可以省去右边的参数:

ArrayList<Employee> staff = new ArrayList<>();
// 这种被称为“菱形”语法。

使用add方法可以将元素添加到数组列表中。

staff.add(new Employee("Harry Hacker",...));
staff.add(new Employee("Harry Tester",...));

size方将返回数组列表中包含的实际元素数目。如

staff.size()

java.util.ArrayList<>

ArrayList<E>()
// 构造一个空数组列表
ArrayList<E>(int initalCapacity)
// 用指定容量构造一个空数组列表  参数:initalCapcity  数组类别的最初容量
boolean add(E obj)
// 在数组列表的尾端添加一个元素。永远返回true
int size()
// 返回存储在数组列表中的当前元素数量。
void ensureCapcity(int capacity)
// 确保数组列表在不重新分配存储空间的情况下就就能够保存给定数量的元素。
void trimToSize()
// 将数组列表的存储容量削减到当前尺寸

访问数组列表元素

使用get和set方法实现访问或改变数组元素的操作,而不使用人们喜爱的[]语法格式。

如,要设置第i个元素,可以使用:

staff.set(i, harry)

使用下列格式获取数组列表的元素:

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);
staff.add(n,e);
// 为了插入一个元素,位于n之后的所有元素都要向后移动一个位置。如果插入新元素后,数组列表的大小超过了容量,数组列表就会被重写分配新的空间。
staff.remove(n);
// 位于这个位置之后的所有元素都向前移动一个位置,并且数组的大小减1.

对象包装器与自动装箱

有时,需要将int这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。如,Integer类对应基本类型int.通常,这些类称为包装器。这些对象包装器类拥有很明显的名字:Integer,Long,Float、Double、Short、Byte、Character、Void和Boolean。对象包装器类时不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类。

进行下面的调用:

ArrayList<Integer> list = new ArrayList<>();
list.add(3);
将自动地变换成
list.add(Integer.valueOf(3));
// 这种现象被称为自动装箱
int n = list.get(i);
int n = list.get(i).intValue();

大家都知道,==运算符也可以应用于对象包装器对象,只不对检测的是对象是否指向同一个存储区域,因此,下面的比较通常是不会成立的。

Integer a = 1000;
Integer b = 1000;
if(a == b)
    ....

Java实现却有可能实现成立,如果将经常出现的值包装到同一个对象中,这种比较有可能成立,但是这种不确定的结果并不是我们希望的。解决这个方法是在俩个包装器对象比较时调用equals方法。

boolean 、byte、char<= 127,介于-128~127之间的short和int被包装到固定的对象中。

要想将字符串转换成整型,可以使用下面这条语句:

int x = Integer.parseInt(s);
// 这里与Integer对象没有任何关系,parseInt是一个静态方法。但Integer类时放置这个方法的一个号地方。

枚举类

下面是一个典型的例子

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

实际上,这个声明定义的是一个类,它刚好有4个实例,在此尽量不要构造新对象。

在比较两个枚举类型的值时,永远不需要调用equals,而直接使用"=="就可以了。

如果需要的话,可以在枚举类型中添加一些构造器、方法和域。当然,构造器只是在构造枚举常量的时候被调用。

public enum Size{
    
    SAMLL("S"), MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
    
    private String abbreviation;
    private Size(String abbreviation){
        this.abbreviation = abbreviation;
    }
    public String getAbbreviation(){
        return abbreviation;
    }
}

API

static Enum valueOf(Class enumClass,String name)
// 返回指定名字、给定类的枚举常量
String toString()
// 返回枚举常量名
int ordinal()
// 返回枚举常量在enum声明中的位置,位置从0开始计数
int compareTo(E other)
// 如果枚举常量出现在other之前,返回一个负值;如果this == other,则返回0;否则,返回正值。枚举常量的出现次序在enum声明中给出。

反射

能够分析类能力的程序称为反射。反射机制的功能极其强大,反射机制可以用来:

  • 在运行时分析类的能力
  • 在运行时查看对象,例如,编写一个toString方法供所有类使用
  • 实现通用的数组操作代码
  • 利用Method对象,这个对象就像C++中的函数指针。

Class类

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

然而,可以通过专门的Java类访问这些信息。保存这些信息的类被称为Class,这个名字很容易让人混淆。Object类中的getClass()方法将会返回一个Class类型的实例。

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

同用一个Employee对象表示一个特定的雇员属性一样,一个Class对象将表示一个特定类的属性。最常用的Class方法是getName。这个方法将返回类的名字。

System.out.println(e.getClass().getName()+" "+ e.getName());
// 如果e是一个雇员,则会打印出
Employee Harry Hacker
// 如果e是经理,则会打印输出
Manager Harry Hacker

还可以调用静态方法forName获得类名对应的Class对象

String className = "java.util.Random";
Class cl = Class.forName(className);
// 如果类名保存在字符串中,并可在运行中改变,就可以使用这个方法。当然,这个方法只有在className是类名或接口名时才能执行。否则,forName方法将抛出一个checked exception(已检查异常)

获取Class类对象的第三种方法非常简单。如果T是任意的Java类型,T.class将代表匹配的类对象。如:

Class cl1 = Random.class;
Class cl2 = int.class;
Class cl3 = Double[].class;

还有一个很有用的方法newInstance(),可以用来动态创建一个类的实例。如:

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

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

String s = "java.util.Random";
Object m = Class.forName(s).newInstance();

捕获异常

当程序运行过程中发生错误时,就会“抛出异常”。抛出异常比终止程序要灵活得多,这是因为可以提供一个捕获异常的处理器对异常情况进行处理。

异常有两种类型:未检查异常已检查异常。对于已检查异常,编译器将会检查是否提供了处理器。

并不是所有的错误都是可以避免的。如果竭尽全力还是发生了异常,编译器就要求提供一个处理器。Class.forName方法就是一个抛出已检查异常的例子。

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

try{
    statements that might throw exceptions
}
catch(Exception e){
    handler action 
}
// 如果try中的语句抛出异常,程序将直接进入到catch语句中。如果try块中没有抛出异常,难么会跳过catch子句的处理器代码。

利用反射分析类的能力

在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。这三个类都有一个叫做getName的方法,用来返回项目的名称。Field类有一个getType方法,用来返回描述域所属类型的Class对象。Method和Construct类有能够报告参数类型的方法,Method类还有一个可以报告类型的方法。这三个类还有一个叫做getModifiers的方法,它将返回一个整型数值。

Class类中的getFields、getMethods和getConstructs方法将分别返回类提供的public域、方法和构造器数组。其中包括超类的公有成员。Class类的getDeclareFileds、getDeclareMethods和getDeclareConstructors方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。

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

之前我们学习到如何查看任意对象的数据域名称和类型、

  • 获得对应的Class对象
  • 通过Class对象调用getDeclareFields.

查看对象域的关键方法是Field类中的get方法。如果f是一个Field类型的对象,obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域的当前值。

Employee harry = new Employee("Harry Hacker",35000,10,1,1980);
Class cl = harry.getClass();
Field f = c1.getDeclaredField("name");
Object v = f.get(harry);

实际上这段代码存在一个问题,由于name是一个私有域,所以get方法将会抛出一个IllegalAccessException。只有利用get方法才能得到可访问域的值。除非拥有访问权限,否则Java安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。

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

f.setAccessible(true); // now OK to call f.get(harry)

setAccessible方法是AccessibleObject类中的一个方法,它是Field、Method和Constructor类的公共超类。这个特性是调试、持久存储和相似机制提供的。

API

java.lang.reflect.AccessibleObject
void setAccessible(boolean flag)
// 为反射对象设置可访问标志。flag为true表明屏蔽Java语言的访问检查,使得对象的私有属性也可以被查询和设置
boolean isAccessible()
// 返回反射对象的可访问标志的值
static void setAccessible(AccessibleObject[] array,boolean flag)
// 是一种设置对象数组可访问标志的快捷方法
    
java.lang.class
Field getField(String name)
Field[] getField()
// 返回指定名称的公有域,或包含所有域的数组
Field getDeclaredField(String name)
Field[] getDeclareFields()
返回类中声明的给定名称的域,或者包含声明的全部域的数组

java.lang.reflect.Field
Object get(Object obj)
// 返回obj对象中Field对象表示的域
void set(Object boj,Object newValue)
// 用一个新值设置obj对象中Field对象表示的域

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

java.lang.reflect包中的Array类允许动态地创建数组。

使用java.lang.reflect包中Array类的一些方法,其中最关键的是Array类中的静态方法newInstance,它能够构造新数组。在调用它时必须提供两个参数,一个是数组的元素类型,一个是数组的长度。

Object newArray = Array.newInstance(componentType,newLength);
// 为了能够实际地运行,需要获得新数组的长度和元素类型

可以通过调用Array.getLength(a)获得数组的长度,也可以通过Array类的静态getLength方法的返回值得到任意数组的长度。而要获得新数组元素类型,就需要进行一下工作:

  1. 首先获得数组a的类对象。
  2. 确认它是一个数组。
  3. 使用Class类的getComponentType方法确定数组对应的类型。

为什么getLength是Array的方法,而getComponentType是Class的方法呢?

public static Object goodCopyOf(Object a,int length){
    Class cl = a.getClass();
    if(!c1.isArray())return null;
    Class componentType = cl.getComponentType();
    Object newArray = Array.newInstance(componentType,newLength);
    System.arrayCopy(a,0,newArray,0,Math.min(length,newLength);
    return newArray;
}

调用任意方法

在Method类中有一个invoke方法,它允许调用包装在当前Method对象中的方法。invoke方法的签名是:

Object invoke(Object obj,Object...args)
// 第一个参数是隐式参数,其余的对象提供了显示参数。
// 对于静态方法,第一个参数可以被忽略,即可以将它设置为null.

继承的设计技巧

  • 将公共操作和域放在超类

    这就是为什么将姓名域放在Person类中,而没有将它放在Employee和Student类中的原因。

  • 不要使用受保护的域

    使用protected机制并不能够带来更好的保护,其原因主要有两点。第一,子类集合时无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性。第二,在Java程序设计语言中,在同一个包中的所以类都可以访问protected域,而不管它是否为这个类的子类。

  • 使用继承实现“is-a”的关系。

  • 除非所以继承的方法都有意义,否则不要使用继承

  • 在覆盖方法时,不要改变预期的行为

  • 使用多态,而非类型信息

  • 不要过多地使用反射。

return null;
Class componentType = cl.getComponentType();
Object newArray = Array.newInstance(componentType,newLength);
System.arrayCopy(a,0,newArray,0,Math.min(length,newLength);
return newArray;
}


### 调用任意方法

在Method类中有一个invoke方法,它允许调用包装在当前Method对象中的方法。invoke方法的签名是:

```java
Object invoke(Object obj,Object...args)
// 第一个参数是隐式参数,其余的对象提供了显示参数。
// 对于静态方法,第一个参数可以被忽略,即可以将它设置为null.

继承的设计技巧

  • 将公共操作和域放在超类

    这就是为什么将姓名域放在Person类中,而没有将它放在Employee和Student类中的原因。

  • 不要使用受保护的域

    使用protected机制并不能够带来更好的保护,其原因主要有两点。第一,子类集合时无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性。第二,在Java程序设计语言中,在同一个包中的所以类都可以访问protected域,而不管它是否为这个类的子类。

  • 使用继承实现“is-a”的关系。

  • 除非所以继承的方法都有意义,否则不要使用继承

  • 在覆盖方法时,不要改变预期的行为

  • 使用多态,而非类型信息

  • 不要过多地使用反射。

上一篇:MySQL 索引和锁机制总结


下一篇:Java 泛型