Java 8 是 Java 的一个重大版本,有人认为,虽然这些新特性令 Java 开发人员十分期待,但同时也需要花不少精力去学习。在这一小节中,我们将介绍 Java 8 的大部分新特性。
java8 引入了一个新的操作符->
,它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理。以前的话只能使用匿名内部类。(省去定义,重写方法,直接表达)
Lambda表达式的标准格式为:
(参数类型 参数名称) -> { 代码语句 }
->
是新引入的语法格式,代表指向动作。使用前提
Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。省略格式
在 Lambda 标准格式的基础上,使用省略写法的规则为:
使用示例
//使用匿名内部类的方式,实现多线程
new Thread(new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 新线程创建了");
}
}).start();
//使用Lambda表达式,实现多线程
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 新线程创建了");
}
).start();
//优化省略Lambda
new Thread(()->System.out.println(Thread.currentThread().getName()+" 新线程创建了")).start();
// 匿名内部类
Comparator<Person> comp = new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
};
Arrays.sort(array, comp); // 第二个参数为排序规则,即Comparator接口实例
//Lambda
Arrays.sort(array, ( a, b) -> a.getAge() - b.getAge());
Lambda 的延迟执行
有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以 作为解决方案,提升性能
public class Demo01Logger {
private static void log(int level, String msg) {
if (level == 1) System.out.println(builder.buildMessage());
}
public static void main(String[] args) {
String msgA = "Hello";
String msgB = "World";
String msgC = "Java";
log(1, () ‐> msgA + msgB + msgC );
} }
变量问题
由于 Java 是静态类型语言,它需要在编译时知道所有的对象和变量的类型。Java 8编译器在类型推断方面有很大的提升,在很多场景下编译器可以推导出某个参数的数据类型,从而使得代码更为简洁。
最直接的例子就是之前讲的,在 lambda 表达式的参数列表中省略类型
//定义一个计算两个int整数和的方法并返回结果
public abstract int calc(int a,int b);
// 定义一个方法,计算两个整数的和
public static void invokeCalc(int a,int b,Calculator c){
int sum = c.calc(a,b);
System.out.println(sum);
}
//main
public static void main(String[] args) {
//使用Lambda表达式简化匿名内部类的书写
invokeCalc(120,130,(int a,int b)->{
return a + b;
});
//优化省略Lambda
invokeCalc(120,130,(a,b)-> a + b);
}
Lambda 的设计者们为了让现有的功能与 Lambda 表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念。函数接口指的是有且仅有一个抽象方法的接口,这样的接口可以隐式转换为 Lambda 表达式。java.lang.Runnable
和 java.util.concurrent.Callable
是函数式接口的最佳例子。
在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解**@FunctionalInterface**,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。举个简单的函数式接口的定义:
@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();
default void defaultMethod() {
}
}
常用函数式接口
JDK 提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在 java.util.function
包中被提供
java.util.function.Supplier
接口仅包含一个无参的方法: T get()
。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的 Lambda 表达式需要“对外提供”一个符合泛型类型的对象数据。
//求数组元素最大值
public class Demo02Test {
//定一个方法,方法的参数传递Supplier,泛型使用Integer
public static int getMax(Supplier<Integer> sup){
return sup.get();
}
public static void main(String[] args) {
int arr[] = {2,3,4,52,333,23};
//调用getMax方法,参数传递Lambda
int maxNum = getMax(()‐>{
//计算数组的最大值
int max = arr[0];
for(int i : arr){ if(i>max){
max = i;
}
}
return max;
});
System.out.println(maxNum);
}
}
java.util.function.Consumer
接口则正好与 Supplier 接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。
方法
void accept(T t)
,意为消费一个指定泛型的数据。andThen
,消费数据的时候,首先做一个操作,然后再做一个操作,实现组合//格式化打印, 姓名:XX。性别:XX
public class DemoConsumer {
public static void main(String[] args) {
String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" };
printInfo(s ‐> System.out.print("姓名:" + s.split(",")[0]),
s ‐> System.out.println("。性别:" + s.split(",")[1] + "。"),
array);
}
private static void printInfo(Consumer<String> one, Consumer<String> two, String[] array) {
for (String info : array) {
one.andThen(two).accept(info); // 姓名:迪丽热巴。性别:女。
}
}
}
有时候我们需要对某种类型的数据进行判断,从而得到一个 boolean 值结果。这时可以使用
java.util.function.Predicate
接口。
方法
boolean test(T t)
用于条件判断and
, 实现“并且”的效果时,or
, 实现逻辑关系中的“或”//集合信息筛选,姓名为4个字的女生
public class DemoPredicate {
public static void main(String[] args) {
String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" }; List<String> list = filter(array,
s ‐> "女".equals(s.split(",")[1]),
s ‐> s.split(",")[0].length() == 4);
System.out.println(list);
}
private static List<String> filter(String[] array, Predicate<String> one,
Predicate<String> two) {
List<String> list = new ArrayList<>();
for (String info : array) {
if (one.and(two).test(info)) { list.add(info);
}
}
return list;
}
}
java.util.function.Function
接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
方法
R apply(T t)
,根据类型 T 的参数获取类型 R 的结果andThen
,用来进行组合操作,和Consumer 中的andThen 差不多//自定义函数模型拼接
//1.将字符串截取数字年龄部分,得到字符串;
//2.将上一步的字符串转换成为int类型的数字;
//3.将上一步的int数字累加100,得到结果int数字。
public class DemoFunction {
public static void main(String[] args) {
String str = "赵丽颖,20";
int age = getAgeNum(str, s ‐> s.split(",")[1],
s ‐>Integer.parseInt(s),
n ‐> n += 100);
System.out.println(age);
}
private static int getAgeNum(String str, Function<String, String> one,
Function<String, Integer> two,
Function<Integer, Integer> three) {
return one.andThen(two).andThen(three).apply(str);
}
}
在使用 Lambda 表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?
方法引用
双冒号::
为引用运算符,而它所在的表达式被称为方法引用。如果 Lambda 要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。
s -> System.out.println(s);
System.out::println
推导与省略
如果使用 Lambda,那么根据可推导就是可省略的原则,无需指定参数类型,也无需指定的重载形式——它们都
将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。
函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。
使用场合
对象名
引用 成员方法类名称
引用 静态方法super
、this
引用 成员方法使用条件
方法引用示例,因使用上类似,便拿一个举例即可:
@FunctionalInterface
public interface Printable {
void print(String str);
}
public class MethodRefObject {
public void printUpperCase(String str) {
System.out.println(str.toUpperCase());
}
}
public class Demo04MethodRef {
private static void printString(Printable lambda) {
lambda.print("Hello");
}
public static void main(String[] args) {
//通过 对象名 引用 成员方法,
MethodRefObject obj = new MethodRefObject();
printString(obj::printUpperCase);
}
}
构造器引用:
由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new
的格式表示。
//Person类
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
//getter、setter
}
//接口
public interface PersonBuilder {
Person buildPerson(String name);
}
//test
public class Demo10ConstructorRef {
public static void printName(String name, PersonBuilder builder) {
System.out.println(builder.buildPerson(name).getName());
}
public static void main(String[] args) {
//printName("赵丽颖", name ‐> new Person(name));
//这里实现 PersonBuilder接口,重写里面方法,根据name ,new 一个Person 对象
printName("赵丽颖", Person::new);
}
}
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。int[]::new
Java 8 使用两个新概念扩展了接口的含义:默认方法和静态方法。
接口名称.静态方法名(参数)
public default 返回值类型 方法名称(参数列表){
方法体
}
public static 返回值类型 方法名称(参数列表){
方法体
}
Optional 不是函数是接口,这是个用来防止 NullPointerException
异常的辅助类型。Optional 被定义为一个简单的容器,其值可能是 null 或者不是 null。在Java 8之前一般某个函数应该返回非空对象但是偶尔却可能返回了null,而在 Java 8中,不推荐你返回 null 而是返回 Optional。建议参考
Optional<String> optional = Optional.of("bam");
optional.isPresent(); // true 检查是否有值
optional.get(); // "bam" 取回实际值对象,值为 null 的时候抛出异常
optional.orElse("fallback"); // "bam" 有值则返回该值,否则返回传递给它的参数值
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。它可以指定您希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。
写在前面
for-each
或者使用Iterator
在外部迭代;Stream 是内部迭代(好处:并行操作)流的获取
of
,如Stream.of(array)
list.stream()
map.keySet().stream();
map.values().stream();
map.entrySet().stream();
常用方法
运用Stream操作分三步:创建Stream流、流中间操作、终止流操作
流模型的操作很丰富,这里介绍一些常用的 API。这些方法可以被分成两种:
count
和 forEach
方法。//终结
long count();
void forEach(Consumer<? super T> action);
//非终结
Stream<T> filter(Predicate<? super T> predicate);
Stream<T> limit(long maxSize);
Stream<T> skip(long n);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b);
简要代码:
//第一支队伍
ArrayList<String> one = new ArrayList<>();
one.add("迪丽热巴");
one.add("宋远桥");
one.add("苏星河");
one.add("石破天");
one.add("石中玉");
one.add("老子");
one.add("庄子");
one.add("洪七公");
//第二支队伍
ArrayList<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("张无忌");
two.add("赵丽颖");
two.add("张三丰");
two.add("尼古拉斯赵四");
two.add("张天爱");
two.add("张二狗");
// 第一个队伍只要名字为3个字的成员姓名;
// 第一个队伍筛选之后只要前3个人;
Stream<String> streamOne = one.stream().filter(s ‐> s.length() == 3).limit(3);
// 第二个队伍只要姓张的成员姓名;
// 第二个队伍筛选之后不要前2个人;
Stream<String> streamTwo = two.stream().filter(s ‐> s.startsWith("张")).skip(2);
// 将两个队伍合并为一个队伍;
// 根据姓名创建Person对象;
// 打印整个队伍的Person对象信息。
Stream.concat(streamOne, streamTwo).map(Person::new).forEach(System.out::println);
Stream 的注意事项:
接下来,拓展一些常用方法
排序是一个中间操作,返回的是排序好后的Stream。如果你不指定一个自定义的Comparator则会使用默认排序。
排序只创建了一个排列好后的 Stream,而不会影响原有的数据源,排序之后原数据是不会被修改的。
//排序
Stream<T> sorted();//自然排序
Stream<T> sorted(Comparator<? super T> comparator);//定制排序
示例:
List<String> list = Arrays.asList("ccc", "aaa", "bbb", "ddd", "eee");
//自然排序
list.stream()
.sorted()
.forEach(System.out::println);
System.out.println("------------------------------");
//定制排序
employees.stream()
.sorted((e1, e2) -> {
if (e1.getAge() == e2.getAge()){
return e1.getName().compareTo(e2.getName());
}else{
return Integer.compare(e1.getAge(), e2.getAge());
}
}).forEach(System.out::println);
中间操作
Stream<T> distinct();
Stream<String> stringStream = Stream.of( "11","22", "33","11");
stringStream.map(Integer::parseInt).distinct().forEach(System.out::println);
和 map 类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中
flatMap包含两个操作:会将每一个输入对象输入映射为一个新集合,然后把这些新集合连成一个大集合。
举例
{苹果,梨子}.flatMap(切碎) = {苹果碎片1,苹果碎片2,梨子碎片1,梨子碎片2}
其中: “切碎”函数的类型为:A => List
再比如 List
变成List
就需要先切碎,再连接。
概括来讲:把几个小的 list 转换到一个大的 list。 参考
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> var1);
List<String> list = new ArrayList();
list.add("1,2,3");
list.add("4,5,6");
list.add("7,8");
List<String> collect = list.stream()
.flatMap(
str -> Arrays.stream(str.split(","))
).collect(Collectors.toList());
在Java中,将原始类型转换为对应的引用类型的机制,这个机制叫做装箱。将引用类型转换为对应的原始类型,叫做拆箱。在 java 中装箱和拆箱是自动完成的。
但是这么做(int被装箱成Integer)在性能方面是要付出代价的,装箱的本质就是将原始类型包裹起来,并保存在堆里。因此装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
java8 引入了三个原始类型特化流来解决这个问题;IntStream、DoubleStream、LongStream 分别将流中元素特化为 int、double、long,从而避免了暗含装箱的成本。每个接口都带来了常用数值归约的新方法,例如sum、max、min
我们需要返回的是 int 类型,并且 mapToInt 返回的也是int类型,这其中是省去了一个装箱的成本。
mapToLong 之类的类似,就不展示了
IntStream mapToInt(ToIntFunction<? super T> mapper);
IntStream flatMapToInt(Function<? super T, ? extends IntStream> var1);//flatMap也有
int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
继续讲解 boxed
注意:是在 IntStream 中,之前都在 Stream中;当然啦。类似的 DoubleStream 中也有
Stream<Integer> boxed();
IntStream.range(0, 10).collect(Collectors.toList());//会报错
IntStream.range(0, 10).mapToObj(i->new Product()).collect(Collectors.toList());
IntStream.range(0,10).boxed().collect(Collectors.toList());
boxed 的作用应该不言而喻了,装箱(将基本类型的元素装箱为包装类型)
//第一种方法,常用些
<R, A> R collect(Collector<? super T, A, R> collector);
//第二种方法
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
第二种方法说明(没用过,做了解)
更多关于收集器介绍,建议阅读
第一种示例:
看结构就行了
//大致结构
List<SpuBo> spuBos = spus.stream().map(spu -> { 业务处理 }).collect(Collectors.toList());
//spu集合转化成spubo集合
List<SpuBo> spuBos = spus.stream().map(spu -> {
SpuBo spuBo = new SpuBo();
//copy共同属性的值到新对象
BeanUtils.copyProperties(spu, spuBo);
//查询品牌名称
Brand brand = this.brandMapper.selectByPrimaryKey(spu.getBrandId());
spuBo.setBname(brand.getName());
//查询分类名称
List<String> names = this.categoryService.queryNameByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
spuBo.setCname(StringUtils.join(names, "/"));
return spuBo;//勿忘
}
).collect(Collectors.toList());
Stream 提供了多种匹配操作,允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是最终操作,并返回一个boolean类型的值。
//检查是否匹配所有元素
boolean allMatch(Predicate<? super T> predicate);
//检查是否至少匹配一个元素
boolean anyMatch(Predicate<? super T> predicate);
//检查是否没有匹配的元素
boolean noneMatch(Predicate<? super T> predicate);
示例:
boolean anyStartsWithA =
stringCollection
.stream()
.anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA); // true
boolean allStartsWithA =
stringCollection
.stream()
.allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA); // false
boolean noneStartsWithZ =
stringCollection
.stream()
.noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ); // true
这是一个最终操作
Optional<T> findFirst();//返回第一个元素
Optional<T> findAny();//返回当前流中的任意元素
示例:
Optional<Employee> op = emps.stream()
.sorted(Comparator.comparingDouble(Employee::getSalary))
.findFirst();
System.out.println(op.get());
System.out.println("--------------------------------");
Optional<Employee> op2 = emps.parallelStream()
.filter((e) -> e.getStatus().equals(Status.FREE))
.findAny();
System.out.println(op2.get());
这是一个最终操作,如果需要获取最大和最小值,可以使用 max 和 min 方法
Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(Comparator<? super T> comparator);
Optional<Integer> max = Stream.of(5, 3, 6, 1).max( (o1,o2) -> o1 - o2); // 取升序的最后一个
System.out.println("max = " + max.get());
Optional<Integer> min = Stream.of(5, 3, 6, 1).min( (o1,o2) -> o1 - o2); // 取升序的第一个
System.out.println("min = " + min.get());
这是一个最终操作,允许通过指定的函数来讲 stream中的多个元素规约为一个元素,规约后的结果是通过Optional
接口表示的.
Optional<T> reduce(BinaryOperator<T> accumulator)//一个参数的Reduce
T reduce(T identity, BinaryOperator<T> accumulator)//两个参数的Reduce,多了一个初始化的值。
指定初始值,示例:
// 以下结果将会是: [value]testt1t2teeeeeaaaataaa
Stream<String> s = Stream.of("test", "t1", "t2", "teeeee", "aaaa", "taaa");
/*
System.out.println(s.reduce("[value]", new BinaryOperator() {
@Override
public String apply(String s, String s2) {
return s.concat(s2);
}
}));
*/
System.out.println(s.reduce("[value]", (s1, s2) -> s1.concat(s2)));
Stream 有串行和并行两种,串行Stream上的操作是在一个线程中依次完成,而并行Stream则是在多个线程上同时执行。
改成并行,唯一需要做的改动就是将stream()
改为parallelStream()
。
long t0 = System.nanoTime();
long count = values.parallelStream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("parallel sort took: %d ms", millis));
方法名 | 方法作用 | 返回值类型 | 方法种类 |
---|---|---|---|
collect | 如:将数组转为集合List | R | 终结 |
count | 统计个数 | long | 终结 |
forEach | 逐一处理 | void | 终结 |
all(any、none)Match | 匹配元素 | boolean | 终结 |
findFirst(Any) | 返回元素 | Optional |
终结 |
max、min | 获取最大和最小值 | Optional |
终结 |
reduce | 归约,如累加累乘 | T | 终结 |
filter | 过滤筛选 | Stream | 函数拼接 |
limit | 取用前几个 | Stream | 函数拼接 |
skip | 跳过前几个 | Stream | 函数拼接 |
map | 一对一映射 | Stream | 函数拼接 |
flatMap | 可处理复杂映射 | Stream | 函数拼接 |
concat | 组合,合并流 | Stream | 函数拼接 |
sorted | 排序 | Stream | 函数拼接 |
distinct | 去重 | Stream | 函数拼接 |
mapToInt | 包装类 | IntStream | 函数拼接 |
归约的优势和并行化
来自《 Java8 In Action》。参考博客
相比于用 foreach 逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum,这不是那么容易并行化的,可变的累加模式对于并行化来说是死路一条。你需要一种新的模式,这正是 reduce 所提供的。传递给 reduce 的 lambda 不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。
流操作的状态:无状态和有状态
诸如map和filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般是无状态的:他们没有内部状态(假设用户提供的lambda或者方法引用没有内部可变状态)。
但诸如 reduce、sum、max等操作需要内部状态来累积结果。在前面的情况下,内部状态很小。在我们的例子里就是一个int或者double。不管流中有多少元素要处理,内部状态都是有界的。
相反,诸如 sort 或 distinct 等操作一开始都和 filter 和 map 差不多–都是接受一个流,再生成一个流(中间操作), 但有一个关键的区别。从流中排序和删除重复项都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们他不存在)。我们把这些操作叫做有状态操作。
Java 8 在包 java.time
下包含了一组全新的时间日期API。新的日期 API 和开源的 Joda-Time 库差不多,但又不完全一样,下面的例子展示了这组新 API 里最重要的一些部分:以下内容出自
Clock类提供了访问当前日期和时间的方法,Clock是时区敏感的,可以用来取代 System.currentTimeMillis()
来获取当前的微秒数。某一个特定的时间点也可以使用 Instant 类 来表示,Instant 类也可以用来创建老的java.util.Date
对象。
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant); // legacy java.util.Date
在新API中时区使用 ZoneId 来表示。时区可以很方便的使用静态方法of来获取到。 时区定义了到UTS时间的时间差,在 Instant 时间点对象到本地日期对象之间转换的时候是极其重要的。
System.out.println(ZoneId.getAvailableZoneIds());
// prints all available timezone ids
ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]
LocalTime 定义了一个没有时区信息的时间,例如 晚上10点,或者 17:30:15。下面的例子使用前面代码创建的时区创建了两个本地时间。之后比较时间并以小时和分钟为单位计算两个时间的时间差:
LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);
System.out.println(now1.isBefore(now2)); // false
long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
System.out.println(hoursBetween); // -3
System.out.println(minutesBetween); // -239
LocalTime 提供了多种工厂方法来简化对象的创建,包括解析时间字符串。
LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late); // 23:59:59
DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.GERMAN);
LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime); // 13:37
LocalDate 表示了一个确切的日期,比如 2014-03-11。该对象值是不可变的,用起来和 LocalTime 基本一致。下面的例子展示了如何给Date对象加减天/月/年。另外要注意的是这些对象是不可变的,操作返回的总是一个新实例。
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek); // FRIDAY
从字符串解析一个LocalDate类型和解析LocalTime一样简单:
DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.GERMAN);
LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas); // 2014-12-24
LocalDateTime 同时表示了时间和日期,相当于前两节内容合并到一个对象上了。LocalDateTime和 LocalTime还有 LocalDate 一样,都是不可变的。LocalDateTime提供了一些能访问具体字段的方法。
LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek); // WEDNESDAY
Month month = sylvester.getMonth();
System.out.println(month); // DECEMBER
long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay); // 1439
只要附加上时区信息,就可以将其转换为一个时间点Instant对象,Instant时间点对象可以很容易的转换为老式的java.util.Date
。
Instant instant = sylvester
.atZone(ZoneId.systemDefault())
.toInstant();
Date legacyDate = Date.from(instant);
System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014
格式化 LocalDateTime 和格式化时间和日期一样的,除了使用预定义好的格式外,我们也可以自己定义格式:
DateTimeFormatter formatter =
DateTimeFormatter
.ofPattern("MMM dd, yyyy - HH:mm");
LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string); // Nov 03, 2014 - 07:13
和 java.text.NumberFormat
不一样的是新版的 DateTimeFormatter
是不可变的,所以它是线程安全的。