目标一 封装和OO设计
陈述封装在面向对象设计中的好处,并编写实现紧密封装的类的代码,陈述“is a”和“has a”的关系。
“Is a”和“has a”关系
这是一个很基础的OO问题,你很可能在考试中碰到一个题目。本质上,它是为了考察你是否理解何时在谈论对象所属的类结构以及何时是在谈论一个类拥有的方法或域。
因此,猫是动物的一种(IS A),猫有尾巴(HAS A)。当然,区别可能会模糊不清。如果你是一名动物学家并且知道动物种类群的正确名字,你可能会说猫是(IS A)longlatinwordforanimalgroupwithtails(一个很长的表示有尾巴的动物群的拉丁单词)。
但是出于考试的目的,这不在考虑范围之内。
考试题目趋向于这种类型:根据一段对于潜在层次结构的描述,你会得到诸如什么应该是域,什么应该是新的子类的问题。这些问题乍一看比较复杂,但是如果你仔细阅读的话都十分明显。
封装
Java1.1的目标中没有特别提到封装,虽然你会被急切的要求学习Java而不没有机会接触概念。封装包含将类的接口从实现中分离出来。这意味着你无法“偶然地”破坏某个域的值,你必须使用方法来修改值。
通常,要实现这一点,需要创建私有变量(域),它们只能通过方法来更新和提取。这些方法的标准命名规范是
setFieldName
getFieldName
例如,你要改变形状的颜色,你会创建如下形式的方法对
public void setColor (Color c) {
cBack = c;
}
public Color getColor () {
return cBack;
}
控制变量访问的主要关键字为
public
private
protected
不要受到误导而认为访问控制系统与安全有关。它不是为防止程序员攻击变量而设计的,而是为了帮助避免不期望的修改。
使用上面Color例子的标准方法是将cBack域设为私有的。一个私有域只在当前类内部可见。这意味着程序员不能偶然地在另一个类中写代码来修改它的值。这有助于减少bug的引入。
接口与实现的分离使得在一个类中修改代码而不破坏其他代码变得更简单。
对于类的设计者这使他们能够修改类而不必破坏使用它的程序。类的设计者可以为域修改的“安全检查”插入额外的检查流程。我曾经致力于保险项目,此项目中的客户的年龄值可能小于0。如果这个值被保存在简单的域中,比如整数,就没有明显的地方可以存放检查流程。如果年龄只可以通过set和get方法访问,就可以通过这种不破坏现存代码的方式来对插入进行0或负数年龄检查。当然,随着开发的进行,会发现更多需要检查的情况。
对于类的最终用户,这意味着他们不需要理解内部工作,呈现在他们面前的是一个清晰的处理数据的接口。最终用户可以相信更新类代码不会破坏他们现有的代码。
运行时类型
因为多态机制允许在运行时选择执行方法的版本,有时候将要运行的方法并不明显的。以如下代码为例。
class Base {
int i = 99;
public void amethod () {
System.out.println (“Base.amethod ()”);
}
}
public class RType extends Base {
int i = -1;
public static void main (String argv []) {
Base b = new RType (); //<= Note the type
System.out.println (b.i);
b.amethod ();
}
public void amethod () {
System.out.println (“RType.amethod ()”);
}
}
注意,b引用的类型是Base,但是实际的类型是类RType。对amethod的调用将启动RType中的版本,但是b.i输出的调用将引用Base类中的域i。
课后测试题
问题1)假设你被给予如下设计
“一个人有姓名,年龄,地址和性别。你将要设计一个类来表示一类叫做病人的人。这种人可以被给予诊断,有配偶并且可能活着”。假设表示人的类已经创建了,当你设计病人类时如下哪些应该被包含在内?
-
registration date
-
age
-
sex
-
diagnosis
问题 2)当你试图编译并运行如下代码时会发生什么?
class Base {
int i = 99;
public void amethod () {
System.out.println (“Base.amethod ()”);
}
Base () {
amethod ();
}
}
public class RType extends Base {
int i = -1;
public static void main (String argv []) {
Base b = new RType ();
System.out.println (b.i);
b.amethod ();
}
public void amethod () {
System.out.println (“RType.amethod ()”);
}
}
1)
RType.amethod
-1
RType.amethod
2)
RType.amethod
99
RType.amethod
3)
99
RType.amethod
4)
Compile time error
问题 3)你的首席软件设计者向你展示了她正要创建的新电脑部件系统的草图。在层次结构的顶端是一个叫做Computer的类,在此之下是两个子类。一个叫做LinuxPC,另一个叫做WindowsPC。两者之间最大的不同点是一个运行Linux操作系统,另一个运行Windows系统(当然另一个不同在于一个需要不停的重启,另一个则能够可靠的运行)。在WindowsPC之下是两个子类,一个叫做Server,另一个叫做Workstation。你如何评价你的设计者的工作?
-
Give the go ahead for further design using the current scheme
-
Ask for a re-design of the hierarchy with changing the Operation System to a field rather than Class type
-
Ask for the option of WindowsPC to be removed as it will soon be absolete
-
Change the hierarchy to remove the need for the superfluous Computer Class.
问题 4)假设有如下类
class Base {
int Age = 33;
}
关于对Age域的访问,你会如何修改来改进这个类?
-
Define the variable Age as private
-
Define the variable Age as protected
-
Define the variable Age as private and create a get method that returns it and a set method that updates it
-
Define the variable Age as protected and create a set method that returns it and a get method that updates it
问题 5)下面哪些是封装的好处?
-
All variables can be manipulated as Objects instead of primitives
-
by making all variables protected they are protected from accidental corruption
-
The implementation of a class can be changed without breaking code that uses it
-
Making all methods protected prevents accidental corruption of data
问题 6)指出三个面向对象编程的主要特点?
-
encapsulation, dynamic binding, polymorphism
-
polymorphism, overloading, overriding
-
encapsulation, inheritance, dynamic binding
-
encapsulation, inheritance, polymorphism
问题 7)你如何在类中实现封装?
-
make all variables protected and only allow access via methods
-
make all variables private and only allow access via methods
-
ensure all variables are represented by wrapper classes
-
ensure all variables are accessed through methods in an ancestor class
答案
答案1)
-
registration date
-
diagnosis
对于病人来说,注册日期是一个合理的添加域,并且设计明确地指出病人应该有诊断报告。由于病人是人的一种,它应该有域age和sex(假设它们没有被声明为私有的)。
答案 2)
2)
RType.amethod
99
RType.amethod
如果这个答案看起来靠不住,试着编译并运行代码。原因是这段代码创建了一个RType类的实例但是把它赋予一个指向Base类的引用。在这种情况下,涉及的任何域,比如i,都会指向Base类中的值,但是方法的调用将会指向实际类中的方法而不是引用句柄中的方法。
答案 3)
-
Ask for a re-design of the hierarchy with changing the Operating System to a field rather than Class type
-
答案 4)
-
Define the variable Age as private and create a get method that returns it and a set method that updates it
-
答案 5)
-
The implementation of a class can be changed without breaking code that uses it
答案 6)
-
encapsulation, inheritance, polymorphism
我曾经在一次工作面试上遇到这个问题。我得到了那份工作。不能保证你一定会在考试中遇到类似的问题,但是知道的话会很有用。
答案 7)
2)make all variables private and only allow access via methods
目标二 重写和重载
编写调用重写或重载的方法以及父类的或重载过的构造函数;并且描述调用这些方法的效果。
目标的评论
术语重载(overloaded)和重写(overridden)太相近了以至于会造成混淆。我记忆的方式是想象某物被践踏(overridden)字面上的意思是被沉重的交通工具压过并且不再是其原来的样子。某物负载过重(overloaded)仍然在移动,但是负担过重的功能将使其花费巨大的努力。这只是一个区别两者的小窍门,跟Java中实际的操作没有任何关系。
重载方法
重载是Java中实现面向对象,多态机制等概念的方式之一。多态性(Polymorphism)是由多个单词组成的词语,Ply意为“很多”,“morphism”暗示着含义。因此,重载允许同一个方法名称具有多种意思或用途。方法重载是编译器的技巧,依赖于不同的参数,它允许你使用相同的名称来完成不同的动作。这样做的好处是Java可以在运行时决定调用的方法而不是在编译时决定。
因而,设想一下你正在为模拟Java认证考试设计系统接口。答案可能作为整数,布尔数或文本字符串得到。你可以为每一个参数类型创建一个方法,并给予相应的名字,比如
markanswerboolean (Boolean answer) {
}
markanswerint (int answer) {
}
markanswerString (String answer ) {
}
这样可以正常运行,但这也意味着类的未来用户需要知道更多不必要的方法名。使用一个单一的方法名会更实用,编译器可以根据参数类型和数目来决定调用的实际代码。
进行方法重载不需要记住任何关键字,你只要创建多个具有不同数目或类型的参数的同名方法就可以了。参数的名称并不重要,但是数目和类型必须不同。如下是一个markanswer方法重载的例子
void markanwwer (String answer) {
}
void markanswer (int answer) {
}
如下不是重载的实例,它会导致编译时错误,指出这是重复的方法声明。
void markanswer (String answer) {
}
void markanswer (String title) {
}
返回值类型并不是实现重载署名的要素。
因此,改变如上代码使其放回int值仍然会导致编译时错误,但是这一次指出方法不能用不同的返回值类型进行重新定义。
重写方法
重写方法意味着它的所有功能被完全取代了。重写是在子类中对一个定义在父类中的方法进行修改。为了重写方法,要在子类中定义一个与父类中具有完全相同署名的方法。这样做会覆盖父类中的方法,并且此方法的功能再也不能被直接访问了。
Java提供了一个重写的例子,就是每个类都从最高父类Object中继承的equals方法。继承的equals版本仅仅在内存中比较类引用的实例。这通常不是我们想要的,特别是对于String。对于String,你通常希望通过逐个字符的比较来确定两个字符串是否相同。为了做到这一点,String中的equals版本进行了重写,并能执行逐个字符的比较。
调用基类的构造函数
构造函数是一种在每次创建类的实例时自动运行的特殊方法。Java能够识别构造函数,因为它们具有与类本身相同的名字,并不需要返回值。与其他方法一样,构造函数可以接受参数,并且根据如何初始化类,你可以传递不同的参数。如此,以AWT包中的Button类为例,通过重载提供了两个构造函数的版本。一个是
Button ()
Button (String label)
因此,你可以创建一个没有标签的按钮,并在稍后设定,或者使用普通的版本在创建的时候就设定标签。
但是,构造函数是不能被继承的,所以如果你想从父类中获得一些有用的构造函数,缺省是不可用的。因此,如下代码将不能编译通过
class Base {
public Base () {}
public Base (int i) {}
}
public class MyOver extends Base {
public static void main (String argv []) {
MyOver m = new MyOver (10); // Will Not compile
}
}
要从父类中得到构造函数,你需要使用神奇的关键字super。这个关键字可以被当作一个方法来使用,并且传递适当的参数使之与你要求的父类中的构造函数相吻合。在以下修改了上述代码的例子中,关键字super被用来调用基类中接受integer参数的构造函数版本,这段代码编译时不会报错。
class Base {
public Base () {}
public Base (int i) {}
}
public class MyOver extends Base {
public static void main (String arg []) {
MyOver m = new MyOver (10);
}
MyOver (int i) {
super (i);
}
}
使用this ()调用构造函数
与使用super ()调用基类中构造函数的方式相同,你可以使用this调用当前类中的其他构造函数。这样,在前面的例子中你可以像下面那样定义另一个构造函数
MyOver (String s, int i) {
this (i);
}
如你猜测的,这将会调用当前类中那个只接受一个整数参数的构造函数。如果你在构造函数中使用super ()或this (),必须第一个调用它。由于只有一个能被第一个调用,你不能在构造函数中既使用super ()又使用this ()。
因此,如下代码会导致编译时错误。
MyOver (String s, int i) {
this (i);
super (); // Causes a compile time error
}
基于构造函数不能被继承的知识,很明显重写是不切合实际的。如果你有一个叫做Base的基类,你创建了一个继承它的子类,对于要重写构造函数的子类,它的名字必须跟父类相同。这会导致编译时错误。这是一个没有层次意义的例子。
class Base {}
class Base extends Base {} //Compile time error!
构造函数和类层次
构造函数总是从层次结构的顶端开始称作向下。在考试中,你很可能会遇到一些题目涉及到在类层次中多次调用this和super,你必须指出输出什么内容。当你遇到复杂的层次结构时请格外小心,这可能跟构造函数没有关系,可能由于构造函数同时调用了this和super,而导致编译时错误。
有如下例子
class Mammal {
Mammal () {
System.out.println (“Creating Mammal”);
}
}
public class Human extends Mammal {
public static void main (String argv []) {
Human h = new Human ();
}
Human () {
System.out.println (“Create Humn”);
}
}
当运行代码时,由于隐式调用了基类中的无参构造函数,首先会输出字符串“Create Mammal”。
课后测试题
问题 1)假设有如下类定义,如下哪些方法可以合法放置在“//Here”的注释之后?
public class Rid {
public void amethod (int i, String s) {}
// Here
}
-
public void amethod (String s, int i) {}
-
public int amethod (int i, String s) {}
-
public void amethod (int i, String mystring) {}
-
public void Amethod (int i, String s) {}
问题 2)假设有如下类定义,哪些代码可以被合法放置在注释“//Here”之后?
class Base {
public Base (int i) {}
}
public class MyOver extends Base {
public static void main (String arg []) {
MyOver m = new MyOver (10);
}
MyOver (int i) {
super (i);
}
MyOver (String s, int i) {
this (i);
//Here
}
}
-
MyOver m = new MyOver ():
-
super ();
-
this (“Hello”, 10);
-
Base b = new Base (10);
问题 3)假设有如下类定义
class Mammal {
Mammal () {
System.out.println (“Mamml”);
}
}
class Dog extends Mammal {
Dog () {
System.out.println (“Dog”);
}
}
public class Collie extends Dog {
public static void main (String argv []) {
Collie c = new Collie ();
}
Collie () {
this (“Good Dog”);
System.out.println (“Collie”);
}
Collie (String s) {
System.out.println (s);
}
}
将会输出什么?
-
Compile time error
-
Mammal, Dog, Good Dog, Collie
-
Good Dog, Collie, Dog, Mammal
-
Good Dog, Collie
问题 4)下面哪些论述是正确的?
-
Constructors are not inherited
-
Constructors can be overridden
-
A parental constructor can be invoked using this
-
Any method may contain a call to this or super
问题 5)试图编译并运行下面代码会发生什么?
class Base {
public void amethod (int i, String s) {
System.out.println (“Base amethod”);
}
Base () {
System.out.println (“Base Constructor”);
}
}
public class Child extends Base {
int i;
String Parm = “Hello”;
public static void main (String argv []) {
Child c = new Child ():
c.amethod ():
}
void amethod (int i, String Parm) {
super.amethod (i, Parm);
}
public void amethod () {}
}
-
Compile time error
-
Error caused by illegal syntax super.amethod (i, Parm)
-
Output of “Base Constructor”
-
Error caused by incorrect parameter names in call to super.amethod
问题 6)试图编译并运行如下代码时将发生什么?
class Mammal {
Mammal () {
System.out.println (“Four”);
}
public void ears () {
System.out.println (“Two”);
}
}
class Dog extends Mammal {
Dog () {
super.ears ();
System.out.println (“Three”);
}
}
public class Scottie extends Dog {
public static void main (String argv []) {
System.out.println (“One”);
Scottie h = new Scottie ();
}
}
-
One, Three, Two, Four
-
One, Four, Three, Two
-
One, Four, Two, Three
-
Compile time error
答案
答案 1)
-
public void amethod (String s, int i) {}
4)public void Amethod (int i, String s) {}
Amethod中的大写字母A意味着这是不同的方法。
答案 2)
4)Base b = new Base (10);
任何this或super的调用都必须是构造函数中的第一行。由于方法已经调用了this,不能有别的调用插入了。
答案 3)
-
Mammal, Dog, Good Dog, Collie
答案 4)
-
Constructors are not inherited
父类的构造函数应该使用super调用,而不是this。
答案 5)
-
Compile time error
这会导致一个错误,意思是说“你不能重写方法使其访问权限更靠近私有”。基类的amethod版本被明确的标注为public,但是在子类中没有标识符。好了,所以这不是在考察你的构造函数重载的知识,但是他们也没在考试中告诉你主题。若这段代码没有省略关键字public,将会输出“Base constructor”,选项3。
答案 6)
3)One, Four, Two, Three
类是从层次的根部往下创建的。因此,首先输出One,因为它在Scottie h初始化之前创建。然后,JVM移动到层次的基类,运行“祖父类”Mammal的构造函数。这会输出“Four”。然后,运行Dog的构造函数。Dog的构造函数调用Mammal中的ears方法,因此输出“Two”。最后,Dog的构造函数完成,输出“Three”。
目标三 创建类实例
编写创建任何具体类实例的代码,包括正常的高层次类,内部类,静态内部类和匿名内部类。
目标的注释
这份材料的一些内容在别的地方提到过,特别是目标4.1中。
实例化类
具体类是指能够被实例化为对象引用(也简称为对象)的类。因此,抽象类是不能被实例化的,所以不能创建对象引用。记住,包含任何抽象方法的类本身也是抽象的,并且不能被实例化。
实例化类的关键是使用关键字new。典型地,如下所示
Button b = new Button ();
这个语法意为变量b是Button类型的,并且包含指向Button实例的引用。但是,尽管引用的类型经常与被实例化的类的类型是一样的,但这不是必要的。因此,如下代码也是合法的
Object b = new Button ();
这个语法指出b引用的类型是Object而不是Button。
声明和实例化不是必须出现在同一行上。可以这样创建一个类的实例。
Button b;
b = new Button ();
内部类是随着JDK1.1的发布而加入的。它们允许一个类在另一个类中定义。
内部类
内部类是随着JDK1.1的发布而引入的。它们允许类被定义在其他类中,有时候被称作嵌套类。它们被广阔的使用在新的1.1事件处理模型中。你肯定会在考试中遇到嵌套类范围的问题。
这是一个简单的例子
class Nest {
class NetIn {}
}
这段代码编译后的输出是两个class文件。如你所想的,第一个是
Nest.class
另一个是
Nest$NestIn.class
这说明了嵌套类通常只是个命名规范,而不是一种新的类文件。内部类允许你逻辑性地组织类。它们在你希望访问变量时也有广泛的好处。
嵌套高层类
嵌套高层类是一个包容高层类的静态成员。
这样,修改之前的简单例子
class Nest {
static class NestIn {}
}
这种类型的嵌套经常用来简单的组合相关的类。因为类是静态的,它不需要在外部类实例存在的情况下才能实例化内部类。
成员类
我认为成员类是“普通内部类”。成员类类似于类的其他成员,你必须在创建内部类的实例之前首先实例化外部类。由于需要结合外部类的实例,Sun引入了新的语法允许在创建内部类的同时创建外部类的实例。这形成如下形式
Outer.Inner i = new Outer ().new Inner ();
为了弄清楚为此提供的新语法的意思,设法认为在上面例子中使用的关键字new属于this当前存在的实例中,因此,你可以将创建实例的代码修改为
Inner i = this.new Inner ();
因为成员类无法脱离外部类的实例存在,它可以访问外部类中的变量。
创建在方法中的类
这种类更正确的叫法应该是局部类,但是把它们当作创建在方法中,有助于让你知道最有可能在什么地方遇到它们。
局部类只在它的代码块或方法中可见。在局部类定义中的代码只能使用包容块中的final局部变量或方法的参数。你很有可能在考试中遇到这样的题目。
匿名类
你对于匿名内部类的第一反应可能是“你为什么要这么做,而且如果它没有名字,你怎么能引用它呢?”
要回答这些问题,请考虑下面的情形。你可能会遇到不停的为类实例捏造自我描述的名字的情况。这样,对于事件处理,两件需要了解的重要事情是等待处理的事件和处理器附属的模块的名字。为事件处理器实例取名字不会有多大价值。
至于如果类没有名字,如何引用该类的问题,你是做不到,而如果你需要通过名字来引用它,就不应该创建匿名类。缺乏名字有另一个副作用,就是你不能为它设定任何构造函数。
这是一个创建匿名内部类的例子
class Nest {
public static void main (String argv []) {
Nest n = new Nest ();
n.mymethod (new anon () {});
}
public void mymethod (anon i) {}
}
class anon {}
请注意匿名内部类是如何在mymethod的调用的圆括号中同时声明和定义的。
课后测试题
问题 1)下面哪些论述是正确的?
1)A class defined within a method can only access static methods of the enclosing method
2)A class defined within a method can only access final variables of the enclosing method
3)A class defined with a method cannot access any of the fields within the enclosing method
4)A class defined within a method can access any fields accessible by the enclosing method
问题 2)下面哪些论述是正确的?
-
An anonymous class cannot have any constructors
-
An anonymous class can only be created within the body of a method
-
An anonymous class can only access static fields of the enclosing class
-
The class type of an anonymous class can be retrieved using the getName method
问题 3)下面哪些论述是正确的?
-
Inner classes cannot be marked private
-
An instance of a top level nested class can be created without an instance of its enclosing class
-
A file containing an outer and an inner class will only produce one .class output file
-
To create an instance of an member class an instance of its enclosing class is required.
答案
答案 1)
2)A class defined within a method can only access final variables of the enclosing method
这种类可以访问传递给包容方法的参数
答案 2)
-
An anonymous class cannot have any constructors
答案 3)
-
An instance of a top level nested class can be created without an instance of its enclosing class
4)To create an instance of a member class an instance of its enclosing class is required.
内部类会被放在它自己的.class输出文件中,使用格式
Outer$Inner.class.
高层次嵌套类是一个静态类,因而不需要包容类的实例。成员类是普通的非静态类,因而需要有一个包容类的实例。