程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

Java8新特性学习笔记

发布于2021-04-18 13:54     阅读(281)     评论(0)     点赞(1)     收藏(0)


一、接口默认方法和静态方法

接口中可以使用default关键字,来实现方法。

// NumInterface.java
public interface NumInterface
{
    // 定义一个抽象方法
	int toDoubleDouble(int number);

    // 默认方法
	default int toDouble(int number) {
		return number * 2;
	}
}

接口中实现的默认方法,它的实现类可以直接调用。

// Num.java
public class Num implements NumInterface
{
	@Override
	public int toDoubleDouble(int number) {
		return toDouble(number) * 2;
	}
}

// Main.java
public static void main(String[] args) {
    NumInterface num = new Num(){};
    System.out.println(num.toDouble(3)); // 输出6
}

当然,实现类也能重写接口中的默认方法,调用时依据”就近原则“,即实现类如果重写了,就先调用实现类的,否则调用最近接口的。


接口中还能实现静态方法:

// NumInterface.java
public interface NumInterface
{
    // 抽象方法
	int toDoubleDouble(int number);

    // 默认方法
	default int toDouble(int number) {
		return number * 2;
	}
	
    // 静态方法
	static void sayHello() {
		System.out.println("Hello");
	}
}

二、Lambda表达式

例如对一个用户列表进行排序,排序规则是他们的年龄(假设都存在),那么以往的代码是这么写的:

ArrayList<User> userList = new ArrayList<>();
userList.add(user1);
userList.add(user2);
...

userList.sort(new Comparator<User>()
{
	@Override
	public int compare(User o1, User o2) {
		return o1.getAge() - o2.getAge();
	}
});

上面的代码传入了一个匿名内部类,来实现Comparator接口中的compare方法。

现在就能简化这一个操作。

userList.sort((User a, User b) -> {
	return a.getAge() - b.getAge();
});

对于只包含一行方法的代码块,我们可以省略大括号和return

甚至能更加简化:

userList.sort((a, b) -> a.getAge() - b.getAge());

使用Lambda表达式的前提是,接口必须为函数式接口

三、函数式接口

函数式接口,就是只包含一个需要被实现的抽象方法的接口。

也就是说:接口中可以定义其他的方法,比如默认方法、抽象方法、甚至是equalstoString这些不需要被实现的方法(Object类中实现了),只需要保证只有一个抽象方法需要被实现就行。

为了保证一个接口明确的被定义为一个函数式接口,可以在接口上添加注解:@FunctionalInterface,如果接口中添加了其他的抽象方法,就会报错。

// NumInterface.java
@FunctionalInterface
public interface NumInterface
{
    // 定义一个抽象方法
	int toDoubleDouble(int number);

    // 默认方法
	default int toDouble(int number) {
		return number * 2;
	}
}

上面的代码,即使去掉 @FunctionalInterface 也是好使的,它仅仅是一种约束而已。

四、方法引用

String::compareToIgnoreCase 这种“对象/类名 + 双冒号 + 方法名”格式就是方法引用。

所谓方法引用,是指如果某个方法签名和接口的恰好一致,就可以直接传入方法引用。

例如这一段代码:

public static void main(String[] args) {
	String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
	Arrays.sort(array, Main::cmp);
	System.out.println(String.join(", ", array));
}

static int cmp(String s1, String s2) {
    return s1.compareTo(s2);
}

本来Arrays.sort()中的第二个参数是传入的int compare(String, String)这种类型的,而cmp方法的参数列表、返回类型和需求一致。因此,两者的方法签名一致,可以直接把方法名作为lambda表达式传入:

Arrays.sort(array, Main::cmp);

当然,里面也可传入String::compareTo,而compareTo方法中只有一个参数。这是因为方法参数中含有一个隐藏的this,每次调用compareTo时,实际上是调用public static int compareTo(this, String o),所以也是可行的。


方法引用的四个种类:

  1. 静态方法引用

格式:类名::静态方法名

上面的例子Main:cmp就是静态方法引用,和静态方法调用相比,就是把.换成了::

例如:

Math::pow 等效于 (x, y) -> Math.pow(x, y)

String::valueOf 等效于 (x) -> String.valueOf(x)

  1. 实例方法引用

格式:实例名::实例中的方法名

例如:

public static void main(String[] args) {
    List<String> list = Arrays.asList("hello", "kitty");
    list.forEach(new Main()::printWithSpace);
}

private void printWithSpace(String s) {
    for (int i = 0 ; i < s.length() ; i++) {
        System.out.print(s.charAt(i) + " ");
    }
}
  1. 实例的超类方法引用

格式:super::超类中的方法名

这里的super指的是当前类的父类。

例如:

public class Example extends BaseExample{
	@Test
	public void test() {
		List<String> list = Arrays.asList("aaaa", "bbbb", "cccc");
		//对象的超类方法语法: super::methodName 
		list.forEach(super::print);
	}
}

class BaseExample {
	public void print(String content) {
		System.out.println(content);
	}
}
  1. 构造方法引用

格式:类名::new

代表创建这个类的对象。String::new 等效于 () -> new String()

无参构造:

public class Main {
    public static void main(String[] args) {
        BirdFactory factory = Bird::new;
        Bird bird = factory.create();
    }
}

class Bird
{
}

interface BirdFactory {
    Bird create();
}

带参构造:

public class Main {
    public static void main(String[] args) {
        BirdFactory factory = Bird::new;
        Bird bird = factory.create("hello");
    }
}

class Bird
{
    private String name;

    Bird(String name) {
        this.name = name;
    }
}

interface BirdFactory {
    Bird create(String name);
}

五、常用的函数式接口

1. 消费型接口:Consumer

消费型接口Consumer<T>只接受一个参数,无返回值

例子:

Consumer consumer = System.out::println;
consumer.accept("hello function"); // 输出hello function

下面看一下Consumer接口的源码:

@FunctionalInterface
public interface Consumer<T> {
    // 唯一抽象方法
    void accept(T t);

    // 后置挂钩
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

里面只有一个void accept(T t)抽象方法,所以创建一个Consumer实例时,就是传入了和accept方法签名同样的方法。

andThen()方法能接受一个Consumer参数,指的是消费过后再消费。

例子:

public static void main(String[] args) {
    Consumer<String> consumer = System.out::print;
    Consumer<String> after = (str) -> {
        for (int i = 0 ; i < str.length() ; i++) {
            System.out.print(str.charAt(i) + " ");
        }
    };
    consumer.andThen(after).accept("hello");
    // 输出 hello h e l l o
}

2. 供给型接口:Supplier

供给型接口Supplier<T>无参数,有返回值

例子:

Supplier<Double> supplier = Math::random;
System.out.println(supplier.get());

Supplier接口中只有一个T get()方法,所以没什么多说的,创建后直接用即可。

3. 函数型接口:Function

函数型接口Function<T,R>接受参数类型为T的参数,返回参数类型为R的结果

例子:

Function<String, Integer> parseInt = Integer::parseInt;
System.out.println(parseInt.apply("123"));

查看源码如下:

@FunctionalInterface
public interface Function<T, R> {
    // 唯一抽象方法
    R apply(T t);

    // 前置方法
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    
    // 后置方法
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
    
    // 返回参数自己
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

compose是前置方法,andThen是后置方法,identity用的比较少。

4. 断言型接口:Predicate

断言型接口Predicate<T>接受一个参数,返回一个布尔类型结果

例子:

Predicate<Character> isDigit = Character::isDigit;
System.out.println(isDigit.test('a')); // false
System.out.println(isDigit.test('6')); // true

查看源码如下:

@FunctionalInterface
public interface Predicate<T> {
	// 唯一抽象接口
    boolean test(T t);

    // 相当于 &&
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    // 取反
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    // 相当与 ||
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    // 判断相等
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

六、Stream流

1. 简介

流式API:Stream API。它位于java.util.stream包中。

Stream与常见的InputStream/OutputStream不同,它代表的是任意Java对象的序列。

但它和List容器不同,List是存储已经存在于内存中的Java对象,而Stream存储的元素可能并没有预先存在内存中,而是实时计算出来的。

例如:

Stream<BigInteger> naturals = createNaturalStream(); // 获取全体自然数,先不管这个怎么实现的
naturals.map(n -> n.multiply(n)) // 1, 4, 9, 16, 25...
        .limit(100)
        .forEach(System.out::println); // 最后一步才计算

因此,Stream API的用法是,创建一个Stream,然后做若干次转换,最后调用一个求值方法获取结果。

int result = createNaturalStream() // 创建Stream
             .filter(n -> n % 2 == 0) // 任意个转换
             .map(n -> n * n) // 任意个转换
             .limit(100) // 任意个转换
             .sum(); // 最终计算结果

2. Stream创建方式

1)Stream.of()

创建Stream最简单的方式是直接用Stream.of()静态方法:

Stream<String> stream = Stream.of("A", "B", "C", "D");
stream.forEach(System.out::println);

2)基于数组或集合

查看Arrays源码,里面定义了多个重载stream()方法。

Collection接口中也有一个stream()方法。

所以对于数组类型或者集合类型,可以直接获取对应的Stream流。

// 集合直接调用stream()方法获取
List<String> list = Arrays.asList("Hello", "kitty");
Stream<String> stringStream = list.stream();
// 数组通过Arrays.stream()获取
Integer [] nums = {1, 2, 3};
Stream<Integer> integerStream = Arrays.stream(nums);

3)基于Supplier

使用Stream.generate()传入Supplier,获取对应的Stream流。

public static void main(String[] args) {
    Supplier<String> supplier = () -> "hello";
    Stream<String> stringStream = Stream.generate(supplier);
    stringStream.limit(10).forEach(System.out::println); // 输出10个hello
}

4)基本类型流创建

对于基本类型,如intdoublelong。是不能作为Stream<T>中的参数T的,而使用对应的包装类型后,会频繁进行装箱拆箱,对效率有影响。

Java标准库提供了IntStreamLongStreamDoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率:

// 创建方式一
IntStream intStream = IntStream.of(1, 2, 3, 4);
// 创建方式二
int [] nums = {1, 2, 3};
IntStream intStream = Arrays.stream(nums);

3. Stream常用方法

1)转换操作:map

Stream.map()Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream

例如把一个[1, 2, 3, 4, 5]的序列,按f(x) = x * x的方式映射到另一个序列上,就得到了[1, 4, 9, 16, 25]

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
stream.map((x) -> x * x).forEach(System.out::println);

查看map方法的源码:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

该方法是接受一个函数型接口(R Function<T, R>),所以要传入一个包含参数和返回值的lambda表达式。

函数型接口忘了可以看第五节的第3点。

2)转换操作:filter

filter是对stream中的所有元素进行"一一测试",不满足条件的元素就会被过滤掉,剩下的元素形成了新的stream

例如对于一串数,过滤掉其中的偶数(只剩下奇数):

Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
integerStream.filter(num -> (num & 1) == 1).forEach(System.out::println);

所以很容易猜到,filter的参数是一个断言式函数接口:

Stream<T> filter(Predicate<? super T> predicate);

3)聚合操作:reduce

Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。

Stream<Integer> integerStream = Stream.of(1, 3, 5, 2, 3);
Integer reduce = integerStream.reduce(0, (x, y) -> x + y); // 初始值为0,遍历所有元素并相加
System.out.println(reduce); // 14

乍一看,对于reduce的参数有点难以理解,下面看一下源码:

T reduce(T identity, BinaryOperator<T> accumulator);

左边是一个常规参数,右边要传入一个BinaryOperator接口,它定义了一个apply()方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果:

@FunctionalInterface
public interface BinaryOperator<T> {
    // Bi操作:两个输入,一个输出
    T apply(T t, T u);
}

所以上面的例子,实际上是做了如下操作:

// 计算过程:
acc = 0 // 初始化为指定值
acc = acc + n = 0 + 1 = 1 	// n = 1
acc = acc + n = 1 + 3 = 4 	// n = 3
acc = acc + n = 4 + 5 = 9 	// n = 5
acc = acc + n = 9 + 2 = 11 	// n = 2
acc = acc + n = 11 + 3 = 14 // n = 3

对于reduce方法,还有一个重载方法,不用接收初始值,最后返回一个Optional对象:

Optional<T> reduce(BinaryOperator<T> accumulator);

这是因为如果流中的元素是0个,这样就没法调用reduce()的聚合函数了,因此返回Optional对象,需要进一步判断结果是否存在。

注意:使用reduce来求积,初始值要设置为1。


例如要将按行读取到的配置文件,映射成map,可以使用map()reduce()完成:

public static void main(String[] args) {
    // 按行读取配置文件:
    List<String> props = Arrays.asList("profile=native", "debug=true", "logging=warn", "interval=500");
    Map<String, String> proMap = props.stream().map(str -> {
        Map<String, String> map = new HashMap<>();
        int index = str.indexOf('=');
        map.put(str.substring(0, index), str.substring(index + 1));
        return map;
    }).reduce(new HashMap<>(), (identity, incremental) -> {
        identity.putAll(incremental);
        return identity;
    });
    // 打印结果
    proMap.forEach((k, v) -> {
        System.out.println(k + " = " + v);
    });
}

4)聚合操作:collect

collect也是Stream的一个聚合方法,它的作用主要是将当前流输出到一个集合里面。

可以输出到List,也可以输出到Set、或者输出到Map

例如:

// 将流中的以a开头的字符串保存在List中
Stream<String> strs = Stream.of("abc", "bds", "xx", "axxy", "hello");
List<String> stringList = strs.filter(str -> str.startsWith("a"))
                              .collect(Collectors.toList());

保存到Map就要多传入几个参数:

Stream<String> props = Stream.of("profile=native", "debug=true", "logging=warn", "interval=500");
Map<String, String> map = props.collect(Collectors.toMap(str -> str.substring(0, str.indexOf('=')),
															 str -> str.substring(str.indexOf('=') + 1)));
System.out.println(map); // {debug=true, profile=native, logging=warn, interval=500}

查看toMap的定义:

toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)

需要在toMap()中传入key,和value。

如果key重复,就会抛出异常。

collect还有一个常用的功能:分组输出

Collector中的groupingBy方法,能对流中的元素进行分组。

// 接受两个参数,第一个是分组条件,第二个是存放容器
groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream)

如果想对一串字符串按首字母分组,并放入List中:

Stream<String> strs = Stream.of("abc", "bds", "xx", "axxy", "hello");
Map<String, List<String>> collect = strs.collect(
		Collectors.groupingBy(str -> str.substring(0, 1), Collectors.toList()));
System.out.println(collect); // {a=[abc, axxy], b=[bds], h=[hello], x=[xx]}

4. Stream执行顺序

对于一段流的操作代码,应该分为两个大块:转换操作聚合操作

List<String> myList =
    Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList
    .stream() 						// 创建流
    .filter(s -> s.startsWith("c")) // 转换操作
    .map(String::toUpperCase) 		// 转换操作
    .sorted() 						// 转换操作
    .forEach(System.out::println); 	// 聚合操作
  • 对于转换操作,通常是操作后继续返回一个流,所以我们可以链接多个转换操作。
  • 对于终端操作,通常是返回一个void或者非流。通常执行完了终端操作,就不能继续操作了。

流的处理顺序有一个重要的特性:延迟性

体现在:只有执行终端操作时,才会按顺序执行转换操作

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s)
        return s.startsWith("a"); // 过滤出以 a 为前缀的元素
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase(); // 转大写
    })
    .forEach(s -> System.out.println("forEach: " + s)); // for 循环输出

// filter:  d2
// filter:  a2
// map:     a2
// forEach: A2
// filter:  b1
// filter:  b3
// filter:  c

不是对所有的元素先执行filter、再执行map、最后forEach。而是对每一个元素跑一遍转换操作和聚合操作(当然,不满足条件的元素就终止执行链了)。

七、新的日期API

从Java 8开始,java.time包提供了新的日期和时间API,最常用的是本地日期和时间:LocalDateTimeLocalDateLocalTime

和旧的API相比,新API严格区分了时刻、本地日期、本地时间和带时区的日期时间,并且,对日期和时间进行运算更加方便。

LocalDate d = LocalDate.now(); // 当前日期
LocalTime t = LocalTime.now(); // 当前时间
LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间
System.out.println(d); // 严格按照ISO 8601格式打印
System.out.println(t); // 严格按照ISO 8601格式打印
System.out.println(dt); // 严格按照ISO 8601格式打印

时间格式类:DateTimeFormatter,可以进行自定义格式串来输出、或者创建时间对象。

// 自定义格式化:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
System.out.println(dtf.format(LocalDateTime.now()));

// 用自定义格式解析:
LocalDateTime dt2 = LocalDateTime.parse("2019/11/30 15:16:17", dtf);
System.out.println(dt2);

还有一些调整日期的API,这里不做过多介绍。

原文链接:https://blog.csdn.net/God_woodson/article/details/115758309



所属网站分类: 技术文章 > 博客

作者:Djfj

链接:http://www.javaheidong.com/blog/article/159845/a4c331e169c6f2a0e059/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

1 0
收藏该文
已收藏

评论内容:(最多支持255个字符)