Java8新特性学习笔记
一、接口和日期处理
1、接口增强
1.1、JDK8以前 VS JDK8
1)接口定义:
- jdk8以前
interface 接口名{ 静态常量; 抽象方法; }
- jdk8
//JDK8之后对接口做了增加,接口中可以有默认方法和静态方法 interface 接口名{ 静态常量; 抽象方法; 默认方法; 静态方法; }
1.2、默认方法(default)
1)默认方法格式
interface 接口名{ default 返回值类型 方法名(参数类型 参数名...){ 方法体; } }
2)默认方法的两种使用方式
- 实现类直接调用接口的默认方法
- 实现类重写接口的默认方法
3)为什么要增加默认方法
在JDK8以前接口中只能有抽象方法和静态常量,会存在以下的问题: - 如果接口中新增抽象方法,那么实现类都必须要抽象这个抽象方法,非常不利于接口的扩展(例如Map接口)。
1.3、静态方法(static)
1)静态方法格式
interface 接口名{ static 返回值类型 方法名(参数类型 参数名...){ 方法体; } }
2)静态方法的使用
- 接口中的静态方法在实现类中是不能被重写的,调用的话只能通过接口类型来实现: 接口名.静态方法名();
1.4、默认和静态的区别
- 默认方法通过实例调用,静态方法通过接口名调用。
- 默认方法可以被继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法。
- 静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用。
1.5、最佳实践
/** * 场景1、类似使用模版方法设计模式:定义一个default方法,设计其他抽象方法的执行流程。缺点:抽象方法不能私有。 * 场景2、接口定义一系列default空方法,在实现类需要的时候再重写相关方法,不需要全部实现。 * 场景3、接口定义的default方法类似钩子函数(before、after),返回默认值或者啥也不干。绝大部分实现类不需要重写即可满足使用。 */
2、日期处理类
2.1、旧版日期时间的问题
1)设计不合理
- java.util.Date构造方法是从1900年开始,月份从0开始,且很多方法是弃用的,已经标记为@Deprecated。
java.util.Date 和 java.util.Calendar 类易用性差,不支持时区,有些地方隐含的使用了系统默认时区进行转化(例如toString()方法)。
Date如果不格式化,打印出的日期可读性差。Wed Mar 23 18:45:56 CST 2022
2)非线程安全
java.util.Date是非线程安全的,所有的日期类都是可变的。
使用 SimpleDateFormat 对Date进行格式化(format)和解析(parse)是线程不安全的。
format:格式化的时候,内部使用的 calendar 是共享变量,并且这个共享变量没有做线程安全控制。当多个线程同时使用相同的 SimpleDateFormat 对象调用format方法时,多个线程会同时调用 calendar.setTime 方法。可能一个线程刚设置好 time 值另外的一个线程马上把设置的 time 值给修改了。
parse:解析的时候,parse 方法实际调用 CalendarBuilder.establish(calendar).getTime() 方法来解析,CalendarBuilder.establish(calendar) 方法里主要完成了以下步骤:(这三步不是原子操作,多线程环境下导致解析出来的时间可能是错误的或者抛出异常)
- 重置日期对象calendar的属性值
- 使用CalendarBuilder中的属性设置calendar
- 返回设置好的calendar对象
多线程并发如何保证SimpleDateFormat线程安全?
避免线程之间共享一个 SimpleDateFormat 对象。
每个线程使用时都创建一次 SimpleDateFormat 对象 。(创建和销毁对象的开销大)
对使用 format 和 parse 方法的地方进行加锁。(线程阻塞性能差)
使用 ThreadLocal,每个线程都拥有自己的SimpleDateFormat对象副本。
@Test public void test00() { ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 10, 0, TimeUnit.MINUTES, new LinkedBlockingQueue<>(10)); int i = 10; // 1、使用全局共享SimpleDateFormat // SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //3、使用ThreadLocal final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; while (i-- > 0) { poolExecutor.execute(new Runnable() { @Override public void run() { // 2、使用局部SimpleDateFormat // SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //3、使用ThreadLocal SimpleDateFormat simpleDateFormat = THREAD_LOCAL.get(); String dateString = simpleDateFormat.format(new Date()); try { Date parseDate = simpleDateFormat.parse(dateString); String dateString2 = simpleDateFormat.format(parseDate); System.out.println(dateString.equals(dateString2)); } catch (Exception e) { e.printStackTrace(); } } }); } }
2.2、新版日期时间API介绍
1)简单介绍:
- JDK 8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于 java.time 包中。
- 关键类:
- LocalDate :表示日期,包含年月日,格式为 2022-03-23
- LocalTime :表示时间,包含时分秒,格式为 18:52:51.564
- LocalDateTime :表示日期时间,包含年月日,时分秒,格式为 2022-03-23T18:52:51.564
- DateTimeFormatter :日期时间格式化类。
- Instant:时间戳,表示自1970-01-01T00:00:00以来的某个时间,类似Date(只精确到毫秒,1个long类型字段)。Instant可以精确到纳秒(两个long类型字段)。
- Duration:用来度量秒和纳秒之间的时间值。可以用于计算2个日期时间(LocalTime、LocalDateTime)的间隔。
- Period:用来度量年月日和几天之间的时间值。可以用于计算2个日期(LocalDate,年月日)的间隔。
- ZonedDateTime :包含时区的时间。
2)日期时间的常见操作
- LocalDate
/** * 日期操作 */ @Test public void test01() { // 1.创建指定的日期 LocalDate date1 = LocalDate.of(2021, 05, 06); System.out.println("date1 = " + date1);//date1 = 2021-05-06 // 2.得到当前的日期 LocalDate now = LocalDate.now(); System.out.println("now = " + now);//now = 2022-03-23 // 3.根据LocalDate对象获取对应的日期信息 System.out.println("年:" + now.getYear());//年:2022 System.out.println("月:" + now.getMonth().getValue());//月:3 System.out.println("日:" + now.getDayOfMonth());//日:23 System.out.println("星期:" + now.getDayOfWeek().getValue());//星期:3 }
- LocalTime
/** * 时间操作 */ @Test public void test02() { // 1.得到指定的时间 LocalTime time = LocalTime.of(5, 26, 33, 100); System.out.println(time);//05:26:33.000000100 // 2.获取当前的时间 LocalTime now = LocalTime.now(); System.out.println(now);//19:15:48.547 // 3.获取时间信息 System.out.println(now.getHour());//19 System.out.println(now.getMinute());//15 System.out.println(now.getSecond());//48 System.out.println(now.getNano());//547000000 }
- LocalDateTime
/** * 日期时间操作 */ @Test public void test03() { // 获取指定的日期时间 LocalDateTime dateTime = LocalDateTime.of(2020 , 06 , 01 , 12 , 12 , 33 , 213); System.out.println(dateTime);//2020-06-01T12:12:33.000000213 // 获取当前的日期时间 LocalDateTime now = LocalDateTime.now(); System.out.println(now);//2022-03-23T19:18:22.029 // 获取日期时间信息 System.out.println(now.getYear());//2022 System.out.println(now.getMonth().getValue());//3 System.out.println(now.getDayOfMonth());//23 System.out.println(now.getDayOfWeek().getValue());//3 System.out.println(now.getHour());//19 System.out.println(now.getMinute());//18 System.out.println(now.getSecond());//22 System.out.println(now.getNano());//29000000 }
3)日期时间的修改和比较
- 日期时间的修改
/** * 日期时间的修改 */ @Test public void test04(){ LocalDateTime now = LocalDateTime.now(); // 在进行日期时间修改的时候,原来的LocalDat对象是不会被修改,每次操作都是返回了一个新的对象,所以在多线程场景下是数据安全的。 LocalDateTime localDateTime = now.withYear(1998); System.out.println("now :"+now);//now :2022-03-23T19:21:05.334 System.out.println("修改后的:" + localDateTime);//修改后的:1998-03-23T19:21:05.334 System.out.println("月份:" + now.withMonth(10));//月份:2022-10-23T19:21:05.334 System.out.println("天:" + now.withDayOfMonth(6));//天:2022-03-06T19:21:05.334 System.out.println("小时:" + now.withHour(8));//小时:2022-03-23T08:21:05.334 System.out.println("分钟:" + now.withMinute(15));//分钟:2022-03-23T19:15:05.334 // 在当前日期时间的基础上 加上或者减去指定的时间 System.out.println("两天后:" + now.plusDays(2));//两天后:2022-03-25T19:21:05.334 System.out.println("10年后:"+now.plusYears(10));//10年后:2032-03-23T19:21:05.334 System.out.println("6个月后:" + now.plusMonths(6));//6个月后:2022-09-23T19:21:05.334 System.out.println("10年前:" + now.minusYears(10));//10年前:2012-03-23T19:21:05.334 System.out.println("半年前:" + now.minusMonths(6));//半年前:2021-09-23T19:21:05.334 System.out.println("一周前:" + now.minusDays(7));//一周前:2022-03-16T19:21:05.334 }
- 日期时间的比较
/** * 日期时间的比较 */ @Test public void test05() { LocalDate now = LocalDate.now();//2022-03-23 LocalDate date = LocalDate.of(2020, 1, 3); // 在JDK8中要实现 日期的比较 isAfter isBefore isEqual 通过这几个方法来直接比较 System.out.println(now.isAfter(date)); // true System.out.println(now.isBefore(date)); // false System.out.println(now.isEqual(date)); // false }
4)格式化和解析操作
- 在JDK8中可以通过 java.time.format.DateTimeFormatter 类可以进行日期的解析和格式化操作,类不可变且线程安全。
/** * 日期格式化和解析操作 */ @Test public void test06() { LocalDateTime now = LocalDateTime.now(); // 指定格式 使用系统默认的格式 DateTimeFormatter isoLocalDateTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME; // 将日期时间转换为字符串 String format = now.format(isoLocalDateTime); System.out.println("format = " + format);//format = 2022-03-23T19:30:34.372 // 通过 ofPattern 方法来指定特定的格式 DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String format1 = now.format(dateTimeFormatter); System.out.println("format1 = " + format1);//format1 = 2022-03-23 19:30:34 // 将字符串解析为一个 日期时间类型 LocalDateTime parse = LocalDateTime.parse("1997-05-06 22:45:16", dateTimeFormatter); System.out.println("parse = " + parse);//parse = 1997-05-06T22:45:16 }
5)Instant类
- JDK8新增一个Instant类(时间戳),内部保存了从1970年1月1日 00:00:00以来的秒和纳秒。
/** * Instant 时间戳 从1970年1月1日 00:00:00开始,默认是UTC时区。 */ @Test public void test07() throws Exception { Instant now = Instant.now(); System.out.println(now);//2022-03-24T06:22:59.301Z Thread.sleep(500); Instant now1 = Instant.now(); //Instant.toEpochMilli()和Date.getTime()一样,同一时间得到的结果和System.currentTimeMillis()一样。 System.out.println("now时间戳:" + now.toEpochMilli());//now时间戳:1648102979301 System.out.println("now1时间戳:" + now1.toEpochMilli());//now1时间戳:1648102979863 System.out.println("耗时(毫秒):" + (now1.toEpochMilli() - now.toEpochMilli()));//耗时(毫秒):562 }
6)计算日期时间差
- JDK8中提供了两个工具类 Duration/Period 计算日期时间差。
/** * 计算日期时间差 */ @Test public void test08(){ LocalTime now = LocalTime.now(); LocalTime time = LocalTime.of(22, 48, 59); System.out.println("now = " + now);//now = 15:06:40.742 // 通过Duration来计算时间差(每个结果都是两个区间的真实间隔) Duration duration = Duration.between(now, time); System.out.println(duration.toDays()); // 0 System.out.println(duration.toHours()); // 7 System.out.println(duration.toMinutes()); // 462 System.out.println(duration.toMillis()); // 27738258 // 通过Period计算日期差(每个结果只是区间同一属性字段的间隔) LocalDate nowDate = LocalDate.now(); LocalDate date = LocalDate.of(1997, 12, 5); Period period = Period.between(date, nowDate); System.out.println(period.getYears()); // 24 (1997->2022) System.out.println(period.getMonths()); // 3 (12->3) System.out.println(period.getDays()); // 19 (5->24) }
7)时间校正器
TemporalAdjuster:时间校正器
TemporalAdjusters:通过该类静态方法提供了大量的常用TemporalAdjuster的实现。
/** * 时间校正器 */ @Test public void test09(){ LocalDateTime now = LocalDateTime.now(); // 将当前的日期调整到下个月的一号 TemporalAdjuster adJuster = new TemporalAdjuster() { @Override public Temporal adjustInto(Temporal temporal) { LocalDateTime dateTime = (LocalDateTime) temporal; LocalDateTime nextMonth = dateTime.plusMonths(1).withDayOfMonth(1); System.out.println("nextMonth = " + nextMonth);//nextMonth = 2022-04-01T15:41:28.243 return nextMonth; } }; // 通过TemporalAdjusters 来实现 // LocalDateTime nextMonth = now.with(adJuster); LocalDateTime nextMonth = now.with(TemporalAdjusters.firstDayOfNextMonth()); System.out.println("nextMonth = " + nextMonth);//nextMonth = 2022-04-01T15:41:28.243 }
8)日期时间的时区
ZonedDateTime:Java8中加入了对时区的支持。
ZoneId:该类中包含了所有的时区信息。每个时区都对应着 ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等。
/** * 时区操作,ZonedDateTime可以和LocalDateTime相互转换。 */ @Test public void test10() { // 获取当前时间 中国使用的 东八区的时区,比标准时间早8个小时 LocalDateTime now = LocalDateTime.now(); System.out.println("now = " + now); //now = 2022-03-24T15:36:22.102 // 获取标准时间 ZonedDateTime bz = ZonedDateTime.now(Clock.systemUTC()); System.out.println("bz = " + bz); //bz = 2022-03-24T07:36:22.103Z // 使用计算机默认的时区,创建日期时间 ZonedDateTime now1 = ZonedDateTime.now(); System.out.println("now1 = " + now1); //now1 = 2022-03-24T15:36:22.104+08:00[Asia/Shanghai] // 使用指定的时区创建日期时间 ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Marigot")); System.out.println("now2 = " + now2);//now2 = 2022-03-24T03:36:22.104-04:00[America/Marigot] }
2.3、总结
- 新版日期时间API中,日期和时间对象是不可变,操作日期不会影响原来的值,而是生成一个新的实例。
- 新版日期的修改以及计算日期时间差比旧版要方便得多,日期时间格式化和解析是线程安全的。
二、Lambda表达式
1、Lambda初体验
- 使用匿名内部类
@Test public void test01() { // 开启一个新的线程 new Thread(new Runnable() { @Override public void run() { System.out.println("新线程中执行的代码 : " + Thread.currentThread().getName()); } }).start(); }
- 使用Lambda表达式
@Test public void test02() { // 开启一个新的线程 new Thread(() -> { System.out.println("新线程Lambda表达式..." + Thread.currentThread().getName()); }).start(); }
2、语法规则
- Lambda省去了面向对象的条条框框,标准格式由3个部分组成:
([参数类型 参数名称]) -> { 代码体; }
格式说明
- ([参数类型 参数名称]):参数列表,可以没有参数。
- {代码体;} :方法体。
- -> : 箭头,分割参数列表和方法体。
3、省略写法
在lambda表达式的标准写法基础上,可以使用省略写法的规则为:
小括号内的参数类型可以省略。
如果小括号内有且仅有一个参数,则小括号可以省略。
如果大括号内有且仅有一个语句,可以同时省略大括号,return 关键字及语句分号(需要一起省略)。
@Test public void test02() { // 开启一个新的线程 new Thread(() -> System.out.println("新线程Lambda表达式..." + Thread.currentThread().getName())).start(); }
4、实现原理
public class Test { public static void main(String[] args) { // 开启一个新的线程 new Thread(new Runnable() { @Override public void run() { System.out.println("新线程中执行的代码 : " + Thread.currentThread().getName()); } }).start(); System.out.println("主线程中的代码:" + Thread.currentThread().getName()); System.out.println("---------------"); // new Thread(() -> { // System.out.println("新线程Lambda表达式..." + Thread.currentThread().getName()); // }).start(); } }
匿名内部类
匿名内部类的本质是在编译时生成一个Class 文件。XXXXX$1.class。
通过反编译工具XJad来查看生成的代码。
Lambda表达式
Lambda在编译时只有一个本身的Class 文件。通过反编译工具XJad无法打开。
通过JDK自带的一个工具 javap 对字节码进行反汇编操作。
//javap -c -p 文件名.class //-c:表示对代码进行反汇编 //-p:显示所有的类和成员 javap -c -p Test.class
上面的效果可以理解为如下:
public class Test { public static void main(String[] args) { .... } private static void lambda$main$0() { System.out.println("新线程Lambda表达式..." + Thread.currentThread().getName()); } }
为了更加直观的理解这个内容,在运行的时候添加 - Djdk.internal.lambda.dumpProxyClasses(加上这个参数会将内部class码输出到一个文件中)
//java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名
- 可以看到这个匿名的内部类实现了Runnable接口,并重写了run()方法。在run方法中调用了 Test.lambda$main$0(),也就是调用了Lambda中的内容。
匿名内部类 VS Lambda表达式
- 所需类型不一样
- 匿名内部类的类型可以是 类,抽象类,接口。
- Lambda表达式需要的类型必须是接口。
- 抽象方法的数量不一样
- 匿名内部类所需的接口中的抽象方法的数量是随意的。
- Lambda表达式所需的接口中只能有一个抽象方法。
- 实现原理不一样
- 匿名内部类是在编译后形成一个class。
- Lambda表达式是在程序运行的时候动态生成class。
三、函数式编程
1、函数式接口注解
1)FunctionalInterface注解
- 使用Lambda表达式的前提:
- 方法的参数或局部变量类型必须为接口。
- 接口中有且仅有一个抽象方法。
- @FunctionalInterface:被该注解修饰的接口表明是一个函数接口,只能声明一个抽象方法。
2)自定义函数式接口
用@FunctionalInterface修饰接口。
只能声明一个抽象方法。允许有多个default、static方法。
2、函数式接口实战
- Lambda表达式使用时不关心接口名和抽象方法名,只关心抽象方法的参数列表和返回值类型。因此在JDK中提供了大量常用的函数式接口,主要是在 java.util.function 包中。
1)Supplier
介绍:无参,有返回值的接口。需要提供一个返回数据的类型。作用类似生产者。
@FunctionalInterface public interface Supplier<T> { /** * Gets a result. * * @return a result */ T get(); }
使用:
public ResponseData<SipResponse> syncRequest(Supplier<SendRequestContext> supplier) { //由调用方决定是SendRequestContext怎么生成的 SendRequestContext context = supplier.get(); //... return response; }
SendRequestContext doRequest1(XXX xxx, YYY yyy) { //... return context; } SendRequestContext doRequest2(XXX xxx, YYY yyy) { //... return context; } //调用方 ResponseData<SipResponse> broadcastResponse = sipRequestSupport.syncRequest( () -> doRequest1(xxx, yyy)); ResponseData<SipResponse> broadcastResponse = sipRequestSupport.syncRequest( () -> doRequest2(xxx, yyy));
2)Consumer
介绍:有参,无返回值的接口。需要指定一个泛型来定义参数类型。作用类似消费者。
@FunctionalInterface public interface Consumer<T> { /** * Performs this operation on the given argument. * * @param t the input argument */ void accept(T t); }
使用:
public class ConsumerTest { public static void main(String[] args) { test("Hello World", msg -> { System.out.println(msg + "-> 转换为小写:" + msg.toLowerCase()); }); } public static void test(String msg, Consumer<String> consumer) { consumer.accept(msg); } }
//类似定义前置或者后置操作,由调用方决定具体行为 protected void responseOk(XXX xxx, YYY yyy, Consumer<Response> consumer) { //... if (consumer != null) { consumer.accept(response); } }
andThen方法:
//参数和返回值全部是Consumer类型。 效果:消费一个数据的时候,首先做一个操作,然后再做一个操作。(将两个行为重新定义为一个行为) default Consumer<T> andThen(Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } //使用 public static void test2(String msg, Consumer<String> c1, Consumer<String> c2){ c1.andThen(c2).accept(msg);//先消费c1,再消费c2。 }
3)Function
介绍:有参,有返回值的接口。是根据一个类型的数据得到另一个类型的数据。作用类似map映射。
@FunctionalInterface public interface Function<T, R> { /** * Applies this function to the given argument. * * @param t the function argument * @return the function result */ R apply(T t); }
使用:传入一个字符串返回一个数字。
public class FunctionTest { public static void main(String[] args) { test("666", msg -> { return Integer.parseInt(msg); }); } public static void test(String msg, Function<String, Integer> function) { Integer apply = function.apply(msg); System.out.println("apply = " + apply);//apply = 666 } }
andThen方法:
//参数和返回值全部是Function类型。 效果:转换一个数据的时候,先转换为一个中间值,然后中间值再转换为最终值。 default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } //使用 public static void test2(Function<String,Integer> f1, Function<Integer,Integer> f2){ Integer i = f1.andThen(f2).apply("666");//先通过f1把String转换为中间值Integer,再通过f2把中间值Integer转换为结果Integer。 }
compose方法:
//作用和andThen类似,执行顺序和andThen方法刚好相反。 default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } //使用 public static void test2(Function<String,Integer> f1, Function<Integer,Integer> f2){ Integer i = f2.compose(f1).apply("666");//先通过f1把String转换为中间值Integer,再通过f2把中间值Integer转换为结果Integer。 }
identity方法:
//静态方法,返回一个Function,这个function的行为是:输入什么参数就返回什么参数 static <T> Function<T, T> identity() { return t -> t; }
4)Predicate
介绍:有参,返回值为Boolean的接口。主要用于判断、断言。
@FunctionalInterface public interface Predicate<T> { /** * Evaluates this predicate on the given argument. * * @param t the input argument * @return {@code true} if the input argument matches the predicate, * otherwise {@code false} */ boolean test(T t); }
使用:判断传入的字符串长度是否大于3。
public class PredicateTest { public static void main(String[] args) { test("HelloWorld", msg -> { return msg.length() > 3; }); } private static void test(String msg, Predicate<String> predicate) { boolean b = predicate.test(msg); System.out.println("b:" + b);//b:true } }
Predicate中的默认方法提供了逻辑关系操作:and、or、negate方法。
//将两个判断逻辑重新组织为一个新的判断逻辑。(&&:两个逻辑都要满足) default Predicate<T> and(Predicate<? super T> other) { Objects.requireNonNull(other); return (t) -> test(t) && other.test(t); } //将两个判断逻辑重新组织为一个新的判断逻辑。(||:两个逻辑满足一个即可) default Predicate<T> or(Predicate<? super T> other) { Objects.requireNonNull(other); return (t) -> test(t) || other.test(t); } //将该判断逻辑重新组织为一个新的判断逻辑。(!:判断结果取反) default Predicate<T> negate() { return (t) -> !test(t); } private static void test(Predicate<String> p1, Predicate<String> p2) { // 同时满足p1和p2则为true boolean bb1 = p1.and(p2).test("Hello"); // 满足p1或者p2则为true boolean bb2 = p1.or(p2).test("Hello"); // 不满足p1则为true boolean bb3 = p1.negate().test("Hello"); }
isEqual方法:
//静态方法,返回一个Predicate,这个predicate的判断逻辑是:输入的参数是否与默认的targetRef相等。 static <T> Predicate<T> isEqual(Object targetRef) { return (null == targetRef) ? Objects::isNull : object -> targetRef.equals(object); }
5)其他内置接口
3、方法引用
1)使用场景
如果Lambda表达式所要实现的方案,已经有其他方法存在相同的方案,那么则可以使用方法引用。
例子:
public class FunctionRefTest01 { public static void main(String[] args) { printMax(a -> { // Lambda表达式中的代码和 getTotal中的代码冗余了 int sum = 0; for (int i : a) { sum += i; } System.out.println("数组之和:" + sum); }); //使用方法引用简化代码 printMax(FunctionRefTest01::getTotal); } /** * 求数组中的所有元素的和 * * @param a */ public static void getTotal(int a[]) { int sum = 0; for (int i : a) { sum += i; } System.out.println("数组之和:" + sum); } private static void printMax(Consumer<int[]> consumer) { int[] a = {10, 20, 30, 40, 50, 60}; consumer.accept(a); } }
- 注意:
- 被引用的方法,参数要和接口中的抽象方法的参数一样。
- 当接口抽象方法有返回值时,被引用的方法也必须有返回值。
- 方法引用只能引用已经存在的方法。
2)使用格式
对象名::成员方法名
类名::静态方法名
类名::成员方法名(Java面向对象中,类名只能调用静态方法,类名引用成员方法是有前提的,实际上是拿第一个参数作为方法的调用者)
//该例子中,apply方法的第一个参数是String类型,所以可以使用String::length。length()是成员方法。 public class FunctionRefTest02 { public static void main(String[] args) { Function<String, Integer> function = (s) -> { return s.length(); }; System.out.println(function.apply("hello")); // 通过方法引用来实现 Function<String, Integer> function1 = String::length; System.out.println(function1.apply("hello")); } } //该例子改造getTotal为成员方法,使用FunctionRefTest01::getTotal引用。consumer.accept的第一个参数为FunctionRefTest01,所以可以引用。 public class FunctionRefTest01 { public static void test(String[] args) { BiConsumer<FunctionRefTest01, int []> consumer = FunctionRefTest01::getTotal; printMax(consumer); } /** * 求数组中的所有元素的和 * * @param a */ public void getTotal(int a[]) { int sum = 0; for (int i : a) { sum += i; } System.out.println("数组之和:" + sum); } private static void printMax(BiConsumer<FunctionRefTest01, int []> consumer) { int[] a = {10, 20, 30, 40, 50, 60}; consumer.accept(new FunctionRefTest01(), a); } }
类名::new(构造器方法)
数组::new(数组的构造方法)例如:String[]::new
public static void main(String[] args) { Function<Integer, String[]> fun1 = (len) -> { return new String[len]; }; String[] a1 = fun1.apply(3); System.out.println("数组的长度是:" + a1.length);//数组的长度是:3 // 方法引用 的方式来调用数组的构造器 Function<Integer, String[]> fun2 = String[]::new; String[] a2 = fun2.apply(5); System.out.println("数组的长度是:" + a2.length);//数组的长度是:5 }
四、Optional类
1、简单介绍
Optional是一个没有子类的工具类。
Optional是一个可以为null的容器对象,它的主要作用就是为了避免Null显式检查,防止NullpointerException。
2、创建方式
- of(T t)方法,of方法是不支持null的。
- ofNullable(T t)方法,ofNullable方法支持null。
- empty()方法
/** * Optional对象的创建方式 */ @Test public void test01() { // 第一种方式 通过of方法 of方法是不支持null的 Optional<String> op1 = Optional.of("zhangsan"); //Optional<Object> op2 = Optional.of(null);//报错,of方法不支持null // 第二种方式通过 ofNullable方法 支持null Optional<String> op3 = Optional.ofNullable("lisi"); Optional<Object> op4 = Optional.ofNullable(null); // 第三种方式 通过empty方法直接创建一个空的Optional对象 Optional<Object> op5 = Optional.empty(); }
3、常用方法
- **isPresent()**方法:判断是否包含值,包含值返回true,不包含值返回false。
- **get()**方法:如果Optional有值则返回,否则抛出NoSuchElementException异常。 get()通常和isPresent方法一块使用。
- **orElse(T t)**方法:如果调用对象包含值,就返回该值,否则返回t。
- orElseGet(Supplier)方法:如果调用对象包含值,就返回该值,否则返回通过Supplier生产的值。
- orElseThrow(Supplier)方法:如果调用对象包含值,就返回该值,否则抛出通过Supplier生产的异常。
- isPresent(Consumer)方法:如果存在值就执行传进来的consumer行为。
- filter(Predicate)方法:如果存在值且通过断言判断为true,则返回自身,否则返回空。
- map(Function)方法:如果存在值就通过function行为来转换为另一个可以为空的值,否则返回空。
/** * Optional常用方法:isPresent()、get()、orElse(T t) */ @Test public void test02() { Optional<String> op1 = Optional.of("zhangsan"); Optional<String> op2 = Optional.empty(); // 获取Optional中的值 if (op1.isPresent()) { String s1 = op1.get(); System.out.println("用户名称:" + s1);//用户名称:zhangsan } if (op2.isPresent()) { System.out.println(op2.get()); } else { System.out.println("op2是一个空Optional对象");//op2是一个空Optional对象 } String s3 = op1.orElse("李四"); System.out.println(s3);//zhangsan String s4 = op2.orElse("王五"); System.out.println(s4);//王五 }
4、最佳实践
/** * 1、定义方法的返回值为Optional<T>对象。提醒调用方注意是否需要null值处理。 * 2、通过Optional.ofNullable(thisValue).orElse(otherValue)设置默认值,简化代码。 * 3、通过Optional.ofNullable(thisValue).orElseThrow(Supplier)检查null值并且抛出业务异常,简化代码。 */
五、Stream API
1、Stream流式思想概述
Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工处理。
Stream流的各种操作可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。
Stream API能完成许多复杂的操作,如筛选、切片、映射、查找、去重,统计,匹配和归约等。
2、Stream流处理过程
元素集合——》生成流——》中间操作——》终结操作
3、Stream流的获取方式
3.1、根据Collection获取
java.util.Collection 接口中加入了default方法 stream(),所以Collection接口下的所有实现类对象都可以通过steam方法来获取Stream流。
public static void main(String[] args) { List<String> list = new ArrayList<>(); list.stream(); Set<String> set = new HashSet<>(); set.stream(); Vector vector = new Vector(); vector.stream(); }
Map接口没有实现Collection接口,可以根据Map先获取对应的key-value的集合,再获取Stream流。
public static void main(String[] args) { Map<String, Object> map = new HashMap<>(); Stream<String> stream = map.keySet().stream(); // key Stream<Object> stream1 = map.values().stream(); // value Stream<Map.Entry<String, Object>> stream2 = map.entrySet().stream(); //entry }
3.2、通过Stream的of方法
Stream接口中提供了静态方法of创建流。
public static void main(String[] args) { Stream<String> a1 = Stream.of("a1", "a2", "a3"); String[] arr1 = {"aa", "bb", "cc"}; Stream<String> arr11 = Stream.of(arr1); Integer[] arr2 = {1, 2, 3, 4}; Stream<Integer> arr21 = Stream.of(arr2); // 注意:基本数据类型的数组创建的流的元素不是数组里元素,而是数组本身。 int[] arr3 = {1, 2, 3, 4}; Stream<int[]> arr31 = Stream.of(arr3); arr31.forEach(System.out::println);//[I@16c0663d }
4、Stream流的常用方法
4.1、非终结方法
返回值类型仍然是 Stream 类型的方法,支持链式调用。
filter:作用是用来过滤数据,返回符合条件的数据。该方法接收一个Predicate函数式接口参数作为筛选条件。
Stream<T> filter(Predicate<? super T> predicate);
public static void main(String[] args) { Stream.of("a1", "a2", "a3", "bb", "cc", "aa", "dd") .filter((s) -> s.contains("a"))//筛选包含a的元素,返回的是新的流。 .forEach(System.out::println); }
limit:作用是对流进行截取处理,只取前n个数据。参数是一个long类型的数值,如果集合当前长度大于该参数就进行截取,否则不操作。
Stream<T> limit(long maxSize);
public static void main(String[] args) { Stream.of("a1", "a2", "a3", "bb", "cc", "aa", "dd") .limit(3)//只取前3个数据,返回的是新的流。 .forEach(System.out::println); }
skip:作用是跳过流前面几个元素。参数是一个long类型的数值。
Stream<T> skip(long n);
public static void main(String[] args) { Stream.of("a1", "a2", "a3", "bb", "cc", "aa", "dd") .skip(3)//跳过前3个数据,返回的是新的流。 .forEach(System.out::println); }
map:作用是将流中的元素映射到另一个流中(一一对应)。需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的数据。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
public static void main(String[] args) { Stream.of("1", "2", "3", "4", "5", "6", "7") //.map(msg->Integer.parseInt(msg))//将String元素转换为Integer元素 .map(Integer::parseInt)//这里使用方法引用 .forEach(System.out::println); }
//将Stream中的元素转换成int类型。例如将Integer元素转为int,作用:Integer占用的内存比int多很多,在Stream流操作中会自动装修和拆箱操作。 IntStream mapToInt(ToIntFunction<? super T> mapper); //扁平化映射,把每个元素映射成小流,最终汇成一个流。作用:可以将多个维度的数据映射成一个维度的数据。 <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); /** * 例如(多个年级的学生数据先映射成每个年级一个学生小数据流,最后映射成一个总的学生数据流) * [{id:1, stus: [{name:a},{name:b}]}, {id:2, stus: [{name:c},{name:d}]}, {id:3, stus: [{name:e},{name:f},{name:g}]}] => * [{name:a},{name:b},{name:c},{name:d},{name:e},{name:f},{name:g}] 3个元素集合变成7个元素集合。 */
distinct:作用是去掉流中重复的数据。
Stream<T> distinct();
public static void main(String[] args) { Stream.of(1, 3, 3, 4, 0, 1, 7) .distinct() // 去掉重复的记录 .forEach(System.out::println); }
sorted:作用是将数据排序,可以根据自然规则排序,也可以通过比较器来指定对应的排序规则。
Stream<T> sorted(); Stream<T> sorted(Comparator<? super T> comparator);
public static void main(String[] args) { Stream.of(1, 3, 2, 4, 0, 9, 7) //.sorted() // 根据数据的自然顺序排序 .sorted((o1, o2) -> o2 - o1) // 根据比较器指定排序规则 .forEach(System.out::println); }
concat:静态方法,作用是将两个流合并成为一个流。
public static void main(String[] args) { Stream<String> stream1 = Stream.of("a", "b", "c"); Stream<String> stream2 = Stream.of("x", "y", "z"); // 通过concat方法将两个流合并为一个新的流 Stream.concat(stream1, stream2).forEach(System.out::println); }
4.2、终结方法
返回值类型不再是 Stream 类型的方法。
- forEach:作用是遍历流中的数据。该方法接受一个Consumer接口,会将每一个流元素交给函数处理。
void forEach(Consumer<? super T> action);
count:作用是统计流中的元素个数。该方法返回一个long值,代表元素的个数。
long count(); public static void main(String[] args) { long count = Stream.of("a1", "a2", "a3").count(); System.out.println(count); }
match:判断数据是否匹配指定的条件。
boolean anyMatch(Predicate<? super T> predicate);// 元素是否有任意一个满足条件 boolean allMatch(Predicate<? super T> predicate);// 元素是否都满足条件 boolean noneMatch(Predicate<? super T> predicate);// 元素是否都不满足条件
public static void main(String[] args) { boolean b = Stream.of(1, 3, 3, 4, 5, 1, 7) //.allMatch(s -> s > 0) //.anyMatch(s -> s >4) .noneMatch(s -> s > 4); System.out.println(b); }
find:作用是找到某些数据。
Optional<T> findFirst();//返回流中的第一个元素, Optional<T> findAny();//返回流中的元素是随机的,一般情况,串行流返回的是第一个元素。并行流返回的元素是不确定的。 //如果流为空则返回空的optional,如果选中的元素为null则抛出异常。
public static void main(String[] args) { Optional<String> first = Stream.of("1", "3", "3", "4", "5", "1", "7").findFirst(); System.out.println(first.get()); Optional<String> any = Stream.of("1", "3", "3", "4", "5", "1", "7").findAny(); System.out.println(any.get()); }
max和min:作用是获取流中的最大值、最小值。接受一个Comparator比较器参数。
Optional<T> max(Comparator<? super T> comparator); Optional<T> min(Comparator<? super T> comparator);
public static void main(String[] args) { Optional<Integer> max = Stream.of(1, 3, 3, 4, 5, 1, 7) .max((o1, o2) -> o1 - o2); System.out.println(max.get()); Optional<Integer> min = Stream.of(1, 3, 3, 4, 5, 1, 7) .min((o1, o2) -> o1 - o2); System.out.println(min.get()); }
reduce:作用是要将所有数据归纳得到一个数据。
T reduce(T identity, BinaryOperator<T> accumulator);
public static void main(String[] args) { Integer sum = Stream.of(4, 5, 3, 9) // identity初始值 // 第一次的时候会将初始值赋值给x // 之后每次会将 上一次的操作结果赋值给x y就是每次从数据中获取的元素 .reduce(0, (x, y) -> { System.out.println("x=" + x + ",y=" + y); return x + y; }); System.out.println(sum); }
4.3、使用注意
- Stream流只能操作一次,每次操作返回的Stream是新的流。
- Stream流不调用终结方法,中间的非终结方法的操作不会被执行。
5、Stream流的结果收集
5.1、结果收集到集合中
通过collect(Collector)方法。
<R, A> R collect(Collector<? super T, A, R> collector);
/** * Stream结果收集 * 收集到集合中 */ @Test public void test01() { // 收集到 List集合中 List<String> list = Stream.of("aa", "bb", "cc", "aa") .collect(Collectors.toList()); System.out.println(list); // 收集到 Set集合中 Set<String> set = Stream.of("aa", "bb", "cc", "aa") .collect(Collectors.toSet()); System.out.println(set); // 如果需要获取的类型为具体的实现,比如:ArrayList HashSet ArrayList<String> arrayList = Stream.of("aa", "bb", "cc", "aa") //.collect(Collectors.toCollection(() -> new ArrayList<>())); .collect(Collectors.toCollection(ArrayList::new)); System.out.println(arrayList); HashSet<String> hashSet = Stream.of("aa", "bb", "cc", "aa") .collect(Collectors.toCollection(HashSet::new)); System.out.println(hashSet); }
5.2、结果收集到数组中
通过toArray方法。
Object[] toArray();//返回的数组类型为Object <A> A[] toArray(IntFunction<A[]> generator);//返回的数组类型为A
/** * Stream结果收集到数组中 */ @Test public void test02() { Object[] objects = Stream.of("aa", "bb", "cc", "aa").toArray(); // 返回的数组中的元素是 Object类型 System.out.println(Arrays.toString(objects)); // 如果需要指定返回的数组中的元素类型 String[] strings = Stream.of("aa", "bb", "cc", "aa").toArray(String[]::new); System.out.println(Arrays.toString(strings)); }
5.3、对流中的数据做聚合计算
使用Stream流处理数据后,可以通过Collectors工具类像数据库的聚合函数一样对某个字段进行操作,比如获得最大值,最小值,求和,平均值,统计数量。
maxBy(Comparator)、minBy(Comparator)、summingInt(ToIntFunction)、averagingInt(ToIntFunction)、counting()
/** * Stream流中数据的聚合计算 */ @Test public void test03() { // 获取年龄的最大值 Optional<Person> maxAge = Stream.of( new Person("张三", 18) , new Person("李四", 22) , new Person("张三", 13) , new Person("王五", 15) , new Person("张三", 19) ).collect(Collectors.maxBy((p1, p2) -> p1.getAge() - p2.getAge())); System.out.println("最大年龄:" + maxAge.get()); // 获取年龄的最小值 Optional<Person> minAge = Stream.of( new Person("张三", 18) , new Person("李四", 22) , new Person("张三", 13) , new Person("王五", 15) , new Person("张三", 19) ).collect(Collectors.minBy((p1, p2) -> p1.getAge() - p2.getAge())); System.out.println("最新年龄:" + minAge.get()); // 求所有人的年龄之和 Integer sumAge = Stream.of( new Person("张三", 18) , new Person("李四", 22) , new Person("张三", 13) , new Person("王五", 15) , new Person("张三", 19) ).collect(Collectors.summingInt(Person::getAge)); System.out.println("年龄总和:" + sumAge); // 年龄的平均值 Double avgAge = Stream.of( new Person("张三", 18) , new Person("李四", 22) , new Person("张三", 13) , new Person("王五", 15) , new Person("张三", 19) ).collect(Collectors.averagingInt(Person::getAge)); System.out.println("年龄的平均值:" + avgAge); // 统计数量 Long count = Stream.of( new Person("张三", 18) , new Person("李四", 22) , new Person("张三", 13) , new Person("王五", 15) , new Person("张三", 19) ).filter(p -> p.getAge() > 18).collect(Collectors.counting()); System.out.println("满足条件的记录数:" + count); }
5.4、对流中数据做分组操作
使用Stream流处理数据后,可以通过Collectors工具类根据某个属性将数据分组。
groupingBy(Function)、groupingBy(Function, Collector)等
/** * 分组操作 */ @Test public void test04() { // 根据账号对数据进行分组 Map<String, List<Person>> map1 = Stream.of( new Person("张三", 18, 175) , new Person("李四", 22, 177) , new Person("张三", 14, 165) , new Person("李四", 15, 166) , new Person("张三", 19, 182) ).collect(Collectors.groupingBy(Person::getName)); map1.forEach((k, v) -> System.out.println("k=" + k + "\t" + "v=" + v)); System.out.println("-----------"); // 根据年龄分组 如果大于等于18 成年否则未成年 Map<String, List<Person>> map2 = Stream.of( new Person("张三", 18, 175) , new Person("李四", 22, 177) , new Person("张三", 14, 165) , new Person("李四", 15, 166) , new Person("张三", 19, 182) ).collect(Collectors.groupingBy(p -> p.getAge() >= 18 ? "成年" : "未成年")); map2.forEach((k, v) -> System.out.println("k=" + k + "\t" + "v=" + v)); } 输出结果: k=李四 v=[Person{name='李四', age=22, height=177}, Person{name='李四', age=15, height=166}] k=张三 v=[Person{name='张三', age=18, height=175}, Person{name='张三', age=14, height=165}, Person{name='张三', age=19, height=182}] ----------- k=未成年 v=[Person{name='张三', age=14, height=165}, Person{name='李四', age=15, height=166}] k=成年 v=[Person{name='张三', age=18, height=175}, Person{name='李四', age=22, height=177}, Person{name='张三', age=19, height=182}]
多级分组: 先根据name分组然后根据年龄分组
/** * 分组计算--多级分组 */ @Test public void test05() { // 先根据name分组,然后根据age(成年和未成年)分组 Map<String, Map<Object, List<Person>>> map = Stream.of( new Person("张三", 18, 175) , new Person("李四", 22, 177) , new Person("张三", 14, 165) , new Person("李四", 15, 166) , new Person("张三", 19, 182) ).collect(Collectors.groupingBy( Person::getName , Collectors.groupingBy(p -> p.getAge() >= 18 ? "成年" : "未成年") )); map.forEach((k, v) -> { System.out.println(k); v.forEach((k1, v1) -> { System.out.println("\t" + k1 + "=" + v1); }); }); } 输出结果: 李四 未成年=[Person{name='李四', age=15, height=166}] 成年=[Person{name='李四', age=22, height=177}] 张三 未成年=[Person{name='张三', age=14, height=165}] 成年=[Person{name='张三', age=18, height=175}, Person{name='张三', age=19, height=182}]
5.5、对流中的数据做分区操作
**Collectors.partitioningBy(Predicate) **会根据值是否为true,把集合中的数据分割为两个列表,一个true列表,一个 false列表。
/** * 分区操作 */ @Test public void test06() { Map<Boolean, List<Person>> map = Stream.of( new Person("张三", 18, 175) , new Person("李四", 22, 177) , new Person("张三", 14, 165) , new Person("李四", 15, 166) , new Person("张三", 19, 182) ).collect(Collectors.partitioningBy(p -> p.getAge() > 18)); map.forEach((k, v) -> System.out.println(k + "\t" + v)); } 输出结果: false [Person{name='张三', age=18, height=175}, Person{name='张三', age=14, height=165}, Person{name='李四', age=15, height=166}] true [Person{name='李四', age=22, height=177}, Person{name='张三', age=19, height=182}]
5.6、对流中的数据做拼接
Collectors.joining 会根据指定的连接符,将所有的元素连接成一个字符串。
/** * 对流中的数据做拼接操作 */ @Test public void test07() { String s1 = Stream.of( new Person("张三", 18, 175) , new Person("李四", 22, 177) , new Person("张三", 14, 165) , new Person("李四", 15, 166) , new Person("张三", 19, 182) ).map(Person::getName).collect(Collectors.joining()); // 张三李四张三李四张三 System.out.println(s1); String s2 = Stream.of( new Person("张三", 18, 175) , new Person("李四", 22, 177) , new Person("张三", 14, 165) , new Person("李四", 15, 166) , new Person("张三", 19, 182) ).map(Person::getName).collect(Collectors.joining("_")); // 张三_李四_张三_李四_张三 System.out.println(s2); String s3 = Stream.of( new Person("张三", 18, 175) , new Person("李四", 22, 177) , new Person("张三", 14, 165) , new Person("李四", 15, 166) , new Person("张三", 19, 182) ).map(Person::getName).collect(Collectors.joining("_", "###", "$$$")); // ###张三_李四_张三_李四_张三$$$ System.out.println(s3); }
6、并行Stream流介绍
- 前面使用的Stream流都是串行,也就是在一个线程上面执行。
- 并行Stream流就是一个并行执行的流,通过默认的ForkJoinPool,可以提高多线程任务的速度。
6.1、获取并行流
- 通过Collection接口中的parallelStream方法来获取。
- 通过已有的串行流转换为并行流( parallel()方法 )
/** * 获取并行流的两种方式 */ @Test public void test02() { List<Integer> list = new ArrayList<>(); // 通过集合接口 直接获取并行流 Stream<Integer> integerStream = list.parallelStream(); // 将已有的串行流转换为并行流 Stream<Integer> parallel = Stream.of(1, 2, 3).parallel(); }
6.2、并行流操作
例子:
/** * 并行流操作 */ @Test public void test03() { Stream.of(1, 4, 2, 6, 1, 5, 9) .parallel() // 将流转换为并发流,Stream处理的时候就会通过多线程处理 .filter(s -> { System.out.println(Thread.currentThread() + " s=" + s); return s > 2; }).count(); }
结果:
Thread[main,5,main] s=1 Thread[ForkJoinPool.commonPool-worker-1,5,main] s=9 Thread[ForkJoinPool.commonPool-worker-2,5,main] s=4 Thread[ForkJoinPool.commonPool-worker-3,5,main] s=5 Thread[ForkJoinPool.commonPool-worker-1,5,main] s=1 Thread[main,5,main] s=6 Thread[ForkJoinPool.commonPool-worker-2,5,main] s=2
Stream并行处理的过程会分而治之,将一个大的任务切分成了多个小任务,每个任务都是一个线程操作。参考Fork/Join原理。
6.3、线程安全问题
在多线程的处理下,肯定会出现数据安全问题。如下:
@Test public void test04() { List<Integer> list = new ArrayList<>(); for (int i = 0; i < 1000; i++) { list.add(i); } System.out.println(list.size()); List<Integer> listNew = new ArrayList<>(); // 使用并行流来向集合中添加数据 list.parallelStream().forEach(listNew::add); System.out.println(listNew.size()); } 结果: 1000 983 或者抛出java.lang.ArrayIndexOutOfBoundsException异常。
解决方案:
- 加同步锁。
- 使用线程安全的容器。
- 通过Stream中的toArray/collect操作。
// 通过集合接口 直接获取并行流
Stream<Integer> integerStream = list.parallelStream(); // 将已有的串行流转换为并行流 Stream<Integer> parallel = Stream.of(1, 2, 3).parallel(); }
6.2、并行流操作
例子:
/** * 并行流操作 */ @Test public void test03() { Stream.of(1, 4, 2, 6, 1, 5, 9) .parallel() // 将流转换为并发流,Stream处理的时候就会通过多线程处理 .filter(s -> { System.out.println(Thread.currentThread() + " s=" + s); return s > 2; }).count(); }
结果:
Thread[main,5,main] s=1 Thread[ForkJoinPool.commonPool-worker-1,5,main] s=9 Thread[ForkJoinPool.commonPool-worker-2,5,main] s=4 Thread[ForkJoinPool.commonPool-worker-3,5,main] s=5 Thread[ForkJoinPool.commonPool-worker-1,5,main] s=1 Thread[main,5,main] s=6 Thread[ForkJoinPool.commonPool-worker-2,5,main] s=2
Stream并行处理的过程会分而治之,将一个大的任务切分成了多个小任务,每个任务都是一个线程操作。参考Fork/Join原理。
6.3、线程安全问题
在多线程的处理下,肯定会出现数据安全问题。如下:
@Test public void test04() { List<Integer> list = new ArrayList<>(); for (int i = 0; i < 1000; i++) { list.add(i); } System.out.println(list.size()); List<Integer> listNew = new ArrayList<>(); // 使用并行流来向集合中添加数据 list.parallelStream().forEach(listNew::add); System.out.println(listNew.size()); } 结果: 1000 983 或者抛出java.lang.ArrayIndexOutOfBoundsException异常。
解决方案:
- 加同步锁。
- 使用线程安全的容器。
- 通过Stream中的toArray/collect操作。