锁定老帖子 主题:SCJP笔记_章二_面向对象
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
作者 | 正文 | |||||||||||||||||||||||||||||||||||||||
发表时间:2009-07-15
最后修改:2009-07-15
第二章 面向对象
这章开头说,“作为一名SCJP6,意味着你必须精通Java中面向对象的知识。必须熟悉继承层次结构,灵活自如地应用多态性的强大能力,内聚与松散耦合必须成为你的第二性征,复合则成为你的谋生之道。”(复合类型就是引用类型,包括类引用、接口引用、数组引用)。 在如今框架横飞的年代,我们就像拼装工人一样,不问为什么,不去思考,没有思想。我常常对自己说,我不要再写垃圾代码了。但总是昨夜还沉浸在《Java与模式》的优雅,第二天就又屈服于疲于奔命的进度和莫名其妙的设计文档了。 每个夜晚你总会在写字楼的6层看见一个为编程痴狂的老小子,一边敲着键盘,一边摇头晃脑的吼着汪峰的《我想要怒放的生命》,加油!
2.1 封装 考试目标5.1 编写代码,实现类中的紧封装、松耦合和高内聚,并描述这样做的优点。
为什么要封装? 通常在类中,我们的实例变量(定义在类中,但位于任何方法之外,并且只有在实例化类时才会被初始化的变量),还有一些只有本类会用到的方法,都用private来声明,然后如果需要对实例变量访问,就写一些getter和setter。 如果都public了呢?比如你定义了一条记录的ID是用sequence获取的,一个调用者自己指定了一个旧的ID给对象,做插入数据库的操作,那一定是要抛异常了。
如何实现封装:
2.2 继承、IS-A、HAS-A关系 考试目标5.5 编写代码,实现IS-A关系和/或HAS-A关系。
书云:“不使用继承,即便编译最微小的Java程序也几乎是不可能的。”,继承可以说是面向对象的基础。 子类继承超类,子类继承了超类的非私有的成员变量和成员方法,就像这些成员本来就是他们自己的一样。 需要注意的是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中依然不用关心它是什么。 } }
2.2.1 IS-A关系 在OO中,IS-A的概念基于类继承和接口实现。在Java中,使用extends和implements来表达IS-A关系。 IS-A:书云“这个东西是那个东西的一种”。我觉得这个解释足够了,不再将这个概念妖魔化了。= = 类A继承类B,可以说“类A IS-A 类B”.
2.2.2 HAS-A关系 HAS-A关系基于引用。类A中的代码具有对类B实例的引用,则“类A HAS-A 类B”。
书云:“IS-A、HAS-A关系以及封装只是面向对象设计的冰山一角”。其实我们在设计架构的时候,就是从这些基本的概念出发的。记得和同事在设计类的时候,一个同事说:“是它的就是它的,不可分割的就给它;不是它的就不是它的,不能生加上去。”这是一个原则,但是我也反对为了面向对象而面向对象。
2.3 多态性 考试目标5.2 给定一个场景,编写代码,演示多态性的使用。而且,要判断何时需要强制转化,还要区分与对象引用强制转换相关的编译器错误和运行时错误。
多态性:可以传递多个IS-A测试的任何Java对象都可以被看作是多态的。 访问对象的唯一方式是通过引用变量。关于引用,要记住:
前面2.2我们说到“在OO中,IS-A的概念基于类继承和接口实现”,2.2中基于类继承的说的比较多,接口同样可以表达IS-A的关系,实现多态。
Java为什么没有多重继承? 如果一个类扩展另外两个类,并且两个超类都具有doStuff()方法,那么问题就出险了:子类将继承doStuff()方法的哪个版本讷?这个问题可能导致一种“致命的死亡菱形”的情形,B、C继承A,D继承B、C,而且B、C都重写类A中的一个方法,那么从理论上讲,类D就继承了同一个方法的两种不同实现。Java的设计者考虑到这种可能的混乱,所以规定一个类只能直接继承一个超类。
那如果我有下面的需求该怎么办呢? 比如上面的GameShape例子,它的子类通过继承它,获得了displayShape()方法,来显示图像。现在我想让GameShape的子类都可以使用Animatable类的animate()方法,来实现游戏贴图的一些动画。 由于animate()方法不仅提供给GameShape的子类,也会被其他的类使用,比如Game2Shape,所以我不能把animate()写在GameShape中。但又不能继承两个类,而在每个子类中都写一个自己的animate()显然又不优雅。 这时,我们就可以用接口来实现这个需求。即 interface Animatable{ void animate(); } class PlayerPiece extends GameShape implements Animatable{ public void animate(){ //code } } 这就完成了用接口实现IS-A关系。
以PlayerPiece为例,我们可以说
以上就体现了PlayerPiece的多态性。 多态方法调用仅使用于实例方法。不涉及静态方法和变量。 而在继承中子类可以使用超类的方法,也可以自己来实现这一方法,这就涉及到了重写(override)。
2.4 重写和重载 考试目标1.5 给定一个代码示例,判断一个方法是否正确地重写或重载了另一个方法,并判断该方法的合法返回值(包括协变式返回值)。 考试目标5.4 给定一个场景,编写代码,声明和/或调用重写方法或重载方法。编写代码,声明和/或调用超类、重写构造函数或重载构造函数。
2.4.1 重写方法(override) 重写的规则:
调用被重写方法的超类版本:super关键字
2.4.2 重载方法(overload) 重载的规则:
调用重载方法:调用哪个重载方法,取决于变元的类型。
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"
重载方法和重写方法中的多态性 用一个例子来说明: 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){ //这里是下表中“方法调用的代码” } }
不同调用方法的结果:
重载方法和重写方法的区别:
2.5 引用变量强制转换 考试目标5.2 给定一个场景,编写代码,演示多态性的使用。而且,要判断何时需要强制转化,还要区分与对象引用强制转换相关的编译器错误和运行时错误。
向下转型:把引用变量转换为子类类型。如Horse h = (Horse) new Animal();但如果调用父类里没有的方法,可以通过编译,但运行时会抛出java.lang.ClassCastException异常。 向上转型:把引用变量转换为超类类型。如Animal a = new Horse(); 不需要转化,这是天然的IS-A 关系。
2.6 实现接口 考试目标1.2 编写代码,声明接口。编写代码,实现或扩展一个或多个接口。编写代码,声明抽象类。编写代码,扩展抽象类。
在第一章的“声明接口”里说过,接口就是一种契约,任何实现这个接口的实现类都必须同意为该接口的所有方法提供实现。 合法的非抽象实现类必须执行以下操作:
两条规则:
2.7 合法的返回类型 考试目标1.5 给定一个代码示例,判断一个方法是否正确地重写或重载了另一个方法,并判断该方法的合法返回值(包括协变式返回值)。
2.7.1 返回类型的声明 哪些内容声明为返回类型,这主要取决于是在重写方法、重载方法还是在声明新方法。
重载方法上的返回类型 没有什么限制,重载方法关键是变元要变化。
重写、返回类型和协变式返回 从Java5开始,只要新的返回类型是被重写的(超类)方法所声明的返回类型的子类型,就允许更改重写方法中的返回类型(这就是传说中的协变式返回)。以前的Java版本要求重写的方法返回类型一定要与原来的一致。
2.7.2 返回值 六条规则: 1.可以在具有对象引用返回类型的方法中返回null。 2.数组是完全合法的返回类型。 public String[] go(){ return new String[]{"Neil","Neo","Nail"}; } 3.在具有基本返回类型的方法内,可以返回任何值或变量,只要它们能够隐式转换为所声明的返回类型。 public int foo(){ char c ='c'; return c; } 4.在具有基本返回类型的方法内,可以返回任何值或变量,只要它们能够显式地强制转换为所声明的返回类型。 public int foo(){ float f = 32.5f; return (int) f; } 5.一定不能从返回类型为void的方法返回任何值。 6.在具有对象引用返回类型的方法内,可以返回任何对象类型,只要它们能够隐式地强制转换为所声明的返回类型。换句话说,能通过IS-A测试的(也就是使用instanceof运算符测试为true)任何对象都能够从那个方法中返回。 //声明返回超类,实际返回子类 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 构造函数和实例化 考试目标1.6 给定一组类和超类,为一个或多个类编写构造函数。给定一个类声明,半段是否会创建默认构造函数。如果会,请确定该构造函数的行为。给定一个嵌套类或非嵌套类清单,编写代码,实例化该类。 考试目标5.3 解释继承对构造函数、实例或静态变量,以及实例或静态方法在修饰符方面的影响。 考试目标5.4 给定一个场景,编写代码,声明和/或调用重写方法或重载方法。编写代码,声明和/或调用超类、重写构造函数或重载构造函数。
构造函数基础:
构造函数链: 当 Horse h = new Horse(); 的时候究竟发生了什么?(Horse extends Animal,Animal extends Object)
构造函数规则:
2.8.1 判断是否会创建默认构造函数
如何证明会创建默认构造函数? 只有在类代码中没有构造函数的,才会生成默认构造函数。
如何知道它就是默认构造函数? 默认构造函数的特征:
public class Foo{ public Foo(){ super(); } }
如果超类构造函数有变元会怎样? 那在new的时候必须带参 new Animal(“monkey”);
2.8.2 重载构造函数 重载构造的时候要注意:
class A { A(){ this("foo"); } A(String s){ this(); } }
2.9 静态成员 考试目标1.3 编写代码,将基本类型、数组、枚举和对象作为静态变量、实例变量和局部变量声明、初始化并使用。此外,使用合法的标识符为变量命名。
2.9.1 静态变量和静态方法 当方法永远与实例完全无关时,我们就将它声明为static。
访问静态方法和变量:
static方法的重定义问题: 我们都知道静态方法是不能被重写的,但是可以被重定义。这个问题很迷糊人,从代码上来看,重写和重定义没有区别。那么重定义(redefine)和重写(override)有啥区别呢? 重定义操作的是静态方法,静态方法跟类有关;重写操作的是非静态方法,跟实例对象有关。看下下面的代码: 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 耦合与内聚 考试目标5.1 编写代码,实现类中的紧封装、松耦合和高内聚,并描述这样做的优点。
书云:“Sun考试对内聚和耦合所下的定义略带主观性”、“本章所讨论的内容是从考试角度出发的,绝不是关于这两条OO设计原则的真经”。 很多东西都是“兼容”标准,各自实现。 Java的OO设计目标:紧封装、松耦合、高内聚。以实现易于创建、易于维护、易于增强的目标。
耦合(Coupling):耦合是指一个类了解另一个类的程度。如果类A对类B的了解很少,仅限于类B通通过其接口公开的信息,类A并不知道B的更多具体实现,那就称类A和类B是松耦合的。我们说类B做到了紧封装。 内聚(Cohesion):内聚用于表示一个类具有单一的、明确目标的程度。一个类的目标越明确,其内聚性越高。
书中举了一个报表类的低内聚和高内聚例子,内聚性低的报表类将报表的保存、选择库、打印等方法都写在一个类里面。内聚性高的设计将这些目标明确,一个目标封装在一个类里面,如报表的打印类、选库类、保存类等等。 我个人觉得凡事都讲个度,要根据需求的复杂程度来决定设计,既不能让运行的花销太大,也不能为了XXXX而死板的设计。
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
||||||||||||||||||||||||||||||||||||||||
返回顶楼 | ||||||||||||||||||||||||||||||||||||||||
浏览 1549 次