说明: 文章根据个人学习《疯狂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()),则编译时会出错。