文章目录
1.2 数据抽象
-
数据类型:指的是一组值和对这些值的操作的集合。
本节重点学习定义和使用数据类型,这个过程也被称为数据抽象 -
Java编程基础主要是使用class关键字构造被称为引用类型的数据类型,(面向对象的编程风格)
-
抽象数据类型(ADT)是一种能够对使用者隐藏数据表示的数据类型
-
类比:用Java类实现抽象数据类型
用一组静态方法实现一个函数库 -
我们研究同一个问题的不同算法的主要原因是在于他们的性能特点不同,抽象数据类型可以:在不修改任何用例代码的情况下用一种算法替换另一种算法,并改变所有用例的性能
1.2.1使用抽象数据类型
要实现一种数据类型并不一定非得知道它是如何实现的
本章用一个编写的Counter(计数器)来举例,简单介绍:
- 抽象数据类型的API
- 继承的方法
- 用例代码
- 对象
- 创建对象
- 调用实例方法
- 使用对象
- 赋值语句
- 将对象作为参数
- 将对象作为返回值
等内容
1.2.1.1抽象数据类型的API
我们用API(应用程序编程接口)来说明抽象数据类型的行为
它将列出所有的构造函数和实例方法(操作)
public class Counter | ||
---|---|---|
Counter(String id) | 创建一个名为 id 的计数器 | |
void | increment() | 将计数器的值加1 |
int | tally() | 该对象创建之后计数器被加1的次数 |
String | toString() | 对象的字符串表示 |
1.2.1.2继承的方法
Java中所有的数据类型都会继承 toString( ) 方法来返回String表示的该类型的值。
toString() 的默认实现并不实用(返回对象的名字和内存地址)
public class test {
public static void main(String[] args) {
Counter cd = new Counter();
System.out.println(cd.toString());
}
}
class Counter{
String id;
int count = 0;
public Counter(String id){}
public Counter(){}
}
Counter@2f4d3709
因此我们常常提供实现来重载默认实现,并在此时在API中加上 toString() 方法
此类方法的例子还包括:euqals(), compareTo(), hashCode()
1.2.1.3用例代码
1.2.1.4对象
数据抽象的基础概念:对象是能够承载数据类型的值的实体
对象的三大重要特性:
- 状态:数据类型中的值,存储在变量中
(状态的作用:为用例代码提供信息,或产生某种副作用,或被数据类型的操作所改变) - 标识:能够将一个对象区别于另一个对象,可以认为该标识就是这个对象在内存中的位置
- 行为:数据类型的操作
数据类型的实现的唯一职责就是维护一个对象的身份,这样用例代码在使用数据类型的时候只需要遵循描述对象行为的API即可,无需关注对象状态的表示方法。
引用是访问对象的一种方式,可以认为引用就是内存地址
1.2.1.5创建对象
要创建一个对象,我们用关键字new并紧跟类名以及()来触发它的构造函数。构造函数没有返回值,因为它总是返回它的数据类型的对象的引用
每当调用了new(),系统都会:
- 为新的对象分配内存空间
- 调用构造函数,初始化对象中的值
- 返回该对象的一个引用
和使用原始数据类型时一样,我们会在一条声明语句中创建一个对象并通过将它和一个变量关联起来,来初始化该变量。
和使用原始数据类型不一样:变量关联的时指向对象的引用,而不是数据类型的值
1.2.1.6调用实例方法
实例方法的意义在于操作数据类型中的值。
实例方法可能会改变数据类型中的值,也可能只是访问其中的值
实例方法的每次触发都是和一个对象相关的
实例方法 | 静态方法 | |
---|---|---|
举例 | heads.increment() | Math.sqrt(2.0) |
调用方式 | 对象名 | 类名 |
参量 | 对象的引用和方法的参数 | 方法的参数 |
主要作用 | 访问或改变对象的值(实现数据类型的操作) | 计算返回值(实现函数) |
1.2.1.7使用对象
要开发某种给定数据类型的用例,我们需要:
- 声明该类型的变量
- 使用关键字new触发能够创建该类型的对象的一个构造函数
- 使用变量名在语句或表达式中调用实例方法
使用对象举例:Flip类,模拟了抛硬币的过程,打印出正面的次数和反面的次数,并计算差值,该类调用了Counter类,和StdRandom类(官方静态方法库中),该类运行时接受命令行参数 T
import edu.princeton.cs.algs4.Counter;
import edu.princeton.cs.algs4.StdRandom;
/**
* StdRandom.bernoulli(double d):
* Params:
* p – the probability of returning true
* Returns:
* true with probability p and false with probability 1 - p
*/
public class filp {
public static void main(String[] args) {
int T = Integer.parseInt(args[0]);
Counter heads = new Counter("heads");
Counter tails = new Counter("tails");
for (int i = 0; i<T;i++){
if (StdRandom.bernoulli(0.5)){
heads.increment();
}
else {
tails.increment();
}
}
System.out.println(heads);
System.out.println(tails);
int d = heads.tally() - tails.tally();
System.out.println("delta:"+ Math.abs(d));
}
}
下面几节将对上述用例进行分析
1.2.1.8赋值语句
使用引用数据类型的赋值语句将会创建该引用的一个副本,赋值语句不会创建新的对象,而只是创建另一个指向某个已经存在的对象的引用,这种情况叫做别名
别名:两个变量同时指向同一个对象
要理解 引用数据类型 和 原始数据类型 在 赋值语句 上的差异:
- 如果x和y是原始数据类型的变量:那么赋值语句x = y会将y的值复制到x中
- 如果x和y是引用数据类型:赋值语句x = y会将y中保存的指向对象的引用复制到x中。
(在Java中,别名是bug的常见原因)
以下代码将输出 2
Counter c1 = new Counter("ones");
c1.increment();
Counter c2 = c1;
c2.increment();
System.out.println(c1);
2
因为c2,是c1所指向的对象的别名,改变一个对象的状态将会影响到所有和该对象别名有关的代码,我们习惯于认为两个不同的原始数据类型的变量是互相独立的,但是这种感觉对于引用类型的变量并不适用
1.2.1.9将对象作为参数
Java将参数值的一个副本从调用端传递给了方法,这种方式称为按值传递。
这种方法的一个重要后果就是方法无法改变调用端变量的值。
- 对于原始数据类型来说:这种策略正是我们所期望的(两个变量相互独立)
- 对于引用数据类型来说:因为我们创建的变量都是对象的别名,传递的是对象的引用,虽然该方法不会改变原始的引用,但它能够改变引用对象的值,从而造成其他引用也会受到影响
1.2.1.10 将对象作为返回值
- 方法可以将它的参数对象返回
- 也可以创建一个对象并返回它的引用
import edu.princeton.cs.algs4.Counter;
import edu.princeton.cs.algs4.StdRandom;
public class FlipsMax {
public static Counter max(Counter x, Counter y){
if (x.tally()>y.tally()){
return x;
}
else{
return y;
}
}
public static void main(String[] args) {
int T = Integer.parseInt(args[0]);
Counter heads = new Counter("heads");
Counter tails = new Counter("tails");
//根据条件返回该方法的参数对象
for (int t = 0; t < T; t++){
if (StdRandom.bernoulli(0.5)){
heads.increment();
}
else {
tails.increment();
}
}
if (heads.tally()==tails.tally()){
System.out.println("Tie");
}
else {
System.out.println(max(heads,tails)+" Wins");
}
}
}
1.2.1.11 数组也是对象
在Java中,所有非袁术数据类型的值都是对象————数组也是对象
当我们将数组传递给一个方法,或者将一个数组变量放在赋值语句的右侧时,我们都是在创建该数组引用的一个副本,而不是数组的副本
1.2.1.12 对象的数组
我们可以看到数组元素可以是任意类型的数据:我们实现的main( )方法的args [ ]参数就是一个String类型的数组
创建对象数组的步骤:
- 使用方括号语法调用数组的构造函数创建数组
- 对于每个数组元素,调用该元素的构造函数创建相应的对象
对象的数组即是一个由对象的引用组成的数组,而非所有对象本身组成的数组,好处:
- 如果对象非常大,那么在移动他们的时候由于只需要操作引用而非对象本身,可以大大提高效率
坏处
- 如果对象很小,每次获取信息的时候都需要引用,反而会降低效率
import edu.princeton.cs.algs4.Counter;
import edu.princeton.cs.algs4.StdRandom;
/**
* 模拟掷骰子
* 创建一个数组,数组中每个元素为骰子的一个面
* 即以每个面命名的Counter*/
public class Rolls {
public static void main(String[] args) {
//从命令行取值
int T = Integer.parseInt(args[0]);
//常量6——骰子的6个面
int SIDES = 6;
//创建Counter数组
Counter[] rolls = new Counter[SIDES + 1];
//Counter数组赋初始值——每个元素为Counter对象的引用
for (int i = 1; i <= SIDES;i++){
rolls[i] = new Counter(i+"'s");
}
for (int t = 0; t < T; t++){
//之所以是(1,SIDE+1)是因为uniform方法的参数列表是左闭右开的
int result = StdRandom.uniform(1,SIDES+1);
rolls[result].increment();
}
for (int i = 1; i <= SIDES; i++){
System.out.println(rolls[i]);
}
}
总结
运用数据抽象的思想编写代码(定义和使用数据类型,将数据类型的值封装在对象中)的方式称为面向对象的编程。
一个数据类型的实现所支持的操作如下:
- 创建对象(创造它的标识):使用new关键字触发构造函数并创建对象,初始化对象中的值,并返回它的引用
- 操作对象中的值(控制对象的行为,可能会改变对象的状态):使用和对象关联的变量调用实例方法来对对象中的值进行操作
- 操作多个对象:创建对象的数组,像原始数据类型的值一样将他们传递给方法或是从方法中返回,只是变量关联的是对象的引用,而非对象本身