软件构造复习3
数据类型与类型校验基本数据类型、对象数据类型静态类型检查、动态类型检查静态检查动态检查
可变性与不可变性 Mutable and Immutable变量类型数组 Array(定长数组不可改变长度)列表/动态数组 List列表/数组 Set字典/键值对 Map
迭代/遍历 Iteration
值的改变、引用的改变表示泄露、防御式拷贝Unmodifiable Collections空指针 Null References
快照图 Snapshot diagram规约 Specification、前置/后置条件Diagramming Specifications行为等价性Spec的写法、Spec的强度测试 Test
ADT(抽象数据类型)操作的四种类型构造器(从无到有) Creator生产器(从有到新) Producer观察器(不改变) Observer变值器(改变对象属性的方法) Mutator一些例子
表示独立性 Representation Independent不变量、表示不变量RI不变量 Invariants of ADT用不变量取代前提条件precondition
表示空间、抽象空间、AF表示不变量RICheckRep()
以注释形式写AF、RI、Safety接口、抽象类、具体类OOP对象object类class接口interface枚举类型enumeration
继承Inheritance、重写Override继承Inheritancesth. about final
重写Override抽象类Abstract Class
多态Polymorphism重载Overload重写和重载的区别泛型Generics
hashCode()等价性equals()equals()与==操作符:instanceof()
equals()的自反性、对称性、传递性
toString()不可变对象的引用等价性不可变对象的对象等价性可变对象的观察等价性可变对象的行为等价性
数据类型与类型校验
数据类型代表的是一组值,对应着对于这组值的一组相关操作变量用特定数据类型定义,可存储满足类型约束的值
基本数据类型、对象数据类型
基本数据类型 Primitive types int, long, boolean, double, char, float, etc…对象数据类型 Objective types String, BigInteger 根节点Object,所有的子类都继承于Object 例如,class Guitar extend Instrument {…} 可以将基本类型包装为对象类型 boolean => Boolean;int => Integer;etc… 原因:colletions要求所有元素都是对象类型,但一般情况下避免使用基本类型包装而成的对象类型(会降低性能)
List
<Integer
> list
= new ArrayList<>();
list
.add(1);
list
.add(Interger
.valueOf(1));
静态类型检查、动态类型检查
int a
= 2;
double a
= 2;
int a
= (int
)18.7;
double a
= (double
)2/3;
int a
= 18.7;
String a
= 1;
double a
= 2/3;
静态检查
静态类型语言:所有类型在编译阶段进行类型检查静态检查可能存在的错误 语法错误;类名/函数名错误;参数数目错误;参数类型错误;返回值类型错误
int n
= 5;
if(n
) {
n
++;
}
动态检查
动态类型语言:运行阶段动态检查可能存在的错误 非法的参数值;非法的返回值;越界;空指针 (可以理解为:静态检查“类型”,动态检查“值”)
int big
= 200000;
big
= big
* big
;
无错误,但结果错误
double probability
= 1/5;
int sum
= 0;
int n
= 0;
int average
= sum
/n
;
double num
= 7;
double n
= 0;
double average
= sum
/n
;
——————————————— 静态检查 >> 动态检查 >> 无检查 ———————————————
可变性与不可变性 Mutable and Immutable
改变变量 和 改变变量的值 的 区别? 改变变量:将该变量指向另一个值的存储空间 改变变量的值:将改变量当前指向的存储空间 存储的值 写入一个新值
基本类型及其封装类型都是不可变的
尽量使用可变类型的不可变形式;使用可变类型时,防御性拷贝
变量类型
不可变类型指的是对象所在内存块里面的值不可以改变,有数值、字符串、元组;可变类型则是可以改变,主要有列表、字典
数组 Array(定长数组不可改变长度)
List
<Integer
> list
= new ArrayList<Integer
>();
列表/动态数组 List
List
<Integer
> list
= new ArrayList<Integer
>();
列表/数组 Set
(List有序,Set无序;List可重复值,Set不可)
Set
<Integer
> s1
= new TreeSet<Integer
>();
Set
<Integer
> s1
= new HashSet<Integer
>();
字典/键值对 Map
迭代/遍历 Iteration
List
<String
> cities
= new ArrayList<>();
Set
<Integer
> numbers
= new HashSet<>();
Map
<String
, Turtle
> turtles
= new HashMap<>();
for (String city
: cities
) {
System
.out
.println(city
);
}
for (int num
: numbers
) {
System
.out
.println(num
);
}
for (int ii
=0; ii
<cities
.size(); i
++) {
System
.out
.println(cities
.get(ii
));
}
for (String key
: turtles
.KeySet()) {
System
.out
.println(key
+ ": " + turtles
.get(key
));
}
用迭代器删除元素
for(String subject
: subjects
) {
if(subject
.startsWith("6.")) {
subjects
.remove(subject
);
}
}
Iterator iter
= subjects
.iterator();
while(iter
.hasNext()) {
String subject
= iter
.next();
if(subject
.startsWith("6.")) {
iter
.remove();
}
}
SUMMURY 一定是对象数据类型,而非基本数据类型
值的改变、引用的改变
不变数据类型:一旦被创建,其值不能改变 (final变量,局部变量;final类无法派生子类;final变量无法改变/引用;final方法无法被子类重写)
final int n
= 5;
final Person a
= new Person("Ross");
不变对象:一旦被创建,始终指向同一个值/引用可变对象;拥有方法可以修改自己的值/引用
String s
= "a";
s
= s
.concat("b");
StringBuilder sb
= new StringBuilder("a");
sb
.append("b");
(只有一个引用指向该值时无区别;有多个引用时,针对同一个地址空间进行修改,可以理解为共用?)
SUMMURY 不可变类型会多很多个临时拷贝(需要回收),效率低但更安全; 可变类型减少拷贝,实现数据共享,以提高效率,但不利于程序分析; 因此只有一个引用时可以安全地使用可变类型,而存在多个引用最好使用不可变类型
表示泄露、防御式拷贝
public static int
sum(List
<Integer
> list
) {
int sum
= 0;
for(int x
: list
)
sum
+= x
;
return sum
;
}
public static int
sumAbsolute(List
<Integer
> list
) {
for(int
=0; i
<list
.size(); i
++) {
list
.set(i
, Math
.abs(list
.get(i
)));
}
return sum(list
);
}
public static void main(String
[] args
) {
List
<Integer
> myData
= Arrays
.asList(-5, -3, -2);
System
.out
.println(sumAbsolute(myData
));
System
.out
.println(sum(myData
));
}
public static Datae
startOfSpring() {
return askGroundhog();
}
public static Datae
startOfSpring() {
if(groundhogAnswer
== null)
groundhogAnswer
= askGroundhog();
return groundhogAnswer
;
}
private static Date groundhogAnswer
= null;
public static Datae
startOfSpring() {
if(groundhogAnswer
== null)
groundhogAnswer
= askGroundhog();
return new Date(groundhogAnswer
.getTime());
}
Unmodifiable Collections
可变类型如何返回不可变引用?
Collections
.unmodifiableList(List
)
Collections
.unmodifiableSet(Set
)
Collections
.unmodifiableMap(Map
)
这样得到的包装(结果)是不可变的,只能看;这种“不可变”是在运行阶段获得的,编译阶段无法进行对其的静态检查
List
<String
> list
= new ArrayList<String
>();
list
.add("ab");
List
<String
> listCopy
= Collections
.unmodifiableList(List
)
listCopy
.add("c");
list
.add("c");
System
.out
.[println(listCopy
.size());
空指针 Null References
int size
= null;
String name
= null;
int
[] points
= null;
String
[] names
= new String[] {null}
List
<Double
> sizes
= new ArrayList<>();
sizes
.add(null);
快照图 Snapshot diagram
对象类型(可变:单线圈;不可变:双线圈) 不可变引用:双线箭头;可变引用:单线箭头
(1)引用不可变,但指向的值可以是可变的【举例】
final StringBuilder sb
= new StringBuilder("abc");
sb
.append("d");
sb
= new StringBuilder("e");
System
.out
.println(sb
);
(2)可变的引用也可以指向不可变的值【举例】
String s1
= new String("abc");
List
<String
> list
= new ArrayList<String
>();
list
.add(s1
);
s1
= s1
.concat("d");
System
.out
.println(list
.get(0));
String s2
= s1
.concat("e");
list
.set(0, s2
);
System
.out
.println(list
.get(0));
规约 Specification、前置/后置条件
API 类的层次结构,接口,子类,具体描述,构造函数及其描述,相关方法及其描述(签名、功能、参数、返回)
设计决策:代码 => 编译器;注释 => 开发者
规约 Specification 目的:达成共识/理解;定位错误;区分责任;隔离变化,无需通知客户端;提高代码效率 包含内容:输入/输出的数据类型;功能和正确性;性能(规约只讲能做什么,不讲怎么实现)
前置条件 requires:满足算法成功运行的输入条件 是对客户端的约束,使用方法时需满足的条件 @param
后置条件 effects:前提条件满足的情况下,输出应满足的条件 是对开发者的约束,方法结束时需满足的条件 @return 或 @throws
例: 静态类型声明是一种规约,可根据此进行静态类型检查 方法前的注释也是一种规约,但需人工判定是否满足
规约的特点 1)内聚的(Spec描述的功能应单一、简单、易理解) 2)信息丰富 3)足够强(给足客户足够的信息) 4)足够弱(文件权限与客户权限不等对) 5)尽量使用抽象类型(给予方法实现体和客户端更大的自由度;比如用List而不是具体到ArrayList/HashList) 6)前置条件?(不写Pre,就要在代码内部check;若代价太大就在规约里加入Pre,把责任交给客户端)
Diagramming Specifications
满足规约在圈内;否则在圈外 更强的规约,范围更小
行为等价性
两个函数的行为是否等价 取决于 代码的Spec 因此编写代码前,需要弄清楚Spec如何协商形成、如何撰写
Spec的写法、Spec的强度
除非在后置条件里声明过,否则方法内部不应该改变输入参数尽量不设计mutating的Spec尽量避免使用可变对象(程序中会有多个引用指向同一个可变对象)
测试 Test
依据规约设计测试用例,不考虑实现
int
[ array
= new int[] {7, 7, 7};
assertEquals(0, find(array
, 7));
assertEquals(7, array
[find(array
, 7)]);
规约的强度 强度S2>=S1 <=> S2前置条件更弱 and S2后置条件更强 上述情况下可以用S2替代S1 SUMMURY:Spec变强 == 更放松的前置条件+更严格的后置条件 == 更少的实现+更多可用的客户端
确定规约 & 欠定规约 确定:给定一个满足Precondition的输入,其输出是唯一的、明确的 欠定:同一个输入可以有多个输出(比如有重复元素的数组返回下标,跟寻找方式有关) 非确定:同一个输入,多次执行得到的输出不同 (欠定的规约通常有确定的实现)
操作式规约 & 声明式规约 操作式:伪代码 声明式:只有起始状态/功能 (声明式规约更有价值)
ADT(抽象数据类型)操作的四种类型
除了编程语言提供的基本数据类型和对象数据类型,程序员可定义自己的数据类型数据抽象:由一组操作所刻画的数据类型 例如,定义Bool:
TrueFalse
single bit10int58String“True”“False”
可变和不可变数据类型 可变:可改变内部值;不可变:构造新的对象 一些类型提供两种形式:String & StringBuilder
构造器(从无到有) Creator
构造器可能实现为构造函数或静态函数;实现为静态方法的构造器称为工厂方法 例如,Array.asList()、List.of()
生产器(从有到新) Producer
例如,String.concat():String X String -> String
观察器(不改变) Observer
例如,List.size():List -> int (Getter)
变值器(改变对象属性的方法) Mutator
通常返回void,如果返回void,说明它必然改变了对象的某些内部状态;也可能返回非空类型,例如Set.add() (Setter)
一些例子
CreatorProducerObserverMutator
int0,1,2+,-,*==,!=,<,>noneStringString constructorsconcat,substringlength,charAtnoneListArrayList constructors,Collections.singletonListCollections.unmodifiableListsize,getadd,remove,addAll,Collctions.sort
表示独立性 Representation Independent
设计良好的抽象数据结构ADT,通过封装来避免客户端获取数据的内部表示(即“表示泄露”)
表示独立性 Client在使用ADT时无需考虑其内部数据结构如何实现,ADT内部表示的变化不应影响Spec和客户端
ADT由什么刻画? 前提条件requires、后置条件effects
如何实现? Spec规定了客户端Client和实现者Implementer之间的契约,明确了Client知道哪些内容可以写入规约,明确了Implementer知道可以安全更改哪些内容(不破坏契约就能更改)
例如:
public class MyString {
public static MyString
valueOf(boolean b
) {...}
public int
length() {...}
public char
charAt(int i
) {...}
public MyString
substring(int start
, int end
) {...}
}
test case(Test-First Programming)
MyString s
= MyString
.valueOf(true);
assertEquals("true", s
);
assertEquals("true", s
.toString());
assertEquals(4, s
.length());
assertEquals("t", s
.charAt(0));
assertEquals("r", s
.charAt(1));
assertEquals("u", s
.charAt(2));
assertEquals("e", s
.charAt(3));
MyString t
= s
.substring(0, 2);
assertEquals("t", t
.charAt(0));
例题中,可以用字符数组 private char[] a; 表示String数组 ———————— 也可以用数据结构 private char[] a; private int start; private int end; 表示String数组
MyString s
= MyString
.valueOf(True
);
MyString t
= s
.substring(1, 3);
不变量、表示不变量RI
规约Specification
class Family {
public List
<Person
> people
;
public List
<Person
> getMembers() {
return people
;
}
}
class Family {
public Set
<Person
> people
;
public List
<Person
> getMembers() {
return new ArrayList<>(people
);
}
}
void client3(Family f
) {
Person anybody
= f
.getMembers().get(0);
}
不变量 Invariants of ADT
ADT需要始终保持其不变量(任何时候都是true,比如immutability)不变量与client的任何行为无关,应改由ADT限制(就是代码里的)尽量使用private final,避免public
public class Tweet{
private final String author
;
private final String text
;
public Tweet(String author
) {
this.author
= author
;
this.text
= text
;
}
public String
getAuthor() {
return author
;
}
public String
getText() {
return text
;
}
}
public static Tweet
retweetLater(Tweet t
) {
return new Tweet("rbmllr", t
.getText());
}
用不变量取代前提条件precondition
(简单来说就是用自己定义的方法来规定client的操作,比如传入的参数必须满足方法规定的要求,实验3写过)
static String
exclusiveOr(String set1
, String set2
);
static SortedSet
<Charecter
> exlusiveOr(SortedSet
<character
> set1
, SortedSet
<character
> set2
);
最可靠的方法就是immutable,彻底避免表示泄露
表示空间、抽象空间、AF
表示空间R:实现者看到和使用的值抽象空间A :client看到和使用的值 R和A之间的关系 抽象函数:从R到A的映射函数 一定是满射,未必是单射,未必是双射
表示不变量RI
Rep invariant某个具体表示是否是合法的(也就是对R中的表示值,在A中有无映射,有则合法(如"abc",“bac”),无则不合法(如"abbc"))RI是一个集合,是所有表示量R的一个子集,包含所有合法的表示值RI也可以看作一个条件,描述了合法的表示值(是否为一个逻辑判断)同样的表示空间R,可以有不同的RI 同样的R、RI,可以有不同的AF(解释不同)如何建立RI? 1)在对象的初始状态不变量为true,对象发生变化时不变量也要为true 2)creator和producer在创建对象时要确保不变量为true 3)在每个方法return前,用checkRep()检查不变量是否得以保持确保无 4)三个要点: 4.1)由creators和producers建立 4.2)由mutators和observers保护 4.3)避免表示泄露!
CheckRep()
在所有可能改变rep的方法内都要检查,随时检查RI是否满足
以注释形式写AF、RI、Safety
RI:rep中的所有fields何为有效AF:如何解释每一个R值(保证满射)
public class CharSet {
private String s
;
}
接口、抽象类、具体类
OOP
对象object、类class、属性attribute、方法method、接口interface、枚举类型enumeration
对象object
状态state:变量/属性fields 行为behavior:方法methods
类class
定义方法和属性
class Complex {
private double re
;
private double im
;
...
}
...
public class ComplexUser {
public static void main(String args
[]) {
Complex c
= new Complex(-1, 0);
}
}
class Difference {
public static void main(String args
[]) {
display();
Difference t
= new Difference();
t
.show();
}
static void display() {
...
}
void show() {
...
}
}
public class MyStatic {
private String name
;
private static String staticStr
= "S";
public Mystatic(String name
) {
this.name
= name
;
}
public static void main(String args
[]) {
MyStatic
.testStaticMethod();
MyStatic msm
= new MyStatic("test");
msm
.testObjectMethod();
}
public static void testStaticMethod() {
System
.out
.println(MyStatic
.staticStr
);
}
public void testObjectMethod() {
System
.out
.println(this.name
);
System
.out
.println(MyStatic
.staticStr
);
}
}
变量的可见性
private私有的 只有类内部的方法可以访问protected 只有当前类和子类的方法可以访问public 所有类都可以访问
接口interface
一个类可以实现多个接口(具备多个接口中的方法)一个接口可以有多个实现类 (也就是接口确定ADT规约,类来实现ADT)接口中只有方法定义,接口之间可以扩展
public interface Complex {
double
realPart();
Complex
plus(Comlex c
);
}
public OrdinaryComplex
implements Complex {
double re
;
double im
;
public OrdinaryComplex(double re
, double im
) {
this.re
= re
;
this.im
= im
;
}
public double
realPart() {
return re
;
}
public Complex
plus(Complex c
) {
...
}
}
public class ComplexUser {
public static void main(String args
[]) {
Complex c
= new OrdinaryComplex(-1, 0);
}
}
接口:确定ADT规约;类:实现ADT
public interface MyString {
public static MyString
valueOf(boolean b
) {
return new FastMyString(true);
}
}
...
MyString s
= MyString
.valueOf(true);
System
.out
.println("The fistr character is: " + s
.charAt(0));
枚举类型enumeration
public enum Month
{
JANUARY, FEBRUARY, MARCH,..., NOVEMBER, DECEMBER;
}
Month month
= Month
.MARCH;
if(month
.equals(Month
.MARCH)) {...}
for(Month m
: Month
.values()) {
m
.name();
m
.ordinal();
...
}
继承Inheritance、重写Override
继承Inheritance
继承 == (线程中的)泛化;代码的复用(只需写一次就可多次使用)
class A extends B
可重写继承Rewriteable 子类获得父类方法可进行重写严格继承Strict 子类只能添加新方法,不可重写 key word:fanal
例:
public class Car {
int serial
;
public final
void drive() {...}
public void playmusic(int n
) {
serial
= n
;
}
}
public LuxuryCar
extends Car {
Position s
;
public void playmusic(int n
) {
...
}
}
sth. about final
a final field:表示变量是不可变的**(引用不可变)**a final method:严格继承**(不可重写)**a final class:不能被继承
重写Override
重写的函数具有完全一样的signature重写不要改变父类函数的本意子类中的重写标识@override
class Device {
int serialnr
;
public void setSerialnr(int n
) {}
}
class Valve extends Device {
Position s
;
public void on() {...}
public void setSerialnr(int n
) {
seriennr
= n
+ s
.serialnr
;
}
}
抽象类Abstract Class
Abstract method 只有定义没有实现的方法Abstract class 至少有一个抽象方法的类
public abstract
class AbstractAccount implements Account {
protected long balance
= 0;
public boolean
withdraw(long amount
) {
}
}
abstract
class GraphicObject {
int x
, y
;
void moveTo(int newX
, int newY
) {...}
abstract
void draw();
}
...
class Circle extends GraphicObject {
void draw() {...}
}
多态Polymorphism
重载Overload
特殊多态Ad hoc Polymorphism 一个方法可以有多个同名实现**(方法重载)** (方便client调用,可以用不同参数调用相同函数)
参数必须不同返回值可以相同/不同public/private/protected可以相同/不同抛出异常可以相同/不同可以在一个类内重载,也可以在子类中重载
public class Overload {
public static void main(String args
[]) {
System
.out
.println(add("C", "D"));
System
.out
.println(add("C", "D", "E"));
}
public static String
add(String c
, String d
) {
return c
.concat(d
);
}
public static String
add(String c
, String d
, String e
) {
return c
.concat(d
).concat(e
);
}
}
重写和重载的区别
重写重载
签名相同不同方法/子类重载父类方法后,子类继承被重载的方法参数必须不变必须变化返回类型不变或与子类型相同无要求异常只能抛出相同或更加宽泛的异常无要求权限不能更加严格无要求
泛型Generics
参数化多态Parametric Polymorphism 一个类型名字可以代表多个类型**(泛型编程)** (泛型只存在于编译阶段)
泛型类 类中声明了>=1个泛型变量
class ClassName <T,E,...>{
private Tvar
;
...
}
public class PaperJar<T> {
private List
<T> itemList
= new ArrayList<>();
public void add(T item
) {
itemList
.add(item
);
}
...
public static void main(String args
[]) {
PaperJar
<String
> paperStr
= new PaperJar<>();
paperStr
.add("Lion");
PaperJat
<Integer
> paperInt
= new PaperJar<>();
paperInt
.add(new Integer(6));
}
}
泛型接口 具体同泛型类,略泛型方法 选学,略
子类型多态/包含多态Subtyping 一个变量名字可以代表多个类的实例(子类型) (子类型的规约不能弱化超类型的规约)
子类型 -> 父类型 √ 父类型 -> 子类型 × 避免类型转换Type casting
hashCode()
键-值映射表可以无序包含一个数组键-值对中的key被映射为hashcode,每个hashcode对应数组的index,∴hashcode决定了数据在数组中的存储位置
public final
class PhoneNumber {
private final short areaCode
;
private final short prefix
;
private final short lineNumber
;
@Override
public int
hashCode() {
return arrays
.hashCode(areaCode
, profix
, lineNumber
);
}
}
class Person {
private String firstName
;
private String lastName
;
...
public boolean
equals(Object obj
) {
if(!(obj
instanceof Peerson)) return false;
Person that
= (Person
) obj
;
return this.lastName
.toUpperCase().equals(that
.lastName
.toUpperCase());
}
public int
hashCode() {
return 42;
return firstName
.toUpperCase();
return lastName
.toUpperCase().hashCode();
return firstName
.shCode() + lastName
.hashCode();
}
}
等价性equals()
public final
class PhoneNumber {
private final short areaCode
;
private final short prefix
;
private final short lineNumber
;
...
@Override
public boolean
equals(Object o
) {
if(!(o
instanceof PhoneNumber))
return false;
PhoneNumber pn
= (PhoneNumber
) o
;
return pn
.lineNumber
== lineNumber
&& pn
.prefix
== prefix
&& pn
.areaCode
== areaCode
;
}
...
}
equals()与==
==:引用等价性(判断地址空间;基本数据类型) equals():对象等价性(判断两个对象;对象类型)
if (input
== "yes")
if (input
.equals("yes"))
站在外部观察者的角度,对两个对象调用任何相同操作,都会得到相同结果 <=> 两个对象是等价的自定义ADT时,需要根据对等价的要求决定是否重写Object的equals()x.equals(null) 返回的一定是false
public class Duration {
public boolean
equals(Duration that
) {
return this.getLength() == that
.getLength();
}
}
...
public class Duration extends Object {
public boolean
equals(Duration that
) {
return this.getLength() == that
.getLength();
}
public boolean
equals(Object that
) {
return this == that
;
}
}
...
Duration d1
= new Duration(1, 2);
Duration d2
= new Duration(1, 2);
Object o2
= d2
;
d1
.equals(d2
);
d1
.equals(o2
);
操作符:instanceof()
用于判断某个对象是不是特定的类型(或子类型)属于动态类型检查,不是静态除了用于实现equals()方法,尽量避免使用
equals()的自反性、对称性、传递性
E ⊆ T x T
自反性:E(t, t) ∀t对称性:E(t, u) => E(u, t)传递性:E(t, u) ^ E(u, y) => E(t, y)
布尔值boolean
自反性:t==t ∀t对称性:tu => ut传递性:tu ^ uy => t==y
toString()
final calss PhoneNumber
{
private final short areaCode
;
private final short prefix
;
private final short lineNumber
;
...
@Override
public String
toString() {
return String
.format("(d) d-d", areaCode
, prefix
, lineNumber
);
}
}
...
Number jennie
= ...;
System
.out
.println(jennie
);
不可变对象的引用等价性
不可变对象的对象等价性
可变对象的观察等价性
两个索引在不改变各自对象状态的前提下不能被区分。即通过只调用observer,producer和creator的方法,它测试的是这两个索引在当前程序状态下“看起来”相等对于可变对象,java通常实现观察等价性可能导致bug,甚至破坏RI 例:
List
<String
> list
= new ArrayList<>();
list
.add("a");
Set
<List
<List
>> set = new HashSet<List
<String
>>();
set.add(list
);
set.contains(list
);
list
.add("goobye");
set.contains(list
);
可变对象的行为等价性
两个索引在任何代码的情况下都不能被区分,即使有一个对象调用了改造者。它测试的是两个对象是否会在未来所有的状态下“行为”相等。对于不可变对象,观察相等和行为相等是完全等价的,因为它们没有改造者改变对象内部的状态 因此只有可变类型需要写equals()和hashCode(),不可变类型无需