1.override
子类尽管继承了超类的私有域,但不能直接访问之,只有使用超类的方法才能访问私有域。
为此可以使用特定的关键字sufer解决这个问题。
super.method()
注:1)有些人认为super与this引用是类似的概念。实际上,这样比较并不太恰当。这是因为super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
2)允许子类将覆盖方法的返回类型定义为原返回类型的子类型。
3)在覆盖一个方法时,子类方法不能低于超类方法的可见性。特别是,超类方法是public,子类方法一定要声明为public。
2.子类构造器
由于子类的构造器不能访问超类的私有域,所以必须利用超类的构造器对这部分私有域进行初始化,我们可以通过super实现对超类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。
如果子类的构造器没有显式的调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,则Java编译器将报告错误。
3.多态
在Java中,对象变量是多态的。
“is-a"规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。
然而,不能将一个超类的引用赋给子类变量。
注:在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。尽管编译器接纳此赋值操作,但会发生超类调用不存在的实例域而发生错误。
为了确保不发生这类错误,所有数组都要牢记创建它们的元素类型,并负责监督仅将类型兼容的引用存储到数组中。
4.理解方法调用
下面假设要调用x.f(args),隐式参数x声明为类C的一个对象。下面是调用过程的详细描述:
1)编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是:有可能存在多个名字f,但参数类型不一样的方法。编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法。
至此,编译器已获得所有可能被调用的候选方法。
2)接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程称为重载解析。由于允许类型转换,这个过程可能十分复杂。
如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。
至此,编译器已获得需要调用的方法名字和参数类型。
3)如果是private方法,static方法,final方法或者构造器,那么编译器将可以准确知道应该调用哪个方法,我们将这种调用方式称为静态绑定。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定(在运行时能够自动地选择调用哪个方法的现象成为动态绑定)。
4)当程序运行时,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最适合的那个类的方法。(假设x的实际类型是D,它是C类的子类。如果D类定义了此方法,就直接调用它;否则,将在D类的超类中寻找f方法,以此类推。)
每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。
注:动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。
5.final类和方法
希望阻止人们利用某个类定义子类,不允许扩展的类被称为final类。
类中的方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法(final类中的所有方法自动地成为final方法)。
将类和方法声明为final主要目的是:确保它们不会在子类中改变语义。
注:如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程被成为内联。
6.强制类型转换
进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。
如果试图在继承链上进行向下的类型转换,Java运行时系统将报告这个错误,并产生一个ClassCastException异常。因此,应该养成这样一个良好的程序设计习惯:在进行类型转换之前,先查看一下是否能够成功转换。这个过程简单地使用instanceof操作符就可以实现。
实际上,通过类型转换调整对象的类型并不是一种好的做法。因为大多数情况因为实现多态性的动态绑定机制能够自动地找到相应的方法。只有在使用类中特有的方法时才需要进行类型转换。
7. 抽象类
包含一个或多个抽象方法的类本身必须被声明为抽象的。
除了抽象方法外,抽象类还可以包含具体数据和具体方法。建议尽量将通用的域和方法(不管是否是抽象的)放在超类(不管是否是抽象类中)。
类即使不含抽象方法,也可以将类声明为抽象类。
抽象类不能被实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。
注:可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的实例。
8.四个访问修饰符
1)仅对本类可见----private
2)对所有类可见---public
3)对本包和所有子类可见---protected
4)对本包可见---默认(不需要修饰符)
9.Object类
可以使用Object类型的变量引用任何类型的对象;当然,Object类型的变量只能用于作为各种值的通用持有者,要想对其中的内容进行具体的操作,还需要清楚对象的原始类型,并进行相应的类型转换。
1)equals方法
Object类中的equals方法用于检测一个对象是否等于另外一个对象。在Object类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。
在子类中定义equals方法时,首先调用超类的equals。如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域。
下面给出编写一个完美的equals方法的建议:
a)显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。
b)检测this与otherObject是否引用同一个对象。
if(this==otherObject){return true;}
c)检测otherObject是否为null,如果为null,返回false。这项检测是很有必要的。
if(otherObject==null){return false;}
d)比较this与otherObject是否属于同一个类。如果equals的语义在每个子类中有所改变,就使用getClass检测:
if(getClass()!=otherObject.getClass()){return false;}
如果所有的子类都拥有统一的语义,就使用instanceof检测
if(!(otherObject instanceof ClassName)){return false;}
e)将otherObject转换为相应的类类型变量:
ClassName other=(ClassName) otherObject;
f)现在开始对所有需要比较的域进行比较了。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true,否则返回false。如果在子类中重新定义equals,就要在其中包含调用super.equals(other)。
2)hashCode方法
如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。
equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么,x.hashcode()就必须与y.hashCode()具有相同的值。
3)toString方法
设计子类的程序员也应该定义自己的toString方法,并将子类域的描述添加进去。如果超类使用了getClass().getName()。那么子类只要调用super.toString()就可以了。
只要对象与一个字符串通过操作符“+”连接起来,Java编译器就会自动地调用toString方法,以便获得这个对象的字符串描述。
注:数组继承了Object类的toString方法,数组类型将按照旧的格式打印。
int[] luckyNumbers={2,3,5,7,11,13}; String s=""+luckyNumbers;//prints [I@1a46e30
修正的方式是调用静态方法Arrays.toString方法。
10.泛型数组列表
ArrayList是一个采用类型参数的泛型类。
API:
ArrayList<E>()
构造一个空数组列表
ArrayList<E>(int initialCapacity)
用指定容量构造一个空数组列表
boolean add(E obj)
在数组列表的尾端添加一个元素,永远返回true。
int size()
返回存储在素组列表中的当前元素数量
void ensureCapacity(int capacity)
确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素。
void trimToSize()
将数组列表的存储容量削减到当前尺寸
void set(int index,E obj)
设置数组列表指定位置的元素值,这个操作将覆盖这个位置原有的内容。
E get(int index)
获得指定位置的元素值
void add(int index,E obj)
向后移动元素,以便插入元素
E remove(int index)
删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回
注:1)数组列表的容量与数组的大小有一个非常重要的区别。如果为数组分配100个元素的存储空间,数组就有100个空位置可以使用。而容量为100的数组列表只是拥有保存100元素的潜力(实际上,重新分配空间的话,将会超过100),但是在最初,甚至完成初始化构造之后,数组列表根本就不含有任何元素。
11.对象包装器与自动装箱
由于包装器类引用可以为null,所以自动装箱有可能会抛出一个NullPointerException异常。
如果在一个条件表达式中混合使用Integer和Double类型,Integer值就会自动拆箱,提升为double,再装箱为Double。
装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。
12.参数数量可变的方法
printf方法是这样定义的
public class PrintStream{ public PrintStream printf(String fmt,Object... args){ return format(fmt,args); } }
这里的省略号...是Java代码的一部分,它表明这个方法可以接收任意数量的对象。
实际上,printf方法接受两个参数,一个是格式字符串,另一个是Object[] 数组,其中保存着所有的参数。现在将扫描fmt字符串,并将第i个格式说明符与args[i]的值匹配起来。
换句话说,对于printf的实现者来说,Object... 参数类型与Object[] 完全一样。
注:允许将一个数组传递给可变参数方法的最后一个参数。
13.反射
能够分析类能力的程序称为反射。反射机制可以用来:
1)在运行时分析类的能力
2)在运行时查看对象,例如,编写一个toString方法供所有类使用
3)实现通用的数组操作代码
4)利用Method对象,这个对象很像C++中的函数指针
14.Class类
在运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行(动态绑定)。
可以通过专门的Java类访问这些信息。保存这些信息的类被称为Class。
获得Class对象的三种方法:
1)Object类中的getClass()方法。
2)调用静态方法forName()获得类名对应的Class对象。
3)如果T是任意的Java类型。T.class将代表匹配的类的对象。
Class cl1=Random.class//if you import java.util.* Class cl2=int.class Class cl3=Double[].class
虚拟机为每个类型管理一个Class对象。因此可以利用==运算符实现两个类对象比较的操作。
if(e.getClass()==Employee.class){...}
还有一个很有用的方法newInstance(),可以用来动态地创建一个类的实例。
e.getClass().newInstance();
注:a:在启动时,包含main方法的类被加载。它会加载所有需要的类。这些被加载的类又要加载它们需要的类。
b:Class类实际上是一个泛型类。
15.利用反射分析类的能力
在java.lang.reflect包中有三个类Field,Method和Constructor分别用于描述类的域,方法和构造器。这三个类都有一个叫做getName的方法,用来返回项目的名称。
1)Field类有一个getType方法,用来返回描述域所属类型的Class对象。
2)Method和Constructor类有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。
3)这三个类还有一个叫做getModifiers的方法,它将返回一个整型数值,用不同的位开关描述public和static这样的修饰符使用状况。另外还可以用java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的整型数值。
Class类中的getFields,getMethods和getConstructors方法将分别返回类提供的public域,方法和构造器,其中包括超类的公有成员。
Class类的getDeclareFields,getDeclareMethods和getDeclareConstructors方法将分别返回类中的声明的全部域,方法和构造器,其中包含私有和受保护成员,但不包括超类的成员。