面向对象的三大特征

说明: 文章根据个人学习《疯狂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修饰符,我们隐藏了nameage属性,程序无法直接操作该属性,必须通过事先约定好的setter方法,对属性进行操作,这就可以保证了数据的检验机制能够得到实现。

在这里,需要介绍一下访问修饰符的作用域:

1. private:成员变量和方法只能在类内被访问,具有类可见性

2. default: 成员变量和方法只能被同一个包里的类访问,具有包可见性。

3. protected:可以被同一个包中的类访问,被同一个项目中不同包中的子类访问

4. public:可以被同一个项目中所有的类访问,具有项目可见性,这是最大的访问权限

程序只能通过类本身定义的方法(settergetter)来对该类所实例化的对象进行数据的访问和处理。如果想对实例化的对象添加其它的一个方法和属性是不可能的,这就体现的类的封装性。

  

二、继承

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()),则编译时会出错。


上一篇:【错误记录】SeeMusic 一直卡在主界面无法使用 ( 删除 C:\Users\用户名称\AppData\LocalLow\Visual Music Design 应用信息 )


下一篇:Flex AIR应用换肤功能(Android和IOS)