一、java容器类简介
java容器类用途是保存对象(不能存储基本类型,基本类型可以通过自动装箱和拆箱完成),包括List、Set、Queue和Map,将其划分为两个不同的概念(在java中都是通过接口来实现的):
Collection:一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,Set不能有重复元素(通过比较hashcode和equals方法)但是也没有顺序,Queue按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
1. collection的初始化
new初始化参数
addAll
Collections.addAll(两个参数)
Arrays.asList
- 首选方式:构建一个空的Cllection,然后调用Collections.addAll(), Arrays.asList构造产生的list可以作为参数传递给上面三种初始化方法,这样可以生成允许使用所有方法的普通容器。
- addAll()只能接收另一个Collection对象作为参数,没有Arrays.asList()和Collections.addAll()灵活,但是比new的时候传入Arrays.asList()参数运行速度要快很多。
- Collections.addAll()方法接受的参数:一个Collection对象以及一个数组或者一个用逗号分隔的元素列表(从Collection对象了解到目标类型,不会产生类型错误)
- Arrays.asList()方法接受的参数:一个数组,一个用逗号分隔的元素列表(可能是对象,但是仅能接收直接子类);
Arrays.asList()产生的List对象会使用底层数组作为其物理实现,是一个浅拷贝,任何会引起底层数据结构的尺寸进行修改的方法(add, remove, clear)都会产生一个UnsupportedOperationException.
说明:asList的返回对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍然是数组。
String[] str = new String[] { "you", "wu" };
List list = Arrays.asList(str);
第一种情况:list.add("yangguanbao"); 运行时异常。
第二种情况:str[0] = "gujin"; 那么list.get(0)也会随之修改。
2. 打印输出
数组需要使用Arrays.toString()来产生数组的可打印表示,容器无需任何帮助可直接打印(容器默认会提供toString()方法),打印输出
List/Set: [rat, cat, dog, dog]
Map: {dog=Spot, cat=Rags, rat=Fuzzy}
3. 使用泛型的好处
- 将运行时错误变为编译期错误,防止在容器中插入错误的对象类型
- 获取到的对象类型可以直接使用,无需强转换
- 泛型不仅限于只能将确切类型的对象放置到容器中,向上转型也可以像作用于其他类型一样作用于泛型
4. 和数组对比
- 数组具有固定尺寸,不支持泛型
- 容器类可以自动调整尺寸,支持泛型
5. 迭代器
迭代器是一个轻量级对象,不关心java容器类的具体类型,具有以下限制(仅针对Collection对象):
1,使用方法iterator()要求容器类返回一个Iterator, Iterator将准备好返回第一个元素。
2,使用next()获取序列中的下一个元素
3,使用hasNext()检查序列中是否还有元素
4,使用remove()将迭代器新近返回的元素删除
ListIterator是一个更强大的Iterator的子类型,只能用于各种List类的访问,通过调用listIterator()方法产生一个指向List开始处的ListIterator,还可以通过listIterator(n)方法产生一个指向列表索引为n的ListIterator。
所有方法:next() previous() hasNext() hasPrevious() remove() nextIndex() previousIndex()
Foreach与迭代器
1,foreach语法主要用于数组,但是它也可以用于任何Collection对象(因为Iterable接口被foreach用来在序列中移动,因此任何实现了Iterable的类,都可以将它用于foreach语句中)
2,foreach语法用于数组和其他任何Iterable,但是并不意味着数组肯定也是一个Iterable,而且任何自动包装也不会自动发生(不存在任何从数组到Iterable的自动转换)
二、List: ArrayList, LinkedList
所有操作方法:
add(object) add(index, object) addAll(list) addAll(index, list) contains(object) containsAll(list): 和顺序无关 remove(object or index) removeAll(list): 只移除特定元素,不会重复移除 get(index) indexOf(object) subList(fromIndex, toIndex) Collections.sort(list) Collections.shuffle(list, rand): Mix it up retainAll(list): 取交集 set(index, object): replace an element isEmpty() clear() toArray(new Pet[list.size()]): 将任意的Collection转换为一个数组,这是一个重载方法,无参数返回的是Object数组,若强转其它类型数组将出现 ClassCastException 错误;参数为目标类型的数据,将产生指定类型的数据(假设能通过参数类型检查),如果参数数组太小,存放不下List中的所有元素,toArray()方法将创建一个具有合适尺寸的数组;如果数组元素大于实际所需,下标为[ list.size() ]的数组 元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素 个数一致。
- ArrayList:
- 优点:随机访问元素比较快
- 缺点:在List的中间插入和移除元素比较慢
使用ArrayList相当简单:创建一个实例,用add()插入对象,然后用get()访问这些对象
如果ArrayList没有使用泛型,则默认保存Object类型的对象:
ArrayList<T> myArrayList = new ArrayList<T>();
- LinkedList:
- 优点:在List的中间插入和移除元素比较快,包含的操作比ArrayList多;
- 缺点:随机访问比较慢
注: 两者都是线程不安全的
- 线程安全
1,CopyOnWriteArrayList:
public boolean add(E e) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}
}
2,Collections.synchronizedList:
List<Integer> list2 = Collections.synchronizedList(new ArrayList<Integer>());
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
SynchronizedList的listIterator操作没有加锁,其他操作都加了synchronized锁,锁对象就是其自身
3,两者适用场景
CopyOnWriteArrayList,发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主,读操作远远大于写操作的场景中使用,比如缓存。而Collections.synchronizedList则可以用在CopyOnWriteArrayList不适用,但是有需要同步列表的地方, 读写操作都比较均匀的地方。
- 栈:
方法:push pop peek
- 队列:
除了并发应用,Queue在Java SE5中仅有的两个实现是LinkedList和PriorityQueue,它们的差异在于排序行为而不在于性能。
LinkedList实现了Queue接口,因此LinkedList可以看作是Queue的一种实现
方法:
offer:往队列添加元素如果队列已满直接返回false,队列未满则直接插入并返回true
add: 往队列添加元素如果队列已满抛异常IllegalStateException:,队列未满则直接插入并返回true,如果用在空间有限制的情况下用add方法
poll:获取并且移除队头元素,如果队列为空则返回null
remove: 获取并且移除队头元素,如果队列为空则抛异常:NoSuchElementException
peak:获取队头元素,如果队列为空则返回null
element:获取队头元素,如果队列为空则抛异常:NoSuchElementException
private List<Runnable> drainQueue() {
BlockingQueue<Runnable> q = workQueue;
ArrayList<Runnable> taskList = new ArrayList<Runnable>();
// 调用BlockingQueue的drainTo()方法转移元素
q.drainTo(taskList);
if (!q.isEmpty()) {
// 一个一个地转移元素
for (Runnable r : q.toArray(new Runnable[0])) {
if (q.remove(r))
taskList.add(r);
}
}
return taskList;
}
PriorityQueue
完全二叉树有规律,所以它可以用一个数组表示而不需要使用链表。
List容器用于查找效率很低,因此Collections API提供了两个附加容器Set和Map,它们对插入,删除和查找等基本操作提供有效实现。
三、Set: HashSet, TreeSet, LinkedHashSet
Set:具有与Collection完全一样的接口,没有额外的功能,实际上Set就是Collection,只是行为不同。加入Set的元素必须定义equals()方法以确保对象的唯一性
HashSet: 最快获取元素的方式,必须定义hashCode(),存储在散列表,作用:常用来去重
TreeSet:按照比较结果的升序保存对象,将元素存储在红黑树数据结构中
LinkedHashSet:按照被添加的顺序保存对象,因为查询速度的原因也使用了散列
SortedSet:
Object first()返回容器的第一个元素
Object last()返回容器的最后一个元素
SortedSet subSet(fromElement, toElement)生成此Set的子集,范围从fromElement(包含)到toElement(不包含)
SortedSet headSet(toElement)生成此Set的子集,由小于toElement(不包含)的元素组成
SortedSet tailSet(fromElement)生成此Set的子集,由大于或等于fromElement的元素组成
对于良好的编程风格,在覆盖equals()方法时,总是同时覆盖hashCode()方法。
默认情况下,排序假设TreeSet中的项实现Comparable接口,另一种排序可以通过用Comparator实例化TreeSet来确定。
ComParable和ComParator的区别:
- Comparable和Comparator都是用于比较数据的大小的,实现Comparable接口需要重写compareTo方法,实现Comparator接口需要重写compare方法,这两个方法的返回值都是int,用int类型的值来确定比较结果
- 在Collections工具类中有一个排序方法sort,此方法可以只传一个集合,另一个重载版本是传入集合和比较器,前者默认使用的就是Comparable中的compareTo方法,后者使用的便是我们传入的比较器Comparator,java的很多类已经实现了Comparable接口,比如说String,Integer等类。
- Comparator接口中方法很多,但是我们只需要实现一个,也是最重要的一个compare,也许有的人会好奇为什么接口中的方法可以不用实现,因为这是JDK8以后的新特性,在接口中用default修饰的方法可以有方法体,在实现接口的时候可以不用重写,可以类比抽象类。
java中大部分我们常用的数据类型的类都实现了Comparable接口,而仅仅只有一个抽象类RuleBasedCollator实现了Comparator接口 ,还是我们不常用的类,这并不是要用Comparable而不要使用Comparator,在设计初时有需求就选择Comparable,若后期需要扩展或增加排序需求,再增加一个比较器Comparator,毕竟能写Collections.sort(arg1),没人乐意写Collections.sort(arg1,arg2)。
Collections.sort(fileList, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
if (f1.isDirectory() && f2.isFile()) {
return -1;
}
if (f1.isFile() && f2.isDirectory()) {
return 1;
}
return f1.getName().compareTo(f2.getName());
}
});
四、Map: HashMap(默认使用), TreeMap, LinkedHashMap
HashMap:提供最快查找技术,基于散列表实现(取代了Hashtable),插入和查询“键值对”的开销是固定的,可以通过构造器设置容量和负载因子,以调整容器的性能。
默认负载因子:0.75f
初始capacity:16, 但是在开发过程中初始化以后的size为0
初始化HashMap时传入的尺寸大小:expectedSize / 0.75f + 1.0f (HashMap默认会调整为传入参数最近的2的整数次幂)
扩容:默认为前一个数组大小的2倍
HashMap对象的key、value值均可为null(最多只允许一条记录的key为null,可以允许多个值为null);HahTable对象的key、value值均不可为null。
重新调整HashMap大小存在什么问题吗
在多线程的情况下,可能产生条件竞争(race condition)。如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了
为什么String, Interger这样的wrapper类适合作为键
String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。(1)因为String是不可变的,也是final的,(2)而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
散列表操作中费时多的部分就是计算hashCode方法,String类中的hashCode方法包含一个重要的优化:(3)每个String对象内部都存储它的hashCode值,该值初始为0,但若hashCode被调用,那么这个值就被记住。因此,如果hashCode对同一个String对象被第二次计算,则可以避免昂贵的重新计算。这种技巧叫作闪存散列代码。
可以使用自定义的对象作为键吗
这是前一个问题的延伸。当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。
可以使用ConcurrentHashMap来代替Hashtable吗
Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。
在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作。不支持存入null的key和value,否则发生空指针异常
TreeMap:基于红黑树的实现。TreeMap是唯一带有subMap()方法的Map,它可以返回一个子树。
LinkedHashMap:按照插入顺序保存键,同时还保留了HashMap的查询速度。
WeakHashMap: 弱键(weak key)映射,允许释放映射所指向的对象,这是为解决某类特殊问题而设计的。如果映射之外没有引用指向某个“键”,则此键可以被垃圾回收器回收。
IdentifyHashMap:使用==代替equals()对“键”进行比较的散列映射,专门为解决特殊问题而设计的。
方法:
生成一个Collection: keySet() values() entrySet()
isEmpty, clear, size, containsKey, containsValue, get, put, putAll,
remove, map.keySet().removeAll(map.keySet()): 该操作会导致map变空,由于这些Collection背后是由Map支持的,所以对Collection的任何改动都会反映到与之相关联的Map(values/clear效果相同)
entrySet()使用了HashSet来保存键-值对。
方法put将键值对置入Map中,或者返回null,或者返回与key相联系的旧值
通过一个Map进行迭代要比Collection复杂,因为Map不提供迭代器,而是提供3种方法,将Map的对象的视图作为Collection对象返回。这些视图本身就是Collection,因此它们可以被迭代。
Set<KeyType> keySet()
Collection<ValueType> values()
Set<Map.Entry<KeyType, ValueType>> entrySet()
HashMap的初始化:https://blog.youkuaiyun.com/dujianxiong/article/details/54849079
Map<String, String> map = new HashMap<>();
注:
1,对于集合类型的静态成员变量,应该使用静态码块赋值,而不是使用集合实现
2,用ImmutableMap.of需要慎重,再做put操作会报UnsupportedOperationException
- SparseArrayCompat:
- 是SparseArray的兼容版本,可以在比较低的手机上运行
- SparseArrayCompat()其实是一个map容器,它使用了一套算法优化了hashMap,可以节省至少50%的缓存.
- 缺点但是有局限性只针对下面类型.
key: int; value: object
private static final SparseArrayCompat<ViewHolderFactory> FACTORIES = new SparseArrayCompat<>()
static {
FACTORIES.put(TYPE_FOOTER, new FooterViewHolder.Factory());
// ...
}
- LongSparseArray
key: long; value: object
- ArrayMap:
ArrayMap是Android专门针对内存优化而设计的,用于取代Java API中的HashMap数据结构,非线程安全。使用场景:
1.数据量不大,最好在千级以内;2.数据结构类型为Map类型
- 容器空类型:
以Map为例,List和Set类似。
Map myMap = Collections.emptyMap();
优点:
1,new 一个空的 Map ,而这个 Map 以后也不会再添加元素,否则throw new UnsupportedOperationException()。
2,Collections.emptyMap相比于new HashMap<>()不用分配内存空间,而且多次调用返回同一个static对象。
3,比如说一个方法返回类型是Map,当没有任何结果的时候,返回null,有结果的时候,返回Map集合列表。那样的话,调用这个方法的地方,就需要进行null判断。使用emptyMap这样的方法,可以方便方法调用者。返回的就不会是null,省去重复代码。
4,如果直接使用myMap,会报错:throw new UnsupportedOperationException(),所以使用之前需要初始化,分配内存空间:myMap = new HashMap<>(); 或者指向另一个HashMap.
五、散列与散列码
Object的hashCode()方法默认使用对象的地址计算散列码。如果要使用自己的类作为HashMap的键,必须同时重载hashCode()和equals()。散列码不必是独一无二的,但是通过hashCode()和equals()必须能够完全确定对象的身份。
散列函数具有以下两种性质:
1,散列函数必须可在常数时间(即与表中项的个数无关)内计算。
2,散列函数必须将各项均匀分布在数组单元中。
提升查询速度:
1,保持键的排序状态,然后使用Collections.binarySearch()进行查询,时间复杂度为O(log(n))。
2,散列更进一步,散列是一种以常数平均时间执行插入、删除和查找的技术,但是不支持元素间任何排序信息的树操作。对于HashMap,将键用数组保存(存储一组元素最快的数据结构是数组),数组并不保存键本身,而是通过键对象生成一个数字,将其作为数组的下标,这个数字就是散列码。数组并不直接保存值,而是保存值的list。