Java集合
本文主要讲解JDK内部集合类、Apache Commons Collection中的集合类、Guava中的集合类、并发集合类、Redission分布式集合类。
# JDK内部集合
# 1、数组
两种初始化方法:
// 方式1
String[] arr = new String[2];
// 方式1
String[] arr = new String[]{"", ""};
多维数组:
String[][] arr = new String[2][3];
用法:
// 访问元素
arr[0]
// 循环-方式1
for (String s : arr) {
...
}
// 循环-方式2
for (int i = 0; i < arr.length; i++) {
String s = arr[i];
...
}
// 循环-方式3
Arrays.stream(arr).forEach(s -> {});
// 循环-倒序
for (int i = arr.length - 1; i >= 0; i--) {
String s = arr[i];
...
}
Arrays工具类中的方法:
stream
:数组转为Stream流copyOfRange
:将指定数组的指定范围复制到新数组中copyOf
:复制指定的数组,截断或填充空值(如有必要),使副本具有指定的长度binarySearch
:二分搜索fill
:将指定的值分配给数组的每个元素setAll
:和fill类似,不过指定值为自定义Functionsort
:升序排序spliterator
:拆分器。大多数情况下,你都不需要使用到这个方法,他主要是跟StreamSupport配合使用。参考java8 Stream之Spliterator (opens new window)parallelPrefix
:使用提供的函数并行累积给定数组的每个元素。相当于并行的reduce操作parallelSetAll
:setAll的并行版本parallelSort
:sort的并行版本toString、equals、hashCode
:共用基础方法,不多说。此外这三个方法还有对应的deep系列,分别是deepToString、deepEquals、deepHashCode
这其中要重点说一下数组拷贝,其实Java中数组拷贝最终都是调用System.arraycopy()
方法,这个方法的性能是非常高的,因为它是直接对内存进行复制。所以关于数组复制尽量调用此方法。
需要注意的是System.arraycopy()
属于浅复制,也就是复制对象和二维数组的时候复制的是引用,修改复制后的对象会影响到原始对象。
数组特点:
- 访问速度快。插入和查询时间复杂度是O(1)
- 初始化之后长度不可变,不支持动态扩容
# 2、List
为了解决数组动态扩容的问题,产生了List集合。
主要有两个实现类:
ArrayList
:数组实现。遍历和随机访问效率高LinkedList
:链表实现。插入和删除效率高
初始化:
List<Integer> list = new ArrayList<>();
list.add(2);
list.add(3);
// java9之后。of方法返回的是不可变List
List<Integer> list = List.of(2,3)
List继承自Collection接口,Collection继承自Iterable接口。
Iterable中的方法:
iterator
:迭代器。有了他我们就可以使用foreach循环了forEach
:java8之后新增方法,lambda式的forEachspliterator
:拆分器
Collection中的方法:
size
:获得集合中的元素数isEmpty
:此集合是否不包含任何元素contains
:是否包含指定元素toArray
:转换为array数组。toArray默认返回时Object数组,但他有两个重载方法,可以返回指定类型的数组
String[] arr = list.toArray(new String[]{});
String[] arr2 = list.toArray(i->new String[]{});
add
:添加元素remove
:删除元素containsAll
:是否包含指定的所有元素addAll
:添加指定的所有元素removeAll
:删除指定的所有元素removeIf
:删除满足条件的元素retainAll
:保留指定的所有元素,其他元素则删除clear
:清空集合stream
:获得stream流parallelStream
:获得并行stream流
List中的方法:
addAll(i,col)
:指定位置添加元素replaceAll
:按照指定规则替换所有元素sort
:根据Comparator进行排序。在java8中使用lambda表达式特别方便
// 对于基本类型
list.sort(String::compareTo);
//对于Comparator<String> compare = String::compareTo;你可能会比较迷惑,其实他的转换过程是这样的
BiFunction<String, String, Integer> bf = String::compareTo;
Comparator<String> compare = (Comparator<String>) bf; // 可以看到Comparator函数接口的结果和BiFunction是相似的,java做了简化处理
// 对于Bean
list.sort(Comparator.comparing(TUser::getId))
get
:获得指定位置元素set
:设置指定位置元素add(i,e)
:添加指定位置元素。会导致原先此位置的元素后移remove(i)
:删除指定位置元素。如果List中泛型为Integer,这里会和Collection中的remove有一个冲突,具体这么做
// 调用的是Collection.remove
intList.remove(Integer.valueOf(2));
// 调用的是List.remove
intList.remove(5);
indexOf
:指定元素第一次出现时的索引lastIndexOf
:指定元素最后一次出现时的索引listIterator
:获得ListIterator,支持在迭代期间修改列表subList
:返回指定位置范围的视图
List还有两个静态方法:
of
:java9之后支持。获得不可修改的列表copyOf
:java11之后支持。拷贝一个不可修改的列表
# 3、Set
Set是不包含重复元素的集合。继承自Collection接口,主要实现类:
HashSet
:不保证迭代顺序和插入顺序一致。内部借助HashMap来实现LinkedHashSet
:能保证迭代顺序和插入顺序一致。内部借助LinkedHashMap来实现TreeSet
:元素按照自然顺序或者提供的Comparator来排序
初始化:
// 方式1
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
// 方式2。返回不可变Set
Set<Integer> integers = Set.of(1, 2);
HashSet和LinkedHashSet没有自己独立的方法,参考Collection接口即可。
Set接口有of和copyOf方法,功能同List。
TreeSet继承自NavigableSet,NavigableSet继承自SortedSet。我们来看看他们其中的方法。
SortedSet提供了对元素排序的功能,其中的方法有:
subSet
:返回此集合部分的视图,其元素范围从fromElement (包括)到toElement (不包括)headSet
:返回此集合中元素严格小于toElement的部分的视图tailSet
:返回此集合中元素大于或等于fromElement的部分的视图first
:第一个元素last
:最后一个元素
NavigableSet是对SortedSet的导航扩展,报告给定搜索目标的最接近匹配。参考NavigableSet Api (opens new window)
Set和List都继承自Collection接口,JDK提供了Collections工具类,其中有一些常用的方法:
sort
:自然排序binarySearch
:二分搜索reverse
:反转集合顺序shuffle
:打乱顺序(洗牌)swap
:根据下标交换指定元素的位置copy
:列表拷贝min
:根据排序规则,取得最小值max
:根据排序规则,取得最大值rotate
:旋转,元素的位置将要被重新调整为(i - distance) mod list.size()。用于快速元素移动很有效
List<Integer> list = Lists.newArrayList(1, 2, 3, 4);
// 将元素整体后移一位
Collections.rotate(list, 1); // [4, 1, 2, 3]
// 将元素整体前移一位
Collections.rotate(list, -1); // [2, 3, 4, 1]
// 仅移动部分区间数据
Collections.rotate(list.subList(1,3),1); // [1, 3, 2, 4]
replaceAll
:替换集合中旧值为新值indexOfSubList
:列表级别的indexOfunmodifiableCollection
:变为不可修改集合。还有系列方法:unmodifiableSet、unmodifiableSortedSet、unmodifiableNavigableSet、unmodifiableMap等synchronizedCollection
:变为现场安全的集合。系统方法同上。Java1.5之后新增了同步容器ConcurrentMap等,所以不再推荐使用此系列方法checkedCollection
:返回指定集合的动态类型安全视图,只允许插入指定的类型。系列方法同上emptyIterator
:返回一个没有元素的迭代器。还有系列方法:emptySet、emptyList、emptyMap等singleton
:返回一个只包含指定对象的不可变集合。还有系列方法:singletonList、singletonMapnCopies
:n个对象副本组成的不可变列表reverseOrder
:排序辅助方法,返回逆序的Comparatorenumeration
:返回集合的Enumeration对象list
:Enumeration对象转换为List对象frequency
:返回指定集合中等于指定对象的元素数,其实可以叫getCountdisjoint
:两集合没有共同的元素,则返回true
# 4、Map
Map是键值对,每个键最多映射到一个值上。主要实现类有:
HashMap
:基于哈希表的实现,允许null键和null值。内部使用数组+链表的数据结构,为了保证性能,Java8之后链表元素数量大于阈值会变为红黑树LinkedHashMap
:保证了迭代顺序和插入顺序一致,允许null键和null值。内部使用哈希表+双向链表实现TreeMap
:键值按照自然顺序或者提供的Comparator来排序,不允许null键,允许null值。内部使用红黑树来实现HashTable
:线程安全的哈希表,因为性能太差,不推荐使用。讲到并发集合的时候,用ConcurrentMap替代它
Map接口有of和copyOf方法,功能同List。此外还有两个静态方法:entry和ofEntries。
初始化:
// 方式1
Map<String, Integer> map = new HashMap<>();
map.put("a",1);
map.put("b",2);
// 方式2。返回不可变Map
Map<Integer, Integer> integerMap = Map.of(1, 2);
// 方式3。返回不可变Map
Map<String, Integer> stringIntegerMap = Map.ofEntries(Map.entry("a", 1), Map.entry("b", 2));
Map接口的方法:
size
:键值数量isEmpty
:是否为空字典containsKey
:是否包含指定键containsValue
:是否包含指定值get
:根据键获得值put
:添加键值remove
:删除键值putAll
:批量添加clear
:清空键值字典keySet
:获得键Setvalues
:获得值集合entrySet
:获得键值对Set,泛型为Map.EntrygetOrDefault
:安全的get,没有值则返回默认值forEach
:foreach的lambda版本replaceAll
:替换匹配的所有元素putIfAbsent
:仅当键不存在时,才会执行给定的函数来计算新的值,并用这个新值插入到 Map 中replace
:替换computeIfAbsent
:putIfAbsent的Function版本computeIfPresent
:仅当键存在时,才会执行给定的函数来计算新的值,并用这个新值替换原来的值。compute
:无论键是否存在,都会执行给定的函数来计算新的值,并用这个新值替换原来的值。merge
:如果键存在,则使用提供的 BiFunction 函数来合并旧值和新值;如果键不存在,则直接将新值插入到 Map 中。
后四个方法为Java 8中新增的,有必要演示一下:
// 基础数据
Map<String, Object> map = new HashMap<>();
map.put("name", "Jack");
map.put("age", 25);
// computeIfAbsent:name不会被插入成功。因为已经存在了name键
System.out.println(map.computeIfAbsent("name", k -> k + " random")); // 返回值为"Jack"
System.out.println(map); // map:{name=Jack, age=25}
// 插入一个新的键值对
System.out.println(map.computeIfAbsent("score", k -> 90)); // 返回值为90
System.out.println(map); // map:{name=Jack, age=25, score=90}
// computeIfPresent:name会被插入成功,会替换掉旧值
System.out.println(map.computeIfPresent("name", (k, v) -> k + " random")); // 返回值为"name random"
System.out.println(map); // map:{name=name random, age=25}
// 按照你想要的规则进行替换
System.out.println(map.computeIfPresent("name", (k, v) -> v + " random")); // {name=Jack random, age=25}
System.out.println(map);
System.out.println(map.computeIfPresent("name", (k, v) -> k + v)); // {name=name Jack random, age=25}
System.out.println(map);
// 替换新值为null的话键值会被删除
System.out.println(map.computeIfPresent("name", (k, v) -> null)); // 返回值为null
System.out.println(map); // map:{age=25}
// 插入一个不存在的键值会失败
System.out.println(map.computeIfPresent("score", (k, v) -> 90)); // 返回值为null
System.out.println(map); // map:{age=25, score=90}
// compute:可定制的计算,computeIfPresent的增强版,可以获得现有的k,v对齐进行操作
System.out.println(map.compute("age", (k, v) -> (Integer) v + 1)); // 返回值为26
System.out.println(map); // map:{age=26, score=90}
// merge:合并。如果存在旧值,则使用BiFunction进行替换;否则赋予新值
System.out.println(map.merge("score", 80, (oldValue, newValue) -> (Integer) oldValue + newValue)); // 返回值为170
System.out.println(map); // map:{age=26, score=170}
// 合并一个不存在的键值对
System.out.println(map.merge("height", 180, (oldValue, newValue) -> (Integer) oldValue + newValue)); // 返回值为180
System.out.println(map); // map:{age=26, score=170, height=180}
HashMap和LinkedHashMap没有自己独有的方法,只是实现了Map接口。
TreeMap还实现了NavigableMap->SortedMap,参考NavigableMap Api (opens new window)
# 5、其他
Queue接口定义了队列,有offer、poll、peek等队列专有方法。主要实现类有PriorityQueue(优先级队列)。
Deque继承自Queue,定义了双端队列,有offer/poll/peek-First/Last等系列方法,主要实现类是ArrayDeque、LinkedList。
Stack是Java中栈的实现类,有push、pop、peek等专有方法。
# Apache Commons Collection集合
Apache Commons系列工具包是Java开发中常用的工具包,其中关于集合方面的扩展要使用到commons-collections4:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
此工具工具包主要提供了三方面的扩展
# 1、工具类
主要是SetUtils、ListUtils、CollectionUtils、MapUtils。这些工具类有一些方法在Java8之后有点过时了,比如:
List<Integer> list = List.of(1, 2, 3, 4);
// select方法
List<Integer> select = ListUtils.select(list, e -> e > 2);
// java8之后这么做
select = list.stream().filter(e->e>2).collect(Collectors.toList());
其他诸如filter、transform、getCardinalityMap等方法,用Stream操作会更简单一些。
下面列出一些提高效率的便利方法:
subtract
:取得差集disjunction
:获得彼此差集之后的并集intersection
:取得交集union
:取得并集containsAny
:包含任何permutations
:返回列表的所有排列组合,大小是列表大小的阶乘级别,注意性能和内存问题
# 2、容器类
Bag 会记录对象在集合中出现的次数,用法:
Bag bag = new HashBag();
bag.add("ONE", 6); // 添加6次
bag.remove("ONE", 2); // 删除2次
bag.getCount("ONE"); // 返回4
主要实现类有HashBag和TreeBag(自然排序)
List的扩展,主要有如下类:
CursorableLinkedList
:提供了可修改的迭代器。在JDK的List.listIterator中提供了类似功能FixedSizeList
:固定大小的List,add、remove、clear和retain操作是不被支持的,set方法是允许的但是不会影响列表大小GrowthList
:可以使其在因set或add操作造成索引超出异常时无缝的增加列表长度,可以避免大多数的IndexOutOfBoundsExceptionLazyList
:可以自动增长,但只发生在get时--允许指定一个Factory获得超出索引位置的值PredicatedList
:添加元素时进行校验SetUniqueList
:不允许重复元素的列表TransformedList
:在set和add时对元素进行转换后再添加TreeList
:实现了一个快速插入、删除,同时查询性能也可观的List
看完之后,怎么说呢?这些类用的频率都不太高,但又弃之可惜。
Set的扩展有如下类:
CompositeSet
:多个Set的组合视图- 剩余的Predicated、Transformed、Unmodifiable和List一样,不赘言
Map的扩展有如下类:
CaseInsensitiveMap
:大小写不敏感的Map,会把键都转换为小写CompositeMap
:多个Map的组合视图DefaultedMap
:键不存在,返回默认对象FixedSizeMap
:固定大小的MapIdentityMap
:匹配键值不是通过==,而是equalsMultiKeyMap
:允许多个键关联到一个值上MultiValueMap
:允许多个值关联到一个键上BidiMap
:双向Map,可以通过值查找键。主要实现类是DualHashBidiMap、TreeBidiMapLRUMap
:固定大小Map,容器满了之后删除最近最少使用(Least Recently Used)的元素- LazyMap/PredicatedMap/TransformedMap/TypedMap/UnmodifiableMap,逻辑同List扩展一样,不赘言
# 3、辅助类
主要是Comparator、Predicate、Transformer等辅助类,这些辅助类在Java8 Stream面前都显得有些过时了,不赘言
# Guava集合
如果说commons-collections4是质朴村花的话,那么Guava就是靓丽女神了。
Guava中最受欢迎的就是他的集合类。
# 1、不可变系列对象
比如ImmutableList、ImmutableSet,比如JDK内部的Collections.unmodifiable系列方法来说,具有如下优点:
- 线程安全
- 更加节省空间
需要注意的是,所有Immutable都拒绝空值。
使用:
ImmutableSet.of("red", "orange", "yellow")
Set<Bar> bars = ...
ImmutableSet.copyOf(bars)
ImmutableSet.<Color>builder().addAll(WEBSAFE_COLORS).add(new Color(0, 191, 255)).build();
# 2、新的集合类型
Multiset,定义一个集合,该集合计算对象在集合中出现的次数。
主要方法有add、remove、getCount,使用起来比较简单:
MultiSet<Object> multiSet = new HashMultiSet<>();
multiSet.add("name");
multiSet.add("name");
multiSet.add("name", 3);
multiSet.getCount("name"); // 5
Multimap,一个键可以关联多个值。
初始化:
ListMultimap<String, Integer> multimap1 = MultimapBuilder.hashKeys().arrayListValues().build();
SetMultimap<String, Integer> multimap2 = MultimapBuilder.treeKeys().hashSetValues().build();
使用:
multimap.put("type",1);
multimap.putAll("type", Lists.newArrayList(2,3));
List<Integer> types = multimap.get("type");
Map<String, Collection<Integer>> map = multimap.asMap();
BiMap,双向Map。在commons-collections4有类似实现
BiMap<String, Integer> userId = HashBiMap.create();
...
String userForId = userId.inverse().get(id);
Table,一个索引映射多个值,类似Map<FirstName, Map<LastName, Person>>
这样的结果
用法:
Table<String, String, Double> table = HashBasedTable.create();
table.put("cny", "usd", 0.235);
table.put("cny", "jpy", 8.523);
table.put("hkd", "jpy", 2.053);
table.get("cny", "usd"); // 0.235
table.row("cny"); //{usd=0.235, jpy=8.523}
table.column("jpy"); //{cny=8.523, hkd=2.053}
table.rowKeySet(); //cny, hkd
RangeSet,包含范围的集合。
用法:
RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 10)); // {[1, 10]}
rangeSet.add(Range.closedOpen(11, 15)); // disconnected range: {[1, 10], [11, 15)}
rangeSet.add(Range.closedOpen(15, 20)); // connected range; {[1, 10], [11, 20)}
rangeSet.add(Range.openClosed(0, 0)); // empty range; {[1, 10], [11, 20)}
rangeSet.remove(Range.open(5, 10)); // splits [1, 10]; {[1, 5], [10, 10], [11, 20)}
Set<Range<Integer>> ranges = rangeSet.asRanges();
RangeMap,包含范围的Map。与RangeSet不同的是它不合并相邻映射。
用法:
RangeMap<Integer, String> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closed(1, 10), "foo"); // {[1, 10] => "foo"}
rangeMap.put(Range.open(3, 6), "bar"); // {[1, 3] => "foo", (3, 6) => "bar", [6, 10] => "foo"}
rangeMap.put(Range.open(10, 20), "foo"); // {[1, 3] => "foo", (3, 6) => "bar", [6, 10] => "foo", (10, 20) => "foo"}
rangeMap.remove(Range.closed(5, 11)); // {[1, 3] => "foo", (3, 5) => "bar", (11, 20) => "foo"}
# 3、实用工具类
比如Lists、Sets等,你可能想象不到,Guava中使用频率最高的是如下代码:
List<Integer> integers = Lists.newArrayList(2, 3, 4);
不由得令人感慨,偷懒是最高生产力。
这些工具类中方法众多,具体参考:Guava 集合工具扩展 (opens new window)
这里列出一些使用频率较高,令人眼前一亮的工具扩展:
Lists.partition
:分割列表。返回连续子列表,每个子列表大小系统(最后一个可能更小)Sets.subSet
:提取范围内的子视图Maps.subMap
:提取范围内的子视图
# 并发集合
前面说道不推荐使用Collections.synchronized系列方法,原因是它内部只是提供了一个简单的互斥操作,在高并发情况下,并不十分高效。
Java 5之后利用CAS指令(乐观锁)提供了更高效的并发容器,它们分别是:
ConcurrentLinkedQueue
:高并发环境下性能最好的队列CopyOnWriteArrayList
:只在写时加锁的ArrayList,采用写时复制的思想,读性能大幅提升BlockingQueue
:一个阻塞式的数据共享通道,参考:阻塞队列BlockingQueue。他有三个主要实现:ArrayBlockingQueue
:数组支持的有界队列LinkedBlockingQueue
:链表支持的可选有界队列LinkedBlockingDeque
:链表支持的双向可选有界队列
ConcurrentMap
:线程安全的map,主要实现有:ConcurrentHashMap
:高并发哈希表ConcurrentSkipListMap
:跳表,类似LinkedHashMap,提供有序的遍历。类似于平衡树,特点是插入和删除只需要对局部数据进行操作即可。平衡树的话需要全局锁,而跳表只需要部分锁。
# Redission分布式集合
缓存作为提升性能和体验的必要手段,使用非常广泛。
在同一个JVM内部,使用ConcurrentMap作为缓存是常见手段。
在分布式环境中,可能需要集中式缓存服务,目前比较流行的是Redis,它提供了多种数据结果供我们使用。
Redission作为Redis客户端库,更进一步的把原生Redis数据结构封装成了Java大家熟悉的集合容器。
以Map举例:
RMap<String, SomeObject> map = redisson.getMap("anyMap");
SomeObject prevObject = map.put("123", new SomeObject());
SomeObject currentObject = map.putIfAbsent("323", new SomeObject());
SomeObject obj = map.remove("123");
RFuture<SomeObject> putAsyncFuture = map.putAsync("321");
它提供了同步和异步的添加和删除方法。
Redission还对分布式环境下常用操作做了一些封装,这已经超出了本章要讨论的内容,参考:Redisson项目介绍 (opens new window)
# 总结
本文对Java中的集合进行了讨论,主要讲到JDK内部集合类、Apache Commons Collection中的集合类、Guava中的集合类、并发集合类、Redission分布式集合类的使用。
就我个人经验来说,学习一门语言不难,难的是对语言中的各种工具库、编程范式做到信手拈来、熟练使用,达到这一步,工作效率会有一个质的提升。
当然,实际中也不提倡死记硬背,只需要在脑海中有一副地图,这样遇到问题的时候能快速的索引到对应的位置,多使用多历练,慢慢就成为一个高手。
祝你变得更强!