Java的基本数据类型和对象数据类型:
Object类是所有类的基类,所有类默认都继承了Object类。
基本数据类型的包装类:Boolean,Integer,Short,Long,Character,Float,Double
▪ 静态类型检查、动态类型检查
Java是静态类型的语言,在编译时需要知道所有变量的类型,编译器可以推断所有表达式的类型。而在像Python这样的动态类型语言中,这种检查会延迟到程序运行时。
静态检查和动态检查
语言可以提供的三种自动检查:
静态检查:在程序运行之前自动发现错误。
动态检查:执行代码时会自动发现错误。
不检查:语言根本无法帮助您找到错误。 你必须自己观察,或者得到错误的答案。
毋庸置疑,静态捕获错误比动态捕获它更好,动态捕获它比完全捕获它更好。
静态检查意味着在编译时检查错误。
▪错误的名称,如Math.sine(2)。 (正确的名字是sin)
▪参数数量错误,如Math.sin(30,20)。
▪错误的参数类型,如Math.sin(“30”)。
▪错误的返回值类型,如返回“30”(字符串); 从声明为返回int的函数。
动态检查的内容:
非法的参数
无法表示的返回值
超出范围的索引值
调用空对象的方法
静态检查针对类型,与变量特定值无关的错误,动态检查针对由特定值引起的错误。
▪ Mutable/Immutable
不变性是一种主要的设计原则
不变数据类型创建完后,值不可修改
不变引用,一旦指定引用位置,不可再次指定
Final关键字对不可变引用提供了静态检查的支持
如final int n = 5;
则n的值不能再被改变。
final的类声明意味着它不能被继承。
final的变量意味着它始终包含相同的值,不能更改;
final的方法意味着它不能被子类覆盖/重写;
对象是不可变的:一旦创建,它们总是表示相同的值。
对象是可变的:它们具有改变对象值的方法。
可变类型的优点
良好的性能
方便共享
可变性的风险
不可变类型的bug更少,更容易理解,灵活性更高。
传递可变对象容易产生潜在错误,且难以跟踪。
只在局部使用或者只有一个引用时,可变类型的使用是安全的。
对可变类型的多个引用(别名)是带来风险的原因
▪ 值的改变、引用的改变
指向可变值的不可变引用(引用不可变,值可变)
指向不可变值的可变引用(值不可变,引用可变)
▪ 防御式拷贝
返回变量是返回其一份拷贝而不是其本身。
▪ Snapshot diagram
快照图
为了理解微妙的问题,我们可以在运行时绘制正在发生的事情的图片。
快照图表示程序在运行时的内部状态 - 其堆栈(正在进行的方法及其局部变量)及其堆(当前存在的对象)。
快照图为我们提供了一种可视化更改变量和更改值之间区别的方法:
当分配给变量或字段时,您正在更改变量箭头指向的位置。 您可以将其指向不同的值。
当分配可变值的内容(例如数组或列表)时,您正在更改该值内的引用。
快照图中的原始值和对象值
▪原始值
原始值由裸常数表示。 传入箭头是对变量或对象字段中的值的引用。
▪对象值
对象值是由其类型标记的圆圈。 当我们想要显示更多细节时,我们在其中写入字段名称,箭头指向它们的值。
不可变对象在快照图中用双边框表示。
▪ Specification、前置/后置条件
规格说明
规格说明是团队开发的关键,是分配责任的基础。
规格说明是实现者和使用者之间的一种契约,实现者有责任满足契约,使用者可以信赖契约。
准确的规格说明利于确定错误的位置和责任;客户端不需要阅读代码,通过说明了解程序。
规格说明给了实现者实现的自由,在保证约定下,可以自由修改实现。
通过在说明中增加对输入的限定,省略掉耗时的正确性检查工作,提升效率。(保证输入正确性的责任由调用者承担)
方法规范包含以下几个子句:
前提条件,由关键字表示
后置条件,由关键字效果指示
例外行为:违反前提条件时的行为
前提条件是客户(即方法的调用者)的义务(义务)。 这是调用该方法的状态。
后置条件是该方法实施者的义务。 如果前提条件适用于调用状态,则该方法必须遵循后置条件,返回适当的值,抛出指定的异常,修改或不修改对象等。
规格说明蕴含了以下逻辑:如果前置条件满足了,则后置条件必须满足。
反之,如果前置条件不满足,后置条件则无需满足。
▪ 行为等价性
确定行为的等价,关键是一个规格说明实现是否可以替换另一个.
等价的判定由调用者视角确定。
判定可替代与否,需要对调用端依赖内容的准确描述。
Spec不应涉及实现的内部变量和私有域。
规约的强度
Deterministic(确定性):当呈现满足前提条件的状态时,结果完全确定
Under-deterministic(低确定的): 允许同一输入存在多个有效输出。
Nondeterministic(非确定的):输出结果不确定。
underdetermined (欠定的):不是确定性的spec。
规格S2强于或等于规格S1,如果
S2的先决条件是弱于或等于S1
对于满足S1前提条件的状态,S2的后置条件强于或等于S1。
然后,满足S2的实现也可用于满足S1,并且在程序中用S2替换S1是安全的。
(要求的更少,承诺的更多)
Diagramming specifications
每个点代表一个实现。
一个规格说明定义了一个区域,其中包含的点为其实现。
一个实现或者满足规格说明(区域内),或者不满足(区域外)。
加强后置条件,意味着对输出要求更多,实现自由变少。
弱化前置条件,实现中需要处理更多的情况。
上述两点使可满足的实现变少(点变少),故规格说明越强,区域越小。
▪ ADT操作的四种类型
Creators:产生类型的新对象
Producers:在已有对象基础上产生新的对象
Observers:输入抽象类型的对象,返回其他类型的对象
Mutators: 改变对象
▪ 表示独立性
好的ADT应该是表示独立的。
这意味着抽象类型的使用与其表示(用于实现它的实际数据结构或数据字段)无关,因此表示的更改对抽象类型本身之外的代码没有影响。
只有当我们通过前置条件和后置条件充分明确了ADT的操作,使调用者知道可以依赖哪些内容,实现者知道可以安全更改哪些内容,此时才可以修改内部表示。
仅依赖其public方法,而不依赖其private字段,在不检查和更改所有调用者代码的情况下进行更改。
▪ 表示泄露
rep暴露意味着类外的代码可以直接修改rep。
对表示泄露安全相关的参数,特别是输入参数和返回值,给出保证不泄露内部表示的策略。
▪ 不变量、表示不变量RI
好的ADT最重要属性是保持其不变性。
Rep Invariant
将rep值映射到布尔值的rep不变量:
RI:R→布尔值
▪对于rep值r,当且仅当r由AF映射时,RI(r)为真。
▪ 表示空间、抽象空间、AF
R:表示值空间(rep值)——实现时用到的值空间
A:抽象空间——需要支持的值空间
Abstraction Function 抽象函数
▪将rep值映射到abstractvalues的抽象函数
他们代表:
AF:R→A
图中的弧显示抽象函数。AF是满射的,未必单射,未必双射。
▪ 接口、抽象类、具体类
接口
Java的接口是一种用于设计和表达ADT的有用语言机制,其实现是实现该接口的类。
Java中的接口是方法签名列表,但没有方法体。
如果一个类在其implements子句中声明了接口,则它实现一个接口,并为所有接口的方法提供方法体。
接口和实现
▪API的多个实现可以共存
多个类可以实现相同的API
他们可以在性能和行为上有所不同,
▪在Java中,API由接口或类指定
Interface仅提供API
接口定义但不实现API
Class提供API和实现
Class可以实现多个接口
接口的优点
调用者理解ADT时只需要理解接口即可。
调用者不能在ADT的表示上创建无意的依赖。
不同实现可以在不同的类中。
抽象数据类型的多个不同表示可以共存于同一个程序中,作为实现接口的不同类。
抽象方法和抽象类
抽象方法:
具有签名但没有实现的方法(也称为Cabstract操作)只有定义没有实现
由关键字abstract定义
抽象类:
包含至少一个抽象方法的类称为抽象类
接口:一个只有抽象方法的抽象类
接口主要用于系统或子系统的规范。 实现由子类或其他机制提供。
具体类→抽象类→接口
▪ 继承、override
继承用于代码重用
Overriding (覆盖/重写)
子类可以重新实现父类中的方法。
子类中的实现通过提供与父类中的方法具有相同名称,相同参数或签名以及相同返回类型的方法来覆盖(替换)超类中的实现。
执行的方法版本将由用于调用它的对象确定。
根据调用时的对象类型决定被调用方法的版本
默认父类中的方法均是可被重写的
子类只能向超类添加新方法,它不能覆盖它们
如果某个方法无法在Java程序中覆盖,则必须以关键字final作为前缀
子类中可以通过super关键字调用父类中被重写的方法
如果要覆盖方法:
签名保持一致
使用@Override注释,compiler 会检查覆盖方法和被覆盖的方法签名是否完全一致
复制粘贴声明(或让IDE为你做)
可见性可以保持不变或增加,但不会减少。
▪ 多态、overload
多态性是指为不同类型的实体提供一个接口,或者使用一个符号来表示多个不同的类型。
一个函数可以有多个同名的实现(方法重载)
一个类型名字可以代表多个类型(泛型编程)
一个变量名字可以代表多个类的实例(子类型)
Overloading 重载
重载是指在一个类中存在多个同名函数,必须具有不同的参数列表,返回值类型可同可不同。
重载是一种静态多态
通过参数列表决定依赖哪个实现。
调用重载函数时进行静态检查,在编译时决定调用哪个方法。
重载规则
函数重载中的规则:重载函数必须有参数数量或数据类型的不同
重载方法必须改变参数列表。
重载方法可以更改返回类型。
重载方法可以更改访问修饰符。
重载方法可以声明新的或更广泛的检查异常。
方法可以在同一个类或子类中重载。
对某个方法的重载可以在一个类中进行,也可以在子类中进行。此时需要注意同重写的区别:如果父类和子类中两个方法的签名相同,则为重写;名称一样,参数列表不一样时,则为重载。
重写时父类和子类中的方法具有相同的签名,签名不同时则为重载。
子类重载了父类的方法后,子类仍然继承了被重载的方法。、
▪ 泛型
Generics (泛型):参数多态性——方法针对多种类型时具有同样的行为,此时可使用统一的类型表达多种类型。
泛型编程是一种编程风格,其中数据类型和函数是根据待指定的类型编写的,随后在需要时根据参数提供的特定类型进行实例化。
泛型编程围绕“从具体进行抽象”的思想,将采用不同数据表示的算法进行抽象,得到泛型化的算法,可以得到复用性、通用性更强的软件。
Java中的泛型
类型变量(type type variable)是非限定(无限制的)标识符。
使用<>,占位符类型(占位符),菱形运算符,以帮助声明类型变量。
类型变量(type type variable)是非限定(无限制的)标识符。
它们由泛型类声明,泛型接口声明,泛型方法声明引入
类中如果声明了一个或多个泛型变量,则为泛型类
这些类型变量称为类的类型参数
它定义了一个或多个充当参数的类型变量。
泛型类声明定义了一组参数化类型,每个类型参数部分都可以调用一个。
所有这些参数化类型在运行时共享同一个类。
如果接口声明了一个或多个类型变量,则接口是泛型的。
这些类型变量称为接口的类型参数。
它定义了一个或多个充当参数的类型变量。
通用接口声明定义了一组类型,每种类型都可以调用类型参数部分。
所有参数化类型在运行时共享相同的接口。
▪ 等价性equals()和= =
Java有两种不同的操作来测试相等性,具有不同的语义。
==运算符比较引用。
equals()操作比较对象内容 - 换句话说,对象相等。
对于原始数据类型,必须使用==
对于对象引用类型,定义新数据类型时,需要考虑等价的含义,然后实现equals()方法。
▪ equals()的自反、传递、对称性
equals方法实现了等价关系:
自反性:对于任何非空引用值x,x.equals(x)必须返回true。自反性
对称性:对于任何非空引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
传递性:对于任何非空引用值x,y,z,如果x.equals(y)返回true且y.equals(z)返回true,则x.equals(z)必须返回true。
一致性:对于任何非空引用值x和y,x.equals(y)的多次调用始终返回true或始终返回false,前提是未修改对象的equals比较中使用的信息。
空值处理:对于任何非空引用值x,x.equals(null)必须返回false。
equals是所有对象的全局等价关系(对所有对象都生效)
▪ hashCode()
哈希表实现了键-值之间的映射。
它包含一个数组,键值对中的key被映射为hashcode,对应到数组的index, hashcode决定了数据被存储到数组的那个位置。
hashcode设计时会尽量确保均匀分布到数组,当多个key散列到同一个index时(冲突),哈希表维护一个列表来记录这些键值对(bucket)
只要比较操作用到的信息没有被修改,那么对这同一个对象调用多次,hashCode()方法必须始终返回相同整数。
两个对象equals操作相等,则hashcode必须相等。因此,重写equals时,必须重写hashCode。
如果equals比较相等,则要求hashcode相等;如果equals不等,则hashcode是否相等均可,但最好不等(提升性能)
▪ 可变对象的观察等价性、行为等价性
观察等价:在不改变对象状态的情况下(不使用mutator),无法区分对象
行为等价:改变一个对象而不改变另外一个时,仍然无法区分对象
对于不可变对象,观察和行为相等是相同的,因为没有任何mutator方法。
对于可变对象,使用观察等价性貌似可行,但是会带来潜在的问题。
▪Java使用大多数可变数据类型(如集合)的观察相等,但其他可变类(如StringBuilder)使用行为相等。
▪如果两个不同的List对象包含相同的元素序列,则equals()报告它们是相等的。
▪但是使用观察性相等会导致细微的错误,实际上我们可以轻松地破坏其他集合数据结构的rep不变量。
Equals()应该实现行为等价
两个引用相等意味着它们指向了同一个对象
可变类型应该继承Object类的equals()和hashCode()
对于不可变类型:
equals()应该比较抽象值。 这与说equals()应该提供行为平等是一样的。
hashCode()应该将抽象值映射到整数。
对于可变类型:
equals()应该比较引用,就像==。 同样,这与说equals()应该提供行为平等是一样的。
hashCode()应该将引用映射为整数。
可变类型不应该重写equals()和hashCode(),采用Object默认实现的即可。