利用继承(inheritance),人们可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求。这是Java程序设计中的一项核心技术。“is-a”关系是继承的一个明显特征

类、超类和子类

1、定义子类

extends:

​ 关键字extends表明正在构造的新类派生于一个已存在的类。已存在的类称为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。超类和子类是Java程序员最常用的两个术语,而了解其他语言的程序员可能更加偏爱使用父类和孩子类,这些都是继承时使用的术语。

TIP:

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

在Java中,所有的继承都是公有继承,而没有C++中的私有继承和保护继承。

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

2、覆盖方法

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

在Java中使用关键字super调用超类的方法

TIP:

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

3、子类构造器

通过super实现对超类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。

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

TIP:

super关键字也有两个用途:一是调用超类的方法,二是调用超类的构造器。调用构造器的语句只能作为另一个构造器的第一条语句出现。

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

4、继承层次

继承并不仅限于一个层次。由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy),在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。一个祖先类可以拥有多个子孙继承链。

tip:

Java不支持多继承

5、多态

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

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

tip:

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

所有数组都要牢记创建它们的元素类型,并负责监督仅将类型兼容的引用存储到数组中。

6、理解方法调用

静态绑定(static binding)动态绑定(dynamic binding)两种调用方式。

调用x.f(args),x是类C的对象。

调用过程详细描述:

  1. 编译器查看对象的声明类型和方法名。一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法。(编译器获得了所有可能被调用的候选方法)
  2. 编译器查看调用方法时提供的参数类型。所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析(overloadingresolution)。(编译器已获得需要调用的方法名字和参数类型。)
  3. 如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,这种调用方式称为静态绑定(static binding)
  4. 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。先在实际类型中寻找f方法,如果没有则在其超类中寻找。

解析过程:

  • 首先,虚拟机提取x的实际类型的方法表。
  • 接下来,虚拟机搜索定义getSalary签名的类。此时,虚拟机已经知道应该调用哪个方法。
  • 最后,虚拟机调用方法。

tip:

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

方法的名字和参数列表称为方法的签名

每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。

7、阻止继承:final类和方法

  • 不允许扩展的类被称为final类。

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

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

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

  • 如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程为称为内联(inlining)。

8、强制类型转换

  • 对象引用的转换语法与数值表达式的类型转换类似,仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。Teacher t1=(Teacher)Student;

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

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

tip:

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

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

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

9、抽象类

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

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

  • 建议尽量将通用的域和方法(不管是否是抽象的)放在超类(不管是否是抽象类)中。

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

抽象类不能被实例化。如果将一个类声明为abstract,就不能创建这个类的对象。但可以创建一个具体子类的对象。需要注意,可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。

new People(1,"tom");//people是抽象类无法实例化
People people = new Student(1, "tom", "02");//可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。

10、受保护访问

  • 最好将类中的域标记为private,而方法标记为public。

  • 在实际应用中,要谨慎使用protected属性。

  • 受保护的方法更具有实际意义。这种方法的一个最好的示例就是Object类中的clone方法

tip:

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

Object:所有类的超类

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

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

在Java中,只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。

1、 equals方法

Object类中的equals方法用于检测一个对象是否等于另外一个对象。在Object类中,这个方法将判断两个对象是否具有相同的引用。

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。

3、hashCode方法

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

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

字符串的散列码是由内容导出的

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

4、toString方法

用于返回表示对象值的字符串。


泛型数组列表

Java允许在运行时确定数组的大小。

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

ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。

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

tip:

  • 如果调用add且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。

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

  • 把初始容量传递给ArrayList构造器:ArrayList<String> arrayList=new ArrayList<>(100);

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

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

  • 一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素时,再调用trimToSize

1、访问数组列表元素

  • 数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。其原因是ArrayList类并不是Java程序设计语言的一部分;它只是一个由某些人编写且被放在标准库中的一个实用类

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

  • 使用toArray方法将数组元素拷贝到一个数组中。

  • 在数组列表的中间插入元素,使用带索引参数的add方法。为了插入一个新元素,位于n之后的所有元素都要向后移动一个位置。如果插入新元素后,数组列表的大小超过了容量,数组列表就会被重新分配存储空间。

  • 使用remove方法从数据列表删除元素。位于这个位置之后的所有元素都向前移动一个位置,并且数组的大小减1。

arrayList.set(0,"tom2");
arrayList.get(0);
b.toArray(a)//(b会直接覆盖掉a中原有的元素)
list.add(list.size() / 2, "e");
list.remove(0);

tip:

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

对数组实施插入和删除元素的操作其效率比较低。对于小型数组来说,这一点不必担心。但如果数组存储的元素数比较多,又经常需要在中间位置插入、删除元素,就应该考虑使用链表了


对象包装器与自动装箱

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

  • 对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值(类中没有能够改变类对象的方法)。
  • 同时,对象包装器类还是final,因此不能定义它们的子类。

自动装箱(autoboxing):自动将基本数据类型转换为包装器类型

自动拆箱:自动将包装器类型转换为基本数据类型

tip:

装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。(xxx代表对应的基本数据类型)。

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

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

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

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


参数数量可变的方法

枚举类

7、


Java      Java

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!