Java核心技術卷一 4. java接口、lambda、內部類和代理
接口
接口概念
接口不是類,而是對類的一組需求描述,這些類要遵從接口描述的統一格式進行定義。
如果類遵從某個特定接口,那麽久履行這項服務。
public interface Comparable<T>{
int compareTo(T other);
}
任何實現 Comparable 接口的類都需要包含 compareTo 方法,並且這個方法的參數必須是一個 T 對象,返回一個整形數值。
接口的特點:
- 接口中所有方法自動地屬於 public,所以接口的方法不需要提供關鍵字 public 。
- 接口中可以定義常量。更多請看接口的特性
接口不能提供的功能:
- 不能含有實例域
- 不能在接口中實現方法(java 8 之前)
接口與抽象類的區別:
- 可以將接口看成是沒有實例域的抽象類,但還是有一定區別。
讓類實現一個接口的步驟:
- 將類聲明為實現給定的接口。
- 對接口中的所有方法進行定義。
class Employee implements Comparable<Employee> {
public int compareTo(Employee other){
return Double.compare(salary, other.salary);
}
...
}
實現類中的特點:
- 實現方法的方法聲明為 public ,因為接口中的方法都自動地是 public。
- 為泛型 Comparable 接口提供一個類型參數,就可以不使用 Object 類型,使得程序省略了強制裝換的步驟。
當使用 Array.sort() 方法時,必須實現 Comparable 接口方法,並且元素之間必須是可比較的,不然會報異常:
public class ArrayGood {
public static void main(String[] args) {
int[] a = Arrays.copyOf(new int[2], 100);
System.out.println(a.length);//100
Employee[] employees = new Employee[10];
employees = Arrays.copyOf(employees, 100);
int[] aint = {5, 3, 6, 14, 9, 7, 22, 10};
System.out.println(Arrays.toString(aint));
Arrays.sort(aint);
System.out.println(Arrays.toString(aint));
Employee[] employees1 = {new Employee("n")
, new Employee("h")
, new Employee("e")
, new Employee("n")
, new Employee("a")
, new Employee("r")
, new Employee("n")
, new Employee("i")
, new Employee("m")};
Arrays.sort(employees1);//java.lang.ClassCastException: Employee cannot be cast to java.lang.Comparable
}
}
public class Employee {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
Employee(String name){
this.name = name;
}
}
實現接口並實現方法後,Employee 數組調用了排序 sort() 方法,實現了排序:
public class ArrayGood {
public static void main(String[] args) {
int[] a = Arrays.copyOf(new int[2], 100);
System.out.println(a.length);//100
Employee[] employees = new Employee[10];
employees = Arrays.copyOf(employees, 100);
int[] aint = {5, 3, 6, 14, 9, 7, 22, 10};
System.out.println(Arrays.toString(aint));
Arrays.sort(aint);
System.out.println(Arrays.toString(aint));
Employee[] employees1 = {new Employee("n")
, new Employee("h")
, new Employee("e")
, new Employee("n")
, new Employee("a")
, new Employee("r")
, new Employee("n")
, new Employee("i")
, new Employee("m")};
Arrays.sort(employees1);
for (Employee emp:
employees1 ) {
System.out.print(emp.getName() + " ");
}
//a e h i m n n n r
}
}
public class Employee implements Comparable<Employee>{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
Employee(String name){
this.name = name;
}
@Override
public int compareTo(Employee o) {
return name.compareTo(o.name);
//return Double.compare(double x, double y);
//return Integer.compare(int x, int y);
}
}
所以,要讓一個類使用排序服務必須讓它實現 compareTo 方法。這是理所當然的,因為要向 sort 方法提供對象的比較方法。
疑問:為什麽不能在 Employee 類直接提供一個 compareTo 方法,而必須實現 Comparable 接口呢?
解答:原因在於 Java 程序設計語言是一種強類型語言。在調用方法的時候,編譯器將會檢查這個方法是否存在。調用 compareTo 方法時,sort 傳入的 Object 對象會被強制轉換為 Comparable 類型,因為只有一個 Comparable 對象才確保有 compareTo 方法。又因為存在這個強制轉換,所以類必須還實現 Comparable 接口,這樣才可以將 Object 引用的參數轉化為一個 Comparable。
反對稱規則:如果子類之間的比較含義不一樣,那就屬於不同類對象的非法比較。每個 compareTo 方法都應該在開始時進行下列檢測
if (getClass() != other.getClass()) throw new ClassCastException();
接口的特性
接口不是類,不能使用new實例化
可以聲明接口的變量
接口變量必須引用實現了接口的類對象,可以使用instance檢查一個對象是否實現了某個特定的接口
java x = new Comparable(...);//ERROR Comparable x;//OK x = new Employee(...)//Employee實現了接口 if(anObject instanceof Comparable) {...}
- 如果類不實習接口的方法,那麽這個類就要定義為抽象類
```java
public interface Named {
String getName();
}
abstract class Student implements Named{
}
```
- 接口可以被擴展,並且能用 extends 擴展多個接口
```
public interface Moveable{
void move(double x, double y);
}
public interface Powered extends Moveable, Comparable
- 接口不能包含實例域或靜態方法,但可以包含常量,接口中的域自動設為
public static final
public interface Powered extends Moveable{ double milesPerGallon(); double SPEED_LIMIT = 95; }
- 接口只能繼承一個超類,卻可以實現多個接口
class Employee implements Cloneable, Comparable{ ... }
接口與抽象類
疑問:為什麽引用接口概念,為什麽不將 Comparable 直接設計為抽象類。
解答:如果使用抽象類表示通用屬性存在一個問題,每個類只能擴展一個類。每個類卻可以實現多個接口。
沒有多重繼承:許多設計語言允許一個類有多個超類,如 C++。而 Java 沒有多繼承是因為它會讓語言本身變得非常復雜,降低效率。
靜態方法
Java SE 8 中,允許在接口中增加靜態方法。這有違接口作為抽象規範的初衷。
目前的方法(2018)都是將靜態方法放在伴隨類中。標準庫中的接口和工具類,可能只包含一些工廠方法。
如 Path 接口定義了 Paths 類中的工廠方法,這樣一來,Paths 類就不再是必要的了。
默認方法
Java SE 8 中可以為接口方法提供一個默認實現。必須用 default 修飾符標記:
public interface Comparable<T> {
default int compareTo(T oter){
return 0;
}
}
一般情況沒有用處,因為方法會被覆蓋。
但有時一個接口定義了大量的方法,我們又不需要實現這麽多方法,只關心其中一兩個方法。在 Java SE 8 中,可以把所有方法聲明為默認方法,這些默認方法聲明也不做。
public interface MouseListener{
default void moseClicked(MouseEvent event){}
default void mosePressed(MouseEvent event){}
default void moseReleased(MouseEvent event){}
default void moseEntered(MouseEvent event){}
default void moseExited(MouseEvent event){}
}
默認方法可以調用任何其他方法:
public interface Collection{
int size();
default boolean isEmpty(){
retrun size()==0;
}
...
}
public class Test implements Collection{
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.size());//0
System.out.println(test.isEmpty());//true
}
@Override
public int size() {
return 0;
}
}
這樣實現 Collection 的程序員就不用操心實現isEmpty
方法了。
解決默認方法沖突
如果接口中定義了默認方法,然後又在超類或另一個接口中定義了同樣的方法,規則如下:
- 超類優先。如果超類提供了一個具體方法,同名而且有相同參數類型的默認方法會被忽略。
- 接口沖突。如果一個超接口提供了一個默認方法,另一個接口提供了自個同名而且參數類型相同的方法,必須覆蓋這個方法來解決沖突。
來看看接口沖突的場景:
interface Named{
default String getName() {
return getClass().getName() + "_" + hashCode();
}
...
}
interface Person {
default String getName() {
return getClass().getName() + "_" + hashCode();
}
...
}
class Student implements Person, Named {
public String getName() {
return Person.super.getName();
}
}
當 Student 繼承兩個接口時,Java 編譯器會報告一個錯誤,讓程序員重寫有沖突的方法解決二義性。我們使用接口類型.super.接口方法
的方法,在兩個接口中選擇一個方法,解決二義性。
假設 Named 接口沒有為 getName 提供默認實現:
interface Named{
String getName();
}
如果這兩個接口有一個提供了實現,那麽編譯器就會報告錯誤,讓程序員解決二義性。
如果兩個接口沒有提供默認實現,那麽編譯器不會報錯,程序員實現這個方法即可。不實現他們的方法,則類定義為抽象類。
類優先規則
如果類繼承的類和繼承的接口有相同的方法,那麽接口的默認方法都會被忽略。
接口示例
接口與回調
回調:常見的程序設計模式,可以指出某個特定事件發生時應該采取的動作。
下面程序給出了定時器和監聽器的操作行為。定時器啟動以後,程序彈出一個消息對話框,並等待用戶點擊 OK 按鈕來終止程序的執行。在程序等待用戶操作的同時,每隔 10 秒顯示一次當前的時間。
public class TimerTest {
public static void main(String[] args) {
ActionListener listener = new TimePrinter();
Timer t = new Timer(10000, listener);
t.start();
JOptionPane.showMessageDialog(null, "Quit program?");
t.stop();
}
static class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is" + new Date());
}
}
}
Comparator 接口
Arrays.sort 的用比較器作為參數,比較器實現了 Comparator 接口的類的實例:
public interface Comparator<T>{
int compare(T first, T second);
}
如果要比較字符串:
class LengthComparator implements Comparator<String>{
public int compare(String first, String second){
return first.compareTo(second);
//return first.length() - second.length();
}
}
具體完成比較時,需要建立一個實例:
LengthComparator comp = new LengthComparator();
if (comp.compare(words[i], words[j]) > 0)) ...
與words[i].compareTo(words[j])
比較,compare 方法要在比較器對象上調用,而不是在字符串本身上調用。
要對一個數組排序,需要為 Arrays.sort 方法傳入一個 LengthComparator 對象:
String[] friends = {"Peter", "Paul", "Mary"};
Arrays.sort(friends, new LengthComparator());
對象克隆
討論 Cloneable 接口,它指示一個類提供一個安全的 clone 方法。克隆不太常見,細節技術性強,不做深入討論。
首先,回憶一個包含對象引用的變量建立副本時會發生什麽。原變量和副本都是同一個對象的引用:
Employee original = new Employee("John Public", 50000);
Employee copy = original;
copy.raiseSalary(10);//original 也會改變
說明,引用相同的任何一個變量改變都會影響另一個變量。
想要讓 copy 的初始狀態與 original 相同,但之後他們各自又會有不同的狀態,這種情況就要使用 clone 方法:
Employee copy = original.clone();
copy.raiseSalary(10);//original 沒有改變
這種拷貝基於 Object 類的 clone 方法屬於淺拷貝,如果拷貝的對象的域有子對象,他們之間還是會有聯系,改變子對象時,被淺拷貝的對象的子對象也會改變。
通常子對象都是可變的,必須重新定義 clone 方法來建立一個深拷貝,同時克隆所有子對象。
需要確定:
- 默認的 clone 方法是否滿足要求;
- 是否可以在可變的子對象上調用 clone 來修補默認的 clone 方法;
- 是否不該使用 clone。
選擇 1 或 2,類必須:
- 實現 Cloneable 接口;
- 重新定義 clone 方法,並指定 public 訪問修飾符。
Cloneable接口只是個標簽接口,不含任何需要實現的方法:
public class Employee implements Cloneable {
public Employee clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();//對象
cloned.hireDay = (Date) hireDay.clone();//對象子對象
return cloned;
}
}
調用super.clone()
得到的是當前調用類的副本,而不是父類的副本。根本沒有必用調用this.clone()
,並且也用不了 this.clone()
因為這裏已經重寫了。
lambda 表達式
lambda 的好處
lambda 表達式是一個可傳遞的代碼塊,可以在以後執行一次或多次。
有些地方,要將一個代碼塊傳遞到某個對象,這個代碼塊會在將來某個時間調用。在 Java 中傳遞一個代碼段並不容易,不能直接傳遞代碼段。Java 是一種面向對象語言,所以必須構造一個對象,這個對象的類型需要有一個方法能包含所需的代碼。
Java SE 8 有了好方法來處理代碼塊。
lambda 表達式的語法
簡單的 lambda 表達式:
(String first, String second)
-> first.length() - second.length()
也可以像寫方法一樣,把這些代碼放在{}
中,並包含顯式的return
語句:
(String first, String second) -> {
if(first.length() < second.length()) return -1;
else if(first.length() > second.length()) return 1;
else return 0;
}
即使沒有參數,任然要提供空括號:
() -> {
for (int i = 100; i >= 0; i--){
System.out.println(i);
}
}
如果可以推導出一個 lambda 表達式的參數類型,則可以忽略其類型:
Comparator<String> comp = (first, second) ->
first.length() - second.length();
//編譯器可以推導出 first 和 second 必然是字符串,
//因為 lambda 表達式將賦給一個字符串比較器。
如果方法只有一個參數,並且類型可以推出省略,那麽可以省略參數括號:
ActionListener listener = event ->
System.out.println("The time is " + new Date());
無需指定 lambda 表達式的返回類型。它的返回類型會有上下文推到得出。
如果一個 lambda 表達式在一個分支返回一個值,另外一些分支不返回值,這是不合法的:
(int x) -> {
if(x >= 0) return 1;//不合法
}
函數式接口
對於只有一個抽象方法的接口,需要這種接口的對象時,可以提供一個 lambda 表達式,這種接口成為函數式接口。
Arrays.sort(words,
(first, second) -> first.length() - second.length());
lambda 表達式可以轉換為接口:
Timer t = new Timer(1000, event -> {
System.out.println("At the tone, the time is " + new Date());
Toolkit.getDefaultToolkit().beep();
})
java 沒有增強函數類型,所以不能聲明函數類型。
方法引用
可能已經有現成的方法而已完成要傳遞到其他代碼的某個動作:
Timer t = new Timer(1000, event -> System.out.println(event));
Timer t = new Timer(1000, System.out::println);
System.out::println
是一個方法引用,它們是等價的。
主要有3種情況:
- object::instanceMethod 如,
System.out::println
等價x -> System.out.println(x)
- Class::staticMethod 如,
Math::pow
等價(x, y) -> Math.pow(x, y)
- Class::instanceMethod 如,
String::compareTolgnoreCase
等價(x, y) -> x.compareTolgnoreCase(y)
也可以在方法引用中,使用this
和super
參數:
super::instanceMethod
this::instanceMethod
構造引用
Person::new
是Person
構造器的一個引用,用那個構造器取決於上下文
變量作用域
lambda 表達式可以捕獲外圍作用域中變量的值,lambda 表達式中只能引用變量的值而不能改變變量的值。另外在 lambda 表達式中引用變量,而這個變量可能在外部改變,這也是不合法的。
規則:lambda 表達式中捕獲的變量必須實際上是最終變量。
lambda 表達式的體與嵌套塊有相同的作用域。
lambda 表達式中的 this 關鍵字,指創建這個 lambda 表達式的方法的 this 參數。
處理 lambda 表達式
使用 lambda 表達式重點是延遲執行。
repeat(10, () -> System.out.println("Hello, World!"));
public static void repeat(int n, Runnable action){
for(int i = 0; i < n; i++) action.run();
}
常用的函數式接口:
略 240 頁
內部類
內部類是定義在另一個類中的類,使用原因:
- 內部類方法可以訪問該類定義所在的作用域中的數據,包括私有數據。
- 內部類可以對同一個包中的其他類隱藏起來。
- 想要定義一個回調函數且不想編寫大量代碼時,使用匿名內部類很便捷。
使用內部類訪問對象狀態
內部類方法可以訪問自身的數據域,也可以訪問創建它的外圍類對象的數據域,包括私有數據。
public class Outer {
int num = 10;
class Inner{
int count = 20;
public void print(){
System.out.println("直接訪問外部類屬性"+num);
}
}
public void show(){
System.out.println("外部類。。。");
System.out.println("在外部類訪問內部類屬性" + new Inner().count);
System.out.println("在外部類訪問內部類方法:");
new Inner().print();
}
public static void main(String[] args) {
new Outer().show();
System.out.println();
System.out.println("main。。。");
System.out.println("在mian中訪問內部類屬性" + new Outer().new Inner().count);
System.out.println("在mian中訪問內部類方法:");
new Outer().new Inner().print();
}
}
內部類的特殊語法規則
內部類訪問外部類的復雜形式:Outer.this.屬性
外部類訪問內部類的形式:Inner inner = new Inner()
其他類訪問外部類中的內部類的形式:Outer.Inner inner = new Outer().new Inner()
- 內部類中聲明的靜態域必須是 final
- 內部類不能有 static 方法。也允許有,但只能訪問外圍類的靜態域和方法。
私有內部類
class Out {
private int age = 12;
private class In {
public void print() {
System.out.println(age);
}
}
public void outPrint() {
new In().print();
}
}
public class Demo {
public static void main(String[] args) {
//此方法無效
/*
Out.In in = new Out().new In();
in.print();
*/
Out out = new Out();
out.outPrint();
}
}
如果一個內部類只希望被外部類中的方法操作,那麽可以使用private聲明內部類。
上面的代碼中,我們必須在Out類裏面生成In類的對象進行操作,而無法再使用Out.In in = new Out().new In() 生成內部類的對象。
也就是說,此時的內部類只有外部類可控制;如同是,我的心臟只能由我的身體控制,其他人無法直接訪問它。
局部內部類
局部內部類可以對外界完美的隱藏起來。除了 Print 方法沒人知道這個內部類。
我們將內部類移到了外部類的方法中,然後在外部類的方法中再生成一個內部類對象去調用內部類方法,這就是局部內部類。
class Out {
private int age = 12;
public void Print(final int x) {
class In {
public void inPrint() {
System.out.println(x);
System.out.println(age);
}
}
new In().inPrint();
}
}
public class Demo {
public static void main(String[] args) {
Out out = new Out();
out.Print(3);
}
}
由外部方法訪問變量
局部內部類,不僅能夠訪問包含他們的外部類,還可以訪問局部變量,但必須被聲明為 final。
匿名內部類
加入只創建這個類的一個對象,就不必命名了,這種了被稱為匿名內部類。
類名可以是一個接口,於是內部類就是實現這個接口;也可以是一個類,於是內部類就是對這個類擴展。
abstract class Person {
public abstract void eat();
}
public class Demo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}
- 匿名類沒有類名,所以類名沒有構造器。
- 構造器參數會傳遞給超類構造器。內部類實現接口的時候,不能有任何構造器。
- 構造參數後面加個
{}
就代表是匿名內部類。
靜態內部類
有時,使用內部類知識為了把一個類隱藏在另外一個類的內部,並不需要內部類引用外圍類對象。可以將內部類聲明為 static,以便取消產生的引用。
如果用 static 將內部內靜態化,那麽內部類就只能訪問外部類的靜態成員變量,具有局限性。
其次,因為內部類被靜態化,因此Out.In
可以當做一個整體看,可以直接new
出內部類的對象(通過類名訪問static
,生不生成外部類對象都沒關系)。
class Out {
private static int age = 12;
//靜態內部類
static class In {
public void print() {
System.out.println(age);
}
}
}
public class Demo {
public static void main(String[] args) {
Out.In in = new Out.In();
in.print();
}
}
- 只有內部類可以聲明為 static 。
- 內部類只能訪問外圍類的靜態成員變量,具有局限性。
- 靜態內部類可以有靜態域和方法。
- 聲明在接口中的內部類自動成為
public static
類。
代理
利用代理可以在運行時創建一個實現了一組給定接口的新類。
只有在編譯時無法確定需要實現那個接口時才有必要使用。
何時使用代理
有一個便是接口的 Class 對象,要想構造一個實現這些接口的類,需要使用 newInstance 方法或反射找出這個類的構造器。但是,不能實例化一個接口,需要在程序處於運行狀態時定義一個新類。
代理類可以在運行時創建全新的類。這樣代理類能夠實現指定的接口:
- 指定接口所需要的全部方法。
- Object 類中的全部方法。
但不能運行時定義這些方法的新代碼,而要提供一個調用處理器。
調用處理器是實現了 InvocationHandler 接口的類對象,只有一個方法:
Object invoke(Object proxy, Method method, Object[] args)
創建代理對象
使用 Proxy 類的 newProxyInstance 方法創建一個代理對象,它有三個參數:
- 一個類加載器。可以使用不同的類加載器,null 表示使用默認的類加載器。
- 一個Class對象數組,每個元素都是需要實現的接口。
- 一個調用處理器。現了 InvocationHandler 接口的類對象
使用代理的原因:
- 路由對遠程服務器的方法調用。
- 在程序運行期間,將用戶接口事件與動作關聯起來。
- 為調試、跟蹤方法調用。
使用代理和調用處理器跟蹤方法調用,並且定義了一個 TraceHander 包裝器類存儲包裝的對象。其中的 invoke 方法打印出被調用方法的名字和參數,隨後用包裝好的對象作為隱式參數調用這個方法:
public class TraceHandler implements InvocationHandler{
private Object target;
public TraceHandler(Object t){
target = t;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.print(target);
System.out.print("." + method.getName() + "(");
if (args != null){
for (int i = 0; i < args.length; i++) {
System.out.print(args[i]);
if (i < args.length - 1) System.out.print(",");
}
}
System.out.println(")");
return method.invoke(target, args);
}
public static void main(String[] args) {
Object[] elements = new Object[100];
for (int i = 0; i < elements.length; i++) {
Integer value = i + 1;
InvocationHandler handler = new TraceHandler(value);
Object proxy = Proxy.newProxyInstance(null, new Class[] {Comparable.class}, handler);
elements[i] = proxy;
}
Integer key = new Random().nextInt(elements.length) + 1;//隨機生成一個數 [1, i + 1]
int result = Arrays.binarySearch(elements, key);//二分搜索法來搜索指定數組,以獲得指定對象的位置
if (result >= 0) System.out.println(elements[result]);
}
}
只要 proxy 調用了某個方法,這個方法 method 的名字和參數 args 就會打印出來。
代理的特性
代理類是在程序運行過程中創建的,一旦被創建就變成了常規類,與虛擬機中的任何其他類沒有什麽區別。
代理類都擴展於 Proxy 類。一個代理類只有一個實例域(調用處理器)。任何附加數據都必須存儲在調用處理器中。
代理類都覆蓋了 Object 類中的方法。覆蓋的方法,僅僅調用了調用處理器的 invoke。有些沒有被重寫定義,如 clone 和 getClass。
沒有定義代理類的名字,虛擬機將會自動生成一個類名。
特定的類加載器和預設的一組接口,只能有一個代理類。調用兩次 newProxyInstance 方法也只能得到同一個類的兩個對象,利用 getProxyClass 方法可以獲得這個類:
java Class proxyClass = Proxy.getProxyClass(null, interfaces);
- 代理類一個是 public 和 final。如果代理類實現的接口都是 public ,代理類就不屬於某個特定的包;否則,所有非公有的接口都必須屬於同一個包,同時,代理類也屬於這個包。通過 Proxy 中的 isProxyClass 方法檢測一個特定的 Class 對象是否代表一個代理類。
java System.out.println(Proxy.isProxyClass(elements[3].getClass()));
Java核心技術卷一 4. java接口、lambda、內部類和代理