Github代码链接: https://github.com/deyou123/corejava.git
第五章 继承
5.1 类、超类、子类
- 超类、基类、父类
- 子类、派生类、孩子类
5.1.2 覆盖方法
- 子类不能直接访问父类的 私有域
- 访问父类的方法: super.funciont();
5.1.3 子类构造器
- super(), 调用父类构造器
- 如果子类的构造器没有显式的调用父类构造器,则将自动地调用父类默认构造器;如果父类没有不带参数的构造器,将会报错
多态:一个对象变量可以指示多种实际类型的现象。
绑定: 在运行时能够自动的选择调用哪个方法的现象。
5.1.4 继承层次
继承层次: 有一个公共父类派生出来的的所有类的集合。
继承链: 从某个特定的类到其祖先的路径被称为该类的继承链。
- 一个祖先类可以拥有多个子孙继承链。
5.1.5 多态
- Java中,对象变量是多态的。
- 一个父类的变量,既可以引用父类的对象,也可以引用任何一个子类的对象。
- 父类对象的变量不能使用子类的方法;
- 反过来,不能将父类的引用赋值给子类变量;
警告
- 子类数组的引用可以转换成父类数组的引用。
5.1.6 理解方法调用
x.f(args),隐式参数x声明为类C的一个对象。
1) 编译器查看对象的声明类型和方法名。
- 有多个名字为f,但参数类型不一样的方法。
- 编译器会一一列举所有C类中名为f的方法和其父类中访问属性为public且名为f的方法。(父类的私有方法不可访问)
2)编译器将查看调用方法时提供的参数类型。
- 如果在所有名为f的方法中存在一个与提供参数类型完全匹配,就选择他。 这叫做重载解析
- 如果找不到就报错。
3)静态绑定: 如果是 private方法、staic方法、final方法或者构造器,那么编译器将可以准确的知道应该调用哪个方法。
动态绑定:调用方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
4)当程序运行时,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。
- x是子类,那就用子类的方法,否则到父类去找
tips:每次调用方法都要进行搜索,时间开销大。
- 虚拟机预先为每一个类创建了一个 方法表,其中列出了所有方法的 签名和实际调用的方法。
- 真正调用方法的时候,查看这个表就行了。
动态绑定的特性:无需对现存的代码进行修改,就可以对程序进行扩展。
5.1.7 阻止继承: final类和方法
有时希望阻止人们利用某个类定义子类。
- 不允许扩展类被成为final类:在定义类的时候用了final修饰符
- 类中特定的方法也可以被声明为final;子类不能广覆盖这个方法:确保他们不会在子类中改变语义。
内联: 如果一个方法没有被覆盖并且很短,编译器就能对他进行优化处理。
5.1.8 强制类型转换
- 有时可能需要将某个类的对象引用转换成另一个类对象引用。
- 需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。
- 原因: 在暂时忽视对象的实际类型之后,使用对象的全部功能。(也就是使用子类扩展的功能)
- object1 instanceof object2: Object1能否转成Object2
结论:
- 只能在继承层次内进行类型转换
- 在将父类转换成子类之前,应该使用instanceof进行检查
- 应该尽量少使用类型转换和instanceof运算符
5.1.9 抽象类
- 抽象类中不能包含具体方法。 建议尽量将通用的域和方法放在超类中。
- 类计时不含抽象方法,也可以将类声明为抽象类。
- 抽象类不能被实例化
5.1.10 protected
- 子类可以访问protected域
- 受保护的方法更具有实际意义。
归纳:
1) private:仅对本类可见
2) public: 对所有类可见。
3) protected: 对本包和所有子类可见(与c++不同)
4) 默认: 对本包可见
5.2 Object:所有类的超类
5.2.1 equals方法
public boolean equals(Object otherObject)
- this == otherObj
- otherObj != null
- getClass() == otherObj.getClass()
- 域相等
先调用父类equals方法,再比较子类中的实例域
5.2.2 相等测试与继承
equals方法具有的特性:
1)自反性: x.equals(x) = true
2)对称性:如果x.equals(y),则y.equals(x)
3) 传递性:如果x.equals(y), y.equals(z),则x.equals(z)
4) 一致性:如果x和y引用的对象不发生变化,那么x.equals(y)应该返回同样的结果。
5) 对域任意非空引用x, x.equals(null) = false
instanceof的不完美
编写一个完美的equals方法的建议:
1)显式参数命名为 otherObject,稍后需要换成other的变量。
2) 检测this与otherObject是否引用同一个对象:
if(this == otherObject) reutrn ture;
3)检测otherObject是否为nul,
if(otherObject == null)return false;
4)比较this与otherObject是否属于同一个类。
if(getClass(0 != otherObject.getClass()) return false;
如果所有的子类都拥有统一的语义,就用instanceof检测:
if(!(otherObject.instanceof ClassName))return false;
- 将otherObject转换成相应的类类型变量:
ClassName other = (ClassName) otherObject
6)开始对所有需要比较的域进行比较, == 比较基本类型,equals比较对象域。
return field1 = other.field1 && Objects.equals(field2, other.field2)
&& ...;
5.2.3 hashCode
散列码:由对象导出的一个整型值。
- 没有规律
- 不同的对象, x.hashCode() 和 y.hashCode()基本上不会相同。
- hashCode定义在Object中,每个对象都有一个默认的散列码,值为对象的存储地址。
String类计算散列码:
int hash = 0;
for(int i =0; i < length(); i++)
hash = 31 * hash + charAt(i);
- hashCode应该返回一个整型数值,并合理的组合实例域的散列码,以便让哥哥不同的对象产生的散列码更加均匀。
ex:
public class Employee{
public int hashCode(){
return 7* name.hashCOde() + 11 * new Double(salary).hashCode() +13* hireDay.hashCode();}
}
更好的方法:
- 使用null安全的 Objects.hashCode
- 使用静态方法 Double.hashCode来避免创建Double对象
public int hashCode(){
return 7*Objects.hashCode(name)
+ 11 * Double.hashCode(salary)
+ 13 * Objects.hashCode(hireDay);
}
更好的方法,使用Objects.hash组合多个散列值
public int hashCode(){
return Objects.hash(name,salary,hireDay);
}
Equals与hashCode的定义必须一致: 如果x.equals(y) = true,那么 x.hashCode() = y.hashCode();
5.2.4 toString
用于返回表示对象值的字符串。
- 绝大多数toString遵循这样的格式: 类的名字,随后是一对方括号括起来的域值。
public String toString(){
return "Employee[name=" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}
- 通过getClass.getName()获得类名和字符串
public String toString(){
return getClass().getName()
+ "[name=" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}
- 遵循子类调用
public String toString(){
return super.toString()
+ "[bonus=" + bonus
+ "]";
}
- Object类定义了toString方法,用来打印输出对象所属的类名和散列码。
数组toString
- Arrays.toString(array)
- 多维数组: Arrays.deepToString();
5.3 泛型数组列表
运行时确定数组大小
ArrayList是一个采用 类型参数的泛型类。
- 如果调用add且内部数组已经满了,数组列表就将自动的创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
- 预估除数组可能存储的元素数量: staff.ensureCapacity(100)
- ArrayList staff new ArrayLIst(100);
- 能够确认数组列表的大小不再变化,就可以调用trimToSize方法,将存储区域的大小调整为当前元素数量所需要的存储空间数目,垃圾回收器将回收多余的存储空间。
5.3.1 访问数组列表元素
- get、set方法
- 没有泛型类时,ArrayList返回Object,需要进行类型转换。
- ArrayList.toArray(数组),可以灵活的扩展数组,并且转换成数组。
- 向数组间插入元素:带索引参数的add方法add(n,e)
为了插入一个新元素,位于n之后的所有元素都要向后移动一个位置;如果超过了容量,会重新分配存储空间。 - 删除一个元素 remove(n)
所有元素都要向前移动一个位置。 - 如果需要再中间位置插入、删除元素,就应该考虑使用链表。
- for each循环遍历数组列表
ArrayList
void set(int index, E obj):设置数组列表指定位置的元素值,覆盖原有类容
E get(int index): 获取指定位置的元素值
void add(int index, E obj): 插入元素
E remove(int index): 删除
5.3.2 类型化与原始数组列表的兼容性
5.4 对象包装器与自动装箱
包装器: 所有基本类型都有一个与之对应的类。 Integer-》int
ex: Integer、Long、Lloat、Double、Short、Byte、Character、Void、Boolean。
- 对象包装器类是不可变的,不允许更改包装在其中的值。
- 对象包装器是final,不能定义他们的子类。
自动装箱: 3 自动变成 Integer.valueOf(3)
自动拆箱: 将一个Integer对象赋给一个int值时:int n = list.get(i);
== 检测的是对象是否指向同一个引用地址。
- 包装器类引用可以为null
- 混合使用Integer和Double,Integer值拆箱提升为double
- 装箱和拆箱时编译器认可的,而不是虚拟机。
5.5 参数数量可变的方法
变参方法:现在版本提供了可以用 可变的参数数量调用的方法
ex:printf
public class PrintStream{
public PrintStream printf(String fnt, Object... args){
return format(fnt, args);}
}
省略号 … 是java代码的一部分,他表示这个方法可以接受任意数量的对象。
main:
public static void main(String... args);
5.6 枚举类
- 比较枚举类型的值时,永远不要用equals,而直接使用“==”
- 如果需要的话,可以在枚举类型中添加构造器、方法和域
public enum Size{
SMALL("S"),MEDIUM("M"),LARGE("L"),EXREA_LARGE("XL");
private String abbreviation;
private Size(String abbreviation){this.abbreviation = abbreviation;}
public String getAbbreviation(){return abbreviation;}
}
- 每个枚举类型都有一个静态的values方法,返回一个包含全部枚举值的数组。
- Size[] values = Size.values().
5.7 反射
- 反射库 提供了一个非常丰富且精心设计的工具库,以便编写能够动态操纵Java代码的程序。
- 在设计或运行中添加新类时,能够快速地应用开发工具动态的查询新添加类地能力。
- 反射: 能够分析类能力的程序。
反射机制可以用来:
- 在运行时分析类地能力。
- 在运行时查看对象,toString
- 实现通用地数组操作代码。
- 利用Method对象,这个对象很想C++中地函数指针。
5.7.1 Class类
- 在程序运行期间,Java运行时系统始终为所有对象维护一个被称为运行时地类型标识。
- 这个信息跟踪者每个对象所属的类;虚拟机利用运行时类型信息选择相应的方法执行。
- Object类中的getClass()方法将会返回一个Class类型的实例。
用法:
- 最常用的Class方法是getName(): 将返回类的名字。
- 如果类在一个包里,包的名字也作为类名的一部分:java.util.Random
- 还可以调用静态方法forName获得类名对应的Class对象。
String className = "java.util.Random";
Class c1 = Class.forName(className);
- 如果类名保存在字符串中,并可在运行中改变,就可以使用这个方法。
- 只有是className是类名或接口名时才能够执行。
注释:
-
Class类实际上是一个泛型类
-
getName应用于数组类型的时候会返回一个很奇怪的名字。
-
虚拟机为每个类型管理一个Class对象,可以用==运算符实现两个类对象的比较操作。
if(e.getClass()==Employee.class)… -
newInstance() 可以用来动态的创建一个类的实例。调用默认构造器初始化对象(没有则异常)。
ex:e.getClass().newInstance(); 创建一个与e相同类型的实例。
5.7.2 捕获异常
两种:未检查异常 和 已检查异常。
已检查异常:编译器将会检查是否提供了处理器。
未检查异常:常见的异常: 访问null引用等,编译器不会查看是否为这些错误提供了处理器。
- 并不是所有的错误都是可以避免的。
- 如果竭尽全力还是发生了异常,比那一起就要求提供一个处理器。
- try-catch: 如果异常,将跳过try块中的剩余代码,程序直接进入catich子句; 如果try没有异常,就不会执行catich子句。
- 对域已检查异常,只需要提供一个异常处理器;可以很容易地发现会抛出已检查异常地方法。
- 如果调用了一个抛出已检查异常地方法,而又没有提供处理器,编译器就会报错。
5.7.3 利用反射分析类的能力
检查类地结构。
package reflection;
import java.util.*;
import java.lang.reflect.*;
/**
* This program uses reflection to print all features of a class.
* @version 1.1 2004-02-21
* @author Cay Horstmann
*/
public class ReflectionTest
{
public static void main(String[] args)
{
// read class name from command line args or user input
String name;
if (args.length > 0) name = args[0];
else
{
Scanner in = new Scanner(System.in);
System.out.println("Enter class name (e.g. java.util.Date): ");
name = in.next();
}
try
{
// print class name and superclass name (if != Object)
Class cl = Class.forName(name);
Class supercl = cl.getSuperclass();
String modifiers = Modifier.toString(cl.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print("class " + name);
if (supercl != null && supercl != Object.class) System.out.print(" extends "
+ supercl.getName());
System.out.print("\n{\n");
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println();
printFields(cl);
System.out.println("}");
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
System.exit(0);
}
/**
* Prints all constructors of a class
* @param cl a class
*/
public static void printConstructors(Class cl)
{
Constructor[] constructors = cl.getDeclaredConstructors();
for (Constructor c : constructors)
{
String name = c.getName();
System.out.print(" ");
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(name + "(");
// print parameter types
Class[] paramTypes = c.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++)
{
if (j > 0) System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* Prints all methods of a class
* @param cl a class
*/
public static void printMethods(Class cl)
{
Method[] methods = cl.getDeclaredMethods();
for (Method m : methods)
{
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
// print modifiers, return type and method name
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(retType.getName() + " " + name + "(");
// print parameter types
Class[] paramTypes = m.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++)
{
if (j > 0) System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* Prints all fields of a class
* @param cl a class
*/
public static void printFields(Class cl)
{
Field[] fields = cl.getDeclaredFields();
for (Field f : fields)
{
Class type = f.getType();
String name = f.getName();
System.out.print(" ");
String modifiers = Modifier.toString(f.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.println(type.getName() + " " + name + ";");
}
}
}
5.7.4 在运行时使用反射分析对象
如何查看任意对象地数据与名称和类型:
- 获得对应地Class对象
- 通过Class对象调用getDeclaredFields
- Field类中的get方法查看对象域:
如果f是一个Field类型的对象(getDeclaredFields得到对象),obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,值为obj域当前值。
5.7.5 使用反射编写泛型数组代码
public static Object goodCopyOf(Object a, int newLength){
Class cl = a.getClass();
if(!cl.isArray())return null;
Class ComponentType = cl.getComponentType();
int length = Array.getLength(a);
Object newArray = Array.newInstance(componentType,newLnegth);
System.arraycopy(a,0,newArray,0,Math.min(length,newLength));
return newArray;
}
- 整型数组类型int[] 可以被转换成Object,但不能装换成Object[]。
5.7.6 调用任意方法
- 反射机制允许你调用任意方法。
5.8 继承的设计技巧
1、将公共操作和域放在超类中
2、不要使用受保护的域
- 子类集合时无限制的,任何一个人都能由某个类派产生一个子类,并编写代码直接访问protected的实例域,从而破坏封装性。
- 在Java中,同一个包中的所有类都可以访问protected域,而不管它是否为这个类的子类。
3、使用继承实现“is-a”关系
使用继承很容易达到节省代码的目的,但有时候也被人们滥用了。
4、除非所有继承的方法都有意义,否则不要使用继承
5、在覆盖方法是,不要改变与其的行为。
置换原则不因应用于语法,而且也可以应用于行为。
6、使用多态,而非类型信息。
- 以便使用多态性提供的动态分派机制执行相应的动作。
- 使用堕胎方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。
7、不要过多的使用反射
- 反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。
- 但通常不适于编写应用程序。
- 反射式脆弱的;编译器很难帮助人们发现程序中的错误(只有运行时发现错误并导致异常)