Java核心技术笔记 继承

《Java核心技术 卷Ⅰ》 第5章 继承前端

  • 类、超类、子类
  • Object:全部类的超类
  • 泛型数组列表
  • 对象包装器与自动装箱
  • 参数数量可变的方法
  • 枚举类
  • 继承的设计技巧

类、超类和子类

定义子类

关键字extend表示继承。java

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

extend代表正在构造的新类派生于一个已存在的类。git

已存在的类称为超类(superclass)、基类(base class)或父类(parent class);
新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。程序员

子类有超类没有的功能,子类封装了更多的数据,拥有更多的功能。github

因此在扩展超类定义子类时,仅须要指出子类与超类的不一样之处。数组

覆盖方法

有时候,超类的有些方法并不必定适用于子类,为此要提供一个新的方法来覆盖(override)超类中的这个方法:安全

public class Manager extends Employee
{
  private double bonus;
  ...
  public double getSalary()
  {
    double baseSalary = super.getSalary();
    return baseSalary + bonus;
  }
  ...
}

这里因为Manager类的getSalary方法并不能直接访问超类的私有域app

这是由于尽管子类拥有超类的全部域,可是子类无法直接获取到超类的私有部分,由于超类的私有部分只有超类本身才可以访问。而子类想要获取到私有域的内容,只能经过超类共有的接口。ide

而这里Employee的公有方法getSalary正是这样的一个接口,而且在调用超类方法使,要使用super关键字。工具

那你可能会好奇:

  • 不加super关键字不行么?
  • Employee类的getSalary方法不该该是被Manager类所继承了么?

这里若是不使用super关键字,那么在getSalary方法中调用一个getSalary方法,势必会引发无限次的调用本身。

关于superthis须要注意的是:他们并不相似,由于super不是一个对象的引用,不能将super赋给另外一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

子类构造器

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

语句super(...)是调用超类中含有对应参数的构造器。

Q1:为何要这么作

A1:因为子类的构造器不能访问超类的私有域,因此必须利用超类的构造器对这部分私有域进行初始化。可是注意,使用super调用构造器的语句必须是子类构造器的第一条语句

Q2:必定要使用么

A2:若是子类的构造器没有显示地调用超类构造器,则将自动地调用超类默认(没有参数)的构造器;若是超类并无不带参数构造器,而且子类构造器中也没有显示调用,则Java编译器将报告错误

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

这里将e声明为Emplyee对象,可是实际上e既能够引用Employee对象,也能够引用Manager对象。

  • 当引用Employee对象时,e.getSalary()调用的是EmployeegetSalary方法
  • 当引用Manager对象时,e.getSalary()调用的是ManagergetSalary方法

虚拟机知道e实际引用的对象类型,因此可以正确地调用相应的方法。

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

继承层次

集成并不只限于一个层次。

由一个公共超类派生出来的全部类的集合被称为继承层次(inheritance hierarchy),在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。

Java不支持多继承,有关Java中多继承功能的实现方式,见下一章有关接口的部分。

多态

"is-a"规则的另外一种表述法是置换法则,它代表程序中出现超类对象的任何地方均可以用子类对象置换

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

在Java程序设计语言中,对象变量是多态的

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,而setBonus不是Employee类的方法。

尽管把子类赋值给超类引用变量是没有问题的,但这并不意味着反过来也能够:

Manager m = staff[2]; // Error

若是这样赋值成功了,那么编译器将m当作是一个Manager类,在调用setBonus因为所引用的Employee类并无该方法,从而会发生运行时错误。

理解方法调用

弄清楚如何在对象上应用方法调用很是重要。

好比有一个C类对象xC有一个方法f(args)

如今以调用x.f(args)为例,说明调用过程的详细描述:

  1. 编译器查看对象的声明类型和方法名。C类中可能有多个同名的方法,编译器将列举全部C类中名为f的方法和其超类中属性为public且名为f的方法(超类私有没法访问)。
  2. 接下来,编译器查看调用方法时提供的参数类型。若是存在一个彻底匹配的f,就选择这个方法,这个过程被称为重载解析(overloading resoluton)。这个过程容许类型转换(int转double,Manager转Employee等等)。若是编译器没有找到,或者发现类型转换后,有多个方法与之匹配,就会报告一个错误。
  3. 若是是private方法,static方法、final方法或者构造器,编译器将能够准确知道应该调用哪一个方法,这种称为静态绑定(static binding)。若是不是这些,那调用的方法依赖于隐式参数的实际类型,而且在运行时动态绑定,好比x.f(args)这个例子。
  4. 当程序运行时,而且动态绑定调用时,虚拟机必定调用与x所引用对象的实际类型最合适的那个类的方法。好比x实际是C类,它是D类的子类,若是C类定义了方法f(String),就直接调用;不然将在C类的超类中寻找,以此类推。简单说就是顺着继承层次从下到上的寻找方法。

若是每次调用方法都要深度/广度遍历搜索继承链,时间开销很是大。

所以虚拟机预先为每一个类建立一个方法表(method table),其中列出了全部方法的签名和实际调用的方法,这样一来在真正调用时,只须要查表便可。

若是调用super.f(args),编译器将对隐式参数超类的方法表进行搜索。

以前的Employee类和Manager类的方法表:

Employee:
  getName() -> Employee.getName()
  getSalary() -> Employee.getSalary()
  getHireDay() -> Employee.getHireDay()
  raiseSalary(double) -> Employee.raiseSalary(double)

Manager:
  getName() -> Employee.getName()
  getSalary() -> Manager.getSalary()
  getHireDay() -> Employee.getHireDay()
  raiseSalary(double) -> Employee.raiseSalary(double)
  setBonus(double) -> Manager.setBonus(double)

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

  • 虚拟机提取实际类型的方法表(之因此叫实际类型,是由于Employee e能够引用全部Employee类的子类,因此要肯定实际引用的类型)。
  • 虚拟机搜索定义getSalary签名的类,虚拟机肯定调用哪一个方法。
  • 最后虚拟机调用方法。

注:在覆盖一个方法时,子类方法不能低于超类方法的可见性,特别是超类方法是public,子类覆盖方法时必定声明为public,由于常常会发生这样的错误:在声明子类方法时,由于遗漏public而使编译器把它解释为更严格的访问权限。

阻止继承:final类和方法

有时候,可能但愿阻止人们利用某个类定义子类。

不容许扩展的类被称为 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,而不包括域。

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

强制类型转换

有时候就像将浮点数转换为整数同样,也可能须要将某个类的对象引用转换成另外一个类的对象引用。

对象引用的转换语法与数值表达式的类型转换相似,仅须要一对圆括号将目标类名括起来,并放置在须要转换的对象引用以前就能够了。

Manager boss = (Manager) staff[0];
// 由于以前把boss这个Manager类对象也存在了Employee数组中
// 如今经过强制类型转换回复成Manager类
进行类型转换的惟一缘由:在暂时忽略对象的实际类型以后,使用对象的所有功能。

在Java中,每一个对象变量都属于一个类型,类型描述了这个变量所引用的以及可以引用的对象类型。

将一个值存入变量时,编译器将检查是否容许该操做:

  • 将一个子类的引用赋给一个超类变量,编译器时容许的
  • 可是将一个超类引用赋给一个子类变量,必须进行类型转化,这样才能经过运行时的检查

若是试图在继承链上进行向下的类型转换,并谎报有关对象包含的内容(好比硬要把一个Employee类对象转换成Manager类对象):

Manager boss = (Manager) staff[1]; // Error

运行时,Java运行时系统将报告这个错误(不是在编译阶段),并产生一个ClassCastException异常,若是没有捕获异常,程序将会终止。

因此应该养成一个良好习惯:在进行类型强转以前,先查看一下是否能成功转换,使用instanceof操做符便可:

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

注:若是xnull,则它对任何一个类进行instanceof返回值都是false,它由于没有引用任何对象。

抽象类

位于上层的类一般更具备通用性,甚至可能更加抽象,对于祖先类,咱们一般只把它做为派生其余类的基类,而不做为想使用的特定的实例类,好比Person类对于EmployeeStudent类而言。

因为Person对子类一无所知,可是又想规范他们,一种作法是提供一个方法,而后返回空的值,另外一种就是使用abstract关键字,这样Person就彻底不用实现这个方法了。

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

为了提供程序的清晰度,包含一个或多个抽象方法的类自己必须被声明为抽象的。

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

除了抽象方法外,抽象类还能够包含具体数据和具体方法。

尽管许多人认为,在抽象类中不能包含具体方法,可是仍是建议尽可能把通用的域和方法(无论是否抽象)都放在超类(无论是否抽象)中。

虽然你能够声明一个抽象类的引用变量,可是只能引用非抽象子类的对象,由于抽象类不能被实例化。

在非抽象子类中定义抽象类的方法:

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

尽管Person类中没有具体定义getDescription的具体内容,可是当一个Person类型引用变量p使用p.getDescription()也是没有问题的,由于根据前面的方法调用过程,在运行时,方法的实际寻找是从实际类型开始寻找的,而实际类型都是定义了这个方法的具体内容。

那你可能会问,我能够只在Student类中定义getDescription不就好了么?为何还要在Person去声明?由于若是这样的话,就不能经过p调用getDescription方法了,由于编译器只容许调用在类中声明的方法。

受保护访问

有些时候,人们但愿超类中的某些方法容许被子类访问,或容许子类的方法访问超类中的某个域,而不让其余类访问到。

为此,须要将这些方法或域声明为protected

例如,若是Employee中的hireDay声明为protected,而不是private,则Manager中的方法就能够直接访问它。

不过,Manager中的方法只可以访问Manager对象中的hireDay,而不能访问其余Employee对象中的这个域,这样使得子类只能得到访问受保护域的权利。

对于受保护的域来讲,可是这在必定程度上违背了OOP提倡的数据封装原则,由于若是当一个超类进行了一些修改,就必须通知全部使用这个类的程序员(而不像普通的private域,只能经过开放的方法去访问)。

相比较,受保护的方法更具备实际意义。若是须要限制一个方法的使用,就能够声明为protected,这代表子类获得信任,能够正确地使用这个方法,而其余类(非子类)不行。

这种方法的一个最好示例就是Object类中的clone方法。

概括总结Java控制可见性的4个访问修饰符:

  1. public:对全部类可见
  2. protected:对本包和全部子类可见
  3. private:仅对本类可见
  4. 默认,无修饰符:仅对本包可见

Object:全部类的超类

Obejct类是Java中全部类的始祖,Java中每一个类都是它扩展而来。

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

天然地,可使用Object类型的变量引用任何类型的对象:

Obejct obj = new Employee("Harry Hacker", 35000);

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

Employee e = (Employee) obj;

equals方法

Object类中的 equals方法用于检测一个对象 是否等于另一个对象。

这里的等于指的是判断两个对象是否具备相同的引用

可是在判断两个不肯定是否为null的对象是否相等时,须要使用Objects.equals方法,若是两个都是null,将返回true;若是其中一个为null,另外一个不是,则返回false;若是两个都不为null,则调用a.equals(b)

固然大多数时候Object.equals并不能知足,通常来讲咱们须要比较两个对象的状态是否相等,这个时候须要重写这个方法:

public class Manager extends Employee
{
  ...
  public boolean equals(Object otherObject)
  {
    // 首先要调用超类的equals
    if(!super.equals(otherObejct)) return false;
    Manager other = (Manager) otherObject;
    return bonus == other.bonus;
  }
}

相等测试与继承

在阅读后面的书籍笔记内容以前,首先补充一下getClassinstanceof究竟是什么:

  • obejct.getClass():返回此object的运行时类Class(Java中有一个类叫Class)。好比一个Person变量p,则p.getClass()返回的就是Person这个类的Class对象,Class类提供了不少方法来获取这个类的相关信息
  • obejct instanceof ClassName:用来在运行时指出这个对象是不是这个特定类或者是它的子类的一个实例,好比manager instanceof Employee是返回true

好了,让咱们回到原书吧。

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

有许多程序员喜欢使用instanceof来进行检测:

if(!otherObject instanceof Employee) return false;

这样作不但没有解决otherObject是子类的状况,而且还可能招致一些麻烦。

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

  • 自反性:任何非空引用xx.equals(x)应返回true
  • 对称性:任何引用xyy.equals(x)返回true,则x.equals(y)也应该返回true
  • 传递性:任何引用xyz,若是x.equals(y)返回truey.equals(z)返回true,则x.equals(z)也应该返回true
  • 一致性:若是xy引用对象没有发生变化,反复调用x.equals(y)应该返回一样结果
  • 任意非空引用xx.equals(null)应该返回false

从两个不一样的状况看一下这个问题:

  • 若是子类可以拥有本身的相等概念,则对称性需求将强制采用getClass进行检测
  • 若是由超类决定相等的概念,那么就可使用instanceof进行检测,这样能够在不一样子类的对象之间进行相等的比较

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

  1. 显示参数命名为otherObejct,稍后将它转换成另外一个叫作other的变量
  2. 检测thisotherObject是否因用同一个对象:

    if(this == otherObject) return true;
  3. 检测otherObject是否为null

    if(otherObject == null) return false;
  4. 比较thisotherObject是否属于同一个类

    // 若是equals语义在每一个子类中有改变,就用getClass
    if(getClass() != otherObject.getClass()) return false;
    // 若是子类拥有统一的语义,就用instanceof检测
    if(!(otherObejct instanceof ClassName)) return false;
  5. otherObejct转换为相应的类类型变量:

    ClassName other  = (ClassName) otherObejct;
  6. 开始进行域的比较,使用==比较基本类型域,使用Objects.equals比较对象域

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

若是在子类中从新定义equals,还要在其中包含调用super.equals(other)

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

hashCode方法

散列码(hash code)是由对象导出的一个整数值。

散列码是没有规律的,若是xy是两个不一样的对象,x.hashCode()y.hashCode()基本上不会相同。

对于String类而言,字符串的散列码是由内容导出的。

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

若是从新定义 equals方法,就必须从新定义 hashCode方法,以便用户能够将对象插入到散列表中。

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

例如,Employee类的hashCode方法:

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

不过若是使用null安全的方法Objects.hashCode(...)就更好了,若是参数为null,这个方法返回0。

另外,使用静态方法Double.hashCode(salary)来避免建立Double对象。

还有更好的作法,须要组合多个散列值时,能够调用Objects.hash并提供多个参数。

public int hashCode()
{
  return Obejcts.hash(name, salary, hireDay);
}
equalshashCode的定义必须一致:若是 x.equals(y)返回 true,那么 x.hashCode()就必须与 y.hashCode()具备相同的值。

toString方法

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

绝大多数(但不是所有)的toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。

public String toString()
{
  return getClass().getName()
    + "[name=" + name
    + ",salary=" + salary
    + ",hireDay=" + hireDay
    + "]";
}

toString方法也能够供子类调用。

固然,设计子类的程序员也应该定义本身的toString方法,并将子类域的描述添加进去。

若是超类使用了getClass().getName(),子类只须要调用super.toString()便可。

public class Manager extends Employee
{
  ...
  public String toString()
  {
    return super.toString()
      + "[bonus=" + bonus
      + "]";
  }
}

如今,Manager对象将打印输出以下所示内容:

Manager[name=...,salary=...,hireDay=...][bonus=...]

注意这里在子类中调用的super.toString(),不是在超类Employee中调用的么?为何打印出来的是Manager

由于getClass正如前面所说,获取的是这个对象运行时的类,与在哪一个类中调用无关。

若是任何一个对象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@1a46e30

前缀[I代表是一个整形数组,若是想要获得里面内容的字符串,应该使用Arrays.toString

String s = Arrays.toString(luckyNumbers);
// s [2,3,5,7,11,13]

若是想要打印多维数组,应该使用Arrays.deepToString方法。

强烈建议为自定义的每个类增长 toString方法。

泛型数组列表

在许多程序设计语言中,必须在编译时就肯定整个数组大小。

在Java中,容许运行时肯定数组的大小

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

固然,这段代码并无彻底解决运行时动态更改数组的问题。一旦肯定了大小,想要改变就不容易了。

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

ArrayList是一个采用类型参数(type paraneter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,须要用一对尖括号将类名括起来加在后面,例如ArrayList<Employee>

ArrayList<Employee> staff = new ArrayList<Employee>();
// 两边都是用参数有些繁琐,在Java SE 7中,能够省去右边的类型参数
ArrayList<Employee> staff = new ArrayList<>();

这通常叫作“菱形语法”(<>),能够结合new操做符使用。

若是赋值给一个变量,或传递到某个方法,或者从某个方法返回,编译器会检查这个变量、参数或方法的泛型类型,而后将这个类型放在<>中。

在这个例子中,new ArrayList<>()将赋值给一个类型为ArrayList<Employee>的变量,因此泛型类型为Employee

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

staff.add(new Employee(...));

数组列表管理着对象引用的一个内部数组,最终数组空间有可能被用尽,这时数组列表将会自动建立一个更大的数组,并将全部的对象从较小数组中拷贝到较大数组中。

也能够肯定存储的元素数量,在填充数组前调用ensureCapacity方法:

// 分配一个包含100个对象的内部数组
// 在100次调用add时不用再每次都从新分配空间
staff.ensureCapacity(100);
// 固然也能够经过把初始容量传递给构造器实现
ArrayList<Employee> staff = new ArrayList<>(100);

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

staff.size()

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

访问数组列表元素

数组列表自动扩展容量的便利增长了访问元素语法的复杂程度。

须要使用getset方法实现或改变数组元素的操做,而不是[index]语法格式。

staff.set(i, harry);
Employee e = staff.get(i);

当没有泛型类时,原始的ArrayList类提供的get方法别无选择只能返回Object,所以,get方法的调用者必须对返回值进行类型转换:

Employee e = (Employee) staff.get(i);

固然仍是有一个比较方便的方法来灵活扩展又方便访问:

ArrayList<X> list = new ArrayList<>();
while(...)
{
  x = ...;
  list.add(x);
}
X[] a = new X[list.size()];
// 使用toArray方法把数组元素拷贝到一个数组中
list.toArray(a);

还能够在数组列表的中间插入元素:

int n = staff.size()/2;
staff.add(n, e);

固然也能够删除一个元素:

Employee e = staff.remove(n);

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

for(Employee e : staff)
  do sth with e

对象包装器与自动装箱

有时须要将int这样的基本类型转换为对象,全部基本类型都有一个与之对应的类。

例如,Integer类对应基本类型int,一般这些类称为包装器(wrapper)。

这些对象包装器有很明显的名字:IntegerLongFloatDoubleShortByteCharacterVoidBoolean(前6个类派生于公共超类Number)。

对象包装器类是不可变的,即一旦构造了包装器,就不容许更改包装在其中的值。

同时,对象包装器类仍是final,所以不能定义它们的子类。

有一个颇有用的特性,便于添加int类型的元素到ArrayList<Integer>中。

ArrayList<Integer> list = new ArrayList<>();
list.add(3);
// 这里将自动地变为
list.add(Integer.valueOf(3));

这种变换被称为自动装箱(autoboxing)。

相反地,将一个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中上面的判断是有可能(may)成立的(这也太玄学了),因此解决办法通常是使用equals方法。

还有一些须要强调的:

  • 包装器引用能够为null,因此自动装箱可能会抛出NullPointerException异常
  • 若是条件表达式中混用IntegerDouble类型,Integer值就会拆箱,提高为double,再装箱为Double
  • 装箱和拆箱是编译器承认的,而不是虚拟机,编译器在生成类字节码时,插入必要的方法调用,虚拟机只是执行这些字节码(就至关于一个语法糖吧)。

使用数值对象包装器还有另一个好处,能够将某些基本方法放置在包装器中,好比,将一个数字字符串转换成数值。

int x = Integer.parseInt(s);

参数数量可变的方法

Java SE 5之前的版本中,每一个Java方法都有固定数量的参数,然而如今的版本提供了可变的参数数量调用的方法。

好比printf方法的定义:

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

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

实际上,printf方法接收两个参数,一个是格式字符串,另外一个是Object[]数组,其中保存着全部的参数。

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

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

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

// 找出最大值
public static double max(double... values)
{
  double largest = Double.NEGATIVE_INFINITY;
  for(double v : values) if(v > largest) largest = v;
  return largest;
}

枚举类

public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };
实际上这个声明定义的类型是一个类,它恰好有4个实例。

所以比较两个枚举类型值时,不须要调用equals,直接使用==就能够了。

若是须要的话,能够在枚举类型中添加一些构造器、方法和域,构造器只在构造枚举常量的时候被调用。

public enum Size
{
  SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");

  private String abbreviation;

  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方法,返回一个包含所有枚举值的数组。

Sizep[] values = Size.values();

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

反射

反射是一种功能强大且复杂的机制,使用它的主要人员是工具构造者,而不是应用程序员。

因此这部分先跳过,将会在之后一个专题单独来讲明。

继承的设计技巧

  1. 将公共操做和域放在超类
  2. 不要使用受保护的域
  3. 使用继承实现"is-a"关系
  4. 除非全部继承的方法都有意义,不然不要使用继承
  5. 在覆盖方法时,不要改变预期的行为,不要偏离最初的设计想法
  6. 使用多态,而非类型信息
  7. 不要过多地使用反射

Java继承总结

  • 子类(定义、构造器、方法覆盖)
  • 继承层次
  • 多态
  • 方法调用的过程细节
  • final类和方法
  • 强制类型转换
  • 抽象类
  • protected受保护访问
  • Object全部类的超类
  • equals方法
  • 相等测试与继承
  • hashCode方法
  • toString方法
  • 泛型数组列表
  • 对象包装器与自动装箱
  • 参数数量可变的方法
  • 枚举类
  • 继承设计技巧

我的静态博客:

相关文章
相关标签/搜索