Java核心技术-继承
1 类、超类和子类
"is-a"关系是继承的一个明显特征。
1.1 定义子类
关键字extends表示继承
关键字extends表明正在构造的新类派生于一个已存在的类,已存在的类称为超类,新类称为子类,子类比超类拥有的功能更加丰富。
在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中。
1.2 覆盖方法
子类继承来的私有域只有通过超类的方法才能访问。(super关键字)
super与this的区别:
super关键字不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
在子类中可以增加域、增加方法或覆盖超类的方法,但不能删除继承的任何域和方法。
1.3 子类构造器
由于子类的构造器不能访问超类的私有域,所以必须利用超类的构造器对这部分私有域进行初始化,我们可以通过super实现对超类构造器的调用。
使用super调用构造器的语句必须是子类构造器的第一条语句。
关键字this的两个用途:1.引用隐式参数;2.调用该类的其他的构造器。
关键字super的两个用途:1.调用超类的方法;2.调用超类的构造器
相同点:调用构造器的语句只能作为另一个构造器的第一条语句出现。
。在运行时能够自动地选择调用哪个方法的现象称为动态绑定。
1.4 继承层次
由一个公共超类派生出来的所有类的集合被称为继承层次,从某个特定的类到其祖先的路径被称为该类的继承链。
1.5 多态
一个对象变量可以指示多种实际类型的现象被称为多态
用来判断是否应该设计为继承关系的简单规则:”is-a“规则,它表明子类的每个对象也是超类的对象。
对象变量是多态的。
1.6 理解方法调用
当调用类C的一个对象的f方法时:
1.提取对象实际类型的方法表:编译器会一一列举C类的所有名为f的方法和其超类中访问属性为public且名为f的方法
2.重载解析:找到参数类型完全匹配的方法。(允许子类将覆盖方法的返回类型定义为原返回类型的子类型)。
3.静态绑定:如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法(否则执行4)
4.动态绑定:虚拟机调用与引用对象的实际类型最合适的类方法。
动态绑定的重要特性:无需对现存代码进行修改,就可以对程序进行扩展。
在覆盖一个方法的时候,子类方法不能低于超类方法的可见性(超类方法为public,子类方法不能遗漏public修饰符)。
1.7 阻止继承:final类和方法
不允许扩展的类被称为final类,final方法不允许子类覆盖(final类中的所有方法自动称为final方法,不包括域)
例如:String类是final类;Calendar类中的getTime和setTime方法声明为final。
内联:如果一个方法很短、经常被调用并且没有真正地被覆盖,即时编译器就能够对它进行优化处理(e.getName()->e.name)
1.8 强制类型转换
将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换。
只有在需要使用子类中特有的方法时才需要进行类型转换。
一个良好的设计习惯:在进行类型转换之前,先查看一下是否能够成功地转换。(使用instanceof操作符)
总结:
*只能在继承层次内进行类型转换。
*在将超类转换成子类之前,应该使用instanceof进行检查。
1.9 抽象类
被abstract关键字修饰的类称为抽象类,可以不包含抽象方法。
包含一个或多个抽象方法的类本身必须被声明为抽象的(abstract),除了抽象方法外,抽象类还可以包含具体数据和方法。
1.10 受保护访问
如果希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域,可以将这些方法或域声明为protected。
Java用于控制可见性的4个访问修饰符:
1.仅对本类可见——private
2.对所有类可见——public
3.对本包和所有子类可见——protected
4.对本包可见——默认,不需要修饰符
2 Object:所有类的超类
Object类是Java中所有类的始祖,在Java中每个类都是由它扩展而来。
在Java中只有基本类型不是对象。
2.1 equals方法
Object类中的equals方法用于判断一个对象是否等于另一个对象(通过是否具有相同引用的方式)
一般情况下需要覆盖这种判断方式,通过两个对象状态的相等性来判断。
为了防备null情况,需要使用Object.equals(a,b)方法。如果两个参数都为null返回true,如果其中一个为null返回false,如果两个参数都不为null,调用a.equals(b).
2.2 相等测试与继承
对于数组类型的域,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等。
2.3 hashCode方法
散列码是由对象导出的一个整型值。。
由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码。其值为对象的存储地址。
如果重新定义equals方法,就必须重新定义hashCode方法。
使用Object.hash方法计算各个域值hash。
Object.hash(name,salary,hireDay)
equals中比较使用的域值应该和hashCode中相同。
2.4 toString方法
Object类定义了toString方法,用来打印输出对象所属的类名和散列码。
绝大多数的toString方法都遵循这样的格式:类的名字(getClass().getName()),随后是一对方括号阔气来的域值.
数组继承了object类的toString方法,修正方式是调用静态的Arrays.toString或者Arrays.deepToString方法。
强烈建议为自定义的没一个类增加一个toString方法
3 泛型数组列表
ArrayList是一个采用类型参数的泛型类。
使用”菱形“语法可以省去右边的类型参数。
一旦能够确认数组列表的大小不再变化,可以调用trimToSize方法。垃圾回收器将回收多余的存储空间。
3.1 访问数组列表元素
使用get、set、add、remove方法操作数组列表
如果数组存储的元素比较多,又经常需要在中间位置插入、删除元素,就应该考虑使用链表。
数组和数组列表比较:
*不必指出数组的大小
*使用add将任意多的元素添加到数组中
*使用size()代替length计算元素数目
*使用a.get(i)代替a[i]访问元素
3.2 类型化与原始数组列表的兼容性
鉴于兼容性的考虑,编译器在对类型转换进行检查之后,如果没有发现违反规则的现象,就将所有的类型化数组列表转换成原始数组列表。
在与遗留的代码进行交叉操作时,研究一下编译器的警告性提示,并确保这些警告不会造成太严重的后果就行了。
4 对象包装器与自动装箱
所有的基本类型都有一个与之对应的类,这些类称为包装器。
对象包装器类是不可变的,一旦构造了包装器,就不允许更改包装器的值。
对象包装器类是final类,不能定义他们的子类。
由于每个值分别包装在对象中,所以ArrayList<Integer>的效率远远低于int[]数组。
自动装箱:当将一个int值赋给Integer对象时
自动拆箱:当将一个Integer对象赋给一个int值时
装箱和拆箱是编译器认可的,而不是虚拟机。
使用数值对象包装器还有另一个好处:可以将某些基本方法放置在包装器中(Integer.parseInt(s))
5 参数数量可变的方法
public PrintStream printf(String fmt,Object... args)
这里的...是Java代码的一部分,它表明这个方法可以接收任意数量的对象。
编译器会对方法进行转换,将可变参数绑定到Object[]数组上,并在有必要的时候进行自动装箱。
可以将已存在且最后一个参数是数组的方法重新定义为可变参数方法。
6 枚举类
public enum Size { SMALL,MEDIUM,LARGE};
这个声明定义了一个枚举类,它有四个实例。
比较两个枚举类型的值时,永远不要用equals,而直接使用==
所有的枚举类都是Enum类的子类
toString()-返回枚举常量名
valueOf()-toString()的逆方法
values()-返回一个包含全部枚举值的数组
ordinal()-返回枚举常量的位置
7 反射
反射机制可以用来:
*在运行时分析类的能力
*在运行时查看对象
*实现通用的数组操作代码
*利用Method对象
7.1 Class类
在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。
获取Class类的三种方式:
1.Object类中的getClass()方法将返回一个Class类型的实例。
Class c1 = e.getClass();
2.调用静态方法forName获得类名对应的Class对象;
Class c1=Class.forName(className);
3.如果T是任意的Java类型,T.class将代表匹配的类对象。
Class c1=Random.class;
最常用的Class方法是getName,这个方法将返回类的名字。
一个Class对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int不是类,但int.class是一个Class类型的对象。
鉴于历史原因,数组类型的Class类调用getName方法会返回一个很奇怪的名字。
虚拟机为每个类型管理一个Class对象。可以利用==运算符实现两个类对象比较的操作。
if(e.getClass()==Employee.class)
newInstance()可以用来动态地创建一个类的实例(调用默认的构造器)。
e.getClass().newInstance();
将forName与newInstance配合起来使用,可以根据存储在字符串中的类名创建一个对象。
Class.forName("java.util.Random").newInstance();
如果需要向构造器中提供参数,需要使用Constructor类中的newInstance方法。
7.2 捕获异常
抛出异常比终止程序要灵活得多,这是因为可以提供一个”捕获“异常得处理器对异常情况进行处理。
异常有两种类型:未检查异常和已检查异常。对于已检查异常,编译器将会检查是否提供了处理器。对于未检查异常,编译器不会查看处理器。
最简单得处理器:将可能抛出已检查异常的一个或多个方法调用代码放在try块中,然后在catch子句中提供处理器代码。
Throwable是Exception类的超类。
7.3 利用反射分析类的能力
反射机制最重要的内容——检查类的结构
在java.lang.reflect包中有三个类Field、Method和Constructor,分别用于描述类的域、方法和构造器。
这三个类都有一个getName方法用来返回项目的名称,有一个getModifiers方法,返回一个整型数值,用不同的位开关描述public和static这样的修饰符使用情况。
Field类有一个getType方法用来返回描述域所属类型。
Method和Constructor类有能够报告参数类型的方法。
Method类还有一个可以报告返回类型的方法。
java.lang.reflect包中的Modifier类的静态方法也可以分析getModifiers返回的整型数值(isPublic、isPrivate或isFinal),还可以使用Modifier.toString方法将修饰符打印出来。
Class类中的getFields、getMethods和getConstructors方法将返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。
Class类中的getDeclareFields、getDeclareMethods和getDeclaredConstructors方法将返回类中声明的全部域、方法和构造器,但不包括超类的成员。
7.4 在运行时使用反射分析对象
查看任意对象的数据域名称和类型:
1.获得对应的Class对象。
2.通过Class对象调用getDeclareFields
查看对象域的关键方法是Field类中的get方法。如果f是一个Field类型的对象,obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域中与f同名的域的当前值。
Employee harry=new Employee("Harry Hacker,35000,10,1,1989"); Class c1=harry.getClass(); Field f=c1.getDeclaredField("name"); Object v=f.get(harry);
反射机制的默认行为受限于Java的访问控制。需要调用Field、Method或Constructor对象的setAccessible方法。
f.setAccessible(true)
setAccessible是AccessibleObject类中的一个方法,它是Field、Method和Constructor类的公共超类。
当然,可以获得就可以设置。调用f.set(obj,value)可以将obj对象的f域设置成新值。
下面是一个可供任意类使用的通用toString方法。
import java.lang.reflect.AccessibleObject; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; public class ObjectAnalyzer { private ArrayList<Object> visited = new ArrayList<>(); /** * Converts an object to a string representation that lists all fields. * @param obj an object * @return a string with the object's class name and all field names and * values */ public String toString(Object obj) { if (obj == null) return "null"; if (visited.contains(obj)) return "..."; visited.add(obj); Class cl = obj.getClass(); if (cl == String.class) return (String) obj; if (cl.isArray()) { String r = cl.getComponentType() + "[]{"; for (int i = 0; i < Array.getLength(obj); i++) { if (i > 0) r += ","; Object val = Array.get(obj, i); if (cl.getComponentType().isPrimitive()) r += val; else r += toString(val); } return r + "}"; } String r = cl.getName(); // inspect the fields of this class and all superclasses do { r += "["; Field[] fields = cl.getDeclaredFields(); AccessibleObject.setAccessible(fields, true); // get the names and values of all fields for (Field f : fields) { if (!Modifier.isStatic(f.getModifiers())) { if (!r.endsWith("[")) r += ","; r += f.getName() + "="; try { Class t = f.getType(); Object val = f.get(obj); if (t.isPrimitive()) r += val; else r += toString(val); } catch (Exception e) { e.printStackTrace(); } } } r += "]"; cl = cl.getSuperclass(); } while (cl != null); return r; } }
可以通过以下方式使用通用的toString方法实现自己类中的toString方法(实体类中的域使用基本类型):
public String toString() { return new ObjectAnalyzer().toString(this); }
7.5 使用反射编写泛型数组代码
如何构造泛型数组?
考虑这样的问题:
一个Employee[]临时的转换成Object[]数组,然后再把它转换回来是可以的,但一个从开始就是Object[]的数组却永远不能转换成Employee[]数组。
因此,我们需要能够创建与原数组类型相同的新数组(需要java.lang.reflect包中的Array类的一些方法,其中关键的是Array类中的静态方法newInstance,它能够构造新数组)
Object newArray=Array.newInstance(componentType,newLength);
在调用这个方法时需要提供两个参数:
一个是数组的长度——Array.getLength(a)
一个是数组的元素类型——1.首先获得a数组的类对象;2.确认它是一个数组;3.使用Class类的getComponentType方法确定数组对应的类型。
下面是一个可扩展任意类型数组的方法:
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, newLength); System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength)); return newArray; }
此处将参数声明为Object类型而不是Object[]类型的原因是:整型数组类型int[]可以被转换成Object,而不能转换成对象数组。
7.6 调用任意方法
反射机制允许调用任意方法
类似于Field类的get方法查看对象域过程,在Method类中有一个invoke方法,它允许调用包装在当前Method对象中的方法。
invoke方法的签名:
Object invoke(Object obj,Object... args)
第一个参数是隐式参数,其余参数是显示参数,对于静态方法,第一个参数可以被忽略,即设置为null。
例如:m1.invoke(harry)——m1是Employee的getName方法(非静态方法)
f.invoke(null,6)——f是Math类的sqrt方法(静态方法)
如何获得Method对象:
通过Class类中的getMethod方法得到想要的方法,于getField类似(getField方法根据表示域名的字符串,返回一个Field对象),注意,有可能存在若干个相同名字的方法,所以还必须提供想要的方法的参数类型。
getMethod方法的签名:
Method getMethod(String name,Class... parameterTypes)
例如: Method m1=Employee.class.getMethod("raiseSalary",double.class);
使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些,有鉴于此,建议仅在必要的时候才使用Method对象,而最好使用接口以及lambda表达式。
特别要重申:建议Java开发者不要使用Method对象的回调功能。使用接口进行回调会使得代码的执行速度更快,更易维护。
7.8 继承的设计技巧
1.将公共操作和域放在超类
2.不要使用受保护的域
3.使用继承实现“is-a”关系
4.除非所有的继承方法都有意义,否则不要使用继承
5.在覆盖方法时,不要改变预期的行为
6.使用多态,而非类型信息
if(x is of type 1)
action1(x);
else if(x is of type 2)
action2(x);
考虑使用多态。
使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。
7.不要过多的使用反射