基本工具(guava)

1.1-使用和避免null

輕率地使用null可能會導致很多令人驚愕的問題。通過學習Google底層代碼庫,我們發現95%的集合類不接受null值作爲元素。我們認爲, 相比默默地接受null,使用快速失敗操作拒絕null值對開發者更有幫助。

此外,Null的含糊語義讓人很不舒服。Null很少可以明確地表示某種語義,例如,Map.get(key)返回Null時,可能表示map中的值是null,亦或map中沒有key對應的值。Null可以表示失敗、成功或幾乎任何情況。使用Null以外的特定值,會讓你的邏輯描述變得更清晰。

Null確實也有合適和正確的使用場景,如在性能和速度方面Null是廉價的,而且在對象數組中,出現Null也是無法避免的。但相對於底層庫來說,在應用級別的代碼中,Null往往是導致混亂,疑難問題和模糊語義的元兇,就如同我們舉過的Map.get(key)的例子。最關鍵的是,Null本身沒有定義它表達的意思。

鑑於這些原因,很多Guava工具類對Null值都採用快速失敗操作,除非工具類本身提供了針對Null值的因變措施。此外,Guava還提供了很多工具類,讓你更方便地用特定值替換Null值。

具體案例

不要在Set中使用null,或者把null作爲map的鍵值。使用特殊值代表null會讓查找操作的語義更清晰。

如果你想把null作爲map中某條目的值,更好的辦法是 不把這一條目放到map中,而是單獨維護一個”值爲null的鍵集合” (null keys)。Map 中對應某個鍵的值是null,和map中沒有對應某個鍵的值,是非常容易混淆的兩種情況。因此,最好把值爲null的鍵分離開,並且仔細想想,null值的鍵在你的項目中到底表達了什麼語義。

如果你需要在列表中使用null——並且這個列表的數據是稀疏的,使用Map<Integer, E>可能會更高效,並且更準確地符合你的潛在需求。

此外,考慮一下使用自然的null對象——特殊值。舉例來說,爲某個enum類型增加特殊的枚舉值表示null,比如java.math.RoundingMode就定義了一個枚舉值UNNECESSARY,它表示一種不做任何舍入操作的模式,用這種模式做舍入操作會直接拋出異常。

如果你真的需要使用null值,但是null值不能和Guava中的集合實現一起工作,你只能選擇其他實現。比如,用JDK中的Collections.unmodifiableList替代Guava的ImmutableList

Optional

大多數情況下,開發人員使用null表明的是某種缺失情形:可能是已經有一個默認值,或沒有值,或找不到值。例如,Map.get返回null就表示找不到給定鍵對應的值。

Guava用Optional<T>表示可能爲null的T類型引用。一個Optional實例可能包含非null的引用(我們稱之爲引用存在),也可能什麼也不包括(稱之爲引用缺失)。它從不說包含的是null值,而是用存在或缺失來表示。但Optional從不會包含null值引用。

Optional<Integer> possible = Optional.of(5);
possible.isPresent(); // returns true`
possible.get(); // returns 5` 

Optional無意直接模擬其他編程環境中的”可選” or “可能”語義,但它們的確有相似之處。

Optional最常用的一些操作被羅列如下:

創建Optional實例(以下都是靜態方法):

方法 詳情
Optional.of(T) 創建指定引用的Optional實例,若引用爲null則快速失敗
Optional.absent() 創建引用缺失的Optional實例
Optional.fromNullable(T) 創建指定引用的Optional實例,若引用爲null則表示缺失

用Optional實例查詢引用(以下都是非靜態方法):

方法 詳情
boolean isPresent() 如果Optional包含非null的引用(引用存在),返回true
T get() 返回Optional所包含的引用,若引用缺失,則拋出java.lang.IllegalStateException
T or(T) 返回Optional所包含的引用,若引用缺失,返回指定的值
T orNull() 返回Optional所包含的引用,若引用缺失,返回null
Set<T> asSet() 返回Optional所包含引用的單例不可變集,如果引用存在,返回一個只有單一元素的集合,如果引用缺失,返回一個空集合。

使用****Optional****的意義在哪兒?

使用Optional除了賦予null語義,增加了可讀性,最大的優點在於它是一種傻瓜式的防護。Optional迫使你積極思考引用缺失的情況,因爲你必須顯式地從Optional獲取引用。直接使用null很容易讓人忘掉某些情形,儘管FindBugs可以幫助查找null相關的問題,但是我們還是認爲它並不能準確地定位問題根源。

如同輸入參數,方法的返回值也可能是null。和其他人一樣,你絕對很可能會忘記別人寫的方法method(a,b)會返回一個null,就好像當你實現method(a,b)時,也很可能忘記輸入參數a可以爲null。將方法的返回類型指定爲Optional,也可以迫使調用者思考返回的引用缺失的情形。

其他處理null的便利方法

當你需要用一個默認值來替換可能的null,請使用Objects.firstNonNull(T, T) 方法。如果兩個值都是null,該方法會拋出NullPointerException。Optional也是一個比較好的替代方案,例如:Optional.of(first).or(second).

還有其它一些方法專門處理null或空字符串:emptyToNull(String)nullToEmpty(String)<tt>,</tt>isNullOrEmpty(String)。我們想要強調的是,這些方法主要用來與混淆null/空的API進行交互。當每次你寫下混淆null/空的代碼時,Guava團隊都淚流滿面。(好的做法是積極地把null和空區分開,以表示不同的含義,在代碼中把null和空同等對待是一種令人不安的壞味道。

1.2-前置條件

前置條件:讓方法調用的前置條件判斷更簡單。

Guava在Preconditions類中提供了若干前置條件判斷的實用方法,我們強烈建議在Eclipse中靜態導入這些方法。每個方法都有三個變種:

  • 沒有額外參數:拋出的異常中沒有錯誤消息;
  • 有一個Object對象作爲額外參數:拋出的異常使用Object.toString() 作爲錯誤消息;
  • 有一個String對象作爲額外參數,並且有一組任意數量的附加Object對象:這個變種處理異常消息的方式有點類似printf,但考慮GWT的兼容性和效率,只支持%s指示符。例如:
checkArgument(i >= 0, "Argument was %s but expected nonnegative", i);
checkArgument(i < j, "Expected i < j, but %s > %s", i, j);
方法聲明(不包括額外參數) 描述 檢查失敗時拋出的異常
checkArgument(boolean) 檢查boolean是否爲true,用來檢查傳遞給方法的參數。 IllegalArgumentException
checkNotNull(T) 檢查value是否爲null,該方法直接返回value,因此可以內嵌使用checkNotNull<tt>。</tt> NullPointerException
checkState(boolean) 用來檢查對象的某些狀態。 IllegalStateException
checkElementIndex(int index, int size) 檢查index作爲索引值對某個列表、字符串或數組是否有效。index>=0 && index<size * IndexOutOfBoundsException
checkPositionIndex(int index, int size) 檢查index作爲位置值對某個列表、字符串或數組是否有效。index>=0 && index<=size * IndexOutOfBoundsException
checkPositionIndexes(int start, int end, int size) 檢查[start, end]表示的位置範圍對某個列表、字符串或數組是否有效* IndexOutOfBoundsException

譯者注:

****索引值常用來查找列表、字符串或數組中的元素,如*List.get(int), String.charAt(int)

*位置值和位置範圍常用來截取列表、字符串或數組,如List.subList(int,int), String.substring(int)

相比Apache Commons提供的類似方法,我們把Guava中的Preconditions作爲首選。Piotr Jagielski在他的博客中簡要地列舉了一些理由:

  • 在靜態導入後,Guava方法非常清楚明晰。checkNotNull清楚地描述做了什麼,會拋出什麼異常;
  • checkNotNull直接返回檢查的參數,讓你可以在構造函數中保持字段的單行賦值風格:this.field = checkNotNull(field)
  • 簡單的、參數可變的printf風格異常信息。鑑於這個優點,在JDK7已經引入Objects.requireNonNull的情況下,我們仍然建議你使用checkNotNull。

在編碼時,如果某個值有多重的前置條件,我們建議你把它們放到不同的行,這樣有助於在調試時定位。此外,把每個前置條件放到不同的行,也可以幫助你編寫清晰和有用的錯誤消息。

1.3-常見Object方法

equals

當一個對象中的字段可以爲null時,實現Object.equals方法會很痛苦,因爲不得不分別對它們進行null檢查。使用<tt>Objects.equal</tt>幫助你執行null敏感的equals判斷,從而避免拋出NullPointerException。例如:

Objects.equal("a", "a"); // returns true

Objects.equal(null, "a"); // returns false

Objects.equal("a", null); // returns false

Objects.equal(null, null); // returns true

注意:JDK7引入的Objects類提供了一樣的方法<tt>Objects.equals</tt>

hashCode

用對象的所有字段作散列[hash]運算應當更簡單。Guava的<tt>Objects.hashCode(Object...)</tt>會對傳入的字段序列計算出合理的、順序敏感的散列值。你可以使用Objects.hashCode(field1, field2, …, fieldn)來代替手動計算散列值。

注意:JDK7引入的Objects類提供了一樣的方法<tt>Objects.hash(Object...)</tt>

toString

好的toString方法在調試時是無價之寶,但是編寫toString方法有時候卻很痛苦。使用 Objects.toStringHelper可以輕鬆編寫有用的toString方法。例如:

   // Returns "ClassName{x=1}" 

   Objects.toStringHelper(this).add("x", 1).toString(); 

   // Returns "MyObject{x=1}" 

   Objects.toStringHelper("MyObject").add("x", 1).toString(); 

compare/compareTo

實現一個比較器[Comparator],或者直接實現Comparable接口有時也傷不起。考慮一下這種情況:

   class Person implements Comparable<Person> { 

   private String lastName; 

   private String firstName; 

   private int zipCode; 

     

   public int compareTo(Person other) { 

   int cmp = lastName.compareTo(other.lastName); 

   if (cmp != 0) { 

   return cmp; 

   } 

   cmp = firstName.compareTo(other.firstName); 

   if (cmp != 0) { 

   return cmp; 

   } 

   return Integer.compare(zipCode, other.zipCode); 

   } 

   } 

這部分代碼太瑣碎了,因此很容易搞亂,也很難調試。我們應該能把這種代碼變得更優雅,爲此,Guava提供了<tt>ComparisonChain</tt>

ComparisonChain執行一種懶比較:它執行比較操作直至發現非零的結果,在那之後的比較輸入將被忽略。

   public int compareTo(Foo that) { 

   return ComparisonChain.start() 

   .compare(this.aString, that.aString) 

   .compare(this.anInt, that.anInt) 

   .compare(this.anEnum, that.anEnum, Ordering.natural().nullsLast()) 

   .result(); 

   } 

這種Fluent接口風格的可讀性更高,發生錯誤編碼的機率更小,並且能避免做不必要的工作。更多Guava排序器工具可以在下一節裏找到。

排序: Guava強大的”流暢風格比較器”

排序器[Ordering]是Guava流暢風格比較器[Comparator]的實現,它可以用來爲構建複雜的比較器,以完成集合排序的功能。

從實現上說,Ordering實例就是一個特殊的Comparator實例。Ordering把很多基於Comparator的靜態方法(如Collections.max)包裝爲自己的實例方法(非靜態方法),並且提供了鏈式調用方法,來定製和增強現有的比較器。

創建排序器:常見的排序器可以由下面的靜態方法創建

方法 描述
natural() 對可排序類型做自然排序,如數字按大小,日期按先後排序
usingToString() 按對象的字符串形式做字典排序[lexicographical ordering]
from(Comparator) 把給定的Comparator轉化爲排序器

實現自定義的排序器時,除了用上面的from方法,也可以跳過實現Comparator,而直接繼承Ordering:

   Ordering<String> byLengthOrdering = new Ordering<String>() { 

   public int compare(String left, String right) { 

   return Ints.compare(left.length(), right.length()); 

   } 

   };

鏈式調用方法:通過鏈式調用,可以由給定的排序器衍生出其它排序器

方法 描述
reverse() 獲取語義相反的排序器
nullsFirst() 使用當前排序器,但額外把null值排到最前面。
nullsLast() 使用當前排序器,但額外把null值排到最後面。
compound(Comparator) 合成另一個比較器,以處理當前排序器中的相等情況。
lexicographical() 基於處理類型T的排序器,返回該類型的可迭代對象Iterable<T>的排序器。
onResultOf(Function) 對集合中元素調用Function,再按返回值用當前排序器排序。

例如,你需要下面這個類的排序器。

   class Foo { 

   @Nullable String sortedBy; 

   int notSortedBy; 

   } 

考慮到排序器應該能處理sortedBy爲null的情況,我們可以使用下面的鏈式調用來合成排序器:

   Ordering<Foo> ordering = Ordering.natural().nullsFirst().onResultOf(new Function<Foo, String>() { 

   public String apply(Foo foo) { 

   return foo.sortedBy; 

   } 

   }); 

當閱讀鏈式調用產生的排序器時,應該從後往前讀。上面的例子中,排序器首先調用apply方法獲取sortedBy值,並把sortedBy爲null的元素都放到最前面,然後把剩下的元素按sortedBy進行自然排序。之所以要從後往前讀,是因爲每次鏈式調用都是用後面的方法包裝了前面的排序器。

注:用compound方法包裝排序器時,就不應遵循從後往前讀的原則。爲了避免理解上的混亂,請不要把compound寫在一長串鏈式調用的中間,你可以另起一行,在鏈中最先或最後調用compound。

超過一定長度的鏈式調用,也可能會帶來閱讀和理解上的難度。我們建議按下面的代碼這樣,在一個鏈中最多使用三個方法。此外,你也可以把Function分離成中間對象,讓鏈式調用更簡潔緊湊。

Ordering<Foo> ordering = Ordering.natural().nullsFirst().onResultOf(sortKeyFunction)

運用排序器:Guava的排序器實現有若干操縱集合或元素值的方法

方法 描述 另請參見
greatestOf(Iterable iterable, int k) 獲取可迭代對象中最大的k個元素。 leastOf
isOrdered(Iterable) 判斷可迭代對象是否已按排序器排序:允許有排序值相等的元素。 isStrictlyOrdered
sortedCopy(Iterable) 判斷可迭代對象是否已嚴格按排序器排序:不允許排序值相等的元素。 immutableSortedCopy
min(E, E) 返回兩個參數中最小的那個。如果相等,則返回第一個參數。 max(E, E)
min(E, E, E, E...) 返回多個參數中最小的那個。如果有超過一個參數都最小,則返回第一個最小的參數。 max(E, E, E, E...)
min(Iterable) 返回迭代器中最小的元素。如果可迭代對象中沒有元素,則拋出NoSuchElementException。 max(Iterable), min(Iterator), max(Iterator)

1.5-Throwables:簡化異常和錯誤的傳播與檢查

有時候,你會想把捕獲到的異常再次拋出。這種情況通常發生在Error或RuntimeException被捕獲的時候,你沒想捕獲它們,但是聲明捕獲Throwable和Exception的時候,也包括了了Error或RuntimeException。Guava提供了若干方法,來判斷異常類型並且重新傳播異常。例如:

   try { 

   someMethodThatCouldThrowAnything(); 

   } catch (IKnowWhatToDoWithThisException e) { 

   handle(e); 

   } catch (Throwable t) { 

   Throwables.propagateIfInstanceOf(t, IOException.class); 

   Throwables.propagateIfInstanceOf(t, SQLException.class); 

   throw Throwables.propagate(t); 

   } 

所有這些方法都會自己決定是否要拋出異常,但也能直接拋出方法返回的結果——例如,throw Throwables.propagate(t);—— 這樣可以向編譯器聲明這裏一定會拋出異常。

Guava中的異常傳播方法簡要列舉如下:

方法 描述
RuntimeException propagate(Throwable) 如果Throwable是Error或RuntimeException,直接拋出;否則把Throwable包裝成RuntimeException拋出。返回類型是RuntimeException,所以你可以像上面說的那樣寫成<tt>throw Throwables.propagate(t)</tt>,Java編譯器會意識到這行代碼保證拋出異常。
void propagateIfInstanceOf( Throwable, Class<X extends Exception>) throws X Throwable類型爲X才拋出
void propagateIfPossible( Throwable) Throwable類型爲Error或RuntimeException才拋出
void propagateIfPossible( Throwable, Class<X extends Throwable>) throws X Throwable類型爲X, Error或RuntimeException才拋出

Throwables.propagate的用法

模仿Java7的多重異常捕獲和再拋出

通常來說,如果調用者想讓異常傳播到棧頂,他不需要寫任何catch代碼塊。因爲他不打算從異常中恢復,他可能就不應該記錄異常,或者有其他的動作。他可能是想做一些清理工作,但通常來說,無論操作是否成功,清理工作都要進行,所以清理工作可能會放在finallly代碼塊中。但有時候,捕獲異常然後再拋出也是有用的:也許調用者想要在異常傳播之前統計失敗的次數,或者有條件地傳播異常。

當只對一種異常進行捕獲和再拋出時,代碼可能還是簡單明瞭的。但當多種異常需要處理時,卻可能變得一團糟:

   @Override public void run() { 

   try { 

   delegate.run(); 

   } catch (RuntimeException e) { 

   failures.increment(); 

   throw e; 

   }catch (Error e) { 

   failures.increment(); 

   throw e; 

   } 

   } 

Java7用多重捕獲解決了這個問題:

   } catch (RuntimeException  Error e) { 

   failures.increment(); 

   throw e; 

   } 

非Java7用戶卻受困於這個問題。他們想要寫如下代碼來統計所有異常,但是編譯器不允許他們拋出Throwable(譯者注:這種寫法把原本是Error或RuntimeException類型的異常修改成了Throwable,因此調用者不得不修改方法簽名):

  } catch (Throwable t) { 

  failures.increment(); 

  throw t; 

  } 

解決辦法是用throw Throwables.propagate(t)替換throw t。在限定情況下(捕獲Error和RuntimeException),Throwables.propagate和原始代碼有相同行爲。然而,用Throwables.propagate也很容易寫出有其他隱藏行爲的代碼。尤其要注意的是,這個方案只適用於處理RuntimeException 或Error。如果catch塊捕獲了受檢異常,你需要調用propagateIfInstanceOf來保留原始代碼的行爲,因爲Throwables.propagate不能直接傳播受檢異常。

總之,Throwables.propagate的這種用法也就馬馬虎虎,在Java7中就沒必要這樣做了。在其他Java版本中,它可以減少少量的代碼重複,但簡單地提取方法進行重構也能做到這一點。此外,使用propagate會意外地包裝受檢異常。

非必要用法:把拋出的Throwable轉爲Exception

有少數API,尤其是Java反射API和(以此爲基礎的)Junit,把方法聲明成拋出Throwable。和這樣的API交互太痛苦了,因爲即使是最通用的API通常也只是聲明拋出Exception。當確定代碼會拋出Throwable,而不是Exception或Error時,調用者可能會用Throwables.propagate轉化Throwable。這裏有個用Callable執行Junit測試的範例:

  public Void call() throws Exception { 

  try { 

  FooTest.super.runTest(); 

  } catch (Throwable t) { 

  Throwables.propagateIfPossible(t, Exception.class); 

  Throwables.propagate(t); 

  } 

    

  return null; 

  } 

在這兒沒必要調用propagate()方法,因爲propagateIfPossible傳播了Throwable之外的所有異常類型,第二行的propagate就變得完全等價於throw new RuntimeException(t)。(題外話:這個例子也提醒我們,propagateIfPossible可能也會引起混亂,因爲它不但會傳播參數中給定的異常類型,還拋出Error和RuntimeException)

這種模式(或類似於throw new RuntimeException(t)的模式)在Google代碼庫中出現了超過30次。(搜索’propagateIfPossible[^;]* Exception.class[)];’)絕大多數情況下都明確用了”throw new RuntimeException(t)”。我們也曾想過有個”throwWrappingWeirdThrowable”方法處理Throwable到Exception的轉化。但考慮到我們用兩行代碼實現了這個模式,除非我們也丟棄propagateIfPossible方法,不然定義這個throwWrappingWeirdThrowable方法也並沒有太大必要。

Throwables.propagate的有爭議用法

爭議一:把受檢異常轉化爲非受檢異常

原則上,非受檢異常代表bug,而受檢異常表示不可控的問題。但在實際運用中,即使JDK也有所誤用——如Object.clone()、Integer. parseInt(String)、URI(String)——或者至少對某些方法來說,沒有讓每個人都信服的答案,如URI.create(String)的異常聲明。

因此,調用者有時不得不把受檢異常和非受檢異常做相互轉化:

  try { 

  return Integer.parseInt(userInput); 

  } catch (NumberFormatException e) { 

  throw new InvalidInputException(e); 

  } 

  try { 

  return publicInterfaceMethod.invoke(); 

  } catch (IllegalAccessException e) { 

  throw new AssertionError(e); 

  } 

有時候,調用者會使用Throwables.propagate轉化異常。這樣做有沒有什麼缺點?最主要的恐怕是代碼的含義不太明顯。throw Throwables.propagate(ioException)做了什麼?throw new RuntimeException(ioException)做了什麼?這兩者做了同樣的事情,但後者的意思更簡單直接。前者卻引起了疑問:”它做了什麼?它並不只是把異常包裝進RuntimeException吧?如果它真的只做了包裝,爲什麼還非得要寫個方法?”。應該承認,這些問題部分是因爲”propagate”的語義太模糊了(用來拋出未聲明的異常嗎?)。也許”wrapIfChecked”更能清楚地表達含義。但即使方法叫做”wrapIfChecked”,用它來包裝一個已知類型的受檢異常也沒什麼優點。甚至會有其他缺點:也許比起RuntimeException,還有更合適的類型——如IllegalArgumentException。
我們有時也會看到propagate被用於傳播可能爲受檢的異常,結果是代碼相比以前會稍微簡短點,但也稍微有點不清晰:

  } catch (RuntimeException e) { 

  throw e; 

  }catch (Exception e) { 

  throw new RuntimeException(e); 

  } 

  } catch (Exception e) { 

  throw Throwables.propagate(e); 

  } 

然而,我們似乎故意忽略了把檢查型異常轉化爲非檢查型異常的合理性。在某些場景中,這無疑是正確的做法,但更多時候它被用於避免處理受檢異常。這讓我們的話題變成了爭論受檢異常是不是壞主意了,我不想對此多做敘述。但可以這樣說,Throwables.propagate不是爲了鼓勵開發者忽略IOException這樣的異常。

爭議二:異常穿隧

但是,如果你要實現不允許拋出異常的方法呢?有時候你需要把異常包裝在非受檢異常內。這種做法挺好,但我們再次強調,沒必要用propagate方法做這種簡單的包裝。實際上,手動包裝可能更好:如果你手動包裝了所有異常(而不僅僅是受檢異常),那你就可以在另一端解包所有異常,並處理極少數特殊場景。此外,你可能還想把異常包裝成特定的類型,而不是像propagate這樣統一包裝成RuntimeException。

爭議三:重新拋出其他線程產生的異常

 try { 

 return future.get(); 

 } catch (ExecutionException e) { 

 throw Throwables.propagate(e.getCause()); 

 } 

對這樣的代碼要考慮很多方面:

  • ExecutionException的cause可能是受檢異常,見上文”爭議一:把檢查型異常轉化爲非檢查型異常”。但如果我們確定future對應的任務不會拋出受檢異常呢?(可能future表示runnable任務的結果——譯者注:如ExecutorService中的submit(Runnable task, T
    result)方法
    )如上所述,你可以捕獲異常並拋出AssertionError。尤其對於Future,請考慮 Futures.get方法。(TODO:對future.get()拋出的另一個異常InterruptedException作一些說明)
  • ExecutionException的cause可能直接是Throwable類型,而不是Exception或Error。(實際上這不大可能,但你想直接重新拋出cause的話,編譯器會強迫你考慮這種可能性)見上文”用法二:把拋出Throwable改爲拋出Exception”。
  • ExecutionException的cause可能是非受檢異常。如果是這樣的話,cause會直接被Throwables.propagate拋出。不幸的是,cause的堆棧信息反映的是異常最初產生的線程,而不是傳播異常的線程。通常來說,最好在異常鏈中同時包含這兩個線程的堆棧信息,就像ExecutionException所做的那樣。(這個問題並不單單和propagate方法相關;所有在其他線程中重新拋出異常的代碼都需要考慮這點)

異常原因鏈

Guava提供瞭如下三個有用的方法,讓研究異常的原因鏈變得稍微簡便了,這三個方法的簽名是不言自明的:

方法
Throwable getRootCause(Throwable)
List<Throwable> getCausalChain(Throwable)
String getStackTraceAsString(Throwable)

原創文章,轉載請註明: 轉載自併發編程網 – ifeve.com本文鏈接地址: [Google Guava] 1.5-Throwables:簡化異常和錯誤的傳播與檢查

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章