Java学习笔记
第五章——面向对象
类和对象
- Java是面向对象的程序设计语言,Java语言提供了定义类、成员变量、方法等最基本的功能。类可被认为是一种自定义的数据类型,可以使用类来定义变量,所有使用类定义的变量都是引用变量,他们将会引用到类的对象。类用于描述客观世界里某一类对象的共同特征,而对象则是类的具体存在。
定义类
- Java语言里定义类的简单语法如下:
[修饰符] class 类名
{
零个到多个构造器定义
零个到多个成员变量
零个到多个方法
}
- 在上面的语法格式中,修饰符可以是
public final abstract
,或者完全省略这三个修饰符。 - 对一个类定义来说,可以包含三种最常见的成员:构造器、成员变量和方法,三种成员都可以是零个或多个,如果三种成员都只定义0个,就是定义了一个空类,这没有太大意义。
- 类里各成员之间的定义顺序没有任何影响,各成员之间可以相互调用,但需要指出的是,被static修饰的成员不能访问没有被static修饰的成员。
- 成员变量用于定义该类或该类的实例所包含的状态数据,方法则用于定义该类或该类的实例的行为特征或者功能实现。构造器用于构造该类的实例,Java语言通过new关键字来调用构造器,从而返回该类的实例。
- 构造器是一个类创建对象的根本途径,如果一个类没有构造器,这个类通常无法创建实例。因此Java语言提供了一个功能:如果程序员没有为一个类编写构造器,则系统会为该类提供一个默认的构造器。一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器。
- 定义成员变量的语法格式如下:
[修饰符] 类型 成员变量名 [= 默认值]
- 对定义成员变量语法格式的详细说明如下:
- **修饰符:**可省略,也可以是
public protected private static final
,其中前三个最多只能出现其一,可以与后两个的组合来修饰成员变量。 - **类型:**类型可以使Java语言允许的任何数据类型,包括基本类型和现在介绍的引用类型
- 成员变量名
- **默认值:**定义成员变量还可以为其指定一个默认值。
- **修饰符:**可省略,也可以是
- 定义方法的语法格式如下
[修饰符] 方法返回值类型 方法名 (形参列表)
{
//有零条到多条可执行性语句组成的方法体
}
- 对定义方法语法格式的详细说明如下:
- **修饰符:**可省略,也可以是
public protected private static final abstract
,其中前三个最多只能出现其一,final和abstract最多能出现其一,他们可以与static组合起来修饰方法。 - **方法返回值类型:**返回值类型可以使Java语言允许的任何数据类型,如果声明了返回值类型,则方法体中必须要有一个有效的return语句,该语句返回一个变量或一个表达式,这个变量或者表达式必须与此处声明的类型相匹配。如果一个方法没有返回值,则必须使用void来声明没有返回值。
- **方法名:**方法名的命名规则与成员变量的命名规则基本相同,但由于方法用于描述该类或该类的实例的行为特征或功能实现,因此通常建议方法名以英文动词开头。
- **形参列表:**形参列表用于定义该方法可以接受的参数,形参列表由零组到多组
参数类型 形参名
组成,多组参数之间以英文逗号,
隔开,形参类型和形参名之间以英文空格隔开,一旦在定义方法时指定了形参列表,则调用该方法时必须传入对应的参数值。
- **修饰符:**可省略,也可以是
- static是一个特殊的关键字,它可以用于修饰方法、成员变量等成员。static修饰的成员表明他属于这个类本身,而不属于该类的单个实例,因为通常把static修饰的成员变量和方法也成为类变量、类方法。不使用static修饰的普通方法、成员变量这属于该类的单个实例,而不属于该类。因为通常把不使用static修饰的成员变量和方法也称为实例变量、实例方法。
- 构造器是一个特殊的方法,定义构造器的语法格式与定义方法的语法格式很想:定义构造器的语法格式如下:
[修饰符] 构造器名 (形参列表)
{
//由零条到多条可执行性语句组成的构造器执行体
}
- 对定义构造器语法格式的详细说明如下:
- 修饰符:修饰符可以省略,也可以是
public protected private
其中之一。 - 构造器名:构造器名必须和类名相同
- 形参列表:和定义方法形参列表的格式完全相同
- 修饰符:修饰符可以省略,也可以是
- 值得指出的是,构造器既不能定义返回值类型,也不能使用void声明构造器没有返回值。如果为构造器定义了返回值类型,或使用了void声明构造器没有返回值,编译时不会出错,但Java会把这个所谓的构造器当做方法来处理。
- 下面的程序将定义一个Person类
public class Person {
public String name;
public int age;
public void say(String content)
{
System.out.println(content);
}
}
- 上面的Person类代码里没有定义构造器,系统将为他提供一个默认的构造器,系统提供的构造器是没有参数的。
- 定义类后,接下来即可使用该类了,Java的类大致有如下作用
- 定义变量
- 创建对象
- 调用类方法或访问类的类变量
对象的产生和使用
- 创建对象的根本途径是构造器,通过new关键字来调用某个类的构造器即可创建这个类的实例。
Person p;
p = new Person();
- 上面的程序也可以写成如下形式:
Person p = new Person();
- 创建对象后,接下来即可使用该对象了,Java的对象大致有如下作用。
- 访问对象的实例变量
- 调用对象的方法
- static修饰的方法和成员变量,即可以通过类来调用,也可以通过实例来调用;没有使用static修饰的普通方法和成员变量,只可以通过实例来调用。下面代码中通过Person实例来调用Person的成员变量和方法
p.name = "lancibe";
p.say("lancibe programming NO.1!");
System.out.println(p.name);
- 上面代码中通过Person实例调用了say()方法,调用方法时必须为方法的形参赋值。
- 大部分时候,定义一个类是为了重复创建该类的实例,同一个类的多个实例具有相同的特征,而类则是定义了多个实例的共同特征。
Person p2 = p;
- 上面代码把p变量的值赋值给p2变量,其实还是改变了他的指向。
不管是数组还是对象,当程序访问引用变量的成员变量或方法时,实际上是访问该引用变量所引用的数组、对象和成员变量或方法。
对象的this引用
- Java提供了一个this关键字,this关键字总是指向调用该方法的对象。根据this出现的位置的不同,this作为对象的默认引用有两种情形。
- 构造器中引用该构造器正在初始化的对象。
- 在方法中引用调用该方法的对象。
- this关键字最大的作用是让类中的一个方法,访问该类里另一个方法或实例变量。假设定义了一个Dog类,这个Dog对象的run()方法需要调用它的Jump()方法,那么应该如果做,是否该定义如下的Dog类呢。
public class Dog {
public void jump()
{
System.out.println("正在jump");
}
public void run()
{
Dog d = new Dog();
d.jump();
System.out.println("正在run");
}
}
使用这种方法来定义这个Dog类,确实可以实现在run()方法中调用jump()方法。那么这种做法是否好呢?我们发现,在main方法内,需要再次创建一个Dog对象,这显然是过余的。
- 因此需要在run()方法中获得调用该方法的对象,通过this关键字就可以满足条件。
public class Dog {
public void jump()
{
System.out.println("正在jump");
}
public void run()
{
this.jump();
System.out.println("正在run");
}
}
- 采用这种方法的Dog类更加符合实际意义。
- Java允许对象的一个成员直接调用另一个成员,可以省略this前缀。
public void run()
{
jump();
System.out.println("正在run");
}
- 这样也是正确的。
- 对于static修饰的方法而言,可以使用类来直接调用该方法,如果在static修饰的方法中使用this关键字,它就无法指向合适的对象:静态成员不能直接访问非静态成员。
- 除此之外,this引用也可以用作构造器中作为默认引用,由于构造器是直接使用new关键词来调用,所以this在构造器中表示该构造器正在初始化的对象。
public class ThisInConstructor {
public int foo;
public ThisInConstructor()
{
int foo = 0;
//下面的代码将会把该构造器正在初始化的对象的foo成员变量设为6
this.foo = 6;
}
public static void main(String[] args){
System.out.println(new ThisInConstructor().foo);
}
}
- 与普通方法类似的是,大部分时候,在构造器中访问其他成员变量和方法时都可以省略this前缀,但如果构造器中有一个成员变量同名的局部变量,又必须在构造器中访问这个被覆盖的成员变量,则必须使用this前缀,如上所示。
- 当this作为对象的默认引用使用时,程序可以像访问普通引用变量一样来访问this引用,甚至可以吧this当成普通方法的返回值:
public class ReturnThis {
public int age;
public ReturnThis grow()
{
age++;
return this;
}
public static void main(String[] args)
{
ReturnThis rt = new ReturnThis();
rt.grow().grow().grow();
System.out.println("rt的age成员变量是:"+rt.age);
}
}
- 从上面程序中可以看出,如果在某个方法中把this作为返回值,则可以多次连续调用同一个方法,从而使代码简洁,但可能造成实际意义的模糊。
方法详解
- 方法是类或对象的行为特征的抽象,方法是类或对象最重要的组成部分。但从功能上来看,方法完全类似于传统结构化程序设计里的函数。值得提出的是,Java里的方法不能独立存在,必须被定义在类里。方法在逻辑上要么属于类,要么属于对象。
方法的所属性
- 如果需要定义方法,则只能在类体内定义,不能独立定义一个方法。一旦将一个方法定义在某个类的类体内,如果这个方法使用了static修饰,则这个方法属于这个类,否则属于该类的实例。
- 因为使用static修饰的方法是属于这个类的,因此使用该类的任何对象来调用这个方法时将会得到相同的执行结果,这是由于底层仍然是使用这些实例所属的类作为调用者。
方法的参数传递机制
- Java里方法的参数传递只有一种:值传递。
public class PrimitiveTransferTest {
public static void swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
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("main方法内"+"a="+a+"b="+b);
}
}
- 在main方法中调用swap方法时,main方法还未结束。因此,系统分别为他们两个方法分类两块栈区,用于保存他们的局部变量。main方法的a、b变量作为值传入swap方法,实际上是在swap方法栈区中重新产生了两个局部变量a、b,并将main方法栈区中的a、b变量的值分别赋给swap方法a、b参数。
class DataWrap
{
int a;
int b;
}
public class ReferenceTransferTest {
public static void swap(DataWrap dw)
{
int temp = dw.a;
dw.a = dw.b;
dw.b = temp;
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("main方法内"+"a="+dw.a+",b="+dw.b);
}
}
- 执行上面程序,看到如下结果
swap方法内a=9,b=6
main方法内a=9,b=6
- 从上面运行结果来看,在swap方法内,a、b两个成员变量的值被交换成功。但其实,main方法和swap方法都有栈内存,保存了引用该对象的引用变量,而堆内存中保存了对象本身。两个方法中的dw变量引用的是同一个对象。也就是说,dw指向堆内存中的空间地址没有改变,但该地址包含的数据被改变了。
形参个数可变的方法
- Java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的形参。如果在定义方法是,在最后一个形参的类型后加上三点
...
,则表明该形参可以接受多个参数,多个参数值被当成数组传入。
public class Varargs {
public static void test(int a, String... books)
{
for (String book:books)
{
System.out.println(book);
}
System.out.println(a);
}
public static void main(String[] args)
{
test(5, "lancibe","123");
}
}
- 从上面运行结果可以看出,当调用test方法时,books参数可以传入多个字符串作为参数值。当然也可以直接使用数组形参来定义方法
public static void test(int a, String[] boos);
- 这两种形式都包含了一个名为books的形参,在两个方法的方法体内都可以把books当成数组处理。但是,如果使用数组形参来声明方法,调用时则必须传给该形参一个数组
test(23, new String[]{
"lancibe", "123", "linux"};
- 对比两种方法,明显第一种形式更加简洁。实际上即使采用了形参个数可变的形式来定义方法,调用该方法时也一样可以为个数可变的形参传入一个数组。
- 最后还要指出的是,数组形式的形参可以处于形参列表的任意位置,但个数可变的形参只能处于形参列表的最后。也就是说,一个方法中最多只能有一个个数可变的形参。
递归
- 一个方法调用方法本省,就叫做递归
public class Recursive {
public static int func(int n)
{
if(n==0)
{
return 1;
}
else if(n==1)
{
return 4;
}
else
{
return 2*func(n-1) + func(n-2);
}
}
public static void main(String[] args)
{
System.out.println(func(10));
}
}
- 使用递归时,只需要注意递归的方向:递归一定要朝已知方向递归。
方法重载overload
- Java允许同一个类里定义多个同名方法,只要形参列表不同就行。如果同一个类中包含了两个或多个以上方法的方法名相同,但形参列表不同,则被称为方法重载。
- 方法重载的要求是两同一不同,同一个类中方法名相同,参数列表不同。至于方法的其他部分,如方法返回值类型、修饰符等,与方法重载没有关系。
public class Overload {
public void test()
{
System.out.println("test方法");
}
public void test(String msg)
{
System.out.println(msg);
}
public static void main(String[] args)
{
Overload ol = new Overload();
ol.test();
ol.test("lancibe");
}
}
- 编译运行上面的程序完全正常,虽然两个test方法的方法名相同,但因为他们的形参列表不同,所以系统可以正常区分出这两个方法。
- 如果被重载的方法里包含了个数可变的形参,则需要注意。
public class OverloadVarargs {
public void test(String msg)
{
System.out.println("方法一");
}
public void test(String ... books)
{
System.out.println("方法二");
}
public static void main(String[] args)
{
OverloadVarargs olv = new OverloadVarargs();
//下面两次调用将执行第二个test方法
olv.test();
olv.test("aa","bb");
//下面调用将执行第一个test方法
olv.test("aa");
//下面调用将执行第二个test方法
olv.test(new String[]{
"aa"});
}
}
- 可以看到,如果想只传入一个参数,还调用第二个方法,只能使用字符串数组的形式传入参数。
成员变量与局部变量
- 在Java语言中,根据定义变量位置的不同,可以将变量分为两大类:成员变量和局部变量。他们的运行机制存在较大差异。
- 成员变量指的是在类里定义的变量,也就是前面所介绍的field;局部变量指的是在方法里定义的变量。不管是成员变量还是局部变量,都应该遵守相同的命名规则。
- 成员变量通过有没有被static修饰被分为类变量和实例变量两种。其中类变量从该类的准备阶段开始存在,直到系统完全销毁这个类,类变量的作用域与这个类的生存范围相同;而实例变量则从该类的实例被创建起开始存在,直到系统完全销毁这个实例,实例变量的作用域与对应实例的生存范围相同。
class Person
{
public String names;
public static int eyeNum;
Person(int i){
};
}
public class PersonTest {
public static void main(String[] args)
{
//第一次主动使用Person类,该类自动进行初始化,则eyeNum变量开始起作用,输出0
System.out.println("Person的eyeNum类变量"+Person.eyeNum);
//创建Person对象
Person p = new Person(1);
//通过Person对象的引用p来访问Person对象的names实例变量,并通过实例访问eyeNum类变量
System.out.println("p变量的names变量值是:"+p.names +"p对象的eyeNum变量值是:"+p.eyeNum);
p.names = "lancibe";
p.eyeNum = 2;
//再次通过Person对象来访问name实例变量和eyeNum类变量
System.out.println("p变量的names变量值是:"+p.names +"p对象的eyeNum变量值是:"+p.eyeNum);
//前面通过p修改了Person的eyeNum,此处的Person.eyeNum将输出2
System.out.println("Person的eyeNum类变量值:"+Person.eyeNum);
Person p2 = new Person(2);
//p2访问的eyeNum类变量依然引用Person类的,因此依然输出2
System.out.println("p2对象的eyeNum类变量值:"+ p2.eyeNum);
}
}
- 从上面的程序运行结果来看,类变量的作用域比实例变量的作用域更大:实例变量随实例的存在而存在,而类变量则随类的存在而存在。实例也可以访问类变量,同一个类的所有实例访问类变量的时候实际上访问的是该类本身的同一个变量,也就是说,访问了同一片内存区。
- 局部变量的定义与C语言类似,当局部变量覆盖成员变量时,依然可以在方法中显式指定类名和this作为调用者来访问被覆盖的成员变量,这使得编程更加自由。
- 还应注意:当程序需要访问类变量时,尽量使用类作为主调,而不要使用对象作为主调,这样可以避免程序中产生歧义,提高程序可读性。
隐藏和封装
理解封装
- 封装(Encapsulation)是面向对象的三大特征之一(另外两个是继承和多态),他指的是将对象状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
- 封装是面向对象程序语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以实现以下目的:
- 隐藏类的实现细节
- 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问
- 可进行数据检查,从而有利于保证对象信息的完整性
- 便与修改,提高代码的可维护性
- 为实现良好的封装,需要从两个方面进行考虑
- 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问
- 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作
- 因此,封装实际上有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。这两个方面都需要通过使用Java所提供的访问控制符来实现。
使用访问控制符
- Java提供了3个访问控制符:
private protected public
,另外还有一个不加任何访问控制符的访问级别,也就是提供了4个访问级别。 - private:当前类访问权限,只能在当前类的内部被访问。
- default:包访问权限,default访问控制的成员或外部类可以被相同包下的其他类访问。
- protected:子类访问权限,既可以被同一个包中的其他类访问,也可以被不同包中的子类访问。在通常情况下,如果使用protected来修饰一个方法,通常是希望其子类来重写这个方法。
- public:公共访问权限,可以被所有类访问,不管是不是在同一个包中或是否具有父子继承关系。
private | default | protected | public | |
---|---|---|---|---|
同一个类中 | permitted | permitted | permitted | permitted |
同一个包中 | permitted | permitted | permitted | |
子类中 | permitted | permitted | ||
全局范围内 | permitted |
如果一个Java源文件内定义的所有类都没有使用public修饰,则这个Java源文件的文件名可以是一切合法的文件名;但如果一个Java源文件里定义了一个public修饰的类,则这个源文件的文件名必须与public修饰的类的类名相同。
public class PersonTest2 {
private String name;
private int age;
public void setName(String name)
{
if(name.length() > 6 || name.length() < 2)
{
System.out.println("不合要求");
return;
}
else
{
this.name = name;
}
}
public String getName()
{
return this.name;
}
public void setAge(int age)
{
if(age > 100 || age < 0)
{
System.out.println("不合要求");
return;
}
else
{
this.age = age;
}
}
public int getAge()
{
return this.age;
}
}
- 正如这样的程序,main方法不可直接修改Person对象的name和age两个实例变量,只能通过各自对应的setter方法来操作它们的值。因为使用setter方法来操作这两个实例变量,就允许程序员在setter方法中增加自己的控制逻辑,从而保证Person对象的两个实例变量不会出现与事实不符的情形。
- 关于访问控制符的使用,存在如下几条基本规则
- 类里的绝大部分成员都应该使用private修饰,只有一些用static修饰的、类似全局变量的成员变量,才可能考虑public修饰。除此之外,有些方法只用于辅助实现该类的其他方法,这些方法被称为工具方法,工具方法也应该使用private来修饰。
- 如果某个类主要用作其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不像被外界直接调用,应该使用protected。
- 希望暴露出来给其他类自由调用的方法应该使用public修饰。因此,类的构造器通过使用public修饰,从而允许在其他地方创建该类的实例。因为外部类通常都希望被其他类自由使用,所以大部分外部类都应该使用public修饰。
package、import和import static
- Java允许将一组功能相关的类放在同一个package下,从而组成逻辑上的类库单元。如果希望把一个类放在指定的包结构下,应该在Java源程序的第一个非注释行放置如下格式的代码
package packageName;
- 一旦在Java源文件中使用了这个package语句,就意味着该源文件里定义的所有类都属于这个包,位于包中的每个类的完整类名都应该是包名和类名的组合,如果其他人需要使用该包下的类,也应该使用包名加类名的组合
package lan;
public class Hello {
public static void main(String[] args)
{
System.out.println("hello, lancibe!");
}
}
- 使用如下命令来编译这个Java文件
javac -d . Hello.java
- 发现生成了文件夹lan,而Hello.class在lan内。
- 运行命令:
java lan.Hello
- 发现上面程序正常输出
- 如果进入lan路径再使用
java Hello
命令,系统将提示错误。 - 如果需要使用不同包中的其他类时,总是需要该类的全名
lan.sub.Apple a = new lan.sub.Apple;
- 为了简化编程,Java引出了import关键字,import可以向某个Java文件中导入指定包层次下的某一个类或全部类,import语句应该出现在package语句(如果有的话)之后、类定义之前。一个Java源文件只能包含一个package语句,但可以包含多个import语句,多个import语句用于导入多个包层次下的类。
- 使用import语句导入单个类的用法如下:
import package.subpackage...ClassName;
- 例如
import lan.sub.Apple;
- 使用import语句导入指定包下全部类的用法如下:
import package.subpackage...*;
- 上面语句中的
*
只能代表类,不能代表包。 - 一旦在Java源文件中使用import语句来导入指定类,在该源文件中使用这些类时就可以省略包前缀,不需要再使用类全名。
静态导入
import static package.subpackage...ClassName.fieldName|methodName;
- 专用来导入静态成员变量或方法名
- 所谓静态成员变量、静态方法其实就是前面介绍的类变量、类方法。一句话归纳import和import static的作用:使用import可以省略写包名,而使用import static甚至可以省略类名。
Java常用包
- java.lang:包含了Java语言的核心类,如String、Math、System和Thread等,使用这个包下的类系统自动导入
- java.util:包含了大量工具类、接口和集合框架类、接口,例如Arrays和List、Set等。
- java.net:包含了一些Java网络编程相关的类、接口
- java.io:包含了输入输出编程相关的接口
- java.text:包含了Java格式化相关的类
- java.sql:包含了Java进行JDBC数据库编程的相关类、接口
- java.awt:包含了抽象窗口工具集的相关类、接口,主要用于构件图形用户界面(GUI)程序。
- java.swing:这个包下包含了Swing图形用户界面编程的相关类、接口,可用于构建平台无关的GUI程序。
深入构造器
- 构造器是一个特殊的方法,这个特殊方法用于创建实例时执行初始化。构造器是创建对象的重要途径(即使使用工厂模式、反射等方式创建对象,其实质依然是依赖于构造器),因此,Java类必须包含一个或一个以上的构造器。
使用构造器执行初始化
- 前面讲过,系统会对对象的实例变量进行默认初始化,这种默认初始化把所有基本类型的实例变量设为0。
- 如果想改变这种默认的初始化,想让系统创建对象时就为该对象的实例变量显式指定初始值,就可以通过构造器来实现。
- 下面的类提供了一个自定义的构造器,通过这个构造器就可以让程序员进行自定义的初始化操作。
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("lancibe", 11);
System.out.println(tc.name);
System.out.println(tc.count);
}
}
- 运行上面程序,发现name实例变量不再是null,count实例变量也不是0,这就是提供自定义构造器的作用。
- 如果希望该类保留无参数的构造器,或者希望有多个初始化过程,则可以为该类提供多个构造器,如果一个类里提供了多个构造器,就形成了构造器的重载。
构造器重载
- 同一个类里有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。构造器重载允许Java类里包含多个初始化逻辑,从而允许使用不同的构造器来初始化Java对象。
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 oc1 = new ConstructorOverload();
ConstructorOverload oc2 = new ConstructorOverload("lancibe", 11);
System.out.println(oc1.name + " " + oc1.count);
System.out.println(oc2.name + " " + oc2.count);
}
}
- 上面的ConstructorOverload类提供了两个重载的构造器,两个构造器的名字相同,但形参列表不同。系统通过new调用构造器时,系统将根据传入的实参列表来决定调用哪个构造器。
- 如果两个构造器之间是完全包含的情况,如果是方法,则可以直接调用。但构造器不能被直接调用,他必须使用new关键字来调用。但一旦使用new关键字来调用构造器,将会导致重新创建一个对象。Java系统允许使用this关键字来调用相应的构造器。
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;
}
}
- 上面Apple类里包含了三个构造器,其中第三个构造器通过this来调用另一个重载构造器的初始化代码。程序中的
this(name, color);
调用表明调用该类的另一个带两个字符串参数的构造器。 - 使用this调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句。使用this调用重载的构造器时,系统会根据this后括号内的实参来调用形参列表与之对应的构造器。
类的继承
- 继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Java的继承具有单继承的特点,每个子类只有一个直接父类。
继承的特点
- Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类被称为父类,有的也称其为基类、超类。父类和子类的关系是一种一般和特殊的关系。
- 因为子类是一种特殊的父类,因此父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,子类是小类。
- Java里子类继承父类的语法格式如下
修饰符 class SubClass extends SuperClass
{
//类定义部分
}
- 从上面的语法格式来看,定义子类的语法非常简单,只需要在原来的类定义上增加
extend SuperClass
即可,即表明该子类继承了SuperClass类。
package fruit;
public class Fruit {
public double weight;
public void info()
{
System.out.println("fruit"+weight);
}
}
package fruit;
public class Apple2 extends fruit.Fruit {
public static void main(String[] args)
{
Apple2 a = new Apple2();
a.weight = 56;
a.info();
}
}
- 上面的Apple2类基本只是一个空类,但他的Apple2对象可以访问weight实例变量和info()方法,则表明Apple2对象也具有了weight实例变量和info()方法,这就是继承的作用。
- Java语言中每一个类最多只有一个直接父类,但它可以有无限多个间接父类:
class Fruit extends Plant{
...}
class Apple extends Fruit{
...}
- 如果定义一个Java类时并未显式指定这个类的直接父类则这个类默认扩展java.lang.Object类。因此,该类是所有类的父类,要么是其直接父类、要么是其间接父类。因此所有的Java对象都可以调用java.lang.Object类所定义的实例方法。
重写父类的方法
- 子类扩展了父类,子类是一种特殊的父类。大部分时候子类总是以父类为基础,额外增加新的成员变量和方法。但有一种例外:子类需要重写父类的方法。
package bird;
public class Bird {
public void fly()
{
System.out.println("我会飞");
}
}
package bird;
import bird.*;
public class Ostrich extends bird.Bird {
public void fly()
{
System.out.println("我不会飞");
}
public static void main(String[] args)
{
Ostrich os = new Ostrich();
os.fly();
}
}
- 这种子类包含与父类同名方法的现象被称为方法重写(Override),也被称为方法覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。
- 方法的重写要遵循“两同两小一大”规则,两同即方法名相同、形参列表也相同,两小指子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等。一大指的是子类方法的访问权限应比父类方法访问权限更大或相等。尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法。不能一个是类方法、一个是实例方法。
- 如果父类方法具有private访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中定义了一个新方法。
class BaseClass
{
//test()方法是private访问权限,子类不可访问该方法
}
class SubClass extends BaseClass
{
//此处并不是方法重写,所以可以增加static关键字
public static void test() {
...}
}
- 父类方法和子类方法之间也可能发生重载,因为子类会获得父类方法,如果子类定义了一个与父类方法有相同的方法名,但参数列表不同的方法,就会形成父类方法和子类方法的重载。
super限定
- 如果需要在子类方法中调用父类被覆盖的实例方法,则可以使用super限定来调用父类被覆盖的实例方法。为上面的Ostrich类添加一个方法,在这个方法中调用bird类中被覆盖的fly方法
public void callOverridedMethod()
{
//在子类方法中通过super显式调用父类被覆盖的实例方法
super.fly();
}
- 如果子类里定义了和父类同名的实例变量,则会发生子类实例变量隐藏父类实例变量的情形。在正常情况下,子类里定义的方法直接访问该实例变量默认会访问到子类中定义的实例变量,无法访问到父类中被隐藏的实例变量。在子类定义的实例方法中可以通过super关键字来访问父类中被隐藏的实例变量
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();
sc.accessBase();
}
}
- 当系统创建了SubClass对象时,实际上会为SubClass对象分配两块内存,一块用于储存在SubClass中定义的a实例变量,一块用于储存从BaseClass类继承得到的a实例变量
- 如果在一个方法中访问名为a的成员变量,但没有显式指定调用者,则系统查找a的顺序为:
- 查找该方法中是否有名为a的局部变量
- 查找当前类中是否包含名为a的成员变量
- 查找a的直接父类中是否包含名为a的成员变量,并依次上溯a的所有父类,直到java.lang.Object类,如果最终找不到名为a的成员变量,则系统出现编译错误
- 如果被覆盖的是类变量,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量。
- 因为子类中定义与父类中同名的实例变量并不会完全覆盖父类中定义的实例变量,他只是简单地隐藏了父类中的实例变量,所以会出现如下特殊的情形
package chapter5;
class Parent
{
public String tag = "123";
}
class Derived extends Parent
{
//定义一个私有的tag实例变量来隐藏父类的tag实例变量
private String tag = "456";
}
public class HideTest {
public static void main(String[] args)
{
Derived d = new Derived();
//程序不可访问d的私有变量tag,所以下面语句将引起编译错误
//System.out.println(d.tag);
//将d变量显式的向上转型为Parent后,即可访问tag实例变量
//程序将输出:123
System.out.println(((Parent)d).tag);
}
}
- super还可以用来调用父类构造器,这与this很像,但由于他们都要出现在构造器执行体的第一行,所以不能同时出现。
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(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();
}
}
- 运行结果如下:
Creature无参数构造器
Animal带一个参数的构造器,该动物的name为灰太狼
Animal带两个参数的构造器,该动物的age为3
Wolf无参数的构造器
- 从上面的运行结果来看,创建任何对象总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。如果某个父类通过this调用了同类中重载的构造器,就会依次执行此父类的多个构造器。
多态
- Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态(Polymorphism)。
多态性
- 当把一个子类对象直接赋给父类引用变量时,例如
BaseClass ploymophicBc = new SubClass();
这个ploymophicBc引用变量的编译时类型是BaseClass,而运行时类型是SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。
引用变量的强制类型转换
- 编写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 b = (boolean)in;
Object obj = "hello";
String objStr = (String)obj;
System.out.println(objStr);
Object objPri = Integer.valueOf(5);
//可以强制类型转换,而objPri变量的实际类型是Integer
//所以下面代码运行时会引发ClassCastException异常
String str = (String)objPri;
}
}
- 考虑到进行强制类型转换时可能出现异常,因此进行类型转换之前应该先通过instanceof来判断是够可以成功转换。例如,上面的
String str = (String)objPri;
代码运行时会引发ClassCastException异常,这是因为objPri不可转换成String类型。为了让程序更加健壮,可以将代码改为如下格式:
if (objPri instanceof String)
{
String str = (String)objPri;
}
instanceof运算符
- instanceof运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是一个接口,可以把接口理解成一种特殊的类),它用于判断前面的对象是否是后面的类,或其子类、实现类的实例。如果是,返回true,否则返回false。
- 要注意,使用instanceof运算符时,前面的操作数要么与后面的类相同,要么与后面的类具有父子继承关系,否则编译错误。
public class InstanceofTest {
public static void main(String[] args)
{
Object hello = "Hello";
System.out.println("字符串是否是Object类的实例:"+(hello instanceof Object));
System.out.println("字符串是否是String类的实例:"+(hello instanceof String));
System.out.println("字符串是否是Math类的实例:"+(hello instanceof Math));
System.out.println("字符串是否是Comparable接口的实例"+(hello instanceof Comparable));
String a = "Hello";
//String类与Math类没有继承关系,所以下面代码无法通过编译
//System.out.println("字符串是否是Math类的实例"+(a instanceof Math));
}
}
- instanceof和(type)是Java提供的两个相关的运算符,通常先用instanceof判断一个对象是否可以强制类型转换,然后再使用(type)运算符进行强制类型转换,从而保证程序不会出现错误。
继承和组合
- 继承是实现类复用的重要手段,但继承带来了一个最大的坏处,破坏封装。相比之下,组合也是实现类复用的重要方式,而采用组合方式来实现类复用则能提供更好的封装性。
使用继承的注意点
- 子类扩展父类时,子类可以从父类继承得到成员变量和方法,如果访问权限允许,子类可以直接访问父类的成员变量和方法,相当于子类可以直接复用父类的成员变量和方法。但是继承严重的破坏了父类的封装性。
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();
}
}
- 当系统视图创建Sub对象时,同样会先执行其父类的构造器,如果父类的构造器调用了被其子类重写的方法,则变成了调用被重写后的方法。当创建Sub对象时,会先执行Base类中的Base构造器,Base构造器调用了子类中的test()方法,此时对象的name实例变量是null,因此将引发空指针异常。
- 如果把某些类设置成最终类,既不能被当成父类,则可以使用final修饰这个类,除此之外,还可以使用private修饰这个类的所有构造器,即子类无法调用、继承该类的构造器,也就无法继承该类。
- 使用子类的情况如下:
- 子类需要额外增加属性,而不仅仅是属性值的改变。例如从Person类派生出Student子类,Student类需要grade属性,而Person类并没有提供,这就符合Java继承的前提
- 子类需要增加自己独有的行为方式,包括增加新的方法或重写父类的方法,例如从Person类派生出Teacher类,其中Teacher类包含了一个新的teaching()方法,该方法用于描述Teacher对象的独有行为方式。
利用组合实现复用
- 对于继承而言,子类可以直接获得父类的public方法,程序使用子类时,将可以直接访问该子类从父类那里继承到的方法;而组合则是把旧类对象作为新类的成员变量组合进来,用以实现新类的功能,用户看到的还是新类的方法而不能看到被组合对象的方法。因此,通常需要在新类里使用private来修饰被组合的旧类对象。
class Animals
{
private void beat()
{
System.out.println("心脏跳动");
}
public void breath()
{
beat();
System.out.println("呼吸中");
}
}
class Birds extends Animals
{
public void fly()
{
System.out.println("自由地飞翔");
}
}
class Wolfs extends Animals
{
public void run()
{
System.out.println("快速地奔跑");
}
}
public class InheritTest {
public static void main(String[] args)
{
Birds b = new Birds();
b.breath();
b.fly();
Wolfs w = new Wolfs();
w.breath();
w.run();
}
}
- 如上所示,通过让Birds和Wolfs继承Animals,从而允许他们获得Animals的方法,从而复用了Animals提供的breath()方法。通过这种方式,相当于让Wolfs类和Birds类同时拥有其父类Animals的breath()方法,从而让Wolfs对象和Birds对象都可以直接复用Animals里定义的breath()方法。
- 如果仅从软件复用的角度来看,将上面三个类的定义改为如下形式也可以实现相同的复用。
package chapter5.CompositeTest;
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;
}
public void breath()
{
a.breath();
}
public void fly()
{
System.out.println("自由地飞翔");
}
}
class Wolf
{
private Animal a;
public Wolf(Animal a)
{
this.a = a;
}
public void breath()
{
a.breath();
}
public void run()
{
System.out.println("快速地奔跑");
}
}
public class CompositeTest {
public static void main(String[] args)
{
// 此时需要显式创建被组合的对象
Animal a1 = new Animal();
Bird b = new Bird(a1);
b.breath();
b.fly();
Animal a2 = new Animal();
Wolf w = new Wolf(a2);
w.breath();
w.run();
}
}
- 与之前的程序效果完全一致。
- 大部分时候,继承关系中从多个子类里抽象出共有父类的过程,类似于组合关系中从多个整体里提取被组合类的过程;继承关系中从父类派生子类的过程,则类似于组合关系中把被组合类组合到整体类的过程。
初始化块
- Java使用构造器来对单个对象进行初始化操作,使用构造器先完成整个Java对象的状态初始化,然后将Java对象返回给程序,从而让该Java对象的信息更加完整。与构造器作用非常类似的是初始化块,他也可以对Java对象进行初始化操作。
使用初始化块
- 初始化块是Java类里可以出现的第四种成员,一个类里可以有多个初始化块,相同类型的初始化块之间有顺序:前面定义的初始化块先执行,后面定义的初始化块后执行。初始化块的语法格式如下:
[修饰符] {
//初始化块的可执行代码
...
}
- 初始化块的修饰符只能是static,或不加。
package chapter5.Person;
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();
}
}
- 上面程序的main方法只创建了一个Person对象,程序输出如下:
Person初始化块:局部变量a的值大于4
Person的初始化块
Person的第二个初始化块
Person类的无参数构造器
- 从运行结果来看,创建Java对象时,系统总是先调用该类里定义的初始化块。
- 从上面代码可以看出,初始化块和构造器的作用非常相似,他们都用于对Java对象执行指定的初始化操作,但他们之间仍存在一些差异:
- 普通初始化块、声明实例变量指定的默认值都可以认为是对象的初始化代码,他们的执行顺序与源程序中的排列顺序相同。
public class InstanceIntiTest {
//先执行初始化块将a实例变量赋值为6
{
a=6;
}
//再执行将a的实例变量赋值为9
int a =9;
public static void main(String[] args)
{
//下面代码将输出9
System.out.println(new InstanceIntiTest().a);
}
}
- 上面程序定义了两次对a实例变量赋值,执行结果是a实例变量的值为9,这表明
int a = 9;
这一行代码比初始化块后执行。
初始化块和构造器
- 与构造器不同的是,初始化块是一种固定执行的代码,他不能接受任何参数。因此初始化块对同一个类的所有对象进行的初始化处理完全相同。基于这个原因,不难发现初始化块的基本用法。但其实,在使用javac编译后,该java类中的初始化块会消失,被“还原”到每个构造器中,且位于构造器所有代码的前面。
静态初始化块
- 即使用了static修饰符的初始化块,也被称为类初始化块,负责对类进行初始化。
- 与普通初始化块类似的是,系统在类初始化阶段执行静态初始化块时,不仅会执行本类的静态初始化块,而且还会一直上溯到java.lang.Object类(如果它包含静态初始化块),先执行java.lang.Object的静态初始化块,再执行其子类的……最后才执行该类的静态初始化块。只有当类初始化完成后,才可以在系统中使用这个类,包括访问类方法、类变量或用这个类创建实例。
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("lancibe");
System.out.println("执行Leaf的构造器");
}
}
public class Test {
public static void main(String[] args)
{
new Leaf();
new Leaf();
}
}
- 程序运行结果如下
Root静态初始化块
Mid静态初始化块
Leaf静态初始化块
Root普通初始化块
Root无参数构造器
Mid普通初始化块
Mid无参数构造器
Mid带参数构造器,其参数值lancibe
Leaf普通初始化块
执行Leaf的构造器
Root普通初始化块
Root无参数构造器
Mid普通初始化块
Mid无参数构造器
Mid带参数构造器,其参数值lancibe
Leaf普通初始化块
执行Leaf的构造器