1 数组
1.1 性能
- printList打印表:线性时间 O(N);
- find(x)返回指定位置上的元素:常数时间 O(1);
- 插入和删除:最坏情况O(N), 最好情况O(1), 平均为线性时间;
所以数组一般通过在高端进行插入操作建成,之后只访问,不进行插入和删除,尤其是在表的前端。
1.2 栈内存与堆内存
新建的数组中保存的实际对象是保存在堆(heap)中的;如果引用该数组的数组引用变量是一个局部变量,那么它被保存在栈(stack)中。例如:
int[] test = new int[100];
test这个引用变量是保存在栈中的,那新建的100int的数据是保存在堆中的。虽然它还没有初始化,但是内存已经分配给它了。
当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的局部变量都会放入这块内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁。因此,所有在方法中定义的局部变量都是放在栈中的。
但是,程序中被new出来的东西一般都放在堆内存中以便程序反复利用,这个堆内存也叫作【运行时数据区】,这个称号更能表述它的性质不是吗。
堆中的对象是不会随方法的结束而立刻销毁,只要还有引用变量指向它,GC就不会回收。这一点在方法的参数传递中很常见。只有当堆中的对象没有一个引用变量指向它,GC才会在合适的时候回收它。所以在JDK源码中,我们经常看到一些没用了的引用变量被显式地赋值为null,就是为了让GC尽快回收被创建的对象。
Java 数组初始化的两种方法:
- 静态初始化: 程序员在初始化数组时为数组每个元素赋值;
- 动态初始化: 数组初始化时,程序员只指定数组的长度,由系统为每个元素赋初值。
public class ArrayInit {
public static void main(String[] args) {
//静态初始化数组:方法一
String cats[] = new String[] {
"Tom","Sam","Mimi"
};
//静态初始化数组:方法二
String dogs[] = {"Jimmy","Gougou","Doggy"};
//动态初始化数据
String books[] = new String[2];
books[0] = "Thinking in Java";
books[1] = "Effective Java";
System.out.println(cats.length);
System.out.println(dogs.length);
System.out.println(books.length);
}
}
典型错误是:
int[] nums = new int[6]{1,2,3,4,5,6};
1.3 多维数组
常规的说法是“Java支持多维数组”,但是,从本质上来讲,根本没用什么多维数组,不过是引用变量数组而已。当然,说法上不用太纠结,你开心就好^_^
这里就强调一下多维数组的新建,两种:
//先新建第一纬度,再新建第二纬度
int[][] test = new int[4][];
for(int i=0; i<test.length;i++){
test[i] = new int[100];
}
//两个维度同时新建
int[][] test2 = new int[4][100];
System.out.println(test2.length);
System.out.println(test2[0].length);
1.4 Arrays
Arrays 在Java 8 的 java.util 包下,包含一些static的可以直接操纵数组的方法:binarySearch、copyOf、copyOfRange、equals、fill、sort、toString。使数组与String越来越像了。
2 简单链表
- printList和find(x):线性时间
- remove:遍历到该节点,修改一个next
- insert:遍历,新建节点,调整两个引用
3 java.util.Collection接口:集合
Collection接口扩展了Iterable接口。
4 java.util.Iterable接口
实现了Iterable接口的类可以拥有增强的for循环:
for(AnyType item: coll)
5 什么是“可选操作”
简单说就是抽象类的的某些派生类实现里,或者接口的某个实现类里面,某个方法可能是无意义的,调用该方法会抛出一个异常。例如在collection的某些实现类里,里面的元素可能都是只读的,那么add这个接口是无意义的,调用会抛出UnspportedOperationException异常。
从设计的角度说,如果一个接口的方法设计为optional,表示这个方法不是为所有的实现而设定的,而只是为某一类的实现而设定的。
6 List
对List的实现有两种:
- ArrayList:可增长的数组。因为是数组嘛,get,set效率高,remove和insert代价高;
- LinkedList:双链表。insert和remove的代价相对于数组更高一些,但是平均时间开销也是线性的。更加不建议使用get和set。一般使用LinkedList进行最多的应该是两端的操作,所以它提供了addFirst,removeFirst,addLast,removeLast,以及getFirst,getLast等操作。
为了更好的理解这两个List,接下来会对它们的源码简要分析,然后着重看看add/remove,get/set这四个最常用的方法的源码。
7 ArrayList源码分析
首先,来看看它的成员变量与基本的构造函数,将它就是个数组的屌丝特性暴露无遗:
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}
7.1 add
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
上述代码不难看出,add(E e) 是将新元素追加到数组末尾的。
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
这里的void java.lang.System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)即将数组从index到末尾的元素统一往后移了一位,然后将elementData[index]赋值为指定的数值,也就是典型的数组插入嘛。
7.2 remove
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
检查index是否越界,保存要删除元素的值,数组shift,elementData[–size] = null,返回删除的值。
7.3 get
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
数组取值O(1)爽啊!
7.4 set
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
数组赋值效率高啊!
8 LinkedList源码分析
首先,我把可以反映LinkedList本质的一些源码贴在一起:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
看到上面这些代码,我们应该立刻明白,几个特殊情况:
- 如果list为空则first==null,last==null;
- 如果list只有一个元素,则first==last==那个Node的指针;
- 如果一个Node为表头,则node.prev==null;
- 如果一个Node为表尾,则node.next==null;
再来看看内部有特点的方法,反映了处理双向链表的编程艺术,值得好好看看,而且只有把这些方法弄明白了,就可以把LinkedList理解透彻了。
linkFirst与linkLast需要注意的特殊情况是list是否为空:
/**
* Links e as first element.
*/
private void linkFirst(E e) {
final Node<E> f = first;
//newNode prev为null,值为e,next为f
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
//如果原list为空的话,把last也赋值为newNode;不为空则将原first的prev指向新first
if (f == null)
last = newNode;
else
f.prev = newNode;
//时刻注意维护size
size++;
modCount++;
}
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
//newNode prev为原last,值为e,next为null
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
//如果为空
if (l == null)
first = newNode;
//如果不为空
else
l.next = newNode;
size++;
modCount++;
}
linkBefore需要注意的特殊情况是插入的位置是否在表头:
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
//一步就在succ与succ.prev之间插入了一个新Node,接下来调整引用
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
//如果插入的位置为表头,则first需要更改
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
unlinkFirst需要注意的特殊情况是如果list只有一个Node,则last也需要修改
/**
* Unlinks non-null first node f.
*/
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
unlinkLast需要注意的特殊情况是如果list只有一个元素,则first也需要修改
/**
* Unlinks non-null last node l.
*/
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
unlink任意节点需要注意的特殊情况是这个节点在两端。一般的思想可能是如果是首节点,写一段代码;是尾节点写一段代码;在中间写一段代码。但这里的思路是以要操纵的节点为中心,考虑它的前一个节点后后一个节点的情况。代码量减少,思路更清晰,这样的思路更具有普遍意义。
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
然后来看看add/remove,get/set是怎么实现的,与ArrayList有什么区别。
8.1 add
public boolean add(E e) {
linkLast(e);
return true;
}
空间add就是添加在list的末尾。
8.2 remove
public boolean remove(Object o) {
//哈哈,果然源码是最准确的,原来remove(null)都可以,我才知道!!!
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
//删除掉第一个找到的节点
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
//移除首节点除外
public E remove() {
return removeFirst();
}
8.3 get
如果index在前半段,从first开始遍历;如果在后半段,从last开始遍历
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
8.4 set
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}