黑马程序员——其他4:JDK1.5新特性介绍

来源:互联网 发布:北京网络教学综合平台 编辑:程序博客网 时间:2024/06/04 18:12
------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

1  增强for循环

1.1  增强for循环概述

        通常,从集合或者数组中获取元素都是通过for循环的方式对容器进行遍历。比如下面的代码,

代码1:

import java.util.*; class ForEachDemo{public static void main(String[] args){List<Integer> list = new ArrayList<Integer>(); list.add(45);list.add(12);list.add(29); //get方法for(int x=0; x<list.size(); x++){System.out.println(list.get(x));}System.out.println("=========="); //迭代器for(Iterator<Integer> it = list.iterator();it.hasNext(); ){System.out.println(it.next());}}}
执行结果为:

45

12

29

==========

45

12

29

        从早期Vector集合的迭代器Enumeration到List、Set等集合的Iterator迭代器,这一优化过程主要是对迭代器类名和方法名进行了简化,提高了易用性,而为了进一步简化代码的书写,在JDK1.5版本以后推出了所谓的增强for循环——foreach。

        我们首先来查阅Collection接口的API文档,在JDK1.5版本时,Collection不再是集合类的根父接口,它本身也实现了一个接口——Iterable,意思是可迭代的,打开该接口的API文档,其方法摘要中只有一个方法——iterator,换句话说Iterable接口的出现就是将迭代器方法抽取出来了,这样一来如果今后定义了新的集合框架系列(独立于Collection和Map以外的集合),只需要实现Iterable接口就能够具备迭代器方法。

        除此以外,Iterable接口的描述只有一句话:实现这个接口允许对象成为"foreach"语句的目标。相对应的,Map接口并没有实现Iterable,因此Map接口并没有迭代器方法,也不能直接使用增强for循环,但是可以间接使用,在后面的内容中会进行介绍。

1.2  增强for循环格式与应用

        Sun公司为Java程序员提供的Java语言语法规则文档中,给出了增强for循环的书写格式,格式如下,

EnhancedForStatement:

        for ( VariableModifiersopt Type Identifier : Expression)Statement

这里我们对以上的格式进行几点说明,

(1)    增强for循环的关键字依然是for,与普通for循环是一样的,有些面试题中会将for写成foreach,大家一定要注意;

(2)    圆括号中冒号左侧的Identifier可以理解为临时变量,用于存储每一个从容器中遍历到的元素,Type即是这一临时变量的类型。当然这一类型必须要与冒号右侧容器中存储的元素类型相同。

(3)    冒号右侧的Expression意思为表达式,换句话说,这里我们可以直接放置一个数组或集合,也可以通过一行代码返回一个容器。再次强调,这一容器的必须实现了Iterable接口,才能实现增强for循环。

(4)    Statement表示的就是对每个元素进行的操作。

(5)    VariableModifier表示可以为临时变量定义访问权限限制。比如,将临时变量定义为final,这样就可以令其被内部类所访问。当然这一访问权限的定义是可选操作,不定义并不违反语法规则。

我们根据以上格式,再次遍历代码1中的集合,

代码2:

import java.util.*; class ForEachDemo2{public static void main(String[] args){List<Integer> list = new ArrayList<Integer>(); list.add(45);list.add(12);list.add(29);             //通过增强for循环遍历集合for(Integer in : list){System.out.println(in);}}}
执行结果同样为:

45

12

29

        以上是对集合的遍历,这里需要注意的是,只有当集合定义了泛型类型时,才可以令临时变量的类型与集合泛型类型相同,如果集合并没有定义泛型时,临时变量类型只能定义为Object。

我们再演示对数组的遍历,

代码3:

import java.util.*; class ForEachDemo3{public static void main(String[] args){int[] arr = {1,2,3}; for(int i : arr){System.out.println(i);}}}
执行结果为:

1

2

3

        这里可以给大家一些提示,当我们使用增强for循环时,被遍历集合(包括数组)的名称通常为某个单词的复数,表示该集合中存储的是多个这一类事物,那么用于存储集合中元素的临时变量名就可以定义为这一类事物名称的单数形式。

1.3  增强for循环原理

        实际上所谓的增强for循环底层依然还是使用了传统的遍历方式:对于集合而言是迭代器,而数组而言就是普通for循环,因此增强for循环的升级,主要目的就是简化代码的书写。

        增强for循环中的临时变量首先初始化为容器中的第一个元素,随后每一次执行过程中,该临时变量就会按顺序依次指向容器中的其他元素。此外,如果在遍历过程中,对临时变量进行重新赋值,是不会影响到集合中原有的元素的,比如我们对代码3进行如下改动,

代码4:

//只呈现循环部分代码for(Integer in : list){in = 50;System.out.println(in);}System.out.println(list);
执行结果为:

50

50

50

[45, 12, 29]

        结果显示,虽然在遍历过程中将临时变量赋予了其他值,但是最终打印集合中内容时,显示的依然是原集合中的元素。这也就体现了临时变量的作用:每一次循环时,用于临时存储从容器中获取到的元素,这一动作显然是不会更改原容器中的内容的。因此增强for循环虽然方便,但是却具有较大的局限性——无法像Iteraotr(remove方法)或者ListIterator那样对集合中的元素直接进行增删改查等操作。

        除此以外,增强for循环的另一个局限性是:若要实现循环,必须具备被遍历的目标,而这一目标就是处于冒号右侧表示容器的变量。举个例子,比如需要重复打印"Hello World!"100次,传统for循环可以轻易做到,但是却无法通过增强for循环实现——因为没有被遍历的目标,换句话说没有容器。

        基于以上原因,当需要遍历集合和数组时,尽可能使用传统for循环,因为传统for循环能够实现更为复杂的功能。

1.4  Map集合实现增强for循环

        在前述内容中,我们曾经提到由于Map集合并不是Iterable接口的实现接口,因此不能直接通过增强for循环获取到集合中的键值对。但是,获取Map集合中的元素必须依靠keySet和entrySet方法来实现,而这两个方法返回的两种Set集合是可以应用增强for循环的,如下代码所示,

代码5:

import java.util.*; class ForEachDemo4{public static void main(String[] args){HashMap<String,Integer> hm = new HashMap<String,Integer>(); hm.put("Jack",34);hm.put("Tom",21);hm.put("Anna",28); //keySetSet<String> keyset = hm.keySet();for(String key : keyset){System.out.println(key+"="+hm.get(key));}System.out.println("===========");      //entrySetSet<Map.Entry<String,Integer>> entryset = hm.entrySet();for(Map.Entry<String,Integer> me : entryset){String key = me.getKey();Integer value = me.getValue();System.out.println(key+"="+value);}}}
执行结果为:

Jack=34

Tom=21

Anna=28

===========

Jack=34

Tom=21

Anna=28

以上代码中,分别对keySet和entrySet方法间接应用了增强for循环,实现了对Map集合中键值对的遍历。

2  可变参数列表

2.1  问题的提出

        可变参数列表同样是JDK1.5版本出现的新特性,我们首先通过下面的例子来引出可变参数列表的由来。

假设我们想定义一个加法运算方法,如下所示,

代码6:

public int plus(int a, int b){return a+b;}
如果我们接着想定义一个对三个数值的加法运算,通常我们需要定义一个重载方法,比如,

代码7:

public int plus(int a, int b, int c){return a+b+c;}
按照这一思路,如果我们想定义一个对四个整数的加法运算,那就又要定义一个新的重载方法了,这看似非常的繁琐。

        为了免去不断定义重载方法的麻烦,我们针对这一需求再思考另一种思路:我们想对几个整数进行加法运算,就创建包含几个元素的数组,然后将这一数组传递到加法方法中,最后获取数组中的元素进行运算即可,如以下代码所示,

代码8:

public static int plus(int[] arr){int sum = 0;for(int x=0; x<arr.length; x++){sum += arr[x];}        return sum;}
假设我们在主函数中定义数组int[] arr = {23, 12, 67},并在调用代码3中的plus方法时传递该arr数组,执行结果为:102。

        该方法虽然解决了不断定义不同长度参数列表的麻烦,但是每次进行计算以前,都要创建一个包含需要进行加法运算的数值的数组,应用起来还是有些不便。

2.2  问题的解决

        那么为了尽可能解决参数长度不确定而导致的种种不便,Java语言的工程师们设计了一个巧妙的解决方法:可变参数列表。我们以上述需求为例介绍这一新特性,其定义格式如下,

代码9:

import java.util.*; class ParaMethodDemo{public static void main(String[] args){int a = 23, b = 12, c = 67;int d = 40;int e = 44, f = 90, g = 34, h = 56, i = 33; System.out.println(plus(a, b, c));System.out.println(plus(d));System.out.println(plus(e, f, g, h, i));}public static int plus(int... arr)//定义可变参数列表{System.out.println(arr);System.out.println(arr.length); int sum = 0;for(int x=0; x<arr.length; x++){sum += arr[x];} return sum;}}
执行结果为:

[I@175078b

3

102

[I@42552c

1

40

[I@e5bbd6

5

257

        每一组执行结果的第一、二行表明,形式参数arr依然是一个数组,只不过该参数的变量类型为“int…”,而数据类型后的“…”就表示该参数列表是一个可变参数列表,也就是说,可以向plus方法传递任意个数的参数,而不必再创建数组,更不需要定义参数列表长度不同的重载方法了。由于可变参数列表的底层原理还是使用了一个数组接收参数,因此我们可以将可变参数完全当做数组来操作,比如调用其length成员变量等,只不过不再需要程序员手动创建数组,而是隐式地将这一过程封装了起来,最终的目的就是用于简化代码的书写,提高开发效率。需要提醒大家的是,在定义可变参数列表时,“int”、“…”以及变量名之间可以存在空格,也可以不加,比如“int … arr”、“int… arr”或者“int …arr”这些写法都是被允许的,并没有严格的语法限定。当然像“int… arr”这样,将变量类型与变量名分开写的方式更具有可读性。

        在前面的内容中,我们曾经介绍了Arrays工具类的静态方法asList,该方法的参数列表就是可变参数列表。

2.3  注意事项

        在定义带有可变参数列表的方法时,一定要注意,可变参数列表参数一定要置于方法参数列表的末尾,如下所示,

代码10:

public void method(String str, int... arr){//方法体}//如下的定义方式将无法通过编译public void method(int… arr, String str){//方法体}
        出现这一问题的原因是,当我们向第二种method方法传递参数时,首先传递一串整数,然后传递一个字符串,而按照可变参数列表的特点,它会自动把传递进来的参数全部添加到数组中,包括最后添加的一个字符串,这就会导致数组类型与参数类型不一致的问题。而反过来,如果先传递一个字符串,那么方法的形式参数str将该字符接收以后,就会通过可变参数列表将后面的所有整数值都存储起来了。

3  静态导入

        在前面的内容中,我们介绍过两种常用的集合工具类Collections和Arrays。在使用这两个工具类之前,如果没有进行导包动作,就需要使用这些类的全类名来调用其静态方法,就像“java.util.Arrays.方法名”,或者“java.util.Collections.方法名”等。那么为了简化代码的书写,通常我们都会在“.java”文件的开头添加代码“import java.util.*”,以此来完成导包动作,避免了使用全类名的麻烦,就像下面的代码所示(以Arrays为例),

代码11:

import java.util.*; class StaticImport{public static void main(String[] args){int[] arr = new int[]{412,890,224,609}; Arrays.sort(arr);//排序System.out.println(Arrays.toString(arr));//将数组转换为字符串System.out.println("index="+Arrays.binarySearch(arr,609));//二分查找}}
执行结果为:

[224, 412, 609, 890]

index=2

虽然导包简化了类名的书写,但还是需要不断重复地通过类名去调用各种方法,而静态导入的出现可以进一步对代码进行简化——不必通过类名,而是直接调用静态方法即可,如下代码所示,

代码12:

import java.util.Arrays;//首先导入Arrays类import static java.util.Arrays.*;//再导入Arrays类中的所有静态成员 class StaticImport2{public static void main(String[] args){//Arrays应用演示int[] arr = new int[]{412,890,224,609}; sort(arr);//排序System.out.println(Arrays.toString(arr));//将数组转换为字符串System.out.println("index="+binarySearch(arr,609));//二分查找}}
执行结果为:

[224, 412, 609, 890]

index=2

结果表明,进行静态导入后,不再需要通过类名,而是直接调用方法即可。那么关于静态导入,需要强调三点注意事项:

(1)    静态导入格式一定是“importstatic”后接包名+类名+方法名(或者*,表示全部静态成员),不要写成“static import”,否则无法通过编译;

(2)    上述代码中toString方法依然还是通过类名进行调用,这是因为StaticImport2类本身继承自Object类,而Object类中同样定义了toString方法,那么两个类的方法名重复时,就必须指明方法所属的类了。那么同样,一段代码中涉及到同名类时,就需要在类名前指定类所在包名,以示区分。

(3)    就像代码2那样,在进行静态导入以前,也就是导入某个类的静态成员以前,首先需要导入该类,如果仅导入类的静态成员,就会因找不到静态成员所属类而发生编译错误。

        我们再举一个我们常用的工具类——System。该类位于Java标准类库的java.lang包中,我们创建的任意一个“.java”文件中都隐式地导入了这个类,因此我们可以直接使用该类的方法进行打印——System.out.println()。查阅System的API文档可知,out是System类的一个静态成员变量,因此也可以对其进行静态导入,简化书写,如下代码所示,

代码13:

import static java.lang.System.*;//导入System类的所有静态成员 class StaticImport3{public static void main(String[] args){out.println("HelloWorld!");//不再需要通过类名调用}}
执行结果为:

Hello World!

        至此,我们对静态导入做一个简单总结:非静态导入,导入的都是类;静态导入的都是某各类的静态成员。

        需要注意的是,当我们在eclipse软件下应用JDK1.5的新特性时,一定要将当前工作空间下的编译器和Java虚拟机版本设置为1.5(5.0)及以上版本,否则由于编译器无法识别这些语法而报错。

4  基本数据类型包装类

        其实,基本数据类型包装类在JDK1.0版本时就已经存在,只不过在1.5版本时为其添加了一些新的特性,因此将包装类放到这里来介绍。

        大家都知道Java语言中有8种基本数据类型,它们分别是:boolean、char、byte、short、int、long、float、double。大家在编写自己的代码的时候,经常需要将某个表示十进制数的字符串对象转换为int型变量,或者将int型变量转换为对应的字符串,而自己设计算法进行转换时比较麻烦的。那么设计基本数据类型对象包装类的最大意义就是为我们提供了非常便捷的方法解决上述两个问题。下面就是基本数据类型及其包装类的对应关系:

基本数据类型       基本数据类型对象包装类

boolean                 Boolean

char                      Character

byte                      Byte

short                     Short

int                        Integer

long                      Long

float                      Float

double                   Double

4.1  基本数据类型对象包装类方法介绍

(1)  将基本数据类型转换为字符串

        其实不通过包装类也是可以实现这个需求的,通常的做法就是:

基本数据类型变量+“”;

        不过,包装类给我们提供了更为专业的做法,这里我们以Integer类为例进行说明。该类对外提供了一个静态的toString方法,方法描述如下:

static String toString(int i):该方法将指定的int变量转换为对应的字符串并返回。注意与从Object类继承来的toString方法相区别。

代码演示,

代码14:

class IntegerDemo{public static void main(String[] args){int x = 100; //方法一String str1 = x + "";System.out.println(str1);             //方法二String str2 = Integer.toString(x);System.out.println(str2);}}
运行结果为:

100

100

(2) 将字符串转换为基本数据类型

        当我们在使用一些软件的时候,经常会向软件输入一些数字,软件会在底层对这些数字进行数学上的判断或者计算,但是通常,软件都会以字符串的形式接收这些数字,那么在进行判断或者计算之前,就需要将这些字符串转换为基本数据类型。这里我们还是以Integer类为例进行说明。有两种方法可以通过Integer类将字符串转为基本数据类型。

方法一:

        Integer类对外提供了一个静态方法,方法描述如下,

static int parseInt(String s):将指定的字符串转换对应的int类型变量,并返回。如果向该方法传入的字符串中包含除0-9以外的字符,就会抛出NumberFormatException异常,因此参数字符串中只能包含数字。

代码演示,

代码15:

class IntegerDemo2{public static void main(String[] args){//将字符串转换为对应的int型变量int num = Integer.parseInt(“100”);num+ = 50;             System.out.println(“num= “+num);}}
运行结果为:

num = 150

        除Character类以外的包装类均对外提供了与上述方法类似的转换方法,方法名均为:parse+包转类名。

关于该方法有两点特殊的地方需要强调:

(1)    当调用Boolean类的parseBoolean方法时,传入参数除非是“True”或者“true”,否则一律返回false。

(2)    大家如果查阅Character类的API文档会发现,该类并没有对外提供parse方法,这是因为字符串本身就是字符,无需进行转换。另外,再多说一句,Character类提供了很多专门用于操作字符的方法,比如将一个表示小写字符的char类型变量转换为大写形式等等。

方法二:

        此外,Integer类还提供了一种参数为字符串的构造方法,在创建Integer对象的时候,为其初始化一个字符串形式的数字,如果需要获取该字符串对应的整型数,可以通过该对象调用intValue方法,方法描述为:

intintValue():以int类型返回该Integer对象的值。

代码演示,

代码16:

class IntegerDemo3{public static void main(String[] args){Integer i = new Integer("100");int value = i.intValue();             System.out.println(value);}}
运行结果为:

100

4.2  进制转换

        Integer类还对外提供了很多其他的方法,这里就不一一介绍了,请大家自行查阅API文档。在这里我们重点介绍进制转换方法。

(1)  十进制转换为其他进制

staticString toBinaryString(int i):将指定整型变量转换为对应的二进制形式字符窜,并返回。

staticString toOctalString(int i):将指定整型变量转换为对应的八进制形式字符窜,并返回。

staticString toHexString(int i):将指定整型变量转换为对应的十六进制形式字符窜,并返回。

代码17:

class IntegerDemo4{public static void main(String[] args){int num = 60;System.out.println("二进制形式:"+Integer.toBinaryString(num));System.out.println("八进制形式:"+Integer.toOctalString(num));System.out.println("十六进制形式:"+Integer.toHexString(num));}}
运行结果为:

二进制形式:111100

八进制形式:74

十六进制形式:3c

(2)  其他进制转换为十进制

        static intparseInt(String s, int radix):将指定字符串当做指定进制的数转换为对应的十进制数,并返回。其中radix表示指定的进制。

代码演示,

代码18:

class IntegerDemo5{public static void main(String[] args){String num = "110"; //作为十进制打印System.out.println(Integer.parseInt(num,10));//作为二进制转换为十进制打印System.out.println(Integer.parseInt(num,2));//作为八进制转换为十进制打印System.out.println(Integer.parseInt(num,8));//作为十六进制转换为十进制打印System.out.println(Integer.parseInt(num,16));}}
运行结果为:

110

6

72

272

        注意调用该方法时,如果传入“3C”和10,将会抛出NumberFormatException异常,因为“C”并不属于十进制数。因此,传递的字符串形式数字和进制之间必须要匹配。

4.3  比较

        先来看下面的代码,

代码19:

class IntegerDemo6{public static void main(String[] args){Integer i1 = new Integer("100");Integer i2 = new Integer(100); //用比较运算符比较System.out.println("i1== i2 ? "+(i1 == i2));//用equals方法比较System.out.println("i1.equals(i2)? "+i1.equals(i2));       }}
运行结果为:

i1 == i2 ? false

i1.equals(i2) ? true

        第一行运行结果是显然的,因为i1和i2指向了两个不同的对象。而第二行结果之所以使true,是因为,Integer类与String类类似,也复写了Object类的equals方法,比较的是两个对象的内容,而不是地址值。

 

小知识点1:

        Integer类除了对外提供了上述方法以外,在该类内部还定义了两个字段,它们分别是MAX_VALUE和MIN_VALUE,而这两个值就分别对应了int类型变量的上限231-1和下限-231。当我们需要对某个整型变量进行安全性判断(检查该值是否在int类型范围之内)时,无需记忆上下限值,就可以直接使用,非常方便。

 

4.4  包装类1.5版本新特性

(1)  自动装箱

        在JDK1.5版本以前,如果想为Integer对象赋予某个数值,通常采用以下的方式,

Integer in1 = new Integer(100);

而在1.5版本以后代码可以简化为,

Integer in2 = 100;

这就是所谓的自动装箱动作。实际上赋值符号右端底层还是通过new关键字创建了一个Integer对象,并为其初始化100,只不过是隐式完成而已,因此自动装箱的意义就在于简化书写。

(2)  自动拆箱

        Integer类变量不仅可以直接被赋予int型数值,还可以与int型数值进行运算,比如,

代码20:

class IntegerDemo7{public static void main(String[] args){//自动装箱Integer in = 20;//自动拆箱in += 5;             System.out.println(in);}}
运行结果为:

25

        实际上,上述代码中自动拆箱的原理是Integer对象调用了intValue方法获取到了该对象的int数值,并与5进行运算以后,再重新封装到了Integer对象中。

        大家需要注意的是,上述代码7中in是一个引用行变量,那么它就可以被赋予null。在这种情况下,一旦与其他int型数值进行运算就会抛出NullPointerException异常。因此,自动装箱拆箱在简化代码书写的同时,降低了代码健壮性。因此,在对包装类对象进行调用或运算前一定要进行安全性判断。

(3)  基本数据类型包装类的享元设计模式

        先看下面的代码,

代码21:

class IntegerDemo8{public static void main(String[] args){Integer in1 = 128;Integer in2 = 128;System.out.println(in1 == in2);System.out.println(); Integer in3 = 127;Integer in4 = 127;System.out.println(in3 == in4);}}
运行结果为:

false

 

true

        第一行结果是显然的,因为in1和in2分别指向了两个不同的对象。但是in3和in4为什么指向了同一个对象呢?这是因为,当我们通过自动装箱的方式创建Integer对象时,如果封装的int值在-128~127(byte型数值的范围)内时,Java虚拟机为了节省内存空间,就会将这一整型值缓存到了常量池中。当再次通过相同方式创建一个封装有同一个整型值的Integer对象时,Java虚拟机首先会去搜索常量池中是否已经存在有该整型值,如果有就令新对象直接指向常量池中的int值,而不再开辟新的空间创建新Integer对象了,换句话说,新旧两个Integer对象,指向了同一个整型值,它们引用的地址是相同的。这一点于字符串对象非常相似。那么当我们通过new关键字创建了两个封装有相同整型值的Integer对象时,即使整型值的大小在-128~127范围内,它们也是代表了两个不同的对象。

        以上所述就是所谓的享元设计模式(Flyweight),大家可以理解为共享同一个元素,或者单元。那么享元设计模式的思想是,当我们需要很多体积较小的对象,并且这些对象中封装有部分相同内容时,就可以将这些对象抽取为一个对象,而将这些对象中的不同部分,变为外部的属性,作为方法的参数传入,通过不同的参数,使得方法的调用产生不同的效果,以此来区分这些对象。

        而之所以包装类要应用享元设计模式设计是因为,字节值大小范围内的数字,在一段代码中重复出现的频率可能非常高的(相比于较大的数字而言),并且通常将某个整型值封装为包装类对象后,一般不会去修改其中的值。在这种情况下,如果每创建一个包装类就新开辟内存空间,而很多包装类中的封装值都是重复的话,将非常浪费内存空间,因此将这一范围的数字变为常量缓存起来,以便于节省内存开支。

        我们再举一个享元设计模式的例子。比如文档编辑软件Word,当我们向文本区域中输入英文文本时,每个英文字母就是一个字符对象,那么一个1000个字符的文本就需要创建1000个对象,而其中有很多字母是重复的,因为英文字母只有26个。因此就可以将26个字母缓存为26个字符对象常量,不同位置处的同一个字母所指向的内存地址是相同的。而为了体现同一字母在不同位置的显示效果,可以为字符对象定义一个display(int x, int y)方法,参数表示该字符将要显示的行数和列数(或者坐标)。这样一篇文章的显示效果就是通过调用每个字符对象的display方法实现,而不需要创建那么多的对象。

        最后还想提醒大家Integer类的静态方法valueOf。该方法有两种重载形式,如下所示:

publicstatic Integer valueOf(int i)

publicstatic Integer valueOf(String s)

以上两种方法,前者表示将一个整型值转换为一个Integer对象,而后者表示将一个字符串形式的数字转换为Integer对象。无论是哪一种重载形式,他们的调用效果与通过自动装箱的方式创建的Integer对象时相同的——当整型值大小在字节值大小范围时,两Integer对象的将指向常量池中的同一个地址,当超过这一范围时,比较两同值Integer对象是否相同(通过“==”判断)时的返回值为false。

0 0