精编Java8新特性

一、Lambda表达式

在JDK8之前,一个方法能接受的参数都是变量,例如: object.method(Object o) 那么,如果需要传入一个动作呢?比如回调。那么你可能会想到匿名内部类。
例如:
匿名内部类是需要依赖接口的,所以需要先定义个接口

@FunctionalInterface 
public interface PersonCallback {     
void callback(Person person);
}

Person类:

public class Person {     
       private int id;     
       private String name;  
   
       public Person(int id, String name) {         
             this.id = id;         
             this.name = name;
       }

     // 创建一个Person之后,进行回调
     public static void create(Integer id, String name, PersonCallback personCallback) {
         Person person = new Person(id, name);         
         personCallback.callback(person);
    }
}

public static void main(String[] args) {
        Person.create(1, "zero", new PersonCallback() {             
             public void callback(Person person) {
                System.out.println("竹子爱熊猫….");
            }
        });
        Person.create(2, "xxx", new PersonCallback() {             
                public void callback(Person person) {
                System.out.println("xxx同学很厉害…..");
            }
        });
    }

上面的PersonCallback其实就是一种动作,但是我们真正关心的只有callback方法里的内容而已,我们用Lambda 表示,可以将上面的代码就可以优化成:

Person.create(1, "竹子爱熊猫", (Person person) -> {System.out.println("ABCDEFG……");})

有没有发现特别简单了...,但是我们发现Person.create这个方法其实接收的对象依然是PersonCallback这个接口,但是现在传的是一个Lambda表达式,那么难道难道Lambda表达式也实现了这个接口?问题也放这,我们先看一下Lambda表达式的接口。

Lambda允许把函数作为一个方法的参数,一个lambda由用逗号分隔的参数列表、–>符号、函数体三部分表示。对于上面的表达式:

  1. (Person person)为Lambda表达式的入参,{System.out.println("去注册去注册...");}为函数体;
  2. 重点是这个表达式是没有名字的;

我们知道,当我们实现一个接口的时候,肯定要实现接口里面的方法,那么现在一个Lambda表达式应该也要遵循这一个基本准则,那么一个Lambda表达式它实现了接口里的什么方法呢?
答案是:一个Lambda表达式实现了接口里的有且仅有的唯一一个抽象方法。那么对于这种接口就叫做函数式接函数式接口。
Lambda表达式其实完成了实现接口并且实现接口里的方法这一功能,也可以认为Lambda表达式代表一种动作,我们可以直接把这种特殊的动作动作进行传递。
当然,对于上面的Lambda表达式你可以简化:

Person.create(1, "zero", person -> System.out.println("竹子爱熊猫….."));

这归功于Java8的类型推导机制。因为现在接口里只有一个方法,那么现在这个Lambda表达式肯定是对应实现了这个方法,既然是唯一的对应关系,那么入参肯定是Person类,所以可以简写,并且方法体只有唯一的一条语句,所以也可以简写,以达到表达式简洁的效果。

二、函数式接口

函数式接口是新增的一种接口定义。
函数式接口是新增的一种接口定义。
@FunctionalInterface修饰的接口叫做函数式接口,或者,函数式接口就是一个只具有一个抽象方法的普通接口,@FunctionalInterface可以起到校验的作用。

下面的接口只有一个抽象方法能编译正确:

@FunctionalInterface 
public interface TestFunctionalInterface {     
        void test1();
}

下面的接口有多个抽象方法会编译错误:

@FunctionalInterface 
public interface TestFunctionalInterface {     
      void test1();     
      void test2();
}

在JDK7中其实就已经有一些函数式接口了,比如Runnable、Callable、FileFilter等等。在JDK8中也增加了很多函数式接口,比如java.util.function包。

接口 描述
Supplier 无参数,返回一个结果
Function 接受一个输入参数,返回一个结果
Consumer 接受一个输入参数,无返回结果
Predicate 接受一个输入参数,返回一个布尔值结果

那么Java8中给我们加了这么多函数式接口有什么作用?
上文我们分析到,一个Lambda表达式其实也可以理解为一个函数式接口的实现者,但是作为表达式,它的写法其实是多种多样的,比如:

  1. () -> {return 0;},没有传入参数,有返回值
  2. (int i) -> {return 0;},传入一个参数,有返回值
  3. (int i) -> {System.out.println(i)},传入一个int类型的参数,但是没有返回值
  4. (int i, int j) -> {System.out.println(i)},传入两个int类型的参数,但是没有返回值
  5. (int i, int j) -> {return i+j;},传入两个int类型的参数,返回一个int值
  6. (int i, int j) -> {return i>j;},传入两个int类型的参数,返回一个boolean值
    等等,还有许多许多种情况。那么这每种表达式的写法其实都应该是某个函数式接口的实现类,需要特定函数式接口进行对应函数式接口进行对应,比如上面的四种情况就分别对应Supplier<T> , Function<T,R> , Consumer<T> , BiConsumer<T, U> , BiFunction<T, U, R> , BiPredicate<T, U>。答案已经明显了,Java8中提供给我们这么多函数式接口就是为了让我们写Lambda表达式更加方便表达式更加方便,当然遇到特殊情况,你还是需要定义你自己的函数式接口然后才能写对应的Lambda表达式。
    总的来说,如果没有函数式接口,就不能写Lambda表达式。

三、接口默认方法与静态方法

在JDK7中,如果想对接口Collection新增一个方法,那么你需要修改它所有的实现类源码(这是非常恐怖的),在那么Java8之前是怎么设计来解决这个问题的呢,用的是抽象类,比如:现在有一个接口PersonInterface接口,里面有1个抽象方法:

public interface PersonInterface{
    void a();
}

有俩个实现类:

public class MinPerson  implements  PersonInterface{
    @Override 
    public void a(){
         System.out.println("a......MinPerson....");
    }
}
public class MaxPerson  implements  PersonInterface{
    @Override 
    public void a(){
         System.out.println("a......MaxPerson....");
    }
}

现在我需要在PersonInterface接口中新增一个方法,那么势必它的俩个实现类都需要做相应改动才能编译通过,这里我就不进行演示了,那么我们在最开始设计的时候,其实可以增加一个抽象类PersonAbstract,俩个实现类改为继承这个抽象类,按照这种设计方法,对PersonInterface接口中新增一个方法是,其实只需要改动PersonAbstract类去实现新增的方法就好了,其他实现类不需要改动了:
接口:

public interface PersonInterface{
    void a();
}

抽象类:

public class PersonAbstract implements  PersonInterface{
    @Override 
    public void a(){
         System.out.println("a......MaxPerson....");
    }
}

俩个实现类:

public class MinPerson  extends PersonAbstract {
    
}
public class MaxPerson  extends PersonAbstract {
    @Override 
    public void a(){
         System.out.println("a......MaxPerson....");
    }
}

这就是Java8之前无论是框架还是JDK源码都经常使用的一种方式,来保证灵活性,因为每次版本的升级迭代都是会对一个接口中或多或少的进行增加或者删减、修改等操作的,因为不可能一开始就对于一个接口把它的当中的方法确定下来之后就不修改了,因此,在JDK1.8之前,都是使用以上方式。那么在Java8中支持直接在接口中添加已经实现了的方法,一种是Default方法(默认方法),一种Static方法(静态方法):

3.1、接口默认方法

在接口中用default修饰的方法称为默认方法默认方法。
接口中的默认方法一定要有默认实现(方法体),接口实现者可以继承它,也可以覆盖它。

    default void testDefault(){
         System.out.println("default");
}

3.2、接口静态方法

在接口中用static修饰的方法称为静态方法静态方法。

    static void testStatic(){        
        System.out.println("static");
    };

调用方式:

    TestInterface.testStatic();

因为有了默认方法和静态方法,所以你不用去修改它的实现类了,可以进行直接调用。

四、方法引用

有个函数式接口Consumer,里面有个抽象方法accept能够接收一个参数但是没有返回值,这个时候我想实现 accept方法,让它的功能为打印接收到的那个参数,那么我可以使用Lambda表达式这么做:

Consumer<String> consumer = s -> System.out.println(s); 
consumer.accept("熊猫吃竹子");

但是其实我想要的这个功能PrintStream类(也就是System.out的类型)的println方法已经实现了,这一步还可以再简单点,如:

Consumer<String> consumer =System.out::println; 
consumer.accept("熊猫吃竹子");

这就是方法引用,方法引用方法的参数列表必须与函数式接口的抽象方法的参数列表保持一致,返回值不作要求。

4.1、使用方法

引用方法

实例对象::实例方法名

//Consumer<String> consumer = s -> System.out.println(s); 
Consumer<String> consumer = System.out::println; 
consumer.accept("竹子爱熊猫");

System.out代表的就是PrintStream类型的一个实例,println是这个实例的一个方法。

类名::静态方法名

//Function<Long, Long> f = x -> Math.abs(x);
Function<Long, Long> f = Math::abs;
Long result = f.apply(-3L);

Math是一个类而abs为该类的静态方法。Function中的唯一抽象方法apply方法参数列表与abs方法的参数列表相同,都是接收一个Long类型参数。

类名::实例方法名
若Lambda表达式的参数列表的第一个参数,是实例方法的调用者,第二个参数(或无参)是实例方法的参数时,就可以使用这种方法:

//BiPredicate<String, String> b = (x,y) -> x.equals(y);
 BiPredicate<String, String> b = String::equals;
b.test("a", "b");

String是一个类而equals为该类的定义的实例方法。BiPredicate中的唯一抽象方法test方法参数列表与equals方法的参数列表相同,都是接收两个String类型参数。

引用构造器

在引用构造器的时候,构造器参数列表要与接口中抽象方法的参数列表一致,格式为 类名::new。如:

//Function<Integer, StringBuffer> fun = n -> new StringBuffer(n); 
Function<Integer, StringBuffer> fun = StringBuffer::new;
StringBuffer buffer = fun.apply(10);

Function接口的apply方法接收一个参数,并且有返回值。在这里接收的参数是Integer类型,与StringBuffer类的一个构造方法StringBuffer(int capacity)对应,而返回值就是StringBuffer类型。上面这段代码的功能就是创建一个 Function实例,并把它apply方法实现为创建一个指定初始大小的StringBuffer对象。

引用数组

引用数组和引用构造器很像,格式为 类型[]::new,其中类型可以为基本类型也可以是类。如:

// Function<Integer, int[]> fun = n -> new int[n];
 Function<Integer, int[]> fun = int[]::new; 
int[] arr = fun.apply(10);
Function<Integer, Integer[]> fun2 = Integer[]::new; 
Integer[] arr2 = fun2.apply(10);

五、Optional

空指针异常是导致Java应用程序失败的最常见原因,以前,为了解决空指针异常,Google公司著名的Guava项目引入了Optional类,Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到Google Guava的启发,Optional类已经成为Java 8类库的一部分。
Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
创建Optional对象的几个方法:

  1. Optional.of(T value), 返回一个Optional对象,value不能为空,否则会出空指针异常
  2. Optional.ofNullable(T value), 返回一个Optional对象,value可以为空
  3. Optional.empty(),代表空
    其他API:
  4. optional.isPresent(),是否存在值(不为空)
  5. optional.ifPresent(Consumer<? super T> consumer), 如果存在值则执行consumer
  6. optional.get(),获取value
  7. optional.orElse(T other),如果没值则返回other
  8. optional.orElseGet(Supplier<? extends T> other),如果没值则执行other并返回
  9. optional.orElseThrow(Supplier<? extends X> exceptionSupplier),如果没值则执行exceptionSupplier,并抛出异常
    那么,我们之前对于防止空指针会这么写:
public class Order {     
     private String name;     
     public String getOrderName(Order order ){
         if (order == null){
             return null;
        }         
        return order.name;
    }
}

现在用Optional,会改成:

public class Order{
    private String name;
    public String getOrderName(Order order ){
 //        if (order == null) { 
//            return null;
//        }
//
//        return order.name;
        Optional<Order> orderOptional =Optional.ofNullable(order);         
        if (!orderOptional.isPresent()) {
             return null;
        }         
    return orderOptional.get().name;
    }
}

那么如果只是改成这样,实质上并没有什么分别,事实上isPresent() 与 obj != null 无任何分别,并且在使用get() 之前最好都使用isPresent() ,比如下面的代码在IDEA中会有警告:'Optional.get()' without 'isPresent()' check。

    public void createOrder(String userName){
         Optional optional =Optional.ofNullable(userName);         
         optional.get();
    }

另外把 Optional 类型用作属性或是方法参数在 IntelliJ IDEA 中更是强力不推荐的。对于上面的代码我们利用IDEA的提示可以优化成一行(666!):

public class Order{
     String name;
public String getOrderName(Order order ){
 //        if (order == null) { 
//            return null;
//        }
//
//        return order.name;
        return Optional.ofNullable(order).map(order1 -> order1.name).orElse(null);
    }
}

这个优化过程中map()起了很大作用。

高级API:

  1. optional.map(Function<? super T, ? extends U> mapper),映射,映射规则由function指定,返回映射值的Optional,所以可以继续使用Optional的API。
  2. optional.flatMap(Function<? super T, Optional< U > > mapper),同map类似,区别在于map中获取的返回值自动被Optional包装,flatMap中返回值保持不变,但入参必须是Optional类型。
  3. optional.filter(Predicate<? super T> predicate),过滤,按predicate指定的规则进行过滤,不符合规则则返回empty,也可以继续使用Optional的API。

Optional总结
使用 Optional 时尽量不直接调用 Optional.get() 方法, Optional.isPresent() 更应该被视为一个私有方法, 应依赖于其他像 Optional.orElse(), Optional.orElseGet(), Optional.map() 等这样的方法。

六、Stream流

Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。
在传统的 J2EE 应用中,Java 代码经常不得不依赖于关系型数据库的操作如:取平均值、取最大最小值、取汇总值、或者进行分组等等类似的这些操作。
但在当今这个数据大爆炸的时代,在数据来源多样化、数据海量化的今天,很多时候不得不脱离 RDBMS,或者以底层返回的数据为基础进行更上层的数据统计。而 Java 的集合 API 中,仅仅有极少量的辅助型方法,更多的时候是程序员需要用 Iterator 来遍历集合,完成相关的聚合应用逻辑。这是一种远不够高效、笨拙的方法。在Java 7 中,如果要找一年级的所有学生,然后返回按学生分数值降序排序好的学生ID的集合,我们需要这样写:

public class Student {
    private Integer id;        // ID     
    private Grade grade;       // 年纪  
    private Integer score;     // 分数
    public Student(Integer id, Grade grade, Integer score){
         this.id = id;
         this.grade = grade;
         this.score = score;
    }
    public Integer getId(){
         return id;
    }
   public void setId(Integer id){
         this.id = id;
    }
   public Grade getGrade() {
        return grade;
    }
   public void setGrade(Grade grade){
         this.grade = grade;
    }
   public Integer getScore(){
         return score;
    }
   public void setScore(Integer score){
         this.score = score;
    }
}
public enum  Grade {     
FIRST, SECOND, THTREE
}
   public static void main(String[] args) {
        final Collection<Student> students=Arrays.asList(                 
                new Student(1, Grade.FIRST, 60),
                new Student(2, Grade.SECOND, 80),
                new Student(3, Grade.FIRST, 100)
        );
        List<Student> gradeOneStudents = Lists.newArrayList();         
        for (Student student: students) {
             if (Grade.FIRST.equals(student.getGrade())){
                 gradeOneStudents.add(student);
            }
        }
        Collections.sort(gradeOneStudents, new Comparator<Student>() {
            @Override            
             public int compare(Student o1, Student o2){
                   return o2.getScore().compareTo(o1.getScore());
            }
        });
        List<Integer> studentIds = new ArrayList<>();         
        for(Student t:gradeOneStudents){
             studentIds.add(t.getId());
        }
    }

而在 Java 8 使用 Stream,代码更加简洁易读;而且使用并发模式,程序执行速度更快,如果我们使用Stream流可以这样写:

public static void main(String[] args) {
         final Collection< Student > students=Arrays.asList(
                 new Student(1, Grade.FIRST, 60),
                 new Student(2, Grade.SECOND, 80),
                 new Student(3, Grade.FIRST, 100)
        );
        List<Integer> studentIds = students.stream()
              .filter(student ->student.getGrade().equals(Grade.FIRST))
                .sorted(Comparator.comparingInt(Student::getScore))
                .map(Student::getId)
                .collect(Collectors.toList());
    }

6.1、什么是Stream?

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的Iterator,而且它和java的IO流没有任何关系!!!

6.2、Stream产生的原因?

其实类似于Stream流能做的聚合操作都是能够在数据库中使用SQL的聚合函数来解决,比如:条件过滤有where,分组有group By,排序有order by.....,但是我们为什么还要使用Stream流来处理集合元素(一般在开发中集合中的元素都是从数据库中查询的)呢?因为如果开发中涉及到多数据源,比如我可能一个集合中的元素一部分是从ES、redis、DB.....不同地方拿出来的,这时候SQL语句的聚合函数就显得有些鸡肋了,并且还有一个点,如果有朋友做过分布式或者分库分表环境下的开发,都是能知道这样的数据一般都是走数据聚合的方式,因为有多个库,无法通过DB为我们提供的原生函数来对数据进行精准过滤,总的来说,Stream流更适合的是在多数据源的情况下使用(其实不使用Stream流也能够通过迭代器或者for去做,但是代码量可能会很大)。

6.3、Stream的特点

  1. Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
  2. Stream 就如同一个Iterator,单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
  3. Stream 可以并行化操作,Iterator只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架来拆分任务和加速处理过程。

6.4、Stream的构成

当我们使用一个流的时候,通常包括三个基本步骤:
获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。
图 1. 流管道 (Stream Pipeline) 的构成


image.png

6.5、生产Stream Source的方式

从Collection 和数组生成

  1. Collection.stream()
  2. Collection.parallelStream()
  3. Arrays.stream(T array)
  4. Stream.of(T t)

从 BufferedReader

  1. java.io.BufferedReader.lines()

静态工厂

  1. java.util.stream.IntStream.range()
  2. java.nio.file.Files.walk()

自己构建

  1. java.util.Spliterator

其他

  1. Random.ints()
  2. BitSet.stream()
  3. Pattern.splitAsStream(java.lang.CharSequence)
  4. JarFile.stream()

6.6、Stream的操作类型

中间操作(Intermediate Operation): 一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
终止操作(Terminal Operation): 一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果。

Intermediate Operation又可以分为两种类型:
无状态操作(Stateless Operation):操作是无状态的,不需要知道集合中其他元素的状态,每个元素之间是相互独立的,比如map()、filter()等操作。
有状态操作(Stateful Operation):有状态操作,操作是需要知道集合中其他元素的状态才能进行的,比如sort()、distinct()。

Terminal Operation从逻辑上可以分为两种:
短路操作(short-circuiting):短路操作是指不需要处理完所有元素即可结束整个过程
非短路操作(non-short-circuiting):非短路操作是需要处理完所有元素之后才能结束整个过程

6.7、Stream的使用

简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个最终结果,或者导致一个副作用。

// 1. Individual values 
Stream stream = Stream.of("a", "b","c");
// 2. Arrays
String [] strArray = new String[] {"a","b","c"};
stream = Stream.of(strArray); 
stream = Arrays.stream(strArray); 
// 3. Collections 
List<String> list = Arrays.asList(strArray); stream = list.stream();

需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:
IntStream、LongStream、DoubleStream。当然我们也可以用 Stream、Stream >、Stream,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。
Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。

数值流的构造

IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println); 

流转换为其他数据结构

Stream<String> stream = Stream.<String>of(new String[]{"1", "2", "3"});
List<String> list1 = stream.collect(Collectors.toList()); 
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
String str = stream.collect(Collectors.joining());
System.out.println(str);

一个 Stream 只可以使用一次,上面的代码为了简洁而重复使用了数次。

6.8、流的典型用法

map/flatMap
我们先来看 map。如果你熟悉 scala 这类函数式语言,对这个方法应该很了解,它的作用就是把 input Stream 的每一个元素,映射成 output Stream 的另外一个元素。例子:

Stream<String> stream = Stream.<String>of(new String[]{"a", "b", "c"});
stream.map(String::toUpperCase).forEach(System.out::println);

这段代码把所有的字母转换为大写。map 生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。还有一些场景,是一对多映射关系的,这时需要 flatMap。

Stream<List<Integer>> inputStream = Stream.of(
                Arrays.asList(1),
                Arrays.asList(2, 3),                 
                Arrays.asList(4, 5, 6)
        );
        Stream<Integer> mapStream = inputStream.map(List::size);
        Stream<Integer> flatMapStream = inputStream.flatMap(Collection::stream);

filter
filter 对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。

Integer[] nums = new Integer[]{1,2,3,4,5,6};         
Arrays.stream(nums).filter(n -> n<3).forEach(System.out::println);

将小于3的数字留下来。

forEach
forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次 terminal 运算。下面的代码会报错。

Integer[] nums = new Integer[]{1,2,3,4,5,6}; 
Stream stream = Arrays.stream(nums); 
stream.forEach(System.out::print); 
stream.forEach(System.out::print);

相反,具有相似功能的 intermediate 操作 peek 可以达到上述目的。

Integer[] nums = new Integer[]{1,2,3,4,5,6};
Stream stream = Arrays.stream(nums);         
stream.peek(System.out::print).peek(System.out::print).collect(Collectors.toList());

forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。下面的代码还是打印出所有元素,并不会提前返回。

Integer[] nums = new Integer[]{1,2,3,4,5,6};
 Arrays.stream(nums).forEach(integer ->{
       System.out.print(integer);
       return;
 });

forEach和常规 for 循环的差异不涉及到性能,它们仅仅是函数式风格与传统Java 风格循环的差异。

reduce
这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于:

Integer[] nums = new Integer[]{1,2,3,4,5,6};
Integer sum = Arrays.stream(nums).reduce(0, (integer, integer2) -> integer+integer2);
System.out.println(sum);

也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。

Integer[] nums = new Integer[]{1,2,3,4,5,6};
 // 有初始值
Integer sum = Arrays.stream(nums).reduce(0, Integer::sum);
 // 无初始值
Integer sum1 = Arrays.stream(nums).reduce(Integer::sum).get();

Limit/skip
limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素。

Integer[] nums = new Integer[]{1,2,3,4,5,6};
Arrays.stream(nums).limit(3).forEach(System.out::print);
System.out.println();         
Arrays.stream(nums).skip(2).forEach(System.out::print);

结果:

123
3456

sorted/min/max/distinct
对 Stream 的排序通过 sorted 进行,它比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、filter、 limit、skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。

Integer[] nums = new Integer[]{1,2,3,4,5,6};
Arrays.stream(nums).sorted((i1, i2) -> i2.compareTo(i1)).limit(3).forEach(System.out::print);         
System.out.println();         
Arrays.stream(nums).sorted((i1, i2) -> i2.compareTo(i1)).skip(2).forEach(System.out::print);
Integer[] nums = new Integer[]{1, 2, 2, 3, 4, 5, 5, 6};
System.out.println(Arrays.stream(nums).min(Comparator.naturalOrder()).get());         
System.out.println(Arrays.stream(nums).max(Comparator.naturalOrder()).get());
Arrays.stream(nums).distinct().forEach(System.out::print);

Match
Stream 有三个 match 方法,从语义上说:

  1. allMatch:Stream 中全部元素符合传入的 predicate,返回 true 。
  2. anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true。
  3. noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true。

它们都不是要遍历全部元素才能返回结果。例如 allMatch 只要一个元素不满足条件,就 skip 剩下的所有元素,返回 false。

Integer[] nums = new Integer[]{1, 2, 2, 3, 4, 5, 5, 6};                   
System.out.println(Arrays.stream(nums).allMatch(integer -> integer < 7)); 
System.out.println(Arrays.stream(nums).anyMatch(integer -> integer < 2)); 
System.out.println(Arrays.stream(nums).noneMatch(integer -> integer < 0));

结果都为true。
用Collectors来进行reduction操作
java.util.stream.Collectors 类的主要作用就是辅助进行各类有用的 reduction 操作,例如转变输出为 Collection,把 Stream 元素进行归组。
例如对上面的Student进行按年级进行分组:

 final Collection<Student> students = Arrays.asList(                 
                 new Student(1, Grade.FIRST, 60),                 
                 new Student(2, Grade.SECOND, 80),
                 new Student(3, Grade.FIRST, 100)
        );
        // ᤈᬰᕆଙೲړᕟ
        students.stream().collect(Collectors.groupingBy(Student::getGrade)).forEach(((grade, students1) -> {
                 System.out.println(grade);             
                 students1.forEach(student ->                  
                 System.out.println(student.getId()+","+student.getGrade()+","+student.getScore()));
        }));
结果:FIRST
1,FIRST,60
3,FIRST,100 SECOND
2,SECOND,80

例如对上面的Student进行按分数段进行分组:

students.stream().collect(Collectors.partitioningBy(student -> student.getScore() <=60))
.forEach(((grade, students1) -> {
             System.out.println(grade);
             students1.forEach(student ->       
            System.out.println(student.getId()+","+student.getGrade()+","+student.getScore()));
        }));
结果:
false
2,SECOND,80 
3,FIRST,100 true
1,FIRST,60

parallelStream
parallelStream其实就是一个并行执行的流.它通过默认的ForkJoinPool,可以提高你的多线程任务的速度。
parallelStream的使用

Arrays.stream(nums).parallel().forEach(System.out::print); 
System.out.println(Arrays.stream(nums).parallel().reduce(Integer::sum).get());
System.out.println();
Arrays.stream(nums).forEach(System.out::print); 
System.out.println(Arrays.stream(nums).reduce(Integer::sum).get());

parallelStream要注意的问题
parallelStream底层是使用的ForkJoin。而ForkJoin里面的线程是通过ForkJoinPool来运行的,Java 8为ForkJoinPool添加了一个通用线程池,这个线程池用来处理那些没有被显式提交到任何线程池的任务。它是 ForkJoinPool类型上的一个静态元素。它拥有的默认线程数量等于运行计算机上的处理器数量,所以这里就出现了这个java进程里所有使用parallelStream的地方实际上是公用的同一个ForkJoinPool。parallelStream提供了更简单的并发执行的实现,但并不意味着更高的性能,它是使用要根据具体的应用场景。如果cpu资源紧张 parallelStream不会带来性能提升;如果存在频繁的线程切换反而会降低性能。

Stream总结

  1. 不是数据结构,它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、
    IO channel)抓取数据
  2. 它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素
  3. 所有 Stream 的操作必须以 lambda 表达式为参数
  4. 惰性化,很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始,Intermediate 操作永远是惰性化的。
  5. 当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。

七、Date/Time API

Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。对日期与时间的操作一直是
Java程序员 痛苦的地方之一。标准的 java.util.Date以及后来的java.util.Calendar一点没有改善这种情况(可以这么说,它们一定程度上更加复杂)。
这种情况直接导致了Joda-Time——一个可替换标准日期/时间处理且功能非常强大的Java API的诞生。Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影响,并且吸取了其精髓。
LocaleDate只持有ISO-8601格式且无时区信息的日期部分:

 LocalDate date = LocalDate.now();  //       当前日期         
date = date.plusDays(1);           // 增加一天         
date = date.plusMonths(1);         // 增加一月         
date = date.minusDays(1);          // 减少一天         
date = date.minusMonths(1);        // 减少一月
System.out.println(date);          // 依然输出当前时间  

LocalTime类
LocaleTime只持有ISO-8601格式且无时区信息的时间部分

LocalTime time = LocalTime.now();    // 当前时间         
time = time.plusMinutes(1);          // 增加一分钟         
time = time.plusSeconds(1);          // 增加一秒钟         
time = time.minusMinutes(1);         // 减少一分钟         
time = time.minusSeconds(1);         // 减少一秒钟
System.out.println(time);            // 依然输出当前时间

LocaleDateTime类和格式化
LocaleDateTime把LocaleDate与LocaleTime的功能合并起来,它持有的是ISO-8601格式无时区信息的日期与时间。

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime);      // 2019-12-18T21:13:07.465҅UTC格式
System.out.println(localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));  // 2019-12-18 21:13:07 自定义格式

ZonedDateTime类
如果你需要特定时区的日期/时间,那么ZonedDateTime是你的选择。它持有ISO-8601格式具具有时区信息的日期与时间。

final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) );
System.out.println(zonedDatetimeFromZone);        //  2019-12-18T05:15:34.486-08:00[America/Los_Angeles]

Clock类
它通过指定一个时区,然后就可以获取到当前的时刻,日期与时间。Clock可以替换System.currentTimeMillis()与
TimeZone.getDefault()。

final Clock utc = Clock.systemUTC();  // 协调世界时,又称为世界统一时间、世界标准时间、国际协调时间
final Clock shanghai = Clock.system(ZoneId.of("Asia/Shanghai"));  // 上海
System.out.println(LocalDateTime.now(utc));         // 2019-12-18T13:18:09.176
System.out.println(LocalDateTime.now(shanghai));    // 2019-12-18T21:18:09.177

Duration类
Duration使计算两个日期间的不同变的十分简单。

final LocalDateTime from = LocalDateTime.parse("2018-12-17 18:50:50", DateTimeFormatter.ofPattern("yyyyMM-dd HH:mm:ss")); 
final LocalDateTime to = LocalDateTime.parse("2018-12-18 19:50:50", DateTimeFormatter.ofPattern("yyyyMM-dd HH:mm:ss"));
final Duration duration = Duration.between(from, to);
System.out.println("Duration in days: " + duration.toDays()); // 1
System.out.println("Duration in hours: " + duration.toHours()); // 25

八、其他特性

8.1、重复注解

假设,现在有一个服务我们需要定时运行,就像Linux中的cron一样,假设我们需要它在每周三的12点运行一次,那我们可能会定义一个注解,有两个代表时间的属性。

public @interface Schedule {   
    int dayOfWeek() default 1;   // 周几
    int hour() default 0;    // 几点ᅩ
}

所以我们可以给对应的服务方法上使用该注解,代表运行的时间:

public class ScheduleService{    
// 每周三的12点运行
    @Schedule(dayOfWeek = 3, hour = 12)     
public void start() {
        // 执行业务
    }
}

那么如果我们需要这个服务在每周四的13点也需要运行一下,如果是JDK8之前,那么...尴尬了!你不能像下面的代码,会编译错误

public class ScheduleService{
// jdk中俩个相同的注解会编译报错
    @Schedule(dayOfWeek = 3, hour = 12)     
    @Schedule(dayOfWeek = 4, hour = 13)     
    public void start() {
        // 执行业务
    }
}

那么如果是JDK8,你可以改一下注解的代码,在自定义注解上加上@Repeatable元注解,并且指定重复注解的存储注解(其实就是需要需要数组来存储重复注解),这样就可以解决上面的编译报错问题。

@Repeatable(value =Schedule.Schedules.class) 
public @interface Schedule {     
int dayOfWeek() default 1;     
int hour() default 0;
@interface Schedules{
         Schedule[] value();
    }
}

同时,反射相关的API提供了新的函数getAnnotationsByType()来返回重复注解的类型。添加main方法:

public static void main(String[] args){
     try {
            Method method = ScheduleService.class.getMethod("start"); 
            for (Annotation annotation : method.getAnnotations()){
                 System.out.println(annotation);
            }             
            for (Schedule s : method.getAnnotationsByType(Schedule.class)){
                 System.out.println(s.dayOfWeek() + "|" +s.hour());
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
輸出:
@repeatannotation.Schedule$Schedules(value=[@repeatannotation.Schedule(hour=12, dayOfWeek=3), 
@repeatannotation.Schedule(hour=13, dayOfWeek=4)])
3|12
4|13

8.2、扩展注解

注解就相当于一种标记,在程序中加了注解就等于为程序加了某种标记。
JDK8之前的注解只能加在:

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,        // 类、枚举、接口
    /** Field declaration (includes enum constants) */
    FIELD,       // 类型变量
    /** Method declaration */
    METHOD,      // 方法
    /** Parameter declaration */
    PARAMETER,   // 方法参数
    /** Constructor declaration */     CONSTRUCTOR,  // 构造方法
    /** Local variable declaration */
    LOCAL_VARIABLE, // 局部变量
    /** Annotation type declaration */
    ANNOTATION_TYPE, // 注解类型
    /** Package declaration */
    PACKAGE           // 包
}

JDK8中新增了两种:

  1. TYPE_PARAMETER,表示该注解能写在类型变量的声明语句中。
  2. TYPE_USE,表示该注解能写在使用类型的任何语句中
    checkerframework中的各种校验注解,比如:@Nullable, @NonNull等等。
public class GetStarted {     void sample() {
        @NonNull Object ref = null;
    }
}

8.3、更好的类型推测机制

直接看代码:

public class Value<T> {     
public static<T> T defaultValue(){
         return null;
    }
public T getOrDefault(T value, T defaultValue){
         return value != null ? value : defaultValue;
    }
public static void main(String[] args) {
         Value<String> value = new Value<>();         
        System.out.println(value.getOrDefault("22", Value.defaultValue()));
    }
}

上面的代码重点关注value.getOrDefault("22", Value.defaultValue()), 在JDK8中不会报错,那么在JDK7中呢?
答案是会报错: Wrong 2nd argument type. Found: 'java.lang.Object', required: 'java.lang.String' 。所以Value.defaultValue()的参数类型在JDK8中可以被推测出,所以就不必明确给出。

8.4、参数名字保留在字节码中

先来想一个问题:JDK8之前,怎么获取一个方法的参数名列表?在JDK7中一个Method对象有下列方法:
Method.getParameterAnnotations()获取方法参数上的注解
Method.getParameterTypes()获取方法的参数类型列表
但是没有能够获取到方法的参数名字列表!在JDK8中增加了两个方法:
Method.getParameters()获取参数名字列表
Method.getParameterCount()获取参数名字个数

8.5、StampedLock

是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发。

8.6、ReentrantLock

ReentrantLock类,实现了Lock接口,是一种可重入的独占锁,它具有与使用 synchronized 相同的一些基本行为和语义,但功能更强大。ReentrantLock内部通过内部类实现了AQS框架(AbstractQueuedSynchronizer)的API来实现独占锁的功能。

8.7、ReentrantReadWriteLock

ReentrantReadWriteLock和ReentrantLock不同,ReentrantReadWriteLock实现的是ReadWriteLock接口。
读写锁的概念:加读锁时其他线程可以进行读操作但不可进行写操作,加写锁时其他线程读写操作都不可进行。
但是,读写锁如果使用不当,很容易产生“饥饿”问题,在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会阻塞。,在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。

8.8、并行数组

Java 8增加了大量的新方法来对数组进行并行处理。可以说, 重要的是parallelSort()方法,因为它可以在多核机器上极大提高数组排序的速度。下面的例子展示了新方法(parallelXxx)的使用。
下面的代码演示了先并行随机生成20000个0-1000000的数字,然后打印前10个数字,然后使用并行排序,再次打印前10个数字。

 long[] arrayOfLong = new long [ 20000 ];
Arrays.parallelSetAll( arrayOfLong, index -> ThreadLocalRandom.current().nextInt( 1000000 ) );     
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(i -> System.out.print( i + " " ) );         
System.out.println();
Arrays.parallelSort( arrayOfLong );         
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(                 
i -> System.out.print( i + " " ) );
System.out.println();

8.9、获取方法参数的名字

在Java8之前,我们如果想获取方法参数的名字是非常困难的,需要使用ASM、javassist等技术来实现,现在,在Java8中则可以直接在Method对象中就可以获取了。

public class ParameterNames {     
public void test(String p1, String p2) {}
public static void main(String[] args) throws Exception {
         Method method = ParameterNames.class.getMethod("test", String.class, String.class);
        for (Parameter parameter : method.getParameters()){
             System.out.println(parameter.getName());
        }
        System.out.println(method.getParameterCount());
    }
}

输出:arg0 arg1 2
从结果可以看出输出的参数个数正确,但是名字不正确!需要在编译编译时增加–parameters参数后再运行。
在Maven中增加:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>     
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <configuration>
        <compilerArgument>-parameters</compilerArgument>
        <source>1.8</source>
        <target>1.8</target>     
    </configuration>
</plugin>

输出结果则变为:p1 p2 2

8.X.1 、CompletableFuture

当我们Javer说异步调用时,我们自然会想到Future,比如:

public class FutureDemo {
    /**
*   异步进行一个计算
*   @param args
     */     
public static void main(String[]args) {
ExecutorService executor = Executors.newCachedThreadPool();         
Future<Integer> result = executor.submit(new Callable<Integer>(){
             public Integer call() throws Exception {
                 int sum=0;
                System.out.println("ྋࣁᦇᓒ...");                 
                for (int i=0; i<100; i++){
                     sum = sum + i;
                }
                 Thread.sleep(TimeUnit.SECONDS.toSeconds(3)); 
                  System.out.println("计算完毕!");                 
              return sum;
            }
        });
        System.out.println("做其他事情…");
        try {
            System.out.println("result:" +result.get());         
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("事情都做完了...");
                executor.shutdown();
    }
}

那么现在如果想实现异步计算完成之后,立马能拿到这个结果继续异步做其他事情呢?这个问题就是一个线程依赖另外一个线程,这个时候Future就不方便,我们来看一下CompletableFuture的实现:

public static void main(String[] args) {         
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture result = CompletableFuture.supplyAsync(() ->{
      int sum=0;
     System.out.println("正在计算...");             
      for (int i=0; i<100; i++) {
                 sum = sum + i;
       }
             try {
                Thread.sleep(TimeUnit.SECONDS.toSeconds(3));             
             } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"ᓒਠԧѺ");             
      return sum;
        }, executor).thenApplyAsync(sum -> {
            System.out.println(Thread.currentThread().getName()+"಑ܦ"+sum);             
      return sum;         
}, executor);
        System.out.println("做其他事情”);
        try {
            System.out.println("result:" + result.get());         
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("事情都做完了...");
       executor.shutdown();
}
结果:
正在计算…..
正在做其他事….
Pool-1-thread-1算
了 Pool-1-thread-2打印4950
事情都做完了...

只需要简单的使用thenApplyAsync就可以实现了。当然CompletableFuture还有很多其他的特性,我们下次单独开个专题来讲解。
Java8的特性还有Stream和Optional,这两个也是用的特别多的,相信很多同学早有耳闻,并且已经对这两个的特性有所了解,所以本片博客就不进行讲解了,有机会再单独讲解,可讲的内容还是非常之多的对于Java8,新增的特性还是非常之多的,就是目前Java11已经出了,但是Java8中的特性肯定会一直在后续的版本中保留的,至于这篇文章的这些新特性我们估计用的比较少,所以特已此篇来进行一个普及,希望都有所收货。

8.X.2、Java虚拟机(JVM)的新特性

PermGen空间被移除了,取而代之的是Metaspace(JEP 122)。JVM选项-XX:PermSize与-XX:MaxPermSize分别被-XX:MetaSpaceSize与-XX:MaxMetaspaceSize所代替。

九、java8特性总结

对于java版本的特性而言,其实Java8是整个Java版本中改变最大的一次,因为在8中引入了函数式编程风格和解决了以前版本一直遗留的一些问题等等,这也是为什么8版本会成为主流版本的原因之一,我们下一代主流版本应该是11或者13,当然,这只是我个人的猜想。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 2019年9月19日java13已正式发布,感叹java社区强大,经久不衰。由于国内偏保守,新东西总要放一放,让其...
    CodingAndLiving阅读 812评论 0 1
  • 前言:Java 8 已经发布很久了,很多报道表明Java 8 是一次重大的版本升级。在Java Code Geek...
    糖宝_阅读 1,321评论 1 1
  • Java 8自Java 5(发行于2004)以来最具革命性的版本。Java 8 为Java语言、编译器、类库、开发...
    谁在烽烟彼岸阅读 887评论 0 4
  • 與k姑娘見面。 有些日子不見,她眼角有些許皺紋,還是白净秀气,我喜歡的樣子。 緣是她看到我發的關於瑜伽的文章,她問...
    Wendyzhen阅读 324评论 0 0
  • 我从未感觉到人生能有一天有着这样飞一样的速度。 我现在仍记得高考的那天,我低低的扎着马尾,穿着绿色格子衬衣和浅蓝色...
    辛辛辛烷阅读 919评论 8 6