数组是一种效率最高的存储和随机访问对象引用的一个简单的线性序列,虽然访问快速,但为之付出的代价是数组的大小固定,并且在其生命周期中不可改变。数组与其他容器之间的区别在于:效率、类型和保存基本类型的能力。但随着自动包装机制的出现,容器已经可以与数组几乎一样方便,而数组仅存的优点就是效率。
应该 “优先选择容器而不是数组”,只有在已经证明性能称为问题(并且切换到数组对性能提高有所帮助)时,你才应该将程序重构为使用数组
数组标识符就是一个引用,用以保存指向其他对象的引用,数组的一个单元指向在堆中创建的一个真实对象。对像数组与基本类型数组的区别在于,对象保存的是引用,基本类型数组保存的是基本类型的值。
数组的 length
属性,表示数组的大小,而不是实际保存的元素个数。新生成的对象数组,会被默认的初始化为 null,基本类型数组则会初始化为对应基本类型的默认值,对于对象数组,可以检测其中的引用是否为 null,进而确定某个位置是否存在对象。当数组不被需要时,垃圾回收器会负责清理数组,否则将一直存在。
System.arraycopy()
:复制一个数组比用 for 循环复制要快得多,且该方法对所有类型做了重载。当复制对象数组时,只是复制对象的引用(属浅拷贝)。 Arrays.asList(T... a):
将多个元素转换成 List 列表,底层表示的还是数组,当尝试进行 add()
、 delete()
操作时将在运行时报错。 Arrays.sort(T[] a, Comparator<? super T> c)
:按照给定的比较器对指定的数组进行排序。 Arrays.binarySearch(T[] a,T key, Comparator<? super T> c)
:对一个有序的数组采用二分查找获取对象下标,如果数组存在重复元素,可能返回的不精确。<span color="red">注:如果使用了Comparator排序了一个对象数组,则调用此方法时传入的Comparator必须是同一个。</span>
comparator用于指定元素的排列顺序,在常见的数据类型中已经被默认实现,对于需要按照某种规则进行排序的时候,提供Comparator或者实现 java.lang.Comparable
接口。
class DemoComparator implements Comparator<Demo> { @Override public int compare(Demo o1, Demo o2) { if (o1.value == o2.value) { return 0; } return o1.value < o2.value ? 1 : -1; } }
一个独立元素的序列,这些元素都服从一条或多条规则,提供了存放一组对象的方式。List必须按照插入的顺序保存元素,而Set不能有重复元素。Queue按照队列排队规则来确定对象产生的顺序。
PriorityQueue:优先级队列,声明下一个弹出最需要的元素(具有最高的优先级),通过默认排序或提供 Comparator。当调用 peek()
、 poll()
、 remove()
方法时,获取的元素是队列中优先级最高的。
类 | 描述 |
---|---|
ArrayList | 常用于随机访问元素,但是在 List的中间插入和删除元素时较慢。 |
LinkedList | 对中间插入和删除元素的操作中提供了优化的顺序列表,在随机访问方面相对比较慢,但是它的特性集较 ArrayList更大。 |
ListIterator:Iterator的子类,只用于对各种 List 类的访问,可以实现双向移动。 hasNext() / next()
、 hasPrevious() / previous()
get/set
操作,背后有数组支持的 List 比 ArrayList 稍快一点,但是对于 LinkedList,将会变得很慢,因为它不是针对随机访问操作而设计的。
最佳的做法是将 ArrayList 作为默认首选,当因为经常插入和删除操作而性能下降时,才去选择 LinkedList。如果使用的是固定数量的元素,既可以选择List,也可以选择数组。
类 | 描述 |
---|---|
Set (interface) |
存入Set的每一个元素都必须要唯一,因为Set不允许重复元素。加入Set的元素必须定义 equals()
方法以确保对象的唯一性。Set 和 Collection 有完全一样的接口。 Set 接口不保证维护元素的次序。 |
HashSet |
为快速查找而设计的Set。存入 HashSet的元素必须定义 hashCode()
。 |
TreeSet | 保持次序的 Set,底层为数据结构。使用它可以从 Set中提取有序的序列。元素必须实现 Comparator接口。 |
LinkedHashSet |
具有 HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代遍历 Set时,结果会按元素插入的次序显示。元素必须定义 hashCode()
方法。 |
HashSet的性能基本上总是比 TreeSet好,特别在添加和查询元素时。 TreeSet存在的唯一原因是它维持元素的排序顺序,因此迭代的时候,TreeSet通常比用HashSet要快。
Map的各个实现类的行为特性的不同在于,效率、键值对的保存及呈现次序、对象的保存周期、映射表如何在多线程程序中工作和判定 ”键“等价的策略等方面。
类 | 描述 |
---|---|
HashMap | Map基于散列表的实现(取代HashTable)。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量和负载因子,以调整容器的性能。 |
LinkedHashMap | 类似于HashMap,但是迭代遍历它时,取得 “键值对” 的顺序就是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点;而在迭代访问时反而快,因为它使用链表维护内部次序。 |
TreeMap |
基于红黑树的实现。查看 “键” 或“键值对”时,它们会被排序。TreeMap的特点在于,所得到的的结果是经过排序的。TreeSet是唯一一个带有 subMap()
方法的Map,它可以返回一个子树。 |
WeekHashMap | 弱键映射,允许释放映射所指向的对象,这是为解决某类特殊问题而设计的。如果映射 之外没有引用指向某个“键”,则此“键”可以被垃圾收集器回收。 |
ConcurrentHashMap | 一种线程安全的Map,它不涉及同步加锁。 |
IdentityHashMap |
使用 ==
代替 equals()
对“键”进行比较的散列映射。专为解决特殊问题而设计的 |
插入操作随着Map尺寸的变大,速度会明显变慢(除了 IdentityHashMap),但是查找所耗费的代价比插入小很多。对于插入操作,由于维护链表所带来的的开销,导致LinkedHashSet 比 HashSet的代价高很多。
散列码是一个无符号的十六进制数,散列的目的在于使用一个对象来查找另一个对象;散列的价值在于能够快速进行查询。
在Map上的实现:通过键对象生成一个数字,这个数字就是散列码。将散列码作为数组的下标,该数组的一个单元指向一个链表,链表上的一个节点就是一个 Entry 对象。数组的容量是固定的,不同的键可以产生相同的下标,这将导致 “冲突”。
Example:参考Java编程思想 P493
static final int SIZE = 1024; LinkedList<Entry<K, V>>[] buckets = new LinkedList[SIZE]; public V put(K key, V value) { V oldValue = null; // 对散列值取余获取数组下标 int index = Math.abs(key.hashCode()) % SIZE; if (buckets[index] == null) { buckets[index] = new LinkedList<Entry<K,V>>(); } // 获取下标所指向的链表 LinkedList<Entry<K, V>> bucket = buckets[index]; Entry<K, V> pair = new Entry<>(); boolean found = false; // 获取迭代器进行迭代,判断是否存在,存在则覆盖 ListIterator<Entry<K, V>> it = bucket.listIterator(); while (it.hasNext()) { Entry<K, V> iPair = it.next(); if (iPair.getKey().equals(key)) { oldValue = iPair.getValue(); it.set(pair); found = true; break; } } // 没有找到就添加 if (!found) { buckets[index].add(pair); } return oldValue; }
x.equals(null)
hashcode()
都应该生成同样的值。 hashCode()
和 equals()
,必须能够完全确定对象的身份。 hashCode()
应该产生分布均匀的散列码。赋予一个非零常量值。 public int hashCode() { int result = 19; return result * field.hashCode(); }