第四章面向对象(上)

4.1类和对象

4.1.1定义类

定义类的简单语法:

[修饰符] class 类名
{
零个到多个构造器定义。。。
零个到多个成员变量。。。
零个到多个方法。。。
}

定义成员变量的语法格式如下:

[修饰符] 类型 成员变量名 [= 默认值];

对定义成员变量语法格式的详细说明如下。

  • 修饰符:修饰符可以省略,也可以是public、protected、private、static、final,其中public、protected、private三个最多只能出现其中之一,可以与static、final组合起来修饰成员变量。
  • 类型:类型可以是Java语言允许的任何数据类型,包括基本类型和现在介绍的引用类型。
  • 成员变量名:成员变量名只要是一个合法的标识符即可,但这只是从语法角度来说的;如果从程序可读性来看,成员变量名应由一个或多个有意义的单词连缀而成,第一个单词首字母小写,后面每个单词首字母大写,其他字母全部小写,单词与单词之间不要使用任何分隔符。成员变量用与描述类或对象包括的状态数据,因此成员变量名应建议使用英文名词。
  • 默认值:定义成员变量还可以指定一个可选的默认值。
    定义方法的语法格式如下:
[修饰符] 方法返回值类型 方法名(形参列表)
{
//由零条到多条可执行性语句组成的方法体
}

对定义方法语法格式的详细说明如下。

  • 修饰符:修饰符可以省略,也可以是public、protected、private、static、final、abstract,其中public、protected、private三个最多只能出现其中之一;abstract和final最多只能出现其中之一,它们可以与static组合起来修饰方法。
  • 方法返回值类型:返回值类型可以是Java语言允许的任何数据类型,包括基本类型和引用类型;如果声明了方法值类型,则方法体内必须由一个有效的return语句,该语句返回一个变量或一个表达式,这个变量或者表达式的类型必须与此处声明的类型匹配。除此之外,如果一个方法没有返回值,则必须使用void来声明没有返回值。
  • 方法名:方法名的命名规则与成员变量的命名规则相同,但由于方法用于描述该类或该类的实例的行为特征或功能实现,因此通常建议方法名以英文动词开头。
  • 形参列表:形参列表用于定义该方法可以接受的参数,形参列表由零组到多组“参数类型 形参名”组合而成,多组参数之间以英文逗号(,)隔开,形参类型和形参名之间以英文空格隔开。一旦在定义方法时指定了形参列表,则调用该方法时必须传入对应的参数值——谁调用方法,谁负责为形参赋值。

定义构造器的语法格式如下:

[修饰符] 构造器名(形参列表)
{
//由零条到多条可执行性语句组成的构造器执行体
}

定义构造器语法格式的详细说明如下:

  • 修饰符:修饰符可以省略,也可以是public、protected、private其中之一。
  • 构造器名:构造器名必须和类名相同。
  • 形参列表:和定义方法形参列表的格式完全相同。

Java类大致有如下作用:

  • 定义变量
  • 创建对象
  • 调用类的类方法或访问类的类变量。
4.1.2对象的产生和使用

创建对象的根本途径是构造器,通过new关键字来调用某个类来调用某个类的构造器即可创建这个类的实例。
例:

Person p = new Person();

Java的对象大致有如下作用:

  • 访问对象的实例变量
  • 调用对象的方法
4.1.3对象、引用和指针

与前面介绍的数组类型类似,类也是一种引用数据类型,因此程序中定义的Person类型的变量实际上是一个引用,它被存放在栈内存里,指向实际的Person对象;而真正的Person对象则存放在堆(heap)内存中。
如果希望通知垃圾回收机制回收某个对象,只需切断该对象的所有引用变量和它之间的关系即可,也就是把这些引用该变量赋值为null。

4.1.4对象的this引用

Java提供了一个this关键字,this关键字总是指向调用该方法的对象。根据this出现位置的不同,this作为对象的默认引用有两种情形。

  • 构造器中引用该构造器正在初始化的对象。
  • 在方法中引用调用该方法的对象。

对于static修饰的方法而言,则可以使用类来直接调用该方法,如果在static修饰的方法中使用this关键字,则这个关键字就无法指向合适的对象。所以,static修饰的方法中不能使用this引用。由于static修饰的方法不能使用this引用,所以static修饰的方法不能访问不使用static修饰的普通成员,因此Java语法规定:静态成员不能直接访问非静态成员。
请读者牢记一点:Java编程时不要使用对象区调用static修饰的成员变量,方法,而是应该使用类去调用static修饰的成员变量、方法!

4.2方法详解

4.2.1方法的所属性

如果需要定义方法,则只能在类体内定义,不能独立定义一个方法。一旦将一个方法定义在某个类的类体内,如果这个方法使用了static修饰,则这个方法属于这个类,否则这个方法属于这个类的实例。
Java语言的所属性主要体现在如下几个方面。

  • 方法不能独立定义,方法只能在类体里定义。
  • 从逻辑意义上看,方法要么属于该类本身,要么属于该类的一个对象。
  • 永远不能独立执行方法,执行方法必须使用类或对象作为调用者。
4.2.2方法的参数传递机制

Java里方法的参数传递方式只有一种:值传递。
Java对于引用类型的参数传递,一样采用的是值传递方式。

4.2.3形参个数可变的方法

如果在定义方法时,在最后一个形参的类型后增加三点(…),则表明该形参可以接受多个参数值,多个参数值被当成数组传入。例:

public class Varargs{
    //定义了形参个数可变的方法
    public static void test(int a, String... books){
        //books被当成数组处理
        for (String tmp : books){
            System.out.println(tmp);
        }
        //输出整数变量a的值
        System.out.println(a);
    }

    public static void main(String[] args) {
        //调用test方法
        test(5, "疯狂Java讲义", "轻量级Java EE企业应用实战");
    }
}

数组形式的形参可以处于形参列表的任意位置,但个数可变的形参只能处与形参列表的最后。也就是说,一个方法中最多只能有一个个数可变的形参。

4.2.4递归方法

只要一个方法的方法体实现中再次调用了方法本身,就是递归方法。递归一定要向已知方向递归。

4.2.5方法重载

Java允许同一个类里定义多个同名方法,只要形参列表不同就行。如果同一个类中包含了两个或两个以上方法的方法名相同,但形参不同,则被称为重载。
Java程序中确定一个方法需要三个要素。

  • 调用者,也就是方法的所属者,既可以是类,也可以是对象。
  • 方法名,方法的标识。
  • 形参列表,当调用方法时,系统将会根据传入的实参列表匹配。

方法重载的要求就是两同一不同:同一个类中方法名相同,参数列表不同。至于方法的其他部分,如方法返回值类型、修饰符等,与方法重载没有任何关系。
(大部分时候并不推荐重载形参个数可变的方法,因为这样做确实没有太大的意义,而且容易降低程序的可读性)。

4.3成员变量和局部变量

4.3.1成员变量和局部变量

成员变量指的是类里定义的变量,也就是前面所介绍的field;局部变量指的是在方法里定义的变量。

局部变量根据定义形式的不同,有可以被分为如下三种。

  • 形参:在定义方法签名时定义的变量,形参的作用域在整个方法内有效。
  • 方法局部变量:在方法体内定义的局部变量,它的作用域是从定义该变量的地方生效,到该方法结束时生效。
  • 代码块局部变量:在代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,到该代码块结束时生效。

Java允许局部变量和成员变量同名,如果方法里的局部变量和成员变量同名,局部变量会覆盖成员变量,如果需要在这个方法里引用被覆盖的成员变量,则可使用this(对于实例变量)或类名(对于类变量)作为调用者来限定访问成员变量。

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

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

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

局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。

4.3.4变量的使用规则

如果有如下的几种情形,则应该考虑使用成员变量。

  • 如果需要定义的变量适用于描述某个类或某个对象的固有信息的 ,例如人的身高、体重等信息,它们是人对象的固有信息,每个人对象都具有这些信息。这种变量应该定义为成员变量。
  • 如果在某个类中需要以一个变量来保存该类或者实例运行时的状态信息,应该使用成员变量。
  • 如果在某个信息需要在某个类的多个方法之间进行共享,则这个信息应该使用成员变量来保存。

4.4隐藏和封装

4.4.1理解封装

封装(Encapsulation)是面向对象的三大特征之一(另外两个是继承和多态),它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
对一个类或对象实现良好的封装,可以实现一下目的。

  • 隐藏类的实现细节。
  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问。
  • 可进行数据检查,从而有利于保证对象信息的完整性。
  • 便于修改,提高代码的可维护性。
    为了实现良好的封装,需要从两个方面考虑。
  • 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问。
  • 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。
4.4.2使用访问控制符

Java提供了3个访问控制符:private、protected和public。
Java的访问控制级别由小到大:private -> default -> protected -> public

  • private(当前类访问权限):如果类里的一个成员(包括成员变量、方法和构造器等)使用private访问控制符来修饰,则这个成员只能在当前类的内部被访问。很显然,这个访问控制符用于修饰成员变量最合适,使用它来修饰成员变量就可以把成员变量隐藏在该类的内部。
  • default(包访问权限):如果类里的一个成员(包括成员变量、方法和构造器等)或者一个外部类不使用任何访问控制符修饰,就称它是访问权限的,default访问权限的成员变量或外部类可以被相同包下的其他类访问。
  • protected(子类访问权限):吐过一个成员(包括成员变量、方法和构造器等)使用protected访问控制符修饰,那么这个成员既可以被同一个包中的其他类访问,也可以被不同包中的子类访问。在通常情况下,如果使用protected来修饰一个方法,通常是希望其子类重写这个方法。
  • public(公共访问权限):这是一个最宽松的访问权限控制级别,如果一个成员(包括成员变量、方法和控制器等)或者一个外部类使用public访问控制符修饰,那么这个成员或外部类就可以被所有类访问,不管访问类和被访问类是否处于同一个包中,是否具有父子继承关系。

关于访问控制符分使用,存在如下几条基本原则。

  • 类里的绝大部分成员变量都应该使用private修饰,只有一些static修饰的、类似全部变量的成员变量,才可能考虑使用public修饰。除此之外,有些方法只用于辅助实现该类的其他方法,这个方法被称为工具方法,工具方法也应该使用private修饰。
  • 如果某个类主要用做其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不像被外界直接调用,则应该使用protected修饰这些方法。
  • 希望暴露出来给其他类自由调用的方法应该使用public修饰。由此,类的构造器通过使用public修饰,从而允许在其他地方创建该类的实例。因为外部类通常都希望被其他类自由使用,所以大部分外部类都是用public修饰。
4.4.3package、import和import static

使用import语句导入单个类的用法如下:

import package.subpackage...ClassName;

使用import语句导入指定包下全部类的用法如下:

import package.subpackage...*;

导入指定类的单个静态成员变量、方法的语法格式如下:

import static package.subpackage...ClassName|methodName;

导入指定类的全部静态成员变量、方法的语法格式如下:

import static package.subpackage...ClassName.*;

现在可以总结出Java源文件的大体结构如下:

package 语句 //0个或1个,必须放在文件开始
import | import static 语句 //0个或多个,必须放在所有类定义之前
public classDefinition | interfaceDefinition | enumDefinition //0个或1个public类、接口或枚举定义
classDefinition | interfaceDefinition | enumDefinition //0个或多个普通类、接口或枚举定义
4.4.4Java的常用包

Java语言中常用的包。

  • java.lang:这个包下包含了Java语言的核心类,如String、Math、System和Thread类等,使用这个包下的类无需使用import语句导入,系统会自动导入这个包下的所有类。
  • java.util:这个包下包含了Java的大量工具类/接口和集合框架类/接口,例如Arrays和List、Set等。
  • java.net:这个包下包含了一些Java网络编程相关的类/接口。
  • java.io:这个包下包含了一些Java输入/输出编程相关的类/接口。
  • java.text:这个包下包含了一些Java格式化相关的类。
  • java.sql:这个包下包含了Java进行JDBC数据库编程的相关类/接口。
  • java.swing:这个包下包含了Swing图形用户界面编程的相关类/接口,这些类可用于构建平台无关的GUI程序。

4.5深入构造器

4.5.1使用构造器执行初始化

构造器最大的用处就是在创建对象时执行初始化。

4.5.2构造器重载

同一个类里具有多个构造器,多个构造器的形参列表不同,既被称为构造器的重载。

4.6类的继承

4.6.1继承的特点

Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类被称为父类,有的也称为基类、超类。
Java里子类继承父类的语法格式如下:

修饰符 class SubClass extends SupperClass{
    //类定义部分
}
4.6.2重写父类的方法

子类包含与父类同名方法的现象被称为方法重写(Override),也被称为方法覆盖。

4.6.3super的限定

super是Java提供的一个关键字,super用于限定该对象调用它从父类继承得到的实例变量或方法。
如果在某个方法中访问名为a的成员变量,但没有显式指定调用者,则系统查找a的顺序为:
(1)查找该方法中是否有名为a的局部变量。
(2)查找该类中是否包含名为a的成员变量。
(3)查找a的直接父类中是否包含名为a的成员变量,依次上溯a的所有父类,直到java.lang.Object类,如果最终不能找到名为a的成员变量,则系统出现编译错误。

4.6.4调用父类构造器

子类构造器总会调用父类构造器一次。子类构造器调用父类构造器分如下几种情况。

  • 子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器。
  • 子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器时即会调用父类构造器。
  • 子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。

4.7多态

4.7.1多态性

因为子类其实是一种特殊的父类,因此Java允许把一个子类对象直接赋给一个父类引用变量,无需任何类型转换,或者被称为向上转型(upcasting),向上转型由系统自动完成。
与方法不同的是,对象的实例变量则不具备多态性。
(引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。)

4.7.2引用变量的强制类型转换

类型转换运算符是小括号,类型转换运算符的作用是:(type)variable,这种用法可以将variable变量转换成一个type类型的变量。
这种强制类型转换不是万能的,当进行强制类型转换时需要注意:

  • 基本类型之间的转换只能在数值类型之间进行,这里所说的数值类型包括整数类型、字符类型和浮点型。但数值类型和布尔类型之间不能进行类型转换。
  • 引用类型之间的转换只能在具有继承关系的两个类型之间进行,如果时两个没有任何继承关系的类型,则无法进行类型转换,狗则编译时就会出现错误。如果试图把一个父类实例转换成子类类型,则这个对象必须实际上时子类实例才行(即编译时类型为父类类型,而运行时类型是子类类型),否则将在运行时引发ClassCastException异常。
4.7.3instanceof运算符

instanceof运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是接口,可以把接口理解成一种特殊的类),它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是,则返回true,反则返回false。
instanceof运算符的作用是:在进行强制类型转换之前,首先判断一个对象是否是后一个类的实例,是否可以成功转换,从而保证代码更加强壮。

4.8初始化块

4.8.1使用初始化块

初始化块是Java类里可出现的第4种成员(前面依次有成员变量、方法和构造器),一个类可以有多个初始化块,相同类型的初始化块之间有顺序:前面定义的初始化块先执行,后面定义的初始化块后执行。初始化块的语法格式如下:

[修饰符]{
        //初始化块的可执行性代码
        ...
}

初始化代码块的修饰符只能是static,使用static修饰的初始化块被称为静态初始化块。初始化块里的代码可以包含任何可执行语句,包括定义局部变量、调用其他对象的方法,以及使用分支、循环语句等。

4.8.2初始化块和构造器

(实际上初始化块是一个假象,使用javac命令编译Java类后,该Java类种的初始化块会消失——初始化块中代码会被“还原”到每个构造器中,且位置构造器所有代码的前面。

4.8.3静态初始化块

如果定义初始化块时使用了static修饰符,则这个初始化块就变成了静态初始化块,也被称为类初始化块(普通初始化块负责对对象执行初始化,类初始化块则负责对类进行初始化)。

本章练习

1.编写一个学生类,提供name、age、gender、phone、address、email成员变量,且为每个成员变量提供setter、getter方法。为学生类提供默认的构造器和带所有成员变量的构造器。为学生类提供方法,用于描绘吃、喝、玩、睡等行为。
2.利用第1题定义的Student类,定义一个Student[]数组保存多个Student对象作为通讯录数据。程序可通过name、email、address查询,如果找不到数据,则进行友好提示。
3.定义普通人、老师、班主任、学生、学校这些类,提供适当的成员变量、方法用于描述其内部数据和行为特征,并提供主类使之运行。要求有良好的封装性,将不同类放在不同的包下面,增加文档注释,生成API文档。
4.定义交通工具、汽车、火车、飞机这些类,注意它们的继承关系,为这些类提供超过3个不同的构造器,并通过初始化块提取构造器中的通用代码。