201871010125 王玉江 《面向對象程序設計(Java)》第八周實驗總結
|
項目 |
內容 |
|
這個作業屬于哪個課程 |
http://www.rzrgm.cn/nwnu-daizh/ |
|
這個作業的要求在哪里 |
http://www.rzrgm.cn/nwnu-daizh/p/11703678.html |
|
作業學習目標 |
(1) 掌握接口定義方法; (2) 掌握實現接口類的定義要求; (3) 掌握實現了接口類的使用要求; (4) 掌握程序回調設計模式; (5) 掌握Comparator接口用法; (6) 掌握對象淺層拷貝與深層拷貝方法; (7) 掌握Lambda表達式語法; (8) 了解內部類的用途及語法要求。 |
第一部分:總結第六章理論知識(30分)
6.1 接口
6.1.1 接口概念
1.Comparable接口:都需要包含compareTo方法,注意在Java SE 5.0中,Comparable接口已經改進為泛型類型。
2.(1)接口的所有方法自動屬于public。因此,在接口中聲明方法時不必提供關鍵字public。
(2)接口絕不能含有實例域,在Java SE 8之前,也不能在接口中實現方法。
(3)提供實例域和方法實現的任務應該由實現接口的那個類來完成。
3.為了讓類實現一個接口,通常需要下面兩個步驟:
1)將類聲明為實現給定接口
2)對接口中的所有方法進行定義
4.Double.compare靜態方法,如果第一個參數小于第二個參數,它會返回一個負值;如果二者相等則返回0;否則返回一個正值。
注意:在實現接口時,必須把方法聲明為public;否則編譯器將認為這個方法的訪問屬性是包可見性,即類的默認訪問屬性,之后編譯器就會給出試圖提供更嚴格的訪問權限的警告信息。
提示:Comparable接口中的compareTo方法將返回一個整型數值。如果兩個對象不相等,則返回一個正值或者一個負值。
主要原因在于 Java 程序設計語言是一種強類型 (strongly typed) 語言。在調用方法的時候, 編譯器將會檢查這個方法是否存在。
注釋: 有人認為, 將 Arrays 類中的 sort 方法定義為接收一個 Comparable[ ] 數組就可以在使用元素類型沒有實現 Comparable 接口的數組作為參數調用 sort 方法時, 由編譯器給出錯誤報告。但事實并非如此。 在這種情況下, sort 方法可以接收一個 Object[ ] 數組, 并對其進行笨拙的類型轉換。
【API】java.lang.Comparable<T> 1.0 :
int compareTo(T other) 用這個對象與 other 進行比較。 如果這個對象小于 other 則返回負值; 如果相等則返回0;否則返回正值。
【API】java.util.Arrays 1.2 :
static void sort(Object[] a) 使用 mergesort 算法對數組 a 中的元素進行排序。要求數組中的元素必須屬于實現了Comparable 接口的類, 并且元素之間必須是可比較的 。
【API】java.lang.Integer 1.0 :
static int compare(int x, int y) 7
如果 x < y 返回一個負整數;如果 x 和 y 相等,則返回 0; 否則返回一個負整數。
【API】java.lang.Double 1.0 :
static int compare(double x, double y) 1.4
5.如果 x < y 返回一個負數;如果 x 和 y 相等則返回 0; 否則返回一個負數,與 equals 方 法 一 樣, 在 繼 承 過 程 中 有 可 能 會 出 現 問 題。
6.1.2 接口的特性
1.接口不是類,尤其不能使用 new 運算符實例化一個接口然而, 盡管不能構造接口的對象,卻能聲明接口的變量接口變量必須引用實現了接口的類對象接下來, 如同使用 instanceof 檢查一個對象是否屬于某個特定一一樣, 也可以使用instanceof 檢查一個對象是否實現了某個特定的接口,與可以建立類的繼承關系一樣,接口也可以被擴展。這里允許存在多條從具有較高通用性的接口到較高專用性的接口的鏈 。雖然在接口中不能包含實例域或靜態方法,但卻可以包含常量。
2.與接口中的方法都自動地被設置為 public—樣,接口中的域將被自動設為 public static final。
注意:可以將接口方法標記為 public, 將域標記為 public static final
3.有些接口只定義了常量, 而沒有定義方法。
例如, 在標準庫中有一個 SwingConstants就是這樣一個接口, 其中只包含 NORTH、 SOUTH 和 HORIZONTAL 等常量。 任何實現SwingConstants 接口的類都自動地繼承了這些常量, 并可以在方法中直接地引用 NORTH,而不必采用 SwingConstants.NORTH 這樣的形式。
4. 盡管每個類只能夠擁有一個超類, 但卻可以實現多個接口。 如果某個類實現了這個 Cloneable 接口,Object 類中的 clone 方法就可以創建類對象的一個拷貝。
1 )因為java不支持多重繼承,所以有了接口,一個類只能繼承一個父類,但可以實現多個接口,接口本身也可以繼承多個接口。
2 )接口里面的成員變量默認都是public static final類型的。必須被顯示的初始化。
3 )接口里面的方法默認都是public abstract類型的。隱式聲明。
4 )接口沒有構造方法,不能被實例化。
5 )接口不能實現另一個接口,但可以繼承多個接口。
6 )類如果實現了一個接口,那么必須實現接口里面的所有抽象方法,否則類要被定義為抽象類。
6.1.3 接口和抽象類
1.用abstract來聲明,沒有具體實例對象的類,不能用new來創建對象。可包含常規類所包含的任何東西。抽象類必須由子類繼承,如果abstract類的子類不是抽象類,那么子類必須重寫父類中所有的abstract方法。
2.接口:用interface聲明,是抽象方法和常量值定義的集合。從本質上講,接口是一種特殊的抽象類,這種抽象類中只包含常量和方法的定義,而沒有變量和方法的定義。接口中只能定義抽象方法,而且這些方法默認為是public的。只要類實現了接口,就可以在任何需要該接口的地方使用這個類的對象。此外,一個類可以實現多個接口。
3.接口與抽象類的區別:(1)接口不能實現任何方法,而抽象類可以。(2)類可以實現許多接口,但只有一個父類。(3)接口不是類分級結構的一部分,無任何聯系的類可以實現相同的接口
6.1.4 靜態方法
在 Java SE 8 中,允許在接口中增加靜態方法。理論上講,沒有任何理由認為這是不合法的。 只是這有違于將接口作為抽象規范的初衷。目前為止, 通常的做法都是將靜態方法放在伴隨類中。在標準庫中, 你會看到成對出現的接口和實用工具類,不過整個 Java 庫都以這種方式重構也是不太可能的, 但是實現你自己的接口時,不再需要為實用工具方法另外提供一個伴隨類。
6.1.5 默認方法
1.可以為接口方法提供一個默認實現。 必須用 default 修飾符標記這樣一個方法。當然, 這并沒有太大用處, 因為 Comparable 的每一個實際實現都要覆蓋這個方法。不過有些情況下, 默認方法可能很有用。默認方法可以調用任何其他方法。
2.默認方法的一個重要用法是“‘ 接口演化” (interface evolution)。 以 Collection 接口為例,這個接口作為 Java 的一部分已經有很多年了。 假設很久以前你提供了這樣一個類:public class Bag implements Collection
假設 stream 方法不是一個默認方法。那么 Bag 類將不能編譯, 因為它沒有實現這個新方法。 后來, 在 JavaSE 8 中, 又為這個接口增加了一個 stream 方法。 為接口增加一個非默認方法不能保證“ 源代碼兼容 ”(source compatible)。
不過, 假設不重新編譯這個類, 而只是使用原先的一個包含這個類的 JAR 文件。這個類仍能正常加載,盡管沒有這個新方法。程序仍然可以正常構造 Bag 實例, 不會有意外發生。不過, 如果程序在一個 Bag 實例上調用 stream方法,就會出現一個 AbstractMethodError。
將方法實現為一個默認方法就可以解決這兩個問題。 Bag 類又能正常編譯了。另外如果沒有重新編譯而直接加載這個類, 并在一個 Bag 實例上調用 stream 方法, 將調用 Collection.stream 方法。
6.1.6 解決默認方法沖突
1.如果先在一個接口中將一個方法定義為默認方法, 然后又在超類或另一個接口中定義了同樣的方法, 會發生的情況規則如下:
1 ) 超類優先。如果超類提供了一個具體方法,同名而且有相同參數類型的默認方法會被忽略。
2 ) 接口沖突。 如果一個超接口提供了一個默認方法,另一個接口提供了一個同名而且參數類型 (不論是否是默認參數)相同的方法, 必須覆蓋這個方法來解決沖突。
2.第二個規則。
Java 設計者更強調一致性。兩個接口如何沖突并不重要。 如果至少有一個接口提供了一個實現, 編譯器就會報告錯誤。
注釋:當然,如果兩個接口都沒有為共享方法提供默認實現, 那么就與 Java SE 8之前的情況一樣,這里不存在沖突。 實現類可以有兩個選擇: 實現這個方法, 或者干脆不實現。如果是后一種情況, 這個類本身就是抽象的。
3.另一種情況, 一個類擴展了一個超類,同時實現了一個接口,并從超類和接口繼承了相同的方法。在這種情況下, 只會考慮超類方法, 接口的所有默認方法都會被忽略。 這正是“ 類優先” 規則。“ 類優先” 規則可以確保與 Java SE 7 的兼容性。 如果為一個接口增加默認方法,這對于有這個默認方法之前能正常工作的代碼不會有任何影響。
注意:千萬不要讓一個默認方法重新定義 Object 類中的某個方法。 例如, 不能為 toString或 equals 定義默認方法, 盡
6.2 接口示例
6.2.1 接口和回調
1.回調( callback) 是一種常見的程序設計模式。在這種模式中, 可以指出某個特定事件發生時應該采取的動作。
【API】javax.swing.JOptionPane 1.2 :
static void showMessageDialog(Component parent, Object message) 顯示一個包含一條消息和 OK 按鈕的對話框。 這個對話框將位于其 parent 組件的中央。如果 parent 為 null , 對話框將顯示在屏幕的中央。
【API】javax.swing.Timer 1.2 :
Timer(int interval, ActionListener listener) 構造一個定時器, 每隔 interval 毫秒通告 listener—次 。
void start() 啟動定時器。一旦啟動成功, 定時器將調用監聽器的 actionPerformed。
void stop() 停止定時器。一旦停止成功, 定時器將不再調用監聽器的 actionPerformed。
【API】java.awt.Toolkit 1.0 :
static Toolkit getDefaultToolKit() 獲得默認的工具箱。 工具箱包含有關 GUI 環境的信息。
6.2.2 Comparator接口
1.我們希望按長度遞增的順序對字符串進行排序,而不是按字典順序進行排序。肯定不能讓 String 類用兩種不同的方式實現 compareTo 方法—更何況,String 類也不應由我們來修改。要處理這種情況,Arrays.Sort 方法還有第二個版本, 有一個數組和一個比較器 ( comparator ) 作為參數, 比較器是實現了 Comparator 接口的類的實例。將這個調用與 words[i].compareTo(words[j]) 做比較。這個 compare 方法要在比較器對象上調用, 而不是在字符串本身上調用。
注意:盡管 LengthComparator 對象沒有狀態, 不過還是需要建立這個對象的一個實例。我們需要這個實例來調用 compare 方法——它不是一個靜態方法, 利用 lambda 表達式可以更容易地使用 Comparator。
6.2.3 對象克隆
1.如果希望 copy 是一個新對象,它的初始狀態與 original 相同, 但是之后它們各自會有自己不同的狀態, 這種情況下就可以使用 clone 方法。不過,clone 方法是 Object 的一個 protected 方法, 這說明你的代碼不能直接調用這個方法。可以看到, 默認的克隆操作是“ 淺拷貝”,并沒有克隆對象中引用的其他對象。
2.淺拷貝的影響
如果原對象和淺克隆對象共享的子對象是不可變的, 那么這種共享就是安全的。如果子對象屬于一個不可變的類, 如 String, 就是這種情況。或者在對象的生命期中, 子對象一直包含不變的常量, 沒有更改器方法會改變它, 也沒有方法會生成它的引用,這種情況下同樣是安全的。不過, 通常子對象都是可變的, 必須重新定義 clone 方法來建立一個深拷貝, 同時克隆所有子對象。
3.對于每一個類,需要確定:
1 ) 默認的 clone 方法是否滿足要求;
2 ) 是否可以在可變的子對象上調用 clone 來修補默認的 clone 方法;
3 ) 是否不該使用 clone 。
實際上第 3 個選項是默認選項。 如果選擇第 1 項或第 2 項,類必須:
1 ) 實現 Cloneable 接口;
2 ) 重新定義 clone 方法,并指定 public 訪問修飾符 。
注釋:Cloneable 接口是 Java 提供的一組標記接口 ( tagging interface ) 之一, Comparable 等接口的通常用途是確保一個類實現一個或一組特定的方法。
標記接口不包含任何方法; 它唯一的作用就是允許在類型查詢中使用 instanceof。建議你自己的程序中不要使用標記接口。
即使 clone 的默認(淺拷貝)實現能夠滿足要求, 還是需要實現 Cloneable 接口, 將 clone重新定義為 public, 再調用 super.clone()。
4.與 Object.clone 提供的淺拷貝相比, 前面的 clone 方法并沒有為它增加任何功能。這里只是讓這個方法是公有的。 要建立深拷貝, 還需要做更多工作,克隆對象中可變的實例域。
如果在一個對象上調用 clone, 但這個對象的類并沒有實現 Cloneable 接口, Object 類的 clone 方法就會拋出一個 CloneNotSupportedException。必須當心子類的克隆。
5.在自己的類中實現 clone , 如果需要建立深拷貝,可能就需要實現這個方法。有些人認為應該完全避免使用 clone, 而實現另一個方法來達到同樣的目的。
注意:所有數組類型都有一個 public 的 clone 方法, 而不是 protected: 可以用這個方法建立一個新數組, 包含原數組所有元素的副本。
6.淺層拷貝與深層拷貝淺層拷貝:被拷貝對象的所有常量成員和基本類型屬性都有與原來對象相同的拷貝值,而若成員域是一個對象,則被拷貝對象該對象域的對象引用仍然指向原來的對象。
深層拷貝:被拷貝對象的所有成員域都含有與原來對象相同的值,且對象域將指向被復制過的新對象,而不是原有對象被引用的對象。換言之,深層拷貝將拷貝對象內引用的對象也拷貝一遍。
6.3 lambda表達式
6.3.1 為什么引入lambda表達式
1.lambda 表達式是一個可傳遞的代碼塊, 可以在以后執行一次或多次。
2.在 Java 中傳遞一個代碼段并不容易, 不能直接傳遞代碼段 。Java 是一種面向對象語言, 所以必須構造一個對象,這個對象的類需要有一個方法能包含所需的代碼 。
6.3.2 lambda表達式的語法
1.Lambda表達式的語法基本結構(arguments)->body
有如下幾種情況: 參數類型可推導時,不需要指定類型,如(a)->System.out.println(a)
只有一個參數且類型可推導時,不強制寫(),如a->System.out.println(a)
參數指定類型時,必須有括號,如(inta)>System.out.println(a)
參數可以為空,如()->System.out.println(“hello”)
body需要用包含語句,當只有一條語句時&可省略;
2.Java Lambda表達式是Java8引入的一個新的功能,主要用途是提供一個函數化的語法來簡化編碼。
Lambda表達式本質上是一個匿名方法。
public int add(int x,int y){returnx+y;}
轉成Lambda表達式后是這個樣子:(int x,int y)->x+y;
參數類型也可以省略,Java編譯器會根據上下文推斷出來:(x,y)->x+y;//返回兩數之和或者(x,y)->{returnx+y;}//顯式指明返回值
6.3.3 函數式接口
1.對于只有一個抽象方法的接口, 需要這種接口的對象時, 就可以提供一個 lambda 表達式。這種接口稱為函數式接口 (functional interface )。
注意: 接口完全有可能重新聲明 Object 類的方法, 如 toString 或 clone,這些聲明有可能會讓方法不再是抽象的。
2. Arrays.sort 方法。它的第二個參數需要一個Comparator 實例, Comparator 就是只有一個方法的接口, 所以可以提供一個 lambda 表達式 。
在底層, Arrays.sort 方法會接收實現了 Comparator<String> 的某個類的對象。 在這個對象上調用 compare 方法會執行這個 lambda 表達式的體。
這些對象和類的管理完全取決于具體實現, 與使用傳統的內聯類相比, 這樣可能要高效得多。最好把 lambda 表達式看作是一個函數, 而不是一個對象, 另外要接受 lambda 表達式可以傳遞到函數式接口。
3. lambda 表達式可以轉換為接口,在 Java 中, 對 lambda 表達式所能做的也只是能轉換為函數式接口。在其他支持函數字面量的程序設計語言中,可以聲明函數類型(如(String, String) -> int )、 聲明這些類型的變量,還可以使用變量保存函數表達式。
注意:(1)甚至不能把lambda 表達式賦給類型為 Object 的變量,Object 不是一個函數式接口。 *
(2)Java API 在 java.util.function 包中定義了很多非常通用的函數式接口。其中一個接口BiFunction<T, U, R> 描述了參數類型為 T 和 U 而且返回類型為 R 的函數。可以把我們的字符串比較 lambda 表達式保存在這個類型的變量中 。
(3)java.util.function 包中有一個尤其有用的接口 Predicate 。
(4 )ArrayList 類有一個 removeIf 方法, 它的參數就是一個 Predicate。這個接口專門用來傳遞lambda 表達式。
6.3.4 方法引用
1.表達式 System.out::println 是一個方法引用( method reference ), 它等價于 lambda 表達式x -> System.out.println(x)
2.要用 :: 操作符分隔方法名與對象或類名。 主要有3種情況:
object::instanceMethod
Class::staticMethod
Class::instanceMethod
在前 2 種情況中, 方法引用等價于提供方法參數的 lambda 表達式。前面已經提到,System.out::println 等價于 x -> System.out.println(x) 。類似地, Math::pow 等價于(x,y) ->Math.pow(x, y)。
對于第 3 種情況, 第 1 個參數會成為方法的目標。例如,String::compareToIgnoreCase 等
3.如果有多個同名的重載方法, 編譯器就會嘗試從上下文中找出你指的那一個方法。例如, Math.max 方法有兩個版本, 一個用于整數, 另一個用于 double 值。選擇哪一個版本取決于 Math::max 轉換為哪個函數式接口的方法參數。 類似于 lambda 表達式, 方法引用不能獨立存在,總是會轉換為函數式接口的實例。 *
4.可以在方法引用中使用 this 參數。 例如, this::equals 等同于 x-> this.equals(x)。 使用super 也是合法的。Super::instanceMethod使用this為目標,會調用給定方法的超類版本。
6.3.5 構造器引用
1.構造器引用與方法引用很類似,只不過方法名為 new。
2.可以用數組類型建立構造器引用。例如, int[]::new 是一個構造器引用,它有一個參數:即數組的長度。這等價于 lambda 表達式 x-> new int[x] 。
3.Java 有一個限制,無法構造泛型類型 T 的數組。數組構造器引用對于克服這個限制很有用。表達式 new T[n] 會產生錯誤,因為這會改為 new Object[n] 。對于開發類庫的人來說,這是一個問題。例如,假設我們需要一個 Person 對象數組。 Stream 接口有一個 toArray 方法可以返回 Object 數組。
6.3.6 變量作用域
1.對 lambda 表達式的理解 lambda 表達式有 3個部分:
1 ) 一個代碼塊;
2 ) 參數;
3 ) 自由變量的值, 這是指非參數而且不在代碼中定義的變量。
2.表示 lambda 表達式的數據結構必須存儲自由變量的值,(如: 可以把一個 lambda 表達式轉換為包含一個方法的對象,這樣自由變量的值就會復制到這個對象的實例變量中。)
3.lambda 表達式可以捕獲外圍作用域中變量的值。 在 Java 中, 要確保所捕獲的值是明確定義的,這里有一個重要的限制。在 lambda 表達式中, 只能引用值不會改變的變量。之所以有這個限制是有原因的。 如果在 lambda 表達式中改變變量, 并發執行多個動作時就會不安全。對于目前為止我們看到的動作不會發生這種情況,不過一般來講,這確實是一個嚴重的問題。另外如果在 lambda 表達式中引用變量, 而這個變量可能外部改變,這也是不合法的。
3.(1)lambda 表達式中捕獲的變量必須實際上是最終變量 ( effectively final)。實際上的最終變量是指, 這個變量初始化之后就不會再為它賦新值。
(2)lambda 表達式的體與嵌套塊有相同的作用域。這里同樣適用命名沖突和遮蔽的有關規則。在 lambda 表達式中聲明與一個局部變量同名的參數或局部變量是不合法的。
(3)在方法中,不能有兩個同名的局部變量, 因此, lambda 表達式中同樣也不能有同名的局部變量。
4.在一個 lambda 表達式中使用 this 關鍵字時, 是指創建這個 lambda 表達式的方法的 this參數。 在 lambda 表達式中, this 的使用并沒有任何特殊之處。lambda 表達式的作用域嵌套在 init 方法中,與出現在這個方法中的其他位置一樣, lambda 表達式中 this 的含義并沒有變化 。
6.3.7 處理lambda表達式
1.使用 lambda 表達式的重點是延遲執行( deferred execution )。 之所以希望以后再執行代碼, 這有很多原因, 如:在一個單獨的線程中運行代碼;多次運行代碼;在算法的適當位置運行代碼 (例如, 排序中的比較操作;)發生某種情況時執行代碼 (如, 點擊了一個按鈕, 數據到達, 等等;)只在必要時才運行代碼。
2.(1)Runnable接口,(2)基本類型的函數式接口:
假設要編寫一個方法來處理滿足某個特定條件的文件。 對此有一個遺留接口 java.io.FileFilter, 不過最好使用標準的Predicate<File> , 只有一當已經有很多有用的方法可以生成 FileFilter 。
注意:(1)大多數標準函數式接口都提供了非抽象方法來生成或合并函數。 例如, Predicate.isEqual(a) 等同于 a::equals, 不過如果 a 為 null 也能正常工作。 已經提供了默認方法 and、or 和 negate 來合并謂詞。 (2)如果設計你自己的接口,其中只有一個抽象方法,可以用 @FunctionalInterface 注解來標記這個接口。 這樣做有兩個優點。 如果你無意中增加了另一個非抽象方法, 編譯器會產生一個錯誤消息。 另外 javadoc 頁里會指出你的接口是一個函數式接口。并不是必須使用注解根據定義, 任何有一個抽象方法的接口都是函數式接口。
6.3.8 再談Comparator
1.Comparator 接口包含很多方便的靜態方法來創建比較器。 這些方法可以用于 lambda 表達式或方法引用。
2.(1)靜態 comparing 方法取一個“ 鍵提取器” 函數, 它將類型 T 映射為一個可比較的類型( 如 String )。 對要比較的對象應用這個函數, 然后對返回的鍵完成比較。
(2)另外, comparing 和 thenComparing 方法都有變體形式,可以避免 int、 long 或 double 值的裝箱。
(3)如果鍵函數可以返回 null, 可能就要用到 nullsFirst 和 nullsLast適配器。 這些靜態方法會修改現有的比較器,從而在遇到 null 值時不會拋出異常, 而是將這個值標記為小于或大于正常值。
3.nullsFirst 方法需要一個比較器, 在這里就是比較兩個字符串的比較器。 naturalOrder 方法可以為任何實現了 Comparable 的類建立一個比較器。
4.靜態 reverseOrder 方法會提供自然順序的逆序。要讓比較器逆序比較, 可以使用 reversed實例方法 。
6.4 內部類
內部類(inner class)是定義在一個類內部的類。外層的類成為外部類(outer class).內部類主要用于事件處理。使用內部類的原因:
內部類方法可以訪問該類定義所在的作用域中的數據, 包括私有的數據。
內部類可以對同一個包中的其他類隱藏起來。
當想要定義一個回調函數且不想編寫大量代碼時,使用匿名 (anonymous) 內部類比較便捷。
6.4.1 使用內部類訪問對象狀態
1.內部類既可以訪問自身的數據域,也可以訪問創建它的外圍類對象的數據域為了能夠運行這個程序, 內部類的對象總有一個隱式引用, 它指向了創建它的外部類對象。 這個引用在內部類的定義中是不可見的。外圍類的引用在構造器中設置。編譯器修改了所有的內部類的構造器, 添加一個外圍類引用的參數。
注意:只有內部類可以是私有類,而常規類只可以具有包可見性,或公有可見性。
6.4.2 內部類的特殊語法規則
1.使用外圍類引用:OuterClass.this
if(TalkingCLock.this.beep) ...
2.編寫內部類對象的構造器:outerObject.new InnerClass(contruction parameters)
ActionListener listener = this.new TimerPrinter();
3.在外圍類的作用域之外引用內部類:
OuterClass.InnerClass
注意: 內部類中聲明的所有靜態域都必須是 final。因為我們希望一個靜態域只有一個實例, 不過對于每個外部對象, 會分別有一個單獨的內部類實例。如果這個域不是 final, 它可能就不是唯一的。
內部類不能有 static 方法。Java 語言規范對這個限制沒有做任何解釋。也可以允許有靜態方法,但只能訪問外圍類的靜態域和方法。
6.4.3 內部類是否有用、必要和安全
1.編譯器為了引用外圍類, 生成了一個附加的實例域 this$0 (名字this$0 是由編譯器合成的,在自己編寫的代碼中不能夠引用它)。另外,還可以看到構造器的TalkingClock(外圍類) 參數 。
由于內部類擁有訪問特權, 所以與常規類比較起來功能更加強大。
注意:假設將 TimePrinter 轉換為一個內部類。在虛擬機中不存在私有類, 因此編譯器將會利用私有構造器生成一個包可見的類:private TalkingClock$TimePrinter(TalkingClock);
當然,沒有人可以調用這個構造器, 因此, 存在第二個包可見構造器TalkingClock$TimePrinter(TalkingGock, TalkingClock$1);它將調用第一個構造器。
編譯器將 TalkingClock 類 start 方法中的構造器調用翻譯為:new TalkingClock$TimePrinter(this, null)
6.4.4 局部內部類
1.可以在一個方法中定義局部類,并且不能用public或private訪問說明符進行聲明,它的作用域被限定在聲明這個局部類的塊中。
2.局部類可以對外部世界完全隱藏起來,即使方法所在類中的其他代碼也不能訪問。除了定義它的方法外,沒有任何方法知道它的存在。
3.局部類的另一個優勢:不僅可以訪問包含它們的外部類,還可以訪問局部變量,但那些局部變量必須被聲明為final
public void start(int interval,final boolean beep)
{
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
if(beep)//局部類訪問局部變量
Toolkit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Time t = new Timer(interval,listener);
t.start();
}
4.局部類不能用 public 或 private 訪問說明符進行聲明。它的作用域被限定在聲明這個局部類的塊中。
6.4.5 由外部方法訪問變量
1.與其他內部類相比較, 局部類的優點。它們不僅能夠訪問包含它們的外部類, 還可以訪問局部變量。不過, 那些局部變量必須事實上為 final。這說明, 它們一旦賦值就絕不會改變。
注意:在 JavaSE 8 之前, 必須把從局部類訪問的局部變量聲明為 final。有時, final 限制顯得并不太方便。 補救的方法是使用一個長度為 1 的數組
在內部類被首次提出時, 原型編譯器對內部類中修改的局部變量自動地進行轉換。不過, 后來這種做法被廢棄。。同時在多個線程中執行內部類中的代碼時, 這種并發更新會導致競態條件
6.4.6 匿名內部類
1.將局部內部類的使用再深人一步。 假如只創建這個類的一個對象,就不必命名了。這種類被稱為匿名內部類(anonymous inner class)。
2.由于構造器的名字必須與類名相同, 而匿名類沒有類名, 所以, 匿名類不能有構造器。取而代之的是,將構造器參數傳遞給超類 ( superclass) 構造器。尤其是在內部類實現接口的時候, 不能有任何構造參數。如果構造參數的閉小括號后面跟一個開大括號, 正在定義的就是匿名內部類 。
注意:建立一個與超類大體類似(但不完全相同)的匿名子類通常會很方便。不過, 對于 equals 方法要特別當心。
3.SuperType可以是接口,內部類就要實現這個接口;也可以是一個類,內部類就要擴展它。
new SuperType(construction parameters)
{
inner class methods and data
}
new InterfaceType()
{
methods and data
}
public void start(int interval, final boolean beep)
{
//創建一個實現AL接口的類的新對象
ActionListener listener = new ActionListener()
{
...
}
...
}
6.4.7 靜態內部類
1.有時使用內部類只是為了把一個類隱藏在另外一個類的內部,并不需要內部類引用外圍類對象。為此,可以將內部類聲明為 static, 以便取消產生的引用。 只有內部類可以聲明為 static。靜態內部類的對象除了沒有對生成它的外圍類對象的引用特權外, 與其他所冇內部類完全一樣。在我們列舉的示例中, 必須使用靜態內部類,這是由于內部類對象是在靜態方法中構造的
注意:(1)在內部類不需要訪問外圍類對象的時候, 應該使用靜態內部類。 有些程序員用嵌套類 (nested class ) 表示靜態內部類
(2)與常規內部類不同,靜態內部類可以有靜態域和方法。
(3)聲明在接口中的內部類自動成為 static 和 public 類
6.5 代理
代理 ( proxy)。 利用代理可以在運行時創建一個實現了一組給定接口的新類 : 這種功能只有在編譯時無法確定需要實現哪個接口時才有必要使用。
6.5.1 何時使用代理
1.假設有一個表示接口的 Class 對象(有可能只包含一個接口,) 它的確切類型在編譯時無法知道。這確實有些難度。要想構造一個實現這些接口的類, 就需要使用 newlnstance 方法或反射找出這個類的構造器。但是, 不能實例化一個接口,需要在程序處于運行狀態時定義一個新類。為了解決這個問題, 有些程序將會生成代碼;將這些代碼放置在一個文件中;調用編譯器;然后再加載結果類文件。很自然, 這樣做的速度會比較慢,并且需要將編譯器與程序放在一起。而代理機制則是一種更好的解決方案。代理類可以在運行時創建全新的類。這樣的代理類能夠實現指定的接口。
2、不能在運行時定義這些方法的新代碼。而是要提供一個調用處理器(invocation handler)。調用處理器是實現了 InvocationHandler 接口的類對象。在這個接口中只有一個方法:
Object invoke(Object proxy, Method method, Object[] args)
無論何時調用代理對象的方法, 調用處理器的 invoke 方法都會被調用, 并向其傳遞Method 對象和原始的調用參數。 調用處理器必須給出處理調用的方式。
6.5.2 創建代理對象
1.創建一個代理對象, 需要使用 Proxy 類的 newProxylnstance 方法。 這個方法有三個參數:
一個類加載器(class loader)。作為 Java 安全模型的一部分, 對于系統類和從因特網上下載下來的類,可以使用不同的類加載器。有關類加載器的詳細內容將在卷 II 第 9章中討論。目前, 用 null 表示使用默認的類加載器。
一個 Class 對象數組, 每個元素都是需要實現的接口。
一個調用處理器。
還有兩個需要解決的問題。 如何定義一個處理器? 能夠用結果代理對象做些什么?
(1)路由對遠程服務器的方法調用。(2)在程序運行期間,將用戶接口事件與動作關聯起來。(3)為調試, 跟蹤方法調用 。
注意: Integer 類實際上實現了 Comparable<Integer>。 然而, 在運行時, 所有的泛型類都被取消, 代理將它們構造為原 Comparable 類的類對象。
6.5.3 代理類的特性
1. 代理類是在程序運行過程中創建的。 然而, 一旦被創建, 就變成了常規類, 與虛擬機中的任何其他類沒有什么區別。
2.(1)所有的代理類都擴展于 Proxy 類。一個代理類只有一個實例域—調用處理器,它定義在 Proxy 的超類中。 為了履行代理對象的職責, 所需要的任何附加數據都必須存儲在調用處理器中。
(2)所有的代理類都覆蓋了 Object 類中的方法 toString、 equals 和 hashCode。如同所有的代理方法一樣,這些方法僅僅調用了調用處理器的 invoke。Object 類中的其他方法沒有被重新定義。
(3)沒有定義代理類的名字,Sun 虛擬機中的 Proxy類將生成一個以字符串 $Proxy 開頭的類名。
3.對于特定的類加載器和預設的一組接口來說, 只能有一個代理類。 也就是說, 如果使用同一個類加載器和接口數組調用兩次 newProxylnstance 方法的話, 那么只能夠得到同一個類的兩個對象,也可以利用 getProxyClass方法獲得這個類
4.代理類一定是 public 和 final。 如果代理類實現的所有接口都是 public, 代理類就不屬于某個特定的包;否則, 所有非公有的接口都必須屬于同一個包,同時,代理類也屬于這個包。可以通過調用 Proxy 類中的 isProxyClass 方法檢測一個特定的 Class 對象是否代表一個代理類。
【API】java.Iang.reflect.InvocationHandler 1.3 :
Object invoke(Object proxy,Method method,0bject[] args) 定義了代理對象調用方法時希望執行的動作。
【API】 java.Iang.reflect.Proxy 1.3 :
static Class<?> getProxyClass(Cl assLoader loader, Class<?>... interfaces) 返回實現指定接口的代理類。
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler) 構造實現指定接口的代理類的一個新實例。所有方法會調用給定處理器對象的 invoke 方法。
static boolean isProxyClass(Class<?> cl)
如果 cl 是一個代理類則返回 true
第二部分:實驗部分
實驗1: 導入第6章示例程序,測試程序并進行代碼注釋。
測試程序1:6-1,6-2代碼如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package interfaces;import java.util.*;/** * This program demonstrates the use of the Comparable interface. * @version 1.30 2004-02-27 * @author Cay Horstmann */public class EmployeeSortTest //EmployeeSortTest關聯Employee;{ public static void main(String[] args) { var staff = new Employee[3]; //局部對象數組; staff[0] = new Employee("Harry Hacker", 35000); staff[1] = new Employee("Carl Cracker", 75000); staff[2] = new Employee("Tony Tester", 38000); Arrays.sort(staff); //進行排序; // print out information about all Employee objects for (Employee e : staff) System.out.println("name=" + e.getName() + ",salary=" + e.getSalary()); }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
package interfaces;public class Employee implements Comparable<Employee>//Employee實現JDK內置接口Comparable{ private String name; private double salary; //構造方法 public Employee(String name, double salary) { this.name = name; this.salary = salary; } //訪問器 public String getName() { return name; } public double getSalary() { return salary; } //調用方法 public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } /** * Compares employees by salary * @param other another Employee object * @return a negative value if this employee has a lower salary than * otherObject, 0 if the salaries are the same, a positive value otherwise */ public int compareTo(Employee other) { return Double.compare(salary, other.salary);//靜態Double.compare方法 }} |
運行截圖:

1.接口的實現
接口在定義后,就可以在類中實現該接口。在類中實現接口可以使用關鍵字implements,其基本格式如下:
[修飾符] class <類名> [extends 父類名] [implements 接口列表]{
}
修飾符:可選參數,用于指定類的訪問權限,可選值為public、abstract和final。
類名:必選參數,用于指定類的名稱,類名必須是合法的Java標識符。一般情況下,要求首字母大寫。
extends 父類名:可選參數,用于指定要定義的類繼承于哪個父類。當使用extends關鍵字時,父類名為必選參數。
implements 接口列表:可選參數,用于指定該類實現的是哪些接口。當使用implements關鍵字時,接口列表為必選參數。當接口列表中存在多個接口名時,各個接口名之間使用逗號分隔。
在類中實現接口時,方法的名字、返回值類型、參數的個數及類型必須與接口中的完全一致,并且必須實現接口中的所有方法。
2.Comparable 是排序接口。
(1)若一個類實現了Comparable接口,就意味著“該類支持排序”。 即然實現Comparable接口的類支持排序,假設現在存在“實現Comparable接口的類的對象的List列表(或數組)”,則該List列表(或數組)可以通過 Collections.sort(或 Arrays.sort)進行排序。
此外,“實現Comparable接口的類的對象”可以用作“有序映射(如TreeMap)”中的鍵或“有序集合(TreeSet)”中的元素,而不需要指定比較器
(2)Comparable 接口僅僅只包括一個函數,它的定義如下
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
Comparable 可以讓實現它的類的對象進行比較,具體的比較規則是按照 compareTo 方法中的規則進行。這種順序稱為 自然順序。
(3)compareTo 方法的返回值有三種情況:
e1.compareTo(e2) > 0 即 e1 > e2
e1.compareTo(e2) = 0 即 e1 = e2
e1.compareTo(e2) < 0 即 e1 < e2
注意:
1.由于 null 不是一個類,也不是一個對象,因此在重寫 compareTo 方法時應該注意 e.compareTo(null) 的情況,即使 e.equals(null) 返回 false,compareTo 方法也應該主動拋出一個空指針異常 NullPointerException。
2.Comparable 實現類重寫 compareTo 方法時一般要求 e1.compareTo(e2) == 0 的結果要和 e1.equals(e2) 一致。將來使用 SortedSet 等根據類的自然排序進行排序的集合容器時保證保存的數據的順序和想象中一致。
測試程序二:
l編輯、編譯、調試以下程序,結合程序運行結果理解程序;代碼如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package test;interface A{ double g=9.8; void show( );}class C implements A{ public void show( ) {System.out.println("g="+g);}}public class InterfaceTest{ public static void main(String[ ] args) { A a=new C( ); a.show( ); System.out.println("g="+C.g); }} |
運行截圖:

測試程序三:
6-3代碼如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
package timer;/** @version 1.02 2017-12-14 @author Cay Horstmann*/import java.awt.*;import java.awt.event.*;import java.time.*;import javax.swing.*;public class TimerTest{ public static void main(String[] args) { var listener = new TimePrinter(); //創建類對象; // construct a timer that calls the listener // once every second //時間間隔為10秒; var timer = new Timer(1000, listener); //創建Timer類對象; timer.start(); // keep program running until the user selects "OK" //顯示一個包含一條消息和OK按鈕的對話框; JOptionPane.showMessageDialog(null, "Quit program?"); //parent為null時對話框顯示在屏幕的中央; System.exit(0); }}class TimePrinter implements ActionListener //接口定義在implement包中;{ public void actionPerformed(ActionEvent event)//入口參數為ActionEvent event; { System.out.println("At the tone, the time is " + Instant.ofEpochMilli(event.getWhen())); Toolkit.getDefaultToolkit().beep(); //工具箱包含有關GUI環境的信息; }} |
運行截圖:

經測試可知:
回調
在A類中調用B類的C方法,然后B類調用A類中的D方法。方法D被稱為回調方法。回調是實現異步的基礎。經典的回調方式如下:
Class A實現回調接口CallBack——背景1
class A中包含一個class B的引用b ——背景2
class B有一個參數為callback的方法f(CallBack callback) ——背景3
A的對象a調用B的方法 f(CallBack callback) ——A類調用B類的某個方法 C
然后b就可以在f(CallBack callback)方法中調用A的方法 ——B類調用A類的某個方法D
測試程序4
6-4,6-5代碼如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package clone;/** * This program demonstrates cloning. * @version 1.11 2018-03-16 * @author Cay Horstmann */public class CloneTest{ public static void main(String[] args) throws CloneNotSupportedException { var original = new Employee("John Q. Public", 50000); original.setHireDay(2000, 1, 1); Employee copy = original.clone(); //新對象copy初始狀態與original相同,之后會有各自不同的狀態; copy.raiseSalary(10); copy.setHireDay(2002, 12, 31); System.out.println("original=" + original); System.out.println("copy=" + copy); }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
package clone;import java.util.Date;import java.util.GregorianCalendar;public class Employee implements Cloneable{ private String name; private double salary; private Date hireDay; public Employee(String name, double salary) { this.name = name; this.salary = salary; hireDay = new Date(); } public Employee clone() throws CloneNotSupportedException //重新定義clone為public,創建深拷貝的clone的方法; { // call Object.clone() // 創建深拷貝的clone方法; Employee cloned = (Employee) super.clone(); // clone mutable fields //克隆可變的字段, cloned.hireDay = (Date) hireDay.clone(); return cloned; } /** * Set the hire day to a given date. * @param year the year of the hire day * @param month the month of the hire day * @param day the day of the hire day */ public void setHireDay(int year, int month, int day) { Date newHireDay = new GregorianCalendar(year, month - 1, day).getTime(); // example of instance field mutation 實例字段突變的例子; hireDay.setTime(newHireDay.getTime()); } public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } public String toString() { return "Employee[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]"; }} |
運行截圖:

經測試可知:
實現克隆:
兩種不同的克隆方法,淺克隆(ShallowClone)和深克隆(DeepClone)。
在Java語言中,數據類型分為值類型(基本數據類型)和引用類型,值類型包括int、double、byte、boolean、char等簡單數據類型,引用類型包括類、接口、數組等復雜類型。淺克隆和深克隆的主要區別在于是否支持引用類型的成員變量的復制。
有兩種方式:
1). 實現Cloneable接口并重寫Object類中的clone()方法;
2). 實現Serializable接口,通過對象的序列化和反序列化實現克隆,可以實現真正的深度克隆;
實現clone方法的步驟
(1)實現Cloneable接口
(2)重載Object類中的clone()方法,重載時需定義為public
(3)在重載方法中,調用super.clone()
解釋:
(1)clone()方法是定義在java.lang.Object類中,該方法是一個protected的方法,所以重載時要把clone()方法的屬性設置為public,這樣其它類才能調用這個clone類的clone()方法
(2)實現Cloneable接口:Cloneable接口是不包含任何方法的!其實這個接口僅僅是一個標志,而且這個標志也僅僅是針對Object類中clone()方法的,如果clone類沒有實現Cloneable接口,并調用了Object的clone()方法(也就是調用了super.Clone()方法),那么Object的clone()方法就會拋出 CloneNotSupportedException異常。
區別:
淺拷貝和深拷貝的區別:
淺拷貝
對一個已知對象進行拷貝,編譯系統會自動調用一種構造函數——拷貝構造函數,如果用戶未定義拷貝構造函數,則會調用默認拷貝構造函數,調用一次構造函數,調用兩次析構函數,兩個對象的指針成員所指內存相同,但是程序結束時該內存卻被釋放了兩次,會造成內存泄漏問題。
深拷貝
在對含有指針成員的對象進行拷貝時,必須要自己定義拷貝構造函數,使拷貝后的對象指針成員有自己的內存空間,即進行深拷貝,這樣就避免了內存泄漏發生,調用一次構造函數,一次自定義拷貝構造函數,兩次析構函數。兩個對象的指針成員所指內存不同。
總結:淺拷貝只是對指針的拷貝,拷貝后兩個指針指向同一個內存空間,深拷貝不但對指針進行拷貝,而且對指針指向的內容進行拷貝,經深拷貝后的指針是指向兩個不同地址的指針。
實驗2: 導入第6章示例程序6-6,學習Lambda表達式用法。
6-6代碼如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
package lambda;import java.util.*;import javax.swing.*;import javax.swing.Timer;/** * This program demonstrates the use of lambda expressions. * @version 1.0 2015-05-12 * @author Cay Horstmann */public class LambdaTest{ public static void main(String[] args) { var planets = new String[] { "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune" }; //定義數組plants; System.out.println(Arrays.toString(planets)); System.out.println("Sorted in dictionary order:"); Arrays.sort(planets);//Arrays.sort方法接受Lambda類的對象; System.out.println(Arrays.toString(planets)); System.out.println("Sorted by length:"); Arrays.sort(planets, (first, second) -> first.length() - second.length());//檢查一個字符串是否比另一個短; System.out.println(Arrays.toString(planets));//提供lanbda表達式在底層,Arrays.sort方法會接收實現Comparator<string>某各類的對象; var timer = new Timer(1000, event -> System.out.println("The time is " + new Date()));//用已有的方法完成要傳遞到其他代碼的某個動作; timer.start(); // keep program running until user selects "OK" JOptionPane.showMessageDialog(null, "Quit program?"); //保持程序運行,直到用戶選擇“OK" System.exit(0); }} |
運行截圖:

經測試可知:
1.lambda 表達式的語法格式如下:
(parameters) -> expression或(parameters) ->{ statements; }
以下是lambda表達式的重要特征:
可選類型聲明:不需要聲明參數類型,編譯器可以統一識別參數值。
可選的參數圓括號:一個參數無需定義圓括號,但多個參數需要定義圓括號。
可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
可選的返回關鍵字:如果主體只有一個表達式返回值則編譯器會自動返回值,大括號需要指定明表達式返回了一個數值。
使用 Lambda 表達式需要注意以下兩點:
Lambda 表達式主要用來定義行內執行的方法類型接口,例如,一個簡單方法接口。在上面例子中,我們使用各種類型的Lambda表達式來定義MathOperation接口的方法。然后我們定義了sayMessage的執行。
Lambda 表達式免去了使用匿名方法的麻煩,并且給予Java簡單但是強大的函數化的編程能力。
變量作用域
lambda 表達式只能引用標記了 final 的外層局部變量,這就是說不能在 lambda 內部修改定義在域外的局部變量,否則會編譯錯誤。
實驗3: 編程練習
l 編制一個程序,將身份證號.txt 中的信息讀入到內存中;
l 按姓名字典序輸出人員信息;
l 查詢最大年齡的人員信息;
l 查詢最小年齡人員信息;
l 輸入你的年齡,查詢身份證號.txt中年齡與你最近人的姓名、身份證號、年齡、性別和出生地;
l 查詢人員中是否有你的同鄉。
實驗代碼如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
package ID;import java.io.BufferedReader;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayList;import java.util.Arrays;import java.util.Scanner;import java.util.Collections;//對集合進行排序、查找、修改等;public class Main { private static ArrayList<Citizen> citizenlist; public static void main(String[] args) { citizenlist = new ArrayList<>(); Scanner scanner = new Scanner(System.in); File file = new File("D:/java/身份證號.txt"); //異常捕獲 try { FileInputStream fis = new FileInputStream(file); BufferedReader in = new BufferedReader(new InputStreamReader(fis)); String temp = null; while ((temp = in.readLine()) != null) { Scanner linescanner = new Scanner(temp); linescanner.useDelimiter(" "); String name = linescanner.next(); String id = linescanner.next(); String sex = linescanner.next(); String age = linescanner.next(); String birthplace = linescanner.nextLine(); Citizen citizen = new Citizen(); citizen.setName(name); citizen.setId(id); citizen.setSex(sex); // 將字符串轉換成10進制數 int ag = Integer.parseInt(age); citizen.setage(ag); citizen.setBirthplace(birthplace); citizenlist.add(citizen); } } catch (FileNotFoundException e) { System.out.println("信息文件找不到"); e.printStackTrace(); } catch (IOException e) { System.out.println("信息文件讀取錯誤"); e.printStackTrace(); } boolean isTrue = true; while (isTrue) { System.out.println("1.按姓名字典序輸出人員信息"); System.out.println("2.查詢最大年齡的人員信息、查詢最小年齡人員信息"); System.out.println("3.查詢人員中是否有你的同鄉"); System.out.println("4.輸入你的年齡,查詢文件中年齡與你最近人的姓名、身份證號、年齡、性別和出生地"); System.out.println("5.退出"); int nextInt = scanner.nextInt(); switch (nextInt) { case 1: Collections.sort(citizenlist); System.out.println(citizenlist.toString()); break; case 2: int max = 0, min = 100; int m, k1 = 0, k2 = 0; for (int i = 1; i < citizenlist.size(); i++) { m = citizenlist.get(i).getage(); if (m > max) { max = m; k1 = i; } if (m < min) { min = m; k2 = i; } } System.out.println("年齡最大:" + citizenlist.get(k1)); System.out.println("年齡最小:" + citizenlist.get(k2)); break; case 3: System.out.println("出生地:"); String find = scanner.next(); String place = find.substring(0, 3); for (int i = 0; i < citizenlist.size(); i++) { if (citizenlist.get(i).getBirthplace().substring(1, 4).equals(place)) System.out.println("出生地" + citizenlist.get(i)); } break; case 4: System.out.println("年齡:"); int yourage = scanner.nextInt(); int near = peer(yourage); int j = yourage - citizenlist.get(near).getage(); System.out.println("" + citizenlist.get(near)); break; case 5: isTrue = false; System.out.println("程序已退出!"); break; default: System.out.println("輸入有誤"); } } } public static int peer(int age) { int flag = 0; int min = 53, j = 0; for (int i = 0; i < citizenlist.size(); i++) { j = citizenlist.get(i).getage() - age; if (j < 0) j = -j; if (j < min) { min = j; flag = i; } } return flag; }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
package ID;public class Citizen implements Comparable<Citizen> { private String name; private String id; private String sex; private int age; private String birthplace; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public int getage() { return age; } public void setage(int age) { this.age = age; } public String getBirthplace() { return birthplace; } public void setBirthplace(String birthplace) { this.birthplace = birthplace; } public int compareTo(Citizen other) { return this.name.compareTo(other.getName()); } public String toString() { return name + "\t" + sex + "\t" + age + "\t" + id + "\t" + birthplace + "\n"; }} |
運行截圖如下:

實驗4:內部類語法驗證實驗
實驗程序1:
l 編輯、調試運行教材246頁-247頁程序6-7,結合程序運行結果理解程序;
l 了解內部類的基本用法
代碼如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
package innerClass;import java.awt.*;import java.awt.event.*;import java.util.*;import javax.swing.*;import javax.swing.Timer;/** * This program demonstrates the use of inner classes. * @version 1.11 2015-05-12 * @author Cay Horstmann */public class InnerClassTest{ public static void main(String[] args) { TalkingClock clock = new TalkingClock(1000, true);//實現了TalkingClock的類對象 clock.start(); // keep program running until user selects "Ok" JOptionPane.showMessageDialog(null, "Quit program?"); System.exit(0);// }}/** * A clock that prints the time in regular intervals. */class TalkingClock{ //聲明屬性 private int interval; private boolean beep; /** * Constructs a talking clock * @param interval the interval between messages (in milliseconds) * @param beep true if the clock should beep */ public TalkingClock(int interval, boolean beep) { this.interval = interval; this.beep = beep; }//構造方法 /** * Starts the clock. */ public void start() { ActionListener listener = new TimePrinter(); Timer t = new Timer(interval, listener); t.start(); } public class TimePrinter implements ActionListener//實現ActionListener的公共類TimePrinter { public void actionPerformed(ActionEvent event) { System.out.println("At the tone, the time is " + new Date()); if (beep) Toolkit.getDefaultToolkit().beep(); } }} |
實驗結果:
在主類中無法直接訪問或者創建內部類
所以內部類存在原因如下:
內部類可以直接訪問外部類的私有域(包括私有屬性和私有方法)
內部類是另外一種封裝(保護性),對外部的其他類隱藏(心臟包在人身體內部)
內部類可以實現Java單繼承的局限。
但是內部類也存在缺點:結構復雜。
創建內部類
在外部類內部創建內部類
內部類 內部類引用=new 內部類();
Inner in=new Inner( ); (可參考上文中不用內部類代碼)
在外部類外部創建內部類
在外部類外部創建非靜態內部類
外部類.內部類 內部類引用 =new 外部類( ).new 內部類( );
Outter.Inner in =new Outter( ).new Inner( );
實驗程序2:
l 編輯、調試運行教材254頁程序6-8,結合程序運行結果理解程序;
了解匿名內部類的用法。
代碼如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
package anonymousInnerClass;import java.awt.*;import java.awt.event.*;import java.util.*;import javax.swing.*;import javax.swing.Timer;/** * This program demonstrates anonymous inner classes. * @version 1.11 2015-05-12 * @author Cay Horstmann */public class AnonymousInnerClassTest{ public static void main(String[] args) { TalkingClock clock = new TalkingClock();//TalkingClock類聲明為私有的 clock.start(1000, true); // keep program running until user selects "Ok" JOptionPane.showMessageDialog(null, "Quit program?"); System.exit(0); }}/** * A clock that prints the time in regular intervals. */class TalkingClock{ /** * Starts the clock. * @param interval the interval between messages (in milliseconds) * @param beep true if the clock should beep */ public void start(int interval, boolean beep) { ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent event) { System.out.println("At the tone, the time is " + new Date()); if (beep) Toolkit.getDefaultToolkit().beep(); //外圍類引用. } }; Timer t = new Timer(interval, listener); t.start(); }} |
實驗結果:

匿名內部類:
(1).匿名內部類是直接使用 new 來生成一個對象的引用;
(2).對于匿名內部類的使用它是存在一個缺陷的,就是它僅能被使用一次,創建匿名內部類時它會立即創建一個該類的實例,
該類的定義會立即消失,所以匿名內部類是不能夠被重復使用;
(3).使用匿名內部類時,我們必須是繼承一個類或者實現一個接口,但是兩者不可兼得,同時也只能繼承一個類或者實現一個接口;
(4).匿名內部類中是不能定義構造函數的,匿名內部類中不能存在任何的靜態成員變量和靜態方法;
(5).匿名內部類中不能存在任何的靜態成員變量和靜態方法,匿名內部類不能是抽象的,它必須要實現繼承的類或者實現的接口的所有抽象方法
(6).匿名內部類初始化:使用構造代碼塊!利用構造代碼塊能夠達到為匿名內部類創建一個構造器的效果
(7).匿名內部類為局部內部類,所以局部內部類的所有限制同樣對匿名內部類生效
實驗程序3:
l 在elipse IDE中調試運行教材257頁-258頁程序6-9,結合程序運行結果理解程序;
了解靜態內部類的用法。
代碼如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
package staticInnerClass;/** * This program demonstrates the use of static inner classes. * @version 1.02 2015-05-12 * @author Cay Horstmann */public class StaticInnerClassTest{ public static void main(String[] args) { double[] d = new double[20]; for (int i = 0; i < d.length; i++) d[i] = 100 * Math.random();//算法 ArrayAlg.Pair p = ArrayAlg.minmax(d); System.out.println("min = " + p.getFirst()); System.out.println("max = " + p.getSecond()); }//訪問器}class ArrayAlg{ /** * A pair of floating-point numbers */ public static class Pair { //聲明私有屬性 private double first; private double second; /** * Constructs a pair from two floating-point numbers * @param f the first number * @param s the second number */ public Pair(double f, double s) { first = f; second = s; } /** * Returns the first number of the pair * @return the first number */ public double getFirst() { return first; } // 訪問器 /** * Returns the second number of the pair * @return the second number */ public double getSecond() { return second; } } /** * Computes both the minimum and the maximum of an array * @param values an array of floating-point numbers * @return a pair whose first element is the minimum and whose second element * is the maximum */ public static Pair minmax(double[] values) { double min = Double.POSITIVE_INFINITY; double max = Double.NEGATIVE_INFINITY;//變量 for (double v : values) { if (min > v) min = v; if (max < v) max = v; } return new Pair(min, max); }} |
實驗結果:

靜態內部類:在定義內部類的時候,可以在其前面加上一個權限修飾符static。此時這個內部類就變為了靜態內部類。
通常稱為嵌套類,當內部類是static時,意味著:
[1]要創建嵌套類的對象,并不需要其外圍類的對象;
[2]不能從嵌套類的對象中訪問非靜態的外圍類對象(不能夠從靜態內部類的對象中訪問外部類的非靜態成員);
嵌套類與普通的內部類還有一個區別:普通內部類的字段的字段與方法,只能放在類的外部層次上,所以普通的內部類不能有static數據和static字段,也不能包含嵌套類。但是在嵌套類里可以包含所有這些東西。也就是說,在非靜態內部類中不可以聲明靜態成員,只有將某個內部類修飾為靜態類,然后才能夠在這個類中定義靜態的成員變量與成員方法。
另外,在創建靜態內部類時不需要將靜態內部類的實例綁定在外部類的實例上。普通非靜態內部類的對象是依附在外部類對象之中的,要在一個外部類中定義一個靜態的內部類,不需要利用關鍵字new來創建內部類的實例。靜態類和方法只屬于類本身,并不屬于該類的對象,更不屬于其他外部類的對象。
實驗總結:(10分)
在這一章學習了接口定義方法,了解和熟悉了接口的使用方法,Lambda表達式語法,Comparator接口用法,還能夠區分淺拷貝和深拷貝的異同之處,最后基本了解了內部類的定義和使用。

浙公網安備 33010602011771號