一.理解集合
- Collection:List、Set
- Map:Hashtable、HashMap、WeakHashMap
集合类型主要有3种:set(集)、list(列表)和map(映射)。
1.collection接口
Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。JDK提供的类都是继承自Collection的“子接口”(List和Set)。
遍历Collection中的每一个元素:使用iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。
Iterator it = collection.iterator(); // 获得一个迭代子
while(it.hasNext()) {
Object obj = it.next(); // 得到下一个元素
}
boolean add(E o) 确保此 collection 包含指定的元素(可选操作)。 | |
boolean addAll(Collection<? extends E> c) 将指定 collection 中的所有元素都添加到此 collection 中(可选操作。 | |
void clear() 移除此 collection 中的所有元素(可选操作)。 | |
boolean contains(Object o) 如果此 collection 包含指定的元素,则返回 true。 | |
boolean containsAll(Collection<?> c) 如果此 collection 包含指定 collection 中的所有元素,则返回true。 | |
boolean equals(Object o) 比较此 collection 与指定对象是否相等。 | |
int hashCode() 返回此 collection 的哈希码值。 | |
boolean
isEmpty() 如果此 collection 不包含元素,则返回true。 | |
Iterator<E> iterator() 返回在此 collection 的元素上进行迭代的迭代器。 | |
boolean remove(Object o) 从此 collection 中移除指定元素的单个实例,如果存在的话(可选操作)。 | |
boolean removeAll(Collection<?> c) 移除此 collection 中包含在指定 collection 中的所有元素(可选操作)。 | |
boolean retainAll(Collection<?> c) 保留此 collection 中包含在指定 collection 的元素(可选操作)。 | |
int size() 返回此 collection 中的元素数。 | |
Object[] toArray() 返回包含此 collection 中所有元素的数组。
|
1.1 List接口
List
接口继承了 Collection
接口以定义一个允许重复项的有序集合。
list的特殊方法List subList(int fromIndex, int toIndex)
处理subList()
时,位于fromIndex
的元素在子列表中,而位于toIndex
的元素不是。
Iterator:只能正向遍历集合,适用于获取移除元素。ListIerator:继承Iterator,可以双向列表的遍历,同样支持元素的修改。
list方法举例:
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class TestSet {
public static void main(String[] args) {
List<Integer> list=new LinkedList<Integer>();
for(int i=0;i<=9;i++){
list.add(i);
}
Collections.shuffle(list);//随机排序
System.out.println("shuffle:"+list);
Collections.reverse(list);
System.out.println("reverse:"+list);//原来list序列基础上逆序输出
Collections.sort(list);
System.out.println("sort:"+list);//正序输出
//折半查找
System.out.println(Collections.binarySearch(list, 4));
}
}
输出结果:
shuffle:[6, 2, 7, 3, 8, 4, 9, 0, 5, 1]
reverse:[1, 5, 0, 9, 4, 8, 3, 7, 2, 6]
sort:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
4
list接口扩容机制
集合是我们在Java编程中使用非常广泛的,容器的量变得非常大的时候,它的初始容量就会显得很重要了,因为扩容是需要消耗内存的。同样的道理,Collection的初始容量也显得异常重要。所以:对于已知的情景,请为集合指定初始容量。
举例:
import java.util.ArrayList;
import java.util.List;
public class User {
private String userName;
private Integer age;
public User(Integer age,String userName){
this.age=age;
this.userName=userName;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public static void main(String[] args) {
User user = null;
long begin1 = System.currentTimeMillis();
List<User> list1 = new ArrayList<User>();//不指定初始容量
for(int i = 0 ; i < 100000; i++){
user = new User(i,"caka"+i);
list1.add(user);
}
long end1 = System.currentTimeMillis();
System.out.println("list1 time:" + (end1 - begin1));
long begin2 = System.currentTimeMillis();
List<User> list2 = new ArrayList<User>(100000); //指定初始容量
for(int i = 0 ; i < 100000; i++){
user = new User(i,"caka"+i);
list2.add(user);
}
long end2 = System.currentTimeMillis();
System.out.println("list2 time:" + (end2 - begin2));
}
}
输出结果:
list1 time:43
list2 time:16
从上面的运行结果可以看出ArrayList的扩容机制是比较消耗资源的
ArrayList每次新增一个元素,就会检测ArrayList的当前容量是否已经到达临界点,如果到达临界点则会扩容1.5倍。ArrayList不是线程安全的,只能用在单线程环境下。
线程不安全验证举例:
add方法的底层实现
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
size++这边,主要分为两个步骤:1)将add的元素放到size位置;2)将size加1
假设size=10.若线程A在10位置存放了值caka,获得size=10,但还没来得及将size加1写入主存。此时线程B在也在10位置存放了值july,也获得size=10,而后A、B分别将size加1后写入主存,size=11,即两个线程执行两次add()后size只加了1。
举例如下:
import java.util.ArrayList;
public class User {
static ArrayList<String> list = new ArrayList<String>();
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
list.add("caka");
}
}).start();
}
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("size:"+list.size());
}
}
}
输出结果应该是20,但是实际却可能小于20.ArryList在多线程环境下不安全
参考资料说要想实现线程安全需要这样写:List<String> list = Collections.synchronizedList(new ArrayList<String>())
对于List中常用的ArrayList的总结:
- 无参构造方法构造的ArrayList的容量默认为10
- ArrayList在每次增加元素,当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1。如果设置后的新容量还不够,则直接新容量设置为传入的参数,再用Arrays.copyof()方法将元素拷贝到新的数组。
从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则使用LinkedList。
- 在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理,ArrayList中允许元素为null。
- LinkedList类允许为空,是非同步的;如果要实现同步,需要自己在创建List的时候构造一个同步List。(List list = Collections.synchronizedList(new LinkedList(....)));
- ArraryList实现了可变大小的数组。允许所有元素,包括null,是非同步的。每个ArraryList实例都有一个容量,用于存储元素数组的大小。该容量可随着不断增加新元素而自动增加。每次增加为原来的一半。
- Vector类类似与ArraryList类。但Vetor是同步的。所以当一个Iterator被创建而且被使用的时候,另一个线程改变了Vetor的状态。比如增加或者删除元素。这时条用Iterator的方法就会抛出ConcurrentModificationException. 需要捕获该异常。Vector数据增长每次增长为原来的一倍。
元素。
1.2 set接口
set接口不允许集合中存在重复项。具体的Set
实现类依赖添加的对象的 equals()
方法来检查等同性。“集合框架”支持 Set
接口两种普通的实现:HashSet
和TreeSet,添加到 HashSet
的对象需要采用恰当分配散列码的方式来实现hashCode()
方法。添加到TreeSet
的元素必须是可排序的,必须实现Comparable
set集合实例:
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public class TestSet {
public static void main(String[] args) {
Set<String> set = new HashSet<String>();
set.add("旺仔牛奶");
set.add("旺仔牛奶");//重复性验证,结果只输出一次
set.add("b旺仔牛奶");
set.add("a旺仔牛奶");
set.add("c旺仔牛奶");
set.add("d旺仔牛奶");
System.out.println(set);
Set<String> sortedSet = new TreeSet<String>(set);
System.out.println(sortedSet);//排序输出
}
}
输出结果:
[b旺仔牛奶, 旺仔牛奶, d旺仔牛奶, c旺仔牛奶, a旺仔牛奶]
[a旺仔牛奶, b旺仔牛奶, c旺仔牛奶, d旺仔牛奶, 旺仔牛奶]
1.3 queue接口
Queue继承了Collection接口。LinkedList实现了Queue接 口。Queue用于模拟队列。通常是指“先进先出(FIFO)”。新元素插入到队列的尾部,取出元素会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。
Queue接口中定义了如下的几个方法:
add
put
offer
remove
poll
take
element
peek
阻塞队列
java.ulil.concurrent包有阻塞队列的4个变种。
LinkedBlockingQueue:LinkedBlockingQueue的容量默认情况下为Integer.MAX_VALUE,但是也可以选择指定其最大容量,它是基于链表的队列,此队列按 FIFO(先进先出)排序元素。
ArrayBlockingQueue 在构造时需要指定容量, 并可以选择是否需要公平性,如果公平参数被设置true,等待时间最长的线程会优先得到处理。
PriorityBlockingQueue是一个带优先级的 队列,而不是先进先出队列。元素按优先级顺序被移除,PriorityQueue保存队列元素的顺序不是按照元素添加的顺序来保存的,而是在添加元素的时候对元素的大小排序后再保存的。因此在PriorityQueue中使用peek()或pool()取出队列中头部的元素,取出的不是最先添加的元素,而是最小的元素。该队列也没有上限有容量限制的,与ArrayList一样,所以在优先阻塞 队列上put时是不会受阻的。虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会导致 OutOfMemoryError,但是如果队列为空,那么取元素的操作take就会阻塞,所以它的检索操作take是受阻的。
package example;
import java.util.PriorityQueue;
public class PriorityQueueTest {
public static void main(String[] args){
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.offer(6);
priorityQueue.add(-3);
priorityQueue.add(20);
priorityQueue.offer(18);
System.out.println(priorityQueue);
}
}
输出结果:
[-3, 6, 20, 18]
DelayQueue(基于PriorityQueue来实现的)是一个存放Delayed 元素的无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且poll将返回null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,则出现期满,poll就以移除这个元素了。此队列不允许使用 null 元素。放入DelayQueue的元素还将要实现compareTo方法,DelayQueue使用这个来为元素排序。
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
2.map接口
该接口描述了从不重复的键到值的映射。键和值都可以为 null
。但是,不能把Map
作为一个键或值添加给自身。
相关方法:
Object put(Object key, Object value)返回值是被替换的值 |
Object remove(Object key) |
void putAll(Map mapping) |
void clear() |
Object get(Object key)返回指定键关联的值 |
boolean containsKey(Object key)包含指定键的映射返回true |
boolean containsValue(Object value)此 Map 将一个或多个键映射到指定值,返回 true |
int size() |
boolean isEmpty() |
集合框架提供两种常规的Map
实现:HashMap
和TreeMap。在
Map
中插入、删除和定位元素,HashMap
是最好的选择,(使用HashMap
要求添加的键类明确定义了hashCode()
实现)如果要按顺序遍历键,那么TreeMap
会更好。根据集合大小,先把元素添加到HashMap
,再把这种映射转换成一个用于有序键遍历的TreeMap
。
1.使用entries来遍历(如果遍历的是一个空的map对象,for-each循环将抛出NullPointerException,因此在遍历前你总是应该检查空引用。)
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
2.通过keySet或values来实现遍历
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
//遍历map中的键
for (Integer key : map.keySet()) {
System.out.println("Key = " + key);
}
//遍历map中的值
for (Integer value : map.values()) {
System.out.println("Value = " + value);
}
3.使用Iterator遍历
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry<Integer, Integer> entry = entries.next();
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
补充一种:通过Map.value()遍历所有value,但不能遍历Key.
for(Integer v : map.values()) {
System.out.println("value =" + v);
}
注意点:
由于作为Key的对象将通过计算其散列函数来确定与之相对应的value的位置。所以作为Key的对象都必须实现hashCode和equals方法。这两种方法继承自根类Object,如果需要自定义的类当作key的话,散列函数的定义是:如果两个对象相同,比如object1.equals(object2)= true; 则他们的hashCode必须相同。但如果两个对象不同,他们的hashCode不一定不同。
如果相同的对象有不同的hashCode,哈希表的操作会出现get方法返回null.所以一定要同时复写equals和hashCode方法。
hashCode()缺省情况下返回的是对象的内存地址。所以每个java对象都可以生成hashCode;equals是根类的方法,比较两个对象地址是否相同。
HashMap类:
HashMap允许为null。是非同步的,HashMap的迭代器(Iterator)是fail-fast迭代器。如果某个集合对象创建了Iterator或者ListIterator,然后其它的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。HashMap使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当给put()方法传递键和值时,先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。hashMap使用containsKey和containsValue判断是否包含key值或者value值。Java 5提供了ConcurrentHashMap,线程安全。
HashTable类:
HashTable不允许null值。是同步的。使用Enumeration作为迭代器。速度更慢。