万万没想到,都0202年了,Sun都亡了,老夫还要从Java5的新特性开始写,还要重点写Java8的新特性。。。
其实网上这种玩意一大堆,为啥老夫还要写呢?
这个速成版资料特点有:
src/test/java
下)
https://github.com/zhaochuninhefei/study-czhao/tree/master/jdk11-test
或 : https://gitee.com/XiaTangShaoBing/study/tree/master/jdk11-test
这一章主要快速地讲一下Java5到Java7在语法上的一些重要的新特性,以及一些重要的新的类库API。
Java5新特性比较多,但大部分我们都已经很熟悉了,简单过一下:
泛型即参数化类型(Parameterized Type)。引入泛型之后,允许指定集合里元素的类型,免去了强制类型转换,并且能在编译时刻进行类型检查。泛型是长度可变的参数列表(vararg)、注解(annotation)、枚举(enumeration)、集合(collection)的基石。
List<String> lst01 = new ArrayList<String>(); // 用 ? 表示接受任何类型,可以避免调用方法时类型检查警告。 private void test01(List<?> list) { for (Iterator<?> i = list.iterator(); i.hasNext(); ) { System.out.println((i.next().toString())); } } // 限制类型,此处表示参数类型必须继承TestCase01Generic private <T extends TestCase01Generic> void test02(T t) { t.doSomething(); }
枚举类是一种特殊的类,它和普通的类一样,有自己的成员变量、成员方法、构造器 (只能使用 private 访问修饰符,所以无法从外部调用构造器,构造器只在构造枚举值时被调用);enum 定义的枚举类默认继承了 java.lang.Enum 类,并实现了 java.lang.Seriablizable 和 java.lang.Comparable 两个接口;所有的枚举值默认都是 public static final 的(无需显式添加),且非抽象的枚举类不能再派生子类;枚举类的所有实例(枚举值)必须在枚举类的第一行显式地列出,否则这个枚举类将永远不能产生实例。列出这些实例(枚举值)时,系统会自动添加 public static final 修饰,无需程序员显式添加。
enum Color { black, white, red, yellow } // 枚举经常用于switch语句 private void test01(Color color) { switch (color) { case red: System.out.println("霜叶红于二月花"); break; case black: System.out.println("黑云压城城欲摧"); break; case white: System.out.println("一行白鹭上青天"); break; case yellow: System.out.println("故人西辞黄鹤楼"); break; } System.out.println(Color.black.compareTo(color)); System.out.println(Color.white.compareTo(color)); System.out.println(Color.red.compareTo(color)); System.out.println(Color.yellow.compareTo(color)); }
八种primitive类型与其封装引用类型的自动装箱与拆箱:Boolean、Byte、Short、Character、Integer、Long、Float、Double
List<Integer> lstInt = new ArrayList<Integer>(); lstInt.add(1); lstInt.add(2); lstInt.add(3); for (int i = 0; i < lstInt.size(); i++) { System.out.println(lstInt.get(i).toString()); System.out.println(lstInt.get(i) + 1); }
me.test01("One ring to rule them all,"); me.test01("one ring to find them,", "One ring to bring them all ", "and in the darkness bind them."); private void test01(String ... args) { for (String s : args) { System.out.println(s); } }
注解用于为 Java 代码提供元数据。一般来说注解不会直接影响代码执行,很多注解的作用就是做数据约束和标准定义,可以将其理解成代码的规范标准(代码的模板),但有一些注解可以存活到JVM运行时,因此可以结合其他手段(如反射)来影响实际运行的代码逻辑。所以注解的目的一般来说有二:一则规范代码;二则动态注入(需要配合其他手段实现)。
通常注解可以分为四类:
// 编译器看到 @Override 注解,就知道这个方法须是重写父类的方法 // 因此会严格检查方法声明信息是否与父类对应方法相同 // 如返回值类型,参数列表等等 @Override public String toString() { return "解落三秋叶,能开二月花。"; } // 一个自定义注解的例子,用于AOP中对方法参数进行非空检查 @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ParamNotEmpty { }
List<Integer> numbers = new ArrayList<Integer>(); for (int i = 0; i < 10; i++) { numbers.add(i + 1); } for(Integer number : numbers) { System.out.println(number); }
package java5; import static java5.TestCase07ImportStatic.TestInner.test; import static java.lang.System.out; import static java.lang.Integer.*; /** * @author zhaochun */ public class TestCase07ImportStatic { public static void main(String[] args) { test(); out.println(MIN_VALUE); out.println(toBinaryString(100)); } static class TestInner { public static void test() { System.out.println("TestInner"); } } }
private void test01_formatter() { StringBuilder sb = new StringBuilder(); Formatter formatter = new Formatter(sb); // " 前不见古人, 后不见来者。 念天地之悠悠, 独怆然而涕下。" formatter.format("%4$7s,%3$7s。%2$7s,%1$7s。%n", "独怆然而涕下", "念天地之悠悠", "后不见来者", "前不见古人"); // "祖冲之的迷之数字 : +3.1415927 " formatter.format("祖冲之的迷之数字 : %+5.7f %n", Math.PI); // "某款手机价格 : ¥ 5,988.00" formatter.format("某款手机价格 : ¥ %(,.2f", 5988.0); System.out.println(formatter.toString()); formatter.close(); } private void test02_printf() { List<String> lines = new ArrayList<>(); lines.add("人闲桂花落,"); lines.add("夜静春山空。"); lines.add("月出惊山鸟,"); lines.add("时鸣春涧中。"); for (int i = 0; i < lines.size(); i++) { System.out.printf("Line %d: %s%n", i + 1, lines.get(i)); } } private void test03_stringFormat() { Calendar c = new GregorianCalendar(2020, Calendar.MAY, 28); System.out.println(String.format("今天是个好日子: %1$tY-%1$tm-%1$te", c)); } private void test04_messageFormat() { String msg = "您好,{0}!有您的快递哦!请到{1}号柜拿取您的快递,每超时{2}小时要收费{3}元哦~~~"; MessageFormat mf = new MessageFormat(msg); String fmsg = mf.format(new Object[]{"张三", 3, 8, 2}); System.out.println(fmsg); } private void test05_dateFormat() { String str = "2020-05-28 14:55:21"; SimpleDateFormat format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat format2 = new SimpleDateFormat("yyyyMMddHHmmss"); try { System.out.println(format2.format(format1.parse(str))); } catch (Exception e) { e.printStackTrace(); } }
Java6的新特性很少,对开发几乎没有影响,简单看一下。
还有一些其他的,不列了。
Java7的新特性也不多,但相比Java6,还是有几个新语法或新类库API能改善开发效率的,我们来看一下。
private String test01_switch(String title) { switch (title) { case "鹿柴": return "空山不见人,但闻人语响。返景入深林,复照青苔上。"; case "山中送别": return "山中相送罢,日暮掩柴扉。春草明年绿,王孙归不归。"; case "渭城曲": return "渭城朝雨浥轻尘,客舍青青柳色新。劝君更尽一杯酒,西出阳关无故人。"; default: return ""; } }
List<String> tempList = new ArrayList<>();
try-with-resources
新语法。 String filePath = "/home/work/sources/jdk11-test/src/test/java/java7/TestCaseForJava7.java"; try (FileInputStream fis = new FileInputStream(filePath); InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(isr)) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); }
try { if (n < 0) { throw new FileNotFoundException(); } if (n > 0) { throw new SQLException(); } System.out.println("No Exceptions."); } catch (FileNotFoundException | SQLException e) { e.printStackTrace(); }
0b
开头直接写二进制数字。 int num1 = 1_000_000; System.out.println(num1); int num2 = 0b11; System.out.println(num2);
Path
,并且提供了对指定目录进行监视的 WatchService
,能够监听指定目录下文件的增删改事件。(但并不能直接监听文件变化内容) private void test06_newIO2() { Path path = Paths.get("/home/zhaochun/test"); System.out.printf("Number of nodes: %s %n", path.getNameCount()); System.out.printf("File name: %s %n", path.getFileName()); System.out.printf("File root: %s %n", path.getRoot()); System.out.printf("File parent: %s %n", path.getParent()); try { Files.deleteIfExists(path); Files.createDirectory(path); watchFile(path); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } private void watchFile(Path path) throws IOException, InterruptedException { WatchService service = FileSystems.getDefault().newWatchService(); Path pathAbs = path.toAbsolutePath(); pathAbs.register(service, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); while (true) { WatchKey key = service.take(); for (WatchEvent<?> event : key.pollEvents()) { String fileName = event.context().toString(); String kind = event.kind().name(); System.out.println(String.format("%s : %s", fileName, kind)); if ("end".equals(fileName) && "ENTRY_DELETE".equals(kind)) { return; } } key.reset(); } }
Java8是Java继Java5之后又一个具有里程碑意义的大版本,有很多革命性的新特性。
当然,Java8新特性虽多,但我们主要讲那些对开发影响比较大的,语法上的新特性:
Java8最重要的新特性就是添加了对lambda表达式的支持,使得Java可以进行函数式编程(functional programming)。
Lambda表达式就是可按引用传递的代码块,类似于其他语言的闭包的概念:它们是实现某项功能的代码,可接受一个或多个输入参数,而且可返回一个结果值。闭包是在一个上下文中定义的,可访问来自上下文的值。
而在Java8中,lambda表达式可以被具体地表述为函数式接口的一个具体实现。所谓函数式接口,就是只定义了一个抽象方法的interface。(函数式接口可以通过添加注解 @FunctionalInterface
,从而在编译时强制检查该接口是否只有一个抽象方法。但这个注解不是必须的。)
我们先看一个具体的例子:
假设我们有这样一个接口,它只有一个抽象方法,是一个函数式接口:
@FunctionalInterface interface TestLambda { String join(String a, String b); }
以及一个使用它的方法:(显然这个方法并不需要知道具体实现TestLambda接口的类是谁)
private String joinStr(TestLambda testLambda, String a, String b) { return testLambda.join(a, b); }
接下来,我们尝试使用 joinStr
方法来连接两个字符串。在Java8之前,我们往往使用匿名内部类在需要的地方直接实现 TestLambda
接口:
String s1 = joinStr(new TestLambda() { @Override public String join(String a, String b) { return a + ", " + b; } }, "问君能有几多愁", "恰似一江春水向东流"); System.out.println(s1);
从Java8开始,你可以使用lambda表达式代替匿名内部类,就是下面代码中的 (a, b) -> a + ", " + b;
这种写法,很简洁,语义直观,更接近自然语言:
TestLambda simpleJoin = (a, b) -> a + ", " + b; String s2 = joinStr(simpleJoin, "高堂明镜悲白发", "朝如青丝暮成雪"); System.out.println(s2);
或直接写为:
String s3 = joinStr((a, b) -> a + ", " + b, "高堂明镜悲白发", "朝如青丝暮成雪"); System.out.println(s3);
当你要实现的接口逻辑比较复杂时,你可以用 {}
把代码块包起来;你还可以给每个入参声明类型:
TestLambda joinWithCheck = (String a, String b) -> { if (a != null && b != null) { return a + ", " + b; } else { return "空空如也"; } }; String s4 = joinStr(joinWithCheck, null, null); System.out.println(s4);
至此我们可以知道:
(函数的参数列表) -> {函数实现} {} {} {}
lambda表达式内部是可以访问外部变量的。 但要注意的是,如果这个外部变量是局部变量,那么这个局部变量必须是final的(可以不声明为final,但不能对其二次赋值,即,需要隐式final)。
private void test02_finalVars() { String a = "王维"; new Thread(() -> { // lambda表达式里可以使用外部的final局部变量(不用显式声明final) System.out.println(a); // 下面这句编译不过,不能对"lambda表达式里使用的外部局部变量"重新赋值。 // 即lambda内部使用的外部局部变量是隐式final的。 // a = "李白"; }).start(); // 在lambda外面也不能对a重新赋值,因为需要在lambda表达式里使用,因此a是隐式final的。 // a = "李白"; }
注意是局部变量不能重新赋值。对于实例变量,静态变量来说,可以在lambda表达式里随意访问,包括重新赋值。
Java8除了提供比较标准(对比其他语言)的lambda表达式以外,还提供了一种叫做方法引用的简便形式。
new Thread(this::test02_finalVars).start(); // 上面这句等价于下面这句: new Thread(() -> this.test02_finalVars()).start();
new Thread(TestCase01Lambda::printSomething).start(); // 等价于: new Thread(() -> TestCase01Lambda.printSomething()).start(); ... private static void printSomething() { System.out.println("大漠孤烟直,长河落日圆。"); }
List<String> lines = new ArrayList<>(); lines.add("a005"); lines.add("a001"); lines.add("a003"); Collections.sort(lines, String::compareTo); // 等价于: Collections.sort(lines, (o1, o2) -> o1.compareTo(o2)); System.out.println(lines);
Set<String> lineSet = transferElements(lines, HashSet::new); // 等价于 lineSet = transferElements(lines, () -> new HashSet<>()); System.out.println(lineSet); ... private static <T, SOURCE extends Collection<T>, DEST extends Collection<T>> DEST transferElements( SOURCE sourceCollection, Supplier<DEST> collectionFactory) { DEST result = collectionFactory.get(); result.addAll(sourceCollection); return result; }
之前我们说过了,lambda表达式实现的只能是函数式接口,即,只有一个抽象方法定义的接口。Java8还为了lambda接口的广泛使用,增加了新的 java.util.function
包,定义了一些可以广泛使用lambda的函数式接口。
这些标准函数式接口在Stream的操作中得到了广泛的应用,我们后面讲到Stream的时候会处处看到它们的身影。
Java8中新增的Stream API是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式, 极大的提高了编程效率和程序可读性 。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用fork/join并行方式(Java7的新特性,因为很少直接用,我们没有讲这个) 来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。
而Java8提供的Stream API,则让聚合操作的开发变得非常简单;代码可读性更高;在多核机器上面对耗时的可并发聚合操作时,使用并行模式的性能表现也会更好。
现在我们有了个初步的概念,就是Stream是在对数据集做聚合操作。我们先看一个典型的Stream完成聚合操作的例子:
int sum = Stream.of("", "1", null, "2", " ", "3") .filter(s -> s != null && s.trim().length() > 0) .map(s -> Integer.parseInt(s)) .reduce((left, right) -> right += left) .orElse(0);
这个例子是在计算一个集合中所有数字的合计值。
先简单讲解一下上面这个Stream操作的过程:
Stream.of("", "1", null, "2", " ", "3")
: 获取数据源的Stream对象; .filter(s -> s != null && s.trim().length() > 0)
: 过滤前面返回的Stream对象,并返回过滤后的新的Stream对象; .map(s -> Integer.parseInt(s))
: 将前面返回的Stream对象中的字符串转换为数字,并返回新的Stream对象; .reduce((left, right) -> right += left)
: 对前面返回的Stream对象执行合计操作,并返回合计值(Optional对象,包括最后的 orElse
,是Java8另外的新特性。后面再讲,这里先无视)。 从上述经典示例中,我们可以看到,一个Stream操作可以分为三个基本步骤:
1.获取数据源 Source --> 2.数据转换 Transform --> 3.执行操作 Operation
再细致一点,可以将其视为一个管道流操作:
数据集:Stream | filter:Stream | map:Stream | reduce
其中,filter与map属于 数据转换 Transform
,而reduce属于 执行操作 Operation
。每次Transform的时候,不会改变原有的Stream对象,而是返回一个新的Stream对象,因此允许对其进行链式操作,从而形成一个管道。
1.从 Collection 和数组
Collection.stream() Collection.parallelStream() Arrays.stream(T array) or Stream.of()
2.从 BufferedReader
java.io.BufferedReader.lines()
3.静态工厂
java.util.stream.IntStream.range() java.nio.file.Files.walk()
4.自己构建
java.util.Spliterator
5.其它
Random.ints() BitSet.stream() Pattern.splitAsStream(java.lang.CharSequence) JarFile.stream()
Stream操作类型:
常见的Intermediate操作:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
常见的Terminal操作:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
常见的Short-circuiting操作:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
多次的Intermediate操作并不会导致多次的数据集遍历,因为这些Intermediate是惰性的,这些转换操作只会在 Terminal 操作的时候融合起来,一次遍历完成。
至于Stream的哪些操作是Intermediate,哪些是Terminal,一个简单的标准就是看方法返回值是不是Stream。
如果你没有用过Stream,那么你看完前面对Stream的介绍估计就只能是雾里看花了。来,骚年,跟老夫一起动手把代码撸起来。
先准备一个数据集,它的元素如下(Poet,诗人):
class Poet { private String name; private int age; private int evaluation; public Poet() { } public Poet(String name, int age, int evaluation) { this.name = name; this.age = age; this.evaluation = evaluation; } @Override public String toString() { return "Poet{" + "name='" + name + '/'' + ", age=" + age + ", evaluation=" + evaluation + '}'; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getEvaluation() { return evaluation; } public void setEvaluation(int evaluation) { this.evaluation = evaluation; } }
然后准备一个唐代著名诗人的集合:
List<Poet> poets = preparePoets(); ... private List<Poet> preparePoets() { List<Poet> poets = new ArrayList<>(); // 年龄未必准确,评价不能当真 poets.add(new Poet("王维", 61, 4)); poets.add(new Poet("李白", 61, 5)); poets.add(new Poet("杜甫", 58, 5)); poets.add(new Poet("白居易", 74, 4)); poets.add(new Poet("李商隐", 45, 4)); poets.add(new Poet("杜牧", 50, 4)); poets.add(new Poet("李贺", 26, 4)); return poets; }
// foreach 等价于 poets.stream().forEach(System.out::println); poets.forEach(System.out::println);
Stream<Poet> poetStream = poets.stream(); poetStream.forEach(System.out::println); try { // 不能对同一个stream对象做两次操作,stream是流,不能回头,操作过一次之后就不能再操作了。 poetStream.forEach(System.out::println); } catch (Throwable t) { System.out.println("stream has already been operated upon or closed. 别人嚼过的甘蔗你就别嚼了。。。"); } // 但是重新从集合获取stream是可以重复操作的,因为是一个新的stream对象。 poets.stream().forEach(System.out::println);
String strPoets = poets.stream() .map(poet -> poet.getName() + " 唐代大诗人") .collect(Collectors.joining(",")); System.out.println(strPoets);
Collectors提供了很多操作,可以对各个元素进行连接操作,可以将元素导入其他Collection(List或Set),等等。
Set<String> poetsLi = poets.stream() .filter(poet -> poet.getName().startsWith("李")) .map(poet -> "唐诗三李 之 " + poet.getName()) .collect(Collectors.toSet()); System.out.println(poetsLi);
之前说对同一个stream对象只能操作一次,为何这里链式多次操作?
Poet topPoet = poets.stream() .filter(poet -> poet.getEvaluation() > 4) .findAny() // .findFirst() // 关于 orElse, 后面讲 Optional 的时候再解释 .orElse(new Poet("杜甫", 58, 5)); System.out.println("最牛的诗人之一:" + topPoet.getName());
boolean all50plus = poets.stream() .allMatch(poet -> poet.getAge() > 50); System.out.println("大诗人们都活了50岁以上吗?" + (all50plus ? "是的" : "并没有")); boolean any50plus = poets.stream() .anyMatch(poet -> poet.getAge() > 50); System.out.println("大诗人们有活到50岁以上的吗?" + (any50plus ? "有的有的" : "居然没有");
// 5星诗人数量 count System.out.println("5星诗人数量:" + poets.stream() .filter(poet -> poet.getEvaluation() == 5) .count()); // 年龄最大的诗人 System.out.println("年龄最大的诗人:" + poets.stream() .max(Comparator.comparingInt(Poet::getAge)) .orElse(null)); // 年龄最小的诗人 System.out.println("年龄最小的诗人:" + poets.stream() .min(Comparator.comparingInt(Poet::getAge)) .orElse(null)); // 年龄合计 System.out.println("诗人们年龄合计:" + poets.stream() .mapToInt(Poet::getAge) .sum());
Java8的Stream API为int,long,double专门提供了 mapToInt()
, mapToLong()
, mapToDouble()
三个方法。从语义上来说,你自己写map操作得到一个泛型为Integer/Long/Double的Stream对象,然后做后续操作当然可以。但直接使用 mapToInt()
可以提高性能表现,因为会省去后续操作的循环中的自动装箱解箱处理。
int sumAge = poets.stream() .mapToInt(Poet::getAge) .reduce((age, sum) -> sum += age) // .reduce(Integer::sum) .orElse(0); System.out.println("reduce计算出的年龄合计:" + sumAge);
注意,reduce做统计是可以有起始值的,例如:
// 假设唐代其他诗人们的评价合计已经有了,假设是 100,但还未包括前面的7位,这里从 100 开始继续统计评价总值 int sumEvaluation = poets.stream() .mapToInt(Poet::getEvaluation) .reduce(100, (left, right) -> right += left); // .reduce(100, Integer::sum); System.out.println("reduce计算出的有起始值的评价合计:" + sumEvaluation);
System.out.println("生成一个等差数组,限制长度为10:"); Stream.iterate(1, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));
String distinctEvaluation = poets.stream() .map(poet -> String.valueOf(poet.getEvaluation())) .distinct() .collect(Collectors.joining(",")); System.out.println("诗人们的评价分数(去重):" + distinctEvaluation);
System.out.println("诗人们按年龄排序:"); poets.stream() .sorted(Comparator.comparingInt(Poet::getAge)) .forEach(System.out::println);
Map<String, List<Poet>> poetsByAge = poets.stream() .collect(Collectors.groupingBy(poet -> { int age = poet.getAge(); if (age < 20) { return "1~19"; } else if (age < 30) { return "20~29"; } else if (age < 40) { return "30~39"; } else if (age < 50) { return "40~49"; } else if (age < 60) { return "50~59"; } else if (age < 70) { return "60~69"; } else { return "70~"; } })); System.out.println("将诗人们按年龄分组:"); poetsByAge.keySet().stream() .sorted(String::compareTo) .forEach(s -> System.out.println( String.format("%s : %s", s, poetsByAge.get(s).stream().map(Poet::getName).collect(Collectors.joining(",")))));
System.out.println("通过flatmap将分组后的诗人集合扁平化:"); List<Poet> lstFromGroup = poetsByAge.values().stream() .flatMap(poets1 -> poets1.stream()) .collect(Collectors.toList()); lstFromGroup.forEach(System.out::println);
刚刚的例子,都是Stream的串行模式,现在我们通过parallelStream获取Stream的并行模式。要注意并行模式与串行模式有时执行相同操作会得到不同的结果:
System.out.println("findAny:"); for (int i = 0; i < 10; i++) { Poet topPoet1 = poets.parallelStream() .filter(poet -> poet.getEvaluation() > 4) .findAny() .orElse(new Poet("XX", 50, 5)); System.out.println("最牛的诗人之一:" + topPoet1.getName()); } System.out.println("findFirst:"); for (int i = 0; i < 10; i++) { Poet topPoet2 = poets.parallelStream() .filter(poet -> poet.getEvaluation() > 4) .findFirst() .orElse(new Poet("XX", 50, 5)); System.out.println("最牛的诗人之一:" + topPoet2.getName()); }
上述代码的执行结果中,findFirst与串行并无不同,但findAny有时与串行结果不一样。想想为什么。
int sumEvaluation = poets.parallelStream() .mapToInt(Poet::getEvaluation) .reduce(100, Integer::sum); System.out.println("reduce计算有初始值时,不应该用并行运算:" + sumEvaluation);
并行模式很吸引人,但前提是你要清楚什么时候才能使用。这个例子很好的说明了带起始值的reduce操作并不适合用并行模式。
Fork/Join的本质和Hadoop的MapReduce一样,都是基于分而治之的思想,将一个任务拆成多个可以并行的小任务执行(Map、fork),最后集中到一起(Reduce、join)。当然Hadoop更复杂,处理的是在不同节点上的分布式进程,而Fork/Join是一个进程(JVM)里的多个线程。
为什么我们很少直接用Fork/Join呢?因为用起来麻烦。。。还是简单说一下吧。。。
在Stream操作中,有时我们需要写很长的lambda函数,这时我们可以灵活运用IDE的重构功能,将较长的lambda表达式重构为变量或方法。
Predicate<Poet> poetPredicate = poet -> poet.getEvaluation() < 5; Consumer<Poet> poetConsumer = poet -> System.out.println(poet.getName()); poets.stream() .filter(poetPredicate) .forEach(poetConsumer); Function<Poet, String> poetStringFunction = poet -> { int age = poet.getAge(); if (age < 20) { return "1~19"; } else if (age < 30) { return "20~29"; } else if (age < 40) { return "30~39"; } else if (age < 50) { return "40~49"; } else if (age < 60) { return "50~59"; } else if (age < 70) { return "60~69"; } else { return "70~"; } }; Map<String, List<Poet>> poetsByAge = poets.stream() .collect(Collectors.groupingBy(poetStringFunction)); System.out.println("将诗人们按年龄分组:"); Consumer<String> stringConsumer = s -> System.out.println( String.format("%s : %s", s, poetsByAge.get(s).stream().map(Poet::getName).collect(Collectors.joining(",")))); poetsByAge.keySet().stream() .sorted(String::compareTo) .forEach(stringConsumer);
Stream的性能不能简单地表述为比以前的集合遍历操作快或者慢,而是要根据具体场景的不同的性能约束条件去确认。
这里简单考虑三种场景:
下面的代码使用的硬件环境:
老夫本地可供程序使用的CPU资源:6 core (i7 4核8线程,但有两个core常年被虚拟机占用,因此共 6 个core可以使用。)
对于单个数据集的简单遍历来说,总体上讲,Stream的串行操作的性能表现大约介于fori循环与迭代器循环之间;而Stream的并行模式,在运行平台具有多核,且循环中的单次操作比较耗时的前提下,确实可以有效提高性能表现(比fori,迭代器,Stream串行都要更好)。
对于单个数据集的遍历来说,从下面的示例代码中,我们可以发现会影响性能表现的约束条件最少包括以下几点:
对于下面的代码,建议大家多尝试一下不同的约束条件,比如:
另外,代码中的 LocalDateTime
与 Duration
是Java8的又一个新特性,后面会介绍,现在不用在意。
List<String> numbers = new ArrayList<>(); for (int i = 0; i < 100; i++) { numbers.add("a" + i); } System.out.println("=== loop with fori ==="); LocalDateTime startTime = LocalDateTime.now(); for (int i = 0; i < numbers.size(); i++) { String whatever = numbers.get(i) + "b"; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } LocalDateTime stopTime = LocalDateTime.now(); System.out.println("loop with fori time(millis):" + Duration.between(startTime, stopTime).toMillis()); System.out.println("=== loop with Iterator ==="); startTime = LocalDateTime.now(); for (String num : numbers) { String whatever = num + "b"; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } stopTime = LocalDateTime.now(); System.out.println("loop with Iterator time(millis):" + Duration.between(startTime, stopTime).toMillis()); System.out.println("=== loop with stream ==="); startTime = LocalDateTime.now(); numbers.stream().forEach(num -> { String whatever = num + "b"; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } }); stopTime = LocalDateTime.now(); System.out.println("loop with stream time(millis):" + Duration.between(startTime, stopTime).toMillis()); System.out.println("=== loop with parallelStream ==="); startTime = LocalDateTime.now(); numbers.parallelStream().forEach(num -> { String whatever = num + "b"; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } }); stopTime = LocalDateTime.now(); System.out.println("loop with parallelStream time(millis):" + Duration.between(startTime, stopTime).toMillis());
上面的代码在本地运行的时候,切记件数大的时候把sleep调小甚至注释掉,省的跑半天出不来结果。。。
上面的例子仅仅是单个数据集的遍历,但在实际开发当中,我们往往会更多地遇到更复杂的数据集操作。比如最典型的,两个数据集的join操作。
首先我们在 Poet
之外,再定义两个Class: Evaluation
与 PoetExt
:
class Evaluation { private int evaluation; private String description; public Evaluation() { } public Evaluation(int evaluation, String description) { this.evaluation = evaluation; this.description = description; } public int getEvaluation() { return evaluation; } public void setEvaluation(int evaluation) { this.evaluation = evaluation; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } } class PoetExt extends Poet { private String description; public PoetExt(String name, int age, int evaluation, String description) { super(name, age, evaluation); this.description = description; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public String toString() { return "PoetExt{" + "name='" + this.getName() + '/'' + ", description='" + description + '/'' + '}'; } }
很显然, Poet
对应诗人的定义数据, Evaluation
对应评价的定义数据。我们需要实现的需求是,poets 与 evaluations 做 join 获得 PoetExt集合。这个用关系型数据库的SQL来说,就是主表为Poet,副表为Evaluation,以 Poet.evaluation = Evaluation.evaluation
为条件连接查询数据。
Java8之前,如果我们需要在Java应用中实现这样的两个数据集的join操作,那么我们往往采用的是显式的双层迭代器循环嵌套的写法,而Java8开始,我们可以利用Stream操作实现两个数据集的join操作。而根据该场景的需求,我们还可以使用Stream的并行模式。
代码如下所示,分别比较了三种写法的性能表现(显式双层迭代器遍历,Stream,并行Stream):
// poets件数 int n = 100000; // evaluations件数 int m = 100000; List<Poet> poets = new ArrayList<>(); for (int i = 0; i < n; i++) { String name = String.format("诗人%010d", i + 1); poets.add(new Poet(name, (int) (80 * Math.random()) + 10, (int) (m * Math.random()) + 1)); } List<Evaluation> evaluations = new ArrayList<>(); for (int i = 0; i < m; i++) { evaluations.add(new Evaluation(i + 1, (i + 1) + "星")); } // 要实现的逻辑是,poets 与 evaluations 做 join 获得 PoetExt集合 // 显式双层迭代器循环嵌套的写法: List<PoetExt> poetExts = new ArrayList<>(); System.out.println("=== 显式双层迭代器循环 ==="); LocalDateTime startTime = LocalDateTime.now(); for(Poet poet : poets) { int eva = poet.getEvaluation(); for(Evaluation evaluation : evaluations) { if (eva == evaluation.getEvaluation()) { PoetExt poetExt = new PoetExt(poet.getName(), poet.getAge(), eva, evaluation.getDescription()); poetExts.add(poetExt); break; } } } LocalDateTime stopTime = LocalDateTime.now(); System.out.println("显式双层迭代器循环 time(millis):" + Duration.between(startTime, stopTime).toMillis()); System.out.printf("%s 的件数: %d 与第一件结果: %s %n", "显式双层迭代器循环", poetExts.size(), poetExts.get(0).toString()); // Stream写法: System.out.println("=== Stream ==="); startTime = LocalDateTime.now(); poetExts = poets.stream() .map(poet -> { Evaluation eva = evaluations.stream() .filter(evaluation -> evaluation.getEvaluation() == poet.getEvaluation()) .findAny() .orElseThrow(); return new PoetExt(poet.getName(), poet.getAge(), poet.getEvaluation(), eva.getDescription()); }) .collect(Collectors.toList()); stopTime = LocalDateTime.now(); System.out.println("Stream time(millis):" + Duration.between(startTime, stopTime).toMillis()); System.out.printf("%s 的件数: %d 与第一件结果: %s %n", "Stream", poetExts.size(), poetExts.get(0).toString()); // parallelStream System.out.println("=== parallelStream ==="); startTime = LocalDateTime.now(); poetExts = poets.parallelStream() .map(poet -> { Evaluation eva = evaluations.parallelStream() .filter(evaluation -> evaluation.getEvaluation() == poet.getEvaluation()) .findAny() .orElseThrow(); return new PoetExt(poet.getName(), poet.getAge(), poet.getEvaluation(), eva.getDescription()); }) .collect(Collectors.toList()); stopTime = LocalDateTime.now(); System.out.println("parallelStream time(millis):" + Duration.between(startTime, stopTime).toMillis()); System.out.printf("%s 的件数: %d 与第一件结果: %s %n", "parallelStream", poetExts.size(), poetExts.get(0).toString());
老夫本地不同约束条件下的运行结果: 时间单位:毫秒
poets件数 | evaluations件数 | 显式双层迭代器循环 | Stream | parallelStream |
---|---|---|---|---|
1000 | 1000 | 53 | 44 | 145 |
10000 | 10000 | 772 | 603 | 520 |
100000 | 100000 | 27500 | 48351 | 11958 |
10000 | 100000 | 4375 | 4965 | 1510 |
100000 | 10000 | 3078 | 5053 | 1915 |
100000 | 1000000 | 421999 | 787188 | 186758 |
1000000 | 100000 | 278927 | 497239 | 122923 |
100000 | 100 | 140 | 306 | 895 |
100 | 100000 | 111 | 110 | 111 |
由此可见,在老夫本地硬件环境下(6个core可用),数据量较小时(join双方数据集件数都在1万以下),三者区别不大,显式双层迭代器循环与Stream接近,而parallelStream在1千的数据量时甚至会慢一点;而当数据量来到10万件以上的规模时,三者性能表现出现较为明显的差距,parallelStream优势明显,显式双层迭代器循环次之,Stream串行最慢。
但要注意:
其实比较完上述两个场景的性能表现之后,我们大约已经可以得到一个粗略的印象:
我们看这样的一个例子:单个数据集的多次数据转换操作。
首先仍然是诗人集合与评价集合:
// poets件数 int n = 100000; // evaluations件数 int m = 1000; List<Poet> poets = new ArrayList<>(); for (int i = 0; i < n; i++) { String name = String.format("诗人%010d", i + 1); poets.add(new Poet(name, (int) (80 * Math.random()) + 10, (int) (m * Math.random()) + 1)); } List<Evaluation> evaluations = new ArrayList<>(); for (int i = 0; i < m; i++) { evaluations.add(new Evaluation(i + 1, (i + 1) + "星")); }
为了避免双层遍历,我们把评价集合转换为HashMap:
Map<Integer, String> evaluationMap = evaluations.stream() .collect(Collectors.toMap(Evaluation::getEvaluation, Evaluation::getDescription));
下面我们模拟这样一段逻辑:从 poets 中找到所有评价 > m/2 的诗人,把它们拼接为"诗人名:评价描述"的字段,然后再过滤掉"诗人名:评价描述"中不包含0的记录。
虽然上述逻辑可以在一次循环中实现,但在实际开发中,往往有更复杂的逻辑导致我们经常按业务逻辑把它拆成数个循环处理。因此下面我们的模拟代码并未做一次循环搞定的优化。
System.out.println("=== 多次循环实现数据转换逻辑 ==="); LocalDateTime startTime = LocalDateTime.now(); List<Poet> betterPoets = new ArrayList<>(); for(Poet poet : poets) { if (poet.getEvaluation() > m / 2) { betterPoets.add(poet); } } List<String> poetWithEva2 = new ArrayList<>(); for(Poet poet : betterPoets) { poetWithEva2.add(poet.getName() + ":" + evaluationMap.get(poet.getEvaluation())); } List<String> poetWithEva3 = new ArrayList<>(); for(String s : poetWithEva2) { if (s != null && s.contains("0")) { poetWithEva3.add(s); } } LocalDateTime stopTime = LocalDateTime.now(); System.out.println("多次循环实现数据转换逻辑 time(millis):" + Duration.between(startTime, stopTime).toMillis());
然后我们用Stream实现相同的逻辑:
System.out.println("=== Stream实现数据转换逻辑 ==="); startTime = LocalDateTime.now(); List<String> poetWithEva = poets.stream() .filter(poet -> poet.getEvaluation() > m / 2) .map(poet -> poet.getName() + ":" + evaluationMap.get(poet.getEvaluation())) .filter(s -> s.contains("0")) .collect(Collectors.toList()); stopTime = LocalDateTime.now(); System.out.println("Stream实现数据转换逻辑 time(millis):" + Duration.between(startTime, stopTime).toMillis());
再将三次显式迭代器循环优化为一次循环:
System.out.println("=== 一次循环实现数据转换逻辑 ==="); startTime = LocalDateTime.now(); List<String> lastLst = new ArrayList<>(); for(Poet poet : poets) { if (poet.getEvaluation() > m / 2) { String tmp = poet.getName() + ":" + evaluationMap.get(poet.getEvaluation()); if (tmp.contains("0")) { lastLst.add(tmp); } } } stopTime = LocalDateTime.now(); System.out.println("一次循环实现数据转换逻辑 time(millis):" + Duration.between(startTime, stopTime).toMillis());
从运行结果上看,Stream与一次循环(迭代器)的差距微乎其微,但都比多次循环优势明显。原因当然很浅显,因为Stream也是最后一次遍历。
当然了,水平高点的程序员也是可以一次写出优化后的一次循环的,但你看两者的代码,就问你哪个优雅?哪个更容易读懂代码的目的?结果是显而易见的,Stream在易读性和可维护性上,远比显式循环的写法更有优势。
因此再强调一遍: 没有极致的性能要求的话,优先用Stream操作。
直接给结论:
关于并行模式的CPU消耗,各位在本地运行前面的性能测试代码时,可以打开本地的资源监视器,看看Stream串行与并行模式下的CPU使用率。你会发现,Stream串行与显式迭代器循环在运行时,基本上只有一个core的使用率达到100%,而并行模式时,所有core的使用率都会达到100%。如果这时你的应用有其他并发的,也比较消耗CPU的请求过来,你猜它会比平时慢呢,还是慢呢,还是慢呢?如果你的应用还是个高并发的系统,那你能否保证对CPU产生大量消耗的并行操作只发生在并发低的时间段呢?(当然是假设的你高并发系统是有高并发峰值时间段的,峰值时间段以外不存在高并发场景。。。)
做什么
就是你需要调用Stream的哪个方法,而 怎么做
就是你需要给Stream的方法传入什么样的函数,即,lambda表达式!
首先,Stream是管道流操作。从前面的Stream操作的代码示例中我们可以看到,整个Stream操作就是一个管道流操作,开始和中间操作总是返回一个新的Stream对象,后面继续对这个Stream对象进行操作,犹如接力,直到最后执行操作获得结果。
其次,Stream就如同一个迭代器(Iterator)那样,最后的Terminal对数据集的遍历是单向的,不可往复的。数据只能遍历一次,遍历过一次后就结束了,不可逆转,恰似黄河之水天上来,奔流到海不复回。
故名 Stream
。
Stream与以前的集合操作相比,不同的地方在于,以前的集合操作(包括Iterator)只能命令式的,串行的操作。而Stream具有如下特点:
这么多好处,就问你爽不爽,漫卷诗书喜欲狂了没?
从Java的并行编程API(或者说多线程编程)的角度来看,我们可以看到其在Java各个大版本中的发展壮大过程大致如下:
前面讲Lamdba表达式的标准函数式接口的时候,各位敏锐的小伙伴们应该就发现了,这些接口里面居然有已经实现了的方法。。。这是怎么回事呢?岂不是违反了Java自己关于接口没有实现方法的规定?
emmm,确实违反了,当然这是有原因的,后面我们再说。。。先看看接口里的方法实现是怎么回事。
Java8开始,你可以给接口添加default方法。如下所示:
public interface Printer { default void print() { System.out.println("众鸟高飞尽"); } default void printAnathor() { System.out.println("孤云独去闲"); } }
这些默认的实现不要求implements该接口的Class重写就可以直接使用,如下所示:
PrintClass printClass = new PrintClass(); printClass.print(); printClass.printAnathor(); ... class PrintClass implements Printer { }
当然你偏要重写接口的default方法也是没有问题的。
接口不同于抽象类,抽象类使用继承,而Java是单继承的,因此不会出现继承的方法冲突问题。但接口可以写default方法后,就有了方法冲突的可能。因为Java中一个类可以实现多个接口,那么当这些接口中有相同的default方法时,就会出现default方法冲突。
例如接口 Printer2
中也实现了方法 print
:
public interface Printer2 { default void print() { System.out.println("只有敬亭山"); } }
此时如果一个类同时实现接口 Printer
与 Printer2
:
class PrintClass2 implements Printer, Printer2 { }
此时就会因为default方法冲突而编译错误。
如何解决呢?我们可以在 PrintClass2
中重写 print
方法:
class PrintClass2 implements Printer, Printer2 { @Override public void print() { System.out.println("相看两不厌"); } }
但如果想要调用某个接口中的default方法怎么办呢?这时可以通过 Printer2.super.print();
这种特殊写法实现:
class PrintClass2 implements Printer, Printer2 { @Override public void print() { System.out.println("相看两不厌"); Printer2.super.print(); } }
总的规则如下:
Java8的接口中不仅可以写default方法,还可以写static方法:
public interface Printer2 { default void print() { System.out.println("只有敬亭山"); } static void printHello(String name) { System.out.println("Hello " + name); } static void printBye(String name) { System.out.println("Goodbye " + name); } }
调用时使用 接口.静态方法
即可:
class PrintClass2 implements Printer, Printer2 { @Override public void print() { System.out.println("相看两不厌"); Printer2.super.print(); } public void helloAndBye() { Printer2.printHello("Java8"); Printer2.printBye("Java8"); } }
Java8给接口增加默认方法是引起不同意见比较多的一个新特性。不喜欢的认为这一点破坏了Java作为面向对象语言的规范性,容易引起方法引用混乱不易维护,比如以前的老夫;喜欢的觉得增加了Java的灵活性,只要能控制住范围还挺好用的,比如现在觉得真香的老夫。。。
为什么Java要增加接口的默认方法?
但不管是以前添加新的实现类,还是现在可以直接在接口中添加默认方法,都是不可以滥用的。前者会破坏代码的类继承体系甚至引起类爆炸,导致代码难以维护;后者可能导致方法引用混乱,进而同样导致代码难以维护。运用之妙存乎一心,可以用,但不能滥用。
目前的话,老夫有个小小的建议:
前面讲Stream的时候,看到有的Terminal操作会返回一个Optional对象,我们对其进行 orElse
之类的操作。
Optional是Java8新增的用来解决NullPointerException的一个容器类,其中包含对其他对象的引用。
这货其实有点高冷,你对它不够熟悉的话,它其实不是很好用。。。熟了以后你才会觉得真香。。。
不逼逼,直接看代码。
在Java8之前,我们的代码中总需要大量的非空判断:
private void printLineOld(String line) { if (line != null) { System.out.println(line.trim()); } }
Java8之后,你可以使用 Optional
优雅的完成不够优雅的非空判断。。。
首先,你需要用Optional把不知道是不是null的对象包起来:
// 如果确定line不是null Optional<String> line1 = Optional.of(line); // 如果line是null,需要使用ofNullable Optional<String> empty = Optional.ofNullable(line);
还有其他的一些创建Optional对象的方法,这里不再一一介绍。面对未知是否是null的变量,老夫建议使用 Optional.ofNullable
将其封装起来。
然后,在使用变量的地方,改为使用Optional对象:
// 假设 line 是一个 Optional<String> 类型的对象 try { System.out.println(line.get().trim()); } catch (NoSuchElementException e) { System.out.println("Optional.get 如果line是null,get会抛NoSuchElementException异常!"); } // 仅在原来对象非null时执行传入的lambda表达式 line.ifPresent(s -> System.out.println(s.trim())); // 利用orElse,当原来对象是null时,使用orElse传入的默认值 System.out.println(line.orElse("")); // 利用orElseGet,当原来对象是null时,使用orElseGet传入的lambda表达式 System.out.println(line.orElseGet(() -> "天生我材必有用," + "千金散尽还复来。")); // 利用orElseThrow,当原来对象是null时,抛出自己定义的异常 System.out.println(line.orElseThrow(() -> new RuntimeException("也可以抛出自己定义的异常!")));
其中:
但要注意的是,使用 Optional
需要一个正确的打开姿势。。。
先看一个 不正确 的姿势:
// 不推荐将参数类型设计为Optional,Optional适合用于返回值类型 public void printLine(Optional<String> line) { ... }
Optional.ofNullable
显式传入参数呢?你挡不住人家放飞自我直接传 null
的。。。
除此以外, Optional
也尽量不要用于实例变量,因为它不能被序列化,当做字段属性时可能会出问题。
来,看看正确的打开姿势:
private void test02_returnOptional(String line) { Optional<String> lineOpt = createLineOptional(line); // 仅在原来对象非null时执行传入的lambda表达式 lineOpt.ifPresent(s -> System.out.println(s.trim())); // 利用orElse,当原来对象是null时,使用orElse传入的默认值 System.out.println(lineOpt.orElse("")); // 利用orElseGet,当原来对象是null时,使用orElseGet传入的lambda表达式 System.out.println(lineOpt.orElseGet(() -> "天生我材必有用," + "千金散尽还复来。")); // 利用orElseThrow,当原来对象是null时,抛出自己定义的异常 System.out.println(lineOpt.orElseThrow(() -> new RuntimeException("也可以抛出自己定义的异常!"))); } private Optional<String> createLineOptional(String line) { // 实际开发中,这里也许会有比较复杂的逻辑,用于返回一个对象,而该方法不保证返回对象不为null; // 因此使用该方法的地方必须判断返回值是否为null。。。 // 但如果我们将返回值用Optional包起来,那么对于调用该方法的地方而言,非空判断就可以很优雅了。 return Optional.ofNullable(line); }
之前讲Stream的时候,细心的小伙伴会发现,没有从Map生成Stream的操作。是的,Map没有stream()方法,没法直接获取Map的Stream对象,因为Java到现在也不支持元组甚至二维元组,因此Map的元素键值对(key, value)没法作为Stream<T>的泛型来使用。。。
当然,Java8的Map提供了一些新的方法来满足我们日常操作的需要。
我们先看看一个集合(List或Set)如何转换为Map。
// 还是之前的诗人集合 List<Poet> poets = Poet.preparePoets(); // 利用 Collectors.toMap 将Stream中的数据集转换为Map Map<String, Poet> poetMap = poets.stream().collect(Collectors.toMap(Poet::getName, poet -> poet));
之前Stream的示例代码中也有类似的例子。。。强大的Collectors大家要多多亲近。。。
接下来,让我们看看Map都有哪些好用的新方法:
poetMap.forEach((s, poet) -> { System.out.printf("%s 活了 %s 岁。 %n", s, poet.getAge()); System.out.printf("%s 评价 : %s 。 %n", s, poet.getEvaluation()); });
这个看起来已经很像一个二维元组了。。。
Poet censhen = poetMap.get("岑参"); if (censhen == null) { censhen = new Poet("岑参", 51, 4); poetMap.put("岑参", censhen); } System.out.println(censhen); // 上面的代码现在可以直接使用 putIfAbsent 了。 poetMap.putIfAbsent("岑参", new Poet("岑参", 51, 5)); // 结果 "岑参" 的评价依旧是 4 而不是 5,因为 putIfAbsent 不会替换已经存在的value。 System.out.println(poetMap.get("岑参"));
比较一下以前的写法和现在的写法,是不是优雅了很多?优雅就是战斗力,优雅即正义。。。
// "岑参"已经加入了poetMap poetMap.computeIfPresent("岑参", (s, poet) -> new Poet(s, 51,4)); // computeIfPresent会替换已经存在的value System.out.println(poetMap.get("岑参")); // "孟浩然"尚未加入poetMap poetMap.computeIfPresent("孟浩然", (s, poet) -> new Poet(s, 51,3)); // computeIfPresent只在key已经存在时替换value System.out.println(poetMap.containsKey("孟浩然"));
poetMap.computeIfAbsent("孟浩然", s -> new Poet(s, 51,3)); System.out.println(poetMap.get("孟浩然"));
computeIfAbsent 与 putIfAbsent 区别在于传入参数不同,一个是lambda表达式,一个是具体的value。
poetMap.remove("孟浩然", new Poet("孟浩然", 51,3)); // 删除失败,因为value不是一个对象 System.out.println(poetMap.containsKey("孟浩然")); poetMap.remove("孟浩然", poetMap.get("孟浩然")); // 删除成功 System.out.println(poetMap.containsKey("孟浩然"));
System.out.println(poetMap.getOrDefault("孟浩然", new Poet("XX", 20, 1)));
Map<String, String> lines = new HashMap<>(); lines.merge("杜甫名句", "星垂平野阔,", (value, newValue) -> value.concat(newValue)); System.out.println(lines.get("杜甫名句")); lines.merge("杜甫名句", "月涌大江流。", String::concat); System.out.println(lines.get("杜甫名句"));
Java8对HashMap的性能也做了一定的优化。
这节都是理论,对HashMap机制不熟悉的小伙伴要回头自己补补课了。。。
不管我们知道还是假装知道hashmap的机制,这里都简单回顾一下(Java8之前):
hash值
与 数组长度-1
的 与运算
得到每个key在数组中的存储下标; 0.75
,数组长度超过 容量×负载系数
时,HashMap就会乘2扩容,即2的指数加1,然后大家重新排排坐(重新算下标)。 在Java8以前,HashMap的性能瓶颈主要有两个地方:
在Java8中,对这两点做了一定的优化:
扩容时,可能会导致红黑树又被拆分为两个链表。
Java 8 在包java.time下包含了一组全新的时间日期API,功能更强大,也更安全。
// 系统Clock对象 采用系统默认时区 Clock clock = Clock.systemDefaultZone(); System.out.println(clock); // 系统当前微妙数 long millis = clock.millis(); System.out.println(millis); // 获取以前的Date对象 Instant instant = clock.instant(); Date legacyDate = Date.from(instant); System.out.println(legacyDate); // 获取可用时区 System.out.println(ZoneId.getAvailableZoneIds()); // 获取指定时区 ZoneId zoneSh = ZoneId.of("Asia/Shanghai"); System.out.println(zoneSh.getRules()); ZoneId zoneTk = ZoneId.of("Asia/Tokyo"); System.out.println(zoneTk.getRules()); ZoneId zoneNy = ZoneId.of("America/New_York"); System.out.println(zoneNy.getRules());
LocalTime、LocalDate与LocalDateTime都是Java8提供的新的日期API,它们具有如下特点:
// LocalTime 没有年月日和时区信息,只有时分秒及以下 LocalTime localTimeNowDefault = LocalTime.now(ZoneId.systemDefault()); System.out.println(localTimeNowDefault); LocalTime localTimeNowTk = LocalTime.now(ZoneId.of("Asia/Tokyo")); System.out.println(localTimeNowTk); // 计算时间差 long hoursBetween = ChronoUnit.HOURS.between(localTimeNowDefault, localTimeNowTk); System.out.println(hoursBetween); long minutesBetween = ChronoUnit.MINUTES.between(localTimeNowDefault, localTimeNowTk); System.out.println(minutesBetween); // 获取一个任意时间的 LocalTime LocalTime late = LocalTime.of(23, 59, 59); System.out.println(late); // 根据格式转换字符串为 LocalTime (因为LocalTime只有小时以下,因此格式有限制,只能用FormatStyle.SHORT) DateTimeFormatter dtf_localtime = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) .withLocale(Locale.GERMAN); LocalTime leetTime = LocalTime.parse("13:37", dtf_localtime); System.out.println(leetTime);
// LocalDate 年月日 LocalDate today = LocalDate.now(); LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS); LocalDate yesterday = tomorrow.minusDays(2); LocalDate new_year_day = LocalDate.of(2020, Month.JANUARY, 1); DayOfWeek dayOfWeek = new_year_day.getDayOfWeek(); System.out.printf("今天是%s,明天是%s,昨天是%s,元旦是%s,%s。 %n", today, tomorrow, yesterday, new_year_day, dayOfWeek); // 格式化 DateTimeFormatter dtf_localdate = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.GERMAN); LocalDate children_day = LocalDate.parse("01.06.2020", dtf_localdate); System.out.println(children_day);
// LocalDateTime 日期加时间 LocalDateTime now = LocalDateTime.now(); System.out.println(now); LocalDateTime laborDay = LocalDateTime.of(2020, Month.MAY, 1, 14, 41, 3); System.out.println(laborDay); System.out.println(laborDay.getDayOfWeek()); System.out.println(laborDay.getMonth()); System.out.println(laborDay.getLong(ChronoField.MINUTE_OF_DAY)); // 通过时间点Instance对象转换为Date Instant laborInstant = laborDay.atZone(ZoneId.systemDefault()).toInstant(); Date laborDate = Date.from(laborInstant); System.out.println(laborDate); // 自定义格式化 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); String strNow = formatter.format(LocalDateTime.now()); System.out.println(strNow); LocalDateTime ldtNow = LocalDateTime.parse(strNow, formatter); System.out.println(ldtNow); // 计算时间差 System.out.println(ChronoUnit.DAYS.between(ldtNow, laborDay)); System.out.println(Duration.between(ldtNow, laborDay).toDays());
Java8之前,多线程开发中,如果主线程需要子线程结束后再进行下一步的处理,那么只能同步阻塞的等待,无论你是在主线程中调用子线程的join方法,还是用Future的get方法。
Java8增加了新的 CompletableFuture
类,可以配合lamda表达式,给子线程传入函数用于子线程执行结束后的回调。
看个简单的例子:
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return "明月出天山,苍茫云海间。"; }); completableFuture.thenApply(s -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return s.concat("/n").concat("长风几万里,吹度玉门关。"); }).thenApply(s -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return s.concat("/n").concat("汉下白登道,胡窥青海湾。"); }).thenApply(s -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return s.concat("/n").concat("由来征战地,不见有人还。"); }).thenApply(s -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return s.concat("/n").concat("戍客望边邑,思归多苦颜。"); }).thenApply(s -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return s.concat("/n").concat("高楼当此夜,叹息未应闲。"); }).thenAccept(System.out::println); System.out.println("关山月 唐 李白"); try { Thread.sleep(8000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("==================");
CompletableFuture.supplyAsync
定义了一个子线程,异步执行传入的lamda表达式,它返回一个CompletableFuture对象。 supplyAsync
方法被重载为两个方法,一个如上面示例,只有一个参数。另一个重载的方法有两个参数,一个是传入的子线程处理逻辑(lambda表达式),另一个是线程池对象。不传入线程池对象时,使用默认线程池(对于多核机器来说是一个forkjoin线程池)。
CompletableFuture对象的 thenApply
方法传入了一个回调函数,这个回调函数会在子线程执行结束后被子线程回调,且回调函数以子线程的执行返回为入参,并返回本次回调处理的结果。可以看到,当连续用 thenApply
方法传入多个回调函数时,这些回调函数会被串行回调。
而CompletableFuture对象的 thenAccept
传入的回调函数只接收子线程的执行结果,本身没有返回值。
一串的 thenApply
最后接一个 thenAccept
是一种常见用法。
再看一个例子:
CompletableFuture<Double> futurePrice = CompletableFuture.supplyAsync(() -> { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } double price = Math.random() * 100; System.out.println("Price is " + price); return price; }); CompletableFuture<Integer> futureCount = CompletableFuture.supplyAsync(() -> { try { Thread.sleep((long) (Math.random() * 1000)); } catch (InterruptedException e) { e.printStackTrace(); } int count = (int) (Math.random() * 100); System.out.println("Count is " + count); return count; }); CompletableFuture<Double> futureTotal = futurePrice.thenCombine(futureCount, (price, count) -> price * count); futureTotal.thenAccept(total -> System.out.println("Total is " + total)); System.out.println("鬼知道要多久。。。该干嘛干嘛去。。。"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
这个例子中,我们需要先计算出price和count,然后相乘得到总价。假设求取price和count的处理不知何时方能完成。
所以我们先分别异步执行price和count的子线程,然后通过 thenCombine
方法,执行这样一个逻辑:等两个子线程都结束以后,将它们的返回值作为参数执行回调函数。
这样我们就达成了等两个子线程都结束后再回调的逻辑,同时主线程依然该干嘛干嘛,不会阻塞。
CompletableFuture提供了很多方法,上面我们的例子理解之后,就可以自行去看看这些方法都是什么功能,适用于什么场景了:
Java8还有很多新特性,比如多重注解,Arrays.parallelSort,StampedLock等等,这里不再一一介绍,有需要的小伙伴可以自行学习。
因为java9和Java10都是过渡版本,我们直接以Java11(Java8之后第一个LTS版本)为边界来讲讲9到11有哪些比较影响我们开发的新特性。
Java11相对Java8,在语法上的新特性并不多。主要有:
Java10以后可以用var定义一个局部变量,不用显式写出它的类型。但要注意,被var定义的变量仍然是静态类型,编译器会试图去推断其类型。
String strBeforeJava10 = "strBeforeJava10"; var strFromJava10 = "strFromJava10"; System.out.println(strBeforeJava10); System.out.println(strFromJava10);
因此,要注意:
// 例如下面的语句编译会失败,"InCompatible types." strFromJava10 = 10;
// 例如下面这些都无法通过编译: var testVarWithoutInitial; var testNull = null; var testLamda = () -> System.out.println("test"); var testMethodByLamda = () -> giveMeString(); var testMethod2 = this::giveMeString;
而推荐使用类型推断的场景有:
// 如下所示,Map <String,List <Integer >>类型,可以被简化为单个var关键字 var testList = new ArrayList<Map<String, List<Integer>>>(); for (var curEle : testList) { // curEle能够被推断出类型是 Map<String, List<Integer>> if (curEle != null) { curEle.put("test", new ArrayList<>()); } }
// 从Java 11开始,lambda参数也允许使用var关键字: Predicate<String> predNotNull = (var a) -> a != null && a.trim().length() > 0; String strAfterFilter = Arrays.stream((new String[]{"a", "", null, "x"})) .filter(predNotNull) .collect(Collectors.joining(",")); System.out.println(strAfterFilter);
Java 9开始引入HttpClient API来处理HTTP请求。 从Java 11开始,这个API正式进入标准库包。参考网址: http://openjdk.java.net/groups/net/httpclient/intro.html
HttpClient具有以下特性:
要发送http请求,首先要使用其构建器创建一个HttpClient。这个构建器能够配置每个客户端的状态:
一旦构建完成,就可以使用HttpClient发送多个请求。
HttpRequest是由它的构建器创建的。请求的构建器可用于设置:
HttpRequest构建之后是不可变的,但可以发送多次。
请求既可以同步发送,也可以异步发送。当然同步的API会导致线程阻塞直到HttpResponse可用。异步API立即返回一个CompletableFuture,当HttpResponse可用时,它将获取HttpResponse并执行后续处理。
CompletableFuture是Java 8添加的新特性,用于可组合的异步编程。
请求和响应的主体作为响应式流(具有非阻塞背压的异步数据流)供外部使用。HttpClient实际上是请求正文的订阅者和响应正文字节的发布者。BodyHandler接口允许在接收实际响应体之前检查响应代码和报头,并负责创建响应BodySubscriber。
HttpRequest和HttpResponse类型提供了许多便利的工厂方法,用于创建请求发布者和响应订阅者,以处理常见的主体类型,如文件、字符串和字节。这些便利的实现要么累积数据,直到可以创建更高级别的Java类型(如String),要么就文件流传输数据。BodySubscriber和BodyPublisher接口可以实现为自定义反应流处理数据。
HttpRequest和HttpResponse还提供了转换器,用于将 java.util.concurrent.Flow 的 Publisher/Subscriber 类型转换为 HTTP Client的 BodyPublisher/BodySubscriber 类型。
Java HTTP Client支持 HTTP/1.1 和 HTTP/2。默认情况下,客户端将使用 HTTP/2 发送请求。发送到尚不支持 HTTP/2 的服务器的请求将自动降级为 HTTP/1.1。以下是HTTP/2带来的主要改进:
由于HTTP/2是默认的首选协议,并且在需要的地方无缝地实现回退到HTTP/1.1,那么当HTTP/2被更广泛地部署时,Java HTTP客户端就无需修正它的应用代码。
https://docs.oracle.com/en/ja...
代码中请求的网址中, localhost:30001
的相关uri来自工程 https://github.com/zhaochuninhefei/study-czhao/tree/master/jdk11-test
。
package jdk11; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.WebSocket; import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.TimeUnit; /** * HttpClient * * @author zhaochun */ public class TestCase02HttpClient { public static void main(String[] args) throws Exception { TestCase02HttpClient me = new TestCase02HttpClient(); me.testHttpClientGetSync(); me.testHttpClientGetAsync(); me.testHttpClientPost(); // 同一个HttpClient先登录网站获取token,再请求受限制资源,从而爬取需要认证的资源 me.testLogin(); // HttpClient支持websocket me.testWebsocket(); } private void testHttpClientGetSync() { var url = "https://openjdk.java.net/"; var request = HttpRequest.newBuilder() .uri(URI.create(url)) .GET() .build(); var client = HttpClient.newHttpClient(); try { System.out.println(String.format("send begin at %s", LocalDateTime.now())); // 同步请求 HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(String.format("send end at %s", LocalDateTime.now())); System.out.println(String.format("receive response : %s", response.body().substring(0, 10))); } catch (Exception e) { e.printStackTrace(); } } private void testHttpClientGetAsync() { var url = "https://openjdk.java.net/"; var request = HttpRequest.newBuilder() .uri(URI.create(url)) .GET() .build(); var client = HttpClient.newHttpClient(); try { System.out.println(String.format("sendAsync begin at %s", LocalDateTime.now())); // 异步请求 client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(stringHttpResponse -> { System.out.println(String.format("receive response at %s", LocalDateTime.now())); return stringHttpResponse.body(); }) .thenAccept(s -> System.out.println(String.format("receive response : %s at %s", s.substring(0, 10), LocalDateTime.now()))); System.out.println(String.format("sendAsync end at %s", LocalDateTime.now())); // 为了防止异步请求尚未返回主线程就结束(jvm会退出),这里让主线程sleep 10秒 System.out.println("Main Thread sleep 10 seconds start..."); Thread.sleep(10000); System.out.println("Main Thread sleep 10 seconds stop..."); } catch (Exception e) { e.printStackTrace(); } } private void testHttpClientPost() { var url = "http://localhost:30001/jdk11/test/helloByPost"; var request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Content-Type", "text/plain") .POST(HttpRequest.BodyPublishers.ofString("zhangsan")) .build(); var client = HttpClient.newHttpClient(); try { HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); System.out.println(response.body()); } catch (Exception e) { e.printStackTrace(); } } private void testLogin() throws Exception { var client = HttpClient.newHttpClient(); // 某测试环境用户登录URL var urlLogin = "http://x.x.x.x:xxxx/xxx/login"; var requestObj = new HashMap<String, Object>(); requestObj.put("username", "xxxxxx"); requestObj.put("password", "xxxxxxxxxxxxxxxx"); var objectMapper = new ObjectMapper(); var requestBodyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(requestObj); var requestLogin = HttpRequest.newBuilder() .uri(URI.create(urlLogin)) .header("Content-Type", "application/json;charset=UTF-8") .POST(HttpRequest.BodyPublishers.ofString(requestBodyJson)) .build(); HttpResponse<String> responseLogin = client.send(requestLogin, HttpResponse.BodyHandlers.ofString()); // 这里的登录网站使用token,而没有使用session,因此我们需要从返回的报文主体中查找token信息; // 如果是使用session的网站,这里需要从响应的headers中查找"set-cookie"从而获取session id,并在后续请求中,将sid设置到header的Cookie中。 // 如: responseLogin.headers().map().get("set-cookie")获取cookies,再从中查找sid。 var loginResponse = responseLogin.body(); var mpLoginResponse = objectMapper.readValue(loginResponse, Map.class); var dataLogin = (Map<String, Object>) mpLoginResponse.get("data"); var token = dataLogin.get("token").toString(); // 测试环境获取某资源的URL var urlGetResource = "http://xxxx:xxxx/xxx/resource"; var requestRes = HttpRequest.newBuilder() .uri(URI.create(urlGetResource)) .header("Content-Type", "application/json;charset=UTF-8") // 注意,token并非一定设置到header的Authorization中,这取决于网站验证的方式,也有可能token也放到cookie里。 // 但对于使用session的网站,sid都是设置在cookie里的。如: .header("Cookie", "JSESSIONID=" + sid) .header("Authorization", token) .GET() .build(); HttpResponse<String> responseResource = client.send(requestRes, HttpResponse.BodyHandlers.ofString()); var response = responseResource.body(); System.out.println(response); } private void testWebsocket() { var wsUrl = "ws://localhost:30001/ws/test"; var httpClient = HttpClient.newHttpClient(); WebSocket websocketClient = httpClient.newWebSocketBuilder() .buildAsync(URI.create(wsUrl), new WebSocket.Listener() { @Override public void onOpen(WebSocket webSocket) { System.out.println("onOpen : webSocket opened."); webSocket.request(1); } @Override public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) { System.out.println("onText"); webSocket.request(1); return CompletableFuture.completedFuture(data) .thenAccept(System.out::println); } @Override public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) { System.out.println("ws closed with status(" + statusCode + "). cause:" + reason); webSocket.sendClose(statusCode, reason); return null; } @Override public void onError(WebSocket webSocket, Throwable error) { System.out.println("error: " + error.getLocalizedMessage()); webSocket.abort(); } }).join(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } // last参数用于指示websocketClient,本次发送的数据是否是完整消息的最后部分。 // 如果是false,则websocketClient不会把消息发送给websocket后台的listener,只会把数据缓存起来; // 当传入true时,会将之前缓存的数据和这次的数据拼接起来一起发送给websocket后台的listener。 websocketClient.sendText("test1", false); websocketClient.sendText("test2", true); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } websocketClient.sendText("org_all_request", true); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } websocketClient.sendText("employee_all_request", true); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } websocketClient.sendClose(WebSocket.NORMAL_CLOSURE, "Happy ending."); } }
List,Set,Map有了新的增强方法: of
与 copyOf
。
List.of根据传入的参数列表创建一个新的不可变List集合;List.copyOf根据传入的list对象创建一个不可变副本。
var listImmutable = List.of("a", "b", "c"); var listImmutableCopy = List.copyOf(listImmutable);
由于拷贝的集合本身就是一个不可变对象,因此拷贝实际上并没有创建新的对象,直接使用了原来的不可变对象。
// 结果为true System.out.println(listImmutable == listImmutableCopy); // 不可变对象不能进行修改 try { listImmutable.add("d"); } catch (Throwable t) { System.out.println("listImmutable can not be modified!"); } try { listImmutableCopy.add("d"); } catch (Throwable t) { System.out.println("listImmutableCopy can not be modified!"); }
如果想快速新建一个可变的集合对象,可以直接使用之前的不可变集合作为构造参数,创建一个新的可变集合。
var listVariable = new ArrayList<>(listImmutable); var listVariableCopy = List.copyOf(listVariable);
新创建的可变集合当然是一个新的对象,从这个新对象拷贝出来的不可变副本也是一个新的对象,并不是之前的不可变集合。
System.out.println(listVariable == listImmutable); // false System.out.println(listVariable == listVariableCopy); // false System.out.println(listImmutable == listVariableCopy); // false // 新的可变集合当然是可以修改的 try { listVariable.add("d"); } catch (Throwable t) { System.out.println("listVariable can not be modified!"); } // 可变集合拷贝出来的副本依然是不可变的 try { listVariableCopy.add("d"); } catch (Throwable t) { System.out.println("listVariableCopy can not be modified!"); }
Set的of和copyOf与List类似。
var set = Set.of("a", "c", "r", "e"); var setCopy = Set.copyOf(set); System.out.println(set == setCopy);
但要注意,用of创建不可变Set时,要确保元素不重复,否则运行时会抛出异常: "java.lang.IllegalArgumentException: duplicate element"
try { var setErr = Set.of("a", "b", "a"); } catch (Throwable t) { t.printStackTrace(); }
当然创建可变set后添加重复元素不会抛出异常,但会被去重
var setNew = new HashSet<>(set); setNew.add("c"); System.out.println(setNew.toString());
Map的of和copyOf与list,set类似,注意of方法的参数列表是依次传入key和value:
var map = Map.of("a", 1, "b", 2); var mapCopy = Map.copyOf(map); System.out.println(map == mapCopy);
当然也要注意创建不可变Map时,key不能重复
try { var mapErr = Map.of("a", 1, "b", 2, "a", 3); } catch (Throwable t) { t.printStackTrace(); }
Java8开始引入的stream,Java11提供了一些扩展:
注意null与""的区别:
long size1 = Stream.ofNullable(null).count(); System.out.println(size1); // 0 long size2 = Stream.ofNullable("").count(); System.out.println(size2); // 1
dropWhile,对于有序的stream,从头开始去掉满足条件的元素,一旦遇到不满足元素的就结束
List lst1 = Stream.of(1, 2, 3, 4, 5, 4, 3, 2, 1) .dropWhile(e -> e < 3) .collect(Collectors.toList()); System.out.println(lst1); // [3, 4, 5, 4, 3, 2, 1]
takeWhile,对于有序的stream,从头开始保留满足条件的元素,一旦遇到不满足的元素就结束
List lst2 = Stream.of(1, 2, 3, 4, 5, 4, 3, 2, 1) .takeWhile(e -> e < 3) .collect(Collectors.toList()); System.out.println(lst2); // [1, 2]
即使把剩下的元素都收集到了无序的set中,但在此之前,stream对象是有序的,因此结果包含了原来stream中最后的[a2]和[a1]:
Set set1 = Stream.of("a1", "a2", "a3", "a4", "a5", "a4", "a3", "a2", "a1") .dropWhile(e -> "a3".compareTo(e) > 0) .collect(Collectors.toSet()); System.out.println(set1); // [a1, a2, a3, a4, a5]
如果先创建一个无序不重复的set集合,set无序更准确的说法是不保证顺序不变,事实上是有顺序的。
因此这里会发现,dropWhile还是按set当前的元素顺序判定的,一旦不满足条件就结束。
Set<String> set = new HashSet<>(); for (int i = 1; i <= 100 ; i++) { set.add("test" + i); } System.out.println(set); Set setNew = set.stream() .dropWhile(s -> "test60".compareTo(s) > 0) .collect(Collectors.toSet()); System.out.println(setNew);
java8里可以创建一个无限流,比如下面这个数列,起始值是1,后面每一项都在前一项的基础上 * 2 + 1,通过limit限制这个流的长度:
Stream<Integer> streamInJava8 = Stream.iterate(1, t -> 2 * t + 1); // 打印出该数列的前十个: 1,3,7,15,31,63,127,255,511,1023 System.out.println(streamInJava8.limit(10).map(Object::toString).collect(Collectors.joining(",")));
从Java9开始,iterate方法可以添加一个判定器,例如,限制数的大小不超过1000
Stream<Integer> streamFromJava9 = Stream.iterate(1, t -> t < 1000, t -> 2 * t + 1); // 这里打印的结果是 1,3,7,15,31,63,127,255,511 System.out.println(streamFromJava9.map(Objects::toString).collect(Collectors.joining(",")));
Optional.of("Hello openJDK11").stream() .flatMap(s -> Arrays.stream(s.split(" "))) .forEach(System.out::println);
System.out.println(Optional.empty() .or(() -> Optional.of("default")) .get());
String方面,针对空白字符(空格,制表符,回车,换行等),提供了一些新的方法。
判断目标字符串是否是空白字符。以下结果全部为 true
:
// 半角空格 System.out.println(" ".isBlank()); // 全角空格 System.out.println(" ".isBlank()); // 半角空格的unicode字符值 System.out.println("/u0020".isBlank()); // 全角空格的unicode字符值 System.out.println("/u3000".isBlank()); // 制表符 System.out.println("/t".isBlank()); // 回车 System.out.println("/r".isBlank()); // 换行 System.out.println("/n".isBlank()); // 各种空白字符拼接 System.out.println(" /t/r/n ".isBlank());
去除首尾的空白字符:
// 全角空格 + 制表符 + 回车 + 换行 + 半角空格 + <内容> + 全角空格 + 制表符 + 回车 + 换行 + 半角空格 var strTest = " /t/r/n 你好 jdk11 /t/r/n "; // strip 去除两边空白字符 System.out.println("[" + strTest.strip() + "]"); // stripLeading 去除开头的空白字符 System.out.println("[" + strTest.stripLeading() + "]"); // stripTrailing 去除结尾的空白字符 System.out.println("[" + strTest.stripTrailing() + "]");
重复字符串内容,拼接新的字符串:
var strOri = "jdk11"; var str1 = strOri.repeat(1); var str2 = strOri.repeat(3); System.out.println(str1); System.out.println(str2); // repeat传入参数为1时,不会创建一个新的String对象,而是直接返回原来的String对象。 System.out.println(str1 == strOri);
lines方法用 r 或 n 或 rn 对字符串切割并返回stream对象:
var strContent = "hello java/rhello jdk11/nhello world/r/nhello everyone"; // lines方法用 /r 或 /n 或 /r/n 对字符串切割并返回stream对象 strContent.lines().forEach(System.out::println); System.out.println(strContent.lines().count());
InputStream提供了一个新的方法 transferTo
,将输入流直接传输到输出流:
inputStream.transferTo(outputStream);
package jdk11; import java.io.*; /** * InputStream增强 * * @author zhaochun */ public class TestCase07InputStream { public static void main(String[] args) { TestCase07InputStream me = new TestCase07InputStream(); me.test01_transferTo(); } private void test01_transferTo() { var filePath = "/home/work/sources/test/jdk11-test/src/main/resources/application.yml"; var tmpFilePath = "/home/work/sources/test/jdk11-test/src/main/resources/application.yml.bk"; File tmpFile = new File(tmpFilePath); if (tmpFile.exists() && tmpFile.isFile()) { tmpFile.delete(); } try(InputStream inputStream = new FileInputStream(filePath); OutputStream outputStream = new FileOutputStream(tmpFilePath)) { // transferTo将 InputStream 的数据直接传输给 OutputStream inputStream.transferTo(outputStream); } catch (IOException e) { e.printStackTrace(); } } }
Java9到Java11还有一些其他的新特性,比如模块化开发,REPL交互式编程,单文件源代码程序的直接执行,新的垃圾回收器等等,对目前的开发来说,影响比较小,有兴趣的小伙伴可以查阅老夫另一篇文章:
https://segmentfault.com/a/1190000022654702