如果一个程序只包含固定数量的并且生命周期都是已知的对象,那么这是一个非常简单的程序。
学习或精通一门语言,熟悉类库很大程度上也是必须的要求。
面向接口编程而不是面向实现编程
Introduction
在程序中包含固定个数的对象常常来讲是不太可能的,总会有大量的数据在一个地方等着我们把他们放到一个桶里一起带走。在JAVA中这个持有大量对象的桶就是 数组和容器。事实上,在深入数组和容器的时,也需要深入理解到更为通用的一个接口:Iterable。
在评估到底是用数组还是更加灵活的容器时,虽然数组在当前版本有效率(性能)优势,但是在实际编程时应当“优选容器而不是数组”,只有在证明性能成为问题时才应该将程序重构为数组。
数组
数组时保存一组对象最有效的方式,如果保存一组基本类型的数据数组也是合适的选择。但是数组有着固有的缺陷:固定的尺寸。
- 自动包装机制使得容器可以和数组一样方便的使用基本类型
- 数组唯一可以访问的字段或方法时length,length指标是数组能够容纳的元素,不知道数组中确切的有多少元素
- 对象数组保存的是引用,基本类型数组直接保存基本类型的值。引用数组的所有引用初始化为null,数值型基本类型数组初始化为0.
- 多维数组中,构成矩阵的每个向量都可以具有任意的长度(粗糙数组)
容器
collection、map两类容器;
(这里需要继承图,但是图床尚未就绪,稍后再添上,继承层次图及其重要!!!)
大体上容器分为两大类:collection、map。更细一点来说,事实上只有四种容器:List、Set、Queue、Map。
Collection接口继承了Iterable,这是一个重要的特性,意味着实现Collection可以被foreach遍历。Map没有继承Iterable,并且Map. Entry也没有继承Iterable,所以两者都不可以被foreach遍历,但是entrySet方法可以返回set对象,set实现了Iterable接口进而可以使用Iterable。
元素顺序
- HashSet、HashMap 按存储顺序无实际意义,无序
- TreeSet、TreeMap 按升序保存对象
- LinkedHashSet、LinkedHashMap 按添加顺序保存对象
容器接口的认识
我们编写的大部分代码都是在与接口打交道,尽管并非总是这样,但大部分情况下是如此的。
-
使用如下这句话,ArrayList被向上转型为List,使用接口的目的在于决定修改实现的时候,只需要在创建处修改。如下所示将ArrayList的实现转换为LinkedList的实现。
List<Apple> apples = new ArrayList<Apple>(); List<Apple> apples = new LinkedList<Apple>();
-
如果需要创建一个具体类的对象,将其转型成为接口,在其他地方使用接口。这样做不会损失什么,但可能是一个良好的编程习惯的养成。
-
如果需要使用某些类的额外功能,那么就不能向上转型为通用的接口。
数组注意事项
数组与泛型
不能实例化具有参数化类型的数组,例如如下示例,擦除会移除参数类型信息,而数组必须知道他们持有的确切类型,以强制保证类型安全
Peel<Bannana>[] peels = new Peel<Bannana>[10];
但是可以参数化数组本身的类型,如下示例
class ClassParameter<T>{
public T[] f(T[] arg){return arg;}
}
class MethodParameter{
public static <T> T[] f(T[] arg){return arg;}
}
public class ParameterizedArrayType{
public static void main(String[] args){
Integer[] ints = {1,2,3,4,5};
Integer[] ints2 =
new ClassParameter<Integet>().f(ints);
ints2 =
MethodParameter.f(ints);
}
}
不能创建实际的持有泛型的数组对象,但是可以创建非泛型的数组,然后将其转型(如下代码示例),注意代码中的List<String>[]是一个object[]
List<String>[] ls;
List[] la = new List[19];
ls =(List<String>[])la; //"unchecked" warning
泛型在类或方法的边界处很有效,而在类或方法的内部,擦除通常会使泛型变得不适用,例如我们不能创建泛型数组。但是可以创建Object数组,然后将其转型。
T[] arrag = new T[10];
数组比较、排序、查找
-
equals用于比较两个数组是否相等,deepEquals用于多维数组
-
使用标准类库System.arraycopy方法复制数组比用for循环复制快得多
-
复制对象数组知识复制了对象的引用,而不是对象的拷贝,是浅拷贝
-
数组相等条件:元素个数相等、对应位置元素相等,这需要调用每一个元素的equals,因此需要注意equals方法的编写
-
使用内置的排序方法可以对任意基本类型、实现了Comparable接口的对象、具有关联的Comparator的对象进行排序
- 如果数组已经排好序了,可以使用Arrays.binarySearch()执行快速查找,对未排序的数组调用结果未知。
容器注意事项
泛型 & 类型安全性
-
当对下面的apples使用get()时,取出的不是我们认为的Apple,而是Object引用,必须转型为Apple
ArrayList apples = new ArrayList(); apples.add(new Apple()); apples.get(); // Object (Apple)apples.get(); // Apple
-
使用泛型将运行期错误可以提前到编译器错误
-
向上转型适用于泛型容器
初始化 & 填充(添加元素)
java.util.Arrays和java.util.Collection、java.util.Collections在类库中提供了方便的添加元素的方法。另外基于设计模式的基本思想可以创建Generator解决方案。
java.util.*
添加一组元素用于初始化
-
Arrays.asList()方法接收一个数组或是一个逗号分隔的元素列表(可变参数列表),并转换成为一个List对象
-
Collections.addAll()方法接收一个Collection对象,以及一个数组或都好分割的列表(可变参数列表)
-
Collection.addAll()方法接受一个Collection对象
-
三个方法的进一步分析
- collection的构造器可以接收另一个Collection来初始化,但是Collection.addAll()运行起来快得多,构造一个空构造器然后调用Collection.addAll()方法很方便,是首选的方式
-
Collection.addAll()不如另外两者更加灵活,因为他们接收可变参数列表
-
其他填充方式例如:Collections.nCopies()、Collection.fill();但是注意两者创建的引用指向的是相同的对象,fill()方法更为有限,只能替换已经在List中存在的元素,不能添加新的元素
Arrays.asList()
-
如果直接使用Arrays.asList()输出的List,那么由于这个List底层是数组,所以不能改变这个list的尺寸,不能add或者delete
-
传递基本类型数组的注意事项!需要注意的是,Arrays.asList()接收的是一个参数列表,但并不是意味着可以把一个数组对象当作是它的参数。如下示例(虽然在诸多文章中雷同的出现,本处加以引用),示例中传入参数列表的是一个int数组,因此myList持有的是一个数组的列表,每一个元素都是一个数组。故而size是1。
int[] myArray = { 1, 2, 3 }; List myList = Arrays.asList(myArray); System.out.println(myList.size()); //...... //output size is 1
需要注意的是这是基本数据类型数组,基本数据类型不是对象,如果使用Integer则可以size为3,即如下所示,因为Integer数组可以转化成为一组Integer的对象(这么说好像也并不是很通顺,暂时先这样吧)(关于可变参数可以自行Google)
Integer[] myArray = { 1, 2, 3 }; List myList = Arrays.asList(myArray); System.out.println(myList.size()); //...... //output size is 3
(这种问题,在查看并简单的分析asList源码即可解决,这也是一个可以不用Google就可以快速解决问题的例子,值得在自己能力低下的时候警惕。)
Arrays.asList()源码如下,可以注意到泛型T,传入的每一个参数都是T类型,传入一个数组并且数组内不能转化成为对象,则T自然是一个数组类型。
@SafeVarargs @SuppressWarnings("varargs") public static <T> List<T> asList(T... a) { return new ArrayList<>(a); }
Generator
从generator创建数组
todo: 懒
...创建List
todo: 懒
...创建Map
todo: 懒
AbstractCollection
AbstractCollection提供类collection的默认实现,使得我们可以创建它的子类型。实现collection意味着需要提供iterator方法、size方法。
todo: 懒
List
有三种基本的List:ArrayList、LinkedList、Vector: ArrayList 擅长随机访问元素,插入移除元素慢,代价高昂;LinkedList在随机访问上慢,但插入删除代价较低,顺序访问有一定优化;Vector是提供了线程安全的List
如何选择ArrayList和LinkedList?
如Think in Java书中所讲,"最好的策略是置之不理,直到需要担心这个问题。如果开始在某个ArrayList中间执行过多插入操作,程序开始变慢,那么List的实现采用ArrayList很可能是罪魁祸首"。在编写之初过多的思考反而不利于开发,不知道在哪里看到这种思想,但是实际编程和很多地方看到这样的说法也确实说明有道理,优化问题提前过多的考量是不是也算在了超前设计里面。
equals对容器方法的影响
当确定元素是否属于某个List,发现某个元素的索引,以及从List中移除一个元素时、retainAll、removeAll(其余很多地方也会),都会采用equals方法,必须意识到List的行为根据持有的对象的equals行为而有所变化!(在许多次实践吃尽了苦头)对于其他用到equals情况也应当注意。
持有引用和源对象的问题
subList()方法产生的列表的幕后就是初始列表,对subList返回的列表的修改都会反映到初始列表中,对初始列表的修改亦然。
-
如果将subList方法产生的片段用来传递给较大列表的containsAll()方法,会得到true,即使这个片段上调用了shuffle也会返回true,因为这与顺序无关,
-
这些问题同样可以从源码反应出来,下面以ArrayList的subList源码反应,可以看到内部类SubList持有一个较大列表的引用,并保存一个offset值,很明显这样就能回答上面的问题
... public List<E> subList(int fromIndex, int toIndex) { subListRangeCheck(fromIndex, toIndex, size); return new SubList(this, 0, fromIndex, toIndex); } ... private class SubList extends AbstractList<E> implements RandomAccess { private final AbstractList<E> parent; private final int parentOffset; private final int offset; int size; SubList(AbstractList<E> parent, int offset, int fromIndex, int toIndex) { this.parent = parent; this.parentOffset = fromIndex; this.offset = offset + fromIndex; this.size = toIndex - fromIndex; this.modCount = ArrayList.this.modCount; } ... } ...
Map
todo: 懒
Iterator & Iterable
- 从高层角度看,要使用容器,必须针对容器的确切类型编程,但是迭代器可以改变这个情形
- 试想原本对着List编码,但是后来发现把相同的代码应用于Set会显得非常方便,如果从头开始,啊,那你真是一个伟大的码农,一个站在天台码农
- 接收对象容器并传递它,从而在每个对象上都执行操作(???什么意思,尚未搞懂这一句话 todo: 懒)
- 迭代器是一个轻量级对象:创建的代价很小
- JAVA的Iterator只能单向移动
- 常用方法:iterator、next、hasNext、remove
- 还可以移除由next产生的最后一个元素,即先调用next在调用remove会删除之前的一个元素
- iterator的真正威力:能够将遍历序列的操作与序列底层的结构分离,由此而言,迭代器统一了对容器的访问方式(例子 todo: 懒)
- ListIterator可以双向移动
- listIterator(n)从索引为n的元素的listIterator,listIterator()为从开始处
- todo: 懒 如果使用栈的行为,使用继承就不合适了,这样会产生具有LinkedList的其他所有方法的类
- todo: 懒 foreach 与 迭代器
- Iterable接口:任何实现了该接口的类都可以用于foreach,collection也可以应用于foreach,数组也可以应用于foreach,但是数组不是iterable
- 迭代器也可以重载为向前的方向或向后的方向(例子 todo: 懒)
END
在适应容器类库上,确实需要花一些时间,但是总是会找到自己的路。学习或精通一门语言,熟悉类库很大程度上也是必须的要求。
所以同志们我们总结到了什么?
- 及时查看源码可以解决很多问题
- 其他的去文中再复习一遍吧
参考
- Think in java 第四版 第十一、十六、十七章