先说一下单个元素存储的集合的继承关系
ArrayList继承自List,List继承自Collection,Collection继承自Iterable,Iterable里面有一个iterator方法返回一个Iterator(迭代器)。
HashSet继承自Set,Set继承自Collection,Collection继承自Iterable,Iterable里面有一个iterator方法返回一个Iterator(迭代器)。
先写一下怎么使用,我直接写一段最简单的代码:
这是代码,运行看看就知道了,三种遍历方式都写了。
public class ListTest {
public static void main(String[] args) {
List<String> list= new LinkedList<>();
//添加元素
list.add("a");
list.add("b");
list.add("d");
list.add("s");
list.add("f");
list.add(2,"k");//这个方法是List类型的集合特有的方法,往指定位置插入元素
//以上代码执行完毕,这个集合实际元素排序是{"a","b","k","d","s","f"}
//本来元素d的角标是2,最后一行添加的时候指定k在角标2的位置,k插队了,这时候d就只能向后移动一位,后面的其他元素一起移动一位
//不过这种向指定位置添加元素用的不多,而且这种方式效率太低了。
//和角标有关的操作都是List集合特有的,Set集合和Map集合就没有,因为他们是无序的,没有角标
//fori遍历,如果你需要使用角标号,就用这个方式,List类型的可以这样遍历,Set集合不能这样这样遍历,因为Set是无序的,没有角标
System.out.println("--------------------fori--------------------");
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
System.out.println("--------------------迭代器遍历--------------------");
//迭代器遍历,这是所有Collection集合通用的遍历方式
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){//iterator.hasNext():是判断是否还有下个一元素
String next = iterator.next();//iterator.next():是让这个iterator迭代器向下移动一个位置,并返回当前元素
//如果要删除某些元素,一定要用迭代器,
//否则其他方式遍历的时候删除,需要手动处理角标,比较麻烦,所以需要删除就直接用这个迭代器遍历删除就行了
if ("b".equals(next)||"s".equals(next)){
iterator.remove();//从集合中移除这个元素
}
System.out.println(next);
}
System.out.println("--------------------foreach遍历--------------------");
//foreach遍历,这种写法拿不到角标,就是写起来方便点
for (String s : list) {
System.out.println(s);
}
}
}
List集合都是有序,可重复的。有两种最常用的ArrayList和LinkedList,还有其他的就是不太常用。
很多时候都是需要角标的,所以我特别常用List集合。
ArrayList底层是数组结构,就是他在存放的时候,是按照add的顺序,顺序排列在内存中的,是连续的,角标从0开始。
这种模式的优点是:在查询的时候速度比较快。缺点就是在增删的时候比较耗时,但是如果只是往末尾添加,就没啥影响了。
通俗点说:打个比方,你有100本书,你需要整理书籍,按照ABCD…的顺序摆放,当别人说一个书名的时候,你是不是能很快找到对方说的书名在哪个位置,这就是查询比较快的原因。但是你有一个习惯,就是每本书就必须连续紧挨着摆放,当你从中间抽出一本书,就要把后面所有的书籍挪一下位置,保证一直是连续挨着的书籍,这样操作是不是比较麻烦,这就是增删比较耗时的原因。
面试可能会问下面这个问题,比如ArrayList默认初始容量是多少?
ArrayList源码:默认初始化容量为10,但是如果你没有添加元素,实际是一共空数组,当你第一次添加元素的时候,在初始化容量。
jdk1.8以前其实创建的就是一个容量为10的数组,1.8开始创建的是空数组的。
//这是ArrayList部分源码
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
然后在容量填满的时候,容器如何扩容的呢,在源码中,如果容量填满了,有个**>>符号,这个是位移符号**,这是二进制右移的意思。如果是 <<这种就是二进制左移,移动几位,后面就写几。
我把原因卸载下面的代码块中了。
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//这里写的是新容量 = 旧容量 + 旧容量右移一位。最后计算出的新容量其实就是旧容量的1.5倍
//为啥不直接乘以1.5呢,因为这样效率高,计算机直接运行2进制肯定比运算十进制要快呀,计算机本身就是操作2进制的。
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList还有一个不常用的构造函数,可以做一个比较牛的操作,就是把Set集合转成ArrayList集合。
public class ListTest {
public static void main(String[] args) {
Collection<String> sets = new HashSet<>();
//这样就能转了,不过,不怎么常用,反正我一般不会这样干
List<String> list = new ArrayList<>(sets);
}
}
还有一个重要的内容,怎么把ArrayList变成线程安全的呢。利用Collections这个工具类
public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//加上下面这行代码,就可以变成线程安全的,Collections是个工具类
Collections.synchronizedList(list);
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
list.add("ee");
list.add("aa");
}
}
LinkedList底层是双向链表,存放的每一个元素都有上一个元素的地址值和下一个元素的地址值,同时还存放着你放入集合中的对象(Object),是首尾相连的结构。优点是增删比较快,但是查询的时候就相对的慢了。
既然是链表,那就是像一条链子一样。通俗点说:自行车的链条,就非常的像,你把后轮抬起来,自行车踏板是不是可以正转也可以反转。如果其中有一个坏了,是不是把坏掉的这个拆下来,把好的那部分重新连上就行了,所以增删就快多了,查询慢体现在哪呢,假设你只能看到链条的一个部分,不能把链条从自行车上拆下来修,这样你在转脚踏板的时候,查看坏的部分是不是只能一个一个的看,所以查询就慢了。不要较真,我就是打个比方,可能比较好理解。
下面看看源码,看看是如何通过代码的方式实现的链表结构。
//类名,还有LinkedList的继承和实现关系就不贴出来了,我就把一些关键的部分贴出来吧
//size都知道吧,就是容器的大小,transient可能不认识,被这个关键字修饰的变量,不会被序列化。
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
* 这里有个对象Node,这是啥?其实就是LinkedList里面的一个私有静态内部类
* first,很明显是代表第一个元素
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
* last,代表的就是最后一个元素
*/
transient Node<E> last;
/**
* Constructs an empty list.
* 这里是空参构造
*/
public LinkedList() {
}
//就是通过这个内部类,实现的链表结构
private static class Node<E> {
//这个item其实就是实际保存的数据
E item;
//next就是代表下一个节点
Node<E> next;
//prev就是上一个节点
Node<E> prev;
//只有一个带参构造
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
/**
* Appends the specified element to the end of this list.
*
* <p>This method is equivalent to {@link #addLast}.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
* 这就是常用的添加一个数据到集合的add方法,这个方法调用了一个linkLast(e)方法
* 我在下面贴出来
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
* 这个方法就是关键了,上面有last和first,都是成员变量
* 这个l是一个局部变量,这个方法里面创建了一个新的Node对象,把l和e传进去了,还有一个null
* 对照上面的构造函数,就是l就是上一个节点,null就是下一个节点
* 假设有这样一段代码
* List<String> list = new LinkedList<>();
* list.add("a");
* list.add("b");
* list.add("c");
* 你思考一下添加第一个元素的时候,last是自己,first也是自己。
* 但是如果添加第二个元素的时候呢,l=last,这时候l就不是null了,l就是"a"
* new出来的newNode,把l传进去了也就是"b"的上一个是"a"
* 当执行到last=newNode;这个时候last就等于"b"
* 然后l.next=newNode;就相当于"a"的下一个等于"b"
* 这样执行下来是不是就是:"a"的下一个等于"b","b"的上一个是"a"。
* 如果再把"c"添加进去呢,大家自己想想,会是什么效果。
* 应该是"a"的下一个是"b","b"的上一个是"c","b"的下一个是"c","c"的上一个是"b"。
* 是不是这个效果呢,我觉得是。如果写的不对,希望有大佬帮我指出来。
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
Vector底层也是数组结构,和ArrayList的区别是,Vector是线程安全的,但是效率比较低,目前实现线程安全有更好的方式,所以Vector一般不使用。
Set集合都是无序,不可重复的。也是有两个比较常用的,HashSet和TreeSet,也有其他的,就是不太常用。
HashSet底层是HashMap,存放数据实际上是存到了HashMap的key里面了。HashSet是无序的,就是你放进去的顺序和你取出来的顺序可能不一样。而且没有角标,不能使用fori方式遍历,只能时候迭代器和foreach方式遍历集合。
TreeSet底层是TreeMap,存放的数据实际上是存到了TreeMap的key里面了。TreeSet虽然也是无序的,但是你在便利的时候,这个集合会按照自然排序的方式自动排序,比如你放入1,4,2,6。遍历的时候自动就变成了1,2,4,6。遍历方式同上,只有两种。
注意:自己创建的bean对象,一定要重写equals方法。因为所有的类都继承Object,Object里面的equals方法是用==比较地址值。每new一个对象,地址值都是不一样的,所以你要重写equals方法,一个属性一个属性的比较,这样集合才能判断对象是否重复。