java编程思想_初始化与清理_0505
5.8.1可变参数列表
第二种形式提供了一种方便的语法来创建对象并调用方法,以获得与C的可变参数列表(C通常把它简称为varargs)一样的效果。这可以应用于参数个数或类型未知的场合。由于所有的类型都直接或间接继承于Object类(随着本书的进展,读者会对此有更深入的认识),所以可以创建以Object数组为参数的方法,并像下面这样调用。
1 //:initialization/VarArgs.java 2 //Using array syntax to create variable argument lists. 3 package initialization; 4 5 class A{} 6 7 public class VarArgs { 8 static void printArray(Object[] args) { 9 for(Object obj:args) 10 System.out.print(obj+" "); 11 System.out.println(); 12 13 } 14 15 public static void main(String[] args) { 16 printArray(new Object[] { 17 new Integer(47),new Float(3.14),new Double(11.11) 18 }); 19 printArray(new Object[] {"one","two","three"}); 20 printArray(new Object[] {new A(),new A(),new A()}); 21 22 } 23 24 }
Output:
可以看到printArray()方法使用Object数组作为参数,然后使用foreach语法遍历数组,打印每个对象。标准Java库中的类能输出有意义的内容,但这里建立的类的对象,打印出的内容只是类的名称以及后面紧跟着的一个@符号以及多个十六进制数字。于是,默认行为(如果没有定义toString()方法的话,后面会讲这个方法的)就是打印类的名字和对象的地址。
你可能看到过像上面这样编写的Java SE5之前的代码,它们可以产生可变的参数列表。然而,在JavaSE5中,这种盼望已久的特性终于添加了进来,因此你现在可以使用它们来定义可变参数列表了,就像在printArray()中看到的那样:
1 //:initialization/NewVarArgs.java 2 //using array syntax to create varialbe argument lists. 3 package initialization; 4 5 public class NewVarArgs { 6 static void printArray(Object...args) { 7 for(Object obj:args) 8 System.out.print(obj+" "); 9 System.out.println(); 10 } 11 12 public static void main(String[] args) { 13 //Can take individual elements: 14 printArray(new Integer(47),new Float(3.14),new Double(11.11)); 15 printArray(47,3.14F,11,11); 16 printArray("one","two","three"); 17 printArray(new A(),new A(),new A()); 18 //Or an array: 19 printArray((Object[])new Integer[] {1,2,3,4}); 20 printArray();//Empty list is OK 21 22 23 } 24 25 }
Output:
有了可变参数,就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你去填充数组。你获取的仍旧是一个数组,这就是为什么print()可以使用foreach来迭代该数组的原因。但是,这不仅仅只是从元素列表到数组的自动转换,请注意程序中倒数第二行,一个Integer数组(通过使用自动包装而创建的)被转型为一个Object数组(以便移除编译器警告信息),并且传递给了printArray()。很明显,编译器会发现它已经是一个数组了,所以不会在其上执行任何转换。因此,如果你有一组事物,可以把它们当作列表传递,而如果你已经有了一个数组,该方法可以把它们当作可变参数列表来接受。
该程序的最后一行表明将0个参数传递给可变参数列表是可行的,当具有可选的尾随参数时,这一特性就会很有用:
1 //:initialization/OptionalTrailingArguments.java 2 package initialization; 3 4 public class OptionalTrailingArguments { 5 static void f(int required,String...trailing) { 6 System.out.print("required:"+required+" "); 7 for(String s:trailing) 8 System.out.print(s+""); 9 System.out.println(); 10 } 11 12 public static void main(String[] args) { 13 f(1,"one"); 14 f(2,"two","three"); 15 f(0); 16 17 } 18 19 }
Output:
这个程序还展示了你可以如何使用具有Object之外类型的可变参数列表。这里所有的可变参数都必须是String对象。在可变参数列表中可以使用任何类型的参数,包括基本类型。
下面的例子展示了可变参数列表变为数组的情形,并且如果在该列表中没有任何元素,那么转变成的数据的尺寸为0:
1 //:initialization/VarargType.java 2 package initialization; 3 4 public class VarargType { 5 static void f(Character...args) { 6 System.out.print(args.getClass()); 7 System.out.println("length"+args.length); 8 } 9 static void g(int...args) { 10 System.out.print(args.getClass()); 11 System.out.println("length"+args.length); 12 } 13 14 public static void main(String[] args) { 15 f('a'); 16 f(); 17 g(1); 18 g(); 19 System.out.println("int[]:"+new int[0].getClass()); 20 21 } 22 23 }
Output:
getClass()方法属Object的一部分,我们将在第14章中做全面介绍。它将产生对象的类,并且在打印该类时,可以看到表示该类类型的编码字符串。前导的“["表示这是一个后面紧随的类型的数据,而紧随的“I"表示基本类型int。为了进行双重检查,我在最后一行创建了一个int数组,并打印了其类型。这样也就验证了使用可变参数列表不依赖于自动包装机制,而实际上使用的是基本类型。
然而,可变参数列表与自动包装机制可以和谐共处,例如:
1 //:initialization/AutoboxingVarargs.java 2 package initialization; 3 4 public class AutoboxingVarargs { 5 public static void f(Integer...args) { 6 for(Integer i:args) 7 System.out.print(i+" "); 8 System.out.println(); 9 } 10 11 public static void main(String[] args) { 12 f(new Integer(1),new Integer(2)); 13 f(4,5,6,7,8,9); 14 f(10,new Integer(11),12); 15 16 } 17 18 }
Output:
你可以在单一的参数列表中将类型混合在一起,而自动包装机制将有选择地将int参数提升为Integer。
可变参数列表使得重载过程变得复杂了,尽管乍一看会显得足够安全:
1 //:initialization/OverloadingVarargs.java 2 package initialization; 3 4 public class OverloadingVarargs { 5 static void f(Character...args) { 6 System.out.print("first"); 7 for(Character c:args) 8 System.out.print(" "+c); 9 System.out.println(); 10 } 11 static void f(Integer...args) { 12 System.out.print("second"); 13 for(Integer i:args) 14 System.out.print(" "+i); 15 System.out.println(); 16 } 17 static void f(Long...args) { 18 System.out.println("third"); 19 } 20 21 public static void main(String[] args) { 22 f('a','b','c'); 23 f(1); 24 f(2,1); 25 f(0); 26 f(0L); 27 //!f();//Won't compile--ambiguous 28 29 } 30 31 }
Output:
在每一种情况中,编译器都会使用自动包装机制来匹配重载的方法,然后调用最明确匹配的方法。
但是在不使用参数调用f()时,编译器就无法知道应该调用哪一个方法了。尽管这个错误可以弄清楚,但是它可能会使客户端程序员大感意外。
你可能会通过在某个方法中增加一个非可变参数来解决该问题:
1 //:initialization/OverloadingVarargs2.java 2 //{CompileTimeError} (Won't compile) 3 package initialization; 4 5 public class OverloadingVarargs2 { 6 static void f(float i,Character...args) { 7 System.out.println("first"); 8 } 9 static void f(Character...args) { 10 System.out.print("second"); 11 } 12 13 public static void main(String[] args) { 14 f(1,'a'); 15 f('a','b'); 16 17 } 18 19 }
Output:
{CompileTimeError}注释标签把该文件排除在了本书的Ant构建之外。如果你手动编译它,就会得到下面的错误消息:
reference to f is ambiguous,both method f(float,java.lang.Character...)
in OverloadingVarargs2 and method f(java.lang.Character...)in
OverloadingVarargs2 match
如果你给这两个方法都添加一个非可变参数,就可以解决问题了:
1 //:initialization/OverloadingVarargs3.java 2 package initialization; 3 4 public class OverloadingVarargs3 { 5 static void f(float i,Character...args) { 6 System.out.println("first"); 7 } 8 static void f(char c,Character...args) { 9 System.out.println("second"); 10 } 11 12 public static void main(String[] args) { 13 f(1,'a'); 14 f('a','b'); 15 16 } 17 18 }
Output:
你应该总是只在重载方法的一个版本上使用可变参数列表,或者压根就不是用它。
练习19:
(2)写一个类,它接受一个可变参数的String数组。验证你可以向该方法传递一个用逗号分隔的String列表,或是一个String[]。
1 package initialization; 2 3 public class InTest19 { 4 static void f(String...args) { 5 for(String i:args) 6 System.out.print(i+" "); 7 System.out.println(); 8 } 9 10 public static void main(String[] args) { 11 String[] s=new String[] {"ddd","eee","fff"}; 12 f("aaa","bbb","ccc"); 13 f(s); 14 15 16 } 17 18 }
Output:
练习20:
(1)创建一个使用可变参数列表而不是普通的main()语法的main()。打印所产生的args数组的所有元素,并用各种不同数量的命令行参数来测试它。
1 package initialization; 2 3 public class InTest20 { 4 5 public static void main(String...args) { 6 for(String s:args) 7 System.out.println(s); 8 System.out.println(args.length); 9 10 } 11 12 }
Output:
5.9枚举类型
在JavaSE5中添加了一个看似很小的特性,即enum关键字,它使得我们在需要群组并使用枚举类型集时,可以很方便地处理。在此之前,你需要创建一个整型常量集,但是这些枚举值并不会必然地将其自身的取值限制在这个常量集的范围之内,因此它们显得更有风险,且更难以使用。枚举类型属于非常普遍的需求,C、C++和其他许多语言都已经拥有它了。在JavaSE5之前,Java程序员在需要使用枚举类型时,必须了解很多细节并需要格外仔细,以正确地产生enum的效果。现在Java也有了enum,并且它的功能比C/C++中的枚举类型要完备得多。下面是一个简单的例子:
1 //:initialization/Spiciness.java 2 package initialization; 3 4 public enum Spiciness { 5 NOT,MILD,MEDIUM,HOT,FLAMING 6 }
这里创建了一个名为Spiciness的枚举类型,它具有5个具名值。由于枚举类型的实例是常量,因此按照命名惯例它们都用大写字母表示(如果在一个名字中有多个单词,用下划线将它们隔开)。
为了使用enum,需要创建一个该类型的引用,并将其赋值给某个实例:
1 //:initialization/SimpleEnumUse.java 2 package initialization; 3 4 public class SimpleEnumUse { 5 6 public static void main(String[] args) { 7 Spiciness howHot=Spiciness.MEDIUM; 8 System.out.println(howHot); 9 10 } 11 12 }
Output:
在你创建enum时,编译器会自动添加一些有用的特性。例如,它会创建toString()方法,以便你可以很方便的显示某个enum实例的名字,这正是上面的打印语句如何产生其输出的答案。编译器还会创建ordinal()方法,用来表示某个特定enum常量的声明顺序,以及static values()方法,用来按照enum常量的声明顺序,产生由这些常量值构成的数组:
1 //:initialization/EnumOrder.java 2 package initialization; 3 4 5 public class EnumOrder { 6 7 public static void main(String[] args) { 8 for(Spiciness s:Spiciness.values()) 9 System.out.println(s+",ordinal"+s.ordinal()); 10 11 } 12 13 }
Output:
尽管enum看起来像是一种新的数据类型,但是这个关键字只是为enum生成对应的类时,产生了某些编译器行为,因此在很大程度上,你可以将enum当作其他任何类来处理。事实上,enum确实是类,并且具有自己的方法。
enum有一个特别实用的特性,即它可以在switch语句内使用:
1 //:initialization/Burrito.java 2 package initialization; 3 4 public class Burrito { 5 Spiciness degree; 6 public Burrito(Spiciness degree) {this.degree=degree;} 7 public void describe() { 8 System.out.print("This burrito is"); 9 switch(degree) { 10 case NOT:System.out.println("not spicy at all."); 11 break; 12 case MILD: 13 case MEDIUM:System.out.println("a little hot."); 14 break; 15 case HOT: 16 case FLAMING: 17 default:System.out.println("maybe too hot."); 18 } 19 } 20 21 public static void main(String[] args) { 22 Burrito plain=new Burrito(Spiciness.NOT), 23 greenChile=new Burrito(Spiciness.MEDIUM), 24 jalapeno=new Burrito(Spiciness.HOT); 25 plain.describe(); 26 greenChile.describe(); 27 jalapeno.describe(); 28 29 } 30 31 }
Output:
由于switch是要在有限的可能值集合中进行选择,因此它与enum正是绝佳的组合。请注意enum的名字是如何能够倍加清楚地表明程序意欲何为的。
大体上,你可以将enum用作另外一种创建数据类型的方式,然后直接将所得到的类型拿来使用。这正是关键所在,因此你不必过多地考虑它们。在JavaSE5引进enum之前,你必须花费大量的精力去保证与其等价的枚举类型是安全可用的。
这些介绍对于你理解和使用基本的enum已经足够了,但是我们将在第19章中更加深入地探讨它们。
练习21:
(1)创建一个enum,它包含纸币中最小面值的6中类型。通过values()循环并打印每一个值及其ordinal()。
1 package initialization; 2 3 enum Money{ 4 YI_JIAO,ER_JIAO,WU_JIAO,YI_YUAN,ER_YUAN,WU_YUAN 5 } 6 7 public class InTest21 { 8 9 public static void main(String[] args) { 10 for(Money m:Money.values()) 11 System.out.println(m+",ordinal"+m.ordinal()); 12 13 } 14 15 }
Output:
练习22:
(2)在前面的例子中,为enum写一个switch语句,对于每一个case,输出该特定货币的描述。
1 package initialization; 2 3 enum Money{ 4 YI_JIAO,ER_JIAO,WU_JIAO,YI_YUAN,ER_YUAN,WU_YUAN 5 } 6 7 public class InTest22 { 8 static void describe(Money m) { 9 switch(m) { 10 case YI_JIAO:System.out.println("1角");break; 11 case ER_JIAO:System.out.println("2角");break; 12 case WU_JIAO:System.out.println("5角");break; 13 case YI_YUAN:System.out.println("1元");break; 14 case ER_YUAN:System.out.println("2元");break; 15 case WU_YUAN:System.out.println("5元");break; 16 17 18 } 19 } 20 21 public static void main(String[] args) { 22 describe(Money.YI_YUAN); 23 24 } 25 26 }
Output:
5.10总结
构造器,这种精巧的初始化机制,应该给了读者很强的暗示:初始化在Java中占有至关重要的地位。C++的发明人Bjarne Stroistrup在设计C++期间,在针对C语言的生产效率所进行的最初调查中发现,大量编程错误都源于不正确的初始化。这种错误很难发现,并且不恰当的清理也会导致类似问题。构造器能保证正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象),所以有了完全的控制,也很安全。
在C++中,“析构”相当重要,因为用new创建的对象必须明确被销毁。在Java中,垃圾回收器会自动为对象释放内存,所以在很多场合下,类似的清理方法在Java中就不太需要了(不过当要用到的时候,你就只能自己动手了)。在不需要类似析构函数的行为的时候,Java的垃圾回收器可用极大地简化编程工作,而且在处理内存的时候也更安全。有些垃圾回收器甚至能清理其他资源,比如图形和文件句柄。然而,垃圾回收器确实也增加了运行时的开销。而且Java解释器从来就很慢,所以这种开销到底造成了多大的影响也很难看出。随着时间的推移,Java在性能方面已经取得了长足的进步,但速度问题仍然是它涉足某些特定编程领域的障碍。
由于要保证所有对象都被创建,构造器实际上要比这里所讨论的更复杂。特别当通过组合或继承生成新类的时候,这种保证仍然成立,并且需要一些附加的语法来提供支持。在后面的章节中,读者将学习到有关组合、继承以及它们对构造器造成的影响等方面的知识。