目录
集合与数组
hashCode方法的作用?
层次关系
集合类遍历
List集合层次图
对比 Vector、ArrayList、LinkedList 有何区别?
读写效率:
扩容:
一般来说,也可以补充一下不同容器类型适合的场景:
Set 集合的几种实例
线程安全
在 Java 9 中,Java 标准类库提供了一系列的静态工厂方法
集合与数组
数组(可以存储基本数据类型)是用来存现对象的一种容器,但是数组的长度固定,不适合在对象数量未知的情况下使用。
集合(只能存储对象,对象类型可以不一样)的长度可变,可在多数情况下使用。
hashCode方法的作用?
Set集合元素无序,但元素不可重复。那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。于是,Java采用了哈希表的原理,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
层次关系
图中,实线边框的是实现类,折线边框的是抽象类,而点线边框的是接口
Collection接口是集合类的根接口。扩展开提供了三大类集合,分别是:
- List,有序集合,它提供了方便的访问、插入、删除等操作。
- Set,Set 是不允许重复元素的,这是和 List 最明显的区别,也就是不存在两个对象 equals 返回 true。我们在日常开发中有很多需要保证元素唯一性的场合。
- Queue/Deque,则是 Java 提供的标准队列结构的实现,除了集合的基本功能,它还支持类似先入先出(FIFO, First-in-First-Out)或者后入先出(LIFO,Last-In-First-Out)等特定行为。这里不包括 BlockingQueue,因为BlockingQueue通常是用在并发编程场合,所以被放置在并发包里。
Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分。Map包含了key-value键值对。Map不能包含重复的key,但是可以包含相同的value。
Iterator,所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含以下三种方法:
- hasNext()是否还有下一个元素。
- next()返回下一个元素。
- remove()删除当前元素。
集合类遍历
在类集中提供了以下四种的常见输出方式:
1、Iterator:迭代输出,是使用最多的输出方式。
Iterator it = arr.iterator();
while (it.hasNext()) {
Object o = it.next();
}
2、ListIterator:是Iterator的子接口,专门用于输出List中的内容。
Iterator it = arr.listIterator();
while (it.hasNext()) {
Object o = it.next();
}
3、foreach输出:JDK1.5之后提供的新功能,可以输出数组或集合。
for (Object i:arr) {
...
}
4、for循环
for (int i = 0; i <arr.size() ; i++) {
...
}
对比 Vector、ArrayList、LinkedList 有何区别?
这三者都是实现集合框架中的 List,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供按照位置进行定位、添加或者删除的操作,都提供迭代器以遍历其内容等。但因为具体的设计区别,在行为、性能、线程安全等方面,表现又有很大不同。
Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。
ArrayList 是非线程安全动态数组实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,vector增长率为目前数组长度的100%,而Arraylist增长率为目前数组长度的50%。
LinkedList 顾名思义是 Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的。它本身有自己特定的方法,如: addFirst(),addLast(),getFirst(),removeFirst(),removeLast()等.。
读写效率:
在删除可插入对象的动作时,为什么ArrayList的效率会比较低呢?
- Vector 和 ArrayList 作为动态数组,除了尾部,从数组的其它位置插入和删除元素,需要移动后段的数组元素,从而会重新调整索引顺序,调整索引顺序会消耗一定的时间,所以速度上就会比LinkedList要慢许多,比如我们在中间位置插入一个元素,需要移动后续所有元素。
- 相反,LinkedList是使用链表实现的,若要从链表中删除或插入某一个对象,只需要改变前后对象的引用即可。
扩容:
ArrayList 在执行插入元素是超过当前数组预定义的最大值时,数组需要扩容,扩容过程需要调用底层System.arraycopy()方法进行大量的数组复制操作;在删除元素时并不会减少数组的容量(如果需要缩小数组容量,可以调用trimToSize()方法);在查找元素时要遍历数组,对于非null的元素采取equals的方式寻找。
Vector 与ArrayList 仅在插入元素时容量扩充机制不一致。Vector 与ArrayList 默认创建一个大小为10的Object数组,如 Vector的容量为10,一次扩容后是容量为20,如 ArrayList的容量为10,一次扩容后是容量为16。
对于Vector,将capacityIncrement设置为0;当插入元素数组大小不够时,如果capacityIncrement大于0,则将Object数组的大小扩大为现有size+capacityIncrement;如果capacityIncrement<=0,则将Object数组的大小扩大为现有大小的2倍。
一般来说,也可以补充一下不同容器类型适合的场景:
- Vector 和 ArrayList 作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。
- LinkedList 经常用在增删操作较多而查询操作很少的情况下,ArrayList则相反。
- LinkedList 实现Stack(堆栈)使用removeFirst()方法,实现Queue(队列)使用removeLast()方法。前者先进后出,后者是先进先出。LinkedList还是实现了Queue接口,因此可以直接作为队列使用。
Set 集合的几种实例
TreeSet
- TreeSet 实际是利用 TreeMap 实现的,TreeSet 支持自然顺序访问,但是添加、删除、包含等操作要相对低效(log(n) 时间)。
- TreeSet支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。
- TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0
HashSet
- 其实是以 HashMap 为基础实现的。
- HashSet 不能保证元素的排列顺序,顺序有可能发生变化,不是同步的,集合元素可以是null,但只能放入一个null。
- 当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。
LinkedHashSet
- 是根据元素的hashCode值来决定元素的存储位置。
- 内部构建了一个记录插入顺序的双向链表,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
- LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。
在遍历元素时,HashSet 性能受自身容量影响,所以初始化时,除非有必要,不然不要将其背后的 HashMap 容量设置过大。而对于 LinkedHashSet,由于其内部链表提供的方便,遍历性能只和元素多少有关系。
线程安全
不是线程安全的集合,并不代表这些集合完全不能支持并发编程的场景,在 Collections 工具类中,提供了一系列的 synchronized 方法,比如:
static <T> List<T> synchronizedList(List<T> list)
我们完全可以利用类似方法来实现基本的线程安全集合:
List list = Collections.synchronizedList(new ArrayList());
它的实现,基本就是将每个基本方法,比如 get、set、add 之类,都通过 synchronizd 添加基本的同步支持,非常简单粗暴,但也非常实用。注意这些方法创建的线程安全集合,都符合迭代时 fail-fast 行为,当发生意外的并发修改时,尽早抛出 ConcurrentModificationException 异常,以避免不可预计的行为。
在 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 静态工厂方法创建的实例,还应用了一些我们所谓的最佳实践,比如,它是不可变的,符合我们对线程安全的需求;它因为不需要考虑扩容,所以空间上更加紧凑等。
如果我们去看 of 方法的源码,你还会发现一个特别有意思的地方:我们知道 Java 已经支持所谓的可变参数(varargs),但是官方类库还是提供了一系列特定参数长度的方法,看起来似乎非常不优雅,为什么呢?这其实是为了最优的性能,JVM 在处理变长参数的时候会有明显的额外开销,如果你需要实现性能敏感的 API,也可以进行参考。