如果一个程序只包含固定数量且其生命期都是已知的对象,那么这是一个非常简单的程序。
“容器”(List、set、Map)提供了完善的方法来保存对象,并且保存数量巨大。
java中常用的集合框架体系图如下图所示,之后用到的再另作说明。
各种集合的特点
Collection(单列集合)
List(有序,可重复)
ArrayList
ArrayList
:底层结构式数组、查询快、增删慢;线程不安全
为什么说ArrayList查询快、增删慢?
数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要讲已经有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
Vector
Vector
:底层结构式数组,查询快、增删慢;线程安全
LinkedList
LinkedList
:底层结构式链表、查询慢、增删快;线程不安全
LinkedList
添加了可以使其用作栈、队列或双端队列的方法
getFirst()
和element()
完全一样,都返回列表的头(第一个元素),列表为空抛出异常。
removeFirst()
和remove()
也完全一样,它们移除并返回列表的头,列表为空抛出异常,poll()
稍有差异,返回null
addFirst()
添加到列表的首端,与add()
和addLast()
相反,后两个方法将某个元素插入列表的尾端。
removeLast()
移除并返回列表最后一个元素。
Set(无序,不重复)
HashSet
HashSet
:
底层数据结构是哈希表。
哈希表依赖两个方法:hashCode()和equals()
HashSet
的实现原理
HashSet中不允许有重复元素,这是因为HashSet是基于HashMap实现的。
HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个private static final Object PRESENT = new Object();。HashSet跟HashMap一样,都是一个存放链表的数组。
HashSet中add方法调用的是底层HashMap中的put()方法。而如果是在HashMap中调用put,首先会判断key是否存在,如果key存在则修改value值,如果key不存在这插入这个key-value。而在set中,因为value值没有用,也就不存在修改value值的说法,因此往HashSet中添加元素,首先判断元素(也就是key)是否存在,如果不存在则插入,如果存在则不插入,这样HashSet中就不存在重复值。
详细请参考:深入Java集合学习系列:HashSet的实现原理
TreeSet
TreeSet
:
底层数据结构是红黑树。
如何保证元素唯一性?
- 自然排序(元素具备比较性):让元素所属的类实现Comparable接口
自然排序步骤:
1 自然排序实现Comparable<T>
接口
2 重写Comparable接口中的Compareto方法
public class Student implements Comparable<Student> {
private String name;
private int age;
public Student() {
super();
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
//省略set、get方法
/**
*主要是实现这个类
*/
@Override
public int compareTo(Student s) {
//return -1; //-1表示放在红黑树的左边,即逆序输出
//return 1; //1表示放在红黑树的右边,即顺序输出
//return o; //表示元素相同,仅存放第一个元素
//主要条件 姓名的长度,如果姓名长度小的就放在左子树,否则放在右子树
int num = this.name.length() - s.name.length();
//姓名的长度相同,不代表内容相同,如果按字典顺序此 String 对象位于参数字符串之前,则比较结果为一个负整数。
//如果按字典顺序此 String 对象位于参数字符串之后,则比较结果为一个正整数。
//如果这两个字符串相等,则结果为 0
int num1 = num == 0 ? this.name.compareTo(s.name) : num;
//姓名的长度和内容相同,不代表年龄相同,所以还要判断年龄
int num2 = num1 == 0 ? this.age - s.age : num1;
return num2;
}
public static void main(String[] args) {
Set<Student> ts=new TreeSet<Student>();
ts.add(new Student("zhangsan",20));
ts.add(new Student("lis",22));
ts.add(new Student("wangwu",24));
ts.add(new Student("chenliu",26));
ts.add(new Student("zhangsan",22));
ts.add(new Student("qianqi",24));
for(Student s:ts){
System.out.println(s.getName()+"-----------"+s.getAge());
}
}
}/*out
lis-----------22
qianqi-----------24
wangwu-----------24
chenliu-----------26
zhangsan-----------20
zhangsan-----------22
*/
- 比较器排序(集合具备比较性):让集合接收一个Comparator的实现类对象
比较器排序步骤:
1 单独创建一个比较类,这里以MyComparator为例,并且要让其继承Comparator<T>
接口
2 重写Comparator接口中的Compare
方法
3 在主类中使用下面的 构造方法
TreeSet(Comparator<? superE> comparator) // 构造一个新的空 TreeSet,它根据指定比较器进行排序。
新建MyComparator类:
public class MyComparator implements Comparator<Student>{
@Override
public int compare(Student s1,Student s2) {
// 姓名长度
int num = s1.getName().length() - s2.getName().length();
// 姓名内容
int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
// 年龄
int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
return num3;
}
}
学生类(不需继承Comparetable
接口)
public class Student{
private String name;
private int age;
public Student() {
super();
// TODO Auto-generated constructor stub
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
//省略set/get
}
测试
public static void main(String[] args) {
//创建集合对象
//TreeSet(Comparator<? super E> comparator) 构造一个新的空 TreeSet,它根据指定比较器进行排序。
TreeSet<Student> ts=new TreeSet<Student>(new MyComparator());
//将元素对象添加到集合对象中
ts.add(new Student("zhangsan",20));
ts.add(new Student("lis",22));
ts.add(new Student("wangwu",24));
ts.add(new Student("chenliu",26));
ts.add(new Student("zhangsan",22));
ts.add(new Student("qianqi",24));
//遍历
for(Student s:ts){
System.out.println(s.getName()+"-----------"+s.getAge());
}
}
可以把比较器写成匿名内部类的形式
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.getName().length() - s2.getName().length();
// 姓名内容
int num2 = num == 0 ? s1.getName().compareTo(s2.getName())
: num;
// 年龄
int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
return num3;
}
});
//将元素对象添加到集合对象中
ts.add(new Student("zhangsan",20));
ts.add(new Student("lis",22));
ts.add(new Student("wangwu",24));
ts.add(new Student("chenliu",26));
ts.add(new Student("zhangsan",22));
ts.add(new Student("qianqi",24));
// 遍历
for (Student s : ts) {
System.out.println(s.getName() + "---" + s.getAge());
}
}
}
Map(双列集合)
A:Map集合的数据结构仅仅针对键有效,与值无关。
B:存储的是键值对形式的元素,键唯一,值可重复。
HashMap
底层数组+链表实现,可以存储null键和null值,线程不安全。
扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入。
从源码可以看出:HashMap底层数据结构为Node
类型数组,Node
类型的数据结构为链表,当链表长度大于8的时候,会转成红黑树
- 初始化大小 HashMap默认初始大小为16,如有特殊情况需自定义初始化大小可调用
HashMap(int initialCapacity)
自定义。 - 负载因子:负载因子默认为 0.75,当 HashMap 当前已使用容量大于当前大小 的 0.75 倍时,自动扩容一倍空间,如有特殊情况下需要自定义初始化大小时可调用
public HashMap(int initialCapacity, float loadFactor)
进行自定义。 - 树形阀值:树型阀值这个名字是作者根据字面意思自己翻译的,大家看看就好了,对应参数为
TREEIFY_THRESHOLD
,之前提到过 HashMap 的结构为 Node 型数组,而 Node 的数据结构为链表,树型阀值就是当链表长度超过这个值时,将 Node 的数据结构修改为红黑树,以便优化查找时间,默认值为8static final int TREEIFY_THRESHOLD = 8;
- 初始值:
HashMap
有四种构造方法进行初始化
put
方法
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)
//根据hash值来确认存放的位置。如果当前位置是空直接添加到table中
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))))
//首先判断hashCode是否相等,随后判断key值是否相等,若hashCode和key都相等,则把p传递给e
e = p;
else if (p instanceof TreeNode)
//否则如果p是红黑树,将p转成红黑树并赋值
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//否则如果是链表,新增node
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//如果
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
//若容量不足,扩容
resize();
afterNodeInsertion(evict);
return null;
}
HashTable
底层数组+链表实现,无论key还是value都不能为null
,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable
,效率低,ConcurrentHashMap
做了相关优化。
TreeMap
底层数据结构是红黑树。(是一种自平衡的二叉树)
两种排序方式:
- 自然排序
让元素所属的类实现Comparable
接口 - 比较排序
让集合接收一个Comparator
的实现类对象
集合的常见方法及遍历方式
Collection:
add()、remove()、contains()、 iterator()、size()
iterator()
size()
遍历方式:
增强for、迭代器
迭代器
|--List
get()
遍历:普通for
|--Set
Map:
put()、remove()、 containskey()、containsValue()、 keySet()、 get()、 value()、 entrySet()、 size()
遍历方式:
根据键找值
根据键值对对象分别找键和值。
添加一组元素
Arrays.saList()
接受一个数组或是一个用逗号分隔的元素列表(使用可变参数),将其转化为List
对象。
Collections.addAll()
方法接受一个Collection对象,将一个数组或是可变参数,将元素添加到Collection
中。
构建一个不包含元素的Collection,再使用
Collections.addAll()
要比Arrays.List()
更快。Collections.addAll()
是首选。但是
Collection.addAll()
成员方法只能接受Collection
作为参数,不如Arrays.asList()
或Collections.addAll()
灵活,这两个都接受可变参数。
class Snow {}
class Powder extends Snow {}
class Light extends Powder {}
class Heavy extends Powder {}
class Crusty extends Snow {}
class Slush extends Snow {}
public class AsListInference {
public static void main(String[] args) {
List<Snow> snow1 = Arrays.asList(
new Crusty(), new Slush(), new Powder());
// 不能编译,编译器会混淆类型:
// List<Snow> snow2 = Arrays.asList(
// new Light(), new Heavy());
// 编译器报错:
// found : java.util.List<Powder>
// required: java.util.List<Snow>
// Collections.addAll() doesn't get confused:
List<Snow> snow3 = new ArrayList<Snow>();
Collections.addAll(snow3, new Light(), new Heavy());
// 告诉编译器产生的目标类型:
List<Snow> snow4 = Arrays.<Snow>asList(
new Light(), new Heavy());
}
} ///:~
上面的例子中Arrays.asList()
若有继承关系会混淆产生的类型,需要加上产生的目标类型。Collections.addAll()
对这种情况不会混淆。
容器的打印
必须使用Array.toString()
来打印数组,但是打印容器无需任何帮助,无论是List
、Set
还是Map
,都可以直接打印。
public class PrintingContainers {
static Collection fill(Collection<String> collection) {
//List、Set都继承自Collection
collection.add("rat");
collection.add("cat");
collection.add("dog");
collection.add("dog");
return collection;
}
static Map fill(Map<String,String> map) {
map.put("rat", "Fuzzy");
map.put("cat", "Rags");
map.put("dog", "Bosco");
map.put("dog", "Spot");
return map;
}
public static void main(String[] args) {
print(fill(new ArrayList<String>()));
print(fill(new LinkedList<String>()));
print(fill(new HashSet<String>()));
print(fill(new TreeSet<String>()));
print(fill(new LinkedHashSet<String>()));
print(fill(new HashMap<String,String>()));
print(fill(new TreeMap<String,String>()));
print(fill(new LinkedHashMap<String,String>()));
}
} /* Output:
[rat, cat, dog, dog]
[rat, cat, dog, dog]
[dog, cat, rat]
[cat, dog, rat]
[rat, cat, dog]
{dog=Spot, cat=Rags, rat=Fuzzy}
{cat=Rags, dog=Spot, rat=Fuzzy}
{rat=Fuzzy, cat=Rags, dog=Spot}
*///:~
迭代器——Iterator
如何做到只是使用容器,而不关心容器的类型?如何不重写代码就可以应用于不同类型的容器?比如:原来使用的是List
,现在要使用Set
,这时候,迭代器就用上了。
迭代器(也是一种设计模式)是一个对象,它的工作是遍历并选择序列中的对象,客户端程序员不必关心该序列底层的结构。
java中的Iterator
只能单向移动,这个Iterator
只能用来:
-
.iterator()
准备好返回系列第一个元素 -
.next()
获取序列下一个元素 -
.hasNext()
判断是否还有元素 -
.remove()
将迭代器最新返回的元素删除`
public class SimpleIteration {
public static void main(String[] args) {
List<String> strs = new ArrayList<String>();
Collections.addAll(strs, "a", "b", "c");
Iterator<String> it = strs.iterator();
//第一种遍历方式,使用iterator
while (it.hasNext()) {
String p = it.next();
System.out.print(p + " ");
}
System.out.println();
//第二种遍历方式,foreach,当仅仅遍历时,可以使用这种方式
for (String p : strs)
System.out.print(p + " ");
System.out.println();
//iterator可以remove当前遍历的元素:
it = strs.iterator();
for (int i = 0; i < 1; i++) {
//移除遍历的第一个元素
it.next();
it.remove();
}
System.out.println(strs);
}
} /* Output:
a b c
a b c
[b, c]
*///:~
遍历List的方式有两种,使用iterator,使用foreach,使用iterator的方式可以移除最后遍历出来的那个元素。
List的迭代器——Listlterator
ListIterator
是一个更加强大的Iterator的子类型,它只能用于各种List类的访问。Iterator
只能单向移动,但ListIterator
可双向移动。下面是它的作用:
- 可以产生当前位置的前一个和后一个元素的索引;
- 可以使用
set()
方法替换它访问过的最后一个元素; - 可以通过调用
listIterator(n)
方法创建一个一开始就指向列表索引为n处的ListIterator
public class ListIteration {
public static void main(String[] args) {
List<String> pets = new ArrayList<String>();
Collections.addAll(pets, "a", "b", "c", "d");
ListIterator<String> it = pets.listIterator();
while (it.hasNext())
System.out.print(it.next() + ", " + it.nextIndex() +
", " + it.previousIndex() + "; ");
System.out.println();
// Backwards:
while (it.hasPrevious())
System.out.print(it.previous() + " ");
System.out.println();
System.out.println(pets);
it = pets.listIterator(3);
while (it.hasNext()) {
it.next();
it.remove();
}
System.out.println(pets);
}
} /* Output:
a, 1, 0; b, 2, 1; c, 3, 2; d, 4, 3;
d c b a
[a, b, c, d]
[a, b, c]
*///:~
栈——Stack
"栈" 通常指 “后进先出” 的容器。
LinkedList
具有能够直接实现栈的所有功能的方法,因此可以直接将LinkedList
作为栈使用。
public class Stack<T> {
private LinkedList<T> storage = new LinkedList<T>();
public void push(T v) { storage.addFirst(v); }
public T peek() { return storage.getFirst(); }
public T pop() { return storage.removeFirst(); }
public boolean empty() { return storage.isEmpty(); }
public String toString() { return storage.toString(); }
} ///:~
如果仅仅使用“栈”的功能,上面的代码是最合适的,而java.util.Stack
因为继承的关系增加了很多多余的方法。
“栈”在编程语言中经常用来对表达式求值
队列——Queue
队列是一个“先进先出”的容器。
LinkedList
可以用作Queue
的一种实现。它实现了Queue接口。
public class QueueDemo {
public static void printQ(Queue queue) {
while (queue.peek() != null)
System.out.print(queue.remove() + " ");
System.out.println();
}
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<Integer>();
Random rand = new Random(47);
for (int i = 0; i < 10; i++)
queue.offer(rand.nextInt(i + 10));
printQ(queue);
Queue<Character> qc = new LinkedList<Character>();
for (char c : "Brontosaurus".toCharArray())
qc.offer(c);
printQ(qc);
}
} /* Output:
8 1 1 1 5 14 3 1 0 1
B r o n t o s a u r u s
*///:~
offer()
方法是queue相关的方法之一,将一个元素插入到队尾,或返回false。
peek()
和element()
都在不移除的情况下返回队头,前一个在队列为空返回null,后一个抛出异常。
poll()
和remove()
将移除和返回队头,前一个在队列为空时返回null,后一个抛出异常。
增加权重的队列——PriorityQueue
offer()
方法插入一个对象,这个对象会在队列中自然排序。
可以通过提供自己的Comparator
来修改这个顺序,这样当调用peek()
、poll()
、remove()
方法时,获取的元素就是队列中优先级最高的元素。
Collection 和 Iterator
Collection
是描述所有序列容器的共性的接口,它可能会被认为是一个“附属接口”。
实现Collection
接口非常困恼,必须实现Collection的所有方法。这里不贴出例子。我们一般会选择实现Iterator
,继承并创建迭代器,这种方式会容易得多。下面是实现Iterator的例子。
public class IteratorTest {
protected List<Pet> pets = Arrays.asList(Pets.createArray(8 ));
public Iterator<Pet> iterator(){
return new Iterator<Pet>(){
private int index = 0;
@Override
public boolean hasNext() {
return index < pets.size();
}
@Override
public Pet next() {
return pets.get(index++);
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
IteratorTest i = new IteratorTest();
Iterator<Pet> iterator = i.iterator();
while (iterator.hasNext()){
Pet next = iterator.next();
System.out.println(next.toString());
}
}
}
也可以继承AbstractCollection
来实现遍历的功能。但是如果你的类继承了其他类,那就不能再继承AbstractCollection
了。
Foreach与迭代器
Foreach
之所以能工作,是因为 java SE5
引入了新的呗称为Iterable
的接口,该接口包含一个能够产生Iterator
的iterator()
方法,并且Iterable
接口被foreach用来在序列中移动。
只要实现了Iterable
接口,都可以将它用于foreach
中。
public class IterableClass implements Iterable<String> { //实现Iterable接口
protected String[] words = ("And that is how " +
"we know the Earth to be banana-shaped.").split(" ");
public Iterator<String> iterator() { //返回iterator
return new Iterator<String>() {
private int index = 0;
public boolean hasNext() {
return index < words.length;
}
public String next() {
return words[index++];
}
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for (String s : new IterableClass())
System.out.print(s + " ");
}
}
forearch
语句可以用于数组或其他任何的Iterable
,但这并不意味着数组肯定也是一个Iterable
。任何自动包装也不会自动发生:
public class ArrayIsNotIterable {
static <T> void test(Iterable<T> ib) {
for (T t : ib)
System.out.print(t + " ");
}
public static void main(String[] args) {
test(Arrays.asList(1, 2, 3));
String[] strings = {"A", "B", "C"};
// An array works in foreach, but it's not Iterable:
//! test(strings);
// 必须转换成一个Iterable:
test(Arrays.asList(strings));
}
}
适配器方法惯用法
我们可以通过适配器的方法产生“多样”的迭代器,比如:反向迭代器,随机迭代器。下面是反向迭代器的例子。
class ReversibleArrayList<T> extends ArrayList<T> {
public ReversibleArrayList(Collection<T> c) {
super(c);
}
public Iterable<T> reversed() {
return new Iterable<T>() {
public Iterator<T> iterator() {
return new Iterator<T>() {
int current = size() - 1;
public boolean hasNext() {
return current > -1;
}
public T next() {
return get(current--);
}
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
};
}
}
public class AdapterMethodIdiom {
public static void main(String[] args) {
ReversibleArrayList<String> ral =
new ReversibleArrayList<String>(
Arrays.asList("To be or not to be".split(" ")));
// Grabs the ordinary iterator via iterator():
for (String s : ral)
System.out.print(s + " ");
System.out.println();
// Hand it the Iterable of your choice
for (String s : ral.reversed())
System.out.print(s + " ");
}
}
参考资料:
《java编程思想》