这一章我们讨论集合。
这个名词不仅仅出现在计算机中,在数学中也有。但数学中的集合和计算机中的集合,除了都是"一堆东西在一起"外,没有任何的关系。
(类似的还有计算机中的二叉树和金融中的二叉树,除了都有两个叉之外,没有任何关系。)
集合
在《3.最常用的Java自带的类》 ,我们讨论了ArrayList,当时我们说:ArrayList是集合的一种,集合是指一种存储空间可变的存储模型,存储的数据容量可以发生改变。
但当时我们只讨论了这些,现在我做更详细的讨论。
首先,是集合的体系结构。
集合可以分为两类,分别是单列和双列、单列又有List和Set两种。
注意,Collection、Map、List和Set都是接口。
我们一个一个讨论。
Collection
概述
Collection是单列集合的顶层接口,JDK不提供此接口的任何直接实现,但为该接口的子接口(Set和List)提供了具体实现。
我们举一个例子,来说明Collection的基本使用。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.util.ArrayList;import java.util.Collection;public class M { public static void main (String[] args) { Collection<String> c = new ArrayList<String>(); c.add("hello" ); c.add("world" ); c.add("java" ); System.out.println(c); } }
运行结果:
这是一种典型的向上转型的多态,从子到父,父类引用指向子类对象。
常用方法
Collection的常用方法有:
方法名
说明
boolean add(E e)
添加元素
boolean remove(Object o)
从集合中移除指定的元素
void clear()
清空集合中的元素
boolean contains(Object o)
判断集合中是否存在指定的元素
boolean isEmpty()
判断集合是否为空
int size()
集合的长度,也就是集合中元素的个数
注意,没有set方法和get方法。没有不是因为不常用,是因为本身就没有。
迭代器遍历
在上一章《3.最常用的Java自带的类》 讨论ArrayList的时候,我们说ArrayList有size方法有set和get方法,那么就可以做遍历了。
但是在这里没有set方法和get方法,可以做遍历呢?
也可以。
迭代器
迭代器,集合的专有的遍历方式。
方法如下:
1 Iterator<E> iterator = 集合对象.iterator()
我们来看个例子。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.kakawanyifan;import java.util.ArrayList;import java.util.Collection;import java.util.Iterator;public class IteratorDemo { public static void main (String[] args) { Collection<String> c = new ArrayList<String>(); c.add("hello" ); c.add("world" ); c.add("java" ); Iterator<String> it = c.iterator(); while (it.hasNext()) { String s = it.next(); System.out.println(s); } } }
运行结果:
迭代器源码
it.next()
运行一次就输出下次的结果,就像有指针一直记录着已经next多少记录了一样。
实际上,还真是有指针在记。
我们来看源码。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private class Itr implements Iterator <E > { int cursor; 【部分代码略】 Itr() {} public boolean hasNext () { return cursor != size; } @SuppressWarnings ("unchecked" ) public E next () { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this .elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1 ; return (E) elementData[lastRet = i]; } 【部分代码略】 }
每调用一次next方法,cursor变量都会加一。
题外话 类似效果的函数,还有Python中读取文件的readline
。
示例代码:
1 2 3 4 5 6 7 8 with open(fileName, 'r' ) as fr: while True : line = fr.readline() if not line: break
List
Collection接口有两个子接口,List和Set。我们先讨论List。
概述
List是有序集合,用户可以精确控制列表中每个元素的插入位置,可以通过整数索引访问元素。
(与Set集合不同,List中允许重复的元素。)
特有方法
List继承自Collection,除了Collection有的方法,List中还有一些特有的方法。
方法名
描述
void add(int index,E element)
在此集合中的指定位置插入指定的元素。
E remove(int index)
删除指定索引处的元素,返回被删除的元素。
E set(int index,E element)
修改指定索引处的元素,返回被修改的元素。
E get(int index)
返回指定索引处的元素。
并发修改异常
接下来。我们来看一个很有意思的现象。
并发修改异常
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.kakawanyifan;import java.util.ArrayList;import java.util.Iterator;import java.util.List;public class IteratorDemo { public static void main (String[] args) { List<String> list = new ArrayList<String>(); list.add("hello" ); list.add("world" ); list.add("java" ); Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); System.out.println(s); if (s.equals("world" )) { list.add("javaee" ); } } } }
运行结果:
1 2 3 4 5 6 hello world Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911) at java.util.ArrayList$Itr.next(ArrayList.java:861) at com.kakawanyifan.IteratorDemo.main(IteratorDemo.java:20)
出现异常了。
ArrayList源码分析
我们根据at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
,点击ArrayList.java:911
,找到这么一段代码。
示例代码:
1 2 3 4 final void checkForComodification () { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
如果modCount
和expectedModCount
不相等,就抛出异常。
checkForComodification方法属于ArrayList的一个内部类Itr,在这个Itr中,就定义了int expectedModCount = modCount;
。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 private class Itr implements Iterator <E > { int cursor; int lastRet = -1 ; int expectedModCount = modCount; Itr() {} public boolean hasNext () { return cursor != size; } @SuppressWarnings ("unchecked" ) public E next () { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this .elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1 ; return (E) elementData[lastRet = i]; } public void remove () { 【部分代码略】 } @Override @SuppressWarnings ("unchecked" ) public void forEachRemaining (Consumer<? super E> consumer) { 【部分代码略】 } final void checkForComodification () { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
有很多方法都会调用checkForComodification这个方法,但是在这里调用checkForComodification这个方法的就是next方法。next的第一行就是checkForComodification();
,这也印证了报错的第二行at java.util.ArrayList$Itr.next(ArrayList.java:861)
。
expectedModCount
属于Itr这个内部类的成员变量,如果代码规范的话,不应该有方法去直接修改内部类的expectedModCount
,所以比较有可能是modCount
被修改了。或许是add方法的时候,修改了modCount
?
右键modCount
,Go to,Declaration or Usage,找到modCount
的定义,是在AbstractList
这个类中被定义的,而且是AbstractList
这个类的成员变量。而ArrayList继承了这个类。
来看看add方法。
1 2 3 4 5 public boolean add (E e) { ensureCapacityInternal(size + 1 ); elementData[size++] = e; return true ; }
再来看看被add方法调用的ensureCapacityInternal方法。
1 2 3 private void ensureCapacityInternal (int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
再来看看ensureExplicitCapacity方法。
1 2 3 4 5 6 7 private void ensureExplicitCapacity (int minCapacity) { modCount++; if (minCapacity - elementData.length > 0 ) grow(minCapacity); }
找到原因了!modCount++;
!
小结一下,ArrayList的部分源码结构如图所示。
并发修改方案
那么,怎么解决呢?
用for循环遍历,然后用集合对象做对应的操作。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.kakawanyifan;import java.util.ArrayList;import java.util.List;public class ForDemo { public static void main (String[] args) { List<String> list = new ArrayList<String>(); list.add("hello" ); list.add("world" ); list.add("java" ); for (int i = 0 ; i < list.size(); i++) { String s = list.get(i); System.out.println(s); if (s.equals("world" )) { list.add("javaee" ); } } System.out.println(list); } }
运行结果:
1 2 3 4 5 hello world java javaee [hello, world, java, javaee]
列表迭代器
在ArrayList中还有一个内部类:ListIterator
。该内部类通过listIterator
方法实例化。其特点是可以沿着next和previous两个方向进行遍历,并且提供了add和set方法。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.kakawanyifan;import java.util.ArrayList;import java.util.List;import java.util.ListIterator;public class ListIteratorDemo { public static void main (String[] args) { List<String> list = new ArrayList<String>(); list.add("hello" ); list.add("world" ); list.add("java" ); ListIterator<String> lit = list.listIterator(); while (lit.hasNext()) { String s = lit.next(); System.out.println(s); if (s.equals("world" )) { lit.add("javaee" ); } } System.out.println("------" ); while (lit.hasPrevious()) { String s = lit.previous(); System.out.println(s); } System.out.println(list); } }
运行结果:
1 2 3 4 5 6 7 8 9 hello world java ------ java javaee world hello [hello, world, javaee, java]
特别注意,这时候并发修改并没有引起异常。我们来看看add方法都做了什么。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 public void add (E e) { checkForComodification(); try { int i = cursor; ArrayList.this .add(i, e); cursor = i + 1 ; lastRet = -1 ; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
注意:expectedModCount = modCount
增强for循环
还有一种for循环,这种for循环相比之前的for循环更方便。
格式如下:
1 2 3 for(元素数据类型 变量名 : 数组/集合对象名) { 循环体; }
但是,相比之前的for循环,也有不足。我们来看例子。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.kakawanyifan;import java.util.ArrayList;import java.util.List;public class ForDemo { public static void main (String[] args) { List<String> list = new ArrayList<String>(); list.add("hello" ); list.add("world" ); list.add("java" ); for (String s : list) { System.out.println(s); if (s.equals("world" )) { list.add("javaee" ); } } System.out.println(list); } }
运行结果:
1 2 3 4 5 6 hello world Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911) at java.util.ArrayList$Itr.next(ArrayList.java:861) at com.kakawanyifan.ForDemo.main(ForDemo.java:16)
原因是,这种For循环的内部原理是一个Iterator迭代器。所以,同样有并发修改异常。
LinkedList的特有方法
除了ArrayList,还有一种是LinkeList。顾名思义,基于链表实现的。
关于数组和链表,我们在《算法入门经典(Java与Python描述):2.数组、链表》 有非常详细的讨论,这里不赘述。
数组:查询快、增删慢
链表:查询慢、增删快
ArrayList:基于数组实现,查询快、增删慢
LinkedList:基于链表实现,查询慢、增删快
LinkedList有一些特有的方法。
方法名
说明
public void addFirst(E e)
在该列表开头插入指定的元素
public void addLast(E e)
将指定的元素追加到此列表的末尾
public E getFirst()
返回此列表中的第一个元素
public E getLast()
返回此列表中的最后一个元素
public E removeFirst()
从此列表中删除并返回第一个元素
public E removeLast()
从此列表中删除并返回最后一个元素
等等!
刚刚说了,LinkedList,基于链表实现,怎么LinkedList还专门提供了针对last的操作?链表,last,那我不得一个一个指过去?这还能快吗?
LinkedList源码分析
因为LinkedList有两个成员变量first和last,在add和remove等操作的时候,都会维护这两个成员变量。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public class LinkedList <E > extends AbstractSequentialList <E > implements List <E >, Deque <E >, Cloneable , java .io .Serializable { transient Node<E> first; transient Node<E> last; public boolean add (E e) { linkLast(e); return true ; } void linkLast (E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null ); last = newNode; if (l == null ) first = newNode; else l.next = newNode; size++; modCount++; } public boolean remove (Object o) { if (o == null ) { for (Node<E> x = first; x != null ; x = x.next) { if (x.item == null ) { unlink(x); return true ; } } } else { for (Node<E> x = first; x != null ; x = x.next) { if (o.equals(x.item)) { unlink(x); return true ; } } } return false ; } E unlink (Node<E> x) { final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null ) { first = next; } else { prev.next = next; x.prev = null ; } if (next == null ) { last = prev; } else { next.prev = prev; x.next = null ; } x.item = null ; size--; modCount++; return element; }
该示例代码来自JDK的源码,在入选本博客的时候,有删改。
Set
Set的特点
第二种单列的Collection是Set。
其特点是:
元素存取无序
没有索引、只能通过迭代器或增强for循环遍历
不能存储重复元素
我们来看个例子。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.kakawanyifan;import java.util.HashSet;import java.util.Set;public class SetDemo { public static void main (String[] args) { Set<String> set = new HashSet<String>(); set.add("hello" ); set.add("world" ); set.add("java" ); set.add("world" ); for (String s : set) { System.out.println(s); } System.out.println(set); } }
运行结果:
1 2 3 4 world java hello [world, java, hello]
执行了两次set.add("world");
,但只记录一条world
。
哈希值
那么HashSet是如何做到数据唯一的呢?这就得从哈希值开始说起了。
哈希值是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。
在Object类中有一个方法public int hashCode()
,该方法返回对象的哈希值。
哈希值的特点:
同一个对象多次调用hashCode()方法返回的哈希值是相同的。
默认情况下,不同对象的哈希值是不同的。
如果重写hashCode()方法,可以实现让不同对象的哈希值相同。
比如在下面的例子中,"通话"和"重地"拥有相同的哈希值,是因为String类重写了hashCode方法。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.kakawanyifan;public class Student { private String name; private int age; public Student (String name,int age) { this .name = name; this .age = age; } public void setName (String name) { this .name = name; } public String getName () { return name; } public void setAge (int age) { this .age = age; } public int getAge () { return age; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.kakawanyifan;public class HashDemo { public static void main (String[] args) { Student s1 = new Student("林青霞" ,30 ); System.out.println(s1.hashCode()); System.out.println(s1.hashCode()); System.out.println("--------" ); Student s2 = new Student("林青霞" ,30 ); System.out.println(s2.hashCode()); System.out.println("--------" ); System.out.println("hello" .hashCode()); System.out.println("world" .hashCode()); System.out.println("java" .hashCode()); System.out.println("world" .hashCode()); System.out.println("--------" ); System.out.println("重地" .hashCode()); System.out.println("通话" .hashCode()); System.out.println("通话" .equals("重地" )); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 460141958 460141958 -------- 1163157884 -------- 99162322 113318802 3254818 113318802 -------- 1179395 1179395 false
注意"重地"和"通话",拥有相同的哈希值。
但是"重地".equals("通话")
,返回的是false
。我们可以看String类源码的equals方法,并没有利用哈希值,这个我们在上一章《3.最常用的Java自带的类》 提到过。
HashSet源码分析
那么,HashSet是怎么保证元素唯一性的呢?
利用哈希值?哈希值一样的就认为是一样的元素?
来,看个反例。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import java.util.HashSet;import java.util.Set;public class SetDemo { public static void main (String[] args) { String s1 = "重地" ; String s2 = "通话" ; System.out.println(s1.hashCode()); System.out.println(s2.hashCode()); Set<String> set = new HashSet<>(); set.add(s1); set.add(s2); System.out.println(set); } }
运行结果:
1 2 3 1179395 1179395 [重地, 通话]
居然"重地"和"通话"都被加进去了?
难道和String的equals方法一样?就没有利用哈希值?
没有利用哈希值,那叫什么"HashSet"?
利用的哈希值,但是对于"重地"和"通话",做了if-else的特殊处理?
不会吧,这么糟糕的代码?
不要猜了!直接看源码!
我们找到HashSet的add方法的源码。
1 2 3 public boolean add (E e) { return map.put(e, PRESENT)==null ; }
发现数据是通过map.put(e, PRESENT)
添加的,再来看看map.put(e, PRESENT)
的源码。
1 2 3 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
发现数据是通过putVal(hash(key), key, value, false, true)
添加的。此外,还有一个hash(key)
方法。我们来看看两个方法的源码
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
到这里,其实可以解答我们的疑惑了。利用了哈希值,但不是在哈希值的基础上还做了操作。
最后,再看一下putVal()
方法都做了啥。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0 ) n = (tab = resize()).length; if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null ); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this , tab, hash, key, value); else { for (int binCount = 0 ; ; ++binCount) { if ((e = p.next) == null ) { p.next = newNode(hash, key, value, null ); if (binCount >= TREEIFY_THRESHOLD - 1 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
该部分的代码,其逻辑结构如图。
LinkedHashSet的概述和特点
除了HashSet,还有LinkedHashSet。
LinkedHashSet的特点:
哈希表和链表实现的Set接口。
元素的存储和取出顺序是一致的。
元素唯一,没有重复的元素。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.kakawanyifan;import java.util.LinkedHashSet;public class LinkedHashSetDemo { public static void main (String[] args) { LinkedHashSet<String> linkedHashSet = new LinkedHashSet<String>(); linkedHashSet.add("hello" ); linkedHashSet.add("world" ); linkedHashSet.add("java" ); linkedHashSet.add("world" ); for (String s : linkedHashSet) { System.out.println(s); } } }
运行结果:
TreeSet
TreeSet的特点
在《算法入门经典(Java与Python描述):7.哈希表》 中,我们讨论过TreeMap,特点是所有的key都是排序好的。
与之对应的有TreeSet,特点是:
元素有序,此外可以通过构造方法自定义排序。
构造方法一 TreeSet():根据其元素的自然排序进行排序
构造方法二 TreeSet(Comparator comparator) :根据指定的比较器进行排序
没有带索引的方法,所以不能使用普通for循环遍历
不包含重复元素的集合
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.util.TreeSet;public class M { public static void main (String[] args) { TreeSet<Integer> ts = new TreeSet<Integer>(); ts.add(10 ); ts.add(40 ); ts.add(30 ); ts.add(50 ); ts.add(20 ); ts.add(30 ); for (Integer i : ts) { System.out.println(i); } } }
运行结果:
接下来,我们利用TreeSet的特点,来做一件事情。
按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
自然排序
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.kakawanyifan;public class Student { private String name; private int age; public Student (String name, int age) { this .name = name; this .age = age; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.kakawanyifan;import java.util.TreeSet;public class TreeSetDemo { public static void main (String[] args) { TreeSet<Student> ts = new TreeSet<Student>(); Student s1 = new Student("x" , 29 ); Student s2 = new Student("y" , 28 ); Student s3 = new Student("z" , 30 ); Student s4 = new Student("za" , 33 ); Student s5 = new Student("a" , 33 ); Student s6 = new Student("a" , 33 ); ts.add(s1); ts.add(s2); ts.add(s3); ts.add(s4); ts.add(s5); ts.add(s6); for (Student s : ts) { System.out.println(s.getName() + "," + s.getAge()); } } }
运行结果:
1 2 3 4 5 Exception in thread "main" java.lang.ClassCastException: com.kakawanyifan.Student cannot be cast to java.lang.Comparable at java.util.TreeMap.compare(TreeMap.java:1294) at java.util.TreeMap.put(TreeMap.java:538) at java.util.TreeSet.add(TreeSet.java:255) at com.kakawanyifan.TreeSetDemo.main(TreeSetDemo.java:20)
居然报错了?
因为没有实现Comparable接口。
那我们就实现一下Comparable的接口。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.kakawanyifan;public class Student implements Comparable <Student > { private String name; private int age; public Student (String name, int age) { this .name = name; this .age = age; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } @Override public int compareTo (Student o) { return 0 ; } }
运行结果:
compareTo(Student o)
这个方法默认return 0
,我们不修改,试一下。居然之输出了一个?
因为0
代表元素是相等的,所以不会添加。
如果是1
呢?
1 2 3 4 5 6 x,29 y,28 z,30 za,33 a,33 a,33
和存储顺序一样?
s1存储不需要比,s2和s1比,返回的是正数,s2比s1大,所以s2在s1后面,所以s3在s2后面。
同理如果是-1
1 2 3 4 5 6 a,33 a,33 za,33 z,30 y,28 x,29
小结一下,A.compareTo(B)
所以呢,为了实现目标,我们修改compareTo方法如下即可
示例代码:
1 2 3 4 5 6 @Override public int compareTo (Student o) { int num = this .getAge() - o.getAge(); int num2 = num == 0 ? this .getName().compareTo(o.getName()) : num; return num2; }
运行结果:
1 2 3 4 5 y,28 x,29 z,30 a,33 za,33
String类中已经有了compareTo方法,直接利用即可。
比较器排序
关于比较器,我们在《算法入门经典(Java与Python描述):9.堆》 也有过讨论,这里我们复习一下。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.kakawanyifan;import java.util.Comparator;import java.util.TreeSet;public class TreeSetDemo { public static void main (String[] args) { TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() { @Override public int compare (Student s1, Student s2) { int num = s1.getAge() - s2.getAge(); int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num; return num2; } }); Student s1 = new Student("x" , 29 ); Student s2 = new Student("y" , 28 ); Student s3 = new Student("z" , 30 ); Student s4 = new Student("za" , 33 ); Student s5 = new Student("a" , 33 ); Student s6 = new Student("a" , 33 ); ts.add(s1); ts.add(s2); ts.add(s3); ts.add(s4); ts.add(s5); ts.add(s6); for (Student s : ts) { System.out.println(s.getName() + "," + s.getAge()); } } }
运行结果:
1 2 3 4 5 y,28 x,29 z,30 a,33 za,33
拼音排序
拼音排序有两种方法:
基于java.util.Locale.CHINA
,该方法存在准确率的问题,主要是对于多音字不够准确。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.kakawanyifan;import java.text.Collator;import java.util.Arrays;import java.util.Locale;public class main { public static void main (String[] args) { Collator cmp = Collator.getInstance(Locale.CHINA); String[] arr = { "张三" , "李四" , "王五" }; Arrays.sort(arr, cmp); System.out.println(Arrays.toString(arr)); String[] arr2 = { "上海" , "重庆" }; Arrays.sort(arr2, cmp); System.out.println(Arrays.toString(arr2)); } }
运行结果:
基于第三方的com.ibm.icu.util.ULocale.SIMPLIFIED_CHINESE
,该方法更准确。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.kakawanyifan;import com.ibm.icu.text.Collator;import com.ibm.icu.util.ULocale;import java.util.Arrays;public class main { public static void main (String[] args) { Collator cmp = Collator.getInstance(ULocale.SIMPLIFIED_CHINESE); String[] arr = { "张三" , "李四" , "王五" }; Arrays.sort(arr, cmp); System.out.println(Arrays.toString(arr)); String[] arr2 = { "上海" , "重庆" }; Arrays.sort(arr2, cmp); System.out.println(Arrays.toString(arr2)); } }
运行结果:
附,方法二的包:
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > com.ibm.icu</groupId > <artifactId > icu4j</artifactId > <version > 71.1</version > </dependency > </dependencies >
元素的内容被改变,不会重排
其实希望元素内容被改变还重排,更像我们的一厢情愿。
元素内容被改变,还要重排?
这个TreeSet的确不好做,如果一定要做,有两个思路。
元素内容被改变了,去触发TreeSet重排
TreeSet有一个很巧妙的办法主动监控
同时,还需要考虑性能问题,以及安全问题。(比如,重排期间被读取。)
总之,不好做,是我们的一厢情愿。
现象
假设存在一批数据,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 [{ "cnt" : 1 , "name" : "标签一" , }, { "cnt" : 2 , "name" : "标签二" }, { "cnt" : 3 , "name" : "标签三" }, { "cnt" : 4 , "name" : "标签一" }]
现在需要对这一批数据进行合并,相同name
的合为一个,其cnt
进行相加。并按照cnt从大到小进行排序。
即,如下形式:
1 2 3 4 5 6 7 8 9 10 [{ "cnt" : 5 , "name" : "标签一" , }, { "cnt" : 3 , "name" : "标签三" }, { "cnt" : 2 , "name" : "标签二" }]
因为需要从大到小排序,这里考虑TreeSet,这个自带排序。
但是在TreeSet中如果元素内容被改变,不会重排。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.kakawanyifan;public class Label implements Comparable <Label > { private String name; private Integer cnt; public Label (String name, Integer cnt) { super (); this .name = name; this .cnt = cnt; } public String getName () { return name; } public void setName (String name) { this .name = name; } public Integer getCnt () { return cnt; } public void setCnt (Integer cnt) { this .cnt = cnt; } @Override public int compareTo (Label o) { return o.cnt - cnt; } @Override public String toString () { return "Label [name=" + name + ", cnt=" + cnt + "]" ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.kakawanyifan;import java.util.Set;import java.util.TreeSet;import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;public class TreeSetDemo { public static void main (String[] args) { Set<Label> labelSet = new TreeSet<Label>(); String input = "[{\"cnt\":1,\"name\":\"标签一\",},{\"cnt\":2,\"name\":\"标签二\"},{\"cnt\":3,\"name\":\"标签三\"},{\"cnt\":4,\"name\":\"标签一\"}]" ; JSONArray ja = JSONArray.parseArray(input); for (int i = 0 ; i < ja.size(); i++) { JSONObject jo = ja.getJSONObject(i); String name = jo.getString("name" ); Integer cnt = jo.getInteger("cnt" ); boolean contain = false ; Label conLabel = null ; for (Label tempLabel : labelSet) { if (tempLabel.getName().equals(name)) { conLabel = tempLabel; contain = true ; break ; } } if (contain) { conLabel.setCnt(conLabel.getCnt() + cnt); }else { Label newLabel = new Label(name, cnt); labelSet.add(newLabel); } } System.out.println(labelSet.toString()); } }
运行结果:
1 [Label [name=标签三, cnt=3], Label [name=标签二, cnt=2], Label [name=标签一, cnt=5]]
解释说明:
name
为标签一
的Label
被放在第三个位置,因为后期其cnt
被修改了,但是并不会重排。
解决
可以让其先去除再加入,以此触发重排。但是这样代码的确不雅。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 【部分代码略】 if (contain) { labelSet.remove(conLabel); conLabel.setCnt(conLabel.getCnt() + cnt); labelSet.add(conLabel); }else { Label newLabel = new Label(name, cnt); labelSet.add(newLabel); } 【部分代码略】
运行结果:
1 [Label [name=标签一, cnt=5], Label [name=标签三, cnt=3], Label [name=标签二, cnt=2]]
或者先用List,最后统一加入到Set中。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.kakawanyifan;import java.util.ArrayList;import java.util.List;import java.util.Set;import java.util.TreeSet;import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;public class TreeSetDemo { public static void main (String[] args) { Set<Label> labelSet = new TreeSet<Label>(); List<Label> labelList = new ArrayList<Label>(); String input = "[{\"cnt\":1,\"name\":\"标签一\",},{\"cnt\":2,\"name\":\"标签二\"},{\"cnt\":3,\"name\":\"标签三\"},{\"cnt\":4,\"name\":\"标签一\"}]" ; JSONArray ja = JSONArray.parseArray(input); for (int i = 0 ; i < ja.size(); i++) { JSONObject jo = ja.getJSONObject(i); String name = jo.getString("name" ); Integer cnt = jo.getInteger("cnt" ); boolean contain = false ; Label conLabel = null ; for (Label tempLabel : labelList) { if (tempLabel.getName().equals(name)) { conLabel = tempLabel; contain = true ; break ; } } if (contain) { conLabel.setCnt(conLabel.getCnt() + cnt); }else { Label newLabel = new Label(name, cnt); labelList.add(newLabel); } } for (Label label:labelList) { labelSet.add(label); } System.out.println(labelSet.toString()); } }
运行结果:
1 [Label [name=标签一, cnt=5], Label [name=标签三, cnt=3], Label [name=标签二, cnt=2]]
compareTo方法的注意
现象
我们修改一下输入,添加两个{"cnt": 1,"name": "标签三"}
,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [{ "cnt" : 1 , "name" : "标签一" , }, { "cnt" : 2 , "name" : "标签二" }, { "cnt" : 3 , "name" : "标签三" }, { "cnt" : 4 , "name" : "标签一" }, { "cnt" : 1 , "name" : "标签三" }, { "cnt" : 1 , "name" : "标签三" }]
这时候"标签一"和"标签三"的cnt,都是5了。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.kakawanyifan;import java.util.ArrayList;import java.util.List;import java.util.Set;import java.util.TreeSet;import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;public class TreeSetDemo { public static void main (String[] args) { Set<Label> labelSet = new TreeSet<Label>(); List<Label> labelList = new ArrayList<Label>(); String input = "[{\"cnt\":1,\"name\":\"标签一\",},{\"cnt\":2,\"name\":\"标签二\"},{\"cnt\":3,\"name\":\"标签三\"},{\"cnt\":4,\"name\":\"标签一\"},{\"cnt\":1,\"name\":\"标签三\"},{\"cnt\":1,\"name\":\"标签三\"}]" ; JSONArray ja = JSONArray.parseArray(input); for (int i = 0 ; i < ja.size(); i++) { JSONObject jo = ja.getJSONObject(i); String name = jo.getString("name" ); Integer cnt = jo.getInteger("cnt" ); boolean contain = false ; Label conLabel = null ; for (Label tempLabel : labelList) { if (tempLabel.getName().equals(name)) { conLabel = tempLabel; contain = true ; break ; } } if (contain) { conLabel.setCnt(conLabel.getCnt() + cnt); }else { Label newLabel = new Label(name, cnt); labelList.add(newLabel); } } for (Label label:labelList) { labelSet.add(label); } System.out.println(labelSet.toString()); } }
运行结果:
1 [Label [name=标签一, cnt=5], Label [name=标签二, cnt=2]]
解释说明:
原因分析
会是需要重写equals和hashCode方法吗?
不会!
所有的类都会继承自java.lang.Object
。
java.lang.Object
的hashCode值是根据内存地址换算出来的一个值。
java.lang.Object
的equals方法是判断两个对象是否严格相等(object1 == object2
),有时候,我们需要的不是一种严格意义上的相等,而是一种业务上的对象相等。在这种情况下,就需要重写equals方法。
但在这里,{name=标签三, cnt=5}
,{name=标签一, cnt=5}
的hashCode
必定不相等,equals
也必定为false
解决
修改label类的compareTo
方法,即可解决问题。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 【部分代码略】 @Override public int compareTo (Label o) { if (o.cnt - cnt > 0 ) { return 1 ; }else { return -1 ; } } 【部分代码略】
运行结果:
1 [Label [name=标签三, cnt=5], Label [name=标签一, cnt=5], Label [name=标签二, cnt=2]]
解释说明:
返回1
,那么当前的值会排在被比较者后面。
返回0
,那么当前的值【不会被加入到 TreeSet 中】,因为当前的值【被认为和现有的某一个值相等】。
返回-1
,会被添加到被比较者的前边。
Collections
在讨论了Collection的两个子接口List和Set之后,我们来讨论一个工具类,Collections。
注意是Collections,不是Collection,多了一个s
Collections的常用方法:
方法名
说明
public static void sort(List list)
将指定的列表按升序排序
public static void reverse(List<?> list)
反转指定列表中元素的顺序
public static void shuffle(List<?> list)
使用默认的随机源随机排列指定的列表
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.kakawanyifan;import java.util.ArrayList;import java.util.Collections;import java.util.List;public class CollectionsDemo { public static void main (String[] args) { List<Integer> list = new ArrayList<Integer>(); list.add(30 ); list.add(20 ); list.add(50 ); list.add(10 ); list.add(40 ); Collections.sort(list); System.out.println(list); Collections.reverse(list); System.out.println(list); Collections.shuffle(list); System.out.println(list); } }
运行结果:
1 2 3 [10, 20, 30, 40, 50] [50, 40, 30, 20, 10] [10, 30, 40, 20, 50]
Collections中同样可以利用比较器。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package com.kakawanyifan;import java.util.ArrayList;import java.util.Collections;import java.util.Comparator;import java.util.List;public class CollectionsDemo { public static void main (String[] args) { List<Student> array = new ArrayList<Student>(); Student s1 = new Student("x" , 29 ); Student s2 = new Student("y" , 28 ); Student s3 = new Student("z" , 30 ); Student s4 = new Student("za" , 33 ); Student s5 = new Student("a" , 33 ); Student s6 = new Student("a" , 33 ); array.add(s1); array.add(s2); array.add(s3); array.add(s4); array.add(s5); Collections.sort(array, new Comparator<Student>() { @Override public int compare (Student s1, Student s2) { int num = s1.getAge() - s2.getAge(); int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num; return num2; } }); for (Student s : array) { System.out.println(s.getName() + "," + s.getAge()); } } }
运行结果:
1 2 3 4 5 y,28 x,29 z,30 a,33 za,33
Map
接下来,我们讨论集合的双列,Map。
概述和特点
Map是一个接口
描述的是键值对映射关系,一个键对应一个值,键不能重复,值可以重复。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.kakawanyifan;import java.util.HashMap;import java.util.Map;public class MapDemo { public static void main (String[] args) { Map<String,String> map = new HashMap<String,String>(); map.put("001" ,"林青霞" ); map.put("002" ,"张曼玉" ); map.put("003" ,"王祖贤" ); System.out.println(map); map.put("003" ,"柳岩" ); System.out.println(map); } }
运行结果:
1 2 {001=林青霞, 002=张曼玉, 003=王祖贤} {001=林青霞, 002=张曼玉, 003=柳岩}
map.put("003","柳岩")
覆盖了map.put("003","王祖贤")
。
Map的常用方法
方法名
说明
V put(K key,V value)
添加元素。
V get(Object key)
获取元素。
V remove(Object key)
根据键删除键值对元素。
void clear()
移除所有的键值对元素。
boolean containsKey(Object key)
判断集合是否包含指定的键。
boolean containsValue(Object value)
判断集合是否包含指定的值。
boolean isEmpty()
判断集合是否为空。
int size()
集合的长度,也就是集合中键值对的个数。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.kakawanyifan;import java.util.HashMap;import java.util.Map;public class MapDemo { public static void main (String[] args) { Map<String,String> map = new HashMap<String,String>(); map.put("张无忌" ,"赵敏" ); map.put("郭靖" ,"黄蓉" ); map.put("杨过" ,"小龙女" ); System.out.println(map); System.out.println(map.remove("郭靖" )); System.out.println(map.remove("郭襄" )); map.clear(); System.out.println(map.containsKey("郭靖" )); System.out.println(map.containsKey("郭襄" )); System.out.println(map.isEmpty()); System.out.println(map.size()); System.out.println(map); } }
运行结果:
1 2 3 4 5 6 7 8 {杨过=小龙女, 郭靖=黄蓉, 张无忌=赵敏} 黄蓉 null false false true 0 {}
Map的获取方法
方法名
说明
V get(Object key)
根据键获取值。
Set keySet()
获取所有键的集合。
Collection values()
获取所有值的集合。
Set<Map.Entry<K,V>> entrySet()
获取所有键值对对象的集合。
我们可以获取所有的键,可以根据键获取值。
我们还可以获取所有的键值对对象的集合。
另外,如果我们只想要值的话,可以直接获取值的集合。
所以,我们有三种遍历方式。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.kakawanyifan;import java.util.Collection;import java.util.HashMap;import java.util.Map;import java.util.Set;public class MapDemo { public static void main (String[] args) { Map<String, String> map = new HashMap<String, String>(); map.put("张无忌" , "赵敏" ); map.put("郭靖" , "黄蓉" ); map.put("杨过" , "小龙女" ); System.out.println(map.get("张无忌" )); System.out.println(map.get("张三丰" )); System.out.println("------" ); Set<String> keySet = map.keySet(); for (String key : keySet) { System.out.println(key + "," + map.get(key)); } System.out.println("------" ); Collection<String> values = map.values(); for (String value : values) { System.out.println(value); } System.out.println("------" ); Set<Map.Entry<String, String>> entrySet = map.entrySet(); for (Map.Entry<String, String> me : entrySet) { String key = me.getKey(); String value = me.getValue(); System.out.println(key + "," + value); } } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 赵敏 null ------ 杨过,小龙女 郭靖,黄蓉 张无忌,赵敏 ------ 小龙女 黄蓉 赵敏 ------ 杨过,小龙女 郭靖,黄蓉 张无忌,赵敏
HashMap的源码分析
现象
现在,我们来做一件事情,以学生作为键,以分数作为值。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.kakawanyifan;import java.util.HashMap;import java.util.Set;public class HashMapDemo { public static void main (String[] args) { HashMap<Student, Integer> hm = new HashMap<Student, Integer>(); Student s1 = new Student("林青霞" , 30 ); Student s2 = new Student("张曼玉" , 35 ); Student s3 = new Student("王祖贤" , 33 ); Student s4 = new Student("王祖贤" , 33 ); hm.put(s1, 99 ); hm.put(s2, 98 ); hm.put(s3, 87 ); hm.put(s4, 90 ); Set<Student> keySet = hm.keySet(); for (Student key : keySet) { int value = hm.get(key); System.out.println(key.getName() + "," + key.getAge() + "," + value); } } }
运行结果:
1 2 3 4 王祖贤,33,87 张曼玉,35,98 林青霞,30,99 王祖贤,33,90
出BUG了!
我们用的是HashMap啊,怎么出现了重复的键?两个王祖贤,33
?
到底怎么回事?
源码分析
我们来看看这个put方法到底做了什么。
1 2 3 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
先来看看hash方法。
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
哦,利用了hashCode,所以我们的Student类继承自Object类,我们要重写Student类的hashCode方法。
再来看看putVal方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0 ) n = (tab = resize()).length; if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null ); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this , tab, hash, key, value); else { for (int binCount = 0 ; ; ++binCount) { if ((e = p.next) == null ) { p.next = newNode(hash, key, value, null ); if (binCount >= TREEIFY_THRESHOLD - 1 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
还利用了equals方法,我们还要重写Student类的equals方法。
即,需要重写Student类的hashCode方法和equals方法。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package com.kakawanyifan;import java.util.Objects;public class Student { private String name; private int age; public Student (String name, int age) { this .name = name; this .age = age; } public void setName (String name) { this .name = name; } public String getName () { return name; } public void setAge (int age) { this .age = age; } public int getAge () { return age; } @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; Student student = (Student) o; if (age != student.age) return false ; return Objects.equals(name, student.name); } @Override public int hashCode () { int result = name != null ? name.hashCode() : 0 ; result = 31 * result + age; return result; } }
为什么是31?
因为31在JVM中是一个神奇的数字。
任何数n*31都可以被jvm优化为(n<<5)-n,移位和减法的操作效率比乘法的操作效率高很多。
集合嵌套
ArrayList嵌套HashMap
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.kakawanyifan;import java.util.ArrayList;import java.util.HashMap;import java.util.Set;public class ArrayListIncludeHashMapDemo { public static void main (String[] args) { ArrayList<HashMap<String, String>> array = new ArrayList<HashMap<String, String>>(); HashMap<String, String> hm1 = new HashMap<String, String>(); hm1.put("孙策" , "大乔" ); hm1.put("周瑜" , "小乔" ); array.add(hm1); HashMap<String, String> hm2 = new HashMap<String, String>(); hm2.put("郭靖" , "黄蓉" ); hm2.put("杨过" , "小龙女" ); array.add(hm2); HashMap<String, String> hm3 = new HashMap<String, String>(); hm3.put("令狐冲" , "任盈盈" ); hm3.put("林平之" , "岳灵珊" ); array.add(hm3); for (HashMap<String, String> hm : array) { Set<String> keySet = hm.keySet(); for (String key : keySet) { String value = hm.get(key); System.out.println(key + "," + value); } } } }
运行结果:
1 2 3 4 5 6 孙策,大乔 周瑜,小乔 杨过,小龙女 郭靖,黄蓉 令狐冲,任盈盈 林平之,岳灵珊
HashMap嵌套ArrayList
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.kakawanyifan;import java.util.ArrayList;import java.util.HashMap;import java.util.Set;public class ArrayListIncludeHashMapDemo { public static void main (String[] args) { HashMap<String, ArrayList<String>> hm = new HashMap<String, ArrayList<String>>(); ArrayList<String> sgyy = new ArrayList<String>(); sgyy.add("诸葛亮" ); sgyy.add("赵云" ); hm.put("三国演义" ,sgyy); ArrayList<String> xyj = new ArrayList<String>(); xyj.add("唐僧" ); xyj.add("孙悟空" ); hm.put("西游记" ,xyj); ArrayList<String> shz = new ArrayList<String>(); shz.add("武松" ); shz.add("鲁智深" ); hm.put("水浒传" ,shz); Set<String> keySet = hm.keySet(); for (String key : keySet) { System.out.println(key); ArrayList<String> value = hm.get(key); for (String s : value) { System.out.println("\t" + s); } } } }
运行结果:
1 2 3 4 5 6 7 8 9 水浒传 武松 鲁智深 三国演义 诸葛亮 赵云 西游记 唐僧 孙悟空
泛型
最后一个话题,泛型。
在《3.最常用的Java自带的类》 这么一章,讨论ArrayList的时候,我们提到了泛型。
1 ArrayList<String> array = new ArrayList<String>();
这里有一个<>
,里面写着String。这个含义是说这个ArrayList的元素的数据类型是String。
除了String,还可以是其他各种各样的引用类型,比如ArryList<Student>
,表示每一个元素的数据类型都是Student。
这就是泛型的使用,用于约束集合中存储元素的数据类型。
在本章,我们其实也大量用到了泛型。
现在我们系统的讨论一下泛型。
泛型概述
根据之前的讨论,我们知道泛型的本质是一个参数化类型,也就是说所操作的数据类型被指定为一个参数。
根据参数的个数,泛型的定义有两种格式,两种定义格式,其实我们都已经用过了。
第一种是格式是:
这种格式,我们在Collection中用过。
第二种格式是
这种格式,我们在Map中用过。
泛型类
泛型类初步
泛型类的定义格式
泛型类的使用格式
1 类名<具体的数据类型> 对象名 = new 类名<具体的数据类型>();
在JDK8及之后,后面的<>中的具体的数据类型可以省略不写。
1 类名<具体的数据类型> 对象名 = new 类名<>();
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;public class Generic <T > { private T t; public T getT () { return t; } public void setT (T t) { this .t = t; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.kakawanyifan;public class GenericDemo { public static void main (String[] args) { Generic<String> g1 = new Generic<String>(); g1.setT("林青霞" ); System.out.println(g1.getT()); Generic<Integer> g2 = new Generic<Integer>(); g2.setT(30 ); System.out.println(g2.getT()); Generic<Boolean> g3 = new Generic<Boolean>(); g3.setT(true ); System.out.println(g3.getT()); } }
运行结果:
特别的,如果实例化了泛型,但是又没有指定数据类型呢?
那么就是Object。
泛型标识
在刚刚的例子中,泛型类的类型参数是T
,这里的T
也被称为称为泛型标识。
泛型标识有如下几种,以及墨守成规的是惯例。
E,含义是Element,在集合中使用,因为集合中存放的是元素。
T,含义是Type,表示类。
K,含义是Key,表示键,指的是Map中的key。
V,含义是Value,表示值,值得是Map种的Value。
N,含义是Number,表示数值类型。
?,表示不确定的类。
S、U、V,也是类,在多个泛型的时候,以示区分。
泛型类的继承
泛型类也有其继承方法。
根据子类是不是泛型类分为两种。
子类是泛型类
子类是泛型类,子类和父类的泛型标识要一致。
1 class Zi<T> extends Fu<T>
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;public class Fu <E > { private E e; public E getE () { return e; } public void setE (E e) { this .e = e; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;public class Zi <T > extends Fu <T > { private T t; public T getT () { return t; } public void setT (T t) { this .t = t; } }
子类不是泛型类
子类不是泛型类,父类要明确泛型的数据类型。
1 class Zi extends Fu<Integer>
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kakawanyifan;public class Zi extends Fu <Integer > { @Override public Integer getE () { return super .getE(); } @Override public void setE (Integer integer) { super .setE(integer); } }
泛型接口
泛型接口定义格式
1 修饰符 interface 接口名<类型> { }
与继承类似,根据泛型接口的实现类是不是泛型类,在接口实现上分为两种情况。
实现类也是泛型类,实现类和接口的泛型标识要一致。
实现类不是泛型类,接口要明确数据类型。
示例代码:
1 2 3 4 5 package com.kakawanyifan;public interface Generic <T > { void show (T t) ; }
1 2 3 4 5 6 7 8 package com.kakawanyifan;public class GenericImpl <T > implements Generic <T > { @Override public void show (T t) { System.out.println(t); } }
1 2 3 4 5 6 7 8 9 10 11 package com.kakawanyifan;public class GenericDemo { public static void main (String[] args) { Generic<String> g1 = new GenericImpl<String>(); g1.show("林青霞" ); Generic<Integer> g2 = new GenericImpl<Integer>(); g2.show(30 ); } }
运行结果:
泛型方法
泛型方法的定义格式
1 修饰符 <类型> 返回值类型 方法名(类型 变量名) { }
<类型>
这一部分是泛型方法和其他方法主要区别,只有有这一部分的方法才能被称为泛型方法。
所以,在上文泛型类的成员方法,不可以被称为泛型方法。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.kakawanyifan;public class GenericDemo { private static <T> void show (T t) { System.out.println(t); } public static void main (String[] args) { show("林青霞" ); show(30 ); show(true ); show(12.34 ); } }
运行结果:
类型通配符
类型通配符的分类:
<?>
例如List<?>
:表示元素类型未知的List,它的元素可以匹配任何的类型。
<? extends 类型>
例如List<? extends Number>
:表示的类型是Number或者其子类型。
<? super 类型>
例如:List<? super Number>
:它表示的类型是Number或者其父类型。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.kakawanyifan;import java.util.ArrayList;import java.util.List;public class GenericDemo { public static void main (String[] args) { List<?> list1 = new ArrayList<Object>(); List<?> list2 = new ArrayList<Number>(); List<?> list3 = new ArrayList<Integer>(); System.out.println("--------" ); List<? extends Number> list5 = new ArrayList<Number>(); List<? extends Number> list6 = new ArrayList<Integer>(); System.out.println("--------" ); List<? super Number> list7 = new ArrayList<Object>(); List<? super Number> list8 = new ArrayList<Number>(); } }
泛型的特点
泛型的特点有:
泛型的类型参数只能是引用数据类型,不能是基本数据类型。
泛型的类型在逻辑上可以看成是多个不同的类型,但实际上都是相同类型。
第二个特点或许不好理解,我们来看一个例子。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kakawanyifan;public class GenericDemo { public static void main (String[] args) { Generic<String> g1 = new Generic<String>(); System.out.println(g1.getClass()); Generic<Integer> g2 = new Generic<Integer>(); System.out.println(g2.getClass()); Generic<Boolean> g3 = new Generic<Boolean>(); System.out.println(g3.getClass()); } }
运行结果:
1 2 3 class com.kakawanyifan.Generic class com.kakawanyifan.Generic class com.kakawanyifan.Generic
那么,现在问题来了,其实都是同一种类型吧,那是怎么做到又能支持"String",又能支持"Integer"的呢?
这就涉及到了泛型的原理,类型擦除。
泛型原理:类型擦除
泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的,但是泛型代码能够很好地和之前版本的代码兼容。那是因为,泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,我们称之为:类型擦除。
类型擦除有四种。
无限制类型擦除
有限制类型擦除
擦除方法中类型定义的参数
桥接方法
我们分别讨论。
无限制类型擦除
无限制类型擦除,在编译的时候,把范型擦除为Object类型。
我们可以通过反射看看,到底是不是这样。
(关于反射,我们在《9.类的加载与反射》 有做专题讨论。)
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kakawanyifan;public class Erasure <T > { private T t; public T getT () { return t; } public void setT (T t) { this .t = t; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.kakawanyifan;import java.lang.reflect.Field;public class ErasureDemo { public static void main (String[] args) { Erasure<Integer> erasure = new Erasure<>(); Class<? extends Erasure> clz = erasure.getClass(); Field[] declaredFields = clz.getDeclaredFields(); for (Field declareField:declaredFields) { System.out.println(declareField.getName() + " : " + declareField.getType().getName()); } } }
运行结果:
成员变量的名字依旧是t
,但是类型已经成为了java.lang.Object
,符合我们上文的描述,证明完毕。
也是因为这个原因,在反射中还有一个神奇的现象,越过泛型检查,我们会在《9.类的加载与反射》 讨论反射的时候讨论。
有限制类型擦除
有限制类型擦除,把类型擦除为其extend的类型。
证明过程略。
擦除方法中类型定义的参数
擦除方法中类型定义的参数,该种擦除同样存在有限和无限两种情况。
我们利用反射来看一下。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.kakawanyifan;import java.util.List;public class Erasure <T extends Number > { private T t; public T getT () { return t; } public void setT (T t) { this .t = t; } public <T extends List> T rtn (T t) { return t; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.kakawanyifan;import java.lang.reflect.Method;public class ErasureDemo { public static void main (String[] args) { Erasure<Integer> erasure = new Erasure<>(); Class<? extends Erasure> clz = erasure.getClass(); Method[] declaredMethods = clz.getDeclaredMethods(); for (Method declaredMethod:declaredMethods) { System.out.println(declaredMethod.getName() + " : " + declaredMethod.getReturnType().getName()); } } }
运行结果:
1 2 3 setT : void rtn : java.util.List getT : java.lang.Number
确实存在一个方法,方法名是rtn,返回类型是java.util.List
,符合我们上文的描述,证明完毕。
桥接方法
最后一种是桥接方法,这种主要存在与接口的实现类中。其特点是利用一个桥接方法,保持接口和类的联系。
我们利用反射来看一下。
示例代码:
1 2 3 4 5 package com.kakawanyifan;public interface Info <T > { T info (T t) ; }
1 2 3 4 5 6 7 8 9 package com.kakawanyifan;public class InfoImpl implements Info <Integer > { @Override public Integer info (Integer integer) { return integer; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.kakawanyifan;import java.lang.reflect.Method;import java.lang.reflect.Parameter;public class InfoImplDemo { public static void main (String[] args) { InfoImpl infoImpl = new InfoImpl(); Class<? extends InfoImpl> clz = infoImpl.getClass(); Method[] declaredMethods = clz.getDeclaredMethods(); for (Method declaredMethod:declaredMethods) { System.out.println(declaredMethod.getName() + " : " + declaredMethod.getReturnType().getName()); Parameter[] parameters = declaredMethod.getParameters(); for (Parameter parameter:parameters) { System.out.println(parameter.getType().getName()); } System.out.println(); } } }
运行结果:
1 2 3 4 5 info : java.lang.Integer java.lang.Integer info : java.lang.Object java.lang.Object
确实存在两个方法,其中一个是原始的逻辑处理,另一个是桥接方法。符合我们上文的描述,证明完毕。证明完毕。
可变参数
可变参数即参数个数可变,用作方法的形参出现,指该方法参数个数就是可变的。
可变参数定义格式
1 修饰符 返回值类型 方法名(数据类型... 变量名) { }
可变参数的注意事项
这里的变量其实是一个数组
如果一个方法有多个参数,包含可变参数,可变参数要放在最后 。
我们来看个具体的例子。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.kakawanyifan;public class ArgsDemo { public static void main (String[] args) { System.out.println(sum(0 )); System.out.println(sum(0 , 1 )); System.out.println(sum(0 , 1 , 2 )); System.out.println(sum(0 , 1 , 2 , 3 )); System.out.println(sum(0 , 1 , 2 , 3 , 4 )); System.out.println(sum(0 , 1 , 2 , 3 , 4 , 5 )); System.out.println(sum(0 , 1 , 2 , 3 , 4 , 5 , 6 )); System.out.println(sum(0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 )); System.out.println(sum(0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 )); System.out.println(sum(0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ,9 )); } public static int sum (int ... a) { int sum = 0 ; for (int i : a) { sum += i; } return sum; } }
运行结果:
1 2 3 4 5 6 7 8 9 10 0 1 3 6 10 15 21 28 36 45
Arrays源码分析
这不是我们第一次分析Arrays的源码,在《3.最常用的Java自带的类》 这一章我们也分析过Arrays的源码,当时我们以Arrays的源码为例,说设计工具类时候,非常建议的两个准则。
构造方法用private,防止外界创建对象。
成员方法用public static,为了使用类名访问成员方法。
这次,我们来看Arrays的一个似乎不是特别好的地方。
asList的注意
在Arrays类的asList方法中,我们可以看到可变参数,还可以看到泛型。
1 2 3 public static <T> List<T> asList (T... a) { return new ArrayList<>(a); }
修饰符之后紧跟着的<T>
,说明这是一个泛型方法。
T...
,说明这是一个可变参数方法。
我们来看个例子。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan;import java.util.Arrays;import java.util.List;public class ArgsDemo { public static void main (String[] args) { List<String> a = Arrays.asList("a" , "b" , "c" ); System.out.println(a); a.set(0 , "d" ); System.out.println(a); a.add("e" ); System.out.println(a); } }
运行结果:
1 2 3 4 5 6 [a, b, c] [d, b, c] Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.add(AbstractList.java:148) at java.util.AbstractList.add(AbstractList.java:108) at com.kakawanyifan.ArgsDemo.main(ArgsDemo.java:13)
可以修改,但是不能新增?不支持的操作?
看看asList方法的源代码。
1 2 3 public static <T> List<T> asList (T... a) { return new ArrayList<>(a); }
调用的是ArrayList的构造方法,再来看看ArrayList的构造方法。
1 2 3 4 5 6 7 8 9 10 11 12 private static class ArrayList <E > extends AbstractList <E > implements RandomAccess , java .io .Serializable { private static final long serialVersionUID = -2764017481108945198L ; private final E[] a; ArrayList(E[] array) { a = Objects.requireNonNull(array); } 【部分代码略】 }
注意看!a居然是用final修饰的。在《2.面向对象》 ,讨论继承的访问权限的时候,我们讨论过。“fianl修饰类:该类不能被继承(不能有子类,但是可以有父类)”
此外,我们还讨论了
只要是final修饰的变量,就是不能再次进行赋值了,但是对于修饰的是基本数据类型的变量还是引用数据类型的变量,还有区别:
fianl修饰基本数据类型变量:final修饰指的是基本类型的数据值不能发生改变。
final修饰引用数据类型变量:final修饰指的是引用类型的地址值不能发生改变,但是地址里面的内容是可以发生改变的。
这样就可以解释,为什么我们不可以增加删除,但是可以修改了。
解决方法
那么,我们怎么把一个数组转为ArrayList,同时还能对其进行增删操作呢?
两种方法。
遍历数组,然后一个一个加进去。
利用Collections工具类
的addAll()
方法。
第一种方法不举例子,第二种方法,我们举个例子。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.kakawanyifan;import java.util.ArrayList;import java.util.Collections;import java.util.List;public class ExceptionDemo { public static void main (String[] args) { String[] array = {"a" , "b" , "c" }; List<String> arrayList = new ArrayList<>(); Collections.addAll(arrayList,array); System.out.println(arrayList); arrayList.set(0 ,"d" ); System.out.println(arrayList); arrayList.add("e" ); System.out.println(arrayList); } }
运行结果:
1 2 3 [a, b, c] [d, b, c] [d, b, c, e]