java面向对象的核心——类和对象

要点:

  • 定义类、成员变量和方法

  • 创建并使用对象

  • 对象和引用

  • 方法必须属于类或对象

  • Java方法的参数传递机制

  • 递归方法

  • 方法的重载

  • 实现良好的封装

  • 使用package和import

  • 构造器的作用 和构造器重载

  • 继承的特点和用法

  • 重写父类方法

  • super 关键字的用法

  • 继承和多态

  • 向上转型和强制类型转换

  • 继承和组合的关系

  • 使用组合来实现复用

  • 构造器和初始化块的作用及区别

  • 静态初始化块

面向对象和面向过程

我们都知道Java是面向对象的语言,那么什么是面向对象?什么是面向过程呢?首先通过下面这个例子简单了解一下面向对象和面向过程吧。

面向过程:关注点->过程(步骤)
想吃面包 自己做
买面->和面->模型 - > 烤熟->吃面包

面向对象:关注点->对象
想吃面包 面包师 让别人做
买面->和面->模型 - > 烤熟->吃面包

面向对象中有两个重要的概念:类和对象(也称实例),类是某一批对象的抽象,对象才是一个具体存在的实体。它们是面向对象的核心。

类和对象

所有类是引用类型。

定义类

  1. 类:对具有共同属性和行为特征的一类事物的抽象。
  2. 对象:通过类创建的实例。类是创建对象的模板。
  3. 类分为工具类,测试类(成员入口类)

Java 语言里定义类的简单语法如下:

[修饰符]   class 类名
{
     零个到多个构造器定义. . . 
     类成员的定义:成员变量,成员方法,块{} 
}

在上面的语法格式中,修饰符可以是 public、 final、 abstract,或者完全省略这三个修饰符。

定义构造器的语法 格式如下:

[修饰符]构造器名(形参列表)
{
   // 由零条到多条可执行性语句组成的构造器执行体
}

构造器名:构造器名必须和类名相同。
值得指出的是,构造器既不能定义返回值类型,也不能使用 void 声明构造器没有返回值

构造器不是没有返回值吗?为什么不能用void声明?

实际上,构造器是有返回值的,当使用new关键字调用构造器时,构造器返回该类的实例,可以把这个类的实例当成构造器的返回值,因此构造器的返回值类型总是当前类,无需定义返回值类型。但必须注意:不要在构造器里显示用return来返回当前类的对象,因为构造器的返回值是隐式的。

构造器用于构造该类的实例, Java 通过 new 关键字来调用构造器,从而返回该类的实例(即对象)。如果一个类没有构造器,这个类通常无法创建实例。因此, Java 语言提供了一个功能: 如果程序员没有为一个类编写构造器,则系统会为该类提供一个默认的构造器,系统提供的构造器总是没有参数的 ,且一定是public 修饰的。一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器。
构造方法的个数:有变量时,多个,根据变量类型的个数确定,具体看下面代码:

    int x,y;
	//构造方法
	public   Demo() {
         System.out.println("a");
	}
	public   Demo(int x,int y) {
         System.out.println("a");
	}
	/*这两种写法只能有一种存在,否则报错,因为是相同数据类型
	public   Demo(int y) {
         System.out.println("a");
	}
	public   Demo(int x) {
         System.out.println("a");
	}*/
class **abstract** A{
 class B{
  //并不是这个方法创建对象
  //因为底层实际上是return new A;
  public A getA(){
  //return new A;
  /**/return new A(){};   匿名类,抽象类的子类的对象,抽象类不能创建对象**

多个构造方法一定重载的。
普通方法尽量不要和类名同,若相同也不会报错:

    //构造方法
	public   Demo() {
		// TODO Auto-generated method stub
         System.out.println("a");
	}
	//普通方法
	public  void  Demo() {
		// TODO Auto-generated method stub
         System.out.println("a");
	}

在进行方法调用时,上面两方法都执行以下程序,可发现,构造方法不能使用对象打点调用

Demo demo=new Demo();
demo.Demo();

定义成员变量的语法格式如下:

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

对定义成员变量语法格式的详细说明如下:
修饰符:修饰符可以省略,也可以是 public、 protected、 private、 static、final。

变量的分类

  • 定义的位置:
    成员变量:定义到类体中
    局部变量:方法内或者参数列表
  • 访问范围:
    成员变量:整个类内
    局部变量:当前方法

注意:局部变量在使用之前必须赋值,但是如果是方法参数列表中的局部变量,则不需赋值,详细看下面代码:

    void f(int x) {
		int y;
		//可以输出x,因为在调用这个方法时就会给它赋值
		System.out.println(x);
		//不能打印y,因为y时局部变量,必须先赋值
		//所以这句报错
		//System.out.println(y);
	}

this关键字

  • this:表示当前类对象

  • 访问类成员:this.成员

  • 调用本类的构造方法:this(实参);

  • this()必须是构造方法的第一条有效语句

//构造方法
public   Demo() {
	// TODO Auto-generated method stub
     System.out.println("demo");
}
public   Demo(int a,int b) {
	// TODO Auto-generated method stub
	this();
	//this(3);
	x=a;
	y=b;
	System.out.println("demo!!!!!!");
}
Demo demo=new Demo(1,2);

结果:

demo
demo!!!!!!

从上面代码看,创建对象时,一定会调用父类的无参构造方法。

定义方法的语法格式如下:

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

}

对定义方法语法格式的详细说明如下。

  • 修饰符:修饰符可以省略,也可以是public、 protected、 private、 static、 final、abstract。
  • 方法返回值类型:如果声明了方法返回值类型,则方法体内必须有一个有效的 return 语句。 如果一个方法没有返回值,则必须使用 void
    来声明没有返回值(void里可以有return;)。
  • 形参列表:多组参数之间以英文逗号 (,)隔开。 一旦在定义方法时指定了形参列表,则调用该方法时必须传入对应的参数值——谁调用方法,
    谁负责为形参赋值。

方法的返回值类型<=方法返回类型

   //ok!!
   public static double sum(){
       return 1+2;
   }

static 关键字,它可用于修饰方法、成员变量等成员 。 static修饰的成员表明它属于这个类本身,而不属于该类的单个实例,因为通常把 static 修饰的成员变量和方法也称为类变量、类方法。 不使用static 修饰的普通方法、成员变量则属于该类的单个实例,而不属于该类。 因为通常把不使用 static修饰的成员变量和方法也称为实例变量、实例方法。
由于 static 的英文直译就是静态的意思,因此有时也把 static 修饰的成员变量和方法称为静态变量 和静态方法,把不使用 static 修饰的成员变量和方法称为非静态变量和非静态方法。 静态成员不能直接访问非静态成员 。

  • 构造块: 在构造方法之前执行,初始化成员变量
  • 局部块:执行完毕,立即回收,可以提高内存的利用率
//构造快,在构造方法之前执行
	{
		//int x=2; ok
		x=2;
		System.out.println("x=2");
	}
	
	//构造方法
	public   Demo() {
		// TODO Auto-generated method stub
         System.out.println("demo");
	}
    public  void test() {
		//局部块执行完,立即回收,可以提高内存利用率
		{
			int x=9;
		}
		System.out.println("test");
	}
 Demo demo=new Demo();
    demo.test();
x=2
demo
test

注意:块变量出了块就失效了

定义类之后,接下来就可以使用该类了。

对象的产生和使用

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

Person p = new Person() ;

创建对象之后,接下来即可使用该对象了, Java 的对象大致有如下作用 。

  1. 访问对象的实例变量。
  2. 调用对象的方法。

类或实例访问方法或成员变量的语法是:类.类变量|方法,或者实例.实例变量|方法.

没有使用 static 修饰的方法和成员变量,只可通过实例来调用 。static 修饰的方法和成员变量,既可通过类来调用,也可通过实例来调用;下面代码中通过 Person 实例来调用 Person 的成员变量和方法。

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

对象、引用和指针

下面程序将定义一个 Person 类。

public class Person
{ 
//下面定义了两个成员变量 
public String name; 
public int age; 
//下面定义了一个 say 方法
public void say(Str工ng content) 
System.out.println(content) ; 
}
}
//定义 p 变量的同时并为 p 变量赋值 
Person p = new Person() ; 

图 5.2 显示了将 Person 对象赋给一个引用变量的示意图。
在这里插入图片描述

当一个对象被创建成功以后,这个对象将保存在堆内存中, Java 程序不允许直接访问堆内存中的对象, 只能通过该对象的引用操作该对象。
如图 5.2 所示,p 引用变量本身只存储了一个地址值,并未包含任何实际数据。
堆内存里的对象可以有多个引用 ,即多个引用变量指向同 一个对象 代码如下)

// 变量的值赋值给 p2 变量
Person p2 = p ;

上面代码把p变量的值赋值给p2变量,就是将p变量保存的地址值赋给 p2变量,这样p2变量和p变量将指向堆内存里的同一个 Person 对象。

对象的初始化过程(内存)

创建对象时,成员变量会被初始化。

对象this的引用

this 关键宇最大的作用就是让类中一个方法,访问该类里的另一个方法或实例变量。假设定义了一个Dog 类,这个 Dog 对象的 run 方法需要调用它的 jump 方法,那么应该如何做?

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

因此需要在 run方法中获得调用该方法的对象,通过 this 关键字就可以满足这个要求。
Java 允许对象的一个成员直接调用另一个成员 可以省略 this 前缀,也就是说,将上面 run 方法改为如下形式也完全正确。

public vo run (){
     jump();
    System.out pr ntln( 正在 run 方法 ") ; }

static 修饰的方法中不能使用 this 引用。 所以 static 修饰的方法不能访问不使用 static 修饰的普通成员,因此 Java语法规定:静态成员不能直接访问非静态成员。

如果构造器中有一个与成员变量同名的局部变量,又必须在构造器中访问这个被覆盖的成员变量,则必须使用 this 前缀。如下面的代码所示。

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

方法详解

方法的所属性

Java 言里方法的所属主要体现在如下几个方面:
》方法不能独立定义,方法只能在类体里定义
〉方法要么属于该类本身,要么属于该类的一个对象
》永远不能独立执行方法,执行方法必须使用类或对象作为调用者

方法的参数传递机制

Java 里方法的参数传递方式只有一种: 值传递。 所谓值传递,就是将实际参数值的副本(复制品)传入方法内 , 而参数本身不会受到任何影响

提示:Java 里的参数传递类似于《西游记》里的孙悟空,孙悟空复制了一个假孙悟空,这个假孙悟空具有和孙悟空相同的能力,可除妖或被砍头。但不管这个假孙悟空遇到什么事,真孙悟空不会受到任何影响。与此类似,传入方法的是实际参数值的复制品,不管方法中对这个复制品如何操作,实际参数值本身不会到任何影响。

下面程序演示了方法参数传递的效果。

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);
	}
}

运行上面程序,看到如下运行结果:

swap 方法里,a 的值是 9; b的值是 6
交换结束后,变量 a的值是 6; 变量 b的值是 9

从这个运行结果可以看出, main() 方法里的变量 a和b ,并不是 swap() 方法里的 a和b。正如前面讲,swap() 方法的 a和b 只是 main() 方法里变量 a和b的复制品。下面通过示意图来说明上面程序的执行过程。Java 程序总是从 main() 方法开始执行, main() 方法开始定义了a、b两个局部变量,两个变量在内存中的存储示意图如图 5.3 所示。
当程序执行 swap() 方法时, main()方法中的a、b 变量作为参数值传入swap() 方法,传入 swap() 方法的只是 a、b的副本,而不是 a、b本身,进入 swap()方法后系统中产生了4个变量,这4个变量在内存中的存储示意图如图5.4所示。
在这里插入图片描述
在main() 方法中调用 swap() 方法时,main() 方法还未结束 。因此,系统分别为 main()方法和 swap()方法分配两块栈区,用于保存 main() 方法和 swap()方法的局部变量。main()中的a、b变量作为参数值传入 swap() 方法,实际上是在 swap() 方法栈区中重新产生了两个变量,并将 main() 方法栈区中a、b变量的值分别赋给 swap() 方法栈区中的a、b 参数。此时,系统存在两个a变量、两个b变量,只是存在于不同的方法栈区中而己。
程序在 swap()方法中交换a、b 两个变量的值,实际上是对图5 .4中灰色覆盖区域的 a、b变量进行交换,交换结束后 swap()方法中输出 a、b变量的值,看到 a的值为9,b 的值为6 ,此时内存中的存储示意图如图 5.5 所示。
在这里插入图片描述
对比图 5.5 与图 5.3 ,两个示意图中 main() 方法栈区中a、b 的值并未有任何改变,程序改变的只是swap()方法栈区中的 a、b。这就是值传递的实质:当系统开始执行方法时,系统为形参执行初始化,就是把实参变量的值赋给方法的形参变量,方法里操作的并不是实际的实参变量。
前面看到的是基本类型的参数传递, 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);
		// 把dw直接赋为null,让它不再指向任何有效地址。
		dw = null;
	}
	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

从上面运行结果来看,在 swap() 方法里, 两个成员变量的值被交换成功。下面还是结合示意图来说明程序的执行过程。
main()方法开始创建了 DataWrap 对象,并定义了 dw 引用变量来指向 DataWrap 对象,这是一个与基本类型不同的地方,创建一个对象时,系统内存中有两个东西:堆内存中保存了对象本身,栈内存中保存了引用该对象的引用变量。接着程序通过引用来操作 DataWrap对象,把该对象的a、b两个成员变量分别赋值为6、9。 此时系统内存中的存储示意图如图 5.6 所示
在这里插入图片描述
接下来, main()方法中开始调用 swap()(方法, main()方法并未结束,系统会分别为 main()、 swap()开辟出两个栈区,用于存放 main() 、swap()方法的局部变量。 调用 swap()方法时, dw 变量作为实参传swap()方法,同样采用值传递方式:把 main()方法里 dw 的值赋给 swap()方法里的 dw 形参,从
而完成 swap() 方法的 dw 形参的初始化。值得指出的是, main()方法中的 dw 是一个引用(也就是指针),它保存了 DataWrap 对象的地址值,当把 dw 的值赋给 swap()方法的 dw 形参后,即让 swap()的 dw 形参也保存这个地址值,即也会引用到堆内存中的 DataWrap 。图5.7 显示了 dw 传入 swap()方法后的存储示意图。

在这里插入图片描述

方法重载

方法重载 要求就是两同一不同 :同一个类中方法名相同,参数列表不同 。至于方法的其他部分,如方法返回值类型、修饰符等,与方法重载没有任何关系。
下面程序中包含了方法重载的示例。

    // 下面定义了两个test()方法,但方法的形参列表不同
	// 系统可以区分这两个方法,这种被称为方法重载
	public void test()
	{
		System.out.println("无参数");
	}
	public void test(String msg)
	{
		System.out.println("重载的test方法 " + msg);
	}

成员变量和局部变量

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

Java 程序中的变量划分如图 5.9 所示。
在这里插入图片描述
成员变量被分为类变量和实例变量两种,定义成员变量时没有 static 修饰的就是实例变量,有 static 修饰的是类变量。其中类变量从该类的准备阶段起开始存在, 直到系统完全销毁这个类,类变量的作用域与这个类的生存范围相同 :而实例变量则从该类的实例被创建起开始存在,直到系统完全销毁这个实例,实例变量的作用域与对应实例的生存范围相同。

提示:一个类在使用之前要经过类加载、类验证、类准备、类解析、类初始化等几个阶段.

在程序中访问类变量通过如下语法:

.类变量
实例.类变量

但由于这个实例并不拥有这个类变量,因此它访问的并不是这个实例的变量,依然是访问它对应类的类变量。同一个类的所有实例访问类变量时,实际上访问的是该类本身的同一个变量, 也就是说,访问了同一片内存区

在程序中访问实例变量通过如下语法:

实例 实例变量

成员变量无须显式初始 ,只要为一个类定义了类变量或实例变量,系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化

局部变量根据定义形式的不同,又可以被分为如下3种。
〉形参
〉方法局部变量
》代码块局部变量:在代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,到该代码块结束时失效。
与成员变量不同的是,局部变量除形参之外,都必须显式初始化 。也就是说,必须先给方法局部变量和代码块局部变量指定初始值,否则不可以访问它们。形参的初始化在调用该方法时由系统完成,形参的值由方法的调用者负责指定。
下面代码是定义代码块局部变量的实例程序。

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);
	}
}

从上面代码中可以看出,只要离开了代码块局部变量所在的代码块,这个局部变量就立即被销毁,变为不可见。
一个类里不能定义两个同名的成员变量,即使一个是类变量,一个是实例变 也不行; 一个方法里不能定义两个同名的局部变量,方法局部变量与形参也不能同名;同一个方法中不同代码块内的代码块局部变量可以同名。
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);
	}
}

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

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

// 创建第一个 Person 对象
Person pl = new Person( );
// 创建第二个 Person 对象
Pers on p2 = new Person() ;
//分别为两个 Person 对象 name 实例变量赋值
pl.name = 张三 ";
p2.name = 悟空 ";
//分别为两个 Person 对象的 eyeNum 类变量赋值
pl.eyeNum = 2 ;
p2.eyeNum = 3 ;

如果是第一次使用Person类,则系统通常会在第一次使用Person 类时加载这个类,并初始化这个类。在类的准备阶段,系统将会为该类的类变量eyeNum分配内存空间,并指定默认初始值为0。当Person类初始化完成后,系统内
存中的存储示意图如图5.10所示。
在这里插入图片描述
系统接着创建了一个Person对象,并把这个Person对象赋给p1变量,Person对象里包含了名为name的实例变量,实例变量是在创建实例时分配内存空间并指定初始值的。当创建了第一个Person对象后,系统内存中的存储示意图如图5.11所示。
在这里插入图片描述
从图5.11中可以看出,eyeNum类变量并不属于Person 对象,它是属Person类的,所以创建第一个Person对象时并不需要为eyeNum类变量分配内存,系统只是为name实例变量分配了内存空间,并指定默认初始值: null.
接着执行Person p2 = new Person();代码创建第二个Person对象,此时因为Person类已经存在于堆内存中了,所以不再需要对Person类进行初始化。创建第二个Person对象与创建第一个Person 对象并没有什么不同。
当程序执行pl.name = “张三”;代码时,将为p1的name实例变量赋值,也就是让图5.11中堆内存中的name指向“张三“字符串。执行完成后,两个Person对象在内存中的存储示意图如图5.12所示。
直到执行pl.eyeNum = 2;代码时,此时通过Person对象来修改Person的类变量,从图5.12中不难看出,Person 对象根本没有保存eyeNum这个变量,通过p1访问的eyeNum类变量,其实还是Person类的eyeNum类变量。因此,此时修改的是Person类的eyeNum类变量。修改成功后,内存中的存储示
意图如图5.13所示。
在这里插入图片描述

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

局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。
与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中。如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;如果局部变量是一个引用类型的变量,则这个变量里存放的是地址。

变量的使用规则

大部分时候都可以直接使用类变量或者实例变量来解决问题,无须使用局部变量。但实际上这种做法相当错误,因为定义一个成员变量时,成员变量将被放置到堆内存中,成员变量的作用域将扩大到类存在范围或者对象存在范围,这种范围的扩大有两个害处。
➢增大了变量的生存时间,这将导致更大的内存开销。
➢扩大了变量的作用域,这不利于提高程序的内聚性。

隐藏和封装

理解封装

比如将某个 erson age 成员变量直接设 000 ,这在语法上没有任何问题,但显然违背了现实。因此, Java 程序推荐将类和对象的成员变量行封装。
封装(Encapsulation)是面向对象的三大特征之一( 另外两个是继承和多态),它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
为了实现良好的封装,需要从两个方面考虑。
➢将对象的成员变量和实现细节隐藏起来,不允许外部直接访问。
➢把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。
因此,封装实际上有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。

使用访问控制符

在这里插入图片描述
这4个访问控制级别的详细介绍如下。
➢private(当前类访问权限)
➢default (包访问权限):default 访问控制的成员或外部类可以被相同包下的其他类访问
➢protected (子类访问权限):如果一个成员(包括成员变量、方法和构造器等)使用protected访问控制符修饰,那么这个成员既可以被同一个包中的其他类访问,也可以被不同包中的子类访问
➢public (公共访问权限)
对于局部变量而言 ,其作用域就是它所在的方法,不可能被其他类访问,因此不能使用访问控制符来修饰

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

下面通过使用合理的访问控制符来定义 Person ,这个 Person类实现了良好的封装。

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;
	}
}

定义了上面的 Perso 类之后,该类的 name和 age 两个成员变量只有在Person 类内才可以操作和访问,Perso 类之外只能通过各自对应的 setter和 getter 方法来操作和访问它们。
下面程序在 main()方法中创建一个 Person对象, 并尝试操作和访问该对象的 age和name 两个实例变量。

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());
	}
}

package和import

package 语句必须作为源文件的第一条非注释性语句,一个源文件只能指定 个包。
import 可以向某个 Java 文件中导入, import 语句应该出现在 package 语句(如果有的话 之后、类定义之前 )。一个Java 源文件可包含多个 lmport 语句。

深入构造器

使用构造器执行初始化

构造器是创建Java对象的途径,是不是说构造器完全负责创建Java对象?

不是!构造器是创建Java对象的重要途径,通过new关键字调用构造器时,构造器也确实返回了该类的对象,但这个对象并不是完全由构造器负责创建的。实际上,当程序员调用构造器时,系统会先为该对象分配内存空间,并为这个对象执行默认初始化,这个对象已经产生了,这些操作在构造器执行之前就都完成了。也就是说,当系统开始执行构造器的执行体之前,系统已经创建了一个对象,只是这个对象还不能被外部程序访问,只能在该构造器中通过this来引用。当构造器的执行体执行结束后,这个对象作为构造器的返回值被返回通常还会赋给另一个引用类型的变量,从而让外部程序可以访问该对象。

构造器重载

如果系统中包含了多个构造器,其中一个构造器的执行体里完全包含另一个构造器的执行体,如图5.16所示。
在这里插入图片描述
从图5.16中可以看出,构造器B完全包含了构造器A。对于这种完全包含的情况,如果是两个方法之间存在这种关系,则可在方法B中调用方法A。但构造器不能直接被调用,构造器必须使用new关键字来调用。但一旦使用new关键字来调用构造器,将会导致系统重新创建一个对象。为了在构造器B中调用构造器A中的初始化代码,又不会重新创建一个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;
	}
}

使用this调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句,使用this调用重载的构造器时,系统会根据this后括号里的实参来调用形参列表与之对应的构造器。
为什么要用this来调用另一个重载的构造器?我把另一个构造器里的代码复制、粘贴到这个构造器里不就可以了吗?

如果仅仅从软件功能实现上来看,这样复制、粘贴确实可以实现这个效果;但从软件工程的角度来看,这样做是相当糟糕的。在软件开发里有一个规则: 不要把相同的代码段书写两次以上!因为软件是一个需要不断更新的产品,如果有一天需要更新图5.16中构造器A的初始化代码,假设构造器B、构造器 …里都包含了相同的初始化代码,则需要同时打开构造器A、构造器B、构造器…的代码进行修改;反之,如果构造器B、构造器…是通过this,调用了构造器A的初始化代码,则只需要打开构造器A进行修改即可。因此,尽量避免相同的代码重复出现,充分复用每一段代码,既可以让程序代码更加简洁,也可以降低软件的维护成本。

类的继承

继承的特点

Java 类只能有一个直接父类。
子类扩展父类时,子类可以从父类继承得到成员变量和方法

重写父类的方法

子类包含与父类同名方法的现象被称为方法重写(Override), 也被称为方法覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。
方法的重写要遵循“两同两小一大”规则,“两同”即方法名相同、形参列表相同;“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等,“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。例如,如下代码将会引发编译错误。

class BaseClass{
       public static void test() { ... }
}
class SubClass extends BaseClass{
       public void test() {.. . } 
}

当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中使用 super(被覆盖的是实例方法)或者父类类名(被覆盖是类方法)作为调用者来调用父类中被覆盖的方法。

如果父类方法具有private 访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。例如,下面代码
是完全正确的。

class BaseClass
{
       // test ()方法是 private 访问权限,子类不可访问该方法
       private void test () { . . . }
}
class SubClass extends BaseClass {
      // 此处并不是方法重写,所以可以增加 static
      public static void test(){. .. }
   }

super限定

this 不能出现在 statlc 修饰的方法中,super 也不能出现在 static 修饰的方法中。 static 修饰的方法是属于类的,该方法的调用者可能是一个类,而不是对象,因而 super 限定也就失去了意义。
在子类定义的实例方法中可以通过 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(); // 输出7
		sc.accessBase(); // 输出5
	}
}

如果在某个方法中访问名为a的成员变量,但没有显式指定调用者,则系统查找a的顺序为:
(1)查找该方法中是否有名为a的局部变量。
(2)查找当前类中是否包含名为a的成员变量。.
(3)查找a的直接父类中是否包含名为a的成员变量,依次上溯a的所有父类,直到java.lang Object类,如果最终不能找到名为a的成员变量,则系统出现编译错误。
如果被覆盖的是类变量,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量。

调用父类构造器

子类不会获得父类的构造器
在一个构造器中调用另一个重载的构造器使用 this 调用来完成, 在子类构造器中调用父类构造器使super 调用来完成。
看下面程序定义了 Base 类和 Sub 类,其中 Sub 类是 Base 类的子类,程序在 Sub 的构造器中使super 来调用 Baas 构造器的初始代码.

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);
	}
}

当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行.
下面程序定义了 个类,它们之间有严格的继承关系。

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();
	}
}

上面程序的 main 方法只创建了 Wolf 对象,但系统在底层完成了复杂的操作,运行上面程序看到如下运行结果.

Creature 无参数的构造器
Animal 带一个参数的构造器,该动物的 name 为灰太狼
Animal 带两个参数的构造器,其 age为3
Wolf 无参数的构造器

多态

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

多态性

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 ploymophicBc = new SubClass();
		// 输出6 —— 表明访问的是父类对象的实例变量
		System.out.println(ploymophicBc.book);
		// 下面调用将执行从父类继承到的base()方法
		ploymophicBc.base();
		// 下面调用将执行从当前类的test()方法
		ploymophicBc.test();
		// 因为ploymophicBc的编译类型是BaseClass,
		// BaseClass类没有提供sub方法,所以下面代码编译时会出现错误。
		// ploymophicBc.sub();
	}
}

上面程序的main()方法中显式创建了三个引用变量,对于前两个引用变量bc和sc,它们编译时类型和运行时类型完全相同,因此调用它们的成员变量和方法非常正常,完全没有任何问题。但第三个引用变量ploymophicBc则比较特殊,它的编译时类型是BaseClass, 而运行时类型是SubClass, 当调用该引用变量的test0方法(BaseClass类中定义了该方法,子类SubClass覆盖了父类的该方法)时,实际执行的是SubClass类中覆盖后的test(方法, 这就可能出现多态了。
与方法不同的是,对象的实例变量则不具备多态性。比如上面ploymophicBc引用变量,程序中输出它的book实例变量时,并不是输出SubClass 类里定义的实例变量,而是输出BaseClass 类的实例变量。

引用变量的强制类型转换

引用类型之间的转换只能在具有继承关系的两个类型之间进行,如果是两个没有任何继承关系的类型,则无法进行类型转换,否则编译时就会出现错误 如果试图把一个父类实例转换成子类类型,则这个对象必须实际上是子类实例才行。
下面是进行强制类型转换的示范程序。

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;
		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异常
		String str = (String)objPri;
	}
}

考虑到进行强制类型转换时可能出现异常,因此进行类型转换之前应先通过 instanceof 运算符来判断是否可以成功转换。

instanceof 运算符

instanceof 算符的前一个操作数通常是一个引用类型变量,后 个操作数通常是一个类 ,它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例 。如果是 ,则返回 true ,否则返回 false。
在使用 instanceof 运算符时需要注意 instanceof 运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误,下面程序示范了 instanceof 运算符的用法。

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)); // 返回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));
	}
}

如果使用 String a =”hello"; 代码定义的变量a也就不能执行 a instanceof Math ,因为a的编译类型是String, String 类型既不是 Math 类型, 也不是 Math 类型的父类,所以这行代码编译就会出错.

初始化块

使用初始化块

初始化块的语法格式如下

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

初始化块的修饰符只能是static 使用 static 修饰的初始化块被称为静态初始化块。
下面程序定义了一个 Person类,它既包含了构造器,也包含了初始化块。下面看看在程序中创建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 对象,程序的输出如下:

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

初始化块只在创建 Java 对象时隐式执行,而且在执行构造器之前执行

注意:当Java 创建一个对象时,系统先为该对象的所有实例变量分配内存(前提是该类已经被加载过了),接着程序开始对这些实例变量执行初始化,其初始化顺序是:先执行初始化块或声明实例变量时指定的初始值(这两个地方指定初始值的执行顺序与它们在源代码中的排列顺序相同),再执行构造器里指定的初始值。

静态初始化块

静态初始化块是类相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行,因此静态初始化块总是比普通初始化块先执行
静态初始化块不能对实例变量进行初始化处理。

注意:静态初始化块不能访问非静态成员,包括不能访问实例变量和实例方法。

与普通初始化块类似的是,系统在类初始化阶段执行静态初始化块时,最后才执行该类的静态初始化块。

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();
	}
}

结果:
在这里插入图片描述
从图5.23 来看,第一次创建 Leaf 对象时,因为系统中还不存在 Leaf 类,因此需要先加载并初始化 Leaf 类,初始化 Leaf 类时会先执行其顶层父类的静态初始化块,再执行其直接父类的静态初始化块,最后才执行 Leaf 本身的静态初始化块。
Leaf 类初始化成功后, Leaf 类在该虚拟机里将一直存在,因此当第二次创建 Leaf 实例时无须再次对 Leaf 类进行初始化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值