第八章 封装、继承和多态及this,super,final,static关键字

一、封装

1、什么是封装

所谓封装,即是对信息(属性、方法的实现细节等)进行隐藏。封装亦是面向对象编程中基本的、核心的特征之一。

Java中,一个类就是一个封装了数据(属性)以及操作这些数据的代码(方法)的逻辑实体。具体的说,在将客观事物按照面向对象思想抽象成类的过程中,开发人员可以给类中不同的成员提供不同级别的保护。比如可以公开某些成员使其能够被外界访问;可以将某些成员私有,使其不能被外界访问;或者给予某些成员一定的访问权限,使其只能被特定、有限的外界访问。

通过封装,Java中的类既提供了能够与外部联系的必要的API,也尽可能的隐藏了类的实现细节,避免了这些细节被外部程序意外的改变或被错误的使用。

封装为软件提供了一种安全的、健壮的、模块化的设计机制。类的设计者提供标准化的类,而使用者根据实际需求选择和组装各种功能的类,通过API使它们协同工作,从而实现软件系统。在具体开发的过程中,类的设计者需要考虑如何定义类中的成员变量和方法,如何设置其访问权限等问题。类的使用者只需要知道有哪些类可以选择,每个类有哪些功能,每个类中有哪些可以访问的成员变量和成员方法等,而不需要了解其实现的细节。

2、类中成员的访问权限

按照封装的原则,类的设计者既要提供类与外部的联系方式,又要尽可能的隐藏类的实现细节,具体办法就是为类的成员变量和成员方法设置合理的访问权限。

Java为类中的成员提供了四种访问权限修饰符,它们分别是public(公开)、protected(保护)、缺省private(私有),它们的具体作用如下:
在这里插入图片描述

public:被public修饰的成员变量和成员方法可以在所有类中访问。(注意:所谓在某类中访问某成员变量是指在该类的方法中给该成员变量赋值和取值。所谓在某类中访问某成员方法是指在该类的方法中调用该成员方法。)

protected:被protected修饰的成员变量成员方法可以在声明它的类中访问,在该类的子类中访问,也可以在与该类位于同一个包中的类访问,但不能在位于其它包的非子类中访问。

default(缺省):缺省指不使用权限修饰符。不使用权限修饰符修饰的成员变量和成员方法可以在声明它的类中访问,也可以在与该类位于同一个包中的类访问,但不能在位于其它包的类中访问。

private:被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、getters/setters访问器

在之前的例子中,类中的成员变量都是缺省权限修饰符的,这在一定程度上破坏了类的封装性。事实上,在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、生活中的继承

面向对象的方法论告诉我们,在认识世界的过程中,万事万物皆为对象,对象按状态和行为可以归类。而在现实世界中进行对象的归类时,会发现类与类之间也经常存在包含与从属的关系。

兔子和羊属于食草动物,狮子和豹属于食肉动物,食草动物和食肉动物又同时属于动物,图示如下:

此时,可以认为食草动物、食肉动物继承了动物,动物派生了食草动物、食肉动物;兔子、羊继承了食草动物,食草动物派生了兔子、羊;狮子、豹继承了食肉动物,食肉动物派生了狮子、豹。

严格的继承需要符合的关系是 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 cn.edu.bennett;

/**
 * 动物类
 */
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 cn.edu.bennett;

/**
 * 猫类
 */
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()方法,或者某些其他线程中断当前线程,或一定量的实时时间

2.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、多态的概念

1.1、生活中的多态

多态简单的理解就是多种形态、多种形式。具体来说,多态是指同一个行为具有多个不同表现形式或形态。

1.2、Java中的多态

Java中,多态是指同一名称的方法可以有多种实现(方法实现是指方法体)。系统根据调用方法的参数或调用方法的对象自动选择某一个具体的方法实现来执行。多态亦是面向对象的核心特征之一。

多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们可以通过相同的方式予以调用。

多态一种方法,多种实现

重载
重写
多态
参数来判别
调用对象的真实类型来判别

2、Java中多态的实现

Java中,多态可以通过方法重载overload)和方法重写override)来实现。

2.1、方法的重载(overload)

重载(overload)就是在一个类中,多个方法具有相同的方法名称,但却具有不同的参数列表,与返回值无关,称作方法重载(overload)。

重载的方法在程序设计阶段根据调用方法时的参数便已经可以确定调用的是具体哪一个方法实现,故方法重载体现了设计时多态。

小结:

  • 1、构成重载的条件?
    两同三不同
    • 两同:同一个的同名方法
    • 三不同:方法的参数(个数,类型,顺序)
      • 仅是参数名称不同会不会构成重载?不行。why?默认情况下。形参编译后名字被擦除(字节码文件中不存在形参的名称)。
      • 仅是返回类型不同,能不能构成重载?不行。why?有返回类型的方法,在调用时也可以不接收返回值,所以靠返回值没法定位函数。
      • 父类的同名方法,能否和子类的方法构成重载?不能,但是它可以和从父类继承来的方法构成重载(在子类中没有重写父类中的同名方法)。
  • 2、常见的重载方法有哪些?
    • System.out.println()要做那么多重载?这样有什么好处?
      ​ 任意数据类型都能打印。为什么不写8个方法?而是重载成同名方法?不让程序员那么操心,你只要打印,认真的关注业务逻辑。具体的打印方式,我会使用重载,分类解决。
    • 构造方法,有参无参的构造方法和参数个数不同的构造方法。

示例1:
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 = "未知";
	}
}

Test类:

package cn.edu.bennett.test;

/**
 * 宠物店(测试类)
 */
public class Test {
	/**
	 * 用来测试的main方法
	 */
	public static void main(String[] args) {
		// 定义若干动物对象,使用了各种不同的Animal类的构造方法
		Animal animal1 = new Animal("狗", "牧羊犬", "大黑");
		Animal animal2 = new Animal("猫", "大花");
		Animal animal3 = new Animal("大鸭");
	}
}

解释
Test类的main方法中使用了各种不同的Animal类构造方法,在设计阶段,根据这些构造方法的参数个数,开发者就已经可以确定调用哪一个具体的构造方法。

示例
PlayBasketBall

package cn.edu.bennett.test;

/**
 * @version:
 * @Description:打篮球
 * @author bennett
 * @date: 2021年9月8日 下午2:51:12
 */ 
public class PlayBasketBall {
}

PlayFootball

package cn.edu.bennett.test;

/**
 * @version:
 * @Description:踢足球
 * @author bennett
 * @date: 2021年9月8日 下午2:51:24
 */ 
public class PlayFootball {
}

Parent

package cn.edu.bennett.test;

/**
 * @version:
 * @Description: 父类
 * @author bennett
 * @date: 2021年9月8日 下午2:52:08
 */ 
public class Parent {
	public void test(byte b) {
		System.out.println("我就是父类的test(byte)方法");
	}
}

Test

package cn.edu.bennett.test;

public class Test extends Parent {
	public static void main(String[] args) {
		OverloadTest test = new OverloadTest();
		test.play(new PlayBasketBall());
		test.play(new PlayFootball());
		
//		程序也能做到一个(同名)方法,有多种不同的实现 ?根据处理的对象不同,自己去定位,调用那种实现。
		int ret=test.test();
		System.out.println(ret);
		
		byte b = 12;
		test.test(b);
		test.test(1);
		test.test("a");
		test.test("a", 1);
		test.test(1,"a");
	}	
	/**
	 * @Description:重载方法play
	 * @param basketBall
	 */
	public void play(PlayBasketBall basketBall) {
		System.out.println("我在打篮球");
	}
	/**
	 * @Description:重载方法play
	 * @param football
	 */
	public void play(PlayFootball football) {
		System.out.println("我在踢足球");
	}
	/**
	 * @Description:重载方法test
	 * @return null
	 */
	public int test() {
		return 0;
	}
	/**
	 * @Description:重载方法test
	 * @param int a
	 */
	public void test(int a) {
		System.out.println("我返回int");
	}
	/**
	 *本类中创建了和父类parent相同的方法(同名,同参),即重写了父类方法
	 */
	public void test(byte b) {
		System.out.println("我继承了parent类,parent类中也有和我一样的方法。");
	}
	//继承(Parent父类)的test方法和本类中方法同名,参数类型也相同,在本类中未创建test(byte b)时也形成了方法重载
	/*
	 * public void test(byte b) { System.out.println("我就是父类的test(byte)方法"); }
	 */
	
	/**
	 * @Description:重载方法test
	 * @param str
	 */
	private void test(String str) {
		System.out.println("我返回string");
	}
	/**
	 * @Description:重载方法test
	 * @param string str
	 * @param int i
	 */
	private void test(String str,int i) {
		System.out.println("我返回string和int");
	}
	/**
	 * @Description:重载方法test
	 * @param int i
	 * @param string str
	 */
	private void test(int i,String str) {
		System.out.println("我返回int和string");
	}	
}

解释

  1. PlayFootballPlayBasketball是引用类型,创建对象后调用重载方法test(Object obj)
  2. 创建重载方法test:test(),test(int i),test(String str),test(String str,int i),test(int i,String str).方法的重载就是方法要同名,但是不同参数个数,参数类型和参数顺序,简称“一同三不同”。
  3. test(byte b)方法在继承类中没有创建时,基本类对象调用时会调用基类的test(byte b)方法,但当派生类中创建之后就不法调用基类中的test(byte b)方法了。是由于派生类对该方法进行类重写。
  4. 注意,在派生类未创建test(byte b)方法方法时,其实派生类当中已经默认存在继承自基类中的test(byte b)方法,此时该方法就与派生类当中的其他同名方法之间形成了方法重载。

2.2、方法的重写(override)

方法重写override)指在继承关系中,派生类重写基类的方法,以达到同一个方法在不同的派生类中有不同的实现。

如果基类中的方法实现不适合派生类,派生类便可以重新定义。派生类中定义的方法与基类中的方法具有相同返回值方法名称参数列表,但具有不同方法体,称之为派生类重写了基类的方法。

仅仅在派生类中重写了基类的方法,仍然不足以体现出多态性,还需要使用面向对象程序设计中的一条基本原则,即 里氏替换原则

里氏替换原则表述为,任何基类可以出现的地方,派生类一定可以出现。直白的说就是基类类型的变量可以引用派生类的对象(即基类类型的变量代表的内存中存储的是一个派生类的对象在内存中的地址编号)。此时,通过基类类型的变量调用基类中的方法,真正的方法执行者是派生类的对象,被执行的方法如果在派生类中被重写过,实际执行的便是派生类中的方法体。

通过上述方式,相同基类类型的变量调用相同方法,根据调用方法的具体派生类对象的不同,便可以执行不同的方法实现。由于在程序运行阶段,变量引用的内存地址才能最终确定,故这种形式的多态体现了运行时多态

小结:

  • 1、构成重写的条件?
    两同一不同两小一大
    两同:同名同参(同名:方法名相同;同参:参数类型,顺序,个数相同)
    一不同:继承链上不同的类
    两小: 返回的数据类型,可以是不同的,而且是可以变小的;子类重写的方法返回类型是可以是父类返回类型的派生类。
    异常会变小*(没讲)
    一大:权限修饰符可以不一致,可以变大的,注意:重写是基于继承的,没有继承,就没有重写。

  • 2、@override注解 它的作用就是指明这个方法,必须是重写超类方法。

    注解是什么?

    • 注解就是一个标签、标记
    • 注解是一种特殊的接口,可以写在类,方法,构造方法,成员变量,形参
    • 其实就是一种的标记,位以上元素提供特殊附加功能。
    • jdk5以后才有。
  • 3、instanceof运算符

    • instanceof就是判断对象的类型,如果是该类型,返回true,不是返回false.
    • 语法格式:对象 instanceof 类型
    • obj instanceof Object
    • 严格来说,对象进行强制类型转换之前,都应该instanceof一下。
    • instanceof判断支持本类型和父类类型。

重写------示例1:

Animal类:

package cn.edu.bennett.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());
	}
}

测试类AnimalTest类:

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用来说明其后的方法是一个重写的方法。

注意,重写的方法不能缩小基类中被重写方法的访问权限。

实现运行时多态的三个必要条件:

  • 继承
  • 方法重写
  • 基类变量引用派生类对象

重写------示例2:

Dog类:

package cn.edu.bennett.test;

public class Dog {
	public void cry() {
		System.out.println("狗叫:汪汪!!!");
	}
}

YellowDog类:

package cn.edu.bennett.test;

public class YellowDog extends Dog {
//	重写父类的cry()方法
	@Override
	public void cry() {
		System.out.println("我是只黄狗,汪汪!!");
	}
}

BlackDog

package cn.edu.bennett.test;

public class BlackDog extends Dog {
//	重写父类的cry()方法
	@Override
	public void cry() {
		System.out.println("我是只黑狗,汪汪!!!");
	}
}

测试类DogTest类:

package cn.edu.bennett.test;

public class DogTest {
	public static void main(String[] args) {
//		1、定义父类Dog
		Dog dog = new Dog();
//		2、定义子类黄狗Dog1
		YellowDog dog1 = new YellowDog();
//		3、定义子类黑狗Dog2
		BlackDog dog2 = new BlackDog();
		
//		调用方法
		dog.cry();
		dog1.cry();
		dog2.cry();
	}
}

运行结果:

狗叫:汪汪!!!
我是只黄狗,汪汪!!
我是只黑狗,汪汪!!!

解释:
父类Dog类中有非静态方法cry(),由YellowDogBlackDog继承,并且重写类cry()方法。

OverideParent类:

package cn.edu.bennett.test;

public class OverideParent {
	private String name;//父类属性:姓名
	// 无参构造
	public OverideParent() {
		super();
	}
	// 含参构造
	public OverideParent(String name) {
		super();
		this.name = name;
	}
	// get/set方法
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	// 非静态方法
	// 返回类型Dog的test方法(前面定义的Dog类)
	Dog test() {
		System.out.println("这是父类中的test()方法");
		return null;
	}
	// 静态方法
	public static void show() {
		System.out.println("父类中的静态show()方法");
	}
}

OverideSub类:

package cn.edu.bennett.test;

public class OverideSub extends OverideParent{
	private String interest;//子类属性:兴趣
    // 无参构造
	public OverideSub() {
		super();
	}
	// 含参构造
	public OverideSub(String interest) {
		super();
		this.interest = interest;
	}
	// get/set方法
	public String getInterest() {
		return interest;
	}
	public void setInterest(String interest) {
		this.interest = interest;
	}
	// 返回类型BlackDog的test方法(前面定义的BlackDog类是	Dpg的子类)
	@Override
	public BlackDog test() {
		System.out.println("这是子类中的test()方法。");
		return null;
	}
//	静态方法
	public static void show() {
		System.out.println("子类中的静态show()方法");
	}
}

OverideTest类:

package cn.edu.bennett.test;

public class OverideTest {
	public static void main(String[] args) {
//		1、父类引用指向父类对象
		OverideParent parent = new OverideParent();
		parent.test();
		
//		2、父类引用指向子类对象      小范围-->大范围,自动类型转换
		OverideParent subParent = new OverideSub();//父类引用可以直接直向子类的对象,小范围-->大范围,自动类型转换
		/*
		 * 里氏代换原则:可以,使用父类对象的地方,一定能被子类对象替换。
		 * 
		 * 思考:subParent调用test()方法到底是OverideParent类的还是OverideSub类的??
		 * 解析:在编译期间调用OverideParent类的test()方法,在运行阶段调用OverideSub类的test()方法。
		 * 
		 * */
		subParent.test();
		
//		3、子类引用指向子类对象
		OverideSub sub = new OverideSub();
		/*
		 * 子类对象调用test()方法时,如果子类没有重写test()方法,则会调用父类的test()方法
		 * 子类对象调用test()方法时,如果子类重写了test()方法,则会调用子类的test()方法
		 * */
		sub.test();
		
//		4、子类引用指向父类对象        大范围-->小范围,强制类型转换
//		OverideSub sub1 = (OverideSub) new OverideParent();//子类引用无法直接直向父类,需要将父类对象强制转换为子类对象,
		
		/*
		 * if (sub1 instanceof OverideSub) { System.out.println("可以"); }else {
		 * System.out.println("不可以"); }
		 */
		
		/*
		 * 注意:此时虽然没有了编译错误,但是会抛出异常
		 * exception in thread "main" java.lang.ClassCastException: cn.edu.xxx.test0909.OverideParent 
		 * cannot be cast to cn.edu.xxx.test0909.OverideSubat cn.edu.xxx.test0909.OverideTest.main(OverideTest.java:9)
		 * */
		
//		5、属性的调用            
		OverideParent parent2 = new OverideParent("张三");
		System.out.println("父类属性名称:"+parent2.getName());
		
		OverideSub sub2 = new OverideSub("打篮球");
		sub2.setName("李四");//子类设置继承父类的属性姓名
		System.out.println("子类属性兴趣:"+sub2.getInterest());
		System.out.println("子类继承父类属性姓名:"+sub2.getName());
//		parent2.getInterest();//编译错误    父类不能调用子类特有的属性,但是子类可以调用父类的属性
		
//		6、调用静态方法(父子类中都有的静态方法show())
//		父类引用调用静态的方法进行测试时,静态的成员推荐使用“类名.成员名称”
		parent2.show();//警告
		OverideParent.show();
		subParent.show();//父类引用指向的子类对象调用静态方法是会执行父类中的show()方法
		sub.show();//子类对象调用show()方法
	}
}

解释:

  • 多态的效果
    1、指向子类对象的父类对象,在编译期间只能调用父类的方法,不能直接调用子类的方法
    2、父子类都有的非静态方法来说,最终调用子类的重写版本。
    3、父子类中都有的静态方法来说,最终调用父类中的版本,与指向的对象类型无关。

2.3 重载和重写的异同

  • 相同

    • 两者都是由多态的作用
    • 都可以让同名方法,由不同的实现
    • 并且让java自动根据情况(重载是根据参数、重写是根据调用对象)调用
    • 让程序员更多的关注业务逻辑,而不是实现细节
  • 区别

    • 确定方法的方式
      • 重载根据参数
      • 重写根据调用对象的真实数据类型
    • 范围不同
      • 重载是同类的同名方法
      • 重写是不同类
    • 实现不同
      • 重载 两同三不同
      • 重写 两同一不同两小一大
  • 扩展

    • 实现原理
      • 重载是编译时实现,在编译过程同名方法,由编成不同名方法。(编译时多态) 静态多态
      • 重写,是在运行过程,动态判断对象数据类型,从而确定调用那个方法。(运行时多态)动态多态

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修饰的变量(包括成员变量和局部变量),变量所代表的内存中存储的数据只能初始化一次,不能被二次赋值。

附加内容

提及final关键字都会想到一个常见的面试题就是:
final、finally、finalize区别
在这里我们来简单回忆一下这三者的区别,

final: Final用于修饰类、成员变量和成员方法。final修饰的类,不能被继承(String、StringBuilder、StringBuffer、Math,不可变类),其中所有的方法都不能被重写(这里需要注意的是不能被重写,但是可以被重载,这里很多人会弄混),所以不能同时用abstract和final修饰类(abstract修饰的类是抽象类,抽象类是用于被子类继承的,和final起相反的作用);Final修饰的方法不能被重写,但是子类可以用父类中final修饰的方法;Final修饰的成员变量是不可变的,如果成员变量是基本数据类型,初始化之后成员变量的值不能被改变,如果成员变量是引用类型,那么它只能指向初始化时指向的那个对象,不能再指向别的对象,但是对象当中的内容是允许改变的。
我们来深入理解一下被final修饰的类、方法、变量

1、final修饰的类

final修饰的类无法继承。

public final class FinalTest {
}
class Test extends FinalTest{	 
}

FinalTest下标处红色下划线,提示编译错误。

public /*final*/ class FinalTest {
}
class Test extends FinalTest{	 
}

红色下划线消失,编译正确。

解释:
那这里就有一个问题了,为什么设计了继承还要有final来破坏这种继承关系呢。
这个解释在《Java编程思想》说的比较清楚:
使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的 Java 版本中,不需要使用 final 方法进行这些优化了。“

2、被final修饰的变量

我们来看一个有意思的代码

public class Main {
   public static void main(String[] args) {
       String a = "xiaomeng2";
       final String b = "xiaomeng";
       String d = "xiaomeng";
       String c = b + 2;
       String e = d + 2;
       System.out.println((a == c));
       System.out.println((a == e));
   }
}
这段代码的输出结果是什么呢?
答案是: truefalse

原因:

变量a指的是字符串常量池中的 xiaomeng2;
变量 b 是 final 修饰的,变量 b 的值在编译时候就已经确定了它的确定值,换句话说就是提前知道了变量 b 的内容到底是个啥,相当于一个编译期常量;
变量 c 是 b + 2得到的,由于 b 是一个常量,所以在使用 b 的时候直接相当于使用 b 的原始值(xiaomeng)来进行计算,所以 c 生成的也是一个常量,a 是常量,c 也是常量,都是 xiaomeng2 而 Java 中常量池中只生成唯一的一个 xiaomeng2 字符串,所以 a 和 c 是相等的!
d 是指向常量池中 xiaomeng,但由于 d 不是 final 修饰,也就是说在使用 d 的时候不会提前知道 d 的值是什么,所以在计算 e 的时候就不一样了,e的话由于使用的是 d 的引用计算,变量d的访问却需要在运行时通过链接来进行,所以这种计算会在堆上生成 xiaomeng2 ,所以最终 e 指向的是堆上的 xiaomeng2 , 所以 a 和 e 不相等。
总得来说就是:a、c是常量池的xiaomeng2,e是堆上的xiaomeng2

然后我们来看一下被final修饰的引用变量和基本变量有什么不同:

public class FinalTest {
	public static void main(String[] args) {
		final int num = 3;
		num = 4;//num出现编译错误
	}
}

final修饰的普通变量是不可变的。

对于引用变量被final修饰了:引用变量引用不可变,但是引用对象的内容可以改变。
Student类

package com.bennett.test0915;

public class Stuent {
	private String name;
	private int age;
	
	public Stuent() {
		super();
	}
	public Stuent(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}
package com.bennett.test0915;

public class FinalTest {
	public static void main(String[] args) {
		final Stuent student = new Stuent();
		student = new Stuent();//studenth出现编译错误
	}
}

可以看到使用final修饰了的类不能再指向别处
如何理解内容可变呢:

package com.bennett.test0915;

public class FinalTest {
	public static void main(String[] args) {
		final Stuent student = new Stuent("张欣",20);
		System.out.println(student.getName());
		System.out.println(student.getAge());
		
		student.setName("玛丽");
		student.setAge(22);
		
		System.out.println(student.getName());
		System.out.println(student.getAge());		
	}
}

参考文档
总结:
final关键字的应用

修饰
修饰
修饰
final关键字
方法
变量

final关键字总结:

  • final关键字修饰类——类无法被继承
  • final关键字修饰方法——方法无法被重写
  • final关键字修饰变量
    • 基本数据类型——常量或不可变的量(无法修改)
    • 引用数据类型——引用数据类型对象不能再次创建。
      • 对象的属性可以修改

五、static关键字

static也是Java中一个非常重要的关键字。static的字面意思是静止的,静态的。在Java中,static关键字可以用来声明类的静态成员声明静态导入等。

注意
static 的使用对象:类、变量、方法、初始化函数(注意:修饰类时只能修饰 内部类

1、静态成员

之前不断提到类的成员(成员变量和成员方法),在Java中,类的成员也可以分为两种,分别是实例成员类成员

实例(对象)成员是属于对象的,实例成员包括实例成员变量实例成员方法。只有创建了对象之后,才能通过对象访问实例成员变量、调用实例成员方法。

类成员是属于的,类成员在声明时需要使用static修饰符修饰。类成员包括类成员变量类成员方法。通过类名可以直接访问类成员变量、调用类成员方法,也可以通过对象名访问类成员变量、调用类成员方法。

没有被static修饰符修饰的成员变量为实例成员变量(实例变量),没有被static修饰符修饰的成员方法为实例成员方法(实例方法)
static修饰符修饰的成员变量为类成员变量(静态成员变量、类变量、静态变量),被static修饰符修饰的成员方法为类成员方法(静态成员方法、类方法、静态方法)

实例成员变量
类成员变量
实例成员方法
类成员方法
成员
成员变量
实例变量
静态成员变量 类变量 静态变量
成员方法
实例方法
静态成员方法 类方法 静态方法

实例成员类成员核心的区别在于内存分配机制的不同,实例成员变量随着对象的创建在堆中分配内存,每个对象都有独立的内存空间存储各自的实例成员变量;
类成员变量在程序运行期间,首次使用类名时在方法区中分配内存并且只分配一次,无论使用类名还是对象访问类成员变量时,访问的都是方法区中同一块内存。

实例成员方法必须由堆中的对象调用;
类成员方法可以直接使用类名调用.

注意:

  1. 由于实例成员和类成员内存分配机制的不同,显而易见的现象是,在类体中,可以在一个实例成员方法中调用类成员方法,反之则不行;可以将一个类成员变量赋值给一个实例成员变量,反之则不行。
  2. 类成员方法不能(事实上也无需)被重写,无法表现出多态性。

示例1:
People类:

package cn.edu.bennett.test;

public class People {
	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 People(String name, int age, int sex) {
		super();
		this.name = name;
		this.age = age;
		this.sex = sex;
	}
}

Test类:

package cn.edu.bennett.test;

public class Test {
	public static void main(String[] args) {
		People people1 = new People("张三", 18, 1);
		People people2 = new People("李四", 19, 1);
		
		//类调用静态(类)成员变量
		System.out.println(People.eyeColor); 
		System.out.println(People.skinColor);
		//对象调用静态(类)成员变量
		System.out.println(people1.eyeColor);
		System.out.println(people1.skinColor);
		System.out.println(people2.eyeColor);
		System.out.println(people2.skinColor);
	}
}

执行输出结果:

黑色
黄色
黑色
黄色
黑色
黄色

解释

People类中的成员变量eyeColorskinColor是静态的,无论使用People类名还是People类的对象people1people2,访问这两个类成员变量时,都访问的是方法区中的同一块内存地址。图示如下:
在这里插入图片描述

类成员也体现了面向对象的思想,它可以描述某一类中具有共性的,可以不依赖于对象而存在的成员。比如本例中,就算没有任何一个人的对象存在,人的眼睛颜色也应该是黑色的,皮肤颜色也应该是黄色的。

示例2:
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 {
	// 代码
}

示例3:
People类:

package cn.edu.bennett.test;
/**
 * 中国人类
 */
public class people {
	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;
	}
}

Test类:

package cn.edu.bennett.test;

public class Test {
	public static void main(String[] args) {
		People people1 = new People("张三", 18, 1);
		People people2 = new People("李四", 19, 1);
		
		//类调用静态(类)成员变量
		System.out.println(People.eyeColor); 
		System.out.println(People.skinColor);
		//对象调用静态(类)成员变量
		System.out.println(people1.eyeColor);
		System.out.println(people1.skinColor);
		System.out.println(people2.eyeColor);
		System.out.println(people2.skinColor);
	}
}

执行输出结果:

黑色
黄色
黑色
黄色
黑色
黄色

3、静态导入

在一个类中使用其他类的静态方法静态变量时,可以使用static关键字静态导入其他类的静态成员,该类中就可以直接使用其他类的静态成员。

静态导入的语法格式:

import static 类完全限定名.静态成员名

示例4:

package cn.edu.bennett.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方法中可以直接使用。

综合实例

package cn.edu.bennett.test;

/**
 * @version:1.0
 * @Description:static关键字的练习
 * @author gxd
 * @date: 2021年9月7日 下午3:49:05
 */ 
public class StaticTest {
//	1、成员变量
	private String str1 = "实例(对象)成员变量";
	private static String str2 = "(类)成员变量";

//	2、构造方法	不能用static修饰
	StaticTest() {// 不行
	}

//	3、代码块
	{
		System.out.println("普通代码块");
	}
	static {
		System.out.println("静态代码块");
	}

//	4、内部类
	private static class InnerClass {

	}
	private class InnerClass2 {

	}
//	5、注解
//	static @Override //不行
//	public String toString() {
//		// TODO Auto-generated method stub
//		return super.toString();
//	}
	
// 6、方法
//  static的类外访问方式
	private static void test1() {
//		static int i=2;//静态变量(java不支持)
//		System.out.println(str1);//另外的一个类,不能直接访问实例变量
		StaticTest test = new StaticTest();
		System.out.println(test.str1);// 实例变量在其它类必须通过对象访问
//		System.out.println(StaticTest.str1);//实例变量在其它类必须通过对象访问,不能通过类名
		
		System.out.println(StaticTest.str2);// 在其它类访问类变量 可以直接使用类名.类变量访问
		System.out.println(test.str2);// 在其它类访问类变量 可以直接使用对象.类变量访问
		
		StaticTest.InnerClass innerClass = new StaticTest.InnerClass();//静态内部类可以直接用类名.内部类名 来创建内部类对象。
		StaticTest.InnerClass2 innerClass2 = test.new InnerClass2();// 普通内部类只能通过类对象.内部类名 来创建内部类对象
	}
//	非static的本类访问方式
	private void test2() {// 实例方法
//		System.out.println(str1);//实例方法,可以直接访问实例变量
		System.out.println(str2);// 实例方法,也可以直接访问类变量
//		test3();
	}

//	static的本类访问方式
	private static void test3() {// 类方法
//		System.out.println(str1);//类方法,可不能直接访问实例变量
		System.out.println(StaticTest.str2);// 方法,也可以直接访问类变量
		System.out.println(str2);// 方法,也可以不加类名前缀直接访问类变量
	}

//  成员变量的共享性
	private static void test4() {
		StaticTest test1 = new StaticTest();
		test1.str1 = "看看值变不变???";
//		System.out.println(test1.str1);
		StaticTest test2 = new StaticTest();
//		System.out.println(test2.str1);
//		StaticTest.str2="我改变整个世界";
		test1.str2 = "这样行不行???";
		
		System.out.println(test1.str2);
		System.out.println(test2.str2);
		System.out.println(StaticTest.str2);

	}

	public static void main(String[] args) {
//		test1();
		test4();
	}
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值