JavaSE基础(7)面试

八、Java 的集合

1. 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,对的,就是他,他是Map 结构,也是链表结构,有序的,更可喜的是他是 HashMap 的子类,我们返回 LinkedHashMap<Integer,User>即可,还符合面向接口(父类编程的思想)。但凡是对集合的操作,我们应该保持一个原则就是能用 JDK 中的 API 就有 JDK 中的 API,比如排序算法我们不应该去用冒泡或者选择,而是首先想到用 Collections 集合工具类。

2. public class HashMapTest {
3. public static void main(String[] args) {
4. HashMap<Integer, User> users = new HashMap<>();
5. users.put(1, new User("张三", 25));
6. users.put(3, new User("李四", 22));
7. users.put(2, new User("王五", 28));
8. System.out.println(users);
9. HashMap<Integer,User> sortHashMap = sortHashMap(users);
10. System.out.println(sortHashMap);
11. /**
12. * 控制台输出内容
13. * {1=User [name=张三, age=25], 2=User [name=王五, age=28], 3=User [name=李四, age=22]}
14. {2=User [name=王五, age=28], 1=User [name=张三, age=25], 3=User [name=李四, age=22]}
15. */
16. }
16.
17. public static HashMap<Integer, User> sortHashMap(HashMap<Integer, User> map) {
18. // 首先拿到 map 的键值对集合
19. Set<Entry<Integer, User>> entrySet = map.entrySet();
20.
20. // 将 set 集合转为 List 集合,为什么,为了使用工具类的排序方法
21. List<Entry<Integer, User>> list = new ArrayList<Entry<Integer, User>>(entrySet);
22. // 使用 Collections 集合工具类对 list 进行排序,排序规则使用匿名内部类来实现
23. Collections.sort(list, new Comparator<Entry<Integer, User>>() {
25.
24. @Override
25. public int compare(Entry<Integer, User> o1, Entry<Integer, User> o2) {
26. //按照要求根据 User 的 age 的倒序进行排
27. return o2.getValue().getAge()-o1.getValue().getAge();
28. }
29. });
30. //创建一个新的有序的 HashMap 子类的集合
31. LinkedHashMap<Integer, User> linkedHashMap = new LinkedHashMap<Integer, User>();
32. //将 List 中的数据存储在 LinkedHashMap 中
33. for(Entry<Integer, User> entry : list){
34. linkedHashMap.put(entry.getKey(), entry.getValue());
35. }
36. 38. //返回结果
39. return linkedHashMap;
40. }
41. }

2. 集合的安全性问题
请问 ArrayList、HashSet、HashMap 是线程安全的吗?如果不是我想要线程安全的集合怎么办?
我们都看过上面那些集合的源码(如果没有那就看看吧),每个方法都没有加锁,显然都是线程不安全的。话又说过来如果他们安全了也就没第二问了。在集合中 Vector 和 HashTable 倒是线程安全的。你打开源码会发现其实就是把各自核心方法添加上了synchronized 关键字。
Collections 工具类提供了相关的 API,可以让上面那 3 个不安全的集合变为安全的。

  1. // Collections.synchronizedCollection©
  2. // Collections.synchronizedList(list)
  3. // Collections.synchronizedMap(m)
  4. // Collections.synchronizedSet(s)

上面几个函数都有对应的返回值类型,传入什么类型返回什么类型。打开源码其实实现原理非常简单,就是将集合的核心方法添加上了 synchronized 关键字。
3. ArrayList 内部用什么实现的?
(回答这样的问题,不要只回答个皮毛,可以再介绍一下 ArrayList 内部是如何实现数组的增加和删除的,因为数组在创建的时候长度是固定的,那么就有个问题我们往ArrayList 中不断的添加对象,它是如何管理这些数组呢?)
ArrayList 内部是用 Object[]实现的。接下来我们分别分析 ArrayList 的构造、add、remove、clear 方法的实现原理。
一、构造函数
1)空参构造

/**
* 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]数组。
2)带参构造 1

/**
* 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,则创建一个长度为该值的新数组。

3)带参构造 2

/**
* 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。这里面它先判断 a.getClass 是否等于 Object[].class,其实一般都是相等的,我也暂时没想明白为什么多加了这个判断,toArray 方法是 Collection 接口定义的,因此其所有的子类都有这样的方法,list 集合的 toArray 和 Set 集合的 toArray返回的都是 Object[]数组。
这里讲些题外话,其实在看 Java 源码的时候,作者的很多意图都很费人心思,我能知道他的目标是啥,但是不知道他为何这样写。比如对于 ArrayList, array 是他的成员变量,但是每次在方法中使用该成员变量的时候作者都会重新在方法中开辟一个局部变量,然后给局部变量赋值为 array,然后再使用,有人可能说这是为了防止并发修改 array,毕竟 array 是成员变量,大家都可以使用因此需要将 array 变为局部变量,然后再使用,这样的说法并不是都成立的,
也许有时候就是老外们写代码的一个习惯而已。
二、add 方法
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;
}

1、首先将成员变量 array 赋值给局部变量 a,将成员变量 size 赋值给局部变量 s。 2、判断集合的长度 s 是否等于数组的长度(如果集合的长度已经等于数组的长度了,说明数组已经满了,该重新
分配新数组了),重新分配数组的时候需要计算新分配内存的空间大小,如果当前的长度小于
MIN_CAPACITY_INCREMENT/2(这个常量值是 12,除以 2 就是 6,也就是如果当前集合长度小于 6)则分配 12 个长度,如果集合长度大于 6 则分配当前长度 s 的一半长度。这里面用到了三元运算符和位运算,s >> 1,意思就是将
s 往右移 1 位,相当于 s=s/2,只不过位运算是效率最高的运算。
3、将新添加的 object 对象作为数组的 a[s]个元素。
4、修改集合长度 size 为 s+1
5、modCotun++,该变量是父类中声明的,用于记录集合修改的次数,记录集合修改的次数是为了防止在用迭代
器迭代集合时避免并发修改异常,或者说用于判断是否出现并发修改异常的。
6、return true,这个返回值意义不大,因为一直返回 true,除非报了一个运行时异常。
三、remove 方法

remove 方法有两个重载,我们只研究 remove(int index)方法。
/**
* 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;
}

1、先将成员变量 array 和 size 赋值给局部变量 a 和 s。
2、判断形参 index 是否大于等于集合的长度,如果成了则抛出运行时异常
3、获取数组中脚标为 index 的对象 result,该对象作为方法的返回值
4、调用 System 的 arraycopy 函数,拷贝原理如下图所示。
在这里插入图片描述
5、接下来就是很重要的一个工作,因为删除了一个元素,而且集合整体向前移动了一位,因此需要将集合最后一
个元素设置为 null,否则就可能内存泄露。
6、重新给成员变量 array 和 size 赋值
7、记录修改次数
8、返回删除的元素(让用户再看最后一眼)
四、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。
4. 并发集合和普通集合如何区别?
并发集合常见的有 ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque 等。并发集合位 于 java.util.concurrent 包 下 , 是 jdk1.5 之 后 才 有 的 , 主 要 作 者 是 Doug Lea
http://baike.baidu.com/view/3141057.htm)完成的。
在 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
内存模型、并发机制深刻掌握的体现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值