1.封装
封装指将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
比如Person对象的age变量,只能随着岁月的流失,age才会增加,通常不能随意修改Person对象的age。对一个类或对象实现良好的封装,可以实现以下目的:
-
隐藏类的实现细节。
-
让使用者只能通过事先预定的方法来访问数据,从而可以在该方法中加入控制逻辑,限制对成员变量的不合理访问。
-
可进行数据检查,从而有利于保证对象信息的完整性。
-
便于修改,提高代码的可维护性。
访问控制符
Java提供了3个访问控制符:private、protected、public来实现封装。这三个访问控制符分别代表了3个访问控制级别,另外还有一个不加任何访问控制符的访问控制级别。如下表所示:
private | default | protected | public | |
---|---|---|---|---|
同一个类中 | √ | √ | √ | √ |
同一个包中 | √ | √ | √ | |
子类中 | √ | √ | ||
全局范围内 | √ |
关于访问控制符的使用,存在如下几条基本原则:
- 类里的绝大部分成员变量都应该使用private修饰,只有一些static修饰的、类似全局变量的成员变量,才可能考虑使用public修饰。除此之外,有些方法只用于辅助实现该类的其他方法,这些方法被称为工具方法,工具方法也应该使用private修饰。
- 如果某个类主要用做其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰这些方法
- 希望暴露出来给其他类自由调用的方法应该使用public修饰。因此,类的构造器通过使用public修饰,从而允许在其他地方创建该类的实例。因为外部类通常都希望被其他类自由使用,所以大部分外部类都是用public修饰。
2.继承
继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Java语言摒弃了C++中难以理解的多继承特征,Java的继承具有单继承的特点,每个子类只有一个直接父类。
Java里子类继承父类的语法格式如下:
修饰符 class SubClass extends SuperClass
{
// 类定义部分
}
2.1 重写父类方法
举一个形象的例子:鸟类都包含了飞翔方法,其中鸵鸟是一种特殊的鸟类,因此鸵鸟应该是鸟的子类,因此它也将从鸟类获得飞翔方法,但这个飞翔方法明显不适合鸵鸟,为此,鸵鸟需要重写鸟类的方法。
下面程序先定义一个Bird类。
public class Bird
{
// Bird类的fly()方法
public void fly()
{
System.out.println("我在天空里自由自在地飞翔...");
}
}
下面再定义一个Ostrich类,这个类扩展了Bird类,重写了Bird类的fly()方法。
public class Ostrich extends Bird
{
// 重写Bird类的fly()方法
public void fly()
{
System.out.println("我只能在地上奔跑...");
}
public static void main(String[] args)
{
// 创建Ostrich对象
Ostrich os = new Ostrich();
// 执行Ostrich对象那个的fly()方法,将输出"我只能在地上奔跑..."
os.fly();
}
}
这种子类包含与父类同名方法的现象被称为方法重写(Override),也被称为方法覆盖。
方法的重写需要遵循“两同两小一大”规则:
两同即方法名相同、形参列表相同;
两小即子类方法返回值类型比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
一大即子类方法的访问权限应比父类方法的访问权限更大或相等。
尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。
2.2继承与组合
继承是实现类复用的重要手段,但继承带来了一个最大的坏处:破坏封装。相比之下,组合也是实现类复用的重要方式,而采用组合方式来实现类复用则能提供更好的封装性。下面将详细介绍继承和组合之间的联系与区别。
2.2.1 使用继承的注意点
子类扩展父类时,可以从父类继承得到成员变量和方法,如果访问权限允许,子类可以直接访问父类的成员变量和方法,相当于子类可以直接复用父类的成员变量和方法。确实非常方便。
继承带来了高度复用的同时,也带来了一个严重的问题:**继承严重破坏了父类的封装性。**在继承关系中,子类可以直接访问父类的成员变量(内部信息)和方法,从而造成子类和父类的严重耦合。子类可以改变父类方法的实现细节(例如,通过方法重写的方式改变父类方法实现),从而导致子类可以恶意篡改父类的方法。
为保证父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则:
- 尽量隐藏父类的内部数据。尽量把父类的所有成员变量都设置成private访问类型,不要让子类直接访问父类的成员变量。
- 不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,则必须以public修饰,但又不希望子类重写该方法,可以使用final修饰符来修饰该方法;如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则可以使用protected来修饰该方法。
- 尽量不要在父类构造器调用将要被子类重写的方法。
注意:如果父类构造器调用了被其子类重写的方法,则变成调用被子类重写后的方法。
如果想把某些类设置成最终类,即不能被当成父类,则可以使用final修饰这个类,例如JDK提供的java.lang.String类和java.lang.System类。除此之外,使用private修饰这个类的所有构造器,从而保证子类无法调用该类的构造器,也就无法继承该类。对于把所有的构造器都是用private修饰的父类而言,可另外提供一个静态方法,用于创建该类的实例。
到底何时需要从父类派生新的子类呢?不仅需要保证子类是一种特殊的父类,而且需要具备以下两个条件之一。
- 子类需要额外增加属性,而不仅仅是属性值的改变。
- 子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。
2.2.2 利用组合实现复用
组合是把旧类对象作为新类的成员变量组合进来,用以实现新类的功能,用户看到的是新类的方法,而不能看到被组合对象的方法。因此,通常需要在新类里使用private修饰被组合的旧类对象。
到底该用继承?还是该用组合呢?
继承是对已有的类做一番改造,以此获得一个特殊的版本。简言之,就是将一个较为抽象的类改造成能使用于某些特定需求的类。比如Wolf和Animal的关系,用继承更能表达其现实意义。反之,如果两个类之间有明确的整体、部分的关系,例如Person类需要复用Arm类的方法,此时就应该采用组合关系来实现复用。
**总之,继承要表达的是一种“是(is-a)”的关系,而组合表达的是“有(has-a)”的关系。
##3.多态
Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态 (Polymorphism)。
下面看一个例子:
class BaseClass
{
public int book = 6;
public void base()
{
System.out.println("父类的普通方法");
}
public void test()
{
System.out.println("父类的被覆盖的方法");
}
}
public class SubClass extends BaseClass
{
// 重新定义一个book实例变量隐藏父类的book实例变量
public String book = "海底两万里";
public void test()
{
System.out.println("子类的覆盖父类的方法");
}
public void sub()
{
System.out.println("子类的普通方法");
}
public static void main(String[] args)
{
//下面编译时类型和运行时类型不一样,多态发生
BaseClass ploymophicBc = new SubClass();
// 输出6——表明访问的是父类对象的实例变量
System.out.println("ploymophicBc.book");
// 下面调用将执行从父类继承到的base()方法
ploymophicBc.base();
// 下面调用将执行当前类的test()方法
ploymophicBc.test();
// 因为ploymophicBc的编译时类型是BaseClass
// BaseClass类没有提供sub()方法,所以下面代码编译时会出现错误
// ploymophicBc.sub();
}
}
上面程序的main()方法中显式创建的引用变量ploymophicBc的编译时类型是BaseClass,而运行时类型是SubClass,当调用该引用变量的test()方法(BaseClass类中定义了该方法,子类SubClass覆盖了父类的该方法)时,实际执行的是SubClass类中覆盖的test()方法,这就可能出现多态了。
因为子类其实是一种特殊的父类,因此Java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,或者被称为向上转型(upcasting),向上转型由系统自动完成。
当把一个子类对象直接赋给父类引用变量时,例如上面的BaseClass ploymophicBc = new SubClass();
这个ploymophicBc引用变量的编译时类型是BaseClass,而运行时类型是SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。
上面代码注释了ploymophicBc.sub();
会在编译时引发错误。虽然ploymophicBc引用变量实际上确实包含sub()方法(例如,可以通过反射来执行该方法),但因为它的编译时类型为BaseClass,因此编译时无法调用SubClass类的sub()方法。
**注意:**引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。因此,编写Java程序代码时,引用变量只能调用声明该变量时所用类里包含的方法。例如,通过
Object p = new Person();
代码定义一个变量p,则这个p只能调用Object类的方法,而不能调用Person类里定义的方法。
与方法不同的是,对象的实例变量则不具备多态性。比如上面的ploymophicBc引用变量,程序中输出它的book实例变量时,并不是输出SubClass类里定义的实例变量,而是输出BaseClass类的实例变量。
**注意:**通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量。