一、修饰符
private 成员随时都是“私有”的,任何人不得访问。但在实际应用中,经常想把某些东西深深地藏起来,但同时允许访问衍
生类的成员。protected 关键字可帮助我们做到这一点。它的意思是“它本身是私有的,但可由从这个类继承的任何东西或者同一个包内的其他任何东西访问”
采取的最好的做法是保持成员的private 状态——无论如何都应保留对基 础的实施
细节进行修改的权利。在这一前提下,可通过protected 方法允许类的继承者进行受到控制的访问
final关键词
1、final修饰用来表示常量,分为编译期的常量(永不改变)、运行期的常量(每运行一次值会改变)
final可以修饰变量(基本数据变量,引用数据变量--即对象句柄,数组等),都必须显式的赋初值。final修饰对象句柄时,对象句柄初始地指向一个对象后,不能指向其他对象,但是对象本身可以改变。
package lesson12Final关键词;public class FinalData { // Can be compile-time constants 编译期常数 无论运行多少次,值永远不变 int i0; final int i1;//被final修饰的变量必须显示的指定初始值 {i1=9;} final static int i2=99; // Cannot be compile-time constants 不能是编译期间的常数,第二次运行时,值改变 final int i3=(int)(Math.random()*20); final static int i4=(int)(Math.random()*20); //final修饰对象句柄实时,必须初始化到一个具体的对象,永远不能指向另一个对象;但是对象本身可以改变 Value v1=new Value(); final Value v2=new Value(); final static Value v3=new Value(); //final Value v4;error 不指定初始值出错 // Arrays: final int[] a = { 1, 2, 3, 4, 5, 6 }; public static void main(String[] args) { FinalData data1=new FinalData(); //编译期的常量, //data1.i1++; error final基础数据不能重新赋值 System.out.println("i1:"+data1.i1); System.out.println("i2:"+i2); //运行期的常量 System.out.println("data1.i3:"+data1.i3); System.out.println("i4:"+i4); data1.v1=new Value(); //data1.v2=new Value();error 对象句柄不能重新赋值 int n=++data1.v2.i; //对象本身可以重新赋值 int m=++v3.i; System.out.println("n:"+n); for(int i = 0; i < data1.a.length; i++) data1.a[i]++;//对象本身可以重新赋值 FinalData data2=new FinalData(); //运行期的常量 System.out.println("data2.i3:"+data2.i3); System.out.println("i4:"+i4); }}
运行一次结果:
i1:9
i2:99data1.i3:16i4:18n:2data2.i3:4i4:18两个对象的i4的值一样的,是因为i4是static的,static变量只初始化一次
2、final修饰方法出于对两方面理由的考虑:
第一个是为方法“上锁”,防止任何继承类改变它的本来含义
第二个理由是程序执行的效率:
将一个方法设成final 后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个final 方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法【将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理】,相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受不到嵌入代码所带来的任何性能提升
3、final的一个重要用途是来定义宏变量,满足宏变量的充分必要条件是:
必须在定义final变量时初始化,而不是在构造器或者非静态代码块中初始化;
必须编译期就确定了初始值。
static关键词 见
二、合成与继承
合成经典的例子Car---合成一系列组件
因此合成是包含的关系,汽车包含了很多零部件,比如,门,窗,轮子等。
//: Car.java// Composition with public objectsclass Engine {public void start() {}public void rev() {}public void stop() {}}class Wheel {public void inflate(int psi) {}}class Window {public void rollup() {}public void rolldown() {}}class Door {public Window window = new Window();public void open() {}public void close() {}}public class Car {public Engine engine = new Engine();public Wheel[] wheel = new Wheel[4];public Door left = new Door(),right = new Door(); // 2-doorCar() {for(int i = 0; i < 4; i++)wheel[i] = new Wheel();}public static void main(String[] args) {Car car = new Car();car.left.window.rollup();car.wheel[0].inflate(72);}} ///:~
继承是属于的关系,比如汽车是“属于”车辆的,他是车辆的一个类别。
总结:实际中有些时候,需通过“合成”技术用现成的类来构造新类,而继承是最少见的一种做法。尽管继承在学习OOP 的过程中得到了大量的强调,但并不意味着应该尽可能地到处使用它。相反,使用它时要特别慎重。只有在清楚知道继承在所有方法中最有效的前提下,才可考虑它。为判断自己到底应该选用合成还是继承,一个最简单的办法就是考虑是否需要从新类上溯造型回基础类。若必须上溯,就需要继承。但如果不需要上溯造型,就应提醒自己防止继承的滥用。
三、java上溯造型和多形性
何谓绑定? 绑定是指将一个方法的调用和该方法所在的方法主体(类)关联起来。对于java而言,绑定分为静态(前期)绑定和动态(后期)绑定。静态绑定:在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。如:C语言动态绑定:在程序运行时根据具体对象的类型进行绑定。
如果一种语言实现了动态绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的具体类型,但方法调用机制能自行去调查,找到正确的方法主体。不同的语言对动态绑定的实现方法也是不同的。但至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。
public class Base { public int count=2; public void display(){ System.out.println("base display:"+this.count); } public void show(){ System.out.println("base show:"+this.count); } }public class Son extends Base{ int count=20; public void display(){ System.out.println("son display:"+this.count); }}public class Main { public static void main(String[] args) { // TODO Auto-generated method stub Base b=new Base(); System.out.println(b.count); b.display(); Son s=new Son(); System.out.println(s.count); s.display(); s.show(); Base bs=new Son(); System.out.println(bs.count); bs.display(); bs.show(); Base bs2; bs2=s; System.out.println(bs2.count); bs2.display(); bs.show(); }}运行结果:2base display:220son display:20base show:22son display:20base show:22son display:20base show:2
上面的程序中,不管是s,bs,bs2变量都是指向了一个Son类型,不管他们在声明时是什么类型,当通过这些变量调用方法时,方法的行为总是表现出他们实际类型的行为;但是通过这些变量访问他们的实例变量,这些实例变量的值总是表现出声明这些bi变量所用类型的行为。
总结: 在java中,几乎所有的方法都是后期绑定的,在运行时动态绑定方法属于子类还是基类。但是也有特殊,针对static方法和final方法由于不能被继承,因此在编译时就可以确定他们的值,他们是属于前期绑定的。特别说明一点,private声明的方法和成员变量不能被子类继承,所有的private方法都被隐式的指定为final的(由此可知,将方法声明为final类型的一是为了防止方法被覆盖,二是为了有效的关闭java中的动态绑定)。java中的后期绑定是有JVM来实现的,我们不用去显式的声明它,而C++则不同,必须明确的声明某个方法具备后期绑定。 java当中的向上转型或者说多态是借助于动态绑定实现的,所以理解了动态绑定,也就搞定了向上转型和多态。 对于java当中的方法而言,除了final,static,private和构造方法是前期绑定外,其他的方法全部为动态绑定。而动态绑定的典型发生在父类和子类的转换声明之下: 比如:Base b = new Son(); 具体过程如下: 1. 编译器检查对象的声明类型和方法名。假设我们调用x.f(args)方法,并且x已经被声明为C类的对象,那么编译器会列举出C类中所有的名称为f的方法和从C类的超类继承过来的f方法; 2. 接下来编译器检查方法调用中提供的参数类型。如果在所有名称为f 的方法中有一个参数类型和调用提供的参数类型最为匹配,那么就调用这个方法,这个过程叫做“重载解析”; 3. 当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同x所指向的对象的实际类型相匹配的方法版本。假设实际类型为D(C的子类),如果D类定义了f(String)那么该方法被调用,否则就在D的超类中搜寻方法f(String),依次类推。
运行时动态确定要调用的方法的版本,是根据字节码指令invokevirtual来完成的。需要从这个指令的"多态查找"过程说起。
invokevirtual的运行过程如下:
第一、找到操作数栈顶元素的实际类型,记做C,
第二、如果在C中找到与常量中描述符和简单名称都相同的方法,则进行权限校验,如果通过,则返回这个方法的直接引用,查找过程结束。权限校验不通过则返回java的illegalaccesserror。
第三、如果经过第二步没找到也没抛出异常,则按照继承关系在父类中查找。
第四、如果始终没有找到,则抛出abstractmethoderror。
由于invokevirtual指令在执行的第一步就是确定接收者的实际类型,所以调用的时候,针对不同的调用者(即所说的方法接收者)会把常量池中的类方法解析到不同的直接引用上,这个过程就是java语言“覆盖”父类方法的本质,也就是“重写”的本质。