4、无所不在的JAVA——JAVA8实战

用Optional取代null

  • null引用引发的问题,以及为什么要避免null引用
  • 从null到Optional:以null安全的方式重写你的域模型
  • 让Optional发光发热: 去除代码中对null的检查
  • 读取Optional中可能值的几种方法
  • 对可能缺失值的再思考

null带来的种种问题

  • 是错误之源
    • NullPointerException是目前Java程序开发中最典型的异常。
  • 代码膨胀
    • 代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶。
  • 自身是毫无意义
    • null自身没有任何的语义,尤其是,代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模。
  • 破坏了Java的哲学
    • Java一直试图避免让程序员意识到指针的存在,唯一的例外是:null指针
  • 它在Java的类型系统上开了个口子
    • null并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,原因是当这个变量被传递到系统中的另一个部分后,将无法获知这个null变量最初的赋值到底是什么类型。

:::info
使用Optional而不是null的一个非常重要而又实际的语义区别是,第一个例子中,我们在声明变量时使用的是Optional类型,而不是Car类型,这句声明非常清楚地表明了这里发生变量缺失是允许的。与

此相反,使用Car这样的类型,可能将变量赋值为null,这意味着需要独立面对这些,只能依赖对业务模型的理解,判断一个null是否属于该变量的有效范畴。

:::

public class Person {
    private Optional<Car> car;       ←---- 人可能有汽车,也可能没有汽车,因此将这个字段声明为Optional
    public Optional<Car> getCar() { return car; }
}
public class Car {
    private Optional<Insurance> insurance;       ←---- 汽车可能进行了保险,也可能没有保险,所以将这个字段声明为Optional
    public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance {
    private String name;       ←---- 保险公司必须有名字
    public String getName() { return name; }
}

应用Optional的几种模式

//静态工厂方法Optional.empty创建一个空的Optional对象:
Optional<Car> optCar = Optional.empty();
//还可以使用静态工厂方法Optional.of依据一个非空值创建一个Optional对象:
Optional<Car> optCar = Optional.of(car);

//如果car是一个null,这段代码就会立即抛出一个NullPointerException,而不是等到你试图访问car的属性值时才返回一个错误。
//最后,使用静态工厂方法Optional.ofNullable,创建一个允许null值的Optional对象:

Optional<Car> optCar = Optional.ofNullable(car);
//如果car是null,那么得到的Optional对象就是个空对象。

使用map从Optional对象中提取和转换值

public String getCarInsuranceName(Person person) {
    return person.getCar().getInsurance().getName();
}

使用flatMap链接Optional对象


Optional<Person> optPerson = Optional.of(person);
Optional<String> name =
    optPerson.map(Person::getCar)
             .map(Car::getInsurance)
             .map(Insurance::getName);
//非法调用。
//最外层的Optional对象包含了另一个Optional对象的值

嵌套式Optional结构

解决办法——flatMap方法。

使用流时,flatMap方法接受一个函数作为参数,这个函数的返回值是另一个流。

这个方法会应用到流中的每一个元素,最终形成一个新的流的流。但是flagMap会用流的内容替换每个新生成的流。

换句话说,由方法生成的各个流会被合并或者扁平化为一个单一的流。将两层的Optional合并为一个。

flatMap方法在Stream和Optional类之间的相似性

这里传给流的flatMap方法的函数,会转换每个正方形到一个包含两个三角形的流中。

如果将该函数应用于简单的map,那么map结果将是包含了其他三个流的流,这三个流都分别包含两个三角形。

如果该函数应用于flatMap方法,结果则不一样,flatMap会捋平两层结构的流为总计包含六个三角形的单层流。类似地,传递给Optional的flatMap方法的函数,会转换原始Optional中的正方形到一个Optional中(包含一个三角形)。

如果函数传递给map方法,那么map结果是包含了一个Optional的Optional,相应地,最里层的Optional包含一个三角形。

但flatMap会捋平两层结构的Optional为一个包含了一个三角形的单层结构

:::info
map 是对流元素进行转换,flatMap 是对流中的元素(数组)进行平铺后合并,即对流中的每个元素平铺后又转换成为了 Stream 流。

:::

public String getCarInsuranceName(Optional<Person> person) {
    return person.flatMap(Person::getCar)
                 .flatMap(Car::getInsurance)
                 .map(Insurance::getName)
                 .orElse("Unknown");       ←---- 如果Optional的结果值为空,设置默认值
}

//结果作为Optional类型返回,让你的同事或者未来你方法的使用者,
//很清楚地知道它可以接受空值,或者可能返回一个空值。

使用Optional解引用串接的Person/Car/Insurance对象

结合使用之前介绍的map和flatMap方法,从Person中解引用出Car,从Car中解引用出Insurance,从Insurance对象中解引用出包含insurance公司名称的字符串

使用Optional解引用串接的Person/Car/Insurance

返回的Optional可能是两种情况:如果调用链上的任何一个方法返回一个空的Optional,那么结果就为空,否则返回的值就是你期望公司名称

:::info
Optional类设计时就没特别考虑将其作为类的字段使用,因此它也并未实现Serializable接口。

由于这个原因,如果应用使用了某些要求序列化的库或者框架,在域模型中使用Optional,有可能引发应用程序故障。

:::

public class Person {
    private Car car;
    public Optional<Car> getCarAsOptional() {
        return Optional.ofNullable(car);
    }
}

操纵由Optional对象构成的Stream

Java 9引入了Optional的stream()方法,使用该方法可以把一个含值的Optional对象转换成由该值构成的Stream对象,或者把一个空的Optional对象转换成等价的空Stream。

处理的对象是由Optional对象构成的Stream时,你需要将这个Stream转换为由原Stream中非空Optional对象值组成的新Stream。

public Set<String> getCarInsuranceNames(List<Person> persons) {
    return persons.stream()
                  .map(Person::getCar)   ←---- 将person列表转换为Optional<Car>组成的流,car是列表中person名下的汽车
                  .map(optCar -> optCar.flatMap(Car::getInsurance))   ←---- 对每个Optional<Car>执行flatMap操作,将其转换成对应的Optional<Insurance>对象
                  .map(optIns -> optIns.map(Insurance::getName))   ←---- 将每个Optional<Insurance>映射成包含对应保险公司名字的Optional<String>
                  .flatMap(Optional::stream)   ←---- 将Stream<Optional<String>>转换为Stream<String>对象,只保留流中那些存在保险公司名的对象
                  .collect(toSet());   ←---- 收集处理的结果字符串,将其保存到一个不含重复值的Set中
}


//解包出其他对象的值,并把结果保存到集合Set中
Stream<Optional<String>> stream = ...
Set<String> result = stream.filter(Optional::isPresent)
                           .map(Optional::get)
                           .collect(toSet());
//解包Optional对象,提取其中的值,跳过那些空的对象,所有这一切都只需执行一次操作。

默认行为及解引用Optional对象

  • get()是这些方法中最简单但又最不安全的方法。如果变量存在,那它直接返回封装的变量值,否则就抛出一个NoSuchElementException异常。所以,除非非常确定Optional变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于嵌套式的null检查,也并未体现出多大的改进。
  • orElse(T other)允许你在Optional对象不包含值时提供一个默认值。
  • orElseGet(Supplier other)是orElse方法的延迟调用版,因为Supplier方法只有在Optional对象不含值时才执行调用。如果创建默认值是件耗时费力的工作,考虑采用这种方式(借此提升程序的性能),或者需要非常确定某个方法仅在Optional为空时才进行调用,也可以考虑该方式(使用orElseGet时至关重要)。
  • or(Supplier> supplier)与前面介绍的orElseGet方法很像,不过它不会解包Optional对象中的值,即便该值是存在的。实战中,如果Optional对象含有值,这一方法(自Java 9引入)不会执行任何额外的操作,直接返回该Optional对象。如果原始Optional对象为空,该方法会延迟地返回一个不同的Optional对象。
  • orElseThrow(Supplier exceptionSupplier)和get方法非常类似,遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希望抛出的异常类型。
  • ifPresent(Consumerconsumer)变量值存在时,执行一个以参数形式传入的方法,否则就不进行任何操作。ifPresentOrElse(Consumer action, Runnable emptyAction)。该方法不同于ifPresent,它接受一个Runnable方法,如果Optional对象为空,就执行该方法所定义的动作。

以不解包的方式组合两个Optional对象

public Optional<Insurance> nullSafeFindCheapestInsurance(
                              Optional<Person> person, Optional<Car> car) {
    return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}

使用filter剔除特定的值

Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance ->
                        "CambridgeInsurance".equals(insurance.getName()))
            .ifPresent(x -> System.out.println("ok"));

//对Optional封装的Person对象进行filter操作,设置相应的条件谓词,
//即如果person的年龄大于minAge参数的设定值,就返回该值,并将谓词传递给filter方法
public String getCarInsuranceName(Optional<Person> person, int minAge) {
    return person.filter(p -> p.getAge() >= minAge)
                 .flatMap(Person::getCar)
                 .flatMap(Car::getInsurance)
                 .map(Insurance::getName)
                 .orElse("Unknown");
}
方法 描述
empty 返回一个空的Optional实例
filter 如果值存在并且满足提供的谓词,就返回包含该值的Optional对象;否则返回一个空的Optional对象
flatMap 如果值存在,就对该值执行提供的mapping函数调用,返回一个Optional类型的值,否则就返回一个空的Optional对象
get 如果值存在,就将该值用Optional封装返回,否则抛出一个NoSuchElementException异常
ifPresent 如果值存在,就执行使用该值的方法调用,否则什么也不做
ifPresentOrElse 如果值存在,就以值作为输入执行对应的方法调用,否则执行另一个不需任何输入的方法
isPresent 如果值存在就返回true,否则返回false
map 如果值存在,就对该值执行提供的mapping函数调用
of 将指定值用Optional封装之后返回,如果该值为null,则抛出一个NullPointerException异常
ofNullable 将指定值用Optional封装之后返回,如果该值为null,则返回一个空的Optional对象
or 如果值存在,就返回同一个Optional对象,否则返回由支持函数生成的另一个Optional对象
orElse 如果有值则将其返回,否则返回一个默认值
orElseGet 如果有值则将其返回,否则返回一个由指定的Supplier接口生成的值
orElseThrow 如果有值则将其返回,否则抛出一个由指定的Supplier接口生成的异常
stream 如果有值,就返回包含该值的一个Stream,否则返回一个空的Stream

用Optional封装可能为null的值

Map<String, Object>方法,访问由key索引的值时,如果map中没有与key关联的值,该次调用就会返回一个null

Object value = map.get("key");

//使用Optional封装map的返回值,可以对这段代码进行优化。
//要达到这个目的有两种方式:使用笨拙的if-then-else判断语句,毫无疑问这种方式会增加代码的复杂度;
//或者采用前文介绍的Optional.ofNullable方法

Optional<Object> value = Optional.ofNullable(map.get("key"));
//每次你希望安全地对潜在为null的对象进行转换,将其替换为Optional对象时,都可以考虑使用这种方法

异常与Optional的对比

//这种情况比较典型的例子是使用静态方法Integer.parseInt(String),将String转换为int
public static Optional<Integer> stringToInt(String s) {
    try {
        return Optional.of(Integer.parseInt(s));       ←---- 如果String能转换为对应的Integer,将其封装在Optional对象中返回
    } catch (NumberFormatException e) {
        return Optional.empty();       ←---- 否则返回一个空的Optional对象
    }
}

//可以将多个类似的方法封装到一个工具类中,让我们称之为OptionalUtility。
//通过这种方式,你以后就能直接调用OptionalUtility.stringToInt方法,
//将String转换为一个Optional<Integer>对象,
//而不再需要记得你在其中封装了笨拙的try/catch的逻辑了

基础类型的Optional对象,以及为什么应该避免使用它们

:::info

  • 与Stream对象一样,Optional也提供了类似的基础类型—— OptionalInt、OptionalLong以及OptionalDouble
  • 基础类型的Optional不支持map、flatMap以及filter方法,而这些是Optional类最有用的方法
  • 返回的是OptionalInt类型的对象,你就不能将其作为方法引用传递给另一个Optional对象的flatMap方法

:::

public int readDuration(Properties props, String name) {
    String value = props.getProperty(name);
    if (value != null) {       ←---- 确保名称对应的属性存在
        try {
            int i = Integer.parseInt(value);       ←---- 将String属性转换为数字类型
            if (i > 0) {       ←---- 检查返回的数字是否为正数
                return i;
            }
        } catch (NumberFormatException nfe) { }
    }
    return 0;       ←---- 如果前述的条件都不满足,返回0
}

//使用Optional从属性中读取
//OptionalUtility.stringToInt方法的引用,将Optional<String>转换为Optional<Integer>。
//轻易地就可以过滤掉负数。这种方式下,如果任何一个操作返回一个空的Optional对象,
//该方法都会返回orElse方法设置的默认值0;否则就返回封装在Optional对象中的正整数。
public int readDuration(Properties props, String name) {
    return Optional.ofNullable(props.getProperty(name))
                   .flatMap(OptionalUtility::stringToInt)
                   .filter(i -> i > 0)
                   .orElse(0);
}

:::info
Optional和Stream时的那些通用模式。

它们都是对数据库查询过程的反思,查询时,多种操作会被串接在一起执行。

:::

小结

  • null引用在历史上被引入到程序设计语言中,目的是为了表示变量值的缺失。
  • Java 8中引入了一个新的类java.util.Optional,对存在或缺失的变量值进行建模。
  • 你可以使用静态工厂方法Optional.empty、Optional.of以及Optional.ofNullable创建Optional对象。
  • Optional类支持多种方法,比如map、flatMap、filter,它们在概念上与Stream类中对应的方法十分相似。
  • 使用Optional会迫使你更积极地解引用Optional对象,以应对变量值缺失的问题,最终,你能更有效地防止代码中出现不期而至的空指针异常。
  • 使用Optional能帮助你设计更好的API,用户只需要阅读方法签名,就能了解该方法是否接受一个Optional类型的值。

新的日期和时间API

用于以语言无关方式格式化和解析日期或时间的DateFormat方法就只在Date类里有。DateFormat方法也有它自己的问题。比如,它不是线程安全的。这意味着两个线程如果尝试使用同一个formatter解析日期,你可能会得到无法预期的结果。

LocalDate、LocalTime、LocalDateTime、Instant、Duration以及Period

LocalDate date = LocalDate.of(2017, 9, 21);       ←---- 2017-09-21
int year = date.getYear();       ←---- 2017
Month month = date.getMonth();       ←---- SEPTEMBER
int day = date.getDayOfMonth();       ←---- 21
DayOfWeek dow = date.getDayOfWeek();       ←---- THURSDAY
int len = date.lengthOfMonth();       ←---- 30 (days in September)
boolean leap = date.isLeapYear();       ←---- false (not a leap year)
//LocalDate和LocalTime都可以通过解析代表它们的字符串创建。使用静态方法parse
LocalDate date = LocalDate.parse("2017-09-21");
LocalTime time = LocalTime.parse("13:45:20");
//传递的字符串参数无法被解析为合法的LocalDate或LocalTime对象,
//这两个parse方法都会抛出一个继承自RuntimeException的DateTimeParseException异常

//LocalDateTime,是LocalDate和LocalTime的合体。
//它同时表示了日期和时间,但不带有时区信息,
//可以直接创建,也可以通过合并日期和时间对象创建
// 2017-09-21T13:45:20
LocalDateTime dt1 = LocalDateTime.of(2014, Month.SEPTEMBER, 21, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);

//Instant的设计初衷是为了便于机器使用。
//它包含的是由秒及纳秒所构成的数字。
//所以,它无法处理那些非常容易理解的时间单位。比如语句

int day = Instant.now().get(ChronoField.DAY_OF_MONTH);

定义Duration或Period

//所有类都实现了Temporal接口,Temporal接口定义了如何读取和操纵为时间建模的对象的值

//计算两个Instant对象之间的Duration
Duration d1 = Duration.between(time1, time2);
Duration d1 = Duration.between(dateTime1, dateTime2);
Duration d2 = Duration.between(instant1, instant2);
//LocalDateTime和Instant是为不同的目的而设计的,一个是为了便于人阅读使用,
//另一个是为了便于机器处理,因此不能将二者混用


日期–时间类中表示时间间隔的通用方法

方法名 是否是静态方法 方法描述
between 创建两个时间点之间的interval
from 由一个临时时间点创建interval
of 由它的组成部分创建interval的实例
parse 由字符串创建interval的实例
addTo 创建该interval的副本,并将其叠加到某个指定的temporal对象
get 读取该interval的状态
isNegative 检查该interval是否为负值,不包含零
isZero 检查该interval的时长是否为零
minus 通过减去一定的时间创建该interval的副本
multipliedBy interval的值乘以某个标量创建该interval的副本
negated 以忽略某个时长的方式创建该interval的副本
plus 以增加某个指定的时长的方式创建该interval的副本
subtractFrom 从指定的temporal对象中减去该interval

操纵、解析和格式化日期

以比较直观的方式操纵LocalDate的属性

LocalDate date1 = LocalDate.of(2017, 9, 21);       ←---- 2017-09-21
LocalDate date2 = date1.withYear(2011);       ←---- 2011-09-21
LocalDate date3 = date2.withDayOfMonth(25);       ←---- 2011-09-21
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2);       ←---- 2011-02-25
采用更通用的with方法能达到同样的目的
它们都声明于Temporal接口,所有的日期和时间API类都实现这两个方法,它们定义了单点的时间,
比如LocalDate、LocalTime、LocalDateTime以及Instant。更确切地说,使用get和with方法,
可以将Temporal对象值的读取和修改1区分开。如果Temporal对象不支持请求访问的字段,
它就会抛出一个UnsupportedTemporalTypeException异常,
比如试图访问Instant对象的ChronoField.MONTH_OF_YEAR字段,
或者LocalDate对象的ChronoField.NANO_OF_SECOND字段时都会抛出这样的异常。

:::info
注意,使用with方法并不会直接修改现有的Temporal对象,它会创建现有对象的副本并更新对应的字段。这一过程也被称作函数式更新

:::

LocalDate date1 = LocalDate.of(2017, 9, 21);       ←---- 2017-09-21
LocalDate date2 = date1.plusWeeks(1);       ←---- 2017-09-28
LocalDate date3 = date2.minusYears(6);       ←---- 2011-09-28
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);       ←---- 2012-03-28

LocalDate、LocalTime、LocalDateTime以及Instant这样表示时间点的日期–时间类提供了大量通用的方法

方法名 是否是静态方法 描述
from 依据传入的Temporal对象创建对象实例
now 依据系统时钟创建Temporal对象
of Temporal对象的某个部分创建该对象的实例
parse 由字符串创建Temporal对象的实例
atOffset Temporal对象和某个时区偏移相结合
atZone Temporal对象和某个时区相结合
format 使用某个指定的格式器将Temporal对象转换为字符串(Instant类不提供该方法)
get 读取Temporal对象的某一部分的值
minus 创建Temporal对象的一个副本,通过将当前Temporal对象的值减去一定的时长创建该副本
plus 创建Temporal对象的一个副本,通过将当前Temporal对象的值加上一定的时长创建该副本
with 以该Temporal对象为模板,对某些状态进行修改创建该对象的副本

合并日期和时间

//直接创建LocalDateTime对象,或者通过合并日期和时间的方式创建
// 2017-09-21T13:45:20
LocalDateTime dt1 = LocalDateTime.of(2014, Month.SEPTEMBER, 21, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);

机器的日期和时间格式

   从计算机的角度来看,建模时间最自然的格式是表示一个持续时间段上某个点的单一大整型数。这也是新的java.time.Instant类对时间建模的方式,基本上它是以Unix元年时间(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的秒数进行计算。

   静态工厂方法ofEpochSecond传递代表秒数的值创建一个该类的实例。静态工厂方法ofEpochSecond传递代表秒数的值创建一个该类的实例。

   LocalDate及其他为便于阅读而设计的日期-时间类中所看到的那样,Instant类也支持静态工厂方法now,它能够帮你获取当前时刻的时间戳。特别强调一点,Instant的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。所以,它无法处理那些非常容易理解的时间单位
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);
/**
java.time.temporal.UnsupportedTemporalTypeException: Unsupported field:
            DayOfMonth
**/

定义Duration或Period

   我们需要创建两个Temporal对象之间的Duration。Duration类的静态工厂方法between就是为这个目的而设计的。你可以创建两个LocalTime对象、两个LocalDateTime对象,或者两个Instant对象之间的Duration

处理不同的时区和历法

   使用新版日期和时间API时区的处理被极大地简化了。新版java.time.ZoneId类是老版java.util.TimeZone类的替代品。它的设计目标就是要让你无须为时区处理的复杂和烦琐而操心,比如处理夏令时(daylight saving time, DST)这种问题。跟其他日期和时间API类一样,ZoneId类也是无法修改的。
ZoneId romeZone = ZoneId.of("Europe/Rome");
/**
地区ID都为“{区域}/{城市}”的格式,
这些地区集合的设定都由因特网编号分配机构(IANA)的时区数据库提供。
可以通过Java 8的新方法toZoneId将一个老的时区对象转换为ZoneId
**/
ZoneId zoneId = TimeZone.getDefault().toZoneId();

//为时间点添加时区信息
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);

对ZonedDateTime的组成部分进行了说明,相信能够帮助你理解LocaleDate、LocalTime、LocalDateTime以及ZoneId之间的差异。

//将LocalDateTime转换为Instant:
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
Instant instantFromDateTime = dateTime.toInstant(romeZone);

利用和UTC/格林尼治时间的固定偏差计算时区

另一种比较通用的表达时区的方式是利用当前时区和UTC/格林尼治的固定偏差。基于这个理论,“纽约落后于伦敦5小时”。这种情况下,使用ZoneOffset类,它是ZoneId的一个子类,表示的是当前时间和伦敦格林尼治子午线时间的差异

LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(dateTime, newYorkOffset);

ISO-8601日历系统是世界文明日历系统的事实标准。

Java 8中另外还提供了四种其他的日历系统。

每一个都有一个对应的日期类,ThaiBuddhistDate、MinguoDate、JapaneseDate以及HijrahDate。

以上加LocalDate都实现了ChronoLocalDate接口,能够对公历的日期进行建模。

利用LocalDate对象,创建这些类的实例。使用它们提供的静态工厂方法,创建任何一个Temporal对象的实例

Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
ChronoocalDate now = japaneseChronology.dateNow();

//尽量在你的应用中使用LocalDate,包括存储、操作、业务规则的解读;
//不过如果你需要将程序的输入或者输出本地化,那么应该使用ChronoLocalDate类

:::info
HijrahDate(伊斯兰教日历)只能首先可见于沙特阿拉伯,支持HijrahDate这一标准,Java 8中还包括了乌姆库拉(Umm Al-Qura)变量

:::

ISO日历中计算当前伊斯兰年中斋月的起始和终止日期

小结

Java 8之前老版的java.util.Date类以及其他用于建模日期和时间的类有很多不一致及设计上的缺陷,包括易变性以及糟糕的偏移值、默认值和命名。

❏ 新版的日期和时间API中,日期-时间对象是不可变的。

❏ 新的API提供了两种不同的时间表示方式,有效地区分了运行时人和机器的不同需求。

❏ 你可以用绝对或者相对的方式操纵日期和时间,操作的结果总是返回一个新的实例,老的日期-时间对象不会发生变化。

❏ TemporalAdjuster让你能够用更精细的方式操纵日期,不再局限于一次只能改变它的一个值,还可按照需求定义自己的日期转换器。

❏ 可以按照特定的格式需求,定义自己的格式器,打印输出或者解析日期-时间对象。这些格式器可以通过模板创建,也可以编程创建,并且它们都是线程安全

❏ 用相对于某个地区/位置的方式,或者以与UTC/格林尼治时间的绝对偏差的方式表示时区,并将其应用到日期-时间对象上,对其进行本地化。

❏ 使用不同于ISO-8601标准系统的其他日历系统

默认方法

其一,Java 8允许在接口内声明静态方法

其二,Java 8引入了一个新功能,叫默认方法,通过默认方法你可以指定接口方法的默认实现。

:::info
默认方法。List接口中的sort,Collection接口中的stream

引入默认方法的目的:它让类可以自动地继承接口的一个默认实现。

:::

default void sort(Comparator<? super E> c){
    Collections.sort(this, c);
}

Java API这样的类库的演进问题

静态方法及接口

:::info
定义接口以及工具辅助类(companion class)是Java语言常用的一种模式,工具类定义了与接口实例协作的很多静态方法。比如,Collections就是处理Collection对象的辅助类

:::

不同类型的兼容性:二进制、源代码和函数行为

变更对Java程序的影响大体可以分成三种类型的兼容性

:::info
二进制级的兼容、源代码级的兼容,以及函数行为的兼容

:::

二进制级的兼容性表示现有的二进制执行文件能无缝持续链接(包括验证、准备和解析)和运行。

比如,为接口添加一个方法就是二进制级的兼容,这种方式下,如果新添加的方法不被调用,接口已经实现的方法就可以继续运行,不会出现错误。

简单地说,源代码级的兼容性表示引入变化之后,现有的程序依然能成功编译通过。比如,向接口添加新的方法就不是源码级的兼容,因为遗留代码并没有实现新引入的方法,所以它们无法顺利通过编译。

最后,函数行为的兼容性表示变更发生之后,程序接受同样的输入能得到同样的结果。比如,为接口添加新的方法就是函数行为兼容的,因为新添加的方法在程序中并未被调用(抑或该接口在实现中被覆盖了)。

概述默认方法

默认方法由default修饰符修饰,并像类中声明的其他方法一样包含方法体。比如,你可以像下面这样在集合库中定义一个名为Sized的接口,在其中定义一个抽象方法size,以及一个默认方法isEmpty:

:::info
Collection接口的stream方法就是默认方法。List接口的sort方法也是默认方法。

很多函数式接口,比如Predicate、Function以及Comparator也引入了新的默认方法,

比如Predicate.and或者Function.andThen

(记住,函数式接口只包含一个抽象方法,默认方法是种非抽象方法)。

:::

Java 8中的抽象类和抽象接口

:::info
一个类只能继承一个抽象类,但是一个类可以实现多个接口。

其次,一个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变量的。

:::

default boolean removeIf(Predicate<? super E> filter) {
    boolean removed = false;
    Iterator<E> each = iterator();
    while(each.hasNext()) {
        if(filter.test(each.next())) {
            each.remove();
            removed = true;
        }
    }
    return removed;
}

Iterator接口就为remove方法提供了一个默认实现

interface Iterator<T> {
  boolean hasNext();
  T next();
  default void remove() {
    throw new UnsupportedOperationException();
  }
}
//减少无效的模板代码。实现Iterator接口的每一个类都不需要再声明一个空的remove方法了,
//因为它现在已经有一个默认的实现

行为的多继承

默认方法让之前无法想象的事以一种优雅的方式得以实现,即行为的多继承。这是一种让类从多个来源重用代码的能力

Java的类只能继承单一的类,但是一个类可以实现多接口

类型的多继承

ArrayList继承了一个类,实现了四个接口。因此ArrayList实际是七个类型的直接子类,分别是:AbstractList、List、RandomAccess、Cloneable、Serializable、Iterable和Collection。

所以,在某种程度上,JAVA早就有了类型的多继承。

利用正交方法的精简接口

假设需要为正在创建的游戏定义多个具有不同特质的形状。

有的形状需要调整大小,但是不需要有旋转的功能;有的需要能旋转和移动,但是不需要调整大小。

这种情况下,该怎样设计才能尽可能地重用代码?你可以定义一个单独的Rotatable接口,并提供两个抽象方法setRotationAngle和getRotationAngle。

该接口还定义了一个默认方法rotateBy,你可以通过setRotationAngle和getRotationAngle实现该方法.

多继承

复用Moveable和Rotatable接口的默认实现

:::info
假设需要修改moveVertically的实现,让它更高效地运行。

可以在Moveable接口内直接修改它的实现,所有实现该接口的类会自动继承新的代码(这里假设用户并未定义自己的方法实现)。

:::

关于继承的一些错误观点

继承不应该成为一谈到代码复用就试图倚靠的“万精油”。

比如,从一个拥有100个方法及字段的类进行继承就不是个好主意,因为这其实会引入不必要的复杂性。

完全可以使用代理有效地规避这种窘境,即创建一个方法通过该类的成员变量直接调用该类的方法。

:::info
有些类被刻意地声明为final类型:声明为final的类不能被其他的类继承,避免发生这样的反模式,防止核心代码的功能被污染。

注意,有的时候声明为final的类都会有其不同的原因,比如,String类被声明为final,因为不希望有人对这样的核心功能产生干扰。

这种思想同样也适用于使用默认方法的接口。通过精简的接口,你能获得最有效的组合,因为你可以只选择需要的实现。

:::

解决冲突的规则

Java语言中一个类只能继承一个父类,但是一个类可以实现多个接口

默认方法在Java 8中引入,有可能出现一个类继承了多个方法但它们使用的是同样的函数签名

解决问题的三条规则

如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,那么通过三条规则可以进行判断。

(1) 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。

(2) 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。

(3) 最后,如果还是无法判断,那么继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。

选择提供了最具体实现的默认方法的接口

提供最具体的默认方法实现的接口,其优先级更高

按照规则(2),应该选择的是提供了最具体实现的默认方法的接口。由于B比A更具体,因此应该选择B的hello方法。所以,程序会打印输出“Hello from B"

继承一个类,实现两个接口的情况

依据规则(1),类中声明的方法具有更高的优先级。D并未覆盖hello方法,可是它实现了接口A。所以它就拥有了接口A的默认方法。

规则(2)说如果类或者父类没有对应的方法,那么就应该选择提供了最具体实现的接口中的方法。因此,编译器会在接口A和接口B的hello方法之间做选择。因为B更加具体,所以程序会再次打印输出“Hello from B“

冲突及如何显式地消除歧义

public interface A {
  default void hello() {
    System.out.println("Hello from A");
  }
}
public interface B {
  default void hello() {
    System.out.println("Hello from B");
  }
}
public class C implements B, A { }

/*这时规则(2)就无法进行判断了,因为从编译器的角度看没有哪一个接口的实现更加具体,两个都差不多。
A接口和B接口的hello方法都是有效的选项。
所以,Java编译器这时就会抛出一个编译错误,因为它无法判断哪一个方法更合适:
“Error: class C inherits unrelated defaults for hello() from types B and A.
*/

冲突的解决解决这种两个可能的有效方法之间的冲突没有太多方案,只能显式地决定希望在C中使用哪一个方法。为了达到这个目的,可以在类C中覆盖hello方法,在它的方法体内显式地调用你希望调用的方法。

Java 8中引入了一种新的语法X.super.m(...),其中X是你希望调用的m方法所在的父接口

菱形继承问题

这种情况下类D中的默认方法到底继承自什么地方 ——源自B的默认方法,还是源自C的默认方法?实际上只有一个方法声明可以选择。只有A声明了一个默认方法。由于这个接口是D的父接口,因此代码会打印输出“Hello from A”。

解决机制

三条准则就能解决所有可能的冲突。

(1) 首先,类或父类中显式声明的方法,其优先级高于所有的默认方法。

(2) 如果用第一条无法判断,方法签名又没有区别,那么选择提供最具体实现的默认方法的接口。

(3) 最后,如果冲突依旧无法解决,只能在你的类中覆盖该默认方法,显式地指定在类中使用哪一个接口中的方法。

小结

Java 8中的接口可以通过默认方法和静态方法提供方法的代码实现。

❏ 默认方法的开头以关键字default修饰,方法体与常规的类方法相同。

❏ 向发布的接口添加抽象方法不是源码兼容的。

❏ 默认方法的出现能帮助库的设计者以后向兼容的方式演进API。

❏ 默认方法可以用于创建可选方法和行为的多继承。

❏ 我们有办法解决由于一个类从多个接口中继承了拥有相同函数签名的方法而导致的冲突。

❏ 类或者父类中声明的方法的优先级高于任何默认方法。如果前一条无法解决冲突,那就选择同函数签名的方法中实现得最具体的那个接口的方法。

❏ 两个默认方法都同样具体时,你需要在类中覆盖该方法,显式地选择使用哪个接口中提供的默认方法。

模块系统

Java 9中引入的最主要的新特性,模块系统

两个设计模式,即关注点分离(separation of concern, SoC)和信息隐藏(information hiding)

关注点分离

模块是具备内聚特质的一组代码,它与其他模块代码之间很少有耦合

通过模块组织类,可以清晰地表示应用程序中类与类之间的可见性关系

Java 9的模块能提供粒度更细的控制,可设定哪个类能够访问哪个类,并且这种控制是编译期检查的。而Java的包并未从本质上支持模块化

:::info
好处包括:

❏ 使各项工作独立开展,减少组件间相互依赖,从而便于团队合作完成项目;

❏ 有利于推动组件重用;

❏ 系统整体的维护性更好。

:::

信息隐藏

隐藏内部实现细节能帮你减少局部变更对程序其他部分的影响,从而有效地避免变更传递

Java 9出现之前,编译器无法依据语言结构判断某个类或者包仅供某个特定目标访问

模块化的局限性

  • 有限的可见性控制
  • 类的路径
    • Java 9之前,无论是Java还是JVM都不支持显式地声明依赖。这些问题碰到一起就产生了“JAR地狱”或“类路径地狱”的问题。这些问题的直接结果就是不停地在类路径上添加和删除类文件,希望能通过实验找出合适的搭配,让JVM顺利地执行应用,不再抛出让人头疼的ClassNotFound Exception

Java模块:全局视图

Java 9为Java程序提供了一个新的单位:模块。模块通过一个新的关键字[插图]module声明,紧接着是模块的名字及它的主体。这样的模块描述符(module descriptor)定义在一个特殊文件,即module-info.java中,最终被编译为module-info.class。模块描述符的主体包含一系列的子句,其中最重要的两个子句是requires和exports。requires子句用于指定执行模块还需要哪些模块的支持,exports子句声明了模块中哪些包可以被其他模块访问和使用

Java模块描述符的核心结构(module-info.java)

模块中的exports和requires看作相互独立的部分,就像拼图游戏(这可能也是Jigsaw项目名称的起源)中的凸块(或者标签)与凹块的关系,对理解模块是非常有益的。

使用多个模块

一个采用拼图游戏风格构建的Java系统,它由四个模块(A、B、C、D)组成。模块A依赖于模块B和模块C,需要访问包pkgB和包pkgC(分别由模块B和模块C导出)。模块C同样需要使用pkgD,因此它对模块D有依赖性,不过模块B不需要使用pkgD

:::info
使用Maven这样的构建工具时,模块描述之类的细节都被集成开发环境解决了,用户也就看不到这些琐碎的事情了

:::

posted @ 2024-12-18 15:21  李好秀  阅读(20)  评论(0编辑  收藏  举报