Java 8发布于2014-03-18,发布至今已经8年了,是目前企业中使用最广泛的一个版本。Java 8是一次重大的版本升 级,带来了很多的新特性。
JDK 8新特性
关注对数据进行操作
优点:
Lambda是JDK8中的一个语法糖,可以对某些匿名内部类的写法进行简化,它是函数式编程思想的一个重要体现,让我们不用关注是什么对象,而是关注我们对数据进行了什么操作。
核心原则
可推导可省略
(参数列表)—>{
代码
}
匿名内部类创建线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类创建线程");
}
}
).start();
Lambda表达式写法:
new Thread(()->{
System.out.println("Lambda表达式创建线程");
}).start();
public interface Swimmable {
public abstract void run();
}
public class DemoLambdaUse {
public static void main(String[] args) {
//匿名内部类写法
goRun(new Swimmable() {
@Override
public void run() {
System.out.println("匿名内部类写法");
}
});
//Lambda表达式写法
goRun(()->{
System.out.println("Lambda表达式写法");
});
}
public static void goRun(Swimmable swimmable){
swimmable.run();
}
}
下面举例演示 java.util.Comparator 接口的使用场景代码,其中的抽象方法定义为:
public abstract int compare(T o1, T o2);
当需要对一个对象集合进行排序时, Collections.sort 方法需要一个 Comparator 接口实例来指定排序的规则。
ArrayList list = new ArrayList<>();
list.add(1);
list.add(9);
list.add(5);
//匿名内部类写法
Collections.sort(list, new Comparator(){
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
//Lambda表达式写法
Collections.sort(list, (Integer o1, Integer o2) ->{
return o1 - o2;
});
for (Integer integer : list) {
System.out.println(integer);
}
List asList = Arrays.asList(11, 33, 22, 44);
//匿名内部类写法
asList.forEach(new Consumer() {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});
//Lambda表达式写法
asList.forEach((s) -> {
System.out.println(s);
});
//Lambda表达式写法 变形
asList.forEach(s->System.out.println(s));
asList.forEach(s->{
if (s > 20){
System.out.println(s);
}
});
匿名内部类实现原理
说实现原理前先说说匿名内部类实现原理
public class DemoLambdaUse {
public static void main(String[] args) {
//匿名内部类写法
goRun(new Swimmable() {
@Override
public void run() {
System.out.println("匿名内部类写法");
}
});
}
public static void goRun(Swimmable swimmable){
swimmable.run();
}
}
匿名内部类会在编译后产生一个类: DemoLambdaImpl$1.class
反编译后的内容
package com.sl.java8;
final class DemoLambdaUse$1 implements Swimmable {
DemoLambdaUse$1() {
}
public void run() {
System.out.println("匿名内部类写法");
}
}
Lambda表达式实现原理
public class DemoLambdaUse {
public static void main(String[] args) {
//Lambda表达式写法
goRun(()->{
System.out.println("Lambda表达式写法");
});
}
public static void goRun(Swimmable swimmable){
swimmable.run();
}
}
运行程序,控制台可以得到预期的结果,但是并没有出现一个新的类,也就是说Lambda并没有在编译的时候产生一 个新的类。
我们使用 JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令。 在DOS命令行输入:
javap -c -p 文件名.class
-c:表示对代码进行反汇编
-p:显示所有类和成员
F:\workspace\java_projcet\study\java8\target\classes\com\sl\java8>javap -c -p DemoLambdaUse.class
Compiled from "DemoLambdaUse.java"
public class com.sl.java8.DemoLambdaUse {
public com.sl.java8.DemoLambdaUse();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Lcom/sl/java8/Swimmable;
5: invokestatic #3 // Method goRun:(Lcom/sl/java8/Swimmable;)V
8: return
public static void goRun(com.sl.java8.Swimmable);
Code:
0: aload_0
1: invokeinterface #4, 1 // InterfaceMethod com/sl/java8/Swimmable.run:()V
6: return
private static void lambda$main$0();
Code:
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String Lambda表达式写法
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
多了一个私有的静态方法
private static void lambda$main$0();
其实Lambda在运行的时候会生成一个内部类,为了验证是否生成内部类,可以在运行时加 上 -Djdk.internal.lambda.dumpProxyClasses ,加上这个参数后,运行时会将生成的内部类class码输出到一个文 件中。使用java命令如下:
F:\workspace\java_projcet\study\java8\target\classes>java -Djdk.internal.lambda.dumpProxyClasses com.sl.java8.DemoLambdaUse
Lambda表达式写法
反编译DemoLambdaUse$$Lambda$1.class
package com.sl.java8;
import java.lang.invoke.LambdaForm.Hidden;
// $FF: synthetic class
final class DemoLambdaUse$$Lambda$1 implements Swimmable {
private DemoLambdaUse$$Lambda$1() {
}
@Hidden
public void run() {
DemoLambdaUse.lambda$main$0();
}
}
可以看到这个匿名内部类实现了 Swimmable 接口,并且重写了 swimming 方法, swimming 方法调用 Demo04LambdaImpl.lambda$main$0() ,也就是调用Lambda中的内容。最后可以将Lambda理解为:
public class Demo04LambdaImpl {
public static void main(String[] args) {
goSwimming(new Swimmable() {
public void swimming() {
Demo04LambdaImpl.lambda$main$0();
}
});
}
private static void lambda$main$0() {
System.out.println("Lambda表达式游泳");
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
总结:
Lambda在程序运行的时候形成一个类
在Lambda标准格式的基础上,使用省略写法的规则为:
(int a) -> {
return new Person();
}
省略后
a -> new Person()
Lambda表达式不是随便使用的,使用时有几个条件要特别注意:
Lambda表达式的前提条件:
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
ava 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注 解可用于一个接口的定义上:
//检测这个接口是不是只有一个抽象方法
@FunctionalInterface
public interface Swimmable {
public abstract void run();
}
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即 使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
Supplier接口
它们主要在 java.util.function 包中。下面是最常用的几个接口。
java.util.function.Supplier 接口,它意味着"供给" , 对应的Lambda表达式需要“对外提供”一个符合泛型类 型的对象数据。
@FunctionalInterface
public interface Supplier {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
示例
public class TestSupplier {
public static void printMax(Supplier supplier){
System.out.println(supplier.get());
}
public static void main(String[] args) {
printMax(()->{
int[] arry = {11, 33, 22};
Arrays.sort(arry);
return arry[arry.length -1];
});
}
}
java.util.function.Consumer 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
public interface Consumer {
void accept(T t);
default Consumer andThen(Consumer super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
默认方法:andThen
如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。
java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出 NullPointerException 异常。
这省去了重复编写if语句和抛出空指针异常的麻烦。
示例:
public class TestConsumer {
public static void test(Consumer c1, Consumer c2){
String s = "HelloWorld";
c1.accept(s);
}
public static void main(String[] args) {
test(s -> {
System.out.println(s.toLowerCase());
});
}
}
示例2:
public class TestConsumer {
public static void test(Consumer c1, Consumer c2){
String s = "HelloWorld";
c1.andThen(c2).accept(s);
}
public static void main(String[] args) {
test(s -> {
System.out.println(s.toLowerCase());
},s -> {
System.out.println(s.toUpperCase());
});
}
}
java.util.function.Function 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件, 后者称为后置条件。有参数有返回值。
@FunctionalInterface
public interface Function {
R apply(T t);
default Function compose(Function super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default Function andThen(Function super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static Function identity() {
return t -> t;
}
}
示例:
public class TestFunction {
public static void test(Function function){
Integer apply = function.apply("10");
System.out.println("in:" + (apply+ 5));
}
public static void main(String[] args) {
test(s->{
return Integer.parseInt(s);
});
}
}
默认方法:andThen
Function 接口中有一个默认的 andThen 方法,用来进行组合操作。
该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多:
public class Demo09FunctionAndThen {
public static void main(String[] args) {
// Lambda表达式
test((String s) -> {
return Integer.parseInt(s);
}, (Integer i) -> {
return i * 10;
});
}
public static void test(Function f1, Function f2) {
// Integer in = f1.apply("66"); // 将字符串解析成为int数字
// Integer in2 = f2.apply(in);// 将上一步的int数字乘以10
Integer in3 = f1.andThen(f2).apply("66");
System.out.println("in3: " + in3); // 660
}
}
请注意,Function的前置条件泛型和后置条件泛型可以相同。
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用 java.util.function.Predicate 接口。
@FunctionalInterface
public interface Predicate {
boolean test(T t);
default Predicate and(Predicate super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate negate() {
return (t) -> !test(t);
}
default Predicate or(Predicate super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static Predicate isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
示例:
public class TestPredicate {
private static void test(Predicate predicate){
boolean test = predicate.test("1");
System.out.println(test);
}
public static void main(String[] args) {
test(s->{
return s.equals("1");
});
}
}
匿名内部类 | Lambda表达式 | |
---|---|---|
所需类型 | 可以是类,抽象类,接口 | 必须是接口 |
抽象方法数量 | 随意 | 只能有一个抽象方法 |
实现原理 | 在编译后会形成class | 在程序运行时动态生成class |
总结:
当接口中只有一个抽象方法时,建议使用Lambda表达式,其他其他情况还是需要使用匿名内部类
接口增强介绍
jdk8之前:
public interface Swimmable {
静态常量;
抽象方法;
}
增强后:
可以有默认方法和静态方法
interface 接口名 {
静态常量;
抽象方法;
默认方法;
静态方法;
}
格式
interface 接口名 {
修饰符 default 返回值类型 方法名() {
代码;
}
}
public interface Swimmable {
//默认方法;
default void func(){
System.out.println("接口中的抽象方法");
}
}
class SwimmableImpl implements Swimmable{
public static void main(String[] args) {
new SwimmableImpl().func();
}
}
使用
格式
interface 接口名 {
修饰符 static 返回值类型 方法名() {
代码;
}
}
例如
public interface Swimmable {
//静态方法;
static void staticFunc(){
System.out.println("接口中的静态方法");
}
public static void main(String[] args) {
//直接接口名.静态方法调用
Swimmable.staticFunc();
}
}
注意:
静态方法不可以重写
传统写法
public class TestConsumer {
public static void test(Consumer consumer){
int[] arry = {10, 20, 30};
consumer.accept(arry);
}
public static void main(String[] args) {
//Lambda表达式
test(s->{
int count = 0;
for (int i : s) {
count+=i;
}
System.out.println(count);
});
}
}
方法引用
public class TestConsumer {
public static void test(Consumer consumer){
int[] arry = {10, 20, 30};
consumer.accept(arry);
}
public static void getMax(int [] arry){
int count = 0;
for (int i : arry) {
count+=i;
}
System.out.println(count);
}
public static void main(String[] args) {
//方法引用
test(TestConsumer::getMax);
}
}
方法引用的格式
符号表示 ::
符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。
应用场景 : 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用。
常见引用方式
方法引用在JDK 8中使用方式相当灵活,有以下几种形式:
instanceName::methodName 对象::方法名
ClassName::staticMethodName 类名::静态方法
ClassName::methodName 类名::普通方法
ClassName::new 类名::new 调用的构造器
TypeName[]::new String[]::new 调用数组的构造器
小结
首先了解Lambda表达式的冗余情况,体验了方法引用,了解常见的方法引用方式
这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:
// 对象::实例方法
@Test
public void test01() {
Date now = new Date();
Supplier supp = () -> {
return now.getTime();
};
System.out.println(supp.get());
Supplier supp2 = now::getTime;
System.out.println(supp2.get());
}
方法引用的注意事项
由于在 java.lang.System 类中已经存在了静态方法 currentTimeMillis ,所以当我们需要通过Lambda来调用该 方法时,可以使用方法引用 , 写法是:
// 类名::静态方法
@Test
public void test02() {
Supplier supp = () -> {
return System.currentTimeMillis();
};
System.out.println(supp.get());
Supplier supp2 = System::currentTimeMillis;
System.out.println(supp2.get());
}
Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。
// 类名::实例方法
@Test
public void test03() {
Function f1 = s -> {
return s.length();
};
System.out.println(f1.apply("abc"));
Function f2 = String::length;
System.out.println(f2.apply("abc"));
BiFunction bif = String::substring;
String hello = bif.apply("hello", 2);
System.out.println("hello = " + hello);
}
由于构造器的名称与类名完全一样。所以构造器引用使用 类名称::new 的格式表示。首先是一个简单的 Person 类:
public class Person {
private String name;
private int age;
public Person() {
System.out.println("无参构造方法");
}
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("参构造方法 name:" + name + ",age:" + age);
}
}
要使用这个函数式接口,可以通过方法引用传递:
// 类名::new
@Test
public void test04() {
Supplier sup = () -> {
return new Person();
};
System.out.println(sup.get());
Supplier sup2 = Person::new;
System.out.println(sup2.get());
BiFunction fun2 = Person::new;
System.out.println(fun2.apply("张三", 18));
}
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。
// 类型[]::new
@Test
public void test05() {
Function fun = (len) -> {
return new String[len];
};
String[] arr1 = fun.apply(10);
System.out.println(arr1 + ", " + arr1.length);
Function fun2 = String[]::new;
String[] arr2 = fun.apply(5);
System.out.println(arr2 + ", " + arr2.length);
}
小结
方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为 Lambda表达式的缩写形式 , 不过要注意的是方法引用只能"引用"已经存在的方法!
集合处理数据的弊端
当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。我们来体验 集合操作数据的弊端,需求如下:
一个ArrayList集合中存储有以下数据:张无忌,周芷若,赵敏,张强,张三丰 需求:
1.拿到所有姓张的
2.拿到名字长度为3个字的
3.打印这些数据
@Test
public void test06(){
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
// 1.拿到所有姓张的
// {"张无忌", "张强", "张三丰"}
ArrayList zhangList = new ArrayList<>();
for (String name : arrayList) {
if (name.startsWith("张")) {
zhangList.add(name);
}
}
// 2.拿到名字长度为3个字的
// {"张无忌", "张三丰"}
ArrayList threeList = new ArrayList<>();
for (String name : zhangList) {
if (name.length() == 3) {
threeList.add(name);
}
}
// 3.打印这些数据
for (String name : threeList) {
System.out.println(name);
}
}
循环遍历的弊端
这段代码中含有三个循环,每一个作用不同:
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。每个需求都要循环一次,还要搞一个新集合来装数据,如果希望再次遍历,只能再使 用另一个循环从头开始。
Stream的更优写法
@Test
public void test06(){
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
//stream流写法
arrayList.stream()
.filter(s -> s.startsWith("张"))
.filter(s -> s.length()==3)
.forEach(System.out::println);
}
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:
获取流、过滤姓张、过滤长度为3、逐一打印。
我们真 正要做的事情内容被更好地体现在代码中。
Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工 处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。
Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。
获取一个流非常简单,有以下几种常用的方式:
java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P8tz9P6k-1646227178503)(C:/Users/86178/AppData/Roaming/Typora/typora-user-images/image-20220211212226552.png)]
@Test
public void test07(){
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
//list
Stream stream = arrayList.stream();
//set
HashSet hashSet = new HashSet<>();
Stream stringStream = hashSet.stream();
//queue
Deque arrayDeque = new ArrayDeque();
Stream arrayDequeStream = arrayDeque.stream();
}
java.util.Map 接口不是 Collection 的子接口,所以获取对应的流需要分key、value或entry等情况:
//map
HashMap hashMap = new HashMap<>();
Stream keyStream = hashMap.keySet().stream();
Stream valueStream = hashMap.values().stream();
Stream> entryStream = hashMap.entrySet().stream();
由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单:
@Test
public void test08(){
Stream stringStream = Stream.of("a", "b", "c");
String[] arr = {"a", "b", "c"};
Stream arr1 = Stream.of(arr);
Integer[] arr2 = {1, 2, 3};
Stream integerStream = Stream.of(arr2);
//注意:基本数据类型的数组不行, 下面这种写法,将int数据作为一个数据
int[] arr3 = {1, 2, 3};
Stream stream = Stream.of(arr3);
}
Stream流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。本小节中,终结方法包括 count 和 forEach 方法。
非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结 方法。)
forEach 用来遍历流中的数据
@Test
public void test07(){
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
Stream stream = arrayList.stream();
//forEach遍历数据
//stream.forEach(s -> System.out.println(s));
//简写
stream.forEach(System.out::println);
}
Stream流提供 count 方法来统计其中的元素个数:
@Test
public void test07(){
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
//count统计数据个数
System.out.println(arrayList.stream().count());
}
filter用于过滤数据,返回符合过滤条件的数据
可以通过 filter 方法将一个流转换成另一个子集流。
筛选出张开头的数据
@Test
public void test07(){
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
//filter过滤数据,返回符合过滤条件的数据
Stream stream = arrayList.stream().filter(s -> s.startsWith("张"));
stream.forEach(System.out::println);
}
//打印数据
张无忌
张强
张三丰
limit 方法可以对流进行截取,只取用前n个。
Stream limit(long maxSize);
参数是一个long型,如果集合当前长度大于参数则进行截取。否则不进行操作。基本使用:
@Test
public void test07(){
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
//limit 对流进行截取,只取用前n个
arrayList.stream().limit(3).forEach(System.out::println);
}
//打印数据
张无忌
周芷若
赵敏
如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流。
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:
@Test
public void test07(){
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
//skip 跳过前几个数据,得到一个新的流
arrayList.stream().skip(3).forEach(System.out::println);
}
//打印数据
张强
张三丰
如果需要将流中的元素映射到另一个流中,可以使用 map 方法。
该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
@Test
public void test08(){
Stream stringStream = Stream.of("11", "22", "33");
Stream integerStream = stringStream.map(Integer::parseInt);
integerStream.forEach(s-> System.out.println(s + 10));
}
这段代码中, map 方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为 Integer 类对象)。
如果需要将Stream中的Integer类型数据转成int类型,可以使用 mapToInt 方法。方法签名:
IntStream mapToInt(ToIntFunction mapper);
@Test
public void test11(){
// Integer占用的内存比int多,在Stream流操作中会自动装箱和拆箱
Stream stream = Arrays.stream(new Integer[]{1, 2, 3, 4, 5});
// 先将流中的Integer数据转成int,后续都是操作int类型
IntStream intStream = stream.mapToInt(Integer::intValue);
//找到大于3的数据并打印
intStream.filter(n-> n > 3).forEach(System.out::println);
}
IntStream和Stream相互转换
@Test
public void test12(){
//int[] -> IntStream -> Stream -> Integer[]
int[] num = {3, 4, 5};
//1. int[] -> IntStream
IntStream stream = Arrays.stream(num);
//2. IntStream -> Stream
Stream boxed = stream.boxed();
//3. Stream -> Integer[]
Integer[] result = boxed.toArray(Integer[]::new);
System.out.println(Arrays.toString(result));
// one line
Integer[] oneLineResult = Arrays.stream(num).boxed().toArray(Integer[]::new);
System.out.println(Arrays.toString(oneLineResult));
}
如果需要将数据排序,可以使用 sorted 方法。方法签名:
Stream sorted();
Stream sorted(Comparator comparator);
基本使用
public void test08(){
Stream stringStream = Stream.of("b", "a", "c");
//sorted(): 根据元素的自然顺序排序
stringStream.sorted().forEach(System.out::println);
Stream.of(33, 45, 23, 10).sorted().forEach(System.out::println);
//10
//23
//33
//45
// sorted(Comparator super T> comparator): 根据比较器指定的规则排序
Stream.of(33, 45, 23, 10).sorted((s1, s2) -> s2 - s1).forEach(System.out::println);
//45
//33
//23
//10
}
如果需要去除重复数据,可以使用 distinct 方法。方法签名:
@Test
public void test08(){
Stream stringStream = Stream.of("b", "a", "c", "c", "b");
stringStream.distinct().forEach(System.out::println);
}
//打印数据
b
a
c
如果是自定义类型如何是否也能去除重复的数据呢?
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Test
public void testDistinct2() {
Stream.of(
new Person("刘德华", 58),
new Person("张学友", 56),
new Person("张学友", 56),
new Person("黎明", 52))
.distinct()
.forEach(System.out::println);
}
//打印数据
Person{name='刘德华', age=58}
Person{name='张学友', age=56}
Person{name='黎明', age=52}
自定义类型是根据对象的hashCode和equals来去除重复元素的。
如果需要判断数据是否匹配指定的条件,可以使用 Match 相关方法。
boolean allMatch(Predicate predicate);
boolean anyMatch(Predicate predicate);
boolean noneMatch(Predicate predicate);
@Test
public void test09(){
Stream integerStream = Stream.of(11, 44, 33);
//allMatch: 元素是否全部满足条件
//boolean b = integerStream.allMatch(integer -> integer > 0);
//System.out.println(b);
//anyMatch: 元素是否任意有一个满足条件
//boolean b1 = integerStream.anyMatch(integer -> integer > 0);
//System.out.println(b1);
//noneMatch: 元素是否全部不满足条件
boolean b2 = integerStream.noneMatch(integer -> integer > 0);
System.out.println(b2);
}
如果需要找到某些数据,可以使用 find 相关方法。方法签名:
Optional findFirst();
Optional findAny();
@Test
public void test07(){
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
//findFirst 获取第一个数据
Optional first = arrayList.stream().findFirst();
System.out.println(first.get());
System.out.println(arrayList.stream().findAny().get());
}
findFirst是从流中找出第一个元素。
findAny则是从流中找出任意一个元素。
findAny()操作,返回的元素是不确定的,对于同一个列表多次调用findAny()有可能会返回不同的值。使用findAny()是为了更高效的性能。如果是数据较少,串行地情况下,一般会返回第一个结果,如果是并行的情况,那就不能确保是第一个。
如果需要获取最大和最小值,可以使用 max 和 min 方法。方法签名:
Optional max(Comparator comparator);
Optional min(Comparator comparator);
@Test
public void test09(){
Stream integerStream = Stream.of(11, 44, 33, -1, 15);
//max
Optional max = integerStream.max((o1, o2) -> o1 - o2);
System.out.println(max.get());
//min
Stream integerStream2 = Stream.of(11, 44, 33, -1, 15);
System.out.println(integerStream2.min((o1, o2) -> o1 - o2).get());
}
如果需要将所有数据归纳得到一个数据,可以使用 reduce 方法。方法签名:
T reduce(T identity, BinaryOperator accumulator);
@Test
public void test09(){
Stream integerStream = Stream.of(4, 5, 3, 9);
//T reduce(T identity, BinaryOperator accumulator);
//T identity 默认值
//BinaryOperator accumulator 对数据处理方式
// reduce:
// 第一次将默认做赋值给x, 取出第一个元素赋值给y,进行操作
// 第二次,将第一次的结果赋值给x, 取出二个元素赋值给y,进行操作
// 第三次,将第二次的结果赋值给x, 取出三个元素赋值给y,进行操作
// 第四次,将第三次的结果赋值给x, 取出四个元素赋值给y,进行操作
//求和1
Integer reduce = integerStream.reduce(0, (x, y) -> x + y);
System.out.println(reduce);
//求和2
Stream integerStream1 = Stream.of(4, 5, 3, 9);
System.out.println(integerStream1.reduce(0, Integer::sum));
//取最大值
Stream integerStream2 = Stream.of(4, 5, 3, 9);
System.out.println(integerStream2.reduce(0, (x, y)-> x > y ? x : y ));
}
@Test
public void testDistinct2() {
// 求出所有年龄的总和
int totalAge = Stream.of(
new Person("刘德华", 58),
new Person("张学友", 56),
new Person("郭富城", 54),
new Person("黎明", 52))
.map((p) -> p.getAge())
.reduce(0, (x, y) -> x + y);
System.out.println("totalAge = " + totalAge);
// 找出最大年龄
// 求出所有年龄的总和
int maxAge = Stream.of(
new Person("刘德华", 58),
new Person("张学友", 56),
new Person("郭富城", 54),
new Person("黎明", 52))
.map((p) -> p.getAge())
.reduce(0, (x, y) -> x > y ? x :y);
System.out.println("maxAge = " + maxAge);
}
如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :
static Stream concat(Stream a, Stream b)
@Test
public void test08(){
Stream stringStream1 = Stream.of("a");
Stream stringStream2 = Stream.of("b");
Stream concat = Stream.concat(stringStream1, stringStream2);
concat.forEach(System.out::println);
}
@Test
public void test08(){
/**
* 现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下
* 若干操作步骤:
* 1. 第一个队伍只要名字为3个字的成员姓名;
* 2. 第一个队伍筛选之后只要前3个人;
* 3. 第二个队伍只要姓张的成员姓名;
* 4. 第二个队伍筛选之后不要前2个人;
* 5. 将两个队伍合并为一个队伍;
* 6. 根据姓名创建 Person 对象;
* 7. 打印整个队伍的Person对象信息。
*/
ArrayList one = new ArrayList<>();
Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七公");
ArrayList two = new ArrayList<>();
Collections.addAll(two, "古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱", "张三");
//1. 第一个队伍只要名字为3个字的成员姓名
//2. 第一个队伍筛选之后只要前3个人;
Stream oneStream = one.stream().filter(s -> s.length() == 3).limit(3);
//3. 第二个队伍只要姓张的成员姓名;
//4. 第二个队伍筛选之后不要前2个人;
Stream twoStream = two.stream().filter(s -> s.startsWith("张")).skip(2);
//5. 将两个队伍合并为一个队伍;
//6. 根据姓名创建 Person 对象;
//7. 打印整个队伍的Person对象信息。
Stream.concat(oneStream, twoStream).map(Person::new).forEach(System.out::println);
}
对流操作完成之后,如果需要将流的结果保存到数组或集合中,可以收集流中的数据。
Stream流提供 collect 方法,其参数需要一个 java.util.stream.Collector 接口对象来指定收集到哪种集合中。java.util.stream.Collectors 类提供一些方法,可以作为 Collector`接口的实例:
public static Collector> toList() //转换为 List 集合。
public static Collector> toSet() //转换为 Set 集合。
@Test
public void test09(){
//collect 将流中的数据收集到list集合中
List list = Stream.of(4, 5, 3, 9).collect(Collectors.toList());
System.out.println("list = " + list);
//将流中的数据收集到set集合中
Set set = Stream.of(4, 5, 3, 9).collect(Collectors.toSet());
System.out.println("set = " + set);
//将流中的数据收集到指定(ArrayList)集合中
ArrayList
当我们使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作。比如获取最大值,获取最小 值,求总和,平均值,统计数量。
@Test
public void testDistinct2() {
// 求出所有年龄的总和
Stream personStream = Stream.of(
new Person("刘德华", 58),
new Person("张学友", 56),
new Person("郭富城", 54),
new Person("黎明", 52));
//最大值
//Optional personMax = personStream.collect(Collectors.maxBy((p1, p2) -> p1.getAge() - p2.getAge()));
//System.out.println("personMax.get() = " + personMax.get());
//最小值
//Optional min = personStream.collect(Collectors.minBy((p1, p2) -> p1.getAge() - p2.getAge()));
//System.out.println("min.get() = " + min.get());
//求和
int sum = personStream.collect(Collectors.summingInt(s -> s.getAge()));
System.out.println("sum = " + sum);
//求平均值
//Double averaging = personStream.collect(Collectors.averagingInt(s -> s.getAge()));
//System.out.println("averaging = " + averaging);
//统计数量
//Long count = personStream.collect(Collectors.counting());
//System.out.println("count = " + count);
}
@Test
public void testDistinct2() {
// 求出所有年龄的总和
Stream personStream = Stream.of(
new Person("刘德华", 58),
new Person("张学友", 40),
new Person("郭富城", 54),
new Person("黎明", 20));
//分组
Map> groupMap = personStream.collect(Collectors.groupingBy(s -> {
if (s.getAge() > 50) {
return "老年";
} else {
return "青年";
}
}));
groupMap.forEach((k, v)-> System.out.println(k + "::" + v));
}
//打印结果
青年::[Person{name='张学友', age=40}, Person{name='黎明', age=20}]
老年::[Person{name='刘德华', age=58}, Person{name='郭富城', age=54}]
多级分组
@Test
public void testDistinct2() {
// 求出所有年龄的总和
Stream personStream = Stream.of(
new Person("刘德华", 58),
new Person("张学友", 40),
new Person("郭富城", 54),
new Person("小明", 15),
new Person("黎明", 20));
//多级分组
Map> groupMap = personStream.collect(Collectors.groupingBy(s -> {
if (s.getAge() >= 50) {
return "老年";
} else if (s.getAge() >= 30 && s.getAge() < 50){
return "中年";
} else if (s.getAge() >= 18 && s.getAge() < 30){
return "青年";
} else {
return "未成年";
}
}));
groupMap.forEach((k, v)-> System.out.println(k + "::" + v));
}
//数据打印
青年::[Person{name='黎明', age=20}]
未成年::[Person{name='小明', age=15}]
老年::[Person{name='刘德华', age=58}, Person{name='郭富城', age=54}]
中年::[Person{name='张学友', age=40}]
Collectors.partitioningBy 会根据值是否为true,把集合分割为两个列表,一个true列表,一个false列表。
@Test
public void testDistinct2() {
// 求出所有年龄的总和
Stream personStream = Stream.of(
new Person("刘德华", 58),
new Person("张学友", 40),
new Person("郭富城", 54),
new Person("小明", 15),
new Person("黎明", 20));
//partitioningBy 分区
Map> collect = personStream.collect(Collectors.partitioningBy(s -> s.getAge() > 50));
collect.forEach((k, v) -> System.out.println(k + "::" + v));
}
//打印结果
false::[Person{name='张学友', age=40}, Person{name='小明', age=15}, Person{name='黎明', age=20}]
true::[Person{name='刘德华', age=58}, Person{name='郭富城', age=54}]
Collectors.joining 会根据指定的连接符,将所有元素连接成一个字符串。
@Test
public void testDistinct2() {
// 求出所有年龄的总和
Stream personStream = Stream.of(
new Person("刘德华", 58),
new Person("张学友", 40),
new Person("郭富城", 54),
new Person("小明", 15),
new Person("黎明", 20));
//joining 拼接
String collect = personStream.map(p -> p.getName()).collect(Collectors.joining("_", "<", ">"));
System.out.println("collect = " + collect);
}
//打印结果
collect = <刘德华_张学友_郭富城_小明_黎明>
总结:
收集Stream流中的结果
到集合中: Collectors.toList()/Collectors.toSet()/Collectors.toCollection()
到数组中: toArray()/toArray(int[]::new)
聚合计算: Collectors.maxBy/Collectors.minBy/Collectors.counting/Collectors.summingInt/Collectors.averagingInt
分组: Collectors.groupingBy
分区: Collectors.partitionBy
拼接: Collectors.joinging
目前我们使用的Stream流是串行的,就是在一个线程上执行。
@Test
public void test0Serial() {
long count = Stream.of(4, 5, 3, 9, 1, 2, 6)
.filter(s -> {
System.out.println(Thread.currentThread() + ", s = " + s);
return true;
})
.count();
System.out.println("count = " + count);
}
Thread[main,5,main], s = 4
Thread[main,5,main], s = 5
Thread[main,5,main], s = 3
Thread[main,5,main], s = 9
Thread[main,5,main], s = 1
Thread[main,5,main], s = 2
Thread[main,5,main], s = 6
count = 7
parallelStream其实就是一个并行执行的流。它通过默认的ForkJoinPool,可能提高多线程任务的速度。
@Test
public void testgetParallelStream() {
ArrayList list = new ArrayList<>();
// 直接获取并行的流
Stream parallelStream = list.parallelStream();
// 将串行流转成并行流
Stream integerStream = list.stream().parallel();
}
并行操作代码:
@Test
public void test13(){
long count = Stream.of(4, 5, 3, 9, 1, 2, 6)
.parallel()
.filter(s -> {
System.out.println(Thread.currentThread() + ", s = " + s);
return true;
})
.count();
System.out.println("count = " + count);
}
//打印结果
Thread[main,5,main], s = 1
Thread[ForkJoinPool.commonPool-worker-2,5,main], s = 4
Thread[ForkJoinPool.commonPool-worker-1,5,main], s = 5
Thread[ForkJoinPool.commonPool-worker-3,5,main], s = 3
Thread[ForkJoinPool.commonPool-worker-5,5,main], s = 9
Thread[ForkJoinPool.commonPool-worker-4,5,main], s = 6
Thread[main,5,main], s = 2
count = 7
使用for循环,串行Stream流,并行Stream流来对5亿个数字求和。看消耗的时间。
private static long times = 50000000000L;
private long start;
@Before
public void init(){
start = System.currentTimeMillis();
}
@After
public void destory(){
long end = System.currentTimeMillis();
System.out.println("消耗时间:" + (end - start));
}
//并发Stream 消耗时间:3520
@Test
public void parallelStream(){
LongStream.rangeClosed(0, times)
.parallel().
reduce(0, Long::sum);
}
//串行Stream 消耗时间:16013
@Test
public void serialStream() {
LongStream.rangeClosed(0, times)
.reduce(0, Long::sum);
}
//for循环 消耗时间:10972
@Test
public void forAdd() {
long result = 0L;
for (long i = 0L; i <= times; i++) {
result += i;
}
}
我们可以看到parallelStream的效率是最高的。
Stream并行处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作。
@Test
public void test14(){
ArrayList list = new ArrayList<>();
//将1-1000个数,循环add到list中
IntStream.rangeClosed(1, 1000).parallel().forEach( i -> list.add(i));
System.out.println(list.size());//957
}
正常list.size()=1000,但是由于是并行的,list.sizez没有1000个。
并行问题解决方案
//并发问题解决方案一:加锁
Object o = new Object();
IntStream.rangeClosed(1, 1000).parallel().forEach( i -> {
synchronized (o) {
list.add(i);
}
});
System.out.println(list.size());
//并发问题解决方案二:使用线程安全的集合
Vector integers = new Vector<>();
IntStream.rangeClosed(1, 1000).parallel().forEach( i -> integers.add(i));
System.out.println(integers.size());
//并发问题解决方案三:使用Collections.synchronizedList
ArrayList list2 = new ArrayList<>();
List synchronizedList = Collections.synchronizedList(list2);
IntStream.rangeClosed(1, 1000).parallel().forEach( i -> synchronizedList.add(i));
System.out.println(synchronizedList.size());
//并发问题解决方案四:调用collect/toArray
List integerList = IntStream.rangeClosed(1, 1000).parallel().boxed().collect(Collectors.toList());
System.out.println(integerList.size());
parallelStream使用的是Fork/Join框架。Fork/Join框架自JDK 7引入。Fork/Join框架可以将一个大任务拆分为很多小 任务来异步执行。 Fork/Join框架主要包含三个模块:
ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。
典型的应用比如快速排序算法, ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成 两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处 理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在 于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。
Fork/Join最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的cpu,那么如何利用好这个空闲的 cpu就成了提高性能的关键,而这里我们要提到的工作窃取(work-stealing)算法就是整个Fork/Join框架的核心理念。
Fork/Join工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。
那么为什么需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖 的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来 执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的 任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就 去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任 务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永 远从双端队列的尾部拿任务执行。
优点:充分利用线程进行并行计算,并减少了线程间的竞争,
缺点:在某些情况下还是存在竞争, 比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
上文中已经提到了在Java 8引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行,也就是我们使用了ForkJoinPool的ParallelStream。
对于ForkJoinPool通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。可以通过设置 系统属性:java.util.concurrent.ForkJoinPool.common.parallelism=N (N为线程数量),来调整ForkJoinPool的线 程数量,可以尝试调整成不同的参数来观察每次的输出结果。
//1.创建一个求和的任务
class SumRecursiveTask extends RecursiveTask{
//数据拆分临界值
private static final long THRESHOLD = 5000L;
//起始值
private final long start;
//结束值
private final long end;
SumRecursiveTask(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end - start;
if (length < THRESHOLD){
// 任务不用再拆分了.可以计算了
long sum = 0;
for (long i = start; i <= end; i++) {
sum +=i;
}
System.out.println("计算: " + start + " -> " + end + ",结果为: " + sum);
return sum;
}else {
// 数量大于预定的数量,任务还需要再拆分
long middle = (start + end) / 2;
System.out.println("拆分: 左边 " + start + " -> " + middle + ", 右边 " + (middle + 1) + " -> " + end);
SumRecursiveTask left = new SumRecursiveTask(start, middle);
left.fork();
SumRecursiveTask right = new SumRecursiveTask(middle + 1, end);
right.fork();
return left.join() + right.join();
}
}
}
@Test
public void testForkJoin(){
//1、创建ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool();
//2、创建task
SumRecursiveTask sumRecursiveTask = new SumRecursiveTask(1, 1000000L);
//3、执行
Long result = forkJoinPool.invoke(sumRecursiveTask);
System.out.println("result = " + result);
}
以前对null的处理方式
@Test
public void test15() {
String userName = "凤姐";
// String userName = null;
if (userName != null) {
System.out.println("用户名为:" + userName);
} else {
System.out.println("用户名不存在");
}
}
Optional是一个没有子类的工具类,Optional是一个可以为null的容器对象。它的作用主要就是为了解决避免Null检 查,防止NullPointerException。
Optional类的创建方式:
//Optional.of(T t) : 创建一个 Optional 实例
//Optional.empty() : 创建一个空的 Optional 实例
//Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
上面的例子用Optional:
@Test
public void test16() {
Optional userNameO = Optional.of("凤姐");
// isPresent() : 判断是否包含值,包含值返回true,不包含值返回false。
if (userNameO.isPresent()) {
// get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException。
String userName = userNameO.get();
System.out.println("用户名为:" + userName);
} else {
System.out.println("用户名不存在");
}
}
//isPresent() : 判断是否包含值,包含值返回true,不包含值返回false
//get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException
//orElse(T t) : 如果调用对象包含值,返回该值,否则返回参数t
//orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
//map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()
使用示例:
@Test
public void test15() {
String userName = "凤姐";
// String userName = null;
if (userName != null) {
System.out.println("用户名为:" + userName);
} else {
System.out.println("用户名不存在");
}
//Optional.of(T t) : 创建一个 Optional 实例
//Optional.empty() : 创建一个空的 Optional 实例
//Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
Optional optional1 = Optional.of("凤姐");
Optional optional2 = Optional.empty();
Optional optional3 = Optional.ofNullable(null);
//isPresent() : 判断是否包含值,包含值返回true,不包含值返回false
System.out.println("optional1.isPresent() = " + optional1.isPresent());
System.out.println("optional2.isPresent() = " + optional2.isPresent());
System.out.println("optional3.isPresent() = " + optional3.isPresent());
//ifPresent():如果有值执行函数
optional1.ifPresent(s-> System.out.println("s = " + s));
optional2.ifPresent(s-> System.out.println("s = " + s));
//(java9特性)存在做的什么,不存在做点什么
/*
optional1.ifPresentOrElse(s -> System.out.println("用户名为" + s)
, () -> System.out.println("用户名不存在"));
optional2.ifPresentOrElse(s -> System.out.println("用户名为" + s)
, () -> System.out.println("用户名不存在"));
*/
//get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException
System.out.println("optional1.get() = " + optional1.get());
//System.out.println("optional2.get() = " + optional2.get());
//System.out.println("optional3.get() = " + optional3.get());
//orElse(T t) : 如果调用对象包含值,返回该值,否则返回参数t
String orElse1 = optional1.orElse("没有值");
System.out.println("orElse1 = " + orElse1);
String orElse2 = (String) optional2.orElse("没有值");
System.out.println("orElse2 = " + orElse2);
Object orElse3 = optional3.orElse("没有值");
System.out.println("orElse3 = " + orElse3);
//orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
String s1 = optional1.orElseGet(()->"没有值");
System.out.println("s1 = " + s1);
String s2 = (String) optional2.orElseGet(()->"没有值");
System.out.println("s2 = " + s2);
Object s3 = optional3.orElseGet(() -> "没有值");
System.out.println("s3 = " + s3);
//map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()
String s4 = optional1.map(s -> s.toUpperCase()).orElse(null);
System.out.println("s4 = " + s4);
String s5 = optional2.map(s -> s.toString().toUpperCase()).orElse(null);
System.out.println("s5 = " + s5);
}
Optional是一个可以为null的容器对象。orElse,ifPresent,ifPresentOrElse,map等方法避免对null的判断,写出 更加优雅的代码。
设计很差: 在java.util和java.sql的包中都有日期类,java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期。
此外用于格式化和解析的类在java.text包中定义。
非线程安全:java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
时区处理麻烦:日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和 java.util.TimeZone类,但他们同样存在上述所有的问题。
JDK 8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于 java.time 包 中,下面是一些关键类。
Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公历。平年有365天,闰年是366 天。
此外Java 8还提供了4套其他历法,分别是:
JDK 8新的日期和时间 API的优势:
表示日期,包含年月日,格式为 2019-10-16
@Test
public void test01() {
// 创建指定日期
LocalDate fj = LocalDate.of(1985, 9, 23);
System.out.println("fj = " + fj); // 1985-09-23
// 得到当前日期
LocalDate nowDate = LocalDate.now();
System.out.println("nowDate = " + nowDate); // 2019-10-16
// 获取日期信息
System.out.println("年: " + nowDate.getYear()); //
System.out.println("月: " + nowDate.getMonth()); //月: FEBRUARY
System.out.println("月: " + nowDate.getMonthValue());//月: 2
System.out.println("日: " + nowDate.getDayOfMonth());//
System.out.println("星期: " + nowDate.getDayOfWeek());//星期: SUNDAY
System.out.println("星期: " + nowDate.getDayOfWeek().getValue());//星期: 7
}
LocalTime类: 获取时间信息。格式为 16:38:54.158549300
@Test
public void test02() {
// 得到指定的时间
LocalTime time = LocalTime.of(12, 15, 28, 129_900_000);
System.out.println("time = " + time);
// 得到当前时间
LocalTime nowTime = LocalTime.now();
System.out.println("nowTime = " + nowTime);
// 获取时间信息
System.out.println("小时: " + nowTime.getHour());
System.out.println("分钟: " + nowTime.getMinute());
System.out.println("秒: " + nowTime.getSecond());
System.out.println("纳秒: " + nowTime.getNano());
}
LocalDateTime类: 获取日期时间信息。格式为 2018-09-06T15:33:56.750
@Test
public void test03(){
// 得到指定的时间
LocalDateTime fj = LocalDateTime.of(1985, 9, 23, 9, 10, 20);
System.out.println("fj = " + fj);
// 得到当前日期时间
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now); //now = 2022-02-13T20:59:13.723
//年
System.out.println("now.getYear() = " + now.getYear());
//年份的天数
System.out.println("now.getDayOfYear() = " + now.getDayOfYear());
//月
System.out.println("now.getMonth() = " + now.getMonth());
System.out.println("now.getMonthValue() = " + now.getMonthValue());
//日
System.out.println("now.getDayOfMonth() = " + now.getDayOfMonth());
//时分秒
System.out.println("now.getHour() = " + now.getHour());
System.out.println("now.getMinute() = " + now.getMinute());
System.out.println("now.getSecond() = " + now.getSecond());
System.out.println("now.getNano() = " + now.getNano());
}
对日期时间的修改,对已存在的LocalDate对象,创建它的修改版,最简单的方式是使用withAttribute方法。 withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。以下所有的方法都返回了一个修改属性的对 象,他们不会影响原来的对象。
@Test
public void test05() {
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now);
// 修改日期时间
LocalDateTime setYear = now.withYear(2078);
System.out.println("修改年份: " + setYear);
System.out.println("now == setYear: " + (now == setYear));
System.out.println("修改月份: " + now.withMonth(6));
System.out.println("修改小时: " + now.withHour(9));
System.out.println("修改分钟: " + now.withMinute(11));
// 在当前对象的基础上加上或减去指定的时间
//加5天,返回新对象
LocalDateTime localDateTime = now.plusDays(5);
System.out.println("5天后: " + localDateTime);
System.out.println("now == localDateTime: " + (now == localDateTime));
System.out.println("10年后: " + now.plusYears(10));
System.out.println("20月后: " + now.plusMonths(20));
System.out.println("20年前: " + now.minusYears(20));
System.out.println("5月前: " + now.minusMonths(5));
System.out.println("100天前: " + now.minusDays(100));
}
// 日期时间的比较
@Test
public void test06() {
// 在JDK8中,LocalDate类中使用isBefore()、isAfter()、equals()方法来比较两个日期,可直接进行比较。
LocalDate now = LocalDate.now();
LocalDate date = LocalDate.of(2018, 8, 8);
System.out.println(now.isBefore(date)); // false
System.out.println(now.isAfter(date)); // true
System.out.println(now.equals(date));
}
通过 java.time.format.DateTimeFormatter 类可以进行日期时间解析与格式化。
// 日期格式化
@Test
public void test04() {
// 得到当前日期时间
LocalDateTime now = LocalDateTime.now();
System.out.println("now = " + now);
// 将日期时间格式化为字符串
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String format = now.format(formatter);
System.out.println("format = " + format);
// 将字符串解析为日期时间
LocalDateTime dateTime = LocalDateTime.parse("1985-09-23 10:12:22", formatter);
System.out.println("dateTime = " + dateTime);
}
//打印数据
now = 2022-02-13T22:21:51.182
format = 2022-02-13 22:21:51
dateTime = 1985-09-23T10:12:22
@Test
public void test08(){
Instant now = Instant.now();
System.out.println("当前时间戳 = " + now);
//得到秒
System.out.println("now.getEpochSecond() = " + now.getEpochSecond());
// 得到纳秒
System.out.println("now.getNano() = " + now.getNano());
//加操作
Instant plusSeconds = now.plusSeconds(20);
System.out.println("plusSeconds = " + plusSeconds);
//减操作
Instant minusSeconds = now.minusSeconds(20);
System.out.println("minusSeconds = " + minusSeconds);
}
Duration/Period类: 计算日期时间差。
@Test
public void test09(){
//Duration:计算2个时间(LocalTime,时分秒)的距离
LocalTime now = LocalTime.now();
LocalTime ofTime = LocalTime.of(22, 42, 0);
Duration duration = Duration.between(ofTime, now);
System.out.println("相差的天数 = " + duration.toDays());
System.out.println("相差的小时数 = " + duration.toHours());
System.out.println("相差的分钟数 = " + duration.toMinutes());
System.out.println("相差的秒数 = " + duration.getSeconds());
System.out.println("相差的毫秒数 = " + duration.toMillis());
System.out.println("相差的纳秒数 = " + duration.toNanos());
// Period:计算2个日期(LocalDate,年月日)的距离
LocalDate date = LocalDate.now();
LocalDate date1 = LocalDate.of(2021, 3, 3);
Period between = Period.between(date1, date);
System.out.println("相差的年 = " + between.getYears());
System.out.println("相差的月 = " + between.getMonths());
System.out.println("相差的天 = " + between.getDays());
}
有时我们可能需要获取例如:将日期调整到“下一个月的第一天”等操作。可以通过时间校正器来进行。
//TemporalAdjuster
@Test
public void test10(){
LocalDateTime now = LocalDateTime.now();
//将日期调整到 下个月的第一天
TemporalAdjuster adjuster = temporal -> {
LocalDateTime temporal1 = (LocalDateTime) temporal;
return temporal1.plusMonths(1).withDayOfMonth(1);//下一个月的第一天
};
LocalDateTime localDateTime = now.with(adjuster);
System.out.println("localDateTime = " + localDateTime);
}
TemporalAdjusters提供了大量的常用TemporalAdjuster的实现。
@Test
public void test10(){
LocalDateTime now = LocalDateTime.now();
//获取当月第一天
System.out.println("当月第一天:"+now.with(TemporalAdjusters.firstDayOfMonth()));
//获取下月第一天
System.out.println("下月第一天:"+now.with(TemporalAdjusters.firstDayOfNextMonth()));
//获取明年第一天
System.out.println("明年第一天:"+now.with(TemporalAdjusters.firstDayOfNextYear()));
//获取本年第一天
System.out.println("本年第一天:"+now.with(TemporalAdjusters.firstDayOfYear()));
//获取当月最后一天
System.out.println("当月最后一天:"+now.with(TemporalAdjusters.lastDayOfMonth()));
//获取本年最后一天
System.out.println("本年最后一天:"+now.with(TemporalAdjusters.lastDayOfYear()));
//获取当月第三周星期五
System.out.println("当月第三周星期五:"+now.with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.FRIDAY)));
//获取上周一
System.out.println("上周一:"+now.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)));
//获取下周日
System.out.println("下周日:"+now.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)));
}
Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。
其中每个时区都对应着 ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等。
ZoneId:该类中包含了所有的时区信息。
@Test
public void test11(){
// 设置日期时间的时区
// 1、获取所有的时区ID
ZoneId.getAvailableZoneIds().forEach(System.out::println);
//默认不带时区
LocalDateTime now = LocalDateTime.now(); //中国使用的东八区的时区
System.out.println("now = " + now);
// 2、带时区的类
//now(Clock.systemUTC()) :创建世界标准时间
ZonedDateTime now1 = ZonedDateTime.now(Clock.systemUTC());
System.out.println("now1 = " + now1);
//now() :使用计算机的默认的时区,创建日期时间
ZonedDateTime now2 = ZonedDateTime.now();
System.out.println("now2 = " + now2);
//使用指定的时区创建日期时间
ZonedDateTime now3 = ZonedDateTime.now(ZoneId.of("America/Hermosillo"));
System.out.println("now3 = " + now3);
//修改时区
//withZoneSameInstant 即修改时区,也修改时间
ZonedDateTime now4 = now2.withZoneSameInstant(ZoneId.of("America/Hermosillo"));
System.out.println("now4 = " + now4);
//withZoneSameLocal 只修改时区,不修改时间
ZonedDateTime now5 = now2.withZoneSameLocal(ZoneId.of("America/Hermosillo"));
System.out.println("now5 = " + now5);
}
详细学习了新的日期是时间相关类,
LocalDate表示日期,包含年月日,
LocalTime表示时间,包含时分 秒,
LocalDateTime = LocalDate + LocalTime,
时间的格式化和解析,通过DateTimeFormatter类型进行.
学习了Instant类,方便操作秒和纳秒,一般是给程序使用的
学习Duration/Period计算日期或时间的距离,还使用时间调 整器方便的调整时间
学习了带时区的3个类ZoneDate/ZoneTime/ZoneDateTime
JDK 8新的日期和时间 API的优势:
自从Java 5中引入 注解 以来,注解开始变得非常流行,并在各个框架和项目中被广泛使用。不过注解有一个很大的限 制是:在同一个地方不能多次使用同一个注解。JDK 8引入了重复注解的概念,允许在同一个地方多次使用同一个注 解。在JDK 8中使用**@Repeatable**注解定义重复注解。
package com.sl.java8;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* @author 86178
* @version 1.0.0
* @ClassName Test02.java
* @Description TODO
* @createTime 2022年02月14日 21:11:00
*/
//3. 配置多个重复的注解
@Test02.Mytest("test01")
@Test02.Mytest("test02")
@Test02.Mytest("test01")
public class Test02 {
//1、定义一个重复的注解容器
@Retention(RetentionPolicy.RUNTIME)
@interface MYTests{//重复注解容器
Mytest[] value();
}
//2、定义一个可以重复的注解
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(MYTests.class)
@interface Mytest{
String value();
}
//3. 配置多个重复的注解
@Mytest("tm1")
@Mytest("tm2")
@Mytest("tm3")
public void test(){
}
public static void main(String[] args) {
//4、解析重复注解
//获取类上的注解
//getAnnotationsByType 新增的API用于获取重复的注解
Mytest[] annotationsByType = Test02.class.getAnnotationsByType(Mytest.class);
for (Mytest mytest : annotationsByType) {
System.out.println("mytest = " + mytest);
}
//获取方法上的注解
try {
Mytest[] tests = Test02.class.getMethod("test").getAnnotationsByType(Mytest.class);
for (Mytest test : tests) {
System.out.println("test = " + test);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
JDK 8为@Target元注解新增了两种类型: TYPE_PARAMETER , TYPE_USE 。
TYPE_PARAMETER :表示该注解能写在类型参数的声明语句中。 类型参数声明如:
TYPE_USE :表示注解可以再任何用到类型的地方使用。
TYPE_PARAMETER的使用
public class Test03<@TyptParam t> {
public @NotNull int a = 0;
public <@TyptParam E extends Integer> void test01(){
}
}
@Target(ElementType.TYPE_PARAMETER)
@interface TyptParam {
}
@Target(ElementType.TYPE_USE)
@interface NotNull {
}
通过@Repeatable元注解可以定义可重复注解, TYPE_PARAMETER 可以让注解放在泛型上, TYPE_USE 可以让注解放 在类型的前面