javascript中,成员(域和方法统称为成员)是没有访问限制的。而java中,无论是类还是成员都有一堆的修饰符去修饰它们,掐指一算,有 private、protected、public、static、final、abstract,一开始可能会理不清这些修饰符的含义和关系,特别是在涉及继承时,这些修饰符又会起到什么作用,今日得空稍作整理,并以此为出发点,再扯一扯java中面向对象的一些细节。
一、用public、default、private决定类和成员的访问权限
1.1、用public、default修饰类
java文件都是放在包里的,用public修饰的类可以在当前包和其它包中访问,而用default修饰的类(也就是没有修饰符的类)只可在当前包中访问。
1.2、用public、default、private修饰成员
public成员可在当前包和其它包中通过 对象/类.成员 访问;
default成员只可在当前包中通过 对象/类.成员 访问;
private成员只可在当前类中访问。
相同的修饰符,修饰类和修饰成员,其意义是不同的:
- 对于类来说,就是决定能不能从包外面看到包内的类。此处,包是分界点
- 对于成员来说,就是决定能不能从类的外面看到类的内部。此处,类是分界点
二、用static划分类的作用域
类有三层作用域:(1)最高一层,是用static修饰的成员所处的作用域;(2)其次一层,是没有用static修饰的成员所处的作用域;(3)最后一层,是对象方法内部的作用域。
概括起来就是:静态域、静态方法 → 实例域、对象方法 → 对象方法内部
低层作用域中可以访问高层作用域中的变量,反之则不能。所以对象方法的内部有关键字this表示隐式参数来指代对象本身,并且可以访问实例域和静态域;而静态方法的内部没有this,并且不能访问实例域只能访问静态域。
额外一点:
一个对象方法内部可以访问所属对象的私有数据,这毋庸置疑。但不仅仅如此,它还能访问所属类的所有对象的私有数据。这个特性会让人感到有点奇怪。
三、继承之时
3.1、同名方法覆盖,同名域共存
发生继承时,子类中同名同签名的方法会把超类的该方法覆盖掉,两者是不共存的;而子类中同名的域不会覆盖超类的该域,两者是共存的。
class Animal{
String name = "AnimalName";
}
class Person extends Animal {
String name ="PersonName";
}
Person a = new Person();
这是一个十分重要的特点,后面讲到多态和对象的强制类型转换就是根据这个特点来的。
而且这个特点也是如下要求的原因:
继承时,子类中与超类同名同签名的方法的访问权限不可比超类中该方法的访问权限低,而子类中与超类同名的域则没有这个要求。
3.2、用protected让子类可访问超类的私有成员
子类可以从超类继承private成员,并且这个成员也确实成为了子类成员中的一员,但是在子类中是无法直接访问这个private成员的,只能借助于超类的公有接口。
然而在子类中我就是想直接访问超类的成员,但是又不想把超类的这个成员设为public呢?一个折中的办法就是把超类的这个成员设为protected。protected成员的访问权限和default成员一样,只不过前者比后者多一个权限——可以在子类中直接访问。
在实际应用中,要谨慎使用protected,因为这使得子类可能直接依赖于超类的私有成员。超类的内部结构若作变动,则所有依赖直接操作超类私有数据的子类都可能需要作相应变动,这违背了数据封装原则。
3.3、多态的实现原理
class Animal {
String name = "animalName";
void howl(){
System.out.println("Wow");
}
}
class Person extends Animal{
String name = "personName";
void howl(){
System.out.println("Hi");
}
}
示例一:
Animal a = new Person();
a.howl(); //打印"Hi"
由于多态,a变量虽是Animal类型的,但是其实际上引用的是Person类型的对象,故 a.howl() 打印”Hi”。
示例二:
Animal a = new Person();
System.out.println(a.name); //打印"animalName"
Person p = (Person)a;
System.out.println(p.name); //打印"personName"
a变量自始至终保存了Person对象的所有成员,无论a变量再怎么经过强制类型转换,它的信息都不会减少或丢失。只不过当a变量属于某种类型时,只会对外展示该种类型的成员。
示例三:
Animal a = new Person();
a.howl(); //打印"Hi"
Animal a2 = (Animal)a;
a2.howl(); //还是打印"Hi"
这个就有点奇怪了,a.howl()打印”Hi”很好理解,为什么a2.howl()还是打印”Hi”呢?回到前面说过的一点:继承时,同名方法覆盖,同名域共存。所以此时a变量有两个同名的name域,却只有一个howl()方法,所以a变量企图通过强制类型转换,转换成Animal类型去调用Animal类型的howl()方法时,它其实调用的还是覆盖后的Person类型的howl()方法。
至此,多态的实现原理就可以看出来了。还是那句话:继承时,同名方法覆盖,同名域共存。 通过 Animal a = new Person(),a变量只能调用Animal类型具有的方法,但a变量实际上引用的是Person类型对象,a变量中也只有一个howl()方法,并且是覆盖后的Person类型的howl()方法,所以通过 a.howl(),a变量也只能调用Person类型的howl()方法。这就是动态绑定的实现经过了,是不是感觉这过程好傻阿,一点都称不上不动态,但就是这么神奇!
3.4、instanceof操作符
Animal a = new Person();
Animal a2 = (Animal)a;
System.out.println(a instanceof Animal); //true
System.out.println(a instanceof Person); //true
System.out.println(a2 instanceof Person); //true
可见 ,instanceof操作符跟变量的类型没有任何关系,而是检测变量实际引用的对象的类型。
3.5、强制类型转换
Animal a = new Animal();
p = (Person)a; //抛异常,java.lang.ClassCastException
强制类型转换的特点之前说过了,下面提两点规则:
- 只能在继承层次内进行类型转换
- a变量欲转换成Person类型,要求a变量引用的对象的类型必须是Person类型或其超类
3.6、用final阻止继承
- final修饰的域不可修改,final修饰的方法不可覆盖,final修饰的类不可被继承
- 若将一个类声明为final,则该类所有的方法自动成为final,但不包括域
3.7、用abstract声明抽象类
- 包含一个或多个抽象方法的类必须被声明为抽象的
- 类即使不含抽象方法,也可声明为抽象类
3.8、接口
- 接口中的所有方法自动属于public,因此在接口中声明方法时无需写修饰符
- 接口中的所有域自动属于public static final,故而也无需写修饰符
- 实现接口时,必须把方法声明为public
附录一、寻找一个类的顺序
在一个java文件中,我们使用了另一个java文件中的类,那在这个java文件中,是如何、按照什么顺序去寻找这个类的实现的呢?
- 通过类的全限定名去找
- 在只导入一个类的import语句中找
- 在当前包中找
- 在导入一个包的import语句中找
附录二、对象的初始化顺序
在new的时候会生成该类的一个实例,该实例的域都是有值的,显然是经过初始化的。java中有多种方式对域进行初始化,此时我们就需要知道他们各自相对的执行顺序:
- 所有数据域被初始化为默认值(0、false、null)
- 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块
- 不仅可用常量对域初始化,也可调用方法进行初始化
- 初始化块分普通初始化块和静态初始化块
- 执行调用的构造函数(若是调用默认构造函数则没有这一步)
附录三、java面向对象编程的几点注意
- 只有在没有手动添加任何构造器的情况下,系统才会自动添加一个默认构造器
- 子类构造函数的第一句必须是调用超类的构造函数,若没有手动调用,则将自动调用超类的默认构造函数
- 不要编写返回引用类型的get方法,最好加个克隆
- final修饰符大都应用于基本类型域或不可变类的域,而不要用于引用类型
参考书籍:
《Java核心技术 卷I》
第4章 对象与类
第5章 继承
第6章 接口与内部类
参考连接:
Java继承中属性、方法和对象的关系