转载

Java核心技术笔记 继承

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

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

类、超类和子类

定义子类

关键字 extend 表示继承。

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

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

已存在的类称为 超类 (superclass)、 基类 (base class)或 父类 (parent class);

新类称为 子类 (subclass)、 派生类 (derived class)或 孩子类 (child class)。

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

所以在扩展超类定义子类时,仅需要指出子类与超类的不同之处。

覆盖方法

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

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

这里由于 Manager 类的 getSalary 方法并 不能直接访问超类的私有域

这是因为尽管子类拥有超类的所有域,但是子类没法直接获取到超类的私有部分,因为超类的私有部分只有超类自己才能够访问。而子类想要获取到私有域的内容,只能通过超类共有的接口。

而这里 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个访问修饰符:

public
protected
private

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开始技术。

反射

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

所以这部分先跳过,将会在以后一个专题单独来说明。

继承的设计技巧

"is-a"

Java继承总结

protected
Object
equals
hashCode
toString

个人静态博客:

  • 气泡的前端日记: https://rheabubbles.github.io
原文  https://segmentfault.com/a/1190000018068208
正文到此结束
Loading...