1、封装
Thinking in java中说道,“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。
因此,我们可以这样来解释封装,字面上的意思就是包装的意思,专业一点就是信息隐藏,是指利用抽象数据类型将数据以及基于这些数据的操作封装在一起,成为一个不可分割的独立实体。
外界不能直接访问数据,只能通过包裹在数据之外的已授权的操作进行交流和交互。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的实现细节,只提供一些可以对其进行访问的公共的方式来与外部发生联系。
抽象数据类型(ADT)是指一个数学模型及定义在该模型上的一组操作。 事实上,抽象数据类型体现了程序设计中问题分解和信息隐藏的特征。它把问题分解为多个规模较小且容易处理的问题,然后把每个功能模块的实现为一个独立单元,通过一次或多次调用来实现整个问题。
对于封装而言,一个对象它所封装的就是自己的属性和方法,所以它不需要依赖任何对象就能完成自己的操作。
通常情况下,封装方式有两种:
- 将某一功能、属性抽离出来,独立写成单独的方法或类
- 设置访问权限,类:public(公共的) 、default(默认的,不写就默认是它);类中成员:public、protected、default(默认的)、private
封装的好处
- 减少耦合度,提高代码的复用性
- 隐藏信息,实现细节
- 类内部的结构可以自由修改。即让我们更容易修改类的内部实现,而无需修改使用了该类的客户代码。
首先我们来看下面这个类:Student.java
public class Student{
/*
* 对属性的封装
* 一个人的姓名、性别、年龄、妻子都是这个人的私有属性
*/
private String name ;
private String sex ;
private int age ;
/*
* setter()、getter()是该对象对外开发的接口
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
例如:有一天需要修改该类时,例如将age属性修改为String类型?倘若你只有一处使用了这个类还好,如果你有几十个甚至上百个这样地方,你是不是要改到崩溃?如果使用了封装,我们完全可以不需要做任何修改,只需要稍微改变下该类的setAge()方法即可。
//将age属性由int类型转变成String类型
public void setAge(int age) {
//转换即可
this.age = String.valueOf(age);
}
- 可以对成员进行更精确的控制例子
例如:如果有时候脑袋犯浑,不小心把年龄设置为300岁,麻烦就大了。但是使用封装我们就可以避免这个问题,我们对age的访问入口做一些控制(setter)如:
public void setAge(int age) {
if(age > 120){
System.out.println("ERROR:error age input...."); //提示錯誤信息
}else{
this.age = age;
}
}
2、继承
讲解之前我们先看一个例子
public class Student{
private String name ;
private String sex ;
private int age ;
private Teacher teacher;
}
public class Teacher{
private String name ;
private String sex ;
private int age ;
private Student student;
}
从这里我们可以看出,Student、Teacher两个类除了各自的Student、Teacher外其余部分全部相同,这样子就造成了重复的代码。尽可能地复用代码是程序员一直追求的,而继承就是复用代码的方式之一。
这个例子我们可以发现不管是学生还是老师,他们都是人,他们都拥有人的属性和行为,同时也是从人那里继承来的这些属性和行为的。
因此代码可以如下改进:
public class Person{
private String name ;
private String sex ;
private int age ;
}
public class Teacher extends Person{
private Student student;
}
public class Student extends Person{
private Teacher teacher;
}
可以看出这个例子使用继承后,除了代码量的减少我们还能够非常明显的看到他们的关系。
继承所描述的是“is-a”的关系,实际上继承者是被继承者的特殊化,它除了拥有被继承者的特性外,还拥有自己独有的特性。
例如猫有抓老鼠、爬树等其他动物没有的特性。同时在继承关系中,继承者完全可以替换被继承者,反之则不可以,例如我们可以说猫是动物,但不能说动物是猫就是这个道理,这样将猫看做动物称之为“向上转型”。
向上转型:将子类转换成父类,在继承关系上面是向上移动的,所以一般称之为向上转型。由于向上转型是从一个叫专用类型向较通用类型转换,所以它总是安全的,唯一发生变化的可能就是属性和方法的丢失。这就是为什么编译器在“未曾明确表示转型”活“未曾指定特殊标记”的情况下,仍然允许向上转型的原因。
诚然,继承定义了类如何相互关联,共享特性。对于若干个相同或者相识的类,我们可以抽象出他们共有的行为或者属相并将其定义成一个父类或者超类,然后用这些类继承该父类,他们不仅可以拥有父类的属性、方法还可以定义自己独有的属性或者方法。
同时在使用继承时需要记住三句话:
- 子类拥有父类非private的属性和方法。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
综上所述,使用继承确实有许多的优点,除了将所有子类的共同属性放入父类,实现代码共享,避免重复外,还可以使得修改扩展继承而来的实现比较简单。
组合和继承
组合和继承是两种复用代码的方法:
组合:只需要在新的类中产生现有类的对象,由于新的类是由现有类的对象组成的,所以这个方法称为组合,该方法只是复用了现有程序代码的功能,而并非它的形式。
继承:按照现有的类的类型进行创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新的代码,称之为继承。
总得来说,继承表达的是“is-a"(是一个)的关系,而组合表达的是“has-a”(有一个)的关系。
在面向对象编程中,生成和使用程序最有可能采用的方法就是直接将数据和方法包装进一个类中,并使用该类的对象。也可以运用组合技术使用现有类来开发新的类;而继承技术其实是不太常用的。
谨慎继承
上面讲了继承所带来的诸多好处,那我们是不是就可以大肆地使用继承呢?送你一句话:慎用继承。
首先我们需要明确,继承存在如下缺陷:
- 父类变,子类就必须变。
- 继承破坏了封装,对于父类而言,它的实现细节对与子类来说都是透明的。
- 继承是一种强耦合关系。
所以说当我们使用继承的时候,我们需要确信使用继承确实是有效可行的办法。那么到底要不要使用继承呢?《Thinking in java》中提供了解决办法:问一问自己是否需要从子类向父类进行向上转型。如果必须向上转型,则继承是必要的,但是如果不需要,则应当好好考虑自己是否需要继承。
多态
1、概念定义
一个引用变量究竟会指向哪一个实例对象,该引用变量发出的方法调用究竟是哪一个类的实现方法,必须由程序运行期间才能决定。
好处:不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
指向子类的父类引用:既能引用父类的共性,也能使用子类强大的实现。该引用既可以处理父类对象,也可以处理子类对象。当相同的消息发送给子类或者父类的对象时,该对象会根据自己所属的引用来执行不用的方法。
向上转型:只能使用父类的属性和方法。对于子类中存在而父类中不存在的方法,该方法是不能引用的,例如重载;对于子类重写父类的方法,调用这些方法时,使用子类定义的方法。
编译时多态(静态,运行时不是多态):重载
运行时多态(动态绑定,运行时多态):重写
2、多态的实现
2.1 实现条件(三个):继承、重写、向上转型(详讲)。
向上转型:由导出类转型为基类,即在多态中需要将子类的引用赋给父类对象(指向子类的父类引用),只有这样该引用才能够具备技能调用父类的方法和子类的方法。这是从较专用类型向较通用类型转换,所以总是很安全的。
例子:
public enum Note {
MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}
class Instrument {
public void play(Note n) {
print("Instrument.play()");
}
}
//继承
public class Wind extends Instrument {
// 重写:
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
}
public class Music {
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // 向上转型
}
}
/* Output:
* Wind.play() MIDDLE_C
*/
分析: Music.tune()方法接受一个Instrument引用,同时也接受任何导出自Instrument的类。在main方法中,当一个Wind引用传递到tune()方法时,就会出现这种情况,而不需要任何类型转换。这样做是允许的,因为Wind是从Instrument继承而来,所以Instrument的接口必定存在于Wind中。从Wind向上转型到Instrument可能会“缩小”接口,但不会比Instrument的全部接口更窄。
忘记对象类型
Music.Java看起来似乎有些奇怪。为什么所有人都故意忘记对象的类型呢?在进行向上转型时,就会产生这种情况;并且如果让tune()方法直接接受一个Wind引用作为自己的参数,似乎会更为直观。但这样引发的一个重要问题是:如果那样做,就需要创建多个tune()方法来接受不同类型的参数。假设按这种推理,现在再加入Stringed(弦乐)这种Instrument(乐器):
class Stringed extends Instrument {
public void play(Note n) {
print("Stringed.play() " + n);
}
}
public class Music2 {
public static void tune(Wind i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Stringed i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
Stringed violin = new Stringed();
tune(flute); //没有向上转型
tune(violin);
}
} /* Output:
Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
*/
这样做行得通,但是有一个主要缺点:必须为添加的每一个新Instrument类编写特定类型的方法,这意味着在开始时就需要更多的编程。
如果我们只写这样一个简单的方法,它仅接受基类作为参数,而不是那些特殊的导出类,这样做情况就会变得更好,也就是说,如果我们不管导出类的存在,编写的代码只是与基类打交道,是更好的方式。这也正是多态多允许的。
2.2 转机
运行这个程序后,我们便会发现Music.java的难点所在。
public static void tune(Instrument i){
//...
i.play(Note.MIDDLE_C);
tune()方法它接受一个Instrument引用。那么在这种情况下,编译器怎么样才能知道这个Instrument引用指向的是Wind对象,而不是Brass对象或Stringed对象呢?实际上编译器无法得知。为了理解这么问题,有必要说明下有关绑定的问题。
2.2.1 方法调用绑定
将一个方法调用同一个方法主体关联起来被称为绑定。若在程序执行前进行绑定,叫作前期绑定。在运行时根据对象的类型进行绑定,叫作后期绑定(也叫作动态绑定或运行时绑定)。
Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定,它会自动发生。
注意:只有非public的方法才可以被覆盖。只有在导出类中是覆盖了基类的方法这种情况时,才会有所谓的基类引用调用指向的导出类的方法。
为什么要将某个方法声明为final呢?它可以防止其他人覆盖该方法,但更重要的一点或许是:这样做可以有效地“关闭”动态绑定,或者说,告诉编译器不需要对其进行动态绑定。
2.2.2 产生正确的行为
一旦知道Java中所有方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只与基类打交道的代码了,并且这些代码对所有的导出类都可以正确运行。或者换一种说法,发送消息给某个对象,让该对象去断定应该做什么事。
面向对象程序设计中,有一个经典例子就是“几何形状”。这个例子中,有一个基类Sharp,以及多个导出类,如Circle、Square、Triangle等。
向上转型可以像下面这条语句这么简单:Sharp s = new Cicle();这里创建了一个Circle对象,并把得到的引用立即赋值给Sharp,这样做看似错误(将一种类型赋值给另一种类型);但实际上是没问题的,因为通过继承,Circle就是一种Shape。因此,编译器认可这条语句。
2.3 构造器和多态
通常,构造器不用于其他种类的方法。涉及到多态时仍是如此。尽管构造器并不具有多态性(它们实际上是static方法,只不过该static声明是隐式的),但还是非常有必要理解构造器怎样通过多态在复杂的层次结构中运作。
构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以便每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确的构造。
导出类只能访问它自己的成员,不能访问基类中的成员(基类成员通常是private类型)。只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此,必须令所有构造器都得到调用,否则就不可能正确构造完整对象。这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。
在导出类的构造器主体中,如果没有明确指定调用某个基类构造器,它就会“默默”调用默认构造器。如果不存在默认构造器,编译器就会报错(若某个类没有构造器,编译器会自动合成出一个默认构造器)。
class Meal {
Meal() { print("Meal()"); }
}
class Bread {
Bread() { print("Bread()"); }
}
class Cheese {
Cheese() { print("Cheese()"); }
}
class Lunch extends Meal {
Lunch() { print("Lunch()"); }
}
class PortableLunch extends Lunch {
PortableLunch() { print("PortableLunch()");}
}
public class Sandwich extends PortableLunch {
private Bread b = new Bread();
private Cheese c = new Cheese();
public Sandwich() { print("Sandwich()"); }
public static void main(String[] args) {
new Sandwich();
}
} /* Output:
* Meal()
* Lunch()
* PortableLunch()
* Bread()
* Cheese()
* Sandwich()
*/
这也表明了这一复杂对象调用构造器要遵照下面的顺序:
- 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。(防止如果在一个构造器的内部调用正在构造的某个对象的某个动态绑定方法,而这个方法所操作的成员可能还未初始化的灾难)
- 调用基类构造器。这个步骤会不断的反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最低层的导出类。
- 按声明顺序调用成员的初始化方法。
- 调用导出类构造器的主体。
2.4 实现方法
基于继承的多态:对于引用子类的父类类型,在处理该引用时,它适用于继承该父类的所有子类,子类对象的不同,对方法的实现也就不同,执行相同动作产生的行为也就不同。即当子类重写父类的方法被调用时,只有对象继承链中的最末端的方法才会被调用。
基于接口的多态: 在接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。
2.5 多态机制遵循的原则
当父类对象引用变量 引用 子类对象时,被引用对象的类型(子类)而不是引用变量的类型(父类)决定了调用谁的成员方法,但是这个被调用的方法必须是在父类中定义过的,也就是说被子类覆盖的方法(重写)。