第4章 主流软件开发方法——面向对象编程入门
◎ 本章教学微视频:17个 100分钟
学习指引
Java是一种面向对象的程序设计语言,了解面向对象的编程思想对于学习Java开发相当重要。本章介绍如何使用面向对象的思想开发Java程序,主要内容包括类和对象、类的方法、类的封装、继承和多态等。
重点导读
- 熟悉面向对象的基础知识。
- 掌握Java类和对象的相关知识。
- 掌握Java对象值的传递方式。
- 掌握作用域修饰符的使用。
- 掌握Java封装、继承的使用。
- 掌握Java重载、多态的使用。
- 掌握定义和导入包的方法。
4.1 面向对象简介
Java是一种面向对象的程序设计语言,了解面向对象的编程思想对于学习Java开发相当重要。面向对象技术是一种数据抽象和信息隐藏的技术,它使软件的开发更加简单化,符合人们的思维习惯,同时又能降低软件的复杂性,提高软件的生产效率,因此得到了广泛的应用。
4.1.1 什么是面向对象
面向对象是一种符合人类思维习惯的编程思想。在现实生活中,存在着各种不同形态的事物,这些事物之间存在着各种各样的联系。在程序中使用对象来映射现实中的事物,适用对象的关系来描述事物之间的联系,这种思想就是面向对象。
面向对象编程(Object Oriented Programming,OOP)是相对于面向过程编程而言的。传统的面向过程编程语言(如C语言)是以过程为中心,以算法为驱动,而面向对象的编程语言则是以对象为中心,以消息为驱动。
面向过程就是分析解决问题所需要的步骤,然后用函数把这些步骤一一实现,使用的时候依次调用就可以了。面向对象则是把解决的问题按照一定规则划分为多个独立的对象,然后通过调用对象的方法来解决问题。一个应用程序会包含多个对象,通过多个对象的相互配合来实现应用程序的功能。当某个应用程序功能需要改变时,只需要修改个别的对象即可,这样可以使代码更容易维护,效率更高。
4.1.2 面向对象的特点
面向对象方法作为一种独具优越性的方法引起全世界越来越广泛的关注和高度的重视,它被誉为“研究高技术的好方法”,更是当前计算机界关心的重点。
几乎所有面向对象的程序设计语言都有3个特性,即封装性、继承性和多态性。
1.封装性
封装性是面向对象的核心思想。将对象的属性和方法封装起来,不需要让外界知道具体实现的细节,这就是封装的思想。封装可以使数据的安全性得到保证。当把过程和数据封装起来后,对数据的访问只能通过已定义的接口进行。
(1)属性的封装。Java中类的属性的访问权限的默认值不是private,要想隐藏该属性或方法,就可以加private(私有)修饰符来限制只能在类的内部进行访问。对于类中的私有属性,要对其给出一对方法(getXxx()和setXxx())访问私有属性,保证对私有属性的操作的安全性。
(2)方法的封装。对于方法的封装,该公开的公开,该隐藏的隐藏。方法公开的是方法的声明(定义),即只要知道参数和返回值就可以调用该方法。隐藏方法的实现会使实现的改变对架构的影响最小化。完全的封装是类的属性全部私有化,并且提供一对方法来访问属性。
2.继承性
继承主要指的是类与类之间的关系。通过继承,可以效率更高地对原有类的功能进行扩展。继承不仅增强了代码的复用性,提高了开发效率,更为程序的修改补充提供了便利。
Java中的继承要使用extends关键字,并且Java中只允许单继承,即一个类只能有一个父类。这样的继承关系呈树状,体现了Java的简单性。子类只能继承在父类中可以访问的属性和方法,实际上父类中私有的属性和方法也会被子类继承,只是子类无法访问。
3.多态性
多态是把子类型的对象主观地看作其父类型的对象,那么父类型就可以是很多种类型。编译时类型指被看作的类型,是主观认定的。运行时类型指实际的对象实例的类型,是客观的,不可改变(也被看作类型的子类型)。
多态有以下特性:对象实例确定后则不可改变(客观不可改变);只能调用编译时类型所定义的方法;运行时会根据运行时类型去调用相应类型中定义的方法。
4.2 类和对象
在面向对象的概念中,将具有相同属性及相同行为的一组对象称为类(class)。类是用于组合各个对象所共有操作和属性的一种机制。类的具体化就是对象,即对象就是类的实例化。例如,图4-1中,男孩、女孩为类,而具体的每个人为其中某个类的对象。
图4-1 类和对象
4.2.1 什么是类
类是一个独立的单位,它有一个类名,其内部包括用于描述对象属性的成员变量和用于描述对象行为的成员方法。在Java程序设计中,类被认为是一种抽象的数据类型。在使用类之前,必须先声明,类的声明格式如下:
[标识符] class 类名 { 类的成员变量 类的方法 }
声明类需要使用关键字class,在class之后是类名。标识符可以是public、private、protected或者完全省略。类名应该由一个或多个有意义的单词连缀而成,每个单词首字母大写,单词之间不要使用任何分隔符。
总之,类可以看成是创建Java对象的模板。通过下面一个简单的类来理解Java中类的定义,具体代码如下:
在上述代码中,可以看到一个类可以包含以下3种类型的变量:
- 局部变量。是在方法、构造方法或者语句块中定义的变量。这种变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
- 成员变量。是定义在类中、方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中的方法、构造方法和特定类的语句块访问。
- 类变量。也声明在类中、方法体之外,但必须声明为static类型。
另外,一个类还可以拥有多个方法,在上面的例子中,barking()、hungry()和sleeping()都是Dog类的方法。
【例4-1】(实例文件:ch04\Chap4.1.txt)创建类应用实例。
public class Person { String name; int age; void speak() { System.out.println("我叫" + name + ",今年" + age + "岁。"); } }
4.2.2 类的方法
在Java中,方法定义在类中,它和类的成员属性一起构成一个完整的类。一个方法有4个要素,分别是方法名、返回值类型、参数列表和方法体。定义一个方法的语法格式如下:
修饰符 返回值类型 方法名(参数列表) { 方法体 return 返回值; }
方法包含一个方法头和一个方法体。方法头包括修饰符、返回值类型、方法名和参数列表。具体介绍如下:
- 修饰符:定义了该方法的访问类型,这是可选的。
- 返回值类型:指定了方法返回的数据类型。它可以是任意有效的类型,如果方法没有返回值,则其返回值类型必须是void,不能省略。方法体中的返回值类型要与方法头中定义的返回值类型一致。
- 方法名称:要遵循Java标识符命名规范,通常以英文中的动词开头。
- 参数列表:由类型、标识符组成,每个参数之间用逗号分隔。方法可以没有参数,但方法名后面的括号不能省略。
- 方法体:指方法头后{}内的内容,主要用来实现一定的功能。
【例4-2】(实例文件:ch04\Chap4.2.txt)类的方法应用实例。
class Person { String name; int age; void setName(String name2) { name = name2; } void setAge(int age2) { age = age2; } void speak() { System.out.println("我叫" + name + ",今年" + age + "岁。"); } } public class Test { public static void main(String[] args) { Person p1 = new Person(); p1.setName("张三"); p1.setAge(18); p1.speak(); } }
程序运行结果如图4-2所示。
4.2.3 构造方法
在创建类的对象时,对类中的所有成员变量都要初始化,赋值过程比较麻烦。如果在对象最初被创建时就完成对其成员变量的初始化,程序将更加简洁。Java允许对象在创建时进行初始化,初始化是通过构造方法来完成的。
在创建类的对象时,使用new关键字和一个与类名相同的方法来完成,该方法在实例化过程中被调用,称为构造方法。构造方法是一种特殊的成员方法,有以下几个主要特点:
- 构造方法的名称必须与类的名称完全相同。
- 构造方法不返回任何数据,也不需要使用void关键字声明。
- 构造方法的作用是创建对象并初始化成员变量。
- 在创建对象时,系统会自动调用类的构造方法。
- 构造方法一般用public关键字声明。
- 每个类至少有一个构造方法。如果不定义构造方法,Java将提供一个默认的不带参数且方法体为空的构造方法。
- 构造方法也可以重载。
【例4-3】(实例文件:ch04\Chap4.3.txt)类的构造方法应用实例。
class Person { String name; int age;
public Person(String name, int age) { //定义构造方法,有两个参数 this.name = name; this.age = age; }
void speak() { System.out.println("我叫" + name + ",今年" + age + "岁。"); } }
public class Test { public static void main(String[] args) { Person p1 = new Person("张三", 18); //根据构造方法,必须含有两个参数,如果不写会报错 //为了避免这种情况,应该再添加一种无参数的构造方法:public Person() { } //也就是说,如果自定义了构造方法,为了避免出错 //应该定义一个无参数的构造方法,这样也实现了构造方法的重载 p1.speak(); } }
程序运行结果如图4-3所示。
图4-3 类的构造方法应用实例
构造方法和方法在修饰符、返回值、命名上的区别如下:
和方法一样,构造方法可以有任何访问修饰符,如public、protected、private,或者没有修饰(通常被package和friendly调用)。而不同于方法的是,构造方法不能有abstract、final、native、static或synchronized等非访问修饰符。
方法能返回任何类型的值或者无返回值(void);构造方法没有返回值,也不需要void。
构造方法使用和类相同的名字,而方法则不同。按照习惯,方法通常以小写字母开始,而构造方法通常以大写字母开始。构造方法通常是一个名词,因为它和类名相同;而方法通常是动词,因为它说明一个操作。
4.2.4 认识对象
对象是根据类创建的。在Java中,使用关键字new来创建一个新的对象。创建对象需要以下3步:
(1)声明。声明一个对象,包括对象名称和对象类型。
(2)实例化。使用关键字new来创建一个对象。
(3)初始化。使用new创建对象时,会调用构造方法初始化对象。
对象是对类的实例化。在Java的世界里“一切皆为对象”,面向对象的核心就是对象。由类产生对象的格式如下:
类名 对象名 = new 类名( );
例如,声明一个对象:
Person p1;
然后,实例化一个对象:
p1 = new Person();
这时就可以连起来写:
Person p1 = new Person();
另外,访问对象的成员变量或者方法格式如下:
对象名.属性名 对象名.方法名()
例如,访问Person类的成员变量和方法代码如下:
p1.name; p1.age; p1.speak();
最后,给成员变量赋值:
p1.name = "张三"; p1.age = 18;
【例4-4】(实例文件:ch04\Chap4.4.txt)创建对象应用实例。
程序运行结果如图4-4所示。
图4-4 创建对象应用实例
4.2.5 类的设计
在面向对象编程中,类的设计是一个核心问题,需要在不断学习和实践后才能有所收获,这是需要大量实践和时间的。在设计类的时候需要考虑以下几点:
- 考虑问题域里有哪些类哪些对象。
- 考虑这些类有什么属性,即成员变量和方法(成员函数),要明确这个类是做什么用的。
- 考虑这些类之间的关系。类之间的关系有关联、继承、聚合、实现和多态。
初学者不要急着开始就设计出很复杂的类,要尽可能理解类,先设计一些具有简单属性和方法的类,一步一步熟练之后再增加类的复杂程度。在定义类的时候如果不知道要定义几个类,可以按照需要实现的事情里面有几个名词来定义出需要的类。
例如,学生去北京。可以这样定义类:学生是名词,那么可以定义一个学生类,这个类里面有姓名、年龄等属性,还有去北京这个方法。那学生怎么去北京呢?坐汽车、火车还是飞机?需要设计一个交通工具类,里面有区别各个对象的属性和方法。
4.2.6 类和对象的关系
类和对象是面向对象方法的核心概念,类是对某一类事物的描述,是抽象的、概念上的定义,对象是实际存在的该类事物的个体,例如,可以定义一个桌子类,通过这个桌子类,可以定义多个桌子对象,还可以把桌子类看成是一个模板或者图纸,按照这个图纸就可以生产出许多桌子。
对象和对象之间可以不同,改变其中一个对象的某些属性,不会影响到其他的对象,例如按照桌子的图纸,可以生产出相同的桌子,也可以生产出不同高度的桌子。
4.3 对象值的传递
Java中没有指针,所以也没有引用传递,仅仅有值传递。不过可以通过对象的方式来实现引用传递。
4.3.1 值传递
方法调用时,实际参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参数的值。传递值的数据类型主要是基本数据类型,包括整型、浮点型等。
【例4-5】(实例文件:ch04\Chap4.5.txt)值传递应用实例。
public class Test { public static void change(int i, int j) { int temp = i; i = j; j = temp; }
public static void main(String[] args) { int a = 3; int b = 4; change(a, b); System.out.println("a=" + a); System.out.println("b=" + b); } }
程序运行结果如图4-5所示。
图4-5 值传递应用实例
在本实例中,首先定义了一个静态方法change,该方法有两个参数i和j。在方法内定义变量temp,将参数i的值赋给temp,再将参数j的值赋给i,再将temp的值赋给j。初始化变量a和b,将a和b的值作为change方法的参数,也就是说a相当于i,b相当于j。输出的结果是a和b的值保持不变。由此可以确定,传递的值并不会改变原值。
4.3.2 引用传递
引用传递也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中对应的形式参数,在方法执行中,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变将会影响实际参数值。
传递地址值的数据类型为除String以外的所有复合数据类型,包括数组、类和接口等。
【例4-6】(实例文件:ch04\Chap4.6.txt)引用传递(对象)应用实例。
class A { //定义一个类 int i = 0; }
public class Test { public static void add(A a) { //a = new A(); a.i++; }
public static void main(String args[]) { A a = new A(); add(a); System.out.println(a.i); } }
程序运行结果如图4-6所示。
图4-6 引用传递(对象)应用实例
在本实例中,当把a=new A();行注释掉时,输出的结果是1;当该行没有被注释掉时是0,原因是a= new A();构造了新的A对象,不是传递的那个对象了。
4.4 作用域修饰符
在Java语言中有许多修饰符,主要分为访问修饰符和非访问修饰符两种。修饰符用来定义类、方法或者变量,通常放在语句的最前面。
4.4.1 访问修饰符
在Java语言中,可以使用访问修饰符来规定对类、变量、方法和构造方法的访问。Java提供了4种不同的访问权限,以实现不同范围的访问能力。表4-1列出了这些访问权限,其中3种有访问修饰符。
表4-1 访问修饰符的作用范围
1.私有的访问修饰符private
private修饰符对应最严格的访问级别,被声明为private的方法、变量和构造方法只能被所属类访问,并且类和接口不能声明为private。
声明为私有访问类型的变量只能通过类中的公共方法被外部类访问。private修饰符主要用来隐藏类的实现细节和保护类的数据。
【例4-7】(实例文件:ch04\Chap4.7.txt)private修饰符应用实例。
程序运行结果如图4-7所示。在本例中,定义了一个私有的成员变量name,通过它的set方法为成员变量name赋值,get方法获取成员变量name的值。在main()方法中创建类的对象p,通过p.setName()方法设置name的值,再通过调用p.getName()方法打印输出name的值。
图4-7 private修饰词实例
2.无访问修饰符
不使用访问修饰符声明的变量和方法,可以被这个类本身或者与类在同一个包内的其他类访问。接口中的变量都隐式声明为public static final,而接口中的方法默认情况下访问权限为public,因此无访问修饰符的情况也称为默认访问修饰符。
【例4-8】(实例文件:ch04\Chap4.8.txt)变量和方法的声明,不使用任何访问修饰符。
程序运行结果如图4-8所示。在本案例中,使用默认访问修饰符定义了成员变量name、成员方法getName()和setName()。它们可以被当前类或者与类在同一个包中的其他类访问。
图4-8 默认访问修饰符实例
3.受保护的访问修饰符protected
protected修饰符不能修饰类和接口,方法和成员变量能够声明为protected,但是接口的成员变量和成员方法不能声明为protected。
【例4-9】(实例文件:ch04\Chap4.9.txt)在父类Person中,使用protected声明了方法;在子类Women中,访问父类中用protected声明的方法。
程序运行结果如图4-9所示。
图4-9 protected修饰符
在本案例中,用protected声明了父类Person中的sing()方法和成员变量name,它可以被子类访问。在main()方法中创建了子类对象m,通过m访问了父类的sing()方法,并为父类的name属性赋值,再在控制台打印它的值。
如果把sing()方法声明为private,那么除了父类Person之外的类将不能访问该方法。如果把sing()方法声明为public,那么所有的类都能够访问该方法。如果不给sing()方法加访问修饰符,那么只有在同一个包中的类才可以访问它。
4.公有的访问修饰符public
被声明为public的类、方法、构造方法和接口能够被任何其他类访问。如果几个相互访问的public类分布在不同的包中,则需要用关键字import导入相应的public类所在的包。由于类的继承性,类所有的公有方法和变量都能被其子类继承。
【例4-10】(实例文件:ch04\Chap4.10.txt)在类中定义public的方法,在不同包中访问它。
程序运行结果如图4-10所示。
图4-10 public修饰符
在本案例中,定义了两个不同包中的类,两个类之间没有继承关系。在访问PublicTest类的main()方法中,访问Person类中的public修饰的test()方法。
4.4.2 非访问修饰符
Java语言不仅提供了访问修饰符,还提供了许多非访问修饰符,如static、final、abstract、synchronized、transient和volatile等。
1.static修饰符
static修饰符用来修饰类的成员变量和成员方法,也可以形成静态代码块。static修饰的成员变量和成员方法一般称为静态变量和静态方法,可以直接通过类名访问它们。访问的语法格式一般为
类名.静态方法名(参数列表) ; 类名.静态变量名;
用static修饰的代码块表示静态代码块,当Java虚拟机(JVM)加载类时,就会执行该代码块。
1)静态变量
static修饰的成员变量独立于该类的任何对象,被类的所有对象共享。无论一个类实例化多少对象,它的静态变量都只有一份。只要加载这个类,Java虚拟机就能根据类名在运行时数据区的方法区内找到它们。因此,static对象可以在它的任何对象创建之前访问,无须引用任何对象。静态变量也称为类变量。局部变量不能被声明为static变量。
2)静态方法
static用来声明独立于对象的静态方法。静态方法不能使用类的非静态变量。静态方法从参数列表得到数据,然后计算这些数据。由于static修饰的方法独立于任何对象,因此static方法必须被实现,而不能是抽象的abstract。
静态方法直接通过类名调用,任何对象也都可以调用它,因此静态方法中不能用this和super关键字,不能直接访问所属类的成员变量和成员方法,只能访问所属类的静态成员变量和静态成员方法。
3)static代码块
static代码块也称为静态代码块,是在类中独立于类成员的static语句块,可以有多个,位置可以随便放,它不在任何方法体内。JVM加载类时会执行这些静态的代码块,如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
4)static和final
用static和final修饰的成员变量一旦初始化,它的值就不可以修改,并且要通过类名访问,它的名称一般建议使用大写字母。用static和final修饰的成员方法不可以被重写,并且通过类名直接访问。
需要注意,对于被static和final修饰的成员常量,成员变量本身的值不能再改变了,但对于一些容器类型(如ArrayList、HashMap)的成员变量,不可以改变容器变量本身的值,但可以修改容器中存放的对象,这种成员变量类似于对象的引用。
【例4-11】(实例文件:ch04\Chap4.11.txt)static修饰符的使用。
程序运行结果如图4-11所示。
图4-11 static修饰符
在本例中,定义了static和final修饰的常量BANANA并初始化,定义了static修饰的静态成员变量price并初始化;定义static块,在类加载时执行;在main()方法中创建类的对象st,通过对象st调用test(),并通过对象st调用类的静态成员变量和常量打印输出它们的值。
2.final修饰符
final可以修饰类、方法和变量,意义不同,但是本质相同,都是表示不可改变。
1)final修饰类中的变量
用final修饰的成员变量表示常量,值一旦给定就无法改变。final修饰的变量有3种,分别是静态变量、成员变量和局部变量。变量的初始化可以在两个地方,一是在定义时初始化,二是在构造方法中赋值。
final变量定义的时候,可以先声明,而不给初值,这种变量也称为final空白,无论什么情况,编译器都确保final空白在使用之前必须被初始化。但是,final空白在final关键字的使用上提供了更大的灵活性,为此,一个类中的final数据成员就可以实现既根据对象而有所不同,又保持其恒定不变的特征。
2)final修饰类中的方法
如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法。使用final方法的原因有两个:一是把方法锁定,防止任何继承类修改它的意义和实现;二是高效。编译器在遇到调用final方法时,会转入内嵌机制,大大提高执行效率。
类的成员方法使用final修饰,方法不能再被重写。final声明方法的格式如下:
[修饰符] final 返回值类型 方法名([参数类型 参数,…]){ 方法体 }
3)final修饰类
用final声明的类不能被继承,即最终类。因此final类的成员方法没有机会被覆盖,默认都是final的。在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会被扩展,那么就设计为final类。final声明类的语法格式一般为
final class 类名{ 类体 }
【例4-12】(实例文件:ch04\Chap4.12.txt)final关键字的使用。
public class Father { //定义父类 final int f = 9; final void work(){ //使用final修饰方法 System.out.println("我在上班..."); } } public class Son extends Father{ //子类继承父类 public static void main(String[] args){ Son s = new Son(); s.f = 12; System.out.println(s.f); } void work(){ //子类尝试重写父类的work() } }
将Father.java和Son.java复制到E:\java目录下,在DOS命令提示符下,编译上述子类(Son类)时出现错误提示信息,如图4-12所示。
图4-12 final关键字应用实例
在本例中,父类使用final声明了work()方法,使用final声明了整型变量f。在子类中为变量f赋值,编译错误信息提示“错误:无法为最终变量f分配值”。Son类重写父类的work()方法时,编译出现错误信息提示:“Son中的work()无法覆盖Father中的work(),被覆盖的方法为final”,即final定义的成员方法不能被重写。
3.abstract修饰符
abstract用来修饰类,这个类称为抽象类。抽象类不能用来实例化对象,声明抽象类的唯一目的是为了将来对该类进行扩充。
抽象类可以包含抽象方法和非抽象方法。如果一个类包含若干个抽象方法,那么该类必须声明为抽象类。抽象类可以不包含抽象方法。抽象方法的声明以分号结尾。
抽象方法不能被声明成final和static。抽象方法是一种没有任何实现的方法,该方法的具体实现由子类提供。任何继承抽象类的子类必须实现父类的所有抽象方法,除非该子类也是抽象类。
4.synchronized修饰符
synchronized声明的方法同一时间只能被一个线程访问。synchronized的作用范围有如下两种:
(1)在某个对象内,synchronized修饰的方法可以防止多个线程同时访问。这时,不同的对象的synchronized方法是不相干扰的。也就是说,其他线程照样可以同时访问相同类的另一个对象中的synchronized方法。如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其他线程就不能同时访问这个对象中任何一个synchronized方法。
(2)在某个类中,用synchronized修饰静态方法可以防止多个线程同时访问这个类中的静态方法。它可以对类的所有对象起作用。
5.transient修饰符
序列化的对象包含被transient修饰的成员变量时,JVM跳过该特定的变量。该修饰符包含在定义变量的语句中,用来预处理类和变量的数据类型。
6.volatile修饰符
Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了同步块和volatile关键字机制。volatile修饰的成员变量在每次被线程访问时都强制从共享内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。一个volatile对象引用可能是null。
4.5 封装
封装是把过程和数据包围起来,对数据的访问只能通过已定义的接口。面向对象计算始于以下基本概念:现实世界可以被描绘成一系列完全自治的、封装的对象,这些对象通过一个受保护的接口访问其他对象。
对于封装而言,一个对象所封装的是自己的属性和方法,所以它不需要依赖其他对象就可以完成自己的操作。封装的优点如下:
- 良好的封装能够减少耦合。
- 类内部的结构可以自由修改。
- 可以对成员变量进行更精确的控制。
- 隐藏信息,实现细节。
【例4-13】(实例文件:ch04\Chap4.13.txt)类的封装。
程序运行结果如图4-13所示。
图4-13 f类的封装
在本例中,将name、age、weight 3个属性设置为private,这样其他类就不能访问这3个属性。然后又为每个属性写了两个方法getXxx()和setXxx(),将这两个方法设置为public,其他类可以通过setXxx()方法来设置对应的属性,通过getXxx()来获得对应的属性。将setXxx()方法简称为set方法,将getXxx()方法简称为get方法。
封装就是这样把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果不想被外界访问,可以不给外界提供方法。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。例如,将一个房子看作一个对象,房子内部的装修装饰、家具、沙发等都是该房子的私有属性,但是如果没有墙壁来遮挡,别人就会对屋子内的一切一览无余,没有一点儿隐私。正因为那个遮挡的墙壁的存在,我们既能够有自己的隐私,也可以随意更改里面的摆设而不会影响到其他的人。但是如果没有门,一个包裹得严严实实的黑盒子又有什么存在的意义呢?通过门别人也能够进到房子里。门就是房子对象留给外界访问的接口。
我们继续深入这个例子,如果将age设置成500或者负数,也是不会报错的,将weight设置成1000或者负数也是不会报错的,但这是不符合实际情况的,谁会是500岁或者年龄是负数呢?谁会是1000kg或者体重是负数呢?这个问题使用封装就可以很好地解决。
【例4-14】(实例文件:ch04\Chap4.14.txt)验证类的属性。
程序运行结果如图4-14所示。
图4-14 验证类的属性
在本例中,在age和weight两个属性的set方法内加入了判断,如果符合要求就按照参数进行设置,如果不符合要求就设置为一个默认值,这样就避免了不切合实际情况的发生。
封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而言,它的内部细节是隐藏的,暴露给外界的只是它的访问方法。
4.6 继承
继承是Java面向对象编程技术的一块基石。继承能以已有的类为基础,派生出新的类,可以简化类的定义,扩展类的功能。在Java中支持类的单继承和多层继承,但是不支持多继承,也就是说,一个类只能继承一个类而不能继承多个类,即一个类只能有一个父类,不能有多个父类。但是一个类却可以被多个类继承,也就是说一个类可以拥有多个子类。
子类继承父类的特征和行为,使得子类具有父类的各种属性和方法。在继承关系中,父类更通用,子类更具体。父类具有更一般的特征和行为,而子类除了具有父类的特征和行为,还具有一些自己特殊的特征和行为。
在继承关系中,父类和子类需要满足is-a的关系,即子类是父类。表示父类和子类的术语——父类和子类、超类和子类、基类和派生类,它们表示的是同一个意思。
所有类都直接或者间接地继承了java.lang.Object类,Object类中定义了所有的java对象都具有的相同行为,是所有类的祖先。
一个类如果没有使用extends关键字,那么这个类直接继承自Object类。另外,使用关键字extends可以实现继承。具体格式如下:
class子类名extends父类名
【例4-15】(实例文件:ch04\Chap4.15.txt)类的继承。
在本例中,父类Animal定义了一个公有的属性name、一个私有的属性id和两个公有的方法eat()、sleep();子类Cat继承Animal,虽然只定义了一个方法shout(),但会从父类继承一个公有属性name和两个公有方法eat()、sleep(),父类私有的属性id不能被子类继承。
1.子类继承父类的成员变量
当子类继承了某个类之后,便可以使用父类中的成员变量,但是并不是完全继承父类的所有成员变量。具体的原则如下:
- 能够继承父类的public和protected成员变量,不能够继承父类的private成员变量。
- 对于父类的包访问权限成员变量,如果子类和父类在同一个包下,则子类能够继承,否则不能继承。
- 对于子类可以继承的父类成员变量,如果在子类中出现了同名的成员变量,则会发生隐藏现象,即子类的成员变量会屏蔽父类的同名成员变量。如果要在子类中访问父类中的同名成员变量,需要使用super关键字来进行引用。
2.子类继承父类的方法
同样地,子类也并不是完全继承父类的所有方法。具体的原则如下:
- 子类能够继承父类的public和protected成员方法,不能够继承父类的private成员方法。
- 对于父类的包访问权限成员方法,如果子类和父类在同一个包下,则子类能够继承,否则不能继承。
- 对于子类可以继承的父类成员方法,如果在子类中出现了同名的成员方法,则称为覆盖,即子类的成员方法会覆盖父类的同名成员方法。如果要在子类中访问父类中的同名成员方法,需要使用super关键字来进行引用。
3.构造方法
子类不能够继承父类的构造方法。需要注意的是,如果父类的构造方法都是带有参数的,则必须在子类的构造方法中显式地通过super关键字调用父类的构造方法并配以适当的参数列表。如果父类有无参构造方法,则在子类的构造方法中调用父类构造方法则不是必须使用super关键字,如果没有使用super关键字,系统会自动调用父类的无参构造方法。
【例4-16】(实例文件:ch04\Chap4.16.txt)继承构造方法在继承中的实例。
本例中的代码是没有问题的,但如果把父类的无参构造方法去掉,则下面的代码必然会出错:
可以改为如下代码:
由于父类没有无参构造方法,所以子类的构造方法必须先使用super方法调用父类的有参构造方法,这样确实比较麻烦,因此父类在设计构造方法时应该含有一个无参构造方法。
4.7 重载
在Java中,同一个类中的多个方法可以有相同的名字,只要它们的参数列表不同即可,这被称为方法重载(method overloading)。参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同。重载是面向对象的一个基本特性。
【例4-17】(实例文件:ch04\Chap4.17.txt)方法的重载。
程序运行结果如图4-15所示。
图4-15 方法的重载
通过本例可以看出,重载就是在一个类中有相同的函数名称但形参不同的函数。重载可以让一个程序段尽量减少代码和方法的种类。方法的重载有以下几点要特别注意:
- 方法名称必须相同。
- 方法的参数列表(参数类型、参数个数、参数顺序)至少有一项不同,仅仅参数变量名称不同是不可以的。
- 方法的返回值类型和修饰符不做要求,可以相同,也可以不同。
4.8 多态
多态是面向对象程序设计中实现代码重用的一种机制。前面讲过的重载,即调用一系列具有相同名称的方法,这些方法可根据传入参数的不同而得到不同的处理结果,这其实就是多态性的一种体现,属于静态多态。这种多态是在代码编译阶段就确定下来的。还有一种多态形式,在程序运行阶段才能体现出来,称为动态多态。
在实际编写程序时,动态多态的用法更为广泛和有效。下面讲解动态多态,简称多态。还有两个重要的概念:
- 向上转型:父类对象通过子类对象实例化。
- 向下转型:父类对象可以转换为子类对象,但必须强制转换。
另外,多态的存在要有3个前提:
- 要有继承关系。
- 子类要重写父类的方法。
- 父类引用指向子类。
【例4-18】(实例文件:ch04\Chap4.18.txt)多态实例。
程序运行结果如图4-16所示。
图4-16 多态实例
以上的3段代码充分体现了多态的3个前提,分别如下:
(1)存在继承关系。Dog类继承了Animal类。
(2)子类要重写父类的方法。子类重写(override)了父类的两个成员方法eat()和run()。其中eat()是非静态的,run()是静态的。
(3)父类数据类型的引用指向子类对象,即Animal a1 = new Dog();。
【例4-19】(实例文件:ch04\Chap4.19.txt)调用子类独有的属性和方法。
运行会报错!
通过上面两个例子的运行结果可以看到,多态有以下几个特点:
- 指向子类的父类引用只能访问父类中拥有的方法和属性。
- 对于子类中存在而父类中不存在的方法,该引用是不能使用的。
- 若子类重写了父类中的某些方法,在调用这些方法的时候,必定是使用子类中定义的这些方法。
那么,如何使例子中的a1可以访问子类独有的方法和属性呢?可以通过向下转型来实现。
【例4-20】(实例文件:ch04\Chap4.20.txt)向下转型,调用子类独有的属性和方法。
程序运行结果如图4-17所示。
图4-17 向下转型
通过本例可以看出,父类对象a1通过向下转型,强制转换为子类Dog,转型后就可以访问子类Dog独有的属性和方法了。
4.9 定义和导入包
为了更好地组织类,Java提供了包(package)机制,用于区别类名的命名空间。包具有以下作用:
- 把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
- 如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名是不同的,不同的包中的类名是可以相同的,当同时调用两个不同包中类名相同的类时,应该加上包名加以区别。因此,包可以避免类名冲突。
- 包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。
包语句的语法格式为
package 包名1[.包名2[.包名3…]];
创建包的时候,需要为这个包取一个合适的名字。之后,如果另一个源文件包含了这个包提供的类、接口、枚举或者注释类型的时候,都必须将这个包的声明放在这个源文件的开头。
包声明应该在源文件的第一行,每个源文件只能有一个包声明,这个文件中的每个类型都属于它。如果一个源文件中没有使用包声明,那么其中的类、函数、枚举、注释等将被放在一个无名的包(unnamed package)中。
【例4-21】(实例文件:ch04\Chap4.21.txt)包定义。
这个例子创建了一个叫做animals的包。通常使用小写的字母来命名避免与类、接口名字的冲突。在animals包中加入一个类Animal:
接下来,在同一个包中加入该类的一个子类Dog:
最后,在同一个包中加入该类的一个测试类Test:
程序运行测试类结果如图4-18所示。
图4-18 包定义
为了能够使用某一个包的成员,需要在Java程序中明确导入该包。使用import语句可完成此功能。在Java源文件中import语句应位于package语句之后并位于所有类的定义之前,可以没有,也可以有多条,其语法格式为
import package1[.package2…].(classname|*);
如果在一个包中,一个类想要使用本包中的另一个类,那么该包名可以省略。
【例4-22】(实例文件:ch04\Chap4.22.txt)导入包。
import java.util.Date; //为了使用Java API中定义的Date类,导入java.util.Date包 public class Test { public static void main(String args[]) { Date date = new Date(); System.out.println(date.toString()); } }
程序运行结果如图4-19所示。
图4-19 导入包
4.10 就业面试解析与技巧
4.10.1 面试解析与技巧(一)
面试官:重载和重写有什么区别?重载的方法是否可以改变返回值的类型?
应聘者:方法的重写和重载是Java多态性的不同表现。重写是父类与子类之间多态性的一种表现,重载是一个类中多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数,就说该方法被重写。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被屏蔽了。如果在一个类中定义了多个同名的方法,它们或有不同的参数个数,或有不同的参数类型,则称为方法的重载,重载的方法可以改变返回值的类型。
4.10.2 面试解析与技巧(二)
面试官:为什么Java文件中只能含有一个public类?
应聘者:Java程序是从一个public类的main()函数开始执行的,就像C程序是从main()函数开始执行一样,main()函数只能有一个。public类是为了给类装载器提供方便。一个public类只能定义在以它的类名为文件名的文件中。每个编译单元都只有一个public类。因为每个编译单元都只能有一个公共接口,用public类来表现。该接口可以按照要求包含众多的支持包访问权限的类。如果有不止一个public类,编译器就会报错。并且public类的名称必须与文件名相同,不过,严格区分大小写。当然一个编译单元内也可以没有public类。