Interfaces
There are a number of situations in software engineering when it is important for disparate groups of programmers to agree to a "contract" that spells out how their software interacts. Each group should be able to write their code without any knowledge of how the other group's code is written. Generally speaking, interfaces are such contracts.
在软件工程的很多情形中,不想干的小组或程序员达成一致的合约(合约用于阐述软件如何交互)是非常重要的。在合约的帮助下,每个小组写代码时不必了解其它小组的代码时如何编写的。一般来说,这样的合约由接口来充当。
For example, imagine a futuristic society where computer-controlled robotic cars transport passengers through city streets without a human operator. Automobile manufacturers write software (Java, of course) that operates the automobile—stop, start, accelerate, turn left, and so forth. Another industrial group, electronic guidance instrument manufacturers, make computer systems that receive GPS (Global Positioning System) position data and wireless transmission of traffic conditions and use that information to drive the car.
譬如说,想象一下在不久的将来,由电脑控制的机器人汽车载着乘客穿过城市街道,而车是无人驾驶的。而汽车制造商用JAVA编写的软件操作汽车——停止、启动、加速、向左转等等。而另一个产业群电子制导仪器制造商使用电脑系统接收GPS数据和交通状况的无线传输数据,并用接收到的数据驾车汽车。
The auto manufacturers must publish an industry-standard interface that spells out in detail what methods can be invoked to make the car move (any car, from any manufacturer). The guidance manufacturers can then write software that invokes the methods described in the interface to command the car. Neither industrial group needs to know how the other group's software is implemented. In fact, each group considers its software highly proprietary and reserves the right to modify it at any time, as long as it continues to adhere to the published interface.
该汽车的制造商必须发布行业标准接口,阐述调用什么方法可以使车移动,然后指导制造商可以调用接口描述中的方法控制汽车,而工业集团不必知道其他集团的软件是如何实现的。事实上,每个集团都认为自己对软件高度专有,可以随时修改软件,前提是遵循发布的接口规范。
Interfaces in Java
In the Java programming language, an interface is a reference type, similar to a class, that can contain only constants, method signatures, default methods, static methods, and nested types. Method bodies exist only for default methods and static methods. Interfaces cannot be instantiated—they can only be implemented by classes or extended by other interfaces. Extension is discussed later in this lesson.
在JAVA中,接口和类相似,都是引用类型。接口只能包含方法、默认方法、静态方法和嵌套方法。方法主题只能以默认方法和静态方法的形式存在。接口不能被实例化,只能被类实现或被其它的接口继承。详细会在稍后讨论。
Defining an interface is similar to creating a new class:
定义一个结合和创建一个类非常相似:
1 public interface OperateCar { 2 3 // constant declarations, if any 4 5 // method signatures 6 7 // An enum with values RIGHT, LEFT 8 int turn(Direction direction, double radius, double startSpeed, double endSpeed); 9 10 int changeLanes(Direction direction, double startSpeed, double endSpeed); 11 12 int signalTurn(Direction direction, boolean signalOn); 13 14 int getRadarFront(double distanceToCar, double speedOfCar); 15 16 int getRadarRear(double distanceToCar, 17 double speedOfCar);...... 18 // more method signatures 19 }
Note that the method signatures have no braces and are terminated with a semicolon.
To use an interface, you write a class that implements the interface. When an instantiable class implements an interface, it provides a method body for each of the methods declared in the interface. For example,
注意:方法没有(),以分号结束。
要使用接口,只需写一个类实现接口即可。当一个类实现一个接口时,它必须实现接口描述中的每个方法。例如:
1 public class OperateBMW760i implements OperateCar { 2 3 // the OperateCar method signatures, with implementation -- 4 // for example: 5 int signalTurn(Direction direction, boolean signalOn) { 6 // code to turn BMW's LEFT turn indicator lights on 7 // code to turn BMW's LEFT turn indicator lights off 8 // code to turn BMW's RIGHT turn indicator lights on 9 // code to turn BMW's RIGHT turn indicator lights off 10 } 11 12 // other members, as needed -- for example, helper classes not 13 // visible to clients of the interface 14 }
In the robotic car example above, it is the automobile manufacturers who will implement the interface. Chevrolet's implementation will be substantially different from that of Toyota, of course, but both manufacturers will adhere to the same interface. The guidance manufacturers, who are the clients of the interface, will build systems that use GPS data on a car's location, digital street maps, and traffic data to drive the car. In so doing, the guidance systems will invoke the interface methods: turn, change lanes, brake, accelerate, and so forth.
以上述无人驾驶汽车为例,汽车制造商将实现接口。雪福来和丰田的实现可能有所不同,但是他们讲遵循相同的接口。作为接口客户端的指导制造商将构建一个系统,该系统使用汽车位置的GPS数据、数字街道地图、交通数据来驾车汽车。这样做,指导系统将调用接口的方法:转弯、变道、刹车、加速等等。
Interfaces as APIs
The robotic car example shows an interface being used as an industry standard Application Programming Interface (API). APIs are also common in commercial software products. Typically, a company sells a software package that contains complex methods that another company wants to use in its own software product. An example would be a package of digital image processing methods that are sold to companies making end-user graphics programs. The image processing company writes its classes to implement an interface, which it makes public to its customers. The graphics company then invokes the image processing methods using the signatures and return types defined in the interface. While the image processing company's API is made public (to its customers), its implementation of the API is kept as a closely guarded secret—in fact, it may revise the implementation at a later date as long as it continues to implement the original interface that its customers have relied on.
无人驾驶汽车的例子展示了接口被用作行业标准API。APIs在商业软件产品中也很常见。通常情况下,有的公司出售包含复杂方法的软件包,让其他公司在他们的软件产品中使用。可能有这样的例子:一个提供图像处理方法的包卖给了做终端用户图形程序的公司。图形处理公司编写实现公共接口的类,图形公司调用接口中定义的方法,并返回接口中定义的类型。对客户来说,图像处理公司的API是公开的,API的实现是私密的——事实上,图像处理公司今后可以修改API的实现,只要它继续实现客户依赖的接口。
---------------------------------------------------------------------------------------
Defining an Interface
An interface declaration consists of modifiers, the keyword interface
, the interface name, a comma-separated list of parent interfaces (if any), and the interface body. For example:
接口声明包括:修饰符、接口关键字、接口名称、用逗号分隔的父接口列表(如果有父接口的话)和接口主体。例如:
1 public interface GroupedInterface extends Interface1, Interface2, Interface3 { 2 3 // constant declarations 4 5 // base of natural logarithms 6 double E = 2.718282; 7 8 // method signatures 9 void doSomething(int i, double x); 10 11 int doSomethingElse(String s); 12 }
The public
access specifier indicates that the interface can be used by any class in any package. If you do not specify that the interface is public, then your interface is accessible only to classes defined in the same package as the interface.
An interface can extend other interfaces, just as a class subclass or extend another class. However, whereas a class can extend only one other class, an interface can extend any number of interfaces. The interface declaration includes a comma-separated list of all the interfaces that it extends.
其中public修饰符说明此接口可以被任何package中的class使用。如果不指定public修饰符,那么接口只能被和此接口在同一个package中的类使用。
接口可以继承其它接口,就像子类或继承其它类一样。然而,类只能继承一个类,而接口可以继承多个接口。接口声明包含一个用逗号分隔的要继承的接口列表。
The Interface Body
The interface body can contain abstract methods, default methods, and static methods. An abstract method within an interface is followed by a semicolon, but no braces (an abstract method does not contain an implementation). Default methods are defined with the default
modifier, and static methods with the static
keyword. All abstract, default, and static methods in an interface are implicitly public
, so you can omit thepublic
modifier.
In addition, an interface can contain constant declarations. All constant values defined in an interface are implicitly public
, static
, and final
. Once again, you can omit these modifiers.
接口主题可以包含抽象方法、默认方法和静态方法。接口内的抽象方法后面紧跟着一个分号,但是没有括号(抽象方法不包含方法实现);默认方法定义时使用default修饰符;静态方法使用static关键字。接口中的所有抽象方法、默认方法和静态方法默认都是public,所以你可以省略public修饰符。
另外,接口可以包含常量声明。在接口中定义的所有常量默认都是public、static、finale。你可以省略这些修饰符。
To declare a class that implements an interface, you include an implements
clause in the class declaration. Your class can implement more than one interface, so the implements
keyword is followed by a comma-separated list of the interfaces implemented by the class. By convention, the implements
clause follows the extends
clause, if there is one.
要声明一个实现接口的类,只需在类的声明中包含一个implements字句。类可以实现多个接口,一个类实现多个接口时,只需在 implements
关键字后跟用逗号分隔的多个接口名即可。按照惯例,implements的条款遵循继承的条款。
A Sample Interface, Relatable
Consider an interface that defines how to compare the size of objects.
想象有这样一个接口:如何比较对象的大小。
1 public interface Relatable { 2 3 // this (object calling isLargerThan) 4 // and other must be instances of 5 // the same class returns 1, 0, -1 6 // if this is greater than, 7 // equal to, or less than other 8 public int isLargerThan(Relatable other); 9 }
If you want to be able to compare the size of similar objects, no matter what they are, the class that instantiates them should implement Relatable
.
如果你想比较两个相似对象的大小,不管这个两个对象是什么,用于实例化的类需要实现Relatable接口。
Any class can implement Relatable
if there is some way to compare the relative "size" of objects instantiated from the class. For strings, it could be number of characters; for books, it could be number of pages; for students, it could be weight; and so forth. For planar geometric objects, area would be a good choice (see the RectanglePlus
class that follows), while volume would work for three-dimensional geometric objects. All such classes can implement the isLargerThan()
method.
不管是什么类,只要它需要比较两个实例化的对象的大小,它都可以实现Relatable接口。例如,对于字符串,它可能需要比较一些字符的大小;对于书,它可能是需要比较书的页面的大小;对于学生,他可能是学生的重量;对于平面几何对象,可能是面积。所有这些类都可以实现Relatable接口的isLargerThan()方法。
If you know that a class implements Relatable
, then you know that you can compare the size of the objects instantiated from that class.
如果一个类实现了Relatable接口,那么你可以比较这个类的实例化的对象的大小。( 通过isLargerThan()
)
Implementing the Relatable Interface
Here is the Rectangle
class that was presented in the Creating Objects section, rewritten to implement Relatable
.
下面是在创建对象一节讲到的Rectangle类,现在重写为实现Relatable接口:
1 public class RectanglePlus 2 implements Relatable { 3 public int width = 0; 4 public int height = 0; 5 public Point origin; 6 7 // four constructors 8 public RectanglePlus() { 9 origin = new Point(0, 0); 10 } 11 public RectanglePlus(Point p) { 12 origin = p; 13 } 14 public RectanglePlus(int w, int h) { 15 origin = new Point(0, 0); 16 width = w; 17 height = h; 18 } 19 public RectanglePlus(Point p, int w, int h) { 20 origin = p; 21 width = w; 22 height = h; 23 } 24 25 // a method for moving the rectangle 26 public void move(int x, int y) { 27 origin.x = x; 28 origin.y = y; 29 } 30 31 // a method for computing 32 // the area of the rectangle 33 public int getArea() { 34 return width * height; 35 } 36 37 // a method required to implement 38 // the Relatable interface 39 public int isLargerThan(Relatable other) { 40 RectanglePlus otherRect 41 = (RectanglePlus)other; 42 if (this.getArea() < otherRect.getArea()) 43 return -1; 44 else if (this.getArea() > otherRect.getArea()) 45 return 1; 46 else 47 return 0; 48 } 49 }
Because RectanglePlus
implements Relatable
, the size of any two RectanglePlus
objects can be compared.
因为RectanglePlus实现了Relatable接口,所以任意两个RectanglePlus对象都能比较。
Note: The isLargerThan
method, as defined in the Relatable
interface, takes an object of type Relatable
. The line of code, shown in bold in the previous example, casts other
to a RectanglePlus
instance. Type casting tells the compiler what the object really is. Invoking getArea
directly on the other
instance (other.getArea()
) would fail to compile because the compiler does not understand that other
is actually an instance of RectanglePlus
.
---------------------------------------------------------------------------------------
When you define a new interface, you are defining a new reference data type. You can use interface names anywhere you can use any other data type name. If you define a reference variable whose type is an interface, any object you assign to it must be an instance of a class that implements the interface.
As an example, here is a method for finding the largest object in a pair of objects, for any objects that are instantiated from a class that implements Relatable
:
1 public Object findLargest(Object object1, Object object2) { 2 Relatable obj1 = (Relatable)object1; 3 Relatable obj2 = (Relatable)object2; 4 if ((obj1).isLargerThan(obj2) > 0) 5 return object1; 6 else 7 return object2; 8 }
By casting object1
to a Relatable
type, it can invoke the isLargerThan
method.
If you make a point of implementing Relatable
in a wide variety of classes, the objects instantiated from any of those classes can be compared with the findLargest()
method—provided that both objects are of the same class. Similarly, they can all be compared with the following methods:
1 public Object findSmallest(Object object1, Object object2) { 2 Relatable obj1 = (Relatable)object1; 3 Relatable obj2 = (Relatable)object2; 4 if ((obj1).isLargerThan(obj2) < 0) 5 return object1; 6 else 7 return object2; 8 }
1 public boolean isEqual(Object object1, Object object2) { 2 Relatable obj1 = (Relatable)object1; 3 Relatable obj2 = (Relatable)object2; 4 if ( (obj1).isLargerThan(obj2) == 0) 5 return true; 6 else 7 return false; 8 }
These methods work for any "relatable" objects, no matter what their class inheritance is. When they implement Relatable
, they can be of both their own class (or superclass) type and a Relatable
type. This gives them some of the advantages of multiple inheritance, where they can have behavior from both a superclass and an interface.
-------------------------------------------------------
Evolving Interfaces
Consider an interface that you have developed called DoIt
:
1 public interface DoIt { 2 void doSomething(int i, double x); 3 int doSomethingElse(String s); 4 }
Suppose that, at a later time, you want to add a third method to DoIt
, so that the interface now becomes:
1 public interface DoIt { 2 3 void doSomething(int i, double x); 4 int doSomethingElse(String s); 5 boolean didItWork(int i, double x, String s); 6 7 }
If you make this change, then all classes that implement the old DoIt
interface will break because they no longer implement the old interface. Programmers relying on this interface will protest loudly.
Try to anticipate all uses for your interface and specify it completely from the beginning. If you want to add additional methods to an interface, you have several options. You could create a DoItPlus
interface that extends DoIt
:
public interface DoItPlus extends DoIt { boolean didItWork(int i, double x, String s); }
Now users of your code can choose to continue to use the old interface or to upgrade to the new interface.
Alternatively, you can define your new methods as default methods. The following example defines a default method named didItWork
:
1 public interface DoIt { 2 3 void doSomething(int i, double x); 4 int doSomethingElse(String s); 5 default boolean didItWork(int i, double x, String s) { 6 // Method body 7 } 8 9 }
Note that you must provide an implementation for default methods. You could also define new static methods to existing interfaces. Users who have classes that implement interfaces enhanced with new default or static methods do not have to modify or recompile them to accommodate the additional methods.
The section Interfaces describes an example that involves manufacturers of computer-controlled cars who publish industry-standard interfaces that describe which methods can be invoked to operate their cars. What if those computer-controlled car manufacturers add new functionality, such as flight, to their cars? These manufacturers would need to specify new methods to enable other companies (such as electronic guidance instrument manufacturers) to adapt their software to flying cars. Where would these car manufacturers declare these new flight-related methods? If they add them to their original interfaces, then programmers who have implemented those interfaces would have to rewrite their implementations. If they add them as static methods, then programmers would regard them as utility methods, not as essential, core methods.
Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.
Consider the following interface, TimeClient
, as described in 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(); }
The following class, SimpleTimeClient
, implements 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()); } } Suppose that you want to add new functionality to the TimeClient interface, such as the ability to specify a time zone through a ZonedDateTime object (which is like a LocalDateTime object except that it stores time zone information): 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); }
Following this modification to the TimeClient
interface, you would also have to modify the class SimpleTimeClient
and implement the method getZonedDateTime
. However, rather than leaving getZonedDateTime
as abstract
(as in the previous example), you can instead define a default implementation. (Remember that an abstract method is a method declared without an implementation.)
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)); } }
You specify that a method definition in an interface is a default method with the default
keyword at the beginning of the method signature. All method declarations in an interface, including default methods, are implicitly public
, so you can omit the public
modifier.
With this interface, you do not have to modify the class SimpleTimeClient
, and this class (and any class that implements the interface TimeClient
), will have the method getZonedDateTime
already defined. The following example, TestSimpleTimeClient
, invokes the methodgetZonedDateTime
from an instance of SimpleTimeClient
:
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()); } }
Extending Interfaces That Contain Default Methods
When you extend an interface that contains a default method, you can do the following:
- Not mention the default method at all, which lets your extended interface inherit the default method.
- Redeclare the default method, which makes it
abstract
. - Redefine the default method, which overrides it.
Suppose that you extend the interface TimeClient
as follows:
public interface AnotherTimeClient extends TimeClient { }
Any class that implements the interface AnotherTimeClient
will have the implementation specified by the default method TimeClient.getZonedDateTime
.
Suppose that you extend the interface TimeClient
as follows:
public interface AbstractZoneTimeClient extends TimeClient { public ZonedDateTime getZonedDateTime(String zoneString); }
Any class that implements the interface AbstractZoneTimeClient
will have to implement the method getZonedDateTime
; this method is an abstract
method like all other nondefault (and nonstatic) methods in an interface.
Suppose that you extend the interface TimeClient
as follows:
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()); } } }
Any class that implements the interface HandleInvalidTimeZoneClient
will use the implementation of getZonedDateTime
specified by this interface instead of the one specified by the interface TimeClient
.
Static Methods
In addition to default methods, you can define static methods in interfaces. (A static method is a method that is associated with the class in which it is defined rather than with any object. Every instance of the class shares its static methods.) This makes it easier for you to organize helper methods in your libraries; you can keep static methods specific to an interface in the same interface rather than in a separate class. The following example defines a static method that retrieves a ZoneId
object corresponding to a time zone identifier; it uses the system default time zone if there is no ZoneId
object corresponding to the given identifier. (As a result, you can simplify the method 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)); } }
Like static methods in classes, you specify that a method definition in an interface is a static method with the static
keyword at the beginning of the method signature. All method declarations in an interface, including static methods, are implicitly public
, so you can omit the public
modifier.
Integrating Default Methods into Existing Libraries
Default methods enable you to add new functionality to existing interfaces and ensure binary compatibility with code written for older versions of those interfaces. In particular, default methods enable you to add methods that accept lambda expressions as parameters to existing interfaces. This section demonstrates how the Comparator
interface has been enhanced with default and static methods.
Consider the Card
and Deck
classes as described in Questions and Exercises: Classes. This example rewrites the Card
and Deck
classes as interfaces. The Card
interface contains two enum
types (Suit
and Rank
) and two abstract methods (getSuit
and getRank
):
package defaultmethods; 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(); }
The Deck
interface contains various methods that manipulate cards in a deck:
package defaultmethods; 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; }
The class PlayingCard
implements the interface Card
, and the class StandardDeck
implements the interface Deck
.
The class StandardDeck
implements the abstract method Deck.sort
as follows:
public class StandardDeck implements Deck { private List<Card> entireDeck; // ... public void sort() { Collections.sort(entireDeck); } // ... }
The method Collections.sort
sorts an instance of List
whose element type implements the interface Comparable
. The member entireDeck
is an instance of List
whose elements are of the type Card
, which extends Comparable
. The class PlayingCard
implements theComparable.compareTo
method as follows:
public int hashCode() { return ((suit.value()-1)*13)+rank.value(); } public int compareTo(Card o) { return this.hashCode() - o.hashCode(); }
The method compareTo
causes the method StandardDeck.sort()
to sort the deck of cards first by suit, and then by rank.
What if you want to sort the deck first by rank, then by suit? You would need to implement the Comparator
interface to specify new sorting criteria, and use the method sort(List<T> list, Comparator<? super T> c)
(the version of the sort
method that includes aComparator
parameter). You can define the following method in the class StandardDeck
:
public void sort(Comparator<Card> c) { Collections.sort(entireDeck, c); }
With this method, you can specify how the method Collections.sort
sorts instances of the Card
class. One way to do this is to implement the Comparator
interface to specify how you want the cards sorted. The example SortByRankThenSuit
does this:
package defaultmethods; 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(); } }
The following invocation sorts the deck of playing cards first by rank, then by suit:
StandardDeck myDeck = new StandardDeck(); myDeck.shuffle(); myDeck.sort(new SortByRankThenSuit());
However, this approach is too verbose; it would be better if you could specify what you want to sort, not how you want to sort. Suppose that you are the developer who wrote the Comparator
interface. What default or static methods could you add to the Comparator
interface to enable other developers to more easily specify sort criteria?
To start, suppose that you want to sort the deck of playing cards by rank, regardless of suit. You can invoke the StandardDeck.sort
method as follows:
StandardDeck myDeck = new StandardDeck(); myDeck.shuffle(); myDeck.sort( (firstCard, secondCard) -> firstCard.getRank().value() - secondCard.getRank().value() );
Because the interface Comparator
is a functional interface, you can use a lambda expression as an argument for the sort
method. In this example, the lambda expression compares two integer values.
It would be simpler for your developers if they could create a Comparator
instance by invoking the method Card.getRank
only. In particular, it would be helpful if your developers could create a Comparator
instance that compares any object that can return a numerical value from a method such as getValue
or hashCode
. The Comparator
interface has been enhanced with this ability with the static method comparing
:
myDeck.sort(Comparator.comparing((card) -> card.getRank()));
In this example, you can use a method reference instead:
myDeck.sort(Comparator.comparing(Card::getRank));
This invocation better demonstrates what to sort rather than how to do it.
The Comparator
interface has been enhanced with other versions of the static method comparing
such as comparingDouble
and comparingLong
that enable you to create Comparator
instances that compare other data types.
Suppose that your developers would like to create a Comparator
instance that could compare objects with more than one criteria. For example, how would you sort the deck of playing cards first by rank, and then by suit? As before, you could use a lambda expression to specify these sort criteria:
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(); } );
It would be simpler for your developers if they could build a Comparator
instance from a series of Comparator
instances. The Comparator
interface has been enhanced with this ability with the default method thenComparing
:
myDeck.sort( Comparator .comparing(Card::getRank) .thenComparing(Comparator.comparing(Card::getSuit)));
The Comparator
interface has been enhanced with other versions of the default method thenComparing
(such as thenComparingDouble
and thenComparingLong
) that enable you to build Comparator
instances that compare other data types.
Suppose that your developers would like to create a Comparator
instance that enables them to sort a collection of objects in reverse order. For example, how would you sort the deck of playing cards first by descending order of rank, from Ace to Two (instead of from Two to Ace)? As before, you could specify another lambda expression. However, it would be simpler for your developers if they could reverse an existing Comparator
by invoking a method. The Comparator
interface has been enhanced with this ability with the default method reversed
:
myDeck.sort( Comparator.comparing(Card::getRank) .reversed() .thenComparing(Comparator.comparing(Card::getSuit)));
This example demonstrates how the Comparator
interface has been enhanced with default methods, static methods, lambda expressions, and method references to create more expressive library methods whose functionality programmers can quickly deduce by looking at how they are invoked. Use these constructs to enhance the interfaces in your libraries.
An interface declaration can contain method signatures, default methods, static methods and constant definitions. The only methods that have implementations are default and static methods.
A class that implements an interface must implement all the methods declared in the interface.
An interface name can be used anywhere a type can be used.
Questions
- What methods would a class that implements the
java.lang.CharSequence
interface have to implement? - What is wrong with the following interface?
public interface SomethingIsWrong { void aMethod(int aValue){ System.out.println("Hi Mom"); } }
- Fix the interface in question 2.
- Is the following interface valid?
public interface Marker { }
Exercises
- Write a class that implements the
CharSequence
interface found in thejava.lang
package. Your implementation should return the string backwards. Select one of the sentences from this book to use as the data. Write a smallmain
method to test your class; make sure to call all four methods. - Suppose you have written a time server that periodically notifies its clients of the current date and time. Write an interface the server could use to enforce a particular protocol on its clients.