——总结自《java编程思想》
数组
数组的特殊性
- 效率,数组是效率最高的存储和随机访问对象引用序列的方式。数组就是一个简单线性序列,使得元素访问非常快速。代价就是数组对象的大小被固定,并且在生命周期内不可改变。 虽然ArrayList具有弹性,但是这种弹性变化需要开销,通过创建一个新实例,然后把旧实例中所有的引用移到新实例中,从而实现更多空间的自动分配。因此ArrayList的效率比数组低得多。
- 类型,泛型之前,其他容器类在处理对象时,都将视作没有任何具体类型,即这些对象都将当作Object处理。数组之所以优于泛型之前的容器,就是可以创建一个持有具体类型的数组。 这就意味着可以通过编译期检查来防止差啊如错误类型和抽取不正当类型。
- 保存基本类型的能力,数组可以持有基本类型,而泛型之前的容器则不能。不过有了泛型,容器可以指定并检查他们所持有的对象类型,并且有了自动包装机制。
无论使用哪种类型的数组,数组标识其实只是一个引用,指向在堆中创建的一个真实对象,这个数组对象用以保存指向其它对象的引用。
数组不能与泛型很好的结合。因为泛型擦除会移除参数类型信息,而数据必须知道它们所持有的确切类型,以强制保证类型安全。
不能创建泛型数组这一说法并不准确,编译器确实不让实例化泛型数组,但允许创建对这种数组的引用:List[] ls(可通过编译器而不报任何错误)。
尽管不能创建实际的持有泛型的数组对象,但可以创建非泛型数组然后转型
Arrays.deepToString()与Arrays.toString():Arrays.deepToString()主要用于数组中还有数组的情况,而Arrays.toString()则相反,对于Arrays.toString()而言,当数组中有数组时,不会打印出数组中的内容,只会以地址的形式打印出来。
Arrays实用功能:
- 复制数组。arraycopy()
- 数组的比较。equals()用来比较整个数组
- 数组元素的比较。实现comparable接口
- 数组排序。sort()
- 在已排序的数组中查找。binarySearch()
容器
下图为java容器类库的简化图:
Abstract 类
每个java.util容器都有其自己的Abstract类,它们提供了该容器的部分实现,因此必须做的只是去实现那些产生想要的容器所必需的方法。
享元模式:可在普通解决方案需要过多对象,或者产生普通对象太占用空间时使用享元。享元模式使得对象的一部分可以被具体化,因此,与对象中的所有事物都包含在对象内部不同,可以在更加高效的外部表中查找对象的一部分或整体。
Collection的功能方法
其中不包括随机访问所选择元素的get方法,因为Collection包括Set,而Set是自己维护内部顺序的。
可选操作
执行不同的添加或移除方法在collection接口中都是可选操作。这意味着实现类并不需要为这些方法提供功能定义。
将方法定义为可选的是因为这个做可以防止在设计中出现接口爆炸的情况。
可选操作声明调用某些方法将不会执行有意义的行为,相反,它们会抛出异常(unsupportedOperationException:未获支持的操作)。如果一个操作是可选的,编译器仍旧会严格要求你只能调用该接口中的方法。
未获支持的操作
:最常见的未获支持的操作都来源于背后由固定尺寸的数据结构支持的容器。例如:
public class ListTest {
public static void main(String[] args) {
String[] array = {"1","2","3","4","5"};
List<String> list = Arrays.asList(array);
list.add("6");
}
}
// 执行结果:
//Exception in thread "main" java.lang.UnsupportedOperationException
// 修改为:
public class ListTest {
public static void main(String[] args) {
String[] array = {"1","2","3","4","5"};
List<String> list = Arrays.asList(array);
List arrList = new ArrayList(list);
arrList.add("6");
}
}
发生问题的原因如下:
调用Arrays.asList()生产的List的add、remove方法时报异常,这是由Arrays.asList() 返回的是Arrays的内部类ArrayList, 而不是java.util.ArrayList。Arrays的内部类ArrayList和java.util.ArrayList都是继承AbstractList,remove、add等方法AbstractList中是默认throw UnsupportedOperationException而且不作任何操作。java.util.ArrayList重新了这些方法而Arrays的内部类ArrayList没有重新,所以会抛出异常。
Set
存储顺序不同的set实现不仅具有不同的行为,而且它们对于可以在特定的set中放置元素的类型也有不同的要求:
HashSet如果没有其他限制,应该默认选择,因为它对速度进行了优化。
必须为散列存储和树形储存都创建equals方法,但是hashCode只有在类被置于HashSet和LinkedHashSet时才必须。但对于良好的编程风格来言,都应该覆盖equals方法,总是同时覆盖hashCode方法。
SortedSet中的元素可以保证处于排序状态,
Queue
除了并发应用,Queue在Java中仅有的两个实现是LinkedList和PriorityQueue,它们的差异在于排序行为而不是性能。
优先级队列:列表中的每个对象都包含一个字符串和一个主要的以及次要的优先级值,该列表的排序顺序也是通过实现Comparable而进行控制的。
双向队列
双向队列就像一个队列,但是可以在任意一段添加或移除元素。在LinkedList中包含支持双向队列的方法,但在Java标准类库中没有任何显式的用于双向队列的接口。因此,LinkedList无法去实现这样的接口,无法像前面转型到Queue那样向上转型为Deque。但是可以使用组合创建一个Deque类,并直接从LinkedList中暴露相关方法
Map
映射表的基本思想是维护键值关联。
Map的基本实现:HashMap、TreeMap、LinkedHashMap、WeakHashMap、ConcurrentHashMap、IdentityHashMap
get进行线性搜索时,执行速度会相当慢,这正是HashMap提高速度的地方。HashMap使用特殊值,称作散列码,来取代对键的缓慢搜索。散列码是相对唯一的,用以代表对象的int值,它是通过将该对象的某些信息进行转换而生成的。hashCode()是跟类Object中的方法,因此所有Java对象都能产生散列码。HashMap就是使用对象的hashCode进行快速查询的,此方法能够显著提高性能。
没有其他的限制,HashMap应该成为默认选择,因为它对速度进行了优化。其他实现强调了其他的特性,因此都不如HashMap快。
SortMap:使用SortedMap(TreeMap的唯一实现),可确保键处于排序状态。
LinkedHashMap:为了提高速度,LinkedHashMap散列化所有的元素,但在遍历键值对时,却又以元素的插入顺序返回键值对。此外,可以在构造器中设定LinkedHashMap,使之使用基于访问的最近最少使用(LRU)算法,于是没有被访问过的元素就会出现在队列的前面。对于需要定期清理元素以节省空间的程序来说,此功能是的程序很容易实现。
正确的equals必须满足5个条件:
- 自反省,对任意x,x.equals(x)一定返回true 。
- 对称性,对任意x和y,如果x.equals(y)为true,则y.equals(x)也为true 。
- 传递性,对任意x,y,z,如果有x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true 。
- 一致性,对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回结果应该保持一致 。
- 对任何不是null的x,x.equals(null)一定返回false。
性能
List
- 对于背后由数组支持的List和ArrayList,无论列表的大小如何,这些访问都很快速和一致;而对LinkedList,访问时间对于较大的列表将明显增加。很显然,如果需要执行大量的随机访问,链接链表不会是一种好的选择。
- i当使用迭代器在列表中间插入新的元素。对于ArrayList,当列表变大时,其开销将变得很高昂,但是对于LinkedList,相对来说比较低廉,并且不随列表尺寸而发生变化。这是因为ArrayList在插入时,必须创建空间并将它的所有引用向前移动,这会随ArrayList的尺寸增加而产生高昂的代价。LinkedList只需链接新的元素,而不必修改列表中剩余的元素,因此可以认为无论列表尺寸如何变化,其代价大致相同。
- 在LinkedList中的插入和移除代价相当低廉,并且不随列表尺寸发生变化,但是对于ArrayList,插入操作代价特别高昂,并且其代价将随列表尺寸的增加而增加。
- 对于随机访问的get()和set()操作,背后由数组支撑的List只比ArrayList稍快一点。
- 应该避免使用Vertor,它只存在于支持遗留代码的类库中(在此程序中它能正常工作的唯一原因,只是因为为了向前兼容,他被适配成了List)。
- 最佳的做法可能是将ArrayList做为默认首选,只要需要使用额外的功能,或者当程序的性能因为经常从表中间进行插入和删除而变差的时候,才会选择LinkedList。如果使用的是固定数量的元素,那么既可以选择使用背后有数组支撑的List,也可以选择真正的数组。
- CopyOnWriteArrayList是List的一个特殊实现,专门用于并发编程。
Set
- HashSet的性能基本上总是比TreeSet好,特别是在添加和查询元素时。TreeSet存在的唯一原因是它可以维持元素的排序状态;所以,只有当需要一个排好序的Set时,才应该使用TreeSet。因为其内部结构支持排序,并且因为迭代是更有可能执行的操作,所以,用TreeSet迭代通常比用HashSet更快。
- 对于插入操作,LinkedHashSet比HashSet的代价更高,这是由维护链表所带来额外开销造成的。
Map
- 除了IdentityHashMap,所有Map实现的插入操作都会随着Map的尺寸的变大而明显变慢。但是,查找的代价通常比插入小得多。
- Hashtable的性能大体上与HashMap相当,因为HashMap是用来替换Hashtable的,因此它们使用了相同的底层存储和查找机制。
- TreeMap通常比HashMap要慢,并且不必进行特殊排序。一旦填充了一个TreeMap,就可以调用keySet()方法来获取键的Set视图,然后调用toArray()来产生由这些键构成的数组。之后可以使用Arrays.binarySearch()在排序数组中查找对象。当然,这只有HashMap的行为不可接受的情况下才有意义。因为hashMap本身就被设计为可以快速查找键。
- LinkedHashMap在插入时比HashMap慢一点,因为它维护散列数据结构的同时还要维护链表(以保持插入顺序)。正是因为这给列表,使得其迭代速度更快。
IdentityHashMap则具有完全不同的性能,因为它使用==而不是equals来比较元素。
HashMap的性能因子
容量:表中的桶位数
初始容量:表在创建时所拥有的桶位数,HashMap和HashSet都具有允许指定初始化容量的构造器
尺寸:表中当前存储的项数
负载因子:尺寸/容量。空表的负载因子是0,而半满表的负载因子是0.5。以此类推。负载轻的表产生冲突的可能性小,因此对于插入和查找都是最理想的。HashMap和HashSet都具有允许指定负载因子的构造器,表示当负载情况达到该负载因子的水平时,容器将自动增加其容量,实现方式是是容量大致加倍,并重新将现有对象分布到新的桶位集中(成为再散列)。
HashMap使用默认的负载因子0.75,这个因子在时间和空间代价之间到达了平衡。更高的负载因子可以降低表所需的空间,但是会增加查找代价。因为查找是我们在大多数时间里所做的操作。
如果知道将要在HashMap中存储多少项,那么创建一个具有恰当大小的初始容量将可以避免自动再散列的开销。