Java8 到 Java17之间的特性描述

java8

主要变化:
1.lambda表达式与Stream API(Lambda Expression and Stream API)
2.方法引用(Method Reference)
3.接口默认方法(Default Methods)
4.类型注解(Type Annotations)
5.可重复注解(Repeating Annotations)
6.方法参数反射(Method Parameter Reflection)

lambda表达式与Stream API

在没有lambda表达式与Stream API之前,对于集合数据的处理入下图所示:

public class LambdaExpressions {
    public static List findCarsOldWay(List cars) {
        List selectedCars = new ArrayList<>();
        for (Car car : cars) {
            if (car.kilometers < 50000) {
                selectedCars.add(car);
            }
        }
        return selectedCars;
    }
}

现在我们看下使用lambda表达式与Stream API进行操作的示例:

public class LambdaExpressions {
    public static List findCarsUsingLambda(List cars) {
        return cars.stream().filter(car -> car.kilometers < 50000)
                .collect(Collectors.toList());
    }
}

我们需要通过调用stream()方法将集合类型的cars变量转换为stream,内部通过filter()方法设定对应的过滤条件(car对象的公里数小于5000),最后通过colletct()方法将最后的结果集包装为List返回。

方法引用(Method Reference)

没引入方法引用的时候

public class MethodReference {
    List withoutMethodReference =
            cars.stream().map(car -> car.toString())
                    .collect(Collectors.toList());
}

方法引用允许我们使用一种特定类型的语法 "::" 调用函数,上述示例经由方法引用修改后如下所示:

public class MethodReference {
    List methodReference = cars.stream().map(Car::toString)
            .collect(Collectors.toList());
}

我们还是使用的lambda表达式,但是对于toString()方法的调用改成了方法引用Car::toString,这会使得代码更加简洁并且可读性更好。

接口默认方法(Default Methods)

假设一个场景,我们现在有个简单的方法log(String message)用于在调用的时候打印消息,后来我们发现在日志消息中添加时间戳会更加的易于搜索,当引入这个变化以后我们不希望客户端有任何感知。这时候,就可以使用接口默认方法(Default Methods)来实现这个功能。

public class DefaultMethods {

    public interface Logging {
        void log(String message);
    }

    public class LoggingImplementation implements Logging {
        @Override
        public void log(String message) {
            System.out.println(message);
        }
    }
}

这时候添加一个新方法到接口中:

public class DefaultMethods {

    public interface Logging {
        void log(String message);
        
        void log(String message, Date date);
    }
}

这时候引用了Logging 接口的客户端的所有实现类都会出现编译报错

Class 'LoggingImplementation' must either be declared abstract 
or implement abstract method 'log(String, Date)' in 'Logging'`.

使用接口默认方法就能完美的解决这个问题

public class DefaultMethods {

    public interface Logging {
        void log(String message);

        default void log(String message, Date date) {
            System.out.println(date.toString() + ": " + message);
        }
    }
}

使用关键字default能让我们在接口中新增方法的实现。现在LoggingImplementation类的编译报错将不会存在。

类型注解(Type Annotations)

类型注解是java8引入的又一个特性。虽然在这之前我们就有可用的注解,但是现在我们可以在使用类型的任何地方使用它们,使用场景包括但不限于:

  • 本地变量定义
public class TypeAnnotations {

    public static void main(String[] args) {
        @NotNull String userName = args[0];
    }
}
  • 构造器调用
public class TypeAnnotations {

    public static void main(String[] args) {
        List request =
                new @NotEmpty ArrayList<>(Arrays.stream(args).collect(
                        Collectors.toList()));
    }
}
  • 泛型
public class TypeAnnotations {

    public static void main(String[] args) {
        List<@Email String> emails;
    }
}

可重复注解(Repeating Annotations)

创建可重复注解

public class RepeatingAnnotations {
    
    @Repeatable(Notifications.class)
    public @interface Notify {
        String email();
    }

    public @interface Notifications {
        Notify[] value();
    }
}

我们创建了@Notify注解作为一个常用注解,但是我们在它上面增加了@Repeatable元注解,这说明我们可以将@Notifications注解 当做@Notify 注解的对象集合来使用。

使用可重复注解

@Notify(email = "[email protected]")
@Notify(email = "[email protected]")
public class UserNotAllowedForThisActionException
        extends RuntimeException {
    final String user;

    public UserNotAllowedForThisActionException(String user) {
        this.user = user;

    }
}

上述逻辑说明当一个用户试图做他不允许做的事的时候,会抛出一个自定义的异常,注解提供的功能就是在捕获自定义异常的时候,能够发送通知给对应的注解中的邮箱地址。

java9

主要变化:
1.java 模块化(Java Module System)
2.带有内部匿名类的菱形语法(Diamond Syntax with Inner Anonymous Classes)
3.私有接口方法(Private Interface Methods)

java 模块化(Java Module System)

模块化是一组包(package)以及包的依赖与资源,相对于包而言,它提供了更加广泛的功能。

当我们创建新模块的时候,我们需要提供以下几个属性:

  • 名称(Name)
  • 依赖(Dependencies)
  • 公开包(Public Packages) - 默认情况下,所有包都是模块私有的
  • 服务提供(Services Offered)
  • 服务消费(Services Consumed)
  • 反射权限(Reflection Permissions)

创建模块
以我们常用的hello world为例,第一个模块中打印"Hello", 第二个模块打印"World"。

新建两个模块,分别是hello.module和world.module,这两个模块正好对应maven的hello和world模块(前者是java9对应的模块概念,后者是maven中的模块概念),在hello和world模块分别新建module-info.java文件,这个文件定义了java9中的模块,在文件中我们需要定义那些包(packages)我们需要对外开放,通过关键字exports处理;以及那些模块是独立的。

world模块对应的module-info.java文件内容:

module world.module {
    exports com.practice.jdk9.module.world;
}

通过module关键字以及模块对应的名称来引用需要引用的模块,通过exports关键字告诉模块系统(module system)已经将包com.practice.jdk9.module.world对外部模块可见。

当然除了上述说到的关键字以外,还有如下关键字也会在模块化中用到:

  • requires
  • requires transitive
  • exports to
  • provides with
  • open
  • opens
  • opens to

hello模块对应的module-info.java文件内容:

module hello.module {
    requires world.module;
}

带有内部匿名类的菱形语法(Diamond Syntax with Inner Anonymous Classes)

在java9之前,我们不能在匿名内部类中使用菱形(<>)操作符,如下示例所示,我们定义了一个抽象类StringAppender,包含一个将两个字符串通过“-”分隔符进行拼接的方法,我们将使用匿名类来提供append()方法的实现:

public class DiamondOperator {

    StringAppender appending = new StringAppender<>() {
        @Override
        public String append(String a, String b) {
            return new StringBuilder(a).append("-").append(b).toString();
        }
    };
    
    public abstract static class StringAppender {
        public abstract T append(String a, String b);
    }
}

我们是用菱形操作符(<>)来省略new StringAppender<>()构造器中的类型,在java8中,会出现编译错误,idea中具体信息如下:

Cannot use '<>' with anonymous inner classes
无法推断org.example.test.DiamondOperator.StringAppender的类型参数

通过project structure 将jdk切换到9之后,对应的编译报错信息就会消失,DiamondOperator 文件能正确编译通过。

私有接口方法(Private Interface Methods)

在上述java8中我们已经了解过默认方法(default methods),在java9中我们可以在接口中定义私有方法,私有方法的主要目的在于通用方法的封装以提升其在别的默认方法中的可复用性。

public class PrivateInterfaceMethods {

    public static void main(String[] args) {
        TestingNames names = new TestingNames();
        System.out.println(names.fetchInitialData());
    }

    public static class TestingNames implements NamesInterface {
        public TestingNames() {
        }
    }

    public interface NamesInterface {
        default List fetchInitialData() {
            try (BufferedReader br = new BufferedReader(
                    new InputStreamReader(this.getClass()
                            .getResourceAsStream("/names.txt")))) {
                return readNames(br);
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }

        private List readNames(BufferedReader br)
                throws IOException {
            ArrayList names = new ArrayList<>();
            String name;
            while ((name = br.readLine()) != null) {
                names.add(name);
            }
            return names;
        }
    }
}

java10

局部变量类型推断(Local Variable Type Inference)

java总是需要在局部变量中显式的定义类型,当我们编写与阅读代码的时候,我们总是知道我们期望的类型;换句话说,很多代码显得很冗余。java10中的var类型允许我们省略语句左侧的类型声明。

老式的写法

public class LocalTypeVar {

    public void explicitTypes() {
        Person Roland = new Person("Roland", "Deschain");
        Person Susan = new Person("Susan", "Delgado");
        Person Eddie = new Person("Eddie", "Dean");
        Person Detta = new Person("Detta", "Walker");
        Person Jake = new Person("Jake", "Chambers");

        List persons =
                List.of(Roland, Susan, Eddie, Detta, Jake);

        for (Person person : persons) {
            System.out.println(person.name + " - " + person.lastname);
        }
    }
}

使用var后的写法

ublic class LocalTypeVar {

    public void varTypes() {
        var Roland = new Person("Roland", "Deschain");
        var Susan = new Person("Susan", "Delgado");
        var Eddie = new Person("Eddie", "Dean");
        var Detta = new Person("Detta", "Walker");
        var Jake = new Person("Jake", "Chambers");

        var persons = List.of(Roland, Susan, Eddie, Detta, Jake);

        for (var person : persons) {
            System.out.println(person.name + " - " + person.lastname);
        }
    }
}

java11

Lambda表达式中的局部变量类型推断(Local Variable Type in Lambda Expressions)

在java11中针对java10中的局部变量类型推断这个特性进行了改善,使得我们可以在lambda表达式中使用var进行操作。

public class LocalTypeVarLambda {

    public void explicitTypes() {
        var Roland = new Person("Roland", "Deschain");
        var Susan = new Person("Susan", "Delgado");
        var Eddie = new Person("Eddie", "Dean");
        var Detta = new Person("Detta", "Walker");
        var Jake = new Person("Jake", "Chambers");

        var filteredPersons =
                List.of(Roland, Susan, Eddie, Detta, Jake)
                        .stream()
                        .filter((var x) -> x.name.contains("a"))
                        .collect(Collectors.toList());
        System.out.println(filteredPersons);
    }
}

在filter方法中,我们使用var来进行类型推断而不是明确的类型声明。

java14

Switch表达式(Switch Expressions)

Switch语句老的用法

public class SwitchExpression {

    public static void main(String[] args) {
        int days = 0;
        Month month = Month.APRIL;

        switch (month) {
            case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER :
                days = 31;
                break;
            case FEBRUARY :
                days = 28;
                break;
            case APRIL, JUNE, SEPTEMBER, NOVEMBER :
                days = 30;
                break;
            default:
                throw new IllegalStateException();
        }
    }
}

我们需要确定每一个case分支中都会存在一个break关键字,如果不加会导致days变量的赋值出现错误,因为会匹配到别的case分支。

使用Switch表达式

public class SwitchExpression {

    public static void main(String[] args) {
        int days = 0;
        Month month = Month.APRIL;

        days = switch (month) {
            case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> 31;
            case FEBRUARY -> 28;
            case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
            default -> throw new IllegalStateException();
        };
    }
}

在case块中,我们使用了箭头标识符(->)而不是原先的冒号(:),即便我们没有使用break关键字,也会在匹配到的第一个case分支中跳出switch语句。

yield 关键字
当然上面给出的示例,case中的逻辑比较简单,就是一个赋值操作,如果case中的逻辑复杂一些,就需要使用到yield 关键字进行结果返回(注意yield关键字只能在switch表达式中使用)。

public class SwitchExpression {

    public static void main(String[] args) {
        int days = 0;
        Month month = Month.APRIL;

        days = switch (month) {
            case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> {
                System.out.println(month);
                yield 31;
            }
            case FEBRUARY -> {
                System.out.println(month);
                yield 28;
            }
            case APRIL, JUNE, SEPTEMBER, NOVEMBER -> {
                System.out.println(month);
                yield 30;
            }
            default -> throw new IllegalStateException();
        };
    }
}

如上所示,在一个多行的代码块逻辑中,我们需要用yield关键字从case块中进行返回值操作。

java15

文本块(Text Blocks)

文本块可以理解为对格式化字符串的一个优化操作,从 Java 15 开始,我们可以编写一个跨越多行的字符串作为常规文本。相比于在java15之前,我们需要跨多行,使用"+"号进行多行字符串拼接,会更加的方便并且可读性更高。

没有Text Blocks的时候,需要人为的保证字符串的格式化操作

public class TextBlocks {

    public static void main(String[] args) {
        System.out.println(
        "\n" +
                "\n" +
                "     \n" +
                "        Example\n" +
                "    \n" +
                "    \n" +
                "        

This is an example of a simple HTML " + "page with one paragraph.

\n" + " \n" + "\n"); } }

使用Text Blocks之后

public class TextBlocks {
    
    public static void main(String[] args) {
        System.out.println(
               """
                
                
                    
                        Example
                    
                    
                        

This is an example of a simple HTML page with one paragraph.

""" ); } }

我们使用特殊的语法进行文本块的开始和结束:""";其中有一些既定的规则需要注意

  • 在第一个开引号"""后需要进行换行操作,否则编译会报错。
  • 如果我们想要使用\n换行结束我们的文本块, 我们可以在闭符号"""前添加新行进行处理。

java16

instanceof 模式匹配(Pattern Matching of instanceof)

instanceof 模式匹配允许我们以内联形式转换变量,并在所需的if-else块中使用它而无需进行强转。

假设我们有一个基类Vehicle,它有两个子类Car 和 Bicycle,逻辑比较简单,就是根据Vehicle的具体子类类型计算价格。
老式的写法需要在if-else块中将Vehicle转换为正确的类型:

public class PatternMatching {
    public static double priceOld(Vehicle v) {
        if (v instanceof Car) {
            Car c = (Car) v;
            return 10000 - c.kilomenters * 0.01 -
                    (Calendar.getInstance().get(Calendar.YEAR) -
                            c.year) * 100;
        } else if (v instanceof Bicycle) {
            Bicycle b = (Bicycle) v;
            return 1000 + b.wheelSize * 10;
        } else throw new IllegalArgumentException();
    }
}

使用模式匹配写法

public class PatternMatching {
    public static double price(Vehicle v) {
        if (v instanceof Car c) {
            return 10000 - c.kilomenters * 0.01 -
                    (Calendar.getInstance().get(Calendar.YEAR) -
                            c.year) * 100;
        } else if (v instanceof Bicycle b) {
            return 1000 + b.wheelSize * 10;
        } else throw new IllegalArgumentException();
    }
}

需要注意的是,对应的变量范围,只在当前所属的if-else代码块中可见。

档案类(Records)

回想下自己写过的 POJOs(Plain Old Java Objects) 有多少,这些类提供的功能基本一样,包含get/set,equals,hashcode,toString以及构造器函数等,通常来说java对于像这样的模板代码生成不是很好,好在第三方的插件lombok能解决这样的问题。

java16中引入了档案类用以删除大量的重复声明代码,档案类其实就是一个普通的POJO,大部分代码都是从模板定义中生成的。
老式的POJO定义

public class Vehicle {
    String code;
    String engineType;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getEngineType() {
        return engineType;
    }

    public void setEngineType(String engineType) {
        this.engineType = engineType;
    }

    public Vehicle(String code, String engineType) {
        this.code = code;
        this.engineType = engineType;
    }

    @Override
    public boolean equals(Object o) ...

    @Override
    public int hashCode() ...

    @Override
    public String toString() ...
}

使用档案类定义

public record VehicleRecord(String code, String engineType) {}

仅仅一行就能涵盖原先老式POJO中50多行对应的功能,是不是非常的神奇,当然档案类是一个不可变类final,所以我们不能继承它。

java17

封闭类(Sealed Classes)

在class上的final 修饰符能禁止任何类继承它,如果我们想继承一个类但是却只有某些指定的类才能实现的时候,该怎么做?

这就是java17封闭类带来的全新特性,封闭类允许我们只能针对permit指定的类进行继承扩展,除此之外,封闭类对于其它类而言,可以理解为是带有final修饰符的类。

还是以之前基类Vehicle 与 它的两个子类Car 和 Bicycle为例:

public sealed class Vehicle permits Bicycle, Car {...}

我们增加了sealed 修饰符在Vehicle基类,并且新增了permits 关键字,用于指定允许继承Vehicle 基类的子类,但是加好了之后,编译器还是会报错,原因在于封闭类对于permits关键字指定的子类的修饰符有要求,必须要增加如下几种修饰符中的一种:

  • final
  • sealed
  • non-sealed
public final class Bicycle extends Vehicle {...}

当然封闭类也会带来相应的条件约束,包括:

  • Permitted 子类在编译期必须能够被封闭类访问
  • Permitted 子类必须直接继承封闭类
  • Permitted 子类必须有如下修饰符中的一个进行修饰:final,sealed,non-sealed
  • Permitted 子类必须在同一个java模块中

参考链接:https://reflectoring.io/java-release-notes/

下列是对于极客时间《深入剖析 Java 新特性》学习的思维导图地址:https://www.processon.com/collaboration/link/61f6a8f77d9c0806abad428b

JDK新特性.png

你可能感兴趣的:(Java8 到 Java17之间的特性描述)