java基础复习3-Collection集合详细版

先说一下单个元素存储的集合的继承关系
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方法,一个属性一个属性的比较,这样集合才能判断对象是否重复。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值