1.jdk1.8新特性
JDK1.8相比1.7之前版本,有以下几方面的优化:
- 速度更快;
- 代码更少(Lambda表达式);
- 强大Stream API;
- 便于并行;
- 最大化减少空指针异常(OPtional类)。
新特性
JDK1.8新增了非常多的特性,最主要的包括以下几个方面:
- Lambda表达式:Lambda允许把函数作为一个方法的参数(函数作为参数传递到方法中)。
- 方法引用:方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
- 默认方法:默认方法就是一个在接口里面有了一个实现的方法。
- 新工具:新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。
- Stream API:新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。
- Date Time API:加强对日期与时间的处理。
- Optional类:Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
- Nashorn:JavaScript引擎:JDK1.8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。
2.Lambda表达式
Lambda表达式体现的是函数式表达思想,只需要将要执行的代码放到函数中(函数就是类中的方法)
Lambda是一个匿名函数,可以理解为一段可以传递的代码。
2.1 标准格式:
Lambda省去面向对象的条条框框,Lambda的标准格式格式由3个部分组成:
(参数类型 参数名称) -> {
代码体;
}
需要注意:
- 参数类型可省略,编译器可以自己推断
- 如果只有一个参数,圆括号可以省略
- 代码块如果只是一行代码,大括号也可以省略
- 如果代码块是一行,且是有结果的表达式,return可以省略
注意:事实上,把Lambda表达式可以看做是匿名内部类的一种简写方式。当然,前提是这个匿名内部类对应的必须是接口,而且接口中必须只有一个函数!Lambda表达式就是直接编写函数的:参数列表、代码体、返回值等信息,用函数来代替完整的匿名内部类!
Lambda与方法的对比
匿名内部类
public void run() {
System.out.println("aa");
}
Lambda:
() -> System.out.println("bb")
2.2用法示例
- 示例一:多线程
我们创建一个线程,原来是这样写的:
jdk1.7写法
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("新线程任务执行!");
}
}).start();
- 体验lambda表达式:
new Thread(() -> {
System.out.println("lambda表达式执行!");
}).start();
-
lambda表达式的好处:可以简化匿名内部类,使代码更加精简。
-
无参数无返回值的Lambda案例
首先创建一个Swimmable接口:
public interface Swimmable {
public abstract void swimming();
}
在测试类中调用接口中的方法:
public class LambdaDemo2 {
public static void main(String[] args) {
goswimming(new Swimmable(){
@Override
public void swimming() {
System.out.println("匿名内部类游泳");
}
});
}
public static void goswimming(Swimmable swimmable){
swimmable.swimming();
}
}
使用lambda表达式简写为:
goswimming(() -> {
System.out.println("lambda表达式游泳");
});
以后我们看到方法的参数是接口就可以考虑使用lambda表达式
我们可以这样认为,lambda表达式就是对接口中的抽象方法的重写
- 有参数有返回值的Lambda案例
创建Smokeable接口:
public interface Smokeable {
public abstract int smoking(String name);
}
在测试类中调用接口中的方法:
public class LambdaDemo3 {
public static void main(String[] args) {
gosmoking(new Smokeable() {
@Override
public int smoking(String name) {
System.out.println("匿名内部类抽了"+name+"的烟");
return 5;
}
});
}
public static void gosmoking(Smokeable smokeable){
int i = smokeable.smoking("中华");
System.out.println("返回值:"+i);
}
}
使用lambda表达式简写为:
gosmoking((name) -> {
System.out.println("lambda表达式抽了"+name+"的烟");
return 6;
});
调用实体类对年龄排序
public class Person {
private String name;
private int age;
private int height;
//省略其他
}
在测试类中创建集合并排序:
假设我们要对集合排序,我们先看JDK7的写法,需要通过匿名内部类来构造一个Comparator:
public class LambdaDemo4 {
public static void main(String[] args) {
List<Person> persons = new ArrayList<Person>();
persons.add(new Person("张三",25,175));
persons.add(new Person("李四",28,179));
persons.add(new Person("王五",21,180));
persons.add(new Person("赵六",30,165));
Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o2.getAge() - o1.getAge();
}
});
for (Person person:persons){
System.out.println(person);
}
}
}
使用lambda表达式简写为:
Collections.sort(persons,(o1,o2)->{
return o1.getAge()-o2.getAge();
});
但在jdk1.8中有一个集合API:sort(Comparator c)方法,接收一个比较器,使排序更加简便:
persons.sort((o1,o2)->{
return o1.getAge()-o2.getAge();
});
集合遍历
下面新建一个集合并遍历打印:
jdk1.7我们是这样写的:
List<Integer> list = Arrays.asList(1,2,3,4,5,6);
for(Integer i:list){
System.out.println(i);
}
jdk1.8给集合添加了一个方法:foreach() ,接收一个对元素进行操作的函数:
list.forEach(integer -> System.out.println(integer));
2.3Lambda的实现原理
匿名内部类在编译的时候会一个class文件
Lambda在程序运行的时候形成一个类
- 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码
- 还会形成一个匿名内部类,实现接口,重写抽象方法
- 在接口的重写方法中会调用新生成的方法.
2.4Lambda省略格式
在Lambda标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略
- 如果小括号内有且仅有一个参数,则小括号可以省略
- 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
(int a) -> { return new Person(); }
省略后:
a -> new Person()
前面写的使用Colleactions的sort方法排序,可以简写为:
//lambda表达式
Collections.sort(persons,(o1,o2)->{
return o1.getAge()-o2.getAge();
});
//简写后:
Collections.sort(persons,(o1,o2) -> o1.getAge() - o2.getAge());
2.5Lambda的前提条件
Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:
- 方法的参数或局部变量类型必须为接口才能使用Lambda
- 接口中有且仅有一个抽象方法
@FunctionalInterface //检测这个接口是不是只有一个抽象方法
public interface Flyable {
public abstract void flying();
//public abstract void flying2();
}
public static void main(String[] args) {
test(() ->{
});
Flyable f = new Flyable() {
@Override
public void flying() {
}
};
Flyable f2 = () -> {
};
}
public static void test(Flyable flyable){
flyable.flying();
}
2.6函数式接口
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。
只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
- FunctionalInterface注解
与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
2.7Lambda和匿名内部类对比
Lambda和匿名内部类在使用上的区别:
- 所需的类型不一样
匿名内部类,需要的类型可以是类,抽象类,接口
Lambda表达式,需要的类型必须是接口 - 抽象方法的数量不一样
匿名内部类所需的接口中抽象方法的数量随意
Lambda表达式所需的接口只能有一个抽象方法 - 实现原理不同
匿名内部类是在编译后会形成class
Lambda表达式是在程序运行的时候动态生成class
当接口中只有一个抽象方法时,建议使用Lambda表达式,其他其他情况还是需要使用匿名内部类
3.JDK 8接口新增的两个方法
3.1JDK 8接口增强介绍
JDK 8以前的接口:
interface 接口名 {
静态常量;
抽象方法;
}
JDK 8对接口的增强,接口还可以有默认方法和静态方法:
interface 接口名 {
静态常量;
抽象方法;
默认方法;
静态方法;
}
3.2默认方法
3.2.1接口引入默认方法的背景
- 在JDK 8以前接口中只能有抽象方法。存在以下问题: 如果给接口新增抽象方法,所有实现类都必须重写这个抽象方法。不利于接口的扩展。
例如,JDK 8 时在Map接口中增加了 forEach 方法:
public interface Map<K, V> {
...
abstract void forEach(BiConsumer<? super K, ? super V> action);
}
通过API可以查询到Map接口的实现类如:
如果在Map接口中增加一个抽象方法,所有的实现类都需要去实现这个方法,那么工程量时巨大的。
因此,在JDK 8时为接口新增了默认方法,效果如下:
public interface Map<K, V> {
...
default void forEach(BiConsumer<? super K, ? super V> action) {
...
}
}
接口中的默认方法实现类不必重写,可以直接使用,实现类也可以根据需要重写。这样就方便接口的扩展。
3.2.2接口默认方法的使用
方式一:实现类直接调用接口默认方法
方式二:实现类重写接口默认方法
public class interfaceDemo2 {
public static void main(String[] args) {
//方式一:实现类直接调用接口默认方法
B b = new B();
b.test2();
//方式二:调用实现类重写接口默认方法
C c = new C();
c.test2();
}
}
interface A{
public default void test2(){
System.out.println("Atest2");
}
}
class B implements A{
}
class C implements A{
@Override
public void test2() {
System.out.println("ctest2");
}
}
3.3静态方法
- 接口静态方法的使用:
-直接使用接口名调用即可:接口名.静态方法名();
public class interfaceDemo3 {
public static void main(String[] args) {
//直接使用接口名调用即可
AAA.test1();
}
}
interface AAA{
static void test1(){
System.out.println("AAA接口的静态方法");
}
}
class BBB implements AAA{
/*@Override 静态方法不能重写
static void test1(){
System.out.println("AAA接口的静态方法");
}*/
}
3.4接口默认方法和静态方法的区别
- 默认方法通过实例调用,静态方法通过接口名调用。
- 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
- 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。
接口中新增的两种方法:
默认方法和静态方法
如何选择呢?如果这个方法需要被实现类继承或重写,使用默认方法,如果接口中的方法不需要被继承就使用静态方法
4.常用内置函数式接口
4.1内置函数式接口由来
- 我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽
象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。
public class FunctionIntefaceDemo1 {
public static void main(String[] args) {
//调用函数式接口
method((arr) -> {
int sum = 0;
for (int i:arr){
sum += i;
}
return sum;
});
}
//使用自定义的函数式接口作为方法的参数
static void method(Operator operator){
int [] arr = {10, 22, 33, 44};
int sum = operator.getSum(arr);
System.out.println("sum="+sum);
}
}
interface Operator{
public abstract int getSum(int [] arr);
}
4.2常用内置函数式接口介绍
它们主要在 java.util.function 包中,下面是最常用的几个接口:
- Supplier接口
@FunctionalInterface
public interface Supplier<T> {
public abstract T get();
}
- Consumer接口
@FunctionalInterface
public interface Consumer<T> {
public abstract void accept(T t);
}
- Function接口
@FunctionalInterface
public interface Function<T, R> {
public abstract R apply(T t);
}
- Predicate接口
@FunctionalInterface
public interface Predicate<T> {
public abstract boolean test(T t);
}
Predicate接口用于做判断,返回boolean类型的值
4.3 Supplier接口
- java.util.function.Supplier 接口,它意味着"供给" ,
对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
@FunctionalInterface
public interface Supplier<T> {
public abstract T get();
}
供给型接口,通过Supplier接口中的get方法可以得到一个值,无参有返回的接口。
使用Lambda表达式返回数组元素最大值
使用 Supplier 接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。提示:接口的泛型请使用java.lang.Integer 类。
public class SupplierDemo {
public static void main(String[] args) {
printMax(() -> {
int [] arr = {10,22,33,5,66};
Arrays.sort(arr);//数组排序
return arr[arr.length-1];//最后就是最大的
});
}
public static void printMax(Supplier<Integer> supplier){
int max = supplier.get();
System.out.println("max="+max);
}
}
4.4 Consumer接口
- java.util.function.Consumer 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛
型参数决定。
@FunctionalInterface
public interface Consumer<T> {
public abstract void accept(T t);
}
使用Lambda表达式将一个字符串转成大写:
Consumer消费型接口,可以拿到accept方法参数传递过来的数据进行处理, 有参无返回的接口。
public class ConsumerDemo {
public static void main(String[] args) {
test(str -> {
System.out.println(str.toUpperCase());
});
}
static void test(Consumer<String> consumer){
consumer.accept("HelloWord");
}
}
默认方法:andThen
- 如果一个方法的参数和返回值全都是 Consumer
类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer接口中的default方法 andThen 。下面是JDK的源代码:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
备注: java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。
要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是“一步接一步”操作。
使用Lambda表达式将一个字符串先转大写再转小写
public class ConsumerDemo {
public static void main(String[] args) {
test(str -> {System.out.println(str.toUpperCase());},str -> {
System.out.println(str.toLowerCase());
});
}
static void test(Consumer<String> c1,Consumer<String> c2){
String str = "HelloWord";
/*c1.accept(str);
c2.accept(str);*/
c1.andThen(c2).accept(str);
}
}
4.5 Function接口
- java.util.function.Function<T,R>
接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件,有参数有返回值。
@FunctionalInterface
public interface Function<T, R> {
public abstract R apply(T t);
}
使用Lambda表达式将字符串转成数字
Function转换型接口,对apply方法传入的T类型数据进行处理,返回R类型的结果,有参有返回的接口。
例如:将 String 类型转换为 Integer 类型。
public class FunctionDemo {
public static void main(String[] args) {
test((str) -> {
return Integer.parseInt(str);;
});
}
static void test(Function<String,Integer> function){
Integer i = function.apply("10");
System.out.println(i);
}
}
运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。当然,通过链式写法可以实现更多步骤的组合。
默认方法:andThen
Function 接口中有一个默认的 andThen 方法,用来进行组合操作。
JDK源代码如下:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多:
public class FunctionDemo {
public static void main(String[] args) {
test((str) -> {
return Integer.parseInt(str);
},(i) -> {
return i*10;
});
}
static void test(Function<String,Integer> f1,Function<Integer,Integer> f2){
/*Integer i = f1.apply("10");
Integer i2 = f2.apply(i);
System.out.println(i2);*/
Integer i = f1.andThen(f2).apply("10");
System.out.println(i);
}
}
第一个操作是将字符串解析成为int数字,第二个操作是乘以10。两个操作通过 andThen 按照前后顺序组合到了一起。
请注意,Function的前置条件泛型和后置条件泛型可以相同。
4.6 Predicate接口
- 有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate< T> 接口。
@FunctionalInterface
public interface Predicate<T> {
public abstract boolean test(T t);
}
Predicate接口用于做判断,返回boolean类型的值
使用Lambda判断一个人名如果超过3个字就认为是很长的名字
对test方法的参数T进行判断,返回boolean类型的结果。用于条件判断的场景:
public class PredicateDemo {
public static void main(String[] args) {
test((str) -> str.length()>3);
}
static void test(Predicate<String> predicate){
if(predicate.test("迪丽热巴")){
System.out.println("名字有点长!");
}
}
}
默认方法:and
既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法 and 。
其JDK源码为:
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
使用Lambda表达式判断一个字符串中即包含W,也包含H
public class PredicateDemo {
public static void main(String[] args) {
test((str) -> {
return str.contains("H");
},(str) -> {
return str.contains("W");
});
}
static void test(Predicate<String> p1,Predicate<String> p2){
String str = "Hello World";
/*boolean b1 = p1.test(str);
boolean b2 = p2.test(str);
if(b1 && b2){
System.out.println("既包含H,又包含W");
}*/
if(p1.and(p2).test(str)){
System.out.println("既包含H,又包含W");
}
}
}
默认方法:or
与 and 的“与”类似,默认方法 or 实现逻辑关系中的“或”。
JDK源码为:
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
使用Lambda表达式判断一个字符串中包含W或者包含H
/*if(p1.test(str) || p2.test(str)){
System.out.println("可能包含H或W");
}*/
if(p1.or(p2).test(str)){
System.out.println("可能包含H或W");
}
默认方法:negate
“与”、“或”已经了解了,剩下的“非”(取反)也会简单。
JDK源代码为:
default Predicate<T> negate() {
return (t) -> !test(t);
}
从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行“!”取反而已。一定要在 test 方法调用之前调用 negate 方法,正如 and 和 or 方法一样:
使用Lambda表达式判断一个字符串中即不包含W
/*if(!p1.test(str)){
System.out.println("没有H");
}*/
if(p1.negate().test(str)){
System.out.println("没有H");
}
5.方法引用
5.1Lambda的冗余场景
使用Lambda表达式求一个数组的和
public class MethodDemo1 {
public static void getSum(int [] arr){
int sum = 0;
for(int i : arr){
sum += i;
}
System.out.println(sum);
}
public static void main(String[] args) {
printSum((arr) -> {
int sum = 0;
for(int i : arr){
sum += i;
}
System.out.println(sum);
});
}
static void printSum(Consumer<int[]> consumer){
int [] arr = {1,55,67,34,3};
consumer.accept(arr);
}
}
体验方法引用简化Lambda
如果我们在Lambda中所指定的功能,已经有其他方法存在相同方案,那是否还有必要再写重复逻辑?可以直接“引 用”过去就好了:
public static void main(String[] args) {
/*printSum((arr) -> {
int sum = 0;
for(int i : arr){
sum += i;
}
System.out.println(sum);
});*/
printSum(MethodDemo1::getSum);
}
请注意其中的双冒号 :: 写法,这被称为“方法引用”,是一种新的语法。
5.2方法引用的格式
- 符号表示 : ::
符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。 应用场景 : 如果Lambda所要实现的方案, 已经有其他方法存在相同方案,那么则可以使用方法引用。
常见引用方式
方法引用在JDK 8中使用方式相当灵活,有以下几种形式:
- instanceName::methodName 对象::方法名
- ClassName::staticMethodName 类名::静态方法
- ClassName::methodName 类名::普通方法
- ClassName::new 类名::new 调用的构造器
- TypeName[]::new String[]::new 调用数组的构造器
5.3 对象名::引用成员方法
这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:
// 对象::实例方法
@Test
public void test01(){
Date now = new Date();
//Lambda表达式写法
Supplier<Long> sup = () -> {
return now.getTime();
};
System.out.println(sup.get());
//使用方法引用
Supplier<Long> sup1 = now::getTime;
System.out.println(sup1.get());
}
方法引用的注意事项:
- 被引用的方法,参数要和接口中抽象方法的参数一样
- 当接口抽象方法有返回值时,被引用的方法也必须有返回值
5.4 类名::引用静态方法
由于在 java.lang.System 类中已经存在了静态方法 currentTimeMillis ,所以当我们需要通过Lambda来调用该方法时,可以使用方法引用 , 写法是:
// 类名::静态方法
@Test
public void test02(){
Supplier<Long> sup = () -> {
return System.currentTimeMillis();
};
System.out.println(sup.get());
Supplier<Long> sup1 = System::currentTimeMillis;
System.out.println(sup1.get());
}
5.5 类名::引用实例方法
Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。
// 类名::实例方法
@Test
public void test03(){
Function<String, Integer> f1 = (str) -> {
return str.length();
};
System.out.println(f1.apply("helloworld"));
Function<String,Integer> f2 = String::length;
System.out.println(f2.apply("helloworld"));
BiFunction<String,Integer,String> f3 = String::substring;
System.out.println(f3.apply("helloworld",3));
}
5.6 类名::new引用构造器
由于构造器的名称与类名完全一样。所以构造器引用使用 类名称::new 的格式表示。首先是一个简单的 Person 类:
public class Person {
private String name;
private int age;
//无参构造
//有参构造
//setget方法
//tostring
}
// 类名::new
@Test
public void test04(){
Supplier<Person> sup = () -> {
return new Person();
};
System.out.println(sup.get());
Supplier<Person> sup2 = Person::new;
System.out.println(sup2.get());
BiFunction<String,Integer,Person> bf = Person::new;
System.out.println(bf.apply("张三",16));
}
5.7 数组::new 引用数组构造器
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。
// 类型[]::new
@Test
public void test05(){
Function<Integer , String []> f1 = len -> new String[len];
String[] str = f1.apply(6);
System.out.println(str+","+str.length);
Function<Integer , String []> f2 = String[]::new;
String[] str1 = f1.apply(8);
System.out.println(str1+","+str1.length);
}
方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为Lambda表达式的缩写形式 , 不过要注意的是方法引用只能"引用"已经存在的方法!
6.Stream流
6.1集合处理数据的弊端
当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。我们来体验集合操作数据的弊端,需求如下:
一个ArrayList集合中存储有以下数据:张无忌,周芷若,赵敏,张强,张三丰
需求:1.拿到所有姓张的 2.拿到名字长度为3个字的 3.打印这些数据
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌","周芷若","赵敏","张强","张三丰");
//1.拿到所有姓张的 2.拿到名字长度为3个字的 3.打印这些数据
for (String str : list){
if(str.startsWith("张")){
if(str.length() == 3){
System.out.println(str);
}
}
}
}
使用上面这种循环遍历的方法会显得有些冗余,虽然结果也能成功获得,但是过程有些麻烦。
那Stream能给我们带来怎样更加优雅的写法呢?
下面来看一下借助Java 8的Stream API,修改后的代码:
list.stream().filter(str -> str.startsWith("张"))
.filter(str -> str.length() == 3)
.forEach(str -> System.out.println(str));
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。我们真正要做的事情内容被更好地体现在代码中。
6.2 Stream流式思想概述
注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象!
Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。
Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。
6.3获取Stream流的两种方式
获取一个流非常简单,有以下几种常用的方式:
- 所有的 Collection 集合都可以通过 stream 默认方法获取流;
- Stream 接口的静态方法 of 可以获取数组对应的流。
方式1 : 根据Collection获取流
首先, java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流。
public interface Collection {
default Stream<E> stream()
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
Stream<String> stream2 = set.stream();
Vector<String> vector = new Vector<>();
Stream<String> stream3 = vector.stream();
}
java.util.Map 接口不是 Collection 的子接口,所以获取对应的流需要分key、value或entry等情况:
//Map获取流
Map<String,String> map = new HashMap<>();
Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
方式2 : Stream中的静态方法of获取流
由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单:
public static void main(String[] args) {
//stream中的静态方法:static stream of(T... values)
Stream<String> stream = Stream.of("aa", "bb", "cc");
String[] str = {"aa","bb","cc"};
Stream<String> stream1 = Stream.of(str);
Integer[] arr1 = {1,2,3,4};
Stream<Integer> integerStream = Stream.of(arr1);
//注意:基础数据类型的数组不行,会将整个数组看成一个元素操作
int[] arr = {1,2,3,4};
Stream<int[]> stream2 = Stream.of(arr);
}
备注: of 方法的参数其实是一个可变参数,所以支持数组。
6.4 Stream常用方法和注意事项
Stream常用方法
Stream流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
- 终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。本小节中,终结方法包括 count 和 forEach 方法。
- 非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结 方法。)
Stream注意事项(重要)
- Stream只能操作一次
- Stream方法返回的是新的流
- Stream不调用终结方法,中间的操作不会执行
6.5 Stream流的forEach方法
forEach 用来遍历流中的数据
void forEach(Consumer<? super T> action);
该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。例如:
@Test
public void testForeach(){
List<String> list = new ArrayList<>();
Collections.addAll(list,"易烊千玺","王俊凯","王源","李易峰","赵丽颖");
list.stream().forEach(s -> System.out.println(s));
//简写
list.stream().forEach(System.out::println);
}
6.6 Stream流的count方法
Stream流提供 count 方法来统计其中的元素个数:
long count();
该方法返回一个long值代表元素个数。基本使用:
@Test
public void testCount(){
List<String> list = new ArrayList<>();
Collections.addAll(list,"易烊千玺","王俊凯","王源","李易峰","赵丽颖");
System.out.println(list.stream().count());
}
6.7 Stream流的filter方法
filter用于过滤数据,返回符合过滤条件的数据
可以通过 filter 方法将一个流转换成另一个子集流。方法声明:
Stream<T> filter(Predicate<? super T> predicate);
该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
Stream流中的 filter 方法基本使用的代码如:
@Test
public void testFilter(){
List<String> list = new ArrayList<>();
Collections.addAll(list,"易烊千玺","王俊凯","王源","李易峰","赵丽颖");
list.stream().filter(s -> s.length() >2).forEach(System.out::println);
}
6.8 Stream流的limit方法
limit 方法可以对流进行截取,只取用前n个。方法签名:
Stream<T> limit(long maxSize);
参数是一个long型,如果集合当前长度大于参数则进行截取,否则不进行操作。基本使用:
@Test
public void testLimit(){
List<String> list = new ArrayList<>();
Collections.addAll(list,"易烊千玺","王俊凯","王源","李易峰","赵丽颖");
list.stream().limit(3).forEach(System.out::println);
}
6.9 Stream流的skip方法
如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:
Stream<T> skip(long n);
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:
@Test
public void testSkip(){
List<String> list = new ArrayList<>();
Collections.addAll(list,"易烊千玺","王俊凯","王源","李易峰","赵丽颖");
list.stream().skip(2).forEach(System.out::println);
}
6.10 Stream流的map方法
如果需要将流中的元素映射到另一个流中,可以使用 map 方法。方法签名:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
Stream流中的 map 方法基本使用的代码如:
@Test
public void testMap(){
Stream<String> stream = Stream.of("1", "22", "33", "6");
//Stream<Integer> result = stream.map(s -> Integer.parseInt(s));
Stream<Integer> result = stream.map(Integer::parseInt);
result.forEach(s -> System.out.println(s+10));
}
这段代码中, map 方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为 Integer 类对象)。
6.11 Stream流的sorted方法
如果需要将数据排序,可以使用 sorted 方法。方法签名:
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
基本使用:
Stream流中的 sorted 方法基本使用的代码如:
@Test
public void testSorted(){
//sorted(): 根据元素的自然顺序排序
//sorted(Comparator<? super T) comparator>:根据比较器指定的规则排序
Stream.of(11,44,2,3,45).sorted().sorted((a1,a2) -> a2-a1).forEach(System.out::println);
}
这段代码中, sorted 方法根据元素的自然顺序排序,也可以指定比较器排序。
6.12 Stream流的distinct方法
如果需要去除重复数据,可以使用 distinct 方法。方法签名:
Stream<T> distinct();
基本使用
Stream流中的 distinct 方法基本使用的代码如:
@Test
public void testDistinct(){
Stream.of(11,2,33,11,33).distinct().forEach(System.out::println);
}
如果是自定义类型如何是否也能去除重复的数据呢?
@Test
public void testDistinct2(){
Stream.of(new Person("张三",11),
new Person("李四",12),
new Person("王五",18),
new Person("李四",12),
new Person("赵六",19),
new Person("张三",11))
.distinct().forEach(System.out::println);
//实体类中重写hashCode和equals方法才能去重
}
自定义类型是根据对象的hashCode和equals来去除重复元素的。
6.13 Stream流的match方法
如果需要判断数据是否匹配指定的条件,可以使用 Match 相关方法。方法签名:
boolean allMatch(Predicate<? super T> predicate);
boolean anyMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
基本使用
Stream流中的 Match 相关方法基本使用的代码如:
@Test
public void testMatch(){
boolean b = Stream.of(1, 2, 33, 45, 6, 7)
//.allMatch(a -> a > 0); // allMatch: 元素是否全部满足条件
//.anyMatch(a -> a > 3); // anyMatch: 元素是否任意有一个满足条件
.noneMatch(a -> a < 0); // noneMatch: 元素是否全部不满足条件
System.out.println("b=" +b);
}
6.14 Stream流的find方法
如果需要找到某些数据,可以使用 find 相关方法。方法签名:
Optional<T> findFirst();
Optional<T> findAny();
基本使用
Stream流中的 find 相关方法基本使用的代码如:
@Test
public void testFind(){
Optional<Integer> first = Stream.of(1, 2, 5, 76, 3, 11).findFirst();
System.out.println(first.get());
Optional<Integer> any = Stream.of(1, 2, 5, 76, 3, 11).findAny();
System.out.println(any.get());
}
6.15 Stream流的max和min方法
如果需要获取最大和最小值,可以使用 max 和 min 方法。方法签名:
Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(Comparator<? super T> comparator);
基本使用
Stream流中的 max 和 min 相关方法基本使用的代码如:
@Test
public void testMax_Min(){
Optional<Integer> max = Stream.of(1, 3, 4, 2, 7).max((o1, o2) -> o1 - o2);
System.out.println("max="+max.get());
Optional<Integer> min = Stream.of(1, 3, 4, 2, 7).min((o1, o2) -> o1 - o2);
System.out.println("min="+min.get());
}
6.16 Stream流的reduce方法
如果需要将所有数据归纳得到一个数据,可以使用 reduce 方法。方法签名:
T reduce(T identity, BinaryOperator<T> accumulator);
基本使用
Stream流中的 reduce 相关方法基本使用的代码如:
@Test
public void testReduce(){
Integer reduce = Stream.of(1, 3, 4, 2, 7).reduce(0, (a, b) -> {
System.out.println("a=" + a + ",b=" + b);
return a + b;
});
// reduce:
// 第一次将默认做赋值给x, 取出第一个元素赋值给y,进行操作
// 第二次,将第一次的结果赋值给x, 取出二个元素赋值给y,进行操作
// 第三次,将第二次的结果赋值给x, 取出三个元素赋值给y,进行操作
// 第四次,将第三次的结果赋值给x, 取出四个元素赋值给y,进行操作
System.out.println("reduce = " + reduce);
Integer reduce1 = Stream.of(1, 3, 4, 2, 7).reduce(0, (x, y) -> Integer.sum(x, y));
//简化
Integer reduce2 = Stream.of(1, 3, 4, 2, 7).reduce(0,Integer::sum);
System.out.println(reduce2);
//Integer reduce3 = Stream.of(1, 3, 4, 2, 7).reduce(0, (x, y) -> x > y ? x : y);
Integer reduce3 = Stream.of(1, 3, 4, 2, 7).reduce(0,Integer::max);
System.out.println(reduce3);
}
6.17 Stream流的map和reduce组合使用
@Test
public void testMapReduce(){
//求出所有年纪的总和
Integer reduce = Stream.of(new Person("张三", 11),
new Person("李四", 12),
new Person("王五", 18),
new Person("赵六", 19))
.map(p -> p.getAge())
.reduce(0, (x, y) -> x + y);
System.out.println(reduce);
//统计数字2出现的次数
Integer count = Stream.of(1, 2, 3, 4, 2, 1, 2, 4, 2).map(o -> {
if (o == 2) {
return 1;
} else {
return 0;
}
}).reduce(0, Integer::sum);
System.out.println("count="+count);
}
6.18 Stream流的mapToInt
如果需要将Stream中的Integer类型数据转成int类型,可以使用 mapToInt 方法。方法签名:
IntStream mapToInt(ToIntFunction<? super T> mapper);
基本使用
Stream流中的 mapToInt 相关方法基本使用的代码如:
@Test
public void testMapToInt(){
// Integer占用的内存比int多,在Stream流操作中会自动装箱和拆箱
Stream<Integer> stream = Arrays.stream(new Integer[]{1, 2, 3, 4,5,6});
//把大于3的和打印出来
/*Integer sum = stream.filter(i -> i.intValue() > 3).reduce(0, Integer::sum);
System.out.println("sum="+sum);*/
//先将流中的Integer数据转成int,后续都是操作int类型
IntStream intStream = stream.mapToInt(Integer::intValue);
int reduce = intStream.filter(i -> i > 3).reduce(0, Integer::sum);
System.out.println(reduce);
//将intStream转换为Stream<Integer>
IntStream intStream1 = IntStream.rangeClosed(0, 10);
Stream<Integer> boxed = intStream1.boxed();
boxed.forEach(s -> System.out.println(s.getClass()+","+s));
}
6.19 Stream流的concat方法
如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
备注:这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的。
该方法的基本使用代码如:
@Test
public void testConcat(){
Stream<String> stream1 = Stream.of("张三");
Stream<String> stream2 = Stream.of("李四");
Stream<String> concat = Stream.concat(stream1, stream2);
concat.forEach(System.out::println);
}
6.20 Stream综合案例
现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下若干操作步骤:
- 第一个队伍只要名字为3个字的成员姓名;
- 第一个队伍筛选之后只要前3个人;
- 第二个队伍只要姓张的成员姓名;
- 第二个队伍筛选之后不要前2个人;
- 将两个队伍合并为一个队伍;
- 根据姓名创建 Person 对象;
- 打印整个队伍的Person对象信息。
两个队伍(集合)的代码如下:
List<String> one = new ArrayList<>();
Collections.addAll(one,"迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七公");
List<String> two = new ArrayList<>();
Collections.addAll(two,"古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱", "张三");
而 Person 类的代码为:
public class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
Stream方式
public class ArrayListDemo {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
Collections.addAll(one,"迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七公");
List<String> two = new ArrayList<>();
Collections.addAll(two,"古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱", "张三");
//1. 第一个队伍只要名字为3个字的成员姓名;
//2. 第一个队伍筛选之后只要前3个人;
Stream<String> stream1 = one.stream().filter(o -> o.length() == 3).limit(3);
//3. 第二个队伍只要姓张的成员姓名;
//4. 第二个队伍筛选之后不要前2个人;
Stream<String> stream2 = two.stream().filter(o -> o.startsWith("张")).skip(2);
//5. 将两个队伍合并为一个队伍;
Stream<String> concat = Stream.concat(stream1, stream2);
//6. 根据姓名创建 Person 对象;
//7. 打印整个队伍的Person对象信息。
concat.map(Person::new).forEach(System.out::println);
}
}
6.21 串行的Stream流
目前我们使用的Stream流是串行的,就是在一个线程上执行。
@Test
public void testSerial(){
Stream.of(1,2,3,4,6,4,32,1)
.filter(s -> {
System.out.println(Thread.currentThread()+", s="+s);;
return true;
}).count();
}
执行结果:
Thread[main,5,main], s=1
Thread[main,5,main], s=2
Thread[main,5,main], s=3
Thread[main,5,main], s=4
Thread[main,5,main], s=6
Thread[main,5,main], s=4
Thread[main,5,main], s=32
Thread[main,5,main], s=1
并行的Stream流
parallelStream其实就是一个并行执行的流。它通过默认的ForkJoinPool,可能提高多线程任务的速度。
获取并行Stream流的两种方式
- 直接获取并行的流
- 将串行流转成并行流
@Test
public void testgetParallelStream(){
List<Integer> list = new ArrayList<>();
// 直接获取并行的流
Stream<Integer> stream1 = list.parallelStream();
// 将串行流转成并行流
Stream<Integer> stream2 = list.stream().parallel();
}
并行操作代码:
@Test
public void testParallel(){
long count = Stream.of(1, 2, 3, 4, 5, 9, 7)
.parallel() //将流转成并发流,stream处理的时候才去
.filter(s -> {
System.out.println(Thread.currentThread() + ",s=" + s);
return true;
}).count();
System.out.println("count="+count);
}
执行结果:
@Test
public void testParallel(){
long count = Stream.of(1, 2, 3, 4, 5, 9, 7)
.parallel() //将流转成并发流,stream处理的时候才去
.filter(s -> {
System.out.println(Thread.currentThread() + ",s=" + s);
return true;
}).count();
System.out.println("count="+count);
}
- 获取并行流有两种方式:
直接获取并行流: parallelStream()
将串行流转成并行流: parallel()
并行和串行Stream流的效率对比
使用for循环,串行Stream流,并行Stream流来对5亿个数字求和。看消耗的时间。
public class Demo6 {
private static long times = 500000000L;
private long start;
@Before
public void init(){
start = System.currentTimeMillis();
}
@After
public void destory(){
long end = System.currentTimeMillis();
System.out.println("消耗时间:"+ (end-start));
}
// 测试效率,parallelStream
@Test
public void parallelStream(){
System.out.println("parallelStream");
LongStream.rangeClosed(0,times).parallel().reduce(0,Long::sum);
}
// 测试效率,普通Stream
@Test
public void serialStream(){
System.out.println("serialStream");
LongStream.rangeClosed(0,times).reduce(0,Long::sum);
}
//测试效率,正常for循环
@Test
public void forAdd(){
System.out.println("forAdd");
long result = 0l;
for (long i = 1L;i<times;i++){
result += i;
}
}
}
我们可以看到parallelStream的效率是最高的。
Stream并行处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作。
6.22 parallelStream线程安全问题
@Test
public void parallelStreamNotice(){
List<Integer> list = new ArrayList<>();
for (int i = 0;i<1000;i++){
list.add(i);
}
List<Integer> newlist = new ArrayList<>();
//list.parallelStream().forEach(s -> newlist.add(s));
list.parallelStream().forEach(newlist::add);
System.out.println("newList = " + newList.size());
}
运行效果:
我们明明是往集合中添加1000个元素,而实际上只有903个元素。
解决方法: 加锁、使用线程安全的集合或者调用Stream的 toArray() / collect() 操作就是满足线程安全的了。
parallelStream背后的技术
- Fork/Join框架介绍
parallelStream使用的是Fork/Join框架。Fork/Join框架自JDK 7引入。Fork/Join框架可以将一个大任务拆分为很多小任务来异步执行。
Fork/Join框架主要包含三个模块:
- 线程池:ForkJoinPool
- 任务对象:ForkJoinTask
- 执行任务的线程:ForkJoinWorkerThread
Fork/Join原理-分治法
ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。
Fork/Join原理-工作窃取算法
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的线程数量,可以尝试调整成不同的参数来观察每次的输出结果。
Fork/Join案例
需求:使用Fork/Join计算1-10000的和,当一个任务的计算数量大于3000时拆分任务,数量小于3000时计算。
public class ForkJoinDemo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
SumRecursiveTask task = new SumRecursiveTask(1, 10000L);
Long result = pool.invoke(task);
System.out.println("result = " + result);
long end = System.currentTimeMillis();
System.out.println("消耗的时间为: " + (end - start));
}
}
class SumRecursiveTask extends RecursiveTask<Long> {
private static final long THRESHOLD = 3000L;
private final long start;
private final long end;
public 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();
}
}
}
- parallelStream是线程不安全的
- parallelStream适用的场景是CPU密集型的,只是做到别浪费CPU,假如本身电脑CPU的负载很大,那还到处用并行流,那并不能起到作用
- I/O密集型 磁盘I/O、网络I/O都属于I/O操作,这部分操作是较少消耗CPU资源,一般并行流中不适用于I/O密集型的操作,就比如使用并流行进行大批量的消息推送,涉及到了大量I/O,使用并行流反而慢了很多
- 在使用并行流的时候是无法保证元素的顺序的,也就是即使你用了同步集合也只能保证元素都正确但无法保证其中的顺序
7.学习JDK 8新增的Optional类
以前对null的处理方式
@Test
public void test1(){
String username = "张三";
//String username = null;
if(username != null){
System.out.println("用户名为:"+username);
}else {
System.out.println("用户名不存在");
}
}
7.1 Optional类介绍
Optional是一个没有子类的工具类,Optional是一个可以为null的容器对象。它的作用主要就是为了解决避免Null检查,防止NullPointerException。
7.2 Optional的基本使用
- Optional类的创建方式:
Optional.of(T t) : 创建一个 Optional 实例
Optional.empty() : 创建一个空的 Optional 实例
Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
- Optional类的常用方法:
isPresent() : 判断是否包含值,包含值返回true,不包含值返回false
get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException
orElse(T t) : 如果调用对象包含值,返回该值,否则返回参数t
orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()
@Test
public void test02(){
//Optional<String> username = Optional.of("张三");
//Optional<String> username = Optional.of(null);
//Optional<String> username = Optional.ofNullable(null);
Optional<String> username = Optional.empty();
if(username.isPresent()){
System.out.println(username.get());
}else {
System.out.println("用户名不存在");
}
}
Optional的高级使用
@Test
public void test03(){
Optional<String> username = Optional.of("张三");
//Optional<String> username = Optional.empty();
username.ifPresent(s -> System.out.println("用户名为:"+username));
//username.ifPresentOrElse(s -> System.out.println("用户名为" + s) , () -> System.out.println("用户名不存在"));
}
@Test
public void test04(){
//Optional<String> username = Optional.of("张三");
Optional<String> username = Optional.empty();
// 如果调用对象包含值,返回该值,否则返回参数t
System.out.println("用户名为:"+username.orElse("null"));
// 如果调用对象包含值,返回该值,否则返回参数Supplier得到的值
String s = username.orElseGet(() -> "未知用户名");
System.out.println(s);
}
@Test
public void test05(){
//Person person = new Person("张三",18);
//Person person = new Person(null, 18);
//Person person = null;
//System.out.println(getUpperCaseUsername1(person));
// 我们将可能会为null的变量构造成Optional类型
//Person person = new Person("zhangsan",18);
Person person = new Person(null,18);
Optional<Person> op = Optional.of(person);
System.out.println(getUpperCaseUsername2(op));
}
public String getUpperCaseUsername2(Optional<Person> o){
//return o.map(o1 -> o1.getName())
// .map(o1 -> o1.toUpperCase())
// .orElse("null");
return o.map(Person::getName)
.map(String::toUpperCase)
.orElse("null");
}
public String getUpperCaseUsername1(Person p){
if(p != null){
String name = p.getName();
if (name != null){
return name;
}else {
return null;
}
}else {
return null;
}
}
8. JDK 8新的日期和时间 API
8.1 新旧日期时间API对比
旧版日期时间 API 存在的问题
- 设计很差: 在java.util和java.sql的包中都有日期类,java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期。此外用于格式化和解析的类在java.text包中定义。
- 非线程安全:java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
- 时区处理麻烦:日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
新日期时间 API介绍
- JDK 8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于 java.time包中,下面是一些关键类。
LocalDate :表示日期,包含年月日,格式为 2019-10-16
LocalTime :表示时间,包含时分秒,格式为 16:38:54.158549300
LocalDateTime :表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750
DateTimeFormatter :日期时间格式化类。
Instant:时间戳,表示一个特定的时间瞬间。
Duration:用于计算2个时间(LocalTime,时分秒)的距离
Period:用于计算2个日期(LocalDate,年月日)的距离
ZonedDateTime :包含时区的时间
Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公历。平年有365天,闰年是366天。此外Java 8还提供了4套其他历法,分别是:
- ThaiBuddhistDate:泰国佛教历
- MinguoDate:中华民国历
- JapaneseDate:日本历
- HijrahDate:伊斯兰历
8.2JDK 8的日期和时间类
- LocalDate、LocalTime、LocalDateTime类的实例是不可变的对象,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
LocalDateTime类实例:
//LocalDateTime类: 获取日期时间信息。格式为 2021-04-20T15:33:56.750
@Test
public void test03(){
//得到指定日期时间
LocalDateTime localDateTime = LocalDateTime.of(1999, 1, 1, 9, 10, 20);
System.out.println("localDateTime="+localDateTime);//localDateTime=1999-01-01T09:10:20
//获取当前日期时间
LocalDateTime now = LocalDateTime.now();
System.out.println("now="+now);//now=2021-04-20T22:26:59.403
//获取时间信息
System.out.println("年:"+now.getYear());//年:2021
System.out.println("月:"+now.getMonthValue());//月:4
System.out.println("日:"+now.getDayOfMonth());//日:20
System.out.println("时:"+now.getHour());//时:22
System.out.println("分:"+now.getMinute());//分:26
System.out.println("秒:"+now.getSecond());//秒:59
System.out.println("纳秒:"+now.getNano());//纳秒:403000000
}
LocalTime类实例:
//LocalTime类: 获取时间信息。格式为 16:38:54.158549300
@Test
public void test02(){
//得到指定时间
LocalTime time = LocalTime.of(22, 22, 10,129_900_000);
System.out.println("time="+time);//time=22:22:10.129900
//得到当前时间
LocalTime now = LocalTime.now();
System.out.println("now="+now);//now=22:20:31.159
//获取时间信息
System.out.println("小时:"+now.getHour());//小时:22
System.out.println("分钟:"+now.getMinute());//分钟:20
System.out.println("秒:"+now.getSecond());//秒:31
System.out.println("纳秒:"+now.getNano());//纳秒:159000000
}
LocalDateTime类实例:
//LocalDateTime类: 获取日期时间信息。格式为 2021-04-20T15:33:56.750
@Test
public void test03(){
//得到指定日期时间
LocalDateTime localDateTime = LocalDateTime.of(1999, 1, 1, 9, 10, 20);
System.out.println("localDateTime="+localDateTime);//localDateTime=1999-01-01T09:10:20
//获取当前日期时间
LocalDateTime now = LocalDateTime.now();
System.out.println("now="+now);//now=2021-04-20T22:26:59.403
//获取时间信息
System.out.println("年:"+now.getYear());//年:2021
System.out.println("月:"+now.getMonthValue());//月:4
System.out.println("日:"+now.getDayOfMonth());//日:20
System.out.println("时:"+now.getHour());//时:22
System.out.println("分:"+now.getMinute());//分:26
System.out.println("秒:"+now.getSecond());//秒:59
System.out.println("纳秒:"+now.getNano());//纳秒:403000000
}
- 对日期时间的修改,对已存在的LocalDate对象,创建它的修改版,最简单的方式是使用withAttribute方法。
withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。以下所有的方法都返回了一个修改属性的对象,他们不会影响原来的对象。
// LocalDateTime类: 对日期时间的修改
@Test
public void test05(){
LocalDateTime now = LocalDateTime.now();
System.out.println("now="+now);//now=2021-04-20T22:31:04.222
//修改日期时间
LocalDateTime setYear = now.withYear(2078);
System.out.println("修改年份:"+setYear);//修改年份:2078-04-20T22:31:04.222
System.out.println("(now == setYear)?"+(now == setYear));//(now == setYear)?false
System.out.println("修改月份:"+now.withMonth(5));
System.out.println("修改小时:"+now.withHour(8));
System.out.println("修改分钟:"+now.withMinute(20));
// 在当前对象的基础上加上或减去指定的时间
LocalDateTime localDateTime = now.plusDays(3);
System.out.println("3天后:"+localDateTime);//3天后:2021-04-23T22:37:07.316
System.out.println("(now == localDateTime)?"+(now == localDateTime));//(now == localDateTime)?false
System.out.println("5年后:"+now.plusYears(5));
System.out.println("10月后:"+now.plusMonths(10));
System.out.println("10年前:"+now.minusYears(10));
System.out.println("5月前:"+now.minusMonths(5));
System.out.println("8天前:"+now.minusDays(8));
}
日期时间的比较:
// 日期时间的比较
@Test
public void test06(){
// 在JDK8中,LocalDate类中使用isBefore()、isAfter()、equals()方法来比较两个日期,可直接进行比较。
LocalDate now = LocalDate.now();
LocalDate date = LocalDate.of(2019, 7, 19);
System.out.println(now.isBefore(date));//false
System.out.println(now.isAfter(date));//true
}
8.3 JDK 8的时间格式化与解析
通过 java.time.format.DateTimeFormatter 类可以进行日期时间解析与格式化。
//日期格式化
@Test
public void test07(){
//得到当前日期时间
LocalDateTime now = LocalDateTime.now();
//日期格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
//将日期格式化为字符串
String format = now.format(formatter);
System.out.println("format="+format);//format=2021-04-20 22:49:58
//将字符串解析为日期时间
LocalDateTime parse = LocalDateTime.parse("2020-01-30 10:20:13", formatter);
System.out.println("parse="+parse);//parse=2020-01-30T10:20:13
}
8.4 JDK 8的 Instant 类
Instant 时间戳/时间线,内部保存了从1970年1月1日 00:00:00以来的秒和纳秒。
// 时间戳
@Test
public void test08(){
Instant now = Instant.now();
System.out.println("当前时间戳:"+now);
// 获取从1970年1月1日 00:00:00的秒
System.out.println(now.getNano());
System.out.println(now.getEpochSecond());
System.out.println(now.toEpochMilli());
System.out.println(System.currentTimeMillis());
Instant instant = Instant.ofEpochSecond(5);
System.out.println(instant);
}
8.5 JDK 8的计算日期时间差类
Duration/Period类: 计算日期时间差。
- Duration:用于计算2个时间(LocalTime,时分秒)的距离
- Period:用于计算2个日期(LocalDate,年月日)的距离
// Duration/Period类: 计算日期时间差
@Test
public void test09(){
// Duration计算时间的距离
LocalTime now = LocalTime.now();
LocalTime time = LocalTime.of(0, 0, 0);
Duration duration = Duration.between(now,time);
System.out.println("相差的天数:"+duration.toDays());
System.out.println("相差的小时数::"+duration.toHours());
System.out.println("相差的分钟数:"+duration.toMinutes());
// Period计算日期的距离
LocalDate nowDate = LocalDate.now();
LocalDate localDate = LocalDate.of(2021, 4, 21);
// 让后面的时间减去前面的时间
Period period = Period.between(localDate, nowDate);
System.out.println("相差的年:"+period.getYears());
System.out.println("相差的月:"+period.getMonths());
System.out.println("相差的天:"+period.getDays());
}
8.6 JDK 8的时间校正器
有时我们可能需要获取例如:将日期调整到“下一个月的第一天”等操作。可以通过时间校正器来进行。
- TemporalAdjuster : 时间校正器。
- TemporalAdjusters : 该类通过静态方法提供了大量的常用TemporalAdjuster的实现。
// TemporalAdjuster类:自定义调整时间
@Test
public void test01(){
LocalDateTime now = LocalDateTime.now();
//得到下一个月的第一天
TemporalAdjuster firsWeekDayOfNextMonth = temporal -> {
LocalDateTime dateTime = (LocalDateTime)temporal;
LocalDateTime nextMonth = dateTime.plusMonths(1).withDayOfMonth(1);
System.out.println("nextMonth="+nextMonth);
return nextMonth;
};
LocalDateTime nextMonth = now.with(firsWeekDayOfNextMonth);
System.out.println("nextMonth="+nextMonth);
}
8.7 JDK 8设置日期时间的时区
Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别
为:ZonedDate、ZonedTime、ZonedDateTime。
其中每个时区都对应着 ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等。
ZoneId:该类中包含了所有的时区信息。
// 设置日期时间的时区
@Test
public void test02(){
//获取所有的时区id
ZoneId.getAvailableZoneIds().forEach(System.out::println);
//不带时间,获取计算机的当前时间
LocalDateTime now = LocalDateTime.now();// 中国使用的东八区的时区.比标准时间早8个小时
System.out.println("now="+now);
//2.操作带时区的类
//now(Clock.systemUTC()):创建世界标准时间
ZonedDateTime bz = ZonedDateTime.now(Clock.systemUTC());
System.out.println("bz="+bz);
// now(): 使用计算机的默认的时区,创建日期时间
ZonedDateTime now1 = ZonedDateTime.now();
System.out.println("now1="+now1);
//使用指定的时区创建日期时间
ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Vancouver"));
System.out.println("now2="+now2);
}
JDK 8新的日期和时间 API的优势:
- 新版的日期和时间API中,日期和时间对象是不可变的。操纵的日期不会影响老值,而是新生成一个实例。
- 新的API提供了两种不同的时间表示方式,有效地区分了人和机器的不同需求。
- TemporalAdjuster可以更精确的操纵日期,还可以自定义日期调整器。
- 是线程安全的
9 JDK 8重复注解与类型注解
9.1重复注解的使用
自从Java 5中引入 注解 以来,注解开始变得非常流行,并在各个框架和项目中被广泛使用。不过注解有一个很大的限制是:在同一个地方不能多次使用同一个注解。JDK 8引入了重复注解的概念,允许在同一个地方多次使用同一个注解。在JDK 8中使用@Repeatable注解定义重复注解。
重复注解的使用步骤:
- 定义重复的注解容器注解
@Retention(RetentionPolicy.RUNTIME)
@interface MyTests {
MyTest[] value();
}
- 定义一个可以重复的注解
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(MyTests.class)
@interface MyTest {
String values();
}
- 配置多个重复的注解
@Test
@MyTest(values = "abc")
@MyTest(values = "abd")
public void test() throws NoSuchMethodException{
}
- 解析得到指定注解
@MyTest(values = "tba")
@MyTest(values = "tbb")
@MyTest(values = "tbc")
public class Demo1 {
@Test
@MyTest(values = "abc")
@MyTest(values = "abd")
public void test() throws NoSuchMethodException{
//4.解析得到类上指定注解
MyTest[] tests = Demo1.class.getAnnotationsByType(MyTest.class);
for (MyTest test:tests){
System.out.println(test.values());
}
//得到方法上的注解
MyTest[] tests1 = Demo1.class.getMethod("test").getAnnotationsByType(MyTest.class);
for (MyTest test:tests1){
System.out.println(test.values());
}
}
}
9.2 类型注解的使用
- JDK 8为@Target元注解新增了两种类型: TYPE_PARAMETER , TYPE_USE 。
- TYPE_PARAMETER:表示该注解能写在类型参数的声明语句中。 类型参数声明如: < T>
- TYPE_USE :表示注解可以再任何用到类型的地方使用。
TYPE_PARAMETER的使用:
@Target(ElementType.TYPE_PARAMETER)//表示该注解能写在类型参数的声明语句中
@interface TyptParam {
}
public class Demo2<@TyptParam T> {
public static void main(String[] args) {
}
public <@TyptParam E> void test(String a){
}
}
TYPE_USE的使用:
@Target(ElementType.TYPE_USE)
@interface NotNull {
}
public class Demo3<@TyptParam T extends String> {
private @NotNull int a = 10;
public static void main(@NotNull String[] args) {
@NotNull int x = 1;
@NotNull String s = new @NotNull String();
}
public <@TyptParam E> void test(String a){
}
}
通过@Repeatable元注解可以定义可重复注解, TYPE_PARAMETER 可以让注解放在泛型上, TYPE_USE 可以让注解放在类型的前面