1. 程式人生 > >Java核心技術卷一 4. java接口、lambda、內部類和代理

Java核心技術卷一 4. java接口、lambda、內部類和代理

arc pau 代理 需求 targe emp 服務 cat 我們

接口

接口概念

接口不是類,而是對類的一組需求描述,這些類要遵從接口描述的統一格式進行定義。

如果類遵從某個特定接口,那麽久履行這項服務。

public interface Comparable<T>{
    int compareTo(T other);
}

任何實現 Comparable 接口的類都需要包含 compareTo 方法,並且這個方法的參數必須是一個 T 對象,返回一個整形數值。

接口的特點

  • 接口中所有方法自動地屬於 public,所以接口的方法不需要提供關鍵字 public 。
  • 接口中可以定義常量。更多請看接口的特性

接口不能提供的功能

  • 不能含有實例域
  • 不能在接口中實現方法(java 8 之前)

接口與抽象類的區別

  • 可以將接口看成是沒有實例域的抽象類,但還是有一定區別。

讓類實現一個接口的步驟

  1. 將類聲明為實現給定的接口。
  2. 對接口中的所有方法進行定義。
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方法了。

解決默認方法沖突

如果接口中定義了默認方法,然後又在超類或另一個接口中定義了同樣的方法,規則如下:

  1. 超類優先。如果超類提供了一個具體方法,同名而且有相同參數類型的默認方法會被忽略。
  2. 接口沖突。如果一個超接口提供了一個默認方法,另一個接口提供了自個同名而且參數類型相同的方法,必須覆蓋這個方法來解決沖突。

來看看接口沖突的場景:

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 方法來建立一個深拷貝,同時克隆所有子對象。

需要確定:

  1. 默認的 clone 方法是否滿足要求;
  2. 是否可以在可變的子對象上調用 clone 來修補默認的 clone 方法;
  3. 是否不該使用 clone。

選擇 1 或 2,類必須:

  1. 實現 Cloneable 接口;
  2. 重新定義 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)

也可以在方法引用中,使用thissuper參數:

super::instanceMethod
this::instanceMethod

構造引用

Person::newPerson構造器的一個引用,用那個構造器取決於上下文

變量作用域

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 頁

內部類

內部類是定義在另一個類中的類,使用原因:

  1. 內部類方法可以訪問該類定義所在的作用域中的數據,包括私有數據。
  2. 內部類可以對同一個包中的其他類隱藏起來。
  3. 想要定義一個回調函數且不想編寫大量代碼時,使用匿名內部類很便捷。

使用內部類訪問對象狀態

內部類方法可以訪問自身的數據域,也可以訪問創建它的外圍類對象的數據域,包括私有數據。

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、內部類和代理