一、继承
1、继承的概念
1.1、生活中的继承
面向对象的方法论告诉我们,在认识世界的过程中,万事万物皆为对象,对象按状态和行为可以归类。而在现实世界中进行对象的归类时,会发现类与类之间也经常存在包含与从属的关系。
1.2、Java中的继承
Java是一种面向对象的编程语言,其非常注重的一点便是让开发人员在设计软件系统时能够运用面向对象的思想自然地描述现实生活中的问题域,事实上,使用Java编程时,也能够描述现实生活中的继承关系,甚至可以说,继承是Java面向对象编程技术的一块基石。
Java中的继承允许开发人员创建分等级层次的类。利用继承机制,可以先创建一个具有共性的一般类,根据该一般类再创建具有特殊性的新类,新类继承一般类的属性和行为,并根据需要定制它自己的属性和行为。通过继承创建的新类称为== 派生类==(或子类),被继承的具有共性的一般类称为== 基类==(或超类、父类)。
继承使派生类获得了能够直接使用基类的属性和行为的能力,也使得基类能够在无需重新编写代码的情况下通过派生类进行功能的扩展。继承的过程,就是从一般到特殊的过程。类的继承机制是Java面向对象程序设计中的核心特征,是实现软件可重用性的重要手段,是实现多态的基础。
2、Java中继承的实现
2.1、Java中继承的语法
Java中声明派生类继承某基类的语法格式如下:
[修饰符] class 派生类名 extends 基类名 {
// 派生类成员变量
// 派生类成员方法
}
说明:
如上,Java中使用extends
关键字实现类的继承。
/**
* 动物类
*/
public class Animal {
// 属性
String type; // 类型
String breed; // 品种
String name; // 名称
/**
* 构造函数
* @param type 类型
* @param breed 品种
* @param name 名称
*/
public Animal(String type, String breed, String name) {
this.type = type;
this.breed = breed;
this.name = name;
}
/**
* 自我介绍方法
*/
public void introduce() {
System.out.printf("主人好,我是%s,我的品种是%s,我的名字叫%s。",
this.type, this.breed, this.name);
}
}
猫继承动物类:
/**
* 猫类
*/
public class Cat extends Animal {
/**
* 派生类构造函数
* @param breed 品种
* @param name 名称
*/
public Cat(String breed, String name) {
super("猫", breed, name);
}
}
走来了一只狗:
/**
* 狗类
*/
public class Dog extends Animal {
/**
* 派生类构造函数
* @param breed 品种
* @param name 名称
*/
public Dog(String breed, String name) {
super("狗", breed, name);
}
}
唐老鸭来了:
/**
* 鸭子类
*/
public class Duck extends Animal {
/**
* 派生类构造函数
* @param breed 品种
* @param name 名称
*/
public Duck(String breed, String name) {
super("鸭子", breed, name);
}
}
测试类:
/**
* 宠物店(测试类)
*/
public class PetShop {
/**
* 用来测试的main方法
*/
public static void main(String[] args) {
Cat cat = new Cat("波斯猫", "大花"); // 定义猫对象cat
Dog dog = new Dog("牧羊犬","大黑"); // 定义狗对象dog
Duck duck = new Duck("野鸭","大鸭"); // 定义鸭子对象duck
cat.introduce(); // 猫调用自我介绍方法
dog.introduce(); // 狗调用自我介绍方法
duck.introduce(); // 鸭子调用自我介绍方法
}
}
说明:
main
方法中的猫类对象cat
、狗类对象dog
、鸭子类对象duck
都可以调用introduce()
方法,但Cat类、Dog类、Duck类中并未定义introduce()
方法,这是因为Cat类、Dog类、Duck类从父类Animal类中继承了introduce()
方法。- 派生类不能继承基类的构造方法,因为基类的构造方法用来初始化基类对象,派生类需要自己的构造方法来初始化派生类自己的对象。
- 派生类初始化时,会先初始化基类对象。如果基类没有无参的构造方法,需要在派生类的构造方法中使用
super
关键字显示的调用基类拥有的某个构造方法。 this
和super
都是Java中的关键字,this
可以引用当前类的对象,super
可以引用基类的对象。关于这两个关键字将在后面的内容中展开介绍。
2.2、Java中支持的继承类型
说明:
- 需要注意的是:Java中的类不支持多继承!
3、this和super关键字
3.1、this关键字
this
是Java中的关键字,this
可以理解为指向当前对象(正在执行方法的对象)本身的一个引用。
Java中,this
关键字只能在没有被static
关键字修饰的方法中使用。其主要的应用场景有下面几种。
第一,作为当前对象本身的引用直接使用。
第二,访问当前对象本身的成员变量。当方法中有局部变量和成员变量重名时,访问成员变量需要使用this.成员变量名
。
第三,调用当前对象本身的成员方法。作用与访问成员变量类似。
第四,调用本类中的其他构造方法,语法格式为:
this([参数1, ..., 参数n]);
3.2、super关键字
super
也是Java中的关键字,super
可以理解为是指向当前对象的基类对象的一个引用,而这个基类指的是离自己最近的一个基类。
Java中,super
关键字也只能在没有被static
关键字修饰的方法中使用。其主要的应用场景有下面几种。
第一,访问当前对象的基类对象的成员变量。当方法中有基类成员变量和其他变量重名时,访问基类成员变量需要使用super.基类成员变量名
。
第二,访问当前对象的基类对象的成员方法。作用与访问成员变量类似。
第三,调用基类的构造方法,语法格式为:
super([参数1, ..., 参数n]);
4、object类
在Java中,java.lang.Object
类是所有类的基类,当一个类没有使用extends
关键字显式继承其他类的时候,该类默认继承了Object类,因此所有类都是Object类的派生类,都继承了Object类的属性和方法。
4.1、常用API
Object类的API如下:
方法 | 返回值类型 | 方法说明 |
---|---|---|
getClass() | Class<?> | 返回此Object 所对应的Class 类实例 |
clone() | Object | 创建并返回此对象的副本 |
hashCode () | int | 返回对象的哈希码值 |
equals(Object obj) | boolean | 判断其他对象是否等于此对象 |
toString() | String | 返回对象的字符串表示形式 |
finalize() | void | 当垃圾收集确定不再有对该对象的引用时,垃圾收集器在对象上调用该对象 |
notify() | void | 唤醒正在等待对象监视器的单个线程 |
notifyAll() | void | 唤醒正在等待对象监视器的所有线程 |
wait() | void | 导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll() 方法 |
wait(long timeout) | void | 导致当前线程等待,直到另一个线程调用 notify() 方法或该对象的 notifyAll() 方法,或者指定的时间已过 |
wait(long timeout, int nanos) | void | 导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll() 方法,或者某些其他线程中断当前线程,或一定量的实时时间 |
4.2、案例
/**
* 动物类
*/
public class Animal {
// 属性
String type; // 类型
String breed; // 品种
String name; // 名称
/**
* 构造函数
* @param type 类型
* @param breed 品种
* @param name 名称
*/
public Animal(String type, String breed, String name) {
this.type = type;
this.breed = breed;
this.name = name;
}
/**
* 自我介绍方法
*/
public void introduce() {
System.out.printf("主人好,我是%s,我的品种是%s,我的名字叫%s。",
this.type, this.breed, this.name);
}
}
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
// 实例化若干Animal
Animal animal1 = new Animal("猫","波斯猫", "大花");
Animal animal2 = new Animal("狗","牧羊犬", "大黑");
// 调用继承自Object类的一些方法
System.out.println("animal1.hashCode() = " + animal1.hashCode());
System.out.println("animal1.toString() = " + animal1.toString());
System.out.println("animal1.equals(animal2) = " + animal1.equals(animal2));
}
}
说明:
- 上例中,通过Animal类的对象调用了Animal类继承自基类Object类的方法。
查看Object
类中toString()
方法的实现,如下:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
可以看到,Object
类中toString()
方法打印了类的完全限定名
+@
+hashCode()方法的返回值
。
- 查看
Object
类中equals(Object obj)
方法的实现,如下:
public boolean equals(Object obj) {
return (this == obj);
}
可以看到,Object
类中equals(Object obj)
方法就是对两个对象进行了==
操作符比较运算,并返回比较结果。
二、封装
1、封装的概念
所谓封装,即是对信息(属性、方法的实现细节等)进行隐藏。封装亦是面向对象编程中基本的、核心的特征之一。
在Java中,一个类就是一个封装了数据(属性)以及操作这些数据的代码(方法)的逻辑实体。具体的说,在将客观事物按照面向对象思想抽象成类的过程中,开发人员可以给类中不同的成员提供不同级别的保护。比如可以公开某些成员使其能够被外界访问;可以将某些成员私有,使其不能被外界访问;或者给予某些成员一定的访问权限,使其只能被特定、有限的外界访问。
通过封装,Java中的类既提供了能够与外部联系的必要的API,也尽可能的隐藏了类的实现细节,避免了这些细节被外部程序意外的改变或被错误的使用。
封装为软件提供了一种安全的、健壮的、模块化的设计机制。类的设计者提供标准化的类,而使用者根据实际需求选择和组装各种功能的类,通过API使它们协同工作,从而实现软件系统。在具体开发的过程中,类的设计者需要考虑如何定义类中的成员变量和方法,如何设置其访问权限等问题。类的使用者只需要知道有哪些类可以选择,每个类有哪些功能,每个类中有哪些可以访问的成员变量和成员方法等,而不需要了解其实现的细节。
2、类中成员的访问权限
按照封装的原则,类的设计者既要提供类与外部的联系方式,又要尽可能的隐藏类的实现细节,具体办法就是为类的成员变量和成员方法设置合理的访问权限。
Java为类中的成员提供了四种访问权限修饰符,它们分别是public
(公开)、protected
(保护)、缺省和private
(私有),它们的具体作用如下:
public
:被public
修饰的成员变量和成员方法可以在所有类中访问。(注意:所谓在某类中访问某成员变量是指在该类的方法中给该成员变量赋值和取值。所谓在某类中访问某成员方法是指在该类的方法中调用该成员方法。)protected
:被protected
修饰的成员变量和成员方法可以在声明它的类中访问,在该类的子类中访问,也可以在与该类位于同一个包中的类访问,但不能在位于其它包的非子类中访问。- 缺省:缺省指不使用权限修饰符。不使用权限修饰符修饰的成员变量和成员方法可以在声明它的类中访问,也可以在与该类位于同一个包中的类访问,但不能在位于其它包的类中访问。
private
:被private
修饰的成员变量和成员方法只能在声明它们的类中访问,而不能在其它类(包括子类)中访问。
public | protected | 缺省 | private | |
---|---|---|---|---|
当前类中能否访问 | ||||
同包子类中能否访问 | ||||
同包非子类中能否访问 | ||||
不同包子类中能否访问 | ||||
不同包非子类中能否访问 |
3、getter/setter访问器
类中的成员变量都是缺省权限修饰符的,这在一定程度上破坏了类的封装性。事实上,在Java中极力提倡使用private
修饰类的成员变量,然后提供一对public
的getter
方法和setter
方法对私有属性进行访问。这样的getter
方法和setter
方法也被称为属性访问器。
/**
* 人类
*/
public class Person {
private String name; // 名称
private int age; // 年龄
private int sex; // 性别( 1:男 0:女 )
private String favourite; // 爱好
/**
* 构造方法
*
* @param name 姓名
* @param age 年龄
* @param sex 性别
* @param favourite 爱好
*/
public Person(String name, int age, int sex, String favourite) {
this.name = name;
this.sex = sex;
this.age = age;
this.favourite = favourite;
}
/**
* 获取名称的方法
* @return 名称
*/
public String getName() {
return name;
}
/**
* 设置名称的方法
* @param name 要设置的名称
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取年龄的方法
* @return 年龄
*/
public int getAge() {
return age;
}
/**
* 设置年龄的方法
* @param age 要设置的年龄
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取性别的方法
* @return 性别
*/
public int getSex() {
return sex;
}
/**
* 设置性别的方法
* @param sex 要设置的性别
*/
public void setSex(int sex) {
this.sex = sex;
}
/**
* 获取爱好的方法
* @return 爱好
*/
public String getFavourite() {
return favourite;
}
/**
* 设置爱好的方法
* @param favourite 要设置的爱好
*/
public void setFavourite(String favourite) {
this.favourite = favourite;
}
}
说明:
- 本例中,
Person
类的成员变量都被private
修饰,只有在Person
类的内部才能直接访问,在Person
类的外部,需要使用Person
类提供的属性访问器才可以访问。
4、类的访问权限
通常情况下(不考虑内部类的情况,内部类将在后面的章节中详细介绍),声明类时只能使用public
访问权限修饰符或缺省。虽然一个Java源文件可以定义多个类,但只能有一个类使用public
修饰符,该类的类名与类文件的文件名必须相同,而其他类需要缺省权限修饰符。
使用public
修饰的类,在其他类中都可以被使用,而缺省权限修饰符的类,只有在同包的情况下才能被使用。
三、多态
1、多态的概念
1.1、生活中的多态
多态简单的理解就是多种形态、多种形式。具体来说,多态是指同一个行为具有多个不同表现形式或形态。
1.2、Java中的多态
在Java中,多态是指同一名称的方法可以有多种实现(方法实现是指方法体)。系统根据调用方法的参数或调用方法的对象自动选择某一个具体的方法实现来执行。多态亦是面向对象的核心特征之一。
多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们可以通过相同的方式予以调用。
2、Java中多态的实现
2.1、方法的重载(overload)
在之前的章节中已经介绍过方法的重载(overload
)。在一个类中,多个方法具有相同的方法名称,但却具有不同的参数列表,与返回值无关,称作方法重载(overload
)。
重载的方法在程序设计阶段根据调用方法时的参数便已经可以确定调用的是具体哪一个方法实现,故方法重载体现了设计时多态。
/**
* 动物类
*/
public class Animal {
// 属性
private String type; // 类型
private String breed; // 品种
private String name; // 名称
/**
* 构造函数(明确知道类型、品种、名称)
* @param type 类型
* @param breed 品种
* @param name 名称
*/
public Animal(String type, String breed, String name) {
this.type = type;
this.breed = breed;
this.name = name;
}
/**
* 构造函数(只知道类型和名称,但是品种未知)
* @param type 类型
* @param name 名称
*/
public Animal(String type, String name) {
this.type = type;
this.name = name;
this.breed = "未知";
}
/**
* 构造函数(只知道名称,但是类型和品种都未知)
* @param name 名称
*/
public Animal(String name) {
this.name = name;
this.type = "未知";
this.breed = "未知";
}
}
/**
* 宠物店(测试类)
*/
public class PetShop {
/**
* 用来测试的main方法
*/
public static void main(String[] args) {
// 定义若干动物对象,使用了各种不同的Animal类的构造方法
Animal animal1 = new Animal("狗", "牧羊犬", "大黑");
Animal animal2 = new Animal("猫", "大花");
Animal animal3 = new Animal("大鸭");
}
}
2.2、方法的重写(override)
方法重写(override
)指在继承关系中,派生类重写基类的方法,以达到同一个方法在不同的派生类中有不同的实现。
如果基类中的方法实现不适合派生类,派生类便可以重新定义。派生类中定义的方法与基类中的方法具有相同的返回值、方法名称和参数列表,但具有不同的方法体,称之为派生类重写了基类的方法。
仅仅在派生类中重写了基类的方法,仍然不足以体现出多态性,还需要使用面向对象程序设计中的一条基本原则,即 里氏替换原则 。里氏替换原则 表述为,任何基类可以出现的地方,派生类一定可以出现。直白的说就是基类类型的变量可以引用派生类的对象(即基类类型的变量代表的内存中存储的是一个派生类的对象在内存中的地址编号)。此时,通过基类类型的变量调用基类中的方法,真正的方法执行者是派生类的对象,被执行的方法如果在派生类中被重写过,实际执行的便是派生类中的方法体。
通过上述方式,相同基类类型的变量调用相同方法,根据调用方法的具体派生类对象的不同,便可以执行不同的方法实现。由于在程序运行阶段,变量引用的内存地址才能最终确定,故这种形式的多态体现了运行时多态。
/**
* 动物类
*/
public class Animal {
// 属性
private String type; // 类型
private String breed; // 品种
private String name; // 名称
/**
* 构造函数
* @param type 类型
* @param breed 品种
* @param name 名称
*/
public Animal(String type, String breed, String name) {
this.type = type;
this.breed = breed;
this.name = name;
}
/**
* 获取名称的方法
* @return 名称
*/
public String getName() {
return this.name;
}
/**
* 发出声音
*/
public void makeSound() { }
}
/**
* 猫类
*/
public class Cat extends Animal {
/**
* 派生类构造函数
* @param breed 品种
* @param name 名称
*/
public Cat(String breed, String name) {
super("猫", breed, name);
}
/**
* 重写基类的makeSound()方法
*/
@Override
public void makeSound() {
System.out.printf("%s发出叫声,喵喵喵。\n", super.getName());
}
}
/**
* 狗类
*/
public class Dog extends Animal {
/**
* 派生类构造函数
* @param breed 品种
* @param name 名称
*/
public Dog(String breed, String name) {
super("狗", breed, name);
}
/**
* 重写基类的makeSound()方法
*/
@Override
public void makeSound() {
System.out.printf("%s发出叫声,汪汪汪。\n", super.getName());
}
}
/**
* 宠物店(测试类)
*/
public class PetShop {
/**
* 用来测试的main方法
*/
public static void main(String[] args) {
// 实例化Cat类和Dog类的对象,并将它们赋值给基类Animal类的变量
Cat cat = new Cat("波斯猫", "大花");
Animal animal1 = cat;
Animal animal2 = new Dog("牧羊犬", "大黑");
// 使用Animal类的变量调用Animal类中被派生类重写过的方法
animal1.makeSound();
animal2.makeSound();
}
}
说明:
- 本例中,对象
animal1
的数据类型是Animal
,但该变量实际引用的是一个Cat
类型的实例,由animal1
调用makeSound()
方法时,实际执行的是Cat
类中makeSound()
方法的方法体;对象animal2
的数据类型也是Animal
,但该变量实际引用的是一个Dog
类型的实例,由animal2
调用makeSound()
方法时,实际执行的是Dog
类中makeSound()
方法的方法体。 - 注意,在派生类中重写的
makeSound()
方法上标注了一个@Override
,这种由一个@
+单词
组成的标注在Java中称为注解(也叫元数据),是一种代码级别的说明。注解可以出现在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明。本例中的注解@Override
用来说明其后的方法是一个重写的方法。 - 注意,重写的方法不能缩小基类中被重写方法的访问权限。
- 实现运行时多态的三个必要条件:继承、方法重写、基类变量引用派生类对象。
3、重写toString()和equals(Object obj)方法
前文中提到,java.lang.Object
类是所有类的基类,故该类中常用的方法被所有类继承。在很多情况下,开发人员需要重写继承自java.lang.Object
类的一些常用方法,toString()
方法和equals(Object obj)
方法便是比较有代表性的方法。
3.1、重写toString()方法
toString()
方法可以返回对象的字符串表示形式,该方法在java.lang.Object
类中的实现为返回类的完全限定名
+@
+hashCode()方法的返回值
,在调用System.out.println(Object x)
方法打印对象时,便会调用被打印对象的toString()
方法。在实际开发中,toString()
方法经常被重写。
3.2、重写equals(Object obj)方法
equals(Object obj)
方法用来判断其他对象是否等于当前对象,该方法在java.lang.Object
类中的实现为返回两个对象使用==
操作符进行比较运算的结果。在实际开发中,有时需要当两个对象属性值完全对应相同时即认为两个对象相同,此时,equals(Object obj)
方法需要被重写。
四、final关键字
final
是Java中一个非常重要的关键字。final
的字面意思是最终的,最后的,决定性的,不可改变的。在Java中,final
关键字表达的亦是这层含义。final
关键字可以用来修饰类、成员方法、成员变量、局部变量。
1、final关键字修饰类
有时候,出于安全考虑,有些类不允许继承。有些类定义的已经很完美,不需要再生成派生类。凡是不允许继承的类需要声明为final
类。
final
关键字修饰类的语法格式为:
[修饰符] final class 类名 {}
在JDK中,有许多声明为final
的类,比如String
、Scanner
、Byte
、Short
、Integer
、Double
等类都是final
类。
2、final关键字修饰成员方法
出于封装考虑有些成员方法不允许被派生类重写,不允许被派生类重写的方法需要声明为final
方法。
final
关键字修饰方法的语法格式为:
[修饰符] final 返回值类型 方法名称([参数列表]) {
// 方法体
}
在JDK中,也有许多被final
关键字修饰的方法,比如Object类的notify()
方法、notifyAll()
方法、wait()
方法等。
3、final关键字修饰成员变量
final
关键字也可以用来修饰成员变量,final
修饰的成员变量只能显示初始化或者构造函数初始化的时候赋值一次,以后不允许更改。final
修饰的成员变量也被称为常量,常量名称一般所有字母大写,单词中间使用下划线(_
)分割。
final
关键字修饰成员变量的语法格式为:
[修饰符] final 数据类型 常量名称;
4、final关键字修饰局部变量
final
关键字也可以用来修饰局部变量,和修饰成员变量一样,表示该变量不能被第二次赋值。final
关键字修饰局部变量的语法格式和修饰成员变量的语法相同。
说明:
- 注意,对于引用类型的局部变量,被
final
修饰的变量所代表的内存中存储的内存地址编号只能初始化一次,但引用的对象中的数据是可以被多次赋值的。 - 本质上,被
final
修饰的变量(包括成员变量和局部变量),变量所代表的内存中存储的数据只能初始化一次,不能被二次赋值。
五、static关键字
static
也是Java中一个非常重要的关键字。final
的字面意思是静止的,静态的。在Java中,static
关键字可以用来声明类的静态成员、声明静态导入等。
1、静态成员
在Java中,类的成员也可以分为两种,分别是实例成员和类成员。
实例成员是属于对象的,实例成员包括实例成员变量和实例成员方法。只有创建了对象之后,才能通过对象访问实例成员变量、调用实例成员方法。
类成员是属于类的,类成员在声明时需要使用static
修饰符修饰。类成员包括类成员变量和类成员方法。通过类名可以直接访问类成员变量、调用类成员方法,也可以通过对象名访问类成员变量、调用类成员方法。
没有被static
修饰符修饰的成员变量为实例成员变量(实例变量),没有被static
修饰符修饰的成员方法为实例成员方法(实例方法);被static
修饰符修饰的成员变量为类成员变量(静态成员变量、类变量、静态变量),被static
修饰符修饰的成员方法为类成员方法(静态成员方法、类方法、静态方法)。如图:
实例成员和类成员核心的区别在于内存分配机制的不同,实例成员变量随着对象的创建在堆中分配内存,每个对象都有独立的内存空间存储各自的实例成员变量;类成员变量在程序运行期间,首次使用类名时在方法区中分配内存,并且只分配一次,无论使用类名还是对象访问类成员变量时,访问的都是方法区中同一块内存。实例成员方法必须由堆中的对象调用;类成员方法可以直接使用类名调用。
需要注意的是,由于实例成员和类成员内存分配机制的不同,显而易见的现象是,在类体中,可以在一个实例成员方法中调用类成员方法,反之则不行;可以将一个类成员变量赋值给一个实例成员变量,反之则不行。
另外,还需提到的是,类成员方法不能(事实上也无需)被重写,无法表现出多态性。
2、静态代码块
类中除了成员变量和成员方法外,还有其他一些成员,静态代码块便是其中之一。
实例成员是在new
时分配内存, 并且有构造函数初始化。静态成员是在 类名首次出现时分配内存 的,静态成员需要由静态代码块来初始化。首次使用类名时,首先为静态成员分配内存,然后就调用静态代码块,为静态成员初始化。注意,静态代码块只调用一次。另外,显而易见的,静态代码块中无法访问类的实例成员变量,也无法调用类的实例成员方法。
声明静态代码块的语法格式如下:
static {
// 代码
}
3、静态导入
在一个类中使用其他类的静态方法或静态变量时,可以使用static
关键字静态导入其他类的静态成员,该类中就可以直接使用其他类的静态成员。
静态导入的语法格式如下:
import static 类完全限定名.静态成员名