《疯狂Java讲义》读书笔记——第5章 面向对象(上)

 

本章重点

  • 定义类、成员变量和方法
  • 对象和引用
  • 方法必须属于类或对象
  • Java方法的参数传递机制
  • 递归方法
  • 方法的重载
  • 实现良好的封装
  • 使用package和import
  • 构造器的作用和构造器重载
  • 继承的特点和用法
  • 重写父类方法
  • super关键字的用法
  • 继承和多态
  • 向上转型和强制类型转换
  • 继承和组合的关系
  • 使用组合来实现复用
  • 构造器和初始化块的作用及区别
  • 静态初始化块

目        录


本章重点

5.1 类和对象

5.1.1 定义类

5.1.2 对象的产生和使用

5.1.3 对象、引用和指针

5.1.4 对象的this引用

5.2 方法详解

5.2.1 方法的所属性

5.2.2 方法的参数传递机制

5.2.3 形参个数可变的方法

5.2.4 递归方法

5.2.5 方法重载

5.3 成员变量和局部变量

5.3.1 成员变量和局部变量是什么

5.3.2 成员变量的初始化和内存中的运行机制

5.3.3 局部变量的初始化和内存中的运行机制

5.3.4 变量的使用规则

5.4 隐藏和封装

5.4.1 理解封装

5.4.2 使用访问控制符

5.4.3 package、import和import static

5.4.4 Java的常用包

5.5 深入构造器

5.5.1 使用构造器执行初始化

5.5.2 构造器重载(this)——可以通过不同的构造器来创建Java对象

5.6 类的继承

5.6.1 继承的特点

5.6.2 重写父类的方法

5.6.3 super限定

5.6.4 调用父类构造器

5.7 多态

5.7.1 多态性

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

5.7.3 instanceof运算符

5.8 继承与组合

5.8.1 使用继承的注意点

5.8.2 利用组合实现复用

5.9 初始化块

5.9.1 使用初始化块

5.9.2 初始化块和构造器

5.9.3 静态初始化块(被static修饰)


5.1 类和对象

可以使用类定义变量——统称为引用变量,即所有类是引用类型。

5.1.1 定义类

1、定义类的语法:

[修饰符]  class  类名

{

零个到多个构造器定义...

零个到多个成员变量..

零个到多个方法...

}

  • 三个修饰符:public、final、abstract。
  • 三种成员:构造器、成员变量、方法。

类里各成员之间可以互相调用,但是,static修饰的成员不能访问没有static修饰的成员。

  • 构造器——用于构造类的实例。
  • 成员变量——用于定义类或类的实例所包含状态数据。
  • 方法——用于定义类或类的实例的行为特征或者功能实现。

2、构造器的定义

语法格式:

[修饰符]  构造器名(形参列表)

{

//由零条到多条可执行性语句组成的构造器执行体

}

  • 修饰符:public、protected、private其中之一。
  • 构造器名:必须和类名相同。
  • 形参列表:和定义方法形参列表的格式完全相同。
  • 构造器既不能定义返回值类型,也不能使用void声明构造器没有返回值。

构造器通过new关键字来调用,如果程序员没有为一个类编写构造器,则系统为该类提供一个默认的构造器。

3、成员变量的定义

语法格式:

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

  • 修饰符:public、protected、private三个中只能出现一个与static、final组合起来修饰成员变量。
  • 类型:基本类型和引用类型
  • 成员变量名:合法的标识符即可。注意:第一个单词首字母小写,后面每个单词首字母大写,单词之间不要使用任何分隔符。

4、方法的定义

语法格式:

[修饰符]  方法返回值类型  方法名(形参列表)

{

//由零条到多条可执行性语句组成的方法体

}

  • 修饰符:public、protected、private、final、abstrac、static,其中public、protected、private三个最多只能出现其中一个;final和abstra最多只能出现其中一个,它们与static组合使用。
  • 方法返回值类型:包括基本类型和引用类型。如果有返回值类型,方法体内必须有一个有效的return语句;如果没有一个方法返回值类型,必须使用void声明没有返回值。
  • 方法名:与成员变量的命名规则相同,建议方法名以英文动词开头。
  • 形参列表:由零组到多组“参数类型 形参名”组合而成,多组参数之间以英文逗号隔开——谁调用方法,谁负责为形参赋值。

static是一个特殊的关键字,可修饰方法、成员变量等成员。

类变量、类方法(静态变量、静态方法)——static修饰的成员变量和方法。

实例变量、实例方法(非静态变量、非静态方法)——不使用static修饰的成员变量和方法。

静态成员不能直接访问非静态成员。

有static修饰的成员属于类本身,没有static修饰的成员属于类的实例。

5、Java的类的作用

  • 定义变量。
  • 创建对象。
  • 调用类的类方法或访问类的类变量。

看以下实例:

/*
 *程序名称:Person.java
 *程序功能:本类里没有定义构造器,系统会为它提供没有参数的构造器
 *编写时间:2019-1-2
 */
public class Person
{
	//下面定义两个成员变量
	public String name;
	public int age;
	//下面定义一个say方法
	public void say(String content)
	{
		System.out.println(content);
	}
}

5.1.2 对象的产生和使用

创建对象的根本途径是构造器,通过new关键字来调用某个类的构造器即可创建这个类的实例。

如下实例代码PersonTest.java:

//使用Person类定义一个Person类型的变量
Person p;
//通过new关键字调用Person可得构造器,返回一个Person实例
//将该Person实例赋给p变量
p=new Person();

上面的代码也可以简写成如下形式:

//定义p变量的同时并为p变量赋值
Person p = new Person();

创建对象之后就是使用对象,Java的对象的作用大致如下:

  • 访问对象的实例变量。
  • 调用对象的方法。

语法格式:

类.类变量|方法

实例.实例变量|方法

注意:static修饰的方法和成员变量,即可通过类来调用,也可通过实例来调用;没有使用static修饰的普通方法和成员变量,只可通过实例变量来调用。

下面的代码通过Person实例来调用Person的成员变量和方法。

//访问p的name实例变量,直接为该变量赋值
p.name = "李刚";
//调用p的say()方法,声明say()方法时定义了一个形参
//调用该方法必须为形参指定一个值
p.say("Java语言很简单,学习很容易!");
//直接输出p的name实例变量,将输出 李刚
System.out.println(p.name);

5.1.3 对象、引用和指针

如PersonTest.java代码中,Person p=new Person()这行代码,创建了一个Person实例,也被称为Person对象,它被赋给p变量。如下图显示了Person对象在内存中的存储示意图。

引用变量里存放的仅仅是一个引用,它指向实际的对象。

类也是一种引用数据类型,因此程序中定义的Person类型的变量实际上是一个引用,它被存放在栈内存里,指向实际的Person对象;真正的Person对象则存放在堆(heap)内存中。图5.2显示了将Person对象赋给一个引用变量的示意图。

栈内存里的引用变量并未真正存储对象的成员变量,对象的成员变量数据实际存放在堆内存里;而引用变量只是指向该堆内存里的对象。Java程序不允许直接访问堆内存中的对象,只能通过该对象的引用操作该对象。即不管是数组还是对象,都只能通过引用来访问它们。

5.1.4 对象的this引用

this关键字总是指向调用该方法的对象。根据this出现位置的不同,this作为对象的默认引用有两种情形:

(1)、构造器中引用该构造器正在初始化的对象;

(2)、在方法中引用调用该方法的对象。

this关键字最大的作用就是让类中一个方法,访问该类里的另一个方法或实例变量。

this可以代表任何对象。

例:

/*
 *程序名称:Dog.java
 *程序功能:this关键字的最大作用就是让类中一个方法,访问类里的另一个方法或实例变量
 *编写时间:2019-1-3
 */

public class Dog
{

	//定义一个jump()方法
	public void jump()
	{
		System.out.println("正在执行jump方法");
	}
	//定义一个run()方法,run()方法需要借助jump()方法
	public void run()
	{
		//Dog d=new Dog();
		//d.jump();
		this.jump();
		System.out.println("正在执行run方法");
	}
}

Java允许对象的一个成员直接调用另一个成员,可以省略this前缀。static修饰的方法中不能使用this引用。因此,Java语法规定,静态成员不能直接访问非静态成员变量。

由于构造器是直接使用new关键字来调用,而不是使用对象来调用,所以this在构造器中代表该构造器正在初始化的对象。

例如:

public class ThisInConstructor
{
	//定义一个名为foo的成员变量
	public int foo;
	public ThisInConstructor()
	{
		//在构造器里定义一个foo变量
		int foo=0;
		//使用this代表该构造器正在初始化的对象
		//下面的代码将会把该构造器正在初始化的对象的foo成员变量设为6
		this.foo=6;
	}
	public static void main(String[] args) {
		//所有使用ThisInConstructor创建的对象的foo成员变量
		//都将设为6,所以下面的代码都将输出6
		System.out.println(new ThisInConstructor().foo);
	}
}

上例中构造器中有一个与成员变量同名的局部变量,又必须在构造器中访问这个被覆盖的成员变量,则必须使用this前缀。

使用this作为方法的返回值可以让代码更加简洁,但可能造成实际意义的模糊。例如:

public class ReturnThis
{
	public int age;
	public ReturnThis grow()
	{
		age++;
		//return this返回调用方法的对象
		return this;
	}
	public static void main(String[] args) {
		ReturnThis rt=new ReturnThis();
		//可以连续调用同一个方法
		rt.grow()
		  .grow()
		  .grow();
		System.out.println("rt的age成员变量值是:"+rt.age);
	}
}

5.2 方法详解

方法是类或对象的行为特征的抽象,是类或对象最重要的组成部分。在Java里方法不能独立存在,在逻辑上要么属于类,要么属于对象。

5.2.1 方法的所属性

主要体现在如下几个方面:

(1)、方法不能独立定义,方法只能在类体里定义。

(2)、从逻辑意义上来看,方法要么属于该类本身,要么属于该类的一个对象。

(3)、永远不能独立执行方法,执行方法必须使用类或对象作为调用者。

另外,使用static修饰的方法,既可以使用类作为调用者,也可以使用对象作为调用者没有使用static修饰方法,只能使用对象作为调用者来调用,不能使用类作为调用者调用。

5.2.2 方法的参数传递机制

Java里方法的参数传递方式只有一种:值传递

值传递——就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响。

例如:

/*
 *程序名称:PrimitiveTransferTest.java
 *程序功能:基本类型的参数传递
 *编写日期:2019-1-8
 */
public class PrimitiveTransferTest
{
	public static void swap(int a,int b)
	{
		//下面三行代码实现a,b变量的值交换
		//定义一个临时变量来保存a变量的值
		int tmp = a;
		//把b的值赋给a
		a=b;
		//把临时变量tmp的值赋给b
		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);
	}
}

又如:

/*
 *程序名称:ReferenceTransferTest.java
 *程序功能:引用类型的参数传递
 *编写日期:2019-1-9
 */
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);
		//把dw赋值为null,让它不再指向任何有效地址
		dw=null
	}
	public static void main(String[] args) 
	{
		//创建DataWrap对象,并定义一个dw引用变量指向DataWrap对象
		DataWrap dw = new DataWrap();
		dw.a=6;
		dw.b=9;
		swap(dw);
		System.out.println("交换结束后,变量a成员变量的值是"+dw.a+":b成员变量的值是"+dw.b);
	}
}

不管是基本类型的参数传递,还是引用类型的参数传递,都采用的是值传递方式。

5.2.3 形参个数可变的方法

从JDK1.5之后,Java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的形参,如果在定义方法时,在最后一个形参的类型后增加三点(...),则表明该形参可以接受多个参数值,多个参数值被当成数组传入。

例如:

/*
 *程序名称:Varargs.java
 *程序功能:形参个数可变的方法
 *(个数可变的形参只能处于形参列表的最后,一个方法中最多只能包含一个个数可变的形参,其本质是一个数组类型的形参)
 *编写日期:2019-1-9
 */
public class Varargs
{
	public static void test(int a,String... books)	//调用方法时更简洁
	//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,"疯狂Java讲义","轻量级Java EE 企业应用实战");
		//调用test()方法时传入一个数组
		//test(23,new String[]{"疯狂Java讲义","轻量级Java EE 企业应用实战"});
	}
}

因此,个数可变的形参只能处于形参列表的最后。一个方法中最多只能包含一个个数可变的形参。个数可变的形参本质就是一个数组类型的形参,所以调用包含个数可变形参的方法时,该个数可变的形参既可以传入多个参数,也可以传入一个数组。

5.2.4 递归方法

——一个方法体内调用它本身,被称为方法调用。方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。

例1:

/*
 *程序名称:Recursive.java
 *程序功能:递归方法
 *注意:递归一定要向已知方向递归
 *编写日期:2019-1-9
 */
//已知有一个数列,f(0)=1,f(1)=4,f(n+2)=2*f(n+1)+f(n),
//其中n是大于0的整数,求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));
	}
} 

例2:

/*
 *程序名称:Recursive2.java
 *程序功能:递归方法
 *注意:递归一定要向已知方向递归
 *编写日期:2019-1-9
 */
//已知有一个数列,f(20)=1,f(21)=4,f(n+2)=2*f(n+1)+f(n),
//其中n是大于0的整数,求f(10)的值。
public class Recursive2
{
	public static int fn(int n)
	{
		if (n==20) 
		{
			return 1;
		}
		else if (n==21) 
		{
			return 4;
		}
		else
		{
			//方法中调用它自身,就是方法递归
			return fn(n+2)-2*fn(n+1);
		}
	}
	public static void main(String[] args) {
		//输出fn(10)的结果
		System.out.println(fn(10));
	}
} 

5.2.5 方法重载

(1)、定义:如果同一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。

(2)、确定一个方法的三个要素:

  • 调用者,也就是方法的所属者,既可以是类,也可以是对象。
  • 方法名,方法的标识。
  • 形参列表,当调用方法时,系统将会根据传入的实参列表匹配。

(3)、方法重载的要求:两同一不同——同一个类中方法名相同,参数列表不同。

例如:

/*
 *程序名称:Overload.java
 *程序功能:方法重载的示例
 *编写日期:2019-1-11
 *方法重载的要求:两同一不同——即同一个类中方法名相同,参数列表不同。
 */
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 ol =new Overload();
		//调用test()方法时没有传入参数,因此系统调用上面没有参数的test()方法
		ol.test();
		//调用test()方法传入了一个字符串参数
		//因此系统调用上面带了一个字符串参数的test()方法
		ol.test("hello");
	}
}

在Java里不能使用方法返回值类型作为区分方法重载的依据。

又例如:

/*
 *程序名称:OverloadVarargs.java
 *程序功能:定义两个方法重载的方法
 *编写日期:2019-1-11
 *下面的示例中第二个test()方法包含了个数可变的形参。
 */
public class OverloadVaragrs
{
	public void test(String msg)
	{
		System.out.println("只有一个字符串参数的test()方法");
	}
	//因为前面已经有了一个test()方法,test()方法里有一个字符串参数
	//此处的个数可变参数里不包含一个字符串参数的形式
	public void test(String... books)
	{
		System.out.println("****形参个数可变的test()方法****");
	}
	public static void main(String[] args) 
	{
		OverloadVaragrs olv=new OverloadVaragrs();
		//下面两次调用将执行第二个test()方法
		olv.test();
		olv.test("aa","bb");
		//下面调用执行第一个test()方法
		olv.test("aa");
		//下面调用执行第二个test()方法
		olv.test(new String[]("aa"));
	}
}

不推荐重载形参个数可变的方法,容易降低程序的可读性。

5.3 成员变量和局部变量

5.3.1 成员变量和局部变量是什么

成员变量:指的是在类里定义的变量。

局部变量:指的是在方法里定义的变量。

变量分类图

访问变量的语法:

类.类变量

实例.实例变量

实例.类变量

例如:

/*
 *程序名称:PersonTest.java
 *程序功能:下面程序定义了一个Person类,在这个Person类中定义两个成员变量,一个实例变量:name
 *以及一个类变量:eyeNum。程序通过PersonTest类来创建Person实例,并分别通过Person类和Person实
 *例来访问实例变量和类变量。
 *编写日期:2019-1-11
 */
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);
	}
}

与成员变量不同的是,局部变量除形参之外,都必须显式初始化。也就是说,必须先给方法局部变量和代码块局部变量指定初始值,否则不可以访问它们。

例如:

/*
 *程序名称:BlockTest.java
 *程序功能:定义代码块局部变量
 *编写日期:2019-1-13
 */
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.println(a);
	}
}

又例如:

/*
 *程序名称:MethodLocalVariableTest.java
 *程序功能:示范方法局部变量的作用域
 *编写日期:2019-1-13
 */
public class MethodLocalVariableTest
{
	public static void main(String[] args) 
	{
		//定义一个方法局部变量a
		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="李刚";
	//定义一个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();
	}
	public void info()
	{
		//方法里的局部变量,局部变量覆盖成员变量
		String name="孙悟空";
		//直接访问name变量,将输出name局部变量的值:“孙悟空”
		System.out.println(name);
		//使用this来作为name变量的限定,将输出name实例变量的值:"李刚"
		System.out.println(this.name);
	}
}

5.3.2 成员变量的初始化和内存中的运行机制

当系统加载类或创建类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值。

当程序需要访问类变量时,尽量使用类作为主调,而不要使用对象作为主调,这样可以避免程序产生歧义,提高程序的可读性。

 

5.3.3 局部变量的初始化和内存中的运行机制

(1)、局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。

(2)、局部变量不属于任何类或实例,总是存在其所在方法的栈内存中。

(3)、如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;如果局部变量是一个引用类型的变量,则这个变量存放的是地址,通过该地址引用到该变量实际引用的对象或数组。

(4)、栈内存中的变量无需系统垃圾回收,随方法或代码块的结束而结束。

(5)、局部变量所占的内存区通常比较小。

5.3.4 变量的使用规则

何时应该考虑使用成员变量?

(1)、如果需要定义的变量是用于描述某个类或某个对象的固有信息的;

(2)、如果在某个类中需要以一个变量来保存该类或实例运行时的状态信息的;(例如五子棋中的棋盘数组)

(3)、如果某个信息需要在某个类的多个方法之间进行共享。

应该尽可能的缩小局部变量的作用范围,节省内存。

5.4 隐藏和封装

5.4.1 理解封装

封装(Encapsulation)是面向对象的三大特征之一,它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。

封装实际上有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。——这些需要通过使用Java提供的访问控制符来实现。

5.4.2 使用访问控制符

表5.1 访问控制级别表
        privatedefaultprotectedpublic
同一个类中
同一个包中 
子类中  
全局范围中   

如果一个Java源文件里定义的所有类都没有使用publ修饰,则这个Java源文件的文件名可以是一切合法的文件名;但如果一个Java源文件里定义了一个publ修饰的类,则这个源文件的文件名必须与public修饰的类的类名相同。

例如:

/*
 *程序名称:Person.java
 *程序功能:通过使用合理的访问控制符来定义一个Person类,实现这个Person类的良好的封装。
 *编写日期:2019-1-13
 */
public class Person
{
	//使用private修饰成员变量,将这些成员变量隐藏起来
	private String name;
	private int age;
	//提供方法来操作name成员变量
	public void setName(String name)
	{
		//执行合理性检验,要求用户名必须在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;
	}
}
/*
 *程序名称:PersonTest.java
 *程序功能:在main()方法中创建一个Person对象,并尝试操作和访问该对象的age和name两个实例变量。
 *编写日期:2019-1-13
 */
public class PersonTest
{
	public static void main(String[] args) 
	{
		Person p = new Person();
		//因为age成员变量已被隐藏,所以下面语句将出现编译错误。
		//p.age=1000;
		//下面语句编译不会出现错误,但运行时将提示“您设置的年龄不合法”
		//程序不会修改p的age成员变量
		p.setAge(1000);
		//访问p的age成员变量也必须通过其对应的getter方法
		//因为上面从成功设置p的age成员变量,故此处输出0
		System.out.println("未能设置age成员变量时:"+p.getAge());
		//成功修改p的age成员变量
		p.setAge(30);
		//因为上面成功设置了p的age成员变量,故此处输出30
		System.out.println("成功设置age成员变量后:"+p.getAge());
		//不能直接操作p的name成员变量,只能通过其对应的setter方法
		//因为“李刚”字符串长度满足2-6的条件,所以成功设置
		p.setName("李刚");
		System.out.println("成功设置name成员变量后:"+p.getName());
	}
}

访问控制符的使用的基本原则:

  • 类里的绝大部分成员变量都应该使用private修饰,只有一些static修饰的、类似全局变量的成员变量,才可能考虑使用public修饰。除此之外,工具方法也应该使用private修饰。
  • 如果某个类主要用作其他类的父类,该类里包含的大部分方法也可能仅希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰这些方法。
  • 希望暴露出来给其他类自由调用的方法应该使用public修饰。因此,类的构造器通过使用public修饰,从而允许在其他地方创建该类的实例。因为外部类通常都希望被其他类自由使用,所以大部分外部类都使用public修饰。

5.4.3 package、import和import static

1、为了解决类的命名冲突、类文件管理等问题,Java语言引入了包(package)机制,提供了类的多层命名空间。

具体用法见实例:

/*
 *程序名称:Hello.java
 *程序功能:在lee包下定义一个简单的Java包。
 *编写日期:2019-1-14
 */
package lee;
public class Hello
{
	public static void main(String[] args) 
	{
		System.out.println("Hello World!");
	}
}

Java规定:位于包中的类,在文件系统中也必须有与包名层次相同的目录结构。

因此,在编译源文件时,应该使用如下命令:

java  -d  .   Hello.java

一定要加上-d选项。

注意:

为Java类添加包必须在Java源文件中通过package语句指定,单靠目录名是没法指定的。Java的包机制需要两个方面保证:⑴源文件里使用package语句指定包名;⑵class文件必须放在对应的路径下。

package语句必须作为源文件的第一条非注释性语句,一个源文件只能指定一个包,即只能包含一条package语句,该源文件中可以定义多个类,则这些类将全部位于该包下。

例:

/*
 *程序名称:HelloTest.java
 *程序功能:把HelloTest类也放在lee包下,可以直接访问lee包下的Hello类,无需添加包前缀
 *编写日期:2019-1-14
 */
package lee;
//使用import导入lee.sub.Apple类
import lee.sub.Apple;

public class HelloTest
{
	public static void main(String[] args) 
	{
		//直接访问相同包下的另一个类,无需使用包前缀
		Hello h=new Hello();
		//使用类全名的写法
		lee.sub.Apple a=new lee.sub.Apple();
		//如果使用import语句来导入Apple类,就可以不再使用全类名了
		Apple aa=new Apple();
	}
}

2、为了简化编程,Java引入import关键字,其可以向某个Java文件中导入指定包层次下某个类或全部类,import语句应该出现在package语句之后、类定义之前。一个Java源文件只能包含一个package语句,但可以包含多个import语句,多个import语句用于导入多个包层次下的类。(见上例)

3、JDK 1.5之后增加了一种静态导入的语法,它使用import static语句,用于导入指定类的某个静态成员变量、方法或全部的静态成员变量、方法。

用一句话来归纳import和import static的作用:使用import可以省略写包名;而使用import static则可以连类名都省略。

例如:

/*
 *程序名称:StaticImportTest.java
 *程序功能:使用import static语句导入java.lang.System类下的全部静态成员变量,从而可以将程序简化
 *import和import static的作用:使用import可以省略写包名,而使用import static则可以连类名都省略。
 *编写日期:2019-1-14
 */
import static java.lang.System.*;
import static java.lang.Math.*;
public class StaticImportTest
{
	public static void main(String[] args) 
	{
		//out是java.lang.System类的静态成员变量,代表标准输出
		//PI是java.lang.Math类的静态成员变量,表示π常量
		out.println(PI);
		//直接调用Math类的sqrt静态方法
		out.println(sqrt(256));
	}
}

5.4.4 Java的常用包

  1. java.lang:包含了Java语言的核心类,如String、Math、System和Thread类等。使用这个包下的类无需使用import语句导入,系统会自动导入这个包下的所有类。
  2. java.util:包含了Java的大量的工具类/接口和集合框架类/接口,例如Arrays和LIst、Set等。
  3. java.net:包含了一些Java网络编程相关的类/接口。
  4. java.io:包含了一些Java输入/输出编程相关的类/接口。
  5. java.text:包含了一些Java格式化相关的类。
  6. java.sql:包含了Java进行JDBC数据库编程的相关类/接口。
  7. java.awt:包含了抽象窗口工具集的相关类/接口。这些类主要用于构建图形用户界面(GUI)程序。
  8. java.swing:包含了Swing图形用户界面编程的相关类/接口,这些类可用于构建平台无关的GUI程序。

5.5 深入构造器

Java类必须包含一个或一个以上的构造器。

5.5.1 使用构造器执行初始化

构造器最大的用处就是在创建时执行初始化。

如果程序员没有为Java类提供任何构造器,则系统会为这个类提供一个无参数的构造器,这个构造器的执行体为空,不做任何事情。无论如何,Java类至少包含一个构造器。

例如:

/*
 *程序名称:ConstructorTest.java
 *程序功能:自定义一个构造器,通过这个构造器可以让程序员进行自定义的初始化操作。
 *编写日期:2019-1-14
 */
public class ConstructorTest
{
	public int count;	//实例变量
	public String name;	//实例变量
	//提供自定义的构造器,该构造器包含两个参数
	public ConstructorTest(String name,int count)
	{
		//构造器里的this代表它进行初始化的对象
		//下面两行代码将传入的2个参数赋给this代表对象的name和count实例变量
		this.name=name;
		this.count=count;
	}
	public static void main(String[] args) 
	{
		//使用自定义的构造器来创建对象
		//系统将会对该对象执行自定义的初始化
		ConstructorTest tc=new ConstructorTest("疯狂Java讲义",90000);
		//输出ConstructorTest对象的name和count两个实例变量
		System.out.println(tc.name);
		System.out.println(tc.count);
	}
}

5.5.2 构造器重载(this)——可以通过不同的构造器来创建Java对象

定义:同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。

例1:

/*
 *程序名称:ConstructorOverload.java
 *程序功能:利用构造器重载可以通过不同的构造器来创建Java对象
 *编写日期:2019-1-14
 */
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对象
		ConstructorOverload oc2=new ConstructorOverload("轻量级Java EE企业应用实战",30000);
		System.out.println(oc1.name+" "+oc1.count);
		System.out.println(oc2.name+" "+oc2.count);
	}
}

例2:

/*
 *程序名称:Apple.java
 *程序功能:实现在一个构造器中直接使用另一个构造器的初始化代码。——使用this关键字调用相应的构造器
 *编写日期:2019-1-14
 */
public class Apple
{
	public String name;
	public String color;
	public double weight;
	public Apple(){}
	//两个参数的构造器
	public Apple(String name,String color)
	{
		this.name=name;
		this.color=color;
	}
	//三个参数的构造器
	public Apple(String name,String color,double weight)
	{
		//通过this调用另外一个重载的构造器的初始化代码
		this.(name,color);
		//下面this引用该构造器正在初始化的java对象
		this.weight=weight;
	}
}

使用this调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句。

使用this调用重载的构造器时,系统会根据this后花括号里的实参来调用形参列表与之对应的构造器。

5.6 类的继承

继承是实现软件复用的重要手段,Java的继承具有单继承的特点,每一个子类只有一个直接父类。

5.6.1 继承的特点

语法格式:

修饰符 class SubClass extends SuperClass
{
    //类定义部分
}

1、Java的继承通过关键字extends来实现,实现继承的类被称为子类,被继承的类被称为父类。

2、父类是大类,子类是小类。父类的范围大于子类。

3、Java的子类不能获得父类的构造器。

4、Java类只能有一个直接父类,但可以有无限多个间接父类。

例:

/*
 *程序名称:Fruit.java
 *程序功能:子类继承父类
 *编写日期:2019-1-14
 */
public class Fruit
{
	public double weight;
	public void info()
	{
		System.out.println("我是一个水果!重"+weight+"g");
	}
}
/*
 *程序名称:Apple.java
 *程序功能:子类继承父类
 *编写日期:2019-1-14
 */
public class Apple extends Fruit
{
	public static void main(String[] args) 
	{
		//创建Apple对象
		Apple a=new Apple();
		//Apple对象本身没有weight成员变量
		//因为Apple的父类有weight成员变量,也可以访问Apple对象的weight成员变量
		a.weight=56;
		//调用Apple对象的info()方法
		a.info();
	}
}

5.6.2 重写父类的方法

子类包含与父类同名方法的现象被称为方法重写(Override),也被称为方法覆盖。

例如:

/*
 *程序名称:Bird.java
 *程序功能:重写父类方法
 *编写日期:2019-1-14
 */
public class Bird
{
	//Bird类的fly()方法
	public void fly()
	{
		System.out.println("我在天空里自由自在地飞翔...");
	}
}
/*
 *程序名称:Ostrich.java
 *程序功能:子类继承父类
 *编写日期:2019-1-14
 */
public class Ostrich extends Bird
{
	//重写Bird类的fly()方法
	public void fly()
	{
		System.out.println("我只能在地上奔跑...");
	}
	
	public static void main(String[] args) 
	{
		//创建Ostrich对象
		Ostrich os=new Ostrich();
		//执行Ostrich对象的fly()方法,将输出"我只能在地上奔跑..."
		os.fly();
	}
}

方法的重写要遵循“两同两小一大”的规则,“两同”即方法名相同、形参列表相同;“两小”值得是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。

需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个实例方法。

重载与重写的区别:(1)重载(overload)主要发生在同一个类的多个同名方法之间,而重写(override)发生在子类和父类的同名方法之间。

5.6.3 super限定

如果需要在子类方法中调用父类被覆盖的实例方法,则可使用super限定来调用父类被覆盖的实例方法。

例如:为上例的Ostrich类添加一个方法,在这个方法中调用Bird类中被覆盖的fly方法。

public void callOverridedMethod()
	{
		//在子类方法中通过super显式调用父类被覆盖的实例方法
		super.fly();
	}

(1)super同this一样,也不能出现在static修饰的方法中。

(2)如果在构造器中使用super,则其用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。

(3)如果子类定义了和父类同名的实例变量,则会发生子类实例变量隐藏父类实例变量的情形。在子类定义的实例方法中可以通过super来访问父类中被隐藏的实例变量。

例如:

/*程序名称:SubClass.java
 *程序功能:
 *编写日期:2019-4-1
*/
class BaseClass
{
    public int a = 5;
}
public class SubClass extends BaseClass
{
    public int a =7;
    public void accessOwner()
    {
        System.out.println(a);
    }
    public void accessBase()
    {
        //通过super来限定访问从父类继承得到的a实例变量
        System.out.println(super.a);
    }
    public static void main(String[] args)
    {
        SubClass sc = new SubClass();
        sc.accessOwner();    //输出7
        sc.accessBase();    //输出5
    }
}

(4)如果被覆盖的类变量,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量。

例如:

/*
 *程序名称:HideTest.java
 *程序功能:
 *编写日期:2019-4-1
 */
class Parent
{
    public String tag = "疯狂Java讲义";
}
class Derived extends Parent
{
    //定义一个私有的tag实例变量来隐藏父类的tag实例变量
    private String tag = "轻量级Java EE企业应用实战";
}
public class HideTest
{
    public static void main(String[] args)
    {
        Derived d = new Derived();
        //程序不可访问d的私有变量tag,所以下面语句将引起编译错误
        //System.out.println(d.tag);
        //将d变量显式地向上转型为Parent后,即可访问tag实例变量
        //程序将输出:“疯狂Java讲义”
        System.out.println(((Parent)d).tag);
     }
}

5.6.4 调用父类构造器

子类不会获得父类的构造器,但子类构造器里可以(用super)调用父类构造器的初始化代码,类似于前面介绍的一个构造器调用另一个重载的构造器。

例如:

/*
 *程序名称:Sub.java
 *程序功能:在Sub类的构造器中使用super来调用Base构造器的初始化代码
 *编写时间:2019-4-1
 */
class Base
{
    public double size;
    public String name;
    public Base(double size,String name)
    {
        this.size = size;
        this.name = name;
    }
}
public class Sub extends Base
{
    public String color;
    public Sub(double size,String name,String color)
    {
        //通过super调用父类构造器的初始化过程
        super(size,name);
        this.color = color;
    }
    public static void main(String[] args)
    {
        Sub s = new Sub(5.6,"测试对象","红色");    
        //输出Sub对象的三个实例变量
        System.out.println(s.size + "--" + s.name + "--" + s.color);
    }
}

this和super的区别:super调用的是其父类的构造器,而this调用的是同一个类中重载的构造器。使用super调用父类构造器也必须出现在子类构造器执行体的第一行,所以this调用和super调用不会同时出现。

子类构造器调用父类构造器分几种情况:

  • 子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实例列表调用父类对应的构造器。
  • 子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器时即会调用父类构造器。
  • 子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。

注意:创建任何Java对象,最先执行的总是java.lang.Object类的构造器。

例如:

/*
 *程序名称:Wolf.java
 *程序功能:构造器之间的调用关系
 *编写日期:2019-4-1
 */
class Creature
{
    public Creature()
    {
        System.out.println("Creature无参数构造器");
    }
}
class Animal extends Creature
{
    public Animal(String name)
    {
        System.out.println("Animal带一个参数的构造器,"+"该动物的name为"+name);
    }
    public Animal(String name,int age)
    {
        //使用this调用同一个重载的构造器
        this(name);
        System.out.println("Animal带两个参数的构造器," + "其age为" + age);
    }
}
public class Wolf extends Animal
{
    public Wolf()
    {
        //显式调用父类有两个参数的构造器
        super("灰太狼",3);
        System.out.println("Wolf无参数的构造器");
    }
    public static void main(String[] args)
    {
        new Wolf();
    }
}

5.7 多态

定义:Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态(Polymorphism)

5.7.1 多态性

(1)、引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。

(2)、通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量。

例如:

class BaseClass
{
	public int book=6;
	public void base()
	{
		System.out.println("父类的普通方法");
	}
	public void test()
	{
		System.out.println("父类的被覆盖的方法");
	}
}

public class SubClass extends BaseClass
{
	//重新定义一个book实例变量隐藏父类的book实例变量
	public String book="轻量级Java EE企业应用实战";
	public void test()
	{
		System.out.println("子类的覆盖父类的方法");
	}
	public void sub()
	{
		System.out.println("子类的普通方法");
	}
	public static void main(String[] args) 
	{
		//下面编译时类型和运行时类型完全一样,因此不存在多态
		BaseClass bc = new BaseClass();
		//输出6
		System.out.println(bc.book);
		//下面两次调用将执行BaseClass的方法
		bc.base();
		bc.test();
		//下面编译时类型和运行时类型完全一样,因此不存在多态
		SubClass sc = new SubClass();
		//输出"轻量级Java EE企业应用实战"
		System.out.println(sc.book);
		//下面将调用从父类继承到的base()方法
		sc.base();
		//下面将调用从父类继承到的test()方法
		sc.test();
		//下面编译时类型和运行时类型完全不一样,多态发生
		BaseClass polymorphismBc=new SubClass();
		//输出6——表明输出的是父类的实例变量
		System.out.println(polymorphismBc.book);
		//下面将调用从父类继承到的base()方法
		polymorphismBc.base();
		//下面调用将执行当前类的test()方法
		polymorphismBc.test();
		//因为polymorphismBc的编译时类型BaseClass
		//BaseClass类没有提供sub()方法,所以下面代码编译时会出现错误
		/*注意:通过引用变量来访问其包含的实例变量时,系统总是试图访
		//问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量。
		//polymorphismBc.sub();
	}
}

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

解决5.7.1中的问题,就是强制类型转换,而其要借助于类型转换运算符。具体用法:(type)variable。

进行强制类型转换需要注意一下几点:

(1)、基本类型之间的转换只能在数值类型(整数型、字符型和浮点型)之间进行。数值类型和布尔类型之间不能进行类型转换。

(2)、引用类型之间的转换只能在具有继承关系的两个类型之间进行,如果是两个没有任何继承关系的类型,则无法进行类型转换,否则编译时就会出现错误。如果试图把一个父类实例转换成子类类型,则这个对象必须实际上是子类实例才行(即编译时类型为父类类型,运行时类型是子类类型),否则将在运行时引发ClassCastException异常。

例如:

/*
 *程序名称:ConversionTest.java
 *程序功能:强制类型转换的示范程序。
 *编写日期:2019-1-16
 */
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 b=(boolean)in;
		Object obj="Hello";
		//obj变量的编译时类型为Object,Object与String存在继承关系,可以强制类型转换
		//而且obj变量的实际类型是String,所以运行时也可以通过。
		String objStr=(String)obj;
		System.out.println(objStr);
		//定义一个objPri变量,编译时类型为Object,实际类型为Integer
		Object objPri=Integer.valueOf(5);
		//objPri变量的编译时类型为Object,objPri的运行时类型为Integer
		//Object与Integer存在继承关系
		//可以强制类型转换,而objPri变量的实际类型是Integer
		//所以下面代码运行时引发ClassCastException异常,因此可以改为如下:
		if (objPri instanceof String) {
			String str=(String)objPri;
		}
		System.out.println(objPri);
	}
}

5.7.3 instanceof运算符

(1)、语法格式:引用类型变量    instanceof 类[接口]

(2)、作用:用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是,则返回true,否则返回false。

(3)、注意事项:instanceof运算符前面的操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。

例如:

/*
 *程序名称:InstanceofTest.java
 *程序功能:instanceof运算符的用法
 *格式:引用型变量  instanceof 类[接口]
 *作用:判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是,则返回true,否则返回false。
 *编写日期:2019-1-16
 */
public class InstanceofTest
{
	public static void main(String[] args) 
	{
		//声明hello时使用Object类,则hello的编译类型是Object
		//Objec是所有类的父类,但hello变量的实际类型是String
		Object hello="Hello";
		//String与Object存在继承关系,可以进行instanceof运算。返回true
		System.out.println("字符串是否是Object类的实例:"+(hello instanceof Object));
		System.out.println("字符串是否是String类的实例:"+(hello instanceof String));	//返回true
		//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));
	}
}

5.8 继承与组合

继承是实现复用的重要手段,组合则是实现复用的重要方式,继承破坏了封装,组合则提供更好的封装性。

5.8.1 使用继承的注意点

(1)设计父类通常应该遵循的原则:

  • 尽量隐藏父类的内部数据(private)。
  • 不要让子类可以随意访问、修改父类的方法。(private、public、final、protected四个修饰符的用法)
  • 尽量不要在父类构造器中调用将要被子类重写的方法。

(2)何时从父类派生子类需要具备以下两个条件之一:

  • 子类需要额外增加属性,而不仅仅是属性值的改变。
  • 子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)

例如:

/*
 *程序名称:Sub.java
 *程序功能:继承关系存在的问题
 *编写日期:2019-1-17
 */
class Base
{
	public Base()
	{
		test();
	}
	public void test()
	{
		System.out.println("将被子类重写的方法");
	}
}
public class Sub extends Base
{
	private String name;
	public void test()
	{
		System.out.println("子类重写父类的方法,"+"其name字符串长度"+name.length());
	}
	public static void main(String[] args) 
	{
		//下面代码会引发空指针异常
		Sub s=new Sub();
	}
}

5.8.2 利用组合实现复用

(1)、继承实现类复用

例如:

/*
 *程序名称:InheritTest.java
 *程序功能:通过让Bird和Wolf继承Animal,从而允许Wolf和Bird获得Animal的方法,
 *从而复用了Animal通过的breath()方法。
 *编写日期:2019-1-17
 */
class Animal
{
	private void beat()
	{
		System.out.println("心脏跳动...");
	}
	public void breath()
	{
		beat();
		System.out.println("吸一口气,吐一口气,呼吸中...");
	}
}
//继承Animal,直接复用父类的breath()方法
class Bird extends Animal
{
	public void fly()
	{
		System.out.println("我在天空自在飞翔...");
	}
}
class Wolf extends Animal
{
	public void run()
	{
		System.out.println("我在陆地上飞快地奔跑...");
	}
}
public class InheritTest
{
	public static void main(String[] args) 
	{
		Bird b=new Bird();
		b.breath();
		b.fly();
		Wolf w=new Wolf();
		w.breath();
		w.run();
	}
}

(2)、组合实现复用

例如:

/*
 *程序名称:CompositeTest.java
 *程序功能:将InheritTest.java改为如下代码,利用组合实现复用。
 *编写日期:2019-1-17
 */
class Animal
{
	private void beat()
	{
		System.out.println("心脏跳动...");
	}
	public void breath()
	{
		beat();
		System.out.println("吸一口气,吐一口气,呼吸中...");
	}
}
class Bird
{
	//将原来的父类组合到原来的子类,作为子类的一个组合成分
	private Animal a;
	public Bird(Animal a)
	{
		this.a=a;
	}
	//重新定义一个自己的breath()方法
	public void breath()
	{
		//直接复用Animal通过的breath()方法来实现Bird的breath()方法
		a.breath();
	}
	public void fly()
	{
		System.out.println("我在天空自在飞翔...");
	}
}
class Wolf
{
	//将原来的父类组合到原来的子类,作为子类的一个组合成分
	private Animal a;
	public Wolf(Animal a)
	{
		this.a=a;
	}
	//重新定义一个自己的breath()方法
	public void breath()
	{
		//直接复用Animal通过的breath()方法来实现Wolf的breath()方法
		a.breath();
	}
	public void run()
	{
		System.out.println("我在陆地上飞快地奔跑...");
	}
}
public class CompositeTest
{
	public static void main(String[] args) 
	{
		//此时需要显式创建被组合的对象
		Animal al=new Animal();
		Bird b=new Bird(al);
		b.breath();
		b.fly();
		//此时需要显式创建被组合的对象
		Animal a2=new Animal();
		Wolf w=new Wolf(a2);
		w.breath();
		w.run();
	}
}

总之,继承要表达的是一种“是(is-a)”的关系,而组合表达的是“有(has-a)”的关系。

5.9 初始化块

初始化块与构造器类似,可以对Java对象进行初始化操作。

5.9.1 使用初始化块

语法格式:

[修饰符]  {
    //初始化块的可执行代码
    .....
}

说明:初始化块的修饰符只能是static,使用static修饰的初始化块被称为静态初始化块。初始化块只在创建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();
    }
}

初始化顺序是:先执行初始化代码块或声明实例变量时指定的初始值,再执行构造器里指定的初始值。

如:

/*
 *程序名称:InstanceInitTest.java
 *程序功能:普通初始化块、声明实例变量指定的默认值都可认为
 *是对象的初始化代码,它们的执行顺序与源代码中排列顺序相同。
 *编写日期:2019-1-17
 */
public class InstanceInitTest
{
	//先执行初始化块将a实例变量赋值为6
	{
		a=6;
	}
	//再执行将a实例变量赋值为9
	int a=9;
	public static void main(String[] args) 
	{
		//下面代码将输出9
		System.out.println(new InstanceInitTest().a);
	}
}

5.9.2 初始化块和构造器

初始化块是一段固定执行的代码,它不能接受任何参数。它是构造器的补充,总是在构造器执行之前执行。

5.9.3 静态初始化块(被static修饰)

静态初始化块也被称为类初始化块,也属于类的静态成员,同样需要遵循静态成员不能访问非静态成员的规则,因此静态初始化块不能访问非静态成员,包括不能访问实例变量和实例方法。

例如:

/*
 *程序名称:Test.java
 *程序功能:程序创建三个类:Root、Mid、Leaf,这三个类都提供了静态初始化块和普通初始化块,而且
 *Mid类里还使用this调用重载的构造器,而Leaf使用super显式调用其父类指定的构造器。
 *编写日期:2019-1-17
 */
class Root
{
	static {
		System.out.println("Root的静态初始化块");
	}
	{
		System.out.println("Root的普通初始化块");
	}
	public Root()
	{
		System.out.println("Root的无参数构造器");
	}
}
class Mid extends Root
{
	static{
		System.out.println("Mid的静态初始化块");
	}
	{
		System.out.println("Mid的普通初始化块");
	}
	public Mid()
	{
		System.out.println("Mid的无参数构造器");
	}
	public Mid(String msg)
	{
		//通过this调用同一类中重载的构造器
		this();
		System.out.println("Mid的带参数构造器,其参数值:"+msg);
	}
}
class Leaf extends Mid
{
	static{
		System.out.println("Leaf的静态初始化块");
	}
	{
		System.out.println("Leaf的普通初始化块");
	}
	public Leaf()
	{
		//通过super调用父类中有一个字符串参数的构造器
		super("疯狂Java讲义");
		System.out.println("执行Leaf的构造器");
	}
}
public class Test
{
	public static void main(String[] args) 
	{
		new Leaf();
		//第一次静态初始化块执行后,第二次不会再执行。
		new Leaf();
	}
}

静态初始化块和声明静态成员变量时所指定的初始值都是该类的初始化代码,它们的执行顺序与源程序中的排列顺序相同。

例如:

/*
 *程序名称:StaticInitTest.java
 *程序功能:静态初始化块和声明静态成员变量时指定的初始值都是该类的初始化代码,它们
 *的执行顺序与程序中的排列顺序相同。
 *编写日期:2019-1-17
 */
public class StaticInitTest
{
	//先执行静态初始化块将a静态成员变量赋值为6
	static{
		a=6;
	}
	//再将静态成员变量a赋值为9
	static int a=9;
	public static void main(String[] args) 
	{
		//下面代码将输出9
		System.out.println(StaticInitTest.a);
	}
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值