总有一天你我会越过高山,对酒当歌,逸欢,相见恨晚——岛屿心情《蝼蚁》
超类也就是父类,子类是继承父类延申出来的类。
“is-a” 关系是继承的一个明显特征。
关键字 extends 表示继承。 已存在的类称为超类( superclass)、 基类( base class) 或父类(parentclass); 新类称为子类(subclass、) 派生类( derived class) 或孩子类(child class)。
在设计类的时候应该将通用的方法放在超类中, 而将具有特殊用途的方法放在子类中,这种将通用的功能放到超类的做法,在面向对象程序设计中十分普遍。
超类中的有些方法对子类并不一定适用。
覆盖就是在子类中写一个和超类中想要覆盖的方法的方法签名完全相同的方法。子类中的返回值是超类返回值的子类,或者相同。具有可协变性。
在子类中想要使用继承过来的private域中的某一个属性Xxx,需要使用
super.getXxx();
来获取。
有些人认为 super 与 this 引用是类似的概念, 实际上,这样比较并不太恰当。这是因为 super 不是一个对象的引用, 不能将 super 赋给另一个对象变量, 它只是一个指示编译器调用超类方法的特殊关键字。
子类可以通过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)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定。
这里指的是父类的变量可以赋值为其任何一个子类对象的引用。
继承并不仅限于一个层次,但是不要理解为可以多继承。由一个公共的超类派生出来的所有类的集合称为继承层次。从某个特定的类到其祖先的路径被称为该类的继承链。
Java不支持多继承。有关Java中多继承功能的实现,和下一章要复习的接口有关。
“ is-a” 规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。
可以将子类的引用赋值给超类的变量,但是不能通过超类的变量调用子类自己的方法。不能将超类的引用赋值给子类的变量。
关于数组,不要在超类数组和子类数组之间进行转换,每个数组都要牢记创建他们的类型,并监督仅将类型兼容的引用存储到数组中。在数组里面放类型不兼容的对象时一般会报ArrayStoreException的异常。
方法调用的全过程:
有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为 final 类。前面曾经说过, 域也可以被声明为 final。 对于 final 域来说,构造对象之后就不允许改变它们的值了。不过, 如果将一个类声明为 final, 只有其中的方法自动地成为 final,而不包括域。
如果有一个String的引用,它引用的一定是一个String对象,而不可能是其他类的对象。
子类引用可以直接赋值给父类变量。
但是想将父类引用赋值给子类变量需要进行强制转换,仅限于原父类变量的值就是子类的引用,可以强制转换,但是原父类变量不是子类的引用的时候强制转换会有ClassCastException异常。
所以进行强制转换之前,先查看一下是否能够成功的转换。这个过程简单的使用instanceof操作符就可以实现了。例如
//A 转换为 B
if(A instanceof B){
//进行转换
}
进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。一般情况下应该尽量少用类型转换和instanceof运算符。
抽象方法充当着占位的角色, 它们的具体实现在子类中。扩展抽象类可以有两种选择。一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。
受保护的域对象继承过来之后,子类可以直接访问,但是不能其他父对象中的这个域 ,实际中应该少用protected属性。
在实际应用中,要谨慎使用 protected 属性。假设需要将设计的类提供给其他程序员使用,而在这个类中设置了一些受保护域, 由于其他程序员可以由这个类再派生出新类,并访问其中的受保护域。在这种情况下,如果需要对这个类的实现进行修改,就必须通知所有使用这个类的程序员。这违背了 OOP 提倡的数据封装原则。
在 Java 中,只有基本类型 ( primitive types) 不是对象, 例如,数值、 字符和布尔类型的值都不是对象。
所有的数组类塱,不管是对象数组还是基本类型的数组都扩展了 Object 类。
在 Object 类中,这个方法将判断两个对象是否具有相同的引用。。然而,对于多数类来说, 这种判断并没有什么意义。所以一般要在继承之后覆盖重写。
如果隐式和显式的参数不属于同一个类, 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
所以判断隐式和显式是否是一个类有下面两种情况
那如何编写一个完美的equals方法呢?
if (this = otherObject) return true;
if (otherObject = null) return false;
if (getClass() != otherObject.getCIassO) return false;
如果所有的子类都拥有统一的语义,就使用 instanceof 检测:
if (!(otherObject instanceof ClassName)) return false;
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;
}
散列码( 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);
}
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);
ArrayList 是一个采用类型参数( type parameter ) 的泛型类( generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面, 例如,ArrayList
staff.ensureCapacity(100);
ArrayList staff = new ArrayList0(100);
初始化后容量为100,size不添加元素还是0。
对数组实施插人和删除元素的操作其效率比较低。对于小型数组来说,这一点不必担心。但如果数组存储的元素数比较多, 又经常需要在中间位置插入、删除元素, 就应该考虑使用链表了。
将一个原始 ArrayList 赋给一个类型化 ArrayList 会得到一个警告。
ArrayList<Employee> result = employeeDB.find(query); // yields warning
使用类型转换并不能避免出现警告。将会得到另外一个警告信息, 指出类型转换有误。一旦能确保不会造成严重的后果,可以用 @SuppressWamings(“unchecked”) 标注来标记这个变量能够接受类型转换
假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型,也就是说,不允许写成 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;
}
public void f(Object...args)
所有的枚举类型都是 Enum 类的子类。它们继承了这个类的许多方法。其中最有用的一个是 toString, 这个方法能够返回枚举常量名。例如,Size.SMALL.toString( ) 将返回字符串“SMALL”。
toString 的逆方法是静态方法 valueOf。例如, 语句:
Size s = Enum.valueOf(Size.class, "SMALL");
每个枚举类型都有一个静态的 values 方法, 它将返回一个包含全部枚举值的数组。 例如,如下调用
Size[] values = Size.values() ;
能够分析类能力的程序称为反射(reflective )。
反射是一种功能强大且复杂的机制。 使用它的主要人员是工具构造者,而不是应用程序员。
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。如同用一个 Employee 对象表示一个特定的雇员属性一样, 一个 Class 对象将表示一个特定类的属性。
一个 Class 对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int 不是类, 但 int.class 是一个 Class 类型的对象。
Double[] class.getName( ) //返回[Ljava.lang.Double;
int[ ].class.getName( ) //返回[I ” ,
newlnstance方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认的构造器, 就会抛出一个异常 。
应该精心地编写代码来避免这些错误的发生, 而不要将精力花在编写异常处理器上。
在 java.lang.reflect 包中有三个类 Field、 Method 和 Constructor 分别用于描述类的域、 方法和构造器。 这三个类都有一个叫做 getName 的方法, 用来返回项目的名称。
Class类中的 getFields、getMethods和getConstructors方法将分别返 回类提供的public 域、方法和构造器数组,其中包括超类的公有成员。Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、 方法和构造器, 其中包括私有和受保护成员,但不包括超类的成员。
int i=Constructor.getModifiers()
Modifier.toString(i);
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();
}
将一个 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;
}
Object invoke(Object obj, Object... args)
对于静态方法,第一个参数可以被忽略, 即可以将它设置为 null。
缺陷:使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些。有鉴于此,建议仅在必要的时候才使用 Method 对象,而最好使用接口以及Java SE 8中 的 lambda 表达式(第 6 章中介绍)。特别要重申: 建议 Java 开发者不要使用 Method 对象的回调功能。使用接口进行回调会使得代码的执行速度更快, 更易于维护。