`
寂地流年
  • 浏览: 26700 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

.你所不知道的五件事情--Java集合框架

阅读更多

概要:你可以在任何地方使用Java集合框架,但不要想当然地使用它们。集合框架有神秘之处,如果你不能正确地对待它,它就会为你惹麻烦。Ted Neward探索了Java集合框架API中复杂且可变的部分,还给出了一些帮助你更好地利用IterableHashMapSortedSet的窍门,这些窍门将会使你的代码不会产生Bug

   
设计java.util包中集合框架类的目的就是帮助,也即替代数组,这也就提高了Java的能力。如你在上一篇文章中所学习到的,它们仍具可塑性,它们希望以不同的途径,好的方式,整洁的代码去进行定制和扩展。
   
集合框架仍然强大,但它是可变的:要小心使用之,若滥用之则会使你陷入危机中。

1. List
不同于数组

    Java
开发者经常错误地猜想ArrayList只是Java数组的替代品。集合框架的背后就是数组,这就使得在集合对象中随机地查找元素时能有好的性能。另外,如同数组那样,集合对象使用整数序数去获取特定元素。即便如此,集合对象仍不是数组的简易替代品。
将集合对象与数组区分开来的技巧就是要知道顺序与位置之间的区别。例如,List是一个接口,它为置入集合中的元素维护了顺序,如清单1所示:

清单1. Mutable keys

import java.util.*;

public class OrderAndPosition
{
    public static <T> void dumpArray(T[] array)
    {
        System.out.println("=============");
        for (int i=0; i<array.length; i++)
            System.out.println("Position " + i + ": " + array[i]);
    }
    public static <T> void dumpList(List<T> list)
    {
        System.out.println("=============");
        for (int i=0; i<list.size(); i++)
            System.out.println("Ordinal " + i + ": " + list.get(i));
    }
    
    public static void main(String[] args)
    {
        List<String> argList = new ArrayList<String>(Arrays.asList(args));

        dumpArray(args);
        args[1] = null;
        dumpArray(args);
        
        dumpList(argList);
        argList.remove(1);
        dumpList(argList);
    }
}


当删除上面List中的第三个元素时,该元素"下面"的其它元素会向上移动以填补空位。很清楚,集合对象的行为不同于数组。(事实上,从数组中删除一个元素与从List中删除一个元素大为不同--从数组中"删除"一个元素就是用一个新的引用变量或null去覆盖该元素所处的位置。)

2.
迭代器,令我大为吃惊!

   
毫无疑问,Java开发者喜欢Java集合框架中的Iterator,但你最后一次看到Iterator接口是在什么时候呢?可以这么说,多数时候,我们只是将Iterator置入for循环或改进的for循环中。
   
但对于那些善于挖掘的人,Iterator内藏两大惊人之处:
   
第一,通过调用Iterator本身的remove()方法,Iterator拥有了从来源集合对象中安全地删除元素的能力。此处的关键点在于避免了 ConcurrentModifiedException,顾名思意:当迭代器正在遍历集合对象时,又正在修改该集合。一些集合对象不会让你向正在被遍历的集合中删除或添加元素,但调用Iteratorremove()方法是一个安全的实践方式。
   
第二,Iterator支持派生出的(且功能更强大的)兄弟。ListIterator,它只存在于List实例中,支持在遍历过程中向List中添加和删除元素,并且能双向滚动(bidirectional scrolling)List对象。
双向滚动(bidirectional scrolling)在某些场景下有特别强大的功能,例如无处不在的"结果集滑动",即,从数据库或其它集合对象的众多结果中展示其中的10个。它还可以被用于"向后遍历"一个集合或列表,而不用试图从前向后地访问每个元素。使用ListIterator要比利用向下计数的整数参数的List.get() 方法去"向后遍历"一个List容易得多。

3.
并不是所有的Iterable实例都来自于集合对象
    Ruby
Groovy开发者喜欢炫耀他们怎样使用一行代码就遍历了整篇文本,并将其中的内容打印到控制台上。多数时候,他们会说,使用Java来做同样的事情需要编写许多代码:打开一个FileReader,再创建一个BufferedReader,然后创建一个while()循环去调用 getLine()方法,直到返回null为止。当然,你还必须得在一个try/catch/finally语句块中做上述事情,这个语句块用于处理异常且在结束时关闭文件句柄。
   
看起来这像是一个微不足道,学究式的争论,但它还是有些意义的。
   
他们(包括一些Java开发者)不知道并不是所有Iterable实例都要来自于集合对象。相反地,一个Iterable实例可以创建一个 Iterator实例,这个Iterator知道如何去凭空地造出下一个元素,而不是在一个预先已存在集合对象的内部默默地进行处理。

清单2 Iterating a file

// FileUtils.java
import java.io.*;
import java.util.*;

public class FileUtils
{
    public static Iterable<String> readlines(String filename)
        throws IOException
    {
        final FileReader fr = new FileReader(filename);
        final BufferedReader br = new BufferedReader(fr);
        
        return new Iterable<String>() {
            public <code>Iterator</code><String> iterator() {
                return new <code>Iterator</code><String>() {
                    public boolean hasNext() {
                        return line != null;
                    }
                    public String next() {
                        String retval = line;
                        line = getLine();
                        return retval;
                    }
                    public void remove() {
                        throw new UnsupportedOperationException();
                    }
                    String getLine() {
                        String line = null;
                        try {
                            line = br.readLine();
                        }
                        catch (IOException ioEx) {
                            line = null;
                        }
                        return line;
                    }
                    String line = getLine();
                };
            }    
        };
    }
}

//DumpApp.java
import java.util.*;

public class DumpApp
{
    public static void main(String[] args)
        throws Exception
    {
        for (String line : FileUtils.readlines(args[0]))
            System.out.println(line);
    }
}


该方法的优点在于不需要在内存中处理整个文件的内容,但有一个告诫,如上面所编写的代码,它不能关闭下层的文件句柄。(readLing()方法返回 null时就关闭文件句柄,通过该方法可以修正这一问题,但当Iterator未能遍历完整个文件时,该方法也解决不了这个问题。)

4.
意识到可变的hashCode()方法

    Map
是很好的集合对象,它带给我们只有在其它编程语言,如Perl,中才能体会到的键-值对集合的乐趣。并且JDK为我们提供了一个很棒的Map实现,HashMap,该实现在内部使用散列表,这使得快速地通过键来查找对应的值。但在那儿就会出现一个细微的问题:支持散列码的键会依赖内容可变的字段,这很容易就产生Bug。即使对那些最有耐心的Java开发者,这样的Bug也会使他们发疯。
   
想像清单3中的Person对象,它有一个典型的hashCode()方法(该方法使用firstNamelastNameage字段--所有的字段都不是final--去计算散列码),调用Mapget()方法将可能失败并返回null

清单3 可变的hashCode()使人犯错

// Person.java
import java.util.*;

public class Person
    implements Iterable<Person>
{
    public Person(String fn, String ln, int a, Person
 kids)
    {
        this.firstName = fn; this.lastName = ln; this.age = a;
        for (Person kid : kids)
            children.add(kid);
    }
    
    // 

    
    public void setFirstName(String value) { this.firstName = value; }
    public void setLastName(String value) { this.lastName = value; }
    public void setAge(int value) { this.age = value; }
    
    public int hashCode() {
        return firstName.hashCode() & lastName.hashCode() & age;
    }

    // 


    private String firstName;
    private String lastName;
    private int age;
    private List<Person> children = new ArrayList<Person>();
}


// MissingHash.java
import java.util.*;

public class MissingHash
{
    public static void main(String[] args)
    {
        Person p1 = new Person("Ted", "Neward", 39);
        Person p2 = new Person("Charlotte", "Neward", 38);
        System.out.println(p1.hashCode());
        
        Map<Person, Person> map = new HashMap<Person, Person>();
        map.put(p1, p2);
        
        p1.setLastName("Finkelstein");
        System.out.println(p1.hashCode());
        
        System.out.println(map.get(p1));
    }
}


更明确地说,上述方法令人痛楚,但解决方法却很简单:HashMap的键永远不要使用可变对象。

5. equals() vs Comparable
   
浏览Javadoc时,Java开发者们常会遇到SortedSet类型(JDK中,它的唯一实现是TreeSet)。因为SortedSet java.util包中唯一提供了某种指定排序行为的集合类,所以开发者们在一开始使用它时并没有仔细地考究其中的细节。清单4证明了这一点:

清单4 SortedSet,很高兴发现你

import java.util.*;

public class UsingSortedSet
{
    public static void main(String[] args)
    {
        List<Person> persons = Arrays.asList(
            new Person("Ted", "Neward", 39),
            new Person("Ron", "Reynolds", 39),
            new Person("Charlotte", "Neward", 38),
            new Person("Matthew", "McCullough", 18)
        );
        SortedSet ss = new TreeSet(new Comparator<Person>() {
            public int compare(Person lhs, Person rhs) {
                return lhs.getLastName().compareTo(rhs.getLastName());
            }
        });
        ss.addAll(perons);
        System.out.println(ss);
    }
}


在用了上述代码一段时间之后,你可能会发现Set的核心特性之一:它不允许重复。这一特性在SetJavadoc中有明确的描述。Set"不包含重复元素的集合"。更准确地说,对于元素e1e2,如果有e1.eqauls(e2),那么Set就不能同时包含它们,并且最多只能包含一个null元素。
   
但这似乎不是实际情况--虽然清单4没有Person对象是相等的(根据Person所实现的equals()方法),但当打印该TreeSet时,只展示了三个Person对象。
   
Set的天然状态相反,TreeSet要求对象要么实现Comparable接口,要么向构造器中直接传入一个Comparator实现,不用 equals()方法相比较对象;而是使用Comparator/Comparable中的compare/comparaTo方法。
   
存储在Set中的对象有两种潜在的方法来判定相等性:期望中的equals()方法;Comparable/Comparator方法,这依赖于调用这些方法的上下文。
更糟的是,如此简单的描述还不足以表明这二者是不同的,因为以排序为目的的比较不同于以等价性为目的的比较:当按姓氏进行排序时,某两个Person对象是相等的,但它们的内容却是不等的。
   
总是要明确equals()Comparable.compareTo()方法的区别--当实现Set时,返回零必须是清晰的。甚至于,应该在你的文档中清晰地描述这一区别。

结论
    Java
集合框架遍布有用之物,只要知道它们,就能使你的生活更简单也更富有成效。然而,挖掘出的这些有用之物经常伴随着一定的复杂度,例如,你会发现只要不在键中使用可变对象,就可以按你自己的方式去使用HashMap
   
到目前为止,我们已经对集合框架进行了深入挖掘,但我们还未触及这其中的"金矿":由Java 5引入的并发集合。本系列的后5个窍门将关注包java.util.concurrent

分享到:
评论

相关推荐

    Java 面试宝典

    Java 基础部分..................................................................................................................... 7 1、一个".java"源文件中是否可以包括多个类(不是内部类)?有什么...

    MySQL 5.1官方简体中文参考手册

    7.4.7. MyISAM索引统计集合 7.4.8. MySQL如何计算打开的表 7.4.9. MySQL如何打开和关闭表 7.4.10. 在同一个数据库中创建多个表的缺陷 7.5. 优化MySQL服务器 7.5.1. 系统因素和启动参数的调节 7.5.2. 调节服务器参数 ...

    MySQL 5.1参考手册

    7.4.7. MyISAM索引统计集合 7.4.8. MySQL如何计算打开的表 7.4.9. MySQL如何打开和关闭表 7.4.10. 在同一个数据库中创建多个表的缺陷 7.5. 优化MySQL服务器 7.5.1. 系统因素和启动参数的调节 7.5.2. 调节服务器参数 ...

    MySQL5.1参考手册官方简体中文版

    7.4.7. MyISAM索引统计集合 7.4.8. MySQL如何计算打开的表 7.4.9. MySQL如何打开和关闭表 7.4.10. 在同一个数据库中创建多个表的缺陷 7.5. 优化MySQL服务器 7.5.1. 系统因素和启动参数的调节 7.5.2. 调节服务器参数 ...

    Java经典入门教程pdf完整版

    从J5E向内看,是 Java micro edition(JME),它所支持的只有核心类的了集合,在JME CLDC的规格之中,只支持java.lang.*、java.io.*、以及java.uti1.*这些类。此版本 也增加了一些攴持“微小装置”的扩充类,如 Javax. ...

    java面试题

    答:int是java的原始数据类型,Integer是java为int提供的封装类,java为每个原始数据类型都提供了封装类。 String和StringBuffer的区别? 答:String是不可变的对象,每次对String类型进行改变都相当于产生了一个新...

    mysql官方中文参考手册

    7.4.7. MyISAM索引统计集合 7.4.8. MySQL如何计算打开的表 7.4.9. MySQL如何打开和关闭表 7.4.10. 在同一个数据库中创建多个表的缺陷 7.5. 优化MySQL服务器 7.5.1. 系统因素和启动参数的调节 7.5.2. 调节服务器参数 ...

    MYSQL中文手册

    7.4.7. MyISAM索引统计集合 7.4.8. MySQL如何计算打开的表 7.4.9. MySQL如何打开和关闭表 7.4.10. 在同一个数据库中创建多个表的缺陷 7.5. 优化MySQL服务器 7.5.1. 系统因素和启动参数的调节 7.5.2. 调节...

    MySQL 5.1参考手册中文版

    7.4.7. MyISAM索引统计集合 7.4.8. MySQL如何计算打开的表 7.4.9. MySQL如何打开和关闭表 7.4.10. 在同一个数据库中创建多个表的缺陷 7.5. 优化MySQL服务器 7.5.1. 系统因素和启动参数的调节 7.5.2. 调节服务器...

    MySQL 5.1中文手冊

    7.4.7. MyISAM索引统计集合 7.4.8. MySQL如何计算打开的表 7.4.9. MySQL如何打开和关闭表 7.4.10. 在同一个数据库中创建多个表的缺陷 7.5. 优化MySQL服务器 7.5.1. 系统因素和启动参数的调节 7.5.2. 调节服务器参数 ...

    MySQL 5.1参考手册 (中文版)

    7.4.7. MyISAM索引统计集合 7.4.8. MySQL如何计算打开的表 7.4.9. MySQL如何打开和关闭表 7.4.10. 在同一个数据库中创建多个表的缺陷 7.5. 优化MySQL服务器 7.5.1. 系统因素和启动参数的调节 7.5.2. 调节服务器参数 ...

    mysql5.1中文手册

    MyISAM索引统计集合 7.4.8. MySQL如何计算打开的表 7.4.9. MySQL如何打开和关闭表 7.4.10. 在同一个数据库中创建多个表的缺陷 7.5. 优化MySQL服务器 7.5.1. 系统因素和启动参数的调节 7.5.2. ...

    大数据培训课程安排.pdf

    主要技术包括:java基础语法、java⾯向对象(类、对象、封装、继承、多态、 抽象类、接⼝、常见类、内部类、常见修饰符等) 、异常、集合、⽂件、IO、 MYSQL(基本SQL语句操作、多表查询、⼦查询、存储过程、事务、...

    C#微软培训资料

    在本章中你将了解 Microsoft.NET 的概念 .NET 框架 C#语言在.NET 框架中的作用及其特性 1.1 Microsoft.NET 一场新的革命 1.1.1 什么是.NET 2000 年 6 月 22 日 不论对 Microsoft 还是对整个 ...

    JINI 核心技术

    3.4 Jini的五个基本概念 34 3.4.1 发现 35 3.4.2 查找 37 3.4.3 租借 41 3.4.4 远程事件 45 3.4.5 事务 51 3.5 后面的内容 56 第4章 部署方案 57 4.1 成为Jini服务 57 4.2 如何为设备和服务使用Jini 58 4.3 在通用...

    JINI核心技术

    3.4 Jini的五个基本概念 34 3.4.1 发现 35 3.4.2 查找 37 3.4.3 租借 41 3.4.4 远程事件 45 3.4.5 事务 51 3.5 后面的内容 56 第4章 部署方案 57 4.1 成为Jini服务 57 4.2 如何为设备和服务使用Jini 58 4.3 在通用...

Global site tag (gtag.js) - Google Analytics