资讯专栏INFORMATION COLUMN

Java™ 教程(默认方法)

zhisheng / 3441人阅读

默认方法

接口部分描述了一个涉及计算机控制汽车制造商的例子,他们发布了行业标准接口,描述了可以调用哪些方法来操作他们的汽车,如果那些计算机控制的汽车制造商为他们的汽车添加新的功能,如飞行,该怎么办?这些制造商需要指定新的方法,以使其他公司(如电子制导仪器制造商)能够使其软件适应飞行汽车,这些汽车制造商将在哪里声明这些与飞行有关的新方法?如果他们将它们添加到原始接口,那么实现了这些接口的程序员将不得不重写他们的实现,如果他们将它们作为静态方法添加,那么程序员会将它们视为实用方法,而不是必要的核心方法。

默认方法使你能够向库的接口添加新功能,并确保与为这些接口的旧版本编写的代码的二进制兼容性。

考虑以下接口TimeClient

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();
}

以下类SimpleTimeClient实现了TimeClient

package defaultmethods;

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对象):

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,但是,你可以改为定义默认实现,而不是将getZonedDateTime保留为抽象(如上例所示),请记住,抽象方法是在没有实现的情况下声明的方法。

package defaultmethods;
 
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,以下示例TestSimpleTimeClientSimpleTimeClient的实例调用方法getZonedDateTime

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

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());
    }
}
扩展包含默认方法的接口

扩展包含默认方法的接口时,可以执行以下操作:

完全不要提到默认方法,它允许扩展接口继承默认方法。

重新声明默认方法,使其成为抽象方法。

重新定义覆盖它的默认方法。

假设你按如下方式扩展TimeClient接口:

public interface AnotherTimeClient extends TimeClient { }

任何实现接口AnotherTimeClient的类都将具有默认方法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指定的实现。

静态方法

除了默认方法,你还可以在接口中定义静态方法(静态方法是一种与定义它的类相关联的方法,而不是与任何对象相关联的方法,该类的每个实例都共享其静态方法)。这使你可以更轻松地在库中组织辅助方法,你可以在同一个接口中保留特定于接口的静态方法,而不是在多带带的类中。以下示例定义一个静态方法,该方法检索与时区标识符对应的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接口。

考虑CardDeck类,此示例将Card和Deck类重写为接口,Card接口包含两个枚举类型(SuitRank)和两个抽象方法(getSuitgetRank):

package defaultmethods;

public interface Card extends Comparable {
    
    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接口包含操作deck中卡片的各种方法:

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

    Map deal(int players, int numberOfCards)
        throws IllegalArgumentException;

}

PlayingCard类实现了接口Card,而StandardDeck类实现了接口Deck

StandardDeck类实现了抽象方法Deck.sort,如下所示:

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

方法Collections.sort对List的实例进行排序,其元素类型实现了Comparable接口,成员entireDeck是List的一个实例,其元素的类型为Card,它扩展了ComparablePlayingCard类实现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()首先按花色对一副牌进行排序,然后按等级排序。

如果你想先按等级排序,然后按花色排序怎么办?你需要实现Comparator接口以指定新的排序条件,并使用方法sort(List list, Comparator c)(包含Comparator参数的sort方法的版本),你可以在StandardDeck类中定义以下方法:

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

使用此方法,你可以指定方法Collections.sort如何对Card类的实例进行排序,一种方法是实现Comparator接口,以指定你希望卡片的排序方式,示例SortByRankThenSuit执行此操作:

package defaultmethods;

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

public class SortByRankThenSuit implements Comparator {
    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(); 
    }
}

以下调用首先按等级对扑克牌组进行排序,然后按照花色进行排序:

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

但是,这种方法过于冗长,如果你可以指定你想要排序的东西,而不是你想要排序的方式会更好,假设你是编写Comparator接口的开发人员,你可以向Comparator接口添加哪些默认或静态方法,以使其他开发人员能够更轻松地指定排序条件?

首先,假设你想要按等级对扑克牌进行排序,而不考虑花色,你可以按如下方式调用StandardDeck.sort方法:

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

因为接口Comparator是一个功能性接口,所以可以使用lambda表达式作为sort方法的参数,在此示例中,lambda表达式比较两个整数值。

如果他们可以通过仅调用Card.getRank方法创建Comparator实例,那么对于你的开发人员来说会更简单,特别是,如果你的开发人员可以创建一个Comparator实例来比较任何可以从getValuehashCode等方法返回数值的对象,那将会很有帮助。使用此静态方法comparing增强了Comparator接口:

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

在此示例中,你可以使用方法引用:

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

此调用更好地演示了要排序的内容而不是如何进行排序。

Comparator接口已经通过静态方法comparing的其他版本得到了增强,例如comparingDouble和comparingLong,它们使你能够创建比较其他数据类型的Comparator实例。

假设你的开发人员想要创建一个可以用多个标准比较对象的Comparator实例,例如,你如何先按等级排序扑克牌,然后按花色排序?和以前一样,你可以使用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实例,那么对于你的开发人员来说会更简单,Comparator接口已通过默认方法thenComparing增强了此功能:

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

Comparator接口已使用其他版本的默认方法thenComparing进行了增强(例如thenComparingDouble和thenComparingLong),使你可以构建比较其他数据类型的Comparator实例。

假设你的开发人员想要创建一个Comparator实例,使其能够以相反的顺序对对象集合进行排序,例如,你如何按照等级的降序排序扑克牌组,从Ace到2(而不是从2到Ace)?和以前一样,你可以指定另一个lambda表达式,但是,如果你的开发人员可以通过调用方法来反转现有的Comparator,那么它会更简单,使用此功能增强了Comparator接口,默认方法reversed:

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

这个例子演示了Comparator接口是如何通过默认方法、静态方法、lambda表达式和方法引用来增强的,从而创建更具表现力的库方法,程序员可以通过查看调用这些方法的方式来快速推断这些方法的功能,使用这些构造来增强库中的接口。

上一篇:不断发展的接口 下一篇:继承

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72878.html

相关文章

  • Java基础学习教程,eclipse简单使用教程Java集成开发工具)

    摘要:占市场份额,剩下是其他的开发工具。总之集成开发工具就是为了提高开发速度。编写第一个程序在上点击右键填写上类名在下面有一个选中创建方法。 使用集成开发工具eclipse 1、java的集成开发工具很多,包括:eclipse、Intellij IDEA、netbeans..... eclips...

    AlanKeene 评论0 收藏0
  • Java教程(不断发展的接口)

    不断发展的接口 考虑一下你开发的名为DoIt的接口: public interface DoIt { void doSomething(int i, double x); int doSomethingElse(String s); } 假设稍后你要向DoIt添加第三个方法,这样现在接口变为: public interface DoIt { void doSomething(i...

    libin19890520 评论0 收藏0
  • Java教程(接口)

    接口 软件工程中存在许多情况,当不同的程序员团队同意一份合约来阐明他们的软件如何交互时很重要,每个组都应该能够在不知道如何编写其他组代码的情况下编写代码,一般来说,接口就是这样的合约。 例如,想象一个未来主义社会,计算机控制的机器人汽车在没有人工操作员的情况下将乘客运送到城市街道,汽车制造商编写操作汽车的软件(当然是Java) - 停止,启动,加速,向左转,等等,另一个工业集团,电子制导仪器制造商...

    amuqiao 评论0 收藏0
  • Spring Boot 2.x基础教程:工程结构推荐

    摘要:典型示例以下结构是比较推荐的组织方式,所有的类和其他都在之下。应用主类,该类直接位于下。默认情况下,的应用主类会自动扫描以及所有子包下的所有类来进行初始化。 Spring Boot框架本身并没有对工程结构有特别的要求,但是按照最佳实践的工程结构可以帮助我们减少可能会遇见的坑,尤其是Spring包扫描机制的存在,如果您使用最佳实践的工程结构,可以免去不少特殊的配置工作。 典型示例 以下结...

    CollinPeng 评论0 收藏0
  • Java教程(目录)

    Java™ 教程 Java教程是为JDK 8编写的,本页面中描述的示例和实践没有利用在后续版本中引入的改进。 Java教程是希望使用Java编程语言创建应用程序的程序员的实用指南,其中包括数百个完整的工作示例和数十个课程,相关课程组被组织成教程。 覆盖基础知识的路径 这些教程以书籍的形式提供,如Java教程,第六版,前往Amazon.com购买。 入门 介绍Java技术和安装Java开发软件并使用...

    lifesimple 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<