Java學習總結之抽象類、接口、lambda表達式與內部類

抽象類

在繼承的層次結構中,每個新子類都使類變得越來越明確具體。如果從一個子類追溯到父類,類就會變得更通用和抽象。類的設計應該確保父類包含它子類的共同特征。如果一個父類設計得非常抽象,以至于它沒有任何具體的實例,這樣的類稱為抽象類,使用abstract關鍵字修飾。抽象類定義了相關子類的共同行為。

抽象方法

如果一個方法非常抽象,只定義了方法,沒有提供方法的具體實現,那么我們把它定義為一個抽象方法,它的具體實現由子類提供,即子類覆蓋抽象方法提供方法體。
抽象方法由abstract關鍵字修飾,只有方法頭,沒有花括號和方法體,以分號結尾。比如一個GeometricObject類定義了一個名為getArea的抽象方法,即public abstract double getArea();

幾點說明

1.抽象方法應該定義為public,以便子類進行重寫。
2.抽象類的構造器應該定義為protected,因為抽象類不能通過new直接創建實例,其構造器只被子類調用。創建一個具體子類的實例時,它的父類的構造器被調用以初始化父類中定義的數據域。
3.一個包含抽象方法的類必須定義為抽象類,一個不包含抽象方法的類也可以定義為抽象類(如果不想讓某類創建實例,可以把它定義為抽象類)
4.如果子類繼承抽象類時沒有覆蓋其所有的抽象方法,即子類中仍有抽象方法,子類也應該定義為抽象的
5.抽象方法是非靜態的
6.子類可以覆蓋父類的方法并將它定義為abstract,這種情況很少見,但它在當父類方法實現在子類中變得無效時是很有用的,在這種情況下,子類必須定義為abstract
7.即使子類的父類是具體的,這個子類也可以是抽象的。例如,Object是具體的,但它的子類GeometricObject是抽象的。
8.不能使用new操作符從一個抽象類創建一個實例,但是抽象類可以用作一種數據類型。下面的語句創建一個GeometricObject類型的數組是正確的:GeometricObject[] objects = new GeometricObject[10];然后可以創建一個具體子類的實例并把它的引用賦給數組,如:objects[0] = new Circle();

接口

接口在很多方面都與抽象類很相似,但它的目的是指明相關或者不相關類的多個對象的共同行為,屬性成員都是公共靜態常量,成員方法都是公共抽象非靜態方法。例如,使用正確的接口,可以指明這些對象是可比較的、可克隆的。為了區分接口和類,Java采用Interface關鍵字定義接口。在一個java文件內,只能有一個public類或一個public接口,即public類和public接口不能同文件共存。接口沒有構造器,沒有實例域,也不能使用new操作符創建實例。接口沒有構造器的原因有三點:
1.構造器用于初始化成員變量,接口沒有成員變量,不需要構造器
2.類可以實現多個接口,如果多個接口都有構造方法,不好確定構造方法鏈的調用次序
3.作為高度抽象的概念,接口不能實例化對象,也就不需要構造器

像常規類一樣,每個接口都被編譯為獨立的字節碼文件,可以作為引用變量的數據類型和類型轉換的結果,可以使用instanceof關鍵字等。
類實現接口用implements關鍵字,一個類可以實現多個接口,用逗號隔開即可,一個類必須實現它實現接口的所有方法,否則要定義為抽象類。一個接口可以繼承多個接口,用extends關鍵字,此時實現類需要重寫接口繼承鏈上所有接口的所有抽象方法。如果接口在繼承在多個父接口時,父接口中出現了重名的默認方法沖突,就要在該接口中提供一個同名默認方法來解決沖突。
在定義接口中的數據域和方法時可以簡寫,例如:

public interface T{
    public static final int K = 1;
    public abstract void p();
 }

可簡寫成

public interface T{
    int K = 1;
    void p();
}

要注意接口中所有的數據域都是public static final,所有的方法都是public abstract,在定義接口中允許省略修飾符,但在子類重寫方法時不可缺省public修飾符,否則方法的可見性會縮小為包內可見。
接口只能使用public修飾符或缺省訪問控制修飾符。
如果在具體實現類中定義了和接口中常量同名的常量,那么用接口變量指向實現類引用時變量調用的常量仍然是接口中定義的常量。這是因為常量無法被子類覆蓋。

靜態方法

從Java SE 8開始,允許在接口中增加靜態方法,并給靜態方法提供方法體實現,該靜態方法只能通過接口名.靜態方法來調用。實現語法只要在方法前面加static關鍵字即可,這理論上講是可以的,但這有違于接口作為抽象規范的初衷。靜態方法只能被具體實現類繼承,不能在實現類中重寫。

默認方法

可以為接口方法提供一個默認方法體實現,在方法前加default修飾符即可,這樣子類無需重寫這個方法也能得到一個接口的默認實現。例如:

public interface Collection
{
    int size();
    default boolean isEmpty()
    {
        return size() == 0;
    }
}

這樣實現Collection的程序員就不用操心實現isEmpty方法了。
當然,默認方法也可以被具體實現類重寫。在實現類中調用默認方法要使用接口名.super.默認方法來調用。
默認方法的一個重要用法是“接口演化”。以Collection接口為例,這個接口作為Java的一部分已經很多年了,假設很久以前定義了一個實現Collection接口的類Bag。后來在Collection接口中增加了一個stream方法,假設stream方法不是一個默認方法,那么Bag類將不能編譯,因為它沒有實現這個新方法。如果不重新編譯這個類,而是使用原先包含這個類的JAR文件,這個類仍能正常加載,正常構造實例,但如果在一個Bag實例上調用stream方法,會出現一個AbstractMethodError。但如果把stream方法定義為默認方法就可以解決這個問題,既可以重新編譯也可以使用JAR文件加載類并調用stream方法。

解決默認方法的沖突

如果先在一個接口中將一個方法定義為默認方法,然后又在超類或另一個接口中定義了同樣的方法,會發生沖突。解決沖突規則如下:

  1. 超類和接口沖突。如果超類提供了一個具體方法,那么根據超類優先原則,同名而且有相同參數類型的默認方法會被忽略。
  2. 多接口之間沖突。如果一個實現類實現了多個接口,一個接口提供了一個默認方法,另一個接口提供了一個同名而且參數類型(不論是否是默認參數)相同的方法,此時就發生了接口沖突,必須在實現類中重寫這個方法來解決沖突。

解決重名常量的沖突

1)超類和接口沖突。如果一個類繼承了一個超類和實現了若干接口,此時不像默認方法沖突一樣有超類優先原則。只能通過在實現類中覆蓋該常量來解決沖突。
2)多接口之間沖突。如果一個類實現了多個接口,而這些接口又有重名常量,此時會發生沖突。必須用接口名.常量的方式來精確指明要使用的常量。

Comparable接口

Comparable接口定義了compareTo方法,用于比較對象。當想使用Arrays類的sort方法對對象數組進行排序時,對象所屬的類必須實現了Comparable接口。
Comparable接口是一個帶泛型的接口,定義為:

public interface Comparable<E>{
    public int compareTo(E o);
 }

compareTo應該與equals保持一致,即當且僅當o1.equals(o2)為true時,o1.compareTo(o2) == 0成立。以下是compareTo方法的實現:

class Employee implements Comparable<Employee>{
public int compareTo(Employee other){
   return Double.compare(salary,other.salary);
   }
}

在比較浮點數時可以使用Double的靜態方法compare,這樣就不必擔心溢出或精度損失,類似的還有Integer.compare方法等
繼承過程中的compareTo,如果由子類決定相等的概念,每個compare方法都應該在開始時檢測:if(getClass() != other.getClass()) throw new ClassCastException()如果父類決定相等的概念,應該在超類中提供一個compareTo方法,并將這個方法聲明為final。

Comparator接口

Comparator接口意為"比較器"接口,是一個泛型接口,可用于自定義排序規則和大小比較等。要進行自定義排序,Arrays.sort方法有一個重載版本,需要提供一個數組和一個比較器作為參數,比較器是實現了Comparator接口的類的實例。接口定義為:

public interface Comparator<T>
{
   int compare(T first,T second);
}

如果要按長度比較字符串,由于String是按字典序比較字符串,肯定不能讓String類用兩種方法實現compareTo方法 —— 況且String類也不由我們修改。此時可以定義如下實現Comparator<String>的類:

class lengthComparator implements Comparator<String>
{
   public int compare(String first,String second){
       return first.length() - second.length();
   }
}

因為要調用compare方法,所以具體比較大小和排序時都要創建一個lengthComparator的實例:
大小比較

Comparator<String> comp = new LengthComparator();
if(comp.compare(words[i],words[j]) > 0) ...

自定義排序

String[] friends = {"Peter","Paul","Mary"};
Arrays.sort(friends,new LengthComparator());

Comparable接口和Comparator接口都可以用于自定義排序。比較如下:
1、Comparable接口需要在定義待比較的類的同時實現,比如自定義的類,使用sort的不帶比較器的方法排序。如果類設計者沒有考慮到比較問題而沒有實現 Comparable 接口,此時我們無法修改類的定義,可以在外部定義一個實現了Comparator的比較器,并使用sort帶比較器的方法排序。這種情況下,我們是不需要改變類的。當然也可以在類設計時就實現Comparator接口。
2、在集合中,我們可能需要有多重的排序標準,并在不同情況下靈活切換排序規則,這時候如果使用 Comparable 就有些捉襟見肘了,可以自己繼承 Comparator 提供多種標準的比較器進行排序。

下面對于一個學生類的兩個關鍵字進行排序,先按分數從高到低排序,分數相同按年齡從小到大排序。
方法一:實現Comparable接口
重寫的compareTo方法為:

public int compareTo(Student stu){    
        if(this.score>stu.score){
            return -1 ;
        }else if(this.score < stu.score){
            return 1 ;
        }else{
            if(this.age>stu.age){
                return 1 ;
            }else if(this.age < stu.age){
                return -1 ;
            }else{
                return 0 ;
            }
        }    
    }

方法二:實現Comparator接口
重寫的compare方法為:

 public int compare(Student stu1,Student stu2){    
        if(stu1.score>stu2.score){
            return -1 ;
        }else if(stu1.score<stu2.score){
            return 1 ;
        }else{
            if(stu1.age>stu2.age){
                return 1 ;
            }else if(stu1.age<stu2.age){
                return -1 ;
            }else{
                return 0 ;
            }
        }    
    }

自定義排序總結:無論是重寫compare方法還是compareTo方法,對大于、小于、等于三種情況都要有返回值,否則無法通過編譯。在compareTo方法中,規定 this.xxx > o.xxx 返回 1,this.xxx == o.xxx 返回0,this.xxx < o.xxx 返回-1是升序排列,反之就是降序排列。在compare方法中,規定o1.xxx > o2.xxx返回1,o1.xxx == o2.xxx返回0,o1.xxx < o2.xxx返回 -1是升序排列,反之就是降序排列。
技巧:如果要比較的屬性也實現了Comparable接口,就可以調用它的compareTo方法。如果要降序排列,就交換compareTo的參數順序即可。如果要比較的類是基本數據類型,可以返回差值,如果差值不是int類型,就轉換為int類型。

Cloneable接口

首先,我們考慮為一個包含對象引用的變量建立副本會發生什么,例如:

Employee original = new Employee("John Public",50000);
Employee copy = original;
copy.ratseSalary(10); //original的salary也被改變

原變量和副本都會指向同一個對象,這說明,任何一個變量的改變都會影響到另一個變量。如果有一個對象original,希望創建一個對象copy使得其初始狀態與original相同,但是之后它們各自回有自己不同的狀態,這種情況下就可以使用克隆,例如:

Employee copy = original.clone();
copy.raiseSalary(10); //original的salary不會被改變

Object類中的clone方法將原始對象的每個數據域復制給目標對象,如果一個數據域是基本數據類型,復制的就是它的值,如果是引用類型,復制的就是它的引用,這種克隆稱為淺復制,即original != copy,但original.hireDay == copy.hireDay。這有時是不符合我們要求的,我們不希望在改變某個對象的引用類型的數據域時影響到另一個對象,這時我們需要深復制,即如果數據域是引用類型,復制的是對象的內容而不是引用。
Object類中提供的原始clone方法的方法頭是protected native Object clone() throws CloneNotSupportedException,關鍵字native表明這個方法不是用Java寫的,但它是JVM針對自身平臺實現的。關鍵字protected限定方法只能在同一個包內或在其子類中訪問。由于這個原因:必須在要實現克隆的子類中覆蓋這個方法并把可見性改為public。
無論是淺復制還是深復制,我們都需要實現Cloneable接口,否則即使已經重寫了clone()方法將類的可見性從protected擴大到了public,仍然會拋出一個必檢異常CloneNotSupportedException。Cloneable接口的定義是:

public interface Cloneable{

}

我們發現這個接口是空的,一個帶空體的接口稱為標記接口。一個標記接口既不包括常量也不包括方法,它用來表示一個類擁有的某些特定的屬性,其惟一的作用是允許在類型查詢中使用instanceof關鍵字。但如果一個請求克隆的對象不實現這個接口,會產生CloneNotSupportedException,即使clone的默認(淺拷貝)實現能夠滿足要求,還是要實現這一接口。應該注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一個 protected 方法。Cloneable 接口只是規定,如果一個類沒有實現 Cloneable 接口又調用了 clone() 方法,就會拋出 CloneNotSupportedException。

下面給出一個淺復制的例子:

class Employee implements Cloneable
{
    public Employee clone() throws CloneNotSupportedException
    {
    return (Employee) super.clone();
    }
    . . .
}

下面給出一個深復制的例子:

class Employee implements Cloneable
{
   public Employee clone() throws CloneNotSupportedException
   {
    . . .
    Employee cloned = (Employee) super.clone;
    cloned.hireDay = (Date)hireDay.clone();
    return cloned;
   }
}

我們注意到Object類的clone方法的返回值類型是Object,而Employee類的clone方法返回值類型是Employee,這叫做協變返回類型,即子類在重寫父類方法時可以返回父類返回值類型的子類型。clone方法聲明異常也可以改成捕獲異常,如:

public Employee clone()
{
    try
    {
        Employee cloned = (Employee) super.clone();
        . . .
    }
    catch(CloneNotSupportedException e){ return null;}
 }

復制數組的四種方法

1.申請一個新數組,遍歷原數組逐一復制元素
2.使用System類的靜態方法arraycopy
3.使用數組對象.clone()返回一個數組克隆的引用
4.使用Arrays類的copyOf方法

接口和抽象類

區別:
1.接口所有的變量必須是public static final;抽象類的變量無限制
2.接口沒有構造方法,不能用new操作符實例化;抽象類有構造方法,由子類通過構造方法鏈調用,不能用new操作符實例化
3.接口所有方法必須是公共抽象實例方法(Java SE 8開始允許定義靜態方法),抽象類無限制
4.一個類只可以繼承一個父類,但可以實現多個接口
5.所有的類有一個共同的根Object類,接口沒有共同的根
6.抽象類和子類的關系應該是強的“是一種”關系(strong is-a relationship),而接口和子類的關系是弱的"是一種"關系(weak is-a relationship)。接口比抽象類更靈活,因為實現接口的子類只需要具有統一的行為即可,不需要都屬于同一個類型的類。

接口與回調

回調是一種常見的程序設計模式?;卣{是一種雙向調用模式,也就是說,被調用方在接口被調用時也會調用對方的接口。
見博客:Java回調機制(CallBack)詳解

內部類

內部類,或者稱為嵌套類,是一個定義在另一個類范圍中的類。一個內部類可以如常規類一樣使用。通常,在一個類只被它的外部類所使用的時候,才將它定義為內部類,內部類機制主要用于設計具有互相協作關系的類集合。比如:

//OuterClass.java: inner class demo
public class OuterClass {
   private int data;
   /** A method in the outer class */
   public void m(){
   //Do something
   }
   // An inner class
   class InnerClass {
   /** A method in the inner class */
   public void mi(){
       data++;
       m();
     }
   }
 }

使用內部類的好處:
1.內部類可以很好地實現隱藏:一般的非內部類,是不允許有 private 與protected權限的,但內部類可以。
2.成員內部類擁有外圍類的所有元素的訪問權限:內部類可以訪問包含它的外部類的所有數據域(包括私有數據域)和方法,沒有必要將外部類對象的引用傳遞給內部類的構造方法,內部類有一個指向外部類對象的隱式引用,如果顯式寫出,外部類的引用是OuterClass.this。
3.可以間接實現多重繼承:比如在A類中定義兩個內部類innerClass1和innerClass2分別繼承B類和C類,則A類就具有了B類和C類的屬性和方法,間接實現了多重繼承。
4.減小了類文件編譯后的產生的字節碼文件的大小

內部類具有一下特征:

  1. 一個內部類被編譯成一個名為OuterClassName$InnerClassName的類。例如,一個定義在Test類中的成員內部類A被編譯成Test$A.class 。
  2. 內部類對象通常在外部類中創建,但是你也可以從另外一個類中來創建一個內部類的對象。如果是成員內部類,你必須先創建一個外部類的實例,然后使用下面的語法創建一個內部類對象:OuterClass.InnerClass innerObject = outerObject.new InnerClass(); 如果是靜態內部類的,使用下面語法來創建一個內部類對象:OuterClass.InnerClass innerObject = new OuterClass.InnerClass();。

一般建議在外部類中定義一個用于獲取內部類對象的方法,以便于從外部類外獲取內部類對象,比如:

public InnerClass getInnerClass(){
   return new InnerClass();
}

一個簡單的內部類的用途是將相互依賴的類結合到一個主類中,這樣做減少了源文件的數量(因為非內部類如果用public修飾必須放在不同的源文件中,而內部類可放在同一源文件中),這樣也使得類文件容易組織,因為它們都將主類名作為前綴。另外一個內部類的實際用途是避免類名沖突。

內部類對于定義處理器類非常有用,一個處理器類被設計為針對一個GUI組件創建一個處理器對象(比如,一個按鈕)。處理器類不會被其他應用所共享,所以將它定義在主類里面作為一個內部類使用是恰如其分的。

廣泛意義上的內部類一般來說包括四種:成員內部類局部內部類、匿名內部類靜態內部類。下面就先來了解一下這四種內部類的用法。

成員內部類

成員內部類是最普通的內部類,一個成員內部類可以使用可見性修飾符(public、private、protected、default)所定義,和應用于一個類中成員的可見性規則一樣。
形如下面的形式:

class Circle {
  private double radius = 0;
  public static int count =1;
  public Circle(double radius) {
      this.radius = radius;
  }
   
  class Draw {     //內部類
      public void drawSahpe() {
          System.out.println(radius);  //外部類的private成員
          System.out.println(count);   //外部類的靜態成員
      }
  }
}

這樣看起來,類Draw像是類Circle的一個成員,Circle稱為外部類。成員內部類隱式持有外部類的引用 OuterClass.this ,可以無條件訪問外部類的所有成員屬性和成員方法(包括private成員和靜態成員),但外部類想要訪問內部類的成員屬性和方法時必須先實例化內部類對象。
 不過要注意的是,當成員內部類擁有和外部類同名的成員變量或者方法時,會發生隱藏現象,即默認情況下訪問的是成員內部類的成員。如果要訪問外部類的同名成員,需要顯式通過外部類的引用進行訪問:外部類.this.成員變量 外部類.this.成員方法。
如果要在非外部類的其他類中實例化成員內部類對象,則需要先實例化外部類對象。
即:

OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

注意:成員內部類中不能定義靜態變量和靜態方法,只能定義靜態常量。

靜態內部類

有時候,使用內部類只是為了把一個類隱藏在另外一個類的內部,并不需要內部類引用外圍類的元素。為此,可以為內部類加上static關鍵字聲明為靜態內部類。
靜態內部類不持有外部類的引用,只能訪問外部類的靜態成員變量和方法,而不能訪問外部類的非靜態成員屬性和非靜態方法,如果要調用和訪問,必須實例化外部類對象。當靜態內部類擁有和外部類同名的成員變量或者方法時,會發生隱藏現象,即默認情況下訪問的是靜態內部類的成員。如果要訪問外部類的非靜態同名成員,不能再使用外部類.this.成員的形式,而是要實例化外部類對象。如果要訪問外部類的靜態同名成員,可以通過外部類.成員的方式來訪問。
與常規內部類不同,靜態內部類可以有靜態變量靜態方法??梢酝ㄟ^外部類.內部類.靜態成員方式來訪問。
如果要在非外部類的其他類中實例化靜態內部類對象,不需要先實例化外部類對象:

OuterClass.InnerClass innerObject = new OuterClass.InnerClass();

下面是一個使用靜態內部類的典型例子。考慮一下計算一個數組中最大值和最小值的問題,當然,可以編寫兩個方法,一個計算最大值,一個計算最小值,在調用這兩個方法的時候,數組被遍歷兩次,而如果數組只被遍歷一次就可以計算出最大值和最小值,那么效率就大大提高了。通過一個方法就計算出最大值和最小值:這個方法需要返回兩個數(max 和 min),為此可以定義一個Pair類來封裝這種數據結構,但是Pair是個非常大眾的名字,可能在其他地方定義過,會發生名字沖突,此時可以將Pair定義為ArrayAlg類的內部類ArrayAlg.Pair。又因為Pair沒有必要訪問外圍類ArrayAlg的數據域或方法,應該定義為靜態內部類。
下面給出代碼:


public class ArrayAlg{
   //Pair類,起數據封裝的作用
   public static class Pair{
       private double first;
       private double second;

       public Pair(double f, double s){
           first = f;
           second = s;
       }

       public double getFirst(){
           return first;
       }

       public double getSecond(){
           return second;
       }
   }

   public static Pair maxmin(double[] values){
       double min = Double.POSITIVE_INFNITY;
       double max = Double.NEGATIVE_INFNITY;

       for(double x : values){
           if(x<min) min = x;
           if(x>max) max = x;
       }
       return new Pair(max,min);
   }

   public static void main(String[] args){
       Test te = new Test();
       double[] teArgs = new double[]{2.13,100.0,11.2,34.5,67.1,88.9};
       Pair res = te.maxmin(teArgs);
       System.out.println("max = "+res.getFirst());
       System.out.println("min = "+res.getSecond());
   }
}

特別注意:代碼中的Pair類如果沒有聲明為static,就不能在靜態方法minmax中構造Pair的實例,編譯器會給出錯誤報告:沒有可用的隱式ArrayAlg類型對象初始化內部類對象

局部內部類

可以把內部類定義在一個方法中,稱為局部內部類,也叫方法內部類。局部內部類就像是方法里面的一個局部變量一樣,不能有public、protected、private以及static修飾符。它的作用域被限定在聲明這個局部類的塊中。局部類有一個優勢,即對外部世界完全隱藏起來。即使外部類中的其他代碼也不能訪問它。除了其所在的方法之外,沒有任何方法知道該局部類的存在。局部內部類只能訪問被final修飾的局部變量。
局部內部類被編譯器編譯成一個OuterClassName$1InnerClassName的類。序號逐漸遞增。

class People{
   public People() {
        
   }
}

class Man{
   public Man(){
        
   }
    
   public People getWoman(){
       class Woman extends People{   //局部內部類
           int age =0;
       }
       return new Woman();
   }
}

注意:上述代碼中通過調用getWoman()獲取了局部內部類Woman的引用,不能通過局部內部類引用.屬性的方式來直接訪問局部內部類的成員,所以我們一般會在該方法中直接調用局部內部類的方法進行某種操作,然后返回操作結果。

匿名內部類

有時我們在程序中對一個類只使用一次,此時就可以把類的定義和實例化對象整合在一起,來簡化對于抽象類和接口實現的操作,這就是匿名內部類。
一個匿名內部類是一個沒有名字的內部類,其語法如下:

new SuperClassName/InterfaceName(){
   //implement or override methods in superclass or interface
   
    //Other methods if necessary
 }

其含義是創建一個繼承自SuperClass或實現Interface的類的實例,并在類塊內重寫父類或接口的抽象方法,應該將匿名內部類理解成一個匿名子類的匿名對象,而不是理解成一個類。

匿名內部類有如下特征:
1.沒有可見性修飾符
2.沒有構造方法(因為沒有名字,無法命名構造方法),但可以有構造代碼塊,也可以調用父類的構造方法,即new SuperClassName()調用父類無參構造方法,new SuperClassName(args1,...)調用父類有參構造方法。如果實現的是接口,則不能有任何參數,但是小括號仍然不可缺省
3.必須總是從一個父類繼承或者實現一個接口,但是它不能有顯式的extends或者implements子句
4.必須實現父類或接口中的所有抽象方法
5.一個匿名內部類被編譯成一個名為OuterClassName$n.class的類,例如:如果外部類Test有兩個匿名內部類,分別被編譯成Test$1.classTest$2.class
6.匿名內部類不能訪問外部類方法中的局部變量,除非該變量被聲明為final類型。從jdk1.8開始,如果局部變量被匿名內部類訪問,那么該局部變量相當于自動使用了final修飾。
這樣設計的具體原因見分析:JAVA中匿名內部類訪問的局部變量為什么要用final修飾?

應用一
下面的技巧稱為"雙括號初始化",這里利用了內部類語法。假設你想構造一個數組列表,并將它傳遞到一個方法。

ArrayList<String> friends = new ArrayList<String>();
friends.add("Harry");
friends.add("Tony");
invite(friends);

如果不再需要這個數組列表,最好讓它作為一個匿名列表。語法如下:

invite(new ArrayList<String> 
{
   {
       add("Harry");
       add("Tony");
    }
});

注意這里的雙括號,外括號建立了一個ArrayList的匿名子表,內括號則是一個對象構造塊。

應用二
生成日志或調試消息時,通常希望包含當前類的類名,如:
System.err.println("Something awful happened in " + getClass());
不過這對于靜態方法并不湊效,因為調用getClass()調用的是this.getClass(),但靜態方法里沒有this,所以應該使用下面的表達式:new Object(){}.getClass().getEnclosingClass(),在這里,new Object(){} 會建立Object的一個匿名子類的匿名對象,getEnclosingClass則得到其外圍類,也就是包含這個靜態方法的類

lambda表達式

Lambda表達式(也叫做閉包)是Java 8中最大的也是期待已久的變化。它允許我們將一個函數當作方法的參數(傳遞函數),或者說把代碼當作數據,這是每個函數式編程者熟悉的概念。它是一種表示可以在將來某個時間點執行的代碼塊的簡潔方法。使用lambda表達式,可以用一種精簡的方式表示使用回調或變量行為的代碼。如果要編譯器理解lambda表達式,其代替的匿名內部類實現的接口必須有且僅有一個抽象方法,但是可以有多個非抽象方法,這樣的接口被稱為函數式接口(功能接口、單抽象方法接口)。在底層,接受lambda表達式的方法會接受實現某函數式接口的類的對象,并在這個對象上調用接口的方法,所以可以把lambda表達式賦給函數式接口(lambda表達式實際是一個實現了該函數式接口的類的類型,這里用到了多態),不能把lambda表達式賦給Object變量,因為Object不是一個函數式接口。
一個lambda表達式就是一個代碼塊,以及必須傳入代碼的變量規范。其基礎語法是(expression只有一條語句,不用花括號,也不用分號結尾)

(type1 param1, type2 param2, ...) -> expression

或者(statements是多條語句,要花括號,每條語句之后要分號結尾)

 (type1 param1, type2 param2, ...) -> {statements;}

一個參數的數據類型既可以顯式聲明,也可以由編譯器隱式推斷。如果只有一個參數,并且沒有顯式的數據類型,圓括號可以被省略。如:

e -> {
// Code for processing event e
}

即使lambda表達式沒有參數,也要提供空括號,就像無參數方法一樣:

() -> {for(int i = 100;i >=0 ;i--) System.out.println(i);}

無需指定lambda表達式的返回類型,編譯器會由上下文推斷,例如:

(String first,String second) -> first.length() - second.length()

可以在需要int類型結果的上下文中使用

如果一個lambda表達式只在某些分支上返回一個值,而在另外一些分支不返回值,是不合法的。例如:

(int x) -> {if(x >= 0) return 1;}

Comparator接口是一個函數式接口,可以用lambda表達式實現自定義排序的簡化:

Arrays.sort(words,(first,second) 
-> first.length() - second.length());

函數式接口

對于只有一個抽象方法的接口,需要這種接口的對象時,就可以提供一個lambda表達式,這種接口稱為函數式接口。
如果自己設計了一個函數式接口,可以用@FunctionalInterface注解來標記這個接口,這樣做有兩個好處:
1.可以在你無意中增加一個非抽象方法時產生編譯錯誤
2.javadoc頁里會指出你的接口是一個函數式接口

方法引用

方法引用提供了非常有用的語法,可以直接引用已有Java類或對象(實例)的方法或構造器。與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗余代碼。例如,假設你希望只要出現一個定時器事件就打印這個事件對象,可以調用:

Timer t = new Timer(1000,event -> System.out.println(event));

可以直接把println方法傳遞到Timer的構造器:

Timer t = new Timer(1000,System.out::println);

表達式System.out::println是一個方法引用,它等價于lambda表達式x -> System.out.println(x)
我們再看一個例子,假設要對字符串排序,而不考慮字母的大小寫,可以調用Arrays.sort(strings,String::compareToIgnoreCase);

方法引用主要有三種情況:

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod

對于前兩種情況,方法引用等價于提供方法參數的lambda表達式。比如:System.out::println等價于x -> System.out.println(x),Math::pow等價于(x,y) -> Math.pow(x,y)。第三種情況的第一個參數會稱成為調用方法的目標對象,其余參數成為方法參數,比如:String::compareToIgnoreCase等價于(x,y) -> x.compareToIgnoreCase(y)
可以在方法里使用this和super,this::equals等同于x -> this.equals(x),super::greet等同于() -> super.greet()

類似于lambda表達式,方法引用不能獨立存在,總是會轉換為函數式接口的實例。

構造器引用

構造器引用與方法引用類似,只不過方法名為new。例如Employee::new是Employee構造器的一個引用。至于是哪一個構造器取決于上下文,比如Function<Integer,Employee> func1 = Employee :: new;就相當于Function<Integer,Employee> func = x -> new Employee(x);
數組類型也有構造器引用,如int[]::new等價于lambda表達式x -> new int[x]

處理lambda表達式

我們之前提到,lambda表達式的重點是延遲執行,之所以希望以后再執行代碼,有很多原因,如:

  • 在一個單獨的線程中運行代碼
  • 多次運行代碼
  • 在算法的恰當位置運行代碼(例如,排序中的比較操作)
  • 發生某種情況時執行代碼(如,點擊了一個按鈕、數據到達等)
  • 只在必要時才運行代碼

下面是常用的函數式接口和基本類型的函數式接口:

1.png
2.png

下面來看一個簡單的例子。假設你想要重復一個動作n次。將這個動作和重復次數傳遞給一個repeat方法:

repeat(10,() -> System.out.println("Hello world"));

要接受這個lambda表達式,需要選擇一個函數式接口。在這里,我們可以使用Runnable接口:

public static void repeat(int n,Runnable action)
{
    for(int i = 0;i < n;i++) 
    action.run();
}

現在讓這個例子更復雜一點,我們希望告訴這個動作它出現在那一次迭代中。為此需要選擇一個合適的函數式接口,其中要包含一個方法。這個方法有一個int參數而且返回類型為void。處理int值的標準接口如下:

public interface IntConsumer
{
    void accept(int value);
}

下面給出repeat方法的改進版本:

public static void repeat(int n,IntConsumer action)
{
    for(int i = 0;i < n;i++)  action.accept(i);
}

可以如下調用它:

repeat(10,i -> System.out.println("Countdown: " + (9 - i)));

大多數函數標準函數式接口都提供了非抽象方法來生成或合并函數。例如,Predicate.isEqual(a)等同于a::equals,不過如果a為null也能正常工作。已經提供了默認方法and、or和negate來合并謂詞。例如,Predicate.isEqual(a).or(Predicate.isEqual(b))就等同于x -> a.equals(x) || b.equals(x)

通過三種方式實現事件處理器

1.內部類

public class HandleEvent extends Application {

   @Override
   public void start(Stage primaryStage) throws Exception {
       HBox pane = new HBox(10);
       pane.setAlignment(Pos.CENTER);
       Button btOK = new Button("OK");
       OKHandlerClass handler1 = new OKHandlerClass();
       btOK.setOnAction(handler1);
       Button btCancel = new Button("Cancel");
       CancelHandlerClass handler2 = new CancelHandlerClass();
       btCancel.setOnAction(handler2);
       pane.getChildren().addAll(btOK,btCancel);
       
       Scene scene = new Scene(pane,100,50);
       primaryStage.setTitle("HandleEvent");
       primaryStage.setScene(scene);
       primaryStage.show();
   }

   
   class OKHandlerClass implements EventHandler<ActionEvent>{
       @Override
       public void handle(ActionEvent e) {
           System.out.println("OK button clicked");
       }
   }
   
   class CancelHandlerClass implements EventHandler<ActionEvent>{
       @Override
       public void handle(ActionEvent e) {
           System.out.println("Cancel button clicked");
       }
   }
   public static void main(String[] args) {
       Application.launch(args);

   }

}

2.匿名內部類

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class AnonymousHandlerDemo extends Application {

   @Override
   public void start(Stage primaryStage) throws Exception {
       HBox hBox = new HBox();
       hBox.setSpacing(10);
       hBox.setAlignment(Pos.CENTER);
       Button btNew = new Button("New");
       Button btOpen = new Button("Open");
       Button btSave= new Button("Save");
       Button btPrint = new Button("Print");
       hBox.getChildren().addAll(btNew,btOpen,btSave,btPrint);
       
       btNew.setOnAction(new EventHandler<ActionEvent>() {
           @Override
           public void handle(ActionEvent e) {
               System.out.println("Process New");
           }
       });
       
       btOpen.setOnAction(new EventHandler<ActionEvent>() {
           @Override
           public void handle(ActionEvent e) {
               System.out.println("Process Open");
           }
       });
       
       btSave.setOnAction(new EventHandler<ActionEvent>() {
           @Override
           public void handle(ActionEvent e) {
               System.out.println("Process Save");
           }
       });
       
       btPrint.setOnAction(new EventHandler<ActionEvent>() {
           @Override
           public void handle(ActionEvent e) {
               System.out.println("Process Print");
           }
       });
       
       Scene scene = new Scene(hBox,300,50);
       primaryStage.setTitle("AnonymousHandlerDemo");
       primaryStage.setScene(scene);
       primaryStage.show();
   }

   public static void main(String[] args) {
       Application.launch(args);

   }

}

3.lambda表達式

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class LambdaHandlerDemo extends Application {

   @Override
   public void start(Stage primaryStage) throws Exception {
       HBox hBox = new HBox();
       hBox.setSpacing(10);
       hBox.setAlignment(Pos.CENTER);
       Button btNew = new Button("New");
       Button btOpen = new Button("Open");
       Button btSave= new Button("Save");
       Button btPrint = new Button("Print");
       hBox.getChildren().addAll(btNew,btOpen,btSave,btPrint);
       
       btNew.setOnAction((ActionEvent e)->{System.out.println("Process New");});
       
       btOpen.setOnAction((e)->{System.out.println("Process Open");});
       
       btSave.setOnAction(e->{System.out.println("Process Save");});
       
       btPrint.setOnAction(e->System.out.println("Process Print"));
       
       Scene scene = new Scene(hBox,300,50);
       primaryStage.setTitle("LambdaHandlerDemo");
       primaryStage.setScene(scene);
       primaryStage.show();
   }

   public static void main(String[] args) {
       Application.launch(args);

   }

}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容