Vector、ArrayList、LinkedList的原理和區別?
ArrayList
總體結構:
先看源碼中ArrayList的Field:
private static final long serialVersionUID = 8683452581122892189L;//用於序列化
private static final int DEFAULT_CAPACITY = 10;//默認的數組大小
private static final Object[] EMPTY_ELEMENTDATA = {};//空數組
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//默認空數組
transient Object[] elementData; //數組緩存
private int size;//大小,注意:和elementData的length是不一樣的,size只能小於等於length,因爲ArrayList經常不會存滿。
第一眼看到Object[] elementData,ArrayList的原理是基於數組的,而後面的LinkedList 是基於Node的一個雙向鏈表(待會會說。)
下面我們來分析一些ArrayList的主要方法:
ArrayList()
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;//如果明確指明大小爲0時,elementData就指向EMPTY_ELEMENTDATA
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//沒有指定大小時,elementData指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)//這句話是判斷是否是Object類型的數組(比如int,long就不是Object類型的數組)
elementData = Arrays.copyOf(elementData, size, Object[].class);//如果不是就需要轉成Object類型的數組
} else {//如果集合爲空,則elementData指向EMPTY_ELEMENTDATA
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
add()
public boolean add(E e) {
ensureCapacityInternal(size + 1); //這一步就是去計算容量大小的方法。
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//計算所需容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//當elementData爲空數組時,容量就爲DEFAULT_CAPACITY和minCapacity較大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//這裏需要說一下爲什麼要有一個DEFAULT_CAPACITY,因爲是爲了讓數組緩存在一定時間內夠用,
//而不用反覆的重新創建適當容量大小的新數組緩存,因爲創建數組的開銷較大,效率較低,但是
//DEFAULT_CAPACITY的值又不能過大,這樣會浪費內存空間。
//所以DEFAULT_CAPACITY相當於一個域值,當不是認爲的指定了大小時,即使calculateCapacity()中指定了一個
//minCapacity,也不能小於DEFAULT_CAPACITY的大小。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code 這裏做了一個判斷,只有給定的minCapacity大於現在的容量時纔會grow,
//否則沒有意義
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//這一步可以看出自動分配的新容量時原來的1.5倍
if (newCapacity - minCapacity < 0)//但是當新容量仍然不能滿足時,就直接用指定的minCapacity
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);//這裏就是重新創建一個新容量的數組
}
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;
}
從這個方法可以看出即使是刪除掉了一個元素,也不影響elementData的長度,也就是說內存空間並沒有變。
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
即使是清空,elementData在內存中所佔的長度仍然沒有變。
trimToSize()
刪除和清空操作都不能改變所佔內存,這是一件很操蛋的事情,下面這個方法就是釋放沒有使用的空間。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
Vector
總體結構:
先看源碼中ArrayList的Field:
protected Object[] elementData;//數組緩存
protected int elementCount;//元素個數
protected int capacityIncrement;//容量增長量
插:看了ArrayList的源碼,可以知道ArrayList的元素個數變量名字爲size,而這裏叫做elementCount,可以猜測是兩波人寫的東西。
果然,在JDK1.8中,ArrayList是Josh Bloch和 Neal Gafter,Vector是 Lee Boynton和Jonathan Payne
再一次看到Object[] elementData,我們就可以知道,和ArrayList一樣,Vector的原理是基於數組的,elementData就是用於存放數據的緩存。
下面我們來分析一些Vector的主要方法:
//指定初始容量,和增長量的構造方法
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
//只指定初始容量的構造方法
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
//無參構造方法
public Vector() {
this(10);//重點:默認是10的大小
}
//指定集合的構造方法
public Vector(Collection<? extends E> c) {
elementData = c.toArray();
elementCount = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
可以看到無參構造方法中,Vector默認給定的大小是10
最主要的我們來看一下grow()方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//重點
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
當capacityIncrement 大於0的時候,新的容量就等於老容量加上capacityIncrement ,如果capacityIncrement 小於等於0,新容量就是老容量的兩倍
Vector中的增刪改類型的方法前面均有synchronized關鍵字,說明Vector中的操作是線程同步的。
其實與ArrayList相比,Vector和ArrayList的關係更像是兄弟關係,父親完全一樣,只有自身的特點不一樣,增刪改的方法都差不多,只是Vector加了線程同步控制,這樣Vector的效率會低很多,但是是線程安全的。
另外grow()中,ArrayList是增長1.5倍,而Vector在沒有指定增長量的情況下默認增加2倍。
LinkedList
總體結構:
可以看出LinkedList 其實就比ArrayList 多實現了隊列的方法,而且是一個雙端隊列。
但是將LinkedList僅僅只看做是一個雙端隊列也不合理,因爲我們知道雙端隊列,兩個端口都可以入隊出隊,但是不能中間插入,但是LinkedList不僅可以兩端插入,刪除,還可以中間插入,這就是一個雙向鏈表的結構。
詳細結構:
從樹形結構最頂部往下一次分析:
Iterable:(接口)
forEach:
default void forEach(Consumer<? super T> action) {
//判斷是否爲null,是null拋出空指針異常。
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
這個接口最主要的就是定義了一個遍歷的方法。
default關鍵字可以讓接口中的方法可以有默認的函數體,這是jdk8的關鍵字,當一個類實現這個接口時,可以不用去實現這個方法,當然,這個類若實現這個方法,就等於子類覆蓋了這個方法,最終運行結果符合Java多態特性。
Collection:(接口)
任何一個數據容器都應該包含幾類常用方法:
增
boolean add(E e):向集合添加元素
boolean addAll(Collection<? extends E> c)
刪
`boolean remove(Object o):`
`boolean removeAll(Collection<?> c)//集合刪除`
查
Iterator<E> iterator();//通過遍歷器去遍歷查詢每個元素
包含
boolean contains(Object o):判斷集合是否包含某個元素
boolean containsAll(Collection<?> c)
大小
int size():求集合大小
常用工具方法
boolean retainAll(Collection<?> c)//取交集 list1 與 list2 存在相同元素,list1集合只保留list2中存在的元素,list1 與 list3 不存在相同元素,list1集合變爲空
boolean isEmpty():判斷集合是否爲空
Object[] toArray():將集合中的元素轉換爲元素數組
<T> T[] toArray(T[] a):?
void clear()//清空
boolean equals(Object o)
int hashCode()
AbstractCollection(抽象類)
看到這個類前面有個Abstract,可以知道這是一個抽象類,那麼我們需要了解一下抽象類的作用,這會幫助你瞭解代碼設計的一些思想。
抽象類在我看來就一個作用:提供接口的骨架實現,以儘量減少實現此接口所需的工作量。
contains
public boolean contains(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return true;
} else {
while (it.hasNext())
if (o.equals(it.next()))
return true;
}
return false;
}
可以看出包含方法的原理是遍歷,一個一個去比較。
但是這裏需要注意的是,這裏比較是否包含的規則是,equals,也就是說不一定是內存地址相同。那麼equals規則可以由元素自己來確定。這種思想就是個人自掃門前雪。
toArray
public Object[] toArray() {
// Estimate size of array; be prepared to see more or fewer elements
Object[] r = new Object[size()];
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
// fewer elements than expected
//這裏是一個控制,當實際元素個數少於預測個數時,直接重新分配一個適量的數組。
if (! it.hasNext())
return Arrays.copyOf(r, i);
r[i] = it.next();
}
//當實際元素個數大於預測個數時,依然重新分配一個數組。
return it.hasNext() ? finishToArray(r, it) : r;
}
這裏需要理解的是:size()只是一個準備的大小,並不一定是真實大小。
如果不懂Arrays.copyOf(r, i)的使用可以參看我的這篇博客《Arrays的常用方法詳解》
add
public boolean add(E e) {
throw new UnsupportedOperationException();
}
這個方法更簡單,直接拋出一個不支持異常,所以這個方法是肯定需要子類重寫的。
addAll
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
這個方法定義了一個modified標誌,就是說只要有一個元素添加成功了,就說明源集合已經被修改過了,
所以這裏可以看出addAll的返回值爲true時,並不是代表全部添加成功,而是代表源集合被修改過,或者說至少添加了一個元素進去
。
remove
public boolean remove(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext()) {
if (it.next()==null) {
it.remove();
return true;
}
}
} else {
while (it.hasNext()) {
if (o.equals(it.next())) {
it.remove();
return true;
}
}
}
return false;
}
可以看出刪除元素的原理還是遍歷,先找到這個元素,然後刪除這個元素
removeAll
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
如果理解了addAll,那麼removeAll也很好理解了,
removeAll的返回值爲true的時候並不是代表全部刪除成功,而是表示至少刪除了一個元素
。
containsAll
如果此集合包含指定 集合中的所有元素,則返回true。
注意這裏是必須包含所有元素哦,也就是說只要有一個元素不包含就返回false,這也是下面源碼中算法理解
public boolean containsAll(Collection<?> c) {
for (Object e : c)
if (!contains(e))
return false;
return true;
}
retainAll
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<E> it = iterator();
while (it.hasNext()) {
if (!c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
retainAll是求交集的意思,原理其實就是在源集合中刪除不是交集的元素,也就是說這個方法會改變本集合的元素。
從源碼中可以看出,返回值如果爲true不代表求交集成功,而只能代表本集合有改變。
clear
public void clear() {
Iterator<E> it = iterator();
while (it.hasNext()) {
it.next();
it.remove();
}
}
clear就很好理解了,就是遍歷,一個一個的刪除。
toString
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {//無限循環,退出循環的控制在內部使用return來控制。
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
這裏可以看出原理是使用StringBuilder 來進行字符串的拼接,這裏之所以不用String來拼接,是因爲String的拼接效率會很慢,至於具體爲什麼,可以參看我的這篇博客《String、StringBuffer、StringBuilder 的區別?String爲什麼是不可變的?》
另外這裏有一個小技巧,就是無限循環的使用,我主要是想說一下我們一般在什麼時候使用無限循環。通常我們的循環都是在循環之前就已經知道循環次數,
那麼如果我們在循環之前不知道具體的循環次數呢,這個時候就需要使用無限循環,退出循環的控制在循環體內部來控制。
List(接口)
在總體結構圖中可以看出,List接口集成Collection接口,說明List也是一種特殊的Collection,那麼其中Collection的方法我就不分析了,只是說一些List中定義的List獨有方法。
sort
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
最終的排序還是使用的數組排序方法Arrays.sort(),讓後將排序號的數組,一個一個遍歷重新放置到list中。
Arrays.sort()的方法具體用法可以參看《Arrays的常用方法詳解》
既然是list就肯定有序號,那麼就會有序號的相關方法
E get(int index);
根據序號獲得元素
E set(int index, E element);
給某個位置設置元素
void add(int index, E element);
在某個位置後面插入一個元素
E remove(int index);
刪除某個位置的元素
int indexOf(Object o);
求某個元素的位置
int lastIndexOf(Object o);
求一個元素最後出現的位置
List subList(int fromIndex, int toIndex);
根據位置截取這個list,得到一個新的list
Queue(接口)
Queue是隊列的意思,也是繼承Collection接口的,說明隊列也是一種特殊的集合。
那麼隊列有哪些常見的操作呢?
boolean add(E e);
增加一個元索, 如果隊列已滿,則拋出一個IIIegaISlabEepeplian異常
boolean offer(E e);
添加一個元素並返回true, 如果隊列已滿,則返回false
E remove();
移除並返回隊列頭部的元素, 如果隊列爲空,則拋出一個NoSuchElementException異常
E poll();
移除並返問隊列頭部的元素,如果隊列爲空,則返回null
E element();
返回隊列頭部的元素,如果隊列爲空,則拋出一個NoSuchElementException異常
E peek();
返回隊列頭部的元素,如果隊列爲空,則返回null
Queue接口總結下來定義了6個方法,增刪查各兩個方法。都是一個方法拋異常,一個方法返回null。
AbstractList(抽象類)
這個抽象類繼承了AbstractCollection也實現了List接口的部分方法,剛剛我們說了抽象類的主要目的是實現一個骨架,那麼AbstractList骨架就是在AbstractCollection骨架的基礎上繼續添加一些新的方法,就是List接口中的部分方法。
indexOf
public int indexOf(Object o) {
ListIterator<E> it = listIterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return it.previousIndex();
} else {
while (it.hasNext())
if (o.equals(it.next()))
return it.previousIndex();
}
return -1;
}
這段代碼其實也很好理解,但是有個地方我需要說明一下,就是it.next()這個方法,取得元素之後,指針會往後面一個元素移動,所以要獲取到前面一個元素的index就必須使用
previousIndex()方法。
lastIndexOf
public int lastIndexOf(Object o) {
ListIterator<E> it = listIterator(size());
if (o==null) {
while (it.hasPrevious())
if (it.previous()==null)
return it.nextIndex();
} else {
while (it.hasPrevious())
if (o.equals(it.previous()))
return it.nextIndex();
}
return -1;
}
這個方法其實和indexOf方法一樣,只不過一個是從前面開始往後面找,這個方法是反過來,從後面開始往前面找。
另外需要注意的是ListIterator是AbstractList裏的一個內部類,通過listIterator()方法來獲取這個內部類,這裏使用這個內部類的最大作用是爲其擴展一些方法。比如我們這裏擴展了hasPrevious的方法,實際上都是在對AbstractList中的元素進行操作,但是,說到擴展方法,爲什麼我們不直接將hasPrevious方法寫到AbstractList類裏面呢,因爲如果直接寫到AbstractList類裏面,會出現繼承關係混亂,臃腫,有些時候我們其實並不希望AbstractList的子類直接繼承到hasPrevious這個方法,這個時候就需要我們的內部類了,具體可以參看《Java內部類的作用》
subList
public List<E> subList(int fromIndex, int toIndex) {
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));
}
這個方法是最值得注意的一個方法,從名字上面看是截取這個list,但是我們看了SubList類之後我們會發現,其實我們真正操作的還是當前List,也就是說SubList只是對當前List的一個包裝,我們操作這個Sublist的數據的時候,當前list的數據也會改變。
Deque(接口)
雙端隊列
概念解釋:
deque 即雙端隊列。 (deque,全名double-ended queue)是一種具有隊列和棧的性質的數據結構。
雙端隊列中的元素可以從兩端彈出,其限定插入和刪除操作在表的兩端進行
。
雙端隊列是限定插入和刪除操作在表的兩端進行的線性表。這兩端分別稱做端點1和端點2。也可像棧一樣,可以用一個鐵道轉軌網絡來比喻雙端隊列。在實際使用中,還可以有輸出受限的雙端隊列
(即一個端點允許插入和刪除,另一個端點只允許插入的雙端隊列)和輸入受限的雙端隊列
(即一個端點允許插入和刪除,另一個端點只允許刪除的雙端隊列)。而如果限定雙端隊列從某個端點插入的元素只能從該端點刪除,則該雙端隊列就蛻變爲兩個棧底相鄰的棧了。
增
void addFirst(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
刪
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
查
E getFirst();
E getLast();
E peekFirst();
E peekLast();
boolean removeFirstOccurrence(Object o);//刪除第一次出現在隊列中的這個元素
boolean removeLastOccurrence(Object o);//刪除最後一次出現在隊列中的這個元素
記憶:雙端隊列依舊是隊列,當然具備隊列的特質,入隊,出隊等,但是又由於雙端隊列體現了雙端的特點,所以就必定有一個頭(first)、一個尾(last)。
LinkedList
終於到了這個類,先看一下field
transient int size = 0;//大小
/**
* Pointer to first node.//指向第一個節點
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.//指向第最後一個節點
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
Node是LinkedList中的一個內部類,用於輔助LinkedList。
我們來分析一下LinkedList這個名字,就是鏈表的意思,而且是個雙向鏈表,關於雙向鏈表的概念,我百度一個,大家參考一下:
雙向鏈表也叫雙鏈表,是鏈表的一種,
它的每個數據結點中都有兩個指針,分別指向直接後繼和直接前驅
。所以,從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和後繼結點。一般我們都構造雙向循環鏈表。
看到重點沒有?就是說每個節點裏面都有前一個節點和後一個節點的引用,這樣就可以把節點與節點關聯起來,看看Node的代碼:
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;
}
}
好了,到這裏LinkedList的結構就說完了,其他的就是基於這個結構的一些方法了。這些方法都是接口中沒有實現的方法。這裏說幾個我覺得比較有學習價值的方法
其中最主要的就是linkFirst()、linkLast()、linkBefore()、unlinkFirst()、unlinkLast()、unlink()這六個方法。
linkFirst
/**
* Links e as first element.將元素鏈接到首端
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);//構建頭結點
first = newNode;
//更改結點的鏈接
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
linkLast
/**
* Links e as last element.將元素鏈接到尾端
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
linkBefore
/**
* 在succ結點前插入元素爲e的一個新的結點
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
unlinkFirst
斷開第一個節點鏈接
/**
* 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 幫助GC回收
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
get
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
想要獲得index位置的元素就必須要先獲得index位置的結點。node(index)
node
/**
* 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;
}
}
這裏有一個重點,就是先判斷index是屬於前面半段還是後面半段,也就是先定位一個大概範圍,然後再遍歷查找
。
toArray
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)//重點
result[i++] = x.item;
return result;
}
爲什麼把這個方法拿出來說,主要是說一下這個for循環,打破了我們常規的書寫方式,那是因爲我們以前都已經寫習慣了i++這種方式,其實for循環不僅僅是這種寫法,總結一下,只要有初始值,判斷條件,和控制語句,就可以組成循環。所以結構只要符合(初始語句;布爾值;控制語句)就可以實現循環。for(;;)這種情況就是無限循環。