LinkedList它实现的基础是双向链表,因此在插入删除方面具有性能优势,它也可以用来实现stack和queue。顺便说一句,Java容器框架中有一个遗留的类Stack,它是基于Vector实现的,被大师们评价为“幼稚的设计”,我们不要用。
LinkedList主要有三个属性:
int size
Node first
Node 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 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 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 list1 = new ArrayList<>();
for (int i=0; ii1-i2);
end=System.currentTimeMillis();
System.out.println(list1);
System.out.println(end-start);
Random r = new Random();
List list2 = new ArrayList<>();
for (int i=0; ii1-i2);
end=System.currentTimeMillis();
System.out.println(list2);
System.out.println(end-start);
List 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 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 map = new HashMap<>();
map.put("BJC", "北京首都机场");
map.put("PDX", "上海虹桥机场");
map.put("GZB", "广州白云机场");
map.put("SZX", "深圳宝安机场");
Set keys = map.keySet();
keys.forEach(System.out::println);
for (String key : map.keySet()) {
System.out.println("value=" +map.get(key));
}
Set> entries = map.entrySet();
entries.forEach((Map.Entry entry) -> {
String key = entry.getKey();
String value = entry.getValue();
System.out.println("key=" + key + ", value=" + value);
});
map.replace("PDX", "上海浦东机场");
Iterator> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry 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 valuesList = new ArrayList(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 created = new ArrayList();
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 map = new HashMap();
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 s1 = new HashSet<>();
s1.add("北京首都机场");
s1.add("上海虹桥机场");
s1.add("广州白云机场");
s1.add("深圳宝安机场");
s1.add("上海虹桥机场");
Set 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 s1, Set s2) {
Set s1Unions2 = new HashSet<>(s1);
s1Unions2.addAll(s2);
System.out.println("s1 union s2: " + s1Unions2);
}
public static void doIntersection(Set s1, Set s2) {
Set s1Intersections2 = new HashSet<>(s1);
s1Intersections2.retainAll(s2);
System.out.println("s1 intersection s2: " + s1Intersections2);
}
public static void doDifference(Set s1, Set s2) {
Set s1Differences2 = new HashSet<>(s1);
s1Differences2.removeAll(s2);
Set 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 s1, Set 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 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。普通应用主要用它们几个,一般也认为容器类是一门实用的语言最重要的类。大家要好好掌握这些基本的使用方法。