数据抽象

数据抽象指的是定义和使用数据类型的过程。数据类型指的是一组值和一组对这些值的操作的集合。原则上所有的程序都只需要使用原始数据类型即可,但在更高层次的抽象上编写程序会更加方便。

抽象数据类型(ADT)是一种能够对使用者隐藏数据表示的数据类型。用Java类来实现数据类型和用一组静态方法实现一个函数库并没有什么不同。抽象数据类型的主要不同之处在于将数据和函数的实现关联,并将数据的表示方法隐藏起来。

使用抽象数据类型

编写一个名为Counter的简单数据类型的程序。

public class Counter {
	private final String name;
	private int count;

	public Counter(String id){
		name=id;
	}
	public void increment(){
		count++;
	}
	public int tally(){
		return count;
	}
	public String toString(){
		return count+" "+name;
	}

}

抽象数据类型的API

计数器的API

继承的方法

根据Java的约定,任意数据类型都能通过在API中包含特定的方法从Java的内在机制中获益。例如,Java中的所有数据类型都会继承toString()方法来返回用String表示的该类型的值。Java会在用“+”运算符将任意数据类型的值和String值连接时调用该方法。

对象

对象是能够承载数据类型的值的实体,或数据类型的实例(三大特性)

  • 状态:数据类型中的值

  • 标识:能够将一个对象区别于另一个对象。可以认为对象的标识就是它在内存中的位置

  • 行为:数据类型的操作

创建对象

每种数据类型中的值都存储于一个对象中。要创建(或实例化)一个对象,用关键字new并紧跟类名以及括号()(若构造函数需要则指定一系列参数)来触发它的构造函数。构造函数没有返回值,因为它总是返回它的数据类型的对象的引用。

每当用例调用了new(),系统都会:

  • 为新的对象分配内存空间

  • 调用构造函数初始化对象中的值

  • 返回该对象一个引用

我们一般都会在一条声明语句中创建一个对象并通过将它和一个变量关联来初始化该变量,和使用原始数据类型时一样。和原始数据不同的是,变量关联的是指向对象的引用而并非数据类型的值本身。

调用实例方法

实例方法拥有与静态方法很多相同的性质:

  • 参数按值传递

  • 方法名可以被重载

  • 方法可以有返回值,也许还会产生一些副作用

  • 特别地,方法的每次触发都是和一个对象相关的

触发实例方法的各种方式:

  • 通过关键字new(触发构造函数)

  • 通过语句(没有返回值)

  • 通过表达式

静态方法的主要作用是实现函数;非静态(实例)方法的主要作用是实现数据类型的操作。静态方法调用的开头是类名(按习惯为大写),而非静态方法调用的开头总是对象名(按习惯为小写)。

使用对象

要开发某种给定数据类型的用例,我们需要:

  • 声明该类型的变量,以用来引用对象

  • 使用关键字new触发能够创建该类型的对象的一个构造函数

  • 使用变量名在语句或表达式中调用实例方法

赋值语句

使用引用类型的赋值语句将会创建该引用的一个副本。赋值语句不会创建新的对象,而只是创建另一个指向某个已经存在的对象的引用

将对象作为参数

可以将对象作为参数传递给方法,这一般都能简化用例代码。当我们调用一个需要参数的方法时,该动作在Java中的效果相当于每个参数值都出现在了一个赋值语句的右侧,而参数名则在该赋值语句的左侧。也就是说,Java将参数值的一个副本从调用端传递给了方法,这种方式称为按值传递。

对于原始数据来说,按值传递无法改变调用端变量的值,两个变量相互独立。但每当使用引用类型作为参数时我们创建的都是别名,换句话说,这种约定将会传递引用的值(复制引用),也就是传递对象的引用,虽然无法改变原始的引用,但它能够改变该对象的值。

将对象作为返回值

方法可以将它的参数对象返回。Java中方法只能有一个返回值——有了对象我们的代码实际上就能返回多个值。

public static Counter max(Counter x,Counter y){
		if(x.tally()>y.tally()) return x;
		else					return y;
		}

在Java中,所有非原始数据类型的值都是对象。

对象的数组

创建一个对象的数组需要以下两个步骤:

  • 使用方括号语法调用数组的构造函数创建数组

  • 对于每个数组元素调用它的构造函数创建相应的对象

在Java中,对象数组即是一个由对象的引用组成的数组,而非所有对象本身组成的数组。

public class Rolls {
	public static void main(String[] args){
		int T=Integer.parseInt(args[0]);
		int SIDES=6;
		//调用数组的构造函数创建数组
		Counter[] rolls=new Counter[SIDES];
		//对每个数组元素调用构造函数创建相应的对象
		for(int i=0;i<SIDES;i++){
			rolls[i]=new Counter(i+"'s");
		}
		for(int i=0;i<T;i++){
			int result=StdRandom.uniform(0, 6);
			rolls[result].increment();
		}
		for(Counter value:rolls){
			StdOut.println(value);
		}
	}

}

运用数据抽象的思想编写代码(定义和使用数据类型,将数据类型的值封装在对象中)的方式称为面对对象编程

抽象数据类型的实现

实例变量

实例变量和熟悉的静态方法或是某个代码中的局部变量最关键的区别在于:每一时刻每个局部变量只会有一个值,但每个实例变量则对应着无数值(数据类型的每个实例对象都会有一个)。这并不会产生二义性,因为我们在访问实例变量时都需要通过一个对象——我们访问的是这个对象的值。

同样,每个实例变量的声明都需要一个可见性修饰符。在抽象数据类型的实现中,我们会使用private。如果该值在初始化后不应该再改变,我们也会使用final。如果使用public修饰Counter类型中的两个实例变量(Java中是允许的),那么根据定义,这种数据类型就不再是抽象的了。

构造函数

每个Java类都至少含有一个构造函数以创建一个对象的标识。如果没有定义构造函数,类将会隐式定义一个默认情况下不接受任何参数的构造函数并将所有实例变量初始化为默认值。

实例方法

实现数据类型的实例方法(即每个对象的行为)的代码和实现静态方法(函数)的代码完全相同

  • 返回值类型

  • 签名(指定方法名、返回值类型和所有参数变量的名称)

  • 主体(一系列语句组成,包括一个返回语句来将一个返回类型的值传递给调用者)

实例方法的行为和静态方法部分相同,只有一点关键的不同:它们可以访问并操作实例变量。例如调用heads.increment()时,increment()方法中的代码访问的是heads中的实例变量,并且能改变它的值。

作用域

总的来说,在实现实例方法的Java代码中使用了三种变量:

  • 参数变量。方法的签名定义参数变量,在方法被调用时参数变量会被初始化为调用者提供的值。参数变量的作用域是整个方法。

  • 局部变量。局部变量的声明和初始化都在方法的主体中,作用域是当前代码段它的定义之后的所有语句。

Last updated