Administrator
发布于 2024-09-11 / 10 阅读
0
0

Jdk17 Feature

refer to : https://segmentfault.com/a/1190000044386436

https://openjdk.org/projects/jdk/17/jeps-since-jdk-11

https://docs.oracle.com/en/java/javase/17/language/switch-expressions.html

Switch

https://docs.oracle.com/en/java/javase/17/language/switch-expressions.html

https://openjdk.org/jeps/361

tranditional kind: case L:

传统的switch case,我们使用``case L:` 这种方式表示,存在一个很严重的问题:fall through

传统的switch,存在fall through现象。 fall through,意思就是贯穿。

我们看一个传统的switch case例子

 public static void main(String[] args) {
        String letter = "B";
        switch (letter){
            case "A":
                System.out.println("Letter A");
            case "B":
                System.out.println("Letter B");
                // 没有 break,继续执行下一个
            case "C":
                System.out.println("Letter C");
            case "D":
                System.out.println("Letter D");
                break;
            case "E":
                System.out.println("Letter E");
            default:
                System.out.println("Other Letter");
        }

输出:

Letter B
Letter C
Letter D

可以看到,当匹配到case B以后,因为没有break,导致他会一直执行next case中语句,知道遇到break语句。

其实,我们可以对switch这样理解:

我们可以想象,从飞机上跳下来,进行跳伞。当有一个case语句匹配上以后,就类似于开启了弹射,往下降落,在降落过程中,会穿过一朵又一朵的云(这里的云,指的就是例子中的System.out.println),直到遇到一个挡板后(这里挡板,指的就是例子中的break),才不会继续往下降落。

而在遇到挡板(break)之前,会一直往下降落,穿过一朵又一朵的云(System.out.println),这个现象,就叫做fall through。

a "case L:" label along with its code to the right is called a switch labeled statement group:

new kind of case label :case L ->

The new kind of case label has the following form:

case label_1, label_2, ..., label_n -> expression;|throw-statement;|block 

先看一个例子:

 public static void main(String[] args) {
        int numLetters = 0;
        Day day = Day.SATURDAY;
        switch (day) {
            case MONDAY, FRIDAY, SUNDAY -> numLetters = 6;
            case TUESDAY                -> numLetters = 7;
            case THURSDAY, SATURDAY     -> numLetters = 8;
            case WEDNESDAY              -> numLetters = 9;
            default -> throw new IllegalStateException("Invalid day: " + day);
        };
        System.out.println(numLetters);

    }

输出:

8

我们看到,上面的例子中,没有break语句,但是也没有出现fall through现象。

也就是说,case L ->,这种新的case lable方式,自动prevent fall through,不需要我们手动写break语句了

yield statement

public static void main(String[] args) {
        Day day = Day.THURSDAY;
        int numLetters = switch (day) {
            case MONDAY:
            case FRIDAY:
            case SUNDAY:
                System.out.println(6);
                yield 6;
            case TUESDAY:
                System.out.println(7);
                yield 7;
            case THURSDAY:
            case SATURDAY:
                System.out.println(8);
                yield 8;
            case WEDNESDAY:
                System.out.println(9);
                yield 9;
            default:
                throw new IllegalStateException("Invalid day: " + day);
        };
        System.out.println(numLetters);
    }

输出:

8
8

可以看到,上面我们将switch case语句的结果,赋值给了numLetters变量。这里是使用了yield关键字,并结合traditional kind case L:

上面的这个switch 返回了一个值的例子,叫做switch expression。

上面 case L ->例子,叫做switch statement。

Note:It's recommended that you use "case L ->" labels.

It's easy to forget to insert break or yield statements when using "case L:" labels; if you do, you might introduce unintentional fall through in your code.

code block

For "case L ->" labels, to specify multiple statements or code that are not expressions or throw statements, enclose them in a block.

Specify the value that the case label produces with the yield statement:

 int numLetters = switch (day) {
        case MONDAY, FRIDAY, SUNDAY -> {
            System.out.println(6);
            yield 6;
        }
        case TUESDAY -> {
            System.out.println(7);
            yield 7;
        }
        case THURSDAY, SATURDAY -> {
            System.out.println(8);
            yield 8;
        }
        case WEDNESDAY -> {
            System.out.println(9);
            yield 9;
        }
        default -> {
            throw new IllegalStateException("Invalid day: " + day);
        }
    };

Text-blocks

https://docs.oracle.com/en/java/javase/17/language/text-blocks.html

https://docs.oracle.com/en/java/javase/17/text-blocks/index.html

Introduction

A text block's principalis munus is to provide clarity by way of minimizing the Java syntax required to render a string that spans multiple lines.

In earlier releases of the JDK, embedding multi-line code snippets required a tangled mess of explicit line terminators, string concatenations, and delimiters. Text blocks eliminate most of these obstructions, allowing you to embed code snippets and text sequences more or less as-is.

A text block is an alternative form of Java string representation that can be used anywhere a traditional double quoted string literal can be used. For example:

// Using a literal string
String dqName = "Pat Q. Smith";

// Using a text block
String tbName = """
                Pat Q. Smith""";

The object produced from a text block is a java.lang.String with the same characteristics as a traditional double quoted string. This includes object representation and interning. Continuing with dqName and tbName from the examples above,

// Both dqName and tbName are strings of equal value
dqName.equals(tbName)    // true

// Both dqName and tbName intern to the same string
dqName == tbName         // true

Text blocks can be used anywhere a string literal can be used. For example, text blocks may be intermixed with string literals in a string concatentation expression:

String str = "The old";
String tb = """
            the new""";
String together = str + " and " + tb + ".";

Text blocks may be used as a method argument:

System.out.println("""
    This is the first line
    This is the second line
    This is the third line
    """);

String methods may be applied to a text block:

"""
John Q. Smith""".substring(8).equals("Smith")    // true

A text block can be used in place of a string literal to improve the readability and clarity of the code. This primarily occurs when a string literal is used to represent a multi-line string. In this case there is considerable clutter from quotation marks, newline escapes, and concatentation operators:

// ORIGINAL
String message = "'The time has come,' the Walrus said,\n" +
                 "'To talk of many things:\n" +
                 "Of shoes -- and ships -- and sealing-wax --\n" +
                 "Of cabbages -- and kings --\n" +
                 "And why the sea is boiling hot --\n" +
                 "And whether pigs have wings.'\n";

Using text blocks removes much of the clutter:

// BETTER
String message = """
    'The time has come,' the Walrus said,
    'To talk of many things:
    Of shoes -- and ships -- and sealing-wax --
    Of cabbages -- and kings --
    And why the sea is boiling hot --
    And whether pigs have wings.'
    """;

Using Text Blocks

Text Block Syntax

A text block begins with three double-quote characters followed by a line terminator. You can't put a text block on a single line, nor can the contents of the text block follow the three opening double-quotes without an intervening line terminator. The reason for this is that text blocks are primarily designed to support multi-line strings, and requiring the initial line terminator simplifies the indentation handling rules (see the section below, Incidental White Space).

// ERROR
String name = """Pat Q. Smith""";

// ERROR
String name = """red
                 green
                 blue
                 """;

// OK
String name = """
    red
    green
    blue
    """;

This last example is equivalent to the following string literal:

String name = "red\n" +
              "green\n" +
              "blue\n";

That Final New Line

Note that the example above,

String name = """
    red
    green
    blue
    """;

is equivalent to "red\ngreen\nblue\n". What if you want to represent a multi-line string without that final \n?

String name = """
    red
    green
    blue""";

This text block is equivalent to is "red\ngreen\nblue". Thus, placing the closing delimiter on the last visible line effectively drops the last \n.

Record Classes

https://docs.oracle.com/en/java/javase/17/language/records.html

Introduction

Record classes, which are a special kind of class, help to model plain data aggregates with less ceremony than normal classes.

A record's fields are final because the class is intended to serve as a simple "data carrier".

A record class declaration consists of a name; optional type parameters (generic record declarations are supported); a header, which lists the "components" of the record; and a body.

For example, here is a record class with two fields:

record Rectangle(double length, double width) { }

This concise declaration of a rectangle is equivalent to the following normal class:

public final class Rectangle {
    private final double length;
    private final double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    double length() { return this.length; }
    double width()  { return this.width; }

    // Implementation of equals() and hashCode(), which specify
    // that two record objects are equal if they
    // are of the same type and contain equal field values.
    public boolean equals...
    public int hashCode...

    // An implementation of toString() that returns a string
    // representation of all the record class's fields,
    // including their names.
    public String toString() {...}
}

A record class declaration consists of a name; optional type parameters (generic record declarations are supported); a header, which lists the "components" of the record; and a body.

A record class declares the following members automatically:

  • For each component in the header, the following two members:
    • A private final field with the same name and declared type as the record component. This field is sometimes referred to as a component field.
    • A public accessor method with the same name and type of the component; in the Rectangle record class example, these methods are Rectangle::length() and Rectangle::width().
  • A canonical constructor whose signature is the same as the header. This constructor assigns each argument from the new expression that instantiates the record class to the corresponding component field.
  • Implementations of the equals and hashCode methods, which specify that two record classes are equal if they are of the same type and contain equal component values.
  • An implementation of the toString method that includes the string representation of all the record class's components, with their names.

As record classes are just special kinds of classes, you create a record object (an instance of a record class) with the new keyword, for example:

Rectangle r = new Rectangle(4,5);

Common Example

package org.example;

public record User(String name,int age,boolean man) {
}

package org.example;

public class UserRecordTest {
    public static void main(String[] args) {
        User zhangsan = new User("zhangsan", 15, true);
        System.out.println(zhangsan.name());
        System.out.println(zhangsan.age());
        System.out.println(zhangsan.man());

    }
}

输出:

zhangsan
15
true

Canonical Constructor & Compact constructor

The following example explicitly declares the canonical constructor for the Rectangle record class. It verifies that length and width are greater than zero. If not, it throws an IllegalArgumentException:

record Rectangle(double length, double width) {
    public Rectangle(double length, double width) {
        if (length <= 0 || width <= 0) {
            throw new java.lang.IllegalArgumentException(
                String.format("Invalid dimensions: %f, %f", length, width));
        }
        this.length = length;
        this.width = width;
    }
}

Repeating the record class's components in the signature of the canonical constructor can be tiresome and error-prone. To avoid this, you can declare a compact constructor whose signature is implicit (derived from the components automatically).

For example, the following compact constructor declaration validates length and width in the same way as in the previous example:

record Rectangle(double length, double width) {
    public Rectangle {
        if (length <= 0 || width <= 0) {
            throw new java.lang.IllegalArgumentException(
                String.format("Invalid dimensions: %f, %f", length, width));
        }
    }
}

This succinct form of constructor declaration is only available in a record class.

Note that the statements this.length = length; and this.width = width; which appear in the canonical constructor do not appear in the compact constructor.

At the end of a compact constructor, its implicit formal parameters are assigned to the record class's private fields corresponding to its components.

Features of Record Classes

A record class is implicitly final, so you cannot explicitly extend a record class. However, beyond these restrictions, record classes behave like normal classes:

  • You can create a generic record class, for example:
record Triangle<C extends Coordinate> (C top, C left, C right) { }
  • You can declare a record class that implements one or more interfaces, for example:
record Customer(...) implements Billable { }
  • You can annotate a record class and its individual components, for example:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface GreaterThanZero { }
record Rectangle(
    @GreaterThanZero double length,
    @GreaterThanZero double width) { }

If you annotate a record component, then the annotation may be propagated to members and constructors of the record class.

This propagation is determined by the contexts in which the annotation interface is applicable.

In the previous example, the @Target(ElementType.FIELD) meta-annotation means that the @GreaterThanZero annotation is propagated to the field corresponding to the record component.

Consequently, this record class declaration would be equivalent to the following normal class declaration:

public final class Rectangle {
    private final @GreaterThanZero double length;
    private final @GreaterThanZero double width;
    
    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    
    double length() { return this.length; }
    double width() { return this.width; }
}

Sealed Classes

https://docs.oracle.com/en/java/javase/17/language/sealed-classes-and-interfaces.html

Sealed classes and interfaces restrict which other classes or interfaces may extend or implement them.

For background information about sealed classes and interfaces, see JEP 409.

One of the primary purposes of inheritance is code reuse: When you want to create a new class and there is already a class that includes some of the code that you want, you can derive your new class from the existing class. In doing this, you can reuse the fields and methods of the existing class without having to write (and debug) them yourself.

However, what if you want to model the various possibilities that exist in a domain by defining its entities and determining how these entities should relate to each other? For example, you're working on a graphics library. You want to determine how your library should handle common geometric primitives like circles and squares. You've created a Shape class that these geometric primitives can extend. However, you're not interested in allowing any arbitrary class to extend Shape; you don't want clients of your library declaring any further primitives. By sealing a class, you can specify which classes are permitted to extend it and prevent any other arbitrary class from doing so.

Declaring Sealed Classes

To seal a class, add the sealed modifier to its declaration. Then, after any extends and implements clauses, add the permits clause. This clause specifies the classes that may extend the sealed class.

For example, the following declaration of Shape specifies three permitted subclasses, Circle, Square, and Rectangle:

public sealed class Shape
    permits Circle, Square, Rectangle {
}

Define the following three permitted subclasses, Circle, Square, and Rectangle, in the same module or in the same package as the sealed class:

public final class Circle extends Shape {
    public float radius;
}

public non-sealed class Square extends Shape {
   public double side;
}   

public sealed class Rectangle extends Shape permits FilledRectangle {
    public double length, width;
}

public final class FilledRectangle extends Rectangle {
    public int red, green, blue;
}

Alternatively, you can define permitted subclasses in the same file as the sealed class. If you do so, then you can omit the permits clause:

package com.example.geometry;

public sealed class Figure
    // The permits clause has been omitted
    // as its permitted classes have been
    // defined in the same file.
{ }

final class Circle extends Figure {
    float radius;
}
non-sealed class Square extends Figure {
    float side;
}
sealed class Rectangle extends Figure {
    float length, width;
}
final class FilledRectangle extends Rectangle {
    int red, green, blue;
}

Constraints on Permitted Subclasses

Permitted subclasses have the following constraints:

  • They must be accessible by the sealed class at compile time.

    For example, to compile Shape.java, the compiler must be able to access all of the permitted classes of Shape: Circle.java, Square.java, and Rectangle.java. In addition, because Rectangle is a sealed class, the compiler also needs access to FilledRectangle.java.

  • They must directly extend the sealed class.

  • They must have exactly one of the following modifiers to describe how it continues the sealing initiated by its superclass:

    • final: Cannot be extended further
    • sealed: Can only be extended by its permitted subclasses
    • non-sealed: Can be extended by unknown subclasses; a sealed class cannot prevent its permitted subclasses from doing this

    For example, the permitted subclasses of Shape demonstrate each of these three modifiers: Circle is final while Rectangle is sealed and Square is non-sealed.

  • They must be in the same module as the sealed class (if the sealed class is in a named module) or in the same package (if the sealed class is in the unnamed module, as in the Shape.java example).

    For example, in the following declaration of com.example.graphics.Shape, its permitted subclasses are all in different packages. This example will compile only if Shape and all of its permitted subclasses are in the same named module.

package com.example.graphics;

public sealed class Shape 
    permits com.example.polar.Circle,
            com.example.quad.Rectangle,
            com.example.quad.simple.Square { }

Declaring Sealed Interfaces

Like sealed classes, to seal an interface, add the sealed modifier to its declaration. Then, after any extends clause, add the permits clause, which specifies the classes that can implement the sealed interface and the interfaces that can extend the sealed interface.

The following example declares a sealed interface named Expr. Only the classes ConstantExpr, PlusExpr, TimesExpr, and NegExpr may implement it:

package com.example.expressions;

public class TestExpressions {
  public static void main(String[] args) {
    // (6 + 7) * -8
    System.out.println(
      new TimesExpr(
        new PlusExpr(new ConstantExpr(6), new ConstantExpr(7)),
        new NegExpr(new ConstantExpr(8))
      ).eval());
   }
}

sealed interface Expr
    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
    public int eval();
}

final class ConstantExpr implements Expr {
    int i;
    ConstantExpr(int i) { this.i = i; }
    public int eval() { return i; }
}

final class PlusExpr implements Expr {
    Expr a, b;
    PlusExpr(Expr a, Expr b) { this.a = a; this.b = b; }
    public int eval() { return a.eval() + b.eval(); }
}

final class TimesExpr implements Expr {
    Expr a, b;
    TimesExpr(Expr a, Expr b) { this.a = a; this.b = b; }
    public int eval() { return a.eval() * b.eval(); }
}

final class NegExpr implements Expr {
    Expr e;
    NegExpr(Expr e) { this.e = e; }
    public int eval() { return -e.eval(); }
}

Record Classes as Permitted Subclasses

You can name a record class in the permits clause of a sealed class or interface. See Record Classes for more information.

Record classes are implicitly final, so you can implement the previous example with record classes instead of ordinary classes:

package com.example.records.expressions;

public class TestExpressions {
  public static void main(String[] args) {
    // (6 + 7) * -8
    System.out.println(
      new TimesExpr(
        new PlusExpr(new ConstantExpr(6), new ConstantExpr(7)),
        new NegExpr(new ConstantExpr(8))
      ).eval());
   }
}

sealed interface Expr
    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
    public int eval();
}

record ConstantExpr(int i) implements Expr {
    public int eval() { return i(); }
}

record PlusExpr(Expr a, Expr b) implements Expr {
    public int eval() { return a.eval() + b.eval(); }
}

record TimesExpr(Expr a, Expr b) implements Expr {
    public int eval() { return a.eval() * b.eval(); }
}

record NegExpr(Expr e) implements Expr {
    public int eval() { return -e.eval(); }
}

Pattern Matching

https://docs.oracle.com/en/java/javase/17/language/pattern-matching.html

Pattern matching involves testing whether an object has a particular structure, then extracting data from that object if there's a match. You can already do this with Java; however, pattern matching introduces new language enhancements that enable you to conditionally extract data from objects with code that's more concise and robust.

Pattern Matching for the instanceof Operator

The following example calculates the perimeter of the parameter shape only if it's an instance of Rectangle or Circle:

interface Shape { }
record Rectangle(double length, double width) implements Shape { }
record Circle(double radius) implements Shape { }
...
    public static double getPerimeter(Shape shape) throws IllegalArgumentException {
        if (shape instanceof Rectangle r) {
            return 2 * r.length() + 2 * r.width();
        } else if (shape instanceof Circle c) {
            return 2 * c.radius() * Math.PI;
        } else {
            throw new IllegalArgumentException("Unrecognized shape");
        }
    }

A pattern is a combination of a test, which is called a predicate; a target; and a set of local variables, which are called pattern variables:

  • The predicate is a Boolean-valued function with one argument; in this case, it’s the instanceof operator testing whether the Shape argument is a Rectangle or a Circle.
  • The target is the argument of the predicate, which is the Shape value.
  • The pattern variables are those that store data from the target only if the predicate returns true, which are the variables r and s.

See Pattern Matching for instanceof for more information.

Local Variable Type Inference

https://docs.oracle.com/en/java/javase/17/language/local-variable-type-inference.html#GUID-D2C58FE6-1065-4B50-9326-57DD8EC358AC

In JDK 10 and later, you can declare local variables with non-null initializers with the var identifier, which can help you write code that’s easier to read.

Consider the following example, which seems redundant and is hard to read:

URL url = new URL("http://www.oracle.com/"); 
URLConnection conn = url.openConnection(); 
Reader reader = new BufferedReader(
    new InputStreamReader(conn.getInputStream()));

You can rewrite this example by declaring the local variables with the var identifier. The type of the variables are inferred from the context:

var url = new URL("http://www.oracle.com/"); 
var conn = url.openConnection(); 
var reader = new BufferedReader(
    new InputStreamReader(conn.getInputStream()));

var is a reserved type name, not a keyword, which means that existing code that uses var as a variable, method, or package name is not affected. However, code that uses var as a class or interface name is affected and the class or interface needs to be renamed.

var can be used for the following types of variables:

  • Local variable declarations with initializers:
var list = new ArrayList<String>();    // infers ArrayList<String>
var stream = list.stream();            // infers Stream<String>
var path = Paths.get(fileName);        // infers Path
var bytes = Files.readAllBytes(path);  // infers bytes[]

  • Enhanced for-loop indexes:
List<String> myList = Arrays.asList("a", "b", "c");
for (var element : myList) {...}  // infers String
  • Index variables declared in traditional for loops:
for (var counter = 0; counter < 10; counter++)  {...}   // infers int
  • try-with-resources variable:
try (var input = 
     new FileInputStream("validation.txt")) {...}   // infers FileInputStream

  • Formal parameter declarations of implicitly typed lambda expressions: A lambda expression whose formal parameters have inferred types is implicitly typed:
BiFunction<Integer, Integer, Integer> = (a, b) -> a + b;

In JDK 11 and later, you can declare each formal parameter of an implicitly typed lambda expression with the var identifier:

(var a, var b) -> a + b;

As a result, the syntax of a formal parameter declaration in an implicitly typed lambda expression is consistent with the syntax of a local variable declaration; applying the var identifier to each formal parameter in an implicitly typed lambda expression has the same effect as not using var at all.

You cannot mix inferred formal parameters and var-declared formal parameters in implicitly typed lambda expressions nor can you mix var-declared formal parameters and manifest types in explicitly typed lambda expressions. The following examples are not permitted:

(var x, y) -> x.process(y)      // Cannot mix var and inferred formal parameters
                                // in implicitly typed lambda expressions
(var x, int y) -> x.process(y)  // Cannot mix var and manifest types
// in explicitly typed lambda expressions

Local Variable Type Inference: Style Guidelines

https://openjdk.org/projects/amber/guides/lvti-style-guide

Interface Default Method & Private Method

Default Method

https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

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 method getZonedDateTime 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 non-default (and non-static) 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.

Private Method

上面,我们介绍了default Method,这个default Method,能够被接口的各实现类重复使用,减少了重复代码的编写。

但是,这带来了一个问题,如果default Method内,逻辑比较复杂,抽象出了b方法:

public interface A {
    
    default void a() {
        // code...
        
        b();
        
        // code...
    } 
    
    default void b() {
        
    }
}

此时,相当于有2个default method,但是b方法是专门给a方法使用的,不希望在A interface的实现类中,调用或者重写b方法,这个问题应该怎么解决呢?

这里,就用到了private method。

我们将b方法,定义成private method,如下:

public interface A {
    
    default void a() {
        // code...
        
        b();
        
        // code...
    } 
    
    private void b() {
        
    }
}

这样,在A interface的实现类中,就不能调用或者重写b方法了。

这个private method feature, allows us to declare private methods that help to share common code between non-abstract methods.


评论