发布于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");
}
}
例如对一个用户列表进行排序,排序规则是他们的年龄(假设都存在),那么以往的代码是这么写的:
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表达式的前提是,接口必须为函数式接口。
函数式接口,就是只包含一个需要被实现的抽象方法的接口。
也就是说:接口中可以定义其他的方法,比如默认方法、抽象方法、甚至是equals
、toString
这些不需要被实现的方法(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)
,所以也是可行的。
方法引用的四个种类:
格式:类名::静态方法名
上面的例子Main:cmp
就是静态方法引用,和静态方法调用相比,就是把.
换成了::
例如:
Math::pow 等效于 (x, y) -> Math.pow(x, y)
String::valueOf 等效于 (x) -> String.valueOf(x)
格式:实例名::实例中的方法名
例如:
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) + " ");
}
}
格式: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);
}
}
格式:类名::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);
}
消费型接口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
}
供给型接口Supplier<T>
,无参数,有返回值。
例子:
Supplier<Double> supplier = Math::random;
System.out.println(supplier.get());
Supplier
接口中只有一个T get()
方法,所以没什么多说的,创建后直接用即可。
函数型接口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
用的比较少。
断言型接口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);
}
}
流式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(); // 最终计算结果
创建Stream
最简单的方式是直接用Stream.of()
静态方法:
Stream<String> stream = Stream.of("A", "B", "C", "D");
stream.forEach(System.out::println);
查看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);
使用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
}
对于基本类型,如int
、double
、long
。是不能作为Stream<T>
中的参数T
的,而使用对应的包装类型后,会频繁进行装箱拆箱,对效率有影响。
Java标准库提供了IntStream
、LongStream
和DoubleStream
这三种使用基本类型的Stream
,它们的使用方法和范型Stream
没有大的区别,设计这三个Stream
的目的是提高运行效率:
// 创建方式一
IntStream intStream = IntStream.of(1, 2, 3, 4);
// 创建方式二
int [] nums = {1, 2, 3};
IntStream intStream = Arrays.stream(nums);
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点。
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);
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);
});
}
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]}
对于一段流的操作代码,应该分为两个大块:转换操作和聚合操作。
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
。而是对每一个元素跑一遍转换操作和聚合操作(当然,不满足条件的元素就终止执行链了)。
从Java 8开始,java.time
包提供了新的日期和时间API,最常用的是本地日期和时间:LocalDateTime
,LocalDate
,LocalTime
。
和旧的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黑洞网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
---无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑,坏掉你本来就不多的热情和定力
Copyright © 2018-2021 java黑洞网 All Rights Reserved 版权所有,并保留所有权利。京ICP备18063182号-2
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!