萬萬沒想到,都0202年了,Sun都亡了,老夫還要從Java5的新特性開始寫,還要重點寫Java8的新特性。。。html
其實網上這種玩意一大堆,爲啥老夫還要寫呢?java
這個速成版資料特色有:node
固然,光說不練假把式,這個速成版資料全部示例代碼位於下面的github或gitee倉庫,請各位自行下載,在本地準備好Java與IDE後,自行參考並練習:(代碼位於src/test/java
下)
https://github.com/zhaochuninhefei/study-czhao/tree/master/jdk11-test
或 :https://gitee.com/XiaTangShaoBing/study/tree/master/jdk11-test
python
這一章主要快速地講一下Java5到Java7在語法上的一些重要的新特性,以及一些重要的新的類庫API。react
Java5新特性比較多,但大部分咱們都已經很熟悉了,簡單過一下:git
泛型即參數化類型(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運行時,所以能夠結合其餘手段(如反射)來影響實際運行的代碼邏輯。因此註解的目的通常來講有二:一則規範代碼;二則動態注入(須要配合其餘手段實現)。程序員
一般註解能夠分爲四類:github
- Java自帶的標準註解,如@Override、@Deprecated、@SuppressWarnings等,一般編譯時就會依據這些註解對代碼進行檢查;
- 元註解,用於定義註解的註解,包括@Retention、@Target、@Inherited、@Documented等。
- 第三方註解,如spring,mybatis,lombok都提供了本身的註解。
- 自定義註解,使用@interface與元註解配合定義。
// 編譯器看到 @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(); } }
其餘諸如ProcessBuilder,Scanner,加強反射,加強集合框架,StringBuilder,concurrent併發工具包,等等,由於要麼用得少,要麼你們已經很熟悉了,這裏就再也不一一介紹了。web
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(); } }
Java7還有一些其餘的新特性,但對開發影響不大,這裏就不一一講述了。
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();
test02_finalVars是前面例子裏的一個實例方法。
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的時候會到處看到它們的身影。
若是你如今就去看這些接口的源碼,你會發現它們雖然都只定義了一個抽象方法,但內部每每還有一些default的實例方法。是否是有點懵逼,接口不是沒有實例方法的嗎?這個咱們後面講到Java8的另外一個新特性(接口默認方法)的時候再具體說。
Java8中新增的Stream API是對集合(Collection)對象功能的加強,它專一於對集合對象進行各類很是便利、高效的聚合操做(aggregate operation),或者大批量數據操做 (bulk data operation)。Stream API 藉助於一樣新出現的 Lambda 表達式,極大的提升了編程效率和程序可讀性。同時它提供串行和並行兩種模式進行匯聚操做,併發模式可以充分利用多核處理器的優點,使用fork/join並行方式(Java7的新特性,由於不多直接用,咱們沒有講這個) 來拆分任務和加速處理過程。一般編寫並行代碼很難並且容易出錯, 但使用 Stream API 無需編寫一行多線程的代碼,就能夠很方便地寫出高性能的併發程序。因此說,Java 8 中首次出現的 java.util.stream 是一個函數式語言+多核時代綜合影響的產物。
所謂聚合操做,就是對數據集的各類統計操做,好比:平均值,總和,最小,最大,計數等等。在咱們開發的信息系統中,這些聚合操做每每是經過關係型數據庫的各類查詢SQL完成的。若是想在Java應用中完成這些操做,那麼咱們須要本身開發集合操做,經過不停的顯式地對集合進行遍歷,循環執行運算邏輯來達成。這些程序不但開發繁瑣,也不易維護,同時不當心就會出現性能問題。而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操做示例的時候會有所介紹,沒必要着急。
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反覆操做,演示以下:
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對象只能操做一次,爲什麼這裏鏈式屢次操做?
由於 map, filter這些方法是Intermediate操做,返回了一個新的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有時與串行結果不同。想一想爲何。
parallelStream使用要謹慎,並非全部的運算均可以並行執行的。
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呢?由於用起來麻煩。。。仍是簡單說一下吧。。。
- 首先你須要像線程池那樣定義一個ForkJoinPool,而後定義一個執行任務的ForkJoinTask,在ForkJoinPool中提交這個ForkJoinTask;
- 而後你的ForkJoinTask須要本身實現什麼樣的條件或閾值下,把你要處理的數據集拆開,對應new 幾個新的ForkJoinTask,而後調用這些子task的fork方法,再調用它們的join方法(即分而治之);
- Fork/Join中關鍵的機制叫作Work-stealing策略,它將子任務放到不一樣的雙端隊列中,每一個隊列對應一個線程去獲取並執行隊列中的子任務。所謂雙端隊列,就是正常來講線程從隊列的一端獲取接下來要執行的子任務,而當某個線程空閒時,它會從其餘線程的隊列的另外一端偷子任務來執行。。。Work-stealing的優點是能充分利用線程進行並行計算;缺點是隊列中任務較少時,爲了不線程對子任務的競爭,須要同步機制,此時會產生額外的性能損耗。(因此後面咱們驗證Stream的性能時,會發現,數據量較少時,parallelStream有時會更慢,就有這裏所說的緣由。)
在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串行都要更好)。
對於單個數據集的遍從來說,從下面的示例代碼中,咱們能夠發現會影響性能表現的約束條件最少包括如下幾點:
- 機器硬件條件,好比是否是多核,核數有多少。(兩核未必能保證並行比串行效率高,由於要考慮線程上下文切換的損耗。)
- 數據集件數,數據集的件數在不一樣量級(百件,千件,萬件,十萬,百萬,千萬。。。)下,不一樣的遍歷方式的性能表現的差異是顯著的。
- 單次循環的耗時,好比納秒級別的耗時,那麼Stream的並行模式並沒有優點(一樣由於線程上下文切換的損耗),但耗時達到數百毫秒的級別時,並行模式的優點就至關明顯了。(固然要在多核機器上運行)
對於下面的代碼,建議你們多嘗試一下不一樣的約束條件,好比:
- sleep時間調整,例如,從沒有sleep,到sleep 500毫秒;
- 數據集件數調整,例如,從1百件,到1千,1萬,10萬,百萬,千萬。。。(固然,件數大的時候適當減小sleep甚至去除sleep,省得跑過久)
- 不一樣硬件條件的機器,這個有條件的能夠試試CPU核數差距較大的機器上運行並行模式的結果,沒有條件就算了。
另外,代碼中的
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串行最慢。
但要注意:
- 上述三個join操做並無考慮空間換時間的算法優化,好比將evaluations先轉換到HashMap中,以後在遍歷poets時,經過HashMap直接獲取目標evaluation。沒有考慮這個優化,是由於這裏相比較的就是Stream的隱式雙層遍歷與之前的顯式雙層遍歷之間的性能表現。利用HashMap的優化方法,這三者均可以使用。。。
- 顯式雙層遍歷就沒有考慮fori循環了,由於fori的性能原本就比不上迭代器循環,不必在這裏丟人現眼了。。。
- 數據集數量較大仍是較小的判斷標準取決於硬件環境,不能一律而論。
- 上述測試比較簡陋,每種case都只測了一次。若是你們有時間,建議每種數據量的case都測試10次以上取平均值。
其實比較完上述兩個場景的性能表現以後,咱們大約已經能夠獲得一個粗略的印象:
但這裏老夫仍然要說,沒有極致的性能要求的話,優先用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在易讀性和可維護性上,遠比顯式循環的寫法更有優點。
所以再強調一遍:沒有極致的性能要求的話,優先用Stream操做。
直接給結論:
關於並行模式的CPU消耗,各位在本地運行前面的性能測試代碼時,能夠打開本地的資源監視器,看看Stream串行與並行模式下的CPU使用率。你會發現,Stream串行與顯式迭代器循環在運行時,基本上只有一個core的使用率達到100%,而並行模式時,全部core的使用率都會達到100%。若是這時你的應用有其餘併發的,也比較消耗CPU的請求過來,你猜它會比平時慢呢,仍是慢呢,仍是慢呢?若是你的應用仍是個高併發的系統,那你可否保證對CPU產生大量消耗的並行操做只發生在併發低的時間段呢?(固然是假設的你高併發系統是有高併發峯值時間段的,峯值時間段之外不存在高併發場景。。。)
Stream 其實並非集合或集合的元素,它自己不是數據結構,不保存數據,它實際上是對集合的一種運算框架。它更像一個高級版本的迭代器 Iterator。但不一樣於Iterator只能顯式地一個一個遍歷元素,Stream 只要開發者給出操做意圖及其函數實現(即作什麼和怎麼作),好比 "過濾掉小於0的數字"、"給每一個字符串從左補足10位"等,Stream 就會隱式地在內部進行遍歷,並作出相應的數據轉換。
作什麼
就是你須要調用Stream的哪一個方法,而怎麼作
就是你須要給Stream的方法傳入什麼樣的函數,即,lambda表達式!
首先,Stream是管道流操做。從前面的Stream操做的代碼示例中咱們能夠看到,整個Stream操做就是一個管道流操做,開始和中間操做老是返回一個新的Stream對象,後面繼續對這個Stream對象進行操做,猶如接力,直到最後執行操做得到結果。其次,Stream就如同一個迭代器(Iterator)那樣,最後的Terminal對數據集的遍歷是單向的,不可往復的。數據只能遍歷一次,遍歷過一次後就結束了,不可逆轉,恰似黃河之水天上來,奔流到海不復回。
故名
Stream
。
Stream與之前的集合操做相比,不一樣的地方在於,之前的集合操做(包括Iterator)只能命令式的,串行的操做。而Stream具備以下特色:
這麼多好處,就問你爽不爽,漫卷詩書喜欲狂了沒?
從Java的並行編程API(或者說多線程編程)的角度來看,咱們能夠看到其在Java各個大版本中的發展壯大過程大體以下:
- Java1到Java4 中的 java.lang.Thread
- Java5開始提供,Java6繼續加強的 java.util.concurrent
- Java7引入的 Fork/Join 框架
- Java8新增的Stream並行模式
前面講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,這是不推薦的設計方式。由於做爲方法的提供者,你怎麼保證智商無下限的調用者必定會用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