java 1.8新增特性详解

Java 8新增的特性主要包含以下几个新的语法:

  1. Lambda表达式。
  2. 函数式接口
  3. 方法引用与构造器引用
  4. Stream API
  5. 接口中默认方法与静态方法
  6. 新时间日期API
  7. 其他新的特性

下面分别学习Java 8的新特性。

Lambda表达式

1、什么是lambda表达式

Lambda其实就是一个匿名函数,Lambda允许将函数作为一个方法的参数,传递进方法当中,我们就把Lambda表达式理解为一个可以传递的代码,这样写出来的代码,就可以做到简洁、灵活。

下面看一个例子,对员工按照年龄进行排序,我们在没学Lambda之前,可以这样做。

@Test
    public void test4(){

       List<Employee> lists= Arrays.asList(
            new Employee("张三",18,5555.55),
            new Employee("李四",20,6666.66),

            new Employee("王二",5,10000.55),

            new Employee("麻子",40,50000.55));

        Collections.sort(lists, new Comparator<Employee>() {
            @Override
            public int compare(Employee o1, Employee o2) {
                return Integer.compare(o1.getAge(),o2.getAge());
            }
        });
        for(Employee employee:lists){
            System.out.println(employee);
        }


    }

运行结果:

如果我们学过Lambda之后可以这样做:

List<Employee> lists= Arrays.asList(
            new Employee("张三",18,5555.55),
            new Employee("李四",20,6666.66),

            new Employee("王二",5,10000.55),

            new Employee("麻子",40,50000.55));

Collections.sort(lists,(x,y) -> Integer.compare(x.getAge(),y.getAge()));

for(Employee employee:lists){
            System.out.println(employee);
        }

同样能够做到按照年龄排序,从Lambda表达式我们可以看出,Lambda表达式替代了Comparator匿名内部类的位子,作为了一个可以传递数据的代码段,是不是很简洁,很高效,下面来了解一下Lambda表达式的语法规则。

2、Lambda表达式的语法规则

Lambda表达式在1.8版本中引入了新的语法规则,包括元素与“->”操作符,“->”操作符将Lambda分为两个部分:

    左侧:指定了Lambda表达式需要传递的参数。 右侧:指定了Lambda体,也就是要写的一些代码功能。

语法格式一:无参,没有返回值,并且Lambda只含有一行代码;

Runnable runnable=() -> System.out.print("Hello Lambda");

语法格式二:只含有一个参数,没有返回值;当只有一个参数时,()也可以省略。

Consumer<String> consumer=(x) -> System.out.print(x); 当然也可以这样写 Consumer<String> consumer=x -> System.out.print(x);

语法格式三:多个参数时,并且有返回值,当Lambda代码多于两行时,别忘了加{}哦。

BinaryOperator<Integer> bo=(x,y)->{ System.out.println("这是实现函数式接口的方法"); return x+y; };

不知道大家有没有发现,Lambda表达式中的代码实现,其实就是实现了函数式接口中的方法。什么是函数式接口?稍后进行学习。

语法格式四:当Lambda只有一条语句时,return与大括号可以省略

BinaryOperator<Integer> bo=(x,y)->return x+y;

语法格式五:Lambda表达式中的参数类型,可以省略不写,编译器可以根据上下文进行推断得出。

BinaryOperator<Integer> bo=(x,y)->{ System.out.println("这是实现函数式接口的方法"); return x+y; }; 当然也可以这样写 BinaryOperator<Integer> bo=(Integer x,Integer y)->{ System.out.println("这是实现函数式接口的方法"); return x+y; };

上述的Lambda表达式中的参数类型都是由编译器推断得出的。Lambda表达式中无需指定类型,程序依然可以编译,这是因为javac根据程序的上下文,在后台推断出了参数的类型。Lambda表达式的类型依赖上下文环境,是由编译器推断出来的。

函数式接口

上面我们接触到了函数式接口这个名词,实际上,上面讲的Lambda表达式需要函数式接口的支持,也就是Lambda表达式的对函数式接口中的方法进行了实现。下面了解一下函数式接口。

什么是函数式接口

    只包含一个抽象方法的接口,称为函数式接口。 可以通过Lambda表达式创建该接口的对象。(若Lambda表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)。 可以在任何函数式接口上使用@FunctionalInterface注解,这样做可以检查它是否是一个函数式接口,同时javadoc也会包含一条声明,说明这个接口是一个函数式接口。

从上面,我们可以看出,Lumbda表达式作为参数进行传递,为了将Lambda表达式作为参数传递,接受参数类型必须与Lambda表达式兼容的函数式接口的类型。

四大核心内置函数:

函数式接口 参数类型 返回类型 用途 Consumer<T> 消费型接口 T void 对类型T参数操作,无返回结果,包含方法void accept(T t) Supplier<T> 供给型接口 无 T 返回类型为T的对象,包含方法:T get() Function<T,R> 函数型接口 T R 对类型为T的对象应用操作,并返回结果。结果是R类型的对象,包含方法 R apply(T t) Predicate<T> 断定型接口 T boolean 确定类型为T的对象是否满足某约束,并返回boolean值,包含方法boolean test(T t)

内置函数的使用:

/**
     * 四大内置函数
     * 1、void Consumer<T> 消费型接口
     * 2、T Supplier 供给型接口
     * 3、Function<T,R> 函数型接口
     * 4、boolean Predicate<T>断言型接口
     *
     */
    @Test
    public void test1(){
        double price=10000.0;

        consumer(price,(p) -> System.out.print("一共消费了:"+p+"元"));
    }

    public void consumer(double price, Consumer<Double> consumer){
        consumer.accept(price);
    }

    @Test//利用供给型接口随机生成5个大于50小于100的数
    public void test2(){
        int num=5;
        List<Integer> list=(List)supplier(num,()-> {
            return (int)(Math.random()*50+50);

        });
        list.forEach(System.out::println);

    }

    public T supplier(int num, Supplier<Integer> supplier ){
        List<Integer> list=new ArrayList<>();
        for(int i=0;i<num;++i){
            Integer integer=supplier.get();
            list.add(integer);
        }
        return (T)list;
    }

    @Test//函数式接口 对字符串进行处理
    public void test3(){
        String s=strHandle("			 Function<T,R> ",(str) -> str.trim());
        System.out.print(s);

    }
    public String strHandle(String str, Function<String,String> function){
        return function.apply(str);
    }

    @Test//断言式接口 将字符个数大于3的字符串输出
    public void test4(){
        List<String> list= Arrays.asList("asdbjkfd","shdfk","dfds","a","v");
        List<String> newList=filterString(list,(str)-> str.length()>3 );
        newList.forEach(System.out::println);
    }
    public List<String> filterString(List<String> list, Predicate<String> predicate){
        List<String> newList=new ArrayList<>();
        for(String str:list){
            if(predicate.test(str)){
                newList.add(str);
            }
        }
        return newList;

    }

四大函数式接口,当然还有其他的函数接口类型,他们的设计都是为Lambda表达式服务的,这些内置的函数,涵盖了我们常用的类型,即,消费型接口Consumer<T>,供给型接口Supplier<T>,函数型接口Function<T,R>,断言型接口Predicate<T>,这是一些基本的函数式接口类型,我们可以不用自己定义一些接口,而使用java给我们提供的这些内置接口,更好的服务于lambda表达式。

Stream API

在java 1.8中新增的两个重要改变分别是:Lambda表达式;Stream API。Stream是Java8中处理集合非常厉害的工具,它可以对集合进行查找、过滤、映射等数据操作。Stream的出现使得对数据源的操作更加高效。Stream是操作数据源(集合、数组等)所生成的元素序列,它不会存储数据,而是对数据进行操作,当然这个操作并不会改变源数组,相反,会返回一个操作后的一个结果Stream。Stream操作的特点是延迟执行。这意味着他们会等到需要结果的时候才会执行,也叫做“惰性求值”。

Stream操作的基本流程

从上图中不难发现,主要分为3步

    根据数据源获取流Stream。 一系列的中间操作,对数据进行处理。 终止操作,执行中间的操作链,并产生结果。

获取Stream流

当然,在操作Stream之前,我们需要做的就是先获取Stream。获取Stream主要有以下几种方式:

获取Stream方式   在java8中的Collection接口被扩展,提供了两个获取流的方法: list.stream();//通过创建串行流 list.parallelStream();//创建并行流 当然Java8的Arrays的静态方法stream()也可以获得流 Arrays.stream(T[] array);//java8的Arrays的静态方法stream可以获取数组流   可以通过静态方法获取流 Stream.of(T...values)//通过Stream.of方式创建流   由函数创建流 Stream.interate()和Stream.generate()创建无限流

Stream的中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何处理。而只会在终止操作时一次性全部处理,称为“惰性求值”。

方法 描述 map(Function f) 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素 filter(Predicate p) 接收Lambda表达式,从流中过滤出某些元素 distinct() 筛选,通过流生成元素的hashCode()和equals()去除重复元素 limit(long maxSize) 截断流,使元素不超过给定数量 skip(long n) 跳过元素,返回一个扔掉了前n个元素的流。若流中元素不足n个,则返回一个空流。 flatMap(Function f) 接收一个函数作为参数,将流中的每一个值都换成另一个流,然后把所有流连接成一个流。 sorted() 产生一个新流,其中按自然顺序排序。 sorted(Comparator com) 产生一个新流,其中按自定义比较强顺序排序
@Test
    public void test1(){

        Stream<Employee> stream=lists.stream()
                .filter(emp -> emp.getSalary()>6000)//筛选出工资大于6000的员工
                .distinct()//元素去重
                .limit(2)//使元素不超过2个
                .skip(2);//跳过2个元素

            stream.forEach(System.out::println);

            Stream<String> stream1=lists.stream()
                    .map(e -> e.getName())//将每一个Employee对象的姓名替换原来的Employee
                    .sorted()//自然排序
                    .distinct();
            stream1.forEach(System.out::println);


    }

Stream终止操作

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是void。

Collector 接口中方法的实现决定了如何对流执行收集操作(如收集到 List、Set、Map)。但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例。

@Test
    public void test2(){
        //返回流中元素总数
        long num=lists.stream()
                .count();
        System.out.println(num);

        //返回流中最大值
        Optional<Double> maxSalary=lists.stream()
                .map(Employee::getSalary)
                .max(Double::compareTo);

        System.out.println(maxSalary.get());

        //是否匹配所有元素
        boolean bool=lists.stream()
                .allMatch(e -> e.getName().equals("张三"));
        System.out.println(bool);

        //是否至少匹配一个元素
        boolean bool2=lists.stream()
                .anyMatch(e -> e.getName().equals("张三"));
        System.out.println(bool2);

        //检查是否没有匹配到所有元素
        boolean bool3=lists.stream()
                .noneMatch(e->e.getName().equals("张三"));
        System.out.println(bool3);

        //将Employee映射为Employee的名字,并且迭代输出
        lists.stream()
                .map(Employee::getName)
                .forEach(System.out::println);

        //利用归约,求出所有员工的工资之和
        Double salarySum=lists.stream()
                .map(Employee::getSalary)
                .reduce(0.0,(x,y)-> x+y);
        System.out.println(salarySum);
        //第二种,利用reduce规约,求出所有员工工资之和,Optional容器的出现,是为了解决空指针异常
        Optional<Double> salarySum2=lists.stream()
                .map(Employee::getSalary)
                .reduce(Double::sum);
        System.out.println(salarySum2.get());

        //收集的功能很强大,该函数需要一个Collector接口,用来实现对流执行收集操作,例如收集到List、Set、Map中去。
        //并且Collectors工具类提供了很多静态方法,能够方便的创建常见地方收集器,需要的话,自行查询API文档
        HashSet<Employee> set=lists.stream()
                .collect(Collectors.toCollection(HashSet::new));

        System.out.println(set);

    }

接口中的默认方法与静态方法

Java8中针对接口又增加了一个新的改进。允许接口中具有具体实现的方法,那么我们就称这个方法为“默认方法”,默认方法的关键字使用default关键字修饰。例如:

public interface MyInterface<T> {
    
    T fun(int a);
    
    default  String getName(){
        
        return "Hello Java8!";
    }
    
}

当然接口中默认的方法,有以下两点需要注意的地方,也就是需要遵循的原则,若一个接口中定义了默认方法,而另外一个父类或者接口中又定义了一个同名的方法时,需要注意以下两点:

    类优先原则:选择父类中方法,如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法就会被忽略。
public interface MyInterface {


    default  String getName(){

        return "Hello Java8!";
    }

}

public class MyClass {

    public String getName(){
        return "Hello,I am a Class";
    }
}

public class TestClass extends MyClass implements MyInterface{


}

@Test
    public void test3(){
        //当一个类继承一个父类,并且实现一个接口是,
        // 这个父类和这个接口中的默认方法,方法名和参数列表都相同,这样的话,这个类就会继承父类 
        //中的这个方法,而接口中的默认方法就会被忽略
        TestClass testClass=new TestClass();
        String name=testClass.getName();
        System.out.println(name);//输出结果 Hello,I am a Class

    }
    接口冲突:如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管是否是默认方法),那么必须覆盖该方法解决冲突。
public interface MyInterface1 {


    default  String getName(){

        return "I am a interface1";
    }

}

public interface MyInterface2 {

    default  String getName(){
        return "I am a interface2";
    }
}

public class TestClass implements MyInterface1,MyInterface2 {
    @Override//覆盖了MyInterface1中的方法
    public String getName() {
        return MyInterface1.super.getName();
    }
}

在Java8中,接口允许添加静态方法,这样我们使用接口中方法就可以直接使用接口名.静态方法名调用接口中的方法。

public interface MyInterface1 {


    default  String getName(){

        return "I am a interface1";
    }
    
    static void show(){
        System.out.println("balabalbal");
    }

}

新时间日期API

Java1.8对日期改动特别大,之前很多API都被废弃,并且在1.8中引入了许多新的API。这是因为之前的日期类型的API都是线程不安全的,还需要动手封装,才能获得线程安全的时间的工具包。Jdk1.8中主要新增了以下几个包:

java.time包:这是基础包,这里包含了我们常用的基础类,例如LocalDate、LocalTime、LocalDateTime。这些类都是不可变且线程安全的。 java.time.chrono包:这个包为非ISO的日历系统定义了一些API,我们可以借助这个包扩展我们自己的日历系统。 java.time.format包:这个包是用来格式化和解析时间对象的,一般,java.time包下的类就基本满足要求。 java.time.temporal包:封装了获取特定日期和时间的接口,例如,获取某月的第一天或最后一天。 java.time.zone包:这个包包含了与时区相关的类。

常用类的介绍

LocalDate

java.time.LocalDate这个类,是用来表示日期的,也仅包含日期。

@Test
    public void test1() {

        //获取当前日期(年月日)
        LocalDate now = LocalDate.now();
        System.out.println(now);

        //根据年月日构建日期
        LocalDate of = LocalDate.of(2018, 12, 26);
        System.out.println(of);
        //字符串转化日期,默认按照yyyy-MM-dd格式
        LocalDate parse = LocalDate.parse("2018-12-26");
        System.out.println(parse);
        //获取本月的第一天
        LocalDate firstDayOfMonth = now.with(TemporalAdjusters.firstDayOfMonth());
        System.out.println(firstDayOfMonth);
        //获取本月的最后一天
        LocalDate lastDayOfMonth = now.with(TemporalAdjusters.lastDayOfMonth());
        System.out.println(lastDayOfMonth);
        //获取本月第5天
        LocalDate dayOfMonth = now.withDayOfMonth(5);
        System.out.println(dayOfMonth);
        //明天
        LocalDate tommorrowDay = now.plusDays(1L);
        System.out.println(tommorrowDay);
        //昨天
        LocalDate yesterday = now.minusDays(1L);
        System.out.println(yesterday);
        //计算两个日期时间的天数
        long days = now.until(lastDayOfMonth, ChronoUnit.DAYS);
        System.out.println(days);

    }

LocalTime

与LocalDate相同,LocalTime与LocalDate都在相同的包下,表示的时间,不包含日期。

public void test2(){
        //获取当前时间
        LocalTime localTime = LocalTime.now();
        //构建时间
        LocalTime localTime1 = LocalTime.of(14, 24, 50);
        LocalTime localTime2 = LocalTime.parse("11:11:11");
        //分别获取时分秒
        localTime.getHour();
        localTime.getMinute();
        localTime.getSecond();
        //根据指定单位,计算到另一个时间的时间量
        long until = localTime.until(localTime1, ChronoUnit.HOURS);
        System.out.println(until);

    }

LocalDateTime

既包含日期,又包含时间,经常和DateTimeFormatter一起使用。

@Test
    public void test3(){
        //获取年月日,时分秒
        LocalDateTime localDateTime = LocalDateTime.now();
        //构建日期时间
        LocalDateTime.of(LocalDate.now(),LocalTime.now());
        //格式化当前时间
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        System.out.println(dateTimeFormatter.format(localDateTime));
    }
LocalDate,LocalTime,LocalDateTime这三个类基本上处理了大部分的日期,时间。而与这三个类经常结合使用的还有如下几个类:Year,Month,YearMonth,MonthDay,DayOfWeek。 并且,我们在LocalDate相关类的操作也可以通过Year,Month等来实现。

TemporalAdjusters

该类是一个计算类,提供了各种计算方法。例如获取某一天,某个月,第一天等等。该内部类基本上是通过JDK8的Lambda实现的

@Test
    public void test4(){
        LocalTime localTime = LocalTime.now();
        //TemporalAdjuster:时间校正器。
        //TemporalAdjusters:该类内部封装了大量的静态方法,TemporalAdjuster的实现。
        //获取本月第一天
        localTime.with(TemporalAdjusters.firstDayOfMonth());
        //获取本月最后一天
        localTime.with(TemporalAdjusters.lastDayOfMonth());
        //获取本年第一天
        localTime.with(TemporalAdjusters.firstDayOfYear());
        //本年最后一天
        localTime.with(TemporalAdjusters.lastDayOfYear());
        //下一个周末
        localTime.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
        
    }

Duration和Period

Period是基于ISO-8601标准的日期方式,用于计算两个日期间的年,月,日的差值。而Duration计算的是两个时间之间的差值,是一种更精确的计算方式。

@Test
    public void test5(){
        //Duration:计算两个“时间”之间的间隔
        //Period:计算两个“日期”之间的间隔
        LocalTime now1 = LocalTime.now();
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        LocalTime now2 = LocalTime.now();
        Duration duration = Duration.between(now1, now2);
        //两个时间之间间隔的毫秒数
        System.out.println(duration.toMillis());


        //计算两个日期之间的差值
        LocalDate now3 = LocalDate.now();

        LocalDate now4 = LocalDate.of(2017, 12, 11);

        Period period = Period.between(now4, now3);
        System.out.println(period.getYears());
        System.out.println(period.getMonths());
        System.out.println(period.getDays());


    }
DateTimeFormatter

该类主要用来对日期进行格式化的。

@Test
    public void test6(){

        //DateTimeFormatter提供了三种格式化的方式
        //预定义的标准格式
        //自定义模式
        //语言环境相关
        DateTimeFormatter format = DateTimeFormatter.ISO_DATE_TIME;  //预定义的标准格式
        LocalDateTime localDate = LocalDateTime.now();
        String s=localDate.format(format);
        System.out.println(s);

        DateTimeFormatter format1= DateTimeFormatter.ofPattern("yyyy年MM月dd日");//自定义模式
        String s2 = format1.format(LocalDateTime.now());
        System.out.println(s2);
    }

当然除了以上讲到的还有一些类,如Instant、ZoneId时区标识、ZoneOffset时区偏移量,与UTC的偏移量、ZoneDateTime与时区有关的日历系统、OffsetDateTime用于与UTC偏移量。

当然以上就是我对java1.8的学习很认识,有不足的话,还请大家多多提出。

经验分享 程序员 微信小程序 职场和发展