5-面向对象(上)

封装

封装 指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象中的信息,而是通过该类所提供的方法来实习对内部信息的操作和访问。实现以下目的:

  • 隐藏类的实现细节。
  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问。
  • 可进行数据检查,从而有利于包装对象的完整性。
  • 便于修改。

继承

继承 是一种实现代码复用的手段。Java 的类只允许单继承,每个子类只能有一个直接父类。

方法覆盖 要遵循 “两同两小一大” 规则,”两同” 即方法名相同、形参列表相同;’’两小” 即子类方法返回值类型要比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应该比父类方法声明抛出的异常类更小或相等;”一大” 指子类方法的访问权限应该比父类方法的访问权限更大或相等。

子类中定义与父类中同名的实例变量不会完全覆盖父类中定义的实例变量,它只是简单地隐藏了父类中的实例变量,可以通过 super 作为限定来进行调用父类的实例变量。

当创建一个子类时,系统不仅会为该类中定义的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存,即使子类定义了与父类中同名的实例变量。

多态

多态 是指在程序运行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

通俗的讲,只通过父类就能够引用不同的子类,或者通过接口就能引用不同的实现类,这就是多态。只有在运行的时候才能知道引用变量所指向的具体实例对象。

对于 A a = new B(),在编译时 a 的类型为 A,因此编译器将不允许调用 B 中没有在 A 中定义的方法。另一方面,运行时 a 的实际类型是 B。这就是多态的本质。如果 B 覆盖了 A 中的方法(比如,一个名叫 play() 的方法),那么调用 a.play() 将导致调用 B (而不是 A) 中 play() 的实现。多态使得在调用方法时对象(指 a 引用的那个)能决定选择哪一个方法实现(或是 A 中的那个,或是 B 中的那个)。

通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量。

初始化顺序

父类静态变量 -> 父类静态代码块 -> 子类静态变量 -> 子类静态代码块 -> 父类非静态变量 -> 父类非静态代码块 -> 父类构造器 -> 子类非静态变量 -> 子类非静态代码块 -> 子类构造器

this 和 super

一个方法含有 隐式参数(即调用该方法的对象) 和 显式参数(方法的形参列表)。

this 有两种用法:

  • 关键字 this 引用方法的隐式参数,即调用该方法的对象。
  • 如果构造器的 第一个语句 形如 this(…) ,这个构造器将调用同一个类的另一个构造器。

super 也有两种用法:

  • 关键字 super 用来调用超类的方法。
  • 作为子类构造器的第一条语句来调用父类构造器。

方法详解

Java程序总是 按值调用,就是说,方法的参数传递方式只有一种:按值传递。就是将实际参数值的副本传入方法内。如果传入的是基本数据类型,那么传递的是数据的一个拷贝;如果传递的是一个引用类型,那么传递的是该引用所指向的对象地址。特别指出的是,方法不能修改传递给它的任何参数变量的内容。

总结一下 Java 中方法参数的使用:

  • 一个方法不能修改一个基本类型数据的参数(即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

成员变量和局部变量


成员变量无须显式初始化,但局部变量必须要显式初始化。

变量的初始化和内存中的运行机制

当系统加载类或创建该类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值。

局部变量保存在其所在方法的栈内存中,如果局部变量是一个基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;如果局部变量是一个引用类型的变量,则这个变量里存放的是实际引用的对象或数组的堆内存地址。局部变量随方法或代码块的运行结束而结束。

引用变量的强制类型转换

在多态中,引用变量只能调用它编译时类型的方法,而不能调用它运行时类型的方法,即使它实际所引用的对象确实包含该方法。如确实需要让这个引用变量调用它运行时类型的方法,则必须把它强制类型转换成运行时类型,强制类型转换需要借助于类型转换运算符。

类型转换运算符的用法是:(type)variable,这种用法可以将 variable 变量转换成一个 type 类型的变量。类型转换运算符除了可以将一个基本类型变量转换成另一个类型外,还可以将一个引用类型变量转换成其子类类型。要注意的是:

  • 基本类型之间的转换只能在数值类型之间进行,包括整数型、字符型和浮点型。
  • 引用类型之间的转换只能在具有继承关系的两个类型之间进行。如果试图把一个父类实例转换成子类类型,则这个对象必须实际上是子类实例才行(即编译时类型为父类类型,而运行时类型是子类类型,即多态)。
1
2
3
4
//obj 变量在编译时类型为 Object,运行时类型为 String,Object 与 String
//存在继承关系,可以强制类型转换
Object obj="Hello";
String objStr=(String)obj;

考虑到进行强制类型转换时可能出现异常,因此进行类型转换之前应该先通过 instanceof 运算符来判断是否可以成功转换。

instanceof 运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类或接口,它用于判断前面的对象是否是后面的类,或其子类、实现类的实例。如果是,则返回 true。

instanceof 运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系。否则会引发编译错误。

1
2
3
4
5
6
7
>//声明 hello 时使用 Object 类,则 hello 的编译类型是 Object
>//Object 是所有类的父类,但 hello 变量的实际类型是 String
>Object hello="Hello";
>//String 与 Object 类存在继承关系,所以进行 instanceof 运算返回 true
>System.out.pritnln((hello instanceof Object));//返回 true
>System.out.pritnln((hello instanceof String));//返回 true
>

继承和组合

继承是实现类复用的重要手段,但继承带来了一个最大的坏处:破坏封装。相比之下,组合也是实现类复用的重要方式,而采用组合方式来实现类复用能提供更好的封装性。

继承之所以破坏了封装性是因为修饰符的不同
封装:通过公有化方法访问私有化属性,使得数据不容易被任意窜改,常用private修饰属性。
继承:通过子类继承父类从而获得父类的属性和方法,正常情况下,用protected修饰属性,专门用于给子类继承的,权限一般在本包下和子类里。
继承破坏了封装:是因为属性的访问修饰符被修改,使得属性在本包和子类里可以任意修改属性的数据,数据的安全性从而得不到保障。

为了包装父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则:

  • 尽量隐藏父类的内部数据。把父类的所有成员变量都用 private 修饰,不要让子类直接访问父类的成员变量。
  • 不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用 private 修饰,让子类无法访问;如果父类中的方法需要被外部类调用,但又不希望子类重写该方法,可以用 final 修饰;如果希望父类的某个方法被子类重写,但不希望被其他类访问,则使用 protected 修饰。
  • 尽量不要在父类构造器中调用将要被子类重写的方法。

初始化块

当创建对象时,系统总是先调用该类里定义的初始化块,如果一个类里定义了2个初始化块,则前面定义的先执行。对初始化块完成隐式执行后,再进行构造器的执行。

静态初始化块

静态初始化块用 static 修饰,系统将在类初始化阶段执行静态初始化块,而不是再创建对象时才执行。与普通初始化块类似的是,系统在类初始化阶段执行静态初始化块时,不仅会执行本类的静态初始化块,而且还会一直上溯到其父类的静态初始化块。