JDK8
介绍
随着java的发展,越来越多的企业开始使用java8
版本。Java8是自从java5之后最重要的版本,这个版本包含语言、编译器、库、工具、JVM等方面的十多个新特性。
🐤JDK8新增的特性如下
- Lambda表达式
- 新的日期API、Datetime
- 引入Optional防止空指针异常
- 使用Base64
- 接口的默认方法和静态方法
- 新增方法引用格式
- 新增Stream类
- 注解相关的改变
- 支持并行(parallel)数组
- 对并发类(Concurrency)的扩展
- JavaFX
接口新特性
接口默认方法
当我们去实现某个框架提供的一个接口时,需要实现其所有的抽象方法,当该框架更新版本,在这个借口中加入了新的抽象方法时,我们就需要对项目重新编译,并且实现其新增的方法。当实现类太多时,操作起来很麻烦
JDK之前是使用开闭设计模式:对扩展开放,对修改关闭。即:创建一个新的接口,继承原有的接口,定义新的方法
但是这样的话,原本的那些实现类并没有新的方法
这时候可以使用接口默认方法
关键字使用default
进行修饰, 方法需要方法体。这样的方法所有的子类会默认实现(不用自己写),如果想要覆盖重写,也可以在实现类中覆盖重写
public interface IUserService {
void method1();
void method2();
}
public class IUserImpl implements IUserService {
@Override
public void method1() {
System.out.println("method1");
}
@Override
public void method2() {
System.out.println("method2");
}
}
public class IUser2Impl implements IUserService {
@Override
public void method1() {
System.out.println("method1");
}
@Override
public void method2() {
System.out.println("method2");
}
}
在上面的示例中一个接口有两个实现类,假如这时需要在接口当中新增一个功能,该接口的所有实现类都需要实现该接口,这样就很麻烦,这时就可以使用接口默认方法,改造后的接口为
public interface IUserService {
void method1();
void method2();
default void methodNew(){
System.out.println("新增的功能");
};
}
进行测试
public class MyTest {
@Test
public void test(){
IUserService user = new IUser2Impl();
user.methodNew();
}
}
这个时候假如
这里需要注意的是:这里的default是JDK8新增的关键字,和访问限定修饰符default
不是一个概念,与switch中的default功能完全不同
与抽象类的不同:抽象类更多的是提供一个模板,子类之间的某个流程大致相同,仅仅是某个步骤可能不一样(模板方法设计模式),这个时候使用抽象类,该步骤定义为抽象方法。而default关键字是用于扩展
接口静态默认方法
/**
* @author :tangyihao
* @version :V1.0
* @program :jdk8
* @date :Created in 2020/8/12 16:25
* @description :从Java8开始,接口当中允许定义静态方法
* 修饰符:static xxx
* 一般类的静态方法用法相同
*/
public interface IAnimal {
void method();
static void getUser() {
System.out.println("静态接口方法");
}
}
接口的静态方法不会被实现类所继承
测试
public class MyTest {
@Test
public void test(){
IAnimal.getUser();
}
}
函数式接口
概念
函数式接口在Java中是指:有且仅有一个抽象方法的接口
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导
格式
确保接口中有且只有一个抽象方法即可
public interface 接口名称 {
返回值 方法名称();
}
🐤@FunctionalInterface注解
有的注解是在编译期起作用,如@Override注解。而@FunctionalInterface也是在编译期起作用。该注解是Java8专门为函数式接口引入的新的注解,作用于一个接口上。一旦使用该注解来定义接口,编译期会强制检查该接口是否符合函数式接口的条件,不符合则会报错。需要注意的是:即使不使用该注解,只要满足函数式接口的定义,该接口就是一个函数式接口
使用注解
@FunctionalInterface
public interface MyFunctionInterface {
void method();
default void defaultMethod(){
System.out.println("接口默认方法");
}
}
不使用注解
public interface MyFunctionInterface {
void method();
default void defaultMethod(){
System.out.println("接口默认方法");
}
}
Lambda表达式
在面向对象的基础上,Java8通过Lambda表达式与方法引用等,为开发者打开了函数式编程的大门。Lambda表达式不是语法糖,而是新的语法
语法
三要素:参数、箭头、代码
(参数) -> {
代码
}
(参数类型 参数1, 参数类型 参数2, ....) -> {代码}
首先定义函数式接口
@FunctionalInterface
public interface MyFunctionInterface {
void method(String str);
default void defaultMethod() {
System.out.println("接口默认方法");
}
}
测试
public class MyTest {
@Test
public void test() {
getMethod((String str) -> {
System.out.println(str);
});
}
private void getMethod(MyFunctionInterface functionInterface) {
functionInterface.method("lamdba表达式");
}
}
- 如果参数有多个,那么使用逗号分隔。如果参数没有,则留空
@FunctionalInterface
public interface MyFunctionInterface {
void method();
default void defaultMethod() {
System.out.println("接口默认方法");
}
}
public class MyTest {
@Test
public void test() {
getMethod(() -> {
});
}
private void getMethod(MyFunctionInterface functionInterface) {
functionInterface.method();
}
}
- 箭头是固定写法
- 大括号相当于方法体
使用Lambda表达式的必要前提:必须是函数式接口
Lambda 省略规则
- 参数类型可以省略。但是只能同时省略所有参数的类型,或者干脆都不省略
@FunctionalInterface
public interface MyFunctionInterface {
void method(String str, Integer age);
default void defaultMethod() {
System.out.println("接口默认方法");
}
}
public class MyTest {
@Test
public void test() {
getMethod((str, age) -> {
System.out.println(str + age);
});
}
private void getMethod(MyFunctionInterface functionInterface) {
functionInterface.method("BNTang", 23);
}
}
- 如果参数有且仅有一个,那么小括号可以省略
@FunctionalInterface
public interface MyFunctionInterface {
void method(String str);
default void defaultMethod() {
System.out.println("接口默认方法");
}
}
public class MyTest {
@Test
public void test() {
getMethod(str -> {
System.out.println(str);
});
}
private void getMethod(MyFunctionInterface functionInterface) {
functionInterface.method("BNTang");
}
}
- 如果大括号内的语句有且仅有一条,那么无论是否有返回值,return、大括号、分号都可以省略
public class MyTest {
@Test
public void test() {
getMethod(str ->
System.out.println(str)
);
}
private void getMethod(MyFunctionInterface functionInterface) {
functionInterface.method("BNTang");
}
}
Lambda的延迟执行
有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能
🐤性能浪费
public class DemoLogger {
private static void log(int level, String msg) {
if (level == 1) {
System.out.println(msg);
}
}
public static void main(String[] args) {
String msgA = "Hello ";
String msgB = "World ";
String msgC = "Java";
log(1, msgA + msgB + msgC);
}
}
这段代码存在问题:无论级别是否满足要求,作为 log 方法的第二个参数,三个字符串一定会首先被拼接并传入方法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费
🐬Lambda表达式优化写法
@FunctionalInterface
public interface MessageBuilder {
String buildMessage();
}
public class DemoLogger {
private static void log(int level, MessageBuilder messageBuilder) {
if (level == 1) {
System.out.println(messageBuilder.buildMessage());
}
}
public static void main(String[] args) {
String msgA = "Hello ";
String msgB = "World ";
String msgC = "Java";
log(2, () -> msgA + msgB + msgC);
}
}
这样一来,只有当满足条件的时候才会进行三个字符串的拼接。否则不会拼接
🐤证明Lambda的延迟
public class DemoLogger {
private static void log(int level, MessageBuilder messageBuilder) {
if (level == 1) {
System.out.println(messageBuilder.buildMessage());
}
}
public static void main(String[] args) {
String msgA = "Hello ";
String msgB = "World ";
String msgC = "Java";
log(2, () -> {
return msgA + msgB + msgC;
});
}
}
从打印结果可以看出,在不符合条件要求的情况下,lambda将不会执行
使用Lambda作为参数和返回值
如果抛开实现原理不说,Java中的Lambda表达式可以被当作是匿名内部类的替代品。如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。使用Lambda表达式作为方法参数,其实就是使用函数式接口作为方法参数
例如:java.lang.Runnable接口就是一个函数式接口,假设有一个startThread方法使用该接口作为参数,那么就可以使用Lambda进行传参。这种情况其实和Thread类的构造方法参数为Runnable没有本质区别
public class DemoRunnable {
private static void startThread(Runnable task) {
new Thread(task).start();
}
public static void main(String[] args) {
startThread(() -> System.out.println("线程任务执行!"));
}
}
类似地,如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。当需要通过一个方法来获取一个java.util.Comparator接口类型的对象作为排序器时,就可以调该方法获取
public class DemoComparator {
private static Comparator<String> newComparator() {
return (a, b) -> b.length() - a.length();
}
public static void main(String[] args) {
String[] array = {"abc", "ab", "abcd"};
System.out.println(Arrays.toString(array));
Arrays.sort(array, newComparator());
System.out.println(Arrays.toString(array));
}
}
常用函数式接口
JDK提供了大量的函数式接口以及丰富的Lambda应用场景。下面是最简单的几个接口以及使用实例
Supplier
java.util.function.Supplier对外提供
一个符合泛型类型的对象数据
public class DemoSupplier {
private static String getString(Supplier<String> function) {
return function.get();
}
public static void main(String[] args) {
String msgA = "Hello";
String msgB = "World";
System.out.println(getString(() -> msgA + msgB));
}
}
🐤求数组元素的最大值
public class DemoTest {
public static int getMax(Supplier<Integer> sup) {
return sup.get();
}
public static void main(String[] args) {
int[] arr = {2, 3, 4, 52, 333, 23};
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
- 抽象方法:accept,为消费一个指定泛型的数据
public class DemoConsumer {
private static void consumeString(Consumer<String> function) {
function.accept("Hello");
}
public static void main(String[] args) {
consumeString(s -> System.out.println(s));
}
}
- 默认方法:andThen
如果一个方法的参数和返回值全都是Consumer类型,那么就可以实现效果:消费数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer接口中的default方法 andThen
要想实现组合,需要两个或多个Lambda表达式即可,而andThen的语义正是一步接一步
操作
public class DemoConsumerAndThen {
private static void consumeString(Consumer<String> one, Consumer<String> two) {
one.andThen(two).accept("Hello");
}
public static void main(String[] args) {
consumeString(s -> System.out.println(s.toUpperCase()), s -> System.out.println(s.toLowerCase()));
}
}
🐬格式化打印信息
- 下面的字符串数组当中存有多条信息,请按照格式“ 姓名:XX。性别:XX。 ”的格式将信息打印出来。要求将打印姓名的动作作为第一个Consumer接口的Lambda实例,将打印性别的动作作为第二个Consumer接口的Lambda实例,将两个 Consumer接口按照顺序“拼接”到一起
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
- 抽象方法:test
public class DemoPredicateTest {
private static void method(Predicate<String> predicate) {
boolean veryLong = predicate.test("HelloWorld");
System.out.println("字符串很长吗:" + veryLong);
}
public static void main(String[] args) {
method(s -> s.length() > 5);
}
}
条件判断的标准是传入lambda表达式逻辑
- 默认方法:and
既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个Predicate条件使用与
逻辑连接起来实现并且
的效果时,可以使用default方法and,如果要判断一个字符串既要包含大写“H”,又要包含大写“W”
public class DemoPredicateAnd {
private static void method(Predicate<String> one, Predicate<String> two) {
boolean isValid = one.and(two).test("Helloworld");
System.out.println("字符串符合要求吗:" + isValid);
}
public static void main(String[] args) {
method(s -> s.contains("H"), s -> s.contains("W"));
}
}
- 默认方法:or
如果希望实现逻辑“字符串包含大写H或者包含大写W”,那么代码只需要将“and”修改为“or”名称即可,其他都不变
public class DemoPredicateAnd {
private static void method(Predicate<String> one, Predicate<String> two) {
boolean isValid = one.or(two).test("Helloworld");
System.out.println("字符串符合要求吗:" + isValid);
}
public static void main(String[] args) {
method(s -> s.contains("H"), s -> s.contains("W"));
}
}
- 默认方法:negate
表示取反
public class DemoPredicateNegate {
private static void method(Predicate<String> predicate) {
boolean veryLong = predicate.negate().test("HelloWorld");
System.out.println("字符串很长吗:" + veryLong);
}
public static void main(String[] args) {
method(s -> s.length() < 5);
}
}
🐤集合信息筛选
- 数组当中有多条“姓名+性别”的信息如下,请通过Predicate接口的拼接将符合要求的字符串筛选到集合ArrayList 中,需要同时满足两个条件
- 必须为女生
- 姓名为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<T,R> 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件
- 抽象方法:apply
Function 接口中最主要的抽象方法为: R apply(T t) ,根据类型T的参数获取类型R的结果。使用的场景例如:将String类型转换为Integer类型
public class DemoFunctionApply {
private static void method(Function<String, Integer> function) {
int num = function.apply("10");
System.out.println(num + 20);
}
public static void main(String[] args) {
method(s -> Integer.parseInt(s));
}
}
- 默认方法:andThen
public class DemoFunctionAndThen {
private static void method(Function<String, Integer> one, Function<Integer, Integer> two) {
int num = one.andThen(two).apply("10");
System.out.println(num + 20);
}
public static void main(String[] args) {
method(str -> Integer.parseInt(str) + 10, i -> i *= 10);
}
}
🐕自定义函数模型拼接
请使用Function进行函数模型的拼接,按照顺序需要执行的多个函数操作为
String str = "赵丽颖,20";
- 将字符串截取数字年龄部分,得到字符串
- 将上一步的字符串转换成为int类型的数字
- 将上一步的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中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑
先看一个简单的函数式接口
@FunctionalInterface
public interface Printable {
void print(String str);
}
public class DemoPrintSimple {
private static void printString(Printable data) {
data.print("Hello, World!");
}
public static void main(String[] args) {
printString(s -> System.out.println(s));
}
}
其中printString方法只管调用Printable接口的print方法,而并不管print方法的具体实现逻辑会将字符串打印到什么地方去。而 main方法通过Lambda表达式指定了函数式接口Printable的具体操作方案为:拿到String(类型可推导,所以可省略)数据后,在控制台中输出它
问题分析
这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现,那就是System.out对象中的 println(String)方法。既然Lambda希望做的事情就是调用println(String) 方法,那何必自己手动调用呢
能否省去Lambda的语法格式(尽管它已经相当简洁)呢?只要“引用”过去就好了
public class DemoPrintRef {
private static void printString(Printable data) {
data.print("Hello, World!");
}
public static void main(String[] args) {
printString(System.out::println);
}
}
请注意其中的双冒号::
写法,这被称为“方法引用”,而双冒号是一种新的语法
方法引用符
双冒号::
为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者
🐤语义分析
例如上例中, System.out对象中有一个重载的println(String)方法恰好就是我们所需要的。那么对printString方法的函数式接口参数,对比下面两种写法,完全等效
- Lambda表达式写法: s -> System.out.println(s);
- 方法引用写法: System.out::println
第一种语义是指:拿到参数之后经Lambda之手,继而传递给System.out.println方法去处理
第二种等效写法的语义是指:直接让 System.out 中的 println 方法来取代Lambda。两种写法的执行效果完全一样,而第二种方法引用的写法复用了已有方案,更加简洁。
注:Lambda中传递的参数一定是方法引用中的那个方法可以接收的类型,否则会抛出异常
🐬推导与省略
如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导
函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟
下面这段代码将会调用 println 方法的不同重载形式,将函数式接口改为int类型的参数
@FunctionalInterface
public interface PrintableInteger {
void print(int str);
}
由于上下文变了之后可以自动推导出唯一对应的匹配重载,所以方法引用没有任何变化
public class DemoPrintOverload {
private static void printInteger(PrintableInteger data) {
data.print(1024);
}
public static void main(String[] args) {
printInteger(System.out::println);
}
}
通过类名称引用静态方法
由于在java.lang.Math类中已经存在了静态方法 abs ,所以当我们需要通过Lambda来调用该方法时,有两种写法。首先是函数式接口
@FunctionalInterface
public interface Calcable {
int calc(int num);
}
第一种写法使用Lambda
public class DemoLambda {
private static void method(int num, Calcable lambda) {
System.out.println(lambda.calc(num));
}
public static void main(String[] args) {
method(-10, n -> Math.abs(n));
}
}
第二种使用方法引用
public class DemoMethodRef {
private static void method(int num, Calcable lambda) {
System.out.println(lambda.calc(num));
}
public static void main(String[] args) {
method(-10, Math::abs);
}
}
两种方式等价
通过super引用成员方法
如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代。首先是函数式接口
@FunctionalInterface
public interface Greetable {
void greet();
}
父类Human的内容
public class Human {
public void sayHello() {
System.out.println("Hello!");
}
}
子类Man的内容
public class Man extends Human {
@Override
public void sayHello() {
System.out.println("大家好,我是Man!");
}
/**
* 定义方法method,参数传递Greetable接口
* @param g
*/
public void method(Greetable g) {
g.greet();
}
public void show() {
// 调用method方法,使用Lambda表达式
method(() -> {
// 创建Human对象,调用sayHello方法
new Human().sayHello();
});
// 简化Lambda
method(() -> new Human().sayHello());
// 使用super关键字代替父类对象
method(() -> super.sayHello());
}
}
但是如果使用方法引用会更好
public class Woman extends Human {
@Override
public void sayHello() {
System.out.println("大家好,我是Man!");
}
public void method(Greetable g) {
g.greet();
}
public void show() {
method(super::sayHello);
}
}
通过this引用成员方法
this代表当前对象,如果需要引用的方法就是当前类中的成员方法,那么可以使用“this::成员方法”的格式来使用方法引用。首先是简单的函数式接口
@FunctionalInterface
public interface Richable {
void buy();
}
public class Husband {
private void marry(Richable lambda) {
lambda.buy();
}
public void beHappy() {
marry(() -> System.out.println("买套房子"));
}
}
开心方法 beHappy 调用了结婚方法 marry ,后者的参数为函数式接口 Richable ,所以需要一个Lambda表达式。但是如果这个Lambda表达式的内容已经在本类当中存在了,则可以对 Husband 丈夫类进行修改
public class Husband {
private void buyHouse() {
System.out.println("买套房子");
}
private void marry(Richable lambda) {
lambda.buy();
}
public void beHappy() {
marry(this::buyHouse);
}
}
通过对象名引用成员方法
这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法
public class User {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
那么当需要使用这个 setName 成员方法来替代 Function 接口的Lambda的时候,已经具有了User 类的对象实例,则可以通过对象名引用成员方法,代码为
@Test
public void testGetUserName() {
String userName = testGetUserName(User::getName);
System.out.println(userName);
}
private String testGetUserName(Function<User, String> function) {
User user = new User();
user.setName("BNTang");
return function.apply(user);
}
Stream流
说到Stream便容易想到I/O Stream,而实际上,在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端
Stream流式操作性能比传统的For循环要低,就性能而言,传统的for循环最高
传统集合的遍历代码
几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如
public class DemoForEach {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("BNTang");
list.add("赵六");
list.add("王五");
list.add("李四");
list.add("张三");
for (String name : list) {
System.out.println(name);
}
}
}
循环遍历的弊端
Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行了对比说明。现在,我们仔细体会一下上例代码,可以发现
- for循环的语法就是“怎么做”
- for循环的循环体才是“做什么”
为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。
思想一下,如果希望对集合中的元素进行筛选过滤
- 将集合A根据条件,过滤为子集B
- 然后再根据条件二过滤为子集C
那怎么办?在Java 8之前的做法可能为
public class DemoNormalFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("BNTang");
list.add("赵六");
list.add("王五");
list.add("李四");
list.add("张三维");
List<String> zhangList = new ArrayList<>();
for (String name : list) {
if (name.startsWith("张")) {
zhangList.add(name);
}
}
List<String> shortList = new ArrayList<>();
for (String name : zhangList) {
if (name.length() == 3) {
shortList.add(name);
}
}
for (String name : shortList) {
System.out.println(name);
}
}
}
这段代码中含有三个循环,每一个作用不同
- 首先筛选所有姓张的人
- 然后筛选名字有三个字的人
- 最后进行对结果进行打印输出
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始,那,Lambda的衍生物Stream
能给我们带来怎样更加优雅的写法呢
Stream更优写法
public class DemoStreamFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("BNTang");
list.add("赵六");
list.add("王五");
list.add("李四");
list.add("张三维");
list.stream().filter(s -> s.startsWith("张"))
.filter(s -> s.length() == 3)
.forEach(System.out::println);
}
}
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中
获取流
java.util.stream.Stream
获取一个流非常简单,有以下几种常用的方式
- 所有的 Collection 集合都可以通过 stream 默认方法获取流
- Stream 接口的静态方法 of 可以获取数组对应的流
根据Collection获取流
首先, java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流
public class DemoGetStream {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
Stream<String> stream2 = set.stream();
}
}
根据Map获取流
java.util.Map 接口不是 Collection 的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分key、value或entry等情况
public class DemoGetStream {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
}
}
根据数组获取流
如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单
public class DemoGetStream {
public static void main(String[] args) {
String[] array = {"骚男", "蛇哥", "张三", "BNTang"};
Stream<String> stream = Stream.of(array);
}
}
常用方法
逐一处理forEach
虽然方法名字叫forEach,但是与for循环不同
public class DemoStreamForEach {
public static void main(String[] args) {
Stream<String> stream = Stream.of("BNTang", "JonathanLee", "张三");
stream.forEach(System.out::println);
}
}
过滤filter
可以通过 filter 方法将一个流转换成另一个子集流
public class DemoStreamFilter {
public static void main(String[] args) {
Stream<String> original = Stream.of("BNTang", "JonathanLee", "张三");
Stream<String> result = original.filter(s -> s.startsWith("张"));
}
}
在这里通过Lambda表达式来指定了筛选的条件:必须姓张
映射map
如果需要将流中的元素映射到另一个流中,可以使用 map 方法。方法签名
public class DemoStreamMap {
public static void main(String[] args) {
Stream<String> original = Stream.of("10", "12", "18");
Stream<Integer> result = original.map(Integer::parseInt);
}
}
这段代码中, map 方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为 Integer 类对象)
统计个数count
public class DemoStreamCount {
public static void main(String[] args) {
Stream<String> original = Stream.of("BNTang", "JonathanLee", "张三");
Stream<String> result = original.filter(s -> s.startsWith("张"));
System.out.println(result.count());
}
}
取用前几个limit
public class DemoStreamLimit {
public static void main(String[] args) {
Stream<String> original = Stream.of("BNTang", "JonathanLee", "张三");
Stream<String> result = original.limit(2);
result.forEach(System.out::println);
}
}
跳过前几个skip
public class DemoStreamSkip {
public static void main(String[] args) {
Stream<String> original = Stream.of("BNTang", "JonathanLee", "张三");
Stream<String> result = original.skip(2);
result.forEach(System.out::println);
}
}
组合concat
如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat
public class DemoStreamConcat {
public static void main(String[] args) {
Stream<String> streamA = Stream.of("BNTang");
Stream<String> streamB = Stream.of("张三");
Stream<String> result = Stream.concat(streamA, streamB);
result.forEach(System.out::println);
}
}
现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下若干操作步骤
- 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中
- 第一个队伍筛选之后只要前3个人;存储到一个新集合中
- 第二个队伍只要姓张的成员姓名;存储到一个新集合中
- 第二个队伍筛选之后不要前2个人;存储到一个新集合中
- 将两个队伍合并为一个队伍;存储到一个新集合中
- 根据姓名创建 User 对象;存储到一个新集合中
- 打印整个队伍的User对象信息
🐤集合元素处理(传统方式)
public class User {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User(String name) {
this.name = name;
}
public User() {}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public class DemoArrayListNames {
public static void main(String[] args) {
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个字的成员姓名;
List<String> oneA = new ArrayList<>();
for (String name : one) {
if (name.length() == 3) {
oneA.add(name);
}
}
// 第一个队伍筛选之后只要前3个人;
List<String> oneB = new ArrayList<>();
for (int i = 0; i < 3; i++) {
oneB.add(oneA.get(i));
}
// 第二个队伍只要姓张的成员姓名;
List<String> twoA = new ArrayList<>();
for (String name : two) {
if (name.startsWith("张")) {
twoA.add(name);
}
}
// 第二个队伍筛选之后不要前2个人;
List<String> twoB = new ArrayList<>();
for (int i = 2; i < twoA.size(); i++) {
twoB.add(twoA.get(i));
}
// 将两个队伍合并为一个队伍;
List<String> totalNames = new ArrayList<>();
totalNames.addAll(oneB);
totalNames.addAll(twoB);
// 根据姓名创建User对象;
List<User> totalUserList = new ArrayList<>();
for (String name : totalNames) {
User user = new User();
user.setName(name);
totalUserList.add(user);
}
}
}
🐬集合元素处理(Stream方式)
public class DemoArrayListNames {
public static void main(String[] args) {
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);
// 将两个队伍合并为一个队伍;
// 根据姓名创建User对象;
// 打印整个队伍的User对象信息。
Stream.concat(streamOne, streamTwo).map(User::new).forEach(System.out::println);
}
}