java集合框架List常见基础面试题

简介:java集合框架List常见基础面试题

考点:list的基础知识掌握情况,对应实现的区别、线程安全、使用场景。

问:Vector和ArrayList、LinkedList联系和区别?分别的使用场景?

答:
(1)线程安全:

ArrayList:底层是数组实现,线程不安全,查询和修改快,但是增加和删除慢。
LinkedList:底层是双向链表,线程不安全,查询和修改慢,新增和删除快。
Vector:底层是数组实现,线程安全的,使用了synchronized加锁。

(2)使用场景:

Vector:很少使用
添加和删除的场景多用LinkedList
查询和修改多用ArrayList

注释:
ArrayList简介
	//初始化空数据,所以ArrayList的底层实现是数据
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
	//线程安全与不安全在于看有没有使用锁机制,add没有使用锁,所以是不安全的
    public boolean add(E e) {
    	//确保容量
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //数据尾部添加新值
        elementData[size++] = e;
        return true;
    }

数组的查询、修改是根据下标操作所以快,但是添加和删除,需要移动元素所以比较慢,比如数组长度10000,添加或删除下标为200的值,下标200以后的元素就需要移动很多所以比较慢。

LinkedList简介
	//双向链表,有链表头,链表尾
	/**
     * first用于记录链表头部第一个节点元素的信息
     */
    transient Node<E> first;

    /**
	 * last用于记录链表尾部最后一个节点元素的信息
     */
    transient Node<E> last;
    
    //这个Node内部私有类存储着一个节点的关键信息
    private static class Node<E> {
    	//item当前节点的元素
        E item;
        //next代表当前节点元素的下一个节点元素
        Node<E> next;
        //prev代表当前节点元素的上一个节点元素
        Node<E> prev;
		//Node的构造方法
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
	//整个添加过程中,系统只做了两件事情,添加一个新的节点,然后保存该节点和前节点的引用关系
	public boolean add(E e) {
	    linkLast(e);
	    return true;
	}
	/**
	 * Links e as last element.
	 */
	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++;
	}

看上面的源码可以知道,当LinkedList添加一个元素时,会默认的往LinkedList最后一个节点后添加,
具体步骤为:
①获得最后一个节点last作为当前节点l
②获得最后一个节点last作为当前节点l
③用当前节点l、添加参数e、null创建一个新的Node对象
④将新创建的Node对象链接到最后节点,也就是last
⑤如果当前的LinkedList为空,那么添加的node就是first,也是last
⑥当前LinkedList的size+1,表示结构改变次数的对象modCount+1
整个添加过程中,系统只做了两件事情,添加一个新的节点,然后保存该节点和前节点的引用关系。所以新增快。

//删除
public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev; 
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

LinkedList的删除,会根据传递的参数进行null判断,null和非null的处理不一样,对于null使用==判断,对非null值使用.equals(Object o)判断。
LinkedList的删除,是改变节点之间的引用关系,当前节点的prev和next和当前链表中的其他元素不存在任何
联系。所以删除比较快。

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    private void checkElementIndex(int index) {
    if (!isElementIndex(index))
	        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
	}
	private boolean isElementIndex(int index) {
	    return index >= 0 && index < size;
	}
    
    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

通过阅读源码,我们也可以解读出LinkedList的查询逻辑
①根据传入的index去判断是否为LinkedList中的元素,判断逻辑为index是否在0和size之间,如果在则调用node(index)方法,否则抛出IndexOutOfBoundsException;
②调用node(index)方法,将size右移1位,即size/2,判断传入的size在LinkedList的前半部分还是后半部分
如果在前半部分,即index < size/2,则从fisrt节点开始遍历匹配
如果在后半部分,即index > size/2,则从last节点开始遍历匹配
可以看出,如果LinkedList链表size越大,则遍历的时间越长,查询所需的时间也越长。所以查询比较慢。

Vector简介
    public Vector() {
        this(10);
    }

    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }
    
    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

	
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

通过阅读源码,可知Vector的初始化是数组实现。并且add等方法使用了synchronized 关键字修饰,所以是线程安全的

	//线程安全与不安全在于看有没有使用锁机制,add没有使用锁,所以是不安全的
    public boolean add(E e) {
    	//确保容量
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //数据尾部添加新值
        elementData[size++] = e;
        return true;
    }

数组的查询、修改是根据下标操作所以快,但是添加和删除,需要移动元素所以比较慢,比如数组长度10000,
添加或删除下标为200的值,下标200以后的元素就需要移动很多所以比较慢。

问:如果需要保证ArrayList的线程安全需要怎么做?有几种方法?

考点:List的掌握情况
答:有3种。
  1. 使用synchronized同步所有ArrayList方法,自己写一个包装类,继承ArrayList,根据业务一般是add/update/remove加锁.
  2. List objects = Collections.synchronizedList(new ArrayList<>());
    方法其实底层也是在集合的所有方法之上加上了synchronized
    static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
        private static final long serialVersionUID = -7754090372962971524L;

        final List<E> list;

        SynchronizedList(List<E> list) {
            super(list);
            this.list = list;
        }
        SynchronizedList(List<E> list, Object mutex) {
            super(list, mutex);
            this.list = list;
        }

        public boolean equals(Object o) {
            if (this == o)
                return true;
            synchronized (mutex) {return list.equals(o);}
        }
        public int hashCode() {
            synchronized (mutex) {return list.hashCode();}
        }

        public E get(int index) {
            synchronized (mutex) {return list.get(index);}
        }
        public E set(int index, E element) {
            synchronized (mutex) {return list.set(index, element);}
        }
        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }
        public E remove(int index) {
            synchronized (mutex) {return list.remove(index);}
        }

        public int indexOf(Object o) {
            synchronized (mutex) {return list.indexOf(o);}
        }
        public int lastIndexOf(Object o) {
            synchronized (mutex) {return list.lastIndexOf(o);}
        }

        public boolean addAll(int index, Collection<? extends E> c) {
            synchronized (mutex) {return list.addAll(index, c);}
        }

        public ListIterator<E> listIterator() {
            return list.listIterator(); // Must be manually synched by user
        }

        public ListIterator<E> listIterator(int index) {
            return list.listIterator(index); // Must be manually synched by user
        }

        public List<E> subList(int fromIndex, int toIndex) {
            synchronized (mutex) {
                return new SynchronizedList<>(list.subList(fromIndex, toIndex),
                                            mutex);
            }
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            synchronized (mutex) {list.replaceAll(operator);}
        }
        @Override
        public void sort(Comparator<? super E> c) {
            synchronized (mutex) {list.sort(c);}
        }

        /**
         * SynchronizedRandomAccessList instances are serialized as
         * SynchronizedList instances to allow them to be deserialized
         * in pre-1.4 JREs (which do not have SynchronizedRandomAccessList).
         * This method inverts the transformation.  As a beneficial
         * side-effect, it also grafts the RandomAccess marker onto
         * SynchronizedList instances that were serialized in pre-1.4 JREs.
         *
         * Note: Unfortunately, SynchronizedRandomAccessList instances
         * serialized in 1.4.1 and deserialized in 1.4 will become
         * SynchronizedList instances, as this method was missing in 1.4.
         */
        private Object readResolve() {
            return (list instanceof RandomAccess
                    ? new SynchronizedRandomAccessList<>(list)
                    : this);
        }
    }
  • CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();使用ReentrantLock加锁.

追问:了解CopyOnWriteArrayList吗?和Collections.synchronizedList实现线程安全有什么区别,使用场景是什么?

答:
  • CopyOnWriteArrayList:执行修改操作,会拷贝一份新的数据(add/set/remove),开销比较大,修改后会将原来的集合指向新的集合,使用ReentrantLock来保证多个线程安全。
    场景:适合于读多写少的场景,(读操作不需要加锁,直接获取。删除和添加需要加锁)
  • Collections.synchronizedList:线程安全是因为每个方法都加了synchronized同步锁.
    场景:写操作比CopyOnWriteArrayList好,但是读操作不如CopyOnWriteArrayList适合,写多读少的业务场景。

追问2:CopyOnWriteArrayList的设计思想是什么?有什么缺点?

设计思想:读写分离+最终一致。缺点:内存占用问题,由于写时复制,内存里同时存再两个对象占用的内存,如果对象大则容易发生YongGC和FullGC。

注释:Copy On Write也是一种重要的思想,简称COW,在写少读多的情况下使用,如果不是写少读多的场景,
使用CopyOnWriteArrayList 开销比较大,每次更新操作(add/set/remove)都会做一次数组copy。

什么是COW?
如果多个用户同时访问相同资源,他们会获取到相同的指针指向相同的资源,直到某个用户修改资源的内容时,
系统才会真正复制一份专用的副本给该用户,而其它用户所见的最初的资源仍然保持不变。有点是如果用户没有修
改该资源,就不会有副本被创建,因此多个用户只是读取操作时可以共享同一份资源。

CopyOnWriteArrayList的特点:
①CopyOnWriteArrayList是线程安全的容器(相对于ArrayList),底层通过复制数组的方式实现.
②CopyOnWriteArrayList在遍历的时候不会抛出ConcurrentModificationException异常,并且遍历的
时候不用额外加锁.
③元素可以为null
追杀问:CopyOnWriteArrayList如何做到并发下遍历容器不发生异常?
通过查看源码iterator方法,返回COWIterator类.COWIterator中
private final Object[] snapshot;数组,根据源码可知这个数组是保存CopyOnWriteArrayList中
数据的数组.所以在迭代过程中修改原来集合的数据不会影响当前迭代器遍历.因为操作的不是同一个数组,
所以CopyOnWriteArrayList也不能保证数据的实时一致性.

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
面试题包括以下十九部分:Java 基础、容器、多线程、反射、对象拷贝、Java Web 模块、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、Mybatis、RabbitMQ、Kafka、Zookeeper、MySql、Redis、JVM 。 目录: 一、Java 基础 1.JDK 和 JRE 有什么区别? 2.== 和 equals 的区别是什么? 3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗? 4.final 在 java 中有什么作用? 5.java 中的 Math.round(-1.5) 等于多少? 6.String 属于基础的数据类型吗? 7.java 中操作字符串都有哪些类?它们之间有什么区别? 8.String str="i"与 String str=new String(“i”)一样吗? 9.如何将字符串反转? 10.String 类的常用方法都有那些? 11.抽象类必须要有抽象方法吗? 12.普通类和抽象类有哪些区别? 13.抽象类能使用 final 修饰吗? 14.接口和抽象类有什么区别? 15.java 中 IO 流分为几种? 16.BIO、NIO、AIO 有什么区别? 17.Files的常用方法都有哪些? 二、容器 18.java 容器都有哪些? 19.Collection 和 Collections 有什么区别? 20.List、Set、Map 之间的区别是什么? 21.HashMap 和 Hashtable 有什么区别? 22.如何决定使用 HashMap 还是 TreeMap? 23.说一下 HashMap 的实现原理? 24.说一下 HashSet 的实现原理? 25.ArrayList 和 LinkedList 的区别是什么? 26.如何实现数组和 List 之间的转换? 27.ArrayList 和 Vector 的区别是什么? 28.Array 和 ArrayList 有何区别? 29.在 Queue 中 poll()和 remove()有什么区别? 30.哪些集合类是线程安全的? 31.迭代器 Iterator 是什么? 32.Iterator 怎么使用?有什么特点? 33.Iterator 和 ListIterator 有什么区别? 34.怎么确保一个集合不能被修改? 三、多线程 35.并行和并发有什么区别? 36.线程和进程的区别? 37.守护线程是什么? 38.创建线程有哪几种方式? 39.说一下 runnable 和 callable 有什么区别? 40.线程有哪些状态? 41.sleep() 和 wait() 有什么区别? 42.notify()和 notifyAll()有什么区别? 43.线程的 run()和 start()有什么区别? 44.创建线程池有哪几种方式? 45.线程池都有哪些状态? 46.线程池中 submit()和 execute()方法有什么区别? 47.在 java 程序中怎么保证多线程的运行安全? 48.多线程锁的升级原理是什么? 49.什么是死锁? 50.怎么防止死锁? 51.ThreadLocal 是什么?有哪些使用场景? 52.说一下 synchronized 底层实现原理? 53.synchronized 和 volatile 的区别是什么? 54.synchronized 和 Lock 有什么区别? 55.synchronized 和 ReentrantLock 区别是什么? 56.说一下 atomic 的原理? 四、反射 57.什么是反射? 58.什么是 java 序列化?什么情况下需要序列化? 59.动态代理是什么?有哪些应用? 60.怎么实现动态代理? 五、对象拷贝 61.为什么要使用克隆? 62.如何实现对象克隆? 63.深拷贝和浅拷贝区别是什么? 六、Java Web 64.jsp 和 servlet 有什么区别? 65.jsp 有哪些内置对象?作用分别是什么? 66.说一下 jsp 的 4 种作用域? 67.session 和 cookie 有什么区别? 68.说一下 session 的工作原理? 69.如果客户端禁止 cookie 能实现 session 还能用吗? 70.spring mvc 和 struts 的区别是什么? 71.如何避免 sql 注入? 72.什么是 XSS 攻击,如何避免? 73.什么是 CSRF 攻击,如何避免? 七、异常 74.throw 和 throws 的区别? 75.final、finally、finalize 有什么区别? 76.try-catch-finally 中哪个部分
回答: Java中的List接口有多种实现,常见的有ArrayList、LinkedList和Vector等。Vector和ArrayList、LinkedList之间的联系和区别在于线程安全性和使用场景。Vector是List接口下线程安全的集合,而ArrayList和LinkedList则不是。ArrayList适用于查询较多的场合,因为它使用数组结构,可以通过索引快速访问元素。LinkedList适用于插入较多的场合,因为它使用链表结构,插入和删除元素的效率更高。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* [java集合框架List常见基础面试题](https://blog.csdn.net/weixin_39554889/article/details/128081878)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [10个Java经典的List面试题!](https://blog.csdn.net/s13166803785/article/details/131158336)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Java经典的List面试题](https://blog.csdn.net/weixin_47924016/article/details/130293657)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值