第七讲 面向对象的编程
用OOP/接口/类实现ADT
提纲
OOP的基本概念:对象、类、属性、方法和接口
OOP明显的特征
- 封装与信息隐藏
- 继承与重写
- 多态、子类型、重载
- 静态与动态分派
Java中一些重要的Object methods
设计好的类
- 设计immutable类型的class
- 设计mutabe类型的class
OOP的历史
1.基础概念
class variable(类成员变量)
与类相关的变量
instance methods(实例方法)和instance variables(实例成员变量)
在类的每个实例中发生一次
class methods(类方法)和class variables(类成员变量)
和类相关,在每个类中发生一次,使用他们不需要创造对象。
方法都存在方法栈中
实例方法每一个对象对应不同的方法栈,类方法每一个对象共享一个方法栈。
类中基本属性在堆中,类中方法属性在栈中。
2.Interface接口
method signatures(方法签名)
方法声明的两个组件构成了方法签名——方法的名称和参数类型
public double calculateAnswer(double A,int B,double C,double D){....}
//方法签名
calculateAnswer(double,int,double,double)
java中的接口
是方法签名的列表,但没有方法体,不能被实例化。
如果类在其实现子句中声明接口,并为接口的所有方法提供方法体,则类将实现接口。
接口之间可以继承和扩展。
一个类可以实现多个接口(从而具备了多个接口中的方法)。
一个接口可以有多种实现类。
类不允许多重继承。
接口:确定ADT规约
类:实现ADT
也可以不需要接口,直接使用类作为ADT,既有ADT定义也有ADT实现。
实际中更倾向于使用接口来定义变量。
- 除非知道一个实现足够了,否则对变量和参数使用接口类型。
- 支持实现的改变
- 防止依赖实现的细节
例如
Set<Criminal>senate=new HashSet<>();//do this...
HashSet<Criminal>senate=new HashSet<>();//Not this...
错误示例
/** Represents an immutable set of elememts of type E. */
public interface Set<E>{
public Set();//java接口不能有构造器
/** @return true if this set contains e as a member */
public boolean contains contains(E e);
/** @return a set which is the union of this and that */
public ArraySet<E> union(Set<E> that);//ArraySet没有表示独立性
}
/** Implementation of Set<E>.*/
public class ArraySet<E> implements Set<E>{
/** make an empty set */
public ArraySet(){...}
/** @return a set which is the union of this and that */
public ArraySet<E> union(Set<E> that){...}
/** add e to this set */
public void add(E e){...}
}
//Java允许类比接口的方法多,但是不能缺少接口中的方法。
//add(E e)违背了Set的规约——它的不变性,所有ArraySet不是Set的合法实现
MyString s=new FastMyString(true);
问题:打破了抽象边界
接口定义中不包含constructors,也无法保证所有实现类中都包含同样名字的constructors。因此,客户端需要知道该接口的某个具体实现类的名字。
从Java 8开始,允许接口中包含静态方法
public interface MyString{
/** @param b a boolean value
*@return string representation of b,either "true" or "false" */
public static MyString valueOf(boolean b){
return new FastMyString(true);
}
MyString s=MyString.valueOf(true);
.....
接口中的每个方法在所有类中都要实现;
通过default方法,在接口中统一实现某些功能,无需在各个类中重复实现它;
以增量式的为借口增加额外的功能而不是破坏已实现的类。
public interface Example{
default int method1(int a){...}//有方法体
static int method2(int b){...}//有方法体
public int method3();//在实现中定义方法体
}
public class C implements Example{
@Override
public int methods(){...}//需要实现method3
public static void main(String[] args){
Example.method2(2);
C c=new C();
c.method1(1);
c.method3();
}
}
3.继承和重写
(1)重写
重写方法
允许重新实现的方法。
Java中默认方法是可以重写的,在没有特殊的关键字情况下。
严格继承——用final实现
子类只能添加新方法,无法重写超类中的方法
如果一个方法不能被重写,它必须被关键字final修饰。
final
- +类:类不能被继承
- +方法:子类不能重写该方法
- +属性:该属性不能改变
- +immutable类型:值不能被改变
- +mutable类型:值可变,引用不可变
父类不能调用子类增加的方法。
模板
在父类中实现所有的方法,子类新增的方法需要在父类中声明。客户端只能通过调用父类的方法,间接调用子类的方法。
重写
父类或者超类中已经实现的方法,在子类中重新实现,重写的函数在父类和子类中有完全相同的签名。
编译时根据对象所属类判断是否可以调用该函数,实际执行时,根据对象具体实现再决定调用哪一个。
public class Animal{
data1;
data2;
i=1;
move(){
i=i+1;
}
eat(){...}
}
public class Dog extends Animal{
data3;
data4;
i=1;
move(){
i=i+2;
}
move(int m){.....}
bark(){....}
}
main(){
Aniaml A =new Dog();
A.i;//属性不继承,是Animal中的i=1;
A.move();//Animal中有move,可以调用,编译器不报错
//具体运行时,A是指向Dog类的,所以调用Dog中的move(),值为3
//若Dog中没有重写move(),则调用Animal中的move(),值为2
int c=3;
A.move(c);//Animal中没有move(int),编译器报错
A.move(3);//Animal中没有move(int),编译器报错
}
如果父类中的某个函数实现体为空,意味着其所有子类型都需要这个功能,但各有差异,没有共性,在每个子类中均需要重写。
class Device{
int serialnr;
public void setSerialNr(int n){}
}
class Value extends Device{
Position s;
public void on(){
.......
}
public void setSerialNr(int n){
seriennr=n+s.serialnr;
}
}
重写之后,利用super()复用了父类型中函数的功能,并对其进行了扩展。
class Thought{
public void message(){
System.out.println("Thought.");
}
}
public class Advice extends Thought{
@override //强调要进行重写
public void message(){
System.out.println("Advice");
super.message();//声明父类中的方法版本。
}
}
Thought parking=new Thought();
parking.message();//prints"Thought."
Thought dates=new Advice();
dates.massage();//prints
//Advice
//THought.
用this和super进行构造
在子类中创建构造器,其中第一行必须是超类的构造器声明。
在子类中还可以声明其他的构造器。(类似于重载)
重写的时候不要改变原方法的本意,符合可替换原则。静态编译检测不出,只能由程序员来保证。
(2)Abstract Class 抽象类
抽象方法
有签名没有实现体的方法,也称为抽象操作。
由关键字abstract定义。
抽象类至少包含一个抽象方法的类。
接口只有抽象方法的类。
抽象类
介于接口和具体类之间,不能进行实例化,可以包含一些具体方法和方法,一般不用抽象类。
如果某些操作是所有子类型共有的,但彼此有差别,可以在父类中设计抽象方法,在各子类中重写。
abstract class G{
int x,y;
abstract void draw();
abstract void resize();
}
class Circle extends G{
void draw(){....}
void resize(){.....}
}
class Re extends G{
void draw(){....}
void resize(){....}
}
4.Polymorphism,subtyping and overloading 多态、子类型、重载
(1)多态的三种类型
Ad hoc polymorphism特殊多态:功能重载
parametric polymorphism参数化多态:generics或generics programming
subtyping子类型多态、包含多态
(2)特殊多态和重载
方法名相同,但参数列表不同,返回值类型也可以不同,实质上是不同的方法,在编译阶段能确定调用哪一个方法。
价值:方便client调用
重载是一种静态多态,根据参数列表进行最佳匹配(参数类型相同优先,若是不同,则找参数类型的父类进行匹配)。
重载能够进行静态类型检查。
区别:
方式 | 确定时间 |
---|---|
重写 | 执行 |
重载 | 编译 |
重载规则
- 必须有不同的参数列表
- 可以有相同的/不同的返回值
- 可以有相同的/不同的public/private/protected
- 可以声明异常
- 可以在同类中重载,也可以在子类中重载。
重载可以发生在父类和子类之间
class Animal{
public void eat(){..}
}
class Horse extends Animal{
public void eat (String food){...}
}
public class UseAnimals{
public void duStuff(Animal a){
System.out.pirntn("Animal");
}
public void doStuff(Horse h){
System.out.println("Horse");
}
}
mian(String [] args)
{
UseAnimal ua =new UseAnimals();
Animal animalobj =new Animal();
Horse horseobj=new Horse();
Animal animalRefToHorse=new Horse();
ua.doStuff(animalobj)//Animal
ua.doStuff(horseobj);//Horse
ua.doStuff(animalRefToHorse);//Animal
//原因:animalRefToHorse的类型是Animal,静态类型检查时确定好要调用的函数是doStuff(Animal)
重载 | 重写 | |
---|---|---|
参数列表 | 必须改变 | 必须不变 |
返回值类型 | 可以改变 | 必须不变 |
异常 | 可以改变 | 异常更少或者更具体 |
通道 | 可以改变 | 不能设置更严格(可以减少限制) |
调用 | 编译阶段 | 对象类型决定选择哪种方式(运行阶段) |
本质 | 不同方法 | 同一方法 |
(3)Parametric polymorphism and Generic programming参数化多态和泛型
参数化多态:
-
一个函数在一系列类型上运行良好时,得到参数化多态;这些类型通常具有出一些共同结构。
-
它能够以通用方式定义函数和类型,以便根据运行时传递的参数工作,即允许静态类型检查,而无需完全指定类型。
泛型:
- 一种编程风格,其中的参数类型和函数按照稍后指定的类型编写,然后在需要时对作为参数提供的特定类型进行实例化。
类型变量:
使用<>来帮助声明类型变量。
List<Integer> ints =new ArrayList<integer>();
public interface List<E>
public class Entry<KeyType,ValueType>
public class PaperJar<T>{
private List<T> itemList =new ArrayList<>();
public void add(T item){
itemList.add(item);
}
public T get(int index){
return (T) itemList.get(index);
}
public static void main (String args[]){
PapersJar<String> papersStr=new PapersJar<>();
papersStr.add("Lion");
String str=(String) papersStr.get(0);
System.out.println(str);
}
}
泛型接口:
way1:泛型街口,非泛型的实现类。
public interface Set<E>{
//....
/**
*Test for membership.
*@param e an element
*@return true iff this set contains e
*/
public boolean contains(E e);
/**
*Modifies this set by adding e to the set.
*@param e element to add
*/
public void add(E e);
//....
}
public class CharSet1 implements Set<Character>{
private String s="";
//...
@Override
public boolean contains(Character e){
checkRep();
return s.indexOf(e)!=-1;
}
@Override
public void add(Character e){
if (!contains(e) )s+=e;
checkRep;
}
//...
}
way2:泛型接口,泛型的实现类
HashSet对于Set就是这样的实现
public interface Set<E>{
//...
}
public class HashSet<E>implements Set<E>{
//...
}
泛型的一些细节:
- 可以有mutable类型的参数
- 通配符?,只在使用泛型的时候出现,不能在定义中出现
- List<?> List =new ArrayList();
- List<? extends Animal>
- List<? super Animal>
- 运行时泛型消失,不能用instanceof()来检查泛型
- 不能有泛型数组
- 不能直接赋值
List<Object> O; List<String> p; O=p;//错误
(4)Subtyping Polymorphism 子类型多态
每个类只能直接继承一个父类,可以实现多个接口。
B是A的子类:意味着每一个B都是一个A。B满足A 的规约,B的规约至少和A的规约一样强。
子类型多态 不同类型的对象可以统一的处理而无需区分,从而隔离了“变化”。
Liskov替换原则(LSP)
如果S是T的子类型,那么T类型的对象可以替换为S类型的对象(即T类型的对象可以替换为任何子类型的对象),而不改变任何T的属性。——可替换性
instanceof
判断对象是否为所给类型的操作
尽可能避免使用instanceof(),不要在超类中使用instanceof来检查子类类型。
<检查对象类型>
- instanceof 是否为该类或其子类
- getclass 只检查是否为该类
5.java中一些重要的对象方法
重写对象方法
- equals()——如果两个对象“相等”,返回true
- hashcode()——用于哈希映射的哈希代码
如果你想要value,必须重写这两个函数,否则不需要重写 - toString()——可打印的字符串表示形式
知道你的对象是什么,会做的更好
除非你知道in不会被调用,否则始终要重写
示例:
public class Name{
private final String first,last;
public Name(String first,String last){
if (first==null || last==null)
throw new NullPointerException();
this.first=first;
this.last=last;
}
public boolean equals(Name o){
return first.equals(o.first)&&last.equals(o.last);
}
public int hashCode(){
return 31*first.hashCode()+last.hashCode();
}
public static void main(String [] args){
Set<Name> s=new HashSet<>();
s.add(new Name("Mickey","Mouse"));
System.out.println(new Name("Mickey","Mouse")));
}
}
//Name 重写了hashCode但是没有重写equals.这两个Name实例不相等。
//修改:重写equals方法
//@overrride 强制编译器检测是否为合理的重写
@override public boolean equals(Object o){
if (!(o instanceof Name)
return false;
Name n=(Name) o;
return n.first.equals(first)&&n.last.equals(last);
}
6.设计好的类
immutable类的优势
- Simplicity
- Excellent building blocks
功能简单便于修改 - Can be shared freely
- No need for defensive copies
允许多个引用指向同一个对象 - Inherently Thread-Safe
固有线程安全
如何写一个immutable类
- 不提供变值器
- 没有方法被重写
- 所有属性用final修饰
- 所有属性private
- 确保任何mutable类型的成分安全(避免表示泄露)
- 实现toString(),hashCode(),clone(),equals()等
什么时候将类设为immutable
- 总是,除非有好的原因
- 总是让小的“value class”是immutable类型
- 例如:Color,PhoneNumber,Unit
- Date和Point是错误的
- 通常使用long代替Date
什么时候让类是mutable类型
- 类表示的是状态需要改变的实体
- 真实世界中:BankAccount,TrafficLight
- 抽象:Iterator迭代器,Matcher匹配器,Collection集合
- 进程类:Thread线程,Timer计时器
- 如何类必须是可变的,则最小化可变性
- 构造函数应完全初始化实例
- 避免重新初始化方法
7.面向对象编程(OOP)的历史
省略