JAVA学习笔记 08 - 继承、封装、多态

本文是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关键字显示调用基类拥有的某个构造方法
  • thissuper都是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私有),它们的具体作用如下:

  1. public:被public修饰的成员变量和成员方法可以在所有类中访问。(注意:所谓在某类中访问某成员变量是指在该类的方法中给该成员变量赋值和取值。所谓在某类中访问某成员方法是指在该类的方法中调用该成员方法。)
  2. protected:被protected修饰的成员变量和成员方法可以声明它的类中访问,在该类的子类中访问,也可以在与该类位于同一个包中的类访问,但不能在位于其它包非子类中访问
  3. 缺省缺省不使用权限修饰符。不使用权限修饰符修饰的成员变量和成员方法可以声明它的类中访问,也可以在与该类位于同一个包中的类访问,但不能在位于其它包的类中访问
  4. 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修饰类的成员变量,然后提供一对publicgetter方法和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类的对象person1person2属性值完全对应相同,故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的类,比如StringScannerByteShortIntegerDouble等类都是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类中的成员变量eyeColorskinColor是静态的,无论使用Chinese类名还是Chinese类的对象chinese1chinese2,访问这两个类成员变量时,都访问的是方法区中的同一块内存地址。图示如下:
    在这里插入图片描述
  • 类成员也体现面向对象思想,它可以描述某一类中具有共性的,可以不依赖于对象而存在的成员。比如本例中,就算没有任何一个中国人的对象存在,中国人的眼睛颜色也应该是黑色的,皮肤颜色也应该是黄色的。

下面是另一个示例:
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类中的成员PIE都是静态的,绝大多数成员方法也都是静态的。将这些成员修饰为静态,使的Math类在语义上更加自然,在开发过程中的使用上更加方便。
  • 注意观察成员PIE,它们都是自然存在的常数,故由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方法中可以直接使用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章