这一部分主要介绍了Java 8的一些其他新特性,如默认方法,Optional,CompletableFuture,新的时间日期API等。并且介绍了由于引入了lambda和stream后,我们该如何重构、测试和调试代码。
学习资料主要参考: 《Java 8 In Action》、《Java 8实战》,以及其源码:Java8 In Action
- 重构、测试和调试
- 默认方法
- 用Optional代替null
- CompletableFuture:组合式异步编程
- 新的日期和时间API
重构、测试和调试
1. 为改善可读性和灵活性重构代码
1)可读性
-
从匿名类到Lambda表达式的转换
匿名类是极其繁琐且容易出错的。
例子:
Runnable r1 = new Runnable(){ public void run(){ System.out.println("Hello"); } }; Runnable r2 = () -> System.out.println("Hello");
注意点:
- 匿名类和Lambda表达式中的this和super的含义是不同的。在匿名类中,this代表的是类自身,但是在Lambda中,它代表的是包含类
- 匿名类可以屏蔽包含类的变量,而Lambda表达式不能
- 在涉及重载的上下文里,将匿名类转换为Lambda表达式可能导致最终的代码更加晦涩。(可使用显式的类型转换解决因重载而无法确定方法的问题)
-
从Lambda表达式到方法引用的转换
将Lambda表达式的内容抽取到一个单独的方法中,将其作为参数传递。既清晰,又可重用。
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(groupingBy(Dish::getCaloricLevel));
还应该尽量考虑使用静态辅助方法(如comparing、maxBy),
//before inventory.sort( (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); //after inventory.sort(comparing(Apple::getWeight));
通用的归约操作(如sum、maximum)。
// before int totalCalories = menu.stream().map(Dish::getCalories) .reduce(0, (c1, c2) -> c1 + c2); // after int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
-
从命令式的数据处理切换到Stream
将所有使用迭代器这种数据处理模式处理集合的代码都转换成Stream API的方 式。 Stream API能更清晰地表达数据处理管道的意图。
例子:
// before List<String> dishNames = new ArrayList<>(); for(Dish dish: menu){ if(dish.getCalories() > 300){ dishNames.add(dish.getName()); } } // after menu.parallelStream() .filter(dish -> dish.getCalories() > 300) .map(Dish::getName) .collect(tList());
注意点:需要考虑控制流语句,比如break、continue、return,并选择使用恰当的流操作。
2)灵活性
要使用lambda,必然要采用函数接口。
引入函数接口的两种模式:
-
有条件的延迟执行
控制语句被混杂在业务逻辑代码之中,如:安全性检查以及日志输出。
如果需要在代码中频繁的判断一个对象的状态,而只是为了传递参数、调用该对象的某个方法,可以实现一个新的方法,以lambda作为参数,在新方法中进行判断。这样子,代码会更易读,结构更清晰,封装性也更好。
-
环绕执行
定义:业务代码千差万别,但是准备与清理阶段一致。
用法:将业务代码用lambda重写。
优点:可以重用准备、清理阶段的代码。
String oneLine = processFile((BufferedReader b) -> b.readLine()); String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine()); //相同的准备、清理阶段 public static String processFile(BufferedReaderProcessor p) throws IOException { try(BufferedReader br = new BufferedReader(new FileReader("java8inaction/chap8/data.txt"))){ return p.process(br); } } //定义接口 public interface BufferedReaderProcessor{ String process(BufferedReader b) throws IOException; }
2. 使用Lambda重构面向对象的设计模式
1)策略模式
策略模式代表了解决一类算法的通用解决方案,你可以在运行时选择使用哪种方案。
直接将策略接口当作函数接口,使用lambda代替繁杂的策略接口实现类。
例子:验证字符串是否合法
-
准备工作
策略接口
public interface ValidationStrategy { boolean execute(String s); }
调用类
public class Validator{ private final ValidationStrategy strategy; public Validator(ValidationStrategy v){ this.strategy = v; } public boolean validate(String s){ return strategy.execute(s); } }
-
接口实现类的实现与调用
实现
public class IsAllLowerCase implements ValidationStrategy { public boolean execute(String s){ return s.matches("[a-z]+"); } } public class IsNumeric implements ValidationStrategy { public boolean execute(String s){ return s.matches("\\d+"); } }
调用
Validator numericValidator = new Validator(new IsNumeric()); boolean b1 = numericValidator.validate("aaaa"); Validator lowerCaseValidator = new Validator(new IsAllLowerCase ()); boolean b2 = lowerCaseValidator.validate("bbbb");
-
lambda实现(无需创建接口实现类)
Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+")); boolean b1 = numericValidator.validate("aaaa"); Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+")); boolean b2 = lowerCaseValidator.validate("bbbb");
2)模板方法
如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,那么采用模板方法设计模式是比较通用的方案。
1.改写前
abstract class OnlineBanking {
public void processCustomer(int id){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy(c);
}
abstract void makeCustomerHappy(Customer c;
}
这个抽象类构建了一个模板,所有的子类都要实现makeCustomerHappy,来面对差异化的需求。
- 改写后
首先添加一个重载的新方法,它多传入一个Consumer接口作为参数
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){ Customer c = Database.getCustomerWithId(id); makeCustomerHappy.accept(c); }
传入lambda
new OnlineBankingLambda().processCustomer(1337, (Customer c) -> System.out.println("Hello " + c.getName());
3)观察者模式
某些事件发生时(比如状态转变),如果一个对象(通常我们称之为主题Subject)需要自动地通知其他多个对象(称为观察者Observer)。
- 准备
interface Subject{ void registerObserver(Observer o); void notifyObservers(String tweet); } class Feed implements Subject{ private final List<Observer> observers = new ArrayList<>(); public void registerObserver(Observer o) { this.observers.add(o); } public void notifyObservers(String tweet) { observers.forEach(o -> o.notify(tweet)); } } interface Observer { void notify(String tweet); }
-
改写前
实现三个观察者
class NYTimes implements Observer{ public void notify(String tweet) { if(tweet != null && tweet.contains("money")){ System.out.println("Breaking news in NY! " + tweet); } } } class Guardian implements Observer{ public void notify(String tweet) { if(tweet != null && tweet.contains("queen")){ System.out.println("Yet another news in London... " + tweet); } } } class LeMonde implements Observer{ public void notify(String tweet) { if(tweet != null && tweet.contains("wine")){ System.out.println("Today cheese, wine and news! " + tweet); } } }
调用
Feed f = new Feed(); f.registerObserver(new NYTimes()); f.registerObserver(new Guardian()); f.registerObserver(new LeMonde()); f.notifyObservers("The queen said her favourite book is Java 8 in Action!");
-
改写后
无需显式地实例化三个观察者对象,直接传递Lambda表达式表示需要执行的行为即可。
f.registerObserver((String tweet) -> { if(tweet != null && tweet.contains("money")){ System.out.println("Breaking news in NY! " + tweet); } }); f.registerObserver((String tweet) -> { if(tweet != null && tweet.contains("queen")){ System.out.println("Yet another news in London... " + tweet); } });
注意:如果观察者的逻辑十分复杂,或者持有了状态,抑或定义了多个方法,诸如此 类。在这些情形下,还是应该继续使用类的方式。
4)责任链模式
责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。
-
改写前
构建抽象类
public abstract class ProcessingObject<T> { protected ProcessingObject<T> successor; public void setSuccessor(ProcessingObject<T> successor){ this.successor = successor; } public T handle(T input){ T r = handleWork(input); if(successor != null){ return successor.handle(r); } return r; } abstract protected T handleWork(T input); }
实现类
public class HeaderTextProcessing extends ProcessingObject<String> { public String handleWork(String text){ return "From Raoul, Mario and Alan: " + text; } } public class SpellCheckerProcessing extends ProcessingObject<String> { public String handleWork(String text){ return text.replaceAll("labda", "lambda"); } }
调用
ProcessingObject<String> p1 = new HeaderTextProcessing(); ProcessingObject<String> p2 = new SpellCheckerProcessing(); p1.setSuccessor(p2); String result = p1.handle("Aren't labdas really sexy?!!"); System.out.println(result);
-
改写后
可以直接使用UnaryOperator
UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text; UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda"); Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing); String result = pipeline.apply("Aren't labdas really sexy?!!")
5)工厂模式
使用工厂模式,你无需向客户暴露实例化的逻辑就能完成对象的创建。
- 改写前
public class ProductFactory {
public static Product createProduct(String name){
switch(name){
case "loan": return new Loan();
case "stock": return new Stock();
case "bond": return new Bond();
default: throw new RuntimeException("No such product " + name);
}
}
}
- 改写后
public class ProductFactory {
final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
map.put("loan", Loan::new);
map.put("stock", Stock::new);
map.put("bond", Bond::new);
}
public static Product createProduct(String name){
Supplier<Product> p = map.get(name);
if(p != null) return p.get();
throw new IllegalArgumentException("No such product " + name);
}
}
3. 测试 Lambda 表达式
测试lambda表达式的正确性时,没必要对Lambda表达式本身进行测试,只需要关注其结果是否正确即可。
4. 调试
1)查看栈跟踪
由于Lambda表达式没有名字, 编译器只能为它们指定一个名字,比如lambda$main$0。
即使你使用了方法引用,还是有可能出现栈无法显示你使用的方法名的情况。
如果方法引用指向的是同一个类中声明的方法,那么它的名称是可以在栈跟踪中显示的。
涉及Lambda表达式的栈跟踪可能非常难理解。这是Java编译器未来版本可以改进的一个方面。
2)输出日志
流提供的 peek 方法在分析Stream流水线时,能将中间变量的值输出到日志中,是非常有用的工具。
peek 的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作。但是它不像 forEach 那样恢复整个流的运行,而是在一个元素上完成操作之后,它只会将操作顺承到流水线中的下一个操作。
例子
List<Integer> numbers = Arrays.asList(2, 3, 4, 5);
List<Integer> result =
numbers.stream()
.peek(x -> System.out.println("from stream: " + x))
.map(x -> x + 17)
.peek(x -> System.out.println("after map: " + x))
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("after filter: " + x))
.limit(3)
.peek(x -> System.out.println("after limit: " + x))
.collect(toList());
输出
from stream: 2
after map: 19
from stream: 3
after map: 20
after filter: 20
after limit: 20
from stream: 4
after map: 21
from stream: 5
after map: 22
after filter: 22
after limit: 22
默认方法
1. API的演进
当一个API接口发布后,它的后续演进会给用户带来一系列问题,比如在原先接口中添加一个新的方法,那么所有用户都需要在接口的实现类中添加这个方法的实现,这是大部分用户不想看到的。
2. 概述默认方法
默认方法由 default 修饰符修饰, 并像类中声明的其他方法一样包含方法体,它会作为接口的一部分由实现类继承。
由于引入了默认方法,就能够以兼容的方式演进库函数。
Java8中抽象类与抽象接口区别
- 一个类只能继承单个抽象类,但是可以实现多个接口
- 抽象类中可以有实例变量,接口中不能。
3. 使用
1)可选方法
类继承了接口,但是对某些接口方法的实现留白,所以代码中会存在很多无用的代码。那么有了默认方法后,就可以对这些方法提供一个默认实现,这样子实体类就无需写上一个空方法。
2)行为的多继承
我们都知道一个类是可以实现多接口的,那么在引入默认方法后,在不同接口中都可以存在一些默认方法。这样子,我们通过组合接口,就可以最大程度地实现代码复用和行为组合。
4. 如何解决冲突
如果一个类实现多个接口,而多个接口中有重名的默认方法时,会发生什么事呢?
看看下面这段代码会输出什么?
public interface A {
default void hello() {
System.out.println("Hello from A");
}
}
public interface B extends A {
default void hello() {
System.out.println("Hello from B");
}
}
public class C implements B, A {
public static void main(String... args) {
new C().hello();
}
}
1)解决问题的三条规则
- 类中的方法优先级最高
- 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
- 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。
2)应用前两条规则
那么回过头来,上面那段代码输出结果应该是什么呢?
答案是: Hello from B。
因为按照规则(2),应该选择的是提供了最具体实现的默认方法的接口。由于B比A更具体,所以应该选择B的hello方法。
继续看,如果是下列代码输出会是什么呢?
public class D implements A{ }
public class C extends D implements B, A {
public static void main(String... args) {
new C().hello();
}
}
答案依然是:Hello from B。
依据规则(1),类中声明的方法具有更高的优先级。D并未覆盖hello方法,可是它实现了接口A。所以它就拥有了接口A的默认方法。规则(2)说如果类或者父类没有对应的方法,那么就应该选择提供了最具体实现的接口中的方法。因此,编译器会在接口A和接口B的hello方法之间做选择。由于B更加具体,所以程序会再次打印输出“Hello from B”。
3)冲突及如何显式地消除歧义
public interface A {
void hello() {
System.out.println("Hello from A");
}
}
public interface B {
void hello() {
System.out.println("Hello from B");
}
}
public class C implements B, A { }
对于以上代码,Java编译器会抛出一个编译错误,因为它无法判断哪一个方法更合适:“Error: class Cinherits unrelated defaults for hello() from types Band A.”
冲突的解决
Java 8中引入了一种新的语法X.super.m(…),意思即为你希望调用的父接口X中的m方法。
public class C implements B, A {
void hello(){
B.super.hello();
}
}
4)菱形继承问题
public interface A{
default void hello(){
System.out.println("Hello from A");
}
}
public interface B extends A { }
public interface C extends A { }
public class D implements B, C {
public static void main(String... args) {
new D().hello();
}
}
B和C继承了A,也同时继承了A的默认方法,所以会打印:“Hello from A”。
想象以下情况:
- 如果B中提供相同签名的默认方法hello
- 如果B和C都使用相同的函数签名声明了hello方法
- 如果在C接口中添加一个抽象的hello方法(这次添加的不是一个默认方法)
分析结果如下:
- B中提供了更加具体的实现,所以会调用B中的方法
- B和C优先级相同,需要显式调用
- 需要在D中实现hello方法,否则无法通过编译
用Optional代替null
nullPointExcption 绝对是java成员常见的异常。所以为了程序的正常运行,我们不得不在使用某个值之前对它进行非null的check。但是这个样子的代码会变得臃肿,丧失易读性,而且难以维护。
1. null 带来的种种问题
- 它是错误之源
- 它会使你的代码膨胀
- 它自身是毫无意义的
- 它破坏了Java的哲学(避免让程序员意识到指针的存在,唯一的例外是: null 指针)
- 它在Java的类型系统上开了个口子。 null并不属于任何类型。
2. 使用Optional
汲取Haskell和Scala的灵感,Java 8中引入了一个新的类java.util.Optional
public class Person {
private Optional<Car> car;
public Optional<Car> getCar() { return car; }
}
public class Car {
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance {
private String name;
public String getName() { return name; }
}
注意:引入Optional类的意图并非要消除每一个null引用。与此相反,它的目标是帮助你更好地设计出普适的API,让程序员看到方法签名,就能了解它是否接受一个Optional的值。这种强制会让你更积极地将变量从Optional中解包出来,直面缺失的变量值。
3. 应用Optional的几种模式
1)创建Optional对象
// 声明一个空的Optional
Optional<Car> optCar = Optional.empty();
// 依据一个非空值创建Optional
Optional<Car> optCar = Optional.of(car);
// 可接受null的Optional
Optional<Car> optCar = Optional.ofNullable(car);
2)使用map从Optional对象中提取和转换值
// before
String name = null;
if(insurance != null){
name = insurance.getName();
}
//After
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
3)使用flatMap链接Optional对象
// before
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
//After
public String getCarInsuranceName(Optional<Person> person) {
return person.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown");
}
注意: 由于Optional类设计时就没特别考虑将其作为类的字段使用,所以它也并未实现Serializable接口,所以它们无法序列化。
4)默认行为及解引用Optional对象
以下是Optional中的一些方法:
- get():如果变量存在,它直接返回封装的变量值,否则就抛出一个NoSuchElementException异常。
- orElse(T other): 在Optional对象不包含值时提供一个默认值
- orElseGet(Supplier<? extends T> other)是orElse方法的延迟调用版,Supplier方法只有在Optional对象不含值时才执行调用。
- orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常类似,它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希望抛出的异常类型。
- ifPresent(Consumer<? super T>)让你能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作。
5)两个Optional对象的组合
比如现在有一个方法,找出最便宜的保险公司。
public Insurance findCheapestInsurance(Person person, Car car) {
// 不同的保险公司提供的查询服务
// 对比所有数据
return cheapestCompany;
}
如果想要一个null-安全的版本的方法,它接受两个Optional对象作为参数,返回值是一个Optional
使用isPresent()方法的实现:
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
if (person.isPresent() && car.isPresent()) {
return Optional.of(findCheapestInsurance(person.get(), car.get()));
} else {
return Optional.empty();
}
}
上述方法可以实现,但是还是在和以前一样,在做值的check。
更好的实现方式:
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
6)使用filter剔除特定的值
// before
Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){ System.out.println("ok");
}
// after
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance ->
"CambridgeInsurance".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));
4. 使用Optional的实战示例
1)用Optional封装可能为null的值
举个例子,当从map中获取某个key对应的value时,如果map中找不到这个key,就会返回一个null。如果为此,我们加上if/else,无疑会使代码变得臃肿。Optional此时就是一个好的选择。
// before
Object value = map.get("key");
// after
Optional value = Optional.ofNullable(map.get("key"));
2)异常与Optional的对比
有时候,Java API会以一个异常来代替返回null,这个时候我们就不得不在调用后,加上一个try/catch,这无疑增加了代码复杂度。然而为了向后兼容,Java API难以更改,所以可以自己构造一个方法,在异常时,返回Optional。
举个Integer.parseInt的例子,可以如此改写:
public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
注意:基础类型的Optional对象,应该避免使用它们,因为它们无法使用map、flatmap和filter方法。
3)结合以上
- 创建一个类
Properties props = new Properties(); props.setProperty("a", "5"); props.setProperty("b", "true"); props.setProperty("c", "-3");
- 创建一个方法,它在value是正整数的字符串时,返回正整数的值,否则返回0
public int readDuration(Properties props, String name)
- 原始readDuration实现方式
public int readDuration(Properties props, String name){ String value = props.getProperty(name); if(value != null){ try{ int i = Integer.valueOf(value); if(i > 0){ return i; } } catch (NumberFormatException nfe){} } return 0; }
- 使用Optional的readDuration实现方式
public int readDuration(Properties props, String name){ return Optional.ofNullable(props.getProperty(name)) .flatMap(OptionalUtility::stringToInt) .filter(i -> i > 0) .orElse(0); }
CompletableFuture:组合式异步编程
在进行软件设计时,有两大趋势:
- 与硬件条件结合,即提高并行能力
- 与其他应用交互,即提高并发能力
对于第一点,Java 7引入了fork/join框架,可以将任务拆分为子任务,Java 8中引入并行流。
对于第二点,问题的本质其实就是该如何处理线程阻塞情况。因为阻塞时,CPU资源是完全浪费的。那在java中是怎么处理呢?
1. Future接口
1)是什么?怎么用?
Java 5中引入了Futrue接口。
它是什么呢?
简单说,它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。
用代码语言讲就是,将耗时的操作封装在一个Callable对象中,再将它提交给 ExecutorService,然后就无需干涉了。
ExecutorService executor = Executors.newCachedThreadPool();
// submit callable 到ExecutorService中,获得future引用
Future<Double> future = executor.submit(new Callable<Double>() {
public Double call() {
return doSomeLongComputation();
}});
// 在执行callable时,可以做其他事情
doSomethingElse();
// 获取异步操作的结果,如果阻塞,最长等待1s
try {
Double result = future.get(1, TimeUnit.SECONDS);
} catch (ExecutionException ee) {
// 计算抛出一个异常
} catch (InterruptedException ie) {
// 当前线程在等待过程中被中断
} catch (TimeoutException te) {
// 在Future对象完成之前超过已过期
}
2)局限性
两个方面
- 不够简洁
- 很难表述清楚各个future之间的依赖关系
一些难以解决的场景
- 将两个异步计算合并为一个,这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果。
- 等待Future集合中的所有任务都完成
- 仅等待Future集合中最快结束的任务完成(有可能因为它们试图通过不同的方式计算同一个值),并返回它的结果。
- 通过编程方式完成一个Future任务的执行(即以手工设定异步操作结果的方式)
- 应对Future的完成事件(即当Future的完成事件发生时会收到通知,并能使用Future计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结果)。
然而,Java 8中CompletableFuture的引入可以使以上皆变为可能。
2. 使用CompletableFuture构建异步应用
1)同步API与异步API
同步API:
你调用了某个方法,调用方在被调用方运行的过程中会等待,被调用方运行结束返回,调用方取得被调用方的返回值并继续运行,即阻塞式调用
异步API:
直接返回,或者至少在被调用方计算完成之前,将它剩余的计算任务交给另一个线程去做,该线程和调用方是异步的,即非阻塞式调用
2)例子
一起来构建”最佳价格查询器”(best-price-finder)的应用。它会查询多个在线商店,依据给定的产品或服务找出最低的价格。
- 定义API
public class Shop { public double getPrice(String product) { // 待实现 } }
- 添加模拟延迟
public static void delay() { try { Thread.sleep(1000L); } catch (InterruptedException e) { throw new RuntimeException(e); } }
- getPrice方法会调用delay方法,并返回一个随机计算的值
public double getPrice(String product) { return calculatePrice(product); } private double calculatePrice(String product) { delay(); return random.nextDouble() * product.charAt(0) + product.charAt(1); }
显然这一步的实现,就是一个同步式API的实现,所有的操作都会为等待同步事件完成而等待1秒钟,这是无法接受的。
- 将同步方法转换为异步方法
public Future<Double> getPriceAsync(String product) { CompletableFuture<Double> futurePrice = new CompletableFuture<>(); new Thread( () -> { double price = calculatePrice(product); futurePrice.complete(price); }).start(); return futurePrice; }
这段代码,创建了一个代表异步计算的CompletableFuture对象实例,它在计算完成时会包含计算的结果。
代码创建了另一个线程去执行实际的价格计算工作,不等该耗时计算任务结束,直接返回一个Future实例。
当请求的产品价格最终计算得出时,你可以使用它的complete方法,结束completableFuture对象的运行,并设置变量的值。
- 使用异步API
Shop shop = new Shop("BestShop"); long start = System.nanoTime(); Future<Double> futurePrice = shop.getPriceAsync("my favorite product"); long invocationTime = ((System.nanoTime() - start) / 1_000_000); System.out.println("Invocation returned after " + invocationTime + " msecs"); // 执行更多任务,比如查询其他商店 doSomethingElse(); // 在计算商品价格的同时 try { double price = futurePrice.get(); System.out.printf("Price is %.2f%n", price); } catch (Exception e) { throw new RuntimeException(e); } long retrievalTime = ((System.nanoTime() - start) / 1_000_000); System.out.println("Price returned after " + retrievalTime + " msecs");
3)错误处理
如果计算价格时,内部产生了错误,而这些异常呢,会被限制在当前线程范围,最终会导致杀死该线程。于是get方法将永远阻塞。
那该怎么办呢?
简单想法是使用get方法的重载版,设置超时参数,这样子就可以解决永远阻塞的问题。
但是内部到底发生了什么问题呢?还是无从查起。
所以引入了新的方法:completeExceptionally,它可以将导致CompletableFuture内发生问题的异常抛出。
public Future<Double> getPriceAsync(String product) {
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread( () -> {
try {
double price = calculatePrice(product);
futurePrice.complete(price);
} catch (Exception ex) {
futurePrice.completeExceptionally(ex);
}
}).start();
return futurePrice;
}
当运行时报错时,客户端会收到一个 ExecutionException 异常,该异常接收了一个包含失败原因的Exception 参数。
这样子我们就可以看到报错信息了。
4)更进一步:使用工厂方法supplyAsync创建CompletableFuture
public Future<Double> getPriceAsync(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
supplyAsync方法接受一个生产者(Supplier)作为参数,返回一个CompletableFuture对象,该对象完成异步执行后会读取调用生产者方法的返回值。
3. 让代码免受阻塞之苦
1)准备与尝试
- 定义一个商家列表
List<Shop> shops = Arrays.asList(new Shop("BestPrice"), new Shop("LetsSaveBig"), new Shop("MyFavoriteShop"), new Shop("BuyItAll"));
- 顺序通过产品名称获取价格
public List<String> findPrices(String product) { return shops.stream() .map(shop -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product))) .collect(toList()); }
-
验证 findPrices 的正确性和执行性能
long start = System.nanoTime(); System.out.println(findPrices("myPhone27S")); long duration = (System.nanoTime() - start) / 1_000_000; System.out.println("Done in " + duration + " msecs");
输出:
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price is 214.13, BuyItAll price is 184.74]
Done in 4032 msecs
可以发现,执行时间为4s多一些,因为shop.getPrice(product)方法是顺序执行的,每个方法都sleep了1s。
那么该如何改善呢?
2)使用并行流对请求进行并行操作
回忆stream中,有一个现成的并行操作:parallelStream。试试看。
public List<String> findPrices(String product) {
return shops.parallelStream()
.map(shop -> String.format("%s price is %.2f",
shop.getName(), shop.getPrice(product)))
.collect(toList());
}
输出:
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price is 214.13, BuyItAll price is 184.74]
Done in 1180 msecs
效果很不错,只花了1s多一点。能不能更好呢?
3)使用 CompletableFuture 发起异步请求
public List<String> findPrices(String product) {
// 使用工厂方法 supplyAsync 创建 CompletableFuture 对象
List<CompletableFuture<String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.supplyAsync(
() -> shop.getName() + " price is " +
shop.getPrice(product)))
.collect(Collectors.toList());
// 等待所有异步操作结束
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(toList());
}
以上的操作使用了两个不同的stream流水线,而不是把两个map合在一起。这是为什么呢?
因为流操作之间是有延时特性的。如果两个map连接在一起,新的 CompletableFuture对象只有在前一个操作完全结束之后,才能创建。这个方法就变成了顺序执行了。
输出:
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price is 214.13, BuyItAll price is 184.74]
Done in 2005 msecs
结果还没有并行流好!
继续改进
4)寻找更好的方案
并行流的版本工作得非常好,那是因为它能并行地执行四个任务,所以它几乎能为每个商家分配一个线程。
所以如果是五个商家呢?
顺序流处理时间为:5s+,并行流处理时间为2s+,CompletableFuture版本时间为:2s+。
随着商店数目增加,并行流和CompletableFuture之间的性能差别不大。
究其原因,是因为:它们内部采用的是同样的通用线程池,默认都使用固定数目的线程,具体线程数取决于 Runtime.getRuntime().availableProcessors() 的返回值。
然而,CompletableFuture更有优势,因为它可以定制化执行器( Executor )。
5)定制执行器
线程池大小与处理器的利用率之比
线程池大小 = 处理器的核的数目 * 期望的CPU利用率(该值应该介于0和1之间) * ( 1 + 等待时间与计算时间的比率 )
为“最优价格查询器”应用定制的执行器
private final Executor executor =
Executors.newFixedThreadPool(Math.min(shops.size(), 100),
new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
// 使用守护线程,不会阻止程序的关停
t.setDaemon(true);
return t;
}
});
该怎么使用这个executor呢?只需要把它当做第二个参数传入supplayAsync中即可。
CompletableFuture.supplyAsync(() -> shop.getName() + " price is " + shop.getPrice(product), executor);
使用流还是 CompletableFutures ?
- 计算密集型,推荐stream,实现简单,同时效率也可能是最高的。
- 涉及I/O操作,使用CompletableFuture 灵活性更好,可以依据等待/计算,或者 W/C的比率设定需要使用的线程数。这种情况不使用并行流的另一个原因是,处理流的流水线中如果发生I/O等待,流的延迟特性会让我们很难判断到底什么时候触发了等待。
4. 对多个异步任务进行流水线操作
1)准备工作
如果所有商店都使用一个折扣服务,
public class Discount {
public enum Code {
NONE(0), SILVER(5), GOLD(10), PLATINUM(15), DIAMOND(20);
private final int percentage;
Code(int percentage) {
this.percentage = percentage;
}
}
// Discount类的其他实现
}
修改getPrice方法,现在以 Shop-Name:price:DiscountCode 的格式返回一个 String 类型的值。
public String getPrice(String product) {
double price = calculatePrice(product);
Discount.Code code = Discount.Code.values()[
random.nextInt(Discount.Code.values().length)];
return String.format("%s:%.2f:%s", name, price, code);
}
private double calculatePrice(String product) {
delay();
return random.nextDouble() * product.charAt(0) + product.charAt(1);
}
2)实现折扣服务
- 创建Quote 类,对商店返回字符串的解析操作进行封装。
public class Quote { private final String shopName; private final double price; private final Discount.Code discountCode; public Quote(String shopName, double price, Discount.Code code) { this.shopName = shopName; this.price = price; this.discountCode = code; } public static Quote parse(String s) { String[] split = s.split(":"); String shopName = split[0]; double price = Double.parseDouble(split[1]); Discount.Code discountCode = Discount.Code.valueOf(split[2]); return new Quote(shopName, price, discountCode); } public String getShopName() { return shopName; } public double getPrice() { return price; } public Discount.Code getDiscountCode() { return discountCode; } }
-
在Discount中实现applyDiscount 方法,它接收一个 Quote 对象,返回一个字符串,表示生成该 Quote 的 shop 中的折扣价格。
public class Discount { public enum Code { // 省略...... } public static String applyDiscount(Quote quote) { return quote.getShopName() + " price is " + Discount.apply(quote.getPrice(), quote.getDiscountCode()); } private static double apply(double price, Code code) { delay(); return format(price * (100 - code.percentage) / 100); } }
-
使用 Discount 服务
public List<String> findPrices(String product) { return shops.stream() .map(shop -> shop.getPrice(product)) .map(Quote::parse) .map(Discount::applyDiscount) .collect(toList()); }
-
构造同步和异步操作
使用 CompletableFuture 实现 findPrices 方法
public List<String> findPrices(String product) { List<CompletableFuture<String>> priceFutures = shops.stream().map(shop -> CompletableFuture.supplyAsync( () -> shop.getPrice(product), executor)) .map(future -> future.thenApply(Quote::parse)) .map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync( () -> Discount.applyDiscount(quote), executor))) .collect(toList()); return priceFutures.stream() .map(CompletableFuture::join) .collect(toList()); }
为了构建priceFutures,分了三步走。
第一步异步调用getPrice方法;
第二步同步调用Quote的parse方法,因为它本身不会产生I/O操作,所以它可以同步操作。用CompletableFuture对象的thenApply直接调用。
第三步异步调用applyDiscount方法。为了以级联的方式串接起两个异步操作进行工作,Java 8的CompletableFuture API中的thenCompose的方法就是为了这个目的而生。
同时API也提供了thenComposeAsync方法,它的意思是将后续的任务在新的线程上运行。
-
将两个CompletableFuture对象整合起来,无论它们是否存在依赖
上一步中使用了thenCompose方法,使用它的场景简单来说,是当第二个CompletableFuture需要上个CompletableFuture结果时。
但是有时候,我们也需要将两个完全不相干的CompletableFuture对象的结果整合起来,而且你也不希望等到第一个任务完全结束才开始第二项任务。
这个时候应该使用thenCombine方法。
我们再添加一个异步操作,让它去查询汇率。
Future<Double> futurePriceInUSD = CompletableFuture.supplyAsync(() -> shop.getPrice(product)) .thenCombine( CompletableFuture.supplyAsync( () -> exchangeService.getRate(Money.EUR, Money.USD)), (price, rate) -> price * rate );
它接收名为BiFunction的第二参数,这个参数定义了当两个CompletableFuture对象完成计算后,结果如何合并。
API也提供了Async版本,它会导致BiFunction中定义的合并操作被提交到线程池中,由另一个任务以异步的方式执行。
5. 响应CompletableFuture的completion事件
现实世界中,请求的延时都是不固定的,写代码来模拟一个。
private static final Random random = new Random();
public static void randomDelay() {
// 0.5s ~ 2.5s
int delay = 500 + random.nextInt(2000);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
现有代码的实现是当所有商店返回结果时,才显示价格。但是如果希望的效果是:只要有商店返回商品价格就在第一时间显示返回值,不管其他商店。
1)尝试
重构findPrices方法返回一个由Future构成的流
public Stream<CompletableFuture<String>> findPricesStream(String product) {
return shops.stream()
.map(shop -> CompletableFuture.supplyAsync(
() -> shop.getPrice(product), executor))
.map(future -> future.thenApply(Quote::parse))
.map(future -> future.thenCompose(quote ->
CompletableFuture.supplyAsync(
() -> Discount.applyDiscount(quote), executor)));
}
Java 8的CompletableFuture通过thenAccept方法提供了:在每个CompletableFuture上注册一个操作,该操作会在CompletableFuture完成执行后使用它的返回值。
如下,我们想打印一下返回值。
findPricesStream("myPhone").map(f -> f.thenAccept(System.out::println));
类似thenCombine,thenAccept 也存在异步版: thenAcceptAsync,可以异步对结果进行消费。
thenAccept方法会返回一个CompletableFuture
CompletableFuture[] futures = findPricesStream("myPhone")
.map(f -> f.thenAccept(System.out::println))
.toArray(size -> new CompletableFuture[size]);
CompletableFuture.allOf(futures).join();
allOf工厂方法接收一个由CompletableFuture构成的数组,数组中的所有CompletableFuture对象执行完成之后,它返回一个CompletableFuture
如果希望只要CompletableFuture对象数组中有任何一个执行完毕就不再等待,可以使用一个类似的工厂方法anyOf。
2)付诸实践
long start = System.nanoTime();
CompletableFuture[] futures = findPricesStream("myPhone27S")
.map(f -> f.thenAccept(
s -> System.out.println(s + " (done in " +
((System.nanoTime() - start) / 1_000_000) + " msecs)")))
.toArray(size -> new CompletableFuture[size]);
CompletableFuture.allOf(futures).join();
System.out.println("All shops have now responded in "
+ ((System.nanoTime() - start) / 1_000_000) + " msecs");
新的日期和时间API
0. 为什么需要新的
之前Java 中时间API的有什么呢?
两个:java.util.Date类与java.util.Calendar类
- 易用性很差
- 两个类同时存在,很使人困惑
- 有的特性只在某一个类有提供
- 这两个类都是可以变的
1. LocalDate、LocalTime、Instant、Duration以及Period
1)使用LocalDate和LocalTime
表示日期的LocalDate
LocalDate date = LocalDate.of(2014, 3, 18); // 2014-03-18
int year = date.getYear(); // 2014
Month month = date.getMonth(); // MARCH
int day = date.getDayOfMonth(); // 18
DayOfWeek dow = date.getDayOfWeek(); // TUESDAY
int len = date.lengthOfMonth(); // 31
boolean leap = date.isLeapYear(); // false
//使用工厂方法从系统时钟中获取当前的日期
LocalDate today = LocalDate.now();
如要过去年份月份等信息,也可以通过get方法,只需传入一个TemporalField参数即可。ChronoField枚举实现了TemporalField接口,所以可以参考下列代码来调用。
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);
表示时间的LocalTime
LocalTime time = LocalTime.of(13, 45, 20); // 13:45:20
int hour = time.getHour(); // 13
int minute = time.getMinute(); // 45
int second = time.getSecond(); // 20
parse方法
LocalDate和LocalTime都可以通过解析代表它们的字符串创建
LocalDate date = LocalDate.parse("2014-03-18");
LocalTime time = LocalTime.parse("13:45:20");
如果需要可以传入自定义的时间格式,只需再传入一个DateTimeFormatter即可。
一旦传递的字符串参数无法被解析为合法的LocalDate或LocalTime对象,这两个parse方法都会抛出一个继承自RuntimeException的DateTimeParseException异常。
2)合并日期和时间
这个复合类名叫LocalDateTime,是LocalDate和LocalTime的合体。
它同时表示了日期和时间,但不带有时区信息,你可以直接创建,也可以通过合并日期和时间对象构造。
// 2014-03-18T13:45:20
LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 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);
反向获取
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();
3)机器的日期和时间格式
使用新的java.time.Instant类,它使用一个单一大整型数来表示一个持续时间段上某个点,也就是一个包含秒及纳秒所构成的数字。
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000); // 为2s加上100w纳秒(1s)
Instant.ofEpochSecond(4, -1_000_000_000); // 为4s减去100w纳秒(1s)
int day = Instant.now(); // 当前时刻的时间戳
注意:这个类无法处理那些我们非常容易理解的时间单位
4)定义Duration或Period
上面所提到的类,都实现了Temporal接口。这个接口定义了如何读取和操纵为时间建模的对象的值。
当我们需要获取两个Temporal对象之间的距离时,该怎么做呢?
-
Duration.between
对与两个LocalTimes对象、两个LocalDateTimes对象,或者两个Instant对象之间的duration,可调用Duration.between方法。
Duration d1 = Duration.between(time1, time2); Duration d1 = Duration.between(dateTime1, dateTime2); Duration d2 = Duration.between(instant1, instant2);
注意:between函数传入的两个参数,类型应该一样。
-
Period.between
那么对两个LocalDate对象呢?
可以调用Period.between。
Period tenDays = Period.between(LocalDate.of(2014, 3, 8), LocalDate.of(2014, 3, 18));
-
Duration和Period类其他的工厂类
Duration threeMinutes = Duration.ofMinutes(3); Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES); Period tenDays = Period.ofDays(10); Period threeWeeks = Period.ofWeeks(3); Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
2. 操纵、格式化以及解析日期
以上所提到的这些日期时间对象都是不可修改的,这是为了更好地支持函数式编 程,确保线程安全,保持领域模式一致性而做出的重大设计决定。
那当我们想要修改日期时,怎么办呢?
1)直观操作
使用withAttribute方法
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.withYear(2011);
LocalDate date3 = date2.withDayOfMonth(25);
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);
以相对方式修改LocalDate对象的属性
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.plusWeeks(1);
LocalDate date3 = date2.minusYears(3);
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);
2)使用TemporalAdjuster
有时候我们需要对时间进行一些复杂的操作,这个时候可以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象。
import static java.time.temporal.TemporalAdjusters.*;
//2014-03-18
LocalDate date1 = LocalDate.of(2014, 3, 18);
//2014-03-23
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));
//2014-03-31
LocalDate date3 = date2.with(lastDayOfMonth());
TemporalAdjuster类中的工厂方法
方法名 | 描述 |
---|---|
dayOfWeekInMonth | 创建一个新的日期,它的值为同一个月中每一周的第几天 |
firstDayOfMonth | 创建一个新的日期,它的值为当月的第一天 |
firstDayOfNextMonth | 创建一个新的日期,它的值为下月的第一天 |
firstDayOfNextYear | 创建一个新的日期,它的值为明年的第一天 |
firstDayOfYear | 创建一个新的日期,它的值为当年的第一天 |
firstInMonth | 创建一个新的日期,它的值为同一个月中,第一个符合星期几要求的值 |
lastDayOfMonth | 创建一个新的日期,它的值为当月的最后一天 |
lastDayOfNextMonth | 创建一个新的日期,它的值为下月的最后一天 |
lastDayOfNextYear | 创建一个新的日期,它的值为明年的最后一天 |
lastDayOfYear | 创建一个新的日期,它的值为今年的最后一天 |
lastInMonth | 创建一个新的日期,它的值为同一个月中,最后一个符合星期几要求的值 |
next/previous | 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期 |
nextOrSame/previousOrSame | 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期,如果该日期已经符合要求,直接返回该对象 |
TemporalAdjuster接口
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
试着自定义一个TemporalAdjuster接口。
就叫做NextWorkingDay类吧,该类能够计算明天的日期,同时过滤掉周六和周日这些节假日。
public class NextWorkingDay implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
DayOfWeek dow =
DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
}
}
//调用
date = date.with(new NextWorkingDay());
换成用lambda实现一下
date = date.with(temporal -> {
DayOfWeek dow =
DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});
同时TemporalAdjusters.ofDateAdjuster可以接受一个UnaryOperator
TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(
temporal -> {
DayOfWeek dow =
DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});
date = date.with(nextWorkingDay);
3)打印输出及解析日期、时间对象
java.time.format包提供了格式化以及解析日期、时间对象的功能。
- 使用 BASIC_ISO_DATE 和 ISO_LOCAL_DATE
LocalDate date = LocalDate.of(2014, 3, 18); // 20140318 String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 2014-03-18 String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
-
使用工厂方法parse
通过解析代表日期或时间的字符串重新创建该日期对象。
LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE); LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);
和老的java.util.DateFormat相比较,所有的DateTimeFormatter实例都是线程安全的。
-
按照某个模式创建DateTimeFormatter
DateTimeFormatter类还支持一个静态工厂方法ofPattern,它可以按照某个特定的模式创建格式器.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); LocalDate date1 = LocalDate.of(2014, 3, 18); String formattedDate = date1.format(formatter); LocalDate date2 = LocalDate.parse(formattedDate, formatter);
-
创建一个本地化的DateTimeFormatter
ofPattern方法也提供了一个重载的版本,使用它你可以创建某个Locale的格式器
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN); LocalDate date1 = LocalDate.of(2014, 3, 18); String formattedDate = date.format(italianFormatter); // 18. marzo 2014 LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);
-
构造一个DateTimeFormatter
DateTimeFormatterBuilder类还提供了更复杂的格式器,以及其他非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精确地匹配指定的模式)、填充,以及在格式器中指定可选节。
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder() .appendText(ChronoField.DAY_OF_MONTH) .appendLiteral(". ") .appendText(ChronoField.MONTH_OF_YEAR) .appendLiteral(" ") .appendText(ChronoField.YEAR) .parseCaseInsensitive() .toFormatter(Locale.ITALIAN);
3. 处理不同的时区和历法
1)基本介绍
之前看到的日期和时间的种类都不包含时区信息。
新的java.time.ZoneId类是老版java.util.TimeZone的替代品。
它的设计目标就是要让你无需为时区处理的复杂和繁琐而操心。
ZoneId类也是无法修改的。
如何创建
// 创建一个ZoneId
ZoneId romeZone = ZoneId.of("Europe/Rome");
// 将一个老的时区对象转换为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);
通过ZoneId,你还可以将LocalDateTime转换为Instant
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
Instant instantFromDateTime = dateTime.toInstant(romeZone);
反向得到LocalDateTime对象
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);
2)利用和UTC/格林尼治时间的固定偏差计算时区
可以使用ZoneOffset类,它是ZoneId的一个子类,表示的是当前时间和伦敦格林尼治子午线时间的差异。
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
注意,使用这种方式定义的ZoneOffset并未考虑任何日光时的影响,所以在大多数情况下,不推荐使用。
使用它创建这样的OffsetDateTime:使用ISO-8601的历法系统,以相对于UTC/格林尼治时间的偏差方式表示日期时间。
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(date, newYorkOffset);
3)使用别的日历系统
新版的日期和时间API还提供了另一个高级特性,即对非ISO历法系统(non-ISO calendaring)的支持。
Java 8中另外还提供了4种其他的日历系统:ThaiBuddhistDate、MinguoDate、JapaneseDate以及HijrahDate。
创建这些类的实例
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
JapaneseDate japaneseDate = JapaneseDate.from(date);
为某个Locale显式地创建日历系统,接着创建该Locale对应的日期的实例
Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
ChronoLocalDate now = japaneseChronology.dateNow();