《Java 核心技术面试》课程笔记(八)

对比 Vector、ArrayList、LinkedList 有何区别?

典型回答

  • 这三者都是实现集合框架中的 List,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供按照位置进行定位、添加或者删除的操作,都提供迭代器以遍历其内容等。但因为具体的设计区别,在行为、性能、线程安全等方面,表现又有很大不同。
  • Vector 是 Java 早期提供的线程安全的动态数组
    • 如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。
    • Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。
  • ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。
    • 与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%。
  • LinkedList 顾名思义是 Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的

考点分析

  • 一般来说,也可以补充一下不同容器类型适合的场景:
    • Vector 和 ArrayList 作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。
    • 除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。
    • 而 LinkedList 进行节点插入、删除却要高效得多,但是随机访问性能则要比动态数组慢。
    • 所以,在应用开发中,如果事先可以估计到,应用操作是偏向于插入、删除,还是随机访问较多,就可以针对性的进行选择。
    • 这也是面试最常见的一个考察角度,给定一个场景,选择适合的数据结构,所以对于这种典型选择一定要掌握清楚。
  • 考察 Java 集合框架,有很多方面需要掌握:
    • Java 集合框架的设计结构,至少要有一个整体印象。
    • Java 提供的主要容器(集合和 Map)类型,了解或掌握对应的数据结构、算法,思考具体技术选择。
    • 将问题扩展到性能、并发等领域。
    • 集合框架的演进与发展。

知识扩展

  • 理解集合框架的整体设计,以类图为准:
    在这里插入图片描述
  • Collection 接口是所有集合的根,然后扩展开提供了三大类集合,分别是:
    • List,也就是有序集合,它提供了方便的访问、插入、删除等操作。
    • Set,Set 是不允许重复元素的,这是和 List 最明显的区别,也就是不存在两个对象 equals 返回 true。我们在日常开发中有很多需要保证元素唯一性的场合。
    • Queue/Deque,则是 Java 提供的标准队列结构的实现,除了集合的基本功能,它还支持类似先入先出(FIFO,First-in-First-Out)或者后入先出(LIFO,Last-In-FirstOut)等特定行为。这里不包括 BlockingQueue,因为通常是并发编程场合,所以被放置在并发包里。
    • 每种集合的通用逻辑,都被抽象到相应的抽象类之中,比如 AbstractList 就集中了各种 List 操作的通用部分。这些集合不是完全孤立的,比如,LinkedList 本身,既是 List,也是 Deque
    • TreeSet 代码里实际默认是利用 TreeMap 实现的,Java 类库创建了一个 Dummy 对象“PRESENT”作为 value,然后所有插入的元素其实是以键的形式放入了 TreeMap 里面;同理,HashSet 其实也是以 HashMap 为基础实现的,原来他们只是 Map 类的马甲。
  • 我们需要对各种具体集合实现,至少了解基本特征和典型使用场景,以 Set 的几个实现为例:
    • TreeSet 支持自然顺序访问,但是添加、删除、包含等操作要相对低效(log(n) 时间)。
    • HashSet 则是利用哈希算法,理想情况下,如果哈希散列正常,可以提供常数时间的添加、删除、包含等操作,但是它不保证有序。
    • LinkedHashSet,内部构建了一个记录插入顺序的双向链表,因此提供了按照插入顺序遍历的能力,与此同时,也保证了常数时间的添加、删除、包含等操作,这些操作性能略低于 HashSet,因为需要维护链表的开销。
    • 在遍历元素时,HashSet 性能受自身容量影响,所以初始化时,除非有必要,不然不要将其背后的 HashMap 容量设置过大。而对于 LinkedHashSet,由于其内部链表提供的方便,遍历性能只和元素多少有关系。
  • 这些集合类,都不是线程安全的,不属于 java.util.concurrent 里面的线程安全容器。但是,并不代表这些集合完全不能支持并发编程的场景,在 Collections 工具类中,提供了一系列的 synchronized 方法:List list = Collections.synchronizedList(new ArrayList());
  • 这些方法创建的线程安全集合,都符合迭代时 fail-fast 行为,当发生意外的并发修改时,尽早抛出 ConcurrentModificationException 异常,以避免不可预计的行为。
  • 另外一个经常会被考察到的问题,就是理解 Java 提供的默认排序算法,具体是什么排序方式以及设计思路等。
    • 这个问题本身就是有点陷阱的意味,因为需要区分是 Arrays.sort() 还是 Collections.sort()(底层是调用 Arrays.sort());什么数据类型;多大的数据集(太小的数据集,复杂排序是没必要的,Java 会直接进行二分插入排序)等。
    • 对于原始数据类型,目前使用的是所谓双轴快速排序(Dual-Pivot QuickSort),是一种改进的快速排序算法。
    • 而对于对象数据类型,目前则是使用TimSort,思想上也是一种归并和二分插入排序(binarySort)结合的优化排序算法。TimSort 并不是 Java 的独创,简单说它的思路是查找数据集中已经排好序的分区,然后合并这些分区来达到排序的目的。
    • 另外,Java 8 引入了并行排序算法(直接使用 parallelSort 方法),这是为了充分利用现代多核处理器的计算能力,底层实现基于 fork-join 框架,当处理的数据集比较小的时候,差距不明显,甚至还表现差一点;但是,当数据集增长到数万或百万以上时,提高就非常大了,具体还是取决于处理器和系统环境。
  • 在 Java 8 之中,Java 平台支持了 Lambda 和 Stream,相应的 Java 集合框架也进行了大范围的增强,以支持类似为集合创建相应 stream 或者 parallelStream 的方法实现,我们可以非常方便的实现函数式代码。
  • 阅读 Java 源代码,这些 API 的设计和实现比较独特,它们并不是实现在抽象类里面,而是以默认方法的形式实现在 Collection 这样的接口里。
    • 这是 Java 8 在语言层面的新特性,允许接口实现默认方法。
    • 理论上来说,我们原来实现在类似 Collections 这种工具类中的方法,大多可以转换到相应的接口上。
  • 在 Java 9 中,Java 标准类库提供了一系列的静态工厂方法,比如,List.of()、Set.of(),大大简化了构建小的容器实例的代码量。
    • 根据业界实践经验,我们发现相当一部分集合实例都是容量非常有限的,而且在生命周期中并不会进行修改。
    • 但是,在原有的 Java 类库中,我们可能不得不写成:
      ArrayList<String> list = new ArrayList<>();
      list.add("Hello");
      list.add("World");
      
    • 而利用新的容器静态工厂方法,一句代码就够了,并且保证了不可变性。
      List<String> simpleList = List.of("Hello","world");
      
    • 更进一步,通过各种 of 静态工厂方法创建的实例,还应用了一些最佳实践,比如,它是不可变的,符合我们对线程安全的需求;它因为不需要考虑扩容,所以空间上更加紧凑等。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值