一.封装
封装是指将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类提供的对外方法进行内部信息的操作和访问。
封装可以达到以下目的:
1)隐藏类的实现细节
2)让使用者只能通过事先预定的方法访问数据,从而可以在该方法了加入控制逻辑,限制对成员变量的不合理访问
3)可进行数据检查,从而有利于保证数据信息的完整性
4)便于修改,提高代码的可维护性。
为了实现良好的封装,需要从两方面考虑:
1)把对象的成员变量和实现细节隐藏起来,不允许外部直接访问
2)把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作
使用访问控制符修饰
Java提供了三个访问控制符:private,protected,public,分别代表了三个访问控制级别。还有一个不加任何访问控制符的访问控制级别。
private(当前类访问权限)→ default (包访问权限)→ protected (包内访问,子类访问权限)→ public(公开访问权限)
访问修饰符使用的基本原则:
1)绝大部分成员变量使用private修饰,,只有一些static修饰的,类似全局变量的成员变量才可能考虑public修饰。除此之外,有些方法只用于辅助实现该类的其他方法,这些方法成为工具方法,工具方法也应该使用private修饰。
2)如果某个类主要用作其他类的父亲,该类里绝大部分方法可能仅希望被其子类重写,而不想被外界直接调用,则应该使用
protected修饰这些方法。
3)希望暴露出来给其他类使用的方法应该使用public修饰。因此,类的构造器通过使用public修饰,从而允许在其他地方创建该类的实例。因为外部类通常希望被其他类自由调用,所以大部分外部类使用public修饰。
深入构造器
1)当程序员调用构造器时,该系统为该对象分配内存空间,这时对象就已经被创建了,只是这个对象不能被外部程序访问,只能在该构造器中通过this来引用
2)如果构造器B完全包含了构造器A的代码,如果通过new关键字来调用构造器,会导致程序重重新创建一个对象,但可以使用this调用相应的构造器,示例如下:
public class Apple{ public String name; public String color; public double weight; public Apple(){} public Apple(String name,String color){ this.name=name; this.color=color; } public Apple(String name,String color,double weight){ this(name,color); //通过this调用另一个重载构造器的初始化代码 this.weight=weight; } }
二.类的继承
Java通过extends关键字来实现类的继承,实现继承的类称为子类,被继承的类叫父类。有的也称其为超类,基类。父类和子类的关系是一种一般和特殊的关系。
因为子类是一种特殊的父类,所以父类的范围要比子类要大,例如水果类范围要比苹果大,所以可以认为父类是大类,子类是小类。
值得指出的是,Java的子类不能获得父类的构造器。
下面是子类继承父类的示范:
public class Fruit(){ public double weight; public void info(){ System.out.println("我是一个水果,重"+weight+"g"); } }
接下来定义Fruit类的子类,代码如下:
public class Apple extends Fruit(){ public static void main(String[] args){ Apple a=new Apple(); a.weight=56; a.info(); } }
上面程序,Apple基本是一个空类,只包含了一个main方法,创建了Apple对象之后,可以访问该对象的weight实例变量和info实例方法,这就是继承的作用,可以理解为继承了父类的遗产。
Java类只能有一个直接父类,但可以有多个甚至无限个间接父类,Oblect是所有类的父类,要么间接父类要么直接父类
重写(Override)父类的方法
子类扩展了父类,子类是一个特殊的父类,大部分时候,子类总是以父类为基础,额外增加了成员变量和方法。但有一种情况例外,子类根据自己的需要,重写父类的方法,重写也叫覆盖,两者是同一个概念。
方法的重写遵循“两同两小一大”的原则,两同指方法名相同,形参列表相同;两小指子类方法返回值类型比父类方法返回值类型更小或相等。子类方法声明抛出的异常类应比父类方法声明抛出的异常更小或相等;一大是子类方法比的访问权限应比父类的访问权限更大或相等。
一些注意点:
1)覆盖方法要么都是类方法要么都是实例方法。
2)子类覆盖了父类方法后,子类对象无法访问父类中被覆盖的方法,当可以在子类方法中调用,可以使用super(父类
实例)关键字或者父类类名或对象引用名作为调用者调用被覆盖的方法
3)如果父类方法具有private修饰,则子类无法访问和重写该方法。如果子类也定义了遵循“两同两小一大”规则的方法依旧不是重写,只能说是定义了一个新方法。
super限定
需要在子类中调用父类被覆盖的实例方法,则可以使用super限定调用父类被覆盖的方法。
super是Java提供的一个关键字,用于限定该对象调用它从父类继承的到的实例变量或方法。正如this不能出现在static修饰的方法中一样,super也不能出现在static修饰的方法中。
如果在构造器中使用super,则super用于限定该构造器初始化的是该对象从父类继承的到的实例变量,而不是该类自己定义的实例变量。
如果子类定义了和父类同名的实例变量,则会发生子类实例变量隐藏父类实例变量情形,这种情况下,子类直接访问实例变量默认访问到子类实例变量无法访问到父类被隐藏的实例变量。这时候,在子类的实例方法中可以使用super关键字来访问父类被隐藏的实例变量。
super调用父类的构造器
子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始化代码,类似于前面讲的一个构造器调用另一个重载的构造器。使用super调用父类构造器必须出现在子类构造器执行体的第一行,所以this调用和super调用不会同时出现。
子类调用父类构造器分以下几种情况:
1)子类构造器执行体在第一行使用super显示调用父类构造器,系统会根据super调用里传入的实参调用父类对应的构造器
2)子类构造器执行体的第一行代码使用this显示调用本类中重载的构造器,系统会根据this调用里传入的实参列表调用本类
对应的另一个构造器,执行本类另一个构造器即会调用父类构造器
3)子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参的构造器
三.多态
Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定。运用时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态(Polymorphism)。
先看以下程序:
class BaseClass { public int book = 6; public void base(){ System.out.println("父类的普通方法"); } public void test(){ System.out.println("父类被覆盖的方法"); } } class SubClass extends BaseClass { public String book = "轻量级JavaEE企业应用实战"; public void test(){ System.out.println("子类覆盖父类的方法"); } public void sub(){ System.out.println("子类的普通方法"); } public static void main(String[] args) { BaseClass bc = new BaseClass(); System.out.println(bc.book); bc.base(); bc.test(); System.out.println(); SubClass sc = new SubClass(); System.out.println(sc.book); sc.sub(); sc.test(); System.out.println(); BaseClass ploymophicBc = new SubClass(); //编译时类型与运行时类型不一致 System.out.println(ploymophicBc.book); ploymophicBc.base(); ploymophicBc.test();
// ploymophicBc.sub(); 编译会出错,无法调用 } }
输出:
---------- 运行java程序 ----------
6
父类的普通方法
父类被覆盖的方法
轻量级JavaEE企业应用实战
子类的普通方法
子类覆盖父类的方法
6
父类的普通方法
子类覆盖父类的方法
输出完成 (耗时 0 秒) - 正常终止
从上面可以看出,编译时类型与引用类型不一致时,调用的是子类覆盖后的方法。
因为子类其实是一种特殊的父类,因此Java允许把一个子类对象直接付给一个父类引用变量,无需任何类型转换,或者成为向上转型,由系统自动完成。
当把子类对象直接赋给父类引用变量时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这可能出现:相同的变量,调用同一个方法时呈现出不同的行为特征
引用变量的强制类型转换
编写Java程序时,引用变量只能调用编译时类型的方法,而不能调用运行时类型的方法,但它实际所引用的对象确实包含该方法。如果需要让这个引用变量调用它运行时类型的方法,则必须把他强制类型转换成运行时类型,强制类型转换格式是(type)valiable。
强制类型转换不是万能的,需要注意:
1)基本类型之间的转换只能在数值类型之间进行,包括整型,字符型,浮点型。不能与布尔型类型进行转换
2)引用类型之间的转换只能在具有继承关系的两个类型之间进行。如果没有继承关系,强制类型转换会编译出错
在进行强制类型转换之前,先用instanceof运算符判断是否可以成功转换,从而避免ClassCastException异常,这样可以使程序更加健壮。,用法如下:
if(objpri instanceof String){ String str = (String)objPri; }
四.继承与组合
继承和组合都是实现类复用的重要手段,但继承的一大坏处是破坏程序的封装性,采用组合来实现类复用有更好的封装性能。
为了保证父类有良好的封装性,不会被子类随意改变,设计父类通常遵循以下规则:
1)尽量隐藏父类的内部数据。尽量把父类的所有成员变量都设计成private访问类型,不要让子类直接访问父类的成
员变量。
2)不要让子类可以随意访问,修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问修饰符
控制修饰,让子类无法访问该方法;如果父类中的方法需要被外部调用,则必须以public修饰,但又不希望被子类
重写该方法,可以使用final修饰符来修饰该方法;如果希望父类的某个方法被子类重写,但不希望被其他类自由
访问,则可以使用protected来修饰该方法。
3)尽量不要在父类构造器中调用要被子类重写的方法。
如下程序:
class Base { public Base(){ test(); } public void test(){ System.out.println("被子类重写的方法"); } } class Sub extends Base { private String name; public void test(){ System.out.println("子类重写父类的方法,其name字符串长度"+name.length()); } public static void main(String[] args) { //这一行引发空指针异常 Sub s = new Sub(); } }
如果想把某些类设置成最终类,既不能当成父类,则可以使用final修饰这个类。除此之外,使用private修饰这个类的所有构造器,从而保证子类无法调用该类的构造器。也就无法继承该类。对于把所有的构造器都使用private修饰的父类,可另外提供一个静态方法,用于创建该类的实例。
到底何时需要派生新的子类呢?不仅保证子类是一种特殊的父类,而且需要具备以下两个条件之一。
1)子类需要额外增加属性,而不仅仅是属性值的改变
2)子类要增加自己独有的行为方式(包括增加新的方法和重写父类的方法)
到底该用继承还是改用组合呢?继承是对已有的类进行一番改造,以此获得特殊的版本,换而言之,就是将一个较为抽象的类改造成适用于某些特定需求的类。如果两个类是整体、部分的关系,就应该采用组合关系来实现复用,例如人和手臂。总之,继承要表达的是一种“is a”的关系。而组合表达的是(has a)的关系。