本文是Java基础课程的第八课。是Java面向对象编程的核心部分,主要介绍Java中的继承、装、多态等特性,最后介绍Java中final关键字和static关键字的作用
文章目录
一、继承
1、继承的概念
1.1、生活中的继承
面向对象的方法论告诉我们,在认识世界的过程中,万事万物皆为对象,对象按状态和行为可以归类。而在现实世界中进行对象的归类时,会发现类与类之间也经常存在包含与从属的关系。
比如将笔记本电脑与台式电脑分别归类,而笔记本电脑和台式电脑又都属于电脑,图示如下:
笔记本电脑和台式电脑都具有电脑的一般特征,但同时又具有各自不同的特征;笔记本电脑、台式电脑相对于电脑要更加具体,而电脑相对于笔记本电脑或台式电脑要更加通用。此时,可以认为笔记本电脑、台式电脑继承了电脑,电脑派生了笔记本电脑、台式电脑。
再比如,兔子和羊属于食草动物,狮子和豹属于食肉动物,食草动物和食肉动物又同时属于动物,图示如下:
此时,可以认为食草动物、食肉动物继承了动物,动物派生了食草动物、食肉动物;兔子、羊继承了食草动物,食草动物派生了兔子、羊;狮子、豹继承了食肉动物,食肉动物派生了狮子、豹。
严格的继承需要符合的关系是 is-a ,父类更通用,子类更具体。即笔记本电脑 is a 电脑,羊 is a 食草动物,食草动物 is a 动物。
1.2、Java中的继承
Java是一种面向对象的编程语言,其非常注重的一点便是让开发人员在设计软件系统时能够运用面向对象的思想自然地描述现实生活中的问题域,事实上,使用Java编程时,也能够描述现实生活中的继承关系,甚至可以说,继承是Java面向对象编程技术的一块基石。
Java中的继承允许开发人员创建分等级层次的类。利用继承机制,可以先创建一个具有共性的一般类,根据该一般类再创建具有特殊性的新类,新类继承一般类的属性和行为,并根据需要定制它自己的属性和行为。通过继承创建的新类称为 派生类(或子类),被继承的具有共性的一般类称为 基类(或超类、父类)。
继承使派生类获得了能够直接使用基类的属性和行为的能力,也使得基类能够在无需重新编写代码的情况下通过派生类进行功能的扩展。继承的过程,就是从一般到特殊的过程。类的继承机制是Java面向对象程序设计中的核心特征,是实现软件可重用性的重要手段,是实现多态的基础。
2、Java中继承的实现
2.1、Java中继承的语法
Java中声明派生类继承某基类的语法格式如下:
[修饰符] class 派生类名 extends 基类名 {
// 派生类成员变量
// 派生类成员方法
}
说明:
- 如上,Java中使用
extends
关键字实现类的继承。
下面是一个示例:
基类Animal
类的源码:
package com.codeke.java.test;
/**
* 动物类
*/
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);
}
}
派生类Cat
类的源码:
package com.codeke.java.test;
/**
* 猫类
*/
public class Cat extends Animal {
/**
* 派生类构造函数
* @param breed 品种
* @param name 名称
*/
public Cat(String breed, String name) {
super("猫", breed, name);
}
}
派生类Dog
类的源码:
package com.codeke.java.test;
/**
* 狗类
*/
public class Dog extends Animal {
/**
* 派生类构造函数
* @param breed 品种
* @param name 名称
*/
public Dog(String breed, String name) {
super("狗", breed, name);
}
}
派生类Duck
类的源码:
package com.codeke.java.test;
/**
* 鸭子类
*/
public class Duck extends Animal {
/**
* 派生类构造函数
* @param breed 品种
* @param name 名称
*/
public Duck(String breed, String name) {
super("鸭子", breed, name);
}
}
测试类PetShop
类的源码:
package com.codeke.java.test;
/**
* 宠物店(测试类)
*/
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对各种形式的继承类型支持情况如下所示:
说明:
- 需要注意的是:Java中的类不支持多继承!
3、this和super
3.1、this
this
是Java中的关键字,this
可以理解为指向当前对象(正在执行方法的对象)本身的一个引用。
Java中,this
关键字只能在没有被static
关键字修饰的方法中使用。其主要的应用场景有下面几种。
第一,作为当前对象本身的引用直接使用。
第二,访问当前对象本身的成员变量。当方法中有局部变量和成员变量重名时,访问成员变量需要使用this.成员变量名
。
下面是一个示例:
Person
类的源码:
package com.codeke.java.test;
/**
* 人类
*/
public class Person {
String name; // 名称
int age; // 年龄
int sex; // 性别( 1:男 0:女 )
Person partner; // 伴侣
/**
* 构造方法
* @param name 姓名
* @param age 年龄
* @param sex 性别
*/
public Person(String name, int age, int sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
/**
* 坠入爱河的方法
* @param person 那个要一同坠入爱河的人
*/
public void fallInLove(Person person) {
// 性别一样不能fall in love
if (this.sex == person.sex) {
System.out.printf("%s和%s性别相同,无法fall in love.\n",
this.name, person.name);
return;
}
// 自己未满18,不能fall in love
if (this.age < 18) {
System.out.printf("%s太小,无法fall in love.\n",
this.name);
return;
}
// 对方未满18,不能fall in love
if (person.age < 18) {
System.out.printf("%s太小,无法fall in love.\n",
person.name);
return;
}
// 自己有对象,不能再fall in love
if (this.partner != null) {
System.out.printf("%s已经fall in love with %s,无法再fall in love with %s.\n",
this.name, this.partner.name, person.name);
return;
}
// 对方有对象,不能再fall in love
if (person.partner != null) {
System.out.printf("%s已经fall in love with %s,无法再fall in love with %s.\n",
person.name, person.partner.name, this.name);
return;
}
// 两人fall in love
this.partner = person;
person.partner = this;
// 打印
System.out.printf("%s fall in love with %s.\n",
this.name, person.name);
}
}
Test
类的源码:
package com.codeke.java.test;
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
// 实例化若干person
Person person1 = new Person("宋江",18, 1);
Person person2 = new Person("武松",19, 1);
Person person3 = new Person("燕青",17, 1);
Person person4 = new Person("扈三娘",16, 0);
Person person5 = new Person("孙二娘",18, 0);
// 开发 fall in love 吧
person1.fallInLove(person3);
person2.fallInLove(person4);
person3.fallInLove(person5);
person1.fallInLove(person5);
person2.fallInLove(person5);
}
}
说明:
- 本例中多次使用
this.成员变量名
来访问当前对象的成员变量。 - 本例中的代码
person.partner = this;
,作用是将方法形参代表的person
对象的伴侣赋值为当前正在执行方法的对象,即将this
作为当前对象本身的引用来使用。
第三,调用当前对象本身的成员方法。作用与访问成员变量类似。
下面是一个示例:
Person
类修改后的源码:
package com.codeke.java.test;
/**
* 人类
*/
public class Person {
// 成员变量部分和上例中一样
// 构造方法和上例中一样
/**
* 自我介绍的方法
*/
public void introduce() {
System.out.printf("大家好,我是%s,我想谈恋爱。\n", this.name);
}
/**
* 坠入爱河的方法
* @param person 那个要一同坠入爱河的人
*/
public void fallInLove(Person person) {
// 先自我介绍下
this.introduce();
// 后面的代码和上例中一样
...
}
}
说明:
- 本例中,为
Person
类增加了自我介绍的方法introduce()
,并在fallInLove(Person person)
方法中使用this.introduce();
对introduce()
方法进行了调用,即仍然是当前执行方法的person
对象调用了introduce()
方法。
第四,调用本类中的其他构造方法,语法格式为:
this([参数1, ..., 参数n]);
下面是一个示例:
Person
类修改后的源码:
package com.codeke.java.test;
/**
* 人类
*/
public class Person {
// 成员变量部分和上例中一样
/**
* 构造方法,调用该构造方法,age属性会初始化为18
* @param name 名称
* @param sex 性别
*/
public Person(String name, int sex) {
this.name = name;
this.sex = sex;
this.age = 18;
}
/**
* 构造方法
* @param name 姓名
* @param age 年龄
* @param sex 性别
*/
public Person(String name, int age, int sex) {
this(name, sex);
this.age = age;
}
// introduce() 方法和 fallInLove(Person person) 方法和上例中一样
}
说明:
- 本例中,
Person
类中新增了构造方法Person(String name, int sex)
,而在另一个构造方法中使用this(name, sex);
调用了新增的构造方法。 this([参数1, ..., 参数n]);
语句必须位于其他构造方法中的第一行。
3.2、super
super
也是Java中的关键字,super
可以理解为是指向当前对象的基类对象的一个引用,而这个基类指的是离自己最近的一个基类。
Java中,super
关键字也只能在没有被static
关键字修饰的方法中使用。其主要的应用场景有下面几种。
第一,访问当前对象的基类对象的成员变量。当方法中有基类成员变量和其他变量重名时,访问基类成员变量需要使用super.基类成员变量名
。
第二,访问当前对象的基类对象的成员方法。作用与访问成员变量类似。
第三,调用基类的构造方法,语法格式为:
super([参数1, ..., 参数n]);
下面是一个示例:
Person
类修改后的源码:
package com.codeke.java.test;
/**
* 人类
*/
public class Person {
String name; // 名称
int age; // 年龄
int sex; // 性别( 1:男 0:女 )
/**
* 构造方法
* @param name 姓名
* @param age 年龄
* @param sex 性别
*/
public Person(String name, int age, int sex) {
this.name = name;
this.sex = sex;
this.age = age;
}
/**
* 自我介绍的方法
*/
public void introduce() {
System.out.printf("大家好,我是%s,我是%s生,我今年%d岁。\n",
this.name, this.sex == 0 ? "女" : "男", this.age);
}
}
Boy
类的源码:
package com.codeke.java.test;
/**
* 男生类
*/
public class Boy extends Person {
/**
* 构造方法
* @param name 姓名
* @param age 年龄
*/
public Boy(String name, int age) {
super(name, age, 1);
System.out.printf("创建了一个%s生对象。\n",
super.sex == 0 ? "女" : "男");
}
/**
* 讲话的方法
*/
public void say() {
super.introduce();
}
}
说明:
- 在本例的
Boy
类中,使用super(name, age, 1);
调用了基类Person
类的构造方法Person(String name, int age, int sex)
;使用super.sex
访问了当前对象的基类对象的成员变量;使用super.introduce();
调用了当前对象的基类对象的成员方法。 super([参数1, ..., 参数n]);
语句必须位于派生类构造方法中的第一行。super([参数1, ..., 参数n]);
语句和this([参数1, ..., 参数n]);
语句无法同时出现在同一个构造方法中。- 事实上,每个派生类的构造方法中,如果第一行没有写
super([参数1, ..., 参数n]);
语句,都会隐含地调用super()
,如果基类没有无参的构造方法,那么在编译的时候就会报错。
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、案例
下面是一个示例:
Animal
类的源码:
package com.codeke.java.test;
/**
* 动物类
*/
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);
}
}
Test
类的源码:
package com.codeke.java.test;
/**
* 测试类
*/
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 | |
---|---|---|---|---|
当前类中能否访问 | ||||
同包子类中能否访问 | ||||
同包非子类中能否访问 | ||||
不同包子类中能否访问 | ||||
不同包非子类中能否访问 |
下面是一个示例:
com.codeke.java.test1
包下Person
类的源码:
package com.codeke.java.test1;
/**
* 人类
*/
public class Person {
public String name; // 名称
protected int age; // 年龄
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;
}
/**
* 自我介绍的方法
*/
public void introduce() {
System.out.printf("大家好,我是%s,我是%s生,我今年%d岁,我的爱好是%s。\n",
this.name, this.sex == 0 ? "女" : "男", this.age, this.favourite);
}
/**
* 阅读的方法
*/
protected void read() {
System.out.printf("我是%s,我正在阅读。\n", this.name);
}
/**
* 写作的方法
*/
void write() {
System.out.printf("我是%s,我正在写作。\n", this.name);
}
/**
* 休息的方法
*/
private void rest() {
System.out.printf("我是%s,我正在休息。\n", this.name);
}
}
com.codeke.java.test1
包下Boy
类的源码:
package com.codeke.java.test1;
/**
* 男生类
*/
public class Boy extends Person {
/**
* 构造方法
* @param name 姓名
* @param age 年龄
* @param favourite 爱好
*/
public Boy(String name, int age, String favourite) {
super(name, age, 1, favourite);
}
/**
* 做某些事的方法
*/
public void doSomething () {
this.introduce();
this.read();
this.write();
}
}
com.codeke.java.test2
包下Girl
类的源码:
package com.codeke.java.test2;
import com.codeke.java.test1.Person;
/**
* 女生类
*/
public class Girl extends Person {
/**
* 构造方法
* @param name 姓名
* @param age 年龄
* @param favourite 爱好
*/
public Girl(String name, int age, String favourite) {
super(name, age, 0, favourite);
}
/**
* 做某些事的方法
*/
protected void doSomething () {
this.introduce();
this.read();
}
}
说明:
- 尝试在
com.codeke.java.test2
包及com.codeke.java.test2
包下新建测试类,在main
方法中创建Person
类、Boy
类、GIrl
类的对象,访问这些对象的属性及方法,观察它们被不同的访问权限修饰符修饰时的效果。
3、getter/setter访问器
在之前的例子中,类中的成员变量都是缺省权限修饰符的,这在一定程度上破坏了类的封装性。事实上,在Java中极力提倡使用private
修饰类的成员变量,然后提供一对public
的getter
方法和setter
方法对私有属性进行访问。这样的getter
方法和setter
方法也被称为属性访问器。
下面是一个示例:
package com.codeke.java.test;
/**
* 人类
*/
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中多态的实现
在Java中,多态可以通过方法重载(overload)和方法重写(override)来实现。
2.1、方法的重载(overload)
在之前的章节中已经介绍过方法的重载(overload)。在一个类中,多个方法具有相同的方法名称,但却具有不同的参数列表,与返回值无关,称作方法重载(overload)。
重载的方法在程序设计阶段根据调用方法时的参数便已经可以确定调用的是具体哪一个方法实现,故方法重载体现了设计时多态。
下面是一个示例:
Animal
类的源码:
package com.codeke.java.test;
/**
* 动物类
*/
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 = "未知";
}
}
测试类PetShop
类的源码:
package com.codeke.java.test;
/**
* 宠物店(测试类)
*/
public class PetShop {
/**
* 用来测试的main方法
*/
public static void main(String[] args) {
// 定义若干动物对象,使用了各种不同的Animal类的构造方法
Animal animal1 = new Animal("狗", "牧羊犬", "大黑");
Animal animal2 = new Animal("猫", "大花");
Animal animal3 = new Animal("大鸭");
}
}
说明:
PetShop
类的main
方法中使用了各种不同的Animal
类的构造方法,在设计阶段,根据这些构造方法的入参,开发者就已经可以确定调用哪一个具体的构造方法。
2.2、方法的重写(override)
方法重写(override)指在继承关系中,派生类重写基类的方法,以达到同一个方法在不同的派生类中有不同的实现。
如果基类中的方法实现不适合派生类,派生类便可以重新定义。派生类中定义的方法与基类中的方法具有相同的返回值、方法名称和参数列表,但具有不同的方法体,称之为派生类重写了基类的方法。
仅仅在派生类中重写了基类的方法,仍然不足以体现出多态性,还需要使用面向对象程序设计中的一条基本原则,即 里氏替换原则 。里氏替换原则 表述为,任何基类可以出现的地方,派生类一定可以出现。直白的说就是基类类型的变量可以引用派生类的对象(即基类类型的变量代表的内存中存储的是一个派生类的对象在内存中的地址编号)。此时,通过基类类型的变量调用基类中的方法,真正的方法执行者是派生类的对象,被执行的方法如果在派生类中被重写过,实际执行的便是派生类中的方法体。
通过上述方式,相同基类类型的变量调用相同方法,根据调用方法的具体派生类对象的不同,便可以执行不同的方法实现。由于在程序运行阶段,变量引用的内存地址才能最终确定,故这种形式的多态体现了运行时多态。
下面是一个示例:
基类Animal
类的源码:
package com.codeke.java.test;
/**
* 动物类
*/
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() { }
}
派生类Cat
类的源码:
package com.codeke.java.test;
/**
* 猫类
*/
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());
}
}
派生类Dog
类的源码:
package com.codeke.java.test;
/**
* 狗类
*/
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());
}
}
测试类PetShop
类的源码:
package com.codeke.java.test;
/**
* 宠物店(测试类)
*/
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()
方法经常被重写。
下面是一个示例:
Person
类的源码:
package com.codeke.java.test;
/**
* 人类
*/
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;
}
/**
* 重写的toString()方法
* @return 描述Person对象的字符串
*/
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", sex=" + sex +
", favourite='" + favourite + '\'' +
'}';
}
}
Test
类的源码:
package com.codeke.java.test;
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
Person person1 = new Person("宋江",18, 1, "结交朋友");
Person person2 = new Person("武松",19, 1, "打架");
System.out.println("person1 = " + person1);
System.out.println("person2 = " + person2);
}
}
执行输出结果:
person1 = Person{name='宋江', age=18, sex=1, favourite='结交朋友'}
person2 = Person{name='武松', age=19, sex=1, favourite='打架'}
说明:
- 观察本例的输出结果,
System.out.println(Object x)
打印Person
类的对象时,使用的是Person
中重写过的toString()
的实现。
3.2、重写equals(Object obj)方法
equals(Object obj)
方法用来判断其他对象是否等于当前对象,该方法在java.lang.Object
类中的实现为返回两个对象使用==
操作符进行比较运算的结果。在实际开发中,有时需要当两个对象属性值完全对应相同时即认为两个对象相同,此时,equals(Object obj)
方法需要被重写。
下面是一个示例:
Person
类的源码:
package com.codeke.java.test;
/**
* 人类
*/
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;
}
/**
* 重写的equals(Object o)方法
* @param o 要比较的对象
* @return 比较结果
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
if (age != person.age) return false;
if (sex != person.sex) return false;
if (name != null ? !name.equals(person.name) : person.name != null) return false;
return favourite != null ? favourite.equals(person.favourite) : person.favourite == null;
}
}
Test
类的源码:
package com.codeke.java.test;
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
Person person1 = new Person("宋江",18, 1, "结交朋友");
Person person2 = new Person("宋江",18, 1, "结交朋友");
System.out.println("person1.equals(person2) = " + person1.equals(person2));
}
}
执行输出结果:
person1.equals(person2) = true
说明:
- 观察本例中重写的
equals(Object obj)
方法,依次比较两个对象内存地址是否相同,数据类型是否相同,属性值是否全部对应相同。Person
类的对象person1
和person2
属性值完全对应相同,故person1.equals(person2)
的结果为true
。 - 之前的章节中提到,字符串比较字面值是否相同时,需要使用字符串的
equals(Object anObject)
方法,其本质便是String
类重写了equals(Object obj)
方法。String
类重写过的equals(Object anObject)
方法如下:public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
四、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 数据类型 常量名称;
下面是一个示例:
MathUtils
类的源码:
package com.codeke.java.test;
/**
* 数学工具类
*/
public class MathUtils {
public final double PI = 3.14159265358979323846;
public final double E;
/**
* 构造方法
*/
public MathUtils(double E) {
this.E = E;
}
}
Test
类的源码:
package com.codeke.java.test;
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
MathUtils mathUtils = new MathUtils(2.7182818284590452354);
}
}
说明:
- JDK中提供了
java.lang.Math
类,该类中提供了常量PI
和常量E
。 - 常量无法改变,故不需要提供
setter
访问器。
4、final关键字修饰局部变量
final
关键字也可以用来修饰局部变量,和修饰成员变量一样,表示该变量不能被第二次赋值。final
关键字修饰局部变量的语法格式和修饰成员变量的语法相同。
下面是一个示例:
package com.codeke.java.test;
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
// final修饰的局部变量,只能初始化一次,不能被第二次赋值
final String str1 = "hello";
// 这样也是只初始化了一次
final String str2;
str2 = "world";
// 对于引用类型的局部变量,被final修饰的变量中的内存地址编号只能初始化一次,
// 但引用的地址中的数据是可以被多次赋值的
final int[] nums = new int[]{1, 2, 3};
nums[0] = 4;
nums[0] = 5;
}
}
说明:
- 注意,对于引用类型的局部变量,被
final
修饰的变量所代表的内存中存储的内存地址编号只能初始化一次,但引用的对象中的数据是可以被多次赋值的。 - 本质上,被
final
修饰的变量(包括成员变量和局部变量),变量所代表的内存中存储的数据只能初始化一次,不能被二次赋值。
五、static关键字
static
也是Java中一个非常重要的关键字。final
的字面意思是静止的,静态的。在Java中,static
关键字可以用来声明类的静态成员、声明静态导入等。
1、静态成员
之前的章节和内容中不断提到类的成员(成员变量和成员方法),在Java中,类的成员也可以分为两种,分别是实例成员和类成员。
实例成员是属于对象的,实例成员包括实例成员变量和实例成员方法。只有创建了对象之后,才能通过对象访问实例成员变量、调用实例成员方法。
类成员是属于类的,类成员在声明时需要使用static
修饰符修饰。类成员包括类成员变量和类成员方法。通过类名可以直接访问类成员变量、调用类成员方法,也可以通过对象名访问类成员变量、调用类成员方法。
没有被static
修饰符修饰的成员变量为实例成员变量(实例变量),没有被static
修饰符修饰的成员方法为实例成员方法(实例方法);被static
修饰符修饰的成员变量为类成员变量(静态成员变量、类变量、静态变量),被static
修饰符修饰的成员方法为类成员方法(静态成员方法、类方法、静态方法)。如下图:
实例成员和类成员核心的区别在于内存分配机制的不同,实例成员变量随着对象的创建在堆中分配内存,每个对象都有独立的内存空间存储各自的实例成员变量;类成员变量在程序运行期间,首次使用类名时在方法区中分配内存,并且只分配一次,无论使用类名还是对象访问类成员变量时,访问的都是方法区中同一块内存。实例成员方法必须由堆中的对象调用;类成员方法可以直接使用类名调用。
需要注意的是,由于实例成员和类成员内存分配机制的不同,显而易见的现象是,在类体中,可以在一个实例成员方法中调用类成员方法,反之则不行;可以将一个类成员变量赋值给一个实例成员变量,反之则不行。
另外,还需提到的是,类成员方法不能(事实上也无需)被重写,无法表现出多态性。
下面是一个示例:
Chinese
类的源码:
package com.codeke.java.Test;
/**
* 中国人类
*/
public class Chinese {
private String name; // 名称
private int age; // 年龄
private int sex; // 性别( 1:男 0:女 )
public static String eyeColor = "黑色"; // 眼睛颜色
public static String skinColor = "黄色"; // 皮肤颜色
/**
* 构造方法
* @param name 姓名
* @param age 年龄
* @param sex 性别
*/
public Chinese(String name, int age, int sex) {
this.name = name;
this.sex = sex;
this.age = age;
}
}
Test
类的源码:
package com.codeke.java.Test;
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
Chinese chinese1 = new Chinese("宋江",18, 1);
Chinese chinese2 = new Chinese("武松",19, 1);
System.out.println(Chinese.eyeColor);
System.out.println(Chinese.skinColor);
System.out.println(chinese1.eyeColor);
System.out.println(chinese1.skinColor);
System.out.println(chinese2.eyeColor);
System.out.println(chinese2.skinColor);
}
}
执行输出结果:
黑色
黄色
黑色
黄色
黑色
黄色
说明:
- 本例中,
Chinese
类中的成员变量eyeColor
和skinColor
是静态的,无论使用Chinese
类名还是Chinese
类的对象chinese1
及chinese2
,访问这两个类成员变量时,都访问的是方法区中的同一块内存地址。图示如下:
- 类成员也体现了面向对象的思想,它可以描述某一类中具有共性的,可以不依赖于对象而存在的成员。比如本例中,就算没有任何一个中国人的对象存在,中国人的眼睛颜色也应该是黑色的,皮肤颜色也应该是黄色的。
下面是另一个示例:
java.lang.Math
类的部分源码:
package java.lang;
public final class Math {
public static final double E = 2.7182818284590452354;
public static final double PI = 3.14159265358979323846;
public static int addExact(int x, int y) {
int r = x + y;
// HD 2-12 Overflow iff both arguments have the opposite sign of the result
if (((x ^ r) & (y ^ r)) < 0) {
throw new ArithmeticException("integer overflow");
}
return r;
}
public static int subtractExact(int x, int y) {
int r = x - y;
// HD 2-12 Overflow iff the arguments have different signs and
// the sign of the result is different than the sign of x
if (((x ^ y) & (x ^ r)) < 0) {
throw new ArithmeticException("integer overflow");
}
return r;
}
}
说明:
java.lang.Math
类中的成员PI
、E
都是静态的,绝大多数成员方法也都是静态的。将这些成员修饰为静态,使的Math
类在语义上更加自然,在开发过程中的使用上更加方便。- 注意观察成员
PI
、E
,它们都是自然存在的常数,故由final
关键字修饰而成为常量;同时,由于它们可以不依赖于Math
类的对象而存在,故又由static
关键子修饰而成为静态成员。此时,它们的唯一性、确定性已经得到了保证,为了方便使用起见,可以将它们的权限修饰符声明为public
。事实上,在Java中,常量通常都是由public static final
共同修饰的。
2、静态代码块
类中除了成员变量和成员方法外,还有其他一些成员,静态代码块便是其中之一。
实例成员是在new
时分配内存, 并且有构造函数初始化。静态成员是在类名首次出现时分配内存的,静态成员需要由静态代码块来初始化。首次使用类名时,首先为静态成员分配内存,然后就调用静态代码块,为静态成员初始化。注意,静态代码块只调用一次。另外,显而易见的,静态代码块中无法访问类的实例成员变量,也无法调用类的实例成员方法。
声明静态代码块的语法格式如下:
static {
// 代码
}
下面是一个示例:
Chinese
类的源码:
package com.codeke.java.Test;
/**
* 中国人类
*/
public class Chinese {
private String name; // 名称
private int age; // 年龄
private int sex; // 性别( 1:男 0:女 )
public static String eyeColor; // 眼睛颜色
public static String skinColor; // 皮肤颜色
static {
eyeColor = "黑色";
skinColor = "黄色";
}
/**
* 构造方法
* @param name 姓名
* @param age 年龄
* @param sex 性别
*/
public Chinese(String name, int age, int sex) {
this.name = name;
this.sex = sex;
this.age = age;
}
}
3、静态导入
在一个类中使用其他类的静态方法或静态变量时,可以使用static
关键字静态导入其他类的静态成员,该类中就可以直接使用其他类的静态成员。
静态导入的语法格式如下:
import static 类完全限定名.静态成员名
下面是一个示例:
package com.codeke.java.test;
import static java.lang.Math.E;
import static java.lang.Math.PI;
import static java.lang.Math.addExact;
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
System.out.println("PI = " + PI);
System.out.println("E = " + E);
System.out.println("addExact(1,2) = " + addExact(1, 2));
}
}
说明:
- 本例的测试类中使用
import static
导入一些了java.lang.Math
类的静态成员,于是这些静态成员在main
方法中可以直接使用。