Java核心技术第10版原书笔记(卷一 第5章)

第5章 继承

概要:

1)类、超类和子类

2)参数数量可变的方法

3)Object: 所有类的超类

4)枚举类

5)泛型数组列表

6)反射(反射是指在程序运行期间发现更多的类及其属性的能力。)

7)对象包装器与自动装箱

8)继承的设计技巧

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

5.1 类、超类和子类

5.1.1 定义子类

1.Manager与Employee之间存在着明显的“is-a”(是)关系,每个经理都是一名雇员:“is-a”关系是继承的一个明显特征。

2.关键字:extends

3.Java中所有继承都是公有继承(c++中有私有继承和保护继承)

  • 公有继承(public):公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
  • 私有继承(private):私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
  • 保护继承(protected):保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。

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

5.1.2 覆盖方法

1.子类有时需要重写超类方法并添加自己的特性。

2.子类可以使用super调用超类的方法。

3.子类可以增加域、方法,但绝对不能删除继承的任何域和方法。

5.1.3 子类构造器

1.子类构造器可以使用super调用超类构造器,注意super语句必须是子类构造器第一条语句(写在最上面)。

2.若子类构造器没有显式地调用超类构造器,则自动调用超类默认的构造器(即无参构造器),若超类没有无参构造器而子类构造器又没有显式调用超类的其他构造器,则编译错误。

3.this和super

  • this的两种用途:(1)引用隐式参数; (2)调用当前类的其他构造器
  • super的两种用途:(1)调用超类的方法; (2)调用超类的构造器(作为第一行)

4.什么是多态?一个对象变量可以表示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。
如定义一个超类Employee和一个子类Manager并且都有getSalary()方法,初始化一个Employee数组new Employee[3], 数组第一个元素可以实例化为Manager,第二第三个元素可以实例化为Employee,这就是一种多态,遍历这个数组并实际调用getSalary()方法的过程存在动态绑定现象(员工调用员工的getSalary()经理调用经理的getSalary())。

5.1.4 继承层次

1.由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。

2.在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。一个祖先类可以有多个子孙继承链。

3.Java不支持多继承

5.1.5 多态

1.在Java程序设计语言中,对象变量是多态的。一个Employee变量既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类的对象(例如,Manager、Executive、Secretary等)。 - is-a 与置换法则

2.不能将一个超类的引用赋给子类变量(因为不是所有Employee都是Manager)。反之可以将一个子类数组的引用转换为超类数组的引用而不需要强制类型转换(如将Manager[] 转为Employee[]是合法的)。

    public static strictfp void main(String[] args) {
        Manager[] managers = new Manager[10];
        Employee[] employees = managers;
        // 上述实际上managers和employees引用同一个数组,而下面赋值竟然是Employee信息
        // 这里我们似乎把一个普通员工擅自归入了经理行列中,这搅乱了相邻存储空间
        // 下面这行编译正常,但运行时报错:java.lang.ArrayStoreException
        employees[0] = new Employee("Jalen", 12.0, 1992, 12, 22);
        managers[0].setBonus(1000);  // 只有经理拥有奖金方法
        // 为了确保不发生这类错误,所有数组都要牢记创建它们的元素类型,并负责监督仅将类型兼容的引用存储到数组中。
        // 例如,使用new managers[10]创建的数组是一个经理数组。
    }

5.1.6 理解方法调用

1.编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法。例如,可能存在方法f(int)和方法f(String)。编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法(超类的私有方法不可访问)。 至此,编译器已获得所有可能被调用的候选方法。

2.接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析(overloading resolution)。例如,对于调用x.f(“Hello”)来说,编译器将会挑选f(String),而不是f(int)。由于允许类型转换(int可以转换成double,Manager可以转换成Employee,等等),所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。 至此,编译器已获得需要调用的方法名字和参数类型。

3.如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定

4.当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型是D,它是C类的子类。如果D类定义了方法f(String),就直接调用它;否则,将在D类的超类中寻找f(String),以此类推。

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

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

5.1.7 阻止继承:final类和方法

1.不允许扩展的类被称为final类。即定义类时使用了final修饰符。

2.类中的方法如果被声明为final,则子类不能覆盖这个方法。(注:final类中所有方法自动地称为final方法)

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

4.内联(inlining)优化与弊端。

5.1.8 强制类型转换

1.将某个类的对象引用转换成另外一个类的对象引用。对象引用的转换语法与数值表达式的类型转换类似,仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。(注意类型检查)

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

2.通过类型转换调整对象的类型不是一种好的做法,通常我们可以检查一下超类的设计是否合理并重新晚上一下超类,应尽量少用类型转换和instanceof运算符。

3.类型转换失败会抛出ClassCastException。

5.1.9 抽象类

1.祖先类更加通用,人们只将他作为派生其他类的基类,而不作为想使用的特定的实例类。

2.包含一个或多个抽象方法的类 本身必须被声明为抽象的,带有abstract修饰符。

3.抽象类还可以包含具体数据和具体方法。

4.扩展抽象类可以有两种选择。一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。

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

6.抽象类不能被实例化。

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

8.在接口interface中将会看到更多的抽象方法。

5.1.10 受保护访问

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

2.谨慎使用protected属性。

3.Object类的clone方法。

4.Java中的受保护部分对所有子类及同一个包中的所有其他类都可见。

5.2 Object:所有类的超类

1.在Java中,只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。

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

5.2.1 equals方法

Object类中的equals方法用于检测一个对象是否等于另外一个对象。在Object类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。从这点上看,将其作为默认操作也是合乎情理的。然而,对于多数类来说,这种判断并没有什么意义。例如,采用这种方式比较两个PrintStream对象是否相等就完全没有意义。然而,经常需要检测两个对象状态的相等性,如果两个对象的状态相等,就认为这两个对象是相等的。

5.2.2 相等测试与继承

1.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。

2.无论集合采用何种方式实现,都需要拥有对任意两个集合进行比较的功能。

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

3.在标准Java库中包含150多个equals方法的实现,包括使用instanceof检测、调用getClass检测、捕获ClassCastException或者什么也不做。可以查看java.sql.Timestamp类的API文档,在这里实现人员不无尴尬地指出,他们使自己陷入了困境。Timestamp类继承自java.util.Date,而后者的equals方法使用了一个instanceof测试,这样一来就无法覆盖实现equals使之同时做到对称且正确。

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

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

5.2.3 hashCode方法

1.散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。如果x和y是两个不同的对象,x.hashCode()与y.hashCode()基本上不会相同。

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

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

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

5.Equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。

5.2.4 toString方法

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

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

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

4.令人烦恼的是,数组继承了object类的toString方法,数组类型将按照旧的格式打印。例如生成字符串“[I@1a46e30”(前缀[I表明是一个整型数组)。修正的方式是调用静态方法Arrays.toString。

5.toString方法是一种非常有用的调试工具。在标准类库中,许多类都定义了toString方法,以便用户能够获得一些有关对象状态的必要信息。

6.强烈建议为自定义的每一个类增加toString方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。

泛型数组列表

1.Java中允许在运行时确定数组的大小。即ArrayList类。它在添加和删除元素时,具有自动调节数组容量的功能。ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,例如,ArrayList<Employee>

ArrayList<Employee> staff = new ArrayList<>; // Java7开始可以省去右边的类型参数,被称为菱形语法。

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

3.在Java的老版本中,程序员使用Vector类实现动态数组。不过,ArrayList类更加有效,没有任何理由一定要使用Vector类。

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

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

6.一旦能够确认数组列表的大小不再发生变化,就可以调用trimToSize方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素时,再调用trimToSize。

7.Java没有运算符重载(运算符重载(英語:operator overloading)是多态的一种。这里,运算符(比如+,=或==)被当作多态函数,它们的行为随着其参数类型的不同而不同。运算符并不一定总是符号。)

5.3.1 访问数组列表的元素

1.很遗憾,天下没有免费的午餐。数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。其原因是ArrayList类并不是Java程序设计语言的一部分;它只是一个由某些人编写且被放在标准库中的一个实用类。 使用get和set方法实现访问或改变数组元素的操作,而不使用人们喜爱的[]语法格式。

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

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

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

与遗留代码交互

5.4 对象包装器与自动装箱

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

2.由于每个值分别包装在对象中,所以ArrayList<Integer>的效率远远低于int[]数组。因此,应该用它构造小型集合,其原因是此时程序员操作的方便性要比执行效率更加重要。

3.自动装箱(大家可能认为自动打包(autowrapping)更加合适,而“装箱(boxing)”这个词源自于C#。):

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

4.自动拆箱

int n = list.get(i);  //翻译成 int n = list.get(i).intValue();

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

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

7.最后强调一下,装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。 使用数值对象包装器还有另外一个好处。Java设计者发现,可以将某些基本方法放置在包装器中,例如,将一个数字字符串转换成数值。

5.5 参数数量可变的方法

1.允许将一个数组传递给可变参数方法的最后一个参数。例如:

2.printf的参数就是可变的。

3.可以将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法,而不会破坏任何已经存在的代码。例如,将main函数的参数Stringp[] args改为String… args完全没有问题。

5.6 枚举类

如同Class类一样,鉴于简化的考虑,Enum类省略了一个类型参数。例如,实际上,应该将枚举类型Size扩展为Enum<Size>。类型参数在compareTo方法中使用。

5.7 反射

1.反射库(reflection library)提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵Java代码的程序。这项功能被大量地应用于JavaBeans中,它是Java组件的体系结构。使用反射,Java可以支持Visual Basic用户习惯使用的工具。特别是在设计或运行中添加新类时,能够快速地应用开发工具动态地查询新添加类的能力。

2.能够分析类能力的程序称为反射(reflective)。反射机制的功能极其强大,在下面可以看到,反射机制可以用来:

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

3.反射是一种功能强大且复杂的机制。使用它的主要人员是工具构造者,而不是应用程序员。如果仅对设计应用程序感兴趣,而对构造工具不感兴趣,可以跳过该方面知识。

5.7.1 Class类

1.在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。 可以通过专门的Java类访问这些信息。保存这些信息的类被称为Class。

  • e.getClass().getName() // get class name
  • Class.forName(“java.util.Random”); //获取类名对应的class对象
  • e.getClass().newInstance(); // 动态地创建一个类的实例
  • Class.forName(“java.util.Random”).newInstance(); //同上

newInstance方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认的构造器,就会抛出一个异常。

2.在启动时,包含main方法的类被加载。它会加载所有需要的类。这些被加载的类又要加载它们需要的类,以此类推。

3.Class类实际上是一个泛型类。

4.鉴于历史原因,getName方法在应用于数组类型的时候会返回一个很奇怪的名字:

  • Double[].class.getName()返回“[Ljava.lang.Double;”。
  • int[].class.getName()返回“[I”。

5.7.2 捕获异常

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

2.异常有两种类型:未检查异常和已检查异常。对于已检查异常,编译器将会检查是否提供了处理器。然而,有很多常见的异常,例如,访问null引用,都属于未检查异常。编译器不会查看是否为这些错误提供了处理器。毕竟,应该精心地编写代码来避免这些错误的发生,而不要将精力花在编写异常处理器上。

3.try{…}catch (Exception e){ e.printStackTrace();} 捕获异常以及打印栈的轨迹。

5.7.3 利用反射分析类的能力

1.反射机制最重要的内容——检查类的结构。

2.在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。这三个类都有一个叫做getName的方法,用来返回项目的名称。Field类有一个getType方法,用来返回描述域所属类型的Class对象。Method和Constructor类有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。这三个类还有一个叫做getModifiers的方法,它将返回一个整型数值,用不同的位开关描述public和static这样的修饰符使用状况。另外,还可以利用java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的整型数值。例如,可以使用Modifier类中的isPublic、isPrivate或isFinal判断方法或构造器是否是public、private或final。我们需要做的全部工作就是调用Modifier类的相应方法,并对返回的整型数值进行分析,另外,还可以利用Modifier.toString方法将修饰符打印出来。

3.Class类中的getFields、getMethods和getConstructors方法将分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。Class类的getDeclareFields、getDeclareMethods和getDeclaredConstructors方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。

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

1.反射机制的默认行为受限于Java的访问控制。然而,如果一个Java程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用Field、Method或Constructor对象的setAccessible方法。setAccessible方法是AccessibleObject类中的一个方法,它是Field、Method和Constructor类的公共超类。这个特性是为调试、持久存储和相似机制提供的。

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

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

2.一Java数组会记住每个元素的类型,即创建数组时new表达式中使用的元素类型。

5.7.6 调用任意方法

1.反射机制允许你调用任意方法。

2.建议仅在必要的时候才使用Method对象,而最好使用接口以及Java SE 8中的lambda表达式。特别要重申:建议Java开发者不要使用Method对象的回调功能。使用接口进行回调会使得代码的执行速度更快,更易于维护。

5.8 继承的设计技巧

  • 1.将公共操作和域放在超类
  • 2.不要使用受保护的域
  • 3.使用继承实现“is-a”关系
  • 4.除非所有继承的方法都有意义,否则不要使用继承
  • 5.在覆盖方法时,不要改变预期的行为
  • 6.使用多态,而非类型信息
  • 7.不要过多地使用反射

1.有些程序员认为,将大多数的实例域定义为protected是一个不错的主意,只有这样,子类才能够在需要的时候直接访问它们。然而,protected机制并不能够带来更好的保护,其原因主要有两点。第一,子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性。第二,在Java程序设计语言中,在同一个包中的所有类都可以访问proteced域,而不管它是否为这个类的子类。 不过,protected方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值