Java学习者论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

手机号码,快捷登录

恭喜Java学习者论坛(https://www.javaxxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,购买链接:点击进入购买VIP会员
JAVA高级面试进阶视频教程Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程

Go语言视频零基础入门到精通

Java架构师3期(课件+源码)

Java开发全终端实战租房项目视频教程

SpringBoot2.X入门到高级使用教程

大数据培训第六期全套视频教程

深度学习(CNN RNN GAN)算法原理

Java亿级流量电商系统视频教程

互联网架构师视频教程

年薪50万Spark2.0从入门到精通

年薪50万!人工智能学习路线教程

年薪50万!大数据从入门到精通学习路线年薪50万!机器学习入门到精通视频教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程 MySQL入门到精通教程
查看: 259|回复: 0

[集合学习]你所不知道的五件事情:当心可变性

[复制链接]
  • TA的每日心情
    开心
    2021-3-12 23:18
  • 签到天数: 2 天

    [LV.1]初来乍到

    发表于 2014-10-30 23:55:47 | 显示全部楼层 |阅读模式
    这是Ted NewardIBM developerWorks5 things系列文章中的一篇,仍然讲述了关于java集合框架的一些应用窍门,值得大家学习。(2010.05.08最后更新)

         概要:你可以在任何地方使用Java集合框架,但不要想当然地使用它们。集合框架有神秘之处,如果你不能正确地对待它,它就会为你惹麻烦。Ted Neward探索了Java集合框架API中复杂且可变的部分,还给出了一些帮助你更好地利用Iterable,HashMap和SortedSet的窍门,这些窍门将会使你的代码不会产生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);
         }
         
    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,顾名思意:当迭代器正在遍历集合对象时,又正在修改该集合。一些集合对象不会让你向正在被遍历的集合中删除或添加元素,但调用Iterator的remove()方法是一个安全的实践方式。
         第二,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
      
    Iterator
    <
    String
    >
      iterator() {
                     
    return
      
    new
      
    Iterator
    <
    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()方法(该方法使用firstName,lastName和age字段--所有的字段都不是final的--去计算散列码),调用Map的get()方法将可能失败并返回null。

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

      
    //
      Person.java


    import
      java.util.
    *
    ;


    public
      
    class
      Person   
    implements
      Iterable
    <
    Person
    >

    {

        public Person(String fn, String ln, int a) {
            this.firstName = fn; this.lastName = ln; this.age = a;

         }
         
    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;
         }

        public Iterator<Person> iterator() { return children.iterator(); }



         
    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(persons);
             System.out.println(ss);
         }
    }

    在用了上述代码一段时间之后,你可能会发现Set的核心特性之一:它不允许重复。这一特性在Set的Javadoc中有明确的描述。Set是"不包含重复元素的集合"。更准确地说,对于元素e1和e2,如果有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。

      

      
      
       
       

         
       

         
       
      
    复制代码

    源码下载:http://file.javaxxz.com/2014/10/30/235547578.zip
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|Java学习者论坛 ( 声明:本站资料整理自互联网,用于Java学习者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-18 21:29 , Processed in 0.398438 second(s), 48 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表