首页 > 编程语言 > 详细

CORE JAVA 第五章 继承

时间:2020-07-21 22:07:09      阅读:79      评论:0      收藏:0      [点我收藏+]

第5章 继承

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

? 此外,本章还阐述了反射(reflection)的概念。反射是指在程序运行期间发现更多的类及其属性的能力。

5.1 类、超类和子类

? “is-a”关系是继承的一个明显特征。

5.1.1 定义子类

? 关键字extends表示继承。

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

? 关键字extends表明正在构造的新类派生于一个已存在的类。已存在的类称为超类(superclass)基类(base class)父类(parent class);新类称为子类(subclass)、派生类(derived class)或孩子类(child class)

? 子类比超类拥有的功能更加丰富

? Employee类的对象不能使用Manager类中新增的方法,但属于Manager类的对象可以使用Employee类的方法,这是因为Manager类自动地继承了超类中的方法,还继承了超类中的域。

? 在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中。

5.1.2 覆盖方法

? 超类中有些方法对子类并不一定适用,为此,需要提供一个新的方法来覆盖超类中的这个方法。

? 如果在子类中定义一个方法,其名称、返回类型及参数签名正好与父类中某个方法的名称、返回类型及参数签名相匹配,那么可以说,子类的方法覆盖了父类的方法。

public class Manager extends Employee
{
    public double getSalary()
    {
        return salary + bonus;//won‘t work
    }
}

?

? 但是,这个方法并不能运行,因为子类Manager的getSalary方法不能够直接地访问超类的私有域。也就是说,尽管每个Manager对象都拥有一个名为salary的域,但在Manager类的getSalary方法中并不能够直接地访问salary域。只有Employee类的方法才能够访问私有部分。如果Manager类的方法一定要访问私有域,就必须借助于公有的接口,Employee类中的公有方法getSalary正是这样一个接口。

? 将对salary域的访问替换成调用getSalary方法:

public double getSalary()
{
    double baseSalary = getSalary(); //still won‘t work
    return baseSalary + bonus;
}

? 上面这段代码仍然不能运行,因为Manager类也有一个getSalary方法,所以这条语句将会导致无限次地调用自己,直到整个程序崩溃为止。

? 这里需要指出:我们希望调用超类Employee中的getSalary方法,而不是当前类的这个方法。为此,我们可以使用关键字super解决这个问题:

super.getSalary()

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

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

? super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

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

5.1.3 子类构造器

? 提供一个构造器:

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

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

super(n, s, year, month, day)

调用超类Employee中含有n、s、year、month、和day参数的构造器的简写形式。

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

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

? 回忆一下:

  • 关键字this有两个用途:
  1. 引用隐式参数
  2. 调用该类其他的构造器
  • 关键字super也有两个用途:
  1. 调用超类的方法
  2. 调用超类的构造器

? 在调用构造器时,这两个关键字的使用方式很相似。调用构造器的语句只能作为另一个构造器的第一条语句出现。构造参数既可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。

? 一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。

? 例如:

Employee[] staff = new Employee[3];
Manager boss = new Manager();
staff[0] = boss;
staff[1] = new Employee();
staff[2] = new Employee();
for(Employee e : staff)
    System.out.println(e.getName() + " " + e.getSalary());

? 以上中e.getSalary()调用能够确定应该执行哪个getSalary方法。尽管这里将e声明为Employee类型,但实际上e即可以引用Employee类型的对象,也可以引用Manager类型的对象。虚拟机知道e实际引用的对象类型,因此能够正确地调用相应的方法。

5.1.4 继承层次

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

? 通常,一个祖先类可以拥有多个子孙继承链。

? Java不支持多继承。

5.1.5 多态

? 多态是同一个行为具有多个不同表现形式或形态的能力。

? 多态就是同一个接口,使用不同的实例而执行不同操作。多态性是对象多种表现形式的体现。

? 多态存在的三个必要条件:

  • 继承

  • 重写

  • 父类引用指向子类对象

    ? 多态的实现方式:

    ? 方式一:重写

    ? 方式二:接口

    ? 方式三:抽象类和抽象方法

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。

父类引用指向子类对象,子类重写了父类的方法,调用父类的方法,实际调用的是子类重写了的父类的该方法。

? “is-a”规则:

  • 子类的每个对象也是超类的对象。

  • 程序中出现超类对象的任何地方都可以用子类对象置换。

    例如,可以将一个子类的对象赋给超类变量。

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

    ? 在Java中,对象变量是多态的。一个Employee变量既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类的对象。

    ?

    ? 然而,不能将一个超类的引用赋给子类变量。

    Manager m = staff[i]; //Error
    

    ? 在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。

    Manager[] managers = new Manager[10];
    Employee[] staff = managers; //OK
    

    ? 这样做可能会产生问题,应尽量避免。所有数组都要牢记创建他们的元素类型,并负责监督仅将类型兼容的引用存储到数组中。

5.1.6 理解方法调用

? 弄清楚如何在对象上应用方法调用非常重要。下面假设要调用x.f(args),隐式参数x声明为类C的一个对象。下面是调用过程的详细描述:

  1. 编译器查看对象的声明类型和方法名。编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法。
  2. 编译器将查看调用方法时提供的参数类型。如果存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析。
  3. 如果是private方法、static方法、final方法或者构造器,那么编译器可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。在示例中,编译器采用动态绑定的方式生成一条调用f(String)的指令。
  4. 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。
    每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。在真正调用方法的时候,虚拟机仅查表就行了。

? 动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。

?

? 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是,如果超类方法是public,子类方法一定要声明为public。

5.1.7 阻止继承:final类和方法

? 不允许扩展的类被称为final类,定义时使用final修饰符。

public final class Executive extends Manager
{
    ……
}

? 类中的特定方法也可以被声明为final,这样做子类就不能覆盖这个方法。final类中的所有方法自动的成为final方法。

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

? 域也可以被声明为final,构造对象之后它们的值就不允许被改变。不过,如果将一个类声明为final,只有其中的方法自动地成为final,而不包括域。

?

? 将方法或类声明为final主要目的是:确保它们不会在子类中改变语义。

? 如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程称为内联,例如,内联调用e.getName()将被替换为访问e.name域。

5.1.8 强制类型转换

? 将表达式x的值转换成整数类型,舍弃了小数部分:

double x = 3.405;
int nx = (int) x;

? 有时候,也可能需要将某个类的对象引用转换成另外一个类的对象引用。

? 对象引用的转换语法:用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。

Manager boss = (Manager) staff[0];

? 进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。

?

? 将一个值存入变量时,编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换,这样才能够通过运行时的检查。

?

Employee em = boss;//	OK

Manager ma = staff[1];
//Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
//	Type mismatch: cannot convert from Employee to Manager
//有Error

Manager ma = (Manager) staff[1];
//Exception in thread "main" java.lang.ClassCastException: a.Employee cannot be cast to a.Manager
//没有Error了,但是仍有异常

? 只是能通过检查,但不一定正确。可能抛出异常,如下;

?

? 如果试图在继承链上进行向下(超类转换成子类)的类型转换,并且谎报有关对象包含的内容,例如:

Manager boss = (Manager) staff[1]; //不可以
//Exception in thread "main" java.lang.ClassCastException: a.Employee cannot be cast to a.Manager

? Java运行时系统将报告这个错误,并产生一个ClassCastException异常。如果没有捕获这个异常,那么程序就会终止。

? instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。

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

staff[1] instanceof Employee;//true
staff[1] instanceof Manager;//false

boss instanceof Employee;//true
boss instanceof Manager;//true

? 良好的程序设计习惯:在进行类型转换之前,先查看一下是否能够成功地转换。使用instanceof操作符就可以实现。 例如:

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

? 如果这个类型转换不可能成功,编译器就不会进行这个转换。例如:

String c = (String) staff[1];

? 将会产生编译错误,因为String不是Employee的子类。

? 综上所述:

  • 只能在继承层次内进行类型转换。
  • 在将超类转换成子类之前,应该使用instanceof进行检查。

? 实际上,通过类型转换调整对象的类型并不是一种好的做法。大多数情况并不需要将Employee对象转换成Manager对象,只有在使用Manager中特有的方法时才需要进行类型转换,但这时候超类的设计可能就不合理了,重新设计超类才是正确选择。

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

5.1.9 抽象类

? 从某种角度看,祖先类更加通用,人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。

? 使用abstract关键字,这样就完全不需要实现这个方法了。

public abstract String getDescription();
// no implementation required

? 包含一个或多个抽象方法的类本身必须被声明为抽象的。

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

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

? 抽象方法充当着占位的角色,它们的具体实现在子类中。

? 扩展抽象类有两种选择:

  1. 在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类。
  2. 定义全部的抽象方法,这样子类就不是抽象的了。

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

? 抽象类不能被实例化,不能创建抽象类的对象。但可以创建一个具体子类的对象。

? 可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。

Person p = new Student();

?

5.1.10 受保护访问

? 在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为protected。

? 例如,如果将超类Employee中的hireDay声明为protected,Manager中的方法就可以直接地访问它。不过,Manager类中的方法只能够访问Manager对象中的hireDay域,而不能访问其他Employee对象中的这个域。

? 归纳一下Java用于控制可见性的4个访问修饰符:

  1. 仅对本类可见——private。
  2. 对所有类可见——public。
  3. 对本包和所有子类可见——protected。
  4. 对本包可见——默认,不需要修饰符。

?

5.2 Object:所有类的超类

? Object类是Java中所有类的始祖,在Java中每个类都是由它扩展而来。但是并不需要这么写:

public class Employee extends Object

? 如果没有明确的指出超类,Object就被认为是这个类的超类。

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

Object obj = new Employee();

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

Employee e = (Employee) obj;

? 在Java中,只有基本类型(数值、字符、布尔类型的值)不是对象。

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

Employee[] staff = new Employee[10];
obj = staff;//OK
obj = new int[10];//OK

5.2.1 equals方法

? Object类中的equals方法用于检测一个对象是否等于另外一个对象。在Object类中,这个方法将判断两个对象是否具有相同的引用(指向同一块存储区域)。如果两个对象具有相同的引用,它们一定是相等的。

? 然而,对于多数类来说,这种判断并没有什么意义。经常需要检测两个对象状态的相等性,如果两个对象的状态相等,就认为这两个对象是相等的。

? 例如,如果两个雇员对象的姓名、薪水和雇用日期都一样,就认为他们是相等的。

? 利用下面这个实例演示equals方法的实现机制:

public class Employee
{
    ……
    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 name.equals(other.name)
            && salary == other.salary
            && hireDay.equals(other.hireDay);
    }
}

? getClass方法将返回一个对象所属的类。在检测中,只有在两个对象属于同一个类时,才有可能相等。

?

? 为了防备name或hireDay可能为null的情况,需要使用Objects.equals方法。

? Objects.equals(a,b):如果两个参数都为null,返回true;其中一个为null,返回false。否则,调用a.equals(b)。

? 最后一条语句要改写为:

return Objects.equals(name, other.name)
    && salary == other.salary
    && Objects.equals(hireDay, other.hireDay);

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

public class Manager extends Employee
{
    ……
    public boolean equals(Object otherObject)
    {
        if(!super.equals(otherObject)) return false;
        
        Manager other = (Manager) otherObject;
        return bonus == other.bonus;
    }
}

5.2.2 相等测试与继承

? 如果隐式和显式的参数不属于同一个类,equals方法将如何处理呢?

? 在前面的例子中,如果发现类不匹配,equals方法就返回false。

? 但是,许多程序员喜欢使用instanceof进行检测:

if (!(othenObject instanceof Employee))	return false;

这样做不但没有解决otherObject是子类的情况,并且还有可能会招致一些麻烦。建议不要使用这种处理方式。

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

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

? 关于对称性,当参数不属于同一个类时:

  • 如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测。(Employee与Manager,如果使用instanceof,则不满足对称性
  • 如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。(AbstractSet的equals方法检测两个集合是否有相同的元素。AbstractSet类的两个具体子类:TreeSet和HashSet分别使用不同的算法实现查找集合元素)

? 在雇员和经理的例子中,只要对应的域相等,就认为两个对象相等。如果两个Manager对象对应的姓名、薪水和雇用日期均相等,而奖金不相等,就认为他们是不相同的,因此可以使用getClass检测。

? 但是假设使用雇员的ID作为相等的检测标准,并且这个相等的概念适用于所有的子类,就可以使用instanceof进行检测,并应该将Employee.equals声明为final。

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

  1. 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。
  2. 检测this与otherObject是否引用同一个对象:
if (this == otherObject)	return true;
  1. 检测otherObject是否为null,如果为null,返回false。

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

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

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

    if (!(otherObject instanceof ClassName))	return false;
    
  3. 将otherObject转换为相应的类类型变量:

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

    return field1 == other.field1
        && Objects.equals(field2,other.field2)
        && ……;
    

如果在子类中重新定义equals,就要在其中包含调用super.equals(other)。

? 对于数组类型的域,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等。

? 可以使用@Override对覆盖超类的方法进行标记。如果出现了错误,并且正在定义一个新方法,编译器就会给出错误报告。

5.2.3 hashCode方法

? 散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。

? String类使用下列算法计算散列码:

int hash = 0;
for (int i = 0; i < length(); i++)
    hash = 31 * hash + chaAr(i);

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

String s = "OK";
StringBuilder sb = new StringBuilder(s);
//s.hashCode():2556	
//sb.hashCode():20526976

String t = new String("OK");
StringBuilder tb = new StringBuilder(t);
//t.hashCode():2556
//tb.hashCode():20527144

? 字符串s和t有相同的散列码,这是因为字符串的散列码是由内容导出的。而字符串缓冲sb与tb却有着不同的散列码,这是因为在StringBuffer类中没有定义hashCode方法,它的散列码是由Object类的默认hashCode方法导出的对象存储地址。

? 如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。

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

例如,下面是Employee类的hashCode方法:

public class Employee
{
    public int hashCode()
    {
        return 7 * name.hashCode()
            + 11 * new Double(salary).hashCode()
            + 13 * hireDay.hashCode();
    }
}

? 改进:

? 使用null安全的Objects.hashCode方法:如果参数为null,这个方法会返回0,否则返回对参数调用hashCode的结果。

另外,使用静态方法Double.hashCode方法避免创建Double对象:

public int hashCode()
{
    return 7 * Objects.hashCode(name)
        + 11 * Double.hashCode(salary)
        + 13 * Objects.hashCode(hireDay);
}

? 还有更好的做法,需要组合多个散列值时,可以调用Objects.hash并提供多个参数。这个方法会对各个参数调用Objects.hashCode,并组合这些散列值。

public int hashCode()
{
    return Objects.hash(name, salary, hireDay);
}

? equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。例如,如果用定义的Employee.equals比较雇员的ID,那么hashCode方法就需要散列ID,而不是雇员的姓名或存储地址。

? 如果存在数组类型的域,可以使用静态的Arrays.hashCode方法计算一个散列码,这个散列码由数组元素的散列码组成。

5.2.4 toString方法

? toString方法返回表示对象值的字符串。绝大多数的toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的值域。

? 最好通过调用getClass().getName()获得类名的字符串,而不要将类名硬加到toString方法中。

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

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

Point p = new Point(10, 20);
String message = "The current position is " + p;
	// automatically invokes p.toString()

? 在调用x.toString()的地方可以用""+x替代。这条语句将一个空串与x的字符串表示相连接。这里的x就是x.toString()。如果x是基本类型,这条语句照样能够执行。

? 如果x是任意一个对象,并调用:

System.out.println(x);

println方法就会直接地调用x.toString(),并打印输出得到的字符串。

? Object类定义了toString方法,用来打印输出对象所属的类名和散列码。例如,调用

System.out.println(System.out)

将输出下列内容:

java.io.PrintStream@2f6684

之所以得到这样的结果是因为PrintStream类的设计者没有覆盖toString方法。

? 数组继承了Object类的toString方法,数组类型将按照旧的格式打印。例如:

int[] luckyNumbers = {2, 3, 5, 7, 11, 13};
String s = "" + luckyNumbers;
// s = [I@7852e922 前缀[I表明是一个整型数组。

? 修正的方式是调用静态方法Arrays.toString()。代码:

String s = Arrays.toString(luckyNumbers);

将生成字符串“[2,3,5,7,11,13]”。

? 打印多维数组,使用Arrays.deepToString方法。

? toString方法是一种非常有用的调试工具。在标准类库中,许多类都定义了toString方法,以便用户能够获得一些有关对象状态的必要信息。像下面这样显示调试信息非常有益:

Logger.global.info("Current position = " + position);//第七章

5.3 泛型数组列表

? 在Java中,允许在运行时确定数组的大小:

int actualSize = …… ;
Employee[] staff = new Employee[actualSize];

? 当然 ,这段代码没有完全解决运行时动态更改数组的问题。使用ArrayList类,在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。

? ArrayList是一个采用类型参数泛型类。下面声明和构造一个保存Employee对象的数组列表:

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

//在Java SE 7中,可以省去右边的参数类型:
ArrayList<Employee> staff = new ArrayList<>();

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

? 数组列表管理着对象引用的一个内部数组。最终,数组的全部空间有可能被用尽。如果调用add且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组拷贝到较大的数组中。

? 如果已经清楚或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity方法:

staff.ensureCapacity(100);

这个方法调用将分配一个包含100个对象的内部数组。然后调用100次add,而不用重新分配空间。

? 另外,还可以把初始容量传递给ArrayList构造器:

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

? 数组列表的容量与数组的大小有一个非常重要的区别。如果为数组分配100个元素的存储空间,数组就有100个空位置可以使用。而容量为100个元素的数组列表只是拥有保存100个元素的潜力(实际上,重新分配空间的话,将会超过100),但是在最初,甚至完成初始化构造之后,数组列表根本就不含有任何元素。

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

? 一旦能够确认数组列表的大小不再发生变化,就可以调用trimToSize方法,将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。

5.3.1 访问数组列表元素

? 设置第i个元素:

staff.set(i, harry);
//等价于a[i] = harry;

只有i小于或等于数组列表的大小时,才能够调用list.set(i,x)。例如,下面这段代码是错误的:

ArrayList<Employee> list = new ArrayList<>(100);// capacity 100, size 0
list.set(0, x);//no element 0 yet

使用add方法为数组添加新元素,而不要使用set方法,它只能替换数组中以及存在的元素内容。

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

Employee e = staff.get(i);
//等价于Employee e = a[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);

位于n之后的所有元素都要向后移动一个位置。

? 同样地,可以从数组列表中间删除一个元素。

Employee e = staff.remove(n);

位于这个位置之后的元素都向前移动一个位置,并且数组的大小减1。

?

? 对数组列表进行插入和删除元素的操作效率比较低。

? 可以使用for each循环遍历数组列表:

for(Employee e : staff)
    	do something with e 

5.3.2 类型化与原始数组列表的兼容性

? 在自己的代码中,可能更愿意使用类型参数来增加安全性。这一节介绍如何与没有使用类型参数的遗留代码交互操作。

? 假设有下面这个遗留下来的类:

public class EmployeeDB
{
    public void update(ArrayList list){……}
    public ArrayList find(String query){……}
}

可以将一个类型化的数组列表传递给update方法,而并不需要进行任何类型转换。

ArrayList<Employee> staff = ……;
employeeDB.update(staff);

也可以将staff对象传递给update方法。

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

? 相反地,将一个原始ArrayList赋给一个类型化ArrayList会得到一个警告。

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

使用类型转换并不能避免出现警告。

ArrayList<Employee> result = (ArrayList<Employee>) employeeDB.find(query);    
// yields another warning
//会指出类型转换有误

? 鉴于兼容性的考虑,编译器在对类型转换进行检查之后,如果没有发现违反规则的现象,就将所有的类型化数组列表转换成原始ArrayList对象在程序运行时,所有的数组列表都是一样的,即没有虚拟机中的类型参数。因此,类型转换(ArrayList)和(ArrayList)将执行相同的运行时检查。

? 在这种情形下,不必做什么。只要在与遗留的代码进行交叉操作时,研究一下编译器的警告性提示,并确保这些警告不会造成太严重的后果就行了。

? 一旦能确保不会造成严重的后果,可以用@SuppressWarnings("unchecked")标注来标记这个变量能够接受类型转换,如下:

@SuppressWarnings("unchecked") ArrayList<Employee> result = 
    (ArrayList<Employee>) employeeDB.find(query);  // yields another warning

5.4 对象包装器与自动装箱

? 有时,需要将int这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类,称为包装器。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前6个类派生于公共的超类Number)。

? 对象包装器是不可变的,一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器还是final,因此不能定义它们的子类。

? 数组列表中的参数类型不允许是基本类型,可以使用Integer对象包装器类。

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

? 有一个特性,便于添加int类型的元素到ArrayList中。下面这个调用:

list.add(3);

将自动地变换成

list.add(Integer.valueOf(3));

这种变换被称为自动装箱

? 相反地,将一个Integer对象赋给一个int值时,将会自动拆箱。编译器将下列语句:

int n = list.get(i);

翻译成:

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

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

Integer n = 3;
n++;

编译器将自动地插入一条对象拆箱的指令,然后进行自增运算,最后再将结果装箱。

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

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

然而,Java实现却有可能让它成立。如果将经常出现的值包装到同一个对象中,这种比较就有可能成立。解决这个问题的办法是在两个包装器对象比较时调用equals方法。

? 自动装箱规范要求boolean、byte、char<=127,介于-128~127之间的short和int被包装到固定的对象中。例如,如果在前面的例子中将a和b初始化为100,对它们进行比较的结果一定成立。

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

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

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

? 使用数值对象包装器还有另外一个好处。可以将某些基本方法放置在包装器中,例如,将一个数字字符串转换成数值:

int x = Integer.parseInt(s);

这里与Integer对象没有任何关系,parseInt是一个静态方法。

? 包装器类也不可以用来实现修改数值参数的方法,因为Integer对象是不可变的,包含在包装器中的内容不会改变。

? 如果想编写一个修改数值参数值的方法,就需要使用在org.omg.CORBA包中定义的holder类型,包括IntHolder、BooleanHolder等。每个holder类型都包含一个公有域值,通过它可以访问存储在其中的值。

public static void triple(IntHolder x)
{
    x.value = 3 * x.value;
}

5.5 参数数量可变的方法

? printf方法就是参数数量可变的方法。

? printf方法的定义:

public class PrintStream
{
    public PrintStream printf(String fmt, Object... args) { return format(fmt, args); }
}

这里的省略号...是Java代码的一部分,它表明这个方法可以接收任意数量的对象(除fmt参数之外)。

? 实际上,printf方法接收两个参数,一个是格式字符串,另一个是Object[]数组,其中保存着所有的参数(如果调用者提供的是整型数组或者其他基本类型的值,自动装箱功能将把它们转换成对象)。现在将扫描fmt字符串,并将第i个格式说明符与args[i]的值匹配起来。

? 换句话说,对于printf的实现者来说,Object...参数类型与Object[]完全一样。

? 编译器需要对printf的每次调用进行转换,以便将参数绑定到数组上,并在必要的时候进行自动装箱:

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

? 用户自己也可以定义可变参数的方法,并将参数指定为任意类型,甚至是基本类型。

? 允许将一个数组传递给可变参数方法的最后一个参数。因此,可以将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法,而不会破坏任何已经存在的代码。

5.6 枚举类

? Java 枚举是一个特殊的类,一般表示一组常量。使用 enum 关键字来定义,各个常量使用逗号 , 来分割。

? 定义枚举类型的例子:

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

实际上,这个声明定义的类型是一个类,它刚好有4个实例,这些枚举成员默认都被 final、public, static 修饰,当使用枚举类型成员时,直接使用枚举名称调用成员即可。在此尽量不要构造新对象。

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

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

public enum Size
{
    SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
    //以上是枚举的成员,必须先定义,而且使用分号结束
    
    private String abbreviation;
    
    //构造函数只能使用 private 访问修饰符
    private Size(String abbreviation) {this.abbreviation = abbreviation;}
    public String getAbbreviation() {return abbreviation;}
}

? 所有的枚举类型都是Enum类的子类,它们继承了这个类的许多方法。其中最有用的一个是toString,这个方法能够返回枚举常量名。例如,Size.SMALL.toString()将返回字符串“SMALL”。

? toString的逆方法是静态方法valueOf。例如,语句:

Size s = Enum.valueOf(Size.class, "SMALL");
// 将s设置成Size.SMALL.

? 每个枚举类型都有一个静态的values方法,他将返回一个包含全部枚举值的数组。

? ordinal方法返回enum声明中枚举常量的位置,从0开始计数。

5.7 反射

? 反射库提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵Java代码的程序。使用反射,在设计或运行中添加新类时,能够快速地应用开发工具动态地查询新添加类的能力。

? 能够分析类能力的程序称为反射。反射机制可以用来:

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

5.7.1 Class类

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

? 可以通过专门的Java类访问这些信息。保存这些信息的类被称为Class,Object类中的getClass()方法会返回一个Class类型的实例。

Employee e;
……
Class c1 = e.getClass();

? 如同用一个Employee对象表示一个特定的雇员属性一样,一个Class对象将表示一个特定类的属性。最常用的Class方法是getName。这个方法将返回类的名字。如果类在一个包里,包的名字也作为类名的一部分。

? 静态方法forName获得类名对应的Class对象。

String className = "java.util.Random";
Class c1 = Class.forName(className);

如果类名保存在字符串中,并可在运行中改变,就可以使用这个方法。当然,这个方法只有在className是类名或接口名时才能够执行。无论何时使用这个方法,都应该提供一个异常处理器。

? 获得Class类对象的第三种方法:如果T是任意的Java类型(或void关键字),T.class将代表匹配的类对象。例如:

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

注意:一个Class对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int不是类,但Int.class是一个Class类型的对象。

? Class类实际上是一个泛型类。

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

? newInstance()方法,可以用来动态地创建一个类的实例。

e.getClass().newInstance();

创建了一个与e具有相同类类型的实例。newInstance方法调用默认的构造器初始化新创建的对象。

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

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

注释:如果需要以这种方式向希望按名称创建的类的构造器提供参数,就不要使用上面那条语句,而必须使用Construtor类中的newInstance方法。

5.7.2 捕获异常

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

? 如果没有提供处理器,程序就会终止,并在控制台上打印一条信息,其中给出了异常的类型。

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

? 最简单的处理器:

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

try
{
    //statements that might throw exceptions
}
catch (Exception e)
{
   // handler action
}

如果try块中没有抛出任何异常,那么会跳过catch子句的处理器代码。

? 对于已检查异常,只需要提供一个异常处理器。可以很容易地发现会抛出已检查异常的方法。如果调用了一个抛出已检查异常的方法,而又没有提供处理器,编译器就会给出错误的报告。

5.7.3 利用反射分析类的能力

? 反射机制最重要的内容:检查类的结构。

? 在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。

  • 这三个类都有一个叫做getName的方法,用来返回项目的名称。
  • Field类有一个getType方法,用来返回描述域所属类型的Class对象。
  • Method和Constructor类有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。
  • 这三个类都有一个叫做getModifiers的方法,它将返回一个整型数值,用不同的位开关描述public和static这样的修饰符使用情况。另外,还可以利用java.lang.reflcet包中的Modifier类的静态方法分析getModifiers返回的整型数值。例如,可以使用Modifier类中的isPublic、isPrivate或isFinal判断方法或构造器是否是public、private或final。

? Class类中的:

  • getFields、getMethods和getConstructors方法将分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。

  • getDeclaredFields、getDeclareMethods和getDeclaredConstructors方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。

? 程序清单5-13可以分析Java解释器能够加载的任何类,能够打印一个类的全部信息。输入类名,输出类中所有的方法和构造器的签名,以及全部域名。

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

? 前一节讲述了如何查看任意对象的数据域名称和类型:

  • 获得对应的Class对象。
  • 通过Class对象调用getDeclaredFields。

本节将进一步查看数据域的实际内容。利用反射机制,可以查看在编译时还不清楚的对象域。

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

//get方法签名
Object get(Object obj)
Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
Class cl = harry.getClass();
	// the class object representing Employee
Field f = cl.getDeclaredFields("name");
	// the name field of the Employee class
Object v = f.get(harry);
	// the value of the name field of the harry object,i.e.,the String object "Harry Hacker"

但是,由于name是一个私有域,所以get方法将会抛出一个IllegalAccessException。

? 只有利用get方法才能得到可访问域的值。除非拥有访问权限,否则Java安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。

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

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

这个特性是为调试、持久存储和相似机制提供的。

? 此外,反射机制可以自动地将基本类型域值打包到相应的对象包装器中,如double->Double。

? 调用f.set(obj, value)可以将obj对象的f域设置成新值。

? 程序清单5-14objectAnalyzer显示了如何编写一个可供任意类使用的通用toString方法。先使用getDeclaredField、setAccessible和get方法获得每个域的名字和值,递归调用toString方法,将每个值转换成字符串。

? 还可以使用通用的toString方法实现自己类中的toString方法,如下:

public String toString()
{
    return new ObjectAnalyzer().toString(this);
}

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

? java.lang.reflect包中的Array类允许动态地创建数组。例如,将这个特性应用到Arrays类中的copyOf方法实现中,这个方法可以用于扩展已经填满的数组。

Employee[] a = new Employee[100];

a = Arrays.copyOf(a, 2 * a.length);

? 如何编写这样一个通用的方法,能够将Employee[]数组转换为Object[]数组?

? 第一次尝试:

public static Object[] badCopyOf(Object[] a, int newLength)	// not useful
{
    Object[] newArray = new Object[newLength];
    System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
    return newArray;
}

然而,在实际使用结果数组时会遇到一个问题。这段代码返回的数组类型是对象数组(Object[])类型,这是因为使用下面这行代码创建的数组:

new Object[newLength];

一个对象数组不能转换成雇员数组。如果这样做,则在运行时Java将会产生ClassCastException异常。

Java数组会记住每个元素的类型,即创建数组时new表达式中使用的元素类型。将一个Employee[]临时的转换成Object[]数组,然后再把它转换回来是可以的,但一个从开始就是Object[]的数组却永远不能转换成Employee[]数组。

? 为了编写这类通用的数组代码,需要能够创建与原数组类型相同的新数组。

Object newArray = Array.newInstance(componentType, newLength);
//param:	componentType:数组元素类型

为了能够实际的运行,需要获得新数组的长度和元素类型。

  • 可以通过调用Array.getLength(a)获得数组的长度,也可以通过Array类的静态getLength方法的返回值得到任意数组的长度。

  • 获得新数组元素类型:

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

代码:

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

这个CopyOf方法可以用来扩展任意类型的数组,而不仅是对象数组。

int[] a = {1, 2, 3, 4, 5};
a = (int[]) goodCopyOf(a, 10);

为了能够实现上述操作,应该将goodCopyOf的参数声明为Object类型,而不要声明为对象型数组(Object[])。整型数组类型int[]可以被转换成Object,但不能转换成对象数组。

5.7.6 调用任意方法

? 反射机制允许调用任意方法。可以使用Method对象实现C语言中函数指针的所有操作。

? 与Field类的get方法查看对象域的过程类似,在Method类中有一个invoke方法,允许调用包装在当前Method对象中的方法:

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

第一个参数是隐式参数,其余的对象提供了显示参数。

对于静态方法,第一个参数可以被忽略,即可以将它设置为null。

? 例如,假设用ml代表Employee类的getName方法,下面这条语句显示了如何调用这个方法:

String n = (String) ml.invoke(harry);

? 如果返回类型是基本类型,invoke方法会返回其包装器类型。

? 如何得到Method对象呢?

  • 可以通过调用getDeclareMethods方法,然后对返回的Method对象数组进行查找,知道发现想要的方法为止。
  • 也可以调用Class类中的getMethod方法得到想要的方法。
//getMethod方法签名:
Method getMethod(String name, Class... parameterTypes)

下面说明了如何获得Employee类的getName方法和raiseSalary方法的方法指针:

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

? 注意:invoke的参数和返回值必须是Object类型的,这就意味着必须进行多次的类型转换。这样做将会使编译器错过检查代码的机会。因此,等到测试阶段才能发现这些错误,找到并改正它们将会更加困难。不仅如此,使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些。

? 有鉴于此,建议仅在必要的时候才使用Method对象,而最好使用接口或lambda表达式。

5.8 继承的设计技巧

  1. 将公共操作和域放在超类
  2. 不要使用受保护的域

protected机制并不能够带来更好的保护:

  • 子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性。
  • 在同一个包中的所有类都可以访问protected域,而不管它是否为这个类的子类。

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

  1. 使用继承实现“is-a"关系
  2. 除非所有继承的方法都有意义,否则不要使用继承
  3. 在覆盖方法时,不要改变预期的行为
  4. 使用多态,而非类型信息

对于下面这种形式的代码:

if (x is of type 1)
    action1(x);
else if (x is of type 2)
    action2(x);

都应该考虑使用多态性。

如果action1和action2表示的是相同的概念,就应该为这个概念定义一个方法,并将其放置在两个类的超类或接口中,然后,就可以调用

x.action();

以便使用多态性提供的动态分配机制(动态分配与静态分配:在Java多态的两种常见用法中,方法重载使用的是静态分派机制,而方法重写使用的是动态分派机制。这也就导致了,方法重载调用的时候是根据变量的静态类型来决定调用哪个方法。而方法重写的时候,则是根据变量的实际类型来决定调用哪个方法。)执行相应的操作。

? 使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更容易维护和扩展。

  1. 不要过多的使用反射

编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。

CORE JAVA 第五章 继承

原文:https://www.cnblogs.com/c1utchfan/p/13356921.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!