Java集合Java面试题

Posted 北芳科技

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java集合Java面试题相关的知识,希望对你有一定的参考价值。

Java集合【Java面试题】

1、Java集合框架图

2、HashMap排序题

已知一个 HashMap<Integer,User>集合, User 有 name(String)和 age(int)属性。

请写一个方法实现对HashMap 的排序功能,该方法接收 HashMap<Integer,User>为形参,
返回类型为 HashMap<Integer,User>,要求对 HashMap 中的 User 的 age 倒序进行排序。

排序时 key=value 键值对不得拆散。

注意:要做出这道题必须对集合的体系结构非常的熟悉。

HashMap本身就是不可排序的,但是该题偏偏让HashMap排序,
那我们就得想在API中有没有这样的 Map 结构是有序的,我们不难发现其中LinkedHashMap就具有这样的结构,

是链表结构有序的,更可喜的是他是 HashMap的子类,
我们返回LinkedHashMap<Integer,User>即可,还符合面向接口编程的思想。

但凡是对集合的操作,我们应该保持一个原则就是能用JDK中的API就用JDK中的 API,
比如排序算法我们不应该去用冒泡或者选择,而是首先想到用 Collections 集合工具类。

import java.util.*;

class HashMapTest 
    public static void main(String[] args) 
        HashMap<Integer, User> users = new HashMap<>();
        users.put(1, new User("张三", 25));
        users.put(3,new User("李四",22));
        users.put(2, new User("王五", 28));
        System.out.println(users);
        HashMap<Integer, User> sortHashMap = sortHashMap(users);
        System.out.println(sortHashMap);
        /**
         * 控制台输出内容
         * 1=User [name=张三, age=25], 2=User [name=王五,age=28], 3=User [name=李四, age=22]
         * 2=User [name=王五, age=28], 1=User [name=张三, age=25], 3=User [name=李四, age=22]
         */
    

    public static HashMap<Integer, User> sortHashMap(HashMap<Integer, User> map) 
        // 首先拿到 map 的键值对集合
        Set<Map.Entry<Integer, User>> entrySet = map.entrySet();
        // 将 set 集合转为 List 集合,为什么,为了使用工具类的排序方法
        List<Map.Entry<Integer,User>> list = new ArrayList<Map.Entry<Integer, User>>(entrySet);
        // 使用 Collections 集合工具类对 list 进行排序,排序规则使用匿名内部类来实现
        Collections.sort(list, new Comparator<Map.Entry<Integer, User>>() 
            @Override
            public int compare(Map.Entry<Integer, User> o1, Map.Entry<Integer, User> o2) 
                //按照要求根据 User 的 age 的倒序进行排
                return o2.getValue().getAge() - o1.getValue().getAge();
            
        );
        //创建一个新的有序的 HashMap 子类的集合
        LinkedHashMap<Integer, User> linkedHashMap = new LinkedHashMap<Integer, User>();
        //将 List 中的数据存储在 LinkedHashMap 中
        for (Map.Entry<Integer,User> entry : list) 
            linkedHashMap.put(entry.getKey(), entry.getValue());
        
        return linkedHashMap;
    


class User 
    private String name;
    private int age;

    public User(String name, int age) 
        this.name = name;
        this.age = age;
    

    public int getAge() 
        return age;
    

    public void setAge(int age) 
        this.age = age;
    

    public String getName() 
        return name;
    

    public void setName(String name) 
        this.name = name;
    

    @Override
    public String toString() 
        return "User" +
                "name='" + name + '\\'' +
                ", age=" + age +
                '';
    

3、请问 ArrayList、HashSet、HashMap 是线程安全的吗?如果不是怎么获取线程安全的集合?

通过以上类的源码进行分析,每个方法都没有加锁,显然都是非线程安全的。

在集合中VectorHashTable是线程安全的。

打开源码会发现其实就是把各自核心方法添加上了synchronized 关键字。

Collections工具类提供了相关的 API,可以让上面那3个不安全的集合变为安全的。

Collections.synchronizedCollection(c);
Collections.synchronizedList(list);
Collections.synchronizedMap(m);
Collections.synchronizedSet(s);

4、ArrayList内部用什么实现的?

回答这样的问题,不要只回答个皮毛,

可以再介绍一下ArrayList内部是如何实现数组的增加和删除的,
因为数组在创建的时候长度是固定的,
那么就有个问题我们往ArrayList中不断的添加对象,
它是如何管理这些数组呢?

通过源码可以看到ArrayList内部是用Object[]实现的。

接下来我们分别分析ArrayList的构造以及add()、remove()、clear()方法的实现原理。

● 无参数构造方法

/**
 * Constructs a new @code ArrayList instance with zero initial capacity.
 */
public ArrayList()
    array=EmptyArray.OBJECT;

array 是一个 Object[]类型。

当我们 new 一个空参构造时系统调用了 EmptyArray.OBJECT 属性,

EmptyArray 仅仅是一个系统的类库,该类源码如下:

public final class EmptyArray 
    private EmptyArray() 
    
    public static final boolean[] BOOLEAN = new boolean[0];
    public static final byte[] BYTE = new byte[0];
    public static final char[] CHAR = new char[0];
    public static final double[] DOUBLE = new double[0];
    public static final int[] INT = new int[0];
    public static final Class<?>[] CLASS = new Class[0];
    public static final Object[] OBJECT = new Object[0];
    public static final String[] STRING = new String[0];
    public static final Throwable[] THROWABLE = new Throwable[0];
    public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];

也就是说当我们 new 一个空参 ArrayList 的时候,系统内部使用了一个 new Object[0]数组。

● 带容量参数的构造器

/**
 * Constructs a new instance of @code ArrayList with the specified
 * initial capacity.
 * @param capacity the initial capacity of this @code ArrayList.
 */
public ArrayList(int capacity) 
    if (capacity < 0) 
        throw new IllegalArgumentException("capacity < 0: " + capacity);
    
    array = (capacity == 0 ? EmptyArray.OBJECT : new Object[capacity]);

该构造函数传入一个 int 值,该值作为数组的长度值。
如果该值小于 0,则抛出一个运行时异常。如果等于 0,则使用一个空数组,如果大于 0,则创建一个长度为该值的新数组。

● 带集合参数的构造器

/**
 * Constructs a new instance of @code ArrayList containing the elements of
 * the specified collection.
 *
 * @param collection the collection of elements to add.
 */
public ArrayList(Collection<? extends E> collection) 
    if (collection == null) 
        throw new NullPointerException("collection == null");
    

    Object[] a = collection.toArray();
    if (a.getClass() != Object[].class) 
        Object[] newArray = new Object[a.length];
        System.arraycopy(a, 0, newArray, 0, a.length);
        a = newArray;
    
    array = a;
    size = a.length;

如果调用构造函数的时候传入了一个 Collection 的子类,那么先判断该集合是否为 null,为 null 则抛出空指针异常。

如果不是则将该集合转换为数组 a,然后将该数组赋值为成员变量 array,将该数组的长度作为成员变量 size。

● add方法

/**
 * Adds the specified object at the end of this @code ArrayList.
 *
 * @param object the object to add.
 * @return always true
 */
@Override
public boolean add(E object) 
    Object[] a = array;
    int s = size;
    if (s == a.length) 
        Object[] newArray = new Object[s +
                (s < (MIN_CAPACITY_INCREMENT / 2) ? MIN_CAPACITY_INCREMENT : s >> 1)];
        System.arraycopy(a, 0, newArray, 0, s);
        array = a = newArray;
    
    a[s] = object;
    size = s + 1;
    modCount++;
    return true;

● 第一:首先将成员变量 array 赋值给局部变量 a,将成员变量 size 赋值给局部变量 s。

● 第二:判断集合的长度 s 是否等于数组的长度(如果集合的长度已经等于数组的长度了,说明数组已经满了,该重新分 配 新 数 组 了 ) ,

重 新 分 配 数 组 的 时 候 需 要 计 算 新 分 配 内 存 的 空 间 大 小 , 如 果 当 前 的 长 度 小 于MIN_CAPACITY_INCREMENT/2(这个常量值是 12,除以 2 就是 6,也就是如果当前集合长度小于 6)则分配 12 个长度,如果集合长度大于 6 则分配当前长度 s 的一半长度。

这里面用到了三元运算符和位运算,s >> 1,意思就是将s 往右移 1 位,相当于 s=s/2,只不过位运算是效率最高的运算。

● 第三:将新添加的 object 对象作为数组的 a[s]个元素。

● 第四:修 改 集 合 长 度size为s+1。

● 第五:modCount++,该变量是父类中声明的,用于记录集合修改的次数,记录集合修改的次数是为了防止在用迭代器迭代集合时避免并发修改异常,或者说用于判断是否出现并发修改异常的。

● 第六:return true,这个返回值意义不大,因为一直返回 true,除非报了一个运行时异常。

● remove方法

/**
 * Removes the object at the specified location from this list.
 *
 * @param index the index of the object to remove.
 * @return the removed object.
 * @throws IndexOutOfBoundsException when @code location < 0 || location >= size()
 */
@Override
public E remove(int index) 
    Object[] a = array;
    int s = size;
    if (index >= s) 
        throwIndexOutOfBoundsException(index, s);
    
    @SuppressWarnings("unchecked") E result = (E) a[index];
    System.arraycopy(a, index + 1, a, index, --s - index);
    a[s] = null; // Prevent memory leak
    size = s;
    modCount++;
    return result;

● 第一:先将成员变量 array 和 size 赋值给局部变量 a 和 s。

● 第二:判断形参 index 是否大于等于集合的长度,如果成了则抛出运行时异常

● 第三:获取数组中脚标为 index 的对象 result,该对象作为方法的返回值

● 第四:调用 System 的 arraycopy 函数完成数组拷贝。

● 第五:接下来就是很重要的一个工作,因为删除了一个元素,而且集合整体向前移动了一位,因此需要将集合最后一个元素设置为 null,否则就可能内存泄露。

● 第六:重新给成员变量 array 和 size 赋值。

● 第七:记录修改次数。

● 第八:返回删除的元素。

● clear方法

/**
 * Removes all elements from this @code ArrayList, leaving it empty.
 *
 * @see #isEmpty
 * @see #size
 */
@Override
public void clear() 
    if (size != 0) 
        Arrays.fill(array, 0, size, null);
        size = 0;
        modCount++;
    

如果集合长度不等于 0,则将所有数组的值都设置为 null,然后将成员变量 size 设置为 0 即可,最后让修改记录加 1。

5、并发集合和普通集合如何区别?

并发集合常见的有ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque等。并发集合位于java.util.concurrent包下,是jdk1.5之后才有的,在 java 中有普通集合、同步(线程安全)的集合、并发集合。

普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性。线程安全集合仅仅是给集合添加了 synchronized 同步锁,严重牺牲了性能,而且对并发的效率就更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率。

参考阅读:ConcurrentHashMap 是线程安全的 HashMap 的实现,默认构造同样有 initialCapacity 和 loadFactor 属性, 不过还多了一个 concurrencyLevel 属性,三属性默认值分别为 16、0.75 及 16。

其内部使用锁分段技术,维持这锁Segment 的数组,在 Segment 数组中又存放着 Entity[]数组,内部 hash 算法将数据较均匀分布在不同锁中。

put 操作:并没有在此方法上加上 synchronized,首先对 key.hashcode 进行 hash 操作,得到 key 的 hash 值。hash 操作的算法和map 也不同,根据此hash 值计算并获取其对应的数组中的Segment 对象(继承自ReentrantLock),接着调用此 Segment 对象的 put 方法来完成当前操作。

ConcurrentHashMap 基于 concurrencyLevel 划分出了多个 Segment 来对 key-value 进行存储,从而避免每次 put 操作都得锁住整个数组。在默认的情况下,最佳情况时可允许 16 个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。

get(key)首先对 key.hashCode 进行 hash 操作,基于其值找到对应的 Segment 对象,调用其 get 方法完成当前操作。而 Segment 的 get 操作首先通过 hash 值和对象数组大小减1的值进行按位与操作来获取数组上对应位置的HashEntry。

在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的 HashEntry 产生不一致性,那么 ConcurrentHashMap 是如何保证的?对象数组大小的改变只有在 put 操作时有可能发生,由于 HashEntry 对象数组对应的变量是 volatile 类型的,因此可以保证如 HashEntry 对象数组大小发生改变,读操作可看到最新的对象数组大小。

在获取到了 HashEntry 对象后,怎么能保证它及其 next 属性构成的链表上的对象不会改变呢?这点ConcurrentHashMap 采用了一个简单的方式,即 HashEntry 对象中的 hash、key、next 属性都是 final 的,这也就意味着没办法插入一个 HashEntry 对象到基于 next 属性构成的链表中间或末尾。这样就可以保证当获取到 HashEntry 对象后,其基于 next 属性构建的链表是不会发生变化的。

ConcurrentHashMap 默认情况下采用将数据分为 16 个段进行存储,并且 16 个段分别持有各自不同的锁Segment,锁仅用于 put 和 remove 等改变集合对象的操作,基于 volatile 及 HashEntry 链表的不变性实现了读取的不加锁。这些方式使得 ConcurrentHashMap 能够保持极好的并发支持,尤其是对于读远比插入和删除频繁的 Map 而言,而它采用的这些方法也可谓是对于 Java 内存模型、并发机制深刻掌握的体现。

5、List 和 Map、Set 的区别?
● 结构特点:List 和 Set 是存储单列数据的集合,Map 是存储键和值这样的双列数据的集合;
List 中存储的数据是有顺序,并且允许重复;Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的,Set 中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的 hashCode 决定,位置是固定的(Set 集合根据 hashCode 来进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说 Set 中的元素还是无序的);

● 实现类:List 接口下的实现类(LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢;ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不便于插入删除;Vector:基于数组实现,线程安全的,效率低)。
Map 接口下的实现类(HashMap:基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键;Hashtable:线程安全,低效,不支持 null 值和 null 键;LinkedHashMap:是HashMap 的一个子类,保存了记录的插入顺序;SortedMap 接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序)。
Set 接口下的实现类(HashSet:底层是由 HashMap 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()hashCode()方法;LinkedHashSet继承与 HashSet,同时又基于LinkedHashMap 来进行实现,底层使用的是LinkedHashMp)。

● 区别:List集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过list.get(i)方法来获取集合中的元素;Map中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复;Set集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定的方式排序,例如 TreeSet类,可以按照默认顺序,也可以通过实现 java.util.Comparator接口来自定义排序方式。

6、HashMap和Hashtable有什么区别?
HashMap是非线程安全的,HashMap是Map的一个实现类,是将键映射到值的对象,不允许键值重复。
允许空键和空值;由于非线程安全,HashMap的效率要较 Hashtable 的效率高一些。Hashtable 是线程安全的一个集合,不允许 null 值作为一个 key 值或者value 值;Hashtable是sychronized,多个线程访问时不需要自己为它的方法实现同步,而 HashMap 在被多个线程访问的时候需要自己为它的方法实现同步。

6、数组和链表分别比较适合用于什么场景,为什么?

● 数组和链表的区别

数组是将元素在内存中连续存储的;它的优点:因为数据是连续存储的,内存地址连续,所以在查找数据的时候效率比较高;它的缺点:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大小。在运行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据两比较大的时候,有可能会出现越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。
在改变数据个数时,增加、插入、删除数据效率比较低。链表是动态申请内存空间,不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,根据需要来动态申请或者删除内存空间,对于数据增加和删除以及插入比数组灵活。还有就是链表中数据在内存中可以在任意的位置,通过应用来关联数据(就是通过存在元素的地址来联系)

● 链表和数组使用场景

数组应用场景:数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建的线性表较稳定。

链表应用场景:对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。

7、Java中ArrayList和LinkedList区别?

ArrayList和Vector使用了数组的实现,可以认为 ArrayList 或者 Vector 封装了对内部数组的操作,比如向数组中添加、删除、插入新的元素或者数据的扩展和重定向。

LinkedList 使用了循环双向链表数据结构。与基于数组的 ArrayList 相比,这是两种截然不同的实现技术,这也决定了它们将适用于完全不同的工作场景。

LinkedList 链表由一系列表项连接而成。一个表项总是包含 3 个部分:元素内容,前驱表和后驱表,如图所示:

在下图展示了一个包含 3 个元素的 LinkedList 的各个表项间的连接关系。
在 JDK 的实现中,无论 LikedList 是否为空,链表内部都有一个 header 表项,它既表示链表的开始,也表示链表的结尾。
表项 header 的后驱表项便是链表中第一个元素,表项 header 的前驱表项便是链表中最后一个元素。

ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。
如果集合数据是对于集合随机访问 get 和 set,ArrayList 绝对优于 LinkedList,因为 LinkedList 要移动指针。
如果集合数据是对于集合新增和删除操作 add 和 remove,LinkedList 比较占优势,因为ArrayList要移动数据。

ArrayList 和 LinkedList 是两个集合类,用于存储一系列的对象引用(references)。例如我们可以用 ArrayList 来存储一系列的 String 或者 Integer。
那么 ArrayList 和 LinkedList 在性能上有什么差别呢?什么时候应该用 ArrayList 什么时候又该用 LinkedList 呢?

● 时间复杂度

首先一点关键的是,ArrayList 的内部实现是基于基础的对象数组的,因此,它使用 get 方法访问列表中的任意一个元素时(random access),它的速度要比 LinkedList 快。
LinkedList 中的 get 方法是按照顺序从列表的一端开始检查,直到另外一端。对 LinkedList 而言,访问列表中的某个指定元素没有更快的方法了。

假设我们有一个很大的列表,它里面的元素已经排好序了,这个列表可能是 ArrayList 类型的也可能是 LinkedList 类型的,现在我们对这个列表来进行二分查找(binary search),比较列表是 ArrayList 和 LinkedList 时的查询速度,看下面的程序:

public class TestList 
    public static final int N = 50000;    //50000 个数
    public static List values; //要查找的集合
    //放入 50000 个数给 value;
    static 
        Integer vals[] = new Integer[N];
        Random r = new Random();
        for (int i = 0, currval = 0; i < N; i++) 
            vals = new Integer(currval);
            currval += r.nextInt(100) + 1;
        
        values = Arrays.asList(vals);
    
    //通过二分查找法查找
    static long timeList(List lst) 
        long start = System.currentTimeMillis();
        for (int i = 0; i < N; i++) 
            int index = Collections.binarySearch(lst, values.get(i));
            if (index != i)
                System.out.println("***错误***");
        
        return System.currentTimeMillis() - startJava集合面试题汇总篇

Java集合---面试题

Java集合面试题看这篇就够了

Java集合面试题看这篇就够了

Java分布式面试题集合

面试题:Java集合面试题(40道)