2023年6月20日发(作者:)

第1部分  语法 程序员们总是被层出不穷的复杂问题所困扰—— 设计和编写程序的语言本身就是复杂的而非它们的解决方案了假如我们最基本的开放工具Smalltalkµ±Ê¹ÓÃÆäËûÓïÑԵijÌÐòԱתÓÃJava来编程时因此这些程序员通常会认为这些特性在Java中和在以前所使用的语言中表现一致这些想法在C++程序员中尤其普遍绊倒

本部分包括以下10个单元什么时候

Item 2较字符串的不同之处==”本单元解释了这两种方法比

 方法并非真的被覆盖了并借鉴了Objective Cu 1 第1部分 语法

Item 3

Item 4我们培训新的Java学员时它本单元解释了基本类型的转换和提升的规则本单元给出了一个经典的当编译器怎么会没发现后

Item 6不能访问被覆盖的方法

Òþ²Ø±äÁ¿³ÉÔ±本单元讨论了这一最常见的陷阱并且和this引用一起讨论提前引用读完以提前引用它设计可继承的构造函数发可重用Java类的程序员来说

Item 9

Item 10以及如何去避免对于每一个想开

本单元对从C++转换到Java的程序员特别有价值

¶Ì·本单元解释了Java编程中另一个常见的陷阱单元中也举了一个使用短路运算符的清晰例子被覆盖的虽然它的本意并非欺骗你想必你已经阅读了一两本这样的Java书籍封装理解这3个概念对于领会Java语言来说至关重要

覆盖实例方法会在Item 5谈到如果你还不明白两者的区别假如你已经急不可待地喊出那么不过

2 t

我承认Item 1: 什么时候方法并非真的被覆盖了

这个例子摘自Java语言规范8.4.8.5节

Goodnight, Dick

要是你得出了同样的输出结果果和答案不一致Sub类继承了Super类个main方法我们先分析一下各个类如果你的结Test类只有一u 3 第1部分 语法

在Test类的第5行中在这里虽然变量s的数据类型为Super类如果你对此有些迷惑变量s是一个被强制转换为Super型的Sub类的实例加上一个字符串紧随其后的是()的返回值还是Sub类的方法两个类中的name()方法都不是静态方法因为Sub类继承了Super类所以Sub类中的name()方法覆盖了Super类中的name()方法这样一来Dick

至此现在我们需要判断被调用的greeting()方法究竟是Super类的还是Sub类的两个类中的greeting()方法都是静态方法尽管事实上Sub类的greeting()方法具有相同的返回类型以及相同的方法参数由于变量s被强制转换为Super型因此ng()的返回值为请记住这条规则静态方法被隐藏”²»Äܸ²¸Ç¾²Ì¬·½·¨µÄ¶ÁÕßÖ®Ò»

现在你可能会问”È»¶øÊµ¼ÊÉÏÎÒÃǸոÕÔÚÕâ¸öSuper/Sub类的例子中已经解释了两者的不同即使变量s是Sub类的一个实例我们仍旧能够将s强制转换为Super型与被隐藏的方法不同除了覆盖它们的类之外这就是为何变量s调用的是Sub类的

本单元简要解释了Java语言中一个不时引起混淆的问题理解隐藏静态方法和覆盖实例方法的区别的最佳方式再重复一次规则被覆盖的方法只有覆盖它们的类才能访问它们

现在你终于明白标题里问题的答案了吧答案就是另外请谨记4 t Item 2: ( )方法与运算符的用法比较

l

l

l

l

试图用子类的静态方法隐藏父类中同样标识的实例方法是不合法的

试图用子类的实例方法覆盖父类中同样标识的静态方法也是不合法的

静态方法和最终方法不能被覆盖

抽象方法必须在具体类1中被覆盖=== =ÕâÖÖÀ§»óÖ÷ÒªÊÇ(...)方法和= =运算符的混淆然而实际上看看下面的例子其中两个被赋值以常量表达式“Programming”ʹÓÃequals(...)方法和“= =”运算符进行比较

(s1): true

(s2): true

s0 == s1: false

s0 == s2: true

1 具体类也就是抽象方法所属抽象类的非抽象子类

u 5 第1部分 语法

()方法比较的是字符串的内容会对字符串中的所有字符如果完全相等在这种情况下个字符串都是相同的我们得到的返回值均为trueÔÚÕâÖÖÇé¿öÏÂs0和s1并不是同一个String实例读者也许会问这个问题的答案来自于Java语言规范中关于字符串常量的章节“Programming”它们在编译期就被确定了例如s2Java确保一个字符串常量只有一份拷贝Java会设置两个变量的引用为同一个常量的引用constant poolJava会跟踪所有的字符串常量并被保存在已编译的.class文件中的一些数据类当然还有字符串常量的信息变量s0和s2被确定constant pool resolution¸ÃÏî²Ù×÷Õë¶Ô×Ö·û´®µÄ´¦Àí¹ý³ÌÕª×ÔJVM规范5.4节

n 如果另一个常量池入口被标记为CONSTANT_String2ͬÑùµÄUnicode字符序列已经被确定

n 否则那么这项操作的结果就是那个相同String实例的引用一个新的String实例会被创建这个String实例就是该项操作的结果当常量池第一次确定一个字符串在常量池中都会得到之前创建的String实例它创建了字符串常量的一份拷贝到另一个String实例中对s0和s1的引用的比较结果是falseÕâ¾ÍÊÇΪºÎs0==s1的操作在某些情况下与(s1)不同而(s1)实际上执行的是字符串内容的比较字符串常量由0个或多个包含在双引号之内的字符组成

2 它在.class内部使用

escape sequence6 t Item 2: ( )方法与运算符的用法比较

存在于.class文件中的常量池并且可以扩充当针对一个String实例调用了intern()方法因为实例已经存在所以已存在的实例的引用被加入到该常量池

01: import .*;

02:

03: public class StringExample2

04: {

05: public static void main (String args[])

06: {

07: String sFileName = "";

08: String s0 = readStringFromFile(sFileName);

09: String s1 = readStringFromFile(sFileName);

10:

11: n("s0 == s1: " + (s0 == s1));

12: n("(s1): " + ((s1)));

13:

14: ();

15: ();

16:

17: n("s0 == s1: " + (s0 == s1));

18: n("s0 == (): " +

19: (s0 == ()));

20: }

21:

22: private static String readStringFromFile (String sFileName)

23: {

24: //…read string from file…

25: }

26: }

这个例子没有设置s0和s1的值为字符串常量并把值分配给readStringFromFile(...)方法创建的String实例序对两个被新创建为具有同样字符值的String实例进行处理你会再次注意到但它们的内容是相同的

程u 7 第1部分 语法

s0 == s1: false

(s1): true

s0 == s1: false

s0 == (): true

第14行所做的是将String实例的引用s0存入常量池对()方法的调用这样一来正是我们所期望的s0与s1仍旧是截然不同的两个String实例而()返回的是常量池中的引用值假如我们希望将实例s1存入常量池中然后请求垃圾回收器回收被指向s0的String实例()方法的调用

总的来说equality comparisonÓ¦¸ÃʼÖÕʹÓÃ(...)方法如果你还是习惯性地使用==运算符因为当n和m均为String实例的引用时假如你打算充分利用常量池的优势

Item 3: Java是强类型语言 每个Java开发人员都需要很好地理解Java所支持的基本数据类型它们与你以前所使用的语言有什么不同呢Java是强类型的这些基本数据类型算得上是构造对象的了Java编译器能够及时地在开发过程中捕捉到许多简单细微的错误不过在Java中仍然有一些微妙之处Java的基本数据类型始终被描述在JVM中这就使得位运算得以较安全地执行boolean型是不可转换的Java不允许你编写在boolean型和非boolean型之间转换的代码那么你可能编写过一些诸如0等于false或非0等于true的“优雅”的代码8 t Item 3: Java是强类型语言

在C语言中编写代码检查一个函数的返回值类似的代码在Java中是不能编译的必须给它一个这样的值所以你由于基本数据类型的转换可以隐性地发生以及如何工作而且一般来说loss of precision±àÒëÆ÷»áÏòÄã·¢³ö¾¯¸æarithmetic operationÊý¿ª·¢ÈËÔ±¶¼Ôø¾-д¹ýµ¼ÖÂÊý¾ÝÒâÍâÇжÏ的代码Truncation类的第10行将会输出“2.0”

 ´ó¶à下面的01: public class Truncation

02: {

03: static void printFloat (float f)

04: {

05: n ("f = " + f);

06: }

07:

08: public static void main (String[] args)

09: {

10: printFloat (12 / 5); // data lost!

11: printFloat ((float) 12 / 5);

12: printFloat (12 / 5.0f);

13: }

14: }

 因为12和5都是integer型分丢失了当printFloat方法并没有得到期望的float型参数时修复很简单那么另一个也会被提升float型第11行和12行操作正常因此小数部为转换操作会自动发生u 9 第1部分 语法

况下的转换被称作是因为它们都会被转换成能够存储较大数据的类型可以为int变量分配一个byte值图1.1展示了扩展转换当一个类型被转换为具有更多比特数的类型时

图1.1 扩展转换

注意型度损失或者把一个long型转换成为double换句话说就是一些最不重要的位可能会丢失下面这个例子的输出结果发生的精对扩展转换有更详细的描述但是基本类型之间的扩展转换永远不会导致运行期异常narrowing例如¼´Èκβ»Í¬ÓÚͼ1.1中从左到右顺序的转换包括float型和double型包括shortoverflow¶¼»áµÃµ½±àÒëÆÚ´íÎócast可以避免这种错误“我清楚我在干什么 10 t Item 3: Java是强类型语言

隐性类型转换

顾名思义时而是自动发生的为了方便short或char型且表达式的值一定为int型当然不能超过变量类型的数值范围程序TypeConversion的第7行中的赋值可以编译通过范围-128到127但是第8行无法编译通过

01: public class TypeConversion

02: {

03: static void convertArg (byte b) { }

04:

05: public static void main (String[] args)

06: {

07: byte b1 = 127;

08: // byte b2 = 128; // won't compile

09:

10: // convertArg (127); // won't compile

11: convertArg ((byte)127);

12:

13: byte c1 = b1;

14: // byte c2 = -b1; // won't compile

15: int i = ++b1; // overflow

16: n ("i = " + i);

17: }

18: }

隐性类型转换能够在3种情况下发生方法调用和算术运算假如表达式两边的类型不同

类似地参数也有可能需要被转换类型()方法期望的参数是double型由于这是一个扩展转换不过请注意隐性的窄化转换不支持方法调用上面例子的第10行不能被编译即对参数进行显性的强制转换

第3种情况被称之为算术运算假如你希望将一个int型和一个float型相加求和其中较窄的类型总是被转换成较宽的类型u 11 第1部分 语法

同理所有的byte但非全部诸如第14行的一元减法运算符×ÜÊÇÖÁÉÙ±»ÌáÉýΪint型将会得到下面的错误信息然后警告你int型的值无法赋值给byte变量可能预料到第15行中byte型的值在增量操作时16行代码将会输出“i = 128”吧由于溢出的发生= -128”你那么第“i

扰几个小时ÓÉÓÚÒ»¸ö¼òµ¥µÄ´íÎóÒýÆðµÄBugÕâÑùµÄBug的特征就在于或许你会因此困看看你要花费多长时间解决它12 t Item 4: 那是构造函数吗

这个IntAdder类相当简单3个private属性分别为x一个构造函数以及main方法在第21行位于第7行的构造函数设置了属性x²¢½«ËüÃǵĺ͸³¸øzÎÒÃǵ÷ÓÃprintResults方法执行IntAdder类后的输出结果应该是如果你认为没有问题该是实际的输出应你是否找出了问题所在呢好吧第21行第7行的IntAdder类构造函数设置了属性x和y的值难道这个构造函数没有工作吗就会明白它实际上是一个方法刚才你一定错误地将带有“void”返回类型的IntAdder方法当成了IntAdder类的构造函数了你也许会问那么当初始化实例ia时调用的构造函数是什么因为代码中没有构造函数这个默认的构造函数不用实现但其功能就和下面的这个构造函数一致在第21行被创建的对象ia的属性x方法被调用时

The value of 'z' is '0'.

当第22行的printResults这个“简单”的小Bug还揭示了几个关键的问题我们注意到但编译并运行它的时候也就是说但是不推荐使用这样的命名这点很容易理解只有构造函数的名字才能和类名相同假如有和类同名的方法从另一个角度来说同样违背了命名规范并且首字母应该小写类名一般应该为名词或者名词短语关于命名规范在u 13 第1部分 语法

这个例子中数的构造函数在一个类中假如没有显式的构造函数存在而且这个构造函数是空的假如类中已经存在了一个带任意参

Item 5: 不能访问被覆盖的方法 设想一下而这个编辑器支持RTF文件在这个应用程序自己的代码中或者通过访问编辑器的DocumentManager对象来打开新的文档一个Document对象被返回文法检查等等你接到了客户的电话你觉得实现这个需求没什么问题而且因为所有新加入的特性都被放置在Document类的一个名为HTMLDocument的子类中无需修改而且当需要的Document对象被DocumentManager类返回后以便使用HTMLDocument类的新特性你拿着新版本的编辑器以及一些利用新的HTML特性的代码这个新功能将在一个月内实现当你还自我感觉良好时上面指出了关于拼写检查程序的一个问题但是你自认为是一个聪明的Java程序员让它使用Doucment类的spellCheck()方法厂商已经声明了Document类的代码未被改动你就尝试着总去调用Document类的spellCheck()方法还是声明一个局部Document类型的变量并赋值为传入的Document对象所有的结果都将是失败的厂商的技术支持也明确地告诉你Document类没有被修改过并最终在8.4.6.1章节中看到了下面这段话14 t Item 5: 不能访问被覆盖的方法

“可以通过包含关键字super的方法调用表达式来访问被覆盖的方法尝试用全局名或强制转换为父类型都是无效的一切都清楚了这第三方文本编辑器的类库总是创建一个HTMLDocument型的对象强制转换而不是所期望的Document类的spellCheck()方法当你创建了一个覆盖了父类实例方法的子类时就是使用super关键字永远不能调用父类的这些被覆盖的实例方法

01: class DocumentManager

02: {

03: public Document newDocument()

04: {

05: return (new HTMLDocument());

06: }

07: }

08:

09: class Document

10: {

11: public boolean spellCheck()

12: {

13: return (true);

14: }

15: }

16:

17: class HTMLDocument extends Document

18: {

19: public boolean spellCheck()

20: {

21: n("Trouble checking these darn hyperlinks!");

22: return (false);

23: }

24: }

25:

26: public class OverridingInstanceApp

27: {

28: public static void main (String args[])

29: {

30: DocumentManager dm = new DocumentManager();

u 15 第1部分 语法

31: Document d = ument();

32: boolean spellCheckSuccessful = heck();

33: if (spellCheckSuccessful)

34: n("No spelling errors where found.");

35: else

36: n("Document has spelling errors.");

37: }

38: }

在第32行中所以这个例子的输出结果是这样的然而无法访问父类中被子类覆盖的方法的原则是非静态方法并非真的覆盖参见Item1ËüÃÇÈÔ¾ÉÄܱ»·ÃÎʽ«µÚ11行和第19行的代码替换为下面这行

public static boolean spellCheck(Document d)

也就然后再将第32行代码替换为下面这行

boolean spellCheckSuccessful = heck(d);

最后执行修改后的程序

No spelling errors where found.

你也许会质疑这里所举的例子是否真实地发生过这个例子是虚构的通常来说也就是父类但是实际上它们得到的是新类的实例JavaSoft有一个不错的例子在旧版本的JDK中例如paint(...)和update(...)它们在新版的JDK中传递的都是Graphics2D对象Graphics2D类覆盖了Graphics类中的一些实例方法draw3DRect()假设在Graphics2D类中的draw3DRect()方法有一个BugÄÇôÕýÈçÄãµÄÅжÏ

16 t Item 6: 避免落入的陷阱

尽管在子类的外部无法访问父类中被覆盖的实例方法假如你怀疑你所使用的某个对象实际上是一个子类的实例又如果你是编写增加了新功能的子类的程序员或者保证在编写程序时而不是覆盖父类方法实现的隐藏变量成员与理解方法是如何被覆盖同等重要的就是假如你认为自己已经理解了方法是如何被覆盖的那么你最好仔细地读读本节无意地隐藏了一个变量成员或者错误地认为已经“覆盖”了一个变量成员

01: public class Wealthy

02: {

03: public String answer = "Yes!";

04: public void wantMoney()

05: {

06: n("Would you like $1,000,000? > "+ answer);

07: }

08: public static void main(String[] args)

09: {

10: Wealthy w = new Wealthy();

11: ney();

12: }

13: }

输出结果为Wealthy类具有一个名为answer的实例变量以及一个main方法一个Wealthy类的实例w被创建输出了一个问题以及作为回答的实例变量answer的值现在让我们来看看一个没有正确回答这个问题的例子u 17 第1部分 语法

01: public class Poor

02: {

03: public String answer = "Yes!";

04: public void wantMoney()

05: {

06: String answer = "No!"; // hides instance variable answer

07: n("Would you like $1,000,000? > " + answer);

08: }

09: public static void main(String[] args)

10: {

11: Poor p = new Poor();

12: ney();

13: }

14: }

输出结果为本例输出中的回答已经变成了“No»Ø´ðµÄ½á¹û¾ÍÊǾֲ¿±äÁ¿µÄÖµ产生了意想不到的结果

l

l

l

不同类型的Java变量

何种变量能被隐藏

如何访问被隐藏的变量

不同类型的Java变量

局部变量answer隐藏了实例变量answerÒ²ÏÔ¶øÒ×¼û在许多复杂的环境下为了避免“数据隐藏”所带来的问题Java一共有6种变量类型实例变量以及局部变量实例变量是在类体中声明的非静态变量方法参数是用来传入一个方法体的18 t

构造函数参数Item 6: 避免落入的陷阱

下面的例子声明了各种类型的变量最后简名是一个变量的专一标识符Types类的类体构造函数体声明的所在代码块

在这个代码块中实例变量x的简名就是“x”±äÁ¿³ÉÔ±x和y的作用范围就是构造函数参数的作用范围就是整个局部变量的作用范围就是它被它在第11行也就是createURL方法中被声明

u 19 第1部分 语法

何种变量能被隐藏

实例变量和类变量能被隐藏局部变量去隐藏一个参数编译器也会报错假如用一个同名的同样地第5行的局部变量不能和方法参数args同名引起编译器错误实例变量和类变量如何被隐藏

第7行的局部变量s也会

同名的局部变量或者同名的参数变量成员也能被子类的同名变量成员隐藏将在其作用范围内与一个变量成员同名的方法参数与一个变量成员同名的构造函数参数依此类推将在catch语句块中隐藏掉这个变量成员20 t Item 6: 避免落入的陷阱

上例中System.out.println方法输出的type变量的值将是构造函数参数type的值

子类的变量成员将会隐藏掉父类中同名的变量成员Bike类中的实例变量typeµÄÀà±äÁ¿½«»áÒþ²Ø¸¸ÀàÖÐÓë֮ͬÃûµÄÀà±äÁ¿ºÍʵÀý±äÁ¿ÀàÖÐÓë֮ͬÃûµÄÀà±äÁ¿ºÍʵÀý±äÁ¿×ÓÀà×ÓÀàµÄʵÀý±äÁ¿Ò²»áÒþ²Ø¸¸

04: }

01: public class Line

02: {

03: int x;

04: }

01: public class MultiLine extends Line implements Stretchable02: {

03: public MultiLine()

04: {

05: n("x = " + x);

06: }

07: }

上例的程序可以编译通过MultiLine类将无法通过编译

那么 u 21 第1部分 语法

如何访问被隐藏的变量

通过全局名的实例变量关键字“this”可以限定一个正被局部变量隐藏类变量也可以被限定

01: public class Wealthy

02: {

03: public String answer = "Yes!";

04: public void wantMoney()

05: {

06: String answer = "No!";

07: n("Do you want to give me $1,000,000? > " +

08: answer);

09: n("Would you like $1,000,000? > " +

10: );

11: }

12: public static void main(String[] args)

13: {

14: Wealthy w = new Wealthy();

15: ney();

16: }

17: }

输出结果为Wealthy类具有一个名为answer的实例变量为了对wantMoney方法中的每个问题都给出正确的回答也需要访问实例变量answer我们告知编译器而非局部变量answerµÚÒ»¸öÎÊÌâµÄ»Ø´ðÊǾֲ¿±äÁ¿answer的值它被关键字“this”限定了

01: public class StillWealthy extends Wealthy

02: {

03: public String answer = "No!";

22 t Item 6: 避免落入的陷阱

04: public void wantMoney()

05: {

06: String answer = "maybe?";

07: n("Did you see that henway? > " + answer);

08: n("Do you want to give me $1,000,000? > " +

09: );

10: n("Would you like $1,000,000? > " + );

11: }

12: public static void main(String[] args)

13: {

14: Wealthy w = new Wealthy();

15: ney();

16: }

17: }

输出结果为上例中第7行问题的回答的是父类Wealthy的实例变量变量隐藏与方法覆盖的区别

隐藏变量和覆盖方法有许多区别或者强制转换自己为其父类的类型它被关键字“this”限定了

第8行问题的回答输出一个类的实例

01: public class Wealthier extends Wealthy

02: {

03: public void wantMoney()

04: {

05: n("Would you like $2,000,000? > " + answer);

06: }

07: public static void main(String[] args)

08: {

09: Wealthier w = new Wealthier();

10: ney();

11: ((Wealthy)w).wantMoney();

12: }

13: }

u 23 第1部分 语法

输出结果为Wealthier类继承了Wealthy类main方法创建了Wealthier类的一个实例w×¢Òâ½Ó×Å并再次调用它的wantMoney()方法上面的例子说明是无法访问父类中被覆盖的方法的一个被隐藏的变量与一个被覆盖的方法的区别强制转换子类的实例为父类类型后

01: public class Poorer extends Wealthier

02: {

03: String answer = "No!";

04: public void wantMoney()

05: {

06: n("Would you like $3,000,000? > " + answer);

07: }

08: public static void main(String[] args)

09: {

10: Poorer p = new Poorer();

11: ((Wealthier)p).wantMoney();

12: n("Are you sure? > " + ((Wealthier)p).answer);

13: }

14: }

输出结果为main方法创建了一个Poorer类的实例pÕýÈçÔÚǰһ¸öÀý×ÓÖнâÊ͵Äͨ¹ýÇ¿ÖÆ×ª»»Òò´ËËüµÄ»Ø´ðÊÇ“No!”main方法又问到这个问题答案就不再是子类变量的值这种情况的出现24 t Item 7: 提前引用

仅仅“隐藏”了父类的变量成员只要将子类实例强制转换为父类类型数据隐藏与方法覆盖的另外一个不同而静态变量相同地而变量成员却可以隐藏父类同名变量成员

通过理解本节讨论的知识点会帮助应用程序得到你所期望的结果

Item 7: 提前引用 类变量以及静态初始化块是在类被加载进JVM时执行初始化操作的“静态初始化块和类变量是按照其在代码中出现的顺序依次执行初始化操作的换句话说一般来说看看下面的代码将会得到一个如下的错误即使变量first和second都处在同一个作用范围内而且编译器会捕捉到这个错误绕开这个保护措施还是有可能的而且方法内部对类变量的访问不会按照这个原则被检查

01: public class ForwardReferenceViaMethod

02: {

03: static int first = accessTooSoon();

04: static int second = 1;

05:

06: static int accessTooSoon()

07: {

u 25 第1部分 语法

08: return (second);

09: }

10:

11: public static void main (String[] args)

12: {

13: n ("first = " + first);

14: }

15: }

然而由于在初始化second之前法得到的是second的默认值输出结果first的值为0

问题并非这么简单那么你必须保证

那么方Item 8: 设计可继承的构造函数 包括Java在内就是继承它

软件开发过程中举例来说你经常会发现代码是如此复杂且难以维护当程序原本就比较容易扩展时一般来说代价就要小得多了你可能会遭遇到许多阻碍甚至是抵触扩展性的陷阱那么当你设计类以及编写代码时

我所见过的最常见的陷阱之一不论你的方法设计得多么完美其它开发人员在继承你的类时因为构造函数是不能被覆盖的子类都必须承受它那么它的任何子类都一定会做同样的工作在编写子类的开发人员无权访问父类的源代码时举例来说

01: import .*;

02: import .*;

03: import .*;

04: import .*;

代码容易被复用不幸的是26 t Item 8: 设计可继承的构造函数

05:

06: import .*;

07: import .*;

08:

09: public class ListDialog extends JDialog

10: implements ActionListener, ListSelectionListener

11: {

12: JList model;

13: JButton selectButton;

14: LDListener listener;

15: Object[] selections;

16:

17: public ListDialog (String title,

18: String[] items,

19: LDListener listener)

20: {

21: super ((Frame)null, title);

22:

23: JPanel buttonPane = new JPanel ();

24: selectButton = new JButton ("SELECT");

25: ionListener (this);

26: bled (false);//nothing selected yet

27: (selectButton);

28:

29: JButton cancelButton = new JButton ("CANCEL");

30: ionListener (this);

31: (cancelButton);

32:

33: tentPane().add (buttonPane, );

34:

35: er = listener;

36: setModel (items);

37: }

38:

39: void setModel (String[] items)

40: {

41: if ( != null)

42: ListSelectionListener (this);

43: = new JList (items);

44: tSelectionListener (this);

45:

46: JScrollPane scroll = new JScrollPane (model);

47: tentPane().add (scroll, );

48: ();

49: }

50:

51: /** Implement ListSelectionListener. Track user selections. */

52:

53: public void valueChanged (ListSelectionEvent e)

54: {

55: selections = ectedValues();

u 27 第1部分 语法

56: if ( > 0)

57: bled (true);

58: }

59:

60: /** Implement ActionListener. Called when the user picks the

61: * SELECT or CANCEL button. Generates the LDEvent. */

62:

63: public void actionPerformed (ActionEvent e)

64: {

65: ible (false);

66: String buttonLabel = ionCommand();

67: if ( ("CANCEL"))

68: selections = null;

69: if (listener != null)

70: {

71: LDEvent lde = new LDEvent (this, selections);

72: alogSelection (lde);

73: }

74: }

75:

76: public static void main (String[] args) // self-testing code

77: {

78: String[] items = (new String[]

79: {"Forest", "Island", "Mountain", "Plains", "Swamp"});

80: LDListener listener =

81: new LDListener()

82: {

83: public void listDialogSelection (LDEvent e)

84: {

85: Object[] selected = ection();

86: if (selected != null) // null if user cancels

87: for (int i = 0; i < ; i++)

88: n (selected[i] .toString());

89: (0);

90: }

91: };

92:

93: ListDialog dialog =

94: new ListDialog ("ListDialog", items, listener);

95: ();

96: }

97: }

 出于程序完整性的考虑 

01: public interface LDListener

02: {

03: public void listDialogSelection (LDEvent e);

04: }

 01: import bject;

28 t Item 8: 设计可继承的构造函数

02:

03: public class LDEvent extends bject

04: {

05: Object source;

06: Object[] selections;

07:

08: public LDEvent (Object source, Object[] selections)

09: {

10: super (source);

11: ions = selections;

12: }

13:

14: public Object[] getSelection()

15: {

16: return (selections);

17: }

18: }

ListDialog类看上去写得相当不错假设你需要开发一个向用户显示一列声音文件的菜单能够听到相应的声音所以你决定提供一个简单的API²¢²â¶¨¸Ã·¾¶ÏµÄÉùÒôÎļþÁбí¾ÍÄܹ»ÊµÏÖÕâ¸öÄ¿µÄÁË以便播放用户选择的声音你可以调用上例第44行显示的addListSelectionListener(...)方法来解决而且没有任何访问它的方法而且你恐怕要从头开始做起了一切都还不错当你尝试去继承ListDialog类上例第一部分的第17行然而你并没有这个资源由于构造函数第一件要做的事就是调用super(...)语句 问题出现了去翻翻Javadoc文档它似乎可以提供帮助你首先在子类构造函数中试着实例化一个items参数为空的ListDialog类调用setModel(...)方法u 29 第1部分 语法

05: String[] items = getItems (path);

06: setModel (items);

07: tSelectionListener (this);

08: ectionMode (_SELECTION);

09: }

这个解决方法看上去合情合理将会得到下面的错误定方法你找到了错误的关键导致了意想不到的结果你终于判尝试调用一个没有item的JList对象的pack()

那么现在该如何是好呢你还算幸运的因此你可以用自定义的同名方法覆盖掉它

01: import .*;

02: import .*;

03: import .*;

04: import .*;

05:

06: import .*;

07: import .*;

08:

09: public class SoundDialog extends ListDialog

10: implements FilenameFilter, ListSelectionListener

11: {

12: String selection;

13:

14: public SoundDialog (String title, LDListener ldl, String path)

15: {

16: super (title, null, ldl);

17: String[] items = getItems (path);

18: setModel (items);

19: }

30 t Item 8: 设计可继承的构造函数

20:

21: public void setModel (String[] items)

22: {

23: if (items != null)

24: {

25: el (items);

26: tSelectionListener (this);

27: ectionMode

28: (_SELECTION);

29: }

30: }

31:

32: public String[] getItems (String path)

33: {

34: File file = new File (path);

35: File soundFiles[] = les (this);

36: String[] items = new String [];

37: for (int i = 0; i < ; i++)

38: items = e();

39: return (items);

40: }

41:

42: // implement FilenameFilter

43: public boolean accept (File dir, String name)

44: {

45: return (th (".aiff") ||

46: th (".au") ||

47: th (".midi") ||

48: th (".rmf") ||

49: th (".wav"));

50: }

51:

52: // implement ListSelectionListener

53: public void valueChanged (ListSelectionEvent e)

54: {

55: hanged (e);

6: JList items = (JList) rce();

57: String fileName = ectedValue().toString();

58: if (! (selection))

59: {

60: selection = fileName;

61: play (selection);

62: }

63: }

64:

65: private void play (String fileName)

66: {

67: try

68: {

69: File file = new File (fileName);

70: URL url = new URL ("file://" + olutePath());

u 31 第1部分 语法

71: AudioClip audioClip = ioClip (url);

72: if (audioClip != null)

73: ();

74: }

75: catch (MalformedURLException e)

76: {

77: n (e + ": " + sage());

78: }

79: }

80:

81: public static void main (String[] args) // self-test

82: {

83: LDListener listener =

84: new LDListener()

85: {

86: public void listDialogSelection (LDEvent e)

87: {

88: Object[] selected = ection();

89: if (selected != null) // null if user cancels

90: for (int i = 0; i < ; i++)

91: n (selected[i].toString());

92: (0);

93: }

94: };

95: SoundDialog dialog =

96: new SoundDialog ("SoundDialog", listener, ".");

97: ();

98: }

99: }

回味一下是不需要的请确信你这么做是必要的为其它不太复杂的构造函数提供一个便利的实现要考虑增加额外版本的构造函数如果你能够提供一个无参数的构造函数那就再好不过了仍未被实例化的变量当然它开发人员使用它创建一些无效的对象了32 t

那么上述这些工作都调用了一个private方法因为它也许会那么你就

例如你必须仔细地检查其它方法没有使用那么你就应该考虑限制访问它了但是你可以不用担心其Item 9: 通过引用传递基本类型

需要注意的是即使一个空的JList对象实际上是无效的那么在使用JList对象之前

显而易见Item 9: 通过引用传递基本类型 假如你曾经是一名C或C++程序员因为它没有指针的概念Java语言没有引入指针指针运算和从一个函数中返回多个值在Java中至于第二件事在通过引用而不是值来传递参数的情况下Java程序员都是利用引用来访问所有被实例化的对象的接口都归为引用类型基本数据类型被用来存储特定类别的信息Java语言一共提供了这些基本数据类型byte型int型char型一个需要理解的重要概念是因此

看到这假如基本数据类型不能通过引用来传递的话而且

虽然基本数据类型不能直接通过引用来传递这也就意味着必须使用引用类型来封装基本数据类型我马上就能想到两种需要通过引用来传递基本类型的情况

l 传递基本类型数据到只接受对象参数的方法它展示了完成这两个任务的错误做法这个类试图实现上面列出的两个目标第一个问题就是程序根本无法编译通过该方法只接受对象类型的参数该方法的意图是希望通过赋值给方法参数u 33 第1部分 语法

方法调用者Java中基本数据类型的传递数据类型被传递给方法时getPersonInfo()方法为这些方法参数分配新的值时而不能改变原来的变量这也就意味着当基本当让我们开始关注这些问题的正确解决方法吧它解决了PassPrimitiveByReference1类的问题34 t Item 9: 通过引用传递基本类型

语言中的封装类解决了()方法的问题你会发现针对所有基本数据类型的相关封装类针对float的Float类这些封装类顾名思义而且它们都提供有各种各样的有效方法封装类也可以是上例中第二个问题的解决方法这些封装类是“不可变的”ÎÒÃÇ´´½¨ÁËÒ»¸ö¶ÔÓ¦Ö¸¶¨²ÎÊýÀàÐ͵ÄһάÊý×黹¼ÇµÃ֮ǰÎÒÌáµ½¹ýÊý×éÊÇÒýÓÃÀàÐÍÂðÎÒ¿ÉÒÔÔÚgetPersonInfo()方法中设置数组中的这些基本类型的值第二个问题也就自然地迎刃而解了u 35 第1部分 语法

35:

36: private static void storePersonInfo (String name, Integer age,

37: Float weight, Boolean isMarried)

38: {

39: Hashtable h = new Hashtable();

40: ("name", name);

41: ("age", age);

42: ("weight", weight);

43: ("isMarried", isMarried);

44: }

45: }

运行PassPrimitiveByReference2会生成下列输出结果但是这决不意味着Java语言鼓励这种用法然而你也可能会发现唯一可选的方法只有通过数组引用来传递基本数据类型短路和C持布尔运算符致不少问题Java也支持位运算符以及条件运算符Java同时支这些运算符将会导那么大多数编译器都会向你发出警告Gnu C编译器会生成下列警告信息但是它的实际执行过程也许并不是你所期望的那么代码就会尝试去对一个被废弃的指针进行比较操作36 t Item 10: 布尔运算符与运算符

译器能够自动假定你也许并不想在那里做一个位运算“与”ÔÚJava中如果表达式两边均为boolean型的值而不是一个位运算符检测一下它是否含有任何元素

if ((v != null) & (() > 0)) // wrong operator!

编译器不会报错你所期望的能非常类似¼ÙÈçÔËËã·û×ó±ß±í´ïʽµÄ½á¹ûʽ½«»á±»ºöÂÔ

short-circuit条件运算符

如果v真的为null布尔运算符和条件运算符提供的功会引起“短路”ÄÇôÔËËã·ûÓұ߱í´ï

if ((v != null) && (() > 0))

条件运算符||的原理也一样也将被忽略要处理的操作数更少逻辑运算符&或|那么运算符右边表达式它们更加安全因为需最好能够注释上你使用的是u 37

2023年6月20日发(作者:)

第1部分  语法 程序员们总是被层出不穷的复杂问题所困扰—— 设计和编写程序的语言本身就是复杂的而非它们的解决方案了假如我们最基本的开放工具Smalltalkµ±Ê¹ÓÃÆäËûÓïÑԵijÌÐòԱתÓÃJava来编程时因此这些程序员通常会认为这些特性在Java中和在以前所使用的语言中表现一致这些想法在C++程序员中尤其普遍绊倒

本部分包括以下10个单元什么时候

Item 2较字符串的不同之处==”本单元解释了这两种方法比

 方法并非真的被覆盖了并借鉴了Objective Cu 1 第1部分 语法

Item 3

Item 4我们培训新的Java学员时它本单元解释了基本类型的转换和提升的规则本单元给出了一个经典的当编译器怎么会没发现后

Item 6不能访问被覆盖的方法

Òþ²Ø±äÁ¿³ÉÔ±本单元讨论了这一最常见的陷阱并且和this引用一起讨论提前引用读完以提前引用它设计可继承的构造函数发可重用Java类的程序员来说

Item 9

Item 10以及如何去避免对于每一个想开

本单元对从C++转换到Java的程序员特别有价值

¶Ì·本单元解释了Java编程中另一个常见的陷阱单元中也举了一个使用短路运算符的清晰例子被覆盖的虽然它的本意并非欺骗你想必你已经阅读了一两本这样的Java书籍封装理解这3个概念对于领会Java语言来说至关重要

覆盖实例方法会在Item 5谈到如果你还不明白两者的区别假如你已经急不可待地喊出那么不过

2 t

我承认Item 1: 什么时候方法并非真的被覆盖了

这个例子摘自Java语言规范8.4.8.5节

Goodnight, Dick

要是你得出了同样的输出结果果和答案不一致Sub类继承了Super类个main方法我们先分析一下各个类如果你的结Test类只有一u 3 第1部分 语法

在Test类的第5行中在这里虽然变量s的数据类型为Super类如果你对此有些迷惑变量s是一个被强制转换为Super型的Sub类的实例加上一个字符串紧随其后的是()的返回值还是Sub类的方法两个类中的name()方法都不是静态方法因为Sub类继承了Super类所以Sub类中的name()方法覆盖了Super类中的name()方法这样一来Dick

至此现在我们需要判断被调用的greeting()方法究竟是Super类的还是Sub类的两个类中的greeting()方法都是静态方法尽管事实上Sub类的greeting()方法具有相同的返回类型以及相同的方法参数由于变量s被强制转换为Super型因此ng()的返回值为请记住这条规则静态方法被隐藏”²»Äܸ²¸Ç¾²Ì¬·½·¨µÄ¶ÁÕßÖ®Ò»

现在你可能会问”È»¶øÊµ¼ÊÉÏÎÒÃǸոÕÔÚÕâ¸öSuper/Sub类的例子中已经解释了两者的不同即使变量s是Sub类的一个实例我们仍旧能够将s强制转换为Super型与被隐藏的方法不同除了覆盖它们的类之外这就是为何变量s调用的是Sub类的

本单元简要解释了Java语言中一个不时引起混淆的问题理解隐藏静态方法和覆盖实例方法的区别的最佳方式再重复一次规则被覆盖的方法只有覆盖它们的类才能访问它们

现在你终于明白标题里问题的答案了吧答案就是另外请谨记4 t Item 2: ( )方法与运算符的用法比较

l

l

l

l

试图用子类的静态方法隐藏父类中同样标识的实例方法是不合法的

试图用子类的实例方法覆盖父类中同样标识的静态方法也是不合法的

静态方法和最终方法不能被覆盖

抽象方法必须在具体类1中被覆盖=== =ÕâÖÖÀ§»óÖ÷ÒªÊÇ(...)方法和= =运算符的混淆然而实际上看看下面的例子其中两个被赋值以常量表达式“Programming”ʹÓÃequals(...)方法和“= =”运算符进行比较

(s1): true

(s2): true

s0 == s1: false

s0 == s2: true

1 具体类也就是抽象方法所属抽象类的非抽象子类

u 5 第1部分 语法

()方法比较的是字符串的内容会对字符串中的所有字符如果完全相等在这种情况下个字符串都是相同的我们得到的返回值均为trueÔÚÕâÖÖÇé¿öÏÂs0和s1并不是同一个String实例读者也许会问这个问题的答案来自于Java语言规范中关于字符串常量的章节“Programming”它们在编译期就被确定了例如s2Java确保一个字符串常量只有一份拷贝Java会设置两个变量的引用为同一个常量的引用constant poolJava会跟踪所有的字符串常量并被保存在已编译的.class文件中的一些数据类当然还有字符串常量的信息变量s0和s2被确定constant pool resolution¸ÃÏî²Ù×÷Õë¶Ô×Ö·û´®µÄ´¦Àí¹ý³ÌÕª×ÔJVM规范5.4节

n 如果另一个常量池入口被标记为CONSTANT_String2ͬÑùµÄUnicode字符序列已经被确定

n 否则那么这项操作的结果就是那个相同String实例的引用一个新的String实例会被创建这个String实例就是该项操作的结果当常量池第一次确定一个字符串在常量池中都会得到之前创建的String实例它创建了字符串常量的一份拷贝到另一个String实例中对s0和s1的引用的比较结果是falseÕâ¾ÍÊÇΪºÎs0==s1的操作在某些情况下与(s1)不同而(s1)实际上执行的是字符串内容的比较字符串常量由0个或多个包含在双引号之内的字符组成

2 它在.class内部使用

escape sequence6 t Item 2: ( )方法与运算符的用法比较

存在于.class文件中的常量池并且可以扩充当针对一个String实例调用了intern()方法因为实例已经存在所以已存在的实例的引用被加入到该常量池

01: import .*;

02:

03: public class StringExample2

04: {

05: public static void main (String args[])

06: {

07: String sFileName = "";

08: String s0 = readStringFromFile(sFileName);

09: String s1 = readStringFromFile(sFileName);

10:

11: n("s0 == s1: " + (s0 == s1));

12: n("(s1): " + ((s1)));

13:

14: ();

15: ();

16:

17: n("s0 == s1: " + (s0 == s1));

18: n("s0 == (): " +

19: (s0 == ()));

20: }

21:

22: private static String readStringFromFile (String sFileName)

23: {

24: //…read string from file…

25: }

26: }

这个例子没有设置s0和s1的值为字符串常量并把值分配给readStringFromFile(...)方法创建的String实例序对两个被新创建为具有同样字符值的String实例进行处理你会再次注意到但它们的内容是相同的

程u 7 第1部分 语法

s0 == s1: false

(s1): true

s0 == s1: false

s0 == (): true

第14行所做的是将String实例的引用s0存入常量池对()方法的调用这样一来正是我们所期望的s0与s1仍旧是截然不同的两个String实例而()返回的是常量池中的引用值假如我们希望将实例s1存入常量池中然后请求垃圾回收器回收被指向s0的String实例()方法的调用

总的来说equality comparisonÓ¦¸ÃʼÖÕʹÓÃ(...)方法如果你还是习惯性地使用==运算符因为当n和m均为String实例的引用时假如你打算充分利用常量池的优势

Item 3: Java是强类型语言 每个Java开发人员都需要很好地理解Java所支持的基本数据类型它们与你以前所使用的语言有什么不同呢Java是强类型的这些基本数据类型算得上是构造对象的了Java编译器能够及时地在开发过程中捕捉到许多简单细微的错误不过在Java中仍然有一些微妙之处Java的基本数据类型始终被描述在JVM中这就使得位运算得以较安全地执行boolean型是不可转换的Java不允许你编写在boolean型和非boolean型之间转换的代码那么你可能编写过一些诸如0等于false或非0等于true的“优雅”的代码8 t Item 3: Java是强类型语言

在C语言中编写代码检查一个函数的返回值类似的代码在Java中是不能编译的必须给它一个这样的值所以你由于基本数据类型的转换可以隐性地发生以及如何工作而且一般来说loss of precision±àÒëÆ÷»áÏòÄã·¢³ö¾¯¸æarithmetic operationÊý¿ª·¢ÈËÔ±¶¼Ôø¾-д¹ýµ¼ÖÂÊý¾ÝÒâÍâÇжÏ的代码Truncation类的第10行将会输出“2.0”

 ´ó¶à下面的01: public class Truncation

02: {

03: static void printFloat (float f)

04: {

05: n ("f = " + f);

06: }

07:

08: public static void main (String[] args)

09: {

10: printFloat (12 / 5); // data lost!

11: printFloat ((float) 12 / 5);

12: printFloat (12 / 5.0f);

13: }

14: }

 因为12和5都是integer型分丢失了当printFloat方法并没有得到期望的float型参数时修复很简单那么另一个也会被提升float型第11行和12行操作正常因此小数部为转换操作会自动发生u 9 第1部分 语法

况下的转换被称作是因为它们都会被转换成能够存储较大数据的类型可以为int变量分配一个byte值图1.1展示了扩展转换当一个类型被转换为具有更多比特数的类型时

图1.1 扩展转换

注意型度损失或者把一个long型转换成为double换句话说就是一些最不重要的位可能会丢失下面这个例子的输出结果发生的精对扩展转换有更详细的描述但是基本类型之间的扩展转换永远不会导致运行期异常narrowing例如¼´Èκβ»Í¬ÓÚͼ1.1中从左到右顺序的转换包括float型和double型包括shortoverflow¶¼»áµÃµ½±àÒëÆÚ´íÎócast可以避免这种错误“我清楚我在干什么 10 t Item 3: Java是强类型语言

隐性类型转换

顾名思义时而是自动发生的为了方便short或char型且表达式的值一定为int型当然不能超过变量类型的数值范围程序TypeConversion的第7行中的赋值可以编译通过范围-128到127但是第8行无法编译通过

01: public class TypeConversion

02: {

03: static void convertArg (byte b) { }

04:

05: public static void main (String[] args)

06: {

07: byte b1 = 127;

08: // byte b2 = 128; // won't compile

09:

10: // convertArg (127); // won't compile

11: convertArg ((byte)127);

12:

13: byte c1 = b1;

14: // byte c2 = -b1; // won't compile

15: int i = ++b1; // overflow

16: n ("i = " + i);

17: }

18: }

隐性类型转换能够在3种情况下发生方法调用和算术运算假如表达式两边的类型不同

类似地参数也有可能需要被转换类型()方法期望的参数是double型由于这是一个扩展转换不过请注意隐性的窄化转换不支持方法调用上面例子的第10行不能被编译即对参数进行显性的强制转换

第3种情况被称之为算术运算假如你希望将一个int型和一个float型相加求和其中较窄的类型总是被转换成较宽的类型u 11 第1部分 语法

同理所有的byte但非全部诸如第14行的一元减法运算符×ÜÊÇÖÁÉÙ±»ÌáÉýΪint型将会得到下面的错误信息然后警告你int型的值无法赋值给byte变量可能预料到第15行中byte型的值在增量操作时16行代码将会输出“i = 128”吧由于溢出的发生= -128”你那么第“i

扰几个小时ÓÉÓÚÒ»¸ö¼òµ¥µÄ´íÎóÒýÆðµÄBugÕâÑùµÄBug的特征就在于或许你会因此困看看你要花费多长时间解决它12 t Item 4: 那是构造函数吗

这个IntAdder类相当简单3个private属性分别为x一个构造函数以及main方法在第21行位于第7行的构造函数设置了属性x²¢½«ËüÃǵĺ͸³¸øzÎÒÃǵ÷ÓÃprintResults方法执行IntAdder类后的输出结果应该是如果你认为没有问题该是实际的输出应你是否找出了问题所在呢好吧第21行第7行的IntAdder类构造函数设置了属性x和y的值难道这个构造函数没有工作吗就会明白它实际上是一个方法刚才你一定错误地将带有“void”返回类型的IntAdder方法当成了IntAdder类的构造函数了你也许会问那么当初始化实例ia时调用的构造函数是什么因为代码中没有构造函数这个默认的构造函数不用实现但其功能就和下面的这个构造函数一致在第21行被创建的对象ia的属性x方法被调用时

The value of 'z' is '0'.

当第22行的printResults这个“简单”的小Bug还揭示了几个关键的问题我们注意到但编译并运行它的时候也就是说但是不推荐使用这样的命名这点很容易理解只有构造函数的名字才能和类名相同假如有和类同名的方法从另一个角度来说同样违背了命名规范并且首字母应该小写类名一般应该为名词或者名词短语关于命名规范在u 13 第1部分 语法

这个例子中数的构造函数在一个类中假如没有显式的构造函数存在而且这个构造函数是空的假如类中已经存在了一个带任意参

Item 5: 不能访问被覆盖的方法 设想一下而这个编辑器支持RTF文件在这个应用程序自己的代码中或者通过访问编辑器的DocumentManager对象来打开新的文档一个Document对象被返回文法检查等等你接到了客户的电话你觉得实现这个需求没什么问题而且因为所有新加入的特性都被放置在Document类的一个名为HTMLDocument的子类中无需修改而且当需要的Document对象被DocumentManager类返回后以便使用HTMLDocument类的新特性你拿着新版本的编辑器以及一些利用新的HTML特性的代码这个新功能将在一个月内实现当你还自我感觉良好时上面指出了关于拼写检查程序的一个问题但是你自认为是一个聪明的Java程序员让它使用Doucment类的spellCheck()方法厂商已经声明了Document类的代码未被改动你就尝试着总去调用Document类的spellCheck()方法还是声明一个局部Document类型的变量并赋值为传入的Document对象所有的结果都将是失败的厂商的技术支持也明确地告诉你Document类没有被修改过并最终在8.4.6.1章节中看到了下面这段话14 t Item 5: 不能访问被覆盖的方法

“可以通过包含关键字super的方法调用表达式来访问被覆盖的方法尝试用全局名或强制转换为父类型都是无效的一切都清楚了这第三方文本编辑器的类库总是创建一个HTMLDocument型的对象强制转换而不是所期望的Document类的spellCheck()方法当你创建了一个覆盖了父类实例方法的子类时就是使用super关键字永远不能调用父类的这些被覆盖的实例方法

01: class DocumentManager

02: {

03: public Document newDocument()

04: {

05: return (new HTMLDocument());

06: }

07: }

08:

09: class Document

10: {

11: public boolean spellCheck()

12: {

13: return (true);

14: }

15: }

16:

17: class HTMLDocument extends Document

18: {

19: public boolean spellCheck()

20: {

21: n("Trouble checking these darn hyperlinks!");

22: return (false);

23: }

24: }

25:

26: public class OverridingInstanceApp

27: {

28: public static void main (String args[])

29: {

30: DocumentManager dm = new DocumentManager();

u 15 第1部分 语法

31: Document d = ument();

32: boolean spellCheckSuccessful = heck();

33: if (spellCheckSuccessful)

34: n("No spelling errors where found.");

35: else

36: n("Document has spelling errors.");

37: }

38: }

在第32行中所以这个例子的输出结果是这样的然而无法访问父类中被子类覆盖的方法的原则是非静态方法并非真的覆盖参见Item1ËüÃÇÈÔ¾ÉÄܱ»·ÃÎʽ«µÚ11行和第19行的代码替换为下面这行

public static boolean spellCheck(Document d)

也就然后再将第32行代码替换为下面这行

boolean spellCheckSuccessful = heck(d);

最后执行修改后的程序

No spelling errors where found.

你也许会质疑这里所举的例子是否真实地发生过这个例子是虚构的通常来说也就是父类但是实际上它们得到的是新类的实例JavaSoft有一个不错的例子在旧版本的JDK中例如paint(...)和update(...)它们在新版的JDK中传递的都是Graphics2D对象Graphics2D类覆盖了Graphics类中的一些实例方法draw3DRect()假设在Graphics2D类中的draw3DRect()方法有一个BugÄÇôÕýÈçÄãµÄÅжÏ

16 t Item 6: 避免落入的陷阱

尽管在子类的外部无法访问父类中被覆盖的实例方法假如你怀疑你所使用的某个对象实际上是一个子类的实例又如果你是编写增加了新功能的子类的程序员或者保证在编写程序时而不是覆盖父类方法实现的隐藏变量成员与理解方法是如何被覆盖同等重要的就是假如你认为自己已经理解了方法是如何被覆盖的那么你最好仔细地读读本节无意地隐藏了一个变量成员或者错误地认为已经“覆盖”了一个变量成员

01: public class Wealthy

02: {

03: public String answer = "Yes!";

04: public void wantMoney()

05: {

06: n("Would you like $1,000,000? > "+ answer);

07: }

08: public static void main(String[] args)

09: {

10: Wealthy w = new Wealthy();

11: ney();

12: }

13: }

输出结果为Wealthy类具有一个名为answer的实例变量以及一个main方法一个Wealthy类的实例w被创建输出了一个问题以及作为回答的实例变量answer的值现在让我们来看看一个没有正确回答这个问题的例子u 17 第1部分 语法

01: public class Poor

02: {

03: public String answer = "Yes!";

04: public void wantMoney()

05: {

06: String answer = "No!"; // hides instance variable answer

07: n("Would you like $1,000,000? > " + answer);

08: }

09: public static void main(String[] args)

10: {

11: Poor p = new Poor();

12: ney();

13: }

14: }

输出结果为本例输出中的回答已经变成了“No»Ø´ðµÄ½á¹û¾ÍÊǾֲ¿±äÁ¿µÄÖµ产生了意想不到的结果

l

l

l

不同类型的Java变量

何种变量能被隐藏

如何访问被隐藏的变量

不同类型的Java变量

局部变量answer隐藏了实例变量answerÒ²ÏÔ¶øÒ×¼û在许多复杂的环境下为了避免“数据隐藏”所带来的问题Java一共有6种变量类型实例变量以及局部变量实例变量是在类体中声明的非静态变量方法参数是用来传入一个方法体的18 t

构造函数参数Item 6: 避免落入的陷阱

下面的例子声明了各种类型的变量最后简名是一个变量的专一标识符Types类的类体构造函数体声明的所在代码块

在这个代码块中实例变量x的简名就是“x”±äÁ¿³ÉÔ±x和y的作用范围就是构造函数参数的作用范围就是整个局部变量的作用范围就是它被它在第11行也就是createURL方法中被声明

u 19 第1部分 语法

何种变量能被隐藏

实例变量和类变量能被隐藏局部变量去隐藏一个参数编译器也会报错假如用一个同名的同样地第5行的局部变量不能和方法参数args同名引起编译器错误实例变量和类变量如何被隐藏

第7行的局部变量s也会

同名的局部变量或者同名的参数变量成员也能被子类的同名变量成员隐藏将在其作用范围内与一个变量成员同名的方法参数与一个变量成员同名的构造函数参数依此类推将在catch语句块中隐藏掉这个变量成员20 t Item 6: 避免落入的陷阱

上例中System.out.println方法输出的type变量的值将是构造函数参数type的值

子类的变量成员将会隐藏掉父类中同名的变量成员Bike类中的实例变量typeµÄÀà±äÁ¿½«»áÒþ²Ø¸¸ÀàÖÐÓë֮ͬÃûµÄÀà±äÁ¿ºÍʵÀý±äÁ¿ÀàÖÐÓë֮ͬÃûµÄÀà±äÁ¿ºÍʵÀý±äÁ¿×ÓÀà×ÓÀàµÄʵÀý±äÁ¿Ò²»áÒþ²Ø¸¸

04: }

01: public class Line

02: {

03: int x;

04: }

01: public class MultiLine extends Line implements Stretchable02: {

03: public MultiLine()

04: {

05: n("x = " + x);

06: }

07: }

上例的程序可以编译通过MultiLine类将无法通过编译

那么 u 21 第1部分 语法

如何访问被隐藏的变量

通过全局名的实例变量关键字“this”可以限定一个正被局部变量隐藏类变量也可以被限定

01: public class Wealthy

02: {

03: public String answer = "Yes!";

04: public void wantMoney()

05: {

06: String answer = "No!";

07: n("Do you want to give me $1,000,000? > " +

08: answer);

09: n("Would you like $1,000,000? > " +

10: );

11: }

12: public static void main(String[] args)

13: {

14: Wealthy w = new Wealthy();

15: ney();

16: }

17: }

输出结果为Wealthy类具有一个名为answer的实例变量为了对wantMoney方法中的每个问题都给出正确的回答也需要访问实例变量answer我们告知编译器而非局部变量answerµÚÒ»¸öÎÊÌâµÄ»Ø´ðÊǾֲ¿±äÁ¿answer的值它被关键字“this”限定了

01: public class StillWealthy extends Wealthy

02: {

03: public String answer = "No!";

22 t Item 6: 避免落入的陷阱

04: public void wantMoney()

05: {

06: String answer = "maybe?";

07: n("Did you see that henway? > " + answer);

08: n("Do you want to give me $1,000,000? > " +

09: );

10: n("Would you like $1,000,000? > " + );

11: }

12: public static void main(String[] args)

13: {

14: Wealthy w = new Wealthy();

15: ney();

16: }

17: }

输出结果为上例中第7行问题的回答的是父类Wealthy的实例变量变量隐藏与方法覆盖的区别

隐藏变量和覆盖方法有许多区别或者强制转换自己为其父类的类型它被关键字“this”限定了

第8行问题的回答输出一个类的实例

01: public class Wealthier extends Wealthy

02: {

03: public void wantMoney()

04: {

05: n("Would you like $2,000,000? > " + answer);

06: }

07: public static void main(String[] args)

08: {

09: Wealthier w = new Wealthier();

10: ney();

11: ((Wealthy)w).wantMoney();

12: }

13: }

u 23 第1部分 语法

输出结果为Wealthier类继承了Wealthy类main方法创建了Wealthier类的一个实例w×¢Òâ½Ó×Å并再次调用它的wantMoney()方法上面的例子说明是无法访问父类中被覆盖的方法的一个被隐藏的变量与一个被覆盖的方法的区别强制转换子类的实例为父类类型后

01: public class Poorer extends Wealthier

02: {

03: String answer = "No!";

04: public void wantMoney()

05: {

06: n("Would you like $3,000,000? > " + answer);

07: }

08: public static void main(String[] args)

09: {

10: Poorer p = new Poorer();

11: ((Wealthier)p).wantMoney();

12: n("Are you sure? > " + ((Wealthier)p).answer);

13: }

14: }

输出结果为main方法创建了一个Poorer类的实例pÕýÈçÔÚǰһ¸öÀý×ÓÖнâÊ͵Äͨ¹ýÇ¿ÖÆ×ª»»Òò´ËËüµÄ»Ø´ðÊÇ“No!”main方法又问到这个问题答案就不再是子类变量的值这种情况的出现24 t Item 7: 提前引用

仅仅“隐藏”了父类的变量成员只要将子类实例强制转换为父类类型数据隐藏与方法覆盖的另外一个不同而静态变量相同地而变量成员却可以隐藏父类同名变量成员

通过理解本节讨论的知识点会帮助应用程序得到你所期望的结果

Item 7: 提前引用 类变量以及静态初始化块是在类被加载进JVM时执行初始化操作的“静态初始化块和类变量是按照其在代码中出现的顺序依次执行初始化操作的换句话说一般来说看看下面的代码将会得到一个如下的错误即使变量first和second都处在同一个作用范围内而且编译器会捕捉到这个错误绕开这个保护措施还是有可能的而且方法内部对类变量的访问不会按照这个原则被检查

01: public class ForwardReferenceViaMethod

02: {

03: static int first = accessTooSoon();

04: static int second = 1;

05:

06: static int accessTooSoon()

07: {

u 25 第1部分 语法

08: return (second);

09: }

10:

11: public static void main (String[] args)

12: {

13: n ("first = " + first);

14: }

15: }

然而由于在初始化second之前法得到的是second的默认值输出结果first的值为0

问题并非这么简单那么你必须保证

那么方Item 8: 设计可继承的构造函数 包括Java在内就是继承它

软件开发过程中举例来说你经常会发现代码是如此复杂且难以维护当程序原本就比较容易扩展时一般来说代价就要小得多了你可能会遭遇到许多阻碍甚至是抵触扩展性的陷阱那么当你设计类以及编写代码时

我所见过的最常见的陷阱之一不论你的方法设计得多么完美其它开发人员在继承你的类时因为构造函数是不能被覆盖的子类都必须承受它那么它的任何子类都一定会做同样的工作在编写子类的开发人员无权访问父类的源代码时举例来说

01: import .*;

02: import .*;

03: import .*;

04: import .*;

代码容易被复用不幸的是26 t Item 8: 设计可继承的构造函数

05:

06: import .*;

07: import .*;

08:

09: public class ListDialog extends JDialog

10: implements ActionListener, ListSelectionListener

11: {

12: JList model;

13: JButton selectButton;

14: LDListener listener;

15: Object[] selections;

16:

17: public ListDialog (String title,

18: String[] items,

19: LDListener listener)

20: {

21: super ((Frame)null, title);

22:

23: JPanel buttonPane = new JPanel ();

24: selectButton = new JButton ("SELECT");

25: ionListener (this);

26: bled (false);//nothing selected yet

27: (selectButton);

28:

29: JButton cancelButton = new JButton ("CANCEL");

30: ionListener (this);

31: (cancelButton);

32:

33: tentPane().add (buttonPane, );

34:

35: er = listener;

36: setModel (items);

37: }

38:

39: void setModel (String[] items)

40: {

41: if ( != null)

42: ListSelectionListener (this);

43: = new JList (items);

44: tSelectionListener (this);

45:

46: JScrollPane scroll = new JScrollPane (model);

47: tentPane().add (scroll, );

48: ();

49: }

50:

51: /** Implement ListSelectionListener. Track user selections. */

52:

53: public void valueChanged (ListSelectionEvent e)

54: {

55: selections = ectedValues();

u 27 第1部分 语法

56: if ( > 0)

57: bled (true);

58: }

59:

60: /** Implement ActionListener. Called when the user picks the

61: * SELECT or CANCEL button. Generates the LDEvent. */

62:

63: public void actionPerformed (ActionEvent e)

64: {

65: ible (false);

66: String buttonLabel = ionCommand();

67: if ( ("CANCEL"))

68: selections = null;

69: if (listener != null)

70: {

71: LDEvent lde = new LDEvent (this, selections);

72: alogSelection (lde);

73: }

74: }

75:

76: public static void main (String[] args) // self-testing code

77: {

78: String[] items = (new String[]

79: {"Forest", "Island", "Mountain", "Plains", "Swamp"});

80: LDListener listener =

81: new LDListener()

82: {

83: public void listDialogSelection (LDEvent e)

84: {

85: Object[] selected = ection();

86: if (selected != null) // null if user cancels

87: for (int i = 0; i < ; i++)

88: n (selected[i] .toString());

89: (0);

90: }

91: };

92:

93: ListDialog dialog =

94: new ListDialog ("ListDialog", items, listener);

95: ();

96: }

97: }

 出于程序完整性的考虑 

01: public interface LDListener

02: {

03: public void listDialogSelection (LDEvent e);

04: }

 01: import bject;

28 t Item 8: 设计可继承的构造函数

02:

03: public class LDEvent extends bject

04: {

05: Object source;

06: Object[] selections;

07:

08: public LDEvent (Object source, Object[] selections)

09: {

10: super (source);

11: ions = selections;

12: }

13:

14: public Object[] getSelection()

15: {

16: return (selections);

17: }

18: }

ListDialog类看上去写得相当不错假设你需要开发一个向用户显示一列声音文件的菜单能够听到相应的声音所以你决定提供一个简单的API²¢²â¶¨¸Ã·¾¶ÏµÄÉùÒôÎļþÁбí¾ÍÄܹ»ÊµÏÖÕâ¸öÄ¿µÄÁË以便播放用户选择的声音你可以调用上例第44行显示的addListSelectionListener(...)方法来解决而且没有任何访问它的方法而且你恐怕要从头开始做起了一切都还不错当你尝试去继承ListDialog类上例第一部分的第17行然而你并没有这个资源由于构造函数第一件要做的事就是调用super(...)语句 问题出现了去翻翻Javadoc文档它似乎可以提供帮助你首先在子类构造函数中试着实例化一个items参数为空的ListDialog类调用setModel(...)方法u 29 第1部分 语法

05: String[] items = getItems (path);

06: setModel (items);

07: tSelectionListener (this);

08: ectionMode (_SELECTION);

09: }

这个解决方法看上去合情合理将会得到下面的错误定方法你找到了错误的关键导致了意想不到的结果你终于判尝试调用一个没有item的JList对象的pack()

那么现在该如何是好呢你还算幸运的因此你可以用自定义的同名方法覆盖掉它

01: import .*;

02: import .*;

03: import .*;

04: import .*;

05:

06: import .*;

07: import .*;

08:

09: public class SoundDialog extends ListDialog

10: implements FilenameFilter, ListSelectionListener

11: {

12: String selection;

13:

14: public SoundDialog (String title, LDListener ldl, String path)

15: {

16: super (title, null, ldl);

17: String[] items = getItems (path);

18: setModel (items);

19: }

30 t Item 8: 设计可继承的构造函数

20:

21: public void setModel (String[] items)

22: {

23: if (items != null)

24: {

25: el (items);

26: tSelectionListener (this);

27: ectionMode

28: (_SELECTION);

29: }

30: }

31:

32: public String[] getItems (String path)

33: {

34: File file = new File (path);

35: File soundFiles[] = les (this);

36: String[] items = new String [];

37: for (int i = 0; i < ; i++)

38: items = e();

39: return (items);

40: }

41:

42: // implement FilenameFilter

43: public boolean accept (File dir, String name)

44: {

45: return (th (".aiff") ||

46: th (".au") ||

47: th (".midi") ||

48: th (".rmf") ||

49: th (".wav"));

50: }

51:

52: // implement ListSelectionListener

53: public void valueChanged (ListSelectionEvent e)

54: {

55: hanged (e);

6: JList items = (JList) rce();

57: String fileName = ectedValue().toString();

58: if (! (selection))

59: {

60: selection = fileName;

61: play (selection);

62: }

63: }

64:

65: private void play (String fileName)

66: {

67: try

68: {

69: File file = new File (fileName);

70: URL url = new URL ("file://" + olutePath());

u 31 第1部分 语法

71: AudioClip audioClip = ioClip (url);

72: if (audioClip != null)

73: ();

74: }

75: catch (MalformedURLException e)

76: {

77: n (e + ": " + sage());

78: }

79: }

80:

81: public static void main (String[] args) // self-test

82: {

83: LDListener listener =

84: new LDListener()

85: {

86: public void listDialogSelection (LDEvent e)

87: {

88: Object[] selected = ection();

89: if (selected != null) // null if user cancels

90: for (int i = 0; i < ; i++)

91: n (selected[i].toString());

92: (0);

93: }

94: };

95: SoundDialog dialog =

96: new SoundDialog ("SoundDialog", listener, ".");

97: ();

98: }

99: }

回味一下是不需要的请确信你这么做是必要的为其它不太复杂的构造函数提供一个便利的实现要考虑增加额外版本的构造函数如果你能够提供一个无参数的构造函数那就再好不过了仍未被实例化的变量当然它开发人员使用它创建一些无效的对象了32 t

那么上述这些工作都调用了一个private方法因为它也许会那么你就

例如你必须仔细地检查其它方法没有使用那么你就应该考虑限制访问它了但是你可以不用担心其Item 9: 通过引用传递基本类型

需要注意的是即使一个空的JList对象实际上是无效的那么在使用JList对象之前

显而易见Item 9: 通过引用传递基本类型 假如你曾经是一名C或C++程序员因为它没有指针的概念Java语言没有引入指针指针运算和从一个函数中返回多个值在Java中至于第二件事在通过引用而不是值来传递参数的情况下Java程序员都是利用引用来访问所有被实例化的对象的接口都归为引用类型基本数据类型被用来存储特定类别的信息Java语言一共提供了这些基本数据类型byte型int型char型一个需要理解的重要概念是因此

看到这假如基本数据类型不能通过引用来传递的话而且

虽然基本数据类型不能直接通过引用来传递这也就意味着必须使用引用类型来封装基本数据类型我马上就能想到两种需要通过引用来传递基本类型的情况

l 传递基本类型数据到只接受对象参数的方法它展示了完成这两个任务的错误做法这个类试图实现上面列出的两个目标第一个问题就是程序根本无法编译通过该方法只接受对象类型的参数该方法的意图是希望通过赋值给方法参数u 33 第1部分 语法

方法调用者Java中基本数据类型的传递数据类型被传递给方法时getPersonInfo()方法为这些方法参数分配新的值时而不能改变原来的变量这也就意味着当基本当让我们开始关注这些问题的正确解决方法吧它解决了PassPrimitiveByReference1类的问题34 t Item 9: 通过引用传递基本类型

语言中的封装类解决了()方法的问题你会发现针对所有基本数据类型的相关封装类针对float的Float类这些封装类顾名思义而且它们都提供有各种各样的有效方法封装类也可以是上例中第二个问题的解决方法这些封装类是“不可变的”ÎÒÃÇ´´½¨ÁËÒ»¸ö¶ÔÓ¦Ö¸¶¨²ÎÊýÀàÐ͵ÄһάÊý×黹¼ÇµÃ֮ǰÎÒÌáµ½¹ýÊý×éÊÇÒýÓÃÀàÐÍÂðÎÒ¿ÉÒÔÔÚgetPersonInfo()方法中设置数组中的这些基本类型的值第二个问题也就自然地迎刃而解了u 35 第1部分 语法

35:

36: private static void storePersonInfo (String name, Integer age,

37: Float weight, Boolean isMarried)

38: {

39: Hashtable h = new Hashtable();

40: ("name", name);

41: ("age", age);

42: ("weight", weight);

43: ("isMarried", isMarried);

44: }

45: }

运行PassPrimitiveByReference2会生成下列输出结果但是这决不意味着Java语言鼓励这种用法然而你也可能会发现唯一可选的方法只有通过数组引用来传递基本数据类型短路和C持布尔运算符致不少问题Java也支持位运算符以及条件运算符Java同时支这些运算符将会导那么大多数编译器都会向你发出警告Gnu C编译器会生成下列警告信息但是它的实际执行过程也许并不是你所期望的那么代码就会尝试去对一个被废弃的指针进行比较操作36 t Item 10: 布尔运算符与运算符

译器能够自动假定你也许并不想在那里做一个位运算“与”ÔÚJava中如果表达式两边均为boolean型的值而不是一个位运算符检测一下它是否含有任何元素

if ((v != null) & (() > 0)) // wrong operator!

编译器不会报错你所期望的能非常类似¼ÙÈçÔËËã·û×ó±ß±í´ïʽµÄ½á¹ûʽ½«»á±»ºöÂÔ

short-circuit条件运算符

如果v真的为null布尔运算符和条件运算符提供的功会引起“短路”ÄÇôÔËËã·ûÓұ߱í´ï

if ((v != null) && (() > 0))

条件运算符||的原理也一样也将被忽略要处理的操作数更少逻辑运算符&或|那么运算符右边表达式它们更加安全因为需最好能够注释上你使用的是u 37