JAVA学习笔记——面向对象编程:继承2

本文针对继承方面的一些零散知识点进行梳理,具体继承相关的知识,请参考这篇文章《JAVA学习笔记——面向对象编程:继承1》。

目录

  • 泛型数组列表
    • 访问数组列表元素
    • 类型化与原始数组列表的兼容性
    • 对象包装器与自动装箱
    • 可变参数的方法
  • 继承的设计技巧

泛型数组列表

ArrayList 是一个采用类型参数 (type parameter) 的泛型类 (generic class)。它使用起来有点像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。

为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,例如,ArrayList。Java SE 7中,可以省去右边的类型参数:ArrayList staff = new ArrayList<>();。这被称为“ 菱形” 语法,因为空尖括号 <> 就像是一个菱形,可以结合 new 操作符使用菱形语法,编译器会检查新值是什么。

// 定义一个泛型数组
ArrayList<Employee> staff = new ArrayList<>();
// 添加一个数组元素
staff.add(new Employee("Harry Hacker", ...));
// 预分配数组空间
staff.ensureCapacity(lOO);
// 或者直接将初始容量传递给 ArrayList 构造器
ArrayList<Employee> staff = new ArrayList<>(100);
// 返回当前数组元素个数
int length = staff.size();

访问数组列表元素

// 得到第 i 个元素
staff.get(i); // i < staff.size()
// 将第 i 个元素置为 x
staff.set(i, x); // i < staff.size()
// 在位置 i 插入一个元素 x
staff.add(i, x); // i < staff.size()
// for 循环遍历数组
for (Employee e: staff){
	// do something with e
}

注意:预先分配数组空间,表示在内存中留出了这些空间用于存放 ArrayList 数组,但未存入数据时,实际占用的空间仍然为 0。因此无论是 getset 方法,引索值 i 都应该小于 staff.size(),添加元素使用 add 方法,否则会报错。

ensureCapacity 方法保证了数组存放元素数量的潜力,在添加元素时,若没有超过预定值,则不必重新创建数组,实际占用大小仍然由当前元素个数决定。事实上,当存入数组的元素个数超过预设值后,数组仍能自动拓展,并生成一个新数组,将元素都存放进去。

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

有些代码中可能用到了原始的数组列表 ArrayList,没有定义后面的类型化参数,在调用这类代码时,需要关注参数的类型和方法内部可能产生的错误。以下方法为例:

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

在调用 update 方法时,我们可能会直接将 staff 数组列表作为参数传入方法,事实上这样的调用方法可能会产生一定错误。staff 是我们定义的 Employee 类型的数组列表,而在 update 方法内部,添加的数组元素不一定是 Employee 类型,由此就会抛出异常。

相反地,将一个原始参数列表 ArrayList 赋给一个类型化参数列表 ArrayList,会得到一个警告(子类变量不能引用父类对象,需要进行类型转换)。若给前者进行强制类型转换,有可能会获得另一个警告(超类对象不能转换成子类对象)。

在这种情形下,不必做什么。只要在与遗留的代码进行交叉操作时,研究一下编泽器的警告性提示,并确保这些警告不会造成太严重的后果就行了。一旦能确保不会造成严重的后果,可以用 @SuppressWamings("unchecked") 标注来标记这个变量能够接受类型转换即可。

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

相关方法

  • java.util.ArrayList
    • ArrayList:构造一个空数组列表。
    • ArrayList(int initialCapacity) :用指定容量构造一个空数组列表。
    • .add(E obj):在数组列表的尾端添加一个元素。永远返回 true。
    • .add(int index, E obj):向后移动元素,以便插入元素。
    • .size():返回存储在数组列表中的当前元素数量,这个值将小于或等于数组列表的容量。
    • .ensureCapacity(int capacity):确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素。
    • .trimToSize():将数组列表的存储容量削减到当前尺寸。
    • .set(int index, E obj):设置数组列表指定位置的元素值,这个操作将覆盖这个位置的原有内容。
    • .get(int index):获得指定位置的元素值。
    • .remove(int index):删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回。

对象包装器与自动装箱

所有的基本类型都有一个与之对应的类。例如,Integer 类对应基本类型 int。通常,这些类称为包装器 (wrapper) 这些对象包装器类拥有很明显的名字:IntegerLongFloatDoubleShortByteCharacterVoidBoolean(前6 个类派生于公共的超类 Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是 final,因此不能定义它们的子类。

假如我们希望定义一个整型数组列表,而类型参数不允许是基本数据类型,这是就可以用到包装器类 Integer,我们可以申请一个 Integer 数组列表:ArrayList list = new ArrayList<>()

包装器类还有一个特性——自动装箱和自动拆箱。有了这个特性,我们可以很方便地在 Integer 数组列表中加入元素:list.add(3);。此时,编译器会自动插入一段代码,最后传给 Java 虚拟机的代码为 list.add(Integer.valueOf(3));,这就是所谓的 “自动装箱”。同理,“自动拆箱” 即为自动装箱的反过程,在必要的时候,编译器将自动执行。

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

虽然有自动装箱和拆箱,但上例中 a == b 通常不会成立,因为自动装箱也有一定规范(附下),而 == 运算符对包装器类是用来比较两个对象是否为同一个对象。若定义为 Integer a = 100; Integer b = 100;,100 会被包装到同一个对象,则条件判断是成立的。

自动装箱规范要求 boolean、byte、char 127, 介于 -128 ~ 127 之间的 short 和 int 被包装到固定的对象中。

其他说明

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

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

相关方法

  • java.lang.Integer
    • .intValue():以 int 的形式返回 Integer 对象的值(在 Number 类中覆盖了 intValue 方法)。
    • .toString(int i):以一个新 String 对象的形式返回给定数值 i 的十进制表示。
    • .toString(int i, int radix):返回数值 i 的基于给定 radix 参数进制的表示。
    • .parselnt(String s):返回字符串 s 表示的整型数值,给定字符串表示的是十进制的整数。
    • .parseInt(String s, int radix):返回字符串 s 表示的整型数值基于给定 radix 参数进制的整数 。
    • .valueOf(String s):返回用 s 表示的整型数值进行初始化后的一个新 Integer 对象,给定字符串表示的是十进制的整数。
    • .valueOf(String s, int radix):返回用 s 表示的整型数值进行初始化后的一个新 Integer 对象,给定字符串表示的是基于给定 radix 参数进制的整数 。
  • java.text.NumberFormat
    • .parse(String s):返回数字值,假设给定的 String 表示了一个数值。

可变参数的方法

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

// 自定义可变参数的方法
public static double max (double... values)
{
	double largest = Double.NEGATIVE_INFINITY;
	for (double v : values) 
		if (v > largest) largest = v;
	return largest;
}

这里的省略号 ... 是 Java 代码的一部分,它表明这个方法可以接收任意数量的对象(除 fmt 参数之外)。实际上,printf方法接收两个参数,一个是格式字符串,另一个是 Object[] 数组,其中保存着所有的参数(如果调用者提供的是整型数组或者其他基本类型的值,自动装箱功能将把它们转换成对象)。

同样,我们也可以自定义可变参数的方法,但需要注意:将可变参数放在参数列表的最后一个,即保证可变参数后不再传入其他参数。

继承的设计技巧

  1. 将公共操作和域放在超类
    这就是为什么将姓名域放在 Person 类中,而没有将它放在 EmployeeStudent 类中的原因。

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

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

  3. 使用继承实现 “is-a” 关系

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

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

  6. 使用多态, 而非类型信息
    使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。

  7. 不要过多地使用反射
    反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。


参考资料

  1. 《Java核心技术 卷1 基础知识》

你可能感兴趣的:(JAVA学习笔记,java,编程语言)