阅读Java核心技术Ⅰ的笔记(Java基础、第五章、继承)

总有一天你我会越过高山,对酒当歌,逸欢,相见恨晚——岛屿心情《蝼蚁》

Java核心技术Ⅰ

  • 第五章
    • 5.1 类、超类和子类
      • 5.1.1 定义子类
      • 5.1.2 覆盖方法
      • 5.1.3 子类构造器
      • 5.1.4 继承层次
      • 5.1.5 多态
      • 5.1.6 理解方法调用
      • 5.1.7 阻止继承:final类和方法
      • 5.1.8 强制类型转换
      • 5.1.9 抽象类
      • 5.1.10 受保护访问
    • 5.2 Object:所有类的超类
      • 5.2.1 equals方法
      • 5.2.2 相等测试与继承
      • 5.2.3 hashCode方法
      • 5.2.4 toString方法
    • 5.3 泛型数组列表
      • 5.3.1 访问数组列表元素
      • 5.3.2 类型化与原始数组列表的兼容性
    • 5.4 对象包装器与自动封装
    • 5.5 参数数量可变的方法
    • 5.6 枚举类
    • 5.7 反射
      • 5.7.1 Class类
      • 5.7.2 捕获异常
      • 5.7.3 利用反射分析类的能力
      • 5.7.4 在运行时使用反射分析对象
      • 5.7.5 使用反射编写泛型数组代码
      • 5.7.6 调用任意方法
    • 5.8 继承的设计技巧

第五章

5.1 类、超类和子类

超类也就是父类,子类是继承父类延申出来的类。
“is-a” 关系是继承的一个明显特征。

5.1.1 定义子类

关键字 extends 表示继承。 已存在的类称为超类( superclass)、 基类( base class) 或父类(parentclass); 新类称为子类(subclass、) 派生类( derived class) 或孩子类(child class)。
在设计类的时候应该将通用的方法放在超类中, 而将具有特殊用途的方法放在子类中,这种将通用的功能放到超类的做法,在面向对象程序设计中十分普遍。

5.1.2 覆盖方法

超类中的有些方法对子类并不一定适用。
覆盖就是在子类中写一个和超类中想要覆盖的方法的方法签名完全相同的方法。子类中的返回值是超类返回值的子类,或者相同。具有可协变性。
在子类中想要使用继承过来的private域中的某一个属性Xxx,需要使用

super.getXxx();

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

5.1.3 子类构造器

子类可以通过super()调用超类的构造器。例如:

public Manager(String name, double salary, int year, int month, int day) {
super(name, salary, year, month, day);
bonus = 0; }

因为子类不能访问超类中的私有域,所以必须调用超类的构造方法对其进行初始化。如上所见,使用super调用构造器的语句必须是子类构造器的第一句。
如果子类的构造器没有显式地调用超类的构造器, 则将自动地调用超类默认(没有参数 )的构造器。 如果超类没有不带参数的构造器, 并且在子类的构造器中又没有显式地调用超类的其他构造器’则 Java 编译器将报告错误。
this的两个用途:
一、引用隐式参数
二、调用该类其他的构造器
super的两个用途:
一、调用超类的方法
二、调用超类的构造器。

一个对象变量(例如, 变量 e ) 可以指示多种实际类型的现象被称为多态( polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定
这里指的是父类的变量可以赋值为其任何一个子类对象的引用。

5.1.4 继承层次

继承并不仅限于一个层次,但是不要理解为可以多继承。由一个公共的超类派生出来的所有类的集合称为继承层次。从某个特定的类到其祖先的路径被称为该类的继承链。
Java不支持多继承。有关Java中多继承功能的实现,和下一章要复习的接口有关。

5.1.5 多态

“ is-a” 规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。
可以将子类的引用赋值给超类的变量,但是不能通过超类的变量调用子类自己的方法。不能将超类的引用赋值给子类的变量。
关于数组,不要在超类数组和子类数组之间进行转换,每个数组都要牢记创建他们的类型,并监督仅将类型兼容的引用存储到数组中。在数组里面放类型不兼容的对象时一般会报ArrayStoreException的异常。

5.1.6 理解方法调用

方法调用的全过程:

  1. 编译器查看对象的声明类型和方法,编译器将会一一列举所有 C 类中名为 f 的方法和其超类中访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)。
  2. 接下来,编译器将査看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重栽解析。由于允许类型转换( int 可以转换成 double, Manager 可以转换成 Employee, 等等,) 所以这个过程可能很复杂。
  3. 至此, 编译器已获得需要调用的方法名字和参数类型。(方法的名字和参数列表称为方法的签名。返回类型不是签名的一部分, 因此,在覆盖方法时, 一定要保证返回类型的兼容性。 允许子类将覆盖方法的返回类型定义为原返回类型的子类型。可协变的返回类型
  4. 如果是 private 方法、 static 方法、 final 方法(有关 final 修饰符的含义将在下一节讲述)或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为静态绑定( static binding )。
  5. 当程序运行,并且采用动态绑定调用方法时, 虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法。假设 x 的实际类型是 D,它是 C 类的子类。如果 D 类定义了方法 f(String,) 就直接调用它;否则, 将在 D 类的超类中寻找 f(String,) 以此类推。每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表。
    在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是, 如果超类方法是 public, 子类方法一定要声明为 public。经常会发生这类错误:在声明子类方法的时候, 遗漏了 public 修饰符。此时,编译器将会把它解释为试图提供更严格的访问权限。

5.1.7 阻止继承:final类和方法

有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为 final 类。前面曾经说过, 域也可以被声明为 final。 对于 final 域来说,构造对象之后就不允许改变它们的值了。不过, 如果将一个类声明为 final, 只有其中的方法自动地成为 final,而不包括域。
如果有一个String的引用,它引用的一定是一个String对象,而不可能是其他类的对象。

5.1.8 强制类型转换

子类引用可以直接赋值给父类变量。
但是想将父类引用赋值给子类变量需要进行强制转换,仅限于原父类变量的值就是子类的引用,可以强制转换,但是原父类变量不是子类的引用的时候强制转换会有ClassCastException异常。
所以进行强制转换之前,先查看一下是否能够成功的转换。这个过程简单的使用instanceof操作符就可以实现了。例如

//A 转换为 B
if(A instanceof B){
	//进行转换
}

进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。一般情况下应该尽量少用类型转换和instanceof运算符。

5.1.9 抽象类

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

  • 一个抽象类可以不含抽象方法
  • 含有抽象方法的一定要是抽象类
  • 抽象类不能被实例化,但是一个抽象类的变量可以被赋值为它的任何子类,这也是多态的一种表达。
    在 Java 程序设计语言中,抽象方法是一个重要的概念。在接口(interface) 中将会看到更多的抽象方法。

5.1.10 受保护访问

受保护的域对象继承过来之后,子类可以直接访问,但是不能其他父对象中的这个域 ,实际中应该少用protected属性。
在实际应用中,要谨慎使用 protected 属性。假设需要将设计的类提供给其他程序员使用,而在这个类中设置了一些受保护域, 由于其他程序员可以由这个类再派生出新类,并访问其中的受保护域。在这种情况下,如果需要对这个类的实现进行修改,就必须通知所有使用这个类的程序员。这违背了 OOP 提倡的数据封装原则。

5.2 Object:所有类的超类

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

5.2.1 equals方法

在 Object 类中,这个方法将判断两个对象是否具有相同的引用。。然而,对于多数类来说, 这种判断并没有什么意义。所以一般要在继承之后覆盖重写。

5.2.2 相等测试与继承

如果隐式和显式的参数不属于同一个类, equals 方法将如何处理呢?许多程序员却喜欢使用 instanceof 进行检测:

if ( KotherObject instanceof Employee)) 
	return false;

Java 语言规范要求 equals 方法具有下面的特性:
1 ) 自反性:对于任何非空引用 x, x.equals(?0 应该返回 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.eqimIS(y) 应该返回同样的结果
5 ) 对于任意非空引用 x, x.equals(null) 应该返回 false。
利用instanceof检测就不满足对称性,例如B是A的子类,如下

B instanceof A //true
A instanceof B //false

所以判断隐式和显式是否是一个类有下面两种情况

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

那如何编写一个完美的equals方法呢?

  1. 显式参数命名为 otherObject, 稍后需要将它转换成另一个叫做 other 的变量。
  2. 检测 this 与 otherObject 是否引用同一个对象:
if (this = otherObject) return true;
  1. 检测 otherObject 是否为 null, 如 果 为 null, 返 回 false。这项检测是很必要的。
if (otherObject = null) return false;
  1. 比较 this 与 otherObject 是否属于同一个类。如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测:
if (getClass() != otherObject.getCIassO) return false;

如果所有的子类都拥有统一的语义,就使用 instanceof 检测:

if (!(otherObject instanceof ClassName)) return false;
  1. 将 otherObject 转换为相应的类类型变量:
ClassName other = (ClassName) otherObject

6.现在开始对所有需要比较的域进行比较了。使用 =比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配, 就返回 true; 否 则 返 回 false。

return fieldl == other.field
&& Objects.equa1s(fie1d2, other.field2)

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

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

 public boolean equals(Object obj){
        if (this==obj)return true;
        if (obj==null)return false;
        if (this.getClass()!=obj.getClass())return false;
        Employee employee= (Employee) obj;
        return this.name.equals(employee.name)
                &&Objects.equals(this.hireDay,employee.hireDay)
                &&this.salary==employee.salary;
   }

5.2.3 hashCode方法

散列码( hash code ) 是由对象导出的一个整型值。散列码是没有规律的。如果 x 和 y 是两个不同的对象, x.hashCode( ) 与 y.hashCode( ) 基本上不会相同。
获取hashCode XXX.hashCode(),XXX是对象,不能是基本数据类型 int、double,要装箱,最好使用null方法安全的Objects.hashCode(xxx),xxx随便传,会自动封箱和拆箱,还可以使用Object.hash(Object…values)。
如果重新定义 equals方法,就必须重新定义 hashCode 方法, 以便用户可以将对象插人到散列表中。

@Override
    public int hashCode() {
        return Objects.hash(name,salary,hireDay);
    }

5.2.4 toString方法

public String toString() {
        return super.toString()+"Manager{" +
                "bounds=" + bounds +
                '}';
    }

调用父类的toString()方法使用super.toString()。
数组继承了 object 类的 toString 方法,数组类型将按照旧的格式打印。
调用静态方法 Arrays.toString

int[] luckyNumbers = { 2, 3, 5, 7,11,13 } ;
String s = Arrays.toString(luckyNumbers);

5.3 泛型数组列表

ArrayList 是一个采用类型参数( type parameter ) 的泛型类( generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面, 例如,ArrayList。它使用起来有点像数组,但在添加或删除元素时, 具有自动调节数组容量的功能,而不需要为此编写任何代码。没有后缀 <…> 仍然可以使用ArrayList, 它将被认为是一个删去了类型参数的“ 原始” 类型。

  • 如果已经清楚或能够估计出数组可能存储的元素数量, 就可以在填充数组之前调用ensureCapacity方法:staff.ensureCapacity(100);
  • 还可以把初始容量传递给 ArrayList 构造器:ArrayList staff = new ArrayList0(100);
  • 一旦能够确认数组列表的大小不再发生变化,就可以调用 trimToSize方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目,谨慎使用。垃圾回收器将回收多余的存储空间。
  • 使用add方法添加新元素,使用set方法置换某个位置的元素

初始化后容量为100,size不添加元素还是0。

5.3.1 访问数组列表元素

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

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

将一个原始 ArrayList 赋给一个类型化 ArrayList 会得到一个警告。

ArrayList<Employee> result = employeeDB.find(query); // yields warning

使用类型转换并不能避免出现警告。将会得到另外一个警告信息, 指出类型转换有误。一旦能确保不会造成严重的后果,可以用 @SuppressWamings(“unchecked”) 标注来标记这个变量能够接受类型转换

5.4 对象包装器与自动封装

假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型,也就是说,不允许写成 ArrayList。这里就用到了 Integer 对象包装器类。我们可以声明一个 Integer对象的数组列表。
由于每个值分别包装在对象中, 所以 ArrayList 的效率远远低于int[ ] 数组。
如果在一个条件表达式中混合使用 Integer 和 Double 类型, Integer 值就会拆箱,提升为 double, 再装箱为 Double:

Integer n = 1;
Double x = 2.0;
System.out.println(true ? n : x); // Prints 1.0

最后强调一下,装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时, 插人必要的方法调用。虚拟机只是执行这些字节码。
不能使用包装器修改类创建修改数值参数的方法,如果想编写一个修改数值参数值的方法, 就需要使用在 org.omg.CORBA 包中定义的持有者( holder) 类型, 包括 IntHolder、BooleanHolder 等。每个持有者类型都包含一个公有 (!)域值,通过它可以访问存储在其中的值。

public static void triple(IntHolder x){ 
	x.value = 3 * x.value;
}

5.5 参数数量可变的方法

public void f(Object...args)

5.6 枚举类

所有的枚举类型都是 Enum 类的子类。它们继承了这个类的许多方法。其中最有用的一个是 toString, 这个方法能够返回枚举常量名。例如,Size.SMALL.toString( ) 将返回字符串“SMALL”。
toString 的逆方法是静态方法 valueOf。例如, 语句:

Size s = Enum.valueOf(Size.class, "SMALL");

每个枚举类型都有一个静态的 values 方法, 它将返回一个包含全部枚举值的数组。 例如,如下调用

Size[] values = Size.values() ;

5.7 反射

能够分析类能力的程序称为反射(reflective )。

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

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

5.7.1 Class类

在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。如同用一个 Employee 对象表示一个特定的雇员属性一样, 一个 Class 对象将表示一个特定类的属性。
一个 Class 对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int 不是类, 但 int.class 是一个 Class 类型的对象。

Double[] class.getName( ) //返回[Ljava.lang.Double; 
int[ ].class.getName( ) //返回[I ” ,

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

5.7.2 捕获异常

应该精心地编写代码来避免这些错误的发生, 而不要将精力花在编写异常处理器上。

5.7.3 利用反射分析类的能力

在 java.lang.reflect 包中有三个类 Field、 Method 和 Constructor 分别用于描述类的域、 方法和构造器。 这三个类都有一个叫做 getName 的方法, 用来返回项目的名称。
Class类中的 getFields、getMethods和getConstructors方法将分别返 回类提供的public 域、方法和构造器数组,其中包括超类的公有成员。Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、 方法和构造器, 其中包括私有和受保护成员,但不包括超类的成员。

  • int getModifiers( )返回一个用于描述构造器、 方法或域的修饰符的整型数值。使用 Modifier 类中的这个方法可以分析这个返回值。
  • static String toString(int modifiers )返回对应 modifiers 中位设置的修饰符的字符串表示。
int i=Constructor.getModifiers()
Modifier.toString(i);

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

setAccessible 方法是 AccessibleObject 类中的一个方法, 它是 Field、 Method 和 Constructor类的公共超类。这个特性是为调试、 持久存储和相似机制提供的。
非常好的例子:

private ArrayList<Object> arrayList=new ArrayList<>();
    public String toString(Object object){
        if (object==null)return "null";
        if (arrayList.contains(object))return "对象已经打印过一次啦";
        arrayList.add(object);
        Class aClass=object.getClass();
        //如果是String类型
        if (aClass==String.class)return (String)object;
        StringBuilder builder=new StringBuilder();
        //如果是Array类型
        if (aClass.isArray()){
            //得到数组的类型
            builder.append(aClass.getComponentType()).append("[]{");
            for (int i=0;i< Array.getLength(object);i++){
                if (i>0)builder.append(",");
                Object o=Array.get(object,i);
                if (aClass.getComponentType().isPrimitive())builder.append(o);
                else builder.append(toString(o));
            }
            return builder.toString();
        }
        builder.append(aClass.getName());
        do {
            builder.append("[");
            Field[] fields = aClass.getDeclaredFields();
            //设置绕开检查规则
            AccessibleObject.setAccessible(fields,true);
            for (Field field:fields){
                //判断是不是static
                if (!Modifier.isStatic(field.getModifiers())){
                    if (!builder.toString().endsWith("[")){
                        builder.append(",");
                    }
                    builder.append(field.getName()).append("=");
                    try {
                        //得到返回值类型
                        Class t=field.getType();
                        Object val=field.get(object);
                        if (t.isPrimitive())builder.append(val);
                        else builder.append(toString(val));
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
            builder.append("]");
            //得到父类的反射类型
            System.out.println(aClass);
            aClass=aClass.getSuperclass();
        }while (aClass!=null);
        return builder.toString();
    }

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

将一个 Employee[ ]临时地转换成 Object[ ] 数组, 然后再把它转换回来是可以的,但一 从开始就是 Object[ ] 的数组却永远不能转换成 Employe[ ]数组。
又一个很好的例子:

public Object copyArray(Object objects,int newLengths){
        //获取数组的反射
        Class aClass=objects.getClass();
        //获取数组的类型
        Class typeClass=aClass.getComponentType();
        //获取数组的长度
        int length= Array.getLength(objects);
        Object newArray=Array.newInstance(typeClass,newLengths);
        System.arraycopy(objects,
                0,//从哪里开始
                newArray,
                0,//复制的数组从哪开始
                Math.min(length,newLengths));
        return newArray;
    }

5.7.6 调用任意方法

Object invoke(Object obj, Object... args)

对于静态方法,第一个参数可以被忽略, 即可以将它设置为 null。
缺陷:使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些。有鉴于此,建议仅在必要的时候才使用 Method 对象,而最好使用接口以及Java SE 8中 的 lambda 表达式(第 6 章中介绍)。特别要重申: 建议 Java 开发者不要使用 Method 对象的回调功能。使用接口进行回调会使得代码的执行速度更快, 更易于维护。

5.8 继承的设计技巧

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

你可能感兴趣的:(java基础)