目录
1. 数据类型
基本数据类型primitive | 只有值,没有ID | immutable | 栈中分配 | int,char,double等小写 | |
对象数据类型object | 即有值,又有ID | 不确定 | 堆中分配 | String,Integer等首字母大写 |
基本数据类型封装成对象数据类型:Boolean,Integer,Character,……
基本数据类型及其封装都是不可变immutable类型。
2. 改变
改变变量:将当前变量指向另一个存储空间;
改变变量的值:在当前变量的存储空间写入新值。
3. 不变对象
不变数据类型:值不可变;
不变引用类型:引用不可变。
4. 关键字final
利用关键字final来强制引用不可变。
但是当编译器无法确定声明为final的变量不会改变,就会提示错误。这种机制属于静态类型检查。
静态类型检查:在编译时发现错误,例如返回类型错误、参数类型错误等;
动态类型检查:在运行时发现错误,例如分母为0,越界错误等。
注意:
对可变值的不可变引用:指向同一个对象,但是值可以改变,例如:
private final Date date;
date被强制指向同一个引用,但是其指向的值可以被改变。
同样的,对不可变值的可变引用允许其指向不同的对象。
因此为了维护不变性,不仅要声明private final,同时要尽量采用immutable类型的数据。
5. 不变量Invariants
首先明确不变性是典型的不变量。在程序的生命周期中,需要维护不变量始终为true。不变量由抽象数据类型ADT表示(不是Java语言中的抽象类、抽象方法的意思)。
定义两个值的集合:
R:表示空间,被ADT的开发者所关注;
A:抽象空间,被client客户端所关注。
在此基础上,定义AF:R→A为抽象函数abstract function,来具象“合法”的要求;
表示不变量Rep Invariant:RI:R→boolean,来验证表示是否“合法”。
6. 例子
聚焦于下列的程序(部分):
./User.java:
public class User
{
public static List<Tweet> tw()
{
List<Tweet> list=new ArrayList<Tweet>();
Date date=new Date();
for(int i=0; i<4; i++)
{
date.setTime(10000*i);
list.add(new Tweet("Typer", date));
}
return list;
}
public static void main(String[] args)
{
List<Tweet> list=tw();
for(Tweet tt: list)
{
System.out.println(tt.getAuthor());
System.out.println(tt.getTime());
}
}
}
./Tweet.java:
public class Tweet
{
private final String author;
private final Date time;
public Tweet(String author, Date time)
{
this.author=author;
this.time=time;
}
public String getAuthor()
{
return author;
}
public Date getTime()
{
return new Date(time.getTime());
}
}
我们可以看到Tweet.java中的getTime方法做了一个简单的防御式拷贝,来避免表示泄露。
方法的目的是打印4个时间段的推文信息(作者,时间)。
输出结果如下:
Typer
Thu Jan 01 08:00:30 CST 1970
Typer
Thu Jan 01 08:00:30 CST 1970
Typer
Thu Jan 01 08:00:30 CST 1970
Typer
Thu Jan 01 08:00:30 CST 1970
这并不出人意料,因为已经在第4节中简要的描述了:对于可变值的不可变引用,使其指向同一个对象,但是对象的值可以被改变。这里要求我们严格区分引用不变与对象不变。
我们看到User.java中的tw()方法只声明了一次date,导致每次for循环执行时,date始终指向同一个引用,而其中的值始终在改变,因此每次new的Tweet中的time字段始终在一个引用上且值一直在改变。
解决方案如下:
一种我们修改client端的方法:
public class User
{
public static List<Tweet> tw()
{
List<Tweet> list=new ArrayList<Tweet>();
for(int i=0; i<4; i++)
{
Date date=new Date();
date.setTime(10000*i);
list.add(new Tweet("Typer", date));
}
return list;
}
……
}
在每个循环体内重新声明一遍date,但是这要求client有极高的“道德”,因此推荐第二种方法:
修改开发端的方法:
public class Tweet
{
private final String author;
private final Date time;
public Tweet(String author, Date time)
{
this.author=author;
this.time=new Date(time.getTime());
}
……
}
在接收的时候,声明一个新的Date类型的变量。
两种方法的输出如下:
Typer
Thu Jan 01 08:00:00 CST 1970
Typer
Thu Jan 01 08:00:10 CST 1970
Typer
Thu Jan 01 08:00:20 CST 1970
Typer
Thu Jan 01 08:00:30 CST 1970
因此所谓的防御式拷贝,不仅要关注getter方法,也要关注setter方法。