Java8相关特性

文章目录

  • 前言
  • Lambda 表达式
  • 类型推断
  • 核心函数式接口
    • Supplier 接口
    • Consumer 接口
    • Predicate 接口
    • Function 接口
  • 方法引用与构造器引用
  • 接口的默认、静态方法
  • Optional
  • Stream API
    • 综合示例
    • Sort 排序
    • Distinct 去重
    • flatMap
    • boxed 装箱
      • mapToInt 类
    • collect 收集流
    • Match 匹配
    • Find 查找
    • max 和 min
    • Reduce 规约
    • 并行与串行
    • 总结补充
  • Date API
    • Clock 时钟
    • Timezones 时区
    • LocalTime 本地时间
    • LocalDate 本地日期
    • LocalDateTime 本地日期时间

前言

Java 8 是 Java 的一个重大版本,有人认为,虽然这些新特性令 Java 开发人员十分期待,但同时也需要花不少精力去学习。在这一小节中,我们将介绍 Java 8 的大部分新特性。

Lambda 表达式

java8 引入了一个新的操作符->,它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理。以前的话只能使用匿名内部类。(省去定义,重写方法,直接表达)

Lambda表达式的标准格式为:

(参数类型 参数名称) -> { 代码语句 }
  • 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
  • ->是新引入的语法格式,代表指向动作。
  • 大括号内的语法与传统方法体要求基本一致。

使用前提

  1. 使用 Lambda 必须具有接口,且要求接口中有且仅有一个抽象方法
    无论是 JDK 内置的RunnableComparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
  2. 使用 Lambda 必须具有上下文推断。也就是方法的参数或局部变量类型必须为 Lambda 对应的接口类型,才能使用 Lambda 作为该接口的实例。

省略格式

在 Lambda 标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略;
  2. 如果小括号内有且仅有一个参,则小括号可以省略;
  3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。

使用示例

  //使用匿名内部类的方式,实现多线程
        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 );
    } }

变量问题

  • 你可以直接访问标记了final的外层局部变量,或者实例的字段以及静态变量。
  • 在 lambda 表达式中试图修改局部变量同样是不允许的。

类型推断

由于 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.Runnablejava.util.concurrent.Callable 是函数式接口的最佳例子。

在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解**@FunctionalInterface**,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。举个简单的函数式接口的定义:

@FunctionalInterface
public interface FunctionalDefaultMethods {
    void method();

    default void defaultMethod() {            
    }        
}
  • 默认方法和静态方法不会破坏函数式接口的定义

常用函数式接口

JDK 提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在 java.util.function 包中被提供

Supplier 接口

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);
	}
}

Consumer 接口

java.util.function.Consumer 接口则正好与 Supplier 接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。

方法

  • 包含抽象方法 void accept(T t),意为消费一个指定泛型的数据。
  • default 方法 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); // 姓名:迪丽热巴。性别:女。
    }
   }
}

Predicate 接口

有时候我们需要对某种类型的数据进行判断,从而得到一个 boolean 值结果。这时可以使用
java.util.function.Predicate接口。

方法

  • 包含一个抽象方法: boolean test(T t)用于条件判断
  • 默认方法:and , 实现“并且”的效果时,
  • 默认方法:or, 实现逻辑关系中的“或”
  • 默认方法:negate,实现逻辑关系中的“非
//集合信息筛选,姓名为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;
    }
}

Function 接口

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的替代者。

  • Lambda表达式写法: s -> System.out.println(s);
  • 方法引用写法: System.out::println

推导与省略

如果使用 Lambda,那么根据可推导就是可省略的原则,无需指定参数类型,也无需指定的重载形式——它们都
将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。

函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。

使用场合

  • 通过 对象名 引用 成员方法
  • 通过 类名称 引用 静态方法
  • 通过 superthis引用 成员方法
  • 类的 构造器引用
  • 数组的 构造器引用

使用条件

  • 方法引用所引用的方法的参数列表必须要和函数式接口中抽象方法的参数列表相同(完全一致)
  • 方法引用所引用的方法的的返回值必须要和函数式接口中抽象方法的返回值相同(完全一致)
  • 构造器参数列表要与接口中抽象方法的参数列表一致

方法引用示例,因使用上类似,便拿一个举例即可:

@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

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"

Stream API

在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。它可以指定您希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API对集合数据进行操作,就类似于使用 SQL 执行的数据库查询

写在前面

  • Collection需要手动for-each或者使用Iterator外部迭代;Stream 是内部迭代(好处:并行操作)
  • 集合中的每个元素都得先计算出来,添加到内存里;流的元素则是按需计算
  • Stream 只能操作一次
  • Stream 方法返回的是新的流
  • Stream 不调用终结方法,中间的操作不会执行

流的获取

  • 根据数组获取流,静态方法of,如Stream.of(array)
  • 根据 Collection获取流,如list.stream()
  • 根据 Map 获取流,其 K-V 数据结构不符合流元素的单一特征,分情况
    • 获取 Key:map.keySet().stream();
    • 获取 Value:map.values().stream();
    • 获取 Entry:map.entrySet().stream();

常用方法

运用Stream操作分三步:创建Stream流流中间操作终止流操作

Java8相关特性_第1张图片

流模型的操作很丰富,这里介绍一些常用的 API。这些方法可以被分成两种:

  • 延迟方法:(中间操作)它的返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方
    法均为延迟方法。)
  • 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调
    。终结方法包括 countforEach 方法。
//终结
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);

综合示例

  1. 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
  2. 第一个队伍筛选之后只要前3个人;存储到一个新集合中。
  3. 第二个队伍只要姓张的成员姓名;存储到一个新集合中。
  4. 第二个队伍筛选之后不要前2个人;存储到一个新集合中。
  5. 将两个队伍合并为一个队伍;存储到一个新集合中。
  6. 根据姓名创建 Person 对象;存储到一个新集合中。
  7. 打印整个队伍的 Person对象信息。

简要代码:

//第一支队伍
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 的注意事项:

  1. Stream 只能操作一次
  2. Stream 方法返回的是新的流
  3. Stream 不调用终结方法,中间的操作不会执行

接下来,拓展一些常用方法

Sort 排序

排序是一个中间操作,返回的是排序好后的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);

Distinct 去重

中间操作

Stream<T> distinct();
 Stream<String> stringStream = Stream.of( "11","22", "33","11");
        stringStream.map(Integer::parseInt).distinct().forEach(System.out::println);

flatMap

和 map 类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合

flatMap包含两个操作:会将每一个输入对象输入映射为一个新集合,然后把这些新集合连成一个大集合

举例

{苹果,梨子}.flatMap(切碎) = {苹果碎片1,苹果碎片2,梨子碎片1,梨子碎片2}

其中: “切碎”函数的类型为:A => List

再比如 List{"1,2,3",",4,5,6","7,8"}变成List{1,2,3,4,5,6,7,8}就需要先切碎,再连接。

概括来讲:把几个小的 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());

boxed 装箱

在Java中,将原始类型转换为对应的引用类型的机制,这个机制叫做装箱。将引用类型转换为对应的原始类型,叫做拆箱。在 java 中装箱和拆箱是自动完成的。

但是这么做(int被装箱成Integer)在性能方面是要付出代价的,装箱的本质就是将原始类型包裹起来,并保存在堆里。因此装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。

java8 引入了三个原始类型特化流来解决这个问题;IntStream、DoubleStream、LongStream 分别将流中元素特化为 int、double、long,从而避免了暗含装箱的成本。每个接口都带来了常用数值归约的新方法,例如sum、max、min

mapToInt 类

我们需要返回的是 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 的作用应该不言而喻了,装箱(将基本类型的元素装箱为包装类型)

collect 收集流

//第一种方法,常用些
<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);

第二种方法说明(没用过,做了解)

  • supplier:动态的提供初始化的值;创建一个可变的结果容器(JAVADOC);对于并行计算,这个方法可能被调用多次,每次返回一个新的对象;
  • accumulator:类型为BiConsumer,注意这个接口是没有返回值的;它必须将一个元素放入结果容器中,代表着如何将元素添加到容器中;
  • combiner:类型也是BiConsumer,因此也没有返回值。它将多个supplier 生成的实例整合到一起,代表着规约操作,将多个结果合并。

更多关于收集器介绍,建议阅读

第一种示例:

看结构就行了

//大致结构
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());

Match 匹配

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

Find 查找

这是一个最终操作

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

这是一个最终操作,如果需要获取最大和最小值,可以使用 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()); 

Reduce 规约

这是一个最终操作,允许通过指定的函数来讲 stream中的多个元素规约为一个元素,规约后的结果是通过Optional 接口表示的.

Optional<T> reduce(BinaryOperator<T> accumulator)//一个参数的Reduce

T reduce(T identity, BinaryOperator<T> accumulator)//两个参数的Reduce,多了一个初始化的值。

  • BinaryOperator,它表示的就是两个相同类型的输入经过计算后产生一个同类型的输出。参考

指定初始值,示例:

// 以下结果将会是: [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 差不多–都是接受一个流,再生成一个流(中间操作), 但有一个关键的区别。从流中排序和删除重复项都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们他不存在)。我们把这些操作叫做有状态操作。

Date API

Java 8 在包 java.time下包含了一组全新的时间日期API。新的日期 API 和开源的 Joda-Time 库差不多,但又不完全一样,下面的例子展示了这组新 API 里最重要的一些部分:以下内容出自

Clock 时钟

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

Timezones 时区

在新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 本地时间

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 本地日期

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 同时表示了时间和日期,相当于前两节内容合并到一个对象上了。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是不可变的,所以它是线程安全的。

你可能感兴趣的:(学习笔记)