说明: 文章根据个人学习《疯狂java讲义》及《疯狂Java:突破程序员基本功的16课》后,学习整理而来,其中部分代码直接使用原文示例:
说起面向对象的三大特性:封装、继承和多态,知道点面向对象的人可谓是耳熟能详,但其中还是有很多知识点、细节需要仔细的品味一番,才能实的掌握这三大特性,为以后理解程序,编写精致的代码提供基本的保证。
一、封装
封装性就是把类(对象)的属性和行为结合成一个独立的相同单位,并尽可能隐蔽类(对象)的内部细节。
封装的特性使得类(对象)以外的部分不能随意存取类(对象)的内部数据(属性),保证了程序和数据不受外部干扰且不被误用。
说的简单点,封装就是使用private修饰符来修饰类中的属性或方法,这样,该属性就不可以被对象直接访问。而如果想访问该属性,程序需提供一个约定好的public方法,如此便可以操作该私有属性,这样保证了数据的安全性。
已知一个类Person,该类的属性和方法如下表所示(示例源自《疯狂java讲义》):
public class Person {
// 将Field使用private修饰,将这些Field隐藏起来
private String name;
private int age;
// 提供方法来操作name Field
public void setName(String name) {
// 执行合理性校验,要求用户名必须在2~6位之间
if (name.length() > 6 || name.length() < 2) {
System.out.println("您设置的人名不符合要求");
return;
} else {
this.name = name;
}
}
public String getName() {
return this.name;
}
// 提供方法来操作age Field
public void setAge(int age) {
// 执行合理性校验,要求用户年龄必须在0~100之间
if (age > 100 || age < 0) {
System.out.println("您设置的年龄不合法");
return;
} else {
this.age = age;
}
}
public int getAge() {
return this.age;
}
}
测试该Person类
public class PersonTest {
public static void main(String[] args) {
Person p = new Person();
// 因为age Field已被隐藏,所以下面语句将出现编译错误。
// p.age = 1000;
// 下面语句编译不会出现错误,但运行时将提示"您设置的年龄不合法"
// 程序不会修改p的age Field
p.setAge(1000);
// 访问p的age Field也必须通过其对应的getter方法
// 因为上面从未成功设置p的age Field,故此处输出0
System.out.println("未能设置age Field时:" + p.getAge());
// 成功修改p的age Field
p.setAge(30);
// 因为上面成功设置了p的age Field,故此处输出30
System.out.println("成功设置age Field后:" + p.getAge());
// 不能直接操作p的name Field,只能通过其对应的setter方法
// 因为"李刚"字符串长度满足2~6,所以可以成功设置
p.setName("李刚");
System.out.println("成功设置name Field后:" + p.getName());
}
}
在上述示例中,我们可以看到,通过private修饰符,我们隐藏了name和age属性,程序无法直接操作该属性,必须通过事先约定好的setter方法,对属性进行操作,这就可以保证了数据的检验机制能够得到实现。
在这里,需要介绍一下访问修饰符的作用域:
1. private:成员变量和方法只能在类内被访问,具有类可见性
2. default: 成员变量和方法只能被同一个包里的类访问,具有包可见性。
3. protected:可以被同一个包中的类访问,被同一个项目中不同包中的子类访问。
4. public:可以被同一个项目中所有的类访问,具有项目可见性,这是最大的访问权限
程序只能通过类本身定义的方法(setter和getter)来对该类所实例化的对象进行数据的访问和处理。如果想对实例化的对象添加其它的一个方法和属性是不可能的,这就体现的类的封装性。
二、继承
1. 继承是实现代码复用的重要手段,通过继承扩展了父类的功能。Java的继承具有单继承的特点,即只能继承自一个父类,每个子类只有一个直接父类,但是其父类又可以继承于另一个类,从而实现了子类可以间接继承多个父类,但其本质上划分仍然是一个父类和子类的关系。
2. Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类称为父类,父类和子类的关系,是一种一般和特殊的关系。就像是水果和苹果的关系,苹果继承了水果,苹果是水果的子类,水果是苹果的父类,则苹果是一种特殊的水果。
3. 创建子类一般形式如下:
class 类名 extends 父类名{
子类体
}
4. 子类与父类的变量、方法关系
子类可以继承父类的所有非私有特性(成员变量、方法)。对于被private修饰的类成员变量或方法,其子类是不可见的,也即不可访问。
子类中可以声明与父类同名的成员变量,这时父类的成员变量就被隐藏起来了,在子类中直接访问到的是子类中定义的成员变量。(此处需记得是隐藏了父类的成员变量,在子类中分配内存空间时,会为其分配一个内存空间,存放的是父类的变量)。
子类中也可以声明与父类相同的成员方法,包括返回值类型、方法名、形式参数都应保持一致,称为方法的覆盖。
如果在子类中需要访问父类中定义的同名成员变量或方法,需要用的关键字super。
Java中通过super来实现对被隐藏或被覆盖的父类成员的访问。super 的使用有三种情况:
1) 访问父类被隐藏的成员变量和成员方法;
super.成员变量名;
2) 调用父类中被覆盖的方法,如:
super.成员方法名([参数列]);
3) 调用父类的构造函数,如:
super([参数列表]);
super( )只能在子类的构造函数中出现,并且永远都是位于子类构造函数中的第一条语句。
举例:
实例一:
class BaseClass {
public double weight;
public void info() {
System.out.println("我的体重是" + weight + "千克");
}
}
public class SubClass extends BaseClass {
public static void main(String[] args) {
// 创建SubClass 对象
SubClass sc = new SubClass();
// SubClass 本身没有weight属性,但是SubClass 的父类有weight属性,也可以访问SubClass 对象的属性
sc.weight = 56;
// 调用SubClass 对象的info()方法
sc.info();
}
}
子类SubClass继承自父类BaseClass,则它继承了父类的非私有属性和方法(weight和info()),此时,便可以在子类中直接访问weight和info(),因为它们都已经被SubClass继承过来,属于了SubClass。
实例二:
class Animal {
String name="animal";
int age;
public void move(){
System.out.println("animal move");
}
}
class Dog extends Animal{
String name="dog"; //隐藏了父类的name属性;
float weight; //子类新增成员变量
public void move(){ //覆盖了父类的方法move()
super.move(); //用super调用父类的方法
System.out.println("Dog Move");
}
}
public class InheritDemo{
public static void main(String args[]){
Dog d=new Dog();
d.age=5;
d.weight=6;
System.out.println(d.name+" is"+d.age+" years old");
System.out.println("weight:"+d.weight);
d.move();
}
}
运行结果:
dog is5 years old
weight:6.0
animal move
Dog Move
在继承过程中,如果子类拥有和父类同名的属性,则父类的这个属性是被隐藏了起来,在子类中分配内存空间时,会为其分配一个内存空间,存放的是父类的变量;如果子类中声明了与父类相同的成员方法,包括返回值类型、方法名、形式参数都应保持一致,称为方法的覆盖,方法覆盖后,子类的对象将无法访问父类中呗覆盖的方法,但可以在子类中通过super调用父类被覆盖的方法。
举例三:
class SuperClass {
SuperClass() {
System.out.println("调用父类无参构造函数");
}
SuperClass(int n) {
System.out.println("调用父类有参构造函数:" + n);
}
}
class SubClass extends SuperClass {
SubClass(int n) {
System.out.println("调用子类有参构造函数:" + n);
}
SubClass() {
super(200);
System.out.println("调用子类无参构造函数");
}
}
public class InheritDemo2 {
public static void main(String arg[]) {
SubClass s1 = new SubClass();
SubClass s2 = new SubClass(100);
}
}
程序运行结果:
调用父类有参构造函数:200
调用子类无参构造函数
调用父类无参构造函数
调用子类有参构造函数:100
由以上程序可以得出结论: 在对象的实例化过程中,无论使用不使用super,程序都会先调用父类的构造器初始化代码; 子类调用父类构造器的几种情况:
1. 子类通过super显示调用父类构造器,此时根绝super里的参数决定调用哪个构造器;
2. 子类通过this调用本类中重载的构造器,系统根据this调用里传入的实参决定调用本类中的哪个构造器。
3. 子类中无super,也无this,将默认调用父类无参构造器。
三、多态
Java的引用变量有两种类型:编译时类型和运行时类型;
编译类型由生命该变量时所用的类型决定,运行时类型由实际赋给该变量的对象决定。
如果编译类型和运行时类型不一致,则会发生多态。
举例1:
class Animal {
public void eat() {
System.out.println("animal eat");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("Dog eat bone");
}
}
class Cat extends Animal {
public void eat() {
System.out.println("Cat eat fish");
}
}
public class PloyDemo {
public static void main(String args[]) {
Animal a;
a = new Animal(); // 编译时类型和运行时类型完全一样,因此不存在多态
a.eat();
a = new Dog(); // 下面编译时类型和运行时类型不一样,多态发生
a.eat();
a = new Cat(); // 下面编译时类型和运行时类型不一样,多态发生
a.eat();
}
}
程序运行结果:
animal eat
Dog eat bone
Cat eat fish
实例2:
class SuperClass {
public int book = 6;
public void base() {
System.out.println("父类的普通方法base()");
}
public void test() {
System.out.println("父类中将被子类覆盖的方法");
}
}
public class PloymorphismTest001 extends SuperClass {
// 重新定义一个book实例属性,覆盖父类的book实例属性
public String book = "Java疯狂讲义";
public void test() {
System.out.println("子类中覆盖父类的方法");
}
private void test1() {
System.out.println("子类中普通的方法");
}
// 主方法
public static void main(String[] args) {
// 下面编译时类型和运行时类型完全一样,因此不存在多态
SuperClass sc = new SuperClass();
System.out.println("book1= " + sc.book);// 打印结果为:6
// 下面两次调用将执行SuperClass的方法
sc.base();
sc.test();
// 下面编译时类型和运行时类型完全一样,因此不存在多态
PloymorphismTest001 pt = new PloymorphismTest001();
System.out.println("book2= " + pt.book); // 打印结果为:Java疯狂讲义
// 下面调用将执行从父类继承到的base方法
pt.base();
// 下面调用将执行当前类的test方法
pt.test();
// 下面编译时类型和运行时类型不一样,多态发生
SuperClass sscc = new PloymorphismTest001();
// 结果表明访问的是父类属性
System.out.println("book3= " + sscc.book);// 打印结果为:6
// 下面调用将执行从父类继承到得base方法
sscc.base();
// 下面调用将执行当前类的test方法
sscc.test();
// 因为sscc的编译类型是SuperClass,SuperClass类没有提供test1()方法
// 所以下面代码编译时会出现错误
// sscc.test1();
}
}
程序运行结果为:
book1= 6
父类的普通方法base()
父类中将被子类覆盖的方法
book2= Java疯狂讲义
父类的普通方法base()
子类中覆盖父类的方法
book3= 6
父类的普通方法base()
子类中覆盖父类的方法
总结上述示例,可发现:
1. 当程序的编译时类型和运行时类型完全一样时,不存在多态,系统依然调用本类中的方法。
2. 当程序的编译时类型和运行时类型不一样,多态发生; 此处,如果对象访问的是属性,则程序根据编译时类型决定要访问的属性; 如果对象访问的是方法,则程序根据运行时类型决定要访问的方法。
3. 如果程序的编译类型是SuperClass,但SuperClass类没有提供父类中的某个方法,比如(test1()),则编译时会出错。