Java 8 – Default Method

Default Methods

传统上,Java程序的接口是将相关方法按照约定组合到一起的方式。实现接口的类必须为接 口中定义的每个方法提供一个实现,或者从父类中继承它的实现。但是,一旦类库的设计者需要更新接口,向其中加入新的方法,这种方式就会出现问题。现实情况是,现存的实体类往往不在接口设计者的控制范围之内,这些实体类为了适配新的接口约定也需要进行修改。由于Java 8的 API在现存的接口上引入了非常多的新方法,这种变化带来的问题也愈加严重,一个例子就是List接口上的sort方法。想象一下其他备选集合框架的维护人员会多么抓狂吧, 像Guava和Apache Commons这样的框架现在都需要修改实现了List接口的所有类,为其添加 sort方法的实现。
且慢,其实你不必惊慌。Java 8为了解决这一问题引入了一种新的机制。Java 8中的接口现在 支持在声明方法的同时提供实现,这听起来让人惊讶!通过两种方式可以完成这种操作。其一, Java 8允许在接口内声明静态方法。其二,Java 8引入了一个新功能,叫Default Methods,通过默认方法你可以指定接口方法的默认实现。换句话说,接口能提供方法的具体实现。因此,实现接口的类如果不显式地提供该方法的具体实现,就会自动继承默认的实现。这种机制可以使你平滑地进行接口的优化和演进。实际上,到目前为止你已经使用了多个默认方法。例如List接口中的sort,以及Collection接口中的stream。 我们看到的List接口中的sort方法是Java 8中全新的方法,它的定义如下:

default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

请注意返回类型之前的新default修饰符。通过它,我们能够知道一个方法是否为默认方法。 由于有了这个新的方法,我们现在可以直接通过调用sort,对列表中的元素进行排序。

 public static void main(String[] args)  {
        List<Integer> numbers = Arrays.asList(3, 5, 1, 2, 6);
        numbers.sort(Comparator.naturalOrder());
        numbers.forEach(System.out::println);
    }

输出如下:

1
2
3
5
6

Default Methods使你能够向库的接口添加新功能,并确保与为这些接口的旧版本编写的代码的二进制兼容性。
我们先看一下下面的接口 TimeClient, 如 Answers to Questions and Exercises: Interfaces所述:


import java.time.*; 
 
public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
}

TimeClient的实现类SimpleTimeClient, 如下所示:

import java.time.*;
import java.lang.*;
import java.util.*;

public class SimpleTimeClient implements TimeClient {
    
    private LocalDateTime dateAndTime;
    
    public SimpleTimeClient() {
        dateAndTime = LocalDateTime.now();
    }
    
    public void setTime(int hour, int minute, int second) {
        LocalDate currentDate = LocalDate.from(dateAndTime);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(currentDate, timeToSet);
    }
    
    public void setDate(int day, int month, int year) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime currentTime = LocalTime.from(dateAndTime);
        dateAndTime = LocalDateTime.of(dateToSet, currentTime);
    }
    
    public void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime timeToSet = LocalTime.of(hour, minute, second); 
        dateAndTime = LocalDateTime.of(dateToSet, timeToSet);
    }
    
    public LocalDateTime getLocalDateTime() {
        return dateAndTime;
    }
    
    public String toString() {
        return dateAndTime.toString();
    }
    
    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println(myTimeClient.toString());
    }
}

假如你要向 TimeClient 接口中添加一个新的功能, 例如通过 ZonedDateTime 对象指定时区的功能(类似于 LocalDateTime 对象,不过ZonedDateTime存储时区信息):

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
        int hour, int minute, int second);
    LocalDateTime getLocalDateTime();                           
    ZonedDateTime getZonedDateTime(String zoneString);
}

在对TimeClient 接口修改之后, 你还必须要修改SimpleTimeClient 类实现getZonedDateTime方法。然后, 你可以定义一个 default implementation 用来替代上面的抽象方法 getZonedDateTime. ( 抽象方法 是没有实现的情况下声明的方法.)

import java.time.*;

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
    
    static ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }
        
    default ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }
}

你在接口中定义默认方法,需要在方法签名的开头使用default关键字。接口中的所有方法声明(包括默认方法)都是隐式public的,因此你可以省略public修饰符。
使用此接口,你不必修改类SimpleTimeClient类,并且所有实现TimeClient接口的任何类将默认定义了getZonedDateTime方法。
接下来的示例,我们将定义一个TestSimpleTimeClient 类,然后演示调用SimpleTimeClient的实例的getZonedDateTime方法:

import java.lang.*;

public class TestSimpleTimeClient {
    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println("Current time: " + myTimeClient.toString());
        System.out.println("Time in California: " +
                myTimeClient.getZonedDateTime("Blah blah").toString());
    }
}

继承含有Default Methods的Interfaces

当你继承一个含有Default Methods的Interfaces,你可以进行如下操作:

  • 如果没有定义Default Methods,那么默认继承Default Methods
  • 你可以将Default Methods重新声明为abstract
  • 你可以重写Default Methods,重写之后将覆盖父接口的Default Methods

如果我们按照如下方式继承了TimeClient:

public interface AnotherTimeClient extends TimeClient { }

任何实现了AnotherTimeClient接口的类都将默认实现default method TimeClient.getZonedDateTime.

如果我们按照如下方式继承了TimeClient:

public interface AbstractZoneTimeClient extends TimeClient {
    public ZonedDateTime getZonedDateTime(String zoneString);
}

那么任何实现AbstractZoneTimeClient接口的类都必须实现getZonedDateTime方法; 此方法是一个抽象方法,就像接口中的其他非默认(和非静态)方法一样。

如果我们按照如下方式继承了TimeClient:

public interface HandleInvalidTimeZoneClient extends TimeClient {
    default public ZonedDateTime getZonedDateTime(String zoneString) {
        try {
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.of(zoneString)); 
        } catch (DateTimeException e) {
            System.err.println("Invalid zone ID: " + zoneString +
                "; using the default time zone instead.");
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.systemDefault());
        }
    }
}

任何实现HandleInvalidTimeZoneClient接口的类都将使用此接口定义的getZonedDateTime实现,而不是TimeClient接口中定义的实现。

Static Methods

除了默认方法, 你还可以在接口中定义 static methods . (静态方法是一种只和定义它的类关联,而不是和具体对象实例关联. 该类的所有实例都共享静态方法.) 这使得你很容易的在你的类库中编写一些辅助方法; 这样你可以在接口中定义静态方法,这样所有实现该接口的类都能默认继承该静态方法,而不需要单独在类中定义. 下面的例子我们定义一个静态方法,该方法检索与时区标识符对应的 ZoneId 对象;如果参数不属于正确的 ZoneId 对象,它将使用系统默认时区。(因此,您可以简化getZonedDateTime方法):

public interface TimeClient {
    // ...
    static public ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }

    default public ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }    
}

与类中的静态方法一样,你在接口中定义静态方法,需要在方法签名的开头使用static关键字。接口中的所有方法声明(包括静态方法)都是隐式public的,因此你可以省略public修饰符。

将默认方法集成到现有库中

默认方法使得能够向现有接口添加新功能,并确保与原来实现旧版本接口的代码的二进制兼容性。特别是,默认方法可以作为lambda表达式的接口参数。本节演示如何使用默认和静态方法增强 Comparator 接口.
下面的示例中有Card and Deck 两个接口。 其中 Card 接口包含两个枚举类型(SuitRank) 和两个抽象方法 (getSuit and getRank):

public interface Card extends Comparable<Card> {
    
    public enum Suit { 
        DIAMONDS (1, "Diamonds"), 
        CLUBS    (2, "Clubs"   ), 
        HEARTS   (3, "Hearts"  ), 
        SPADES   (4, "Spades"  );
        
        private final int value;
        private final String text;
        Suit(int value, String text) {
            this.value = value;
            this.text = text;
        }
        public int value() {return value;}
        public String text() {return text;}
    }
    
    public enum Rank { 
        DEUCE  (2 , "Two"  ),
        THREE  (3 , "Three"), 
        FOUR   (4 , "Four" ), 
        FIVE   (5 , "Five" ), 
        SIX    (6 , "Six"  ), 
        SEVEN  (7 , "Seven"),
        EIGHT  (8 , "Eight"), 
        NINE   (9 , "Nine" ), 
        TEN    (10, "Ten"  ), 
        JACK   (11, "Jack" ),
        QUEEN  (12, "Queen"), 
        KING   (13, "King" ),
        ACE    (14, "Ace"  );
        private final int value;
        private final String text;
        Rank(int value, String text) {
            this.value = value;
            this.text = text;
        }
        public int value() {return value;}
        public String text() {return text;}
    }
    
    public Card.Suit getSuit();
    public Card.Rank getRank();
}

Deck 接口则是包含操作cards的各种方法:

import java.util.*;
import java.util.stream.*;
import java.lang.*;
 
public interface Deck {
    
    List<Card> getCards();
    Deck deckFactory();
    int size();
    void addCard(Card card);
    void addCards(List<Card> cards);
    void addDeck(Deck deck);
    void shuffle();
    void sort();
    void sort(Comparator<Card> c);
    String deckToString();

    Map<Integer, Deck> deal(int players, int numberOfCards)
        throws IllegalArgumentException;

}

PlayingCard 实现了Card接口, StandardDeck 实现了Deck接口.
StandardDeck 实现了抽象方法 Deck.sort,如下所示:

public class StandardDeck implements Deck {
    
    private List<Card> entireDeck;
    
    // ...
    
    public void sort() {
        Collections.sort(entireDeck);
    }
    
    // ...
}

Collections.sort 方法的作用是对List进行排序,但是List中的元素必须实现Comparable接口. 成员变量entireDeck是List的一个实例,其元素的类型为Card, 继承至Comparable. PlayingCard 类实现了Comparable.compareTo 方法,如下所示:

public int hashCode() {
    return ((suit.value()-1)*13)+rank.value();
}

public int compareTo(Card o) {
    return this.hashCode() - o.hashCode();
}

compareTo 方法使得StandardDeck.sort()对集合cards中的deck元素先按suit排序, 然后按rank排序.
如果你想要先按rank排序, 然后按suit排序怎么办? 你需要实现Comparator 接口指定新的排序条件, 并且使用sort(List<T> list, Comparator<? super T> c) 方法(包含一个Comparator 参数的sort方法). 你可以在StandardDeck类中定义如下方法:

public void sort(Comparator<Card> c) {
    Collections.sort(entireDeck, c);
}  

使用此方法, 你可以告诉Collections.sort方法如何对Card实例进行排序. 其中一种办法就是实现Comparator接口,指定你所期望的排序方式. 如例子 SortByRankThenSuit 所示:

import java.util.*;
import java.util.stream.*;
import java.lang.*;

public class SortByRankThenSuit implements Comparator<Card> {
    public int compare(Card firstCard, Card secondCard) {
        int compVal =
            firstCard.getRank().value() - secondCard.getRank().value();
        if (compVal != 0)
            return compVal;
        else
            return firstCard.getSuit().value() - secondCard.getSuit().value(); 
    }
}

下面的调用方式就是先按rank排序, 然后按suit排序:

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(new SortByRankThenSuit());

但是, 这种调用方式太过啰嗦; 如果能够只是指定排序的方式,而不是编写排序的代码会更好一些。假如你是编写Comparator接口的开发人员. 你会向Comparator接口添加那些默认方法或者是静态方法,以便其他开发人员更容易的去指定排序条件呢?
首先,我们先不考虑是否合适,如果只对集合cards中的deck元素先按按rank排序,你可以按照如下方式调用StandardDeck.sort

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
    (firstCard, secondCard) ->
        firstCard.getRank().value() - secondCard.getRank().value()
); 

因为Comparator接口是一个functional interface, 你可以使用lambda表达式作为sort方法的参数.在此示例中,lambda表达式比较两个整数值。
如果可以通过仅仅调用Card.getRank来创建一个Comparator实例,会对开发人员而言会更简单一些。特别是,如果开发人员可以创建一个任意的Comparator实例,比较的数值可以从 getValuehashCode等方法获取,那将会很有帮助。通过增加静态方法comparing,增强了Comparator接口:

myDeck.sort(Comparator.comparing((card) -> card.getRank()));  

而且,你也可是使用方法引用的方式:

myDeck.sort(Comparator.comparing(Card::getRank));  

Comparator 接口增加了一些静态方法, 例如 comparingDoublecomparingLong 等,这样你可以创建比较其他数据类型的Comparator实例.
假设现在开发人员想要创建一个含有多个条件的Comparator实例。例如对集合cards中的deck元素先按rank排序, 然后按suit排序?和以前一样,你可以使用lambda表达式来指定这些排序条件:

StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
    (firstCard, secondCard) -> {
        int compare =
            firstCard.getRank().value() - secondCard.getRank().value();
        if (compare != 0)
            return compare;
        else
            return firstCard.getSuit().value() - secondCard.getSuit().value();
    }      
); 

如果可以通过创建连续的多个Comparator实例来完成如上功能,对于开发人员来说会更简单一些。Comparator 接口通过增加默认方法thenComparing已经实现了该功能,如下所示:

myDeck.sort(
    Comparator
        .comparing(Card::getRank)
        .thenComparing(Comparator.comparing(Card::getSuit)));

同样,Comparator接口也对 thenComparing进行了重载 (例如 thenComparingDoublethenComparingLong),这样你可以创建比较其他数据类型的Comparator实例.
假设你的开发人员想要创建一个 Comparator 实例,使其能够以相反的顺序对对象集合进行排序。例如,如何对cards集合中的deck元素先按照rank进行倒序排序,从Acs 到 Two(而不是从Two到Acs)?参照原来的方式,我们可以再创建一个新的lambda 表达式。但是,如果开发人员通过调用 Comparator实例中已有的一个倒序方法,这样会更简单一些。Comparator 接口已经通过默认方法reversed实现了该功能,如下所示:

myDeck.sort(
    Comparator.comparing(Card::getRank)
        .reversed()
        .thenComparing(Comparator.comparing(Card::getSuit)));

此示例演示了如何使用默认方法,静态方法,lambda表达式和方法引用来增强Comparator接口,以创建更加强大的类库方法。通过学习和推导,我们也可以增强项目中现有的类库。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342