1、final关键字
final关键字通常是表示无法改变的,有三种使用情况:数据、方法、类。
(1)final数据
分两种情况:表示永不改变的编译时常量,可在编译时执行计算,必须是基本类型且必须在定义时确切赋值,若同时还被static修饰则表示是一段不能改变的存储空间,通常大写;一个在运行时被初始化的值,它不希望被改变,可以是基本类型,也可是引用,若是引用,表示引用只能指向一个确切对象(但对象本身可以改变),而在初始化之后不能指向别的对象。
但是不能因为一个基本类型数据被final修饰就认定在编译时知道其值,例如用生成的随机数值初始化它,那么每创建一个对象,其值都不一样,和编译时常量的定义不相符
空白final:被声明为final但没有给定初始值的域,空白final域提供了更大的灵活性,可以根据对象不同而有所不同,一般在构造器中用表达式进行赋值。
final参数:将方法的形参(引用)用final修饰,表示无法在方法中更改参数引用所指向的对象或者参数的值本身,即可读但不可写
(2)final方法
使用final方法是为了把方法锁定,防止继承时被覆盖从而修改其行为,只有明确禁止覆盖时,才将方法设置为final,不要因为性能而将方法设置为final。
final关键字与private关键字:类中所有private方法都隐式指定为final的。这可能会带来混淆,如果基类有一个同名的private方法,导出类是否真的覆盖了这个方法?并没有,为了避免这个问题,要么覆盖时加上注解,要么就避免方法名和基类private方法同名。
(3)final类
将类定义为final,表示该类不能被任何人继承,即该类不需要有任何改动且不能有子类。final类的域可以选择是或不是final,和非final类的域定义为final域的规则一样,但是由于final类禁止继承,所以final类的所有方法都隐式的是final的。
final类用得不多,因为要预见一个类如何被复用通常非常困难。
2. 类之间的关系(复用类的两种形式)
- 组合
在新类中显示地放置子对象, 各个子对象之间没有什么联系,子对象是新类的一部分,新类与放置的子对象是has-a关系,比如电脑有CPU、RAM、显卡等等。
class CPU{
public String toString(){
return " CPU ";
}
}
class RAM{
public String toString(){
return " RAM ";
}
}
class GraphicChip{
public String toString(){
return " GraphicChip ";
}
}
public class Computer {
private CPU cpu;
private RAM ram;
private GraphicChip graphicChip;
public Computer(){
cpu = new CPU();
ram = new RAM();
graphicChip = new GraphicChip();
}
public String toString(){
return "A computer has"+cpu+ram+graphicChip;
}
public static void main(String[] args){
Computer c = new Computer();
System.out.println(c);
}
}
/**
* 输出
A computer has CPU RAM GraphicChip
*/
在组合和继承之间选择,组合是显示放置子对象,继承是隐式放置。组合通常用于想在新类中使用现有类的功能而非它的接口,这种情况下可以在新类中嵌入一个对象,让其实现若需要的功能,如Computer类中的cpu ram 。组合中放置的对象一般为private,但有时将放置的对象声明为public将有助于用户理解如何去使用类。
通过继承,使用现有的通用类开发它的特殊版本,这时用组合是毫无意义的,比如车子继承自交通工具,是is-a关系,使用组合车子与交通工具就是has-a关系,这显然说不通。
利用现有类来开发新的类,使用组合和继承都可以,但是继承其实并不常用,如果需要从新类向基类进行向上转型,就应该选择继承,否则应该考虑组合
- 代理
Java并不直接支持代理,它是继承和组合的折中,将一个类的对象置于新类中(像组合),但同时也暴露了该对象的所有方法(像继承)。
3. 转型
向上转型:将某个对象的引用视为其基类类型的引用,在继承层次中向上移动。由于导出类可能会扩展基类,故向上转型会丢失导出类的信息(成员变量或方法),无法再访问导出类中基类没有的信息。
向下转型:与向上转型相反,在继承层次中向下移动,向下转型要先向上转型,即导出类的对象先向上转型然后再向下转型,因为基类对象如果直接向下转型,由于导出类信息会比基类多,逻辑上说不通,会抛出异常。
转型的原因:向上转型,可以不管导出类的存在,直接和基类打交道,不必为每个导出类编写特定类型的方法;向下转型,由于向上转型会丢失导出类的信息,向下转型的正确性有运行时类型识别(RTTI)作为保证。转型是通过多态来实现的。
class Useful{
public void f(){}
public void g(){}
}
class MoreUseful extends Useful{
public void f(){}//覆盖基类
public void g(){}//覆盖基类
public void u(){}
public void v(){}
public void w(){}
}
public class casting{
public static void main(String[] args){
Useful[] x = {
new Useful(),
new MoreUseful(),//向上转型
};
x[0].f();
x[1].g();
//x[1].u();//向上转型丢失了信息(u方法),
((MoreUseful)x[1]).u();//向下转型
((MoreUseful)x[0]).u();//向下转型先要向上转型,否则运行会抛出ClassCastException
}
}
4. 多态的缺陷
1、试图覆盖私有方法,private方法是隐式的final方法,故不能覆盖,同前面final带来的混淆一样,
2、域与静态方法
只有方法才有多态,域没有多态。若导出类可以访问基类的域,且导出类和基类的域同名,那么在导出类中直接访问域时,访问在编译期就进行解析,而不是运行时确定,故没有多态性,在导出类中实际上是有两个域,一个是它自己的,一个是基类的。 实际中不会发生,因为域一般是private的,且导出类的域也不会和基类的域同名。
静态方法属于类,肯定不会有多态。
class Super{
public int field = 0;
public int getField(){
return field;
}
}
class Sub extends Super{
public int field = 1;
public int getField(){//覆盖基类方法
return field;
}
public int getSuperField(){
return super.field;
}
}
public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub();//向上转型
System.out.println("sup.field="+sup.field+" ,sup.getField="+sup.getField());
Sub sub = new Sub();
System.out.println("sub.field="+sub.field+" ,sub.getField="+sub.getField()+" ,sub.getSuperField="+sub.getSuperField());
}
}
/**
* 输出
sup.field=0 ,sup.getField=1
sub.field=1 ,sub.getField=1 ,sub.getSuperField=0
*/
5. 构造器和多态
构造器显然不具有多态性,因为是隐式static方法,是属于类的。
在基类构造器内部调用一个多态的方法,结果将难于预料,因为在构造器内部,整个对象只是部分形成,只知道基类对象已经进行了初始化,导出类的对象没有初始化。
class Glyph{
Glyph(){
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
void draw(){
System.out.println("Glyph.draw()");
}
}
public class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int radius){
this.radius = radius;
System.out.println("RoundGlyph.RoundGlyph(), radius = "+radius);
}
void draw(){
System.out.println("RoundGlyph.draw(), radius = "+radius);
}
public static void main(String[] args) {
new RoundGlyph(5);
}
}
/**
* 输出
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*/
初始化的顺序:
(1)在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零(如果是引用,则为null,如果忘记初始化,所有东西都将是0)
(2)调用构造器,由于调用导出类的构造器第一步是调用基类构造器,所以从根类开始调用构造器,即先调用基类的构造器,由于多态,此时调用的draw方法是导出类的draw方法,由于步骤1,radius此时为0
(3)按照声明顺序调用成员的初始化方法
(4)调用导出类的构造器