第二章 面向对象java
这章开头说,“做为一名SCJP6,意味着你必须精通Java中面向对象的知识。必须熟悉继承层次结构,灵活自如地应用多态性的强大能力,内聚与松散耦合必须成为你的第二性征,复合则成为你的谋生之道。”(复合类型就是引用类型,包括类引用、接口引用、数组引用)。数据库
在现在框架横飞的年代,咱们就像拼装工人同样,不问为何,不去思考,没有思想。我经常对本身说,我不要再写垃圾代码了。但老是昨夜还沉浸在《Java与模式》的优雅,次日就又屈服于疲于奔命的进度和莫名其妙的设计文档了。编程
每一个夜晚你总会在写字楼的6层看见一个为编程痴狂的老少子,一边敲着键盘,一边摇头晃脑的吼着汪峰的《我想要盛开的生命》,加油!数组
2.1 封装架构
考试目标5.1 编写代码,实现类中的紧封装、松耦合和高内聚,并描述这样作的优势。框架
为何要封装?ide
一般在类中,咱们的实例变量(定义在类中,但位于任何方法以外,而且只有在实例化类时才会被初始化的变量),还有一些只有本类会用到的方法,都用private来声明,而后若是须要对实例变量访问,就写一些getter和setter。函数
若是都public了呢?好比你定义了一条记录的ID是用sequence获取的,一个调用者本身指定了一个旧的ID给对象,作插入数据库的操做,那必定是要抛异常了。测试
如何实现封装:动画
- 保护实例变量(使用访问修饰符,一般是private)
- 创建public访问器方法,强制调用代码使用这些方法而不是直接访问实例变量。
- 对于访问器方法,使用JavaBeans命名规则set<propertyName>和get<propertyName>
2.2 继承、IS-A、HAS-A关系
考试目标5.5 编写代码,实现IS-A关系和/或HAS-A关系。
书云:“不使用继承,即使编译最微小的Java程序也几乎是不可能的。”,继承能够说是面向对象的基础。
子类继承超类,子类继承了超类的非私有的成员变量和成员方法,就像这些成员原本就是他们本身的同样。
须要注意的是Java不支持多重继承,一个类只能直接继承一个类。
继承的做用:
- 促进代码的复用。这个很好理解,全部的类都继承自Object,它提供给全部的类equal()等方法,若是全部的类都要本身实现这个方法的话,那太可怕了。
- 使用多态性。书中给了个载入游戏图形的例子:
//游戏图形的超类,全部子类经过继承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对象均可以被看做是多态的。
访问对象的惟一方式是经过引用变量。关于引用,要记住:
- 引用变量只能属于一种类型。一经声明,类型就永远不能再改变(尽管它引用的对象能够改变类型)。
- 引用是一个变量,所以它能够从新赋予给其余对象(除非该引用被声明为final)。
- 引用变量的类型决定了能够在该变量引用的对象上调用的方法。
- 引用变量能够引用具备与所声明引用的类型相同的任何对象,或者——最重要的一点是——它能够引用所声明类型的任何子类型。
- 引用变量能够声明为类类型或接口类型。若是将变量声明为接口类型,它就能够引用实现该接口的任何类的任何对象。
前面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 IS-A Object
- PlayerPiece IS-A GameShape
- PlayerPiece IS-A PlayerPiece
- PlayerPiece IS-A Animatable
以上就体现了PlayerPiece的多态性。
多态方法调用仅使用于实例方法。不涉及静态方法和变量。
而在继承中子类可使用超类的方法,也能够本身来实现这一方法,这就涉及到了重写(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)
重载的规则:
- 重载方法必须改变变元列表。
- 重载方法能够改变返回类型。
- 重载方法能够改变访问修饰符。
- 重载方法能够声明新的或更广的检验异常。
- 方法可以在同一个类或者一个子类中被重载。
调用重载方法:调用哪一个重载方法,取决于变元的类型。
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){ //这里是下表中“方法调用的代码” } }
不一样调用方法的结果:
方法调用的代码 | 结果 | 解释 |
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 引用变量强制转换
考试目标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 给定一个场景,编写代码,声明和/或调用重写方法或重载方法。编写代码,声明和/或调用超类、重写构造函数或重载构造函数。
构造函数基础:
- 构造函数是用来建立新对象的,每当咱们“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(Animal.NAME)
- 抽象类具备构造函数,这些构造函数老是在实例化具体子类时才调用。
- 接口没有构造函数。接口不是对象继承树的一部分。
- 调用构造函数的惟一方法是从另外一个构造函数内部进行调用。
2.8.1 判断是否会建立默认构造函数
如何证实会建立默认构造函数?
只有在类代码中没有构造函数的,才会生成默认构造函数。
如何知道它就是默认构造函数?
默认构造函数的特征:
- 具备与类相同的访问修饰符。
- 没有任何变元。
- 包含super();
public class Foo{ public Foo(){ super(); } }
若是超类构造函数有变元会怎样?
那在new的时候必须带参 new Animal(“monkey”);
2.8.2 重载构造函数
重载构造的时候要注意:
- this()或super()必定要在第一行。
- 不要写以下的死循环代码:
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而死板的设计。