Collection 接口
Java类中,集合类的基本接口是Collection接口,有两个基本方法:
public interface Collection<E>{
boolean add(E element);
Iterator<E> iterator();
...
}
除了这两个方法外还有其他方法,稍后写到。
add方法用于向集合内添加元素,如果集合发生了变化则返回true,没有发生变化则返回false;集合中不允许相同元素,所以如果添加一个集合中已有的元素,那么就会添加失败
iterator 方法用来返回一个实现了Iterator 接口的对象,可以用这个迭代器对象依次访问集合中的元素
迭代器
Iterator 接口包含四个方法:
public interface Iterator{
E next();
boolean hasNext();
void remove;
default void forEachRemaining(Consume<? super E> action);
}
通过反复调用next() 方法可以逐个访问集合中的每一个元素,但是如果到了结尾还在访问则会抛出NoSuchElementException。因此在访问之前先用hasNext判断是否有下一个next节点。
循环访问:
-
Collection 接口实现了Iterable接口,因此对于标准库内的任何集合都能够用for each循环,编译器会将for each循环转换为带有迭代器的循环
-
iterator 接口中的forEachRemaining 方法访问,这个方法最简单的用法就是调用lambda 表达式:
iterator.forEachRemaining(e->System.out.println(e));
Iterator 接口的remove 方法会删除上次调用next 方法时返回的元素,也就是说,在使用remove方法前必须先使用next 方法。如果remove前没有调用next则会抛出IllegalStateException异常
API:
java.util.Collection
-
Iterator iterator()
返回一个用于访问集合中各元素的迭代器 -
int size()
返回当前存储在集合中的元素个数 -
boolean isEmpty()
如果集合为空返回true -
boolean contains(Object obj)
如果集合中包含一个与obj相等的对象则返回true -
boolean containsAll(Collection other)
如果集合中包含other中的所有元素,则返回true -
boolean add(E e)
向集合中添加元素,如果成功则返回true -
boolean addAll(Collection e)
将e集合中的所有元素添加到集合中,如果原集合发生改变则返回true -
boolean remove(Object obj)
删除一个匹配到的对象 -
boolean removeAll(Collection e)
删除原集合中所有与e集合匹配的元素 -
default boolean removeIf(Predicate<? extends String> filter)
删除集合中filter返回true的所有元素,可以用lambda表达式,下例删除所有值为”S“的元素link.removeIf(e->{ return e.equals("S"); })
-
void clear()
删除集合中的所有元素 -
retainAll(Collection other)
删除原集合中所有other中没有的元素 -
Object[] toArray()
返回这个集合对象的数组 -
T[] toArray(T[] array)
如果array数组足够大,那么用集合的元素填充array,array剩余的空间填充null;如果不够大,分配一个新数组,成员类型与array相同,长度为集合的大小,并填充集合的元素,最后返回这个数组
java.util.Iterator
-
boolean hasNext()
如果存在另一个可访问的元素则返回true -
E next()
返回将要访问的下一个对象 -
void remove()
返回上一次访问的元素 -
default void forEachRemaining(Consume<? super E> action)
访问元素,并传递到指定的动作,可以用lambda表达式iterator.forEachRemaining(action->{ System.out.println(action); })
如果只需要输出,那么利用方法引用
iterator.forEachRemaining(System.out::println);
集合框架中的集合
带List、Set、Queue 的集合都实现了Collection接口,可以用上述Collection中的方法对这些集合进行操作;
Collection 的派生接口有
- List
- Set => SortedSet => NavigableSet
- Queue => Deque
Map 的派生接口有
- SortedMap => NavigableMap
Iterator 的派生接口有
- ListIterator
链表
LinkedList
实现了Collection 接口,底层用双向链表实现
构造一个链表
- **LinkedList() **:构造一个空链表
var link=new LinkedList<String>();
- **LinkedList(Collection<? extends E> e) **:构造一个链表,并添加e集合中的所有元素
var test=new LinkedList<>();
test.add("ABC");
test.add("CDE");
var linkedList=new LinkedList<>(test);
添加一个元素
-
void add(int index,E e) 在指定位置添加一个元素
linkedList.add(1,"Hello");
-
void addFirst(E e) 在链表头添加一个元素 *
linkedList.addFirst("Head");
-
void addLast(E e) 在链表尾添加一个元素 *
linkedList.addLast("Tail");
还有 addAll 就是上面Collection 中的addAll方法
删除一个元素
-
remove() 移除当前位置的元素
linkedList.remove();
-
remove(Object obj) 移除与obj相等的元素
-
remove(int index) 移除指定位置的元素
linkedList.remove(1);linkedList.remove("a");
-
E removeFirst() 移除第一个元素,并返回移除的元素 *
-
E removeLast() 移除最后一个元素,并返回移除的元素 *
linkedList.removeFirst();linkedList.removeLast();
还有一个 removeIf 也是和Collection 实现相同的功能
访问一个元素
- E get(int index) 访问第i个元素,因为链表不能随机访问,所以只能遍历至第i个,时间复杂度为O(n)
- E getFirs() 获取第一个元素
- E getLast() 获取最后一个元素
遍历
对于链表有个专门的迭代器:ListIterator,这个迭代器实现了Iterator接口,但是扩展了 add()方法,而且有previous可以访问前一个节点,也就是说ListIterator 可以正向和反向遍历链表
ListIterator 方法有:
- boolean hasNext() 如果有下一个元素则返回true
- boolean hasPrevious() 如果有上一个元素则返回true
- E next() 返回下一个元素
- E previous() 返回前一个元素
var iter=linkedList.listIterator(); //LinkedList内置了这个用于返回ListIterator对象的方法
while(iter.hasNext()){
System.out.println(iter.next());
}
散列集 HashSet
链表虽然按照次序存储,但是查找的时候却需要遍历查找。如果对于数据的次序没有要求,却需要很快的找到某个元素,那么就要用到散列集。散列集合没有按照添加次序存储,但是查找的时候直接查找根据散列码查找,非常快。
创建
- HashSet() 创建一个空集
- HashSet(Collection<? extends E> e) 创建一个集合,将e集合的元素全部添加进去
- HashSet(int initialCapacity,float loadFactor) 指定容量和装填因子,装填因子在0.0~1.0之间。默认值是0.75,也就是说当集合中的元素达到了容量的75%,那么就会自动扩充(再散列)至原来容量的两倍
添加、移除
HashSet实现了Collection接口,添加和移除都与上面介绍Collection的一致。
HashSet没有供访问的方法,只有一个迭代器(就是Collection那个Iterator)可以用来遍历。
主要用于查找
- boolean contains(Object o) 如果散列集中包含这个对象则返回true
这个方法经过了重写,专门用于散列的查找,众所周知散列查找非常块。
代码块:
var hash=new HashSet<Character >();
try (var file = new FileInputStream("C:\\temp.txt")) {
int x;
while((x=file.read())!=-1){
hash.add((char)x);
}
} catch (IOException e) {
e.printStackTrace();
}
if(hash.contains('x')){
System.out.println("There is an 'x' in the HashSet.");
}else {
System.out.println("Can't found an 'x' in HashSet.");
}
树集 TreeSet
树集与散列集十分相似,不过树集是一个有序集合(sorted collection),添加的元素会自动进行排序。这是使用树数据结构(目前使用的是红黑树(red-black-tree)),新数据都会放在正确的排序位置,所以使用迭代器能够有序访问每一个元素。
在树集中添加元素比散列集要慢,但是与检查数组或者链表相比要快很多。查找元素时间复杂度大概是O(logn) 即为红黑树的查找时间复杂度。
在使用树集的时候必须要在类中实现Comparable接口或者构造的时候传递一个Comparator,因为树集需要有排序的规则(比如说传递的是自定义类的对象,树集是无法自行判断如何排序的)
构造一个树集
- TreeSet() 构造一个空树集
- TreeSet(Comparator<? super E> com) 构造一个根据com比较器排序的树集
- TreeSet(Collection<? extends E> e) 构造一个树集,并添加集合中的所有元素
- TreeSet(SortedSet s) 构造一个树集,添加有序集合中的所有元素,s集合与树集排序方式须一致
获取元素
- Comparator<? super E> comparator() 获取树集的比较器
- E first()
E last() 返回最大值或者最小值 - E higher(E value)
E lower(E value) 返回大于value的最小元素或者小于value的最大元素 - E ceiling(E value)
E floor(E value) 返回大于等于value的最小元素或者小于等于value的最大元素 - E pollFirst()
E pollLast() 删除并返回这个集合中的最大元素或者最小元素,空集时返回null - Iterator descendingIterator 返回一个与比较器相反的排序方式的迭代器(比较器是升序则返回降序迭代器,比较器是降序则返回升序的迭代器,但是这个英文翻译是”降序迭代器“,我特意写了代码测试就是上面的结论)
中间几种访问的方法,描述的时候是用”或者“(返回最大值或者最小值),是因为这些方法是根据比较器不同而返回不同的元素。举个栗子:假如比较器是升序,那么first就是最小值,如果是降序那么first就成为最大值了。
代码块
class Employee implements Comparable<Employee>{
private int salary;
private String name;
private int id;
//构造一个比较器,第一关键字为salary,第二关键字为id,升序
public int compareTo(Employee employee){
if(Integer.compare(this.getSalary(),employee.getSalary())!=0){
return Integer.compare(this.getSalary(),employee.getSalary());
}else {
return Integer.compare(this.getId(),employee.getId());
}
}
public Employee(){}
public Employee(String name, int id, int salary) {
this.name=name;
this.id=id;
this.salary=salary;
}
public String getName() { return name; }
public int getId() { return id; }
public int getSalary() { return salary; }
public void print(){
System.out.println(this.getName()+" "+this.getId()+" "+this.getSalary());
}
}
public class Main {
public static void main(String[] args) {
var tree=new TreeSet<Employee>();
Employee[] employees=new Employee[10];
for(int i=0;i<10;i++){
//添加的值只是信手涂鸦
employees[i]=new Employee(String.valueOf((i+5)*19*5/3+6),i*1000+i*18+5,50000+(i+2)*80);
tree.add(employees[i]);
}
//返回一个迭代器,与上面说的相同,比较器是升序,而这里对应的降序输出
Iterator<Employee> iterator=tree.descendingIterator();
while(iterator.hasNext()){
Employee temp=iterator.next();
System.out.println(temp.getName()+" "+temp.getId()+" "+temp.getSalary());
}
}
}
队列与双端队列
队列允许在尾部高效地添加或删除元素,双端队列(deque)允许在尾部和首部高效地添加或删除元素。不支持在队列中间添加或者删除元素。ArrayDeque和LinkedList都实现了Deque,但是LinkedList作为链表是可以在中间添加或删除元素的,ArrayDeque才是真正的双端队列,只能在两端操作数据。PriorityQueue(优先队列)实现了Queue接口,只能在尾部添加或删除元素
ArrayDeque
ArrayDeque 实现了Deque,可以在头部和尾部高效地添加或删除元素,但是不能处理队列中间的元素
- void addFirst(E e)
void addLast(E e) 在头部或尾部添加元素,如果队列满了则抛出IllegalStateException - boolean offerFirst(E e)
boolean offerLast(E e) 在头部或尾部添加元素,如果队列满了则返回false - E removeFirst()
E removeLast() 删除并返回队头或队尾元素,如果为空则抛出NoSuchElementException - E pollFirst()
E pollLast() 删除并返回队头或队尾元素,如果为空则返回null - E getFirst()
E getLast() 返回但不删除头部或尾部的元素,如果为空则抛出NoSuchElementException - E peekFirst()
E peekLast() 返回但不删除头部或尾部的元素,如果为空则返回null
public class Main {
public static void main(String[] args) {
var q = new ArrayDeque<String>();
q.add("ABC");
q.add("abc");
q.add("BCD");
q.add("CDE");
while (q.size()!=0){
System.out.println(q.pollFirst() + " " + q.pollLast());
}
}
}
PriorityQueue
PriorityQueue实现了Queue接口,只能在队列尾部添加或删除元素,优先队列采用了”堆“数据结构。优先队列可以按照任意顺序存放,但读取的时候是按照有序的顺序读取的,与TreeSet一样,既可以存储实现了Comparable接口的对象,也能够在定义时传递Comparator对象。
- boolean add(E e) 在队尾添加元素,如果队满则抛出IllegalStateException
- boolean offer(E e) 在对位添加元素,如果队满则返回false
- E remove() 删除并返回队头元素,队空则抛出NoSuchElementException
- E poll() 删除并返回队头元素,队空则返回null
- E element() 返回但不删除队头元素,如果队空则抛出异常
- E peek() 返回但不删除队头元素,如果队空则返回null
如果比较器是升序,那么队尾是最小值(只能访问队尾元素);如果是降序,那么队尾是最大值。默认升序
public class Main {
public static void main(String[] args) {
var q=new PriorityQueue<String>((x,y)->y.compareTo(x));
q.add("ABC");
q.add("abc");
q.add("BCD");
q.add("CDE");
while (q.size()!=0){
System.out.println(q.poll());
}
}
}
比较器用了lambda表达式,这里表示降序,String按照字典序排序,所以结果为:
abc->CDE->BCD->ABC
映射
如果需要根据一个元素的精准信息查找这个元素的其他信息(如:根据身份证号码查询人名、出生日期等等),就需要用到映射。映射存储必须按照”键/值“对存储
映射有两个通用的实现:HashMap和TreeMap,HashMap对键值进行散列,TreeMap根据键值建立搜索二叉树。散列或比较函数只应用于键。映射中键与值必须一一对应,如果添加的键/值已经存在于映射中,那么新值会替换旧值(put方法替换时会返回旧值)
基本操作
往映射中添加一个值的时候,必须提供一个键 V put(K key,V value):
var hash=new HashMap<String,String>();
hash.put("10001","Mike");
根据键获取关联的值 E get():
System.out.println(hash.get("10001"));
如果映射中没有与给定键关联的值,那么get方法会返回null。如果不想要null就可以用**getOrDefault(key,value)**方法,
有关联的值则返回值,没有关联的值则返回指定的value:
System.out.println(hash.getOrDefault("10002","Oh No"));
//如果10002有关联的值,则返回关联的值,如果没有则返回"Oh No"
判断映射中是否存在某个键或值
- boolean containsKey(Object key) 如果存在key键则返回true
- boolean containsValue(Object value) 如果存在value值则返回true
Map接口有一个遍历方法 forEach(BiConsumer<? super K,? superV> action),将K、V传给action,这里很适合用lambda表达式:
hash.forEach((k,v)->System.out.println(k+" "+v));
更新映射条目
假设利用映射统计单词出现的频率:键是单词,值是单词出现的次数
单词出现一次则对应的值加1:
hash.put(word,hash.get(word)+1);
问题是如果word还没有出现过,那么hash.get(word)会返回一个null
-
使用getOrDefault() 方法,如果尚未关联则返回一个0
hash.put(word,getOrDefault(word,0)+1);
-
使用putIfAbsent(key,value)方法,如果key没有关联值,则key关联value,已经关联了则不变
hash.putIfAbsent(word,0); hash.put(word,hash.get(word)+1);
-
使用merge()
hash.merge(word,1,Integer::sum);
如果word没有关联值,那么将word与1关联;如果word已经关联值,那么将关联的值和1作为参数传递给sum
Other
子范围
集合中可以取出子范围:subList(start,end)
对这个子范围的对象进行操作的时候会影响原集合
var link = new LinkedList<String>();
for(int i=1;i<=10;i++){
link.addLast(String.valueOf(i*9/4*3/2*5+104));
}
System.out.println(link.size());
var sub=link.subList(0,5); //取出子范围
sub.clear(); //子范围的操作会影响原集合
System.out.println(link.size());
sub的添加、删除都会影响到link集合
同步
如果需要确保集合的线程安全,那么需要用到Collections类的synchronized***
例如需要一个线程安全的映射
var hash=new HashMap<String,Integer>();
...
var safe=Collections.synchronizedMap(hash); //获取这个映射的安全映射
safe.add("NIHAO"); //会影响原映射
...
这和上面的子范围一样,safe的添加、删除都会影响到原映射hash