Java 中的集合体系
集合是Java常用类库最重要的一部分。
集合是数据的容器。相比于数组数组的长度是固定的,数组无法改变自己的大小,即使使用动态扩容,也是创造新的数组。而且数组不是很适合数据的插入。所以数组满足不了我们的需要,所以Java官方内置了各种数据结构,经过发展到了jdk1.2之后集合到了一起。类集是Java对数据结构一种成熟的体现。
下面我将总结一下我对于集合的相关学习。
Collection 接口是在整个 Java 类集中保存单值的最大操作父接口,里面每次操作的时候都只能保存一个对象的数据。 此接口定义在 java.util 包中。
Collection或者其他的子类都拥有相同的方法对数据进行操作。
public boolean add(E e) 插入数据
public E get(int index) 获取数据(List中)
public boolean contains(Object o) 判断集合中是否包含
public E remove(int index) 删除数据
在整个集合中 List 是 Collection 的子接口,里面的所有内容都是允许重复的。
注:List相比于Collection大部分方法是可以使用的,其中有一些方法有重载和扩展。
其中remove方法进行了重载,具体如下:
public E remove(int index)删除指定位置的内容。使用下标进行操作,并且取出来。如果你操作List,想获取和删除共同进行,即可以使用remove方法。
public E set(int index,E element)方法是对某个指定下标进行修改,覆盖的操作。
public void add(int index,E element)方法是对其进行添加,通俗的说就是往后 挤。
List subList(int fromIndex,int toIndex)方法就是给定一段下标进行截取。
List 接口类有如下几个: ArrayList(95%)(线程不安全),Vector(4%)(线程安全),LinkedList(1%)
ArrayList为数组结构,特点:增删慢,查找快
// ArrayList的声明
ArrayList<Integer> data= new ArrayList<>();
根据API查看构造方法:
构造方法内输入数字为数组大小,每次扩容为原来的1.5倍,建议使用一参构造方法根据数组大小输入合适的数。
Q:为什么初始容量为10?
A:根据源码
其中elementData是可以存储任何数据类型的数组,赋值DEFAULTCAPACITY_EMPTY_ELEMENTDATA,的值为
transient Object[] elementData;
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
长度为0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
};
正是因为长度为0,在发现无法存入的时候,就会发生扩容。我们再来查看一下扩容的源码。不同版本的jdk貌似算法有所不同,我以自己电脑上的版本为准。先初始化ArrayList,再进行添加,初始长度为0,则进行扩容。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
返回为true,再查看ensureCapacityInternal()与calculateCapacity()两个方法;
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
可以看出如果数组elementData与DEFAULTCAPACITY_EMPTY_ELEMENTDATA相同,也就是都为空时,就返回DEFAULT_CAPACITY与minCapacity最大值,而minCapacity为1,DEFAULT_CAPACITY默认为10。
private static final int DEFAULT_CAPACITY = 10;
所以扩容长度默认为10.
接下来我们用实例来测试其他的方法,源代码和结果如下:
import java.util.ArrayList;
import java.util.Collection;
public class Test1 {
public static void main(String[] args) {
//数组结构
//特点:增删慢,查找快
ArrayList<Integer> data= new ArrayList<>();
data.add(1);//顺序添加
data.add(2);
data.add(3);
data.add(4);
data.add(5);
data.add(0,6);//第一个参数是下标插入
System.out.println(data); // 打印all对象调用toString()方法
}
}
import java.util.ArrayList;
import java.util.Collection;
public class Test1 {
public static void main(String[] args) {
//数组结构
//特点:增删慢,查找快
ArrayList<Integer> data= new ArrayList<>();
ArrayList<String> data1= new ArrayList<>();
data.add(1);//顺序添加
data.add(2);
data.add(3);
data.add(4);
data.add(5);
data.add(0,6);//第一个参数是下标插入
data.remove(4);//删除下标为4的数
data1.add("hello");//插入数组类型的
data1.add("world");
data1.remove("hello");//删除内容为hello的数组
System.out.println(data); // 打印all对象调用toString()方法
}
}
Vector 本身也属于 List 接口的子类,基本和ArrayList一样。
import java.util.Vector;
public class Test1 {
public static void main(String[] args) {
Vector<String> all = new Vector<String>(); // 实例化List对象,并指定泛型类型
all.add("hello "); // 增加内容,此方法从Collection接口继承而来
all.add(0, "LAMP ");// 增加内容,此方法是List接口单独定义的
all.add("world"); // 增加内容,此方法从Collection接口继承而来
all.remove(1); // 根据索引删除内容,此方法是List接口单独定义的
all.remove("world");// 删除指定的对象
System.out.print(all);
}
}
此类的使用几率是非常低,此类继承了 AbstractList,所以是 List 的子类。但是此类也是 Queue 接口的子类。主要是链表,增删快,查找满。可以当作栈或者队列使用。
新加操作:
public void addFirst(E e) :将指定元素插入此列表的开头。
public void addLast(E e) :将指定元素添加到此列表的结尾。
public E getFirst() :返回此列表的第一个元素。
public E getLast() :返回此列表的最后一个元素。
public E removeFirst() :移除并返回此列表的第一个元素。
public E removeLast() :移除并返回此列表的最后一个元素。
public E pop() :从此列表所表示的堆栈处弹出一个元素。
public void push(E e) :将元素推入此列表所表示的堆栈。
public boolean isEmpty() :如果列表不包含元素,则返回true。
LinkedList<Integer> all = new LinkedList<Integer>(); // 实例化List对象,并指定泛型类型
LinkedList<Integer> all1 = new LinkedList<Integer>();
//压栈
all.push(1);
all.push(2);
all.push(3);
//弹栈
Integer i = all.pop();
//队列
all1.addFirst(4);
all1.addFirst(5);
all1.removeLast();
System.out.println(all);
System.out.println(i);
System.out.println(all1);
Iterator主要是针对输出集合,Ilterator主要是针对Collection,而ListIlterator是针对List。
常用方法:
public E next() :返回迭代的下一个元素。
public boolean hasNext() :如果仍有元素可以迭代,则返回 true。
主要原理是在数组第一个地址之前有一个指针,通过next()方法,不断地向下移动,再用hasNext()判断循环调节,即可进行遍历。换句话说,hasNext()说如果下一个地址有元素,next()就可以将指针往后移动并且返回下一个元素。具体实现如下
ArrayList<Integer> all = new ArrayList<Integer>(); // 实例化List对象,并指定泛型类型
all.add(1); // 增加内容,此方法从Collection接口继承而来
all.add(2);
all.add(3);
all.add(4);
all.add(5);
all.add(6);
Iterator<Integer> iterator = all.iterator();
while(iterator.hasNext()) {
Integer i = iterator.next();
System.out.println(i);
}
其中remove()方法一定要获取后再进行删除,具体实现也就是先next()移动指针再进行remove();但是此时指针已经移动,如果想回复指针初始状态,则必须配合previous进行使用。
增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
ArrayList<Integer> all = new ArrayList<Integer>(); // 实例化List对象,并指定泛型类型
all.add(1); // 增加内容,此方法从Collection接口继承而来
all.add(2);
all.add(3);
all.add(4);
all.add(5);
all.add(6);
for(int i:all) {
System.out.println(i);
}
String类型也是一样:
ArrayList<String> all = new ArrayList<String>(); // 实例化List对象,并指定泛型类型
all.add("杨花落尽子规啼,"); // 增加内容,此方法从Collection接口继承而来
all.add("闻道龙标过无锡。");
all.add("我寄愁心与明月,");
all.add("随风直到夜郎西。");
for(String i:all) {
System.out.println(i);
}
Set 接口也是 Collection 的子接口,与 List 接口最大的不同在于,Set 接口里面的内容是不允许重复的。 Set 接口并没有对 Collection 接口进行扩充,基本上还是与 Collection 接口保持一致。因为此接口没有 List 接口中定义 的 get(int index)方法,所以无法使用循环进行输出。 那么在此接口中有两个常用的子类:HashSet、TreeSet。
总结的来说Set是一个不包含重复元素的单值集合。
HashSet:
方法和Collection一样(因为是子类嘛)但是没有get的方法,可以用interator进行迭代。他的特性是散列存储,也被称之为散列表。
根据源码,HashSet使用add方法,是将元素放入一个HashMap里面。后续会讲解HashMap,总之特性解释乱序。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
private transient HashMap<E,Object> map;
HashSet<String> all = new HashSet<String>(); // 实例化List对象,并指定泛型类型
all.add("杨花落尽子规啼,"); // 增加内容,此方法从Collection接口继承而来
all.add("闻道龙标过无锡。");
all.add("我寄愁心与明月,");
all.add("随风直到夜郎西。");
for(String i:all) {
System.out.println(i);
}
HashSet<String> all = new HashSet<String>(); // 实例化List对象,并指定泛型类型
all.add("杨花落尽子规啼,"); // 增加内容,此方法从Collection接口继承而来
all.add("闻道龙标过无锡。");
all.add("我寄愁心与明月,");
boolean flag1 = all.add("随风直到夜郎西。");
boolean flag2 = all.add("随风直到夜郎西。");
System.out.println(flag1);
System.out.println(flag2);
这里可以显示无法存入相同元素。
TreeSet:
和HashSet不同,TreeSet采用二叉树进行操作,方法都是一样的,但是数据结构不同。顺序不是无序,而是自然顺序。具体展示如下:
TreeSet<String> all = new TreeSet<String>(); // 实例化List对象,并指定泛型类型
all.add("杨花落尽子规啼,"); // 增加内容,此方法从Collection接口继承而来
all.add("闻道龙标过无锡。");
all.add("我寄愁心与明月,");
all.add("随风直到夜郎西。");
for(String i:all) {
System.out.println(i);
}
TreeSet<String> all = new TreeSet<String>(); // 实例化List对象,并指定泛型类型
all.add("z"); // 增加内容,此方法从Collection接口继承而来
all.add("b");
all.add("a");
all.add("e");
for(String i:all) {
System.out.println(i);
}
由此可见,字符串,都是由Unicode码进行排序。但是如果你不使用系统自带的类型,而是自定义的类型,比如class Person类,这个时候使用TreeSet就会发生错误。所以再使用自定义类的时候一定要给出判断大小的定义。
TreeSet<Person> all = new TreeSet<Person>(); // 实例化List对象,并指定泛型类型
Person p1 = new Person("小李",18);
Person p2 = new Person("小卢",19);
all.add(p1);
all.add(p2);
for(Person i:all) {
System.out.println(i);
}
}
static class Person{
private String name;
private int age;
public Person(String name,int age) {
this.name = name;
this.age = age;
}
}
由此引出一个Comparable类,并使用其一个抽象方法compareTo:
public int compareTo(Person per) {
if (this.age > per.age) {
return 1;
}
else if (this.age < per.age) {
return -1;
}
else {
return 0;
}
}
由此,Person类的判断规则是年龄来进行比较,返回值为正数,负数和0。
完善后的代码如下:
public static void main(String[] args) {
TreeSet<Person> all = new TreeSet<Person>(); // 实例化List对象,并指定泛型类型
Person p1 = new Person("小李",18);
Person p2 = new Person("小卢",19);
all.add(p1);
all.add(p2);
for(Person i:all) {
System.out.println(i);
}
}
static class Person implements Comparable<Person>{
private String name;
private int age;
public Person(String name,int age) {
this.name = name;
this.age = age;
}
public String toString() {
return "姓名:" + this.name + ",年龄:" + this.age;
}
public int compareTo(Person per) {
if (this.age > per.age) {
return 1;
}
else if (this.age < per.age) {
return -1;
}
else {
return 0;
}
}
}
Map不再是单值存储,Map和Collection同一级别。存储的是一对数据,为键值对,key,value。
相当于钥匙和锁。数据接相当于锁,需要对应的key去打开。
形象的说身份证号和人,身份证号不可重复,而Map中key也是不可重复的。
Map和数组类似,都是通过键来寻找数据,数组是系统提供的0,1,2,3…,而Map是自定义的。
Map的遍历十分麻烦,先用keyset()把键全部拿出来,再进行迭代进行寻找。
其中添加操作V put(K key,V value),原理是用新值覆盖旧值,如果不存在旧值就返回null,如果存在旧值就返回旧值。
而删除remove(),可以通过键来删除,也可以通过一对键值对来删除。通过键来删除的时候,返回被删除的数据(成功),失败的话就是 null。
boolean containsValue(Object value)判断是否存在值。
boolean containsKey(Object key)判断是否存在键。
哈希表的结构是对象数组+链表。
hashcode的int值与数组长度进行取模的运算。如果取模结果重复了,那将存储在同一个数组中(哈希桶),数组中存在一个链表可以不断地向下存储。
当一个数组中链表的长度大于8是会将链表转化为红黑树,哈希桶中的数据量减小为6时,会从红黑树变为链表。
还存在特殊的特性,Java中初始桶的数量为16,散列因子为0.75.
static final float DEFAULT_LOAD_FACTOR = 0.75f;
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1, "张三A");
map.put(1, "张三B"); // 新的内容替换掉旧的内容
map.put(2, "李四");
map.put(3, "王五");
String val = map.get(1);
System.out.println(val);
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1, "张三A");
map.put(2, "李四");
map.put(3, "王五");
Set<Integer> set = map.keySet(); // 得到全部的key
Collection<String> value = map.values(); // 得到全部的value
Iterator<Integer> iter1 = set.iterator();
Iterator<String> iter2 = value.iterator();
System.out.print("全部的key:");
while (iter1.hasNext()) {
System.out.print(iter1.next() + "、");
}
System.out.print("\n全部的value:");
while (iter2.hasNext()) {
System.out.print(iter2.next() + "、");
}
Map<String, String> map = new HashMap<String, String>();
map.put("ZS", "张三");
map.put("LS", "李四");
map.put("WW", "王五");
map.put("ZL", "赵六");
map.put("SQ", "孙七");
Set<String> set = map.keySet(); // 得到全部的key
Iterator<String> iter = set.iterator();
while (iter.hasNext()) {
String i = iter.next(); // 得到key
System.out.println(i + " --:> " + map.get(i));
}
注:HashMap键的数据对象,一定不能乱改,特别是自定义对象,因为key->value是根据哈希值进行查找,一旦改变将会丢失。
HasshMap,Hashtable,ConcurrentHashMap,三者最大的区别在于多线程,线程的安全。
HasshMap 线程不安全,效率高;Hashtable 线程安全,效率低。
ConcurrentHashMap采用分段锁机制,保证线程安全,和效率高。