Java集合框架这个东西应该是类似于C++的STL标准库, 便于使用一些常见的数据结构:D
目录
一.Java集合框架
1. 将接口和实现分离
2. Collection接口
3. 迭代器
4. 泛型使用方法
5. 集合框架中的接口二. 具体的集合
1. 链表LinkedList(List)
2. 数组列表ArrayList(List)
3. 散列集HashSet(Set)
4. 树集TreeSet(SortedSet)
5. 队列与双端队列Queue&Deque
6. 优先级队列PriorityQueue三. 映射(Map)
1. 基本映射操作
2. 基本映射操作
3. 基本映射操作
一.Java集合框架
CoreJava中提到C++的STL提供了一种泛型算法, 现在想来的确是这样的, 还是挺好用的, 嘤嘤嘤;
本文中有一些地方出现了容器, 就是Java中集合的概念, 是我自己习惯说容器了, 呜呜;
1.将接口和实现分离
不要直接在一个类里面实现一个集合, 可以用一个支持接口的类来实现集合, 比如要实现一个队列, 既可以使用链表LinkedLIstQueue, 也可以使用循环列表CircularArrayQueue, 只需要implements queue就可以用不同的类实现一个队列接口了, 在定义一个队列的时候就可以先这样写一个接口:
然后这样去定义队列了:
Queue<People> humanity = new CircularArrayQueue<>(100);
表示定义一个长度为100的队列, 用循环列表来实现;
2.Collection接口
在Java中集合类的基本接口是Collection, 它提供了2个基本方法:
一个是add, 表示向集合中加入一个元素, 如果加入成功了就返回true, 否则返回false;
一个是Iterator迭代器, 用于遍历集合中的元素;
3.迭代器
迭代器就是Iterator;
可以用Iterator的next方法来遍历容器, 如果没有next就会返回一个NoSuchElementException错误, 所以调用next之前要用hasNext检查;
恕在下直言这笨比方法希望我不要多用, 尽量用for each来直接遍历容器把;
for-each循环可以和任何实现了iterable接口的对象一起工作;
在有了lambda表达式之后可以用forEachRemaining方法配合lambda表达式一起对容器内的元素做相同的操作;
java中的iterator和C++中有一个巨大的不同:
所以在调用it.remove的时候, 是删除了上一个访问过的元素, 所以如果想要删除下一个迭代器的元素, 要先用it.next访问, 然后再remove掉;
如果在使用it.remove之前没有使用it.next, 会报错IllegalStateException
这样测试一下iterator:
import java.util.*;
class test12 {
public static void main(String[] args) {
Collection<String> a = new Vector<String>();
a.add("GodV");
a.add("We1less");
for(Iterator<String>it = a.iterator();it.hasNext();){
System.out.println((String)it.next());
}
}
}
要记得最好不要太早定义iterator, 可以等到用的时候再定义, 因为一旦执行Iterator<String>it = a.iterator()
就会自动把迭代器初始化到容器的第一个位置;
4. 泛型使用方法
在Collection接口中, Java提供了很多有用的方法, 列举一些如下:
在我的测试中遇到了一些问题, 先上我写的MeVector测试代码:
import java.util.*;
import java.lang.reflect.*;
class test12 {
public static void main(String[] args) {
MyVector<String> me = new MyVector<String>();
me.add("GodV");
me.add("We1less");
System.out.println(me.size());
System.out.println(me.contains("GodV"));
for(Iterator<String> it =me.iterator();it.hasNext();){
System.out.println(it.next());
}
AbstractCollection<String> s = new Vector<>();
Class<?> cl = s.getClass();
Method[] Methods = cl.getDeclaredMethods();
for(Method i : Methods){
System.out.println(i);
}
}
}
class MyVector<E> extends AbstractCollection<E>{
private Vector<E> vec;
public MyVector(){
vec = new Vector<E>();
}
@Override
public int size(){
return vec.size();
}
@Override
public Iterator<E> iterator(){
Iterator<E> it = vec.iterator();
return it;
}
public boolean add(E e){
return vec.add(e);
}
}
//output:
//2
//true
//GodV
//We1less
//public synchronized boolean java.util.Vector.add(java.lang.Object)
//private void java.util.Vector.add(java.lang.Object,java.lang.Object[],int)
//public void java.util.Vector.add(int,java.lang.Object)
//public synchronized java.lang.Object java.util.Vector.remove(int)
//public boolean java.util.Vector.remove(java.lang.Object)
//......省略一些
//void java.util.Vector.checkInvariants()
首先, 如果不在MyVector中重写add方法, 就会出现unsupportedoperationexception
异常, 仔细寻找错误后, 我用反射找到了AbstractCollection类中的方法, 发现add方法的返回值是void, 去查询文档中的add方法是这样的:
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
怪不得会出现这样的异常, 所以我重写了add方法;
但是其他的一些方法, 比如contains是可以正常使用的;
5. 集合框架中的接口
上一张图表示框架继承层次:
二. 具体的集合
所有的类都实现了Collection接口, 而以map结尾的类实现了Map接口;
1.链表LinkedList
链表的实现细节就不多说了, 谈谈Java中具体的链表的特点;
Collection中的iterator有next,hasNext和remove等一些方法, 但是没有add方法, previous这些方法;
因为Collection.iterator很通用, 所有对于某些无序集合比如set这些, 所以不能有add和previous这些方法, 因为不是按照下表索引序来的, 是无序的, 所以没有这些方法;
但是List是有序的, 所以现在有了ListIterator这个新的迭代器, 仅用于List, 有add, previous, hasPrevious方法, 就可以按顺序访问, 增加链表中的值, 还有get方法和set方法, 可以获取和修改指定迭代器位置的值;
使用add方法, 新加入的方法会被依次添加到迭代器当前位置之前;
要注意使用previous方法的时候, remove方法的效果和之前的有所不同, remove会删除之前操作过的那个变量, 而不是删除光标前面的那个变量;
在使用多个迭代器的时候要特别注意, 如果两个迭代器同时操作了当前的链表, 那么另一个迭代器的顺序就会发生改变, 容易出现问题, 所以CoreJava中建议, 同时最多使用一个迭代器做既读又写的操作, 其余的迭代器都只做读的工作;
Collection也有toString方法, 返回一个类似于[A, B, C, …]这样的字符串;
使用get方法, 以随机index的方法获取某个元素是效率极低的, 在Java中已经对get做了优化, 如果索引大于 size()/2 就从列表尾端开始搜索元素;
比较高效的方法是nextIndex和previousIndex方法来获取previous和next的元素的索引;
由于链表的随机访问元素效率比较低, 但是增删元素效率比较高;
所以如果经常需要随机访问元素最好就使用数组或者ArrayList;
随手测试一下:
import java.util.*;
class test13 {
public static void main(String[] args) {
List<String> me = new LinkedList<String>();
me.add("GodV");
me.add("We1less");
me.add("weishen");
me.add("mifengaaa");
ListIterator<String> aIterator = me.listIterator();
while(aIterator.hasNext()){
System.out.println((String)aIterator.next());
}
System.out.println();
aIterator.previous();aIterator.previous();
aIterator.remove();
aIterator = me.listIterator();
while(aIterator.hasNext()){
System.out.println((String)aIterator.next());
}
}
}
//output:
//GodV
//We1less
//weishen
//mifengaaa
//
//GodV
//We1less
//mifengaaa
//可见,remove会删除上一个遍历过的元素, 而不是像光标一样删除前面一个元素
上一些总结的常用的List和LinkedList的方法:
2.数组列表ArrayList
ArrayList应该是一个和Vector差不多的东西, 主要特点是可以动态改变大小, 区别在于:
Vector支持线程的同步, 同一时刻只有一个线程能够写Vector, 避免多线程同时写而引起的不一致性, 但实现同步需要很高的花费,所以它比ArrayList慢;
这个东西的详细使用前面写过了, 然后前面关于List的一些方法他也能用;
在这个博客里:Java基础 自学讲义 2. OOP部分特性 二.C.泛型数组列表
3.散列集HashSet
散列集, 散列表, 哈希表啥的都是这个东西;
书上讲的有点皮, 有点没看懂, 用一个比较好的散列函数是比较重要的;
这里贴一篇我以前C++里学散列表的博客, 原理都是一样的;
散列表随手测试一下:
import java.util.*;
class test14 {
public static void main(String[] args) {
Set<String> me =new HashSet<>();
me.add("ZedKing");
me.add("GodV");
me.add("Godv");
me.add("mifengaaa");
me.add("mifengaaa");
System.out.println(me.size());
for(Iterator<String>it=me.iterator();it.hasNext();){
System.out.println(it.next());
}
}
}
//output
//4
//GodV
//Godv
//ZedKing
//mifengaaa
Set是对大小写敏感的;
HashSet的toArray方法返回的是一个Object[]对象, 所以不能用一个ArrayList去接收;
因为在HashSet建立之前, 如果能对他的大概容量和容量使用率能有一个比较准确的预估, 可以大大提升这个哈希表的效率, 所以构造方法提供了以下几种:
最后一种只有在散列表的填充百分比大于loadFactor的百分比时, 散列表才会进行再散列(rehashed);
4. 树集TreeSet
TreeSet是由红黑树实现的(Red-Black Tree), 可以实现有序集合SortedSet
相比于普通Set就是可以实现有一定的顺序;
先上一个我的TreeSet测试代码:
import java.util.*;
import java.lang.reflect.*;
class test15 {
public static void main(String[] args) {
SortedSet<item> aSet = new TreeSet<item>();
aSet.add(new item("GodV",13));
aSet.add(new item("mifengaaa",30));
aSet.add(new item("Aluka",123));
aSet.add(new item("Cpt",33));
aSet.add(new item("GodV",42));
System.out.println(aSet);
NavigableSet<item> bSet = new TreeSet<>(
Comparator.comparingInt(p->p.getNumber())
);
// 像下面这样写也可以:
// NavigableSet<item> bSet = new TreeSet<>(
// Comparator.comparingInt(item::getNumber)
// );
bSet.addAll(aSet);
System.out.println(bSet);
}
}
class item implements Comparable<item>{
private int number;
private String description;
public item(){}
public item(String description, int number){
this.number = number;
this.description = description;
}
public int getNumber(){
return this.number;
}
public String getDescription(){
return this.description;
}
@Override
public int compareTo(item u){
return this.description.compareTo(u.getDescription())==0?Integer.compare(this.number, u.getNumber()):this.description.compareTo(u.getDescription());
}
@Override
public String toString(){
return this.description+"-"+this.number;
}
}
//output
//[Aluka-123, Cpt-33, GodV-13, GodV-42, mifengaaa-30]
//[GodV-13, mifengaaa-30, Cpt-33, GodV-42, Aluka-123]
上面的代码中, 第一部分是我按照item类implement的Comparable接口, 重写了compareTo方法, 按照description排序的, 次关键词number排序, 所以在TreeSet中是直接按照Comparable接口实现的compareTo的方法排序的;
第二部分我查看了TreeSet的Constructor
TreeSet<item> a = new TreeSet<>();
Class<?> cl = a.getClass();
Constructor<?>[] methods = cl.getDeclaredConstructors();
for(Constructor<?> i : methods) System.out.println(i);
发现有下面几种构造器:
public java.util.TreeSet(java.util.SortedSet)
public java.util.TreeSet(java.util.Collection)
public java.util.TreeSet(java.util.Comparator)
public java.util.TreeSet()
java.util.TreeSet(java.util.NavigableMap)
所以也可以使用lambda表达式或者使用method reference传入一个Comparator来实现排序的指定, 这样就不会默认去调用自己的compareTo方法了;
也可以传入一个SortedSet或者Collection来Copy所有的内容;
最后上一些树集常用的方法:
也可以在这里看到:
极客学院TreeSet
还有最后一个问题, 上面我们使用到了三个接口, TreeSet, SortedSet和 NavigableSet; 这三个接口主要是支持的方法有些不一样, 需要的时候酌情选用把…
5. 队列与双端队列
ArrayDeque和LinkedList都实现了deque;
要注意的是接口也对deque提供了push pop方法, 但是我觉得最好不要用, 因为容易分不清前面和后面, 最好用addFirst addLast offerFirst offerLast和removeFirst removeLast pollFirst pollLast, 比较好区分;
给一些常用的方法:
6. 优先级队列
Java中的优先队列是用堆(heap)实现的(最小堆, 小顶堆), 当然也可以用二叉搜索树实现;
堆和二叉搜索树的区别是:
堆一定是完全二叉树, 二叉搜索树不一定是完全二叉树
名字叫PriorityQueue
有一些方法, 比如poll offer peek这种 还有isEmpty这些, 用到自然就知道了…
还有一个好像更牛逼的东西叫PriorityBlockingQueue优先级阻塞队列, 可用于并发?
三.映射
Java里的映射就是map, 提供一种 键-值 的数据结构
1.基本映射操作
Map主要有两种, HashMap和TreeMap, 一个无序一个有序的, HashMap稍微快一点;
有put, get, remove, forEach这些常见的方法, 要注意的是遍历Map的方法, 我总结了4种, CoreJava中给出的是第一种使用forEach+lambda表达式, 我认为这种方法应该是最好的;
遍历Map:
第一种可以使用Map的forEach方法加上Java8的lambda表达式:
aMap.forEach( (k,v)->{System.out.println(k+" "+v);} );
第二种可以使用Map.Entry来遍历Map的条目:
for(Map.Entry<String, String> it : aMap.entrySet()){
System.out.println(it);
}
第三种可以使用for结合Map的keySet和values方法来遍历:
for(String a : aMap.keySet()){
System.out.println(a);
}
for(String a : aMap.values()){
System.out.println(a);
}
第四种是使用迭代器, 这种是看起来比较熟悉而且效率挺高的, 但是要注意, 不能在使用for循环访问迭代器的同时使用remove操作, javadoc说这样会发生不可预期的错误, 如果希望迭代的同时删除元素, 可以使用while来遍历:
for(Iterator<Map.Entry<String, String>> it = aMap.entrySet().iterator();it.hasNext();){
System.out.println(it.next());
}
当然还有第五种是在遍历keySet的时候调用get方法获取对应的值, 但是这种方法太捞了, 效率很低, 不提了, 就上一段测试代码吧:
for(String i : aMap.keySet()){
System.out.println(i+"="+aMap.get(i));
}
测试代码如下:
import java.util.*;
class test16 {
public static void main(String[] args) {
Map<String,String> aMap = new TreeMap<>();
aMap.put("Aluka", "AluWife");
aMap.put("GodV", "mifengaaa");
aMap.put("zz", "lym");
aMap.forEach( (k,v)->{System.out.println(k+" "+v);} );
aMap.remove("zz");
System.out.println(aMap.getOrDefault("zz", "NoThisEntry"));
for(Map.Entry<String, String> it : aMap.entrySet()){
System.out.println(it);
}
for(String a : aMap.keySet()){
System.out.println(a);
}
for(String a : aMap.values()){
System.out.println(a);
}
for(Iterator<Map.Entry<String, String>> it = aMap.entrySet().iterator();it.hasNext();){
System.out.println(it.next());
}
}
}
贴一些map和sortedmap的常用方法: