面试宝典--Java集合类

Java集合框架

Java集合常见面试题:

  1. 集合和数组的区别?
  2. 常见的集合框架及其底层数据结构?
  3. List、Set、Map的区别?
  4. ArrayList的扩容机制?
  5. ArrayList、Vector、LinkedList区别?
  6. HashSet、LinkedHashSet、TreeSet的异同?
  7. HashMap的底层实现?
  8. HashMap和HashTable、HashSet、TreeMap的区别?
  9. HashMap的长度为什么是2的幂次方?
  10. HashMap的几种常见遍历方式?
  11. ConcurrentHashMap 和HashTable的区别?
  12. ConcurrentHashMap的线程安全是如何实现的?

1. 集合和数组的区别

  • 数组是固定的长度、集合长度是可变的;
  • 数组可以存储基本数据类型或者引用数据类型,集合只能存储引用数据类型;
  • 数组存储的元素必须是同一数据类型,集合存储的对象可以是不同的数据类型。

2. 集合框架底层的数据结构

Collection接口和Map接口是所有集合框架的父接口。

  • Collection接口的子接口包括:List接口Set接口

    • List接口的实现类:
      • ArrayList: Object[] 数组
      • Vector: Obejct[] 数组
      • LinkedList: 双向链表
    • Set接口的实现类:
      • HashSet: 底层采用HashMap保存元素(存储的元素无序,唯一)
      • LinkedHashSet: 采用LinkedHashMap保存元素(HashSet的子类)
      • TreeSet: 底层采用了红黑树(存储的元素有序,唯一)
  • Map接口的主要实现类

    • HashMap: JDK1.8之前底层采用数组+链表,JDK1.8以后当链表长度大于阈值(默认为8)时,将链表转化为`红黑树
    • LinkedHashMap: 数组+ 链表(红黑树)
    • TreeMap: 红黑树
    • HashTable: 数组+ 链表
    • ConcurrentHashMap: JDK1.7 采用分段数组+链表,JDK1.8采用Node数组+链表+红黑树,并发采用sychronized和CAS操作

3. List、Set、Map的区别

  • List 存储的元素是有序、可重复的;
  • Set 存储的元素是无序、不可重复的;
  • Map 使用键值对(k-v)存储元素,key是无序,不可重复的,value是无序、可重复的,每个键映射到一个值上。

4. List接口

1 . ArrayList扩容机制


ArrayList的底层采用数组来存储数据,在往ArrayList里面添加元素时,才会涉及到扩容机制。由于采用的是数组存储,所以会给数组设置默认长度10,当数组中没有元素时,是没有设置为默认长度10的,数组是一个空数组。当要添加元素时,会进入ensureCapacityInternal()进行判断,如果数据数组为空数组,在添加第一个元素时才将数组长度扩容为默认值10,如果数组不为空数组,就在ensureCapacityInternal()里面调用ensureExplicitCapacity()来判断是否需要扩容。如果需要扩容,才调用grow()方法进行扩容。在grow()方法里面,会通过右移位运算将数组长度的新长度扩容为原来的1.5倍。扩容后如果新容量不能满足所需容量,就将新容量扩大为所需容量,如果新容量大于最大容量,就调用hugeCapacity()将新容量扩大为最大整数容量。最后将数组中的元素拷贝到扩容后的这个数组,并将原数组的引用设置为拷贝后的数组。

2. ArrayList、Vector区别

  • ArrayListList 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ;

  • VectorList 的古老实现类,底层使用Object[ ] 存储,线程安全的。

  • 要想使得ArrayList变得线程安全 ,有两种方法:

    • 使用synchronized 关键字
    • 使用集合类Collections里面的synchronizedList方法
List<String> data = Collections.synchronizedList(new ArrayList<String>());

3. ArrayList、LinkedList区别

  • 是否保证线程安全: 两者都不是同步的,都不保证线程安全;
  • 底层数据结构: ArrayList底层采用Object数组、LinkedList底层使用的是双向链表;
  • 查找元素: ArrayList支持快速访问(get(int index)方法),LinkedList不支持快速访问,因此ArrayList查找元素更快;
  • 插入和删除元素: ArrayList底层采用数组存储,插入和删除元素受位置影响,LinkedList采用链表存储,插入和删除更加简单;
  • 内存空间: ArrayList 的空间浪费主要体现在数组列表的结尾会预留一定的空间,而LinkedList的空间浪费主要是每个元素都要存放直接前驱和直接后继。

5. Set接口

1. HashSet、LinkedHashSet、TreeSet的异同

  • HashSetSet 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;

  • LinkedHashSetHashSet 的子类,能够按照添加的顺序遍历;

  • TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。

6. Map接口

1. HashMap和HashTable的区别

  • 线程安全
    • HashMap是非线程安全的(要保证线程安全就使用ConcurrentHashMap)。
    • HashTalbe由于内部都使用了synchronized方法修饰,所以它是线程安全的。
  • 底层数据结构
    • HashMap:JDK1.8 之前 数组 + 链表(解决Hash冲突) 、JDK1.8 以后HashMap对链表部分做了修改,当链表长度大于阈值时(默认为8),就判断当前数组的长度是否小于64,如果小于64就对数组进行扩容,否则,就将链表转换为红黑树,以减少搜素时间。
    • HashTable: 数组 + 链表结构, 没有转为红黑树的处理机制。
  • 初始容量和扩容
    • 创建时不指定初始容量
      • HashMap默认初始容量为16,扩容时,容量变为原来的2倍。
      • HashTable默认初始容量为11,扩容时,变为原来的2倍+1。
    • 创建时指定初始容量
      • HashMap将指定的初始容量扩充为2的幂次方大小。
      • HashTable直接使用给定的容量大小。
  • Null Key 和 Null Value 的支持
    • HashMap可以存储Null Key 和 Null Value ,但是Null Key 只能有一个。
    • HashTable不允许Null Key 和 Null Value。
  • 效率:HashMap比HashTalbe的效率更高。

2. HashMap和HashSet的区别

HashMap HashSet
实现了 Map 接口 实现 Set 接口
存储键值对 仅存储对象
调用 put()向 map 中添加元素 调用 add()方法向 Set 中添加元素
HashMap 使用键(Key)计算 hashcode HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性

3. HashMap和TreeMap的区别

TreeMapHashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口。

实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。

实现SortMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序。

综上,相比于HashMap来说 TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。

4. HashMap的底层实现

4.1 JDK 1.8之前

  • 底层采用数组+链表,HashMaph通过它的hash()方法获取key对应的hashCode值,然后通过(n-1) & hash判断当前元素存放的位置,如果当前位置存在元素的话,就判断该元素与要存入的hash值以及key是否相同,如果同就直接覆盖,不相同的话,就通过拉链法解决hash冲突,将元素存放大链表中。

4.2 JDK1.8之后

  • JDK1.8之后底层采用的是数组+(链表和红黑树),主要不同在于解决hash冲突时,当链表长度超过设定的阈值长度时(默认为8),会先判断数组的长度是否小于64(初始最大容量),如果小于的话,并不是直接转为红黑树,而是进行扩容,只有当数组长度大于64时,才会将对应的链表转换为红黑树。

4.3 HashMap里面put一对Key-Value经历的过程:

5. HashMap的长度为什么是2的幂次方?

  • 知道HashMap底层的实现原理后,这个问题的回答就比较简单了:当我们往HashMap里面put元素的时候,会通过hashMap的hash方法,获取key对应的hashCode值,然后拿这个值去判断该元素要存放在那个地方,这里采用的是hash & (n-1) n为HashMap的长度,这个操作只有当n是2的幂次方时,才和hash % n 表示的是一个意思,这样才能找到这个key在数组中对应的位置。
  • 如果不是2的幂次方, hash & (n-1) 是不等于 hash % n的,采用位运算的好处是: 相较于%操作,它的效率更高。

6. HashMap有那几种常见的遍历方式

HashMap 遍历从大的方向来说,可分为以下 4 类

  • 迭代器(Iterator)方式遍历;

  • For Each 方式遍历;

  • Lambda 表达式遍历(JDK 1.8+);

  • Streams API 遍历(JDK 1.8+)。

但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:

  1. 使用迭代器(Iterator)EntrySet 的方式进行遍历;
  2. 使用迭代器(Iterator)KeySet 的方式进行遍历;
  3. 使用 For Each EntrySet 的方式进行遍历;
  4. 使用 For Each KeySet 的方式进行遍历;
  5. 使用 Lambda 表达式的方式进行遍历;
  6. 使用 Streams API 单线程的方式进行遍历;
  7. 使用 Streams API 多线程的方式进行遍历。

7. ConcurrentHashMap和HashTable的区别

  • 底层数据结构

    • JDK1.7 的ConcurrentHashMap底层使用的是segement数组+ HashEntry数组+链表实现的,JDK1.8则采用的是Node数组+(链表和红黑树)实现的
    • HashTable的底层则是数组+链表,数组是主体,链表则是为了解决hash冲突的
  • 线程安全的实现

    • JDK1.7 以前ConcurrentHashMap使用的是分段锁来保证线程安全,会对整个桶数组进行分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里的不同数据时,就不会存在锁竞争,提高了并发效率。 JDK1.8以后,摒弃了Segment的概念,而是直接使用数组+(链表和红黑树)的数据结构来实现,并发控制则采用synchronized 和 CAS(Compare And Swap) 来操作。
    • HashTable则使用synchronized来保证线程安全,但是它锁的是整个数组,效率非常低下,当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。


8. ConcurrentHashMap的线程安全是怎么实现的?

JDK1.7

​ 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成

Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}Copy to clipboardErrorCopied

​ 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。

1.4.10.2. JDK1.8

ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。

附: 另外附上简书上程序员追风博主总结的HashMap面试必问的6个点,娓娓道来,值得学习:

一、HashMap的实现原理?

此题可以组成如下连环炮来问

你看过HashMap源码嘛,知道原理嘛?

为什么用数组+链表?

hash冲突你还知道哪些解决办法?

我用LinkedList代替数组结构可以么?

既然是可以的,为什么HashMap不用LinkedList,而选用数组?

1.你看过HashMap源码嘛,知道原理嘛?

针对这个问题,嗯,当然是必须看过HashMap源码。至于原理,下面那张图很清楚了:

HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体。只是在JDK1.8中,链表长度大于8的时候,链表会转成红黑树!

2.为什么用数组+链表?

​ 数组是用来确定桶的位置,利用元素的key的hash值对数组长度取模得到链表是用来解决hash冲突问题,当出现hash值一样的情形,就在数组上的对应位置形成一条链表。ps:这里的hash值并不是指hashcode,而是将hashcode高低十六位异或过的。至于为什么要这么做,继续往下看。

3.hash冲突你还知道哪些解决办法?

​ 比较出名的有四种:

  • 开放定址法 :所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入 。
  • 链地址法:拉链法,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索 引上的多个节点可以用这个单向链表连接起来。
  • 再哈希法:再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
  • 公共溢出区域法:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

4.我用LinkedList代替数组结构可以么?

​ 这里我稍微说明一下,此题的意思是,源码中是这样的

​ Entry[] table = new Entry[capacity];

​ ps:Entry就是一个链表节点。

​ 那我用下面这样表示

​ List table = new LinkedList();

​ 是否可行?

答案很明显,必须是可以的。

5.既然是可以的,为什么HashMap不用LinkedList,而选用数组?

​ 因为用数组效率最高!在HashMap中,定位桶的位置是利用元素的key的哈希值对数组长度取模得到。此时,我们已得到桶的位置。显然数组的查找效率比LinkedList大。

那ArrayList,底层也是数组,查找也快啊,为啥不用ArrayList?

​ 因为采用基本数组结构,扩容机制可以自己定义,HashMap中数组扩容刚好是2的次幂,在做取模运算的效率高。

​ 而ArrayList的扩容机制是1.5倍扩容,那ArrayList为什么是1.5倍扩容这就不在本文说明了。

二、HashMap在什么条件下扩容?

此题可以组成如下连环炮来问

HashMap在什么条件下扩容?

为什么扩容是2的n次幂?

为什么为什么要先高16位异或低16位再取模运算?

1.HashMap在什么条件下扩容?

​ 如果bucket满了(超过load factor*current capacity),就要resize。load factor为0.75,为了最大程度避免哈希冲突current capacity为当前数组大小。

2.为什么扩容是2的次幂?
HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;这个算法实际就是取模,hash%length。但是,大家都知道这种运算不如位移运算快。因此,源码中做了优化hash&(length-1)。也就是说hash%length==hash&(length-1)

那为什么是2的n次方呢?

因为2的n次方实际就是1后面n个0,2的n次方-1,实际就是n个1。

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

而长度为5的时候,3&(5-1)=0 2&(5-1)=0,都在0上,出现碰撞了。

所以,保证容积是2的n次方,是为了保证在做(length-1)的时候,每一位都能&1 ,也就是和1111……1111111进行与运算。

3.为什么为什么要先高16位异或低16位再取模运算?

我先晒一下,jdk1.8里的hash方法。1.7的比较复杂,咱就不看了。


hashmap这么做,只是为了降低hash冲突的几率。打个比方,当我们的length为16的时候,哈希码(字符串“abcabcabcabcabc”的key对应的哈希码)对(16-1)与操作,对于多个key生成的hashCode,只要哈希码的后4位为0,不论不论高位怎么变化,最终的结果均为0。

如下图所示

而加上高16位异或低16位的“扰动函数”后,结果如下

可以看到: 扰动函数优化前:1954974080 % 16 = 1954974080 & (16 - 1) = 0 扰动函数优化后:1955003654 % 16 = 1955003654 & (16 - 1) = 6 很显然,减少了碰撞的几率。

三、讲讲hashmap的get/put的过程?

此题可以组成如下连环炮来问

知道hashmap中put元素的过程是什么样么?

知道hashmap中get元素的过程是什么样么?

你还知道哪些hash算法?

说说String中hashcode的实现?(此题很多大厂问过)

1.知道hashmap中put元素的过程是什么样么?

  • 对key的hashCode()做hash运算,计算index;
  • 如果没碰撞直接放到bucket里;
  • 如果碰撞了,以链表的形式存在bucket后;
  • 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树(JDK1.8中的改动);
  • 如果节点已经存在就替换old value(保证key的唯一性)
  • 如果bucket满了(超过load factor*current capacity),就要resize。

*2.知道hashmap中get元素的过程是什么样么?*

对key的hashCode()做hash运算,计算index;

如果在bucket里的第一个节点里直接命中,则直接返回;

如果有冲突,则通过key.equals(k)去查找对应的Entry;

若为树,则在树中通过key.equals(k)查找,O(logn);

若为链表,则在链表中通过key.equals(k)查找,O(n)。

3.你还知道哪些hash算法?

先说一下hash算法干嘛的,Hash函数是指把一个大范围映射到一个小范围。把大范围映射到一个小范围的目的往往是为了节省空间,使得数据容易保存。比较出名的有MurmurHash、MD4、MD5等等

4.说说String中hashcode的实现?(此题频率很高)

public int hashCode() {
  int h = hash;
  if (h == 0 && value.length > 0) {
     char val[] = value;
     for (int i = 0; i < value.length; i++) {
       h = 31 * h + val[i];
     }
     hash = h;
  }
  return h;
}

​ String类中的hashCode计算方法还是比较简单的,就是以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模。

哈希计算公式可以计为s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]

那为什么以31为质数呢?

主要是因为31是一个奇质数,所以31i=32i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算快很多。

四、为什么hashmap的在链表元素数量超过8时改为红黑树?

此题可以组成如下连环炮来问

知道jdk1.8中hashmap改了啥么?

为什么在解决hash冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

我不用红黑树,用二叉查找树可以么?

那为什么阀值是8呢?

当链表转为红黑树后,什么时候退化为链表?

1.知道jdk1.8中hashmap改了啥么?

​ 由数组+链表的结构改为数组+链表+红黑树。优化了高位运算的hash算法:h^(h>>>16)

​ 扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。

​ 最后一条是重点,因为最后一条的变动,hashmap在1.8中,不会在出现死循环问题。

2.为什么在解决hash冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

​ 因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于8个当时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于8个的时候,此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

3.我不用红黑树,用二叉查找树可以么?

​ 可以。但是二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。

4.那为什么阀值是8呢?

​ jdk作者选择8,一定经过了严格的运算,觉得在长度为8的时候,与其保证链表结构的查找开销,不如转换为红黑树,改为维持其平衡开销。

5.当链表转为红黑树后,什么时候退化为链表?

​ 为6的时候退转为链表。中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

五、HashMap的并发问题?

此题可以组成如下连环炮来问

HashMap在并发编程环境下有什么问题啊?

在jdk1.8中还有这些问题么?

你一般怎么解决这些问题的?

1. HashMap在并发编程环境下有什么问题啊?

(1)多线程扩容,引起的死循环问题

(2)多线程put的时候可能导致元素丢失

(3)put非null元素后get出来的却是null

2. 在jdk1.8中还有这些问题么?

​ 在jdk1.8中,死循环问题已经解决。其他两个问题还是存在。

3. 你一般怎么解决这些问题的?

​ 比如ConcurrentHashmap,Hashtable等线程安全等集合类。

六、你一般用什么作为HashMap的key?

此题可以组成如下连环炮来问

健可以为Null值么?

你一般用什么作为HashMap的key?

我用可变类当HashMap的key有什么问题?

如果让你实现一个自定义的class作为HashMap的key该如何实现?

1.健可以为Null值么?

必须可以,key为null的时候,hash算法最后的值以0来计算,也就是放在数组的第一个位置。

2.你一般用什么作为HashMap的key?

一般用Integer、String这种不可变类当HashMap当key,而且String最为常用。

  • 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
  • 因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的,这些类已经很规范的覆写了hashCode()以及equals()方法。

3.我用可变类当HashMap的key有什么问题?

​ hashcode可能发生改变,导致put进去的值,无法get出,如下所示

HashMap<List<String>, Object> changeMap = new HashMap<>();

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

list.add("hello");

Object objectValue = new Object();

changeMap.put(list, objectValue);

System.out.println(changeMap.get(list));

list.add("hello world");//hashcode发生了改变

System.out.println(changeMap.get(list));

输出值如下:

java.lang.Object@74a14482
null

4.如果让你实现一个自定义的class作为HashMap的key该如何实现?

此题考察两个知识点

​ 重写hashcode和equals方法注意什么?

4.1 如何设计一个不变类?

针对问题一,记住下面四个原则即可

​ (1)两个对象相等,hashcode一定相等

​ (2)两个对象不等,hashcode不一定不等

​ (3)hashcode相等,两个对象不一定相等

(4)hashcode不等,两个对象一定不等

4.2 如何写一个不可变类?

  • 类添加final修饰符,保证类不被继承。

    如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。

  • 保证所有成员变量必须私有,并且加上final修饰

    通过这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第4点弥补这个不足。

  • 不提供改变成员变量的方法,包括setter

    避免通过其他接口改变成员变量的值,破坏不可变特性。

  • 通过构造器初始化所有成员,进行深拷贝(deep copy)

    ​ 如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。例如:

    public final class ImmutableDemo { 
    
      private final int[] myArray; 
    
      public ImmutableDemo(int[] array) { 
    
         this.myArray = array; // wrong 
      } 
    }
    

​ 这种方式不能保证不可变性,myArray和array指向同一块内存地址,用户可以在ImmutableDemo之外通过修改array对象的值来改变myArray内部的值。

为了保证内部的值不被修改,可以采用深度copy来创建一个新内存保存传入的值。正确做法:

public final class MyImmutableDemo { 

  private final int[] myArray; 

  public MyImmutableDemo(int[] array) { 

     this.myArray = array.clone();  
  }  
}
  • 在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝

    这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。

原文地址:https://www.cnblogs.com/sinlearn/p/14818259.html