Java语言十五讲(第十四讲 容器框架二)

LinkedList它实现的基础是双向链表,因此在插入删除方面具有性能优势,它也可以用来实现stack和queue。顺便说一句,Java容器框架中有一个遗留的类Stack,它是基于Vector实现的,被大师们评价为“幼稚的设计”,我们不要用。
LinkedList主要有三个属性:

int size
Node<E> first
Node<E> last

也就是通过一个链表把size个Node从头串到尾。而Node就是一个类的节点包装类,有item,有prev和next。图示如下:

LinkedList也是List,因此同样具有List的那些操作,这一点跟ArrayList一样,因此下面我们只介绍它不一样的部分。
LinkedList同时也实现了Deque,因此它具有Deque的方法,如下面的12个:

First Element (Head)    Last Element (Tail)
Insert    addFirst(e)
offerFirst(e)
addLast(e)
offerLast(e)

Remove    removeFirst()
pollFirst()
removeLast()
pollLast()

Examine    getFirst()
peekFirst()
getLast()
peekLast()

如果我们把它看成FIFO先进先出的,就成了一个Queue了,Deque扩展了Queue,有几个方法是完全等同的:

Queue Method    Equivalent Deque Method
add(e)
addLast(e)

offer(e)
offerLast(e)

remove()
removeFirst()

poll()
pollFirst()

element()
getFirst()

peek()
peekFirst()

我们如果把它看成FILO先进后出的,那就成了一个stack,事实上有几个方法也是一样的功能:

Stack Method    Equivalent Deque Method
push(e)
addFirst(e)

pop()
removeFirst()

peek()
peekFirst()

下面我们还是用一个例子把上面的12个方法简单演示一下,使用的方法有

addFirst(e)    offerFirst(e)   addLast(e)  offerLast(e)
removeFirst()    pollFirst() removeLast()    pollLast()
getFirst()    peekFirst() getLast()   peekLast()
代码如下(LinkedListTest1.java):
public class LinkedListTest1 {
    public static void main(String[] args) {
        LinkedList<String> list1 = new LinkedList<>();
        list1.addFirst("北京");
        list1.offerFirst("上海");
        list1.addLast("广州");
        list1.offerLast("深圳");
        list1.offer("杭州");
        list1.add("苏州");
        list1.push("厦门");
        System.out.println(list1);

        System.out.println(list1.get(2));
        System.out.println(list1.getLast());
        System.out.println(list1.getFirst());
        System.out.println(list1.peek());
        System.out.println(list1.peekFirst());
        System.out.println(list1.peekLast());
        System.out.println(list1);

        list1.remove();
        list1.removeLast();
        list1.removeFirst();
        list1.remove("深圳");
        list1.poll();
        list1.pollLast();
        list1.pop();
        System.out.println(list1);
    }
}

大家自己运行一下,很简单。

我们提到过,List是有次序的,次序就是放进去的次序。如果要另外排序呢?可以的。我们看一个简单的例子,代码如下(ListSort.java):

public class ListSort {
    public static void main(String[] args) {
        List<Student> list = new ArrayList<>();
        list.add(new Student(2,"b","very good"));
        list.add(new Student(1,"a","good"));
        list.add(new Student(3,"c","basic"));
        System.out.println(list);

        list.sort((s1,s2)->s1.name.compareTo(s2.name));
        System.out.println(list);
    }
}

运行结果:

[b-very good, a-good, c-basic]
[a-good, b-very good, c-basic]

从运行结果可以看出,list重新按照我们给的规则(名字排序)排序了,实现一个Comparator就可以了。
我们这边自定义的是值的比较规则,而排序算法是没有地方选择的,不同的JDK版本内部的排序算法是不一样的,JDK6和之前的版本,都是用的merge sort(归并排序算法),JDK7及之后用的Tim排序算法。Tim排序算法是结合了归并排序和插入排序的新算法,对各种数据排列都比较好,而merge排序算法要对基本排好的数据再排序会很好,而有的数据效果比较差,性能接近o(n2)。

public class ListSort {
    public static void main(String[] args) {
        long start;
        long end;
        int bound = 10;

        List<Integer> list1 = new ArrayList<>();
        for (int i=0; i<bound; i++){
            list1.add(i);
        }
        start=System.currentTimeMillis();
        System.out.println(list1);
        list1.sort((i1,i2)->i1-i2);
        end=System.currentTimeMillis();
        System.out.println(list1);
        System.out.println(end-start);

        Random r = new Random();
        List<Integer> list2 = new ArrayList<>();
        for (int i=0; i<bound; i++){
            list2.add(r.nextInt(bound));
        }
        start=System.currentTimeMillis();
        System.out.println(list2);
        list2.sort((i1,i2)->i1-i2);
        end=System.currentTimeMillis();
        System.out.println(list2);
        System.out.println(end-start);

        List<Integer> list3 = new ArrayList<>();
        for (int i=bound-1; i>=0; i--){
            list3.add(i);
        }
        start=System.currentTimeMillis();
        System.out.println(list3);
        list3.sort((i1,i2)->i1-i2);
        end=System.currentTimeMillis();
        System.out.println(list3);
        System.out.println(end-start);      
    }
}

结果为:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
442
[7, 8, 1, 7, 3, 0, 8, 0, 6, 8]
[0, 0, 1, 3, 6, 7, 7, 8, 8, 8]
2
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2

不用管具体的数值,只是感觉一下原始数据不同排列情况下,排序算法性能差异很大。

我们嘴巴上老是说LinkedList和ArrayList之间性能的差异,现在我用一个例子演示一下,这个例子不是我写的,我直接从《Thinking in Java》里抄过来的,所以版权属于Bruce Eckel。代码如下(ListPerformance.java):

public class ListPerformance {
    private static final int REPS = 100;
    private abstract static class Tester {
        String name;
        int size;
        Tester(String name, int size) {
            this.name = name;
            this.size = size;
        }
        abstract void test(List a);
    }
    private static Tester[] tests = {new Tester("get", 300) {
        void test(List a) {
            for (int i = 0; i < REPS; i++) {
                for (int j = 0; j < a.size(); j++) {
                    a.get(j);
                }
            }
        }
        }, new Tester("iteration", 300) {
            void test(List a) {
                for (int i = 0; i < REPS; i++) {
                    Iterator it = a.iterator();
                    while (it.hasNext()) it.next();
                }
            }
        }, new Tester("insert", 1000) {
            void test(List a) {
                int half = a.size() / 2;
                String s = "test";
                ListIterator it = a.listIterator(half);
                for (int i = 0; i < size * 10; i++) {
                    it.add(s);
                }
            }
        }, new Tester("remove", 5000) {
            void test(List a) {
                ListIterator it = a.listIterator(3);
                while (it.hasNext()) {
                    it.next();
                    it.remove();
                }
            }
        },
    };
    public static void test(List a) {
        System.out.println("Testing " + a.getClass().getName());
        for (int i = 0; i < tests.length; i++) {
            fill(a, tests[i].size);
            System.out.print(tests[i].name);
            long t1 = System.currentTimeMillis();
            tests[i].test(a);
            long t2 = System.currentTimeMillis();
            System.out.print(":" + (t2 - t1)+" ms ");
        }
    }
    public static Collection fill(Collection c, int size) {
        for (int i = 0; i < size; i++) {
            c.add(Integer.toString(i));
        }
        return c;
    }
    public static void main(String[] args) {
        test(new ArrayList());
        System.out.println();
        test(new LinkedList());
    }
}

运行之后的结果是:

Testing java.util.ArrayList
get:4 ms iteration:6 ms insert:6 ms remove:28 ms 
Testing java.util.LinkedList
get:13 ms iteration:5 ms insert:2 ms remove:4 ms 

结果印证了我们的说法,ArrayList确实get比较块,LinkedList确实删除增加比较快,而iterator两者差不多的。

Bruce Eckel还提供了一个更加专业的测试,结果如下:

--- Array as List ---
 size     get     set
   10     130     183
  100     130     164
 1000     129     165
10000     129     165
--------------------- ArrayList ---------------------
 size     add     get     set         iteradd  insert  remove
   10     121     139     191     435    3952     446
  100      72     141     191     247    3934     296
 1000      98     141     194     839    2202     923
10000     122     144     190    6880   14042    7333
--------------------- LinkedList ---------------------
 size     add     get     set         iteradd  insert  remove
   10     182     164     198     658     366     262
  100     106     202     230     457     108     201
 1000     133    1289    1353     430     136     239
10000     172   13648   13187     435     255     239
----------------------- Vector -----------------------
 size     add     get     set         iteradd  insert  remove
   10     129     145     187     290    3635     253
  100      72     144     190     263    3691     292
 1000      99     145     193     846    2162     927
10000     108     145     186    6871   14730    7135
-------------------- Queue tests --------------------
 size            addFirst     addLast     rmFirst      rmLast
   10         199         163         251         253
  100          98          92         180         179
 1000          99          93         216         212
10000         111         109         262         384

按照Bruce Eckel的建议,首选ArrayList,当确认要对数据进行频繁的增加删除的时候,就用LinkedList。

好,我们讲过了List,我们接着讲讲Map。
Java容器框架中,Map是独立的一类。我们讲讲用得最多的HashMap。接口是Map,还有个抽象类AbstractMap,具体的实现类是HashMap。
HashMap 是Java的键值对数据类型容器。它根据键的哈希值(hashCode)来存储数据,访问速度高,性能是常数,没有顺序。HashMap 允许键值为空和记录为空,非线程安全。
先看一个简单的例子,代码如下(HashMapTest.java):

public class HashMapTest {
      public static void main(String[] args) {
            Map<String, String> map = new HashMap<>();
            map.put("BJC", "北京首都机场");
            map.put("PDX", "上海浦东机场");
            map.put("GZB", "广州白云机场");
            map.put("SZX", "深圳宝安机场");

            String usage = map.get("SZX");
            System.out.println("Map: " + map);
            System.out.println("Map Size:  " + map.size());
            System.out.println("Map is empty:  " + map.isEmpty());
            System.out.println("Map contains PDX key:   " + map.containsKey("PDX"));
            System.out.println("Usage:  " + usage);
            System.out.println("removed:  " + map.remove("SZX"));
      }
}

程序很简单,把一个个key-value放入HashMap中,然后执行get(),size(),isEmpty,containsKey(),remove()等操作。
结果如下:

Map: {SZX=深圳宝安机场, PDX=上海浦东机场, BJC=北京首都机场, GZB=广州白云机场}
Map Size:  4
Map is empty:  false
Map contains PDX key:   true
Usage:  深圳宝安机场
removed:  深圳宝安机场

我们翻一下JDK,看看HashMap的介绍。

public class HashMap
extends AbstractMap
implements Map, Cloneable, Serializable
Hash table 实现了Map 接口,允许null values and the null key,不是同步的。这个类不保证数据的次序,特别地,也不保证数据次序的恒定,也就是说,第一次查找的时候是这个次序,下一次可能就变了。
HashMap有两个参数影响性能: initial capacity and load factor. The capacity是bucket桶的数量,默认值是16,load factor是hash表多满后自动扩容,0.75是默认值。每次扩容是增加一倍容量,扩容可以很耗时间。对capacity和load factor,要有一个平衡,合理兼顾空间占用和时间消耗。
HashMap不是同步的,如果要同步需要在外面自己实现,或者用Map m = Collections.synchronizedMap(new HashMap(…));转换成同步的。
跟Collection一样,多线程的情况下,如果一个 iterator遍历中间HashMap有结构性变化,就会fail-fast,抛出 ConcurrentModificationException。

看看HashMap的构造函数:

HashMap()
Constructs an empty HashMap with the default initial capacity (16) and the default load factor (0.75).
HashMap(int initialCapacity)
Constructs an empty HashMap with the specified initial capacity and the default load factor (0.75).
HashMap(int initialCapacity, float loadFactor)
Constructs an empty HashMap with the specified initial capacity and load factor.
HashMap(Map<? extends K,? extends V> m)
Constructs a new HashMap with the same mappings as the specified Map.

对HashMap里面的方法不一一举例了,我们看一个小例子,代码如下(HashMapTest2.java):

public class HashMapTest2 {
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>();
        map.put("BJC", "北京首都机场");
        map.put("PDX", "上海虹桥机场");
        map.put("GZB", "广州白云机场");
        map.put("SZX", "深圳宝安机场");

        Set<String> keys  = map.keySet();
        keys.forEach(System.out::println);

        for (String key : map.keySet()) {
            System.out.println("value=" +map.get(key));
        }

        Set<Map.Entry<String, String>> entries = map.entrySet();
        entries.forEach((Map.Entry<String, String> entry) -> {
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println("key=" + key + ",  value=" + value);
        });

        map.replace("PDX", "上海浦东机场");

        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, String> entry = iterator.next();
            System.out.println("key=" + entry.getKey() + ",  value=" + entry.getValue());
            map.merge(entry.getKey(), "有限公司", (oldVal, newVal) -> oldVal + newVal);
            map.compute(entry.getKey(), (key, oldVal) -> oldVal + "有限公司");
        }

        List<String> valuesList = new ArrayList<String>(map.values());
        for(String str:valuesList){
            System.out.println(str);
        }
    }
}

程序简单。大家熟悉一下几种遍历方式,还有merge(), compute(),和map.values()。

有了这些基础,接下来我们要讲更多的东西,帮助大家更好地理解HashMap。我们先看看数据结构中介绍的一点理论知识。
简单来讲,HashMap底下用的数据结构是数组+链表(红黑树)。Key值通过一个hash函数映射到数组的下标,重复的下标通过链表(红黑树)解决冲突。术语中把此处的数组叫做bucket桶。
有一个图,很形象地说明了HashMap的结构。

table是一个数组,数组每个位置(就是每一个桶)保存一个元素,或者是跟着一个链表或者红黑树(开头都是链表,数据量>8之后,就自动转成红黑树)。查找数据先定位在数组哪个位置,再顺藤摸瓜找到在链表或者红黑树上的哪一个具体节点。
我们知道,查找数据来说,其实数组是最快的,因为可以根据下标直接定位。所以哈希的核心思路是用一个函数将查找的key值转换成一个整数值,然后以此为下标,把key值存放在数组中。这样下次再找的时候,还用这个函数,直接定位了。所以定位数组下标,性能是o(1),如果定位的这个数组后面跟了一个链表,要接着找具体的节点,性能是o(l),其中l是链表长度。自然,链表长度越短越好,意味着需要这个hash函数冲突越少越好。所以,HashMap的性能关键在于要找到一个合适的函数。
要写出一个像样子的哈希函数,在《Effective Java》这本书中,Joshua Bloch给了一个指导:
1 给int变量result赋予一个非零值常量,如17
2 为对象内每个有意义的域f(即每个可以做equals()操作的域)计算出一个int散列码c:

域类型                                计算
boolean                                c=(f?0:1)
byte、char、short或int                c=(int)f
long                                    c=(int)(f^(f>>>32))
float                                    c=Float.floatToIntBits(f);
double                                long l = Double.doubleToLongBits(f);
                                    c=(int)(l^(l>>>32))
Object,其equals()调用这个域的equals()    c=f.hashCode()
数组                                对每个元素应用上述规则
3. 合并计算散列码:result = 37 * result + c; 
4. 返回result。 
5. 检查hashCode()最后生成的结果,确保相同的对象有相同的散列码。

《Thinking in Java》里面给了一个简单的例子,我拷贝到这里,版权属于Bruce Eckel。代码如下(CountedString.java):

public class CountedString {
  private static List<String> created = new ArrayList<String>();
  private String s;
  private int id = 0;
  public CountedString(String str) {
    s = str;
    created.add(s);
    // id is the total number of instances
    // of this string in use by CountedString:
    for(String s2 : created)
      if(s2.equals(s))
        id++;
  }
  public String toString() {
    return "String: " + s + " id: " + id + " hashCode(): " + hashCode();
  }
  public int hashCode() {
    // The very simple approach:
    // return s.hashCode() * id;
    // Using Joshua Bloch's recipe:
    int result = 17;
    result = 37 * result + s.hashCode();
    result = 37 * result + id;
    return result;
  }
  public boolean equals(Object o) {
    return o instanceof CountedString &&
      s.equals(((CountedString)o).s) &&
      id == ((CountedString)o).id;
  }
  public static void main(String[] args) {
    Map<CountedString,Integer> map = new HashMap<CountedString,Integer>();
    CountedString[] cs = new CountedString[5];
    for(int i = 0; i < cs.length; i++) {
      cs[i] = new CountedString("hi");
      map.put(cs[i], i); // Autobox int -> Integer
    }
    System.out.println(map);
    for(CountedString cstring : cs) {
        System.out.println("Looking up " + cstring);
        System.out.println(map.get(cstring));
    }
  }
} 

运行结果如下:

{String: hi id: 4 hashCode(): 146450=3, String: hi id: 5 hashCode(): 146451=4, String: hi id: 2 hashCode(): 146448=1, String: hi id: 3 hashCode(): 146449=2, String: hi id: 1 hashCode(): 146447=0}
Looking up String: hi id: 1 hashCode(): 146447
0
Looking up String: hi id: 2 hashCode(): 146448
1
Looking up String: hi id: 3 hashCode(): 146449
2
Looking up String: hi id: 4 hashCode(): 146450
3
Looking up String: hi id: 5 hashCode(): 146451
4

大家可以看出对给定的key值生成的不一样的hashcode。

讲完了HashMap,我再简单介绍一下Set。大家或许觉得奇怪,Set不是Collection里面的一员吗?没什么不放在更前面谈?我这么讲是因为Set底层是基于Map实现的,所以讲授放在哪一边都是可以的。说白了,Set是Map的一层马甲。Set不能有重复数据。
Set是实现Collection接口的,除了Collection的常规操作,还有一些与集合相关的操作,并,交,补等等。
看一个简单的例子,代码如下(HashSetTest.java):

public class HashSetTest {
    public static void main(String[] args) {
        Set<String> s1 = new HashSet<>();
        s1.add("北京首都机场");
        s1.add("上海虹桥机场");
        s1.add("广州白云机场");
        s1.add("深圳宝安机场");
        s1.add("上海虹桥机场");

        Set<String> s2 = new HashSet<>();
        s2.add("上海虹桥机场");
        s2.add("长沙黄花机场");
        s2.add("杭州萧山机场");

        for(String s : s1) {
            System.out.print(s+" ");
        }
        System.out.println("");
        Iterator iterator = s2.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next()+" ");          
        }
        System.out.println("");

        doUnion(s1, s2);
        doIntersection(s1, s2);
        doDifference(s1, s2);
        isSubset(s1, s2);
    }

    public static void doUnion(Set<String> s1, Set<String> s2) {
        Set<String> s1Unions2 = new HashSet<>(s1);
        s1Unions2.addAll(s2);
        System.out.println("s1 union  s2: " + s1Unions2);
    }

    public static void doIntersection(Set<String> s1, Set<String> s2) {
        Set<String> s1Intersections2 = new HashSet<>(s1);
        s1Intersections2.retainAll(s2);
        System.out.println("s1 intersection  s2: " + s1Intersections2);
    }

    public static void doDifference(Set<String> s1, Set<String> s2) {
        Set<String> s1Differences2 = new HashSet<>(s1);
        s1Differences2.removeAll(s2);

        Set<String> s2Differences1 = new HashSet<>(s2);
        s2Differences1.removeAll(s1);

        System.out.println("s1 difference s2: " + s1Differences2);
        System.out.println("s2 difference s1: " + s2Differences1);
    }

    public static void isSubset(Set<String> s1, Set<String> s2) {
        System.out.println("s2 is  subset s1: " + s1.containsAll(s2));
        System.out.println("s1 is  subset s2: " + s2.containsAll(s1));
    }
}

简单,不解释了。大家只要注意s1.add("上海虹桥机场");执行了两遍,但是最后Set里面只有一个。因为判断这是同一个对象。
这儿要多提一下,世界上没有两片完全一样的树叶,两个字符串,怎么会认为是同一个呢?这是因为判断是否为同一个采用的方法是调用equals()方法。所以对自定义的类,需要重新写equals()方法,否则就是直接用的Object自带的equals()方法,那是比较的引用地址,肯定就不同了,而我们需要比较的是对象里面的内容。
看一个例子。以前讲过,再试一遍。
先写一个自定义类Student:

public class Student {
    int id = 0;
    String name = "";
    String mark = "";

    public Student() {
    }
    public Student(int id, String name, String mark) {
        this.id = id;
        this.name = name;
        this.mark = mark;
    }

    public void setId(int id) {
        this.id = id;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setMark(String mark) {
        this.mark = mark;
    }   
    public String toString() {
        return name + "-" + mark;
    }
}

再用一个测试程序看看,代码如下(HashSetTest2.java):

public class HashSetTest2 {
    public static void main(String[] args) {
        Set<Student> s1 = new HashSet<>();
        s1.add(new Student(1,"a","aaa"));
        s1.add(new Student(2,"b","bbb"));
        s1.add(new Student(3,"c","ccc"));
        s1.add(new Student(1,"a","aaa"));

        System.out.print(s1);
    }
}

运行结果如下:

[c-ccc, a-aaa, b-bbb, a-aaa]
注意了,aaa添加了两遍,在Set中也有两份。这不是重复了吗?造成这种情况的原因就是重复不重复,是看的equals()。因此,我们必须改写equals(),Student程序增加一个方法如下:
    public boolean equals (Object obj){
        if(this==obj){
            return true;
        }
        if(!(obj instanceof Student)){
            return false;
        } 
       Student s=(Student) obj;
        if(this.id==s.id&&this.name.equals(s.name)&&this.mark.equals(s.mark)) {
            return true;
        }
        return false;
    }

我们重写equals()覆盖Object默认的方法,比较对象内部的内容。
再次运行,结果是:
[c-ccc, a-aaa, b-bbb, a-aaa]
没有变化!这是怎么回事呢?我们回顾一下HashMap的查找方法,第一步是比较hashcode,相同的话,就在同一个bucket桶里找相同的元素,这个时候才会调用在equals()。我们的程序,在hashcode这一层就被挡住了,不会调用equals(),所以,我们对Student类,还需要重写hashCode(),Student程序修改一下,增加hashCode():

public int hashCode(){
    return this.name.hashCode();
}

再运行,就出现了我们想要的结果。同时也印证了我们的说法,Set其实是基于Map的。JDK说明中,对HashSet的第一句话就是:This class implements the Set interface, backed by a hash table (actually a HashMap instance)。

好,到此为止,我们就把几个基本的类介绍过了,ArrayList,LinkedList,HashMap,HashSet。普通应用主要用它们几个,一般也认为容器类是一门实用的语言最重要的类。大家要好好掌握这些基本的使用方法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值