java基础学习(六):面向对象

6 类与对象

  Java是面向对象的程序设计语言,类是面向对象的重要内容,可以把类当成一种自定义类型,可以使用类来定义变量,这种类型的变量统称为引用变量。所有类都是引用变量。

6.1 定义类

  面向对象的程序设计过程中有两个重要概念:类(class)和对象(object,也被称为实例,instance),其中类是某一批对象的抽象,可以把类理解成某种概念;对象才是一个具体存在的实体。

6.1.1 定义类的简单语法格式如下:

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

对定义类语法格式的详细说明如下:

  1. 修饰符可以是public、final、abstract,或者完全省略三个修饰符,类名只要是一个合法的标识符即可。
  2. 定一个类定义而言,可以包括三种最常见的成员:构造器,成员变量和方法。
  3. static修饰的成员不能访问没有static修饰的成员
  4. 构造器是一个类创造对象的根本途径,如果一个类没有构造器,这个类通常无法创建实例。因此,Java语言提供了一个功能:如果程序员没有一个类编写的构造器,则系统会为该类提供一个默认的构造器,一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器了。

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

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

对定义成员变量语法格式的详细说明如下:

  1. 修饰符:修饰符可以省略,也可以是public、protected、private、static、final,其中public、protected、private三个最多只能出现其中之一,可以与static、final组合起来修饰成员变量。
  2. 类型:类型可以是Java语言中允许的任何数据类型,包括基本类型和现在介绍的引用类型。
  3. 成员变量名:只要合法即可,但是还是不要乱起名了。
  4. 默认值:定义一个成员变量还可以指定一个可选的默认值。

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

[修饰符] 方法返回值类型 方法名(形参列表)
{
	//由零条到多条可执行性语句组成的方法体
}

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

  1. 修饰符:修饰符可以省略,也可以是public、protected、static、final、abstract,其中public、protected、private三个最多只能出现一个;final和abstract最多只能出现其中一个,他们可以以static组合起来修饰方法。
  2. 方法返回值类型:返回值类型可以是Java语言允许的任何数据类型,包括基本类型和引用类型;如果声明了方法返回类型,则方法体内必须有一个有效的return语句。
  3. 方法名:方法名的命名规则与成员变量的命名规则相同
  4. 形参列表:形参列表用于定义该方法可以接受的参数。

  方法体里多条执行语句之间有严格的执行顺序,排在方法体面前的语句总是先执行,排在方法后的语句总是慢执行。
  static 是一个特殊的关键字,它可用于修饰方法、成员变量等成员。static修饰的成员表明它属于这个类本身,而不属于该类的单个实例,因为通常把static修饰的成员变量和方法也称为类变量、类方法。不适用static修饰的普通方法、成员变量则属于该类的单个实例,而不属于该类。通常把不使用static修饰的成员变量和方法也称为实例变量、实例方法。
  由于static的英文直译就是静态的意思,因此有时也把static修饰的成员变量和方法称为静态变量和静态方法,把不使用static修饰的成员变量和方法称为非静态变量和非静态方法。静态成员不能直接访问非静态成员。

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

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

对定义构造器语法格式的详细说明如下:

  1. 修饰符:修饰符可以省略,也可以是public、protected、private其中之一
  2. 构造器名:构造器名必须和类名相同
  3. 形参列表:和定义方法形参相同

  值得指出,构造器既不能定义返回值类型,也不能使用void声明构造器没有返回值。如果为构造器定义了返回值类型,或使用void声明构造器没有返回值,编译时不会出错,但Java会把这个所谓的构造器当成方法来处理。

6.2 对象的产生和使用

6.2.1创造一个对象

在这里写一个Person类:

public class Person
{
	//下面定义两个成员变量
	public String name;
	public int age;
	//下面定义了一个say方法
	public void say(String content)
	{
		System.out.println(content);
	}
}

6.2.2 创建一个实例

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

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

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

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

6.2.3 调用实例变量和方法

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

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

6.2.4 对象的this引用

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

  • 构造器中引用该构造器正在初始化的对象
  • 在方法中引用调用该方法的对象
public class Dog
{
	//定义一个jump()方法
	public void jump()
	{
		System.out.println("正在执行jump方法");
	}
	
	public void run()
	{
		this.jump();
		System.out.println("正在执行run方法");
	}
}

  对于static修饰的方法而言,则可以使用类来直接调用该方法,如果在static修饰的方法中使用this关键字,则这个关键字就无法指向合适的对象。所以,static修饰的方法中不能使用this引用,由于static修饰的方法不能使用this引用,所以static修饰的方法不能访问不适用static修饰的普通成员:静态成员不能直接访问非静态成员

下面程序演示了静态方法直接访问非静态方法时引发错误。

public class StaticAccessNonStatic
{
	public void info()
	{
		System.out.println("简单的info方法");
	}
	public static void main(String[] args)
	{
	// 因为main()方法是静态方法,而info()是非静态方法
	// 调用main的方法的是该类本身,而不是该类的实例
	//因此省略的this无法指向有效的对象
	info();
	}
}

会出现下面这样的错误:

无法从静态上下文中引用非静态方法 info()

  注意:Java中static修饰的成员属于类本身,而不属于该类的实例,既然static修饰的成员完全不属于该类的实例,那么就不应该允许使用实例去调用static修饰的成员变量和方法!记住:Java编程时不要使用对象去调用static修饰的成员变量、方法,而是应该使用类去调用static修饰的成员变量、方法!

  除此以外,this引用也可以用于构造器中作为默认引用,由于构造器是直接使用new关键字来调用,而不是使用对象来调用的,所以在this在构造器中代表该构造器正在初始化的对象。

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

6.3 方法详解

  如果需要定义方法,则只能在类体内定义,不能独立定义一个方法,如果这个方法使用了static修饰,则这个方法属于这个类,否则这个方法属于这个类的实例。

  Java语言里的方法的所属性主要体现在如下方面:

  • 方法不能独立定义,方法只能在类体里定义
  • 从逻辑意义上来看,方法要么属于该类本身,要么属于该类的一个对象
  • 永远不能独立执行方法,执行方法必须使用类或对象作为调用者

  使用 static 修饰的方法属于这个类本身,使用 static 修饰的方法既可以使用类作为调用者来调用,可以使用对象作为调用者来调用。但值得指出的是,因为使用 statlc 修饰的方法还是属于这个类的,因此使用该类的任何对象来调用这个方法时将会得到相同的执行结果,这是由于底层依然是使用这些实例所属的类作为调用者。

6.3.1 方法的参数传递机制

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

class DataWrap
{
	int a;
	int b;
{
public class ReferenceTranferTest
{
	public static void swap(DataWrap dw)
	{
		//下面三行代码实现dw的a、b两个成员变量的值交换
		// 定义一个临时变量来保存dw对象的a成员变量的值
		int tmp = dw.a;
		dw.a = 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 + " ; 成员变量的值是 + dw .b) ;
	}
}
	

  程序从main()方法开始执行,main()方法开始创建了一个DataWrap对象,并定义一个dw引用变量来指向DataWrap对象,这是一个与基本类型不同的地方。

6.3.2 形参个数可变的方法

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

public class Varargs
{
	//定义了形参个数可变的方法
	public static void test(int a, String... boos)
	{
		//books 被当数组出路
		for(String tmp : books)
		{
			System.out.peintln(tmp);
		}
		// 输出整数变量a的值
		System.out.println(a);
	}
	public static void main(String[] args)
	{
		// 调用test方法
		test(5, "哈哈哈","123h");
	}
}

6.3.3 方法重载

  Java 允许同一个类里定义多个同名方法,只要形参列表不同就行。如果同一个类中包含了两个或两个以上方法的方法名相同,但形参列表,则被称为方法重载。
  从上面介绍可以看出,在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 ol = new Overload();
		ol.test();
		ol.test("heelo");
	}
}

6.4 成员变量和局部变量

  变量可以分为两大类:成员变量和局部变量

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

  成员变量指的是在类里定义的变量,也就是前面所介绍的field:局部变量指的是在方法里方法里定义的变量。
java基础学习(六):面向对象_第1张图片
  成员变量被分为类变量和实例变量两种,定义成员变量时没有static修饰的是就是实例变量,有static修饰的就是类变量。

6.5 隐藏和封装

  前面程序中经常出现通过某个对象的直接访问其成员变量的情形,比如将某个Person的age成员变量直接设为1000,显然不合理。

6.5.1 理解封装

  封装(Encapsulation)是面向对象的三大特性之一(另外两个是继承和多态),它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
  对一个类进行封装,可以实现下面目的:

  • 隐藏类的实现细节
  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问
  • 可进行数据检查,从而有利于保证对象信息的完整性
  • 便于修改,提高代码的可维护性

6.5.1 使用访问控制符

java基础学习(六):面向对象_第2张图片
  Java提供三个访问控制符:private、protected和public,分别代表了3个访问控制级别,另外还有一个不加任何访问控制符的访问控制级别。详细介绍如下:

  • private(当前类访问权限):如果类里的一个成员(包括成员变量、方法和构造器等)使用private访问控制符来修饰,则这个成员只能在当前类的内部被访问。这个访问控制符用于修饰成员变量最合适,使用它来修饰成员变量就可以把成员变量隐藏在该类得内部。
  • default(包访问权限):如果类里的一个成员(包括成员变量、方法和构造器等)或者一个个外部类不使用任何访问控制符修饰,就称它使包访问权限的,default访问控制的成员或外部类可以被相同包下的其他类访问。
  • protected(子类访问权限):如果一个成员(包括成员变量、方法和构造器等)使用protected访问控制符修饰,那么这个成员既可以被同一个包中的其他类访问,也可以被不同包中的子类访问。
  • public(公共访问权限):这个是一个最宽松的访问控制级别,如果一个成员或者一个外部类使用public访问控制符修饰,那么这个成员或外部类就可以被所有类访问,不管访问类和被访问类是否处于同一个包中,是否具有父子继承关系。
    java基础学习(六):面向对象_第3张图片
      对于外部类而言,它也可以使用访问控制符修饰,但外部类只能有两种访问控制级别:public和默认,外部类不能使用private喝protected修饰,因为外部类没有处于任何类的内部,也就没有其他所在类的内部、所在类的子类两个范围,所以private和protected访问控制符对外部类没有意义。
      外部类可以使用 public 包访问控制权限 ,使用 public 修饰的外部类可以被所有类使用,如声明变量、创建实例 不使用任何访问控制符修饰的外部类只能被同 个包中的其他类使用。
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
	}
}

  注意:Java类里实例变量的setter和getter方法有非常重要的意义。例如,某个类里包含了一个名为abc的实例变量,则其对应的setter和getter方法名应为setAbc() 和 getAbc()(将原实例变量名的首字母大写,并在前面分别增加 set get 动坷,就变成 setter,getter方法名)。

关于访问控制符的使用,存在如下几条基本原则

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

6.6 package、import和import static

  前面提到包范围,先来回忆一个场景:在我们漫长的求学、工作生涯中可曾遇到过与自己同名的同学或同事?因为笔者姓名的缘故,笔者经常会遭遇此类事情。如果同一个班级里出现两个叫“李刚”的同学,那老师怎么处理呢?老师通常会在我们的名字前增加一个限定,例如大李刚、小李刚以示区分。
  Java允许将一组功能相关的类放在同一个package下,从而组成逻辑上的类库单元。如果希望把一个类放在指定的包结构下,应该在Java源程序的第一个非注释行放置如下格式的代码:

package packageName;

  一旦在Java源文件中使用了这个package语句,就意味着源文件里定义的所有类都属于这个包。位于包中的每个类的完整类名都应该是包名和类名的组合,如果其他人需要使用该包下的类,也应该使用包名加类名的组合。

package lee;
public class Hello
{
	public static void main(String[] args)
	{
		System.out.println("Hello World");
	}
}

在该目录下运行:

javac -d . Hello.java

这时候不会单单出现一个 .java文件,还会出现lee文件夹,Hello.java文件会在lee目录中。这时候需要执行文件即

java lee.Hello

上面定义的的位于lee包下的Hello.java及生成的Hello.class文件,建议以下图形式存放:
java基础学习(六):面向对象_第4张图片
  为了简化编程,Java引入了import关键字,import可以向某个Java文件中导入指定包层次下某个类或全部类,import语句应该出现在package语句之后、类定义之前。一个Java源文件只能包含一个package语句,但是可以有多个import语句。使用import语句导入单个类的用法如下:

import package.subpackage...ClassName;

上面语句用于直接导入指定Java类,使用下面代码:

import lee.sub.Apple;

使用import语句导入指定包下全部类的用法如下:

import package.subpackage...*;

  上面import语句中的星号(*)只能代表类,不能代表包。因此使用import lee.*;语句时,它表明导入lee包下的所有类,即Hello类和Hello Test类,而lee包下的sub子包并不导入。

**注意:**Java默认为所有源文件导入java.lang包下的所有类,因此前面在Java程序中使用String、System类时都无须类时都无须使用import语句来导入这些类,但对于前面介绍数组时提到的Arrays类,其位于java.util包下,则必须使用import语句来导入该类。

现在可以总结出Java源文件的大体结构定义:

package 语句  //0个或者1个,必须放在文件开始
import | import static 语句  //0或者多个
public classDefinition | interfaceDefinition | enumDefinition //0个或者一个public类,接口,枚举定义
classDefinition | interfaceDefintion | enumDefinition //0个或多个普通类,接口或枚举定义

6.6 深入构造器

  构造器是一个特殊的方法,这个特殊的方法用于创建实例时执行初始化。构造器是创建对象的重要途径。

6.6.1 使用构造器执行初始化

&esmp; 构造器最大的用出就是在创建对象时执行初始化。当创建一个对象时,系统为这个对象的实例变量进行默认初始化,这种默认的初始化把所有基本类型的实例变量设为0或false和null。

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("ahha",900);
		System.out.println(tc.name);
		System.out.println(tc.count);
	}
}

感觉有点像python中的__init__函数。

6.6.2 构造器重载

  同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。构造器允许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 tc01 = new ConstructorOverload();
		ConstructorOverload tc02 = new ConstructorOverload("ahha",900);
		System.out.println(tc01.name + tc01.count);
		System.out.println(tc02.name + tc02.count);
	}
}

还有一种就是在构造器中调用另一个构造器的例子:
java基础学习(六):面向对象_第5张图片

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.weigth = weight;
	}
}

6.7 类的继承

  继承是面向对象的三大特性之一,也是实现软件复用的重要手段。Java继承具有单继承的特点,每个子类只有一个直接父类。

6.7.1 继承的特点

  Java的继承通过extends来实现。如下:

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

这个比较简单,不写了。

6.7.2 重写父类的方法

  子类扩展了父类,子类是一个特殊的父类。大部分时候。大部分时候,子类总是以父类为基础,额外增加新的成员变量和方法。但是也有需要子类重写父类方法的情况:

public class Bird
{
	public void fly()
	{
		System.out.println("我在飞...");
	}
}
public class Ostrich extends Bird
{
	//重写Bird类的fly()方法
	public void fly()
	{
		System.out.println("我在跑....");
	}
	public static void main(String[] args)
	{
		Ostrich os = new Ostrich();
		os.fly();
	}
}

这种子类包含父类同名的方法的现象称为方法的重写,也称为方法的覆盖。
  方法的重写需要遵守“两同两小一大”规则,“两同”即方法名相同、形参列表相同;“两小”指的是子类方法的返回值类型应该父类方法返回值类型更小或相等,子类方法声明抛出的异常类应该比父类方法声明抛出的异常类更小或相等;“一大”指的是子类方法的访问权限比父类方法权限更大或者相等。

6.7.3 super限定

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

public void callOverrideMethod()
{
	super.fly();
}

  super是Java的一个关键字,super用于限定该对象调用它从父类继承得到的实例变量或方法。正如this不能出现在static修饰的方法中一样,super也不出现在static修饰的方法中。static修饰的方法是属于类的,该方法的调用者可能是一个类,而不是对象。
  如果子类里没有包含和父类同名的成员变 ,那么在子类实例方法中访问该成员变 时,则无须显式使用 super 或父类名作为调用者。如果在某个方法中访问名为 的成员变 ,但没有显式指定调用者,则系统查找a的顺序为:

  • 查找该方法中是否有名为a的局部变量
  • 查找当前类中是否包含名为a的成员变量
  • 查找a的直接父类中是否包含名为a的成员变量,依次上溯a的所有父类。

6.7.4 调用父类构造器

  子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始 代码,类似于前面所介绍的一个构造器调用另一个重载的构造器。
  个构造器中调用另一个重载的构造器使用 this 调用来完成,在子类构造器中调用父类构造器使super 调用来完成。

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(size, name);
		this.color = color;
	}
	public static void main(String[] args)
	{
		Sub s = new Sub(5.6, "ahda", "dad");
		System.out.println(s.size + "--" + s.name + "--" + s.color);
	}
}

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

class Creature
{
	public Creature()
	{
		System.out.println("Creature无参数的构造器");
	}
}

class Animal extends Creature{
	public Animal(String name)
	{
		System.out.println("Aniaml自带一个参数的构造器:" +name);
	}
	public Animal(String name, int age)
	{	
		this(name);
		System.out.println("Animal自带两个参数的构造器:" + name + age);
	}
}

public class Wolf extends Animal
{
	public Wolf()
	{
		super("狼", 3);
		System.out.println("Wolf无参数的构造器");
	}
	public static void main(String[] args)
	{
		new Wolf();
	}
}

运行结果:

Creature无参数的构造器
Aniaml自带一个参数的构造器:狼
Animal自带两个参数的构造器:3
Wolf无参数的构造器

看得出来,构造器都是从最原始往下开始,然后如果有this调用同类的构造器,则依次执行。

6.8 多态

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

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

public class SubClass extends BaseClass
{
	public String book = "hahah";
	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();
		System.out.println(sc.book);
		sc.base();
		sc.test();
		//下面编译时类型和运行时类型不一样,多态发生
		BaseClass ploymophicBc = new SubClass();
		System.out.println(ploymophicBc.book);
		ploymophicBc.base();
		ploymophicBc.test();
	}
}

  当把一个子类对象直接赋给父类引用变量时,例如上面的BaseClass ploymophicBc = new SubClass(); 这个ploymophicBc引用变量的编译时类型为BaseClass,而运行时类型时SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。
  上面的main()方法中如果使用ploymophicBc.sub();这行代码会在编译时引发错误。虽然ploymophicBc引用变量实际上确实包含sub()方法,但编译类型为BaseClass,因此无法调用sub()方法。
  与方法不同的是,对象的实例变量则不具备多态性,比如上面的ploymophicBc引用变量,程序中输出它的book实例变量时,并不是输出SubClass类里定义的实例变量,而是输出BaseClass类的实例变量。

  引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时执行它运行时类型具有的方法,因此,编写Java代码的时候,引用变量只能调用声明该变量时所用类包含的方法。通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量。

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

  编写Java程序时,引用变量只能调用它编译时类型的方法,而不能调用它运行时类型的方法,即使它实际所引用的对象确实包含该方法。如果需要让这个引用变量调用它运行时类型的方法,则必须把它强制类型转换成运行时类型,强制类型转换需要借助于类型转换运算符。
  类型转换运算符是小括号,类型转换运算符的用法是:(type)variable,这种用法可以将variable变量转换成一个type类型的变量。这种强制类型转换需要注意:

  • 基本类型之间转换只能在数值类型之间进行,这里所说的数值类型包括整数型,字符型和浮点型。但数值类型和布尔类型之间不能进行类型转换。
  • 引用类型之间的转换只能在具有继承关系的两个类型之间进行,如果是两个没有任何继承关系的类型,是无法进行类型转换的。如果试图把一个父类实例转换为子类类型,则这个对象必须实际上是子类实例才行。
public class ConversionTest
{
	public static void main(String[] args)
	{
		double d = 13.4;
		long l = (long)d;
		System.out.println(l);
		int in = 5;
		//试图把一个数值类型的变量转换为boolen类型,
		//编译时会提示:不可转换类型
		//boolean b = (boolean)in;
		Object obj = "Hello";
		//obj变量的编译时类型为Object与String存在继承关系,可以强制类型转换
		//而且obj变量的实际类型是String
		String objStr = (String)obj;	
		System.out.println(objStr);
		//定义一个objPri变量,编译时类型为Object,实际类型为Integer
		Object objPri = Integer.valueOf(5);
		String str = (String)objPri;
	}
}

  考虑到进行强制类型转换时可能出现异常,因此进行类型转换之前应先通过。instanceof运算符来判断是否可以成功转换。例如,上面的String str = (String)objPri; 代码运行时会引发 ClassCastException异常,这是因为 objPri 不可转换成 String 类型 为了让程序更加健壮,可以将代码改为如下:

if (objPri instanceof String)
{
	String str = (String)Objpri;
}

在进行强制类型转换之前,先用 instanceof 运算符判断是否可以成功转换,从而避免出现 ClassCastException 异常,这样可以保证程序更加健壮。

6.9 继承和组合

  继承是实现类复用的重要手段,但继承带来了一个最大的坏处:破坏封装。组合也是实现类复用的重要方式,而采用组合方式来实现复用则能提供更好的封装性。

  继承带来了高度复用的同时,也带来了严重的问题:继承严重地破坏了父类地封装性。前面介绍封装时提到:每个类都应该封装它内部信息和实现细节,而只暴露必要的方法给其他类使用。但在继承关系中,子类可以直接访问父类的成员变量和方法,从而创造子类和父类严重耦合。
  为了保证父类具有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则:

  • 尽量隐藏父类的内部数据。尽量把父类的所有成员变量都设置为private访问类型,不要让子类直接访问父类的成员变量
  • 不要让子类可以随意访问、修改父类方法。父类中那些仅为辅助其他的工具,应该使用private访问控制符修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,则必须以public修饰,但又不希望子类重写该方法,可以使用final修饰符来修饰该方法;如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则可以使用protected来修饰该方法。
  • 尽量不要在父类构造器中调用将要被子类重写的方法

6.9.1 利用组合实现复用

  组合是把旧类对象作为新类的成员变量组合进来,以实现新类的功能。通常需要在新类中使用private修饰被组合的旧类对象。

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;
	}
	public void breath()
	{
		//直接复用Animal提供breath()方法来实现Bird的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.fly();

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

6.10 初始化块

  Java使用构造器来对单个对象进行初始化操作,使用构造器完成整个Java对象的状态初始化,然后将Java对象返回给程序,从而让该Java对象的信息更加完整。

6.10.1 使用初始化块

  初始化块是 Java 类里可出现的第 种成员(前面依次有成员变量、方法和构造器), 一个类里可以有多个初始化块,相同类型的初始化块之间有顺序 前面定义的初始化块先执行,后面定义的初始化块后执行。初始化块的语法格式如下:

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

  初始化修饰符只能是static,使用static修饰的初始块被称为静态初始化块。

public class Person
{
	{
		int a = 6;
		if (a > 4){
			System.out.println("Person初始化模块a>4");
		}
		System.out.println("Person第一个初始化模块a>4");
	}
	{
		System.out.println("Person第二个初始化块");
	}
	public Person
	{
		System.out.println("Person类的无参数构造器");
	}
	pubilc static void main(String[] args)
	{
		new Person();
	}
}

输出:

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

从运行结果可以看出,当创建 Java 对象时,系统总是先调用该类里定义的初始化块,如果一个类里定义了2个普通初始化块,则前面定义的初始化块先执行,后面定义的初始化块后执行.

6.10.2 初始化块和构造器

  从某种程度上来看,初始化块是构造器的补充,初始化块总是在构造器执行之前执行。系统同样可使用初始化块来进行对象的初始化操作。
  与构造器不同的是,初始化块是一段固定执行的代码,它不能接收任何参数 因此初始化块对同一个类的所有对象所进行的初始化处理完全相同 。基于这个原因,不难发现初始化块的基本用法,如果有段初始化处理代码对所有对象完全相同,且无须接收任何参数 ,就可以把这段初始化处理代码提取到初始化块中 显示了把两个构造器中的代码提取成初始化块示意图。

你可能感兴趣的:(Java)