第二章 面向对象 2.1 封装 考试目标5.1 编写代码,实现类中的紧封装、松耦合和高内聚,并描述 这样做的优点。 为什么要封装? 通常在类中,我们的实例变量(定义在类中,但位于任何方法之外,并 且只有在实例化类时才会被初始化的变量),还有一些只有本类会用到 的方法,都用private来声明,然后如果需要对实例变量访问,就写一些 getter和setter。 如何实现封装: 保护实例变量(使用访问修饰符,通常是private) 建立public访问器方法,强制调用代码使用这些方法而不是直接访问实 例变量。 对于访问器方法,使用JavaBeans命名规则set和 get 2.2 继承、IS-A、HAS-A关系 继承的作用: 促进代码的复用。这个很好理解,所有的类都继承自Object,它提供给 所有的类equal()等方法,如果所有的类都要自己实现这个方法的话,那 太可怕了。 使用多态性。书中给了个载入游戏图形的例子: Java代码 //游戏图形的超类,所有子类通过继承GameShape来获得显示图形的方 法 class GameShape { //显示图形的方法 public void displayShape(){ System.out.println("displaying shape"); } } //GameShape的一个子类,游戏人物的图形对象 class PlayerPiece extends GameShape{ //code } //GameShape的一个子类,墙砖的图形对象 class TilePiece extends GameShape{ //code } //现在假设有一个GameLauncher类,当我们进入这张地图的时候,它会 把这些图形对象(即GameShape的子类)都载入进来。 //换句话说,GameLauncher的工作就是实例化这些XxxxPiece类,然后让 他们调用父类GameShape的displayShape()方法。 class GameLauncher{ //这个方法并不关心参数是GameShape的哪个子类。 public static void doShapes(GameShape shape){ shape.displayShape(); } public static void main(String[] str){ PlayerPiece player = new PlayerPiece(); TilePiece tile = new TilePiece(); doShapes(player); //体现了多态性的好处,加入后面又加入了新的 Piece, doShapes(tile); //比如WeaponPiece,在doShapes中依然不用关心它 是什么。 } } 只是输出GameShape.displayShape(),因为doshapes()的参数是 GameShape,无论你传入的是哪个子类。 2.2.1 IS-A关系 在OO中,IS-A的概念基于类继承和接口实现。在Java中,使用extends 和implements来表达IS-A关系。 2.2.2 HAS-A关系 HAS-A关系基于引用。类A中的代码具有对类B实例的引用,则“类A HAS-A 类B”。。 2.3 多态性 考试目标5.2 给定一个场景,编写代码,演示多态性的使用。而且,要 判断何时需要强制转化,还要区分与对象引用强制转换相关的编译器错 误和运行时错误。 多态性:可以传递多个IS-A测试的任何Java对象都可以被看作是多态 的。 访问对象的唯一方式是通过引用变量。关于引用,要记住: 引用变量只能属于一种类型。一经声明,类型就永远不能再改变(尽管 它引用的对象可以改变类型)。 引用是一个变量,因此它可以重新赋予给其他对象(除非该引用被声明 为final)。 引用变量的类型决定了可以在该变量引用的对象上调用的方法。 引用变量可以引用具有与所声明引用的类型相同的任何对象,或者—— 最重要的一点是——它可以引用所声明类型的任何子类型。 引用变量可以声明为类类型或接口类型。如果将变量声明为接口类型, 它就可以引用实现该接口的任何类的任何对象。 前面2.2我们说到“在OO中,IS-A的概念基于类继承和接口实现”,2.2中 基于类继承的说的比较多,接口同样可以表达IS-A的关系,实现多态。 多态方法调用仅使用于实例方法。实例方法可以用更一般的引用变量类 型(超类或者接口)引用一个对象,但是在运行时,基于实际对象(而 不是引用类型)动态选择,静态方法和变量不是的。 而在继承中子类可以使用超类的方法,也可以自己来实现这一方法,这 就涉及到了重写(override)。 多态性只是用于实例方法,不适用于静态方法,静态变量和实例变量。 2.4 重写和重载 考试目标1.5 给定一个代码示例,判断一个方法是否正确地重写或重载 了另一个方法,并判断该方法的合法返回值(包括协变式返回值)。 考试目标5.4 给定一个场景,编写代码,声明和/或调用重写方法或重载 方法。编写代码,声明和/或调用超类、重写构造函数或重载构造函 数。 2.4.1 重写方法(override) 重写的规则: 变元列表必须与被重写的方法的变元列表完全匹配。 返回类型必须与超类中被重写方法中原先声明的返回类型或其子类型相 同。 访问级别的限制性一定不能比被重写方法的更严格。 访问级别的限制性可以比被重写方法的弱。 仅当实例方法被子类继承时,它们才能被重写。与实例的超类同包的子 类可以重写未标识为private或final的任何超类方法。不同包的子类只能 重写那些标识为public或protected的非final方法。 重写方法可以抛出任何未检验(运行时)异常,无论被重写方法是否声 明了该异常。 重写方法一定不能抛出比被重写方法声明的检验异常更新或更广的检验 异常。比如,一个声明FileNotFoundException异常的方法不能被一个声 明SQLException、Exception或任何其他非运行时异常的方法重写,除非 它是FileNotFoundException的一个子类。 重写方法能够抛出更少或更有限的异常。 不能重写表示为final的方法。 不能重写标识为static的方法。 调用被重写方法的超类版本:super关键字 2.4.2 重载方法(overload) 重载的规则: 重载方法必须改变变元列表。 重载方法可以改变返回类型。 重载方法可以改变访问修饰符。 重载方法可以声明新的或更广的检验异常。 方法能够在同一个类或者一个子类中被重载。 调用重载方法:调用哪个重载方法,取决于变元的类型。而不是其引用 的类型,重载的var-arg是最后的选择。 Java代码 class Animal {} class Horse extends Animal{} class UseAnimals{ public void doStuff(Animal a){ System.out.print("In the Animal version"); } public void doStuff(Horse h){ System.out.print("In the Horse version"); } public static void main(String[] str){ UseAnimals ua = new UseAnimals(); Animal obj = new Horse(); ua.doStuff(obj); //在这里引用类型决定了调用哪个重载方法 } } //结果显示"In the Animal version" 重载方法和重写方法中的多态性 用一个例子来说明: Java代码 public class Animal { public void eat(){ System.out.println("Generic Animal Eating Generically"); } } public class Horse extends Animal{ public void eat(){ System.out.print("Horse eating hay"); } public void eat(String s){ System.out.print("Horse eating "+s); } } //测试方法 public class Test{ public static void main(String[] str){ //这里是下表中“方法调用的代码” } } 不同调用方法的结果: 方法调用的代码 结果 解释 Animal a = new Animal(); a.eat(); Generic Animal Eating Generically Horse h = new Horse(); h.eat(); Horse eating hay Animal ah = new Horse(); ah.eat(); Horse eating hay 此处是多态性起作用——确定调用的是哪个eat()时,使 用的是实际的对象类型(Horse),而不是引用类型(Animal) 注意和前一个例子的情况相区分,前面的情况是选择用UseAnimals对象 的哪个方法,由变元类型(或说引用类型)来决定(编译时); 现在的情况是选择用哪个对象的eat()方法,Animal里如果没有这个方法 会报编译时错误,但是就算有,运行时还是由实际的对象类型来决定。 Horse he = new Horse(); he.eat("Apples"); Horse eating Apples 调用重载方法eat(String s); Animal a2 = new Animal(); a2.eat("treats"); 编译时错误 Animal没有带String变元的eat()方法 Animal ah2 = new Horse(); ah2.eat("Carrots"); 编译时错误 原因同上 重载方法和重写方法的区别: 重载方法 重写方法 变元 必须改变 一定不能改变 返回类型 可以改变 除协变式返回外,不能改变 异常 可以改变 可以减小或消除。一定不能抛出新的或更广的检验异常 访问级别 可以改变 一定不能执行更严格的限制(可以降低限制) 调用 引用类型决定了选择哪个重载版本(基于声明的变元类型)。在编译时 刻做出决定。 调用的实际方法仍然是一个在运行时发生的虚拟方法调用,但是编译器 总是知道所调 用方法的签名。因此在运行时,不仅是方法所在的类,而且变元匹配也 已经明确了。 对象类型(也就是堆上实际的实例的类型)决定了调用哪个方法。 在运行时决定。 2.5 引用变量强制转换 向下转型:把引用变量转换为子类类型。如Horse h = (Horse) new Animal();但如果调用父类里没有的方法,可以通过编译,但运行时会抛 出java.lang.ClassCastException异常。 一定要先定义好Animal animal =new Horse(); 这样才能进行Horse h =(Horse)animal;否则编译可以通过,但是会有 ClassCaseException。或者在之前进行一次Instanceof的测试。 向上转型:把引用变量转换为超类类型。如Animal a = new Horse(); 不 需要转化,这是天然的IS-A 关系。 2.6 实现接口 在第一章的“声明接口”里说过,接口就是一种契约,任何实现这个接口 的实现类都必须同意为该接口的所有方法提供实现。 合法的非抽象实现类必须执行以下操作: 为来自所声明接口的所有方法提供具体(非抽象)的实现。 遵守合法重写的所有规则。 在实现方法上声明非检验异常,而不是在接口方法上声明,也不是在接 口方法上什么异常的子类。 保持接口方法的签名,并且保持相同的返回类型(或子类型),但是不 必声明在接口方法声明中声明过的异常。 两条规则: 一个类可以实现多个接口。 接口自身可继承另一个接口,而且接口可以继承多个接口。 2.7 合法的返回类型 2.7.1 返回类型的声明 哪些内容声明为返回类型,这主要取决于是在重写方法、重载方法还是 在声明新方法。 重载方法上的返回类型 没有什么限制,重载方法关键是变元要变化。 重写、返回类型和协变式返回 从Java5开始,只要新的返回类型是被重写的(超类)方法所声明的返 回类型的子类型,就允许更改重写方法中的返回类型(这就是传说中的 协变式返回)。以前的Java版本要求重写的方法返回类型一定要与原来 的一致。 2.7.2 返回值 六条规则: 1.可以在具有对象引用返回类型的方法中返回null。 2.数组是完全合法的返回类型。 3.在具有基本返回类型的方法内,可以返回任何值或变量,只要它们能 够隐式转换为所声明的返回类型。 Java代码 public int foo(){ char c ='c'; return c; } 4.在具有基本返回类型的方法内,可以返回任何值或变量,只要它们能 够显式地强制转换为所声明的返回类型。 Java代码 public int foo(){ float f = 32.5f; return (int) f; } 5.一定不能从返回类型为void的方法返回任何值。 6.在具有对象引用返回类型的方法内,可以返回任何对象类型,只要它 们能够隐式地强制转换为所声明的返回类型。换句话说,能通过IS-A测 试的(也就是使用instanceof运算符测试为true)任何对象都能够从那个 方法中返回。 Java代码 //声明返回超类,实际返回子类 public Animal getAnimal(){ return new Horse(); //Assume Horse extends Animal } //声明返回超级父类Object,实际返回数组 public Object getObject(){ int[] nums = {1,2,3}; return nums; //Return an int array,which is still an object } //声明返回接口,实际返回接口的一个实现类 public interface Chewable{} public class Gum implements Chewable{} public class TestChewable{ //Method with an interface return type public Chewable getChewable(){ return new Gum(); //Return interface implementer } } 2.8 构造函数和实例化 构造函数基础: 构造函数是用来创建新对象的,每当我们“new”的时候,JVM就会按照 你所指定的构造函数来创建一个对象实例。 每个类都至少有一个构造函数。 构造函数都没有返回类型(有就成方法了)。不同的构造函数通过不同 的变元来区分(或者为空)。 构造函数链: 当 Horse h = new Horse(); 的时候究竟发生了什么?(Horse extends Animal,Animal extends Object) 调用Horse构造函数。通过一个对super()的(隐式)调用,每个构造函 数都会调用其超类的构造函数,除非构造函数调用同一个类的重载构造 函数。 调用Animal构造函数(Animal是Horse的超类)。 调用Object构造函数(Object是所有类的最终超类,因此,Animal类扩 展Object)。这时,我们处于栈的顶部。 为Object实例变量赋予显式值。 Object构造函数完成。 为Animal实例变量赋予显式值。 Animal构造函数完成。 为Horse实例变量赋予显式值。 Horse构造函数完成。 构造函数规则: 构造函数能使用任何访问修饰符。 构造函数名称必须与类名匹配。 构造函数一定不能有返回类型。 让方法具有与类相同的名称是合法的,但是建议不要这样做。 如果不在类代码中键入构造函数,编译器将自动生成默认构造函数。 默认构造函数总是无变元构造函数。 如果在类代码中已经有带变元的构造函数存在,而没有无变元的构造函 数,那在编译时不会自动生成无变元构造函数。 每个构造函数都必须将对重载构造函数[this()]或超类构造函数[super()] 的调用作为第一条语句。如果没有,编译器会自动插入super();只限于无 参的Super(),或者this(),编译器不会加有参的。如果第一个构造函数没有 该两个方法(super 或this的有参无参形式都可以,只要有一个就 OK),编译器就要自动加上super(),去父类找无参构造函数(父类没有 就编译错误),如果自己加上super(arg),则父类中需要有相应的构造函 数。this()同理。 除非在超类构造函数运行之后,否则不能调用实例方法或访问实例变 量。 只能将静态变量和方法作为调用super()或this()的一部分进行访问。例 如:super(Animal.NAME) 抽象类具有构造函数,这些构造函数总是在实例化具体子类时才调用。 接口没有构造函数。接口不是对象继承树的一部分。 调用构造函数的唯一方法是从另一个构造函数内部进行调用。 关于Java私有构造函数,一般加上一个Public的静态方法来对该私有构 造函数进行调用,典型的就是单例模式。 2.8.1 判断是否会创建默认构造函数 如何证明会创建默认构造函数? 只有在类代码中没有构造函数的,才会生成默认构造函数。 如何知道它就是默认构造函数? 默认构造函数的特征: 具有与类相同的访问修饰符。 没有任何变元。 包含super(); Java代码 public class Foo{ public Foo(){ super(); } } 如果超类构造函数有变元会怎样? 那在new的时候必须带参 new Animal(“monkey”); 2.8.2 重载构造函数 重载构造的时候要注意: this()或super()一定要在第一行。 不要写如下的死循环代码: Java代码 class A { A(){ this("foo"); } A(String s){ this(); } } 2.9 静态成员 2.9.1 静态变量和静态方法 当方法永远与实例完全无关时,我们就将它声明为static。 访问静态方法和变量: 用“ 类名.静态变量/方法 ”来访问。 静态方法不能访问实例(非静态)变量。 静态方法不能访问非静态方法。 静态方法能够访问静态方法和静态变量。 static方法的重定义问题: 我们都知道静态方法是不能被重写的,但是可以被重定义。这个问题很 迷糊人,从代码上来看,重写和重定义没有区别。那么重定义 (redefine)和重写(override)有啥区别呢? 重定义操作的是静态方法,静态方法跟类有关;重写操作的是非静态方 法,跟实例对象有关。看下下面的代码: Java代码 public class Tenor extends Singer{ public static String sing(){ return "fa"; } public String sing2(){ return "fa2"; } public static void main(String[] args){ Tenor t = new Tenor(); Singer s = new Tenor(); System.out.println(t.sing()+" "+s.sing()+" "+t.sing2()+" "+s.sing2()); } } class Singer{ public static String sing(){ return "la"; } public String sing2(){ return "la2"; } } //运行结果是:fa la fa2 fa2 2.10 耦合与内聚 Java的OO设计目标:紧封装、松耦合、高内聚。以实现易于创建、易 于维护、易于增强的目标。 耦合(Coupling):耦合是指一个类了解另一个类的程度。如果类A对 类B的了解很少,仅限于类B通通过其接口公开的信息,类A并不知道B 的更多具体实现,那就称类A和类B是松耦合的。我们说类B做到了紧封 装。 内聚(Cohesion):内聚用于表示一个类具有单一的、明确目标的程 度。一个类的目标越明确,其内聚性越高。