2022 年各大厂最新Java集合面试题资料整合及答案

2022/2/4 12:42:44

本文主要是介绍2022 年各大厂最新Java集合面试题资料整合及答案,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

题1:Iterator 和 Enumeration 接口有哪些区别?

Iterator是JDK 1.2版本中添加的接口,支持HashMap、ArrayList等集合遍历接口。Iterator是支持fail-fast机制,当多个线程对同一个集合的内容进行操作时可能产生fail-fast事件。

Iterator有3个方法接口,Iterator能读取集合数据且可以对数据进行删除操作,而Enumeration只有2个方法接口,通过Enumeration只能读取集合的数据,而不能对数据进行修改。

Enumeration接口的处理性能是Iterator的两倍且内存使用也更少,但是Iterator接口比Enumeration要安全很多,主要是因为其他线程不能够修改正在被iterator遍历的集合中的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration接口来说是不可能的。

迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者从集合中移除元素,而Enumeration不能实现。

Enumeration是JDK 1.0版本中添加的接口。Enumeration的方法接口为Vector、Hashtable等类提供了遍历接口。Enumeration本身不支持同步,而在Vector、Hashtable实现Enumeration时添加了同步。

题2:Vector 和 ArrayList 有什么区别和联系?

相同点:

1)实现原理相同,底层都使用数组。

2)功能相同,实现增删改查等操作的方法相似。

3)都是长度可变的数组结构,很多情况下可以互用。

不同点:

1)Vector是早期JDK版本提供,ArrayList是新版本替代Vector的。

2)Vector线程安全,ArrayList重速度轻安全,线程非安全。

长度需增长时,Vector默认增长一倍,ArrayList增长50%。

题3:Java 中如何判断 “java.util.LinkedList” 字符串实现 List 接口?

格式:判断一个类实现某接口

if(对象名 instanceof 接口名){
 
}

实例:判断“java.util.LinkedList”字符串实现List接口

String classes = "java.util.LinkedList";

if(Class.forName(classes).newInstance() instanceof List) {
	System.out.println(true);
}else {
	System.out.println(false);
}

题4:泛型有什么使用场景?

当类中要操作的引用数据类型不确定时,在JDK1.5版本前使用Object来完成扩展,JDK1.5后推荐使用泛型来完成扩展,同时保证安全性。

题5:Java 中如何创建和遍历单链表?

public class LinkList {
	public Node head;
	public Node current;

	//方法:向链表中添加数据  
	public void add(int data) {
		// 判断链表为空
		if (head == null) {// 如果头结点为空,说明这个链表还没有创建,那就把新的结点赋给头结点
			head = new Node(data);
			current = head;
		} else {
			// 创建新的结点,放在当前节点的后面(把新的结点合链表进行关联)
			current.next = new Node(data);
			// 把链表的当前索引向后移动一位
			current = current.next; // 此步操作完成之后,current结点指向新添加的那个结点
		}
	}

	//方法:遍历链表(打印输出链表。方法的参数表示从节点node开始进行遍历  
	public void print(Node node) {
		if (node == null) {
			return;
		}

		current = node;
		while (current != null) {
			System.out.println(current.data);
			current = current.next;
		}
	}

	class Node {
		//注:此处的两个成员变量权限不能为private,因为private的权限是仅对本类访问。  
		int data; // 数据域
		Node next;// 指针域

		public Node(int data) {
			this.data = data;
		}
	}

	public static void main(String[] args) {
		LinkList list = new LinkList();
		//向LinkList中添加数据  
		for (int i = 0; i < 10; i++) {
			list.add(i);
		}

		list.print(list.head);// 从head节点开始遍历输出
	}

}

执行结果

0
1
2
3
4
5
6
7
8
9

查看上述代码,Node节点采用的是内部类来表示。使用内部类的最大好处是可以和外部类进行私有操作的互相访问。

内部类访问的特点是:内部类可以直接访问外部类的成员,包括私有;外部类要访问内部类的成员,必须先创建对象。

为了方便添加和遍历的操作,在LinkList类中添加一个成员变量current,用来表示当前节点的索引。

遍历链表的方法中,参数node表示从node节点开始遍历,不一定要从head节点遍历。

题6:JDK1.8 中对 HashMap 和 ConcurrentHashMap 做了哪些优化?

JDK1.8之前,其数据结构为数组加链表。JDK1.8之后的优化,其数据结构变成了数组+链表+红黑树

当链表上的结点过多时,查询一个结点,在JDK1.8之前,需要遍历整个节点,效率为O(n)。而在JDK1.8中,如果结点达到阈值TREEIFY_THRESHOLD(默认为8)时,会将链表结构转成红黑树结构,这样再查询时,如果数组的first结点是树结构,则采用树的查询算法,效率为O(logn),否则还是遍历链表。参见JDK1.8源码

当树上结点过多时,阈值为UNTREEIFY_THRESHOLD(默认为6),会进行树转链表操作。至于为什么不是8,是为了防止平凡的进行树–链表的转换。

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 第一个结点为红黑树
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 否则遍历链表
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

2、JDK1.8之前,ConcurrentHashMap通过将整个Map划分成N(默认16个)个Segment,而Segment继承自ReentrantLock ,通过对每个Segment加锁来实现线程安全。而在JDK1.8后,摒弃了这种实现方式,采用了CAS + Synchronized,对链表头结点进行加锁,来实现线程安全。参考JDK1.8源码

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 此处f即为链表的头结点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

题7:Java 数组中可以使用泛型吗?

Array不支持泛型,这是因为编译时会造成类型安全问题。

Java范型停留在编译层运行时,这些范型的信息会被抹除掉。Java中这种做法不必修改JVM,减少了潜在的大幅改动而产生的风险,但是这也许反映出了Java Bytecode规范在设计之初的先天不足。

Java中Object[]数组可以是任何数组的父类或者任何一个数组都可以向上转型成它在定义时指定元素类型的父类的数组,如果这时存放不同于原始数据类型但是满足后来使用父类类型的话,编译不会有问题,但是在运行时会检查加入数组对象的类型,于是会抛出ArrayStoreException异常。

比如代码如下,就会抛出ArrayStoreException异常。

String[] arrays = new String[20];
Object[] obj = arrays;
obj[0] = new Integer(1); // throws ArrayStoreException at runtime

注意的是List可以提供编译期的类型安全保证,而Array不能。

题8:Java 中遍历 List 集合都有哪些方式?

List<String> list = new ArrayList<>();

for (int i = 0; i < list.size(); i++) {
	System.out.println(list.get(i));
}

//使用for-each循环
for(String obj : list){
	System.out.println(obj);
}

//使用迭代器 iterator
Iterator<String> it = list.iterator();
while(it.hasNext()){
  String obj = it.next();
  System.out.println(obj);
}

遍历List集合时使用迭代器方式线程安全,可以保障遍历的集合元素不被修改,否则抛出异常ConcurrentModificationException。

题9:Java 中迭代集合如何避免 ConcurrentModificationException?

遍历集合的时可以使用并发集合类来避免ConcurrentModificationException异常。

例如使用CopyOnWriteArrayList集合,而不是ArrayList集合。

题10:HashMap 长度为什么是2的幂次方?

HashMap默认长度16,扩容是2的n次方。

HashMap为了存取高效,要尽量较少碰撞,通俗的说就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就是要把数据存到哪个链表中的算法。

取模即hash%length,计算机中直接求余效率不如位移运算,JDK源码中做了相应的优化hash&(length-1),hash%length==hash&(length-1)的前提是length是2的n次方;

比如说10%8=2,10的二进制是1010,8的二进制是0100

0000 1010
0000 1000

这种取余操作对应2的幂次实际上也就是除数的值之前的数据被整除,后面的余数就是被除数剩余部分为1的值。

0000 1010除以0000 1000把0000 1000高位以及自己抹除,剩下0000 0010。换成与(&)的操作,就是把n(除数)-1和被除数,取余的结果“(n-1)&hash ”。

为什么这样可以均匀分布减少碰撞呢?

2的n次方实即为1后面有n个0,2的n次方-1即为n个1;

比如长度为9时,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;

比如长度为8时,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞。

其实就是按位“与”的时候,每一位都能&1,也就是和1111……1111111进行与运算。

Hash值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是内存是无法存储这种量级别数组的,所以这个散列值是不能直接使用的。可以针对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“(n-1) & hash”。这也就解释了HashMap的长度为什么是2的幂次方。

题11:java-中如何确保一个集合不能被修改

题12:java-中如何查找单链表中的中间结点

题13:hashmap-为什么多线程会导致死循环

题14:java-中如何合并两个有序单链表后依然有序

题15:jdk1.8-和-jdk1.7-中-arraylist-的初始容量多少

题16:java-迭代器-iterator-是什么

题17:iterater-和-listiterator-都有哪些区别

题18:java-集合中都有哪些根接口

题19:什么是-hashmap

题20:说一说-hashset的使用和原理

题21:java-泛型中-etkv等标记符是什么含义

题22:comparable-和-comparator有什么区别

题23:java-中求单链表中节点的个数

题24:arrays.aslist()-有什么使用限制

题25:为什么-map-接口不继承-collection-接口

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述



这篇关于2022 年各大厂最新Java集合面试题资料整合及答案的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程