1、各集合特点:
可以通过索引来访问List中元素,其接口实现类有ArrayList、Vector、LinkedList。ArrayList和Vector是List接口的实现类,底层数据结构是数组,查询快,增删慢。Vector是老的接口,不再推荐使用。LinkedList底层数据结构是链表,查询慢,增删快。
Set是不能包含相同元素的集合,其接口实现类有TreeSet、HashSet、EnumSet。HashSet底层数据结构是哈希表。LinkedHashSet是HashSet的子类,底层数据结构是链表和哈希表,它使用链表来维护元素插入时的次序,使集合看起来是以插入顺序保存的,其插入删除比HashSet要慢点,因为还要维护链表,但遍历集合比HashSet快,因为可以直接使用链表来遍历。TreeSet底层数据结构是红黑树,遍历其元素是有序的。EnumSet中元素必须是指定枚举类型的枚举值,其底层数据结构为位数组,其查询性能是Set中最高的。
期望收集对象时以队列方式就可以使用Queue,Deque接口继承自Queue,Deque是双端队列,可以从两端来添加、删除元素,所以Deque也可以当做普通的Queue来使用或当成栈来使用(虽然也有Stack类,但其继承自Vector,不推荐使用),Deque的接口实现类有LinkedList、ArrayDeque,如果需要频繁的随机访问操作使用ArrayDeque,对于频繁的插入、删除队列操作使用LinkedList。PriorityQueue是优先级队列,可以自定义优先级来设置元素顺序,其实现的接口为Queue。
Map用来存储键值对元素,Map的接口实现类有HashMap、Hashtable、TreeMap,其中的Hashtable是老的类,不推荐使用。HashMap底层使用哈希表,LinkedHashMap是HashMap的子类,它通过双向链表来维护元素插入时的顺序,所以遍历元素比HashMap要快。TreeMap采用红黑树结构来保存元素,遍历其元素是有序的。
2、Set
Set是不能包含相同元素的集合接口,向Set添加相同元素会失败,HashSet、TreeSet、EnumSet是Set接口的实现类。
HashSet根据元素的hash值来决定元素的存储位置,其元素类型可以不同,元素值也可以是null,它判断元素是否相等的依据是根据equals()和hashCode()都返回相等,所以元素是必须实现了hashCode()和equals()方法。
如果需要重写HashSet元素的equals()方法,那么也应该重写hashCode()方法,重写规则是:如果两个对象通过equals()比较返回true,那么这两个对象的hashCode值也也应该相同。对象中参与equals()、hashCode()计算的成员在加入到hashSet后不应该再修改其值,否则有可能导致无法精确访问该对象。对象包含多个成员时hashCode值的计算可以是多个成员的hash值与一个质数相乘后的和,如:hashCode = mem1.hashCode * 19 + mem2.hashCode * 31。
可以通过构造函数的参数来指定HashSet的初始容量(默认16,超过负载极限后成倍增长)和负载极限(默认0.75)。
package xu;
import java.util.*;
class Foo
{
public String str;
Foo(String s){str = s;}
public boolean equals(Object o)
{
if(this == o)
return true;
if(o != null && getClass == o.getClass())
return str == ((Foo)o).str ? true : false;
return false;
}
public int hashCode()
{
return str.hashCode();
}
public String toString()
{
return str;
}
}
public class Test
{
public static void main(String[] args)
{
HashSet hs = new HashSet();
hs.add(new Foo("c++"));
hs.add(new Foo("java"));
hs.add(new Foo("c#"));
//hs.add(new String("php")); //元素类型可以不同
hs.add(null); //元素可以为null
System.out.println(hs); //输出[c#, null, c++, java]
Foo[] ary = (Foo[])hs.toArray(new Foo[hs.size()]); //通过toArray方法将元素保存到数组中
String s = Arrays.toString(ary); //s为[c#, null, c++, java]
hs.contains(new Foo("c++")); //true
hs.contains("php"); //true
hs.contains(null); //true
}
}
LinkedHashSet是HashSet的子类,它使用链表来维护元素插入时的次序,使集合看起来是以插入顺序保存的。因为其额外使用链表来维护内部顺序,所以插入、删除元素时会比HashSet性能略低,但迭代遍历时效率很高。
TreeSet是SortedSet接口的实现类,它采用红黑树数据结构来存储集合元素,其元素是有序的。TreeSet里的对象元素必须是实现了Comparable接口的类,或者在定义TreeSet时指定管理的Comparable。Comparable接口是一个仅包含抽象方法compareTo()的函数式接口。TreeSet只能添加同一种类型的元素,它只通过equals()方法判断两个元素是否相同。
类似HashSet,当元素加入到TreeSet后不应再修改元素值,因为此时TreeSet不会进行排序。因为在Comparable接口实现compareTo方法中需要将比较对象强制转换成相同类型才能进行比较大小,所以TreeSet中元素类型应该相同,而且不能为null。
package xu;
import java.util.*;
class Foo implements Comparable
{
Foo(int i){ num = i;}
public int num;
public boolean equals(Object o)
{
if(this == o)
return true;
if(o != null && o.getClass() == Foo.class)
return (num == ((Foo)o).num);
return false;
}
public int compareTo(Object o)
{
Foo f = (Foo)o;
return num > f.num ? -1 : num < f.num ? 1 : 0; //倒序排序
return num - f.num; //正序排序
}
public String toString()
{
return "" + num;
}
}
public class Test
{
public static void main(String[] args)
{
/*TreeSet ts = new TreeSet((Object o1, Object o2)->{
Foo f1 = (Foo)o1;
Foo f2 = (Foo)o2;
return f1.num > f2.num ? -1 : f1.num < f2.num ? 1 : 0;
});*/
TreeSet ts = new TreeSet();
ts.add(new Foo(5));
ts.add(new Foo(3));
ts.add(new Foo(10));
System.out.println(ts); //输出为[10, 5, 3]
String s = Arrays.toString(ts.toArray(new Foo[ts.size()])); //调用toString()方法将元素保存到数组中
}
}
对于自定义的类,不具备排序的功能,如果自动排序的容器(如TreeSet)的元素类型是自定义的类,那么该元素类需要实现Comparable接口。当使用Arrays.sort、Collections.sort对自定义类型数组或容器进行排序的时候元素类需要实现Comparable或者指定一个Comparator(比较器)来进行定制排序,若一个类要实现Comparator接口,它一定要实现compare函数来支持排序。
import java.util.*;
class Foo
{
public int num;
Foo(int num){this.num = num;}
}
class PersonCompartor implements Comparator<Foo>
{
@Override
public int compare(Foo o1, Foo o2)
{
return o1.num-o2.num; //正序排序
}
}
public class Test
{
public static void main(String[] args)throws Exception
{
Foo f1 = new Foo(10);
Foo f2 = new Foo(1);
Foo f3 = new Foo(2);
Foo[] ary = new Foo[]{f1, f2, f3};
Arrays.sort(ary, new PersonCompartor());
for(Foo item:ary) //输出为1, 2, 10
{
System.out.println(item.num);
}
}
}
EnumSet中元素必须是指定枚举类型的枚举值,其元素顺序是按照枚举值在枚举类中定义的顺序,不允许插入null元素,我们通过EnumSet中的noneOf()、allOf()、of()等方法来生成EnumSet对象:
package xu;
import java.util.*;
enum Season
{
SPRING, SUMMER, FALL, WINTER
}
public class Test
{
public static void main(String[] args)
{
EnumSet es1 = EnumSet.noneOf(Season.class); //EnumSet类型的空集合,集合元素是Season类的枚举值
es1.add(Season.SPRING);
System.out.println(es1); //输出为[SPRING]
EnumSet es2 = EnumSet.allOf(Season.class); //集合包括Season类的所有枚举值
System.out.println(es2); //输出为[SPRING, SUMMER, FALL, WINTER]
EnumSet es3 = EnumSet.of(Season.SUMMER, Season.FALL);
System.out.println(es3); //输出为[SUMMER, FALL]
}
}
EnumSet在内部以位向量的形式存储,是性能最高的,但其元素只能是同一枚举类的枚举值。
HashSet性能比TreeSet要高,但TreeSet是按照元素值的大小有序保存的。
LinkedHashSet是HashSet的子类,它通过链表使集合看起来是按照插入时顺序排序的,所以在插入、删除上的性能要比HashSet低,但在迭代遍历集合的时候效率很高。
3、List
List是一个可重复的集合接口,可以通过索引来访问List中元素。除了Iterator迭代器,List还增加了一个ListIterator迭代器,以支持向前迭代,ListIterator还支持add()添加元素(Iterator只能通过remove()删除元素)。
ArrayList和Vector是List接口的实现类,他们内部封装了一个自动增加大小的动态Object[]数组,可以在其构造函数中指定数组初始大小(数组默认大小是10),也可以调用ensureCapacity()增加数组的大小。Vector是老旧的集合,推荐使用ArrayList。因为ArrayList内部基于动态数组,所以其在随机访问时性能很好,应该使用随机访问方法get()来遍历集合中元素。
package xu;
import java.util.*;
public class Test
{
public static void main(String[] args)
{
ArrayList l = new ArrayList();
l.add(new String("c++"));
l.add(new String("java"));
l.add(new String("c#"));
//l.add(32); //元素类型可以不同
//l.add(null); //元素可以为null
String s = Arrays.toString(l.toArray(new String[l.size()])); //toArray方法获得元素数组,s为[c++, java, c#]
l.sort((o1, o2)->((String)o1).length() - ((String)o2).length()); //以字符串长短排序
System.out.println(l); //输出为[c#, c++, java]
l.replaceAll(item->((String)item).length()); //替换元素值为原元素的长度
int s = l.size();
for(int i = 0; i < s; i++)
System.out.println(l.get(i)); //输出为2, 3, 4
ListIterator it = l.listIterator();
while(it.hasNext())
{
System.out.println(it.next()); //输出为-1, 4, -1, 3, -1, 2
it.add(-1);
}
while(it.hasPrevious())
System.out.println(it.previous()); //输出为4, 3, 2
}
}
LinkedList也是List的实现类,它同时也实现了Deque接口,其内部以链表为底层实现,所以在中间插入、删除元素时有更好的性能,应该使用迭代器来遍历集合中元素。
package xu;
import java.util.*;
public class Test
{
public static void main(String[] args)
{
LinkedList l = new LinkedList();
l.add(new String("c++"));
l.add(new String("java"));
l.add(1, new String("c#"));
//l.add(34); //元素类型可以不同
//l.add(null); //元素可以为null
System.out.println(l); //输出为[c++, c#, java]
String s = Arrays.toString(l.toArray(new String[l.size()])); //s为[c++, c#, java]
ListIterator it = l.listIterator();
while(it.hasNext())
{
System.out.println(it.next());
it.add(new String("php"));
}
System.out.println(l); //输出为[c++, php, c#, php, java, php]
}
}
Stack是Vector的子类,它用来模拟LIFO的栈,同样推荐使用ArrayDeque来替换Stak。
工具类Arrays里的asList()会返回一个List类型对象,不能增加和删除这个集合里的元素,这个List实际上是Arrays的内部类类型,属于固定长度的List集合。
4、Queue
Queue接口用于模拟FIFO的队列,它允许重复的元素,并且通常不允许随机访问队列中元素。通过使用add()/offer()方法添加元素到队尾,调用peek()/poll()访问队头元素,其实现类有优先级队列PriorityQueue。
PriorityQueue是优先级队列,它按照元素的大小进行排序,队首元素是最小的元素,队尾元素是最大的元素。类似TreeSet,其元素通过实现Comparable接口来实现自定义的排序。
Deque接口用于双端队列,可以从两端来添加、删除元素,它是Queue的子接口,Deque的实现类也可以当成栈来使用(包含push()、pop()方法)。Deque的实现类有ArrayDeque和LinkedList。
ArrayDeque是一个基于动态可再分配的数组实现的双端队列(数组默认长度为16),它可以用作(双端)队列和栈。
5、map
Map的key不允许重复,常用方法:
Object get(Object key):获得指定key的value,key不存在的话返回null。
Object getOrDefault(Object key, V DefaultValue): 获得指定key的value,key不存在的话返回DefaultValue。
put(Object key, Object value):增加一个元素(已存在的话会替换)。
replace(key, newValue)/replace(key, oldValue, newValue):替换指定元素(根据元素的key或key-value)的value(不存在的话不会添加)。
containsKey()/containsValue():判断是否存在指定的key/value。
keySet():获得所有key组成的set集合。
merge(Object key, Object DefaultValue, BiFunction func): 如果key存在且对应的value为null或者key不存在的话会以(key, DefaultValue)替换(增加)元素,如果key存在且对应的value不为null则使用func方法替换旧的value。
forEach():遍历集合,这里的forEach并不是Collection的方法而是自己的方法,因为Map并没有继承Collection接口。
values():返回Collection对象,其中包含了map所有元素的值。
set entrySet():将Map转换成元素类型为Map.Entry<K, V>的set,Entry为Map接口中内部类,它类似c++中pair,代表键值对类型,包含成员方法getKey()、getValue()、setValue()。
HashMap、Hashtable都是Map接口的实现类,Hashtable是老旧的类,不推荐使用。HashMap的key与HashSet中元素类似,必须实现hashCode()和equals()方法,不要修改元素的key值,key类型可以不同,key可以为null。
package xu;
import java.util.*;
public class Test
{
public static void main(String[] args)
{
Map m = new HashMap();
m.put(80, "java");
m.put(10, "python");
m.put(90, "c++");
m.put(120, null);
//m.put("number", 60); //key类型可以不同
//m.put(null, null); //key可以为null
System.out.println(m); //输出为 {80=java, 120=null, 10=python, 90=c++}
int k = 10;
m.merge(k, "unknown", (oldValue, defaultValue)->k + ":" + oldValue + "-" + defaultValue);
for(Object key : m.keySet()) //keySet()获得所有的键,返回类型为set对象
{
String str = (String)(m.get(key));
System.out.println(str); //输出为java, null, 10:python-unknown, c++
}
m.forEach((key, value)->System.out.println(key + ":" + value));
//输出为
//80:java
//120:null
//10:10:python,unknown
//90:c++
m.values().forEach(value->System.out.println(value)); //java、null、10:python-unknown、c++
}
}
LinkedHashMap是HashMap的子类,它通过双向链表来维护元素插入时的顺序,迭代遍历其中的所有元素会有较好的性能。
TreeMap类实现了SortedMap接口, SortedMap接口由Map接口派生,类似TreeSet,它采用红黑树结构来保存元素,根据key为元素排序存储。与TreeSet类似,其key类型不能不同,key不能为null。
HashSet、TreeSet实际上是value为null的HashMap、TreeMap。
TreeMap m = new TreeMap();
m.put(80, "java");
m.put(10, "python");
m.put(90, "c++");
m.put(120, null);
System.out.println(m); //输出为 {10=python, 80=java, 90=c++, 120=null}
WeakHashMap中key是对象的弱引用,如果key所引用的对象没有被其它强引用变量所引用,那么key所引用的对象可能被垃圾回收,而此时WeakHashMap也可能自动删除这个key-value元素。
IdentityHashMap与HashMap不同的是:HashMap以key的equals()方法和hashCode()来判断两个key是否相等,IdentityHashMap以==运算符来判断两个key是否相同。
EnumMap中的key必须是指定枚举类的枚举值。
Properties类相当于是一个key和value都是String类型的Map,使用它可以将key-value值写入到.properties或ini或xml等属性文件,或者从属性文件中读取key-value值,使用loadFromXML()方法来加载XML文件,其常用方法有:
setProperty():增加key-value值。
store():写入到文件。
load():从文件加载。
getProperty():获取指定的key-value。
package xu;
import java.util.*;
import java.io.*;
public class Test
{
public static void main (String[] args)throws Exception
{
//向ini文件写入
Properties props = new Properties();
props.setProperty("name", "leon");
props.setProperty("city", "peking");
props.store(new FileOutputStream("config/data.ini"), "section");
//从ini文件读出
Properties props = new Properties();
props.load(new FileInputStream("config/data.ini"));
String strName = props.getProperty("name");
String strCity = props.getProperty("city");
}
}
6、各集合的特点
Set中不能包含相同元素,Map中也不能包含相同的key,List、Queue则可以包含相同的元素。
List、Queue按元素插入时的顺序存储,Set、Map中顺序为乱序(HashSet、HashMap)或者按照元素大小(TreeSet、TreeMap)排序。
Map包含通过key查找value的方法get(Object o),List、Queue中查找元素方法get(int idx)只能通过索引值,Set中仅包含判断元素是否存在的contains(Object o)方法。
ArrayList、ArrayDeque适合经常遍历集合或者通过索引随机访问元素的情况。
7、Collections
①、工具类Collections中提供了对集合进行操作的静态方法,如对List进行查找、取最大最小、统计元素出现次数(frequency)、统计子List位置、替换(replaceAll)、反转、排序、随机排序(shuffle)、元素交换、元素移动(rotate)等。
②、Collections提供了unmodifiableXxx()方法来返回指定集合的只读视图:
Map m = new HashMap();
Map um = Collections.unmodifiableMap(m); //向um添加元素的话会抛出UnsupportedOperationException异常,向m添加元素的话会直接反应到um上
List、Set、Map的静态方法of()可以传入一个不定长的参数(实际为数组类型)来得到对应的类型对象,这是建立List、Set、Map实例的快捷方法,不过of()创建的示例都是只读的,修改这个只读集合对象的话会抛出 UnsupportedOperationException异常:
import java.util.*;
class Name
{
public String name;
public Name(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args){
List<String> n = List.of("leon", "john");
Map<String, Integer> m = Map.of("java", 95, "c++", 100);
Name na = new Name("mem");
List<Name> s = List.of(na);
na.name = "dcd";
System.out.println(s.get(0).name); //输出为dcd,因为List的of()方法是浅拷贝
String[] names = {"leon", "john"};
List<String> namesLt = Arrays.asList(names);
names[0] = "jake";
namesLt.forEach(str->System.out.println(str)); //输出为jake、john,Arrays的asList()方法同样是浅拷贝
}
}
③、java集合中除了Vector、Stack、Hashtable其它都不是线程安全的,可以使用Collections的synchronizedXxx()方法来包装指定集合为线程安全的:
List<Integer> l = Collection.synchronizedList(new ArrayList<>())
Map m = Collections.synchronizedMap(new HashMap());
需要注意的是Vector、Stack、Hashtable或者使用Collections的synchronizedXxx()方法包装指定集合为线程安全实际上是将add()、remove()、get()等集合方法使用synchronized关键字来声明为原子操作方法,即集合的方法是线程安全的,比如一个线程调用集合的add(),另一个线程调用集合的clear(),这是线程安全的,但集合的最后结果不一定是空的。所以还需要考虑程序逻辑上的线程安全问题,比如下面的func()方法应该使用synchronized声明为同步方法,避免在判断集合非空后其他线程又清空了该集合:
public class Main {
private List<Integer> l;
public synchronized void func(){
if(!l.isEmpty())
{
Integer i = l.get(0);
}
}
public static void main(String[] args)
{
}
}
再比如在遍历集合之前使用synchronized对集合进行加锁,防止在遍历过程中其他线程操作容器引起的迭代器失效问题(Iterator采用“快速失败”机制,一旦在迭代过程中检测到集合被其它线程修改会立即引发ConcurrentModificationException异常。),如下示例1所示。再比如,对于一个Vector,我们想要在插入元素的时候容器中如果已经存在的话就不插入了,代码如下示例2所示,如果是两个线程执行put()方法的话那么就可能不会达到我们的预期,比如一个线程判断到当前无重复数据,此时转到另一个线程也是判断到当前无重复数据,这种情况下就出现了插入两条重复数据的情况:
示例1:
Map m = Collections.synchronizedMap(new HashMap());
m.put(3, 4);
m.put(5, 5);
synchronized(m)
{
//forEach()或增强式for循环底层也是使用迭代器
m.forEach((key, value)->System.out.println(key + "" + value));
/*for(Object o :m.keySet())
{
Integer i = (Integer)m.get(o);
System.out.println(i);
}*/
}
实例2:
Vector vector = new Vector();
public void put(String element)
{
if (!vector.contains(element))
vector.add(element);
}
线程安全的集合的另一种方法是使用ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque、CopyOnWriteArrayList、CopyOnWriteArraySet、BlockingQueue等,它们在java.util.concurrent包下。
CopyOnWriteXxx集合使用Copy-on-write写时拷贝技术,适用于写的情况比较少的集合,当多个线程读取一个容器的时候获得的是当前容器的内容,而且不用加锁,当有写入容器的操作的时候就复制容器出来然后写入到这个备份容器中去,这以后的容器操作会在备份容器中进行(将原来参考旧容器的变量参考至新的变量)。写时复制技术有两个缺点:一个是因为写入的时候要复制内存,如果多个线程同时写入或者要复制的内存本来就比较大的话这时候内存占用会比较多,另一个是数据实时问题,就是在写还没完成的时候其它线程进行读取的话读到的是原来的旧数据。
Vector之所以线程安全,是因为它几乎在每个方法声明处都加了synchronized关键字来使方法原子化,但是我们要在一个线程里遍历Vector,在另一个线程里修改Vector的话,必须在遍历Vector之前和修改Vector之前加锁。使用并发容器CopyOnWriteXXX就可以在遍历和写入前不加锁,一般来说,我们会认为CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArraySet是同步Set的替代品。
Copy-on-write(COW)解决并发的的思路是实行读写分离,如果执行的是写操作,则复制一个新集合,在新集合内添加或者删除元素,待修改完成之后,再将原集合的引用指向新的集合。读取集合或者对集合进行遍历操作不需要加锁,因为当前集合不会添加任何元素。COW的核心理念就是读写分离,写操作在一个复制上进行,读操作还是在原始数据上进行,读写分离,互不影响,当然写操作里还会加锁以应对并发写入。当写入情况不多的情况下使用CopyOnWriteArrayList性能要优于synchronizedList。
CopyOnWriteXXX之所以可以在遍历之前不用加锁,是因为iterator()获得的迭代器指向的是旧的集合数据,如下所示,当本线程或另一个线程在while循环的时候对集合增加或者删除了元素后,迭代器指向的还是原来的老数据,所以不会引发异常,只有当我们再次获取集合的迭代器后,此时集合中数据才是改变后的新数据。forEach遍历实际上也是使用的迭代器遍历。
public class Test {
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList c = new CopyOnWriteArrayList();
c.add("java");
c.add("c#");
c.add("c++");
Iterator it = c.iterator();
boolean b = true;
while (it.hasNext()) { //该循环输出java、c#、c++
if (b) {
c.remove(0); //该删除操作不会引发迭代器遍历的异常
}
b = false;
String s = (String) it.next();
System.out.println(s);
}
Iterator itNew = c.iterator();
while (itNew.hasNext()) { //该循环输出c#、c++
String s = (String) itNew.next();
System.out.println(s);
}
}
}
BlockingQueue是Queue的子接口,新定义了put()、take()等方法,线程调用put()方法在队列已满的情况下会阻塞,线程调用take()方法在队列为空的情况下会阻塞,这样用它来实现生产者-消费者模型的话就不用再使用条件变量来进行同步,省去了wait()、notify()等流程。BlockingQueue接口的操作类有ArrayBlockingQueue、LinkedBlockingQueue等。
Concurrent相当于是使用Collections的synchronizedXxx()方法包装指定集合为线程安全的。前面说过,在迭代容器过程中使用容器的remove()方法删除删除元素的话会产生异常,如下所示。推荐的是使用迭代器的remove()方法。而对于支持多线程操作的ConcurrentXxx集合,则不会产生前面所说的异常。
import java.util.*;
public class Test
{
public static void main(String[] args)
{
Collection c = new ArrayList();
c.add("java");
c.add("c#");
c.add("c++");
Iterator it = c.iterator();
c.remove("c++");
while(it.hasNext())
{
String s = (String)it.next(); //此处会引发异常
System.out.println(s);
}
}
}
ConcurrentMap是Map的子接口,其定义了remove()、replace()、putIfAbsent()等原子操作方法,ConcurrentHashMap是ConcurrentMap的操作类。根据不同JDK版本的实现不同,多线程即使不是写入而是读取HashMap的话也可能造成死循环,所以多线程不管是读还是写都应该使用ConcurrentHashMap。
ConcurrentSkipListMap可以视为TreeMap的线程安全版本。
Java 8为ConcurrentHashMap增加了很多新的方法,如:
forEach系列方法对所有的元素或键或值应用一个函数:forEach()、forEachKey()、forEachValue()等。
search系列方法对所有的元素或键或值应用一个函数,直到该函数返回一个非null的结果,然后search会终止并返回该函数的结果。
reduce会通过提供的累计函数,将所有的键或值结合起来。
以上方法都会提供一个带parallelismThreshold参数的版本,该参数用来指定当集合中元素数量超过该值后就使用多线程并发操作,以充分利用多核CPU。
默认情况下,ConcurrentHashMap支持16个线程并发写入,超过的话需要等待,可以通过构造参数concurrencyLevel来设置更高。
下面为使用reduce计算map的key累加值的示例:
import java.util.concurrent.*;
public class Test
{
public static void main(String[] args)
{
ConcurrentHashMap<Integer, String> m = new ConcurrentHashMap<>();
m.put(80, "java");
m.put(10, "python");
m.put(90, "c++");
int sum = m.reduceKeys(1, Integer::sum); // sum为180
}
}
8、Comparable和Comparator
Collections提供了sort()方法对集合中元素进行排序,必须是支持索引的集合才能使用它来进行排序,而且元素应该是操作了(实现了)函数式接口java.lang.Comparable,Comparable接口中有一个抽象方法compareTo():
import java.util.*;
class MyObject implements Comparable<MyObject>{
MyObject(int n){m_number = n;}
@Override public int compareTo(MyObject o){
return m_number - o.m_number; //由小到大排序
//return o.m_number - m_number; //由大到小排序
}
@Override public String toString(){
return String.valueOf(m_number);
}
private int m_number = 0;
}
public class Main {
public static void main(String[] args){
List l = Arrays.asList(new MyObject(10), new MyObject(5),new MyObject(3));
Collections.sort(l);
l.forEach(item->System.out.println(item));
}
}
如果我们不能修改元素类的代码来实现Comparable或者我们想要对Integer类型的集合进行由大到小排序的话可以传入一个实现了Comparator接口的类的对象给sort(),Comparator接口中的抽象方法为 int compare(T o1, T o2):
import java.util.*;
class MyObject{
MyObject(int n){m_number = n;}
@Override public String toString(){
return String.valueOf(m_number);
}
public int myCompare(MyObject o){
return m_number - o.m_number;
}
private int m_number = 0;
}
public class Main {
public static void main(String[] args){
List<MyObject> l = Arrays.asList(new MyObject(10), new MyObject(5),new MyObject(3));
Collections.sort(l, (o1, o2)->o1.myCompare(o2));
l.forEach(item->System.out.println(item));
}
}
对于Comparator我们查询JDK文档的话会发现其有两个抽象方法,如下所示,其实equals()方法算是Object里的抽象方法,一个最顶级的接口会隐式包含Object中的public方法为抽象方法,然后在接口的实现类中自动实现这些方法。在Comparator中这里其实只是显示声明了equals()抽象方法。
TreeSet中元素是排好序的,PriorityQueue根据优先级排序,它们的元素就必须是实现了Comparable接口或在创建TreeSet时指定Comparator对象。
List实际上包含了sort()成员方法,而且Lambda还可以进一步简化,如下所示:
List<String> words = Arrays.asList("D", "C", "B", "A");
words.sort(String::compareTo); //字符串由小到大排序
words.sort((s1, s2)->-s1.compareTo(s2)); //字符串由大到小排序
for(String s : words)
{
System.out.println(s);
}
JDK8中的Comparator增加了一些静态方法,使用它们可以更方便,比如下面使用Comparator的静态方法comparing()返回了一个Comparator(即lambda表达式):
其中的Function也是一个接口:
import java.util.*;
class Person{
@Override
public String toString() {
return name;
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
private String name;
//......
}
public class Test {
public static void main(String[] args){
List<Person> l = new ArrayList<>();
l.add(new Person("c"));
l.add(new Person("d"));
l.add(new Person("a"));
l.add(new Person("b"));
l.sort(Comparator.comparing(Person::getName)); //根据name来排序,排序方法使用name的compareTo(),即需要排序的对象必须包含方法compareTo
//相当于
//l.sort(Comparator.comparing(p -> p.getName()));
//也相当于
//l.sort((s1, s2)->s1.getName().compareTo(s2.getName()));
l.forEach(System.out::println); //"a"、"b"、"c"、"d"
}
}
再比如我们需要对一个集合进行反向排序,并将null元素排在最前,可以使用Comparator的静态方法nullsFirst()和reverseOrder(),reverseOrder()返回的Comparator会是Comparable对象上定义顺序的反序,nullFirst()接受reverseOrder()返回的Comparator,在其顺序之上加上让null排在最前的规则后返回这个新的Comparator:
import java.util.*;
public class Main {
public static void main(String[] args){
List<String> l = new ArrayList<>();
l.add("c");
l.add("d");
l.add("a");
l.add("b");
l.add(null);
l.sort(Comparator.nullsFirst(Comparator.reverseOrder())); //null, "d", "c"、"b"、"a"
}
}
Comparator中也有一些默认方法,如thenComparing(),使用它可以从现有的Comparator实例组合出更复杂的Comparator实例,如下我们实现了按照姓排序一个list,如果姓相同的话就按照名排序,如果名也相同的话再按邮编排序:
import java.util.*;
import static java.lang.System.out;
import static java.util.Comparator.comparing;
public class Test
{
public static void main(String[] args)
{
List<Customer> l = new ArrayList<>();
l.add(new Customer("zhang", "san", 101));
l.add(new Customer("li", "si", 101));
l.add(new Customer("wang", "wu", 101));
Comparator<Customer> byLastName = comparing(Customer::getFirstName);
//每次Comparator实例调用thenComparing()都会返回新的Comparator组合实例
l.sort(byLastName.thenComparing(Customer::getLastName).thenComparing(Customer::getZipCode));
l.forEach(out::println);
}
}
class Customer
{
private String firstName, lastName;
private Integer zipCode;
public Customer(String firstName, String lastName, Integer zipCode)
{
this.firstName = firstName;
this.lastName = lastName;
this.zipCode = zipCode;
}
@Override
public String toString()
{
return String.format("Customer(%s %s, %d)", firstName, lastName, zipCode);
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public Integer getZipCode() { return zipCode; }
}