CH04 面向对象(上)

版权声明:@Abby-YTJ https://blog.csdn.net/weixin_43564773/article/details/85736961

4.1 面向对象

Java语言是纯粹的面向对象的语言。

4.1.1 结构化程序简介

  • 结构化程序设计主张按功能分析系统需求。主要原则可概括为自顶向下、逐步求精、模块化等。
  • 结构化设计、结构化分析、结构化编程方法来实现系统。
  • 也被称为面向功能的程序设计方法
  • 结构化程序设计方式的局限性:
    • 设计不够直观,与人类思维方式不一致。
    • 适应性差,可扩展性不强。

4.1.2 面向对象程序设计简介

  • 面向对象程序设计方法的基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计。
  • 采用面向对象方式开发的软甲系统,其最小的程序单元是类,这些类可以生成系统中的多个对象,而这些对象则直接映像成客观世界的各种事物。
    • 成员变量(状态数据)+ 方法(行为)= 类定义

4.1.3 面向对象的基本特征

  • 面向对象方法具有三个基本特征:封装Encapsulation、继承Inheritance、多态polymorphism
    • 封装:将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能;
    • 继承:是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法
    • 多态:是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,意味着同一个类型的对象在执行同一个方法时,可以表现出多种行为特征。
  • 面向对象编程的几个概念
    • 对象是面向对象方法中最基本的概念,基本特点有:标识唯一性、分类性、多态性、封装性、模块独立性好。
    • 类是具有共同属性、共同方法的一类事物。类是对象的抽象;对象则是类的实例。而类是整个软件系统中最小的程序单元。
    • 对象间的相互合作需要一个机制协助进行,这样的机制称为“消息”。消息是一个实例与另一个实例之间相互通信的机制。

4.2 类和对象

4.2.1 定义类

  • A class is the template or blueprint from which objects are made.

Java语言里定义类的语法如下

[修饰符] class ClassName
{
    field1
    field2
    ...
    constructor1
    constructor2
    ...
    method1
    method2
    ...
}
  • 修饰符可以是public、final、abstract或省略
  • 一个类可以包含三种常见成员:成员变量、构造器、方法,三种成员都可以定义0个或多个。
    • 成员变量用于定义该类或该类的实例所包含的状态数据
    • 构造器用于构造该类的实例,如果程序员没有为一个类编写构造器,则系统会为该类提供一个默认的构造器。
    • 方法用于定义该类或该类的实例的行为特征或者功能实现。
  • 类的各成员之间的定义顺序没有影响,各成员之间可以相互调用,static修饰的成员不能访问没有static修饰的成员。

定义成员变量的语法如下

[修饰符] 类型 成员变量名 [=默认值];

  • 修饰符:可以省略,或者是public,protected,private,static,final,其中public、protected、private三个最多只能出现其中之一,可以和static、final组合起来修饰成员变量。
  • 类型:可以是Java语言的任何数据类型

定义方法的语法如下

[修饰符] 方法返回值类型 方法名(形参列表)
{
    // 由零条到多条可执行性语句组成的方法体
}
  • 修饰符:可以省略,也可以是public、protect、private、static、final、abstract,其中public、protected、private三个最多只能出现其中之一;abstract和final最多只能出现其中之一,他们可以和static组合起来修饰方法。
  • 方法体内多条可执行语句之间有严格的执行先后顺序。
  • static修饰的成员表明它属于类本身,而不属于该类的单个实例,因此static修饰的成员称为类变量和类方法,不使用static修饰的成员称为实例变量、实例方法。
    • static的真正作用是用于区分成员变量、方法、内部类、初始化块这四种成员到底属于类本身还是属于实例。

定义构造器的语法如下:

  • 构造器是一个特殊的方法
[修饰符] 构造器名(形参列表)
{
    // 由零条到多条可执行性语句组成的构造器执行体
}
  • 修饰符:可以省略,也可以是public、protected、private其中之一。
  • 构造器名:构造器名必须和类名相同
  • 构造器既不能定义返回值类型,也不能使用void声明构造器没有返回值。
    例:Employee.java
import java.time.LocalDate;
public class Employee {
	private String name;
	private double salary;
	private LocalDate hireDay;
	public Employee(String name, double salary, int year, int month, int day) {
		this.name = name;
		this.salary = salary;
		hireDay = LocalDate.of(year, month, day);
	}
	public String getName()
	{
		return name;
	}
	
	public double getSalary()
	{
		return salary;
	}
	
	public LocalDate getHireDay()
	{
		return hireDay;
	}
	public void raiseSalary(double byPercent)
	{
		double raise  = salary * byPercent / 100;
		salary += raise;
	}
}

4.2.2 对象的创建和使用

  • 创建一个对象的步骤
    • 1.分配对象空间并将对象成员变量初始化
    • 2.执行属性值的显式初始化
    • 3.执行构造方法
    • 4.返回对象的地址给相关的变量
Employee stuff = new Employee();
  • 如果访问权限允许,类里定义的方法和成员变量都可以通过类或实例来调用。类或实例访问成员变量的语法是:类.类变量|方法,或者实例.实例变量|方法。
  • static修饰的方法和成员变量,既可以通过类来调用,也可以通过实例来调用;没有使用static修饰的普通方法和成员变量,只可以通过实例来调用。

4.2.3 对象、引用和堆栈

  • 在前面创建Employee类的一个实例stuff时,实际产生了两个东西:一个是stuff变量,一个是Employee对象。
  • Employee类型的变量实际上是一个引用,存放在栈内存里,指向实际的Employee对象;而真正的Employee对象则存放在堆(heap)内存中。
  • 当一个对象被创建成功以后,这个对象将保存在堆内存中,Java程序不允许直接访问堆内存中的对象,只能通过该对象的引用操作该对象。也就是说,不管是数组还是对象,都只能通过引用来访问它们。
  • Java虚拟机的内存分为三个区域:栈、堆和方法区,方法区本质也是堆。
  • 栈的特点:
    • 栈描述的是方法执行的内存模型,每个方法被调用都会创建一个栈帧存储局部变量、操作数、方法出口等。
    • JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实参、局部变量等)。
    • 栈属于线程私有,不能实现线程间的共享。
    • 栈的存储特点是:先进后出,后进先出
    • 栈是由系统自动分配、速度快,栈是一个连续的内存空间。
  • 堆的特点:
    • 堆用于存储创建好的对象和数组(数组也是对象)
    • JVM只要一个堆,被所有线程共享。
    • 堆是一个不连续的内存空间,分配灵活,速度慢
  • 方法区(又叫静态区)的特点:
    • JVM只有一个方法区,被所有线程共享
    • 方法区实际也是堆,只用于存储类、常量相关的信息
    • 用来存放程序中永远是不变或唯一的内容(类信息、静态变量、字符串常量等)

4.2.4 对象的this引用

  • Java中的this关键字总是指向调用该方法的对象。根据this出现的位置不同,this作为对象的默认引用有两种情形:
    • 构造器中引用该构造器正在初始化的对象
    • 在方法中引用调用该方法的对象,让类中的一个方法,访问该类里的另一个方法或实例变量。
      例:Dog.java
public class Dog
{
	// 定义一个jump()方法
	public void jump()
	{
		System.out.println("正在执行jump方法");
	}
	// 定义一个run()方法,run()方法需要借助jump()方法
	public void run()
	{
		this.jump();
		System.out.println("正在执行run方法");
	}
}
  • 注意static修饰的方法中不能使用this引用
  • 在编程时尽量不要使用对象去调用static修饰的成员变量、方法(并不是不允许),而是应用使用类去调用static修饰的成员变量、方法。

4.3 方法详解

  • 方法是类或对象的行为特征的抽象。方法在逻辑上要么属于类,要么属于对象。

4.3.1 方法的所属性

  • Java语言里方法的所属性主要体现在如下几个方面:
    • 方法不能独立定义,方法只能在类体里定义
    • 从逻辑意义上看,方法要么属于该类本身,要么属于该类的一个对象。
    • 永远不能独立执行方法,执行方法必须使用类或对象作为调用者
  • 类方法static methods的用途:
    • When a method doesn’t need to access the object state because all needed parameters are supplied as explicit parameters.
    • When a method only needs to access static fields of the class.
  • Main方法是一个static method. The main method doesn’t operate on any objects.

4.3.2 方法的参数传递机制

  • 如果声明方式时调用了形参,则调用方法时必须给这些形参指定参数值。调用方法时实际传给形参的参数值也称为实参。
  • Java里方法的参数传递机制是值传递。所谓值传递,即将实际参数值的副本(复制品)传入方法内,而参数本身不会受到影响。
  • 基本类型的参数传递:例:PrimitiveTransferTest.java
public class PrimitiveTransferTest {
	public static void swap(int a, int b)
	{
		// 下面三行代码实现a、b变量的值交换
		// 定义一个临时变量来保存a变量的值
		int tmp = a;
		// 把b的值赋给a
		a = b;
		// 把临时变量tmp的值赋给a
		b = tmp;
		System.out.println("swap方法里,a的值是"
				+ a + ": b的值是" + b);
	}
	public static void main(String[] args) {
		int a = 6;
		int b = 9;
		swap(a,b);
		System.out.println("交换结束后,变量a的值是"
				+ a + ": 变量b的值是" + b);
	}
}

运行结果:

swap方法里,a的值是9: b的值是6
交换结束后,变量a的值是6: 变量b的值是9
  • 引用类型的参数传递,例:ReferenceTranferTest.java
class DataWrap
{
	int a;
	int b;
}
public class ReferenceTransferTest {
	public static void swap(DataWrap dw)
	{
		// 下面三行代码实现dw的a、b两个成员变量的值交换
		// 定义一个临时变量来保存dw对象的a成员变量的值
		int tmp = dw.a;
		// 把dw对象的b成员变量的值赋给a成员变量
		dw.a = dw.b;
		// 把临时变量tmp的值赋给dw对象的b成员变量
		dw.b = tmp;
		System.out.println("swap方法里, a成员变量的值是"
				+ dw.a + "; b成员变量的值是" + dw.b);
	}
	public static void main(String[] args) {
		DataWrap dw = new DataWrap();
		dw.a = 6;
		dw.b = 9;
		swap(dw);
		System.out.println("交换结束后,a成员变量的值是"
				+ dw.a + "; b成员变量的值是" + dw.b);
	}
}

运行结果:

swap方法里, a成员变量的值是9; b成员变量的值是6
交换结束后,a成员变量的值是9; b成员变量的值是6
  • 程序从main()入口进入,定义一个dw引用变量指向DataWrap对象(此时,dw是在main栈区,而DataWrap对象在堆内存中)。main()方法调用swap()方法,将dw的副本作为实参传入swap(),此时系统为swap()方法建立swap栈区,swap()方法的dw形参也保存了DataWrap对象的地址。实际操作的是堆内存中的DataWrap对象,swap()方法中交换dw参数所引用DataWrap对象的a、b两个成员变量的值,dw变量引用的也是同样的DataWrap对象,自然其a、b成员变量是发生了变化的。
  • 注意main()方法中的dw和swap()方法中的dw是两个变量(只是值传递)。
  • Here is a summary of what you can and cannot do with method parameters in Java:
    • A method cannot modify a parameter of a primitive type(that is, numbers or boolean values).
    • A method can change the state of an object parameter.
    • A method cannot make an object parameter refer to a new object.

4.3.3 形参个数可变的方法

  • 在定义方法时,在最后一个形参的类型后增加三点(…),则表明该形参可以接受多个参数值,多个参数值被当成数组传入。
public class Varargs {
	// 定义了形参个数可变的方法
	public static void test(int a, String...books)
	{
		// books被当做数组处理
		for (String tmp:books)
		{
			System.out.println(tmp);
		}
		// 输出整数变量a的值
		System.out.println(a);
	}
	public static void main(String[] args) {
		// 调用test方法
		test(5,"Core Java", "Effective Java");
	}
}

运行结果

Core Java
Effective Java
5
  • 个数可变的形参只能处于形参列表的最后,一个方法中最多只能包含一个个数可变的形参。
  • 个数可变的形参本质就是一个数组类型的形参。

4.3.4 递归方法

  • 一个方法体内调用它自身,称为方法递归。
  • 例:已知f(0) = 1, f(1) = 4, f(n+2) = 2*f(n+1) + f(n),其中n是大于零的整数,求f(10)的值。
public class Recursive {
	public static int fn(int n)
	{
		if (n == 0)
		{
			return 1;
		}
		else if (n == 1)
		{
			return 4;
		}
		else 
		{
			return 2* fn(n-1) + fn(n-2);// 方法中调用自身
		}
	}
	public static void main(String[] args) {
		// 输出fn(10)的结果
		System.out.println(fn(10));
	}
}

4.3.5 方法重载

  • 如果同一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。
  • 在Java程序中确定一个方法需要三个要素:
    • 调用者,也就是方法的所属者,可以是类或者对象。
    • 方法名,方法的标识。
    • 形参列表,当调用方法时,系统会根据传入的实参列表匹配。
  • 例:Overload.java
public class Overload {
	// 下面定义了两个test()方法,但方法的形参列表不同
	// 系统可以区分这两种方法,这被称为方法重载
	public void test()
	{
		System.out.println("无参数");
	}
	public void test(String msg)
	{
		System.out.println("重载的test方法 " + msg);
	}
	public static void main(String[] args) {
		Overload o1 = new Overload();
		// 调用test()时没有传入参数,系统会调用上面没有参数的test()方法
		o1.test();
		// 调用test()时传入一个字符串参数
		o1.test("hello");
	}
}

4.4 成员变量和局部变量

  • Java中,根据定义变量的位置不同,可以将变量分为两大类:成员变量和局部变量。

4.4.1 成员变量和局部变量

  • 成员变量指的是在类里定义的变量,包括实例变量(没有static修饰符)和类变量(有static修饰符)
  • 局部变量指的是在方法里定义的变量,包括形参(方法签名中定义的变量),方法局部变量(方法内定义),代码块局部变量(在代码块中定义)

成员变量

  • 类变量:类变量从该类的准备阶段开始存在,直到系统完全销毁这个类,类变量的作用域与这个类的生存范围相同。只要类存在,程序就可以访问该类的类变量。
  • 实例变量:从该类的实例被创建起开始存在,直到系统完全销毁这个实例。实例变量的作用域与对应的生存范围完全相同。只要实例存在,程序就可以访问该类的类变量。
  • 同一个类里,不能定义两个同名的成员变量。
    - 例:PersonTest.java
class Person
{
	// 定义一个实例变量
	public String name;
	// 定义一个类变量
	public static int eyeNum; 
}
public class PersonTest {
	public static void main(String[] args) {
		// 第一次主动使用Person类,该类自动初始化,则eyeNum变量开始起作用,输出0
		System.out.println("Person的eyeNum类变量值:"
				+ Person.eyeNum);
		// 创建Person对象
		Person p = new Person();
		// 通过Person对象的引用p来访问Person对象name实例变量并通过实例访问eyeNum类变量
		System.out.println("p变量的name变量值是:" + p.name
				+ " p对象的eyeNum变量值是:" + p.eyeNum);
		// 直接为name实例变量赋值
		p.name = "孙悟空";
		// 通过p访问eyeNum类变量,依然是访问Person的eyeNum类变量
		p.eyeNum = 2;
		// 再次通过Person对象来访问name实例变量和eyeNum类变量
		System.out.println("p变量的name变量值是:" + p.name
				+ " p对象的eyeNum变量值是: " + p.eyeNum);
		// 前面通过p修改了Person的eyeNum,此处的Person.eyeNum将输出2
		System.out.println("Person的eyeNum类变量值:" + Person.eyeNum);
		Person p2 = new Person();
		// p2访问的eyeNum类变量依然引用的Person类的,因此依然输出2
		System.out.println("p2对象的eyeNum类变量值:" + p2.eyeNum);
		}
}

运行结果:

Person的eyeNum类变量值:0
p变量的name变量值是:null p对象的eyeNum变量值是:0
p变量的name变量值是:孙悟空 p对象的eyeNum变量值是: 2
Person的eyeNum类变量值:2
p2对象的eyeNum类变量值:2
  • 注意不建议使用实例.类变量这种方式来访问类变量(The static field should be accessed in a static way)

局部变量

  • 形参:形参的作用域在整个方法内有效。
  • 方法局部变量:在方法体内定义的局部变量,它的作用域是从定义该变量的地方生效,到该方法介绍时失效。
  • 代码块局部变量:在代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,到该代码块结束时失效。
  • 例:BlockTest.java
public class BlockTest {
	public static void main(String[] args) {
		{
			// 定义一个代码块局部变量a
			int a;
			// 下面代码将出现错误,因为a变量还未初始化
//			System.out.println("代码块局部变量a的值:" + a);
			// 为a赋初值
			a = 5;
			System.out.println("代码块局部变量a的值: " + a);
		}
		// 下面试图访问的a变量并不存在
//		System.out.print(a);
	}
}
  • 例:MethodLoclaVariable.java
public class MethodLocalVariable {
	public static void main(String[] args) {
		// 定义一个方法局部变量
		int a;
		// 下面代码将出现错误,因为a变量还未初始化
//		System.out.println("方法局部变量a的值: " + a);
		// 为a变量赋初值
		a = 5;
		System.out.println("方法局部变量a的值:" + a);
	}
}
  • 形参的作用域是在整个方法体内有效,而且形参也无须显式初始化,形参的初始化在调用该方法时由系统完成,形参的值由方法的调用者负责制定。
  • Java允许局部变量和成员变量同名,如果方法里的局部变量和成员变量同名,局部变量会覆盖成员变量,如果需要在这个方法里引用被覆盖的成员变量,则可使用this(对于实例变量)或类名(对于类变量)作为调用者来限定访问成员变量。
public class VariableOverrideTest {
	// 定义一个name实例变量
	private String name = "Abby";
	// 定义一个price类变量
	private static double price = 78.0;
	public static void main(String[] args)
	{
		// 方法里的局部变量,局部变量覆盖成员变量
		int price = 65;
		// 直接访问price变量,将输出price局部变量的值:65
		System.out.println(price);
		// 使用类名作为price变量的限定,将输出price类变量的值:78.0
		System.out.println(VariableOverrideTest.price);
		// 运行info方法
		new VariableOverrideTest().info();
	}
	private void info() {
		// 方法里的局部变量,局部变量覆盖成员变量
		String name = "八戒";
		// 直接访问name变量,将输出name局部变量的值:"孙悟空"
		System.out.println(name);
		// 使用this来作为name变量的限定,将输出name实例变量的值:“Abby"
		System.out.println(this.name);
	}
}

运行结果:

65
78.0
八戒
Abby

4.4.2 变量的使用规则

  • 如下几种情形考虑使用成员变量:
    • 需要定义的变量是用于描述某个类或某个对象的固有信息的,如人的身高、体重等
    • 在某个类中需要以一个变量来保存该类或者实例运行时的状态信息
    • 某个信息需要在某个类的多个方法之间进行共享

4.5 封装

4.5.1 理解封装

  • 封装指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
  • 对一个类或对象实现良好的封装,可以实现以下目的:
    • 隐藏类的实现细节
    • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问
    • 可进行数据检查,从而有利于保证对象信息的完整性。
    • 便于修改,提高代码的可维护性
  • 为了实现良好的封装,you need to supply three items:
    • A private data field;
    • A public field accessor method;
    • A public field mutator method
  • 因此,封装有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来,这需要Java的访问控制符来实现。

4.5.2 使用访问控制符

  • Java的访问控制级别:

    private -> default -> protected -> public
  • private(当前类访问权限)
  • default(包访问权限)
  • protected(子类访问权限)
  • public(公共访问权限)
    • 访问控制级别表
访问控制符 private default protected public
同一个类中
同一个包中
子类中
全局范围内
  • 对于外部类而言,只能有两种访问控制级别:public和默认,不能用private和protected修饰(没意义)
  • 例:定义一个Person类,Person.java
public class Person
{
    // 使用private修饰成员变量,将这些成员变量隐藏起来
    private String name;
    private int age;
    // 提供方法来操作name成员变量
    public void setName(String name)
    {
        // 执行合理性校验,要求用户名必须在2-6位之间
        if (name.length() > 6 || name.length() < 2)
        {
            System.out.println("您设置的人名不符合要求");
            return;
        }
        else
        {
            this.name = name;
        }
    }
    public String getName()
    {
        return this.name;
    }
    // 提供方法来操作age成员变量
    public void setAge(int age)
    {
        // 执行合理性校验,要求用户年龄必须在0-100之间
        if (age > 100 || age < 0)
        {
            System.out.println("您设置的年龄不合法");
            return;
        }
        else
        {
            this.age = age;
        }
        public int getAge()
        {
            return this.age;
        }
    }
}

例:创建Person对象,并尝试操作和访问该对象的age和name两个实例变量,PersonTest.java

public class PersonTest
{
    public static void main(String[] args)
    {
        Person p = new Person();
        // 因为age成员变量被隐藏,所以下面语句将出现编译错误
        // p.age = 1000;
        // 下面语句不会出现编译错误,但运行时会提示”您设置的年龄不合法“
        p.setAge(1000);
        // 访问p的age成员变量也必须通过对于的getter方法
        // 因为上面未成功设置p的age成员变量,故此处输出0
        System.out.println("未能设置age成员变量时:"
            + p.getAge());
        // 成功修改p的age成员变量
        p.setAge(30);
        System.out.println("成功设置age成员变量后:"
            + p.getAge());
        // 不能直接操作p的name成员变量,只能通过其对应的setter方法
        p.setName("Abby");
        System.out.println("成功设置name成员变量后:"
            p.getName());
    }
}
  • 一个类就是一个小的模块,进行程序设计时应该尽量避免一个模块直接操作和访问另一个模块的数据,模块设计追求高内聚、低耦合。

4.6 深入构造器

  • 构造器是一个特殊的方法,用于创建实例时执行初始化。
  • 构造器是创建对象的重要途径,因此,Java类必须包含一个或一个以上的构造器。

4.6.1 使用构造器执行初始化

  • 当创建一个对象时,系统会为这个对象的实例变量进行默认初始化,如果想改变这种默认的初始化,想让系统创建对象时就为对象的实例变量显式指定初始值,就可以通过构造器来实现。
    • A constructor has the same name as the class.
    • A class can have more than one constructor.
    • A constructor can take zero, one, or more parameters
    • A constructor has no return value.
    • A constructor is always called with the new operator.
  • 例:自定义的构造器 ConstructorTest.java
public class ConstructorTest 
{
	public String name;
	public int count;
	// 提供自定义的构造器,该构造器包含两个参数
	public ConstructorTest(String name, int count) 
	{
		this.name = name;
		this.count = count;
	}
	public static void main(String[] args) {
		ConstructorTest tc = new ConstructorTest("Crazy Java", 99000);
		System.out.println(tc.name);
		System.out.println(tc.count);
	}
}

4.6.2 构造器重载

  • 同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。
public class ConstructorOverload {
	public String name;
	public int count;
	// 提供无参数的构造器
	public ConstructorOverload() {}
	// 提供带两个参数的构造器
	public ConstructorOverload(String name, int count) {
		this.name = name;
		this.count = count;
	}
	public static void main(String[] args) 
	{
		// 通过无参数构造器创建ConstructorOverload对象
		ConstructorOverload oc1 = new ConstructorOverload();
		ConstructorOverload oc2 = new ConstructorOverload("Crazy Java",222222);
		System.out.println(oc1.name + " " + oc1.count);
		System.out.println(oc2.name + " " + oc2.count);
	}
}

4.7 初始化块

  • There are three ways to initialize a data field:
    • By setting a value in a constructor
    • By assigning a value in the declaration
    • By using an initialization block

4.7.1 使用初始化块

  • 初始化块是Java类里可出现的第4种成员(成员变量、方法和构造器),一个类里可以有多个初始化块,相同类型的初始化块之间有顺序。
  • 初始化块的语法格式如下:
[修饰符]
{
    // 初始化块的可执行性代码
    ...
}
  • 例:Person.java
public class Person {
	// 下面定义一个初始化块
	{
		int a = 6;
		if (a > 4)
		{
			System.out.println("Person初始化块:局部变量a的值大于4");
		}
		System.out.println("Person的初始化块");
	}
	// 定义第二个初始化块
	{
		System.out.println("Person的第二个初始化块");
	}
	// 定义无参数的构造器
	public Person()
	{
		System.out.println("Person类的无参数构造器");
	}
	public static void main(String[] args) {
		new Person();
	}
}

运行结果:

Person初始化块:局部变量a的值大于4
Person的初始化块
Person的第二个初始化块
Person类的无参数构造器

4.7.2 静态初始化块

  • 普通初始化块负责对对象执行初始化,类初始化块则负责对类进行初始化
  • 静态初始化块是类相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。因此静态初始化块比普通初始化块先执行。
  • 静态初始化块不能对实例变量进行初始化处理。
import java.util.Random;
class Employee
{
	private static int nextId;
	private int id;
	private String name = ""; // instance field initialization
	private double salary;
	// static initialization block
	static
	{
		Random generator = new Random();
		// set nextId to a random number between 0 and 9999
		nextId = generator.nextInt(10000);
	}
	// object initialization block
	{
		id = nextId;
		nextId++;
	}
	// three overload constructors
	public Employee(String n, double s)
	{
		name = n;
		salary = s;
	}
	public Employee(double s)
	{
		// calls the Employee(String, double) constructor
		this("Enployee #" + nextId, s);
	}
	// the default constructor
	public Employee() {}
	public String getName()
	{
		return name;
	}
	public double getSalary()
	{
		return salary;
	}
	public int getId()
	{
		return id;
	}
}
public class ConstructorTest {
	public static void main(String[] args) {
		// fill the staff array with three Employee objects
		Employee[] staff = new Employee[3];
		staff[0] = new Employee("Harry", 40000);
		staff[1] = new Employee(60000);
		staff[2] = new Employee();
		
		// print out information about all Employee objects
		for (Employee e:staff)
			System.out.println("name=" + e.getName() + ",id="
					+ e.getId() + ",salary=" + e.getSalary());
	}
}

4.8 类的继承

4.8.1 继承的特点

  • 继承是实现代码复用的重要手段。Java的继承具有单继承的特点,每个子类只有一个直接的父类。
  • Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类被称为父类(基类、超类)。父类和子类是一般和特殊的关系。
  • Java里子类继承父类的语法格式如下:
[修饰符] class SubClass extends SuperClass
{
    // 类定义部分
}
  • 子类是对父类的扩展,可以获得父类的全部成员变量和方法,Java中的子类不能获得父类的构造器。
  • 例:定义一个父类,Fruit.java
public class Fruit
{
    public double weight;
    public void info()
    {
        System.out.println("我是一个水果,重" + weight + "g!");
    }
}
  • 定义该Fruit类的子类,Apple.java
public class Apple extends Fruit
{
    public static void main(String[] args)
    {
        // 创建Apple对象
        Apple a = new Apple();
        // Apple对象本身没有weight成员变量,Apple的父类有weight成员变量
        a.weight = 56;
        // 调用Apple对象的info()方法
        a.info();
    }
}

4.8.2 重写父类的方法

  • 子类包含于父类同名方法的现象称为方法重写(override),也成为方法覆盖。
  • 方法的重写要遵循“两同两小一大”规则,“两同”即方法名相同、形参列表相同,“两小“指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;”一大“指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
  • 当子类覆盖了父类的方法后,子类的对象无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。如果需要在子类方法中调用父类中被覆盖的方法,则可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。
  • 如果父类方法具有private访问权限,则该方法对其子类是隐藏的,其子类无法访问该方法,也就是无法重写该方法。
  • 如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写知识在子类中重新定义了一个新方法。

4.8.3 super限定

  • 关键字super用于限定该对象调用它从父类继承得到的实例变量或方法。
  • super也不能出现在static修饰的方法中。
  • 如果在构造器中使用super,则super用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。
  • 例:定义一个父类,Employee.java
import java.time.LocalDate;
public class Employee {
	private String name;
	private double salary;
	private LocalDate hireDay;
	
	public Employee(String name, double salary, int year, int month, int day) {
		this.name = name;
		this.salary = salary;
		hireDay = LocalDate.of(year, month, day);
	}
	public String getName()
	{
		return name;
	}
	public double getSalary()
	{
		return salary;
	}
	public LocalDate getHireDay()
	{
		return hireDay;
	}
	public void raiseSalary(double byPercent)
	{
		double raise = salary * byPercent / 100;
		salary += raise;
	}
}
  • 定义一个子类,Manager.java
public class Manager extends Employee 
{
	private double bonus;
	/** 
	 * @param name the employee's name
	 * @param salary the salary
	 * @param year the hire day
	 * @param month the hire month
	 * @param day the hire day
	 */
	public Manager(String name, double salary, int year, int month, int day) {
		super(name, salary, year, month, day);
		bonus = 0;
	}
	public double getSalary() 
	{
		double baseSalary = super.getSalary();
		return baseSalary + bonus;
	}
	public void setBonus(double b)
	{
		bonus = b;
	}
}
  • 创建Manager实例对象 ManagerTest.java
import inheritance.Employee;
import inheritance.Manager;
public class ManagerTest {
	public static void main(String[] args) 
	{
		// Construct a Manager object
		Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
		boss.setBonus(5000);
		Employee[] staff = new Employee[3];
		// fill the staff array with Manager and Employee objects
		staff[0] = boss;
		staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
		staff[2] = new Employee("Tommy Tester", 40000, 1990, 3,15);
		// print out information about all Employee objects
		for (Employee e:staff)
			System.out.println("name = " + e.getName() +", salary = " + e.getSalary());
	}
}

4.9 多态

4.9.1 多态性

  • Java引用变量有两个类型:编译时类型和运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现多态。
  • 多态指的是同一个方法调用,由于对象不同可能会有不同的行为,一个变量可以保存不同类型的对象。
  • 多态的要点:
    • 多态是方法的多态,不是实例变量的多态,对象的实例变量不具备多态性。
    • 多态的存在要有3个必要条件:继承、方法重写、父类引用指向子类对象。
    • 父类引用指向子类对象后,用该父类引用调用子类重写的方法此时多态就出现了。例如4.8中的ManagerTest.java程序
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;

4.9.2 引用变量的强制类型转换

  • 把子类对象赋给父类引用变量,称为向上造型(upcasting),这种转型编译器是允许的,属于自动类型转换。例:Object obj = new String("...");
  • 向上造型后的父类引用变量只能调用它编译时类型的方法,不能调用它运行时类型的方法。这是我们需要进行类型的强制转换,称为向下造型。例:String str = (String) obj;向下造型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常。
    - 例:ConversionTest.java
public class ConversionTest {
	public static void main(String[] args) {
		double d = 13.4;
		long l = (long)d;
		System.out.println(l);
		int in = 5;
		// 试图把一个数值类型的变量转换为boolean类型,下面代码编译出错
//		boolean b = (boolean)in;
//		System.out.println(b);// 输出 Cannot cast from int to boolean
		Object obj = "Hello";
		// obj变量编译时类型为Object, Object与String存在继承关系,可以强制类型转换
		// 而且obj变量的实际类型是String,所以运行时也通过
		String objStr = (String)obj;
		System.out.println(objStr);
		// 定义一个objPri变量,编译时类型为Object, 实际类型为Integer
		Object objPri = new Integer(5);
		// objPri编译时类型为Object,运行时类型为Integer
		// Object和Integer存在继承关系,可以强制类型转换,而obj变量的实际类型是Integer
		// 所以下面代码运行时引发ClassCastException异常
		String str = (String)objPri;
	}
}
  • 考虑到进行强制类型转换时可能出现异常,因此进行类型转换之前应先通过instanceof运算符来判断是否可以成功转换。例如将上面程序中的String str = (String)objPri;改为如下代码:
if (objPri instanceof String)
{
    String str = (String)objPri;
}

4.9.3 instanceof运算符

  • instanceof运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是一个接口),它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是,则返回true,否则返回false。
  • 使用instanceof运算符时需要注意:instanceof运算符前面的操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。
  • 例:InstanceofTest.java
public class InstanceofTest {
	public static void main(String[] args) {
		// 声明hello时使用Object类,则hello的编译类型是Object
		// Object是所有类的父类,但hello变量的实际类型是String 
		Object hello = "Hello";
		// String与Object类存在继承关系,可以进行instanceof运算。返回true
		System.out.println("字符串是否是Object类的实例:"
				+ (hello instanceof Object));
		System.out.println("字符串是否是String类的实例:"
				+ (hello instanceof String));
		// Math与Object类存在继承关系,可以进行instanceof运算。返回false
		System.out.println("字符串是否是Math类的实例:"
				+ (hello instanceof Math));
		// String实现了Comparable接口,所以返回true
		System.out.println("字符串是否是Comparable接口的实例:"
				+ (hello instanceof Comparable));
		String a = "Hello";
		// String类与Math类没有继承关系,所以下面代码编译无法通过
		System.out.println("字符串是否是Math类的实例:"
				+ (a instanceof Math));
	}
}

猜你喜欢

转载自blog.csdn.net/weixin_43564773/article/details/85736961