面向对象程序设计·学习笔记(1/2)

前言

  课程源自中国大学MOOC:《面向对象程序设计——Java语言》(浙江大学·翁恺),链接: link
整理的内容,主要是方便以后查阅,并不是很好的阅读学习材料。


第1章 类与对象

1.1 用类制造对象

  对象变量是对象的管理者而非所有者,所以涉及到赋值、函数参数传递和比较都与普通变量有所不同。本节尝试自己定义类,然后用自己定义的类来创建对象。下面直接学示例代码(这里省略了Picture等类的代码):

package shapes;
public class MyPic {
	public static void main(String[] args) {
		Picture pic = new Picture(420,300);
		Circle c1 = new Circle(320,40,80);
		Rectangle r1 = new Rectangle(100,100,100,100);
		Triangle t1 = new Triangle(100,100,200,100,150,50);
		Line l1 = new Line(0,205,400,205);
		pic.add(c1);
		pic.add(r1);
		pic.add(t1);
		pic.add(l1);
		pic.draw();
	}
}

  可以看到,Picture类创建出pic对象,Circle类创建出c1对象,再把c1对象通过Picture类的add函数添加到对象pic里,再通过draw函数画出来。
  类和对象的关系:类定义了对象长什么样(所有的属性),对象则是按照类的定义所制造出来的实体,一个类可以创建很多对象,每个对象有自己的数据
  类就是Java中的类型,定义了对象的所有属性,可以用来定义变量。
  对象具有属性(由类定义,体现在数据上),和提供服务(体现在对数据的操作:函数)。

对象示意图:对象=数据(属性)+操作(服务/函数)数据与操作的关系
在这里插入图片描述内部的数据被外部的操作紧密包围

可以通过操作来要求对象提供服务

内部数据由对象来保护,数据不对外公开

把数据和对数据的操作放在一起 → \to 封装

  OOP特性
  1 一切都是对象
  2 程序就是一堆互相发送消息的对象
  3 每个对象有自己的存储空间,里面是其他的对象
  4 每个对象都有一个类型
  5 所有属于某个特定类型的对象可以提供相同的服务

PS1:OOP即Object Oriented Programming,面向对象程序设计。
PS2:“Alt+/” 让eclipse根据输入的部分字母联想相关语法;“Shift+↑或↓” 多行选中;“ctrl+/” 注释和取消注释。

1.2 定义类

  一个类由两种东西组成:表示对象有什么的成员变量和表示对象能做什么的成员函数
  一旦定义了类,我们就可以创建这个类的多个对象,这些对象都会做那个类所定义的动作(函数),但是各自具有不同的数据。
  假设我们要编程实现一个自动售货机,那么这是一个对象,先得定义自动售货机是什么,它有哪些属性,可以做哪些操作。所以这里,类:自动售货机VendingMachine;
  部分属性(成员变量):价格price、余额balance、总额total;
  部分操作(成员函数):显示提示showPrompt、收钱insertMoney、显示余额showBalance、得到货物getFood。
  定义了类,再(根据这个类)用new来创建出具体的对象。如VendingMachine v = new VendingMachine();
  右侧new+类+()创建对象;左侧类名+变量名定义出一个对象变量(这里的类型是自定义的);“=”再将右侧创建出的对象交给对象变量管理,即对象变量是对象的管理者而非所有者(注意它们的赋值、比较、传递进入函数)。如这里的对象变量v管理着一个对象。
  让对象做事,就是:对象变量+ . +类定义的成员函数(操作、服务)。

1.3 成员变量和成员函数

  类里的成员变量表达了类具有的属性。写在类里的成员变量,只是一个声明,变量并不在那里,变量不在类里,变量在每一个对象里。自然的,同一类的各对象的变量是分开的。
  类里的成员函数可以直接写成员变量的名字来访问成员变量。而函数在实际运算时,访问的是【这次调用这个成员函数的对象】的变量,因此成员函数中直接写成员变量名前实际上可以加一个this.,this被称为成员函数的固有本地(对象)变量,用于指代调用了该函数的那个对象。举个不推荐但需要理解的例子:
  一个类已经有了成员变量a,在一个与a有关的成员函数中,成员函数有参数b,b和a具有相似的含义或作用,也命名为a,那么为了区分引用该函数的对象的变量a和函数自己的参数a,就可以用this.a和a做区分。如

	int price =0;
	viod setPrice(int price) {
		this.price = price;
	}

  不推荐是因为完全可以通过规范的命名来实现。
  对象+"." 的调用临时建立了成员函数与具体对象之间的联系。通过点运算符可以调用某个对象的函数。
  类似的,类里的成员函数可以直接写该类的其他成员函数的名字来访问其他成员函数,当然也可以加this.,但没有必要。实际上外层成员函数的this,传递进了该成员函数内部调用的其他成员函数。
  定义在函数内部的变量我们称为本地变量,这次讲的定义在同一个类中、定义在成员函数外部的变量,称为成员变量
  本地变量的生存期作用域都是函数内部,函数的大括号{ }里。
  成员变量的生存期是对象的生存期(起始于new出对象,不关心结束,Java自动回收机制),作用域是类内部(或类内部的成员函数,指类里的成员函数可以使用这些成员变量,成员变量初始化时也可用到其他成员变量)。

1.4 对象初始化

  变量的初始化是程序安全很重要的一环。一旦创建了一个对象,有什么手段可以保证其中的每一个成员变量都有确定的初始值呢?Java提供了多种手段来保障对象创建时的初始化,包括1、给每个成员变量默认的“0”值;2、定义初始化;3、构造函数;4、函数重载;5、用this()来调用其他构造函数的方式。
  对于本地变量而言,如果没有初始化,就无法使用这个本地变量,编译无法通过。
  对于类里面的成员变量,(如果没有初始化,)Java会先给默认“0”值,完成初始化。当然“0”值具体取决于成员变量类型,如果类型是Boolean,0值就是false;如果是对象变量,0值就是null,表示没有联系(管理)着任何对象。

  • 成员变量在定义的地方就可以给出初始值;
  • 没有给出初始值的成员变量会自动获得0值;
  • 对象变量的0值表示没有管理任何对象,也可以主动给null值;
  • 定义初始化可以调用函数(用这个函数的返回值做初始化),甚至可以使用已经定义的成员变量。
      关于调用函数初始化,给出如下例子
public class Price{
	int tax = f();
	int total = 888;
	int f() {
		return 10}
	Price(){
		total = 100}
	Price(int total){
		this.total = total;
	}
}
public static void main(String[] args) {
	Price bookPrice = new Price();
	Price phonePrice = new Price(8999);
}

  上面的Price(){ } 、Price(int total){ } 被称为构造函数:
1.函数名与类的名字相同;
2.没有也不能有返回类型,但是有访问属性,如private Price()、public Price(int i),缺省也是访问属性,即这里虽然显示前面什么都没有,但其实是访问属性;
3.创建类的每一个对象时会自动调用这个构造函数;
4.具体运行时,调用构造函数后,不先执行函数内部指令,而是先将构造函数外的定义初始化做完,再执行构造函数内部;
5.构造函数可以根据参数的不同而不同; 6.同样调用时,根据new的格式调用不同的构造函数来创建类。
  上面的Price类的对象bookPrice(实为该对象变量管理的对象,后面不再说明)的成员变量total是先888再进构造函数Price(),最后为100;而对象phonePrice的成员变量total是先888再进Price(8999),最后8999。它们的tax都是10。
  一个类可以有多个构造函数,只要它们的参数表不同。根据参数的个数、类型等,调用多个同名不同参数的构造函数,被称为重载(音chong)。一个类里的同名但参数表不同的函数构成了重载关系。
  还通过this()还可以调用重载关系下的其他构造函数,但this()只能出现在构造函数中,且放在第一行,且只使用一次,比如

	Price(){
		total = 100}
	Price(int a){
		this();
		total = total + a;
	}

类型自动转换:当函数期望的参数类型比调用参数时给的值类型宽的时候,编译器可以自动把给的值转换成函数要求的参数类型,如输出char→int→double,double比int宽,那么如果调用函数的时候给出了int值,是会被自动转换成double去调用函数的。(反之,当函数期望的参数类型比调用参数时给的值类型窄的时候,需要做强制类型转换。)
但是,如果有两个函数重载,一个是double,一个是int,则不会再发生类型自动转换。有重载会自动匹配类型符合的成员函数。

第2章 对象交互

2.1 对象交互

  面向对象程序设计的第一步,就是在问题领域中识别出有效的对象,然后从识别出的对象中抽象出类来。现实问题往往存在多种对象划分的方式,而不同的划分会带来类的设计以至于程序结构的各种不同。对象划分有一些理论,这里不作深究,而且目前的理论也无法放诸四海皆准。
  一个对象显然可以由其他类的对象来组成。对象是由其他对象组成的,而类定义了这样的组合关系。因此需要弄清楚,当一个对象里有多个对象的时候,那些对象之间是如何交互的,对象和对象之间的联系是如何建立的,对象如何和其他对象交流。
  对象和对象之间的联系紧密程度叫做耦合。对象和对象的耦合程度越紧,表现在源代码上,就是它们的代码是互相依赖、互相牵制的。我们理想的模型,是对象和对象之间的耦合要尽可能的松,平行的对象要尽量减少直接联系,让更高层次的对象来提供通信服务。下面以一个简单的数字钟为例子。
  对于形如"XX:YY"的数字时钟,容易想到一个时钟的对象包含小时和分钟两个对象。
  但应该注意到,小时和分钟实为一个类,时钟是一个更大的类。
  不妨记小时和分钟的类为显示Display,提取它们的共性,有成员变量:某刻的时间值value,上限值limit(24或60),成员函数:时间递增increase,获取某刻的时间getValue(以帮助判断实现23时、59分后时间归零继续递增)等。
 &emsp多说无益,新建clock包,首先新建Display类,直接学习代码:

package clock;

public class Display {
	private int value = 0;
	private int limit = 0;
	
	public Display(int paralimit) {
		limit = paralimit;
	}
	
	public void increase() {
		value++;
		if ( value == limit ) {
			value = 0;
		}
	}
	
	public int getValue() {
		return value;
	}
	
	public static void main(String[] args) {
		Display d = new Display(24);
		for (; ;) {
			d.increase();
			System.out.println(d.getValue());
		}
	}
}

  目前通过Display类实现了一个两位数(24或60)的递增循环显示,而数字时钟显示至少需要小时hour、分钟minute这两个Display类下的对象,这两个对象组成了一个新的对象,时钟,时钟的类记为Clock。即由对象组成更大的对象,组成的对象自然有新的相对应的类。
  同时hour和minute之间存在互动,59到60分钟归零时小时加一。我们希望,同一个类的每个对象尽可能的独立,彼此之间没有直接联系。以这里为例,尽管可以在minute对象里加代码使hour加一,但这样要么hour与minute类不同,要么代码冗杂,因此我们希望hour和minute相互独立。那么,它们之间的联系必然通过凌驾于它们的第三方来实现,在这里就是Clock类下的对象。
  多说无益,再建Clock类,直接学习代码:

package clock;

public class Clock {
	private Display hour = new Display(24);
	private Display minute = new Display(60);

	public void start() {
		for (;;) { //也可以while(true) { }
			minute.increase();
			if (minute.getValue() == 0) {
				hour.increase();
			}			
			// printf输出是带格式的输出,第一个字符串表示需要的格式
			System.out.printf("%02d:%02d\n", hour.getValue(), minute.getValue());
		}
	}

	public static void main(String[] args) {
		Clock clock = new Clock();
		clock.start();
	}

}

  这里的例子说明,一个类里面的成员变量,可以是其他类的对象,反映了一个对象可以由其他对象组成。这里clock类,成员变量是对象minute和hour(实为对象变量,是管理者不是所有者,同样需要创建出来),成员函数是start函数,用来完成minute对象和hour对象间的互动,minute和hour间各自做好自己的事情,代码间互不相关。

2.2 访问属性

  封装,就是把数据和对这些数据的操作放在一起,并且用这些操作把数据掩盖起来,是面向对象的基本概念之一,也是最核心的概念。我们有一个非常直截了当的手段来保证在类的设计的时候做到封装:

  1. 所有的成员变量必须是private的,这样就避免别人任意使用你的内部数据;
  2. 所有public的函数,只是用来实现这个类的对象或类自己要提供的服务的,而不是用来直接访问数据的。除非对数据的访问就是这个类及对象的服务。简单地说,给每个成员变量提供一对用于读写的get/set函数也是不合适的设计。
      前面提到,对象总是可以想象为一个鸡蛋,蛋黄是数据,蛋白是操作,(针对对象、数据的)操作紧密把数据包围。这样把数据和对数据的操作放在一起,就叫做封装。操作把数据保护起来,别人不能直接接触到这些数据。
      Java实现这一方式的途径,就是给所有的成员设定一个访问属性,包括privatepublic、缺省表示的friendlystaticprotected。下面进行会依次进行介绍。

2.2.1 private

  只有类的内部才能访问(该成员)。private属性只能放在类的成员变量或成员函数前(而public、friendly可以是类的属性),表明该成员是 所私有的(而不是对象)。对成员变量而言,有2个地方可以访问它,同一类中的 1.成员函数(构造函数、普通函数、main函数),2成员变量的定义初始化(使用其他的、已经定义的成员变量)。
  Java的基本原则是一个class中的成员变量如果没有充足的理由,那就需要给他一个private属性,来保证他的安全。
  多说无益,建一个分数的类实现分数的加法乘法和化简运算,直接学习代码:

package fraction;

public class Fraction {
	private int numerator;  // 分子
	private int denominator;// 分母

	public Fraction(int paraNumerator, int paraDenominator) {
		numerator = paraNumerator;
		denominator = paraDenominator;
		simplify();
	}

	public double toDouble() {
		return ((double)(numerator)) / denominator;
	}

	public void print() {
		if (numerator == 1 && denominator == 1) {
			System.out.println(1);
		} else {
			System.out.println(numerator + "/" + denominator);
		}
	}

	public Fraction plus(Fraction x) {
		int paraNumerator = numerator * x.denominator + denominator * x.numerator;
		int paraDenominator = denominator * x.denominator;
		return new Fraction(paraNumerator, paraDenominator);
	}

	public Fraction multiply(Fraction x) {
		// 可以写成上面的形式,更为清晰和统一;这里形式更为简单
		return new Fraction(numerator * x.numerator, denominator * x.denominator);
	}

	public int greatestCommonDivisor(int a, int b) { // 求a和b的最大公约数,这里采用辗转相除法
		while (b != 0) {
			int k = a % b;
			a = b;
			b = k;
		}
		return a;
	}

	public void simplify() {
		// 最后,Fraction类的化简函数,完成对自己的分数化简,不需要输入和输出,所以是void 函数名()
		int gcd = greatestCommonDivisor(numerator, denominator);
		numerator = numerator / gcd;
		denominator = denominator / gcd;
	}

	public static void main(String[] args) {
		Fraction a = new Fraction(20, 40);
		Fraction b = new Fraction(33, 99);
		a.print();
		b.print();
		a.plus(b).print();
		a.multiply(b).plus(new Fraction(5, 6)).print();
		System.out.println(a.toDouble());
		System.out.println(b.toDouble());
	}

}

  注意到,上面的Fraction类下的成员函数plus(Fraction x)中,调用该函数的对象a访问到了另一个对象x的成员变量,而这些成员变量的访问属性是private,说明了private的访问权限限制是针对类,而不是针对对象,不同的类无法访问各自的private成员,但同一类间的不同对象间,可以访问彼此的私有成员。
  小结:private表明只有这个类内部可以访问。类内部指类的成员函数和定义初始化;这个限制是针对类而不是对对象的。

2.2.2 public

  任何人可以访问。任何人指的是在任何类的函数或定义初始化中可以使用;使用指的是调用、访问或定义变量。比如,若我们把私有变量value的private改成public,那么在类以外的地方,就不需要借助public的getValue、setValue函数来间接读写数据。当然这样是不安全的。

  public类与文件名
  类前面也可以是public,一个public的类,表明任何人都可以用这个类来定义变量。
  注意到,public的类,类的名字与该类所在的.java文件的名字相同。
  一个.Java文件被称为编译单元(compile,compilation),也是Java的源代码文件,编译的时候,一次对这一个编译单元去做编译的动作, java编译后的文件扩展名是.class。实际上,我们可以在一个编译单元里写很多个类,当编译单元不止一个类时,只有与编译单元(.Java文件)同名的这个类可以是public属性,其他friendly的类,只能在这个package里起作用。
  一个public的类必须有个自己的.java文件,且类与文件名相同。

2.2.3 friendly

  和它位于同一个包package的其他类可以访问。此时前面的访问属性修饰词是没有的,缺省的。与public类似的,friendly可以是成员变量、成员函数、类的属性。
  static属性将在类变量中讲解,protected属性将在继承中讲解。

2.3 包

  当程序越来越大的时候,就需要有一个机制帮助管理一个工程中众多的类。包就是Java的类库管理机制,它借助文件系统的目录来管理类库,在使用别人的类库和部署程序时有重要作用。一个包就是一个目录,一个包内的所有的类必须放在一个目录下,那个目录的名字必须是包的名字。
  可以通过直接拖动,将一个包里的类移到移动到另一个包。
  移动后,原本friendly的成员将不再可以访问,因为是不同包了。同时,要用到另一个包里的类时,需要用到import语句,“import 包名.类名;”,import后才可使用引入的类的定义,但不可访问该类中私有或friendly的成员。
  包里的源代码文件的第一行都是 “package 包名;”
  如果不用import语句,那么也可以在需要用到另一个包中的类进行定义初始化时,直接用 “包名.类名” 这种全名full name的形式来定义初始化,如

package clock;
	//import display.Display;
public class Clock {
	private display.Display hour = new display.Display(24);
	private display.Display minute = new display.Display(60);
}

  此外,还可以用 “import 包名.*” 的方式,表示导入包中的所有东西。但是不推荐这样做,因为万一有重名的东西时,可能引起冲突。因此推荐的做法是,在最开始导入需要导入的类的全名。
  同时,类似文件夹的子文件夹,包里可以有子包。全名中包名部分的点就表示了包或者说文件夹的层次。
  有一个类,就要为它安排一个包,前面要写上package 包名;要使用别的包的类,就要import。

2.4 static - 类变量和类函数

  static字面意思是静态的,但在访问属性中,并不是静态 、动态的那个含义,而是属于的意思。static属性只能放在类的成员变量或成员函数前(与private类似,不可以放在类前),表示该成员不再属于每一个对象,而是属于整个类 ,加上static修饰后,该成员称为类变量或类函数。因此就算不new一个对象出来,也可以对 static 的类变量进行操作,类变量的初始化在类的装载(类进入到程序里)时就已完成,static成员的初始化,与后面对象的创建是没有关系的。
  类函数由于不属于任何对象,因此也没有办法建立与调用它们的对象的关系,就不能访问任何非static的成员变量和成员函数了。如果类函数中使用了成员函数和成员变量,而类函数和类变量又可以在没有对象时直接应用,那么成员变量和成员函数必然会因为没有对象而出错。
  若通过一个对象改了类变量的值,那么其他对象访问该类变量时,值也发生变换,因为这个类变量是属于整个类的。通过每个对象(对象名.类变量/函数)都可以访问到这些类变量和类函数,也可以通过类(类名.类变量/函数)来直接访问它们,且能通过类名来访问的成员只能是static的类变量和类函数。
  也即,类函数中只可以访问、调用类变量和类函数,访问有通过对象名和类名两种方式。
  类变量和类函数不是成员变量或成员函数,它们不属于类定义的任何一个对象,它们属于这个类本身;同时,该类的任何一个对象都拥有这个变量,但所有的对象共有那一份,一改全改,这个变量也不在对象里。
  也因此,static方法(也即类函数)没有“ this”关键字,this指代调用的对象实例,而类函数不属于任何一个对象。

第3章 对象容器

  所谓容器,就是“放东西的东西”(一种对象)。容器是现代程序设计非常基础而重要的手段。
  数组可以看作是一种容器,但是数组的元素个数一旦确定就无法改变,这在实际使用中是很大的不足。一般意义上的容器,是指具有自动增长容量能力存放数据的一种数据结构。在面向对象语言中,这种数据结构本身表达为一个对象,所以才有“放东西的东西”的说法。
  Java具有丰富的容器,Java的容器具有丰富的功能和良好的性能。熟悉并能充分有效地利用好容器,是现代程序设计的基本能力。
  在一些书中,将容器(英文为collectioncontainer)翻译为“集合”,由于数学中的 集合(Set) 也是一种特定的容器类型,我们认为将collection翻译为集合是不恰当的。所以我们只会使用容器一词。

3.1 顺序容器

3.1.1 接口设计

  首先学习的是顺序容器,即放进容器中的对象是按照指定的顺序(放的顺序)排列起来的,而且允许具有相同值的多个对象存在。下面以记事本为例。
  记事本该有的特点:

  1. 能存储记录
  2. 不限制能存储的记录的数量,能不断地、任意地增加容量
  3. 能知道已经存储的记录的数量
  4. 能查看存进去的每一条记录
  5. 能删除一条记录
  6. 能列出所有的记录

  根据上面的需求,给出记事本的“接口设计”,也就是有notebook这样一个类,那这个类应该具有那些功能,什么样的接口:

add(String note);getSize();getNote(int index);removeNote(int index);list();

  在设计程序时,一定要避免初学者的一个倾向:将所有的程序、功能部分等都放在一起,读数据计算输出结果一套完成——这样是人机交互部分(输入、输出的地方)和业务逻辑部分(对输入数据进行计算的地方)完全捆绑、交织在一起的。
  真正的软件一定要实现:人机交互的用户界面(即UI,user interface)要和业务逻辑相分离。
  这里的记事本,是业务逻辑,我们考虑的是它该具有什么样的接口,比如这里的add接口,考虑的是拿到了一个字符串,把这个字符串作为一条内容记下来。这里我们不需要考虑该字符串是怎么获得的,在UI中可能有多种途径获得。
  下面插入接口:

public class Notebook {
	
	public void add(String note) {

	}
	//这里省略其他接口
	public static void main(String[] args) {}
}

3.1.2 泛型类

  泛型:把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型,这样用户在使用的时候就不用担心运行时类型强制转换发生异常的问题了1。如下面的ArrayList<E>,E是the type of elements in this list,具体应用时再具体指定。个人理解的是:
泛型一种带有参数的类型,这个参数是一种在定义时未具体指定的类型(后面具体使用时再指定),即泛型是基于一个不确定类型构建出的新类型。
ArrayList<E>中的E称为类型参数变量(即参数是一种类型,称为类型参数,它又是未明确指定的,是变量);
ArrayList<Integer>中的Integer称为实际类型参数,这个具体指定的过程也被称Parameterized参数化;
整个ArrayList<E>称为泛型;
整个ArrayList<Integer>称为ParameterizedType,直译为参数化的类型,理解为类型参数明确化的泛型。

  在上面的的add接口中,怎么把数据存起来,以前的数组是不行的,数组是长度固定的数据结构,无法满足任意增加的需求。因此我们需要用到容器,即泛型容器类
  在Notebook类中用ArrayList<String>定义一个成员变量notes,ArrayList是一个类,被称为泛型类,而<String>是对应的一种泛型,读作“ArrayList of String”,表示用来存放String的一个ArrayList(数组列表、动态数组,Array的复杂版本)。
  ArrayListArray
  Array在定义时,必须指定这个数组的数据类型及数组的大小;而且ArrayList不用指定长度,在不使用泛型的情况下,是可以添加进不同类型的元素的,在使用泛型时,我们就只能添加一种类型的数据了。
  效率上Array高于ArrayList,但Array必须确定数组大小,且在解决更一般化的问题时,Array的功能就可能受限。
  ArrayList<String> notes = new ArrayList<String>;
  容器类有两个类型:容器的类型 + 元素的类型。
  ArrayList的函数
  1. add(定义类型的数据); :按照添加顺序形成索引下标,和数组一样从0开始;
  2. add(下标,定义类型的数据); :在指代下标位置插入数据;
  3. get(下标); :根据下标返回对应的数据;
  4. size(); :返回数组列表的元素总数;
  5. remove(下标); :删除后会返回删除的该条数据,而不是是否删除的boolean或无返回;
  6. toArray(数组变量); :将现有的动态数组填充到一个长度相同的数组,将该数组输出即理出了所有记录。
  尽管上面的函数或许可以通过别的方式实现,比如toArray可以通过写一个for循环输出,但是充分理解系统给出的类库,知道它具有的函数和用法,能简化思考、代码和工作量,同时提高运行效率。
  最后,学习代码:

package notebook;import java.util.ArrayList;

public class Notebook {

	private ArrayList<String> notes = new ArrayList<String>();

	public void add(String note) {notes.add(note);}

	public void add(int location, String note) {notes.add(location, note);}

	public int getSize() {return notes.size();}

	public String getNote(int index) {return notes.get(index);}

	public void removeNote(int index) {notes.remove(index);}

	public String[] list() {
		String[] a = new String[notes.size()];
//		for (int i = 0; i < notes.size(); i++) {a[i] = notes.get(i);}
		notes.toArray(a);return a;}

	public static void main(String[] args) {
		Notebook note1 = new Notebook();
		note1.add("first");
		note1.add("second");
		note1.add(1, "third");
		System.out.println(note1.getSize());
		System.out.println(note1.getNote(1));
		System.out.println(note1.getNote(2));
		System.out.println("-------------------");
		note1.removeNote(1);
		String[] a = note1.list();
		for (String string : a) {
			System.out.println(string);
		}
	}
}

3.2 对象数组

  当数组的元素的类型是类的时候,数组的每一个元素其实只是对象的管理者而不是对象本身。因此,仅仅创建数组并没有创建其中的每一个对象。

  在前面我们给出了 String[] a = new String[notes.size()]; ,这里的String[] a就是一个对象数组,和普通数组不同,对象数组的每一个元素a[i]不是普通类型的数据。
  先举个数组元素类型是基础数据类型的普通数组的例子: int[] b = new int[10];
  首先,int[] b得到的b是数组变量,new int[10]创建的才是数组本身(但不记作int[10]),数组变量b才是数组的管理者,这很容易混淆,尤其是在后面与对象数组作对比时。注意:
  1:普通数组中的每个元素是一个基础类型数据的所有者,而对象数组的每个元素是对象的管理者(数据的所有者可理解为数据本身,若把基础类型视为包裹类,即基础类型的数据变为一个对象来使用,那么普通数组的每个元素可以对照着理解为对象本身,但注意并不是对象本身,因为基础类型数据对象化,那就是对象数组了);
  2:在这里,b是数组变量,是管理者;b[i]是元素(变量),但它是所有者、是基础类型的数据本身,即b[i]就是一个int型数据,两个基础数据作 “==” 的比较时,只比较值,而对象是比较内存位置;(为了方便,在很多不必进行区分的时候,一般将数据和表示数据的变量视为同一,比如这里的元素数据和元素变量,以及存在实质差异的数组变量和数组数据。但管理者、所有者关系还是需要理清)
  现在给出一个对象数组,为了简单和对比,就给出包裹类(完全可以根据需求给出类,如后面练习代码中的Value): Integer[] c = new Integer[10];
  显然,(对象)数组变量c是管理者(对整个数组的);不同的是,对象数组的元素c[i]也是管理者(对应元素、相应类型的对象的) 。所以:
  new int[10]   得到的普通数组是一个10个基础类型的数据的所有者 的数组
  new Integer[10] 得到的对象数组是一个10个包裹类型的对象的管理者 的数组

  因此,普通数组和对象数组有下面具体的差异:

  1. 初始化的差异。
  new Integer[10]创建了10个管理者,但是没有实体,所以初始化并没有彻底完成,还需给出10个Integer的对象。即对象数组的初始化时除了new一个对象数组,还需初始化10个具体的对象(包裹类可直接给出,如字符串,其他类还要new出来)。而new int[10]会自动初始化为一个数组的实体[0,0,…,0]。

  2. for each循环的差异。
  在普通数组中,如 int[] a = new int[10]; for ( int k : a ) { k = 0;k++;k+=1; } 是没有实际意义的。两方面原因:
  1. 因为上面的每轮循环可拆解为:int k = a[i]; k = 0;,注意此时k是这一轮对a[i]数值的复制品,所以任何与k有关的操作均与a[i]无关,for each循环结束,k没了,普通数组a毫无变化
  2. 另一方面,即使是在Integer类型对象数组中,k = 0;和k++;k+=1; 内存地址也必然发生改变,也即创建出新的数据来交给k管理,k对a[i]的管理权被覆盖,所以这里的对k的操作实际也与对象数组无关
  具体来说,k=0是新建一个对象Integer 0(注意不是int 0,否则就是给k赋值基础类型数据0,这里是给对象管理权限),交给k来管理;k++;和k+=1;是变量+数据的形式,这样也一定会新建一个不同的对象来交给k来管理(即使已有形式完全相同的数据),所以这里是创建了新的对象,分配了新的内存,因此不影响之前管理的对象数组。
注:

String str1 = new String("ABC");
String str2 = new String("ABC");
System.out.println(str1.equals(str2)); // true 比较的是内容。
System.out.println(str1 == str2); // false 比较的是内存地址。

// 会先创建一个不同于前面的str1和str2的对象,在string池中,该内存地址被分配给str3,str4,str5
String str3 = "A" + "B" + "C";
String str4 = "A" + "BC";
String str5 = "ABC";
System.out.println(str3 == str4); // true 
System.out.println(str3 == str5); // true
System.out.println(str1 == str3); // false

String str6 = "AB";
String str7 = str6 + "C";
System.out.println(str3 == str7); // false 即变量+字符串会产生新的字符串,内存地址不同于变量代表的字符串来相加。

  在对象数组中,定义一个Value类,set(int a)是赋值,get()是返回。则对于 Value[] a = new Value[10] &初始化10个对象(略);  for ( Value k : a ) { k.set(0); } 这时是有实际意义的。
  上面的每轮循环可拆解为:Value k = a[i];,和 k.set(0);。又注意到,对象数组的元素是对象的管理者,所以k=a[i]是k共享a[i]管理的对象的管理权;k.set(0)不会创建新的内容,管理权不会迁移,内存不会发生改变,则a[i]=0。而下一轮,k=a[i+1],k变为去管理a[i+1]对应的对象,k.set(0);则a[i+1]=0,所以for each循环结束,这里数组a全为0了,即对象数组因每轮k的变化而整体改变
  类似的,如果把对象数组换成对象的动态数组,即ArrayList<元素的类>,也有上面的差异:每一个元素只是对象的管理者而不是对象本身;1. 元素是管理者,2. for each循环可以用,且与对象数组相同,每轮循环k迁移至不同对象的管理权,但要小心对k赋值很可能使k的管理的内容发生改变,使没达到预期的对于对象数组或动态数组、集合、散列表等的操作。
  当然,ArrayList new出来后可以不对每个元素初始化,此时管理者和实体都是没有的。后面需要添加元素时,add函数即可添加实体和管理者的序号(也可循环add初始化)。
  代码学习:

package notebook;

import java.util.ArrayList;

class Value {
	private int i;
	public void set(int parai) { i = parai; }
	public int get() { return i; }
}

public class Notebook {
	public static void main(String[] args) {
//  test1,为了弄清管理权的变化,将for each循环改写成完整的for循环
	char[] a = new char[2];
	for (int i = 0; i < a.length; i++) { a[i] = (char) ('a'+ i); }
	System.out.println(a);
	
	for ( int i =0; i < 2 ; i++ ) {
		char k = a[i]; k='b'; System.out.println(k == a[i]); 
		//可以看到,基础数据类型“==”只是比较值,而非内存地址
	}
	
//	test2
	System.out.println("……………………2……………………");
	Value[] b = new Value[10];
	for (int i = 0; i < b.length; i++) {
		b[i] = new Value(); b[i].set(i); }

	for (Value value : b) { System.out.print(value.get()+"\t"); 
		value.set(0); }
	
	System.out.println("");
	for (Value value : b) { System.out.print(value.get()+"\t"); }
	System.out.println("");

//	test 3
	System.out.println("……………………3……………………");
	ArrayList<String> c = new ArrayList<String>();
	for (int i = 0; i < 2; i++) { c.add("a"+i); }
	System.out.println(c);
	
	for (String s : c) { String k = s + "a\t"; System.out.print(k); }
	System.out.println();
	System.out.println(c);
	}
}

  输出结果:

ab
false
true
……………………2……………………
0	1	2	3	4	5	6	7	8	9	
0	0	0	0	0	0	0	0	0	0	
……………………3……………………
[a0, a1]
a0a	a1a	
[a0, a1]

  Java支持传递任意数量的参数的函数,实现方式是在参数最后一个类型处添加“”,代码:

public class Notebook {
	public int[] f(int... i){ //注意这里返回的是数组,及参数是类型后的 ... 
	return a; }
	public static void main(String[] args) {
	Notebook n = new Notebook();
	int[] a = n.f(1,2,3,4);//任意个数都可以
	for ( int k : a ) { System.out.println(k); }
}

3.3 集合容器 (Set)

  集合就是数学中的集合的概念:所有的元素都具有唯一的值,元素在其中没有顺序
  格式与ArrayList类似,直接学习代码:

	HashSet<String> s = new HashSet<String>();
	s.add("first");
	s.add("second");
	s.add("first");
	for ( String k : s ) { System.out.println(k); }  //输出结果:first和second
	System.out.println(s);   //输出结果:  [second, first] 

  应该注意到,容器ArrayList和Set均可以直接输出,实际上上面print语句中的print(s)是print(s.toString()),也就是这两个容器类,内置了toString()函数,可以直接输出,格式是[ 元素1 ,元素2,…]。
  显然,因为HashSet 或 Set 是 “包含沒有特定順序的不同元素”的集合,所以不能用get()函数来获得某个位置上的元素,因为元素不存在位置的概念。如果要根据位置索引元素,则应该用数组或List。

3.4 散列表 (Hash)

  传统意义上的Hash表,是能以int做值,将数据存放起来的数据结构。Java的Hash表可以以任何 实现了hash()函数的类 的对象做值来存放对象。
  Hash表是非常有用的数据结构,熟悉并充分使用它,往往能起到事半功倍的效果。
   在英文中,一个硬币的面额对应着一个单词,如1分钱对应penny,5-nickel,10-dime,25-quarter,50-half dollar。如何做一个查找数字对应单词的程序?
  先定义接口,在考虑实现技术。
  新建coins包,定义Coin类,接口满足给出一个int数字返回一个String的单词。
  再来实现技术。当然我们可以选择使用switch case,但是不够优雅,我们希望体现在代码中硬编码的东西应越少越好。我们用HashMap这种容器来实现。
  HashMap是以一对的形式存放数据的数据结构,一个键key对应一个值Value。和其他容器一样,这是个面向对象的世界,存放的数据是各类的对象,不是基础数据,因此定义时的类型是类不是基础类型,如HashMap<Integer,String>中是Integer而不是int。
  在Coin类中,我们由HashMap这种容器类定义一个成员变量coinNames,通过构造函数用该容器实现初始化,自动载入数据。
  对于Hash表来说,键是唯一的,所以在一个键放多个值,只会保留最后一个值。
  HashMap具有的函数有:
  1 put(k,v):储存key和value的数据对,并返回覆盖该key前的上一个value,如果没有返回null;
  2 get(k):返回key对应的value;
  3 containsKey(k):返回是否包含k的boolean;
  4 toString():返回形如 {key1=value1,key2=value2,…} 的字符串;
  5 keySet():得到Hash表所有键组成的集合;又键的个数等价于数据对的个数,所以进一步由集合的size()函数即keySet().size(),得到Hash表的映射关系的个数。
  类似的,如果要遍历整个Hash表,直接对keySet()集合进行for each循环即可。
  下面直接学习代码:

package coins;

import java.util.HashMap;
import java.util.Scanner;

public class Coin {
	private HashMap<Integer,String> coinNames = new HashMap<Integer,String>();
	
	Coin(){
		coinNames.put(1, "thePreviousValue or null");
		System.out.println(coinNames.put(1,"penny"));
		coinNames.put(5, "nickel");coinNames.put(10, "dime");
		coinNames.put(25, "quarter");coinNames.put(50, "half-dollar");
		//测试
		System.out.println(coinNames.keySet());
		System.out.println(coinNames.keySet().size());
		System.out.println(coinNames);
		for (Integer k : coinNames.keySet()) {
			String s = coinNames.get(k); System.out.print(s+" "); }
	}
	
	public String getName0(int amount) {
		//String a;
		 String a = "Not found";
		switch (amount) {
		case 1:	a = "penny"; break; case 5: a = "nickel"; break; case 10: a = "dime"; break;
		case 25: a = "quarter"; break; case 50: a = "half dollar"; break;//default: a = "Not found"; break;
		} return a;
	}
	
	public String getName1(int amount) {
		if (coinNames.containsKey(amount)) {return coinNames.get(amount);
		} else { return "Not found"; }
	}

	public static void main(String[] args) {
		Coin a = new Coin();
		
		System.out.println();
		System.out.println(a.getName0(25));
		System.out.println(a.getName0(2));
		System.out.println("……分……割……线……");
		Scanner input = new Scanner(System.in);
		int key = input.nextInt();
		System.out.println(a.getName1(key));
	}
}

  输出结果:

thePreviousValue or null
[1, 50, 5, 25, 10]
5
{1=penny, 50=half-dollar, 5=nickel, 25=quarter, 10=dime}
penny half-dollar nickel quarter dime 
quarter
Not found
……分……割……线……
1
penny

第4章 继承与多态

4.1 继承

  面向对象程序设计语言有三大特性封装、继承和多态性。继承是面向对象语言的重要特征之一,没有继承的语言只能被称作“使用对象的语言”。继承是非常简单而强大的设计思想,它提供了我们代码重用程序组织的有力工具。
  类是规则,用来制造对象的规则。我们不断地定义类,用定义的类制造一些对象。类定义了对象的属性和行为,就像图纸决定了房子要盖成什么样子。
  一张图纸可以盖很多房子,它们都是相同的房子,但是坐落在不同的地方,会有不同的人住在里面。假如现在我们想盖一座新房子,和以前盖的房子很相似,但是稍微有点不同。任何一个建筑师都会拿以前盖的房子的图纸来,稍加修改,成为一张新图纸,然后盖这座新房子。所以一旦我们有了一张设计良好的图纸,我们就可以基于这张图纸设计出很多相似但不完全相同的房子的图纸来。
  基于已有的设计创造新的设计,就是面向对象程序设计中的继承。在继承中,新的类不是凭空产生的,而是基于一个已经存在的类而定义出来的。通过继承,新的类自动获得了基础类中所有的成员,包括成员变量和方法,包括各种访问属性的成员,无论是public还是private。当然,在这之后,程序员还可以加入自己的新的成员,包括变量和方法。显然,通过继承来定义新的类,远比从头开始写一个新的类要简单快捷和方便。继承是支持代码重用的重要手段之一。
  类这个词有分类的意思,具有相似特性的东西可以归为一类。比如所有的鸟都有一些共同的特性:有翅膀、下蛋等等。鸟的一个子类,比如鸡,具有鸟的所有的特性,同时又有它自己的特性,比如飞不太高等等;而另外一种鸟类,比如鸵鸟,同样也具有鸟类的全部特性,但是又有它自己的明显不同于鸡的特性。
  如果我们用程序设计的语言来描述这个鸡和鸵鸟的关系问题,首先有一个类叫做“鸟”,它具有一些成员变量和方法,从而阐述了鸟所应该具有的特征和行为。然后一个“鸡”类可以从这个“鸟”类派生出来,它同样也具有“鸟”类所有的成员变量和方法,然后再加上自己特有的成员变量和方法。无论是从“鸟”那里继承来的变量和方法,还是它自己加上的,都是它的变量和方法。
  下面把媒体资料库(Database of Media,简写为DoME)从头到尾实现一遍,重点在于弄清楚子类和父类的关系,理解子类从父类继承得到了什么。

  在之前,我们做了一个记事本,现在类似地,我们做一个媒体资料库,可以画类的示意图,这里文字叙述如下。Database类包含如下成员:成员变量:一个装CD的容器Arraylist<CD> listCD;成员函数:添加CD,列出所有已有CD,…。不同于记事本的是,这里放的不是字符串而是定义的CD类,所以要在同一个database包里定义第二个类CD,CD成员包括:变量:描述CD的名称、艺术家、音轨数(即歌曲数)、播放时长,其他说明如是否借出、评论描述等;函数:通过前面的参数录入CD的构造函数。其他的暂时想不到的就先不管,后面发现需要时再添加。(比如在Database的列出函数时,需要CD有个列出函数)
  上面的过程是最基础和重要的,有了上面的框架后,代码也就自然就有了。
  代码省略。
  有了CD后,这个媒体资料库还可以放DVD等,那么无非是多建一个新的DVD类,重复上面的过程,写代码的过程可以发现,只要是新建DVD、MP3等类的方式来扩充媒体库,那么必然会有大量的代码重复(复制),而重复的原因在于我们设计的过程就是重复的过程,我们没有去理解和实现它们的共性和差异。

  为了解决这样冗余的问题,就需要用到 继承

  代码复制,是代码质量不良的一种表现。代码中出现代码复制,意味着这样的代码以后是不容易维护的,比如若需要改同一个函数,则所有的类的该函数都要改。另一方面,这样的代码不具有可扩展性,若增加一个Word的类只能再复制一遍代码。
  我们提取上面的DVD、CD等类的共有部分,做一个基类(父类、超类、SuperClass),命名为Item。然后在DVD、CD后加extends关键字DVD extends Item 表示DVD扩展了Item,也就是DVD继承于Item,是Item的扩展类、派生类、子类。在有基类Item前,Database中的结构关系是:Database类有CD、DVD等类的多个容器;有基类后,Database的关系是Database有一个Item的容器,Item派生CD、DVD等扩展类。

  PS:推荐用基类、扩展类的说法来理解,用父类、子类来说(因为辨识度更高……就这么扯淡,后面用了一小段的基类扩展类,不如父类子类简单粗暴)。父类不如基类更能表达这是作为基础的类的含义;而子类的 “子” 容易和子集的 “子” 相混淆,尽管都要源自父类、原集合的意思,但二者是不同的,因为子集一定包含于原集合,而子类父类二者无必然的包含关系。比如:水果(父类)包含苹果(子类),但水果(父类)派生出不完全是水果的西红柿(子类),再比如A、B、C的交集D作为基类,派生出A、B、C,这时子类包含父类。子类父类的选取是以计算机角度上的减少代码冗余和方便以后维护为目的的,而不完全是数理、逻辑、认知先后的角度来判定的。
  ”子“类只是继承关系(派生、扩展),不能混淆为”子“集的包含关系。

  继承后,基类中的所有东西在扩展类中都存在,但是不一定能使用,还涉及到访问权限的问题。
  继承表达了一种is-a关系,即A继承B有“A is a B”的意思,即“是一个,视为同一”,苹果(扩展类)是个水果(基类)。也就是说,子类的对象可以被看作是父类的对象,但反过来不行,不能说水果是一个苹果。此外还有has-a关系等,即“A has a B”。has-a 是一种组合关系,是关联关系的一种(一个类中有另一个类型的实例),是整体和部分之间的关系,以后遇到再进一步分析。
  Java的继承只允许单继承,即一个类只能有一个父类。
  代码学习,几个类就不粘贴上来了。

4.2 子类父类关系

  对理解继承来说,最重要的是,知道子类从父类那里得到了所有的东西,所有的父类的成员,包括变量和方法,都成为了子类的成员,除了构造函数。构造方法是父类所独有的,因为它们的名字就是类的名字,所以父类的构造方法在子类中不存在(但构建子类对象时可以调用到)。除此之外,子类继承得到了父类所有的成员。
  但是得到不等于可以随便使用。每个成员有不同的访问属性,子类继承得到了父类所有的成员,但是不同的访问属性使得子类在使用这些成员时有所不同:有些父类的成员直接成为子类的对外的界面,有些则被深深地隐藏起来,即使子类自己也不能直接访问。下表列出了不同访问属性的父类成员在子类中的访问属性:

父类成员访问属性在父类中的含义在子类中的含义
public对所有人开放对所有人开放
protected包内的所有类和子类可以访问包内的所有类和子类可以访问
default(缺省)只有包内的类可以访问如果子类与父类在同一个包内,可以访问。否则,相当于private,不能访问
private只有自己可以访问不能访问

  public的成员直接成为子类的public的成员,protected的成员也直接成为子类的protected的成员。Java的protected的意思是包内和子类可访问,所以它比缺省的访问属性要宽一些。而对于父类的缺省的未定义访问属性的成员来说,他们是在父类所在的包内可见,如果子类不属于父类的包,那么在子类里面,这些缺省属性的成员和private的成员是一样的:不可见。父类的private的成员(不管是变量还是函数)在子类里仍然是存在的,只是子类中不能直接访问

  我们不可以在子类中重新定义继承得到的成员的访问属性。如果我们试图重新定义一个在父类中已经存在的成员变量,那么我们是在定义一个与父类的成员变量完全无关的变量,在子类中我们可以访问这个定义在子类中的变量,在父类的方法中访问父类的那个尽管它们同名但是互不影响。

  在构造一个子类的对象时,父类的构造方法也是会被调用的,而且父类的构造方法在子类的构造方法之前被调用。在程序运行过程中,子类对象的一部分空间存放的是父类对象。因为子类从父类得到继承,在子类对象初始化过程中可能会使用到父类的成员。所以父类的空间正是要先被初始化的,然后子类的空间才得到初始化。在这个过程中,如果父类的构造方法需要参数,如何传递参数就很重要了。

  我们可以把DVD、CD中相同的成员放进Item里,但是要注意访问属性,比如父类中的private只有父类可见,子类中有但是不可见。解决父类private成员的2个方案,1:将private改为protected,由仅自己可访问变为包内和子类可访问;2:在定义初始化时,不改变private,在父类中使用这个(些)成员变量,在子类中通过super(成员变量)传过来。

  super()表示调用父类的不带参数的构造函数,同理super(成员变量)表示调用带这个(些)参数的构造函数。super(参数1,参数2,0,false,…)时可以直接代入某些参数的初始值完成赋值,另一些参数仍以参数表示,由子类的构造函数给出。

  super将基类与扩展类的构造函数联系起来,在通过扩展类的构造函数进行定义初始化时,通过以下顺序完成初始化,1通过扩展类的构造函数中的super(成员变量)进入基类带这些成员变量的构造函数里,2离开基类构造函数进入定义初始化部分,3回到基类构造函数继续初始化,4离开基类进入扩展类的定义初始化,5回到扩展类的构造函数继续剩下的初始化。即定义顺序是:父类定义初始化,父类构造函数,子类定义初始化,子类构造函数。
  子类中的构造函数如果不需要调用带参数的父类构造函数,即子类构造函数第一行是super(),则可以省略,会隐式调用,即没有选用带参数的父类构造函数时会默认调用不带任何参数的构造函数super()。
  另一方面,子类的构造函数一定会调用父类的构造函数。如果每个子类不是选定带某些参数的父类构造函数,那么父类中必须有一个不带参数的构造函数,因为那个没有要求的子类会隐式调用super()来调用父类的无参数构造函数。

  前面提到可以定义一个父类中已经存在的成员变量,这时候子类中可以同时存在和出现两个同名的私有成员变量,但是一个是子类的一个是父类的,且子类中属于自己的函数(或初始化时的变量)只会访问属于子类的那个同名变量;而继承的子类调用父类的公有成员函数时,继承于父类的函数可以且只会访问父类中的同名私有变量
  也即尽管父类的私有变量,子类不可见,但可以通过父类的函数进行操作。父类子类出现同名的私有变量,出现在父类的函数里,这个变量就是父类的,出现在子类的函数里,变量就是子类的,二者同名同时出现但互不影响。
  如果出现了同名的公有的成员(如公开函数,protected的变量),子类中也可以通过 super. 的前缀来调用父类的成员。一般来说,成员变量应尽量保持private,protected是实在没有办法的办法。

  一个包内4个java文件,代码学习如下:

package database;
import java.util.ArrayList;
public class Database {
	private ArrayList<Item> listItem = new ArrayList<Item>();
	
	public void add(Item item) {listItem.add(item);}
	
	public void list() {
		for (Item item : listItem) {item.print();}
	}
	
	public static void main(String[] args) {
		Database db = new Database();
		CD cd0 = new CD("东风破","周杰伦",8,36,"fantastic");
		CD cd1 = new CD("冰雨","刘德华",6,28,"wonderful");
		DVD dvd0 = new DVD("新警察故事","成龙",128,"gorgeous");
		DVD dvd1 = new DVD("功夫","周星驰",120,"perfect");
		db.add(cd0);  db.add(cd1);
		db.add(dvd0); db.add(dvd1);
		db.list();
	}
}

父类

package database;
public class Item {
	private String title;
	private int playingTime;
	private boolean gotIt;
	private String comment;

	public Item(String title, int playingTime, boolean gotIt, String comment) {
		super();
		this.title = title;
		this.playingTime = playingTime;
		this.gotIt = gotIt;
		this.comment = comment;
	}

	public void print() { System.out.println(title); }

	public static void main(String[] args) { }
}

子类1:CD

package database;
public class CD extends Item {
	private String artist;
	private int numOfTracks;
	
	public CD(String title, String artist, int numOfTracks, int playingTime, String comment) {
		super(title, playingTime, false, comment);
		this.artist = artist;
		this.numOfTracks = numOfTracks;
	}

	public void print() {
		System.out.print("CD : " + artist + " - ");
		super.print();
	}
	
	public static void main(String[] args) { }
}

子类2:DVD

package database;
public class DVD extends Item {
	private String director;

	public DVD(String title, String director, int playingTime, String comment) {
		super(title, playingTime, false, comment);
		this.director = director;
	}

	public void print() {
		System.out.print("DVD: " + director + "作品 - ");
		super.print();
	}

	public static void main(String[] args) { }
}

结果

CD : 周杰伦 - 东风破
CD : 刘德华 - 冰雨
DVD: 成龙作品 - 新警察故事
DVD: 周星驰作品 - 功夫

4.3 多态变量和向上造型

  类定义了类型,DVD类所创建的对象的类型就是DVD。类可以有子类,所以由那些类定义的类型可以有子类型。在DoME的例子中,DVD类型就是Item类型的子类型。
  子类型类似于类的层次,类型也构成了类型层次。子类所定义的类型是其父类的类型的子类型。
  当把一个对象赋值给一个变量时,对象的类型必须与变量的类型相匹配,如: Car myCar = new Car(); 是一个有效的赋值,因为Car类型的对象被赋值给声明为保存Car类型对象的变量。但是由于引入了继承,这里的类型规则就得叙述得更完整些:

   一个变量可以保存其所声明的类型或该类型的任何子类型。对象变量可以保存其声明的类型的对象,或该类型的任何子类型的对象。

  Java中保存对象类型的变量是多态变量。“多态”这个术语(字面意思是许多形态)是指一个变量可以保存不同类型(即其声明的类型或任何子类型)的对象

  在DoME的例子中,我们定义的是Item类型的add函数,而实际上我们给的是DVD、CD,即Item的子类型(的对象)。我们总是制造了Item的某个子类的对象,然后把它交给add函数,而add函数的参数表说要的是一个Item。这是可行的,因为:
  类定义了类型,子类定义了子类型;子类的对象可以被当作父类的对象来使用:
a.赋值给父类的变量(如new出一个DVD对象赋值给一个Item变量),b.传递给需要父类对象的函数(如add),c.放进存放父类对象的容器里(如将CD对象放进Item的Arraylist动态数组)。

  也即,在需要用到时,子类型的对象会被自动 视为 父类的类型,注意不是 转换为。和基础数据类型的类型转换作对比,子类对象视为父类对象的过程,Java语言和强制类型转换相同,如DVD视为Item:(Item)dvd,对比强制转换一个数:(int)10.2。格式相同,但本质不同,将子类对象视为父类对象,这个过程不改变转换后的类型,即(Item)dvd是可以被系统视作Item类型(进行相关操作)的DVD类的对象,而(int)10.2就是int类型的10了。因此,在英语中,这两个过程都被称为cast,也比较类似,但又存在不同。在基础类型数据及变量中,我们称之为(强制)类型转换;在任意类的对象或对象变量中,我们称之为造型,可以理解为塑型、视为、拟转换的意思。

  Java的(所有的)对象变量都是多态的,即能保存不止一种类型的对象。我们认为任何一个对象变量(也就是多态变量)实际上有两个类型,一个类型是显式的声明类型(或者说静态类型:字面上可根据声明直接看出的类型),另一个是隐式的动态类型(程序运行到这里的时候,该对象变量管理的实际类型),二者可能一致也可能不同(如父类的对象变量管理子类型的对象)。它们可以保存的是声明类型的对象,或声明类型的子类的对象。
  再回顾一次,多态变量的意思是,这个变量运行的时候,在具体某一个时刻它所管理的那个对象的类型是会变化的。

  当把子类的对象赋给父类的变量的时候,就发生了向上造型

  造型 (cast),就是把一个类型的对象赋给另一个类型的变量的过程。
  前面指出子类的对象可以赋值给父类的变量,但是,父类的对象不能赋值给子类的变量

此外,特别注意:Java中不存在对象给对象的赋值。(几乎所有的OOP语言都是这样,除了改造得不够完全的OOP语言:C++)

  Java中,对象是交给相应的对象变量来管理,不存在拿一个对象给另一个对象赋值(比如不存在“a”=“b”,赋值“=”的左侧一定是变量),而是把"=“右侧的对象的管理权交给左侧,或或者让”="两侧的两个对象的管理者去管理一个共同的对象。

  example: String s = “hello”; s = “bye”;

  这个过程是先把字符串 "hello"的管理权交给变量s,再用对象"bye"的管理权覆盖掉s原有的 "hello"的管理权(也就是s前面管“hello”,后面不管“hello”转去管“bye”了)。所有 "hello"并没有被赋值、改变,s管理的内容发生了变化,内存地址发生了改变(3.2对象数组中也提到和说明了这点)。Java不能做用“bye”替换“hello”这种事。可以用跟踪变量()和“==”判断来证明上面的论述:

String s0 = "a";
String s1 = s0;
System.out.println(s0 == s1);  // 结果为true
s1 = "b";
System.out.println(s0 == s1);  // 结果为false
System.out.println(s0 );       // 结果为a

  上面旨在说明,第二行s1 = s0时二者共有“a”的管理权,第四行将s1赋值“b”,这时候不是共享管理权的s0跟着变为“b”,而是二者取消共享关系,s1自己管“b”去了,二者内存地址不再一致。
  若用跟踪变量,在第四行设置断点,可以看到s1的前后id标识发生变化,也就是不是“b”替换/修改掉“a”(对象“b”给对象“a”的赋值),而是不同对象的管理权的交接。
  赋值运算符“=”就是让这个变量指向了另一个对象,去管理另一个对象。

  这个过程,就可能发生 造型。
  让某静态类型的变量管理了一个动态类型与静态类型不一致的对象,就会发生造型。如

Vehicle v;   // Vehicle有子类Car
Car c = new Car();
v = c;   //这是可以的
c = v;   //这会出现编译错误

  可以用造型来解决上面c = v;的编译错误:c = (Car) v;,运行不出错还需v这个变量实际管理的是Car类型才行。
  上面第三行v = c;没出错,因为实际上发生了向上造型,向上就是向父类,造型就是实际上发生了v = (Vehicle)c;。
  向上造型是默认的,总是安全的,不需要运算符,当然写出来也可以。

  通过变量追踪可以看到:向上造型的过程,赋给父类变量v的(Vehicle)c对象仍然是子类型Car,此时父类变量的动态类型和静态类型(父类)不一致。

  造型:

  • 基本类型的类型转换和对象类型的造型,英语都是cast,格式类似,但中文不同;
  • 用括号围起类型放在值的前面,如Vihicle(car),CD(item);
  • 造型是把对象当成另一个类型的对象来看待,并没有改造成另一个类型,对象本身(注意不是管理对象的变量,见例)并没有发生任何变化,所以不是“类型转换”
  • 运行时有机制来检查这样的转化是否合理,classCastException。

  向上造型总是安全的,但一般情况下的造型不总是安全的取决于变量管理的实际类型(动态类型)与造型后的类型是否一致,一致则安全可行。
例:

Car c1 = new Car();   // Car是Vehicle的子类
Vehicle v = c1;       // 向上造型,等价于Vehicle v = (Vehicle)c1; 
c2 = (Car)v;          //安全的向下造型

  这里第二行将c1赋给v,实际上是将一个car类型的对象交给v管理,v的静态类型是Vehicle,动态类型是Car,向上造型没有改变c1和v共同管理的对象的类型,v管理的实际是一个Car对象;第三行(car)v是将对象变量v造型为car,追踪代码可知,c2管理的是一个Car而不是Vehicle,表面上看(Car)v 使得v的类型发生了变换(由Vehicle变为Car),但仔细看,这里实际上是v管理的对象的管理权限又交给c2了,即c1、v和c2共享一个对象的管理权(追踪变量可知,三者的标识id相同),这个对象的类型一直是Car,对象本身没有发生任何变换,即造型不改变所管理的对象的类型,改变的只是管理者的静态类型

4.4 多态

  如果子类的方法**覆盖(override)**了父类的方法,我们也说父类的那个方法在子类有了新的版本或者新的实现。覆盖的新版本具有与老版本相同的方法签名:相同的方法名称和参数表。因此,对于外界来说,子类并没有增加新的方法,仍然是在父类中定义过的那个方法。不同的是,这是一个新版本,所以通过子类的对象调用这个方法,执行的是子类自己的方法。

  覆盖关系并不说明父类中的方法已经不存在了,而是当通过一个子类的对象调用这个方法时,子类中的方法取代了父类的方法,父类的这个方法被“覆盖”起来而看不见了。而当通过父类的对象调用这个方法时,实际上执行的仍然是父类中的这个方法。注意我们这里说的是对象而不是变量,因为一个类型为父类的变量有可能实际指向的是一个子类的对象。

  当通过对象变量调用函数的时候,调用哪个函数,这件事情叫做绑定。即绑定表明了调用一个方法的时候,我们使用的是哪个方法。绑定有两种:一种是静态绑定,这种绑定由变量的声明类型来决定,在编译的时候就确定了;另一种是动态绑定 ,这种绑定由变量的动态类型来决定,即在运行的时候根据变量当时实际所指的对象的类型动态决定调用的方法——这就是多态Java默认使用动态绑定

  在成员函数中调用其他成员函数也是通过缺省的this这个对象变量来实现的,在运行时才能确定具体对象类型,进而调用对应的方法(父类方法或子类里覆盖的方法),即成员函数的调用也都是动态绑定。

  也即,通过父类的变量调用存在覆盖关系的函数时,会调用变量当时所管理的对象所属的类的函数。

  再回顾一次:通过一个变量写了一个函数,我们无需通过if、switch case等去判断变量的实际类型是什么,在运行时系统自动地根据变量的动态类型选择调用的方法,这就是多态。

4.5 类型系统

  Java实现了一个单根(root)结构,即Java中所有的类一定都是一个叫Object的类的子类,这个Object就是Java类型系统中的root。几乎所有的OOP语言都实现了这样的单根结构,除了C++。所以,所有的类都继承自Object类,这里着重说明一下继承的两个函数:toString()和equals()
  toString()是任何类直接输出(缺省)时的调用方法,没有覆盖的情况下,默认输出的字符可以理解为是对象的 项目名.类名@内存地址
  我们可以写一个新的toString覆盖掉原有的默认的toString,这个过程可以用右键在源码source中选择生成toString来快速创建。

  以前有提到,对于对象变量,“=="只能用于判断这两个管理者是否管理着同一个对象。如果要判断变量所管理的对象的内容是否一致,就需要用到equals()函数。但是,Object的equals()函数也是判断两个管理者是否管理着同一个对象。Object类中的equals方法:public boolean equals(Object obj) { return (this == obj); }

  特别说明的是,String类很特殊 ,String类对Object的equals方法进行了重写:先用“==”判断对象的内存地址是否相等,相等则返回true。不等则接着判断括号内的对象上是否是String类型,若是则接着判断两个字符串对象的的长度是否相等,若是则最后判断内容是否相等,如果都相等则返回true,否则false。即String的equals是内容上的相等。具体为:

	public boolean equals(Object anObject) {
		if (this == anObject) {
			return true;
		}
		if (anObject instanceof String) {
			String anotherString = (String) anObject;
			int n = value.length;
			if (n == anotherString.value.length) {
				char v1[] = value;
				char v2[] = anotherString.value;
				int i = 0;
				while (n-- != 0) {
					if (v1[i] != v2[i])
						return false;
					i++;
				}
				return true;
			}
		}
		return false;
	}

  上面String的equals()方法就是一个具体的例子,“==”只能判断是否同一引用,判断内容是否相同需要自己重写equals()函数,或转化到String类的对象的equals()上。
  显然,基本数据类型不是类,没有equal方法,可直接用==比较内容。类似的,八种基本类型的包裹类的equals()方法和String一样进行了覆盖,比较的是内容。
  综上,String和八种基本数据类型的包裹类都覆写了equals()方法,equals()比较的是内容;其他引用数据类型如果没有覆写equals(),则判断是否指向同一个引用。

  自己重写equals()函数时,同样的,可以右键源码source,选择覆盖override/实现implement方法methods来快速创建,它可以选择父类中的方法进行覆盖重写。

  在以Item为父类的框架下,前面的多媒体资料库增加一种新的多媒体类型就很简单了,不需要改动已有的代码,只需要新增一个新的类(Item的子类)即可。具体的实现和前面由CD写DVD相同。

  这种不需要改变现有代码,就可以扩展去适应新的数据、内容的特性,我们称之为 可扩展性

  代码经过修改后,可以去扩展适应,那是 可维护性

  通过更深的继承,可以实现更复杂的结构。最直接的例子,Item是继承自Object类,CD继承自Item,所以Java显然支持多层次的继承。那么,Item可有子类Game,有新成员变量players,Game还有子类SingleGame和OnlineGame等,这样SingleGame和OnlineGame都有成员变量players。


  1. 泛型就这么简单,链接:link↩︎

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值